@githolon/dsl 0.78.0 → 0.79.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,13 @@
1
+ ## 0.54.1
2
+
3
+ - **Fix: principal normalization mismatch in signer recovery** — enrollment wrote
4
+ `identity:user:<uid>#signer` (the kernel-canonical captured principal) while the posture/preflight
5
+ reads stripped `user:` and reported false `missing-signer-enrollment` immediately after a
6
+ successful repair. One canonical normalizer now routes every surface; co2's five-step acceptance
7
+ (repair → immediate enrolled:true → canAuthor ok → birth admits) is the regression test, with
8
+ `user:`-shaped principals, proven failing-then-passing. Typed authz diagnosis now also rides the
9
+ `createWorkspace` relay (no more generic 422).
10
+
1
11
  ## 0.54.0
2
12
 
3
13
  - **Parent-signer recovery** (the reborn-parent scar): `ensureParentSignerEnrollment(...)` —
@@ -48,7 +48,7 @@ const int kWebviewBridgeVersion = 1;
48
48
  /// pre-stamp bundle, which reports nothing) is a stale baked asset and fails loudly at boot.
49
49
  /// MUST move with the paired @githolon/client release — a release-hygiene test pairs it with the
50
50
  /// runner stamp (the pubspec-pins pattern).
51
- const String kExpectedRunnerBundleVersion = '0.78.0';
51
+ const String kExpectedRunnerBundleVersion = '0.78.1';
52
52
 
53
53
  /// A STALE BAKED RUNNER BUNDLE: the app's built assets carry an older nomos-runner.js than this
54
54
  /// nomos_client release ships — Flutter's build cache kept it across a pub upgrade. Thrown from
@@ -1564,7 +1564,20 @@ class NomosBridge {
1564
1564
  throw await _wrapDomainNotInstalled(e,
1565
1565
  domain: domain, domainHash: domainHash, requestedWorkspace: parent);
1566
1566
  }
