@twin.org/dataspace-control-plane-service 0.0.3-next.45 → 0.0.3-next.47
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/dist/es/dataspaceControlPlaneRoutes.js +2 -1
- package/dist/es/dataspaceControlPlaneRoutes.js.map +1 -1
- package/dist/es/dataspaceControlPlaneService.js +268 -31
- package/dist/es/dataspaceControlPlaneService.js.map +1 -1
- package/dist/es/models/IDataspaceControlPlaneServiceConfig.js.map +1 -1
- package/dist/types/dataspaceControlPlaneService.d.ts +24 -3
- package/dist/types/models/IDataspaceControlPlaneServiceConfig.d.ts +8 -0
- package/docs/changelog.md +28 -0
- package/docs/reference/classes/DataspaceControlPlaneService.md +79 -5
- package/docs/reference/interfaces/IDataspaceControlPlaneServiceConfig.md +12 -0
- package/locales/en.json +7 -2
- package/package.json +2 -2
|
@@ -93,6 +93,12 @@ export class DataspaceControlPlaneService {
|
|
|
93
93
|
* @internal
|
|
94
94
|
*/
|
|
95
95
|
_dataPlanePath;
|
|
96
|
+
/**
|
|
97
|
+
* Control plane callback mount path (path only, not full URL). Combined with this node's public
|
|
98
|
+
* origin to form the consumer callbackAddress the provider POSTs DSP transfer messages back to.
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
_callbackPath;
|
|
96
102
|
/**
|
|
97
103
|
* Factory key used to look up the data plane component. Resolved lazily at push-time
|
|
98
104
|
* (not at construction) so the data plane may register after the control plane is built.
|
|
@@ -158,6 +164,9 @@ export class DataspaceControlPlaneService {
|
|
|
158
164
|
this._dataPlanePath = Is.stringValue(options?.config?.dataPlanePath)
|
|
159
165
|
? StringHelper.trimTrailingSlashes(StringHelper.trimLeadingSlashes(options.config.dataPlanePath))
|
|
160
166
|
: undefined;
|
|
167
|
+
this._callbackPath = Is.stringValue(options?.config?.callbackPath)
|
|
168
|
+
? StringHelper.trimLeadingAndTrailingSlashes(options.config.callbackPath)
|
|
169
|
+
: undefined;
|
|
161
170
|
this._taskScheduler = ComponentFactory.getIfExists(options?.taskSchedulerComponentType ?? "task-scheduler");
|
|
162
171
|
this._platformComponent = ComponentFactory.getIfExists(options?.platformComponentType ?? "platform");
|
|
163
172
|
// Data plane component is optional and resolved lazily. The control plane is initialised
|
|
@@ -332,13 +341,18 @@ export class DataspaceControlPlaneService {
|
|
|
332
341
|
* Request a Transfer Process.
|
|
333
342
|
* Creates a new Transfer Process in REQUESTED state.
|
|
334
343
|
* @param request Transfer request message (DSP compliant).
|
|
344
|
+
* @param publicOrigin The public origin of this provider node, resolved by the REST route from the
|
|
345
|
+
* hosting component; used to build the data-plane endpoint when auto-starting.
|
|
346
|
+
* @param options Request options.
|
|
347
|
+
* @param options.autoStart When true, the provider immediately starts the requested transfer (scheduled
|
|
348
|
+
* on the next tick); when omitted/false the provider start must be triggered explicitly.
|
|
335
349
|
* @param trustPayload Trust payload containing authorization information (Base64-encoded token).
|
|
336
350
|
* @returns Transfer Process (DSP compliant) with state REQUESTED, or TransferError if the operation fails.
|
|
337
351
|
*
|
|
338
352
|
* Role Performed: Provider
|
|
339
353
|
* Called by: Consumer when it wants to request a new Transfer Process
|
|
340
354
|
*/
|
|
341
|
-
async requestTransfer(request, trustPayload) {
|
|
355
|
+
async requestTransfer(request, publicOrigin, options, trustPayload) {
|
|
342
356
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "requestTransfer");
|
|
343
357
|
const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(request));
|
|
344
358
|
if (Is.arrayValue(validationFailures)) {
|
|
@@ -405,6 +419,7 @@ export class DataspaceControlPlaneService {
|
|
|
405
419
|
datasetId,
|
|
406
420
|
consumerIdentity,
|
|
407
421
|
providerIdentity,
|
|
422
|
+
localRole: TransferProcessRole.Provider,
|
|
408
423
|
// offerId should reference Catalog Offer (via Agreement)
|
|
409
424
|
// For now, use agreementId as reference (proper flow: Catalog → Negotiation → Agreement)
|
|
410
425
|
offerId: request.agreementId,
|
|
@@ -429,6 +444,16 @@ export class DataspaceControlPlaneService {
|
|
|
429
444
|
agreementId: request.agreementId
|
|
430
445
|
}
|
|
431
446
|
});
|
|
447
|
+
// When auto-start is requested, schedule the provider start asynchronously (mirrors negotiation's
|
|
448
|
+
// setTimeout follow-up). The timer runs inside the request's ALS context, so the [Node, Tenant] + org
|
|
449
|
+
// partition propagates to the deferred start.
|
|
450
|
+
if (options?.autoStart) {
|
|
451
|
+
const consumerPid = request.consumerPid;
|
|
452
|
+
setTimeout(async () => {
|
|
453
|
+
// runProviderStart catches internally and can't reject; awaiting satisfies no-floating-promises.
|
|
454
|
+
await this.runProviderStart(consumerPid, publicOrigin);
|
|
455
|
+
}, 0);
|
|
456
|
+
}
|
|
432
457
|
return {
|
|
433
458
|
"@context": [DataspaceProtocolContexts.Context],
|
|
434
459
|
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
@@ -438,7 +463,7 @@ export class DataspaceControlPlaneService {
|
|
|
438
463
|
};
|
|
439
464
|
}
|
|
440
465
|
/**
|
|
441
|
-
*
|
|
466
|
+
* Prepare a data transfer as a Consumer.
|
|
442
467
|
* Generates a consumerPid, POSTs a TransferRequestMessage to the provider's DSP endpoint,
|
|
443
468
|
* and (only if the provider accepts) persists a local TransferProcess in REQUESTED state.
|
|
444
469
|
* @param agreementId The finalized agreement ID from contract negotiation.
|
|
@@ -456,7 +481,7 @@ export class DataspaceControlPlaneService {
|
|
|
456
481
|
* registration ignores the runtime `endpoint` arg and silently POSTs to its
|
|
457
482
|
* static endpoint instead.
|
|
458
483
|
*/
|
|
459
|
-
async
|
|
484
|
+
async prepareTransfer(agreementId, providerEndpoint, publicOrigin, format, trustPayload) {
|
|
460
485
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "agreementId", agreementId);
|
|
461
486
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "providerEndpoint", providerEndpoint);
|
|
462
487
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "publicOrigin", publicOrigin);
|
|
@@ -467,12 +492,12 @@ export class DataspaceControlPlaneService {
|
|
|
467
492
|
supported: Object.values(DataspaceTransferFormat)
|
|
468
493
|
});
|
|
469
494
|
}
|
|
470
|
-
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "
|
|
495
|
+
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "prepareTransfer");
|
|
471
496
|
await this._loggingComponent?.log({
|
|
472
497
|
level: "info",
|
|
473
498
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
474
499
|
ts: Date.now(),
|
|
475
|
-
message: "
|
|
500
|
+
message: "preparingTransfer",
|
|
476
501
|
data: { agreementId, providerEndpoint, format, identity: trustInfo.identity }
|
|
477
502
|
});
|
|
478
503
|
const agreement = await this.lookupAgreement(agreementId);
|
|
@@ -489,9 +514,18 @@ export class DataspaceControlPlaneService {
|
|
|
489
514
|
const providerIdentity = assignerIds[0];
|
|
490
515
|
const datasetId = this.extractDatasetId(agreement);
|
|
491
516
|
const consumerPid = `urn:uuid:${RandomHelper.generateUuidV7()}`;
|
|
492
|
-
const
|
|
493
|
-
// Fetch context once and reuse across the PUSH dataAddress, trust token, and storage
|
|
517
|
+
const origin = StringHelper.trimTrailingSlashes(publicOrigin);
|
|
518
|
+
// Fetch context once and reuse across the callback, PUSH dataAddress, trust token, and storage.
|
|
494
519
|
const organizationIdentity = await this.resolveContextOrganizationId();
|
|
520
|
+
// Callback the provider POSTs DSP messages back to: mount path + `?organization=` so the inbound
|
|
521
|
+
// POST routes to the right consumer tenant (mirrors buildCallbackUrl). The bare `origin` is kept for
|
|
522
|
+
// the PUSH-inbox base below (a different mount).
|
|
523
|
+
let callbackAddress = Is.stringValue(this._callbackPath)
|
|
524
|
+
? `${origin}/${this._callbackPath}`
|
|
525
|
+
: origin;
|
|
526
|
+
if (Is.stringValue(organizationIdentity)) {
|
|
527
|
+
callbackAddress = HttpUrlHelper.addQueryStringParam(callbackAddress, ContextIdKeys.Organization, organizationIdentity);
|
|
528
|
+
}
|
|
495
529
|
const transferRequestMessage = {
|
|
496
530
|
"@context": [DataspaceProtocolContexts.Context],
|
|
497
531
|
"@type": DataspaceProtocolTransferProcessTypes.TransferRequestMessage,
|
|
@@ -507,7 +541,7 @@ export class DataspaceControlPlaneService {
|
|
|
507
541
|
if (!Is.stringValue(this._dataPlanePath)) {
|
|
508
542
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid });
|
|
509
543
|
}
|
|
510
|
-
let inboxEndpoint = `${
|
|
544
|
+
let inboxEndpoint = `${origin}/${this._dataPlanePath}/inbox`;
|
|
511
545
|
if (Is.stringValue(organizationIdentity)) {
|
|
512
546
|
inboxEndpoint = HttpUrlHelper.addQueryStringParam(inboxEndpoint, ContextIdKeys.Organization, organizationIdentity);
|
|
513
547
|
}
|
|
@@ -521,7 +555,7 @@ export class DataspaceControlPlaneService {
|
|
|
521
555
|
const outboundToken = await this._trustComponent.generate(organizationIdentity, this._overrideTrustGeneratorType, { subject: { consumerPid, agreementId } });
|
|
522
556
|
// Create a remote REST client pointed at the provider endpoint and call requestTransfer.
|
|
523
557
|
const remoteControlPlane = ComponentFactory.create(this._remoteControlPlaneComponentType, { endpoint: providerEndpoint, pathPrefix: "" });
|
|
524
|
-
const result = await remoteControlPlane.requestTransfer(transferRequestMessage, outboundToken);
|
|
558
|
+
const result = await remoteControlPlane.requestTransfer(transferRequestMessage, "", undefined, outboundToken);
|
|
525
559
|
if (getJsonLdType(result) === DataspaceProtocolTransferProcessTypes.TransferError) {
|
|
526
560
|
const transferError = result;
|
|
527
561
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "transferRequestRejectedByProvider", {
|
|
@@ -544,6 +578,7 @@ export class DataspaceControlPlaneService {
|
|
|
544
578
|
datasetId,
|
|
545
579
|
consumerIdentity: trustInfo.identity,
|
|
546
580
|
providerIdentity,
|
|
581
|
+
localRole: TransferProcessRole.Consumer,
|
|
547
582
|
offerId: agreementId,
|
|
548
583
|
policies: [agreement],
|
|
549
584
|
callbackAddress,
|
|
@@ -558,7 +593,7 @@ export class DataspaceControlPlaneService {
|
|
|
558
593
|
level: "info",
|
|
559
594
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
560
595
|
ts: Date.now(),
|
|
561
|
-
message: "
|
|
596
|
+
message: "transferPrepared",
|
|
562
597
|
data: {
|
|
563
598
|
consumerPid,
|
|
564
599
|
providerPid: storageEntity.providerPid,
|
|
@@ -613,6 +648,28 @@ export class DataspaceControlPlaneService {
|
|
|
613
648
|
currentState: entity.state
|
|
614
649
|
}), message);
|
|
615
650
|
}
|
|
651
|
+
// CONSUMER-RECEIVE: the provider POSTed a TransferStart to our callback. Use the dataAddress
|
|
652
|
+
// from the message (rebuilding it is provider work), mark our record STARTED, and notify.
|
|
653
|
+
if (role === TransferProcessRole.Consumer) {
|
|
654
|
+
entity.state = DataspaceProtocolTransferProcessStateType.STARTED;
|
|
655
|
+
entity.dateModified = new Date();
|
|
656
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
657
|
+
await this._loggingComponent?.log({
|
|
658
|
+
level: "info",
|
|
659
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
660
|
+
ts: Date.now(),
|
|
661
|
+
message: "transferProcessStarted",
|
|
662
|
+
data: {
|
|
663
|
+
consumerPid: entity.consumerPid,
|
|
664
|
+
providerPid: entity.providerPid,
|
|
665
|
+
role
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.STARTED);
|
|
669
|
+
await this._internalTransferCallback.onStarted(entity.consumerPid, message);
|
|
670
|
+
return message;
|
|
671
|
+
}
|
|
672
|
+
// PROVIDER-ACT (role === Provider): build the dataAddress, transition to STARTED, deliver below.
|
|
616
673
|
// The previous (pre-transition) state drives the push-subscription branch below
|
|
617
674
|
// (setup vs resume). Do NOT mutate entity.state here as the persisted transition
|
|
618
675
|
// to STARTED happens after the dispatch block succeeds, so any validation or
|
|
@@ -829,16 +886,60 @@ export class DataspaceControlPlaneService {
|
|
|
829
886
|
role
|
|
830
887
|
}
|
|
831
888
|
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
889
|
+
// Notify the consumer by POSTing the TransferStart (with its dataAddress) to the callback.
|
|
890
|
+
// Best-effort: a delivery failure must not roll back the STARTED transition.
|
|
891
|
+
await this.deliverToConsumerCallback(entity, "start", async (remoteControlPlane, token) => remoteControlPlane.startTransfer(response, "", token));
|
|
836
892
|
return response;
|
|
837
893
|
}
|
|
838
894
|
catch (error) {
|
|
839
895
|
return transformToTransferError(error, message);
|
|
840
896
|
}
|
|
841
897
|
}
|
|
898
|
+
/**
|
|
899
|
+
* Start a Transfer Process as the Provider.
|
|
900
|
+
* Builds a TransferStartMessage for a transfer already accepted by this node (REQUESTED, or
|
|
901
|
+
* SUSPENDED to resume), transitions it to STARTED, and POSTs the message to the consumer callback.
|
|
902
|
+
* consumerPid/providerPid/callbackAddress are resolved from the stored record; the call is then
|
|
903
|
+
* forwarded to startTransfer, the single owner of the start state machine (it verifies the caller is
|
|
904
|
+
* the provider, builds the dataAddress for PULL, persists STARTED, and delivers to the consumer).
|
|
905
|
+
* This is the provider-side mirror of prepareTransfer.
|
|
906
|
+
* @param pid The Process ID (consumerPid or providerPid) identifying the transfer to start.
|
|
907
|
+
* @param publicOrigin The public origin URL of this provider node (used to build the data plane endpoint for PULL transfers).
|
|
908
|
+
* @param trustPayload Trust payload proving the caller is the provider.
|
|
909
|
+
* @returns Transfer Start Message (DSP compliant) with dataAddress for PULL transfers, or TransferError if the operation fails.
|
|
910
|
+
*/
|
|
911
|
+
async transferStarted(pid, publicOrigin, trustPayload) {
|
|
912
|
+
let consumerPid;
|
|
913
|
+
let providerPid;
|
|
914
|
+
try {
|
|
915
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "pid", pid);
|
|
916
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "publicOrigin", publicOrigin);
|
|
917
|
+
const { entity, role } = await this.lookupTransferByPid(pid);
|
|
918
|
+
consumerPid = entity.consumerPid;
|
|
919
|
+
providerPid = entity.providerPid;
|
|
920
|
+
// Only the provider initiates a start. startTransfer branches on role, so guard here to give a
|
|
921
|
+
// clear error instead of silently running the consumer-receive path on a consumer-role record.
|
|
922
|
+
if (role !== TransferProcessRole.Provider) {
|
|
923
|
+
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "transferStartNotProvider", {
|
|
924
|
+
pid,
|
|
925
|
+
role
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
const startMessage = {
|
|
929
|
+
"@context": [DataspaceProtocolContexts.Context],
|
|
930
|
+
"@type": DataspaceProtocolTransferProcessTypes.TransferStartMessage,
|
|
931
|
+
consumerPid: entity.consumerPid,
|
|
932
|
+
providerPid: entity.providerPid
|
|
933
|
+
};
|
|
934
|
+
// startTransfer verifies trustPayload is the provider, checks org ownership, applies the
|
|
935
|
+
// REQUESTED|SUSPENDED state guard, builds the dataAddress, persists STARTED, and delivers the
|
|
936
|
+
// TransferStartMessage to the consumer callback.
|
|
937
|
+
return await this.startTransfer(startMessage, publicOrigin, trustPayload);
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
return transformToTransferError(error, { consumerPid, providerPid });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
842
943
|
// ----------------------------------------------------------------------------
|
|
843
944
|
// SHARED STATE MANAGEMENT OPERATIONS (Either Side)
|
|
844
945
|
// ----------------------------------------------------------------------------
|
|
@@ -919,6 +1020,8 @@ export class DataspaceControlPlaneService {
|
|
|
919
1020
|
throw teardownError;
|
|
920
1021
|
}
|
|
921
1022
|
}
|
|
1023
|
+
// Completion is consumer-initiated (the auth above accepts only the consumer): a consumer→provider
|
|
1024
|
+
// notification, so no provider→consumer delivery here — unlike suspend/terminate (either party).
|
|
922
1025
|
if (role === TransferProcessRole.Consumer) {
|
|
923
1026
|
await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.COMPLETED);
|
|
924
1027
|
await this._internalTransferCallback.onCompleted(entity.consumerPid);
|
|
@@ -1021,6 +1124,10 @@ export class DataspaceControlPlaneService {
|
|
|
1021
1124
|
: message.reason;
|
|
1022
1125
|
await this._internalTransferCallback.onSuspended(entity.consumerPid, suspendReason);
|
|
1023
1126
|
}
|
|
1127
|
+
else {
|
|
1128
|
+
// PROVIDER-ACT: forward the suspension (with its reason) to the consumer callback.
|
|
1129
|
+
await this.deliverToConsumerCallback(entity, "suspension", async (remoteControlPlane, token) => remoteControlPlane.suspendTransfer(message, token));
|
|
1130
|
+
}
|
|
1024
1131
|
return {
|
|
1025
1132
|
"@context": [DataspaceProtocolContexts.Context],
|
|
1026
1133
|
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
@@ -1113,6 +1220,10 @@ export class DataspaceControlPlaneService {
|
|
|
1113
1220
|
: message.reason;
|
|
1114
1221
|
await this._internalTransferCallback.onTerminated(entity.consumerPid, terminateReason);
|
|
1115
1222
|
}
|
|
1223
|
+
else {
|
|
1224
|
+
// PROVIDER-ACT: forward the termination (with its reason) to the consumer callback.
|
|
1225
|
+
await this.deliverToConsumerCallback(entity, "termination", async (remoteControlPlane, token) => remoteControlPlane.terminateTransfer(message, token));
|
|
1226
|
+
}
|
|
1116
1227
|
return {
|
|
1117
1228
|
"@context": [DataspaceProtocolContexts.Context],
|
|
1118
1229
|
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
@@ -1676,6 +1787,128 @@ export class DataspaceControlPlaneService {
|
|
|
1676
1787
|
});
|
|
1677
1788
|
}
|
|
1678
1789
|
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Perform the provider-side start of a just-requested transfer (build dataAddress, transition to
|
|
1792
|
+
* STARTED, deliver to the consumer). Only invoked when the request opted into auto-start, so there is no
|
|
1793
|
+
* approval step. Self-contained: failures are logged, never thrown. Runs inside the request's ALS
|
|
1794
|
+
* context, so the deferred storage access inherits the [Node, Tenant] + org partition.
|
|
1795
|
+
* @param consumerPid The consumerPid of the requested transfer.
|
|
1796
|
+
* @param publicOrigin This provider node's public origin, used to build the data-plane endpoint.
|
|
1797
|
+
* @internal
|
|
1798
|
+
*/
|
|
1799
|
+
async runProviderStart(consumerPid, publicOrigin) {
|
|
1800
|
+
try {
|
|
1801
|
+
const { entity } = await this.lookupTransferByPid(consumerPid);
|
|
1802
|
+
if (entity.state !== DataspaceProtocolTransferProcessStateType.REQUESTED) {
|
|
1803
|
+
// Already advanced (e.g. an explicit start raced the auto-start). Nothing to do.
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (!Is.stringValue(entity.providerIdentity)) {
|
|
1807
|
+
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
|
|
1808
|
+
}
|
|
1809
|
+
// Fail closed: without a public origin the data-plane endpoint in the dataAddress would be
|
|
1810
|
+
// broken, so hold the transfer rather than ship a relative/empty endpoint to the consumer.
|
|
1811
|
+
if (!Is.stringValue(publicOrigin)) {
|
|
1812
|
+
await this._loggingComponent?.log({
|
|
1813
|
+
level: "error",
|
|
1814
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
1815
|
+
ts: Date.now(),
|
|
1816
|
+
message: "autoStartPublicOriginMissing",
|
|
1817
|
+
data: { consumerPid }
|
|
1818
|
+
});
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
// Self-token issued as the provider so startTransfer's provider-auth check passes. Auto-start's
|
|
1822
|
+
// caller auth already happened upstream at requestTransfer, so minting the proof here (rather than
|
|
1823
|
+
// requiring an inbound token) is the boundary-correct place for it; transferStarted forwards it as
|
|
1824
|
+
// the trustPayload.
|
|
1825
|
+
const selfToken = await this._trustComponent.generate(entity.providerIdentity, this._overrideTrustGeneratorType, {
|
|
1826
|
+
subject: {
|
|
1827
|
+
consumerPid: entity.consumerPid,
|
|
1828
|
+
providerPid: entity.providerPid,
|
|
1829
|
+
agreementId: entity.agreementId
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
const result = await this.transferStarted(consumerPid, publicOrigin, selfToken);
|
|
1833
|
+
if (getJsonLdType(result) === DataspaceProtocolTransferProcessTypes.TransferError) {
|
|
1834
|
+
await this._loggingComponent?.log({
|
|
1835
|
+
level: "error",
|
|
1836
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
1837
|
+
ts: Date.now(),
|
|
1838
|
+
message: "autoStartFailed",
|
|
1839
|
+
data: { consumerPid }
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
catch (error) {
|
|
1844
|
+
await this._loggingComponent?.log({
|
|
1845
|
+
level: "error",
|
|
1846
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
1847
|
+
ts: Date.now(),
|
|
1848
|
+
message: "autoStartFailed",
|
|
1849
|
+
error: BaseError.fromError(error),
|
|
1850
|
+
data: { consumerPid }
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Deliver a transfer state-change DSP message to the consumer's callback (provider → consumer), via a
|
|
1856
|
+
* remote control-plane client and the supplied sender. Mirrors negotiation's sendOfferToConsumer.
|
|
1857
|
+
* Best-effort: failures are logged, not thrown, so delivery problems don't roll back the transition.
|
|
1858
|
+
* @param entity The transfer process (provider side).
|
|
1859
|
+
* @param messageKind Short label for logging (e.g. "start", "suspension", "termination").
|
|
1860
|
+
* @param send Performs the remote POST given the remote client and an outbound trust token.
|
|
1861
|
+
* @internal
|
|
1862
|
+
*/
|
|
1863
|
+
async deliverToConsumerCallback(entity, messageKind, send) {
|
|
1864
|
+
if (!Is.stringValue(entity.callbackAddress) || !Is.stringValue(entity.providerIdentity)) {
|
|
1865
|
+
// Spec-allowed: no callback supplied → the consumer polls GET /transfers/:pid instead.
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
try {
|
|
1869
|
+
// Issue the token as the provider party (the agreement assigner) — the identity the consumer's
|
|
1870
|
+
// receive gate checks (`!== providerIdentity`), not the tenant-routing org. Keeps the two
|
|
1871
|
+
// identity spaces separate and works whether or not a node's org == its assigner DID.
|
|
1872
|
+
const outboundToken = await this._trustComponent.generate(entity.providerIdentity, this._overrideTrustGeneratorType, {
|
|
1873
|
+
subject: {
|
|
1874
|
+
consumerPid: entity.consumerPid,
|
|
1875
|
+
providerPid: entity.providerPid,
|
|
1876
|
+
agreementId: entity.agreementId
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
const remoteControlPlane = ComponentFactory.create(this._remoteControlPlaneComponentType, { endpoint: entity.callbackAddress, pathPrefix: "" });
|
|
1880
|
+
const result = await send(remoteControlPlane, outboundToken);
|
|
1881
|
+
if (getJsonLdType(result) === DataspaceProtocolTransferProcessTypes.TransferError) {
|
|
1882
|
+
await this._loggingComponent?.log({
|
|
1883
|
+
level: "error",
|
|
1884
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
1885
|
+
ts: Date.now(),
|
|
1886
|
+
message: "transferCallbackRejected",
|
|
1887
|
+
data: {
|
|
1888
|
+
messageKind,
|
|
1889
|
+
consumerPid: entity.consumerPid,
|
|
1890
|
+
providerPid: entity.providerPid,
|
|
1891
|
+
callbackAddress: entity.callbackAddress
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
catch (error) {
|
|
1897
|
+
await this._loggingComponent?.log({
|
|
1898
|
+
level: "error",
|
|
1899
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
1900
|
+
ts: Date.now(),
|
|
1901
|
+
message: "transferCallbackFailed",
|
|
1902
|
+
error: BaseError.fromError(error),
|
|
1903
|
+
data: {
|
|
1904
|
+
messageKind,
|
|
1905
|
+
consumerPid: entity.consumerPid,
|
|
1906
|
+
providerPid: entity.providerPid,
|
|
1907
|
+
callbackAddress: entity.callbackAddress
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1679
1912
|
/**
|
|
1680
1913
|
* Convert a storage entity to model.
|
|
1681
1914
|
* @param storageEntity The entity from storage.
|
|
@@ -1693,6 +1926,7 @@ export class DataspaceControlPlaneService {
|
|
|
1693
1926
|
offerId: storageEntity.offerId,
|
|
1694
1927
|
consumerIdentity: storageEntity.consumerIdentity,
|
|
1695
1928
|
providerIdentity: storageEntity.providerIdentity,
|
|
1929
|
+
localRole: storageEntity.localRole,
|
|
1696
1930
|
format: storageEntity.format,
|
|
1697
1931
|
callbackAddress: storageEntity.callbackAddress,
|
|
1698
1932
|
organizationIdentity: storageEntity.organizationIdentity,
|
|
@@ -1719,6 +1953,7 @@ export class DataspaceControlPlaneService {
|
|
|
1719
1953
|
offerId: entity.offerId,
|
|
1720
1954
|
consumerIdentity: entity.consumerIdentity,
|
|
1721
1955
|
providerIdentity: entity.providerIdentity,
|
|
1956
|
+
localRole: entity.localRole,
|
|
1722
1957
|
format: entity.format,
|
|
1723
1958
|
callbackAddress: entity.callbackAddress,
|
|
1724
1959
|
organizationIdentity: entity.organizationIdentity,
|
|
@@ -1847,25 +2082,27 @@ export class DataspaceControlPlaneService {
|
|
|
1847
2082
|
*/
|
|
1848
2083
|
async lookupTransferByPid(pid) {
|
|
1849
2084
|
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "pid", pid);
|
|
1850
|
-
//
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2085
|
+
// consumerPid is the primary key on BOTH nodes, so the matched key alone is not a reliable role
|
|
2086
|
+
// signal; locate the record by primary, then the providerPid secondary index.
|
|
2087
|
+
let storageEntity = await this._transferProcessStorage.get(pid);
|
|
2088
|
+
let matchedByConsumerPid = true;
|
|
2089
|
+
if (!storageEntity) {
|
|
2090
|
+
storageEntity = await this._transferProcessStorage.get(pid, "providerPid");
|
|
2091
|
+
matchedByConsumerPid = false;
|
|
1857
2092
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
entity: this.storageEntityToModel(providerPidEntity),
|
|
1863
|
-
role: TransferProcessRole.Provider
|
|
1864
|
-
};
|
|
2093
|
+
if (!storageEntity) {
|
|
2094
|
+
throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "transferProcessNotFound", pid, {
|
|
2095
|
+
pid
|
|
2096
|
+
});
|
|
1865
2097
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2098
|
+
// Prefer the role persisted at write time (set in prepareTransfer / requestTransfer). Fall back
|
|
2099
|
+
// to the matched-key heuristic only for legacy records written before localRole existed.
|
|
2100
|
+
const role = storageEntity.localRole ??
|
|
2101
|
+
(matchedByConsumerPid ? TransferProcessRole.Consumer : TransferProcessRole.Provider);
|
|
2102
|
+
return {
|
|
2103
|
+
entity: this.storageEntityToModel(storageEntity),
|
|
2104
|
+
role
|
|
2105
|
+
};
|
|
1869
2106
|
}
|
|
1870
2107
|
/**
|
|
1871
2108
|
* Get raw policy entries from a catalog dataset.
|