@rebasepro/server 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/{GCSStorageController-C7ARrDAp.js → GCSStorageController-40FyFeSx.js} +8 -4
  2. package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
  3. package/dist/{S3StorageController-qevJ0pVE.js → S3StorageController-Bl5shp94.js} +8 -4
  4. package/dist/S3StorageController-Bl5shp94.js.map +1 -0
  5. package/dist/access-BFAUvsSs.js +1897 -0
  6. package/dist/access-BFAUvsSs.js.map +1 -0
  7. package/dist/api/branch-routes.d.ts +25 -0
  8. package/dist/api/closed-database.d.ts +25 -0
  9. package/dist/api/live-schema-routes.d.ts +10 -0
  10. package/dist/api/rest/api-generator.d.ts +9 -1
  11. package/dist/api/rest/idempotency.d.ts +15 -1
  12. package/dist/api/root-error-handler.d.ts +6 -5
  13. package/dist/api/types.d.ts +12 -0
  14. package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
  15. package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
  16. package/dist/auth/access.d.ts +10 -0
  17. package/dist/auth/adapter-middleware.d.ts +2 -1
  18. package/dist/auth/adapter-service-key.d.ts +24 -0
  19. package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
  20. package/dist/auth/api-keys/api-key-store.d.ts +8 -1
  21. package/dist/auth/api-keys/key-grant.d.ts +18 -1
  22. package/dist/auth/expired-token-sweep.d.ts +5 -0
  23. package/dist/auth/impersonation.d.ts +3 -1
  24. package/dist/auth/jwt.d.ts +8 -2
  25. package/dist/auth/oauth-signin-policy.d.ts +21 -35
  26. package/dist/auth/rls-scope.d.ts +17 -7
  27. package/dist/auth/rls-simulator-routes.d.ts +51 -0
  28. package/dist/auth/simulation-read-only.d.ts +28 -0
  29. package/dist/auth/simulation.d.ts +141 -0
  30. package/dist/auth/sql-rate-limit-store.d.ts +15 -1
  31. package/dist/{auth-CteGWxsL.js → auth-izI7AEzC.js} +391 -1893
  32. package/dist/auth-izI7AEzC.js.map +1 -0
  33. package/dist/auth_adapter-B7RUMk6e.js +89 -0
  34. package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
  35. package/dist/backend/boot-announcements.d.ts +13 -0
  36. package/dist/backend/create-backend.d.ts +10 -0
  37. package/dist/backend/options.d.ts +9 -4
  38. package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
  39. package/dist/backend-CaG_Jyso.js.map +1 -0
  40. package/dist/{backup-Dq0lyJ_D.js → backup-BVbHoKo8.js} +8 -8
  41. package/dist/{backup-Dq0lyJ_D.js.map → backup-BVbHoKo8.js.map} +1 -1
  42. package/dist/boot/boot.d.ts +2 -15
  43. package/dist/boot/ddl-bootstrap.d.ts +34 -0
  44. package/dist/boot/env.d.ts +6 -0
  45. package/dist/boot/fetch-bundle.d.ts +5 -1
  46. package/dist/boot/provision-only.d.ts +23 -0
  47. package/dist/boot/provision.d.ts +53 -2
  48. package/dist/boot/role.d.ts +2 -0
  49. package/dist/boot/schema-change-recorder.d.ts +45 -0
  50. package/dist/boot/security-headers.d.ts +22 -0
  51. package/dist/branch-routes-B2vvEx53.js +84 -0
  52. package/dist/branch-routes-B2vvEx53.js.map +1 -0
  53. package/dist/{contract-routes-DiFwiLg2.js → contract-routes-CU2A3JnF.js} +3 -3
  54. package/dist/{contract-routes-DiFwiLg2.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
  55. package/dist/cron/cron-store.d.ts +8 -1
  56. package/dist/{cron-loader-NHqRfKF0.js → cron-loader-mMhqbVsl.js} +3 -3
  57. package/dist/{cron-loader-NHqRfKF0.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
  58. package/dist/{cron-routes-Dhf82HzT.js → cron-routes-Cs33j_Kr.js} +5 -5
  59. package/dist/{cron-routes-Dhf82HzT.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
  60. package/dist/{cron-scheduler-D1DT0nw5.js → cron-scheduler-Be7CNghL.js} +3 -3
  61. package/dist/{cron-scheduler-D1DT0nw5.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
  62. package/dist/{cron-store-DEpVstUU.js → cron-store-DLn11M8H.js} +11 -10
  63. package/dist/cron-store-DLn11M8H.js.map +1 -0
  64. package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
  65. package/dist/data_driver-D0mg1Y_x.js.map +1 -0
  66. package/dist/{ddl-bootstrap-BNqi2NZe.js → ddl-bootstrap-CjRrn12l.js} +92 -3
  67. package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
  68. package/dist/deploy/pod-contract.d.ts +1 -1
  69. package/dist/email/dev-sink.d.ts +6 -0
  70. package/dist/email/index.d.ts +1 -1
  71. package/dist/{errors-_ytoGi8A.js → errors-OMWkKSH-.js} +66 -5
  72. package/dist/errors-OMWkKSH-.js.map +1 -0
  73. package/dist/{soft-delete-params-BAgpmBvp.js → field-access-DYsvjI_P.js} +27 -57
  74. package/dist/field-access-DYsvjI_P.js.map +1 -0
  75. package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
  76. package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
  77. package/dist/{function-loader-DbCSoNJj.js → function-loader-B8YzXEsQ.js} +22 -15
  78. package/dist/function-loader-B8YzXEsQ.js.map +1 -0
  79. package/dist/{function-routes-DNV4RUF0.js → function-routes-RxrCHNuM.js} +2 -2
  80. package/dist/{function-routes-DNV4RUF0.js.map → function-routes-RxrCHNuM.js.map} +1 -1
  81. package/dist/functions/hono-like.d.ts +12 -0
  82. package/dist/functions/index.js +37 -2
  83. package/dist/functions/index.js.map +1 -1
  84. package/dist/functions/request-timeout.d.ts +3 -3
  85. package/dist/history/history-routes.d.ts +6 -0
  86. package/dist/history/retention-sweep.d.ts +5 -0
  87. package/dist/{history-recorder-RZ1Ow4YY.js → history-recorder-Bsau7h4X.js} +7 -7
  88. package/dist/history-recorder-Bsau7h4X.js.map +1 -0
  89. package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
  90. package/dist/history-store-BQz5YCkW.js.map +1 -0
  91. package/dist/index.d.ts +3 -1
  92. package/dist/index.es.js +3003 -681
  93. package/dist/index.es.js.map +1 -1
  94. package/dist/init/process-safety.d.ts +4 -3
  95. package/dist/init/shutdown.d.ts +8 -0
  96. package/dist/init.d.ts +6 -1
  97. package/dist/jobs/job-store.d.ts +8 -1
  98. package/dist/{jobs-8G-_agld.js → jobs-Co6Ni5HP.js} +7 -7
  99. package/dist/jobs-Co6Ni5HP.js.map +1 -0
  100. package/dist/{jwt-YhciR93H.js → jwt-rDS0884_.js} +5 -4
  101. package/dist/{jwt-YhciR93H.js.map → jwt-rDS0884_.js.map} +1 -1
  102. package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
  103. package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
  104. package/dist/{logger-Bt32sHFw.js → logger-CW8pmTS6.js} +35 -3
  105. package/dist/logger-CW8pmTS6.js.map +1 -0
  106. package/dist/{logs-routes-CelTiS7C.js → logs-routes-CBt7mMIL.js} +3 -3
  107. package/dist/{logs-routes-CelTiS7C.js.map → logs-routes-CBt7mMIL.js.map} +1 -1
  108. package/dist/mcp/consent-page.d.ts +38 -1
  109. package/dist/mcp/mcp-routes.d.ts +9 -0
  110. package/dist/mcp/mcp-tools.d.ts +7 -1
  111. package/dist/mcp/oauth-store.d.ts +12 -5
  112. package/dist/metrics/history-recorder.d.ts +4 -1
  113. package/dist/metrics/history-store.d.ts +8 -0
  114. package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
  115. package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
  116. package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
  117. package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
  118. package/dist/{openapi-generator-BamY6-ga.js → openapi-generator-CyxEtYEX.js} +61 -22
  119. package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
  120. package/dist/{proxy-BEKbsil6.js → proxy-BYir8tWK.js} +5 -7
  121. package/dist/proxy-BYir8tWK.js.map +1 -0
  122. package/dist/{query-parser-CGohjyWI.js → query-parser-CDXWf2hH.js} +7 -6
  123. package/dist/{query-parser-CGohjyWI.js.map → query-parser-CDXWf2hH.js.map} +1 -1
  124. package/dist/{request-timeout-CRny5KD0.js → request-timeout-DVjo-c_8.js} +6 -6
  125. package/dist/request-timeout-DVjo-c_8.js.map +1 -0
  126. package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
  127. package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
  128. package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
  129. package/dist/{schema-editor-routes-CnNGCayU.js → schema-editor-routes-Dc3DU_jm.js} +4 -4
  130. package/dist/{schema-editor-routes-CnNGCayU.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
  131. package/dist/soft-delete-params-BRONCOA2.js +59 -0
  132. package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
  133. package/dist/{sql-export-routes-BgTJ7K3S.js → sql-export-routes-QycX8W8S.js} +40 -6
  134. package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
  135. package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
  136. package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
  137. package/dist/storage/GCSStorageController.d.ts +2 -0
  138. package/dist/storage/LocalStorageController.d.ts +8 -3
  139. package/dist/storage/S3StorageController.d.ts +2 -0
  140. package/dist/storage/maintenance-keys.d.ts +17 -0
  141. package/dist/storage/maintenance-routes.d.ts +28 -5
  142. package/dist/storage/maintenance.d.ts +136 -4
  143. package/dist/storage/orphan-scans.d.ts +120 -0
  144. package/dist/storage/types.d.ts +11 -0
  145. package/dist/strings.es-B7QLnHh2.js +96 -0
  146. package/dist/strings.es-B7QLnHh2.js.map +1 -0
  147. package/dist/{upload-validation-I9cCUBIW.js → upload-validation-Bgo626uY.js} +2 -2
  148. package/dist/upload-validation-Bgo626uY.js.map +1 -0
  149. package/dist/utils/dev-dependency-command.d.ts +15 -0
  150. package/dist/utils/dev-port.d.ts +8 -0
  151. package/dist/utils/logger.d.ts +24 -0
  152. package/package.json +5 -5
  153. package/dist/GCSStorageController-C7ARrDAp.js.map +0 -1
  154. package/dist/S3StorageController-qevJ0pVE.js.map +0 -1
  155. package/dist/auth-CteGWxsL.js.map +0 -1
  156. package/dist/auth_adapter-CIItCTc8.js +0 -21
  157. package/dist/auth_adapter-CIItCTc8.js.map +0 -1
  158. package/dist/backend-DTAOsLQc.js.map +0 -1
  159. package/dist/cron-store-DEpVstUU.js.map +0 -1
  160. package/dist/ddl-bootstrap-BNqi2NZe.js.map +0 -1
  161. package/dist/errors-_ytoGi8A.js.map +0 -1
  162. package/dist/function-loader-DbCSoNJj.js.map +0 -1
  163. package/dist/history-recorder-RZ1Ow4YY.js.map +0 -1
  164. package/dist/history-store-DPofGVDp.js.map +0 -1
  165. package/dist/jobs-8G-_agld.js.map +0 -1
  166. package/dist/logger-Bt32sHFw.js.map +0 -1
  167. package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
  168. package/dist/openapi-generator-BamY6-ga.js.map +0 -1
  169. package/dist/proxy-BEKbsil6.js.map +0 -1
  170. package/dist/request-timeout-CRny5KD0.js.map +0 -1
  171. package/dist/soft-delete-params-BAgpmBvp.js.map +0 -1
  172. package/dist/sql-export-routes-BgTJ7K3S.js.map +0 -1
  173. package/dist/strings.es-BeZWAodn.js.map +0 -1
  174. package/dist/upload-validation-I9cCUBIW.js.map +0 -1
@@ -7,9 +7,10 @@
7
7
  * request path is covered — but a fire-and-forget `void syncToCrm(body)`, a
8
8
  * `setTimeout(async () => …)`, or an unawaited `.then()` in one custom function
9
9
  * rejects outside Hono's frame. The default outcome is that one project's
10
- * floating promise ends the process; on the managed runtime that process is
11
- * shared, so every other tenant on the pod goes with it, and the request log
12
- * points nowhere because the request itself succeeded.
10
+ * floating promise ends the process, and with it every other route the
11
+ * project serves — its data API, auth, realtime — and the request log points
12
+ * nowhere because the request itself succeeded. On the managed runtime a
13
+ * project usually has one instance, so that is an outage until it restarts.
13
14
  *
14
15
  * A logged rejection is the better trade here: the process keeps serving, and
15
16
  * the stack reaches the operator. `uncaughtException` is deliberately **not**
@@ -29,6 +29,14 @@ interface ShutdownConfig {
29
29
  };
30
30
  /** The stop `MetricsHistory.start()` returned: its interval writes to the pool. */
31
31
  stopMetricsSampler?: () => void;
32
+ /**
33
+ * The shared rate-limit store the backend created itself
34
+ * (`REBASE_RATE_LIMIT_STORE=sql`): its sweep timer deletes from the pool.
35
+ * Not one the caller passed in, which is the caller's to stop.
36
+ */
37
+ ownedRateLimitStore?: {
38
+ dispose?(): void;
39
+ };
32
40
  realtimeServices: Record<string, RealtimeProvider>;
33
41
  /**
34
42
  * End the responses that stay open until their client leaves — the Logs
package/dist/init.d.ts CHANGED
@@ -57,7 +57,12 @@ export type RebaseBackendInstance = RebaseBackend;
57
57
  *
58
58
  * ```ts
59
59
  * installUnhandledRejectionHandler();
60
- * const backend = await createRebaseBackend({ ...options, provision: true });
60
+ * const backend = await createRebaseBackend({
61
+ * ...options,
62
+ * provision: !resolveProvisionEnv(process.env)
63
+ * ? false
64
+ * : provisionSchema === false ? { collections: false, realtime: false } : true
65
+ * });
61
66
  * setDefaultBackend(backend);
62
67
  * backend.mount(app, { basePath });
63
68
  * await backend.attachRealtime(server);
@@ -75,4 +75,11 @@ export interface JobStore {
75
75
  sweepExpired?(): Promise<number>;
76
76
  fetch(id: string): Promise<JobRecord | null>;
77
77
  }
78
- export declare function createJobStore(driver: DataDriver): JobStore | undefined;
78
+ export declare function createJobStore(driver: DataDriver, options?: {
79
+ /**
80
+ * Default `true`. `false` is a backend that changes nothing in its
81
+ * database: `ensureTable` uses the queue and its enqueue function if
82
+ * they are there, and creates, grants and revokes nothing.
83
+ */
84
+ provision?: boolean;
85
+ }): JobStore | undefined;
@@ -3,12 +3,12 @@ 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-D5Y3N-S4.js";
7
- import { t as isSQLAdmin } from "./backend-DTAOsLQc.js";
8
- import { r as logger } from "./logger-Bt32sHFw.js";
6
+ import "./src-Bh9LRPb9.js";
7
+ import { n as isSQLAdmin } from "./backend-CaG_Jyso.js";
8
+ import { a as logger } from "./logger-CW8pmTS6.js";
9
9
  import { n as revokeInternalTableSql, t as REBASE_USER_ROLE } from "./internal-tables-L2t39RdT.js";
10
10
  import { n as sqlRows, t as firstSqlRow } from "./sql-rows-C6GEc2oE.js";
11
- import { n as createDdlBootstrapper, r as hasInCauseChain } from "./ddl-bootstrap-BNqi2NZe.js";
11
+ import { c as provisioningExec, i as hasInCauseChain, r as createDdlBootstrapper } from "./ddl-bootstrap-CjRrn12l.js";
12
12
  import { randomUUID } from "crypto";
13
13
  //#region src/db/ambient-transaction.ts
14
14
  /** Frozen — see above. `server-postgres/src/services/write-transaction-scope.ts` writes it. */
@@ -506,13 +506,13 @@ var HELD_BY_CLAIM = "id = $1 AND status = 'running' AND locked_by = $2 AND attem
506
506
  function warnStaleOutcome(id, claim, outcome) {
507
507
  logger.warn(`[jobs] Ignored the ${outcome} of attempt ${claim.attempt} of job ${id}: the job was reclaimed from this worker while it ran, so that attempt's outcome is no longer the job's.`);
508
508
  }
509
- function createJobStore(driver) {
509
+ function createJobStore(driver, options = {}) {
510
510
  const admin = driver.admin;
511
511
  if (!isSQLAdmin(admin)) {
512
512
  logger.warn("⚠️ [jobs] DataDriver does not support SQL admin — the durable job queue is unavailable. Work that would have been queued runs inline instead.");
513
513
  return;
514
514
  }
515
- const execRaw = (sqlText, options) => admin.executeSql(sqlText, options?.params ? { params: options.params } : void 0);
515
+ const execRaw = provisioningExec((sqlText, execOptions) => admin.executeSql(sqlText, execOptions?.params ? { params: execOptions.params } : void 0), options.provision);
516
516
  const exec = (sqlText, params) => execRaw(sqlText, params ? { params } : void 0);
517
517
  const ddl = createDdlBootstrapper(execRaw, "jobs");
518
518
  /**
@@ -743,4 +743,4 @@ var jobs_exports = /* @__PURE__ */ __exportAll({
743
743
  //#endregion
744
744
  export { defaultBackoff as a, createJobQueue as i, createJobStore as n, currentAmbientTransaction as o, PermanentJobError as r, jobs_exports as t };
745
745
 
746
- //# sourceMappingURL=jobs-8G-_agld.js.map
746
+ //# sourceMappingURL=jobs-Co6Ni5HP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs-Co6Ni5HP.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 { currentAmbientTransaction } from \"../db/ambient-transaction.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/**\n * How long one attempt may run when `jobs.timeoutMs` is left out. Without a\n * bound, a handler awaiting a socket that never answers held its worker slot\n * and its claim for the life of the process, with `/health` green. Fifteen\n * minutes is far past any job that is merely slow, and a job meant to run\n * longer says so with its own `timeoutMs` — or `0`, for no limit.\n */\nexport const DEFAULT_JOB_TIMEOUT_MS = 15 * 60_000;\n\n/**\n * The longest an idle worker goes without looking for work.\n *\n * An idle worker used to claim every 2 seconds, forever: about 45,000 round\n * trips a day per pod, each with a pool checkout, and a database that could\n * never suspend. Now it is woken instead — by `NOTIFY` when another process\n * enqueues, by its own enqueues, retries and delayed jobs — and this is the\n * safety poll behind that. Where the database cannot notify it, it backs off\n * from `pollIntervalMs` to this, doubling after each look that found nothing.\n */\nconst IDLE_POLL_MAX_MS = 30_000;\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 * How often a worker deletes finished jobs past retention. Hourly keeps the\n * table near its retention size on a pod that is never redeployed; each sweep\n * reads only the finished rows, through `idx_jobs_finished`.\n */\nconst RETENTION_SWEEP_INTERVAL_MS = 60 * 60_000;\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 const configuredTimeoutMs = options.timeoutMs;\n if (configuredTimeoutMs !== undefined && !(Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs >= 0)) {\n // NaN is what a mistyped env var parses to; it would fail every\n // attempt at once.\n throw new Error(`jobs.timeoutMs must be a number of milliseconds, or 0 for no limit (got ${String(configuredTimeoutMs)}).`);\n }\n /** `undefined` means no limit: only an explicit `0` asks for that. */\n const timeoutMs = configuredTimeoutMs === undefined\n ? DEFAULT_JOB_TIMEOUT_MS\n : configuredTimeoutMs === 0 ? undefined : configuredTimeoutMs;\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 /** When {@link timer} fires (epoch ms), so a later wake never postpones an earlier one. */\n let timerDue = Infinity;\n /**\n * The wait after a look that found nothing. Doubles to {@link idleCeilingMs}\n * while the worker polls on its own; back to `pollIntervalMs` whenever\n * there is work or a wake. Fixed at the ceiling while notifications wake it.\n */\n const idleCeilingMs = Math.max(IDLE_POLL_MAX_MS, pollIntervalMs);\n let idleIntervalMs = pollIntervalMs;\n /** Notifications from the database wake this worker — see `JobStore.subscribe`. */\n let listening = false;\n let unsubscribe: (() => Promise<void>) | null = null;\n /** A wake arrived while a claim was in flight: claim again once it is done. */\n let rerun = false;\n let reapTimer: NodeJS.Timeout | null = null;\n let sweepTimer: NodeJS.Timeout | null = null;\n let sweeping = false;\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 const retryAt = job.attempts < job.maxAttempts ? new Date(Date.now() + backoff(job.attempts)) : null;\n await store.fail(job.id, `No handler registered for task \"${job.task}\"`, retryAt, claim);\n if (retryAt) wake(retryAt.getTime());\n return;\n }\n\n const releaseClaim = keepClaim(job, claim);\n const controller = new AbortController();\n let deadline: NodeJS.Timeout | undefined;\n try {\n try {\n const run = Promise.resolve().then(() => handler({\n id: job.id,\n task: job.task,\n payload: job.payload as never,\n attempt: job.attempts,\n maxAttempts: job.maxAttempts,\n signal: controller.signal\n } as never));\n if (timeoutMs === undefined) {\n await run;\n } else {\n // A handler that ignores the signal outlives its attempt;\n // what it does after is not this attempt's outcome.\n run.catch(() => undefined);\n await Promise.race([run, new Promise<never>((_, reject) => {\n deadline = setTimeout(() => {\n const error = new Error(`\"${job.task}\" timed out after ${timeoutMs}ms (jobs.timeoutMs)`);\n controller.abort(error);\n reject(error);\n }, timeoutMs);\n // A deadline is not work: it must not hold a process\n // open that is otherwise done.\n deadline.unref?.();\n })]);\n }\n } finally {\n clearTimeout(deadline);\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 const retryAt = willRetry ? new Date(Date.now() + backoff(job.attempts)) : null;\n await store.fail(job.id, message.slice(0, 4_000), retryAt, claim);\n // This worker knows when the retry is due; nobody will notify it.\n if (retryAt) wake(retryAt.getTime());\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 // Revived jobs are runnable now, and nothing notified anyone.\n if (await store.reapExpired(visibilityTimeoutMs) > 0) wake();\n } catch (error) {\n logger.error(\"[jobs] Failed to reclaim expired jobs\", { error });\n } finally {\n reaping = false;\n }\n }\n\n /**\n * Delete finished jobs past retention. On the worker, on its own timer:\n * the sweep used to run only in the boot path, awaited before the server\n * served, so a pod that lived for weeks never pruned and the next boot\n * paid for all of it at once.\n */\n async function sweep(): Promise<void> {\n if (sweeping || !store.sweepExpired) return;\n sweeping = true;\n try {\n const deleted = await store.sweepExpired();\n if (deleted > 0) logger.info(`[jobs] Removed ${deleted} finished job(s) past retention`);\n } catch (error) {\n logger.warn(\"[jobs] The retention sweep failed; it runs again within the hour\", { error });\n } finally {\n sweeping = 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 // Found work: look again soon if it runs out. Found none: wait longer.\n if (jobs.length > 0) idleIntervalMs = pollIntervalMs;\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 /** Look for work in `delayMs` — unless a look is already due sooner. */\n function schedule(delayMs: number): void {\n if (!running) return;\n const due = Date.now() + delayMs;\n if (timer && timerDue <= due) return;\n if (timer) clearTimeout(timer);\n timerDue = due;\n timer = setTimeout(() => {\n timer = null;\n timerDue = Infinity;\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 /**\n * Work may be runnable at `runAtMs` (now, when absent): a job was enqueued\n * — here, or in another process that notified — or a retry or a reclaimed\n * job came due. Resets the back-off.\n */\n function wake(runAtMs?: number): void {\n if (!running) return;\n idleIntervalMs = pollIntervalMs;\n const delay = runAtMs === undefined ? 0 : Math.max(0, runAtMs - Date.now());\n if (delay === 0 && active.size >= concurrency) {\n // Every slot is busy: the next one to free claims at once.\n backlog = true;\n return;\n }\n schedule(delay);\n }\n\n async function tick(): Promise<void> {\n if (!running) return;\n // A claim already in progress looks again when it finishes — the wake\n // may be for a row its snapshot did not see.\n if (claiming) {\n rerun = true;\n return;\n }\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, and so does a wake that came in mid-claim.\n // Otherwise sleep; a job finishing during a backlog wakes the loop\n // early, and so does anything that calls `wake`.\n if ((backlog && active.size < concurrency) || rerun) {\n rerun = false;\n schedule(0);\n return;\n }\n if (listening) {\n schedule(idleCeilingMs);\n return;\n }\n schedule(idleIntervalMs);\n idleIntervalMs = Math.min(idleIntervalMs * 2, idleCeilingMs);\n }\n\n /** Be woken by the database when another process enqueues — where it can. */\n function listen(): void {\n if (!store.subscribe) return;\n store.subscribe(\n (runAtMs) => wake(runAtMs),\n // Notifications sent while the connection was down are gone.\n () => wake()\n ).then((stop) => {\n if (!running) {\n void stop().catch(() => undefined);\n return;\n }\n unsubscribe = stop;\n listening = true;\n logger.info(`[jobs] Woken by the database on enqueue; looking every ${idleCeilingMs / 1000}s besides`);\n }).catch((error: unknown) => {\n logger.info(\n `[jobs] Not woken by the database (${error instanceof Error ? error.message : String(error)}); ` +\n `polling instead, from every ${pollIntervalMs}ms up to every ${idleCeilingMs / 1000}s while idle`\n );\n });\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 idleIntervalMs = pollIntervalMs;\n logger.info(`[jobs] Worker started (concurrency ${concurrency}, poll ${pollIntervalMs}ms, up to ${idleCeilingMs}ms while idle)`);\n if (configuredTimeoutMs === undefined) {\n logger.info(\n `[jobs] Each attempt is limited to ${DEFAULT_JOB_TIMEOUT_MS / 60_000} minutes, the default jobs.timeoutMs. ` +\n \"Set jobs.timeoutMs to change it, or 0 for no limit.\"\n );\n }\n void reap();\n reapTimer = setInterval(() => { void reap(); }, visibilityTimeoutMs * REAP_INTERVAL_FACTOR);\n reapTimer.unref?.();\n void sweep();\n sweepTimer = setInterval(() => { void sweep(); }, RETENTION_SWEEP_INTERVAL_MS);\n sweepTimer.unref?.();\n listen();\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 timerDue = Infinity;\n }\n listening = false;\n const stopListening = unsubscribe;\n unsubscribe = null;\n if (stopListening) {\n await stopListening().catch((error: unknown) => {\n logger.warn(\"[jobs] Could not close the notification connection\", { error });\n });\n }\n if (reapTimer) {\n clearInterval(reapTimer);\n reapTimer = null;\n }\n if (sweepTimer) {\n clearInterval(sweepTimer);\n sweepTimer = 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 const runAt = new Date(Date.now() + (enqueueOptions.delayMs ?? 0));\n // Read before the insert, which may await: inside a write the row\n // is invisible until that write commits, so that is when to look.\n const ambient = currentAmbientTransaction();\n const id = await store.insert({\n task,\n payload: payload ?? null,\n runAt,\n maxAttempts: enqueueOptions.maxAttempts ?? defaultMaxAttempts,\n idempotencyKey: enqueueOptions.idempotencyKey\n });\n // This process's own worker needs no notification to hear about it.\n if (id !== null && running) {\n if (ambient) ambient.afterCommit(() => wake(runAt.getTime()));\n else wake(runAt.getTime());\n }\n return id;\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, provisioningExec, 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 retention 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/**\n * Rows one retention statement deletes. A sweep repeats it until a batch\n * comes back short, so a backlog — the first sweep on a table a long-lived\n * pod let grow to millions of rows — is many short transactions rather than\n * one that holds locks and writes a burst of WAL for seconds.\n */\nconst RETENTION_BATCH = 5_000;\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 * The `NOTIFY` channel an insert announces itself on, with the job's `run_at`\n * in epoch milliseconds as the payload. Workers listening on it are woken\n * instead of polling; Postgres delivers it when the insert commits, and not at\n * all if it rolls back. Frozen: workers of the previous release listen on it\n * during a rolling deploy.\n */\nexport const JOBS_NOTIFY_CHANNEL = \"rebase_jobs\";\n\n/** `run_at` as the notification's payload: epoch milliseconds, as text. */\nconst NOTIFY_PAYLOAD = (runAt: string) => `(extract(epoch FROM ${runAt}) * 1000)::bigint::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 /**\n * Call `onWake` whenever any process inserts a job, with the job's `run_at`\n * in epoch milliseconds. Resolves, once listening, with the stop; rejects\n * when this database cannot notify this process — no session connection\n * to hold a `LISTEN` on — and the worker polls instead. `onReconnect` is\n * called after a dropped connection is back: what was sent meanwhile is\n * lost. A store without it leaves the worker polling.\n */\n subscribe?(onWake: (runAtMs?: number) => void, onReconnect: () => void): Promise<() => Promise<void>>;\n /**\n * Delete finished jobs past retention — succeeded ones after\n * {@link SUCCEEDED_RETENTION_DAYS} days, failed ones after\n * {@link FAILED_RETENTION_DAYS} — in batches. Resolves with how many went.\n * The worker runs it on a timer; a store without it keeps every row.\n */\n sweepExpired?(): 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(\n driver: DataDriver,\n options: {\n /**\n * Default `true`. `false` is a backend that changes nothing in its\n * database: `ensureTable` uses the queue and its enqueue function if\n * they are there, and creates, grants and revokes nothing.\n */\n provision?: boolean;\n } = {}\n): 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 = provisioningExec(\n (sqlText, execOptions) =>\n admin.executeSql(sqlText, execOptions?.params ? { params: execOptions.params } : undefined),\n options.provision\n );\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 // The retention sweep's index. Partial on the finished rows — the\n // only ones it deletes — so a sweep reads what it removes instead\n // of the whole table; without it every sweep was a sequential scan,\n // six seconds over two million rows.\n await ddl.ensureObject(\"Creating idx_jobs_finished\", `\n CREATE INDEX IF NOT EXISTS idx_jobs_finished\n ON ${TABLE}(status, updated_at)\n WHERE status IN ('succeeded', 'failed')\n `);\n\n const ready = await ddl.isReadable(TABLE);\n\n if (ready) {\n // No retention sweep here. It used to run at boot and only at\n // boot: awaited before the server served, a sequential scan of\n // the whole table, and never again for as long as the pod lived\n // — a worker up for three weeks held seven times the rows\n // retention meant to keep. The worker runs `sweepExpired` on a\n // timer instead.\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 SELECT pg_notify('${JOBS_NOTIFY_CHANNEL}', ${NOTIFY_PAYLOAD(\"p_run_at\")});\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 // The notification rides the insert's own statement, so it\n // is sent exactly when the row commits.\n const rows = await exec(\n `WITH inserted AS (\n INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)\n VALUES ($1, $2::jsonb, $3, $4, $5)\n RETURNING id, run_at\n )\n SELECT id, pg_notify('${JOBS_NOTIFY_CHANNEL}', ${NOTIFY_PAYLOAD(\"run_at\")}) AS notified\n FROM inserted`,\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 sweepExpired(): Promise<number> {\n // `SKIP LOCKED`, so replicas sweeping at once divide the rows\n // rather than queue behind each other's batch. The count comes back\n // as one row, not one id per deleted job.\n let total = 0;\n for (;;) {\n const rows = await exec(\n `WITH swept AS (\n DELETE FROM ${TABLE}\n WHERE id IN (\n SELECT id 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 LIMIT $3\n FOR UPDATE SKIP LOCKED\n )\n RETURNING 1\n )\n SELECT count(*)::int AS deleted FROM swept`,\n [SUCCEEDED_RETENTION_DAYS, FAILED_RETENTION_DAYS, RETENTION_BATCH]\n );\n const deleted = Number(firstSqlRow<{ deleted: number }>(rows)?.deleted ?? 0);\n total += deleted;\n if (deleted < RETENTION_BATCH) return total;\n }\n },\n\n async subscribe(onWake: (runAtMs?: number) => void, onReconnect: () => void): Promise<() => Promise<void>> {\n if (!admin.listen) {\n throw new Error(\"this database driver has no notifications\");\n }\n return admin.listen(JOBS_NOTIFY_CHANNEL, (payload) => {\n const runAtMs = Number(payload);\n onWake(Number.isFinite(runAtMs) && runAtMs > 0 ? runAtMs : undefined);\n }, { onReconnect });\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":";;;;;;;;;;;;;;AA0CA,IAAM,gBAAgB,OAAO,IAAI,0CAA0C;;AAc3E,SAAgB,4BAA4D;CACxE,OAAQ,WAA4B,cAAc,GAAG;AACzD;;;;;;;;;;AC5CA,IAAM,sBAAsB;AAC5B,IAAM,2BAA2B;AACjC,IAAM,gCAAgC,IAAI;AAC1C,IAAM,uBAAuB;;;;;;;;AAS7B,IAAa,yBAAyB,KAAK;;;;;;;;;;;AAY3C,IAAM,mBAAmB;;AAGzB,SAAgB,eAAe,SAAyB;CACpD,OAAO,KAAK,IAAI,MAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,UAAU,CAAC,CAAC,GAAG,KAAK,GAAM;AAC9E;;;;;;;;AASA,IAAM,uBAAuB;;;;;;AAO7B,IAAM,8BAA8B,KAAK;;;;;;;;AASzC,IAAa,oBAAb,cAAuC,MAAM;CACzC,YAAY,SAAiB,SAA+B;EACxD,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CAChB;AACJ;AAqBA,SAAgB,eAAe,OAAiB,UAA2B,CAAC,GAAa;CACrF,MAAM,cAAc,QAAQ,eAAe;CAC3C,MAAM,iBAAiB,QAAQ,kBAAkB;CACjD,MAAM,sBAAsB,QAAQ,uBAAuB;CAC3D,MAAM,qBAAqB,QAAQ,eAAe;CAClD,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,sBAAsB,QAAQ;CACpC,IAAI,wBAAwB,KAAA,KAAa,EAAE,OAAO,SAAS,mBAAmB,KAAK,uBAAuB,IAGtG,MAAM,IAAI,MAAM,2EAA2E,OAAO,mBAAmB,EAAE,GAAG;;CAG9H,MAAM,YAAY,wBAAwB,KAAA,IACpC,yBACA,wBAAwB,IAAI,KAAA,IAAY;CAE9C,MAAM,2BAAW,IAAI,IAA+B;CACpD,KAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC,GAC5D,SAAS,IAAI,MAAM,OAAO;CAM9B,MAAM,WAAW,GAAG,QAAQ,IAAI,GAAG,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC;CAE1D,IAAI,QAA+B;;CAEnC,IAAI,WAAW;;;;;;CAMf,MAAM,gBAAgB,KAAK,IAAI,kBAAkB,cAAc;CAC/D,IAAI,iBAAiB;;CAErB,IAAI,YAAY;CAChB,IAAI,cAA4C;;CAEhD,IAAI,QAAQ;CACZ,IAAI,YAAmC;CACvC,IAAI,aAAoC;CACxC,IAAI,WAAW;CACf,IAAI,UAAU;;;;;;;CAOd,MAAM,yBAAS,IAAI,IAAmB;;CAEtC,IAAI,WAAoC;;CAExC,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,WAAiC;;;;;;;CAQrC,SAAS,UAAU,KAAgB,OAA6B;EAC5D,IAAI,CAAC,MAAM,WAAW,aAAa,KAAA;EACnC,MAAM,QAAQ,MAAM,UAAU,KAAK,KAAK;EACxC,MAAM,WAAW,kBAAkB;GAC/B,MAAM,IAAI,IAAI,KAAK,CAAC,CAAC,MAAM,SAAS;IAChC,IAAI,MAAM;IACV,cAAc,QAAQ;IACtB,OAAO,KACH,WAAW,IAAI,KAAK,8BAA8B,MAAM,QAAQ,mDAC5B,EAAE,OAAO,IAAI,GAAG,CACxD;GACJ,CAAC,CAAC,CAAC,OAAO,UAAmB;IACzB,OAAO,KAAK,wCAAwC,IAAI,KAAK,IAAI;KAAE,OAAO,IAAI;KAAI;IAAM,CAAC;GAC7F,CAAC;EACL,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,sBAAsB,CAAC,CAAC,CAAC;EACnD,SAAS,QAAQ;EACjB,aAAa,cAAc,QAAQ;CACvC;CAEA,eAAe,OAAO,KAA+B;EACjD,MAAM,UAAU,SAAS,IAAI,IAAI,IAAI;EACrC,MAAM,QAAkB;GAAE;GAAU,SAAS,IAAI;EAAS;EAE1D,IAAI,CAAC,SAAS;GAOV,OAAO,KAAK,0CAA0C,IAAI,KAAK,mCAAmC;GAClG,MAAM,UAAU,IAAI,WAAW,IAAI,cAAc,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI;GAChG,MAAM,MAAM,KAAK,IAAI,IAAI,mCAAmC,IAAI,KAAK,IAAI,SAAS,KAAK;GACvF,IAAI,SAAS,KAAK,QAAQ,QAAQ,CAAC;GACnC;EACJ;EAEA,MAAM,eAAe,UAAU,KAAK,KAAK;EACzC,MAAM,aAAa,IAAI,gBAAgB;EACvC,IAAI;EACJ,IAAI;GACA,IAAI;IACA,MAAM,MAAM,QAAQ,QAAQ,CAAC,CAAC,WAAW,QAAQ;KAC7C,IAAI,IAAI;KACR,MAAM,IAAI;KACV,SAAS,IAAI;KACb,SAAS,IAAI;KACb,aAAa,IAAI;KACjB,QAAQ,WAAW;IACvB,CAAU,CAAC;IACX,IAAI,cAAc,KAAA,GACd,MAAM;SACH;KAGH,IAAI,YAAY,KAAA,CAAS;KACzB,MAAM,QAAQ,KAAK,CAAC,KAAK,IAAI,SAAgB,GAAG,WAAW;MACvD,WAAW,iBAAiB;OACxB,MAAM,wBAAQ,IAAI,MAAM,IAAI,IAAI,KAAK,oBAAoB,UAAU,oBAAoB;OACvF,WAAW,MAAM,KAAK;OACtB,OAAO,KAAK;MAChB,GAAG,SAAS;MAGZ,SAAS,QAAQ;KACrB,CAAC,CAAC,CAAC;IACP;GACJ,UAAU;IACN,aAAa,QAAQ;IACrB,aAAa;GACjB;GACA,MAAM,MAAM,SAAS,IAAI,IAAI,KAAK;EACtC,SAAS,OAAO;GACZ,MAAM,UAAU,iBAAiB,QAAS,MAAM,SAAS,MAAM,UAAW,OAAO,KAAK;GACtF,MAAM,YAAY,iBAAiB;GACnC,MAAM,YAAY,CAAC,aAAa,IAAI,WAAW,IAAI;GAInD,MAAM,UAAU,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI;GAC3E,MAAM,MAAM,KAAK,IAAI,IAAI,QAAQ,MAAM,GAAG,GAAK,GAAG,SAAS,KAAK;GAEhE,IAAI,SAAS,KAAK,QAAQ,QAAQ,CAAC;GAEnC,IAAI,WACA,OAAO,KAAK,WAAW,IAAI,KAAK,sBAAsB,IAAI,SAAS,GAAG,IAAI,YAAY,aAAa,EAAE,OAAO,IAAI,GAAG,CAAC;QACjH,IAAI,WACP,OAAO,MAAM,WAAW,IAAI,KAAK,kCAAkC,IAAI,SAAS,iBAAiB;IAAE,OAAO,IAAI;IAAI,OAAO;GAAQ,CAAC;QAKlI,OAAO,MAAM,WAAW,IAAI,KAAK,6BAA6B,IAAI,SAAS,YAAY;IAAE,OAAO,IAAI;IAAI,OAAO;GAAQ,CAAC;EAEhI;CACJ;;;;;;CAOA,eAAe,OAAsB;EACjC,IAAI,SAAS;EACb,UAAU;EACV,IAAI;GAEA,IAAI,MAAM,MAAM,YAAY,mBAAmB,IAAI,GAAG,KAAK;EAC/D,SAAS,OAAO;GACZ,OAAO,MAAM,yCAAyC,EAAE,MAAM,CAAC;EACnE,UAAU;GACN,UAAU;EACd;CACJ;;;;;;;CAQA,eAAe,QAAuB;EAClC,IAAI,YAAY,CAAC,MAAM,cAAc;EACrC,WAAW;EACX,IAAI;GACA,MAAM,UAAU,MAAM,MAAM,aAAa;GACzC,IAAI,UAAU,GAAG,OAAO,KAAK,kBAAkB,QAAQ,gCAAgC;EAC3F,SAAS,OAAO;GACZ,OAAO,KAAK,oEAAoE,EAAE,MAAM,CAAC;EAC7F,UAAU;GACN,WAAW;EACf;CACJ;;CAGA,eAAe,gBAA0C;EACrD,MAAM,OAAO,cAAc,OAAO;EAClC,IAAI,QAAQ,GAAG,OAAO,CAAC;EAOvB,IAAI,SAAS,SAAS,GAAG,OAAO,CAAC;EACjC,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,UAAU,CAAC,GAAG,SAAS,KAAK,CAAC,CAAC;EACnE,UAAU,KAAK,UAAU;EAEzB,IAAI,KAAK,SAAS,GAAG,iBAAiB;EAEtC,OAAO,KAAK,KAAK,QAAQ;GACrB,MAAM,MAAqB,OAAO,GAAG,CAAC,CACjC,OAAO,UAAU;IAId,OAAO,MAAM,2CAA2C,IAAI,KAAK,IAAI;KAAE,OAAO,IAAI;KAAI;IAAM,CAAC;GACjG,CAAC,CAAC,CACD,cAAc;IACX,OAAO,OAAO,GAAG;IAGjB,IAAI,SAAS,SAAS,CAAC;GAC3B,CAAC;GACL,OAAO,IAAI,GAAG;GACd,OAAO;EACX,CAAC;CACL;;CAGA,SAAS,SAAS,SAAuB;EACrC,IAAI,CAAC,SAAS;EACd,MAAM,MAAM,KAAK,IAAI,IAAI;EACzB,IAAI,SAAS,YAAY,KAAK;EAC9B,IAAI,OAAO,aAAa,KAAK;EAC7B,WAAW;EACX,QAAQ,iBAAiB;GACrB,QAAQ;GACR,WAAW;GACX,KAAU;EACd,GAAG,OAAO;EAGV,MAAM,QAAQ;CAClB;;;;;;CAOA,SAAS,KAAK,SAAwB;EAClC,IAAI,CAAC,SAAS;EACd,iBAAiB;EACjB,MAAM,QAAQ,YAAY,KAAA,IAAY,IAAI,KAAK,IAAI,GAAG,UAAU,KAAK,IAAI,CAAC;EAC1E,IAAI,UAAU,KAAK,OAAO,QAAQ,aAAa;GAE3C,UAAU;GACV;EACJ;EACA,SAAS,KAAK;CAClB;CAEA,eAAe,OAAsB;EACjC,IAAI,CAAC,SAAS;EAGd,IAAI,UAAU;GACV,QAAQ;GACR;EACJ;EACA,MAAM,OAAO,cAAc,CAAC,CAAC,OAAO,UAAU;GAC1C,OAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC;GAC5C,UAAU;EACd,CAAC;EACD,WAAW;EACX,MAAM;EACN,WAAW;EAMX,IAAK,WAAW,OAAO,OAAO,eAAgB,OAAO;GACjD,QAAQ;GACR,SAAS,CAAC;GACV;EACJ;EACA,IAAI,WAAW;GACX,SAAS,aAAa;GACtB;EACJ;EACA,SAAS,cAAc;EACvB,iBAAiB,KAAK,IAAI,iBAAiB,GAAG,aAAa;CAC/D;;CAGA,SAAS,SAAe;EACpB,IAAI,CAAC,MAAM,WAAW;EACtB,MAAM,WACD,YAAY,KAAK,OAAO,SAEnB,KAAK,CACf,CAAC,CAAC,MAAM,SAAS;GACb,IAAI,CAAC,SAAS;IACV,KAAU,CAAC,CAAC,YAAY,KAAA,CAAS;IACjC;GACJ;GACA,cAAc;GACd,YAAY;GACZ,OAAO,KAAK,0DAA0D,gBAAgB,IAAK,UAAU;EACzG,CAAC,CAAC,CAAC,OAAO,UAAmB;GACzB,OAAO,KACH,qCAAqC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,iCAC7D,eAAe,iBAAiB,gBAAgB,IAAK,aACxF;EACJ,CAAC;CACL;CAEA,eAAe,QAAuB;EAClC,MAAM;EACN,MAAM,QAAQ,WAAW,CAAC,GAAG,MAAM,CAAC;CACxC;CAEA,OAAO;EACH,QAAc;GACV,IAAI,SAAS;GACb,UAAU;GACV,iBAAiB;GACjB,OAAO,KAAK,sCAAsC,YAAY,SAAS,eAAe,YAAY,cAAc,eAAe;GAC/H,IAAI,wBAAwB,KAAA,GACxB,OAAO,KACH,qCAAqC,yBAAyB,IAAO,0FAEzE;GAEJ,KAAU;GACV,YAAY,kBAAkB;IAAE,KAAU;GAAG,GAAG,sBAAsB,oBAAoB;GAC1F,UAAU,QAAQ;GAClB,MAAW;GACX,aAAa,kBAAkB;IAAE,MAAW;GAAG,GAAG,2BAA2B;GAC7E,WAAW,QAAQ;GACnB,OAAO;GACP,SAAS,CAAC;EACd;EAEA,MAAM,KAAK,WAAmC;GAC1C,UAAU;GACV,IAAI,OAAO;IACP,aAAa,KAAK;IAClB,QAAQ;IACR,WAAW;GACf;GACA,YAAY;GACZ,MAAM,gBAAgB;GACtB,cAAc;GACd,IAAI,eACA,MAAM,cAAc,CAAC,CAAC,OAAO,UAAmB;IAC5C,OAAO,KAAK,sDAAsD,EAAE,MAAM,CAAC;GAC/E,CAAC;GAEL,IAAI,WAAW;IACX,cAAc,SAAS;IACvB,YAAY;GAChB;GACA,IAAI,YAAY;IACZ,cAAc,UAAU;IACxB,aAAa;GACjB;GAIA,aAAa,MAAM,CAAC,CAAC,cAAc;IAAE,WAAW;GAAM,CAAC;GACvD,IAAI,cAAc,KAAA,GAAW;IACzB,MAAM;IACN;GACJ;GACA,IAAI;GACJ,MAAM,WAAW,MAAM,QAAQ,KAAK,CAChC,SAAS,WAAW,IAAI,GACxB,IAAI,SAAgB,YAAY;IAC5B,SAAS,iBAAiB,QAAQ,KAAK,GAAG,SAAS;IACnD,OAAO,QAAQ;GACnB,CAAC,CACL,CAAC;GACD,aAAa,MAAM;GACnB,IAAI,CAAC,UACD,OAAO,KACH,UAAU,OAAO,KAAK,8BAA8B,UAAU,0GAElE;EAER;EAEA,MAAM,UAA2B;GAC7B,MAAM,UAAU,MAAM,cAAc;GACpC,MAAM,QAAQ,WAAW,OAAO;GAChC,OAAO,QAAQ;EACnB;EAEA,SAAsB,MAAc,SAA8B;GAC9D,IAAI,SAAS,IAAI,IAAI,GACjB,OAAO,KAAK,gBAAgB,KAAK,iDAAiD;GAEtF,SAAS,IAAI,MAAM,OAA4B;EACnD;EAEA,YAAqB;GACjB,OAAO;EACX;EAEA,MAAM,QAAqB,MAAc,SAAa,iBAAiC,CAAC,GAA2B;GAC/G,MAAM,QAAQ,IAAI,KAAK,KAAK,IAAI,KAAK,eAAe,WAAW,EAAE;GAGjE,MAAM,UAAU,0BAA0B;GAC1C,MAAM,KAAK,MAAM,MAAM,OAAO;IAC1B;IACA,SAAS,WAAW;IACpB;IACA,aAAa,eAAe,eAAe;IAC3C,gBAAgB,eAAe;GACnC,CAAC;GAED,IAAI,OAAO,QAAQ,SACf,IAAI,SAAS,QAAQ,kBAAkB,KAAK,MAAM,QAAQ,CAAC,CAAC;QACvD,KAAK,MAAM,QAAQ,CAAC;GAE7B,OAAO;EACX;CACJ;AACJ;;;;;;;;;;;AClgBA,IAAM,QAAQ;;AAGd,IAAM,2BAA2B;;;;;;AAMjC,IAAM,wBAAwB;;;;;;;AAQ9B,IAAM,kBAAkB;AAgBxB,SAAS,SAAS,KAAwB;CACtC,OAAO;EACH,IAAI,IAAI;EACR,MAAM,IAAI;EACV,SAAS,IAAI;EACb,QAAQ,IAAI;EACZ,OAAO,IAAI,KAAK,IAAI,MAAM,CAAC,CAAC,YAAY;EACxC,UAAU,OAAO,IAAI,QAAQ;EAC7B,aAAa,OAAO,IAAI,YAAY;EACpC,WAAW,IAAI;EACf,WAAW,IAAI,KAAK,IAAI,UAAU,CAAC,CAAC,YAAY;EAChD,WAAW,IAAI,KAAK,IAAI,UAAU,CAAC,CAAC,YAAY;CACpD;AACJ;;AAGA,SAAS,kBAAkB,KAAuB;CAC9C,OAAO,gBAAgB,MAAM,MAAM,EAAE,SAAS,OAAO;AACzD;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAM,mBAAmB;AACzB,IAAM,oBAAoB,GAAG,iBAAiB;;;;;;;;AAS9C,IAAa,sBAAsB;;AAGnC,IAAM,kBAAkB,UAAkB,uBAAuB,MAAM;;AAwEvE,IAAM,gBAAgB;AAEtB,SAAS,iBAAiB,IAAY,OAAiB,SAAsC;CACzF,OAAO,KACH,sBAAsB,QAAQ,cAAc,MAAM,QAAQ,UAAU,GAAG,yGAE3E;AACJ;AAEA,SAAgB,eACZ,QACA,UAOI,CAAC,GACe;CACpB,MAAM,QAAQ,OAAO;CACrB,IAAI,CAAC,WAAW,KAAK,GAAG;EACpB,OAAO,KACH,+IAEJ;EACA;CACJ;CAKA,MAAM,UAAmB,kBACpB,SAAS,gBACN,MAAM,WAAW,SAAS,aAAa,SAAS,EAAE,QAAQ,YAAY,OAAO,IAAI,KAAA,CAAS,GAC9F,QAAQ,SACZ;CACA,MAAM,QAAQ,SAAiB,WAC3B,QAAQ,SAAS,SAAS,EAAE,OAAO,IAAI,KAAA,CAAS;CAEpD,MAAM,MAAM,sBAAsB,SAAS,MAAM;;;;;;;CAQjD,IAAI,uBAAuB;CAE3B,OAAO;EACH,MAAM,cAA6B;GAC/B,MAAM,IAAI,aAAa,0BAA0B,oCAAoC;GAErF,MAAM,IAAI,aAAa,YAAY,SAAS;6CACX,MAAM;;;;;;;;;;;;;;;aAetC;GAKD,MAAM,IAAI,aAAa,8BAA8B;;qBAE5C,MAAM;;aAEd;GAGD,MAAM,IAAI,aAAa,6BAA6B;;qBAE3C,MAAM;;aAEd;GAOD,MAAM,IAAI,aAAa,iCAAiC;;qBAE/C,MAAM;;aAEd;GAMD,MAAM,IAAI,aAAa,8BAA8B;;qBAE5C,MAAM;;aAEd;GAID,IAAI,MAFgB,IAAI,WAAW,KAAK,GAE7B;IAaP,MAAM,IAAI,KAAK,0CACX,KAAK,uBAAuB,UAAU,MAAM,CAAC,CAAC;IAElD,MAAM,IAAI,aAAa,YAAY,oBAAoB;iDACtB,iBAAiB;;;;;;;;4CAQtB,oBAAoB,KAAK,eAAe,UAAU,EAAE;sCAC1D,MAAM;;;;;iBAK3B;IAID,MAAM,IAAI,KAAK,YAAY,iBAAiB,uBAAuB,YAAY;KAC3E,MAAM,KAAK,0BAA0B,kBAAkB,aAAa;KACpE,MAAM,KAAK;;iFAEkD,iBAAiB;4DACtC,kBAAkB,MAAM,iBAAiB;;;qBAGhF;IACL,CAAC;IACD,uBAAuB,YAAkC,MAAM,KAC3D,2BAA2B,kBAAkB,0BACjD,CAAC,CAAC,EAAE,YAAY;IAChB,IAAI,CAAC,sBACD,OAAO,KACH,aAAa,iBAAiB,qHAElC;IAGJ,OAAO,KAAK,yBAAyB;GACzC,OACI,OAAO,MACH,YAAY,MAAM,2HAEtB;EAER;EAEA,MAAM,OAAO,KAA6B;GACtC,MAAM,SAAS;IACX,IAAI;IACJ,KAAK,UAAU,IAAI,WAAW,IAAI;IAClC,IAAI,MAAM,YAAY;IACtB,IAAI;IACJ,IAAI,kBAAkB;GAC1B;GAIA,MAAM,UAAU,uBAAuB,0BAA0B,IAAI,KAAA;GACrE,IAAI,SAKA,QAAQ,MAJW,QAAQ,KACvB,UAAU,iBAAiB,sEAC3B,MACJ,EAAA,GACe,EAAE,EAAE,MAAoC;GAE3D,IAAI;IAaA,QAAQ,MAVW,KACf;uCACmB,MAAM;;;;2DAIyB,eAAe,QAAQ,EAAE;qCAE3E,MACJ,EAAA,GACe,EAAE,EAAE,MAAiB;GACxC,SAAS,OAAO;IAIZ,IAAI,kBAAkB,KAAK,GAAG,OAAO;IACrC,MAAM;GACV;EACJ;EAEA,MAAM,MAAM,OAAe,UAAkB,OAAiD;GAC1F,IAAI,SAAS,MAAM,WAAW,GAAG,OAAO,CAAC;GAGzC,MAAM,aAAa,QACb,gBAAgB,MAAM,KAAK,GAAG,MAAM,IAAI,IAAI,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,KAC5D;GA6BN,OAAO,QAAgB,MAjBJ,KACf,UAAU,MAAM;;;;;;;sCAOM,MAAM;oEACwB,WAAW;;;;;+BAM/D;IAAC;IAAO;IAAU,GAAI,SAAS,CAAC;GAAE,CACtC,CAC2B,CAAC,CAAC,IAAI,QAAQ;EAC7C;EAEA,MAAM,SAAS,IAAY,OAAgC;GAQvD,IAAI,QAAQ,MAPO,KACf,UAAU,MAAM;;yBAEP,cAAc;gCAEvB;IAAC;IAAI,MAAM;IAAU,MAAM;GAAO,CACtC,CACgB,CAAC,CAAC,WAAW,GAAG,iBAAiB,IAAI,OAAO,SAAS;EACzE;EAEA,MAAM,KAAK,IAAY,OAAe,SAAsB,OAAgC;GAgBxF,IAAI,QAfS,UACP,MAAM,KACJ,UAAU,MAAM;;6BAEP,cAAc;oCAEvB;IAAC;IAAI,MAAM;IAAU,MAAM;IAAS,QAAQ,YAAY;IAAG;GAAK,CACpE,IACE,MAAM,KACJ,UAAU,MAAM;;6BAEP,cAAc;oCAEvB;IAAC;IAAI,MAAM;IAAU,MAAM;IAAS;GAAK,CAC7C,CACY,CAAC,CAAC,WAAW,GAAG,iBAAiB,IAAI,OAAO,SAAS;EACzE;EAEA,MAAM,UAAU,IAAY,OAAmC;GAO3D,OAAO,QAAQ,MANI,KACf,UAAU,MAAM;yBACP,cAAc;gCAEvB;IAAC;IAAI,MAAM;IAAU,MAAM;GAAO,CACtC,CACmB,CAAC,CAAC,SAAS;EAClC;EAEA,MAAM,YAAY,qBAA8C;GAM5D,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,sBAAsB,GAAI,CAAC;GAElE,MAAM,UAAU,MAAM,KAClB,UAAU,MAAM;;;;;gCAMhB,CAAC,OAAO,CACZ;GAEA,MAAM,SAAS,MAAM,KACjB,UAAU,MAAM;;;;;gCAMhB,CAAC,OAAO,CACZ;GAEA,MAAM,SAAS,SAAS,UAAU,MAAM,QAAQ,UAAU;GAC1D,IAAI,QAAQ,GACR,OAAO,KACH,oBAAoB,MAAM,iDACtB,SAAS,UAAU,EAAE,cAAc,QAAQ,UAAU,EAAE,gBAC/D;GAEJ,OAAO;EACX;EAEA,MAAM,eAAgC;GAIlC,IAAI,QAAQ;GACZ,SAAS;IACL,MAAM,OAAO,MAAM,KACf;uCACmB,MAAM;;8CAEC,MAAM;;;;;;;;kEAShC;KAAC;KAA0B;KAAuB;IAAe,CACrE;IACA,MAAM,UAAU,OAAO,YAAiC,IAAI,CAAC,EAAE,WAAW,CAAC;IAC3E,SAAS;IACT,IAAI,UAAU,iBAAiB,OAAO;GAC1C;EACJ;EAEA,MAAM,UAAU,QAAoC,aAAuD;GACvG,IAAI,CAAC,MAAM,QACP,MAAM,IAAI,MAAM,2CAA2C;GAE/D,OAAO,MAAM,OAAO,sBAAsB,YAAY;IAClD,MAAM,UAAU,OAAO,OAAO;IAC9B,OAAO,OAAO,SAAS,OAAO,KAAK,UAAU,IAAI,UAAU,KAAA,CAAS;GACxE,GAAG,EAAE,YAAY,CAAC;EACtB;EAEA,MAAM,MAAM,IAAuC;GAE/C,MAAM,MAAM,YAAoB,MADb,KAAK,iBAAiB,MAAM,iBAAiB,CAAC,EAAE,CAAC,CAChC;GACpC,OAAO,MAAM,SAAS,GAAG,IAAI;EACjC;CACJ;AACJ"}
@@ -3,8 +3,8 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { i as __toESM, r as __require, t as __commonJSMin } from "./rolldown-runtime-dW7B1o5h.js";
6
- import { r as logger } from "./logger-Bt32sHFw.js";
7
- import { a as canonicalStorageId } from "./keys-COzQ2aE8.js";
6
+ import { a as logger } from "./logger-CW8pmTS6.js";
7
+ import { a as canonicalStorageId } from "./keys-BXcdRNhJ.js";
8
8
  import { createPrivateKey, createPublicKey, createSecretKey } from "crypto";
9
9
  //#region src/utils/portable-crypto.ts
10
10
  /**
@@ -4847,7 +4847,8 @@ function jwtServiceFrom(state) {
4847
4847
  scope: typeof decoded.scope === "string" ? decoded.scope : "",
4848
4848
  clientId: typeof decoded.clientId === "string" ? decoded.clientId : "",
4849
4849
  aud: decoded.aud,
4850
- iss: typeof decoded.iss === "string" ? decoded.iss : ""
4850
+ iss: typeof decoded.iss === "string" ? decoded.iss : "",
4851
+ ...typeof decoded.iat === "number" ? { iat: decoded.iat } : {}
4851
4852
  };
4852
4853
  } catch (error) {
4853
4854
  logger.error("[JWT] MCP token verification failed", { error });
@@ -5092,4 +5093,4 @@ var processJwtService = {
5092
5093
  //#endregion
5093
5094
  export { randomHex as C, sha256Hex as E, constantTimeEqual as S, sha256Bytes as T, verifyAccessToken as _, generateAccessToken as a, normalizePemFromEnv as b, getAccessTokenExpiry as c, hasAsymmetricSigningKey as d, hashRefreshToken as f, setDefaultJwtService as g, processJwtService as h, customClaimsOf as i, getJwks as l, jwtOf as m, configureJwt as n, generateDownloadToken as o, isJwtConfigured as p, createJwtService as r, generateRefreshToken as s, MAX_COOKIE_AGE_MS as t, getRefreshTokenExpiry as u, verifyDownloadToken as v, randomInt as w, require_jsonwebtoken as x, verifyJwt as y };
5094
5095
 
5095
- //# sourceMappingURL=jwt-YhciR93H.js.map
5096
+ //# sourceMappingURL=jwt-rDS0884_.js.map