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,31 @@
|
|
|
1
|
+
# Data Model Alignment (GW + Chat + SDK)
|
|
2
|
+
|
|
3
|
+
Alignment reference for SDK consumers and backend integrators.
|
|
4
|
+
|
|
5
|
+
## Business vs Infra Sector
|
|
6
|
+
|
|
7
|
+
- Infra host onboarding uses network routing sector (`HOST_REGISTRY_SECTOR`).
|
|
8
|
+
- Tenant runtime model uses business sector (`SECTOR`).
|
|
9
|
+
- Canonical tenant vault ID:
|
|
10
|
+
- `<SECTOR>_<tenantId>`
|
|
11
|
+
- Example: `health-care_acme`
|
|
12
|
+
|
|
13
|
+
## Individual Organization Model
|
|
14
|
+
|
|
15
|
+
- Section: `<prefix>_individual`
|
|
16
|
+
- Doc ID: `org.schema.Organization.identifier.value` (UUID)
|
|
17
|
+
|
|
18
|
+
## Search Contract (`searchFamilyOrganization`)
|
|
19
|
+
|
|
20
|
+
- Input mapping:
|
|
21
|
+
- `controllerPhone` -> `org.schema.Organization.owner.telephone`
|
|
22
|
+
- `usualname` -> `org.schema.Organization.alternateName`
|
|
23
|
+
- `birthDate` -> `org.schema.Organization.foundingDate` (optional)
|
|
24
|
+
|
|
25
|
+
## Voice Phone Contract
|
|
26
|
+
|
|
27
|
+
- Preferred claim for subject call target:
|
|
28
|
+
- `org.schema.Organization.telephone`
|
|
29
|
+
- Fallback claim:
|
|
30
|
+
- `org.schema.Organization.owner.telephone`
|
|
31
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Data Planes And Scope Matrix (SDK Guide)
|
|
2
|
+
|
|
3
|
+
Canonical reference for scope semantics and resource placement in the Node SDK.
|
|
4
|
+
|
|
5
|
+
Primary backend source: `gdc-unid-node-ts/docs/02-API-AND-ENDPOINTS/02.F-DATA-PLANES-SCOPE-MATRIX.md`.
|
|
6
|
+
|
|
7
|
+
## Quick rules
|
|
8
|
+
|
|
9
|
+
1. Admin/legal plane
|
|
10
|
+
- Scopes: `org.schema/Organization.<cruds>`, `org.schema/Person.<cruds>`.
|
|
11
|
+
- Use for legal/admin governance and registry/discovery metadata.
|
|
12
|
+
|
|
13
|
+
2. Subject data plane
|
|
14
|
+
- Scopes: `organization/<ResourceType>.<cruds|rs|rus>`.
|
|
15
|
+
- Use for private subject operations (`Person`, `RelatedPerson`, `Appointment`, `AppointmentResponse`, `Composition`, etc.).
|
|
16
|
+
|
|
17
|
+
3. Catalog publication
|
|
18
|
+
- Public/legal metadata: `org.schema` plane.
|
|
19
|
+
- Private subject data: never publish raw records in catalog.
|
|
20
|
+
|
|
21
|
+
## Resource matrix
|
|
22
|
+
|
|
23
|
+
| Intent | Scope |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Subject private person profile | `organization/Person.rus` |
|
|
26
|
+
| Emergency contacts | `organization/RelatedPerson.cruds` |
|
|
27
|
+
| Appointments | `organization/Appointment.cruds` |
|
|
28
|
+
| Appointment responses | `organization/AppointmentResponse.cruds` |
|
|
29
|
+
| Subject index composition read | `organization/Composition.rs` |
|
|
30
|
+
|
|
31
|
+
## Notification phone policy
|
|
32
|
+
|
|
33
|
+
1. Subject notification routing (caregiver/spouse/multi-contact) belongs to private data plane and should be stored in `organization/Organization`.
|
|
34
|
+
2. Subject own phone belongs to `organization/Person`.
|
|
35
|
+
3. Emergency contacts belong to `organization/RelatedPerson`.
|
|
36
|
+
4. Do not model these private routing contacts as legal/public `org.schema` publication data.
|
|
37
|
+
|
|
38
|
+
## Recommended default scope bundle for subject apps
|
|
39
|
+
|
|
40
|
+
- `organization/Person.rus`
|
|
41
|
+
- `organization/RelatedPerson.cruds`
|
|
42
|
+
- `organization/Appointment.cruds`
|
|
43
|
+
- `organization/AppointmentResponse.cruds`
|
|
44
|
+
- `organization/Composition.rs`
|
|
45
|
+
|
|
46
|
+
## Test checklist
|
|
47
|
+
|
|
48
|
+
1. Token minted with only `org.schema/*` cannot operate on `organization/*` resources.
|
|
49
|
+
2. Token minted with only `organization/*` cannot mutate legal/admin `org.schema/*` resources.
|
|
50
|
+
3. Scope propagation in flow tests:
|
|
51
|
+
- bootstrap -> consent -> request token -> subject operation.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Developer Use-Case Cookbook (Node SDK)
|
|
2
|
+
|
|
3
|
+
This guide provides exact method calls for real integration flows.
|
|
4
|
+
|
|
5
|
+
Normative scope/resource placement matrix:
|
|
6
|
+
- [DATA_PLANES_SCOPE_MATRIX.md](DATA_PLANES_SCOPE_MATRIX.md)
|
|
7
|
+
|
|
8
|
+
SDK: `dataspace-client-sdk-node` (`DataspaceNodeClient`)
|
|
9
|
+
|
|
10
|
+
## Base Setup
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { DataspaceNodeClient } from 'dataspace-client-sdk-node';
|
|
14
|
+
|
|
15
|
+
const client = new DataspaceNodeClient({
|
|
16
|
+
baseUrl: process.env.GW_BASE_URL!,
|
|
17
|
+
bearerToken: process.env.GW_BEARER_TOKEN, // controller/professional token depending on step
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const ctx = {
|
|
21
|
+
tenantId: 'acme',
|
|
22
|
+
jurisdiction: 'ES',
|
|
23
|
+
sector: 'health-care',
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## UC5.1 Subject bootstrap (personal organization)
|
|
28
|
+
|
|
29
|
+
Terminology note: `subject` names in methods/claims refer to the member (person/patient) orchestrated by a personal organization with controller/manager and associated members.
|
|
30
|
+
|
|
31
|
+
Method: `bootstrapSubjectOrganizationIndex(ctx, input)`
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const result = await client.bootstrapSubjectOrganizationIndex(ctx, {
|
|
35
|
+
registrationPayload: {
|
|
36
|
+
body: {
|
|
37
|
+
data: [{ type: 'Family-registration-form-v1.0', meta: { claims: { '@context': 'org.schema' } } }],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
confirmationPayload: {
|
|
41
|
+
body: {
|
|
42
|
+
data: [{ type: 'Family-order-request-v1.0', meta: { claims: { 'Order.acceptedOffer.identifier': 'urn:offer:123' } } }],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## UC5.2 Legal organization activation in GW (from ICA proof)
|
|
49
|
+
|
|
50
|
+
Method: `activateOrganizationInGatewayFromIcaProof(hostCtx, input)`
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const activation = await client.activateOrganizationInGatewayFromIcaProof(
|
|
54
|
+
{ jurisdiction: 'ES', sector: 'health-care' },
|
|
55
|
+
{
|
|
56
|
+
vpToken: process.env.ICA_VP_TOKEN!,
|
|
57
|
+
organizationVc: process.env.ICA_ORG_VC_JWT,
|
|
58
|
+
legalRepresentativeVc: process.env.ICA_LEGAL_REP_VC_JWT,
|
|
59
|
+
regulatoryEvidence: { sanitaryRegister: 'REG-123' },
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## UC5.3 Create employee / professional license
|
|
65
|
+
|
|
66
|
+
Method: `createOrganizationEmployee(ctx, input)`
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
await client.createOrganizationEmployee(ctx, {
|
|
70
|
+
employeeClaims: {
|
|
71
|
+
'@context': 'org.schema',
|
|
72
|
+
'org.schema.Person.email': 'doctor@example.com',
|
|
73
|
+
'org.schema.Person.hasOccupation': 'ISCO-08|2211',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## UC5.4 Activate employee device
|
|
79
|
+
|
|
80
|
+
Method: `activateEmployeeDeviceWithActivationCode(ctx, input)`
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const device = await client.activateEmployeeDeviceWithActivationCode(ctx, {
|
|
84
|
+
activationCode: 'ACT-123456',
|
|
85
|
+
idToken: process.env.USER_ID_TOKEN!,
|
|
86
|
+
dcrPayload: {
|
|
87
|
+
application_type: 'web',
|
|
88
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
|
89
|
+
jwks: { keys: [] },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## UC5.5 Import IPS/FHIR and update index
|
|
95
|
+
|
|
96
|
+
Method: `importIpsOrFhirAndUpdateIndex(ctx, input)`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
await client.importIpsOrFhirAndUpdateIndex(ctx, {
|
|
100
|
+
format: 'r4',
|
|
101
|
+
compositionPayload: {
|
|
102
|
+
body: {
|
|
103
|
+
data: [{ type: 'Composition-import-request-v1.0', meta: { claims: { subject: 'did:web:subject.example.com' } } }],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## UC5.6 Consent then SMART token (real decoupled flow)
|
|
110
|
+
|
|
111
|
+
Step 1 method: `grantProfessionalAccessSimple(ctx, input)`
|
|
112
|
+
Step 2 method: `requestSmartToken(input)`
|
|
113
|
+
|
|
114
|
+
Domain note: the protected context is modeled as organization (including personal organizations with controller + subject/person/patient members).
|
|
115
|
+
|
|
116
|
+
Scope namespace note:
|
|
117
|
+
- Organization administration (non-FHIR): `org.schema/Organization.<cruds>`, `org.schema/Person.<cruds>`
|
|
118
|
+
- Subject/personal-organization data (FHIR): `organization/<ResourceType>.<cruds|rs>` with optional `?subject=<did:web:...>`
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const consent = await client.grantProfessionalAccessSimple(ctx, {
|
|
122
|
+
subjectPhone: '+34600111222',
|
|
123
|
+
subjectGivenName: 'Ana',
|
|
124
|
+
actor: { identifier: 'did:web:hospital.example.com' },
|
|
125
|
+
actorRole: 'Practitioner',
|
|
126
|
+
purpose: 'TREAT',
|
|
127
|
+
actions: ['organization/Composition.rs'],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const token = await client.requestSmartToken({
|
|
131
|
+
endpointId: 'professional-app',
|
|
132
|
+
scopes: ['organization/Composition.rs'],
|
|
133
|
+
exchangePayload: { grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange' },
|
|
134
|
+
path: '/token',
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## UC5.7 Generate digital twin
|
|
139
|
+
|
|
140
|
+
Method: `generateDigitalTwinFromSubjectData(ctx, input)`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await client.generateDigitalTwinFromSubjectData(ctx, {
|
|
144
|
+
format: 'r4',
|
|
145
|
+
compositionPayload: {
|
|
146
|
+
body: {
|
|
147
|
+
data: [{ type: 'DigitalTwin-composition-request-v1.0', meta: { claims: { source: 'subject-index' } } }],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Additional consent actor patterns
|
|
154
|
+
|
|
155
|
+
Important: canonical claim key is `Consent.actor-identifier`.
|
|
156
|
+
`grantProfessionalAccessSimple` accepts convenience aliases and resolves them into that canonical identifier.
|
|
157
|
+
|
|
158
|
+
Action semantics note:
|
|
159
|
+
- Avoid generic actions like `access` or `read`.
|
|
160
|
+
- Use canonical operation strings aligned with protected resource contracts/scopes (for example `organization/Composition.rs`, `organization/Appointment.cruds`).
|
|
161
|
+
|
|
162
|
+
### A) Actor by canonical `identifier` + role (recommended)
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
await client.grantProfessionalAccessSimple(ctx, {
|
|
166
|
+
subjectDid: 'did:web:subject.example.com',
|
|
167
|
+
actor: { identifier: 'did:web:org.example.com' },
|
|
168
|
+
actorRole: 'Practitioner',
|
|
169
|
+
purpose: 'TREAT',
|
|
170
|
+
actions: ['organization/Composition.rs'],
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### B) Actor by organization URL/domain or NIF + role
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
await client.grantProfessionalAccessSimple(ctx, {
|
|
178
|
+
subjectDid: 'did:web:subject.example.com',
|
|
179
|
+
actor: { url: 'org.example.com' }, // accepts bare domain or full URL; resolves to did:web:org.example.com
|
|
180
|
+
actorRole: 'Practitioner',
|
|
181
|
+
purpose: 'CARE',
|
|
182
|
+
actions: ['organization/Appointment.cruds'],
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
await client.grantProfessionalAccessSimple(ctx, {
|
|
188
|
+
subjectDid: 'did:web:subject.example.com',
|
|
189
|
+
actor: { organizationTaxId: 'B12345678' }, // maps to urn:taxid:B12345678
|
|
190
|
+
actorRole: 'Practitioner',
|
|
191
|
+
purpose: 'CARE',
|
|
192
|
+
actions: ['organization/RelatedPerson.cruds'],
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### C) Actor by email/phone + role
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
await client.grantProfessionalAccessSimple(ctx, {
|
|
200
|
+
subjectDid: 'did:web:subject.example.com',
|
|
201
|
+
actor: { email: 'doctor@example.com' },
|
|
202
|
+
actorRole: 'Practitioner',
|
|
203
|
+
purpose: 'TREAT',
|
|
204
|
+
actions: ['organization/Composition.rs'],
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
await client.grantProfessionalAccessSimple(ctx, {
|
|
210
|
+
subjectDid: 'did:web:subject.example.com',
|
|
211
|
+
actor: { phone: '+34600111222' }, // maps to urn:tel:+34600111222
|
|
212
|
+
actorRole: 'Practitioner',
|
|
213
|
+
purpose: 'TREAT',
|
|
214
|
+
actions: ['organization/AppointmentResponse.cruds'],
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### D) Jurisdiction + role (explicit claims)
|
|
219
|
+
|
|
220
|
+
`grantProfessionalAccessSimple` does not currently expose a `jurisdiction` field.
|
|
221
|
+
For jurisdiction-based actor identifiers, submit explicit claims:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
await client.submitAndPoll(
|
|
225
|
+
client.individualConsentR4BatchPath(ctx),
|
|
226
|
+
client.individualConsentR4PollPath(ctx),
|
|
227
|
+
{
|
|
228
|
+
thid: 'consent-jurisdiction-001',
|
|
229
|
+
body: {
|
|
230
|
+
data: [{
|
|
231
|
+
type: 'Consent-grant-request-v1.0',
|
|
232
|
+
meta: {
|
|
233
|
+
claims: {
|
|
234
|
+
'@context': 'org.hl7.fhir.api',
|
|
235
|
+
'Consent.decision': 'permit',
|
|
236
|
+
'Consent.subject': 'did:web:subject.example.com',
|
|
237
|
+
'Consent.actor-identifier': 'urn:jurisdiction:ES',
|
|
238
|
+
'Consent.actor-role': 'Practitioner',
|
|
239
|
+
'Consent.purpose': 'TREAT',
|
|
240
|
+
'Consent.action': 'LOINC|48765-2,LOINC|10160-0',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
}],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Traceability contract (FHIR + claims)
|
|
250
|
+
|
|
251
|
+
- FHIR resource identity: `resource.id` remains UUID.
|
|
252
|
+
- FHIR version traceability: `resource.meta.versionId` stores CID of canonical FHIR resource version.
|
|
253
|
+
- Claims traceability: `grantProfessionalAccessSimple(...)` now emits `resource.meta.claims["@id"]` as CID of canonical claims (excluding `@context`, `@type`, `@id`).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# E2E Bootstrap (Tenant + Controller)
|
|
2
|
+
|
|
3
|
+
This flow prepares a tenant for frontend E2E tests (`apptemplate`) after ICA proof is available.
|
|
4
|
+
|
|
5
|
+
## 1) What the script does
|
|
6
|
+
|
|
7
|
+
Script: `examples/e2e-bootstrap-tenant.mjs`
|
|
8
|
+
|
|
9
|
+
1. Authenticates to GW (`AUTH_MODE=demo|pkce`).
|
|
10
|
+
2. Calls host activation endpoint (`Organization/_activate`) with `vp_token` and optional ICA JWT VCs.
|
|
11
|
+
3. Optionally creates a controller employee in the tenant (`Employee/_batch`).
|
|
12
|
+
|
|
13
|
+
## 2) Run command
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run example:e2e-bootstrap-tenant
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 3) Required env
|
|
20
|
+
|
|
21
|
+
- `BASE_URL` (default `http://localhost:3000`)
|
|
22
|
+
- `VP_TOKEN` (required)
|
|
23
|
+
|
|
24
|
+
Auth-specific:
|
|
25
|
+
|
|
26
|
+
- Demo:
|
|
27
|
+
- `AUTH_MODE=demo` (default)
|
|
28
|
+
- `AUTH_BEARER` (optional, default `demo-token`)
|
|
29
|
+
- PKCE:
|
|
30
|
+
- `AUTH_MODE=pkce`
|
|
31
|
+
- `GW_API_KEY`
|
|
32
|
+
- `GW_CONTROLLER_PUBLIC_JWK_SIGN` (JSON object)
|
|
33
|
+
|
|
34
|
+
Routing:
|
|
35
|
+
|
|
36
|
+
- `JURISDICTION` (default `ES`)
|
|
37
|
+
- `HOST_REGISTRY_SECTOR` (default `test`)
|
|
38
|
+
- `TENANT_ID` (default `acme`)
|
|
39
|
+
- `SECTOR` (default `health-care`)
|
|
40
|
+
|
|
41
|
+
Optional:
|
|
42
|
+
|
|
43
|
+
- `ORGANIZATION_VC_JWT`
|
|
44
|
+
- `LEGAL_REPRESENTATIVE_VC_JWT`
|
|
45
|
+
- `CREATE_CONTROLLER_EMPLOYEE=true`
|
|
46
|
+
- `CONTROLLER_EMAIL`
|
|
47
|
+
- `CONTROLLER_ROLE`
|
|
48
|
+
|
|
49
|
+
## 4) Typical sequence
|
|
50
|
+
|
|
51
|
+
1. Start local stack (GW + ICA + dependencies).
|
|
52
|
+
2. Obtain `vp_token` from ICA flow (frontend or node).
|
|
53
|
+
3. Run this bootstrap script.
|
|
54
|
+
4. Run `apptemplate` integration profile (`local-demo`, `local-docker`, or `cloud-staging`).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# TODO: SMART EHR Compatibility (patient scope alias)
|
|
2
|
+
|
|
3
|
+
Status: pending
|
|
4
|
+
Owner: TBD
|
|
5
|
+
Priority: medium
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
Support EHR integrations that request SMART scopes using `patient/*` without custom `?subject=...`, while preserving the current gateway profile based on subject-pinned scopes.
|
|
9
|
+
|
|
10
|
+
## Current state (today)
|
|
11
|
+
- Gateway profile expects subject pinning through scope query parameter (`?subject=...`).
|
|
12
|
+
- Current token flow uses actor identity in `sub` and subject context extracted from scope.
|
|
13
|
+
- This differs from SMART App Launch standard patient context behavior.
|
|
14
|
+
|
|
15
|
+
## Target behavior
|
|
16
|
+
1. Keep current gateway profile (`organization/*?subject=...`) for existing clients.
|
|
17
|
+
2. Add compatibility profile for SMART EHR (`patient/*`) without requiring query `subject`.
|
|
18
|
+
3. Resolve patient context via SMART launch context (authorize -> token `patient` context), not via custom query-only requirement.
|
|
19
|
+
4. Keep single-subject enforcement per token request.
|
|
20
|
+
|
|
21
|
+
## Proposed switch
|
|
22
|
+
- Environment flag (default enabled compatibility):
|
|
23
|
+
- `DISABLE_PATIENT_SCOPE_ALIAS=false` (default)
|
|
24
|
+
- `DISABLE_PATIENT_SCOPE_ALIAS=true` disables alias compatibility
|
|
25
|
+
|
|
26
|
+
## Implementation TODO
|
|
27
|
+
1. Scope parser
|
|
28
|
+
- Accept `patient/*` root scopes when alias compatibility is enabled.
|
|
29
|
+
- Normalize internally to canonical authorization checks.
|
|
30
|
+
|
|
31
|
+
2. Subject/patient context resolution
|
|
32
|
+
- Priority order:
|
|
33
|
+
1) launch context patient id (SMART standard)
|
|
34
|
+
2) explicit `?subject=...` (gateway profile)
|
|
35
|
+
- Reject when no resolvable subject context exists.
|
|
36
|
+
|
|
37
|
+
3. Token payload contract
|
|
38
|
+
- Include/propagate patient context field consistent with SMART expectations.
|
|
39
|
+
- Preserve actor identity semantics already used by gateway.
|
|
40
|
+
|
|
41
|
+
4. Policy checks
|
|
42
|
+
- Ensure consent/rule lookup uses resolved subject context.
|
|
43
|
+
- Enforce single-subject invariant.
|
|
44
|
+
|
|
45
|
+
5. Documentation
|
|
46
|
+
- Add compatibility matrix:
|
|
47
|
+
- SMART standard mode (launch/patient context)
|
|
48
|
+
- Gateway mode (scope subject pinning)
|
|
49
|
+
- Clarify required fields per mode.
|
|
50
|
+
|
|
51
|
+
6. Tests
|
|
52
|
+
- Unit tests: scope parsing + subject resolution matrix.
|
|
53
|
+
- Integration tests: EHR-style `patient/*` and gateway-style `organization/*?subject=...`.
|
|
54
|
+
- Negative tests: missing subject context, mixed-subject scopes.
|
|
55
|
+
|
|
56
|
+
## Out of scope (for this TODO)
|
|
57
|
+
- Full redesign of authorization model.
|
|
58
|
+
- Breaking changes in existing gateway clients.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: identity-exchange.v1 backend PKCE auth (Node SDK)
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates authenticateBackendPkceAndExchange — the full B2B auth flow:
|
|
5
|
+
* 1. ICA DCR: bind API key to service public JWK
|
|
6
|
+
* 2. PKCE code: S256 challenge
|
|
7
|
+
* 3. PKCE token: code + verifier → id_token
|
|
8
|
+
* 4. Exchange: id_token → SMART bearer
|
|
9
|
+
*
|
|
10
|
+
* Node.js equivalent of Python connector_sdk `authenticate_backend_pkce_and_exchange`.
|
|
11
|
+
*
|
|
12
|
+
* Prerequisites:
|
|
13
|
+
* - Tenant org activated in the GW (registry/org.schema/Organization/_activate with ICA VC)
|
|
14
|
+
* - API key issued by ICA with required scopes
|
|
15
|
+
* - Service key pair (EC P-384 recommended); only the public JWK is sent to the GW
|
|
16
|
+
*
|
|
17
|
+
* Environment variables:
|
|
18
|
+
* BASE_URL GW base URL (e.g. http://localhost:3000)
|
|
19
|
+
* GW_API_KEY API key issued by ICA for this service
|
|
20
|
+
* GW_PUBLIC_JWK JSON-serialised service public JWK (EC P-384)
|
|
21
|
+
* TENANT_ID Tenant org id (e.g. acme)
|
|
22
|
+
* JURISDICTION Jurisdiction code (e.g. ES)
|
|
23
|
+
* SECTOR Sector code (e.g. health-care)
|
|
24
|
+
* GW_AUTH_BEARER (optional) Static bearer for SECURITY_MODE=demo — skips the full flow
|
|
25
|
+
*
|
|
26
|
+
* Run (production flow):
|
|
27
|
+
* BASE_URL="https://gw.example.com" \
|
|
28
|
+
* GW_API_KEY="<api-key>" \
|
|
29
|
+
* GW_PUBLIC_JWK='{"kty":"EC","crv":"P-384","x":"...","y":"..."}' \
|
|
30
|
+
* TENANT_ID="acme" JURISDICTION="ES" SECTOR="health-care" \
|
|
31
|
+
* node examples/backend-pkce-auth.mjs
|
|
32
|
+
*
|
|
33
|
+
* Run (demo/local bypass — SECURITY_MODE=demo):
|
|
34
|
+
* BASE_URL="http://localhost:3000" GW_AUTH_BEARER="demo-token" \
|
|
35
|
+
* node examples/backend-pkce-auth.mjs
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { DataspaceNodeClient } from '../dist/index.js';
|
|
39
|
+
|
|
40
|
+
// ---- Demo bypass --------------------------------------------------------
|
|
41
|
+
// If GW_AUTH_BEARER is set (SECURITY_MODE=demo), skip the full PKCE flow.
|
|
42
|
+
const staticBearer = process.env.GW_AUTH_BEARER;
|
|
43
|
+
if (staticBearer) {
|
|
44
|
+
console.log('[demo] Using static GW_AUTH_BEARER — skipping identity-exchange.v1 flow.');
|
|
45
|
+
const client = new DataspaceNodeClient({
|
|
46
|
+
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
|
|
47
|
+
bearerToken: staticBearer,
|
|
48
|
+
});
|
|
49
|
+
console.log('[demo] Client ready. bearerToken set from env.');
|
|
50
|
+
// eslint-disable-next-line no-process-exit
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---- Production flow ----------------------------------------------------
|
|
55
|
+
const baseUrl = process.env.BASE_URL;
|
|
56
|
+
const apiKey = process.env.GW_API_KEY;
|
|
57
|
+
const publicJwkRaw = process.env.GW_PUBLIC_JWK;
|
|
58
|
+
|
|
59
|
+
if (!baseUrl || !apiKey || !publicJwkRaw) {
|
|
60
|
+
console.error('Required env vars: BASE_URL, GW_API_KEY, GW_PUBLIC_JWK');
|
|
61
|
+
console.error('Or set GW_AUTH_BEARER for demo/local bypass.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const controllerPublicJwk = JSON.parse(publicJwkRaw);
|
|
66
|
+
|
|
67
|
+
const ctx = {
|
|
68
|
+
tenantId: process.env.TENANT_ID || 'acme',
|
|
69
|
+
jurisdiction: process.env.JURISDICTION || 'ES',
|
|
70
|
+
sector: process.env.SECTOR || 'health-care',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Client without bearer — auth flow will obtain it.
|
|
74
|
+
const client = new DataspaceNodeClient({ baseUrl });
|
|
75
|
+
|
|
76
|
+
console.log('[auth] Starting identity-exchange.v1 backend PKCE flow...');
|
|
77
|
+
console.log('[auth] ctx:', ctx);
|
|
78
|
+
console.log('[auth] apiKey prefix:', apiKey.slice(0, 8) + '...');
|
|
79
|
+
|
|
80
|
+
const auth = await client.authenticateBackendPkceAndExchange({
|
|
81
|
+
ctx,
|
|
82
|
+
apiKey,
|
|
83
|
+
controllerPublicJwk,
|
|
84
|
+
scopes: ['onboarding', 'family-registration', 'license-order'],
|
|
85
|
+
endpointId: 'chatbot-main',
|
|
86
|
+
// codeVerifier: optional — auto-generated as randomUUID() if not provided
|
|
87
|
+
pollOptions: { timeoutMs: 60_000, intervalMs: 2_000 },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (auth.status === 'failed') {
|
|
91
|
+
console.error(`[auth] FAILED at step: ${auth.step}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`[auth] Status: ${auth.status}`); // 'fetched' or 'cached'
|
|
96
|
+
console.log(`[auth] Token type: ${auth.tokenType}`);
|
|
97
|
+
console.log(`[auth] Scopes granted: ${auth.scopes.join(' ')}`);
|
|
98
|
+
console.log(`[auth] Bearer (truncated): ${auth.accessToken.slice(0, 20)}...`);
|
|
99
|
+
|
|
100
|
+
// ---- Use the bearer for subsequent SDK calls ----------------------------
|
|
101
|
+
const authedClient = new DataspaceNodeClient({
|
|
102
|
+
baseUrl,
|
|
103
|
+
bearerToken: auth.accessToken,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Example: submit a family registration draft (DIDComm plain)
|
|
107
|
+
// import { createDidcommPlainMessage } from '../dist/index.js';
|
|
108
|
+
// const payload = createDidcommPlainMessage({ iss: ..., aud: ..., body: { data: [...] } });
|
|
109
|
+
// const result = await authedClient.submitAndPoll(
|
|
110
|
+
// authedClient.individualFamilyOrganizationBatchPath(ctx),
|
|
111
|
+
// authedClient.individualFamilyOrganizationPollPath(ctx),
|
|
112
|
+
// payload,
|
|
113
|
+
// );
|
|
114
|
+
// console.log(result.poll.status, result.poll.body);
|
|
115
|
+
|
|
116
|
+
// ---- Cache check --------------------------------------------------------
|
|
117
|
+
// On subsequent calls, getCachedBearerToken avoids re-running the flow:
|
|
118
|
+
const cached = client.getCachedBearerToken('chatbot-main');
|
|
119
|
+
console.log(`[cache] Cached bearer available: ${cached !== undefined}`);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { DataspaceNodeClient } from '../dist/index.js';
|
|
4
|
+
|
|
5
|
+
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
|
|
6
|
+
const bearerToken = process.env.AUTH_BEARER || 'demo-token';
|
|
7
|
+
|
|
8
|
+
const tenantId = process.env.TENANT_ID || 'acme';
|
|
9
|
+
const jurisdiction = process.env.JURISDICTION || 'ES';
|
|
10
|
+
const sector = process.env.SECTOR || 'animal-care';
|
|
11
|
+
const softwareId = process.env.SOFTWARE_ID || 'excel-adapter';
|
|
12
|
+
const sourceFormat = process.env.SOURCE_FORMAT || 'xlsx';
|
|
13
|
+
const filePath = process.env.SOURCE_FILE;
|
|
14
|
+
|
|
15
|
+
if (!filePath) {
|
|
16
|
+
throw new Error('Missing SOURCE_FILE env var (path to excel/csv file).');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const client = new DataspaceNodeClient({ baseUrl, bearerToken });
|
|
20
|
+
const ctx = { tenantId, jurisdiction, sector };
|
|
21
|
+
|
|
22
|
+
const uploadPath = client.conversionUploadPath(ctx, softwareId, sourceFormat);
|
|
23
|
+
const pollPath = client.conversionUploadPollPath(ctx, softwareId, sourceFormat);
|
|
24
|
+
|
|
25
|
+
const fileName = basename(filePath);
|
|
26
|
+
const bytes = await readFile(filePath);
|
|
27
|
+
|
|
28
|
+
const submit = await client.uploadConversionFile({
|
|
29
|
+
path: uploadPath,
|
|
30
|
+
fileName,
|
|
31
|
+
fileContent: bytes,
|
|
32
|
+
fields: {
|
|
33
|
+
// Optional provider-specific fields. Keep/remove depending on DataConv API contract.
|
|
34
|
+
mode: process.env.CONVERSION_MODE || 'didcomm-plain',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.log('Upload submit:', JSON.stringify(submit, null, 2));
|
|
39
|
+
|
|
40
|
+
const thid =
|
|
41
|
+
process.env.THID ||
|
|
42
|
+
submit.body?.thid ||
|
|
43
|
+
submit.body?.body?.thid ||
|
|
44
|
+
submit.body?.data?.thid;
|
|
45
|
+
|
|
46
|
+
if (!thid) {
|
|
47
|
+
console.log('No thid found in upload response. If your DataConv API is synchronous, stop here.');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const poll = await client.pollUntilComplete(pollPath, { thid }, { timeoutMs: 120000, intervalMs: 5000 });
|
|
52
|
+
console.log('Upload poll:', JSON.stringify(poll, null, 2));
|