gdc-sdk-node-ts 2.0.9 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ }
@@ -37,13 +37,23 @@ export type OrganizationEmployeeCreationInput = {
37
37
  */
38
38
  export type OrganizationEmployeeLifecycleInput = {
39
39
  /**
40
- * Canonical employee/person claims used by GW CORE to locate the employee.
40
+ * Canonical employee/person claims carried as the exportable employee identity.
41
+ *
42
+ * These claims should still include the business/external identifier
43
+ * (`org.schema.Person.identifier`) when available, but runtime lifecycle
44
+ * operations must prefer `resourceId` as the concrete GW profile locator.
45
+ * Treat `resource.id` as the current technical record anchor and
46
+ * `identifier` as the interoperable/exported identity value.
41
47
  */
42
48
  employeeClaims: Record<string, unknown>;
43
49
  /**
44
- * Optional canonical resource id when already known by the caller.
50
+ * Preferred current GW employee profile id returned by create/search.
51
+ *
52
+ * Pass this for disable/purge whenever the caller already knows the active
53
+ * profile row. The SDK forwards it as `Bundle.entry.resource.id`, which GW
54
+ * now treats as the primary operational locator for lifecycle actions.
45
55
  */
46
- resourceId?: string;
56
+ resourceId: string;
47
57
  dataType?: string;
48
58
  };
49
59
  export type OrganizationEmployeeSearchInput = {
@@ -19,6 +19,7 @@ export async function disableOrganizationEmployeeWithDeps(routeCtx, input, optio
19
19
  // TODO(gw-core-lifecycle-target-patch-employee-disable): switch this
20
20
  // legacy DELETE-in-_batch flow to `_batch + PATCH` when GW CORE deploys it.
21
21
  void GwCoreLifecycleTodo.EmployeeDisablePatchMigration;
22
+ assertEmployeeLifecycleResourceId(input.resourceId, 'disableEmployee');
22
23
  const payload = buildEmployeeLifecyclePayload({
23
24
  routeCtx,
24
25
  requestType: input.dataType || GwCoreLifecycleRequestType.EmployeeDisable,
@@ -30,6 +31,7 @@ export async function disableOrganizationEmployeeWithDeps(routeCtx, input, optio
30
31
  return deps.submitAndPoll(deps.employeeBatchPath(routeCtx), deps.employeePollPath(routeCtx), payload, options);
31
32
  }
32
33
  export async function purgeOrganizationEmployeeWithDeps(routeCtx, input, options, deps) {
34
+ assertEmployeeLifecycleResourceId(input.resourceId, 'purgeEmployee');
33
35
  const payload = buildEmployeeLifecyclePayload({
34
36
  routeCtx,
35
37
  requestType: input.dataType || GwCoreLifecycleRequestType.EmployeePurge,
@@ -40,6 +42,12 @@ export async function purgeOrganizationEmployeeWithDeps(routeCtx, input, options
40
42
  });
41
43
  return deps.submitAndPoll(deps.employeePurgePath(routeCtx), deps.employeePurgePollPath(routeCtx), payload, options);
42
44
  }
45
+ function assertEmployeeLifecycleResourceId(resourceId, operation) {
46
+ const normalized = String(resourceId || '').trim();
47
+ if (!normalized) {
48
+ throw new Error(`${operation}: resourceId is required and must be the current GW technical employee id (resource.id).`);
49
+ }
50
+ }
43
51
  export async function searchOrganizationEmployeesWithDeps(routeCtx, input, deps) {
44
52
  return deps.submitAndPoll(deps.employeeSearchPath(routeCtx), deps.employeeSearchPollPath(routeCtx), {
45
53
  thid: input.requestThid || `employee-search-${createRuntimeUuid()}`,
@@ -422,6 +430,8 @@ function buildEmployeeLifecyclePayload(input) {
422
430
  type: input.requestType,
423
431
  method: input.requestMethod,
424
432
  claims,
433
+ // Keep the transport-local GW profile anchor separate from the
434
+ // exportable employee identifier carried in claims.
425
435
  resourceId: input.resourceId,
426
436
  })],
427
437
  },
@@ -0,0 +1,19 @@
1
+ import type { IJobManager } from 'gdc-sdk-core-ts';
2
+ import type { WalletBackedJobManagerOptions } from './wallet-backed-job-manager.types.js';
3
+ export type { WalletBackedJobManagerOptions, WalletBackedJobPollResult, WalletBackedJobSubmitResult, WalletBackedJobTransport, WalletExecutionContextLike, } from './wallet-backed-job-manager.types.js';
4
+ /**
5
+ * Creates one backend/session job manager that stores protected job payloads in
6
+ * one local vault while reusing one shared wallet for protect/unprotect and
7
+ * transport packing.
8
+ *
9
+ * Intended for:
10
+ * - portal/BFF runtimes
11
+ * - short-lived service sessions
12
+ * - send-first or cache-first orchestration over one local in-memory vault
13
+ */
14
+ export declare function createWalletBackedJobManager(options: WalletBackedJobManagerOptions): IJobManager;
15
+ /**
16
+ * Convenience helper for the default transient backend/session mode where one
17
+ * in-memory vault is enough and durable cloud sync is optional.
18
+ */
19
+ export declare function createWalletBackedJobManagerInMemory(options: WalletBackedJobManagerOptions): IJobManager;