@rebasepro/server 0.24.0 → 0.24.1-canary.g56af4a4

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 (181) hide show
  1. package/dist/GCSStorageController-DivE9bAx.js +402 -0
  2. package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
  3. package/dist/S3StorageController-D_vKs4kP.js +480 -0
  4. package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
  5. package/dist/api/rest/api-generator.d.ts +53 -1
  6. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  7. package/dist/api/rest/field-access-query.d.ts +28 -8
  8. package/dist/api/rest/field-ops.d.ts +19 -7
  9. package/dist/api/rest/query-body.d.ts +10 -0
  10. package/dist/api/rest/write-validation.d.ts +7 -5
  11. package/dist/api/schema-editor-routes.d.ts +10 -1
  12. package/dist/api/sql-export-routes.d.ts +8 -0
  13. package/dist/api/types.d.ts +11 -0
  14. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-CSdL0iCk.js} +2 -2
  15. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-CSdL0iCk.js.map} +1 -1
  16. package/dist/auth/access.d.ts +16 -0
  17. package/dist/auth/admin-user-ops.d.ts +24 -2
  18. package/dist/auth/admin-users-route.d.ts +1 -0
  19. package/dist/auth/expired-token-sweep.d.ts +5 -1
  20. package/dist/auth/interfaces.d.ts +10 -0
  21. package/dist/auth/jwt-crypto.d.ts +7 -0
  22. package/dist/auth/jwt-keys.d.ts +4 -0
  23. package/dist/auth/jwt.d.ts +6 -1
  24. package/dist/auth/rate-limiter.d.ts +13 -1
  25. package/dist/auth/registration-policy.d.ts +17 -0
  26. package/dist/auth/user-picking.d.ts +35 -0
  27. package/dist/{auth-DMLngxn_.js → auth-OPq_FeMs.js} +972 -85
  28. package/dist/auth-OPq_FeMs.js.map +1 -0
  29. package/dist/auth_adapter-CIItCTc8.js +21 -0
  30. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  31. package/dist/backend-DTAOsLQc.js.map +1 -1
  32. package/dist/backup/backup-common.d.ts +14 -3
  33. package/dist/backup/backup-routes.d.ts +23 -2
  34. package/dist/backup/backup-storage.d.ts +3 -1
  35. package/dist/backup/index.d.ts +3 -1
  36. package/dist/{backup-CN0s50D2.js → backup-5d7XhLX3.js} +170 -33
  37. package/dist/backup-5d7XhLX3.js.map +1 -0
  38. package/dist/boot/driver.d.ts +3 -0
  39. package/dist/boot/env.d.ts +7 -0
  40. package/dist/boot/http-server.d.ts +22 -0
  41. package/dist/boot/options.d.ts +16 -1
  42. package/dist/boot/runtime-cors.d.ts +17 -0
  43. package/dist/boot/static-routing.d.ts +9 -0
  44. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-02lASyJI.js} +3 -19
  45. package/dist/contract-routes-02lASyJI.js.map +1 -0
  46. package/dist/cron/cron-routes.d.ts +1 -1
  47. package/dist/cron/cron-scheduler.d.ts +69 -3
  48. package/dist/cron/cron-store.d.ts +27 -4
  49. package/dist/cron/define-cron.d.ts +6 -2
  50. package/dist/cron/index.d.ts +1 -1
  51. package/dist/{cron-loader-CwaANlOG.js → cron-loader-D7OmleAb.js} +8 -4
  52. package/dist/cron-loader-D7OmleAb.js.map +1 -0
  53. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-CBfiaqEL.js} +27 -4
  54. package/dist/cron-routes-CBfiaqEL.js.map +1 -0
  55. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-C2vbETMo.js} +298 -32
  56. package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
  57. package/dist/cron-store-B2Ih0gCN.js +540 -0
  58. package/dist/cron-store-B2Ih0gCN.js.map +1 -0
  59. package/dist/email/smtp-email-service.d.ts +23 -0
  60. package/dist/email/types.d.ts +18 -0
  61. package/dist/env.d.ts +0 -37
  62. package/dist/filter-dialect-REl-TjZr.js +1628 -0
  63. package/dist/filter-dialect-REl-TjZr.js.map +1 -0
  64. package/dist/{function-loader-D7o5Epjj.js → function-loader-BDKKiBmy.js} +4 -3
  65. package/dist/function-loader-BDKKiBmy.js.map +1 -0
  66. package/dist/{function-routes-CaNG4waN.js → function-routes-DWB7BBsi.js} +2 -2
  67. package/dist/function-routes-DWB7BBsi.js.map +1 -0
  68. package/dist/functions/context.d.ts +9 -0
  69. package/dist/functions/define-function.d.ts +7 -4
  70. package/dist/functions/index.d.ts +1 -1
  71. package/dist/functions/index.js +25 -2
  72. package/dist/functions/index.js.map +1 -1
  73. package/dist/functions/request-timeout.d.ts +7 -5
  74. package/dist/history/history-routes.d.ts +9 -0
  75. package/dist/history/retention-sweep.d.ts +81 -0
  76. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-DHenr3lm.js} +2 -2
  77. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-DHenr3lm.js.map} +1 -1
  78. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  79. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  80. package/dist/index.d.ts +6 -5
  81. package/dist/index.es.js +22671 -19191
  82. package/dist/index.es.js.map +1 -1
  83. package/dist/init/shutdown.d.ts +12 -2
  84. package/dist/init.d.ts +16 -6
  85. package/dist/internal-tables-L2t39RdT.js +136 -0
  86. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  87. package/dist/jobs/job-queue.d.ts +8 -0
  88. package/dist/jobs/job-store.d.ts +24 -0
  89. package/dist/jobs/types.d.ts +25 -4
  90. package/dist/{jobs-DqYNfquG.js → jobs-BpD4Q0qR.js} +232 -22
  91. package/dist/jobs-BpD4Q0qR.js.map +1 -0
  92. package/dist/{jwt-R6bSPMjk.js → jwt-Bqcu7cGt.js} +171 -65
  93. package/dist/jwt-Bqcu7cGt.js.map +1 -0
  94. package/dist/load-failure-UvzZgGXT.js +48 -0
  95. package/dist/load-failure-UvzZgGXT.js.map +1 -0
  96. package/dist/mcp/mcp-tools.d.ts +8 -0
  97. package/dist/metrics/index.d.ts +18 -0
  98. package/dist/metrics/pool-wait.d.ts +50 -0
  99. package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
  100. package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
  101. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
  102. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
  103. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-BqoUh4Ij.js} +365 -81
  104. package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
  105. package/dist/policy-PdzX-Opd.js +121 -0
  106. package/dist/policy-PdzX-Opd.js.map +1 -0
  107. package/dist/project_manifest-R6y_KHms.js +59 -0
  108. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  109. package/dist/{query-parser-BgiKJKvc.js → query-parser-DK9eMkb9.js} +510 -15
  110. package/dist/query-parser-DK9eMkb9.js.map +1 -0
  111. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  112. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  113. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  114. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  115. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  116. package/dist/schema-edit/local-source.d.ts +29 -0
  117. package/dist/schema-edit/remote-source.d.ts +13 -3
  118. package/dist/schema-edit/source-editor.d.ts +28 -0
  119. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-BFf3i8jm.js} +53 -9
  120. package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
  121. package/dist/services/outbound-post.d.ts +50 -0
  122. package/dist/services/outbound-url-guard.d.ts +34 -5
  123. package/dist/services/routed-realtime-service.d.ts +5 -0
  124. package/dist/services/webhook-service.d.ts +66 -14
  125. package/dist/soft-delete-params-47yBGs3_.js +668 -0
  126. package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
  127. package/dist/sql-export-routes-BNNuGGLP.js +240 -0
  128. package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
  129. package/dist/src-CatHFUym.js.map +1 -1
  130. package/dist/storage/GCSStorageController.d.ts +46 -2
  131. package/dist/storage/LocalStorageController.d.ts +67 -2
  132. package/dist/storage/S3StorageController.d.ts +37 -3
  133. package/dist/storage/image-transform.d.ts +5 -0
  134. package/dist/storage/maintenance-routes.d.ts +23 -0
  135. package/dist/storage/maintenance.d.ts +126 -0
  136. package/dist/storage/rendition-cache.d.ts +9 -11
  137. package/dist/storage/request-principals.d.ts +30 -0
  138. package/dist/storage/routes.d.ts +20 -0
  139. package/dist/storage/source-variables.d.ts +10 -0
  140. package/dist/storage/streams.d.ts +42 -0
  141. package/dist/storage/tus-handler.d.ts +46 -2
  142. package/dist/storage/types.d.ts +82 -7
  143. package/dist/storage/upload-validation.d.ts +13 -0
  144. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  145. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  146. package/dist/strings.es-BeZWAodn.js +256 -0
  147. package/dist/strings.es-BeZWAodn.js.map +1 -0
  148. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  149. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  150. package/dist/utils/byte-size.d.ts +10 -0
  151. package/dist/utils/compression.d.ts +6 -0
  152. package/dist/utils/load-failure.d.ts +14 -0
  153. package/package.json +5 -5
  154. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  155. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  156. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  157. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  158. package/dist/auth-DMLngxn_.js.map +0 -1
  159. package/dist/backup-CN0s50D2.js.map +0 -1
  160. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  161. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  162. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  163. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  164. package/dist/cron-store-D2Q9-Aco.js +0 -300
  165. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  166. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  167. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  168. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  169. package/dist/function-routes-CaNG4waN.js.map +0 -1
  170. package/dist/jobs-DqYNfquG.js.map +0 -1
  171. package/dist/jwt-R6bSPMjk.js.map +0 -1
  172. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  173. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  174. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  175. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  176. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  177. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  178. package/dist/src-I3aG1PcY.js +0 -6870
  179. package/dist/src-I3aG1PcY.js.map +0 -1
  180. package/dist/types-BfKcm9do.js +0 -39
  181. package/dist/types-BfKcm9do.js.map +0 -1
