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.
- package/README.md +310 -0
- package/SDK_PARITY_MAP.md +120 -0
- package/TODO_PROMPT_NEXT_STEPS.md +185 -0
- package/artifacts/update-smart-wallet.js +1016 -0
- package/dist/builders.d.ts +12 -0
- package/dist/builders.js +17 -0
- package/dist/client.d.ts +333 -0
- package/dist/client.js +1229 -0
- package/dist/consent/pdfSignatureVerification.d.ts +18 -0
- package/dist/consent/pdfSignatureVerification.js +23 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/sdk/dataspace-wallet-sdk-node/MultiWalletClient.d.ts +9 -0
- package/dist/sdk/dataspace-wallet-sdk-node/MultiWalletClient.js +21 -0
- package/dist/sdk/dataspace-wallet-sdk-node/WalletClient.d.ts +26 -0
- package/dist/sdk/dataspace-wallet-sdk-node/WalletClient.js +36 -0
- package/dist/sdk/dataspace-wallet-sdk-node/index.d.ts +6 -0
- package/dist/sdk/dataspace-wallet-sdk-node/index.js +6 -0
- package/dist/sdk/dataspace-wallet-sdk-node/provider.d.ts +24 -0
- package/dist/sdk/dataspace-wallet-sdk-node/provider.js +1 -0
- package/dist/sdk/dataspace-wallet-sdk-node/providers/memory-provider.d.ts +41 -0
- package/dist/sdk/dataspace-wallet-sdk-node/providers/memory-provider.js +216 -0
- package/dist/sdk/dataspace-wallet-sdk-node/providers/seed-provider.d.ts +22 -0
- package/dist/sdk/dataspace-wallet-sdk-node/providers/seed-provider.js +28 -0
- package/dist/sdk/dataspace-wallet-sdk-node/types.d.ts +51 -0
- package/dist/sdk/dataspace-wallet-sdk-node/types.js +1 -0
- package/dist/types.d.ts +445 -0
- package/dist/types.js +1 -0
- package/docs/API.md +745 -0
- package/docs/DATA_MODEL_ALIGNMENT.md +31 -0
- package/docs/DATA_PLANES_SCOPE_MATRIX.md +51 -0
- package/docs/DEVELOPER_USE_CASES.md +253 -0
- package/docs/E2E_BOOTSTRAP.md +54 -0
- package/docs/TODO_SMART_EHR_COMPAT.md +58 -0
- package/examples/backend-pkce-auth.mjs +119 -0
- package/examples/conversion-upload.mjs +52 -0
- package/examples/e2e-bootstrap-tenant.mjs +126 -0
- package/examples/e2e-individual-flow.mjs +43 -0
- package/examples/host-activate-and-employee.mjs +75 -0
- package/package.json +26 -0
- package/src/builders.ts +28 -0
- package/src/client.ts +1626 -0
- package/src/consent/pdfSignatureVerification.ts +41 -0
- package/src/index.ts +8 -0
- package/src/sdk/dataspace-wallet-sdk-node/MultiWalletClient.ts +25 -0
- package/src/sdk/dataspace-wallet-sdk-node/WalletClient.ts +63 -0
- package/src/sdk/dataspace-wallet-sdk-node/index.ts +6 -0
- package/src/sdk/dataspace-wallet-sdk-node/provider.ts +44 -0
- package/src/sdk/dataspace-wallet-sdk-node/providers/memory-provider.ts +310 -0
- package/src/sdk/dataspace-wallet-sdk-node/providers/seed-provider.ts +31 -0
- package/src/sdk/dataspace-wallet-sdk-node/types.ts +61 -0
- package/src/types.ts +497 -0
- package/tests/client.test.mjs +892 -0
- package/tests/uc5-org-onboarding.flow.test.mjs +145 -0
- package/tests/uc5-subject-data.flow.test.mjs +198 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
DataspaceNodeClient,
|
|
5
|
+
MemoryWalletProvider,
|
|
6
|
+
MultiWalletClient,
|
|
7
|
+
createDidcommPlainMessage,
|
|
8
|
+
} from '../dist/index.js';
|
|
9
|
+
|
|
10
|
+
function jsonResponse(body, status = 200) {
|
|
11
|
+
return new Response(JSON.stringify(body), {
|
|
12
|
+
status,
|
|
13
|
+
headers: { 'content-type': 'application/json' },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('builds canonical v1 and host registry paths', () => {
|
|
18
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000' });
|
|
19
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
20
|
+
|
|
21
|
+
assert.equal(
|
|
22
|
+
client.v1Path(ctx, 'individual', 'org.hl7.fhir.api', 'Task', '_batch'),
|
|
23
|
+
'/acme/cds-ES/v1/health-care/individual/org.hl7.fhir.api/Task/_batch',
|
|
24
|
+
);
|
|
25
|
+
assert.equal(
|
|
26
|
+
client.hostRegistryPath({ jurisdiction: 'ES', sector: 'test-network' }, 'Organization', '_activate'),
|
|
27
|
+
'/host/cds-ES/v1/test-network/registry/org.schema/Organization/_activate',
|
|
28
|
+
);
|
|
29
|
+
assert.equal(
|
|
30
|
+
client.identityDeviceDcrPath(ctx),
|
|
31
|
+
'/host/cds-ES/v1/health-care/acme/identity/auth/_dcr',
|
|
32
|
+
);
|
|
33
|
+
assert.equal(
|
|
34
|
+
client.conversionUploadPath(ctx, 'excel-adapter', 'xlsx'),
|
|
35
|
+
'/acme/cds-ES/v1/health-care/conversion/excel-adapter/xlsx/_upload',
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('accepts an optional wallet provider without breaking client construction', () => {
|
|
40
|
+
const wallet = new MemoryWalletProvider();
|
|
41
|
+
const client = new DataspaceNodeClient({
|
|
42
|
+
baseUrl: 'http://localhost:3000',
|
|
43
|
+
wallet,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
assert.equal(client.getWallet(), wallet);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('wallet provider supports ES384 sign/verify, compact JWS, and ML-KEM-768 encrypt/decrypt roundtrips', async () => {
|
|
50
|
+
const provider = new MemoryWalletProvider();
|
|
51
|
+
const wallets = new MultiWalletClient(provider);
|
|
52
|
+
const context = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
53
|
+
const wallet = wallets.forContext(context);
|
|
54
|
+
const publicJwks = await wallet.getPublicJwks();
|
|
55
|
+
const signingJwk = publicJwks.find((jwk) => jwk.use === 'sig');
|
|
56
|
+
const encryptionJwk = publicJwks.find((jwk) => jwk.use === 'enc');
|
|
57
|
+
|
|
58
|
+
assert.ok(signingJwk);
|
|
59
|
+
assert.ok(encryptionJwk);
|
|
60
|
+
assert.equal(encryptionJwk.kty, 'OKP', 'encryption JWK must be OKP (ML-KEM)');
|
|
61
|
+
assert.equal(encryptionJwk.crv, 'ML-KEM-768');
|
|
62
|
+
assert.equal(encryptionJwk.alg, 'ML-KEM-768');
|
|
63
|
+
|
|
64
|
+
const signature = await wallet.sign('hello-wallet');
|
|
65
|
+
assert.equal(await wallet.verify('hello-wallet', signature, signingJwk), true);
|
|
66
|
+
assert.equal(await wallet.verify('tampered', signature, signingJwk), false);
|
|
67
|
+
|
|
68
|
+
const compactJws = await wallet.signCompactJws({
|
|
69
|
+
header: { typ: 'JWT', alg: 'ES384' },
|
|
70
|
+
claims: { sub: 'acme-service', aud: 'https://gw.example.com/token' },
|
|
71
|
+
});
|
|
72
|
+
const [encodedHeader, encodedClaims, encodedSignature] = compactJws.split('.');
|
|
73
|
+
assert.ok(encodedHeader);
|
|
74
|
+
assert.ok(encodedClaims);
|
|
75
|
+
assert.ok(encodedSignature);
|
|
76
|
+
assert.equal(
|
|
77
|
+
await wallet.verify(`${encodedHeader}.${encodedClaims}`, encodedSignature, signingJwk),
|
|
78
|
+
true,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const ciphertext = await wallet.encrypt('secret-payload', encryptionJwk);
|
|
82
|
+
const plaintext = await wallet.decrypt(ciphertext);
|
|
83
|
+
assert.equal(Buffer.from(plaintext).toString('utf8'), 'secret-payload');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('authenticateBackendPkceAndExchange resolves controller JWK from wallet when omitted', async () => {
|
|
87
|
+
const provider = new MemoryWalletProvider();
|
|
88
|
+
const client = new DataspaceNodeClient({
|
|
89
|
+
baseUrl: 'http://localhost:3000',
|
|
90
|
+
wallet: provider,
|
|
91
|
+
});
|
|
92
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
93
|
+
const [walletJwk] = await provider.getPublicJwks(ctx);
|
|
94
|
+
const calls = [];
|
|
95
|
+
const originalFetch = globalThis.fetch;
|
|
96
|
+
|
|
97
|
+
globalThis.fetch = async (url, options) => {
|
|
98
|
+
calls.push({ url: String(url), options });
|
|
99
|
+
|
|
100
|
+
switch (calls.length) {
|
|
101
|
+
case 1:
|
|
102
|
+
return jsonResponse({ accepted: true }, 202);
|
|
103
|
+
case 2:
|
|
104
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
105
|
+
case 3:
|
|
106
|
+
return jsonResponse({ accepted: true }, 202);
|
|
107
|
+
case 4:
|
|
108
|
+
return jsonResponse({ code: 'pkce-code-001' }, 200);
|
|
109
|
+
case 5:
|
|
110
|
+
return jsonResponse({ accepted: true }, 202);
|
|
111
|
+
case 6:
|
|
112
|
+
return jsonResponse({ id_token: 'id-token-001' }, 200);
|
|
113
|
+
case 7:
|
|
114
|
+
return jsonResponse({ accepted: true }, 202);
|
|
115
|
+
default:
|
|
116
|
+
return jsonResponse({
|
|
117
|
+
access_token: 'access-token-001',
|
|
118
|
+
token_type: 'Bearer',
|
|
119
|
+
scope: 'onboarding family-registration',
|
|
120
|
+
expires_in: 3600,
|
|
121
|
+
}, 200);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const auth = await client.authenticateBackendPkceAndExchange({
|
|
127
|
+
ctx,
|
|
128
|
+
apiKey: 'api-key-001',
|
|
129
|
+
scopes: ['onboarding', 'family-registration'],
|
|
130
|
+
endpointId: 'wallet-backed-auth',
|
|
131
|
+
codeVerifier: 'verifier-001',
|
|
132
|
+
pollOptions: { timeoutMs: 5000, intervalMs: 1 },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
assert.equal(auth.status, 'fetched');
|
|
136
|
+
assert.equal(auth.accessToken, 'access-token-001');
|
|
137
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_dcr');
|
|
138
|
+
|
|
139
|
+
const dcrPayload = JSON.parse(calls[0].options.body);
|
|
140
|
+
assert.deepEqual(dcrPayload.meta.jws.protected.jwk, walletJwk);
|
|
141
|
+
} finally {
|
|
142
|
+
globalThis.fetch = originalFetch;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('authenticateBackendSmartStandard signs a private_key_jwt assertion with wallet ES384 key', async () => {
|
|
147
|
+
const provider = new MemoryWalletProvider();
|
|
148
|
+
const walletContext = { tenantId: 'service-a', jurisdiction: 'ES', sector: 'health-care' };
|
|
149
|
+
const [walletJwk] = await provider.getPublicJwks(walletContext);
|
|
150
|
+
const client = new DataspaceNodeClient({
|
|
151
|
+
baseUrl: 'http://localhost:3000',
|
|
152
|
+
wallet: provider,
|
|
153
|
+
});
|
|
154
|
+
const calls = [];
|
|
155
|
+
const originalFetch = globalThis.fetch;
|
|
156
|
+
|
|
157
|
+
globalThis.fetch = async (url, options) => {
|
|
158
|
+
calls.push({ url: String(url), options });
|
|
159
|
+
return jsonResponse({
|
|
160
|
+
access_token: 'smart-token-001',
|
|
161
|
+
token_type: 'Bearer',
|
|
162
|
+
scope: 'system/*.read system/*.write',
|
|
163
|
+
expires_in: 3600,
|
|
164
|
+
}, 200);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await client.authenticateBackendSmartStandard({
|
|
169
|
+
clientId: 'service-a',
|
|
170
|
+
scopes: ['system/*.read', 'system/*.write'],
|
|
171
|
+
walletContext,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
assert.equal(result.status, 'fetched');
|
|
175
|
+
assert.equal(result.profile, 'smart-backend.v1');
|
|
176
|
+
assert.equal(calls.length, 1);
|
|
177
|
+
assert.equal(calls[0].url, 'http://localhost:3000/token');
|
|
178
|
+
|
|
179
|
+
const tokenRequest = JSON.parse(calls[0].options.body);
|
|
180
|
+
assert.equal(tokenRequest.client_id, 'service-a');
|
|
181
|
+
assert.equal(tokenRequest.grant_type, 'client_credentials');
|
|
182
|
+
assert.ok(typeof tokenRequest.client_assertion === 'string');
|
|
183
|
+
|
|
184
|
+
const [encodedHeader, encodedClaims, encodedSignature] = tokenRequest.client_assertion.split('.');
|
|
185
|
+
const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString('utf8'));
|
|
186
|
+
const claims = JSON.parse(Buffer.from(encodedClaims, 'base64url').toString('utf8'));
|
|
187
|
+
assert.equal(header.alg, 'ES384');
|
|
188
|
+
assert.equal(header.kid, walletJwk.kid);
|
|
189
|
+
assert.equal(claims.iss, 'service-a');
|
|
190
|
+
assert.equal(claims.sub, 'service-a');
|
|
191
|
+
assert.equal(claims.aud, 'http://localhost:3000/token');
|
|
192
|
+
assert.equal(await provider.verify(`${encodedHeader}.${encodedClaims}`, encodedSignature, walletJwk), true);
|
|
193
|
+
} finally {
|
|
194
|
+
globalThis.fetch = originalFetch;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('submitAndPoll uses DIDComm plain submit and async poll until non-202', async () => {
|
|
199
|
+
const calls = [];
|
|
200
|
+
const originalFetch = globalThis.fetch;
|
|
201
|
+
|
|
202
|
+
globalThis.fetch = async (url, options) => {
|
|
203
|
+
calls.push({ url: String(url), options });
|
|
204
|
+
if (calls.length === 1) {
|
|
205
|
+
return jsonResponse({ accepted: true }, 202);
|
|
206
|
+
}
|
|
207
|
+
if (calls.length === 2) {
|
|
208
|
+
return jsonResponse({ thid: 'thid-001', status: 'PENDING' }, 202);
|
|
209
|
+
}
|
|
210
|
+
return jsonResponse({ thid: 'thid-001', status: 'COMPLETED', body: { ok: true } }, 200);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'demo-token' });
|
|
215
|
+
const payload = createDidcommPlainMessage({
|
|
216
|
+
iss: 'issuer',
|
|
217
|
+
aud: 'audience',
|
|
218
|
+
thid: 'thid-001',
|
|
219
|
+
body: { data: [] },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const result = await client.submitAndPoll(
|
|
223
|
+
'/host/cds-ES/v1/test-network/registry/org.schema/Organization/_batch',
|
|
224
|
+
'/host/cds-ES/v1/test-network/registry/org.schema/Organization/_batch-response',
|
|
225
|
+
payload,
|
|
226
|
+
{ timeoutMs: 5000, intervalMs: 1 },
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
assert.equal(result.submit.status, 202);
|
|
230
|
+
assert.equal(result.poll.status, 200);
|
|
231
|
+
assert.equal(result.poll.attempts, 2);
|
|
232
|
+
|
|
233
|
+
assert.equal(calls[0].options.method, 'POST');
|
|
234
|
+
assert.equal(calls[0].options.headers['Content-Type'], 'application/didcomm-plaintext+json');
|
|
235
|
+
assert.equal(calls[0].options.headers.Authorization, 'Bearer demo-token');
|
|
236
|
+
|
|
237
|
+
assert.equal(calls[1].options.headers['Content-Type'], 'application/json');
|
|
238
|
+
assert.equal(calls[2].options.headers['Content-Type'], 'application/json');
|
|
239
|
+
} finally {
|
|
240
|
+
globalThis.fetch = originalFetch;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('activateOrganizationInGatewayFromIcaProof submits activation and polls to completion', async () => {
|
|
245
|
+
const calls = [];
|
|
246
|
+
const originalFetch = globalThis.fetch;
|
|
247
|
+
|
|
248
|
+
globalThis.fetch = async (url, options) => {
|
|
249
|
+
calls.push({ url: String(url), options });
|
|
250
|
+
if (calls.length === 1) {
|
|
251
|
+
return jsonResponse({ accepted: true }, 202);
|
|
252
|
+
}
|
|
253
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'id-token-001' });
|
|
258
|
+
const result = await client.activateOrganizationInGatewayFromIcaProof(
|
|
259
|
+
{ jurisdiction: 'ES', sector: 'test' },
|
|
260
|
+
{
|
|
261
|
+
vpToken: 'vp-token-001',
|
|
262
|
+
organizationVc: 'org-vc-jwt',
|
|
263
|
+
legalRepresentativeVc: 'legal-vc-jwt',
|
|
264
|
+
},
|
|
265
|
+
{ timeoutMs: 5000, intervalMs: 1 },
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
assert.equal(result.submit.status, 202);
|
|
269
|
+
assert.equal(result.poll.status, 200);
|
|
270
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/test/registry/org.schema/Organization/_activate');
|
|
271
|
+
} finally {
|
|
272
|
+
globalThis.fetch = originalFetch;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('activateEmployeeDeviceWithActivationCode performs exchange then DCR', async () => {
|
|
277
|
+
const calls = [];
|
|
278
|
+
const originalFetch = globalThis.fetch;
|
|
279
|
+
|
|
280
|
+
globalThis.fetch = async (url, options) => {
|
|
281
|
+
calls.push({ url: String(url), options });
|
|
282
|
+
switch (calls.length) {
|
|
283
|
+
case 1:
|
|
284
|
+
return jsonResponse({ accepted: true }, 202); // exchange submit
|
|
285
|
+
case 2:
|
|
286
|
+
return jsonResponse({ body: { initial_access_token: 'initial-access-001' } }, 200); // exchange poll
|
|
287
|
+
case 3:
|
|
288
|
+
return jsonResponse({ accepted: true }, 202); // dcr submit
|
|
289
|
+
default:
|
|
290
|
+
return jsonResponse({ body: { client_id: 'did:web:device-001' } }, 200); // dcr poll
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000' });
|
|
296
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
297
|
+
const result = await client.activateEmployeeDeviceWithActivationCode(ctx, {
|
|
298
|
+
activationCode: 'ACT-001',
|
|
299
|
+
idToken: 'user-id-token-001',
|
|
300
|
+
dcrPayload: {
|
|
301
|
+
application_type: 'web',
|
|
302
|
+
client_name: 'Acme Portal',
|
|
303
|
+
jwks: { keys: [{ kid: 'dev-1', kty: 'EC' }] },
|
|
304
|
+
redirect_uris: ['https://app.example.com/callback'],
|
|
305
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
|
306
|
+
},
|
|
307
|
+
pollOptions: { timeoutMs: 5000, intervalMs: 1 },
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
assert.equal(result.initialAccessToken, 'initial-access-001');
|
|
311
|
+
assert.equal(result.exchange.poll.status, 200);
|
|
312
|
+
assert.equal(result.dcr.poll.status, 200);
|
|
313
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_exchange');
|
|
314
|
+
assert.equal(calls[2].url, 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_dcr');
|
|
315
|
+
assert.equal(calls[2].options.headers.Authorization, 'Bearer initial-access-001');
|
|
316
|
+
} finally {
|
|
317
|
+
globalThis.fetch = originalFetch;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('createOrganizationEmployee submits entity Employee batch and polls', async () => {
|
|
322
|
+
const calls = [];
|
|
323
|
+
const originalFetch = globalThis.fetch;
|
|
324
|
+
|
|
325
|
+
globalThis.fetch = async (url, options) => {
|
|
326
|
+
calls.push({ url: String(url), options });
|
|
327
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
328
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'admin-token' });
|
|
333
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
334
|
+
const result = await client.createOrganizationEmployee(
|
|
335
|
+
ctx,
|
|
336
|
+
{
|
|
337
|
+
employeeClaims: {
|
|
338
|
+
'@context': 'org.schema',
|
|
339
|
+
'org.schema.Person.email': 'doctor1@acme.org',
|
|
340
|
+
'org.schema.Person.hasOccupation': 'ISCO-08|2211',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{ timeoutMs: 5000, intervalMs: 1 },
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
assert.equal(result.submit.status, 202);
|
|
347
|
+
assert.equal(result.poll.status, 200);
|
|
348
|
+
assert.equal(
|
|
349
|
+
calls[0].url,
|
|
350
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/entity/org.schema/Employee/_batch',
|
|
351
|
+
);
|
|
352
|
+
} finally {
|
|
353
|
+
globalThis.fetch = originalFetch;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('bootstrapSubjectOrganizationIndex runs registration and confirmation flow', async () => {
|
|
358
|
+
const calls = [];
|
|
359
|
+
const originalFetch = globalThis.fetch;
|
|
360
|
+
|
|
361
|
+
globalThis.fetch = async (url, options) => {
|
|
362
|
+
calls.push({ url: String(url), options });
|
|
363
|
+
if (calls.length === 1 || calls.length === 3) return jsonResponse({ accepted: true }, 202);
|
|
364
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'controller-token' });
|
|
369
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
370
|
+
const result = await client.bootstrapSubjectOrganizationIndex(ctx, {
|
|
371
|
+
registrationPayload: { body: { data: [{ type: 'Family-registration-form-v1.0' }] } },
|
|
372
|
+
confirmationPayload: { body: { data: [{ type: 'Family-order-request-v1.0' }] } },
|
|
373
|
+
pollOptions: { timeoutMs: 5000, intervalMs: 1 },
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
assert.equal(result.registration.poll.status, 200);
|
|
377
|
+
assert.equal(result.confirmation?.poll.status, 200);
|
|
378
|
+
assert.equal(
|
|
379
|
+
calls[0].url,
|
|
380
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch',
|
|
381
|
+
);
|
|
382
|
+
assert.equal(
|
|
383
|
+
calls[2].url,
|
|
384
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Order/_batch',
|
|
385
|
+
);
|
|
386
|
+
} finally {
|
|
387
|
+
globalThis.fetch = originalFetch;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('importIpsOrFhirAndUpdateIndex submits Composition in individual section', async () => {
|
|
392
|
+
const calls = [];
|
|
393
|
+
const originalFetch = globalThis.fetch;
|
|
394
|
+
|
|
395
|
+
globalThis.fetch = async (url, options) => {
|
|
396
|
+
calls.push({ url: String(url), options });
|
|
397
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
398
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'smart-token' });
|
|
403
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
404
|
+
const result = await client.importIpsOrFhirAndUpdateIndex(ctx, {
|
|
405
|
+
compositionPayload: { body: { data: [{ type: 'Composition-import-request-v1.0' }] } },
|
|
406
|
+
format: 'r4',
|
|
407
|
+
pollOptions: { timeoutMs: 5000, intervalMs: 1 },
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
assert.equal(result.poll.status, 200);
|
|
411
|
+
assert.equal(
|
|
412
|
+
calls[0].url,
|
|
413
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.hl7.fhir.r4/Composition/_batch',
|
|
414
|
+
);
|
|
415
|
+
} finally {
|
|
416
|
+
globalThis.fetch = originalFetch;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('grant professional access submits consent via Consent batch path', async () => {
|
|
421
|
+
const calls = [];
|
|
422
|
+
const originalFetch = globalThis.fetch;
|
|
423
|
+
|
|
424
|
+
globalThis.fetch = async (url, options) => {
|
|
425
|
+
calls.push({ url: String(url), options });
|
|
426
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
427
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'controller-token' });
|
|
432
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
433
|
+
const result = await client.submitAndPoll(
|
|
434
|
+
client.individualConsentR4BatchPath(ctx),
|
|
435
|
+
client.individualConsentR4PollPath(ctx),
|
|
436
|
+
{ thid: 'consent-thread-001', body: { data: [{ type: 'Consent-grant-request-v1.0' }] } },
|
|
437
|
+
{ timeoutMs: 5000, intervalMs: 1 },
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
assert.equal(result.poll.status, 200);
|
|
441
|
+
assert.equal(
|
|
442
|
+
calls[0].url,
|
|
443
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.hl7.fhir.r4/Consent/_batch',
|
|
444
|
+
);
|
|
445
|
+
} finally {
|
|
446
|
+
globalThis.fetch = originalFetch;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('grantProfessionalAccessSimple builds canonical consent claims from basic input', async () => {
|
|
451
|
+
const calls = [];
|
|
452
|
+
const originalFetch = globalThis.fetch;
|
|
453
|
+
|
|
454
|
+
globalThis.fetch = async (url, options) => {
|
|
455
|
+
calls.push({ url: String(url), options });
|
|
456
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
457
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'controller-token' });
|
|
462
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
463
|
+
const result = await client.grantProfessionalAccessSimple(ctx, {
|
|
464
|
+
subjectPhone: '+34 600 111 222',
|
|
465
|
+
subjectGivenName: 'Ana Maria',
|
|
466
|
+
actor: { organizationUrl: 'https://hospital.example.com/staff' },
|
|
467
|
+
actorRole: 'Practitioner',
|
|
468
|
+
purpose: 'TREAT',
|
|
469
|
+
actions: ['access', 'read'],
|
|
470
|
+
pollOptions: { timeoutMs: 5000, intervalMs: 1 },
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
assert.equal(result.consent.poll.status, 200);
|
|
474
|
+
assert.equal(result.actorIdentifier, 'did:web:hospital.example.com');
|
|
475
|
+
assert.equal(result.subjectIdentifier, 'urn:person:phone:+34600111222:given:ana-maria');
|
|
476
|
+
assert.equal(result.consentClaims['Consent.actor-role'], 'Practitioner');
|
|
477
|
+
assert.equal(result.consentClaims['Consent.action'], 'access,read');
|
|
478
|
+
assert.equal(typeof result.consentClaims['@id'], 'string');
|
|
479
|
+
assert.equal(result.claimsCid, result.consentClaims['@id']);
|
|
480
|
+
assert.equal(
|
|
481
|
+
calls[0].url,
|
|
482
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.hl7.fhir.r4/Consent/_batch',
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const submitPayload = JSON.parse(calls[0].options.body);
|
|
486
|
+
assert.equal(submitPayload.body.data[0].type, 'Consent-grant-request-v1.0');
|
|
487
|
+
assert.equal(
|
|
488
|
+
submitPayload.body.data[0].meta.claims['Consent.actor-identifier'],
|
|
489
|
+
'did:web:hospital.example.com',
|
|
490
|
+
);
|
|
491
|
+
} finally {
|
|
492
|
+
globalThis.fetch = originalFetch;
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('requestSmartToken exchanges token in separate step', async () => {
|
|
497
|
+
const calls = [];
|
|
498
|
+
const originalFetch = globalThis.fetch;
|
|
499
|
+
|
|
500
|
+
globalThis.fetch = async (url, options) => {
|
|
501
|
+
calls.push({ url: String(url), options });
|
|
502
|
+
return jsonResponse({
|
|
503
|
+
access_token: 'delegated-token-001',
|
|
504
|
+
token_type: 'Bearer',
|
|
505
|
+
scope: 'employee.healthcare.getIndexComposition',
|
|
506
|
+
}, 200);
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'professional-id-token' });
|
|
511
|
+
const result = await client.requestSmartToken({
|
|
512
|
+
endpointId: 'doctor-1',
|
|
513
|
+
scopes: ['employee.healthcare.getIndexComposition'],
|
|
514
|
+
exchangePayload: { grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange' },
|
|
515
|
+
path: '/token',
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
assert.equal(result.status, 'fetched');
|
|
519
|
+
assert.equal(result.accessToken, 'delegated-token-001');
|
|
520
|
+
assert.equal(calls[0].url, 'http://localhost:3000/token');
|
|
521
|
+
} finally {
|
|
522
|
+
globalThis.fetch = originalFetch;
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('generateDigitalTwinFromSubjectData submits digital twin Composition', async () => {
|
|
527
|
+
const calls = [];
|
|
528
|
+
const originalFetch = globalThis.fetch;
|
|
529
|
+
|
|
530
|
+
globalThis.fetch = async (url, options) => {
|
|
531
|
+
calls.push({ url: String(url), options });
|
|
532
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
533
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'smart-token' });
|
|
538
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
539
|
+
const result = await client.generateDigitalTwinFromSubjectData(ctx, {
|
|
540
|
+
compositionPayload: { body: { data: [{ type: 'DigitalTwin-composition-request-v1.0' }] } },
|
|
541
|
+
format: 'r4',
|
|
542
|
+
pollOptions: { timeoutMs: 5000, intervalMs: 1 },
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
assert.equal(result.poll.status, 200);
|
|
546
|
+
assert.equal(
|
|
547
|
+
calls[0].url,
|
|
548
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/digitaltwin/org.hl7.fhir.r4/Composition/_batch',
|
|
549
|
+
);
|
|
550
|
+
} finally {
|
|
551
|
+
globalThis.fetch = originalFetch;
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test('createPhoneReminderTasks builds canonical Task payload with resource.meta.claims and polls to completion', async () => {
|
|
556
|
+
const calls = [];
|
|
557
|
+
const originalFetch = globalThis.fetch;
|
|
558
|
+
|
|
559
|
+
globalThis.fetch = async (url, options) => {
|
|
560
|
+
calls.push({ url: String(url), options });
|
|
561
|
+
if (calls.length === 1) {
|
|
562
|
+
return jsonResponse({ accepted: true }, 202);
|
|
563
|
+
}
|
|
564
|
+
return jsonResponse({ thid: 'task-reminder-001', status: 'COMPLETED' }, 200);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000' });
|
|
569
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
570
|
+
const result = await client.createPhoneReminderTasks(
|
|
571
|
+
ctx,
|
|
572
|
+
{
|
|
573
|
+
windows: [{ offsetMinutes: 60, remindAt: '2099-12-31T09:30:00.000Z' }],
|
|
574
|
+
locale: 'es-ES',
|
|
575
|
+
notificationPhone: '+34111222333',
|
|
576
|
+
controllerPhone: '+34111222333',
|
|
577
|
+
subjectRef: 'Person/patient-001',
|
|
578
|
+
ownerRef: 'RelatedPerson/controller-001',
|
|
579
|
+
focusRef: 'Appointment/appt-001',
|
|
580
|
+
subjectDisplay: 'Ana',
|
|
581
|
+
reminderSummary: 'Cita 2099-12-31 10:30',
|
|
582
|
+
description: 'Medication reminder call',
|
|
583
|
+
},
|
|
584
|
+
{ timeoutMs: 5000, intervalMs: 1 },
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
assert.equal(result.submit.status, 202);
|
|
588
|
+
assert.equal(result.poll.status, 200);
|
|
589
|
+
assert.equal(calls.length, 2);
|
|
590
|
+
assert.equal(
|
|
591
|
+
calls[0].url,
|
|
592
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.hl7.fhir.api/Task/_batch',
|
|
593
|
+
);
|
|
594
|
+
assert.equal(
|
|
595
|
+
calls[1].url,
|
|
596
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.hl7.fhir.api/Task/_batch-response',
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const payload = JSON.parse(calls[0].options.body);
|
|
600
|
+
const entry = payload?.body?.data?.[0];
|
|
601
|
+
assert.ok(entry);
|
|
602
|
+
assert.equal(entry.type, 'Task');
|
|
603
|
+
assert.ok(entry.resource?.meta?.claims);
|
|
604
|
+
assert.equal(entry.meta, undefined, 'entry.meta should not be used for task claims');
|
|
605
|
+
|
|
606
|
+
const claims = entry.resource.meta.claims;
|
|
607
|
+
assert.equal(claims['@context'], 'org.hl7.fhir.api');
|
|
608
|
+
assert.equal(claims.status, 'scheduled');
|
|
609
|
+
assert.equal(claims.subject, 'Person/patient-001');
|
|
610
|
+
assert.equal(claims.owner, 'RelatedPerson/controller-001');
|
|
611
|
+
assert.equal(claims.focus, 'Appointment/appt-001');
|
|
612
|
+
assert.equal(claims['execution-period-start'], '2099-12-31T09:30:00.000Z');
|
|
613
|
+
assert.equal(claims['trigger-type'], 'phone-call');
|
|
614
|
+
assert.equal(claims.channel, 'phone');
|
|
615
|
+
assert.equal(claims['timing-repeat-offset'], '60');
|
|
616
|
+
assert.equal(claims['max-attempts'], '3');
|
|
617
|
+
assert.equal(claims['based-on-display'], 'Cita 2099-12-31 10:30');
|
|
618
|
+
assert.equal(entry.resource.description, 'Medication reminder call');
|
|
619
|
+
} finally {
|
|
620
|
+
globalThis.fetch = originalFetch;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test('uploadConversionFile sends multipart with file and fields', async () => {
|
|
625
|
+
const calls = [];
|
|
626
|
+
const originalFetch = globalThis.fetch;
|
|
627
|
+
|
|
628
|
+
globalThis.fetch = async (url, options) => {
|
|
629
|
+
calls.push({ url: String(url), options });
|
|
630
|
+
return jsonResponse({ thid: 'upload-thid-001', status: 'queued' }, 202);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000' });
|
|
635
|
+
const submit = await client.uploadConversionFile({
|
|
636
|
+
path: '/acme/cds-ES/v1/animal-care/conversion/excel-adapter/xlsx/_upload',
|
|
637
|
+
fileName: 'input.xlsx',
|
|
638
|
+
fileContent: new Uint8Array([0x01, 0x02, 0x03]),
|
|
639
|
+
fields: { mode: 'didcomm-plain' },
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
assert.equal(submit.status, 202);
|
|
643
|
+
assert.equal(calls.length, 1);
|
|
644
|
+
assert.equal(calls[0].options.method, 'POST');
|
|
645
|
+
|
|
646
|
+
const body = calls[0].options.body;
|
|
647
|
+
assert.ok(body instanceof FormData);
|
|
648
|
+
|
|
649
|
+
const modeValues = body.getAll('mode');
|
|
650
|
+
assert.equal(modeValues.length, 1);
|
|
651
|
+
assert.equal(modeValues[0], 'didcomm-plain');
|
|
652
|
+
|
|
653
|
+
const filePart = body.get('file');
|
|
654
|
+
assert.ok(filePart instanceof Blob);
|
|
655
|
+
} finally {
|
|
656
|
+
globalThis.fetch = originalFetch;
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('wallet signDetachedJws produces header..signature compact form verifiable against payload', async () => {
|
|
661
|
+
const provider = new MemoryWalletProvider();
|
|
662
|
+
const wallets = new MultiWalletClient(provider);
|
|
663
|
+
const context = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
664
|
+
const wallet = wallets.forContext(context);
|
|
665
|
+
|
|
666
|
+
const payload = Buffer.from(JSON.stringify({ sub: 'patient-001', vc: { type: ['VerifiableCredential'] } }));
|
|
667
|
+
const detachedJws = await wallet.signDetachedJws({
|
|
668
|
+
header: { typ: 'JWT', alg: 'ES384' },
|
|
669
|
+
payload,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const parts = detachedJws.split('.');
|
|
673
|
+
assert.equal(parts.length, 3);
|
|
674
|
+
assert.ok(parts[0].length > 0, 'header part must be non-empty');
|
|
675
|
+
assert.equal(parts[1], '', 'payload part must be empty (detached)');
|
|
676
|
+
assert.ok(parts[2].length > 0, 'signature part must be non-empty');
|
|
677
|
+
|
|
678
|
+
// Verify: reconstruct the signing input and check with the sig key
|
|
679
|
+
const publicJwks = await wallet.getPublicJwks();
|
|
680
|
+
const signingJwk = publicJwks.find((jwk) => jwk.use === 'sig');
|
|
681
|
+
const [encodedHeader, , encodedSignature] = parts;
|
|
682
|
+
const signingInput = Buffer.concat([
|
|
683
|
+
Buffer.from(`${encodedHeader}.`, 'ascii'),
|
|
684
|
+
payload,
|
|
685
|
+
]);
|
|
686
|
+
assert.equal(await wallet.verify(signingInput, encodedSignature, signingJwk), true);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test('wallet buildCompactJwe / decryptCompactJwe roundtrip with ML-KEM-768 + A256GCM', async () => {
|
|
690
|
+
const provider = new MemoryWalletProvider();
|
|
691
|
+
const wallets = new MultiWalletClient(provider);
|
|
692
|
+
const context = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
693
|
+
const wallet = wallets.forContext(context);
|
|
694
|
+
|
|
695
|
+
const publicJwks = await wallet.getPublicJwks();
|
|
696
|
+
const encryptionJwk = publicJwks.find((jwk) => jwk.use === 'enc');
|
|
697
|
+
assert.ok(encryptionJwk, 'must have enc JWK');
|
|
698
|
+
assert.equal(encryptionJwk.kty, 'OKP');
|
|
699
|
+
assert.equal(encryptionJwk.crv, 'ML-KEM-768');
|
|
700
|
+
assert.equal(encryptionJwk.alg, 'ML-KEM-768');
|
|
701
|
+
|
|
702
|
+
const plaintext = 'signed.compact.jws.token.here';
|
|
703
|
+
const jwe = await wallet.buildCompactJwe({ plaintext, recipientJwk: encryptionJwk, contentType: 'JWS' });
|
|
704
|
+
|
|
705
|
+
// JWE must be 5 parts
|
|
706
|
+
const parts = jwe.split('.');
|
|
707
|
+
assert.equal(parts.length, 5);
|
|
708
|
+
|
|
709
|
+
// Decode and check header
|
|
710
|
+
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8'));
|
|
711
|
+
assert.equal(header.alg, 'ML-KEM-768');
|
|
712
|
+
assert.equal(header.enc, 'A256GCM');
|
|
713
|
+
assert.equal(header.cty, 'JWS');
|
|
714
|
+
assert.equal(header.kid, encryptionJwk.kid);
|
|
715
|
+
|
|
716
|
+
// Decrypt and check plaintext
|
|
717
|
+
const decrypted = await wallet.decryptCompactJwe(jwe);
|
|
718
|
+
assert.equal(Buffer.from(decrypted).toString('utf8'), plaintext);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('submitBatchEncrypted posts compact JWE with content-type application/didcomm-encrypted+json', async () => {
|
|
722
|
+
const provider = new MemoryWalletProvider();
|
|
723
|
+
const walletContext = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
724
|
+
const client = new DataspaceNodeClient({
|
|
725
|
+
baseUrl: 'http://localhost:3000',
|
|
726
|
+
wallet: provider,
|
|
727
|
+
bearerToken: 'demo-token',
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Use wallet's own enc key as the "recipient" for the test
|
|
731
|
+
const publicJwks = await provider.getPublicJwks(walletContext);
|
|
732
|
+
const encryptionJwk = publicJwks.find((jwk) => jwk.use === 'enc');
|
|
733
|
+
assert.ok(encryptionJwk);
|
|
734
|
+
|
|
735
|
+
const calls = [];
|
|
736
|
+
const originalFetch = globalThis.fetch;
|
|
737
|
+
|
|
738
|
+
globalThis.fetch = async (url, options) => {
|
|
739
|
+
calls.push({ url: String(url), options });
|
|
740
|
+
return new Response(JSON.stringify({ accepted: true }), {
|
|
741
|
+
status: 202,
|
|
742
|
+
headers: { 'content-type': 'application/json' },
|
|
743
|
+
});
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
try {
|
|
747
|
+
const payload = { thid: 'enc-thid-001', body: { data: [] } };
|
|
748
|
+
const result = await client.submitBatchEncrypted(
|
|
749
|
+
'/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch',
|
|
750
|
+
payload,
|
|
751
|
+
encryptionJwk,
|
|
752
|
+
walletContext,
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
assert.equal(result.status, 202);
|
|
756
|
+
assert.equal(calls.length, 1);
|
|
757
|
+
assert.equal(
|
|
758
|
+
calls[0].url,
|
|
759
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch',
|
|
760
|
+
);
|
|
761
|
+
assert.equal(calls[0].options.method, 'POST');
|
|
762
|
+
assert.equal(calls[0].options.headers['Content-Type'], 'application/didcomm-encrypted+json');
|
|
763
|
+
assert.equal(calls[0].options.headers.Authorization, 'Bearer demo-token');
|
|
764
|
+
|
|
765
|
+
// Body must be a compact JWE string (5 base64url parts)
|
|
766
|
+
const body = calls[0].options.body;
|
|
767
|
+
assert.equal(typeof body, 'string');
|
|
768
|
+
const parts = body.split('.');
|
|
769
|
+
assert.equal(parts.length, 5);
|
|
770
|
+
|
|
771
|
+
// Decrypt and verify inner JWS contains the original payload
|
|
772
|
+
const wallets = new MultiWalletClient(provider);
|
|
773
|
+
const wallet = wallets.forContext(walletContext);
|
|
774
|
+
const decryptedBytes = await wallet.decryptCompactJwe(body);
|
|
775
|
+
const jws = Buffer.from(decryptedBytes).toString('utf8');
|
|
776
|
+
const jwsParts = jws.split('.');
|
|
777
|
+
assert.equal(jwsParts.length, 3);
|
|
778
|
+
const innerClaims = JSON.parse(Buffer.from(jwsParts[1], 'base64url').toString('utf8'));
|
|
779
|
+
assert.equal(innerClaims.thid, 'enc-thid-001');
|
|
780
|
+
} finally {
|
|
781
|
+
globalThis.fetch = originalFetch;
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
test('individualFamilyOrganizationSearchPath and BatchPath return canonical paths', () => {
|
|
785
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000' });
|
|
786
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
787
|
+
|
|
788
|
+
assert.equal(
|
|
789
|
+
client.individualFamilyOrganizationSearchPath(ctx),
|
|
790
|
+
'/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_search',
|
|
791
|
+
);
|
|
792
|
+
assert.equal(
|
|
793
|
+
client.individualFamilyOrganizationSearchPollPath(ctx),
|
|
794
|
+
'/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_search-response',
|
|
795
|
+
);
|
|
796
|
+
assert.equal(
|
|
797
|
+
client.individualFamilyOrganizationBatchPath(ctx),
|
|
798
|
+
'/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch',
|
|
799
|
+
);
|
|
800
|
+
assert.equal(
|
|
801
|
+
client.individualFamilyOrganizationPollPath(ctx),
|
|
802
|
+
'/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch-response',
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test('searchFamilyOrganization returns FamilyOrganizationSummary for already_exists', async () => {
|
|
807
|
+
const calls = [];
|
|
808
|
+
const originalFetch = globalThis.fetch;
|
|
809
|
+
|
|
810
|
+
const pollBody = {
|
|
811
|
+
body: {
|
|
812
|
+
data: [{
|
|
813
|
+
type: 'Family-search-result-v1.0',
|
|
814
|
+
meta: {
|
|
815
|
+
claims: {
|
|
816
|
+
'org.schema.FamilyRegistration.status': 'already_exists',
|
|
817
|
+
'org.schema.Organization.alternateName': 'Ana',
|
|
818
|
+
'org.schema.Organization.owner.telephone': '+34600000001',
|
|
819
|
+
'org.schema.Organization.foundingDate': '2010-05-20',
|
|
820
|
+
'org.schema.Offer.identifier': 'offer-uuid-001',
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
resource: { id: 'org-uuid-001' },
|
|
824
|
+
}],
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
globalThis.fetch = async (url, options) => {
|
|
829
|
+
calls.push({ url: String(url), options });
|
|
830
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
831
|
+
return jsonResponse(pollBody, 200);
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'tok' });
|
|
836
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
837
|
+
const result = await client.searchFamilyOrganization(
|
|
838
|
+
ctx,
|
|
839
|
+
{ controllerPhone: '+34600000001', usualname: 'Ana', birthDate: '2010-05-20' },
|
|
840
|
+
{ timeoutMs: 5000, intervalMs: 1 },
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
assert.ok(result, 'result should not be null');
|
|
844
|
+
assert.equal(result.status, 'already_exists');
|
|
845
|
+
assert.equal(result.organizationId, 'org-uuid-001');
|
|
846
|
+
assert.equal(result.offerId, 'offer-uuid-001');
|
|
847
|
+
assert.equal(result.subjectInfo?.nickname, 'Ana');
|
|
848
|
+
assert.equal(result.subjectInfo?.telephone, '+34600000001');
|
|
849
|
+
assert.equal(result.subjectInfo?.birthDate, '2010-05-20');
|
|
850
|
+
assert.equal(
|
|
851
|
+
calls[0].url,
|
|
852
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_search',
|
|
853
|
+
);
|
|
854
|
+
assert.equal(
|
|
855
|
+
calls[1].url,
|
|
856
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_search-response',
|
|
857
|
+
);
|
|
858
|
+
} finally {
|
|
859
|
+
globalThis.fetch = originalFetch;
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test('searchFamilyOrganization returns null for not_found status', async () => {
|
|
864
|
+
const calls = [];
|
|
865
|
+
const originalFetch = globalThis.fetch;
|
|
866
|
+
|
|
867
|
+
globalThis.fetch = async (url, options) => {
|
|
868
|
+
calls.push({ url: String(url), options });
|
|
869
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
870
|
+
return jsonResponse({
|
|
871
|
+
body: {
|
|
872
|
+
data: [{
|
|
873
|
+
type: 'Family-search-result-v1.0',
|
|
874
|
+
meta: { claims: { 'org.schema.FamilyRegistration.status': 'not_found' } },
|
|
875
|
+
}],
|
|
876
|
+
},
|
|
877
|
+
}, 200);
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'tok' });
|
|
882
|
+
const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
|
|
883
|
+
const result = await client.searchFamilyOrganization(
|
|
884
|
+
ctx,
|
|
885
|
+
{ controllerPhone: '+34999999999', usualname: 'Unknown' },
|
|
886
|
+
{ timeoutMs: 5000, intervalMs: 1 },
|
|
887
|
+
);
|
|
888
|
+
assert.equal(result, null);
|
|
889
|
+
} finally {
|
|
890
|
+
globalThis.fetch = originalFetch;
|
|
891
|
+
}
|
|
892
|
+
});
|