@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bcts/xid",
3
- "version": "1.0.0-alpha.17",
3
+ "version": "1.0.0-alpha.18",
4
4
  "type": "module",
5
5
  "description": "Blockchain Commons XID for TypeScript",
6
6
  "license": "BSD-2-Clause-Patent",
@@ -68,11 +68,11 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@bcts/eslint": "^0.1.0",
71
- "@bcts/rand": "^1.0.0-alpha.17",
71
+ "@bcts/rand": "^1.0.0-alpha.18",
72
72
  "@bcts/tsconfig": "^0.1.0",
73
73
  "@eslint/js": "^9.39.2",
74
- "@typescript-eslint/eslint-plugin": "^8.53.1",
75
- "@typescript-eslint/parser": "^8.53.1",
74
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
75
+ "@typescript-eslint/parser": "^8.54.0",
76
76
  "eslint": "^9.39.2",
77
77
  "ts-node": "^10.9.2",
78
78
  "tsdown": "^0.20.1",
@@ -81,10 +81,10 @@
81
81
  "vitest": "^4.0.18"
82
82
  },
83
83
  "dependencies": {
84
- "@bcts/components": "^1.0.0-alpha.17",
85
- "@bcts/dcbor": "^1.0.0-alpha.17",
86
- "@bcts/envelope": "^1.0.0-alpha.17",
87
- "@bcts/known-values": "^1.0.0-alpha.17",
88
- "@bcts/provenance-mark": "^1.0.0-alpha.17"
84
+ "@bcts/components": "^1.0.0-alpha.18",
85
+ "@bcts/dcbor": "^1.0.0-alpha.18",
86
+ "@bcts/envelope": "^1.0.0-alpha.18",
87
+ "@bcts/known-values": "^1.0.0-alpha.18",
88
+ "@bcts/provenance-mark": "^1.0.0-alpha.18"
89
89
  }
90
90
  }
package/src/index.ts CHANGED
@@ -64,5 +64,8 @@ export {
64
64
  XIDVerifySignature,
65
65
  } from "./xid-document";
66
66
 
67
+ // Re-export Attachments and Edges from envelope for convenience
68
+ export { Attachments, Edges, type Edgeable } from "@bcts/envelope";
69
+
67
70
  // Version information
68
71
  export const VERSION = "1.0.0-alpha.3";
package/src/key.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  import { Envelope, type EnvelopeEncodable } from "@bcts/envelope";
11
11
  import { ENDPOINT, NICKNAME, PRIVATE_KEY, SALT, type KnownValue } from "@bcts/known-values";
12
12
  import type { EnvelopeEncodableValue } from "@bcts/envelope";
13
+ import { type Cbor } from "@bcts/dcbor";
13
14
 
14
15
  // Helper to convert KnownValue to EnvelopeEncodableValue
15
16
  const kv = (v: KnownValue): EnvelopeEncodableValue => v as unknown as EnvelopeEncodableValue;
@@ -19,8 +20,12 @@ import {
19
20
  PublicKeys,
20
21
  PrivateKeys,
21
22
  type PrivateKeyBase,
23
+ type SigningPublicKey,
24
+ type EncapsulationPublicKey,
22
25
  type Verifier,
23
26
  type Signature,
27
+ type KeyDerivationMethod,
28
+ defaultKeyDerivationMethod,
24
29
  } from "@bcts/components";
25
30
  import { Permissions, type HasPermissions } from "./permissions";
26
31
  import { type Privilege } from "./privilege";
