@githolon/dsl 0.77.1 → 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 +32 -0
- package/dart/lib/src/webview_bridge.dart +138 -2
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/errors_test.dart +52 -0
- package/dart/test/parent_signer_recovery_test.dart +151 -0
- package/dart_types/CHANGELOG.md +12 -0
- package/dart_types/lib/src/errors.dart +138 -0
- package/dart_types/lib/src/principal.dart +23 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/src/build_package.ts +16 -1
- package/src/compile_package_main.ts +23 -2
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.54.0
|
|
12
|
+
|
|
13
|
+
- **Parent-signer recovery** (the reborn-parent scar): `ensureParentSignerEnrollment(...)` —
|
|
14
|
+
idempotent verify-and-repair of `identity:<uid>#signer` on a configured parent (one relation read
|
|
15
|
+
when healthy; lawful repair via the discovered signing directive — enrollSigner pre-warrant, the
|
|
16
|
+
keyless enrollFirstDevice lane on a warranted parent); `parentSignerEnrollment(...)` posture read;
|
|
17
|
+
`devicePublicOf(secret)`.
|
|
18
|
+
- **`canAuthor(directiveId, ...)` preflight** — evaluates the gate's premises read-only and returns
|
|
19
|
+
the typed verdict ({actor, keyHash, cause}) BEFORE the user fills a form;
|
|
20
|
+
`NomosSession.canAuthorTyped` throws the typed error.
|
|
21
|
+
- Gate refusals classify into `nomos.authz.*` typed errors (see nomos_types 0.4.0). Requires the
|
|
22
|
+
paired runner bundle >= 0.78.0.
|
|
23
|
+
|
|
24
|
+
## 0.53.0
|
|
25
|
+
|
|
26
|
+
- **Stale-baked-bundle detection.** The runner bundle stamps its release version into the `ready`
|
|
27
|
+
frame (build-time injected — it cannot lie); the bridge refuses an older or pre-stamp bundle at
|
|
28
|
+
boot with the typed `StaleRunnerBundle` naming found/expected and the `flutter clean` remedy.
|
|
29
|
+
Flutter silently keeping an old baked bundle across a pub upgrade is now a one-line error at app
|
|
30
|
+
start instead of behavioral archaeology. `allowStaleRunnerBundle: true` is the deliberate
|
|
31
|
+
mixed-version escape hatch. Requires the paired runner bundle >= 0.77.1 (nomos_flutter 0.53.7).
|
|
32
|
+
|
|
1
33
|
## 0.52.0
|
|
2
34
|
|
|
3
35
|
- **Offer ack decoupled from watch fan-out** (co2's 2.08s 3×3-grid report): the durable acknowledgement
|
|
@@ -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.
|
|
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
|
|
@@ -1229,6 +1229,7 @@ class NomosBridge {
|
|
|
1229
1229
|
List<Map<String, Object?>>? attestations,
|
|
1230
1230
|
bool? force,
|
|
1231
1231
|
String? session,
|
|
1232
|
+
String? authorSecret,
|
|
1232
1233
|
}) =>
|
|
1233
1234
|
offerOutcome(
|
|
1234
1235
|
domain: domain,
|
|
@@ -1239,6 +1240,7 @@ class NomosBridge {
|
|
|
1239
1240
|
attestations: attestations,
|
|
1240
1241
|
force: force,
|
|
1241
1242
|
session: session,
|
|
1243
|
+
authorSecret: authorSecret,
|
|
1242
1244
|
);
|
|
1243
1245
|
|
|
1244
1246
|
/// THE ONE OUTCOME-RETURNING OFFER LANE — [offerDirective] answering the full typed [NomosOfferOutcome]
|
|
@@ -1255,6 +1257,9 @@ class NomosBridge {
|
|
|
1255
1257
|
List<Map<String, Object?>>? attestations,
|
|
1256
1258
|
bool? force,
|
|
1257
1259
|
String? session,
|
|
1260
|
+
// Per-offer signing key (base64 device secret): sign THIS offer with a specific device key instead
|
|
1261
|
+
// of the connect-time one — the parent-directed lanes (sibling births after a signer repair) use it.
|
|
1262
|
+
String? authorSecret,
|
|
1258
1263
|
}) async {
|
|
1259
1264
|
final Map<String, Object?> r;
|
|
1260
1265
|
// OFFER SPAN TAXONOMY (bridge legs): stamp the Dart-side send/receive around the op; the runner
|
|
@@ -1274,6 +1279,7 @@ class NomosBridge {
|
|
|
1274
1279
|
if (attestations != null) 'attestations': attestations,
|
|
1275
1280
|
if (force != null) 'force': force,
|
|
1276
1281
|
if (session != null) 'session': session,
|
|
1282
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
1277
1283
|
}));
|
|
1278
1284
|
} catch (e) {
|
|
1279
1285
|
throw await _wrapDomainNotInstalled(e,
|
|
@@ -1558,7 +1564,20 @@ class NomosBridge {
|
|
|
1558
1564
|
throw await _wrapDomainNotInstalled(e,
|
|
1559
1565
|
domain: domain, domainHash: domainHash, requestedWorkspace: parent);
|
|
1560
1566
|
}
|
|
1561
|
-
|
|
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;
|
|
1562
1581
|
}
|
|
1563
1582
|
|
|
1564
1583
|
/// SHARE a workspace/record with a user — the one-call cross-custody grant that lands in their home's
|
|
@@ -1692,6 +1711,87 @@ class NomosBridge {
|
|
|
1692
1711
|
if (authToken != null) 'authToken': authToken,
|
|
1693
1712
|
}));
|
|
1694
1713
|
|
|
1714
|
+
/// Derive the device key's base64 PUBLIC half from its base64 SECRET seed, inside the LOCAL kernel
|
|
1715
|
+
/// (the secret never leaves the device). The recovery lanes use this when only the secret was
|
|
1716
|
+
/// persisted (the device-key store keeps just the seed) — e.g. to mint a device-BOUND IdP token
|
|
1717
|
+
/// (`authTokenForDevice(publicKey)`) for the keyless re-enroll lane.
|
|
1718
|
+
Future<String> devicePublicOf(String secret) async =>
|
|
1719
|
+
(_asMap(await _call('devicePublicOf', {'secret': secret}))['public'] ?? '')
|
|
1720
|
+
.toString();
|
|
1721
|
+
|
|
1722
|
+
/// PARENT-AUTHORITY POSTURE (read-only diagnostic): is this device key a LIVE `signer` for
|
|
1723
|
+
/// `principal` on `parent`'s chain (`identity:<principal>#signer@key:<sha256(publicKey)>`)? One O(1)
|
|
1724
|
+
/// relation read over the open gateway; nothing is authored. Returns
|
|
1725
|
+
/// `{parent, principal, keyHash, object, relation, enrolled, revoked, checkedAt}`.
|
|
1726
|
+
Future<Map<String, Object?>> parentSignerEnrollment(
|
|
1727
|
+
{required String parent,
|
|
1728
|
+
required String principal,
|
|
1729
|
+
String? keyHash,
|
|
1730
|
+
String? publicKey,
|
|
1731
|
+
String? authToken}) async =>
|
|
1732
|
+
_asMap(await _call('parentSignerEnrollment', {
|
|
1733
|
+
'parent': parent,
|
|
1734
|
+
'principal': principal,
|
|
1735
|
+
if (keyHash != null) 'keyHash': keyHash,
|
|
1736
|
+
if (publicKey != null) 'publicKey': publicKey,
|
|
1737
|
+
if (authToken != null) 'authToken': authToken,
|
|
1738
|
+
}));
|
|
1739
|
+
|
|
1740
|
+
/// IDEMPOTENT PARENT-SIGNER RECOVERY — ensure this device key is a live `signer` on `parent`,
|
|
1741
|
+
/// lawfully restoring the tuple a parent custody REBIRTH lost (the device key + the born home stay
|
|
1742
|
+
/// valid; no home rebirth, no hand-posted secrets). Already enrolled ⇒ pure no-op (`repaired:false`).
|
|
1743
|
+
/// Absent ⇒ re-authors the enrollment via the parent's OWN installed signing directive, DISCOVERED
|
|
1744
|
+
/// from its law (never a hardcoded framework domain — a tenant-composed `co2_platform` works);
|
|
1745
|
+
/// `domain`/`directiveId` override the discovery. A REVOKED key is never silently re-enrolled.
|
|
1746
|
+
/// TWO LAWFUL LANES, kernel-decided: a WARRANTED parent (the reborn-platform case) admits a fresh
|
|
1747
|
+
/// device-signer only via the keyless `enrollFirstDevice` — pass the device-bound [idToken]
|
|
1748
|
+
/// (+ [verifiedVia], the parent's AuthProvider issuer); a PRE-WARRANT parent admits the founding
|
|
1749
|
+
/// `enrollSigner` (no token needed).
|
|
1750
|
+
/// Returns `{parent, principal, keyHash, enrolled, repaired, domain?, directiveId?, checkedAt}`.
|
|
1751
|
+
Future<Map<String, Object?>> ensureParentSignerEnrollment(
|
|
1752
|
+
{required String parent,
|
|
1753
|
+
required String principal,
|
|
1754
|
+
String? publicKey,
|
|
1755
|
+
String? authorSecret,
|
|
1756
|
+
String? domain,
|
|
1757
|
+
String? directiveId,
|
|
1758
|
+
String? authToken,
|
|
1759
|
+
String? idToken,
|
|
1760
|
+
String? verifiedVia}) async =>
|
|
1761
|
+
_asMap(await _call('ensureParentSignerEnrollment', {
|
|
1762
|
+
'parent': parent,
|
|
1763
|
+
'principal': principal,
|
|
1764
|
+
if (publicKey != null) 'publicKey': publicKey,
|
|
1765
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
1766
|
+
if (domain != null) 'domain': domain,
|
|
1767
|
+
if (directiveId != null) 'directiveId': directiveId,
|
|
1768
|
+
if (authToken != null) 'authToken': authToken,
|
|
1769
|
+
if (idToken != null) 'idToken': idToken,
|
|
1770
|
+
if (verifiedVia != null) 'verifiedVia': verifiedVia,
|
|
1771
|
+
}));
|
|
1772
|
+
|
|
1773
|
+
/// AUTHORING PREFLIGHT — evaluate the SAME premises the kernel's authorization gate judges,
|
|
1774
|
+
/// READ-ONLY, and return a typed verdict WITHOUT authoring — so the app catches a doomed write
|
|
1775
|
+
/// (e.g. a reborn parent missing this device's signer tuple) BEFORE the user fills a form.
|
|
1776
|
+
/// `ws` targets another workspace (pass the PARENT to preflight a sibling birth). Returns
|
|
1777
|
+
/// `{ok, ws, directiveId, actor, keyHash, cause, causes, domain?, relation?, object?, error?, checkedAt}`
|
|
1778
|
+
/// where `cause` ∈ {directive-not-found, no-signing-key, missing-signer-enrollment, revoked-signer,
|
|
1779
|
+
/// missing-role, null}. Use [canAuthorTyped] on a [NomosSession] for the typed-error variant.
|
|
1780
|
+
Future<Map<String, Object?>> canAuthor(String directiveId,
|
|
1781
|
+
{String? ws,
|
|
1782
|
+
String? principal,
|
|
1783
|
+
String? publicKey,
|
|
1784
|
+
String? keyHash,
|
|
1785
|
+
String? authToken}) async =>
|
|
1786
|
+
_asMap(await _call('canAuthor', {
|
|
1787
|
+
'directiveId': directiveId,
|
|
1788
|
+
if (ws != null) 'ws': ws,
|
|
1789
|
+
if (principal != null) 'principal': principal,
|
|
1790
|
+
if (publicKey != null) 'publicKey': publicKey,
|
|
1791
|
+
if (keyHash != null) 'keyHash': keyHash,
|
|
1792
|
+
if (authToken != null) 'authToken': authToken,
|
|
1793
|
+
}));
|
|
1794
|
+
|
|
1695
1795
|
/// VERIFY_CHAIN — the kernel's from-genesis self-verification of THIS holon's folded ledger (read-only:
|
|
1696
1796
|
/// contiguity → every intent re-admitted through the ONE gate against the state folded from the prefix). For
|
|
1697
1797
|
/// a WARRANTED home this RE-FIRES the parent-attested-birth cert door on the cold-replay lane — so a
|
|
@@ -2258,6 +2358,42 @@ class NomosSession extends NomosBridge {
|
|
|
2258
2358
|
@override
|
|
2259
2359
|
Future<void> dispose() async {}
|
|
2260
2360
|
|
|
2361
|
+
/// TYPED AUTHORING PREFLIGHT — [NomosBridge.canAuthor], returning the raw verdict when the write
|
|
2362
|
+
/// would admit and THROWING the matching typed error when it would not: a missing parent signer
|
|
2363
|
+
/// tuple throws [ParentSignerEnrollmentMissing] (its `recommendedAction` names the ONE remedy —
|
|
2364
|
+
/// [ensureParentSignerEnrollment]); any other failing premise throws [AuthzRefusedError] carrying
|
|
2365
|
+
/// the evaluated {workspace, actor, keyHash, object, relation, cause}. Nothing is authored.
|
|
2366
|
+
Future<Map<String, Object?>> canAuthorTyped(String directiveId,
|
|
2367
|
+
{String? ws,
|
|
2368
|
+
String? principal,
|
|
2369
|
+
String? publicKey,
|
|
2370
|
+
String? keyHash,
|
|
2371
|
+
String? authToken}) async {
|
|
2372
|
+
final v = await canAuthor(directiveId,
|
|
2373
|
+
ws: ws, principal: principal, publicKey: publicKey, keyHash: keyHash, authToken: authToken);
|
|
2374
|
+
if (v['ok'] == true) return v;
|
|
2375
|
+
final cause = v['cause']?.toString();
|
|
2376
|
+
final target = v['ws']?.toString() ?? ws ?? workspace;
|
|
2377
|
+
final info = NomosErrorInfo(
|
|
2378
|
+
code: cause == 'missing-signer-enrollment'
|
|
2379
|
+
? 'nomos.authz.signer_enrollment_missing'
|
|
2380
|
+
: 'nomos.authz.refused',
|
|
2381
|
+
message:
|
|
2382
|
+
'canAuthor($directiveId) on $target: premise "$cause" failed for ${v['actor'] ?? principal ?? '(unknown actor)'}',
|
|
2383
|
+
workspace: target,
|
|
2384
|
+
operation: 'canAuthor',
|
|
2385
|
+
attributes: {
|
|
2386
|
+
if (v['actor'] != null) 'nomos.authz.actor': v['actor'],
|
|
2387
|
+
if (v['keyHash'] != null) 'nomos.authz.keyHash': v['keyHash'],
|
|
2388
|
+
if (v['object'] != null) 'nomos.authz.object': v['object'],
|
|
2389
|
+
if (v['relation'] != null) 'nomos.authz.relation': v['relation'],
|
|
2390
|
+
'nomos.authz.directiveId': directiveId,
|
|
2391
|
+
if (cause != null) 'nomos.authz.cause': cause,
|
|
2392
|
+
},
|
|
2393
|
+
);
|
|
2394
|
+
throw nomosErrorFromInfo(info);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2261
2397
|
// NO per-method Stream/watch/snapshot overrides live here anymore — the base class routes ALL event
|
|
2262
2398
|
// dispatch + plane state through [_eventPlane] (and law-scopes compat/onLawChanged via [_sessionName]),
|
|
2263
2399
|
// and the watch ops still carry this session's workspace because their registration goes through the
|
package/dart/pubspec.lock
CHANGED
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.
|
|
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.
|
|
23
|
+
nomos_types: ^0.4.1
|
|
24
24
|
json_schema: ^5.2.2
|
|
25
25
|
crypto: ^3.0.0
|
|
26
26
|
|
|
@@ -68,6 +68,58 @@ void main() {
|
|
|
68
68
|
expect(l.recommendedAction, NomosRecommendedAction.retry);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
test('authz signer_enrollment_missing mints ParentSignerEnrollmentMissing with the ONE remedy', () {
|
|
72
|
+
final e = nomosErrorFromInfo(info(
|
|
73
|
+
'nomos.authz.signer_enrollment_missing',
|
|
74
|
+
workspace: 'co2-platform',
|
|
75
|
+
attrs: {
|
|
76
|
+
'nomos.authz.actor': 'user:u-1',
|
|
77
|
+
'nomos.authz.keyHash': 'abab',
|
|
78
|
+
'nomos.authz.object': 'identity:u-1',
|
|
79
|
+
'nomos.authz.directiveId': 'birthEstateWorkspace',
|
|
80
|
+
},
|
|
81
|
+
));
|
|
82
|
+
expect(e, isA<NomosBridgeError>());
|
|
83
|
+
expect(e, isA<AuthzRefusedError>());
|
|
84
|
+
expect(e, isA<ParentSignerEnrollmentMissing>());
|
|
85
|
+
final p = e as ParentSignerEnrollmentMissing;
|
|
86
|
+
expect(p.cause, 'missing-signer-enrollment');
|
|
87
|
+
expect(p.workspace, 'co2-platform');
|
|
88
|
+
expect(p.actor, 'user:u-1');
|
|
89
|
+
expect(p.keyHash, 'abab');
|
|
90
|
+
expect(p.object, 'identity:u-1');
|
|
91
|
+
expect(p.relation, 'signer');
|
|
92
|
+
expect(p.directiveId, 'birthEstateWorkspace');
|
|
93
|
+
expect(p.recommendedAction, NomosRecommendedAction.ensureParentSignerEnrollment);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('authz cause=missing-signer-enrollment routes to ParentSignerEnrollmentMissing even under the generic code', () {
|
|
97
|
+
final e = nomosErrorFromInfo(info(
|
|
98
|
+
'nomos.authz.refused',
|
|
99
|
+
attrs: {'nomos.authz.cause': 'missing-signer-enrollment', 'nomos.authz.actor': 'user:u-9'},
|
|
100
|
+
));
|
|
101
|
+
expect(e, isA<ParentSignerEnrollmentMissing>());
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('authz missing_role stays AuthzRefusedError — role-absent is DISTINGUISHED from signer-absent', () {
|
|
105
|
+
final e = nomosErrorFromInfo(info(
|
|
106
|
+
'nomos.authz.missing_role',
|
|
107
|
+
workspace: 'ws-1',
|
|
108
|
+
attrs: {
|
|
109
|
+
'nomos.authz.cause': 'missing-role',
|
|
110
|
+
'nomos.authz.relation': 'creator',
|
|
111
|
+
'nomos.authz.object': 'workspace:self',
|
|
112
|
+
'nomos.authz.actor': 'user:u-2',
|
|
113
|
+
},
|
|
114
|
+
));
|
|
115
|
+
expect(e, isA<AuthzRefusedError>());
|
|
116
|
+
expect(e, isNot(isA<ParentSignerEnrollmentMissing>()));
|
|
117
|
+
final a = e as AuthzRefusedError;
|
|
118
|
+
expect(a.cause, 'missing-role');
|
|
119
|
+
expect(a.relation, 'creator');
|
|
120
|
+
expect(a.object, 'workspace:self');
|
|
121
|
+
});
|
|
122
|
+
|
|
71
123
|
test('custody.* still mints CustodyError (unchanged), unknown codes stay generic', () {
|
|
72
124
|
expect(nomosErrorFromInfo(info('nomos.custody.missing_object')), isA<CustodyError>());
|
|
73
125
|
final g = nomosErrorFromInfo(info('nomos.bridge.error'));
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// PARENT-SIGNER RECOVERY — the Dart surface leg (offline, the real runner + wasm).
|
|
2
|
+
//
|
|
3
|
+
// The co2 gap: after a parent custody rebirth the device key stays valid but the parent's chain no
|
|
4
|
+
// longer carries `identity:<uid>#signer` — and there was NO published Dart recovery path. This proves
|
|
5
|
+
// the new surfaces end-to-end over the node runner:
|
|
6
|
+
// · `canAuthor` / `canAuthorTyped` — the read-only preflight returns the TYPED verdict
|
|
7
|
+
// (cause `missing-signer-enrollment` → ParentSignerEnrollmentMissing) BEFORE any authoring;
|
|
8
|
+
// · `ensureParentSignerEnrollment` — idempotent verify-and-repair on the parent (repairs when the
|
|
9
|
+
// tuple is absent, pure no-op when live) with the signing directive DISCOVERED from the law;
|
|
10
|
+
// · `parentSignerEnrollment` — the diagnostic posture read;
|
|
11
|
+
// · after the repair, canAuthor flips ok and a SIGNED birthEstateWorkspace admits.
|
|
12
|
+
// (The warranted-reborn-parent kernel legs — the exact refusal + the keyless enrollFirstDevice
|
|
13
|
+
// recovery — are proven on the wasm by bench/scale/parent_signer_recovery.smoke.mjs.)
|
|
14
|
+
@TestOn('vm')
|
|
15
|
+
library;
|
|
16
|
+
|
|
17
|
+
import 'dart:io';
|
|
18
|
+
|
|
19
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
20
|
+
import 'package:nomos_types/nomos_types.dart'
|
|
21
|
+
show ParentSignerEnrollmentMissing, NomosRecommendedAction;
|
|
22
|
+
import 'package:test/test.dart';
|
|
23
|
+
|
|
24
|
+
String get _root => '${Directory.current.path}/../..';
|
|
25
|
+
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
|
|
26
|
+
String get _deploy =>
|
|
27
|
+
'$_root/bench/scale/fixtures/estate-birth-signed/build/estate-birth-signed.deploy.json';
|
|
28
|
+
String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
29
|
+
|
|
30
|
+
const _ws = 'estate-birth-signed';
|
|
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';
|
|
37
|
+
|
|
38
|
+
String? _missingPrereq() {
|
|
39
|
+
if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
|
|
40
|
+
if (!File(_deploy).existsSync()) {
|
|
41
|
+
return 'compile the fixture: cd bench/scale/fixtures/estate-birth-signed && node ../../../../dsl/compile_package.mjs';
|
|
42
|
+
}
|
|
43
|
+
if (!File('$_runtimeDir/holon.wasm').existsSync()) {
|
|
44
|
+
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
void main() {
|
|
50
|
+
test(
|
|
51
|
+
'canAuthor preflights the missing parent signer TYPED, ensureParentSignerEnrollment repairs it, '
|
|
52
|
+
'and the signed birth then admits', () async {
|
|
53
|
+
final skip = _missingPrereq();
|
|
54
|
+
if (skip != null) {
|
|
55
|
+
markTestSkipped(skip);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
final harness = await NomosTestHarness.ephemeral(
|
|
59
|
+
deployJsonPaths: [_deploy],
|
|
60
|
+
nodeEntry: _repoEntry,
|
|
61
|
+
runtimeDir: _runtimeDir,
|
|
62
|
+
workspace: _ws,
|
|
63
|
+
);
|
|
64
|
+
try {
|
|
65
|
+
final session = harness.bridge.session(_ws);
|
|
66
|
+
final device = await session.mintDeviceKey();
|
|
67
|
+
final pub = device['public']!.toString();
|
|
68
|
+
final secret = device['secret']!.toString();
|
|
69
|
+
|
|
70
|
+
// devicePublicOf re-derives the public half from the persisted seed (the Dart key store keeps
|
|
71
|
+
// only the secret) — the recovery lanes depend on this.
|
|
72
|
+
expect(await session.devicePublicOf(secret), pub);
|
|
73
|
+
|
|
74
|
+
// 1. THE PREFLIGHT, broken state: the verdict names the failing premise BEFORE any authoring.
|
|
75
|
+
final v0 = await session.canAuthor('birthEstateWorkspace',
|
|
76
|
+
ws: _ws, principal: _principal, publicKey: pub);
|
|
77
|
+
expect(v0['ok'], isNot(true));
|
|
78
|
+
expect(v0['cause'], 'missing-signer-enrollment');
|
|
79
|
+
expect(v0['actor'], _principal,
|
|
80
|
+
reason: 'the canonical actor is the principal VERBATIM — never stripped, never double-prefixed');
|
|
81
|
+
expect((v0['keyHash'] ?? '').toString(), hasLength(64));
|
|
82
|
+
|
|
83
|
+
// ...and the TYPED variant throws the ONE error with the ONE remedy.
|
|
84
|
+
await expectLater(
|
|
85
|
+
session.canAuthorTyped('birthEstateWorkspace', ws: _ws, principal: _principal, publicKey: pub),
|
|
86
|
+
throwsA(isA<ParentSignerEnrollmentMissing>()
|
|
87
|
+
.having((e) => e.recommendedAction, 'recommendedAction',
|
|
88
|
+
NomosRecommendedAction.ensureParentSignerEnrollment)
|
|
89
|
+
.having((e) => e.workspace, 'workspace', _ws)),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// 2. THE POSTURE READ agrees (diagnostic; nothing authored).
|
|
93
|
+
final posture0 = await session.parentSignerEnrollment(
|
|
94
|
+
parent: _ws, principal: _principal, publicKey: pub);
|
|
95
|
+
expect(posture0['enrolled'], false);
|
|
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');
|
|
99
|
+
expect(posture0['relation'], 'signer');
|
|
100
|
+
|
|
101
|
+
// 3. THE REPAIR — idempotent; the signing directive is DISCOVERED from the installed law
|
|
102
|
+
// (this fixture composes it under domain "home", NOT the framework "workspaces").
|
|
103
|
+
final r1 = await session.ensureParentSignerEnrollment(
|
|
104
|
+
parent: _ws, principal: _principal, publicKey: pub, authorSecret: secret);
|
|
105
|
+
expect(r1['enrolled'], true);
|
|
106
|
+
expect(r1['repaired'], true);
|
|
107
|
+
expect(r1['domain'], 'home',
|
|
108
|
+
reason: 'discovery must find the tenant-composed domain, never assume "workspaces"');
|
|
109
|
+
|
|
110
|
+
// 4. Re-run = pure no-op (the idempotence contract).
|
|
111
|
+
final r2 = await session.ensureParentSignerEnrollment(
|
|
112
|
+
parent: _ws, principal: _principal, publicKey: pub, authorSecret: secret);
|
|
113
|
+
expect(r2['enrolled'], true);
|
|
114
|
+
expect(r2['repaired'], false);
|
|
115
|
+
|
|
116
|
+
// 5. The preflight flips OK…
|
|
117
|
+
final v1 = await session.canAuthor('birthEstateWorkspace',
|
|
118
|
+
ws: _ws, principal: _principal, publicKey: pub);
|
|
119
|
+
expect(v1['ok'], true, reason: 'causes: ${v1['causes']}');
|
|
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
|
+
|
|
130
|
+
// 6. …and the SIGNED sibling birth ADMITS (per-offer signing key — the repaired device).
|
|
131
|
+
final lawHash = harness.lawHashes(_ws).first;
|
|
132
|
+
final outcome = await session.offerCreates(
|
|
133
|
+
domain: 'home',
|
|
134
|
+
directiveId: 'birthEstateWorkspace',
|
|
135
|
+
payload: {
|
|
136
|
+
'owner': 'user:$_uid',
|
|
137
|
+
'frameworkHash': harness.frameworkHash,
|
|
138
|
+
'lawHash': lawHash,
|
|
139
|
+
'bornAt': '2026-07-12T00:00:00Z',
|
|
140
|
+
},
|
|
141
|
+
actor: 'user:$_uid',
|
|
142
|
+
authorSecret: secret,
|
|
143
|
+
);
|
|
144
|
+
expect(outcome.head, isNotEmpty);
|
|
145
|
+
expect(outcome.born, isNotEmpty,
|
|
146
|
+
reason: 'the estate child births after the signer repair');
|
|
147
|
+
} finally {
|
|
148
|
+
await harness.dispose();
|
|
149
|
+
}
|
|
150
|
+
}, timeout: const Timeout(Duration(minutes: 3)));
|
|
151
|
+
}
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
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
|
+
|
|
6
|
+
## 0.4.0
|
|
7
|
+
|
|
8
|
+
- Typed authorization diagnosis errors: `AuthzRefusedError` ({workspace, actor, keyHash, object,
|
|
9
|
+
relation, directiveId, cause}) and `ParentSignerEnrollmentMissing` (the one remedy named:
|
|
10
|
+
`ensureParentSignerEnrollment`) — "valid user, but this device cannot prove it" and "user lacks
|
|
11
|
+
the role" are now distinct typed diagnoses. `NomosRecommendedAction.ensureParentSignerEnrollment`.
|
|
12
|
+
|
|
1
13
|
## 0.3.0
|
|
2
14
|
|
|
3
15
|
- `SurfacedValue<T>` — the typed read surface for `Surfaced(inner)` merge-driver fields: sealed
|
|
@@ -23,6 +23,7 @@ import 'error_info.dart' show NomosBridgeError, NomosErrorInfo;
|
|
|
23
23
|
NomosBridgeError nomosErrorFromInfo(NomosErrorInfo info) {
|
|
24
24
|
if (info.code.startsWith('nomos.custody.')) return CustodyError.fromInfo(info);
|
|
25
25
|
if (info.code.startsWith('nomos.law.')) return LawMismatchError.fromInfo(info);
|
|
26
|
+
if (info.code.startsWith('nomos.authz.')) return AuthzRefusedError.fromInfo(info);
|
|
26
27
|
if (info.code.startsWith('nomos.auth.')) return AuthError.fromInfo(info);
|
|
27
28
|
if (info.code.startsWith('nomos.load.')) return LoadError.fromInfo(info);
|
|
28
29
|
return NomosBridgeError(info.message, info: info);
|
|
@@ -56,6 +57,11 @@ class NomosRecommendedAction {
|
|
|
56
57
|
/// Re-open the holon / reconnect fresh (the loaded runtime is unusable).
|
|
57
58
|
static const reopen = 'reopen';
|
|
58
59
|
|
|
60
|
+
/// Restore the device signer on the parent workspace (`ensureParentSignerEnrollment` /
|
|
61
|
+
/// [NomosScope.home] repairs it automatically on reopen) — the ONE remedy for
|
|
62
|
+
/// [ParentSignerEnrollmentMissing] (a reborn parent that lost the signer tuple).
|
|
63
|
+
static const ensureParentSignerEnrollment = 'ensureParentSignerEnrollment';
|
|
64
|
+
|
|
59
65
|
/// No automatic remedy — surface to a human.
|
|
60
66
|
static const none = 'none';
|
|
61
67
|
|
|
@@ -76,6 +82,8 @@ class NomosRecommendedAction {
|
|
|
76
82
|
return 'retry the operation';
|
|
77
83
|
case reopen:
|
|
78
84
|
return 'reopen the holon (reconnect fresh)';
|
|
85
|
+
case ensureParentSignerEnrollment:
|
|
86
|
+
return 'restore the device signer on the parent (ensureParentSignerEnrollment / NomosScope.home repairs it on reopen)';
|
|
79
87
|
case none:
|
|
80
88
|
return 'no automatic remedy — inspect the error';
|
|
81
89
|
default:
|
|
@@ -175,6 +183,136 @@ class LawMismatchError extends NomosBridgeError {
|
|
|
175
183
|
String toString() => 'LawMismatchError($code): $mismatchMessage';
|
|
176
184
|
}
|
|
177
185
|
|
|
186
|
+
/// AUTHZ REFUSAL — the kernel's relation/signer gate refused an authored intent (`nomos.authz.*`).
|
|
187
|
+
/// ONE structured diagnosis instead of hand-comparing relation tuples across two workspaces: it names
|
|
188
|
+
/// the evaluated [workspace], [actor], [keyHash], [object], [relation] and the [cause] the gate
|
|
189
|
+
/// distinguished:
|
|
190
|
+
/// - `missing-signer-enrollment` — the signature is valid but `identity:<actor>#signer@key:<keyHash>`
|
|
191
|
+
/// is absent on the judging workspace (the reborn-parent case) → [ParentSignerEnrollmentMissing].
|
|
192
|
+
/// - `missing-role` — the signer binding held (or was not required) but the directive's
|
|
193
|
+
/// `.requires(<relation>)` tuple is absent for the actor on [object].
|
|
194
|
+
/// These fields ride the UNSEALED presentation lanes only (verdict / 422 body / bridge errorInfo) —
|
|
195
|
+
/// never the sealed chain bytes.
|
|
196
|
+
class AuthzRefusedError extends NomosBridgeError {
|
|
197
|
+
/// The stable machine code, e.g. `nomos.authz.signer_enrollment_missing`.
|
|
198
|
+
final String code;
|
|
199
|
+
|
|
200
|
+
/// The workspace whose gate judged the offer (e.g. the PARENT for a sibling birth).
|
|
201
|
+
final String? workspace;
|
|
202
|
+
|
|
203
|
+
/// The evaluated actor (`user:<uid>`).
|
|
204
|
+
final String? actor;
|
|
205
|
+
|
|
206
|
+
/// sha256(device public key) — the on-chain `key:<hash>` subject the gate checked.
|
|
207
|
+
final String? keyHash;
|
|
208
|
+
|
|
209
|
+
/// The Zanzibar object checked (e.g. `identity:<uid>` / `workspace:self`).
|
|
210
|
+
final String? object;
|
|
211
|
+
|
|
212
|
+
/// The required relation (`signer` for the enrollment premise; the `.requires(...)` relation otherwise).
|
|
213
|
+
final String? relation;
|
|
214
|
+
|
|
215
|
+
/// The refused directive, when known.
|
|
216
|
+
final String? directiveId;
|
|
217
|
+
|
|
218
|
+
/// WHICH premise failed: `missing-signer-enrollment` | `missing-role` | `revoked-signer` | …
|
|
219
|
+
final String? cause;
|
|
220
|
+
|
|
221
|
+
/// The named remedy.
|
|
222
|
+
final String recommendedAction;
|
|
223
|
+
|
|
224
|
+
AuthzRefusedError({
|
|
225
|
+
required this.code,
|
|
226
|
+
required String message,
|
|
227
|
+
this.workspace,
|
|
228
|
+
this.actor,
|
|
229
|
+
this.keyHash,
|
|
230
|
+
this.object,
|
|
231
|
+
this.relation,
|
|
232
|
+
this.directiveId,
|
|
233
|
+
this.cause,
|
|
234
|
+
this.recommendedAction = NomosRecommendedAction.none,
|
|
235
|
+
NomosErrorInfo? info,
|
|
236
|
+
}) : super(message, info: info);
|
|
237
|
+
|
|
238
|
+
factory AuthzRefusedError.fromInfo(NomosErrorInfo info) {
|
|
239
|
+
String? attr(String name) {
|
|
240
|
+
final v = info.attributes['nomos.authz.$name'] ?? info.attributes[name];
|
|
241
|
+
return v?.toString();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
final cause = attr('cause');
|
|
245
|
+
final fields = (
|
|
246
|
+
workspace: info.workspace,
|
|
247
|
+
actor: attr('actor'),
|
|
248
|
+
keyHash: attr('keyHash') ?? attr('key_hash'),
|
|
249
|
+
object: attr('object'),
|
|
250
|
+
relation: attr('relation'),
|
|
251
|
+
directiveId: attr('directiveId') ?? attr('directive_id') ?? attr('directive'),
|
|
252
|
+
);
|
|
253
|
+
if (info.code == 'nomos.authz.signer_enrollment_missing' || cause == 'missing-signer-enrollment') {
|
|
254
|
+
return ParentSignerEnrollmentMissing(
|
|
255
|
+
message: info.message,
|
|
256
|
+
workspace: fields.workspace,
|
|
257
|
+
actor: fields.actor,
|
|
258
|
+
keyHash: fields.keyHash,
|
|
259
|
+
object: fields.object,
|
|
260
|
+
directiveId: fields.directiveId,
|
|
261
|
+
info: info,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return AuthzRefusedError(
|
|
265
|
+
code: info.code,
|
|
266
|
+
message: info.message,
|
|
267
|
+
workspace: fields.workspace,
|
|
268
|
+
actor: fields.actor,
|
|
269
|
+
keyHash: fields.keyHash,
|
|
270
|
+
object: fields.object,
|
|
271
|
+
relation: fields.relation,
|
|
272
|
+
directiveId: fields.directiveId,
|
|
273
|
+
cause: cause,
|
|
274
|
+
recommendedAction: attr('recommendedAction') ?? attr('recommended_action') ?? NomosRecommendedAction.none,
|
|
275
|
+
info: info,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@override
|
|
280
|
+
String toString() =>
|
|
281
|
+
'AuthzRefusedError($code${workspace != null ? ', workspace: $workspace' : ''}'
|
|
282
|
+
'${actor != null ? ', actor: $actor' : ''}${keyHash != null ? ', keyHash: $keyHash' : ''}'
|
|
283
|
+
'${object != null ? ', object: $object' : ''}${relation != null ? ', relation: $relation' : ''}'
|
|
284
|
+
'${cause != null ? ', cause: $cause' : ''}, recommendedAction: $recommendedAction): $message';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// PARENT SIGNER ENROLLMENT MISSING (`nomos.authz.signer_enrollment_missing`) — the device key is valid
|
|
288
|
+
/// and enrolled in the home, but the judging PARENT's chain (e.g. after a custody REBIRTH) no longer
|
|
289
|
+
/// carries the `identity:<actor>#signer@key:<keyHash>` tuple, so a correctly-signed offer (a sibling
|
|
290
|
+
/// birth like `birthEstateWorkspace`) is refused. THE ONE SUPPORTED REMEDY:
|
|
291
|
+
/// `session.ensureParentSignerEnrollment(...)` — [NomosScope.home] runs it automatically on reopen.
|
|
292
|
+
class ParentSignerEnrollmentMissing extends AuthzRefusedError {
|
|
293
|
+
ParentSignerEnrollmentMissing({
|
|
294
|
+
required String message,
|
|
295
|
+
String? workspace,
|
|
296
|
+
String? actor,
|
|
297
|
+
String? keyHash,
|
|
298
|
+
String? object,
|
|
299
|
+
String? directiveId,
|
|
300
|
+
NomosErrorInfo? info,
|
|
301
|
+
}) : super(
|
|
302
|
+
code: 'nomos.authz.signer_enrollment_missing',
|
|
303
|
+
message: message,
|
|
304
|
+
workspace: workspace,
|
|
305
|
+
actor: actor,
|
|
306
|
+
keyHash: keyHash,
|
|
307
|
+
object: object,
|
|
308
|
+
relation: 'signer',
|
|
309
|
+
directiveId: directiveId,
|
|
310
|
+
cause: 'missing-signer-enrollment',
|
|
311
|
+
recommendedAction: NomosRecommendedAction.ensureParentSignerEnrollment,
|
|
312
|
+
info: info,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
178
316
|
/// AUTH — the auth-provider token was missing/malformed/expired/rejected, or its issuer does not match the
|
|
179
317
|
/// workspace's law-declared key (`nomos.auth.*`, e.g. `nomos.auth.issuer_mismatch`, `nomos.auth.token_invalid`,
|
|
180
318
|
/// `nomos.auth.token_expired`). REMEDY is always `reauth`: acquire a fresh token and reconnect.
|
|
@@ -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)}';
|
package/dart_types/pubspec.yaml
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|
package/src/build_package.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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"),
|