@githolon/dsl 0.49.0 → 0.51.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/check_package.mjs +46 -0
- package/dart/example/nomos_secure_key_store.dart +82 -0
- package/dart/lib/src/webview_bridge.dart +442 -6
- package/package.json +3 -2
- package/src/check_package_main.ts +194 -0
- package/src/codegen_dart_client.ts +124 -8
- package/src/codegen_proof.ts +1 -1
- package/src/codegen_ts.ts +470 -14
- package/src/compile_package_main.ts +19 -0
- package/src/evolve_preview.ts +466 -0
- package/src/law_check.ts +777 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `githolon check` — the author-time gate as a standalone command
|
|
8
|
+
* (architecture/author_time_gate.md, slice 1). Loads `nomos.package.mjs`, composes each
|
|
9
|
+
* domain the SAME way `compile` does (`composeDomainModule` — the one owner of directive
|
|
10
|
+
* discovery), runs {@link checkDomainModules}, prints the unified report, and exits
|
|
11
|
+
* non-zero on any REFUSE-level finding. `compile` runs the same twins implicitly.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
|
|
17
|
+
import { composeDomainModule, type Mod } from "./build_package.js";
|
|
18
|
+
import type { DomainModule } from "./codegen_dart.js";
|
|
19
|
+
import { checkDomainModules, formatFindings, hasRefusal, type Finding } from "./law_check.js";
|
|
20
|
+
import {
|
|
21
|
+
currentEvolveLaw,
|
|
22
|
+
evolveDiff,
|
|
23
|
+
evolveLawFromDeployJson,
|
|
24
|
+
evolveLawFromLockfile,
|
|
25
|
+
type EvolveLaw,
|
|
26
|
+
type StableIdLockfile,
|
|
27
|
+
} from "./evolve_preview.js";
|
|
28
|
+
|
|
29
|
+
interface DomainConfig {
|
|
30
|
+
key: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
modules: string[];
|
|
33
|
+
}
|
|
34
|
+
interface PackageConfig {
|
|
35
|
+
name: string;
|
|
36
|
+
domains: DomainConfig[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fail(msg: string): never {
|
|
40
|
+
console.error(`githolon check: ${msg}`);
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compose the package's domain modules from `nomos.package.mjs` — the check-time subset of what
|
|
46
|
+
* `compile_package_main` does: import the config, import each domain's TS modules, and hand them to
|
|
47
|
+
* `composeDomainModule` (the shared composition primitive that discovers directives/aggregates and
|
|
48
|
+
* applies the secondary-ensure auto-declare). Returns the composed modules ready for the twins.
|
|
49
|
+
*/
|
|
50
|
+
export async function loadComposedModules(configArg: string): Promise<DomainModule[]> {
|
|
51
|
+
const configPath = path.resolve(process.cwd(), configArg);
|
|
52
|
+
if (!existsSync(configPath)) fail(`config not found: ${configPath}`);
|
|
53
|
+
const cfgDir = path.dirname(configPath);
|
|
54
|
+
const cfgModule = (await import(pathToFileURL(configPath).href)) as { default?: PackageConfig };
|
|
55
|
+
const cfg = cfgModule.default;
|
|
56
|
+
if (!cfg || typeof cfg.name !== "string" || !Array.isArray(cfg.domains) || cfg.domains.length === 0) {
|
|
57
|
+
fail(`config must default-export { name, domains: [{ key, modules }, ...] }`);
|
|
58
|
+
}
|
|
59
|
+
const composed: DomainModule[] = [];
|
|
60
|
+
for (const d of cfg.domains) {
|
|
61
|
+
if (typeof d.key !== "string" || !Array.isArray(d.modules) || d.modules.length === 0) {
|
|
62
|
+
fail(`each domain needs { key, modules: [path, ...] }; got ${JSON.stringify(d)}`);
|
|
63
|
+
}
|
|
64
|
+
const mods: Mod[] = [];
|
|
65
|
+
for (const rel of d.modules) {
|
|
66
|
+
const abs = path.resolve(cfgDir, rel);
|
|
67
|
+
if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
|
|
68
|
+
mods.push((await import(pathToFileURL(abs).href)) as Mod);
|
|
69
|
+
}
|
|
70
|
+
composed.push(composeDomainModule({ name: d.name ?? d.key, domain: d.key, modules: mods }));
|
|
71
|
+
}
|
|
72
|
+
return composed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load the PRIOR law surface for `--against <path>` (the evolve preview's baseline). The supported
|
|
77
|
+
* prior is a DEPLOY.JSON (`build/<name>.deploy.json`), a BUILD DIR / package dir containing one, or
|
|
78
|
+
* the committed `nomos.stable-ids.json` LOCKFILE (aggregate identity only — no directive payloads).
|
|
79
|
+
* A live `https://…` prior is NOT supported (the installed package's stable-ids aren't retrievable
|
|
80
|
+
* from GET /domains); point `--against` at the deployed law's committed deploy.json instead.
|
|
81
|
+
*/
|
|
82
|
+
function loadPriorEvolveLaw(arg: string): { law: EvolveLaw; source: string; note?: string } {
|
|
83
|
+
if (/^https?:\/\//.test(arg)) {
|
|
84
|
+
fail(
|
|
85
|
+
`--against does not support a live URL (${arg}) — the installed package's #58 stable-ids are ` +
|
|
86
|
+
`not retrievable from GET /domains. Point --against at the deployed law's build/<name>.deploy.json.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const abs = path.resolve(process.cwd(), arg);
|
|
90
|
+
if (!existsSync(abs)) fail(`--against: prior not found: ${abs}`);
|
|
91
|
+
// Resolve a directory to a deploy.json (build dir or package dir) or its lockfile.
|
|
92
|
+
let file = abs;
|
|
93
|
+
if (statSync(abs).isDirectory()) {
|
|
94
|
+
const deploy = findDeployJson(abs);
|
|
95
|
+
const lock = path.join(abs, "nomos.stable-ids.json");
|
|
96
|
+
if (deploy !== undefined) file = deploy;
|
|
97
|
+
else if (existsSync(lock)) file = lock;
|
|
98
|
+
else fail(`--against: no *.deploy.json or nomos.stable-ids.json under ${abs}`);
|
|
99
|
+
}
|
|
100
|
+
let parsed: unknown;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
103
|
+
} catch (e) {
|
|
104
|
+
fail(`--against: could not parse ${file}: ${(e as Error).message}`);
|
|
105
|
+
}
|
|
106
|
+
const p = parsed as Record<string, unknown>;
|
|
107
|
+
if (p.domains !== undefined && p.interface === undefined && p.packageUsda === undefined) {
|
|
108
|
+
return {
|
|
109
|
+
law: evolveLawFromLockfile(parsed as StableIdLockfile),
|
|
110
|
+
source: file,
|
|
111
|
+
note: `prior is a lockfile — aggregate/field identity is diffed, but directive payload changes cannot be (deploy.json carries the interface).`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { law: evolveLawFromDeployJson(parsed as never), source: file };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Find a `*.deploy.json` under a directory — directly, or one level down in `build/`. */
|
|
118
|
+
function findDeployJson(dir: string): string | undefined {
|
|
119
|
+
const scan = (d: string): string | undefined => {
|
|
120
|
+
if (!existsSync(d)) return undefined;
|
|
121
|
+
const hit = readdirSync(d).find((f) => f.endsWith(".deploy.json"));
|
|
122
|
+
return hit !== undefined ? path.join(d, hit) : undefined;
|
|
123
|
+
};
|
|
124
|
+
return scan(dir) ?? scan(path.join(dir, "build"));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Read the committed `nomos.stable-ids.json` lockfile beside the config (the CURRENT identity). */
|
|
128
|
+
function loadCwdLockfile(configArg: string): StableIdLockfile | undefined {
|
|
129
|
+
const cfgDir = path.dirname(path.resolve(process.cwd(), configArg));
|
|
130
|
+
const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
|
|
131
|
+
if (!existsSync(lockPath)) return undefined;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(readFileSync(lockPath, "utf8")) as StableIdLockfile;
|
|
134
|
+
} catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function main(): Promise<void> {
|
|
140
|
+
const argv = process.argv.slice(2);
|
|
141
|
+
// The positional config must SKIP flag VALUES (`check --against prior.json` has no
|
|
142
|
+
// positional — `prior.json` belongs to the flag, not the config slot).
|
|
143
|
+
const againstIdx = argv.findIndex((a) => a === "--against");
|
|
144
|
+
const positionals = argv.filter((a, i) => !a.startsWith("--") && i !== againstIdx + 1);
|
|
145
|
+
const configArg = positionals[0] ?? "nomos.package.mjs";
|
|
146
|
+
const againstArg =
|
|
147
|
+
againstIdx >= 0
|
|
148
|
+
? argv[againstIdx + 1]
|
|
149
|
+
: argv.find((a) => a.startsWith("--against="))?.slice("--against=".length);
|
|
150
|
+
|
|
151
|
+
const modules = await loadComposedModules(configArg);
|
|
152
|
+
const findings: Finding[] = [...checkDomainModules(modules)];
|
|
153
|
+
|
|
154
|
+
// ── EVOLVE PREVIEW (--against): diff the current build against a prior law + pre-fire the gate. ──
|
|
155
|
+
let evolveDispositions: Record<string, unknown> | undefined;
|
|
156
|
+
if (againstIdx >= 0 || againstArg !== undefined) {
|
|
157
|
+
if (againstArg === undefined || againstArg.startsWith("--")) {
|
|
158
|
+
fail(`--against needs a path: --against <build/<name>.deploy.json | build dir | nomos.stable-ids.json>`);
|
|
159
|
+
}
|
|
160
|
+
const lock = loadCwdLockfile(configArg);
|
|
161
|
+
if (lock === undefined) {
|
|
162
|
+
console.log(
|
|
163
|
+
"githolon check: --against — no nomos.stable-ids.json in the config dir; the current law's stable ids " +
|
|
164
|
+
"fall back to NAMES (a rename would read as removal+add). Run `githolon compile` first for a sid-accurate preview.",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const prior = loadPriorEvolveLaw(againstArg);
|
|
168
|
+
if (prior.note !== undefined) console.log(`githolon check: --against note — ${prior.note}`);
|
|
169
|
+
const current = currentEvolveLaw(modules, lock);
|
|
170
|
+
const diff = evolveDiff(prior.law, current);
|
|
171
|
+
findings.push(...diff.findings);
|
|
172
|
+
if (Object.keys(diff.dispositions).length > 0) evolveDispositions = diff.dispositions;
|
|
173
|
+
console.log(`githolon check: evolve preview against ${prior.source}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`githolon check: ${findings.length} finding(s)`);
|
|
177
|
+
for (const line of formatFindings(findings)) {
|
|
178
|
+
const level = line.includes("REFUSE") ? " ✗ " : line.includes("WARN") ? " ⚠ " : " • ";
|
|
179
|
+
console.log(level + line);
|
|
180
|
+
}
|
|
181
|
+
if (findings.length === 0) console.log(" all clear — no static twin flagged anything");
|
|
182
|
+
|
|
183
|
+
// The consolidated, pasteable dispositions the destructive evolves demand (per domain).
|
|
184
|
+
if (evolveDispositions !== undefined) {
|
|
185
|
+
console.log(
|
|
186
|
+
"\ngitholon check: to admit the destructive evolve, add `dispositions` to the domain's deploy JSON:",
|
|
187
|
+
);
|
|
188
|
+
console.log(JSON.stringify(evolveDispositions, null, 2));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
process.exit(hasRefusal(findings) ? 1 : 0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
void main();
|
|
@@ -36,7 +36,7 @@ import type { DomainModule } from "./codegen_dart.js";
|
|
|
36
36
|
import { cap, readModelClassName } from "./codegen_dart.js";
|
|
37
37
|
import { finishCount } from "./count.js";
|
|
38
38
|
import { finishSum } from "./sum.js";
|
|
39
|
-
import { autoStampFields } from "./codegen_ts.js";
|
|
39
|
+
import { autoStampFields, domainEncryptionMap, type EncDomainMap, type InterfaceAggShape } from "./codegen_ts.js";
|
|
40
40
|
|
|
41
41
|
const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
|
|
42
42
|
const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
|
|
@@ -127,6 +127,33 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
127
127
|
const aggById = new Map<string, AggregateHandle>();
|
|
128
128
|
for (const a of mod.aggregates) aggById.set(a.id, a);
|
|
129
129
|
|
|
130
|
+
// THE E2E-SEALING MAP (the sealing client) — non-null iff a field is `.encrypted()`. When null NOTHING
|
|
131
|
+
// below emits (the generated Dart client is byte-identical to a pre-sealing compile). Same map + sids as
|
|
132
|
+
// the TS client (one source of truth: `domainEncryptionMap`); requires the baked interface for the sids.
|
|
133
|
+
const encMap: EncDomainMap | null =
|
|
134
|
+
opts.interface !== undefined
|
|
135
|
+
? domainEncryptionMap(mod, opts.interface as { aggregates?: Record<string, InterfaceAggShape> })
|
|
136
|
+
: null;
|
|
137
|
+
const encPresent = encMap !== null;
|
|
138
|
+
/** The distinct scopes a read of `typeId` must inject keys for (empty ⇒ nothing encrypted on it). */
|
|
139
|
+
const readScopesFor = (typeId: string): string[] => {
|
|
140
|
+
const r = encMap?.reads?.[typeId];
|
|
141
|
+
return r === undefined ? [] : [...new Set(Object.values(r).map((x) => x.scope))].sort();
|
|
142
|
+
};
|
|
143
|
+
const keysArg = (typeId: string): string => {
|
|
144
|
+
const scopes = readScopesFor(typeId);
|
|
145
|
+
return scopes.length ? `, await _readKeys([${scopes.map((s) => dartStr(s)).join(", ")}])` : "";
|
|
146
|
+
};
|
|
147
|
+
// The DISTINCT encryption scopes this domain defines (across encrypted reads + sealing directives) — the
|
|
148
|
+
// set the owner ceremony rotates. Empty ⇒ no `encryption` getter is emitted for this domain.
|
|
149
|
+
const domScopes: string[] = (() => {
|
|
150
|
+
if (encMap === null) return [];
|
|
151
|
+
const s = new Set<string>();
|
|
152
|
+
for (const r of Object.values(encMap.reads)) for (const f of Object.values(r)) s.add(f.scope);
|
|
153
|
+
for (const dir of Object.values(encMap.directives)) for (const f of Object.values(dir)) s.add(f.scope);
|
|
154
|
+
return [...s].sort();
|
|
155
|
+
})();
|
|
156
|
+
|
|
130
157
|
const methods: string[] = [];
|
|
131
158
|
|
|
132
159
|
// ── directives → typed offer ──
|
|
@@ -148,6 +175,34 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
148
175
|
const gateDoc = gate !== undefined
|
|
149
176
|
? ` /// GATED: needs an \`actor\` (the \`${gate.relation}\` relation) — throws MissingOfferAuthority otherwise.`
|
|
150
177
|
: "";
|
|
178
|
+
// E2E SEALING (the sealing client): a directive with `.encrypted()` payload fields SEALS them via the
|
|
179
|
+
// bound [keyStore] before dispatch (a missing key throws [EncryptionKeyUnavailable] — never a plaintext
|
|
180
|
+
// write). One async block covers creates + mutate + gated + stamps; `seal: false` is the escape hatch.
|
|
181
|
+
if (encMap?.directives?.[d.id] !== undefined) {
|
|
182
|
+
const isCreates = (d as { marker?: string }).marker === "creates";
|
|
183
|
+
const retType = isCreates ? "NomosOfferOutcome" : "String";
|
|
184
|
+
const offerFn = isCreates ? "offerCreates" : "offerDirective";
|
|
185
|
+
methods.push(
|
|
186
|
+
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
187
|
+
` /// Encrypted field(s) are SEALED via the bound [keyStore] before dispatch (pass \`seal: false\` to skip).`,
|
|
188
|
+
...(stamps.length > 0 ? [` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`] : []),
|
|
189
|
+
...(gateDoc ? [gateDoc] : []),
|
|
190
|
+
` Future<${retType}> ${method}(${payloadClass} payload, {bool seal = true}) async {`,
|
|
191
|
+
...(gateGuard ? [gateGuard] : []),
|
|
192
|
+
` final data = payload.toPayloadJson();`,
|
|
193
|
+
...(stamps.length > 0
|
|
194
|
+
? [
|
|
195
|
+
` final now = DateTime.now().toUtc().toIso8601String();`,
|
|
196
|
+
...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
|
|
197
|
+
]
|
|
198
|
+
: []),
|
|
199
|
+
` await _sealPayload(data, ${dartStr(d.id)}, seal);`,
|
|
200
|
+
` return bridge.${offerFn}(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, actor: actor, domainHash: domainHash);`,
|
|
201
|
+
` }`,
|
|
202
|
+
``,
|
|
203
|
+
);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
151
206
|
if ((d as { marker?: string }).marker === "creates") {
|
|
152
207
|
// LAW-MINTED ID RETURN: a `.creates` plan mints its aggregate id inside the sealed intent (the payload
|
|
153
208
|
// carries none — callers never supply ids). The bridge's offerCreates hands the minted ids back with the
|
|
@@ -213,9 +268,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
213
268
|
const argMap = `{${q.key.map((k) => `${dartStr(k)}: ${dartKeyParam(k)}`).join(", ")}}`;
|
|
214
269
|
const m = safeMethod(lcFirst(camel(q.id)));
|
|
215
270
|
methods.push(
|
|
216
|
-
` /// Declared query \`${q.id}\` (indexed probe — routed by the read manifest)
|
|
271
|
+
` /// Declared query \`${q.id}\` (indexed probe — routed by the read manifest).${readScopesFor(q.returns).length ? " Encrypted field(s) decrypt when the bound [keyStore] holds the scope key; else they stay opaque envelopes." : ""}`,
|
|
217
272
|
` Future<List<${model}>> ${m}({${params}}) async =>`,
|
|
218
|
-
` (await bridge.query(${dartStr(q.id)}, ${argMap})).map((r) => ${rowToModel(model, "r")}).toList();`,
|
|
273
|
+
` (await bridge.query(${dartStr(q.id)}, ${argMap}${keysArg(q.returns)})).map((r) => ${rowToModel(model, "r")}).toList();`,
|
|
219
274
|
``,
|
|
220
275
|
` /// Reactive \`${q.id}\` — re-emits on every local change (cancel the subscription to stop).`,
|
|
221
276
|
` Stream<List<${model}>> watch${cap(camel(q.id))}({${params}}) =>`,
|
|
@@ -267,9 +322,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
267
322
|
const lc = lcFirst(camel(agg.id));
|
|
268
323
|
const Cap = cap(camel(agg.id));
|
|
269
324
|
methods.push(
|
|
270
|
-
` /// Read one ${agg.id} by aggregate id
|
|
325
|
+
` /// Read one ${agg.id} by aggregate id.${readScopesFor(agg.id).length ? " Encrypted field(s) decrypt when the bound [keyStore] holds the scope key." : ""}`,
|
|
271
326
|
` Future<List<${model}>> ${safeMethod(`${lc}ById`)}(String id) async =>`,
|
|
272
|
-
` (await bridge.queryById(id)).map((r) => ${rowToModel(model, "r")}).toList();`,
|
|
327
|
+
` (await bridge.queryById(id${keysArg(agg.id)})).map((r) => ${rowToModel(model, "r")}).toList();`,
|
|
273
328
|
``,
|
|
274
329
|
` /// Watch one ${agg.id} by aggregate id (local reactive read).`,
|
|
275
330
|
` Stream<List<${model}>> watch${Cap}ById(String id) =>`,
|
|
@@ -277,7 +332,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
277
332
|
``,
|
|
278
333
|
` /// Every ${agg.id} (the type-index probe — O(result), not a scan).`,
|
|
279
334
|
` Future<List<${model}>> ${safeMethod(`list${Cap}s`)}() async =>`,
|
|
280
|
-
` (await bridge.query('aggregatesByType', {'__type': ${dartStr(agg.id)}})).map((r) => ${rowToModel(model, "r")}).toList();`,
|
|
335
|
+
` (await bridge.query('aggregatesByType', {'__type': ${dartStr(agg.id)}}${keysArg(agg.id)})).map((r) => ${rowToModel(model, "r")}).toList();`,
|
|
281
336
|
``,
|
|
282
337
|
);
|
|
283
338
|
}
|
|
@@ -305,7 +360,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
305
360
|
`// client file gives an app the whole typed surface.`,
|
|
306
361
|
`export '${mod.name}.dart';`,
|
|
307
362
|
...(opts.frameworkImportUri !== undefined ? [`export '${opts.frameworkImportUri}';`] : []),
|
|
308
|
-
`export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion;`,
|
|
363
|
+
`export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion${encPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
|
|
309
364
|
``,
|
|
310
365
|
`/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
|
|
311
366
|
`const String ${hashConst} = ${dartStr(opts.domainHash)};`,
|
|
@@ -344,8 +399,30 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
344
399
|
` /// The verified principal uid every offer is authored as (sealed as \`security.actor\` by the one encoder).`,
|
|
345
400
|
` /// Null ⇒ open (un-gated) authoring; a gated directive then throws [MissingOfferAuthority].`,
|
|
346
401
|
` final String? actor;`,
|
|
347
|
-
|
|
402
|
+
...(encPresent
|
|
403
|
+
? [
|
|
404
|
+
``,
|
|
405
|
+
` /// THE KEY CUSTODY surface (E2E sealing). When set, encrypted directive fields SEAL automatically`,
|
|
406
|
+
` /// before dispatch and encrypted read fields DECRYPT automatically; null ⇒ ciphertext passes through`,
|
|
407
|
+
` /// unchanged (today's behavior). See [WrappedKeyStore] for the default over the on-chain WrappedKeys.`,
|
|
408
|
+
` final NomosKeyStore? keyStore;`,
|
|
409
|
+
]
|
|
410
|
+
: []),
|
|
411
|
+
` ${className}(this.bridge, {this.domainHash = ${hashConst}, this.actor${encPresent ? ", this.keyStore" : ""}});`,
|
|
348
412
|
``,
|
|
413
|
+
...(encPresent ? dartEncClassMembers(encMap!) : []),
|
|
414
|
+
// THE OWNER CEREMONY (slice 2): grantAndWrap / revokeAndRotate / rotate — the operator-blind key-epoch
|
|
415
|
+
// handshake as one call. Bind `actor` (the owner) to author the fan-out. Only for a domain with a scope.
|
|
416
|
+
...(domScopes.length > 0
|
|
417
|
+
? [
|
|
418
|
+
` /// OWNER CEREMONY (slice 2): \`grantAndWrap\` / \`revokeAndRotate\` / \`rotate\` — the operator-blind`,
|
|
419
|
+
` /// key-epoch handshake as one call. Bind \`actor\` (the owner) to author the fan-out.`,
|
|
420
|
+
` NomosEncryption get encryption => NomosEncryption(`,
|
|
421
|
+
` bridge: bridge, domain: ${dartStr(domain)}, domainHash: domainHash, actor: actor,`,
|
|
422
|
+
` scopes: const [${domScopes.map((s) => dartStr(s)).join(", ")}]);`,
|
|
423
|
+
``,
|
|
424
|
+
]
|
|
425
|
+
: []),
|
|
349
426
|
...(opts.interface !== undefined
|
|
350
427
|
? [
|
|
351
428
|
` /// This client's structural interface requirement (#72) as JSON — hand it to the`,
|
|
@@ -370,3 +447,42 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
370
447
|
``,
|
|
371
448
|
].join("\n");
|
|
372
449
|
}
|
|
450
|
+
|
|
451
|
+
/** The in-class E2E-sealing members: the baked map + the seal/read private helpers. */
|
|
452
|
+
function dartEncClassMembers(encMap: EncDomainMap): string[] {
|
|
453
|
+
return [
|
|
454
|
+
` /// The baked E2E-sealing map (compile-time, from the SAME sids the law was compiled with): which`,
|
|
455
|
+
` /// directive payload fields seal → { scope, aggSid, fieldSid }, and which read-model fields decrypt.`,
|
|
456
|
+
` static final Map<String, dynamic> encryptionMap = (jsonDecode(${dartRawJson(encMap)}) as Map).cast<String, dynamic>();`,
|
|
457
|
+
``,
|
|
458
|
+
` /// Seal the mapped encrypted fields of \`data\` in place before dispatch. No keyStore / \`seal == false\``,
|
|
459
|
+
` /// ⇒ passthrough (today's behavior); a held key seals; a missing key throws [EncryptionKeyUnavailable].`,
|
|
460
|
+
` Future<void> _sealPayload(Map<String, dynamic> data, String directiveId, bool seal) async {`,
|
|
461
|
+
` if (!seal || keyStore == null) return;`,
|
|
462
|
+
` final fields = (encryptionMap['directives'] as Map)[directiveId] as Map?;`,
|
|
463
|
+
` if (fields == null) return;`,
|
|
464
|
+
` for (final entry in fields.entries) {`,
|
|
465
|
+
` final field = entry.key as String;`,
|
|
466
|
+
` final m = entry.value as Map;`,
|
|
467
|
+
` if (data[field] == null) continue;`,
|
|
468
|
+
` final scope = m['scope'] as String;`,
|
|
469
|
+
` final sk = await keyStore!.scopeKey(scope);`,
|
|
470
|
+
` if (sk == null) throw EncryptionKeyUnavailable(scope, field);`,
|
|
471
|
+
` data[field] = await bridge.cryptoSealField(`,
|
|
472
|
+
` scopeKey: sk.key, scope: scope, epoch: sk.epoch,`,
|
|
473
|
+
` aggSid: m['aggSid'] as String, fieldSid: m['fieldSid'] as String,`,
|
|
474
|
+
` plaintext: data[field].toString(),`,
|
|
475
|
+
` );`,
|
|
476
|
+
` }`,
|
|
477
|
+
` }`,
|
|
478
|
+
``,
|
|
479
|
+
` /// The read keys to inject for \`scopes\`, or null when there is nothing to inject (no keyStore / no`,
|
|
480
|
+
` /// held keys) — the read then behaves exactly as today (encrypted fields stay opaque envelopes).`,
|
|
481
|
+
` Future<List<NomosScopeKey>?> _readKeys(List<String> scopes) async {`,
|
|
482
|
+
` if (keyStore == null || scopes.isEmpty) return null;`,
|
|
483
|
+
` final keys = await keyStore!.readKeys(scopes);`,
|
|
484
|
+
` return keys.isEmpty ? null : keys;`,
|
|
485
|
+
` }`,
|
|
486
|
+
``,
|
|
487
|
+
];
|
|
488
|
+
}
|
package/src/codegen_proof.ts
CHANGED
|
@@ -1384,7 +1384,7 @@ export interface ChildBirthParentSetup {
|
|
|
1384
1384
|
/** A stub `nomos` global (mint + sha256) so a `.births()` plan that mints / hashes a child name runs to
|
|
1385
1385
|
* completion at codegen. The hash is a deterministic STUB (the codegen never needs the real digest — the
|
|
1386
1386
|
* recipe's workspace is read at RUNTIME from the offer's `born`); mint mirrors `harvestPrincipalFlows`. */
|
|
1387
|
-
function withBirthProbeGlobals<T>(run: () => T): T {
|
|
1387
|
+
export function withBirthProbeGlobals<T>(run: () => T): T {
|
|
1388
1388
|
const g = globalThis as unknown as { nomos?: { mint?: (t: string) => string; sha256?: (s: string) => string; read?: unknown } };
|
|
1389
1389
|
const had = "nomos" in g;
|
|
1390
1390
|
const prior = g.nomos;
|