@smartledger/keys 3.0.0 → 3.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/sdk.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/sdk.ts"],"names":[],"mappings":"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"}
package/dist/esm/sdk.js CHANGED
@@ -4,7 +4,27 @@
4
4
  * Integrates KeyRegistry and SignatureSuiteRegistry to provide
5
5
  * a clean, high-level API for key operations.
6
6
  */
7
- import { KeyRegistry, SignatureSuiteRegistry, hashBytes, BIP32Derivation, InMemoryKeyStorage, BsvEcdsaSuite, MlDsa87Suite, } from '@smartledger/crypto';
7
+ import { KeyRegistry, SignatureSuiteRegistry, hashBytes, assertKeyUsable, isKeyUsable, cloneKeyMeta, agentIdOfKey, KeyAlreadyExistsError, BIP32Derivation, InMemoryKeyStorage, BsvEcdsaSuite, MlDsa44Suite, MlDsa65Suite, MlDsa87Suite, } from '@smartledger/crypto';
8
+ export { agentIdOfKey };
9
+ const COUNTER_SUFFIX = /^pk-[^:-]+-(\d+)$/;
10
+ const MAX_KEY_ID_ATTEMPTS = 8;
11
+ function assertAgentId(agentId) {
12
+ if (typeof agentId !== 'string' || agentId.length === 0) {
13
+ throw new Error('agentId must be a non-empty string');
14
+ }
15
+ }
16
+ function toRecord(meta, publicKey) {
17
+ // Copies, so a caller cannot reach registry state through a KeyRecord even
18
+ // when a custom storage backend hands out its own objects.
19
+ return { meta: cloneKeyMeta(meta), publicKey: Uint8Array.from(publicKey) };
20
+ }
21
+ function statusAllows(meta, policy) {
22
+ if (policy === 'any')
23
+ return true;
24
+ if (policy === 'not-revoked')
25
+ return meta.status !== 'revoked';
26
+ return isKeyUsable(meta);
27
+ }
8
28
  /**
9
29
  * Default KeySDK implementation.
10
30
  *
@@ -15,6 +35,7 @@ export class DefaultKeySDK {
15
35
  this.keyCounter = new Map(); // agentId -> counter
16
36
  this.counterInitialized = new Set();
17
37
  this.counterInitPromises = new Map();
38
+ this.getOrCreateInFlight = new Map();
18
39
  this.keyRegistry = config.keyRegistry;
19
40
  this.suiteRegistry = config.suiteRegistry;
20
41
  }
@@ -22,6 +43,7 @@ export class DefaultKeySDK {
22
43
  * Create a new key for an agent.
23
44
  */
24
45
  async createKey(agentId, profile, options = {}) {
46
+ assertAgentId(agentId);
25
47
  // Get suite
26
48
  const suite = this.suiteRegistry.getSuite(profile.primarySignatureSuite);
27
49
  // Generate keypair (with optional derivation)
@@ -43,20 +65,16 @@ export class DefaultKeySDK {
43
65
  // Standard random key generation
44
66
  keypair = await suite.generateKeypair();
45
67
  }
46
- // Generate unique keyId
47
- const counter = await this.getNextCounter(agentId);
48
- const suiteName = profile.primarySignatureSuite.split('-')[0]; // 'ml-dsa' or 'bsv'
49
- const keyId = `${agentId}:pk-${suiteName}-${counter}`;
50
- // Register key
51
- const meta = await this.keyRegistry.registerKey(keyId, keypair, profile.primarySignatureSuite, {
68
+ const meta = await this.registerUnderFreshId(agentId, profile.primarySignatureSuite, keyId => this.keyRegistry.registerKey(keyId, keypair, profile.primarySignatureSuite, {
52
69
  expiresAt: options.expiresAt,
53
70
  usage: options.usage || ['signing'],
54
71
  cryptoProfileVersion: options.cryptoProfileVersion || '1.0.0',
55
- });
56
- return {
57
- meta,
58
- publicKey: keypair.publicKey,
59
- };
72
+ // Record that (and where) the key was derived. Never the seed.
73
+ derivation: options.derivationContext
74
+ ? { path: options.derivationContext.path ?? '' }
75
+ : undefined,
76
+ }));
77
+ return toRecord(meta, keypair.publicKey);
60
78
  }
