@githolon/dsl 0.101.6 → 0.102.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 +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
package/src/usd_state.ts
CHANGED
|
@@ -474,7 +474,7 @@ export function stateScrubStageUsda(opts: {
|
|
|
474
474
|
if (opts.lawLayerRelPathsWeakToStrong.length === 0) {
|
|
475
475
|
fail(
|
|
476
476
|
`scrub stage for '${opts.domain}' has no law layers — emit them first ` +
|
|
477
|
-
`(nomos-compile
|
|
477
|
+
`(nomos-compile always emits it) so state has law to compose over`,
|
|
478
478
|
);
|
|
479
479
|
}
|
|
480
480
|
const strongestFirst = [opts.stateLayerRelPath, ...[...opts.lawLayerRelPathsWeakToStrong].reverse()];
|
package/src/value_object.ts
CHANGED
|
@@ -52,8 +52,10 @@
|
|
|
52
52
|
* and `.optional()` on any of them (arrays/records of optionals excluded). Tuples, dates,
|
|
53
53
|
* non-string-keyed records etc. are deliberately OUT — grow this surface deliberately.
|
|
54
54
|
*/
|
|
55
|
+
|
|
55
56
|
import { z } from "zod";
|
|
56
57
|
|
|
58
|
+
import { isRuntimeLaw } from "./authoring_context.js";
|
|
57
59
|
import type { Driver } from "./drivers.js";
|
|
58
60
|
import {
|
|
59
61
|
valueObjectField,
|
|
@@ -251,28 +253,36 @@ export function unit<Z extends z.ZodTypeAny>(name: string, inner: Z): Z {
|
|
|
251
253
|
}
|
|
252
254
|
|
|
253
255
|
/**
|
|
254
|
-
* THE
|
|
255
|
-
* wire-identical to `z.string()` but
|
|
256
|
-
*
|
|
257
|
-
*
|
|
256
|
+
* THE AGGREGATE-REFERENCE BRAND (dart_codegen_parity.md P2) — `idOf(Site)` in a directive payload is
|
|
257
|
+
* wire-identical to `z.string()` but both generated languages expose it as `NomosRef<Site>`.
|
|
258
|
+
* Passing a bare string or `NomosRef<AnotherAggregate>` where the Site reference belongs becomes a
|
|
259
|
+
* COMPILE error. Payload-side only — the
|
|
258
260
|
* kernel keeps seeing a string; validation and the aggregate field contract are untouched
|
|
259
261
|
* because this is a directive-payload annotation.
|
|
260
262
|
*/
|
|
261
263
|
export const NOMOS_IDOF_BRAND: unique symbol = Symbol.for("nomos.idOf");
|
|
262
264
|
|
|
263
|
-
/** A payload field holding an EXISTING aggregate
|
|
264
|
-
export function idOf(aggregate: { id: string } | string): z.ZodType<string> {
|
|
265
|
+
/** A payload field holding an EXISTING aggregate reference: `siteId: idOf(Site)` -> `NomosRef<Site>`. */
|
|
266
|
+
export function idOf(aggregate: { id: string; __isContract?: true } | string): z.ZodType<string> {
|
|
265
267
|
const aggregateType = typeof aggregate === "string" ? aggregate : aggregate.id;
|
|
266
268
|
if (typeof aggregateType !== "string" || aggregateType === "") {
|
|
267
269
|
throw new Error("idOf: pass the aggregate handle (idOf(Site)) or its type name.");
|
|
268
270
|
}
|
|
269
271
|
const schema = z.string().min(1);
|
|
270
|
-
Object.defineProperty(schema, NOMOS_IDOF_BRAND, {
|
|
272
|
+
Object.defineProperty(schema, NOMOS_IDOF_BRAND, {
|
|
273
|
+
value: {
|
|
274
|
+
aggregateType,
|
|
275
|
+
targetKind: typeof aggregate === "object" && aggregate.__isContract === true
|
|
276
|
+
? "contract"
|
|
277
|
+
: "aggregate",
|
|
278
|
+
},
|
|
279
|
+
enumerable: false,
|
|
280
|
+
});
|
|
271
281
|
return schema;
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
/**
|
|
275
|
-
* THE BRANDED-ID BRAND (dart_codegen_parity.md P2 —
|
|
285
|
+
* THE BRANDED-ID BRAND (dart_codegen_parity.md P2 — deliberately NOT an aggregate reference) —
|
|
276
286
|
* `brandedId("SiteId")` is a NOMINAL string: wire-identical to `z.string()` but the generated TS is a branded alias
|
|
277
287
|
* (`type SiteId = string & { readonly __brand }`) and the generated Dart a zero-cost
|
|
278
288
|
* extension type (`extension type const SiteId(String value) {}`), so passing a bare string
|
|
@@ -313,6 +323,27 @@ export function idOfMetaOf(zt: z.ZodTypeAny): string | undefined {
|
|
|
313
323
|
for (let hops = 0; cur !== undefined && hops < 16; hops++) {
|
|
314
324
|
const branded = (cur as unknown as Record<symbol, unknown>)[NOMOS_IDOF_BRAND];
|
|
315
325
|
if (typeof branded === "string") return branded;
|
|
326
|
+
if (typeof branded === "object" && branded !== null) {
|
|
327
|
+
const aggregateType = (branded as { aggregateType?: unknown }).aggregateType;
|
|
328
|
+
if (typeof aggregateType === "string") return aggregateType;
|
|
329
|
+
}
|
|
330
|
+
const kind = zodKind(cur);
|
|
331
|
+
if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
|
|
332
|
+
cur = zodDef(cur).innerType;
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Whether an `idOf(...)` target is a concrete aggregate or a shared aggregate contract. */
|
|
338
|
+
export function idOfTargetKindOf(zt: z.ZodTypeAny): "aggregate" | "contract" | undefined {
|
|
339
|
+
let cur: z.ZodTypeAny | undefined = zt;
|
|
340
|
+
for (let hops = 0; cur !== undefined && hops < 16; hops++) {
|
|
341
|
+
const branded = (cur as unknown as Record<symbol, unknown>)[NOMOS_IDOF_BRAND];
|
|
342
|
+
if (typeof branded === "object" && branded !== null) {
|
|
343
|
+
const targetKind = (branded as { targetKind?: unknown }).targetKind;
|
|
344
|
+
if (targetKind === "aggregate" || targetKind === "contract") return targetKind;
|
|
345
|
+
}
|
|
346
|
+
if (typeof branded === "string") return "aggregate";
|
|
316
347
|
const kind = zodKind(cur);
|
|
317
348
|
if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
|
|
318
349
|
cur = zodDef(cur).innerType;
|
|
@@ -366,10 +397,9 @@ function describeZod(
|
|
|
366
397
|
case "string": {
|
|
367
398
|
const brandedBrand = (schema as unknown as Record<symbol, unknown>)[NOMOS_BRANDED_ID_BRAND];
|
|
368
399
|
const idOfAgg = (schema as unknown as Record<symbol, unknown>)[NOMOS_IDOF_BRAND];
|
|
369
|
-
// idOf(Agg) REFERENCES the aggregate's canonical
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// brand-extension-type emission. A standalone `brandedId("X")` still declares its own extension type.
|
|
400
|
+
// idOf(Agg) REFERENCES the aggregate's canonical `NomosRef<Agg>`. Carry `idOf` in the stable
|
|
401
|
+
// descriptor so every emitter can recover the aggregate type while the wire remains a string.
|
|
402
|
+
// A standalone `brandedId("X")` remains an unrelated nominal string and declares its own wrapper.
|
|
373
403
|
if (typeof idOfAgg === "string") {
|
|
374
404
|
return { type: "string", brand: `${idOfAgg}Id`, idOf: idOfAgg };
|
|
375
405
|
}
|
|
@@ -545,13 +575,19 @@ function makeValueObject<T>(
|
|
|
545
575
|
geometry?: GeometryKind,
|
|
546
576
|
example?: T,
|
|
547
577
|
): ValueObject<T> {
|
|
578
|
+
if (name === "User" || name === "Identity") {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`valueObject '${name}': '${name}' is a first-class Nomos authority type, not application state. ` +
|
|
581
|
+
`Sorry mate, that concept is already taken by the framework; choose a business-specific name.`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
548
584
|
if (!VO_NAME_RE.test(name)) {
|
|
549
585
|
throw new Error(
|
|
550
586
|
`valueObject: name '${name}' is not a valid identifier ([A-Za-z][A-Za-z0-9_]*) — it becomes the ` +
|
|
551
587
|
`generated TS interface / Dart class name.`,
|
|
552
588
|
);
|
|
553
589
|
}
|
|
554
|
-
if (example !== undefined) {
|
|
590
|
+
if (example !== undefined && !isRuntimeLaw()) {
|
|
555
591
|
const v = zodSchema.safeParse(example);
|
|
556
592
|
if (!v.success) {
|
|
557
593
|
throw new Error(
|
package/src/wire_encode.ts
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
* one event; a multi-aggregate one yields several. Proto/numeric-tag wire is a
|
|
20
20
|
* LATER coordinated migration — we target the current serde_json shape now.
|
|
21
21
|
*/
|
|
22
|
+
import type { z } from "zod";
|
|
23
|
+
|
|
22
24
|
import { encodeDriver } from "./drivers.js";
|
|
23
|
-
import { __qualifyAggregateType } from "./framework/relations.js";
|
|
25
|
+
import { __qualifyAggregateType, __setDispatchDomain } from "./framework/relations.js";
|
|
24
26
|
import { __resetAuthoring, __drainAuthoring } from "./authoring.js";
|
|
25
27
|
import { __resetReads, __drainReads } from "./read.js";
|
|
26
28
|
import { __resetBirths, __drainBirths } from "./births.js";
|
|
@@ -51,6 +53,15 @@ import type {
|
|
|
51
53
|
WireSchema,
|
|
52
54
|
WireValue,
|
|
53
55
|
} from "./wire.js";
|
|
56
|
+
import {
|
|
57
|
+
zodArrayElement,
|
|
58
|
+
zodDef,
|
|
59
|
+
zodEnumValues,
|
|
60
|
+
zodKind,
|
|
61
|
+
zodObjectShape,
|
|
62
|
+
zodSampleNumber,
|
|
63
|
+
zodSampleString,
|
|
64
|
+
} from "./zod_introspection.js";
|
|
54
65
|
|
|
55
66
|
/** Map a directive's referential marker onto the kernel `RefMode` wire string. */
|
|
56
67
|
export function encodeRefMode(marker: ReferentialMarker): WireRefMode {
|
|
@@ -103,6 +114,14 @@ function describeShape(value: unknown): string {
|
|
|
103
114
|
* `op` is passed purely for a NAMED diagnostic (aggregate + field + expected VO type) when a value
|
|
104
115
|
* can't be lowered — never `field '?'`. */
|
|
105
116
|
function encodeKernelValue(field: Field | undefined, value: unknown, op?: FieldOp): WireValue {
|
|
117
|
+
if (value === undefined) {
|
|
118
|
+
const aggName = op?.aggregateType ?? op?.aggregateId ?? "?";
|
|
119
|
+
const fieldName = op?.field ?? field?.kind ?? "?";
|
|
120
|
+
throw new Error(
|
|
121
|
+
`${aggName}.${fieldName} attempted to write undefined. Omit the operation (for example with ` +
|
|
122
|
+
`setOptional(...)) or declare an explicit null/clear policy; undefined can never be ledger state.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
106
125
|
// An `evidence` field encodes as the FIRST-CLASS kernel `Value::Evidence` (NOT a JSON
|
|
107
126
|
// string): the authored {hash, mediaType, byteLength, storageClass} maps to the
|
|
108
127
|
// kernel's snake_case serde shape + the `Git`/`External` unit-variant. Checked BEFORE
|
|
@@ -380,12 +399,14 @@ export function executeDirectiveToIntent<P>(
|
|
|
380
399
|
}
|
|
381
400
|
}
|
|
382
401
|
|
|
383
|
-
// Group ops by aggregate
|
|
384
|
-
// `
|
|
385
|
-
//
|
|
386
|
-
//
|
|
402
|
+
// Group ops by the complete aggregate address `(type, id)`, preserving first-seen order.
|
|
403
|
+
// `id` alone is never an address: two aggregate types may intentionally share a business key
|
|
404
|
+
// (Draft(id) + DraftPredecessorSelection(id)). Collapsing those buckets turns a lawful atomic
|
|
405
|
+
// Create+Ensure fan-out into one contradictory event and is exactly the sort of stringly identity
|
|
406
|
+
// bug `NomosRef<T>` exists to make impossible.
|
|
387
407
|
const order: string[] = [];
|
|
388
408
|
const byAggregate = new Map<string, WireFieldOp[]>();
|
|
409
|
+
const idOf = new Map<string, string>();
|
|
389
410
|
// Per-bucket aggregate TYPE — only present for instance-bound ops. When set, we
|
|
390
411
|
// auto-stamp `__type`/`__id` provenance field-ops so `view()` (api.rs
|
|
391
412
|
// `project_view`) can recover the aggregate's TYPE off a per-instance id.
|
|
@@ -403,31 +424,33 @@ export function executeDirectiveToIntent<P>(
|
|
|
403
424
|
const explicitMarker = new Map<string, WireRefMode>();
|
|
404
425
|
const fieldsOf = agg.fields as Record<string, Field>;
|
|
405
426
|
for (const op of ops) {
|
|
406
|
-
|
|
427
|
+
const bucketKey = JSON.stringify([op.aggregateType ?? null, op.aggregateId]);
|
|
428
|
+
let bucket = byAggregate.get(bucketKey);
|
|
407
429
|
if (bucket === undefined) {
|
|
408
430
|
bucket = [];
|
|
409
|
-
byAggregate.set(
|
|
410
|
-
|
|
431
|
+
byAggregate.set(bucketKey, bucket);
|
|
432
|
+
idOf.set(bucketKey, op.aggregateId);
|
|
433
|
+
order.push(bucketKey);
|
|
411
434
|
}
|
|
412
|
-
if (op.aggregateType !== undefined) typeOf.set(
|
|
435
|
+
if (op.aggregateType !== undefined) typeOf.set(bucketKey, op.aggregateType);
|
|
413
436
|
if (op.marker !== undefined) {
|
|
414
437
|
const encoded = encodeRefMode(op.marker);
|
|
415
|
-
const existing = explicitMarker.get(
|
|
438
|
+
const existing = explicitMarker.get(bucketKey);
|
|
416
439
|
if (existing !== undefined && existing !== encoded) {
|
|
417
440
|
throw new Error(
|
|
418
441
|
`Directive '${directive.id}' emits conflicting markers for aggregate ` +
|
|
419
|
-
`'${op.aggregateId}': ${existing} vs ${encoded}.`,
|
|
442
|
+
`'${op.aggregateType ?? "?"}:${op.aggregateId}': ${existing} vs ${encoded}.`,
|
|
420
443
|
);
|
|
421
444
|
}
|
|
422
|
-
explicitMarker.set(
|
|
445
|
+
explicitMarker.set(bucketKey, encoded);
|
|
423
446
|
}
|
|
424
447
|
// Field kinds are known for ops targeting the directive's own aggregate — by
|
|
425
448
|
// TYPE for a bound op (`aggregateType`), else by id for an unbound op.
|
|
426
449
|
const isOwnTarget =
|
|
427
450
|
op.aggregateType !== undefined ? op.aggregateType === agg.id : op.aggregateId === agg.id;
|
|
428
451
|
// A bucket is the own target if ANY of its ops target the directive's own aggregate.
|
|
429
|
-
if (isOwnTarget) ownTarget.set(
|
|
430
|
-
else if (!ownTarget.has(
|
|
452
|
+
if (isOwnTarget) ownTarget.set(bucketKey, true);
|
|
453
|
+
else if (!ownTarget.has(bucketKey)) ownTarget.set(bucketKey, false);
|
|
431
454
|
// Resolve the field's schema from the OP'S OWN TARGET aggregate, not the dispatch primary: a fanned-out
|
|
432
455
|
// sibling op carries its target's Field (`op.targetField`, captured at op-construction where the handle
|
|
433
456
|
// knew its schema). For an own-target op `op.targetField` IS `fieldsOf[op.field]` (same Field object), so
|
|
@@ -437,13 +460,14 @@ export function executeDirectiveToIntent<P>(
|
|
|
437
460
|
bucket.push(encodeKernelFieldOp(op, field));
|
|
438
461
|
}
|
|
439
462
|
|
|
440
|
-
const events: WireEvent[] = order.map((
|
|
441
|
-
const
|
|
442
|
-
const
|
|
463
|
+
const events: WireEvent[] = order.map((bucketKey) => {
|
|
464
|
+
const aggregate = idOf.get(bucketKey)!;
|
|
465
|
+
const ops = byAggregate.get(bucketKey)!;
|
|
466
|
+
const ty = typeOf.get(bucketKey);
|
|
443
467
|
// The directive's marker applies to its OWN target; a FAN-OUT bucket is a Mutate of an
|
|
444
468
|
// existing sibling aggregate (so the id-mint gate never sees a spurious Create for it).
|
|
445
469
|
const eventMarker: WireRefMode =
|
|
446
|
-
explicitMarker.get(
|
|
470
|
+
explicitMarker.get(bucketKey) ?? (ownTarget.get(bucketKey) ? marker : "Mutate");
|
|
447
471
|
if (ty !== undefined) {
|
|
448
472
|
// Stamp provenance FIRST so the fold carries the TYPE + canonical id the
|
|
449
473
|
// structured `view()` projects (the same `__type`/`__id` reserved fields the
|
|
@@ -487,6 +511,53 @@ export interface PrincipalFlow {
|
|
|
487
511
|
readonly reason: string | null;
|
|
488
512
|
}
|
|
489
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Build a schema-valid-enough value for security discovery without importing a
|
|
516
|
+
* code generator into the wire layer. Business intents validate their payload
|
|
517
|
+
* again while lowering private effects, so the old property-only Proxy could
|
|
518
|
+
* never reach their plans: Zod enumerates object keys before reading them.
|
|
519
|
+
* Unsupported/exotic schemas still fail closed at runtime exactly as before.
|
|
520
|
+
*/
|
|
521
|
+
function principalFlowProbeValue(
|
|
522
|
+
schema: z.ZodTypeAny,
|
|
523
|
+
visiting: ReadonlySet<z.ZodTypeAny> = new Set(),
|
|
524
|
+
hint = "value",
|
|
525
|
+
): unknown {
|
|
526
|
+
if (visiting.has(schema)) return {};
|
|
527
|
+
const next = new Set(visiting).add(schema);
|
|
528
|
+
const def = zodDef(schema);
|
|
529
|
+
switch (zodKind(schema)) {
|
|
530
|
+
case "string": return zodSampleString(schema, hint);
|
|
531
|
+
case "number": return zodSampleNumber(schema);
|
|
532
|
+
case "boolean": return false;
|
|
533
|
+
case "enum": return zodEnumValues(schema)[0];
|
|
534
|
+
case "literal": return def.value ?? def.values?.[0];
|
|
535
|
+
case "array": return [principalFlowProbeValue(zodArrayElement(schema), next, "item")];
|
|
536
|
+
case "object": {
|
|
537
|
+
const value: Record<string, unknown> = {};
|
|
538
|
+
for (const [name, field] of Object.entries(zodObjectShape(schema))) {
|
|
539
|
+
const kind = zodKind(field);
|
|
540
|
+
if (kind === "optional" || kind === "default") continue;
|
|
541
|
+
value[name] = principalFlowProbeValue(field, next, name);
|
|
542
|
+
}
|
|
543
|
+
return value;
|
|
544
|
+
}
|
|
545
|
+
case "record": return {};
|
|
546
|
+
case "union": {
|
|
547
|
+
const first = def.options?.[0];
|
|
548
|
+
return first === undefined ? undefined : principalFlowProbeValue(first, next, hint);
|
|
549
|
+
}
|
|
550
|
+
case "nullable": return null;
|
|
551
|
+
case "optional": return undefined;
|
|
552
|
+
case "default": return def.innerType === undefined ? undefined : principalFlowProbeValue(def.innerType, next, hint);
|
|
553
|
+
case "lazy": return def.getter === undefined ? {} : principalFlowProbeValue(def.getter(), next, hint);
|
|
554
|
+
case "pipe": return def.in === undefined ? {} : principalFlowProbeValue(def.in, next, hint);
|
|
555
|
+
case "unknown":
|
|
556
|
+
case "any": return {};
|
|
557
|
+
default: return {};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
490
561
|
/** Extract the principal flows carried by a plan's field ops (those with an `endorsement`). The ONE
|
|
491
562
|
* op→flow mapping shared by the discovery harvest (compile) and the runtime footprint (Slice B). */
|
|
492
563
|
function flowsFromOps(ops: readonly FieldOp[]): PrincipalFlow[] {
|
|
@@ -509,23 +580,49 @@ function flowsFromOps(ops: readonly FieldOp[]): PrincipalFlow[] {
|
|
|
509
580
|
* the omission is SAFE because the runtime/replay witness (Slice B) refuses any observed flow absent
|
|
510
581
|
* from the manifest. The reasons are static law, so they are intent-independent. SORTED for determinism.
|
|
511
582
|
*/
|
|
512
|
-
export function harvestPrincipalFlows(directive: Directive<unknown
|
|
583
|
+
export function harvestPrincipalFlows(directive: Directive<unknown>, domain?: string): PrincipalFlow[] {
|
|
513
584
|
const ctx: Ports = deterministicPorts({ principal: "principal:probe" });
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
585
|
+
let candidate: unknown;
|
|
586
|
+
try {
|
|
587
|
+
candidate = principalFlowProbeValue(directive.payloadSchema as z.ZodTypeAny);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
590
|
+
throw new Error(`principal-flow discovery for '${directive.id}' could not construct a lawful probe: ${detail}`, { cause: error });
|
|
591
|
+
}
|
|
592
|
+
const parsed = (directive.payloadSchema as z.ZodTypeAny).safeParse(candidate);
|
|
593
|
+
// Keep the historical property proxy as a fallback for unusual schemas whose plan does not validate
|
|
594
|
+
// its payload. A BusinessIntent does validate and therefore needs the enumerable parsed object above.
|
|
595
|
+
const probe = parsed.success ? parsed.data : new Proxy({}, { get: () => "probe-value" });
|
|
517
596
|
// A plan that MINTS (`create(Agg)` — the common `.creates` shape) calls `globalThis.nomos.mint`; the
|
|
518
597
|
// harvest runs at compile, where the engine's real shim is absent. Install a deterministic stub so the
|
|
519
598
|
// probe plan runs to completion and its flows ARE harvested (an un-harvested authority flow would be
|
|
520
599
|
// refused at runtime by the Slice B witness — fail-closed, but a false jam). The minted VALUE is
|
|
521
600
|
// irrelevant — a flow's sink is the field path, not the id. Restore any pre-existing shim after.
|
|
522
|
-
const g = globalThis as unknown as {
|
|
601
|
+
const g = globalThis as unknown as {
|
|
602
|
+
nomos?: {
|
|
603
|
+
mint?: (t: string) => string;
|
|
604
|
+
sha256?: (text: string) => string;
|
|
605
|
+
read?: unknown;
|
|
606
|
+
};
|
|
607
|
+
};
|
|
523
608
|
const hadNomos = "nomos" in g;
|
|
524
|
-
const prior
|
|
525
|
-
if (
|
|
526
|
-
|
|
609
|
+
const prior = g.nomos;
|
|
610
|
+
if (
|
|
611
|
+
!hadNomos ||
|
|
612
|
+
typeof prior?.mint !== "function" ||
|
|
613
|
+
typeof prior?.sha256 !== "function"
|
|
614
|
+
) {
|
|
615
|
+
g.nomos = {
|
|
616
|
+
...(prior ?? {}),
|
|
617
|
+
mint: prior?.mint ?? ((typeTag: string) => `${typeTag}_0192f00d-0000-7000-8000-000000000000`),
|
|
618
|
+
// Discovery needs only to drive the pure plan far enough to observe its
|
|
619
|
+
// declared flows. The address is irrelevant here; the runtime provides
|
|
620
|
+
// the real deterministic hash and separately verifies the plan bytes.
|
|
621
|
+
sha256: prior?.sha256 ?? (() => "0".repeat(64)),
|
|
622
|
+
};
|
|
527
623
|
}
|
|
528
624
|
let ops: FieldOp[];
|
|
625
|
+
if (domain !== undefined) __setDispatchDomain(domain);
|
|
529
626
|
__resetAuthoring(ctx, String(ctx.clock().physical));
|
|
530
627
|
__resetReads();
|
|
531
628
|
try {
|
|
@@ -535,6 +632,7 @@ export function harvestPrincipalFlows(directive: Directive<unknown>): PrincipalF
|
|
|
535
632
|
try { __drainAuthoring(); __drainReads(); } catch { /* sink already drained */ }
|
|
536
633
|
return [];
|
|
537
634
|
} finally {
|
|
635
|
+
if (domain !== undefined) __setDispatchDomain(undefined);
|
|
538
636
|
if (hadNomos && prior !== undefined) g.nomos = prior; else delete g.nomos;
|
|
539
637
|
}
|
|
540
638
|
__drainReads();
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// Durable, framework-owned delivery of a business application's access record.
|
|
2
|
+
//
|
|
3
|
+
// The domain records what should be shared and the business receipts that close
|
|
4
|
+
// that order. Generated stateful clients perform and retry the cross-workspace
|
|
5
|
+
// delivery. Tenant application code never sees a bridge, signer, home index, or
|
|
6
|
+
// delivery protocol.
|
|
7
|
+
|
|
8
|
+
import { aggregate, instance, type AggregateHandle } from "./aggregate.js";
|
|
9
|
+
import { directive, nomosImplementationDetail, type Directive } from "./directive.js";
|
|
10
|
+
import type { BusinessIntent } from "./intent.js";
|
|
11
|
+
import { query, type QueryDecl } from "./query.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { set, withMarker } from "./ops.js";
|
|
14
|
+
import { t } from "./fields.js";
|
|
15
|
+
import { Lww } from "./drivers.js";
|
|
16
|
+
import { endorsementOf } from "./principal_evidence.js";
|
|
17
|
+
import { RelationTuple, writeTuple } from "./framework/relations.js";
|
|
18
|
+
import {
|
|
19
|
+
refAs,
|
|
20
|
+
workspaceInvariant,
|
|
21
|
+
type WorkspaceInvariantDecl,
|
|
22
|
+
} from "./framework/workspace_invariant.js";
|
|
23
|
+
import { recipientMetaOf } from "./recipient.js";
|
|
24
|
+
import { zodKind, zodObjectShape } from "./zod_introspection.js";
|
|
25
|
+
|
|
26
|
+
export type WorkspaceSharingPresence = "present" | "absent";
|
|
27
|
+
|
|
28
|
+
export interface WorkspaceSharingWhen {
|
|
29
|
+
readonly field: string;
|
|
30
|
+
readonly is: WorkspaceSharingPresence;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorkspaceSharingReceipt {
|
|
34
|
+
readonly when?: WorkspaceSharingWhen;
|
|
35
|
+
/** Receipt payload field -> durable order field, when the business names differ. */
|
|
36
|
+
readonly payload?: Readonly<Record<string, string>>;
|
|
37
|
+
readonly records: Directive<any>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkspaceSharingPolicy {
|
|
41
|
+
readonly orders: AggregateHandle;
|
|
42
|
+
readonly initiatedBy: readonly BusinessIntent<any>[];
|
|
43
|
+
readonly pending: QueryDecl;
|
|
44
|
+
readonly pendingValue: string;
|
|
45
|
+
readonly person: string;
|
|
46
|
+
readonly role: string;
|
|
47
|
+
readonly scope: string;
|
|
48
|
+
readonly delivered: readonly WorkspaceSharingReceipt[];
|
|
49
|
+
readonly refused: readonly WorkspaceSharingReceipt[];
|
|
50
|
+
/** Generated law behind `Invitation<User>`; never exported to tenant code. */
|
|
51
|
+
readonly invitationLaw: WorkspaceSharingInvitationLaw;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface WorkspaceSharingInvitationLaw {
|
|
55
|
+
readonly claim: AggregateHandle;
|
|
56
|
+
readonly redeem: Directive<any>;
|
|
57
|
+
readonly gate: WorkspaceInvariantDecl;
|
|
58
|
+
readonly pendingByRecipient: QueryDecl;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One workspace-wide consumption record makes a bearer invitation single-use across every policy. */
|
|
62
|
+
const NomosInvitationClaim = {
|
|
63
|
+
...aggregate("NomosInternalInvitationClaim7c1f5e", {
|
|
64
|
+
publicKey: t.string().merge(Lww),
|
|
65
|
+
redeemedBy: t.string().merge(Lww),
|
|
66
|
+
redeemedAt: t.string().merge(Lww),
|
|
67
|
+
}, { global: true }).public(),
|
|
68
|
+
businessBirthInternal: true as const,
|
|
69
|
+
} as AggregateHandle;
|
|
70
|
+
|
|
71
|
+
export interface WorkspaceSharingOptions {
|
|
72
|
+
readonly orders: AggregateHandle;
|
|
73
|
+
/** Business actions after which the generated application owns delivery. */
|
|
74
|
+
readonly initiatedBy: readonly BusinessIntent<any>[];
|
|
75
|
+
/** An indexed, single-key query whose value selects pending delivery orders. */
|
|
76
|
+
readonly pending: QueryDecl;
|
|
77
|
+
readonly pendingValue?: string;
|
|
78
|
+
readonly person: string;
|
|
79
|
+
readonly role: string;
|
|
80
|
+
readonly scope: string;
|
|
81
|
+
readonly delivered: readonly WorkspaceSharingReceipt[];
|
|
82
|
+
readonly refused: readonly WorkspaceSharingReceipt[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const requireOrderField = (orders: AggregateHandle, field: string, purpose: string): void => {
|
|
86
|
+
if (orders.fields[field] === undefined) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`workspaceSharing(${orders.id}): ${purpose} field '${field}' is not declared on the delivery order`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Declare one durable business sharing policy. The order aggregate is ordinary
|
|
95
|
+
* domain state; delivery and retry are generated framework behaviour.
|
|
96
|
+
*
|
|
97
|
+
* This is the canonical access-delivery lane. Model the pending order and its
|
|
98
|
+
* delivered/refused business receipts here; never call a bridge from application
|
|
99
|
+
* code or mirror pending delivery in UI state.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* language("estate", intents, {
|
|
103
|
+
* workspaceSharing: [workspaceSharing({
|
|
104
|
+
* orders: EstateAccessShareOrder,
|
|
105
|
+
* initiatedBy: [grantEstateAccess, acceptEstateInvitation],
|
|
106
|
+
* pending: estateAccessShareOrdersByStatus,
|
|
107
|
+
* person: "subject",
|
|
108
|
+
* role: "relation",
|
|
109
|
+
* scope: "object",
|
|
110
|
+
* delivered: [{ records: recordEstateAccessDelivered }],
|
|
111
|
+
* refused: [{ records: recordEstateAccessRefused }],
|
|
112
|
+
* })],
|
|
113
|
+
* })
|
|
114
|
+
*/
|
|
115
|
+
export function workspaceSharing(options: WorkspaceSharingOptions): WorkspaceSharingPolicy {
|
|
116
|
+
const { orders, pending } = options;
|
|
117
|
+
if (pending.returns !== orders.id) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`workspaceSharing(${orders.id}): pending query '${pending.id}' returns '${pending.returns}'`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (pending.key.length !== 1) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`workspaceSharing(${orders.id}): pending query '${pending.id}' must have exactly one indexed key`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (options.initiatedBy.length === 0) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`workspaceSharing(${orders.id}): initiatedBy must name at least one business action`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
for (const action of options.initiatedBy) {
|
|
133
|
+
const schema = action.payloadSchema as z.ZodTypeAny;
|
|
134
|
+
const hasTypedRecipient = zodKind(schema) === "object" &&
|
|
135
|
+
Object.values(zodObjectShape(schema)).some((field) => recipientMetaOf(field) !== undefined);
|
|
136
|
+
if (!hasTypedRecipient) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`workspaceSharing(${orders.id}): business action '${action.id}' must accept a recipient() field; ` +
|
|
139
|
+
`plain uid/email/principal strings are not invitation recipients`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
requireOrderField(orders, options.person, "person");
|
|
144
|
+
requireOrderField(orders, options.role, "role");
|
|
145
|
+
requireOrderField(orders, options.scope, "scope");
|
|
146
|
+
if (options.delivered.length === 0 || options.refused.length === 0) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`workspaceSharing(${orders.id}): both delivered and refused business receipts are required`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
for (const receipt of [...options.delivered, ...options.refused]) {
|
|
152
|
+
if (receipt.when !== undefined) {
|
|
153
|
+
requireOrderField(orders, receipt.when.field, "receipt condition");
|
|
154
|
+
}
|
|
155
|
+
for (const [target, source] of Object.entries(receipt.payload ?? {})) {
|
|
156
|
+
requireOrderField(orders, source, `receipt payload '${target}'`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const pendingValue = options.pendingValue ?? "pending";
|
|
160
|
+
const stem = options.orders.id.replace(/[^A-Za-z0-9]/g, "");
|
|
161
|
+
const redeemId = `nomosInternalRedeem${stem}Invitation7c1f5e`;
|
|
162
|
+
const pendingByRecipient = {
|
|
163
|
+
...query(`nomosInternal${stem}InvitationsByRecipient7c1f5e`)
|
|
164
|
+
.key(options.person)
|
|
165
|
+
.returns(options.orders),
|
|
166
|
+
businessBirthInternal: true as const,
|
|
167
|
+
};
|
|
168
|
+
const b64 = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64");
|
|
169
|
+
const redeem = nomosImplementationDetail(
|
|
170
|
+
directive(redeemId)
|
|
171
|
+
.mutates(options.orders)
|
|
172
|
+
.payload(z.object({
|
|
173
|
+
orderId: z.string().min(1),
|
|
174
|
+
object: z.string().min(1),
|
|
175
|
+
relation: z.string().min(1),
|
|
176
|
+
invitationPublicKey: b64,
|
|
177
|
+
invitationSignature: b64,
|
|
178
|
+
invitationId: z.string().regex(/^nomos-invitation-[0-9a-f]{64}$/),
|
|
179
|
+
redeemedAt: z.string().min(1),
|
|
180
|
+
}))
|
|
181
|
+
.plan((facts, ctx) => {
|
|
182
|
+
const principal = endorsementOf(ctx.principal().asAttribution())?.principal ?? "";
|
|
183
|
+
const claim = instance(NomosInvitationClaim, facts.invitationId);
|
|
184
|
+
return [
|
|
185
|
+
...writeTuple({
|
|
186
|
+
object: facts.object,
|
|
187
|
+
relation: facts.relation,
|
|
188
|
+
subject: ctx.principal().asAuthoritySubject(
|
|
189
|
+
"redeem a possession-proven, single-use business invitation",
|
|
190
|
+
),
|
|
191
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
192
|
+
grantedAt: facts.redeemedAt,
|
|
193
|
+
}),
|
|
194
|
+
set(instance(options.orders, facts.orderId), options.person, principal),
|
|
195
|
+
withMarker(set(claim, "publicKey", facts.invitationPublicKey), "ensures"),
|
|
196
|
+
set(claim, "redeemedBy", principal),
|
|
197
|
+
set(claim, "redeemedAt", facts.redeemedAt),
|
|
198
|
+
];
|
|
199
|
+
})
|
|
200
|
+
.alsoEnsures(RelationTuple)
|
|
201
|
+
.alsoEnsures(NomosInvitationClaim),
|
|
202
|
+
);
|
|
203
|
+
const gate = workspaceInvariant(`NomosInvitationGate:${options.orders.id}`)
|
|
204
|
+
.on(redeemId)
|
|
205
|
+
.reads(({ intent }) => [
|
|
206
|
+
refAs("order", options.orders.id, `pre:${String(intent.orderId ?? "")}`),
|
|
207
|
+
refAs("claim", NomosInvitationClaim.id, `pre:${String(intent.invitationId ?? "")}`),
|
|
208
|
+
])
|
|
209
|
+
.assert((snapshots, ctx) => {
|
|
210
|
+
const intent = ctx?.intent ?? {};
|
|
211
|
+
const order = snapshots.order ?? {};
|
|
212
|
+
const claim = snapshots.claim ?? {};
|
|
213
|
+
const publicKey = String(intent.invitationPublicKey ?? "");
|
|
214
|
+
const principal = String(ctx?.principal ?? "");
|
|
215
|
+
const invitationAddress = `nomos-invitation:v1:${publicKey}`;
|
|
216
|
+
if (ctx?.principalVerified !== true || !principal.startsWith("user:")) {
|
|
217
|
+
return { reject: "nomos.invitation.verified_user_required" };
|
|
218
|
+
}
|
|
219
|
+
if (claim.publicKey !== undefined) {
|
|
220
|
+
return { reject: "nomos.invitation.already_redeemed" };
|
|
221
|
+
}
|
|
222
|
+
const invitationId = `nomos-invitation-${(globalThis as unknown as { nomos: { sha256(value: string): string } }).nomos.sha256(publicKey)}`;
|
|
223
|
+
if (intent.invitationId !== invitationId) {
|
|
224
|
+
return { reject: "nomos.invitation.invalid_proof" };
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
order[options.person] !== invitationAddress ||
|
|
228
|
+
order[options.role] !== intent.relation ||
|
|
229
|
+
order[options.scope] !== intent.object ||
|
|
230
|
+
order[pending.key[0]!] !== pendingValue
|
|
231
|
+
) {
|
|
232
|
+
return { reject: "nomos.invitation.not_pending" };
|
|
233
|
+
}
|
|
234
|
+
const verifier = (globalThis as unknown as {
|
|
235
|
+
nomos?: {
|
|
236
|
+
attestationVerify?: (
|
|
237
|
+
publicKey: string,
|
|
238
|
+
signature: string,
|
|
239
|
+
asserterWorkspace: string,
|
|
240
|
+
object: string,
|
|
241
|
+
relation: string,
|
|
242
|
+
subject: string,
|
|
243
|
+
attestedAt: string,
|
|
244
|
+
) => boolean;
|
|
245
|
+
};
|
|
246
|
+
}).nomos?.attestationVerify;
|
|
247
|
+
const valid = verifier?.(
|
|
248
|
+
publicKey,
|
|
249
|
+
String(intent.invitationSignature ?? ""),
|
|
250
|
+
"nomos-invitation-v1",
|
|
251
|
+
String(intent.orderId ?? ""),
|
|
252
|
+
String(intent.relation ?? ""),
|
|
253
|
+
principal,
|
|
254
|
+
String(intent.redeemedAt ?? ""),
|
|
255
|
+
) === true;
|
|
256
|
+
return valid
|
|
257
|
+
? { accept: true }
|
|
258
|
+
: { reject: "nomos.invitation.invalid_proof" };
|
|
259
|
+
});
|
|
260
|
+
const invitationLaw = Object.freeze({ claim: NomosInvitationClaim, redeem, gate, pendingByRecipient });
|
|
261
|
+
|
|
262
|
+
return Object.freeze({
|
|
263
|
+
...options,
|
|
264
|
+
pendingValue,
|
|
265
|
+
delivered: Object.freeze([...options.delivered]),
|
|
266
|
+
refused: Object.freeze([...options.refused]),
|
|
267
|
+
invitationLaw,
|
|
268
|
+
});
|
|
269
|
+
}
|