@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc
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/README.md +27 -2
- package/dist/GCSStorageController-fv6JAZ87.js +402 -0
- package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
- package/dist/S3StorageController-BNg-pXiY.js +480 -0
- package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
- package/dist/api/mount-path.d.ts +30 -0
- package/dist/api/mount.d.ts +6 -13
- package/dist/api/rest/api-generator.d.ts +53 -1
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +7 -5
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +35 -0
- package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
- package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
- package/dist/auth/access.d.ts +57 -8
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +1 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
- package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
- package/dist/auth/builtin-auth-adapter.d.ts +13 -0
- package/dist/auth/email-change-routes.d.ts +3 -0
- package/dist/auth/expired-token-sweep.d.ts +5 -1
- package/dist/auth/interfaces.d.ts +10 -0
- package/dist/auth/jwks-routes.d.ts +5 -1
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +282 -166
- package/dist/auth/magic-link-routes.d.ts +3 -0
- package/dist/auth/mfa-crypto.d.ts +9 -3
- package/dist/auth/mfa-gate.d.ts +4 -2
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/otp-routes.d.ts +3 -0
- package/dist/auth/rate-limiter.d.ts +61 -34
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/routes.d.ts +16 -0
- package/dist/auth/session-routes.d.ts +6 -0
- package/dist/auth/user-picking.d.ts +37 -0
- package/dist/auth/verify-credential.d.ts +25 -4
- package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
- package/dist/auth-GnONzteu.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend/create-backend.d.ts +281 -0
- package/dist/backend/default-backend.d.ts +35 -0
- package/dist/backend/options.d.ts +912 -0
- package/dist/backend/scope.d.ts +23 -0
- package/dist/backend/services.d.ts +54 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +14 -3
- package/dist/backup/backup-routes.d.ts +23 -2
- package/dist/backup/backup-storage.d.ts +3 -1
- package/dist/backup/index.d.ts +3 -1
- package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
- package/dist/backup-BnvkSLHi.js.map +1 -0
- package/dist/boot/boot.d.ts +2 -2
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +9 -2
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/provision.d.ts +4 -2
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/sources.d.ts +2 -0
- package/dist/boot/static-routing.d.ts +9 -0
- package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
- package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
- package/dist/cron/cron-loader.d.ts +77 -18
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +69 -3
- package/dist/cron/cron-store.d.ts +33 -5
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +3 -3
- package/dist/cron-loader-B-u0snqq.js +202 -0
- package/dist/cron-loader-B-u0snqq.js.map +1 -0
- package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
- package/dist/cron-routes-OD6BstUj.js.map +1 -0
- package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
- package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
- package/dist/cron-store-CZ30ZBi-.js +577 -0
- package/dist/cron-store-CZ30ZBi-.js.map +1 -0
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/env.d.ts +0 -37
- package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
- package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
- package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
- package/dist/function-loader-iKhOZatS.js.map +1 -0
- package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
- package/dist/function-routes-j9TwpvQe.js.map +1 -0
- package/dist/functions/context.d.ts +9 -0
- package/dist/functions/define-function.d.ts +10 -6
- package/dist/functions/function-loader.d.ts +75 -18
- package/dist/functions/function-routes.d.ts +2 -1
- package/dist/functions/index.d.ts +5 -4
- package/dist/functions/index.js +42 -13
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/internal.d.ts +2 -2
- package/dist/functions/proxy.d.ts +6 -2
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
- package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
- package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +23 -10
- package/dist/index.es.js +24423 -20038
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +27 -10
- package/dist/init/process-safety.d.ts +7 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +49 -865
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
- package/dist/jobs-BCbflZj2.js.map +1 -0
- package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
- package/dist/jwt-BlvuKoR6.js.map +1 -0
- package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
- package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
- package/dist/load-failure-L5hvfDoG.js +80 -0
- package/dist/load-failure-L5hvfDoG.js.map +1 -0
- package/dist/mcp/mcp-routes.d.ts +6 -0
- package/dist/mcp/mcp-tools.d.ts +8 -0
- package/dist/mcp/oauth-routes.d.ts +7 -0
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
- package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
- package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
- package/dist/proxy-CkTlMGmh.js.map +1 -0
- package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
- package/dist/query-parser-4WkMoA_K.js.map +1 -0
- package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +10 -0
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/remote-source.d.ts +13 -3
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
- package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +5 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/singleton.d.ts +9 -5
- package/dist/soft-delete-params-DfV3vVN5.js +668 -0
- package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
- package/dist/sql-export-routes-ClRwzIsC.js +240 -0
- package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
- package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
- package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +46 -2
- package/dist/storage/LocalStorageController.d.ts +67 -2
- package/dist/storage/S3StorageController.d.ts +37 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/routes.d.ts +20 -0
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +46 -2
- package/dist/storage/types.d.ts +82 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-BSiP1c-f.js +0 -256
- package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
- package/dist/S3StorageController-CAwFRgjV.js +0 -276
- package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
- package/dist/auth-DMLngxn_.js.map +0 -1
- package/dist/backup-CN0s50D2.js.map +0 -1
- package/dist/contract-routes-fz8i4pxs.js.map +0 -1
- package/dist/cron-loader-CwaANlOG.js +0 -118
- package/dist/cron-loader-CwaANlOG.js.map +0 -1
- package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
- package/dist/cron-scheduler-CYQgco86.js.map +0 -1
- package/dist/cron-store-D2Q9-Aco.js +0 -300
- package/dist/cron-store-D2Q9-Aco.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/function-loader-D7o5Epjj.js.map +0 -1
- package/dist/function-routes-CaNG4waN.js.map +0 -1
- package/dist/jobs-DqYNfquG.js.map +0 -1
- package/dist/jwt-R6bSPMjk.js.map +0 -1
- package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
- package/dist/proxy-qRlqeUmO.js.map +0 -1
- package/dist/query-parser-BgiKJKvc.js.map +0 -1
- package/dist/request-timeout-DgH7j8qO.js.map +0 -1
- package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
- package/dist/soft-delete-params-BWPilMPF.js +0 -59
- package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
- package/dist/src-I3aG1PcY.js +0 -6870
- package/dist/src-I3aG1PcY.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-import-X40pTZUQ.js","names":[],"sources":["../src/utils/dynamic-import.ts"],"sourcesContent":["/**\n * Shape returned by a dynamic module import: an object whose `default`\n * property holds the module's default export (CJS `module.exports` is exposed\n * as `default` by Node's ESM interop).\n */\nexport interface ImportedModule {\n default?: unknown;\n [key: string]: unknown;\n}\n\n/**\n * Imports a module by file URL. Injected into the directory loaders so tests\n * can supply a deterministic importer.\n */\nexport type ModuleImporter = (url: string) => Promise<ImportedModule>;\n\n/**\n * Native ESM dynamic import.\n *\n * Wrapped in `new Function` so TypeScript / bundlers don't down-compile the\n * `import()` to `require()` — the loaders must import user-authored `.ts`/`.js`\n * files at runtime using the host's real ESM loader. This is the production\n * default for {@link loadFunctionsFromDirectory} / {@link loadCronJobsFromDirectory}.\n *\n * @internal\n */\nexport const nativeDynamicImport: ModuleImporter = (() => {\n const doImport = new Function(\"url\", \"return import(url)\") as (url: string) => Promise<ImportedModule>;\n return (url: string) => doImport(url);\n})();\n"],"mappings":";;;;;;;;;;;;;;;AA0BA,IAAa,6BAA6C;CACtD,MAAM,WAAW,IAAI,SAAS,OAAO,oBAAoB;CACzD,QAAQ,QAAgB,SAAS,GAAG;AACxC,EAAA,CAAG"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"function-loader-D7o5Epjj.js","names":[],"sources":["../src/functions/function-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { Hono } from \"hono\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\n\nexport interface LoadedFunction {\n /** Endpoint name derived from filename (e.g., \"send-invoice\") */\n name: string;\n /** The Hono sub-app to mount */\n app: Hono<import(\"hono\").Env>;\n}\n\n/** What a directory of function files produced: what mounted, and what did not. */\nexport interface LoadedFunctions {\n /** The functions that will be served. */\n functions: LoadedFunction[];\n /**\n * One entry per file the loader saw and did **not** mount, each already\n * phrased as `<name> (<reason>)`. Returned rather than only logged so the\n * running server can say what is missing — a boot log line is not reachable\n * from `GET /api/functions`.\n */\n problems: string[];\n}\n\n/**\n * Extensions the bundler compiles (or a developer might reasonably write) that\n * this loader cannot import. `rebase build` globs `functions/**\\/*.ts`, so the\n * build's idea of \"a function file\" is strictly wider than the runtime's — the\n * worst direction for a mismatch to go. Anything listed here is reported as a\n * problem instead of vanishing; non-code files (`.md`, `.json`, `.txt`) stay\n * silent, because a README next to your functions is not a mistake.\n */\nconst UNSUPPORTED_CODE_EXTENSIONS = [\".mts\", \".cts\", \".tsx\", \".jsx\", \".mjs\", \".cjs\"];\n\n/**\n * Auto-discover Hono route files from a directory.\n *\n * Each file should default-export a Hono app (or router).\n * The filename (without extension) becomes the mount path:\n * `functions/send-invoice.ts` → mounted at `/send-invoice`\n *\n * This mirrors how `loadCollectionsFromDirectory` works for collections.\n *\n * Returns only what loaded. Use {@link loadFunctionsWithDiagnostics} when the\n * caller also needs to report what did not.\n */\nexport async function loadFunctionsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedFunction[]> {\n return (await loadFunctionsWithDiagnostics(directory, importModule)).functions;\n}\n\n/**\n * {@link loadFunctionsFromDirectory}, plus the list of files that were skipped.\n *\n * Never throws: a single malformed function must not crash server boot. The\n * caller decides what to do with `problems` — `init.ts` mounts the router\n * regardless and surfaces the count on the listing endpoint.\n */\nexport async function loadFunctionsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedFunctions> {\n const functions: LoadedFunction[] = [];\n // Aggregate problem files so a broken function surfaces as one loud\n // summary line, not just a warning buried per-file. We still don't throw:\n // a single malformed function must not crash server boot.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { functions, problems };\n }\n\n // `withFileTypes` so a directory entry is a *reported* skip rather than a\n // filter miss. `readdirSync(dir)` returned bare names, and a subdirectory\n // simply failed the `.ts`/`.js` test — so `functions/admin/users.ts` was\n // compiled by `rebase build`, shipped in the bundle, and then dropped at\n // boot without a single log line.\n const entries = fs.readdirSync(directory, { withFileTypes: true });\n for (const entry of entries) {\n const file = entry.name;\n\n if (entry.isDirectory()) {\n // Dot-directories are tooling (`.git`, `.turbo`), not intent.\n if (file.startsWith(\".\") || file === \"node_modules\") continue;\n logger.warn(\n `[functions] ${file}/: subdirectory ignored. Functions are loaded from the top level of ` +\n `${directory} only, so nothing under ${file}/ is served. Move the file up (or flatten the ` +\n \"name: `admin/users.ts` → `admin-users.ts`).\"\n );\n problems.push(`${file}/ (subdirectory — functions are not loaded recursively)`);\n continue;\n }\n\n const extension = path.extname(file);\n if (\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n UNSUPPORTED_CODE_EXTENSIONS.includes(extension)\n ) {\n logger.warn(\n `[functions] ${file}: ${extension} files are not loaded. Rename it to .ts (or .js) to serve it.`\n );\n problems.push(`${file} (unsupported extension ${extension})`);\n continue;\n }\n\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 = mod.default;\n\n if (!exported) {\n logger.warn(`[functions] ${file}: no default export. Skipping.`);\n problems.push(`${file} (no default export)`);\n continue;\n }\n\n // Accept a Hono instance — use duck-typing to handle different\n // Hono versions which may not share the same prototype.\n if (isHonoLike(exported)) {\n const name = path.basename(file, path.extname(file));\n functions.push({ name,\napp: exported as Hono });\n logger.debug(`⚡ Loaded function route: ${name}`);\n continue;\n }\n\n // Also accept a factory function that returns a Hono instance\n if (typeof exported === \"function\") {\n const result = exported();\n if (isHonoLike(result)) {\n const name = path.basename(file, path.extname(file));\n functions.push({ name,\napp: result as Hono });\n logger.debug(`⚡ Loaded function route: ${name}`);\n continue;\n }\n }\n\n // Provide actionable diagnostics\n const exportType = typeof exported;\n const keys = exported && typeof exported === \"object\"\n ? Object.getOwnPropertyNames(Object.getPrototypeOf(exported)).slice(0, 10).join(\", \")\n : \"N/A\";\n logger.warn(\n `[functions] ${file}: default export is not a Hono app or factory. Skipping.\\n` +\n ` export type: ${exportType}${exported?.constructor?.name ? ` (${exported.constructor.name})` : \"\"}\\n` +\n ` prototype methods: ${keys}\\n` +\n \" Hint: ensure the function exports a Hono app created with the same hono version as the server.\\n\" +\n \" Author with `defineFunction(...)` from @rebasepro/server for a typed, checked contract.\\n\" +\n \" The loader checks for .fetch() and .routes — any Hono-compatible app will work.\"\n );\n problems.push(`${file} (not a Hono app or factory)`);\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n logger.error(`[functions] Failed to load ${file}: ${message}`);\n problems.push(`${file} (threw: ${message})`);\n }\n }\n }\n\n if (problems.length > 0) {\n // Advice keyed to what actually went wrong. The old line said\n // \"author them with `defineFunction(...)`\" for every failure — which is\n // the fix for a wrong export shape and no help at all for the commonest\n // one: a file that threw while being imported, usually a module-scope\n // `process.env.X` read that came back undefined. Told to reach for\n // `defineFunction`, an author rewrites a file whose export was already\n // correct and gets the same failure.\n const advice: string[] = [];\n if (problems.some(p => p.includes(\"(threw:\"))) {\n advice.push(\n \" A file that threw ran at import time. Read configuration *inside* a handler \" +\n \"(`requireEnv(c, \\\"STRIPE_KEY\\\")`, or a lazily-built client) rather than at module \" +\n \"scope, where one undefined variable takes the whole file down before any route exists.\"\n );\n }\n if (problems.some(p => p.includes(\"(no default export)\"))) {\n advice.push(\n \" A file with no default export exports nothing the loader can mount. \" +\n \"`export default defineFunction((app) => { … })`.\"\n );\n }\n if (problems.some(p => p.includes(\"(not a Hono app or factory)\"))) {\n advice.push(\n \" A default export the loader did not recognise is usually two copies of hono. \" +\n \"Author with `defineFunction(...)` from @rebasepro/server/functions, which uses the \" +\n \"server's own copy.\"\n );\n }\n logger.warn(\n `[functions] ${problems.length} function file(s) were skipped and will NOT be served:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n advice.join(\"\\n\")\n );\n }\n\n return { functions, problems };\n}\n\n/**\n * Duck-type check for Hono apps.\n * We avoid `instanceof Hono` because different Hono versions\n * installed in the user's project vs. our dependencies will\n * not share the same prototype, causing false negatives.\n */\nfunction isHonoLike(obj: unknown): boolean {\n if (!obj || typeof obj !== \"object\") return false;\n // Hono instances always have .fetch() and .routes\n const record = obj as Record<string, unknown>;\n return (\n typeof record.fetch === \"function\" &&\n Array.isArray(record.routes)\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"function-routes-CaNG4waN.js","names":[],"sources":["../src/auth/admin-roles.ts","../src/functions/context.ts","../src/functions/guards.ts","../src/functions/function-routes.ts"],"sourcesContent":["/**\n * The admin role and scope matching, for code that may not import\n * `@rebasepro/types`.\n *\n * The custom-functions surface (`@rebasepro/server/functions`) must bundle for\n * a runtime with no Node built-ins and imports nothing but `hono`, so its\n * guards cannot reach the canonical definitions in `@rebasepro/types`\n * (`ADMIN_ROLE`, `hasAdminRole`, `scopeGrants`). These are the same rules,\n * restated, and `test/admin-roles.test.ts` holds them equal.\n *\n * Everything else imports the `@rebasepro/types` versions.\n */\n\n/** The one built-in role. It holds every scope. */\nexport const ADMIN_ROLE_NAME = \"admin\";\n\n/** Does this list of roles include the admin role? */\nexport function holdsAdminRole(roles: readonly string[] | null | undefined): boolean {\n return !!roles?.includes(ADMIN_ROLE_NAME);\n}\n\n/**\n * Does a set of held scopes grant `scope`, optionally on one `target`? The\n * unqualified grant covers every target; `scope:target` covers its own.\n */\nexport function heldScopesGrant(held: readonly string[], scope: string, target?: string): boolean {\n for (const entry of held) {\n if (entry === scope) return true;\n if (target !== undefined && entry === `${scope}:${target}`) return true;\n }\n return false;\n}\n","/**\n * Reading the request context from inside a custom function.\n *\n * The functions router resolves the caller's identity before any handler runs\n * and leaves the result on the Hono context. Getting it back out used to be the\n * user's problem, and the shape made that worse than it sounds: `HonoEnv`\n * types `user` as `AuthResult`, a union that includes `boolean`, `null` and an\n * index signature, because the same slot is filled by four different middlewares\n * — JWT, service key, API key, and a user-supplied validator that may return\n * `true`. Every example in the documentation therefore opened with\n *\n * const user = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n *\n * and an assertion in a security-relevant position is exactly the kind of line\n * that gets copied once and then never re-examined. It is also wrong in one\n * case that occurs in practice: a custom validator returning `true` stores\n * `{ uid: \"default\", roles: [] }`, which the assertion above types as having a\n * `uid` — true here, but nothing checks it.\n *\n * These accessors do the narrowing once, in the framework, where it can be\n * tested. They are also **runtime-neutral by construction** — no crypto, no\n * token parsing, no I/O, nothing but property reads on an object another\n * middleware already populated. That is what lets them live in\n * `@rebasepro/server/functions` and run unchanged on a host that has no Node\n * built-ins.\n *\n * @module\n */\nimport type { Context } from \"hono\";\nimport type { DataDriver } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { ApiKeyMasked } from \"../auth/api-keys/api-key-types\";\nimport { heldScopesGrant, holdsAdminRole } from \"../auth/admin-roles\";\n\n/**\n * The caller, as a custom function sees them.\n *\n * A narrowed view of whatever the auth middleware resolved: `uid` and `roles`\n * are guaranteed, and the index signature keeps any extra claims the token or\n * the adapter carried (`email`, `org_id`, anything a custom validator added)\n * reachable without a cast.\n */\nexport interface FunctionUser {\n /** Stable id of the caller. `\"service\"` for service-key and API-key callers. */\n uid: string;\n /** Roles as resolved for this request. Never `undefined` — an empty array instead. */\n roles: string[];\n /** Present when the identity carried one. Not every auth method does. */\n email?: string;\n /** Any further claim the token, adapter or validator supplied. */\n [claim: string]: unknown;\n}\n\n/** Anything with a Hono-style `.get`, so these work on any `Context` shape. */\ntype CtxLike = Context<HonoEnv> | Context;\n\nfunction read<K extends keyof HonoEnv[\"Variables\"]>(\n c: CtxLike,\n key: K\n): HonoEnv[\"Variables\"][K] | undefined {\n // `c.get` is typed against the app's own Env, which a handler mounted\n // through `app.route()` may have declared more loosely. The cast is\n // confined to this one function rather than repeated at every call site.\n return (c as Context<HonoEnv>).get(key);\n}\n\n/**\n * The authenticated caller, or `undefined` for an anonymous request.\n *\n * **`undefined` is not a permission decision.** The functions router mounts its\n * auth middleware with `requireAuth: false` on purpose — a webhook receiver has\n * no token to send — so an anonymous caller reaches the handler and reads\n * `undefined` here while the handler runs on regardless. Use {@link requireAuth}\n * (or a `!user` branch that returns 401) to make it a decision.\n *\n * A caller who presented a *bad* token never gets this far: both auth\n * middlewares reject an unverifiable token with 401 before the router is\n * reached, precisely so an expired session cannot be silently downgraded to an\n * anonymous one.\n */\nexport function getUser(c: CtxLike): FunctionUser | undefined {\n const raw = read(c, \"user\");\n if (!raw || typeof raw !== \"object\") return undefined;\n\n const record = raw as Record<string, unknown>;\n const uid = typeof record.uid === \"string\" ? record.uid : undefined;\n if (uid === undefined) return undefined;\n\n const roles = Array.isArray(record.roles)\n ? record.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n\n return { ...record,\n uid,\n roles } as FunctionUser;\n}\n\n/** The caller's id, or `undefined` when nobody is signed in. */\nexport function getUserId(c: CtxLike): string | undefined {\n return getUser(c)?.uid;\n}\n\n/** The caller's roles. Empty for an anonymous request — never `undefined`. */\nexport function getRoles(c: CtxLike): string[] {\n return getUser(c)?.roles ?? [];\n}\n\n/**\n * Whether the caller holds **any** of the named roles.\n *\n * Any rather than all, because that is what a route guard means by a list of\n * roles; require several by calling this more than once.\n */\nexport function hasRole(c: CtxLike, ...roles: string[]): boolean {\n if (roles.length === 0) return false;\n const held = new Set(getRoles(c));\n return roles.some(role => held.has(role));\n}\n\n/** Whether the caller holds the `admin` role, which holds every scope. */\nexport function isAdmin(c: CtxLike): boolean {\n return holdsAdminRole(getRoles(c));\n}\n\n/**\n * Everything the caller may do, as `resource:action[:target]` scope strings —\n * the data plane, the app's own `auth.scopes`, and whatever their roles (or\n * their API key) hold. Empty for an anonymous request.\n *\n * Resolved by the framework before the handler runs; `undefined` only when no\n * Rebase auth middleware ran (see {@link identityResolved}).\n */\nexport function getScopes(c: CtxLike): string[] | undefined {\n return read(c, \"scopes\");\n}\n\n/**\n * Whether the caller holds `scope`, on `target` when one is given.\n *\n * For an app scope declared under `auth.scopes` — `project:deploy` — a\n * signed-in person always holds it, so this narrows only API keys and tokens.\n * Whether the *person* may deploy *this* project is still the handler's to\n * decide.\n */\nexport function hasScope(c: CtxLike, scope: string, target?: string): boolean {\n return heldScopesGrant(getScopes(c) ?? [], scope, target);\n}\n\n/** Whether the request carries an identity at all. */\nexport function isAuthenticated(c: CtxLike): boolean {\n return getUser(c) !== undefined;\n}\n\n/**\n * The request-scoped data driver: reads and writes run as **the caller**, with\n * your row-level security policies evaluated against their identity.\n *\n * This is the accessor to reach for when a function serves user-facing data.\n * `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs\n * as `{ uid: \"service\", roles: [\"admin\"] }` for every caller alike, which is\n * correct for trusted background work and wrong for a request.\n *\n * `undefined` only when no Rebase auth middleware ran (see\n * {@link identityResolved}); inside a function mounted by the framework it is\n * always present, anonymous requests included — they get an anon-scoped driver\n * so policies still have an identity to evaluate.\n */\nexport function getDriver(c: CtxLike): DataDriver | undefined {\n return read(c, \"driver\");\n}\n\n/**\n * {@link getDriver}, but throws instead of handing back `undefined`.\n *\n * For the common case where a handler cannot proceed without it and would\n * otherwise write `c.get(\"driver\")!` — an assertion that turns a wiring problem\n * into `Cannot read properties of undefined (reading 'fetchCollection')` twenty\n * lines away from the cause.\n */\nexport function requireDriver(c: CtxLike): DataDriver {\n const driver = getDriver(c);\n if (!driver) {\n throw new Error(\n \"No request-scoped driver on this context. A Rebase auth middleware \" +\n \"populates it before any custom function runs, so this means the handler \" +\n \"was mounted outside the functions router — e.g. added to your own Hono \" +\n \"app directly. Mount it from the functions directory, or use \" +\n \"`rebase.dataAsAdmin` if the work is genuinely service-scoped.\"\n );\n }\n return driver;\n}\n\n/**\n * The API key this request authenticated with, masked, or `undefined` when it\n * did not use one.\n *\n * Useful for attribution and for per-key behaviour. The permission check itself\n * has already happened — reaching a handler means the key was allowed to.\n */\nexport function getApiKey(c: CtxLike): ApiKeyMasked | undefined {\n return read(c, \"apiKey\");\n}\n\n/**\n * The correlation id for this request — generated, or taken from an inbound\n * `X-Request-ID`.\n *\n * Log it. It is the only thing that ties a line written inside a function to\n * the framework's own lines for the same request.\n */\nexport function getRequestId(c: CtxLike): string | undefined {\n return read(c, \"requestId\");\n}\n\n/**\n * Whether a Rebase auth middleware has run on this request.\n *\n * Both middlewares populate `driver` for *every* outcome, anonymous included,\n * and populate `user` whenever there is one. So \"neither is set\" does not mean\n * \"anonymous\" — it means nothing resolved the identity, and treating that as\n * anonymous is the dangerous reading. The guards use this to tell a genuinely\n * anonymous caller (401) from a misconfigured mount (500), because answering\n * 401 to the second sends whoever is debugging it to look at the token.\n */\nexport function identityResolved(c: CtxLike): boolean {\n return read(c, \"user\") !== undefined || read(c, \"driver\") !== undefined;\n}\n","/**\n * Route guards for custom functions.\n *\n * These decide access from the identity the platform already resolved. They do\n * **not** verify tokens, and that division is the point rather than a\n * limitation:\n *\n * - Verifying a token needs a signing key, constant-time comparison and a\n * revocation lookup. That is host work, it belongs to the process that holds\n * the secret, and it is the part of the stack that cannot be made\n * runtime-neutral without rewriting it against WebCrypto.\n * - Deciding whether *this* caller may call *this* route is application work.\n * It needs nothing but the resolved identity, so it costs nothing to make it\n * portable — and it is the half that lives in user code.\n *\n * Splitting there is what lets a function file compile and run unchanged on a\n * host with no Node built-ins, and it is why these live in\n * `@rebasepro/server/functions` while `verifyAccessToken` does not.\n *\n * **Inside the functions router these are equivalent to the guards exported\n * from the package root.** Both auth middlewares resolve the identity before\n * any handler runs: a valid credential populates `user`, an invalid one is\n * rejected with 401 by the middleware itself, and a missing one leaves `user`\n * unset. So the root `requireAuth`'s token-parsing branch is unreachable from a\n * function, and removing it changes no outcome. The one difference is a handler\n * mounted **outside** the framework's router, where no middleware ran: the root\n * guard would parse the `Authorization` header itself, and these refuse the\n * request with a 500 that names the wiring problem. Fail-closed, and legible.\n *\n * @module\n */\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { getUser, isAdmin, getRoles, identityResolved, hasScope } from \"./context\";\n\n/**\n * The answer to \"a guard ran, but no middleware had resolved anything\".\n *\n * Deliberately a 500 and not a 401. A 401 tells the caller their credential is\n * the problem, and here the caller's credential was never looked at — sending\n * them to check their token is sending them to the one place the answer is not.\n */\nfunction unresolvedIdentity(): { error: { message: string; code: string } } {\n return {\n error: {\n message:\n \"This route's identity was never resolved: no Rebase auth middleware ran \" +\n \"before the guard. A function loaded from the functions directory always \" +\n \"has one. This usually means the Hono app was mounted onto your own \" +\n \"server directly, bypassing the functions router.\",\n code: \"AUTH_MIDDLEWARE_MISSING\"\n }\n };\n}\n\n/**\n * Reject anonymous callers with 401.\n *\n * Put it in the route's own middleware slot rather than `app.use(\"/*\", …)`:\n * `use()` covers only the routes declared *below* it, so a route appended later\n * — by you, months from now, at the bottom of the file — is silently\n * unprotected. The per-route form cannot drift that way.\n *\n * @example\n * ```ts\n * app.post(\"/\", requireAuth, async (c) => {\n * const user = getUser(c)!; // guaranteed by the guard\n * return c.json({ uid: user.uid });\n * });\n * ```\n */\nexport const requireAuth: MiddlewareHandler<HonoEnv> = async (c, next) => {\n if (getUser(c)) return next();\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n};\n\n/**\n * Reject callers without the `admin` role with 403.\n *\n * Must come **after** {@link requireAuth}: on its own it answers 401 for an\n * anonymous caller, which is right, but pairing them keeps the two failures\n * distinguishable — 401 \"who are you\", 403 \"not you\".\n *\n * Prefer {@link requireScope} for anything an app declares a scope for: a\n * scope can be granted to a narrower role and to a key, and `admin` cannot.\n */\nexport const requireAdmin: MiddlewareHandler<HonoEnv> = async (c, next) => {\n const user = getUser(c);\n if (!user) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n\n if (!isAdmin(c)) {\n return c.json({\n error: {\n message: \"Admin privileges required for this operation\",\n code: \"FORBIDDEN\"\n }\n }, 403);\n }\n\n return next();\n};\n\n/**\n * Reject callers holding none of the named roles with 403.\n *\n * Any of them, not all — require several by chaining the guard twice. Naming no\n * role at all is a programming error and throws at module load rather than at\n * request time, because `requireRole()` with an empty list would otherwise read\n * as a guard while admitting everyone.\n *\n * @example\n * ```ts\n * app.post(\"/publish\", requireAuth, requireRole(\"editor\", \"admin\"), handler);\n * ```\n */\nexport function requireRole(...roles: string[]): MiddlewareHandler<HonoEnv> {\n if (roles.length === 0) {\n throw new Error(\n \"requireRole() needs at least one role. An empty list would admit every \" +\n \"signed-in caller while reading as a restriction.\"\n );\n }\n\n const allowed = new Set(roles);\n return async (c, next) => {\n const user = getUser(c);\n if (!user) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n\n if (!getRoles(c).some(role => allowed.has(role))) {\n return c.json({\n error: {\n message: `This operation requires one of these roles: ${roles.join(\", \")}`,\n code: \"FORBIDDEN\"\n }\n }, 403);\n }\n\n return next();\n };\n}\n\n/**\n * Reject callers who do not hold `scope` with 403.\n *\n * `scope` is a built-in scope or one the app declares under `auth.scopes` on\n * the users collection. `target` narrows it to one resource — a key holding\n * `project:deploy:p1` passes `requireScope(\"project:deploy\", c => c.req.param(\"project\"))`\n * for `p1` only — and may be read from the request.\n *\n * A signed-in person holds every app scope, so for a person this is no\n * authorization at all: it narrows API keys and tokens. Decide whether the\n * person may act in the handler, as you would without it.\n *\n * @example\n * ```ts\n * app.post(\"/deploy/:project\", requireAuth, requireScope(\"project:deploy\", c => c.req.param(\"project\")), handler);\n * ```\n */\nexport function requireScope(\n scope: string,\n target?: string | ((c: Context<HonoEnv>) => string | undefined)\n): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n if (!getUser(c)) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n const resolvedTarget = typeof target === \"function\" ? target(c) : target;\n if (!hasScope(c, scope, resolvedTarget)) {\n const wanted = resolvedTarget !== undefined ? `${scope}:${resolvedTarget}` : scope;\n return c.json({\n error: {\n message: `This credential does not hold the \"${wanted}\" scope.`,\n code: \"SCOPE_MISSING\",\n details: { requiredScope: wanted }\n }\n }, 403);\n }\n return next();\n };\n}\n","import { Hono } from \"hono\";\nimport { HonoEnv } from \"../api/types\";\nimport { ApiError } from \"../api/errors\";\nimport { LoadedFunction } from \"./function-loader\";\nimport { requireAuth } from \"./guards\";\n\n/** The file a loader problem names, without its extension: `broken.ts (threw: …)` → `broken`. */\nfunction problemName(problem: string): string {\n return problem.split(\" \")[0].replace(/\\/$/, \"\").replace(/\\.[cm]?[jt]s$/, \"\");\n}\n\n/**\n * Mount all loaded function routes under a single Hono router.\n *\n * Each function is mounted at `/<function-name>`, preserving\n * whatever HTTP methods and middleware the Hono sub-app defines.\n *\n * @param functions What loaded. May be empty — the router still mounts, so\n * \"no functions are served\" answers 200 with an empty list instead of 404.\n * @param problems The files the loader saw and could not serve, as\n * `\"<file> (<reason>)\"`. The listing reports a count and a pointer to the log,\n * not the reasons: those carry import errors, and the listing is one guard\n * away from anyone. The unmatched-route handler uses the *names* to answer the\n * one question a 404 on a function nobody can find should answer — \"there is a\n * file for this and it did not load\" — which is the difference between a typo\n * and a broken deploy, and the loader was the only thing that knew.\n */\nexport function createFunctionRoutes(\n functions: LoadedFunction[],\n problems: string[] = [],\n /**\n * Where this router is mounted, so the listing can report a path a caller\n * can actually request. It used to hardcode `/functions/<name>`, which is\n * wrong under every `basePath` including the default `/api`.\n */\n mountPath = \"/functions\"\n): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n const skipped = problems.length;\n\n // Listing endpoint: GET / → list available functions.\n //\n // Functions themselves stay anonymous-callable by default — a webhook\n // receiver has to be — but the index of them does not: it is an inventory\n // of every custom endpoint, for whoever asks. `requireAuth` admits any\n // resolved identity (a signed-in user, an API key, the service key), so\n // `rebase doctor` and `rebase cloud debug` keep their answer; the latter\n // already reads a 401 here as \"mounted\".\n router.get(\"/\", requireAuth, (c) => {\n return c.json({\n functions: functions.map((fn) => ({\n name: fn.name,\n endpoint: `${mountPath}/${fn.name}`\n })),\n ...(skipped > 0 && {\n skipped,\n note: `${skipped} function file(s) failed to load and are NOT served — see the server log for the reason.`\n })\n });\n });\n\n for (const fn of functions) {\n router.route(`/${fn.name}`, fn.app);\n }\n\n // A name that matches nothing, answered in the envelope.\n //\n // Registered last so every real route wins it. Without it, a typo'd\n // function name — the single most likely 404 a developer meets on this\n // surface — fell through to Hono's default `404 Not Found` as `text/plain`,\n // and through the SDK arrived as `RebaseApiError { code: undefined }`, so\n // the documented `e.code === \"FUNCTION_NOT_FOUND\"` branch never ran.\n //\n // What the message may say depends on who is asking. The mounted names are\n // an inventory of every custom endpoint, which is exactly what the listing\n // above requires an identity to see — so an anonymous caller is told their\n // name is unknown and nothing more, and a resolved caller gets the list\n // that turns the 404 into a fix.\n const mounted = new Set(functions.map(fn => fn.name));\n const failedToLoad = new Map(problems.map(p => [problemName(p), p.split(\" \")[0]]));\n\n router.all(\"/:name{.*}\", (c): never => {\n const requested = (c.req.param(\"name\") ?? \"\").split(\"/\").filter(Boolean);\n const name = requested[0] ?? \"\";\n const rest = requested.slice(1).join(\"/\");\n const identified = Boolean(c.get(\"user\"));\n\n const refuse = (message: string): never => {\n throw new ApiError(404, \"FUNCTION_NOT_FOUND\", message, { function: name }, true);\n };\n\n if (!name) {\n refuse(`No function in the request path. Expected ${mountPath}/<function>.`);\n }\n if (mounted.has(name)) {\n refuse(\n `The function '${name}' is served, and has no ${c.req.method} route at '/${rest}'. ` +\n \"The path after the function name is routed by the function's own Hono app.\"\n );\n }\n const file = failedToLoad.get(name);\n if (file) {\n refuse(\n `The function '${name}' is not served: '${file}' failed to load. ` +\n \"The server log records why, at boot.\"\n );\n }\n return refuse(\n `No function named '${name}' on this backend.` +\n (identified\n ? (mounted.size > 0\n ? ` This backend serves: ${[...mounted].sort().join(\", \")}.`\n : \" This backend serves no functions.\")\n : \"\") +\n (skipped > 0 && identified\n ? ` ${skipped} function file(s) failed to load and are not served — see the server log.`\n : \"\")\n );\n });\n\n return router;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAcA,IAAa,kBAAkB;;AAG/B,SAAgB,eAAe,OAAsD;CACjF,OAAO,CAAC,CAAC,OAAO,SAAS,eAAe;AAC5C;;;ACqCA,SAAS,KACL,GACA,KACmC;CAInC,OAAQ,EAAuB,IAAI,GAAG;AAC1C;;;;;;;;;;;;;;;AAgBA,SAAgB,QAAQ,GAAsC;CAC1D,MAAM,MAAM,KAAK,GAAG,MAAM;CAC1B,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU,OAAO,KAAA;CAE5C,MAAM,SAAS;CACf,MAAM,MAAM,OAAO,OAAO,QAAQ,WAAW,OAAO,MAAM,KAAA;CAC1D,IAAI,QAAQ,KAAA,GAAW,OAAO,KAAA;CAE9B,MAAM,QAAQ,MAAM,QAAQ,OAAO,KAAK,IAClC,OAAO,MAAM,QAAQ,SAAyB,OAAO,SAAS,QAAQ,IACtE,CAAC;CAEP,OAAO;EAAE,GAAG;EACR;EACA;CAAM;AACd;;AAGA,SAAgB,UAAU,GAAgC;CACtD,OAAO,QAAQ,CAAC,CAAC,EAAE;AACvB;;AAGA,SAAgB,SAAS,GAAsB;CAC3C,OAAO,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC;AACjC;;;;;;;AAQA,SAAgB,QAAQ,GAAY,GAAG,OAA0B;CAC7D,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,MAAM,OAAO,IAAI,IAAI,SAAS,CAAC,CAAC;CAChC,OAAO,MAAM,MAAK,SAAQ,KAAK,IAAI,IAAI,CAAC;AAC5C;;AAGA,SAAgB,QAAQ,GAAqB;CACzC,OAAO,eAAe,SAAS,CAAC,CAAC;AACrC;;AA2BA,SAAgB,gBAAgB,GAAqB;CACjD,OAAO,QAAQ,CAAC,MAAM,KAAA;AAC1B;;;;;;;;;;;;;;;AAgBA,SAAgB,UAAU,GAAoC;CAC1D,OAAO,KAAK,GAAG,QAAQ;AAC3B;;;;;;;;;AAUA,SAAgB,cAAc,GAAwB;CAClD,MAAM,SAAS,UAAU,CAAC;CAC1B,IAAI,CAAC,QACD,MAAM,IAAI,MACN,6UAKJ;CAEJ,OAAO;AACX;;;;;;;;AASA,SAAgB,UAAU,GAAsC;CAC5D,OAAO,KAAK,GAAG,QAAQ;AAC3B;;;;;;;;AASA,SAAgB,aAAa,GAAgC;CACzD,OAAO,KAAK,GAAG,WAAW;AAC9B;;;;;;;;;;;AAYA,SAAgB,iBAAiB,GAAqB;CAClD,OAAO,KAAK,GAAG,MAAM,MAAM,KAAA,KAAa,KAAK,GAAG,QAAQ,MAAM,KAAA;AAClE;;;;;;;;;;ACzLA,SAAS,qBAAmE;CACxE,OAAO,EACH,OAAO;EACH,SACI;EAIJ,MAAM;CACV,EACJ;AACJ;;;;;;;;;;;;;;;;;AAkBA,IAAa,cAA0C,OAAO,GAAG,SAAS;CACtE,IAAI,QAAQ,CAAC,GAAG,OAAO,KAAK;CAC5B,IAAI,CAAC,iBAAiB,CAAC,GAAG,OAAO,EAAE,KAAK,mBAAmB,GAAG,GAAG;CAEjE,OAAO,EAAE,KAAK,EACV,OAAO;EACH,SAAS;EACT,MAAM;CACV,EACJ,GAAG,GAAG;AACV;;;;;;;;;;;;;;AAiDA,SAAgB,YAAY,GAAG,OAA6C;CACxE,IAAI,MAAM,WAAW,GACjB,MAAM,IAAI,MACN,yHAEJ;CAGJ,MAAM,UAAU,IAAI,IAAI,KAAK;CAC7B,OAAO,OAAO,GAAG,SAAS;EAEtB,IAAI,CADS,QAAQ,CAChB,GAAM;GACP,IAAI,CAAC,iBAAiB,CAAC,GAAG,OAAO,EAAE,KAAK,mBAAmB,GAAG,GAAG;GACjE,OAAO,EAAE,KAAK,EACV,OAAO;IACH,SAAS;IACT,MAAM;GACV,EACJ,GAAG,GAAG;EACV;EAEA,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAK,SAAQ,QAAQ,IAAI,IAAI,CAAC,GAC3C,OAAO,EAAE,KAAK,EACV,OAAO;GACH,SAAS,+CAA+C,MAAM,KAAK,IAAI;GACvE,MAAM;EACV,EACJ,GAAG,GAAG;EAGV,OAAO,KAAK;CAChB;AACJ;;;;;AC3JA,SAAS,YAAY,SAAyB;CAC1C,OAAO,QAAQ,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC,QAAQ,iBAAiB,EAAE;AAC/E;;;;;;;;;;;;;;;;;AAkBA,SAAgB,qBACZ,WACA,WAAqB,CAAC,GAMtB,YAAY,cACC;CACb,MAAM,SAAS,IAAI,KAAc;CACjC,MAAM,UAAU,SAAS;CAUzB,OAAO,IAAI,KAAK,cAAc,MAAM;EAChC,OAAO,EAAE,KAAK;GACV,WAAW,UAAU,KAAK,QAAQ;IAC9B,MAAM,GAAG;IACT,UAAU,GAAG,UAAU,GAAG,GAAG;GACjC,EAAE;GACF,GAAI,UAAU,KAAK;IACf;IACA,MAAM,GAAG,QAAQ;GACrB;EACJ,CAAC;CACL,CAAC;CAED,KAAK,MAAM,MAAM,WACb,OAAO,MAAM,IAAI,GAAG,QAAQ,GAAG,GAAG;CAgBtC,MAAM,UAAU,IAAI,IAAI,UAAU,KAAI,OAAM,GAAG,IAAI,CAAC;CACpD,MAAM,eAAe,IAAI,IAAI,SAAS,KAAI,MAAK,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;CAEjF,OAAO,IAAI,eAAe,MAAa;EACnC,MAAM,aAAa,EAAE,IAAI,MAAM,MAAM,KAAK,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;EACvE,MAAM,OAAO,UAAU,MAAM;EAC7B,MAAM,OAAO,UAAU,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;EACxC,MAAM,aAAa,QAAQ,EAAE,IAAI,MAAM,CAAC;EAExC,MAAM,UAAU,YAA2B;GACvC,MAAM,IAAI,SAAS,KAAK,sBAAsB,SAAS,EAAE,UAAU,KAAK,GAAG,IAAI;EACnF;EAEA,IAAI,CAAC,MACD,OAAO,6CAA6C,UAAU,aAAa;EAE/E,IAAI,QAAQ,IAAI,IAAI,GAChB,OACI,iBAAiB,KAAK,0BAA0B,EAAE,IAAI,OAAO,cAAc,KAAK,8EAEpF;EAEJ,MAAM,OAAO,aAAa,IAAI,IAAI;EAClC,IAAI,MACA,OACI,iBAAiB,KAAK,oBAAoB,KAAK,uDAEnD;EAEJ,OAAO,OACH,sBAAsB,KAAK,uBAC1B,aACM,QAAQ,OAAO,IACZ,yBAAyB,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,KACxD,uCACJ,OACL,UAAU,KAAK,aACV,IAAI,QAAQ,6EACZ,GACV;CACJ,CAAC;CAED,OAAO;AACX"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"jobs-DqYNfquG.js","names":[],"sources":["../src/db/ambient-transaction.ts","../src/jobs/job-queue.ts","../src/jobs/job-store.ts","../src/jobs/index.ts"],"sourcesContent":["/**\n * The write transaction the current code is running inside, when there is one.\n *\n * A collection callback runs inside the transaction that carries its write.\n * Work it hands off — a job, a queue message, a topic event, a webhook\n * delivery — has to share that write's fate: queued when it commits, never\n * queued when it rolls back, and invisible to a worker until the commit. The\n * code that queues it lives in this package and knows no driver; the\n * transaction belongs to the driver. So the driver publishes the transaction it\n * opened for a write, and the queueing code asks for it here.\n *\n * Until this existed the job store wrote through the default driver's own\n * connection, in autocommit, whatever called it. A job enqueued from an\n * `afterSave` that then threw stayed queued for a row that was never written,\n * and a worker could claim it before the row it was about committed.\n *\n * The resolver lives on `globalThis` under a registered symbol, like the\n * `rebase` singleton: a managed runtime can load two copies of this package,\n * and the copy a driver registers with must be the one the job store reads.\n * No Node imports — the store is reachable from code that has to stay portable.\n *\n * **The symbol's key is a contract between packages, and frozen.** The\n * Postgres driver writes the slot itself rather than importing a setter from\n * here: the managed runtime supplies this package from the image while a\n * bundle brings its own driver, and a driver importing a function an older\n * image does not export would fail to link and take the boot down. Written\n * through the slot, any pairing of versions degrades to the old behaviour —\n * jobs committing on their own — instead.\n */\nexport interface AmbientTransaction {\n /**\n * Run one statement on the transaction, `$1…` placeholders as\n * `executeSql` takes them. It runs as the transaction's current role — on\n * Postgres the request's restricted one — so a statement that needs more\n * goes through a function granted to that role.\n */\n exec(sqlText: string, params?: unknown[]): Promise<Record<string, unknown>[]>;\n /** Run `fn` after the transaction commits. Never runs if it rolls back. */\n afterCommit(fn: () => void): void;\n}\n\n/** Frozen — see above. `server-postgres/src/services/write-transaction-scope.ts` writes it. */\nconst RESOLVER_SLOT = Symbol.for(\"rebase.server.ambientTransactionResolver\");\n\ntype ResolverSlot = { [RESOLVER_SLOT]?: () => AmbientTransaction | undefined };\n\n/**\n * Register how to find the write transaction the caller is inside; pass\n * `undefined` to unregister. For tests and for drivers in this repository's\n * lockstep — a driver shipped separately writes the slot, as above.\n */\nexport function setAmbientTransactionResolver(resolve: (() => AmbientTransaction | undefined) | undefined): void {\n (globalThis as ResolverSlot)[RESOLVER_SLOT] = resolve;\n}\n\n/** The write transaction the caller is inside, or `undefined` outside one. */\nexport function currentAmbientTransaction(): AmbientTransaction | undefined {\n return (globalThis as ResolverSlot)[RESOLVER_SLOT]?.();\n}\n","import { randomUUID } from \"crypto\";\nimport { logger } from \"../utils/logger.js\";\nimport type { JobClaim, JobStore } from \"./job-store.js\";\nimport type { EnqueueOptions, JobHandler, JobQueueClient, JobQueueOptions, JobRecord } from \"./types.js\";\n\n/**\n * The worker: claim, run, record, repeat.\n *\n * Everything difficult about running jobs concurrently is in `job-store.ts`,\n * where one `UPDATE … FOR UPDATE SKIP LOCKED` does the arbitration. What is\n * left here is a loop and the decisions around a handler that throws.\n */\n\nconst DEFAULT_CONCURRENCY = 5;\nconst DEFAULT_POLL_INTERVAL_MS = 2_000;\nconst DEFAULT_VISIBILITY_TIMEOUT_MS = 5 * 60_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\n\n/** 1s, 5s, 25s, 125s … capped at an hour. */\nexport function defaultBackoff(attempt: number): number {\n return Math.min(1_000 * Math.pow(5, Math.max(0, attempt - 1)), 60 * 60_000);\n}\n\n/**\n * How often the reaper runs, relative to the visibility timeout.\n *\n * A quarter of it, so a stranded job waits at most 1.25× the timeout rather\n * than 2× — and so the sweep is not itself a per-poll query against a table\n * whose interesting rows are, almost always, none.\n */\nconst REAP_INTERVAL_FACTOR = 0.25;\n\n/**\n * Thrown by a handler for a failure no retry can change — a destination that\n * is refused, a receiver that redirects. The job is dead-lettered on this\n * attempt, with the message as its `last_error`, instead of spending its\n * remaining attempts proving the same thing. Returning normally is not the\n * alternative: that records the job as succeeded.\n */\nexport class PermanentJobError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"PermanentJobError\";\n }\n}\n\nexport interface JobQueue extends JobQueueClient {\n start(): void;\n /**\n * Stop claiming, and wait for the jobs in flight — for at most `timeoutMs`\n * when given. A job still running when the budget runs out keeps its claim\n * and is recovered by the visibility timeout; waiting on it without a bound\n * would let one handler that never settles hold the whole shutdown.\n */\n stop(timeoutMs?: number): Promise<void>;\n /**\n * Claim what fits in the free slots, run it, and resolve with how many jobs\n * ran. For tests and for `/jobs/drain`; not meant to run beside `start()`.\n */\n runOnce(): Promise<number>;\n /** Registered after construction — how `tasks` from config and internal producers meet. */\n register<P = unknown>(task: string, handler: JobHandler<P>): void;\n isRunning(): boolean;\n}\n\nexport function createJobQueue(store: JobStore, options: JobQueueOptions = {}): JobQueue {\n const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const visibilityTimeoutMs = options.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS;\n const defaultMaxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const backoff = options.backoff ?? defaultBackoff;\n\n const handlers = new Map<string, JobHandler<never>>();\n for (const [task, handler] of Object.entries(options.tasks ?? {})) {\n handlers.set(task, handler);\n }\n\n // Identifies this process in `locked_by`: \"which pod had it when it\n // stopped\" is the first question anyone asks of a stuck job, and with the\n // attempt it is the claim an outcome is written under (see `JobClaim`).\n const workerId = `${process.pid}-${randomUUID().slice(0, 8)}`;\n\n let timer: NodeJS.Timeout | null = null;\n let reapTimer: NodeJS.Timeout | null = null;\n let running = false;\n /**\n * Every job this worker is running, tracked one by one rather than per\n * claimed batch: a batch would let its slowest job decide when the next\n * claim happens, so one handler that never settled would idle every other\n * slot — and hold `stop()` with it.\n */\n const active = new Set<Promise<void>>();\n /** The claim in progress, so `stop()` also waits for jobs it is about to hand out. */\n let claiming: Promise<unknown> | null = null;\n /** The last claim filled every free slot, so more work is probably waiting. */\n let backlog = false;\n let reaping = false;\n let draining: Promise<void> | null = null;\n\n /**\n * Renew `claim` while its handler runs, at a third of the visibility\n * timeout, so the reaper only ever takes a job from a worker that stopped\n * renewing — a dead one — and never from one that is merely slow. Returns\n * the stop.\n */\n function keepClaim(job: JobRecord, claim: JobClaim): () => void {\n if (!store.heartbeat) return () => undefined;\n const renew = store.heartbeat.bind(store);\n const interval = setInterval(() => {\n renew(job.id, claim).then((held) => {\n if (held) return;\n clearInterval(interval);\n logger.warn(\n `[jobs] \"${job.task}\" lost its claim on attempt ${claim.attempt} while running; ` +\n \"its outcome will not be recorded\", { jobId: job.id }\n );\n }).catch((error: unknown) => {\n logger.warn(`[jobs] Could not renew the claim on \"${job.task}\"`, { jobId: job.id, error });\n });\n }, Math.max(1, Math.floor(visibilityTimeoutMs / 3)));\n interval.unref?.();\n return () => clearInterval(interval);\n }\n\n async function runJob(job: JobRecord): Promise<void> {\n const handler = handlers.get(job.task);\n const claim: JobClaim = { workerId, attempt: job.attempts };\n\n if (!handler) {\n // Only reachable through a store that ignores the `tasks` filter on\n // `claim` — the one that ships never hands this worker a task it\n // has no handler for. Give the row back for a peer that has one.\n // The attempt the claim spent is not undone, so a task nobody in\n // the fleet implements dead-letters with an error naming it rather\n // than cycling through such a store forever.\n logger.warn(`[jobs] No handler registered for task \"${job.task}\" — returning the job to the queue`);\n await store.fail(\n job.id,\n `No handler registered for task \"${job.task}\"`,\n job.attempts < job.maxAttempts ? new Date(Date.now() + backoff(job.attempts)) : null,\n claim\n );\n return;\n }\n\n const releaseClaim = keepClaim(job, claim);\n try {\n try {\n await handler({\n id: job.id,\n task: job.task,\n payload: job.payload as never,\n attempt: job.attempts,\n maxAttempts: job.maxAttempts\n } as never);\n } finally {\n releaseClaim();\n }\n await store.complete(job.id, claim);\n } catch (error) {\n const message = error instanceof Error ? (error.stack ?? error.message) : String(error);\n const permanent = error instanceof PermanentJobError;\n const willRetry = !permanent && job.attempts < job.maxAttempts;\n\n // Truncated, because `last_error` holds a stack and a queue that\n // accumulates megabytes of them is its own outage.\n await store.fail(job.id, message.slice(0, 4_000), willRetry ? new Date(Date.now() + backoff(job.attempts)) : null, claim);\n\n if (willRetry) {\n logger.warn(`[jobs] \"${job.task}\" failed on attempt ${job.attempts}/${job.maxAttempts}; retrying`, { jobId: job.id });\n } else if (permanent) {\n logger.error(`[jobs] \"${job.task}\" failed permanently on attempt ${job.attempts}; not retrying`, { jobId: job.id, error: message });\n } else {\n // The last attempt is an error, not a warning: nothing else will\n // touch this job, and if nobody looks at the table it is simply\n // lost work.\n logger.error(`[jobs] \"${job.task}\" failed permanently after ${job.attempts} attempts`, { jobId: job.id, error: message });\n }\n }\n }\n\n /**\n * Return jobs stranded by a dead worker. On its own timer, not inside the\n * poll: a poll can be skipped for as long as every slot is busy, and that\n * is exactly when a stranded job most needs a peer to take it back.\n */\n async function reap(): Promise<void> {\n if (reaping) return;\n reaping = true;\n try {\n await store.reapExpired(visibilityTimeoutMs);\n } catch (error) {\n logger.error(\"[jobs] Failed to reclaim expired jobs\", { error });\n } finally {\n reaping = false;\n }\n }\n\n /** Claim as many jobs as there are free slots and start each one on its own. */\n async function claimAndStart(): Promise<Promise<void>[]> {\n const free = concurrency - active.size;\n if (free <= 0) return [];\n\n // Only the tasks this worker can run. During a rolling deploy an\n // instance on older code shares the table with jobs whose task only\n // the newer code implements, and a claim spends an attempt: claiming\n // them here would dead-letter them before an updated peer got to them.\n // A handler registered after `start()` joins the next claim.\n if (handlers.size === 0) return [];\n const jobs = await store.claim(free, workerId, [...handlers.keys()]);\n backlog = jobs.length >= free;\n\n return jobs.map((job) => {\n const run: Promise<void> = runJob(job)\n .catch((error) => {\n // `runJob` handles a throwing handler; this is a store\n // write failing after it. The job keeps its claim, so the\n // visibility timeout recovers it.\n logger.error(`[jobs] Could not record the outcome of \"${job.task}\"`, { jobId: job.id, error });\n })\n .finally(() => {\n active.delete(run);\n // A slot just came free and there is work waiting: take the\n // next job now, not a poll interval from now.\n if (backlog) schedule(0);\n });\n active.add(run);\n return run;\n });\n }\n\n function schedule(delayMs: number): void {\n if (!running) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n void tick();\n }, delayMs);\n // Never hold the process open. A queue with nothing to do should not be\n // the reason `rebase dev` will not exit.\n timer.unref?.();\n }\n\n async function tick(): Promise<void> {\n // A claim already in progress reschedules when it finishes.\n if (!running || claiming) return;\n const work = claimAndStart().catch((error) => {\n logger.error(\"[jobs] Poll failed\", { error });\n backlog = false;\n });\n claiming = work;\n await work;\n claiming = null;\n\n // A full claim with a slot still free — a job already finished — means\n // go straight back. Otherwise sleep; a job finishing during a backlog\n // wakes the loop early.\n schedule(backlog && active.size < concurrency ? 0 : pollIntervalMs);\n }\n\n async function drain(): Promise<void> {\n await claiming;\n await Promise.allSettled([...active]);\n }\n\n return {\n start(): void {\n if (running) return;\n running = true;\n logger.info(`[jobs] Worker started (concurrency ${concurrency}, poll ${pollIntervalMs}ms)`);\n void reap();\n reapTimer = setInterval(() => { void reap(); }, visibilityTimeoutMs * REAP_INTERVAL_FACTOR);\n reapTimer.unref?.();\n schedule(0);\n },\n\n async stop(timeoutMs?: number): Promise<void> {\n running = false;\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (reapTimer) {\n clearInterval(reapTimer);\n reapTimer = null;\n }\n // Jobs in flight keep their claim until they finish or the\n // visibility timeout expires, so waiting here is what turns a\n // graceful shutdown into \"no job runs twice\".\n draining ??= drain().finally(() => { draining = null; });\n if (timeoutMs === undefined) {\n await draining;\n return;\n }\n let budget: NodeJS.Timeout | undefined;\n const finished = await Promise.race([\n draining.then(() => true),\n new Promise<false>((resolve) => {\n budget = setTimeout(() => resolve(false), timeoutMs);\n budget.unref?.();\n })\n ]);\n clearTimeout(budget);\n if (!finished) {\n logger.warn(\n `[jobs] ${active.size} job(s) still running after ${timeoutMs}ms; stopping without them. ` +\n \"They keep their claim and are retried once the visibility timeout reclaims it.\"\n );\n }\n },\n\n async runOnce(): Promise<number> {\n const started = await claimAndStart();\n await Promise.allSettled(started);\n return started.length;\n },\n\n register<P = unknown>(task: string, handler: JobHandler<P>): void {\n if (handlers.has(task)) {\n logger.warn(`[jobs] Task \"${task}\" was already registered; the later handler wins`);\n }\n handlers.set(task, handler as JobHandler<never>);\n },\n\n isRunning(): boolean {\n return running;\n },\n\n async enqueue<P = unknown>(task: string, payload?: P, enqueueOptions: EnqueueOptions = {}): Promise<string | null> {\n return store.insert({\n task,\n payload: payload ?? null,\n runAt: new Date(Date.now() + (enqueueOptions.delayMs ?? 0)),\n maxAttempts: enqueueOptions.maxAttempts ?? defaultMaxAttempts,\n idempotencyKey: enqueueOptions.idempotencyKey\n });\n }\n };\n}\n","import type { DataDriver } from \"@rebasepro/types\";\nimport { isSQLAdmin } from \"@rebasepro/types\";\nimport { REBASE_USER_ROLE, revokeInternalTableSql, sqlRows, firstSqlRow } from \"@rebasepro/common\";\nimport { logger } from \"../utils/logger.js\";\nimport { createDdlBootstrapper, hasInCauseChain, type SqlExec } from \"../boot/ddl-bootstrap.js\";\nimport { currentAmbientTransaction } from \"../db/ambient-transaction.js\";\nimport type { JobRecord } from \"./types.js\";\n\n/**\n * The queue's storage, as SQL against `rebase.jobs`.\n *\n * Split from the worker for the same reason `cron-store` is split from\n * `cron-scheduler`: the interesting parts here are three statements that have\n * to be exactly right under concurrency, and they are much easier to reason\n * about — and to test — away from a polling loop.\n */\n\nconst TABLE = \"rebase.jobs\";\n\n/** How long finished jobs are kept before the boot sweep removes them. */\nconst SUCCEEDED_RETENTION_DAYS = 3;\n/**\n * Failures outlive successes by a lot. A dead-lettered job is evidence, and the\n * person who needs it is usually looking on Monday for something that happened\n * on Friday night.\n */\nconst FAILED_RETENTION_DAYS = 30;\n\n/** A row as Postgres returns it. */\ninterface JobRow {\n id: string;\n task: string;\n payload: unknown;\n status: string;\n run_at: string;\n attempts: number;\n max_attempts: number;\n last_error: string | null;\n created_at: string;\n updated_at: string;\n}\n\nfunction toRecord(row: JobRow): JobRecord {\n return {\n id: row.id,\n task: row.task,\n payload: row.payload,\n status: row.status as JobRecord[\"status\"],\n runAt: new Date(row.run_at).toISOString(),\n attempts: Number(row.attempts),\n maxAttempts: Number(row.max_attempts),\n lastError: row.last_error,\n createdAt: new Date(row.created_at).toISOString(),\n updatedAt: new Date(row.updated_at).toISOString()\n };\n}\n\n/** Same rule as `cron-store`: match the SQLSTATE, never the message. */\nfunction isUniqueViolation(err: unknown): boolean {\n return hasInCauseChain(err, (e) => e.code === \"23505\");\n}\n\n/**\n * How a job gets into `rebase.jobs` from inside somebody else's transaction.\n *\n * An enqueue from a collection callback has to ride the write's transaction —\n * committed with it, discarded with it, invisible to workers until then — and\n * that transaction runs as the request's restricted role, which has no access\n * to `rebase.jobs` at all (see the revoke in `ensureTable`). This function is\n * the one door: `SECURITY DEFINER`, so the insert runs with the owner's rights\n * for that one statement and the transaction's role never changes. Switching\n * the role for the insert instead would elevate the whole connection for as\n * long as the insert took, and a callback's un-awaited statement would run\n * in that window as the owner.\n *\n * The door is narrow on purpose. It takes the five columns an enqueue sets and\n * nothing else, so it cannot claim, complete or rewrite a job — and only the\n * request role is granted it, not PUBLIC. That role runs statements the server\n * writes; nobody outside gets SQL as it.\n *\n * `ON CONFLICT DO NOTHING` rather than a caught unique violation: an error\n * inside a transaction aborts the whole transaction, so an idempotency clash\n * would have failed the customer's write. A clash returns no row, hence NULL.\n */\nconst ENQUEUE_FUNCTION = \"rebase.enqueue_job\";\nconst ENQUEUE_SIGNATURE = `${ENQUEUE_FUNCTION}(text, jsonb, timestamptz, integer, text)`;\n\n/**\n * Which claim an outcome belongs to: the worker holding the job, and the\n * attempt that claim spent.\n *\n * The visibility timeout hands a job to a second worker when the first stops\n * renewing its claim, and the first may still be running — slow, not dead. Its\n * outcome is then about an attempt the table has moved past, and writing it\n * would mark a running job `succeeded`, or send a finished one back to\n * `pending` for another run. Both writes therefore apply only while the claim\n * they name still holds the row. The attempt is part of the identity because a\n * worker's own reaper can hand a job back to the same worker in another slot.\n */\nexport interface JobClaim {\n workerId: string;\n attempt: number;\n}\n\nexport interface JobStore {\n ensureTable(): Promise<void>;\n /** Returns the new job's id, or `null` if an idempotency key matched unfinished work. */\n insert(job: {\n task: string;\n payload: unknown;\n runAt: Date;\n maxAttempts: number;\n idempotencyKey?: string;\n }): Promise<string | null>;\n /**\n * Atomically take up to `limit` runnable jobs for this worker — only jobs\n * whose task is in `tasks`, when it is given. A worker never claims work it\n * cannot run: a claim spends an attempt, so an instance running older code\n * during a rollout would otherwise dead-letter the newer code's jobs.\n */\n claim(limit: number, workerId: string, tasks?: readonly string[]): Promise<JobRecord[]>;\n /** Mark the job `succeeded` — a no-op once `claim` no longer holds it. */\n complete(id: string, claim: JobClaim): Promise<void>;\n /**\n * Back to `pending` with a later `runAt`, or `failed` when out of attempts —\n * a no-op once `claim` no longer holds it.\n */\n fail(id: string, error: string, retryAt: Date | null, claim: JobClaim): Promise<void>;\n /**\n * Renew `claim` on a job whose handler is still running, so the visibility\n * timeout only ever reclaims a job from a worker that stopped renewing.\n * Resolves with whether the claim still held. A store without it leaves a\n * handler that outlives the timeout to be run a second time beside itself.\n */\n heartbeat?(id: string, claim: JobClaim): Promise<boolean>;\n /** Return jobs stranded by a worker that died holding them. Resolves with how many. */\n reapExpired(visibilityTimeoutMs: number): Promise<number>;\n fetch(id: string): Promise<JobRecord | null>;\n}\n\n/** The rows a claim's writes may touch: `$1` the job, `$2` the worker, `$3` the attempt. */\nconst HELD_BY_CLAIM = \"id = $1 AND status = 'running' AND locked_by = $2 AND attempts = $3\";\n\nfunction warnStaleOutcome(id: string, claim: JobClaim, outcome: \"success\" | \"failure\"): void {\n logger.warn(\n `[jobs] Ignored the ${outcome} of attempt ${claim.attempt} of job ${id}: the job was reclaimed ` +\n \"from this worker while it ran, so that attempt's outcome is no longer the job's.\"\n );\n}\n\nexport function createJobStore(driver: DataDriver): JobStore | undefined {\n const admin = driver.admin;\n if (!isSQLAdmin(admin)) {\n logger.warn(\n \"⚠️ [jobs] DataDriver does not support SQL admin — the durable job queue is unavailable. \" +\n \"Work that would have been queued runs inline instead.\"\n );\n return undefined;\n }\n\n // Two shapes of the same call: the bootstrapper's `SqlExec` takes an\n // options object, while everything below reads better with a positional\n // parameter array.\n const execRaw: SqlExec = (sqlText, options) =>\n admin.executeSql(sqlText, options?.params ? { params: options.params } : undefined);\n const exec = (sqlText: string, params?: unknown[]) =>\n execRaw(sqlText, params ? { params } : undefined);\n\n const ddl = createDdlBootstrapper(execRaw, \"jobs\");\n\n /**\n * Whether {@link ENQUEUE_FUNCTION} exists, so an enqueue inside a write can\n * ride its transaction. Decided at boot rather than assumed: a database\n * that refused the function would otherwise fail every write whose callback\n * enqueues, where falling back to the old behaviour only loses atomicity.\n */\n let enqueueInTransaction = false;\n\n return {\n async ensureTable(): Promise<void> {\n await ddl.ensureObject(\"Creating schema rebase\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n\n await ddl.ensureObject(`Creating ${TABLE}`, `\n CREATE TABLE IF NOT EXISTS ${TABLE} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n task TEXT NOT NULL,\n payload JSONB,\n status TEXT NOT NULL DEFAULT 'pending',\n run_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n locked_at TIMESTAMPTZ,\n locked_by TEXT,\n idempotency_key TEXT,\n last_error TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\n )\n `);\n\n // The claim query's index. Partial, because the rows it has to find\n // fast are a shrinking minority of a table that also holds every\n // success and every dead letter.\n await ddl.ensureObject(\"Creating idx_jobs_runnable\", `\n CREATE INDEX IF NOT EXISTS idx_jobs_runnable\n ON ${TABLE}(run_at, created_at)\n WHERE status = 'pending'\n `);\n\n // Finds jobs stranded by a dead worker.\n await ddl.ensureObject(\"Creating idx_jobs_running\", `\n CREATE INDEX IF NOT EXISTS idx_jobs_running\n ON ${TABLE}(locked_at)\n WHERE status = 'running'\n `);\n\n // What makes `idempotencyKey` a guarantee rather than a\n // check-then-act race: two instances reacting to one event both\n // reach the INSERT, and the index decides. Partial on unfinished\n // work — see `EnqueueOptions.idempotencyKey` for why a key must be\n // reusable once its job is done.\n await ddl.ensureObject(\"Creating idx_jobs_idempotency\", `\n CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_idempotency\n ON ${TABLE}(idempotency_key)\n WHERE idempotency_key IS NOT NULL AND status IN ('pending', 'running')\n `);\n\n const ready = await ddl.isReadable(TABLE);\n\n if (ready) {\n await ddl.step(\"Job retention sweep\", async () => {\n await exec(\n `DELETE FROM ${TABLE}\n WHERE (status = 'succeeded' AND updated_at < now() - make_interval(days => $1))\n OR (status = 'failed' AND updated_at < now() - make_interval(days => $2))`,\n [SUCCEEDED_RETENTION_DAYS, FAILED_RETENTION_DAYS]\n );\n });\n\n // Payloads are arbitrary application data — a webhook body, a\n // user id, whatever was passed — and a writable queue lets any\n // signed-in user schedule work of their choosing under the\n // server's own authority. Same reasoning, and the same\n // unconditional re-application, as `cron_claims`.\n await ddl.step(\"Revoking end-user access to jobs\", () =>\n exec(revokeInternalTableSql(\"rebase\", \"jobs\")));\n\n await ddl.ensureObject(`Creating ${ENQUEUE_FUNCTION}`, `\n CREATE OR REPLACE FUNCTION ${ENQUEUE_FUNCTION}(\n p_task text, p_payload jsonb, p_run_at timestamptz,\n p_max_attempts integer, p_idempotency_key text\n ) RETURNS text\n LANGUAGE sql\n SECURITY DEFINER\n SET search_path = pg_catalog, pg_temp\n AS $fn$\n INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)\n VALUES (p_task, p_payload, p_run_at, p_max_attempts, p_idempotency_key)\n ON CONFLICT DO NOTHING\n RETURNING id\n $fn$\n `);\n // Re-applied every boot: a new function is executable by PUBLIC\n // until told otherwise, and the request role may be provisioned\n // after the function was first made.\n await ddl.step(`Granting ${ENQUEUE_FUNCTION} to the request role`, async () => {\n await exec(`REVOKE ALL ON FUNCTION ${ENQUEUE_SIGNATURE} FROM PUBLIC`);\n await exec(`\n DO $grant$ BEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}') THEN\n GRANT EXECUTE ON FUNCTION ${ENQUEUE_SIGNATURE} TO ${REBASE_USER_ROLE};\n END IF;\n END $grant$\n `);\n });\n enqueueInTransaction = firstSqlRow<{ present: boolean }>(await exec(\n `SELECT to_regprocedure('${ENQUEUE_SIGNATURE}') IS NOT NULL AS present`\n ))?.present === true;\n if (!enqueueInTransaction) {\n logger.warn(\n `⚠️ [jobs] ${ENQUEUE_FUNCTION} could not be created, so a job enqueued inside a write ` +\n \"commits on its own: it stays queued if the write rolls back.\"\n );\n }\n\n logger.info(\"✅ Job queue table ready\");\n } else {\n logger.error(\n `❌ [jobs] ${TABLE} is unavailable — nothing can be queued and nothing queued earlier ` +\n \"will run. Callers fall back to running the work inline.\"\n );\n }\n },\n\n async insert(job): Promise<string | null> {\n const params = [\n job.task,\n JSON.stringify(job.payload ?? null),\n job.runAt.toISOString(),\n job.maxAttempts,\n job.idempotencyKey ?? null\n ];\n // Inside a write: on its transaction, through the definer function\n // — see `ENQUEUE_FUNCTION`. Checked and used with no `await`\n // between, so the transaction cannot close in the gap.\n const ambient = enqueueInTransaction ? currentAmbientTransaction() : undefined;\n if (ambient) {\n const rows = await ambient.exec(\n `SELECT ${ENQUEUE_FUNCTION}($1::text, $2::jsonb, $3::timestamptz, $4::integer, $5::text) AS id`,\n params\n );\n return (rows?.[0]?.id as string | null | undefined) ?? null;\n }\n try {\n const rows = await exec(\n `INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)\n VALUES ($1, $2::jsonb, $3, $4, $5)\n RETURNING id`,\n params\n );\n return (rows?.[0]?.id as string) ?? null;\n } catch (error) {\n // The losing side of an idempotency race. Not an error: the\n // work the caller wanted is already queued, which is the\n // outcome they asked for.\n if (isUniqueViolation(error)) return null;\n throw error;\n }\n },\n\n async claim(limit: number, workerId: string, tasks?: readonly string[]): Promise<JobRecord[]> {\n if (tasks && tasks.length === 0) return [];\n // One placeholder per task rather than an array parameter: every\n // driver binds a string, and not every driver binds an array.\n const taskFilter = tasks\n ? `AND task IN (${tasks.map((_, i) => `$${i + 3}`).join(\", \")})`\n : \"\";\n // `FOR UPDATE SKIP LOCKED` is the whole design. The inner select\n // takes row locks on the jobs it picks and *skips* any a concurrent\n // worker already holds, so N workers polling the same table divide\n // the work instead of contending for the head of it — and no job is\n // ever handed to two of them.\n //\n // `attempts` is incremented here, on claim, rather than on failure.\n // A worker that is killed mid-job never reports anything, so\n // counting on failure would let a job that crashes the process be\n // retried forever, once per restart, taking the process down each\n // time.\n const rows = await exec(\n `UPDATE ${TABLE} SET\n status = 'running',\n attempts = attempts + 1,\n locked_at = now(),\n locked_by = $2,\n updated_at = now()\n WHERE id IN (\n SELECT id FROM ${TABLE}\n WHERE status = 'pending' AND run_at <= now() ${taskFilter}\n ORDER BY run_at, created_at\n LIMIT $1\n FOR UPDATE SKIP LOCKED\n )\n RETURNING *`,\n [limit, workerId, ...(tasks ?? [])]\n );\n return sqlRows<JobRow>(rows).map(toRecord);\n },\n\n async complete(id: string, claim: JobClaim): Promise<void> {\n const rows = await exec(\n `UPDATE ${TABLE} SET status = 'succeeded', locked_at = NULL, locked_by = NULL,\n last_error = NULL, updated_at = now()\n WHERE ${HELD_BY_CLAIM}\n RETURNING id`,\n [id, claim.workerId, claim.attempt]\n );\n if (sqlRows(rows).length === 0) warnStaleOutcome(id, claim, \"success\");\n },\n\n async fail(id: string, error: string, retryAt: Date | null, claim: JobClaim): Promise<void> {\n const rows = retryAt\n ? await exec(\n `UPDATE ${TABLE} SET status = 'pending', run_at = $4, locked_at = NULL,\n locked_by = NULL, last_error = $5, updated_at = now()\n WHERE ${HELD_BY_CLAIM}\n RETURNING id`,\n [id, claim.workerId, claim.attempt, retryAt.toISOString(), error]\n )\n : await exec(\n `UPDATE ${TABLE} SET status = 'failed', locked_at = NULL, locked_by = NULL,\n last_error = $4, updated_at = now()\n WHERE ${HELD_BY_CLAIM}\n RETURNING id`,\n [id, claim.workerId, claim.attempt, error]\n );\n if (sqlRows(rows).length === 0) warnStaleOutcome(id, claim, \"failure\");\n },\n\n async heartbeat(id: string, claim: JobClaim): Promise<boolean> {\n const rows = await exec(\n `UPDATE ${TABLE} SET locked_at = now()\n WHERE ${HELD_BY_CLAIM}\n RETURNING id`,\n [id, claim.workerId, claim.attempt]\n );\n return sqlRows(rows).length > 0;\n },\n\n async reapExpired(visibilityTimeoutMs: number): Promise<number> {\n // A worker killed while holding a job cannot release it, so nothing\n // but a timeout will ever free the row. Jobs that still have\n // attempts left go back to `pending`; the rest are dead-lettered\n // with an error that says what happened, because \"attempts: 3,\n // lastError: null\" is otherwise a genuinely baffling row to find.\n const seconds = Math.max(1, Math.round(visibilityTimeoutMs / 1000));\n\n const revived = await exec(\n `UPDATE ${TABLE} SET status = 'pending', locked_at = NULL, locked_by = NULL,\n last_error = 'Worker stopped responding; the job was reclaimed', updated_at = now()\n WHERE status = 'running'\n AND locked_at < now() - make_interval(secs => $1)\n AND attempts < max_attempts\n RETURNING id`,\n [seconds]\n );\n\n const buried = await exec(\n `UPDATE ${TABLE} SET status = 'failed', locked_at = NULL, locked_by = NULL,\n last_error = 'Worker stopped responding on the final attempt', updated_at = now()\n WHERE status = 'running'\n AND locked_at < now() - make_interval(secs => $1)\n AND attempts >= max_attempts\n RETURNING id`,\n [seconds]\n );\n\n const count = (revived?.length ?? 0) + (buried?.length ?? 0);\n if (count > 0) {\n logger.warn(\n `[jobs] Reclaimed ${count} job(s) from a worker that stopped responding ` +\n `(${revived?.length ?? 0} retryable, ${buried?.length ?? 0} dead-lettered)`\n );\n }\n return count;\n },\n\n async fetch(id: string): Promise<JobRecord | null> {\n const rows = await exec(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]);\n const row = firstSqlRow<JobRow>(rows);\n return row ? toRecord(row) : null;\n }\n };\n}\n","export { createJobStore } from \"./job-store\";\nexport type { JobClaim, JobStore } from \"./job-store\";\nexport { createJobQueue, defaultBackoff, PermanentJobError } from \"./job-queue\";\nexport type { JobQueue } from \"./job-queue\";\nexport type {\n EnqueueOptions,\n JobContext,\n JobHandler,\n JobQueueClient,\n JobQueueOptions,\n JobRecord,\n JobStatus\n} from \"./types\";\n"],"mappings":"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"}
|