gdc-common-utils-ts 1.12.0 → 1.14.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/README.md CHANGED
@@ -89,9 +89,68 @@ import { JweObject, JwtCompactParts } from 'gdc-common-utils-ts/models';
89
89
  - [docs/DATASPACE_DISCOVERY_ROADMAP.md](docs/DATASPACE_DISCOVERY_ROADMAP.md)
90
90
  - cross-repo contract for dataspace discovery semantics, EU coverage
91
91
  inference, shared DTOs, and parameterized examples
92
+ - [docs/DATASPACE_DISCOVERY_DEFAULTS_101.md](docs/DATASPACE_DISCOVERY_DEFAULTS_101.md)
93
+ - portal/backend bootstrap guide for `defaults-only`, `default-first`, and
94
+ `internet-first` discovery seeding by `jurisdiction + version + networkType`
92
95
  - [docs/consent-access-matrix-task.md](docs/consent-access-matrix-task.md)
93
96
  - next-step design/task document for active consent aggregation, explicit deny precedence, controller views, permission-request communications, and SMART access evaluation
94
97
 
98
+ ## Dataspace Protocol And Discovery
99
+
100
+ Use `gdc-common-utils-ts` as the shared source of truth for DSP route building,
101
+ `dspace-version` metadata, and normalized discovery DTOs.
102
+
103
+ Main entry points:
104
+
105
+ - [`src/utils/dataspace-protocol.ts`](src/utils/dataspace-protocol.ts)
106
+ - canonical GW CORE path builders for host-scoped and tenant-scoped DSP
107
+ routes
108
+ - [`src/utils/dataspace-discovery.ts`](src/utils/dataspace-discovery.ts)
109
+ - semantic extraction, provider filtering, default DTO builders, and the
110
+ copy/paste fetcher harness used by docs/tests
111
+ - [`src/utils/dataspace-discovery-defaults.ts`](src/utils/dataspace-discovery-defaults.ts)
112
+ - defaults registry for ICAs and hosting operators plus the backend
113
+ `default-first` bootstrap plan used to unblock portal integration
114
+ - [`src/examples/dataspace-discovery.ts`](src/examples/dataspace-discovery.ts)
115
+ - synthetic provider/operator examples that distinguish discovery URL from
116
+ derived catalog artifact URL
117
+ - [`docs/DATASPACE_DISCOVERY_DEFAULTS_101.md`](docs/DATASPACE_DISCOVERY_DEFAULTS_101.md)
118
+ - copy/paste backend bootstrap guide for portal `default-first` rollout
119
+ - [`__tests__/dataspace-discovery-defaults.101.test.ts`](__tests__/dataspace-discovery-defaults.101.test.ts)
120
+ - executable defaults-registry examples for ICAs, hosting operators, and
121
+ source-mode behavior
122
+ - [`__tests__/dataspace-protocol.test.ts`](__tests__/dataspace-protocol.test.ts)
123
+ - executable path and `dspace-version` examples
124
+ - [`__tests__/dataspace-discovery.test.ts`](__tests__/dataspace-discovery.test.ts)
125
+ - executable semantic extraction and filtering examples
126
+
127
+ Copy/paste example:
128
+
129
+ ```ts
130
+ import {
131
+ buildDspaceVersionMetadata,
132
+ buildGwCatalogArtifactPath,
133
+ buildGwDspaceVersionWellKnownPath,
134
+ deriveGwCatalogArtifactUrlFromDspaceVersion,
135
+ } from 'gdc-common-utils-ts/utils/dataspace-protocol';
136
+ import { HostNetworkTypes } from 'gdc-common-utils-ts/constants/network';
137
+
138
+ const hostContext = {
139
+ participantId: 'host',
140
+ jurisdiction: 'ES',
141
+ version: 'v1',
142
+ hostNetwork: HostNetworkTypes.Test,
143
+ };
144
+
145
+ const discoveryPath = buildGwDspaceVersionWellKnownPath(hostContext);
146
+ const metadata = buildDspaceVersionMetadata('/host/cds-ES/v1/test/dsp');
147
+ const catalogPath = buildGwCatalogArtifactPath(hostContext);
148
+ const catalogUrl = deriveGwCatalogArtifactUrlFromDspaceVersion(
149
+ `https://host.example.org${discoveryPath}`,
150
+ metadata,
151
+ );
152
+ ```
153
+
95
154
  ## API Index
96
155
 
97
156
  The canonical API contract should live in JSDoc on exported code. The README acts as a navigable index.
@@ -116,8 +175,8 @@ The canonical API contract should live in JSDoc on exported code. The README act
116
175
  - Reusable professional role/permission examples tying actor role, consent action, SMART scope, and expected FHIR resource types together.
117
176
  - [`DeviceUserClasses`, `DeviceAppTypes`](src/constants/device.ts)
118
177
  - Shared user-class and app/device-type constants used by licensing and SDK flows.
119
- - [`NodeOperatorNetworkTypes`](src/constants/network.ts)
120
- - Shared network/environment labels for node-operator discovery/bootstrap.
178
+ - [`HostNetworkTypes`](src/constants/network.ts)
179
+ - Shared network/environment labels for host discovery/bootstrap.
121
180
  - [`SmartGatewayScopesFhirR4`](src/constants/smart.ts)
122
181
  - Current CORE GW SMART scope literals such as `organization/Consent.cruds`.
123
182
  - Treat these as optional elevated scopes. Do not add them to the first read-only tutorial by default.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared discovery-source policies used by backend/bootstrap layers that must
3
+ * decide whether to start from configured defaults or from live internet/ICA
4
+ * discovery.
5
+ */
6
+ export declare const DataspaceDiscoverySourceMode: Readonly<{
7
+ readonly DefaultFirst: "default-first";
8
+ readonly DefaultsOnly: "defaults-only";
9
+ readonly InternetFirst: "internet-first";
10
+ }>;
11
+ export type DataspaceDiscoverySourceModeValue = typeof DataspaceDiscoverySourceMode[keyof typeof DataspaceDiscoverySourceMode];
@@ -0,0 +1,11 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ /**
3
+ * Shared discovery-source policies used by backend/bootstrap layers that must
4
+ * decide whether to start from configured defaults or from live internet/ICA
5
+ * discovery.
6
+ */
7
+ export const DataspaceDiscoverySourceMode = Object.freeze({
8
+ DefaultFirst: 'default-first',
9
+ DefaultsOnly: 'defaults-only',
10
+ InternetFirst: 'internet-first',
11
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Canonical Dataspace Protocol version identifiers used by the current GW/SDK
3
+ * discovery flows.
4
+ */
5
+ export declare const DataspaceProtocolVersions: Readonly<{
6
+ readonly Current: "2025-1";
7
+ }>;
8
+ /**
9
+ * Canonical well-known paths used by Dataspace Protocol discovery.
10
+ */
11
+ export declare const DataspaceWellKnownPaths: Readonly<{
12
+ readonly VersionMetadata: "/.well-known/dspace-version";
13
+ }>;
14
+ /**
15
+ * Project-local GW CORE DSP binding paths.
16
+ *
17
+ * These preserve the current internal API structure while still ending in the
18
+ * DSP catalog route shapes.
19
+ */
20
+ export declare const GwDataspaceBindingPaths: Readonly<{
21
+ readonly Base: "/dsp";
22
+ readonly CatalogCollectionSuffix: "/catalog";
23
+ readonly CatalogRequestSuffix: "/catalog/request";
24
+ readonly CatalogArtifactSuffix: "/catalog/dcat.json";
25
+ readonly CatalogDatasetsPrefix: "/catalog/datasets";
26
+ }>;
27
+ export type DataspaceProtocolVersionValue = typeof DataspaceProtocolVersions[keyof typeof DataspaceProtocolVersions];
@@ -0,0 +1,27 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ /**
3
+ * Canonical Dataspace Protocol version identifiers used by the current GW/SDK
4
+ * discovery flows.
5
+ */
6
+ export const DataspaceProtocolVersions = Object.freeze({
7
+ Current: '2025-1',
8
+ });
9
+ /**
10
+ * Canonical well-known paths used by Dataspace Protocol discovery.
11
+ */
12
+ export const DataspaceWellKnownPaths = Object.freeze({
13
+ VersionMetadata: '/.well-known/dspace-version',
14
+ });
15
+ /**
16
+ * Project-local GW CORE DSP binding paths.
17
+ *
18
+ * These preserve the current internal API structure while still ending in the
19
+ * DSP catalog route shapes.
20
+ */
21
+ export const GwDataspaceBindingPaths = Object.freeze({
22
+ Base: '/dsp',
23
+ CatalogCollectionSuffix: '/catalog',
24
+ CatalogRequestSuffix: '/catalog/request',
25
+ CatalogArtifactSuffix: '/catalog/dcat.json',
26
+ CatalogDatasetsPrefix: '/catalog/datasets',
27
+ });
@@ -1,6 +1,8 @@
1
1
  export * from './actor-session';
2
2
  export * from './communication';
3
3
  export * from './cryptography';
4
+ export * from './dataspace-discovery';
5
+ export * from './dataspace-protocol';
4
6
  export * from './device';
5
7
  export * from './did-services';
6
8
  export * from './eu-countries';
@@ -1,6 +1,8 @@
1
1
  export * from './actor-session.js';
2
2
  export * from './communication.js';
3
3
  export * from './cryptography.js';
4
+ export * from './dataspace-discovery.js';
5
+ export * from './dataspace-protocol.js';
4
6
  export * from './device.js';
5
7
  export * from './did-services.js';
6
8
  export * from './eu-countries.js';
@@ -1,13 +1,25 @@
1
1
  /**
2
- * Canonical network/environment labels used to identify node-operator networks
3
- * during discovery/bootstrap flows.
2
+ * Canonical network/environment labels used to identify host networks during
3
+ * discovery/bootstrap flows.
4
4
  *
5
- * These labels do not replace the clinical route `sector`. They describe the
6
- * operator environment itself.
5
+ * These labels do not replace the business route `sector`. They describe the
6
+ * host environment itself.
7
+ */
8
+ export declare const HostNetworkTypes: Readonly<{
9
+ readonly Test: "test";
10
+ readonly TestNetwork: "test-network";
11
+ readonly Network: "network";
12
+ }>;
13
+ export type HostNetworkType = typeof HostNetworkTypes[keyof typeof HostNetworkTypes];
14
+ /**
15
+ * @deprecated Use `HostNetworkTypes`.
7
16
  */
8
17
  export declare const NodeOperatorNetworkTypes: Readonly<{
9
18
  readonly Test: "test";
10
19
  readonly TestNetwork: "test-network";
11
20
  readonly Network: "network";
12
21
  }>;
13
- export type NodeOperatorNetworkType = typeof NodeOperatorNetworkTypes[keyof typeof NodeOperatorNetworkTypes];
22
+ /**
23
+ * @deprecated Use `HostNetworkType`.
24
+ */
25
+ export type NodeOperatorNetworkType = HostNetworkType;
@@ -1,13 +1,17 @@
1
1
  // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  /**
3
- * Canonical network/environment labels used to identify node-operator networks
4
- * during discovery/bootstrap flows.
3
+ * Canonical network/environment labels used to identify host networks during
4
+ * discovery/bootstrap flows.
5
5
  *
6
- * These labels do not replace the clinical route `sector`. They describe the
7
- * operator environment itself.
6
+ * These labels do not replace the business route `sector`. They describe the
7
+ * host environment itself.
8
8
  */
9
- export const NodeOperatorNetworkTypes = Object.freeze({
9
+ export const HostNetworkTypes = Object.freeze({
10
10
  Test: 'test',
11
11
  TestNetwork: 'test-network',
12
12
  Network: 'network',
13
13
  });
14
+ /**
15
+ * @deprecated Use `HostNetworkTypes`.
16
+ */
17
+ export const NodeOperatorNetworkTypes = HostNetworkTypes;
@@ -75,6 +75,11 @@ export declare function buildExampleTenantServiceMetaClaims(input?: ExampleDatas
75
75
  * Builds a synthetic published-provider record as it would appear in a host
76
76
  * service-autodiscovery catalog.
77
77
  *
78
+ * URL rule:
79
+ * - `discoveryUrl` is the participant-scoped `/.well-known/dspace-version`
80
+ * entrypoint
81
+ * - `catalogUrl` is the derived `/dsp/catalog/dcat.json` artifact
82
+ *
78
83
  * @param input Optional overrides for the synthetic provider publication.
79
84
  * @returns Shared host-catalog provider entry.
80
85
  */
@@ -82,6 +87,11 @@ export declare function buildExamplePublishedProviderCatalogRecord(input?: Examp
82
87
  /**
83
88
  * Builds a synthetic host/operator service-autodiscovery catalog.
84
89
  *
90
+ * URL rule:
91
+ * - `discoveryUrl` is the canonical entrypoint clients should fetch first
92
+ * - `catalogUrl` is the read-only DSP artifact derived from the advertised
93
+ * base path
94
+ *
85
95
  * @param providers Optional published providers to include.
86
96
  * @returns Shared catalog DTO for host-side public service autodiscovery.
87
97
  */
@@ -1,7 +1,7 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  import { ClaimsOrganizationSchemaorg, ClaimsServiceSchemaorg } from '../constants/schemaorg.js';
3
3
  import { serializeServiceCapabilityTokens, ServiceCapabilityToken } from '../constants/service-capabilities.js';
4
- import { EXAMPLE_HOSTING_OPERATOR_CATALOG_URL, EXAMPLE_HOSTING_OPERATOR_DID, EXAMPLE_PROVIDER_PUBLISHED_ENDPOINT_URL, EXAMPLE_JURISDICTION, EXAMPLE_SECTOR, EXAMPLE_TENANT_SERVICE_DID, } from './shared.js';
4
+ import { EXAMPLE_HOSTING_OPERATOR_CATALOG_ARTIFACT_URL, EXAMPLE_HOSTING_OPERATOR_DSPACE_VERSION_URL, EXAMPLE_HOSTING_OPERATOR_DID, EXAMPLE_PROVIDER_PUBLISHED_ENDPOINT_URL, EXAMPLE_JURISDICTION, EXAMPLE_SECTOR, EXAMPLE_TENANT_SERVICE_DID, } from './shared.js';
5
5
  function firstOrCsv(values) {
6
6
  return values.length <= 1 ? (values[0] || '') : values.join(',');
7
7
  }
@@ -98,6 +98,11 @@ export function buildExampleTenantServiceMetaClaims(input = {}) {
98
98
  * Builds a synthetic published-provider record as it would appear in a host
99
99
  * service-autodiscovery catalog.
100
100
  *
101
+ * URL rule:
102
+ * - `discoveryUrl` is the participant-scoped `/.well-known/dspace-version`
103
+ * entrypoint
104
+ * - `catalogUrl` is the derived `/dsp/catalog/dcat.json` artifact
105
+ *
101
106
  * @param input Optional overrides for the synthetic provider publication.
102
107
  * @returns Shared host-catalog provider entry.
103
108
  */
@@ -109,21 +114,28 @@ export function buildExamplePublishedProviderCatalogRecord(input = {}) {
109
114
  providerDid: input.did || EXAMPLE_TENANT_SERVICE_DID,
110
115
  serviceType: serviceTypes[0] || ServiceCapabilityToken.IndexProvider,
111
116
  category: categories[0] || EXAMPLE_SECTOR,
112
- areaServed: areaServed[0] || 'EU',
117
+ areaServed: firstOrCsv(areaServed) || 'EU',
113
118
  endpointUrl: EXAMPLE_PROVIDER_PUBLISHED_ENDPOINT_URL,
114
- catalogUrl: EXAMPLE_HOSTING_OPERATOR_CATALOG_URL,
119
+ discoveryUrl: EXAMPLE_HOSTING_OPERATOR_DSPACE_VERSION_URL,
120
+ catalogUrl: EXAMPLE_HOSTING_OPERATOR_CATALOG_ARTIFACT_URL,
115
121
  };
116
122
  }
117
123
  /**
118
124
  * Builds a synthetic host/operator service-autodiscovery catalog.
119
125
  *
126
+ * URL rule:
127
+ * - `discoveryUrl` is the canonical entrypoint clients should fetch first
128
+ * - `catalogUrl` is the read-only DSP artifact derived from the advertised
129
+ * base path
130
+ *
120
131
  * @param providers Optional published providers to include.
121
132
  * @returns Shared catalog DTO for host-side public service autodiscovery.
122
133
  */
123
134
  export function buildExampleHostingOperatorDiscoveryCatalog(providers = [buildExamplePublishedProviderCatalogRecord()]) {
124
135
  return {
125
136
  hostingOperatorDid: EXAMPLE_HOSTING_OPERATOR_DID,
126
- catalogUrl: EXAMPLE_HOSTING_OPERATOR_CATALOG_URL,
137
+ discoveryUrl: EXAMPLE_HOSTING_OPERATOR_DSPACE_VERSION_URL,
138
+ catalogUrl: EXAMPLE_HOSTING_OPERATOR_CATALOG_ARTIFACT_URL,
127
139
  providers: [...providers],
128
140
  };
129
141
  }
@@ -10,6 +10,8 @@
10
10
  */
11
11
  export declare const EXAMPLE_TENANT_IDENTIFIER: "acme-id";
12
12
  export declare const EXAMPLE_JURISDICTION: "ES";
13
+ export declare const EXAMPLE_NETWORK_TYPE: "test";
14
+ export declare const EXAMPLE_ROUTE_VERSION: "v1";
13
15
  export declare const EXAMPLE_SECTOR: "health-care";
14
16
  export declare const EXAMPLE_EMAIL_CONTROLLER_ORG: "controller@acme.org";
15
17
  export declare const EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL: "ana.parent@example.org";
@@ -20,7 +22,7 @@ export declare const EXAMPLE_TENANT_ROUTE_CONTEXT: {
20
22
  };
21
23
  export declare const EXAMPLE_HOST_ROUTE_CONTEXT: {
22
24
  readonly jurisdiction: "ES";
23
- readonly sector: "health-care";
25
+ readonly sector: "test";
24
26
  };
25
27
  export declare const EXAMPLE_CONTROLLER_DID: "did:web:people.acme.org:controllers:primary";
26
28
  export declare const EXAMPLE_CONTROLLER_EMAIL: "controller@acme.org";
@@ -33,10 +35,15 @@ export declare const EXAMPLE_PROVIDER_ORGANIZATION_DID: "did:web:hospital.acme.o
33
35
  export declare const EXAMPLE_PROVIDER_ORGANIZATION_URL: "https://hospital.acme.org";
34
36
  export declare const EXAMPLE_GATEWAY_PUBLIC_ORIGIN: "https://gateway.example.com";
35
37
  export declare const EXAMPLE_HOST_PUBLIC_HOSTNAME: "host.example.com";
38
+ export declare const EXAMPLE_DEFAULT_ICA_URL: "https://ica.example.org";
39
+ export declare const EXAMPLE_DEFAULT_ICA_DID: "did:web:ica.example.org";
36
40
  export declare const EXAMPLE_HOSTING_OPERATOR_DID: "did:web:host.example.org";
37
41
  export declare const EXAMPLE_TENANT_SERVICE_DID: "did:web:provider.example.org";
38
42
  export declare const EXAMPLE_SECONDARY_TENANT_SERVICE_DID: "did:web:provider-b.example.org";
39
- export declare const EXAMPLE_HOSTING_OPERATOR_CATALOG_URL: "https://host.example.org/.well-known/dcat3/catalog";
43
+ export declare const EXAMPLE_HOSTING_OPERATOR_DSPACE_VERSION_URL: "https://host.example.org/host/cds-ES/v1/test/.well-known/dspace-version";
44
+ export declare const EXAMPLE_HOSTING_OPERATOR_CATALOG_ARTIFACT_URL: "https://host.example.org/host/cds-ES/v1/test/dsp/catalog/dcat.json";
45
+ /** @deprecated Use `EXAMPLE_HOSTING_OPERATOR_DSPACE_VERSION_URL`. */
46
+ export declare const EXAMPLE_HOSTING_OPERATOR_CATALOG_URL: "https://host.example.org/host/cds-ES/v1/test/.well-known/dspace-version";
40
47
  export declare const EXAMPLE_PROVIDER_PUBLISHED_ENDPOINT_URL: "https://host.example.org/catalog/provider-a";
41
48
  export declare const EXAMPLE_PROVIDER_LEGAL_NAME: "ACME Health Provider";
42
49
  export declare const EXAMPLE_SECONDARY_PROVIDER_LEGAL_NAME: "Reader Only Provider";
@@ -1,6 +1,7 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
2
  // Always create JSDoc, do not use strings inline in keys nor values, use types instead, and reuse the data test examples.
3
3
  import { DataspaceSectors } from '../constants/sectors.js';
4
+ import { HostNetworkTypes } from '../constants/network.js';
4
5
  import { HealthcareActorRoles, HealthcareBasicSections, HealthcareConsentPurposes, } from '../constants/healthcare.js';
5
6
  import { CommunicationClaim } from '../models/interoperable-claims/communication-claims.js';
6
7
  /**
@@ -15,6 +16,8 @@ import { CommunicationClaim } from '../models/interoperable-claims/communication
15
16
  */
16
17
  export const EXAMPLE_TENANT_IDENTIFIER = 'acme-id';
17
18
  export const EXAMPLE_JURISDICTION = 'ES';
19
+ export const EXAMPLE_NETWORK_TYPE = HostNetworkTypes.Test;
20
+ export const EXAMPLE_ROUTE_VERSION = 'v1';
18
21
  export const EXAMPLE_SECTOR = DataspaceSectors.HealthCare;
19
22
  export const EXAMPLE_EMAIL_CONTROLLER_ORG = 'controller@acme.org';
20
23
  export const EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL = 'ana.parent@example.org';
@@ -25,7 +28,7 @@ export const EXAMPLE_TENANT_ROUTE_CONTEXT = {
25
28
  };
26
29
  export const EXAMPLE_HOST_ROUTE_CONTEXT = {
27
30
  jurisdiction: EXAMPLE_JURISDICTION,
28
- sector: EXAMPLE_SECTOR,
31
+ sector: HostNetworkTypes.Test,
29
32
  };
30
33
  export const EXAMPLE_CONTROLLER_DID = 'did:web:people.acme.org:controllers:primary';
31
34
  export const EXAMPLE_CONTROLLER_EMAIL = EXAMPLE_EMAIL_CONTROLLER_ORG;
@@ -38,10 +41,15 @@ export const EXAMPLE_PROVIDER_ORGANIZATION_DID = 'did:web:hospital.acme.org';
38
41
  export const EXAMPLE_PROVIDER_ORGANIZATION_URL = 'https://hospital.acme.org';
39
42
  export const EXAMPLE_GATEWAY_PUBLIC_ORIGIN = 'https://gateway.example.com';
40
43
  export const EXAMPLE_HOST_PUBLIC_HOSTNAME = 'host.example.com';
44
+ export const EXAMPLE_DEFAULT_ICA_URL = 'https://ica.example.org';
45
+ export const EXAMPLE_DEFAULT_ICA_DID = 'did:web:ica.example.org';
41
46
  export const EXAMPLE_HOSTING_OPERATOR_DID = 'did:web:host.example.org';
42
47
  export const EXAMPLE_TENANT_SERVICE_DID = 'did:web:provider.example.org';
43
48
  export const EXAMPLE_SECONDARY_TENANT_SERVICE_DID = 'did:web:provider-b.example.org';
44
- export const EXAMPLE_HOSTING_OPERATOR_CATALOG_URL = 'https://host.example.org/.well-known/dcat3/catalog';
49
+ export const EXAMPLE_HOSTING_OPERATOR_DSPACE_VERSION_URL = `https://host.example.org/host/cds-ES/${EXAMPLE_ROUTE_VERSION}/${EXAMPLE_NETWORK_TYPE}/.well-known/dspace-version`;
50
+ export const EXAMPLE_HOSTING_OPERATOR_CATALOG_ARTIFACT_URL = `https://host.example.org/host/cds-ES/${EXAMPLE_ROUTE_VERSION}/${EXAMPLE_NETWORK_TYPE}/dsp/catalog/dcat.json`;
51
+ /** @deprecated Use `EXAMPLE_HOSTING_OPERATOR_DSPACE_VERSION_URL`. */
52
+ export const EXAMPLE_HOSTING_OPERATOR_CATALOG_URL = EXAMPLE_HOSTING_OPERATOR_DSPACE_VERSION_URL;
45
53
  export const EXAMPLE_PROVIDER_PUBLISHED_ENDPOINT_URL = 'https://host.example.org/catalog/provider-a';
