@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 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
@@ -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(workspace: homeNameOf('user:$firebaseUid'), ...)
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) — e.g. to mint a device-BOUND IdP token
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.0
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",
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
  }
@@ -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 [reclaimChildRequiresRetired, retireChildNotReclaimed, retireChildRequiresBirth]) {
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
  }
@@ -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
- export function generateOpenApiFragment(
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
- for (const [path, methods] of Object.entries(frag.paths)) {
271
- for (const [method, op] of Object.entries(methods)) {
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
  }
@@ -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. It reproduces, byte for
935
- * byte, the `<name>.openapi.json` the compiler wrote: per domain (declaration order) build the
936
- * openapi-relevant DomainModule (directives + aggregates from the live exports; queries/counts/
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, lawHash !== undefined ? { lawHash } : {}));
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.