@raybase/record-mount 0.0.2 → 0.3.4

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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * The mount: read authorization as a declared interface, over one
3
+ * namespace-scoped client.
4
+ *
5
+ * ## Two gates, and they are different questions
6
+ *
7
+ * **Gate 1 — the route's Raybase auth declaration.** `audience` and
8
+ * `policy`, exactly as a Raybase action declares them, validated through
9
+ * Core's own `validateActionPolicy` at mount time and evaluated through
10
+ * Core's own `actionAudienceAllows` / `evaluateActionPolicy` on every
11
+ * call. `defineRecordMount` REFUSES to mount without one — a record layer
12
+ * is not a surface on which "I forgot" may be spellable, so this is
13
+ * stricter than Core: an explicitly public mount says
14
+ * `{ audience: "public", policy: allowAll }` and means it. A viewer who
15
+ * fails this gate is refused `forbidden`: they may not call at all, and
16
+ * pretending the record layer is empty would be a lie about the system.
17
+ *
18
+ * **Gate 2 — the read-policy hook.** Envelope in, allow/deny out, applied
19
+ * to EVERY Row and every `get` the mount serves. A denial is ABSENCE, not
20
+ * an error: a row is filtered out of its page and a `get` answers
21
+ * `not_found`, because "you may not see this record" and "this record does
22
+ * not exist" must be indistinguishable to a caller who is not allowed to
23
+ * learn the difference. v0's default is `allowAnyAuthenticated`; what
24
+ * fills it is the consumer's design.
25
+ *
26
+ * ## The two verbs a per-envelope policy cannot filter
27
+ *
28
+ * `count` and `materialize` cannot be row-filtered. `count` is folded inside
29
+ * the door and comes back as totals, not records: filtering it would mean
30
+ * counting client-side over the whole namespace, and serving it unfiltered
31
+ * discloses the shape of a corpus the viewer may not read. `materialize`
32
+ * streams stored bytes — whole files, several records to a file in some
33
+ * forms — so there is no envelope for a per-envelope hook to judge at all.
34
+ *
35
+ * So they are gated, and the gate arms on a **pair**, decided once at mount
36
+ * time: **can the effective read policy deny a viewer this mount's audience
37
+ * admits?** Armed, both refuse `forbidden` unless the mount declared them in
38
+ * `unfilteredReads`.
39
+ *
40
+ * The pair is the whole point, because each half alone gets it wrong.
41
+ * Asking only "did the consumer install a custom policy?" fails OPEN: the
42
+ * default policy denies public principals, so `{audience: "public", policy:
43
+ * allowAll}` — a shape this package prescribes — served an anonymous caller
44
+ * the namespace's totals and its stored bytes while `list` served that same
45
+ * caller nothing. Asking only "can this policy deny anybody?" fails NOISY:
46
+ * the default denies *some* principal, so every mount would arm and
47
+ * `unfilteredReads` would become mandatory boilerplate rather than a signal
48
+ * a reviewer can read.
49
+ *
50
+ * Worked through: `{signed-in, allowAnyAuthenticated}` admits only
51
+ * authenticated viewers and the default allows every one of them — nothing
52
+ * deniable, unarmed, both verbs flow. `{public, allowAnyAuthenticated}`
53
+ * admits a class the policy denies — armed. Any policy this package cannot
54
+ * see inside — armed, because unrecognised must never be the permissive
55
+ * answer.
56
+ *
57
+ * `unguardedDoor` remains the general escape hatch, for a caller that wants
58
+ * the client with no gate at all and is willing to say so at the call site.
59
+ *
60
+ * ## What the mount does not do, deliberately
61
+ *
62
+ * The read-policy hook is not a write gate, and it is not asked about
63
+ * writes. A filing is authorized by gate 1 and then by the door's own
64
+ * write policy against the actor the binding's props name — an
65
+ * authorization the mount cannot see, cannot influence, and must not
66
+ * duplicate.
67
+ */
68
+ import type { ActionPolicyExpression, RaybaseActionAudience } from "@raybase/core";
69
+ import type { RaybasePrincipal, RaybaseUser } from "@raybase/core";
70
+ import type { RecordDoorClient } from "./client.js";
71
+ import type { RecordAppendResult, RecordCountGroup, RecordCountResult, RecordEnvelope, RecordGetResult, RecordId, RecordListQuery, RecordListResult, RecordMaterializeEntry, RecordQuery, RecordQueryResult, RecordReverseRefsResult, RecordTransitionEvent, RecordTransitionResult } from "./contract.js";
72
+ import type { RecordBodyComposer } from "./body.js";
73
+ /** Who is reading. The server-owned principal is the authority; `user` is
74
+ * the convenience projection Core also carries. An agent run arrives as kind
75
+ * `agent`, and its authority is the principal it acts for. */
76
+ export type RecordViewer = {
77
+ principal: RaybasePrincipal;
78
+ user?: RaybaseUser;
79
+ };
80
+ /** The read verbs a policy hook may be asked about. */
81
+ export type RecordReadVerb = "get" | "list" | "query" | "reverseRefs";
82
+ /**
83
+ * The declared read-authorization interface: envelope in, allow/deny out.
84
+ *
85
+ * It is called once per envelope, for every envelope the mount is about to
86
+ * hand back. It may be async — a hook that reads app tables is the expected
87
+ * case — and it is called at bounded concurrency
88
+ * (`readPolicyConcurrency`, default 16) rather than all at once, so a page
89
+ * of a thousand rows does not become a thousand simultaneous queries. A
90
+ * hook that repeats work across envelopes should still memoize per request.
91
+ *
92
+ * A hook that throws is not treated as a denial. It propagates: a policy
93
+ * that cannot decide has not decided, and swallowing that into "deny"
94
+ * would turn an outage into a silently empty page.
95
+ *
96
+ * The viewer arrives as given, so a hook can tell an agent from the person it
97
+ * acts for. A hook that judges by who is reading should read
98
+ * `principalOnBehalfOf(viewer.principal)`: an agent acting for a public visitor
99
+ * is not kind `public`, and it must not read as signed in.
100
+ */
101
+ export type RecordReadPolicy = (input: {
102
+ envelope: RecordEnvelope;
103
+ viewer: RecordViewer;
104
+ verb: RecordReadVerb;
105
+ }) => boolean | Promise<boolean>;
106
+ /** v0's default: any principal that is not anonymous may read every
107
+ * record the mount's namespace holds. Disabled users are not readers. An agent
108
+ * counts as the principal it acts for. */
109
+ export declare const allowAnyAuthenticated: RecordReadPolicy;
110
+ /** Deny everything. Useful as a base for a mount that opts rows in. */
111
+ export declare const denyAllRecords: RecordReadPolicy;
112
+ /**
113
+ * The two read verbs a per-envelope policy cannot filter, and which a mount
114
+ * carrying a custom policy must therefore declare it wants unfiltered.
115
+ * See this module's header for why each is unfilterable.
116
+ */
117
+ export type RecordUnfilteredRead = "count" | "materialize";
118
+ /** The Raybase auth declaration a mount must carry. */
119
+ export type RecordMountAuth = {
120
+ audience: RaybaseActionAudience;
121
+ policy: ActionPolicyExpression;
122
+ };
123
+ export type DefineRecordMountOptions = {
124
+ /** A name for this mount, used in refusals and in Core's own policy
125
+ * validation messages. Same charset as an action name. */
126
+ name: string;
127
+ /** The one namespace-scoped client this mount serves. One mount, one
128
+ * door, one namespace — the scoping is structural. */
129
+ door: RecordDoorClient;
130
+ /** The route's Raybase auth declaration. Required. */
131
+ auth: RecordMountAuth;
132
+ /** Gate 2. Defaults to `allowAnyAuthenticated`. */
133
+ readPolicy?: RecordReadPolicy;
134
+ /**
135
+ * The unfilterable read verbs this mount deliberately serves whole. Only
136
+ * consulted when a custom `readPolicy` is installed; with the default
137
+ * policy both verbs serve normally.
138
+ */
139
+ unfilteredReads?: readonly RecordUnfilteredRead[];
140
+ /**
141
+ * How many envelopes the read policy is asked about at once. The hook is
142
+ * expected to read app tables, and a page may carry up to a thousand
143
+ * rows — so this is bounded rather than a `Promise.all` over the page.
144
+ * Default 16.
145
+ */
146
+ readPolicyConcurrency?: number;
147
+ /**
148
+ * Service principals, and agents acting for a service, are refused by
149
+ * default (see `gateRoute`). Set this when a job, an API client or a
150
+ * scheduled agent legitimately fronts this mount.
151
+ */
152
+ allowServicePrincipals?: boolean;
153
+ /** The body builder, for a mount that files. Declaring one against a
154
+ * read door refuses at mount time. */
155
+ composer?: RecordBodyComposer;
156
+ };
157
+ export type RecordMount = {
158
+ readonly name: string;
159
+ /** The declared namespace this mount serves. */
160
+ readonly namespace: string;
161
+ /** The frozen auth declaration — spread it onto the action or route
162
+ * that fronts this mount so the two cannot drift. */
163
+ readonly auth: Readonly<RecordMountAuth>;
164
+ /**
165
+ * The client, ungated. Every call through it bypasses gate 2 and is the
166
+ * caller's to justify. Named this way so nobody reaches it by accident.
167
+ */
168
+ readonly unguardedDoor: RecordDoorClient;
169
+ /**
170
+ * One page of rows, gate-2 filtered.
171
+ *
172
+ * Filtering shrinks a page: a `limit: 50` list whose viewer may read
173
+ * twelve of them returns twelve rows and the page's own cursor. The
174
+ * cursor is still correct — it is the door's keyset position, not a
175
+ * count — so a consumer that needs full pages loops until it has
176
+ * enough. This is stated rather than hidden because a shrinking page is
177
+ * exactly what a row policy costs.
178
+ */
179
+ list(q: RecordListQuery, viewer: RecordViewer): Promise<RecordListResult>;
180
+ /** One record, or `not_found` — including when gate 2 denies it. */
181
+ get(id: RecordId, viewer: RecordViewer): Promise<RecordGetResult>;
182
+ /** The substrate query, gate-2 filtered. Same page-shrinking rule. */
183
+ query(q: RecordQuery, viewer: RecordViewer): Promise<RecordQueryResult>;
184
+ /** What cites this record, gate-2 filtered. */
185
+ reverseRefs(id: RecordId, opts: {
186
+ limit?: number;
187
+ order?: "asc" | "desc";
188
+ } | undefined, viewer: RecordViewer): Promise<RecordReverseRefsResult>;
189
+ /**
190
+ * Totals by lifecycle column. Under a custom read policy this refuses
191
+ * `forbidden` unless the mount declared `"count"` in `unfilteredReads` —
192
+ * an aggregate cannot be row-filtered, and an unfiltered one discloses
193
+ * the shape of what the viewer may not read.
194
+ */
195
+ count(args: {
196
+ family?: string;
197
+ groupBy: RecordCountGroup;
198
+ }, viewer: RecordViewer): Promise<RecordCountResult>;
199
+ /**
200
+ * Stored bytes under a prefix, as one iterator over the door's pages.
201
+ * Under a custom read policy this refuses `forbidden` unless the mount
202
+ * declared `"materialize"` in `unfilteredReads` — these entries are
203
+ * files, not envelopes, so a per-envelope hook has nothing to judge.
204
+ */
205
+ materialize(prefix: string, viewer: RecordViewer): Promise<AsyncIterable<RecordMaterializeEntry>>;
206
+ /** Compose a family's canonical body from a form's input and append it.
207
+ * Requires a composer and a write door. */
208
+ file(family: string, input: Record<string, unknown>, viewer: RecordViewer): Promise<RecordAppendResult>;
209
+ /** Move a record's lifecycle. Gate 1 only; the door's write policy is
210
+ * the authorization that matters, and it is not ours to duplicate. */
211
+ transition(args: {
212
+ id: RecordId;
213
+ expected: string;
214
+ next: string;
215
+ event: RecordTransitionEvent;
216
+ }, viewer: RecordViewer): Promise<RecordTransitionResult>;
217
+ };
218
+ export declare function defineRecordMount(options: DefineRecordMountOptions): RecordMount;
219
+ //# sourceMappingURL=mount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"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"}
package/dist/mount.js ADDED
@@ -0,0 +1,372 @@
1
+ /**
2
+ * The mount: read authorization as a declared interface, over one
3
+ * namespace-scoped client.
4
+ *
5
+ * ## Two gates, and they are different questions
6
+ *
7
+ * **Gate 1 — the route's Raybase auth declaration.** `audience` and
8
+ * `policy`, exactly as a Raybase action declares them, validated through
9
+ * Core's own `validateActionPolicy` at mount time and evaluated through
10
+ * Core's own `actionAudienceAllows` / `evaluateActionPolicy` on every
11
+ * call. `defineRecordMount` REFUSES to mount without one — a record layer
12
+ * is not a surface on which "I forgot" may be spellable, so this is
13
+ * stricter than Core: an explicitly public mount says
14
+ * `{ audience: "public", policy: allowAll }` and means it. A viewer who
15
+ * fails this gate is refused `forbidden`: they may not call at all, and
16
+ * pretending the record layer is empty would be a lie about the system.
17
+ *
18
+ * **Gate 2 — the read-policy hook.** Envelope in, allow/deny out, applied
19
+ * to EVERY Row and every `get` the mount serves. A denial is ABSENCE, not
20
+ * an error: a row is filtered out of its page and a `get` answers
21
+ * `not_found`, because "you may not see this record" and "this record does
22
+ * not exist" must be indistinguishable to a caller who is not allowed to
23
+ * learn the difference. v0's default is `allowAnyAuthenticated`; what
24
+ * fills it is the consumer's design.
25
+ *
26
+ * ## The two verbs a per-envelope policy cannot filter
27
+ *
28
+ * `count` and `materialize` cannot be row-filtered. `count` is folded inside
29
+ * the door and comes back as totals, not records: filtering it would mean
30
+ * counting client-side over the whole namespace, and serving it unfiltered
31
+ * discloses the shape of a corpus the viewer may not read. `materialize`
32
+ * streams stored bytes — whole files, several records to a file in some
33
+ * forms — so there is no envelope for a per-envelope hook to judge at all.
34
+ *
35
+ * So they are gated, and the gate arms on a **pair**, decided once at mount
36
+ * time: **can the effective read policy deny a viewer this mount's audience
37
+ * admits?** Armed, both refuse `forbidden` unless the mount declared them in
38
+ * `unfilteredReads`.
39
+ *
40
+ * The pair is the whole point, because each half alone gets it wrong.
41
+ * Asking only "did the consumer install a custom policy?" fails OPEN: the
42
+ * default policy denies public principals, so `{audience: "public", policy:
43
+ * allowAll}` — a shape this package prescribes — served an anonymous caller
44
+ * the namespace's totals and its stored bytes while `list` served that same
45
+ * caller nothing. Asking only "can this policy deny anybody?" fails NOISY:
46
+ * the default denies *some* principal, so every mount would arm and
47
+ * `unfilteredReads` would become mandatory boilerplate rather than a signal
48
+ * a reviewer can read.
49
+ *
50
+ * Worked through: `{signed-in, allowAnyAuthenticated}` admits only
51
+ * authenticated viewers and the default allows every one of them — nothing
52
+ * deniable, unarmed, both verbs flow. `{public, allowAnyAuthenticated}`
53
+ * admits a class the policy denies — armed. Any policy this package cannot
54
+ * see inside — armed, because unrecognised must never be the permissive
55
+ * answer.
56
+ *
57
+ * `unguardedDoor` remains the general escape hatch, for a caller that wants
58
+ * the client with no gate at all and is willing to say so at the call site.
59
+ *
60
+ * ## What the mount does not do, deliberately
61
+ *
62
+ * The read-policy hook is not a write gate, and it is not asked about
63
+ * writes. A filing is authorized by gate 1 and then by the door's own
64
+ * write policy against the actor the binding's props name — an
65
+ * authorization the mount cannot see, cannot influence, and must not
66
+ * duplicate.
67
+ */
68
+ import { actionAudienceAllows, evaluateActionPolicy, principalOnBehalfOf, validateActionPolicy } from "@raybase/core";
69
+ import { recordDoorError } from "./errors.js";
70
+ /** v0's default: any principal that is not anonymous may read every
71
+ * record the mount's namespace holds. Disabled users are not readers. An agent
72
+ * counts as the principal it acts for. */
73
+ export const allowAnyAuthenticated = ({ viewer }) => {
74
+ const principal = principalOnBehalfOf(viewer.principal);
75
+ if (principal.kind === "public")
76
+ return false;
77
+ const user = principal.kind === "user" || principal.kind === "operator" ? principal.user : undefined;
78
+ if (user?.disabled === true)
79
+ return false;
80
+ return true;
81
+ };
82
+ /** Deny everything. Useful as a base for a mount that opts rows in. */
83
+ export const denyAllRecords = () => false;
84
+ /**
85
+ * Which viewer classes each KNOWN policy can deny — the data behind the
86
+ * unfilterable gate's arming decision.
87
+ *
88
+ * A policy in this table is one this package wrote and can therefore reason
89
+ * about. A policy absent from it is opaque: nobody can see inside a
90
+ * consumer's function, so it arms the gate unconditionally. That is also
91
+ * what dissolves the identity-comparison problem — a wrapper around a
92
+ * default and the bare default no longer differ in the dangerous direction,
93
+ * because being unrecognised arms rather than disarms.
94
+ *
95
+ * `allowAnyAuthenticated` also denies a disabled user, which is deliberately
96
+ * NOT listed: Core's session resolution returns null for a disabled account
97
+ * (`packages/core/src/auth.ts`), so no principal is built and that viewer
98
+ * never reaches this gate. The check in the policy is defence in depth, not
99
+ * an admitted class.
100
+ */
101
+ const POLICY_DENIES = new Map([
102
+ [allowAnyAuthenticated, ["public"]],
103
+ [denyAllRecords, ["public", "signed-in"]]
104
+ ]);
105
+ export function defineRecordMount(options) {
106
+ const { name, door } = options;
107
+ if (typeof name !== "string" || name.length === 0) {
108
+ throw new Error("defineRecordMount requires a name");
109
+ }
110
+ if (door === undefined || door === null || typeof door.list !== "function") {
111
+ throw new Error(`record mount ${name} requires a door opened with openRecordDoor()`);
112
+ }
113
+ const auth = options.auth;
114
+ if (auth === undefined || auth === null) {
115
+ throw new Error(`record mount ${name} must declare Raybase auth: { audience, policy }. A record mount serves ` +
116
+ "records to people; an undeclared route is not a default, it is an omission.");
117
+ }
118
+ if (auth.audience === undefined) {
119
+ throw new Error(`record mount ${name} must declare an audience ("signed-in" or "public")`);
120
+ }
121
+ if (auth.policy === undefined) {
122
+ // Core would let a public-only action omit its policy. A mount may
123
+ // not: "public" is a claim worth writing down beside an explicit
124
+ // allowAll, so that reading the declaration tells you what was meant.
125
+ throw new Error(`record mount ${name} must declare a policy, including on a public audience — say ` +
126
+ "policy: allowAll and mean it");
127
+ }
128
+ // Core's own rail does the rest: it refuses the row-dependent policy
129
+ // kinds (owns/operator/servicePrincipal), which belong to gate 2 here.
130
+ validateActionPolicy(auth.policy, name, auth.audience);
131
+ // A public audience admits a principal that carries no user, so a policy
132
+ // narrower than allowAll cannot be evaluated against it — and an
133
+ // unevaluable policy must not resolve in the permissive direction (the
134
+ // same rule the service principal gets below, applied where it is
135
+ // knowable at mount time). A declaration that admits anonymous callers
136
+ // AND names a restriction is contradictory, so it is refused here rather
137
+ // than silently reading as "anyone".
138
+ if (actionAudienceAllows(auth.audience, "public") && auth.policy.kind !== "allowAll") {
139
+ throw new Error(`record mount ${name} admits a public audience but declares policy '${auth.policy.kind}', ` +
140
+ "which cannot be evaluated against an anonymous principal. Either narrow the audience to " +
141
+ "\"signed-in\", or say policy: allowAll and mean it.");
142
+ }
143
+ const readPolicy = options.readPolicy ?? allowAnyAuthenticated;
144
+ /**
145
+ * Whether the unfilterable verbs are gated, decided once from the pair
146
+ * that actually determines it: **can the effective read policy deny a
147
+ * viewer this mount's audience admits?**
148
+ *
149
+ * Asking only "did the consumer install a custom policy?" was wrong in the
150
+ * dangerous direction. The default policy denies public principals, so a
151
+ * mount declared `{audience: "public", policy: allowAll}` — a shape this
152
+ * package's own README prescribes — served an anonymous caller the
153
+ * namespace's totals and its stored bytes while `list` handed the same
154
+ * caller zero rows.
155
+ *
156
+ * The pair gets both ends right. `{signed-in, allowAnyAuthenticated}`
157
+ * admits only authenticated viewers and the default allows every one of
158
+ * them, so nothing is deniable and `count`/`materialize` flow without a
159
+ * declaration. `{public, allowAnyAuthenticated}` admits a class the policy
160
+ * denies, so it arms. Anything opaque arms.
161
+ */
162
+ const deniable = POLICY_DENIES.get(readPolicy);
163
+ const armed = deniable === undefined ||
164
+ deniable.some((viewerClass) => actionAudienceAllows(auth.audience, viewerClass));
165
+ const unfiltered = new Set(options.unfilteredReads ?? []);
166
+ const concurrency = Math.max(1, Math.trunc(options.readPolicyConcurrency ?? 16));
167
+ const allowServicePrincipals = options.allowServicePrincipals === true;
168
+ const composer = options.composer;
169
+ /**
170
+ * The one refusal, written once, thrown from two moments.
171
+ *
172
+ * A composer over a read door is a deploy misconfiguration — the same class
173
+ * as an undeclared auth block, and like those it throws a plain `Error`
174
+ * rather than a `RecordDoorError`: it is not a refusal a caller can act on,
175
+ * it is a mount that should not exist.
176
+ */
177
+ const composerOverReadDoor = () => new Error(`record mount ${name} declares a body composer but its binding '${door.binding}' is a read ` +
178
+ "door — it carries no append verb. Bind the write entrypoint or drop the composer.");
179
+ // Mount time is the right moment whenever writability is already knowable,
180
+ // which on a plain binding it always is.
181
+ if (composer !== undefined && door.writable === false)
182
+ throw composerOverReadDoor();
183
+ /**
184
+ * The same check, at the earliest moment it can fire across an RPC seam.
185
+ *
186
+ * Over a real service binding `door.writable` is `"unknown"` until the
187
+ * client's contract check has resolved a `version()` reply, and
188
+ * `defineRecordMount` is synchronous — so the mount-time throw above simply
189
+ * cannot see a read door there. Rather than let the misconfiguration ride
190
+ * until somebody files, every verb consults this first: it forces the
191
+ * client's own (memoized) contract check, and refuses with the identical
192
+ * message the moment the door's declaration says `writes: false`.
193
+ *
194
+ * Reads pay for it too, deliberately. A mount that declares a composer over
195
+ * a read door is misconfigured whether or not anyone has filed yet, and
196
+ * surfacing that on the first read is how an operator learns at deploy
197
+ * smoke-test time instead of at the first user's first filing.
198
+ *
199
+ * It costs nothing on the mounts that cannot hit it: no composer, or a
200
+ * settled `writable`, returns before awaiting anything.
201
+ */
202
+ const ensureComposerDoorAgrees = async () => {
203
+ if (composer === undefined)
204
+ return;
205
+ // `writable` is a getter that sharpens once the client's contract check
206
+ // resolves, so this reads it twice deliberately: force the check only
207
+ // when the answer is not yet knowable, then read what it settled on.
208
+ if (door.writable === "unknown")
209
+ await door.assertContract();
210
+ if (door.writable === false)
211
+ throw composerOverReadDoor();
212
+ };
213
+ const frozenAuth = Object.freeze({ ...auth });
214
+ /**
215
+ * A refusal this mount authored, marked as such. `mount` is the
216
+ * distinguishing key: the door's own `forbidden` detail carries
217
+ * `{actor, verb, family, id}` and never a mount name, so a consumer can
218
+ * tell "the mount refused you" from "the door refused the app" without
219
+ * parsing a message. Sentinel values imitating the door's other keys
220
+ * would have read as door-authored, which is a refusal lying about where
221
+ * it came from.
222
+ */
223
+ const denied = (verb, why) => recordDoorError("forbidden", { mount: name, verb }, `forbidden: record mount ${name} refused ${verb} — ${why}`);
224
+ /**
225
+ * Gate 1, in Core's own order and with Core's own semantics.
226
+ *
227
+ * Every principal kind is answered for, deliberately. An `agent` is judged
228
+ * as the user, visitor or service it acts for, as Core's own action gate
229
+ * judges it: it carries that principal's authority and gains nothing by
230
+ * being an agent. That leaves four kinds, and the declared policy is
231
+ * evaluable against only two of them — so the other two are ruled here
232
+ * rather than falling through:
233
+ *
234
+ * - `public` — judged by the audience alone, which is what an audience is.
235
+ * - `user` and `operator` — both carry a `RaybaseUser`, so both are
236
+ * evaluated against the declared policy. An operator is not exempt: a
237
+ * mount that says `policy: role("chair")` means it, and Core's operator
238
+ * authority is about Core's own admin surfaces, not about a record layer
239
+ * this package knows nothing about.
240
+ * - `service` — carries no user, so a role policy is not merely unevaluated
241
+ * but unevaluable. It is refused unless the mount declared
242
+ * `allowServicePrincipals`, because "the policy could not be applied" must
243
+ * not read as "the policy passed".
244
+ */
245
+ const gateRoute = (viewer, verb) => {
246
+ if (viewer === undefined || viewer === null || viewer.principal === undefined) {
247
+ throw new Error(`record mount ${name}: ${verb} requires a viewer carrying the request's server-owned ` +
248
+ "principal. A browser client cannot construct one, which is the point.");
249
+ }
250
+ const principal = principalOnBehalfOf(viewer.principal);
251
+ const audience = principal.kind === "public" ? "public" : "signed-in";
252
+ if (!actionAudienceAllows(frozenAuth.audience, audience)) {
253
+ throw denied(verb, `audience '${audience}' is not admitted by this mount`);
254
+ }
255
+ if (principal.kind === "service") {
256
+ if (!allowServicePrincipals) {
257
+ throw denied(verb, "a service principal carries no user, so this mount's declared policy cannot be " +
258
+ "evaluated against it — declare allowServicePrincipals if that is intended");
259
+ }
260
+ return;
261
+ }
262
+ if (principal.kind === "public")
263
+ return;
264
+ if (!evaluateActionPolicy(frozenAuth.policy, principal.user)) {
265
+ throw denied(verb, "the mount's declared policy denied this viewer");
266
+ }
267
+ };
268
+ /**
269
+ * Gate 2 over a page, at bounded concurrency.
270
+ *
271
+ * The hook is expected to read app tables and a page may carry up to a
272
+ * thousand rows, so a `Promise.all` over the page would aim the whole page
273
+ * at the database at once — the unbounded fan-out that is carelessness
274
+ * rather than deferred optimization. Order is preserved: verdicts are
275
+ * written back by index, never by completion.
276
+ */
277
+ const filter = async (envelopes, viewer, verb) => {
278
+ const verdicts = new Array(envelopes.length);
279
+ let next = 0;
280
+ const worker = async () => {
281
+ for (let i = next++; i < envelopes.length; i = next++) {
282
+ verdicts[i] = (await readPolicy({ envelope: envelopes[i], viewer, verb })) === true;
283
+ }
284
+ };
285
+ const workers = Math.min(concurrency, envelopes.length);
286
+ await Promise.all(Array.from({ length: workers }, worker));
287
+ return envelopes.filter((_, i) => verdicts[i]);
288
+ };
289
+ /** The fail-closed gate on the two verbs a per-envelope hook cannot judge. */
290
+ const gateUnfilterable = (verb) => {
291
+ if (!armed || unfiltered.has(verb))
292
+ return;
293
+ throw denied(verb, `'${verb}' cannot be filtered by a per-envelope read policy, and this mount's policy can ` +
294
+ `deny a viewer its audience admits. Declare unfilteredReads: ["${verb}"] if namespace-wide ` +
295
+ `${verb} is intended.`);
296
+ };
297
+ return {
298
+ name,
299
+ namespace: door.namespace,
300
+ auth: frozenAuth,
301
+ unguardedDoor: door,
302
+ async list(q, viewer) {
303
+ gateRoute(viewer, "list");
304
+ await ensureComposerDoorAgrees();
305
+ const page = await door.list(q);
306
+ const rows = await filter(page.rows, viewer, "list");
307
+ return page.cursor === undefined ? { rows } : { rows, cursor: page.cursor };
308
+ },
309
+ async get(id, viewer) {
310
+ gateRoute(viewer, "get");
311
+ await ensureComposerDoorAgrees();
312
+ const found = await door.get(id);
313
+ const allowed = await readPolicy({ envelope: found.envelope, viewer, verb: "get" });
314
+ if (allowed !== true) {
315
+ // Absence, not a leak — and byte-identical absence, which is the
316
+ // whole property. The message is built from the SAME default the
317
+ // door's own `not_found` uses (`${code}: ${JSON.stringify(detail)}`)
318
+ // rather than written out here: a bespoke `not_found: <id>` matched
319
+ // the door on `detail` and differed on `message`, so one probe per
320
+ // id was still an existence oracle. Passing no message is not
321
+ // laziness, it is the mechanism — there is one shape and neither
322
+ // side spells it twice.
323
+ throw recordDoorError("not_found", { id });
324
+ }
325
+ return found;
326
+ },
327
+ async query(q, viewer) {
328
+ gateRoute(viewer, "query");
329
+ await ensureComposerDoorAgrees();
330
+ const page = await door.query(q);
331
+ const envelopes = await filter(page.envelopes, viewer, "query");
332
+ return page.cursor === undefined ? { envelopes } : { envelopes, cursor: page.cursor };
333
+ },
334
+ async reverseRefs(id, opts, viewer) {
335
+ gateRoute(viewer, "reverseRefs");
336
+ await ensureComposerDoorAgrees();
337
+ const found = await door.reverseRefs(id, opts);
338
+ return { rows: await filter(found.rows, viewer, "reverseRefs") };
339
+ },
340
+ async count(args, viewer) {
341
+ gateRoute(viewer, "count");
342
+ await ensureComposerDoorAgrees();
343
+ gateUnfilterable("count");
344
+ return door.count(args);
345
+ },
346
+ async materialize(prefix, viewer) {
347
+ gateRoute(viewer, "materialize");
348
+ await ensureComposerDoorAgrees();
349
+ gateUnfilterable("materialize");
350
+ return door.materialize(prefix);
351
+ },
352
+ async file(family, input, viewer) {
353
+ gateRoute(viewer, "file");
354
+ await ensureComposerDoorAgrees();
355
+ if (composer === undefined) {
356
+ throw new Error(`record mount ${name} was not given a body composer, so it cannot file. Pass one to ` +
357
+ "defineRecordMount() with the family descriptors this mount accepts.");
358
+ }
359
+ // The composer refuses in the door's own taxonomy, so a malformed
360
+ // filing and a rejected filing reach the caller identically — and
361
+ // neither is a 500.
362
+ const body = composer.compose(family, input);
363
+ return door.append(body);
364
+ },
365
+ async transition(args, viewer) {
366
+ gateRoute(viewer, "transition");
367
+ await ensureComposerDoorAgrees();
368
+ return door.transition(args.id, args.expected, args.next, args.event);
369
+ }
370
+ };
371
+ }
372
+ //# sourceMappingURL=mount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"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"}