@bcts/xid 1.0.0-alpha.17 → 1.0.0-alpha.18

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.
@@ -6,13 +6,23 @@
6
6
  * Ported from bc-xid-rust/src/xid_document.rs
7
7
  */
8
8
 
9
- import { Envelope, type EnvelopeEncodable, type EnvelopeEncodableValue } from "@bcts/envelope";
9
+ import {
10
+ Envelope,
11
+ Attachments,
12
+ Edges,
13
+ type Edgeable,
14
+ type EnvelopeEncodable,
15
+ type EnvelopeEncodableValue,
16
+ } from "@bcts/envelope";
17
+ import type { Digest } from "@bcts/envelope";
10
18
  import {
11
19
  KEY,
12
20
  DELEGATE,
13
21
  SERVICE,
14
22
  PROVENANCE,
15
23
  DEREFERENCE_VIA,
24
+ ATTACHMENT_RAW as ATTACHMENT_RAW_VAL,
25
+ EDGE_RAW as EDGE_RAW_VAL,
16
26
  type KnownValue,
17
27
  } from "@bcts/known-values";
18
28
 
@@ -26,6 +36,8 @@ import {
26
36
  type PrivateKeys,
27
37
  type Signer,
28
38
  type EncapsulationPublicKey,
39
+ type SigningPublicKey,
40
+ type SigningPrivateKey,
29
41
  } from "@bcts/components";
