@rebasepro/server 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{GCSStorageController-CRd9bFrc.js → GCSStorageController-40FyFeSx.js} +6 -2
- package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
- package/dist/{S3StorageController-D00EFa5P.js → S3StorageController-Bl5shp94.js} +6 -2
- package/dist/S3StorageController-Bl5shp94.js.map +1 -0
- package/dist/access-BFAUvsSs.js +1897 -0
- package/dist/access-BFAUvsSs.js.map +1 -0
- package/dist/api/branch-routes.d.ts +25 -0
- package/dist/api/live-schema-routes.d.ts +10 -0
- package/dist/api/rest/api-generator.d.ts +9 -1
- package/dist/api/rest/idempotency.d.ts +15 -1
- package/dist/api/root-error-handler.d.ts +6 -5
- package/dist/api/types.d.ts +12 -0
- package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
- package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
- package/dist/auth/access.d.ts +10 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/adapter-service-key.d.ts +24 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
- package/dist/auth/api-keys/api-key-store.d.ts +8 -1
- package/dist/auth/api-keys/key-grant.d.ts +18 -1
- package/dist/auth/expired-token-sweep.d.ts +5 -0
- package/dist/auth/impersonation.d.ts +3 -1
- package/dist/auth/jwt.d.ts +8 -2
- package/dist/auth/oauth-signin-policy.d.ts +21 -35
- package/dist/auth/rls-scope.d.ts +17 -7
- package/dist/auth/rls-simulator-routes.d.ts +51 -0
- package/dist/auth/simulation-read-only.d.ts +28 -0
- package/dist/auth/simulation.d.ts +141 -0
- package/dist/auth/sql-rate-limit-store.d.ts +15 -1
- package/dist/{auth-Dkvo2tTJ.js → auth-izI7AEzC.js} +370 -1880
- package/dist/auth-izI7AEzC.js.map +1 -0
- package/dist/auth_adapter-B7RUMk6e.js +89 -0
- package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
- package/dist/backend/create-backend.d.ts +6 -0
- package/dist/backend/options.d.ts +9 -4
- package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
- package/dist/backend-CaG_Jyso.js.map +1 -0
- package/dist/{backup-B9m3cb3X.js → backup-BVbHoKo8.js} +6 -6
- package/dist/{backup-B9m3cb3X.js.map → backup-BVbHoKo8.js.map} +1 -1
- package/dist/boot/boot.d.ts +2 -15
- package/dist/boot/ddl-bootstrap.d.ts +34 -0
- package/dist/boot/env.d.ts +6 -0
- package/dist/boot/fetch-bundle.d.ts +5 -1
- package/dist/boot/provision-only.d.ts +23 -0
- package/dist/boot/provision.d.ts +53 -2
- package/dist/boot/role.d.ts +2 -0
- package/dist/boot/schema-change-recorder.d.ts +45 -0
- package/dist/boot/security-headers.d.ts +22 -0
- package/dist/branch-routes-B2vvEx53.js +84 -0
- package/dist/branch-routes-B2vvEx53.js.map +1 -0
- package/dist/{contract-routes-CsFXW7j4.js → contract-routes-CU2A3JnF.js} +2 -2
- package/dist/{contract-routes-CsFXW7j4.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
- package/dist/cron/cron-store.d.ts +8 -1
- package/dist/{cron-loader-Dj6lA4HQ.js → cron-loader-mMhqbVsl.js} +2 -2
- package/dist/{cron-loader-Dj6lA4HQ.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
- package/dist/{cron-routes-CwbLZguO.js → cron-routes-Cs33j_Kr.js} +3 -3
- package/dist/{cron-routes-CwbLZguO.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
- package/dist/{cron-scheduler-B04ACoUE.js → cron-scheduler-Be7CNghL.js} +2 -2
- package/dist/{cron-scheduler-B04ACoUE.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
- package/dist/{cron-store-DkyfuLOH.js → cron-store-DLn11M8H.js} +10 -9
- package/dist/cron-store-DLn11M8H.js.map +1 -0
- package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
- package/dist/data_driver-D0mg1Y_x.js.map +1 -0
- package/dist/{ddl-bootstrap-Ck20GSIX.js → ddl-bootstrap-CjRrn12l.js} +91 -2
- package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
- package/dist/deploy/pod-contract.d.ts +1 -1
- package/dist/email/dev-sink.d.ts +6 -0
- package/dist/email/index.d.ts +1 -1
- package/dist/{soft-delete-params-BmlVwecC.js → field-access-DYsvjI_P.js} +27 -57
- package/dist/field-access-DYsvjI_P.js.map +1 -0
- package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
- package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
- package/dist/{function-loader-KKDqTcIr.js → function-loader-B8YzXEsQ.js} +21 -14
- package/dist/function-loader-B8YzXEsQ.js.map +1 -0
- package/dist/functions/hono-like.d.ts +12 -0
- package/dist/functions/index.js +19 -1
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +3 -3
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/history/retention-sweep.d.ts +5 -0
- package/dist/{history-recorder-B2rkuhuy.js → history-recorder-Bsau7h4X.js} +6 -6
- package/dist/history-recorder-Bsau7h4X.js.map +1 -0
- package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
- package/dist/history-store-BQz5YCkW.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.es.js +2449 -543
- package/dist/index.es.js.map +1 -1
- package/dist/init/process-safety.d.ts +4 -3
- package/dist/init/shutdown.d.ts +8 -0
- package/dist/init.d.ts +6 -1
- package/dist/jobs/job-store.d.ts +8 -1
- package/dist/{jobs-h1WRq3cp.js → jobs-Co6Ni5HP.js} +6 -6
- package/dist/jobs-Co6Ni5HP.js.map +1 -0
- package/dist/{jwt-D3oFauZI.js → jwt-rDS0884_.js} +4 -3
- package/dist/{jwt-D3oFauZI.js.map → jwt-rDS0884_.js.map} +1 -1
- package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
- package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
- package/dist/mcp/consent-page.d.ts +38 -1
- package/dist/mcp/mcp-routes.d.ts +9 -0
- package/dist/mcp/mcp-tools.d.ts +7 -1
- package/dist/mcp/oauth-store.d.ts +12 -5
- package/dist/metrics/history-recorder.d.ts +4 -1
- package/dist/metrics/history-store.d.ts +8 -0
- package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
- package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
- package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
- package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
- package/dist/{openapi-generator-BjHr6upl.js → openapi-generator-CyxEtYEX.js} +61 -22
- package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
- package/dist/{proxy-CH8Jsovg.js → proxy-BYir8tWK.js} +4 -6
- package/dist/proxy-BYir8tWK.js.map +1 -0
- package/dist/{query-parser-otFZqJsC.js → query-parser-CDXWf2hH.js} +6 -5
- package/dist/{query-parser-otFZqJsC.js.map → query-parser-CDXWf2hH.js.map} +1 -1
- package/dist/{request-timeout-59crS-9u.js → request-timeout-DVjo-c_8.js} +4 -4
- package/dist/request-timeout-DVjo-c_8.js.map +1 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
- package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
- package/dist/{schema-editor-routes-Bsut0bCC.js → schema-editor-routes-Dc3DU_jm.js} +3 -3
- package/dist/{schema-editor-routes-Bsut0bCC.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
- package/dist/soft-delete-params-BRONCOA2.js +59 -0
- package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
- package/dist/{sql-export-routes-DmB_uZ4l.js → sql-export-routes-QycX8W8S.js} +38 -4
- package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
- package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
- package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +2 -0
- package/dist/storage/LocalStorageController.d.ts +8 -3
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/maintenance-keys.d.ts +17 -0
- package/dist/storage/maintenance-routes.d.ts +28 -5
- package/dist/storage/maintenance.d.ts +136 -4
- package/dist/storage/orphan-scans.d.ts +120 -0
- package/dist/storage/types.d.ts +11 -0
- package/dist/strings.es-B7QLnHh2.js +96 -0
- package/dist/strings.es-B7QLnHh2.js.map +1 -0
- package/dist/upload-validation-Bgo626uY.js.map +1 -1
- package/package.json +5 -5
- package/dist/GCSStorageController-CRd9bFrc.js.map +0 -1
- package/dist/S3StorageController-D00EFa5P.js.map +0 -1
- package/dist/auth-Dkvo2tTJ.js.map +0 -1
- package/dist/auth_adapter-CIItCTc8.js +0 -21
- package/dist/auth_adapter-CIItCTc8.js.map +0 -1
- package/dist/backend-DTAOsLQc.js.map +0 -1
- package/dist/cron-store-DkyfuLOH.js.map +0 -1
- package/dist/ddl-bootstrap-Ck20GSIX.js.map +0 -1
- package/dist/function-loader-KKDqTcIr.js.map +0 -1
- package/dist/history-recorder-B2rkuhuy.js.map +0 -1
- package/dist/history-store-DPofGVDp.js.map +0 -1
- package/dist/jobs-h1WRq3cp.js.map +0 -1
- package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
- package/dist/openapi-generator-BjHr6upl.js.map +0 -1
- package/dist/proxy-CH8Jsovg.js.map +0 -1
- package/dist/request-timeout-59crS-9u.js.map +0 -1
- package/dist/soft-delete-params-BmlVwecC.js.map +0 -1
- package/dist/sql-export-routes-DmB_uZ4l.js.map +0 -1
- package/dist/strings.es-BeZWAodn.js.map +0 -1
|
@@ -6,25 +6,17 @@
|
|
|
6
6
|
/** Why an incoming OAuth identity may not be auto-attached to an existing account. */
|
|
7
7
|
export type AutoLinkRefusal =
|
|
8
8
|
/** The provider did not report that it verified the address. */
|
|
9
|
-
"provider-email-unverified"
|
|
10
|
-
/**
|
|
11
|
-
* The local account holds a password nobody ever proved they own: it was
|
|
12
|
-
* created through `POST /auth/register`, which does not verify the
|
|
13
|
-
* address. Attaching a provider identity to it would hand the session to
|
|
14
|
-
* whoever registered the address first — the classic pre-hijack.
|
|
15
|
-
*/
|
|
16
|
-
| "local-account-unverified"
|
|
17
|
-
/**
|
|
18
|
-
* The local account has no password, and its address was never proven
|
|
19
|
-
* either: it was made by a sign-in through a provider that did not vouch
|
|
20
|
-
* for the address, or by someone who never had to. Its sign-in methods are
|
|
21
|
-
* whatever that someone attached, so this is the same pre-hijack as a
|
|
22
|
-
* password would be. Reported apart from `local-account-unverified` only
|
|
23
|
-
* so a login screen does not tell its owner to use a password.
|
|
24
|
-
*/
|
|
25
|
-
| "local-account-unverified-passwordless";
|
|
9
|
+
"provider-email-unverified";
|
|
26
10
|
export type AutoLinkDecision = {
|
|
27
11
|
allowed: true;
|
|
12
|
+
/**
|
|
13
|
+
* The local account's address was never proven, and this sign-in
|
|
14
|
+
* proves it. The route settles the account the way an email code does
|
|
15
|
+
* (`confirmAddressOwnership`) before it links the identity: the
|
|
16
|
+
* password and every identity whose provider did not vouch for the
|
|
17
|
+
* address go, and every session ends.
|
|
18
|
+
*/
|
|
19
|
+
provesAddress: boolean;
|
|
28
20
|
} | {
|
|
29
21
|
allowed: false;
|
|
30
22
|
reason: AutoLinkRefusal;
|
|
@@ -37,25 +29,19 @@ export interface AutoLinkExistingUser {
|
|
|
37
29
|
/**
|
|
38
30
|
* May an OAuth identity be attached to a pre-existing account found *by email*?
|
|
39
31
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* - the **provider** must have verified the address, or the caller has not
|
|
43
|
-
* shown they control it;
|
|
44
|
-
* - the **local account**'s address must have been verified too. Whoever
|
|
45
|
-
* made an unverified account never proved the address, and every way in
|
|
46
|
-
* they left on it — a password, an identity from a provider that did not
|
|
47
|
-
* vouch for the address — would go on opening the account the owner is
|
|
48
|
-
* about to be signed into.
|
|
49
|
-
*
|
|
50
|
-
* The second rule used to exempt an account with no password, on the theory
|
|
51
|
-
* that one made by an OAuth sign-in holds no credential an attacker could have
|
|
52
|
-
* planted. It holds exactly one: the identity it was made with, which is an
|
|
53
|
-
* attacker's whenever the provider did not vouch for the address.
|
|
32
|
+
* Only when the **provider** verified the address. Otherwise the caller has
|
|
33
|
+
* not shown they control it, and linking would hand them the account.
|
|
54
34
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
35
|
+
* When it did, the caller has proven the address exactly as reading a code out
|
|
36
|
+
* of the inbox does: only the inbox's owner can hold a provider account that
|
|
37
|
+
* vouches for it. If the **local account**'s address was never verified,
|
|
38
|
+
* whoever made it never proved the address, and every way in they left on it —
|
|
39
|
+
* a password, an identity from a provider that did not vouch for the address,
|
|
40
|
+
* a live session — would go on opening the account the owner is about to be
|
|
41
|
+
* signed into. So that case answers `provesAddress: true`, and the route
|
|
42
|
+
* removes those ways in before it links (the pre-hijack defence). It used to
|
|
43
|
+
* refuse instead, which left the owner of the address locked out of their own
|
|
44
|
+
* address until they found a magic link or a reset.
|
|
59
45
|
*/
|
|
60
46
|
export declare function decideOAuthAutoLink(args: {
|
|
61
47
|
providerEmailVerified: boolean | undefined;
|
package/dist/auth/rls-scope.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
|
-
import type { DataDriver } from "@rebasepro/types";
|
|
14
|
+
import type { CallerCredential, DataDriver } from "@rebasepro/types";
|
|
15
15
|
/**
|
|
16
16
|
* The identity the trusted server plane runs as: `rebase.dataAsAdmin`, and any
|
|
17
17
|
* caller presenting the service key.
|
|
@@ -37,6 +37,21 @@ export declare const SERVICE_IDENTITY: {
|
|
|
37
37
|
uid: string;
|
|
38
38
|
roles: string[];
|
|
39
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Who a request runs as, as a driver is scoped to it.
|
|
42
|
+
*
|
|
43
|
+
* `credential` is the API key the request authenticated with, when it was one.
|
|
44
|
+
* It changes nothing the database sees; the driver hands it to collection
|
|
45
|
+
* callbacks as `context.credential`, so a personal key's write can be told
|
|
46
|
+
* from its owner's own.
|
|
47
|
+
*/
|
|
48
|
+
export interface ScopeIdentity {
|
|
49
|
+
uid: string;
|
|
50
|
+
roles?: string[];
|
|
51
|
+
isAnonymous?: boolean;
|
|
52
|
+
claims?: Record<string, unknown>;
|
|
53
|
+
credential?: CallerCredential;
|
|
54
|
+
}
|
|
40
55
|
/**
|
|
41
56
|
* Can this driver run a request as a specific user?
|
|
42
57
|
*
|
|
@@ -71,9 +86,4 @@ export declare function scopeDataDriver(driver: DataDriver,
|
|
|
71
86
|
* without this the database sees the same principal either way. See
|
|
72
87
|
* `rebase.is_anonymous()`.
|
|
73
88
|
*/
|
|
74
|
-
user:
|
|
75
|
-
uid: string;
|
|
76
|
-
roles?: string[];
|
|
77
|
-
isAnonymous?: boolean;
|
|
78
|
-
claims?: Record<string, unknown>;
|
|
79
|
-
}): Promise<DataDriver>;
|
|
89
|
+
user: ScopeIdentity): Promise<DataDriver>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RLS simulator's admin surface, under `/api/admin/rls-simulator`.
|
|
3
|
+
*
|
|
4
|
+
* The data API answers one collection at a time for a simulated identity
|
|
5
|
+
* (`auth/simulation.ts`). This answers the questions the admin panel's
|
|
6
|
+
* "Simulate access" bar asks about all of them at once:
|
|
7
|
+
*
|
|
8
|
+
* - `GET /options` — what can be simulated: the roles the project names (the
|
|
9
|
+
* access model's, the ones its row and field rules mention, and `anon`) and
|
|
10
|
+
* the API keys that are live.
|
|
11
|
+
* - `POST /preview` — for `{ role?, userId?, apiKeyId? }`, each collection's
|
|
12
|
+
* rows as that identity sees them against the caller's own view, whether a
|
|
13
|
+
* key's scopes shut it out, and which fields its roles cannot read.
|
|
14
|
+
*
|
|
15
|
+
* The same decision as the data API: only an administrator's own session, a
|
|
16
|
+
* 403 for anyone else.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import { Hono } from "hono";
|
|
21
|
+
import { type CollectionConfig, type DataDriver } from "@rebasepro/types";
|
|
22
|
+
import type { HonoEnv } from "../api/types.js";
|
|
23
|
+
import type { ApiKeyStore } from "./api-keys/api-key-store.js";
|
|
24
|
+
import type { ImpersonationUserResolver } from "./impersonation.js";
|
|
25
|
+
export interface RlsSimulatorRouteConfig {
|
|
26
|
+
/** The collections this backend serves, as they are now. */
|
|
27
|
+
collections(): readonly CollectionConfig[];
|
|
28
|
+
/** The unscoped driver a collection's rows live in. */
|
|
29
|
+
driverFor(slug: string): DataDriver;
|
|
30
|
+
resolveUser?: ImpersonationUserResolver;
|
|
31
|
+
apiKeyStore?: ApiKeyStore;
|
|
32
|
+
}
|
|
33
|
+
/** One collection, as the simulated identity reaches it. */
|
|
34
|
+
export interface SimulatedCollectionView {
|
|
35
|
+
slug: string;
|
|
36
|
+
name: string;
|
|
37
|
+
/**
|
|
38
|
+
* Why the identity sees nothing here: its key's scopes, row policies
|
|
39
|
+
* hiding every row, or a read that fails for it (`error` says why).
|
|
40
|
+
*/
|
|
41
|
+
hiddenBy?: "scope" | "rls" | "error";
|
|
42
|
+
/** Rows the simulated identity reads. Absent when not counted. */
|
|
43
|
+
visibleRows?: number;
|
|
44
|
+
/** Rows the administrator asking reads. */
|
|
45
|
+
totalRows?: number;
|
|
46
|
+
/** Fields the identity's roles cannot read. */
|
|
47
|
+
maskedColumns: string[];
|
|
48
|
+
/** Why the rows could not be counted. */
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function createRlsSimulatorRoutes(config: RlsSimulatorRouteConfig): Hono<HonoEnv>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Simulate access" is read-only.
|
|
3
|
+
*
|
|
4
|
+
* The simulator answers "what does this role, user or key see". A write made
|
|
5
|
+
* while simulating ran as the simulated identity for real: it committed as
|
|
6
|
+
* `rebase_user` with `app.uid` set to the simulated uid, `entity_history`
|
|
7
|
+
* recorded that uid as the author, callbacks saw `context.user` holding a role
|
|
8
|
+
* the account may not even have, and a role-only simulation stamped owner
|
|
9
|
+
* columns with the nobody uid. Only the request log named the administrator.
|
|
10
|
+
* So every door that honours the simulation headers refuses anything but a
|
|
11
|
+
* read, naming the simulation, rather than performing it as someone else.
|
|
12
|
+
*
|
|
13
|
+
* Its own module so the REST generator, history routes and the functions
|
|
14
|
+
* guard can share it without importing the simulator itself.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import type { DataOperation } from "./api-keys/http-operation.js";
|
|
19
|
+
/**
|
|
20
|
+
* Refuse a write or delete on a request that simulates access.
|
|
21
|
+
*
|
|
22
|
+
* `c` is anything with the request context's `get` — the REST generator calls
|
|
23
|
+
* this with a per-operation stand-in for each operation of a `_batch`.
|
|
24
|
+
* `operation` is what the request does, not its verb: `POST …/query` is a read.
|
|
25
|
+
*/
|
|
26
|
+
export declare function refuseSimulatedWrite(c: {
|
|
27
|
+
get: (key: string) => unknown;
|
|
28
|
+
}, operation: DataOperation): void;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RLS simulator: an administrator's data-plane request run as a role, a
|
|
3
|
+
* user or a service key — `x-rebase-simulate-role`, `x-rebase-simulate-user-id`
|
|
4
|
+
* and `x-rebase-simulate-api-key-id`.
|
|
5
|
+
*
|
|
6
|
+
* Impersonation ({@link ./impersonation}) answers "what does this person see";
|
|
7
|
+
* this answers the question an author asks while writing policies, before any
|
|
8
|
+
* such person exists: "what does a `customer` see, what does `u_1` holding
|
|
9
|
+
* `customer` see, what does the support agent's key see". So a role need not be
|
|
10
|
+
* held by anyone and a uid sent with a role need not name an account. Nothing
|
|
11
|
+
* here widens what the caller could do: the caller is an administrator, whom
|
|
12
|
+
* the default policies already let read and write every row, and every
|
|
13
|
+
* simulated identity is narrower.
|
|
14
|
+
*
|
|
15
|
+
* The identity reaches the database exactly as a real caller's does —
|
|
16
|
+
* `scopeDataDriver` → `withAuth` → `applyAuthContext`, which sets the
|
|
17
|
+
* transaction-local identity (`app.uid`, `app.roles`, `app.jwt`) and switches to
|
|
18
|
+
* `rebase_user`, so every policy is evaluated for it. A simulated key also
|
|
19
|
+
* carries its scopes, so the data API refuses the collections the key cannot
|
|
20
|
+
* reach, as it would refuse the key. Fields the identity's roles cannot read
|
|
21
|
+
* are stripped by the driver as for any caller; {@link reportSimulatedContext}
|
|
22
|
+
* names them on the response.
|
|
23
|
+
*
|
|
24
|
+
* Who may: a signed-in administrator on their own session, judged on their
|
|
25
|
+
* roles as the database has them now. Never an API key, never the service
|
|
26
|
+
* key, never anyone else — a 403, never an answer as themselves.
|
|
27
|
+
*
|
|
28
|
+
* Where: the data API and custom functions, like impersonation. Every other
|
|
29
|
+
* route refuses the headers (`refuseUnhonouredImpersonation`).
|
|
30
|
+
*
|
|
31
|
+
* Read-only: a write, delete, batch, revert or non-GET function call that
|
|
32
|
+
* carries the headers is refused (`SIMULATION_READ_ONLY`, see
|
|
33
|
+
* {@link ./simulation-read-only}) rather than run as the simulated identity.
|
|
34
|
+
*
|
|
35
|
+
* Not simulated: the data API's `requireAuth` gate. A simulated anonymous
|
|
36
|
+
* caller is let past it, so the answer is what the rows' policies give an
|
|
37
|
+
* anonymous caller — a 401 there would send the admin panel to refresh a
|
|
38
|
+
* session that is fine. Everywhere else it is no session, as a real anonymous
|
|
39
|
+
* caller is: it carries no `user`, so a function's `requireAuth` refuses it.
|
|
40
|
+
*
|
|
41
|
+
* @module
|
|
42
|
+
*/
|
|
43
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
44
|
+
import { type AccessSimulation, type ApiKeyMasked, type CollectionConfig, type DataDriver } from "@rebasepro/types";
|
|
45
|
+
import type { HonoEnv } from "../api/types.js";
|
|
46
|
+
import { ApiError } from "../api/errors.js";
|
|
47
|
+
import type { ImpersonationCredential, ImpersonationUserResolver } from "./impersonation.js";
|
|
48
|
+
import type { ApiKeyStore } from "./api-keys/api-key-store.js";
|
|
49
|
+
import { type AccessControl } from "./access.js";
|
|
50
|
+
/**
|
|
51
|
+
* The uid a role simulated on its own runs as: a valid UUID that no row
|
|
52
|
+
* names, so an ownership policy matches nothing and a policy comparing a
|
|
53
|
+
* `uuid` column to `auth.uid()` still casts cleanly.
|
|
54
|
+
*/
|
|
55
|
+
export declare const SIMULATED_NOBODY_UID = "00000000-0000-0000-0000-000000000000";
|
|
56
|
+
/** Who a granted simulation runs as. */
|
|
57
|
+
export interface SimulatedIdentity {
|
|
58
|
+
uid: string;
|
|
59
|
+
roles: string[];
|
|
60
|
+
isAnonymous: boolean;
|
|
61
|
+
claims?: Record<string, unknown>;
|
|
62
|
+
/** A key's scopes. Absent for a person, whose data plane is RLS's to decide. */
|
|
63
|
+
scopes?: string[];
|
|
64
|
+
apiKey?: ApiKeyMasked;
|
|
65
|
+
/** What the response header names: the role (or roles) and the uid. */
|
|
66
|
+
role: string;
|
|
67
|
+
}
|
|
68
|
+
export interface SimulationRequest {
|
|
69
|
+
simulation: AccessSimulation;
|
|
70
|
+
/** What the caller presented. Only a `session` may simulate. */
|
|
71
|
+
credential: ImpersonationCredential;
|
|
72
|
+
caller?: {
|
|
73
|
+
uid: string;
|
|
74
|
+
roles: readonly string[];
|
|
75
|
+
};
|
|
76
|
+
/** Re-reads the caller's roles, and finds a user named without a role. */
|
|
77
|
+
resolveUser?: ImpersonationUserResolver;
|
|
78
|
+
/** Finds a key named by id. Absent: a key simulation is refused. */
|
|
79
|
+
apiKeyStore?: ApiKeyStore;
|
|
80
|
+
/** Whose access model a personal key's owner is judged against. */
|
|
81
|
+
access?: AccessControl;
|
|
82
|
+
/** Where the request came in, for the audit line; absent writes none. */
|
|
83
|
+
audit?: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
export type SimulationDecision = {
|
|
86
|
+
granted: SimulatedIdentity;
|
|
87
|
+
simulator: string;
|
|
88
|
+
} | {
|
|
89
|
+
refused: ApiError;
|
|
90
|
+
};
|
|
91
|
+
/** Read the three simulation headers, or `undefined` when none was sent. */
|
|
92
|
+
export declare function simulationOf(header: (name: string) => string | undefined): AccessSimulation | undefined;
|
|
93
|
+
/** Whether a request carries any simulation header. */
|
|
94
|
+
export declare function carriesSimulation(header: (name: string) => string | undefined): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Decide whether this caller may simulate what they asked for, and who the
|
|
97
|
+
* request then runs as.
|
|
98
|
+
*
|
|
99
|
+
* The caller is judged before anything they named is looked up, so a refusal
|
|
100
|
+
* says nothing about whether a user or key exists. Throws when a lookup fails.
|
|
101
|
+
*/
|
|
102
|
+
export declare function decideSimulation(request: SimulationRequest): Promise<SimulationDecision>;
|
|
103
|
+
export interface SimulationOptions {
|
|
104
|
+
credential: ImpersonationCredential;
|
|
105
|
+
/** The unscoped delegate the request's driver was scoped from. */
|
|
106
|
+
driver: DataDriver;
|
|
107
|
+
resolveUser?: ImpersonationUserResolver;
|
|
108
|
+
apiKeyStore?: ApiKeyStore;
|
|
109
|
+
}
|
|
110
|
+
/** The caller the auth middleware put on the context, narrowed. */
|
|
111
|
+
export declare function callerOf(c: Context<HonoEnv>): {
|
|
112
|
+
uid: string;
|
|
113
|
+
roles: string[];
|
|
114
|
+
} | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Honour or refuse this request's simulation headers.
|
|
117
|
+
*
|
|
118
|
+
* Called by the data-plane auth middlewares once the caller is on the context
|
|
119
|
+
* and before impersonation, which it excludes. On a grant the context's
|
|
120
|
+
* `user`, `driver`, `scopes` and `apiKey` become the simulated identity's,
|
|
121
|
+
* `impersonator` names the administrator for the request log, and
|
|
122
|
+
* `simulation` records what to report on the response.
|
|
123
|
+
*
|
|
124
|
+
* @returns The refusal to send, or `undefined` to carry on.
|
|
125
|
+
*/
|
|
126
|
+
export declare function applySimulation(c: Context<HonoEnv>, { credential, driver, resolveUser, apiKeyStore }: SimulationOptions): Promise<Response | undefined>;
|
|
127
|
+
/**
|
|
128
|
+
* The fields of `collection` the roles cannot read: what the driver strips
|
|
129
|
+
* from every row it hands them. Fields hidden from everyone (`excludeFromApi`)
|
|
130
|
+
* are left out — no role sees those, so they say nothing about this one.
|
|
131
|
+
*/
|
|
132
|
+
export declare function maskedFieldsFor(collection: CollectionConfig, roles: readonly string[]): string[];
|
|
133
|
+
/**
|
|
134
|
+
* Name what a simulated request ran as on its response
|
|
135
|
+
* ({@link SIMULATED_CONTEXT_HEADER}), refusals included.
|
|
136
|
+
*
|
|
137
|
+
* Mounted ahead of the data-plane groups; it reads, after the request is
|
|
138
|
+
* answered, what {@link applySimulation} and the REST generator recorded on
|
|
139
|
+
* the context — the identity, and which collection the request was about.
|
|
140
|
+
*/
|
|
141
|
+
export declare function reportSimulatedContext(): MiddlewareHandler<HonoEnv>;
|
|
@@ -46,6 +46,12 @@ import type { RateLimitStore } from "./rate-limit-store.js";
|
|
|
46
46
|
export interface SqlRateLimitStoreOptions {
|
|
47
47
|
/** How often to delete expired buckets. Defaults to 5 minutes. */
|
|
48
48
|
sweepMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Default `true`. `false` is a backend that changes nothing in its
|
|
51
|
+
* database: the table is used if it is there, never created, and counts
|
|
52
|
+
* stay per-process without it.
|
|
53
|
+
*/
|
|
54
|
+
provision?: boolean;
|
|
49
55
|
/**
|
|
50
56
|
* Clock, for tests. Driving the window directly beats sleeping through half
|
|
51
57
|
* of one — a real sleep overruns on a loaded machine and takes the hits it
|
|
@@ -60,4 +66,12 @@ export interface SqlRateLimitStoreOptions {
|
|
|
60
66
|
* deployment on a driver with no SQL admin should degrade to per-process counts
|
|
61
67
|
* with a warning, not refuse to serve.
|
|
62
68
|
*/
|
|
63
|
-
|
|
69
|
+
/** The shared store, plus the one step a provisioning run takes ahead of time. */
|
|
70
|
+
export interface SqlRateLimitStore extends RateLimitStore {
|
|
71
|
+
/**
|
|
72
|
+
* Create the table now rather than on the first counted request — what a
|
|
73
|
+
* provisioning run calls. Resolves to whether it is usable.
|
|
74
|
+
*/
|
|
75
|
+
ensureTable(): Promise<boolean>;
|
|
76
|
+
}
|
|
77
|
+
export declare function createSqlRateLimitStore(driver: DataDriver, options?: SqlRateLimitStoreOptions): SqlRateLimitStore | undefined;
|