@githolon/dsl 0.98.3 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +6 -0
- package/dart/lib/src/webview_bridge.dart +36 -0
- package/dart/pubspec.yaml +1 -1
- package/dart/test/webview_bridge_test.dart +15 -0
- package/package.json +1 -1
- package/src/births.ts +74 -2
- package/src/build_package.ts +2 -0
- package/src/business_birth.ts +274 -0
- package/src/codegen_dart.ts +33 -5
- package/src/codegen_dart_client.ts +46 -1
- package/src/codegen_openapi.ts +27 -5
- package/src/codegen_proof.ts +9 -0
- package/src/codegen_ts.ts +59 -5
- package/src/compile_package_main.ts +3 -1
- package/src/engine_entry.ts +6 -4
- package/src/index.ts +2 -0
- package/src/law_check.ts +11 -0
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.62.0
|
|
2
|
+
|
|
3
|
+
- Generated business clients expose one `birth(payload)` method. The bound session supplies its verified
|
|
4
|
+
principal from the authentication token, and Nomos owns the identity, provenance, persistence, and
|
|
5
|
+
recovery ceremony beneath the business call.
|
|
6
|
+
|
|
1
7
|
## 0.61.3
|
|
2
8
|
|
|
3
9
|
- cloudBound seeds the KEYLESS enroll-once delegation (mintKeylessDelegation) so a keyless private home
|
|
@@ -829,6 +829,9 @@ class NomosBridge {
|
|
|
829
829
|
// caches the per-workspace [NomosSession] handle so `bridge.session(ws)` is stable + cheap.
|
|
830
830
|
final Set<String> _sessionNames = {};
|
|
831
831
|
final Map<String, NomosSession> _sessionViews = {};
|
|
832
|
+
// The gate-bound identity retained from each session's auth token. Generated clients consume this so
|
|
833
|
+
// application code never has to thread an actor into ordinary business methods.
|
|
834
|
+
final Map<String, String> _sessionPrincipals = {};
|
|
832
835
|
// [acquireSession]'s per-workspace single-flight: workspace → the in-flight open every concurrent
|
|
833
836
|
// acquirer joins (removed when it completes; a live session then hits the fast path instead).
|
|
834
837
|
final Map<String, Future<NomosSession>> _acquiring = {};
|
|
@@ -1079,6 +1082,21 @@ class NomosBridge {
|
|
|
1079
1082
|
.map((e) => (e as Map).cast<String, dynamic>())
|
|
1080
1083
|
.toList();
|
|
1081
1084
|
|
|
1085
|
+
String? _principalFromToken(String? token) {
|
|
1086
|
+
if (token == null || token.isEmpty) return null;
|
|
1087
|
+
try {
|
|
1088
|
+
final parts = token.split('.');
|
|
1089
|
+
if (parts.length < 2) return null;
|
|
1090
|
+
final payload = jsonDecode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
|
|
1091
|
+
final sub = payload is Map ? payload['sub'] : null;
|
|
1092
|
+
if (sub is! String || sub.trim().isEmpty) return null;
|
|
1093
|
+
final value = sub.trim();
|
|
1094
|
+
return value.contains(':') ? value : 'user:$value';
|
|
1095
|
+
} catch (_) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1082
1100
|
// ── the runtime-aware surface (the boundary the generated Dart client wraps) ──
|
|
1083
1101
|
|
|
1084
1102
|
/// THE PRIMARY MULTI-WORKSPACE ENTRY. Return a [NomosSession] handle pinned to [workspace] — the object a
|
|
@@ -1103,6 +1121,16 @@ class NomosBridge {
|
|
|
1103
1121
|
List<String> get connectedSessions =>
|
|
1104
1122
|
List.unmodifiable(_eventPlane._sessionNames);
|
|
1105
1123
|
|
|
1124
|
+
/// The identity already proved by this bound session. Null on an unauthenticated/local-only session, or
|
|
1125
|
+
/// on a multi-session plane whose identity would be ambiguous. Generated business methods use this by
|
|
1126
|
+
/// default; an explicit client actor remains an advanced override.
|
|
1127
|
+
String? get principal {
|
|
1128
|
+
final plane = _eventPlane;
|
|
1129
|
+
final scoped = _sessionName;
|
|
1130
|
+
if (scoped != null) return plane._sessionPrincipals[scoped];
|
|
1131
|
+
return plane._sessionPrincipals.length == 1 ? plane._sessionPrincipals.values.first : null;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1106
1134
|
/// ENUMERATE THE LOCAL PLANE — every workspace on this device + its lifecycle STATE and sync posture:
|
|
1107
1135
|
/// `[{ session, workspace, state, mounted, parked, syncPosture }]`. The explicit **`state`** (co2 DX ask)
|
|
1108
1136
|
/// is the field to branch on — it removes the "listed but `session(ws)` fails on first op" footgun:
|
|
@@ -1173,6 +1201,8 @@ class NomosBridge {
|
|
|
1173
1201
|
if (onCustodyBreak != null) 'onCustodyBreak': onCustodyBreak.wire,
|
|
1174
1202
|
}));
|
|
1175
1203
|
_eventPlane._sessionNames.add(ws); // the open succeeded — the session is real now
|
|
1204
|
+
final boundPrincipal = _principalFromToken(authToken);
|
|
1205
|
+
if (boundPrincipal != null) _eventPlane._sessionPrincipals[ws] = boundPrincipal;
|
|
1176
1206
|
_eventPlane._lastRecovery = (r['recovery'] as Map?)?.cast<String, Object?>();
|
|
1177
1207
|
}
|
|
1178
1208
|
|
|
@@ -1428,6 +1458,12 @@ class NomosBridge {
|
|
|
1428
1458
|
.map((m) => (m['workspace'] ?? '').toString())
|
|
1429
1459
|
.where((s) => s.isNotEmpty),
|
|
1430
1460
|
]..removeWhere((s) => s.isEmpty);
|
|
1461
|
+
final birthPrincipal = actor ?? principal;
|
|
1462
|
+
if (birthPrincipal != null && birthPrincipal.isNotEmpty) {
|
|
1463
|
+
for (final born in bornNames) {
|
|
1464
|
+
_eventPlane._sessionPrincipals[born] = birthPrincipal;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1431
1467
|
// The offer ran on this workspace (NomosRef) — stamp every minted ref with it so it is a complete locator.
|
|
1432
1468
|
final ws = (r['workspace'] ?? '').toString();
|
|
1433
1469
|
return NomosOfferOutcome(
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.62.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -452,6 +452,21 @@ void main() {
|
|
|
452
452
|
expect((req['args'] as Map)['allowUnborn'], isTrue);
|
|
453
453
|
});
|
|
454
454
|
|
|
455
|
+
test('a session retains the authenticated token subject as its default principal', () async {
|
|
456
|
+
await bridge.ready;
|
|
457
|
+
final payload = base64Url
|
|
458
|
+
.encode(utf8.encode(jsonEncode({'sub': 'ada'})))
|
|
459
|
+
.replaceAll('=', '');
|
|
460
|
+
await bridge.connect(
|
|
461
|
+
cloud: 'https://c',
|
|
462
|
+
workspace: 'authenticated',
|
|
463
|
+
authToken: 'header.$payload.signature',
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect(bridge.session('authenticated').principal, 'user:ada');
|
|
467
|
+
expect(bridge.principal, 'user:ada');
|
|
468
|
+
});
|
|
469
|
+
|
|
455
470
|
test(
|
|
456
471
|
'connect / query / queryById / count / sync / lawStatus all wrap the runner',
|
|
457
472
|
() async {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/births.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
4
|
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
5
|
|
|
6
|
+
import type { z } from "zod";
|
|
7
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
8
|
+
import type { AggregateRef } from "./authoring.js";
|
|
9
|
+
import type { Field } from "./fields.js";
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* THE BIRTH EFFECT — birth is an EXPLICIT, DECLARED kernel primitive (G3, the unix model).
|
|
8
13
|
*
|
|
@@ -75,8 +80,68 @@ export function __drainBirths(): BirthRecipe[] {
|
|
|
75
80
|
* .requires("creator"); // grant the parent's owner #creator (self-bound) — the birth authority
|
|
76
81
|
* ```
|
|
77
82
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
/**
|
|
84
|
+
* A business thing that is important enough to have its own durable, sovereign life.
|
|
85
|
+
*
|
|
86
|
+
* This is the ONLY tenant-facing birth declaration. The composer lowers it to the framework's
|
|
87
|
+
* recipe/directive machinery; application code gets one generated `birth(payload)` method. Security,
|
|
88
|
+
* law selection, lineage, storage and recovery are deliberately absent from this shape.
|
|
89
|
+
*/
|
|
90
|
+
export interface BusinessBirthDecl<
|
|
91
|
+
P extends Record<string, unknown> = Record<string, unknown>,
|
|
92
|
+
F extends Record<string, Field> = Record<string, Field>,
|
|
93
|
+
> {
|
|
94
|
+
readonly __isBusinessBirth: true;
|
|
95
|
+
readonly aggregate: AggregateHandle<string, F>;
|
|
96
|
+
readonly payloadSchema: z.ZodObject<z.ZodRawShape>;
|
|
97
|
+
/** A stable business key the caller already knows (an estate reference, account number, etc.). */
|
|
98
|
+
readonly key: (payload: P) => string;
|
|
99
|
+
/** Author the born thing's initial business facts. Nomos supplies the durable identity and lifecycle. */
|
|
100
|
+
readonly plan: (root: AggregateRef<F>, payload: P) => unknown;
|
|
101
|
+
/** Optional business noun used only to make the durable address readable. Defaults to the aggregate id. */
|
|
102
|
+
readonly namespace?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface BusinessBirthSpec<
|
|
106
|
+
P extends Record<string, unknown>,
|
|
107
|
+
F extends Record<string, Field>,
|
|
108
|
+
> {
|
|
109
|
+
readonly payload: z.ZodType<P> & z.ZodObject<z.ZodRawShape>;
|
|
110
|
+
readonly key: (payload: P) => string;
|
|
111
|
+
readonly plan: (root: AggregateRef<F>, payload: P) => unknown;
|
|
112
|
+
readonly namespace?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @internal Framework lane: record an already-lowered child recipe during a directive plan. Tenant law
|
|
116
|
+
* should declare `birth(Aggregate, spec)` and let Nomos own the recipe, identity and recovery machinery. */
|
|
117
|
+
export function birth(recipe: BirthRecipe): void;
|
|
118
|
+
/** Tenant lane: declare that one business aggregate is born as a durable workspace root. */
|
|
119
|
+
export function birth<P extends Record<string, unknown>, F extends Record<string, Field>>(
|
|
120
|
+
aggregate: AggregateHandle<string, F>,
|
|
121
|
+
spec: BusinessBirthSpec<P, F>,
|
|
122
|
+
): BusinessBirthDecl<P, F>;
|
|
123
|
+
export function birth<P extends Record<string, unknown>, F extends Record<string, Field>>(
|
|
124
|
+
recipeOrAggregate: BirthRecipe | AggregateHandle<string, F>,
|
|
125
|
+
spec?: BusinessBirthSpec<P, F>,
|
|
126
|
+
): void | BusinessBirthDecl<P, F> {
|
|
127
|
+
if (spec !== undefined) {
|
|
128
|
+
const aggregate = recipeOrAggregate as AggregateHandle<string, F>;
|
|
129
|
+
if (!aggregate || aggregate.__isAggregateHandle !== true) {
|
|
130
|
+
throw new Error("birth(Aggregate, spec): the first argument must be an aggregate handle");
|
|
131
|
+
}
|
|
132
|
+
if (!spec.payload || typeof (spec.payload as { safeParse?: unknown }).safeParse !== "function") {
|
|
133
|
+
throw new Error("birth(Aggregate, spec): spec.payload must be a z.object(...) schema");
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
__isBusinessBirth: true,
|
|
137
|
+
aggregate,
|
|
138
|
+
payloadSchema: spec.payload,
|
|
139
|
+
key: spec.key,
|
|
140
|
+
plan: spec.plan,
|
|
141
|
+
...(spec.namespace !== undefined ? { namespace: spec.namespace } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
footprint.push(recipeOrAggregate as BirthRecipe);
|
|
80
145
|
}
|
|
81
146
|
|
|
82
147
|
// `nomos.sha256` — the trustless pure hash bound in the sealed engine (like `nomos.mint`), used to derive a
|
|
@@ -118,6 +183,9 @@ interface MintedRef {
|
|
|
118
183
|
}
|
|
119
184
|
|
|
120
185
|
/**
|
|
186
|
+
* @deprecated Tenant law should declare `birth(Aggregate, spec)`. This framework helper remains for the
|
|
187
|
+
* built-in workspace laws that implement that declaration.
|
|
188
|
+
*
|
|
121
189
|
* `birth.keyed(...)` — THE way to birth a child workspace. A workspace exists to be USED, and using it means
|
|
122
190
|
* FINDING IT AGAIN, so its name is ALWAYS derived from a `key` the caller owns (a business id, an owner uid)
|
|
123
191
|
* via {@link workspaceNameForKey}. The client re-derives the SAME name with `deriveWorkspaceName(namespace,
|
|
@@ -177,6 +245,8 @@ export const birthInstance = ({
|
|
|
177
245
|
};
|
|
178
246
|
|
|
179
247
|
/**
|
|
248
|
+
* @deprecated Tenant law should declare `birth(Aggregate, spec)`. This is framework compatibility sugar.
|
|
249
|
+
*
|
|
180
250
|
* `birth.singleton(...)` — a keyed birth whose key is a NATURAL single key (one home per user: key = owner).
|
|
181
251
|
* Sugar over {@link birthKeyed}: `birth.singleton({ keyedBy: owner, namespace: "home" })`. Same addressing
|
|
182
252
|
* story — the client re-derives with `deriveWorkspaceName(namespace, keyedBy)`.
|
|
@@ -200,7 +270,9 @@ export const birthSingleton = ({
|
|
|
200
270
|
// `birth.keyed(...)` — with full types AND the runtime assignment in one place.
|
|
201
271
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
202
272
|
export namespace birth {
|
|
273
|
+
/** @deprecated Tenant law should use `birth(Aggregate, spec)`. Framework compatibility only. */
|
|
203
274
|
export const keyed = birthKeyed;
|
|
275
|
+
/** @deprecated Tenant law should use `birth(Aggregate, spec)`. Framework compatibility only. */
|
|
204
276
|
export const singleton = birthSingleton;
|
|
205
277
|
/** @deprecated Use `birth.keyed` — see {@link birthInstance}. */
|
|
206
278
|
export const instance = birthInstance;
|
package/src/build_package.ts
CHANGED
|
@@ -80,6 +80,7 @@ import { parentAttestedBirthMarker } from "./births.js";
|
|
|
80
80
|
import { discoverUnsanctionedEnsures, sanctioningEnsureDirective } from "./secondary_ensure.js";
|
|
81
81
|
import { assertIdOfLint } from "./idof_lint.js";
|
|
82
82
|
import { assertSurfacedResolvers } from "./surfaced_lint.js";
|
|
83
|
+
import { expandBusinessBirthExports } from "./business_birth.js";
|
|
83
84
|
|
|
84
85
|
// ─────────────────────────────────────────────────────────────────────────────────
|
|
85
86
|
// composeDomainModule — the named-keys `module(...)`
|
|
@@ -173,6 +174,7 @@ export interface DomainModuleSpec {
|
|
|
173
174
|
export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
174
175
|
let merged: Mod = {};
|
|
175
176
|
for (const m of spec.modules) merged = { ...merged, ...m };
|
|
177
|
+
merged = expandBusinessBirthExports(merged, spec.domain ?? spec.name);
|
|
176
178
|
// `capability()` bundles expand into discoverable entries (the package plane's
|
|
177
179
|
// merge point) — the quartet validation below then passes without re-exports.
|
|
178
180
|
merged = expandCapabilityExports(merged);
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// NOMOS — one business birth at the tenant surface; compute-science mechanics lower here.
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ensure } from "./authoring.js";
|
|
5
|
+
import { directive, type Directive } from "./directive.js";
|
|
6
|
+
import {
|
|
7
|
+
birth,
|
|
8
|
+
requireParentAttestedBirth,
|
|
9
|
+
workspaceNameForKey,
|
|
10
|
+
type BusinessBirthDecl,
|
|
11
|
+
type ParentAttestedBirthMarker,
|
|
12
|
+
} from "./births.js";
|
|
13
|
+
import { readFrameworkDeclared } from "./read.js";
|
|
14
|
+
import { AuthProvider, authProvider, setAuthProvider } from "./framework/auth_provider_law.js";
|
|
15
|
+
import {
|
|
16
|
+
BirthGrant,
|
|
17
|
+
BirthPolicy,
|
|
18
|
+
NomosBirthRecord,
|
|
19
|
+
birthGrantByPrincipal,
|
|
20
|
+
birthPolicy,
|
|
21
|
+
birthRecordsByPrincipal,
|
|
22
|
+
} from "./birth_meter.js";
|
|
23
|
+
import { nomosBirthRecordEnsure } from "./birth_meter_ensure.js";
|
|
24
|
+
import { GENESIS_WARRANT_ID, GenesisWarrant, seedGenesis } from "./framework/genesis.js";
|
|
25
|
+
import {
|
|
26
|
+
BIRTH_ATTESTATION_ID,
|
|
27
|
+
BirthAttestation,
|
|
28
|
+
birthAttestation,
|
|
29
|
+
seedBirthAttestation,
|
|
30
|
+
} from "./framework/home.js";
|
|
31
|
+
import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
|
|
32
|
+
import {
|
|
33
|
+
SignerDelegation,
|
|
34
|
+
enrollDelegation,
|
|
35
|
+
enrollFirstDevice,
|
|
36
|
+
enrollSigner,
|
|
37
|
+
identityRelations,
|
|
38
|
+
signerDelegationByUser,
|
|
39
|
+
} from "./framework/signing_identity.js";
|
|
40
|
+
|
|
41
|
+
/** Private recipe selectors. The kernel resolves these against the parent's current installed law. */
|
|
42
|
+
export const CURRENT_LAW_PREFIX = "nomos:current-law:";
|
|
43
|
+
|
|
44
|
+
export interface BusinessBirthMetadata {
|
|
45
|
+
readonly aggregateId: string;
|
|
46
|
+
readonly publicPayloadSchema: z.ZodObject<z.ZodRawShape>;
|
|
47
|
+
readonly rootId: string;
|
|
48
|
+
readonly namespace: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BusinessBirthDirective extends Directive<Record<string, unknown>> {
|
|
52
|
+
readonly businessBirth: BusinessBirthMetadata;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type BusinessBirthInternalDirective<P = Record<string, unknown>> = Directive<P> & {
|
|
56
|
+
readonly businessBirthInternal: true;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function internalDirective<P>(d: Directive<P>): BusinessBirthInternalDirective<P> {
|
|
60
|
+
return { ...d, businessBirthInternal: true } as BusinessBirthInternalDirective<P>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Presentation-only marker: the declaration remains signed law, but tenant codegen omits it. */
|
|
64
|
+
function internalSurface<T extends object>(value: T): T & { readonly businessBirthInternal: true } {
|
|
65
|
+
return { ...value, businessBirthInternal: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const INTERNAL_FIELDS = ["principal", "bornAt", "ownerKeyHash", "birthCert"] as const;
|
|
69
|
+
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every business-born workspace verifies its own travelling birth certificate against its warrant owner.
|
|
73
|
+
* The locators are framework state, not fields the tenant must add to their business aggregate.
|
|
74
|
+
*/
|
|
75
|
+
const businessParentAttestedBirth: ParentAttestedBirthMarker = {
|
|
76
|
+
...requireParentAttestedBirth,
|
|
77
|
+
ownerObject: GENESIS_WARRANT_ID,
|
|
78
|
+
ownerField: "principal",
|
|
79
|
+
certObject: BIRTH_ATTESTATION_ID,
|
|
80
|
+
certField: "certChain",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function businessBirthsOf(mod: Record<string, unknown>): BusinessBirthDecl[] {
|
|
84
|
+
return Object.values(mod).filter(
|
|
85
|
+
(v): v is BusinessBirthDecl =>
|
|
86
|
+
!!v && typeof v === "object" && (v as { __isBusinessBirth?: unknown }).__isBusinessBirth === true,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unknown> {
|
|
91
|
+
const fields = (decl.payloadSchema as z.ZodObject<z.ZodRawShape>).shape;
|
|
92
|
+
for (const field of INTERNAL_FIELDS) {
|
|
93
|
+
if (Object.prototype.hasOwnProperty.call(fields, field)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`birth(${decl.aggregate.id}): business payload field '${field}' is reserved by Nomos; ` +
|
|
96
|
+
"rename the business field (identity, provenance and lifecycle stay framework-owned)",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const stem = decl.aggregate.id.replace(/[^A-Za-z0-9]/g, "");
|
|
102
|
+
const namespace = (decl.namespace ?? decl.aggregate.id).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
103
|
+
const rootId = `${namespace}:self`;
|
|
104
|
+
const seedId = `nomosSeed${stem}`;
|
|
105
|
+
const birthId = `nomosBirth${stem}`;
|
|
106
|
+
const publicSchema = decl.payloadSchema as z.ZodObject<z.ZodRawShape>;
|
|
107
|
+
const internalSchema = publicSchema.extend({
|
|
108
|
+
principal: z.string().min(1),
|
|
109
|
+
bornAt: z.string().min(1),
|
|
110
|
+
ownerKeyHash: sha256Hex.optional(),
|
|
111
|
+
birthCert: z.string().optional(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const seedBase = directive(seedId)
|
|
115
|
+
.ensures(decl.aggregate)
|
|
116
|
+
.payload(internalSchema)
|
|
117
|
+
.plan((p) => {
|
|
118
|
+
decl.plan(ensure(decl.aggregate, rootId), p);
|
|
119
|
+
return [];
|
|
120
|
+
});
|
|
121
|
+
const seed = {
|
|
122
|
+
...seedBase,
|
|
123
|
+
businessBirthInternal: true,
|
|
124
|
+
} as BusinessBirthInternalDirective;
|
|
125
|
+
|
|
126
|
+
const birthBase = directive(birthId)
|
|
127
|
+
.creates(decl.aggregate)
|
|
128
|
+
.payload(internalSchema)
|
|
129
|
+
.plan((p) => {
|
|
130
|
+
const founder = p.principal as string;
|
|
131
|
+
const child = workspaceNameForKey(namespace, String(decl.key(p)));
|
|
132
|
+
const chain: Array<Record<string, unknown>> = [
|
|
133
|
+
{
|
|
134
|
+
domain: "bootstrap",
|
|
135
|
+
directiveId: "installDomain",
|
|
136
|
+
actor: founder,
|
|
137
|
+
payload: { domainHash: `${CURRENT_LAW_PREFIX}nomos` },
|
|
138
|
+
domainHash: `${CURRENT_LAW_PREFIX}nomos`,
|
|
139
|
+
domainPackageB64: "",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
domain: "nomos",
|
|
143
|
+
directiveId: "installDomain",
|
|
144
|
+
actor: founder,
|
|
145
|
+
payload: { domainHash: `${CURRENT_LAW_PREFIX}${domain}` },
|
|
146
|
+
domainHash: `${CURRENT_LAW_PREFIX}${domain}`,
|
|
147
|
+
domainPackageB64: "",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
domain,
|
|
151
|
+
directiveId: seedId,
|
|
152
|
+
actor: founder,
|
|
153
|
+
payload: p,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const apRows = readFrameworkDeclared<Array<Record<string, unknown>>>("authProvider", { scope: "platform" });
|
|
158
|
+
const apRaw = Array.isArray(apRows) && apRows.length > 0 ? apRows[0] : undefined;
|
|
159
|
+
const ap = (apRaw && typeof apRaw === "object" && typeof (apRaw as { data?: unknown }).data === "object"
|
|
160
|
+
? (apRaw as { data: Record<string, unknown> }).data
|
|
161
|
+
: apRaw) as Record<string, unknown> | undefined;
|
|
162
|
+
const apStr = (k: string): string | undefined => {
|
|
163
|
+
const v = ap?.[k];
|
|
164
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
165
|
+
};
|
|
166
|
+
let verified = false;
|
|
167
|
+
try { verified = typeof p.birthCert === "string" && JSON.parse(p.birthCert).verified === true; } catch {}
|
|
168
|
+
if (typeof p.ownerKeyHash === "string") {
|
|
169
|
+
chain.push({
|
|
170
|
+
domain,
|
|
171
|
+
directiveId: "enrollSigner",
|
|
172
|
+
actor: founder,
|
|
173
|
+
payload: {
|
|
174
|
+
keyHash: p.ownerKeyHash,
|
|
175
|
+
grantedBy: founder,
|
|
176
|
+
grantedAt: 1,
|
|
177
|
+
...(verified && apStr("issuer") ? { verifiedVia: apStr("issuer") } : {}),
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (apStr("issuer")) {
|
|
182
|
+
chain.push({
|
|
183
|
+
domain,
|
|
184
|
+
directiveId: "setAuthProvider",
|
|
185
|
+
actor: founder,
|
|
186
|
+
payload: {
|
|
187
|
+
issuer: apStr("issuer"),
|
|
188
|
+
jwksUrl: apStr("jwksUrl") ?? apStr("issuer"),
|
|
189
|
+
...(apStr("keys") ? { keys: apStr("keys") } : {}),
|
|
190
|
+
...(apStr("audience") ? { audience: apStr("audience") } : {}),
|
|
191
|
+
...(apStr("principalClaim") ? { principalClaim: apStr("principalClaim") } : {}),
|
|
192
|
+
...(apStr("rootCert") ? { rootCert: apStr("rootCert") } : {}),
|
|
193
|
+
...(apStr("deviceBindingClaim") ? { deviceBindingClaim: apStr("deviceBindingClaim") } : {}),
|
|
194
|
+
...(apStr("requireDeviceBinding") ? { requireDeviceBinding: apStr("requireDeviceBinding") } : {}),
|
|
195
|
+
label: apStr("label") ?? "inherited-from-parent",
|
|
196
|
+
setBy: founder,
|
|
197
|
+
setAt: 1,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
birth({ workspace: child, genesisChain: chain });
|
|
202
|
+
return [];
|
|
203
|
+
})
|
|
204
|
+
.reads(authProvider)
|
|
205
|
+
.births();
|
|
206
|
+
|
|
207
|
+
const metadata: BusinessBirthMetadata = {
|
|
208
|
+
aggregateId: decl.aggregate.id,
|
|
209
|
+
publicPayloadSchema: publicSchema,
|
|
210
|
+
rootId,
|
|
211
|
+
namespace,
|
|
212
|
+
};
|
|
213
|
+
const parent = { ...birthBase, businessBirth: metadata } as BusinessBirthDirective;
|
|
214
|
+
return {
|
|
215
|
+
[`__nomos_${birthId}`]: parent,
|
|
216
|
+
[`__nomos_${seedId}`]: seed,
|
|
217
|
+
|
|
218
|
+
// THE SOVEREIGN-WORKSPACE CORE. These are ordinary law declarations, composed automatically so the
|
|
219
|
+
// tenant's one business declaration is also a complete child law: it can fold the kernel-injected
|
|
220
|
+
// travelling attestation + warrant, seat its founding device, inherit its verifier, and verify itself
|
|
221
|
+
// offline after custody is moved or reopened. The generated tenant client hides every one of them.
|
|
222
|
+
__nomos_GenesisWarrant: internalSurface(GenesisWarrant),
|
|
223
|
+
__nomos_seedGenesis: internalDirective(seedGenesis),
|
|
224
|
+
__nomos_BirthAttestation: internalSurface(BirthAttestation),
|
|
225
|
+
__nomos_seedBirthAttestation: internalDirective(seedBirthAttestation),
|
|
226
|
+
__nomos_birthAttestation: internalSurface(birthAttestation),
|
|
227
|
+
__nomos_RelationTuple: internalSurface(RelationTuple),
|
|
228
|
+
__nomos_relationTuplesByObject: internalSurface(relationTuplesByObject),
|
|
229
|
+
__nomos_identityRelations: identityRelations,
|
|
230
|
+
__nomos_enrollSigner: internalDirective(enrollSigner),
|
|
231
|
+
__nomos_enrollFirstDevice: internalDirective(enrollFirstDevice),
|
|
232
|
+
__nomos_SignerDelegation: internalSurface(SignerDelegation),
|
|
233
|
+
__nomos_enrollDelegation: internalDirective(enrollDelegation),
|
|
234
|
+
__nomos_signerDelegationByUser: internalSurface(signerDelegationByUser),
|
|
235
|
+
__nomos_AuthProvider: internalSurface(AuthProvider),
|
|
236
|
+
__nomos_setAuthProvider: internalDirective(setAuthProvider),
|
|
237
|
+
__nomos_authProvider: internalSurface(authProvider),
|
|
238
|
+
// Exact export keys prevent the generic `.births()` auto-compose seam from inserting a second,
|
|
239
|
+
// unmarked copy. The values are still the canonical framework declarations.
|
|
240
|
+
NomosBirthRecord: internalSurface(NomosBirthRecord),
|
|
241
|
+
BirthPolicy: internalSurface(BirthPolicy),
|
|
242
|
+
BirthGrant: internalSurface(BirthGrant),
|
|
243
|
+
nomosBirthRecordEnsure: internalDirective(nomosBirthRecordEnsure),
|
|
244
|
+
__nomos_birthRecordsByPrincipal: internalSurface(birthRecordsByPrincipal),
|
|
245
|
+
__nomos_birthPolicy: internalSurface(birthPolicy),
|
|
246
|
+
__nomos_birthGrantByPrincipal: internalSurface(birthGrantByPrincipal),
|
|
247
|
+
__nomos_parentAttestedBirth: businessParentAttestedBirth,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Lower the one tenant-facing birth declaration into ordinary, gate-visible framework directives. */
|
|
252
|
+
export function expandBusinessBirthExports<M extends Record<string, unknown>>(
|
|
253
|
+
mod: M,
|
|
254
|
+
domain: string,
|
|
255
|
+
): Record<string, unknown> {
|
|
256
|
+
const declarations = businessBirthsOf(mod);
|
|
257
|
+
if (declarations.length === 0) return mod;
|
|
258
|
+
if (declarations.length > 1) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`domain '${domain}' declares ${declarations.length} business births; one generated birth(payload) cannot be ` +
|
|
261
|
+
"ambiguous — model one workspace root per domain",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const lowered = lowerOne(declarations[0]!, domain);
|
|
265
|
+
return { ...mod, ...lowered };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function isBusinessBirthDirective(value: unknown): value is BusinessBirthDirective {
|
|
269
|
+
return !!value && typeof value === "object" && !!(value as { businessBirth?: unknown }).businessBirth;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function isBusinessBirthInternalDirective(value: unknown): boolean {
|
|
273
|
+
return !!value && typeof value === "object" && (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
|
|
274
|
+
}
|
package/src/codegen_dart.ts
CHANGED
|
@@ -4243,8 +4243,36 @@ export interface GenerateDartDomainOptions {
|
|
|
4243
4243
|
|
|
4244
4244
|
/** Generate the full Dart source for one domain module. */
|
|
4245
4245
|
export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOptions = {}): string {
|
|
4246
|
+
const publicAggregates = mod.aggregates.filter(
|
|
4247
|
+
(a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
4248
|
+
);
|
|
4249
|
+
const publicAggregateIds = new Set(publicAggregates.map((a) => a.id));
|
|
4250
|
+
mod = {
|
|
4251
|
+
...mod,
|
|
4252
|
+
aggregates: publicAggregates,
|
|
4253
|
+
directives: mod.directives.filter(
|
|
4254
|
+
(d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
4255
|
+
),
|
|
4256
|
+
...(mod.queries === undefined
|
|
4257
|
+
? {}
|
|
4258
|
+
: { queries: mod.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
|
|
4259
|
+
...(mod.readModelAggregates === undefined
|
|
4260
|
+
? {}
|
|
4261
|
+
: { readModelAggregates: mod.readModelAggregates.filter((id) => publicAggregateIds.has(id)) }),
|
|
4262
|
+
};
|
|
4246
4263
|
registerDartNames(mod.aggregates); // `.dartName` overrides for this domain's Dart naming derivations.
|
|
4247
4264
|
const domainKey = mod.domain ?? mod.name;
|
|
4265
|
+
// ONE BUSINESS BIRTH: framework-only genesis/control directives remain law, but are not tenant DTOs.
|
|
4266
|
+
// The one public parent directive is rendered against the business payload schema (identity/provenance
|
|
4267
|
+
// fields are injected by the runtime). This is frontend codegen only; the composed manifest is untouched.
|
|
4268
|
+
const codegenDirectives = mod.directives
|
|
4269
|
+
.filter((d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true)
|
|
4270
|
+
.map((d) => {
|
|
4271
|
+
const businessBirth = (d as { businessBirth?: { publicPayloadSchema?: z.ZodTypeAny } }).businessBirth;
|
|
4272
|
+
return businessBirth?.publicPayloadSchema === undefined
|
|
4273
|
+
? d
|
|
4274
|
+
: { ...d, payloadSchema: businessBirth.publicPayloadSchema } as AnyDirective;
|
|
4275
|
+
});
|
|
4248
4276
|
const skipAggregateIds = opts.skipAggregateIds ?? new Set<string>();
|
|
4249
4277
|
const skipDirectiveIds = opts.skipDirectiveIds ?? new Set<string>();
|
|
4250
4278
|
const skipQueryIds = opts.skipQueryIds ?? new Set<string>();
|
|
@@ -4255,11 +4283,11 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
4255
4283
|
// their generated class — collected from the branded payload schemas, deduped by name against
|
|
4256
4284
|
// the aggregate-derived set. Codegen-only: the IR key stays aggregate-derived (era rule).
|
|
4257
4285
|
const aggVoNames = new Set((mod.valueObjects ?? []).map((d) => d.name));
|
|
4258
|
-
const payloadVos = collectPayloadValueObjectDescriptors(
|
|
4286
|
+
const payloadVos = collectPayloadValueObjectDescriptors(codegenDirectives).filter((d) => !aggVoNames.has(d.name));
|
|
4259
4287
|
// RECURSIVE `z.lazy` payloads → synthesized recursive-class descriptors (full support). They
|
|
4260
4288
|
// are codegen-only (like payloadVos): the law IR / domainHash never sees them (era rule). The
|
|
4261
4289
|
// name map routes each recursive payload field to its class in [emitDirective].
|
|
4262
|
-
const recursiveTypes = synthesizeRecursivePayloadTypes(
|
|
4290
|
+
const recursiveTypes = synthesizeRecursivePayloadTypes(codegenDirectives);
|
|
4263
4291
|
const recursiveDescriptors = [...recursiveTypes.descriptors.values()].filter(
|
|
4264
4292
|
(d) => !aggVoNames.has(d.name) && !payloadVos.some((p) => p.name === d.name),
|
|
4265
4293
|
);
|
|
@@ -4289,7 +4317,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
4289
4317
|
if ("fields" in arm) for (const f of Object.values(arm.fields)) walkUnitField(f);
|
|
4290
4318
|
}
|
|
4291
4319
|
}
|
|
4292
|
-
for (const dir of
|
|
4320
|
+
for (const dir of codegenDirectives) {
|
|
4293
4321
|
const schema = (dir as { payloadSchema?: unknown }).payloadSchema;
|
|
4294
4322
|
if (schema && typeof schema === "object" && zodTypeName(schema as z.ZodTypeAny) === "ZodObject") {
|
|
4295
4323
|
try {
|
|
@@ -4353,7 +4381,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
4353
4381
|
f.valueObject === undefined),
|
|
4354
4382
|
),
|
|
4355
4383
|
);
|
|
4356
|
-
const localDirectives =
|
|
4384
|
+
const localDirectives = codegenDirectives.filter((d) => !skipDirectiveIds.has(d.id));
|
|
4357
4385
|
const localQueries = (mod.queries ?? []).filter((q) => !skipQueryIds.has(q.id));
|
|
4358
4386
|
// Enums are collected from the LOCALLY-emitted symbols only — a framework directive/aggregate's
|
|
4359
4387
|
// enums ride the shared `nomos_framework.dart`, so a domain that skips them emits no dangling enum.
|
|
@@ -4367,7 +4395,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
4367
4395
|
for (const a of mod.aggregates) aggregatesById.set(a.id, a);
|
|
4368
4396
|
const ownIdFieldsByAggregate = collectOwnIdFieldsByAggregate(
|
|
4369
4397
|
mod.aggregates,
|
|
4370
|
-
|
|
4398
|
+
codegenDirectives,
|
|
4371
4399
|
dartTypes,
|
|
4372
4400
|
);
|
|
4373
4401
|
const enumBlock = decls.join("\n\n");
|
|
@@ -167,6 +167,23 @@ export interface DartBridgeClientOptions {
|
|
|
167
167
|
|
|
168
168
|
/** Generate the `<name>.client.dart` async bridge client for one domain module. */
|
|
169
169
|
export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClientOptions): string {
|
|
170
|
+
const publicAggregates = mod.aggregates.filter(
|
|
171
|
+
(a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
172
|
+
);
|
|
173
|
+
const publicAggregateIds = new Set(publicAggregates.map((a) => a.id));
|
|
174
|
+
mod = {
|
|
175
|
+
...mod,
|
|
176
|
+
aggregates: publicAggregates,
|
|
177
|
+
directives: mod.directives.filter(
|
|
178
|
+
(d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
179
|
+
),
|
|
180
|
+
...(mod.queries === undefined
|
|
181
|
+
? {}
|
|
182
|
+
: { queries: mod.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
|
|
183
|
+
...(mod.readModelAggregates === undefined
|
|
184
|
+
? {}
|
|
185
|
+
: { readModelAggregates: mod.readModelAggregates.filter((id) => publicAggregateIds.has(id)) }),
|
|
186
|
+
};
|
|
170
187
|
registerDartNames(mod.aggregates); // `.dartName` overrides for read-model class names in the client.
|
|
171
188
|
const domain = mod.domain ?? mod.name;
|
|
172
189
|
const className = dartClientClassName(domain);
|
|
@@ -223,7 +240,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
223
240
|
// cross-workspace governance offer to a PARENT. It gets a typed `bridge.createWorkspace(...)` wrapper
|
|
224
241
|
// (below), NOT an `offerOutcome`; excluded here so a frontend dev can never call it as a local write.
|
|
225
242
|
const allDirs = (mod.directives as AnyDirective[]).filter(
|
|
226
|
-
(d) => d && typeof d.id === "string"
|
|
243
|
+
(d) => d && typeof d.id === "string"
|
|
244
|
+
&& typeof (d as { payloadSchema?: unknown }).payloadSchema === "object"
|
|
245
|
+
&& (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
227
246
|
);
|
|
228
247
|
const birthDirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild === true);
|
|
229
248
|
const dirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild !== true);
|
|
@@ -324,6 +343,32 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
324
343
|
const payloadClass = `${cap(d.id)}Payload`;
|
|
325
344
|
const method = safeMethod(d.id);
|
|
326
345
|
const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
|
|
346
|
+
const businessBirth = (d as unknown as {
|
|
347
|
+
businessBirth?: { aggregateId: string; rootId: string };
|
|
348
|
+
}).businessBirth;
|
|
349
|
+
if (businessBirth !== undefined) {
|
|
350
|
+
methods.push(
|
|
351
|
+
` /// Give one ${businessBirth.aggregateId} its own durable life. Nomos owns identity proof, signer`,
|
|
352
|
+
` /// seating, provenance, persistence and recovery; the app supplies only business facts.`,
|
|
353
|
+
` Future<NomosRef> birth(${payloadClass} payload) async {`,
|
|
354
|
+
` final principal = actor ?? bridge.principal;`,
|
|
355
|
+
` if (principal == null || principal.trim().isEmpty) {`,
|
|
356
|
+
` throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: 'a signed-in Nomos session');`,
|
|
357
|
+
` }`,
|
|
358
|
+
` final data = payload.toPayloadJson();`,
|
|
359
|
+
` data['principal'] = principal;`,
|
|
360
|
+
` data['bornAt'] = DateTime.now().toUtc().toIso8601String();`,
|
|
361
|
+
` final outcome = await _offer(${dartStr(d.id)}, data);`,
|
|
362
|
+
` if (outcome.born.length != 1) {`,
|
|
363
|
+
` throw StateError('birth() committed without returning exactly one born workspace');`,
|
|
364
|
+
` }`,
|
|
365
|
+
` await bridge.sync();`,
|
|
366
|
+
` return NomosRef(workspace: outcome.born.single, type: ${dartStr(businessBirth.aggregateId)}, id: ${dartStr(businessBirth.rootId)});`,
|
|
367
|
+
` }`,
|
|
368
|
+
``,
|
|
369
|
+
);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
327
372
|
methods.push(
|
|
328
373
|
` /// Births a CHILD WORKSPACE named by \`${payloadClass}\` — a \`.births()\` offer on THIS client's`,
|
|
329
374
|
` /// session, whose workspace IS the parent (session_bound_clients.md ask 4: no loose parent string).`,
|
package/src/codegen_openapi.ts
CHANGED
|
@@ -99,6 +99,18 @@ export function generateOpenApiFragment(
|
|
|
99
99
|
module: DomainModule,
|
|
100
100
|
opts: OpenApiFragmentOptions = {},
|
|
101
101
|
): OpenApiDoc {
|
|
102
|
+
module = {
|
|
103
|
+
...module,
|
|
104
|
+
aggregates: module.aggregates.filter(
|
|
105
|
+
(a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
106
|
+
),
|
|
107
|
+
directives: module.directives.filter(
|
|
108
|
+
(d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
109
|
+
),
|
|
110
|
+
...(module.queries === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { queries: module.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
|
|
113
|
+
};
|
|
102
114
|
const domain = domainKey(module);
|
|
103
115
|
const paths: Record<string, Record<string, unknown>> = {};
|
|
104
116
|
|
|
@@ -111,12 +123,22 @@ export function generateOpenApiFragment(
|
|
|
111
123
|
|
|
112
124
|
// OFFERABLE INTENTS → POST offers. The payload JSON Schema is the intent's own zod, lowered.
|
|
113
125
|
for (const dir of module.directives as AnyDirective[]) {
|
|
126
|
+
if ((dir as { businessBirthInternal?: unknown }).businessBirthInternal === true) continue;
|
|
127
|
+
const businessBirth = (dir as {
|
|
128
|
+
businessBirth?: { aggregateId: string; publicPayloadSchema: unknown };
|
|
129
|
+
}).businessBirth;
|
|
130
|
+
const publicId = businessBirth === undefined ? dir.id : "birth";
|
|
131
|
+
const publicDir = businessBirth === undefined
|
|
132
|
+
? dir
|
|
133
|
+
: { ...dir, payloadSchema: businessBirth.publicPayloadSchema } as AnyDirective;
|
|
114
134
|
const invs = invariantsOf(dir.id);
|
|
115
135
|
const op: Record<string, unknown> = {
|
|
116
|
-
operationId: `${domain}__${
|
|
136
|
+
operationId: `${domain}__${publicId}`,
|
|
117
137
|
tags: [domain],
|
|
118
|
-
summary: `offer: ${dir.id}`,
|
|
119
|
-
description:
|
|
138
|
+
summary: businessBirth === undefined ? `offer: ${dir.id}` : `birth ${businessBirth.aggregateId}`,
|
|
139
|
+
description: businessBirth !== undefined
|
|
140
|
+
? `Give one ${businessBirth.aggregateId} its own durable life.`
|
|
141
|
+
: dir.marker && dir.aggregateId
|
|
120
142
|
? `Offer the \`${dir.id}\` intent (${dir.marker} ${dir.aggregateId}). Admitted or refused by the law; the plan is private.`
|
|
121
143
|
: `Offer the \`${dir.id}\` intent. Admitted or refused by the law.`,
|
|
122
144
|
"x-nomos": {
|
|
@@ -130,7 +152,7 @@ export function generateOpenApiFragment(
|
|
|
130
152
|
},
|
|
131
153
|
requestBody: {
|
|
132
154
|
required: true,
|
|
133
|
-
content: { "application/json": { schema: payloadJsonSchema(
|
|
155
|
+
content: { "application/json": { schema: payloadJsonSchema(publicDir, module) } },
|
|
134
156
|
},
|
|
135
157
|
responses: {
|
|
136
158
|
"200": { description: "admitted — the sealed intent head" },
|
|
@@ -138,7 +160,7 @@ export function generateOpenApiFragment(
|
|
|
138
160
|
"422": { description: "the law refused the intent (typed refusal)" },
|
|
139
161
|
},
|
|
140
162
|
};
|
|
141
|
-
(paths[`/${
|
|
163
|
+
(paths[`/${publicId}`] ??= {}).post = op;
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
// QUERIES → GET reads. Declared queries + the implicit hasMany inverse indexes (the relation IS
|
package/src/codegen_proof.ts
CHANGED
|
@@ -2346,6 +2346,15 @@ function hasBirthDirective(mod: DomainModule): boolean {
|
|
|
2346
2346
|
* when it has a `.creates`; child-birth-provable when it has a `.births()`). NEVER null: every compiled
|
|
2347
2347
|
* domain is at least `compile-valid` (gaps 1+3 — every domain is proof-addressable). */
|
|
2348
2348
|
export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[]): ProofLegs {
|
|
2349
|
+
// Business-birth control declarations are proofed as part of the birth's child genesis, not as
|
|
2350
|
+
// tenant-authored standalone/gated actions. Keep the full `modules` below for recipe resolution while
|
|
2351
|
+
// removing the presentation-only internals from leg selection and diagnostics.
|
|
2352
|
+
mod = {
|
|
2353
|
+
...mod,
|
|
2354
|
+
directives: (mod.directives as Directive<unknown>[]).filter(
|
|
2355
|
+
(d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
2356
|
+
),
|
|
2357
|
+
};
|
|
2349
2358
|
const domain = mod.domain ?? mod.name;
|
|
2350
2359
|
const allBirths = modules.flatMap((m) => childBirthsForModule(m, modules));
|
|
2351
2360
|
// The standalone-create leg (when the domain has a non-birth `.creates`). FAIL SOFT: an exotic payload
|
package/src/codegen_ts.ts
CHANGED
|
@@ -800,6 +800,27 @@ function anyPackedTaxonomy(modules: readonly DomainModule[]): boolean {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
export function generateTsClient(modules: readonly DomainModule[], opts: TsClientOptions): string {
|
|
803
|
+
// Business-birth framework declarations remain in the certified module, but are not application
|
|
804
|
+
// vocabulary. Strip only the presentation marker here; manifests, hashes and the engine see the full law.
|
|
805
|
+
modules = modules.map((mod) => {
|
|
806
|
+
const publicAggregates = mod.aggregates.filter(
|
|
807
|
+
(a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
808
|
+
);
|
|
809
|
+
const publicAggregateIds = new Set(publicAggregates.map((a) => a.id));
|
|
810
|
+
return {
|
|
811
|
+
...mod,
|
|
812
|
+
aggregates: publicAggregates,
|
|
813
|
+
directives: mod.directives.filter(
|
|
814
|
+
(d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
815
|
+
),
|
|
816
|
+
...(mod.queries === undefined
|
|
817
|
+
? {}
|
|
818
|
+
: { queries: mod.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
|
|
819
|
+
...(mod.readModelAggregates === undefined
|
|
820
|
+
? {}
|
|
821
|
+
: { readModelAggregates: mod.readModelAggregates.filter((id) => publicAggregateIds.has(id)) }),
|
|
822
|
+
};
|
|
823
|
+
});
|
|
803
824
|
const hashConst = tsHashConstName(opts.packageName);
|
|
804
825
|
// THE E2E-SEALING MAP (the sealing client): non-null iff any field is `.encrypted()`. When null,
|
|
805
826
|
// NOTHING below emits — the generated client is byte-identical to a pre-sealing compile.
|
|
@@ -812,7 +833,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
812
833
|
// A package declaring ≥1 `.births()` directive emits the branded identity wrappers (below) its typed
|
|
813
834
|
// birth methods take for `actor`/`authorSecret`. Births-free packages emit nothing (byte-identical).
|
|
814
835
|
const hasBirths = modules.some((m) =>
|
|
815
|
-
(m.directives as Directive<unknown>[]).some((d) =>
|
|
836
|
+
(m.directives as Directive<unknown>[]).some((d) =>
|
|
837
|
+
(d as { birthsChild?: boolean })?.birthsChild === true
|
|
838
|
+
&& (d as { businessBirth?: unknown }).businessBirth === undefined,
|
|
839
|
+
),
|
|
816
840
|
);
|
|
817
841
|
// SURFACED MERGE CONFLICTS: a package declaring ≥1 `.merge(Surfaced(...))` field emits the
|
|
818
842
|
// `SurfacedValue<T>` union + guard (below). Surfaced-free packages emit nothing (byte-identical).
|
|
@@ -869,6 +893,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
869
893
|
` * aggregate instance: \`{ workspace, type, id }\`, with the stable interop string`,
|
|
870
894
|
` * \`nomos://<workspace>/<type>/<id>\`. Carried on every read \`Row.ref\` + write outcome + activity ref. */`,
|
|
871
895
|
`export interface NomosRef { workspace: string; type: string; id: string }`,
|
|
896
|
+
`/** A newly born business root. The phantom type makes the returned reference aggregate-specific while`,
|
|
897
|
+
` * its runtime identity stays the one canonical {workspace,type,id} NomosRef. */`,
|
|
898
|
+
`export type NomosBorn<TData> = NomosRef & { readonly __nomosData?: TData };`,
|
|
872
899
|
`/** The stable \`nomos://<workspace>/<type>/<id>\` string form (each segment URI-encoded — a workspace/`,
|
|
873
900
|
` * type/id containing \`/\` or spaces round-trips). Survives URLs, nav params, JSON, logs. */`,
|
|
874
901
|
`export function nomosRefUri(ref: NomosRef): string {`,
|
|
@@ -1025,7 +1052,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1025
1052
|
` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
|
|
1026
1053
|
` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
|
|
1027
1054
|
` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
|
|
1028
|
-
` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean; attestations?: readonly { queryId: string; result: unknown; attestation: unknown }[] }): Promise<string | { head: string; created?: { type: string; id: string }[]; workspace?: string }>;`,
|
|
1055
|
+
` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean; attestations?: readonly { queryId: string; result: unknown; attestation: unknown }[] }): Promise<string | { head: string; created?: { type: string; id: string }[]; workspace?: string; born?: unknown; birthOutcomes?: unknown }>;`,
|
|
1029
1056
|
` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
|
|
1030
1057
|
` offer(intentBytes: string | Uint8Array): Promise<string>;`,
|
|
1031
1058
|
...(hasBirths
|
|
@@ -1038,6 +1065,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1038
1065
|
: []),
|
|
1039
1066
|
` /** The workspace this holon is bound to — the \`workspace\` of every [NomosRef] this client composes. */`,
|
|
1040
1067
|
` workspace: string;`,
|
|
1068
|
+
` /** The identity already proved by this bound session (explicit principal or token subject). */`,
|
|
1069
|
+
` principal?: string;`,
|
|
1070
|
+
` /** Persist local custody and reconcile its configured remote. Business birth awaits this when available. */`,
|
|
1071
|
+
` sync?(): Promise<unknown>;`,
|
|
1041
1072
|
...(encPresent
|
|
1042
1073
|
? [
|
|
1043
1074
|
` /** \`keys\` (E2E sealing): the reader's held scope keys, injected so encrypted fields decrypt;`,
|
|
@@ -1808,7 +1839,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1808
1839
|
for (const mod of modules) {
|
|
1809
1840
|
const domain = mod.domain ?? mod.name;
|
|
1810
1841
|
const dirs = (mod.directives as Directive<unknown>[]).filter(
|
|
1811
|
-
(d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object"
|
|
1842
|
+
(d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object"
|
|
1843
|
+
&& (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
1812
1844
|
);
|
|
1813
1845
|
out.push(`// ── domain "${domain}" ──`, ``);
|
|
1814
1846
|
|
|
@@ -1849,7 +1881,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1849
1881
|
if (usedPayloadNames.has(payloadName)) payloadName = `${cap(camel(domain))}${cap(d.id)}Payload`;
|
|
1850
1882
|
usedPayloadNames.add(payloadName);
|
|
1851
1883
|
dirPayloadNames.set(d.id, payloadName);
|
|
1852
|
-
const
|
|
1884
|
+
const businessBirth = (d as unknown as {
|
|
1885
|
+
businessBirth?: { aggregateId: string; publicPayloadSchema: z.ZodTypeAny; rootId: string; namespace: string };
|
|
1886
|
+
}).businessBirth;
|
|
1887
|
+
const schema = businessBirth?.publicPayloadSchema
|
|
1888
|
+
?? (d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema;
|
|
1853
1889
|
const stamps = autoStampFields(schema);
|
|
1854
1890
|
const mint = mintPlan?.get(d.id);
|
|
1855
1891
|
const placed = placementDirs.has(d.id) ? ["shard"] : [];
|
|
@@ -1897,6 +1933,24 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1897
1933
|
`export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed, lazyReg, d.id)};`,
|
|
1898
1934
|
``,
|
|
1899
1935
|
);
|
|
1936
|
+
// ONE BUSINESS BIRTH: the session is the parent; identity, timestamp, child addressing, law selection,
|
|
1937
|
+
// provenance and device seating are internal. The app supplies only the declared business payload and
|
|
1938
|
+
// receives the born root's typed NomosRef.
|
|
1939
|
+
if (businessBirth !== undefined) {
|
|
1940
|
+
methods.push(
|
|
1941
|
+
` /** Give one ${businessBirth.aggregateId} its own durable life. Nomos owns identity, provenance, persistence and recovery. */`,
|
|
1942
|
+
` birth: async (payload: ${payloadName}): Promise<NomosBorn<${businessBirth.aggregateId}Data>> => {`,
|
|
1943
|
+
` const principal = actor ?? holon.principal;`,
|
|
1944
|
+
` if (!principal || !principal.trim()) throw new Error("birth() needs a signed-in Nomos session");`,
|
|
1945
|
+
` const result = await holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, { ...payload, principal, bornAt: new Date().toISOString() }, { domainHash, actor: principal, returnCreated: true });`,
|
|
1946
|
+
` const born = typeof result === "object" && result !== null && Array.isArray(result.born) ? result.born[0] : undefined;`,
|
|
1947
|
+
` if (typeof born !== "object" || born === null || typeof born.workspace !== "string" || !born.workspace) throw new Error("birth() committed without returning the born workspace");`,
|
|
1948
|
+
` await holon.sync?.();`,
|
|
1949
|
+
` return { workspace: born.workspace, type: ${JSON.stringify(businessBirth.aggregateId)}, id: ${JSON.stringify(businessBirth.rootId)} };`,
|
|
1950
|
+
` },`,
|
|
1951
|
+
);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1900
1954
|
// A `.births()` directive is NOT a same-workspace write — it is a signed CROSS-WORKSPACE governance
|
|
1901
1955
|
// offer to a PARENT. It gets a typed `holon.createWorkspace(...)` wrapper (declaring domain +
|
|
1902
1956
|
// directiveId + baked domainHash bound at codegen time), NOT an `offerDirective`, so a frontend dev
|
|
@@ -2118,7 +2172,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2118
2172
|
` * judge the real author. Omit it for open (un-gated) domains. */`,
|
|
2119
2173
|
`export function ${tsClientFactoryName(domain)}(holon: NomosHolon, opts: { domainHash?: string; actor?: string${encPresent ? "; keyStore?: NomosKeyStore" : ""} } = {}) {`,
|
|
2120
2174
|
` const domainHash = opts.domainHash ?? ${hashConst};`,
|
|
2121
|
-
` const actor = opts.actor;`,
|
|
2175
|
+
` const actor = opts.actor ?? holon.principal;`,
|
|
2122
2176
|
...(encPresent ? [` const keyStore = opts.keyStore;`] : []),
|
|
2123
2177
|
...(opts.compat !== undefined
|
|
2124
2178
|
? [
|
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
import { synthesizePreconditionInvariants } from "./precondition.js";
|
|
94
94
|
import { BIRTH_METER_QUERIES, synthesizeBirthQuotaInvariants } from "./birth_meter.js";
|
|
95
95
|
import { withBirthMeterExports } from "./birth_meter_ensure.js";
|
|
96
|
+
import { expandBusinessBirthExports } from "./business_birth.js";
|
|
96
97
|
import { expandRoleExports, rolesOf } from "./role.js";
|
|
97
98
|
import { collectEngineRouting } from "./engine_entry.js";
|
|
98
99
|
import {
|
|
@@ -1051,7 +1052,7 @@ const NOMOS_TYPES_DEP = "^0.6.0";
|
|
|
1051
1052
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
1052
1053
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
1053
1054
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
1054
|
-
const NOMOS_CLIENT_DEP = "^0.
|
|
1055
|
+
const NOMOS_CLIENT_DEP = "^0.62.0";
|
|
1055
1056
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
1056
1057
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
1057
1058
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|
|
@@ -1616,6 +1617,7 @@ async function main(): Promise<void> {
|
|
|
1616
1617
|
}
|
|
1617
1618
|
let merged: Mod = {};
|
|
1618
1619
|
for (const m of mods) merged = { ...merged, ...m };
|
|
1620
|
+
merged = expandBusinessBirthExports(merged, d.key);
|
|
1619
1621
|
// `capability()` bundles expand into discoverable entries (the CLI plane's
|
|
1620
1622
|
// merge point) — queries/sums/sid-minting/manifests all see the pieces.
|
|
1621
1623
|
merged = expandCapabilityExports(merged);
|
package/src/engine_entry.ts
CHANGED
|
@@ -80,6 +80,7 @@ import type { CombinedDecl } from "./combined.js";
|
|
|
80
80
|
import type { InvariantBody, InvariantEvidence, InvariantVerdict } from "./relation.js";
|
|
81
81
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
82
82
|
import type { QueryRow } from "./report.js";
|
|
83
|
+
import { expandBusinessBirthExports } from "./business_birth.js";
|
|
83
84
|
|
|
84
85
|
/** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
|
|
85
86
|
export type DomainModuleExports = Record<string, unknown>;
|
|
@@ -244,9 +245,10 @@ function combinedsOf(mod: DomainModuleExports): CombinedDecl[] {
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
/** Spread-merge a domain key's ORDERED module list (later wins on collision). */
|
|
247
|
-
function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports {
|
|
248
|
+
function mergeModules(mods: readonly DomainModuleExports[], domain: string): DomainModuleExports {
|
|
248
249
|
let merged: DomainModuleExports = {};
|
|
249
250
|
for (const m of mods) merged = { ...merged, ...m };
|
|
251
|
+
merged = expandBusinessBirthExports(merged, domain);
|
|
250
252
|
// `capability()` bundles expand into discoverable entries here — the engine
|
|
251
253
|
// plane's ONE merge point, so the registry sees the task aggregate + quartet
|
|
252
254
|
// without per-piece re-exports (capability_exports.ts).
|
|
@@ -465,7 +467,7 @@ export function collectEngineRouting(
|
|
|
465
467
|
if (!list.includes(domainName)) list.push(domainName);
|
|
466
468
|
};
|
|
467
469
|
for (const [domainName, mods] of Object.entries(domains)) {
|
|
468
|
-
const slice = buildDomainSlice(mergeModules(mods));
|
|
470
|
+
const slice = buildDomainSlice(mergeModules(mods, domainName));
|
|
469
471
|
// Type-keyed dispatches arrive with the ROW's type — QUALIFIED for a namespaced aggregate (the
|
|
470
472
|
// runtime `__type` stamp), bare for a global one / a pre-namespacing row. Route BOTH keys so
|
|
471
473
|
// either era resolves to the declaring domain (the slice lookup strips the qualifier back off).
|
|
@@ -551,7 +553,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
551
553
|
const sources = config.domains[domainName];
|
|
552
554
|
if (sources === undefined) return undefined;
|
|
553
555
|
const forced = sources.map((m) => (typeof m === "function" ? m() : m));
|
|
554
|
-
const slice = buildDomainSlice(mergeModules(forced));
|
|
556
|
+
const slice = buildDomainSlice(mergeModules(forced, domainName));
|
|
555
557
|
sliceCache.set(domainName, slice);
|
|
556
558
|
return slice;
|
|
557
559
|
}
|
|
@@ -943,7 +945,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
943
945
|
for (const domainName of domainNames) {
|
|
944
946
|
const sources = config.domains[domainName];
|
|
945
947
|
if (sources === undefined) continue;
|
|
946
|
-
const merged = mergeModules(sources.map((m) => (typeof m === "function" ? m() : m)));
|
|
948
|
+
const merged = mergeModules(sources.map((m) => (typeof m === "function" ? m() : m)), domainName);
|
|
947
949
|
// Aggregates dedup by wire id + RelationTuple auto-ensure (the compose contract) — openapi
|
|
948
950
|
// reads aggregates only for the hasMany inverse indexes, so this matches composeDomainModule.
|
|
949
951
|
const byId = aggregatesOf(merged);
|
package/src/index.ts
CHANGED
|
@@ -221,6 +221,8 @@ export {
|
|
|
221
221
|
birthInstance,
|
|
222
222
|
type BirthRecipe,
|
|
223
223
|
type GenesisChainOrBuilder,
|
|
224
|
+
type BusinessBirthDecl,
|
|
225
|
+
type BusinessBirthSpec,
|
|
224
226
|
} from "./births.js";
|
|
225
227
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain): a home/child law exports
|
|
226
228
|
// `requireParentAttestedBirth` to declare it requires a parent-attested-birth cert chain (the
|
package/src/law_check.ts
CHANGED
|
@@ -371,6 +371,9 @@ function planReturnFindings(domain: string, dir: Directive<unknown>, probe: Prob
|
|
|
371
371
|
// Framework-consumed convention fields (auto-stamped `…At`, birth subject/time) are excluded.
|
|
372
372
|
function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: ProbeResult): Finding[] {
|
|
373
373
|
if (probe.threw) return [];
|
|
374
|
+
// A business birth's public fields are consumed by its hidden child-seed directive. That seed is linted
|
|
375
|
+
// independently; warning again on the parent half is a false positive caused by the lowering boundary.
|
|
376
|
+
if ((dir as { businessBirth?: unknown }).businessBirth !== undefined) return [];
|
|
374
377
|
const stamped = new Set(autoStampFields(dir.payloadSchema as z.ZodTypeAny));
|
|
375
378
|
// The GATE reads some payload fields the PLAN never does: `.requires(rel, { objectFrom })` derives
|
|
376
379
|
// the gate object from a named payload id field (per-record authz). Excluding it keeps a real
|
|
@@ -379,6 +382,14 @@ function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: P
|
|
|
379
382
|
const objectFromField = (dir as { requiresRelationGate?: { objectFrom?: { idField?: string } } })
|
|
380
383
|
.requiresRelationGate?.objectFrom?.idField;
|
|
381
384
|
if (typeof objectFromField === "string") gateConsumed.add(objectFromField);
|
|
385
|
+
const firstDeviceGate = (dir as {
|
|
386
|
+
firstDeviceEnrollGate?: { tokenField: string; keyHashField: string; verifiedViaField: string };
|
|
387
|
+
}).firstDeviceEnrollGate;
|
|
388
|
+
if (firstDeviceGate !== undefined) {
|
|
389
|
+
gateConsumed.add(firstDeviceGate.tokenField);
|
|
390
|
+
gateConsumed.add(firstDeviceGate.keyHashField);
|
|
391
|
+
gateConsumed.add(firstDeviceGate.verifiedViaField);
|
|
392
|
+
}
|
|
382
393
|
// A dynamic .readsFrom(fromPayload("<wsField>"), …) edge routes the foreign read by a payload field the
|
|
383
394
|
// ROUTING lane consumes (the plan may pass it straight to read(…, {from}) without a tracked property
|
|
384
395
|
// read on the sampled path) — a declared trust-edge field is never a dead field.
|