@githolon/dsl 0.70.1 → 0.71.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 CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.47.0
2
+
3
+ - **Fix: `NomosBridge.createWorkspace` hardcoded `domain: "workspaces"`** in the underlying signed offer —
4
+ a tenant birthing through their OWN platform's custom law (e.g. a `birthEstateWorkspace` directive under a
5
+ `co2_platform` domain) was refused, because the sealed intent always claimed `domain: "workspaces"`
6
+ regardless of which law actually declares the requested `directiveId`. Added a `domain` parameter
7
+ (default `"workspaces"`, fully back-compatible) — call
8
+ `bridge.createWorkspace(domain: 'co2_platform', directiveId: 'birthEstateWorkspace', domainHash: ..., ...)`
9
+ to birth through a custom platform law. **This release was overdue**: the fix landed in `nomos_flutter`
10
+ 0.52.2's bundled JS runtime, but this Dart-level API surface lives in `nomos_client` and was never
11
+ republished alongside it — a real release-process gap, not a client-side workaround. Requires
12
+ `nomos_flutter >= 0.52.2` to match the bundled runtime behavior.
13
+
1
14
  ## 0.46.0
2
15
 
3
16
  - Custody lifecycle on `NomosBridge`/`NomosSession`: `connect(..., onCustodyBreak: CustodyBreakPolicy)`,
@@ -1169,10 +1169,18 @@ class NomosBridge {
1169
1169
  }));
1170
1170
 
1171
1171
  /// MAKE A WORKSPACE — the one-call tenant path. A child workspace under a platform/[parent] is a SIGNED
1172
- /// control-plane offer; the holon seals `createWorkspace` (or [directiveId] `birthChild`) with the
1173
- /// requester's device key ([authorSecret], as [actor], e.g. `user:<uid>`) and relays it to the parent's
1174
- /// gateway. Returns the worker verdict (`{ok, status, body}`). The tenant move is one line:
1172
+ /// control-plane offer; the holon seals `createWorkspace` (or [directiveId] `birthChild`, or ANY
1173
+ /// tenant-defined `.births()` directive — see [domain]) with the requester's device key ([authorSecret],
1174
+ /// as [actor], e.g. `user:<uid>`) and relays it to the parent's gateway. Returns the worker verdict
1175
+ /// (`{ok, status, body}`). The tenant move is one line:
1175
1176
  /// `await bridge.createWorkspace(name: 'foo', parent: 'my-platform', actor: 'user:$uid', authorSecret: key)`.
1177
+ ///
1178
+ /// [domain] defaults to `'workspaces'` (the generic root-style governance law every platform's
1179
+ /// createWorkspace/birthChild rides). To birth through the PARENT'S OWN custom platform law instead (e.g.
1180
+ /// a tenant `birthEstateWorkspace` under a `co2_platform` domain), pass that domain's key + [domainHash]:
1181
+ /// `bridge.createWorkspace(domain: 'co2_platform', directiveId: 'birthEstateWorkspace', domainHash: ...)`.
1182
+ /// The kernel looks up `(domain, directiveId)` in the ACTIVE installed law — [domain] must name the law
1183
+ /// that actually declares [directiveId], never assume `'workspaces'`.
1176
1184
  Future<Map<String, Object?>> createWorkspace({
1177
1185
  required String parent,
1178
1186
  required String actor,
@@ -1180,6 +1188,7 @@ class NomosBridge {
1180
1188
  String? name,
1181
1189
  Object? payload,
1182
1190
  String directiveId = 'createWorkspace',
1191
+ String domain = 'workspaces',
1183
1192
  String? domainHash,
1184
1193
  String? authToken,
1185
1194
  }) async {
@@ -1192,6 +1201,7 @@ class NomosBridge {
1192
1201
  if (name != null) 'name': name,
1193
1202
  if (payload != null) 'payload': payload,
1194
1203
  'directiveId': directiveId,
1204
+ 'domain': domain,
1195
1205
  if (domainHash != null) 'domainHash': domainHash,
1196
1206
  if (authToken != null) 'authToken': authToken,
1197
1207
  }));
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.46.0
14
+ version: 0.47.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -1,3 +1,11 @@
1
+ ## 0.2.0
2
+
3
+ - `NomosPrincipal` / `NomosDeviceSecret` — branded identity types for the generated cross-workspace birth
4
+ methods (`.births()` directives). `actor`/`authorSecret` were previously bare, adjacent, same-shaped
5
+ strings — a transposition footgun with no compiler check. `NomosPrincipal.user(uid)`/`.parse('user:<uid>')`
6
+ and the opaque `NomosDeviceSecret` (redacts in `toString()`) make swapping them a type error. Purely
7
+ additive.
8
+
1
9
  ## 0.1.4