61
79
  /**
62
80
  * Get an existing key.
@@ -66,13 +84,13 @@ export class DefaultKeySDK {
66
84
  if (!result) {
67
85
  return null;
68
86
  }
69
- return {
70
- meta: result.meta,
71
- publicKey: result.publicKey,
72
- };
87
+ return toRecord(result.meta, result.publicKey);
73
88
  }
74
89
  /**
75
90
  * Sign with a key (private key never exposed).
91
+ *
92
+ * @throws if the key is unknown, not active, past its `expiresAt`, or not
93
+ * registered for signing
76
94
  */
77
95
  async signWithKey(keyId, message) {
78
96
  // Get full keypair (private access)
@@ -80,9 +98,15 @@ export class DefaultKeySDK {
80
98
  if (!result) {
81
99
  throw new Error(`Key not found: ${keyId}`);
82
100
  }
83
- if (result.meta.status !== 'active') {
101
+ if (result.meta.status && result.meta.status !== 'active') {
84
102
  throw new Error(`Key is not active: ${keyId} (status: ${result.meta.status})`);
85
103
  }
104
+ // Status alone is not enough: an expired key keeps status 'active' until
105
+ // markExpiredKeys() runs, and used to go on signing in the meantime.
106
+ assertKeyUsable(result.meta);
107
+ if (!result.meta.usage.some(u => u === 'signing' || u === 'both')) {
108
+ throw new Error(`Key is not registered for signing: ${keyId} (usage: ${result.meta.usage.join(', ')})`);
109
+ }
86
110
  // Get suite and sign
87
111
  const suite = this.suiteRegistry.getSuite(result.meta.suiteId);
88
112
  // Hash the message (consistent with signAgentResponse)
@@ -92,18 +116,29 @@ export class DefaultKeySDK {
92
116
  }
93
117
  /**
94
118
  * Verify a signature.
119
+ *
120
+ * By default this is the cryptographic check only: a signature made by a
121
+ * key that has since been rotated, expired or revoked still verifies, which
122
+ * is what verifying a historical signature needs. Pass `keyStatus` to also
123
+ * require something of the key's state now.
95
124
  */
96
- async verifySignature(keyId, message, signature) {
125
+ async verifySignature(keyId, message, signature, options = {}) {
97
126
  const result = await this.keyRegistry.getPublicKey(keyId);
98
127
  if (!result) {
99
128
  return false;
100
129
  }
101
- const suite = this.suiteRegistry.getSuite(result.meta.suiteId);
102
- // Hash the message
103
- const hashAlg = this.getHashAlgorithm(result.meta.suiteId);
104
- const messageHash = await hashBytes(message, hashAlg);
130
+ if (!statusAllows(result.meta, options.keyStatus ?? 'any')) {
131
+ return false;
132
+ }
133
+ return this.verifyAgainst(result.meta, result.publicKey, message, signature);
134
+ }
135
+ /** The cryptographic check alone. Anything but a strict `true` is a failure. */
136
+ async verifyAgainst(meta, publicKey, message, signature) {
105
137
  try {
106
- return await suite.verify(result.publicKey, messageHash, signature);
138
+ const suite = this.suiteRegistry.getSuite(meta.suiteId);
139
+ const hashAlg = this.getHashAlgorithm(meta.suiteId);
140
+ const messageHash = await hashBytes(message, hashAlg);
141
+ return (await suite.verify(publicKey, messageHash, signature)) === true;
107
142
  }
108
143
  catch {
109
144
  return false;
@@ -113,20 +148,18 @@ export class DefaultKeySDK {
113
148
  * List keys for an agent.
114
149
  */
115
150
  async listKeysForAgent(agentId, activeOnly = true) {
151
+ assertAgentId(agentId);
116
152
  const keys = activeOnly
117
153
  ? await this.keyRegistry.listActiveKeys()
118
154
  : await this.keyRegistry.listKeys();
119
- // Filter by agentId prefix
120
- const agentKeys = keys.filter(meta => meta.keyId.startsWith(`${agentId}:`));
155
+ // Exact ownership, not a prefix match (see agentIdOfKey)
156
+ const agentKeys = keys.filter(meta => agentIdOfKey(meta.keyId) === agentId);
121
157
  // Convert to KeyRecords
122
158
  const records = [];
123
159
  for (const meta of agentKeys) {
124
160
  const result = await this.keyRegistry.getPublicKey(meta.keyId);
125
161
  if (result) {
126
- records.push({
127
- meta: result.meta,
128
- publicKey: result.publicKey,
129
- });
162
+ records.push(toRecord(result.meta, result.publicKey));
130
163
  }
131
164
  }
132
165
  return records;
@@ -151,39 +184,87 @@ export class DefaultKeySDK {
151
184
  }
152
185
  /**
153
186
  * Rotate all keys for an agent.
187
+ *
188
+ * Each replacement gets its predecessor's lifetime again, counted from now
189
+ * (or `options.expiresAt`); it does not inherit the old expiry instant.
190
+ *
191
+ * A key derived from a seed cannot be re-derived here, because the SDK does
192
+ * not keep seeds. By default the call refuses, before changing anything,
193
+ * rather than silently swapping a recoverable key for a random one. See
194
+ * {@link RotateAgentKeysOptions.derivedKeys}.
154
195
  */
155
- async rotateAgentKeys(agentId) {
196
+ async rotateAgentKeys(agentId, options = {}) {
197
+ const derivedPolicy = options.derivedKeys ?? 'error';
156
198
  const activeKeys = await this.listKeysForAgent(agentId, true);
157
- const newKeys = [];
199
+ // Decide everything before changing anything.
200
+ const toRotate = [];
158
201
  for (const keyRecord of activeKeys) {
159
- // Generate new keypair
202
+ // Resolves the suite now, so an unknown suite fails before any rotation.
203
+ this.suiteRegistry.getSuite(keyRecord.meta.suiteId);
204
+ if (keyRecord.meta.derivation) {
205
+ if (derivedPolicy === 'error') {
206
+ throw new Error(`Key ${keyRecord.meta.keyId} was derived from a seed ` +
207
+ `(path ${keyRecord.meta.derivation.path || 'unknown'}). Rotating it here would ` +
208
+ `replace it with a random key that the mnemonic cannot recover. Derive the ` +
209
+ `successor yourself with createKeyFromMnemonic at a new path, or pass ` +
210
+ `{ derivedKeys: 'skip' } or { derivedKeys: 'randomize' }. No keys were rotated.`);
211
+ }
212
+ if (derivedPolicy === 'skip')
213
+ continue;
214
+ }
215
+ toRotate.push(keyRecord);
216
+ }
217
+ // Generate every replacement before committing any, so a failing suite
218
+ // cannot leave the agent half rotated. The commits below are then one
219
+ // registry rotation per key; they are not a single transaction, so a
220
+ // storage failure part-way through leaves the earlier keys rotated.
221
+ const replacements = [];
222
+ for (const keyRecord of toRotate) {
160
223
  const suite = this.suiteRegistry.getSuite(keyRecord.meta.suiteId);
161
- const newKeypair = await suite.generateKeypair();
162
- // Generate new keyId (increment counter)
163
- const counter = await this.getNextCounter(agentId);
164
- const suiteName = keyRecord.meta.suiteId.split('-')[0];
165
- const newKeyId = `${agentId}:pk-${suiteName}-${counter}`;
166
- // Rotate
167
- const newMeta = await this.keyRegistry.rotateKey(keyRecord.meta.keyId, newKeyId, newKeypair);
168
- newKeys.push({
169
- meta: newMeta,
170
- publicKey: newKeypair.publicKey,
171
- });
224
+ replacements.push(await suite.generateKeypair());
225
+ }
226
+ const newKeys = [];
227
+ for (const [i, keyRecord] of toRotate.entries()) {
228
+ const newKeypair = replacements[i];
229
+ const newMeta = await this.registerUnderFreshId(agentId, keyRecord.meta.suiteId, newKeyId => this.keyRegistry.rotateKey(keyRecord.meta.keyId, newKeyId, newKeypair, {
230
+ expiresAt: options.expiresAt,
231
+ }));
232
+ newKeys.push(toRecord(newMeta, newKeypair.publicKey));
172
233
  }
173
234
  return newKeys;
174
235
  }
175
236
  /**
176
237
  * Get or create key (idempotent).
238
+ *
239
+ * Concurrent calls on this SDK instance for the same agent and suite share
240
+ * one creation. Two SDK instances (or processes) on one storage backend can
241
+ * still each create a key; that needs a lock in the backend.
177
242
  */
178
243
  async getOrCreateKey(agentId, profile, options = {}) {
179
- // Check if key exists
180
- const existingKeys = await this.listKeysForAgent(agentId, true);
181
- const primaryKey = existingKeys.find(k => k.meta.suiteId === profile.primarySignatureSuite);
182
- if (primaryKey) {
183
- return primaryKey;
184
- }
185
- // Create new key
186
- return await this.createKey(agentId, profile, options);
244
+ assertAgentId(agentId);
245
+ const slot = JSON.stringify([agentId, profile.primarySignatureSuite]);
246
+ const pending = this.getOrCreateInFlight.get(slot);
247
+ if (pending) {
248
+ const record = await pending;
249
+ return toRecord(record.meta, record.publicKey);
250
+ }
251
+ const work = (async () => {
252
+ // Check if key exists
253
+ const existingKeys = await this.listKeysForAgent(agentId, true);
254
+ const primaryKey = existingKeys.find(k => k.meta.suiteId === profile.primarySignatureSuite);
255
+ if (primaryKey) {
256
+ return primaryKey;
257
+ }
258
+ // Create new key
259
+ return await this.createKey(agentId, profile, options);
260
+ })();
261
+ this.getOrCreateInFlight.set(slot, work);
262
+ try {
263
+ return await work;
264
+ }
265
+ finally {
266
+ this.getOrCreateInFlight.delete(slot);
267
+ }
187
268
  }
188
269
  /**
189
270
  * Create both primary and secondary keys for an agent.
@@ -192,6 +273,7 @@ export class DefaultKeySDK {
192
273
  if (!profile.secondarySignatureSuite) {
193
274
  throw new Error('secondarySignatureSuite is required to create dual signature keys');
194
275
  }
276
+ this.assertDualProfile(profile);
195
277
  const primaryKey = await this.createKey(agentId, {
196
278
  primarySignatureSuite: profile.primarySignatureSuite,
197
279
  }, options);
@@ -207,6 +289,7 @@ export class DefaultKeySDK {
207
289
  if (!profile.secondarySignatureSuite) {
208
290
  throw new Error('secondarySignatureSuite is required to create dual signature keys');
209
291
  }
292
+ this.assertDualProfile(profile);
210
293
  const primaryKey = await this.getOrCreateKey(agentId, {
211
294
  primarySignatureSuite: profile.primarySignatureSuite,
212
295
  }, options);
@@ -215,6 +298,18 @@ export class DefaultKeySDK {
215
298
  }, options);
216
299
  return { primaryKey, secondaryKey };
217
300
  }
301
+ /**
302
+ * Fail before creating either key if the pair could not be a hybrid pair:
303
+ * an unknown suite, or the same suite twice (two ECDSA keys are not a
304
+ * classical + post-quantum pair, whatever the profile calls them).
305
+ */
306
+ assertDualProfile(profile) {
307
+ this.suiteRegistry.getSuite(profile.primarySignatureSuite);
308
+ this.suiteRegistry.getSuite(profile.secondarySignatureSuite);
309
+ if (profile.primarySignatureSuite === profile.secondarySignatureSuite) {
310
+ throw new Error(`Dual signature keys need two different suites; both are ${profile.primarySignatureSuite}`);
311
+ }
312
+ }
218
313
  /**
219
314
  * Sign with one or more suites for an agent.
220
315
  */
@@ -223,7 +318,7 @@ export class DefaultKeySDK {
223
318
  if (activeKeys.length === 0) {
224
319
  throw new Error(`No active keys found for agent: ${agentId}`);
225
320
  }
226
- const suitesToUse = suites?.length ? suites : Array.from(new Set(activeKeys.map(k => k.meta.suiteId)));
321
+ const suitesToUse = Array.from(new Set(suites?.length ? suites : activeKeys.map(k => k.meta.suiteId)));
227
322
  const signatures = [];
228
323
  for (const suiteId of suitesToUse) {
229
324
  const key = activeKeys.find(k => k.meta.suiteId === suiteId);
@@ -236,27 +331,92 @@ export class DefaultKeySDK {
236
331
  return { signatures };
237
332
  }
238
333
  /**
239
- * Verify multiple signatures for an agent.
334
+ * Verify a set of signatures as a hybrid (multi-suite) signature by `agentId`.
335
+ *
336
+ * `allValid` is true only when ALL of these hold:
337
+ * - at least one signature was supplied;
338
+ * - every supplied signature is by a key that belongs to `agentId`, names
339
+ * that key's real suite, passes the key-status policy, and verifies;
340
+ * - every REQUIRED suite has such a signature.
341
+ *
342
+ * The required suites come from the verifier: `options.requiredSuites`, or
343
+ * by default the suites of the agent's currently usable keys in this SDK's
344
+ * registry. They are never read from `signatures`. The check walks the
345
+ * required suites and looks each one up among the valid signatures, so an
346
+ * empty list, or one that merely omits the post-quantum signature, is
347
+ * refused instead of passing because everything in it happens to be valid.
240
348
  */
241
- async verifyWithSuites(agentId, message, signatures) {
349
+ async verifyWithSuites(agentId, message, signatures, options = {}) {
350
+ assertAgentId(agentId);
351
+ const keyStatus = options.keyStatus ?? 'not-revoked';
352
+ const requiredSuites = Array.from(new Set(options.requiredSuites ??
353
+ (await this.listKeysForAgent(agentId, true)).map(k => k.meta.suiteId)));
354
+ const refuse = (error, results = []) => ({
355
+ results,
356
+ allValid: false,
357
+ requiredSuites,
358
+ missingSuites: requiredSuites.filter(s => !results.some(r => r.valid && r.suiteId === s)),
359
+ error,
360
+ });
361
+ if (!Array.isArray(signatures)) {
362
+ return refuse('signatures must be an array');
363
+ }
242
364
  const results = [];
243
365
  for (const entry of signatures) {
366
+ const suiteId = String(entry?.suiteId);
367
+ const keyId = String(entry?.keyId);
368
+ const fail = (error) => results.push({ suiteId, keyId, valid: false, error });
244
369
  try {
245
- const valid = await this.verifySignature(entry.keyId, message, entry.signature);
246
- results.push({ suiteId: entry.suiteId, keyId: entry.keyId, valid });
370
+ if (agentIdOfKey(keyId) !== agentId) {
371
+ fail(`Key does not belong to agent '${agentId}'`);
372
+ continue;
373
+ }
374
+ const key = await this.keyRegistry.getPublicKey(keyId);
375
+ if (!key) {
376
+ fail('Key not found');
377
+ continue;
378
+ }
379
+ if (key.meta.suiteId !== suiteId) {
380
+ fail(`Suite mismatch: key is ${key.meta.suiteId}, signature claims ${suiteId}`);
381
+ continue;
382
+ }
383
+ if (!statusAllows(key.meta, keyStatus)) {
384
+ fail(`Key status not accepted (status: ${key.meta.status ?? 'active'}, policy: ${keyStatus})`);
385
+ continue;
386
+ }
387
+ if (!(entry.signature instanceof Uint8Array)) {
388
+ fail('Signature must be a Uint8Array');
389
+ continue;
390
+ }
391
+ const valid = await this.verifyAgainst(key.meta, key.publicKey, message, entry.signature);
392
+ if (valid) {
393
+ results.push({ suiteId, keyId, valid: true });
394
+ }
395
+ else {
396
+ fail('Signature verification failed');
397
+ }
247
398
  }
248
399
  catch (error) {
249
- results.push({
250
- suiteId: entry.suiteId,
251
- keyId: entry.keyId,
252
- valid: false,
253
- error: error.message,
254
- });
400
+ fail(error instanceof Error ? error.message : String(error));
255
401
  }
256
402
  }
403
+ if (requiredSuites.length === 0) {
404
+ return refuse(`No required suites: agent '${agentId}' has no usable keys and options.requiredSuites was not given`, results);
405
+ }
406
+ if (results.length === 0) {
407
+ return refuse('No signatures supplied', results);
408
+ }
409
+ // Walk the REQUIRED suites and look each up among the valid signatures.
410
+ const missingSuites = requiredSuites.filter(required => !results.some(r => r.valid && r.suiteId === required));
411
+ const everySuppliedValid = results.every(r => r.valid === true);
257
412
  return {
258
413
  results,
259
- allValid: results.every(r => r.valid),
414
+ allValid: everySuppliedValid && missingSuites.length === 0,
415
+ requiredSuites,
416
+ missingSuites,
417
+ ...(missingSuites.length > 0
418
+ ? { error: `No valid signature for required suite(s): ${missingSuites.join(', ')}` }
419
+ : {}),
260
420
  };
261
421
  }
262
422
  /**
@@ -276,22 +436,29 @@ export class DefaultKeySDK {
276
436
  *
277
437
  * @param agentId Agent identifier
278
438
  * @param profile Crypto profile (must use derivation-capable suite)
279
- * @param mnemonic BIP39 mnemonic phrase
439
+ * @param mnemonic BIP39 mnemonic phrase (words and checksum are validated)
280
440
  * @param path BIP32 derivation path (default: "m/44'/0'/0'/0/0")
281
441
  * @param passphrase Optional BIP39 passphrase
282
442
  * @param options Additional key creation options
283
443
  * @returns KeyRecord with derived key
444
+ * @throws if the mnemonic has an unknown word, a wrong word count or a bad checksum
284
445
  */
285
446
  async createKeyFromMnemonic(agentId, profile, mnemonic, path = BIP32Derivation.bitcoinPath(0, 0, 0), passphrase, options = {}) {
286
447
  const bip32 = new BIP32Derivation();
287
448
  const seed = bip32.mnemonicToSeed(mnemonic, passphrase);
288
- return await this.createKey(agentId, profile, {
289
- ...options,
290
- derivationContext: {
291
- seed,
292
- path,
293
- },
294
- });
449
+ try {
450
+ return await this.createKey(agentId, profile, {
451
+ ...options,
452
+ derivationContext: {
453
+ seed,
454
+ path,
455
+ },
456
+ });
457
+ }
458
+ finally {
459
+ // The seed is the root of every key in the wallet; do not leave it behind.
460
+ seed.fill(0);
461
+ }
295
462
  }
296
463
  /**
297
464
  * Get available capabilities for a specific suite.
@@ -323,10 +490,36 @@ export class DefaultKeySDK {
323
490
  .map(suite => suite.suiteId);
324
491
  }
325
492
  /**
326
- * Create a Bitcoin wallet key from mnemonic (convenience method).
493
+ * Create a wallet key from a mnemonic at an explicit BIP44 coin type:
494
+ * `m/44'/coinType'/account'/change/index`.
495
+ *
496
+ * The coin type is required and has no default. It decides which wallets
497
+ * will find the key again: Bitcoin (BTC) is 0, Bitcoin SV is 236.
498
+ *
499
+ * @param agentId Agent identifier
500
+ * @param mnemonic BIP39 mnemonic phrase (words and checksum are validated)
501
+ * @param path Coin type and position in the BIP44 tree
502
+ * @param options Additional key creation options
503
+ * @returns KeyRecord with the derived secp256k1 key
504
+ */
505
+ async createWalletKey(agentId, mnemonic, path, options = {}) {
506
+ if (!path || typeof path.coinType !== 'number') {
507
+ throw new Error('createWalletKey requires path.coinType (SLIP-44: Bitcoin is 0, Bitcoin SV is 236)');
508
+ }
509
+ const derivationPath = BIP32Derivation.bip44Path(path.coinType, path.account ?? 0, path.change ? 1 : 0, path.index ?? 0);
510
+ return this.createKeyFromMnemonic(agentId, { primarySignatureSuite: 'bsv-ecdsa-secp256k1' }, mnemonic, derivationPath, path.passphrase, options);
511
+ }
512
+ /**
513
+ * Create a wallet key from mnemonic at the Bitcoin (BTC) BIP44 path:
514
+ * m/44'/0'/account'/change/addressIndex
327
515
  *
328
- * Uses standard BIP44 Bitcoin path: m/44'/0'/account'/change/addressIndex
516
+ * NOTE: coin type 0 is BTC. A Bitcoin SV wallet restoring the same mnemonic
517
+ * looks under coin type 236 and will not find this key. The path is left
518
+ * as it is because changing it would move every key already created with
519
+ * this method. For new code use {@link createWalletKey}, which makes the
520
+ * coin type explicit.
329
521
  *
522
+ * @deprecated Use createWalletKey(agentId, mnemonic, { coinType, ... }).
330
523
  * @param agentId Agent identifier
331
524
  * @param mnemonic BIP39 mnemonic phrase
332
525
  * @param accountIndex Account index (default: 0)
@@ -348,6 +541,34 @@ export class DefaultKeySDK {
348
541
  }
349
542
  return 'sha256';
350
543
  }
544
+ /**
545
+ * Helper: register a key under the agent's next free key ID.
546
+ *
547
+ * The counter is cached per SDK instance, so another instance (or process)
548
+ * on the same storage can have taken the ID already. The registry refuses
549
+ * to overwrite; on that refusal the counter is re-read from storage and the
550
+ * next ID is tried. Before, the second writer silently replaced the first
551
+ * writer's private key.
552
+ */
553
+ async registerUnderFreshId(agentId, suiteId, register) {
554
+ // 'ml' for every ml-dsa suite, 'bsv' for bsv-ecdsa-secp256k1
555
+ const suiteName = suiteId.split('-')[0].replace(/[^A-Za-z0-9]/g, '') || 'key';
556
+ for (let attempt = 0; attempt < MAX_KEY_ID_ATTEMPTS; attempt++) {
557
+ const counter = await this.getNextCounter(agentId);
558
+ const keyId = `${agentId}:pk-${suiteName}-${counter}`;
559
+ try {
560
+ return await register(keyId);
561
+ }
562
+ catch (error) {
563
+ if (!(error instanceof KeyAlreadyExistsError))
564
+ throw error;
565
+ // Someone else holds that ID: forget the cached counter and re-read.
566
+ this.counterInitialized.delete(agentId);
567
+ this.keyCounter.delete(agentId);
568
+ }
569
+ }
570
+ throw new Error(`Could not allocate a key ID for agent '${agentId}' after ${MAX_KEY_ID_ATTEMPTS} attempts`);
571
+ }
351
572
  /**
352
573
  * Helper: Get next counter for an agent.
353
574
  */
@@ -355,19 +576,25 @@ export class DefaultKeySDK {
355
576
  if (!this.counterInitialized.has(agentId)) {
356
577
  if (!this.counterInitPromises.has(agentId)) {
357
578
  const initPromise = (async () => {
358
- const existing = await this.keyRegistry.listKeys();
359
- const max = existing.reduce((acc, meta) => {
360
- if (!meta.keyId.startsWith(`${agentId}:pk-`))
361
- return acc;
362
- const match = meta.keyId.match(/:pk-[^-]+-(\d+)$/);
363
- if (!match)
364
- return acc;
365
- const num = parseInt(match[1], 10);
366
- return Number.isFinite(num) ? Math.max(acc, num) : acc;
367
- }, 0);
368
- this.keyCounter.set(agentId, max);
369
- this.counterInitialized.add(agentId);
370
- this.counterInitPromises.delete(agentId);
579
+ try {
580
+ const existing = await this.keyRegistry.listKeys();
581
+ const max = existing.reduce((acc, meta) => {
582
+ if (agentIdOfKey(meta.keyId) !== agentId)
583
+ return acc;
584
+ const local = meta.keyId.slice(agentId.length + 1);
585
+ const match = COUNTER_SUFFIX.exec(local);
586
+ if (!match)
587
+ return acc;
588
+ const num = parseInt(match[1], 10);
589
+ return Number.isSafeInteger(num) ? Math.max(acc, num) : acc;
590
+ }, 0);
591
+ // A counter handed out while this scan ran must not be reused.
592
+ this.keyCounter.set(agentId, Math.max(max, this.keyCounter.get(agentId) || 0));
593
+ this.counterInitialized.add(agentId);
594
+ }
595
+ finally {
596
+ this.counterInitPromises.delete(agentId);
597
+ }
371
598
  })();
372
599
  this.counterInitPromises.set(agentId, initPromise);
373
600
  }
@@ -421,9 +648,17 @@ export class DefaultKeySDK {
421
648
  export function createKeySDK(config) {
422
649
  const keyRegistry = config?.keyRegistry || new KeyRegistry(new InMemoryKeyStorage());
423
650
  const suiteRegistry = config?.suiteRegistry || new SignatureSuiteRegistry();
424
- // Register default suites if not provided
651
+ // Register default suites if not provided.
652
+ //
653
+ // All three ML-DSA levels are registered, not just 87. The README documents
654
+ // ml-dsa-65 as the recommended default and ml-dsa-44 for constrained
655
+ // devices; registering only 87 made every one of those examples throw
656
+ // `Unknown signature suite` at runtime, with no way for a caller to fix it
657
+ // -- SignatureSuiteRegistry is not re-exported from this package.
425
658
  if (!config?.suiteRegistry) {
426
659
  suiteRegistry.register(new BsvEcdsaSuite());
660
+ suiteRegistry.register(new MlDsa44Suite());
661
+ suiteRegistry.register(new MlDsa65Suite());
427
662
  suiteRegistry.register(new MlDsa87Suite());
428
663
  }
429
664
  return new DefaultKeySDK({