46
54
  export const EXAMPLE_PROVIDER_LEGAL_NAME = 'ACME Health Provider';
47
55
  export const EXAMPLE_SECONDARY_PROVIDER_LEGAL_NAME = 'Reader Only Provider';
@@ -0,0 +1,78 @@
1
+ import type { DataspaceDiscoverySourceModeValue } from '../constants/dataspace-discovery';
2
+ import type { HostingOperatorSemanticRecord } from './dataspace-discovery';
3
+ /**
4
+ * Shared host/ICA context used to select dataspace discovery defaults.
5
+ *
6
+ * `networkType` belongs to the host/ICA side.
7
+ * Tenant/provider resolution later uses `sector`.
8
+ */
9
+ export type DataspaceDiscoveryNetworkContext = Readonly<{
10
+ jurisdiction: string;
11
+ version: string;
12
+ networkType: string;
13
+ }>;
14
+ /**
15
+ * Configured ICA seed for a given host/network context.
16
+ */
17
+ export type DefaultIcaRegistration = DataspaceDiscoveryNetworkContext & Readonly<{
18
+ icaUrl: string;
19
+ icaDid?: string;
20
+ title?: string;
21
+ }>;
22
+ /**
23
+ * Configured hosting-operator seed for a given host/network context.
24
+ *
25
+ * This mirrors the record shape consumed later by backend resolvers.
26
+ */
27
+ export type DefaultHostingOperatorRegistration = DataspaceDiscoveryNetworkContext & Readonly<{
28
+ operatorDid: string;
29
+ discoveryUrl?: string;
30
+ catalogUrl?: string;
31
+ record: HostingOperatorSemanticRecord;
32
+ title?: string;
33
+ }>;
34
+ /**
35
+ * Optional seed data used when constructing a defaults registry.
36
+ */
37
+ export type DataspaceDiscoveryDefaultsRegistrySeed = Readonly<{
38
+ icas?: readonly DefaultIcaRegistration[];
39
+ hostingOperators?: readonly DefaultHostingOperatorRegistration[];
40
+ }>;
41
+ /**
42
+ * Filter used to list configured ICA defaults for a host/network context.
43
+ */
44
+ export type DataspaceDiscoveryDefaultIcaFilter = Readonly<Partial<DataspaceDiscoveryNetworkContext>>;
45
+ /**
46
+ * Filter used to list configured hosting-operator defaults for a discovery
47
+ * request.
48
+ */
49
+ export type DataspaceDiscoveryDefaultHostingFilter = Readonly<Partial<DataspaceDiscoveryNetworkContext> & {
50
+ sector?: string;
51
+ coverageScope?: string;
52
+ requiredCapabilities?: readonly string[];
53
+ }>;
54
+ /**
55
+ * Input used to decide how a backend should bootstrap discovery for a
56
+ * frontend/API request.
57
+ */
58
+ export type DataspaceDiscoveryBootstrapInput = DataspaceDiscoveryNetworkContext & Readonly<{
59
+ sector: string;
60
+ coverageScope?: string;
61
+ requiredCapabilities?: readonly string[];
62
+ sourceMode?: DataspaceDiscoverySourceModeValue;
63
+ }>;
64
+ /**
65
+ * Bootstrap plan returned by the defaults registry.
66
+ *
67
+ * Backend usage:
68
+ * - use `hostingOperators` immediately when `shouldUseDefaultsFirst` is true
69
+ * - try live ICA/internet only when `shouldTryInternet` is true
70
+ */
71
+ export type DataspaceDiscoveryBootstrapPlan = Readonly<{
72
+ sourceMode: DataspaceDiscoverySourceModeValue;
73
+ icas: DefaultIcaRegistration[];
74
+ hostingOperators: DefaultHostingOperatorRegistration[];
75
+ hasDefaults: boolean;
76
+ shouldUseDefaultsFirst: boolean;
77
+ shouldTryInternet: boolean;
78
+ }>;
@@ -0,0 +1,2 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ export {};
@@ -38,6 +38,7 @@ export type PublishedProviderCatalogRecord = Readonly<{
38
38
  category: string;
39
39
  areaServed?: string;
40
40
  endpointUrl?: string;
41
+ discoveryUrl?: string;
41
42
  catalogUrl?: string;
42
43
  }>;
