dataspace-client-sdk-node 0.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.
Files changed (56) hide show
  1. package/README.md +310 -0
  2. package/SDK_PARITY_MAP.md +120 -0
  3. package/TODO_PROMPT_NEXT_STEPS.md +185 -0
  4. package/artifacts/update-smart-wallet.js +1016 -0
  5. package/dist/builders.d.ts +12 -0
  6. package/dist/builders.js +17 -0
  7. package/dist/client.d.ts +333 -0
  8. package/dist/client.js +1229 -0
  9. package/dist/consent/pdfSignatureVerification.d.ts +18 -0
  10. package/dist/consent/pdfSignatureVerification.js +23 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +8 -0
  13. package/dist/sdk/dataspace-wallet-sdk-node/MultiWalletClient.d.ts +9 -0
  14. package/dist/sdk/dataspace-wallet-sdk-node/MultiWalletClient.js +21 -0
  15. package/dist/sdk/dataspace-wallet-sdk-node/WalletClient.d.ts +26 -0
  16. package/dist/sdk/dataspace-wallet-sdk-node/WalletClient.js +36 -0
  17. package/dist/sdk/dataspace-wallet-sdk-node/index.d.ts +6 -0
  18. package/dist/sdk/dataspace-wallet-sdk-node/index.js +6 -0
  19. package/dist/sdk/dataspace-wallet-sdk-node/provider.d.ts +24 -0
  20. package/dist/sdk/dataspace-wallet-sdk-node/provider.js +1 -0
  21. package/dist/sdk/dataspace-wallet-sdk-node/providers/memory-provider.d.ts +41 -0
  22. package/dist/sdk/dataspace-wallet-sdk-node/providers/memory-provider.js +216 -0
  23. package/dist/sdk/dataspace-wallet-sdk-node/providers/seed-provider.d.ts +22 -0
  24. package/dist/sdk/dataspace-wallet-sdk-node/providers/seed-provider.js +28 -0
  25. package/dist/sdk/dataspace-wallet-sdk-node/types.d.ts +51 -0
  26. package/dist/sdk/dataspace-wallet-sdk-node/types.js +1 -0
  27. package/dist/types.d.ts +445 -0
  28. package/dist/types.js +1 -0
  29. package/docs/API.md +745 -0
  30. package/docs/DATA_MODEL_ALIGNMENT.md +31 -0
  31. package/docs/DATA_PLANES_SCOPE_MATRIX.md +51 -0
  32. package/docs/DEVELOPER_USE_CASES.md +253 -0
  33. package/docs/E2E_BOOTSTRAP.md +54 -0
  34. package/docs/TODO_SMART_EHR_COMPAT.md +58 -0
  35. package/examples/backend-pkce-auth.mjs +119 -0
  36. package/examples/conversion-upload.mjs +52 -0
  37. package/examples/e2e-bootstrap-tenant.mjs +126 -0
  38. package/examples/e2e-individual-flow.mjs +43 -0
  39. package/examples/host-activate-and-employee.mjs +75 -0
  40. package/package.json +26 -0
  41. package/src/builders.ts +28 -0
  42. package/src/client.ts +1626 -0
  43. package/src/consent/pdfSignatureVerification.ts +41 -0
  44. package/src/index.ts +8 -0
  45. package/src/sdk/dataspace-wallet-sdk-node/MultiWalletClient.ts +25 -0
  46. package/src/sdk/dataspace-wallet-sdk-node/WalletClient.ts +63 -0
  47. package/src/sdk/dataspace-wallet-sdk-node/index.ts +6 -0
  48. package/src/sdk/dataspace-wallet-sdk-node/provider.ts +44 -0
  49. package/src/sdk/dataspace-wallet-sdk-node/providers/memory-provider.ts +310 -0
  50. package/src/sdk/dataspace-wallet-sdk-node/providers/seed-provider.ts +31 -0
  51. package/src/sdk/dataspace-wallet-sdk-node/types.ts +61 -0
  52. package/src/types.ts +497 -0
  53. package/tests/client.test.mjs +892 -0
  54. package/tests/uc5-org-onboarding.flow.test.mjs +145 -0
  55. package/tests/uc5-subject-data.flow.test.mjs +198 -0
  56. package/tsconfig.json +13 -0
