@happyvertical/smrt-core 0.51.28 → 0.51.32
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/AGENTS.md +20 -0
- package/README.md +47 -0
- package/agents/build-knowledge.md +16 -0
- package/agents/change-feed.md +95 -4
- package/agents/change-signals.md +6 -1
- package/agents/run-once.md +268 -0
- package/agents/schema-paths.md +19 -5
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -3
- package/dist/change-feed-authz.d.ts +141 -0
- package/dist/change-feed-authz.d.ts.map +1 -0
- package/dist/change-feed-authz.js +260 -0
- package/dist/change-feed-authz.js.map +1 -0
- package/dist/change-feed.d.ts +15 -0
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +4 -0
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts +33 -0
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +77 -0
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +5 -1
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decisions.d.ts +89 -0
- package/dist/decisions.d.ts.map +1 -0
- package/dist/decisions.js +78 -0
- package/dist/decisions.js.map +1 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +36 -1
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +13 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +2 -1
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/generators/events-route.d.ts +35 -4
- package/dist/generators/events-route.d.ts.map +1 -1
- package/dist/generators/events-route.js +78 -14
- package/dist/generators/events-route.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +1 -1
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +17 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +20 -1
- package/dist/migrations/differ.d.ts +43 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +69 -0
- package/dist/migrations/differ.js.map +1 -1
- package/dist/object.d.ts +11 -0
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +96 -2
- package/dist/object.js.map +1 -1
- package/dist/registry/shared-state.js +1 -1
- package/dist/run-once.d.ts +80 -0
- package/dist/run-once.d.ts.map +1 -0
- package/dist/run-once.js +311 -0
- package/dist/run-once.js.map +1 -0
- package/dist/schema/bounded-concurrency.d.ts +31 -0
- package/dist/schema/bounded-concurrency.d.ts.map +1 -0
- package/dist/schema/bounded-concurrency.js +43 -0
- package/dist/schema/bounded-concurrency.js.map +1 -0
- package/dist/schema/foreign-key-orphan-report.d.ts +21 -0
- package/dist/schema/foreign-key-orphan-report.d.ts.map +1 -1
- package/dist/schema/foreign-key-orphan-report.js +90 -55
- package/dist/schema/foreign-key-orphan-report.js.map +1 -1
- package/dist/smrt-knowledge.json +30 -14
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +53 -4
- package/dist/system/retention.d.ts.map +1 -1
- package/dist/system/retention.js +67 -14
- package/dist/system/retention.js.map +1 -1
- package/dist/system/schema.d.ts +33 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +60 -3
- package/dist/system/schema.js.map +1 -1
- package/dist/vite-plugin/changes-route.d.ts.map +1 -1
- package/dist/vite-plugin/changes-route.js +21 -5
- package/dist/vite-plugin/changes-route.js.map +1 -1
- package/dist/vite-plugin/events-route.d.ts.map +1 -1
- package/dist/vite-plugin/events-route.js +9 -0
- package/dist/vite-plugin/events-route.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +14 -0
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/worker-registration.d.ts +34 -0
- package/dist/vite-plugin/worker-registration.d.ts.map +1 -0
- package/dist/vite-plugin/worker-registration.js +108 -0
- package/dist/vite-plugin/worker-registration.js.map +1 -0
- package/package.json +10 -10
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import { ChangeFeedPage, GetChangesOptions } from './change-feed.js';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal shape shared by a durable {@link ChangeFeedEntry} (`change-feed.ts`)
|
|
5
|
+
* and a live `ChangeSignal` (`change-signals.ts`) — both carry exactly
|
|
6
|
+
* table/row/operation/tenant/seq and nothing else observable, so one
|
|
7
|
+
* visibility hook covers catch-up entries and live signals alike.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChangeFeedVisibilityEntry {
|
|
10
|
+
table: string;
|
|
11
|
+
rowId: string | null;
|
|
12
|
+
operation: string;
|
|
13
|
+
tenantId: string | null;
|
|
14
|
+
seq: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The requesting principal's context, common to both hooks. `locals` is
|
|
18
|
+
* SvelteKit's `event.locals`, or `undefined` on the REST transport (which has
|
|
19
|
+
* no `locals` concept). `request` is the SvelteKit route's `event.request`,
|
|
20
|
+
* or the REST route's `authMiddleware`-processed `Request` — a
|
|
21
|
+
* REST-hosting consumer identifies the principal from `request`, since
|
|
22
|
+
* `locals` is never populated there. Every generated route supplies
|
|
23
|
+
* `request`; it is optional only so a low-level unit test can omit it.
|
|
24
|
+
*/
|
|
25
|
+
export interface ChangeFeedRequestContext {
|
|
26
|
+
locals: unknown;
|
|
27
|
+
request?: Request;
|
|
28
|
+
}
|
|
29
|
+
/** Input to the table-level {@link ChangeFeedTableAuthorizer} hook. */
|
|
30
|
+
export interface ChangeFeedTableAuthorizationRequest extends ChangeFeedRequestContext {
|
|
31
|
+
/**
|
|
32
|
+
* The client's `?tables=` filter, or `undefined` when it named none (the
|
|
33
|
+
* unrestricted default). On `_events`, which has no `?tables=` param, this
|
|
34
|
+
* is always `undefined` — the hook's answer alone bounds what the
|
|
35
|
+
* connection may ever see.
|
|
36
|
+
*/
|
|
37
|
+
tables: string[] | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Table-level change-feed authorization hook (#3020). Returns the tables the
|
|
41
|
+
* requester may read from the feed at all; a table it does not name is denied
|
|
42
|
+
* regardless of the client's own `?tables=` filter. Register with
|
|
43
|
+
* {@link setChangeFeedAuthorizer}.
|
|
44
|
+
*/
|
|
45
|
+
export type ChangeFeedTableAuthorizer = (request: ChangeFeedTableAuthorizationRequest) => string[] | Promise<string[]>;
|
|
46
|
+
/** Input to the row-level {@link ChangeFeedEntryVisibility} hook. */
|
|
47
|
+
export interface ChangeFeedEntryVisibilityRequest extends ChangeFeedRequestContext {
|
|
48
|
+
entry: ChangeFeedVisibilityEntry;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Row-level change-feed visibility hook (#3020), for scope narrower than a
|
|
52
|
+
* whole table (a station sees only its own rows). Register with
|
|
53
|
+
* {@link setChangeFeedEntryVisibility}.
|
|
54
|
+
*/
|
|
55
|
+
export type ChangeFeedEntryVisibility = (request: ChangeFeedEntryVisibilityRequest) => boolean | Promise<boolean>;
|
|
56
|
+
declare global {
|
|
57
|
+
var __smrtChangeFeedTableAuthorizer: ChangeFeedTableAuthorizer | undefined;
|
|
58
|
+
var __smrtChangeFeedEntryVisibility: ChangeFeedEntryVisibility | undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Register the table-level change-feed authorization hook used by both
|
|
62
|
+
* generated routes. `undefined` clears it, restoring the pre-#3020 default
|
|
63
|
+
* (tenant-scoped only, no per-table check).
|
|
64
|
+
*/
|
|
65
|
+
export declare function setChangeFeedAuthorizer(authorizeChangeFeed: ChangeFeedTableAuthorizer | undefined): void;
|
|
66
|
+
/**
|
|
67
|
+
* Register the row-level change-feed visibility hook used by both generated
|
|
68
|
+
* routes. `undefined` clears it (every table-authorized row is visible).
|
|
69
|
+
*/
|
|
70
|
+
export declare function setChangeFeedEntryVisibility(isChangeFeedEntryVisible: ChangeFeedEntryVisibility | undefined): void;
|
|
71
|
+
/** Whether a table authorizer is currently registered. */
|
|
72
|
+
export declare function hasChangeFeedTableAuthorizerHook(): boolean;
|
|
73
|
+
/** Whether a row visibility hook is currently registered. */
|
|
74
|
+
export declare function hasChangeFeedEntryVisibilityHook(): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether a resolved allow-list means "match no tables" (an explicit empty
|
|
77
|
+
* array — every table was denied or no hook answer was authorized) as
|
|
78
|
+
* opposed to `undefined` (no hook registered — unchanged "no filter"
|
|
79
|
+
* default). Callers pass this through to `getChangesSince`'s
|
|
80
|
+
* `denyAllTables` option rather than an empty `tables` array — see the
|
|
81
|
+
* module docs' "Cursor correctness" section for why.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isChangeFeedDenyAll(tables: string[] | undefined): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the tables a request may read from the feed: the registered
|
|
86
|
+
* {@link ChangeFeedTableAuthorizer}'s answer intersected with the client's
|
|
87
|
+
* own `?tables=` filter (never widened by it). Returns `requestedTables`
|
|
88
|
+
* unchanged when no hook is registered — the unmodified default behavior —
|
|
89
|
+
* EXCEPT that an explicit empty `requestedTables` (`tables: []`) is
|
|
90
|
+
* normalized to `undefined`: `getChangesSince` (and this module's own
|
|
91
|
+
* {@link isChangeFeedDenyAll}) treat an omitted filter and an empty one as
|
|
92
|
+
* equivalent "no filter", so with no hook registered to actually narrow
|
|
93
|
+
* anything, a client-sent `tables: []` must stay a no-op rather than being
|
|
94
|
+
* read as "deny every table" (#3020 Copilot follow-up).
|
|
95
|
+
*
|
|
96
|
+
* Once a hook is registered, the result is always an explicit array (possibly
|
|
97
|
+
* empty): a throwing or malformed hook fails closed to `[]` (logged, never
|
|
98
|
+
* surfaced as a 5xx — see module docs). A returned table name that is not
|
|
99
|
+
* {@link isValidChangeFeedTableName} (at minimum: contains a NUL byte, or is
|
|
100
|
+
* empty) is treated the same way — the hook's own answer must never be able
|
|
101
|
+
* to reach the database with an unusable value.
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveAuthorizedChangeFeedTables(ctx: ChangeFeedRequestContext, requestedTables: string[] | undefined): Promise<string[] | undefined>;
|
|
104
|
+
/**
|
|
105
|
+
* Whether `entry` is visible to the requester under the registered
|
|
106
|
+
* {@link ChangeFeedEntryVisibility} hook. `true` (visible) when no hook is
|
|
107
|
+
* registered — the unmodified default. A throwing or non-boolean hook fails
|
|
108
|
+
* closed to `false` (hidden), never falls back to visible.
|
|
109
|
+
*/
|
|
110
|
+
export declare function isChangeFeedEntryVisible(ctx: ChangeFeedRequestContext, entry: ChangeFeedVisibilityEntry): Promise<boolean>;
|
|
111
|
+
/**
|
|
112
|
+
* {@link isChangeFeedEntryVisible}, applied to a page of entries in order.
|
|
113
|
+
* Returns `entries` unchanged (same array reference) when no hook is
|
|
114
|
+
* registered.
|
|
115
|
+
*/
|
|
116
|
+
export declare function filterVisibleChangeFeedEntries<T extends ChangeFeedVisibilityEntry>(ctx: ChangeFeedRequestContext, entries: readonly T[]): Promise<T[]>;
|
|
117
|
+
interface AuthorizedChangesInput extends Omit<GetChangesOptions, 'tables' | 'denyAllTables'>, ChangeFeedRequestContext {
|
|
118
|
+
tables?: string[];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* {@link getChangesSince}, additionally applying the registered table and
|
|
122
|
+
* row-level authorization hooks (#3020). Used where the tenant filter (if
|
|
123
|
+
* any) is resolved by the caller rather than the active DispatchBus context —
|
|
124
|
+
* the `_events` catch-up replay (both transports), which captures its tenant
|
|
125
|
+
* scope once at connection open, and the REST `_changes` route, which has no
|
|
126
|
+
* DispatchBus tenant context of its own to resolve.
|
|
127
|
+
*/
|
|
128
|
+
export declare function getAuthorizedChangesSince(db: DatabaseInterface, options: AuthorizedChangesInput): Promise<ChangeFeedPage>;
|
|
129
|
+
/**
|
|
130
|
+
* {@link getTenantScopedChangesSince}, additionally applying the registered
|
|
131
|
+
* table and row-level authorization hooks (#3020). This is what the
|
|
132
|
+
* generated SvelteKit `_changes` route calls.
|
|
133
|
+
*
|
|
134
|
+
* Table authorization narrows `tables` BEFORE the query runs (a denied table
|
|
135
|
+
* is never queried, let alone returned); row visibility filters the fetched
|
|
136
|
+
* page's `changes` AFTER. Neither touches `cursor`/`resyncRequired`/
|
|
137
|
+
* `resyncCursor` — see the module docs' "Cursor correctness" section.
|
|
138
|
+
*/
|
|
139
|
+
export declare function getAuthorizedTenantScopedChangesSince(db: DatabaseInterface, options: Omit<AuthorizedChangesInput, 'tenantId'>): Promise<ChangeFeedPage>;
|
|
140
|
+
export {};
|
|
141
|
+
//# sourceMappingURL=change-feed-authz.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change-feed-authz.d.ts","sourceRoot":"","sources":["../src/change-feed-authz.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { getChangesSince, getTenantScopedChangesSince } from "./change-feed.js";
|
|
2
|
+
import { createLogger } from "@happyvertical/logger";
|
|
3
|
+
//#region src/change-feed-authz.ts
|
|
4
|
+
/**
|
|
5
|
+
* Change-feed table/row authorization seam (issue #3020).
|
|
6
|
+
*
|
|
7
|
+
* The generated `_changes`/`_events` routes authorize on "an authenticated
|
|
8
|
+
* principal" plus tenant scope (`getTenantScopedChangesSince`,
|
|
9
|
+
* `resolveDispatchTenantScope`) — see `change-feed-sensitivity.ts`'s scope
|
|
10
|
+
* note. That is enough to keep one tenant from seeing another's rows, but it
|
|
11
|
+
* cannot express "table X is readable by some principals of the tenant but
|
|
12
|
+
* not others" or "principal P only sees their own rows of table X" (a
|
|
13
|
+
* bay-tablet station versus the back office). `?tables=` was a client-chosen
|
|
14
|
+
* filter, never enforcement: a table the client did not ask for was withheld
|
|
15
|
+
* only because it did not ask, not because it could not have.
|
|
16
|
+
*
|
|
17
|
+
* This module is the consumer-supplied seam that closes that gap, evaluated
|
|
18
|
+
* identically by **every** transport — the generated SvelteKit `_changes`/
|
|
19
|
+
* `_events` routes AND the runtime REST generator's `_changes`/`_events`
|
|
20
|
+
* (`generators/changes-route.ts`, `generators/events-route.ts`) — so no
|
|
21
|
+
* transport can leak what another denies. Two independent hooks,
|
|
22
|
+
* dependency-inverted onto `globalThis` exactly like
|
|
23
|
+
* {@link resolveDispatchTenantScope} (`dispatch/tenant-resolver.ts`) so a
|
|
24
|
+
* consumer app registers them once — wherever it already calls
|
|
25
|
+
* `enableTenancy()`, or its `hooks.server.ts` — with **no change to the
|
|
26
|
+
* generated "DO NOT EDIT" route files' imports**:
|
|
27
|
+
*
|
|
28
|
+
* - `authorizeChangeFeed({ locals, request, tables }) => allowedTables` —
|
|
29
|
+
* table-level. Its result is INTERSECTED with the client's `?tables=`
|
|
30
|
+
* filter (never unioned): a table the hook does not name is never queried,
|
|
31
|
+
* let alone returned, regardless of what the client asked for.
|
|
32
|
+
* - `isChangeFeedEntryVisible({ locals, request, entry }) => boolean` —
|
|
33
|
+
* row-level, for scope narrower than a whole table (a station sees only its
|
|
34
|
+
* own rows). Applied per entry, after the table-level filter, before
|
|
35
|
+
* anything is serialized to the client — including a live `_events` signal,
|
|
36
|
+
* which never carries a payload but still carries the row id and write
|
|
37
|
+
* timing the row hook exists to withhold.
|
|
38
|
+
*
|
|
39
|
+
* ## `locals` vs. `request`
|
|
40
|
+
*
|
|
41
|
+
* SvelteKit routes pass both `event.locals` and `event.request`. The REST
|
|
42
|
+
* generator has no `locals` concept, so it passes `locals: undefined` and the
|
|
43
|
+
* `authMiddleware`-processed `Request` (whatever properties/headers the
|
|
44
|
+
* middleware attached) as `request` — a REST-hosting consumer identifies the
|
|
45
|
+
* principal from `request`, not `locals`. Every real caller supplies
|
|
46
|
+
* `request`; it is typed optional only so a low-level unit test can omit it.
|
|
47
|
+
*
|
|
48
|
+
* Both hooks are optional and independent. When neither is registered, every
|
|
49
|
+
* transport behaves exactly as before (#1540 posture — authenticated +
|
|
50
|
+
* tenant scoped only, no per-table or per-row check): every call in this
|
|
51
|
+
* module is a synchronous no-op pass-through until a consumer opts in.
|
|
52
|
+
*
|
|
53
|
+
* ## Fail-closed
|
|
54
|
+
*
|
|
55
|
+
* A hook that throws, or returns something other than the documented shape,
|
|
56
|
+
* is treated as authorizing nothing for that call — the same posture
|
|
57
|
+
* {@link resolveDispatchTenantScope} takes for a throwing tenant resolver
|
|
58
|
+
* (fail closed rather than fail the request), and the same *shape* of answer
|
|
59
|
+
* (200, empty `changes`, cursor still advances) the change-feed sensitivity
|
|
60
|
+
* filter (#2937) already gives a request naming only tables it may not see.
|
|
61
|
+
* A 5xx here would tell a caller its own hook is broken, which is
|
|
62
|
+
* indistinguishable from "your hook denied me" from the caller's side and
|
|
63
|
+
* would leak that distinction; the contract that matters — never an
|
|
64
|
+
* unfiltered feed — holds identically either way.
|
|
65
|
+
*
|
|
66
|
+
* ## Cursor correctness
|
|
67
|
+
*
|
|
68
|
+
* Every filter here narrows the *returned* `changes` array; nothing here ever
|
|
69
|
+
* touches `cursor`/`resyncRequired`/`resyncCursor`. This mirrors #2937's
|
|
70
|
+
* sensitivity filter (`change-feed.ts`, "Filters ... affect which rows are
|
|
71
|
+
* returned, never how the cursor advances") and for the same reason: a
|
|
72
|
+
* client must advance past a denied entry without re-polling it forever, and
|
|
73
|
+
* the entry's row id/timing must never be inferable from a cursor that stalls
|
|
74
|
+
* on it. Table denial is implemented via `getChangesSince`'s explicit
|
|
75
|
+
* `denyAllTables` option rather than an empty `tables` array —
|
|
76
|
+
* `getChangesSince` treats an omitted/empty `tables` as "no filter", so an
|
|
77
|
+
* empty array would (perversely) widen the read to everything, and an
|
|
78
|
+
* earlier version of this module instead passed a guaranteed-no-match
|
|
79
|
+
* sentinel table name through the ordinary `table_name IN (...)` path; that
|
|
80
|
+
* broke on PostgreSQL, which rejects a NUL byte in a text parameter, turning
|
|
81
|
+
* a denied request into a 500 instead of the required 200 empty page. A
|
|
82
|
+
* `denyAllTables` request skips the `IN (...)` clause entirely — no
|
|
83
|
+
* synthesized value ever reaches the driver — while still running the same
|
|
84
|
+
* horizon/pruned-cursor computation any other filter does, so the cursor
|
|
85
|
+
* still advances correctly; it just matches zero rows.
|
|
86
|
+
*/
|
|
87
|
+
var logger = createLogger({ level: "info" });
|
|
88
|
+
/**
|
|
89
|
+
* Register the table-level change-feed authorization hook used by both
|
|
90
|
+
* generated routes. `undefined` clears it, restoring the pre-#3020 default
|
|
91
|
+
* (tenant-scoped only, no per-table check).
|
|
92
|
+
*/
|
|
93
|
+
function setChangeFeedAuthorizer(authorizeChangeFeed) {
|
|
94
|
+
globalThis.__smrtChangeFeedTableAuthorizer = authorizeChangeFeed;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register the row-level change-feed visibility hook used by both generated
|
|
98
|
+
* routes. `undefined` clears it (every table-authorized row is visible).
|
|
99
|
+
*/
|
|
100
|
+
function setChangeFeedEntryVisibility(isChangeFeedEntryVisible) {
|
|
101
|
+
globalThis.__smrtChangeFeedEntryVisibility = isChangeFeedEntryVisible;
|
|
102
|
+
}
|
|
103
|
+
/** Whether a table authorizer is currently registered. */
|
|
104
|
+
function hasChangeFeedTableAuthorizerHook() {
|
|
105
|
+
return globalThis.__smrtChangeFeedTableAuthorizer !== void 0;
|
|
106
|
+
}
|
|
107
|
+
/** Whether a row visibility hook is currently registered. */
|
|
108
|
+
function hasChangeFeedEntryVisibilityHook() {
|
|
109
|
+
return globalThis.__smrtChangeFeedEntryVisibility !== void 0;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether a resolved allow-list means "match no tables" (an explicit empty
|
|
113
|
+
* array — every table was denied or no hook answer was authorized) as
|
|
114
|
+
* opposed to `undefined` (no hook registered — unchanged "no filter"
|
|
115
|
+
* default). Callers pass this through to `getChangesSince`'s
|
|
116
|
+
* `denyAllTables` option rather than an empty `tables` array — see the
|
|
117
|
+
* module docs' "Cursor correctness" section for why.
|
|
118
|
+
*/
|
|
119
|
+
function isChangeFeedDenyAll(tables) {
|
|
120
|
+
return tables !== void 0 && tables.length === 0;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Whether `name` is a usable change-feed table name: a non-empty string
|
|
124
|
+
* containing no NUL byte. A NUL byte is PostgreSQL's hard failure mode — it
|
|
125
|
+
* rejects one outright in a text parameter — so a hook returning one would
|
|
126
|
+
* otherwise reach the ordinary `table_name IN (...)` clause (built from the
|
|
127
|
+
* hook's OWN answer, not a client-supplied value) and turn a legitimate
|
|
128
|
+
* authorization decision into a 500 instead of the documented fail-closed
|
|
129
|
+
* 200/empty contract. An empty string is equally never a real table name.
|
|
130
|
+
*/
|
|
131
|
+
function isValidChangeFeedTableName(name) {
|
|
132
|
+
return name.length > 0 && !name.includes("\0");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resolve the tables a request may read from the feed: the registered
|
|
136
|
+
* {@link ChangeFeedTableAuthorizer}'s answer intersected with the client's
|
|
137
|
+
* own `?tables=` filter (never widened by it). Returns `requestedTables`
|
|
138
|
+
* unchanged when no hook is registered — the unmodified default behavior —
|
|
139
|
+
* EXCEPT that an explicit empty `requestedTables` (`tables: []`) is
|
|
140
|
+
* normalized to `undefined`: `getChangesSince` (and this module's own
|
|
141
|
+
* {@link isChangeFeedDenyAll}) treat an omitted filter and an empty one as
|
|
142
|
+
* equivalent "no filter", so with no hook registered to actually narrow
|
|
143
|
+
* anything, a client-sent `tables: []` must stay a no-op rather than being
|
|
144
|
+
* read as "deny every table" (#3020 Copilot follow-up).
|
|
145
|
+
*
|
|
146
|
+
* Once a hook is registered, the result is always an explicit array (possibly
|
|
147
|
+
* empty): a throwing or malformed hook fails closed to `[]` (logged, never
|
|
148
|
+
* surfaced as a 5xx — see module docs). A returned table name that is not
|
|
149
|
+
* {@link isValidChangeFeedTableName} (at minimum: contains a NUL byte, or is
|
|
150
|
+
* empty) is treated the same way — the hook's own answer must never be able
|
|
151
|
+
* to reach the database with an unusable value.
|
|
152
|
+
*/
|
|
153
|
+
async function resolveAuthorizedChangeFeedTables(ctx, requestedTables) {
|
|
154
|
+
const authorizer = globalThis.__smrtChangeFeedTableAuthorizer;
|
|
155
|
+
if (!authorizer) return requestedTables && requestedTables.length === 0 ? void 0 : requestedTables;
|
|
156
|
+
let allowed;
|
|
157
|
+
try {
|
|
158
|
+
const result = await authorizer({
|
|
159
|
+
...ctx,
|
|
160
|
+
tables: requestedTables
|
|
161
|
+
});
|
|
162
|
+
if (!Array.isArray(result) || result.some((t) => typeof t !== "string" || !isValidChangeFeedTableName(t))) {
|
|
163
|
+
logger.error("authorizeChangeFeed() returned a non-string-array or invalid table name; failing closed to no tables");
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
allowed = result;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error("authorizeChangeFeed() threw; failing closed to no tables", { error: error instanceof Error ? error.message : String(error) });
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
if (!requestedTables || requestedTables.length === 0) return allowed;
|
|
172
|
+
const allowedSet = new Set(allowed);
|
|
173
|
+
return requestedTables.filter((table) => allowedSet.has(table));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether `entry` is visible to the requester under the registered
|
|
177
|
+
* {@link ChangeFeedEntryVisibility} hook. `true` (visible) when no hook is
|
|
178
|
+
* registered — the unmodified default. A throwing or non-boolean hook fails
|
|
179
|
+
* closed to `false` (hidden), never falls back to visible.
|
|
180
|
+
*/
|
|
181
|
+
async function isChangeFeedEntryVisible(ctx, entry) {
|
|
182
|
+
const predicate = globalThis.__smrtChangeFeedEntryVisibility;
|
|
183
|
+
if (!predicate) return true;
|
|
184
|
+
try {
|
|
185
|
+
return await predicate({
|
|
186
|
+
...ctx,
|
|
187
|
+
entry
|
|
188
|
+
}) === true;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.error("isChangeFeedEntryVisible() threw; failing closed to hidden", { error: error instanceof Error ? error.message : String(error) });
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* {@link isChangeFeedEntryVisible}, applied to a page of entries in order.
|
|
196
|
+
* Returns `entries` unchanged (same array reference) when no hook is
|
|
197
|
+
* registered.
|
|
198
|
+
*/
|
|
199
|
+
async function filterVisibleChangeFeedEntries(ctx, entries) {
|
|
200
|
+
if (entries.length === 0 || !globalThis.__smrtChangeFeedEntryVisibility) return entries;
|
|
201
|
+
const visible = [];
|
|
202
|
+
for (const entry of entries) if (await isChangeFeedEntryVisible(ctx, entry)) visible.push(entry);
|
|
203
|
+
return visible;
|
|
204
|
+
}
|
|
205
|
+
/** Shared implementation behind {@link getAuthorizedChangesSince} and {@link getAuthorizedTenantScopedChangesSince}. */
|
|
206
|
+
async function readAuthorized(ctx, requestedTables, read) {
|
|
207
|
+
const effectiveTables = await resolveAuthorizedChangeFeedTables(ctx, requestedTables);
|
|
208
|
+
const denyAllTables = isChangeFeedDenyAll(effectiveTables);
|
|
209
|
+
const page = await read(denyAllTables ? void 0 : effectiveTables, denyAllTables);
|
|
210
|
+
if (page.changes.length === 0) return page;
|
|
211
|
+
const visibleChanges = await filterVisibleChangeFeedEntries(ctx, page.changes);
|
|
212
|
+
return visibleChanges.length === page.changes.length ? page : {
|
|
213
|
+
...page,
|
|
214
|
+
changes: visibleChanges
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* {@link getChangesSince}, additionally applying the registered table and
|
|
219
|
+
* row-level authorization hooks (#3020). Used where the tenant filter (if
|
|
220
|
+
* any) is resolved by the caller rather than the active DispatchBus context —
|
|
221
|
+
* the `_events` catch-up replay (both transports), which captures its tenant
|
|
222
|
+
* scope once at connection open, and the REST `_changes` route, which has no
|
|
223
|
+
* DispatchBus tenant context of its own to resolve.
|
|
224
|
+
*/
|
|
225
|
+
async function getAuthorizedChangesSince(db, options) {
|
|
226
|
+
const { locals, request, tables, ...rest } = options;
|
|
227
|
+
return readAuthorized({
|
|
228
|
+
locals,
|
|
229
|
+
request
|
|
230
|
+
}, tables, (effectiveTables, denyAllTables) => getChangesSince(db, {
|
|
231
|
+
...rest,
|
|
232
|
+
tables: effectiveTables,
|
|
233
|
+
denyAllTables
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* {@link getTenantScopedChangesSince}, additionally applying the registered
|
|
238
|
+
* table and row-level authorization hooks (#3020). This is what the
|
|
239
|
+
* generated SvelteKit `_changes` route calls.
|
|
240
|
+
*
|
|
241
|
+
* Table authorization narrows `tables` BEFORE the query runs (a denied table
|
|
242
|
+
* is never queried, let alone returned); row visibility filters the fetched
|
|
243
|
+
* page's `changes` AFTER. Neither touches `cursor`/`resyncRequired`/
|
|
244
|
+
* `resyncCursor` — see the module docs' "Cursor correctness" section.
|
|
245
|
+
*/
|
|
246
|
+
async function getAuthorizedTenantScopedChangesSince(db, options) {
|
|
247
|
+
const { locals, request, tables, ...rest } = options;
|
|
248
|
+
return readAuthorized({
|
|
249
|
+
locals,
|
|
250
|
+
request
|
|
251
|
+
}, tables, (effectiveTables, denyAllTables) => getTenantScopedChangesSince(db, {
|
|
252
|
+
...rest,
|
|
253
|
+
tables: effectiveTables,
|
|
254
|
+
denyAllTables
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
export { filterVisibleChangeFeedEntries, getAuthorizedChangesSince, getAuthorizedTenantScopedChangesSince, hasChangeFeedEntryVisibilityHook, hasChangeFeedTableAuthorizerHook, isChangeFeedDenyAll, isChangeFeedEntryVisible, resolveAuthorizedChangeFeedTables, setChangeFeedAuthorizer, setChangeFeedEntryVisibility };
|
|
259
|
+
|
|
260
|
+
//# sourceMappingURL=change-feed-authz.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change-feed-authz.js","names":[],"sources":["../src/change-feed-authz.ts"],"sourcesContent":["/**\n * Change-feed table/row authorization seam (issue #3020).\n *\n * The generated `_changes`/`_events` routes authorize on \"an authenticated\n * principal\" plus tenant scope (`getTenantScopedChangesSince`,\n * `resolveDispatchTenantScope`) — see `change-feed-sensitivity.ts`'s scope\n * note. That is enough to keep one tenant from seeing another's rows, but it\n * cannot express \"table X is readable by some principals of the tenant but\n * not others\" or \"principal P only sees their own rows of table X\" (a\n * bay-tablet station versus the back office). `?tables=` was a client-chosen\n * filter, never enforcement: a table the client did not ask for was withheld\n * only because it did not ask, not because it could not have.\n *\n * This module is the consumer-supplied seam that closes that gap, evaluated\n * identically by **every** transport — the generated SvelteKit `_changes`/\n * `_events` routes AND the runtime REST generator's `_changes`/`_events`\n * (`generators/changes-route.ts`, `generators/events-route.ts`) — so no\n * transport can leak what another denies. Two independent hooks,\n * dependency-inverted onto `globalThis` exactly like\n * {@link resolveDispatchTenantScope} (`dispatch/tenant-resolver.ts`) so a\n * consumer app registers them once — wherever it already calls\n * `enableTenancy()`, or its `hooks.server.ts` — with **no change to the\n * generated \"DO NOT EDIT\" route files' imports**:\n *\n * - `authorizeChangeFeed({ locals, request, tables }) => allowedTables` —\n * table-level. Its result is INTERSECTED with the client's `?tables=`\n * filter (never unioned): a table the hook does not name is never queried,\n * let alone returned, regardless of what the client asked for.\n * - `isChangeFeedEntryVisible({ locals, request, entry }) => boolean` —\n * row-level, for scope narrower than a whole table (a station sees only its\n * own rows). Applied per entry, after the table-level filter, before\n * anything is serialized to the client — including a live `_events` signal,\n * which never carries a payload but still carries the row id and write\n * timing the row hook exists to withhold.\n *\n * ## `locals` vs. `request`\n *\n * SvelteKit routes pass both `event.locals` and `event.request`. The REST\n * generator has no `locals` concept, so it passes `locals: undefined` and the\n * `authMiddleware`-processed `Request` (whatever properties/headers the\n * middleware attached) as `request` — a REST-hosting consumer identifies the\n * principal from `request`, not `locals`. Every real caller supplies\n * `request`; it is typed optional only so a low-level unit test can omit it.\n *\n * Both hooks are optional and independent. When neither is registered, every\n * transport behaves exactly as before (#1540 posture — authenticated +\n * tenant scoped only, no per-table or per-row check): every call in this\n * module is a synchronous no-op pass-through until a consumer opts in.\n *\n * ## Fail-closed\n *\n * A hook that throws, or returns something other than the documented shape,\n * is treated as authorizing nothing for that call — the same posture\n * {@link resolveDispatchTenantScope} takes for a throwing tenant resolver\n * (fail closed rather than fail the request), and the same *shape* of answer\n * (200, empty `changes`, cursor still advances) the change-feed sensitivity\n * filter (#2937) already gives a request naming only tables it may not see.\n * A 5xx here would tell a caller its own hook is broken, which is\n * indistinguishable from \"your hook denied me\" from the caller's side and\n * would leak that distinction; the contract that matters — never an\n * unfiltered feed — holds identically either way.\n *\n * ## Cursor correctness\n *\n * Every filter here narrows the *returned* `changes` array; nothing here ever\n * touches `cursor`/`resyncRequired`/`resyncCursor`. This mirrors #2937's\n * sensitivity filter (`change-feed.ts`, \"Filters ... affect which rows are\n * returned, never how the cursor advances\") and for the same reason: a\n * client must advance past a denied entry without re-polling it forever, and\n * the entry's row id/timing must never be inferable from a cursor that stalls\n * on it. Table denial is implemented via `getChangesSince`'s explicit\n * `denyAllTables` option rather than an empty `tables` array —\n * `getChangesSince` treats an omitted/empty `tables` as \"no filter\", so an\n * empty array would (perversely) widen the read to everything, and an\n * earlier version of this module instead passed a guaranteed-no-match\n * sentinel table name through the ordinary `table_name IN (...)` path; that\n * broke on PostgreSQL, which rejects a NUL byte in a text parameter, turning\n * a denied request into a 500 instead of the required 200 empty page. A\n * `denyAllTables` request skips the `IN (...)` clause entirely — no\n * synthesized value ever reaches the driver — while still running the same\n * horizon/pruned-cursor computation any other filter does, so the cursor\n * still advances correctly; it just matches zero rows.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n type ChangeFeedPage,\n type GetChangesOptions,\n getChangesSince,\n getTenantScopedChangesSince,\n} from './change-feed.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Minimal shape shared by a durable {@link ChangeFeedEntry} (`change-feed.ts`)\n * and a live `ChangeSignal` (`change-signals.ts`) — both carry exactly\n * table/row/operation/tenant/seq and nothing else observable, so one\n * visibility hook covers catch-up entries and live signals alike.\n */\nexport interface ChangeFeedVisibilityEntry {\n table: string;\n rowId: string | null;\n operation: string;\n tenantId: string | null;\n seq: number;\n}\n\n/**\n * The requesting principal's context, common to both hooks. `locals` is\n * SvelteKit's `event.locals`, or `undefined` on the REST transport (which has\n * no `locals` concept). `request` is the SvelteKit route's `event.request`,\n * or the REST route's `authMiddleware`-processed `Request` — a\n * REST-hosting consumer identifies the principal from `request`, since\n * `locals` is never populated there. Every generated route supplies\n * `request`; it is optional only so a low-level unit test can omit it.\n */\nexport interface ChangeFeedRequestContext {\n locals: unknown;\n request?: Request;\n}\n\n/** Input to the table-level {@link ChangeFeedTableAuthorizer} hook. */\nexport interface ChangeFeedTableAuthorizationRequest\n extends ChangeFeedRequestContext {\n /**\n * The client's `?tables=` filter, or `undefined` when it named none (the\n * unrestricted default). On `_events`, which has no `?tables=` param, this\n * is always `undefined` — the hook's answer alone bounds what the\n * connection may ever see.\n */\n tables: string[] | undefined;\n}\n\n/**\n * Table-level change-feed authorization hook (#3020). Returns the tables the\n * requester may read from the feed at all; a table it does not name is denied\n * regardless of the client's own `?tables=` filter. Register with\n * {@link setChangeFeedAuthorizer}.\n */\nexport type ChangeFeedTableAuthorizer = (\n request: ChangeFeedTableAuthorizationRequest,\n) => string[] | Promise<string[]>;\n\n/** Input to the row-level {@link ChangeFeedEntryVisibility} hook. */\nexport interface ChangeFeedEntryVisibilityRequest\n extends ChangeFeedRequestContext {\n entry: ChangeFeedVisibilityEntry;\n}\n\n/**\n * Row-level change-feed visibility hook (#3020), for scope narrower than a\n * whole table (a station sees only its own rows). Register with\n * {@link setChangeFeedEntryVisibility}.\n */\nexport type ChangeFeedEntryVisibility = (\n request: ChangeFeedEntryVisibilityRequest,\n) => boolean | Promise<boolean>;\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtChangeFeedTableAuthorizer: ChangeFeedTableAuthorizer | undefined;\n // eslint-disable-next-line no-var\n var __smrtChangeFeedEntryVisibility: ChangeFeedEntryVisibility | undefined;\n}\n\n/**\n * Register the table-level change-feed authorization hook used by both\n * generated routes. `undefined` clears it, restoring the pre-#3020 default\n * (tenant-scoped only, no per-table check).\n */\nexport function setChangeFeedAuthorizer(\n authorizeChangeFeed: ChangeFeedTableAuthorizer | undefined,\n): void {\n globalThis.__smrtChangeFeedTableAuthorizer = authorizeChangeFeed;\n}\n\n/**\n * Register the row-level change-feed visibility hook used by both generated\n * routes. `undefined` clears it (every table-authorized row is visible).\n */\nexport function setChangeFeedEntryVisibility(\n isChangeFeedEntryVisible: ChangeFeedEntryVisibility | undefined,\n): void {\n globalThis.__smrtChangeFeedEntryVisibility = isChangeFeedEntryVisible;\n}\n\n/** Whether a table authorizer is currently registered. */\nexport function hasChangeFeedTableAuthorizerHook(): boolean {\n return globalThis.__smrtChangeFeedTableAuthorizer !== undefined;\n}\n\n/** Whether a row visibility hook is currently registered. */\nexport function hasChangeFeedEntryVisibilityHook(): boolean {\n return globalThis.__smrtChangeFeedEntryVisibility !== undefined;\n}\n\n/**\n * Whether a resolved allow-list means \"match no tables\" (an explicit empty\n * array — every table was denied or no hook answer was authorized) as\n * opposed to `undefined` (no hook registered — unchanged \"no filter\"\n * default). Callers pass this through to `getChangesSince`'s\n * `denyAllTables` option rather than an empty `tables` array — see the\n * module docs' \"Cursor correctness\" section for why.\n */\nexport function isChangeFeedDenyAll(tables: string[] | undefined): boolean {\n return tables !== undefined && tables.length === 0;\n}\n\n/**\n * Whether `name` is a usable change-feed table name: a non-empty string\n * containing no NUL byte. A NUL byte is PostgreSQL's hard failure mode — it\n * rejects one outright in a text parameter — so a hook returning one would\n * otherwise reach the ordinary `table_name IN (...)` clause (built from the\n * hook's OWN answer, not a client-supplied value) and turn a legitimate\n * authorization decision into a 500 instead of the documented fail-closed\n * 200/empty contract. An empty string is equally never a real table name.\n */\nfunction isValidChangeFeedTableName(name: string): boolean {\n return name.length > 0 && !name.includes('\\u0000');\n}\n\n/**\n * Resolve the tables a request may read from the feed: the registered\n * {@link ChangeFeedTableAuthorizer}'s answer intersected with the client's\n * own `?tables=` filter (never widened by it). Returns `requestedTables`\n * unchanged when no hook is registered — the unmodified default behavior —\n * EXCEPT that an explicit empty `requestedTables` (`tables: []`) is\n * normalized to `undefined`: `getChangesSince` (and this module's own\n * {@link isChangeFeedDenyAll}) treat an omitted filter and an empty one as\n * equivalent \"no filter\", so with no hook registered to actually narrow\n * anything, a client-sent `tables: []` must stay a no-op rather than being\n * read as \"deny every table\" (#3020 Copilot follow-up).\n *\n * Once a hook is registered, the result is always an explicit array (possibly\n * empty): a throwing or malformed hook fails closed to `[]` (logged, never\n * surfaced as a 5xx — see module docs). A returned table name that is not\n * {@link isValidChangeFeedTableName} (at minimum: contains a NUL byte, or is\n * empty) is treated the same way — the hook's own answer must never be able\n * to reach the database with an unusable value.\n */\nexport async function resolveAuthorizedChangeFeedTables(\n ctx: ChangeFeedRequestContext,\n requestedTables: string[] | undefined,\n): Promise<string[] | undefined> {\n const authorizer = globalThis.__smrtChangeFeedTableAuthorizer;\n if (!authorizer) {\n return requestedTables && requestedTables.length === 0\n ? undefined\n : requestedTables;\n }\n\n let allowed: string[];\n try {\n const result = await authorizer({ ...ctx, tables: requestedTables });\n if (\n !Array.isArray(result) ||\n result.some(\n (t) => typeof t !== 'string' || !isValidChangeFeedTableName(t),\n )\n ) {\n logger.error(\n 'authorizeChangeFeed() returned a non-string-array or invalid table name; failing closed to no tables',\n );\n return [];\n }\n allowed = result;\n } catch (error) {\n logger.error('authorizeChangeFeed() threw; failing closed to no tables', {\n error: error instanceof Error ? error.message : String(error),\n });\n return [];\n }\n\n if (!requestedTables || requestedTables.length === 0) return allowed;\n const allowedSet = new Set(allowed);\n return requestedTables.filter((table) => allowedSet.has(table));\n}\n\n/**\n * Whether `entry` is visible to the requester under the registered\n * {@link ChangeFeedEntryVisibility} hook. `true` (visible) when no hook is\n * registered — the unmodified default. A throwing or non-boolean hook fails\n * closed to `false` (hidden), never falls back to visible.\n */\nexport async function isChangeFeedEntryVisible(\n ctx: ChangeFeedRequestContext,\n entry: ChangeFeedVisibilityEntry,\n): Promise<boolean> {\n const predicate = globalThis.__smrtChangeFeedEntryVisibility;\n if (!predicate) return true;\n try {\n return (await predicate({ ...ctx, entry })) === true;\n } catch (error) {\n logger.error('isChangeFeedEntryVisible() threw; failing closed to hidden', {\n error: error instanceof Error ? error.message : String(error),\n });\n return false;\n }\n}\n\n/**\n * {@link isChangeFeedEntryVisible}, applied to a page of entries in order.\n * Returns `entries` unchanged (same array reference) when no hook is\n * registered.\n */\nexport async function filterVisibleChangeFeedEntries<\n T extends ChangeFeedVisibilityEntry,\n>(ctx: ChangeFeedRequestContext, entries: readonly T[]): Promise<T[]> {\n if (entries.length === 0 || !globalThis.__smrtChangeFeedEntryVisibility) {\n return entries as T[];\n }\n const visible: T[] = [];\n for (const entry of entries) {\n if (await isChangeFeedEntryVisible(ctx, entry)) visible.push(entry);\n }\n return visible;\n}\n\ninterface AuthorizedChangesInput\n extends Omit<GetChangesOptions, 'tables' | 'denyAllTables'>,\n ChangeFeedRequestContext {\n tables?: string[];\n}\n\n/** Shared implementation behind {@link getAuthorizedChangesSince} and {@link getAuthorizedTenantScopedChangesSince}. */\nasync function readAuthorized(\n ctx: ChangeFeedRequestContext,\n requestedTables: string[] | undefined,\n read: (\n tables: string[] | undefined,\n denyAllTables: boolean,\n ) => Promise<ChangeFeedPage>,\n): Promise<ChangeFeedPage> {\n const effectiveTables = await resolveAuthorizedChangeFeedTables(\n ctx,\n requestedTables,\n );\n const denyAllTables = isChangeFeedDenyAll(effectiveTables);\n const page = await read(\n denyAllTables ? undefined : effectiveTables,\n denyAllTables,\n );\n if (page.changes.length === 0) return page;\n const visibleChanges = await filterVisibleChangeFeedEntries(\n ctx,\n page.changes,\n );\n return visibleChanges.length === page.changes.length\n ? page\n : { ...page, changes: visibleChanges };\n}\n\n/**\n * {@link getChangesSince}, additionally applying the registered table and\n * row-level authorization hooks (#3020). Used where the tenant filter (if\n * any) is resolved by the caller rather than the active DispatchBus context —\n * the `_events` catch-up replay (both transports), which captures its tenant\n * scope once at connection open, and the REST `_changes` route, which has no\n * DispatchBus tenant context of its own to resolve.\n */\nexport async function getAuthorizedChangesSince(\n db: DatabaseInterface,\n options: AuthorizedChangesInput,\n): Promise<ChangeFeedPage> {\n const { locals, request, tables, ...rest } = options;\n return readAuthorized(\n { locals, request },\n tables,\n (effectiveTables, denyAllTables) =>\n getChangesSince(db, {\n ...rest,\n tables: effectiveTables,\n denyAllTables,\n }),\n );\n}\n\n/**\n * {@link getTenantScopedChangesSince}, additionally applying the registered\n * table and row-level authorization hooks (#3020). This is what the\n * generated SvelteKit `_changes` route calls.\n *\n * Table authorization narrows `tables` BEFORE the query runs (a denied table\n * is never queried, let alone returned); row visibility filters the fetched\n * page's `changes` AFTER. Neither touches `cursor`/`resyncRequired`/\n * `resyncCursor` — see the module docs' \"Cursor correctness\" section.\n */\nexport async function getAuthorizedTenantScopedChangesSince(\n db: DatabaseInterface,\n options: Omit<AuthorizedChangesInput, 'tenantId'>,\n): Promise<ChangeFeedPage> {\n const { locals, request, tables, ...rest } = options;\n return readAuthorized(\n { locals, request },\n tables,\n (effectiveTables, denyAllTables) =>\n getTenantScopedChangesSince(db, {\n ...rest,\n tables: effectiveTables,\n denyAllTables,\n }),\n );\n}\n"],"mappings":"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"}
|
package/dist/change-feed.d.ts
CHANGED
|
@@ -87,6 +87,21 @@ export interface GetChangesOptions {
|
|
|
87
87
|
drain?: boolean;
|
|
88
88
|
/** Restrict to these physical table names. Empty/omitted → all tables. */
|
|
89
89
|
tables?: string[];
|
|
90
|
+
/**
|
|
91
|
+
* Explicit deny-all: match no tables regardless of {@link tables}, while
|
|
92
|
+
* still computing `cursor`/`resyncRequired`/`resyncCursor` exactly as a
|
|
93
|
+
* query that matched zero rows would (the same horizon/pruned-cursor
|
|
94
|
+
* detection that runs for any other filter). Distinct from an
|
|
95
|
+
* omitted/empty `tables` array, which means "no filter" (all tables) — so
|
|
96
|
+
* a caller that has resolved "the requester may read nothing" must set
|
|
97
|
+
* this rather than pass `tables: []`.
|
|
98
|
+
*
|
|
99
|
+
* Used by `change-feed-authz.ts`'s consumer table-authorization seam
|
|
100
|
+
* (#3020) so a denied `_changes`/`_events` request never reaches the
|
|
101
|
+
* `table_name IN (...)` clause with a synthesized value — see that
|
|
102
|
+
* module's docs.
|
|
103
|
+
*/
|
|
104
|
+
denyAllTables?: boolean;
|
|
90
105
|
/**
|
|
91
106
|
* Tenant visibility filter:
|
|
92
107
|
* - omitted/`undefined` → no tenant filter (all rows).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-feed.d.ts","sourceRoot":"","sources":["../src/change-feed.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"change-feed.d.ts","sourceRoot":"","sources":["../src/change-feed.ts"],"names":[],"mappings":"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"}
|
package/dist/change-feed.js
CHANGED
|
@@ -961,6 +961,10 @@ async function getChangesSince(db, options) {
|
|
|
961
961
|
params.push(since);
|
|
962
962
|
conditions.push(`seq <= ${next()}`);
|
|
963
963
|
params.push(servedHorizon);
|
|
964
|
+
if (options.denyAllTables) return {
|
|
965
|
+
changes: [],
|
|
966
|
+
cursor: servedHorizon
|
|
967
|
+
};
|
|
964
968
|
const tables = options.tables?.filter((table) => table.trim().length > 0).filter((table) => !isChangeFeedSensitiveTable(table.trim()));
|
|
965
969
|
if (tables && tables.length > 0) {
|
|
966
970
|
conditions.push(`table_name IN (${tables.map(() => next()).join(", ")})`);
|