@githolon/dsl 0.105.5 → 0.105.7

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.
@@ -2,11 +2,11 @@
2
2
  // Tenant code sees only generated `devices.request()` / `devices.approve(request)`.
3
3
 
4
4
  import { z } from "zod";
5
- import { OWNED_KEY_RELATION, OWNED_READ_RELATION, aggregate, instance, ownedKeyObject, type AggregateHandle } from "../aggregate.js";
5
+ import { OWNED_KEY_RELATION, OWNED_PRINCIPAL_OBJECT_TYPE, OWNED_READ_RELATION, aggregate, instance, ownedKeyObject, type AggregateHandle } from "../aggregate.js";
6
6
  import { Lww } from "../drivers.js";
7
7
  import { directive } from "../directive.js";
8
8
  import { endorsementOf } from "../principal_evidence.js";
9
- import { isRelationTypeDecl } from "../relation_schema.js";
9
+ import { isRelationTypeDecl, key, relationType, self, user } from "../relation_schema.js";
10
10
  import { t } from "../fields.js";
11
11
  import { set, withMarker } from "../ops.js";
12
12
  import { query } from "../query.js";
@@ -19,6 +19,7 @@ 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
23
  import { RelationTuple, __qualifyAggregateType, removeTuple, writeTuple } from "./relations.js";
23
24
  import { SIGNER_RELATION, enrollSigner, signerObject, identityRelations } from "./signing_identity.js";
24
25
  import { impureCapability, impureCapabilityAggregate } from "./impure_capability.js";
@@ -27,6 +28,10 @@ import { impureCapability, impureCapabilityAggregate } from "./impure_capability
27
28
  export const OWNED_DEVICE_APPROVAL_DIRECTIVE = "nomos_internal_approve_owned_device_7c1f5e";
28
29
  export const OWNED_INITIALIZE_DIRECTIVE = "nomos_internal_initialize_owned_identity_7c1f5e";
29
30
  export const OWNED_RECOVERY_RELATION = "nomos_internal_owned_recovery_principal_7c1f5e";
31
+ export const ownedCustodyRelations = relationType(OWNED_PRINCIPAL_OBJECT_TYPE)
32
+ .asGlobal()
33
+ .relation(OWNED_KEY_RELATION, { rewrite: self(), allow: [key()] })
34
+ .relation(OWNED_RECOVERY_RELATION, { rewrite: self(), allow: [user()] });
30
35
  export const OWNED_RECOVERY_PROTECT_DIRECTIVE = "nomos_internal_protect_owned_identity_7c1f5e";
31
36
  export const OWNED_RECOVERY_REQUEST_DIRECTIVE = "nomos_internal_request_owned_recovery_7c1f5e";
32
37
  export const OWNED_RECOVERY_COMPLETE_DIRECTIVE = "nomos_internal_complete_owned_recovery_7c1f5e";
@@ -109,7 +114,33 @@ const ownedRecoveryLifecycle = impureCapability({
109
114
  writeReceipt: (id, r) => [
110
115
  set(instance(OwnedRecoveryOrder, id), "ownedCustody", r.ownedCustody as string),
111
116
  ],
117
+ // This is intentionally unaddressed: opening the designated principal's ordinary generated
118
+ // client is the provider. Declaring the capability still matters because every durable Order must
119
+ // have one typed provider contract; the kernel may then retain the sealed obligation locally until
120
+ // that principal opens the workspace, rather than inventing an untyped host work description.
121
+ capability: "nomos.owned-recovery",
112
122
  });
