@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.
- package/package.json +1 -1
- package/src/aggregate.ts +19 -16
- package/src/build_package.ts +7 -0
- package/src/codegen_dart.ts +9 -0
- package/src/codegen_ts.ts +12 -10
- package/src/compile_package_main.ts +10 -0
- package/src/directive.ts +15 -18
- package/src/engine_entry.ts +8 -2
- package/src/framework/encryption_identity.ts +3 -3
- package/src/framework/genesis.ts +88 -0
- package/src/framework/impure_capability.ts +3 -3
- package/src/framework/relations.ts +28 -4
- package/src/framework/signing_identity.ts +97 -0
- package/src/framework/workspace_invariant.ts +5 -3
- package/src/framework/workspaces.ts +158 -103
- package/src/index.ts +72 -18
- package/src/manifest.ts +0 -6
- package/src/predicate.ts +4 -3
- package/src/relation_schema.ts +298 -0
- package/src/relation_sugar.ts +324 -0
- package/src/usd.ts +142 -7
- package/src/usd_layers.ts +53 -0
- package/src/framework/rbac.ts +0 -418
package/src/framework/rbac.ts
DELETED
|
@@ -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
|
-
}
|