/** * Human-owned spreadsheet provisioning for `hikoutei setup`. * * The setup CLI creates the sync spreadsheet as the logged-in human account * (service accounts cannot own Drive/Workspace assets) using the memory-only * access token obtained by the human auth step, then grants the freshly * provisioned service account writer access through the Drive API and * verifies Drive metadata: the active human must be the owner and the * service account must be a writer. The runtime itself stays service-account * only — the human token is never persisted. * * The create request does NOT carry a client-supplied id (Drive * `files.generateIds` ids cannot create Google Workspace files): instead the * flow generates a local opaque creation marker (a UUID) and the create * request carries it as a private `appProperties` entry in the same atomic * request. A lost response or failed create is reconciled by querying Drive * for that exact marker; the flow never retries the create from the started * state. * * Production implementations use google-auth-library plus @googleapis/drive; * tests inject a fake `HumanSheetApiFactory`, so unit tests never touch the * network. Every SDK response is treated as untrusted input and validated by * runtime guards before promotion. */ /** OAuth scope required to create and write spreadsheets. */ export declare const SPREADSHEETS_SCOPE = "https://www.googleapis.com/auth/spreadsheets"; /** Template for the human-facing spreadsheet edit URL. */ export declare const SPREADSHEET_EDIT_URL_TEMPLATE = "https://docs.google.com/spreadsheets/d//edit"; /** Drive MIME type of a Google Sheets spreadsheet. */ export declare const SPREADSHEET_MIME_TYPE: "application/vnd.google-apps.spreadsheet"; /** Private Drive `appProperties` key carrying the setup creation marker. */ export declare const HIKOUTEI_SETUP_MARKER_KEY: "hikouteiSetupMarker"; /** * Drive fields requested from `drive.files.create` and the marker query. * * `appProperties` is requested so the create response can be validated * against the expected creation marker before the result is promoted; a * response that omits or contradicts the marker is a protocol error and the * outcome is reconciled by marker instead. */ export declare const DRIVE_FILE_CREATE_FIELDS: "id,name,mimeType,appProperties"; /** * Drive fields requested from the creation-marker `drive.files.list` * pagination: every page carries `nextPageToken` so ALL pages are walked * before the flow decides 0/1/many over the complete result, and * `incompleteSearch` so a page the server could not fully search is * refused (a `true` value means the aggregate may be missing files, so the * flow must fail closed instead of trusting a partial lookup). */ export declare const MARKER_FILE_LIST_FIELDS: "files(id,name,mimeType,appProperties),nextPageToken,incompleteSearch"; /** Page size for the creation-marker `drive.files.list` pagination. */ export declare const MARKER_FILE_LIST_PAGE_SIZE = 10; /** Result of creating a spreadsheet as the human owner. */ export interface SpreadsheetCreateResult { readonly spreadsheetId: string; } /** One validated file returned by the creation-marker query. */ export interface MarkerFileInfo { readonly spreadsheetId: string; readonly name: string; readonly mimeType: string; /** Private appProperties; validated as a record when present. */ readonly appProperties: Readonly>; } /** Result of ensuring the service account can write the spreadsheet. */ export interface ShareOutcome { readonly writerRole: "reused" | "upgraded" | "created"; } /** Human-token sheet operations used by the setup flow. */ export interface HumanSheetApi { /** * Creates a spreadsheet owned by the human account with the expected * title and the creation marker as a private `appProperties` entry in the * same atomic request. No client-supplied id is used. Throws on API or * validation failure; the flow reconciles an unknown outcome by marker. */ createSpreadsheet(request: { readonly title: string; readonly marker: string; }): Promise; /** * Lists Drive files carrying the exact creation marker across ALL pages. * * Returns the validated matches aggregated from every `drive.files.list` * page (the flow enforces 0/1 over the complete result). Throws when the * lookup itself fails, so the flow treats the outcome as unknown and * never creates a second spreadsheet. */ findSpreadsheetByMarker(marker: string): Promise; /** * Ensures the service account is a writer on the spreadsheet (reusing an * existing writer/owner role, upgrading a lower role, or creating the * permission without a notification email) and verifies that the active * human is the owner and the service account can write. */ ensureSaWriter(request: { spreadsheetId: string; saEmail: string; ownerEmail: string; }): Promise; } /** * Builds a `HumanSheetApi` for one run; the access token lives only in the * returned client for the duration of the run. */ export type HumanSheetApiFactory = (accessToken: string) => HumanSheetApi; /** Production factory: creates the human-token Sheets/Drive clients on demand. */ export declare function createHumanSheetApiFactory(): HumanSheetApiFactory; /** * Production human-token sheet API. * * Builds an OAuth2Client carrying the access token, then wraps the Drive * clients (create/get/permissions). The token is held in memory only; * errors are rethrown and the flow maps them to `sheet_create_failed`, * `sheet_create_uncertain`, or `sheet_share_failed` with sanitized reasons. */ export declare function createHumanSheetApi(accessToken: string): HumanSheetApi; /** Builds the public edit URL for a spreadsheet id. */ export declare function spreadsheetEditUrl(spreadsheetId: string): string; /** * Builds the atomic `drive.files.create` request for a spreadsheet. * * The request carries the creation marker as a private `appProperties` * entry in the same atomic request (no client-supplied id is ever used) and * asks for `appProperties` in the response fields so the result can be * validated against the marker before promotion. */ export declare function buildDriveFileCreateRequest(title: string, marker: string): { readonly requestBody: { readonly name: string; readonly mimeType: typeof SPREADSHEET_MIME_TYPE; readonly appProperties: { readonly [HIKOUTEI_SETUP_MARKER_KEY]: string; }; }; readonly fields: typeof DRIVE_FILE_CREATE_FIELDS; }; /** * Validates the raw `drive.files.create` payload. * * The created file's id must be a non-empty URL-safe Drive id, the mime * type must be `application/vnd.google-apps.spreadsheet`, the returned name * must match the requested title exactly, and the returned `appProperties` * must be a record whose `hikouteiSetupMarker` value equals the expected * marker exactly; anything else is a protocol violation and the outcome is * treated as unknown (the flow reconciles by marker, never by retrying the * create and never by promoting the response). The id is validated before * it can reach a URL, the `.env` file, a summary, or a command label. The * edit URL is derived from the id deterministically and never stored. */ export declare function extractDriveFileCreateResult(data: unknown, expectedTitle: string, expectedMarker: string): SpreadsheetCreateResult; /** One validated `drive.files.list` page for a creation-marker query. */ export interface MarkerFileListPage { readonly files: readonly MarkerFileInfo[]; /** * Opaque continuation token, validated as a non-empty string when the * untrusted payload carries one; `undefined` means the last page. */ readonly nextPageToken: string | undefined; /** * Drive's completeness flag: the page is only promotable when the server * states the search was complete (`false`). A missing, `true`, or * malformed value fails closed — an incomplete search may hide files the * marker reconciliation depends on, so the flow must never decide 0/1 * over a partial result. */ readonly incompleteSearch: false; } /** * Validates one raw `drive.files.list` page payload for a creation-marker * query. * * Untrusted SDK data: every file entry must carry a non-empty URL-safe * `id`, non-empty `name` and `mimeType` strings, and `appProperties` (when * present) must be a record. The optional `nextPageToken` must be a * non-empty string when present; malformed payloads throw through the * sanitized structured path and never leak payload contents. Whether the * marker itself matches is decided by the flow, which also enforces * exactly one result before promotion. */ export declare function extractMarkerFileListPage(data: unknown): MarkerFileListPage; /** * Validates the raw `drive.files.list` payload for a creation-marker query * (single page). * * Untrusted SDK data: every file entry must carry a non-empty URL-safe * `id`, non-empty `name` and `mimeType` strings, and `appProperties` (when * present) must be a record. The paginating marker lookup uses * `extractMarkerFileListPage`; this wrapper is kept for callers that never * paginate. */ export declare function extractMarkerFileList(data: unknown): readonly MarkerFileInfo[]; /** * A validated Drive permission entry. * * `emailAddress` is required only for identity-carrying types (`user`, * `group`): Google omits it for `anyone`, `anyoneWithLink`, and `domain` * permissions, so those entries promote without it and are safely ignored * by writer planning. */ export type DrivePermission = { readonly id: string; readonly role: string; readonly type: "user" | "group"; readonly emailAddress: string; } | { readonly id: string; readonly role: string; readonly type: string; readonly emailAddress: undefined; }; /** The id of an existing Drive permission entry. */ export type DrivePermissionId = string; /** * Decides the idempotent writer-ensure action for the service account. * * Pure decision helper so tests can cover reuse/upgrade/create without the * network: an existing user permission with writer/owner role is reused, a * lower role is upgraded, and a missing permission is created. Entries of * other types (anyone/domain/group) are ignored safely. */ export type SaWriterPlan = { readonly action: "reuse"; } | { readonly action: "upgrade"; readonly permissionId: DrivePermissionId; } | { readonly action: "create"; }; export declare function planSaWriterAction(permissions: readonly DrivePermission[], saEmail: string): SaWriterPlan; /** * Validates the raw `permissions.list` payload (single page). * * Untrusted SDK data: every entry must carry a non-empty URL-safe id, a * non-empty role and type; entries of type `user`/`group` must additionally * carry a non-empty emailAddress, while other types (where Google omits the * identity) promote without it. The paginating writer ensure uses * `extractPermissionListPage`; this wrapper is kept for callers that never * paginate (the ownership-check file metadata). */ export declare function extractPermissionList(data: unknown): DrivePermission[]; /** Validated Drive file metadata used for the ownership check. */ export interface DriveFileMetadata { readonly ownerEmails: readonly string[]; readonly permissions: readonly DrivePermission[]; } /** * Validates the raw `drive.files.get` payload for the ownership check. * * Untrusted SDK data: the response must carry the expected URL-safe file * id (a mismatched or malformed id means the metadata belongs to a * different file and the check must fail), `owners` must be an array of * objects with a non-empty `emailAddress`, and `permissions` must validate * like a `permissions.list` payload. */ export declare function extractDriveFileMetadata(data: unknown, expectedSpreadsheetId: string): DriveFileMetadata; /** * Hard bound on the number of `permissions.list` pages the writer ensure * will follow before failing closed. * * Spreadsheet permission lists are tiny in practice; the bound exists so a * hostile or broken API can never make the ensure loop forever with an * ever-changing sequence of distinct continuation tokens (the seen-token * guard already stops repeated tokens). */ export declare const MAX_PERMISSION_LIST_PAGES = 50; /** * Hard bound on the number of `drive.files.list` pages the creation-marker * lookup will follow before failing closed, consistent with the permission * pagination bound. The bound exists so a hostile or broken API can never * make the lookup loop forever with an ever-changing sequence of distinct * continuation tokens (the seen-token guard already stops repeated tokens). */ export declare const MAX_MARKER_FILE_LIST_PAGES = 50; /** One validated `permissions.list` page. */ export interface PermissionListPage { readonly permissions: readonly DrivePermission[]; /** * Opaque continuation token, validated as a non-empty string when the * untrusted payload carries one; `undefined` means the last page. */ readonly nextPageToken: string | undefined; } /** * Validates one raw `permissions.list` page payload. * * Untrusted SDK data: every entry must carry a non-empty URL-safe id, a * non-empty role and type; entries of type `user`/`group` must additionally * carry a non-empty emailAddress, while other types (where Google omits the * identity) promote without it. The optional `nextPageToken` must be a * non-empty string when present. Malformed payloads throw through the * sanitized structured path and never leak payload contents. */ export declare function extractPermissionListPage(data: unknown): PermissionListPage; /** * The injectable Drive permission boundary used by the writer ensure. * * Production wraps the @googleapis/drive permissions resource; tests inject * a fake so pagination, cycle guards, and request fields are exercised * without the network. Every response `data` is treated as untrusted input * and validated by runtime guards before promotion. */ export interface DrivePermissionApi { list(request: { fileId: string; fields: string; pageToken?: string; }): Promise<{ readonly data: unknown; }>; update(request: { fileId: string; permissionId: string; requestBody: { readonly role: "writer"; }; }): Promise; create(request: { fileId: string; requestBody: { readonly type: "user"; readonly role: "writer"; readonly emailAddress: string; }; sendNotificationEmail: false; }): Promise; } /** * Collects every permission of a file across ALL `permissions.list` pages. * * Follows `nextPageToken` until it is absent, requesting the fields that * include the token. Fails closed on a malformed page payload or * continuation token, on a repeated token (cycle guard — a non-progress * loop can never spin), and on the hard page bound; API errors propagate * through the sanitized structured path. The file id is validated before * it reaches the API. */ export declare function listAllDrivePermissions(api: Pick, spreadsheetId: string): Promise; /** * The injectable Drive file-list boundary used by the creation-marker * lookup. * * Production wraps the @googleapis/drive files resource; tests inject a * fake so pagination, cycle guards, and request fields are exercised * without the network. Every response `data` is treated as untrusted input * and validated by runtime guards before promotion. */ export interface DriveFileListApi { list(request: { q: string; spaces: string; fields: string; pageSize: number; pageToken?: string; }): Promise<{ readonly data: unknown; }>; } /** * Collects every Drive file carrying the exact creation marker across ALL * `drive.files.list` pages. * * Follows `nextPageToken` until it is absent, requesting the fields that * include the token. Fails closed on a malformed page payload or * continuation token, on a repeated token (cycle guard — a non-progress * loop can never spin), and on the hard page bound; API errors propagate * through the sanitized structured path. The marker is validated before it * reaches the API. All pages are aggregated before the result is returned, * so the flow's 0/1/many reconciliation sees the COMPLETE result: a * duplicate exact marker hidden after a short first page can never be * mistaken for a clean single match. */ export declare function listAllMarkerFiles(api: Pick, marker: string): Promise; /** Grants or reuses the service-account writer permission; returns the outcome. */ export declare function ensureSaWriterPermission(api: DrivePermissionApi, spreadsheetId: string, saEmail: string): Promise; //# sourceMappingURL=sheetsFactory.d.ts.map