gdc-sdk-node-ts 2.0.9 → 2.0.11

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,604 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { createHash, createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify } from 'crypto';
3
+ import { CryptographyService } from 'gdc-common-utils-ts/CryptographyService';
4
+ import { Content } from 'gdc-common-utils-ts/utils/content';
5
+ import { createJwtSigner } from 'gdc-common-utils-ts/utils/jwt-signer';
6
+ import { buildJwtCompact, prepareJwtForSignature } from 'gdc-common-utils-ts/utils/jwt';
7
+ import { NodeCryptoHelper } from './node-crypto-helper.js';
8
+ const DEFAULT_POLICY = {
9
+ defaults: {
10
+ 'actor-signing': 'ES384',
11
+ 'openid-id-token-signing': 'ES384',
12
+ 'vp-token-signing': 'ES384',
13
+ 'vc-signing': 'ES384',
14
+ 'comm-signing': 'ML-DSA-44',
15
+ 'comm-encryption': 'ML-KEM-768',
16
+ },
17
+ };
18
+ /**
19
+ * Node-focused managed wallet implementation for BFF, portal, and backend flows.
20
+ *
21
+ * This adapter keeps actor/profile keys separate from runtime/channel keys and
22
+ * exposes one shared `IWallet` contract suitable for:
23
+ * - user/domain signing
24
+ * - OpenID/JWT signing
25
+ * - DIDComm-style transport wrapping
26
+ * - confidential document protection
27
+ */
28
+ export class NodeManagedWallet {
29
+ /**
30
+ * Creates one managed wallet backed by `CryptographyService` and Node crypto.
31
+ */
32
+ constructor(options = {}) {
33
+ this.owners = new Map();
34
+ this.cryptoHelper = options.cryptoHelper ?? new NodeCryptoHelper();
35
+ this.cryptography = options.cryptography ?? new CryptographyService(this.cryptoHelper);
36
+ this.resolveRecipientJwk = options.resolveRecipientJwk;
37
+ this.policy = {
38
+ defaults: {
39
+ ...DEFAULT_POLICY.defaults,
40
+ ...(options.policy?.defaults ?? {}),
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Legacy provisioning shape kept for app compatibility.
46
+ *
47
+ * It provisions one profile-owned signing key, one runtime communication
48
+ * signing key, and one runtime communication encryption key while returning
49
+ * the signing/encryption public keys expected by older app-facing flows.
50
+ */
51
+ async provisionKeys(entityId) {
52
+ const profileContext = {
53
+ profile: {
54
+ profileId: entityId,
55
+ },
56
+ runtime: {
57
+ runtimeId: `${entityId}:runtime`,
58
+ runtimeType: 'web-app',
59
+ },
60
+ };
61
+ await this.provisionManagedKeys(profileContext, {
62
+ ownerScope: 'profile',
63
+ purposes: ['actor-signing'],
64
+ seedMaterial: entityId,
65
+ mode: 'deterministic',
66
+ });
67
+ const runtimeKeySet = await this.provisionManagedKeys(profileContext, {
68
+ ownerScope: 'runtime',
69
+ purposes: ['comm-signing', 'comm-encryption'],
70
+ seedMaterial: `${entityId}:runtime`,
71
+ mode: 'deterministic',
72
+ });
73
+ const signingKey = await this.getPublicJwks(profileContext, {
74
+ ownerScope: 'profile',
75
+ purpose: 'actor-signing',
76
+ });
77
+ const encryptionKey = runtimeKeySet.keys.find((key) => key.use === 'enc');
78
+ return {
79
+ keys: [
80
+ signingKey[0]?.publicJwk ?? runtimeKeySet.keys[0],
81
+ encryptionKey ?? runtimeKeySet.keys[0],
82
+ ],
83
+ };
84
+ }
85
+ /**
86
+ * Rich provisioning shape for actor/profile keys and runtime/channel keys.
87
+ */
88
+ async provisionManagedKeys(context, request) {
89
+ const ownerState = this.getOrCreateOwnerState(context, request.ownerScope);
90
+ const ownerId = this.resolveOwnerId(context, request.ownerScope);
91
+ const created = [];
92
+ for (const purpose of request.purposes) {
93
+ const existing = ownerState.keys.find((entry) => entry.descriptor.purpose === purpose);
94
+ if (existing) {
95
+ created.push(existing.descriptor.publicJwk);
96
+ continue;
97
+ }
98
+ const descriptor = await this.createManagedKey(context, request.ownerScope, purpose, request, ownerId);
99
+ ownerState.keys.push(descriptor);
100
+ created.push(descriptor.descriptor.publicJwk);
101
+ }
102
+ if (!ownerState.storageKey) {
103
+ ownerState.storageKey = await this.createStorageKey(request, ownerId);
104
+ }
105
+ return { keys: created };
106
+ }
107
+ /**
108
+ * Returns the currently available public JWKs for the selected context and filter.
109
+ */
110
+ async getPublicJwks(context, filter) {
111
+ if (!context && !filter?.keyId)
112
+ return [];
113
+ if (filter?.keyId) {
114
+ for (const ownerState of this.owners.values()) {
115
+ const match = ownerState.keys.find((entry) => entry.descriptor.kid === filter.keyId);
116
+ if (match && this.matchesSelection(match.descriptor, filter)) {
117
+ return [match.descriptor];
118
+ }
119
+ }
120
+ return [];
121
+ }
122
+ const descriptors = [];
123
+ for (const scope of this.inferRelevantScopes(context, filter)) {
124
+ const ownerState = this.tryGetOwnerState(context, scope);
125
+ for (const entry of ownerState?.keys ?? []) {
126
+ if (this.matchesSelection(entry.descriptor, filter)) {
127
+ descriptors.push(entry.descriptor);
128
+ }
129
+ }
130
+ }
131
+ return descriptors;
132
+ }
133
+ /**
134
+ * Computes a digest of a string using the configured runtime helper.
135
+ */
136
+ async digest(data, algorithm) {
137
+ return this.cryptoHelper.digestString(data, algorithm);
138
+ }
139
+ /**
140
+ * Protects one confidential document using one owner-specific symmetric storage key.
141
+ */
142
+ async protectConfidentialData(doc, entityId) {
143
+ return this.protectManagedConfidentialData(doc, {
144
+ profile: { profileId: entityId },
145
+ runtime: { runtimeId: `${entityId}:runtime`, runtimeType: 'web-app' },
146
+ });
147
+ }
148
+ /**
149
+ * Protects one confidential document using the richer execution-context model.
150
+ */
151
+ async protectManagedConfidentialData(doc, context, _options) {
152
+ if (!doc?.content)
153
+ return doc;
154
+ const storageKey = this.requireStorageKey(context);
155
+ const ownerId = this.resolveStorageOwnerId(context);
156
+ const contentString = JSON.stringify(doc.content);
157
+ const encrypted = await this.cryptography.encrypt(contentString, storageKey, ownerId);
158
+ const { content, ...docWithoutContent } = doc;
159
+ return {
160
+ ...docWithoutContent,
161
+ jwe: encrypted,
162
+ };
163
+ }
164
+ /**
165
+ * Decrypts one confidential document using the legacy entity id shape.
166
+ */
167
+ async unprotectConfidentialData(doc, entityId) {
168
+ return this.unprotectManagedConfidentialData(doc, {
169
+ profile: { profileId: entityId },
170
+ runtime: { runtimeId: `${entityId}:runtime`, runtimeType: 'web-app' },
171
+ });
172
+ }
173
+ /**
174
+ * Decrypts one confidential document using the richer execution-context model.
175
+ */
176
+ async unprotectManagedConfidentialData(doc, context, _options) {
177
+ if (!doc?.jwe)
178
+ return doc;
179
+ const storageKey = this.requireStorageKey(context);
180
+ const ownerId = this.resolveStorageOwnerId(context);
181
+ const decrypted = await this.cryptography.decrypt(doc.jwe, storageKey, ownerId);
182
+ const { jwe, ...docWithoutJwe } = doc;
183
+ return {
184
+ ...docWithoutJwe,
185
+ content: JSON.parse(decrypted),
186
+ };
187
+ }
188
+ /**
189
+ * Signs arbitrary bytes or one UTF-8 string using the selected managed key.
190
+ */
191
+ async sign(payload, context, options) {
192
+ const entry = this.requireManagedKey(context, options, 'sig');
193
+ const payloadBytes = typeof payload === 'string' ? Content.stringToBytesUTF8(payload) : payload;
194
+ const alg = entry.descriptor.alg;
195
+ if (alg.startsWith('ML-DSA')) {
196
+ const signature = await this.cryptography.signBytes(payloadBytes, entry.privateMaterial, alg);
197
+ return Content.bytesToRawBase64UrlSafe(signature);
198
+ }
199
+ const privateJwk = entry.privateMaterial;
200
+ const keyObject = createPrivateKey({ key: privateJwk, format: 'jwk' });
201
+ const signature = cryptoSign(this.resolveNodeDigestForAlgorithm(alg), Buffer.from(payloadBytes), keyObject);
202
+ return Buffer.from(signature).toString('base64url');
203
+ }
204
+ /**
205
+ * Verifies one signature against the provided public JWK.
206
+ */
207
+ async verify(payload, signature, jwk, options) {
208
+ const payloadBytes = typeof payload === 'string' ? Content.stringToBytesUTF8(payload) : payload;
209
+ const algorithm = (options?.alg ?? jwk.alg);
210
+ if (!algorithm) {
211
+ throw new Error('NodeManagedWallet.verify requires an algorithm on the key or in options.alg.');
212
+ }
213
+ if (algorithm.startsWith('ML-DSA')) {
214
+ return this.cryptography.verifyBytes(Content.base64ToBytes(signature), payloadBytes, jwk);
215
+ }
216
+ const publicKey = createPublicKey({ key: jwk, format: 'jwk' });
217
+ return cryptoVerify(this.resolveNodeDigestForAlgorithm(algorithm), Buffer.from(payloadBytes), publicKey, Buffer.from(signature, 'base64url'));
218
+ }
219
+ /**
220
+ * Encrypts one payload for the provided recipient public JWK.
221
+ */
222
+ async encrypt(plaintext, recipientJwk, options) {
223
+ if (!options?.context || !options?.key) {
224
+ throw new Error('NodeManagedWallet.encrypt requires options.context and options.key.');
225
+ }
226
+ return this.buildCompactJwe(options.context, {
227
+ plaintext,
228
+ recipientJwk,
229
+ contentType: options.contentType,
230
+ key: options.key,
231
+ });
232
+ }
233
+ /**
234
+ * Decrypts one ciphertext using one selected local encryption key.
235
+ */
236
+ async decrypt(ciphertext, context, options) {
237
+ return this.decryptCompactJwe(ciphertext, context, {
238
+ key: options?.key ?? {
239
+ ownerScope: 'runtime',
240
+ purpose: 'comm-encryption',
241
+ },
242
+ });
243
+ }
244
+ /**
245
+ * Builds one compact JWS using one managed signing key.
246
+ */
247
+ async signCompactJws(context, request) {
248
+ const entry = this.requireManagedKey(context, request.key, 'sig');
249
+ const header = {
250
+ ...request.header,
251
+ alg: entry.descriptor.alg,
252
+ kid: entry.descriptor.kid,
253
+ };
254
+ const prepared = prepareJwtForSignature(header, request.claims);
255
+ const signature = await this.sign(prepared.signingInput, context, request.key);
256
+ return buildJwtCompact(prepared.encodedHeader, prepared.encodedPayload, signature);
257
+ }
258
+ /**
259
+ * Builds one detached compact JWS using one managed signing key.
260
+ */
261
+ async signDetachedJws(context, request) {
262
+ const entry = this.requireManagedKey(context, request.key, 'sig');
263
+ const header = {
264
+ ...request.header,
265
+ alg: entry.descriptor.alg,
266
+ kid: entry.descriptor.kid,
267
+ b64: false,
268
+ crit: ['b64'],
269
+ };
270
+ const payloadBytes = typeof request.payload === 'string' ? Content.stringToBytesUTF8(request.payload) : request.payload;
271
+ const encodedHeader = Content.objectToRawBase64UrlSafe(header);
272
+ const signingBytes = Buffer.concat([Buffer.from(`${encodedHeader}.`, 'ascii'), Buffer.from(payloadBytes)]);
273
+ const signature = await this.sign(signingBytes, context, request.key);
274
+ return `${encodedHeader}..${signature}`;
275
+ }
276
+ /**
277
+ * Builds one compact JWE using one selected local ML-KEM key and one recipient public JWK.
278
+ */
279
+ async buildCompactJwe(context, request) {
280
+ const entry = this.requireManagedKey(context, request.key, 'enc');
281
+ const secretKeyJwk = {
282
+ ...entry.descriptor.publicJwk,
283
+ dBytes: entry.privateMaterial,
284
+ };
285
+ const protectedHeader = {
286
+ enc: 'A256GCM',
287
+ ...(request.contentType ? { cty: request.contentType } : {}),
288
+ };
289
+ const plaintext = typeof request.plaintext === 'string'
290
+ ? request.plaintext
291
+ : Content.bytesToStringUTF8(request.plaintext);
292
+ return this.cryptography.encryptJweToCompact(plaintext, protectedHeader, secretKeyJwk, request.recipientJwk);
293
+ }
294
+ /**
295
+ * Decrypts one compact JWE using one selected local ML-KEM key.
296
+ */
297
+ async decryptCompactJwe(jwe, context, options) {
298
+ const entry = this.requireManagedKey(context, options.key, 'enc');
299
+ const secretKeyJwk = {
300
+ ...entry.descriptor.publicJwk,
301
+ dBytes: entry.privateMaterial,
302
+ };
303
+ const result = await this.cryptography.decryptJwe(jwe, secretKeyJwk);
304
+ return result.decryptedBytes;
305
+ }
306
+ /**
307
+ * Legacy pack shape retained for app compatibility.
308
+ */
309
+ async packForRecipient(content, recipientDid) {
310
+ return this.packForRecipientWithContext(content, recipientDid, {
311
+ context: {
312
+ runtime: {
313
+ runtimeId: 'default-runtime',
314
+ runtimeType: 'web-app',
315
+ },
316
+ },
317
+ });
318
+ }
319
+ /**
320
+ * Packs one payload into a transport envelope signed and encrypted by the runtime.
321
+ */
322
+ async packForRecipientWithContext(content, recipientDidOrJwk, options) {
323
+ const recipientJwk = typeof recipientDidOrJwk === 'string'
324
+ ? await this.resolveRecipientPublicJwk(recipientDidOrJwk)
325
+ : recipientDidOrJwk;
326
+ const signingKey = options.signingKey ?? {
327
+ ownerScope: 'runtime',
328
+ purpose: 'comm-signing',
329
+ };
330
+ const encryptionKey = options.encryptionKey ?? {
331
+ ownerScope: 'runtime',
332
+ purpose: 'comm-encryption',
333
+ };
334
+ const compactJws = await this.signCompactJws(options.context, {
335
+ header: {
336
+ typ: 'JWS',
337
+ },
338
+ claims: {
339
+ payload: content,
340
+ },
341
+ key: signingKey,
342
+ });
343
+ return this.buildCompactJwe(options.context, {
344
+ plaintext: compactJws,
345
+ recipientJwk,
346
+ contentType: 'JWS',
347
+ key: encryptionKey,
348
+ });
349
+ }
350
+ /**
351
+ * Legacy unpack shape retained for app compatibility.
352
+ */
353
+ async unpack(packedMessage) {
354
+ return this.unpackWithContext(packedMessage, {
355
+ context: {
356
+ runtime: {
357
+ runtimeId: 'default-runtime',
358
+ runtimeType: 'web-app',
359
+ },
360
+ },
361
+ });
362
+ }
363
+ /**
364
+ * Unpacks one transport envelope and returns the decoded business payload plus JOSE metadata.
365
+ */
366
+ async unpackWithContext(packedMessage, options) {
367
+ const decryptedBytes = await this.decryptCompactJwe(packedMessage, options.context, {
368
+ key: options.decryptionKey ?? {
369
+ ownerScope: 'runtime',
370
+ purpose: 'comm-encryption',
371
+ },
372
+ });
373
+ const decryptedText = Content.bytesToStringUTF8(decryptedBytes);
374
+ const protectedHeader = this.cryptography.parseCompactJwe(packedMessage).protected;
375
+ if (protectedHeader) {
376
+ const decodedProtectedHeader = Content.base64UrlSafeToJSON(protectedHeader);
377
+ if (decodedProtectedHeader.cty === 'JWS') {
378
+ const compact = decryptedText;
379
+ const parts = compact.split('.');
380
+ if (parts.length !== 3) {
381
+ throw new Error('NodeManagedWallet.unpackWithContext expected a compact JWS payload.');
382
+ }
383
+ const payload = Content.base64UrlSafeToJSON(parts[1]);
384
+ return {
385
+ content: payload.payload,
386
+ meta: {
387
+ jwe: { protected: decodedProtectedHeader },
388
+ jws: {
389
+ compact,
390
+ },
391
+ },
392
+ };
393
+ }
394
+ return {
395
+ content: JSON.parse(decryptedText),
396
+ meta: {
397
+ jwe: { protected: decodedProtectedHeader },
398
+ },
399
+ };
400
+ }
401
+ return {
402
+ content: JSON.parse(decryptedText),
403
+ meta: {},
404
+ };
405
+ }
406
+ getOrCreateOwnerState(context, ownerScope) {
407
+ const ownerId = this.resolveOwnerId(context, ownerScope);
408
+ const existing = this.owners.get(ownerId);
409
+ if (existing)
410
+ return existing;
411
+ const created = {
412
+ keys: [],
413
+ };
414
+ this.owners.set(ownerId, created);
415
+ return created;
416
+ }
417
+ tryGetOwnerState(context, ownerScope) {
418
+ const ownerId = this.resolveOwnerId(context, ownerScope);
419
+ return this.owners.get(ownerId);
420
+ }
421
+ resolveOwnerId(context, ownerScope) {
422
+ if (ownerScope === 'profile') {
423
+ const profileId = context.profile?.profileId;
424
+ if (!profileId) {
425
+ throw new Error('NodeManagedWallet requires context.profile.profileId when ownerScope="profile".');
426
+ }
427
+ return `profile:${profileId}:${context.walletId ?? 'default'}`;
428
+ }
429
+ const runtimeId = context.runtime?.runtimeId;
430
+ if (!runtimeId) {
431
+ throw new Error('NodeManagedWallet requires context.runtime.runtimeId when ownerScope="runtime".');
432
+ }
433
+ return `runtime:${runtimeId}:${context.walletId ?? 'default'}`;
434
+ }
435
+ resolveStorageOwnerId(context) {
436
+ if (context.profile?.profileId)
437
+ return `storage:profile:${context.profile.profileId}`;
438
+ if (context.runtime?.runtimeId)
439
+ return `storage:runtime:${context.runtime.runtimeId}`;
440
+ throw new Error('NodeManagedWallet requires either context.profile or context.runtime for storage operations.');
441
+ }
442
+ async createManagedKey(context, ownerScope, purpose, request, ownerId) {
443
+ const algorithm = this.resolveAlgorithmForPurpose(purpose);
444
+ if (algorithm === 'ML-KEM-768' || algorithm === 'ML-KEM-1024') {
445
+ const seedBytes = request.mode === 'deterministic' && request.seedMaterial !== undefined
446
+ ? this.deriveSeedBytes(request.seedMaterial, ownerId, purpose, 64)
447
+ : undefined;
448
+ const generated = await this.cryptography.generateKeyPairMlKem(seedBytes, algorithm);
449
+ return {
450
+ descriptor: {
451
+ kid: generated.publicJWKey.kid,
452
+ ownerScope,
453
+ purpose,
454
+ use: 'enc',
455
+ alg: algorithm,
456
+ publicJwk: generated.publicJWKey,
457
+ defaultForPurpose: true,
458
+ },
459
+ privateMaterial: generated.secretKeyBytes,
460
+ };
461
+ }
462
+ const signer = await createJwtSigner({
463
+ alg: algorithm,
464
+ purpose: `${ownerId}:${purpose}`,
465
+ seed: request.mode === 'deterministic' && request.seedMaterial !== undefined
466
+ ? this.deriveSignerSeed(request.seedMaterial, ownerId, purpose)
467
+ : undefined,
468
+ cryptography: this.cryptography,
469
+ });
470
+ return {
471
+ descriptor: {
472
+ kid: signer.getKid(),
473
+ ownerScope,
474
+ purpose,
475
+ use: 'sig',
476
+ alg: algorithm,
477
+ publicJwk: signer.getPublicJwk(),
478
+ defaultForPurpose: true,
479
+ },
480
+ privateMaterial: signer.getPrivateMaterial(),
481
+ };
482
+ }
483
+ async createStorageKey(request, ownerId) {
484
+ if (request.mode === 'deterministic' && request.seedMaterial !== undefined) {
485
+ return this.deriveSeedBytes(request.seedMaterial, ownerId, 'document-at-rest', 32);
486
+ }
487
+ return this.cryptoHelper.getRandomBytes(32);
488
+ }
489
+ requireStorageKey(context) {
490
+ if (context.profile?.profileId) {
491
+ const state = this.owners.get(`profile:${context.profile.profileId}:${context.walletId ?? 'default'}`);
492
+ if (state?.storageKey)
493
+ return state.storageKey;
494
+ }
495
+ if (context.runtime?.runtimeId) {
496
+ const state = this.owners.get(`runtime:${context.runtime.runtimeId}:${context.walletId ?? 'default'}`);
497
+ if (state?.storageKey)
498
+ return state.storageKey;
499
+ }
500
+ throw new Error('NodeManagedWallet has no storage key for the provided context. Provision keys first.');
501
+ }
502
+ requireManagedKey(context, selection, expectedUse) {
503
+ if (selection.keyId) {
504
+ for (const ownerState of this.owners.values()) {
505
+ const match = ownerState.keys.find((entry) => entry.descriptor.kid === selection.keyId);
506
+ if (match) {
507
+ if (expectedUse && match.descriptor.use !== expectedUse) {
508
+ throw new Error(`NodeManagedWallet key '${selection.keyId}' is not usable for '${expectedUse}'.`);
509
+ }
510
+ return match;
511
+ }
512
+ }
513
+ }
514
+ const scopes = this.inferRelevantScopes(context, selection);
515
+ for (const scope of scopes) {
516
+ const ownerState = this.tryGetOwnerState(context, scope);
517
+ const match = ownerState?.keys.find((entry) => this.matchesSelection(entry.descriptor, selection));
518
+ if (match) {
519
+ if (expectedUse && match.descriptor.use !== expectedUse) {
520
+ throw new Error(`NodeManagedWallet key '${match.descriptor.kid}' is not usable for '${expectedUse}'.`);
521
+ }
522
+ return match;
523
+ }
524
+ }
525
+ throw new Error(`NodeManagedWallet found no managed key for selection ${JSON.stringify(selection)}.`);
526
+ }
527
+ inferRelevantScopes(context, selection) {
528
+ if (selection?.ownerScope)
529
+ return [selection.ownerScope];
530
+ const scopes = [];
531
+ if (context?.profile?.profileId)
532
+ scopes.push('profile');
533
+ if (context?.runtime?.runtimeId)
534
+ scopes.push('runtime');
535
+ return scopes.length > 0 ? scopes : ['runtime', 'profile'];
536
+ }
537
+ matchesSelection(descriptor, selection) {
538
+ if (!selection)
539
+ return true;
540
+ if (selection.keyId && descriptor.kid !== selection.keyId)
541
+ return false;
542
+ if (selection.ownerScope && descriptor.ownerScope !== selection.ownerScope)
543
+ return false;
544
+ if (selection.purpose && descriptor.purpose !== selection.purpose)
545
+ return false;
546
+ if (selection.alg && descriptor.alg !== selection.alg)
547
+ return false;
548
+ return true;
549
+ }
550
+ resolveAlgorithmForPurpose(purpose) {
551
+ const resolved = this.policy.defaults[purpose];
552
+ if (!resolved) {
553
+ throw new Error(`NodeManagedWallet has no default algorithm configured for purpose '${purpose}'.`);
554
+ }
555
+ return resolved;
556
+ }
557
+ deriveSignerSeed(seedMaterial, ownerId, purpose) {
558
+ const base = seedMaterial instanceof Uint8Array
559
+ ? Buffer.from(seedMaterial).toString('base64url')
560
+ : seedMaterial;
561
+ return `${base}:${ownerId}:${purpose}`;
562
+ }
563
+ deriveSeedBytes(seedMaterial, ownerId, purpose, size) {
564
+ const source = seedMaterial instanceof Uint8Array
565
+ ? Buffer.from(seedMaterial).toString('base64url')
566
+ : seedMaterial;
567
+ const blocks = [];
568
+ let counter = 0;
569
+ while (Buffer.concat(blocks).length < size) {
570
+ blocks.push(Buffer.from(this.deriveDeterministicBlock(source, ownerId, purpose, counter), 'hex'));
571
+ counter += 1;
572
+ }
573
+ return Buffer.concat(blocks).subarray(0, size);
574
+ }
575
+ deriveDeterministicBlock(seed, ownerId, purpose, counter) {
576
+ return createHash('sha512')
577
+ .update(seed, 'utf8')
578
+ .update(':', 'utf8')
579
+ .update(ownerId, 'utf8')
580
+ .update(':', 'utf8')
581
+ .update(purpose, 'utf8')
582
+ .update(':', 'utf8')
583
+ .update(String(counter), 'utf8')
584
+ .digest('hex');
585
+ }
586
+ resolveNodeDigestForAlgorithm(algorithm) {
587
+ switch (algorithm) {
588
+ case 'ES256K':
589
+ return 'sha256';
590
+ case 'ES384':
591
+ return 'sha384';
592
+ case 'RS256':
593
+ return 'sha256';
594
+ default:
595
+ throw new Error(`NodeManagedWallet does not support Node digest signing for algorithm '${algorithm}'.`);
596
+ }
597
+ }
598
+ async resolveRecipientPublicJwk(recipientDid) {
599
+ if (!this.resolveRecipientJwk) {
600
+ throw new Error(`NodeManagedWallet cannot resolve recipient DID '${recipientDid}' without options.resolveRecipientJwk.`);
601
+ }
602
+ return this.resolveRecipientJwk(recipientDid);
603
+ }
604
+ }
@@ -125,6 +125,12 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
125
125
  * - `_transaction` is the new host onboarding step
126
126
  * - this runtime does not chain `_activate` after `_transaction`
127
127
  * - `_activate` remains available only for the older ICA `_verify` based flow
128
+ *
129
+ * Commercial contract:
130
+ * - the final poll response is expected to mint
131
+ * `meta.claims['org.schema.Offer.identifier']`
132
+ * - callers should then pass that exact value to
133
+ * `confirmLegalOrganizationOrder(...)`
128
134
  */
129
135
  submitLegalOrganizationVerificationTransaction(hostCtx: HostRouteContext, input: NodeLegalOrganizationVerificationTransactionInput, pollOptions?: PollOptions): Promise<SubmitAndPollResult>;
130
136
  /**
@@ -135,6 +141,7 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
135
141
  * - reuse the same signed evidence/controller binding contract as `_transaction`
136
142
  * - do not create a new Offer
137
143
  * - expect GW CORE to reissue one controller activation code in the response
144
+ * - do not call `confirmLegalOrganizationOrder(...)` after this flow
138
145
  */
139
146
  submitLegalOrganizationIssue(hostCtx: HostRouteContext, input: NodeLegalOrganizationVerificationTransactionInput, pollOptions?: PollOptions): Promise<SubmitAndPollResult>;
140
147
  /**
@@ -162,10 +169,19 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
162
169
  * headers instead of plaintext `meta`
163
170
  * - this mirrored metadata is transport fallback only; the canonical
164
171
  * activation contract remains `body.vp_token` plus `body.controller.*`
172
+ *
173
+ * Commercial contract:
174
+ * - legacy `_activate` is still expected to mint
175
+ * `meta.claims['org.schema.Offer.identifier']`
176
+ * - callers should then pass that exact value to
177
+ * `confirmLegalOrganizationOrder(...)`
165
178
  */
166
179
  activateOrganizationInGatewayFromIcaProof(hostCtx: HostRouteContext, input: NodeOrganizationActivationInput, pollOptions?: PollOptions): Promise<SubmitAndPollResult>;
167
180
  /**
168
181
  * Confirms a host-side legal organization order after the initial activation.
182
+ *
183
+ * Use this only when the previous flow actually returned one canonical Offer
184
+ * identifier. `_transaction` and legacy `_activate` do; `_issue` does not.
169
185
  */
170
186
  confirmLegalOrganizationOrder(hostCtx: HostRouteContext, input: LegalOrganizationOrderInput, pollOptions?: PollOptions): Promise<SubmitAndPollResult>;
171
187
  /**
@@ -244,6 +260,15 @@ export declare class HttpRuntimeClient implements NodeRuntimeClient {
244
260
  confirmOrganizationLicenseOrder(ctx: RouteContext, input: OrganizationLicenseOrderConfirmInput, pollOptions?: PollOptions): Promise<SubmitAndPollResult>;
245
261
  /**
246
262
  * Starts the onboarding flow for an individual-oriented tenant or index.
263
+ *
264
+ * Commercial contract:
265
+ * - this SDK method targets the family/individual commercial bootstrap flow
266
+ * - the registration poll response is expected to return one Offer id
267
+ * - callers should then confirm it through
268
+ * `confirmIndividualOrganizationOrder(...)`
269
+ *
270
+ * This is distinct from embedded legacy individual registration helpers in
271
+ * GW CORE that may persist an individual record without minting an Offer.
247
272
  */
248
273
  startIndividualOrganization(input: IndividualOrganizationBootstrapInput): Promise<IndividualOrganizationStartResult>;
249
274
  /**