@@ -47,6 +52,7 @@ export enum XIDPrivateKeyOptions {
47
52
  export interface XIDPrivateKeyEncryptConfig {
48
53
  type: XIDPrivateKeyOptions.Encrypt;
49
54
  password: Uint8Array;
55
+ method?: KeyDerivationMethod;
50
56
  }
51
57
 
52
58
  /**
@@ -121,8 +127,8 @@ export class Key implements HasNickname, HasPermissions, EnvelopeEncodable, Veri
121
127
  * Create a new Key with private key base (derives keys from it).
122
128
  */
123
129
  static newWithPrivateKeyBase(privateKeyBase: PrivateKeyBase): Key {
124
- const privateKeys = privateKeyBase.ed25519PrivateKeys();
125
- const publicKeys = privateKeyBase.ed25519PublicKeys();
130
+ const privateKeys = privateKeyBase.schnorrPrivateKeys();
131
+ const publicKeys = privateKeyBase.schnorrPublicKeys();
126
132
  return Key.newWithPrivateKeys(privateKeys, publicKeys);
127
133
  }
128
134
 
@@ -172,6 +178,20 @@ export class Key implements HasNickname, HasPermissions, EnvelopeEncodable, Veri
172
178
  return this._publicKeys.reference();
173
179
  }
174
180
 
181
+ /**
182
+ * Get the signing public key.
183
+ */
184
+ signingPublicKey(): SigningPublicKey {
185
+ return this._publicKeys.signingPublicKey();
186
+ }
187
+
188
+ /**
189
+ * Get the encapsulation public key.
190
+ */
191
+ encapsulationPublicKey(): EncapsulationPublicKey {
192
+ return this._publicKeys.encapsulationPublicKey();
193
+ }
194
+
175
195
  // ============================================================================
176
196
  // Verifier Interface
177
197
  // ============================================================================
@@ -271,9 +291,13 @@ export class Key implements HasNickname, HasPermissions, EnvelopeEncodable, Veri
271
291
  case XIDPrivateKeyOptions.Encrypt: {
272
292
  if (typeof privateKeyOptions === "object") {
273
293
  const privateKeysEnvelope = Envelope.new(data.privateKeys.taggedCborData());
294
+ const method: KeyDerivationMethod =
295
+ privateKeyOptions.method ?? defaultKeyDerivationMethod();
274
296
  const encrypted = (
275
- privateKeysEnvelope as unknown as { encryptSubject(p: Uint8Array): Envelope }
276
- ).encryptSubject(privateKeyOptions.password);
297
+ privateKeysEnvelope as unknown as {
298
+ lockSubject(m: KeyDerivationMethod, p: Uint8Array): Envelope;
299
+ }
300
+ ).lockSubject(method, privateKeyOptions.password);
277
301
  envelope = envelope.addAssertion(kv(PRIVATE_KEY), encrypted);
278
302
  envelope = envelope.addAssertion(kv(SALT), salt.toData());
279
303
  }
@@ -316,19 +340,29 @@ export class Key implements HasNickname, HasPermissions, EnvelopeEncodable, Veri
316
340
  asByteString(): Uint8Array | undefined;
317
341
  subject(): Envelope;
318
342
  assertionsWithPredicate(p: unknown): Envelope[];
319
- decryptSubject(p: Uint8Array): Envelope;
343
+ unlockSubject(p: Uint8Array): Envelope;
344
+ isLockedWithPassword(): boolean;
320
345
  };
321
346
  const env = envelope as EnvelopeExt;
322
347
 
323
- // Extract PublicKeys from subject (stored as tagged CBOR)
324
- // The envelope may be a node (with assertions) or a leaf
348
+ // Extract PublicKeys from subject.
349
+ // Rust-generated documents store PublicKeys as tagged CBOR directly in the leaf.
350
+ // TS-generated documents may store the tagged CBOR binary inside a byte string.
325
351
  const envCase = env.case();
326
352
  const subject = envCase.type === "node" ? env.subject() : env;
353
+ let publicKeys: PublicKeys;
327
354
  const publicKeysData = (subject as EnvelopeExt).asByteString();