@@ -14,9 +14,13 @@ interface ShutdownConfig {
14
14
  rlsAudit?: {
15
15
  stop(): void;
16
16
  };
17
- /** The expired-token sweep's timer. */
17
+ /** The expired-token sweep: its timer, and the tick it may have running. */
18
18
  authTokenSweep?: {
19
- stop(): void;
19
+ stop(): Promise<void> | void;
20
+ };
21
+ /** The history retention sweep: its timer, and the sweep it may have running. */
22
+ historyRetentionSweep?: {
23
+ stop(): Promise<void> | void;
20
24
  };
21
25
  /** The stop `MetricsHistory.start()` returned: its interval writes to the pool. */
22
26
  stopMetricsSampler?: () => void;
@@ -27,6 +31,12 @@ interface ShutdownConfig {
27
31
  * one of these left open held the shutdown to its force timeout.
28
32
  */
29
33
  closeLongLivedResponses?: () => void;
34
+ /**
35
+ * The email service, when one is configured. A pooled SMTP transport keeps
36
+ * its connections open between sends; one with a `close()` is closed once
37
+ * nothing else can send, rather than left for its idle timer.
38
+ */
39
+ emailService?: object;
30
40
  }
31
41
  /**
32
42
  * Minimal structural view of the backend instance needed by
package/dist/init.d.ts CHANGED
@@ -23,7 +23,15 @@ export interface LiveSchemaConfig extends SchemaEditPolicy {
23
23
  * read from the repository, rewritten, and committed back through the Git
24
24
  * Data API — no clone, no working tree.
25
25
  *
26
- * Absent, live schema editing needs the project mounted and says so.
26
+ * Configuring it is the opt-in for a deployment with no source of its own:
27
+ * live editing then works under `NODE_ENV=production` and with
28
+ * `schemaEditor: false` (which a bundle boots with), because the write
29
+ * goes to this repository and never to the files beside the process. The
30
+ * collections directory is read from it whole, so a relation can name
31
+ * another collection and a deletion can check its importers.
32
+ *
33
+ * Absent, live schema editing needs the project mounted and says so. A
34
+ * bundle sets it from `REBASE_LIVE_SCHEMA_REPOSITORY` and its credential.
27
35
  */
28
36
  repository?: RemoteRepositoryConfig;
29
37
  }
