@githolon/dsl 0.107.4 → 0.108.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dart/CHANGELOG.md +24 -0
  2. package/dart/lib/src/application_framework_calls.dart +299 -0
  3. package/dart/lib/src/generated_call.dart +96 -0
  4. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  5. package/dart/lib/src/node_runner_transport.dart +77 -1
  6. package/dart/lib/src/result_usda.dart +487 -0
  7. package/dart/lib/src/runner/node-entry.mjs +10634 -6122
  8. package/dart/lib/src/runner/offline-install.mjs +4187 -2716
  9. package/dart/lib/src/runtime_v2_gateway.dart +54 -0
  10. package/dart/lib/src/runtime_v2_transport.dart +117 -0
  11. package/dart/lib/src/session_bound.dart +7 -2
  12. package/dart/lib/src/test_harness.dart +373 -282
  13. package/dart/lib/src/webview_bridge.dart +2146 -725
  14. package/dart/pubspec.lock +17 -1
  15. package/dart/pubspec.yaml +3 -2
  16. package/dart/test/acquire_session_credentials_test.dart +1 -1
  17. package/dart/test/acquire_session_test.dart +68 -14
  18. package/dart/test/adv_realm_interference_test.dart +6 -4
  19. package/dart/test/cluster_births_test.dart +11 -8
  20. package/dart/test/cluster_local_autosync_test.dart +1 -1
  21. package/dart/test/cluster_scale_test.dart +3 -2
  22. package/dart/test/dlq_session_surface_test.dart +52 -35
  23. package/dart/test/first_app_gauntlet_test.dart +1 -1
  24. package/dart/test/local_sync_inert_test.dart +23 -26
  25. package/dart/test/offer_authentication_proof_test.dart +193 -0
  26. package/dart/test/result_usda_test.dart +49 -0
  27. package/dart/test/session_bound_test.dart +8 -11
  28. package/dart/test/stale_bundle_test.dart +18 -3
  29. package/dart/test/test_harness_test.dart +14 -27
  30. package/dart/test/webview_bridge_test.dart +504 -83
  31. package/dart_types/CHANGELOG.md +17 -0
  32. package/dart_types/lib/nomos_kernel_protocol.dart +11 -0
  33. package/dart_types/lib/src/custody.dart +64 -19
  34. package/dart_types/lib/src/local_work_rejected.dart +30 -45
  35. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
  36. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +17945 -0
  38. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +933 -0
  39. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pb.dart +245 -0
  40. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbenum.dart +160 -0
  41. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
  42. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
  43. package/dart_types/lib/src/surfaced.dart +30 -25
  44. package/dart_types/pubspec.lock +16 -0
  45. package/dart_types/pubspec.yaml +3 -1
  46. package/dart_types/test/surfaced_test.dart +2 -1
  47. package/package.json +1 -1
  48. package/runtime/generated-application-host.js +5760 -1879
  49. package/src/authoring.ts +3 -2
  50. package/src/birth_meter_ensure.ts +2 -0
  51. package/src/births.ts +21 -0
  52. package/src/build_package.ts +30 -0
  53. package/src/business_birth.ts +41 -35
  54. package/src/codegen_call_usda.ts +1111 -0
  55. package/src/codegen_dart.ts +207 -282
  56. package/src/codegen_dart_application.ts +195 -252
  57. package/src/codegen_dart_client.ts +1978 -593
  58. package/src/codegen_dart_framework_calls.ts +291 -0
  59. package/src/codegen_dart_scenario.ts +7 -0
  60. package/src/codegen_proof.ts +3 -4
  61. package/src/codegen_provider_dart.ts +21 -7
  62. package/src/codegen_query_usda.ts +214 -0
  63. package/src/codegen_result_usda.ts +685 -0
  64. package/src/codegen_ts.ts +3945 -1111
  65. package/src/compile_package_main.ts +392 -46
  66. package/src/directive.ts +19 -4
  67. package/src/engine_entry.ts +45 -4
  68. package/src/framework/application_home.ts +4 -0
  69. package/src/framework/auth_provider_law.ts +296 -20
  70. package/src/framework/birth_certificate_law.ts +24 -0
  71. package/src/framework/home.ts +83 -40
  72. package/src/framework/owned_custody.ts +25 -12
  73. package/src/framework/owned_custody_value.ts +51 -0
  74. package/src/framework/relations.ts +47 -21
  75. package/src/framework/signing_identity.ts +100 -17
  76. package/src/framework/workspaces.ts +39 -10
  77. package/src/framework_call_definitions.ts +53 -0
  78. package/src/framework_read_definitions.ts +45 -0
  79. package/src/identity_frontier.ts +58 -0
  80. package/src/index.ts +22 -1
  81. package/src/intent.ts +216 -16
  82. package/src/law_check.ts +19 -1
  83. package/src/law_graph.ts +57 -8
  84. package/src/law_graph_usda.ts +73 -5
  85. package/src/law_value_type.ts +106 -19
  86. package/src/read.ts +47 -0
  87. package/src/read_projection_namespace.ts +7 -0
  88. package/src/shape_compat.ts +44 -42
  89. package/src/stable_ids.ts +37 -2
  90. package/src/test_call_usda.ts +83 -0
  91. package/src/wire.ts +36 -2
  92. package/src/wire_encode.ts +45 -1
  93. package/src/workspace_sharing.ts +27 -10