2
10
 
3
11
  - Custody-lifecycle types: `NomosCustodyBreak`, `NomosCustodyStatus`, `NomosCustodyEvent`,
@@ -33,6 +33,9 @@ export 'src/errors.dart';
33
33
  export 'src/foreign_ref.dart';
34
34
  export 'src/activity.dart';
35
35
  export 'src/nomos_ref.dart';
36
+ // Branded identity wrappers (NomosPrincipal / NomosDeviceSecret) for the generated cross-workspace
37
+ // birth methods — actor/authorSecret are distinct types, so a transposition is a compile error.
38
+ export 'src/principal.dart';
36
39
  // The CUSTODY-LIFECYCLE types (rebirth_and_custody_lifecycle.md): NomosCustodyBreak / NomosCustodyStatus /
37
40
  // CustodyBreakPolicy — custody incompatibility as a first-class, typed, app-decidable event. Pure data.
38
41
  export 'src/custody.dart';
@@ -0,0 +1,56 @@
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 QuickJS engine}.
4
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /// BRANDED IDENTITY TYPES for the generated cross-workspace birth methods.
7
+ ///
8
+ /// A `.births()` directive's generated wrapper takes an `actor` (WHO births) and an optional
9
+ /// `authorSecret` (the signing key). Both were previously bare strings — two adjacent same-shaped
10
+ /// params that a caller can transpose with NO compiler error and a silent runtime failure (the same
11
+ /// bug class as a raw `domain`/`directiveId` swap). These wrappers make the transposition a type
12
+ /// error: `NomosPrincipal` and `NomosDeviceSecret` are distinct types, so passing one where the other
13
+ /// belongs won't compile. The generated method unwraps `.wire` to call the (unchanged) runtime
14
+ /// primitive `NomosBridge.createWorkspace(actor: String, authorSecret: String?, …)`.
15
+
16
+ /// A verified principal identity (`user:<uid>` on the wire). Construct with [NomosPrincipal.user]
17
+ /// from a bare uid, or [NomosPrincipal.parse] from a `user:<uid>` wire string.
18
+ class NomosPrincipal {
19
+ /// The bare uid (no `user:` prefix).
20
+ final String uid;
21
+ const NomosPrincipal._(this.uid);
22
+
23
+ /// From a bare uid (e.g. an auth-provider user id) — the common case.
24
+ factory NomosPrincipal.user(String uid) => NomosPrincipal._(uid);
25
+
26
+ /// From a `user:<uid>` wire string — throws [ArgumentError] on any other shape.
27
+ factory NomosPrincipal.parse(String wire) {
28
+ if (!wire.startsWith('user:')) {
29
+ throw ArgumentError('NomosPrincipal.parse: expected "user:<uid>", got "$wire"');
30
+ }
31
+ return NomosPrincipal._(wire.substring(5));
32
+ }
33
+
34
+ /// The wire form the kernel seals as the captured principal: `user:<uid>`.
35
+ String get wire => 'user:$uid';
36
+
37
+ @override
38
+ bool operator ==(Object other) => other is NomosPrincipal && other.uid == uid;
39
+ @override
40
+ int get hashCode => uid.hashCode;
41
+ @override
42
+ String toString() => wire;
43
+ }
44
+
45
+ /// A device signing secret (base64). A distinct type from [NomosPrincipal] so it can't be swapped
46
+ /// with `actor`; its [toString] NEVER leaks the secret (redacted), so it is safe in logs/errors.
47
+ class NomosDeviceSecret {
48
+ final String _base64;
49
+ const NomosDeviceSecret(this._base64);
50
+
51
+ /// The base64 wire form handed to the (unchanged) runtime primitive.
52
+ String get wire => _base64;
53
+
54
+ @override
55
+ String toString() => 'NomosDeviceSecret(***)';
56
+ }
@@ -11,7 +11,7 @@ description: >-
11
11
  Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
12
12
  no runtime. The hand-written serde-JSON-compatible types plus the offer codec,
13
13
  reactive read engine, schema validation, and data-only error/trace types.
14
- version: 0.1.4
14
+ version: 0.2.0
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.70.1",
3
+ "version": "0.71.0",
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",
@@ -184,9 +184,14 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
184
184
  const methods: string[] = [];
