@rebasepro/server 0.19.1 → 0.19.2-canary.g09316f6
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.
- package/dist/{GCSStorageController-ZXPoNqW3.js → GCSStorageController-CLIJXwGS.js} +2 -2
- package/dist/{GCSStorageController-ZXPoNqW3.js.map → GCSStorageController-CLIJXwGS.js.map} +1 -1
- package/dist/{S3StorageController-5pAXyv31.js → S3StorageController-Dcuf8lMA.js} +2 -2
- package/dist/{S3StorageController-5pAXyv31.js.map → S3StorageController-Dcuf8lMA.js.map} +1 -1
- package/dist/api/errors.d.ts +6 -1
- package/dist/api/rest/api-generator.d.ts +80 -0
- package/dist/api/rest/batch.d.ts +71 -0
- package/dist/api/rest/conflict-target.d.ts +20 -0
- package/dist/api/rest/etag.d.ts +55 -0
- package/dist/api/rest/field-access-query.d.ts +66 -0
- package/dist/api/rest/field-ops.d.ts +86 -0
- package/dist/api/rest/query-parser.d.ts +16 -2
- package/dist/api/rest/soft-delete-params.d.ts +37 -0
- package/dist/api/rest/write-validation.d.ts +51 -8
- package/dist/api/types.d.ts +28 -3
- package/dist/{ast-schema-editor-CNgFJ3NF.js → ast-schema-editor-C6mDz0XN.js} +2 -2
- package/dist/{ast-schema-editor-CNgFJ3NF.js.map → ast-schema-editor-C6mDz0XN.js.map} +1 -1
- package/dist/auth/jwt.d.ts +17 -0
- package/dist/auth/rls-scope.d.ts +1 -0
- package/dist/{auth-BRiOuyq8.js → auth-DJsLXsCR.js} +61 -60
- package/dist/auth-DJsLXsCR.js.map +1 -0
- package/dist/{backup-C8P6Cl3G.js → backup-DGu0v9Ku.js} +2 -2
- package/dist/{backup-C8P6Cl3G.js.map → backup-DGu0v9Ku.js.map} +1 -1
- package/dist/boot/bundle.d.ts +12 -3
- package/dist/boot/driver.d.ts +11 -4
- package/dist/boot/env.d.ts +2 -2
- package/dist/{contract-routes-CusnEB5h.js → contract-routes-eLxV0le1.js} +2 -2
- package/dist/{contract-routes-CusnEB5h.js.map → contract-routes-eLxV0le1.js.map} +1 -1
- package/dist/{cron-loader-DnmIePn_.js → cron-loader-CQjvjpEw.js} +2 -2
- package/dist/{cron-loader-DnmIePn_.js.map → cron-loader-CQjvjpEw.js.map} +1 -1
- package/dist/{cron-routes-D3x2ydMa.js → cron-routes-Bfwni8Zg.js} +3 -3
- package/dist/{cron-routes-D3x2ydMa.js.map → cron-routes-Bfwni8Zg.js.map} +1 -1
- package/dist/{cron-store-CCQXwgVL.js → cron-store-Bsiw4Q6u.js} +3 -3
- package/dist/{cron-store-CCQXwgVL.js.map → cron-store-Bsiw4Q6u.js.map} +1 -1
- package/dist/{errors-HjfaPlvY.js → errors-DMImyqyR.js} +8 -3
- package/dist/errors-DMImyqyR.js.map +1 -0
- package/dist/{function-routes-gQ0EShVG.js → function-routes-C4nB2h0z.js} +2 -2
- package/dist/{function-routes-gQ0EShVG.js.map → function-routes-C4nB2h0z.js.map} +1 -1
- package/dist/functions/index.js +7 -2
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-B1FwXx9J.js → history-recorder-r5_IzSHK.js} +2 -2
- package/dist/{history-recorder-B1FwXx9J.js.map → history-recorder-r5_IzSHK.js.map} +1 -1
- package/dist/{history-store-LHXaQywp.js → history-store-D4RVK-uZ.js} +2 -2
- package/dist/{history-store-LHXaQywp.js.map → history-store-D4RVK-uZ.js.map} +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +2429 -7303
- package/dist/index.es.js.map +1 -1
- package/dist/{jobs-DkkD9mPV.js → jobs-CW5lm_Ix.js} +3 -3
- package/dist/{jobs-DkkD9mPV.js.map → jobs-CW5lm_Ix.js.map} +1 -1
- package/dist/{jwt-DkhXwMzR.js → jwt-DATvkKB_.js} +39 -3
- package/dist/{jwt-DkhXwMzR.js.map → jwt-DATvkKB_.js.map} +1 -1
- package/dist/{keys-g8lbVC_o.js → keys-Qfc4XieN.js} +2 -2
- package/dist/{keys-g8lbVC_o.js.map → keys-Qfc4XieN.js.map} +1 -1
- package/dist/{logs-routes-CbsTpozn.js → logs-routes-DnJINsMu.js} +2 -2
- package/dist/{logs-routes-CbsTpozn.js.map → logs-routes-DnJINsMu.js.map} +1 -1
- package/dist/{openapi-generator-BIBbO1Tq.js → openapi-generator-DGyLbISS.js} +374 -48
- package/dist/openapi-generator-DGyLbISS.js.map +1 -0
- package/dist/{query-parser-C68Q9EX4.js → query-parser-BQiPZrM-.js} +257 -17
- package/dist/query-parser-BQiPZrM-.js.map +1 -0
- package/dist/{request-timeout-DESvlfrS.js → request-timeout-BR-OBwES.js} +2 -2
- package/dist/{request-timeout-DESvlfrS.js.map → request-timeout-BR-OBwES.js.map} +1 -1
- package/dist/{schema-editor-routes-CcZKh50q.js → schema-editor-routes-C3TLZqAC.js} +13 -13
- package/dist/{schema-editor-routes-CcZKh50q.js.map → schema-editor-routes-C3TLZqAC.js.map} +1 -1
- package/dist/{src-DHK4fHkw.js → src-Br6ARbs6.js} +26 -2
- package/dist/src-Br6ARbs6.js.map +1 -0
- package/dist/{src-Dq-I3Ybx.js → src-DqZ9YiGA.js} +1387 -94
- package/dist/src-DqZ9YiGA.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/property-limits.d.ts +77 -0
- package/dist/storage/routes.d.ts +13 -0
- package/dist/storage/tus-handler.d.ts +22 -1
- package/package.json +7 -6
- package/dist/auth-BRiOuyq8.js.map +0 -1
- package/dist/errors-HjfaPlvY.js.map +0 -1
- package/dist/openapi-generator-BIBbO1Tq.js.map +0 -1
- package/dist/query-parser-C68Q9EX4.js.map +0 -1
- package/dist/schemas-C3234HWE.js +0 -6827
- package/dist/schemas-C3234HWE.js.map +0 -1
- package/dist/src-DHK4fHkw.js.map +0 -1
- package/dist/src-Dq-I3Ybx.js.map +0 -1
|
@@ -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 { n as errorHandler, t as ApiError } from "./errors-
|
|
6
|
+
import { n as errorHandler, t as ApiError } from "./errors-DMImyqyR.js";
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { Hono } from "hono";
|
|
@@ -161,4 +161,4 @@ var backup_exports = /* @__PURE__ */ __exportAll({
|
|
|
161
161
|
//#endregion
|
|
162
162
|
export { parseBackupTimestamp as a, parseBackupDestination as i, createBackupRoutes as n, readBackupBytes as o, listBackupObjects as r, backup_exports as t };
|
|
163
163
|
|
|
164
|
-
//# sourceMappingURL=backup-
|
|
164
|
+
//# sourceMappingURL=backup-DGu0v9Ku.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backup-C8P6Cl3G.js","names":[],"sources":["../src/backup/backup-common.ts","../src/backup/backup-routes.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\";\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 * List the backups at a destination as {@link BackupInfo}, newest first.\n * Only `.dump` files are considered.\n */\nexport async function listBackupObjects(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupInfo[]> {\n if (dest.kind === \"local\") {\n if (!fs.existsSync(dest.path)) return [];\n const stat = fs.statSync(dest.path);\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n if (!fs.existsSync(dir)) return [];\n return fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".dump\"))\n .map((f): BackupInfo => {\n const full = path.join(dir, f);\n const st = fs.statSync(full);\n const createdAt = parseBackupTimestamp(f) ?? st.mtime;\n return {\n key: full,\n name: f,\n sizeBytes: st.size,\n createdAt: createdAt.toISOString(),\n destinationKind: \"local\"\n };\n })\n .sort(byNewest);\n }\n\n if (!storage) return [];\n const result = await storage.listObjects(dest.prefix ? `${dest.prefix}/` : \"\", {\n bucket: dest.bucket,\n maxResults: 1000\n });\n return result.items\n .map((item) => item.fullPath)\n .filter((key) => key.endsWith(\".dump\"))\n .map((key): BackupInfo => {\n const createdAt = parseBackupTimestamp(key);\n return {\n key,\n name: key.split(\"/\").pop() || key,\n createdAt: createdAt ? createdAt.toISOString() : undefined,\n destinationKind: dest.kind as BackupDestinationKind\n };\n })\n .sort(byNewest);\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/** 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 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 .dump files.\n if (!resolved.startsWith(resolvedDir + path.sep) || !resolved.endsWith(\".dump\")) {\n return null;\n }\n if (!fs.existsSync(resolved)) return null;\n return { bytes: new Uint8Array(fs.readFileSync(resolved)), name: path.basename(resolved) };\n }\n\n if (!storage) return null;\n if (!key.endsWith(\".dump\")) 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","import { Hono } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport type { StorageController } from \"../storage\";\nimport { BackupDestination, listBackupObjects, readBackupBytes } from \"./backup-common\";\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 /** Storage controller for object-storage destinations. */\n storage?: StorageController;\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\n * GET /download → download a backup's bytes (?key=…)\n */\nexport function createBackupRoutes(config: BackupRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n router.get(\"/\", async (c) => {\n const dest = config.getDestination();\n if (!dest) {\n return c.json({ backups: [], destinationKind: \"local\" as const, configured: false });\n }\n const backups = await listBackupObjects(dest, config.storage);\n return c.json({ backups, destinationKind: dest.kind, configured: true });\n });\n\n router.get(\"/download\", async (c) => {\n const dest = config.getDestination();\n if (!dest) {\n throw ApiError.badRequest(\"Backups are not configured (set BACKUP_DESTINATION).\");\n }\n const key = c.req.query(\"key\");\n if (!key) {\n throw ApiError.badRequest(\"Missing 'key' query parameter.\");\n }\n const result = await readBackupBytes(dest, key, config.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-Disposition\", `attachment; filename=\"${result.name}\"`);\n return c.body(result.bytes);\n });\n\n return router;\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 } from \"./backup-routes\";\nexport type { BackupRoutesConfig } from \"./backup-routes\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"backup-DGu0v9Ku.js","names":[],"sources":["../src/backup/backup-common.ts","../src/backup/backup-routes.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\";\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 * List the backups at a destination as {@link BackupInfo}, newest first.\n * Only `.dump` files are considered.\n */\nexport async function listBackupObjects(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupInfo[]> {\n if (dest.kind === \"local\") {\n if (!fs.existsSync(dest.path)) return [];\n const stat = fs.statSync(dest.path);\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n if (!fs.existsSync(dir)) return [];\n return fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".dump\"))\n .map((f): BackupInfo => {\n const full = path.join(dir, f);\n const st = fs.statSync(full);\n const createdAt = parseBackupTimestamp(f) ?? st.mtime;\n return {\n key: full,\n name: f,\n sizeBytes: st.size,\n createdAt: createdAt.toISOString(),\n destinationKind: \"local\"\n };\n })\n .sort(byNewest);\n }\n\n if (!storage) return [];\n const result = await storage.listObjects(dest.prefix ? `${dest.prefix}/` : \"\", {\n bucket: dest.bucket,\n maxResults: 1000\n });\n return result.items\n .map((item) => item.fullPath)\n .filter((key) => key.endsWith(\".dump\"))\n .map((key): BackupInfo => {\n const createdAt = parseBackupTimestamp(key);\n return {\n key,\n name: key.split(\"/\").pop() || key,\n createdAt: createdAt ? createdAt.toISOString() : undefined,\n destinationKind: dest.kind as BackupDestinationKind\n };\n })\n .sort(byNewest);\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/** 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 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 .dump files.\n if (!resolved.startsWith(resolvedDir + path.sep) || !resolved.endsWith(\".dump\")) {\n return null;\n }\n if (!fs.existsSync(resolved)) return null;\n return { bytes: new Uint8Array(fs.readFileSync(resolved)), name: path.basename(resolved) };\n }\n\n if (!storage) return null;\n if (!key.endsWith(\".dump\")) 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","import { Hono } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport type { StorageController } from \"../storage\";\nimport { BackupDestination, listBackupObjects, readBackupBytes } from \"./backup-common\";\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 /** Storage controller for object-storage destinations. */\n storage?: StorageController;\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\n * GET /download → download a backup's bytes (?key=…)\n */\nexport function createBackupRoutes(config: BackupRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n router.get(\"/\", async (c) => {\n const dest = config.getDestination();\n if (!dest) {\n return c.json({ backups: [], destinationKind: \"local\" as const, configured: false });\n }\n const backups = await listBackupObjects(dest, config.storage);\n return c.json({ backups, destinationKind: dest.kind, configured: true });\n });\n\n router.get(\"/download\", async (c) => {\n const dest = config.getDestination();\n if (!dest) {\n throw ApiError.badRequest(\"Backups are not configured (set BACKUP_DESTINATION).\");\n }\n const key = c.req.query(\"key\");\n if (!key) {\n throw ApiError.badRequest(\"Missing 'key' query parameter.\");\n }\n const result = await readBackupBytes(dest, key, config.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-Disposition\", `attachment; filename=\"${result.name}\"`);\n return c.body(result.bytes);\n });\n\n return router;\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 } from \"./backup-routes\";\nexport type { BackupRoutesConfig } from \"./backup-routes\";\n"],"mappings":"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"}
|
package/dist/boot/bundle.d.ts
CHANGED
|
@@ -83,6 +83,14 @@ export declare function loadBundle(bundleDir: string): LoadedBundle;
|
|
|
83
83
|
/**
|
|
84
84
|
* The Drizzle schema a bundle ships: tables, enums and relations, as generated
|
|
85
85
|
* from the project's collections.
|
|
86
|
+
*
|
|
87
|
+
* **Not what the runtime serves.** The Postgres driver builds its tables from
|
|
88
|
+
* `information_schema` after boot-ensure has run, so this module is loaded for
|
|
89
|
+
* one purpose only: comparing it against the live database at boot and saying
|
|
90
|
+
* so when the committed file has fallen behind (`generated-schema-diff.ts` in
|
|
91
|
+
* `@rebasepro/server-postgres`). It used to be authoritative, and a file one
|
|
92
|
+
* commit behind the database was then able to 500 every request, refuse a boot,
|
|
93
|
+
* or lose a relation from a list read.
|
|
86
94
|
*/
|
|
87
95
|
export interface BundleSchemaExports {
|
|
88
96
|
tables?: Record<string, unknown>;
|
|
@@ -90,10 +98,11 @@ export interface BundleSchemaExports {
|
|
|
90
98
|
relations?: Record<string, unknown>;
|
|
91
99
|
}
|
|
92
100
|
/**
|
|
93
|
-
* Import the bundle's Drizzle schema module.
|
|
101
|
+
* Import the bundle's Drizzle schema module, for the boot-time staleness check.
|
|
94
102
|
*
|
|
95
|
-
* Returns `undefined` when the bundle declares none
|
|
96
|
-
*
|
|
103
|
+
* Returns `undefined` when the bundle declares none, and that is not a
|
|
104
|
+
* degraded boot: every table the server serves is read from the database. All
|
|
105
|
+
* that is lost is the warning that the committed file no longer describes it.
|
|
97
106
|
*/
|
|
98
107
|
export declare function loadBundleSchema(bundle: LoadedBundle): Promise<BundleSchemaExports | undefined>;
|
|
99
108
|
/**
|
package/dist/boot/driver.d.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface InitializedDataSource {
|
|
|
52
52
|
bootstrapper: BackendBootstrapper;
|
|
53
53
|
connection: DriverConnection;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The generated Drizzle module a bundle ships — a diagnostic, not the schema an
|
|
57
|
+
* adapter serves. See `BundleSchemaExports` in ./bundle.ts.
|
|
58
|
+
*/
|
|
55
59
|
export interface BundleSchema {
|
|
56
60
|
tables?: Record<string, unknown>;
|
|
57
61
|
enums?: Record<string, unknown>;
|
|
@@ -84,10 +88,13 @@ export declare function adapterToBootstrapper(adapter: DatabaseAdapter, id: stri
|
|
|
84
88
|
/**
|
|
85
89
|
* Build a driver for one data source.
|
|
86
90
|
*
|
|
87
|
-
* Only the default source receives the bundle's Drizzle schema
|
|
88
|
-
*
|
|
89
|
-
* the
|
|
90
|
-
*
|
|
91
|
+
* Only the default source receives the bundle's Drizzle schema, which is now a
|
|
92
|
+
* diagnostic rather than a description: every adapter reads its tables out of
|
|
93
|
+
* the database it is connected to, and this is what the boot-time check
|
|
94
|
+
* compares against to say the committed `schema.generated.ts` has fallen
|
|
95
|
+
* behind. The default source is where the generated file's tables live, and a
|
|
96
|
+
* secondary source's stale-file warning is not worth handing a second adapter a
|
|
97
|
+
* module describing a database it is not connected to.
|
|
91
98
|
*/
|
|
92
99
|
export declare function initializeDataSource(source: ResolvedDataSourceConfig, schema: BundleSchema | undefined, resolveFrom?: string[]): Promise<InitializedDataSource>;
|
|
93
100
|
/**
|
package/dist/boot/env.d.ts
CHANGED
|
@@ -145,11 +145,11 @@ declare const bootEnvExtension: z.ZodObject<{
|
|
|
145
145
|
CORS_ORIGINS: z.ZodOptional<z.ZodString>;
|
|
146
146
|
REBASE_ROLE: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
147
147
|
"": "";
|
|
148
|
-
functions: "functions";
|
|
149
148
|
all: "all";
|
|
149
|
+
functions: "functions";
|
|
150
150
|
api: "api";
|
|
151
151
|
worker: "worker";
|
|
152
|
-
}>>, z.ZodTransform<"
|
|
152
|
+
}>>, z.ZodTransform<"all" | "functions" | "api" | "worker", "" | "all" | "functions" | "api" | "worker">>;
|
|
153
153
|
REBASE_CRON_SCHEDULER: z.ZodPipe<z.ZodOptional<z.ZodEnum<{
|
|
154
154
|
"": "";
|
|
155
155
|
true: "true";
|
|
@@ -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 "./src-
|
|
6
|
+
import "./src-Br6ARbs6.js";
|
|
7
7
|
import { r as logger } from "./logger-DO2PZc4i.js";
|
|
8
8
|
import { Hono } from "hono";
|
|
9
9
|
//#region ../types/src/types/project_manifest.ts
|
|
@@ -277,4 +277,4 @@ function createContractRoutes(config) {
|
|
|
277
277
|
//#endregion
|
|
278
278
|
export { SCHEMA_VERSION_HEADER as i, createContractRoutes as n, computeSchemaVersion as r, contract_routes_exports as t };
|
|
279
279
|
|
|
280
|
-
//# sourceMappingURL=contract-routes-
|
|
280
|
+
//# sourceMappingURL=contract-routes-eLxV0le1.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-routes-CusnEB5h.js","names":[],"sources":["../../types/src/types/project_manifest.ts","../../types/src/types/collection_contract.ts","../../types/src/types/schema_version.ts","../src/api/contract-routes.ts"],"sourcesContent":["/**\n * The project manifest (`rebase.json`) and the build artifacts derived from it.\n *\n * Three separate documents live in this file, and keeping them distinct matters:\n *\n * 1. {@link RebaseProjectManifest} — `rebase.json`. **Authored** by the developer,\n * committed to the repository. Declares topology only: which runtime major the\n * project targets, and which apps *this repository* contributes to the project.\n * Schema, security rules, hooks and functions stay in TypeScript under the\n * config package — nothing that needs a type system belongs here.\n *\n * 2. {@link RebaseProjectLink} — the per-checkout link (`.rebase/cloud.json`).\n * **Not committed**, because it is per-developer like a git remote. Says which\n * deployed project this working copy points at, whether that is a Rebase Cloud\n * project or the base URL of a self-hosted backend.\n *\n * 3. {@link RebaseBundleManifest} — `manifest.json` inside a built bundle.\n * **Generated**, never hand-edited. It is the lockfile analogue: the exact\n * contract a built artifact claims to satisfy, which the runtime validates\n * before it boots and a control plane validates before it deploys.\n *\n * A repository declares only the apps it contains. The set of apps belonging to a\n * project is held by the project itself, which is what makes multi-repo projects\n * work: two repositories never need to know about each other, only about the\n * project.\n */\n\nimport type { StorageSourceDefinition } from \"./storage_source\";\nimport type { ResourceGraph } from \"./resources\";\n\n/**\n * Which kind of thing an app is.\n *\n * - `backend` — the collections/hooks/functions that define the project's API.\n * Exactly one per *project* (not per repository); the registry enforces it.\n * - `static` — a pre-built client bundle (SPA, static site), served from the\n * backend process at its declared `path` or from a CDN. The admin panel is\n * one of these: it is an app in the user's repository like any other.\n *\n * That is the whole list. Ownership of the server process is a property of the\n * backend app ({@link RebaseBackendAppConfig.runtime}), not an app type.\n */\nexport type RebaseAppType = \"backend\" | \"static\";\n\n/**\n * The backend app: the project's API surface.\n *\n * Paths are relative to the directory holding `rebase.json`. The defaults match\n * the layout `rebase init` scaffolds, so a stock project may declare simply\n * `{ \"type\": \"backend\", \"runtime\": \"managed\" }`.\n */\nexport interface RebaseBackendAppConfig {\n type: \"backend\";\n /**\n * Who owns the process this backend runs in.\n *\n * - `managed` — the platform's runtime image boots this project's bundle.\n * You supply collections, functions, crons and schema; Rebase supplies the\n * server.\n * - `custom` — this repository builds its own image and entrypoint. The\n * escape hatch: full control, no managed-runtime guarantees.\n *\n * Independent of *where* it runs. Both run on Rebase Cloud and both\n * self-host — the destination lives in `.rebase/cloud.json`, not here. See\n * `infra/docker/docker-compose.selfhost.yml`, which boots a managed bundle on a\n * developer's own Docker host.\n *\n * This is authored rather than inferred on purpose. It is the single most\n * consequential fact about a deployment, and inferring it is what used to\n * land projects on the custom runtime without anyone choosing it.\n */\n runtime: \"managed\" | \"custom\";\n /** Directory of the config package (collections + index). Default `config`. */\n config?: string;\n /** Directory of server functions. Default `backend/functions`. */\n functions?: string;\n /** Directory of cron job definitions. Default `backend/crons` when present. */\n crons?: string;\n /**\n * Path to the generated Drizzle schema module (tables/enums/relations).\n * Default `backend/src/schema.generated.ts`.\n */\n schema?: string;\n /**\n * Module path (relative to `config`) exporting the auth users collection as\n * its default export. Default `collections/users`.\n */\n usersCollection?: string;\n\n /**\n * `runtime: \"custom\"` only. Dockerfile path relative to the repository root.\n * Default `Dockerfile`.\n */\n dockerfile?: string;\n /** `runtime: \"custom\"` only. Build context relative to the root. Default `.`. */\n context?: string;\n /** `runtime: \"custom\"` only. Port the container listens on. Default 8080. */\n port?: number;\n}\n\n/**\n * A static client bundle — SPA or static site — built here and served at `path`.\n */\nexport interface RebaseStaticAppConfig {\n type: \"static\";\n /** Package directory containing the client sources. */\n root: string;\n /** Command that produces `output`. Run from the repository root. */\n build?: string;\n /** Directory of built assets, relative to the repository root. */\n output: string;\n /**\n * Public base path this app is served under. Default `/`.\n *\n * Several static apps run in one process, each at its own path — the API at\n * `/api`, a site at `/`, the admin at `/admin` — which is what keeps a\n * self-hosted deployment a single container.\n *\n * **This is a build-time input, not only a serving concern.** An app mounted\n * at `/admin` must be *built* for `/admin` (Vite's `base`), or `index.html`\n * loads and every asset 404s: a blank page with no server error. `rebase\n * build` passes it as `REBASE_APP_BASE` and asserts the emitted HTML honours\n * it. Changing this value requires rebuilding the app.\n */\n path?: string;\n /**\n * Serve `index.html` for unmatched paths under `path` (client-side routing).\n * Default `true` — the overwhelmingly common case for a client app, and a\n * static *site* generator emits real files for its routes anyway.\n */\n spa?: boolean;\n}\n\nexport type RebaseAppConfig = RebaseBackendAppConfig | RebaseStaticAppConfig;\n\n/**\n * Path prefixes the backend owns, which no static app may claim.\n *\n * One process — and, on the platform, one hostname — serves both the API and\n * however many static apps a project has. Mounting is longest-path-first, so an\n * app declaring `/api` would win against the API itself and every request to it\n * would be answered with that app's `index.html`: a 200 carrying HTML where the\n * caller expected JSON, from a project that looks deployed and healthy.\n *\n * Declared here rather than in either enforcer because both must agree. The CLI\n * checks it so a developer finds out while editing `rebase.json`; the control\n * plane checks it again at deploy intake, because the front door's correctness\n * cannot rest on a check that ran in somebody else's CLI — and a repository can\n * be deployed by a CLI older than this rule.\n */\nexport const RESERVED_BACKEND_PREFIXES = [\"/api\", \"/health\", \"/healthz\", \"/livez\", \"/readyz\", \"/metrics\"] as const;\n\n/**\n * Whether `path` collides with a prefix the backend owns.\n *\n * Compares at segment boundaries, so `/api` and `/api/v2` collide while\n * `/apidocs` does not — the same rule the router matches with, because a check\n * that is stricter than the router rejects paths that would have worked, and one\n * that is looser admits paths that will not.\n */\nexport function reservedPrefixFor(path: string): string | undefined {\n const normalized = path.endsWith(\"/\") && path !== \"/\" ? path.slice(0, -1) : path;\n return RESERVED_BACKEND_PREFIXES.find(\n reserved => normalized === reserved || normalized.startsWith(`${reserved}/`)\n );\n}\n\n/**\n * One declared storage source, as authored in `rebase.json`.\n *\n * The key comes from the enclosing record, so this is\n * {@link StorageSourceDefinition} minus its `key` — the same document the\n * runtime registry and the frontend router consume, expressed the way a JSON\n * object naturally expresses \"a set of named things\".\n */\nexport interface RebaseStorageSourceConfig {\n /** Engine backing this source: `local`, `s3`, `gcs`, or a custom id. */\n engine: string;\n /**\n * How the frontend reaches it. Default `server` (proxied through\n * `/api/storage`). `direct` means a provider SDK talks to the bucket and the\n * backend is not in the upload path.\n */\n transport?: \"server\" | \"direct\";\n /** Human-readable label for the console and the admin UI. */\n label?: string;\n}\n\n/**\n * `rebase.json` — the authored project manifest.\n */\nexport interface RebaseProjectManifest {\n /** JSON Schema URL, for editor completion. Ignored by the tooling. */\n $schema?: string;\n /**\n * The runtime contract **major** this project targets, as a semver range\n * (e.g. `^1`, `~1.4`, or an exact `1.4.2` to pin).\n *\n * The platform upgrades patches and minors underneath a project without\n * asking; it never crosses a major. See {@link RUNTIME_CONTRACT_VERSION}.\n *\n * Named `rebase` rather than `runtime` so that `runtime` means exactly one\n * thing — {@link RebaseBackendAppConfig.runtime}, who owns the process. It\n * reads like `engines` in a `package.json`, which is what it is.\n */\n rebase: string;\n /**\n * Apps this repository contributes, keyed by app name. The key is the app's\n * identity within the project: it is what `rebase deploy <app>` names, what\n * client credentials are issued against, and what a second repository must\n * not collide with.\n */\n apps: Record<string, RebaseAppConfig>;\n /**\n * Buckets are NOT declared here any more.\n *\n * They were, and the runtime merged this block with the declarations in\n * config code — a bucket named in both had one engine kept and the other\n * silently discarded. Two homes for one concept, with a merge to decide\n * between them, is the shape this whole model replaced.\n *\n * `bucket(\"media\", { engine: \"s3\" })` in the project's config declares one\n * now, and `rebase resources --write` generates `rebase.resources.json`,\n * which is what a host reads before a build. A `storage` block left in this\n * file is refused by the validator, by name, with the replacement in the\n * message — not ignored, because a key that still parses and does nothing\n * is the failure this removed.\n */\n /**\n * Repository-wide opt-out from anonymous CLI usage sharing.\n *\n * **Only `false` does anything.** It suppresses sharing for everyone who\n * clones this repository, overriding each developer's own opt-in — an\n * organisation setting policy for work done on its behalf, the same shape\n * as a committed `.npmrc`.\n *\n * `true` is deliberately ignored, and the CLI says so rather than obeying\n * quietly. This file is committed, so a `true` here would be one developer\n * answering a privacy question for every colleague who later clones the\n * repo — consent by proxy, which is the exact thing opt-in exists to\n * prevent. Individuals opt in with `rebase telemetry enable`.\n */\n telemetry?: boolean;\n}\n\n/**\n * The per-checkout project link.\n *\n * Deliberately separate from `rebase.json`: the manifest is committed and shared,\n * while the link is per-developer. Keeping them in one file would mean either\n * committing someone's project id or gitignoring the topology.\n */\nexport interface RebaseProjectLink {\n /**\n * A Rebase Cloud project id, or the base URL of any running Rebase backend\n * (`https://api.example.com`). Both are first-class: every command that\n * accepts a project reference accepts either, so a self-hosted project has\n * the same tooling as a cloud one.\n */\n project: string;\n /** Organization slug. Cloud projects only. */\n org?: string;\n /** Explicit API base URL, when it differs from the project's default. */\n apiUrl?: string;\n}\n\n/**\n * Whether a project can run on the managed runtime, and if not, precisely why.\n *\n * The reasons are returned rather than summarised so tooling can print something\n * a developer can act on. \"Not eligible\" is never a dead end — it selects the\n * custom-runtime path, which still deploys.\n */\nexport interface ManagedCompatibility {\n eligible: boolean;\n reasons: string[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Bundle\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Version of the bundle *format* itself.\n *\n * Bumped only when the on-disk layout changes in a way an older runtime could\n * not read. A runtime accepts any bundle whose `bundleFormat` is less than or\n * equal to its own — old bundles keep booting on new runtimes, which is the\n * whole point of separating the artifact from the engine.\n *\n * - **1** — `mode: \"cms\" | \"baas\" | \"static\"`, `entry.static` a single directory\n * string, `entry.admin` for a bundled admin panel.\n * - **2** — `kind: \"backend\" | \"static\"`, `entry.static` a list of\n * {@link RebaseBundleStatic}, `entry.admin` removed. A format-1 runtime reading\n * one of these would find no `mode` and an array where it expects a string, so\n * the bump is what turns that into a refusal to boot instead of a bundle that\n * starts and serves nothing.\n */\nexport const BUNDLE_FORMAT_VERSION = 2;\n\n/**\n * The runtime contract major.\n *\n * Distinct from the `@rebasepro/server` package version: the package may release\n * any number of minors and patches while this stays put. It changes only when\n * the bundle/runtime contract breaks compatibility, and a project's\n * `manifest.runtime` range is matched against *this*.\n *\n * ## v2 — resources are declared, not configured\n *\n * `RebaseBackendConfig.dataSources` and `.storageSources` are gone. A project\n * declares its databases and buckets with `database()` / `bucket()` in its\n * config, and the runtime reads those declarations.\n *\n * This had to be a major, and the reason is the managed tier: it moves projects\n * onto new images WITHOUT rebuilding them. A bundle built against v1 exports\n * those keys, and a v2 runtime refuses them at boot — so without this bump, one\n * image rollout would crash-loop every tenant that had ever declared a second\n * database or bucket, in a wave, with the cause in a container log nobody is\n * watching.\n *\n * With the bump, a v1 bundle on a v2 runtime is refused by\n * `assertBundleCompatibility` with the remedy in the message, and the platform\n * keeps it on a v1 image until it is rebuilt. That is the whole purpose of this\n * number.\n *\n * **Release order matters and is not optional.** The control plane is the side\n * that rejects, so it ships FIRST: raise `SUPPORTED_RUNTIME_CONTRACT` in the\n * saas repo (it rejects only `contract >` its own, so it then accepts both),\n * deploy that, and only then release a runtime implementing v2. Shipping the\n * runtime first turns every deploy into a rejected intake blaming the tenant's\n * bundle.\n */\nexport const RUNTIME_CONTRACT_VERSION = 1;\n\n/** Where the runtime finds each part of the bundle. Paths are bundle-relative. */\nexport interface RebaseBundleEntrypoints {\n /** Compiled config package directory (collections live under it). */\n config?: string;\n /** Compiled collections directory, when it differs from `<config>/collections`. */\n collections?: string;\n /** Compiled functions directory. */\n functions?: string;\n /** Compiled crons directory. */\n crons?: string;\n /** Compiled Drizzle schema module. */\n schema?: string;\n /** Module exporting the auth users collection (default export). */\n usersCollection?: string;\n /**\n * Built static apps to serve from this process, in declaration order.\n *\n * A list rather than a single directory because one process serves several\n * apps at different paths — a site at `/` and the admin at `/admin`. The\n * runtime mounts them longest-path-first so the `/`-rooted app's catch-all\n * does not claim its siblings' URLs.\n */\n static?: RebaseBundleStatic[];\n}\n\n/** One built static app inside a bundle. */\nexport interface RebaseBundleStatic {\n /** Public base path, e.g. `/` or `/admin`. */\n path: string;\n /** Bundle-relative directory holding the built assets. */\n dir: string;\n /** Serve `index.html` for unmatched paths under `path`. */\n spa: boolean;\n}\n\n/**\n * A native module found in the dependency closure.\n *\n * Recorded rather than merely counted so a rejection can name the offending\n * package instead of saying \"something here is native\".\n */\nexport interface NativeDependency {\n name: string;\n /** Why it was flagged — a `.node` binary, a gyp build, or an install script. */\n reason: string;\n}\n\n/**\n * `manifest.json` — generated, and the document the runtime and control plane\n * both validate against.\n */\n/**\n * One custom function, as recorded in a built bundle.\n *\n * @see RebaseBundleManifest.functions\n */\nexport interface RebaseBundleFunction {\n /**\n * The filename without its extension — which is also the URL segment it\n * mounts at (`/api/functions/<name>`), the API-key permission that grants\n * it, and the name `REBASE_FUNCTIONS_ONLY` selects by. One identity, used\n * everywhere.\n */\n name: string;\n /** Path inside the bundle, so a host can point at the file. */\n file: string;\n /**\n * `false` when the function's own source imports a Node built-in or a\n * package that needs one.\n *\n * Descriptive, never a gate: nothing refuses to build or deploy on this. It\n * says where this function *could* run, not where it should.\n */\n portable: boolean;\n /**\n * Why it is not portable — one short phrase per reason, deduplicated.\n * Absent when it is.\n */\n requires?: string[];\n}\n\nexport interface RebaseBundleManifest {\n /** @see BUNDLE_FORMAT_VERSION */\n bundleFormat: number;\n runtime: {\n /** The `runtime` range copied from `rebase.json`. */\n range: string;\n /** Exact `@rebasepro/server` version this bundle was built against. */\n builtAgainst: string;\n /** Runtime contract major this bundle requires. */\n contract: number;\n };\n /**\n * Hash of the compiled collection definitions.\n *\n * This is the contract stamp. A generated SDK records the value it was built\n * from, a client sends it back, and a mismatch is what lets the platform say\n * \"this app was built against an older schema\" instead of failing mysteriously\n * at the first request. It covers collections only — a hook edit does not\n * change a client's contract, so it must not invalidate every SDK.\n */\n schemaVersion: string;\n /** Which app in `rebase.json` this bundle was built from. */\n app: string;\n /**\n * What the runtime does with this bundle.\n *\n * - `backend` — boot the full server: database, auth and the data API, plus\n * any static apps in `entry.static`.\n * - `static` — no backend at all: serve `entry.static` and nothing else. No\n * database, no auth, no data sources. This is how a static app runs on the\n * same image as the backend.\n *\n * Replaces an earlier `mode: \"cms\" | \"baas\" | \"static\"`. The cms/baas\n * distinction was never a third kind of thing — it is simply whether\n * `entry.config` is present, so it is derived rather than declared.\n */\n kind: \"backend\" | \"static\";\n entry: RebaseBundleEntrypoints;\n /** Collection slugs contained in the bundle, for quick inspection. */\n collections?: string[];\n /**\n * Every custom function in the bundle, named and classified.\n *\n * Two things are recorded per function, and both are answers a host would\n * otherwise have to get by importing user code:\n *\n * - **What it is called.** That name is the function's identity everywhere —\n * the URL segment it mounts at, the `functions/<name>` API-key\n * permission, the value `REBASE_FUNCTIONS_ONLY` selects by. A host that\n * wants to give one slow function its own replica count currently has to\n * boot the bundle to discover what is in it.\n * - **Whether it needs Node.** Purely descriptive: a function that opens a\n * file or runs raw SQL is a fine function, and every deployment today is\n * a Node process. It is recorded because the question \"which of these\n * could run somewhere else\" has to be answerable from the artifact, and\n * because answering it per-file after the fact — across a codebase\n * already written — is the expensive version of the same question.\n *\n * Absent on a bundle built before this field existed, which is why every\n * consumer must treat it as optional rather than as an empty list.\n */\n functions?: RebaseBundleFunction[];\n hooks: {\n /**\n * Whether the dependency closure contains native code.\n *\n * The managed runtime refuses these: a prebuilt binary cannot be run on\n * an image the platform did not build it for, and the honest failure is\n * at deploy time rather than at 3am in a crash loop.\n */\n native: boolean;\n nativeModules?: NativeDependency[];\n };\n /**\n * What the bundle's config says about storage access control.\n *\n * Storage is not under RLS and its keys share one flat namespace, so a\n * deployment with file storage enabled and no access model serves every\n * user's files to every signed-in user. The runtime refuses to boot in that\n * state — which, on a hosted platform that enables storage from the *console*\n * rather than from the bundle, surfaces as a crash loop the developer cannot\n * read.\n *\n * Recording it here lets a host reject the deploy with the reason instead.\n * Absent on bundles built before this field existed.\n */\n storage?: {\n /** Whether the config package exports a `storageAuthorize` hook. */\n authorize: boolean;\n /**\n * Buckets, on bundles built before {@link RebaseBundleManifest.resources}.\n *\n * No longer written. A host reads `resources`, which carries every kind\n * in one list; this stays declared so a control plane can keep reading\n * the bundles a project shipped before it was rebuilt.\n */\n sources?: StorageSourceDefinition[];\n };\n /**\n * Everything the project declares it needs — databases, buckets, topics,\n * and whatever kind is registered next.\n *\n * Recorded so a host can tell, from the artifact alone and before starting\n * anything, what a deploy will need provisioned. That question used to be\n * answerable for buckets and for nothing else, because buckets were the\n * only kind written into an artifact — which is how a project's databases\n * became invisible to the platform that runs them.\n *\n * Absent on bundles built before this field existed.\n */\n resources?: ResourceGraph;\n deps: {\n /** Runtime dependencies of user code, as declared. */\n declared: Record<string, string>;\n /**\n * The dependency tree ships *inside* the bundle, already installed.\n *\n * Absent or false means the tree is declared but not present, and\n * whoever boots the bundle has to install it. On the managed runtime that\n * install runs in an init container on **every** pod start — the bundle\n * lives on a volume that is wiped each time — and it is the single\n * largest cost in a managed pod's life: 35–55 seconds of a 40–60 second\n * cold start. Since a pod restarts on every eviction, node failure, OOM\n * and runtime rollout, that number is not a startup detail. It is what an\n * outage costs.\n *\n * Vendoring moves the install to build time, where it happens once. It is\n * skipped when the closure contains native code, because a prebuilt\n * binary is only valid for the platform it was built for — see\n * {@link vendorTarget} for what \"the platform\" means here.\n */\n vendored?: boolean;\n /**\n * What {@link vendored} was resolved for, recorded so a mismatch can be\n * refused rather than discovered at import time.\n *\n * Cross-platform vendoring is safe for pure JavaScript and unsafe for\n * anything compiled, and the boundary between them is not always visible\n * in a dependency list: `esbuild` is pure-JS with a *platform-specific\n * optional dependency* holding the actual binary, so an install run on a\n * developer's Mac silently produces a tree that cannot run on the Linux\n * image. The install therefore resolves optional dependencies for the\n * target explicitly rather than for the machine it runs on, and records\n * the answer here.\n */\n vendorTarget?: {\n /** npm `--os`, e.g. `linux`. */\n os: string;\n /** npm `--cpu`, e.g. `x64`. */\n cpu: string;\n /** Node major the tree was resolved for. */\n node: string;\n };\n };\n build: {\n /** `@rebasepro/cli` version that produced this bundle. */\n cli: string;\n /** Node major the bundle was compiled on. */\n node: string;\n /** ISO-8601. */\n createdAt: string;\n };\n}\n\n/** The contract a running backend serves at `GET /api/meta/contract`. */\nexport interface RebaseProjectContract {\n /** Matches {@link RebaseBundleManifest.schemaVersion}. */\n schemaVersion: string;\n runtime: {\n /** `@rebasepro/server` version currently running. */\n version: string;\n contract: number;\n };\n /** Full collection definitions, serialized — the input to SDK generation. */\n collections: unknown[];\n /** Collection slugs, for cheap inspection without parsing the definitions. */\n collectionSlugs: string[];\n generatedAt: string;\n}\n\n/** Header carrying the schema version an SDK was generated from. */\nexport const SCHEMA_VERSION_HEADER = \"x-rebase-schema\";\n","import type { CollectionConfig } from \"./collections\";\n\n/**\n * Serializing collections so they survive a network hop.\n *\n * A collection definition is not plain data. Relations point at their target\n * with a *function* (`target: () => usersCollection`) so two collections can\n * reference each other without an import cycle, and collections also carry\n * callbacks, custom views and component references. `JSON.stringify` silently\n * drops every one of those, which matters because the SDK generator *calls*\n * `relation.target()` to decide whether a foreign key is a string or a number.\n * Serialize naively and remote SDK generation produces subtly wrong types\n * instead of failing — the worst possible outcome.\n *\n * So relation targets are resolved to a slug reference on the way out and\n * rebuilt into functions on the way in. Everything else that cannot cross a wire\n * is dropped deliberately: an SDK is generated from the *shape* of the data, and\n * server-side behaviour is neither useful to a client nor safe to publish.\n */\n\n/** Marker replacing a relation's `target` function in serialized form. */\nexport interface SerializedCollectionRef {\n __collectionRef: string;\n}\n\nexport function isSerializedCollectionRef(value: unknown): value is SerializedCollectionRef {\n return typeof value === \"object\"\n && value !== null\n && typeof (value as SerializedCollectionRef).__collectionRef === \"string\";\n}\n\n/** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */\nconst MAX_DEPTH = 64;\n\n/**\n * Resolve whatever a `target` thunk returns down to a collection.\n *\n * A target may be the collection, a module namespace (when the authoring file\n * used `import * as`), or a default-export wrapper. All three appear in real\n * projects, and the SDK generator already unwraps them the same way.\n */\nfunction unwrapTarget(value: unknown): CollectionConfig | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const candidate = value as { default?: unknown; __esModule?: boolean; properties?: unknown };\n if (candidate.default || candidate.__esModule) {\n const inner = candidate.default;\n if (inner && typeof inner === \"object\") return inner as CollectionConfig;\n }\n if (candidate.properties) return value as CollectionConfig;\n return undefined;\n}\n\n/** The identity a serialized reference uses. Slug first — it is the routing key. */\nfunction refFor(collection: CollectionConfig | undefined): string | undefined {\n if (!collection) return undefined;\n const withPath = collection as CollectionConfig & { path?: string };\n return collection.slug || withPath.path || collection.name;\n}\n\n/**\n * Deep-copy a value into something JSON can carry.\n *\n * `target` keys are special-cased into refs. Other functions vanish, cycles are\n * cut, and everything else is copied structurally.\n */\n/** Shared walk state: the memo, plus a count of depth-cap hits. */\ninterface WalkState {\n memo: WeakMap<object, unknown>;\n /**\n * How many times the walk has truncated a subtree — by hitting the depth\n * cap, or by cutting a cycle.\n *\n * Either kind of truncation makes a result valid only at the *position* it\n * was produced at, so caching it and serving it elsewhere silently drops\n * content that would have been included. Comparing this counter before and\n * after a node's children tells us whether its result is position-\n * independent and therefore safe to memoize.\n *\n * The cycle case is the subtle one: with `a.b = b` and `b.a = a`, serializing\n * `{ first: b, second: a }` visits `a` beneath `b` — where the cycle back to\n * `b` is cut — and would then reuse that truncated `a` for `second`, where\n * nothing needed cutting.\n */\n truncations: number;\n}\n\nfunction toSerializable(\n value: unknown,\n seen: WeakSet<object>,\n depth: number,\n state: WalkState,\n key?: string\n): unknown {\n if (depth > MAX_DEPTH) {\n state.truncations++;\n return undefined;\n }\n\n if (typeof value === \"function\") {\n // Only a relation target carries information a client needs. Calling it\n // is safe here — this runs on the server, where the target module is\n // already loaded — and a throwing target simply yields no reference,\n // which degrades the generated FK type rather than failing the request.\n if (key === \"target\") {\n try {\n const resolved = unwrapTarget((value as () => unknown)());\n const ref = refFor(resolved);\n return ref ? { __collectionRef: ref } : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) return value.toISOString();\n if (value instanceof RegExp) return value.source;\n\n if (seen.has(value as object)) {\n state.truncations++;\n return undefined;\n }\n\n // A shared (non-cyclic) subgraph is reachable by many paths, and `seen` is a\n // *path* set — released in the `finally` below so a node referenced twice in\n // different branches is emitted twice rather than dropped as a false cycle.\n // Without memoization that makes the walk exponential in depth: a diamond\n // graph 20 levels deep took ~400ms, and each further level doubled it. The\n // result is a plain data tree, so handing back the same converted object for\n // a repeat visit is indistinguishable after JSON.stringify.\n const cached = state.memo.get(value as object);\n if (cached !== undefined) return cached;\n\n seen.add(value as object);\n const truncationsBefore = state.truncations;\n const memoize = (result: unknown): unknown => {\n // Only cache a result that nothing was cut from.\n if (result !== undefined && state.truncations === truncationsBefore) {\n state.memo.set(value as object, result);\n }\n return result;\n };\n\n try {\n if (Array.isArray(value)) {\n const items = value\n .map(item => toSerializable(item, seen, depth + 1, state))\n .filter(item => item !== undefined);\n // A container that had content, none of which can be represented, is\n // itself unrepresentable — see the note below.\n return memoize(value.length > 0 && items.length === 0 ? undefined : items);\n }\n\n // A React element or component reference has no meaning to a client and\n // will not survive JSON anyway.\n if (\"$$typeof\" in (value as Record<string, unknown>)) return undefined;\n\n const entries = Object.entries(value as Record<string, unknown>);\n const out: Record<string, unknown> = {};\n for (const [k, v] of entries) {\n const converted = toSerializable(v, seen, depth + 1, state, k);\n if (converted !== undefined) out[k] = converted;\n }\n\n // Drop a container whose entire content was dropped.\n //\n // `callbacks: { beforeSave() {…} }` would otherwise serialize to\n // `callbacks: {}` — an empty husk that carries no information but is not\n // *nothing*, so it lands in the payload and, worse, in the schema hash.\n // Editing a hook would then change every client's schema version and\n // report perfectly current SDKs as stale.\n //\n // A container that started empty stays empty: `properties: {}` is a\n // deliberate statement, not a casualty.\n if (entries.length > 0 && Object.keys(out).length === 0) return undefined;\n\n return memoize(out);\n } finally {\n // Released so a collection referenced twice in different branches is\n // emitted twice rather than being dropped as a false cycle.\n seen.delete(value as object);\n }\n}\n\n/**\n * Serialize collections for transport over the contract endpoint.\n *\n * Sorted by slug so the output — and therefore the schema hash computed from it\n * — does not depend on filesystem ordering.\n */\nexport function serializeCollections(collections: CollectionConfig[]): unknown[] {\n return [...collections]\n .sort((a, b) => String(a.slug ?? \"\").localeCompare(String(b.slug ?? \"\")))\n .map(collection => toSerializable(withoutAdminBlock(collection), new WeakSet(), 0, {\n memo: new WeakMap(),\n truncations: 0\n }))\n .filter((c): c is Record<string, unknown> => c !== undefined);\n}\n\n/**\n * Drop the admin block before the walk.\n *\n * Nothing downstream of serialization is an admin panel. The contract endpoint\n * feeds remote SDK generation, and `rebase build` writes the result into a bundle\n * manifest that only the backend runtime reads. The block would survive the walk\n * as a husk anyway — its React elements and component functions are dropped\n * individually — and that husk has two costs worth avoiding: it puts every custom\n * component's *file path* on an endpoint whose job is to describe data shapes, and\n * it grows a payload that is fetched and cached per project.\n *\n * Removing it here rather than at each call site means one chokepoint, so a future\n * consumer of `serializeCollections` cannot forget.\n *\n * Child collections carry their own block, so this recurses — stripping only the\n * top level was the mistake `stripNonClientFields` in the contract routes already\n * had to fix once for security rules.\n */\nfunction withoutAdminBlock(collection: CollectionConfig): CollectionConfig {\n const { admin: _admin, ...rest } = collection as CollectionConfig & Record<string, unknown>;\n const nested = rest as Record<string, unknown>;\n if (Array.isArray(nested.subcollections)) {\n nested.subcollections = nested.subcollections.map(\n (child) => withoutAdminBlock(child as CollectionConfig)\n );\n }\n return rest as CollectionConfig;\n}\n\n/**\n * Rebuild collections received from a contract endpoint.\n *\n * Relation refs become real thunks resolving through the returned set, so\n * downstream consumers — the SDK generator above all — see exactly the shape\n * they would have seen had the collections been imported from source.\n *\n * A ref naming a collection that is not in the payload resolves to `undefined`\n * rather than throwing: the generator already tolerates an unresolvable target\n * by falling back to a permissive key type, and a partial contract should still\n * produce a usable SDK.\n */\nexport function deserializeCollections(payload: unknown[]): CollectionConfig[] {\n const collections = payload\n .filter((c): c is Record<string, unknown> => typeof c === \"object\" && c !== null)\n .map(c => ({ ...c })) as unknown as CollectionConfig[];\n\n const bySlug = new Map<string, CollectionConfig>();\n for (const collection of collections) {\n const ref = refFor(collection);\n if (ref) bySlug.set(ref, collection);\n }\n\n const rehydrate = (value: unknown, depth: number): void => {\n if (depth > MAX_DEPTH || !value || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n for (const item of value) rehydrate(item, depth + 1);\n return;\n }\n\n const record = value as Record<string, unknown>;\n for (const [key, child] of Object.entries(record)) {\n if (key === \"target\" && isSerializedCollectionRef(child)) {\n const slug = child.__collectionRef;\n record.target = () => bySlug.get(slug);\n continue;\n }\n rehydrate(child, depth + 1);\n }\n };\n\n for (const collection of collections) rehydrate(collection, 0);\n return collections;\n}\n","import type { CollectionConfig } from \"./collections\";\nimport { serializeCollections } from \"./collection_contract\";\n\n/**\n * The schema version stamp.\n *\n * One function, used in three places that must agree or the whole drift-detection\n * story is noise: `rebase build` writes it into a bundle manifest, the runtime\n * serves it from the contract endpoint, and a generated SDK records the value it\n * was built from. If any two of those computed it differently, every client would\n * look permanently out of date.\n *\n * It covers **collections only** — the client's contract is the shape of the\n * data, so editing a hook or a server function must not invalidate every SDK in\n * every repository. That is a deliberate narrowing, not an oversight.\n */\n\n/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(\",\")}}`;\n}\n\n/**\n * Reduce a collection to the parts a generated client is actually built from.\n *\n * The version answers one question — \"is this SDK stale?\" — so it must change\n * exactly when the generated types could change, and never otherwise. Hashing a\n * whole collection fails both halves of that:\n *\n * - Security rules, callbacks, icons, groups and UI settings do not appear in a\n * generated client, so including them reports perfectly current SDKs as stale.\n * - Worse, they are not stable *inputs*. The runtime applies default security\n * rules when it loads collections, so the same source hashed before and after\n * loading produced two different answers — a build-time stamp that could never\n * match the server that served it.\n *\n * Codegen reads the slug (for the `Database` key and type names), the properties,\n * and the relations. That is the projection.\n */\nfunction projectForCodegen(collection: CollectionConfig): Record<string, unknown> {\n const source = collection as CollectionConfig & {\n relations?: unknown;\n subcollections?: CollectionConfig[];\n path?: string;\n engine?: unknown;\n dataSource?: unknown;\n };\n\n return {\n slug: collection.slug ?? source.path,\n properties: collection.properties,\n relations: source.relations,\n // The engine decides whether relations are resolved at all: codegen asks\n // `getDataSourceCapabilities(collection.engine).supportsRelations`, and an\n // engine that answers no drops every foreign-key column from the\n // generated Row/Insert/Update types. Moving a collection to such an\n // engine is a real change to the generated types, so it has to move the\n // version. `dataSource` is what resolves to `engine`, so it counts too.\n engine: source.engine,\n dataSource: source.dataSource,\n subcollections: source.subcollections?.map(projectForCodegen)\n };\n}\n\n/**\n * Compute the canonical string a schema version hashes.\n *\n * Exposed separately so the hashing itself can differ by environment: Node has\n * `crypto`, and callers without it can still compare canonical forms directly.\n */\nexport function canonicalSchemaPayload(collections: CollectionConfig[]): string {\n const projected = serializeCollections(collections)\n .map(collection => projectForCodegen(collection as CollectionConfig));\n return canonicalize(projected);\n}\n\n/**\n * A short, non-cryptographic digest of the canonical payload.\n *\n * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a\n * security boundary: nothing trusts a schema version to prove anything, it only\n * answers \"is this the same schema as before\". A hand-rolled hash keeps this\n * module free of `node:crypto`, so the identical function runs in the browser,\n * in the CLI, and in the runtime — which is the property that actually matters.\n */\nexport function computeSchemaVersion(collections: CollectionConfig[]): string {\n const payload = canonicalSchemaPayload(collections);\n\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193;\n\n for (let i = 0; i < payload.length; i++) {\n const code = payload.charCodeAt(i);\n h1 ^= code;\n // Multiply by the FNV prime using shifts to stay in 32-bit integer math.\n h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;\n h2 ^= code + i;\n h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;\n }\n\n const hex = (n: number): string => n.toString(16).padStart(8, \"0\");\n return `v1:${hex(h1)}${hex(h2)}`;\n}\n","import { Hono } from \"hono\";\nimport {\n RUNTIME_CONTRACT_VERSION,\n SCHEMA_VERSION_HEADER,\n computeSchemaVersion,\n serializeCollections,\n type CollectionConfig,\n type RebaseProjectContract\n} from \"@rebasepro/types\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The project contract endpoint.\n *\n * This is what makes a repository able to build against a project it does not\n * contain. Without it, a typed client can only be generated from local\n * collection *source*, which means every frontend must live in the same\n * repository as the backend. Serving the contract turns that around: an app\n * asks the project what its shape is, so a web app, a second web app and a\n * mobile app can each live wherever they like and none of them needs to know\n * about the others.\n *\n * Admin-gated. Collection definitions describe every table, column and relation\n * in the project, including ones no security rule would ever expose — that is a\n * map of the database, not public API documentation.\n */\n\nexport interface ContractRoutesConfig {\n collectionRegistry: { getRawCollections(): CollectionConfig[] };\n /**\n * The schema version recorded at build time.\n *\n * Preferred over recomputing, so that what a client is told matches exactly\n * what the bundle claims. It is recomputed only when a bundle did not record\n * one — a `baas`-mode project derives its collections from the live database\n * at boot, so there was nothing to hash when it was built.\n */\n schemaVersion?: string;\n /** Runtime package version, surfaced so a client can report what it built against. */\n runtimeVersion?: string;\n}\n\n/**\n * Strip everything a client does not need from a serialized collection.\n *\n * The generator reads the slug, the properties and the relations. It never reads\n * a security rule — but `securityRules` carries the raw SQL of every RLS\n * predicate guarding the project, which is a description of the authorization\n * model rather than of the data shape. Publishing it to anyone who can generate\n * an SDK gives away more than the endpoint is for, so it is removed here rather\n * than trusted not to matter.\n */\nfunction stripNonClientFields(collection: unknown): unknown {\n if (!collection || typeof collection !== \"object\") return collection;\n const {\n securityRules: _securityRules,\n callbacks: _callbacks,\n ...rest\n } = collection as Record<string, unknown>;\n\n // Subcollections are collections, and carry their own rules. Stripping only\n // the top level published every nested policy — the leak this exists to\n // prevent, just one level down.\n if (Array.isArray(rest.subcollections)) {\n rest.subcollections = rest.subcollections.map(stripNonClientFields);\n }\n\n return rest;\n}\n\nexport function createContractRoutes(config: ContractRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n // Computing a version walks and canonicalizes every collection, and\n // `/schema-version` is deliberately unauthenticated and meant to be polled.\n // Recomputing per request would make a CI convenience into a CPU\n // amplification anyone could aim at the server. Collections do not change\n // after boot, so once is enough.\n let cachedVersion: string | undefined;\n const schemaVersionOf = (collections: CollectionConfig[]): string => {\n if (config.schemaVersion) return config.schemaVersion;\n if (cachedVersion === undefined) cachedVersion = computeSchemaVersion(collections);\n return cachedVersion;\n };\n\n router.get(\"/contract\", (c) => {\n const collections = config.collectionRegistry.getRawCollections();\n const serialized = serializeCollections(collections).map(stripNonClientFields);\n\n // In `baas` mode the collections are whatever introspection found at\n // boot, so the version has to be computed from them rather than taken\n // from a build that never saw them.\n const schemaVersion = schemaVersionOf(collections);\n\n const contract: RebaseProjectContract = {\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n },\n collections: serialized,\n collectionSlugs: collections\n .map(collection => collection.slug)\n .filter((slug): slug is string => Boolean(slug))\n .sort(),\n generatedAt: new Date().toISOString()\n };\n\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json(contract);\n });\n\n /**\n * Cheap drift check.\n *\n * Deliberately unauthenticated and deliberately tiny: two version stamps\n * and nothing else. A CI job that only wants to know whether its generated\n * SDK is stale should not need admin credentials, and a version stamp\n * reveals nothing about the schema it stands for.\n *\n * `runtime` is here as well as on `/contract` because the two answer\n * different questions and only one of them was reachable. Which runtime a\n * project is on decides whether a client's wire format is understood at\n * all, and it was published solely on the admin-gated route — so a CLI or\n * an SDK, the two callers that actually need to know, could not ask. That\n * is the same shape as the header this route echoes: a documented signal\n * with no reachable sender. `contract` is the number that matters for\n * compatibility; `version` names the release a human should quote.\n */\n router.get(\"/schema-version\", (c) => {\n const schemaVersion = schemaVersionOf(config.collectionRegistry.getRawCollections());\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json({\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n }\n });\n });\n\n logger.debug(\"Contract routes mounted\");\n return router;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contract-routes-eLxV0le1.js","names":[],"sources":["../../types/src/types/project_manifest.ts","../../types/src/types/collection_contract.ts","../../types/src/types/schema_version.ts","../src/api/contract-routes.ts"],"sourcesContent":["/**\n * The project manifest (`rebase.json`) and the build artifacts derived from it.\n *\n * Three separate documents live in this file, and keeping them distinct matters:\n *\n * 1. {@link RebaseProjectManifest} — `rebase.json`. **Authored** by the developer,\n * committed to the repository. Declares topology only: which runtime major the\n * project targets, and which apps *this repository* contributes to the project.\n * Schema, security rules, hooks and functions stay in TypeScript under the\n * config package — nothing that needs a type system belongs here.\n *\n * 2. {@link RebaseProjectLink} — the per-checkout link (`.rebase/cloud.json`).\n * **Not committed**, because it is per-developer like a git remote. Says which\n * deployed project this working copy points at, whether that is a Rebase Cloud\n * project or the base URL of a self-hosted backend.\n *\n * 3. {@link RebaseBundleManifest} — `manifest.json` inside a built bundle.\n * **Generated**, never hand-edited. It is the lockfile analogue: the exact\n * contract a built artifact claims to satisfy, which the runtime validates\n * before it boots and a control plane validates before it deploys.\n *\n * A repository declares only the apps it contains. The set of apps belonging to a\n * project is held by the project itself, which is what makes multi-repo projects\n * work: two repositories never need to know about each other, only about the\n * project.\n */\n\nimport type { StorageSourceDefinition } from \"./storage_source\";\nimport type { ResourceGraph } from \"./resources\";\n\n/**\n * Which kind of thing an app is.\n *\n * - `backend` — the collections/hooks/functions that define the project's API.\n * Exactly one per *project* (not per repository); the registry enforces it.\n * - `static` — a pre-built client bundle (SPA, static site), served from the\n * backend process at its declared `path` or from a CDN. The admin panel is\n * one of these: it is an app in the user's repository like any other.\n *\n * That is the whole list. Ownership of the server process is a property of the\n * backend app ({@link RebaseBackendAppConfig.runtime}), not an app type.\n */\nexport type RebaseAppType = \"backend\" | \"static\";\n\n/**\n * The backend app: the project's API surface.\n *\n * Paths are relative to the directory holding `rebase.json`. The defaults match\n * the layout `rebase init` scaffolds, so a stock project may declare simply\n * `{ \"type\": \"backend\", \"runtime\": \"managed\" }`.\n */\nexport interface RebaseBackendAppConfig {\n type: \"backend\";\n /**\n * Who owns the process this backend runs in.\n *\n * - `managed` — the platform's runtime image boots this project's bundle.\n * You supply collections, functions, crons and schema; Rebase supplies the\n * server.\n * - `custom` — this repository builds its own image and entrypoint. The\n * escape hatch: full control, no managed-runtime guarantees.\n *\n * Independent of *where* it runs. Both run on Rebase Cloud and both\n * self-host — the destination lives in `.rebase/cloud.json`, not here. See\n * `infra/docker/docker-compose.selfhost.yml`, which boots a managed bundle on a\n * developer's own Docker host.\n *\n * This is authored rather than inferred on purpose. It is the single most\n * consequential fact about a deployment, and inferring it is what used to\n * land projects on the custom runtime without anyone choosing it.\n */\n runtime: \"managed\" | \"custom\";\n /** Directory of the config package (collections + index). Default `config`. */\n config?: string;\n /** Directory of server functions. Default `backend/functions`. */\n functions?: string;\n /** Directory of cron job definitions. Default `backend/crons` when present. */\n crons?: string;\n /**\n * Path to the generated Drizzle schema module (tables/enums/relations).\n * Default `backend/src/schema.generated.ts`.\n */\n schema?: string;\n /**\n * Module path (relative to `config`) exporting the auth users collection as\n * its default export. Default `collections/users`.\n */\n usersCollection?: string;\n\n /**\n * `runtime: \"custom\"` only. Dockerfile path relative to the repository root.\n * Default `Dockerfile`.\n */\n dockerfile?: string;\n /** `runtime: \"custom\"` only. Build context relative to the root. Default `.`. */\n context?: string;\n /** `runtime: \"custom\"` only. Port the container listens on. Default 8080. */\n port?: number;\n}\n\n/**\n * A static client bundle — SPA or static site — built here and served at `path`.\n */\nexport interface RebaseStaticAppConfig {\n type: \"static\";\n /** Package directory containing the client sources. */\n root: string;\n /** Command that produces `output`. Run from the repository root. */\n build?: string;\n /** Directory of built assets, relative to the repository root. */\n output: string;\n /**\n * Public base path this app is served under. Default `/`.\n *\n * Several static apps run in one process, each at its own path — the API at\n * `/api`, a site at `/`, the admin at `/admin` — which is what keeps a\n * self-hosted deployment a single container.\n *\n * **This is a build-time input, not only a serving concern.** An app mounted\n * at `/admin` must be *built* for `/admin` (Vite's `base`), or `index.html`\n * loads and every asset 404s: a blank page with no server error. `rebase\n * build` passes it as `REBASE_APP_BASE` and asserts the emitted HTML honours\n * it. Changing this value requires rebuilding the app.\n */\n path?: string;\n /**\n * Serve `index.html` for unmatched paths under `path` (client-side routing).\n * Default `true` — the overwhelmingly common case for a client app, and a\n * static *site* generator emits real files for its routes anyway.\n */\n spa?: boolean;\n}\n\nexport type RebaseAppConfig = RebaseBackendAppConfig | RebaseStaticAppConfig;\n\n/**\n * Path prefixes the backend owns, which no static app may claim.\n *\n * One process — and, on the platform, one hostname — serves both the API and\n * however many static apps a project has. Mounting is longest-path-first, so an\n * app declaring `/api` would win against the API itself and every request to it\n * would be answered with that app's `index.html`: a 200 carrying HTML where the\n * caller expected JSON, from a project that looks deployed and healthy.\n *\n * Declared here rather than in either enforcer because both must agree. The CLI\n * checks it so a developer finds out while editing `rebase.json`; the control\n * plane checks it again at deploy intake, because the front door's correctness\n * cannot rest on a check that ran in somebody else's CLI — and a repository can\n * be deployed by a CLI older than this rule.\n */\nexport const RESERVED_BACKEND_PREFIXES = [\"/api\", \"/health\", \"/healthz\", \"/livez\", \"/readyz\", \"/metrics\"] as const;\n\n/**\n * Whether `path` collides with a prefix the backend owns.\n *\n * Compares at segment boundaries, so `/api` and `/api/v2` collide while\n * `/apidocs` does not — the same rule the router matches with, because a check\n * that is stricter than the router rejects paths that would have worked, and one\n * that is looser admits paths that will not.\n */\nexport function reservedPrefixFor(path: string): string | undefined {\n const normalized = path.endsWith(\"/\") && path !== \"/\" ? path.slice(0, -1) : path;\n return RESERVED_BACKEND_PREFIXES.find(\n reserved => normalized === reserved || normalized.startsWith(`${reserved}/`)\n );\n}\n\n/**\n * One declared storage source, as authored in `rebase.json`.\n *\n * The key comes from the enclosing record, so this is\n * {@link StorageSourceDefinition} minus its `key` — the same document the\n * runtime registry and the frontend router consume, expressed the way a JSON\n * object naturally expresses \"a set of named things\".\n */\nexport interface RebaseStorageSourceConfig {\n /** Engine backing this source: `local`, `s3`, `gcs`, or a custom id. */\n engine: string;\n /**\n * How the frontend reaches it. Default `server` (proxied through\n * `/api/storage`). `direct` means a provider SDK talks to the bucket and the\n * backend is not in the upload path.\n */\n transport?: \"server\" | \"direct\";\n /** Human-readable label for the console and the admin UI. */\n label?: string;\n}\n\n/**\n * `rebase.json` — the authored project manifest.\n */\nexport interface RebaseProjectManifest {\n /** JSON Schema URL, for editor completion. Ignored by the tooling. */\n $schema?: string;\n /**\n * The runtime contract **major** this project targets, as a semver range\n * (e.g. `^1`, `~1.4`, or an exact `1.4.2` to pin).\n *\n * The platform upgrades patches and minors underneath a project without\n * asking; it never crosses a major. See {@link RUNTIME_CONTRACT_VERSION}.\n *\n * Named `rebase` rather than `runtime` so that `runtime` means exactly one\n * thing — {@link RebaseBackendAppConfig.runtime}, who owns the process. It\n * reads like `engines` in a `package.json`, which is what it is.\n */\n rebase: string;\n /**\n * Apps this repository contributes, keyed by app name. The key is the app's\n * identity within the project: it is what `rebase deploy <app>` names, what\n * client credentials are issued against, and what a second repository must\n * not collide with.\n */\n apps: Record<string, RebaseAppConfig>;\n /**\n * Buckets are NOT declared here any more.\n *\n * They were, and the runtime merged this block with the declarations in\n * config code — a bucket named in both had one engine kept and the other\n * silently discarded. Two homes for one concept, with a merge to decide\n * between them, is the shape this whole model replaced.\n *\n * `bucket(\"media\", { engine: \"s3\" })` in the project's config declares one\n * now, and `rebase resources --write` generates `rebase.resources.json`,\n * which is what a host reads before a build. A `storage` block left in this\n * file is refused by the validator, by name, with the replacement in the\n * message — not ignored, because a key that still parses and does nothing\n * is the failure this removed.\n */\n /**\n * Repository-wide opt-out from anonymous CLI usage sharing.\n *\n * **Only `false` does anything.** It suppresses sharing for everyone who\n * clones this repository, overriding each developer's own opt-in — an\n * organisation setting policy for work done on its behalf, the same shape\n * as a committed `.npmrc`.\n *\n * `true` is deliberately ignored, and the CLI says so rather than obeying\n * quietly. This file is committed, so a `true` here would be one developer\n * answering a privacy question for every colleague who later clones the\n * repo — consent by proxy, which is the exact thing opt-in exists to\n * prevent. Individuals opt in with `rebase telemetry enable`.\n */\n telemetry?: boolean;\n}\n\n/**\n * The per-checkout project link.\n *\n * Deliberately separate from `rebase.json`: the manifest is committed and shared,\n * while the link is per-developer. Keeping them in one file would mean either\n * committing someone's project id or gitignoring the topology.\n */\nexport interface RebaseProjectLink {\n /**\n * A Rebase Cloud project id, or the base URL of any running Rebase backend\n * (`https://api.example.com`). Both are first-class: every command that\n * accepts a project reference accepts either, so a self-hosted project has\n * the same tooling as a cloud one.\n */\n project: string;\n /** Organization slug. Cloud projects only. */\n org?: string;\n /** Explicit API base URL, when it differs from the project's default. */\n apiUrl?: string;\n}\n\n/**\n * Whether a project can run on the managed runtime, and if not, precisely why.\n *\n * The reasons are returned rather than summarised so tooling can print something\n * a developer can act on. \"Not eligible\" is never a dead end — it selects the\n * custom-runtime path, which still deploys.\n */\nexport interface ManagedCompatibility {\n eligible: boolean;\n reasons: string[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Bundle\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Version of the bundle *format* itself.\n *\n * Bumped only when the on-disk layout changes in a way an older runtime could\n * not read. A runtime accepts any bundle whose `bundleFormat` is less than or\n * equal to its own — old bundles keep booting on new runtimes, which is the\n * whole point of separating the artifact from the engine.\n *\n * - **1** — `mode: \"cms\" | \"baas\" | \"static\"`, `entry.static` a single directory\n * string, `entry.admin` for a bundled admin panel.\n * - **2** — `kind: \"backend\" | \"static\"`, `entry.static` a list of\n * {@link RebaseBundleStatic}, `entry.admin` removed. A format-1 runtime reading\n * one of these would find no `mode` and an array where it expects a string, so\n * the bump is what turns that into a refusal to boot instead of a bundle that\n * starts and serves nothing.\n */\nexport const BUNDLE_FORMAT_VERSION = 2;\n\n/**\n * The runtime contract major.\n *\n * Distinct from the `@rebasepro/server` package version: the package may release\n * any number of minors and patches while this stays put. It changes only when\n * the bundle/runtime contract breaks compatibility, and a project's\n * `manifest.runtime` range is matched against *this*.\n *\n * ## v2 — resources are declared, not configured\n *\n * `RebaseBackendConfig.dataSources` and `.storageSources` are gone. A project\n * declares its databases and buckets with `database()` / `bucket()` in its\n * config, and the runtime reads those declarations.\n *\n * This had to be a major, and the reason is the managed tier: it moves projects\n * onto new images WITHOUT rebuilding them. A bundle built against v1 exports\n * those keys, and a v2 runtime refuses them at boot — so without this bump, one\n * image rollout would crash-loop every tenant that had ever declared a second\n * database or bucket, in a wave, with the cause in a container log nobody is\n * watching.\n *\n * With the bump, a v1 bundle on a v2 runtime is refused by\n * `assertBundleCompatibility` with the remedy in the message, and the platform\n * keeps it on a v1 image until it is rebuilt. That is the whole purpose of this\n * number.\n *\n * **Release order matters and is not optional.** The control plane is the side\n * that rejects, so it ships FIRST: raise `SUPPORTED_RUNTIME_CONTRACT` in the\n * saas repo (it rejects only `contract >` its own, so it then accepts both),\n * deploy that, and only then release a runtime implementing v2. Shipping the\n * runtime first turns every deploy into a rejected intake blaming the tenant's\n * bundle.\n */\nexport const RUNTIME_CONTRACT_VERSION = 1;\n\n/** Where the runtime finds each part of the bundle. Paths are bundle-relative. */\nexport interface RebaseBundleEntrypoints {\n /** Compiled config package directory (collections live under it). */\n config?: string;\n /** Compiled collections directory, when it differs from `<config>/collections`. */\n collections?: string;\n /** Compiled functions directory. */\n functions?: string;\n /** Compiled crons directory. */\n crons?: string;\n /** Compiled Drizzle schema module. */\n schema?: string;\n /** Module exporting the auth users collection (default export). */\n usersCollection?: string;\n /**\n * Built static apps to serve from this process, in declaration order.\n *\n * A list rather than a single directory because one process serves several\n * apps at different paths — a site at `/` and the admin at `/admin`. The\n * runtime mounts them longest-path-first so the `/`-rooted app's catch-all\n * does not claim its siblings' URLs.\n */\n static?: RebaseBundleStatic[];\n}\n\n/** One built static app inside a bundle. */\nexport interface RebaseBundleStatic {\n /** Public base path, e.g. `/` or `/admin`. */\n path: string;\n /** Bundle-relative directory holding the built assets. */\n dir: string;\n /** Serve `index.html` for unmatched paths under `path`. */\n spa: boolean;\n}\n\n/**\n * A native module found in the dependency closure.\n *\n * Recorded rather than merely counted so a rejection can name the offending\n * package instead of saying \"something here is native\".\n */\nexport interface NativeDependency {\n name: string;\n /** Why it was flagged — a `.node` binary, a gyp build, or an install script. */\n reason: string;\n}\n\n/**\n * `manifest.json` — generated, and the document the runtime and control plane\n * both validate against.\n */\n/**\n * One custom function, as recorded in a built bundle.\n *\n * @see RebaseBundleManifest.functions\n */\nexport interface RebaseBundleFunction {\n /**\n * The filename without its extension — which is also the URL segment it\n * mounts at (`/api/functions/<name>`), the API-key permission that grants\n * it, and the name `REBASE_FUNCTIONS_ONLY` selects by. One identity, used\n * everywhere.\n */\n name: string;\n /** Path inside the bundle, so a host can point at the file. */\n file: string;\n /**\n * `false` when the function's own source imports a Node built-in or a\n * package that needs one.\n *\n * Descriptive, never a gate: nothing refuses to build or deploy on this. It\n * says where this function *could* run, not where it should.\n */\n portable: boolean;\n /**\n * Why it is not portable — one short phrase per reason, deduplicated.\n * Absent when it is.\n */\n requires?: string[];\n}\n\nexport interface RebaseBundleManifest {\n /** @see BUNDLE_FORMAT_VERSION */\n bundleFormat: number;\n runtime: {\n /** The `runtime` range copied from `rebase.json`. */\n range: string;\n /** Exact `@rebasepro/server` version this bundle was built against. */\n builtAgainst: string;\n /** Runtime contract major this bundle requires. */\n contract: number;\n };\n /**\n * Hash of the compiled collection definitions.\n *\n * This is the contract stamp. A generated SDK records the value it was built\n * from, a client sends it back, and a mismatch is what lets the platform say\n * \"this app was built against an older schema\" instead of failing mysteriously\n * at the first request. It covers collections only — a hook edit does not\n * change a client's contract, so it must not invalidate every SDK.\n */\n schemaVersion: string;\n /** Which app in `rebase.json` this bundle was built from. */\n app: string;\n /**\n * What the runtime does with this bundle.\n *\n * - `backend` — boot the full server: database, auth and the data API, plus\n * any static apps in `entry.static`.\n * - `static` — no backend at all: serve `entry.static` and nothing else. No\n * database, no auth, no data sources. This is how a static app runs on the\n * same image as the backend.\n *\n * Replaces an earlier `mode: \"cms\" | \"baas\" | \"static\"`. The cms/baas\n * distinction was never a third kind of thing — it is simply whether\n * `entry.config` is present, so it is derived rather than declared.\n */\n kind: \"backend\" | \"static\";\n entry: RebaseBundleEntrypoints;\n /** Collection slugs contained in the bundle, for quick inspection. */\n collections?: string[];\n /**\n * Every custom function in the bundle, named and classified.\n *\n * Two things are recorded per function, and both are answers a host would\n * otherwise have to get by importing user code:\n *\n * - **What it is called.** That name is the function's identity everywhere —\n * the URL segment it mounts at, the `functions/<name>` API-key\n * permission, the value `REBASE_FUNCTIONS_ONLY` selects by. A host that\n * wants to give one slow function its own replica count currently has to\n * boot the bundle to discover what is in it.\n * - **Whether it needs Node.** Purely descriptive: a function that opens a\n * file or runs raw SQL is a fine function, and every deployment today is\n * a Node process. It is recorded because the question \"which of these\n * could run somewhere else\" has to be answerable from the artifact, and\n * because answering it per-file after the fact — across a codebase\n * already written — is the expensive version of the same question.\n *\n * Absent on a bundle built before this field existed, which is why every\n * consumer must treat it as optional rather than as an empty list.\n */\n functions?: RebaseBundleFunction[];\n hooks: {\n /**\n * Whether the dependency closure contains native code.\n *\n * The managed runtime refuses these: a prebuilt binary cannot be run on\n * an image the platform did not build it for, and the honest failure is\n * at deploy time rather than at 3am in a crash loop.\n */\n native: boolean;\n nativeModules?: NativeDependency[];\n };\n /**\n * What the bundle's config says about storage access control.\n *\n * Storage is not under RLS and its keys share one flat namespace, so a\n * deployment with file storage enabled and no access model serves every\n * user's files to every signed-in user. The runtime refuses to boot in that\n * state — which, on a hosted platform that enables storage from the *console*\n * rather than from the bundle, surfaces as a crash loop the developer cannot\n * read.\n *\n * Recording it here lets a host reject the deploy with the reason instead.\n * Absent on bundles built before this field existed.\n */\n storage?: {\n /** Whether the config package exports a `storageAuthorize` hook. */\n authorize: boolean;\n /**\n * Buckets, on bundles built before {@link RebaseBundleManifest.resources}.\n *\n * No longer written. A host reads `resources`, which carries every kind\n * in one list; this stays declared so a control plane can keep reading\n * the bundles a project shipped before it was rebuilt.\n */\n sources?: StorageSourceDefinition[];\n };\n /**\n * Everything the project declares it needs — databases, buckets, topics,\n * and whatever kind is registered next.\n *\n * Recorded so a host can tell, from the artifact alone and before starting\n * anything, what a deploy will need provisioned. That question used to be\n * answerable for buckets and for nothing else, because buckets were the\n * only kind written into an artifact — which is how a project's databases\n * became invisible to the platform that runs them.\n *\n * Absent on bundles built before this field existed.\n */\n resources?: ResourceGraph;\n deps: {\n /** Runtime dependencies of user code, as declared. */\n declared: Record<string, string>;\n /**\n * The dependency tree ships *inside* the bundle, already installed.\n *\n * Absent or false means the tree is declared but not present, and\n * whoever boots the bundle has to install it. On the managed runtime that\n * install runs in an init container on **every** pod start — the bundle\n * lives on a volume that is wiped each time — and it is the single\n * largest cost in a managed pod's life: 35–55 seconds of a 40–60 second\n * cold start. Since a pod restarts on every eviction, node failure, OOM\n * and runtime rollout, that number is not a startup detail. It is what an\n * outage costs.\n *\n * Vendoring moves the install to build time, where it happens once. It is\n * skipped when the closure contains native code, because a prebuilt\n * binary is only valid for the platform it was built for — see\n * {@link vendorTarget} for what \"the platform\" means here.\n */\n vendored?: boolean;\n /**\n * What {@link vendored} was resolved for, recorded so a mismatch can be\n * refused rather than discovered at import time.\n *\n * Cross-platform vendoring is safe for pure JavaScript and unsafe for\n * anything compiled, and the boundary between them is not always visible\n * in a dependency list: `esbuild` is pure-JS with a *platform-specific\n * optional dependency* holding the actual binary, so an install run on a\n * developer's Mac silently produces a tree that cannot run on the Linux\n * image. The install therefore resolves optional dependencies for the\n * target explicitly rather than for the machine it runs on, and records\n * the answer here.\n */\n vendorTarget?: {\n /** npm `--os`, e.g. `linux`. */\n os: string;\n /** npm `--cpu`, e.g. `x64`. */\n cpu: string;\n /** Node major the tree was resolved for. */\n node: string;\n };\n };\n build: {\n /** `@rebasepro/cli` version that produced this bundle. */\n cli: string;\n /** Node major the bundle was compiled on. */\n node: string;\n /** ISO-8601. */\n createdAt: string;\n };\n}\n\n/** The contract a running backend serves at `GET /api/meta/contract`. */\nexport interface RebaseProjectContract {\n /** Matches {@link RebaseBundleManifest.schemaVersion}. */\n schemaVersion: string;\n runtime: {\n /** `@rebasepro/server` version currently running. */\n version: string;\n contract: number;\n };\n /** Full collection definitions, serialized — the input to SDK generation. */\n collections: unknown[];\n /** Collection slugs, for cheap inspection without parsing the definitions. */\n collectionSlugs: string[];\n generatedAt: string;\n}\n\n/** Header carrying the schema version an SDK was generated from. */\nexport const SCHEMA_VERSION_HEADER = \"x-rebase-schema\";\n","import type { CollectionConfig } from \"./collections\";\n\n/**\n * Serializing collections so they survive a network hop.\n *\n * A collection definition is not plain data. Relations point at their target\n * with a *function* (`target: () => usersCollection`) so two collections can\n * reference each other without an import cycle, and collections also carry\n * callbacks, custom views and component references. `JSON.stringify` silently\n * drops every one of those, which matters because the SDK generator *calls*\n * `relation.target()` to decide whether a foreign key is a string or a number.\n * Serialize naively and remote SDK generation produces subtly wrong types\n * instead of failing — the worst possible outcome.\n *\n * So relation targets are resolved to a slug reference on the way out and\n * rebuilt into functions on the way in. Everything else that cannot cross a wire\n * is dropped deliberately: an SDK is generated from the *shape* of the data, and\n * server-side behaviour is neither useful to a client nor safe to publish.\n */\n\n/** Marker replacing a relation's `target` function in serialized form. */\nexport interface SerializedCollectionRef {\n __collectionRef: string;\n}\n\nexport function isSerializedCollectionRef(value: unknown): value is SerializedCollectionRef {\n return typeof value === \"object\"\n && value !== null\n && typeof (value as SerializedCollectionRef).__collectionRef === \"string\";\n}\n\n/** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */\nconst MAX_DEPTH = 64;\n\n/**\n * Resolve whatever a `target` thunk returns down to a collection.\n *\n * A target may be the collection, a module namespace (when the authoring file\n * used `import * as`), or a default-export wrapper. All three appear in real\n * projects, and the SDK generator already unwraps them the same way.\n */\nfunction unwrapTarget(value: unknown): CollectionConfig | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const candidate = value as { default?: unknown; __esModule?: boolean; properties?: unknown };\n if (candidate.default || candidate.__esModule) {\n const inner = candidate.default;\n if (inner && typeof inner === \"object\") return inner as CollectionConfig;\n }\n if (candidate.properties) return value as CollectionConfig;\n return undefined;\n}\n\n/** The identity a serialized reference uses. Slug first — it is the routing key. */\nfunction refFor(collection: CollectionConfig | undefined): string | undefined {\n if (!collection) return undefined;\n const withPath = collection as CollectionConfig & { path?: string };\n return collection.slug || withPath.path || collection.name;\n}\n\n/**\n * Deep-copy a value into something JSON can carry.\n *\n * `target` keys are special-cased into refs. Other functions vanish, cycles are\n * cut, and everything else is copied structurally.\n */\n/** Shared walk state: the memo, plus a count of depth-cap hits. */\ninterface WalkState {\n memo: WeakMap<object, unknown>;\n /**\n * How many times the walk has truncated a subtree — by hitting the depth\n * cap, or by cutting a cycle.\n *\n * Either kind of truncation makes a result valid only at the *position* it\n * was produced at, so caching it and serving it elsewhere silently drops\n * content that would have been included. Comparing this counter before and\n * after a node's children tells us whether its result is position-\n * independent and therefore safe to memoize.\n *\n * The cycle case is the subtle one: with `a.b = b` and `b.a = a`, serializing\n * `{ first: b, second: a }` visits `a` beneath `b` — where the cycle back to\n * `b` is cut — and would then reuse that truncated `a` for `second`, where\n * nothing needed cutting.\n */\n truncations: number;\n}\n\nfunction toSerializable(\n value: unknown,\n seen: WeakSet<object>,\n depth: number,\n state: WalkState,\n key?: string\n): unknown {\n if (depth > MAX_DEPTH) {\n state.truncations++;\n return undefined;\n }\n\n if (typeof value === \"function\") {\n // Only a relation target carries information a client needs. Calling it\n // is safe here — this runs on the server, where the target module is\n // already loaded — and a throwing target simply yields no reference,\n // which degrades the generated FK type rather than failing the request.\n if (key === \"target\") {\n try {\n const resolved = unwrapTarget((value as () => unknown)());\n const ref = refFor(resolved);\n return ref ? { __collectionRef: ref } : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) return value.toISOString();\n if (value instanceof RegExp) return value.source;\n\n if (seen.has(value as object)) {\n state.truncations++;\n return undefined;\n }\n\n // A shared (non-cyclic) subgraph is reachable by many paths, and `seen` is a\n // *path* set — released in the `finally` below so a node referenced twice in\n // different branches is emitted twice rather than dropped as a false cycle.\n // Without memoization that makes the walk exponential in depth: a diamond\n // graph 20 levels deep took ~400ms, and each further level doubled it. The\n // result is a plain data tree, so handing back the same converted object for\n // a repeat visit is indistinguishable after JSON.stringify.\n const cached = state.memo.get(value as object);\n if (cached !== undefined) return cached;\n\n seen.add(value as object);\n const truncationsBefore = state.truncations;\n const memoize = (result: unknown): unknown => {\n // Only cache a result that nothing was cut from.\n if (result !== undefined && state.truncations === truncationsBefore) {\n state.memo.set(value as object, result);\n }\n return result;\n };\n\n try {\n if (Array.isArray(value)) {\n const items = value\n .map(item => toSerializable(item, seen, depth + 1, state))\n .filter(item => item !== undefined);\n // A container that had content, none of which can be represented, is\n // itself unrepresentable — see the note below.\n return memoize(value.length > 0 && items.length === 0 ? undefined : items);\n }\n\n // A React element or component reference has no meaning to a client and\n // will not survive JSON anyway.\n if (\"$$typeof\" in (value as Record<string, unknown>)) return undefined;\n\n const entries = Object.entries(value as Record<string, unknown>);\n const out: Record<string, unknown> = {};\n for (const [k, v] of entries) {\n const converted = toSerializable(v, seen, depth + 1, state, k);\n if (converted !== undefined) out[k] = converted;\n }\n\n // Drop a container whose entire content was dropped.\n //\n // `callbacks: { beforeSave() {…} }` would otherwise serialize to\n // `callbacks: {}` — an empty husk that carries no information but is not\n // *nothing*, so it lands in the payload and, worse, in the schema hash.\n // Editing a hook would then change every client's schema version and\n // report perfectly current SDKs as stale.\n //\n // A container that started empty stays empty: `properties: {}` is a\n // deliberate statement, not a casualty.\n if (entries.length > 0 && Object.keys(out).length === 0) return undefined;\n\n return memoize(out);\n } finally {\n // Released so a collection referenced twice in different branches is\n // emitted twice rather than being dropped as a false cycle.\n seen.delete(value as object);\n }\n}\n\n/**\n * Serialize collections for transport over the contract endpoint.\n *\n * Sorted by slug so the output — and therefore the schema hash computed from it\n * — does not depend on filesystem ordering.\n */\nexport function serializeCollections(collections: CollectionConfig[]): unknown[] {\n return [...collections]\n .sort((a, b) => String(a.slug ?? \"\").localeCompare(String(b.slug ?? \"\")))\n .map(collection => toSerializable(withoutAdminBlock(collection), new WeakSet(), 0, {\n memo: new WeakMap(),\n truncations: 0\n }))\n .filter((c): c is Record<string, unknown> => c !== undefined);\n}\n\n/**\n * Drop the admin block before the walk.\n *\n * Nothing downstream of serialization is an admin panel. The contract endpoint\n * feeds remote SDK generation, and `rebase build` writes the result into a bundle\n * manifest that only the backend runtime reads. The block would survive the walk\n * as a husk anyway — its React elements and component functions are dropped\n * individually — and that husk has two costs worth avoiding: it puts every custom\n * component's *file path* on an endpoint whose job is to describe data shapes, and\n * it grows a payload that is fetched and cached per project.\n *\n * Removing it here rather than at each call site means one chokepoint, so a future\n * consumer of `serializeCollections` cannot forget.\n *\n * Child collections carry their own block, so this recurses — stripping only the\n * top level was the mistake `stripNonClientFields` in the contract routes already\n * had to fix once for security rules.\n */\nfunction withoutAdminBlock(collection: CollectionConfig): CollectionConfig {\n const { admin: _admin, ...rest } = collection as CollectionConfig & Record<string, unknown>;\n const nested = rest as Record<string, unknown>;\n if (Array.isArray(nested.subcollections)) {\n nested.subcollections = nested.subcollections.map(\n (child) => withoutAdminBlock(child as CollectionConfig)\n );\n }\n return rest as CollectionConfig;\n}\n\n/**\n * Rebuild collections received from a contract endpoint.\n *\n * Relation refs become real thunks resolving through the returned set, so\n * downstream consumers — the SDK generator above all — see exactly the shape\n * they would have seen had the collections been imported from source.\n *\n * A ref naming a collection that is not in the payload resolves to `undefined`\n * rather than throwing: the generator already tolerates an unresolvable target\n * by falling back to a permissive key type, and a partial contract should still\n * produce a usable SDK.\n */\nexport function deserializeCollections(payload: unknown[]): CollectionConfig[] {\n const collections = payload\n .filter((c): c is Record<string, unknown> => typeof c === \"object\" && c !== null)\n .map(c => ({ ...c })) as unknown as CollectionConfig[];\n\n const bySlug = new Map<string, CollectionConfig>();\n for (const collection of collections) {\n const ref = refFor(collection);\n if (ref) bySlug.set(ref, collection);\n }\n\n const rehydrate = (value: unknown, depth: number): void => {\n if (depth > MAX_DEPTH || !value || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n for (const item of value) rehydrate(item, depth + 1);\n return;\n }\n\n const record = value as Record<string, unknown>;\n for (const [key, child] of Object.entries(record)) {\n if (key === \"target\" && isSerializedCollectionRef(child)) {\n const slug = child.__collectionRef;\n record.target = () => bySlug.get(slug);\n continue;\n }\n rehydrate(child, depth + 1);\n }\n };\n\n for (const collection of collections) rehydrate(collection, 0);\n return collections;\n}\n","import type { CollectionConfig } from \"./collections\";\nimport { serializeCollections } from \"./collection_contract\";\n\n/**\n * The schema version stamp.\n *\n * One function, used in three places that must agree or the whole drift-detection\n * story is noise: `rebase build` writes it into a bundle manifest, the runtime\n * serves it from the contract endpoint, and a generated SDK records the value it\n * was built from. If any two of those computed it differently, every client would\n * look permanently out of date.\n *\n * It covers **collections only** — the client's contract is the shape of the\n * data, so editing a hook or a server function must not invalidate every SDK in\n * every repository. That is a deliberate narrowing, not an oversight.\n */\n\n/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(\",\")}}`;\n}\n\n/**\n * Reduce a collection to the parts a generated client is actually built from.\n *\n * The version answers one question — \"is this SDK stale?\" — so it must change\n * exactly when the generated types could change, and never otherwise. Hashing a\n * whole collection fails both halves of that:\n *\n * - Security rules, callbacks, icons, groups and UI settings do not appear in a\n * generated client, so including them reports perfectly current SDKs as stale.\n * - Worse, they are not stable *inputs*. The runtime applies default security\n * rules when it loads collections, so the same source hashed before and after\n * loading produced two different answers — a build-time stamp that could never\n * match the server that served it.\n *\n * Codegen reads the slug (for the `Database` key and type names), the properties,\n * and the relations. That is the projection.\n */\nfunction projectForCodegen(collection: CollectionConfig): Record<string, unknown> {\n const source = collection as CollectionConfig & {\n relations?: unknown;\n subcollections?: CollectionConfig[];\n path?: string;\n engine?: unknown;\n dataSource?: unknown;\n };\n\n return {\n slug: collection.slug ?? source.path,\n properties: collection.properties,\n relations: source.relations,\n // The engine decides whether relations are resolved at all: codegen asks\n // `getDataSourceCapabilities(collection.engine).supportsRelations`, and an\n // engine that answers no drops every foreign-key column from the\n // generated Row/Insert/Update types. Moving a collection to such an\n // engine is a real change to the generated types, so it has to move the\n // version. `dataSource` is what resolves to `engine`, so it counts too.\n engine: source.engine,\n dataSource: source.dataSource,\n subcollections: source.subcollections?.map(projectForCodegen)\n };\n}\n\n/**\n * Compute the canonical string a schema version hashes.\n *\n * Exposed separately so the hashing itself can differ by environment: Node has\n * `crypto`, and callers without it can still compare canonical forms directly.\n */\nexport function canonicalSchemaPayload(collections: CollectionConfig[]): string {\n const projected = serializeCollections(collections)\n .map(collection => projectForCodegen(collection as CollectionConfig));\n return canonicalize(projected);\n}\n\n/**\n * A short, non-cryptographic digest of the canonical payload.\n *\n * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a\n * security boundary: nothing trusts a schema version to prove anything, it only\n * answers \"is this the same schema as before\". A hand-rolled hash keeps this\n * module free of `node:crypto`, so the identical function runs in the browser,\n * in the CLI, and in the runtime — which is the property that actually matters.\n */\nexport function computeSchemaVersion(collections: CollectionConfig[]): string {\n const payload = canonicalSchemaPayload(collections);\n\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193;\n\n for (let i = 0; i < payload.length; i++) {\n const code = payload.charCodeAt(i);\n h1 ^= code;\n // Multiply by the FNV prime using shifts to stay in 32-bit integer math.\n h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;\n h2 ^= code + i;\n h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;\n }\n\n const hex = (n: number): string => n.toString(16).padStart(8, \"0\");\n return `v1:${hex(h1)}${hex(h2)}`;\n}\n","import { Hono } from \"hono\";\nimport {\n RUNTIME_CONTRACT_VERSION,\n SCHEMA_VERSION_HEADER,\n computeSchemaVersion,\n serializeCollections,\n type CollectionConfig,\n type RebaseProjectContract\n} from \"@rebasepro/types\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The project contract endpoint.\n *\n * This is what makes a repository able to build against a project it does not\n * contain. Without it, a typed client can only be generated from local\n * collection *source*, which means every frontend must live in the same\n * repository as the backend. Serving the contract turns that around: an app\n * asks the project what its shape is, so a web app, a second web app and a\n * mobile app can each live wherever they like and none of them needs to know\n * about the others.\n *\n * Admin-gated. Collection definitions describe every table, column and relation\n * in the project, including ones no security rule would ever expose — that is a\n * map of the database, not public API documentation.\n */\n\nexport interface ContractRoutesConfig {\n collectionRegistry: { getRawCollections(): CollectionConfig[] };\n /**\n * The schema version recorded at build time.\n *\n * Preferred over recomputing, so that what a client is told matches exactly\n * what the bundle claims. It is recomputed only when a bundle did not record\n * one — a `baas`-mode project derives its collections from the live database\n * at boot, so there was nothing to hash when it was built.\n */\n schemaVersion?: string;\n /** Runtime package version, surfaced so a client can report what it built against. */\n runtimeVersion?: string;\n}\n\n/**\n * Strip everything a client does not need from a serialized collection.\n *\n * The generator reads the slug, the properties and the relations. It never reads\n * a security rule — but `securityRules` carries the raw SQL of every RLS\n * predicate guarding the project, which is a description of the authorization\n * model rather than of the data shape. Publishing it to anyone who can generate\n * an SDK gives away more than the endpoint is for, so it is removed here rather\n * than trusted not to matter.\n */\nfunction stripNonClientFields(collection: unknown): unknown {\n if (!collection || typeof collection !== \"object\") return collection;\n const {\n securityRules: _securityRules,\n callbacks: _callbacks,\n ...rest\n } = collection as Record<string, unknown>;\n\n // Subcollections are collections, and carry their own rules. Stripping only\n // the top level published every nested policy — the leak this exists to\n // prevent, just one level down.\n if (Array.isArray(rest.subcollections)) {\n rest.subcollections = rest.subcollections.map(stripNonClientFields);\n }\n\n return rest;\n}\n\nexport function createContractRoutes(config: ContractRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n // Computing a version walks and canonicalizes every collection, and\n // `/schema-version` is deliberately unauthenticated and meant to be polled.\n // Recomputing per request would make a CI convenience into a CPU\n // amplification anyone could aim at the server. Collections do not change\n // after boot, so once is enough.\n let cachedVersion: string | undefined;\n const schemaVersionOf = (collections: CollectionConfig[]): string => {\n if (config.schemaVersion) return config.schemaVersion;\n if (cachedVersion === undefined) cachedVersion = computeSchemaVersion(collections);\n return cachedVersion;\n };\n\n router.get(\"/contract\", (c) => {\n const collections = config.collectionRegistry.getRawCollections();\n const serialized = serializeCollections(collections).map(stripNonClientFields);\n\n // In `baas` mode the collections are whatever introspection found at\n // boot, so the version has to be computed from them rather than taken\n // from a build that never saw them.\n const schemaVersion = schemaVersionOf(collections);\n\n const contract: RebaseProjectContract = {\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n },\n collections: serialized,\n collectionSlugs: collections\n .map(collection => collection.slug)\n .filter((slug): slug is string => Boolean(slug))\n .sort(),\n generatedAt: new Date().toISOString()\n };\n\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json(contract);\n });\n\n /**\n * Cheap drift check.\n *\n * Deliberately unauthenticated and deliberately tiny: two version stamps\n * and nothing else. A CI job that only wants to know whether its generated\n * SDK is stale should not need admin credentials, and a version stamp\n * reveals nothing about the schema it stands for.\n *\n * `runtime` is here as well as on `/contract` because the two answer\n * different questions and only one of them was reachable. Which runtime a\n * project is on decides whether a client's wire format is understood at\n * all, and it was published solely on the admin-gated route — so a CLI or\n * an SDK, the two callers that actually need to know, could not ask. That\n * is the same shape as the header this route echoes: a documented signal\n * with no reachable sender. `contract` is the number that matters for\n * compatibility; `version` names the release a human should quote.\n */\n router.get(\"/schema-version\", (c) => {\n const schemaVersion = schemaVersionOf(config.collectionRegistry.getRawCollections());\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json({\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n }\n });\n });\n\n logger.debug(\"Contract routes mounted\");\n return router;\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 declareCron } from "./src-
|
|
6
|
+
import { t as declareCron } from "./src-Br6ARbs6.js";
|
|
7
7
|
import { r as logger } from "./logger-DO2PZc4i.js";
|
|
8
8
|
import { t as nativeDynamicImport } from "./dynamic-import-X40pTZUQ.js";
|
|
9
9
|
import * as fs$1 from "fs";
|
|
@@ -115,4 +115,4 @@ function cronResourceOptions(definition) {
|
|
|
115
115
|
//#endregion
|
|
116
116
|
export { loadCronJobsFromDirectory as n, loadCronJobsWithDiagnostics as r, cron_loader_exports as t };
|
|
117
117
|
|
|
118
|
-
//# sourceMappingURL=cron-loader-
|
|
118
|
+
//# sourceMappingURL=cron-loader-CQjvjpEw.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cron-loader-DnmIePn_.js","names":[],"sources":["../src/cron/cron-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { declareCron, type CronJobDefinition, type CronResourceOptions } from \"@rebasepro/types\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\n\nexport interface LoadedCronJob {\n /** Job ID derived from filename (e.g. \"cleanup-sessions\"). */\n id: string;\n /** The full definition. */\n definition: CronJobDefinition;\n}\n\n/** {@link loadCronJobsWithDiagnostics}' answer: what loaded, and what did not. */\nexport interface LoadedCronJobs {\n jobs: LoadedCronJob[];\n /** One entry per file that will NOT be scheduled, with the reason. */\n problems: string[];\n}\n\n/**\n * Auto-discover cron job files from a directory.\n *\n * Each file should default-export a `CronJobDefinition`.\n * The filename (without extension) becomes the job ID:\n * `crons/cleanup-sessions.ts` → id = \"cleanup-sessions\"\n *\n * Follows the same discovery pattern as `loadFunctionsFromDirectory` — see\n * {@link loadCronJobsWithDiagnostics} for what happens to the files it cannot\n * load.\n */\nexport async function loadCronJobsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJob[]> {\n return (await loadCronJobsWithDiagnostics(directory, importModule)).jobs;\n}\n\n/**\n * {@link loadCronJobsFromDirectory}, plus the list of files that were skipped.\n *\n * A job that does not load does not run, and until now the only trace was one\n * `warn` per file in the boot log — next to nothing at 3am when the report that\n * should have been mailed was not. A cron file is written once and then trusted\n * for months, so \"it silently never ran\" is the failure mode this surface has,\n * and a log line is not a surface.\n *\n * Never throws, for the same reason `loadFunctionsWithDiagnostics` does not: one\n * malformed file must not take the server down, and the other jobs in the\n * directory are still worth scheduling. The difference is that the skips are now\n * counted, summarised in one place, and answered by `GET /api/cron` — so the\n * question \"why did nothing run?\" has an answer that does not require log\n * access.\n */\nexport async function loadCronJobsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJobs> {\n const jobs: LoadedCronJob[] = [];\n // Aggregated so a broken job surfaces as one loud summary rather than a\n // warning buried per-file among the boot noise.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { jobs, problems };\n }\n\n const files = fs.readdirSync(directory);\n for (const file of files) {\n if (\n (file.endsWith(\".ts\") || file.endsWith(\".js\")) &&\n // Dotfiles: notably macOS bsdtar AppleDouble sidecars (`._foo.ts`),\n // binary blobs that cannot be imported.\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n !file.endsWith(\".d.ts\") &&\n file !== \"index.ts\" &&\n file !== \"index.js\"\n ) {\n const filePath = path.join(directory, file);\n try {\n const fileUrl = pathToFileURL(filePath).href;\n\n const mod = await importModule(fileUrl);\n\n const exported: unknown = mod.default;\n\n if (!exported || typeof exported !== \"object\") {\n problems.push(`${file} (no default export)`);\n logger.warn(`[cron] ${file}: no valid default export. Skipping.`);\n continue;\n }\n\n const def = exported as Record<string, unknown>;\n if (typeof def.schedule !== \"string\" || typeof def.handler !== \"function\") {\n problems.push(`${file} (default export has no 'schedule' or no 'handler')`);\n logger.warn(`[cron] ${file}: default export missing required 'schedule' or 'handler'. Skipping.`);\n continue;\n }\n\n const id = path.basename(file, path.extname(file));\n // Spread first, then normalise. Rebuilding this object field by\n // field dropped `catchUpWindowSeconds` — and since this loader\n // is the only production caller of `registerJobs`, catch-up\n // never ran for any job authored the documented way: 64 lines\n // of docblock, a docs section, a unit suite and a Postgres e2e\n // all describing a feature that was switched off in the one\n // path that matters. Every existing test built `LoadedCronJob`\n // literals directly, so none of them went through here.\n //\n // The shape is the bug, not the missing line: a field added to\n // `CronJobDefinition` tomorrow would be dropped the same way.\n const definition: CronJobDefinition = {\n ...(def as Partial<CronJobDefinition>),\n schedule: def.schedule as string,\n name: (def.name as string) || id,\n enabled: def.enabled !== false,\n timeoutSeconds: (def.timeoutSeconds as number) || 300,\n handler: def.handler as CronJobDefinition[\"handler\"]\n };\n\n // The declaration, keyed by the same id the scheduler, the\n // routes and the Studio use — the filename. `defineCron` cannot\n // do this: it never sees the filename, and `name` is a label.\n // The derive step runs this same loader, so the graph a host\n // reads and the schedule this process runs come from one place.\n // From the raw export, not the normalised definition: the\n // defaults filled in above (`enabled: true`, `timeoutSeconds:\n // 300`) are this process's, and a graph that records them for\n // every cron would say something the author never wrote.\n declareCron(id, cronResourceOptions(def as unknown as CronJobDefinition));\n\n jobs.push({ id,\ndefinition });\n logger.info(\n `⏰ Loaded cron job: ${id} (${definition.schedule}` +\n `${definition.timezone ? ` ${definition.timezone}` : \" in the process's local time\"})`\n );\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n problems.push(`${file} (threw: ${message})`);\n // At `debug`: the aggregate warning below lists every problem\n // by file, so this line was the same text a second time — and\n // when the throw is a Zod error, that text is ten lines of\n // serialised issues. The reader met it twice before reaching\n // the sentence that says what to do.\n logger.debug(`[cron] Failed to load ${file}: ${message}`);\n }\n }\n }\n\n if (problems.length > 0) {\n logger.warn(\n `[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n \" Fix these or author them with `defineCron(...)` for a typed, compile-checked contract.\"\n );\n }\n\n return { jobs, problems };\n}\n\n/**\n * The declaration a definition makes: every field a host can read without the\n * handler. Spelled once here so the graph and the scheduler cannot disagree\n * about what a cron is.\n */\nexport function cronResourceOptions(definition: CronJobDefinition): CronResourceOptions {\n return {\n schedule: definition.schedule,\n ...(definition.timezone !== undefined ? { timezone: definition.timezone } : {}),\n ...(definition.description !== undefined ? { description: definition.description } : {}),\n ...(definition.enabled !== undefined ? { enabled: definition.enabled } : {}),\n ...(definition.timeoutSeconds !== undefined ? { timeoutSeconds: definition.timeoutSeconds } : {}),\n ...(definition.catchUpWindowSeconds !== undefined ? { catchUpWindowSeconds: definition.catchUpWindowSeconds } : {}),\n ...(definition.name ? { label: definition.name } : {})\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,0BAClB,WACA,eAA+B,qBACP;CACxB,QAAQ,MAAM,4BAA4B,WAAW,YAAY,EAAA,CAAG;AACxE;;;;;;;;;;;;;;;;;AAkBA,eAAsB,4BAClB,WACA,eAA+B,qBACR;CACvB,MAAM,OAAwB,CAAC;CAG/B,MAAM,WAAqB,CAAC;CAE5B,IAAI,CAAC,KAAG,WAAW,SAAS,GACxB,OAAO;EAAE;EAAM;CAAS;CAG5B,MAAM,QAAQ,KAAG,YAAY,SAAS;CACtC,KAAK,MAAM,QAAQ,OACf,KACK,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,KAAK,MAG5C,CAAC,KAAK,WAAW,GAAG,KACpB,CAAC,KAAK,SAAS,QAAQ,KACvB,CAAC,KAAK,SAAS,OAAO,KACtB,SAAS,cACT,SAAS,YACX;EACE,MAAM,WAAW,OAAK,KAAK,WAAW,IAAI;EAC1C,IAAI;GACA,MAAM,UAAU,cAAc,QAAQ,CAAC,CAAC;GAIxC,MAAM,YAAoB,MAFR,aAAa,OAAO,EAAA,CAER;GAE9B,IAAI,CAAC,YAAY,OAAO,aAAa,UAAU;IAC3C,SAAS,KAAK,GAAG,KAAK,qBAAqB;IAC3C,OAAO,KAAK,UAAU,KAAK,qCAAqC;IAChE;GACJ;GAEA,MAAM,MAAM;GACZ,IAAI,OAAO,IAAI,aAAa,YAAY,OAAO,IAAI,YAAY,YAAY;IACvE,SAAS,KAAK,GAAG,KAAK,oDAAoD;IAC1E,OAAO,KAAK,UAAU,KAAK,qEAAqE;IAChG;GACJ;GAEA,MAAM,KAAK,OAAK,SAAS,MAAM,OAAK,QAAQ,IAAI,CAAC;GAYjD,MAAM,aAAgC;IAClC,GAAI;IACJ,UAAU,IAAI;IACd,MAAO,IAAI,QAAmB;IAC9B,SAAS,IAAI,YAAY;IACzB,gBAAiB,IAAI,kBAA6B;IAClD,SAAS,IAAI;GACjB;GAWA,YAAY,IAAI,oBAAoB,GAAmC,CAAC;GAExE,KAAK,KAAK;IAAE;IAC5B;GAAW,CAAC;GACI,OAAO,KACH,sBAAsB,GAAG,IAAI,WAAW,WACrC,WAAW,WAAW,IAAI,WAAW,aAAa,+BAA+B,EACxF;EACJ,SAAS,KAAc;GACnB,MAAM,UACF,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GACnD,SAAS,KAAK,GAAG,KAAK,WAAW,QAAQ,EAAE;GAM3C,OAAO,MAAM,yBAAyB,KAAK,IAAI,SAAS;EAC5D;CACJ;CAGJ,IAAI,SAAS,SAAS,GAClB,OAAO,KACH,UAAU,SAAS,OAAO,2DAC1B,SAAS,KAAK,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,IAAI,IAAI,4FAEjD;CAGJ,OAAO;EAAE;EAAM;CAAS;AAC5B;;;;;;AAOA,SAAgB,oBAAoB,YAAoD;CACpF,OAAO;EACH,UAAU,WAAW;EACrB,GAAI,WAAW,aAAa,KAAA,IAAY,EAAE,UAAU,WAAW,SAAS,IAAI,CAAC;EAC7E,GAAI,WAAW,gBAAgB,KAAA,IAAY,EAAE,aAAa,WAAW,YAAY,IAAI,CAAC;EACtF,GAAI,WAAW,YAAY,KAAA,IAAY,EAAE,SAAS,WAAW,QAAQ,IAAI,CAAC;EAC1E,GAAI,WAAW,mBAAmB,KAAA,IAAY,EAAE,gBAAgB,WAAW,eAAe,IAAI,CAAC;EAC/F,GAAI,WAAW,yBAAyB,KAAA,IAAY,EAAE,sBAAsB,WAAW,qBAAqB,IAAI,CAAC;EACjH,GAAI,WAAW,OAAO,EAAE,OAAO,WAAW,KAAK,IAAI,CAAC;CACxD;AACJ"}
|
|
1
|
+
{"version":3,"file":"cron-loader-CQjvjpEw.js","names":[],"sources":["../src/cron/cron-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { declareCron, type CronJobDefinition, type CronResourceOptions } from \"@rebasepro/types\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\n\nexport interface LoadedCronJob {\n /** Job ID derived from filename (e.g. \"cleanup-sessions\"). */\n id: string;\n /** The full definition. */\n definition: CronJobDefinition;\n}\n\n/** {@link loadCronJobsWithDiagnostics}' answer: what loaded, and what did not. */\nexport interface LoadedCronJobs {\n jobs: LoadedCronJob[];\n /** One entry per file that will NOT be scheduled, with the reason. */\n problems: string[];\n}\n\n/**\n * Auto-discover cron job files from a directory.\n *\n * Each file should default-export a `CronJobDefinition`.\n * The filename (without extension) becomes the job ID:\n * `crons/cleanup-sessions.ts` → id = \"cleanup-sessions\"\n *\n * Follows the same discovery pattern as `loadFunctionsFromDirectory` — see\n * {@link loadCronJobsWithDiagnostics} for what happens to the files it cannot\n * load.\n */\nexport async function loadCronJobsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJob[]> {\n return (await loadCronJobsWithDiagnostics(directory, importModule)).jobs;\n}\n\n/**\n * {@link loadCronJobsFromDirectory}, plus the list of files that were skipped.\n *\n * A job that does not load does not run, and until now the only trace was one\n * `warn` per file in the boot log — next to nothing at 3am when the report that\n * should have been mailed was not. A cron file is written once and then trusted\n * for months, so \"it silently never ran\" is the failure mode this surface has,\n * and a log line is not a surface.\n *\n * Never throws, for the same reason `loadFunctionsWithDiagnostics` does not: one\n * malformed file must not take the server down, and the other jobs in the\n * directory are still worth scheduling. The difference is that the skips are now\n * counted, summarised in one place, and answered by `GET /api/cron` — so the\n * question \"why did nothing run?\" has an answer that does not require log\n * access.\n */\nexport async function loadCronJobsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJobs> {\n const jobs: LoadedCronJob[] = [];\n // Aggregated so a broken job surfaces as one loud summary rather than a\n // warning buried per-file among the boot noise.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { jobs, problems };\n }\n\n const files = fs.readdirSync(directory);\n for (const file of files) {\n if (\n (file.endsWith(\".ts\") || file.endsWith(\".js\")) &&\n // Dotfiles: notably macOS bsdtar AppleDouble sidecars (`._foo.ts`),\n // binary blobs that cannot be imported.\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n !file.endsWith(\".d.ts\") &&\n file !== \"index.ts\" &&\n file !== \"index.js\"\n ) {\n const filePath = path.join(directory, file);\n try {\n const fileUrl = pathToFileURL(filePath).href;\n\n const mod = await importModule(fileUrl);\n\n const exported: unknown = mod.default;\n\n if (!exported || typeof exported !== \"object\") {\n problems.push(`${file} (no default export)`);\n logger.warn(`[cron] ${file}: no valid default export. Skipping.`);\n continue;\n }\n\n const def = exported as Record<string, unknown>;\n if (typeof def.schedule !== \"string\" || typeof def.handler !== \"function\") {\n problems.push(`${file} (default export has no 'schedule' or no 'handler')`);\n logger.warn(`[cron] ${file}: default export missing required 'schedule' or 'handler'. Skipping.`);\n continue;\n }\n\n const id = path.basename(file, path.extname(file));\n // Spread first, then normalise. Rebuilding this object field by\n // field dropped `catchUpWindowSeconds` — and since this loader\n // is the only production caller of `registerJobs`, catch-up\n // never ran for any job authored the documented way: 64 lines\n // of docblock, a docs section, a unit suite and a Postgres e2e\n // all describing a feature that was switched off in the one\n // path that matters. Every existing test built `LoadedCronJob`\n // literals directly, so none of them went through here.\n //\n // The shape is the bug, not the missing line: a field added to\n // `CronJobDefinition` tomorrow would be dropped the same way.\n const definition: CronJobDefinition = {\n ...(def as Partial<CronJobDefinition>),\n schedule: def.schedule as string,\n name: (def.name as string) || id,\n enabled: def.enabled !== false,\n timeoutSeconds: (def.timeoutSeconds as number) || 300,\n handler: def.handler as CronJobDefinition[\"handler\"]\n };\n\n // The declaration, keyed by the same id the scheduler, the\n // routes and the Studio use — the filename. `defineCron` cannot\n // do this: it never sees the filename, and `name` is a label.\n // The derive step runs this same loader, so the graph a host\n // reads and the schedule this process runs come from one place.\n // From the raw export, not the normalised definition: the\n // defaults filled in above (`enabled: true`, `timeoutSeconds:\n // 300`) are this process's, and a graph that records them for\n // every cron would say something the author never wrote.\n declareCron(id, cronResourceOptions(def as unknown as CronJobDefinition));\n\n jobs.push({ id,\ndefinition });\n logger.info(\n `⏰ Loaded cron job: ${id} (${definition.schedule}` +\n `${definition.timezone ? ` ${definition.timezone}` : \" in the process's local time\"})`\n );\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n problems.push(`${file} (threw: ${message})`);\n // At `debug`: the aggregate warning below lists every problem\n // by file, so this line was the same text a second time — and\n // when the throw is a Zod error, that text is ten lines of\n // serialised issues. The reader met it twice before reaching\n // the sentence that says what to do.\n logger.debug(`[cron] Failed to load ${file}: ${message}`);\n }\n }\n }\n\n if (problems.length > 0) {\n logger.warn(\n `[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n \" Fix these or author them with `defineCron(...)` for a typed, compile-checked contract.\"\n );\n }\n\n return { jobs, problems };\n}\n\n/**\n * The declaration a definition makes: every field a host can read without the\n * handler. Spelled once here so the graph and the scheduler cannot disagree\n * about what a cron is.\n */\nexport function cronResourceOptions(definition: CronJobDefinition): CronResourceOptions {\n return {\n schedule: definition.schedule,\n ...(definition.timezone !== undefined ? { timezone: definition.timezone } : {}),\n ...(definition.description !== undefined ? { description: definition.description } : {}),\n ...(definition.enabled !== undefined ? { enabled: definition.enabled } : {}),\n ...(definition.timeoutSeconds !== undefined ? { timeoutSeconds: definition.timeoutSeconds } : {}),\n ...(definition.catchUpWindowSeconds !== undefined ? { catchUpWindowSeconds: definition.catchUpWindowSeconds } : {}),\n ...(definition.name ? { label: definition.name } : {})\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,0BAClB,WACA,eAA+B,qBACP;CACxB,QAAQ,MAAM,4BAA4B,WAAW,YAAY,EAAA,CAAG;AACxE;;;;;;;;;;;;;;;;;AAkBA,eAAsB,4BAClB,WACA,eAA+B,qBACR;CACvB,MAAM,OAAwB,CAAC;CAG/B,MAAM,WAAqB,CAAC;CAE5B,IAAI,CAAC,KAAG,WAAW,SAAS,GACxB,OAAO;EAAE;EAAM;CAAS;CAG5B,MAAM,QAAQ,KAAG,YAAY,SAAS;CACtC,KAAK,MAAM,QAAQ,OACf,KACK,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,KAAK,MAG5C,CAAC,KAAK,WAAW,GAAG,KACpB,CAAC,KAAK,SAAS,QAAQ,KACvB,CAAC,KAAK,SAAS,OAAO,KACtB,SAAS,cACT,SAAS,YACX;EACE,MAAM,WAAW,OAAK,KAAK,WAAW,IAAI;EAC1C,IAAI;GACA,MAAM,UAAU,cAAc,QAAQ,CAAC,CAAC;GAIxC,MAAM,YAAoB,MAFR,aAAa,OAAO,EAAA,CAER;GAE9B,IAAI,CAAC,YAAY,OAAO,aAAa,UAAU;IAC3C,SAAS,KAAK,GAAG,KAAK,qBAAqB;IAC3C,OAAO,KAAK,UAAU,KAAK,qCAAqC;IAChE;GACJ;GAEA,MAAM,MAAM;GACZ,IAAI,OAAO,IAAI,aAAa,YAAY,OAAO,IAAI,YAAY,YAAY;IACvE,SAAS,KAAK,GAAG,KAAK,oDAAoD;IAC1E,OAAO,KAAK,UAAU,KAAK,qEAAqE;IAChG;GACJ;GAEA,MAAM,KAAK,OAAK,SAAS,MAAM,OAAK,QAAQ,IAAI,CAAC;GAYjD,MAAM,aAAgC;IAClC,GAAI;IACJ,UAAU,IAAI;IACd,MAAO,IAAI,QAAmB;IAC9B,SAAS,IAAI,YAAY;IACzB,gBAAiB,IAAI,kBAA6B;IAClD,SAAS,IAAI;GACjB;GAWA,YAAY,IAAI,oBAAoB,GAAmC,CAAC;GAExE,KAAK,KAAK;IAAE;IAC5B;GAAW,CAAC;GACI,OAAO,KACH,sBAAsB,GAAG,IAAI,WAAW,WACrC,WAAW,WAAW,IAAI,WAAW,aAAa,+BAA+B,EACxF;EACJ,SAAS,KAAc;GACnB,MAAM,UACF,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GACnD,SAAS,KAAK,GAAG,KAAK,WAAW,QAAQ,EAAE;GAM3C,OAAO,MAAM,yBAAyB,KAAK,IAAI,SAAS;EAC5D;CACJ;CAGJ,IAAI,SAAS,SAAS,GAClB,OAAO,KACH,UAAU,SAAS,OAAO,2DAC1B,SAAS,KAAK,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,IAAI,IAAI,4FAEjD;CAGJ,OAAO;EAAE;EAAM;CAAS;AAC5B;;;;;;AAOA,SAAgB,oBAAoB,YAAoD;CACpF,OAAO;EACH,UAAU,WAAW;EACrB,GAAI,WAAW,aAAa,KAAA,IAAY,EAAE,UAAU,WAAW,SAAS,IAAI,CAAC;EAC7E,GAAI,WAAW,gBAAgB,KAAA,IAAY,EAAE,aAAa,WAAW,YAAY,IAAI,CAAC;EACtF,GAAI,WAAW,YAAY,KAAA,IAAY,EAAE,SAAS,WAAW,QAAQ,IAAI,CAAC;EAC1E,GAAI,WAAW,mBAAmB,KAAA,IAAY,EAAE,gBAAgB,WAAW,eAAe,IAAI,CAAC;EAC/F,GAAI,WAAW,yBAAyB,KAAA,IAAY,EAAE,sBAAsB,WAAW,qBAAqB,IAAI,CAAC;EACjH,GAAI,WAAW,OAAO,EAAE,OAAO,WAAW,KAAK,IAAI,CAAC;CACxD;AACJ"}
|
|
@@ -3,8 +3,8 @@ 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 { n as errorHandler, t as ApiError } from "./errors-
|
|
7
|
-
import { a as resolveListLimitParam } from "./query-parser-
|
|
6
|
+
import { n as errorHandler, t as ApiError } from "./errors-DMImyqyR.js";
|
|
7
|
+
import { a as resolveListLimitParam } from "./query-parser-BQiPZrM-.js";
|
|
8
8
|
import { Hono } from "hono";
|
|
9
9
|
//#region src/cron/cron-routes.ts
|
|
10
10
|
var cron_routes_exports = /* @__PURE__ */ __exportAll({ createCronRoutes: () => createCronRoutes });
|
|
@@ -69,4 +69,4 @@ function createCronRoutes(scheduler, skipped = 0) {
|
|
|
69
69
|
//#endregion
|
|
70
70
|
export { cron_routes_exports as n, createCronRoutes as t };
|
|
71
71
|
|
|
72
|
-
//# sourceMappingURL=cron-routes-
|
|
72
|
+
//# sourceMappingURL=cron-routes-Bfwni8Zg.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cron-routes-
|
|
1
|
+
{"version":3,"file":"cron-routes-Bfwni8Zg.js","names":[],"sources":["../src/cron/cron-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { CronScheduler } from \"./cron-scheduler\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { resolveListLimitParam } from \"../api/rest/query-parser\";\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\n * GET /:id/logs → get execution logs for a job\n * PUT /:id → update job (enable/disable)\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(\"/\", (c) => {\n const jobs = scheduler.listJobs();\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 rejected.length > 0 ? `${rejected.length} job(s) have an invalid schedule` : 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\", (c) => {\n const id = c.req.param(\"id\");\n const job = scheduler.getJob(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 job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n const log = await scheduler.triggerJob(id);\n return c.json({ log,\njob: scheduler.getJob(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);\n return c.json({ logs });\n });\n\n // Enable/disable a job\n router.put(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const body = await c.req.json().catch(() => ({})) as { enabled: boolean };\n\n if (typeof body.enabled !== \"boolean\") {\n throw ApiError.badRequest(\"Missing 'enabled' boolean in body\");\n }\n\n const job = scheduler.setJobEnabled(id, body.enabled);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n return c.json({ job });\n });\n\n return router;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgBA,SAAgB,iBAAiB,WAA0B,UAAU,GAAkB;CACnF,MAAM,SAAS,IAAI,KAAc;CAGjC,OAAO,QAAQ,YAAY;CAG3B,OAAO,IAAI,MAAM,MAAM;EACnB,MAAM,OAAO,UAAU,SAAS;EAahC,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,GACzD,SAAS,SAAS,IAAI,GAAG,SAAS,OAAO,oCAAoC,KAAA,CACjF,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,OAAO,IAC1B,wBACC,SAAS,SAAS,IACb,oCACA,MACN;GACR;EACJ,CAAC;CACL,CAAC;CAGD,OAAO,IAAI,SAAS,MAAM;EACtB,MAAM,KAAK,EAAE,IAAI,MAAM,IAAI;EAC3B,MAAM,MAAM,UAAU,OAAO,EAAE;EAC/B,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;EAE3B,IAAI,CADQ,UAAU,OAAO,EACxB,GACD,MAAM,SAAS,SAAS,aAAa,GAAG,YAAY;EAGxD,MAAM,MAAM,MAAM,UAAU,WAAW,EAAE;EACzC,OAAO,EAAE,KAAK;GAAE;GACxB,KAAK,UAAU,OAAO,EAAE;EAAE,CAAC;CACvB,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;EACvD,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC;CAC1B,CAAC;CAGD,OAAO,IAAI,QAAQ,OAAO,MAAM;EAC5B,MAAM,KAAK,EAAE,IAAI,MAAM,IAAI;EAC3B,MAAM,OAAO,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,aAAa,CAAC,EAAE;EAEhD,IAAI,OAAO,KAAK,YAAY,WACxB,MAAM,SAAS,WAAW,mCAAmC;EAGjE,MAAM,MAAM,UAAU,cAAc,IAAI,KAAK,OAAO;EACpD,IAAI,CAAC,KACD,MAAM,SAAS,SAAS,aAAa,GAAG,YAAY;EAGxD,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC;CACzB,CAAC;CAED,OAAO;AACX"}
|
|
@@ -3,8 +3,8 @@ 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 "./src-
|
|
7
|
-
import "./src-
|
|
6
|
+
import "./src-DqZ9YiGA.js";
|
|
7
|
+
import "./src-Br6ARbs6.js";
|
|
8
8
|
import { n as createDdlBootstrapper, o as isSQLAdmin, r as hasInCauseChain } from "./ddl-bootstrap-CfNvxMuK.js";
|
|
9
9
|
import { t as revokeInternalTableSql } from "./internal-tables-DYVcFFSv.js";
|
|
10
10
|
import { r as logger } from "./logger-DO2PZc4i.js";
|
|
@@ -182,4 +182,4 @@ function rowToLogEntry(row) {
|
|
|
182
182
|
//#endregion
|
|
183
183
|
export { cron_store_exports as n, createCronStore as t };
|
|
184
184
|
|
|
185
|
-
//# sourceMappingURL=cron-store-
|
|
185
|
+
//# sourceMappingURL=cron-store-Bsiw4Q6u.js.map
|