@@ -31,11 +31,16 @@ import { sha256Digest, t } from "../fields.js";
31
31
  import { Lww } from "../drivers.js";
32
32
  import { directive } from "../directive.js";
33
33
  import { ensure } from "../authoring.js";
34
- import { set, type PlannedOp } from "../ops.js";
34
+ import { set, withMarker, type PlannedOp } from "../ops.js";
35
35
  import { query } from "../query.js";
36
36
  import { birth } from "../births.js";
37
- import { readFrameworkDeclared } from "../read.js";
38
- import { authProvider as authProviderQuery, seedAuthProvider } from "./auth_provider_law.js";
37
+ import {
38
+ authProvider as authProviderQuery,
39
+ authProviderCertificates as authProviderCertificatesQuery,
40
+ authProviderKeys as authProviderKeysQuery,
41
+ readAuthProviderVerifyingConfiguration,
42
+ seedAuthProvider,
43
+ } from "./auth_provider_law.js";
39
44
  import { readRef, readSet, workspaceInvariant, refAs } from "./workspace_invariant.js";
40
45
  import { identityRelations } from "./signing_identity.js";
41
46
  import { RelationTuple, writeTuple } from "./relations.js";
@@ -47,6 +52,7 @@ import {
47
52
  DomainInstallExecutor,
48
53
  domainInstallGenesisOffer,
49
54
  } from "./domain_lifecycle.js";
55
+ import { birthCertificatePayload } from "./birth_certificate_law.js";
50
56
 
51
57
  // `nomos.sha256` — the TRUSTLESS, pure hash computed IN the githolon (Rust sha2 bound in the sealed engine;
52
58
  // see deterministic-rquickjs). Available as a global to plans + invariant predicates (like `nomos.mint`); it