@@ -0,0 +1,41 @@
1
+ // pdfSignatureVerification.ts
2
+ // Node.js SDK contract for PDF signature verification
3
+
4
+ export interface PdfVerifySubmission {
5
+ pdf_buffer: Buffer; // PDF as Buffer
6
+ content_type: string; // standardized snake_case for API
7
+ // Optionally: patient_id, metadata, etc.
8
+ }
9
+
10
+ export interface PdfVerifyResult {
11
+ ok: boolean;
12
+ signer?: string;
13
+ signing_time?: string;
14
+ notes?: string[];
15
+ // Optionally: extracted_claims, credential, etc.
16
+ }
17
+
18
+ export interface PdfSignatureVerificationApi {
19
+ verifyPdfSignature(submission: PdfVerifySubmission): Promise<PdfVerifyResult>;
20
+ }
21
+
22
+ import fetch from 'node-fetch';
23
+
24
+ export class PdfSignatureVerificationHttpApi implements PdfSignatureVerificationApi {
25
+ constructor(private readonly endpoint: string) {}
26
+
27
+ async verifyPdfSignature(submission: PdfVerifySubmission): Promise<PdfVerifyResult> {
28
+ const res = await fetch(this.endpoint, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/pdf' },
31
+ body: submission.pdf_buffer,
32
+ });
33
+ if (!res.ok) throw new Error('PDF verification failed: ' + res.status);
34
+ return (await res.json()) as PdfVerifyResult;
35
+ }
36
+ }
37
+
38
+ // Usage (Node):
39
+ // const api = new PdfSignatureVerificationHttpApi('https://api.example.com/verify-pdf');
40
+ // const result = await api.verifyPdfSignature({ pdf_buffer, content_type: 'application/pdf' });
41
+ // if (result.ok) { ... }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // TODO: AdapterCryptoSdkNode and other cryptography adapters should be re-exported here
2
+ // to allow external consumers (tests, services) to import them from the SDK directly.
3
+ // For now, consumers must import from gdc-common-utils-ts/adapters/node/crypto directly.
4
+ // See subjectVaultPhoneResolution.test.ts for context and migration plan.
5
+ export * from './types.js';
6
+ export * from './builders.js';
7
+ export * from './client.js';
8
+ export * from './sdk/dataspace-wallet-sdk-node/index.js';
@@ -0,0 +1,25 @@
1
+ import type { WalletProvider } from './provider.js';
2
+ import type { WalletContext } from './types.js';
3
+ import { WalletClient } from './WalletClient.js';
4
+
5
+ function contextKey(context: WalletContext): string {
6
+ return [context.tenantId, context.jurisdiction, context.sector, context.walletId ?? 'default'].join(':');
7
+ }
8
+
9
+ export class MultiWalletClient {
10
+ private readonly walletClients = new Map<string, WalletClient>();
11
+
12
+ public constructor(private readonly provider: WalletProvider) {}
13
+
14
+ public forContext(context: WalletContext): WalletClient {
15
+ const key = contextKey(context);
16
+ const existing = this.walletClients.get(key);
17
+ if (existing) {
18
+ return existing;
19
+ }
20
+
21
+ const created = new WalletClient(this.provider, context);
22
+ this.walletClients.set(key, created);
23
+ return created;
24
+ }
25
+ }
@@ -0,0 +1,63 @@
1
+ import type { WalletProvider } from './provider.js';
2
+ import type {
3
+ CompactJwsHeader,
4
+ DecryptOptions,
5
+ EncryptOptions,
6
+ PublicJwk,
7
+ SignOptions,
8
+ VerifyOptions,
9
+ WalletContext,
10
+ } from './types.js';
11
+ // CompactJweHeader is used transitively through WalletProvider
12
+
13
+ export class WalletClient {
14
+ public constructor(
15
+ private readonly provider: WalletProvider,
16
+ private readonly context: WalletContext,
17
+ ) {}
18
+
19
+ public getPublicJwks(): Promise<PublicJwk[]> {
20
+ return this.provider.getPublicJwks(this.context);
21
+ }
22
+
23
+ public sign(payload: Uint8Array | string, options?: SignOptions): Promise<string> {
24
+ return this.provider.sign(payload, this.context, options);
25
+ }
26
+
27
+ public verify(
28
+ payload: Uint8Array | string,
29
+ signature: string,
30
+ jwk: PublicJwk,
31
+ options?: VerifyOptions,
32
+ ): Promise<boolean> {
33
+ return this.provider.verify(payload, signature, jwk, options);
34
+ }
35
+
36
+ public signCompactJws(params: { header: CompactJwsHeader; claims: Record<string, unknown> }): Promise<string> {
37
+ return this.provider.signCompactJws(this.context, params);
38
+ }
39
+
40
+ public signDetachedJws(params: { header: CompactJwsHeader; payload: Uint8Array | string }): Promise<string> {
41
+ return this.provider.signDetachedJws(this.context, params);
42
+ }
43
+
44
+ public buildCompactJwe(params: {
45
+ plaintext: Uint8Array | string;
46
+ recipientJwk: PublicJwk;
47
+ contentType?: string;
48
+ }): Promise<string> {
49
+ return this.provider.buildCompactJwe(this.context, params);
50
+ }
51
+
52
+ public decryptCompactJwe(jwe: string): Promise<Uint8Array> {
53
+ return this.provider.decryptCompactJwe(jwe, this.context);
54
+ }
55
+
56
+ public encrypt(plaintext: Uint8Array | string, recipientJwk: PublicJwk, options?: EncryptOptions): Promise<string> {
57
+ return this.provider.encrypt(plaintext, recipientJwk, options);
58
+ }
59
+
60
+ public decrypt(ciphertext: string, options?: DecryptOptions): Promise<Uint8Array> {
61
+ return this.provider.decrypt(ciphertext, this.context, options);
62
+ }
63
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './provider.js';
3
+ export * from './WalletClient.js';
4
+ export * from './MultiWalletClient.js';
5
+ export * from './providers/memory-provider.js';
6
+ export * from './providers/seed-provider.js';
@@ -0,0 +1,44 @@
1
+ import type {
2
+ CompactJweHeader,
3
+ CompactJwsHeader,
4
+ DecryptOptions,
5
+ EncryptOptions,
6
+ PublicJwk,
7
+ SignOptions,
8
+ VerifyOptions,
9
+ WalletContext,
10
+ WalletInitOptions,
11
+ WalletProviderKind,
12
+ } from './types.js';
13
+
14
+ export interface WalletProvider {
15
+ readonly kind: WalletProviderKind;
16
+ init?(options?: WalletInitOptions): Promise<void> | void;
17
+ getPublicJwks(context: WalletContext): Promise<PublicJwk[]>;
18
+ sign(payload: Uint8Array | string, context: WalletContext, options?: SignOptions): Promise<string>;
19
+ verify(
20
+ payload: Uint8Array | string,
21
+ signature: string,
22
+ jwk: PublicJwk,
23
+ options?: VerifyOptions,
24
+ ): Promise<boolean>;
25
+ signCompactJws(
26
+ context: WalletContext,
27
+ params: { header: CompactJwsHeader; claims: Record<string, unknown> },
28
+ ): Promise<string>;
29
+ signDetachedJws(
30
+ context: WalletContext,
31
+ params: { header: CompactJwsHeader; payload: Uint8Array | string },
32
+ ): Promise<string>;
33
+ buildCompactJwe(
34
+ context: WalletContext,
35
+ params: { plaintext: Uint8Array | string; recipientJwk: PublicJwk; contentType?: string },
36
+ ): Promise<string>;
37
+ decryptCompactJwe(jwe: string, context: WalletContext): Promise<Uint8Array>;
38
+ encrypt(
39
+ plaintext: Uint8Array | string,
40
+ recipientJwk: PublicJwk,
41
+ options?: EncryptOptions,
42
+ ): Promise<string>;
43
+ decrypt(ciphertext: string, context: WalletContext, options?: DecryptOptions): Promise<Uint8Array>;
44
+ }
@@ -0,0 +1,310 @@
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ createHash,
5
+ createPublicKey,
6
+ generateKeyPairSync,
7
+ randomBytes,
8
+ sign as cryptoSign,
9
+ verify as cryptoVerify,
10
+ type KeyObject,
11
+ } from 'node:crypto';
12
+ import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
13
+ import type {
14
+ CompactJwsHeader,
15
+ DecryptOptions,
16
+ EncryptOptions,
17
+ PrivateKeyRef,
18
+ PublicJwk,
19
+ SignOptions,
20
+ VerifyOptions,
21
+ WalletContext,
22
+ WalletInitOptions,
23
+ WalletProviderKind,
24
+ } from '../types.js';
25
+ import type { WalletProvider } from '../provider.js';
26
+
27
+ type StoredKeyPair = {
28
+ signingPublicJwk: PublicJwk;
29
+ signingPublicKey: KeyObject;
30
+ signingPrivateKey: KeyObject;
31
+ signingPrivateKeyRef: PrivateKeyRef;
32
+ encryptionPublicJwk: PublicJwk;
33
+ /** ML-KEM-768 secret key bytes (2400 bytes). Never exposed externally. */
34
+ mlKemSecretKeyBytes: Uint8Array;
35
+ encryptionPrivateKeyRef: PrivateKeyRef;
36
+ };
37
+
38
+ function contextKey(context: WalletContext): string {
39
+ return [context.tenantId, context.jurisdiction, context.sector, context.walletId ?? 'default'].join(':');
40
+ }
41
+
42
+ function normalizeBytes(input: Uint8Array | string): Uint8Array {
43
+ return typeof input === 'string' ? Buffer.from(input, 'utf8') : input;
44
+ }
45
+
46
+ function toBase64Url(buffer: Uint8Array): string {
47
+ return Buffer.from(buffer).toString('base64url');
48
+ }
49
+
50
+ function fromBase64Url(value: string): Buffer {
51
+ return Buffer.from(value, 'base64url');
52
+ }
53
+
54
+ function encodeJsonBase64Url(value: Record<string, unknown>): string {
55
+ return toBase64Url(Buffer.from(JSON.stringify(value), 'utf8'));
56
+ }
57
+
58
+ function buildKid(context: WalletContext, jwk: PublicJwk): string {
59
+ const thumbprint = createHash('sha256')
60
+ .update(JSON.stringify({
61
+ crv: jwk.crv,
62
+ e: jwk.e,
63
+ kty: jwk.kty,
64
+ n: jwk.n,
65
+ x: jwk.x,
66
+ y: jwk.y,
67
+ }))
68
+ .digest('base64url')
69
+ .slice(0, 16);
70
+
71
+ return `wallet:${context.tenantId}:${context.jurisdiction}:${context.sector}:${thumbprint}`;
72
+ }
73
+
74
+ function buildPrivateKeyRef(kid: string, algorithm: PrivateKeyRef['algorithm']): PrivateKeyRef {
75
+ return {
76
+ kid,
77
+ kind: 'managed',
78
+ algorithm,
79
+ };
80
+ }
81
+
82
+ function buildSigningKeyPair(context: WalletContext): {
83
+ publicJwk: PublicJwk;
84
+ publicKey: KeyObject;
85
+ privateKey: KeyObject;
86
+ privateKeyRef: PrivateKeyRef;
87
+ } {
88
+ const generated = generateKeyPairSync('ec', {
89
+ namedCurve: 'secp384r1',
90
+ });
91
+ const publicJwk = generated.publicKey.export({ format: 'jwk' }) as PublicJwk;
92
+ const kid = buildKid(context, publicJwk);
93
+ const normalizedPublicJwk: PublicJwk = {
94
+ ...publicJwk,
95
+ kid,
96
+ alg: 'ES384',
97
+ use: 'sig',
98
+ key_ops: ['verify'],
99
+ };
100
+
101
+ return {
102
+ publicJwk: normalizedPublicJwk,
103
+ publicKey: createPublicKey({ key: normalizedPublicJwk, format: 'jwk' }),
104
+ privateKey: generated.privateKey,
105
+ privateKeyRef: buildPrivateKeyRef(kid, 'ES384'),
106
+ };
107
+ }
108
+
109
+ /** Generate a ML-KEM-768 key pair for content-key encapsulation (JWE `encrypted_key`). */
110
+ function buildMlKemEncryptionKeyPair(context: WalletContext): {
111
+ publicJwk: PublicJwk;
112
+ secretKeyBytes: Uint8Array;
113
+ privateKeyRef: PrivateKeyRef;
114
+ } {
115
+ const seed = randomBytes(64);
116
+ const { publicKey: publicKeyBytes, secretKey: secretKeyBytes } = ml_kem768.keygen(seed);
117
+ const x = toBase64Url(publicKeyBytes);
118
+ const kid = createHash('sha256')
119
+ .update(JSON.stringify({ crv: 'ML-KEM-768', kty: 'OKP', x }))
120
+ .digest('base64url')
121
+ .slice(0, 16);
122
+ const fullKid = `wallet:${context.tenantId}:${context.jurisdiction}:${context.sector}:${kid}`;
123
+ const publicJwk: PublicJwk = {
124
+ kty: 'OKP',
125
+ crv: 'ML-KEM-768',
126
+ x,
127
+ kid: fullKid,
128
+ alg: 'ML-KEM-768',
129
+ use: 'enc',
130
+ key_ops: ['wrapKey'],
131
+ };
132
+ return {
133
+ publicJwk,
134
+ secretKeyBytes,
135
+ privateKeyRef: buildPrivateKeyRef(fullKid, 'ML-KEM-768'),
136
+ };
137
+ }
138
+
139
+ export class MemoryWalletProvider implements WalletProvider {
140
+ public readonly kind: WalletProviderKind = 'mem';
141
+ private readonly keysByContext = new Map<string, StoredKeyPair>();
142
+
143
+ public init(options?: WalletInitOptions): void {
144
+ for (const context of options?.contexts ?? []) {
145
+ this.ensureKeyPair(context);
146
+ }
147
+ }
148
+
149
+ public async getPublicJwks(context: WalletContext): Promise<PublicJwk[]> {
150
+ const pair = this.ensureKeyPair(context);
151
+ return [pair.signingPublicJwk, pair.encryptionPublicJwk];
152
+ }
153
+
154
+ public async sign(payload: Uint8Array | string, context: WalletContext, options?: SignOptions): Promise<string> {
155
+ const pair = this.selectSigningKeyPair(context, options?.keyId);
156
+ const signature = cryptoSign('sha384', normalizeBytes(payload), pair.signingPrivateKey);
157
+ return toBase64Url(signature);
158
+ }
159
+
160
+ public async verify(
161
+ payload: Uint8Array | string,
162
+ signature: string,
163
+ jwk: PublicJwk,
164
+ _options?: VerifyOptions,
165
+ ): Promise<boolean> {
166
+ const publicKey = createPublicKey({ key: jwk, format: 'jwk' });
167
+ return cryptoVerify('sha384', normalizeBytes(payload), publicKey, fromBase64Url(signature));
168
+ }
169
+
170
+ public async signCompactJws(
171
+ context: WalletContext,
172
+ params: { header: CompactJwsHeader; claims: Record<string, unknown> },
173
+ ): Promise<string> {
174
+ const pair = this.selectSigningKeyPair(context, params.header.kid);
175
+ const header: CompactJwsHeader = {
176
+ ...params.header,
177
+ alg: params.header.alg || 'ES384',
178
+ kid: params.header.kid || pair.signingPublicJwk.kid,
179
+ };
180
+ const signingInput = `${encodeJsonBase64Url(header as Record<string, unknown>)}.${encodeJsonBase64Url(params.claims)}`;
181
+ const signature = cryptoSign('sha384', Buffer.from(signingInput, 'ascii'), pair.signingPrivateKey);
182
+ return `${signingInput}.${toBase64Url(signature)}`;
183
+ }
184
+
185
+ public async encrypt(
186
+ plaintext: Uint8Array | string,
187
+ recipientJwk: PublicJwk,
188
+ options?: EncryptOptions,
189
+ ): Promise<string> {
190
+ // Delegate to buildCompactJwe; returns a compact JWE string.
191
+ return this.buildCompactJwe(undefined as unknown as WalletContext, {
192
+ plaintext,
193
+ recipientJwk,
194
+ });
195
+ }
196
+
197
+ public async decrypt(ciphertext: string, context: WalletContext, options?: DecryptOptions): Promise<Uint8Array> {
198
+ // ciphertext is a compact JWE produced by encrypt / buildCompactJwe.
199
+ return this.decryptCompactJwe(ciphertext, context);
200
+ }
201
+
202
+ protected ensureKeyPair(context: WalletContext): StoredKeyPair {
203
+ const key = contextKey(context);
204
+ const existing = this.keysByContext.get(key);
205
+ if (existing) {
206
+ return existing;
207
+ }
208
+
209
+ const signingKeyPair = buildSigningKeyPair(context);
210
+ const encryptionKeyPair = buildMlKemEncryptionKeyPair(context);
211
+
212
+ const pair: StoredKeyPair = {
213
+ signingPublicJwk: signingKeyPair.publicJwk,
214
+ signingPublicKey: signingKeyPair.publicKey,
215
+ signingPrivateKey: signingKeyPair.privateKey,
216
+ signingPrivateKeyRef: signingKeyPair.privateKeyRef,
217
+ encryptionPublicJwk: encryptionKeyPair.publicJwk,
218
+ mlKemSecretKeyBytes: encryptionKeyPair.secretKeyBytes,
219
+ encryptionPrivateKeyRef: encryptionKeyPair.privateKeyRef,
220
+ };
221
+
222
+ this.keysByContext.set(key, pair);
223
+ return pair;
224
+ }
225
+
226
+ protected selectSigningKeyPair(context: WalletContext, keyId?: string): StoredKeyPair {
227
+ const pair = this.ensureKeyPair(context);
228
+ if (keyId && pair.signingPrivateKeyRef.kid !== keyId) {
229
+ throw new Error(`Wallet key not found for kid: ${keyId}`);
230
+ }
231
+ return pair;
232
+ }
233
+
234
+ protected selectEncryptionKeyPair(context: WalletContext, keyId?: string): StoredKeyPair {
235
+ const pair = this.ensureKeyPair(context);
236
+ if (keyId && pair.encryptionPrivateKeyRef.kid !== keyId) {
237
+ throw new Error(`Wallet key not found for kid: ${keyId}`);
238
+ }
239
+ return pair;
240
+ }
241
+
242
+ public async signDetachedJws(
243
+ context: WalletContext,
244
+ params: { header: CompactJwsHeader; payload: Uint8Array | string },
245
+ ): Promise<string> {
246
+ const pair = this.selectSigningKeyPair(context, params.header.kid);
247
+ const header: CompactJwsHeader = {
248
+ ...params.header,
249
+ alg: params.header.alg || 'ES384',
250
+ kid: params.header.kid || pair.signingPublicJwk.kid,
251
+ b64: false,
252
+ crit: ['b64'],
253
+ };
254
+ const payloadBytes = normalizeBytes(params.payload);
255
+ const encodedHeader = encodeJsonBase64Url(header as Record<string, unknown>);
256
+ const signingInput = Buffer.concat([Buffer.from(`${encodedHeader}.`, 'ascii'), payloadBytes]);
257
+ const signature = cryptoSign('sha384', signingInput, pair.signingPrivateKey);
258
+ return `${encodedHeader}..${toBase64Url(signature)}`;
259
+ }
260
+
261
+ public async buildCompactJwe(
262
+ context: WalletContext,
263
+ params: { plaintext: Uint8Array | string; recipientJwk: PublicJwk; contentType?: string },
264
+ ): Promise<string> {
265
+ const kid = params.recipientJwk.kid;
266
+ const header: Record<string, unknown> = {
267
+ alg: 'ML-KEM-768',
268
+ enc: 'A256GCM',
269
+ ...(params.contentType ? { cty: params.contentType } : {}),
270
+ ...(kid ? { kid } : {}),
271
+ };
272
+ const encodedHeader = encodeJsonBase64Url(header);
273
+
274
+ // ML-KEM-768: encapsulate derives a 32-byte CEK and produces a cipherText (1088 bytes)
275
+ // that the recipient can decapsulate with their secret key.
276
+ const publicKeyBytes = fromBase64Url(params.recipientJwk.x as string);
277
+ const { cipherText: encapsulatedKey, sharedSecret: cek } = ml_kem768.encapsulate(publicKeyBytes);
278
+
279
+ const iv = randomBytes(12);
280
+ const cipher = createCipheriv('aes-256-gcm', cek, iv);
281
+ cipher.setAAD(Buffer.from(encodedHeader, 'ascii'));
282
+ const ciphertext = Buffer.concat([cipher.update(normalizeBytes(params.plaintext)), cipher.final()]);
283
+ const authTag = cipher.getAuthTag();
284
+
285
+ return [
286
+ encodedHeader,
287
+ toBase64Url(encapsulatedKey),
288
+ toBase64Url(iv),
289
+ toBase64Url(ciphertext),
290
+ toBase64Url(authTag),
291
+ ].join('.');
292
+ }
293
+
294
+ public async decryptCompactJwe(jwe: string, context: WalletContext): Promise<Uint8Array> {
295
+ const parts = jwe.split('.');
296
+ if (parts.length !== 5) {
297
+ throw new Error('Invalid compact JWE: expected 5 parts.');
298
+ }
299
+ const [encodedHeader, encapsulatedKeyB64, ivB64, ciphertextB64, tagB64] = parts;
300
+ const pair = this.selectEncryptionKeyPair(context);
301
+
302
+ // Recover CEK by decapsulating the ML-KEM ciphertext with the secret key.
303
+ const cek = ml_kem768.decapsulate(fromBase64Url(encapsulatedKeyB64), pair.mlKemSecretKeyBytes);
304
+
305
+ const decipher = createDecipheriv('aes-256-gcm', cek, fromBase64Url(ivB64));
306
+ decipher.setAAD(Buffer.from(encodedHeader, 'ascii'));
307
+ decipher.setAuthTag(fromBase64Url(tagB64));
308
+ return Buffer.concat([decipher.update(fromBase64Url(ciphertextB64)), decipher.final()]);
309
+ }
310
+ }
@@ -0,0 +1,31 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { WalletContext } from '../types.js';
3
+ import { MemoryWalletProvider } from './memory-provider.js';
4
+
5
+ function stableWalletId(seed: string, context: WalletContext): string {
6
+ return createHash('sha256')
7
+ .update(`${seed}:${context.tenantId}:${context.jurisdiction}:${context.sector}:${context.walletId ?? 'default'}`)
8
+ .digest('base64url')
9
+ .slice(0, 24);
10
+ }
11
+
12
+ /**
13
+ * Dev-only provider.
14
+ *
15
+ * It stabilizes wallet identity selection from a seed, but it is not intended
16
+ * for production custody or HSM-backed key management.
17
+ */
18
+ export class SeedWalletProvider extends MemoryWalletProvider {
19
+ public readonly kind = 'seed' as const;
20
+
21
+ public constructor(private readonly seed: string) {
22
+ super();
23
+ }
24
+
25
+ protected override ensureKeyPair(context: WalletContext) {
26
+ return super.ensureKeyPair({
27
+ ...context,
28
+ walletId: stableWalletId(this.seed, context),
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,61 @@
1
+ export type WalletContext = {
2
+ tenantId: string;
3
+ jurisdiction: string;
4
+ sector: string;
5
+ walletId?: string;
6
+ };
7
+
8
+ export type PublicJwk = JsonWebKey & {
9
+ kid?: string;
10
+ alg?: string;
11
+ use?: string;
12
+ key_ops?: string[];
13
+ };
14
+
15
+ export type PrivateKeyRef = {
16
+ kid: string;
17
+ kind: 'managed';
18
+ algorithm: 'ES384' | 'ML-KEM-768' | 'RSA-OAEP-256/RS256';
19
+ };
20
+
21
+ export type SignOptions = {
22
+ keyId?: string;
23
+ algorithm?: 'ES384' | 'RS256';
24
+ };
25
+
26
+ export type VerifyOptions = {
27
+ algorithm?: 'ES384' | 'RS256';
28
+ };
29
+
30
+ export type EncryptOptions = {
31
+ keyId?: string;
32
+ /** Defaults to ML-KEM-768 when recipient JWK has kty:'OKP', crv:'ML-KEM-768'; falls back to RSA-OAEP-256 for legacy RSA JWKs. */
33
+ algorithm?: 'ML-KEM-768' | 'RSA-OAEP-256';
34
+ };
35
+
36
+ export type DecryptOptions = {
37
+ keyId?: string;
38
+ algorithm?: 'RSA-OAEP-256';
39
+ };
40
+
41
+ export type WalletProviderKind = 'mem' | 'seed' | 'external';
42
+
43
+ export type WalletInitOptions = {
44
+ contexts?: WalletContext[];
45
+ };
46
+
47
+ export type CompactJwsHeader = {
48
+ alg: string;
49
+ typ?: string;
50
+ kid?: string;
51
+ [key: string]: unknown;
52
+ };
53
+
54
+ export type CompactJweHeader = {
55
+ /** KEM algorithm. ML-KEM-768 (post-quantum) or RSA-OAEP-256 (legacy). */
56
+ alg: 'ML-KEM-768' | 'RSA-OAEP-256' | string;
57
+ enc: 'A256GCM' | 'A128GCM' | string;
58
+ cty?: string;
59
+ kid?: string;
60
+ [key: string]: unknown;
61
+ };