@rebasepro/server 0.25.0-canary.ge06257e → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{GCSStorageController-CAKa_EFW.js → GCSStorageController-C7ARrDAp.js} +3 -3
  2. package/dist/{GCSStorageController-CAKa_EFW.js.map → GCSStorageController-C7ARrDAp.js.map} +1 -1
  3. package/dist/{S3StorageController-Kt-LJpf3.js → S3StorageController-qevJ0pVE.js} +3 -3
  4. package/dist/{S3StorageController-Kt-LJpf3.js.map → S3StorageController-qevJ0pVE.js.map} +1 -1
  5. package/dist/api/errors.d.ts +6 -10
  6. package/dist/{auth-xk37vrHO.js → auth-CteGWxsL.js} +20 -20
  7. package/dist/{auth-xk37vrHO.js.map → auth-CteGWxsL.js.map} +1 -1
  8. package/dist/{backup-CpaJWXSc.js → backup-Dq0lyJ_D.js} +4 -4
  9. package/dist/{backup-CpaJWXSc.js.map → backup-Dq0lyJ_D.js.map} +1 -1
  10. package/dist/boot/boot.d.ts +10 -0
  11. package/dist/{cron-routes-CPIhdasM.js → cron-routes-Dhf82HzT.js} +3 -3
  12. package/dist/{cron-routes-CPIhdasM.js.map → cron-routes-Dhf82HzT.js.map} +1 -1
  13. package/dist/{errors-CHSJYpoy.js → errors-_ytoGi8A.js} +11 -18
  14. package/dist/errors-_ytoGi8A.js.map +1 -0
  15. package/dist/{function-routes-C0emkHtX.js → function-routes-DNV4RUF0.js} +2 -2
  16. package/dist/{function-routes-C0emkHtX.js.map → function-routes-DNV4RUF0.js.map} +1 -1
  17. package/dist/functions/index.js.map +1 -1
  18. package/dist/index.es.js +128 -41
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/{logs-routes-BM2cDory.js → logs-routes-CelTiS7C.js} +2 -2
  21. package/dist/{logs-routes-BM2cDory.js.map → logs-routes-CelTiS7C.js.map} +1 -1
  22. package/dist/{openapi-generator-IKwDAQGO.js → openapi-generator-BamY6-ga.js} +2 -2
  23. package/dist/{openapi-generator-IKwDAQGO.js.map → openapi-generator-BamY6-ga.js.map} +1 -1
  24. package/dist/{query-parser-BnhugNwZ.js → query-parser-CGohjyWI.js} +3 -3
  25. package/dist/{query-parser-BnhugNwZ.js.map → query-parser-CGohjyWI.js.map} +1 -1
  26. package/dist/{request-timeout-LiGpCCP1.js → request-timeout-CRny5KD0.js} +2 -2
  27. package/dist/{request-timeout-LiGpCCP1.js.map → request-timeout-CRny5KD0.js.map} +1 -1
  28. package/dist/{schema-editor-routes-zjVKreVQ.js → schema-editor-routes-CnNGCayU.js} +2 -2
  29. package/dist/{schema-editor-routes-zjVKreVQ.js.map → schema-editor-routes-CnNGCayU.js.map} +1 -1
  30. package/dist/{soft-delete-params-uHxJoETb.js → soft-delete-params-BAgpmBvp.js} +2 -2
  31. package/dist/{soft-delete-params-uHxJoETb.js.map → soft-delete-params-BAgpmBvp.js.map} +1 -1
  32. package/dist/{sql-export-routes-D86u7N9i.js → sql-export-routes-BgTJ7K3S.js} +2 -2
  33. package/dist/{sql-export-routes-D86u7N9i.js.map → sql-export-routes-BgTJ7K3S.js.map} +1 -1
  34. package/dist/{upload-validation-pXcDy_EQ.js → upload-validation-I9cCUBIW.js} +2 -2
  35. package/dist/{upload-validation-pXcDy_EQ.js.map → upload-validation-I9cCUBIW.js.map} +1 -1
  36. package/package.json +5 -5
  37. package/dist/errors-CHSJYpoy.js.map +0 -1
@@ -6,10 +6,10 @@ import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
6
  import "./src-D5Y3N-S4.js";
7
7
  import { a as parseEnvBoolean, r as isAlreadyExecutingSkip } from "./cron-scheduler-D1DT0nw5.js";
8
8
  import { r as logger } from "./logger-Bt32sHFw.js";
9
- import { i as errorHandler, t as ApiError } from "./errors-CHSJYpoy.js";
9
+ import { i as errorHandler, t as ApiError } from "./errors-_ytoGi8A.js";
10
10
  import { m as jwtOf } from "./jwt-YhciR93H.js";
11
- import { t as S3StorageController } from "./S3StorageController-Kt-LJpf3.js";
12
- import { t as GCSStorageController } from "./GCSStorageController-CAKa_EFW.js";
11
+ import { t as S3StorageController } from "./S3StorageController-qevJ0pVE.js";
12
+ import { t as GCSStorageController } from "./GCSStorageController-C7ARrDAp.js";
13
13
  import { Hono } from "hono";
14
14
  import fs from "fs";
15
15
  import path from "path";