328
- if (publicKeysData === undefined) {
329
- throw XIDError.component(new Error("Could not extract public keys from envelope"));
355
+ if (publicKeysData !== undefined) {
356
+ // TS format: tagged CBOR binary stored as a byte string
357
+ publicKeys = PublicKeys.fromTaggedCborData(publicKeysData);
358
+ } else {
359
+ // Rust format: tagged CBOR stored directly as the leaf CBOR value
360
+ const leaf = (subject as unknown as { asLeaf(): Cbor | undefined }).asLeaf?.();
361
+ if (leaf === undefined) {
362
+ throw XIDError.component(new Error("Could not extract public keys from envelope"));
363
+ }
364
+ publicKeys = PublicKeys.fromTaggedCbor(leaf);
330
365
  }
331
- const publicKeys = PublicKeys.fromTaggedCborData(publicKeysData);
332
366
 
333
367
  // Extract optional private key
334
368
  let privateKeyData: { data: PrivateKeyData; salt: Salt } | undefined;
@@ -356,13 +390,12 @@ export class Key implements HasNickname, HasPermissions, EnvelopeEncodable, Veri
356
390
  if (assertionCase.type === "assertion") {
357
391
  const privateKeyObject = assertionCase.assertion.object() as EnvelopeExt;
358
392
 
359
- // Check if encrypted
360
- const objCase = privateKeyObject.case();
361
- if (objCase.type === "encrypted") {
393
+ // Check if locked with password (uses hasSecret assertion with EncryptedKey)
394
+ if (privateKeyObject.isLockedWithPassword()) {
362
395
  if (password !== undefined) {
363
396
  try {
364
- const decrypted = privateKeyObject.decryptSubject(password) as EnvelopeExt;
365
- const decryptedData = decrypted.asByteString();
397
+ const decrypted = privateKeyObject.unlockSubject(password) as EnvelopeExt;
398
+ const decryptedData = (decrypted.subject() as EnvelopeExt).asByteString();
366
399
  if (decryptedData !== undefined) {
367
400
  // Parse PrivateKeys from tagged CBOR
368
401
  const privateKeys = PrivateKeys.fromTaggedCborData(decryptedData);
@@ -431,6 +464,39 @@ export class Key implements HasNickname, HasPermissions, EnvelopeEncodable, Veri
431
464
  return new Key(publicKeys, privateKeyData, nickname, endpoints, permissions);
432
465
  }
433
466
 
467
+ /**
468
+ * Get the private key envelope, optionally decrypting it.
469
+ *
470
+ * Returns:
471
+ * - undefined if no private keys
472
+ * - The decrypted private key envelope if unencrypted
473
+ * - The decrypted envelope if encrypted + correct password
474
+ * - The encrypted envelope as-is if encrypted + no password
475
+ * - Throws on wrong password
476
+ */
477
+ privateKeyEnvelope(password?: string): Envelope | undefined {
478
+ if (this._privateKeyData === undefined) {
479
+ return undefined;
480
+ }
481
+ const { data } = this._privateKeyData;
482
+ if (data.type === "decrypted") {
483
+ return Envelope.new(data.privateKeys.taggedCborData());
484
+ }
485
+ // Encrypted case
486
+ if (password !== undefined) {
487
+ try {
488
+ const decrypted = (
489
+ data.envelope as unknown as { unlockSubject(p: Uint8Array): Envelope }
490
+ ).unlockSubject(new TextEncoder().encode(password));
491
+ return decrypted;
492
+ } catch {
493
+ throw XIDError.invalidPassword();
494
+ }
495
+ }
496
+ // No password — return encrypted envelope as-is
497
+ return data.envelope;
498
+ }
499
+
434
500
  /**
435
501
  * Check equality with another Key.
436
502
  */
package/src/provenance.ts CHANGED
@@ -9,14 +9,21 @@
9
9
 
10
10
  import { Envelope, type EnvelopeEncodable, type EnvelopeEncodableValue } from "@bcts/envelope";
11
11
  import { PROVENANCE_GENERATOR, SALT, type KnownValue } from "@bcts/known-values";
12
- import { Salt } from "@bcts/components";
12
+ import { Salt, type KeyDerivationMethod, defaultKeyDerivationMethod } from "@bcts/components";
13
13
 
14
14
  // Helper to convert KnownValue to EnvelopeEncodableValue
15
15
  const kv = (v: KnownValue): EnvelopeEncodableValue => v as unknown as EnvelopeEncodableValue;
16
16
  import { ProvenanceMark, ProvenanceMarkGenerator } from "@bcts/provenance-mark";
17
- import { cborData, decodeCbor } from "@bcts/dcbor";
18
17
  import { XIDError } from "./error";
19
18
 
19
+ // Encode generator JSON as bytes for storage in envelope
20
+ const encodeGeneratorJSON = (json: Record<string, unknown>): Uint8Array =>
21
+ new TextEncoder().encode(JSON.stringify(json));
22
+
23
+ // Decode generator JSON from bytes stored in envelope
24
+ const decodeGeneratorJSON = (data: Uint8Array): Record<string, unknown> =>
25
+ JSON.parse(new TextDecoder().decode(data)) as Record<string, unknown>;
26
+
20
27
  /**
21
28
  * Options for handling generators in envelopes.
22
29
  */
@@ -37,6 +44,7 @@ export enum XIDGeneratorOptions {
37
44
  export interface XIDGeneratorEncryptConfig {
38
45
  type: XIDGeneratorOptions.Encrypt;
39
46
  password: Uint8Array;
47
+ method?: KeyDerivationMethod;
40
48
  }
41
49
 
42
50
  /**
@@ -150,7 +158,8 @@ export class Provenance implements EnvelopeEncodable {
150
158
  */
151
159
  generatorMut(password?: Uint8Array): ProvenanceMarkGenerator | undefined {
152
160
  type EnvelopeExt = Envelope & {
153
- decryptSubject(p: Uint8Array): Envelope;
161
+ unlockSubject(p: Uint8Array): Envelope;
162
+ subject(): Envelope;
154
163
  tryUnwrap(): Envelope;
155
164
  asByteString(): Uint8Array | undefined;
156
165
  };
@@ -165,12 +174,12 @@ export class Provenance implements EnvelopeEncodable {
165
174
  if (password !== undefined) {
166
175
  const encryptedEnvelope = this._generator.data.envelope as EnvelopeExt;
167
176
  try {
168
- const decrypted = encryptedEnvelope.decryptSubject(password) as EnvelopeExt;
169
- const unwrapped = decrypted.tryUnwrap() as EnvelopeExt;
177
+ const decrypted = encryptedEnvelope.unlockSubject(password) as EnvelopeExt;
178
+ const unwrapped = (decrypted.subject() as EnvelopeExt).tryUnwrap() as EnvelopeExt;
170
179
  // Extract generator from unwrapped envelope
171
180
  const generatorData = unwrapped.asByteString();
172
181
  if (generatorData !== undefined) {
173
- const json = decodeCbor(generatorData) as unknown as Record<string, unknown>;
182
+ const json = decodeGeneratorJSON(generatorData);
174
183
  const generator = ProvenanceMarkGenerator.fromJSON(json);
175
184
  // Replace encrypted with decrypted
176
185
  this._generator = {
@@ -187,6 +196,47 @@ export class Provenance implements EnvelopeEncodable {
187
196
  throw XIDError.invalidPassword();
188
197
  }
189
198
 
199
+ /**
200
+ * Get the generator envelope, optionally decrypting it.
201
+ *
202
+ * Returns:
203
+ * - undefined if no generator
204
+ * - An envelope containing the generator if unencrypted
205
+ * - The decrypted envelope if encrypted + correct password
206
+ * - The encrypted envelope as-is if encrypted + no password
207
+ * - Throws on wrong password
208
+ */
209
+ generatorEnvelope(password?: string): Envelope | undefined {
210
+ type EnvelopeExt = Envelope & {
211
+ unlockSubject(p: Uint8Array): Envelope;
212
+ subject(): Envelope;
213
+ tryUnwrap(): Envelope;
214
+ };
215
+
216
+ if (this._generator === undefined) {
217
+ return undefined;
218
+ }
219
+ const { data } = this._generator;
220
+ if (data.type === "decrypted") {
221
+ const generatorBytes = encodeGeneratorJSON(data.generator.toJSON());
222
+ return Envelope.new(generatorBytes);
223
+ }
224
+ // Encrypted case
225
+ if (password !== undefined) {
226
+ try {
227
+ const decrypted = (data.envelope as EnvelopeExt).unlockSubject(
228
+ new TextEncoder().encode(password),
229
+ ) as EnvelopeExt;
230
+ const unwrapped = (decrypted.subject() as EnvelopeExt).tryUnwrap();
231
+ return unwrapped;
232
+ } catch {
233
+ throw XIDError.invalidPassword();
234
+ }
235
+ }
236
+ // No password — return encrypted envelope as-is
237
+ return data.envelope;
238
+ }
239
+
190
240
  /**
191
241
  * Convert to envelope with specified options.
192
242
  */
@@ -194,7 +244,7 @@ export class Provenance implements EnvelopeEncodable {
194
244
  type EnvelopeExt = Envelope & {
195
245
  elide(): Envelope;
196
246
  wrap(): Envelope;
197
- encryptSubject(p: Uint8Array): Envelope;
247
+ lockSubject(m: KeyDerivationMethod, p: Uint8Array): Envelope;
198
248
  };
199
249
 
200
250
  // Create envelope with the mark as subject
@@ -215,13 +265,13 @@ export class Provenance implements EnvelopeEncodable {
215
265
 
216
266
  switch (option) {
217
267
  case XIDGeneratorOptions.Include: {
218
- const generatorBytes = cborData(data.generator.toJSON());
268
+ const generatorBytes = encodeGeneratorJSON(data.generator.toJSON());
219
269
  envelope = envelope.addAssertion(kv(PROVENANCE_GENERATOR), generatorBytes);
220
270
  envelope = envelope.addAssertion(kv(SALT), salt.toData());
221
271
  break;
222
272
  }
223
273
  case XIDGeneratorOptions.Elide: {
224
- const generatorBytes2 = cborData(data.generator.toJSON());
274
+ const generatorBytes2 = encodeGeneratorJSON(data.generator.toJSON());
225
275
  const baseAssertion = Envelope.newAssertion(kv(PROVENANCE_GENERATOR), generatorBytes2);
226
276
  const elidedAssertion = (baseAssertion as EnvelopeExt).elide();
227
277
  envelope = envelope.addAssertionEnvelope(elidedAssertion);
@@ -230,10 +280,15 @@ export class Provenance implements EnvelopeEncodable {
230
280
  }
231
281
  case XIDGeneratorOptions.Encrypt: {
232
282
  if (typeof generatorOptions === "object") {
233
- const generatorBytes3 = cborData(data.generator.toJSON());
283
+ const generatorBytes3 = encodeGeneratorJSON(data.generator.toJSON());
234
284
  const generatorEnvelope = Envelope.new(generatorBytes3) as EnvelopeExt;
235
285
  const wrapped = generatorEnvelope.wrap() as EnvelopeExt;
236
- const encrypted = wrapped.encryptSubject(generatorOptions.password);
286
+ const method: KeyDerivationMethod =
287
+ generatorOptions.method ?? defaultKeyDerivationMethod();
288
+ const encrypted = (wrapped as unknown as EnvelopeExt).lockSubject(
289
+ method,
290
+ generatorOptions.password,
291
+ );
237
292
  envelope = envelope.addAssertion(kv(PROVENANCE_GENERATOR), encrypted);
238
293
  envelope = envelope.addAssertion(kv(SALT), salt.toData());
239
294
  }
@@ -261,8 +316,10 @@ export class Provenance implements EnvelopeEncodable {
261
316
  static tryFromEnvelope(envelope: Envelope, password?: Uint8Array): Provenance {
262
317
  type EnvelopeExt = Envelope & {
263
318
  asByteString(): Uint8Array | undefined;
319
+ subject(): Envelope;
264
320
  assertionsWithPredicate(p: unknown): Envelope[];
265
- decryptSubject(p: Uint8Array): Envelope;
321
+ unlockSubject(p: Uint8Array): Envelope;
322
+ isLockedWithPassword(): boolean;
266
323
  tryUnwrap(): Envelope;
267
324
  };
268
325
  const env = envelope as EnvelopeExt;
@@ -304,16 +361,15 @@ export class Provenance implements EnvelopeEncodable {
304
361
  if (assertionCase.type === "assertion") {
305
362
  const generatorObject = assertionCase.assertion.object() as EnvelopeExt;
306
363
 
307
- // Check if encrypted
308
- const objCase = generatorObject.case();
309
- if (objCase.type === "encrypted") {
364
+ // Check if locked with password (uses hasSecret assertion with EncryptedKey)
365
+ if (generatorObject.isLockedWithPassword()) {
310
366
  if (password !== undefined) {
311
367
  try {
312
- const decrypted = generatorObject.decryptSubject(password) as EnvelopeExt;
313
- const unwrapped = decrypted.tryUnwrap() as EnvelopeExt;
368
+ const decrypted = generatorObject.unlockSubject(password) as EnvelopeExt;
369
+ const unwrapped = (decrypted.subject() as EnvelopeExt).tryUnwrap() as EnvelopeExt;
314
370
  const generatorData = unwrapped.asByteString();
315
371
  if (generatorData !== undefined) {
316
- const json = decodeCbor(generatorData) as unknown as Record<string, unknown>;
372
+ const json = decodeGeneratorJSON(generatorData);
317
373
  const gen = ProvenanceMarkGenerator.fromJSON(json);
318
374
  generator = {
319
375
  data: { type: "decrypted", generator: gen },
@@ -338,7 +394,7 @@ export class Provenance implements EnvelopeEncodable {
338
394
  // Plain text generator
339
395
  const generatorData = generatorObject.asByteString();
340
396
  if (generatorData !== undefined) {
341
- const json2 = decodeCbor(generatorData) as unknown as Record<string, unknown>;
397
+ const json2 = decodeGeneratorJSON(generatorData);
342
398
  const gen = ProvenanceMarkGenerator.fromJSON(json2);
343
399
  generator = {
344
400
  data: { type: "decrypted", generator: gen },
package/src/service.ts CHANGED
@@ -12,7 +12,7 @@ import { KEY, DELEGATE, NAME, CAPABILITY, ALLOW, type KnownValue } from "@bcts/k
12
12
 
13
13
  // Helper to convert KnownValue to EnvelopeEncodableValue
14
14
  const kv = (v: KnownValue): EnvelopeEncodableValue => v as unknown as EnvelopeEncodableValue;
15
- import type { Reference } from "@bcts/components";
15
+ import { Reference, type PublicKeys, type XID } from "@bcts/components";
16
16
  import { Permissions, type HasPermissions } from "./permissions";
17
17
  import { privilegeFromEnvelope } from "./privilege";
18
18
  import { XIDError } from "./error";
@@ -147,6 +147,22 @@ export class Service implements HasPermissions, EnvelopeEncodable {
147
147
  this.addDelegateReferenceHex(delegateReference.toHex());
148
148
  }
149
149
 
150
+ /**
151
+ * Add a key by its public keys provider (convenience method).
152
+ * Matches Rust's `add_key(&mut self, key: &dyn PublicKeysProvider)`.
153
+ */
154
+ addKey(keyProvider: { publicKeys(): PublicKeys }): void {
155
+ this.addKeyReference(keyProvider.publicKeys().reference());
156
+ }
157
+
158
+ /**
159
+ * Add a delegate by its XID provider (convenience method).
160
+ * Matches Rust's `add_delegate(&mut self, delegate: &dyn XIDProvider)`.
161
+ */
162
+ addDelegate(xidProvider: { xid(): XID }): void {
163
+ this.addDelegateReference(Reference.hash(xidProvider.xid().toData()));
164
+ }
165
+
150
166
  /**
151
167
  * Get the name.
152
168
  */