43
44
  /**
@@ -61,6 +62,7 @@ export type DataspaceDiscoveryFilter = Readonly<{
61
62
  */
62
63
  export type HostingOperatorDiscoveryCatalog = Readonly<{
63
64
  hostingOperatorDid?: string;
65
+ discoveryUrl?: string;
64
66
  catalogUrl?: string;
65
67
  providers: PublishedProviderCatalogRecord[];
66
68
  }>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * One advertised Dataspace Protocol version entry from `/.well-known/dspace-version`.
3
+ */
4
+ export type DspaceProtocolVersionEntry = Readonly<{
5
+ version: string;
6
+ path: string;
7
+ }>;
8
+ /**
9
+ * Minimal version metadata payload returned by the DSP well-known discovery
10
+ * endpoint.
11
+ */
12
+ export type DspaceVersionMetadata = Readonly<{
13
+ protocolVersions: readonly DspaceProtocolVersionEntry[];
14
+ }>;
@@ -0,0 +1,2 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ export {};
@@ -13,6 +13,8 @@ export * from './consent-rule';
13
13
  export * from './consent-access';
14
14
  export * from './crypto';
15
15
  export * from './dataspace-discovery';
16
+ export * from './dataspace-discovery-defaults';
17
+ export * from './dataspace-protocol';
16
18
  export * from './device-license';
