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.
Files changed (37) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +44 -1
  3. package/dist/client.d.ts +153 -33
  4. package/dist/client.js +619 -93
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +1 -0
  7. package/dist/types.d.ts +112 -1
  8. package/dist/vp-token.d.ts +37 -0
  9. package/dist/vp-token.js +56 -0
  10. package/docs/API.md +19 -4
  11. package/docs/BACKEND_NODE_INTEGRATION.md +249 -0
  12. package/docs/CONTROLLER_FLOW_STEP_BY_STEP.md +283 -0
  13. package/docs/DATA_MODEL_ALIGNMENT.md +37 -13
  14. package/docs/DEVELOPER_USE_CASES.md +10 -2
  15. package/docs/E2E_LOCAL_GW_UC5.md +49 -0
  16. package/docs/ENDPOINT_ID_CATALOG.md +90 -0
  17. package/docs/LEGAL_ORGANIZATION_FLOW_STEP_BY_STEP.md +84 -0
  18. package/docs/PERSONAL_FLOW_STEP_BY_STEP.md +70 -0
  19. package/docs/PORTAL_BACKEND_INTEGRATION_HANDOVER.md +343 -0
  20. package/docs/PRACTITIONER_FLOW_STEP_BY_STEP.md +182 -0
  21. package/docs/REACT_WEB_INTEGRATION.md +72 -0
  22. package/examples/e2e-bootstrap-tenant.mjs +3 -2
  23. package/examples/e2e-individual-flow.mjs +13 -8
  24. package/examples/host-activate-and-employee.mjs +3 -2
  25. package/examples/smoke-legal-org-local.mjs +40 -0
  26. package/package.json +4 -3
  27. package/src/client.ts +784 -132
  28. package/src/index.ts +1 -0
  29. package/src/types.ts +123 -1
  30. package/src/vp-token.ts +91 -0
  31. package/tests/client.test.mjs +491 -0
  32. package/tests/fixtures/ica-vp-minimal.json +67 -0
  33. package/tests/helpers/vp-token-fixture.mjs +23 -0
  34. package/tests/live-gw-uc5.e2e.test.mjs +108 -0
  35. package/SDK_PARITY_MAP.md +0 -120
  36. package/TODO_PROMPT_NEXT_STEPS.md +0 -185
  37. package/artifacts/update-smart-wallet.js +0 -1016
@@ -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
+