@@ -582,4 +582,4 @@ var backup_exports = /* @__PURE__ */ __exportAll({
582
582
  //#endregion
583
583
  export { listBackupObjects as a, readBackupBytes as c, backupStorageFromEnv as i, createBackupLinkRoutes as n, parseBackupDestination as o, createBackupRoutes as r, parseBackupTimestamp as s, backup_exports as t };
584
584
 
585
- //# sourceMappingURL=backup-CpaJWXSc.js.map
585
+ //# sourceMappingURL=backup-Dq0lyJ_D.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"backup-CpaJWXSc.js","names":[],"sources":["../src/backup/backup-common.ts","../src/backup/backup-storage.ts","../src/backup/backup-routes.ts","../src/backup/backup-schedule.ts","../src/backup/index.ts"],"sourcesContent":["/**\n * Storage-generic backup helpers used by the admin backup routes.\n *\n * These live in server (which owns {@link StorageController}) so the\n * admin API can list backups without depending on the Postgres driver —\n * `server` must not import from `server-postgres`. The `pg_dump`-side\n * copies of the tiny pure parsers live in `@rebasepro/server-postgres`;\n * keep the two in sync if the naming scheme ever changes.\n */\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { BackupInfo, BackupDestinationKind } from \"@rebasepro/types\";\nimport type { StorageController } from \"../storage\";\nimport { logger } from \"../utils/logger\";\n\nexport type BackupDestination =\n | { kind: \"local\"; path: string }\n | { kind: \"s3\"; bucket: string; prefix: string }\n | { kind: \"gcs\"; bucket: string; prefix: string };\n\n/** Parse a destination string (`s3://…`, `gs://…`, or a local path). */\nexport function parseBackupDestination(out: string): BackupDestination {\n const s3 = out.match(/^s3:\\/\\/([^/]+)\\/?(.*)$/);\n if (s3) return { kind: \"s3\", bucket: s3[1], prefix: s3[2].replace(/\\/+$/, \"\") };\n const gcs = out.match(/^gs:\\/\\/([^/]+)\\/?(.*)$/);\n if (gcs) return { kind: \"gcs\", bucket: gcs[1], prefix: gcs[2].replace(/\\/+$/, \"\") };\n return { kind: \"local\", path: out };\n}\n\n/**\n * Recover the UTC creation time encoded in a `rebase-<db>-<ts>.dump` name.\n * Returns `null` for names that don't match, so foreign objects are ignored.\n */\nexport function parseBackupTimestamp(fileName: string): Date | null {\n const base = fileName.split(\"/\").pop() ?? fileName;\n const match = base.match(/-(\\d{8})T(\\d{6})Z\\.dump$/);\n if (!match) return null;\n const [, ymd, hms] = match;\n const iso =\n `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +\n `T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * A backup is two files: the `.dump`, and beside it a `.globals.sql` holding\n * the cluster roles the dump's GRANTs and RLS policies name. Without that\n * sidecar a restore into a fresh cluster stops at the first GRANT to a role\n * that does not exist there. The CLI uploads, restores and prunes the two as\n * a pair. This listing used to know only the `.dump`, so a backup downloaded\n * from Studio arrived without its roles.\n *\n * The suffix mirrors `globalsFileForDump` in `@rebasepro/server-postgres`.\n */\nconst DUMP_SUFFIX = \".dump\";\nconst GLOBALS_SUFFIX = \".globals.sql\";\n\nfunction globalsKeyFor(dumpKey: string): string {\n return dumpKey.slice(0, -DUMP_SUFFIX.length) + GLOBALS_SUFFIX;\n}\n\n/** The only files the download route will serve. */\nfunction isBackupFile(key: string): boolean {\n return key.endsWith(DUMP_SUFFIX) || key.endsWith(GLOBALS_SUFFIX);\n}\n\n/**\n * How many pages of an object-storage listing to read before giving up — at\n * the providers' 1,000 keys a page, 100,000 objects, or 50,000 backups.\n */\nconst MAX_LIST_PAGES = 100;\n\n/**\n * Every key under a prefix, across pages.\n *\n * S3 and GCS answer a listing a page at a time, in ascending key order, and a\n * backup's key is `rebase-<db>-<UTC timestamp>`. Reading one page therefore\n * returned the OLDEST thousand objects: with retention unset and an hourly\n * schedule, the newest backup the panel and `rebase db backups list` showed\n * stopped advancing after about three weeks, which reads as \"backups stopped\".\n */\nexport async function listAllObjectKeys(\n storage: StorageController,\n prefix: string,\n bucket: string\n): Promise<string[]> {\n const keys: string[] = [];\n let pageToken: string | undefined;\n for (let page = 0; page < MAX_LIST_PAGES; page++) {\n const result = await storage.listObjects(prefix, { bucket, maxResults: 1000, pageToken });\n for (const item of result.items) keys.push(item.fullPath);\n pageToken = result.nextPageToken;\n if (!pageToken) return keys;\n }\n logger.warn(\n `[backups] Stopped listing ${bucket}/${prefix} after ${MAX_LIST_PAGES} pages (${keys.length} objects). ` +\n \"Set BACKUP_RETENTION_DAYS so old backups are pruned, or give the backups a prefix of their own.\"\n );\n return keys;\n}\n\n/**\n * Whether an object key is one the backup routes may serve: a backup file, at\n * the destination's prefix and nowhere else.\n *\n * The local branch has always been held to the backup directory; this branch\n * checked only the suffix, so any `.dump` in the bucket — `uploads/x.dump`, a\n * neighbouring `nightlyish/` prefix — was readable by naming it. A key is taken\n * as written, so a scheme (`s3://other-bucket/…`, which S3StorageController\n * would follow) or a `.`/`..`/empty segment (which a filesystem-backed or\n * S3-compatible store may resolve) is refused rather than interpreted.\n */\nfunction isObjectBackupKey(dest: { prefix: string }, key: string): boolean {\n if (!isBackupFile(key) || /^[a-z][a-z0-9+.-]*:\\/\\//i.test(key)) return false;\n const segments = key.split(\"/\");\n if (segments.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) return false;\n return dest.prefix === \"\" || key.startsWith(`${dest.prefix}/`);\n}\n\n/**\n * List the backups at a destination as {@link BackupInfo}, newest first.\n * One entry per `.dump`, with its `.globals.sql` sidecar attached as\n * `globalsKey` when there is one.\n */\nexport async function listBackupObjects(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupInfo[]> {\n if (dest.kind === \"local\") {\n // Asynchronous throughout, a few stats at a time. This runs on the\n // Backups panel's request path, and a year of hourly backups is 8,760\n // dumps: stat'ed synchronously, the server answered nothing else\n // while it listed them.\n const stat = await fs.promises.stat(dest.path).catch(() => null);\n if (!stat) return [];\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n const files = await fs.promises.readdir(dir);\n const present = new Set(files);\n const dumps = files.filter((f) => f.endsWith(DUMP_SUFFIX));\n const stats = await mapBounded(dumps, LOCAL_STAT_CONCURRENCY, (f) => fs.promises.stat(path.join(dir, f)));\n return dumps\n .map((f, i): BackupInfo => {\n const full = path.join(dir, f);\n const st = stats[i];\n const createdAt = parseBackupTimestamp(f) ?? st.mtime;\n const globals = globalsKeyFor(f);\n return {\n key: full,\n name: f,\n sizeBytes: st.size,\n createdAt: createdAt.toISOString(),\n destinationKind: \"local\",\n ...(present.has(globals) ? { globalsKey: path.join(dir, globals) } : {})\n };\n })\n .sort(byNewest);\n }\n\n // An empty list is a claim about the bucket. Without a controller for it\n // nothing was read, and the panel said \"No backups yet.\" about a bucket\n // full of them.\n if (!storage) throw new Error(noControllerFor(dest));\n const keys = await listAllObjectKeys(storage, dest.prefix ? `${dest.prefix}/` : \"\", dest.bucket);\n const present = new Set(keys);\n return keys\n .filter((key) => key.endsWith(DUMP_SUFFIX))\n .map((key): BackupInfo => {\n const createdAt = parseBackupTimestamp(key);\n const globals = globalsKeyFor(key);\n return {\n key,\n name: key.split(\"/\").pop() || key,\n createdAt: createdAt ? createdAt.toISOString() : undefined,\n destinationKind: dest.kind as BackupDestinationKind,\n ...(present.has(globals) ? { globalsKey: globals } : {})\n };\n })\n .sort(byNewest);\n}\n\n/** Stats in flight at once while listing a local backup folder. */\nconst LOCAL_STAT_CONCURRENCY = 32;\n\n/** `fn` over `items`, at most `limit` at a time; results in input order. */\nasync function mapBounded<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n const worker = async () => {\n while (next < items.length) {\n const i = next++;\n results[i] = await fn(items[i]);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n return results;\n}\n\nfunction noControllerFor(dest: { kind: \"s3\" | \"gcs\"; bucket: string; prefix: string }): string {\n const scheme = dest.kind === \"s3\" ? \"s3\" : \"gs\";\n return `No storage controller for ${scheme}://${dest.bucket}${dest.prefix ? `/${dest.prefix}` : \"\"}, so its backups cannot be read.`;\n}\n\nfunction byNewest(a: BackupInfo, b: BackupInfo): number {\n const ta = a.createdAt ? Date.parse(a.createdAt) : 0;\n const tb = b.createdAt ? Date.parse(b.createdAt) : 0;\n return tb - ta;\n}\n\n/**\n * The absolute path of a local backup file, or `null` for anything the download\n * route must not serve: a path outside the backup directory, a file that is not\n * a backup, or one that does not exist.\n */\nfunction resolveLocalBackupFile(dest: { path: string }, key: string): string | null {\n const stat = fs.existsSync(dest.path) ? fs.statSync(dest.path) : null;\n const dir = stat?.isDirectory() ? dest.path : path.dirname(dest.path);\n const resolvedDir = path.resolve(dir);\n const resolved = path.resolve(key);\n // Only allow reads inside the backup directory, and only backup files.\n if (!resolved.startsWith(resolvedDir + path.sep) || !isBackupFile(resolved)) {\n return null;\n }\n return fs.existsSync(resolved) ? resolved : null;\n}\n\n/** Read a single backup's bytes. Guards against path traversal for local. */\nexport async function readBackupBytes(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ bytes: Uint8Array<ArrayBuffer>; name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n if (!resolved) return null;\n return { bytes: new Uint8Array(fs.readFileSync(resolved)), name: path.basename(resolved) };\n }\n\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n const file = await storage.getObject(key, dest.bucket);\n if (!file) return null;\n return { bytes: new Uint8Array(await file.arrayBuffer()), name: key.split(\"/\").pop() || key };\n}\n\n/**\n * Whether `key` names a backup the download route would serve, without\n * opening it: the same guards as {@link openBackupStream}, and the file or\n * object is there. An object store without `statObject` is taken at its key —\n * reading the object to find out would be the download itself.\n */\nexport async function findBackup(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n return resolved ? { name: path.basename(resolved) } : null;\n }\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n if (storage.statObject && !(await storage.statObject(key, dest.bucket))) return null;\n return { name: key.split(\"/\").pop() || key };\n}\n\n/**\n * A single backup, opened for streaming, with its size. Same guards as\n * {@link readBackupBytes}.\n *\n * What the download route serves. A dump is the size of the database, and\n * reading it whole put all of it on the API process's heap — twice for a local\n * destination (`readFileSync`, then a copy) and on top of the controller's own\n * buffer for object storage — and a file over 2 GiB never downloaded at all:\n * `readFileSync` throws `ERR_FS_FILE_TOO_LARGE` there.\n *\n * A local file is read from disk as the response is written, and so is an\n * object through a controller with `getObjectStream` (the built-in S3 and GCS\n * ones). A custom controller without it still arrives through\n * {@link StorageController.getObject}, a buffered `File` by contract;\n * streaming that avoids the second copy.\n */\nexport async function openBackupStream(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ stream: ReadableStream<Uint8Array>; size: number; name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n if (!resolved) return null;\n const { size } = await fs.promises.stat(resolved);\n return { stream: fileStream(resolved), size, name: path.basename(resolved) };\n }\n\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n const name = key.split(\"/\").pop() || key;\n if (storage.getObjectStream) {\n const object = await storage.getObjectStream(key, { bucket: dest.bucket });\n return object ? { stream: object.stream, size: object.size, name } : null;\n }\n const file = await storage.getObject(key, dest.bucket);\n if (!file) return null;\n return { stream: file.stream(), size: file.size, name };\n}\n\n/** A file on disk as a web stream, read a chunk at a time as the response is written. */\nfunction fileStream(filePath: string): ReadableStream<Uint8Array> {\n const file = fs.createReadStream(filePath);\n const chunks: AsyncIterator<Buffer> = file[Symbol.asyncIterator]();\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n const next = await chunks.next();\n if (next.done) controller.close();\n else controller.enqueue(next.value);\n },\n cancel() {\n file.destroy();\n }\n });\n}\n","/**\n * The storage controller that reads a backup bucket.\n *\n * A backup destination is its own bucket, named by `BACKUP_DESTINATION`, and\n * the documented backup cron writes to it through a controller it builds from\n * the `S3_*` variables (GCS: application default credentials) — as\n * `rebase db backups list` does. The admin routes have to read it the same way.\n * The app's file storage is not that controller: it is configured for the\n * uploads bucket, it is a local directory in development, and in production\n * without an uploads bucket there is none at all.\n */\nimport { parseEnvBoolean } from \"@rebasepro/types\";\nimport { GCSStorageController } from \"../storage/GCSStorageController\";\nimport { S3StorageController } from \"../storage/S3StorageController\";\nimport type { StorageController } from \"../storage/types\";\nimport { parseBackupDestination, type BackupDestination } from \"./backup-common\";\n\n/** A destination in object storage — `s3://bucket/prefix` or `gs://bucket/prefix`. */\nexport type ObjectBackupDestination = Exclude<BackupDestination, { kind: \"local\" }>;\n\n/** The destination `BACKUP_DESTINATION` names, or `null` when it is unset. */\nexport function backupDestinationFromEnv(env: Record<string, string | undefined>): BackupDestination | null {\n const out = env.BACKUP_DESTINATION?.trim();\n return out ? parseBackupDestination(out) : null;\n}\n\n/** The destination as `BACKUP_DESTINATION` spells it. */\nexport function describeBackupDestination(dest: BackupDestination): string {\n if (dest.kind === \"local\") return dest.path;\n const scheme = dest.kind === \"s3\" ? \"s3\" : \"gs\";\n return `${scheme}://${dest.bucket}${dest.prefix ? `/${dest.prefix}` : \"\"}`;\n}\n\n/**\n * A controller for an object-storage destination's bucket, built from the\n * environment the way the backup cron and `rebase db backups list` build\n * theirs. Throws when an S3 destination has no credentials to read it with.\n */\nexport function backupStorageFromEnv(\n dest: ObjectBackupDestination,\n env: Record<string, string | undefined>\n): StorageController {\n if (dest.kind === \"gcs\") {\n return new GCSStorageController({ type: \"gcs\", bucket: dest.bucket });\n }\n // s3 also covers R2, MinIO and the other S3-compatible stores, through S3_ENDPOINT.\n if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) {\n throw new Error(\n \"An S3 backup destination needs S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in the environment \" +\n \"— the same S3_* variables the backup cron uses.\"\n );\n }\n return new S3StorageController({\n type: \"s3\",\n bucket: dest.bucket,\n region: env.S3_REGION || \"auto\",\n accessKeyId: env.S3_ACCESS_KEY_ID,\n secretAccessKey: env.S3_SECRET_ACCESS_KEY,\n endpoint: env.S3_ENDPOINT,\n // Unset stays unset, so the controller decides from the endpoint.\n forcePathStyle: parseEnvBoolean(env.S3_FORCE_PATH_STYLE)\n });\n}\n","import { Hono, type Context } from \"hono\";\nimport type { BackupDownloadLink, BackupListing, BackupScheduleStatus } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { jwtOf, type JwtService } from \"../auth/jwt\";\nimport type { StorageController } from \"../storage/types\";\nimport { BackupDestination, findBackup, listBackupObjects, openBackupStream } from \"./backup-common\";\nimport { backupStorageFromEnv, describeBackupDestination, type ObjectBackupDestination } from \"./backup-storage\";\n\nexport interface BackupRoutesConfig {\n /**\n * Resolve the current backup destination, or `null` when backups are not\n * configured (`BACKUP_DESTINATION` unset). Read lazily so a restart isn't\n * required to pick up config.\n */\n getDestination: () => BackupDestination | null;\n /**\n * The controller that reads an object-storage destination. It has to\n * address the destination's own bucket — never the app's file storage,\n * which is another bucket, a local directory, or nothing. Defaults to\n * {@link backupStorageFromEnv} over `process.env`, which is how the backup\n * cron builds the one it writes with.\n */\n storageFor?: (dest: ObjectBackupDestination) => StorageController;\n /**\n * The scheduled backup job and its last run — see `readBackupSchedule`.\n * Omitted where this process has no cron scheduler; the listing then\n * answers `schedule: null`.\n */\n getSchedule?: () => Promise<BackupScheduleStatus | null>;\n /**\n * True when this process does not run the scheduled jobs (the `api` role\n * of a split deployment). A local destination read here is then this\n * process's disk, not the one the scheduled backup writes to.\n */\n scheduledElsewhere?: boolean;\n}\n\n/** Why a destination could not be read, as the answer says it. */\nfunction unreadable(dest: BackupDestination, err: unknown): ApiError {\n const reason = err instanceof Error ? err.message : String(err);\n return ApiError.serviceUnavailable(`Cannot read the backups at ${describeBackupDestination(dest)}: ${reason}`);\n}\n\n// ─── Download links ──────────────────────────────────────────────────\n\n/**\n * The `purpose` of a download link's token. `verifyAccessToken` refuses\n * any token with a purpose, and the storage, MFA and MCP verifiers each demand\n * their own, so a link is never a session or any other credential, and none of\n * those is a link.\n */\nconst BACKUP_DOWNLOAD_PURPOSE = \"backup-download\";\n/** The token's audience: the one route that redeems it. */\nconst BACKUP_DOWNLOAD_AUDIENCE = \"rebase:admin/backups/download\";\n/** How long a link works. Long enough to start the download, not to pass around. */\nexport const BACKUP_DOWNLOAD_LINK_TTL_SECONDS = 60;\n\n/** What a link grants: one backup, at one destination. */\nfunction linkScope(dest: BackupDestination, key: string): string {\n return `backup:${describeBackupDestination(dest)}:${key}`;\n}\n\n/**\n * Whether `token` is a live link for `key` at `dest`, signed by `jwt` — the\n * backend answering. Never throws: anything else is a no.\n */\nasync function linkGrants(jwt: JwtService, token: string, dest: BackupDestination, key: string): Promise<boolean> {\n if (!jwt.isConfigured()) return false;\n const claims = await jwt.verifyPurposeToken<{ aud?: unknown; scope?: unknown }>(BACKUP_DOWNLOAD_PURPOSE, token)\n .catch(() => null);\n return claims?.aud === BACKUP_DOWNLOAD_AUDIENCE && claims.scope === linkScope(dest, key);\n}\n\n/** The configured destination, or the 400 every backup route answers without one. */\nfunction destinationOf(config: Pick<BackupRoutesConfig, \"getDestination\">): BackupDestination {\n const dest = config.getDestination();\n if (!dest) throw ApiError.badRequest(\"Backups are not configured (set BACKUP_DESTINATION).\");\n return dest;\n}\n\n/** {@link BackupRoutesConfig.storageFor}, with its default, for a destination that has a controller. */\nfunction storageResolver(config: Pick<BackupRoutesConfig, \"storageFor\">): (dest: BackupDestination) => StorageController | undefined {\n const storageFor = config.storageFor ?? ((dest: ObjectBackupDestination) => backupStorageFromEnv(dest, process.env));\n /** The controller for `dest`, or none for a local one. A destination it cannot read is an error, never `[]`. */\n return (dest) => {\n if (dest.kind === \"local\") return undefined;\n try {\n return storageFor(dest);\n } catch (err) {\n throw unreadable(dest, err);\n }\n };\n}\n\n/** One backup's bytes as the response, streamed: a dump is the size of the database. See `openBackupStream`. */\nasync function serveBackup(c: Context<HonoEnv>, dest: BackupDestination, key: string, storage: StorageController | undefined): Promise<Response> {\n const result = await openBackupStream(dest, key, storage);\n if (!result) {\n throw ApiError.notFound(`Backup not found: ${key}`);\n }\n c.header(\"Content-Type\", \"application/octet-stream\");\n c.header(\"Content-Length\", String(result.size));\n c.header(\"Content-Disposition\", `attachment; filename=\"${result.name}\"`);\n // A whole database, and on a link a URL that grants it: nothing in between keeps a copy.\n c.header(\"Cache-Control\", \"no-store\");\n return c.body(result.stream);\n}\n\n/**\n * `GET /download?key=…&token=…`: a download link spent in place of the bearer\n * header, for the one backup it names.\n *\n * Mount it at the backups prefix AHEAD of every gate on the admin prefix: the\n * auth adapter's own admin routers authenticate everything under `/admin`\n * before the Backups gate is reached. A request with no `token` passes\n * through untouched; one with a token is answered here — the backup, or a\n * 401 — and never falls through to the header: a bad link is a bad link.\n *\n * A browser cannot put a header on a navigation, so before links the SDK\n * fetched the dump and held it whole in the tab as a Blob. With a link the\n * browser streams it to disk.\n */\nexport function createBackupLinkRoutes(config: Pick<BackupRoutesConfig, \"getDestination\" | \"storageFor\">): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n const storageOf = storageResolver(config);\n router.get(\"/download\", async (c, next) => {\n const token = c.req.query(\"token\");\n if (token === undefined) return next();\n const dest = destinationOf(config);\n const key = c.req.query(\"key\") ?? \"\";\n if (!(await linkGrants(jwtOf(c), token, dest, key))) {\n throw ApiError.unauthenticated(\n \"This download link is not valid for this backup, or it has expired. Links last \" +\n `${BACKUP_DOWNLOAD_LINK_TTL_SECONDS} seconds: ask for a new one.`\n );\n }\n return serveBackup(c, dest, key, storageOf(dest));\n });\n return router;\n}\n\n/**\n * Admin REST routes for the Backups panel.\n *\n * Routes (mounted under `/admin/backups`, admin-guarded by the caller):\n * GET / → list available backups, and the scheduled job's last run\n * GET /download → download a backup's bytes (?key=…)\n * POST /download-url → a 60-second link to one backup, `{ key }` → {@link BackupDownloadLink}\n *\n * The link is spent at `GET /download?key=…&token=…`, which\n * {@link createBackupLinkRoutes} answers ahead of the gate.\n */\nexport function createBackupRoutes(config: BackupRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n const storageOf = storageResolver(config);\n\n router.get(\"/\", async (c) => {\n // Read whether or not a destination is configured: a backup cron with\n // no BACKUP_DESTINATION fails every run, and that is exactly what the\n // panel has to be able to say.\n const schedule = config.getSchedule ? await config.getSchedule() : null;\n const dest = config.getDestination();\n if (!dest) {\n return c.json({ backups: [], destinationKind: \"local\", configured: false, schedule } satisfies BackupListing);\n }\n const backups = await listBackupObjects(dest, storageOf(dest)).catch((err: unknown) => {\n throw unreadable(dest, err);\n });\n // Only where there is a scheduled backup to write elsewhere.\n const localDiskOfThisProcess = dest.kind === \"local\" && config.scheduledElsewhere === true && schedule !== null;\n return c.json({\n backups,\n destinationKind: dest.kind,\n configured: true,\n schedule,\n ...(localDiskOfThisProcess ? { localDiskOfThisProcess } : {})\n } satisfies BackupListing);\n });\n\n router.get(\"/download\", async (c) => {\n const dest = destinationOf(config);\n const key = c.req.query(\"key\");\n if (!key) {\n throw ApiError.badRequest(\"Missing 'key' query parameter.\");\n }\n return serveBackup(c, dest, key, storageOf(dest));\n });\n\n router.post(\"/download-url\", async (c) => {\n const dest = destinationOf(config);\n const body: unknown = await c.req.json().catch(() => null);\n const key = typeof body === \"object\" && body !== null && \"key\" in body ? body.key : undefined;\n if (typeof key !== \"string\" || !key) {\n throw ApiError.badRequest(\"Send the backup's key: { \\\"key\\\": \\\"…\\\" }.\");\n }\n // The download route's own guards, so a link is never minted for a\n // key it would refuse; and the backup is there, so the browser is not\n // sent to a 404 in place of a file.\n if (!(await findBackup(dest, key, storageOf(dest)))) {\n throw ApiError.notFound(`Backup not found: ${key}`);\n }\n const jwt = jwtOf(c);\n if (!jwt.isConfigured()) {\n throw ApiError.serviceUnavailable(\n \"Download links are signed with the JWT secret, and this server has none (auth.jwtSecret). \" +\n \"Download through GET /download with the Authorization header instead.\"\n );\n }\n const token = await jwt.signPurposeToken(\n BACKUP_DOWNLOAD_PURPOSE,\n { aud: BACKUP_DOWNLOAD_AUDIENCE, scope: linkScope(dest, key) },\n BACKUP_DOWNLOAD_LINK_TTL_SECONDS\n );\n const download = new URL(\"download\", c.req.url).pathname;\n return c.json({\n url: `${download}?key=${encodeURIComponent(key)}&token=${encodeURIComponent(token)}`,\n token,\n expiresIn: BACKUP_DOWNLOAD_LINK_TTL_SECONDS\n } satisfies BackupDownloadLink);\n });\n\n return router;\n}\n","/**\n * The scheduled backup job, as the Backups panel reports it.\n *\n * A backup that fails every night has to be visible where the backups are\n * listed. Before this the panel read the destination and nothing else, so a\n * cron that could not run `pg_dump` left an empty list under \"wait for the next\n * scheduled run\" — while each failure sat in `cron_logs`, one panel away.\n */\nimport type {\n BackupRunOutcome,\n BackupScheduleStatus,\n CronJobDefinition,\n CronJobLogEntry,\n CronJobStatus,\n RejectedCronJob\n} from \"@rebasepro/types\";\nimport { isAlreadyExecutingSkip } from \"../cron/cron-scheduler\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The mark `createBackupCron` (in `@rebasepro/server-postgres`) puts on the\n * definition it returns.\n *\n * A registry symbol, so it is the same value across two installed copies of\n * either package, and neither package has to import the other: `server` must\n * not depend on `server-postgres`. The job cannot be recognised any other way —\n * its id is the cron file's name and its display name is configurable.\n */\nexport const BACKUP_CRON_MARK: unique symbol = Symbol.for(\"rebase.backupCron\");\n\n/** Whether a cron definition is the scheduled backup. */\nexport function isBackupCronDefinition(definition: CronJobDefinition): boolean {\n return Reflect.get(definition, BACKUP_CRON_MARK) === true;\n}\n\n/** The part of the cron scheduler this reads. */\nexport interface BackupScheduleSource {\n jobIdsWhere(predicate: (definition: CronJobDefinition) => boolean): string[];\n rejectedJobsWhere(predicate: (definition: CronJobDefinition) => boolean): RejectedCronJob[];\n fetchJob(id: string): Promise<CronJobStatus | undefined>;\n getJobLogsFromDb(id: string, limit?: number): Promise<CronJobLogEntry[]>;\n}\n\n/**\n * How many recent log entries to look through for the last scheduled run. A\n * skip — a slot that found the previous run still going — is logged as a\n * success with nothing done, and must not stand in for the run it skipped; nor\n * may a run someone started by hand, which is not the schedule's.\n */\nconst RECENT_RUNS = 50;\n\n/** A log entry as the panel reports a run. */\nfunction outcomeOf(entry: CronJobLogEntry): BackupRunOutcome {\n return {\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n success: entry.success,\n ...(entry.success || !entry.error ? {} : { error: entry.error }),\n manual: entry.manual === true\n };\n}\n\n/**\n * The scheduled backup job and its last run, or `null` when the deployment\n * has none — registered or refused.\n */\nexport async function readBackupSchedule(source: BackupScheduleSource): Promise<BackupScheduleStatus | null> {\n const [jobId] = source.jobIdsWhere(isBackupCronDefinition);\n if (jobId === undefined) {\n // A refused job is not registered, and answered `null` here it read\n // as \"no backup cron\" under a hint to add the file the project has.\n const [refused] = source.rejectedJobsWhere(isBackupCronDefinition);\n return refused\n ? { jobId: refused.id, name: refused.name, schedule: refused.schedule, enabled: false, refused: refused.reason }\n : null;\n }\n\n const job = await source.fetchJob(jobId);\n if (!job) return null;\n // Off because its own definition says so — the documented cron file\n // exports `enabled: false` while BACKUP_SCHEDULE is unset — rather than\n // paused by someone. Resuming that in Cron Jobs would run the placeholder.\n const disabledInCode = !job.enabled &&\n source.jobIdsWhere(definition => isBackupCronDefinition(definition) && definition.enabled === false).includes(jobId);\n\n let recent: CronJobLogEntry[];\n let historyError: string | undefined;\n try {\n recent = await source.getJobLogsFromDb(jobId, RECENT_RUNS);\n } catch (err) {\n // Said, not read as \"has not run yet\". The listing itself still\n // answers: the backups at the destination are worth showing.\n logger.error(`[backups] Could not read the run history of the backup job \"${jobId}\"`, { error: err });\n recent = [];\n historyError = \"The backup job's run history could not be read. The server log has the reason.\";\n }\n const runs = recent.filter(entry => !isAlreadyExecutingSkip(entry));\n // \"Last scheduled backup\" is the schedule's: a later \"Run Now\" that worked\n // hid a failed nightly, and a failed test run read as a failed nightly.\n const lastScheduled = runs.find(entry => entry.manual !== true);\n // A run by hand since then is reported too, as what it is.\n const lastManual = runs.find(entry => entry.manual === true);\n const manualSince = lastManual && (!lastScheduled || lastManual.startedAt > lastScheduled.startedAt)\n ? lastManual\n : undefined;\n\n return {\n jobId,\n name: job.name,\n schedule: job.schedule,\n enabled: job.enabled,\n ...(disabledInCode ? { disabledInCode } : {}),\n ...(job.nextRunAt ? { nextRunAt: job.nextRunAt } : {}),\n ...(historyError ? { historyError } : {}),\n ...(lastScheduled ? { lastRun: outcomeOf(lastScheduled) } : {}),\n ...(manualSince ? { lastManualRun: outcomeOf(manualSince) } : {})\n };\n}\n","/**\n * Storage-generic backup helpers and admin routes for the Backups panel.\n * The `pg_dump`/`pg_restore` machinery lives in `@rebasepro/server-postgres`.\n */\nexport * from \"./backup-common\";\nexport { createBackupRoutes, createBackupLinkRoutes, BACKUP_DOWNLOAD_LINK_TTL_SECONDS } from \"./backup-routes\";\nexport { backupDestinationFromEnv, backupStorageFromEnv } from \"./backup-storage\";\nexport type { ObjectBackupDestination } from \"./backup-storage\";\nexport type { BackupRoutesConfig } from \"./backup-routes\";\nexport { readBackupSchedule, isBackupCronDefinition, BACKUP_CRON_MARK } from \"./backup-schedule\";\nexport type { BackupScheduleSource } from \"./backup-schedule\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"backup-Dq0lyJ_D.js","names":[],"sources":["../src/backup/backup-common.ts","../src/backup/backup-storage.ts","../src/backup/backup-routes.ts","../src/backup/backup-schedule.ts","../src/backup/index.ts"],"sourcesContent":["/**\n * Storage-generic backup helpers used by the admin backup routes.\n *\n * These live in server (which owns {@link StorageController}) so the\n * admin API can list backups without depending on the Postgres driver —\n * `server` must not import from `server-postgres`. The `pg_dump`-side\n * copies of the tiny pure parsers live in `@rebasepro/server-postgres`;\n * keep the two in sync if the naming scheme ever changes.\n */\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { BackupInfo, BackupDestinationKind } from \"@rebasepro/types\";\nimport type { StorageController } from \"../storage\";\nimport { logger } from \"../utils/logger\";\n\nexport type BackupDestination =\n | { kind: \"local\"; path: string }\n | { kind: \"s3\"; bucket: string; prefix: string }\n | { kind: \"gcs\"; bucket: string; prefix: string };\n\n/** Parse a destination string (`s3://…`, `gs://…`, or a local path). */\nexport function parseBackupDestination(out: string): BackupDestination {\n const s3 = out.match(/^s3:\\/\\/([^/]+)\\/?(.*)$/);\n if (s3) return { kind: \"s3\", bucket: s3[1], prefix: s3[2].replace(/\\/+$/, \"\") };\n const gcs = out.match(/^gs:\\/\\/([^/]+)\\/?(.*)$/);\n if (gcs) return { kind: \"gcs\", bucket: gcs[1], prefix: gcs[2].replace(/\\/+$/, \"\") };\n return { kind: \"local\", path: out };\n}\n\n/**\n * Recover the UTC creation time encoded in a `rebase-<db>-<ts>.dump` name.\n * Returns `null` for names that don't match, so foreign objects are ignored.\n */\nexport function parseBackupTimestamp(fileName: string): Date | null {\n const base = fileName.split(\"/\").pop() ?? fileName;\n const match = base.match(/-(\\d{8})T(\\d{6})Z\\.dump$/);\n if (!match) return null;\n const [, ymd, hms] = match;\n const iso =\n `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +\n `T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * A backup is two files: the `.dump`, and beside it a `.globals.sql` holding\n * the cluster roles the dump's GRANTs and RLS policies name. Without that\n * sidecar a restore into a fresh cluster stops at the first GRANT to a role\n * that does not exist there. The CLI uploads, restores and prunes the two as\n * a pair. This listing used to know only the `.dump`, so a backup downloaded\n * from Studio arrived without its roles.\n *\n * The suffix mirrors `globalsFileForDump` in `@rebasepro/server-postgres`.\n */\nconst DUMP_SUFFIX = \".dump\";\nconst GLOBALS_SUFFIX = \".globals.sql\";\n\nfunction globalsKeyFor(dumpKey: string): string {\n return dumpKey.slice(0, -DUMP_SUFFIX.length) + GLOBALS_SUFFIX;\n}\n\n/** The only files the download route will serve. */\nfunction isBackupFile(key: string): boolean {\n return key.endsWith(DUMP_SUFFIX) || key.endsWith(GLOBALS_SUFFIX);\n}\n\n/**\n * How many pages of an object-storage listing to read before giving up — at\n * the providers' 1,000 keys a page, 100,000 objects, or 50,000 backups.\n */\nconst MAX_LIST_PAGES = 100;\n\n/**\n * Every key under a prefix, across pages.\n *\n * S3 and GCS answer a listing a page at a time, in ascending key order, and a\n * backup's key is `rebase-<db>-<UTC timestamp>`. Reading one page therefore\n * returned the OLDEST thousand objects: with retention unset and an hourly\n * schedule, the newest backup the panel and `rebase db backups list` showed\n * stopped advancing after about three weeks, which reads as \"backups stopped\".\n */\nexport async function listAllObjectKeys(\n storage: StorageController,\n prefix: string,\n bucket: string\n): Promise<string[]> {\n const keys: string[] = [];\n let pageToken: string | undefined;\n for (let page = 0; page < MAX_LIST_PAGES; page++) {\n const result = await storage.listObjects(prefix, { bucket, maxResults: 1000, pageToken });\n for (const item of result.items) keys.push(item.fullPath);\n pageToken = result.nextPageToken;\n if (!pageToken) return keys;\n }\n logger.warn(\n `[backups] Stopped listing ${bucket}/${prefix} after ${MAX_LIST_PAGES} pages (${keys.length} objects). ` +\n \"Set BACKUP_RETENTION_DAYS so old backups are pruned, or give the backups a prefix of their own.\"\n );\n return keys;\n}\n\n/**\n * Whether an object key is one the backup routes may serve: a backup file, at\n * the destination's prefix and nowhere else.\n *\n * The local branch has always been held to the backup directory; this branch\n * checked only the suffix, so any `.dump` in the bucket — `uploads/x.dump`, a\n * neighbouring `nightlyish/` prefix — was readable by naming it. A key is taken\n * as written, so a scheme (`s3://other-bucket/…`, which S3StorageController\n * would follow) or a `.`/`..`/empty segment (which a filesystem-backed or\n * S3-compatible store may resolve) is refused rather than interpreted.\n */\nfunction isObjectBackupKey(dest: { prefix: string }, key: string): boolean {\n if (!isBackupFile(key) || /^[a-z][a-z0-9+.-]*:\\/\\//i.test(key)) return false;\n const segments = key.split(\"/\");\n if (segments.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) return false;\n return dest.prefix === \"\" || key.startsWith(`${dest.prefix}/`);\n}\n\n/**\n * List the backups at a destination as {@link BackupInfo}, newest first.\n * One entry per `.dump`, with its `.globals.sql` sidecar attached as\n * `globalsKey` when there is one.\n */\nexport async function listBackupObjects(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupInfo[]> {\n if (dest.kind === \"local\") {\n // Asynchronous throughout, a few stats at a time. This runs on the\n // Backups panel's request path, and a year of hourly backups is 8,760\n // dumps: stat'ed synchronously, the server answered nothing else\n // while it listed them.\n const stat = await fs.promises.stat(dest.path).catch(() => null);\n if (!stat) return [];\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n const files = await fs.promises.readdir(dir);\n const present = new Set(files);\n const dumps = files.filter((f) => f.endsWith(DUMP_SUFFIX));\n const stats = await mapBounded(dumps, LOCAL_STAT_CONCURRENCY, (f) => fs.promises.stat(path.join(dir, f)));\n return dumps\n .map((f, i): BackupInfo => {\n const full = path.join(dir, f);\n const st = stats[i];\n const createdAt = parseBackupTimestamp(f) ?? st.mtime;\n const globals = globalsKeyFor(f);\n return {\n key: full,\n name: f,\n sizeBytes: st.size,\n createdAt: createdAt.toISOString(),\n destinationKind: \"local\",\n ...(present.has(globals) ? { globalsKey: path.join(dir, globals) } : {})\n };\n })\n .sort(byNewest);\n }\n\n // An empty list is a claim about the bucket. Without a controller for it\n // nothing was read, and the panel said \"No backups yet.\" about a bucket\n // full of them.\n if (!storage) throw new Error(noControllerFor(dest));\n const keys = await listAllObjectKeys(storage, dest.prefix ? `${dest.prefix}/` : \"\", dest.bucket);\n const present = new Set(keys);\n return keys\n .filter((key) => key.endsWith(DUMP_SUFFIX))\n .map((key): BackupInfo => {\n const createdAt = parseBackupTimestamp(key);\n const globals = globalsKeyFor(key);\n return {\n key,\n name: key.split(\"/\").pop() || key,\n createdAt: createdAt ? createdAt.toISOString() : undefined,\n destinationKind: dest.kind as BackupDestinationKind,\n ...(present.has(globals) ? { globalsKey: globals } : {})\n };\n })\n .sort(byNewest);\n}\n\n/** Stats in flight at once while listing a local backup folder. */\nconst LOCAL_STAT_CONCURRENCY = 32;\n\n/** `fn` over `items`, at most `limit` at a time; results in input order. */\nasync function mapBounded<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n const worker = async () => {\n while (next < items.length) {\n const i = next++;\n results[i] = await fn(items[i]);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n return results;\n}\n\nfunction noControllerFor(dest: { kind: \"s3\" | \"gcs\"; bucket: string; prefix: string }): string {\n const scheme = dest.kind === \"s3\" ? \"s3\" : \"gs\";\n return `No storage controller for ${scheme}://${dest.bucket}${dest.prefix ? `/${dest.prefix}` : \"\"}, so its backups cannot be read.`;\n}\n\nfunction byNewest(a: BackupInfo, b: BackupInfo): number {\n const ta = a.createdAt ? Date.parse(a.createdAt) : 0;\n const tb = b.createdAt ? Date.parse(b.createdAt) : 0;\n return tb - ta;\n}\n\n/**\n * The absolute path of a local backup file, or `null` for anything the download\n * route must not serve: a path outside the backup directory, a file that is not\n * a backup, or one that does not exist.\n */\nfunction resolveLocalBackupFile(dest: { path: string }, key: string): string | null {\n const stat = fs.existsSync(dest.path) ? fs.statSync(dest.path) : null;\n const dir = stat?.isDirectory() ? dest.path : path.dirname(dest.path);\n const resolvedDir = path.resolve(dir);\n const resolved = path.resolve(key);\n // Only allow reads inside the backup directory, and only backup files.\n if (!resolved.startsWith(resolvedDir + path.sep) || !isBackupFile(resolved)) {\n return null;\n }\n return fs.existsSync(resolved) ? resolved : null;\n}\n\n/** Read a single backup's bytes. Guards against path traversal for local. */\nexport async function readBackupBytes(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ bytes: Uint8Array<ArrayBuffer>; name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n if (!resolved) return null;\n return { bytes: new Uint8Array(fs.readFileSync(resolved)), name: path.basename(resolved) };\n }\n\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n const file = await storage.getObject(key, dest.bucket);\n if (!file) return null;\n return { bytes: new Uint8Array(await file.arrayBuffer()), name: key.split(\"/\").pop() || key };\n}\n\n/**\n * Whether `key` names a backup the download route would serve, without\n * opening it: the same guards as {@link openBackupStream}, and the file or\n * object is there. An object store without `statObject` is taken at its key —\n * reading the object to find out would be the download itself.\n */\nexport async function findBackup(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n return resolved ? { name: path.basename(resolved) } : null;\n }\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n if (storage.statObject && !(await storage.statObject(key, dest.bucket))) return null;\n return { name: key.split(\"/\").pop() || key };\n}\n\n/**\n * A single backup, opened for streaming, with its size. Same guards as\n * {@link readBackupBytes}.\n *\n * What the download route serves. A dump is the size of the database, and\n * reading it whole put all of it on the API process's heap — twice for a local\n * destination (`readFileSync`, then a copy) and on top of the controller's own\n * buffer for object storage — and a file over 2 GiB never downloaded at all:\n * `readFileSync` throws `ERR_FS_FILE_TOO_LARGE` there.\n *\n * A local file is read from disk as the response is written, and so is an\n * object through a controller with `getObjectStream` (the built-in S3 and GCS\n * ones). A custom controller without it still arrives through\n * {@link StorageController.getObject}, a buffered `File` by contract;\n * streaming that avoids the second copy.\n */\nexport async function openBackupStream(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ stream: ReadableStream<Uint8Array>; size: number; name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n if (!resolved) return null;\n const { size } = await fs.promises.stat(resolved);\n return { stream: fileStream(resolved), size, name: path.basename(resolved) };\n }\n\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n const name = key.split(\"/\").pop() || key;\n if (storage.getObjectStream) {\n const object = await storage.getObjectStream(key, { bucket: dest.bucket });\n return object ? { stream: object.stream, size: object.size, name } : null;\n }\n const file = await storage.getObject(key, dest.bucket);\n if (!file) return null;\n return { stream: file.stream(), size: file.size, name };\n}\n\n/** A file on disk as a web stream, read a chunk at a time as the response is written. */\nfunction fileStream(filePath: string): ReadableStream<Uint8Array> {\n const file = fs.createReadStream(filePath);\n const chunks: AsyncIterator<Buffer> = file[Symbol.asyncIterator]();\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n const next = await chunks.next();\n if (next.done) controller.close();\n else controller.enqueue(next.value);\n },\n cancel() {\n file.destroy();\n }\n });\n}\n","/**\n * The storage controller that reads a backup bucket.\n *\n * A backup destination is its own bucket, named by `BACKUP_DESTINATION`, and\n * the documented backup cron writes to it through a controller it builds from\n * the `S3_*` variables (GCS: application default credentials) — as\n * `rebase db backups list` does. The admin routes have to read it the same way.\n * The app's file storage is not that controller: it is configured for the\n * uploads bucket, it is a local directory in development, and in production\n * without an uploads bucket there is none at all.\n */\nimport { parseEnvBoolean } from \"@rebasepro/types\";\nimport { GCSStorageController } from \"../storage/GCSStorageController\";\nimport { S3StorageController } from \"../storage/S3StorageController\";\nimport type { StorageController } from \"../storage/types\";\nimport { parseBackupDestination, type BackupDestination } from \"./backup-common\";\n\n/** A destination in object storage — `s3://bucket/prefix` or `gs://bucket/prefix`. */\nexport type ObjectBackupDestination = Exclude<BackupDestination, { kind: \"local\" }>;\n\n/** The destination `BACKUP_DESTINATION` names, or `null` when it is unset. */\nexport function backupDestinationFromEnv(env: Record<string, string | undefined>): BackupDestination | null {\n const out = env.BACKUP_DESTINATION?.trim();\n return out ? parseBackupDestination(out) : null;\n}\n\n/** The destination as `BACKUP_DESTINATION` spells it. */\nexport function describeBackupDestination(dest: BackupDestination): string {\n if (dest.kind === \"local\") return dest.path;\n const scheme = dest.kind === \"s3\" ? \"s3\" : \"gs\";\n return `${scheme}://${dest.bucket}${dest.prefix ? `/${dest.prefix}` : \"\"}`;\n}\n\n/**\n * A controller for an object-storage destination's bucket, built from the\n * environment the way the backup cron and `rebase db backups list` build\n * theirs. Throws when an S3 destination has no credentials to read it with.\n */\nexport function backupStorageFromEnv(\n dest: ObjectBackupDestination,\n env: Record<string, string | undefined>\n): StorageController {\n if (dest.kind === \"gcs\") {\n return new GCSStorageController({ type: \"gcs\", bucket: dest.bucket });\n }\n // s3 also covers R2, MinIO and the other S3-compatible stores, through S3_ENDPOINT.\n if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) {\n throw new Error(\n \"An S3 backup destination needs S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in the environment \" +\n \"— the same S3_* variables the backup cron uses.\"\n );\n }\n return new S3StorageController({\n type: \"s3\",\n bucket: dest.bucket,\n region: env.S3_REGION || \"auto\",\n accessKeyId: env.S3_ACCESS_KEY_ID,\n secretAccessKey: env.S3_SECRET_ACCESS_KEY,\n endpoint: env.S3_ENDPOINT,\n // Unset stays unset, so the controller decides from the endpoint.\n forcePathStyle: parseEnvBoolean(env.S3_FORCE_PATH_STYLE)\n });\n}\n","import { Hono, type Context } from \"hono\";\nimport type { BackupDownloadLink, BackupListing, BackupScheduleStatus } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { jwtOf, type JwtService } from \"../auth/jwt\";\nimport type { StorageController } from \"../storage/types\";\nimport { BackupDestination, findBackup, listBackupObjects, openBackupStream } from \"./backup-common\";\nimport { backupStorageFromEnv, describeBackupDestination, type ObjectBackupDestination } from \"./backup-storage\";\n\nexport interface BackupRoutesConfig {\n /**\n * Resolve the current backup destination, or `null` when backups are not\n * configured (`BACKUP_DESTINATION` unset). Read lazily so a restart isn't\n * required to pick up config.\n */\n getDestination: () => BackupDestination | null;\n /**\n * The controller that reads an object-storage destination. It has to\n * address the destination's own bucket — never the app's file storage,\n * which is another bucket, a local directory, or nothing. Defaults to\n * {@link backupStorageFromEnv} over `process.env`, which is how the backup\n * cron builds the one it writes with.\n */\n storageFor?: (dest: ObjectBackupDestination) => StorageController;\n /**\n * The scheduled backup job and its last run — see `readBackupSchedule`.\n * Omitted where this process has no cron scheduler; the listing then\n * answers `schedule: null`.\n */\n getSchedule?: () => Promise<BackupScheduleStatus | null>;\n /**\n * True when this process does not run the scheduled jobs (the `api` role\n * of a split deployment). A local destination read here is then this\n * process's disk, not the one the scheduled backup writes to.\n */\n scheduledElsewhere?: boolean;\n}\n\n/** Why a destination could not be read, as the answer says it. */\nfunction unreadable(dest: BackupDestination, err: unknown): ApiError {\n const reason = err instanceof Error ? err.message : String(err);\n return ApiError.serviceUnavailable(`Cannot read the backups at ${describeBackupDestination(dest)}: ${reason}`);\n}\n\n// ─── Download links ──────────────────────────────────────────────────\n\n/**\n * The `purpose` of a download link's token. `verifyAccessToken` refuses\n * any token with a purpose, and the storage, MFA and MCP verifiers each demand\n * their own, so a link is never a session or any other credential, and none of\n * those is a link.\n */\nconst BACKUP_DOWNLOAD_PURPOSE = \"backup-download\";\n/** The token's audience: the one route that redeems it. */\nconst BACKUP_DOWNLOAD_AUDIENCE = \"rebase:admin/backups/download\";\n/** How long a link works. Long enough to start the download, not to pass around. */\nexport const BACKUP_DOWNLOAD_LINK_TTL_SECONDS = 60;\n\n/** What a link grants: one backup, at one destination. */\nfunction linkScope(dest: BackupDestination, key: string): string {\n return `backup:${describeBackupDestination(dest)}:${key}`;\n}\n\n/**\n * Whether `token` is a live link for `key` at `dest`, signed by `jwt` — the\n * backend answering. Never throws: anything else is a no.\n */\nasync function linkGrants(jwt: JwtService, token: string, dest: BackupDestination, key: string): Promise<boolean> {\n if (!jwt.isConfigured()) return false;\n const claims = await jwt.verifyPurposeToken<{ aud?: unknown; scope?: unknown }>(BACKUP_DOWNLOAD_PURPOSE, token)\n .catch(() => null);\n return claims?.aud === BACKUP_DOWNLOAD_AUDIENCE && claims.scope === linkScope(dest, key);\n}\n\n/** The configured destination, or the 400 every backup route answers without one. */\nfunction destinationOf(config: Pick<BackupRoutesConfig, \"getDestination\">): BackupDestination {\n const dest = config.getDestination();\n if (!dest) throw ApiError.badRequest(\"Backups are not configured (set BACKUP_DESTINATION).\");\n return dest;\n}\n\n/** {@link BackupRoutesConfig.storageFor}, with its default, for a destination that has a controller. */\nfunction storageResolver(config: Pick<BackupRoutesConfig, \"storageFor\">): (dest: BackupDestination) => StorageController | undefined {\n const storageFor = config.storageFor ?? ((dest: ObjectBackupDestination) => backupStorageFromEnv(dest, process.env));\n /** The controller for `dest`, or none for a local one. A destination it cannot read is an error, never `[]`. */\n return (dest) => {\n if (dest.kind === \"local\") return undefined;\n try {\n return storageFor(dest);\n } catch (err) {\n throw unreadable(dest, err);\n }\n };\n}\n\n/** One backup's bytes as the response, streamed: a dump is the size of the database. See `openBackupStream`. */\nasync function serveBackup(c: Context<HonoEnv>, dest: BackupDestination, key: string, storage: StorageController | undefined): Promise<Response> {\n const result = await openBackupStream(dest, key, storage);\n if (!result) {\n throw ApiError.notFound(`Backup not found: ${key}`);\n }\n c.header(\"Content-Type\", \"application/octet-stream\");\n c.header(\"Content-Length\", String(result.size));\n c.header(\"Content-Disposition\", `attachment; filename=\"${result.name}\"`);\n // A whole database, and on a link a URL that grants it: nothing in between keeps a copy.\n c.header(\"Cache-Control\", \"no-store\");\n return c.body(result.stream);\n}\n\n/**\n * `GET /download?key=…&token=…`: a download link spent in place of the bearer\n * header, for the one backup it names.\n *\n * Mount it at the backups prefix AHEAD of every gate on the admin prefix: the\n * auth adapter's own admin routers authenticate everything under `/admin`\n * before the Backups gate is reached. A request with no `token` passes\n * through untouched; one with a token is answered here — the backup, or a\n * 401 — and never falls through to the header: a bad link is a bad link.\n *\n * A browser cannot put a header on a navigation, so before links the SDK\n * fetched the dump and held it whole in the tab as a Blob. With a link the\n * browser streams it to disk.\n */\nexport function createBackupLinkRoutes(config: Pick<BackupRoutesConfig, \"getDestination\" | \"storageFor\">): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n const storageOf = storageResolver(config);\n router.get(\"/download\", async (c, next) => {\n const token = c.req.query(\"token\");\n if (token === undefined) return next();\n const dest = destinationOf(config);\n const key = c.req.query(\"key\") ?? \"\";\n if (!(await linkGrants(jwtOf(c), token, dest, key))) {\n throw ApiError.unauthenticated(\n \"This download link is not valid for this backup, or it has expired. Links last \" +\n `${BACKUP_DOWNLOAD_LINK_TTL_SECONDS} seconds: ask for a new one.`\n );\n }\n return serveBackup(c, dest, key, storageOf(dest));\n });\n return router;\n}\n\n/**\n * Admin REST routes for the Backups panel.\n *\n * Routes (mounted under `/admin/backups`, admin-guarded by the caller):\n * GET / → list available backups, and the scheduled job's last run\n * GET /download → download a backup's bytes (?key=…)\n * POST /download-url → a 60-second link to one backup, `{ key }` → {@link BackupDownloadLink}\n *\n * The link is spent at `GET /download?key=…&token=…`, which\n * {@link createBackupLinkRoutes} answers ahead of the gate.\n */\nexport function createBackupRoutes(config: BackupRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n const storageOf = storageResolver(config);\n\n router.get(\"/\", async (c) => {\n // Read whether or not a destination is configured: a backup cron with\n // no BACKUP_DESTINATION fails every run, and that is exactly what the\n // panel has to be able to say.\n const schedule = config.getSchedule ? await config.getSchedule() : null;\n const dest = config.getDestination();\n if (!dest) {\n return c.json({ backups: [], destinationKind: \"local\", configured: false, schedule } satisfies BackupListing);\n }\n const backups = await listBackupObjects(dest, storageOf(dest)).catch((err: unknown) => {\n throw unreadable(dest, err);\n });\n // Only where there is a scheduled backup to write elsewhere.\n const localDiskOfThisProcess = dest.kind === \"local\" && config.scheduledElsewhere === true && schedule !== null;\n return c.json({\n backups,\n destinationKind: dest.kind,\n configured: true,\n schedule,\n ...(localDiskOfThisProcess ? { localDiskOfThisProcess } : {})\n } satisfies BackupListing);\n });\n\n router.get(\"/download\", async (c) => {\n const dest = destinationOf(config);\n const key = c.req.query(\"key\");\n if (!key) {\n throw ApiError.badRequest(\"Missing 'key' query parameter.\");\n }\n return serveBackup(c, dest, key, storageOf(dest));\n });\n\n router.post(\"/download-url\", async (c) => {\n const dest = destinationOf(config);\n const body: unknown = await c.req.json().catch(() => null);\n const key = typeof body === \"object\" && body !== null && \"key\" in body ? body.key : undefined;\n if (typeof key !== \"string\" || !key) {\n throw ApiError.badRequest(\"Send the backup's key: { \\\"key\\\": \\\"…\\\" }.\");\n }\n // The download route's own guards, so a link is never minted for a\n // key it would refuse; and the backup is there, so the browser is not\n // sent to a 404 in place of a file.\n if (!(await findBackup(dest, key, storageOf(dest)))) {\n throw ApiError.notFound(`Backup not found: ${key}`);\n }\n const jwt = jwtOf(c);\n if (!jwt.isConfigured()) {\n throw ApiError.serviceUnavailable(\n \"Download links are signed with the JWT secret, and this server has none (auth.jwtSecret). \" +\n \"Download through GET /download with the Authorization header instead.\"\n );\n }\n const token = await jwt.signPurposeToken(\n BACKUP_DOWNLOAD_PURPOSE,\n { aud: BACKUP_DOWNLOAD_AUDIENCE, scope: linkScope(dest, key) },\n BACKUP_DOWNLOAD_LINK_TTL_SECONDS\n );\n const download = new URL(\"download\", c.req.url).pathname;\n return c.json({\n url: `${download}?key=${encodeURIComponent(key)}&token=${encodeURIComponent(token)}`,\n token,\n expiresIn: BACKUP_DOWNLOAD_LINK_TTL_SECONDS\n } satisfies BackupDownloadLink);\n });\n\n return router;\n}\n","/**\n * The scheduled backup job, as the Backups panel reports it.\n *\n * A backup that fails every night has to be visible where the backups are\n * listed. Before this the panel read the destination and nothing else, so a\n * cron that could not run `pg_dump` left an empty list under \"wait for the next\n * scheduled run\" — while each failure sat in `cron_logs`, one panel away.\n */\nimport type {\n BackupRunOutcome,\n BackupScheduleStatus,\n CronJobDefinition,\n CronJobLogEntry,\n CronJobStatus,\n RejectedCronJob\n} from \"@rebasepro/types\";\nimport { isAlreadyExecutingSkip } from \"../cron/cron-scheduler\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The mark `createBackupCron` (in `@rebasepro/server-postgres`) puts on the\n * definition it returns.\n *\n * A registry symbol, so it is the same value across two installed copies of\n * either package, and neither package has to import the other: `server` must\n * not depend on `server-postgres`. The job cannot be recognised any other way —\n * its id is the cron file's name and its display name is configurable.\n */\nexport const BACKUP_CRON_MARK: unique symbol = Symbol.for(\"rebase.backupCron\");\n\n/** Whether a cron definition is the scheduled backup. */\nexport function isBackupCronDefinition(definition: CronJobDefinition): boolean {\n return Reflect.get(definition, BACKUP_CRON_MARK) === true;\n}\n\n/** The part of the cron scheduler this reads. */\nexport interface BackupScheduleSource {\n jobIdsWhere(predicate: (definition: CronJobDefinition) => boolean): string[];\n rejectedJobsWhere(predicate: (definition: CronJobDefinition) => boolean): RejectedCronJob[];\n fetchJob(id: string): Promise<CronJobStatus | undefined>;\n getJobLogsFromDb(id: string, limit?: number): Promise<CronJobLogEntry[]>;\n}\n\n/**\n * How many recent log entries to look through for the last scheduled run. A\n * skip — a slot that found the previous run still going — is logged as a\n * success with nothing done, and must not stand in for the run it skipped; nor\n * may a run someone started by hand, which is not the schedule's.\n */\nconst RECENT_RUNS = 50;\n\n/** A log entry as the panel reports a run. */\nfunction outcomeOf(entry: CronJobLogEntry): BackupRunOutcome {\n return {\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n success: entry.success,\n ...(entry.success || !entry.error ? {} : { error: entry.error }),\n manual: entry.manual === true\n };\n}\n\n/**\n * The scheduled backup job and its last run, or `null` when the deployment\n * has none — registered or refused.\n */\nexport async function readBackupSchedule(source: BackupScheduleSource): Promise<BackupScheduleStatus | null> {\n const [jobId] = source.jobIdsWhere(isBackupCronDefinition);\n if (jobId === undefined) {\n // A refused job is not registered, and answered `null` here it read\n // as \"no backup cron\" under a hint to add the file the project has.\n const [refused] = source.rejectedJobsWhere(isBackupCronDefinition);\n return refused\n ? { jobId: refused.id, name: refused.name, schedule: refused.schedule, enabled: false, refused: refused.reason }\n : null;\n }\n\n const job = await source.fetchJob(jobId);\n if (!job) return null;\n // Off because its own definition says so — the documented cron file\n // exports `enabled: false` while BACKUP_SCHEDULE is unset — rather than\n // paused by someone. Resuming that in Cron Jobs would run the placeholder.\n const disabledInCode = !job.enabled &&\n source.jobIdsWhere(definition => isBackupCronDefinition(definition) && definition.enabled === false).includes(jobId);\n\n let recent: CronJobLogEntry[];\n let historyError: string | undefined;\n try {\n recent = await source.getJobLogsFromDb(jobId, RECENT_RUNS);\n } catch (err) {\n // Said, not read as \"has not run yet\". The listing itself still\n // answers: the backups at the destination are worth showing.\n logger.error(`[backups] Could not read the run history of the backup job \"${jobId}\"`, { error: err });\n recent = [];\n historyError = \"The backup job's run history could not be read. The server log has the reason.\";\n }\n const runs = recent.filter(entry => !isAlreadyExecutingSkip(entry));\n // \"Last scheduled backup\" is the schedule's: a later \"Run Now\" that worked\n // hid a failed nightly, and a failed test run read as a failed nightly.\n const lastScheduled = runs.find(entry => entry.manual !== true);\n // A run by hand since then is reported too, as what it is.\n const lastManual = runs.find(entry => entry.manual === true);\n const manualSince = lastManual && (!lastScheduled || lastManual.startedAt > lastScheduled.startedAt)\n ? lastManual\n : undefined;\n\n return {\n jobId,\n name: job.name,\n schedule: job.schedule,\n enabled: job.enabled,\n ...(disabledInCode ? { disabledInCode } : {}),\n ...(job.nextRunAt ? { nextRunAt: job.nextRunAt } : {}),\n ...(historyError ? { historyError } : {}),\n ...(lastScheduled ? { lastRun: outcomeOf(lastScheduled) } : {}),\n ...(manualSince ? { lastManualRun: outcomeOf(manualSince) } : {})\n };\n}\n","/**\n * Storage-generic backup helpers and admin routes for the Backups panel.\n * The `pg_dump`/`pg_restore` machinery lives in `@rebasepro/server-postgres`.\n */\nexport * from \"./backup-common\";\nexport { createBackupRoutes, createBackupLinkRoutes, BACKUP_DOWNLOAD_LINK_TTL_SECONDS } from \"./backup-routes\";\nexport { backupDestinationFromEnv, backupStorageFromEnv } from \"./backup-storage\";\nexport type { ObjectBackupDestination } from \"./backup-storage\";\nexport type { BackupRoutesConfig } from \"./backup-routes\";\nexport { readBackupSchedule, isBackupCronDefinition, BACKUP_CRON_MARK } from \"./backup-schedule\";\nexport type { BackupScheduleSource } from \"./backup-schedule\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,uBAAuB,KAAgC;CACnE,MAAM,KAAK,IAAI,MAAM,yBAAyB;CAC9C,IAAI,IAAI,OAAO;EAAE,MAAM;EAAM,QAAQ,GAAG;EAAI,QAAQ,GAAG,EAAE,CAAC,QAAQ,QAAQ,EAAE;CAAE;CAC9E,MAAM,MAAM,IAAI,MAAM,yBAAyB;CAC/C,IAAI,KAAK,OAAO;EAAE,MAAM;EAAO,QAAQ,IAAI;EAAI,QAAQ,IAAI,EAAE,CAAC,QAAQ,QAAQ,EAAE;CAAE;CAClF,OAAO;EAAE,MAAM;EAAS,MAAM;CAAI;AACtC;;;;;AAMA,SAAgB,qBAAqB,UAA+B;CAEhE,MAAM,SADO,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,SAAA,CACvB,MAAM,0BAA0B;CACnD,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,GAAG,KAAK,OAAO;CACrB,MAAM,MACF,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,CAAC,EAAA,GACnD,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE;CAC9D,MAAM,OAAO,IAAI,KAAK,GAAG;CACzB,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,IAAI,OAAO;AACjD;;;;;;;;;;;AAYA,IAAM,cAAc;AACpB,IAAM,iBAAiB;AAEvB,SAAS,cAAc,SAAyB;CAC5C,OAAO,QAAQ,MAAM,GAAG,EAAmB,IAAI;AACnD;;AAGA,SAAS,aAAa,KAAsB;CACxC,OAAO,IAAI,SAAS,WAAW,KAAK,IAAI,SAAS,cAAc;AACnE;;;;;AAMA,IAAM,iBAAiB;;;;;;;;;;AAWvB,eAAsB,kBAClB,SACA,QACA,QACiB;CACjB,MAAM,OAAiB,CAAC;CACxB,IAAI;CACJ,KAAK,IAAI,OAAO,GAAG,OAAO,gBAAgB,QAAQ;EAC9C,MAAM,SAAS,MAAM,QAAQ,YAAY,QAAQ;GAAE;GAAQ,YAAY;GAAM;EAAU,CAAC;EACxF,KAAK,MAAM,QAAQ,OAAO,OAAO,KAAK,KAAK,KAAK,QAAQ;EACxD,YAAY,OAAO;EACnB,IAAI,CAAC,WAAW,OAAO;CAC3B;CACA,OAAO,KACH,6BAA6B,OAAO,GAAG,OAAO,SAAS,eAAe,UAAU,KAAK,OAAO,2GAEhG;CACA,OAAO;AACX;;;;;;;;;;;;AAaA,SAAS,kBAAkB,MAA0B,KAAsB;CACvE,IAAI,CAAC,aAAa,GAAG,KAAK,2BAA2B,KAAK,GAAG,GAAG,OAAO;CAEvE,IADiB,IAAI,MAAM,GACvB,CAAA,CAAS,MAAM,YAAY,YAAY,MAAM,YAAY,OAAO,YAAY,IAAI,GAAG,OAAO;CAC9F,OAAO,KAAK,WAAW,MAAM,IAAI,WAAW,GAAG,KAAK,OAAO,EAAE;AACjE;;;;;;AAOA,eAAsB,kBAClB,MACA,SACqB;CACrB,IAAI,KAAK,SAAS,SAAS;EAKvB,MAAM,OAAO,MAAM,GAAG,SAAS,KAAK,KAAK,IAAI,CAAC,CAAC,YAAY,IAAI;EAC/D,IAAI,CAAC,MAAM,OAAO,CAAC;EACnB,MAAM,MAAM,KAAK,YAAY,IAAI,KAAK,OAAO,KAAK,QAAQ,KAAK,IAAI;EACnE,MAAM,QAAQ,MAAM,GAAG,SAAS,QAAQ,GAAG;EAC3C,MAAM,UAAU,IAAI,IAAI,KAAK;EAC7B,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,SAAS,WAAW,CAAC;EACzD,MAAM,QAAQ,MAAM,WAAW,OAAO,yBAAyB,MAAM,GAAG,SAAS,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC;EACxG,OAAO,MACF,KAAK,GAAG,MAAkB;GACvB,MAAM,OAAO,KAAK,KAAK,KAAK,CAAC;GAC7B,MAAM,KAAK,MAAM;GACjB,MAAM,YAAY,qBAAqB,CAAC,KAAK,GAAG;GAChD,MAAM,UAAU,cAAc,CAAC;GAC/B,OAAO;IACH,KAAK;IACL,MAAM;IACN,WAAW,GAAG;IACd,WAAW,UAAU,YAAY;IACjC,iBAAiB;IACjB,GAAI,QAAQ,IAAI,OAAO,IAAI,EAAE,YAAY,KAAK,KAAK,KAAK,OAAO,EAAE,IAAI,CAAC;GAC1E;EACJ,CAAC,CAAC,CACD,KAAK,QAAQ;CACtB;CAKA,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gBAAgB,IAAI,CAAC;CACnD,MAAM,OAAO,MAAM,kBAAkB,SAAS,KAAK,SAAS,GAAG,KAAK,OAAO,KAAK,IAAI,KAAK,MAAM;CAC/F,MAAM,UAAU,IAAI,IAAI,IAAI;CAC5B,OAAO,KACF,QAAQ,QAAQ,IAAI,SAAS,WAAW,CAAC,CAAC,CAC1C,KAAK,QAAoB;EACtB,MAAM,YAAY,qBAAqB,GAAG;EAC1C,MAAM,UAAU,cAAc,GAAG;EACjC,OAAO;GACH;GACA,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;GAC9B,WAAW,YAAY,UAAU,YAAY,IAAI,KAAA;GACjD,iBAAiB,KAAK;GACtB,GAAI,QAAQ,IAAI,OAAO,IAAI,EAAE,YAAY,QAAQ,IAAI,CAAC;EAC1D;CACJ,CAAC,CAAC,CACD,KAAK,QAAQ;AACtB;;AAGA,IAAM,yBAAyB;;AAG/B,eAAe,WAAiB,OAAqB,OAAe,IAA2C;CAC3G,MAAM,UAAU,IAAI,MAAS,MAAM,MAAM;CACzC,IAAI,OAAO;CACX,MAAM,SAAS,YAAY;EACvB,OAAO,OAAO,MAAM,QAAQ;GACxB,MAAM,IAAI;GACV,QAAQ,KAAK,MAAM,GAAG,MAAM,EAAE;EAClC;CACJ;CACA,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,GAAG,MAAM,CAAC;CAC/E,OAAO;AACX;AAEA,SAAS,gBAAgB,MAAsE;CAE3F,OAAO,6BADQ,KAAK,SAAS,OAAO,OAAO,KACA,KAAK,KAAK,SAAS,KAAK,SAAS,IAAI,KAAK,WAAW,GAAG;AACvG;AAEA,SAAS,SAAS,GAAe,GAAuB;CACpD,MAAM,KAAK,EAAE,YAAY,KAAK,MAAM,EAAE,SAAS,IAAI;CAEnD,QADW,EAAE,YAAY,KAAK,MAAM,EAAE,SAAS,IAAI,KACvC;AAChB;;;;;;AAOA,SAAS,uBAAuB,MAAwB,KAA4B;CAEhF,MAAM,OADO,GAAG,WAAW,KAAK,IAAI,IAAI,GAAG,SAAS,KAAK,IAAI,IAAI,KAAA,EAC/C,YAAY,IAAI,KAAK,OAAO,KAAK,QAAQ,KAAK,IAAI;CACpE,MAAM,cAAc,KAAK,QAAQ,GAAG;CACpC,MAAM,WAAW,KAAK,QAAQ,GAAG;CAEjC,IAAI,CAAC,SAAS,WAAW,cAAc,KAAK,GAAG,KAAK,CAAC,aAAa,QAAQ,GACtE,OAAO;CAEX,OAAO,GAAG,WAAW,QAAQ,IAAI,WAAW;AAChD;;AAGA,eAAsB,gBAClB,MACA,KACA,SACgE;CAChE,IAAI,KAAK,SAAS,SAAS;EACvB,MAAM,WAAW,uBAAuB,MAAM,GAAG;EACjD,IAAI,CAAC,UAAU,OAAO;EACtB,OAAO;GAAE,OAAO,IAAI,WAAW,GAAG,aAAa,QAAQ,CAAC;GAAG,MAAM,KAAK,SAAS,QAAQ;EAAE;CAC7F;CAEA,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gBAAgB,IAAI,CAAC;CACnD,IAAI,CAAC,kBAAkB,MAAM,GAAG,GAAG,OAAO;CAC1C,MAAM,OAAO,MAAM,QAAQ,UAAU,KAAK,KAAK,MAAM;CACrD,IAAI,CAAC,MAAM,OAAO;CAClB,OAAO;EAAE,OAAO,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;EAAG,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CAAI;AAChG;;;;;;;AAQA,eAAsB,WAClB,MACA,KACA,SACgC;CAChC,IAAI,KAAK,SAAS,SAAS;EACvB,MAAM,WAAW,uBAAuB,MAAM,GAAG;EACjD,OAAO,WAAW,EAAE,MAAM,KAAK,SAAS,QAAQ,EAAE,IAAI;CAC1D;CACA,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gBAAgB,IAAI,CAAC;CACnD,IAAI,CAAC,kBAAkB,MAAM,GAAG,GAAG,OAAO;CAC1C,IAAI,QAAQ,cAAc,CAAE,MAAM,QAAQ,WAAW,KAAK,KAAK,MAAM,GAAI,OAAO;CAChF,OAAO,EAAE,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,IAAI;AAC/C;;;;;;;;;;;;;;;;;AAkBA,eAAsB,iBAClB,MACA,KACA,SACkF;CAClF,IAAI,KAAK,SAAS,SAAS;EACvB,MAAM,WAAW,uBAAuB,MAAM,GAAG;EACjD,IAAI,CAAC,UAAU,OAAO;EACtB,MAAM,EAAE,SAAS,MAAM,GAAG,SAAS,KAAK,QAAQ;EAChD,OAAO;GAAE,QAAQ,WAAW,QAAQ;GAAG;GAAM,MAAM,KAAK,SAAS,QAAQ;EAAE;CAC/E;CAEA,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gBAAgB,IAAI,CAAC;CACnD,IAAI,CAAC,kBAAkB,MAAM,GAAG,GAAG,OAAO;CAC1C,MAAM,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CACrC,IAAI,QAAQ,iBAAiB;EACzB,MAAM,SAAS,MAAM,QAAQ,gBAAgB,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC;EACzE,OAAO,SAAS;GAAE,QAAQ,OAAO;GAAQ,MAAM,OAAO;GAAM;EAAK,IAAI;CACzE;CACA,MAAM,OAAO,MAAM,QAAQ,UAAU,KAAK,KAAK,MAAM;CACrD,IAAI,CAAC,MAAM,OAAO;CAClB,OAAO;EAAE,QAAQ,KAAK,OAAO;EAAG,MAAM,KAAK;EAAM;CAAK;AAC1D;;AAGA,SAAS,WAAW,UAA8C;CAC9D,MAAM,OAAO,GAAG,iBAAiB,QAAQ;CACzC,MAAM,SAAgC,KAAK,OAAO,cAAc,CAAC;CACjE,OAAO,IAAI,eAA2B;EAClC,MAAM,KAAK,YAAY;GACnB,MAAM,OAAO,MAAM,OAAO,KAAK;GAC/B,IAAI,KAAK,MAAM,WAAW,MAAM;QAC3B,WAAW,QAAQ,KAAK,KAAK;EACtC;EACA,SAAS;GACL,KAAK,QAAQ;EACjB;CACJ,CAAC;AACL;;;;;;;;;;;;;;;AC3SA,SAAgB,yBAAyB,KAAmE;CACxG,MAAM,MAAM,IAAI,oBAAoB,KAAK;CACzC,OAAO,MAAM,uBAAuB,GAAG,IAAI;AAC/C;;AAGA,SAAgB,0BAA0B,MAAiC;CACvE,IAAI,KAAK,SAAS,SAAS,OAAO,KAAK;CAEvC,OAAO,GADQ,KAAK,SAAS,OAAO,OAAO,KAC1B,KAAK,KAAK,SAAS,KAAK,SAAS,IAAI,KAAK,WAAW;AAC1E;;;;;;AAOA,SAAgB,qBACZ,MACA,KACiB;CACjB,IAAI,KAAK,SAAS,OACd,OAAO,IAAI,qBAAqB;EAAE,MAAM;EAAO,QAAQ,KAAK;CAAO,CAAC;CAGxE,IAAI,CAAC,IAAI,oBAAoB,CAAC,IAAI,sBAC9B,MAAM,IAAI,MACN,6IAEJ;CAEJ,OAAO,IAAI,oBAAoB;EAC3B,MAAM;EACN,QAAQ,KAAK;EACb,QAAQ,IAAI,aAAa;EACzB,aAAa,IAAI;EACjB,iBAAiB,IAAI;EACrB,UAAU,IAAI;EAEd,gBAAgB,gBAAgB,IAAI,mBAAmB;CAC3D,CAAC;AACL;;;;ACvBA,SAAS,WAAW,MAAyB,KAAwB;CACjE,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC9D,OAAO,SAAS,mBAAmB,8BAA8B,0BAA0B,IAAI,EAAE,IAAI,QAAQ;AACjH;;;;;;;AAUA,IAAM,0BAA0B;;AAEhC,IAAM,2BAA2B;;AAKjC,SAAS,UAAU,MAAyB,KAAqB;CAC7D,OAAO,UAAU,0BAA0B,IAAI,EAAE,GAAG;AACxD;;;;;AAMA,eAAe,WAAW,KAAiB,OAAe,MAAyB,KAA+B;CAC9G,IAAI,CAAC,IAAI,aAAa,GAAG,OAAO;CAChC,MAAM,SAAS,MAAM,IAAI,mBAAuD,yBAAyB,KAAK,CAAC,CAC1G,YAAY,IAAI;CACrB,OAAO,QAAQ,QAAQ,4BAA4B,OAAO,UAAU,UAAU,MAAM,GAAG;AAC3F;;AAGA,SAAS,cAAc,QAAuE;CAC1F,MAAM,OAAO,OAAO,eAAe;CACnC,IAAI,CAAC,MAAM,MAAM,SAAS,WAAW,sDAAsD;CAC3F,OAAO;AACX;;AAGA,SAAS,gBAAgB,QAA4G;CACjI,MAAM,aAAa,OAAO,gBAAgB,SAAkC,qBAAqB,MAAM,QAAQ,GAAG;;CAElH,QAAQ,SAAS;EACb,IAAI,KAAK,SAAS,SAAS,OAAO,KAAA;EAClC,IAAI;GACA,OAAO,WAAW,IAAI;EAC1B,SAAS,KAAK;GACV,MAAM,WAAW,MAAM,GAAG;EAC9B;CACJ;AACJ;;AAGA,eAAe,YAAY,GAAqB,MAAyB,KAAa,SAA2D;CAC7I,MAAM,SAAS,MAAM,iBAAiB,MAAM,KAAK,OAAO;CACxD,IAAI,CAAC,QACD,MAAM,SAAS,SAAS,qBAAqB,KAAK;CAEtD,EAAE,OAAO,gBAAgB,0BAA0B;CACnD,EAAE,OAAO,kBAAkB,OAAO,OAAO,IAAI,CAAC;CAC9C,EAAE,OAAO,uBAAuB,yBAAyB,OAAO,KAAK,EAAE;CAEvE,EAAE,OAAO,iBAAiB,UAAU;CACpC,OAAO,EAAE,KAAK,OAAO,MAAM;AAC/B;;;;;;;;;;;;;;;AAgBA,SAAgB,uBAAuB,QAAkF;CACrH,MAAM,SAAS,IAAI,KAAc;CACjC,OAAO,QAAQ,YAAY;CAC3B,MAAM,YAAY,gBAAgB,MAAM;CACxC,OAAO,IAAI,aAAa,OAAO,GAAG,SAAS;EACvC,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO;EACjC,IAAI,UAAU,KAAA,GAAW,OAAO,KAAK;EACrC,MAAM,OAAO,cAAc,MAAM;EACjC,MAAM,MAAM,EAAE,IAAI,MAAM,KAAK,KAAK;EAClC,IAAI,CAAE,MAAM,WAAW,MAAM,CAAC,GAAG,OAAO,MAAM,GAAG,GAC7C,MAAM,SAAS,gBACX,+GAEJ;EAEJ,OAAO,YAAY,GAAG,MAAM,KAAK,UAAU,IAAI,CAAC;CACpD,CAAC;CACD,OAAO;AACX;;;;;;;;;;;;AAaA,SAAgB,mBAAmB,QAA2C;CAC1E,MAAM,SAAS,IAAI,KAAc;CACjC,OAAO,QAAQ,YAAY;CAE3B,MAAM,YAAY,gBAAgB,MAAM;CAExC,OAAO,IAAI,KAAK,OAAO,MAAM;EAIzB,MAAM,WAAW,OAAO,cAAc,MAAM,OAAO,YAAY,IAAI;EACnE,MAAM,OAAO,OAAO,eAAe;EACnC,IAAI,CAAC,MACD,OAAO,EAAE,KAAK;GAAE,SAAS,CAAC;GAAG,iBAAiB;GAAS,YAAY;GAAO;EAAS,CAAyB;EAEhH,MAAM,UAAU,MAAM,kBAAkB,MAAM,UAAU,IAAI,CAAC,CAAC,CAAC,OAAO,QAAiB;GACnF,MAAM,WAAW,MAAM,GAAG;EAC9B,CAAC;EAED,MAAM,yBAAyB,KAAK,SAAS,WAAW,OAAO,uBAAuB,QAAQ,aAAa;EAC3G,OAAO,EAAE,KAAK;GACV;GACA,iBAAiB,KAAK;GACtB,YAAY;GACZ;GACA,GAAI,yBAAyB,EAAE,uBAAuB,IAAI,CAAC;EAC/D,CAAyB;CAC7B,CAAC;CAED,OAAO,IAAI,aAAa,OAAO,MAAM;EACjC,MAAM,OAAO,cAAc,MAAM;EACjC,MAAM,MAAM,EAAE,IAAI,MAAM,KAAK;EAC7B,IAAI,CAAC,KACD,MAAM,SAAS,WAAW,gCAAgC;EAE9D,OAAO,YAAY,GAAG,MAAM,KAAK,UAAU,IAAI,CAAC;CACpD,CAAC;CAED,OAAO,KAAK,iBAAiB,OAAO,MAAM;EACtC,MAAM,OAAO,cAAc,MAAM;EACjC,MAAM,OAAgB,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,YAAY,IAAI;EACzD,MAAM,MAAM,OAAO,SAAS,YAAY,SAAS,QAAQ,SAAS,OAAO,KAAK,MAAM,KAAA;EACpF,IAAI,OAAO,QAAQ,YAAY,CAAC,KAC5B,MAAM,SAAS,WAAW,4CAA4C;EAK1E,IAAI,CAAE,MAAM,WAAW,MAAM,KAAK,UAAU,IAAI,CAAC,GAC7C,MAAM,SAAS,SAAS,qBAAqB,KAAK;EAEtD,MAAM,MAAM,MAAM,CAAC;EACnB,IAAI,CAAC,IAAI,aAAa,GAClB,MAAM,SAAS,mBACX,iKAEJ;EAEJ,MAAM,QAAQ,MAAM,IAAI,iBACpB,yBACA;GAAE,KAAK;GAA0B,OAAO,UAAU,MAAM,GAAG;EAAE,GAAA,EAEjE;EACA,MAAM,WAAW,IAAI,IAAI,YAAY,EAAE,IAAI,GAAG,CAAC,CAAC;EAChD,OAAO,EAAE,KAAK;GACV,KAAK,GAAG,SAAS,OAAO,mBAAmB,GAAG,EAAE,SAAS,mBAAmB,KAAK;GACjF;GACA,WAAA;EACJ,CAA8B;CAClC,CAAC;CAED,OAAO;AACX;;;;;;;;;;;;ACtMA,IAAa,mBAAkC,OAAO,IAAI,mBAAmB;;AAG7E,SAAgB,uBAAuB,YAAwC;CAC3E,OAAO,QAAQ,IAAI,YAAY,gBAAgB,MAAM;AACzD;;;;;;;AAgBA,IAAM,cAAc;;AAGpB,SAAS,UAAU,OAA0C;CACzD,OAAO;EACH,WAAW,MAAM;EACjB,YAAY,MAAM;EAClB,SAAS,MAAM;EACf,GAAI,MAAM,WAAW,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,MAAM,MAAM;EAC9D,QAAQ,MAAM,WAAW;CAC7B;AACJ;;;;;AAMA,eAAsB,mBAAmB,QAAoE;CACzG,MAAM,CAAC,SAAS,OAAO,YAAY,sBAAsB;CACzD,IAAI,UAAU,KAAA,GAAW;EAGrB,MAAM,CAAC,WAAW,OAAO,kBAAkB,sBAAsB;EACjE,OAAO,UACD;GAAE,OAAO,QAAQ;GAAI,MAAM,QAAQ;GAAM,UAAU,QAAQ;GAAU,SAAS;GAAO,SAAS,QAAQ;EAAO,IAC7G;CACV;CAEA,MAAM,MAAM,MAAM,OAAO,SAAS,KAAK;CACvC,IAAI,CAAC,KAAK,OAAO;CAIjB,MAAM,iBAAiB,CAAC,IAAI,WACxB,OAAO,aAAY,eAAc,uBAAuB,UAAU,KAAK,WAAW,YAAY,KAAK,CAAC,CAAC,SAAS,KAAK;CAEvH,IAAI;CACJ,IAAI;CACJ,IAAI;EACA,SAAS,MAAM,OAAO,iBAAiB,OAAO,WAAW;CAC7D,SAAS,KAAK;EAGV,OAAO,MAAM,+DAA+D,MAAM,IAAI,EAAE,OAAO,IAAI,CAAC;EACpG,SAAS,CAAC;EACV,eAAe;CACnB;CACA,MAAM,OAAO,OAAO,QAAO,UAAS,CAAC,uBAAuB,KAAK,CAAC;CAGlE,MAAM,gBAAgB,KAAK,MAAK,UAAS,MAAM,WAAW,IAAI;CAE9D,MAAM,aAAa,KAAK,MAAK,UAAS,MAAM,WAAW,IAAI;CAC3D,MAAM,cAAc,eAAe,CAAC,iBAAiB,WAAW,YAAY,cAAc,aACpF,aACA,KAAA;CAEN,OAAO;EACH;EACA,MAAM,IAAI;EACV,UAAU,IAAI;EACd,SAAS,IAAI;EACb,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;EAC3C,GAAI,IAAI,YAAY,EAAE,WAAW,IAAI,UAAU,IAAI,CAAC;EACpD,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;EACvC,GAAI,gBAAgB,EAAE,SAAS,UAAU,aAAa,EAAE,IAAI,CAAC;EAC7D,GAAI,cAAc,EAAE,eAAe,UAAU,WAAW,EAAE,IAAI,CAAC;CACnE;AACJ"}
@@ -1,10 +1,12 @@
1
1
  import { type Server } from "http";
2
2
  import { Hono } from "hono";
3
+ import { type StorageSourceDefinition } from "@rebasepro/types";
3
4
  import type { RebaseBackendInstance } from "../init.js";
4
5
  import type { HonoEnv } from "../api/types.js";
5
6
  import { type Logger } from "../utils/logger.js";
6
7
  import { type RebaseBootEnv } from "./env.js";
7
8
  import { type LoadedBundle } from "./bundle.js";
9
+ import { resolveStorageSources, type EnvBag } from "./sources.js";
8
10
  import { type InitializedDataSource } from "./driver.js";
9
11
  /** A running runtime, and the handle to stop it. */
10
12
  export interface BootedRuntime {
@@ -163,6 +165,14 @@ export declare function reportBootFailure(err: unknown, log?: BootFailureLog, en
163
165
  * the two need separate sentences: bump the *image*, not the package.json.
164
166
  */
165
167
  export declare function warnOnDriverSkew(dataSources: InitializedDataSource[], runtimeVersion: string | undefined): void;
168
+ /**
169
+ * The storage step of {@link bootFromBundle}: every storage source this process
170
+ * serves, from the declarations and the environment.
171
+ *
172
+ * Local storage that `STORAGE_PATH` does not place keeps its files in
173
+ * {@link defaultStorageDirectory}.
174
+ */
175
+ export declare function resolveBootStorage(bundle: LoadedBundle, env: EnvBag, definitions: StorageSourceDefinition[] | undefined, production: boolean): ReturnType<typeof resolveStorageSources>;
166
176
  /**
167
177
  * Warn about a bundle whose shape makes provisioning impossible.
168
178
  *
@@ -5,8 +5,8 @@ __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
6
  import { r as isAlreadyExecutingSkip } from "./cron-scheduler-D1DT0nw5.js";
7
7
  import { r as logger } from "./logger-Bt32sHFw.js";
8
- import { a as resolveListLimitParam } from "./query-parser-BnhugNwZ.js";
9
- import { i as errorHandler, t as ApiError } from "./errors-CHSJYpoy.js";
8
+ import { a as resolveListLimitParam } from "./query-parser-CGohjyWI.js";
9
+ import { i as errorHandler, t as ApiError } from "./errors-_ytoGi8A.js";
10
10
  import { Hono } from "hono";
11
11
  //#region src/cron/cron-routes.ts
12
12
  var cron_routes_exports = /* @__PURE__ */ __exportAll({ createCronRoutes: () => createCronRoutes });
@@ -116,4 +116,4 @@ function createCronRoutes(scheduler, skipped = 0) {
116
116
  //#endregion
117
117
  export { cron_routes_exports as n, createCronRoutes as t };
118
118
 
119
- //# sourceMappingURL=cron-routes-CPIhdasM.js.map
119
+ //# sourceMappingURL=cron-routes-Dhf82HzT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cron-routes-CPIhdasM.js","names":[],"sources":["../src/cron/cron-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { Context } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { isAlreadyExecutingSkip, type CronScheduler } from \"./cron-scheduler\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { resolveListLimitParam } from \"../api/rest/query-parser\";\nimport { logger } from \"../utils/logger.js\";\n\n/** Who made a change, as `cron_job_state.updated_by` records it. */\nfunction actorOf(c: Context<HonoEnv>): string | undefined {\n const user = c.get(\"user\");\n if (typeof user === \"object\" && user !== null && typeof user.uid === \"string\") return user.uid;\n const apiKey = c.get(\"apiKey\");\n return apiKey ? `api-key:${apiKey.id}` : undefined;\n}\n\n/**\n * The manual trigger's `?async` switch. Absent means synchronous, as it always\n * was; anything other than a plain yes or no is refused rather than read as\n * one, so `?async=yes-please` does not quietly hold the request open.\n */\nfunction readAsyncFlag(raw: string | undefined): boolean {\n if (raw === undefined) return false;\n const value = raw.trim().toLowerCase();\n if (value === \"1\" || value === \"true\") return true;\n if (value === \"\" || value === \"0\" || value === \"false\") return false;\n throw ApiError.badRequest(`?async must be 1, true, 0 or false — got \"${raw}\"`);\n}\n\n/**\n * Create admin REST routes for managing cron jobs.\n *\n * Routes:\n * GET / → list all cron jobs\n * GET /:id → get a single job's status\n * POST /:id/trigger → manually trigger a job (`?async=1`: 202 once it has started)\n * GET /:id/logs → get execution logs for a job\n * PUT /:id → update job (enable/disable, or `null` to follow the code)\n *\n * Every answer about a job's state is read from the store, not from this\n * process's memory: the process serving these routes is often not the one\n * running the jobs (the `api` role never starts its scheduler), and is only\n * ever one replica of several.\n */\nexport function createCronRoutes(scheduler: CronScheduler, skipped = 0): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n // Hono's onError does NOT propagate from parent to child routers, so this\n // child router registers its own handler to format thrown ApiErrors.\n router.onError(errorHandler);\n\n // List all jobs\n router.get(\"/\", async (c) => {\n const jobs = await scheduler.fetchJobs();\n // A file that failed to load is not a job, so it appears nowhere in\n // this list — and \"my job is missing\" and \"my job is not scheduled\"\n // look identical from here. Say how many were dropped, as the\n // functions listing does, so the Studio panel and anyone with curl can\n // see it without boot-log access.\n //\n // Two ways to be dropped, counted together and reported apart. A file\n // the loader could not read has only a count: the failure happened\n // before there was a job to name. A schedule the scheduler refused has a\n // name and a reason — most often \"Expected 5 fields, got 6\", from an\n // expression copied out of a tool that supports seconds — and quoting it\n // here turns a job that silently never fires into a one-line fix.\n const rejected = scheduler.listRejectedJobs();\n const total = skipped + rejected.length;\n return c.json({\n jobs,\n ...(total > 0 && {\n skipped: total,\n ...(rejected.length > 0 && { rejected }),\n note: [\n skipped > 0 ? `${skipped} cron file(s) failed to load` : undefined,\n // Not \"an invalid schedule\": a job is also refused for an\n // unknown timezone or a timeout that is not a positive\n // number, and `rejected` says which.\n rejected.length > 0 ? `${rejected.length} job(s) were refused` : undefined\n ].filter(Boolean).join(\" and \") +\n \" — NOT scheduled. \" +\n (rejected.length > 0\n ? \"See `rejected` for the reason; \"\n : \"\") +\n \"the server log has the rest.\"\n })\n });\n });\n\n // Get single job\n router.get(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const job = await scheduler.fetchJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n return c.json({ job });\n });\n\n // Trigger a job manually\n router.post(\"/:id/trigger\", async (c) => {\n const id = c.req.param(\"id\");\n const asyncRun = readAsyncFlag(c.req.query(\"async\"));\n const job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n // `?async=1`: answer once the run has started rather than when it\n // ends. A long job's synchronous answer is cut by any proxy with a\n // shorter timeout than the job (60 s is common) while the run carries\n // on; this one is not. The outcome is in `GET /:id/logs`, as the entry\n // with `manual: true` and this `startedAt`.\n if (asyncRun) {\n const outcome = await scheduler.startJob(id);\n if (!outcome) throw ApiError.notFound(`Cron job \"${id}\" not found`);\n if (!outcome.started) {\n throw ApiError.conflict(\n `Cron job \"${id}\" is already executing — try again when that run has finished`,\n \"CRON_JOB_ALREADY_EXECUTING\",\n { log: outcome.log }\n );\n }\n return c.json({\n accepted: true,\n startedAt: outcome.startedAt,\n job: await scheduler.fetchJob(id)\n }, 202);\n }\n\n const log = await scheduler.triggerJob(id);\n // Nothing ran, so this is not a success: the job is executing already —\n // on this process, or on whichever one holds its run lease (the worker,\n // seen from the api role). The skip is in the run history either way;\n // `details.log` carries it, and its line names the process.\n if (log && isAlreadyExecutingSkip(log)) {\n throw ApiError.conflict(\n `Cron job \"${id}\" is already executing — try again when that run has finished`,\n \"CRON_JOB_ALREADY_EXECUTING\",\n { log }\n );\n }\n return c.json({ log,\njob: await scheduler.fetchJob(id) });\n });\n\n // Get job logs\n router.get(\"/:id/logs\", async (c) => {\n const id = c.req.param(\"id\");\n // Validated, not `parseInt`-ed. `?limit=abc` used to reach the store as\n // `NaN`, where Postgres refused `LIMIT NaN`, the store swallowed the\n // error and returned `[]` — a 200 with an empty list, which reads as\n // \"this job has never run\". The data plane answers 400 for the same\n // input; so does this now.\n const limit = resolveListLimitParam(c.req.query(\"limit\") ?? null, { defaultLimit: 50 });\n\n const job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n const logs = await scheduler.getJobLogsFromDb(id, limit).catch((err: unknown) => {\n // Not an empty 200: Studio shows that as \"No executions yet\".\n logger.error(`[cron] Could not read the run history of \"${id}\"`, { error: err });\n throw ApiError.serviceUnavailable(\n `The run history of cron job \"${id}\" could not be read. The server log has the reason.`\n );\n });\n return c.json({ logs });\n });\n\n // Enable/disable a job — for every process, and across redeploys\n router.put(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const body: unknown = await c.req.json().catch(() => ({}));\n const enabled = typeof body === \"object\" && body !== null && \"enabled\" in body ? body.enabled : undefined;\n\n if (typeof enabled !== \"boolean\" && enabled !== null) {\n throw ApiError.badRequest(\n \"Missing 'enabled' in body: true or false to override the job's code, or null to follow it again\"\n );\n }\n\n if (!scheduler.getJob(id)) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n try {\n await scheduler.persistJobEnabled(id, enabled, actorOf(c));\n } catch (err) {\n logger.error(`[cron] Could not save the enabled state of \"${id}\"`, { error: err });\n throw ApiError.serviceUnavailable(\n `The change to cron job \"${id}\" could not be saved, so no process was changed. ` +\n \"The server log has the reason.\"\n );\n }\n\n return c.json({ job: await scheduler.fetchJob(id) });\n });\n\n return router;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cron-routes-Dhf82HzT.js","names":[],"sources":["../src/cron/cron-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { Context } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { isAlreadyExecutingSkip, type CronScheduler } from \"./cron-scheduler\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { resolveListLimitParam } from \"../api/rest/query-parser\";\nimport { logger } from \"../utils/logger.js\";\n\n/** Who made a change, as `cron_job_state.updated_by` records it. */\nfunction actorOf(c: Context<HonoEnv>): string | undefined {\n const user = c.get(\"user\");\n if (typeof user === \"object\" && user !== null && typeof user.uid === \"string\") return user.uid;\n const apiKey = c.get(\"apiKey\");\n return apiKey ? `api-key:${apiKey.id}` : undefined;\n}\n\n/**\n * The manual trigger's `?async` switch. Absent means synchronous, as it always\n * was; anything other than a plain yes or no is refused rather than read as\n * one, so `?async=yes-please` does not quietly hold the request open.\n */\nfunction readAsyncFlag(raw: string | undefined): boolean {\n if (raw === undefined) return false;\n const value = raw.trim().toLowerCase();\n if (value === \"1\" || value === \"true\") return true;\n if (value === \"\" || value === \"0\" || value === \"false\") return false;\n throw ApiError.badRequest(`?async must be 1, true, 0 or false — got \"${raw}\"`);\n}\n\n/**\n * Create admin REST routes for managing cron jobs.\n *\n * Routes:\n * GET / → list all cron jobs\n * GET /:id → get a single job's status\n * POST /:id/trigger → manually trigger a job (`?async=1`: 202 once it has started)\n * GET /:id/logs → get execution logs for a job\n * PUT /:id → update job (enable/disable, or `null` to follow the code)\n *\n * Every answer about a job's state is read from the store, not from this\n * process's memory: the process serving these routes is often not the one\n * running the jobs (the `api` role never starts its scheduler), and is only\n * ever one replica of several.\n */\nexport function createCronRoutes(scheduler: CronScheduler, skipped = 0): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n // Hono's onError does NOT propagate from parent to child routers, so this\n // child router registers its own handler to format thrown ApiErrors.\n router.onError(errorHandler);\n\n // List all jobs\n router.get(\"/\", async (c) => {\n const jobs = await scheduler.fetchJobs();\n // A file that failed to load is not a job, so it appears nowhere in\n // this list — and \"my job is missing\" and \"my job is not scheduled\"\n // look identical from here. Say how many were dropped, as the\n // functions listing does, so the Studio panel and anyone with curl can\n // see it without boot-log access.\n //\n // Two ways to be dropped, counted together and reported apart. A file\n // the loader could not read has only a count: the failure happened\n // before there was a job to name. A schedule the scheduler refused has a\n // name and a reason — most often \"Expected 5 fields, got 6\", from an\n // expression copied out of a tool that supports seconds — and quoting it\n // here turns a job that silently never fires into a one-line fix.\n const rejected = scheduler.listRejectedJobs();\n const total = skipped + rejected.length;\n return c.json({\n jobs,\n ...(total > 0 && {\n skipped: total,\n ...(rejected.length > 0 && { rejected }),\n note: [\n skipped > 0 ? `${skipped} cron file(s) failed to load` : undefined,\n // Not \"an invalid schedule\": a job is also refused for an\n // unknown timezone or a timeout that is not a positive\n // number, and `rejected` says which.\n rejected.length > 0 ? `${rejected.length} job(s) were refused` : undefined\n ].filter(Boolean).join(\" and \") +\n \" — NOT scheduled. \" +\n (rejected.length > 0\n ? \"See `rejected` for the reason; \"\n : \"\") +\n \"the server log has the rest.\"\n })\n });\n });\n\n // Get single job\n router.get(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const job = await scheduler.fetchJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n return c.json({ job });\n });\n\n // Trigger a job manually\n router.post(\"/:id/trigger\", async (c) => {\n const id = c.req.param(\"id\");\n const asyncRun = readAsyncFlag(c.req.query(\"async\"));\n const job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n // `?async=1`: answer once the run has started rather than when it\n // ends. A long job's synchronous answer is cut by any proxy with a\n // shorter timeout than the job (60 s is common) while the run carries\n // on; this one is not. The outcome is in `GET /:id/logs`, as the entry\n // with `manual: true` and this `startedAt`.\n if (asyncRun) {\n const outcome = await scheduler.startJob(id);\n if (!outcome) throw ApiError.notFound(`Cron job \"${id}\" not found`);\n if (!outcome.started) {\n throw ApiError.conflict(\n `Cron job \"${id}\" is already executing — try again when that run has finished`,\n \"CRON_JOB_ALREADY_EXECUTING\",\n { log: outcome.log }\n );\n }\n return c.json({\n accepted: true,\n startedAt: outcome.startedAt,\n job: await scheduler.fetchJob(id)\n }, 202);\n }\n\n const log = await scheduler.triggerJob(id);\n // Nothing ran, so this is not a success: the job is executing already —\n // on this process, or on whichever one holds its run lease (the worker,\n // seen from the api role). The skip is in the run history either way;\n // `details.log` carries it, and its line names the process.\n if (log && isAlreadyExecutingSkip(log)) {\n throw ApiError.conflict(\n `Cron job \"${id}\" is already executing — try again when that run has finished`,\n \"CRON_JOB_ALREADY_EXECUTING\",\n { log }\n );\n }\n return c.json({ log,\njob: await scheduler.fetchJob(id) });\n });\n\n // Get job logs\n router.get(\"/:id/logs\", async (c) => {\n const id = c.req.param(\"id\");\n // Validated, not `parseInt`-ed. `?limit=abc` used to reach the store as\n // `NaN`, where Postgres refused `LIMIT NaN`, the store swallowed the\n // error and returned `[]` — a 200 with an empty list, which reads as\n // \"this job has never run\". The data plane answers 400 for the same\n // input; so does this now.\n const limit = resolveListLimitParam(c.req.query(\"limit\") ?? null, { defaultLimit: 50 });\n\n const job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n const logs = await scheduler.getJobLogsFromDb(id, limit).catch((err: unknown) => {\n // Not an empty 200: Studio shows that as \"No executions yet\".\n logger.error(`[cron] Could not read the run history of \"${id}\"`, { error: err });\n throw ApiError.serviceUnavailable(\n `The run history of cron job \"${id}\" could not be read. The server log has the reason.`\n );\n });\n return c.json({ logs });\n });\n\n // Enable/disable a job — for every process, and across redeploys\n router.put(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const body: unknown = await c.req.json().catch(() => ({}));\n const enabled = typeof body === \"object\" && body !== null && \"enabled\" in body ? body.enabled : undefined;\n\n if (typeof enabled !== \"boolean\" && enabled !== null) {\n throw ApiError.badRequest(\n \"Missing 'enabled' in body: true or false to override the job's code, or null to follow it again\"\n );\n }\n\n if (!scheduler.getJob(id)) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n try {\n await scheduler.persistJobEnabled(id, enabled, actorOf(c));\n } catch (err) {\n logger.error(`[cron] Could not save the enabled state of \"${id}\"`, { error: err });\n throw ApiError.serviceUnavailable(\n `The change to cron job \"${id}\" could not be saved, so no process was changed. ` +\n \"The server log has the reason.\"\n );\n }\n\n return c.json({ job: await scheduler.fetchJob(id) });\n });\n\n return router;\n}\n"],"mappings":";;;;;;;;;;;;;AASA,SAAS,QAAQ,GAAyC;CACtD,MAAM,OAAO,EAAE,IAAI,MAAM;CACzB,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,OAAO,KAAK,QAAQ,UAAU,OAAO,KAAK;CAC3F,MAAM,SAAS,EAAE,IAAI,QAAQ;CAC7B,OAAO,SAAS,WAAW,OAAO,OAAO,KAAA;AAC7C;;;;;;AAOA,SAAS,cAAc,KAAkC;CACrD,IAAI,QAAQ,KAAA,GAAW,OAAO;CAC9B,MAAM,QAAQ,IAAI,KAAK,CAAC,CAAC,YAAY;CACrC,IAAI,UAAU,OAAO,UAAU,QAAQ,OAAO;CAC9C,IAAI,UAAU,MAAM,UAAU,OAAO,UAAU,SAAS,OAAO;CAC/D,MAAM,SAAS,WAAW,6CAA6C,IAAI,EAAE;AACjF;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,WAA0B,UAAU,GAAkB;CACnF,MAAM,SAAS,IAAI,KAAc;CAGjC,OAAO,QAAQ,YAAY;CAG3B,OAAO,IAAI,KAAK,OAAO,MAAM;EACzB,MAAM,OAAO,MAAM,UAAU,UAAU;EAavC,MAAM,WAAW,UAAU,iBAAiB;EAC5C,MAAM,QAAQ,UAAU,SAAS;EACjC,OAAO,EAAE,KAAK;GACV;GACA,GAAI,QAAQ,KAAK;IACb,SAAS;IACT,GAAI,SAAS,SAAS,KAAK,EAAE,SAAS;IACtC,MAAM,CACF,UAAU,IAAI,GAAG,QAAQ,gCAAgC,KAAA,GAIzD,SAAS,SAAS,IAAI,GAAG,SAAS,OAAO,wBAAwB,KAAA,CACrE,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,OAAO,IAC1B,wBACC,SAAS,SAAS,IACb,oCACA,MACN;GACR;EACJ,CAAC;CACL,CAAC;CAGD,OAAO,IAAI,QAAQ,OAAO,MAAM;EAC5B,MAAM,KAAK,EAAE,IAAI,MAAM,IAAI;EAC3B,MAAM,MAAM,MAAM,UAAU,SAAS,EAAE;EACvC,IAAI,CAAC,KACD,MAAM,SAAS,SAAS,aAAa,GAAG,YAAY;EAExD,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC;CACzB,CAAC;CAGD,OAAO,KAAK,gBAAgB,OAAO,MAAM;EACrC,MAAM,KAAK,EAAE,IAAI,MAAM,IAAI;EAC3B,MAAM,WAAW,cAAc,EAAE,IAAI,MAAM,OAAO,CAAC;EAEnD,IAAI,CADQ,UAAU,OAAO,EACxB,GACD,MAAM,SAAS,SAAS,aAAa,GAAG,YAAY;EAQxD,IAAI,UAAU;GACV,MAAM,UAAU,MAAM,UAAU,SAAS,EAAE;GAC3C,IAAI,CAAC,SAAS,MAAM,SAAS,SAAS,aAAa,GAAG,YAAY;GAClE,IAAI,CAAC,QAAQ,SACT,MAAM,SAAS,SACX,aAAa,GAAG,gEAChB,8BACA,EAAE,KAAK,QAAQ,IAAI,CACvB;GAEJ,OAAO,EAAE,KAAK;IACV,UAAU;IACV,WAAW,QAAQ;IACnB,KAAK,MAAM,UAAU,SAAS,EAAE;GACpC,GAAG,GAAG;EACV;EAEA,MAAM,MAAM,MAAM,UAAU,WAAW,EAAE;EAKzC,IAAI,OAAO,uBAAuB,GAAG,GACjC,MAAM,SAAS,SACX,aAAa,GAAG,gEAChB,8BACA,EAAE,IAAI,CACV;EAEJ,OAAO,EAAE,KAAK;GAAE;GACxB,KAAK,MAAM,UAAU,SAAS,EAAE;EAAE,CAAC;CAC/B,CAAC;CAGD,OAAO,IAAI,aAAa,OAAO,MAAM;EACjC,MAAM,KAAK,EAAE,IAAI,MAAM,IAAI;EAM3B,MAAM,QAAQ,sBAAsB,EAAE,IAAI,MAAM,OAAO,KAAK,MAAM,EAAE,cAAc,GAAG,CAAC;EAGtF,IAAI,CADQ,UAAU,OAAO,EACxB,GACD,MAAM,SAAS,SAAS,aAAa,GAAG,YAAY;EAGxD,MAAM,OAAO,MAAM,UAAU,iBAAiB,IAAI,KAAK,CAAC,CAAC,OAAO,QAAiB;GAE7E,OAAO,MAAM,6CAA6C,GAAG,IAAI,EAAE,OAAO,IAAI,CAAC;GAC/E,MAAM,SAAS,mBACX,gCAAgC,GAAG,oDACvC;EACJ,CAAC;EACD,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC;CAC1B,CAAC;CAGD,OAAO,IAAI,QAAQ,OAAO,MAAM;EAC5B,MAAM,KAAK,EAAE,IAAI,MAAM,IAAI;EAC3B,MAAM,OAAgB,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,aAAa,CAAC,EAAE;EACzD,MAAM,UAAU,OAAO,SAAS,YAAY,SAAS,QAAQ,aAAa,OAAO,KAAK,UAAU,KAAA;EAEhG,IAAI,OAAO,YAAY,aAAa,YAAY,MAC5C,MAAM,SAAS,WACX,iGACJ;EAGJ,IAAI,CAAC,UAAU,OAAO,EAAE,GACpB,MAAM,SAAS,SAAS,aAAa,GAAG,YAAY;EAGxD,IAAI;GACA,MAAM,UAAU,kBAAkB,IAAI,SAAS,QAAQ,CAAC,CAAC;EAC7D,SAAS,KAAK;GACV,OAAO,MAAM,+CAA+C,GAAG,IAAI,EAAE,OAAO,IAAI,CAAC;GACjF,MAAM,SAAS,mBACX,2BAA2B,GAAG,gFAElC;EACJ;EAEA,OAAO,EAAE,KAAK,EAAE,KAAK,MAAM,UAAU,SAAS,EAAE,EAAE,CAAC;CACvD,CAAC;CAED,OAAO;AACX"}
@@ -26,16 +26,12 @@ var _schemaDriftHinted = false;
26
26
  /**
27
27
  * The schema-drift remedy, in the words that work on *this* database.
28
28
  *
29
- * The three copies of this message hard-coded `Run \`pnpm db:push\``, and on a
30
- * stock scaffold — where the managed PGlite database is the default — that
31
- * command answers `✗ rebase db push does not work on the managed development
32
- * database.` and exits 1. So the one instruction the server gave when a
33
- * developer's schema had drifted was a command their project refuses.
34
- *
35
- * Atlas plans a push by diffing against a second, empty database, and PGlite
36
- * serves exactly one — which is why it cannot run there, and why the remedy
37
- * has to know which database is under this run. There, boot applies additive
38
- * changes, so restarting `rebase dev` *is* the fix.
29
+ * On the managed development database boot applies additive changes, so
30
+ * restarting `rebase dev` is the first fix there, and `rebase db push` — which
31
+ * plans against a throwaway database of its own — applies what boot leaves
32
+ * alone. Elsewhere the push or the migrations are the fix, and a deployed
33
+ * runtime applies the schema at boot, so the remedy has to know which database
34
+ * is under this run.
39
35
  *
40
36
  * `REBASE_DEV_DATABASE_KIND` is set by the CLI from the database it resolved,
41
37
  * the same variable `rebase schema generate`'s closing line already branches
@@ -44,16 +40,13 @@ var _schemaDriftHinted = false;
44
40
  */
45
41
  function schemaDriftRemedy() {
46
42
  if (hostEnv().REBASE_DEV_DATABASE_KIND === "managed") return {
47
- short: "Restart `rebase dev` — boot applies additive schema changes to the managed database.",
43
+ short: "Restart `rebase dev` — boot applies additive schema changes to the managed database — or run `rebase db push` for a change boot leaves alone.",
48
44
  lines: [
49
45
  " Quick fixes (managed development database):",
50
46
  " restart `rebase dev` boot applies additive changes",
51
- " rebase doctor full 3-way drift report",
52
- "",
53
- " `rebase db push` does not run here: Atlas plans against a",
54
- " second, empty database and PGlite serves one. For a change",
55
- " boot leaves alone, use your own Postgres (DATABASE_URL)",
56
- " or `rebase dev --docker`."
47
+ " rebase db push applies what boot leaves alone",
48
+ " (a drop or a retype; it asks first)",
49
+ " rebase doctor full 3-way drift report"
57
50
  ]
58
51
  };
59
52
  return {
@@ -678,4 +671,4 @@ function codeToStatus(code) {
678
671
  //#endregion
679
672
  export { isDataException as a, errorHandler as i, DATABASE_UNAVAILABLE_MESSAGE as n, isDatabaseUnreachable as o, declaredErrorAnswer as r, schemaDriftRemedy as s, ApiError as t };
680
673
 
681
- //# sourceMappingURL=errors-CHSJYpoy.js.map
674
+ //# sourceMappingURL=errors-_ytoGi8A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-_ytoGi8A.js","names":[],"sources":["../src/api/errors.ts"],"sourcesContent":["import type { Context, ErrorHandler } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\nimport { hostEnv } from \"../utils/host\";\n\n/**\n * A stale caller's schema stamp, as the cause of the error it explains.\n *\n * `createSchemaDriftDetector` puts the two stamps on the context when a request\n * carries an `x-rebase-schema` older than this backend's. It lives next door;\n * this half is here because `errors.ts` is in the graph of\n * `@rebasepro/server/functions` and may not reach `@rebasepro/types` at runtime.\n */\nfunction schemaDriftCause(drift: { client: string; server: string } | undefined): {\n code: string;\n message: string;\n clientSchema: string;\n serverSchema: string;\n} | undefined {\n if (!drift) return undefined;\n return {\n code: \"SCHEMA_DRIFT\",\n message:\n `This client was generated against schema ${drift.client}; this backend serves `\n + `${drift.server}. If the field named above was renamed or removed, regenerate the `\n + \"SDK (`rebase generate-sdk`) and rebuild.\",\n clientSchema: drift.client,\n serverSchema: drift.server\n };\n}\n\n/** Tracks whether we've already shown the doctor hint (once per process). */\nlet _schemaDriftHinted = false;\n\n/**\n * The schema-drift remedy, in the words that work on *this* database.\n *\n * On the managed development database boot applies additive changes, so\n * restarting `rebase dev` is the first fix there, and `rebase db push` — which\n * plans against a throwaway database of its own — applies what boot leaves\n * alone. Elsewhere the push or the migrations are the fix, and a deployed\n * runtime applies the schema at boot, so the remedy has to know which database\n * is under this run.\n *\n * `REBASE_DEV_DATABASE_KIND` is set by the CLI from the database it resolved,\n * the same variable `rebase schema generate`'s closing line already branches\n * on. Absent — a deployed backend, a container, anything not started by\n * `rebase dev` — the answer is the general one.\n */\nexport function schemaDriftRemedy(): { short: string; lines: string[] } {\n if (hostEnv().REBASE_DEV_DATABASE_KIND === \"managed\") {\n return {\n short: \"Restart `rebase dev` — boot applies additive schema changes to the managed database — \" +\n \"or run `rebase db push` for a change boot leaves alone.\",\n lines: [\n \" Quick fixes (managed development database):\",\n \" restart `rebase dev` boot applies additive changes\",\n \" rebase db push applies what boot leaves alone\",\n \" (a drop or a retype; it asks first)\",\n \" rebase doctor full 3-way drift report\"\n ]\n };\n }\n\n return {\n short: \"Run `rebase db push` to sync your schema, or `rebase db migrate` to apply pending migrations.\",\n lines: [\n \" Quick fixes (local dev, against DATABASE_URL):\",\n \" rebase db push sync schema to database (dev)\",\n \" rebase db migrate apply pending migrations (prod)\",\n \" rebase doctor full 3-way drift report\",\n \"\",\n \" Managed cloud: the runtime applies schema + RLS at boot\",\n \" (REBASE_MIGRATE_ON_BOOT); redeploy rather than db push,\",\n \" which cannot reach the tenant database.\"\n ]\n };\n}\n\n/** Shape of Postgres / network errors with diagnostic codes */\ninterface PgLikeError {\n code?: string;\n address?: string;\n port?: number;\n message?: string;\n table?: string;\n column?: string;\n schema?: string;\n detail?: string;\n hint?: string;\n constraint?: string;\n}\n\n/** 5-character SQLSTATE, e.g. `42501`, `23505`. */\nconst SQLSTATE_RE = /^[0-9A-Z]{5}$/;\n\n/**\n * What SQLSTATE 25006 means here, in the only terms that help the author fix it.\n *\n * Every request-scoped read runs `withTransaction(..., { accessMode: \"read\n * only\" })`, so a write attempted anywhere under it — including from a\n * `context.data` call inside an `afterRead` callback — is refused by Postgres\n * rather than by us. The callback name is in the message because that is the\n * file the reader has to open, and nothing else on a read path can raise this.\n */\nconst READ_ONLY_TRANSACTION_MESSAGE =\n \"An `afterRead` callback tried to write. Request-scoped reads run in a READ ONLY \" +\n \"transaction, so neither the callback nor anything it calls (context.data included) \" +\n \"may write. Move the write outside the read: enqueue a background job, or use \" +\n \"`rebase.dataAsAdmin` from a job or a custom function.\";\n\n/**\n * Walk the cause chain for the underlying database error, identified by a\n * 5-char SQLSTATE `code`. Drizzle wraps the pg error in `.cause`, and route\n * code sometimes wraps drizzle again, so the real error may sit several\n * levels down.\n */\nfunction extractDbError(error: unknown, depth = 0): PgLikeError | null {\n if (!error || typeof error !== \"object\" || depth > 8) return null;\n const e = error as PgLikeError & { cause?: unknown };\n if (typeof e.code === \"string\" && SQLSTATE_RE.test(e.code)) return e;\n if (e.cause && typeof e.cause === \"object\") return extractDbError(e.cause, depth + 1);\n return null;\n}\n\n/**\n * Did the database refuse a value as unreadable for its column's type —\n * SQLSTATE class 22, anywhere down the cause chain?\n *\n * For a lookup by an id the caller supplied, that is an answer rather than a\n * failure: `abc` against a UUID key names no row, exactly as a well-formed id\n * nobody has does.\n */\nexport function isDataException(error: unknown): boolean {\n return extractDbError(error)?.code?.startsWith(\"22\") === true;\n}\n\n/**\n * The two ways a request fails before it ever reaches the database, read off\n * the cause chain by pg-pool's own wording (it sets no code on either).\n *\n * - `timeout exceeded when trying to connect` — every pooled connection was\n * busy for the whole of `connectionTimeoutMillis`. pg-pool raises it only\n * from its wait queue, so it means the pool is exhausted: slow queries\n * holding connections, or `DB_POOL_MAX` too small for the load.\n * - `Connection terminated due to connection timeout` — a fresh connection\n * took longer than that to open: the database is slow or far away.\n *\n * Either is load, not a fault. Answered as a 500 they read as a crash and gave\n * a client nothing to back off on; as a 503 with `Retry-After` the SDK's\n * existing retry path takes them (`offline-connectivity.ts`).\n */\nconst POOL_FAILURES: ReadonlyArray<{ message: string; code: \"DB_POOL_EXHAUSTED\" | \"DB_CONNECT_TIMEOUT\" }> = [\n { message: \"timeout exceeded when trying to connect\", code: \"DB_POOL_EXHAUSTED\" },\n { message: \"Connection terminated due to connection timeout\", code: \"DB_CONNECT_TIMEOUT\" }\n];\n\nfunction poolFailureOf(error: unknown, depth = 0): \"DB_POOL_EXHAUSTED\" | \"DB_CONNECT_TIMEOUT\" | undefined {\n if (!error || typeof error !== \"object\" || depth > 8) return undefined;\n const message = Reflect.get(error, \"message\");\n if (typeof message === \"string\") {\n const match = POOL_FAILURES.find(failure => message === failure.message);\n if (match) return match.code;\n }\n return poolFailureOf(Reflect.get(error, \"cause\"), depth + 1);\n}\n\n/** Seconds a client is asked to wait after a pool failure — one pool timeout's worth is too long to make a person wait. */\nconst POOL_RETRY_AFTER_SECONDS = 1;\n\n/**\n * SQLSTATEs that mean the database dropped or refused the connection, not\n * that it judged the statement: class 08 (connection exception), and the\n * three shutdown codes — `57P01` a server shutting down or an administrator\n * ending the session, `57P02` a crash restart, `57P03` a server still\n * starting.\n */\nconst UNREACHABLE_SQLSTATES = new Set([\"57P01\", \"57P02\", \"57P03\"]);\n\n/**\n * What node-postgres says when the socket under a client closed, in the words\n * it sets no code on.\n */\nconst CONNECTION_LOST_MESSAGES = new Set([\n \"Connection terminated unexpectedly\",\n \"Client has encountered a connection error and is not queryable\"\n]);\n\n/** The socket failures Node raises when a host cannot be reached or drops the connection. */\nconst SOCKET_FAILURE_CODES = new Set([\n \"ECONNREFUSED\", \"ECONNRESET\", \"ENOTFOUND\", \"EAI_AGAIN\", \"EHOSTUNREACH\", \"ENETUNREACH\", \"ETIMEDOUT\", \"EPIPE\"\n]);\n\n/**\n * A Node socket failure: one of {@link SOCKET_FAILURE_CODES} on an error the\n * runtime raised for a system call, or the `AggregateError` `net` raises when\n * every address a host resolved to failed that way.\n */\nfunction isSocketFailure(error: object, depth = 0): boolean {\n const code = Reflect.get(error, \"code\");\n if (typeof code === \"string\" && SOCKET_FAILURE_CODES.has(code) && typeof Reflect.get(error, \"syscall\") === \"string\") {\n return true;\n }\n const inner = Reflect.get(error, \"errors\");\n return depth < 2 && Array.isArray(inner) && inner.length > 0 &&\n inner.every(item => typeof item === \"object\" && item !== null && isSocketFailure(item, depth + 1));\n}\n\n/**\n * Did the request fail because the database could not be reached at all — not\n * because it refused the statement, and not because some other service is down?\n *\n * Read off the error the database driver raised, wherever it sits in the cause\n * chain:\n *\n * - a SQLSTATE that means the connection was refused or dropped (class `08`,\n * `57P01`–`57P03`). The database's own word, so it settles the question\n * either way: a SQLSTATE of any other class is the database answering;\n * - node-postgres's own sentence for a socket that closed under a query;\n * - a socket failure (`ECONNREFUSED`, `ENOTFOUND`, …), but only where it can\n * only be the database's: thrown as it is, which is how node-postgres hands\n * back a pool connection it could not open, or as the cause of a failed\n * Drizzle query (the wrapper that carries the statement). Under any other\n * wrapper it belongs to another client: `fetch failed` is a `fetch()` to\n * some other host, and an S3 call that cannot connect carries the AWS SDK's\n * `$metadata`. Answering those \"the database is unreachable\" would send an\n * operator to a database that is fine.\n */\nexport function isDatabaseUnreachable(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null) return false;\n\n const reported = databaseReport(error);\n if (reported !== undefined) return reported;\n\n const socketFailureOf = (candidate: unknown): boolean =>\n typeof candidate === \"object\" && candidate !== null &&\n isSocketFailure(candidate) && Reflect.get(candidate, \"$metadata\") === undefined;\n if (socketFailureOf(error)) return true;\n return typeof Reflect.get(error, \"query\") === \"string\" && socketFailureOf(Reflect.get(error, \"cause\"));\n}\n\n/**\n * What the database itself said about the connection, down the cause chain:\n * `true` for a lost or refused connection, `false` for a SQLSTATE that is the\n * database answering, `undefined` when nothing on the chain is the database's.\n */\nfunction databaseReport(error: object, depth = 0): boolean | undefined {\n const code = Reflect.get(error, \"code\");\n // `EPIPE` has a SQLSTATE's shape, and is a socket's.\n if (typeof code === \"string\" && SQLSTATE_RE.test(code) && !SOCKET_FAILURE_CODES.has(code)) {\n return code.startsWith(\"08\") || UNREACHABLE_SQLSTATES.has(code);\n }\n const message = Reflect.get(error, \"message\");\n if (typeof message === \"string\" && CONNECTION_LOST_MESSAGES.has(message)) return true;\n const cause = Reflect.get(error, \"cause\");\n return depth < 8 && typeof cause === \"object\" && cause !== null ? databaseReport(cause, depth + 1) : undefined;\n}\n\n/**\n * The operator's half of a connection that failed — to the database, or to a\n * service a request called — in a few words: the socket failure and the host\n * it names (`ECONNREFUSED at 127.0.0.1:5432`), or what the database or its\n * client said about the connection.\n */\nfunction describeConnectionFailure(error: unknown, depth = 0): string {\n if (typeof error !== \"object\" || error === null || depth > 8) return \"the connection failed\";\n const code = Reflect.get(error, \"code\");\n const message = Reflect.get(error, \"message\");\n if (typeof code === \"string\" && SOCKET_FAILURE_CODES.has(code)) {\n const address = Reflect.get(error, \"address\");\n const port = Reflect.get(error, \"port\");\n const hostname = Reflect.get(error, \"hostname\");\n if (address !== undefined) return `${code} at ${String(address)}${port !== undefined ? `:${String(port)}` : \"\"}`;\n if (typeof hostname === \"string\") return `${code} for ${hostname}`;\n }\n if (typeof message === \"string\" && (CONNECTION_LOST_MESSAGES.has(message) ||\n (typeof code === \"string\" && (code.startsWith(\"08\") || UNREACHABLE_SQLSTATES.has(code))))) {\n return typeof code === \"string\" ? `${message} (${code})` : message;\n }\n const inner = Reflect.get(error, \"errors\");\n if (Array.isArray(inner) && inner.length > 0) return describeConnectionFailure(inner[0], depth + 1);\n const cause = Reflect.get(error, \"cause\");\n if (typeof cause === \"object\" && cause !== null) return describeConnectionFailure(cause, depth + 1);\n return typeof code === \"string\" ? code : \"the connection failed\";\n}\n\n/**\n * Is there a Node socket failure under this error, anywhere down its cause\n * chain?\n *\n * Asked only once {@link isDatabaseUnreachable} has said no, so what it finds\n * belongs to another service: a `fetch()` to some other host (`fetch failed`,\n * its `cause` the socket's error), an S3 call that cannot connect (the AWS SDK\n * hands back the socket's own error with `$metadata` on it), or anything else\n * wrapping one. An error that only carries a socket code, with no system call\n * behind it, is not one.\n */\nfunction hasUpstreamSocketFailure(error: unknown, depth = 0): boolean {\n if (typeof error !== \"object\" || error === null || depth > 8) return false;\n return isSocketFailure(error) || hasUpstreamSocketFailure(Reflect.get(error, \"cause\"), depth + 1);\n}\n\n/** What a caller is told when a service the request needs cannot be reached. The host stays in the log. */\nconst UPSTREAM_UNAVAILABLE_MESSAGE =\n \"A service this request depends on could not be reached, so it was not served. Retry shortly.\";\n\n/** What a caller is told when the database cannot be reached. Retryable, and not about them. */\nexport const DATABASE_UNAVAILABLE_MESSAGE =\n \"The database could not be reached, so this request was not served. \" +\n \"Retry shortly; if it keeps happening, the server's database is down or unreachable from it.\";\n\n/** Seconds a client is asked to wait before retrying a request the database could not be reached for. */\nconst DATABASE_RETRY_AFTER_SECONDS = 5;\n\n/** The operator's line for an unreachable database, at most once a minute — see {@link warnPoolFailure}. */\nlet lastUnreachableWarningAt = 0;\n\nfunction warnDatabaseUnreachable(detail: string): void {\n const now = Date.now();\n if (now - lastUnreachableWarningAt < POOL_WARNING_INTERVAL_MS) return;\n lastUnreachableWarningAt = now;\n logger.error(\n `❌ The database is unreachable (${detail}), so requests that need it are answered 503 DATABASE_UNAVAILABLE. ` +\n \"Check that it is running and that this server can reach it; requests recover on their own once it is back. \" +\n \"Not repeated for a minute.\"\n );\n}\n\n/**\n * The operator's line, at most once a minute. A pool that is exhausted is\n * exhausted for every request at once, and a line per request buries the one\n * thing to do about it.\n */\nconst POOL_WARNING_INTERVAL_MS = 60_000;\nlet lastPoolWarningAt = 0;\n\nfunction warnPoolFailure(code: \"DB_POOL_EXHAUSTED\" | \"DB_CONNECT_TIMEOUT\"): void {\n const now = Date.now();\n if (now - lastPoolWarningAt < POOL_WARNING_INTERVAL_MS) return;\n lastPoolWarningAt = now;\n logger.warn(code === \"DB_POOL_EXHAUSTED\"\n ? \"⚠️ Database pool exhausted: every connection stayed busy for DB_POOL_CONNECT_TIMEOUT, so requests \" +\n \"are answered 503. Raise DB_POOL_MAX (default 20; DB_POOL_MAX__<KEY> for another data source), or \" +\n \"find the slow queries or long transactions holding connections. Not repeated for a minute.\"\n : \"⚠️ Opening a database connection took longer than DB_POOL_CONNECT_TIMEOUT, so requests are \" +\n \"answered 503. The database is slow to accept connections or far away. Not repeated for a minute.\");\n}\n\n/**\n * Extract the missing table or column name from a PG error.\n * PG 42P01 messages look like: 'relation \"my_table\" does not exist'\n * PG 42703 messages look like: 'column \"my_col\" does not exist' or 'column my_table.my_col does not exist'\n */\nfunction extractMissingIdentifier(pgMessage?: string): string | null {\n if (!pgMessage) return null;\n // Match quoted identifier: relation \"xxx\" / column \"xxx\"\n const quoted = pgMessage.match(/(?:relation|column|table)\\s+\"([^\"]+)\"/i);\n if (quoted) return quoted[1];\n // Match unquoted: column table.col does not exist\n const unquoted = pgMessage.match(/(?:relation|column|table)\\s+([\\w.]+)\\s+does not exist/i);\n if (unquoted) return unquoted[1];\n return null;\n}\n\n/**\n * The sentence a `INVALID_FILTER_VALUE` answer carries.\n *\n * Built from Postgres's own wording rather than passed through, because the\n * driver hands the whole failed statement over as `error.message` and this one\n * goes to the caller in production too. What is quoted back is the type name\n * and the literal the caller themselves sent — never a table, a column list or\n * a statement.\n *\n * The three shapes are the ones a filter actually produces: an unparseable\n * literal (22P02, `?id=eq.abc`), a value outside an enum's labels (22P02 with\n * different wording), and a number past the column type's range (22003). A\n * fourth SQLSTATE in class 22 lands on the general sentence, which still says\n * the useful thing: it is the value that is wrong, not the server.\n */\nfunction describeDataException(dbError?: PgLikeError): string {\n const message = dbError?.message ?? \"\";\n const column = dbError?.column ? ` for column \"${dbError.column}\"` : \"\";\n\n const syntax = message.match(/invalid input syntax for type ([\\w ]+): \"(.*)\"/);\n if (syntax) return `\"${syntax[2]}\" is not a valid ${syntax[1]}${column}.`;\n\n const enumValue = message.match(/invalid input value for enum ([\\w.\"]+): \"(.*)\"/);\n if (enumValue) return `\"${enumValue[2]}\" is not one of the values of ${enumValue[1]}${column}.`;\n\n const range = message.match(/value \"(.*)\" is out of range for type ([\\w ]+)/);\n if (range) return `\"${range[1]}\" is out of range for ${range[2]}${column}.`;\n\n return `A value in this request could not be read as the type of the column it was compared against${column}.`;\n}\n\n/**\n * Standardized API error class.\n * Throw this from any route handler — the errorHandler middleware\n * will format it into `{ error: { message, code, details? } }`.\n */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n public readonly details?: unknown;\n /**\n * Whether this outcome is a routine part of normal operation rather than\n * something an operator should look at. Expected errors log at debug; every\n * other operational error logs at warn.\n *\n * The motivating case is `POST /auth/refresh` with no session: clients\n * refresh on page load before they know whether one exists, so every\n * anonymous page view is a 401 — correct, and not worth a warning line.\n *\n * The other class is a caller-caused 4xx that never reached the database: a\n * mistyped filter operator, sort direction or limit, a request for a\n * collection that does not exist. Nothing on this server is wrong, and the\n * response body has already told the caller what to fix — while one client\n * holding a stale name would otherwise write a warning per request, forever,\n * until the level means nothing. See `api/rest/query-parser.ts`.\n *\n * What stays at warn is anything that says something about the *server*:\n * a schema that has drifted from the code, a permission the database\n * refused, a dependency that failed. Those are 4xx too, and they are still\n * incidents.\n */\n public readonly expected: boolean;\n\n constructor(statusCode: number, code: string, message: string, details?: unknown, expected = false) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n this.details = details;\n this.expected = expected;\n }\n\n // ── Factory methods ──────────────────────────────────────────────\n\n static badRequest(message: string, code = \"BAD_REQUEST\", details?: unknown): ApiError {\n return new ApiError(400, code, message, details);\n }\n\n static unauthorized(message: string, code = \"UNAUTHORIZED\"): ApiError {\n return new ApiError(401, code, message);\n }\n\n /**\n * A 401 that is a normal outcome, not an incident — logged at debug.\n * See {@link ApiError.expected}.\n */\n static unauthenticated(message: string, code = \"UNAUTHORIZED\"): ApiError {\n return new ApiError(401, code, message, undefined, true);\n }\n\n static forbidden(message: string, code = \"FORBIDDEN\"): ApiError {\n return new ApiError(403, code, message);\n }\n\n static notFound(message: string, code = \"NOT_FOUND\"): ApiError {\n return new ApiError(404, code, message);\n }\n\n /**\n * `details` because a 409 is usually a `23505`, and the one thing the\n * caller needs is *which field* collided. The column name goes in; the\n * value never does — see `pgFieldViolations`.\n */\n static conflict(message: string, code = \"CONFLICT\", details?: unknown): ApiError {\n return new ApiError(409, code, message, details);\n }\n\n static internal(message: string, code = \"INTERNAL_ERROR\"): ApiError {\n return new ApiError(500, code, message);\n }\n\n static serviceUnavailable(message: string, code = \"SERVICE_UNAVAILABLE\"): ApiError {\n return new ApiError(503, code, message);\n }\n\n /**\n * `503 DATABASE_UNAVAILABLE`: the database could not be reached. What\n * every door answers then — the data API, sign-in, the admin routes and\n * the realtime socket — so a client sees one retryable answer, sent with\n * `Retry-After`, rather than a permission failure or a crash.\n *\n * @see isDatabaseUnreachable\n */\n static databaseUnavailable(message = DATABASE_UNAVAILABLE_MESSAGE): ApiError {\n return new ApiError(503, \"DATABASE_UNAVAILABLE\", message);\n }\n}\n\n/**\n * Canonical error response shape:\n * `{ error: { message: string, code: string, details?: unknown } }`\n */\nexport interface ErrorResponse {\n error: {\n message: string;\n code: string;\n details?: unknown;\n /** Request correlation ID for tracing (echoes X-Request-ID). */\n requestId?: string;\n /**\n * Why this request was going to fail whatever it asked for.\n *\n * Only `SCHEMA_DRIFT` today: the caller's `x-rebase-schema` stamp is\n * older than this backend's, so a 400 naming an unknown field is very\n * likely a rename the client has not regenerated for. The error itself\n * is unchanged — this explains it, it does not cause it.\n */\n cause?: {\n code: string;\n message: string;\n clientSchema: string;\n serverSchema: string;\n };\n };\n}\n\n/**\n * General shape of errors that flow through the API error handler.\n * Extends Error with optional HTTP status, error code, and details.\n */\nexport interface RebaseApiError extends Error {\n statusCode?: number;\n code?: string;\n details?: unknown;\n}\n\n/**\n * The answer an error chose for itself, read the same way at every door.\n *\n * @see declaredErrorAnswer\n */\nexport interface DeclaredErrorAnswer {\n /** The HTTP status the error carries. A socket frame has no slot for it. */\n status: number;\n code: string;\n message: string;\n details?: unknown;\n /** See {@link ApiError.expected}: log it at debug, not warn. */\n expected: boolean;\n}\n\n/**\n * The status, code and message an error carries as its own answer — or\n * `undefined` for an error that carries none, which is a server fault and gets\n * masked.\n *\n * Two classes carry one. The server's `ApiError`, and `RebaseApiError` (or its\n * `RebaseClientError` subclass) from `@rebasepro/types` once it has a status.\n * The second is the browser-safe class: a `config/collections/*.ts` file is\n * bundled into the admin SPA and cannot import this package, so it is what a\n * collection callback throws, and what a callback refusal becomes —\n * `callbackRefusal` returns one, and `toCallbackError` wraps anything thrown\n * that does not already carry a status.\n *\n * One function because several doors turn an error into an answer: the REST\n * error handler, the two WebSocket servers, and the Postgres realtime\n * subscriptions. Each used to list the classes it recognised by hand. The\n * sockets listed only `ApiError`, so a `beforeDelete` veto that REST\n * answered as 400 `CALLBACK_REJECTED` with the author's message reached the\n * admin panel — which writes through the socket — as `INTERNAL_ERROR`, and in\n * production as \"An unexpected error occurred\".\n *\n * Matched by name as well as `instanceof`: a monorepo can resolve two copies of\n * a package, and `instanceof` is false across them. Name matching is also why\n * this file needs no runtime import of `@rebasepro/types`, which it may not\n * have — it is in the graph of `@rebasepro/server/functions`.\n */\nexport function declaredErrorAnswer(error: unknown): DeclaredErrorAnswer | undefined {\n if (error === null || typeof error !== \"object\") return undefined;\n const e = error as { name?: unknown; message?: unknown; code?: unknown; details?: unknown; statusCode?: unknown; status?: unknown };\n\n let status: number | undefined;\n if (error instanceof ApiError || e.name === \"ApiError\") {\n status = typeof e.statusCode === \"number\" ? e.statusCode : undefined;\n } else if (typeof e.name === \"string\" && /^Rebase(Api|Client)Error$/.test(e.name)) {\n // It spells its status `status`; `statusCode` wins when both are set.\n status = typeof e.statusCode === \"number\" ? e.statusCode\n : typeof e.status === \"number\" ? e.status\n : undefined;\n // Without a status it has not chosen an answer — `RebaseClientError`\n // is also raised for plain logic errors — so it is not one here.\n if (status === undefined) return undefined;\n } else {\n return undefined;\n }\n\n return {\n status: status || 500,\n code: typeof e.code === \"string\" && e.code ? e.code : \"INTERNAL_ERROR\",\n message: typeof e.message === \"string\" ? e.message : String(e.message ?? \"\"),\n ...(e.details !== undefined && { details: e.details }),\n expected: error instanceof ApiError && error.expected\n };\n}\n\n// `isRebaseApiError` was here. It read `return error instanceof Error`, so it\n// answered yes to every error while being named and used as though it\n// discriminated — the create and update handlers guarded a \"classify this as\n// BAD_REQUEST\" branch on it, and an unreachable database was therefore reported\n// to callers as a bad request. Deleted rather than repaired: the shape it\n// claimed to test is not decidable from an `Error`, and the layer that does\n// know — the driver, which holds the SQLSTATE — raises a real `ApiError`.\n\n/**\n * Leave the code and the message where the request log will find them.\n *\n * A failed request used to produce two lines, each holding half of it: this\n * handler had the code and the diagnosis, `requestLogger` had the user, the\n * collection, the status and the latency. Correlating them meant matching on\n * the request id — which only one of them printed reliably — and the pair cost\n * twice the volume for less than one line's worth of meaning.\n */\nfunction handOffToRequestLog(c: Context<HonoEnv>, code: string, message: string, expected = false): void {\n if (typeof c.set !== \"function\") return;\n c.set(\"errorSummary\", { code, message, expected });\n}\n\n/**\n * Is a request line coming for this request?\n *\n * `requestLogger` claims it before the handler runs, so by the time an error\n * reaches here the answer is already known. When nothing claimed it — a router\n * a project mounted onto its own Hono app, a test driving `app.fetch`\n * directly — this handler stays the only thing that would report the failure,\n * so it still writes its own line. Silence is the one outcome neither half may\n * produce.\n */\nfunction requestWillBeLogged(c: Context<HonoEnv>): boolean {\n return typeof c.get === \"function\" && c.get(\"requestLogged\") === true;\n}\n\n/**\n * Hono error-handling middleware (`app.onError`).\n * Converts any error into the canonical `{ error: { message, code } }` shape.\n */\nexport const errorHandler: ErrorHandler<HonoEnv> = (err, c) => {\n // Typecast custom error properties\n const error: RebaseApiError = err;\n const reqId = typeof c.get === \"function\" ? c.get(\"requestId\") : undefined;\n\n /* A stale SDK, named on the errors it explains.\n\n 400 and 404 only: those are what a renamed or removed field produces —\n an unknown filter field is a 400, a collection gone from under its slug\n is a 404 — and they are the two a caller can act on by regenerating.\n Attaching it to a 500 would be noise, since a server fault has nothing to\n do with how old the caller's schema is. */\n const driftFor = (status: number) =>\n (status === 400 || status === 404) && typeof c.get === \"function\"\n ? schemaDriftCause(c.get(\"schemaDrift\"))\n : undefined;\n\n // An error that chose its own answer — `ApiError`, or the browser-safe\n // `RebaseApiError` a collection callback throws. The same predicate the\n // WebSocket servers use; see `declaredErrorAnswer`.\n const answer = declaredErrorAnswer(error);\n if (answer) {\n // Operational errors — log at warn, unless the error declares itself a\n // routine outcome (see ApiError.expected), which would otherwise put a\n // warning in the log for every anonymous page view.\n handOffToRequestLog(c, answer.code, answer.message, answer.expected);\n if (answer.code === \"DATABASE_UNAVAILABLE\") c.header(\"Retry-After\", String(DATABASE_RETRY_AFTER_SECONDS));\n if (!requestWillBeLogged(c)) {\n const line = `[API] ${c.req.method} ${c.req.path} → ${answer.status} ${answer.code}: ${answer.message}` +\n (reqId ? ` [${reqId}]` : \"\");\n if (answer.expected) {\n logger.debug(line);\n } else {\n logger.warn(`⚠️ ${line}`);\n }\n }\n const apiErrorDrift = driftFor(answer.status);\n return c.json({\n error: {\n message: answer.message,\n code: answer.code,\n ...(answer.details !== undefined && { details: answer.details }),\n ...(reqId && { requestId: reqId }),\n ...(apiErrorDrift && { cause: apiErrorDrift })\n }\n } satisfies ErrorResponse, answer.status as ContentfulStatusCode);\n }\n\n // A body that is not JSON, read with `c.req.json()` — every auth and admin\n // route reads its body that way. It threw a bare `SyntaxError`, which\n // landed here as a 500 logged with a stack: a server fault for the\n // caller's typo, where the data API (which parses its own body) answered\n // 400. Mapped once, here, rather than at each of forty call sites.\n if (err instanceof SyntaxError && /JSON/i.test(err.message)) {\n handOffToRequestLog(c, \"INVALID_JSON\", \"Invalid JSON body\");\n return c.json({\n error: {\n message: \"Invalid JSON body\",\n code: \"INVALID_JSON\",\n ...(reqId && { requestId: reqId })\n }\n } satisfies ErrorResponse, 400);\n }\n\n let statusCode = error.statusCode || codeToStatus(error.code) || 500;\n let code = error.code || \"INTERNAL_ERROR\";\n\n // Handle DB connection and specific system errors for better logging\n let logMessage = error.message;\n\n // Resolve the actual cause — Node's net module wraps dual-stack failures\n // in an AggregateError whose inner errors carry the real address/port.\n let resolvedCause: PgLikeError | undefined;\n if (error.cause && typeof error.cause === \"object\" && error.cause !== null && \"code\" in error.cause) {\n const cause = error.cause as PgLikeError & { errors?: PgLikeError[] };\n if (cause.code === \"ECONNREFUSED\" && !cause.address && Array.isArray(cause.errors)) {\n // AggregateError — pick the first inner error that has address info\n resolvedCause = cause.errors.find(e => e.address) || cause;\n } else {\n resolvedCause = cause;\n }\n }\n\n // The real database error may sit several levels down the cause chain.\n // Losing it turns a precise failure (e.g. an RLS denial) into an opaque\n // \"Failed query: …\" 500 that is undiagnosable without direct DB access.\n const dbError = extractDbError(error);\n const poolFailure = dbError ? undefined : poolFailureOf(error);\n // Ahead of every SQLSTATE arm below: `57P01` is a SQLSTATE too, and it\n // says the connection is gone, not that the statement was wrong.\n const unreachable = !poolFailure && isDatabaseUnreachable(error);\n // A socket failure that is not the database's is another service's: a\n // `fetch()`, an S3 call. It answered `500` with the errno as the code\n // (`ECONNREFUSED`), or `INTERNAL_ERROR` — a server fault, for a service it\n // calls being down.\n const upstream = !poolFailure && !unreachable && !dbError && hasUpstreamSocketFailure(error);\n\n if (poolFailure) {\n code = poolFailure;\n statusCode = 503;\n logMessage = poolFailure === \"DB_POOL_EXHAUSTED\"\n ? \"Timed out waiting for a database connection: every pooled connection was busy (DB_POOL_MAX).\"\n : \"Timed out opening a database connection (DB_POOL_CONNECT_TIMEOUT).\";\n warnPoolFailure(poolFailure);\n c.header(\"Retry-After\", String(POOL_RETRY_AFTER_SECONDS));\n } else if (unreachable) {\n code = \"DATABASE_UNAVAILABLE\";\n statusCode = 503;\n const reason = describeConnectionFailure(error);\n logMessage = `The database could not be reached: ${reason}.`;\n warnDatabaseUnreachable(reason);\n c.header(\"Retry-After\", String(DATABASE_RETRY_AFTER_SECONDS));\n } else if (upstream) {\n code = \"SERVICE_UNAVAILABLE\";\n statusCode = 503;\n logMessage = `A service this request depends on could not be reached: ${describeConnectionFailure(error)}.`;\n } else if (resolvedCause && (resolvedCause.code === \"ENETUNREACH\" || resolvedCause.code === \"ECONNREFUSED\")) {\n // Some other service: a database that cannot be reached is the arm\n // above, so this one does not say \"database\".\n const cause = resolvedCause;\n logMessage = cause.code === \"ENETUNREACH\"\n ? `Network unreachable: cannot connect to ${cause.address}:${cause.port}.`\n : `Connection refused by ${cause.address}:${cause.port}.`;\n } else if (\"code\" in error && error.code === \"ENETUNREACH\") {\n const netErr = error as PgLikeError;\n logMessage = `Network unreachable. Cannot connect to service at ${netErr.address}:${netErr.port}.`;\n } else if (dbError && (dbError.code === \"42703\" || dbError.code === \"42P01\")) {\n code = \"SCHEMA_DRIFT\";\n const issue = dbError.code === \"42703\" ? \"column\" : \"table\";\n const identifier = dbError.table || dbError.column || extractMissingIdentifier(dbError.message) || \"unknown\";\n logMessage = `Schema drift: ${issue} \"${identifier}\" does not exist in the database. ${schemaDriftRemedy().short}`;\n } else if (dbError) {\n const parts = [`[PG ${dbError.code}] ${dbError.message}`];\n if (dbError.detail) parts.push(`Detail: ${dbError.detail}`);\n if (dbError.hint) parts.push(`Hint: ${dbError.hint}`);\n if (dbError.table) parts.push(`Table: ${dbError.table}`);\n if (dbError.column) parts.push(`Column: ${dbError.column}`);\n if (dbError.constraint) parts.push(`Constraint: ${dbError.constraint}`);\n if (dbError.code === \"42501\") {\n code = \"DB_PERMISSION_DENIED\";\n const ungranted = missingGrant(dbError.message);\n parts.push(ungranted\n ? `The role this request runs as was never granted ${ungranted.kind} \"${ungranted.name}\" — ` +\n \"a missing GRANT, not a row-level security policy.\"\n : \"The database rejected the statement for lack of privilege — usually a row-level \" +\n `security policy${dbError.table ? ` on \"${dbError.table}\"` : \"\"} denying this role, ` +\n \"or a stale FORCE ROW LEVEL SECURITY flag binding the owner connection.\"\n );\n }\n // 25006 read_only_sql_transaction. A request-scoped read opens its\n // transaction `READ ONLY`, so the only way to reach this is user code on\n // a read path attempting a write — which means an `afterRead` callback,\n // or something it called. Left in the generic branch it was a 500\n // \"Internal Server Error\", indistinguishable from the database being\n // down; it is the caller's own code, and it is not a server failure.\n if (dbError.code === \"25006\") {\n code = \"READ_ONLY_TRANSACTION\";\n statusCode = 409;\n parts.push(READ_ONLY_TRANSACTION_MESSAGE);\n }\n // SQLSTATE class 22 — data exception. The caller sent a value the\n // column's type cannot hold: `?id=eq.abc` on an integer key,\n // `?status=eq.nope` on an enum, a timestamp that is not one, a number\n // past the type's range. Every *other* bad query parameter already has\n // a precise 400 (`INVALID_LIMIT`, `UNKNOWN_FILTER_FIELD`,\n // `INVALID_LOGICAL_GROUP`); a bad *value* fell off the end of this\n // chain and answered 500 INTERNAL_ERROR — and in production `dbMessage`\n // is stripped, so the caller got a bare 500 naming nothing and had no\n // way to learn their own typo was the cause.\n if (dbError.code?.startsWith(\"22\")) {\n code = \"INVALID_FILTER_VALUE\";\n statusCode = 400;\n }\n logMessage = parts.join(\". \");\n }\n\n const isDbSchemaMismatch = code === \"SCHEMA_DRIFT\";\n\n // `logMessage`, not the sanitized client message: the request line is a\n // server log, and the whole point of this branch is the diagnosis it built.\n handOffToRequestLog(c, code, logMessage);\n\n if (isDbSchemaMismatch) {\n // Database schema mismatch is logged as a warning instead of a fatal error\n if (!requestWillBeLogged(c)) logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n // In dev mode, show a one-time hint to run `rebase doctor`\n if (!_schemaDriftHinted && hostEnv().NODE_ENV !== \"production\") {\n _schemaDriftHinted = true;\n // Drawn rather than hand-aligned: the remedy inside it now varies\n // with the database, and a box whose rows were padded by hand\n // stayed straight only for the text it was written around.\n const WIDTH = 62;\n const row = (text: string) => `│${text.padEnd(WIDTH).slice(0, WIDTH)}│`;\n logger.warn([\n \"\",\n `┌${\"─\".repeat(WIDTH)}┐`,\n // One space short, deliberately: the emoji occupies two columns\n // in a terminal and one in `String.length`.\n `│${\" 💡 TIP: Run `rebase doctor` for full schema diagnostics\".padEnd(WIDTH - 1)}│`,\n row(\"\"),\n ...schemaDriftRemedy().lines.map(row),\n `└${\"─\".repeat(WIDTH)}┘`,\n \"\"\n ].join(\"\\n\"));\n }\n } else if (poolFailure || unreachable) {\n // Said once a minute by `warnPoolFailure` / `warnDatabaseUnreachable`;\n // the request line carries each occurrence.\n if (!requestWillBeLogged(c)) logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n } else if (code === \"READ_ONLY_TRANSACTION\" || code === \"INVALID_FILTER_VALUE\") {\n // A 4xx: the application's own callback, refused — or a filter value\n // the caller's own request could not have worked with. Not a server\n // fault, so not an ❌ in the log either — and, like the drift arm above,\n // not a second line when the request log is already going to carry it.\n if (!requestWillBeLogged(c)) logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n } else if (!requestWillBeLogged(c)) {\n // Unexpected errors — log at error level\n logger.error(\n `❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n }\n\n // Suppress the huge stack trace for known DB errors: it is noisy, and the\n // extracted [PG …] line above carries the signal. The SQL and the bound\n // params it used to leak are no longer this branch's problem — `logger`\n // strips Drizzle's `Failed query: … / params: …` wrapper out of every\n // message and stack it emits, so the fallbacks below (a connection dropped\n // mid-statement carries no SQLSTATE, so `dbError` is null and the stack is\n // logged) are covered too.\n const suppressStack = isDbSchemaMismatch || dbError !== null || poolFailure !== undefined || unreachable ||\n (statusCode < 500 && code === \"BAD_REQUEST\");\n if (!suppressStack) {\n // The error goes in as a value, not as `String(error.stack)`. A string\n // is a leaf to the logger: `serialiseError` — the `.cause`/\n // `AggregateError` walker the boot path relies on — never runs on one,\n // so the request path used to print the outer wrapper's stack and drop\n // the sentence that says what actually failed (`connect ECONNRESET`,\n // sitting two `.cause` links down). Structured, it walks the chain and\n // redacts each link on the way.\n logger.error(\"unhandled request error\", { error });\n }\n\n // Sanitize the message for the client to prevent leaking sensitive details\n // like SQL queries or internal IP addresses.\n let clientMessage = \"An unexpected error occurred\";\n if (code === \"READ_ONLY_TRANSACTION\") {\n // Ahead of the generic 4xx arm below, which would echo the raw driver\n // message (\"Failed query: insert into …\") back to the caller.\n clientMessage = READ_ONLY_TRANSACTION_MESSAGE;\n } else if (code === \"INVALID_FILTER_VALUE\") {\n // Also ahead of the 4xx arm: `error.message` here is the driver's\n // \"Failed query: select … / params: …\", which is both unhelpful and the\n // one thing this envelope must never carry.\n clientMessage = describeDataException(dbError || (error as PgLikeError));\n } else if (statusCode < 500 && error.message) {\n // If it's a 4xx error (e.g. from validation), it's generally safe to send the message\n clientMessage = error.message;\n } else if (error instanceof ApiError || error.name === \"ApiError\") {\n // We already handled ApiError above, but just in case\n clientMessage = error.message;\n } else if (code === \"SCHEMA_DRIFT\") {\n const pgErr = dbError || (error as PgLikeError);\n const issue = pgErr.code === \"42703\" ? \"column\" : \"table\";\n const identifier = pgErr.table || pgErr.column || extractMissingIdentifier(pgErr.message || error.message) || \"unknown\";\n clientMessage = `Schema drift: ${issue} \"${identifier}\" does not exist. ${schemaDriftRemedy().short}`;\n } else if (poolFailure) {\n clientMessage = \"The database is busy: no connection was free in time. Retry shortly.\";\n } else if (unreachable) {\n clientMessage = DATABASE_UNAVAILABLE_MESSAGE;\n } else if (upstream) {\n clientMessage = UPSTREAM_UNAVAILABLE_MESSAGE;\n } else if (code === \"DB_PERMISSION_DENIED\") {\n const ungranted = missingGrant(dbError?.message);\n clientMessage = ungranted\n ? `Permission denied by the database: the role this request runs as has no privilege on ${ungranted.kind} ` +\n `\"${ungranted.name}\". That is a missing GRANT to that role, not a row-level security policy.`\n : `Permission denied by the database${dbError?.table ? ` on \"${dbError.table}\"` : \"\"} (row-level security). Check the RLS policies for this table.`;\n } else if (code === \"INTERNAL_ERROR\") {\n clientMessage = \"Internal Server Error\";\n }\n\n // Database diagnostics for the envelope: the SQLSTATE is always safe to\n // return; message/detail/hint can reference schema internals, so only\n // outside production.\n const dbDetails = dbError ? {\n dbCode: dbError.code,\n ...(hostEnv().NODE_ENV !== \"production\" && {\n dbMessage: dbError.message,\n ...(dbError.detail && { detail: dbError.detail }),\n ...(dbError.hint && { hint: dbError.hint })\n })\n } : undefined;\n\n const drift = driftFor(statusCode);\n\n return c.json({\n error: {\n message: clientMessage,\n code,\n ...(error.details !== undefined\n ? { details: error.details }\n : dbDetails !== undefined ? { details: dbDetails } : {}),\n ...(reqId && { requestId: reqId }),\n ...(drift && { cause: drift })\n }\n } satisfies ErrorResponse, statusCode as ContentfulStatusCode);\n};\n\n/**\n * The object a `42501` names when no policy was involved.\n *\n * One SQLSTATE covers two different failures. A policy refusing a row says\n * \"… violates row-level security policy …\"; a role that was never granted an\n * object says \"permission denied for schema rebase\" (or table, function, …).\n * Answering the second with \"check the RLS policies\" sent people looking at\n * policies that were fine, for a grant that was missing.\n */\nfunction missingGrant(message: string | undefined): { kind: string; name: string } | undefined {\n const match = message?.match(/^permission denied for (schema|table|relation|view|materialized view|sequence|function|procedure|routine|type|column|database|foreign table|large object|tablespace|language) (.+)$/i);\n return match ? { kind: match[1].toLowerCase(), name: match[2].replace(/^\"|\"$/g, \"\") } : undefined;\n}\n\n/**\n * Map known error codes to HTTP status codes.\n */\nfunction codeToStatus(code?: string): number | undefined {\n if (!code) return undefined;\n const map: Record<string, number> = {\n BAD_REQUEST: 400,\n INVALID_INPUT: 400,\n WEAK_PASSWORD: 400,\n UNAUTHORIZED: 401,\n INVALID_CREDENTIALS: 401,\n INVALID_TOKEN: 401,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n CONFLICT: 409,\n EMAIL_EXISTS: 409,\n ROLE_EXISTS: 409,\n READ_ONLY_TRANSACTION: 409,\n INVALID_FILTER_VALUE: 400,\n SCHEMA_DRIFT: 500,\n DB_PERMISSION_DENIED: 500,\n INTERNAL_ERROR: 500,\n NOT_CONFIGURED: 503,\n SERVICE_UNAVAILABLE: 503\n };\n return map[code];\n}\n\n\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import { t as ApiError } from "./errors-CHSJYpoy.js";
6
+ import { t as ApiError } from "./errors-_ytoGi8A.js";
7
7
  import { t as holdsAdminRole } from "./admin-roles-DI9RsgN3.js";
8
8
  import { Hono } from "hono";
9
9
  //#region src/functions/context.ts
@@ -258,4 +258,4 @@ function createFunctionRoutes(functions, problems = [], mountPath = "/functions"
258
258
  //#endregion
259
259
  export { getDriver as a, getUser as c, identityResolved as d, isAdmin as f, getApiKey as i, getUserId as l, requireDriver as m, function_routes_exports as n, getRequestId as o, isAuthenticated as p, requireRole as r, getRoles as s, createFunctionRoutes as t, hasRole as u };
260
260
 
261
- //# sourceMappingURL=function-routes-C0emkHtX.js.map
261
+ //# sourceMappingURL=function-routes-DNV4RUF0.js.map