@githolon/dsl 0.1.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/LICENSE.md +36 -0
- package/compile_package.mjs +50 -0
- package/package.json +59 -0
- package/src/aggregate.ts +167 -0
- package/src/authoring.ts +119 -0
- package/src/build_package.ts +636 -0
- package/src/certified_read.ts +313 -0
- package/src/codegen_dart.ts +2732 -0
- package/src/codegen_dot.ts +466 -0
- package/src/codegen_provider_dart.ts +358 -0
- package/src/codegen_ts.ts +365 -0
- package/src/codegen_usda.ts +388 -0
- package/src/combined.ts +195 -0
- package/src/compile_engine.ts +567 -0
- package/src/compile_package_main.ts +496 -0
- package/src/compose.ts +317 -0
- package/src/count.ts +218 -0
- package/src/ctx.ts +57 -0
- package/src/derived.ts +138 -0
- package/src/directive.ts +306 -0
- package/src/drivers.ts +95 -0
- package/src/emits_guard.ts +123 -0
- package/src/engine_entry.ts +449 -0
- package/src/exists.ts +170 -0
- package/src/extremum.ts +227 -0
- package/src/fields.ts +291 -0
- package/src/framework/bootstrap.ts +22 -0
- package/src/framework/disclosure.ts +108 -0
- package/src/framework/domain_lifecycle.ts +108 -0
- package/src/framework/identity.ts +537 -0
- package/src/framework/impure_capability.ts +643 -0
- package/src/framework/rbac.ts +418 -0
- package/src/framework/repair.ts +150 -0
- package/src/framework/sync_lifecycle.ts +125 -0
- package/src/framework/workspace_invariant.ts +128 -0
- package/src/framework/workspaces.ts +817 -0
- package/src/index.ts +317 -0
- package/src/manifest.ts +947 -0
- package/src/ops.ts +145 -0
- package/src/ordered_read.ts +228 -0
- package/src/predicate.ts +203 -0
- package/src/query/compile.ts +0 -0
- package/src/query/relations.ts +144 -0
- package/src/query.ts +151 -0
- package/src/read.ts +54 -0
- package/src/relation.ts +189 -0
- package/src/report/csv.ts +54 -0
- package/src/report.ts +401 -0
- package/src/spatial.ts +115 -0
- package/src/sum.ts +194 -0
- package/src/usd.ts +563 -0
- package/src/wire.ts +149 -0
- package/src/wire_encode.ts +250 -0
|
@@ -0,0 +1,418 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
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/repair.ts — the REPAIR surface as a tenant-style Nomos domain (#260 Stage 2).
|
|
10
|
+
*
|
|
11
|
+
* ── Why this module exists ──────────────────────────────────────────────────
|
|
12
|
+
* "Nomos is powerful enough to define its own repair surface as a Nomos domain"
|
|
13
|
+
* (Jack 2026-06-05, TARGET_repair.dot). The dogfood proof: repair intents go through
|
|
14
|
+
* THE ONE GATE — no bespoke crate, no AssumeVerified, no 2nd write path. This is Stage
|
|
15
|
+
* 2 (the `revert` directive); supersede / upcast / drain are later stages.
|
|
16
|
+
*
|
|
17
|
+
* ── The RepairRecord aggregate ──────────────────────────────────────────────
|
|
18
|
+
* A `revert` is an ACCOUNTABLE act: it leaves an immutable audit record (RepairRecord)
|
|
19
|
+
* in the ledger so every repair is traceable — who struck what, why, and when. This
|
|
20
|
+
* aggregate also satisfies the directive builder's mandatory aggregate-marker
|
|
21
|
+
* requirement (a strike-only directive has no aggregate to `.creates`/`.mutates` unless
|
|
22
|
+
* we supply one — the audit record is the natural candidate).
|
|
23
|
+
*
|
|
24
|
+
* ── The `revert` directive ──────────────────────────────────────────────────
|
|
25
|
+
* `revert` is the KEYSTONE (#260 Stage 2):
|
|
26
|
+
* • `.creates(RepairRecord)` — keyed by a KERNEL-MINTED `recordId`
|
|
27
|
+
* (`RepairRecord_<uuidv7>`). The kernel mints it (the marker-driven front-door),
|
|
28
|
+
* captures it, and commits it; replay reads the captured id (idempotent by
|
|
29
|
+
* capture, never re-minted). There is no peer-supplied id — every peer runs the
|
|
30
|
+
* same kernel and the id-mint gate verifies the minted shape on admit.
|
|
31
|
+
* • `.plan(...)` returns BOTH `strike(p.target)` AND the audit `set(...)` ops
|
|
32
|
+
* in one `PlannedOp[]` — one atomic intent carrying both channels.
|
|
33
|
+
* • The `strike(target)` op flows through the engine plan output (`{events, strikes}`)
|
|
34
|
+
* so it is VERIFIED at the gate — never a forgeable, out-of-band retraction
|
|
35
|
+
* (determinism law, ops.ts: "the strike flows through the verified plan output").
|
|
36
|
+
*
|
|
37
|
+
* ── Scope (Stage 2 only — do NOT add supersede / upcast / drain here) ──────
|
|
38
|
+
* supersede needs policy-control event shapes; upcast is an on-read migration;
|
|
39
|
+
* drain is a re-dispatch. All are later stages (TARGET_repair.dot build order §2-5).
|
|
40
|
+
*/
|
|
41
|
+
import { z } from "zod";
|
|
42
|
+
import { aggregate, instance } from "../aggregate.js";
|
|
43
|
+
import { t } from "../fields.js";
|
|
44
|
+
import { Lww } from "../drivers.js";
|
|
45
|
+
import { directive } from "../directive.js";
|
|
46
|
+
import { set, strike } from "../ops.js";
|
|
47
|
+
|
|
48
|
+
// ── The repair-kind vocabulary (Stage 2: "revert" only; expand later) ───────
|
|
49
|
+
|
|
50
|
+
/** The known repair operations. Stage 2 carries only `"revert"`. */
|
|
51
|
+
// `revert` = a pure strikeout (generic, framework-only). `replace` = strikeout PLUS
|
|
52
|
+
// corrected events — only expressible in a COMPOSED domain (e.g. tenant_repairs = repair ⊕ tenant),
|
|
53
|
+
// because the corrected events are in the TENANT's vocabulary. The repair domain owns the
|
|
54
|
+
// kind vocabulary + the audit record; a composed domain authors the replace directive.
|
|
55
|
+
export const REPAIR_KINDS = ["revert", "replace"] as const;
|
|
56
|
+
export type RepairKind = (typeof REPAIR_KINDS)[number];
|
|
57
|
+
|
|
58
|
+
// ── RepairRecord aggregate ──────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The RepairRecord aggregate — the immutable audit log of a repair act.
|
|
62
|
+
*
|
|
63
|
+
* Every repair is a RECORDED, ACCOUNTABLE act: the record carries WHO struck WHAT,
|
|
64
|
+
* WHY, and WHEN — so the ledger history is never silently mutated. This serves two
|
|
65
|
+
* purposes:
|
|
66
|
+
*
|
|
67
|
+
* 1. AUDIT. A struck intent (e.g. a bad move) is retracted from the fold, but the
|
|
68
|
+
* RepairRecord survives as evidence that the retraction happened, authorised by
|
|
69
|
+
* whom, and for what reason. Operators can query "who reverted what".
|
|
70
|
+
*
|
|
71
|
+
* 2. DIRECTIVE MARKER. The DSL directive builder requires every directive to carry
|
|
72
|
+
* a `.creates` or `.mutates` marker (the referential-marker invariant). A
|
|
73
|
+
* pure `strike`-only directive (no field ops) has no natural aggregate to create
|
|
74
|
+
* or mutate. RepairRecord IS that aggregate — a create keyed by a KERNEL-MINTED
|
|
75
|
+
* `recordId` (idempotent on replay by capture, like every create).
|
|
76
|
+
*
|
|
77
|
+
* Fields are all Lww (the "immutable-after-create" gap, same as identity.ts NOTE 1a):
|
|
78
|
+
* a RepairRecord should never be rewritten, but there is no `immutableAfterCreate`
|
|
79
|
+
* driver yet — Lww is the safe fallback (a re-author merely converges, never corrupts).
|
|
80
|
+
*/
|
|
81
|
+
export const RepairRecord = aggregate("RepairRecord", {
|
|
82
|
+
/** The aggregate instance id — a KERNEL-MINTED `RepairRecord_<uuidv7>`. */
|
|
83
|
+
recordId: t.string().merge(Lww),
|
|
84
|
+
/** The kind of repair (Stage 2: "revert" only). */
|
|
85
|
+
kind: t.enum(REPAIR_KINDS).merge(Lww),
|
|
86
|
+
/** The id of the struck intent — the target of the retraction. */
|
|
87
|
+
target: t.string().merge(Lww),
|
|
88
|
+
/** Human-readable reason for the repair (audit trail). */
|
|
89
|
+
reason: t.string().merge(Lww),
|
|
90
|
+
/** Who authored the repair (actor id / display name). */
|
|
91
|
+
revertedBy: t.string().merge(Lww),
|
|
92
|
+
/** Epoch-ms the repair was authored. */
|
|
93
|
+
revertedAt: t.int().merge(Lww),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── `revert` directive ──────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `revert` — the keystone directive of the repair domain.
|
|
100
|
+
*
|
|
101
|
+
* Lowers to a `WireIntent` carrying BOTH channels in one atomic commit:
|
|
102
|
+
* • `strikes = [p.target]` — the kernel strikeout (folded by parity; parity-even
|
|
103
|
+
* is a redo). Verified at the gate: the engine
|
|
104
|
+
* re-derives the strike from the plan, so it is
|
|
105
|
+
* IMPOSSIBLE to forge.
|
|
106
|
+
* • `events = [RepairRecord]` — the Create of the audit record, keyed by a
|
|
107
|
+
* KERNEL-MINTED `recordId` (`RepairRecord_<uuidv7>`),
|
|
108
|
+
* minted by the marker-driven front-door, captured +
|
|
109
|
+
* committed (idempotent on replay by capture).
|
|
110
|
+
*
|
|
111
|
+
* The `recordId` is KERNEL-MINTED — there is no peer-supplied id. Every peer runs the
|
|
112
|
+
* same deterministic kernel; the kernel mints the id from a host-injected entropy
|
|
113
|
+
* capability whose draw is captured into the ledger, and the id-mint gate verifies the
|
|
114
|
+
* minted shape + type tag on admit (the same path every create takes).
|
|
115
|
+
*/
|
|
116
|
+
export const revert = directive("revert")
|
|
117
|
+
.creates(RepairRecord)
|
|
118
|
+
.payload(
|
|
119
|
+
z.object({
|
|
120
|
+
/** The RepairRecord instance id — a KERNEL-MINTED `RepairRecord_<uuidv7>`. */
|
|
121
|
+
recordId: z.string(),
|
|
122
|
+
/** The intent id to retract (strike). */
|
|
123
|
+
target: z.string(),
|
|
124
|
+
/** Human-readable reason (audit). */
|
|
125
|
+
reason: z.string(),
|
|
126
|
+
/** Who is performing the repair. */
|
|
127
|
+
revertedBy: z.string(),
|
|
128
|
+
/** Epoch-ms of authorship (captured deterministically). */
|
|
129
|
+
revertedAt: z.number().int(),
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
.plan((p) => {
|
|
133
|
+
// Key the RepairRecord by its KERNEL-MINTED `recordId` (idempotent on replay by
|
|
134
|
+
// capture). instance(RepairRecord, recordId) → the create's `__id` (the marker-driven
|
|
135
|
+
// front-door mints this id; the gate verifies its shape + type tag on admit).
|
|
136
|
+
const rec = instance(RepairRecord, p.recordId);
|
|
137
|
+
return [
|
|
138
|
+
// CHANNEL 1: the strikeout — retract the target intent.
|
|
139
|
+
// `strike(target)` routes onto WireIntent.strikes (via lower.ts partition);
|
|
140
|
+
// the engine emits the {events, strikes} object so the gate can verify it.
|
|
141
|
+
strike(p.target),
|
|
142
|
+
// CHANNEL 2: the audit record — immutable log of this repair act.
|
|
143
|
+
set(rec, "recordId", p.recordId),
|
|
144
|
+
set(rec, "kind", "revert"),
|
|
145
|
+
set(rec, "target", p.target),
|
|
146
|
+
set(rec, "reason", p.reason),
|
|
147
|
+
set(rec, "revertedBy", p.revertedBy),
|
|
148
|
+
set(rec, "revertedAt", p.revertedAt),
|
|
149
|
+
];
|
|
150
|
+
});
|