@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/GCSStorageController-fv6JAZ87.js +402 -0
- package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
- package/dist/S3StorageController-BNg-pXiY.js +480 -0
- package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
- package/dist/api/mount-path.d.ts +30 -0
- package/dist/api/mount.d.ts +6 -13
- package/dist/api/rest/api-generator.d.ts +53 -1
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +7 -5
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +35 -0
- package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
- package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
- package/dist/auth/access.d.ts +57 -8
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +1 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
- package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
- package/dist/auth/builtin-auth-adapter.d.ts +13 -0
- package/dist/auth/email-change-routes.d.ts +3 -0
- package/dist/auth/expired-token-sweep.d.ts +5 -1
- package/dist/auth/interfaces.d.ts +10 -0
- package/dist/auth/jwks-routes.d.ts +5 -1
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +282 -166
- package/dist/auth/magic-link-routes.d.ts +3 -0
- package/dist/auth/mfa-crypto.d.ts +9 -3
- package/dist/auth/mfa-gate.d.ts +4 -2
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/otp-routes.d.ts +3 -0
- package/dist/auth/rate-limiter.d.ts +61 -34
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/routes.d.ts +16 -0
- package/dist/auth/session-routes.d.ts +6 -0
- package/dist/auth/user-picking.d.ts +37 -0
- package/dist/auth/verify-credential.d.ts +25 -4
- package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
- package/dist/auth-GnONzteu.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend/create-backend.d.ts +281 -0
- package/dist/backend/default-backend.d.ts +35 -0
- package/dist/backend/options.d.ts +912 -0
- package/dist/backend/scope.d.ts +23 -0
- package/dist/backend/services.d.ts +54 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +14 -3
- package/dist/backup/backup-routes.d.ts +23 -2
- package/dist/backup/backup-storage.d.ts +3 -1
- package/dist/backup/index.d.ts +3 -1
- package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
- package/dist/backup-BnvkSLHi.js.map +1 -0
- package/dist/boot/boot.d.ts +2 -2
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +9 -2
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/provision.d.ts +4 -2
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/sources.d.ts +2 -0
- package/dist/boot/static-routing.d.ts +9 -0
- package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
- package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
- package/dist/cron/cron-loader.d.ts +77 -18
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +69 -3
- package/dist/cron/cron-store.d.ts +33 -5
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +3 -3
- package/dist/cron-loader-B-u0snqq.js +202 -0
- package/dist/cron-loader-B-u0snqq.js.map +1 -0
- package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
- package/dist/cron-routes-OD6BstUj.js.map +1 -0
- package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
- package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
- package/dist/cron-store-CZ30ZBi-.js +577 -0
- package/dist/cron-store-CZ30ZBi-.js.map +1 -0
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/env.d.ts +0 -37
- package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
- package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
- package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
- package/dist/function-loader-iKhOZatS.js.map +1 -0
- package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
- package/dist/function-routes-j9TwpvQe.js.map +1 -0
- package/dist/functions/context.d.ts +9 -0
- package/dist/functions/define-function.d.ts +10 -6
- package/dist/functions/function-loader.d.ts +75 -18
- package/dist/functions/function-routes.d.ts +2 -1
- package/dist/functions/index.d.ts +5 -4
- package/dist/functions/index.js +42 -13
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/internal.d.ts +2 -2
- package/dist/functions/proxy.d.ts +6 -2
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
- package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
- package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +23 -10
- package/dist/index.es.js +24423 -20038
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +27 -10
- package/dist/init/process-safety.d.ts +7 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +49 -865
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
- package/dist/jobs-BCbflZj2.js.map +1 -0
- package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
- package/dist/jwt-BlvuKoR6.js.map +1 -0
- package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
- package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
- package/dist/load-failure-L5hvfDoG.js +80 -0
- package/dist/load-failure-L5hvfDoG.js.map +1 -0
- package/dist/mcp/mcp-routes.d.ts +6 -0
- package/dist/mcp/mcp-tools.d.ts +8 -0
- package/dist/mcp/oauth-routes.d.ts +7 -0
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
- package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
- package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
- package/dist/proxy-CkTlMGmh.js.map +1 -0
- package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
- package/dist/query-parser-4WkMoA_K.js.map +1 -0
- package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +10 -0
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/remote-source.d.ts +13 -3
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
- package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +5 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/singleton.d.ts +9 -5
- package/dist/soft-delete-params-DfV3vVN5.js +668 -0
- package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
- package/dist/sql-export-routes-ClRwzIsC.js +240 -0
- package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
- package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
- package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +46 -2
- package/dist/storage/LocalStorageController.d.ts +67 -2
- package/dist/storage/S3StorageController.d.ts +37 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/routes.d.ts +20 -0
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +46 -2
- package/dist/storage/types.d.ts +82 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-BSiP1c-f.js +0 -256
- package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
- package/dist/S3StorageController-CAwFRgjV.js +0 -276
- package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
- package/dist/auth-DMLngxn_.js.map +0 -1
- package/dist/backup-CN0s50D2.js.map +0 -1
- package/dist/contract-routes-fz8i4pxs.js.map +0 -1
- package/dist/cron-loader-CwaANlOG.js +0 -118
- package/dist/cron-loader-CwaANlOG.js.map +0 -1
- package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
- package/dist/cron-scheduler-CYQgco86.js.map +0 -1
- package/dist/cron-store-D2Q9-Aco.js +0 -300
- package/dist/cron-store-D2Q9-Aco.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/function-loader-D7o5Epjj.js.map +0 -1
- package/dist/function-routes-CaNG4waN.js.map +0 -1
- package/dist/jobs-DqYNfquG.js.map +0 -1
- package/dist/jwt-R6bSPMjk.js.map +0 -1
- package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
- package/dist/proxy-qRlqeUmO.js.map +0 -1
- package/dist/query-parser-BgiKJKvc.js.map +0 -1
- package/dist/request-timeout-DgH7j8qO.js.map +0 -1
- package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
- package/dist/soft-delete-params-BWPilMPF.js +0 -59
- package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
- package/dist/src-I3aG1PcY.js +0 -6870
- package/dist/src-I3aG1PcY.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
|
@@ -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"}
|
package/dist/jobs/job-queue.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/jobs/job-store.d.ts
CHANGED
|
@@ -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;
|
package/dist/jobs/types.d.ts
CHANGED
|
@@ -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
|
|
93
|
-
* that on for every backend that happens to run on
|
|
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
|
-
/**
|
|
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
|
/**
|