185
185
 
186
186
  // ── directives → typed offer ──
187
- const dirs = (mod.directives as AnyDirective[]).filter(
187
+ // A `.births()` directive (birthsChild === true) is NOT a same-workspace write — it is a signed
188
+ // cross-workspace governance offer to a PARENT. It gets a typed `bridge.createWorkspace(...)` wrapper
189
+ // (below), NOT an `offerOutcome`; excluded here so a frontend dev can never call it as a local write.
190
+ const allDirs = (mod.directives as AnyDirective[]).filter(
188
191
  (d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object",
189
192
  );
193
+ const birthDirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild === true);
194
+ const dirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild !== true);
190
195
  for (const d of dirs) {
191
196
  const payloadClass = `${cap(d.id)}Payload`;
192
197
  const stamps = autoStampFields((d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema);
@@ -268,6 +273,43 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
268
273
  }
269
274
  }
270
275
 
276
+ // ── `.births()` directives → typed CROSS-WORKSPACE birth (bridge.createWorkspace) ──
277
+ // Each births a CHILD WORKSPACE under `parent` via a signed governance offer routed by the PARENT'S
278
+ // law (POST /v2/workspaces/<parent>/<directiveId>) — never a same-workspace write. The declaring
279
+ // domain's key + directiveId + baked domainHash are all bound at codegen time, so a frontend dev
280
+ // never hand-writes the four raw strings this replaces (the co2 production bug).
281
+ for (const d of birthDirs) {
282
+ const payloadClass = `${cap(d.id)}Payload`;
283
+ const method = safeMethod(d.id);
284
+ methods.push(
285
+ ` /// Births a CHILD WORKSPACE named by \`${payloadClass}\` under [parent], via a SIGNED cross-workspace`,
286
+ ` /// governance offer to the parent's \`${domain}/${d.id}\` law (POST /v2/workspaces/<parent>/${d.id}) —`,
287
+ ` /// NOT a same-workspace write, which is why it takes [parent]/[actor] (the requesting principal, e.g.`,
288
+ ` /// \`user:<uid>\`) unlike the other generated methods. The declaring domain, directiveId, and law hash`,
289
+ ` /// are baked in. Returns the parent gateway's verdict; [authorSecret] defaults to the bridge's device key.`,
290
+ ` /// [actor] is a [NomosPrincipal] and [authorSecret] a [NomosDeviceSecret] — distinct types so the two`,
291
+ ` /// can never be transposed (a bare-string swap would silently misfire).`,
292
+ ` Future<Map<String, Object?>> ${method}({`,
293
+ ` required String parent,`,
294
+ ` required NomosPrincipal actor,`,
295
+ ` NomosDeviceSecret? authorSecret,`,
296
+ ` required ${payloadClass} payload,`,
297
+ ` String? authToken,`,
298
+ ` }) =>`,
299
+ ` bridge.createWorkspace(`,
300
+ ` domain: ${dartStr(domain)},`,
301
+ ` directiveId: ${dartStr(d.id)},`,
302
+ ` domainHash: domainHash,`,
303
+ ` parent: parent,`,
304
+ ` actor: actor.wire,`,
305
+ ` authorSecret: authorSecret?.wire,`,
306
+ ` payload: payload.toPayloadJson(),`,
307
+ ` authToken: authToken,`,
308
+ ` );`,
309
+ ``,
310
+ );
311
+ }
312
+
271
313
  // ── declared queries → one-shot + reactive ──
272
314
  for (const q of (mod.queries ?? []) as QueryDecl[]) {
273
315
  const model = readModelClassName(q.returns);
package/src/codegen_ts.ts CHANGED
@@ -787,6 +787,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
787
787
  );
788
788
  const encPresent = encMap !== null;
789
789
  const encConst = encryptionConstName(opts.packageName);
790
+ // A package declaring ≥1 `.births()` directive emits the branded identity wrappers (below) its typed
791
+ // birth methods take for `actor`/`authorSecret`. Births-free packages emit nothing (byte-identical).
792
+ const hasBirths = modules.some((m) =>
793
+ (m.directives as Directive<unknown>[]).some((d) => (d as { birthsChild?: boolean })?.birthsChild === true),
794
+ );
790
795
  // ACTIVITY FEED (architecture/activity_descriptors.md): the package's declared `.activity(...)` descriptors.
791
796
  // Empty ⇒ NOTHING activity-related emits (byte-identical to a pre-activity compile).