@@ -377,10 +385,12 @@ export interface RebaseBackendConfig {
377
385
  /**
378
386
  * Live schema editing — changing the database and committing the change.
379
387
  *
380
- * Distinct from `schemaEditor`, which only rewrites collection source.
381
- * There is nothing to switch on here: the surface mounts whenever there is
382
- * a `collectionsDir`, and refuses in its own words when it cannot act. What
383
- * this configures is *who* may act.
388
+ * Distinct from `schemaEditor`, which only rewrites collection source on
389
+ * this machine. Without a `repository`, live editing writes through that
390
+ * editor, so it is on exactly where the editor is. With one, it is on
391
+ * whatever `schemaEditor` and `NODE_ENV` say — that is the opt-in for a
392
+ * built bundle, which has no source to edit locally (see
393
+ * {@link LiveSchemaConfig.repository}). Either way it is admin-only.
384
394
  */
385
395
  liveSchema?: LiveSchemaConfig;
386
396
  /** Options that only apply when collections are derived from the database. */
@@ -835,7 +845,7 @@ export interface RebaseBackendInstance {
835
845
  * ```ts
836
846
  * const { jobQueue } = await initializeRebaseBackend({ jobs: { enabled: true } });
837
847
  * const dispatcher = new WebhookDispatcher({ jobQueue });
838
- * jobQueue?.register(WEBHOOK_DELIVERY_TASK, ctx => dispatcher.deliverQueuedJob(ctx.payload));
848
+ * jobQueue?.register(WEBHOOK_DELIVERY_TASK, ctx => dispatcher.deliverQueuedJob(ctx.payload, ctx));
839
849
  * ```
840
850
  */
841
851
  jobQueue?: import("./jobs/index.js").JobQueue;
@@ -0,0 +1,136 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ //#region ../common/src/util/internal-tables.ts
6
+ /**
7
+ * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
8
+ * end-user role away from them.
9
+ *
10
+ * ## Why this exists
11
+ *
12
+ * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
13
+ * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
14
+ * in the schemas a project uses — including `rebase`, because a project's own
15
+ * collections are allowed to live there (the scaffold puts `users` there). It
16
+ * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
17
+ * migrating role inherits the same grant.
18
+ *
19
+ * Every framework-internal table is created later: auth's tables come up during
20
+ * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
21
+ * job registers, `idempotency_keys` on the first request that carries a key. So
22
+ * they all inherited full DML for the end-user role — and none of them enables
23
+ * row-level security, because none of them is a collection with
24
+ * `securityRules`. Measured on a freshly provisioned database, `SET ROLE
25
+ * rebase_user` could read `rebase.refresh_tokens` (session token hashes),
26
+ * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
27
+ * `rebase.api_keys` (including its `admin` flag), and insert into
28
+ * `rebase.app_config`.
29
+ *
30
+ * Nothing routes a user-context query at those tables today, so this was not
31
+ * reachable over the API. That is the wrong thing to depend on: the documented
32
+ * model is that RLS is the authorization boundary, and these tables sat outside
33
+ * it. The boundary is now a privilege boundary instead — the role simply cannot
34
+ * address them.
35
+ *
36
+ * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
37
+ *
38
+ * RLS with no policy denies every row, which is the same outcome, but it is the
39
+ * *weaker* statement: it leaves the grant in place, so a later policy — or a
40
+ * `FORCE` flag cleared by some future migration — reopens the table. There is no
41
+ * row of `refresh_tokens` any end user should ever reach, so the honest encoding
42
+ * is "this role has no privilege here at all". It also keeps the owner
43
+ * connection (which auth actually runs on) completely unaffected.
44
+ *
45
+ * ## Keeping it true
46
+ *
47
+ * `packages/rls-check` scans the `rebase` schema — it used to skip it as a
48
+ * "platform" schema — and its `rls-disabled` check fires on exactly the
49
+ * condition this module removes: RLS off *and* a DML grant to a reachable role.
50
+ * So a table added here without a revoke is caught by `pnpm rls:check`, not by
51
+ * someone re-reading this file.
52
+ */
53
+ /**
54
+ * The Postgres role authenticated requests run as.
55
+ *
56
+ * Defined here rather than in the Postgres driver because both the driver (which
57
+ * provisions the role) and this module (which revokes on its behalf) need it,
58
+ * and a second spelling of a role name is a silent no-op waiting to happen.
59
+ */
60
+ var REBASE_USER_ROLE = "rebase_user";
61
+ /** Postgres identifiers this module is willing to interpolate. */
62
+ var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
63
+ /**
64
+ * A single statement that takes every privilege on `schema.table` away from the
65
+ * end-user role.
66
+ *
67
+ * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
68
+ * happen in practice:
69
+ *
70
+ * - the role does not exist when the connection is unprivileged (Rebase then
71
+ * relies on native RLS rather than a role switch), and a bare `REVOKE` on a
72
+ * missing role is an error, not a no-op;
73
+ * - the table may not exist yet — `cron_logs` never appears in a project with
74
+ * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
75
+ *
76
+ * The third guard is the one that decides whether the *right* table is being
77
+ * revoked. The names in {@link REBASE_INTERNAL_TABLES} are unqualified, and the
78
+ * boot-time sweep in `ensureAppRole` applies all of them to every schema a
79
+ * project uses — including the schema its own collections live in. A project is
80
+ * free to call a collection `jobs`, `branches` or `api_keys`, and when it does,
81
+ * the sweep was revoking `rebase_user`'s DML on the project's table on every
82
+ * single boot. That is not a subtle degradation: the collection's whole API
83
+ * answers 500 `permission denied for table …` from then on, which is what
84
+ * happened to a public job board whose vacancies live in `public.jobs`.
85
+ *
86
+ * `relrowsecurity` separates the two cleanly, and it is the same fact this
87
+ * module already relies on. Framework-internal tables carry no RLS — that is the
88
+ * premise stated at the top of this file, and the reason a revoke is needed at
89
+ * all. Every collection table has it enabled, because that is how Rebase
90
+ * enforces `securityRules`. So "RLS is off" is exactly "this is not somebody's
91
+ * collection", and the guard also subsumes the hand-carved `users` exception:
92
+ * the auth user table is a collection, has RLS, and would now be skipped on its
93
+ * own merits rather than by being kept off a list.
94
+ *
95
+ * The fourth guard is about cost, and leaves the end state exactly as it was:
96
+ * the `REVOKE` runs only when it would change something. A `REVOKE` rewrites the
97
+ * table's `pg_class` row whether or not anything was granted, which invalidates
98
+ * every cached plan that touches the table on every connection. Every store
99
+ * that owns an internal table runs this on every boot. So it is skipped when the
100
+ * end-user role holds nothing on the table — no entry in its ACL or in any
101
+ * column's ACL — and the ACL has been written. A table whose ACL has never been
102
+ * written (`relacl` NULL) is still revoked once: the `REVOKE` writes the owner's
103
+ * default ACL out, exactly as before, and from then on it is skipped.
104
+ *
105
+ * One command, so it is safe on handles that speak the extended query protocol
106
+ * and reject multi-statement strings.
107
+ */
108
+ function revokeInternalTableSql(schema, table) {
109
+ if (!SAFE_IDENTIFIER.test(schema)) throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);
110
+ if (!SAFE_IDENTIFIER.test(table)) throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);
111
+ const qualified = `"${schema}"."${table}"`;
112
+ const role = `(SELECT oid FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')`;
113
+ return `
114
+ DO $rebase_revoke$
115
+ BEGIN
116
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
117
+ AND to_regclass('${qualified}') IS NOT NULL
118
+ AND NOT (SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass('${qualified}'))
119
+ AND (
120
+ (SELECT relacl IS NULL FROM pg_class WHERE oid = to_regclass('${qualified}'))
121
+ OR EXISTS (SELECT 1 FROM pg_class c, aclexplode(c.relacl) a
122
+ WHERE c.oid = to_regclass('${qualified}') AND a.grantee = ${role})
123
+ OR EXISTS (SELECT 1 FROM pg_attribute att, aclexplode(att.attacl) a
124
+ WHERE att.attrelid = to_regclass('${qualified}') AND att.attacl IS NOT NULL
125
+ AND a.grantee = ${role})
126
+ ) THEN
127
+ EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
128
+ END IF;
129
+ END
130
+ $rebase_revoke$;
131
+ `.trim();
132
+ }
133
+ //#endregion
134
+ export { revokeInternalTableSql as n, REBASE_USER_ROLE as t };
135
+
136
+ //# sourceMappingURL=internal-tables-L2t39RdT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal-tables-L2t39RdT.js","names":[],"sources":["../../common/src/util/internal-tables.ts"],"sourcesContent":["/**\n * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the\n * end-user role away from them.\n *\n * ## Why this exists\n *\n * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role\n * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table\n * in the schemas a project uses — including `rebase`, because a project's own\n * collections are allowed to live there (the scaffold puts `users` there). It\n * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the\n * migrating role inherits the same grant.\n *\n * Every framework-internal table is created later: auth's tables come up during\n * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first\n * job registers, `idempotency_keys` on the first request that carries a key. So\n * they all inherited full DML for the end-user role — and none of them enables\n * row-level security, because none of them is a collection with\n * `securityRules`. Measured on a freshly provisioned database, `SET ROLE\n * rebase_user` could read `rebase.refresh_tokens` (session token hashes),\n * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and\n * `rebase.api_keys` (including its `admin` flag), and insert into\n * `rebase.app_config`.\n *\n * Nothing routes a user-context query at those tables today, so this was not\n * reachable over the API. That is the wrong thing to depend on: the documented\n * model is that RLS is the authorization boundary, and these tables sat outside\n * it. The boundary is now a privilege boundary instead — the role simply cannot\n * address them.\n *\n * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY\n *\n * RLS with no policy denies every row, which is the same outcome, but it is the\n * *weaker* statement: it leaves the grant in place, so a later policy — or a\n * `FORCE` flag cleared by some future migration — reopens the table. There is no\n * row of `refresh_tokens` any end user should ever reach, so the honest encoding\n * is \"this role has no privilege here at all\". It also keeps the owner\n * connection (which auth actually runs on) completely unaffected.\n *\n * ## Keeping it true\n *\n * `packages/rls-check` scans the `rebase` schema — it used to skip it as a\n * \"platform\" schema — and its `rls-disabled` check fires on exactly the\n * condition this module removes: RLS off *and* a DML grant to a reachable role.\n * So a table added here without a revoke is caught by `pnpm rls:check`, not by\n * someone re-reading this file.\n */\n\n/**\n * The Postgres role authenticated requests run as.\n *\n * Defined here rather than in the Postgres driver because both the driver (which\n * provisions the role) and this module (which revokes on its behalf) need it,\n * and a second spelling of a role name is a silent no-op waiting to happen.\n */\nexport const REBASE_USER_ROLE = \"rebase_user\";\n\n/**\n * Framework-internal table names, unqualified.\n *\n * Deliberately NOT including `users`: the auth user table is also a collection,\n * with `securityRules`, RLS enabled and policies applied. Users read their own\n * row through it — revoking there would break sign-in. `revokeInternalTableSql`\n * now skips any table with RLS enabled, so that exception is enforced rather\n * than merely remembered — and so is the same hazard for every other name here,\n * any of which a project may legitimately use for a collection of its own.\n *\n * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`\n * because `db migrate apply` passes `--revisions-schema rebase`.\n *\n * Every entry here must also be revoked by whatever creates it, and vice versa:\n * the creation-time revoke fires once, on the boot that first makes the table,\n * so it cannot help a database provisioned before that revoke existed. This\n * list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep\n * is the only thing that can repair an already-granted table. A table revoked\n * at creation but missing here is therefore permanently stranded on any\n * database that predates its revoke.\n *\n * These names are unqualified, and the boot-time sweep applies them to every\n * schema a project uses — so an entry here is a claim on that name in `public`\n * as much as in `rebase`. `jobs` is Rebase's queue at `rebase.jobs` AND a\n * perfectly ordinary collection name, and revoking `public.jobs` from a project\n * that owns it leaves every read failing 42501 with correct policies applied\n * and nothing in the RLS logs to explain it. The `relrowsecurity` guard in\n * `revokeInternalTableSql` is what makes a common noun safe here — it is not a\n * licence to claim more of them.\n */\nexport const REBASE_INTERNAL_TABLES: readonly string[] = [\n // auth\n \"user_identities\",\n \"refresh_tokens\",\n \"password_reset_tokens\",\n \"magic_link_tokens\",\n \"mfa_factors\",\n \"mfa_challenges\",\n \"recovery_codes\",\n \"app_config\",\n \"schema_meta\",\n // platform services\n \"api_keys\",\n \"cron_logs\",\n \"cron_claims\",\n \"cron_job_state\",\n \"jobs\",\n \"rate_limit_hits\",\n \"idempotency_keys\",\n \"entity_history\",\n \"branches\",\n \"metric_samples\",\n // realtime channels — authorization for these lives in the channel rules the\n // server evaluates before it reads or writes, never in a row policy\n \"channel_messages\",\n \"channel_cursors\",\n \"channel_presence\",\n // migration bookkeeping\n \"atlas_schema_revisions\"\n];\n\n/** Postgres identifiers this module is willing to interpolate. */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\n/**\n * A single statement that takes every privilege on `schema.table` away from the\n * end-user role.\n *\n * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which\n * happen in practice:\n *\n * - the role does not exist when the connection is unprivileged (Rebase then\n * relies on native RLS rather than a role switch), and a bare `REVOKE` on a\n * missing role is an error, not a no-op;\n * - the table may not exist yet — `cron_logs` never appears in a project with\n * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.\n *\n * The third guard is the one that decides whether the *right* table is being\n * revoked. The names in {@link REBASE_INTERNAL_TABLES} are unqualified, and the\n * boot-time sweep in `ensureAppRole` applies all of them to every schema a\n * project uses — including the schema its own collections live in. A project is\n * free to call a collection `jobs`, `branches` or `api_keys`, and when it does,\n * the sweep was revoking `rebase_user`'s DML on the project's table on every\n * single boot. That is not a subtle degradation: the collection's whole API\n * answers 500 `permission denied for table …` from then on, which is what\n * happened to a public job board whose vacancies live in `public.jobs`.\n *\n * `relrowsecurity` separates the two cleanly, and it is the same fact this\n * module already relies on. Framework-internal tables carry no RLS — that is the\n * premise stated at the top of this file, and the reason a revoke is needed at\n * all. Every collection table has it enabled, because that is how Rebase\n * enforces `securityRules`. So \"RLS is off\" is exactly \"this is not somebody's\n * collection\", and the guard also subsumes the hand-carved `users` exception:\n * the auth user table is a collection, has RLS, and would now be skipped on its\n * own merits rather than by being kept off a list.\n *\n * The fourth guard is about cost, and leaves the end state exactly as it was:\n * the `REVOKE` runs only when it would change something. A `REVOKE` rewrites the\n * table's `pg_class` row whether or not anything was granted, which invalidates\n * every cached plan that touches the table on every connection. Every store\n * that owns an internal table runs this on every boot. So it is skipped when the\n * end-user role holds nothing on the table — no entry in its ACL or in any\n * column's ACL — and the ACL has been written. A table whose ACL has never been\n * written (`relacl` NULL) is still revoked once: the `REVOKE` writes the owner's\n * default ACL out, exactly as before, and from then on it is skipped.\n *\n * One command, so it is safe on handles that speak the extended query protocol\n * and reject multi-statement strings.\n */\nexport function revokeInternalTableSql(schema: string, table: string): string {\n if (!SAFE_IDENTIFIER.test(schema)) {\n throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);\n }\n if (!SAFE_IDENTIFIER.test(table)) {\n throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);\n }\n const qualified = `\"${schema}\".\"${table}\"`;\n const role = `(SELECT oid FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')`;\n return `\n DO $rebase_revoke$\n BEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')\n AND to_regclass('${qualified}') IS NOT NULL\n AND NOT (SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass('${qualified}'))\n AND (\n (SELECT relacl IS NULL FROM pg_class WHERE oid = to_regclass('${qualified}'))\n OR EXISTS (SELECT 1 FROM pg_class c, aclexplode(c.relacl) a\n WHERE c.oid = to_regclass('${qualified}') AND a.grantee = ${role})\n OR EXISTS (SELECT 1 FROM pg_attribute att, aclexplode(att.attacl) a\n WHERE att.attrelid = to_regclass('${qualified}') AND att.attacl IS NOT NULL\n AND a.grantee = ${role})\n ) THEN\n EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';\n END IF;\n END\n $rebase_revoke$;\n `.trim();\n}\n\n/**\n * Revoke on every internal table in `schema`, one statement at a time.\n *\n * Best-effort per table: a connection that does not own one of them (a\n * pre-provisioned database, a platform-managed ledger) cannot revoke on it, and\n * that must not take down a boot. The caller decides how loud to be — `onError`\n * exists so the driver can warn without this module importing a logger.\n */\nexport async function revokeInternalTableAccess(\n execute: (sql: string) => Promise<unknown>,\n schema: string,\n options?: { tables?: readonly string[]; onError?: (table: string, error: unknown) => void }\n): Promise<void> {\n for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) {\n try {\n await execute(revokeInternalTableSql(schema, table));\n } catch (error) {\n options?.onError?.(table, error);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,mBAAmB;;AAgEhC,IAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CxB,SAAgB,uBAAuB,QAAgB,OAAuB;CAC1E,IAAI,CAAC,gBAAgB,KAAK,MAAM,GAC5B,MAAM,IAAI,MAAM,qDAAqD,KAAK,UAAU,MAAM,GAAG;CAEjG,IAAI,CAAC,gBAAgB,KAAK,KAAK,GAC3B,MAAM,IAAI,MAAM,oDAAoD,KAAK,UAAU,KAAK,GAAG;CAE/F,MAAM,YAAY,IAAI,OAAO,KAAK,MAAM;CACxC,MAAM,OAAO,8CAA8C,iBAAiB;CAC5E,OAAO;;;iEAGsD,iBAAiB;kCAChD,UAAU;uFAC2C,UAAU;;mFAEd,UAAU;;2DAElC,UAAU,qBAAqB,KAAK;;kEAE7B,UAAU;kDAC1B,KAAK;;yCAEd,UAAU,QAAQ,iBAAiB;;;;MAItE,KAAK;AACX"}
@@ -1,5 +1,13 @@
1
1
  import type { JobStore } from "./job-store.js";
