import type { EncryptedPostgresUrl, PostgresUrlEncryptionRequest, } from './db-session-crypto'; import { WORKFLOW_EXECUTOR_TOKEN_TTL_SECONDS } from './runtime-constants'; // Workflow DB sessions must not expire before the workflow run/retry state they // pair with. Run/retry state lives 60 min (WORKFLOW_RUN_STATE_TTL_MS in the // coordinator), so these are aligned to 60 min. Raising the default WITHOUT // raising the max would leave the coordinator DO clamp / request validator // silently capping the session at 30 min, so both move together. export const DB_SESSION_DEFAULT_TTL_SECONDS = 60 * 60; export const DB_SESSION_MAX_TTL_SECONDS = 60 * 60; // A sandbox cannot mint new database authority after launch. Its preloaded // sessions therefore have to remain valid for the same bounded activity as // the executor token that unwraps them. Keep the shorter default/max above for // renewable control-plane sessions; they do not govern launch-time preloads. export const PRELOADED_RUNTIME_DB_SESSION_TTL_SECONDS = WORKFLOW_EXECUTOR_TOKEN_TTL_SECONDS; export const DB_SESSION_OPERATIONS = [ 'rows.read', 'rows.append', 'rows.upsert', 'rows.replace', ] as const; export type DbSessionOperation = (typeof DB_SESSION_OPERATIONS)[number]; export const RUNTIME_SHEET_ROWS_LOGICAL_TABLE = 'sheet_rows'; export const RUNTIME_WORK_RECEIPT_LOGICAL_TABLE = 'work_receipts'; export const RUNTIME_WORK_RECEIPT_TABLE_NAMESPACE = 'work_receipts'; export const RUNTIME_WORK_RECEIPT_POSTGRES_TABLE = '_deepline_step_receipts'; export const DB_LOGICAL_TABLES = [ RUNTIME_SHEET_ROWS_LOGICAL_TABLE, RUNTIME_WORK_RECEIPT_LOGICAL_TABLE, ] as const; export type DbLogicalTable = (typeof DB_LOGICAL_TABLES)[number]; export type DbSessionTarget = { orgId: string; tableNamespace: string; logicalTable: DbLogicalTable; }; export type DbSessionLimits = { maxRows?: number; maxBytes?: number; maxRequests?: number; }; export type CreateDbSessionRequest = { playName: string; runId?: string | null; target: Omit; operations: DbSessionOperation[]; limits?: DbSessionLimits; sheetContract?: unknown | null; ttlSeconds?: number; userEmail?: string | null; postgresUrlEncryption?: PostgresUrlEncryptionRequest | null; }; export type CreateDbSessionResponse = { sessionId: string; executionRole?: string; postgresUrl?: string; encryptedPostgresUrl?: EncryptedPostgresUrl; postgres?: { schema: string; sheetTable: string; summaryTable: string; columnSummaryTable: string; receiptTable?: string; }; expiresAt: string; playName: string; target: DbSessionTarget; operations: DbSessionOperation[]; limits: DbSessionLimits; }; export type PreloadedRuntimeDbSession = { tableNamespace: string; logicalTable: DbLogicalTable; operations: DbSessionOperation[]; limits?: DbSessionLimits; session: CreateDbSessionResponse; }; export type RowsWriteDisposition = 'completed' | 'accepted'; export type RowsWriteResponse = { disposition: RowsWriteDisposition; writtenRows?: number; jobId?: string; }; type SafeParseSuccess = { success: true; data: T }; type SafeParseFailure = { success: false; error: Error }; type SafeParseResult = SafeParseSuccess | SafeParseFailure; type RuntimeSchema = { parse(value: unknown): T; safeParse(value: unknown): SafeParseResult; }; function createSchema(parse: (value: unknown) => T): RuntimeSchema { return { parse, safeParse(value) { try { return { success: true, data: parse(value) }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error(String(error)), }; } }, }; } function isRecord(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value); } function parseString(value: unknown, path: string): string { if (typeof value !== 'string' || value.length === 0) { throw new Error(`${path} must be a non-empty string.`); } return value; } function parseOptionalString(value: unknown, path: string): string | undefined { if (value === undefined) return undefined; return parseString(value, path); } function parseLiteral( value: unknown, expected: T, path: string, ): T { if (value !== expected) { throw new Error(`${path} must be ${expected}.`); } return expected; } function parseNullableString( value: unknown, path: string, ): string | null | undefined { if (value === undefined) return undefined; if (value === null) return null; return parseString(value, path); } function parsePositiveInteger(value: unknown, path: string): number { if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) { throw new Error(`${path} must be a positive integer.`); } return value; } function parseNonnegativeInteger(value: unknown, path: string): number { if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) { throw new Error(`${path} must be a nonnegative integer.`); } return value; } function parseEnum( value: unknown, allowed: T, path: string, ): T[number] { if ( typeof value === 'string' && (allowed as readonly string[]).includes(value) ) { return value as T[number]; } throw new Error(`${path} must be one of: ${allowed.join(', ')}.`); } function parseArray( value: unknown, path: string, parseItem: (item: unknown, itemPath: string) => T, ): T[] { if (!Array.isArray(value) || value.length === 0) { throw new Error(`${path} must be a non-empty array.`); } return value.map((item, index) => parseItem(item, `${path}[${index}]`)); } function parseLimits(value: unknown, path: string): DbSessionLimits { if (!isRecord(value)) { throw new Error(`${path} must be an object.`); } return { ...(value.maxRows !== undefined ? { maxRows: parsePositiveInteger(value.maxRows, `${path}.maxRows`) } : {}), ...(value.maxBytes !== undefined ? { maxBytes: parsePositiveInteger(value.maxBytes, `${path}.maxBytes`) } : {}), ...(value.maxRequests !== undefined ? { maxRequests: parsePositiveInteger( value.maxRequests, `${path}.maxRequests`, ), } : {}), }; } function parseOperation(value: unknown, path: string): DbSessionOperation { return parseEnum(value, DB_SESSION_OPERATIONS, path); } function parseLogicalTable(value: unknown, path: string): DbLogicalTable { return parseEnum(value, DB_LOGICAL_TABLES, path); } function parseTarget(value: unknown, path: string): DbSessionTarget { if (!isRecord(value)) { throw new Error(`${path} must be an object.`); } return { orgId: parseString(value.orgId, `${path}.orgId`), tableNamespace: parseString(value.tableNamespace, `${path}.tableNamespace`), logicalTable: parseLogicalTable(value.logicalTable, `${path}.logicalTable`), }; } function parseRequestTarget( value: unknown, path: string, ): Omit { if (!isRecord(value)) { throw new Error(`${path} must be an object.`); } return { tableNamespace: parseString(value.tableNamespace, `${path}.tableNamespace`), logicalTable: parseLogicalTable(value.logicalTable, `${path}.logicalTable`), }; } function parsePostgresMetadata( value: unknown, path: string, ): CreateDbSessionResponse['postgres'] { if (value === undefined) return undefined; if (!isRecord(value)) { throw new Error(`${path} must be an object.`); } return { schema: parseString(value.schema, `${path}.schema`), sheetTable: parseString(value.sheetTable, `${path}.sheetTable`), summaryTable: parseString(value.summaryTable, `${path}.summaryTable`), columnSummaryTable: parseString( value.columnSummaryTable, `${path}.columnSummaryTable`, ), ...(value.receiptTable !== undefined ? { receiptTable: parseString(value.receiptTable, `${path}.receiptTable`), } : {}), }; } function parseEncryptedPostgresUrl( value: unknown, path: string, ): EncryptedPostgresUrl { if (!isRecord(value)) { throw new Error(`${path} must be an object.`); } const alg = parseString(value.alg, `${path}.alg`); if (alg === 'A256GCM') { return { alg, kid: parseLiteral( value.kid, 'deepline-runtime-db-session-url:v1', `${path}.kid`, ), iv: parseString(value.iv, `${path}.iv`), ciphertext: parseString(value.ciphertext, `${path}.ciphertext`), tag: parseString(value.tag, `${path}.tag`), }; } if (alg === 'RSA-OAEP-256+A256GCM') { return { alg, kid: parseLiteral( value.kid, 'deepline-runtime-db-session-url:v2', `${path}.kid`, ), wrappedKey: parseString(value.wrappedKey, `${path}.wrappedKey`), iv: parseString(value.iv, `${path}.iv`), ciphertext: parseString(value.ciphertext, `${path}.ciphertext`), tag: parseString(value.tag, `${path}.tag`), }; } throw new Error(`${path}.alg must be a supported encryption algorithm.`); } function parsePostgresUrlEncryptionRequest( value: unknown, path: string, ): PostgresUrlEncryptionRequest { if (!isRecord(value)) { throw new Error(`${path} must be an object.`); } const alg = parseLiteral(value.alg, 'RSA-OAEP-256+A256GCM', `${path}.alg`); const publicKeyJwkValue = value.publicKeyJwk; if (!isRecord(publicKeyJwkValue)) { throw new Error(`${path}.publicKeyJwk must be an object.`); } const publicKeyJwk = { ...publicKeyJwkValue } as unknown as JsonWebKey; if (publicKeyJwk.kty !== 'RSA') { throw new Error(`${path}.publicKeyJwk.kty must be RSA.`); } if ( typeof publicKeyJwk.n !== 'string' || typeof publicKeyJwk.e !== 'string' ) { throw new Error(`${path}.publicKeyJwk must include RSA n and e values.`); } return { alg, publicKeyJwk, }; } export const dbSessionOperationSchema = createSchema( (value) => parseOperation(value, 'operation'), ); export const dbLogicalTableSchema = createSchema((value) => parseLogicalTable(value, 'logicalTable'), ); export const dbSessionTargetSchema = createSchema((value) => parseTarget(value, 'target'), ); export const dbSessionLimitsSchema = createSchema((value) => parseLimits(value, 'limits'), ); export const createDbSessionRequestSchema = createSchema((value) => { if (!isRecord(value)) { throw new Error('Create DB session request must be an object.'); } const ttlSeconds = value.ttlSeconds === undefined ? undefined : parsePositiveInteger(value.ttlSeconds, 'ttlSeconds'); if (ttlSeconds !== undefined && ttlSeconds > DB_SESSION_MAX_TTL_SECONDS) { throw new Error(`ttlSeconds must be <= ${DB_SESSION_MAX_TTL_SECONDS}.`); } return { playName: parseString(value.playName, 'playName'), ...(value.runId !== undefined ? { runId: parseNullableString(value.runId, 'runId') } : {}), target: parseRequestTarget(value.target, 'target'), operations: parseArray(value.operations, 'operations', parseOperation), ...(value.limits !== undefined ? { limits: parseLimits(value.limits, 'limits') } : {}), ...(value.sheetContract !== undefined ? { sheetContract: value.sheetContract } : {}), ...(ttlSeconds !== undefined ? { ttlSeconds } : {}), ...(value.userEmail !== undefined ? { userEmail: parseNullableString(value.userEmail, 'userEmail') } : {}), ...(value.postgresUrlEncryption !== undefined ? { postgresUrlEncryption: value.postgresUrlEncryption === null ? null : parsePostgresUrlEncryptionRequest( value.postgresUrlEncryption, 'postgresUrlEncryption', ), } : {}), }; }); export const createDbSessionResponseSchema = createSchema((value) => { if (!isRecord(value)) { throw new Error('Create DB session response must be an object.'); } return { sessionId: parseString(value.sessionId, 'sessionId'), ...(value.executionRole !== undefined ? { executionRole: parseOptionalString( value.executionRole, 'executionRole', ), } : {}), ...(value.postgresUrl !== undefined ? { postgresUrl: parseOptionalString(value.postgresUrl, 'postgresUrl') } : {}), ...(value.encryptedPostgresUrl !== undefined ? { encryptedPostgresUrl: parseEncryptedPostgresUrl( value.encryptedPostgresUrl, 'encryptedPostgresUrl', ), } : {}), ...(value.postgres !== undefined ? { postgres: parsePostgresMetadata(value.postgres, 'postgres') } : {}), expiresAt: parseString(value.expiresAt, 'expiresAt'), playName: parseString(value.playName, 'playName'), target: parseTarget(value.target, 'target'), operations: parseArray(value.operations, 'operations', parseOperation), limits: value.limits === undefined ? {} : parseLimits(value.limits, 'limits'), }; }); export const rowsWriteResponseSchema = createSchema( (value) => { if (!isRecord(value)) { throw new Error('Rows write response must be an object.'); } return { disposition: parseEnum( value.disposition, ['completed', 'accepted'] as const, 'disposition', ), ...(value.writtenRows !== undefined ? { writtenRows: parseNonnegativeInteger( value.writtenRows, 'writtenRows', ), } : {}), ...(value.jobId !== undefined ? { jobId: parseOptionalString(value.jobId, 'jobId') } : {}), }; }, );