@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 +10 -0
- package/dart/lib/src/webview_bridge.dart +15 -2
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/parent_signer_recovery_test.dart +25 -8
- package/dart_types/CHANGELOG.md +5 -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,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.
|
|
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
|
-
|
|
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
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.
|
|
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.
|
|
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:
|
|
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'],
|
|
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:
|
|
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:
|
|
94
|
+
parent: _ws, principal: _principal, publicKey: pub);
|
|
89
95
|
expect(posture0['enrolled'], false);
|
|
90
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -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)}';
|
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.4.
|
|
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.4.
|
|
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.
|
|
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"),
|