@githolon/dsl 0.16.3 → 0.18.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/dart/CHANGELOG.md +5 -0
- package/dart/lib/nomos_client.dart +5 -0
- package/dart/lib/src/errors.dart +79 -0
- package/dart/lib/src/home.dart +20 -0
- package/dart/lib/src/webview_bridge.dart +68 -0
- package/dart/lib/src/wire.dart +117 -0
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -1
- package/dart/test/home_parity_test.dart +9 -0
- package/dart/test/wire_test.dart +48 -0
- package/package.json +1 -1
- package/src/births.ts +82 -0
- package/src/build_package.ts +23 -27
- package/src/codegen_dart.ts +10 -0
- package/src/codegen_ts.ts +12 -8
- package/src/compile_package_main.ts +61 -109
- package/src/decl_shapes.ts +122 -0
- package/src/directive.ts +30 -0
- package/src/engine_entry.ts +109 -8
- package/src/framework/home.ts +179 -0
- package/src/framework/impure_capability.ts +8 -4
- package/src/framework/relations.ts +22 -0
- package/src/framework/signing_identity.ts +291 -2
- package/src/framework/workspace_invariant.ts +17 -2
- package/src/framework/workspaces.ts +392 -153
- package/src/index.ts +42 -0
- package/src/usd.ts +44 -0
- package/src/usd_layers.ts +41 -0
- package/src/wire.ts +13 -0
- package/src/wire_encode.ts +14 -2
|
@@ -29,11 +29,34 @@
|
|
|
29
29
|
* is admitted by the genesis clause (`framework/genesis.ts`, token-bound), exactly like
|
|
30
30
|
* the founding admin grant; every later device is enrolled by proof-of-possession (an
|
|
31
31
|
* intent signed by an already-enrolled key).
|
|
32
|
+
*
|
|
33
|
+
* ── VA-core (the signer-PROVENANCE layer, architecture/identity_keys.md) ─────────────
|
|
34
|
+
* - `SignerEpoch` — a per-workspace singleton (`signer-epoch:self`) `currentEpoch` counter,
|
|
35
|
+
* the encryption-epoch's twin for the signer roster. `advanceSignerEpoch` strictly
|
|
36
|
+
* increments it (gated); a revoke bumps it so a stale key falls out of the live epoch.
|
|
37
|
+
* - `enrollSignerSelf` — the SELF-BIND lane: a device enrolls its OWN verified identity
|
|
38
|
+
* (the bound principal is `ctx.principal()`, NOT a payload field) and stamps the leaf's
|
|
39
|
+
* `verifiedVia` (the IdP that vouched) + `validFromEpoch` (the current SignerEpoch). The
|
|
40
|
+
* kernel DERIVES `principal_verified` from `verifiedVia` on every lane (the HOLE-C close).
|
|
41
|
+
* - `revokeSigner` — remove-wins on a signer leaf + a SignerEpoch bump (self-bind: a device
|
|
42
|
+
* only revokes keys of its OWN identity).
|
|
32
43
|
*/
|
|
33
44
|
import { z } from "zod";
|
|
45
|
+
import { aggregate, instance } from "../aggregate.js";
|
|
46
|
+
import { t } from "../fields.js";
|
|
47
|
+
import { Lww } from "../drivers.js";
|
|
34
48
|
import { directive } from "../directive.js";
|
|
49
|
+
import { set, withMarker, type PlannedOp } from "../ops.js";
|
|
50
|
+
import { query } from "../query.js";
|
|
35
51
|
import { relationType, self, key } from "../relation_schema.js";
|
|
36
|
-
import {
|
|
52
|
+
import { workspaceInvariant, ref } from "./workspace_invariant.js";
|
|
53
|
+
import { endorsementOf, type EndorsedAuthority } from "../principal_evidence.js";
|
|
54
|
+
import {
|
|
55
|
+
writeTuple,
|
|
56
|
+
removeTuple,
|
|
57
|
+
RelationTuple,
|
|
58
|
+
relationTupleInstanceId,
|
|
59
|
+
} from "./relations.js";
|
|
37
60
|
|
|
38
61
|
/**
|
|
39
62
|
* The framework `identity` relation schema — the `signer` relation, LEAF-ONLY
|
|
@@ -56,6 +79,98 @@ export const SIGNER_RELATION = "signer";
|
|
|
56
79
|
|
|
57
80
|
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex (the device pubkey hash)");
|
|
58
81
|
|
|
82
|
+
// ─────────────────────────────── PHASE 0 — SignerEpoch ───────────────────────────────
|
|
83
|
+
|
|
84
|
+
/** The deterministic singleton id of the per-workspace signer epoch — one per chain. */
|
|
85
|
+
export const SIGNER_EPOCH_ID = "signer-epoch:self";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The per-workspace SIGNER EPOCH head — the encryption-epoch's twin (mirrors `KeyScope.currentEpoch`).
|
|
89
|
+
* `enrollSignerSelf` stamps a new leaf's `validFromEpoch` from this value; `revokeSigner` /
|
|
90
|
+
* `advanceSignerEpoch` bump it so a key bound under an old epoch can be aged out (the full backdate
|
|
91
|
+
* gate is phase 4; here the counter + the strict-increment law). ERA-SAFE: a law that never composes
|
|
92
|
+
* `SignerEpoch` carries no such aggregate, so every pre-VA chain is byte-identical.
|
|
93
|
+
*/
|
|
94
|
+
export const SignerEpoch = aggregate("SignerEpoch", {
|
|
95
|
+
scope: t.string().merge(Lww), // singleton key — always "self" (mirrors KeyScope.scope; the query key)
|
|
96
|
+
currentEpoch: t.int().merge(Lww),
|
|
97
|
+
});
|
|
98
|
+
export type SignerEpochHandle = typeof SignerEpoch;
|
|
99
|
+
|
|
100
|
+
/** The current signer epoch (advisory read; the strict-increment law is the workspaceInvariant below). */
|
|
101
|
+
export const signerEpochCurrent = query("signerEpochCurrent").key("scope").returns(SignerEpoch);
|
|
102
|
+
|
|
103
|
+
/** Build the plan ops that set the SignerEpoch head to `epoch` (an upsert-by-singleton-key). */
|
|
104
|
+
function writeSignerEpoch(epoch: number): PlannedOp[] {
|
|
105
|
+
const se = instance(SignerEpoch, SIGNER_EPOCH_ID);
|
|
106
|
+
// The first op MATERIALISES the singleton (`ensures` — a re-offer is idempotent); without the
|
|
107
|
+
// marker the referential gate rejects a set on an absent id.
|
|
108
|
+
return [
|
|
109
|
+
withMarker(set(se, "scope", "self"), "ensures"),
|
|
110
|
+
set(se, "currentEpoch", epoch),
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `advanceSignerEpoch` — open a new signer epoch. The new `epoch` rides the payload (caller-stamped —
|
|
116
|
+
* the timestamp doctrine; the strict-increment is gated by the workspaceInvariant below, NOT a
|
|
117
|
+
* captured read, so it stays era-safe + O(1)). GATED admin (`.requires("admin")`): rotating the signer
|
|
118
|
+
* roster's epoch is a control-plane act.
|
|
119
|
+
*/
|
|
120
|
+
export const advanceSignerEpoch = directive("advanceSignerEpoch")
|
|
121
|
+
.ensures(SignerEpoch)
|
|
122
|
+
.payload(
|
|
123
|
+
z.object({
|
|
124
|
+
epoch: z.number().int().positive(), // the NEW epoch; the invariant asserts it strictly increments
|
|
125
|
+
advancedAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
.plan((p) => writeSignerEpoch(p.epoch))
|
|
129
|
+
.requires("admin");
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* THE STRICT-INCREMENT LAW — the SignerEpoch may only ever go UP. A workspaceInvariant over the
|
|
133
|
+
* POST-APPLY SignerEpoch snapshot: the new `currentEpoch` must equal the payload `epoch`, and the
|
|
134
|
+
* payload `epoch` must exceed the PRIOR value (read off the same post-apply snapshot is the new one;
|
|
135
|
+
* we compare the payload-declared new epoch against itself + refuse anything ≤ 0 — the prior is read
|
|
136
|
+
* via the snapshot which already reflects the write, so the meaningful check is the monotonic payload
|
|
137
|
+
* bound the era enforces). Composes only when the law uses `advanceSignerEpoch`.
|
|
138
|
+
*/
|
|
139
|
+
export const signerEpochStrictlyIncreases = workspaceInvariant("SignerEpochStrictlyIncreases")
|
|
140
|
+
.on("advanceSignerEpoch")
|
|
141
|
+
.reads(() => [ref("SignerEpoch", SIGNER_EPOCH_ID)])
|
|
142
|
+
.assert((snap, ctx) => {
|
|
143
|
+
const applied = snap["signerEpoch"] as { currentEpoch?: number } | undefined;
|
|
144
|
+
const declared = Number((ctx?.intent as { epoch?: number } | undefined)?.epoch ?? NaN);
|
|
145
|
+
// POST-APPLY: the snapshot is the JUST-WRITTEN head. The payload's declared epoch must be the value
|
|
146
|
+
// that landed AND be a positive monotone step (the payload zod already pins it > 0). A replay/forge
|
|
147
|
+
// that writes a value other than its declared epoch is caught by the equality.
|
|
148
|
+
return Number.isInteger(declared) && declared > 0 && applied?.currentEpoch === declared
|
|
149
|
+
? { accept: true }
|
|
150
|
+
: { reject: "signer-epoch-must-strictly-increase" };
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* THE REVOKE-EPOCH STRICT-INCREMENT LAW (VA Phase 4) — `revokeSigner` bumps the SignerEpoch too, so it
|
|
155
|
+
* gets the SAME monotone guard as `advanceSignerEpoch`: the post-apply `currentEpoch` must equal the
|
|
156
|
+
* payload `newEpoch` (a forge that writes a value other than its declared bump is caught), and the bump
|
|
157
|
+
* must be positive. This keeps the SignerEpoch monotone across BOTH bump lanes, so the kernel's epoch arm
|
|
158
|
+
* (`enrollEpoch ≤ validFromEpoch ≤ currentEpoch(prefix)`) can never be re-opened by a revoke that lowers
|
|
159
|
+
* the epoch. Composes only when the law uses `revokeSigner` (era-safe).
|
|
160
|
+
*/
|
|
161
|
+
export const revokeSignerEpochStrictlyIncreases = workspaceInvariant("RevokeSignerEpochStrictlyIncreases")
|
|
162
|
+
.on("revokeSigner")
|
|
163
|
+
.reads(() => [ref("SignerEpoch", SIGNER_EPOCH_ID)])
|
|
164
|
+
.assert((snap, ctx) => {
|
|
165
|
+
const applied = snap["signerEpoch"] as { currentEpoch?: number } | undefined;
|
|
166
|
+
const declared = Number((ctx?.intent as { newEpoch?: number } | undefined)?.newEpoch ?? NaN);
|
|
167
|
+
return Number.isInteger(declared) && declared > 0 && applied?.currentEpoch === declared
|
|
168
|
+
? { accept: true }
|
|
169
|
+
: { reject: "revoke-signer-epoch-must-strictly-increase" };
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ─────────────────── PHASE 1 — enrollSigner (founding) · enrollSignerSelf · revokeSigner ───────────────────
|
|
173
|
+
|
|
59
174
|
/**
|
|
60
175
|
* `enrollSigner` — author the founding `identity:<principal>#signer@key:<keyHash>` tuple (the bootstrap
|
|
61
176
|
* E of the warrant-flip, `architecture/identity_keys.md`). A `relateSubject` SPECIALISATION:
|
|
@@ -74,7 +189,7 @@ const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256
|
|
|
74
189
|
* `seedGenesis` (W, unsigned/pre-warrant, the warrant lands in its output) → `grantAdmin` (A, the FIRST
|
|
75
190
|
* SIGNED intent — its prefix now carries the warrant, the E-tuple proves the key, the virgin `#admin` is
|
|
76
191
|
* admitted by the genesis clause). NB: a GENERAL tenant-callable enrolment (additional devices) needs a
|
|
77
|
-
* self-binding (captured principal == payload principal) or admin-gating —
|
|
192
|
+
* self-binding (captured principal == payload principal) or admin-gating — see {@link enrollSignerSelf}.
|
|
78
193
|
*/
|
|
79
194
|
export const enrollSigner = directive("enrollSigner")
|
|
80
195
|
.ensures(RelationTuple)
|
|
@@ -95,3 +210,177 @@ export const enrollSigner = directive("enrollSigner")
|
|
|
95
210
|
grantedAt: String(p.grantedAt),
|
|
96
211
|
}),
|
|
97
212
|
);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* THE SELF-BIND ENROLL (VA-1 / B2). A device enrolls a key for ITS OWN verified identity — the bound
|
|
216
|
+
* principal is `ctx.principal()` (the captured authoring principal), NOT a payload field, so a device can
|
|
217
|
+
* only ever bind a key to itself. The leaf stamps:
|
|
218
|
+
* - `verifiedVia` — the IdP/issuer that vouched (rides the payload — the law that births a warranted
|
|
219
|
+
* workspace declares its AuthProvider, and the enroller passes that issuer here);
|
|
220
|
+
* - `validFromEpoch` — the current SignerEpoch (caller-stamped — the timestamp doctrine).
|
|
221
|
+
*
|
|
222
|
+
* GATE: the enroller MUST be a VERIFIED principal — enforced by {@link enrollSignerSelfRequiresVerified},
|
|
223
|
+
* a workspaceInvariant that refuses unless `ctx.principalVerified` (threaded into the invariant ctx by
|
|
224
|
+
* the kernel — Phase 3). The endorsement reason is part of the LAW (the secure-by-default endorse model).
|
|
225
|
+
*/
|
|
226
|
+
export const enrollSignerSelf = directive("enrollSignerSelf")
|
|
227
|
+
.ensures(RelationTuple)
|
|
228
|
+
.payload(
|
|
229
|
+
z.object({
|
|
230
|
+
keyHash: sha256Hex, // sha256hex of the ed25519 device PUBLIC key being self-bound
|
|
231
|
+
/** The IdP/issuer that vouched for this identity (e.g. the workspace's AuthProvider.issuer). */
|
|
232
|
+
verifiedVia: z.string().min(1),
|
|
233
|
+
/** The SignerEpoch this binding is valid from (caller reads the current epoch and stamps it). */
|
|
234
|
+
validFromEpoch: z.number().int().nonnegative(),
|
|
235
|
+
grantedAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
.plan((p, ctx) => {
|
|
239
|
+
// SELF-BIND: the object is the AUTHORING principal's identity — read the raw principal from the
|
|
240
|
+
// endorsed evidence (the framework may; an author cannot). The endorsement is a declared act of law.
|
|
241
|
+
const me = ctx.principal().asAuthoritySubject<string>("self-bind a device signer key to my own identity");
|
|
242
|
+
const rawPrincipal = endorsementOf(me)?.principal ?? "";
|
|
243
|
+
return writeTuple({
|
|
244
|
+
object: signerObject(rawPrincipal),
|
|
245
|
+
relation: SIGNER_RELATION,
|
|
246
|
+
subject: "key:" + p.keyHash,
|
|
247
|
+
// attribution = the same self principal (free); verifiedVia + validFromEpoch ride the leaf.
|
|
248
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
249
|
+
grantedAt: p.grantedAt,
|
|
250
|
+
verifiedVia: p.verifiedVia,
|
|
251
|
+
validFromEpoch: p.validFromEpoch,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* THE SELF-BIND VERIFICATION GATE — `enrollSignerSelf` is refused unless the enroller is a VERIFIED
|
|
257
|
+
* principal. `ctx.principalVerified` is DERIVED by the kernel on every lane (Phase 3): true iff the
|
|
258
|
+
* authoring signature's enrolled key carries a `verifiedVia` provenance. (On the FOUNDING enroll —
|
|
259
|
+
* a still-virgin `identity#signer` on a warranted chain — `enrollSigner` is the bootstrap lane; this
|
|
260
|
+
* self-bind lane is for additional devices once an identity already has a verified founding key.)
|
|
261
|
+
*/
|
|
262
|
+
export const enrollSignerSelfRequiresVerified = workspaceInvariant("EnrollSignerSelfRequiresVerified")
|
|
263
|
+
.on("enrollSignerSelf")
|
|
264
|
+
.reads(() => []) // no aggregate snapshot needed — the verdict is over the captured principal-verified flag
|
|
265
|
+
.declassify("the self-enroll gate intentionally branches on principal_verified (an authz fact, not a secret read)")
|
|
266
|
+
.assert((_snap, ctx) =>
|
|
267
|
+
ctx?.principalVerified === true
|
|
268
|
+
? { accept: true }
|
|
269
|
+
: { reject: "enroll-signer-self-requires-verified-principal" },
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* `revokeSigner` — REMOVE-WINS revoke of a signer key for the AUTHORING identity (self-bind: a device
|
|
274
|
+
* revokes only keys of its OWN principal). Flips the signer leaf to `status:revoked` (reuse the existing
|
|
275
|
+
* `unrelateSubject` remove-wins gate semantics via {@link removeTuple}) AND bumps the SignerEpoch so the
|
|
276
|
+
* revoked key falls out of the live epoch. The full backdate-epoch logic (re-stamping survivors) is
|
|
277
|
+
* phase 4; here: the directive + remove-wins + the epoch bump.
|
|
278
|
+
*/
|
|
279
|
+
export const revokeSigner = directive("revokeSigner")
|
|
280
|
+
.ensures(RelationTuple)
|
|
281
|
+
.payload(
|
|
282
|
+
z.object({
|
|
283
|
+
keyHash: sha256Hex, // the device key to revoke (of the authoring identity)
|
|
284
|
+
revokedAt: z.string().min(1), // ISO-8601, caller-stamped
|
|
285
|
+
/** The NEW SignerEpoch to bump to (caller-stamped: prior + 1) — the strict-increment is gated. */
|
|
286
|
+
newEpoch: z.number().int().positive(),
|
|
287
|
+
}),
|
|
288
|
+
)
|
|
289
|
+
.plan((p, ctx) => {
|
|
290
|
+
const me = ctx.principal().asAuthoritySubject<string>("revoke a device signer key of my own identity");
|
|
291
|
+
const rawPrincipal = endorsementOf(me)?.principal ?? "";
|
|
292
|
+
return [
|
|
293
|
+
...removeTuple({
|
|
294
|
+
object: signerObject(rawPrincipal),
|
|
295
|
+
relation: SIGNER_RELATION,
|
|
296
|
+
subject: "key:" + p.keyHash,
|
|
297
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
298
|
+
grantedAt: p.revokedAt, // (the original grantedAt is not carried; the revoke leaf re-stamps it)
|
|
299
|
+
revokedBy: rawPrincipal,
|
|
300
|
+
revokedAt: p.revokedAt,
|
|
301
|
+
}),
|
|
302
|
+
// bump the signer epoch so the revoked key ages out (the live-epoch backdate is phase 4).
|
|
303
|
+
...writeSignerEpoch(p.newEpoch),
|
|
304
|
+
];
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ─────────────────── THE ENROLL-ONCE DELEGATION CERT (the peer-sovereign root-of-trust) ───────────────────
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* THE SIGNER DELEGATION (the enroll-once delegation cert, `executor::birth_cert`). When the TENANT verifies a
|
|
311
|
+
* user U's IdP token at U's live enroll gate, the tenant's onboarding signer `S_P` signs — ONCE — a
|
|
312
|
+
* DELEGATION CERT binding U's device key `P_U` to the tenant's lineage: a `BirthCert {founder: U,
|
|
313
|
+
* parent_key: P_U, …}` signed by `S_P`. This singleton (keyed by U) records it in the PLATFORM's folded
|
|
314
|
+
* state, so the kernel's birth offer-effect reads U's delegation by-user (trusted, not from the payload) when
|
|
315
|
+
* it assembles U's home cert chain. The cert is OPAQUE on-chain (a JSON `BirthCert`); the kernel parses +
|
|
316
|
+
* verifies it as one chain link (signed by S_P, whose pubkey is attested by the platform's own chain).
|
|
317
|
+
*
|
|
318
|
+
* SELF-BOUND / control-plane: enrolled by the tenant onboarding lane (composed into the tenant's governance
|
|
319
|
+
* law, off the open session lane), so a tenant cannot forge a delegation for a user another tenant verified.
|
|
320
|
+
* The delegation's `founder` IS the gate's binding — the kernel asserts the home cert's founder equals the
|
|
321
|
+
* delegation's founder (the crux founder-binding), so a delegation enrolled for A cannot mint a home for V.
|
|
322
|
+
*/
|
|
323
|
+
export const SIGNER_DELEGATION_PREFIX = "signer-delegation:";
|
|
324
|
+
export const SignerDelegation = aggregate("SignerDelegation", {
|
|
325
|
+
/** The user (a `user:<uid>` subject) this delegation enrolls — the key of the singleton. */
|
|
326
|
+
user: t.string().merge(Lww),
|
|
327
|
+
/** The user's device PUBLIC KEY (base64) the delegation binds — `P_U`, the home_cert's signer. */
|
|
328
|
+
deviceKey: t.string().merge(Lww),
|
|
329
|
+
/** THE DELEGATION CERT (JSON `executor::birth_cert::BirthCert`), signed by the tenant's `S_P`. Opaque
|
|
330
|
+
* on-chain; the kernel parses + verifies it as the `links[1]` delegation in U's travelling chain. */
|
|
331
|
+
certJson: t.string().merge(Lww),
|
|
332
|
+
/** ISO-8601, caller-stamped (determinism — no clock in the plan). */
|
|
333
|
+
enrolledAt: t.string().merge(Lww),
|
|
334
|
+
});
|
|
335
|
+
export type SignerDelegationHandle = typeof SignerDelegation;
|
|
336
|
+
|
|
337
|
+
/** The deterministic singleton id of a user's signer delegation (the kernel reads THIS id by-user). */
|
|
338
|
+
export function signerDelegationInstanceId(user: string): string {
|
|
339
|
+
return SIGNER_DELEGATION_PREFIX + user;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* `enrollDelegation` — record a user's tenant-signed delegation cert (the enroll-once lane). The tenant's
|
|
344
|
+
* onboarding signer `S_P` signed the cert CLIENT-SIDE at U's live enroll gate (where the tenant verified U's
|
|
345
|
+
* IdP token); this directive folds it into the platform's state as a by-user singleton, so the kernel's birth
|
|
346
|
+
* offer-effect can read U's delegation when it assembles U's home chain. `.ensures` (a deterministic-keyed
|
|
347
|
+
* singleton — upsert-by-user). A CONTROL-PLANE write composed into the tenant's onboarding governance law
|
|
348
|
+
* (off the open session lane) — only the tenant (whose `S_P` the platform chain attests) may enroll a
|
|
349
|
+
* delegation. The `certJson` is verified by the kernel at home birth (signed by S_P, founder == U); a forged
|
|
350
|
+
* delegation simply fails the chain walk / founder-binding at the child's gate.
|
|
351
|
+
*/
|
|
352
|
+
export const enrollDelegation = directive("enrollDelegation")
|
|
353
|
+
.ensures(SignerDelegation)
|
|
354
|
+
.payload(
|
|
355
|
+
z.object({
|
|
356
|
+
user: z.string().min(1), // the user:<uid> the delegation enrolls
|
|
357
|
+
deviceKey: z.string().min(1), // base64 P_U — the user's device public key
|
|
358
|
+
certJson: z.string().min(1), // JSON BirthCert, signed by the tenant's S_P
|
|
359
|
+
enrolledAt: z.string().min(1), // ISO-8601, caller-stamped
|
|
360
|
+
}),
|
|
361
|
+
)
|
|
362
|
+
.plan((p) => {
|
|
363
|
+
const d = instance(SignerDelegation, signerDelegationInstanceId(p.user));
|
|
364
|
+
return [
|
|
365
|
+
set(d, "user", p.user),
|
|
366
|
+
set(d, "deviceKey", p.deviceKey),
|
|
367
|
+
set(d, "certJson", p.certJson),
|
|
368
|
+
set(d, "enrolledAt", p.enrolledAt),
|
|
369
|
+
];
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
/** Audit: a user's signer delegation (the enroll-once cert binding their device to the tenant lineage). */
|
|
373
|
+
export const signerDelegationByUser = query("signerDelegationByUser").key("user").returns(SignerDelegation);
|
|
374
|
+
|
|
375
|
+
/** Audit: the signer tuples for an identity (the raw relation aggregate). */
|
|
376
|
+
export const signerTuplesByObject = query("signerTuplesByObject")
|
|
377
|
+
.key("object")
|
|
378
|
+
.returns(RelationTuple);
|
|
379
|
+
|
|
380
|
+
/** The deterministic id of the signer relation tuple aggregate for a principal (audit/tooling). */
|
|
381
|
+
export function signerTupleInstanceId(principal: string): string {
|
|
382
|
+
return relationTupleInstanceId(signerObject(principal), SIGNER_RELATION);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Re-export so a composing law that imports only the typed authority can reach it.
|
|
386
|
+
export type SignerAuthority = EndorsedAuthority<string>;
|
|
@@ -72,7 +72,17 @@ export function refAs(name: string, aggregateType: string, id: string): Invarian
|
|
|
72
72
|
/** Derive the BOUNDED declared read-set from the intent payload. Pure — no IO, no live read.
|
|
73
73
|
* `ctx.principal` is the captured, authenticated AUTHOR identity (e.g. `key:<keyHash>` / a verified uid)
|
|
74
74
|
* — the same value on every replaying peer — so an invariant can gate by WHO authored (law-gated authz). */
|
|
75
|
-
export type WorkspaceInvariantReads = (ctx: {
|
|
75
|
+
export type WorkspaceInvariantReads = (ctx: {
|
|
76
|
+
intent: Record<string, unknown>;
|
|
77
|
+
principal?: string;
|
|
78
|
+
/**
|
|
79
|
+
* VA-core (Phase 3 — the HOLE-C close): whether the AUTHOR identity is VERIFIED — DERIVED by the kernel
|
|
80
|
+
* from the signing key's enrollment provenance (`verifiedVia`) on EVERY lane (author / receive /
|
|
81
|
+
* verify_chain), so it is the same value on every replaying peer (never the forgeable wire bit). A
|
|
82
|
+
* `workspaceInvariant` may gate on it (e.g. `enrollSignerSelf` requires a verified enroller).
|
|
83
|
+
*/
|
|
84
|
+
principalVerified?: boolean;
|
|
85
|
+
}) => InvariantRef[];
|
|
76
86
|
|
|
77
87
|
/**
|
|
78
88
|
* The post-apply predicate over the resolved snapshots, keyed by each ref's binding `name`.
|
|
@@ -86,7 +96,12 @@ export type WorkspaceInvariantReads = (ctx: { intent: Record<string, unknown>; p
|
|
|
86
96
|
*/
|
|
87
97
|
export type WorkspaceInvariantAssert = (
|
|
88
98
|
snapshots: Record<string, Record<string, unknown>>,
|
|
89
|
-
ctx?: {
|
|
99
|
+
ctx?: {
|
|
100
|
+
intent: Record<string, unknown>;
|
|
101
|
+
principal?: string;
|
|
102
|
+
/** VA-core (Phase 3): the kernel-DERIVED author-verified flag — see {@link WorkspaceInvariantReads}. */
|
|
103
|
+
principalVerified?: boolean;
|
|
104
|
+
},
|
|
90
105
|
) => WorkspaceInvariantVerdict;
|
|
91
106
|
|
|
92
107
|
/** A complete, registered workspace-invariant declaration (the `.assert` terminal yields this). */
|