792
797
  const activityEntries = collectActivityDescriptors(modules);
@@ -950,9 +955,17 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
950
955
  ` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
951
956
  ` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
952
957
  ` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
953
- ` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean }): Promise<string>;`,
958
+ ` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean }): Promise<string | { head: string; created?: { type: string; id: string }[]; workspace?: string }>;`,
954
959
  ` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
955
960
  ` offer(intentBytes: string | Uint8Array): Promise<string>;`,
961
+ ...(hasBirths
962
+ ? [
963
+ ` /** BIRTH A CHILD WORKSPACE (a signed CROSS-WORKSPACE governance offer to \`parent\`) — OPTIONAL:`,
964
+ ` * @githolon/client's connect() attaches it. The generated \`.births()\` methods call this, binding`,
965
+ ` * the declaring domain/directiveId/law hash so a frontend dev never hand-writes those raw strings. */`,
966
+ ` createWorkspace?(opts: { domain: string; directiveId: string; domainHash?: string; parent: string; actor: string; authorSecret?: string; payload?: unknown; name?: string; authToken?: string }): Promise<{ ok?: boolean; status?: number; body?: unknown }>;`,
967
+ ]
968
+ : []),
956
969
  ` /** The workspace this holon is bound to — the \`workspace\` of every [NomosRef] this client composes. */`,
957
970
  ` workspace: string;`,
958
971
  ...(encPresent
@@ -1046,6 +1059,25 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1046
1059
  ``,
1047
1060
  );
1048
1061
 