123
+ const hiddenRecoveryLifecycle = {
124
+ aggregate: hidden(OwnedRecoveryOrder),
125
+ obligation: hidden(ownedRecoveryLifecycle.obligation),
126
+ complete: hidden(ownedRecoveryLifecycle.complete),
127
+ fail: hidden(ownedRecoveryLifecycle.fail),
128
+ block: hidden(ownedRecoveryLifecycle.block),
129
+ deadLetter: hidden(ownedRecoveryLifecycle.deadLetter),
130
+ };
131
+ const ownedRecoveryCapability = {
132
+ ...ownedRecoveryLifecycle,
133
+ // Capability expansion deliberately re-seats the lifecycle quartet after application-language
134
+ // closure. Carry the private declarations there too; pointing back at the raw impure-capability
135
+ // declarations silently reintroduced framework verbs into proof selection and generated names.
136
+ aggregate: hiddenRecoveryLifecycle.aggregate,
137
+ obligation: hiddenRecoveryLifecycle.obligation,
138
+ complete: hiddenRecoveryLifecycle.complete,
139
+ fail: hiddenRecoveryLifecycle.fail,
140
+ block: hiddenRecoveryLifecycle.block,
141
+ deadLetter: hiddenRecoveryLifecycle.deadLetter,
142
+ __isCapabilityDecl: true as const,
143
+ };
113
144
 
114
145
  export const ownedRecoveryOrdersByRecoveryPrincipal = query("nomosOwnedRecoveryOrdersByRecoveryPrincipal")
115
146
  .key("recoveryPrincipal")
@@ -157,7 +188,7 @@ const protectOwnedIdentity = Object.assign(
157
188
  .alsoEnsures(RelationTuple)
158
189
  .alsoEnsures(KeyScope)
159
190
  .alsoEnsures(WrappedKey),
160
- { businessBirthInternal: true as const },
191
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
161
192
  );
162
193
 
163
194
  const acceptOwnedRecovery = Object.assign(
@@ -181,18 +212,9 @@ const acceptOwnedRecovery = Object.assign(
181
212
  ownedCustody: p.ownedCustody,
182
213
  });
183
214
  }),
184
- { businessBirthInternal: true as const },
215
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
185
216
  );
186
217
 
187
- const hiddenRecoveryLifecycle = {
188
- aggregate: hidden(OwnedRecoveryOrder),
189
- obligation: hidden(ownedRecoveryLifecycle.obligation),
190
- complete: hidden(ownedRecoveryLifecycle.complete),
191
- fail: hidden(ownedRecoveryLifecycle.fail),
192
- block: hidden(ownedRecoveryLifecycle.block),
193
- deadLetter: hidden(ownedRecoveryLifecycle.deadLetter),
194
- };
195
-
196
218
  export type OwnedRecordAction = "share" | "unshare" | "transfer";
197
219
 
198
220
  /** One collision-resistant framework id per aggregate/action. Tenant code never sees these ids. */
@@ -256,6 +278,7 @@ const approval = directive(OWNED_DEVICE_APPROVAL_DIRECTIVE)
256
278
  /** Branded internal so ordinary directive codegen never exposes its cryptographic payload. */
257
279
  export const approveOwnedDevice = Object.assign(approval, {
258
280
  businessBirthInternal: true as const,
281
+ ownedAuthoringInternal: true as const,
259
282
  });
260
283
 
261
284
  /** Establish this principal's shared owned-data root before another principal shares to them. */
@@ -281,7 +304,7 @@ const initializeOwnedIdentity = Object.assign(
281
304
  ownedCustody: p.ownedCustody,
282
305
  });
283
306
  }),
284
- { businessBirthInternal: true as const },
307
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
285
308
  );
286
309
 