2
2
  import type { JobHandler, JobQueueClient, JobQueueOptions } from "./types.js";
3
+ /**
4
+ * How long one attempt may run when `jobs.timeoutMs` is left out. Without a
5
+ * bound, a handler awaiting a socket that never answers held its worker slot
6
+ * and its claim for the life of the process, with `/health` green. Fifteen
7
+ * minutes is far past any job that is merely slow, and a job meant to run
8
+ * longer says so with its own `timeoutMs` — or `0`, for no limit.
9
+ */
10
+ export declare const DEFAULT_JOB_TIMEOUT_MS: number;
3
11
  /** 1s, 5s, 25s, 125s … capped at an hour. */
4
12
  export declare function defaultBackoff(attempt: number): number;
5
13
  /**
@@ -1,5 +1,13 @@
1
1
  import type { DataDriver } from "@rebasepro/types";
2
2
  import type { JobRecord } from "./types.js";
3
+ /**
4
+ * The `NOTIFY` channel an insert announces itself on, with the job's `run_at`
5
+ * in epoch milliseconds as the payload. Workers listening on it are woken
6
+ * instead of polling; Postgres delivers it when the insert commits, and not at
7
+ * all if it rolls back. Frozen: workers of the previous release listen on it
8
+ * during a rolling deploy.
9
+ */
10
+ export declare const JOBS_NOTIFY_CHANNEL = "rebase_jobs";
3
11
  /**
4
12
  * Which claim an outcome belongs to: the worker holding the job, and the
5
13
  * attempt that claim spent.
@@ -49,6 +57,22 @@ export interface JobStore {
49
57
  heartbeat?(id: string, claim: JobClaim): Promise<boolean>;
50
58
  /** Return jobs stranded by a worker that died holding them. Resolves with how many. */