17
19
  export * from './did';
18
20
  export * from './fhir-documents';
@@ -13,6 +13,8 @@ export * from './consent-rule.js';
13
13
  export * from './consent-access.js';
14
14
  export * from './crypto.js';
15
15
  export * from './dataspace-discovery.js';
16
+ export * from './dataspace-discovery-defaults.js';
17
+ export * from './dataspace-protocol.js';
16
18
  export * from './device-license.js';
17
19
  export * from './did.js';
18
20
  export * from './fhir-documents.js';
@@ -0,0 +1,66 @@
1
+ import type { DataspaceDiscoveryBootstrapInput, DataspaceDiscoveryBootstrapPlan, DataspaceDiscoveryDefaultHostingFilter, DataspaceDiscoveryDefaultIcaFilter, DataspaceDiscoveryDefaultsRegistrySeed, DefaultHostingOperatorRegistration, DefaultIcaRegistration } from '../models/dataspace-discovery-defaults';
2
+ /**
3
+ * Mutable in-memory registry for discovery defaults used by portal/backend
4
+ * bootstrap code.
5
+ *
6
+ * Scope:
7
+ * - ICA defaults are indexed by `jurisdiction + version + networkType`
8
+ * - hosting defaults are indexed by `jurisdiction + version + networkType`
9
+ * - sector filtering is applied only when listing hosting operators, because
10
+ * sectors belong to tenant/provider discovery rather than to the host network
11
+ *
12
+ * This registry is intentionally simple and synchronous so backend code can
13
+ * preload defaults during startup and immediately serve the frontend in
14
+ * `defaults-only` or `default-first` mode.
15
+ */
16
+ export declare class DataspaceDiscoveryDefaultsRegistry {
17
+ private readonly icas;
18
+ private readonly hostingOperators;
19
+ constructor(seed?: DataspaceDiscoveryDefaultsRegistrySeed);
20
+ /**
21
+ * Adds one ICA default for a concrete host/network context.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * registry.addIca({
26
+ * jurisdiction: 'ES',
27
+ * version: 'v1',
28
+ * networkType: 'test',
29
+ * icaUrl: 'https://ica.example.org',
30
+ * });
31
+ * ```
32
+ */
33
+ addIca(input: DefaultIcaRegistration): this;
34
+ /**
35
+ * Adds one hosting-operator default for a concrete host/network context.
36
+ *
37
+ * The record itself may later serve multiple sectors depending on the
38
+ * semantic categories and capabilities published in `record`.
39
+ */
40
+ addHostingOperator(input: DefaultHostingOperatorRegistration): this;
41
+ /**
42
+ * Lists configured ICA defaults for a host/network context.
43
+ */
44
+ listIcas(filter?: DataspaceDiscoveryDefaultIcaFilter): DefaultIcaRegistration[];
45
+ /**
46
+ * Lists configured hosting-operator defaults for a frontend/backend discovery
47
+ * request.
48
+ *
49
+ * Sector and capability filtering are applied against the semantic host
50
+ * record because those dimensions belong to the provider/tenant side.
51
+ */
52
+ listHostingOperators(filter?: DataspaceDiscoveryDefaultHostingFilter): DefaultHostingOperatorRegistration[];
53
+ /**
54
+ * Builds the backend bootstrap plan for one provider-discovery request.
55
+ *
56
+ * Current default-first policy:
57
+ * - use configured hosting defaults immediately when they exist
58
+ * - only try live ICA/internet when no host defaults match the request and at
59
+ * least one ICA default is configured for the same host/network context
60
+ */
61
+ buildBootstrapPlan(input: DataspaceDiscoveryBootstrapInput): DataspaceDiscoveryBootstrapPlan;
62
+ }
63
+ /**
64
+ * Convenience factory for the in-memory defaults registry.
65
+ */
66
+ export declare function createDataspaceDiscoveryDefaultsRegistry(seed?: DataspaceDiscoveryDefaultsRegistrySeed): DataspaceDiscoveryDefaultsRegistry;
@@ -0,0 +1,172 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { DataspaceDiscoverySourceMode, } from '../constants/dataspace-discovery.js';
3
+ import { matchesHostingOperatorDiscoveryFilter } from './dataspace-discovery.js';
4
+ function normalizeString(value) {
5
+ return typeof value === 'string' ? value.trim() : '';
6
+ }
7
+ function normalizeCapabilities(values) {
8
+ return Array.from(new Set((values || []).map((value) => normalizeString(value)).filter(Boolean)));
9
+ }
10
+ function normalizeIcaRegistration(input) {
11
+ return {
12
+ jurisdiction: normalizeString(input.jurisdiction),
13
+ version: normalizeString(input.version),
14
+ networkType: normalizeString(input.networkType),
15
+ icaUrl: normalizeString(input.icaUrl),
16
+ icaDid: normalizeString(input.icaDid) || undefined,
17
+ title: normalizeString(input.title) || undefined,
18
+ };
19
+ }
20
+ function normalizeHostingOperatorRegistration(input) {
21
+ return {
22
+ jurisdiction: normalizeString(input.jurisdiction),
23
+ version: normalizeString(input.version),
24
+ networkType: normalizeString(input.networkType),
25
+ operatorDid: normalizeString(input.operatorDid),
26
+ discoveryUrl: normalizeString(input.discoveryUrl) || undefined,
27
+ catalogUrl: normalizeString(input.catalogUrl) || undefined,
28
+ record: {
29
+ ...input.record,
30
+ subjectId: normalizeString(input.record.subjectId) || undefined,
31
+ serviceTypes: normalizeCapabilities(input.record.serviceTypes),
32
+ categories: Array.from(new Set((input.record.categories || []).map((value) => normalizeString(value)).filter(Boolean))),
33
+ areaServed: Array.from(new Set((input.record.areaServed || []).map((value) => normalizeString(value)).filter(Boolean))),
34
+ addressCountry: normalizeString(input.record.addressCountry) || undefined,
35
+ coverageScope: normalizeString(input.record.coverageScope) || undefined,
36
+ },
37
+ title: normalizeString(input.title) || undefined,
38
+ };
39
+ }
40
+ function matchesNetworkContext(left, right) {
41
+ if (right.jurisdiction && normalizeString(right.jurisdiction) !== left.jurisdiction)
42
+ return false;
43
+ if (right.version && normalizeString(right.version) !== left.version)
44
+ return false;
45
+ if (right.networkType && normalizeString(right.networkType) !== left.networkType)
46
+ return false;
47
+ return true;
48
+ }
49
+ /**
50
+ * Mutable in-memory registry for discovery defaults used by portal/backend
51
+ * bootstrap code.
52
+ *
53
+ * Scope:
54
+ * - ICA defaults are indexed by `jurisdiction + version + networkType`
55
+ * - hosting defaults are indexed by `jurisdiction + version + networkType`
56
+ * - sector filtering is applied only when listing hosting operators, because
57
+ * sectors belong to tenant/provider discovery rather than to the host network
58
+ *
59
+ * This registry is intentionally simple and synchronous so backend code can
60
+ * preload defaults during startup and immediately serve the frontend in
61
+ * `defaults-only` or `default-first` mode.
62
+ */
63
+ export class DataspaceDiscoveryDefaultsRegistry {
64
+ icas = [];
65
+ hostingOperators = [];
66
+ constructor(seed = {}) {
67
+ for (const registration of seed.icas || []) {
68
+ this.addIca(registration);
69
+ }
70
+ for (const registration of seed.hostingOperators || []) {
71
+ this.addHostingOperator(registration);
72
+ }
73
+ }
74
+ /**
75
+ * Adds one ICA default for a concrete host/network context.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * registry.addIca({
80
+ * jurisdiction: 'ES',
81
+ * version: 'v1',
82
+ * networkType: 'test',
83
+ * icaUrl: 'https://ica.example.org',
84
+ * });
85
+ * ```
86
+ */
87
+ addIca(input) {
88
+ this.icas.push(normalizeIcaRegistration(input));
89
+ return this;
90
+ }
91
+ /**
92
+ * Adds one hosting-operator default for a concrete host/network context.
93
+ *
94
+ * The record itself may later serve multiple sectors depending on the
95
+ * semantic categories and capabilities published in `record`.
96
+ */
97
+ addHostingOperator(input) {
98
+ this.hostingOperators.push(normalizeHostingOperatorRegistration(input));
99
+ return this;
100
+ }
101
+ /**
102
+ * Lists configured ICA defaults for a host/network context.
103
+ */
104
+ listIcas(filter = {}) {
105
+ return this.icas
106
+ .filter((registration) => matchesNetworkContext(registration, filter))
107
+ .map((registration) => ({ ...registration }));
108
+ }
109
+ /**
110
+ * Lists configured hosting-operator defaults for a frontend/backend discovery
111
+ * request.
112
+ *
113
+ * Sector and capability filtering are applied against the semantic host
114
+ * record because those dimensions belong to the provider/tenant side.
115
+ */
116
+ listHostingOperators(filter = {}) {
117
+ return this.hostingOperators
118
+ .filter((registration) => matchesNetworkContext(registration, filter))
119
+ .filter((registration) => {
120
+ if (!filter.sector)
121
+ return true;
122
+ return matchesHostingOperatorDiscoveryFilter(registration.record, {
123
+ sector: normalizeString(filter.sector),
124
+ coverageScope: normalizeString(filter.coverageScope) || undefined,
125
+ requiredCapabilities: normalizeCapabilities(filter.requiredCapabilities),
126
+ });
127
+ })
128
+ .map((registration) => ({
129
+ ...registration,
130
+ record: {
131
+ ...registration.record,
132
+ serviceTypes: [...registration.record.serviceTypes],
133
+ categories: [...registration.record.categories],
134
+ areaServed: [...registration.record.areaServed],
135
+ },
136
+ }));
137
+ }
138
+ /**
139
+ * Builds the backend bootstrap plan for one provider-discovery request.
140
+ *
141
+ * Current default-first policy:
142
+ * - use configured hosting defaults immediately when they exist
143
+ * - only try live ICA/internet when no host defaults match the request and at
144
+ * least one ICA default is configured for the same host/network context
145
+ */
146
+ buildBootstrapPlan(input) {
147
+ const sourceMode = normalizeString(input.sourceMode)
148
+ || DataspaceDiscoverySourceMode.DefaultFirst;
149
+ const icas = this.listIcas(input);
150
+ const hostingOperators = this.listHostingOperators(input);
151
+ const hasDefaults = Boolean(icas.length || hostingOperators.length);
152
+ const shouldUseDefaultsFirst = sourceMode !== DataspaceDiscoverySourceMode.InternetFirst;
153
+ const shouldTryInternet = sourceMode !== DataspaceDiscoverySourceMode.DefaultsOnly
154
+ && icas.length > 0
155
+ && (sourceMode === DataspaceDiscoverySourceMode.InternetFirst
156
+ || hostingOperators.length === 0);
157
+ return {
158
+ sourceMode,
159
+ icas,
160
+ hostingOperators,
161
+ hasDefaults,
162
+ shouldUseDefaultsFirst,
163
+ shouldTryInternet,
164
+ };
165
+ }
166
+ }
167
+ /**
168
+ * Convenience factory for the in-memory defaults registry.
169
+ */
170
+ export function createDataspaceDiscoveryDefaultsRegistry(seed = {}) {
171
+ return new DataspaceDiscoveryDefaultsRegistry(seed);
172
+ }
@@ -1,4 +1,46 @@
1
1
  import { type DataspaceDiscoveryFilter, type DataspaceServiceSemanticRecord, type HostingOperatorSemanticRecord, type HostingOperatorDiscoveryCatalog, type PublishedProviderCatalogRecord, type TenantServiceSemanticRecord } from '../models/dataspace-discovery';
