@githolon/dsl 0.100.3 → 0.100.5
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 +6 -0
- package/dart/lib/src/home.dart +1 -1
- package/dart/lib/src/webview_bridge.dart +1 -2
- package/dart/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/src/birth_meter_ensure.ts +16 -0
- package/src/build_package.ts +11 -1
- package/src/codegen_openapi.ts +82 -34
- package/src/engine_entry.ts +7 -6
- package/src/framework/birth_meter.ts +82 -0
- package/src/index.ts +5 -0
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.62.1
|
|
2
|
+
|
|
3
|
+
- Align the public Dart documentation with the one Flutter application entry point: identity comes from the
|
|
4
|
+
token passed to `NomosScope.home(application:, authToken:)`; application code no longer supplies a subject
|
|
5
|
+
or device-token callback.
|
|
6
|
+
|
|
1
7
|
## 0.62.0
|
|
2
8
|
|
|
3
9
|
- Generated business clients expose one `birth(payload)` method. The bound session supplies its verified
|
package/dart/lib/src/home.dart
CHANGED
|
@@ -10,7 +10,7 @@ import 'package:crypto/crypto.dart';
|
|
|
10
10
|
/// every peer and on the cloud. Use it directly as the `workspace`:
|
|
11
11
|
///
|
|
12
12
|
/// ```dart
|
|
13
|
-
/// NomosScope(
|
|
13
|
+
/// NomosScope.home(application: 'my-app', authToken: idToken, ...)
|
|
14
14
|
/// ```
|
|
15
15
|
///
|
|
16
16
|
/// Formula: `home-` + the first 40 hex chars of `sha256(utf8(subject))`.
|
|
@@ -1895,8 +1895,7 @@ class NomosBridge {
|
|
|
1895
1895
|
|
|
1896
1896
|
/// Derive the device key's base64 PUBLIC half from its base64 SECRET seed, inside the LOCAL kernel
|
|
1897
1897
|
/// (the secret never leaves the device). The recovery lanes use this when only the secret was
|
|
1898
|
-
/// persisted (the device-key store keeps just the seed)
|
|
1899
|
-
/// (`authTokenForDevice(publicKey)`) for the keyless re-enroll lane.
|
|
1898
|
+
/// persisted (the device-key store keeps just the seed), including low-level device-bound enrollment.
|
|
1900
1899
|
Future<String> devicePublicOf(String secret) async =>
|
|
1901
1900
|
(_asMap(await _call('devicePublicOf', {'secret': secret}))['public'] ?? '')
|
|
1902
1901
|
.toString();
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.62.
|
|
14
|
+
version: 0.62.1
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -32,8 +32,13 @@ import {
|
|
|
32
32
|
} from "./birth_meter.js";
|
|
33
33
|
import {
|
|
34
34
|
reclaimChild,
|
|
35
|
+
reclaimChildAsAdmin,
|
|
36
|
+
reclaimChildAsAdminRequiresRetired,
|
|
35
37
|
reclaimChildRequiresRetired,
|
|
36
38
|
retireChild,
|
|
39
|
+
retireChildAsAdmin,
|
|
40
|
+
retireChildAsAdminNotReclaimed,
|
|
41
|
+
retireChildAsAdminRequiresBirth,
|
|
37
42
|
retireChildRequiresBirth,
|
|
38
43
|
retireChildNotReclaimed,
|
|
39
44
|
} from "./framework/birth_meter.js";
|
|
@@ -82,8 +87,19 @@ export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M)
|
|
|
82
87
|
if (!("BirthGrant" in out)) out["BirthGrant"] = presented(BirthGrant);
|
|
83
88
|
if (!("retireChild" in out)) out["retireChild"] = presented(retireChild);
|
|
84
89
|
if (!("reclaimChild" in out)) out["reclaimChild"] = presented(reclaimChild);
|
|
90
|
+
if (!("retireChildAsAdmin" in out)) out["retireChildAsAdmin"] = presented(retireChildAsAdmin);
|
|
91
|
+
if (!("reclaimChildAsAdmin" in out)) out["reclaimChildAsAdmin"] = presented(reclaimChildAsAdmin);
|
|
85
92
|
if (!("reclaimChildRequiresRetired" in out)) out["reclaimChildRequiresRetired"] = reclaimChildRequiresRetired;
|
|
93
|
+
if (!("reclaimChildAsAdminRequiresRetired" in out)) {
|
|
94
|
+
out["reclaimChildAsAdminRequiresRetired"] = reclaimChildAsAdminRequiresRetired;
|
|
95
|
+
}
|
|
86
96
|
if (!("retireChildRequiresBirth" in out)) out["retireChildRequiresBirth"] = retireChildRequiresBirth;
|
|
87
97
|
if (!("retireChildNotReclaimed" in out)) out["retireChildNotReclaimed"] = retireChildNotReclaimed;
|
|
98
|
+
if (!("retireChildAsAdminRequiresBirth" in out)) {
|
|
99
|
+
out["retireChildAsAdminRequiresBirth"] = retireChildAsAdminRequiresBirth;
|
|
100
|
+
}
|
|
101
|
+
if (!("retireChildAsAdminNotReclaimed" in out)) {
|
|
102
|
+
out["retireChildAsAdminNotReclaimed"] = retireChildAsAdminNotReclaimed;
|
|
103
|
+
}
|
|
88
104
|
return out as M;
|
|
89
105
|
}
|
package/src/build_package.ts
CHANGED
|
@@ -82,7 +82,10 @@ import { assertIdOfLint } from "./idof_lint.js";
|
|
|
82
82
|
import { assertSurfacedResolvers } from "./surfaced_lint.js";
|
|
83
83
|
import { expandBusinessBirthExports } from "./business_birth.js";
|
|
84
84
|
import {
|
|
85
|
+
reclaimChildAsAdminRequiresRetired,
|
|
85
86
|
reclaimChildRequiresRetired,
|
|
87
|
+
retireChildAsAdminNotReclaimed,
|
|
88
|
+
retireChildAsAdminRequiresBirth,
|
|
86
89
|
retireChildNotReclaimed,
|
|
87
90
|
retireChildRequiresBirth,
|
|
88
91
|
} from "./framework/birth_meter.js";
|
|
@@ -316,7 +319,14 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
316
319
|
const workspaceInvariants = [...(spec.workspaceInvariants ?? [])];
|
|
317
320
|
if (declaresBirth) {
|
|
318
321
|
const haveInvariant = new Set(workspaceInvariants.map((w) => w.id));
|
|
319
|
-
for (const lifecycle of [
|
|
322
|
+
for (const lifecycle of [
|
|
323
|
+
reclaimChildRequiresRetired,
|
|
324
|
+
reclaimChildAsAdminRequiresRetired,
|
|
325
|
+
retireChildNotReclaimed,
|
|
326
|
+
retireChildAsAdminNotReclaimed,
|
|
327
|
+
retireChildRequiresBirth,
|
|
328
|
+
retireChildAsAdminRequiresBirth,
|
|
329
|
+
]) {
|
|
320
330
|
if (!haveInvariant.has(lifecycle.id)) workspaceInvariants.push(lifecycle);
|
|
321
331
|
}
|
|
322
332
|
}
|
package/src/codegen_openapi.ts
CHANGED
|
@@ -50,11 +50,18 @@ export interface OpenApiDoc {
|
|
|
50
50
|
openapi: "3.1.0";
|
|
51
51
|
info: Record<string, unknown>;
|
|
52
52
|
paths: Record<string, Record<string, unknown>>;
|
|
53
|
+
/**
|
|
54
|
+
* Law-derived routes needed by Nomos itself (device seating, provenance, recovery, ...).
|
|
55
|
+
* The host consumes this only on its internal describe path and strips it from public OpenAPI.
|
|
56
|
+
*/
|
|
57
|
+
"x-nomos-framework-routes"?: { paths: Record<string, Record<string, unknown>> };
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export interface OpenApiFragmentOptions {
|
|
56
61
|
/** The content hash of the installed law (the spec's version + ingest cache key). */
|
|
57
62
|
lawHash?: string;
|
|
63
|
+
/** Include the private framework router manifest used by the Nomos host. Public codegen omits it. */
|
|
64
|
+
includeFrameworkRoutes?: boolean;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/** Lower a directive's payload zod to JSON Schema; a non-representable schema falls back to a
|
|
@@ -95,22 +102,10 @@ function domainKey(module: DomainModule): string {
|
|
|
95
102
|
* the merge namespaces any cross-domain collision. The offer/query distinction rides on the HTTP
|
|
96
103
|
* method (POST/GET) AND the authoritative `x-nomos.kind` — the host reads the latter, never the name.
|
|
97
104
|
*/
|
|
98
|
-
|
|
105
|
+
function generateOpenApiSurface(
|
|
99
106
|
module: DomainModule,
|
|
100
107
|
opts: OpenApiFragmentOptions = {},
|
|
101
108
|
): OpenApiDoc {
|
|
102
|
-
module = {
|
|
103
|
-
...module,
|
|
104
|
-
aggregates: module.aggregates.filter(
|
|
105
|
-
(a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
106
|
-
),
|
|
107
|
-
directives: module.directives.filter(
|
|
108
|
-
(d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
109
|
-
),
|
|
110
|
-
...(module.queries === undefined
|
|
111
|
-
? {}
|
|
112
|
-
: { queries: module.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
|
|
113
|
-
};
|
|
114
109
|
const domain = domainKey(module);
|
|
115
110
|
const paths: Record<string, Record<string, unknown>> = {};
|
|
116
111
|
|
|
@@ -123,7 +118,6 @@ export function generateOpenApiFragment(
|
|
|
123
118
|
|
|
124
119
|
// OFFERABLE INTENTS → POST offers. The payload JSON Schema is the intent's own zod, lowered.
|
|
125
120
|
for (const dir of module.directives as AnyDirective[]) {
|
|
126
|
-
if ((dir as { businessBirthInternal?: unknown }).businessBirthInternal === true) continue;
|
|
127
121
|
const businessBirth = (dir as {
|
|
128
122
|
businessBirth?: { aggregateId: string; publicPayloadSchema: unknown };
|
|
129
123
|
}).businessBirth;
|
|
@@ -247,11 +241,79 @@ export function generateOpenApiFragment(
|
|
|
247
241
|
};
|
|
248
242
|
}
|
|
249
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Generate the developer-facing OpenAPI plus, only when requested by the runtime, a separate
|
|
246
|
+
* framework routing manifest. The split is deliberate: filtering framework declarations out of
|
|
247
|
+
* `paths` is presentation; it must never make Nomos's own law declarations unroutable.
|
|
248
|
+
*/
|
|
249
|
+
export function generateOpenApiFragment(
|
|
250
|
+
module: DomainModule,
|
|
251
|
+
opts: OpenApiFragmentOptions = {},
|
|
252
|
+
): OpenApiDoc {
|
|
253
|
+
const isInternal = (value: unknown) =>
|
|
254
|
+
!!value && typeof value === "object"
|
|
255
|
+
&& (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
|
|
256
|
+
const publicModule: DomainModule = {
|
|
257
|
+
...module,
|
|
258
|
+
aggregates: module.aggregates.filter((a) => !isInternal(a)),
|
|
259
|
+
directives: module.directives.filter((d) => !isInternal(d)),
|
|
260
|
+
...(module.queries === undefined ? {} : { queries: module.queries.filter((q) => !isInternal(q)) }),
|
|
261
|
+
};
|
|
262
|
+
const publicDoc = generateOpenApiSurface(publicModule, opts);
|
|
263
|
+
if (opts.includeFrameworkRoutes !== true) return publicDoc;
|
|
264
|
+
|
|
265
|
+
const frameworkModule: DomainModule = {
|
|
266
|
+
...module,
|
|
267
|
+
aggregates: module.aggregates.filter(isInternal),
|
|
268
|
+
directives: module.directives.filter(isInternal),
|
|
269
|
+
...(module.queries === undefined ? {} : { queries: module.queries.filter(isInternal) }),
|
|
270
|
+
// Framework routing is for declared internal offers/queries. Business maintained reads remain
|
|
271
|
+
// on the public module; do not duplicate them into the private table.
|
|
272
|
+
counts: [],
|
|
273
|
+
sums: [],
|
|
274
|
+
mins: [],
|
|
275
|
+
maxes: [],
|
|
276
|
+
spatials: [],
|
|
277
|
+
};
|
|
278
|
+
const frameworkDoc = generateOpenApiSurface(frameworkModule, opts);
|
|
279
|
+
if (Object.keys(frameworkDoc.paths).length === 0) return publicDoc;
|
|
280
|
+
return {
|
|
281
|
+
...publicDoc,
|
|
282
|
+
"x-nomos-framework-routes": { paths: frameworkDoc.paths },
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
250
286
|
/** Stable JSON of an operation, to detect a genuine duplicate vs a real collision in the merge. */
|
|
251
287
|
function operationKey(op: unknown): string {
|
|
252
288
|
return JSON.stringify(op);
|
|
253
289
|
}
|
|
254
290
|
|
|
291
|
+
function mergePathsInto(
|
|
292
|
+
paths: Record<string, Record<string, unknown>>,
|
|
293
|
+
incoming: Record<string, Record<string, unknown>>,
|
|
294
|
+
): void {
|
|
295
|
+
for (const [path, methods] of Object.entries(incoming)) {
|
|
296
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
297
|
+
const existing = paths[path]?.[method];
|
|
298
|
+
if (existing === undefined) {
|
|
299
|
+
(paths[path] ??= {})[method] = op;
|
|
300
|
+
} else if (operationKey(existing) === operationKey(op)) {
|
|
301
|
+
// identical (a framework operation shared by many domains) — keep the one entry.
|
|
302
|
+
} else {
|
|
303
|
+
// genuine collision: two domains, same path+method, different operations. Namespace BOTH
|
|
304
|
+
// under their domain so neither wins silently.
|
|
305
|
+
const domOf = (o: unknown): string =>
|
|
306
|
+
String((o as { "x-nomos"?: { domain?: string } })?.["x-nomos"]?.domain ?? "domain");
|
|
307
|
+
const bucket = paths[path]!;
|
|
308
|
+
(paths[`/${domOf(existing)}${path}`] ??= {})[method] = existing;
|
|
309
|
+
(paths[`/${domOf(op)}${path}`] ??= {})[method] = op;
|
|
310
|
+
delete bucket[method];
|
|
311
|
+
if (Object.keys(bucket).length === 0) delete paths[path];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
255
317
|
/**
|
|
256
318
|
* mergeOpenApiFragments — the WORKSPACE spec = the union of its installed domains' fragments. The
|
|
257
319
|
* runtime step (driven by `currentLaw`), kept here as a pure, law-blind function:
|
|
@@ -266,27 +328,10 @@ export function mergeOpenApiFragments(
|
|
|
266
328
|
opts: { title?: string; lawHash?: string } = {},
|
|
267
329
|
): OpenApiDoc {
|
|
268
330
|
const paths: Record<string, Record<string, unknown>> = {};
|
|
331
|
+
const frameworkPaths: Record<string, Record<string, unknown>> = {};
|
|
269
332
|
for (const frag of fragments) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const existing = paths[path]?.[method];
|
|
273
|
-
if (existing === undefined) {
|
|
274
|
-
(paths[path] ??= {})[method] = op;
|
|
275
|
-
} else if (operationKey(existing) === operationKey(op)) {
|
|
276
|
-
// identical (a framework operation shared by many domains) — keep the one entry.
|
|
277
|
-
} else {
|
|
278
|
-
// genuine collision: two domains, same path+method, different operations. Namespace BOTH
|
|
279
|
-
// under their domain so neither wins silently.
|
|
280
|
-
const domOf = (o: unknown): string =>
|
|
281
|
-
String((o as { "x-nomos"?: { domain?: string } })?.["x-nomos"]?.domain ?? "domain");
|
|
282
|
-
const bucket = paths[path]!;
|
|
283
|
-
(paths[`/${domOf(existing)}${path}`] ??= {})[method] = existing;
|
|
284
|
-
(paths[`/${domOf(op)}${path}`] ??= {})[method] = op;
|
|
285
|
-
delete bucket[method];
|
|
286
|
-
if (Object.keys(bucket).length === 0) delete paths[path];
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
333
|
+
mergePathsInto(paths, frag.paths);
|
|
334
|
+
mergePathsInto(frameworkPaths, frag["x-nomos-framework-routes"]?.paths ?? {});
|
|
290
335
|
}
|
|
291
336
|
return {
|
|
292
337
|
openapi: "3.1.0",
|
|
@@ -296,5 +341,8 @@ export function mergeOpenApiFragments(
|
|
|
296
341
|
...(opts.lawHash ? { "x-nomos-law-hash": opts.lawHash } : {}),
|
|
297
342
|
},
|
|
298
343
|
paths,
|
|
344
|
+
...(Object.keys(frameworkPaths).length > 0
|
|
345
|
+
? { "x-nomos-framework-routes": { paths: frameworkPaths } }
|
|
346
|
+
: {}),
|
|
299
347
|
};
|
|
300
348
|
}
|
package/src/engine_entry.ts
CHANGED
|
@@ -931,11 +931,9 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
931
931
|
* THE SELF-DESCRIBE VERB (G1) — the lump's OWN OpenAPI, derived from the SAME modules it
|
|
932
932
|
* dispatches. The kernel runs this over the FOLDED law (the lump IS `nomos:executable:
|
|
933
933
|
* javascriptHex` in the package USDA in custody), so the workspace's API surface is a pure,
|
|
934
|
-
* deterministic function of installed law — NOT a host-staged blob.
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
* sums/mins/maxes/spatials/workspaceInvariants from `config.openapiDecls`), `generateOpenApi
|
|
938
|
-
* Fragment`, then `mergeOpenApiFragments` — the EXACT calls compile_package_main makes.
|
|
934
|
+
* deterministic function of installed law — NOT a host-staged blob. Its public `paths` reproduce
|
|
935
|
+
* the `<name>.openapi.json` the compiler wrote; describe also carries a private, law-derived
|
|
936
|
+
* framework routing manifest which the host consumes internally and never publishes.
|
|
939
937
|
* `opts.lawHash`/`opts.title` are injected by the kernel at describe-time (the version stamp +
|
|
940
938
|
* package title), matching the compiler's `domainHash`/`"<name> (nomos package)"`.
|
|
941
939
|
*/
|
|
@@ -969,7 +967,10 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
969
967
|
? { workspaceInvariants: [...decls.workspaceInvariants] }
|
|
970
968
|
: {}),
|
|
971
969
|
} as unknown as DomainModule;
|
|
972
|
-
fragments.push(generateOpenApiFragment(module,
|
|
970
|
+
fragments.push(generateOpenApiFragment(module, {
|
|
971
|
+
...(lawHash !== undefined ? { lawHash } : {}),
|
|
972
|
+
includeFrameworkRoutes: true,
|
|
973
|
+
}));
|
|
973
974
|
}
|
|
974
975
|
const title = opts?.title ?? config.packageTitle;
|
|
975
976
|
return mergeOpenApiFragments(fragments, {
|
|
@@ -86,6 +86,32 @@ export const retireChild = directive("retireChild")
|
|
|
86
86
|
];
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The operator half of the same retirement action. This is deliberately a separate, framework-internal
|
|
91
|
+
* directive because the relation gate is structural: creator recovery and platform administration have
|
|
92
|
+
* different authority proofs even though the client presents one `retire` action and both write the same
|
|
93
|
+
* lifecycle fact. A broken creator key must never make a malformed child impossible to converge.
|
|
94
|
+
*/
|
|
95
|
+
export const retireChildAsAdmin = directive("retireChildAsAdmin")
|
|
96
|
+
.ensures(NomosChildLifecycle)
|
|
97
|
+
.payload(z.object({
|
|
98
|
+
child: z.string().min(1),
|
|
99
|
+
principal: z.string().min(1),
|
|
100
|
+
retiredBy: z.string().min(1),
|
|
101
|
+
retiredAt: z.number().int().nonnegative(),
|
|
102
|
+
}))
|
|
103
|
+
.plan((p) => {
|
|
104
|
+
const rec = instance(NomosChildLifecycle, childLifecycleId(p.child));
|
|
105
|
+
return [
|
|
106
|
+
set(rec, "child", p.child),
|
|
107
|
+
set(rec, "principal", p.principal),
|
|
108
|
+
set(rec, "status", "retired"),
|
|
109
|
+
set(rec, "retiredBy", p.retiredBy),
|
|
110
|
+
set(rec, "retiredAt", p.retiredAt),
|
|
111
|
+
];
|
|
112
|
+
})
|
|
113
|
+
.requires("admin");
|
|
114
|
+
|
|
89
115
|
export const reclaimChild = directive("reclaimChild")
|
|
90
116
|
.ensures(NomosChildLifecycle)
|
|
91
117
|
.payload(z.object({
|
|
@@ -102,6 +128,24 @@ export const reclaimChild = directive("reclaimChild")
|
|
|
102
128
|
];
|
|
103
129
|
});
|
|
104
130
|
|
|
131
|
+
/** Admin-gated recovery half of {@link reclaimChild}; hidden behind the client's one reclaim action. */
|
|
132
|
+
export const reclaimChildAsAdmin = directive("reclaimChildAsAdmin")
|
|
133
|
+
.ensures(NomosChildLifecycle)
|
|
134
|
+
.payload(z.object({
|
|
135
|
+
child: z.string().min(1),
|
|
136
|
+
reclaimedBy: z.string().min(1),
|
|
137
|
+
reclaimedAt: z.number().int().nonnegative(),
|
|
138
|
+
}))
|
|
139
|
+
.plan((p) => {
|
|
140
|
+
const rec = instance(NomosChildLifecycle, childLifecycleId(p.child));
|
|
141
|
+
return [
|
|
142
|
+
set(rec, "status", "reclaimed"),
|
|
143
|
+
set(rec, "reclaimedBy", p.reclaimedBy),
|
|
144
|
+
set(rec, "reclaimedAt", p.reclaimedAt),
|
|
145
|
+
];
|
|
146
|
+
})
|
|
147
|
+
.requires("admin");
|
|
148
|
+
|
|
105
149
|
/** Reclaim is two-phase; a child must first be explicitly withdrawn. */
|
|
106
150
|
export const reclaimChildRequiresRetired = workspaceInvariant("ReclaimChildRequiresRetired")
|
|
107
151
|
.on("reclaimChild")
|
|
@@ -115,6 +159,18 @@ export const reclaimChildRequiresRetired = workspaceInvariant("ReclaimChildRequi
|
|
|
115
159
|
: { reject: "reclaim-child-requires-retired" },
|
|
116
160
|
);
|
|
117
161
|
|
|
162
|
+
/** Operator recovery has the same two-phase law, without depending on the broken creator credential. */
|
|
163
|
+
export const reclaimChildAsAdminRequiresRetired = workspaceInvariant("ReclaimChildAsAdminRequiresRetired")
|
|
164
|
+
.on("reclaimChildAsAdmin")
|
|
165
|
+
.reads((ctx) => [
|
|
166
|
+
refAs("record", "NomosChildLifecycle", "pre:" + childLifecycleId(String(ctx.intent.child ?? ""))),
|
|
167
|
+
])
|
|
168
|
+
.assert(({ record }) =>
|
|
169
|
+
(record as { status?: string } | undefined)?.status === "retired"
|
|
170
|
+
? { accept: true }
|
|
171
|
+
: { reject: "reclaim-child-requires-retired" },
|
|
172
|
+
);
|
|
173
|
+
|
|
118
174
|
/** Reclaimed is permanent: retirement cannot move a child back out of the terminal state. */
|
|
119
175
|
export const retireChildNotReclaimed = workspaceInvariant("RetireChildNotReclaimed")
|
|
120
176
|
.on("retireChild")
|
|
@@ -127,6 +183,18 @@ export const retireChildNotReclaimed = workspaceInvariant("RetireChildNotReclaim
|
|
|
127
183
|
: { accept: true },
|
|
128
184
|
);
|
|
129
185
|
|
|
186
|
+
/** Reclaimed is terminal for operator recovery too. */
|
|
187
|
+
export const retireChildAsAdminNotReclaimed = workspaceInvariant("RetireChildAsAdminNotReclaimed")
|
|
188
|
+
.on("retireChildAsAdmin")
|
|
189
|
+
.reads((ctx) => [
|
|
190
|
+
refAs("record", "NomosChildLifecycle", "pre:" + childLifecycleId(String(ctx.intent.child ?? ""))),
|
|
191
|
+
])
|
|
192
|
+
.assert(({ record }) =>
|
|
193
|
+
(record as { status?: string } | undefined)?.status === "reclaimed"
|
|
194
|
+
? { reject: "reclaimed-child-is-terminal" }
|
|
195
|
+
: { accept: true },
|
|
196
|
+
);
|
|
197
|
+
|
|
130
198
|
/** A lifecycle cannot mint authority for arbitrary custody: retirement must name its exact birth witness. */
|
|
131
199
|
export const retireChildRequiresBirth = workspaceInvariant("RetireChildRequiresBirth")
|
|
132
200
|
.on("retireChild")
|
|
@@ -143,6 +211,20 @@ export const retireChildRequiresBirth = workspaceInvariant("RetireChildRequiresB
|
|
|
143
211
|
: { reject: "retire-child-requires-exact-birth-record" };
|
|
144
212
|
});
|
|
145
213
|
|
|
214
|
+
/** Admin recovery may act for the recorded creator, but still cannot invent or retarget custody. */
|
|
215
|
+
export const retireChildAsAdminRequiresBirth = workspaceInvariant("RetireChildAsAdminRequiresBirth")
|
|
216
|
+
.on("retireChildAsAdmin")
|
|
217
|
+
.reads((ctx) => [
|
|
218
|
+
refAs("birth", "NomosBirthRecord", "pre:" + birthRecordId(String(ctx.intent.child ?? ""))),
|
|
219
|
+
])
|
|
220
|
+
.assert(({ birth }, ctx) => {
|
|
221
|
+
const record = birth as { child?: string; principal?: string } | undefined;
|
|
222
|
+
const intent = ctx?.intent as { child?: string; principal?: string } | undefined;
|
|
223
|
+
return record?.child === intent?.child && record?.principal === intent?.principal
|
|
224
|
+
? { accept: true }
|
|
225
|
+
: { reject: "retire-child-requires-exact-birth-record" };
|
|
226
|
+
});
|
|
227
|
+
|
|
146
228
|
function sameLifecyclePrincipal(recorded: string | undefined, author: string | undefined): boolean {
|
|
147
229
|
const normalize = (s: string | undefined): string => s?.startsWith("user:") ? s.slice(5) : (s ?? "");
|
|
148
230
|
return normalize(recorded) !== "" && normalize(recorded) === normalize(author);
|
package/src/index.ts
CHANGED
|
@@ -477,10 +477,15 @@ export {
|
|
|
477
477
|
seedBirthPolicy,
|
|
478
478
|
grantBirthAllowance,
|
|
479
479
|
retireChild,
|
|
480
|
+
retireChildAsAdmin,
|
|
480
481
|
reclaimChild,
|
|
482
|
+
reclaimChildAsAdmin,
|
|
481
483
|
reclaimChildRequiresRetired,
|
|
484
|
+
reclaimChildAsAdminRequiresRetired,
|
|
482
485
|
retireChildRequiresBirth,
|
|
486
|
+
retireChildAsAdminRequiresBirth,
|
|
483
487
|
retireChildNotReclaimed,
|
|
488
|
+
retireChildAsAdminNotReclaimed,
|
|
484
489
|
} from "./framework/birth_meter.js";
|
|
485
490
|
// THE PLATFORM FOUNDING-ADMIN BOOTSTRAP (the platform analog of seedHomeCreator) — a tenant-importable
|
|
486
491
|
// factory that seats a tenant-named governance relation for a write-once, author-bound founder at genesis.
|