@@ -354,18 +360,9 @@ export function birthReceivingGroupForDomain(
354
360
  // THE PARENT'S DECLARED VERIFIER. This captured read supplies the inherited provider seed.
355
361
  // Birth-certificate verification and founding-signer enrollment belong to the kernel-owned
356
362
  // attestation step, so neither fact is duplicated in this tenant plan or payload.
357
- const apRows = p.inheritAuthProvider !== false
358
- ? readFrameworkDeclared<Array<Record<string, unknown>>>("authProvider", { scope: "platform" })
359
- : undefined;
360
- const apRaw = Array.isArray(apRows) && apRows.length > 0 ? apRows[0] : undefined;
361
- // Projected rows are `{type,id,data:{…fields}}`; folded shapes may be flat — accept both.
362
- const ap = (apRaw && typeof apRaw === "object" && typeof (apRaw as { data?: unknown }).data === "object"
363
- ? (apRaw as { data: Record<string, unknown> }).data
364
- : apRaw) as Record<string, unknown> | undefined;
365
- const apStr = (k: string): string | undefined => {
366
- const v = ap?.[k];
367
- return typeof v === "string" && v.length > 0 ? v : undefined;
368
- };
363
+ const inheritedProvider = p.inheritAuthProvider === false
364
+ ? undefined
365
+ : readAuthProviderVerifyingConfiguration();
369
366
  const genesisChain: Array<{ domain: string; directiveId: string; actor: string; payload: Record<string, unknown>; domainHash?: string }> = [
370
367
  // THE ORDER-DECLARED LAW INSTALL: two typed closure identities. The kernel resolves each
371
368
  // closure from the parent's custody when it folds the birth; law bytes never ride the intent.
@@ -424,23 +421,14 @@ export function birthReceivingGroupForDomain(
424
421
  // (the up-front captured read above) AS DATA into the child genesis. The child owns the copy as its
425
422
  // own law-state; nothing is inherited-by-reference. Opt out: `inheritAuthProvider: false`.
426
423
  if (p.inheritAuthProvider !== false) {
427
- const str = apStr;
428
- const issuer = str("issuer");
429
- if (issuer !== undefined) {
424
+ if (inheritedProvider !== undefined) {
430
425
  genesisChain.push({
431
426
  domain: groupLawDomain,
432
427
  directiveId: "seedAuthProvider",
433
428
  actor: founder,
434
429
  payload: {
435
- issuer,
436
- jwksUrl: str("jwksUrl") ?? issuer, // required by the framework schema; a keys-only provider mirrors its issuer
437
- ...(str("keys") !== undefined ? { keys: str("keys") } : {}),
438
- ...(str("audience") !== undefined ? { audience: str("audience") } : {}),
439
- ...(str("principalClaim") !== undefined ? { principalClaim: str("principalClaim") } : {}),
440
- ...(str("rootCert") !== undefined ? { rootCert: str("rootCert") } : {}),
441
- ...(str("deviceBindingClaim") !== undefined ? { deviceBindingClaim: str("deviceBindingClaim") } : {}),
442
- ...(str("requireDeviceBinding") !== undefined ? { requireDeviceBinding: str("requireDeviceBinding") } : {}),
443
- label: str("label") ?? "inherited-from-parent",
430
+ ...inheritedProvider,
431
+ label: "inherited-from-parent",
444
432
  setBy: p.owner,
445
433
  setAt: 1, // genesis position — deterministic, like enrollSigner's grantedAt
446
434
  },
@@ -454,6 +442,8 @@ export function birthReceivingGroupForDomain(
454
442
  return [];
455
443
  })
456
444
  .reads(authProviderQuery) // the parent-side AuthProvider capture the inheritance copies (the #58 lane)
445
+ .reads(authProviderKeysQuery)
446
+ .reads(authProviderCertificatesQuery)
457
447
  .births(); // BIRTH IS A DECLARED KERNEL PRIMITIVE — emits the `nomosBirthGate` era key; the kernel folds the recipe
458
448
  return {
459
449
  ...birthDirective,
@@ -581,6 +571,7 @@ export const sharedByObject = query("sharedByObject").key("object").returns(Shar
581
571
  * verify doors); this aggregate is the human-readable mirror.
582
572
  */
583
573
  export const BIRTH_ATTESTATION_ID = "birth-attestation:self";
574
+ export const BIRTH_ATTESTATION_LINK_PREFIX = "birth-attestation-link:";
584
575
  export const BirthAttestation = aggregate("BirthAttestation", {
585
576
  /** This home's self-id (`home-<sha256(founder)>`) — the cert's `child`. */
586
577
  child: t.string().merge(Lww),
@@ -594,12 +585,29 @@ export const BirthAttestation = aggregate("BirthAttestation", {
594
585
  parentKey: t.string().merge(Lww),
595
586
  /** The parent's SignerEpoch at the attestation (informational; the cert is judged at birth-time validity). */
596
587
  epoch: t.int().merge(Lww),
597
- /** THE TRAVELLING SIGNED CERT CHAIN (JSON: `executor::birth_cert::BirthCertChain`) — the home's own
598
- * P-signed cert FIRST, then each ancestor's birth-cert up to root. The kernel verify doors parse THIS,
599
- * verify the signature chain to the pinned `K_root` OFFLINE (no cross-ws read), and assert it binds this
600
- * home (`child`==self-id, `founder`==HomeOwner.principal, `verified`). It travels because it folds into
601
- * the home's own genesis (seeded by the parent's offer-effect), so a peer holding only the home has it. */
602
- certChain: t.string().merge(Lww),
588
+ /** Legacy replay slot for histories authored before `BirthAttestationLink`. New histories write the
589
+ * travelling chain only as ordered, typed link records below. */
590
+ certChain: t.string().merge(Lww).optional(),
591
+ }).public();
592
+
593
+ /** One exact link in the travelling certificate chain. The zero-padded index is custody order; every
594
+ * certificate member is a separate typed USDA property. `certChain` above remains only as a replay fallback
595
+ * for histories authored before this aggregate existed. */
596
+ export const BirthAttestationLink = aggregate("BirthAttestationLink", {
597
+ index: t.int().merge(Lww),
598
+ child: t.string().merge(Lww),
599
+ founder: t.string().merge(Lww),
600
+ verified: t.bool().merge(Lww),
601
+ parentKey: t.string().merge(Lww),
602
+ parentId: t.string().merge(Lww),
603
+ epoch: t.int().merge(Lww),
604
+ signature: t.string().merge(Lww),
605
+ verifiedVia: t.string().merge(Lww).optional(),
606
+ idpToken: t.string().merge(Lww).optional(),
607
+ capturedClock: t.int().merge(Lww).optional(),
608
+ authProviderIssuer: t.string().merge(Lww).optional(),
609
+ authProviderJwks: t.string().merge(Lww).optional(),
610
+ authProviderAudience: t.string().merge(Lww).optional(),
603
611
  }).public();
604
612
 
605
613
  /** Audit read: this home's birth attestation (who attested it + the verified verdict). */
@@ -611,7 +619,7 @@ export const birthAttestation = query("birthAttestation").key("child").returns(B
611
619
  * the [`BirthAttestation`] singleton so the home's own state carries its travelling cert. A CONTROL-PLANE
612
620
  * write seeded once at birth (the open session lane structurally refuses it by directive id). The values
613
621
  * are the parent's GATE VERDICT (`founder`/`verified` lowered from the captured port — CAS-as-law, never
614
- * the offer payload) + the signed chain bytes; the home's verify doors re-derive trust from the chain.
622
+ * the offer payload) + the typed signed links; the home's verify doors re-derive trust from the chain.
615
623
  */
616
624
  export const seedBirthAttestation = directive("seedBirthAttestation")
617
625
  .ensures(BirthAttestation)
@@ -625,21 +633,56 @@ export const seedBirthAttestation = directive("seedBirthAttestation")
625
633
  parentId: z.string(),
626
634
  parentKey: z.string().min(1), // base64 K_P
627
635
  epoch: z.number().int().nonnegative(),
628
- certChain: z.string().min(1), // JSON BirthCertChain
636
+ links: z.array(birthCertificatePayload).min(1),
637
+ /** Law-declared target roles for the verified immediate parent workspace. The kernel injects this
638
+ * value from the admitted birth recipe and supplies `parentId`; no host chooses either fact. */
639
+ parentWorkspaceRoles: z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9_-]*$/)).optional(),
640
+ }).superRefine((payload, ctx) => {
641
+ if ((payload.parentWorkspaceRoles?.length ?? 0) > 0 && payload.parentId.length === 0) {
642
+ ctx.addIssue({ code: "custom", message: "parent workspace roles require a verified non-root parentId" });
643
+ }
629
644
  }),
630
645
  )
631
646
  .plan((p) => {
632
647
  const h = instance(BirthAttestation, BIRTH_ATTESTATION_ID);
633
- return [
648
+ const header = [
634
649
  set(h, "child", p.child),
635
650
  set(h, "founder", p.founder),
636
651
  set(h, "verified", p.verified),
637
652
  set(h, "parentId", p.parentId),
638
653
  set(h, "parentKey", p.parentKey),
639
654
  set(h, "epoch", p.epoch),
640
- set(h, "certChain", p.certChain),
641
655
  ];
642
- });
656
+ const links = p.links.flatMap((certificate, index) => {
657
+ const link = instance(BirthAttestationLink, `${BIRTH_ATTESTATION_LINK_PREFIX}${String(index).padStart(8, "0")}`);
658
+ return [
659
+ withMarker(set(link, "index", index), "ensures"),
660
+ withMarker(set(link, "child", certificate.child), "ensures"),
661
+ withMarker(set(link, "founder", certificate.founder), "ensures"),
662
+ withMarker(set(link, "verified", certificate.verified), "ensures"),
663
+ withMarker(set(link, "parentKey", certificate.parentKey), "ensures"),
664
+ withMarker(set(link, "parentId", certificate.parentId), "ensures"),
665
+ withMarker(set(link, "epoch", certificate.epoch), "ensures"),
666
+ withMarker(set(link, "signature", certificate.signature), "ensures"),
667
+ ...(certificate.verifiedVia ? [withMarker(set(link, "verifiedVia", certificate.verifiedVia), "ensures")] : []),
668
+ ...(certificate.idpToken ? [withMarker(set(link, "idpToken", certificate.idpToken), "ensures")] : []),
669
+ ...(certificate.capturedClock === undefined ? [] : [withMarker(set(link, "capturedClock", certificate.capturedClock), "ensures")]),
670
+ ...(certificate.authProviderIssuer ? [withMarker(set(link, "authProviderIssuer", certificate.authProviderIssuer), "ensures")] : []),
671
+ ...(certificate.authProviderJwks ? [withMarker(set(link, "authProviderJwks", certificate.authProviderJwks), "ensures")] : []),
672
+ ...(certificate.authProviderAudience ? [withMarker(set(link, "authProviderAudience", certificate.authProviderAudience), "ensures")] : []),
673
+ ];
674
+ });
675
+ const parentAuthority = (p.parentWorkspaceRoles ?? []).flatMap((relation) => writeTuple({
676
+ object: "workspace:self",
677
+ relation,
678
+ subject: `workspace:${p.parentId}`,
679
+ grantedBy: `workspace:${p.parentId}`,
680
+ grantedAt: String(p.epoch),
681
+ }));
682
+ return [...header, ...links, ...parentAuthority];
683
+ })
684
+ .alsoEnsures(BirthAttestationLink)
685
+ .alsoEnsures(RelationTuple);
643
686
 
644
687
  /**
645
688
  * THE ERA DECLARATION (the `nomosParentAttestedBirth` USD key). Exporting this sentinel from the home law
@@ -652,8 +695,8 @@ export const seedBirthAttestation = directive("seedBirthAttestation")
652
695
  export const parentAttestedBirth: ParentAttestedBirthMarker = {
653
696
  ...requireParentAttestedBirth,
654
697
  // DECLARE the owner + cert locators so the KERNEL reads them (it names neither): the home owner lives at
655
- // the `HomeOwner` singleton's `principal`, the travelling cert at the `BirthAttestation` singleton's
656
- // `certChain`. Mirrors how `.attestationAuthorized()` declares its owner-source — the kernel hardcodes none.
698
+ // the `HomeOwner` singleton's `principal`. `certObject`/`certField` retain the legacy chain locator for
699
+ // old histories; current histories are discovered through the typed `BirthAttestationLink` aggregate.
657
700
  ownerObject: RECEIVING_GROUP_ID,
658
701
  ownerField: "principal",
659
702
  certObject: BIRTH_ATTESTATION_ID,
@@ -19,10 +19,11 @@ import {
19
19
  writeWrap,
20
20
  } from "./encryption_scope.js";
21
21
  import { GenesisWarrant, seedGenesis } from "./genesis.js";
22
- import { BirthAttestation, seedBirthAttestation } from "./home.js";
22
+ import { BirthAttestation, BirthAttestationLink, seedBirthAttestation } from "./home.js";
23
23
  import { RelationTuple, __qualifyAggregateType, removeTuple, writeTuple } from "./relations.js";
24
24
  import { SIGNER_RELATION, enrollSigner, signerObject, identityRelations } from "./signing_identity.js";
25
25
  import { impureCapability, impureCapabilityAggregate } from "./impure_capability.js";
26
+ import { OwnedCustodyValue, OwnedDeviceRequestValue } from "./owned_custody_value.js";
26
27
 
27
28
  /** Obscure stable law id: deliberately unavailable as a generated tenant directive method. */
28
29
  export const OWNED_DEVICE_APPROVAL_DIRECTIVE = "nomos_internal_approve_owned_device_7c1f5e";
@@ -68,7 +69,7 @@ export const OwnedRecoveryPolicy = aggregate("NomosOwnedRecoveryPolicy", {
68
69
  const recoveryOrderParams = {
69
70
  ownerPrincipal: t.string().merge(Lww),
70
71
  recoveryPrincipal: t.string().merge(Lww),
71
- deviceRequest: t.string().merge(Lww),
72
+ deviceRequest: OwnedDeviceRequestValue.merge(Lww),
72
73
  };
73
74
 
74
75
  /**
@@ -81,14 +82,14 @@ const recoveryOrderParams = {
81
82
  * the last thing that should degrade quietly.
82
83
  */
83
84
  const recoveryReceipt = {
84
- ownedCustody: z.string().min(1),
85
+ ownedCustody: OwnedCustodyValue.zod,
85
86
  };
86
87
 
87
88
  /** Order fields ⊕ the receipt field, so the aggregate can hold what the Receipt writes. */
88
89
  const recoveryOrderFields = {
89
90
  ...recoveryOrderParams,
90
91
  // Absent until the recovery principal's Receipt lands (the read-decode discipline).
91
- ownedCustody: t.string().merge(Lww).optional(),
92
+ ownedCustody: OwnedCustodyValue.merge(Lww).optional(),
92
93
  };
93
94
 
94
95
  /** Private framework Order -> Receipt journal. Generated business clients never expose this aggregate. */
@@ -105,7 +106,7 @@ const ownedRecoveryLifecycle = impureCapability({
105
106
  paramsSchema: {
106
107
  ownerPrincipal: principal,
107
108
  recoveryPrincipal: principal,
108
- deviceRequest: z.string().min(1),
109
+ deviceRequest: OwnedDeviceRequestValue.zod,
109
110
  },
110
111
  writeParams: (id, p) => [
111
112
  set(instance(OwnedRecoveryOrder, id), "ownerPrincipal", p.ownerPrincipal),
@@ -114,7 +115,7 @@ const ownedRecoveryLifecycle = impureCapability({
114
115
  ],
115
116
  receiptSchema: recoveryReceipt,
116
117
  writeReceipt: (id, r) => [
117
- set(instance(OwnedRecoveryOrder, id), "ownedCustody", r.ownedCustody as string),
118
+ set(instance(OwnedRecoveryOrder, id), "ownedCustody", OwnedCustodyValue.zod.parse(r.ownedCustody)),
118
119
  ],
119
120
  // This is intentionally unaddressed: opening the designated principal's ordinary generated
120
121
  // client is the provider. Declaring the capability still matters because every durable Order must
@@ -132,6 +133,13 @@ const hiddenRecoveryLifecycle = {
132
133
  deadLetter: hidden(ownedRecoveryLifecycle.deadLetter),
133
134
  reschedule: hidden(ownedRecoveryLifecycle.reschedule),
134
135
  };
136
+
137
+ /** Compiler-contract handles for the two generated recovery calls used by the
138
+ * host. They remain framework-private directives; exporting the authored
139
+ * objects here lets code generation derive the cross-language signatures
140
+ * instead of maintaining another payload mirror. */
141
+ export const requestOwnedRecovery = hiddenRecoveryLifecycle.obligation;
142
+ export const completeOwnedRecovery = hiddenRecoveryLifecycle.complete;
135
143
  const ownedRecoveryCapability = {
136
144
  ...ownedRecoveryLifecycle,
137
145
  // Capability expansion deliberately re-seats the lifecycle quartet after application-language
@@ -152,7 +160,7 @@ export const ownedRecoveryOrdersByRecoveryPrincipal = query("nomosOwnedRecoveryO
152
160
  .returns(OwnedRecoveryOrder);
153
161
 
154
162
  /** Opt in once. Retrying the same policy is idempotent; replacing the principal is refused by the kernel. */
155
- const protectOwnedIdentity = Object.assign(
163
+ export const protectOwnedIdentity = Object.assign(
156
164
  directive(OWNED_RECOVERY_PROTECT_DIRECTIVE)
157
165
  .ensures(OwnedRecoveryPolicy)
158
166
  .payload(z.object({
@@ -196,13 +204,13 @@ const protectOwnedIdentity = Object.assign(
196
204
  { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
197
205
  );
198
206
 
199
- const acceptOwnedRecovery = Object.assign(
207
+ export const acceptOwnedRecovery = Object.assign(
200
208
  directive(OWNED_RECOVERY_ACCEPT_DIRECTIVE)
201
209
  .ensures(RelationTuple)
202
210
  .payload(z.object({
203
211
  requestId: z.string().min(1),
204
212
  newKeySubject: keySubject,
205
- ownedCustody: z.string().min(1),
213
+ ownedCustody: OwnedCustodyValue.zod,
206
214
  recoveredAt: z.string().min(1),
207
215
  }))
208
216
  .plan((p, ctx) => {
@@ -249,7 +257,7 @@ const approval = directive(OWNED_DEVICE_APPROVAL_DIRECTIVE)
249
257
  .ensures(RelationTuple)
250
258
  .payload(z.object({
251
259
  newKeySubject: keySubject,
252
- ownedCustody: z.string().min(1),
260
+ ownedCustody: OwnedCustodyValue.zod,
253
261
  verifiedVia: z.string().min(1),
254
262
  validFromEpoch: z.number().int().nonnegative(),
255
263
  approvedAt: z.string().min(1),
@@ -287,12 +295,12 @@ export const approveOwnedDevice = Object.assign(approval, {
287
295
  });
288
296
 
289
297
  /** Establish this principal's shared owned-data root before another principal shares to them. */
290
- const initializeOwnedIdentity = Object.assign(
298
+ export const initializeOwnedIdentity = Object.assign(
291
299
  directive(OWNED_INITIALIZE_DIRECTIVE)
292
300
  .ensures(RelationTuple)
293
301
  .payload(z.object({
294
302
  newKeySubject: keySubject,
295
- ownedCustody: z.string().min(1),
303
+ ownedCustody: OwnedCustodyValue.zod,
296
304
  initializedAt: z.string().min(1),
297
305
  }))
298
306
  .plan((p, ctx) => {
@@ -430,6 +438,7 @@ const CUSTODY_RELATIONS_EXPORT = "__nomos_owned_custody_relations_7c1f5e";
430
438
  const WARRANT_EXPORT = "__nomos_owned_genesis_warrant_7c1f5e";
431
439
  const SEED_EXPORT = "__nomos_owned_seed_genesis_7c1f5e";
432
440
  const BIRTH_ATTESTATION_EXPORT = "__nomos_owned_birth_attestation_7c1f5e";
441
+ const BIRTH_ATTESTATION_LINK_EXPORT = "__nomos_owned_birth_attestation_link_7c1f5e";
433
442
  const BIRTH_ATTESTATION_SEED_EXPORT = "__nomos_owned_seed_birth_attestation_7c1f5e";
434
443
  const ENROLL_EXPORT = "__nomos_owned_enroll_signer_7c1f5e";
435
444
  const INITIALIZE_EXPORT = "__nomos_owned_initialize_identity_7c1f5e";
@@ -452,6 +461,7 @@ const RECOVERY_CAPABILITY_EXPORT = "__nomos_owned_recovery_capability_7c1f5e";
452
461
  const ownedGenesisWarrant = hidden(GenesisWarrant);
453
462
  const ownedSeedGenesis = hidden(seedGenesis);
454
463
  const ownedBirthAttestation = hidden(BirthAttestation);
464
+ const ownedBirthAttestationLink = hidden(BirthAttestationLink);
455
465
  const ownedSeedBirthAttestation = hidden(seedBirthAttestation);
456
466
  const ownedEnrollSigner = hidden(enrollSigner);
457
467
  const ownedIdentityRelations = hidden(identityRelations);
@@ -543,6 +553,7 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
543
553
  [WARRANT_EXPORT, ownedGenesisWarrant],
544
554
  [SEED_EXPORT, ownedSeedGenesis],
545
555
  [BIRTH_ATTESTATION_EXPORT, ownedBirthAttestation],
556
+ [BIRTH_ATTESTATION_LINK_EXPORT, ownedBirthAttestationLink],
546
557
  [BIRTH_ATTESTATION_SEED_EXPORT, ownedSeedBirthAttestation],
547
558
  [ENROLL_EXPORT, ownedEnrollSigner],
548
559
  ] as const) {
@@ -574,6 +585,7 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
574
585
  [GenesisWarrant, ownedGenesisWarrant],
575
586
  [seedGenesis, ownedSeedGenesis],
576
587
  [BirthAttestation, ownedBirthAttestation],
588
+ [BirthAttestationLink, ownedBirthAttestationLink],
577
589
  [seedBirthAttestation, ownedSeedBirthAttestation],
578
590
  [enrollSigner, ownedEnrollSigner],
579
591
  ] as const) {
@@ -639,6 +651,7 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
639
651
  [WARRANT_EXPORT]: ownedGenesisWarrant,
640
652
  [SEED_EXPORT]: ownedSeedGenesis,
641
653
  [BIRTH_ATTESTATION_EXPORT]: ownedBirthAttestation,
654
+ [BIRTH_ATTESTATION_LINK_EXPORT]: ownedBirthAttestationLink,
642
655
  [BIRTH_ATTESTATION_SEED_EXPORT]: ownedSeedBirthAttestation,
643
656
  [ENROLL_EXPORT]: ownedEnrollSigner,
644
657
  };
@@ -0,0 +1,51 @@
1
+ // NOMOS — framework-owned typed custody values shared by relation facts, recovery Orders,
2
+ // generated clients and the kernel. USDA is the canonical semantic representation; these
3
+ // descriptors are the authored source from which TS/Dart encoders and decoders are generated.
4
+
5
+ import { z } from "zod";
6
+ import { valueObject, type ValueObjectValue } from "../value_object.js";
7
+
8
+ const base64 = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
9
+ const sha256 = z.string().regex(/^[0-9a-f]{64}$/);
10
+
11
+ export const OwnedDeviceRequestValue = valueObject("OwnedDeviceRequest", {
12
+ v: z.number().int().min(1).max(255),
13
+ workspace: z.string().min(1),
14
+ principal: z.string().min(1),
15
+ scope: z.string().min(1),
16
+ signingPublic: base64,
17
+ wrappingPublic: base64,
18
+ signature: base64,
19
+ });
20
+
21
+ export type OwnedDeviceRequest = ValueObjectValue<typeof OwnedDeviceRequestValue>;
22
+
23
+ export const OwnedCustodyValue = valueObject("OwnedCustody", {
24
+ v: z.number().int().min(1).max(255),
25
+ request: OwnedDeviceRequestValue,
26
+ hpkeEpk: base64,
27
+ ct: base64,
28
+ keyCommitment: sha256,
29
+ principalWrappingPublic: base64,
30
+ });
31
+
32
+ export type OwnedCustody = ValueObjectValue<typeof OwnedCustodyValue>;
33
+
34
+ /** Deterministic compile/proof value. It is structurally valid but intentionally has no
35
+ * cryptographic authority; the live executor will reject it if it ever reaches admission. */
36
+ export const OWNED_CUSTODY_PROBE: OwnedCustody = {
37
+ v: 1,
38
+ request: {
39
+ v: 1,
40
+ workspace: "nomos-probe",
41
+ principal: "user:nomos-probe",
42
+ scope: "nomos-probe",
43
+ signingPublic: "AA==",
44
+ wrappingPublic: "AA==",
45
+ signature: "AA==",
46
+ },
47
+ hpkeEpk: "AA==",
48
+ ct: "AA==",
49
+ keyCommitment: "0".repeat(64),
50
+ principalWrappingPublic: "AA==",
51
+ };
@@ -41,7 +41,7 @@
41
41
  */
42
42
  import { z } from "zod";
43
43
  import { aggregate, instance } from "../aggregate.js";
44
- import { t, type JsonValue } from "../fields.js";
44
+ import { t } from "../fields.js";
45
45
  import { Lww, MapOf } from "../drivers.js";
46
46
  import { directive } from "../directive.js";
47
47
  import { set, setEntry, withMarker, type PlannedOp } from "../ops.js";
@@ -49,6 +49,8 @@ import { query } from "../query.js";
49
49
  import { endorsementOf, type AttributionPrincipal, type EndorsedAuthority } from "../principal_evidence.js";
50
50
  import { isRelationObjectToken, qualifyRelationType, type RelationObjectToken } from "../relation_schema.js";
51
51
  import { qualifyReadProjectionType } from "../read_projection_namespace.js";
52
+ import { valueObject } from "../value_object.js";
53
+ import { OwnedCustodyValue, type OwnedCustody } from "./owned_custody_value.js";
52
54
 
53
55
  // ─────────────────────────────── the dispatch context (relation-namespacing) ───────────────────────────────
54
56
  //
@@ -192,46 +194,70 @@ export function relationTupleInstanceId(object: string, relation: string): strin
192
194
  * - relation Lww (the edge label)
193
195
  * - subjects MapOf(Lww) — subject → typed `RelationshipSubject` runtime leaf
194
196
  */
197
+ /** The exact law-level value stored for one subject in a relation tuple.
198
+ *
199
+ * This used to be `t.json()` and forced every generated reader to recover an
200
+ * unstructured object after the kernel had already proved a typed
201
+ * `RelationshipSubject`. Keeping the descriptor here makes the DSL graph the
202
+ * one authority for TS, Dart, USDA and the kernel's map leaf. */
203
+ export const RelationshipSubjectValue = valueObject("RelationshipSubject", {
204
+ status: z.enum(["active", "revoked"]),
205
+ grantedBy: z.string(),
206
+ grantedAt: z.string(),
207
+ revokedBy: z.string().optional(),
208
+ revokedAt: z.string().optional(),
209
+ validFromEpoch: z.number().int().nonnegative().optional(),
210
+ verifiedVia: z.string().optional(),
211
+ ownedCustody: OwnedCustodyValue.optional(),
212
+ });
213
+
195
214
  export const RelationTuple = aggregate("RelationTuple", {
196
215
  object: t.string().merge(Lww),
197
216
  relation: t.string().merge(Lww),
198
- subjects: t.map(t.json()).merge(MapOf(Lww)),
217
+ subjects: t.map(RelationshipSubjectValue.field()).merge(MapOf(Lww)),
199
218
  }).public();
200
219
  export type RelationTupleHandle = typeof RelationTuple;
201
220
 
202
221
  /** The per-subject leaf the `subjects` map stores as a typed kernel value. */
203
222
  export interface RelationSubjectLeaf {
204
- status: RelationStatus;
205
- grantedBy: string;
206
- grantedAt: string;
207
- revokedBy?: string;
208
- revokedAt?: string;
223
+ readonly status: RelationStatus;
224
+ readonly grantedBy: string;
225
+ readonly grantedAt: string;
226
+ readonly revokedBy?: string;
227
+ readonly revokedAt?: string;
209
228
  /**
210
229
  * VA-core (signer-provenance) — the SignerEpoch this leaf was authored under. OMITTED when absent so
211
230
  * every pre-VA chain (and every non-signer relation) stays byte-identical: `compact()` strips an
212
231
  * `undefined`. A revoke (`unrelateSubject`) backdates by bumping the SignerEpoch; a key whose
213
232
  * `validFromEpoch` predates the current epoch is stale (the full backdate gate is phase 4).
214
233
  */
215
- validFromEpoch?: number;
234
+ readonly validFromEpoch?: number;
216
235
  /**
217
236
  * VA-core (signer-provenance) — the IdP/issuer that VOUCHED for the enrolling principal at enroll time
218
237
  * (e.g. the `AuthProvider.issuer`). The kernel's `verify_captured_principal` DERIVES `principal_verified`
219
238
  * from THIS leaf field (provenance, not the wire): an enrollment vouched by a real IdP reads verified on
220
239
  * every lane. OMITTED when absent (legacy / unverified enrollment) so existing chains stay byte-identical.
221
240
  */
222
- verifiedVia?: string;
241
+ readonly verifiedVia?: string;
223
242
  /**
224
243
  * Secure-by-default owned-data device handoff. Framework-reserved and omitted from every ordinary
225
- * relation leaf; the opaque JSON is independently verified by the kernel before it may land.
244
+ * relation leaf; the typed encrypted handoff is independently verified by the kernel before it may land.
226
245
  */
227
- ownedCustody?: string;
246
+ readonly ownedCustody?: OwnedCustody;
228
247
  }
229
248
 
230
249
  /** Strip undefined keys so the typed leaf has one stable optional-field shape. */
231
- function compact(obj: Record<string, unknown>): RelationSubjectLeaf & Record<string, JsonValue> {
232
- const out: Record<string, unknown> = {};
233
- for (const [k, v] of Object.entries(obj)) if (v !== undefined) out[k] = v;
234
- return out as RelationSubjectLeaf & Record<string, JsonValue>;
250
+ function compact(leaf: RelationSubjectLeaf): RelationSubjectLeaf {
251
+ return {
252
+ status: leaf.status,
253
+ grantedBy: leaf.grantedBy,
254
+ grantedAt: leaf.grantedAt,
255
+ ...(leaf.revokedBy === undefined ? {} : { revokedBy: leaf.revokedBy }),
256
+ ...(leaf.revokedAt === undefined ? {} : { revokedAt: leaf.revokedAt }),
257
+ ...(leaf.validFromEpoch === undefined ? {} : { validFromEpoch: leaf.validFromEpoch }),
258
+ ...(leaf.verifiedVia === undefined ? {} : { verifiedVia: leaf.verifiedVia }),
259
+ ...(leaf.ownedCustody === undefined ? {} : { ownedCustody: leaf.ownedCustody }),
260
+ };
235
261
  }
236
262
 
237
263
  interface WriteTupleInput {
@@ -261,7 +287,7 @@ interface WriteTupleInput {
261
287
  /** VA-core: the IdP/issuer that vouched for the enrolling principal (signer relations only; omitted otherwise). */
262
288
  verifiedVia?: string;
263
289
  /** Framework-internal encrypted owned-data handoff (owned device relation only). */
264
- ownedCustody?: string;
290
+ ownedCustody?: OwnedCustody;
265
291
  }
266
292
 
267
293
  /** Lower a tuple write to plan ops. The first op MATERIALIZES the (object, relation)
@@ -297,13 +323,13 @@ function lowerTuple(
297
323
  status: p.status,
298
324
  grantedBy: grantedByRaw,
299
325
  grantedAt: p.grantedAt,
300
- revokedBy: revokedByRaw,
301
- revokedAt: p.revokedAt,
326
+ ...(revokedByRaw === undefined ? {} : { revokedBy: revokedByRaw }),
327
+ ...(p.revokedAt === undefined ? {} : { revokedAt: p.revokedAt }),
302
328
  // VA-core signer-provenance leaf fields — `compact` strips an `undefined`, so a non-signer relation
303
329
  // (these never set) stays byte-identical to a pre-VA leaf (the era rule on the wire shape).
304
- validFromEpoch: p.validFromEpoch,
305
- verifiedVia: p.verifiedVia,
306
- ownedCustody: p.ownedCustody,
330
+ ...(p.validFromEpoch === undefined ? {} : { validFromEpoch: p.validFromEpoch }),
331
+ ...(p.verifiedVia === undefined ? {} : { verifiedVia: p.verifiedVia }),
332
+ ...(p.ownedCustody === undefined ? {} : { ownedCustody: p.ownedCustody }),
307
333
  });
308
334
  const subjectEntry = {
309
335
  ...setEntry(rt, "subjects", subjectRaw, leaf),