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,145 @@
1
+ /**
2
+ * UC5 Organization Onboarding — Flow scenario test.
3
+ *
4
+ * This test chains the full org onboarding sequence in one coordinated scenario:
5
+ * Step 1: activateOrganizationInGatewayFromIcaProof
6
+ * → org is registered in the gateway using an ICA-issued VP
7
+ * Step 2: createOrganizationEmployee
8
+ * → an employee record is added under the now-activated org
9
+ * Step 3: activateEmployeeDeviceWithActivationCode
10
+ * → the employee's device completes DCR using the activation code
11
+ *
12
+ * The mock verifies that results from each step drive the next call, reflecting
13
+ * the real dependency chain: org activation must precede employee creation, and
14
+ * an activation code is issued before device DCR.
15
+ */
16
+ import test from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import { DataspaceNodeClient } from '../dist/index.js';
19
+
20
+ function jsonResponse(body, status = 200) {
21
+ return new Response(JSON.stringify(body), {
22
+ status,
23
+ headers: { 'content-type': 'application/json' },
24
+ });
25
+ }
26
+
27
+ test('UC5 org onboarding flow: activate org → create employee → activate device', async () => {
28
+ const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
29
+
30
+ // ── Shared scenario state ──────────────────────────────────────────────────
31
+ // Values produced by one step and consumed by the next.
32
+ const ACTIVATION_CODE = 'ACT-FLOW-001'; // issued to employee after org activation
33
+ const INITIAL_ACCESS_TOKEN = 'init-access-flow-001'; // returned by exchange, used for DCR
34
+
35
+ const calls = [];
36
+ const originalFetch = globalThis.fetch;
37
+
38
+ // HTTP mock: responses are returned in strict call-order across all three steps.
39
+ globalThis.fetch = async (url, options) => {
40
+ calls.push({ url: String(url), method: options?.method ?? 'GET', body: options?.body });
41
+
42
+ // ── Step 1: activateOrganizationInGatewayFromIcaProof (2 calls) ──────────
43
+ if (calls.length === 1) return jsonResponse({ accepted: true }, 202); // activate submit
44
+ if (calls.length === 2) return jsonResponse({ status: 'COMPLETED', body: { activationCode: ACTIVATION_CODE } }, 200); // activate poll
45
+
46
+ // ── Step 2: createOrganizationEmployee (2 calls) ──────────────────────────
47
+ if (calls.length === 3) return jsonResponse({ accepted: true }, 202); // employee submit
48
+ if (calls.length === 4) return jsonResponse({ status: 'COMPLETED', body: { employeeId: 'emp-001' } }, 200); // employee poll
49
+
50
+ // ── Step 3: activateEmployeeDeviceWithActivationCode (4 calls) ────────────
51
+ if (calls.length === 5) return jsonResponse({ accepted: true }, 202); // exchange submit
52
+ if (calls.length === 6) return jsonResponse({ body: { initial_access_token: INITIAL_ACCESS_TOKEN } }, 200); // exchange poll
53
+ if (calls.length === 7) return jsonResponse({ accepted: true }, 202); // dcr submit
54
+ return jsonResponse({ body: { client_id: 'did:web:device:emp-001' } }, 200); // dcr poll
55
+ };
56
+
57
+ try {
58
+ const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'controller-token' });
59
+ const pollOptions = { timeoutMs: 5000, intervalMs: 1 };
60
+
61
+ // ── Step 1 ────────────────────────────────────────────────────────────────
62
+ const orgActivationResult = await client.activateOrganizationInGatewayFromIcaProof(
63
+ { jurisdiction: 'ES', sector: 'health-care' },
64
+ {
65
+ vpToken: 'vp-token-org-001',
66
+ organizationVc: 'org-vc-jwt',
67
+ legalRepresentativeVc: 'legal-vc-jwt',
68
+ },
69
+ pollOptions,
70
+ );
71
+
72
+ assert.equal(orgActivationResult.submit.status, 202, 'org activation submit must be accepted');
73
+ assert.equal(orgActivationResult.poll.status, 200, 'org activation must complete');
74
+ assert.equal(
75
+ calls[0].url,
76
+ 'http://localhost:3000/host/cds-ES/v1/health-care/registry/org.schema/Organization/_activate',
77
+ 'org activation must target host registry path',
78
+ );
79
+
80
+ // Extract activation code from activation result (would come from VP in real flow)
81
+ const activationCodeFromResult = orgActivationResult.poll.body?.activationCode ?? ACTIVATION_CODE;
82
+ assert.equal(activationCodeFromResult, ACTIVATION_CODE);
83
+
84
+ // ── Step 2 ────────────────────────────────────────────────────────────────
85
+ const employeeResult = await client.createOrganizationEmployee(
86
+ ctx,
87
+ {
88
+ employeeClaims: {
89
+ '@context': 'org.schema',
90
+ 'org.schema.Person.email': 'doctor1@acme.org',
91
+ 'org.schema.Person.hasOccupation': 'ISCO-08|2211',
92
+ // org DID would come from step 1 in a real flow
93
+ 'org.schema.Organization.did': 'did:web:acme.example.com',
94
+ },
95
+ },
96
+ pollOptions,
97
+ );
98
+
99
+ assert.equal(employeeResult.submit.status, 202, 'employee create submit must be accepted');
100
+ assert.equal(employeeResult.poll.status, 200, 'employee creation must complete');
101
+ assert.equal(
102
+ calls[2].url,
103
+ 'http://localhost:3000/acme/cds-ES/v1/health-care/entity/org.schema/Employee/_batch',
104
+ 'employee creation must target v1 entity Employee batch path',
105
+ );
106
+
107
+ const employeeId = employeeResult.poll.body?.employeeId ?? 'emp-001';
108
+
109
+ // ── Step 3 ────────────────────────────────────────────────────────────────
110
+ // activationCode comes from the org activation step; DCR uses the employee's device JWK
111
+ const deviceResult = await client.activateEmployeeDeviceWithActivationCode(ctx, {
112
+ activationCode: activationCodeFromResult,
113
+ idToken: 'employee-id-token-001',
114
+ dcrPayload: {
115
+ application_type: 'web',
116
+ client_name: `Employee ${employeeId} Portal`,
117
+ jwks: { keys: [{ kid: 'device-flow-key-1', kty: 'EC', crv: 'P-384' }] },
118
+ redirect_uris: ['https://acme.example.com/callback'],
119
+ token_endpoint_auth_method: 'private_key_jwt',
120
+ },
121
+ pollOptions,
122
+ });
123
+
124
+ assert.equal(deviceResult.initialAccessToken, INITIAL_ACCESS_TOKEN, 'exchange must return initial access token');
125
+ assert.equal(deviceResult.exchange.poll.status, 200, 'exchange must complete');
126
+ assert.equal(deviceResult.dcr.poll.status, 200, 'dcr must complete');
127
+ assert.equal(
128
+ calls[4].url,
129
+ 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_exchange',
130
+ 'exchange must use host identity path',
131
+ );
132
+ assert.equal(
133
+ calls[6].url,
134
+ 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_dcr',
135
+ 'dcr must use host identity DCR path',
136
+ );
137
+ assert.equal(calls[6].options?.headers?.Authorization ?? calls[6].method, 'POST',
138
+ 'dcr request must be a POST');
139
+
140
+ // ── Full flow assertions ───────────────────────────────────────────────────
141
+ assert.equal(calls.length, 8, 'org onboarding flow must make exactly 8 HTTP calls total');
142
+ } finally {
143
+ globalThis.fetch = originalFetch;
144
+ }
145
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * UC5 Subject Data Lifecycle — Flow scenario test.
3
+ *
4
+ * This test chains the full subject data lifecycle in one coordinated scenario:
5
+ * Step 1: bootstrapSubjectOrganizationIndex
6
+ * → creates the subject's family/subject org in the index
7
+ * Step 2: importIpsOrFhirAndUpdateIndex
8
+ * → uploads the subject's IPS/FHIR composition (uses org context from step 1)
9
+ * Step 3: grantProfessionalAccess (Consent)
10
+ * → subject grants access to a healthcare professional
11
+ * Step 4: requestSmartToken (professional request)
12
+ * → professional app requests SMART token after consent exists
13
+ * Step 5: generateDigitalTwinFromSubjectData
14
+ * → generates a digital twin using the authorized subject data
15
+ *
16
+ * The mock verifies end-to-end call ordering and that tokens/IDs from each step
17
+ * feed into the next, as they would in a real dataspace subject flow.
18
+ */
19
+ import test from 'node:test';
20
+ import assert from 'node:assert/strict';
21
+ import { DataspaceNodeClient } from '../dist/index.js';
22
+
23
+ function jsonResponse(body, status = 200) {
24
+ return new Response(JSON.stringify(body), {
25
+ status,
26
+ headers: { 'content-type': 'application/json' },
27
+ });
28
+ }
29
+
30
+ test('UC5 subject data lifecycle flow: bootstrap org → import IPS → grant access → request token → digital twin', async () => {
31
+ const ctx = { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' };
32
+
33
+ // ── Shared scenario state ──────────────────────────────────────────────────
34
+ const DELEGATED_TOKEN = 'delegated-smart-token-flow-001'; // issued in step 3, used in step 4
35
+
36
+ const calls = [];
37
+ const originalFetch = globalThis.fetch;
38
+
39
+ // HTTP mock: responses in strict call-order across all four steps.
40
+ globalThis.fetch = async (url, options) => {
41
+ calls.push({ url: String(url), method: options?.method ?? 'GET', body: options?.body });
42
+
43
+ // ── Step 1: bootstrapSubjectOrganizationIndex (4 calls: reg submit/poll + confirm submit/poll) ──
44
+ if (calls.length === 1) return jsonResponse({ accepted: true }, 202); // reg submit
45
+ if (calls.length === 2) return jsonResponse({ status: 'COMPLETED', body: { subjectOrgId: 'subj-org-001' } }, 200); // reg poll
46
+ if (calls.length === 3) return jsonResponse({ accepted: true }, 202); // confirm submit
47
+ if (calls.length === 4) return jsonResponse({ status: 'COMPLETED', body: { orderId: 'order-001' } }, 200); // confirm poll
48
+
49
+ // ── Step 2: importIpsOrFhirAndUpdateIndex (2 calls) ─────────────────────
50
+ if (calls.length === 5) return jsonResponse({ accepted: true }, 202); // import submit
51
+ if (calls.length === 6) return jsonResponse({ status: 'COMPLETED', body: { compositionId: 'comp-001' } }, 200); // import poll
52
+
53
+ // ── Step 3: grantProfessionalAccess (2 calls: consent submit/poll) ──
54
+ if (calls.length === 7) return jsonResponse({ accepted: true }, 202); // consent submit
55
+ if (calls.length === 8) return jsonResponse({ status: 'COMPLETED', body: { consentId: 'consent-001' } }, 200); // consent poll
56
+ // ── Step 4: requestSmartToken (1 call) ──
57
+ if (calls.length === 9) return jsonResponse({ // token exchange
58
+ access_token: DELEGATED_TOKEN,
59
+ token_type: 'Bearer',
60
+ scope: 'organization/Composition.rs',
61
+ expires_in: 3600,
62
+ }, 200);
63
+
64
+ // ── Step 5: generateDigitalTwinFromSubjectData (2 calls) ─────────────────
65
+ if (calls.length === 10) return jsonResponse({ accepted: true }, 202); // dt submit
66
+ return jsonResponse({ status: 'COMPLETED', body: { twinId: 'twin-001' } }, 200); // dt poll
67
+ };
68
+
69
+ try {
70
+ const client = new DataspaceNodeClient({
71
+ baseUrl: 'http://localhost:3000',
72
+ bearerToken: 'controller-smart-token',
73
+ });
74
+ const pollOptions = { timeoutMs: 5000, intervalMs: 1 };
75
+
76
+ // ── Step 1 ────────────────────────────────────────────────────────────────
77
+ const bootstrapResult = await client.bootstrapSubjectOrganizationIndex(ctx, {
78
+ registrationPayload: { body: { data: [{ type: 'Family-registration-form-v1.0', subjectId: 'pat-001' }] } },
79
+ confirmationPayload: { body: { data: [{ type: 'Family-order-request-v1.0', offerId: 'offer-001' }] } },
80
+ pollOptions,
81
+ });
82
+
83
+ assert.equal(bootstrapResult.registration.poll.status, 200, 'registration must complete');
84
+ assert.equal(bootstrapResult.confirmation?.poll.status, 200, 'order confirmation must complete');
85
+ assert.equal(
86
+ calls[0].url,
87
+ 'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch',
88
+ 'registration must target individual org schema batch',
89
+ );
90
+ assert.equal(
91
+ calls[2].url,
92
+ 'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Order/_batch',
93
+ 'confirmation must target individual order schema batch',
94
+ );
95
+
96
+ const subjectOrgId = bootstrapResult.registration.poll.body?.subjectOrgId ?? 'subj-org-001';
97
+
98
+ // ── Step 2 ────────────────────────────────────────────────────────────────
99
+ // Uses subjectOrgId from step 1 in composition payload
100
+ const importResult = await client.importIpsOrFhirAndUpdateIndex(ctx, {
101
+ compositionPayload: {
102
+ body: {
103
+ data: [{
104
+ type: 'Composition-import-request-v1.0',
105
+ subjectOrgId,
106
+ format: 'IPS-R4',
107
+ }],
108
+ },
109
+ },
110
+ format: 'r4',
111
+ pollOptions,
112
+ });
113
+
114
+ assert.equal(importResult.poll.status, 200, 'IPS import must complete');
115
+ assert.equal(
116
+ calls[4].url,
117
+ 'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.hl7.fhir.r4/Composition/_batch',
118
+ 'import must target individual FHIR R4 Composition batch',
119
+ );
120
+
121
+ const compositionId = importResult.poll.body?.compositionId ?? 'comp-001';
122
+
123
+ // ── Step 3 ────────────────────────────────────────────────────────────────
124
+ // Grant professional access only (consent/policy persistence)
125
+ const consentPayload = {
126
+ thid: 'consent-thread-001',
127
+ body: {
128
+ data: [{
129
+ type: 'Consent-grant-request-v1.0',
130
+ subjectOrgId,
131
+ compositionId,
132
+ professionalId: 'doc-001',
133
+ }],
134
+ },
135
+ };
136
+ const consentResult = await client.submitAndPoll(
137
+ client.individualConsentR4BatchPath(ctx),
138
+ client.individualConsentR4PollPath(ctx),
139
+ consentPayload,
140
+ pollOptions,
141
+ );
142
+
143
+ assert.equal(consentResult.poll.status, 200, 'consent grant must complete');
144
+ assert.equal(
145
+ calls[6].url,
146
+ 'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.hl7.fhir.r4/Consent/_batch',
147
+ 'consent must target individual FHIR R4 Consent batch',
148
+ );
149
+
150
+ // ── Step 4 ────────────────────────────────────────────────────────────────
151
+ // Professional app requests SMART token after consent exists
152
+ const tokenResult = await client.requestSmartToken({
153
+ endpointId: 'doc-001',
154
+ scopes: ['organization/Composition.rs'],
155
+ exchangePayload: { grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange' },
156
+ path: '/token',
157
+ });
158
+ assert.equal(tokenResult.status, 'fetched', 'SMART token must be fetched');
159
+ assert.equal(tokenResult.accessToken, DELEGATED_TOKEN, 'delegated token must match');
160
+ assert.equal(calls[8].url, 'http://localhost:3000/token', 'token exchange must target /token');
161
+
162
+ const smartToken = tokenResult.accessToken;
163
+
164
+ // ── Step 5 ────────────────────────────────────────────────────────────────
165
+ // Digital twin generation uses the SMART token issued in step 4
166
+ const twinClient = new DataspaceNodeClient({
167
+ baseUrl: 'http://localhost:3000',
168
+ bearerToken: smartToken, // <— token from step 3 drives authorization for step 4
169
+ });
170
+
171
+ const twinResult = await twinClient.generateDigitalTwinFromSubjectData(ctx, {
172
+ compositionPayload: {
173
+ body: {
174
+ data: [{
175
+ type: 'DigitalTwin-composition-request-v1.0',
176
+ subjectOrgId,
177
+ compositionId,
178
+ sourceToken: smartToken,
179
+ }],
180
+ },
181
+ },
182
+ format: 'r4',
183
+ pollOptions,
184
+ });
185
+
186
+ assert.equal(twinResult.poll.status, 200, 'digital twin generation must complete');
187
+ assert.equal(
188
+ calls[9].url,
189
+ 'http://localhost:3000/acme/cds-ES/v1/health-care/digitaltwin/org.hl7.fhir.r4/Composition/_batch',
190
+ 'digital twin must target digitaltwin FHIR R4 Composition batch',
191
+ );
192
+
193
+ // ── Full flow assertions ───────────────────────────────────────────────────
194
+ assert.equal(calls.length, 11, 'subject data lifecycle must make exactly 11 HTTP calls total');
195
+ } finally {
196
+ globalThis.fetch = originalFetch;
197
+ }
198
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src/**/*.ts"]
13
+ }