/** * Env-driven sync auto-start bridge for the public `createTypedSheets()`. * * This internal module owns the mapping from environment variables to the * sync service bootstrap: spreadsheet URL parsing, service-account credentials * file validation, polling interval parsing, request-start pacing override * parsing, projection auto-generation from entity descriptors, and fail-closed * startup failure classification. It is re-exported to applications ONLY * through the lazy public wrapper `src/api/syncRuntime.ts`, so importing the * package root never loads the sync module graph; the public factory calls it * only when `HIKOUTEI_SYNC_SPREADSHEET_URL` is set, so local-only users never * load the sync module graph either. * * The env reader, transport, and diagnostic sink are injectable so tests can * exercise every startup branch with a stub transport, a fake env, and a * captured log without credentials or network access. When no transport is * injected, the real Application Default Credentials (ADC) transport is built * by the sync service, exactly like the production path. * * Failures are classified into stable `HikouteiError` codes, logged through * the diagnostic sink (console by default), and thrown fail-closed: a startup * failure never leaves a half-open runtime behind. */ import { type HikouteiDescriptorFile, type HikouteiEntity } from "../../api/entity.js"; import { type Hikoutei, type HikouteiProviderOptions } from "../../api/hikouteiCore.js"; import { HikouteiError } from "../../api/errors.js"; import type { GoogleSheetsApiTransport } from "../../../contracts/sheets/googleSheetsApi.js"; import { type ExistingSheetAdoptionRunReport } from "./adopt/existingSheetAdoption.js"; import type { InternalSyncProjectionConfig } from "./contracts.js"; import { type InternalSyncService } from "./SyncServiceBootstrap.js"; /** Env keys consumed by the sync auto-start bridge. */ export declare const SYNC_ENV_KEYS: { /** Spreadsheet URL; absent means sync stays disabled (local-only). */ readonly SPREADSHEET_URL: "HIKOUTEI_SYNC_SPREADSHEET_URL"; /** Service-account key file path (JSON, ADC standard). */ readonly CREDENTIALS_FILE: "GOOGLE_APPLICATION_CREDENTIALS"; /** Optional User_Input polling cadence in ms; defaults to 60 seconds. */ readonly POLLING_INTERVAL_MS: "HIKOUTEI_SYNC_POLLING_INTERVAL_MS"; /** Optional metadata safety full-scan cadence in ms; defaults to 60 seconds. */ readonly FULL_SCAN_INTERVAL_MS: "HIKOUTEI_SYNC_FULL_SCAN_INTERVAL_MS"; /** * Optional request-start pacing in ms for the direct provider's * independent read and write request-start limiters; absent uses the safe * default (800 ms). Internal only — never part of the root public API. */ readonly RATE_LIMIT_INTERVAL_MS: "HIKOUTEI_SYNC_RATE_LIMIT_INTERVAL_MS"; /** * Optional service-account credential POOL: a comma-separated list of * ADC-format key-file paths, each a distinct Google quota principal. When * set, per-identity pacing and round-robin signing multiply the effective * per-user quota by the pool size. Absent (or blank) keeps the single * `GOOGLE_APPLICATION_CREDENTIALS` credential exactly as before. An * explicit pool overrides `providerOptions.serviceAccountKeyFiles`, like * the pacing env override does for `rateLimitIntervalMs`; conversely, a * NONEMPTY `providerOptions.serviceAccountKeyFiles` with this env key * unset also forms the effective pool (ADC is then not required). The pool * is RESOLVED AND VALIDATED on every startup path (it replaces the * mandatory `GOOGLE_APPLICATION_CREDENTIALS` gate); the file list is * PLUMBED into the provider only on the real-provider path — an injected * test transport never uses the pool identities. */ readonly CREDENTIAL_POOL_FILES: "HIKOUTEI_SYNC_CREDENTIALS"; /** * Optional worker pass concurrency: the maximum number of route-disjoint * dispatch units a single worker pass runs concurrently. Absent or blank * means the safe default (1 = the historical fully-sequential pass). * Values must be plain decimal integers between 1 and 8; same-route * serialization is enforced structurally regardless of the value. Real- * provider path only — an injected test transport never consults it. */ readonly MAX_CONCURRENT_UNITS: "HIKOUTEI_SYNC_MAX_CONCURRENT_UNITS"; }; /** * Default cadences applied when the interval env vars are absent; the single * source of these values is `./cadence.js`. */ export { SYNC_FULL_SCAN_INTERVAL_MS as DEFAULT_SYNC_FULL_SCAN_INTERVAL_MS, SYNC_POLLING_INTERVAL_MS as DEFAULT_SYNC_POLLING_INTERVAL_MS, } from "./cadence.js"; /** Lower bound for the sync request-start pacing env override (2 seconds). */ export declare const MIN_SYNC_RATE_LIMIT_INTERVAL_MS = 2000; /** * Upper bound for the sync request-start pacing env override. * * A worst-case effect dispatch performs THREE sequential paced transport * calls (two preflight/postcondition reads plus one batch write). The effect * lease must still cover the whole sequence with the 30-second provider * headroom, and a dispatch can wait up to one FULL interval for its first * slot because the request's own read or write class limiter may hold a prior * reservation — admission * is BOUNDED to one interval, so a request whose slot lies further out is * refused before any SDK call (delivery-uncertain, requeued durably) * instead of waiting past the lease. With the DEFAULT lease, write timeout, * and read timeout the interval must satisfy * `120s > 60s + I + 2 * max(10s, I) + 30s`, i.e. `I < 10s`. Below * the 10 s read timeout the two read slots cost 2 x 10 s and the interval * adds once, so the bound is `I < 120 - 60 - 2x10 - 30 = 10 s`; at or * above the read timeout the interval dominates (`3I < 30s` has no * solution at or above 10 s), so the below-read-slot branch is binding. * The ceiling below is derived from those defaults (10,000 - 1 = 9,999 ms) * and the service-level lease-headroom validation applies the same math to * the ACTIVE lease and timeouts, so an override that could let pacing push * a dispatch past the lease is rejected with a stable startup failure * instead of risking lease expiry and duplicate remote delivery. */ export declare const MAX_SYNC_RATE_LIMIT_INTERVAL_MS: number; /** Diagnostic log levels emitted by the auto-start bridge. */ export type SyncDiagnosticLevel = "info" | "error"; /** Diagnostic sink; defaults to console. Receives only stable class/code summaries, never full failure messages. */ export type SyncDiagnostic = (level: SyncDiagnosticLevel, message: string) => void; /** One entity's existing-sheet adoption request (public adopt API, design D1/D4). */ export interface AdoptEntitySpec { /** The existing tab that becomes this entity's User_Input route (D1). */ readonly tabName: string; /** * Sheet header that carries the business key. `"auto"` (or absent) prefers * the column matching the entity's primary-key property and falls back to * appending a generated PK column (D4). MVP: an alias whose header differs * from the PK property name is blocked with IDENTITY_ALIAS_UNSUPPORTED. */ readonly identityFrom?: string | "auto"; /** * Tab name for the freshly provisioned System_State projection. Defaults to * `_System`. */ readonly systemStateTabName?: string; /** * Tab name for the freshly provisioned Sync_Conflicts projection. Defaults * to `_Conflicts`. */ readonly syncConflictsTabName?: string; /** * §12: explicit header → property bindings for sheets whose headers differ * from the property names (adoption-only). Mapped headers take precedence * over name matching; a mapped PK header absorbs the identityFrom alias. */ readonly columnMap?: Readonly>; } /** Public existing-sheet adoption spec (design `design/existing-sheet-adoption-design.md` §4.1). */ export interface AdoptSpec { readonly mode: "dry-run" | "adopt"; readonly entities: Readonly>; } /** Internal auto-start options; none are part of the root application contract. */ export interface SyncAutoStartOptions { readonly dbName: string; readonly entities: readonly HikouteiEntity[]; /** * File-form entity descriptors, built through the same * `defineTypedSheetsEntity` builder and appended after `entities` (mirrors * the public `CreateTypedSheetsOptions.descriptors` contract for the * internal bridge, including the stub-transport test path). */ readonly descriptors?: readonly HikouteiDescriptorFile[]; /** Injectable env reader; defaults to an empty env (sync disabled). */ readonly env?: Readonly>; /** Stub transport for credential-free tests; omitted builds the real ADC client. */ readonly transport?: GoogleSheetsApiTransport; /** Injectable diagnostic sink for tests; defaults to console. */ readonly onDiagnostic?: SyncDiagnostic; /** * Existing-sheet adoption (MVP, direct mode only). In `dry-run` mode the * result is `{ kind: "adopt-dry-run", report }` — the spreadsheet was not * mutated and no service started. In `adopt` mode the adopted tab becomes * the entity's User_Input route, every existing row is bound + seeded * (fail-closed, D5), and the normal sync service starts. */ readonly adopt?: AdoptSpec; /** * Public provider subset forwarded verbatim into the sync-service * `googleSheetsApi` settings (real-transport path only). `onRequest` here * chains AFTER the engine's internal request-telemetry sink (see * `remoteProvider.ts`); a per-request env pacing override still wins over * `providerOptions.rateLimitIntervalMs`. */ readonly providerOptions?: HikouteiProviderOptions; } /** Local-only result: no sync service was started (env absent or blank). */ export interface LocalSyncRuntimeResult { readonly kind: "local"; readonly hikoutei: Hikoutei; } /** Sync result: the running internal service plus its public runtime handle. */ export interface RunningSyncServiceResult { readonly kind: "sync"; /** Same object as `service.hikoutei`; convenient for the public factory. */ readonly hikoutei: Hikoutei; /** Internal service handle (supervisors, storage) for tests and tooling. */ readonly service: InternalSyncService; } /** * Adoption dry-run result: the read-only report was produced and the * spreadsheet was NOT mutated; no sync service was started. */ export interface AdoptDryRunResult { readonly kind: "adopt-dry-run"; readonly report: ExistingSheetAdoptionRunReport; } export type TypedSheetsWithSyncResult = LocalSyncRuntimeResult | RunningSyncServiceResult | AdoptDryRunResult; /** * Extracts the spreadsheet ID from a Google Sheets URL. * * Supports `https://docs.google.com/spreadsheets/d//edit` (with `#gid=`, * `?usp=sharing`, `/view`, trailing slashes, and scheme-less forms) and a * top-level `/d/` right after the host. The ID must be its own path * segment, so a Docs URL such as `/document/d/` is rejected instead of * being misread as a spreadsheet. Returns `undefined` when no ID segment can * be extracted. The ID itself is the only part ever echoed in diagnostics; * full URLs are never logged. */ export declare function parseSpreadsheetIdFromUrl(url: string): string | undefined; /** * Opens the runtime for the declared entities, starting the internal sync * service when the env is configured. * * When `HIKOUTEI_SYNC_SPREADSHEET_URL` is absent the function logs the * "sync disabled" info diagnostic and returns the plain local-only runtime — * the exact path `createTypedSheets()` uses without sync. When present it * validates the URL and the credentials file, auto-generates the projection * routes from the entity descriptors, and fails closed with a classified * `HikouteiError` on any startup problem. */ export declare function createTypedSheetsWithSync(options: SyncAutoStartOptions): Promise; /** * Validates the service-account credentials file and returns its client email. * * Env parsing and stable `HikouteiError` codes stay here; file * loading/validation is owned by the shared `@hikoutei/google-auth` loader * (Batch A) and only mapped to codes below. The file is validated before * any remote contact so a misconfigured deployment fails fast with a * precise message instead of an ADC stack trace. */ export declare function validateSyncCredentialsFile(path: string | undefined): Promise<{ readonly clientEmail: string; }>; /** * Parses and validates `HIKOUTEI_SYNC_CREDENTIALS`: a comma-separated list * of service-account key-file paths forming the credential pool. * * Every entry is validated through the same fail-closed file check as the * primary ADC credentials (readable JSON with the required service-account * fields) BEFORE any remote contact, so a misconfigured pool fails fast with * a stable `HikouteiError`. Returns `undefined` only when the key is absent * or genuinely blank (empty/whitespace-only — the single-credential path, * zero behavior change). A NON-blank value with an empty segment (e.g. * `",,"` or `"a.json,,b.json"`) is rejected instead of being silently * filtered into "unset": a typo'd pool must never degrade to the primary * credential without a word. Diagnostics carry paths only — never client * emails or key material. */ export declare function resolveSyncCredentialPoolEnv(env: Readonly>): Promise; /** * Auto-generates the internal projection routes from public entity descriptors. * * Every entity owns three tabs named `_System`, `_Input`, * and `_Conflicts`. The System_State range covers every property * plus the `__typed_sheets_deleted` tombstone, the User_Input range covers the * user-owned fields (all properties) plus the internal `__hikoutei_row_id` * row-anchor system column, and Sync_Conflicts uses the fixed 15-header * `A:O` audit range. All properties are user-owned so polling can accept * human edits on any field. */ export declare function buildSyncProjections(entities: readonly HikouteiEntity[], spreadsheetId: string): InternalSyncProjectionConfig; /** * Classifies a sync service startup failure into a stable Hikoutei error. * * Transport rejections with a proven HTTP status map to authentication, * access-denied (using the validated client_email), and not-found codes; * remote schema/provisioning contract failures map to provisioning; anything * else (timeout, network, invalid options) maps to the generic startup code. */ export declare function classifySyncStartupFailure(error: unknown, spreadsheetId: string, clientEmail: string): HikouteiError; /** * Resolves the sync request-start pacing env override, or undefined. * * `HIKOUTEI_SYNC_RATE_LIMIT_INTERVAL_MS` is the internal override for the * direct provider's independent read and write request-start limiters. Absent or * blank means the provider's safe default (800 ms) applies; a present * value must be a plain decimal integer between 2,000 ms and * `MAX_SYNC_RATE_LIMIT_INTERVAL_MS` (~10 s). The ceiling is the largest * interval whose worst-case three-request paced dispatch still finishes * inside the default effect lease (120 s) with the default write timeout * (60 s), read timeout (10 s), and provider headroom (30 s): a dispatch * can wait up to one full interval for its first slot because the * request's own class limiter may hold a prior reservation, so the strict * default-safe bound * is `120s > 60s + I + 2 * max(10s, I) + 30s` and a larger interval could * let pacing push the dispatch past the lease and risk duplicate remote * delivery. Values outside those bounds (including hex, * exponent, signed, padded, fractional, and non-numeric forms) fail closed * with the stable startup code so a mistyped override can never silently * drop pacing to a burst or outlive the effect lease. The key stays internal * and is never part of the root public API. */ export declare function resolveSyncRateLimitIntervalMs(env: Readonly>): number | undefined; /** Upper bound for the worker concurrency env override (sanity, not quota). */ export declare const MAX_SYNC_CONCURRENT_UNITS = 8; /** * `HIKOUTEI_SYNC_MAX_CONCURRENT_UNITS` is the internal override for the * worker pass's concurrent route-disjoint dispatch units. Absent or blank * means the safe default (1 = the historical fully-sequential pass). A * present value must be a plain decimal integer between 1 and 8; values * outside those bounds fail closed with the stable startup code so a * mistyped override can never silently raise concurrency. The key stays * internal and is never part of the root public API. */ export declare function resolveSyncMaxConcurrentUnits(env: Readonly>): number | undefined; /** * Stable redacted startup summary for diagnostic sinks: class and code * only, each checked against the shared stable allowlists. * * `failure.name` and `failure.code` are runtime strings that could carry * secret-like text (paths, emails, tokens) if an unexpected error shape * ever reached the sink path, so unknown or malformed values collapse to * the fixed `unknown` class/code. The full public message is carried by * the thrown HikouteiError alone and never reaches any sink. */ export declare function stableStartupDiagnostic(failure: HikouteiError): string; //# sourceMappingURL=syncAutoStart.d.ts.map