@githolon/dsl 0.105.23 → 0.105.24
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 +9 -0
- package/dart/lib/src/runner/node-entry.mjs +4461 -267
- package/dart/lib/src/runner/offline-install.mjs +4337 -115
- package/dart/lib/src/test_harness.dart +316 -235
- package/dart/lib/src/webview_bridge.dart +1478 -1087
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/src/errors.dart +78 -58
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +4235 -102
- package/src/codegen_dart_client.ts +7 -10
- package/src/codegen_ts.ts +3 -6
- package/src/compat_manifest.ts +6 -7
- package/src/compile_package_main.ts +9 -11
- package/src/index.ts +9 -11
- package/src/kernel_capabilities.ts +79 -237
|
@@ -55,8 +55,11 @@ typedef NomosEngine = NomosBridge;
|
|
|
55
55
|
/// One stable bounded page from an authenticated declared-query index. [cursor] is opaque framework
|
|
56
56
|
/// state: pass it back unchanged to request the next page; [done] means this page exhausted the query.
|
|
57
57
|
final class NomosPage<T> {
|
|
58
|
-
const NomosPage(
|
|
59
|
-
|
|
58
|
+
const NomosPage({
|
|
59
|
+
required this.rows,
|
|
60
|
+
required this.cursor,
|
|
61
|
+
required this.done,
|
|
62
|
+
});
|
|
60
63
|
|
|
61
64
|
final List<T> rows;
|
|
62
65
|
final String? cursor;
|
|
@@ -98,57 +101,59 @@ final class NomosBirthCertificate {
|
|
|
98
101
|
final String? authProviderAudience;
|
|
99
102
|
|
|
100
103
|
static String _string(
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
Map<String, Object?> wire,
|
|
105
|
+
String camel,
|
|
106
|
+
String snake,
|
|
107
|
+
) => (wire[camel] ?? wire[snake] ?? '').toString();
|
|
103
108
|
|
|
104
109
|
static List<int> _bytes(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
110
|
+
Map<String, Object?> wire,
|
|
111
|
+
String camel,
|
|
112
|
+
String snake,
|
|
113
|
+
) => ((wire[camel] ?? wire[snake]) as List? ?? const <Object?>[])
|
|
114
|
+
.map((value) => (value as num).toInt())
|
|
115
|
+
.toList(growable: false);
|
|
116
|
+
|
|
117
|
+
factory NomosBirthCertificate.fromWire(
|
|
118
|
+
Map<String, Object?> wire,
|
|
119
|
+
) => NomosBirthCertificate(
|
|
120
|
+
child: _string(wire, 'child', 'child'),
|
|
121
|
+
founder: _string(wire, 'founder', 'founder'),
|
|
122
|
+
verified: wire['verified'] == true,
|
|
123
|
+
parentKey: _bytes(wire, 'parentKey', 'parent_key'),
|
|
124
|
+
parentId: _string(wire, 'parentId', 'parent_id'),
|
|
125
|
+
epoch: (wire['epoch'] as num?)?.toInt() ?? 0,
|
|
126
|
+
signature: _bytes(wire, 'signature', 'signature'),
|
|
127
|
+
verifiedVia: (wire['verifiedVia'] ?? wire['verified_via'])?.toString(),
|
|
128
|
+
idpToken: (wire['idpToken'] ?? wire['idp_token'])?.toString(),
|
|
129
|
+
capturedClock: ((wire['capturedClock'] ?? wire['captured_clock']) as num?)
|
|
130
|
+
?.toInt(),
|
|
131
|
+
authProviderIssuer:
|
|
132
|
+
(wire['authProviderIssuer'] ?? wire['auth_provider_issuer'])
|
|
133
|
+
?.toString(),
|
|
134
|
+
authProviderJwks: (wire['authProviderJwks'] ?? wire['auth_provider_jwks'])
|
|
135
|
+
?.toString(),
|
|
136
|
+
authProviderAudience:
|
|
137
|
+
(wire['authProviderAudience'] ?? wire['auth_provider_audience'])
|
|
138
|
+
?.toString(),
|
|
139
|
+
);
|
|
134
140
|
|
|
135
141
|
Map<String, Object?> toWire() => <String, Object?>{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
};
|
|
142
|
+
'child': child,
|
|
143
|
+
'founder': founder,
|
|
144
|
+
'verified': verified,
|
|
145
|
+
'parentKey': parentKey,
|
|
146
|
+
'parentId': parentId,
|
|
147
|
+
'epoch': epoch,
|
|
148
|
+
'signature': signature,
|
|
149
|
+
if (verifiedVia != null) 'verifiedVia': verifiedVia,
|
|
150
|
+
if (idpToken != null) 'idpToken': idpToken,
|
|
151
|
+
if (capturedClock != null) 'capturedClock': capturedClock,
|
|
152
|
+
if (authProviderIssuer != null) 'authProviderIssuer': authProviderIssuer,
|
|
153
|
+
if (authProviderJwks != null) 'authProviderJwks': authProviderJwks,
|
|
154
|
+
if (authProviderAudience != null)
|
|
155
|
+
'authProviderAudience': authProviderAudience,
|
|
156
|
+
};
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
/// Facts signed by [NomosBridge.signBirthCert]. The secret remains a separate argument and never
|
|
@@ -183,21 +188,20 @@ final class NomosBirthCertificateSpec {
|
|
|
183
188
|
final String? authProviderAudience;
|
|
184
189
|
|
|
185
190
|
Map<String, Object?> toWire() => <String, Object?>{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
};
|
|
191
|
+
'child': child,
|
|
192
|
+
'founder': founder,
|
|
193
|
+
'verified': verified,
|
|
194
|
+
'parentKey': parentKey,
|
|
195
|
+
'parentId': parentId,
|
|
196
|
+
'epoch': epoch,
|
|
197
|
+
if (verifiedVia != null) 'verifiedVia': verifiedVia,
|
|
198
|
+
if (idpToken != null) 'idpToken': idpToken,
|
|
199
|
+
if (capturedClock != null) 'capturedClock': capturedClock,
|
|
200
|
+
if (authProviderIssuer != null) 'authProviderIssuer': authProviderIssuer,
|
|
201
|
+
if (authProviderJwks != null) 'authProviderJwks': authProviderJwks,
|
|
202
|
+
if (authProviderAudience != null)
|
|
203
|
+
'authProviderAudience': authProviderAudience,
|
|
204
|
+
};
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
/// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
|
|
@@ -247,8 +251,11 @@ class TransferProgress {
|
|
|
247
251
|
final int received;
|
|
248
252
|
final int? total;
|
|
249
253
|
final String kind; // 'delta' (incremental) | 'full' (initial clone)
|
|
250
|
-
const TransferProgress(
|
|
251
|
-
|
|
254
|
+
const TransferProgress({
|
|
255
|
+
required this.received,
|
|
256
|
+
this.total,
|
|
257
|
+
this.kind = 'delta',
|
|
258
|
+
});
|
|
252
259
|
double? get fraction => (total == null || total == 0)
|
|
253
260
|
? null
|
|
254
261
|
: (received / total!).clamp(0.0, 1.0);
|
|
@@ -332,18 +339,17 @@ class SyncStatus {
|
|
|
332
339
|
DateTime? lastSyncedAt,
|
|
333
340
|
String? lastError,
|
|
334
341
|
bool clearError = false,
|
|
335
|
-
}) =>
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
);
|
|
342
|
+
}) => SyncStatus(
|
|
343
|
+
phase: phase ?? this.phase,
|
|
344
|
+
ahead: ahead ?? this.ahead,
|
|
345
|
+
behind: behind ?? this.behind,
|
|
346
|
+
saved: saved ?? this.saved,
|
|
347
|
+
onMain: onMain ?? this.onMain,
|
|
348
|
+
deadLetters: deadLetters ?? this.deadLetters,
|
|
349
|
+
transfer: clearTransfer ? null : (transfer ?? this.transfer),
|
|
350
|
+
lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt,
|
|
351
|
+
lastError: clearError ? null : (lastError ?? this.lastError),
|
|
352
|
+
);
|
|
347
353
|
}
|
|
348
354
|
|
|
349
355
|
bool _sameTransferProgress(TransferProgress? left, TransferProgress? right) =>
|
|
@@ -374,13 +380,19 @@ class NomosCreated {
|
|
|
374
380
|
/// The workspace the offer ran on (NomosRef). Additive — defaults to '' on older runners that
|
|
375
381
|
/// don't stamp it; then [ref] is workspace-blank (still a valid locator on the local plane).
|
|
376
382
|
final String workspace;
|
|
377
|
-
const NomosCreated(
|
|
378
|
-
|
|
383
|
+
const NomosCreated({
|
|
384
|
+
required this.type,
|
|
385
|
+
required this.id,
|
|
386
|
+
this.workspace = '',
|
|
387
|
+
});
|
|
379
388
|
|
|
380
389
|
/// The canonical [NomosRef] view — the `{workspace,type,id}` triple, followable/serializable
|
|
381
390
|
/// (`nomos://<workspace>/<type>/<id>`). One reference, everywhere.
|
|
382
391
|
NomosRef<Object?> get ref => NmsInternalNomosRefs.create<Object?>(
|
|
383
|
-
|
|
392
|
+
workspace: workspace,
|
|
393
|
+
type: type,
|
|
394
|
+
id: id,
|
|
395
|
+
);
|
|
384
396
|
@override
|
|
385
397
|
String toString() => id;
|
|
386
398
|
}
|
|
@@ -407,36 +419,40 @@ class NomosBbox {
|
|
|
407
419
|
double? minLat,
|
|
408
420
|
double? maxLng,
|
|
409
421
|
double? maxLat,
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
422
|
+
}) : minX = (minX ?? minLng)!,
|
|
423
|
+
minY = (minY ?? minLat)!,
|
|
424
|
+
maxX = (maxX ?? maxLng)!,
|
|
425
|
+
maxY = (maxY ?? maxLat)!;
|
|
414
426
|
|
|
415
427
|
/// Deprecated geographic accessors — spatial axes are generic. Prefer [minX]/[minY]/[maxX]/[maxY].
|
|
416
428
|
@Deprecated(
|
|
417
|
-
|
|
429
|
+
'Spatial axes are generic; use minX/minY. Kept for source compat.',
|
|
430
|
+
)
|
|
418
431
|
double get minLng => minX;
|
|
419
432
|
@Deprecated(
|
|
420
|
-
|
|
433
|
+
'Spatial axes are generic; use minX/minY. Kept for source compat.',
|
|
434
|
+
)
|
|
421
435
|
double get minLat => minY;
|
|
422
436
|
@Deprecated(
|
|
423
|
-
|
|
437
|
+
'Spatial axes are generic; use maxX/maxY. Kept for source compat.',
|
|
438
|
+
)
|
|
424
439
|
double get maxLng => maxX;
|
|
425
440
|
@Deprecated(
|
|
426
|
-
|
|
441
|
+
'Spatial axes are generic; use maxX/maxY. Kept for source compat.',
|
|
442
|
+
)
|
|
427
443
|
double get maxLat => maxY;
|
|
428
444
|
|
|
429
445
|
Map<String, Object?> toJson() => {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
446
|
+
'minX': minX,
|
|
447
|
+
'minY': minY,
|
|
448
|
+
'maxX': maxX,
|
|
449
|
+
'maxY': maxY,
|
|
450
|
+
// Geographic aliases so a pre-genericisation runner (which reads minLng/…) still works.
|
|
451
|
+
'minLng': minX,
|
|
452
|
+
'minLat': minY,
|
|
453
|
+
'maxLng': maxX,
|
|
454
|
+
'maxLat': maxY,
|
|
455
|
+
};
|
|
440
456
|
}
|
|
441
457
|
|
|
442
458
|
/// One member's structural compat verdict — `ok`, plus the exact break [reason] when not.
|
|
@@ -497,8 +513,9 @@ class NomosCompat {
|
|
|
497
513
|
final install = (m['install'] as Map?)?.cast<String, Object?>() ?? const {};
|
|
498
514
|
return NomosCompat(
|
|
499
515
|
generatedFor: (m['generatedFor'] as String?) ?? '',
|
|
500
|
-
workspaceRuns:
|
|
501
|
-
|
|
516
|
+
workspaceRuns: ((m['workspaceRuns'] as List?) ?? const [])
|
|
517
|
+
.map((e) => '$e')
|
|
518
|
+
.toList(),
|
|
502
519
|
matches: m['matches'] == true,
|
|
503
520
|
offerKnown: m['offerKnown'] == true,
|
|
504
521
|
structurallyCompatible: m['structurallyCompatible'] as bool?,
|
|
@@ -528,21 +545,22 @@ class NomosScopeKey {
|
|
|
528
545
|
|
|
529
546
|
/// Framework-private deterministic child address, present only for an unborn owned business aggregate.
|
|
530
547
|
final String? workspace;
|
|
531
|
-
const NomosScopeKey(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
548
|
+
const NomosScopeKey({
|
|
549
|
+
required this.scope,
|
|
550
|
+
required this.epoch,
|
|
551
|
+
required this.key,
|
|
552
|
+
this.ownedKeySubject,
|
|
553
|
+
this.ownedCustody,
|
|
554
|
+
this.workspace,
|
|
555
|
+
});
|
|
538
556
|
Map<String, Object?> toJson() => {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
557
|
+
'scope': scope,
|
|
558
|
+
'epoch': epoch,
|
|
559
|
+
'key': key,
|
|
560
|
+
if (ownedKeySubject != null) 'ownedKeySubject': ownedKeySubject,
|
|
561
|
+
if (ownedCustody != null) 'ownedCustody': ownedCustody,
|
|
562
|
+
if (workspace != null) 'workspace': workspace,
|
|
563
|
+
};
|
|
546
564
|
}
|
|
547
565
|
|
|
548
566
|
/// The entire app-facing multi-device surface for owned business state. The request is deliberately an
|
|
@@ -569,8 +587,10 @@ class NomosDevices {
|
|
|
569
587
|
if (principal == null || principal.trim().isEmpty) {
|
|
570
588
|
throw StateError('devices.approve() needs a signed-in Nomos session');
|
|
571
589
|
}
|
|
572
|
-
final payload =
|
|
573
|
-
|
|
590
|
+
final payload = await bridge.nomosPrepareOwnedDeviceApproval(
|
|
591
|
+
request,
|
|
592
|
+
scope,
|
|
593
|
+
);
|
|
574
594
|
return bridge.offerOutcome(
|
|
575
595
|
domain: domain,
|
|
576
596
|
directiveId: 'nomos_internal_approve_owned_device_7c1f5e',
|
|
@@ -600,7 +620,8 @@ class NomosRecovery {
|
|
|
600
620
|
Future<Map<String, Object?>> protectWith(String person) {
|
|
601
621
|
if (person.trim().isEmpty) {
|
|
602
622
|
throw ArgumentError(
|
|
603
|
-
|
|
623
|
+
'recovery.protectWith() needs the person who will recover you',
|
|
624
|
+
);
|
|
604
625
|
}
|
|
605
626
|
return bridge.nomosProtectOwnedIdentity(
|
|
606
627
|
scope: scope,
|
|
@@ -644,25 +665,33 @@ class WrappedKeyStore implements NomosKeyStore {
|
|
|
644
665
|
final String principal;
|
|
645
666
|
final String deviceSecret;
|
|
646
667
|
final Map<String, Future<List<NomosScopeKey>>> _cache = {};
|
|
647
|
-
WrappedKeyStore(
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
668
|
+
WrappedKeyStore({
|
|
669
|
+
required this.bridge,
|
|
670
|
+
required this.principal,
|
|
671
|
+
required this.deviceSecret,
|
|
672
|
+
});
|
|
651
673
|
|
|
652
674
|
Future<List<NomosScopeKey>> _loadScope(String scope) {
|
|
653
675
|
return _cache.putIfAbsent(scope, () async {
|
|
654
|
-
final rows = await bridge
|
|
655
|
-
|
|
676
|
+
final rows = await bridge.query('wrappedKeysByRecipient', {
|
|
677
|
+
'recipientPrincipal': principal,
|
|
678
|
+
});
|
|
656
679
|
final held = <NomosScopeKey>[];
|
|
657
680
|
for (final row in rows) {
|
|
658
681
|
final d = ((row['data'] ?? row) as Map);
|
|
659
682
|
if (d['scope']?.toString() != scope) continue;
|
|
660
683
|
final key = await bridge.cryptoUnwrapKey(
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
684
|
+
secret: deviceSecret,
|
|
685
|
+
hpkeEpk: d['hpkeEpk'].toString(),
|
|
686
|
+
ct: d['ct'].toString(),
|
|
687
|
+
);
|
|
688
|
+
held.add(
|
|
689
|
+
NomosScopeKey(
|
|
690
|
+
scope: scope,
|
|
691
|
+
epoch: (d['epoch'] as num?)?.toInt() ?? 0,
|
|
692
|
+
key: key,
|
|
693
|
+
),
|
|
694
|
+
);
|
|
666
695
|
}
|
|
667
696
|
held.sort((a, b) => b.epoch - a.epoch);
|
|
668
697
|
return held;
|
|
@@ -749,16 +778,19 @@ class BirthIncomplete implements Exception {
|
|
|
749
778
|
@override
|
|
750
779
|
String toString() {
|
|
751
780
|
final list = failedBirths
|
|
752
|
-
.map(
|
|
753
|
-
|
|
754
|
-
|
|
781
|
+
.map(
|
|
782
|
+
(o) =>
|
|
783
|
+
'${o['workspace'] ?? '?'} [${o['status']}${o['code'] != null ? ': ${o['code']}' : ''}]'
|
|
784
|
+
'${o['reason'] != null ? ' — ${o['reason']}' : ''}',
|
|
785
|
+
)
|
|
755
786
|
.join('; ');
|
|
756
|
-
final lawUnavailable =
|
|
757
|
-
|
|
787
|
+
final lawUnavailable = failedBirths.any(
|
|
788
|
+
(o) => o['code'] == 'BirthLawPackageUnavailable',
|
|
789
|
+
);
|
|
758
790
|
return 'BirthIncomplete: ${failedBirths.length} child workspace(s) did NOT come up — $list. '
|
|
759
791
|
'The parent offer admitted, but the child was not fully born, so opening it would poll head:null '
|
|
760
792
|
'until timeout. ${lawUnavailable ? "The child's law package is not resolvable from the parent's "
|
|
761
|
-
|
|
793
|
+
"custody — deploy it to the parent (POST /v2/workspaces/<parent>/domains) BEFORE birthing." : "See the outcome's reason/code."}';
|
|
762
794
|
}
|
|
763
795
|
}
|
|
764
796
|
|
|
@@ -768,8 +800,11 @@ class NomosEpochResult {
|
|
|
768
800
|
final String head;
|
|
769
801
|
final int epoch;
|
|
770
802
|
final List<String> roster;
|
|
771
|
-
const NomosEpochResult(
|
|
772
|
-
|
|
803
|
+
const NomosEpochResult({
|
|
804
|
+
required this.head,
|
|
805
|
+
required this.epoch,
|
|
806
|
+
required this.roster,
|
|
807
|
+
});
|
|
773
808
|
}
|
|
774
809
|
|
|
775
810
|
/// THE OWNER CEREMONY surface — `<Domain>Client(bridge, actor: owner).encryption`. All three verbs open a
|
|
@@ -784,18 +819,20 @@ class NomosEncryption {
|
|
|
784
819
|
/// The encryption scopes this domain defines (each a `requires(cap)`).
|
|
785
820
|
final List<String> scopes;
|
|
786
821
|
|
|
787
|
-
NomosEncryption(
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
822
|
+
NomosEncryption({
|
|
823
|
+
required this.bridge,
|
|
824
|
+
required this.domain,
|
|
825
|
+
this.domainHash,
|
|
826
|
+
this.actor,
|
|
827
|
+
required this.scopes,
|
|
828
|
+
});
|
|
793
829
|
|
|
794
830
|
String _defScope([String? scope]) {
|
|
795
831
|
if (scope != null && scope.isNotEmpty) return scope;
|
|
796
832
|
if (scopes.length == 1) return scopes.first;
|
|
797
833
|
throw StateError(
|
|
798
|
-
|
|
834
|
+
'this domain defines ${scopes.length} encryption scopes $scopes — pass a scope to pick one',
|
|
835
|
+
);
|
|
799
836
|
}
|
|
800
837
|
|
|
801
838
|
// A BARE scope IS `workspace:self#<relation>` (the era rule); a per-record scope is `<type>:<id>#<relation>`.
|
|
@@ -815,13 +852,14 @@ class NomosEncryption {
|
|
|
815
852
|
final subjects = rows.isEmpty
|
|
816
853
|
? const <String, dynamic>{}
|
|
817
854
|
: ((rows.first['data'] as Map?)?['subjects'] as Map?) ??
|
|
818
|
-
|
|
855
|
+
const <String, dynamic>{};
|
|
819
856
|
final out = <String>[];
|
|
820
857
|
subjects.forEach((subj, leafRaw) {
|
|
821
858
|
Map? leaf;
|
|
822
859
|
try {
|
|
823
|
-
leaf =
|
|
824
|
-
|
|
860
|
+
leaf = leafRaw is String
|
|
861
|
+
? jsonDecode(leafRaw) as Map
|
|
862
|
+
: (leafRaw as Map);
|
|
825
863
|
} catch (_) {
|
|
826
864
|
leaf = null;
|
|
827
865
|
}
|
|
@@ -844,31 +882,38 @@ class NomosEncryption {
|
|
|
844
882
|
Future<String> _freshScopeKey() async {
|
|
845
883
|
final kp = await bridge.cryptoKeygen();
|
|
846
884
|
final secret = base64Decode((kp['secret'] ?? '').toString());
|
|
847
|
-
return base64Encode(
|
|
848
|
-
|
|
885
|
+
return base64Encode(
|
|
886
|
+
secret.sublist(0, 32),
|
|
887
|
+
); // the 32-byte epoch secret; NEVER lands on chain
|
|
849
888
|
}
|
|
850
889
|
|
|
851
|
-
Future<String?> _memberPubKey(
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
890
|
+
Future<String?> _memberPubKey(
|
|
891
|
+
String principal, {
|
|
892
|
+
String? newcomer,
|
|
893
|
+
String? newcomerPub,
|
|
894
|
+
Map<String, String>? memberKeys,
|
|
895
|
+
}) async {
|
|
855
896
|
if (memberKeys != null && memberKeys[principal] != null) {
|
|
856
897
|
return memberKeys[principal];
|
|
857
898
|
}
|
|
858
899
|
if (newcomer == principal && newcomerPub != null) return newcomerPub;
|
|
859
900
|
// Published enc key (encKeyByPrincipal) — present only when the domain composed the registry; tolerate absence.
|
|
860
901
|
try {
|
|
861
|
-
final reg =
|
|
862
|
-
|
|
902
|
+
final reg = await bridge.query('encKeyByPrincipal', {
|
|
903
|
+
'principal': principal,
|
|
904
|
+
});
|
|
863
905
|
if (reg.isNotEmpty) {
|
|
864
906
|
final pub = (reg.first['data'] as Map?)?['encPublicKey'];
|
|
865
907
|
if (pub != null && pub.toString().isNotEmpty) return pub.toString();
|
|
866
908
|
}
|
|
867
|
-
} catch (_) {
|
|
909
|
+
} catch (_) {
|
|
910
|
+
/* not routed — fall through to the prior-wrap lane */
|
|
911
|
+
}
|
|
868
912
|
// The member's most-recent WrappedKey pins the pubkey it was last sealed to (rotation needs no re-publish).
|
|
869
913
|
try {
|
|
870
|
-
final wraps = await bridge
|
|
871
|
-
|
|
914
|
+
final wraps = await bridge.query('wrappedKeysByRecipient', {
|
|
915
|
+
'recipientPrincipal': principal,
|
|
916
|
+
});
|
|
872
917
|
int bestEpoch = -1;
|
|
873
918
|
String? bestPub;
|
|
874
919
|
for (final w in wraps) {
|
|
@@ -881,7 +926,9 @@ class NomosEncryption {
|
|
|
881
926
|
}
|
|
882
927
|
}
|
|
883
928
|
if (bestPub != null) return bestPub;
|
|
884
|
-
} catch (_) {
|
|
929
|
+
} catch (_) {
|
|
930
|
+
/* not routed */
|
|
931
|
+
}
|
|
885
932
|
return null;
|
|
886
933
|
}
|
|
887
934
|
|
|
@@ -911,10 +958,12 @@ class NomosEncryption {
|
|
|
911
958
|
final key = scopeKey ?? await _freshScopeKey();
|
|
912
959
|
final wraps = <Map<String, Object?>>[];
|
|
913
960
|
for (final principal in members) {
|
|
914
|
-
final pub = await _memberPubKey(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
961
|
+
final pub = await _memberPubKey(
|
|
962
|
+
principal,
|
|
963
|
+
newcomer: addPrincipal,
|
|
964
|
+
newcomerPub: addPublicKey,
|
|
965
|
+
memberKeys: memberKeys,
|
|
966
|
+
);
|
|
918
967
|
if (pub == null) throw EncryptionMemberKeyMissing(principal, scope);
|
|
919
968
|
final w = await bridge.cryptoWrapKey(recipientPub: pub, scopeKey: key);
|
|
920
969
|
wraps.add({
|
|
@@ -937,8 +986,7 @@ class NomosEncryption {
|
|
|
937
986
|
},
|
|
938
987
|
actor: actor,
|
|
939
988
|
domainHash: domainHash,
|
|
940
|
-
))
|
|
941
|
-
.head;
|
|
989
|
+
)).head;
|
|
942
990
|
return NomosEpochResult(head: head, epoch: epoch, roster: members);
|
|
943
991
|
}
|
|
944
992
|
|
|
@@ -958,18 +1006,21 @@ class NomosEncryption {
|
|
|
958
1006
|
}) async {
|
|
959
1007
|
final s = _defScope(scope);
|
|
960
1008
|
await bridge.offerOutcome(
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1009
|
+
domain: domain,
|
|
1010
|
+
directiveId: grantDirective,
|
|
1011
|
+
payload: grantPayload,
|
|
1012
|
+
actor: actor,
|
|
1013
|
+
domainHash: domainHash,
|
|
1014
|
+
);
|
|
1015
|
+
return _advance(
|
|
1016
|
+
s,
|
|
1017
|
+
addPrincipal: principal,
|
|
1018
|
+
addPublicKey: publicKey,
|
|
1019
|
+
scopeKey: scopeKey,
|
|
1020
|
+
memberKeys: memberKeys,
|
|
1021
|
+
expectRoster: expectRoster,
|
|
1022
|
+
openedAt: openedAt,
|
|
1023
|
+
);
|
|
973
1024
|
}
|
|
974
1025
|
|
|
975
1026
|
/// Revoke a member + open a fresh epoch re-wrapping the key to the SURVIVORS only. FORWARD-ONLY: the removed
|
|
@@ -986,17 +1037,20 @@ class NomosEncryption {
|
|
|
986
1037
|
}) async {
|
|
987
1038
|
final s = _defScope(scope);
|
|
988
1039
|
await bridge.offerOutcome(
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1040
|
+
domain: domain,
|
|
1041
|
+
directiveId: revokeDirective,
|
|
1042
|
+
payload: revokePayload,
|
|
1043
|
+
actor: actor,
|
|
1044
|
+
domainHash: domainHash,
|
|
1045
|
+
);
|
|
1046
|
+
return _advance(
|
|
1047
|
+
s,
|
|
1048
|
+
remove: [principal],
|
|
1049
|
+
scopeKey: scopeKey,
|
|
1050
|
+
memberKeys: memberKeys,
|
|
1051
|
+
expectRoster: expectRoster,
|
|
1052
|
+
openedAt: openedAt,
|
|
1053
|
+
);
|
|
1000
1054
|
}
|
|
1001
1055
|
|
|
1002
1056
|
/// Routine rotation — open a fresh epoch re-wrapping the key to the CURRENT roster (no membership change).
|
|
@@ -1006,12 +1060,13 @@ class NomosEncryption {
|
|
|
1006
1060
|
Map<String, String>? memberKeys,
|
|
1007
1061
|
List<String>? expectRoster,
|
|
1008
1062
|
String? openedAt,
|
|
1009
|
-
}) =>
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1063
|
+
}) => _advance(
|
|
1064
|
+
_defScope(scope),
|
|
1065
|
+
scopeKey: scopeKey,
|
|
1066
|
+
memberKeys: memberKeys,
|
|
1067
|
+
expectRoster: expectRoster,
|
|
1068
|
+
openedAt: openedAt,
|
|
1069
|
+
);
|
|
1015
1070
|
}
|
|
1016
1071
|
|
|
1017
1072
|
/// The result of [NomosBridge.offerCreates]: the new local [head], every aggregate the plan minted, and —
|
|
@@ -1036,9 +1091,8 @@ final class NomosOperationStatus {
|
|
|
1036
1091
|
/// adopted that shared position. Callers never inspect workspaces, URLs, credentials, headers or Git heads.
|
|
1037
1092
|
final class NomosOperationReceipt {
|
|
1038
1093
|
NomosOperationReceipt._()
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
_changes = StreamController<NomosOperationStatus>.broadcast();
|
|
1094
|
+
: _status = const NomosOperationStatus(NomosOperationPhase.locallyAccepted),
|
|
1095
|
+
_changes = StreamController<NomosOperationStatus>.broadcast();
|
|
1042
1096
|
|
|
1043
1097
|
NomosOperationStatus _status;
|
|
1044
1098
|
final StreamController<NomosOperationStatus> _changes;
|
|
@@ -1099,7 +1153,8 @@ class NomosOfferOutcome {
|
|
|
1099
1153
|
NomosOperationReceipt get receipt =>
|
|
1100
1154
|
_receipt ??
|
|
1101
1155
|
(throw StateError(
|
|
1102
|
-
|
|
1156
|
+
'This legacy offer outcome has no operation receipt; regenerate the application client.',
|
|
1157
|
+
));
|
|
1103
1158
|
|
|
1104
1159
|
const NomosOfferOutcome({
|
|
1105
1160
|
required this.head,
|
|
@@ -1123,8 +1178,13 @@ class NomosOfferOutcome {
|
|
|
1123
1178
|
/// (`final estate = await engine.follow(outcome.bornRefs.single);`) so app code never carries a
|
|
1124
1179
|
/// loose workspace string. The ref's type is the reserved `nomos.Workspace` locator tag.
|
|
1125
1180
|
List<NomosRef<Object?>> get bornRefs => born
|
|
1126
|
-
.map(
|
|
1127
|
-
|
|
1181
|
+
.map(
|
|
1182
|
+
(w) => NmsInternalNomosRefs.create<Object?>(
|
|
1183
|
+
workspace: w,
|
|
1184
|
+
type: 'nomos.Workspace',
|
|
1185
|
+
id: w,
|
|
1186
|
+
),
|
|
1187
|
+
)
|
|
1128
1188
|
.toList();
|
|
1129
1189
|
}
|
|
1130
1190
|
|
|
@@ -1141,11 +1201,16 @@ extension NomosRefAcquire<TAggregate> on NomosRef<TAggregate> {
|
|
|
1141
1201
|
}) {
|
|
1142
1202
|
final workspace = NmsInternalNomosRefs.workspace(this);
|
|
1143
1203
|
if (workspace.isEmpty) {
|
|
1144
|
-
throw ArgumentError.value(
|
|
1145
|
-
|
|
1204
|
+
throw ArgumentError.value(
|
|
1205
|
+
this,
|
|
1206
|
+
'ref',
|
|
1207
|
+
'NomosRef.acquire: the ref carries no workspace — only a workspace-complete ref is openable',
|
|
1208
|
+
);
|
|
1146
1209
|
}
|
|
1147
|
-
return bridge.acquireSession(
|
|
1148
|
-
|
|
1210
|
+
return bridge.acquireSession(
|
|
1211
|
+
workspace,
|
|
1212
|
+
ifAbsentCredentials: ifAbsentCredentials,
|
|
1213
|
+
);
|
|
1149
1214
|
}
|
|
1150
1215
|
}
|
|
1151
1216
|
|
|
@@ -1190,16 +1255,14 @@ final class _NomosRetainedCloudAccess {
|
|
|
1190
1255
|
final String cloud;
|
|
1191
1256
|
final Future<String?> Function() signIn;
|
|
1192
1257
|
|
|
1193
|
-
const _NomosRetainedCloudAccess({
|
|
1194
|
-
required this.cloud,
|
|
1195
|
-
required this.signIn,
|
|
1196
|
-
});
|
|
1258
|
+
const _NomosRetainedCloudAccess({required this.cloud, required this.signIn});
|
|
1197
1259
|
|
|
1198
1260
|
Future<NomosSessionCredentials> credentials() async {
|
|
1199
1261
|
final token = await signIn();
|
|
1200
1262
|
if (token == null || token.trim().isEmpty) {
|
|
1201
1263
|
throw StateError(
|
|
1202
|
-
|
|
1264
|
+
'Nomos could not refresh the signed-in identity while opening a referenced business workspace',
|
|
1265
|
+
);
|
|
1203
1266
|
}
|
|
1204
1267
|
return NomosSessionCredentials(cloud: cloud, authToken: token);
|
|
1205
1268
|
}
|
|
@@ -1247,9 +1310,9 @@ class NomosBridge {
|
|
|
1247
1310
|
// The DERIVED app-ready sync state. seeded-on-listen with a metrics poll; updated on sync/signal/transfer.
|
|
1248
1311
|
late final StreamController<SyncStatus> _syncStatusCtrl =
|
|
1249
1312
|
StreamController<SyncStatus>.broadcast(
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1313
|
+
onListen: _startStatusPoll,
|
|
1314
|
+
onCancel: _stopStatusPollIfUnused,
|
|
1315
|
+
);
|
|
1253
1316
|
SyncStatus _status = const SyncStatus();
|
|
1254
1317
|
final Map<String, SyncStatus> _statusBySession = {};
|
|
1255
1318
|
final Map<String, StreamController<SyncStatus>> _syncStatusBySession = {};
|
|
@@ -1286,7 +1349,7 @@ class NomosBridge {
|
|
|
1286
1349
|
// child sessions. The callback receives only the typed review object; child
|
|
1287
1350
|
// addresses, archives, credentials and lifecycle directives remain private.
|
|
1288
1351
|
final Map<String, Future<void> Function(DomainClientModelAdoptionRequired)>
|
|
1289
|
-
|
|
1352
|
+
_sessionModelChangeHandlers = {};
|
|
1290
1353
|
// [acquireSession]'s per-workspace single-flight: workspace → the in-flight open every concurrent
|
|
1291
1354
|
// acquirer joins (removed when it completes; a live session then hits the fast path instead).
|
|
1292
1355
|
final Map<String, Future<NomosSession>> _acquiring = {};
|
|
@@ -1414,9 +1477,11 @@ class NomosBridge {
|
|
|
1414
1477
|
final ctrl = _watches[m['subId']];
|
|
1415
1478
|
if (ctrl != null && !ctrl.isClosed) {
|
|
1416
1479
|
if (m['ok'] == false) {
|
|
1417
|
-
final info = NomosErrorInfo.fromBridgeMessage(
|
|
1418
|
-
|
|
1419
|
-
|
|
1480
|
+
final info = NomosErrorInfo.fromBridgeMessage(
|
|
1481
|
+
m,
|
|
1482
|
+
fallbackMessage: 'the watch read failed',
|
|
1483
|
+
fallbackOperation: 'watch',
|
|
1484
|
+
);
|
|
1420
1485
|
ctrl.addError(nomosErrorFromInfo(info));
|
|
1421
1486
|
} else {
|
|
1422
1487
|
ctrl.add((m['rows'] as List?) ?? const []);
|
|
@@ -1427,9 +1492,11 @@ class NomosBridge {
|
|
|
1427
1492
|
final cctrl = _compatWatches[m['subId']];
|
|
1428
1493
|
if (cctrl != null && !cctrl.isClosed) {
|
|
1429
1494
|
if (m['ok'] == false) {
|
|
1430
|
-
final info = NomosErrorInfo.fromBridgeMessage(
|
|
1431
|
-
|
|
1432
|
-
|
|
1495
|
+
final info = NomosErrorInfo.fromBridgeMessage(
|
|
1496
|
+
m,
|
|
1497
|
+
fallbackMessage: 'the compat watch failed',
|
|
1498
|
+
fallbackOperation: 'watchCompatibility',
|
|
1499
|
+
);
|
|
1433
1500
|
cctrl.addError(nomosErrorFromInfo(info));
|
|
1434
1501
|
} else {
|
|
1435
1502
|
cctrl.add(((m['rows'] as Map?)?.cast<String, Object?>()) ?? const {});
|
|
@@ -1440,8 +1507,9 @@ class NomosBridge {
|
|
|
1440
1507
|
if (event == 'lawChanged') {
|
|
1441
1508
|
// COMPAT SUPERPOWER (#72b): a synced install/upgrade/retire moved the workspace's offer under this
|
|
1442
1509
|
// live client. Cache the fresh verdict + emit it so the app re-checks feature flags without polling.
|
|
1443
|
-
final c =
|
|
1444
|
-
|
|
1510
|
+
final c = NomosCompat.fromJson(
|
|
1511
|
+
(m['compat'] as Map?)?.cast<String, Object?>(),
|
|
1512
|
+
);
|
|
1445
1513
|
if (c != null) {
|
|
1446
1514
|
_lastCompat = c;
|
|
1447
1515
|
final sess = m['session'] as String?;
|
|
@@ -1468,21 +1536,24 @@ class NomosBridge {
|
|
|
1468
1536
|
// telemetry; the app-ready status needs only one legible error and an honest error phase.
|
|
1469
1537
|
final detail = m['error']?.toString() ?? 'automatic sync failed';
|
|
1470
1538
|
_updateScopedStatus(
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1539
|
+
session,
|
|
1540
|
+
(current) => current.copyWith(
|
|
1541
|
+
phase: SyncPhase.error,
|
|
1542
|
+
clearTransfer: true,
|
|
1543
|
+
lastError: detail,
|
|
1544
|
+
),
|
|
1545
|
+
);
|
|
1476
1546
|
} else if (result?['offline'] == true) {
|
|
1477
1547
|
// Offline is expected transport debt. The durable local branch remains ahead and autoSync owns the
|
|
1478
1548
|
// retry; do not claim a completed sync or overwrite the previous successful timestamp.
|
|
1479
1549
|
_updateScopedStatus(
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1550
|
+
session,
|
|
1551
|
+
(current) => current.copyWith(
|
|
1552
|
+
phase: SyncPhase.offline,
|
|
1553
|
+
clearTransfer: true,
|
|
1554
|
+
lastError: (result?['pushError'] as Map?)?['error']?.toString(),
|
|
1555
|
+
),
|
|
1556
|
+
);
|
|
1486
1557
|
} else if (result?['pushFailed'] == true) {
|
|
1487
1558
|
// The sync round itself completed, but shared custody refused the
|
|
1488
1559
|
// push. `stillAhead` is durable debt, not success: surface the
|
|
@@ -1491,24 +1562,30 @@ class NomosBridge {
|
|
|
1491
1562
|
// out behind a generic saving state.
|
|
1492
1563
|
final detail = _syncPushFailureDetail(result!);
|
|
1493
1564
|
_updateScopedStatus(
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1565
|
+
session,
|
|
1566
|
+
(current) => current.copyWith(
|
|
1567
|
+
phase: SyncPhase.error,
|
|
1568
|
+
clearTransfer: true,
|
|
1569
|
+
lastError: detail,
|
|
1570
|
+
),
|
|
1571
|
+
);
|
|
1499
1572
|
} else {
|
|
1500
1573
|
// A completed round → idle (the next metrics poll folds in residual ahead/behind), stamp the time.
|
|
1501
1574
|
_updateScopedStatus(
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1575
|
+
session,
|
|
1576
|
+
(current) => current.copyWith(
|
|
1577
|
+
phase: SyncPhase.idle,
|
|
1578
|
+
clearTransfer: true,
|
|
1579
|
+
clearError: true,
|
|
1580
|
+
lastSyncedAt: DateTime.now(),
|
|
1581
|
+
),
|
|
1582
|
+
);
|
|
1508
1583
|
}
|
|
1509
1584
|
} else if (m['status'] == 'closed' || m['status'] == 'offline') {
|
|
1510
|
-
_updateScopedStatus(
|
|
1511
|
-
|
|
1585
|
+
_updateScopedStatus(
|
|
1586
|
+
m['session']?.toString(),
|
|
1587
|
+
(current) => current.copyWith(phase: SyncPhase.offline),
|
|
1588
|
+
);
|
|
1512
1589
|
}
|
|
1513
1590
|
return;
|
|
1514
1591
|
}
|
|
@@ -1535,11 +1612,13 @@ class NomosBridge {
|
|
|
1535
1612
|
final tp = TransferProgress.fromMessage(m);
|
|
1536
1613
|
if (!_transferEvents.isClosed) _transferEvents.add(tp);
|
|
1537
1614
|
_updateScopedStatus(
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1615
|
+
m['session']?.toString(),
|
|
1616
|
+
(current) => current.copyWith(
|
|
1617
|
+
phase: tp.done ? SyncPhase.syncing : SyncPhase.transferring,
|
|
1618
|
+
transfer: tp,
|
|
1619
|
+
clearTransfer: tp.done,
|
|
1620
|
+
),
|
|
1621
|
+
);
|
|
1543
1622
|
return;
|
|
1544
1623
|
}
|
|
1545
1624
|
if (event == 'trace') {
|
|
@@ -1552,10 +1631,11 @@ class NomosBridge {
|
|
|
1552
1631
|
if (!_signalEvents.isClosed) _signalEvents.add(m);
|
|
1553
1632
|
// the doorbell says canonical main advanced → we're behind until the next pull lands.
|
|
1554
1633
|
_updateScopedStatus(
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1634
|
+
m['session']?.toString(),
|
|
1635
|
+
(current) => current.phase == SyncPhase.idle
|
|
1636
|
+
? current.copyWith(phase: SyncPhase.behind)
|
|
1637
|
+
: current,
|
|
1638
|
+
);
|
|
1559
1639
|
return;
|
|
1560
1640
|
}
|
|
1561
1641
|
if (event == 'span') {
|
|
@@ -1584,14 +1664,20 @@ class NomosBridge {
|
|
|
1584
1664
|
.map((e) => e.cast<String, Object?>())
|
|
1585
1665
|
.toList();
|
|
1586
1666
|
if (failedBirths != null && failedBirths.isNotEmpty) {
|
|
1587
|
-
completer.completeError(
|
|
1588
|
-
|
|
1667
|
+
completer.completeError(
|
|
1668
|
+
BirthIncomplete(
|
|
1669
|
+
failedBirths,
|
|
1670
|
+
head: ((m['errorInfo'] as Map?)?['head'] ?? '').toString(),
|
|
1671
|
+
),
|
|
1672
|
+
);
|
|
1589
1673
|
return;
|
|
1590
1674
|
}
|
|
1591
|
-
final info = NomosErrorInfo.fromBridgeMessage(
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1675
|
+
final info = NomosErrorInfo.fromBridgeMessage(
|
|
1676
|
+
m,
|
|
1677
|
+
fallbackOperation: op,
|
|
1678
|
+
fallbackTrace: trace,
|
|
1679
|
+
fallbackMessage: 'the githolon refused the request',
|
|
1680
|
+
);
|
|
1595
1681
|
completer.completeError(nomosErrorFromInfo(info));
|
|
1596
1682
|
}
|
|
1597
1683
|
}
|
|
@@ -1647,7 +1733,8 @@ class NomosBridge {
|
|
|
1647
1733
|
final parts = token.split('.');
|
|
1648
1734
|
if (parts.length < 2) return null;
|
|
1649
1735
|
final payload = jsonDecode(
|
|
1650
|
-
|
|
1736
|
+
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
|
|
1737
|
+
);
|
|
1651
1738
|
final sub = payload is Map ? payload['sub'] : null;
|
|
1652
1739
|
if (sub is! String || sub.trim().isEmpty) return null;
|
|
1653
1740
|
final value = sub.trim();
|
|
@@ -1658,11 +1745,14 @@ class NomosBridge {
|
|
|
1658
1745
|
}
|
|
1659
1746
|
|
|
1660
1747
|
NomosOperationReceipt _registerOperationReceipt(
|
|
1661
|
-
|
|
1748
|
+
String workspace,
|
|
1749
|
+
String intentId,
|
|
1750
|
+
) {
|
|
1662
1751
|
if (intentId.isEmpty) {
|
|
1663
1752
|
throw StateError(
|
|
1664
|
-
|
|
1665
|
-
|
|
1753
|
+
'Nomos accepted a local write without returning its stable intent identity. '
|
|
1754
|
+
'The runtime and kernel release are incompatible.',
|
|
1755
|
+
);
|
|
1666
1756
|
}
|
|
1667
1757
|
final plane = _eventPlane;
|
|
1668
1758
|
// A locally accepted write is not current until custody convergence
|
|
@@ -1684,7 +1774,9 @@ class NomosBridge {
|
|
|
1684
1774
|
}
|
|
1685
1775
|
|
|
1686
1776
|
void _advanceOperationReceipts(
|
|
1687
|
-
|
|
1777
|
+
String workspace,
|
|
1778
|
+
Map<String, Object?> result,
|
|
1779
|
+
) {
|
|
1688
1780
|
final pending = _eventPlane._operationReceipts[workspace];
|
|
1689
1781
|
if (pending == null || pending.isEmpty) return;
|
|
1690
1782
|
|
|
@@ -1703,12 +1795,14 @@ class NomosBridge {
|
|
|
1703
1795
|
}
|
|
1704
1796
|
|
|
1705
1797
|
bool _syncNeedsCredentialRefresh(
|
|
1706
|
-
|
|
1798
|
+
Map<String, Object?> message,
|
|
1799
|
+
Map<String, Object?> result,
|
|
1800
|
+
) {
|
|
1707
1801
|
final push = (result['pushError'] as Map?)?.cast<String, Object?>();
|
|
1708
1802
|
final status = push?['status'];
|
|
1709
1803
|
if (status == 401 || status == 403) return true;
|
|
1710
|
-
final text =
|
|
1711
|
-
|
|
1804
|
+
final text = '${message['error'] ?? ''} ${push?['error'] ?? ''}'
|
|
1805
|
+
.toLowerCase();
|
|
1712
1806
|
return text.contains('unauthenticated') ||
|
|
1713
1807
|
text.contains('token expired') ||
|
|
1714
1808
|
text.contains('invalid token');
|
|
@@ -1735,7 +1829,8 @@ class NomosBridge {
|
|
|
1735
1829
|
final token = (await retained.signIn())?.trim() ?? '';
|
|
1736
1830
|
if (token.isEmpty) {
|
|
1737
1831
|
throw StateError(
|
|
1738
|
-
|
|
1832
|
+
'Nomos could not refresh the signed-in identity for background sync.',
|
|
1833
|
+
);
|
|
1739
1834
|
}
|
|
1740
1835
|
await plane.session(workspace)._call('refreshCredentials', {
|
|
1741
1836
|
'authToken': token,
|
|
@@ -1804,24 +1899,27 @@ class NomosBridge {
|
|
|
1804
1899
|
const [])
|
|
1805
1900
|
.map((e) => (e as Map).cast<String, Object?>())
|
|
1806
1901
|
.map((w) {
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1902
|
+
final ws = w['workspace']?.toString() ?? '';
|
|
1903
|
+
// `connected` is the app's own connect() truth; `parked`/resident come from the runner realm.
|
|
1904
|
+
w['state'] = (w['parked'] == true)
|
|
1905
|
+
? 'parked'
|
|
1906
|
+
: (connected.contains(ws) ? 'connected' : 'resident');
|
|
1907
|
+
return w;
|
|
1908
|
+
})
|
|
1909
|
+
.toList();
|
|
1814
1910
|
}
|
|
1815
1911
|
|
|
1816
1912
|
/// Framework-private plane persistence primitive. It snapshots exact local custody even when [workspace]
|
|
1817
1913
|
/// was born in the kernel but no Dart runner session has ever acquired it. This does not connect, mount a
|
|
1818
1914
|
/// tenant session, fetch from cloud, or grant authority; the serving realm exports its resident plane view.
|
|
1819
1915
|
Future<({String workspace, String head, String? snapshot, bool unchanged})>
|
|
1820
|
-
|
|
1821
|
-
final r = _asMap(
|
|
1822
|
-
'
|
|
1823
|
-
|
|
1824
|
-
|
|
1916
|
+
nomosInternalSnapshotWorkspace(String workspace, {String? ifHead}) async {
|
|
1917
|
+
final r = _asMap(
|
|
1918
|
+
await _call('snapshotWorkspace', {
|
|
1919
|
+
'workspace': workspace,
|
|
1920
|
+
if (ifHead != null && ifHead.isNotEmpty) 'ifHead': ifHead,
|
|
1921
|
+
}),
|
|
1922
|
+
);
|
|
1825
1923
|
return (
|
|
1826
1924
|
workspace: (r['workspace'] ?? workspace).toString(),
|
|
1827
1925
|
head: (r['head'] ?? '').toString(),
|
|
@@ -1833,51 +1931,55 @@ class NomosBridge {
|
|
|
1833
1931
|
/// Open a workspace on this plane: boot its LOCAL githolon under the installed law (from [restoreFrom]
|
|
1834
1932
|
/// custody, or a cloud pull when [cloud] is given). This is the sugar the single-workspace path uses; a
|
|
1835
1933
|
/// multi-workspace app names a [session] per workspace and holds their [session] handles.
|
|
1836
|
-
Future<void> connect(
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1934
|
+
Future<void> connect({
|
|
1935
|
+
String? cloud,
|
|
1936
|
+
required String workspace,
|
|
1937
|
+
String? clientId,
|
|
1938
|
+
String? authToken,
|
|
1939
|
+
String? authorSecret,
|
|
1940
|
+
String? restoreFrom,
|
|
1941
|
+
bool allowUnborn = false,
|
|
1942
|
+
bool recover = false,
|
|
1943
|
+
CustodyBreakPolicy? onCustodyBreak,
|
|
1944
|
+
String? session,
|
|
1945
|
+
int? maxMounted,
|
|
1946
|
+
}) async {
|
|
1848
1947
|
final ws = session ?? workspace;
|
|
1849
1948
|
// Record the session name ONLY on a SUCCESSFUL open. Adding it before the await left a PHANTOM session
|
|
1850
1949
|
// behind on a failed connect (a corrupt restoreFrom, an unreachable cloud): acquireSession/follow fast-path
|
|
1851
1950
|
// on `_sessionNames.contains` and would hand back a dead handle whose every op refuses "session not
|
|
1852
1951
|
// connected", and ifAbsentCredentials would never fire. The `_call` throwing skips the add below.
|
|
1853
|
-
final r = _asMap(
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1952
|
+
final r = _asMap(
|
|
1953
|
+
await _call('connect', {
|
|
1954
|
+
// [cloud] is OPTIONAL — omit it to open a LOCAL-ONLY workspace (a local githolon with no cloud
|
|
1955
|
+
// counterpart; the runner uses a `local://` sentinel and sync is a no-op). Pass it to make the
|
|
1956
|
+
// workspace cloud-bound (per-workspace sync posture). A device's plane freely mixes both.
|
|
1957
|
+
if (cloud != null) 'cloud': cloud,
|
|
1958
|
+
'workspace': workspace,
|
|
1959
|
+
// MULTI-WORKSPACE: name the [session] this connect opens on this device's local plane (default =
|
|
1960
|
+
// [workspace]). Omitted on the single-workspace sugar path (byte-identical wire to before).
|
|
1961
|
+
if (session != null) 'session': session,
|
|
1962
|
+
if (clientId != null) 'clientId': clientId,
|
|
1963
|
+
if (authToken != null) 'authToken': authToken,
|
|
1964
|
+
// The device signing secret (base64, from [authorKeygen]) — the warrant default for every offer.
|
|
1965
|
+
// Stays client-side; the kernel mints the signed-author attestation so authorship is replay-provable.
|
|
1966
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
1967
|
+
if (restoreFrom != null) 'restoreFrom': restoreFrom,
|
|
1968
|
+
if (allowUnborn) 'allowUnborn': true,
|
|
1969
|
+
// the realm's mounted-workspace budget (LRU park/remount above it) — first connect wins.
|
|
1970
|
+
if (maxMounted != null) 'maxMounted': maxMounted,
|
|
1971
|
+
// RECOVER-IN-PLACE over a corrupt restoreFrom snapshot: salvage local-only writes + DLQ, refold fresh
|
|
1972
|
+
// from cloud, re-offer the salvaged work. The report lands on [lastRecovery]. Nothing local is lost.
|
|
1973
|
+
if (recover) 'recover': true,
|
|
1974
|
+
// CUSTODY-BREAK POLICY (rebirth_and_custody_lifecycle.md): "adopt"/"reject" decide automatically;
|
|
1975
|
+
// "ask" surfaces the break on [custodyEvents] (kind "ask") and awaits [resolveCustodyBreak]. Omitted ⇒
|
|
1976
|
+
// fail closed (the break surfaces as an error on the sync/pull that hit it; the holon stays local).
|
|
1977
|
+
if (onCustodyBreak != null) 'onCustodyBreak': onCustodyBreak.wire,
|
|
1978
|
+
}),
|
|
1979
|
+
);
|
|
1980
|
+
_eventPlane._sessionNames.add(
|
|
1981
|
+
ws,
|
|
1982
|
+
); // the open succeeded — the session is real now
|
|
1881
1983
|
// Opening succeeded, but scoped metrics have not yet proved that this
|
|
1882
1984
|
// workspace is current. Unknown readiness must fail closed: otherwise a
|
|
1883
1985
|
// generated `whenCurrent()` can observe the default idle value and return
|
|
@@ -1893,8 +1995,8 @@ class NomosBridge {
|
|
|
1893
1995
|
if (authorSecret != null && authorSecret.isNotEmpty) {
|
|
1894
1996
|
_eventPlane._sessionAuthorSecrets[ws] = authorSecret;
|
|
1895
1997
|
}
|
|
1896
|
-
_eventPlane._lastRecovery =
|
|
1897
|
-
|
|
1998
|
+
_eventPlane._lastRecovery = (r['recovery'] as Map?)
|
|
1999
|
+
?.cast<String, Object?>();
|
|
1898
2000
|
if (_eventPlane._syncStatusCtrl.hasListener) {
|
|
1899
2001
|
unawaited(_eventPlane._pollMetricsOnce());
|
|
1900
2002
|
}
|
|
@@ -1918,21 +2020,30 @@ class NomosBridge {
|
|
|
1918
2020
|
/// Where a session sits on the custody spectrum (incarnation, posture, pending/salvaged local work,
|
|
1919
2021
|
/// supersession history) — for a status banner or a "you're offline on old truth" indicator.
|
|
1920
2022
|
Future<NomosCustodyStatus> custodyStatus({String? session}) async =>
|
|
1921
|
-
NomosCustodyStatus.fromJson(
|
|
1922
|
-
|
|
2023
|
+
NomosCustodyStatus.fromJson(
|
|
2024
|
+
_asMap(
|
|
2025
|
+
await _call('custodyStatus', {
|
|
2026
|
+
if (session != null) 'session': session,
|
|
2027
|
+
}),
|
|
2028
|
+
),
|
|
2029
|
+
);
|
|
1923
2030
|
|
|
1924
2031
|
/// Continue an `ask`-policy custody break with the user's decision (adopt|reject) — the async UX lane.
|
|
1925
|
-
Future<Map<String, Object?>> resolveCustodyBreak(
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2032
|
+
Future<Map<String, Object?>> resolveCustodyBreak(
|
|
2033
|
+
String decision, {
|
|
2034
|
+
String? session,
|
|
2035
|
+
}) async => _asMap(
|
|
2036
|
+
await _call('resolveCustodyBreak', {
|
|
2037
|
+
'decision': decision,
|
|
2038
|
+
if (session != null) 'session': session,
|
|
2039
|
+
}),
|
|
2040
|
+
);
|
|
1931
2041
|
|
|
1932
2042
|
/// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
|
|
1933
2043
|
Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
|
|
1934
|
-
final r = _asMap(
|
|
1935
|
-
|
|
2044
|
+
final r = _asMap(
|
|
2045
|
+
await _call('salvagedIntents', {if (session != null) 'session': session}),
|
|
2046
|
+
);
|
|
1936
2047
|
return ((r['salvaged'] as List?) ?? const [])
|
|
1937
2048
|
.map((e) => (e as Map).cast<String, Object?>())
|
|
1938
2049
|
.toList();
|
|
@@ -1940,16 +2051,26 @@ class NomosBridge {
|
|
|
1940
2051
|
|
|
1941
2052
|
/// Re-offer parked salvaged work under the current (adopted) law — the explicit app move (never automatic
|
|
1942
2053
|
/// across incarnations). [id] re-offers one; omitted re-offers all.
|
|
1943
|
-
Future<Map<String, Object?>> reofferSalvaged(
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2054
|
+
Future<Map<String, Object?>> reofferSalvaged({
|
|
2055
|
+
String? id,
|
|
2056
|
+
String? session,
|
|
2057
|
+
}) async => _asMap(
|
|
2058
|
+
await _call('reofferSalvaged', {
|
|
2059
|
+
if (id != null) 'id': id,
|
|
2060
|
+
if (session != null) 'session': session,
|
|
2061
|
+
}),
|
|
2062
|
+
);
|
|
1947
2063
|
|
|
1948
2064
|
/// Discard one parked salvaged intent — the app's explicit choice.
|
|
1949
|
-
Future<Map<String, Object?>> discardSalvaged(
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2065
|
+
Future<Map<String, Object?>> discardSalvaged(
|
|
2066
|
+
String id, {
|
|
2067
|
+
String? session,
|
|
2068
|
+
}) async => _asMap(
|
|
2069
|
+
await _call('discardSalvaged', {
|
|
2070
|
+
'id': id,
|
|
2071
|
+
if (session != null) 'session': session,
|
|
2072
|
+
}),
|
|
2073
|
+
);
|
|
1953
2074
|
|
|
1954
2075
|
// ── THE DEAD-LETTER QUEUE (refused-but-legitimate work is NEVER lost) — the Dart mirror of the
|
|
1955
2076
|
// JS holon's DLQ surface (`deadLetters()` / `retryDeadLetter()` / `discardDeadLetter()`). A
|
|
@@ -1961,7 +2082,8 @@ class NomosBridge {
|
|
|
1961
2082
|
/// the intent bytes stay in the durable queue.
|
|
1962
2083
|
Future<List<Map<String, Object?>>> deadLetters({String? session}) async {
|
|
1963
2084
|
final r = _asMap(
|
|
1964
|
-
|
|
2085
|
+
await _call('deadLetters', {if (session != null) 'session': session}),
|
|
2086
|
+
);
|
|
1965
2087
|
return ((r['dead'] as List?) ?? const [])
|
|
1966
2088
|
.map((e) => (e as Map).cast<String, Object?>())
|
|
1967
2089
|
.toList();
|
|
@@ -1970,16 +2092,26 @@ class NomosBridge {
|
|
|
1970
2092
|
/// Retry one dead letter (after the law fix landed): pulls canon first (it may already be
|
|
1971
2093
|
/// admitted), else re-offers the parked bytes locally. `{ok, head?|alreadyOnMain?|error?}` —
|
|
1972
2094
|
/// a refusal leaves the entry parked (with the fresh error), never lost.
|
|
1973
|
-
Future<Map<String, Object?>> retryDeadLetter(
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2095
|
+
Future<Map<String, Object?>> retryDeadLetter(
|
|
2096
|
+
String id, {
|
|
2097
|
+
String? session,
|
|
2098
|
+
}) async => _asMap(
|
|
2099
|
+
await _call('retryDeadLetter', {
|
|
2100
|
+
'id': id,
|
|
2101
|
+
if (session != null) 'session': session,
|
|
2102
|
+
}),
|
|
2103
|
+
);
|
|
1977
2104
|
|
|
1978
2105
|
/// Discard one dead letter — the app's EXPLICIT choice to drop the work. `{ok}`.
|
|
1979
|
-
Future<Map<String, Object?>> discardDeadLetter(
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2106
|
+
Future<Map<String, Object?>> discardDeadLetter(
|
|
2107
|
+
String id, {
|
|
2108
|
+
String? session,
|
|
2109
|
+
}) async => _asMap(
|
|
2110
|
+
await _call('discardDeadLetter', {
|
|
2111
|
+
'id': id,
|
|
2112
|
+
if (session != null) 'session': session,
|
|
2113
|
+
}),
|
|
2114
|
+
);
|
|
1983
2115
|
|
|
1984
2116
|
/// SALVAGE — best-effort extraction of this holon's local-only authored work (un-synced pending
|
|
1985
2117
|
/// writes + the DLQ), each with its full `changeB64` bytes: `{pendingWrites, deadLetters}`. The
|
|
@@ -1990,15 +2122,17 @@ class NomosBridge {
|
|
|
1990
2122
|
/// ABSORB salvaged work into THIS holon: pending writes are re-offered under the current law
|
|
1991
2123
|
/// (a refusal dead-letters — never lost), and dead-letter entries are re-queued into the DLQ.
|
|
1992
2124
|
/// Returns the recovery report `{salvagedWrites, requeuedDeadLetters, unsalvageable}`.
|
|
1993
|
-
Future<Map<String, Object?>> absorbSalvage(
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2125
|
+
Future<Map<String, Object?>> absorbSalvage({
|
|
2126
|
+
List<Map<String, Object?>> pendingWrites = const [],
|
|
2127
|
+
List<Map<String, Object?>> deadLetters = const [],
|
|
2128
|
+
String? session,
|
|
2129
|
+
}) async => _asMap(
|
|
2130
|
+
await _call('absorbSalvage', {
|
|
2131
|
+
'pendingWrites': pendingWrites,
|
|
2132
|
+
'deadLetters': deadLetters,
|
|
2133
|
+
if (session != null) 'session': session,
|
|
2134
|
+
}),
|
|
2135
|
+
);
|
|
2002
2136
|
|
|
2003
2137
|
/// The salvage report from the most recent `connect(recover: true)` — `{ salvagedWrites, requeuedDeadLetters,
|
|
2004
2138
|
/// unsalvageable }`, or null if the connection didn't recover. Show "recovered N writes" / surface
|
|
@@ -2008,9 +2142,10 @@ class NomosBridge {
|
|
|
2008
2142
|
|
|
2009
2143
|
/// LOCAL-FIRST offer: carry opaque intent bytes into the GitHolon. Returns the new local head; durable on [sync].
|
|
2010
2144
|
@Deprecated(
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2145
|
+
'Engine-level generic writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
2146
|
+
'dispatch through a generated <Domain>Client bound to a NomosSession '
|
|
2147
|
+
'(XClient.bind(session)). Retained for CLIs/admin tooling/generated internals only.',
|
|
2148
|
+
)
|
|
2014
2149
|
Future<String> offer({required String intentBytes}) async {
|
|
2015
2150
|
final r = _asMap(await _call('offer', {'intentBytes': intentBytes}));
|
|
2016
2151
|
return r['head'].toString();
|
|
@@ -2025,10 +2160,11 @@ class NomosBridge {
|
|
|
2025
2160
|
/// inject evidence. [force] carries the deploy/evolve force flag through. [session] pins the workspace in
|
|
2026
2161
|
/// a cluster harness (absent ⇒ the runner's sole-session default).
|
|
2027
2162
|
@Deprecated(
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2163
|
+
'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
2164
|
+
'dispatch through a generated <Domain>Client bound to a NomosSession '
|
|
2165
|
+
'(XClient.bind(session) — no domain/directive strings at call sites). '
|
|
2166
|
+
'Retained for CLIs/admin tooling/generated internals only.',
|
|
2167
|
+
)
|
|
2032
2168
|
Future<String> offerDirective({
|
|
2033
2169
|
required String domain,
|
|
2034
2170
|
required String directiveId,
|
|
@@ -2043,20 +2179,22 @@ class NomosBridge {
|
|
|
2043
2179
|
String? authorSecret,
|
|
2044
2180
|
NomosBirthCertificate? birthCertificate,
|
|
2045
2181
|
}) async {
|
|
2046
|
-
final r = _asMap(
|
|
2047
|
-
'
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2182
|
+
final r = _asMap(
|
|
2183
|
+
await _call('offerDirective', {
|
|
2184
|
+
'domain': domain,
|
|
2185
|
+
'directiveId': directiveId,
|
|
2186
|
+
'payload': payload,
|
|
2187
|
+
if (actor != null) 'actor': actor,
|
|
2188
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
2189
|
+
if (nomosConflictSnapshots != null)
|
|
2190
|
+
'__nomosConflictSnapshots': nomosConflictSnapshots,
|
|
2191
|
+
if (force != null) 'force': force,
|
|
2192
|
+
if (session != null) 'session': session,
|
|
2193
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
2194
|
+
if (birthCertificate != null)
|
|
2195
|
+
'birthCertificate': birthCertificate.toWire(),
|
|
2196
|
+
}),
|
|
2197
|
+
);
|
|
2060
2198
|
return r['head'].toString();
|
|
2061
2199
|
}
|
|
2062
2200
|
|
|
@@ -2065,9 +2203,10 @@ class NomosBridge {
|
|
|
2065
2203
|
/// generated create method returns the typed id with NO query-after round-trip. [created] is empty for
|
|
2066
2204
|
/// pure mutations.
|
|
2067
2205
|
@Deprecated(
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2206
|
+
'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
2207
|
+
'dispatch through a generated <Domain>Client bound to a NomosSession. '
|
|
2208
|
+
'Retained for CLIs/admin tooling/generated internals only.',
|
|
2209
|
+
)
|
|
2071
2210
|
Future<NomosOfferOutcome> offerCreates({
|
|
2072
2211
|
required String domain,
|
|
2073
2212
|
required String directiveId,
|
|
@@ -2079,19 +2218,18 @@ class NomosBridge {
|
|
|
2079
2218
|
String? session,
|
|
2080
2219
|
String? authorSecret,
|
|
2081
2220
|
NomosBirthCertificate? birthCertificate,
|
|
2082
|
-
}) =>
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
);
|
|
2221
|
+
}) => offerOutcome(
|
|
2222
|
+
domain: domain,
|
|
2223
|
+
directiveId: directiveId,
|
|
2224
|
+
payload: payload,
|
|
2225
|
+
actor: actor,
|
|
2226
|
+
domainHash: domainHash,
|
|
2227
|
+
nomosConflictSnapshots: nomosConflictSnapshots,
|
|
2228
|
+
force: force,
|
|
2229
|
+
session: session,
|
|
2230
|
+
authorSecret: authorSecret,
|
|
2231
|
+
birthCertificate: birthCertificate,
|
|
2232
|
+
);
|
|
2095
2233
|
|
|
2096
2234
|
/// THE ONE OUTCOME-RETURNING OFFER LANE — [offerDirective] answering the full typed [NomosOfferOutcome]
|
|
2097
2235
|
/// (head + law-minted `created` NomosRefs + births), for EVERY generated method (create or mutate). A pure
|
|
@@ -2121,23 +2259,29 @@ class NomosBridge {
|
|
|
2121
2259
|
// attributable breakdown of one offer with no new API.
|
|
2122
2260
|
final t0 = DateTime.now().millisecondsSinceEpoch;
|
|
2123
2261
|
try {
|
|
2124
|
-
r = _asMap(
|
|
2125
|
-
'
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2262
|
+
r = _asMap(
|
|
2263
|
+
await _call('offerDirective', {
|
|
2264
|
+
'domain': domain,
|
|
2265
|
+
'directiveId': directiveId,
|
|
2266
|
+
'payload': payload,
|
|
2267
|
+
if (actor != null) 'actor': actor,
|
|
2268
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
2269
|
+
if (nomosConflictSnapshots != null)
|
|
2270
|
+
'__nomosConflictSnapshots': nomosConflictSnapshots,
|
|
2271
|
+
if (force != null) 'force': force,
|
|
2272
|
+
if (session != null) 'session': session,
|
|
2273
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
2274
|
+
if (birthCertificate != null)
|
|
2275
|
+
'birthCertificate': birthCertificate.toWire(),
|
|
2276
|
+
}),
|
|
2277
|
+
);
|
|
2138
2278
|
} catch (e) {
|
|
2139
|
-
throw await _wrapDomainNotInstalled(
|
|
2140
|
-
|
|
2279
|
+
throw await _wrapDomainNotInstalled(
|
|
2280
|
+
e,
|
|
2281
|
+
domain: domain,
|
|
2282
|
+
domainHash: domainHash,
|
|
2283
|
+
requestedWorkspace: session,
|
|
2284
|
+
);
|
|
2141
2285
|
}
|
|
2142
2286
|
final t1 = DateTime.now().millisecondsSinceEpoch;
|
|
2143
2287
|
final timing = r['timing'];
|
|
@@ -2150,17 +2294,19 @@ class NomosBridge {
|
|
|
2150
2294
|
}
|
|
2151
2295
|
|
|
2152
2296
|
noteLocalSpan(
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2297
|
+
track: 'submit',
|
|
2298
|
+
name: 'offer: bridge ipc (up-leg)',
|
|
2299
|
+
kind: 'bridge-ipc',
|
|
2300
|
+
t0: t0,
|
|
2301
|
+
t1: within(recv),
|
|
2302
|
+
);
|
|
2158
2303
|
noteLocalSpan(
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2304
|
+
track: 'submit',
|
|
2305
|
+
name: 'offer: response delivery (down-leg)',
|
|
2306
|
+
kind: 'response-delivery',
|
|
2307
|
+
t0: within(done),
|
|
2308
|
+
t1: t1,
|
|
2309
|
+
);
|
|
2164
2310
|
}
|
|
2165
2311
|
}
|
|
2166
2312
|
final births = ((r['birthOutcomes'] as List?) ?? const [])
|
|
@@ -2182,8 +2328,9 @@ class NomosBridge {
|
|
|
2182
2328
|
final bornRaw = (r['born'] as List?);
|
|
2183
2329
|
final bornNames = <String>[
|
|
2184
2330
|
if (bornRaw != null)
|
|
2185
|
-
...bornRaw
|
|
2186
|
-
|
|
2331
|
+
...bornRaw.map(
|
|
2332
|
+
(e) => (e is Map ? (e['workspace'] ?? '') : e).toString(),
|
|
2333
|
+
)
|
|
2187
2334
|
else
|
|
2188
2335
|
...births
|
|
2189
2336
|
.map((m) => (m['workspace'] ?? '').toString())
|
|
@@ -2196,7 +2343,8 @@ class NomosBridge {
|
|
|
2196
2343
|
}
|
|
2197
2344
|
}
|
|
2198
2345
|
final sourceSession = session ?? _sessionName;
|
|
2199
|
-
final birthAuthorSecret =
|
|
2346
|
+
final birthAuthorSecret =
|
|
2347
|
+
authorSecret ??
|
|
2200
2348
|
(sourceSession == null
|
|
2201
2349
|
? null
|
|
2202
2350
|
: _eventPlane._sessionAuthorSecrets[sourceSession]);
|
|
@@ -2219,17 +2367,23 @@ class NomosBridge {
|
|
|
2219
2367
|
workspace: ws,
|
|
2220
2368
|
created: ((r['created'] as List?) ?? const [])
|
|
2221
2369
|
.whereType<Map>()
|
|
2222
|
-
.map(
|
|
2370
|
+
.map(
|
|
2371
|
+
(m) => NomosCreated(
|
|
2223
2372
|
type: (m['type'] ?? '').toString(),
|
|
2224
2373
|
id: (m['id'] ?? '').toString(),
|
|
2225
|
-
workspace: ws
|
|
2374
|
+
workspace: ws,
|
|
2375
|
+
),
|
|
2376
|
+
)
|
|
2226
2377
|
.toList(),
|
|
2227
2378
|
affected: ((r['affected'] as List?) ?? const [])
|
|
2228
2379
|
.whereType<Map>()
|
|
2229
|
-
.map(
|
|
2380
|
+
.map(
|
|
2381
|
+
(m) => NomosCreated(
|
|
2230
2382
|
type: (m['type'] ?? '').toString(),
|
|
2231
2383
|
id: (m['id'] ?? '').toString(),
|
|
2232
|
-
workspace: ws
|
|
2384
|
+
workspace: ws,
|
|
2385
|
+
),
|
|
2386
|
+
)
|
|
2233
2387
|
.toList(),
|
|
2234
2388
|
born: bornNames,
|
|
2235
2389
|
birthOutcomes: births,
|
|
@@ -2241,12 +2395,16 @@ class NomosBridge {
|
|
|
2241
2395
|
/// context (which session, what it actually runs, the likely causes) — the string itself
|
|
2242
2396
|
/// originates in the sealed kernel, so the bridge (not the kernel) owns the ergonomics.
|
|
2243
2397
|
/// Anything that isn't that refusal passes through unchanged.
|
|
2244
|
-
Future<Object> _wrapDomainNotInstalled(
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2398
|
+
Future<Object> _wrapDomainNotInstalled(
|
|
2399
|
+
Object e, {
|
|
2400
|
+
required String domain,
|
|
2401
|
+
String? domainHash,
|
|
2402
|
+
String? requestedWorkspace,
|
|
2403
|
+
}) async {
|
|
2404
|
+
if (!RegExp(
|
|
2405
|
+
r"no active installed law declares domain '",
|
|
2406
|
+
).hasMatch(e.toString()))
|
|
2407
|
+
return e;
|
|
2250
2408
|
final self = this;
|
|
2251
2409
|
final sessionWs = self is NomosSession ? self.workspace : null;
|
|
2252
2410
|
var activeKeys = const <String>[];
|
|
@@ -2272,8 +2430,9 @@ class NomosBridge {
|
|
|
2272
2430
|
/// installed package, read off the LOCAL projection (offline). Keys = the ACTIVE domain keys the
|
|
2273
2431
|
/// workspace serves. Null when no on-ledger interface is observed yet (pre-#72e law).
|
|
2274
2432
|
Future<Map<String, Object?>?> interfaceOffer({String? session}) async {
|
|
2275
|
-
final r = _asMap(
|
|
2276
|
-
|
|
2433
|
+
final r = _asMap(
|
|
2434
|
+
await _call('interfaceOffer', {if (session != null) 'session': session}),
|
|
2435
|
+
);
|
|
2277
2436
|
return (r['offer'] as Map?)?.cast<String, Object?>();
|
|
2278
2437
|
}
|
|
2279
2438
|
|
|
@@ -2301,26 +2460,32 @@ class NomosBridge {
|
|
|
2301
2460
|
/// cloud (omit for the local plane — a locally-born child mounts in place, no re-clone).
|
|
2302
2461
|
/// [authorSecret] — the device signing key for the followed session's writes (era-0: an attested-born
|
|
2303
2462
|
/// child is WARRANTED; write access needs its owner's enrolled device key).
|
|
2304
|
-
Future<NomosSession> follow<TAggregate>(
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2463
|
+
Future<NomosSession> follow<TAggregate>(
|
|
2464
|
+
NomosRef<TAggregate> ref, {
|
|
2465
|
+
String? cloud,
|
|
2466
|
+
String? clientId,
|
|
2467
|
+
String? authToken,
|
|
2468
|
+
bool allowUnborn = false,
|
|
2469
|
+
String? authorSecret,
|
|
2470
|
+
}) async {
|
|
2310
2471
|
final ws = NmsInternalNomosRefs.workspace(ref);
|
|
2311
2472
|
if (ws.isEmpty) {
|
|
2312
|
-
throw ArgumentError.value(
|
|
2313
|
-
|
|
2473
|
+
throw ArgumentError.value(
|
|
2474
|
+
ref,
|
|
2475
|
+
'ref',
|
|
2476
|
+
'follow: the NomosRef carries no workspace — only a workspace-complete ref is followable',
|
|
2477
|
+
);
|
|
2314
2478
|
}
|
|
2315
2479
|
if (!_sessionNames.contains(ws)) {
|
|
2316
2480
|
await connect(
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2481
|
+
cloud: cloud,
|
|
2482
|
+
workspace: ws,
|
|
2483
|
+
session: ws,
|
|
2484
|
+
clientId: clientId,
|
|
2485
|
+
authToken: authToken,
|
|
2486
|
+
allowUnborn: allowUnborn,
|
|
2487
|
+
authorSecret: authorSecret,
|
|
2488
|
+
);
|
|
2324
2489
|
}
|
|
2325
2490
|
return session(ws);
|
|
2326
2491
|
}
|
|
@@ -2339,10 +2504,12 @@ class NomosBridge {
|
|
|
2339
2504
|
/// which every attested-born child is — refuses unsigned writes, so a locally-born child the app will
|
|
2340
2505
|
/// WRITE to must be acquired with its owner's device key). Applies to the resident (local) open;
|
|
2341
2506
|
/// the credentials path takes its own `NomosSessionCredentials.authorSecret` (falling back to this).
|
|
2342
|
-
Future<NomosSession> acquireSession(
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2507
|
+
Future<NomosSession> acquireSession(
|
|
2508
|
+
String workspace, {
|
|
2509
|
+
Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
|
|
2510
|
+
String? authorSecret,
|
|
2511
|
+
String? inheritedPrincipal,
|
|
2512
|
+
}) {
|
|
2346
2513
|
// Fast path: a live session needs no runner round-trip (and never asks for credentials).
|
|
2347
2514
|
final plane = _eventPlane;
|
|
2348
2515
|
final sourcePrincipal = inheritedPrincipal ?? principal;
|
|
@@ -2351,7 +2518,8 @@ class NomosBridge {
|
|
|
2351
2518
|
final existing = plane._sessionPrincipals[workspace];
|
|
2352
2519
|
if (existing != null && existing != sourcePrincipal) {
|
|
2353
2520
|
throw StateError(
|
|
2354
|
-
|
|
2521
|
+
'acquireSession("$workspace"): the workspace is already bound to a different signed-in person',
|
|
2522
|
+
);
|
|
2355
2523
|
}
|
|
2356
2524
|
plane._sessionPrincipals[workspace] = sourcePrincipal;
|
|
2357
2525
|
final source = _sessionName;
|
|
@@ -2389,41 +2557,43 @@ class NomosBridge {
|
|
|
2389
2557
|
return ready(session(workspace));
|
|
2390
2558
|
}
|
|
2391
2559
|
// SINGLE-FLIGHT: one in-flight open per workspace per plane; concurrent callers join it.
|
|
2392
|
-
return plane._acquiring
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
required ==
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2560
|
+
return plane._acquiring
|
|
2561
|
+
.putIfAbsent(workspace, () async {
|
|
2562
|
+
// RESIDENCY IS THE KERNEL'S TO DEMAND AND OURS TO SATISFY. Replaying a chain can reach a
|
|
2563
|
+
// colocated read whose source workspace is not mounted; the kernel refuses TYPED, naming it.
|
|
2564
|
+
// Mount the named sibling — which consults local custody first and only then clones — and
|
|
2565
|
+
// retry. Bounded, and each name is satisfied at most once, so a genuinely missing workspace
|
|
2566
|
+
// fails with the kernel's own refusal instead of spinning.
|
|
2567
|
+
final satisfied = <String>{};
|
|
2568
|
+
for (var round = 0; ; round++) {
|
|
2569
|
+
try {
|
|
2570
|
+
return await _acquireSessionOnce(
|
|
2571
|
+
workspace,
|
|
2572
|
+
ready: ready,
|
|
2573
|
+
inheritPrincipal: inheritPrincipal,
|
|
2574
|
+
ifAbsentCredentials: ifAbsentCredentials,
|
|
2575
|
+
authorSecret: authorSecret,
|
|
2576
|
+
);
|
|
2577
|
+
} catch (error) {
|
|
2578
|
+
final required = nomosInternalResidencyRequiredWorkspace(error);
|
|
2579
|
+
if (required == null ||
|
|
2580
|
+
required == workspace ||
|
|
2581
|
+
!satisfied.add(required) ||
|
|
2582
|
+
round >= 8) {
|
|
2583
|
+
rethrow;
|
|
2584
|
+
}
|
|
2585
|
+
await acquireSession(
|
|
2586
|
+
required,
|
|
2587
|
+
ifAbsentCredentials: ifAbsentCredentials,
|
|
2588
|
+
authorSecret: authorSecret,
|
|
2589
|
+
inheritedPrincipal: sourcePrincipal,
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2415
2592
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
inheritedPrincipal: sourcePrincipal,
|
|
2421
|
-
);
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
}).whenComplete(() {
|
|
2425
|
-
plane._acquiring.remove(workspace);
|
|
2426
|
-
});
|
|
2593
|
+
})
|
|
2594
|
+
.whenComplete(() {
|
|
2595
|
+
plane._acquiring.remove(workspace);
|
|
2596
|
+
});
|
|
2427
2597
|
}
|
|
2428
2598
|
|
|
2429
2599
|
Future<NomosSession> _acquireSessionOnce(
|
|
@@ -2445,18 +2615,20 @@ class NomosBridge {
|
|
|
2445
2615
|
// credentials (an explicitly local tool/test), preserve the local-only open.
|
|
2446
2616
|
var resident = false;
|
|
2447
2617
|
try {
|
|
2448
|
-
resident = (await listWorkspaces())
|
|
2449
|
-
|
|
2618
|
+
resident = (await listWorkspaces()).any(
|
|
2619
|
+
(w) => w['workspace']?.toString() == workspace,
|
|
2620
|
+
);
|
|
2450
2621
|
} catch (_) {
|
|
2451
2622
|
// a pre-connect plane has no registry yet — fall through to the credentials decision below.
|
|
2452
2623
|
}
|
|
2453
2624
|
if (resident) {
|
|
2454
2625
|
if (ifAbsentCredentials == null) {
|
|
2455
2626
|
await connect(
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2627
|
+
workspace: workspace,
|
|
2628
|
+
session: workspace,
|
|
2629
|
+
allowUnborn: true,
|
|
2630
|
+
authorSecret: authorSecret,
|
|
2631
|
+
);
|
|
2460
2632
|
} else {
|
|
2461
2633
|
final c = await ifAbsentCredentials();
|
|
2462
2634
|
await connect(
|
|
@@ -2496,8 +2668,9 @@ class NomosBridge {
|
|
|
2496
2668
|
}
|
|
2497
2669
|
if (ifAbsentCredentials == null) {
|
|
2498
2670
|
throw StateError(
|
|
2499
|
-
|
|
2500
|
-
|
|
2671
|
+
'acquireSession("$workspace"): the workspace is not on this device\'s plane and no '
|
|
2672
|
+
'ifAbsentCredentials was given — a cloud open is genuinely required',
|
|
2673
|
+
);
|
|
2501
2674
|
}
|
|
2502
2675
|
final c = await ifAbsentCredentials();
|
|
2503
2676
|
await connect(
|
|
@@ -2519,9 +2692,7 @@ class NomosBridge {
|
|
|
2519
2692
|
/// to this process-wide plane. Child opens consult it before cloning from
|
|
2520
2693
|
/// cloud; the kernel remains the only component that can authenticate or
|
|
2521
2694
|
/// interpret the returned bytes.
|
|
2522
|
-
void nomosInternalRetainLocalCustodyLoader(
|
|
2523
|
-
NomosLocalCustodyLoader loader,
|
|
2524
|
-
) {
|
|
2695
|
+
void nomosInternalRetainLocalCustodyLoader(NomosLocalCustodyLoader loader) {
|
|
2525
2696
|
_eventPlane._localCustodyLoader = loader;
|
|
2526
2697
|
}
|
|
2527
2698
|
|
|
@@ -2535,8 +2706,10 @@ class NomosBridge {
|
|
|
2535
2706
|
/// Refresh client-held credentials on this already-open workspace without
|
|
2536
2707
|
/// replacing its resident holon or refolding custody. This grants no
|
|
2537
2708
|
/// authority: every offer is still judged by the kernel's author gate.
|
|
2538
|
-
Future<void> refreshCredentials(
|
|
2539
|
-
|
|
2709
|
+
Future<void> refreshCredentials({
|
|
2710
|
+
String? authToken,
|
|
2711
|
+
String? authorSecret,
|
|
2712
|
+
}) async {
|
|
2540
2713
|
if (authToken == null && authorSecret == null) return;
|
|
2541
2714
|
await _call('refreshCredentials', {
|
|
2542
2715
|
if (authToken != null) 'authToken': authToken,
|
|
@@ -2557,12 +2730,16 @@ class NomosBridge {
|
|
|
2557
2730
|
|
|
2558
2731
|
/// @internal The immutable genesis of a sibling custody chain. This is observational metadata used to
|
|
2559
2732
|
/// compose an ordinary source-enrolment offer; the kernel still reopens and proves every foreign read.
|
|
2560
|
-
Future<String> nomosWorkspaceGenesis(
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2733
|
+
Future<String> nomosWorkspaceGenesis(
|
|
2734
|
+
String workspace, {
|
|
2735
|
+
String? session,
|
|
2736
|
+
}) async =>
|
|
2737
|
+
(_asMap(
|
|
2738
|
+
await _call('nomosWorkspaceGenesis', {
|
|
2739
|
+
'workspace': workspace,
|
|
2740
|
+
if (session != null) 'session': session,
|
|
2741
|
+
}),
|
|
2742
|
+
)['genesis'] ??
|
|
2566
2743
|
'')
|
|
2567
2744
|
.toString();
|
|
2568
2745
|
|
|
@@ -2575,9 +2752,10 @@ class NomosBridge {
|
|
|
2575
2752
|
/// the holon encodes the v2 offer envelope via the single codec and admits it. Returns the new local head.
|
|
2576
2753
|
/// (This is the canonical name; [offerDirective] is the legacy alias the generated clients still call.)
|
|
2577
2754
|
@Deprecated(
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2755
|
+
'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
2756
|
+
'dispatch through a generated <Domain>Client bound to a NomosSession. '
|
|
2757
|
+
'Retained for CLIs/admin tooling/generated internals only.',
|
|
2758
|
+
)
|
|
2581
2759
|
Future<String> offerIntent({
|
|
2582
2760
|
required String domain,
|
|
2583
2761
|
required String directiveId,
|
|
@@ -2587,26 +2765,25 @@ class NomosBridge {
|
|
|
2587
2765
|
List<Map<String, Object?>>? nomosConflictSnapshots,
|
|
2588
2766
|
bool? force,
|
|
2589
2767
|
String? session,
|
|
2590
|
-
}) =>
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
///
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
{String? lawHash, String? session}) async {
|
|
2768
|
+
}) => offerDirective(
|
|
2769
|
+
domain: domain,
|
|
2770
|
+
directiveId: directiveId,
|
|
2771
|
+
payload: payload,
|
|
2772
|
+
actor: actor,
|
|
2773
|
+
domainHash: domainHash,
|
|
2774
|
+
nomosConflictSnapshots: nomosConflictSnapshots,
|
|
2775
|
+
force: force,
|
|
2776
|
+
session: session,
|
|
2777
|
+
);
|
|
2778
|
+
|
|
2779
|
+
/// Resolve the generated law's standard OSGi Contract Namespace requirements before bind.
|
|
2780
|
+
Future<void> assertKernelCoherence(
|
|
2781
|
+
List<Map<String, String>> kernelContracts, {
|
|
2782
|
+
String? lawHash,
|
|
2783
|
+
String? session,
|
|
2784
|
+
}) async {
|
|
2608
2785
|
await _call('assertKernelCoherence', {
|
|
2609
|
-
'
|
|
2786
|
+
'kernelContracts': kernelContracts,
|
|
2610
2787
|
if (lawHash != null) 'lawHash': lawHash,
|
|
2611
2788
|
if (session != null) 'session': session,
|
|
2612
2789
|
});
|
|
@@ -2626,10 +2803,11 @@ class NomosBridge {
|
|
|
2626
2803
|
/// The kernel looks up `(domain, directiveId)` in the ACTIVE installed law — [domain] must name the law
|
|
2627
2804
|
/// that actually declares [directiveId], never assume `'workspaces'`.
|
|
2628
2805
|
@Deprecated(
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2806
|
+
'Engine-level generic writes with a loose parent string are the footgun '
|
|
2807
|
+
'(session_bound_clients.md asks 1/4/7): births go through the generated <Domain>Client '
|
|
2808
|
+
'bound to the PARENT NomosSession (its birth method infers parent = the session workspace). '
|
|
2809
|
+
'Retained for CLIs/admin tooling/generated compatibility shims only.',
|
|
2810
|
+
)
|
|
2633
2811
|
Future<Map<String, Object?>> createWorkspace({
|
|
2634
2812
|
required String parent,
|
|
2635
2813
|
required String actor,
|
|
@@ -2645,20 +2823,26 @@ class NomosBridge {
|
|
|
2645
2823
|
// a frontend dev never threads a key through.
|
|
2646
2824
|
final Map<String, Object?> r;
|
|
2647
2825
|
try {
|
|
2648
|
-
r = _asMap(
|
|
2649
|
-
'
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2826
|
+
r = _asMap(
|
|
2827
|
+
await _call('createWorkspace', {
|
|
2828
|
+
'parent': parent,
|
|
2829
|
+
'actor': actor,
|
|
2830
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
2831
|
+
if (name != null) 'name': name,
|
|
2832
|
+
if (payload != null) 'payload': payload,
|
|
2833
|
+
'directiveId': directiveId,
|
|
2834
|
+
'domain': domain,
|
|
2835
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
2836
|
+
if (authToken != null) 'authToken': authToken,
|
|
2837
|
+
}),
|
|
2838
|
+
);
|
|
2659
2839
|
} catch (e) {
|
|
2660
|
-
throw await _wrapDomainNotInstalled(
|
|
2661
|
-
|
|
2840
|
+
throw await _wrapDomainNotInstalled(
|
|
2841
|
+
e,
|
|
2842
|
+
domain: domain,
|
|
2843
|
+
domainHash: domainHash,
|
|
2844
|
+
requestedWorkspace: parent,
|
|
2845
|
+
);
|
|
2662
2846
|
}
|
|
2663
2847
|
final verdict = (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
|
|
2664
2848
|
// STRUCTURED AUTHZ REFUSAL (co2 DX): a refused relay used to surface only the generic 422 body.
|
|
@@ -2695,17 +2879,19 @@ class NomosBridge {
|
|
|
2695
2879
|
String? authToken,
|
|
2696
2880
|
}) async {
|
|
2697
2881
|
// [authorSecret] defaults to the connection's signing key — the frontend dev never handles a key.
|
|
2698
|
-
final r = _asMap(
|
|
2699
|
-
'
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2882
|
+
final r = _asMap(
|
|
2883
|
+
await _call('shareWith', {
|
|
2884
|
+
'subject': subject,
|
|
2885
|
+
'relation': relation,
|
|
2886
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
2887
|
+
'asserterPrincipal': asserterPrincipal,
|
|
2888
|
+
if (homeLawHash != null) 'homeLawHash': homeLawHash,
|
|
2889
|
+
'object': object,
|
|
2890
|
+
if (asserterWorkspace != null) 'asserterWorkspace': asserterWorkspace,
|
|
2891
|
+
if (attestedAt != null) 'attestedAt': attestedAt,
|
|
2892
|
+
if (authToken != null) 'authToken': authToken,
|
|
2893
|
+
}),
|
|
2894
|
+
);
|
|
2709
2895
|
return (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
|
|
2710
2896
|
}
|
|
2711
2897
|
|
|
@@ -2714,13 +2900,14 @@ class NomosBridge {
|
|
|
2714
2900
|
List<String>? actions,
|
|
2715
2901
|
String domain = '',
|
|
2716
2902
|
String directiveId = 'undo',
|
|
2717
|
-
}) async =>
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2903
|
+
}) async => _asMap(
|
|
2904
|
+
await _call('undoMostRecent', {
|
|
2905
|
+
'scope': scope,
|
|
2906
|
+
if (actions != null) 'actions': actions,
|
|
2907
|
+
'domain': domain,
|
|
2908
|
+
'directiveId': directiveId,
|
|
2909
|
+
}),
|
|
2910
|
+
);
|
|
2724
2911
|
|
|
2725
2912
|
/// REDO the most-recent undo (strike the strike → its target re-applies). Same [scope]/[domain]/[directiveId]
|
|
2726
2913
|
/// as [undoMostRecent]. Returns `{ok, redone, head}`.
|
|
@@ -2728,12 +2915,13 @@ class NomosBridge {
|
|
|
2728
2915
|
String scope = 'all',
|
|
2729
2916
|
String domain = '',
|
|
2730
2917
|
String directiveId = 'undo',
|
|
2731
|
-
}) async =>
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2918
|
+
}) async => _asMap(
|
|
2919
|
+
await _call('redoMostRecent', {
|
|
2920
|
+
'scope': scope,
|
|
2921
|
+
'domain': domain,
|
|
2922
|
+
'directiveId': directiveId,
|
|
2923
|
+
}),
|
|
2924
|
+
);
|
|
2737
2925
|
|
|
2738
2926
|
/// Framework-private business correction seam. Generated clients bind the domain, its actions and actor;
|
|
2739
2927
|
/// application code supplies only the correction kind and its mandatory reason.
|
|
@@ -2744,15 +2932,16 @@ class NomosBridge {
|
|
|
2744
2932
|
required String reason,
|
|
2745
2933
|
String? domainHash,
|
|
2746
2934
|
String? actor,
|
|
2747
|
-
}) async =>
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2935
|
+
}) async => _asMap(
|
|
2936
|
+
await _call('correctMostRecent', {
|
|
2937
|
+
'kind': kind,
|
|
2938
|
+
'domain': domain,
|
|
2939
|
+
'actions': actions,
|
|
2940
|
+
'reason': reason,
|
|
2941
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
2942
|
+
if (actor != null) 'actor': actor,
|
|
2943
|
+
}),
|
|
2944
|
+
);
|
|
2756
2945
|
|
|
2757
2946
|
/// Framework-private exact-history adoption seam. Generated clients expose this as
|
|
2758
2947
|
/// `model.adopt()` and, only when prior business outcomes differ, a chained
|
|
@@ -2762,11 +2951,13 @@ class NomosBridge {
|
|
|
2762
2951
|
required String candidateHash,
|
|
2763
2952
|
String? because,
|
|
2764
2953
|
}) async {
|
|
2765
|
-
final r = _asMap(
|
|
2766
|
-
'
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2954
|
+
final r = _asMap(
|
|
2955
|
+
await _call('adoptGeneratedModel', {
|
|
2956
|
+
'domain': domain,
|
|
2957
|
+
'candidateHash': candidateHash,
|
|
2958
|
+
if (because != null) 'because': because,
|
|
2959
|
+
}),
|
|
2960
|
+
);
|
|
2770
2961
|
return ((r['verdict'] as Map?) ?? const <String, Object?>{})
|
|
2771
2962
|
.cast<String, Object?>();
|
|
2772
2963
|
}
|
|
@@ -2794,11 +2985,13 @@ class NomosBridge {
|
|
|
2794
2985
|
required String currentHash,
|
|
2795
2986
|
String? because,
|
|
2796
2987
|
}) async {
|
|
2797
|
-
final r = _asMap(
|
|
2798
|
-
'
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2988
|
+
final r = _asMap(
|
|
2989
|
+
await _call('restorePreviousGeneratedModel', {
|
|
2990
|
+
'domain': domain,
|
|
2991
|
+
'currentHash': currentHash,
|
|
2992
|
+
if (because != null) 'because': because,
|
|
2993
|
+
}),
|
|
2994
|
+
);
|
|
2802
2995
|
return ((r['verdict'] as Map?) ?? const <String, Object?>{})
|
|
2803
2996
|
.cast<String, Object?>();
|
|
2804
2997
|
}
|
|
@@ -2820,12 +3013,16 @@ class NomosBridge {
|
|
|
2820
3013
|
/// DECODED directive payload; affected = the aggregate refs its sealed events touched). `base` = a commit-
|
|
2821
3014
|
/// oid cursor (only intents newer than it); omitted = from the tip bounded by
|
|
2822
3015
|
/// `limit` (default 200). Generic raw rows — the typed shaping is the codegen `renderActivity`.
|
|
2823
|
-
Future<List<Map<String, dynamic>>> activityLog(
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
3016
|
+
Future<List<Map<String, dynamic>>> activityLog({
|
|
3017
|
+
String? base,
|
|
3018
|
+
int? limit,
|
|
3019
|
+
}) async {
|
|
3020
|
+
final r = _asMap(
|
|
3021
|
+
await _call('activityLog', {
|
|
3022
|
+
if (base != null) 'base': base,
|
|
3023
|
+
if (limit != null) 'limit': limit,
|
|
3024
|
+
}),
|
|
3025
|
+
);
|
|
2829
3026
|
return ((r['rows'] as List?) ?? const [])
|
|
2830
3027
|
.map((e) => (e as Map).cast<String, dynamic>())
|
|
2831
3028
|
.toList();
|
|
@@ -2842,10 +3039,16 @@ class NomosBridge {
|
|
|
2842
3039
|
|
|
2843
3040
|
/// Watch the RUNTIME activity feed: a stream that re-emits the newest-first activity rows on every local
|
|
2844
3041
|
/// change (doorbell/sync tick). `base` = a commit-oid cursor. Cancelling drops the watch on the runner.
|
|
2845
|
-
Stream<List<dynamic>> watchActivity(
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
3042
|
+
Stream<List<dynamic>> watchActivity({
|
|
3043
|
+
String? base,
|
|
3044
|
+
int? limit,
|
|
3045
|
+
String? session,
|
|
3046
|
+
}) => _watchStream(
|
|
3047
|
+
kind: 'activity',
|
|
3048
|
+
base: base,
|
|
3049
|
+
limit: limit,
|
|
3050
|
+
session: session,
|
|
3051
|
+
);
|
|
2849
3052
|
|
|
2850
3053
|
/// Mint an ed25519 DEVICE KEY in-kernel → `{secret, public}` (base64). The SECRET stays on this device
|
|
2851
3054
|
/// (persist it in secure storage); pass it to [connect] as `authorSecret` (or per-offer `signWith`) so the
|
|
@@ -2866,9 +3069,18 @@ class NomosBridge {
|
|
|
2866
3069
|
/// ed25519 PUBLIC half), parentId, epoch}`; `deviceSecret` is that key's base64 SECRET half, handed to the
|
|
2867
3070
|
/// LOCAL kernel only (it never reaches a server). Returns typed authority evidence suitable for an offer.
|
|
2868
3071
|
Future<NomosBirthCertificate> signBirthCert(
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
3072
|
+
NomosBirthCertificateSpec spec,
|
|
3073
|
+
String deviceSecret,
|
|
3074
|
+
) async => NomosBirthCertificate.fromWire(
|
|
3075
|
+
_asMap(
|
|
3076
|
+
_asMap(
|
|
3077
|
+
await _call('signBirthCert', {
|
|
3078
|
+
'fields': spec.toWire(),
|
|
3079
|
+
'deviceSecret': deviceSecret,
|
|
3080
|
+
}),
|
|
3081
|
+
)['cert'],
|
|
3082
|
+
),
|
|
3083
|
+
);
|
|
2872
3084
|
|
|
2873
3085
|
/// MINT THE KEYLESS ENROLL-ONCE DELEGATION (`enrollFirstDevice.delegationCert`, the verified-first-device
|
|
2874
3086
|
/// lane). The device SELF-signs it; its authority is the carried `idpToken` (the kernel re-verifies that to
|
|
@@ -2877,26 +3089,33 @@ class NomosBridge {
|
|
|
2877
3089
|
/// keyless home is born with a malformed cert chain (the platform_cert falls into the delegation slot →
|
|
2878
3090
|
/// `home cert founder != delegation cert founder` at the home's own verify door).
|
|
2879
3091
|
Future<Map<String, Object?>> mintKeylessDelegation(
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
3092
|
+
Map<String, Object?> spec,
|
|
3093
|
+
String deviceSecret,
|
|
3094
|
+
) async =>
|
|
3095
|
+
_asMap(
|
|
3096
|
+
await _call('mintKeylessDelegation', {
|
|
3097
|
+
'spec': spec,
|
|
3098
|
+
'deviceSecret': deviceSecret,
|
|
3099
|
+
}),
|
|
3100
|
+
)['cert']
|
|
3101
|
+
as Map<String, Object?>? ??
|
|
2885
3102
|
const {};
|
|
2886
3103
|
|
|
2887
3104
|
/// ENROLL THIS DEVICE — the LIVE gate: bind a device key's PUBLIC half to `principal` on-chain as a
|
|
2888
3105
|
/// `signer` so the holon's gate can PROVE this device's authorship on replay (and the VA chain attest it).
|
|
2889
3106
|
/// `publicKey` is the base64 PUBLIC half (from [mintDeviceKey]); `authToken` (defaults to the connect-time
|
|
2890
3107
|
/// bearer) is the VERIFIED identity that authorizes the enrollment. Returns the offer verdict.
|
|
2891
|
-
Future<Map<String, Object?>> enrollDevice(
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3108
|
+
Future<Map<String, Object?>> enrollDevice({
|
|
3109
|
+
required String principal,
|
|
3110
|
+
required String publicKey,
|
|
3111
|
+
String? authToken,
|
|
3112
|
+
}) async => _asMap(
|
|
3113
|
+
await _call('enrollDevice', {
|
|
3114
|
+
'principal': principal,
|
|
3115
|
+
'publicKey': publicKey,
|
|
3116
|
+
if (authToken != null) 'authToken': authToken,
|
|
3117
|
+
}),
|
|
3118
|
+
);
|
|
2900
3119
|
|
|
2901
3120
|
/// Derive the device key's base64 PUBLIC half from its base64 SECRET seed, inside the LOCAL kernel
|
|
2902
3121
|
/// (the secret never leaves the device). The recovery lanes use this when only the secret was
|
|
@@ -2913,21 +3132,23 @@ class NomosBridge {
|
|
|
2913
3132
|
/// Pass `remote: true` to force the CLOUD-gateway read even when this holon IS the parent — the
|
|
2914
3133
|
/// seated barrier's proof-by-read: `enrolled: true` then means the shared chain durably holds this
|
|
2915
3134
|
/// device's signer tuple, not merely that the local fold ran.
|
|
2916
|
-
Future<Map<String, Object?>> parentSignerEnrollment(
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
3135
|
+
Future<Map<String, Object?>> parentSignerEnrollment({
|
|
3136
|
+
required String parent,
|
|
3137
|
+
required String principal,
|
|
3138
|
+
String? keyHash,
|
|
3139
|
+
String? publicKey,
|
|
3140
|
+
String? authToken,
|
|
3141
|
+
bool remote = false,
|
|
3142
|
+
}) async => _asMap(
|
|
3143
|
+
await _call('parentSignerEnrollment', {
|
|
3144
|
+
'parent': parent,
|
|
3145
|
+
'principal': principal,
|
|
3146
|
+
if (keyHash != null) 'keyHash': keyHash,
|
|
3147
|
+
if (publicKey != null) 'publicKey': publicKey,
|
|
3148
|
+
if (authToken != null) 'authToken': authToken,
|
|
3149
|
+
if (remote) 'remote': true,
|
|
3150
|
+
}),
|
|
3151
|
+
);
|
|
2931
3152
|
|
|
2932
3153
|
/// IDEMPOTENT PARENT-SIGNER RECOVERY — ensure this device key is a live `signer` on `parent`,
|
|
2933
3154
|
/// lawfully restoring the tuple a parent custody REBIRTH lost (the device key + the born home stay
|
|
@@ -2940,27 +3161,29 @@ class NomosBridge {
|
|
|
2940
3161
|
/// (+ [verifiedVia], the parent's AuthProvider issuer); a PRE-WARRANT parent admits the founding
|
|
2941
3162
|
/// `enrollSigner` (no token needed).
|
|
2942
3163
|
/// Returns `{parent, principal, keyHash, enrolled, repaired, domain?, directiveId?, checkedAt}`.
|
|
2943
|
-
Future<Map<String, Object?>> ensureParentSignerEnrollment(
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3164
|
+
Future<Map<String, Object?>> ensureParentSignerEnrollment({
|
|
3165
|
+
required String parent,
|
|
3166
|
+
required String principal,
|
|
3167
|
+
String? publicKey,
|
|
3168
|
+
String? authorSecret,
|
|
3169
|
+
String? domain,
|
|
3170
|
+
String? directiveId,
|
|
3171
|
+
String? authToken,
|
|
3172
|
+
String? idToken,
|
|
3173
|
+
String? verifiedVia,
|
|
3174
|
+
}) async => _asMap(
|
|
3175
|
+
await _call('ensureParentSignerEnrollment', {
|
|
3176
|
+
'parent': parent,
|
|
3177
|
+
'principal': principal,
|
|
3178
|
+
if (publicKey != null) 'publicKey': publicKey,
|
|
3179
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
3180
|
+
if (domain != null) 'domain': domain,
|
|
3181
|
+
if (directiveId != null) 'directiveId': directiveId,
|
|
3182
|
+
if (authToken != null) 'authToken': authToken,
|
|
3183
|
+
if (idToken != null) 'idToken': idToken,
|
|
3184
|
+
if (verifiedVia != null) 'verifiedVia': verifiedVia,
|
|
3185
|
+
}),
|
|
3186
|
+
);
|
|
2964
3187
|
|
|
2965
3188
|
/// AUTHORING PREFLIGHT — evaluate the SAME premises the kernel's authorization gate judges,
|
|
2966
3189
|
/// READ-ONLY, and return a typed verdict WITHOUT authoring — so the app catches a doomed write
|
|
@@ -2969,20 +3192,23 @@ class NomosBridge {
|
|
|
2969
3192
|
/// `{ok, ws, directiveId, actor, keyHash, cause, causes, domain?, relation?, object?, error?, checkedAt}`
|
|
2970
3193
|
/// where `cause` ∈ {directive-not-found, no-signing-key, missing-signer-enrollment, revoked-signer,
|
|
2971
3194
|
/// missing-role, null}. Use [canAuthorTyped] on a [NomosSession] for the typed-error variant.
|
|
2972
|
-
Future<Map<String, Object?>> canAuthor(
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3195
|
+
Future<Map<String, Object?>> canAuthor(
|
|
3196
|
+
String directiveId, {
|
|
3197
|
+
String? ws,
|
|
3198
|
+
String? principal,
|
|
3199
|
+
String? publicKey,
|
|
3200
|
+
String? keyHash,
|
|
3201
|
+
String? authToken,
|
|
3202
|
+
}) async => _asMap(
|
|
3203
|
+
await _call('canAuthor', {
|
|
3204
|
+
'directiveId': directiveId,
|
|
3205
|
+
if (ws != null) 'ws': ws,
|
|
3206
|
+
if (principal != null) 'principal': principal,
|
|
3207
|
+
if (publicKey != null) 'publicKey': publicKey,
|
|
3208
|
+
if (keyHash != null) 'keyHash': keyHash,
|
|
3209
|
+
if (authToken != null) 'authToken': authToken,
|
|
3210
|
+
}),
|
|
3211
|
+
);
|
|
2986
3212
|
|
|
2987
3213
|
/// VERIFY_CHAIN — the kernel's from-genesis self-verification of THIS holon's folded ledger (read-only:
|
|
2988
3214
|
/// contiguity → every intent re-admitted through the ONE gate against the state folded from the prefix). For
|
|
@@ -2995,16 +3221,16 @@ class NomosBridge {
|
|
|
2995
3221
|
/// Framework-private receiving-group setup. Generated application hosts own this ceremony; tenant code
|
|
2996
3222
|
/// receives only the resulting business application.
|
|
2997
3223
|
Future<Map<String, Object?>> nomosInternalBirthReceivingGroup(
|
|
2998
|
-
|
|
2999
|
-
|
|
3224
|
+
Map<String, Object?> spec,
|
|
3225
|
+
) async => _asMap(await _call('nomosInternalBirthReceivingGroup', spec));
|
|
3000
3226
|
|
|
3001
3227
|
/// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
|
|
3002
3228
|
/// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
|
|
3003
3229
|
/// (the free global invitation system): the granting workspace signs that it shares `object#relation`
|
|
3004
3230
|
/// with `subject`, and the invitee's home gate verifies the signature before recording it.
|
|
3005
3231
|
Future<Map<String, Object?>> attestationSign(
|
|
3006
|
-
|
|
3007
|
-
|
|
3232
|
+
Map<String, Object?> args,
|
|
3233
|
+
) async => _asMap(await _call('attestationSign', {'args': args}));
|
|
3008
3234
|
|
|
3009
3235
|
/// Issue a ROLE-DELEGATION cert (the `.delegatedRole()` lane, kernel `delegationCertSign` op). A platform
|
|
3010
3236
|
/// signs — with its grantor secret `signerSecret` (base64) — a cert granting `relation` to `subject`,
|
|
@@ -3021,33 +3247,38 @@ class NomosBridge {
|
|
|
3021
3247
|
required String subject,
|
|
3022
3248
|
int epoch = 1,
|
|
3023
3249
|
List<Object?>? certChain,
|
|
3024
|
-
}) async =>
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3250
|
+
}) async => _asMap(
|
|
3251
|
+
await _call('issueDelegationCert', {
|
|
3252
|
+
'args': {
|
|
3253
|
+
'signerSecret': signerSecret,
|
|
3254
|
+
'grantorId': grantorId,
|
|
3255
|
+
'relation': relation,
|
|
3256
|
+
'subject': subject,
|
|
3257
|
+
'epoch': epoch,
|
|
3258
|
+
if (certChain != null) 'certChain': certChain,
|
|
3259
|
+
},
|
|
3260
|
+
}),
|
|
3261
|
+
);
|
|
3035
3262
|
|
|
3036
3263
|
/// A declared query's rows from the local projection. `keys` (E2E sealing) injects the reader's held
|
|
3037
3264
|
/// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
|
|
3038
3265
|
/// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
|
|
3039
3266
|
/// Both are OMITTED from the wire when absent, so a public un-keyed read is byte-identical to before.
|
|
3040
|
-
Future<List<Map<String, dynamic>>> query(
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3267
|
+
Future<List<Map<String, dynamic>>> query(
|
|
3268
|
+
String queryId, [
|
|
3269
|
+
Map<String, Object?> params = const {},
|
|
3270
|
+
List<NomosScopeKey>? keys,
|
|
3271
|
+
String? principal,
|
|
3272
|
+
String? session,
|
|
3273
|
+
]) async {
|
|
3274
|
+
return _asRows(
|
|
3275
|
+
await _call('query', {
|
|
3276
|
+
'queryId': queryId,
|
|
3277
|
+
'params': params,
|
|
3278
|
+
...?_readAuthArgs(keys, principal),
|
|
3279
|
+
if (session != null) 'session': session,
|
|
3280
|
+
}),
|
|
3281
|
+
);
|
|
3051
3282
|
}
|
|
3052
3283
|
|
|
3053
3284
|
/// One bounded query page. Unlike [query], this never requires the kernel to materialize or serialize
|
|
@@ -3064,14 +3295,16 @@ class NomosBridge {
|
|
|
3064
3295
|
if (pageSize < 1 || pageSize > 1000) {
|
|
3065
3296
|
throw RangeError.range(pageSize, 1, 1000, 'pageSize');
|
|
3066
3297
|
}
|
|
3067
|
-
final result = _asMap(
|
|
3068
|
-
'
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3298
|
+
final result = _asMap(
|
|
3299
|
+
await _call('queryPage', {
|
|
3300
|
+
'queryId': queryId,
|
|
3301
|
+
'params': params,
|
|
3302
|
+
'pageSize': pageSize,
|
|
3303
|
+
if (cursor != null) 'cursor': cursor,
|
|
3304
|
+
...?_readAuthArgs(keys, principal),
|
|
3305
|
+
if (session != null) 'session': session,
|
|
3306
|
+
}),
|
|
3307
|
+
);
|
|
3075
3308
|
return NomosPage<Map<String, dynamic>>(
|
|
3076
3309
|
rows: _asRows(result),
|
|
3077
3310
|
cursor: result['cursor'] as String?,
|
|
@@ -3080,32 +3313,39 @@ class NomosBridge {
|
|
|
3080
3313
|
}
|
|
3081
3314
|
|
|
3082
3315
|
/// A single aggregate by id (the by-id read). `keys`/`principal`: see [query].
|
|
3083
|
-
Future<List<Map<String, dynamic>>> queryById(
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3316
|
+
Future<List<Map<String, dynamic>>> queryById(
|
|
3317
|
+
String id, [
|
|
3318
|
+
List<NomosScopeKey>? keys,
|
|
3319
|
+
String? principal,
|
|
3320
|
+
String? session,
|
|
3321
|
+
]) async => _asRows(
|
|
3322
|
+
await _call('queryById', {
|
|
3323
|
+
'id': id,
|
|
3324
|
+
...?_readAuthArgs(keys, principal),
|
|
3325
|
+
if (session != null) 'session': session,
|
|
3326
|
+
}),
|
|
3327
|
+
);
|
|
3092
3328
|
|
|
3093
3329
|
/// Compare an immutable publication pin with the stable series' one compiled
|
|
3094
3330
|
/// current pointer. This is an O(current-state) check: it neither resolves the
|
|
3095
3331
|
/// pinned publication nor reconstructs aggregate history.
|
|
3096
|
-
Future<ReferenceVersionStatus<TVersion>>
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
) async {
|
|
3100
|
-
final result = _asMap(
|
|
3101
|
-
'
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3332
|
+
Future<ReferenceVersionStatus<TVersion>> checkReferenceVersion<
|
|
3333
|
+
TVersion,
|
|
3334
|
+
TSeries
|
|
3335
|
+
>(VersionedNomosRef<TVersion, TSeries> reference) async {
|
|
3336
|
+
final result = _asMap(
|
|
3337
|
+
await _call('checkReferenceVersions', {
|
|
3338
|
+
'refs': [
|
|
3339
|
+
{
|
|
3340
|
+
'pinned': NmsInternalNomosRefs.toJson(reference.pinned),
|
|
3341
|
+
'series': NmsInternalNomosRefs.toJson(reference.series),
|
|
3342
|
+
'currentField': NmsInternalVersionedNomosRefs.currentField(
|
|
3343
|
+
reference,
|
|
3344
|
+
),
|
|
3345
|
+
},
|
|
3346
|
+
],
|
|
3347
|
+
}),
|
|
3348
|
+
);
|
|
3109
3349
|
final unique = result['unique'] as List?;
|
|
3110
3350
|
final first = unique == null || unique.isEmpty
|
|
3111
3351
|
? const <String, Object?>{}
|
|
@@ -3130,10 +3370,12 @@ class NomosBridge {
|
|
|
3130
3370
|
/// as `client.past.moments()`; callers pass the opaque [NomosMoment] back to `past.at(moment)` and never
|
|
3131
3371
|
/// need to know that the framework uses commit identities underneath.
|
|
3132
3372
|
Future<List<NomosMoment>> moments({int limit = 100, String? session}) async {
|
|
3133
|
-
final r = _asMap(
|
|
3134
|
-
'
|
|
3135
|
-
|
|
3136
|
-
|
|
3373
|
+
final r = _asMap(
|
|
3374
|
+
await _call('commits', {
|
|
3375
|
+
'limit': limit,
|
|
3376
|
+
if (session != null) 'session': session,
|
|
3377
|
+
}),
|
|
3378
|
+
);
|
|
3137
3379
|
return ((r['commits'] as List?) ?? const [])
|
|
3138
3380
|
.map((e) => NomosMoment.fromJson((e as Map).cast<String, Object?>()))
|
|
3139
3381
|
.toList();
|
|
@@ -3141,56 +3383,80 @@ class NomosBridge {
|
|
|
3141
3383
|
|
|
3142
3384
|
/// Run one declared business query against the verified world at [moment], without moving the live world.
|
|
3143
3385
|
/// `keys`/`principal` have exactly the same private/encrypted-read semantics as [query].
|
|
3144
|
-
Future<List<Map<String, dynamic>>> queryAt(
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3386
|
+
Future<List<Map<String, dynamic>>> queryAt(
|
|
3387
|
+
NomosMoment moment,
|
|
3388
|
+
String queryId, [
|
|
3389
|
+
Map<String, Object?> params = const {},
|
|
3390
|
+
List<NomosScopeKey>? keys,
|
|
3391
|
+
String? principal,
|
|
3392
|
+
String? session,
|
|
3393
|
+
]) async => _asRows(
|
|
3394
|
+
await _call('queryAt', {
|
|
3395
|
+
'commit': moment.id,
|
|
3396
|
+
'queryId': queryId,
|
|
3397
|
+
'params': params,
|
|
3398
|
+
...?_readAuthArgs(keys, principal),
|
|
3399
|
+
if (session != null) 'session': session,
|
|
3400
|
+
}),
|
|
3401
|
+
);
|
|
3156
3402
|
|
|
3157
3403
|
/// Read one aggregate from the verified world at [moment]. `keys`/`principal`: see [queryAt].
|
|
3158
|
-
Future<List<Map<String, dynamic>>> queryByIdAt(
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3404
|
+
Future<List<Map<String, dynamic>>> queryByIdAt(
|
|
3405
|
+
NomosMoment moment,
|
|
3406
|
+
String id, [
|
|
3407
|
+
List<NomosScopeKey>? keys,
|
|
3408
|
+
String? principal,
|
|
3409
|
+
String? session,
|
|
3410
|
+
]) async => _asRows(
|
|
3411
|
+
await _call('queryByIdAt', {
|
|
3412
|
+
'commit': moment.id,
|
|
3413
|
+
'id': id,
|
|
3414
|
+
...?_readAuthArgs(keys, principal),
|
|
3415
|
+
if (session != null) 'session': session,
|
|
3416
|
+
}),
|
|
3417
|
+
);
|
|
3168
3418
|
|
|
3169
3419
|
/// Maintained historic count/sum values at [moment]. These are the temporal counterparts of [count]/[sum].
|
|
3170
|
-
Future<int> countAt(
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3420
|
+
Future<int> countAt(
|
|
3421
|
+
NomosMoment moment,
|
|
3422
|
+
String countId, [
|
|
3423
|
+
String group = '',
|
|
3424
|
+
String? session,
|
|
3425
|
+
]) async =>
|
|
3426
|
+
((_asMap(
|
|
3427
|
+
await _call('countAt', {
|
|
3428
|
+
'commit': moment.id,
|
|
3429
|
+
'countId': countId,
|
|
3430
|
+
'group': group,
|
|
3431
|
+
if (session != null) 'session': session,
|
|
3432
|
+
}),
|
|
3433
|
+
)['count'])
|
|
3434
|
+
as num)
|
|
3178
3435
|
.toInt();
|
|
3179
3436
|
|
|
3180
|
-
Future<int> sumAt(
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3437
|
+
Future<int> sumAt(
|
|
3438
|
+
NomosMoment moment,
|
|
3439
|
+
String sumId, [
|
|
3440
|
+
String group = '',
|
|
3441
|
+
String? session,
|
|
3442
|
+
]) async =>
|
|
3443
|
+
((_asMap(
|
|
3444
|
+
await _call('sumAt', {
|
|
3445
|
+
'commit': moment.id,
|
|
3446
|
+
'sumId': sumId,
|
|
3447
|
+
'group': group,
|
|
3448
|
+
if (session != null) 'session': session,
|
|
3449
|
+
}),
|
|
3450
|
+
)['sum'])
|
|
3451
|
+
as num)
|
|
3188
3452
|
.toInt();
|
|
3189
3453
|
|
|
3190
3454
|
/// The read auth sidecar (private reads + E2E): `keys`/`principal` as top-level call args, or null when
|
|
3191
3455
|
/// neither is present (so the wire is byte-identical to before this existed).
|
|
3192
3456
|
Map<String, Object?>? _readAuthArgs(
|
|
3193
|
-
|
|
3457
|
+
List<NomosScopeKey>? keys,
|
|
3458
|
+
String? principal,
|
|
3459
|
+
) {
|
|
3194
3460
|
final out = <String, Object?>{};
|
|
3195
3461
|
if (principal != null && principal.isNotEmpty) out['principal'] = principal;
|
|
3196
3462
|
if (keys != null && keys.isNotEmpty)
|
|
@@ -3207,33 +3473,41 @@ class NomosBridge {
|
|
|
3207
3473
|
|
|
3208
3474
|
/// AUTHOR-SIDE SEAL: encrypt `plaintext` into its on-chain ciphertext envelope, bound to the scope + #58
|
|
3209
3475
|
/// stable sids (rename-safe). Returns the envelope string the encrypted field value is written as.
|
|
3210
|
-
Future<String> cryptoSealField(
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
'
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3476
|
+
Future<String> cryptoSealField({
|
|
3477
|
+
required String scopeKey,
|
|
3478
|
+
required String scope,
|
|
3479
|
+
required int epoch,
|
|
3480
|
+
required String aggSid,
|
|
3481
|
+
required String fieldSid,
|
|
3482
|
+
required String plaintext,
|
|
3483
|
+
}) async =>
|
|
3484
|
+
(_asMap(
|
|
3485
|
+
await _call('cryptoSealField', {
|
|
3486
|
+
'scopeKey': scopeKey,
|
|
3487
|
+
'scope': scope,
|
|
3488
|
+
'epoch': epoch,
|
|
3489
|
+
'aggSid': aggSid,
|
|
3490
|
+
'fieldSid': fieldSid,
|
|
3491
|
+
'plaintext': plaintext,
|
|
3492
|
+
}),
|
|
3493
|
+
)['envelope'] ??
|
|
3225
3494
|
'')
|
|
3226
3495
|
.toString();
|
|
3227
3496
|
|
|
3228
3497
|
/// FRAMEWORK-INTERNAL `.owned()` key lane. The connected JS holon derives epoch 0 from its
|
|
3229
3498
|
/// already-persisted device credential; generated clients call this without exposing key custody to apps.
|
|
3230
|
-
Future<NomosScopeKey> nomosOwnedScopeKey(
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3499
|
+
Future<NomosScopeKey> nomosOwnedScopeKey(
|
|
3500
|
+
String scope, {
|
|
3501
|
+
String? recordType,
|
|
3502
|
+
String? recordId,
|
|
3503
|
+
}) async {
|
|
3504
|
+
final out = _asMap(
|
|
3505
|
+
await _call('cryptoOwnedScopeKey', {
|
|
3506
|
+
'scope': scope,
|
|
3507
|
+
if (recordType != null && recordId != null)
|
|
3508
|
+
'record': {'type': recordType, 'id': recordId},
|
|
3509
|
+
}),
|
|
3510
|
+
);
|
|
3237
3511
|
return NomosScopeKey(
|
|
3238
3512
|
scope: (out['scope'] ?? scope).toString(),
|
|
3239
3513
|
epoch: (out['epoch'] as num?)?.toInt() ?? 0,
|
|
@@ -3245,30 +3519,38 @@ class NomosBridge {
|
|
|
3245
3519
|
|
|
3246
3520
|
/// Every key epoch this principal currently holds for one owned record. Generated reads use the
|
|
3247
3521
|
/// complete set so history remains readable after rotations, shares and transfers.
|
|
3248
|
-
Future<List<NomosScopeKey>> nomosOwnedScopeKeys(
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3522
|
+
Future<List<NomosScopeKey>> nomosOwnedScopeKeys(
|
|
3523
|
+
String scope, {
|
|
3524
|
+
required String recordType,
|
|
3525
|
+
required String recordId,
|
|
3526
|
+
}) async {
|
|
3527
|
+
final out = _asMap(
|
|
3528
|
+
await _call('cryptoOwnedScopeKeys', {
|
|
3529
|
+
'scope': scope,
|
|
3530
|
+
'record': {'type': recordType, 'id': recordId},
|
|
3531
|
+
}),
|
|
3532
|
+
);
|
|
3254
3533
|
return (out['keys'] as List? ?? const [])
|
|
3255
3534
|
.map((raw) => _asMap(raw))
|
|
3256
|
-
.map(
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
)
|
|
3535
|
+
.map(
|
|
3536
|
+
(key) => NomosScopeKey(
|
|
3537
|
+
scope: (key['scope'] ?? scope).toString(),
|
|
3538
|
+
epoch: (key['epoch'] as num?)?.toInt() ?? 0,
|
|
3539
|
+
key: (key['key'] ?? '').toString(),
|
|
3540
|
+
ownedKeySubject: key['ownedKeySubject']?.toString(),
|
|
3541
|
+
ownedCustody: key['ownedCustody']?.toString(),
|
|
3542
|
+
),
|
|
3543
|
+
)
|
|
3263
3544
|
.toList();
|
|
3264
3545
|
}
|
|
3265
3546
|
|
|
3266
3547
|
/// Make this signed-in principal ready to receive record shares. Generated clients call this while
|
|
3267
3548
|
/// binding; tenant code never handles a key, certificate or identity tuple.
|
|
3268
|
-
Future<void> nomosEnsureOwnedIdentity(
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3549
|
+
Future<void> nomosEnsureOwnedIdentity({
|
|
3550
|
+
required String scope,
|
|
3551
|
+
required String domain,
|
|
3552
|
+
String? domainHash,
|
|
3553
|
+
}) async {
|
|
3272
3554
|
await _call('cryptoOwnedEnsureIdentity', {
|
|
3273
3555
|
'scope': scope,
|
|
3274
3556
|
'domain': domain,
|
|
@@ -3282,39 +3564,49 @@ class NomosBridge {
|
|
|
3282
3564
|
required String domain,
|
|
3283
3565
|
required String domainHash,
|
|
3284
3566
|
required String recoveryPrincipal,
|
|
3285
|
-
}) async =>
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3567
|
+
}) async => _asMap(
|
|
3568
|
+
await _call('cryptoOwnedProtectIdentity', {
|
|
3569
|
+
'scope': scope,
|
|
3570
|
+
'domain': domain,
|
|
3571
|
+
'domainHash': domainHash,
|
|
3572
|
+
'recoveryPrincipal': recoveryPrincipal,
|
|
3573
|
+
}),
|
|
3574
|
+
);
|
|
3292
3575
|
|
|
3293
3576
|
/// Prepare the private framework payload behind generated share/unshare/transfer methods.
|
|
3294
|
-
Future<Map<String, Object?>> nomosPrepareOwnedRecordAction<TAggregate>(
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3577
|
+
Future<Map<String, Object?>> nomosPrepareOwnedRecordAction<TAggregate>({
|
|
3578
|
+
required String action,
|
|
3579
|
+
required String scope,
|
|
3580
|
+
required NomosRef<TAggregate> record,
|
|
3581
|
+
required String peer,
|
|
3582
|
+
}) async => _asMap(
|
|
3583
|
+
await _call('cryptoOwnedPrepareRecordAction', {
|
|
3584
|
+
'action': action,
|
|
3585
|
+
'scope': scope,
|
|
3586
|
+
'record': {
|
|
3587
|
+
'type': NmsInternalNomosRefs.type(record),
|
|
3588
|
+
'id': NmsInternalNomosRefs.id(record),
|
|
3589
|
+
},
|
|
3590
|
+
'peer': peer,
|
|
3591
|
+
}),
|
|
3592
|
+
);
|
|
3308
3593
|
|
|
3309
3594
|
/// FRAMEWORK-INTERNAL business birth: derive the unborn child's owned key from its deterministic address.
|
|
3310
|
-
Future<NomosScopeKey> nomosOwnedBirthScopeKey(
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3595
|
+
Future<NomosScopeKey> nomosOwnedBirthScopeKey(
|
|
3596
|
+
String scope,
|
|
3597
|
+
String namespace,
|
|
3598
|
+
Object? key,
|
|
3599
|
+
String recordType,
|
|
3600
|
+
String recordId,
|
|
3601
|
+
) async {
|
|
3602
|
+
final out = _asMap(
|
|
3603
|
+
await _call('cryptoOwnedBirthScopeKey', {
|
|
3604
|
+
'scope': scope,
|
|
3605
|
+
'namespace': namespace,
|
|
3606
|
+
'key': key,
|
|
3607
|
+
'record': {'type': recordType, 'id': recordId},
|
|
3608
|
+
}),
|
|
3609
|
+
);
|
|
3318
3610
|
return NomosScopeKey(
|
|
3319
3611
|
scope: (out['scope'] ?? scope).toString(),
|
|
3320
3612
|
epoch: (out['epoch'] as num?)?.toInt() ?? 0,
|
|
@@ -3327,64 +3619,88 @@ class NomosBridge {
|
|
|
3327
3619
|
|
|
3328
3620
|
/// FRAMEWORK-INTERNAL: issue an opaque request for this session's current device.
|
|
3329
3621
|
Future<String> nomosOwnedDeviceRequest(String scope) async =>
|
|
3330
|
-
(_asMap(
|
|
3331
|
-
|
|
3622
|
+
(_asMap(
|
|
3623
|
+
await _call('cryptoOwnedDeviceRequest', {'scope': scope}),
|
|
3624
|
+
)['request'] ??
|
|
3332
3625
|
'')
|
|
3333
3626
|
.toString();
|
|
3334
3627
|
|
|
3335
3628
|
/// FRAMEWORK-INTERNAL: validate an opaque request and prepare the atomic signer + custody offer.
|
|
3336
3629
|
Future<Map<String, Object?>> nomosPrepareOwnedDeviceApproval(
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3630
|
+
String request,
|
|
3631
|
+
String scope,
|
|
3632
|
+
) async => _asMap(
|
|
3633
|
+
await _call('cryptoOwnedPrepareApproval', {
|
|
3634
|
+
'request': request,
|
|
3635
|
+
'scope': scope,
|
|
3636
|
+
}),
|
|
3637
|
+
);
|
|
3342
3638
|
|
|
3343
3639
|
/// Wrap a scope key TO a recipient's public key (committer side): `{hpkeEpk, ct}` base64 ciphertext.
|
|
3344
|
-
Future<Map<String, Object?>> cryptoWrapKey(
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3640
|
+
Future<Map<String, Object?>> cryptoWrapKey({
|
|
3641
|
+
required String recipientPub,
|
|
3642
|
+
required String scopeKey,
|
|
3643
|
+
}) async => _asMap(
|
|
3644
|
+
await _call('cryptoWrapKey', {
|
|
3645
|
+
'recipientPub': recipientPub,
|
|
3646
|
+
'scopeKey': scopeKey,
|
|
3647
|
+
}),
|
|
3648
|
+
);
|
|
3348
3649
|
|
|
3349
3650
|
/// Unwrap an on-chain WrappedKey with this device's secret → the base64 scope key (reader side).
|
|
3350
|
-
Future<String> cryptoUnwrapKey(
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
'
|
|
3357
|
-
|
|
3358
|
-
|
|
3651
|
+
Future<String> cryptoUnwrapKey({
|
|
3652
|
+
required String secret,
|
|
3653
|
+
required String hpkeEpk,
|
|
3654
|
+
required String ct,
|
|
3655
|
+
}) async =>
|
|
3656
|
+
(_asMap(
|
|
3657
|
+
await _call('cryptoUnwrapKey', {
|
|
3658
|
+
'secret': secret,
|
|
3659
|
+
'hpkeEpk': hpkeEpk,
|
|
3660
|
+
'ct': ct,
|
|
3661
|
+
}),
|
|
3662
|
+
)['scopeKey'] ??
|
|
3359
3663
|
'')
|
|
3360
3664
|
.toString();
|
|
3361
3665
|
|
|
3362
3666
|
/// THE SPATIAL READ: rows of the declared spatial index's of-type whose geometry
|
|
3363
3667
|
/// intersects [bbox] — an R*Tree probe on the local projection, same row shape as [query].
|
|
3364
3668
|
Future<List<Map<String, dynamic>>> spatialWithin(
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3669
|
+
String spatialId,
|
|
3670
|
+
NomosBbox bbox, [
|
|
3671
|
+
String? session,
|
|
3672
|
+
]) async => _asRows(
|
|
3673
|
+
await _call('spatialWithin', {
|
|
3674
|
+
'spatialId': spatialId,
|
|
3675
|
+
'bbox': bbox.toJson(),
|
|
3676
|
+
if (session != null) 'session': session,
|
|
3677
|
+
}),
|
|
3678
|
+
);
|
|
3372
3679
|
|
|
3373
3680
|
/// A maintained O(1) count / sum.
|
|
3374
|
-
Future<int> count(
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3681
|
+
Future<int> count(
|
|
3682
|
+
String countId, [
|
|
3683
|
+
String group = '',
|
|
3684
|
+
String? session,
|
|
3685
|
+
]) async =>
|
|
3686
|
+
((_asMap(
|
|
3687
|
+
await _call('count', {
|
|
3688
|
+
'countId': countId,
|
|
3689
|
+
'group': group,
|
|
3690
|
+
if (session != null) 'session': session,
|
|
3691
|
+
}),
|
|
3692
|
+
)['count'])
|
|
3693
|
+
as num)
|
|
3381
3694
|
.toInt();
|
|
3382
3695
|
Future<int> sum(String sumId, [String group = '', String? session]) async =>
|
|
3383
|
-
((_asMap(
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3696
|
+
((_asMap(
|
|
3697
|
+
await _call('sum', {
|
|
3698
|
+
'sumId': sumId,
|
|
3699
|
+
'group': group,
|
|
3700
|
+
if (session != null) 'session': session,
|
|
3701
|
+
}),
|
|
3702
|
+
)['sum'])
|
|
3703
|
+
as num)
|
|
3388
3704
|
.toInt();
|
|
3389
3705
|
|
|
3390
3706
|
/// THE PENDING-CONFLICTS READ (Surfaced merge conflicts — the DX layer): the MAINTAINED,
|
|
@@ -3394,35 +3710,43 @@ class NomosBridge {
|
|
|
3394
3710
|
/// DERIVED, NOT DRAINED: a resolution write folding in removes its row by derivation.
|
|
3395
3711
|
/// Empty for a Surfaced-free workspace. (The generated typed client wraps these rows as
|
|
3396
3712
|
/// `PendingConflictRow` and pairs them with the designated `resolve…` helpers.)
|
|
3397
|
-
Future<List<Map<String, dynamic>>> pendingConflicts(
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3713
|
+
Future<List<Map<String, dynamic>>> pendingConflicts([
|
|
3714
|
+
String? session,
|
|
3715
|
+
]) async => _asRows(
|
|
3716
|
+
await _call('pendingConflicts', {if (session != null) 'session': session}),
|
|
3717
|
+
);
|
|
3402
3718
|
|
|
3403
3719
|
/// Framework-filtered pending conflicts for one generated aggregate type. This is the
|
|
3404
3720
|
/// primitive generated clients use; application code never filters the heterogeneous queue.
|
|
3405
|
-
Future<List<Map<String, dynamic>>> pendingConflictsFor(
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3721
|
+
Future<List<Map<String, dynamic>>> pendingConflictsFor(
|
|
3722
|
+
String aggregateType, [
|
|
3723
|
+
String? session,
|
|
3724
|
+
]) async => _asRows(
|
|
3725
|
+
await _call('pendingConflicts', {
|
|
3726
|
+
'aggregateType': aggregateType,
|
|
3727
|
+
if (session != null) 'session': session,
|
|
3728
|
+
}),
|
|
3729
|
+
);
|
|
3411
3730
|
|
|
3412
3731
|
/// Counts-only workspace summary over the maintained conflict index.
|
|
3413
|
-
Future<Map<String, Object?>> pendingConflictSummary(
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3732
|
+
Future<Map<String, Object?>> pendingConflictSummary([
|
|
3733
|
+
String? session,
|
|
3734
|
+
]) async => _asMap(
|
|
3735
|
+
await _call('pendingConflictSummary', {
|
|
3736
|
+
if (session != null) 'session': session,
|
|
3737
|
+
}),
|
|
3738
|
+
);
|
|
3418
3739
|
|
|
3419
3740
|
/// Push local writes → the session branch → cloud admission, and pull canonical main back.
|
|
3420
|
-
Future<Map<String, Object?>> sync(
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3741
|
+
Future<Map<String, Object?>> sync({
|
|
3742
|
+
bool admit = true,
|
|
3743
|
+
String? session,
|
|
3744
|
+
}) async => _asMap(
|
|
3745
|
+
await _call('sync', {
|
|
3746
|
+
'admit': admit,
|
|
3747
|
+
if (session != null) 'session': session,
|
|
3748
|
+
}),
|
|
3749
|
+
);
|
|
3426
3750
|
|
|
3427
3751
|
/// PERSIST: a base64 snapshot of the whole local holon (the bare nomos.git incl. un-synced writes + the
|
|
3428
3752
|
/// DLQ). Store it on disk and pass it back as `connect(restoreFrom:)` next launch — the app then boots from
|
|
@@ -3439,12 +3763,15 @@ class NomosBridge {
|
|
|
3439
3763
|
/// local write flushes the up leg at once; `fallbackMs` is only a just-in-case safety pull (default 60s).
|
|
3440
3764
|
/// Each round arrives on [syncEvents] as `{ event:'sync', reason, result }`; socket state as `{event:'link'}`.
|
|
3441
3765
|
/// Call once after [connect] — then the app never polls.
|
|
3442
|
-
Future<void> autoSync(
|
|
3443
|
-
|
|
3766
|
+
Future<void> autoSync({
|
|
3767
|
+
int? debounceMs,
|
|
3768
|
+
int? fallbackMs,
|
|
3769
|
+
String? signalId,
|
|
3770
|
+
}) async {
|
|
3444
3771
|
await _call('autoSync', {
|
|
3445
3772
|
if (debounceMs != null) 'debounceMs': debounceMs,
|
|
3446
3773
|
if (fallbackMs != null) 'fallbackMs': fallbackMs,
|
|
3447
|
-
if (signalId != null) 'signalId': signalId
|
|
3774
|
+
if (signalId != null) 'signalId': signalId,
|
|
3448
3775
|
});
|
|
3449
3776
|
final workspace = _sessionName;
|
|
3450
3777
|
if (workspace != null) _eventPlane._autoSyncSessions.add(workspace);
|
|
@@ -3482,16 +3809,16 @@ class NomosBridge {
|
|
|
3482
3809
|
: _eventPlane._currentSessionStatus(session);
|
|
3483
3810
|
}
|
|
3484
3811
|
|
|
3485
|
-
Stream<SyncStatus> _sessionSyncStatus(String session) =>
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3812
|
+
Stream<SyncStatus> _sessionSyncStatus(String session) => _eventPlane
|
|
3813
|
+
._syncStatusBySession
|
|
3814
|
+
.putIfAbsent(
|
|
3815
|
+
session,
|
|
3816
|
+
() => StreamController<SyncStatus>.broadcast(
|
|
3817
|
+
onListen: () => _eventPlane._startSessionStatusPoll(session),
|
|
3818
|
+
onCancel: _eventPlane._stopStatusPollIfUnused,
|
|
3819
|
+
),
|
|
3820
|
+
)
|
|
3821
|
+
.stream;
|
|
3495
3822
|
|
|
3496
3823
|
SyncStatus _currentSessionStatus(String session) =>
|
|
3497
3824
|
_eventPlane._statusBySession[session] ?? const SyncStatus();
|
|
@@ -3534,14 +3861,17 @@ class NomosBridge {
|
|
|
3534
3861
|
phase = SyncPhase.error;
|
|
3535
3862
|
} else if (statuses.any((status) => status.phase == SyncPhase.offline)) {
|
|
3536
3863
|
phase = SyncPhase.offline;
|
|
3537
|
-
} else if (statuses
|
|
3538
|
-
|
|
3864
|
+
} else if (statuses.any(
|
|
3865
|
+
(status) => status.phase == SyncPhase.transferring,
|
|
3866
|
+
)) {
|
|
3539
3867
|
phase = SyncPhase.transferring;
|
|
3540
3868
|
} else if (statuses.any(
|
|
3541
|
-
|
|
3869
|
+
(status) => status.phase == SyncPhase.behind || status.behind > 0,
|
|
3870
|
+
)) {
|
|
3542
3871
|
phase = SyncPhase.behind;
|
|
3543
3872
|
} else if (statuses.any(
|
|
3544
|
-
|
|
3873
|
+
(status) => status.phase == SyncPhase.syncing || status.ahead > 0,
|
|
3874
|
+
)) {
|
|
3545
3875
|
phase = SyncPhase.syncing;
|
|
3546
3876
|
} else {
|
|
3547
3877
|
phase = SyncPhase.idle;
|
|
@@ -3553,9 +3883,10 @@ class NomosBridge {
|
|
|
3553
3883
|
.map((status) => status.lastSyncedAt)
|
|
3554
3884
|
.whereType<DateTime>()
|
|
3555
3885
|
.fold<DateTime?>(
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3886
|
+
null,
|
|
3887
|
+
(oldest, value) =>
|
|
3888
|
+
oldest == null || value.isBefore(oldest) ? value : oldest,
|
|
3889
|
+
);
|
|
3559
3890
|
final error = statuses
|
|
3560
3891
|
.map((status) => status.lastError)
|
|
3561
3892
|
.whereType<String>()
|
|
@@ -3565,17 +3896,19 @@ class NomosBridge {
|
|
|
3565
3896
|
.whereType<TransferProgress>()
|
|
3566
3897
|
.firstOrNull;
|
|
3567
3898
|
|
|
3568
|
-
_pushStatus(
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3899
|
+
_pushStatus(
|
|
3900
|
+
SyncStatus(
|
|
3901
|
+
phase: phase,
|
|
3902
|
+
ahead: total((status) => status.ahead),
|
|
3903
|
+
behind: total((status) => status.behind),
|
|
3904
|
+
saved: total((status) => status.saved),
|
|
3905
|
+
onMain: total((status) => status.onMain),
|
|
3906
|
+
deadLetters: total((status) => status.deadLetters),
|
|
3907
|
+
transfer: transfer,
|
|
3908
|
+
lastSyncedAt: lastSynced,
|
|
3909
|
+
lastError: error,
|
|
3910
|
+
),
|
|
3911
|
+
);
|
|
3579
3912
|
}
|
|
3580
3913
|
|
|
3581
3914
|
void _startStatusPoll() {
|
|
@@ -3609,8 +3942,9 @@ class NomosBridge {
|
|
|
3609
3942
|
|
|
3610
3943
|
void _stopStatusPollIfUnused() {
|
|
3611
3944
|
if (_syncStatusCtrl.hasListener ||
|
|
3612
|
-
_syncStatusBySession.values
|
|
3613
|
-
|
|
3945
|
+
_syncStatusBySession.values.any(
|
|
3946
|
+
(controller) => controller.hasListener,
|
|
3947
|
+
)) {
|
|
3614
3948
|
return;
|
|
3615
3949
|
}
|
|
3616
3950
|
_stopStatusPoll();
|
|
@@ -3682,13 +4016,14 @@ class NomosBridge {
|
|
|
3682
4016
|
// Counters prove custody debt, but they do not prove that a failed or
|
|
3683
4017
|
// offline transport recovered. Only a subsequent successful sync/link
|
|
3684
4018
|
// event may clear those lifecycle states.
|
|
3685
|
-
final phase =
|
|
4019
|
+
final phase =
|
|
4020
|
+
current.transfer != null ||
|
|
3686
4021
|
current.phase == SyncPhase.error ||
|
|
3687
4022
|
current.phase == SyncPhase.offline
|
|
3688
4023
|
? current.phase
|
|
3689
4024
|
: (behind > 0
|
|
3690
|
-
|
|
3691
|
-
|
|
4025
|
+
? SyncPhase.behind
|
|
4026
|
+
: (ahead > 0 ? SyncPhase.syncing : SyncPhase.idle));
|
|
3692
4027
|
_pushSessionStatus(
|
|
3693
4028
|
session,
|
|
3694
4029
|
current.copyWith(
|
|
@@ -3705,7 +4040,8 @@ class NomosBridge {
|
|
|
3705
4040
|
_foldApplicationStatusFromSessions();
|
|
3706
4041
|
} catch (e) {
|
|
3707
4042
|
_pushStatus(
|
|
3708
|
-
|
|
4043
|
+
_status.copyWith(phase: SyncPhase.error, lastError: e.toString()),
|
|
4044
|
+
);
|
|
3709
4045
|
}
|
|
3710
4046
|
}
|
|
3711
4047
|
|
|
@@ -3758,7 +4094,7 @@ class NomosBridge {
|
|
|
3758
4094
|
'kind': kind,
|
|
3759
4095
|
't0': t0,
|
|
3760
4096
|
if (head != null && head.isNotEmpty) 'head': head,
|
|
3761
|
-
}
|
|
4097
|
+
},
|
|
3762
4098
|
},
|
|
3763
4099
|
{
|
|
3764
4100
|
'event': 'span',
|
|
@@ -3768,7 +4104,7 @@ class NomosBridge {
|
|
|
3768
4104
|
'id': sid,
|
|
3769
4105
|
't1': t1,
|
|
3770
4106
|
if (head != null && head.isNotEmpty) 'head': head,
|
|
3771
|
-
}
|
|
4107
|
+
},
|
|
3772
4108
|
},
|
|
3773
4109
|
]) {
|
|
3774
4110
|
plane._spanBuffer.add(m);
|
|
@@ -3781,9 +4117,7 @@ class NomosBridge {
|
|
|
3781
4117
|
|
|
3782
4118
|
/// Ledger size (commits) + branch-vs-main (ahead) + projection bytes — the dashboard's slow state.
|
|
3783
4119
|
Future<Map<String, Object?>> metrics([String? session]) async =>
|
|
3784
|
-
_asMap(await _call('metrics', {
|
|
3785
|
-
if (session != null) 'session': session,
|
|
3786
|
-
}));
|
|
4120
|
+
_asMap(await _call('metrics', {if (session != null) 'session': session}));
|
|
3787
4121
|
|
|
3788
4122
|
/// TIER-2 SYNC SIZE — how many bytes this workspace would transfer to catch up to main, WITHOUT downloading
|
|
3789
4123
|
/// them (a HEAD on the serving pack; the host answers Content-Length only, cached ~15s). Sum across your open
|
|
@@ -3799,28 +4133,42 @@ class NomosBridge {
|
|
|
3799
4133
|
|
|
3800
4134
|
/// Reactive local read: a stream of row-sets that re-emits on every local change. Cancelling the
|
|
3801
4135
|
/// subscription tells the runner to drop the watch.
|
|
3802
|
-
Stream<List<dynamic>> watch(
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
4136
|
+
Stream<List<dynamic>> watch(
|
|
4137
|
+
String queryId, [
|
|
4138
|
+
Map<String, Object?> params = const {},
|
|
4139
|
+
String? session,
|
|
4140
|
+
String? principal,
|
|
4141
|
+
int? intervalMs,
|
|
4142
|
+
]) => _watchStream(
|
|
4143
|
+
kind: 'query',
|
|
4144
|
+
queryId: queryId,
|
|
4145
|
+
params: params,
|
|
4146
|
+
session: session,
|
|
4147
|
+
principal: principal,
|
|
4148
|
+
intervalMs: intervalMs,
|
|
4149
|
+
);
|
|
4150
|
+
Stream<List<dynamic>> watchById(
|
|
4151
|
+
String id, [
|
|
4152
|
+
String? session,
|
|
4153
|
+
String? principal,
|
|
4154
|
+
]) => _watchStream(
|
|
4155
|
+
kind: 'byId',
|
|
4156
|
+
id: id,
|
|
4157
|
+
session: session,
|
|
4158
|
+
principal: principal,
|
|
4159
|
+
);
|
|
3818
4160
|
|
|
3819
4161
|
/// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
|
|
3820
|
-
Stream<List<dynamic>> watchSpatial(
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
4162
|
+
Stream<List<dynamic>> watchSpatial(
|
|
4163
|
+
String spatialId,
|
|
4164
|
+
NomosBbox bbox, [
|
|
4165
|
+
String? session,
|
|
4166
|
+
]) => _watchStream(
|
|
4167
|
+
kind: 'spatial',
|
|
4168
|
+
spatialId: spatialId,
|
|
4169
|
+
bbox: bbox,
|
|
4170
|
+
session: session,
|
|
4171
|
+
);
|
|
3824
4172
|
|
|
3825
4173
|
/// Reactive PENDING-CONFLICTS read: re-emits the maintained Surfaced "decisions needed"
|
|
3826
4174
|
/// rows whenever the index changes (derived, not drained — a resolution write clears its row).
|
|
@@ -3831,12 +4179,14 @@ class NomosBridge {
|
|
|
3831
4179
|
/// (its baked `…InterfaceJson`, decoded) against the workspace's ON-LEDGER offer (auto-sourced).
|
|
3832
4180
|
/// `{ ok, missing[], incompatible[] }` — names the exact directive/aggregate/field, not a hash.
|
|
3833
4181
|
Future<Map<String, Object?>> checkCompatibility(
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
4182
|
+
Map<String, Object?> requirement, [
|
|
4183
|
+
Map<String, Object?>? offer,
|
|
4184
|
+
]) async => _asMap(
|
|
4185
|
+
await _call('checkCompatibility', {
|
|
4186
|
+
'requirement': requirement,
|
|
4187
|
+
if (offer != null) 'offer': offer,
|
|
4188
|
+
}),
|
|
4189
|
+
);
|
|
3840
4190
|
|
|
3841
4191
|
/// THE COMPAT SUPERPOWER (#72b): compute the LIVE per-member verdict for this client's baked
|
|
3842
4192
|
/// [compat] manifest + structural [requirement] against the workspace's on-ledger offer. The
|
|
@@ -3844,14 +4194,19 @@ class NomosBridge {
|
|
|
3844
4194
|
/// CLIENT-SIDE at write time ([LawMismatchError] surfaced through `offerDirective`) — the JS
|
|
3845
4195
|
/// runtime holds the verdict + the pre-call guard, and a Dart offer through this bridge is guarded
|
|
3846
4196
|
/// too. Returns the verdict (also cached on [compat]) with per-member `{ ok, reason? }`.
|
|
3847
|
-
Future<NomosCompat?> checkCompat(
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
4197
|
+
Future<NomosCompat?> checkCompat(
|
|
4198
|
+
Map<String, Object?> compat, [
|
|
4199
|
+
Map<String, Object?>? requirement,
|
|
4200
|
+
]) async {
|
|
4201
|
+
final r = _asMap(
|
|
4202
|
+
await _call('checkCompat', {
|
|
4203
|
+
'compat': compat,
|
|
4204
|
+
if (requirement != null) 'requirement': requirement,
|
|
4205
|
+
}),
|
|
4206
|
+
);
|
|
4207
|
+
final v = NomosCompat.fromJson(
|
|
4208
|
+
(r['compat'] as Map?)?.cast<String, Object?>(),
|
|
4209
|
+
);
|
|
3855
4210
|
if (v != null) {
|
|
3856
4211
|
_eventPlane._lastCompat = v;
|
|
3857
4212
|
// a session's verdict is per-workspace state on the PLANE (so `session.compat` reads it back).
|
|
@@ -3865,26 +4220,28 @@ class NomosBridge {
|
|
|
3865
4220
|
/// the opened session in one call — `final home = await bridge.open(workspace: 'home-…');` then
|
|
3866
4221
|
/// `HomeClient(home)`. The bare [NomosBridge] methods remain the ONE-workspace sugar over an inferred
|
|
3867
4222
|
/// default session; an app that opens a second workspace should hold handles, not call the plane.
|
|
3868
|
-
Future<NomosSession> open(
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
4223
|
+
Future<NomosSession> open({
|
|
4224
|
+
String? cloud,
|
|
4225
|
+
required String workspace,
|
|
4226
|
+
String? clientId,
|
|
4227
|
+
String? authToken,
|
|
4228
|
+
String? authorSecret,
|
|
4229
|
+
String? restoreFrom,
|
|
4230
|
+
bool allowUnborn = false,
|
|
4231
|
+
bool recover = false,
|
|
4232
|
+
String? session,
|
|
4233
|
+
}) async {
|
|
3878
4234
|
await connect(
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
4235
|
+
cloud: cloud,
|
|
4236
|
+
workspace: workspace,
|
|
4237
|
+
clientId: clientId,
|
|
4238
|
+
authToken: authToken,
|
|
4239
|
+
authorSecret: authorSecret,
|
|
4240
|
+
restoreFrom: restoreFrom,
|
|
4241
|
+
allowUnborn: allowUnborn,
|
|
4242
|
+
recover: recover,
|
|
4243
|
+
session: session,
|
|
4244
|
+
);
|
|
3888
4245
|
return this.session(session ?? workspace);
|
|
3889
4246
|
}
|
|
3890
4247
|
|
|
@@ -3924,24 +4281,30 @@ class NomosBridge {
|
|
|
3924
4281
|
/// workspace's installed law changes (another peer installs/upgrades/retires a domain and it
|
|
3925
4282
|
/// syncs in). The verdict carries `offerKnown`. The app degrades/prompts on `ok:false`.
|
|
3926
4283
|
Stream<Map<String, Object?>> watchCompatibility(
|
|
3927
|
-
|
|
3928
|
-
|
|
4284
|
+
Map<String, Object?> requirement, [
|
|
4285
|
+
String? session,
|
|
4286
|
+
]) {
|
|
3929
4287
|
// Controllers live on the EVENT PLANE (where _onMessage dispatches); the registration `_call` stays
|
|
3930
4288
|
// THIS instance's (a session's override stamps `args.session`) — see [_eventPlane].
|
|
3931
4289
|
final subId = 'c${++_eventPlane._seq}';
|
|
3932
4290
|
late final StreamController<Map<String, Object?>> ctrl;
|
|
3933
4291
|
ctrl = StreamController<Map<String, Object?>>(
|
|
3934
4292
|
onListen: () {
|
|
3935
|
-
unawaited(
|
|
3936
|
-
'
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
4293
|
+
unawaited(
|
|
4294
|
+
_call('watch', {
|
|
4295
|
+
'subId': subId,
|
|
4296
|
+
'kind': 'compat',
|
|
4297
|
+
'requirement': requirement,
|
|
4298
|
+
if (session != null) 'session': session,
|
|
4299
|
+
}).then<void>(
|
|
4300
|
+
(_) {},
|
|
4301
|
+
onError: (Object error, StackTrace stackTrace) {
|
|
4302
|
+
if (!ctrl.isClosed && ctrl.hasListener) {
|
|
4303
|
+
ctrl.addError(error, stackTrace);
|
|
4304
|
+
}
|
|
4305
|
+
},
|
|
4306
|
+
),
|
|
4307
|
+
);
|
|
3945
4308
|
},
|
|
3946
4309
|
onCancel: () {
|
|
3947
4310
|
_eventPlane._compatWatches.remove(subId);
|
|
@@ -3955,42 +4318,48 @@ class NomosBridge {
|
|
|
3955
4318
|
return ctrl.stream;
|
|
3956
4319
|
}
|
|
3957
4320
|
|
|
3958
|
-
Stream<List<dynamic>> _watchStream(
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
4321
|
+
Stream<List<dynamic>> _watchStream({
|
|
4322
|
+
required String kind,
|
|
4323
|
+
String? queryId,
|
|
4324
|
+
Map<String, Object?>? params,
|
|
4325
|
+
String? id,
|
|
4326
|
+
String? spatialId,
|
|
4327
|
+
NomosBbox? bbox,
|
|
4328
|
+
String? base,
|
|
4329
|
+
int? limit,
|
|
4330
|
+
String? session,
|
|
4331
|
+
String? principal,
|
|
4332
|
+
int? intervalMs,
|
|
4333
|
+
}) {
|
|
3970
4334
|
// Controllers live on the EVENT PLANE (where _onMessage dispatches); the registration `_call` stays
|
|
3971
4335
|
// THIS instance's (a session's override stamps `args.session`) — see [_eventPlane].
|
|
3972
4336
|
final subId = 'w${++_eventPlane._seq}';
|
|
3973
4337
|
late final StreamController<List<dynamic>> ctrl;
|
|
3974
4338
|
ctrl = StreamController<List<dynamic>>(
|
|
3975
4339
|
onListen: () {
|
|
3976
|
-
unawaited(
|
|
3977
|
-
'
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
4340
|
+
unawaited(
|
|
4341
|
+
_call('watch', {
|
|
4342
|
+
'subId': subId,
|
|
4343
|
+
'kind': kind,
|
|
4344
|
+
if (queryId != null) 'queryId': queryId,
|
|
4345
|
+
if (params != null) 'params': params,
|
|
4346
|
+
if (id != null) 'id': id,
|
|
4347
|
+
if (spatialId != null) 'spatialId': spatialId,
|
|
4348
|
+
if (bbox != null) 'bbox': bbox.toJson(),
|
|
4349
|
+
if (base != null) 'base': base,
|
|
4350
|
+
if (limit != null) 'limit': limit,
|
|
4351
|
+
if (principal != null) 'principal': principal,
|
|
4352
|
+
if (intervalMs != null) 'intervalMs': intervalMs,
|
|
4353
|
+
if (session != null) 'session': session,
|
|
4354
|
+
}).then<void>(
|
|
4355
|
+
(_) {},
|
|
4356
|
+
onError: (Object error, StackTrace stackTrace) {
|
|
4357
|
+
if (!ctrl.isClosed && ctrl.hasListener) {
|
|
4358
|
+
ctrl.addError(error, stackTrace);
|
|
4359
|
+
}
|
|
4360
|
+
},
|
|
4361
|
+
),
|
|
4362
|
+
);
|
|
3994
4363
|
},
|
|
3995
4364
|
onCancel: () {
|
|
3996
4365
|
_eventPlane._watches.remove(subId);
|
|
@@ -4017,11 +4386,13 @@ class NomosBridge {
|
|
|
4017
4386
|
}
|
|
4018
4387
|
await _sub.cancel();
|
|
4019
4388
|
final pending = _pending.entries
|
|
4020
|
-
.map(
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4389
|
+
.map(
|
|
4390
|
+
(entry) => (
|
|
4391
|
+
completer: entry.value,
|
|
4392
|
+
trace: _pendingTraces[entry.key] ?? const NomosTraceContext(),
|
|
4393
|
+
operation: _pendingOps[entry.key],
|
|
4394
|
+
),
|
|
4395
|
+
)
|
|
4025
4396
|
.toList(growable: false);
|
|
4026
4397
|
_pending.clear();
|
|
4027
4398
|
_pendingOps.clear();
|
|
@@ -4031,10 +4402,11 @@ class NomosBridge {
|
|
|
4031
4402
|
if (!c.isCompleted) {
|
|
4032
4403
|
final operation = entry.operation ?? 'unknown';
|
|
4033
4404
|
final info = NomosErrorInfo.legacy(
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4405
|
+
'bridge disposed while $operation was pending',
|
|
4406
|
+
code: 'nomos.bridge.disposed',
|
|
4407
|
+
operation: entry.operation,
|
|
4408
|
+
trace: entry.trace,
|
|
4409
|
+
);
|
|
4038
4410
|
c.completeError(nomosErrorFromInfo(info));
|
|
4039
4411
|
}
|
|
4040
4412
|
}
|
|
@@ -4132,15 +4504,18 @@ class NomosSession extends NomosBridge {
|
|
|
4132
4504
|
/// An acquired session mounts on the SHARED plane — delegate to the parent (the plane owns the
|
|
4133
4505
|
/// single-flight registry; a session never owns mounts).
|
|
4134
4506
|
@override
|
|
4135
|
-
Future<NomosSession> acquireSession(
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4507
|
+
Future<NomosSession> acquireSession(
|
|
4508
|
+
String workspace, {
|
|
4509
|
+
Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
|
|
4510
|
+
String? authorSecret,
|
|
4511
|
+
String? inheritedPrincipal,
|
|
4512
|
+
}) {
|
|
4139
4513
|
final retained = parent._sessionCloudAccess[this.workspace];
|
|
4140
4514
|
final modelChange = parent._sessionModelChangeHandlers[this.workspace];
|
|
4141
4515
|
final opened = parent.acquireSession(
|
|
4142
4516
|
workspace,
|
|
4143
|
-
ifAbsentCredentials:
|
|
4517
|
+
ifAbsentCredentials:
|
|
4518
|
+
ifAbsentCredentials ??
|
|
4144
4519
|
(retained == null ? null : retained.credentials),
|
|
4145
4520
|
authorSecret:
|
|
4146
4521
|
authorSecret ?? parent._sessionAuthorSecrets[this.workspace],
|
|
@@ -4174,7 +4549,8 @@ class NomosSession extends NomosBridge {
|
|
|
4174
4549
|
final source = _sessionName;
|
|
4175
4550
|
if (source == null || source.isEmpty) {
|
|
4176
4551
|
throw StateError(
|
|
4177
|
-
|
|
4552
|
+
'Generated child model inheritance needs a bound parent session.',
|
|
4553
|
+
);
|
|
4178
4554
|
}
|
|
4179
4555
|
await _call('stageGeneratedModelFromParent', {
|
|
4180
4556
|
'sourceSession': source,
|
|
@@ -4193,7 +4569,8 @@ class NomosSession extends NomosBridge {
|
|
|
4193
4569
|
final workspace = _sessionName;
|
|
4194
4570
|
if (workspace == null || workspace.isEmpty) {
|
|
4195
4571
|
throw StateError(
|
|
4196
|
-
|
|
4572
|
+
'Model change handling needs a bound application session.',
|
|
4573
|
+
);
|
|
4197
4574
|
}
|
|
4198
4575
|
_eventPlane._sessionModelChangeHandlers[workspace] = handler;
|
|
4199
4576
|
}
|
|
@@ -4220,32 +4597,41 @@ class NomosSession extends NomosBridge {
|
|
|
4220
4597
|
}) {
|
|
4221
4598
|
if (cloud.trim().isEmpty) {
|
|
4222
4599
|
throw ArgumentError.value(
|
|
4223
|
-
|
|
4600
|
+
cloud,
|
|
4601
|
+
'cloud',
|
|
4602
|
+
'Nomos cloud URL cannot be empty',
|
|
4603
|
+
);
|
|
4224
4604
|
}
|
|
4225
|
-
parent._sessionCloudAccess[workspace] =
|
|
4226
|
-
|
|
4605
|
+
parent._sessionCloudAccess[workspace] = _NomosRetainedCloudAccess(
|
|
4606
|
+
cloud: cloud,
|
|
4607
|
+
signIn: signIn,
|
|
4608
|
+
);
|
|
4227
4609
|
}
|
|
4228
4610
|
|
|
4229
4611
|
/// A followed ref mounts on the SHARED plane — delegate to the parent (a session never owns mounts).
|
|
4230
4612
|
@override
|
|
4231
|
-
Future<NomosSession> follow<TAggregate>(
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4613
|
+
Future<NomosSession> follow<TAggregate>(
|
|
4614
|
+
NomosRef<TAggregate> ref, {
|
|
4615
|
+
String? cloud,
|
|
4616
|
+
String? clientId,
|
|
4617
|
+
String? authToken,
|
|
4618
|
+
bool allowUnborn = false,
|
|
4619
|
+
String? authorSecret,
|
|
4620
|
+
}) => parent.follow(
|
|
4621
|
+
ref,
|
|
4622
|
+
cloud: cloud,
|
|
4623
|
+
clientId: clientId,
|
|
4624
|
+
authToken: authToken,
|
|
4625
|
+
allowUnborn: allowUnborn,
|
|
4626
|
+
authorSecret: authorSecret ?? parent._sessionAuthorSecrets[workspace],
|
|
4627
|
+
);
|
|
4244
4628
|
|
|
4245
4629
|
@override
|
|
4246
4630
|
Future<Object?> _call(String op, [Map<String, Object?> args = const {}]) =>
|
|
4247
|
-
parent._call(
|
|
4248
|
-
|
|
4631
|
+
parent._call(
|
|
4632
|
+
op,
|
|
4633
|
+
args.containsKey('session') ? args : {...args, 'session': workspace},
|
|
4634
|
+
);
|
|
4249
4635
|
|
|
4250
4636
|
// The parent owns the ONE message loop + lifecycle; a session never subscribes, never signals ready of its
|
|
4251
4637
|
// own, and never tears the plane down (the harness owns `parent.dispose()`).
|
|
@@ -4259,18 +4645,22 @@ class NomosSession extends NomosBridge {
|
|
|
4259
4645
|
/// tuple throws [ParentSignerEnrollmentMissing] (its `recommendedAction` names the ONE remedy —
|
|
4260
4646
|
/// [ensureParentSignerEnrollment]); any other failing premise throws [AuthzRefusedError] carrying
|
|
4261
4647
|
/// the evaluated {workspace, actor, keyHash, object, relation, cause}. Nothing is authored.
|
|
4262
|
-
Future<Map<String, Object?>> canAuthorTyped(
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4648
|
+
Future<Map<String, Object?>> canAuthorTyped(
|
|
4649
|
+
String directiveId, {
|
|
4650
|
+
String? ws,
|
|
4651
|
+
String? principal,
|
|
4652
|
+
String? publicKey,
|
|
4653
|
+
String? keyHash,
|
|
4654
|
+
String? authToken,
|
|
4655
|
+
}) async {
|
|
4656
|
+
final v = await canAuthor(
|
|
4657
|
+
directiveId,
|
|
4658
|
+
ws: ws,
|
|
4659
|
+
principal: principal,
|
|
4660
|
+
publicKey: publicKey,
|
|
4661
|
+
keyHash: keyHash,
|
|
4662
|
+
authToken: authToken,
|
|
4663
|
+
);
|
|
4274
4664
|
if (v['ok'] == true) return v;
|
|
4275
4665
|
final cause = v['cause']?.toString();
|
|
4276
4666
|
final target = v['ws']?.toString() ?? ws ?? workspace;
|
|
@@ -4327,11 +4717,11 @@ class _BridgeForeignWorkspaceReader implements NomosForeignWorkspaceReader {
|
|
|
4327
4717
|
// workspace reads empty (→ ForeignNotFound) rather than throwing. A genuinely unreachable cloud throws,
|
|
4328
4718
|
// which resolveResult classifies as ForeignUnreachable.
|
|
4329
4719
|
Future<void> _ensureMounted() => _mounting ??= _bridge.connect(
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4720
|
+
cloud: _cloud,
|
|
4721
|
+
workspace: _workspace,
|
|
4722
|
+
session: _workspace,
|
|
4723
|
+
allowUnborn: true,
|
|
4724
|
+
);
|
|
4335
4725
|
|
|
4336
4726
|
@override
|
|
4337
4727
|
Future<List<Map<String, dynamic>>> queryById(String id) async {
|
|
@@ -4381,8 +4771,9 @@ class _BridgeForeignWorkspaceReader implements NomosForeignWorkspaceReader {
|
|
|
4381
4771
|
String? nomosInternalResidencyRequiredWorkspace(Object error) {
|
|
4382
4772
|
final text = error.toString();
|
|
4383
4773
|
if (!text.contains('foreign-read-not-ready')) return null;
|
|
4384
|
-
final match =
|
|
4385
|
-
|
|
4774
|
+
final match = RegExp(
|
|
4775
|
+
r"source workspace '([^']+)' is not resident",
|
|
4776
|
+
).firstMatch(text);
|
|
4386
4777
|
final workspace = match?.group(1)?.trim();
|
|
4387
4778
|
return (workspace == null || workspace.isEmpty) ? null : workspace;
|
|
4388
4779
|
}
|