1567
- return (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
1567
+ final verdict = (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
1568
+ // STRUCTURED AUTHZ REFUSAL (co2 DX): a refused relay used to surface only the generic 422 body.
1569
+ // The JS relay now classifies the kernel refusal into `nomos.authz.*` errorInfo — mint the SAME
1570
+ // typed error the offerDirective lane throws ([ParentSignerEnrollmentMissing]/[AuthzRefusedError])
1571
+ // so the app sees the diagnosis (actor/keyHash/object/relation/cause), not prose. Non-authz
1572
+ // refusals keep the verdict-return contract unchanged.
1573
+ if (verdict['ok'] == false) {
1574
+ final info = verdict['errorInfo'];
1575
+ final code = info is Map ? (info['code'] ?? '').toString() : '';
1576
+ if (code.startsWith('nomos.authz.')) {
1577
+ throw nomosErrorFromInfo(NomosErrorInfo.fromJson(info));
1578
+ }
1579
+ }
1580
+ return verdict;
1568
1581
  }
1569
1582
 
1570
1583
  /// SHARE a workspace/record with a user — the one-call cross-custody grant that lands in their home's
package/dart/pubspec.lock CHANGED
@@ -191,7 +191,7 @@ packages:
191
191
  path: "../dart_types"
192
192
  relative: true
193
193
  source: path
194
- version: "0.4.0"
194
+ version: "0.4.1"
195
195
  package_config:
196
196
  dependency: transitive
197
197
  description:
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.54.0
14
+ version: 0.54.1
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -20,7 +20,7 @@ dependencies:
20
20
  # The pure-data core (wire types, offer codec, NomosReads, schema validation, error
21
21
  # info). nomos_client depends on and re-exports it, so existing imports are unchanged.
22
22
  # For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
23
- nomos_types: ^0.4.0
23
+ nomos_types: ^0.4.1
24
24
  json_schema: ^5.2.2
25
25
  crypto: ^3.0.0
26
26
 
@@ -29,6 +29,11 @@ String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime'
29
29
 
30
30
  const _ws = 'estate-birth-signed';
31
31
  const _uid = 'sample-owner';
32
+ // THE co2 REGRESSION SHAPE: the principal arrives ALREADY `user:`-qualified (the captured-actor wire
33
+ // form — exactly what NomosScope/co2 pass). KERNEL TRUTH: the gate checks
34
+ // `identity:<captured principal VERBATIM>`, so every read/repair below must resolve the object to
35
+ // `identity:user:<uid>` — a stripped `identity:<uid>` is the normalization bug this test pins.
36
+ const _principal = 'user:$_uid';
32
37
 
33
38
  String? _missingPrereq() {
34
39
  if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
@@ -68,15 +73,16 @@ void main() {
68
73
 
69
74
  // 1. THE PREFLIGHT, broken state: the verdict names the failing premise BEFORE any authoring.
70
75
  final v0 = await session.canAuthor('birthEstateWorkspace',
71
- ws: _ws, principal: _uid, publicKey: pub);
76
+ ws: _ws, principal: _principal, publicKey: pub);
72
77
  expect(v0['ok'], isNot(true));
73
78
  expect(v0['cause'], 'missing-signer-enrollment');
74
- expect(v0['actor'], 'user:$_uid');
79
+ expect(v0['actor'], _principal,
80
+ reason: 'the canonical actor is the principal VERBATIM — never stripped, never double-prefixed');
75
81
  expect((v0['keyHash'] ?? '').toString(), hasLength(64));
76
82
 
77
83
  // ...and the TYPED variant throws the ONE error with the ONE remedy.
78
84
  await expectLater(
79
- session.canAuthorTyped('birthEstateWorkspace', ws: _ws, principal: _uid, publicKey: pub),
85
+ session.canAuthorTyped('birthEstateWorkspace', ws: _ws, principal: _principal, publicKey: pub),
80
86
  throwsA(isA<ParentSignerEnrollmentMissing>()
81
87
  .having((e) => e.recommendedAction, 'recommendedAction',
82
88
  NomosRecommendedAction.ensureParentSignerEnrollment)
@@ -85,15 +91,17 @@ void main() {
85
91
 
86
92
  // 2. THE POSTURE READ agrees (diagnostic; nothing authored).
87
93
  final posture0 = await session.parentSignerEnrollment(
88
- parent: _ws, principal: _uid, publicKey: pub);
94
+ parent: _ws, principal: _principal, publicKey: pub);
89
95
  expect(posture0['enrolled'], false);
90
- expect(posture0['object'], 'identity:$_uid');
96
+ // KERNEL TRUTH (executor::signer_object): the object is identity:<captured principal VERBATIM>
97
+ // — `identity:user:<uid>`, NOT a stripped `identity:<uid>` (the co2 normalization bug).
98
+ expect(posture0['object'], 'identity:$_principal');
91
99
  expect(posture0['relation'], 'signer');
92
100
 
93
101
  // 3. THE REPAIR — idempotent; the signing directive is DISCOVERED from the installed law
94
102
  // (this fixture composes it under domain "home", NOT the framework "workspaces").
95
103
  final r1 = await session.ensureParentSignerEnrollment(
96
- parent: _ws, principal: _uid, publicKey: pub, authorSecret: secret);
104
+ parent: _ws, principal: _principal, publicKey: pub, authorSecret: secret);
97
105
  expect(r1['enrolled'], true);
98
106
  expect(r1['repaired'], true);
99
107
  expect(r1['domain'], 'home',
@@ -101,15 +109,24 @@ void main() {
101
109
 
102
110
  // 4. Re-run = pure no-op (the idempotence contract).
103
111
  final r2 = await session.ensureParentSignerEnrollment(
104
- parent: _ws, principal: _uid, publicKey: pub, authorSecret: secret);
112
+ parent: _ws, principal: _principal, publicKey: pub, authorSecret: secret);
105
113
  expect(r2['enrolled'], true);
106
114
  expect(r2['repaired'], false);
107
115
 
108
116
  // 5. The preflight flips OK…
109
117
  final v1 = await session.canAuthor('birthEstateWorkspace',
110
- ws: _ws, principal: _uid, publicKey: pub);
118
+ ws: _ws, principal: _principal, publicKey: pub);
111
119
  expect(v1['ok'], true, reason: 'causes: ${v1['causes']}');
112
120
 
121
+ // 5b. co2's step-5 regression VERBATIM: immediately after repaired:true, the POSTURE read (with
122
+ // the SAME user:-qualified principal) reports enrolled:true against the canonical object.
123
+ final posture1 = await session.parentSignerEnrollment(
124
+ parent: _ws, principal: _principal, publicKey: pub);
125
+ expect(posture1['enrolled'], true,
126
+ reason: 'the repair wrote identity:$_principal — a second normalizer that strips user: '
127
+ 'would read identity:$_uid and wrongly report missing-signer-enrollment');
128
+ expect(posture1['object'], 'identity:$_principal');
129
+
113
130
  // 6. …and the SIGNED sibling birth ADMITS (per-offer signing key — the repaired device).
114
131
  final lawHash = harness.lawHashes(_ws).first;
115
132
  final outcome = await session.offerCreates(
@@ -1,3 +1,8 @@
1
+ ## 0.4.1
2
+
3
+ - `canonicalPrincipal` / `signerObjectOf` — THE one principal normalizer (kernel truth:
4
+ `identity:<actor-verbatim>`, never stripped). All signer-recovery surfaces route through it.
5
+
1
6
  ## 0.4.0
2
7
 
3
8
  - Typed authorization diagnosis errors: `AuthzRefusedError` ({workspace, actor, keyHash, object,
@@ -54,3 +54,26 @@ class NomosDeviceSecret {
54
54
  @override
55
55
  String toString() => 'NomosDeviceSecret(***)';
56
56
  }
57
+
58
+ /// THE ONE PRINCIPAL NORMALIZER (Dart surface — mirrors `@githolon/client` `canonicalPrincipal`).
59
+ ///
60
+ /// KERNEL TRUTH (`executor/src/lib.rs::signer_object` + `verify_author_and_derive`): the
61
+ /// authorization gate checks `identity:<captured principal VERBATIM>` where the captured principal
62
+ /// is the actor string the offer presented (`user:<uid>` on every SDK lane). The kernel never strips
63
+ /// or qualifies; neither may we. NEVER strip `user:`; qualify a bare auth-provider uid to
64
+ /// `user:<uid>` exactly once, HERE. Every producer and consumer of `identity:*#signer` objects
65
+ /// routes through this — a second normalizer is the bug (the co2 principal-normalization mismatch).
66
+ ///
67
+ /// - `user:abc` → `user:abc` (verbatim)
68
+ /// - `abc` → `user:abc` (bare uid, qualified once)
69
+ /// - any other namespaced subject (`key:<hash>`, …) rides verbatim
70
+ /// - empty/whitespace → `''`
71
+ String canonicalPrincipal(String? principal) {
72
+ final s = (principal ?? '').trim();
73
+ if (s.isEmpty) return '';
74
+ return s.contains(':') ? s : 'user:$s';
75
+ }
76
+
77
+ /// The Zanzibar OBJECT for a principal's signer relation — `identity:<canonical principal>`
78
+ /// (mirrors `executor::signer_object` over the captured principal).
79
+ String signerObjectOf(String? principal) => 'identity:${canonicalPrincipal(principal)}';
@@ -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.4.0
14
+ version: 0.4.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.78.0",
3
+ "version": "0.79.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",
@@ -74,7 +74,7 @@ import { domainHash, emitManifestBytes } from "./manifest.js";
74
74
  import { collectValueObjectDescriptors } from "./value_object.js";
75
75
  import { emitUsd } from "./usd.js";
76
76
  import { expandCapabilityExports } from "./capability_exports.js";
77
- import { RelationTuple } from "./framework/relations.js";
77
+ import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
78
78
  import { parentAttestedBirthMarker } from "./births.js";
79
79
  import { discoverUnsanctionedEnsures, sanctioningEnsureDirective } from "./secondary_ensure.js";
80
80
  import { assertIdOfLint } from "./idof_lint.js";
@@ -277,6 +277,21 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
277
277
  }
278
278
  }
279
279
  const relationTypes = [...(spec.relationTypes ?? [])];
280
+ // RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX — same idiom as the meter
281
+ // queries above): a domain that declares relation law (a relation schema, a `.requires(...)` gated
282
+ // directive, or relate-family/signer directives ensuring RelationTuple) gets the framework's
283
+ // `relationTuplesByObject` query appended (dedup by id) so the signer-recovery lanes
284
+ // (parentSignerEnrollment / ensureParentSignerEnrollment / canAuthor) can read the tuples on any
285
+ // workspace running the law — no tenant re-export to forget. ERA/OMIT RULE: a relation-free domain
286
+ // appends nothing (byte-identical module/package); a relation-using one gains the query on its NEXT
287
+ // recompile (a named, additive hash move the #58 evolve gate admits silently).
288
+ const usesRelations =
289
+ relationTypes.length > 0 ||
290
+ directives.some((d) => (d as { requiresRelationGate?: unknown }).requiresRelationGate !== undefined) ||
291
+ directives.some((d) => d.aggregateId === RelationTuple.id);
292
+ if (usesRelations && !queries.some((q) => q.id === relationTuplesByObject.id)) {
293
+ queries.push(relationTuplesByObject);
294
+ }
280
295
  // FIRST-CLASS VALUE OBJECTS (value_object.ts): the deduped, sorted descriptors of every
281
296
  // named VO the domain's aggregates carry (transitive nested VOs included) — the input to
282
297
  // the additive `nomosValueObjects` USD key. Omit-when-empty (era rule): a VO-free domain's
@@ -70,6 +70,7 @@ import {
70
70
  isFrameworkDirective,
71
71
  isFrameworkQuery,
72
72
  } from "./framework/framework_aggregates.js";
73
+ import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
73
74
  import { finishCount, type AnyCount } from "./count.js";
74
75
  import type { QueryDecl } from "./query.js";
75
76
  import type { SpatialDecl } from "./spatial.js";
@@ -924,11 +925,11 @@ const DART_TYPES_IMPORT = `import 'package:nomos_types/nomos_types.dart';`;
924
925
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
925
926
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
926
927
  // the pendingConflicts read rows.
927
- const NOMOS_TYPES_DEP = "^0.4.0";
928
+ const NOMOS_TYPES_DEP = "^0.4.1";
928
929
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
929
930
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
930
931
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
931
- const NOMOS_CLIENT_DEP = "^0.54.0";
932
+ const NOMOS_CLIENT_DEP = "^0.54.1";
932
933
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
933
934
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
934
935
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -1588,6 +1589,26 @@ async function main(): Promise<void> {
1588
1589
  if (!haveQuery.has(q.id)) queries.push(q);
1589
1590
  }
1590
1591
  }
1592
+ // RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX, same idiom as the birth-meter
1593
+ // queries above): any domain that declares relation law — a relation schema, a `.requires(...)`
1594
+ // gated directive, or the signer/grant primitives (directives ensuring `RelationTuple`) — gets the
1595
+ // framework's `relationTuplesByObject` query appended (dedup by id), so the recovery lanes
1596
+ // (parentSignerEnrollment / ensureParentSignerEnrollment / canAuthor) can read the signer tuples
1597
+ // on ANY workspace running that law without the tenant remembering the re-export. ERA/OMIT RULE:
1598
+ // a domain with no relation law emits NOTHING new (byte-identical package); a relation-using
1599
+ // domain's package/openapi gain one query on its NEXT recompile — a named, expected hash move
1600
+ // (the stable-ids doctrine: recompiles move identity/package hashes; already-deployed artifacts
1601
+ // are untouched, and a query add is additive so the #58 evolve gate admits it silently).
1602
+ {
1603
+ const dirsAll = directivesOf(merged);
1604
+ const usesRelations =
1605
+ dirsAll.some((dir) => (dir as { requiresRelationGate?: unknown }).requiresRelationGate !== undefined) ||
1606
+ dirsAll.some((dir) => (dir as { aggregateId?: string }).aggregateId === RelationTuple.id) ||
1607
+ discover<RelationTypeDecl>(merged, isRelationTypeDecl).length > 0;
1608
+ if (usesRelations && !queries.some((q) => q.id === relationTuplesByObject.id)) {
1609
+ queries.push(relationTuplesByObject);
1610
+ }
1611
+ }
1591
1612
  const counts = [
1592
1613
  ...discover<AnyCount>(merged, isCountDecl),
1593
1614
  ...resolveNamed<AnyCount>(merged, d.counts, "counts"),