@twin.org/dataspace-control-plane-service 0.9.0 → 0.9.1-next.10
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 +1 -1
- package/dist/es/dataspaceControlPlaneRoutes.js +79 -7
- package/dist/es/dataspaceControlPlaneRoutes.js.map +1 -1
- package/dist/es/dataspaceControlPlaneService.js +121 -356
- package/dist/es/dataspaceControlPlaneService.js.map +1 -1
- package/dist/es/factories/transferHandlerFactory.js +11 -0
- package/dist/es/factories/transferHandlerFactory.js.map +1 -0
- package/dist/es/handlers/httpDataPostTransferHandler.js +111 -0
- package/dist/es/handlers/httpDataPostTransferHandler.js.map +1 -0
- package/dist/es/handlers/httpDataPullTransferHandler.js +108 -0
- package/dist/es/handlers/httpDataPullTransferHandler.js.map +1 -0
- package/dist/es/handlers/httpDataPushTransferHandler.js +114 -0
- package/dist/es/handlers/httpDataPushTransferHandler.js.map +1 -0
- package/dist/es/index.js +7 -0
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IDataspaceControlPlaneServiceConfig.js.map +1 -1
- package/dist/es/models/ITransferHandler.js +2 -0
- package/dist/es/models/ITransferHandler.js.map +1 -0
- package/dist/es/models/ITransferHandlerPrepareContext.js +4 -0
- package/dist/es/models/ITransferHandlerPrepareContext.js.map +1 -0
- package/dist/es/models/ITransferHandlerStartContext.js +2 -0
- package/dist/es/models/ITransferHandlerStartContext.js.map +1 -0
- package/dist/types/dataspaceControlPlaneRoutes.d.ts +1 -1
- package/dist/types/dataspaceControlPlaneService.d.ts +6 -1
- package/dist/types/factories/transferHandlerFactory.d.ts +8 -0
- package/dist/types/handlers/httpDataPostTransferHandler.d.ts +63 -0
- package/dist/types/handlers/httpDataPullTransferHandler.d.ts +63 -0
- package/dist/types/handlers/httpDataPushTransferHandler.d.ts +68 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/models/IDataspaceControlPlaneServiceConfig.d.ts +10 -5
- package/dist/types/models/ITransferHandler.d.ts +62 -0
- package/dist/types/models/ITransferHandlerPrepareContext.d.ts +21 -0
- package/dist/types/models/ITransferHandlerStartContext.d.ts +31 -0
- package/docs/changelog.md +190 -0
- package/docs/reference/classes/DataspaceControlPlaneService.md +18 -0
- package/docs/reference/classes/HttpDataPostTransferHandler.md +235 -0
- package/docs/reference/classes/HttpDataPullTransferHandler.md +235 -0
- package/docs/reference/classes/HttpDataPushTransferHandler.md +242 -0
- package/docs/reference/index.md +7 -0
- package/docs/reference/interfaces/IDataspaceControlPlaneServiceConfig.md +10 -5
- package/docs/reference/interfaces/ITransferHandler.md +180 -0
- package/docs/reference/interfaces/ITransferHandlerPrepareContext.md +35 -0
- package/docs/reference/interfaces/ITransferHandlerStartContext.md +51 -0
- package/docs/reference/variables/TransferHandlerFactory.md +7 -0
- package/locales/en.json +14 -11
- package/package.json +18 -18
|
@@ -9,12 +9,15 @@ import { EngineCoreFactory } from "@twin.org/engine-models";
|
|
|
9
9
|
import { ComparisonOperator } from "@twin.org/entity";
|
|
10
10
|
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
11
11
|
import { OdrlPolicyHelper, PolicyRequesterFactory } from "@twin.org/rights-management-models";
|
|
12
|
-
import { DataspaceProtocolContexts, DataspaceProtocolContractNegotiationTypes,
|
|
12
|
+
import { DataspaceProtocolContexts, DataspaceProtocolContractNegotiationTypes, DataspaceProtocolHelper, DataspaceProtocolTransferProcessStateType, DataspaceProtocolTransferProcessTypes, DataspaceProtocolVersionBindingType } from "@twin.org/standards-dataspace-protocol";
|
|
13
13
|
import { OdrlActionType, OdrlContexts, OdrlPolicyType } from "@twin.org/standards-w3c-odrl";
|
|
14
14
|
import { MetricHelper } from "@twin.org/telemetry-models";
|
|
15
15
|
import { TrustHelper } from "@twin.org/trust-models";
|
|
16
16
|
import { DataspaceControlPlanePolicyRequester } from "./dataspaceControlPlanePolicyRequester.js";
|
|
17
|
-
import {
|
|
17
|
+
import { TransferHandlerFactory } from "./factories/transferHandlerFactory.js";
|
|
18
|
+
import { HttpDataPostTransferHandler } from "./handlers/httpDataPostTransferHandler.js";
|
|
19
|
+
import { HttpDataPullTransferHandler } from "./handlers/httpDataPullTransferHandler.js";
|
|
20
|
+
import { HttpDataPushTransferHandler } from "./handlers/httpDataPushTransferHandler.js";
|
|
18
21
|
import { isCatalogError, isCatalogErrorName, transformToTransferError } from "./utils/transferErrorUtils.js";
|
|
19
22
|
/**
|
|
20
23
|
* Dataspace Control Plane Service implementation.
|
|
@@ -108,11 +111,10 @@ export class DataspaceControlPlaneService {
|
|
|
108
111
|
*/
|
|
109
112
|
_callbackPath;
|
|
110
113
|
/**
|
|
111
|
-
*
|
|
112
|
-
* (not at construction) so the data plane may register after the control plane is built.
|
|
114
|
+
* Data plane component.
|
|
113
115
|
* @internal
|
|
114
116
|
*/
|
|
115
|
-
|
|
117
|
+
_dataPlaneComponent;
|
|
116
118
|
/**
|
|
117
119
|
* Policy requester instance for handling negotiation callbacks.
|
|
118
120
|
* @internal
|
|
@@ -190,11 +192,11 @@ export class DataspaceControlPlaneService {
|
|
|
190
192
|
this._trustComponent = ComponentFactory.get(options?.trustComponentType ?? "trust");
|
|
191
193
|
this._overrideTrustGeneratorType = options?.config?.overrideTrustGeneratorType;
|
|
192
194
|
this._dataPlanePath = Is.stringValue(options?.config?.dataPlanePath)
|
|
193
|
-
? StringHelper.
|
|
195
|
+
? StringHelper.trimLeadingAndTrailingSlashes(options.config.dataPlanePath)
|
|
194
196
|
: undefined;
|
|
195
197
|
this._callbackPath = Is.stringValue(options?.config?.callbackPath)
|
|
196
198
|
? StringHelper.trimLeadingAndTrailingSlashes(options.config.callbackPath)
|
|
197
|
-
:
|
|
199
|
+
: "dataspace-control-plane";
|
|
198
200
|
this._autoStartTransfers = options?.config?.autoStartTransfers ?? false;
|
|
199
201
|
this._stalledNegotiationTimeoutMs =
|
|
200
202
|
options?.config?.stalledNegotiationTimeoutMs ??
|
|
@@ -205,14 +207,7 @@ export class DataspaceControlPlaneService {
|
|
|
205
207
|
this._taskScheduler = ComponentFactory.getIfExists(options?.taskSchedulerComponentType ?? "task-scheduler");
|
|
206
208
|
this._platformComponent = ComponentFactory.get(options?.platformComponentType ?? "platform");
|
|
207
209
|
this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
|
|
208
|
-
|
|
209
|
-
// BEFORE the data plane in engine startup order, so `getRegisteredInstanceTypeOptional`
|
|
210
|
-
// returns undefined when the engine constructs us — the wiring override can't fire here.
|
|
211
|
-
// The default therefore matches the engine's actual factory key
|
|
212
|
-
// (`nameofKebabCase(DataspaceDataPlaneService)`) so the late-bound `requireDataPlane()`
|
|
213
|
-
// lookup at push time finds it regardless of init order.
|
|
214
|
-
this._dataPlaneComponentType =
|
|
215
|
-
options?.dataPlaneComponentType ?? "dataspace-data-plane-service";
|
|
210
|
+
this._dataPlaneComponent = ComponentFactory.get(options?.dataPlaneComponentType ?? "dataspace-data-plane-service");
|
|
216
211
|
this._negotiationCallbacks = new Map();
|
|
217
212
|
this._transferCallbacks = new Map();
|
|
218
213
|
this._internalTransferCallback = this.createInternalTransferCallback();
|
|
@@ -221,6 +216,9 @@ export class DataspaceControlPlaneService {
|
|
|
221
216
|
const internalCallback = this.createInternalCallback();
|
|
222
217
|
this._policyRequester = new DataspaceControlPlanePolicyRequester(options?.loggingComponentType, internalCallback);
|
|
223
218
|
PolicyRequesterFactory.register(DataspaceControlPlaneService._REQUESTER_TYPE, () => this._policyRequester);
|
|
219
|
+
TransferHandlerFactory.register(DataspaceTransferFormat.HttpDataPull, () => new HttpDataPullTransferHandler());
|
|
220
|
+
TransferHandlerFactory.register(DataspaceTransferFormat.HttpDataPush, () => new HttpDataPushTransferHandler());
|
|
221
|
+
TransferHandlerFactory.register(DataspaceTransferFormat.HttpDataPost, () => new HttpDataPostTransferHandler());
|
|
224
222
|
}
|
|
225
223
|
/**
|
|
226
224
|
* Returns the class name of the component.
|
|
@@ -283,72 +281,6 @@ export class DataspaceControlPlaneService {
|
|
|
283
281
|
});
|
|
284
282
|
return;
|
|
285
283
|
}
|
|
286
|
-
await this._loggingComponent?.log({
|
|
287
|
-
level: "info",
|
|
288
|
-
ts: Date.now(),
|
|
289
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
290
|
-
message: "populatingFederatedCatalogue"
|
|
291
|
-
});
|
|
292
|
-
let registeredCount = 0;
|
|
293
|
-
let errorCount = 0;
|
|
294
|
-
let totalDatasets = 0;
|
|
295
|
-
// The platform component execute is used so that the dataset publication runs in the
|
|
296
|
-
// tenant context, also works in single tenant mode
|
|
297
|
-
await this._platformComponent.execute(async () => {
|
|
298
|
-
// The tenant context id is set here for each system tenant
|
|
299
|
-
let cursor;
|
|
300
|
-
do {
|
|
301
|
-
const page = await this._dataspaceAppDatasetStorage.query(undefined, undefined, undefined, cursor);
|
|
302
|
-
if (Is.arrayValue(page.entities)) {
|
|
303
|
-
for (const entity of page.entities) {
|
|
304
|
-
const appDataset = entity;
|
|
305
|
-
totalDatasets++;
|
|
306
|
-
try {
|
|
307
|
-
await this.publishAppDataset(appDataset);
|
|
308
|
-
registeredCount++;
|
|
309
|
-
await this._loggingComponent?.log({
|
|
310
|
-
level: "debug",
|
|
311
|
-
ts: Date.now(),
|
|
312
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
313
|
-
message: "datasetRegistered",
|
|
314
|
-
data: {
|
|
315
|
-
datasetId: appDataset.id,
|
|
316
|
-
appId: appDataset.appId,
|
|
317
|
-
tenantId: appDataset.tenantId
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
catch (error) {
|
|
322
|
-
errorCount++;
|
|
323
|
-
await this._loggingComponent?.log({
|
|
324
|
-
level: "error",
|
|
325
|
-
ts: Date.now(),
|
|
326
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
327
|
-
message: "datasetPublishFailed",
|
|
328
|
-
error: BaseError.fromError(error),
|
|
329
|
-
data: {
|
|
330
|
-
datasetId: appDataset.id,
|
|
331
|
-
appId: appDataset.appId,
|
|
332
|
-
tenantId: appDataset.tenantId
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
cursor = page.cursor;
|
|
339
|
-
} while (Is.stringValue(cursor));
|
|
340
|
-
});
|
|
341
|
-
await this._loggingComponent?.log({
|
|
342
|
-
level: "info",
|
|
343
|
-
ts: Date.now(),
|
|
344
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
345
|
-
message: "federatedCataloguePopulated",
|
|
346
|
-
data: {
|
|
347
|
-
registeredCount,
|
|
348
|
-
errorCount,
|
|
349
|
-
totalDatasets
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
284
|
if (this._taskScheduler) {
|
|
353
285
|
await this._taskScheduler.addTask("control-plane-negotiation-cleanup", [
|
|
354
286
|
{
|
|
@@ -562,12 +494,7 @@ export class DataspaceControlPlaneService {
|
|
|
562
494
|
// Callback the provider POSTs DSP messages back to: mount path + `?organization=` so the inbound
|
|
563
495
|
// POST routes to the right consumer tenant (mirrors buildCallbackUrl). The bare `origin` is kept for
|
|
564
496
|
// the PUSH-inbox base below (a different mount).
|
|
565
|
-
|
|
566
|
-
? `${origin}/${this._callbackPath}`
|
|
567
|
-
: origin;
|
|
568
|
-
if (Is.stringValue(organizationIdentity)) {
|
|
569
|
-
callbackAddress = HttpUrlHelper.addQueryStringParam(callbackAddress, ContextIdKeys.Organization, organizationIdentity);
|
|
570
|
-
}
|
|
497
|
+
const callbackAddress = HttpUrlHelper.addQueryStringParam(Is.stringValue(this._callbackPath) ? `${origin}/${this._callbackPath}` : origin, ContextIdKeys.Organization, organizationIdentity);
|
|
571
498
|
const transferRequestMessage = {
|
|
572
499
|
"@context": [DataspaceProtocolContexts.Context],
|
|
573
500
|
"@type": DataspaceProtocolTransferProcessTypes.TransferRequestMessage,
|
|
@@ -576,23 +503,15 @@ export class DataspaceControlPlaneService {
|
|
|
576
503
|
callbackAddress,
|
|
577
504
|
format
|
|
578
505
|
};
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
inboxEndpoint = HttpUrlHelper.addQueryStringParam(inboxEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
589
|
-
}
|
|
590
|
-
transferRequestMessage.dataAddress = {
|
|
591
|
-
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
592
|
-
endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
|
|
593
|
-
endpoint: inboxEndpoint
|
|
594
|
-
};
|
|
595
|
-
}
|
|
506
|
+
// Delegate consumer dataAddress construction to the format-specific handler.
|
|
507
|
+
// PUSH supplies its /inbox; PULL and POST return undefined (no consumer address needed).
|
|
508
|
+
const transferHandler = TransferHandlerFactory.get(format);
|
|
509
|
+
transferRequestMessage.dataAddress = transferHandler.buildConsumerDataAddress({
|
|
510
|
+
consumerPid,
|
|
511
|
+
origin,
|
|
512
|
+
dataPlanePath: this._dataPlanePath,
|
|
513
|
+
organizationIdentity
|
|
514
|
+
});
|
|
596
515
|
// Generate outbound trust token to authenticate this node to the provider.
|
|
597
516
|
const outboundToken = await this._trustComponent.generate(organizationIdentity, this._overrideTrustGeneratorType, { subject: { consumerPid, agreementId } });
|
|
598
517
|
// Create a remote REST client pointed at the provider endpoint and call requestTransfer.
|
|
@@ -726,200 +645,32 @@ export class DataspaceControlPlaneService {
|
|
|
726
645
|
consumerPid: entity.consumerPid,
|
|
727
646
|
providerPid: entity.providerPid
|
|
728
647
|
};
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (Is.empty(entity.dataAddress) && entity.format === DataspaceTransferFormat.HttpDataPost) {
|
|
749
|
-
// PROVIDER-INITIATED PUSH: Consumer requested push but did not supply an /inbox.
|
|
750
|
-
// Provider returns its own /inbox URL + a signed JWT so the consumer can verify
|
|
751
|
-
// the incoming activities (HttpData-POST / DataspaceTransferFormat.HttpDataPost).
|
|
752
|
-
if (!Is.stringValue(this._dataPlanePath)) {
|
|
753
|
-
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid: entity.consumerPid }), message);
|
|
754
|
-
}
|
|
755
|
-
if (!Is.stringValue(entity.providerIdentity)) {
|
|
756
|
-
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
|
|
757
|
-
}
|
|
758
|
-
const pushProviderId = entity.providerIdentity;
|
|
759
|
-
const accessToken = await this._trustComponent.generate(pushProviderId, this._overrideTrustGeneratorType, {
|
|
760
|
-
subject: {
|
|
761
|
-
consumerPid: entity.consumerPid,
|
|
762
|
-
providerPid: entity.providerPid,
|
|
763
|
-
agreementId: entity.agreementId,
|
|
764
|
-
datasetId: entity.datasetId
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "accessToken", accessToken);
|
|
768
|
-
const tokenString = accessToken;
|
|
769
|
-
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}/inbox`;
|
|
770
|
-
// Bake the provider's organization identity into the /inbox URL so the consumer's
|
|
771
|
-
// inbound POST routes to the right organization via TenantProcessor — mirrors the
|
|
772
|
-
// pull-mode endpoint baking below.
|
|
773
|
-
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
774
|
-
fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
775
|
-
response.dataAddress = {
|
|
776
|
-
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
777
|
-
endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
|
|
778
|
-
endpoint: fullEndpoint,
|
|
779
|
-
endpointProperties: [
|
|
780
|
-
{
|
|
781
|
-
"@type": DataspaceProtocolTransferProcessTypes.EndpointProperty,
|
|
782
|
-
name: EndpointProperties.Authorization,
|
|
783
|
-
value: tokenString
|
|
784
|
-
},
|
|
785
|
-
{
|
|
786
|
-
"@type": DataspaceProtocolTransferProcessTypes.EndpointProperty,
|
|
787
|
-
name: EndpointProperties.AuthType,
|
|
788
|
-
value: "bearer"
|
|
789
|
-
}
|
|
790
|
-
]
|
|
791
|
-
};
|
|
792
|
-
await this._loggingComponent?.log({
|
|
793
|
-
level: "info",
|
|
794
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
795
|
-
ts: Date.now(),
|
|
796
|
-
message: "pushTransferStarted",
|
|
797
|
-
data: {
|
|
798
|
-
consumerPid: entity.consumerPid,
|
|
799
|
-
providerPid: entity.providerPid,
|
|
800
|
-
endpoint: fullEndpoint,
|
|
801
|
-
transferMode: DataspaceTransferFormat.HttpDataPost
|
|
802
|
-
}
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
else if (Is.empty(entity.dataAddress)) {
|
|
806
|
-
if (!Is.stringValue(this._dataPlanePath)) {
|
|
807
|
-
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pullTransfersNotSupported", {
|
|
808
|
-
consumerPid: entity.consumerPid,
|
|
809
|
-
providerPid: entity.providerPid
|
|
810
|
-
}), message);
|
|
811
|
-
}
|
|
812
|
-
// Provider signs the data access token with its own identity.
|
|
813
|
-
// The subject contains the transfer context claims that the data plane
|
|
814
|
-
// will verify when the consumer presents this token
|
|
815
|
-
if (!Is.stringValue(entity.providerIdentity)) {
|
|
816
|
-
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
|
|
817
|
-
}
|
|
818
|
-
const pullProviderId = entity.providerIdentity;
|
|
819
|
-
const accessToken = await this._trustComponent.generate(pullProviderId, this._overrideTrustGeneratorType, {
|
|
820
|
-
subject: {
|
|
821
|
-
consumerPid: entity.consumerPid,
|
|
822
|
-
providerPid: entity.providerPid,
|
|
823
|
-
agreementId: entity.agreementId,
|
|
824
|
-
datasetId: entity.datasetId
|
|
825
|
-
}
|
|
826
|
-
});
|
|
827
|
-
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "accessToken", accessToken);
|
|
828
|
-
const tokenString = accessToken;
|
|
829
|
-
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}`;
|
|
830
|
-
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
831
|
-
fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
832
|
-
response.dataAddress = {
|
|
833
|
-
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
834
|
-
endpointType: DataspaceProtocolEndpointType.HttpsQueryEndpoint,
|
|
835
|
-
endpoint: fullEndpoint,
|
|
836
|
-
endpointProperties: [
|
|
837
|
-
{
|
|
838
|
-
"@type": DataspaceProtocolTransferProcessTypes.EndpointProperty,
|
|
839
|
-
name: EndpointProperties.Authorization,
|
|
840
|
-
value: tokenString
|
|
841
|
-
},
|
|
842
|
-
{
|
|
843
|
-
"@type": DataspaceProtocolTransferProcessTypes.EndpointProperty,
|
|
844
|
-
name: EndpointProperties.AuthType,
|
|
845
|
-
value: "bearer"
|
|
846
|
-
}
|
|
847
|
-
]
|
|
848
|
-
};
|
|
849
|
-
await this._loggingComponent?.log({
|
|
850
|
-
level: "info",
|
|
851
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
852
|
-
ts: Date.now(),
|
|
853
|
-
message: "dataAccessTokenGenerated",
|
|
854
|
-
data: {
|
|
855
|
-
consumerPid: entity.consumerPid,
|
|
856
|
-
providerPid: entity.providerPid,
|
|
857
|
-
endpoint: fullEndpoint,
|
|
858
|
-
transferMode: "PULL"
|
|
859
|
-
}
|
|
860
|
-
});
|
|
648
|
+
// Delegate dataAddress construction and post-start actions to the format-specific handler.
|
|
649
|
+
// Push subscription setup reads the entity from storage and requires state=STARTED,
|
|
650
|
+
// so STARTED is persisted before calling onProviderStart. If subscription setup fails,
|
|
651
|
+
// the state is rolled back so the client can retry.
|
|
652
|
+
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
653
|
+
const transferHandler = TransferHandlerFactory.get(this.inferTransferFormat(entity));
|
|
654
|
+
response.dataAddress = await transferHandler.buildProviderStartDataAddress({
|
|
655
|
+
entity,
|
|
656
|
+
publicOrigin,
|
|
657
|
+
dataPlanePath: this._dataPlanePath,
|
|
658
|
+
organizationIdentity,
|
|
659
|
+
trustComponent: this._trustComponent,
|
|
660
|
+
overrideTrustGeneratorType: this._overrideTrustGeneratorType
|
|
661
|
+
});
|
|
662
|
+
entity.state = DataspaceProtocolTransferProcessStateType.STARTED;
|
|
663
|
+
entity.dateModified = new Date();
|
|
664
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
665
|
+
try {
|
|
666
|
+
await transferHandler.onProviderStart(this._dataPlaneComponent, entity.consumerPid, previousState);
|
|
861
667
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// the TransferRequestMessage.dataAddress. Provider responds with its own /inbox
|
|
865
|
-
// URL so the consumer knows where to route data notifications.
|
|
866
|
-
if (!Is.stringValue(entity.dataAddress?.endpoint) ||
|
|
867
|
-
!Is.stringValue(entity.dataAddress?.endpointType)) {
|
|
868
|
-
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "invalidPushDataAddress", {
|
|
869
|
-
consumerPid: entity.consumerPid
|
|
870
|
-
}), message);
|
|
871
|
-
}
|
|
872
|
-
if (!Is.stringValue(this._dataPlanePath)) {
|
|
873
|
-
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid: entity.consumerPid }), message);
|
|
874
|
-
}
|
|
875
|
-
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}/inbox`;
|
|
876
|
-
// Bake the provider's organization identity into the /inbox URL so the consumer's
|
|
877
|
-
// inbound POST routes to the right organization via TenantProcessor — mirrors the
|
|
878
|
-
// pull-mode endpoint baking.
|
|
879
|
-
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
880
|
-
fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
881
|
-
response.dataAddress = {
|
|
882
|
-
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
883
|
-
endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
|
|
884
|
-
endpoint: fullEndpoint
|
|
885
|
-
};
|
|
886
|
-
await this._loggingComponent?.log({
|
|
887
|
-
level: "info",
|
|
888
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
889
|
-
ts: Date.now(),
|
|
890
|
-
message: "pushTransferStarted",
|
|
891
|
-
data: {
|
|
892
|
-
consumerPid: entity.consumerPid,
|
|
893
|
-
providerPid: entity.providerPid,
|
|
894
|
-
endpoint: fullEndpoint,
|
|
895
|
-
transferMode: DataspaceTransferFormat.HttpDataPush
|
|
896
|
-
}
|
|
897
|
-
});
|
|
898
|
-
const dataPlane = this.requireDataPlane();
|
|
899
|
-
// Push subscription setup reads the entity from storage and requires state=STARTED.
|
|
900
|
-
// Persist STARTED before the data-plane call, and roll back if subscription setup
|
|
901
|
-
// fails so the row doesn't leak to STARTED on a setup-time error.
|
|
902
|
-
entity.state = DataspaceProtocolTransferProcessStateType.STARTED;
|
|
668
|
+
catch (subscriptionError) {
|
|
669
|
+
entity.state = previousState;
|
|
903
670
|
entity.dateModified = new Date();
|
|
904
671
|
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
905
|
-
|
|
906
|
-
if (previousState === DataspaceProtocolTransferProcessStateType.REQUESTED) {
|
|
907
|
-
await dataPlane.setupPushSubscription(entity.consumerPid);
|
|
908
|
-
}
|
|
909
|
-
else if (previousState === DataspaceProtocolTransferProcessStateType.SUSPENDED) {
|
|
910
|
-
await dataPlane.resumePushSubscription(entity.consumerPid);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
catch (subscriptionError) {
|
|
914
|
-
entity.state = previousState;
|
|
915
|
-
entity.dateModified = new Date();
|
|
916
|
-
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
917
|
-
throw subscriptionError;
|
|
918
|
-
}
|
|
672
|
+
throw subscriptionError;
|
|
919
673
|
}
|
|
920
|
-
entity.state = DataspaceProtocolTransferProcessStateType.STARTED;
|
|
921
|
-
entity.dateModified = new Date();
|
|
922
|
-
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
923
674
|
await this._loggingComponent?.log({
|
|
924
675
|
level: "info",
|
|
925
676
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -1050,19 +801,18 @@ export class DataspaceControlPlaneService {
|
|
|
1050
801
|
role
|
|
1051
802
|
}
|
|
1052
803
|
});
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
}
|
|
804
|
+
const transferHandler = TransferHandlerFactory.get(this.inferTransferFormat(entity));
|
|
805
|
+
try {
|
|
806
|
+
await transferHandler.onComplete(this._dataPlaneComponent, entity.consumerPid);
|
|
807
|
+
}
|
|
808
|
+
catch (teardownError) {
|
|
809
|
+
// Symmetric rollback: data-plane teardown failed, revert the transfer state
|
|
810
|
+
// so the client can retry. Without this the storage row is COMPLETED but
|
|
811
|
+
// the subscription is still flowing, and a retry hits invalidStateForComplete.
|
|
812
|
+
entity.state = previousState;
|
|
813
|
+
entity.dateModified = new Date();
|
|
814
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
815
|
+
throw teardownError;
|
|
1066
816
|
}
|
|
1067
817
|
// Completion is consumer-initiated (the auth above accepts only the consumer): a consumer→provider
|
|
1068
818
|
// notification, so no provider→consumer delivery here — unlike suspend/terminate (either party).
|
|
@@ -1147,18 +897,17 @@ export class DataspaceControlPlaneService {
|
|
|
1147
897
|
reason: message.reason
|
|
1148
898
|
}
|
|
1149
899
|
});
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
}
|
|
900
|
+
const transferHandler = TransferHandlerFactory.get(this.inferTransferFormat(entity));
|
|
901
|
+
try {
|
|
902
|
+
await transferHandler.onSuspend(this._dataPlaneComponent, entity.consumerPid);
|
|
903
|
+
}
|
|
904
|
+
catch (suspendError) {
|
|
905
|
+
// Symmetric rollback: data-plane suspend failed, revert transfer state so
|
|
906
|
+
// a retry can repair the subscription instead of failing invalidStateForSuspend.
|
|
907
|
+
entity.state = previousState;
|
|
908
|
+
entity.dateModified = new Date();
|
|
909
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
910
|
+
throw suspendError;
|
|
1162
911
|
}
|
|
1163
912
|
if (role === TransferProcessRole.Consumer) {
|
|
1164
913
|
await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.SUSPENDED);
|
|
@@ -1243,19 +992,18 @@ export class DataspaceControlPlaneService {
|
|
|
1243
992
|
reason: message.reason
|
|
1244
993
|
}
|
|
1245
994
|
});
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
995
|
+
const transferHandler = TransferHandlerFactory.get(this.inferTransferFormat(entity));
|
|
996
|
+
try {
|
|
997
|
+
await transferHandler.onTerminate(this._dataPlaneComponent, entity.consumerPid);
|
|
998
|
+
}
|
|
999
|
+
catch (teardownError) {
|
|
1000
|
+
// Symmetric rollback: data-plane teardown failed, revert transfer state so
|
|
1001
|
+
// a retry can repair the subscription. Terminate is reachable from multiple
|
|
1002
|
+
// states (REQUESTED/STARTED/SUSPENDED), so restore the actual previous one.
|
|
1003
|
+
entity.state = previousState;
|
|
1004
|
+
entity.dateModified = new Date();
|
|
1005
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
1006
|
+
throw teardownError;
|
|
1259
1007
|
}
|
|
1260
1008
|
if (role === TransferProcessRole.Consumer) {
|
|
1261
1009
|
await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.TERMINATED);
|
|
@@ -1796,6 +1544,25 @@ export class DataspaceControlPlaneService {
|
|
|
1796
1544
|
await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.AppDatasetsDeleted);
|
|
1797
1545
|
}
|
|
1798
1546
|
// ============================================================================
|
|
1547
|
+
// DSP VERSION DISCOVERY
|
|
1548
|
+
// ============================================================================
|
|
1549
|
+
/**
|
|
1550
|
+
* Return the Dataspace Protocol versions supported by this connector.
|
|
1551
|
+
* @returns The protocol version response listing all supported DSP versions.
|
|
1552
|
+
*/
|
|
1553
|
+
async getProtocolVersions() {
|
|
1554
|
+
return {
|
|
1555
|
+
protocolVersions: [
|
|
1556
|
+
{
|
|
1557
|
+
version: "2025-1",
|
|
1558
|
+
path: `/${this._callbackPath}`,
|
|
1559
|
+
binding: DataspaceProtocolVersionBindingType.HTTPS,
|
|
1560
|
+
serviceId: "twin-connector"
|
|
1561
|
+
}
|
|
1562
|
+
]
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
// ============================================================================
|
|
1799
1566
|
// PRIVATE HELPER METHODS
|
|
1800
1567
|
// ============================================================================
|
|
1801
1568
|
/**
|
|
@@ -2149,16 +1916,6 @@ export class DataspaceControlPlaneService {
|
|
|
2149
1916
|
}
|
|
2150
1917
|
return agreement;
|
|
2151
1918
|
}
|
|
2152
|
-
/**
|
|
2153
|
-
* Check whether a transfer format string represents a push-mode delivery.
|
|
2154
|
-
* @param format The transfer format string from the TransferProcess entity.
|
|
2155
|
-
* @returns True if the format is a push variant (HttpData-PUSH or HttpData-POST).
|
|
2156
|
-
* @internal
|
|
2157
|
-
*/
|
|
2158
|
-
isPushFormat(format) {
|
|
2159
|
-
return (format === DataspaceTransferFormat.HttpDataPush ||
|
|
2160
|
-
format === DataspaceTransferFormat.HttpDataPost);
|
|
2161
|
-
}
|
|
2162
1919
|
/**
|
|
2163
1920
|
* Extract the dataset ID from an ODRL agreement's target.
|
|
2164
1921
|
* @param agreement The ODRL agreement containing the target.
|
|
@@ -2414,20 +2171,6 @@ export class DataspaceControlPlaneService {
|
|
|
2414
2171
|
}
|
|
2415
2172
|
return dataset;
|
|
2416
2173
|
}
|
|
2417
|
-
/**
|
|
2418
|
-
* Resolve the data plane component or throw if it isn't registered. Push-mode transfers
|
|
2419
|
-
* require the data plane; pull-only deployments may run without it.
|
|
2420
|
-
* @returns The data plane component.
|
|
2421
|
-
* @throws GeneralError if the data plane component is not registered.
|
|
2422
|
-
* @internal
|
|
2423
|
-
*/
|
|
2424
|
-
requireDataPlane() {
|
|
2425
|
-
const dataPlane = ComponentFactory.getIfExists(this._dataPlaneComponentType);
|
|
2426
|
-
if (!dataPlane) {
|
|
2427
|
-
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "dataPlaneNotRegistered");
|
|
2428
|
-
}
|
|
2429
|
-
return dataPlane;
|
|
2430
|
-
}
|
|
2431
2174
|
/**
|
|
2432
2175
|
* Creates the internal INegotiationCallback that fans out to all registered callbacks.
|
|
2433
2176
|
* @returns The internal negotiation callback.
|
|
@@ -2639,6 +2382,28 @@ export class DataspaceControlPlaneService {
|
|
|
2639
2382
|
const contextIds = await ContextIdStore.getContextIds();
|
|
2640
2383
|
return contextIds?.[ContextIdKeys.Tenant];
|
|
2641
2384
|
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Resolve the transfer format for handler dispatch. When the entity already carries a
|
|
2387
|
+
* format string (all transfers created by requestTransfer), that value is used directly.
|
|
2388
|
+
* For entities seeded without a format (e.g. older storage records), the format is inferred
|
|
2389
|
+
* from the presence of a consumer-supplied dataAddress — matching the pre-factory dispatch
|
|
2390
|
+
* logic so existing records continue to work correctly.
|
|
2391
|
+
* @param entity The transfer process entity.
|
|
2392
|
+
* @returns The effective DataspaceTransferFormat for handler lookup.
|
|
2393
|
+
* @internal
|
|
2394
|
+
*/
|
|
2395
|
+
inferTransferFormat(entity) {
|
|
2396
|
+
const knownFormats = Object.values(DataspaceTransferFormat);
|
|
2397
|
+
if (Is.stringValue(entity.format) && knownFormats.includes(entity.format)) {
|
|
2398
|
+
return entity.format;
|
|
2399
|
+
}
|
|
2400
|
+
// Backward-compat: entities without a recognized format string (e.g. stored before
|
|
2401
|
+
// the factory was introduced, or using a non-standard format value) fall back to
|
|
2402
|
+
// dataAddress-based dispatch matching the pre-factory logic.
|
|
2403
|
+
return Is.empty(entity.dataAddress)
|
|
2404
|
+
? DataspaceTransferFormat.HttpDataPull
|
|
2405
|
+
: DataspaceTransferFormat.HttpDataPush;
|
|
2406
|
+
}
|
|
2642
2407
|
/**
|
|
2643
2408
|
* Return an existing full-access agreement for the same-organization (implicit trust) case,
|
|
2644
2409
|
* or create and store one if none exists. Fires onFinalized on all registered callbacks in
|