@bcts/xid 1.0.0-alpha.10

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.
@@ -0,0 +1,862 @@
1
+ /**
2
+ * XID Document
3
+ *
4
+ * Represents an XID document containing keys, delegates, services, and provenance.
5
+ *
6
+ * Ported from bc-xid-rust/src/xid_document.rs
7
+ */
8
+
9
+ import {
10
+ Envelope,
11
+ PrivateKeyBase,
12
+ type PublicKeyBase,
13
+ type EnvelopeEncodable,
14
+ type Signer,
15
+ type EnvelopeEncodableValue,
16
+ } from "@bcts/envelope";
17
+ import {
18
+ KEY,
19
+ DELEGATE,
20
+ SERVICE,
21
+ PROVENANCE,
22
+ DEREFERENCE_VIA,
23
+ type KnownValue,
24
+ } from "@bcts/known-values";
25
+
26
+ // Helper to convert KnownValue to EnvelopeEncodableValue
27
+ const kv = (v: KnownValue): EnvelopeEncodableValue => v as unknown as EnvelopeEncodableValue;
28
+ import { Reference, XID } from "@bcts/components";
29
+ import {
30
+ type ProvenanceMark,
31
+ ProvenanceMarkGenerator,
32
+ ProvenanceMarkResolution,
33
+ } from "@bcts/provenance-mark";
34
+ import { type Cbor } from "@bcts/dcbor";
35
+
36
+ import { Key, XIDPrivateKeyOptions, type XIDPrivateKeyOptionsValue } from "./key";
37
+ import { Delegate, registerXIDDocumentClass } from "./delegate";
38
+ import { Service } from "./service";
39
+ import { Provenance, XIDGeneratorOptions, type XIDGeneratorOptionsValue } from "./provenance";
40
+ import { XIDError } from "./error";
41
+
42
+ // Raw values for predicate matching
43
+ const KEY_RAW = KEY.value();
44
+ const DELEGATE_RAW = DELEGATE.value();
45
+ const SERVICE_RAW = SERVICE.value();
46
+ const PROVENANCE_RAW = PROVENANCE.value();
47
+ const DEREFERENCE_VIA_RAW = DEREFERENCE_VIA.value();
48
+
49
+ /**
50
+ * Options for creating the inception key.
51
+ */
52
+ export type XIDInceptionKeyOptions =
53
+ | { type: "default" }
54
+ | { type: "publicKeyBase"; publicKeyBase: PublicKeyBase }
55
+ | { type: "privateKeyBase"; privateKeyBase: PrivateKeyBase };
56
+
57
+ /**
58
+ * Options for creating the genesis mark.
59
+ */
60
+ export type XIDGenesisMarkOptions =
61
+ | { type: "none" }
62
+ | {
63
+ type: "passphrase";
64
+ passphrase: string;
65
+ resolution?: ProvenanceMarkResolution;
66
+ date?: Date;
67
+ info?: Cbor;
68
+ }
69
+ | {
70
+ type: "seed";
71
+ seed: Uint8Array;
72
+ resolution?: ProvenanceMarkResolution;
73
+ date?: Date;
74
+ info?: Cbor;
75
+ };
76
+
77
+ /**
78
+ * Options for signing an envelope.
79
+ */
80
+ export type XIDSigningOptions =
81
+ | { type: "none" }
82
+ | { type: "inception" }
83
+ | { type: "privateKeyBase"; privateKeyBase: PrivateKeyBase };
84
+
85
+ /**
86
+ * Options for verifying the signature on an envelope when loading.
87
+ */
88
+ export enum XIDVerifySignature {
89
+ /** Do not verify the signature (default). */
90
+ None = "None",
91
+ /** Verify that the envelope is signed with the inception key. */
92
+ Inception = "Inception",
93
+ }
94
+
95
+ // Map to store keys by their hash for efficient lookup
96
+ type KeyMap = Map<string, Key>;
97
+ type DelegateMap = Map<string, Delegate>;
98
+ type ServiceMap = Map<string, Service>;
99
+
100
+ /**
101
+ * Represents an XID document.
102
+ */
103
+ export class XIDDocument implements EnvelopeEncodable {
104
+ private readonly _xid: XID;
105
+ private readonly _resolutionMethods: Set<string>;
106
+ private readonly _keys: KeyMap;
107
+ private readonly _delegates: DelegateMap;
108
+ private readonly _services: ServiceMap;
109
+ private _provenance: Provenance | undefined;
110
+
111
+ private constructor(
112
+ xid: XID,
113
+ resolutionMethods = new Set<string>(),
114
+ keys: KeyMap = new Map(),
115
+ delegates: DelegateMap = new Map(),
116
+ services: ServiceMap = new Map(),
117
+ provenance?: Provenance,
118
+ ) {
119
+ this._xid = xid;
120
+ this._resolutionMethods = resolutionMethods;
121
+ this._keys = keys;
122
+ this._delegates = delegates;
123
+ this._services = services;
124
+ this._provenance = provenance;
125
+ }
126
+
127
+ /**
128
+ * Create a new XIDDocument with the given options.
129
+ */
130
+ static new(
131
+ keyOptions: XIDInceptionKeyOptions = { type: "default" },
132
+ markOptions: XIDGenesisMarkOptions = { type: "none" },
133
+ ): XIDDocument {
134
+ const inceptionKey = XIDDocument.inceptionKeyForOptions(keyOptions);
135
+ const provenance = XIDDocument.genesisMarkWithOptions(markOptions);
136
+
137
+ const xid = XID.from(hashPublicKey(inceptionKey.publicKeyBase()));
138
+ const doc = new XIDDocument(xid, new Set(), new Map(), new Map(), new Map(), provenance);
139
+
140
+ doc.addKey(inceptionKey);
141
+ return doc;
142
+ }
143
+
144
+ private static inceptionKeyForOptions(options: XIDInceptionKeyOptions): Key {
145
+ switch (options.type) {
146
+ case "default": {
147
+ const privateKeyBase = PrivateKeyBase.generate();
148
+ return Key.newWithPrivateKeyBase(privateKeyBase);
149
+ }
150
+ case "publicKeyBase":
151
+ return Key.newAllowAll(options.publicKeyBase);
152
+ case "privateKeyBase":
153
+ return Key.newWithPrivateKeyBase(options.privateKeyBase);
154
+ }
155
+ }
156
+
157
+ private static genesisMarkWithOptions(options: XIDGenesisMarkOptions): Provenance | undefined {
158
+ switch (options.type) {
159
+ case "none":
160
+ return undefined;
161
+ case "passphrase": {
162
+ const resolution = options.resolution ?? ProvenanceMarkResolution.High;
163
+ const generator = ProvenanceMarkGenerator.newWithPassphrase(resolution, options.passphrase);
164
+ const date = options.date ?? new Date();
165
+ const mark = generator.next(date, options.info);
166
+ return Provenance.newWithGenerator(generator, mark);
167
+ }
168
+ case "seed": {
169
+ const resolution = options.resolution ?? ProvenanceMarkResolution.High;
170
+ const generator = ProvenanceMarkGenerator.newUsing(resolution, options.seed);
171
+ const date = options.date ?? new Date();
172
+ const mark = generator.next(date, options.info);
173
+ return Provenance.newWithGenerator(generator, mark);
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Create an XIDDocument from just an XID.
180
+ */
181
+ static fromXid(xid: XID): XIDDocument {
182
+ return new XIDDocument(xid);
183
+ }
184
+
185
+ /**
186
+ * Get the XID.
187
+ */
188
+ xid(): XID {
189
+ return this._xid;
190
+ }
191
+
192
+ /**
193
+ * Get the resolution methods.
194
+ */
195
+ resolutionMethods(): Set<string> {
196
+ return this._resolutionMethods;
197
+ }
198
+
199
+ /**
200
+ * Add a resolution method.
201
+ */
202
+ addResolutionMethod(method: string): void {
203
+ this._resolutionMethods.add(method);
204
+ }
205
+
206
+ /**
207
+ * Remove a resolution method.
208
+ */
209
+ removeResolutionMethod(method: string): boolean {
210
+ return this._resolutionMethods.delete(method);
211
+ }
212
+
213
+ /**
214
+ * Get all keys.
215
+ */
216
+ keys(): Key[] {
217
+ return Array.from(this._keys.values());
218
+ }
219
+
220
+ /**
221
+ * Add a key.
222
+ */
223
+ addKey(key: Key): void {
224
+ const hashKey = key.hashKey();
225
+ if (this._keys.has(hashKey)) {
226
+ throw XIDError.duplicate("key");
227
+ }
228
+ this._keys.set(hashKey, key);
229
+ }
230
+
231
+ /**
232
+ * Find a key by its public key base.
233
+ */
234
+ findKeyByPublicKeyBase(publicKeyBase: PublicKeyBase): Key | undefined {
235
+ const hashKey = publicKeyBase.hex();
236
+ return this._keys.get(hashKey);
237
+ }
238
+
239
+ /**
240
+ * Find a key by its reference.
241
+ */
242
+ findKeyByReference(reference: Reference): Key | undefined {
243
+ for (const key of this._keys.values()) {
244
+ if (key.reference().equals(reference)) {
245
+ return key;
246
+ }
247
+ }
248
+ return undefined;
249
+ }
250
+
251
+ /**
252
+ * Take and remove a key.
253
+ */
254
+ takeKey(publicKeyBase: PublicKeyBase): Key | undefined {
255
+ const hashKey = publicKeyBase.hex();
256
+ const key = this._keys.get(hashKey);
257
+ if (key !== undefined) {
258
+ this._keys.delete(hashKey);
259
+ }
260
+ return key;
261
+ }
262
+
263
+ /**
264
+ * Remove a key.
265
+ */
266
+ removeKey(publicKeyBase: PublicKeyBase): void {
267
+ if (this.servicesReferenceKey(publicKeyBase)) {
268
+ throw XIDError.stillReferenced("key");
269
+ }
270
+ const hashKey = publicKeyBase.hex();
271
+ if (!this._keys.delete(hashKey)) {
272
+ throw XIDError.notFound("key");
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Check if the given public key is the inception signing key.
278
+ */
279
+ isInceptionKey(publicKeyBase: PublicKeyBase): boolean {
280
+ const xidData = hashPublicKey(publicKeyBase);
281
+ return bytesEqual(xidData, this._xid.toData());
282
+ }
283
+
284
+ /**
285
+ * Get the inception key, if it exists in the document.
286
+ */
287
+ inceptionKey(): Key | undefined {
288
+ for (const key of this._keys.values()) {
289
+ if (this.isInceptionKey(key.publicKeyBase())) {
290
+ return key;
291
+ }
292
+ }
293
+ return undefined;
294
+ }
295
+
296
+ /**
297
+ * Get the inception private key base, if available.
298
+ */
299
+ inceptionPrivateKeyBase(): PrivateKeyBase | undefined {
300
+ return this.inceptionKey()?.privateKeyBase();
301
+ }
302
+
303
+ /**
304
+ * Remove the inception key from the document.
305
+ */
306
+ removeInceptionKey(): Key | undefined {
307
+ const inceptionKey = this.inceptionKey();
308
+ if (inceptionKey !== undefined) {
309
+ this._keys.delete(inceptionKey.hashKey());
310
+ }
311
+ return inceptionKey;
312
+ }
313
+
314
+ /**
315
+ * Check if the document is empty (no keys, delegates, services, or provenance).
316
+ */
317
+ isEmpty(): boolean {
318
+ return (
319
+ this._resolutionMethods.size === 0 &&
320
+ this._keys.size === 0 &&
321
+ this._delegates.size === 0 &&
322
+ this._provenance === undefined
323
+ );
324
+ }
325
+
326
+ /**
327
+ * Get all delegates.
328
+ */
329
+ delegates(): Delegate[] {
330
+ return Array.from(this._delegates.values());
331
+ }
332
+
333
+ /**
334
+ * Add a delegate.
335
+ */
336
+ addDelegate(delegate: Delegate): void {
337
+ const hashKey = delegate.hashKey();
338
+ if (this._delegates.has(hashKey)) {
339
+ throw XIDError.duplicate("delegate");
340
+ }
341
+ this._delegates.set(hashKey, delegate);
342
+ }
343
+
344
+ /**
345
+ * Find a delegate by XID.
346
+ */
347
+ findDelegateByXid(xid: XID): Delegate | undefined {
348
+ return this._delegates.get(xid.toHex());
349
+ }
350
+
351
+ /**
352
+ * Find a delegate by reference.
353
+ */
354
+ findDelegateByReference(reference: Reference): Delegate | undefined {
355
+ for (const delegate of this._delegates.values()) {
356
+ if (delegate.reference().equals(reference)) {
357
+ return delegate;
358
+ }
359
+ }
360
+ return undefined;
361
+ }
362
+
363
+ /**
364
+ * Take and remove a delegate.
365
+ */
366
+ takeDelegate(xid: XID): Delegate | undefined {
367
+ const hashKey = xid.toHex();
368
+ const delegate = this._delegates.get(hashKey);
369
+ if (delegate !== undefined) {
370
+ this._delegates.delete(hashKey);
371
+ }
372
+ return delegate;
373
+ }
374
+
375
+ /**
376
+ * Remove a delegate.
377
+ */
378
+ removeDelegate(xid: XID): void {
379
+ if (this.servicesReferenceDelegate(xid)) {
380
+ throw XIDError.stillReferenced("delegate");
381
+ }
382
+ const hashKey = xid.toHex();
383
+ if (!this._delegates.delete(hashKey)) {
384
+ throw XIDError.notFound("delegate");
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Get all services.
390
+ */
391
+ services(): Service[] {
392
+ return Array.from(this._services.values());
393
+ }
394
+
395
+ /**
396
+ * Find a service by URI.
397
+ */
398
+ findServiceByUri(uri: string): Service | undefined {
399
+ return this._services.get(uri);
400
+ }
401
+
402
+ /**
403
+ * Add a service.
404
+ */
405
+ addService(service: Service): void {
406
+ const hashKey = service.hashKey();
407
+ if (this._services.has(hashKey)) {
408
+ throw XIDError.duplicate("service");
409
+ }
410
+ this._services.set(hashKey, service);
411
+ }
412
+
413
+ /**
414
+ * Take and remove a service.
415
+ */
416
+ takeService(uri: string): Service | undefined {
417
+ const service = this._services.get(uri);
418
+ if (service !== undefined) {
419
+ this._services.delete(uri);
420
+ }
421
+ return service;
422
+ }
423
+
424
+ /**
425
+ * Remove a service.
426
+ */
427
+ removeService(uri: string): void {
428
+ if (!this._services.delete(uri)) {
429
+ throw XIDError.notFound("service");
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Check service consistency.
435
+ */
436
+ checkServicesConsistency(): void {
437
+ for (const service of this._services.values()) {
438
+ this.checkServiceConsistency(service);
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Check consistency of a single service.
444
+ */
445
+ checkServiceConsistency(service: Service): void {
446
+ if (service.keyReferences().size === 0 && service.delegateReferences().size === 0) {
447
+ throw XIDError.noReferences(service.uri());
448
+ }
449
+
450
+ for (const keyRef of service.keyReferences()) {
451
+ const refBytes = hexToBytes(keyRef);
452
+ const ref = Reference.hash(refBytes);
453
+ if (this.findKeyByReference(ref) === undefined) {
454
+ throw XIDError.unknownKeyReference(keyRef, service.uri());
455
+ }
456
+ }
457
+
458
+ for (const delegateRef of service.delegateReferences()) {
459
+ const refBytes = hexToBytes(delegateRef);
460
+ const ref = Reference.hash(refBytes);
461
+ if (this.findDelegateByReference(ref) === undefined) {
462
+ throw XIDError.unknownDelegateReference(delegateRef, service.uri());
463
+ }
464
+ }
465
+
466
+ if (service.permissions().allow.size === 0) {
467
+ throw XIDError.noPermissions(service.uri());
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Check if any service references the given key.
473
+ */
474
+ servicesReferenceKey(publicKeyBase: PublicKeyBase): boolean {
475
+ const keyRef = Reference.hash(publicKeyBase.data()).toHex();
476
+ for (const service of this._services.values()) {
477
+ if (service.keyReferences().has(keyRef)) {
478
+ return true;
479
+ }
480
+ }
481
+ return false;
482
+ }
483
+
484
+ /**
485
+ * Check if any service references the given delegate.
486
+ */
487
+ servicesReferenceDelegate(xid: XID): boolean {
488
+ const delegateRef = Reference.hash(xid.toData()).toHex();
489
+ for (const service of this._services.values()) {
490
+ if (service.delegateReferences().has(delegateRef)) {
491
+ return true;
492
+ }
493
+ }
494
+ return false;
495
+ }
496
+
497
+ /**
498
+ * Get the provenance mark.
499
+ */
500
+ provenance(): ProvenanceMark | undefined {
501
+ return this._provenance?.mark();
502
+ }
503
+
504
+ /**
505
+ * Get the provenance generator.
506
+ */
507
+ provenanceGenerator(): ProvenanceMarkGenerator | undefined {
508
+ return this._provenance?.generator();
509
+ }
510
+
511
+ /**
512
+ * Set the provenance.
513
+ */
514
+ setProvenance(provenance: ProvenanceMark | undefined): void {
515
+ this._provenance = provenance !== undefined ? Provenance.new(provenance) : undefined;
516
+ }
517
+
518
+ /**
519
+ * Set provenance with generator.
520
+ */
521
+ setProvenanceWithGenerator(generator: ProvenanceMarkGenerator, mark: ProvenanceMark): void {
522
+ this._provenance = Provenance.newWithGenerator(generator, mark);
523
+ }
524
+
525
+ /**
526
+ * Advance the provenance mark using the embedded generator.
527
+ */
528
+ nextProvenanceMarkWithEmbeddedGenerator(password?: Uint8Array, date?: Date, info?: Cbor): void {
529
+ if (this._provenance === undefined) {
530
+ throw XIDError.noProvenanceMark();
531
+ }
532
+
533
+ const currentMark = this._provenance.mark();
534
+ const generator = this._provenance.generatorMut(password);
535
+ if (generator === undefined) {
536
+ throw XIDError.noGenerator();
537
+ }
538
+
539
+ // Validate chain ID matches
540
+ if (!bytesEqual(generator.chainId(), currentMark.chainId())) {
541
+ throw XIDError.chainIdMismatch(currentMark.chainId(), generator.chainId());
542
+ }
543
+
544
+ // Validate sequence number
545
+ const expectedSeq = currentMark.seq() + 1;
546
+ if (generator.nextSeq() !== expectedSeq) {
547
+ throw XIDError.sequenceMismatch(expectedSeq, generator.nextSeq());
548
+ }
549
+
550
+ // Generate next mark
551
+ const nextDate = date ?? new Date();
552
+ const nextMark = generator.next(nextDate, info);
553
+ this._provenance.setMark(nextMark);
554
+ }
555
+
556
+ /**
557
+ * Advance the provenance mark using a provided generator.
558
+ */
559
+ nextProvenanceMarkWithProvidedGenerator(
560
+ generator: ProvenanceMarkGenerator,
561
+ date?: Date,
562
+ info?: Cbor,
563
+ ): void {
564
+ if (this._provenance === undefined) {
565
+ throw XIDError.noProvenanceMark();
566
+ }
567
+
568
+ // Check that document doesn't have embedded generator
569
+ if (this._provenance.hasGenerator() || this._provenance.hasEncryptedGenerator()) {
570
+ throw XIDError.generatorConflict();
571
+ }
572
+
573
+ const currentMark = this._provenance.mark();
574
+
575
+ // Validate chain ID matches
576
+ if (!bytesEqual(generator.chainId(), currentMark.chainId())) {
577
+ throw XIDError.chainIdMismatch(currentMark.chainId(), generator.chainId());
578
+ }
579
+
580
+ // Validate sequence number
581
+ const expectedSeq = currentMark.seq() + 1;
582
+ if (generator.nextSeq() !== expectedSeq) {
583
+ throw XIDError.sequenceMismatch(expectedSeq, generator.nextSeq());
584
+ }
585
+
586
+ // Generate next mark
587
+ const nextDate = date ?? new Date();
588
+ const nextMark = generator.next(nextDate, info);
589
+ this._provenance.setMark(nextMark);
590
+ }
591
+
592
+ /**
593
+ * Convert to envelope with options.
594
+ */
595
+ toEnvelope(
596
+ privateKeyOptions: XIDPrivateKeyOptionsValue = XIDPrivateKeyOptions.Omit,
597
+ generatorOptions: XIDGeneratorOptionsValue = XIDGeneratorOptions.Omit,
598
+ signingOptions: XIDSigningOptions = { type: "none" },
599
+ ): Envelope {
600
+ let envelope = Envelope.new(this._xid.toData());
601
+
602
+ // Add resolution methods
603
+ for (const method of this._resolutionMethods) {
604
+ envelope = envelope.addAssertion(kv(DEREFERENCE_VIA), method);
605
+ }
606
+
607
+ // Add keys
608
+ for (const key of this._keys.values()) {
609
+ envelope = envelope.addAssertion(kv(KEY), key.intoEnvelopeOpt(privateKeyOptions));
610
+ }
611
+
612
+ // Add delegates
613
+ for (const delegate of this._delegates.values()) {
614
+ envelope = envelope.addAssertion(kv(DELEGATE), delegate.intoEnvelope());
615
+ }
616
+
617
+ // Add services
618
+ for (const service of this._services.values()) {
619
+ envelope = envelope.addAssertion(kv(SERVICE), service.intoEnvelope());
620
+ }
621
+
622
+ // Add provenance
623
+ if (this._provenance !== undefined) {
624
+ envelope = envelope.addAssertion(
625
+ kv(PROVENANCE),
626
+ this._provenance.intoEnvelopeOpt(generatorOptions),
627
+ );
628
+ }
629
+
630
+ // Apply signing
631
+ switch (signingOptions.type) {
632
+ case "inception": {
633
+ const inceptionKey = this.inceptionKey();
634
+ if (inceptionKey === undefined) {
635
+ throw XIDError.missingInceptionKey();
636
+ }
637
+ const privateKeyBase = inceptionKey.privateKeyBase();
638
+ if (privateKeyBase === undefined) {
639
+ throw XIDError.missingInceptionKey();
640
+ }
641
+ envelope = (envelope as unknown as { addSignature(s: Signer): Envelope }).addSignature(
642
+ privateKeyBase as unknown as Signer,
643
+ );
644
+ break;
645
+ }
646
+ case "privateKeyBase":
647
+ envelope = (envelope as unknown as { addSignature(s: Signer): Envelope }).addSignature(
648
+ signingOptions.privateKeyBase as unknown as Signer,
649
+ );
650
+ break;
651
+ case "none":
652
+ default:
653
+ break;
654
+ }
655
+
656
+ return envelope;
657
+ }
658
+
659
+ // EnvelopeEncodable implementation
660
+ intoEnvelope(): Envelope {
661
+ return this.toEnvelope();
662
+ }
663
+
664
+ /**
665
+ * Extract an XIDDocument from an envelope.
666
+ */
667
+ static fromEnvelope(
668
+ envelope: Envelope,
669
+ password?: Uint8Array,
670
+ verifySignature: XIDVerifySignature = XIDVerifySignature.None,
671
+ ): XIDDocument {
672
+ const envelopeExt = envelope as unknown as {
673
+ subject(): Envelope & { isWrapped(): boolean; tryUnwrap(): Envelope };
674
+ tryUnwrap(): Envelope;
675
+ hasSignatureFrom(verifier: unknown): boolean;
676
+ };
677
+ switch (verifySignature) {
678
+ case XIDVerifySignature.None: {
679
+ const subject = envelopeExt.subject();
680
+ const envelopeToParse = subject.isWrapped() ? subject.tryUnwrap() : envelope;
681
+ return XIDDocument.fromEnvelopeInner(envelopeToParse, password);
682
+ }
683
+ case XIDVerifySignature.Inception: {
684
+ if (!envelopeExt.subject().isWrapped()) {
685
+ throw XIDError.envelopeNotSigned();
686
+ }
687
+
688
+ const unwrapped = envelopeExt.tryUnwrap();
689
+ const doc = XIDDocument.fromEnvelopeInner(unwrapped, password);
690
+
691
+ const inceptionKey = doc.inceptionKey();
692
+ if (inceptionKey === undefined) {
693
+ throw XIDError.missingInceptionKey();
694
+ }
695
+
696
+ // Verify signature
697
+ if (!envelopeExt.hasSignatureFrom(inceptionKey.publicKeyBase() as unknown as Verifier)) {
698
+ throw XIDError.signatureVerificationFailed();
699
+ }
700
+
701
+ // Verify XID matches inception key
702
+ if (!doc.isInceptionKey(inceptionKey.publicKeyBase())) {
703
+ throw XIDError.invalidXid();
704
+ }
705
+
706
+ return doc;
707
+ }
708
+ }
709
+ }
710
+
711
+ private static fromEnvelopeInner(envelope: Envelope, password?: Uint8Array): XIDDocument {
712
+ const envelopeExt = envelope as unknown as {
713
+ asByteString(): Uint8Array | undefined;
714
+ subject(): Envelope;
715
+ assertions(): Envelope[];
716
+ };
717
+
718
+ // Extract XID from subject
719
+ // The envelope may be a node (with assertions) or a leaf
720
+ const envCase = envelope.case();
721
+ const subject = envCase.type === "node" ? envelopeExt.subject() : envelope;
722
+ const xidData = (
723
+ subject as unknown as { asByteString(): Uint8Array | undefined }
724
+ ).asByteString();
725
+ if (xidData === undefined) {
726
+ throw XIDError.invalidXid();
727
+ }
728
+ const xid = XID.from(xidData);
729
+ const doc = XIDDocument.fromXid(xid);
730
+
731
+ // Process assertions
732
+ for (const assertion of envelopeExt.assertions()) {
733
+ const assertionCase = assertion.case();
734
+ if (assertionCase.type !== "assertion") {
735
+ continue;
736
+ }
737
+
738
+ const predicateEnv = assertionCase.assertion.predicate();
739
+ const predicateCase = predicateEnv.case();
740
+ if (predicateCase.type !== "knownValue") {
741
+ continue;
742
+ }
743
+
744
+ const predicate = predicateCase.value.value();
745
+ const object = assertionCase.assertion.object();
746
+
747
+ switch (predicate) {
748
+ case DEREFERENCE_VIA_RAW: {
749
+ const method = (object as unknown as { asText(): string | undefined }).asText();
750
+ if (method === undefined) {
751
+ throw XIDError.invalidResolutionMethod();
752
+ }
753
+ doc.addResolutionMethod(method);
754
+ break;
755
+ }
756
+ case KEY_RAW: {
757
+ const key = Key.tryFromEnvelope(object, password);
758
+ doc.addKey(key);
759
+ break;
760
+ }
761
+ case DELEGATE_RAW: {
762
+ const delegate = Delegate.tryFromEnvelope(object);
763
+ doc.addDelegate(delegate);
764
+ break;
765
+ }
766
+ case SERVICE_RAW: {
767
+ const service = Service.tryFromEnvelope(object);
768
+ doc.addService(service);
769
+ break;
770
+ }
771
+ case PROVENANCE_RAW: {
772
+ if (doc._provenance !== undefined) {
773
+ throw XIDError.multipleProvenanceMarks();
774
+ }
775
+ doc._provenance = Provenance.tryFromEnvelope(object, password);
776
+ break;
777
+ }
778
+ default:
779
+ throw XIDError.unexpectedPredicate(String(predicate));
780
+ }
781
+ }
782
+
783
+ doc.checkServicesConsistency();
784
+ return doc;
785
+ }
786
+
787
+ /**
788
+ * Create a signed envelope.
789
+ */
790
+ toSignedEnvelope(signingKey: Signer): Envelope {
791
+ const envelope = this.toEnvelope(XIDPrivateKeyOptions.Omit, XIDGeneratorOptions.Omit, {
792
+ type: "none",
793
+ });
794
+ return (envelope as unknown as { addSignature(s: Signer): Envelope }).addSignature(signingKey);
795
+ }
796
+
797
+ /**
798
+ * Get the reference for this document.
799
+ */
800
+ reference(): Reference {
801
+ return Reference.hash(this._xid.toData());
802
+ }
803
+
804
+ /**
805
+ * Check equality with another XIDDocument.
806
+ */
807
+ equals(other: XIDDocument): boolean {
808
+ return this._xid.equals(other._xid);
809
+ }
810
+
811
+ /**
812
+ * Clone this XIDDocument.
813
+ */
814
+ clone(): XIDDocument {
815
+ const doc = new XIDDocument(
816
+ this._xid,
817
+ new Set(this._resolutionMethods),
818
+ new Map(Array.from(this._keys.entries()).map(([k, v]) => [k, v.clone()])),
819
+ new Map(Array.from(this._delegates.entries()).map(([k, v]) => [k, v.clone()])),
820
+ new Map(Array.from(this._services.entries()).map(([k, v]) => [k, v.clone()])),
821
+ this._provenance?.clone(),
822
+ );
823
+ return doc;
824
+ }
825
+
826
+ /**
827
+ * Try to extract from envelope (alias for fromEnvelope with default options).
828
+ */
829
+ static tryFromEnvelope(envelope: Envelope): XIDDocument {
830
+ return XIDDocument.fromEnvelope(envelope, undefined, XIDVerifySignature.None);
831
+ }
832
+ }
833
+
834
+ // Register XIDDocument class with Delegate to resolve circular dependency
835
+ registerXIDDocumentClass(XIDDocument);
836
+
837
+ // Helper interface for Verifier
838
+ interface Verifier {
839
+ verify(data: Uint8Array, signature: { data(): Uint8Array }): boolean;
840
+ }
841
+
842
+ // Helper functions
843
+ function hexToBytes(hex: string): Uint8Array {
844
+ const bytes = new Uint8Array(hex.length / 2);
845
+ for (let i = 0; i < hex.length; i += 2) {
846
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
847
+ }
848
+ return bytes;
849
+ }
850
+
851
+ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
852
+ if (a.length !== b.length) return false;
853
+ for (let i = 0; i < a.length; i++) {
854
+ if (a[i] !== b[i]) return false;
855
+ }
856
+ return true;
857
+ }
858
+
859
+ function hashPublicKey(publicKeyBase: PublicKeyBase): Uint8Array {
860
+ // SHA-256 hash of public key to get XID
861
+ return Reference.hash(publicKeyBase.data()).getDigest().toData();
862
+ }