51
59
  reapExpired(visibilityTimeoutMs: number): Promise<number>;
60
+ /**
61
+ * Call `onWake` whenever any process inserts a job, with the job's `run_at`
62
+ * in epoch milliseconds. Resolves, once listening, with the stop; rejects
63
+ * when this database cannot notify this process — no session connection
64
+ * to hold a `LISTEN` on — and the worker polls instead. `onReconnect` is
65
+ * called after a dropped connection is back: what was sent meanwhile is
66
+ * lost. A store without it leaves the worker polling.
67
+ */
68
+ subscribe?(onWake: (runAtMs?: number) => void, onReconnect: () => void): Promise<() => Promise<void>>;
69
+ /**
70
+ * Delete finished jobs past retention — succeeded ones after
71
+ * {@link SUCCEEDED_RETENTION_DAYS} days, failed ones after
72
+ * {@link FAILED_RETENTION_DAYS} — in batches. Resolves with how many went.
73
+ * The worker runs it on a timer; a store without it keeps every row.
74
+ */
75
+ sweepExpired?(): Promise<number>;
52
76
  fetch(id: string): Promise<JobRecord | null>;
53
77
  }
54
78
  export declare function createJobStore(driver: DataDriver): JobStore | undefined;
@@ -62,6 +62,13 @@ export interface JobContext<P = unknown> {
62
62
  */
63
63
  attempt: number;
64
64
  maxAttempts: number;
65
+ /**
66
+ * Aborted when the attempt runs past the queue's `timeoutMs`. Pass it to
67
+ * `fetch` and anything else that takes one: the attempt is failed and its
68
+ * worker slot freed at the deadline either way, but a handler that ignores
69
+ * the signal keeps running in the background, unrecorded.
70
+ */
71
+ signal: AbortSignal;
65
72
  }
