@githolon/dsl 0.101.6 → 0.102.1
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 +13 -0
- package/dart/lib/nomos_client.dart +3 -52
- package/dart/lib/src/dev_cloud.dart +5 -0
- package/dart/lib/src/home.dart +10 -15
- package/dart/lib/src/nms_conformance_runtime.dart +12 -0
- package/dart/lib/src/nms_generated_runtime.dart +29 -0
- package/dart/lib/src/nms_test_environment.dart +8 -0
- package/dart/lib/src/node_runner_transport.dart +56 -18
- package/dart/lib/src/provider.dart +5 -1
- package/dart/lib/src/record_origin.dart +55 -0
- package/dart/lib/src/runner/node-entry.mjs +9105 -0
- package/dart/lib/src/runner/offline-install.mjs +4729 -0
- package/dart/lib/src/session_bound.dart +23 -10
- package/dart/lib/src/source_trust.dart +51 -0
- package/dart/lib/src/test_harness.dart +490 -58
- package/dart/lib/src/webview_bridge.dart +167 -58
- package/dart/lib/src/workspace_name.dart +7 -1
- package/dart/pubspec.lock +3 -3
- package/dart/pubspec.yaml +4 -2
- package/dart/test/acquire_session_credentials_test.dart +34 -17
- package/dart/test/acquire_session_test.dart +109 -24
- package/dart/test/adv_realm_interference_test.dart +227 -99
- package/dart/test/bundle_version_pairing_test.dart +1 -1
- package/dart/test/cloud_bound_topology_test.dart +1 -1
- package/dart/test/cluster_attested_test.dart +52 -35
- package/dart/test/cluster_births_test.dart +5 -2
- package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
- package/dart/test/cluster_local_autosync_test.dart +5 -2
- package/dart/test/cluster_scale_test.dart +5 -2
- package/dart/test/compat_test.dart +1 -1
- package/dart/test/dlq_session_surface_test.dart +2 -1
- package/dart/test/errors_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +78 -51
- package/dart/test/four_layer_nested_topology_test.dart +1 -1
- package/dart/test/harness_phase_timeout_test.dart +1 -1
- package/dart/test/home_parity_test.dart +9 -5
- package/dart/test/local_sync_inert_test.dart +22 -14
- package/dart/test/node_runner_transport_test.dart +9 -5
- package/dart/test/null_slot_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +5 -2
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +2 -1
- package/dart/test/session_bound_test.dart +92 -119
- package/dart/test/stale_bundle_test.dart +1 -1
- package/dart/test/test_harness_test.dart +77 -22
- package/dart/test/vo_read_typing_test.dart +32 -14
- package/dart/test/watch_after_offer_stall_test.dart +2 -1
- package/dart/test/webview_bridge_test.dart +17 -4
- package/dart/test/wire_test.dart +1 -1
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +7 -6
- package/dart_types/lib/src/activity.dart +23 -13
- package/dart_types/lib/src/foreign_ref.dart +61 -112
- package/dart_types/lib/src/invitation.dart +96 -0
- package/dart_types/lib/src/nomos_ref.dart +97 -59
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +125 -19
- package/package.json +7 -3
- package/runtime/generated-application-host.js +8851 -0
- package/src/activity.ts +2 -2
- package/src/aggregate.ts +24 -1
- package/src/application_composition.ts +282 -0
- package/src/application_topology.ts +151 -0
- package/src/attested_core.ts +2 -2
- package/src/authoring_context.ts +9 -0
- package/src/birth_meter_ensure.ts +1 -1
- package/src/births.ts +53 -5
- package/src/build_package.ts +79 -71
- package/src/business_birth.ts +93 -10
- package/src/clear_directive.ts +3 -1
- package/src/codegen_dart.ts +369 -420
- package/src/codegen_dart_application.ts +1508 -0
- package/src/codegen_dart_client.ts +156 -95
- package/src/codegen_dart_scenario.ts +299 -0
- package/src/codegen_openapi.ts +8 -3
- package/src/codegen_proof.ts +1113 -1137
- package/src/codegen_provider_dart.ts +12 -1
- package/src/codegen_ts.ts +1176 -496
- package/src/compat_package_main.ts +22 -10
- package/src/compile_package_main.ts +1536 -337
- package/src/contract.ts +10 -4
- package/src/dart_barrel_guard.ts +5 -1
- package/src/describe_model.ts +13 -5
- package/src/directive.ts +244 -5
- package/src/engine_entry.ts +56 -13
- package/src/engine_javascript.ts +31 -0
- package/src/evolve_preview.ts +3 -12
- package/src/framework/application_home.ts +24 -10
- package/src/framework/capability.ts +15 -1
- package/src/framework/domain_lifecycle.ts +8 -0
- package/src/framework/framework_aggregates.ts +2 -2
- package/src/framework/genesis.ts +11 -1
- package/src/framework/home.ts +178 -75
- package/src/framework/impure_capability.ts +24 -7
- package/src/framework/relations.ts +13 -5
- package/src/framework/workspaces.ts +13 -12
- package/src/generated_client_names.ts +42 -3
- package/src/idof_lint.ts +65 -13
- package/src/index.ts +46 -12
- package/src/intent.ts +587 -0
- package/src/law_check.ts +64 -16
- package/src/precondition.ts +16 -4
- package/src/read_projection_namespace.ts +2 -7
- package/src/recipient.ts +33 -0
- package/src/relation_schema.ts +93 -0
- package/src/secondary_ensure.ts +11 -2
- package/src/shape_compat.ts +11 -1
- package/src/usd.ts +30 -8
- package/src/usd_layers.ts +447 -246
- package/src/usd_state.ts +1 -1
- package/src/value_object.ts +49 -13
- package/src/wire_encode.ts +124 -26
- package/src/workspace_sharing.ts +269 -0
- package/src/zod_introspection.ts +84 -0
- package/dart/example/nomos_secure_key_store.dart +0 -82
- package/dart/example/nomos_webview_transport.dart +0 -63
- package/dart/lib/nomos_headless.dart +0 -50
- package/dart/lib/testing.dart +0 -14
- package/dart/test/co2_product_gauntlet_test.dart +0 -244
- package/src/framework/identity.ts +0 -537
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
/// The generated per-domain Dart client wraps a [NomosBridge]; the bridge owns request/reply correlation +
|
|
13
13
|
/// reactive `watch` streams. The platform TRANSPORT ([BridgeTransport]) is injected — `dart:js_util` on web,
|
|
14
14
|
/// a WebView JS-channel on mobile — so this protocol logic is pure Dart + fully unit-testable.
|
|
15
|
+
@internal
|
|
15
16
|
library;
|
|
16
17
|
|
|
18
|
+
import 'package:meta/meta.dart' show internal;
|
|
19
|
+
|
|
17
20
|
import 'dart:async';
|
|
18
21
|
import 'dart:convert';
|
|
19
22
|
import 'dart:math';
|
|
@@ -24,12 +27,12 @@ import 'package:nomos_types/nomos_types.dart'
|
|
|
24
27
|
NomosTraceContext,
|
|
25
28
|
NomosErrorInfo,
|
|
26
29
|
NomosRef,
|
|
27
|
-
NomosForeignReader,
|
|
28
|
-
NomosForeignWorkspaceReader,
|
|
29
|
-
ForeignAttestation,
|
|
30
30
|
CustodyBreakPolicy,
|
|
31
31
|
NomosCustodyEvent,
|
|
32
32
|
NomosCustodyStatus;
|
|
33
|
+
import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;
|
|
34
|
+
import 'package:nomos_types/src/foreign_ref.dart'
|
|
35
|
+
show NomosForeignReader, NomosForeignWorkspaceReader;
|
|
33
36
|
|
|
34
37
|
import 'session_bound.dart' show DomainNotInstalledForSession;
|
|
35
38
|
|
|
@@ -107,14 +110,14 @@ class TransferProgress {
|
|
|
107
110
|
class NomosMoment {
|
|
108
111
|
final String id;
|
|
109
112
|
final DateTime? occurredAt;
|
|
110
|
-
final String?
|
|
113
|
+
final String? performedBy;
|
|
111
114
|
final String? domain;
|
|
112
115
|
final String? action;
|
|
113
116
|
|
|
114
117
|
const NomosMoment({
|
|
115
118
|
required this.id,
|
|
116
119
|
this.occurredAt,
|
|
117
|
-
this.
|
|
120
|
+
this.performedBy,
|
|
118
121
|
this.domain,
|
|
119
122
|
this.action,
|
|
120
123
|
});
|
|
@@ -126,7 +129,7 @@ class NomosMoment {
|
|
|
126
129
|
occurredAt: at is num
|
|
127
130
|
? DateTime.fromMillisecondsSinceEpoch(at.toInt(), isUtc: true)
|
|
128
131
|
: null,
|
|
129
|
-
|
|
132
|
+
performedBy: json['actorSubject']?.toString(),
|
|
130
133
|
domain: json['domain']?.toString(),
|
|
131
134
|
action: json['directiveId']?.toString(),
|
|
132
135
|
);
|
|
@@ -198,7 +201,8 @@ class NomosCreated {
|
|
|
198
201
|
|
|
199
202
|
/// The canonical [NomosRef] view — the `{workspace,type,id}` triple, followable/serializable
|
|
200
203
|
/// (`nomos://<workspace>/<type>/<id>`). One reference, everywhere.
|
|
201
|
-
NomosRef get ref =>
|
|
204
|
+
NomosRef<Object?> get ref => NmsInternalNomosRefs.create<Object?>(
|
|
205
|
+
workspace: workspace, type: type, id: id);
|
|
202
206
|
@override
|
|
203
207
|
String toString() => id;
|
|
204
208
|
}
|
|
@@ -838,6 +842,10 @@ class NomosOfferOutcome {
|
|
|
838
842
|
final String head;
|
|
839
843
|
final List<NomosCreated> created;
|
|
840
844
|
|
|
845
|
+
/// Every business aggregate touched by the sealed intent, including creates,
|
|
846
|
+
/// mutations, deterministic ensures and archives.
|
|
847
|
+
final List<NomosCreated> affected;
|
|
848
|
+
|
|
841
849
|
/// The workspace the offer ran on (NomosRef). Additive; '' on older runners. Each [created] ref already
|
|
842
850
|
/// carries this, so `outcome.created.first.ref` is the full followable `{workspace,type,id}` locator.
|
|
843
851
|
final String workspace;
|
|
@@ -859,6 +867,7 @@ class NomosOfferOutcome {
|
|
|
859
867
|
const NomosOfferOutcome({
|
|
860
868
|
required this.head,
|
|
861
869
|
required this.created,
|
|
870
|
+
this.affected = const [],
|
|
862
871
|
this.workspace = '',
|
|
863
872
|
this.born = const [],
|
|
864
873
|
this.birthOutcomes = const [],
|
|
@@ -874,8 +883,9 @@ class NomosOfferOutcome {
|
|
|
874
883
|
/// The born child workspace(s) as followable [NomosRef]s — hand one to [NomosBridge.follow]
|
|
875
884
|
/// (`final estate = await engine.follow(outcome.bornRefs.single);`) so app code never carries a
|
|
876
885
|
/// loose workspace string. The ref's type is the reserved `nomos.Workspace` locator tag.
|
|
877
|
-
List<NomosRef
|
|
878
|
-
.map((w) =>
|
|
886
|
+
List<NomosRef<Object?>> get bornRefs => born
|
|
887
|
+
.map((w) => NmsInternalNomosRefs.create<Object?>(
|
|
888
|
+
workspace: w, type: 'nomos.Workspace', id: w))
|
|
879
889
|
.toList();
|
|
880
890
|
}
|
|
881
891
|
|
|
@@ -885,11 +895,12 @@ class NomosOfferOutcome {
|
|
|
885
895
|
/// genuinely-absent workspace needs [ifAbsentCredentials] (else a named [StateError]). Returns the live
|
|
886
896
|
/// [NomosSession] — so the failure surfaces HERE (at open), never later on the client's first query. Defined
|
|
887
897
|
/// in nomos_client (NOT on the pure `nomos_types` [NomosRef]) so the types package stays bridge-free.
|
|
888
|
-
extension NomosRefAcquire on NomosRef {
|
|
898
|
+
extension NomosRefAcquire<TAggregate> on NomosRef<TAggregate> {
|
|
889
899
|
Future<NomosSession> acquire(
|
|
890
900
|
NomosBridge bridge, {
|
|
891
901
|
Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
|
|
892
902
|
}) {
|
|
903
|
+
final workspace = NmsInternalNomosRefs.workspace(this);
|
|
893
904
|
if (workspace.isEmpty) {
|
|
894
905
|
throw ArgumentError.value(this, 'ref',
|
|
895
906
|
'NomosRef.acquire: the ref carries no workspace — only a workspace-complete ref is openable');
|
|
@@ -927,6 +938,25 @@ class NomosSessionCredentials {
|
|
|
927
938
|
});
|
|
928
939
|
}
|
|
929
940
|
|
|
941
|
+
final class _NomosRetainedCloudAccess {
|
|
942
|
+
final String cloud;
|
|
943
|
+
final Future<String?> Function() signIn;
|
|
944
|
+
|
|
945
|
+
const _NomosRetainedCloudAccess({
|
|
946
|
+
required this.cloud,
|
|
947
|
+
required this.signIn,
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
Future<NomosSessionCredentials> credentials() async {
|
|
951
|
+
final token = await signIn();
|
|
952
|
+
if (token == null || token.trim().isEmpty) {
|
|
953
|
+
throw StateError(
|
|
954
|
+
'Nomos could not refresh the signed-in identity while opening a referenced business workspace');
|
|
955
|
+
}
|
|
956
|
+
return NomosSessionCredentials(cloud: cloud, authToken: token);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
930
960
|
/// THE BRIDGE. Construct over a [BridgeTransport], `await ready`, then `connect(...)` and drive the holon.
|
|
931
961
|
class NomosBridge {
|
|
932
962
|
final BridgeTransport _transport;
|
|
@@ -980,6 +1010,14 @@ class NomosBridge {
|
|
|
980
1010
|
// The gate-bound identity retained from each session's auth token. Generated clients consume this so
|
|
981
1011
|
// application code never has to thread an actor into ordinary business methods.
|
|
982
1012
|
final Map<String, String> _sessionPrincipals = {};
|
|
1013
|
+
// Device signing custody follows the application session internally. A generated application opening a
|
|
1014
|
+
// locally-born child inherits its current session's device key; tenant code never handles authorSecret.
|
|
1015
|
+
final Map<String, String> _sessionAuthorSecrets = {};
|
|
1016
|
+
// Stateful generated applications retain the refreshable cloud access that opened their primary
|
|
1017
|
+
// session. A generated read following a NomosRef can therefore mount a genuinely-absent workspace
|
|
1018
|
+
// without asking tenant code for cloud URLs, tokens, sessions or signing keys. Kept per source
|
|
1019
|
+
// session so one local plane may safely host applications from different clouds/identities.
|
|
1020
|
+
final Map<String, _NomosRetainedCloudAccess> _sessionCloudAccess = {};
|
|
983
1021
|
// [acquireSession]'s per-workspace single-flight: workspace → the in-flight open every concurrent
|
|
984
1022
|
// acquirer joins (removed when it completes; a live session then hits the fast path instead).
|
|
985
1023
|
final Map<String, Future<NomosSession>> _acquiring = {};
|
|
@@ -1263,7 +1301,7 @@ class NomosBridge {
|
|
|
1263
1301
|
final sub = payload is Map ? payload['sub'] : null;
|
|
1264
1302
|
if (sub is! String || sub.trim().isEmpty) return null;
|
|
1265
1303
|
final value = sub.trim();
|
|
1266
|
-
return
|
|
1304
|
+
return 'user:$value';
|
|
1267
1305
|
} catch (_) {
|
|
1268
1306
|
return null;
|
|
1269
1307
|
}
|
|
@@ -1280,8 +1318,8 @@ class NomosBridge {
|
|
|
1280
1318
|
NomosSession session(String workspace) => _eventPlane._sessionViews
|
|
1281
1319
|
.putIfAbsent(workspace, () => NomosSession(_eventPlane, workspace));
|
|
1282
1320
|
|
|
1283
|
-
/// A [NomosForeignReader] backed by
|
|
1284
|
-
///
|
|
1321
|
+
/// A [NomosForeignReader] backed by this bridge's multi-workspace plane. Generated application methods
|
|
1322
|
+
/// use it privately to follow a declared cross-workspace business reference.
|
|
1285
1323
|
/// `foreignWorkspace(ws).queryById(id)` MOUNTS the foreign workspace on this device's plane ON DEMAND
|
|
1286
1324
|
/// ([connect] is idempotent — "first connect wins", and `allowUnborn` so a missing workspace reads empty →
|
|
1287
1325
|
/// `ForeignNotFound`, not a throw) and reads it. [cloud] is the cloud the foreign workspaces sync from
|
|
@@ -1380,6 +1418,9 @@ class NomosBridge {
|
|
|
1380
1418
|
final boundPrincipal = _principalFromToken(authToken);
|
|
1381
1419
|
if (boundPrincipal != null)
|
|
1382
1420
|
_eventPlane._sessionPrincipals[ws] = boundPrincipal;
|
|
1421
|
+
if (authorSecret != null && authorSecret.isNotEmpty) {
|
|
1422
|
+
_eventPlane._sessionAuthorSecrets[ws] = authorSecret;
|
|
1423
|
+
}
|
|
1383
1424
|
_eventPlane._lastRecovery =
|
|
1384
1425
|
(r['recovery'] as Map?)?.cast<String, Object?>();
|
|
1385
1426
|
}
|
|
@@ -1670,6 +1711,16 @@ class NomosBridge {
|
|
|
1670
1711
|
_eventPlane._sessionPrincipals[born] = birthPrincipal;
|
|
1671
1712
|
}
|
|
1672
1713
|
}
|
|
1714
|
+
final sourceSession = session ?? _sessionName;
|
|
1715
|
+
final birthAuthorSecret = authorSecret ??
|
|
1716
|
+
(sourceSession == null
|
|
1717
|
+
? null
|
|
1718
|
+
: _eventPlane._sessionAuthorSecrets[sourceSession]);
|
|
1719
|
+
if (birthAuthorSecret != null && birthAuthorSecret.isNotEmpty) {
|
|
1720
|
+
for (final born in bornNames) {
|
|
1721
|
+
_eventPlane._sessionAuthorSecrets[born] = birthAuthorSecret;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1673
1724
|
// The offer ran on this workspace (NomosRef) — stamp every minted ref with it so it is a complete locator.
|
|
1674
1725
|
final ws = (r['workspace'] ?? '').toString();
|
|
1675
1726
|
return NomosOfferOutcome(
|
|
@@ -1682,6 +1733,13 @@ class NomosBridge {
|
|
|
1682
1733
|
id: (m['id'] ?? '').toString(),
|
|
1683
1734
|
workspace: ws))
|
|
1684
1735
|
.toList(),
|
|
1736
|
+
affected: ((r['affected'] as List?) ?? const [])
|
|
1737
|
+
.whereType<Map>()
|
|
1738
|
+
.map((m) => NomosCreated(
|
|
1739
|
+
type: (m['type'] ?? '').toString(),
|
|
1740
|
+
id: (m['id'] ?? '').toString(),
|
|
1741
|
+
workspace: ws))
|
|
1742
|
+
.toList(),
|
|
1685
1743
|
born: bornNames,
|
|
1686
1744
|
birthOutcomes: births,
|
|
1687
1745
|
);
|
|
@@ -1736,13 +1794,13 @@ class NomosBridge {
|
|
|
1736
1794
|
/// cloud (omit for the local plane — a locally-born child mounts in place, no re-clone).
|
|
1737
1795
|
/// [authorSecret] — the device signing key for the followed session's writes (era-0: an attested-born
|
|
1738
1796
|
/// child is WARRANTED; write access needs its owner's enrolled device key).
|
|
1739
|
-
Future<NomosSession> follow(NomosRef ref,
|
|
1797
|
+
Future<NomosSession> follow<TAggregate>(NomosRef<TAggregate> ref,
|
|
1740
1798
|
{String? cloud,
|
|
1741
1799
|
String? clientId,
|
|
1742
1800
|
String? authToken,
|
|
1743
1801
|
bool allowUnborn = false,
|
|
1744
1802
|
String? authorSecret}) async {
|
|
1745
|
-
final ws =
|
|
1803
|
+
final ws = NmsInternalNomosRefs.workspace(ref);
|
|
1746
1804
|
if (ws.isEmpty) {
|
|
1747
1805
|
throw ArgumentError.value(ref, 'ref',
|
|
1748
1806
|
'follow: the NomosRef carries no workspace — only a workspace-complete ref is followable');
|
|
@@ -1776,10 +1834,23 @@ class NomosBridge {
|
|
|
1776
1834
|
/// the credentials path takes its own `NomosSessionCredentials.authorSecret` (falling back to this).
|
|
1777
1835
|
Future<NomosSession> acquireSession(String workspace,
|
|
1778
1836
|
{Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
|
|
1779
|
-
String? authorSecret
|
|
1837
|
+
String? authorSecret,
|
|
1838
|
+
String? inheritedPrincipal}) {
|
|
1780
1839
|
// Fast path: a live session needs no runner round-trip (and never asks for credentials).
|
|
1781
1840
|
final plane = _eventPlane;
|
|
1841
|
+
final sourcePrincipal = inheritedPrincipal ?? principal;
|
|
1842
|
+
void inheritPrincipal() {
|
|
1843
|
+
if (sourcePrincipal == null || sourcePrincipal.isEmpty) return;
|
|
1844
|
+
final existing = plane._sessionPrincipals[workspace];
|
|
1845
|
+
if (existing != null && existing != sourcePrincipal) {
|
|
1846
|
+
throw StateError(
|
|
1847
|
+
'acquireSession("$workspace"): the workspace is already bound to a different signed-in person');
|
|
1848
|
+
}
|
|
1849
|
+
plane._sessionPrincipals[workspace] = sourcePrincipal;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1782
1852
|
if (plane._sessionNames.contains(workspace)) {
|
|
1853
|
+
inheritPrincipal();
|
|
1783
1854
|
return Future.value(session(workspace));
|
|
1784
1855
|
}
|
|
1785
1856
|
// SINGLE-FLIGHT: one in-flight open per workspace per plane; concurrent callers join it.
|
|
@@ -1801,6 +1872,7 @@ class NomosBridge {
|
|
|
1801
1872
|
session: workspace,
|
|
1802
1873
|
allowUnborn: true,
|
|
1803
1874
|
authorSecret: authorSecret);
|
|
1875
|
+
inheritPrincipal();
|
|
1804
1876
|
return session(workspace);
|
|
1805
1877
|
}
|
|
1806
1878
|
if (ifAbsentCredentials == null) {
|
|
@@ -1818,6 +1890,7 @@ class NomosBridge {
|
|
|
1818
1890
|
authorSecret: c.authorSecret ?? authorSecret,
|
|
1819
1891
|
allowUnborn: c.allowUnborn,
|
|
1820
1892
|
);
|
|
1893
|
+
inheritPrincipal();
|
|
1821
1894
|
return session(workspace);
|
|
1822
1895
|
} finally {
|
|
1823
1896
|
// completed (either way) — the next acquire re-evaluates fresh (a live session hits the fast path).
|
|
@@ -1983,7 +2056,7 @@ class NomosBridge {
|
|
|
1983
2056
|
required String subject,
|
|
1984
2057
|
required String relation,
|
|
1985
2058
|
required String asserterPrincipal,
|
|
1986
|
-
|
|
2059
|
+
String? homeLawHash,
|
|
1987
2060
|
String? authorSecret,
|
|
1988
2061
|
String object = 'workspace:self',
|
|
1989
2062
|
String? asserterWorkspace,
|
|
@@ -1996,7 +2069,7 @@ class NomosBridge {
|
|
|
1996
2069
|
'relation': relation,
|
|
1997
2070
|
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
1998
2071
|
'asserterPrincipal': asserterPrincipal,
|
|
1999
|
-
'homeLawHash': homeLawHash,
|
|
2072
|
+
if (homeLawHash != null) 'homeLawHash': homeLawHash,
|
|
2000
2073
|
'object': object,
|
|
2001
2074
|
if (asserterWorkspace != null) 'asserterWorkspace': asserterWorkspace,
|
|
2002
2075
|
if (attestedAt != null) 'attestedAt': attestedAt,
|
|
@@ -2269,17 +2342,11 @@ class NomosBridge {
|
|
|
2269
2342
|
Future<Map<String, Object?>> verifyChain() async =>
|
|
2270
2343
|
_asMap(await _call('verifyChain'));
|
|
2271
2344
|
|
|
2272
|
-
///
|
|
2273
|
-
///
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
/// tenant's platform whose governance includes `birthHome`) and `principal`; the client discovers the
|
|
2278
|
-
/// private framework route and active law hashes from that application's live contract. `subject`/
|
|
2279
|
-
/// `authToken`/`directiveId` are optional. OURS is the ceremony; the parent is the TENANT's.
|
|
2280
|
-
/// Returns `{home, device:{secret,public}, homeCert, born, verify:{valid,…}}`.
|
|
2281
|
-
Future<Map<String, Object?>> birthHome(Map<String, Object?> spec) async =>
|
|
2282
|
-
_asMap(await _call('birthHome', spec));
|
|
2345
|
+
/// Framework-private receiving-group setup. Generated application hosts own this ceremony; tenant code
|
|
2346
|
+
/// receives only the resulting business application.
|
|
2347
|
+
Future<Map<String, Object?>> nomosInternalBirthReceivingGroup(
|
|
2348
|
+
Map<String, Object?> spec) async =>
|
|
2349
|
+
_asMap(await _call('nomosInternalBirthReceivingGroup', spec));
|
|
2283
2350
|
|
|
2284
2351
|
/// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
|
|
2285
2352
|
/// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
|
|
@@ -2508,15 +2575,18 @@ class NomosBridge {
|
|
|
2508
2575
|
}));
|
|
2509
2576
|
|
|
2510
2577
|
/// Prepare the private framework payload behind generated share/unshare/transfer methods.
|
|
2511
|
-
Future<Map<String, Object?>> nomosPrepareOwnedRecordAction(
|
|
2578
|
+
Future<Map<String, Object?>> nomosPrepareOwnedRecordAction<TAggregate>(
|
|
2512
2579
|
{required String action,
|
|
2513
2580
|
required String scope,
|
|
2514
|
-
required NomosRef record,
|
|
2581
|
+
required NomosRef<TAggregate> record,
|
|
2515
2582
|
required String peer}) async =>
|
|
2516
2583
|
_asMap(await _call('cryptoOwnedPrepareRecordAction', {
|
|
2517
2584
|
'action': action,
|
|
2518
2585
|
'scope': scope,
|
|
2519
|
-
'record': {
|
|
2586
|
+
'record': {
|
|
2587
|
+
'type': NmsInternalNomosRefs.type(record),
|
|
2588
|
+
'id': NmsInternalNomosRefs.id(record),
|
|
2589
|
+
},
|
|
2520
2590
|
'peer': peer,
|
|
2521
2591
|
}));
|
|
2522
2592
|
|
|
@@ -2804,11 +2874,19 @@ class NomosBridge {
|
|
|
2804
2874
|
/// Reactive local read: a stream of row-sets that re-emits on every local change. Cancelling the
|
|
2805
2875
|
/// subscription tells the runner to drop the watch.
|
|
2806
2876
|
Stream<List<dynamic>> watch(String queryId,
|
|
2807
|
-
[Map<String, Object?> params = const {},
|
|
2877
|
+
[Map<String, Object?> params = const {},
|
|
2878
|
+
String? session,
|
|
2879
|
+
String? principal]) =>
|
|
2808
2880
|
_watchStream(
|
|
2809
|
-
kind: 'query',
|
|
2810
|
-
|
|
2811
|
-
|
|
2881
|
+
kind: 'query',
|
|
2882
|
+
queryId: queryId,
|
|
2883
|
+
params: params,
|
|
2884
|
+
session: session,
|
|
2885
|
+
principal: principal);
|
|
2886
|
+
Stream<List<dynamic>> watchById(String id,
|
|
2887
|
+
[String? session, String? principal]) =>
|
|
2888
|
+
_watchStream(
|
|
2889
|
+
kind: 'byId', id: id, session: session, principal: principal);
|
|
2812
2890
|
|
|
2813
2891
|
/// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
|
|
2814
2892
|
Stream<List<dynamic>> watchSpatial(String spatialId, NomosBbox bbox,
|
|
@@ -2931,11 +3009,12 @@ class NomosBridge {
|
|
|
2931
3009
|
'requirement': requirement,
|
|
2932
3010
|
if (session != null) 'session': session,
|
|
2933
3011
|
}),
|
|
2934
|
-
onCancel: ()
|
|
3012
|
+
onCancel: () {
|
|
2935
3013
|
_eventPlane._compatWatches.remove(subId);
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3014
|
+
// Cancellation is a local lifecycle boundary and must complete even if the runner is slow,
|
|
3015
|
+
// disconnected, or being disposed. The remote release remains best-effort; bridge/runner disposal
|
|
3016
|
+
// also drops every outstanding watch, so awaiting this acknowledgement only creates teardown hangs.
|
|
3017
|
+
unawaited(_call('unwatch', {'subId': subId}).catchError((_) => null));
|
|
2939
3018
|
},
|
|
2940
3019
|
);
|
|
2941
3020
|
_eventPlane._compatWatches[subId] = ctrl;
|
|
@@ -2950,7 +3029,8 @@ class NomosBridge {
|
|
|
2950
3029
|
String? spatialId,
|
|
2951
3030
|
NomosBbox? bbox,
|
|
2952
3031
|
String? base,
|
|
2953
|
-
String? session
|
|
3032
|
+
String? session,
|
|
3033
|
+
String? principal}) {
|
|
2954
3034
|
// Controllers live on the EVENT PLANE (where _onMessage dispatches); the registration `_call` stays
|
|
2955
3035
|
// THIS instance's (a session's override stamps `args.session`) — see [_eventPlane].
|
|
2956
3036
|
final subId = 'w${++_eventPlane._seq}';
|
|
@@ -2965,13 +3045,14 @@ class NomosBridge {
|
|
|
2965
3045
|
if (spatialId != null) 'spatialId': spatialId,
|
|
2966
3046
|
if (bbox != null) 'bbox': bbox.toJson(),
|
|
2967
3047
|
if (base != null) 'base': base,
|
|
3048
|
+
if (principal != null) 'principal': principal,
|
|
2968
3049
|
if (session != null) 'session': session,
|
|
2969
3050
|
}),
|
|
2970
|
-
onCancel: ()
|
|
3051
|
+
onCancel: () {
|
|
2971
3052
|
_eventPlane._watches.remove(subId);
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3053
|
+
// A caller owns its local subscription; it must never need a healthy transport in order to release
|
|
3054
|
+
// it. Runner-side cleanup is still requested immediately and is made total by runner disposal.
|
|
3055
|
+
unawaited(_call('unwatch', {'subId': subId}).catchError((_) => null));
|
|
2975
3056
|
},
|
|
2976
3057
|
);
|
|
2977
3058
|
_eventPlane._watches[subId] = ctrl;
|
|
@@ -3078,14 +3159,48 @@ class NomosSession extends NomosBridge {
|
|
|
3078
3159
|
/// single-flight registry; a session never owns mounts).
|
|
3079
3160
|
@override
|
|
3080
3161
|
Future<NomosSession> acquireSession(String workspace,
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3162
|
+
{Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
|
|
3163
|
+
String? authorSecret,
|
|
3164
|
+
String? inheritedPrincipal}) {
|
|
3165
|
+
final retained = parent._sessionCloudAccess[this.workspace];
|
|
3166
|
+
final opened = parent.acquireSession(
|
|
3167
|
+
workspace,
|
|
3168
|
+
ifAbsentCredentials: ifAbsentCredentials ??
|
|
3169
|
+
(retained == null ? null : retained.credentials),
|
|
3170
|
+
authorSecret:
|
|
3171
|
+
authorSecret ?? parent._sessionAuthorSecrets[this.workspace],
|
|
3172
|
+
inheritedPrincipal:
|
|
3173
|
+
inheritedPrincipal ?? parent._sessionPrincipals[this.workspace],
|
|
3174
|
+
);
|
|
3175
|
+
return opened.then((session) {
|
|
3176
|
+
// Navigation can continue from the newly-opened handle without losing the application's private
|
|
3177
|
+
// credential posture (home -> shared home -> child workspace remains one stateful application).
|
|
3178
|
+
if (retained != null)
|
|
3179
|
+
parent._sessionCloudAccess.putIfAbsent(workspace, () => retained);
|
|
3180
|
+
return session;
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
/// Generator/runtime-host seam: retain this application's refreshable cloud access privately so
|
|
3185
|
+
/// generated reference reads can mount another authorized workspace after a cold start.
|
|
3186
|
+
///
|
|
3187
|
+
/// Tenant packages do not export NomosSession; application code reaches this only through generated
|
|
3188
|
+
/// `find`/`watchOne` methods, never through a credential or session callback.
|
|
3189
|
+
void nomosInternalRetainCloudAccess({
|
|
3190
|
+
required String cloud,
|
|
3191
|
+
required Future<String?> Function() signIn,
|
|
3192
|
+
}) {
|
|
3193
|
+
if (cloud.trim().isEmpty) {
|
|
3194
|
+
throw ArgumentError.value(
|
|
3195
|
+
cloud, 'cloud', 'Nomos cloud URL cannot be empty');
|
|
3196
|
+
}
|
|
3197
|
+
parent._sessionCloudAccess[workspace] =
|
|
3198
|
+
_NomosRetainedCloudAccess(cloud: cloud, signIn: signIn);
|
|
3199
|
+
}
|
|
3085
3200
|
|
|
3086
3201
|
/// A followed ref mounts on the SHARED plane — delegate to the parent (a session never owns mounts).
|
|
3087
3202
|
@override
|
|
3088
|
-
Future<NomosSession> follow(NomosRef ref,
|
|
3203
|
+
Future<NomosSession> follow<TAggregate>(NomosRef<TAggregate> ref,
|
|
3089
3204
|
{String? cloud,
|
|
3090
3205
|
String? clientId,
|
|
3091
3206
|
String? authToken,
|
|
@@ -3096,7 +3211,8 @@ class NomosSession extends NomosBridge {
|
|
|
3096
3211
|
clientId: clientId,
|
|
3097
3212
|
authToken: authToken,
|
|
3098
3213
|
allowUnborn: allowUnborn,
|
|
3099
|
-
authorSecret:
|
|
3214
|
+
authorSecret:
|
|
3215
|
+
authorSecret ?? parent._sessionAuthorSecrets[workspace]);
|
|
3100
3216
|
|
|
3101
3217
|
@override
|
|
3102
3218
|
Future<Object?> _call(String op, [Map<String, Object?> args = const {}]) =>
|
|
@@ -3169,13 +3285,6 @@ class _BridgeForeignReader implements NomosForeignReader {
|
|
|
3169
3285
|
@override
|
|
3170
3286
|
NomosForeignWorkspaceReader foreignWorkspace(String workspace) =>
|
|
3171
3287
|
_BridgeForeignWorkspaceReader(_bridge, workspace, _cloud);
|
|
3172
|
-
|
|
3173
|
-
// No attestation hook by default — `ForeignRef.attest` throws unless the app wires the attested-read lane
|
|
3174
|
-
// (trustSource + .readsFrom). That gate is the security model (a locator is not a foreign key), not a
|
|
3175
|
-
// limitation: unverifiable "evidence" would be the real footgun.
|
|
3176
|
-
@override
|
|
3177
|
-
Future<ForeignAttestation> Function(String workspace, String id, String type)?
|
|
3178
|
-
get attestForeign => null;
|
|
3179
3288
|
}
|
|
3180
3289
|
|
|
3181
3290
|
/// A per-workspace read handle on the bridge plane — mounts [_workspace] lazily on the first read.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
@internal
|
|
2
|
+
library;
|
|
3
|
+
|
|
4
|
+
import 'package:meta/meta.dart' show internal;
|
|
5
|
+
|
|
1
6
|
import 'dart:convert';
|
|
2
7
|
|
|
3
8
|
import 'package:crypto/crypto.dart';
|
|
@@ -19,6 +24,7 @@ final String workspaceNameSep = String.fromCharCode(31);
|
|
|
19
24
|
/// final ws = deriveWorkspaceName('estate', estateId); // open / sync / mount it
|
|
20
25
|
/// ```
|
|
21
26
|
String deriveWorkspaceName(String namespace, String key) {
|
|
22
|
-
final digest =
|
|
27
|
+
final digest =
|
|
28
|
+
sha256.convert(utf8.encode('$namespace$workspaceNameSep$key')).toString();
|
|
23
29
|
return '$namespace-${digest.substring(0, 40)}';
|
|
24
30
|
}
|
package/dart/pubspec.lock
CHANGED
|
@@ -162,7 +162,7 @@ packages:
|
|
|
162
162
|
source: hosted
|
|
163
163
|
version: "0.12.20"
|
|
164
164
|
meta:
|
|
165
|
-
dependency:
|
|
165
|
+
dependency: "direct main"
|
|
166
166
|
description:
|
|
167
167
|
name: meta
|
|
168
168
|
sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739
|
|
@@ -191,9 +191,9 @@ packages:
|
|
|
191
191
|
path: "../dart_types"
|
|
192
192
|
relative: true
|
|
193
193
|
source: path
|
|
194
|
-
version: "0.
|
|
194
|
+
version: "0.8.0"
|
|
195
195
|
package_config:
|
|
196
|
-
dependency:
|
|
196
|
+
dependency: "direct main"
|
|
197
197
|
description:
|
|
198
198
|
name: package_config
|
|
199
199
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
package/dart/pubspec.yaml
CHANGED
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
9
9
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
10
10
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
11
|
-
version: 0.
|
|
11
|
+
version: 0.64.0
|
|
12
12
|
|
|
13
13
|
environment:
|
|
14
14
|
sdk: ^3.5.0
|
|
@@ -17,9 +17,11 @@ dependencies:
|
|
|
17
17
|
# The pure-data core (wire types, offer codec, NomosReads, schema validation, error
|
|
18
18
|
# info). nomos_client depends on and re-exports it, so existing imports are unchanged.
|
|
19
19
|
# For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
|
|
20
|
-
nomos_types: ^0.
|
|
20
|
+
nomos_types: ^0.8.0
|
|
21
21
|
json_schema: ^5.2.2
|
|
22
22
|
crypto: ^3.0.0
|
|
23
|
+
package_config: ^2.1.0
|
|
24
|
+
meta: ^1.12.0
|
|
23
25
|
|
|
24
26
|
dev_dependencies:
|
|
25
27
|
test: ^1.25.0
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// returns. Runs hermetically on the offline runner (the "cloud" is the runner's offline shim, so
|
|
6
6
|
// no network is ever contacted; what is under test is the acquire protocol, not the transport).
|
|
7
7
|
//
|
|
8
|
-
// This file also IS the
|
|
8
|
+
// This file also IS the internal conformance regression: it imports only the obscured
|
|
9
|
+
// framework conformance runtime and types a
|
|
9
10
|
// `Future<NomosSessionCredentials> Function()` callback — before the barrel fix that did not compile.
|
|
10
11
|
@TestOn('vm')
|
|
11
12
|
library;
|
|
@@ -13,18 +14,21 @@ library;
|
|
|
13
14
|
import 'dart:async';
|
|
14
15
|
import 'dart:io';
|
|
15
16
|
|
|
16
|
-
import 'package:nomos_client/
|
|
17
|
+
import 'package:nomos_client/src/nms_conformance_runtime.dart';
|
|
17
18
|
import 'package:test/test.dart';
|
|
18
19
|
|
|
19
20
|
String get _root => '${Directory.current.path}/../..';
|
|
20
21
|
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
|
|
21
|
-
String get _docsDeploy =>
|
|
22
|
+
String get _docsDeploy =>
|
|
23
|
+
'$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
|
|
22
24
|
String get _runtimeDir =>
|
|
23
25
|
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
26
|
+
Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
|
|
24
27
|
'${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
25
28
|
|
|
26
29
|
String? _missingPrereq() {
|
|
27
|
-
if (!File(_repoEntry).existsSync())
|
|
30
|
+
if (!File(_repoEntry).existsSync())
|
|
31
|
+
return 'in-repo runner not found at $_repoEntry';
|
|
28
32
|
if (!File(_docsDeploy).existsSync()) {
|
|
29
33
|
return 'missing fixture $_docsDeploy — compile it: '
|
|
30
34
|
'(cd bench/scale/fixtures/surfaced && node ../../../../dsl/compile_package.mjs)';
|
|
@@ -46,12 +50,14 @@ Future<T> _leg<T>(String name, Future<T> Function() body) async {
|
|
|
46
50
|
print('✔ ACQUIRE $name (${sw.elapsedMilliseconds}ms)');
|
|
47
51
|
return r;
|
|
48
52
|
} catch (e, st) {
|
|
49
|
-
throw TestFailure(
|
|
53
|
+
throw TestFailure(
|
|
54
|
+
'ACQUIRE $name FAILED after ${sw.elapsedMilliseconds}ms:\n$e\n$st');
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
void main() {
|
|
54
|
-
test(
|
|
59
|
+
test(
|
|
60
|
+
'acquireSession: the absent-workspace lane invokes ifAbsentCredentials exactly once',
|
|
55
61
|
() async {
|
|
56
62
|
final missing = _missingPrereq();
|
|
57
63
|
if (missing != null) {
|
|
@@ -70,10 +76,13 @@ void main() {
|
|
|
70
76
|
|
|
71
77
|
// ── LEG 1 (gap 1, compile-time + runtime): the callback TYPE is reachable from the headless
|
|
72
78
|
// barrel alone, and a credentials object constructs with the documented fields. ──
|
|
73
|
-
await _leg(
|
|
79
|
+
await _leg(
|
|
80
|
+
'leg 1: internal conformance runtime carries session credentials',
|
|
81
|
+
() async {
|
|
74
82
|
// ignore: prefer_function_declarations_over_variables
|
|
75
|
-
final Future<NomosSessionCredentials> Function() typedCallback =
|
|
76
|
-
const NomosSessionCredentials(
|
|
83
|
+
final Future<NomosSessionCredentials> Function() typedCallback =
|
|
84
|
+
() async => const NomosSessionCredentials(
|
|
85
|
+
cloud: 'https://offline.invalid', allowUnborn: true);
|
|
77
86
|
final c = await typedCallback();
|
|
78
87
|
expect(c.cloud, 'https://offline.invalid');
|
|
79
88
|
expect(c.allowUnborn, isTrue);
|
|
@@ -81,7 +90,9 @@ void main() {
|
|
|
81
90
|
|
|
82
91
|
// ── LEG 2: a genuinely-absent workspace WITHOUT a callback is a NAMED StateError, never a
|
|
83
92
|
// silent cloudless mount. ──
|
|
84
|
-
await _leg(
|
|
93
|
+
await _leg(
|
|
94
|
+
'leg 2: absent workspace without credentials refuses (StateError)',
|
|
95
|
+
() async {
|
|
85
96
|
await expectLater(
|
|
86
97
|
plane.acquireSession('nowhere-at-all'),
|
|
87
98
|
throwsA(isA<StateError>().having((e) => e.toString(), 'message',
|
|
@@ -92,26 +103,32 @@ void main() {
|
|
|
92
103
|
// callback fires EXACTLY ONCE (single-flight), the connect is made with its credentials,
|
|
93
104
|
// and both callers get the SAME mounted session. ──
|
|
94
105
|
var invocations = 0;
|
|
95
|
-
await _leg(
|
|
106
|
+
await _leg(
|
|
107
|
+
'leg 3: concurrent double-acquire → one credentials invocation, one handle',
|
|
96
108
|
() async {
|
|
97
109
|
Future<NomosSessionCredentials> creds() async {
|
|
98
110
|
invocations++;
|
|
99
111
|
return const NomosSessionCredentials(
|
|
100
|
-
cloud: 'https://offline.invalid',
|
|
112
|
+
cloud: 'https://offline.invalid',
|
|
113
|
+
clientId: 'acquired-client',
|
|
114
|
+
allowUnborn: true);
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
final acquired = await Future.wait([
|
|
104
118
|
plane.acquireSession('cloud-opened-ws', ifAbsentCredentials: creds),
|
|
105
119
|
plane.acquireSession('cloud-opened-ws', ifAbsentCredentials: creds),
|
|
106
120
|
]).timeout(const Duration(seconds: 60),
|
|
107
|
-
onTimeout: () =>
|
|
121
|
+
onTimeout: () =>
|
|
122
|
+
throw StateError('the cloud-open acquire never completed'));
|
|
108
123
|
expect(invocations, 1,
|
|
109
|
-
reason:
|
|
124
|
+
reason:
|
|
125
|
+
'a concurrent double-acquire must invoke the credentials callback ONCE');
|
|
110
126
|
expect(identical(acquired[0], acquired[1]), isTrue,
|
|
111
127
|
reason: 'both concurrent acquirers get the SAME session handle');
|
|
112
128
|
expect(acquired[0].workspace, 'cloud-opened-ws');
|
|
113
129
|
final listed = await plane.listWorkspaces();
|
|
114
|
-
expect(listed.map((w) => w['session'].toString()),
|
|
130
|
+
expect(listed.map((w) => w['session'].toString()),
|
|
131
|
+
contains('cloud-opened-ws'),
|
|
115
132
|
reason: 'the acquired workspace is mounted on the plane');
|
|
116
133
|
});
|
|
117
134
|
|
|
@@ -119,8 +136,8 @@ void main() {
|
|
|
119
136
|
// NEVER invoked again. ──
|
|
120
137
|
await _leg('leg 4: re-acquire is credential-free (fast path)', () async {
|
|
121
138
|
final again = await plane.acquireSession('cloud-opened-ws',
|
|
122
|
-
ifAbsentCredentials: () async =>
|
|
123
|
-
|
|
139
|
+
ifAbsentCredentials: () async => fail(
|
|
140
|
+
'the workspace is mounted — credentials must not be requested'));
|
|
124
141
|
expect(again.workspace, 'cloud-opened-ws');
|
|
125
142
|
expect(invocations, 1);
|
|
126
143
|
});
|