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,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
|
+
}
|