@githolon/dsl 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,418 +0,0 @@
1
- // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
- // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
- // wasm32-wasip1 artifact {kernel · projection · embedded
4
- // QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
5
- // wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
- // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
-
8
- /**
9
- * framework/rbac.ts — the FRAMEWORK RBAC mechanism (tenant-agnostic).
10
- *
11
- * ── Why this module exists (the framework↔tenant line) ───────────────────────
12
- * Multi-layer RBAC role-bindings are framework MECHANISM, not tenant content. The
13
- * kernel ships the engine (`identity::authz` — `PermissionState::bindings_for` +
14
- * `effective_caps_for` + chain delegation + captured-deps) and the one shared
15
- * `admit` gate resolves the DECLARED `.requires(cap, scopeFrom)` pre-condition
16
- * against the folded permission projection. A tenant should therefore NOT
17
- * re-implement grant/revoke/delegate — it should DECLARE its roles→capabilities
18
- * (a `RoleCatalogue`) and its resource vocabulary, then USE these framework
19
- * primitives to author the binding-leaf writes.
20
- *
21
- * This module is the DSL surface for that mechanism:
22
- * • `roleBindingSchema(resourceTypes)` — the stored binding-leaf shape (the
23
- * EXACT JSON the kernel `identity::authz::RoleBinding` parses out of the
24
- * `UserPermission.permissions` map);
25
- * • `userPermissionAggregate(resourceTypes)` — the per-user aggregate shape +
26
- * the `userPermissionsInstanceId`/`permissionId` deterministic id rules
27
- * (`user-permissions-<userId>`, `perm-<userId>-<resourceType>-<resourceId>`);
28
- * • `grant` / `revoke` / `delegate` / `revokeDelegation` — the four primitive
29
- * PLAN BUILDERS. Each takes a `scope = [resourceType, resourceId]` (the
30
- * framework's universal scope shape) + the actor/role/provenance and returns
31
- * the `PlannedOp[]` that write the SAME binding leaf the authz engine reads.
32
- * • `roleCatalogue(roles)` — the declaration helper a tenant uses to declare its
33
- * roles→capabilities (the `RoleCatalogue` the kernel resolves a `role` string
34
- * against; opaque to the framework — the framework provides the SHAPE, the
35
- * tenant the CONTENT).
36
- *
37
- * ── CRITICAL — the kernel-read wire shape is FROZEN (`authz.rs` reads it) ──────
38
- * `identity::authz::PermissionState::bindings_for` reads JSON `RoleBinding` leaves
39
- * out of `UserPermission.permissions` (a `MapOf`, keyed
40
- * `user-permissions-<userId>`). The AUTHZ-RELEVANT fields the kernel parses are
41
- * frozen — `permissionId / userId / resourceType / resourceId / role / status /
42
- * grantedBy / grantedAt`, the revocation provenance (`revokedBy / revokedAt /
43
- * revocationReason`), and the OPTIONAL delegation fields (`delegator / attenuation
44
- * / expires`) which the kernel serde-defaults so a plain grant leaf is unchanged.
45
- * The OPTIONAL book-keeping (`displayName / lastViewed / resourceCount`) is NOT in
46
- * the kernel's `RoleBinding` struct — serde drops unknown leaf fields — so it is
47
- * invisible to authz and free to use framework-generic names (`resourceCount` is
48
- * the tenant-agnostic book-keeping count; a tenant maps its own domain noun onto
49
- * it at the directive surface). This is a refactor of WHO authors the leaf, NOT a
50
- * change to any field the kernel reads.
51
- */
52
- import { z } from "zod";
53
- import { aggregate, instance, type AggregateHandle } from "../aggregate.js";
54
- import { t } from "../fields.js";
55
- import { Lww, MapOf } from "../drivers.js";
56
- import { set, setEntry, type PlannedOp } from "../ops.js";
57
-
58
- /** Permission lifecycle — the framework binding-status vocabulary. */
59
- export const PERMISSION_STATUSES = ["active", "revoked", "suspended"] as const;
60
- export type PermissionStatus = (typeof PERMISSION_STATUSES)[number];
61
-
62
- /**
63
- * The framework binding-leaf schema, parameterised over a tenant's resource-type
64
- * vocabulary. A binding is the `(actor, role, scope)` triple from
65
- * identity-access §"Multi-layer RBAC", scope = `(resourceType, resourceId)`, plus
66
- * provenance, optional book-keeping, and the optional delegation fields. This
67
- * lowers to ONE JSON Str leaf (the kernel stores Str|Int leaves only — gap G2).
68
- *
69
- * `resourceTypes` is the tenant's enum; the framework does not fix it. The rest of
70
- * the shape is framework-fixed because the kernel `authz.rs` `RoleBinding` parses
71
- * it field-for-field.
72
- */
73
- export function roleBindingSchema<const T extends readonly [string, ...string[]]>(
74
- resourceTypes: T,
75
- ) {
76
- return z.object({
77
- permissionId: z.string(),
78
- userId: z.string(),
79
- // scope = (resourceType, resourceId), the org→workspace→resource address.
80
- resourceType: z.enum(resourceTypes),
81
- resourceId: z.string(),
82
- // role within that scope (a role = a capability set; resolved by the kernel
83
- // authz slice against the tenant `RoleCatalogue`; opaque string here).
84
- role: z.string(),
85
- status: z.enum(PERMISSION_STATUSES),
86
- grantedBy: z.string(),
87
- grantedAt: z.string(),
88
- // book-keeping (tenant `updatePermissionMetadata`): all optional.
89
- displayName: z.string().optional(),
90
- lastViewed: z.string().optional(),
91
- resourceCount: z.number().int().optional(),
92
- // revocation provenance, set when status flips to "revoked".
93
- revokedBy: z.string().optional(),
94
- revokedAt: z.string().optional(),
95
- revocationReason: z.string().optional(),
96
- // ── DELEGATION (identity-access §"delegated authority") ──
97
- // `delegator` set ⇒ this binding is a DELEGATION (actor A handed a capability
98
- // subset down to `userId`). Honoured only if A held those caps at the
99
- // evaluation point-in-time (kernel `effective_caps_for` recurses to a
100
- // delegator-less root). All three OPTIONAL so a plain grant leaf is unchanged.
101
- delegator: z.string().optional(),
102
- // the capability subset handed down (the attenuation ceiling).
103
- attenuation: z.array(z.string()).optional(),
104
- // point-in-time expiry (ISO/HLC string mirror).
105
- expires: z.string().optional(),
106
- });
107
- }
108
-
109
- /**
110
- * The generic framework binding-leaf type. A tenant narrows `resourceType` via its
111
- * own `roleBindingSchema(RESOURCE_TYPES)`; the framework primitives below accept a
112
- * plain `string` resourceType (the tenant's enum is a subtype) so the mechanism is
113
- * tenant-agnostic.
114
- */
115
- export type FrameworkRoleBinding = {
116
- permissionId: string;
117
- userId: string;
118
- resourceType: string;
119
- resourceId: string;
120
- role: string;
121
- status: PermissionStatus;
122
- grantedBy: string;
123
- grantedAt: string;
124
- displayName?: string;
125
- lastViewed?: string;
126
- resourceCount?: number;
127
- revokedBy?: string;
128
- revokedAt?: string;
129
- revocationReason?: string;
130
- delegator?: string;
131
- attenuation?: string[];
132
- expires?: string;
133
- };
134
-
135
- /**
136
- * The per-user permissions aggregate SHAPE — framework mechanism. Wire id
137
- * `UserPermission` matches the live aggregate type the kernel folds and
138
- * `bindings_for` reads. Multi-instance, keyed `user-permissions-<userId>`.
139
- *
140
- * - userId Lww (identity)
141
- * - permissions MapOf(Lww) — the map-of-driven role-binding entries; each value
142
- * is a JSON-encoded binding (a `t.json` Str leaf), merged per-key
143
- * by Lww (the closest-expressible map driver — gap G1)
144
- * - lastViewedAt Lww (aggregate-level "last touched")
145
- */
146
- export const userPermissionAggregate = aggregate("UserPermission", {
147
- userId: t.string().merge(Lww),
148
- permissions: t.map(t.json()).merge(MapOf(Lww)),
149
- // The grant/ensure primitive folds ONLY userId + a permissions entry (writeBinding),
150
- // never lastViewedAt — a freshly-ensured per-user record lacks it — OPTIONAL on
151
- // the read type (read-decode gap).
152
- lastViewedAt: t.string().merge(Lww).optional(),
153
- });
154
- export type UserPermissionAggregateHandle = typeof userPermissionAggregate;
155
-
156
- /**
157
- * Deterministic per-user aggregate instance id — `user-permissions-<userId>`.
158
- * Reproduces the Dart aggregate-id rule so two users' bindings fold into two
159
- * distinct aggregates (never colliding on the shared type) and replay is stable.
160
- */
161
- export function userPermissionsInstanceId(userId: string): string {
162
- return `user-permissions-${userId}`;
163
- }
164
-
165
- /**
166
- * Deterministic permission-id (map key) — `perm-<userId>-<resourceType>-<resourceId>`.
167
- * Reproduces the Dart `execute()` rule so the same (user, scope) always maps to the
168
- * same map key (kills the dup-grant re-dispatch class).
169
- */
170
- export function permissionId(input: {
171
- userId: string;
172
- resourceType: string;
173
- resourceId: string;
174
- }): string {
175
- return `perm-${input.userId}-${input.resourceType}-${input.resourceId}`;
176
- }
177
-
178
- /**
179
- * A framework scope: `[resourceType, resourceId]`. This is the universal scope the
180
- * framework `grant`/`revoke`/`delegate` primitives take and the same shape a
181
- * directive's `.requires(cap, p => [resourceType, resourceId])` derives so the
182
- * authz gate checks the actor holds the capability at a covering scope.
183
- */
184
- export type Scope = readonly [resourceType: string, resourceId: string];
185
-
186
- /** Bind the per-user aggregate handle to a concrete user instance. */
187
- function userInstance(userId: string) {
188
- return instance(userPermissionAggregate, userPermissionsInstanceId(userId));
189
- }
190
-
191
- /**
192
- * Strip `undefined`-valued keys so the JSON leaf is byte-stable (no `"k":null`,
193
- * no `"k":undefined`-dropped key carrying a slot). Returns a `FrameworkRoleBinding`
194
- * — the omitted-when-undefined optionals are exactly the schema's `.optional()`s.
195
- */
196
- function compact(obj: Record<string, unknown>): FrameworkRoleBinding {
197
- const out: Record<string, unknown> = {};
198
- for (const [k, v] of Object.entries(obj)) {
199
- if (v !== undefined) out[k] = v;
200
- }
201
- return out as FrameworkRoleBinding;
202
- }
203
-
204
- /** Common fields every grant-family primitive consumes. */
205
- interface BindingInput {
206
- userId: string;
207
- scope: Scope;
208
- role: string;
209
- grantedBy: string;
210
- grantedAt: string;
211
- displayName?: string | undefined;
212
- }
213
-
214
- /**
215
- * Build a role-binding leaf for one (user, scope) pair. The single place the
216
- * framework constructs the wire leaf the kernel `authz.rs` reads — every primitive
217
- * routes through here so the shape can never drift.
218
- */
219
- function buildBinding(
220
- p: BindingInput & {
221
- status: PermissionStatus;
222
- revokedBy?: string | undefined;
223
- revokedAt?: string | undefined;
224
- revocationReason?: string | undefined;
225
- delegator?: string | undefined;
226
- attenuation?: string[] | undefined;
227
- expires?: string | undefined;
228
- lastViewed?: string | undefined;
229
- resourceCount?: number | undefined;
230
- },
231
- ): { pid: string; binding: FrameworkRoleBinding } {
232
- const [resourceType, resourceId] = p.scope;
233
- const pid = permissionId({ userId: p.userId, resourceType, resourceId });
234
- const binding = compact({
235
- permissionId: pid,
236
- userId: p.userId,
237
- resourceType,
238
- resourceId,
239
- role: p.role,
240
- status: p.status,
241
- grantedBy: p.grantedBy,
242
- grantedAt: p.grantedAt,
243
- displayName: p.displayName,
244
- lastViewed: p.lastViewed,
245
- resourceCount: p.resourceCount,
246
- revokedBy: p.revokedBy,
247
- revokedAt: p.revokedAt,
248
- revocationReason: p.revocationReason,
249
- delegator: p.delegator,
250
- attenuation: p.attenuation,
251
- expires: p.expires,
252
- });
253
- return { pid, binding };
254
- }
255
-
256
- /**
257
- * Lower a built binding to the `PlannedOp[]` for a per-user aggregate. `ensure`
258
- * (the default) also writes the `userId` Set so the first grant materialises the
259
- * aggregate (matching `EnsuresEntityPayload`); a plain status-flip (revoke) omits
260
- * it (`ensure = false`) since the aggregate already exists.
261
- */
262
- function writeBinding(
263
- userId: string,
264
- pid: string,
265
- binding: FrameworkRoleBinding,
266
- ensure = true,
267
- ): PlannedOp[] {
268
- const upa = userInstance(userId);
269
- const ops: PlannedOp[] = [];
270
- if (ensure) ops.push(set(upa, "userId", userId));
271
- ops.push(setEntry(upa, "permissions", pid, JSON.stringify(binding)));
272
- return ops;
273
- }
274
-
275
- /**
276
- * grant — the framework PRIMITIVE: write one ACTIVE role-binding for `userId` at
277
- * `scope`. The collapse target of a tenant grant-family directive (the scope's
278
- * `resourceType` is fixed per tenant directive). Ensures the per-user aggregate.
279
- */
280
- export function grant(p: BindingInput): PlannedOp[] {
281
- const { pid, binding } = buildBinding({ ...p, status: "active" });
282
- return writeBinding(p.userId, pid, binding, true);
283
- }
284
-
285
- /**
286
- * revoke — the framework PRIMITIVE: flip the binding at `scope` to `status:revoked`
287
- * (an Lww write over the SAME map entry — gap G1: ideally remove-wins). Re-authors
288
- * the full leaf since a JSON leaf is replaced wholesale by Lww.
289
- */
290
- export function revoke(
291
- p: BindingInput & {
292
- revokedBy: string;
293
- revokedAt: string;
294
- reason?: string | undefined;
295
- },
296
- ): PlannedOp[] {
297
- const { pid, binding } = buildBinding({
298
- ...p,
299
- status: "revoked",
300
- revokedBy: p.revokedBy,
301
- revokedAt: p.revokedAt,
302
- revocationReason: p.reason,
303
- });
304
- return writeBinding(p.userId, pid, binding, false);
305
- }
306
-
307
- /**
308
- * delegate — the framework PRIMITIVE: actor `delegatedBy` (A) hands a capability
309
- * subset down to `userId` (B) at `scope`, writing a `delegator`-stamped, attenuated
310
- * ACTIVE binding into B's per-user map. Mirrors `grant`'s write shape but the leaf
311
- * carries `delegator`/`attenuation`/`expires`. Self-authenticating (A signs the
312
- * authoring intent); honoured only if A held the caps point-in-time (kernel
313
- * `effective_caps_for`). Ensures B's aggregate.
314
- */
315
- export function delegate(p: {
316
- userId: string;
317
- scope: Scope;
318
- role: string;
319
- attenuation: string[];
320
- delegatedBy: string;
321
- grantedAt: string;
322
- expires?: string | undefined;
323
- displayName?: string | undefined;
324
- }): PlannedOp[] {
325
- const { pid, binding } = buildBinding({
326
- userId: p.userId,
327
- scope: p.scope,
328
- role: p.role,
329
- grantedBy: p.delegatedBy,
330
- grantedAt: p.grantedAt,
331
- status: "active",
332
- delegator: p.delegatedBy,
333
- attenuation: p.attenuation,
334
- expires: p.expires,
335
- displayName: p.displayName,
336
- });
337
- return writeBinding(p.userId, pid, binding, true);
338
- }
339
-
340
- /**
341
- * revokeDelegation — the framework PRIMITIVE: flip a delegated binding to revoked
342
- * (an Lww write over the SAME entry, like `revoke`, but carrying the
343
- * `delegator`/`attenuation` provenance). Revoking the DELEGATOR's ROOT binding (a
344
- * plain `revoke`) is the load-bearing case — the chained delegate loses its caps
345
- * point-in-time via the fold with NO propagation; this is the symmetric surface
346
- * for revoking a delegated leaf directly. Mutates (the aggregate exists).
347
- */
348
- export function revokeDelegation(p: {
349
- userId: string;
350
- scope: Scope;
351
- role: string;
352
- attenuation: string[];
353
- delegatedBy: string;
354
- grantedAt: string;
355
- revokedBy: string;
356
- revokedAt: string;
357
- reason?: string | undefined;
358
- }): PlannedOp[] {
359
- const { pid, binding } = buildBinding({
360
- userId: p.userId,
361
- scope: p.scope,
362
- role: p.role,
363
- grantedBy: p.delegatedBy,
364
- grantedAt: p.grantedAt,
365
- status: "revoked",
366
- delegator: p.delegatedBy,
367
- attenuation: p.attenuation,
368
- revokedBy: p.revokedBy,
369
- revokedAt: p.revokedAt,
370
- revocationReason: p.reason,
371
- });
372
- return writeBinding(p.userId, pid, binding, false);
373
- }
374
-
375
- /**
376
- * updateMetadata — the framework PRIMITIVE for the book-keeping write
377
- * (`lastViewed`/`resourceCount`/`displayName`) on an existing ACTIVE binding, plus the
378
- * aggregate-level `lastViewedAt`. NOT authz (it never changes role/scope/status) —
379
- * a tenant wraps it as its own directive (e.g. `updatePermissionMetadata`).
380
- */
381
- export function updateMetadata(
382
- p: BindingInput & {
383
- lastViewed?: string | undefined;
384
- resourceCount?: number | undefined;
385
- },
386
- ): PlannedOp[] {
387
- const { pid, binding } = buildBinding({
388
- ...p,
389
- status: "active",
390
- lastViewed: p.lastViewed,
391
- resourceCount: p.resourceCount,
392
- });
393
- const ops = writeBinding(p.userId, pid, binding, false);
394
- if (p.lastViewed !== undefined) {
395
- ops.push(set(userInstance(p.userId), "lastViewedAt", p.lastViewed));
396
- }
397
- return ops;
398
- }
399
-
400
- /**
401
- * A tenant `RoleCatalogue` declaration: `role → capability set` (the directive
402
- * names a role grants). This is the SHAPE the kernel `identity::authz::RoleCatalogue`
403
- * (a `BTreeMap<String, BTreeSet<Capability>>`) resolves a binding's `role` string
404
- * against. The framework provides the declaration HELPER + type; the tenant
405
- * supplies the CONTENT (its roles, its capabilities). Capabilities are directive
406
- * names (a directive's name IS its capability — the `.requires(cap)` convention).
407
- */
408
- export type RoleCatalogue = Readonly<Record<string, readonly string[]>>;
409
-
410
- /**
411
- * Declare a tenant `RoleCatalogue`. Pure passthrough that brands the shape + freezes
412
- * it so a tenant declares `roleCatalogue({ siteAdmin: ["grantSiteAccess", …] })`
413
- * once and the framework/kernel resolve a `role` against it. Tenant-agnostic: the
414
- * framework fixes the SHAPE, never the roles.
415
- */
416
- export function roleCatalogue<const C extends RoleCatalogue>(roles: C): C {
417
- return Object.freeze({ ...roles }) as C;
418
- }