@twin.org/dataspace-control-plane-service 0.0.3-next.29 → 0.0.3-next.30
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/dataspaceControlPlaneService.js +436 -125
- package/dist/es/dataspaceControlPlaneService.js.map +1 -1
- package/dist/es/index.js +1 -0
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IDataspaceControlPlaneServiceConstructorOptions.js.map +1 -1
- package/dist/es/models/endpointProperties.js +18 -0
- package/dist/es/models/endpointProperties.js.map +1 -0
- package/dist/es/utils/transferErrorUtils.js +1 -1
- package/dist/es/utils/transferErrorUtils.js.map +1 -1
- package/dist/types/dataspaceControlPlaneService.d.ts +12 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/models/IDataspaceControlPlaneServiceConstructorOptions.d.ts +6 -1
- package/dist/types/models/endpointProperties.d.ts +18 -0
- package/docs/changelog.md +14 -0
- package/docs/reference/index.md +5 -0
- package/docs/reference/interfaces/IDataspaceControlPlaneServiceConstructorOptions.md +15 -1
- package/docs/reference/type-aliases/EndpointProperties.md +5 -0
- package/docs/reference/variables/EndpointProperties.md +20 -0
- package/locales/en.json +10 -5
- package/package.json +3 -2
- package/dist/es/utils/dataHelpers.js +0 -22
- package/dist/es/utils/dataHelpers.js.map +0 -1
- package/dist/types/utils/dataHelpers.d.ts +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
|
|
2
|
-
import { ArrayHelper, BaseError, ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper, StringHelper, UnauthorizedError, ValidationError } from "@twin.org/core";
|
|
2
|
+
import { ArrayHelper, BaseError, ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, ObjectHelper, RandomHelper, StringHelper, UnauthorizedError, ValidationError } from "@twin.org/core";
|
|
3
3
|
import { JsonLdHelper } from "@twin.org/data-json-ld";
|
|
4
|
-
import { DataspaceAppFactory, TransferProcessRole } from "@twin.org/dataspace-models";
|
|
4
|
+
import { DataspaceAppFactory, DataspaceTransferFormat, TransferProcessRole, getJsonLdId, getJsonLdType } from "@twin.org/dataspace-models";
|
|
5
5
|
import { EngineCoreFactory } from "@twin.org/engine-models";
|
|
6
6
|
import { ComparisonOperator } from "@twin.org/entity";
|
|
7
7
|
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
@@ -9,7 +9,7 @@ import { OdrlPolicyHelper, PolicyRequesterFactory } from "@twin.org/rights-manag
|
|
|
9
9
|
import { DataspaceProtocolContexts, DataspaceProtocolContractNegotiationTypes, DataspaceProtocolEndpointType, DataspaceProtocolHelper, DataspaceProtocolTransferProcessStateType, DataspaceProtocolTransferProcessTypes } from "@twin.org/standards-dataspace-protocol";
|
|
10
10
|
import { TrustHelper } from "@twin.org/trust-models";
|
|
11
11
|
import { DataspaceControlPlanePolicyRequester } from "./dataspaceControlPlanePolicyRequester.js";
|
|
12
|
-
import {
|
|
12
|
+
import { EndpointProperties } from "./models/endpointProperties.js";
|
|
13
13
|
import { isCatalogError, isCatalogErrorName, transformToTransferError } from "./utils/transferErrorUtils.js";
|
|
14
14
|
/**
|
|
15
15
|
* Dataspace Control Plane Service implementation.
|
|
@@ -35,6 +35,14 @@ export class DataspaceControlPlaneService {
|
|
|
35
35
|
* @internal
|
|
36
36
|
*/
|
|
37
37
|
static _STALLED_NEGOTIATION_THRESHOLD_MS = 30 * 60 * 1000;
|
|
38
|
+
/**
|
|
39
|
+
* Matches the base64url encoding of a BLAKE2b-256 hash (32 bytes → 43 base64url
|
|
40
|
+
* chars, no padding). Used to discriminate the tenant-hash portion of a
|
|
41
|
+
* composite tenant identifier from a bare IOTA DID, whose `<id>` segment is
|
|
42
|
+
* always `0x<64-hex>` (66 chars) and cannot match.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
static _TENANT_HASH_PATTERN = /^[\w-]{43}$/;
|
|
38
46
|
/**
|
|
39
47
|
* The logging component.
|
|
40
48
|
* @internal
|
|
@@ -92,6 +100,12 @@ export class DataspaceControlPlaneService {
|
|
|
92
100
|
* @internal
|
|
93
101
|
*/
|
|
94
102
|
_dataPlanePath;
|
|
103
|
+
/**
|
|
104
|
+
* Factory key used to look up the data plane component. Resolved lazily at push-time
|
|
105
|
+
* (not at construction) so the data plane may register after the control plane is built.
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
_dataPlaneComponentType;
|
|
95
109
|
/**
|
|
96
110
|
* Policy requester instance for handling negotiation callbacks.
|
|
97
111
|
* @internal
|
|
@@ -133,61 +147,20 @@ export class DataspaceControlPlaneService {
|
|
|
133
147
|
this._trustComponent = ComponentFactory.get(options?.trustComponentType ?? "trust");
|
|
134
148
|
this._overrideTrustGeneratorType = options?.config?.overrideTrustGeneratorType;
|
|
135
149
|
this._dataPlanePath = Is.stringValue(options?.config?.dataPlanePath)
|
|
136
|
-
? StringHelper.trimLeadingSlashes(options.config.dataPlanePath)
|
|
150
|
+
? StringHelper.trimTrailingSlashes(StringHelper.trimLeadingSlashes(options.config.dataPlanePath))
|
|
137
151
|
: undefined;
|
|
138
152
|
this._taskScheduler = ComponentFactory.getIfExists(options?.taskSchedulerComponentType ?? "task-scheduler");
|
|
153
|
+
// Data plane component is optional and resolved lazily. The control plane is initialised
|
|
154
|
+
// BEFORE the data plane in engine startup order, so `getRegisteredInstanceTypeOptional`
|
|
155
|
+
// returns undefined when the engine constructs us — the wiring override can't fire here.
|
|
156
|
+
// The default therefore matches the engine's actual factory key
|
|
157
|
+
// (`nameofKebabCase(DataspaceDataPlaneService)`) so the late-bound `requireDataPlane()`
|
|
158
|
+
// lookup at push time finds it regardless of init order.
|
|
159
|
+
this._dataPlaneComponentType =
|
|
160
|
+
options?.dataPlaneComponentType ?? "dataspace-data-plane-service";
|
|
139
161
|
this._urlTransformerComponent = ComponentFactory.get(options?.urlTransformerComponentType ?? "url-transformer");
|
|
140
162
|
this._negotiationCallbacks = new Map();
|
|
141
|
-
const internalCallback =
|
|
142
|
-
onStateChanged: async (negotiationId, state, data) => {
|
|
143
|
-
for (const [key, cb] of this._negotiationCallbacks.entries()) {
|
|
144
|
-
try {
|
|
145
|
-
await cb.onStateChanged(negotiationId, state, data);
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
await this._loggingComponent?.log({
|
|
149
|
-
level: "error",
|
|
150
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
151
|
-
ts: Date.now(),
|
|
152
|
-
message: "negotiationCallbackError",
|
|
153
|
-
data: { key, negotiationId, method: "onStateChanged", error }
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
onCompleted: async (negotiationId, agreementId) => {
|
|
159
|
-
for (const [key, cb] of this._negotiationCallbacks.entries()) {
|
|
160
|
-
try {
|
|
161
|
-
await cb.onCompleted(negotiationId, agreementId);
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
await this._loggingComponent?.log({
|
|
165
|
-
level: "error",
|
|
166
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
167
|
-
ts: Date.now(),
|
|
168
|
-
message: "negotiationCallbackError",
|
|
169
|
-
data: { key, negotiationId, method: "onCompleted", error }
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
onFailed: async (negotiationId, reason) => {
|
|
175
|
-
for (const [key, cb] of this._negotiationCallbacks.entries()) {
|
|
176
|
-
try {
|
|
177
|
-
await cb.onFailed(negotiationId, reason);
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
await this._loggingComponent?.log({
|
|
181
|
-
level: "error",
|
|
182
|
-
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
183
|
-
ts: Date.now(),
|
|
184
|
-
message: "negotiationCallbackError",
|
|
185
|
-
data: { key, negotiationId, method: "onFailed", error }
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
};
|
|
163
|
+
const internalCallback = this.createInternalCallback();
|
|
191
164
|
this._policyRequester = new DataspaceControlPlanePolicyRequester(options?.loggingComponentType ?? "logging", internalCallback);
|
|
192
165
|
PolicyRequesterFactory.register(DataspaceControlPlaneService._REQUESTER_TYPE, () => this._policyRequester);
|
|
193
166
|
}
|
|
@@ -371,10 +344,12 @@ export class DataspaceControlPlaneService {
|
|
|
371
344
|
}
|
|
372
345
|
const assigneeIdentity = OdrlPolicyHelper.extractAssigneeIdentity(agreement);
|
|
373
346
|
const assigneeIds = ArrayHelper.fromObjectOrArray(assigneeIdentity);
|
|
374
|
-
|
|
347
|
+
// The agreement's assignee is stamped by the provider as the consumer's composite identity (`consumerNodeDid:hash(consumerTenantId)`).
|
|
348
|
+
const callerComposite = this.buildCallerComposite(trustInfo);
|
|
349
|
+
if (!assigneeIds.includes(callerComposite)) {
|
|
375
350
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForAgreement");
|
|
376
351
|
}
|
|
377
|
-
consumerIdentity =
|
|
352
|
+
consumerIdentity = callerComposite;
|
|
378
353
|
const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
|
|
379
354
|
const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
|
|
380
355
|
if (assignerIds.length > 1) {
|
|
@@ -389,6 +364,8 @@ export class DataspaceControlPlaneService {
|
|
|
389
364
|
return transformToTransferError(error, { consumerPid: request.consumerPid, providerPid });
|
|
390
365
|
}
|
|
391
366
|
const now = new Date();
|
|
367
|
+
const requestContextIds = await ContextIdStore.getContextIds();
|
|
368
|
+
const requestTenantId = requestContextIds?.[ContextIdKeys.Tenant];
|
|
392
369
|
const storageEntity = {
|
|
393
370
|
id: Converter.bytesToHex(RandomHelper.generate(32)),
|
|
394
371
|
consumerPid: request.consumerPid,
|
|
@@ -404,6 +381,7 @@ export class DataspaceControlPlaneService {
|
|
|
404
381
|
policies,
|
|
405
382
|
callbackAddress: request.callbackAddress,
|
|
406
383
|
format: request.format,
|
|
384
|
+
tenantId: Is.stringValue(requestTenantId) ? requestTenantId : undefined,
|
|
407
385
|
dataAddress: request.dataAddress,
|
|
408
386
|
dateCreated: now.toISOString(),
|
|
409
387
|
dateModified: now.toISOString()
|
|
@@ -443,6 +421,7 @@ export class DataspaceControlPlaneService {
|
|
|
443
421
|
* Role Performed: Provider / Consumer
|
|
444
422
|
*/
|
|
445
423
|
async startTransfer(message, publicOrigin, trustPayload) {
|
|
424
|
+
publicOrigin = StringHelper.trimTrailingSlashes(publicOrigin);
|
|
446
425
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "startTransfer");
|
|
447
426
|
const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
|
|
448
427
|
if (Is.arrayValue(validationFailures)) {
|
|
@@ -457,7 +436,15 @@ export class DataspaceControlPlaneService {
|
|
|
457
436
|
}
|
|
458
437
|
try {
|
|
459
438
|
const { entity, role } = await this.lookupTransferByMessage(message);
|
|
460
|
-
this.validateCallerIsProvider(trustInfo
|
|
439
|
+
this.validateCallerIsProvider(this.buildCallerComposite(trustInfo), entity);
|
|
440
|
+
// Only the tenant that owns this transfer can mutate it.
|
|
441
|
+
// Skipped on single-tenant nodes (entity.tenantId undefined).
|
|
442
|
+
const callingTenantId = await this.resolveCallingTenantId();
|
|
443
|
+
if (Is.stringValue(entity.tenantId) &&
|
|
444
|
+
Is.stringValue(callingTenantId) &&
|
|
445
|
+
entity.tenantId !== callingTenantId) {
|
|
446
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
|
|
447
|
+
}
|
|
461
448
|
if (entity.state !== DataspaceProtocolTransferProcessStateType.REQUESTED &&
|
|
462
449
|
entity.state !== DataspaceProtocolTransferProcessStateType.SUSPENDED) {
|
|
463
450
|
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "invalidStateForStart", {
|
|
@@ -466,6 +453,7 @@ export class DataspaceControlPlaneService {
|
|
|
466
453
|
currentState: entity.state
|
|
467
454
|
}), message);
|
|
468
455
|
}
|
|
456
|
+
const previousState = entity.state;
|
|
469
457
|
entity.state = DataspaceProtocolTransferProcessStateType.STARTED;
|
|
470
458
|
entity.dateModified = new Date();
|
|
471
459
|
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
@@ -505,7 +493,68 @@ export class DataspaceControlPlaneService {
|
|
|
505
493
|
//
|
|
506
494
|
// See: https://eclipse-dataspace-protocol-base.github.io/DataspaceProtocol/2025-1-err1/#transfer-start-message
|
|
507
495
|
// ============================================================================
|
|
508
|
-
if (Is.empty(entity.dataAddress)) {
|
|
496
|
+
if (Is.empty(entity.dataAddress) && entity.format === DataspaceTransferFormat.HttpProxyPost) {
|
|
497
|
+
// PROVIDER-INITIATED PUSH: Consumer requested push but did not supply an /inbox.
|
|
498
|
+
// Provider returns its own /inbox URL + a signed JWT so the consumer can verify
|
|
499
|
+
// the incoming activities (HttpProxy-POST / DataspaceTransferFormat.HttpProxyPost).
|
|
500
|
+
if (!Is.stringValue(this._dataPlanePath)) {
|
|
501
|
+
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid: entity.consumerPid }), message);
|
|
502
|
+
}
|
|
503
|
+
if (!Is.stringValue(entity.providerIdentity)) {
|
|
504
|
+
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
|
|
505
|
+
}
|
|
506
|
+
// providerIdentity is a composite `nodeDid:hash(tenantId)`
|
|
507
|
+
const pushProviderId = this.parseTenantIdentifier(entity.providerIdentity);
|
|
508
|
+
const accessToken = await this._trustComponent.generate(pushProviderId.nodeDid, this._overrideTrustGeneratorType, {
|
|
509
|
+
subject: {
|
|
510
|
+
consumerPid: entity.consumerPid,
|
|
511
|
+
providerPid: entity.providerPid,
|
|
512
|
+
agreementId: entity.agreementId,
|
|
513
|
+
datasetId: entity.datasetId
|
|
514
|
+
}
|
|
515
|
+
}, pushProviderId.tenantIdHash);
|
|
516
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "accessToken", accessToken);
|
|
517
|
+
const tokenString = accessToken;
|
|
518
|
+
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}/inbox`;
|
|
519
|
+
// Bake the provider's tenant token into the /inbox URL so the consumer's
|
|
520
|
+
// inbound POST routes to the right tenant via TenantProcessor — mirrors the
|
|
521
|
+
// pull-mode endpoint baking below.
|
|
522
|
+
const inboxContextIds1 = await ContextIdStore.getContextIds();
|
|
523
|
+
const inboxTenantId1 = inboxContextIds1?.[ContextIdKeys.Tenant];
|
|
524
|
+
if (Is.stringValue(inboxTenantId1)) {
|
|
525
|
+
fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", inboxTenantId1);
|
|
526
|
+
}
|
|
527
|
+
response.dataAddress = {
|
|
528
|
+
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
529
|
+
endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
|
|
530
|
+
endpoint: fullEndpoint,
|
|
531
|
+
endpointProperties: [
|
|
532
|
+
{
|
|
533
|
+
"@type": DataspaceProtocolTransferProcessTypes.EndpointProperty,
|
|
534
|
+
name: EndpointProperties.Authorization,
|
|
535
|
+
value: tokenString
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"@type": DataspaceProtocolTransferProcessTypes.EndpointProperty,
|
|
539
|
+
name: EndpointProperties.AuthType,
|
|
540
|
+
value: "bearer"
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
};
|
|
544
|
+
await this._loggingComponent?.log({
|
|
545
|
+
level: "info",
|
|
546
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
547
|
+
ts: Date.now(),
|
|
548
|
+
message: "pushTransferStarted",
|
|
549
|
+
data: {
|
|
550
|
+
consumerPid: entity.consumerPid,
|
|
551
|
+
providerPid: entity.providerPid,
|
|
552
|
+
endpoint: fullEndpoint,
|
|
553
|
+
transferMode: DataspaceTransferFormat.HttpProxyPost
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
else if (Is.empty(entity.dataAddress)) {
|
|
509
558
|
if (!Is.stringValue(this._dataPlanePath)) {
|
|
510
559
|
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pullTransfersNotSupported", {
|
|
511
560
|
consumerPid: entity.consumerPid,
|
|
@@ -514,18 +563,22 @@ export class DataspaceControlPlaneService {
|
|
|
514
563
|
}
|
|
515
564
|
// Provider signs the data access token with its own identity.
|
|
516
565
|
// The subject contains the transfer context claims that the data plane
|
|
517
|
-
// will verify when the consumer presents this token.
|
|
566
|
+
// will verify when the consumer presents this token. Parse
|
|
567
|
+
// the composite providerIdentity to extract the signing nodeDid and
|
|
568
|
+
// the tenant-hash to embed as `tid`.
|
|
518
569
|
if (!Is.stringValue(entity.providerIdentity)) {
|
|
519
570
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
|
|
520
571
|
}
|
|
521
|
-
const
|
|
572
|
+
const pullProviderId = this.parseTenantIdentifier(entity.providerIdentity);
|
|
573
|
+
const accessToken = await this._trustComponent.generate(pullProviderId.nodeDid, this._overrideTrustGeneratorType, {
|
|
522
574
|
subject: {
|
|
523
575
|
consumerPid: entity.consumerPid,
|
|
524
576
|
providerPid: entity.providerPid,
|
|
525
577
|
agreementId: entity.agreementId,
|
|
526
578
|
datasetId: entity.datasetId
|
|
527
579
|
}
|
|
528
|
-
});
|
|
580
|
+
}, pullProviderId.tenantIdHash);
|
|
581
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "accessToken", accessToken);
|
|
529
582
|
const tokenString = accessToken;
|
|
530
583
|
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}`;
|
|
531
584
|
const contextIds = await ContextIdStore.getContextIds();
|
|
@@ -540,12 +593,12 @@ export class DataspaceControlPlaneService {
|
|
|
540
593
|
endpointProperties: [
|
|
541
594
|
{
|
|
542
595
|
"@type": DataspaceProtocolTransferProcessTypes.EndpointProperty,
|
|
543
|
-
name:
|
|
596
|
+
name: EndpointProperties.Authorization,
|
|
544
597
|
value: tokenString
|
|
545
598
|
},
|
|
546
599
|
{
|
|
547
600
|
"@type": DataspaceProtocolTransferProcessTypes.EndpointProperty,
|
|
548
|
-
name:
|
|
601
|
+
name: EndpointProperties.AuthType,
|
|
549
602
|
value: "bearer"
|
|
550
603
|
}
|
|
551
604
|
]
|
|
@@ -564,35 +617,59 @@ export class DataspaceControlPlaneService {
|
|
|
564
617
|
});
|
|
565
618
|
}
|
|
566
619
|
else {
|
|
567
|
-
// PUSH MODE:
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
// -
|
|
583
|
-
|
|
620
|
+
// PUSH MODE (consumer-initiated): Consumer provided their /inbox endpoint in
|
|
621
|
+
// the TransferRequestMessage.dataAddress. Provider responds with its own /inbox
|
|
622
|
+
// URL so the consumer knows where to route data notifications.
|
|
623
|
+
if (!Is.stringValue(entity.dataAddress?.endpoint) ||
|
|
624
|
+
!Is.stringValue(entity.dataAddress?.endpointType)) {
|
|
625
|
+
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "invalidPushDataAddress", {
|
|
626
|
+
consumerPid: entity.consumerPid
|
|
627
|
+
}), message);
|
|
628
|
+
}
|
|
629
|
+
if (!Is.stringValue(this._dataPlanePath)) {
|
|
630
|
+
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid: entity.consumerPid }), message);
|
|
631
|
+
}
|
|
632
|
+
let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}/inbox`;
|
|
633
|
+
// Bake the provider's tenant token into the /inbox URL so the consumer's
|
|
634
|
+
// inbound POST routes to the right tenant via TenantProcessor — mirrors the
|
|
635
|
+
// pull-mode endpoint baking.
|
|
636
|
+
const inboxContextIds2 = await ContextIdStore.getContextIds();
|
|
637
|
+
const inboxTenantId2 = inboxContextIds2?.[ContextIdKeys.Tenant];
|
|
638
|
+
if (Is.stringValue(inboxTenantId2)) {
|
|
639
|
+
fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", inboxTenantId2);
|
|
640
|
+
}
|
|
641
|
+
response.dataAddress = {
|
|
642
|
+
"@type": DataspaceProtocolTransferProcessTypes.DataAddress,
|
|
643
|
+
endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
|
|
644
|
+
endpoint: fullEndpoint
|
|
645
|
+
};
|
|
584
646
|
await this._loggingComponent?.log({
|
|
585
|
-
level: "
|
|
647
|
+
level: "info",
|
|
586
648
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
587
649
|
ts: Date.now(),
|
|
588
|
-
message: "
|
|
650
|
+
message: "pushTransferStarted",
|
|
589
651
|
data: {
|
|
590
652
|
consumerPid: entity.consumerPid,
|
|
591
653
|
providerPid: entity.providerPid,
|
|
592
|
-
|
|
593
|
-
transferMode:
|
|
654
|
+
endpoint: fullEndpoint,
|
|
655
|
+
transferMode: DataspaceTransferFormat.HttpProxyPush
|
|
594
656
|
}
|
|
595
657
|
});
|
|
658
|
+
try {
|
|
659
|
+
const dataPlane = this.requireDataPlane();
|
|
660
|
+
if (previousState === DataspaceProtocolTransferProcessStateType.REQUESTED) {
|
|
661
|
+
await dataPlane.setupPushSubscription(entity.consumerPid);
|
|
662
|
+
}
|
|
663
|
+
else if (previousState === DataspaceProtocolTransferProcessStateType.SUSPENDED) {
|
|
664
|
+
await dataPlane.resumePushSubscription(entity.consumerPid);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch (setupError) {
|
|
668
|
+
entity.state = previousState;
|
|
669
|
+
entity.dateModified = new Date();
|
|
670
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
671
|
+
throw setupError;
|
|
672
|
+
}
|
|
596
673
|
}
|
|
597
674
|
return response;
|
|
598
675
|
}
|
|
@@ -624,7 +701,27 @@ export class DataspaceControlPlaneService {
|
|
|
624
701
|
}
|
|
625
702
|
try {
|
|
626
703
|
const { entity, role } = await this.lookupTransferByMessage(message);
|
|
627
|
-
this.validateCallerIsConsumer(trustInfo
|
|
704
|
+
this.validateCallerIsConsumer(this.buildCallerComposite(trustInfo), entity);
|
|
705
|
+
// Only the tenant that owns this transfer can mutate it.
|
|
706
|
+
// Skipped on single-tenant nodes (entity.tenantId undefined).
|
|
707
|
+
const callingTenantId = await this.resolveCallingTenantId();
|
|
708
|
+
if (Is.stringValue(entity.tenantId) &&
|
|
709
|
+
Is.stringValue(callingTenantId) &&
|
|
710
|
+
entity.tenantId !== callingTenantId) {
|
|
711
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
|
|
712
|
+
}
|
|
713
|
+
// DSP idempotency: re-receiving TransferCompletionMessage for a transfer already
|
|
714
|
+
// in COMPLETED should return the same success response, not invalidStateForComplete.
|
|
715
|
+
// Data-plane teardown already ran on the first attempt.
|
|
716
|
+
if (entity.state === DataspaceProtocolTransferProcessStateType.COMPLETED) {
|
|
717
|
+
return {
|
|
718
|
+
"@context": [DataspaceProtocolContexts.Context],
|
|
719
|
+
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
720
|
+
consumerPid: entity.consumerPid,
|
|
721
|
+
providerPid: entity.providerPid,
|
|
722
|
+
state: entity.state
|
|
723
|
+
};
|
|
724
|
+
}
|
|
628
725
|
if (entity.state !== DataspaceProtocolTransferProcessStateType.STARTED) {
|
|
629
726
|
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "invalidStateForComplete", {
|
|
630
727
|
consumerPid: entity.consumerPid,
|
|
@@ -632,6 +729,7 @@ export class DataspaceControlPlaneService {
|
|
|
632
729
|
currentState: entity.state
|
|
633
730
|
}), message);
|
|
634
731
|
}
|
|
732
|
+
const previousState = entity.state;
|
|
635
733
|
entity.state = DataspaceProtocolTransferProcessStateType.COMPLETED;
|
|
636
734
|
entity.dateModified = new Date();
|
|
637
735
|
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
@@ -646,6 +744,20 @@ export class DataspaceControlPlaneService {
|
|
|
646
744
|
role
|
|
647
745
|
}
|
|
648
746
|
});
|
|
747
|
+
if (this.isPushFormat(entity.format)) {
|
|
748
|
+
try {
|
|
749
|
+
await this.requireDataPlane().teardownPushSubscription(entity.consumerPid);
|
|
750
|
+
}
|
|
751
|
+
catch (teardownError) {
|
|
752
|
+
// Symmetric rollback: data-plane teardown failed, revert the transfer state
|
|
753
|
+
// so the client can retry. Without this the storage row is COMPLETED but
|
|
754
|
+
// the subscription is still flowing, and a retry hits invalidStateForComplete.
|
|
755
|
+
entity.state = previousState;
|
|
756
|
+
entity.dateModified = new Date();
|
|
757
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
758
|
+
throw teardownError;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
649
761
|
return {
|
|
650
762
|
"@context": [DataspaceProtocolContexts.Context],
|
|
651
763
|
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
@@ -679,7 +791,26 @@ export class DataspaceControlPlaneService {
|
|
|
679
791
|
}
|
|
680
792
|
try {
|
|
681
793
|
const { entity, role } = await this.lookupTransferByMessage(message);
|
|
682
|
-
this.validateCallerIsTransferParty(trustInfo
|
|
794
|
+
this.validateCallerIsTransferParty(this.buildCallerComposite(trustInfo), entity);
|
|
795
|
+
// S2 belt-and-braces: only the tenant that owns this transfer can mutate it.
|
|
796
|
+
// Skipped on single-tenant nodes (entity.tenantId undefined).
|
|
797
|
+
const callingTenantId = await this.resolveCallingTenantId();
|
|
798
|
+
if (Is.stringValue(entity.tenantId) &&
|
|
799
|
+
Is.stringValue(callingTenantId) &&
|
|
800
|
+
entity.tenantId !== callingTenantId) {
|
|
801
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
|
|
802
|
+
}
|
|
803
|
+
// DSP idempotency: re-receiving TransferSuspensionMessage for a transfer already
|
|
804
|
+
// in SUSPENDED should return success. Data-plane suspend already ran.
|
|
805
|
+
if (entity.state === DataspaceProtocolTransferProcessStateType.SUSPENDED) {
|
|
806
|
+
return {
|
|
807
|
+
"@context": [DataspaceProtocolContexts.Context],
|
|
808
|
+
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
809
|
+
consumerPid: entity.consumerPid,
|
|
810
|
+
providerPid: entity.providerPid,
|
|
811
|
+
state: entity.state
|
|
812
|
+
};
|
|
813
|
+
}
|
|
683
814
|
if (entity.state !== DataspaceProtocolTransferProcessStateType.STARTED) {
|
|
684
815
|
return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "invalidStateForSuspend", {
|
|
685
816
|
consumerPid: entity.consumerPid,
|
|
@@ -687,6 +818,7 @@ export class DataspaceControlPlaneService {
|
|
|
687
818
|
currentState: entity.state
|
|
688
819
|
}), message);
|
|
689
820
|
}
|
|
821
|
+
const previousState = entity.state;
|
|
690
822
|
entity.state = DataspaceProtocolTransferProcessStateType.SUSPENDED;
|
|
691
823
|
entity.dateModified = new Date();
|
|
692
824
|
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
@@ -702,6 +834,19 @@ export class DataspaceControlPlaneService {
|
|
|
702
834
|
reason: message.reason
|
|
703
835
|
}
|
|
704
836
|
});
|
|
837
|
+
if (this.isPushFormat(entity.format)) {
|
|
838
|
+
try {
|
|
839
|
+
await this.requireDataPlane().suspendPushSubscription(entity.consumerPid);
|
|
840
|
+
}
|
|
841
|
+
catch (suspendError) {
|
|
842
|
+
// Symmetric rollback: data-plane suspend failed, revert transfer state so
|
|
843
|
+
// a retry can repair the subscription instead of failing invalidStateForSuspend.
|
|
844
|
+
entity.state = previousState;
|
|
845
|
+
entity.dateModified = new Date();
|
|
846
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
847
|
+
throw suspendError;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
705
850
|
return {
|
|
706
851
|
"@context": [DataspaceProtocolContexts.Context],
|
|
707
852
|
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
@@ -735,7 +880,27 @@ export class DataspaceControlPlaneService {
|
|
|
735
880
|
}
|
|
736
881
|
try {
|
|
737
882
|
const { entity, role } = await this.lookupTransferByMessage(message);
|
|
738
|
-
this.validateCallerIsTransferParty(trustInfo
|
|
883
|
+
this.validateCallerIsTransferParty(this.buildCallerComposite(trustInfo), entity);
|
|
884
|
+
// Only the tenant that owns this transfer can mutate it.
|
|
885
|
+
// Skipped on single-tenant nodes (entity.tenantId undefined).
|
|
886
|
+
const callingTenantId = await this.resolveCallingTenantId();
|
|
887
|
+
if (Is.stringValue(entity.tenantId) &&
|
|
888
|
+
Is.stringValue(callingTenantId) &&
|
|
889
|
+
entity.tenantId !== callingTenantId) {
|
|
890
|
+
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
|
|
891
|
+
}
|
|
892
|
+
// DSP idempotency: re-receiving TransferTerminationMessage for a transfer already
|
|
893
|
+
// in TERMINATED should return success. Data-plane teardown already ran.
|
|
894
|
+
if (entity.state === DataspaceProtocolTransferProcessStateType.TERMINATED) {
|
|
895
|
+
return {
|
|
896
|
+
"@context": [DataspaceProtocolContexts.Context],
|
|
897
|
+
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
898
|
+
consumerPid: entity.consumerPid,
|
|
899
|
+
providerPid: entity.providerPid,
|
|
900
|
+
state: entity.state
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const previousState = entity.state;
|
|
739
904
|
entity.state = DataspaceProtocolTransferProcessStateType.TERMINATED;
|
|
740
905
|
entity.dateModified = new Date();
|
|
741
906
|
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
@@ -751,6 +916,20 @@ export class DataspaceControlPlaneService {
|
|
|
751
916
|
reason: message.reason
|
|
752
917
|
}
|
|
753
918
|
});
|
|
919
|
+
if (this.isPushFormat(entity.format)) {
|
|
920
|
+
try {
|
|
921
|
+
await this.requireDataPlane().teardownPushSubscription(entity.consumerPid);
|
|
922
|
+
}
|
|
923
|
+
catch (teardownError) {
|
|
924
|
+
// Symmetric rollback: data-plane teardown failed, revert transfer state so
|
|
925
|
+
// a retry can repair the subscription. Terminate is reachable from multiple
|
|
926
|
+
// states (REQUESTED/STARTED/SUSPENDED), so restore the actual previous one.
|
|
927
|
+
entity.state = previousState;
|
|
928
|
+
entity.dateModified = new Date();
|
|
929
|
+
await this._transferProcessStorage.set(this.modelToStorageEntity(entity));
|
|
930
|
+
throw teardownError;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
754
933
|
return {
|
|
755
934
|
"@context": [DataspaceProtocolContexts.Context],
|
|
756
935
|
"@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
|
|
@@ -773,7 +952,7 @@ export class DataspaceControlPlaneService {
|
|
|
773
952
|
const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "getTransferProcess");
|
|
774
953
|
try {
|
|
775
954
|
const { entity, role } = await this.lookupTransferByPid(pid);
|
|
776
|
-
this.validateCallerIsTransferParty(trustInfo
|
|
955
|
+
this.validateCallerIsTransferParty(this.buildCallerComposite(trustInfo), entity);
|
|
777
956
|
await this._loggingComponent?.log({
|
|
778
957
|
level: "info",
|
|
779
958
|
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
@@ -1003,19 +1182,22 @@ export class DataspaceControlPlaneService {
|
|
|
1003
1182
|
}
|
|
1004
1183
|
const agreement = await this.lookupAgreement(entity.agreementId);
|
|
1005
1184
|
const contextIds = await ContextIdStore.getContextIds();
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1185
|
+
const currentComposite = this.buildTenantIdentifier(contextIds?.[ContextIdKeys.Node], contextIds?.[ContextIdKeys.Tenant]);
|
|
1186
|
+
// Identity is `nodeDid:hash(tenantId)` (or just `nodeDid` in
|
|
1187
|
+
// single-tenant). Without a node DID in context we cannot derive any
|
|
1188
|
+
// caller identity at all.
|
|
1189
|
+
if (!Is.stringValue(currentComposite)) {
|
|
1008
1190
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "organizationContextMissing", {
|
|
1009
1191
|
consumerPid
|
|
1010
1192
|
});
|
|
1011
1193
|
}
|
|
1012
1194
|
const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
|
|
1013
1195
|
const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
|
|
1014
|
-
if (!assignerIds.includes(
|
|
1196
|
+
if (!assignerIds.includes(currentComposite)) {
|
|
1015
1197
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "agreementAssignerMismatch", {
|
|
1016
1198
|
consumerPid,
|
|
1017
1199
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1018
|
-
|
|
1200
|
+
expectedComposite: currentComposite,
|
|
1019
1201
|
actualAssigner: assignerIds.join(", ")
|
|
1020
1202
|
});
|
|
1021
1203
|
}
|
|
@@ -1030,7 +1212,7 @@ export class DataspaceControlPlaneService {
|
|
|
1030
1212
|
state: entity.state,
|
|
1031
1213
|
consumerIdentity: entity.consumerIdentity,
|
|
1032
1214
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1033
|
-
organizationId:
|
|
1215
|
+
organizationId: contextIds?.[ContextIdKeys.Organization]
|
|
1034
1216
|
}
|
|
1035
1217
|
});
|
|
1036
1218
|
return {
|
|
@@ -1062,20 +1244,19 @@ export class DataspaceControlPlaneService {
|
|
|
1062
1244
|
}
|
|
1063
1245
|
const agreement = await this.lookupAgreement(entity.agreementId);
|
|
1064
1246
|
const contextIds = await ContextIdStore.getContextIds();
|
|
1065
|
-
const
|
|
1066
|
-
if (!
|
|
1247
|
+
const currentComposite = this.buildTenantIdentifier(contextIds?.[ContextIdKeys.Node], contextIds?.[ContextIdKeys.Tenant]);
|
|
1248
|
+
if (!Is.stringValue(currentComposite)) {
|
|
1067
1249
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "organizationContextMissingProvider", {
|
|
1068
1250
|
providerPid
|
|
1069
1251
|
});
|
|
1070
1252
|
}
|
|
1071
|
-
// Extract assigner UID (can be string, array, or IOdrlParty object)
|
|
1072
1253
|
const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
|
|
1073
1254
|
const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
|
|
1074
|
-
if (!assignerIds.includes(
|
|
1255
|
+
if (!assignerIds.includes(currentComposite)) {
|
|
1075
1256
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "agreementAssignerMismatchProvider", {
|
|
1076
1257
|
providerPid,
|
|
1077
1258
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1078
|
-
|
|
1259
|
+
expectedComposite: currentComposite,
|
|
1079
1260
|
actualAssigner: assignerIds.join(", ")
|
|
1080
1261
|
});
|
|
1081
1262
|
}
|
|
@@ -1105,7 +1286,7 @@ export class DataspaceControlPlaneService {
|
|
|
1105
1286
|
consumerIdentity: entity.consumerIdentity,
|
|
1106
1287
|
providerIdentity: entity.providerIdentity,
|
|
1107
1288
|
agreementId: OdrlPolicyHelper.getUid(agreement),
|
|
1108
|
-
organizationId:
|
|
1289
|
+
organizationId: contextIds?.[ContextIdKeys.Organization]
|
|
1109
1290
|
}
|
|
1110
1291
|
});
|
|
1111
1292
|
return {
|
|
@@ -1147,7 +1328,7 @@ export class DataspaceControlPlaneService {
|
|
|
1147
1328
|
nodeIdentity,
|
|
1148
1329
|
tenantId,
|
|
1149
1330
|
appId,
|
|
1150
|
-
dataset:
|
|
1331
|
+
dataset: ObjectHelper.omit(dataset, ["@id"]),
|
|
1151
1332
|
dateCreated: now,
|
|
1152
1333
|
dateModified: now
|
|
1153
1334
|
};
|
|
@@ -1195,11 +1376,11 @@ export class DataspaceControlPlaneService {
|
|
|
1195
1376
|
}
|
|
1196
1377
|
: undefined, undefined, undefined, cursor, limit);
|
|
1197
1378
|
const entities = (page.entities ?? []).map(entity => ({
|
|
1198
|
-
id: entity.id
|
|
1199
|
-
appId: entity.appId
|
|
1379
|
+
id: entity.id,
|
|
1380
|
+
appId: entity.appId,
|
|
1200
1381
|
dataset: this.restampDatasetId(entity.dataset ?? {}, entity.id ?? ""),
|
|
1201
|
-
dateCreated: entity.dateCreated
|
|
1202
|
-
dateModified: entity.dateModified
|
|
1382
|
+
dateCreated: entity.dateCreated,
|
|
1383
|
+
dateModified: entity.dateModified
|
|
1203
1384
|
}));
|
|
1204
1385
|
return {
|
|
1205
1386
|
entities,
|
|
@@ -1227,7 +1408,7 @@ export class DataspaceControlPlaneService {
|
|
|
1227
1408
|
const updated = {
|
|
1228
1409
|
...existing,
|
|
1229
1410
|
appId,
|
|
1230
|
-
dataset:
|
|
1411
|
+
dataset: ObjectHelper.omit(dataset, ["@id"]),
|
|
1231
1412
|
dateModified: new Date().toISOString()
|
|
1232
1413
|
};
|
|
1233
1414
|
// Side effect first, primary storage last
|
|
@@ -1327,6 +1508,7 @@ export class DataspaceControlPlaneService {
|
|
|
1327
1508
|
providerIdentity: storageEntity.providerIdentity,
|
|
1328
1509
|
format: storageEntity.format,
|
|
1329
1510
|
callbackAddress: storageEntity.callbackAddress,
|
|
1511
|
+
tenantId: storageEntity.tenantId,
|
|
1330
1512
|
dateCreated: new Date(storageEntity.dateCreated),
|
|
1331
1513
|
dateModified: new Date(storageEntity.dateModified),
|
|
1332
1514
|
policies: storageEntity.policies,
|
|
@@ -1352,6 +1534,7 @@ export class DataspaceControlPlaneService {
|
|
|
1352
1534
|
providerIdentity: entity.providerIdentity,
|
|
1353
1535
|
format: entity.format,
|
|
1354
1536
|
callbackAddress: entity.callbackAddress,
|
|
1537
|
+
tenantId: entity.tenantId,
|
|
1355
1538
|
dateCreated: entity.dateCreated.toISOString(),
|
|
1356
1539
|
dateModified: entity.dateModified.toISOString(),
|
|
1357
1540
|
policies: entity.policies,
|
|
@@ -1378,39 +1561,54 @@ export class DataspaceControlPlaneService {
|
|
|
1378
1561
|
}
|
|
1379
1562
|
return agreement;
|
|
1380
1563
|
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Build the caller's composite identity (`nodeDid:tenantIdHash`) from a
|
|
1566
|
+
* verified trust payload.
|
|
1567
|
+
* @param trustInfo The verification info from `TrustHelper.verifyTrust`.
|
|
1568
|
+
* @returns The composite identity.
|
|
1569
|
+
* @internal
|
|
1570
|
+
*/
|
|
1571
|
+
buildCallerComposite(trustInfo) {
|
|
1572
|
+
Guards.object(DataspaceControlPlaneService.CLASS_NAME, "trustInfo", trustInfo);
|
|
1573
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "trustInfo.identity", trustInfo.identity);
|
|
1574
|
+
return Is.stringValue(trustInfo.tenantId)
|
|
1575
|
+
? `${trustInfo.identity}:${trustInfo.tenantId}`
|
|
1576
|
+
: trustInfo.identity;
|
|
1577
|
+
}
|
|
1381
1578
|
/**
|
|
1382
1579
|
* Validate that the caller's verified identity matches the consumer of a transfer process.
|
|
1383
|
-
* @param
|
|
1580
|
+
* @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
|
|
1384
1581
|
* @param entity The transfer process entity.
|
|
1385
1582
|
* @throws UnauthorizedError if the caller is not the consumer.
|
|
1386
1583
|
* @internal
|
|
1387
1584
|
*/
|
|
1388
|
-
validateCallerIsConsumer(
|
|
1389
|
-
if (
|
|
1585
|
+
validateCallerIsConsumer(callerComposite, entity) {
|
|
1586
|
+
if (callerComposite !== entity.consumerIdentity) {
|
|
1390
1587
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsConsumer");
|
|
1391
1588
|
}
|
|
1392
1589
|
}
|
|
1393
1590
|
/**
|
|
1394
1591
|
* Validate that the caller's verified identity matches the provider of a transfer process.
|
|
1395
|
-
* @param
|
|
1592
|
+
* @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
|
|
1396
1593
|
* @param entity The transfer process entity.
|
|
1397
1594
|
* @throws UnauthorizedError if the caller is not the provider.
|
|
1398
1595
|
* @internal
|
|
1399
1596
|
*/
|
|
1400
|
-
validateCallerIsProvider(
|
|
1401
|
-
if (
|
|
1597
|
+
validateCallerIsProvider(callerComposite, entity) {
|
|
1598
|
+
if (callerComposite !== entity.providerIdentity) {
|
|
1402
1599
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsProvider");
|
|
1403
1600
|
}
|
|
1404
1601
|
}
|
|
1405
1602
|
/**
|
|
1406
1603
|
* Validate that the caller's verified identity matches either the consumer or provider of a transfer process.
|
|
1407
|
-
* @param
|
|
1604
|
+
* @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
|
|
1408
1605
|
* @param entity The transfer process entity.
|
|
1409
1606
|
* @throws UnauthorizedError if the caller is not a party to the transfer.
|
|
1410
1607
|
* @internal
|
|
1411
1608
|
*/
|
|
1412
|
-
validateCallerIsTransferParty(
|
|
1413
|
-
if (
|
|
1609
|
+
validateCallerIsTransferParty(callerComposite, entity) {
|
|
1610
|
+
if (callerComposite !== entity.consumerIdentity &&
|
|
1611
|
+
callerComposite !== entity.providerIdentity) {
|
|
1414
1612
|
throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
|
|
1415
1613
|
}
|
|
1416
1614
|
}
|
|
@@ -1420,6 +1618,16 @@ export class DataspaceControlPlaneService {
|
|
|
1420
1618
|
* @returns Dataset ID.
|
|
1421
1619
|
* @internal
|
|
1422
1620
|
*/
|
|
1621
|
+
isPushFormat(format) {
|
|
1622
|
+
return (format === DataspaceTransferFormat.HttpProxyPush ||
|
|
1623
|
+
format === DataspaceTransferFormat.HttpProxyPost);
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Extract the dataset ID from an ODRL agreement's target.
|
|
1627
|
+
* @param agreement The ODRL agreement containing the target.
|
|
1628
|
+
* @returns The dataset ID extracted from the target URN.
|
|
1629
|
+
* @throws GeneralError if the agreement target is missing, has no UID, or has multiple targets.
|
|
1630
|
+
*/
|
|
1423
1631
|
extractDatasetId(agreement) {
|
|
1424
1632
|
if (Is.empty(agreement.target)) {
|
|
1425
1633
|
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "agreementMissingTarget", {
|
|
@@ -1656,22 +1864,124 @@ export class DataspaceControlPlaneService {
|
|
|
1656
1864
|
return dataset;
|
|
1657
1865
|
}
|
|
1658
1866
|
const contextIds = await ContextIdStore.getContextIds();
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1867
|
+
// The publisher attribution is always the composite identifier
|
|
1868
|
+
const compositePublisher = this.buildTenantIdentifier(contextIds?.[ContextIdKeys.Node], contextIds?.[ContextIdKeys.Tenant]);
|
|
1869
|
+
if (compositePublisher) {
|
|
1870
|
+
return { ...dataset, "dcterms:publisher": compositePublisher };
|
|
1662
1871
|
}
|
|
1663
1872
|
return dataset;
|
|
1664
1873
|
}
|
|
1665
1874
|
/**
|
|
1666
|
-
*
|
|
1667
|
-
* @param
|
|
1668
|
-
* @
|
|
1875
|
+
* Build the tenant-attributed identifier.
|
|
1876
|
+
* @param nodeId The node identity (from `ContextIdKeys.Node`).
|
|
1877
|
+
* @param tenantId The plaintext tenant id (from `ContextIdKeys.Tenant`), if
|
|
1878
|
+
* any. Hashed before insertion into the composite.
|
|
1879
|
+
* @returns Composite identifier, or undefined if nodeId is missing.
|
|
1880
|
+
* @internal
|
|
1881
|
+
*/
|
|
1882
|
+
buildTenantIdentifier(nodeId, tenantId) {
|
|
1883
|
+
if (!Is.stringValue(nodeId)) {
|
|
1884
|
+
return undefined;
|
|
1885
|
+
}
|
|
1886
|
+
return Is.stringValue(tenantId) ? `${nodeId}:${TrustHelper.hashTenantId(tenantId)}` : nodeId;
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Parse a composite tenant identifier into its node and tenant-hash portions.
|
|
1890
|
+
* @param composite The composite identifier (`nodeDid` or `nodeDid:hash`).
|
|
1891
|
+
* @returns The parsed parts. `tenantIdHash` is undefined when the composite
|
|
1892
|
+
* has no tenant portion (single-tenant node form).
|
|
1893
|
+
* @throws GuardError if the input is not a non-empty string.
|
|
1894
|
+
* @throws GeneralError if the input does not start with `did:` — the only
|
|
1895
|
+
* valid composite identifier shapes carry a DID as the leading portion.
|
|
1669
1896
|
* @internal
|
|
1670
1897
|
*/
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1898
|
+
parseTenantIdentifier(composite) {
|
|
1899
|
+
Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "composite", composite);
|
|
1900
|
+
if (!composite.startsWith("did:")) {
|
|
1901
|
+
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "invalidCompositeIdentifier", { composite });
|
|
1902
|
+
}
|
|
1903
|
+
const lastColon = composite.lastIndexOf(":");
|
|
1904
|
+
if (lastColon === -1) {
|
|
1905
|
+
return { nodeDid: composite };
|
|
1906
|
+
}
|
|
1907
|
+
const candidateHash = composite.slice(lastColon + 1);
|
|
1908
|
+
if (DataspaceControlPlaneService._TENANT_HASH_PATTERN.test(candidateHash)) {
|
|
1909
|
+
return {
|
|
1910
|
+
nodeDid: composite.slice(0, lastColon),
|
|
1911
|
+
tenantIdHash: candidateHash
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
return { nodeDid: composite };
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Resolve the data plane component or throw if it isn't registered. Push-mode transfers
|
|
1918
|
+
* require the data plane; pull-only deployments may run without it.
|
|
1919
|
+
* @returns The data plane component.
|
|
1920
|
+
* @throws GeneralError if the data plane component is not registered.
|
|
1921
|
+
* @internal
|
|
1922
|
+
*/
|
|
1923
|
+
requireDataPlane() {
|
|
1924
|
+
const dataPlane = ComponentFactory.getIfExists(this._dataPlaneComponentType);
|
|
1925
|
+
if (!dataPlane) {
|
|
1926
|
+
throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "dataPlaneNotRegistered");
|
|
1927
|
+
}
|
|
1928
|
+
return dataPlane;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Creates the internal INegotiationCallback that fans out to all registered callbacks.
|
|
1932
|
+
* @returns The internal negotiation callback.
|
|
1933
|
+
*/
|
|
1934
|
+
createInternalCallback() {
|
|
1935
|
+
return {
|
|
1936
|
+
onStateChanged: async (negotiationId, state, data) => {
|
|
1937
|
+
for (const [key, cb] of this._negotiationCallbacks.entries()) {
|
|
1938
|
+
try {
|
|
1939
|
+
await cb.onStateChanged(negotiationId, state, data);
|
|
1940
|
+
}
|
|
1941
|
+
catch (error) {
|
|
1942
|
+
await this._loggingComponent?.log({
|
|
1943
|
+
level: "error",
|
|
1944
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
1945
|
+
ts: Date.now(),
|
|
1946
|
+
message: "negotiationCallbackError",
|
|
1947
|
+
data: { key, negotiationId, method: "onStateChanged", error }
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
1952
|
+
onCompleted: async (negotiationId, agreementId) => {
|
|
1953
|
+
for (const [key, cb] of this._negotiationCallbacks.entries()) {
|
|
1954
|
+
try {
|
|
1955
|
+
await cb.onCompleted(negotiationId, agreementId);
|
|
1956
|
+
}
|
|
1957
|
+
catch (error) {
|
|
1958
|
+
await this._loggingComponent?.log({
|
|
1959
|
+
level: "error",
|
|
1960
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
1961
|
+
ts: Date.now(),
|
|
1962
|
+
message: "negotiationCallbackError",
|
|
1963
|
+
data: { key, negotiationId, method: "onCompleted", error }
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
},
|
|
1968
|
+
onFailed: async (negotiationId, reason) => {
|
|
1969
|
+
for (const [key, cb] of this._negotiationCallbacks.entries()) {
|
|
1970
|
+
try {
|
|
1971
|
+
await cb.onFailed(negotiationId, reason);
|
|
1972
|
+
}
|
|
1973
|
+
catch (error) {
|
|
1974
|
+
await this._loggingComponent?.log({
|
|
1975
|
+
level: "error",
|
|
1976
|
+
source: DataspaceControlPlaneService.CLASS_NAME,
|
|
1977
|
+
ts: Date.now(),
|
|
1978
|
+
message: "negotiationCallbackError",
|
|
1979
|
+
data: { key, negotiationId, method: "onFailed", error }
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1675
1985
|
}
|
|
1676
1986
|
/**
|
|
1677
1987
|
* Re-stamp `@id` onto a stored payload blob using the entity primary key.
|
|
@@ -1693,8 +2003,9 @@ export class DataspaceControlPlaneService {
|
|
|
1693
2003
|
// Override `Tenant` in the context wrap so federated catalogue captures the
|
|
1694
2004
|
// appDataset's owning tenant. On single-tenant nodes the appDataset has no tenantId,
|
|
1695
2005
|
// so the assignment writes `Tenant: undefined` — equivalent to no override.
|
|
2006
|
+
const contextIds = (await ContextIdStore.getContextIds()) ?? {};
|
|
1696
2007
|
const wrappedContextIds = {
|
|
1697
|
-
...
|
|
2008
|
+
...contextIds,
|
|
1698
2009
|
[ContextIdKeys.Tenant]: appDataset.tenantId
|
|
1699
2010
|
};
|
|
1700
2011
|
await ContextIdStore.run(wrappedContextIds, async () => {
|