@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.
- package/LICENSE +21 -0
- package/README.md +432 -2
- package/dist/body.d.ts +165 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +538 -0
- package/dist/body.js.map +1 -0
- package/dist/client.d.ts +132 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +376 -0
- package/dist/client.js.map +1 -0
- package/dist/contract.d.ts +261 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +83 -0
- package/dist/contract.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +81 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/mount.d.ts +219 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +372 -0
- package/dist/mount.js.map +1 -0
- package/dist/testing.d.ts +149 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +764 -0
- package/dist/testing.js.map +1 -0
- package/package.json +34 -8
package/dist/mount.d.ts
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,QAAQ,EACR,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACX,iBAAiB,EACjB,uBAAuB,EAEvB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAGpD;;+DAE+D;AAC/D,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB,CAAC;AAEF,uDAAuD;AACvD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,aAAa,CAAC;AAEtE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE;IACrC,QAAQ,EAAE,cAAc,CAAC;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,cAAc,CAAC;CACtB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEjC;;2CAE2C;AAC3C,eAAO,MAAM,qBAAqB,EAAE,gBAMnC,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,cAAc,EAAE,gBAA8B,CAAC;AA2B5D;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,aAAa,CAAC;AAE3D,uDAAuD;AACvD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,qBAAqB,CAAC;IAChC,MAAM,EAAE,sBAAsB,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC;+DAC2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,IAAI,EAAE,gBAAgB,CAAC;IACvB,sDAAsD;IACtD,IAAI,EAAE,eAAe,CAAC;IACtB,mDAAmD;IACnD,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAClD;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;2CACuC;IACvC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;0DACsD;IACtD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACzC;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC;IAEzC;;;;;;;;;OASG;IACH,IAAI,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1E,oEAAoE;IACpE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAClE,sEAAsE;IACtE,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACxE,+CAA+C;IAC/C,WAAW,CACT,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,GAAG,SAAS,EAC5D,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC;;;;;OAKG;IACH,KAAK,CACH,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,EACpD,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,WAAW,CACT,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC,CAAC;IAClD;gDAC4C;IAC5C,IAAI,CACF,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC/B;2EACuE;IACvE,UAAU,CACR,IAAI,EAAE;QACJ,EAAE,EAAE,QAAQ,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,qBAAqB,CAAC;KAC9B,EACD,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACpC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,WAAW,CAmThF"}
|
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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"}
|