1062
+ // BRANDED IDENTITY WRAPPERS for the typed cross-workspace birth methods: `actor` (NomosPrincipal) and
1063
+ // `authorSecret` (NomosDeviceSecret) are distinct branded strings, so a caller can't transpose the two
1064
+ // (a bare-string swap would compile and silently misfire). Emitted iff the package declares `.births()`.
1065
+ if (hasBirths) {
1066
+ out.push(
1067
+ `/** A verified principal identity (\`user:<uid>\` on the wire) — construct with [nomosPrincipal]. A distinct`,
1068
+ ` * branded type from [NomosDeviceSecret] so the two birth-method params can never be transposed. */`,
1069
+ `export type NomosPrincipal = string & { readonly __nomosPrincipal: unique symbol };`,
1070
+ `/** A device signing secret (base64) — construct with [nomosDeviceSecret]. Distinct from [NomosPrincipal]. */`,
1071
+ `export type NomosDeviceSecret = string & { readonly __nomosDeviceSecret: unique symbol };`,
1072
+ `/** Brand a principal: a bare uid (\`ada\`) or a wire string (\`user:ada\`) → \`user:ada\`. */`,
1073
+ `export const nomosPrincipal = (uidOrWire: string): NomosPrincipal =>`,
1074
+ ` (uidOrWire.startsWith("user:") ? uidOrWire : \`user:\${uidOrWire}\`) as NomosPrincipal;`,
1075
+ `/** Brand a device signing secret (base64). */`,
1076
+ `export const nomosDeviceSecret = (base64: string): NomosDeviceSecret => base64 as NomosDeviceSecret;`,
1077
+ ``,
1078
+ );
1079
+ }
1080
+
1049
1081
  if (encPresent) {
1050
1082
  out.push(
1051
1083
  `// ── E2E SEALING (the sealing client) — baked map + KeyStore + seal/read helpers ──`,
@@ -1723,6 +1755,27 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1723
1755
  `export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed, lazyReg, d.id)};`,
1724
1756
  ``,
1725
1757
  );
1758
+ // A `.births()` directive is NOT a same-workspace write — it is a signed CROSS-WORKSPACE governance
1759
+ // offer to a PARENT. It gets a typed `holon.createWorkspace(...)` wrapper (declaring domain +
1760
+ // directiveId + baked domainHash bound at codegen time), NOT an `offerDirective`, so a frontend dev
1761
+ // never hand-writes the four raw strings this replaces (the co2 production bug).
1762
+ if ((d as { birthsChild?: boolean }).birthsChild === true) {
1763
+ const birthPayload = stamps.length ? `stampNow(payload, ${JSON.stringify(stamps)})` : "payload";
1764
+ const gateBirthDoc = gate ? ` GATED: needs \`actor\` (\`${gate.relation}\`).` : "";
1765
+ methods.push(
1766
+ ` /** Births a CHILD WORKSPACE (named by \`${payloadName}\`) under \`parent\`, via a SIGNED cross-workspace`,
1767
+ ` * governance offer to the parent's \`${domain}/${d.id}\` law (POST /v2/workspaces/<parent>/${d.id}) — NOT a`,
1768
+ ` * same-workspace write, hence \`parent\`/\`actor\` (the requesting principal, e.g. \`user:<uid>\`) unlike the`,
1769
+ ` * other methods. Declaring domain, directiveId, and law hash are baked in; returns the parent gateway's`,
1770
+ ` * verdict. \`authorSecret\` defaults to the holon's device key.${gateBirthDoc} */`,
1771
+ ` ${d.id}: (opts: { parent: string; actor: NomosPrincipal; authorSecret?: NomosDeviceSecret; payload: ${payloadName}; authToken?: string }): Promise<{ ok?: boolean; status?: number; body?: unknown }> => {`,
1772
+ ...(gateGuard ? [` if (!opts.actor || !opts.actor.trim()) throw new MissingOfferAuthority(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${JSON.stringify(gate!.relation)});`] : []),
1773
+ ` const _payload = opts.payload;`,
1774
+ ` return (holon.createWorkspace ?? (() => { throw new Error("this holon predates cross-workspace births — upgrade @githolon/client"); }))({ domain: ${JSON.stringify(domain)}, directiveId: ${JSON.stringify(d.id)}, domainHash, parent: opts.parent, actor: opts.actor, authorSecret: opts.authorSecret, payload: ${birthPayload.replace("payload", "_payload")}, authToken: opts.authToken });`,
1775
+ ` },`,
1776
+ );
1777
+ continue;
1778
+ }
1726
1779
  // THE ONE WRITE OUTCOME (architecture/nomos_ref.md): EVERY directive method returns Promise<NomosOutcome>
1727
1780
  // — `created` (typed refs of all minted aggregates, from returnCreated) + `head`, plus `target` for the
1728
1781
  // front-door-mint lane (the one statically-known addressed instance). One async block covers every case.
@@ -433,6 +433,9 @@ const DART_SUPPORT_CORE_SRC = [
433
433
  "foreign_ref.dart",
434
434
  "activity.dart",
435
435
  "nomos_ref.dart",
436
+ // Branded identity wrappers (NomosPrincipal/NomosDeviceSecret) — the generated cross-workspace birth
437
+ // methods type `actor`/`authorSecret` as these (not bare strings), so both must vendor. Pure data.
438
+ "principal.dart",
436
439
  // The custody-lifecycle types (NomosCustodyBreak/NomosCustodyStatus/CustodyBreakPolicy) — the generated
437
440
  // client + the vendored bridge reference them through the barrel, so they must vendor. Pure data.
438
441
  "custody.dart",
@@ -865,11 +868,15 @@ function emitDart(
865
868
 
866
869
  /** The pure-core support import generated TYPES files carry in layered-packages mode. */
867
870
  const DART_TYPES_IMPORT = `import 'package:nomos_types/nomos_types.dart';`;
868
- /** Version constraints the scaffolded pubspecs pin — move together with the Dart packages' releases. */
869
- // FLOOR = the release carrying every symbol generated code may reference (0.1.3: NomosRef).
870
- const NOMOS_TYPES_DEP = "^0.1.3";
871
- // FLOOR = the release carrying the bridge methods generated clients call (0.45.0: offerOutcome/NomosCreated.ref).
872
- const NOMOS_CLIENT_DEP = "^0.45.0";
871
+ /** Version constraints the scaffolded pubspecs pin — move together with the Dart packages' releases.
872
+ * Caret on 0.x is EXACT-MINOR (`^0.45.0` = `<0.46.0`), so these must track the CURRENT release, not
873
+ * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
874
+ // 0.1.4: custody-lifecycle types (CustodyBreakPolicy et al).
875
+ // 0.2.0: branded birth identities (NomosPrincipal/NomosDeviceSecret) — the generated cross-workspace
876
+ // `.births()` methods type actor/authorSecret as these, so a layered package needs nomos_types 0.2.0.
877
+ const NOMOS_TYPES_DEP = "^0.2.0";
878
+ // 0.46.0: custody lifecycle on NomosBridge/NomosSession (onCustodyBreak/custodyStatus/salvage lanes).
879
+ const NOMOS_CLIENT_DEP = "^0.46.0";
873
880
 
874
881
  const isPubPackageName = (s: string): boolean => /^[a-z][a-z0-9_]*$/.test(s);
875
882