287
310
  function recordScope(aggregate: AggregateHandle, recordId: string): string {
@@ -295,7 +318,10 @@ function recordObject(aggregate: AggregateHandle, recordId: string): string {
295
318
  function ownedRecordActionDirectives(aggregate: AggregateHandle) {
296
319
  const share = Object.assign(
297
320
  directive(ownedRecordActionDirectiveId("share", aggregate.id))
298
- .ensures(WrappedKey)
321
+ // The owned business record is the semantic target. Relation tuples and wraps are framework
322
+ // effects of sharing it; making a wrap the primary target causes authenticated law to describe
323
+ // this as a key action and the kernel correctly refuses `share(record, person)`.
324
+ .mutates(aggregate)
299
325
  .payload(z.object({
300
326
  recordId: z.string().min(1),
301
327
  peer: principal,
@@ -313,13 +339,14 @@ function ownedRecordActionDirectives(aggregate: AggregateHandle) {
313
339
  }),
314
340
  ...p.wraps.flatMap((entry) => writeWrap(recordScope(aggregate, p.recordId), entry.epoch, entry)),
315
341
  ])
316
- .alsoEnsures(RelationTuple),
317
- { businessBirthInternal: true as const },
342
+ .alsoEnsures(RelationTuple)
343
+ .alsoEnsuresMany(WrappedKey),
344
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
318
345
  );
319
346
 
320
347
  const unshare = Object.assign(
321
348
  directive(ownedRecordActionDirectiveId("unshare", aggregate.id))
322
- .ensures(KeyScope)
349
+ .mutates(aggregate)
323
350
  .payload(z.object({
324
351
  recordId: z.string().min(1),
325
352
  peer: principal,
@@ -340,14 +367,15 @@ function ownedRecordActionDirectives(aggregate: AggregateHandle) {
340
367
  ...writeKeyEpoch(recordScope(aggregate, p.recordId), p.epoch, p.changedAt, p.wraps),
341
368
  ])
342
369
  .alsoEnsures(RelationTuple)
370
+ .alsoEnsures(KeyScope)
343
371
  .alsoEnsuresMany(WrappedKey),
344
- { businessBirthInternal: true as const },
372
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
345
373
  );
346
374
 
347
375
  const ownerField = aggregate.audience?.kind === "owned" ? aggregate.audience.ownerField : "";
348
376
  const transfer = Object.assign(
349
377
  directive(ownedRecordActionDirectiveId("transfer", aggregate.id))
350
- .ensures(KeyScope)
378
+ .mutates(aggregate)
351
379
  .payload(z.object({
352
380
  recordId: z.string().min(1),
353
381
  newOwner: principal,
@@ -383,9 +411,9 @@ function ownedRecordActionDirectives(aggregate: AggregateHandle) {
383
411
  return ops;
384
412
  })
385
413
  .alsoEnsuresMany(RelationTuple)
386
- .alsoMutates(aggregate)
414
+ .alsoEnsures(KeyScope)
387
415
  .alsoEnsuresMany(WrappedKey),
388
- { businessBirthInternal: true as const },
416
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
389
417
  );
390
418
 
391
419
  return { share, unshare, transfer };
@@ -393,8 +421,11 @@ function ownedRecordActionDirectives(aggregate: AggregateHandle) {
393
421
 
394
422
  const APPROVAL_EXPORT = "__nomos_owned_device_approval_7c1f5e";
395
423
  const IDENTITY_EXPORT = "__nomos_owned_identity_relations_7c1f5e";
424
+ const CUSTODY_RELATIONS_EXPORT = "__nomos_owned_custody_relations_7c1f5e";
396
425
  const WARRANT_EXPORT = "__nomos_owned_genesis_warrant_7c1f5e";
397
426
  const SEED_EXPORT = "__nomos_owned_seed_genesis_7c1f5e";
427
+ const BIRTH_ATTESTATION_EXPORT = "__nomos_owned_birth_attestation_7c1f5e";
428
+ const BIRTH_ATTESTATION_SEED_EXPORT = "__nomos_owned_seed_birth_attestation_7c1f5e";
398
429
  const ENROLL_EXPORT = "__nomos_owned_enroll_signer_7c1f5e";
399
430
  const INITIALIZE_EXPORT = "__nomos_owned_initialize_identity_7c1f5e";
400
431
  const KEY_SCOPE_EXPORT = "__nomos_owned_key_scope_7c1f5e";
@@ -411,11 +442,15 @@ const RECOVERY_FAIL_EXPORT = "__nomos_owned_recovery_fail_7c1f5e";
411
442
  const RECOVERY_BLOCK_EXPORT = "__nomos_owned_recovery_block_7c1f5e";
412
443
  const RECOVERY_DEAD_LETTER_EXPORT = "__nomos_owned_recovery_dead_letter_7c1f5e";
413
444
  const RECOVERY_QUERY_EXPORT = "__nomos_owned_recovery_query_7c1f5e";
445
+ const RECOVERY_CAPABILITY_EXPORT = "__nomos_owned_recovery_capability_7c1f5e";
414
446
 
415
447
  const ownedGenesisWarrant = hidden(GenesisWarrant);
416
448
  const ownedSeedGenesis = hidden(seedGenesis);
449
+ const ownedBirthAttestation = hidden(BirthAttestation);
450
+ const ownedSeedBirthAttestation = hidden(seedBirthAttestation);
417
451
  const ownedEnrollSigner = hidden(enrollSigner);
418
452
  const ownedIdentityRelations = hidden(identityRelations);
453
+ const ownedCustodyRelationLaw = hidden(ownedCustodyRelations);
419
454
 
420
455
  /**
421
456
  * The published CLI bundles its compiler while a tenant module imports declarations from the separately
@@ -476,6 +511,7 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
476
511
  [RECOVERY_BLOCK_EXPORT, hiddenRecoveryLifecycle.block],
477
512
  [RECOVERY_DEAD_LETTER_EXPORT, hiddenRecoveryLifecycle.deadLetter],
478
513
  [RECOVERY_QUERY_EXPORT, hidden(ownedRecoveryOrdersByRecoveryPrincipal)],
514
+ [RECOVERY_CAPABILITY_EXPORT, hidden(ownedRecoveryCapability)],
479
515
  ] as const;
480
516
 
481
517
  // These names are deliberately obscure, not magical. A tenant can still reach one accidentally or
@@ -489,6 +525,10 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
489
525
  && !sameFrameworkDeclaration(mod[IDENTITY_EXPORT], ownedIdentityRelations)) {
490
526
  reservedCollision(IDENTITY_EXPORT);
491
527
  }
528
+ if (Object.prototype.hasOwnProperty.call(mod, CUSTODY_RELATIONS_EXPORT)
529
+ && !sameFrameworkDeclaration(mod[CUSTODY_RELATIONS_EXPORT], ownedCustodyRelationLaw)) {
530
+ reservedCollision(CUSTODY_RELATIONS_EXPORT);
531
+ }
492
532
  for (const [key, generated] of recoveryInjections) {
493
533
  if (Object.prototype.hasOwnProperty.call(mod, key) && !sameFrameworkDeclaration(mod[key], generated)) {
494
534
  reservedCollision(key);
@@ -497,6 +537,8 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
497
537
  for (const [key, generated] of [
498
538
  [WARRANT_EXPORT, ownedGenesisWarrant],
499
539
  [SEED_EXPORT, ownedSeedGenesis],
540
+ [BIRTH_ATTESTATION_EXPORT, ownedBirthAttestation],
541
+ [BIRTH_ATTESTATION_SEED_EXPORT, ownedSeedBirthAttestation],
500
542
  [ENROLL_EXPORT, ownedEnrollSigner],
501
543
  ] as const) {
502
544
  if (Object.prototype.hasOwnProperty.call(mod, key) && !sameFrameworkDeclaration(mod[key], generated)) {
@@ -526,6 +568,8 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
526
568
  for (const [framework, generated] of [
527
569
  [GenesisWarrant, ownedGenesisWarrant],
528
570
  [seedGenesis, ownedSeedGenesis],
571
+ [BirthAttestation, ownedBirthAttestation],
572
+ [seedBirthAttestation, ownedSeedBirthAttestation],
529
573
  [enrollSigner, ownedEnrollSigner],
530
574
  ] as const) {
531
575
  if (
@@ -546,6 +590,14 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
546
590
  ) {
547
591
  reservedCollision(`global relation type '${identityRelations.type}'`);
548
592
  }
593
+ if (
594
+ !sameFrameworkDeclaration(value, ownedCustodyRelationLaw)
595
+ && isRelationTypeDecl(value)
596
+ && value.global === true
597
+ && value.type === ownedCustodyRelations.type
598
+ ) {
599
+ reservedCollision(`global relation type '${ownedCustodyRelations.type}'`);
600
+ }
549
601
  if (value !== null && typeof value === "object" && typeof (value as { id?: unknown }).id === "string") {
550
602
  const id = (value as { id: string }).id;
551
603
  const generated = actions.flatMap((entry) => [entry.share, entry.unshare, entry.transfer]);
@@ -560,6 +612,7 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
560
612
  [APPROVAL_EXPORT]: approveOwnedDevice,
561
613
  [INITIALIZE_EXPORT]: initializeOwnedIdentity,
562
614
  [IDENTITY_EXPORT]: ownedIdentityRelations,
615
+ [CUSTODY_RELATIONS_EXPORT]: ownedCustodyRelationLaw,
563
616
  [KEY_SCOPE_EXPORT]: hidden(KeyScope),
564
617
  [WRAPPED_KEY_EXPORT]: hidden(WrappedKey),
565
618
  [KEY_SCOPE_QUERY_EXPORT]: hidden(keyScopeByScope),
@@ -574,11 +627,14 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
574
627
  [RECOVERY_BLOCK_EXPORT]: hiddenRecoveryLifecycle.block,
575
628
  [RECOVERY_DEAD_LETTER_EXPORT]: hiddenRecoveryLifecycle.deadLetter,
576
629
  [RECOVERY_QUERY_EXPORT]: hidden(ownedRecoveryOrdersByRecoveryPrincipal),
630
+ [RECOVERY_CAPABILITY_EXPORT]: hidden(ownedRecoveryCapability),
577
631
  // A local law test starts from an empty root, while production business state is normally born into a
578
632
  // warranted child. Keep the same lawful bootstrap machinery available in both cases, but framework-only:
579
633
  // the harness can seat its test identity and warrant the root without adding a tenant-facing lane.
580
634
  [WARRANT_EXPORT]: ownedGenesisWarrant,
581
635
  [SEED_EXPORT]: ownedSeedGenesis,
636
+ [BIRTH_ATTESTATION_EXPORT]: ownedBirthAttestation,
637
+ [BIRTH_ATTESTATION_SEED_EXPORT]: ownedSeedBirthAttestation,
582
638
  [ENROLL_EXPORT]: ownedEnrollSigner,
583
639
  };
584
640
  for (const { aggregate, share, unshare, transfer } of actions) {
@@ -0,0 +1,123 @@
1
+ import type { StableIds } from "./stable_ids_types.js";
2
+ import type { WireSchema } from "./wire.js";
3
+ import { mintGlobalAggregateSid } from "./stable_ids.js";
4
+ import {
5
+ LAW_FRAMEWORK_PARENT,
6
+ emptyLawIdentityRegistry,
7
+ lawEntityKey,
8
+ validateLawIdentityRegistry,
9
+ type LawIdentityEntity,
10
+ type LawIdentityRegistry,
11
+ } from "./law_identity.js";
12
+
13
+ export interface CompilerIdentityDomain {
14
+ stableIds: StableIds;
15
+ schemas?: Record<string, WireSchema>;
16
+ }
17
+
18
+ /** The materialized lock shape consumed inside the compiler and authoring tools. */
19
+ export interface CompilerIdentityModel {
20
+ v: 1;
21
+ domains: Record<string, CompilerIdentityDomain>;
22
+ /** Compiler-owned entity identity, including retirements. Semantics remain exclusively in USDA. */
23
+ identity?: LawIdentityRegistry;
24
+ }
25
+
26
+ /**
27
+ * Derive the compiler entity registry from domain/aggregate/field SIDs when a first-generation
28
+ * identity layer has no explicit registry yet. Once present, that registry retains every retirement.
29
+ */
30
+ export function identityRegistryFromModel(
31
+ model: CompilerIdentityModel | undefined,
32
+ ): LawIdentityRegistry {
33
+ if (model?.identity !== undefined) {
34
+ validateLawIdentityRegistry(model.identity);
35
+ return model.identity;
36
+ }
37
+ if (model === undefined) return emptyLawIdentityRegistry();
38
+
39
+ const entities: Record<string, LawIdentityEntity> = {};
40
+ const aggregateOccurrences = new Map<string, number>();
41
+ for (const domain of Object.values(model.domains)) {
42
+ for (const aggregate of Object.values(domain.stableIds.aggregates)) {
43
+ aggregateOccurrences.set(aggregate.sid, (aggregateOccurrences.get(aggregate.sid) ?? 0) + 1);
44
+ }
45
+ }
46
+
47
+ for (const [domainName, domain] of Object.entries(model.domains).sort(([a], [b]) =>
48
+ compareText(a, b),
49
+ )) {
50
+ const domainKey = importIdentityEntity(entities, {
51
+ kind: "domain",
52
+ sid: domain.stableIds.domain,
53
+ label: domainName,
54
+ generation: 0,
55
+ status: "active",
56
+ });
57
+ for (const [aggregateName, aggregate] of Object.entries(domain.stableIds.aggregates).sort(
58
+ ([a], [b]) => compareText(a, b),
59
+ )) {
60
+ const parent =
61
+ (aggregateOccurrences.get(aggregate.sid) ?? 0) > 1 ||
62
+ aggregate.sid === mintGlobalAggregateSid(aggregateName)
63
+ ? LAW_FRAMEWORK_PARENT
64
+ : domainKey;
65
+ const aggregateKey = importIdentityEntity(entities, {
66
+ kind: "aggregate",
67
+ sid: aggregate.sid,
68
+ parent,
69
+ label: aggregateName,
70
+ ...(aggregate.was === undefined ? {} : { priorLabels: aggregate.was }),
71
+ generation: 0,
72
+ status: "active",
73
+ });
74
+ for (const [fieldName, field] of Object.entries(aggregate.fields).sort(([a], [b]) =>
75
+ compareText(a, b),
76
+ )) {
77
+ importIdentityEntity(entities, {
78
+ kind: "field",
79
+ sid: field.sid,
80
+ parent: aggregateKey,
81
+ label: fieldName,
82
+ ...(field.was === undefined ? {} : { priorLabels: field.was }),
83
+ generation: 0,
84
+ status: "active",
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ const registry: LawIdentityRegistry = {
91
+ v: 1,
92
+ entities: Object.fromEntries(Object.entries(entities).sort(([a], [b]) => compareText(a, b))),
93
+ };
94
+ validateLawIdentityRegistry(registry);
95
+ return registry;
96
+ }
97
+
98
+ function importIdentityEntity(
99
+ entities: Record<string, LawIdentityEntity>,
100
+ entity: LawIdentityEntity,
101
+ ): string {
102
+ const key = lawEntityKey(entity.kind, entity.sid);
103
+ const prior = entities[key];
104
+ if (prior === undefined) {
105
+ entities[key] = entity;
106
+ return key;
107
+ }
108
+ if (
109
+ prior.kind !== entity.kind ||
110
+ prior.parent !== entity.parent ||
111
+ prior.label !== entity.label ||
112
+ JSON.stringify(prior.priorLabels ?? []) !== JSON.stringify(entity.priorLabels ?? [])
113
+ ) {
114
+ throw new Error(
115
+ `NMS-ID-002 legacy SID ${key} aliases incompatible ${prior.kind} '${prior.label}' and ${entity.kind} '${entity.label}'`,
116
+ );
117
+ }
118
+ return key;
119
+ }
120
+
121
+ function compareText(a: string, b: string): number {
122
+ return a < b ? -1 : a > b ? 1 : 0;
123
+ }