@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +27 -2
  2. package/dist/GCSStorageController-fv6JAZ87.js +402 -0
  3. package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
  4. package/dist/S3StorageController-BNg-pXiY.js +480 -0
  5. package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
  6. package/dist/api/mount-path.d.ts +30 -0
  7. package/dist/api/mount.d.ts +6 -13
  8. package/dist/api/rest/api-generator.d.ts +53 -1
  9. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  10. package/dist/api/rest/field-access-query.d.ts +28 -8
  11. package/dist/api/rest/field-ops.d.ts +19 -7
  12. package/dist/api/rest/query-body.d.ts +10 -0
  13. package/dist/api/rest/write-validation.d.ts +7 -5
  14. package/dist/api/schema-editor-routes.d.ts +10 -1
  15. package/dist/api/sql-export-routes.d.ts +8 -0
  16. package/dist/api/types.d.ts +35 -0
  17. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
  18. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
  19. package/dist/auth/access.d.ts +57 -8
  20. package/dist/auth/admin-user-ops.d.ts +24 -2
  21. package/dist/auth/admin-users-route.d.ts +1 -0
  22. package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
  23. package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
  24. package/dist/auth/builtin-auth-adapter.d.ts +13 -0
  25. package/dist/auth/email-change-routes.d.ts +3 -0
  26. package/dist/auth/expired-token-sweep.d.ts +5 -1
  27. package/dist/auth/interfaces.d.ts +10 -0
  28. package/dist/auth/jwks-routes.d.ts +5 -1
  29. package/dist/auth/jwt-crypto.d.ts +7 -0
  30. package/dist/auth/jwt-keys.d.ts +4 -0
  31. package/dist/auth/jwt.d.ts +282 -166
  32. package/dist/auth/magic-link-routes.d.ts +3 -0
  33. package/dist/auth/mfa-crypto.d.ts +9 -3
  34. package/dist/auth/mfa-gate.d.ts +4 -2
  35. package/dist/auth/mfa-routes.d.ts +6 -0
  36. package/dist/auth/middleware.d.ts +9 -2
  37. package/dist/auth/otp-routes.d.ts +3 -0
  38. package/dist/auth/rate-limiter.d.ts +61 -34
  39. package/dist/auth/registration-policy.d.ts +17 -0
  40. package/dist/auth/require-auth.d.ts +1 -1
  41. package/dist/auth/routes.d.ts +16 -0
  42. package/dist/auth/session-routes.d.ts +6 -0
  43. package/dist/auth/user-picking.d.ts +37 -0
  44. package/dist/auth/verify-credential.d.ts +25 -4
  45. package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
  46. package/dist/auth-GnONzteu.js.map +1 -0
  47. package/dist/auth_adapter-CIItCTc8.js +21 -0
  48. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  49. package/dist/backend/create-backend.d.ts +281 -0
  50. package/dist/backend/default-backend.d.ts +35 -0
  51. package/dist/backend/options.d.ts +912 -0
  52. package/dist/backend/scope.d.ts +23 -0
  53. package/dist/backend/services.d.ts +54 -0
  54. package/dist/backend-DTAOsLQc.js.map +1 -1
  55. package/dist/backup/backup-common.d.ts +14 -3
  56. package/dist/backup/backup-routes.d.ts +23 -2
  57. package/dist/backup/backup-storage.d.ts +3 -1
  58. package/dist/backup/index.d.ts +3 -1
  59. package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
  60. package/dist/backup-BnvkSLHi.js.map +1 -0
  61. package/dist/boot/boot.d.ts +2 -2
  62. package/dist/boot/driver.d.ts +3 -0
  63. package/dist/boot/env.d.ts +9 -2
  64. package/dist/boot/http-server.d.ts +22 -0
  65. package/dist/boot/options.d.ts +16 -1
  66. package/dist/boot/provision.d.ts +4 -2
  67. package/dist/boot/runtime-cors.d.ts +17 -0
  68. package/dist/boot/sources.d.ts +2 -0
  69. package/dist/boot/static-routing.d.ts +9 -0
  70. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
  71. package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
  72. package/dist/cron/cron-loader.d.ts +77 -18
  73. package/dist/cron/cron-routes.d.ts +1 -1
  74. package/dist/cron/cron-scheduler.d.ts +69 -3
  75. package/dist/cron/cron-store.d.ts +33 -5
  76. package/dist/cron/define-cron.d.ts +6 -2
  77. package/dist/cron/index.d.ts +3 -3
  78. package/dist/cron-loader-B-u0snqq.js +202 -0
  79. package/dist/cron-loader-B-u0snqq.js.map +1 -0
  80. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
  81. package/dist/cron-routes-OD6BstUj.js.map +1 -0
  82. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
  83. package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
  84. package/dist/cron-store-CZ30ZBi-.js +577 -0
  85. package/dist/cron-store-CZ30ZBi-.js.map +1 -0
  86. package/dist/email/smtp-email-service.d.ts +23 -0
  87. package/dist/email/types.d.ts +18 -0
  88. package/dist/env.d.ts +0 -37
  89. package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
  90. package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
  91. package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
  92. package/dist/function-loader-iKhOZatS.js.map +1 -0
  93. package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
  94. package/dist/function-routes-j9TwpvQe.js.map +1 -0
  95. package/dist/functions/context.d.ts +9 -0
  96. package/dist/functions/define-function.d.ts +10 -6
  97. package/dist/functions/function-loader.d.ts +75 -18
  98. package/dist/functions/function-routes.d.ts +2 -1
  99. package/dist/functions/index.d.ts +5 -4
  100. package/dist/functions/index.js +42 -13
  101. package/dist/functions/index.js.map +1 -1
  102. package/dist/functions/internal.d.ts +2 -2
  103. package/dist/functions/proxy.d.ts +6 -2
  104. package/dist/functions/request-timeout.d.ts +7 -5
  105. package/dist/history/history-routes.d.ts +9 -0
  106. package/dist/history/retention-sweep.d.ts +81 -0
  107. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
  108. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
  109. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  110. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  111. package/dist/index.d.ts +23 -10
  112. package/dist/index.es.js +24423 -20038
  113. package/dist/index.es.js.map +1 -1
  114. package/dist/init/docs.d.ts +27 -10
  115. package/dist/init/process-safety.d.ts +7 -2
  116. package/dist/init/shutdown.d.ts +18 -3
  117. package/dist/init.d.ts +49 -865
  118. package/dist/internal-tables-L2t39RdT.js +136 -0
  119. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  120. package/dist/jobs/job-queue.d.ts +8 -0
  121. package/dist/jobs/job-store.d.ts +24 -0
  122. package/dist/jobs/types.d.ts +25 -4
  123. package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
  124. package/dist/jobs-BCbflZj2.js.map +1 -0
  125. package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
  126. package/dist/jwt-BlvuKoR6.js.map +1 -0
  127. package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
  128. package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
  129. package/dist/load-failure-L5hvfDoG.js +80 -0
  130. package/dist/load-failure-L5hvfDoG.js.map +1 -0
  131. package/dist/mcp/mcp-routes.d.ts +6 -0
  132. package/dist/mcp/mcp-tools.d.ts +8 -0
  133. package/dist/mcp/oauth-routes.d.ts +7 -0
  134. package/dist/metrics/index.d.ts +18 -0
  135. package/dist/metrics/pool-wait.d.ts +50 -0
  136. package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
  137. package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
  138. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
  139. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
  140. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
  141. package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
  142. package/dist/policy-PdzX-Opd.js +121 -0
  143. package/dist/policy-PdzX-Opd.js.map +1 -0
  144. package/dist/project_manifest-R6y_KHms.js +59 -0
  145. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  146. package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
  147. package/dist/proxy-CkTlMGmh.js.map +1 -0
  148. package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
  149. package/dist/query-parser-4WkMoA_K.js.map +1 -0
  150. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  151. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  152. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  153. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  154. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  155. package/dist/schema-edit/local-source.d.ts +29 -0
  156. package/dist/schema-edit/remote-source.d.ts +13 -3
  157. package/dist/schema-edit/source-editor.d.ts +28 -0
  158. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
  159. package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
  160. package/dist/services/outbound-post.d.ts +50 -0
  161. package/dist/services/outbound-url-guard.d.ts +34 -5
  162. package/dist/services/routed-realtime-service.d.ts +5 -0
  163. package/dist/services/webhook-service.d.ts +66 -14
  164. package/dist/singleton.d.ts +9 -5
  165. package/dist/soft-delete-params-DfV3vVN5.js +668 -0
  166. package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
  167. package/dist/sql-export-routes-ClRwzIsC.js +240 -0
  168. package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
  169. package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
  170. package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
  171. package/dist/storage/GCSStorageController.d.ts +46 -2
  172. package/dist/storage/LocalStorageController.d.ts +67 -2
  173. package/dist/storage/S3StorageController.d.ts +37 -3
  174. package/dist/storage/image-transform.d.ts +5 -0
  175. package/dist/storage/maintenance-routes.d.ts +23 -0
  176. package/dist/storage/maintenance.d.ts +126 -0
  177. package/dist/storage/rendition-cache.d.ts +9 -11
  178. package/dist/storage/request-principals.d.ts +30 -0
  179. package/dist/storage/routes.d.ts +20 -0
  180. package/dist/storage/source-variables.d.ts +10 -0
  181. package/dist/storage/streams.d.ts +42 -0
  182. package/dist/storage/tus-handler.d.ts +46 -2
  183. package/dist/storage/types.d.ts +82 -7
  184. package/dist/storage/upload-validation.d.ts +13 -0
  185. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  186. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  187. package/dist/strings.es-BeZWAodn.js +256 -0
  188. package/dist/strings.es-BeZWAodn.js.map +1 -0
  189. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  190. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  191. package/dist/utils/byte-size.d.ts +10 -0
  192. package/dist/utils/compression.d.ts +6 -0
  193. package/dist/utils/load-failure.d.ts +14 -0
  194. package/package.json +5 -5
  195. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  196. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  197. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  198. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  199. package/dist/auth-DMLngxn_.js.map +0 -1
  200. package/dist/backup-CN0s50D2.js.map +0 -1
  201. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  202. package/dist/cron-loader-CwaANlOG.js +0 -118
  203. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  204. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  205. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  206. package/dist/cron-store-D2Q9-Aco.js +0 -300
  207. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  208. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  209. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  210. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  211. package/dist/function-routes-CaNG4waN.js.map +0 -1
  212. package/dist/jobs-DqYNfquG.js.map +0 -1
  213. package/dist/jwt-R6bSPMjk.js.map +0 -1
  214. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  215. package/dist/proxy-qRlqeUmO.js.map +0 -1
  216. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  217. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  218. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  219. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  220. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  221. package/dist/src-I3aG1PcY.js +0 -6870
  222. package/dist/src-I3aG1PcY.js.map +0 -1
  223. package/dist/types-BfKcm9do.js +0 -39
  224. package/dist/types-BfKcm9do.js.map +0 -1
@@ -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
  /**