dataspace-client-sdk-node 0.1.1 → 0.2.0
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/CHANGELOG.md +31 -0
- package/README.md +44 -1
- package/dist/client.d.ts +153 -33
- package/dist/client.js +619 -93
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +112 -1
- package/dist/vp-token.d.ts +37 -0
- package/dist/vp-token.js +56 -0
- package/docs/API.md +19 -4
- package/docs/BACKEND_NODE_INTEGRATION.md +249 -0
- package/docs/CONTROLLER_FLOW_STEP_BY_STEP.md +283 -0
- package/docs/DATA_MODEL_ALIGNMENT.md +37 -13
- package/docs/DEVELOPER_USE_CASES.md +10 -2
- package/docs/E2E_LOCAL_GW_UC5.md +49 -0
- package/docs/ENDPOINT_ID_CATALOG.md +90 -0
- package/docs/LEGAL_ORGANIZATION_FLOW_STEP_BY_STEP.md +84 -0
- package/docs/PERSONAL_FLOW_STEP_BY_STEP.md +70 -0
- package/docs/PORTAL_BACKEND_INTEGRATION_HANDOVER.md +343 -0
- package/docs/PRACTITIONER_FLOW_STEP_BY_STEP.md +182 -0
- package/docs/REACT_WEB_INTEGRATION.md +72 -0
- package/examples/e2e-bootstrap-tenant.mjs +3 -2
- package/examples/e2e-individual-flow.mjs +13 -8
- package/examples/host-activate-and-employee.mjs +3 -2
- package/examples/smoke-legal-org-local.mjs +40 -0
- package/package.json +4 -3
- package/src/client.ts +784 -132
- package/src/index.ts +1 -0
- package/src/types.ts +123 -1
- package/src/vp-token.ts +91 -0
- package/tests/client.test.mjs +491 -0
- package/tests/fixtures/ica-vp-minimal.json +67 -0
- package/tests/helpers/vp-token-fixture.mjs +23 -0
- package/tests/live-gw-uc5.e2e.test.mjs +108 -0
- package/SDK_PARITY_MAP.md +0 -120
- package/TODO_PROMPT_NEXT_STEPS.md +0 -185
- package/artifacts/update-smart-wallet.js +0 -1016
package/tests/client.test.mjs
CHANGED
|
@@ -268,6 +268,10 @@ test('activateOrganizationInGatewayFromIcaProof submits activation and polls to
|
|
|
268
268
|
assert.equal(result.submit.status, 202);
|
|
269
269
|
assert.equal(result.poll.status, 200);
|
|
270
270
|
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/test/registry/org.schema/Organization/_activate');
|
|
271
|
+
const submitPayload = JSON.parse(calls[0].options.body);
|
|
272
|
+
assert.equal(submitPayload.body.vp_token, 'vp-token-001');
|
|
273
|
+
assert.equal(submitPayload.body.organizationCredential, 'org-vc-jwt');
|
|
274
|
+
assert.equal(submitPayload.body.representativeCredential, 'legal-vc-jwt');
|
|
271
275
|
} finally {
|
|
272
276
|
globalThis.fetch = originalFetch;
|
|
273
277
|
}
|
|
@@ -388,6 +392,90 @@ test('bootstrapSubjectOrganizationIndex runs registration and confirmation flow'
|
|
|
388
392
|
}
|
|
389
393
|
});
|
|
390
394
|
|
|
395
|
+
test('bootstrapIndividualOrganizationSimple registers individual org and confirms family order', async () => {
|
|
396
|
+
const calls = [];
|
|
397
|
+
const originalFetch = globalThis.fetch;
|
|
398
|
+
|
|
399
|
+
globalThis.fetch = async (url, options) => {
|
|
400
|
+
calls.push({ url: String(url), options, body: options?.body ? JSON.parse(options.body) : undefined });
|
|
401
|
+
if (calls.length === 1 || calls.length === 3) return jsonResponse({ accepted: true }, 202);
|
|
402
|
+
if (calls.length === 2) {
|
|
403
|
+
return jsonResponse({
|
|
404
|
+
body: { data: [{ meta: { claims: { 'org.schema.Offer.identifier': 'urn:offer:family-001' } } }] },
|
|
405
|
+
}, 200);
|
|
406
|
+
}
|
|
407
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const client = new DataspaceNodeClient({
|
|
412
|
+
baseUrl: 'http://localhost:3000',
|
|
413
|
+
bearerToken: 'controller-token',
|
|
414
|
+
ctx: { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' },
|
|
415
|
+
});
|
|
416
|
+
const result = await client.bootstrapIndividualOrganizationSimple({
|
|
417
|
+
alternateName: 'ana',
|
|
418
|
+
controllerTelephone: 'tel:+34600111222',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
assert.equal(result.offerId, 'urn:offer:family-001');
|
|
422
|
+
assert.equal(result.registration.poll.status, 200);
|
|
423
|
+
assert.equal(result.confirmation.poll.status, 200);
|
|
424
|
+
assert.equal(
|
|
425
|
+
calls[0].url,
|
|
426
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Organization/_batch',
|
|
427
|
+
);
|
|
428
|
+
assert.equal(
|
|
429
|
+
calls[2].url,
|
|
430
|
+
'http://localhost:3000/acme/cds-ES/v1/health-care/individual/org.schema/Order/_batch',
|
|
431
|
+
);
|
|
432
|
+
const registrationClaims = calls[0].body?.body?.data?.[0]?.meta?.claims || {};
|
|
433
|
+
assert.equal(registrationClaims['org.schema.Organization.alternateName'], 'ana');
|
|
434
|
+
assert.equal(registrationClaims['org.schema.Service.category'], 'health-care');
|
|
435
|
+
assert.equal(registrationClaims['org.schema.Person.telephone'], 'tel:+34600111222');
|
|
436
|
+
assert.equal(registrationClaims['org.schema.Person.hasOccupation'], 'org.hl7.v3.RoleCode|RESPRSN');
|
|
437
|
+
} finally {
|
|
438
|
+
globalThis.fetch = originalFetch;
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('startIndividualOrganizationSimple + confirmIndividualOrganizationOrderSimple support explicit offer acceptance UX', async () => {
|
|
443
|
+
const calls = [];
|
|
444
|
+
const originalFetch = globalThis.fetch;
|
|
445
|
+
|
|
446
|
+
globalThis.fetch = async (url, options) => {
|
|
447
|
+
calls.push({ url: String(url), options, body: options?.body ? JSON.parse(options.body) : undefined });
|
|
448
|
+
if (calls.length === 1 || calls.length === 3) return jsonResponse({ accepted: true }, 202);
|
|
449
|
+
if (calls.length === 2) {
|
|
450
|
+
return jsonResponse({
|
|
451
|
+
body: { data: [{ meta: { claims: { 'org.schema.Offer.identifier': 'urn:offer:family-002', 'org.schema.Offer.price': '0.00' } } }] },
|
|
452
|
+
}, 200);
|
|
453
|
+
}
|
|
454
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const client = new DataspaceNodeClient({
|
|
459
|
+
baseUrl: 'http://localhost:3000',
|
|
460
|
+
bearerToken: 'controller-token',
|
|
461
|
+
ctx: { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' },
|
|
462
|
+
});
|
|
463
|
+
const started = await client.startIndividualOrganizationSimple({
|
|
464
|
+
alternateName: 'maria',
|
|
465
|
+
controllerEmail: 'maria@example.com',
|
|
466
|
+
});
|
|
467
|
+
assert.equal(started.offerId, 'urn:offer:family-002');
|
|
468
|
+
assert.equal(started.offerPreview.amount, '0.00');
|
|
469
|
+
|
|
470
|
+
const confirmed = await client.confirmIndividualOrganizationOrderSimple({
|
|
471
|
+
offerId: started.offerId,
|
|
472
|
+
});
|
|
473
|
+
assert.equal(confirmed.poll.status, 200);
|
|
474
|
+
} finally {
|
|
475
|
+
globalThis.fetch = originalFetch;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
391
479
|
test('importIpsOrFhirAndUpdateIndex submits Composition in individual section', async () => {
|
|
392
480
|
const calls = [];
|
|
393
481
|
const originalFetch = globalThis.fetch;
|
|
@@ -523,6 +611,88 @@ test('requestSmartToken exchanges token in separate step', async () => {
|
|
|
523
611
|
}
|
|
524
612
|
});
|
|
525
613
|
|
|
614
|
+
test('requestSmartTokenSimple uses identity auth exchange async flow with default ctx', async () => {
|
|
615
|
+
const calls = [];
|
|
616
|
+
const originalFetch = globalThis.fetch;
|
|
617
|
+
|
|
618
|
+
globalThis.fetch = async (url, options) => {
|
|
619
|
+
calls.push({ url: String(url), options });
|
|
620
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
621
|
+
return jsonResponse({
|
|
622
|
+
access_token: 'smart-token-ctx-001',
|
|
623
|
+
token_type: 'Bearer',
|
|
624
|
+
scope: 'employee.healthcare.getIndexComposition',
|
|
625
|
+
expires_in: 3600,
|
|
626
|
+
}, 200);
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
const client = new DataspaceNodeClient({
|
|
631
|
+
baseUrl: 'http://localhost:3000',
|
|
632
|
+
ctx: { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' },
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const result = await client.requestSmartTokenSimple({
|
|
636
|
+
idToken: 'employee-id-token-001',
|
|
637
|
+
scopes: ['employee.healthcare.getIndexComposition'],
|
|
638
|
+
timeoutSeconds: 5,
|
|
639
|
+
intervalSeconds: 0.001,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
assert.equal(result.status, 'fetched');
|
|
643
|
+
assert.equal(result.accessToken, 'smart-token-ctx-001');
|
|
644
|
+
assert.equal(calls.length, 2);
|
|
645
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_exchange');
|
|
646
|
+
assert.equal(calls[1].url, 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_exchange-response');
|
|
647
|
+
} finally {
|
|
648
|
+
globalThis.fetch = originalFetch;
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test('activateEmployeeDeviceWithActivationCodeSimple uses one-object input and default ctx', async () => {
|
|
653
|
+
const calls = [];
|
|
654
|
+
const originalFetch = globalThis.fetch;
|
|
655
|
+
|
|
656
|
+
globalThis.fetch = async (url, options) => {
|
|
657
|
+
calls.push({ url: String(url), options });
|
|
658
|
+
switch (calls.length) {
|
|
659
|
+
case 1:
|
|
660
|
+
return jsonResponse({ accepted: true }, 202); // exchange submit
|
|
661
|
+
case 2:
|
|
662
|
+
return jsonResponse({ body: { initial_access_token: 'initial-access-simple-001' } }, 200); // exchange poll
|
|
663
|
+
case 3:
|
|
664
|
+
return jsonResponse({ accepted: true }, 202); // dcr submit
|
|
665
|
+
default:
|
|
666
|
+
return jsonResponse({ body: { client_id: 'did:web:device-simple-001' } }, 200); // dcr poll
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
const client = new DataspaceNodeClient({
|
|
672
|
+
baseUrl: 'http://localhost:3000',
|
|
673
|
+
ctx: { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' },
|
|
674
|
+
});
|
|
675
|
+
const result = await client.activateEmployeeDeviceWithActivationCodeSimple({
|
|
676
|
+
activationCode: 'ACT-SIMPLE-001',
|
|
677
|
+
idToken: 'user-id-token-001',
|
|
678
|
+
dcrPayload: {
|
|
679
|
+
application_type: 'web',
|
|
680
|
+
client_name: 'Acme Portal',
|
|
681
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
|
682
|
+
},
|
|
683
|
+
timeoutSeconds: 5,
|
|
684
|
+
intervalSeconds: 0.001,
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
assert.equal(result.initialAccessToken, 'initial-access-simple-001');
|
|
688
|
+
assert.equal(calls.length, 4);
|
|
689
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_exchange');
|
|
690
|
+
assert.equal(calls[2].url, 'http://localhost:3000/host/cds-ES/v1/health-care/acme/identity/auth/_dcr');
|
|
691
|
+
} finally {
|
|
692
|
+
globalThis.fetch = originalFetch;
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
526
696
|
test('generateDigitalTwinFromSubjectData submits digital twin Composition', async () => {
|
|
527
697
|
const calls = [];
|
|
528
698
|
const originalFetch = globalThis.fetch;
|
|
@@ -890,3 +1060,324 @@ test('searchFamilyOrganization returns null for not_found status', async () => {
|
|
|
890
1060
|
globalThis.fetch = originalFetch;
|
|
891
1061
|
}
|
|
892
1062
|
});
|
|
1063
|
+
|
|
1064
|
+
test('offer helpers extract offerId and preview from activation-like response', () => {
|
|
1065
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000' });
|
|
1066
|
+
const result = {
|
|
1067
|
+
poll: {
|
|
1068
|
+
body: {
|
|
1069
|
+
body: {
|
|
1070
|
+
data: [{
|
|
1071
|
+
meta: {
|
|
1072
|
+
claims: {
|
|
1073
|
+
'org.schema.Offer.identifier': 'urn:offer:abc',
|
|
1074
|
+
'org.schema.Offer.price': '0.00',
|
|
1075
|
+
'org.schema.Offer.priceCurrency': 'EUR',
|
|
1076
|
+
'org.schema.Offer.eligibleQuantity.value': 2,
|
|
1077
|
+
'org.schema.Offer.itemOffered.name': 'License Tier XS',
|
|
1078
|
+
'org.schema.Offer.itemOffered.sku': 'portal-web',
|
|
1079
|
+
'org.schema.Offer.acceptedPaymentMethod': 'Stripe',
|
|
1080
|
+
'org.schema.Offer.checkoutPageURLTemplate': 'https://pay.example.com/checkout/123',
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
}],
|
|
1084
|
+
},
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
assert.equal(client.getOfferIdFromResponse(result), 'urn:offer:abc');
|
|
1090
|
+
const preview = client.getOfferPreviewFromResponse(result);
|
|
1091
|
+
assert.equal(preview.offerId, 'urn:offer:abc');
|
|
1092
|
+
assert.equal(preview.amount, '0.00');
|
|
1093
|
+
assert.equal(preview.currency, 'EUR');
|
|
1094
|
+
assert.equal(preview.seats, 2);
|
|
1095
|
+
assert.equal(preview.planName, 'License Tier XS');
|
|
1096
|
+
assert.equal(preview.sku, 'portal-web');
|
|
1097
|
+
assert.equal(preview.paymentMethod, 'Stripe');
|
|
1098
|
+
assert.equal(preview.checkoutUrl, 'https://pay.example.com/checkout/123');
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test('activateOrganizationInGatewaySimple maps single-object input and seconds options', async () => {
|
|
1102
|
+
const calls = [];
|
|
1103
|
+
const originalFetch = globalThis.fetch;
|
|
1104
|
+
|
|
1105
|
+
globalThis.fetch = async (url, options) => {
|
|
1106
|
+
calls.push({ url: String(url), options, body: options?.body ? JSON.parse(options.body) : undefined });
|
|
1107
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
1108
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
try {
|
|
1112
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'tok' });
|
|
1113
|
+
const result = await client.activateOrganizationInGatewaySimple({
|
|
1114
|
+
jurisdiction: 'ES',
|
|
1115
|
+
sector: 'health-care',
|
|
1116
|
+
vpToken: 'vp-001',
|
|
1117
|
+
serviceProviderUrl: 'https://api.acme.org',
|
|
1118
|
+
controllerEmail: 'admin@acme.org',
|
|
1119
|
+
controllerRole: 'ISCO-08|1112',
|
|
1120
|
+
numberOfMembers: 3,
|
|
1121
|
+
timeoutSeconds: 5,
|
|
1122
|
+
intervalSeconds: 1,
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
assert.equal(result.poll.status, 200);
|
|
1126
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/registry/org.schema/Organization/_activate');
|
|
1127
|
+
const claims = calls[0].body?.body?.data?.[0]?.meta?.claims || {};
|
|
1128
|
+
assert.equal(claims.vp_token, 'vp-001');
|
|
1129
|
+
assert.equal(claims['org.schema.Organization.numberOfEmployees'], 3);
|
|
1130
|
+
assert.equal(claims['org.schema.Service.category'], 'health-care');
|
|
1131
|
+
assert.equal(claims['org.schema.Service.identifier'], 'did:web:api.acme.org');
|
|
1132
|
+
assert.equal(claims['org.schema.Service.url'], 'https://api.acme.org');
|
|
1133
|
+
assert.equal(claims['org.schema.Person.email'], 'admin@acme.org');
|
|
1134
|
+
assert.equal(claims['org.schema.Person.hasOccupation'], 'ISCO-08|1112');
|
|
1135
|
+
} finally {
|
|
1136
|
+
globalThis.fetch = originalFetch;
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
test('confirmLegalOrganizationOrderSimple builds order payload from single object', async () => {
|
|
1141
|
+
const calls = [];
|
|
1142
|
+
const originalFetch = globalThis.fetch;
|
|
1143
|
+
|
|
1144
|
+
globalThis.fetch = async (url, options) => {
|
|
1145
|
+
calls.push({ url: String(url), options, body: options?.body ? JSON.parse(options.body) : undefined });
|
|
1146
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
1147
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
try {
|
|
1151
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'tok' });
|
|
1152
|
+
const result = await client.confirmLegalOrganizationOrderSimple({
|
|
1153
|
+
jurisdiction: 'ES',
|
|
1154
|
+
sector: 'health-care',
|
|
1155
|
+
offerId: 'urn:offer:xyz',
|
|
1156
|
+
timeoutSeconds: 5,
|
|
1157
|
+
intervalSeconds: 1,
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
assert.equal(result.poll.status, 200);
|
|
1161
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/registry/org.schema/Order/_batch');
|
|
1162
|
+
const claims = calls[0].body?.body?.data?.[0]?.meta?.claims || {};
|
|
1163
|
+
assert.equal(claims['Order.acceptedOffer.identifier'], 'urn:offer:xyz');
|
|
1164
|
+
} finally {
|
|
1165
|
+
globalThis.fetch = originalFetch;
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
test('constructor default ctx allows onboarding calls without passing ctx each time', async () => {
|
|
1170
|
+
const calls = [];
|
|
1171
|
+
const originalFetch = globalThis.fetch;
|
|
1172
|
+
globalThis.fetch = async (url, options) => {
|
|
1173
|
+
calls.push({ url: String(url), options, body: options?.body ? JSON.parse(options.body) : undefined });
|
|
1174
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202); // activate submit
|
|
1175
|
+
if (calls.length === 2) {
|
|
1176
|
+
return jsonResponse({
|
|
1177
|
+
body: { data: [{ meta: { claims: { 'org.schema.Offer.identifier': 'urn:offer:def' } } }] },
|
|
1178
|
+
}, 200); // activate poll
|
|
1179
|
+
}
|
|
1180
|
+
if (calls.length === 3) return jsonResponse({ accepted: true }, 202); // order submit
|
|
1181
|
+
return jsonResponse({ status: 'COMPLETED' }, 200); // order poll
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
try {
|
|
1185
|
+
const client = new DataspaceNodeClient({
|
|
1186
|
+
baseUrl: 'http://localhost:3000',
|
|
1187
|
+
bearerToken: 'tok',
|
|
1188
|
+
ctx: { tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' },
|
|
1189
|
+
});
|
|
1190
|
+
const activation = await client.activateOrganizationInGatewayFromIcaProof(undefined, { vpToken: 'vp-ctx-001' }, { timeoutMs: 5000, intervalMs: 1 });
|
|
1191
|
+
const offerId = client.getOfferIdFromResponse(activation);
|
|
1192
|
+
assert.equal(offerId, 'urn:offer:def');
|
|
1193
|
+
const legalOrgOrder = await client.confirmLegalOrganizationOrderSimple({
|
|
1194
|
+
jurisdiction: 'ES',
|
|
1195
|
+
sector: 'health-care',
|
|
1196
|
+
offerId: offerId,
|
|
1197
|
+
timeoutSeconds: 5,
|
|
1198
|
+
intervalSeconds: 1,
|
|
1199
|
+
});
|
|
1200
|
+
assert.equal(legalOrgOrder.poll.status, 200);
|
|
1201
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/registry/org.schema/Organization/_activate');
|
|
1202
|
+
assert.equal(calls[2].url, 'http://localhost:3000/host/cds-ES/v1/health-care/registry/org.schema/Order/_batch');
|
|
1203
|
+
} finally {
|
|
1204
|
+
globalThis.fetch = originalFetch;
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
test('setTenantId/setJurisdiction/setSector configure default ctx for simple methods', async () => {
|
|
1209
|
+
const calls = [];
|
|
1210
|
+
const originalFetch = globalThis.fetch;
|
|
1211
|
+
|
|
1212
|
+
globalThis.fetch = async (url, options) => {
|
|
1213
|
+
calls.push({ url: String(url), options });
|
|
1214
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
1215
|
+
if (calls.length === 2) {
|
|
1216
|
+
return jsonResponse({
|
|
1217
|
+
body: {
|
|
1218
|
+
data: [{
|
|
1219
|
+
meta: { claims: { 'org.schema.Offer.identifier': 'offer-setter-001' } },
|
|
1220
|
+
}],
|
|
1221
|
+
},
|
|
1222
|
+
}, 200);
|
|
1223
|
+
}
|
|
1224
|
+
if (calls.length === 3) return jsonResponse({ accepted: true }, 202);
|
|
1225
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
try {
|
|
1229
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'id-token' });
|
|
1230
|
+
client.setTenantId('acme').setJurisdiction('ES').setSector('health-care');
|
|
1231
|
+
|
|
1232
|
+
const activation = await client.activateOrganizationInGatewaySimple({
|
|
1233
|
+
vpToken: 'vp-token-001',
|
|
1234
|
+
serviceProviderDidWeb: 'did:web:api.acme.org',
|
|
1235
|
+
controllerEmail: 'owner@acme.org',
|
|
1236
|
+
controllerRole: 'ISCO-08|1112',
|
|
1237
|
+
});
|
|
1238
|
+
const offerId = client.getOfferIdFromResponse(activation);
|
|
1239
|
+
assert.equal(offerId, 'offer-setter-001');
|
|
1240
|
+
|
|
1241
|
+
const order = await client.confirmLegalOrganizationOrderSimple({
|
|
1242
|
+
offerId,
|
|
1243
|
+
});
|
|
1244
|
+
assert.equal(order.poll.status, 200);
|
|
1245
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/registry/org.schema/Organization/_activate');
|
|
1246
|
+
assert.equal(calls[2].url, 'http://localhost:3000/host/cds-ES/v1/health-care/registry/org.schema/Order/_batch');
|
|
1247
|
+
} finally {
|
|
1248
|
+
globalThis.fetch = originalFetch;
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
test('setContextOrg configures default ctx for simple methods', async () => {
|
|
1253
|
+
const calls = [];
|
|
1254
|
+
const originalFetch = globalThis.fetch;
|
|
1255
|
+
|
|
1256
|
+
globalThis.fetch = async (url, options) => {
|
|
1257
|
+
calls.push({ url: String(url), options });
|
|
1258
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
1259
|
+
if (calls.length === 2) return jsonResponse({ body: { data: [{ meta: { claims: { 'org.schema.Offer.identifier': 'offer-orgctx-001' } } }] } }, 200);
|
|
1260
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'id-token' });
|
|
1265
|
+
client.setContextOrg({ tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' });
|
|
1266
|
+
const activation = await client.activateOrganizationInGatewaySimple({
|
|
1267
|
+
vpToken: 'vp-token-001',
|
|
1268
|
+
serviceProviderUrl: 'portal.acme.org',
|
|
1269
|
+
controllerTelephone: 'tel:+34600111222',
|
|
1270
|
+
controllerRole: 'ISCO-08|1112',
|
|
1271
|
+
});
|
|
1272
|
+
assert.equal(client.getOfferIdFromResponse(activation), 'offer-orgctx-001');
|
|
1273
|
+
assert.equal(calls[0].url, 'http://localhost:3000/host/cds-ES/v1/health-care/registry/org.schema/Organization/_activate');
|
|
1274
|
+
} finally {
|
|
1275
|
+
globalThis.fetch = originalFetch;
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
test('setDefaultTimeoutSeconds and setDefaultIntervalSeconds apply to simple methods', async () => {
|
|
1280
|
+
const calls = [];
|
|
1281
|
+
const originalFetch = globalThis.fetch;
|
|
1282
|
+
|
|
1283
|
+
globalThis.fetch = async (url, options) => {
|
|
1284
|
+
calls.push({ url: String(url), options });
|
|
1285
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
1286
|
+
if (calls.length === 2) return jsonResponse({ status: 'PENDING' }, 202);
|
|
1287
|
+
return jsonResponse({ status: 'COMPLETED' }, 200);
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
try {
|
|
1291
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000', bearerToken: 'id-token' });
|
|
1292
|
+
client
|
|
1293
|
+
.setContextOrg({ tenantId: 'acme', jurisdiction: 'ES', sector: 'health-care' })
|
|
1294
|
+
.setDefaultTimeoutSeconds(5)
|
|
1295
|
+
.setDefaultIntervalSeconds(0.001);
|
|
1296
|
+
|
|
1297
|
+
const result = await client.activateOrganizationInGatewaySimple({
|
|
1298
|
+
vpToken: 'vp-token-001',
|
|
1299
|
+
serviceProviderDidWeb: 'did:web:api.acme.org',
|
|
1300
|
+
controllerEmail: 'owner@acme.org',
|
|
1301
|
+
controllerRole: 'ISCO-08|1112',
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
assert.equal(result.poll.status, 200);
|
|
1305
|
+
assert.equal(calls.length, 3);
|
|
1306
|
+
} finally {
|
|
1307
|
+
globalThis.fetch = originalFetch;
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
test('activateOrganizationInGatewaySimple throws on business-level error in DIDComm entry', async () => {
|
|
1312
|
+
const calls = [];
|
|
1313
|
+
const originalFetch = globalThis.fetch;
|
|
1314
|
+
|
|
1315
|
+
globalThis.fetch = async (url, options) => {
|
|
1316
|
+
calls.push({ url: String(url), options });
|
|
1317
|
+
if (calls.length === 1) return jsonResponse({ accepted: true }, 202);
|
|
1318
|
+
return jsonResponse({
|
|
1319
|
+
data: [{
|
|
1320
|
+
type: 'Organization-activation-request-v1.0',
|
|
1321
|
+
response: {
|
|
1322
|
+
status: '400',
|
|
1323
|
+
outcome: { issue: [{ diagnostics: 'Missing ICA-issued organization credential.' }] },
|
|
1324
|
+
},
|
|
1325
|
+
}],
|
|
1326
|
+
resourceType: 'Bundle',
|
|
1327
|
+
type: 'batch-response',
|
|
1328
|
+
total: 1,
|
|
1329
|
+
}, 200);
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
try {
|
|
1333
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000' });
|
|
1334
|
+
await assert.rejects(
|
|
1335
|
+
() => client.activateOrganizationInGatewaySimple({
|
|
1336
|
+
jurisdiction: 'ES',
|
|
1337
|
+
sector: 'test',
|
|
1338
|
+
vpToken: 'vp-token-001',
|
|
1339
|
+
serviceProviderDidWeb: 'did:web:api.acme.org',
|
|
1340
|
+
controllerEmail: 'owner@acme.org',
|
|
1341
|
+
controllerRole: 'ISCO-08|1112',
|
|
1342
|
+
}),
|
|
1343
|
+
/Missing ICA-issued organization credential\./,
|
|
1344
|
+
);
|
|
1345
|
+
} finally {
|
|
1346
|
+
globalThis.fetch = originalFetch;
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test('activateOrganizationInGatewaySimple requires controller identity and role', async () => {
|
|
1351
|
+
const client = new DataspaceNodeClient({ baseUrl: 'http://localhost:3000' });
|
|
1352
|
+
await assert.rejects(
|
|
1353
|
+
() => client.activateOrganizationInGatewaySimple({
|
|
1354
|
+
jurisdiction: 'ES',
|
|
1355
|
+
sector: 'test',
|
|
1356
|
+
vpToken: 'vp-token-001',
|
|
1357
|
+
serviceProviderDidWeb: 'did:web:api.acme.org',
|
|
1358
|
+
controllerRole: 'ISCO-08|1112',
|
|
1359
|
+
}),
|
|
1360
|
+
/requires controllerEmail or controllerTelephone/i,
|
|
1361
|
+
);
|
|
1362
|
+
await assert.rejects(
|
|
1363
|
+
() => client.activateOrganizationInGatewaySimple({
|
|
1364
|
+
jurisdiction: 'ES',
|
|
1365
|
+
sector: 'test',
|
|
1366
|
+
vpToken: 'vp-token-001',
|
|
1367
|
+
serviceProviderDidWeb: 'did:web:api.acme.org',
|
|
1368
|
+
controllerEmail: 'admin@acme.org',
|
|
1369
|
+
controllerRole: '',
|
|
1370
|
+
}),
|
|
1371
|
+
/requires controllerRole/i,
|
|
1372
|
+
);
|
|
1373
|
+
await assert.rejects(
|
|
1374
|
+
() => client.activateOrganizationInGatewaySimple({
|
|
1375
|
+
jurisdiction: 'ES',
|
|
1376
|
+
sector: 'test',
|
|
1377
|
+
vpToken: 'vp-token-001',
|
|
1378
|
+
controllerEmail: 'admin@acme.org',
|
|
1379
|
+
controllerRole: 'ISCO-08|1112',
|
|
1380
|
+
}),
|
|
1381
|
+
/requires serviceProviderDidWeb or serviceProviderUrl/i,
|
|
1382
|
+
);
|
|
1383
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"iss": "did:web:controller.example.org",
|
|
3
|
+
"sub": "did:web:controller.example.org",
|
|
4
|
+
"aud": "did:web:host.example.com",
|
|
5
|
+
"vp": {
|
|
6
|
+
"@context": [
|
|
7
|
+
"https://www.w3.org/ns/credentials/v2"
|
|
8
|
+
],
|
|
9
|
+
"type": [
|
|
10
|
+
"VerifiablePresentation"
|
|
11
|
+
],
|
|
12
|
+
"holder": "did:web:controller.example.org",
|
|
13
|
+
"verifiableCredential": [
|
|
14
|
+
{
|
|
15
|
+
"@context": [
|
|
16
|
+
"https://www.w3.org/ns/credentials/v2",
|
|
17
|
+
"https://schema.org"
|
|
18
|
+
],
|
|
19
|
+
"type": [
|
|
20
|
+
"VerifiableCredential",
|
|
21
|
+
"OrganizationCredential"
|
|
22
|
+
],
|
|
23
|
+
"issuer": "did:web:localhost%3A3310",
|
|
24
|
+
"credentialSubject": {
|
|
25
|
+
"id": "did:web:globaldatacare.es:onehealth:organization:taxid:VATES-B00000000",
|
|
26
|
+
"@type": "Organization",
|
|
27
|
+
"legalName": "Example Data Provider SL",
|
|
28
|
+
"taxID": "VATES-B00000000",
|
|
29
|
+
"sameAs": "did:web:provider.example.org",
|
|
30
|
+
"url": "provider.example.org",
|
|
31
|
+
"alternateName": "example-provider",
|
|
32
|
+
"identifier": "did:web:globaldatacare.es:onehealth:organization:taxid:VATES-B00000000",
|
|
33
|
+
"identifierType": "TAX",
|
|
34
|
+
"identifierValue": "VATES-B00000000",
|
|
35
|
+
"address": {
|
|
36
|
+
"@type": "PostalAddress",
|
|
37
|
+
"addressCountry": "ES"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"@context": [
|
|
43
|
+
"https://www.w3.org/ns/credentials/v2",
|
|
44
|
+
"https://schema.org"
|
|
45
|
+
],
|
|
46
|
+
"type": [
|
|
47
|
+
"VerifiableCredential",
|
|
48
|
+
"PersonCredential",
|
|
49
|
+
"LegalRepresentativeCredential"
|
|
50
|
+
],
|
|
51
|
+
"issuer": "did:web:localhost%3A3310",
|
|
52
|
+
"credentialSubject": {
|
|
53
|
+
"id": "urn:person:identifier:IDCES-99999999R",
|
|
54
|
+
"@type": "Person",
|
|
55
|
+
"name": "Alex Example",
|
|
56
|
+
"identifier": "IDCES-99999999R",
|
|
57
|
+
"sameAs": "did:web:controller.example.org",
|
|
58
|
+
"memberOf": {
|
|
59
|
+
"@type": "Organization",
|
|
60
|
+
"taxID": "VATES-B00000000"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
function b64url(input) {
|
|
4
|
+
return Buffer.from(input).toString('base64url');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildUnsignedVpJwt(payload) {
|
|
8
|
+
const now = Math.floor(Date.now() / 1000);
|
|
9
|
+
const header = { alg: 'none', typ: 'JWT' };
|
|
10
|
+
const claims = {
|
|
11
|
+
iat: now,
|
|
12
|
+
exp: now + 600,
|
|
13
|
+
nonce: `nonce-${now}`,
|
|
14
|
+
...payload,
|
|
15
|
+
};
|
|
16
|
+
return `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(claims))}.`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadVpPayloadFixture(path) {
|
|
20
|
+
const raw = fs.readFileSync(path, 'utf8');
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
|