30
42
  import {
31
43
  type ProvenanceMark,
@@ -45,6 +57,8 @@ const DELEGATE_RAW = DELEGATE.value();
45
57
  const SERVICE_RAW = SERVICE.value();
46
58
  const PROVENANCE_RAW = PROVENANCE.value();
47
59
  const DEREFERENCE_VIA_RAW = DEREFERENCE_VIA.value();
60
+ const ATTACHMENT_RAW_VALUE = Number(ATTACHMENT_RAW_VAL);
61
+ const EDGE_RAW_VALUE = Number(EDGE_RAW_VAL);
48
62
 
49
63
  /**
50
64
  * Options for creating the inception key.
@@ -81,8 +95,8 @@ export type XIDGenesisMarkOptions =
81
95
  export type XIDSigningOptions =
82
96
  | { type: "none" }
83
97
  | { type: "inception" }
84
- | { type: "privateKeyBase"; privateKeyBase: PrivateKeyBase }
85
- | { type: "privateKeys"; privateKeys: PrivateKeys };
98
+ | { type: "privateKeys"; privateKeys: PrivateKeys }
99
+ | { type: "signingPrivateKey"; signingPrivateKey: SigningPrivateKey };
86
100
 
87
101
  /**
88
102
  * Options for verifying the signature on an envelope when loading.
@@ -102,13 +116,15 @@ type ServiceMap = Map<string, Service>;
102
116
  /**
103
117
  * Represents an XID document.
104
118
  */
105
- export class XIDDocument implements EnvelopeEncodable {
119
+ export class XIDDocument implements EnvelopeEncodable, Edgeable {
106
120
  private readonly _xid: XID;
107
121
  private readonly _resolutionMethods: Set<string>;
108
122
  private readonly _keys: KeyMap;
109
123
  private readonly _delegates: DelegateMap;
110
124
  private readonly _services: ServiceMap;
111
125
  private _provenance: Provenance | undefined;
126
+ private _attachments: Attachments;
127
+ private _edges: Edges;
112
128
 
113
129
  private constructor(
114
130
  xid: XID,
@@ -117,6 +133,8 @@ export class XIDDocument implements EnvelopeEncodable {
117
133
  delegates: DelegateMap = new Map(),
118
134
  services: ServiceMap = new Map(),
119
135
  provenance?: Provenance,
136
+ attachments?: Attachments,
137
+ edges?: Edges,
120
138
  ) {
121
139
  this._xid = xid;
122
140
  this._resolutionMethods = resolutionMethods;
@@ -124,6 +142,8 @@ export class XIDDocument implements EnvelopeEncodable {
124
142
  this._delegates = delegates;
125
143
  this._services = services;
126
144
  this._provenance = provenance;
145
+ this._attachments = attachments ?? new Attachments();
146
+ this._edges = edges ?? new Edges();
127
147
  }
128
148
 
129
149
  /**
@@ -136,9 +156,9 @@ export class XIDDocument implements EnvelopeEncodable {
136
156
  const inceptionKey = XIDDocument.inceptionKeyForOptions(keyOptions);
137
157
  const provenance = XIDDocument.genesisMarkWithOptions(markOptions);
138
158
 
139
- // Use the reference from PublicKeys (which uses tagged CBOR hash)
140
- // XID is created from the digest data of the reference
141
- const xid = XID.from(inceptionKey.publicKeys().reference().getDigest().toData());
159
+ // XID is the SHA-256 digest of the CBOR encoding of the inception signing public key
160
+ // This matches Rust: XID::new(inception_key.public_keys().signing_public_key())
161
+ const xid = XID.newFromSigningKey(inceptionKey.publicKeys().signingPublicKey());
142
162
  const doc = new XIDDocument(xid, new Set(), new Map(), new Map(), new Map(), provenance);
143
163
 
144
164
  doc.addKey(inceptionKey);
@@ -280,12 +300,11 @@ export class XIDDocument implements EnvelopeEncodable {
280
300
  }
281
301
 
282
302
  /**
283
- * Check if the given public keys is the inception signing key.
303
+ * Check if the given signing public key is the inception signing key.
304
+ * Matches Rust: `is_inception_signing_key(&self, signing_public_key: &SigningPublicKey) -> bool`
284
305
  */
285
- isInceptionKey(publicKeys: PublicKeys): boolean {
286
- // The XID is derived from the reference of the inception PublicKeys
287
- const xidReference = publicKeys.reference();
288
- return bytesEqual(xidReference.getDigest().toData(), this._xid.toData());
306
+ isInceptionSigningKey(signingPublicKey: SigningPublicKey): boolean {
307
+ return this._xid.validate(signingPublicKey);
289
308
  }
290
309
 
291
310
  /**
@@ -293,7 +312,7 @@ export class XIDDocument implements EnvelopeEncodable {
293
312
  */
294
313
  inceptionKey(): Key | undefined {
295
314
  for (const key of this._keys.values()) {
296
- if (this.isInceptionKey(key.publicKeys())) {
315
+ if (this.isInceptionSigningKey(key.publicKeys().signingPublicKey())) {
297
316
  return key;
298
317
  }
299
318
  }
@@ -338,6 +357,180 @@ export class XIDDocument implements EnvelopeEncodable {
338
357
  return inceptionKey;
339
358
  }
340
359
 
360
+ /**
361
+ * Set the name (nickname) for a key identified by its public keys.
362
+ */
363
+ setNameForKey(publicKeys: PublicKeys, name: string): void {
364
+ const key = this.takeKey(publicKeys);
365
+ if (key === undefined) {
366
+ throw XIDError.notFound("key");
367
+ }
368
+ key.setNickname(name);
369
+ this.addKey(key);
370
+ }
371
+
372
+ /**
373
+ * Get the inception signing public key, if it exists.
374
+ */
375
+ inceptionSigningKey(): SigningPublicKey | undefined {
376
+ const key = this.inceptionKey();
377
+ return key?.publicKeys().signingPublicKey();
378
+ }
379
+
380
+ /**
381
+ * Get the verification (signing) key for this document.
382
+ * Prefers the inception key. Falls back to the first key.
383
+ */
384
+ verificationKey(): SigningPublicKey | undefined {
385
+ const inceptionKey = this.inceptionKey();
386
+ if (inceptionKey !== undefined) {
387
+ return inceptionKey.publicKeys().signingPublicKey();
388
+ }
389
+ const firstKey = this._keys.values().next().value;
390
+ return firstKey?.publicKeys().signingPublicKey();
391
+ }
392
+
393
+ /**
394
+ * Extract inception private keys from an envelope (convenience static method).
395
+ */
396
+ static extractInceptionPrivateKeysFromEnvelope(
397
+ envelope: Envelope,
398
+ password: Uint8Array,
399
+ ): PrivateKeys | undefined {
400
+ const doc = XIDDocument.fromEnvelope(envelope, password, XIDVerifySignature.None);
401
+ return doc.inceptionPrivateKeys();
402
+ }
403
+
404
+ /**
405
+ * Get the private key envelope for a specific key, optionally decrypting it.
406
+ */
407
+ privateKeyEnvelopeForKey(publicKeys: PublicKeys, password?: string): Envelope | undefined {
408
+ const key = this.findKeyByPublicKeys(publicKeys);
409
+ if (key === undefined) {
410
+ return undefined;
411
+ }
412
+ return key.privateKeyEnvelope(password);
413
+ }
414
+
415
+ /**
416
+ * Check that the document contains a key with the given public keys.
417
+ * Throws if not found.
418
+ */
419
+ checkContainsKey(publicKeys: PublicKeys): void {
420
+ if (this.findKeyByPublicKeys(publicKeys) === undefined) {
421
+ throw XIDError.keyNotFoundInDocument(publicKeys.toString());
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Check that the document contains a delegate with the given XID.
427
+ * Throws if not found.
428
+ */
429
+ checkContainsDelegate(xid: XID): void {
430
+ if (this.findDelegateByXid(xid) === undefined) {
431
+ throw XIDError.delegateNotFoundInDocument(xid.toString());
432
+ }
433
+ }
434
+
435
+ // ============================================================================
436
+ // Attachable interface implementation
437
+ // ============================================================================
438
+
439
+ /**
440
+ * Get the attachments container.
441
+ */
442
+ getAttachments(): Attachments {
443
+ return this._attachments;
444
+ }
445
+
446
+ /**
447
+ * Add an attachment with the specified payload and metadata.
448
+ */
449
+ addAttachment(payload: EnvelopeEncodableValue, vendor: string, conformsTo?: string): void {
450
+ this._attachments.add(payload, vendor, conformsTo);
451
+ }
452
+
453
+ /**
454
+ * Check if the document has any attachments.
455
+ */
456
+ hasAttachments(): boolean {
457
+ return !this._attachments.isEmpty();
458
+ }
459
+
460
+ /**
461
+ * Remove all attachments.
462
+ */
463
+ clearAttachments(): void {
464
+ this._attachments.clear();
465
+ }
466
+
467
+ /**
468
+ * Get an attachment by its digest.
469
+ */
470
+ getAttachment(digest: Digest): Envelope | undefined {
471
+ return this._attachments.get(digest);
472
+ }
473
+
474
+ /**
475
+ * Remove an attachment by its digest.
476
+ */
477
+ removeAttachment(digest: Digest): Envelope | undefined {
478
+ return this._attachments.remove(digest);
479
+ }
480
+
481
+ // ============================================================================
482
+ // Edgeable interface implementation
483
+ // ============================================================================
484
+
485
+ /**
486
+ * Get the edges container (read-only).
487
+ */
488
+ edges(): Edges {
489
+ return this._edges;
490
+ }
491
+
492
+ /**
493
+ * Get the edges container (mutable).
494
+ */
495
+ edgesMut(): Edges {
496
+ return this._edges;
497
+ }
498
+
499
+ /**
500
+ * Add an edge envelope.
501
+ */
502
+ addEdge(edgeEnvelope: Envelope): void {
503
+ this._edges.add(edgeEnvelope);
504
+ }
505
+
506
+ /**
507
+ * Get an edge by its digest.
508
+ */
509
+ getEdge(digest: Digest): Envelope | undefined {
510
+ return this._edges.get(digest);
511
+ }
512
+
513
+ /**
514
+ * Remove an edge by its digest.
515
+ */
516
+ removeEdge(digest: Digest): Envelope | undefined {
517
+ return this._edges.remove(digest);
518
+ }
519
+
520
+ /**
521
+ * Remove all edges.
522
+ */
523
+ clearEdges(): void {
524
+ this._edges.clear();
525
+ }
526
+
527
+ /**
528
+ * Check if the document has any edges.
529
+ */
530
+ hasEdges(): boolean {
531
+ return !this._edges.isEmpty();
532
+ }
533
+
341
534
  /**
342
535
  * Check if the document is empty (no keys, delegates, services, or provenance).
343
536
  */
@@ -624,7 +817,8 @@ export class XIDDocument implements EnvelopeEncodable {
624
817
  generatorOptions: XIDGeneratorOptionsValue = XIDGeneratorOptions.Omit,
625
818
  signingOptions: XIDSigningOptions = { type: "none" },
626
819
  ): Envelope {
627
- let envelope = Envelope.new(this._xid.toData());
820
+ // Use tagged CBOR representation, matching Rust's Envelope::new(self.xid)
821
+ let envelope = Envelope.newLeaf(this._xid.taggedCbor());
628
822
 
629
823
  // Add resolution methods
630
824
  for (const method of this._resolutionMethods) {
@@ -654,6 +848,12 @@ export class XIDDocument implements EnvelopeEncodable {
654
848
  );
655
849
  }
656
850
 
851
+ // Add attachments before signing so they are included in the signature
852
+ envelope = this._attachments.addToEnvelope(envelope);
853
+
854
+ // Add edges before signing so they are included in the signature
855
+ envelope = this._edges.addToEnvelope(envelope);
856
+
657
857
  // Apply signing (uses sign() which wraps the envelope first)
658
858
  // PrivateKeys implements Signer from @bcts/components, which is compatible with envelope's sign()
659
859
  switch (signingOptions.type) {
@@ -669,18 +869,18 @@ export class XIDDocument implements EnvelopeEncodable {
669
869
  envelope = (envelope as unknown as { sign(s: Signer): Envelope }).sign(privateKeys);
670
870
  break;
671
871
  }
672
- case "privateKeyBase": {
673
- // Derive PrivateKeys from PrivateKeyBase and use for signing
674
- const privateKeys = signingOptions.privateKeyBase.ed25519PrivateKeys();
675
- envelope = (envelope as unknown as { sign(s: Signer): Envelope }).sign(privateKeys);
676
- break;
677
- }
678
872
  case "privateKeys": {
679
873
  envelope = (envelope as unknown as { sign(s: Signer): Envelope }).sign(
680
874
  signingOptions.privateKeys,
681
875
  );
682
876
  break;
683
877
  }
878
+ case "signingPrivateKey": {
879
+ envelope = (envelope as unknown as { sign(s: Signer): Envelope }).sign(
880
+ signingOptions.signingPrivateKey,
881
+ );
882
+ break;
883
+ }
684
884
  case "none":
685
885
  default:
686
886
  break;
@@ -691,6 +891,9 @@ export class XIDDocument implements EnvelopeEncodable {
691
891
 
692
892
  // EnvelopeEncodable implementation
693
893
  intoEnvelope(): Envelope {
894
+ if (this.isEmpty()) {
895
+ return Envelope.new(this._xid.toData());
896
+ }
694
897
  return this.toEnvelope();
695
898
  }
696
899
 
@@ -711,7 +914,16 @@ export class XIDDocument implements EnvelopeEncodable {
711
914
  case XIDVerifySignature.None: {
712
915
  const subject = envelopeExt.subject();
713
916
  const envelopeToParse = subject.isWrapped() ? subject.tryUnwrap() : envelope;
714
- return XIDDocument.fromEnvelopeInner(envelopeToParse, password);
917
+
918
+ // Extract attachments from the envelope
919
+ const attachments = Attachments.fromEnvelope(envelopeToParse);
920
+ // Extract edges from the envelope
921
+ const edges = Edges.fromEnvelope(envelopeToParse);
922
+
923
+ const doc = XIDDocument.fromEnvelopeInner(envelopeToParse, password);
924
+ doc._attachments = attachments;
925
+ doc._edges = edges;
926
+ return doc;
715
927
  }
716
928
  case XIDVerifySignature.Inception: {
717
929
  if (!envelopeExt.subject().isWrapped()) {
@@ -719,6 +931,12 @@ export class XIDDocument implements EnvelopeEncodable {
719
931
  }
720
932
 
721
933
  const unwrapped = envelopeExt.tryUnwrap();
934
+
935
+ // Extract attachments from the unwrapped envelope
936
+ const attachments = Attachments.fromEnvelope(unwrapped);
937
+ // Extract edges from the unwrapped envelope
938
+ const edges = Edges.fromEnvelope(unwrapped);
939
+
722
940
  const doc = XIDDocument.fromEnvelopeInner(unwrapped, password);
723
941
 
724
942
  const inceptionKey = doc.inceptionKey();
@@ -732,10 +950,12 @@ export class XIDDocument implements EnvelopeEncodable {
732
950
  }
733
951
 
734
952
  // Verify XID matches inception key
735
- if (!doc.isInceptionKey(inceptionKey.publicKeys())) {
953
+ if (!doc.isInceptionSigningKey(inceptionKey.publicKeys().signingPublicKey())) {
736
954
  throw XIDError.invalidXid();
737
955
  }
738
956
 
957
+ doc._attachments = attachments;
958
+ doc._edges = edges;
739
959
  return doc;
740
960
  }
741
961
  }
@@ -752,13 +972,28 @@ export class XIDDocument implements EnvelopeEncodable {
752
972
  // The envelope may be a node (with assertions) or a leaf
753
973
  const envCase = envelope.case();
754
974
  const subject = envCase.type === "node" ? envelopeExt.subject() : envelope;
755
- const xidData = (
756
- subject as unknown as { asByteString(): Uint8Array | undefined }
757
- ).asByteString();
758
- if (xidData === undefined) {
975
+
976
+ // Try to extract XID from the subject leaf.
977
+ // Rust-generated documents store the XID as tagged CBOR (Tag 40015 + byte string).
978
+ // TS-generated documents may store it as a raw byte string.
979
+ const leaf = (subject as unknown as { asLeaf(): Cbor | undefined }).asLeaf?.();
980
+ if (leaf === undefined) {
759
981
  throw XIDError.invalidXid();
760
982
  }
761
- const xid = XID.from(xidData);
983
+ let xid: XID;
984
+ try {
985
+ // Try tagged CBOR first (matches Rust's Envelope::new(xid))
986
+ xid = XID.fromTaggedCbor(leaf);
987
+ } catch {
988
+ // Fall back to raw byte string
989
+ const xidData = (
990
+ subject as unknown as { asByteString(): Uint8Array | undefined }
991
+ ).asByteString();
992
+ if (xidData === undefined) {
993
+ throw XIDError.invalidXid();
994
+ }
995
+ xid = XID.from(xidData);
996
+ }
762
997
  const doc = XIDDocument.fromXid(xid);
763
998
 
764
999
  // Process assertions
@@ -808,6 +1043,12 @@ export class XIDDocument implements EnvelopeEncodable {
808
1043
  doc._provenance = Provenance.tryFromEnvelope(object, password);
809
1044
  break;
810
1045
  }
1046
+ case ATTACHMENT_RAW_VALUE:
1047
+ // Handled separately by Attachments.fromEnvelope()
1048
+ break;
1049
+ case EDGE_RAW_VALUE:
1050
+ // Handled separately by Edges.fromEnvelope()
1051
+ break;
811
1052
  default:
812
1053
  throw XIDError.unexpectedPredicate(String(predicate));
813
1054
  }
@@ -821,7 +1062,17 @@ export class XIDDocument implements EnvelopeEncodable {
821
1062
  * Create a signed envelope.
822
1063
  */
823
1064
  toSignedEnvelope(signingKey: Signer): Envelope {
824
- const envelope = this.toEnvelope(XIDPrivateKeyOptions.Omit, XIDGeneratorOptions.Omit, {
1065
+ return this.toSignedEnvelopeOpt(signingKey, XIDPrivateKeyOptions.Omit);
1066
+ }
1067
+
1068
+ /**
1069
+ * Create a signed envelope with private key options.
1070
+ */
1071
+ toSignedEnvelopeOpt(
1072
+ signingKey: Signer,
1073
+ privateKeyOptions: XIDPrivateKeyOptionsValue = XIDPrivateKeyOptions.Omit,
1074
+ ): Envelope {
1075
+ const envelope = this.toEnvelope(privateKeyOptions, XIDGeneratorOptions.Omit, {
825
1076
  type: "none",
826
1077
  });
827
1078
  return (envelope as unknown as { sign(s: Signer): Envelope }).sign(signingKey);
@@ -838,7 +1089,50 @@ export class XIDDocument implements EnvelopeEncodable {
838
1089
  * Check equality with another XIDDocument.
839
1090
  */
840
1091
  equals(other: XIDDocument): boolean {
841
- return this._xid.equals(other._xid);
1092
+ // Match Rust's PartialEq which compares all fields
1093
+ if (!this._xid.equals(other._xid)) return false;
1094
+
1095
+ // Compare resolution methods
1096
+ if (this._resolutionMethods.size !== other._resolutionMethods.size) return false;
1097
+ for (const m of this._resolutionMethods) {
1098
+ if (!other._resolutionMethods.has(m)) return false;
1099
+ }
1100
+
1101
+ // Compare keys
1102
+ if (this._keys.size !== other._keys.size) return false;
1103
+ for (const [hash, key] of this._keys) {
1104
+ const otherKey = other._keys.get(hash);
1105
+ if (otherKey === undefined || !key.equals(otherKey)) return false;
1106
+ }
1107
+
1108
+ // Compare delegates
1109
+ if (this._delegates.size !== other._delegates.size) return false;
1110
+ for (const [hash, delegate] of this._delegates) {
1111
+ const otherDelegate = other._delegates.get(hash);
1112
+ if (otherDelegate === undefined || !delegate.equals(otherDelegate)) return false;
1113
+ }
1114
+
1115
+ // Compare services
1116
+ if (this._services.size !== other._services.size) return false;
1117
+ for (const [uri, service] of this._services) {
1118
+ const otherService = other._services.get(uri);
1119
+ if (otherService === undefined || !service.equals(otherService)) return false;
1120
+ }
1121
+
1122
+ // Compare provenance
1123
+ if (this._provenance === undefined && other._provenance !== undefined) return false;
1124
+ if (this._provenance !== undefined && other._provenance === undefined) return false;
1125
+ if (this._provenance !== undefined && other._provenance !== undefined) {
1126
+ if (!this._provenance.equals(other._provenance)) return false;
1127
+ }
1128
+
1129
+ // Compare attachments
1130
+ if (!this._attachments.equals(other._attachments)) return false;
1131
+
1132
+ // Compare edges
1133
+ if (!this._edges.equals(other._edges)) return false;
1134
+
1135
+ return true;
842
1136
  }
843
1137
 
844
1138
  /**
@@ -853,6 +1147,17 @@ export class XIDDocument implements EnvelopeEncodable {
853
1147
  new Map(Array.from(this._services.entries()).map(([k, v]) => [k, v.clone()])),
854
1148
  this._provenance?.clone(),
855
1149
  );
1150
+
1151
+ // Clone attachments by iterating and re-adding
1152
+ for (const [, env] of this._attachments.iter()) {
1153
+ doc._attachments.addEnvelope(env);
1154
+ }
1155
+
1156
+ // Clone edges by iterating and re-adding
1157
+ for (const [, env] of this._edges.iter()) {
1158
+ doc._edges.add(env);
1159
+ }
1160
+
856
1161
  return doc;
857
1162
  }
858
1163