2
+ export type DiscoveryCatalogFetchResponse = Readonly<{
3
+ ok: boolean;
4
+ status: number;
5
+ json(): Promise<unknown>;
6
+ text(): Promise<string>;
7
+ }>;
8
+ export declare const DiscoveryCatalogSource: Readonly<{
9
+ readonly Internet: "internet";
10
+ readonly Cache: "cache";
11
+ readonly Default: "default";
12
+ }>;
13
+ export type DiscoveryCatalogSourceValue = typeof DiscoveryCatalogSource[keyof typeof DiscoveryCatalogSource];
14
+ export type DiscoveryCatalogFetcherOptions = Readonly<{
15
+ internetCatalogs?: Record<string, HostingOperatorDiscoveryCatalog>;
16
+ internetJsonByUrl?: Record<string, unknown>;
17
+ defaultCatalogs?: Record<string, HostingOperatorDiscoveryCatalog>;
18
+ }>;
19
+ export type DiscoveryCatalogFetcherHarness = Readonly<{
20
+ fetcher(input: string, init?: unknown): Promise<DiscoveryCatalogFetchResponse>;
21
+ calls: string[];
22
+ sources: Map<string, DiscoveryCatalogSourceValue>;
23
+ cache: Map<string, HostingOperatorDiscoveryCatalog>;
24
+ setInternetCatalog(url: string, catalog: HostingOperatorDiscoveryCatalog): void;
25
+ setInternetJson(url: string, payload: unknown): void;
26
+ setInternetFailure(url: string, status?: number, body?: unknown): void;
27
+ clearInternetRoute(url: string): void;
28
+ }>;
29
+ export type DefaultPublishedProviderCatalogRecordInput = Readonly<{
30
+ providerDid: string;
31
+ serviceType: string;
32
+ category: string;
33
+ areaServed?: string | readonly string[];
34
+ endpointUrl?: string;
35
+ discoveryUrl?: string;
36
+ catalogUrl?: string;
37
+ }>;
38
+ export type DefaultHostingOperatorDiscoveryCatalogInput = Readonly<{
39
+ hostingOperatorDid?: string;
40
+ discoveryUrl?: string;
41
+ catalogUrl?: string;
42
+ providers?: ReadonlyArray<PublishedProviderCatalogRecord>;
43
+ }>;
2
44
  /**
3
45
  * Parses the CSV or array representation of `serviceType`.
4
46
  *
@@ -144,3 +186,50 @@ export declare function filterPublishedProvidersByDiscoveryFilter(records: Reado
144
186
  * the requested service-autodiscovery filter.
145
187
  */