66
73
  /**
67
74
  * A handler. Throwing is how a job fails — there is no `return false`, because
@@ -89,16 +96,21 @@ export interface EnqueueOptions {
89
96
  /** How the worker behaves. */
90
97
  export interface JobQueueOptions {
91
98
  /**
92
- * Off unless asked for. A worker polls the database forever, and turning
93
- * that on for every backend that happens to run on Postgres is not a
94
- * default anyone chose.
99
+ * Off unless asked for. A worker looks for work for as long as the process
100
+ * lives, and turning that on for every backend that happens to run on
101
+ * Postgres is not a default anyone chose.
95
102
  */
96
103
  enabled?: boolean;
97
104
  /** Named handlers. A job whose `task` is not here is left alone, not failed — see the worker. */
98
105
  tasks?: Record<string, JobHandler<never>>;
99
106
  /** How many jobs one instance runs at once. Default 5. */
100
107
  concurrency?: number;
101
- /** How often to look for work when the last look found none. Default 2000ms. */
108
+ /**
109
+ * The wait after a look for work that found none — doubled after each
110
+ * further one, up to 30 seconds, and reset whenever there is work. Only
111
+ * where the database cannot wake the worker with `NOTIFY`; where it can,
112
+ * an idle worker looks every 30 seconds as a safety net. Default 2000ms.
113
+ */
102
114
  pollIntervalMs?: number;
103
115
  /**
104
116
  * How long a claim may go unrenewed before another worker may take the job.
@@ -113,6 +125,15 @@ export interface JobQueueOptions {
113
125
  * the job when a run finishes gets to record its outcome.
114
126
  */
115
127
  visibilityTimeoutMs?: number;
128
+ /**
129
+ * How long one attempt may run before it is failed (and retried while
130
+ * attempts remain), in milliseconds. The handler's `signal` is aborted at
131
+ * the deadline and its worker slot is freed. Default: 15 minutes
132
+ * (900000), named in one log line when the worker starts. `0` is no
133
+ * limit — a handler that never settles then keeps its slot, and its
134
+ * claim, for as long as the process lives.
135
+ */
136
+ timeoutMs?: number;
116
137
  /** Attempts before a job is left `failed`. Default 3. */
117
138
  maxAttempts?: number;
118
139
  /**