146
188
  export declare function filterHostingOperatorDiscoveryCatalog(catalog: HostingOperatorDiscoveryCatalog, filter: DataspaceDiscoveryFilter): HostingOperatorDiscoveryCatalog;
189
+ /**
190
+ * Builds a normalized published-provider catalog DTO suitable for:
191
+ *
192
+ * - backend-owned fallback catalogs
193
+ * - default discovery data loaded from configuration
194
+ * - tests that should use the same DTO constructors as production code
195
+ *
196
+ * This helper is intentionally neutral:
197
+ * - no example DIDs
198
+ * - no example URLs
199
+ * - no hidden business defaults
200
+ *
201
+ * @param input Required provider catalog fields plus optional URLs.
202
+ * @returns Normalized published-provider catalog record.
203
+ */
204
+ export declare function buildDefaultPublishedProviderCatalogRecord(input: DefaultPublishedProviderCatalogRecordInput): PublishedProviderCatalogRecord;
205
+ /**
206
+ * Builds a normalized hosting-operator discovery catalog DTO suitable for:
207
+ *
208
+ * - backend-owned fallback catalogs
209
+ * - default host catalogs assembled from configuration
210
+ * - tests that should reuse the same constructor as production code
211
+ *
212
+ * @param input Optional host identity fields plus the provider list.
213
+ * @returns Normalized host discovery catalog.
214
+ */
215
+ export declare function buildDefaultHostingOperatorDiscoveryCatalog(input?: DefaultHostingOperatorDiscoveryCatalogInput): HostingOperatorDiscoveryCatalog;
216
+ /**
217
+ * Creates a generic host-catalog fetcher with cache and default fallback.
218
+ *
219
+ * Intended use:
220
+ * - docs
221
+ * - SDK integration examples
222
+ * - tests that need to demonstrate the transport boundary around discovery
223
+ *
224
+ * Behavior:
225
+ * - successful network catalog refreshes cache
226
+ * - later network failures reuse cached catalog when available
227
+ * - when both network and cache are unavailable, configured defaults are used
228
+ *
229
+ * Production integrations may follow the same policy while replacing this
230
+ * in-memory harness with real logging, metrics, and storage.
231
+ *
232
+ * @param options Optional initial network and default catalogs keyed by URL.
233
+ * @returns Fetcher plus observable state and mutation helpers.
234
+ */
235
+ export declare function createDiscoveryCatalogFetcher(options?: DiscoveryCatalogFetcherOptions): DiscoveryCatalogFetcherHarness;
@@ -11,6 +11,19 @@ function asObject(value) {
11
11
  function asNonEmptyString(value) {
12
12
  return typeof value === 'string' ? value.trim() : '';
13
13
  }
14
+ export const DiscoveryCatalogSource = Object.freeze({
15
+ Internet: 'internet',
16
+ Cache: 'cache',
17
+ Default: 'default',
18
+ });
19
+ function createDiscoveryCatalogFetchResponse(payload, init = {}) {
20
+ return {
21
+ ok: init.ok ?? true,
22
+ status: init.status ?? 200,
23
+ json: async () => payload,
24
+ text: async () => JSON.stringify(payload),
25
+ };
26
+ }
14
27
  function toStringList(value) {
15
28
  if (Array.isArray(value)) {
16
29
  return Array.from(new Set(value
@@ -339,3 +352,131 @@ export function filterHostingOperatorDiscoveryCatalog(catalog, filter) {
339
352
  providers: filterPublishedProvidersByDiscoveryFilter(catalog.providers, filter),
340
353
  };
341
354
  }
355
+ /**
356
+ * Builds a normalized published-provider catalog DTO suitable for:
357
+ *
358
+ * - backend-owned fallback catalogs
359
+ * - default discovery data loaded from configuration
360
+ * - tests that should use the same DTO constructors as production code
361
+ *
362
+ * This helper is intentionally neutral:
363
+ * - no example DIDs
364
+ * - no example URLs
365
+ * - no hidden business defaults
366
+ *
367
+ * @param input Required provider catalog fields plus optional URLs.
368
+ * @returns Normalized published-provider catalog record.
369
+ */
370
+ export function buildDefaultPublishedProviderCatalogRecord(input) {
371
+ const normalizedAreaServed = Array.isArray(input.areaServed)
372
+ ? normalizeList(input.areaServed.map((value) => asNonEmptyString(value)).filter(Boolean))
373
+ : toStringList(input.areaServed);
374
+ return {
375
+ providerDid: asNonEmptyString(input.providerDid),
376
+ serviceType: asNonEmptyString(input.serviceType),
377
+ category: asNonEmptyString(input.category),
378
+ areaServed: normalizedAreaServed.length ? normalizedAreaServed.join(',') : undefined,
379
+ endpointUrl: asNonEmptyString(input.endpointUrl) || undefined,
380
+ discoveryUrl: asNonEmptyString(input.discoveryUrl) || undefined,
381
+ catalogUrl: asNonEmptyString(input.catalogUrl) || undefined,
382
+ };
383
+ }
384
+ /**
385
+ * Builds a normalized hosting-operator discovery catalog DTO suitable for:
386
+ *
387
+ * - backend-owned fallback catalogs
388
+ * - default host catalogs assembled from configuration
389
+ * - tests that should reuse the same constructor as production code
390
+ *
391
+ * @param input Optional host identity fields plus the provider list.
392
+ * @returns Normalized host discovery catalog.
393
+ */
394
+ export function buildDefaultHostingOperatorDiscoveryCatalog(input = {}) {
395
+ return {
396
+ hostingOperatorDid: asNonEmptyString(input.hostingOperatorDid) || undefined,
397
+ discoveryUrl: asNonEmptyString(input.discoveryUrl) || undefined,
398
+ catalogUrl: asNonEmptyString(input.catalogUrl) || undefined,
399
+ providers: [...(input.providers || [])],
400
+ };
401
+ }
402
+ function isHostingOperatorCatalogPayload(value) {
403
+ const objectValue = asObject(value);
404
+ return Boolean(objectValue && Array.isArray(objectValue.providers));
405
+ }
406
+ /**
407
+ * Creates a generic host-catalog fetcher with cache and default fallback.
408
+ *
409
+ * Intended use:
410
+ * - docs
411
+ * - SDK integration examples
412
+ * - tests that need to demonstrate the transport boundary around discovery
413
+ *
414
+ * Behavior:
415
+ * - successful network catalog refreshes cache
416
+ * - later network failures reuse cached catalog when available
417
+ * - when both network and cache are unavailable, configured defaults are used
418
+ *
419
+ * Production integrations may follow the same policy while replacing this
420
+ * in-memory harness with real logging, metrics, and storage.
421
+ *
422
+ * @param options Optional initial network and default catalogs keyed by URL.
423
+ * @returns Fetcher plus observable state and mutation helpers.
424
+ */
425
+ export function createDiscoveryCatalogFetcher(options = {}) {
426
+ const internetResponses = new Map([
427
+ ...Object.entries(options.internetCatalogs || {}).map(([url, catalog]) => [url, createDiscoveryCatalogFetchResponse(catalog, { ok: true, status: 200 })]),
428
+ ...Object.entries(options.internetJsonByUrl || {}).map(([url, payload]) => [url, createDiscoveryCatalogFetchResponse(payload, { ok: true, status: 200 })]),
429
+ ]);
430
+ const internetPayloads = new Map([
431
+ ...Object.entries(options.internetCatalogs || {}).map(([url, catalog]) => [String(url), catalog]),
432
+ ...Object.entries(options.internetJsonByUrl || {}).map(([url, payload]) => [String(url), payload]),
433
+ ]);
434
+ const defaultCatalogs = new Map(Object.entries(options.defaultCatalogs || {}));
435
+ const cache = new Map();
436
+ const sources = new Map();
437
+ const calls = [];
438
+ return {
439
+ calls,
440
+ sources,
441
+ cache,
442
+ setInternetCatalog(url, catalog) {
443
+ internetResponses.set(String(url), createDiscoveryCatalogFetchResponse(catalog, { ok: true, status: 200 }));
444
+ internetPayloads.set(String(url), catalog);
445
+ },
446
+ setInternetJson(url, payload) {
447
+ internetResponses.set(String(url), createDiscoveryCatalogFetchResponse(payload, { ok: true, status: 200 }));
448
+ internetPayloads.set(String(url), payload);
449
+ },
450
+ setInternetFailure(url, status = 503, body = { error: 'temporary failure' }) {
451
+ internetResponses.set(String(url), createDiscoveryCatalogFetchResponse(body, { ok: false, status }));
452
+ internetPayloads.delete(String(url));
453
+ },
454
+ clearInternetRoute(url) {
455
+ internetResponses.delete(String(url));
456
+ internetPayloads.delete(String(url));
457
+ },
458
+ async fetcher(input) {
459
+ const key = String(input);
460
+ calls.push(key);
461
+ const internetResponse = internetResponses.get(key);
462
+ if (internetResponse && internetResponse.ok) {
463
+ const payload = internetPayloads.get(key);
464
+ if (isHostingOperatorCatalogPayload(payload)) {
465
+ cache.set(key, payload);
466
+ }
467
+ sources.set(key, DiscoveryCatalogSource.Internet);
468
+ return internetResponse;
469
+ }
470
+ if (cache.has(key)) {
471
+ sources.set(key, DiscoveryCatalogSource.Cache);
472
+ return createDiscoveryCatalogFetchResponse(cache.get(key), { ok: true, status: 200 });
473
+ }
474
+ if (defaultCatalogs.has(key)) {
475
+ sources.set(key, DiscoveryCatalogSource.Default);
476
+ return createDiscoveryCatalogFetchResponse(defaultCatalogs.get(key), { ok: true, status: 200 });
477
+ }
478
+ sources.set(key, DiscoveryCatalogSource.Default);
479
+ return createDiscoveryCatalogFetchResponse({ error: 'not found' }, { ok: false, status: 404 });
480
+ },
481
+ };
482
+ }
@@ -0,0 +1,66 @@
1
+ import { type DataspaceProtocolVersionValue } from '../constants/dataspace-protocol';
2
+ import type { DspaceProtocolVersionEntry, DspaceVersionMetadata } from '../models/dataspace-protocol';
3
+ /**
4
+ * Route context used to build tenant-scoped or host-scoped GW CORE DSP paths.
5
+ *
6
+ * Semantic rule:
7
+ * - `tenantId` participants use `businessSector`
8
+ * - `host`/ICA/runtime participants use `hostNetwork`
9
+ */
10
+ export type GwDataspaceRouteContext = Readonly<{
11
+ participantId?: string;
12
+ tenantId?: string;
13
+ jurisdiction?: string;
14
+ version?: string;
15
+ hostNetwork?: string;
16
+ /** @deprecated Use `hostNetwork`. */
17
+ hostNetworkOrBusinessSector?: string;
18
+ businessSector?: string;
19
+ sector?: string;
20
+ }>;
21
+ /**
22
+ * Builds the GW CORE base DSP path.
23
+ *
24
+ * Participant scope:
25
+ * - `/{participantId}/cds-{jurisdiction}/{version}/{hostNetwork|businessSector}/dsp`
26
+ */
27
+ export declare function buildGwDataspaceBasePath(input?: GwDataspaceRouteContext): string;
28
+ /**
29
+ * Builds the canonical DSP version-discovery well-known path for GW CORE.
30
+ *
31
+ * Participant scope:
32
+ * - `/{participantId}/cds-{jurisdiction}/{version}/{hostNetwork|businessSector}/.well-known/dspace-version`
33
+ */
34
+ export declare function buildGwDspaceVersionWellKnownPath(input?: GwDataspaceRouteContext): string;
35
+ /**
36
+ * Builds the project-local GW CORE DSP catalog request endpoint.
37
+ */
38
+ export declare function buildGwCatalogRequestPath(input?: GwDataspaceRouteContext): string;
39
+ /**
40
+ * Builds the GW CORE catalog collection base path.
41
+ */
42
+ export declare function buildGwCatalogCollectionPath(input?: GwDataspaceRouteContext): string;
43
+ /**
44
+ * Builds the project-local GW CORE catalog artifact URL path used for public
45
+ * read-only autodiscovery.
46
+ */
47
+ export declare function buildGwCatalogArtifactPath(input?: GwDataspaceRouteContext): string;
48
+ /**
49
+ * Builds the project-local GW CORE dataset lookup path.
50
+ */
51
+ export declare function buildGwCatalogDatasetPath(input?: GwDataspaceRouteContext, datasetId?: string): string;
52
+ /**
53
+ * Builds a minimal DSP version metadata payload for a given GW CORE DSP base
54
+ * path.
55
+ */
56
+ export declare function buildDspaceVersionMetadata(path: string, version?: DataspaceProtocolVersionValue): DspaceVersionMetadata;
57
+ /**
58
+ * Selects the preferred advertised DSP version entry from a `dspace-version`
59
+ * payload.
60
+ */
61
+ export declare function selectDspaceProtocolVersionEntry(metadata: DspaceVersionMetadata | null | undefined, version?: string): DspaceProtocolVersionEntry | undefined;
62
+ /**
63
+ * Derives the GW CORE catalog artifact URL from a `dspace-version` endpoint URL
64
+ * and its parsed payload.
65
+ */
66
+ export declare function deriveGwCatalogArtifactUrlFromDspaceVersion(dspaceVersionUrl: string, metadata: DspaceVersionMetadata | null | undefined, version?: string): string | undefined;
@@ -0,0 +1,109 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { DataspaceProtocolVersions, DataspaceWellKnownPaths, GwDataspaceBindingPaths, } from '../constants/dataspace-protocol.js';
3
+ function resolveParticipantId(input) {
4
+ return String(input.participantId || input.tenantId || '').trim();
5
+ }
6
+ function resolveSectorLikeSegment(input) {
7
+ return String(input.hostNetwork
8
+ || input.hostNetworkOrBusinessSector
9
+ || input.businessSector
10
+ || input.sector
11
+ || '').trim();
12
+ }
13
+ function hasParticipantRouteContext(input) {
14
+ return Boolean(resolveParticipantId(input)
15
+ && String(input.jurisdiction || '').trim()
16
+ && String(input.version || '').trim()
17
+ && resolveSectorLikeSegment(input));
18
+ }
19
+ /**
20
+ * Builds the GW CORE base DSP path.
21
+ *
22
+ * Participant scope:
23
+ * - `/{participantId}/cds-{jurisdiction}/{version}/{hostNetwork|businessSector}/dsp`
24
+ */
25
+ export function buildGwDataspaceBasePath(input = {}) {
26
+ if (!hasParticipantRouteContext(input)) {
27
+ return GwDataspaceBindingPaths.Base;
28
+ }
29
+ return `/${resolveParticipantId(input)}`
30
+ + `/cds-${String(input.jurisdiction).trim()}`
31
+ + `/${String(input.version).trim()}`
32
+ + `/${resolveSectorLikeSegment(input)}`
33
+ + GwDataspaceBindingPaths.Base;
34
+ }
35
+ /**
36
+ * Builds the canonical DSP version-discovery well-known path for GW CORE.
37
+ *
38
+ * Participant scope:
39
+ * - `/{participantId}/cds-{jurisdiction}/{version}/{hostNetwork|businessSector}/.well-known/dspace-version`
40
+ */
41
+ export function buildGwDspaceVersionWellKnownPath(input = {}) {
42
+ if (!hasParticipantRouteContext(input)) {
43
+ return DataspaceWellKnownPaths.VersionMetadata;
44
+ }
45
+ return `/${resolveParticipantId(input)}`
46
+ + `/cds-${String(input.jurisdiction).trim()}`
47
+ + `/${String(input.version).trim()}`
48
+ + `/${resolveSectorLikeSegment(input)}`
49
+ + DataspaceWellKnownPaths.VersionMetadata;
50
+ }
51
+ /**
52
+ * Builds the project-local GW CORE DSP catalog request endpoint.
53
+ */
54
+ export function buildGwCatalogRequestPath(input = {}) {
55
+ return `${buildGwDataspaceBasePath(input)}${GwDataspaceBindingPaths.CatalogRequestSuffix}`;
56
+ }
57
+ /**
58
+ * Builds the GW CORE catalog collection base path.
59
+ */
60
+ export function buildGwCatalogCollectionPath(input = {}) {
61
+ return `${buildGwDataspaceBasePath(input)}${GwDataspaceBindingPaths.CatalogCollectionSuffix}`;
62
+ }
63
+ /**
64
+ * Builds the project-local GW CORE catalog artifact URL path used for public
65
+ * read-only autodiscovery.
66
+ */
67
+ export function buildGwCatalogArtifactPath(input = {}) {
68
+ return `${buildGwDataspaceBasePath(input)}${GwDataspaceBindingPaths.CatalogArtifactSuffix}`;
69
+ }
70
+ /**
71
+ * Builds the project-local GW CORE dataset lookup path.
72
+ */
73
+ export function buildGwCatalogDatasetPath(input = {}, datasetId = ':id') {
74
+ return `${buildGwDataspaceBasePath(input)}${GwDataspaceBindingPaths.CatalogDatasetsPrefix}/${datasetId}`;
75
+ }
76
+ /**
77
+ * Builds a minimal DSP version metadata payload for a given GW CORE DSP base
78
+ * path.
79
+ */
80
+ export function buildDspaceVersionMetadata(path, version = DataspaceProtocolVersions.Current) {
81
+ return {
82
+ protocolVersions: [
83
+ {
84
+ version,
85
+ path,
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ /**
91
+ * Selects the preferred advertised DSP version entry from a `dspace-version`
92
+ * payload.
93
+ */
94
+ export function selectDspaceProtocolVersionEntry(metadata, version = DataspaceProtocolVersions.Current) {
95
+ const entries = metadata?.protocolVersions || [];
96
+ return entries.find((entry) => String(entry.version || '').trim() === String(version).trim())
97
+ || entries[0];
98
+ }
99
+ /**
100
+ * Derives the GW CORE catalog artifact URL from a `dspace-version` endpoint URL
101
+ * and its parsed payload.
102
+ */
103
+ export function deriveGwCatalogArtifactUrlFromDspaceVersion(dspaceVersionUrl, metadata, version = DataspaceProtocolVersions.Current) {
104
+ const entry = selectDspaceProtocolVersionEntry(metadata, version);
105
+ if (!entry?.path)
106
+ return undefined;
107
+ const base = new URL(dspaceVersionUrl);
108
+ return new URL(`${String(entry.path).trim()}${GwDataspaceBindingPaths.CatalogArtifactSuffix}`, `${base.protocol}//${base.host}`).toString();
109
+ }
@@ -8,6 +8,8 @@ export * from './consent';
8
8
  export * from './did';
9
9
  export * from './did-resolution';
10
10
  export * from './dataspace-discovery';
11
+ export * from './dataspace-discovery-defaults';
12
+ export * from './dataspace-protocol';
11
13
  export * from './didcomm';
12
14
  export * from './didcomm-submit';
13
15
  export * from './didcomm-submit-policy';
@@ -8,6 +8,8 @@ export * from './consent.js';
8
8
  export * from './did.js';
9
9
  export * from './did-resolution.js';
10
10
  export * from './dataspace-discovery.js';
11
+ export * from './dataspace-discovery-defaults.js';
12
+ export * from './dataspace-protocol.js';
11
13
  export * from './didcomm.js';
12
14
  export * from './didcomm-submit.js';
13
15
  export * from './didcomm-submit-policy.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },