@twin.org/dataspace-control-plane-service 0.0.3-next.42 → 0.0.3-next.44

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.
@@ -1,4 +1,7 @@
1
- import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
1
+ // Copyright 2025 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { HttpUrlHelper } from "@twin.org/api-models";
4
+ import { ContextIdKeys, ContextIdStore, ContextIdHelper } from "@twin.org/context";
2
5
  import { ArrayHelper, AlreadyExistsError, BaseError, ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, ObjectHelper, RandomHelper, StringHelper, UnauthorizedError, Url, Urn, ValidationError } from "@twin.org/core";
3
6
  import { JsonLdHelper } from "@twin.org/data-json-ld";
4
7
  import { DataspaceAppFactory, DataspaceTransferFormat, TransferProcessRole, getJsonLdId, getJsonLdType } from "@twin.org/dataspace-models";
@@ -35,14 +38,6 @@ export class DataspaceControlPlaneService {
35
38
  * @internal
36
39
  */
37
40
  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}$/;
46
41
  /**
47
42
  * The logging component.
48
43
  * @internal
@@ -77,7 +72,7 @@ export class DataspaceControlPlaneService {
77
72
  */
78
73
  _transferProcessStorage;
79
74
  /**
80
- * Entity storage for tenant-supplied Dataspace App Dataset entities.
75
+ * Entity storage for Dataspace App Dataset entities.
81
76
  * @internal
82
77
  */
83
78
  _dataspaceAppDatasetStorage;
@@ -114,6 +109,11 @@ export class DataspaceControlPlaneService {
114
109
  * @internal
115
110
  */
116
111
  _taskScheduler;
112
+ /**
113
+ * Platform component.
114
+ * @internal
115
+ */
116
+ _platformComponent;
117
117
  /**
118
118
  * Registered negotiation callbacks from upstream callers, keyed by registration key.
119
119
  * @internal
@@ -135,17 +135,12 @@ export class DataspaceControlPlaneService {
135
135
  * @internal
136
136
  */
137
137
  _remoteControlPlaneComponentType;
138
- /**
139
- * The component type name for the hosting component.
140
- * @internal
141
- */
142
- _urlTransformerComponent;
143
138
  /**
144
139
  * Create a new instance of DataspaceControlPlaneService.
145
140
  * @param options The options for the service.
146
141
  */
147
142
  constructor(options) {
148
- this._loggingComponent = ComponentFactory.getIfExists(options?.loggingComponentType ?? "logging");
143
+ this._loggingComponent = ComponentFactory.getIfExists(options?.loggingComponentType);
149
144
  // Retrieve PAP component with default
150
145
  this._policyAdministrationPointComponent =
151
146
  ComponentFactory.get(options?.policyAdministrationPointComponentType ?? "policy-administration-point");
@@ -164,6 +159,7 @@ export class DataspaceControlPlaneService {
164
159
  ? StringHelper.trimTrailingSlashes(StringHelper.trimLeadingSlashes(options.config.dataPlanePath))
165
160
  : undefined;
166
161
  this._taskScheduler = ComponentFactory.getIfExists(options?.taskSchedulerComponentType ?? "task-scheduler");
162
+ this._platformComponent = ComponentFactory.getIfExists(options?.platformComponentType ?? "platform");
167
163
  // Data plane component is optional and resolved lazily. The control plane is initialised
168
164
  // BEFORE the data plane in engine startup order, so `getRegisteredInstanceTypeOptional`
169
165
  // returns undefined when the engine constructs us — the wiring override can't fire here.
@@ -172,14 +168,13 @@ export class DataspaceControlPlaneService {
172
168
  // lookup at push time finds it regardless of init order.
173
169
  this._dataPlaneComponentType =
174
170
  options?.dataPlaneComponentType ?? "dataspace-data-plane-service";
175
- this._urlTransformerComponent = ComponentFactory.get(options?.urlTransformerComponentType ?? "url-transformer");
176
171
  this._negotiationCallbacks = new Map();
177
172
  this._transferCallbacks = new Map();
178
173
  this._internalTransferCallback = this.createInternalTransferCallback();
179
174
  this._remoteControlPlaneComponentType =
180
175
  options?.remoteControlPlaneComponentType ?? "dataspace-control-plane-rest-client";
181
176
  const internalCallback = this.createInternalCallback();
182
- this._policyRequester = new DataspaceControlPlanePolicyRequester(options?.loggingComponentType ?? "logging", internalCallback);
177
+ this._policyRequester = new DataspaceControlPlanePolicyRequester(options?.loggingComponentType, internalCallback);
183
178
  PolicyRequesterFactory.register(DataspaceControlPlaneService._REQUESTER_TYPE, () => this._policyRequester);
184
179
  }
185
180
  /**
@@ -227,10 +222,6 @@ export class DataspaceControlPlaneService {
227
222
  }
228
223
  /**
229
224
  * The service needs to be started when the application is initialized.
230
- * Populates the Federated Catalogue with datasets from registered apps
231
- * and starts the stalled negotiation cleanup task. Also captures the node
232
- * identity from ContextIdStore when tenant-token encryption is configured
233
- * (required to derive the vault key name `${nodeId}/${signingKeyName}`).
234
225
  * @param nodeLoggingComponentType The node logging component type.
235
226
  */
236
227
  async start(nodeLoggingComponentType) {
@@ -254,49 +245,52 @@ export class DataspaceControlPlaneService {
254
245
  let registeredCount = 0;
255
246
  let errorCount = 0;
256
247
  let totalDatasets = 0;
257
- // Walk stored dataspace app datasets one page at a time and publish each in its
258
- // owning tenant's context. Memory stays bounded to one page.
259
- let cursor;
260
- do {
261
- const page = await this._dataspaceAppDatasetStorage.query(undefined, undefined, undefined, cursor);
262
- if (Is.arrayValue(page.entities)) {
263
- for (const entity of page.entities) {
264
- const appDataset = entity;
265
- totalDatasets++;
266
- try {
267
- await this.publishAppDataset(appDataset);
268
- registeredCount++;
269
- await this._loggingComponent?.log({
270
- level: "debug",
271
- ts: Date.now(),
272
- source: DataspaceControlPlaneService.CLASS_NAME,
273
- message: "datasetRegistered",
274
- data: {
275
- datasetId: appDataset.id,
276
- appId: appDataset.appId,
277
- tenantId: appDataset.tenantId
278
- }
279
- });
280
- }
281
- catch (error) {
282
- errorCount++;
283
- await this._loggingComponent?.log({
284
- level: "error",
285
- ts: Date.now(),
286
- source: DataspaceControlPlaneService.CLASS_NAME,
287
- message: "datasetPublishFailed",
288
- error: BaseError.fromError(error),
289
- data: {
290
- datasetId: appDataset.id,
291
- appId: appDataset.appId,
292
- tenantId: appDataset.tenantId
293
- }
294
- });
248
+ // The platform component execute is used so that the dataset publication runs in the
249
+ // tenant context of the hosting component, also works in single tenant mode
250
+ await this._platformComponent?.execute(async () => {
251
+ // The tenant context id is set here for each system tenant
252
+ let cursor;
253
+ do {
254
+ const page = await this._dataspaceAppDatasetStorage.query(undefined, undefined, undefined, cursor);
255
+ if (Is.arrayValue(page.entities)) {
256
+ for (const entity of page.entities) {
257
+ const appDataset = entity;
258
+ totalDatasets++;
259
+ try {
260
+ await this.publishAppDataset(appDataset);
261
+ registeredCount++;
262
+ await this._loggingComponent?.log({
263
+ level: "debug",
264
+ ts: Date.now(),
265
+ source: DataspaceControlPlaneService.CLASS_NAME,
266
+ message: "datasetRegistered",
267
+ data: {
268
+ datasetId: appDataset.id,
269
+ appId: appDataset.appId,
270
+ tenantId: appDataset.tenantId
271
+ }
272
+ });
273
+ }
274
+ catch (error) {
275
+ errorCount++;
276
+ await this._loggingComponent?.log({
277
+ level: "error",
278
+ ts: Date.now(),
279
+ source: DataspaceControlPlaneService.CLASS_NAME,
280
+ message: "datasetPublishFailed",
281
+ error: BaseError.fromError(error),
282
+ data: {
283
+ datasetId: appDataset.id,
284
+ appId: appDataset.appId,
285
+ tenantId: appDataset.tenantId
286
+ }
287
+ });
288
+ }
295
289
  }
296
290
  }
297
- }
298
- cursor = page.cursor;
299
- } while (Is.stringValue(cursor));
291
+ cursor = page.cursor;
292
+ } while (Is.stringValue(cursor));
293
+ });
300
294
  await this._loggingComponent?.log({
301
295
  level: "info",
302
296
  ts: Date.now(),
@@ -380,12 +374,11 @@ export class DataspaceControlPlaneService {
380
374
  }
381
375
  const assigneeIdentity = OdrlPolicyHelper.extractAssigneeIdentity(agreement);
382
376
  const assigneeIds = ArrayHelper.fromObjectOrArray(assigneeIdentity);
383
- // The agreement's assignee is stamped by the provider as the consumer's composite identity (`consumerNodeDid:hash(consumerTenantId)`).
384
- const callerComposite = this.buildCallerComposite(trustInfo);
385
- if (!assigneeIds.includes(callerComposite)) {
377
+ // The agreement's assignee is stamped by the provider as the consumer's organization identity.
378
+ if (!assigneeIds.includes(trustInfo.identity)) {
386
379
  throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForAgreement");
387
380
  }
388
- consumerIdentity = callerComposite;
381
+ consumerIdentity = trustInfo.identity;
389
382
  const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
390
383
  const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
391
384
  if (assignerIds.length > 1) {
@@ -400,8 +393,7 @@ export class DataspaceControlPlaneService {
400
393
  return transformToTransferError(error, { consumerPid: request.consumerPid, providerPid });
401
394
  }
402
395
  const now = new Date();
403
- const requestContextIds = await ContextIdStore.getContextIds();
404
- const requestTenantId = requestContextIds?.[ContextIdKeys.Tenant];
396
+ const organizationIdentity = await this.resolveContextOrganizationId();
405
397
  const storageEntity = {
406
398
  id: Converter.bytesToHex(RandomHelper.generate(32)),
407
399
  consumerPid: request.consumerPid,
@@ -417,7 +409,7 @@ export class DataspaceControlPlaneService {
417
409
  policies,
418
410
  callbackAddress: request.callbackAddress,
419
411
  format: request.format,
420
- tenantId: Is.stringValue(requestTenantId) ? requestTenantId : undefined,
412
+ organizationIdentity,
421
413
  dataAddress: request.dataAddress,
422
414
  dateCreated: now.toISOString(),
423
415
  dateModified: now.toISOString()
@@ -484,8 +476,7 @@ export class DataspaceControlPlaneService {
484
476
  const agreement = await this.lookupAgreement(agreementId);
485
477
  const assigneeIdentity = OdrlPolicyHelper.extractAssigneeIdentity(agreement);
486
478
  const assigneeIds = ArrayHelper.fromObjectOrArray(assigneeIdentity);
487
- const callerComposite = this.buildCallerComposite(trustInfo);
488
- if (!assigneeIds.includes(callerComposite)) {
479
+ if (!assigneeIds.includes(trustInfo.identity)) {
489
480
  throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForAgreement");
490
481
  }
491
482
  const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
@@ -498,11 +489,7 @@ export class DataspaceControlPlaneService {
498
489
  const consumerPid = `urn:uuid:${RandomHelper.generateUuidV7()}`;
499
490
  const callbackAddress = StringHelper.trimTrailingSlashes(publicOrigin);
500
491
  // Fetch context once and reuse across the PUSH dataAddress, trust token, and storage entity.
501
- const contextIds = await ContextIdStore.getContextIds();
502
- const nodeIdentity = contextIds?.[ContextIdKeys.Node];
503
- Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "nodeIdentity", nodeIdentity);
504
- const tenantId = contextIds?.[ContextIdKeys.Tenant];
505
- const organizationIdentity = contextIds?.[ContextIdKeys.Organization];
492
+ const organizationIdentity = await this.resolveContextOrganizationId();
506
493
  const transferRequestMessage = {
507
494
  "@context": [DataspaceProtocolContexts.Context],
508
495
  "@type": DataspaceProtocolTransferProcessTypes.TransferRequestMessage,
@@ -519,8 +506,8 @@ export class DataspaceControlPlaneService {
519
506
  throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid });
520
507
  }
521
508
  let inboxEndpoint = `${callbackAddress}/${this._dataPlanePath}/inbox`;
522
- if (Is.stringValue(tenantId)) {
523
- inboxEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(inboxEndpoint, "tenant", tenantId);
509
+ if (Is.stringValue(organizationIdentity)) {
510
+ inboxEndpoint = HttpUrlHelper.addQueryStringParam(inboxEndpoint, ContextIdKeys.Organization, organizationIdentity);
524
511
  }
525
512
  transferRequestMessage.dataAddress = {
526
513
  "@type": DataspaceProtocolTransferProcessTypes.DataAddress,
@@ -529,7 +516,7 @@ export class DataspaceControlPlaneService {
529
516
  };
530
517
  }
531
518
  // Generate outbound trust token to authenticate this node to the provider.
532
- const outboundToken = await this._trustComponent.generate(nodeIdentity, this._overrideTrustGeneratorType, { subject: { consumerPid, agreementId } }, Is.stringValue(tenantId) ? TrustHelper.hashTenantId(tenantId) : undefined, organizationIdentity);
519
+ const outboundToken = await this._trustComponent.generate(organizationIdentity, this._overrideTrustGeneratorType, { subject: { consumerPid, agreementId } });
533
520
  // Create a remote REST client pointed at the provider endpoint and call requestTransfer.
534
521
  const remoteControlPlane = ComponentFactory.create(this._remoteControlPlaneComponentType, { endpoint: providerEndpoint, pathPrefix: "" });
535
522
  const result = await remoteControlPlane.requestTransfer(transferRequestMessage, outboundToken);
@@ -553,14 +540,14 @@ export class DataspaceControlPlaneService {
553
540
  state: DataspaceProtocolTransferProcessStateType.REQUESTED,
554
541
  agreementId,
555
542
  datasetId,
556
- consumerIdentity: callerComposite,
543
+ consumerIdentity: trustInfo.identity,
557
544
  providerIdentity,
558
545
  offerId: agreementId,
559
546
  policies: [agreement],
560
547
  callbackAddress,
561
548
  format,
562
549
  dataAddress: transferRequestMessage.dataAddress,
563
- tenantId: Is.stringValue(tenantId) ? tenantId : undefined,
550
+ organizationIdentity,
564
551
  dateCreated: now.toISOString(),
565
552
  dateModified: now.toISOString()
566
553
  };
@@ -608,14 +595,13 @@ export class DataspaceControlPlaneService {
608
595
  }
609
596
  try {
610
597
  const { entity, role } = await this.lookupTransferByMessage(message);
611
- this.validateCallerIsProvider(this.buildCallerComposite(trustInfo), entity);
612
- // Only the tenant that owns this transfer can mutate it.
613
- // Skipped on single-tenant nodes (entity.tenantId undefined).
614
- const callingTenantId = await this.resolveCallingTenantId();
615
- if (Is.stringValue(entity.tenantId) &&
616
- Is.stringValue(callingTenantId) &&
617
- entity.tenantId !== callingTenantId) {
618
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
598
+ if (trustInfo.identity !== entity.providerIdentity) {
599
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsProvider");
600
+ }
601
+ // Only the organization that owns this transfer can mutate it.
602
+ const callingOrganizationIdentity = await this.resolveContextOrganizationId();
603
+ if (entity.organizationIdentity !== callingOrganizationIdentity) {
604
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongOrganization");
619
605
  }
620
606
  if (entity.state !== DataspaceProtocolTransferProcessStateType.REQUESTED &&
621
607
  entity.state !== DataspaceProtocolTransferProcessStateType.SUSPENDED) {
@@ -665,27 +651,23 @@ export class DataspaceControlPlaneService {
665
651
  if (!Is.stringValue(entity.providerIdentity)) {
666
652
  throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
667
653
  }
668
- // providerIdentity is a composite `nodeDid:hash(tenantId)`
669
- const pushProviderId = this.parseTenantIdentifier(entity.providerIdentity);
670
- const accessToken = await this._trustComponent.generate(pushProviderId.nodeDid, this._overrideTrustGeneratorType, {
654
+ const pushProviderId = entity.providerIdentity;
655
+ const accessToken = await this._trustComponent.generate(pushProviderId, this._overrideTrustGeneratorType, {
671
656
  subject: {
672
657
  consumerPid: entity.consumerPid,
673
658
  providerPid: entity.providerPid,
674
659
  agreementId: entity.agreementId,
675
660
  datasetId: entity.datasetId
676
661
  }
677
- }, pushProviderId.tenantIdHash);
662
+ });
678
663
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "accessToken", accessToken);
679
664
  const tokenString = accessToken;
680
665
  let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}/inbox`;
681
- // Bake the provider's tenant token into the /inbox URL so the consumer's
682
- // inbound POST routes to the right tenant via TenantProcessor — mirrors the
666
+ // Bake the provider's organization identity into the /inbox URL so the consumer's
667
+ // inbound POST routes to the right organization via TenantProcessor — mirrors the
683
668
  // pull-mode endpoint baking below.
684
- const inboxContextIds1 = await ContextIdStore.getContextIds();
685
- const inboxTenantId1 = inboxContextIds1?.[ContextIdKeys.Tenant];
686
- if (Is.stringValue(inboxTenantId1)) {
687
- fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", inboxTenantId1);
688
- }
669
+ const organizationIdentity = await this.resolveContextOrganizationId();
670
+ fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
689
671
  response.dataAddress = {
690
672
  "@type": DataspaceProtocolTransferProcessTypes.DataAddress,
691
673
  endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
@@ -725,29 +707,24 @@ export class DataspaceControlPlaneService {
725
707
  }
726
708
  // Provider signs the data access token with its own identity.
727
709
  // The subject contains the transfer context claims that the data plane
728
- // will verify when the consumer presents this token. Parse
729
- // the composite providerIdentity to extract the signing nodeDid and
730
- // the tenant-hash to embed as `tid`.
710
+ // will verify when the consumer presents this token
731
711
  if (!Is.stringValue(entity.providerIdentity)) {
732
712
  throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerIdentityMissing");
733
713
  }
734
- const pullProviderId = this.parseTenantIdentifier(entity.providerIdentity);
735
- const accessToken = await this._trustComponent.generate(pullProviderId.nodeDid, this._overrideTrustGeneratorType, {
714
+ const pullProviderId = entity.providerIdentity;
715
+ const accessToken = await this._trustComponent.generate(pullProviderId, this._overrideTrustGeneratorType, {
736
716
  subject: {
737
717
  consumerPid: entity.consumerPid,
738
718
  providerPid: entity.providerPid,
739
719
  agreementId: entity.agreementId,
740
720
  datasetId: entity.datasetId
741
721
  }
742
- }, pullProviderId.tenantIdHash);
722
+ });
743
723
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "accessToken", accessToken);
744
724
  const tokenString = accessToken;
745
725
  let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}`;
746
- const contextIds = await ContextIdStore.getContextIds();
747
- const tenantId = contextIds?.[ContextIdKeys.Tenant];
748
- if (Is.stringValue(tenantId)) {
749
- fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", tenantId);
750
- }
726
+ const organizationIdentity = await this.resolveContextOrganizationId();
727
+ fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
751
728
  response.dataAddress = {
752
729
  "@type": DataspaceProtocolTransferProcessTypes.DataAddress,
753
730
  endpointType: DataspaceProtocolEndpointType.HttpsQueryEndpoint,
@@ -792,14 +769,11 @@ export class DataspaceControlPlaneService {
792
769
  return transformToTransferError(new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid: entity.consumerPid }), message);
793
770
  }
794
771
  let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}/inbox`;
795
- // Bake the provider's tenant token into the /inbox URL so the consumer's
796
- // inbound POST routes to the right tenant via TenantProcessor — mirrors the
772
+ // Bake the provider's organization identity into the /inbox URL so the consumer's
773
+ // inbound POST routes to the right organization via TenantProcessor — mirrors the
797
774
  // pull-mode endpoint baking.
798
- const inboxContextIds2 = await ContextIdStore.getContextIds();
799
- const inboxTenantId2 = inboxContextIds2?.[ContextIdKeys.Tenant];
800
- if (Is.stringValue(inboxTenantId2)) {
801
- fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", inboxTenantId2);
802
- }
775
+ const organizationIdentity = await this.resolveContextOrganizationId();
776
+ fullEndpoint = HttpUrlHelper.addQueryStringParam(fullEndpoint, ContextIdKeys.Organization, organizationIdentity);
803
777
  response.dataAddress = {
804
778
  "@type": DataspaceProtocolTransferProcessTypes.DataAddress,
805
779
  endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
@@ -887,14 +861,13 @@ export class DataspaceControlPlaneService {
887
861
  }
888
862
  try {
889
863
  const { entity, role } = await this.lookupTransferByMessage(message);
890
- this.validateCallerIsConsumer(this.buildCallerComposite(trustInfo), entity);
891
- // Only the tenant that owns this transfer can mutate it.
892
- // Skipped on single-tenant nodes (entity.tenantId undefined).
893
- const callingTenantId = await this.resolveCallingTenantId();
894
- if (Is.stringValue(entity.tenantId) &&
895
- Is.stringValue(callingTenantId) &&
896
- entity.tenantId !== callingTenantId) {
897
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
864
+ if (trustInfo.identity !== entity.consumerIdentity) {
865
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsConsumer");
866
+ }
867
+ // Only the organization that owns this transfer can mutate it.
868
+ const callingOrganizationId = await this.resolveContextOrganizationId();
869
+ if (entity.organizationIdentity !== callingOrganizationId) {
870
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongOrganization");
898
871
  }
899
872
  // DSP idempotency: re-receiving TransferCompletionMessage for a transfer already
900
873
  // in COMPLETED should return the same success response, not invalidStateForComplete.
@@ -981,14 +954,14 @@ export class DataspaceControlPlaneService {
981
954
  }
982
955
  try {
983
956
  const { entity, role } = await this.lookupTransferByMessage(message);
984
- this.validateCallerIsTransferParty(this.buildCallerComposite(trustInfo), entity);
985
- // S2 belt-and-braces: only the tenant that owns this transfer can mutate it.
986
- // Skipped on single-tenant nodes (entity.tenantId undefined).
987
- const callingTenantId = await this.resolveCallingTenantId();
988
- if (Is.stringValue(entity.tenantId) &&
989
- Is.stringValue(callingTenantId) &&
990
- entity.tenantId !== callingTenantId) {
991
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
957
+ if (trustInfo.identity !== entity.consumerIdentity &&
958
+ trustInfo.identity !== entity.providerIdentity) {
959
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
960
+ }
961
+ // S2 belt-and-braces: only the organization that owns this transfer can mutate it.
962
+ const callingOrganizationId = await this.resolveContextOrganizationId();
963
+ if (entity.organizationIdentity !== callingOrganizationId) {
964
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongOrganization");
992
965
  }
993
966
  // DSP idempotency: re-receiving TransferSuspensionMessage for a transfer already
994
967
  // in SUSPENDED should return success. Data-plane suspend already ran.
@@ -1079,14 +1052,14 @@ export class DataspaceControlPlaneService {
1079
1052
  }
1080
1053
  try {
1081
1054
  const { entity, role } = await this.lookupTransferByMessage(message);
1082
- this.validateCallerIsTransferParty(this.buildCallerComposite(trustInfo), entity);
1083
- // Only the tenant that owns this transfer can mutate it.
1084
- // Skipped on single-tenant nodes (entity.tenantId undefined).
1085
- const callingTenantId = await this.resolveCallingTenantId();
1086
- if (Is.stringValue(entity.tenantId) &&
1087
- Is.stringValue(callingTenantId) &&
1088
- entity.tenantId !== callingTenantId) {
1089
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongTenant");
1055
+ if (trustInfo.identity !== entity.consumerIdentity &&
1056
+ trustInfo.identity !== entity.providerIdentity) {
1057
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
1058
+ }
1059
+ // Only the organization that owns this transfer can mutate it.
1060
+ const organizationIdentity = await this.resolveContextOrganizationId();
1061
+ if (entity.organizationIdentity !== organizationIdentity) {
1062
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "transferWrongOrganization");
1090
1063
  }
1091
1064
  // DSP idempotency: re-receiving TransferTerminationMessage for a transfer already
1092
1065
  // in TERMINATED should return success. Data-plane teardown already ran.
@@ -1160,7 +1133,10 @@ export class DataspaceControlPlaneService {
1160
1133
  const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "getTransferProcess");
1161
1134
  try {
1162
1135
  const { entity, role } = await this.lookupTransferByPid(pid);
1163
- this.validateCallerIsTransferParty(this.buildCallerComposite(trustInfo), entity);
1136
+ if (trustInfo.identity !== entity.consumerIdentity &&
1137
+ trustInfo.identity !== entity.providerIdentity) {
1138
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
1139
+ }
1164
1140
  await this._loggingComponent?.log({
1165
1141
  level: "info",
1166
1142
  source: DataspaceControlPlaneService.CLASS_NAME,
@@ -1218,7 +1194,8 @@ export class DataspaceControlPlaneService {
1218
1194
  verifiedIdentity: trustInfo.identity
1219
1195
  }
1220
1196
  });
1221
- const localTrustPayload = await this.generateLocalTrustPayload();
1197
+ const organizationId = await this.resolveContextOrganizationId();
1198
+ const localTrustPayload = await this._trustComponent.generate(organizationId, this._overrideTrustGeneratorType, {});
1222
1199
  const catalogResult = await this._federatedCatalogueComponent.get(datasetId, localTrustPayload);
1223
1200
  if (isCatalogError(catalogResult)) {
1224
1201
  if (isCatalogErrorName(catalogResult, NotFoundError.CLASS_NAME)) {
@@ -1391,23 +1368,14 @@ export class DataspaceControlPlaneService {
1391
1368
  });
1392
1369
  }
1393
1370
  const agreement = await this.lookupAgreement(entity.agreementId);
1394
- const contextIds = await ContextIdStore.getContextIds();
1395
- const currentComposite = this.buildTenantIdentifier(contextIds?.[ContextIdKeys.Node], contextIds?.[ContextIdKeys.Tenant]);
1396
- // Identity is `nodeDid:hash(tenantId)` (or just `nodeDid` in
1397
- // single-tenant). Without a node DID in context we cannot derive any
1398
- // caller identity at all.
1399
- if (!Is.stringValue(currentComposite)) {
1400
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "organizationContextMissing", {
1401
- consumerPid
1402
- });
1403
- }
1371
+ const organizationIdentity = await this.resolveContextOrganizationId();
1404
1372
  const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
1405
1373
  const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
1406
- if (!assignerIds.includes(currentComposite)) {
1374
+ if (!assignerIds.includes(organizationIdentity)) {
1407
1375
  throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "agreementAssignerMismatch", {
1408
1376
  consumerPid,
1409
1377
  agreementId: OdrlPolicyHelper.getUid(agreement),
1410
- expectedComposite: currentComposite,
1378
+ organizationIdentity,
1411
1379
  actualAssigner: assignerIds.join(", ")
1412
1380
  });
1413
1381
  }
@@ -1422,7 +1390,7 @@ export class DataspaceControlPlaneService {
1422
1390
  state: entity.state,
1423
1391
  consumerIdentity: entity.consumerIdentity,
1424
1392
  agreementId: OdrlPolicyHelper.getUid(agreement),
1425
- organizationId: contextIds?.[ContextIdKeys.Organization]
1393
+ organizationId: organizationIdentity
1426
1394
  }
1427
1395
  });
1428
1396
  return {
@@ -1453,20 +1421,14 @@ export class DataspaceControlPlaneService {
1453
1421
  });
1454
1422
  }
1455
1423
  const agreement = await this.lookupAgreement(entity.agreementId);
1456
- const contextIds = await ContextIdStore.getContextIds();
1457
- const currentComposite = this.buildTenantIdentifier(contextIds?.[ContextIdKeys.Node], contextIds?.[ContextIdKeys.Tenant]);
1458
- if (!Is.stringValue(currentComposite)) {
1459
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "organizationContextMissingProvider", {
1460
- providerPid
1461
- });
1462
- }
1424
+ const organizationIdentity = await this.resolveContextOrganizationId();
1463
1425
  const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
1464
1426
  const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
1465
- if (!assignerIds.includes(currentComposite)) {
1427
+ if (!assignerIds.includes(organizationIdentity)) {
1466
1428
  throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "agreementAssignerMismatchProvider", {
1467
1429
  providerPid,
1468
1430
  agreementId: OdrlPolicyHelper.getUid(agreement),
1469
- expectedComposite: currentComposite,
1431
+ organizationIdentity,
1470
1432
  actualAssigner: assignerIds.join(", ")
1471
1433
  });
1472
1434
  }
@@ -1496,7 +1458,7 @@ export class DataspaceControlPlaneService {
1496
1458
  consumerIdentity: entity.consumerIdentity,
1497
1459
  providerIdentity: entity.providerIdentity,
1498
1460
  agreementId: OdrlPolicyHelper.getUid(agreement),
1499
- organizationId: contextIds?.[ContextIdKeys.Organization]
1461
+ organizationId: organizationIdentity
1500
1462
  }
1501
1463
  });
1502
1464
  return {
@@ -1512,7 +1474,7 @@ export class DataspaceControlPlaneService {
1512
1474
  };
1513
1475
  }
1514
1476
  /**
1515
- * Register a dataset for a dataspace app, owned by the calling tenant.
1477
+ * Register a dataset for a dataspace app, owned by the calling organization.
1516
1478
  * @param id Optional explicit id. If omitted, derived from `dataset["@id"]`
1517
1479
  * or generated.
1518
1480
  * @param appId The dataspace app this dataset belongs to.
@@ -1537,8 +1499,7 @@ export class DataspaceControlPlaneService {
1537
1499
  id: resolvedId
1538
1500
  });
1539
1501
  }
1540
- const tenantId = await this.resolveCallingTenantId();
1541
- const nodeIdentity = await this.resolveNodeIdentity();
1502
+ const organizationIdentity = await this.resolveContextOrganizationId();
1542
1503
  const existing = await this._dataspaceAppDatasetStorage.get(resolvedId);
1543
1504
  if (!Is.empty(existing)) {
1544
1505
  throw new AlreadyExistsError(DataspaceControlPlaneService.CLASS_NAME, "datasetAlreadyExists", resolvedId);
@@ -1546,8 +1507,9 @@ export class DataspaceControlPlaneService {
1546
1507
  const now = new Date().toISOString();
1547
1508
  const entity = {
1548
1509
  id: resolvedId,
1549
- nodeIdentity,
1550
- tenantId,
1510
+ organizationIdentity,
1511
+ // Required for out of bound dataset operations that need to correlate back to the owning tenant
1512
+ tenantId: await this.resolveContextTenantId(),
1551
1513
  appId,
1552
1514
  dataset: ObjectHelper.omit(dataset, ["@id"]),
1553
1515
  dateCreated: now,
@@ -1559,19 +1521,19 @@ export class DataspaceControlPlaneService {
1559
1521
  return resolvedId;
1560
1522
  }
1561
1523
  /**
1562
- * Get a dataset record owned by the calling tenant.
1524
+ * Get a dataset record owned by the calling organization.
1563
1525
  * @param id The stored dataset id.
1564
1526
  * @returns The stored dataset record.
1565
1527
  */
1566
1528
  async getAppDataset(id) {
1567
1529
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
1568
- const tenantId = await this.resolveCallingTenantId();
1530
+ const organizationId = await this.resolveContextOrganizationId();
1569
1531
  const entity = await this._dataspaceAppDatasetStorage.get(id);
1570
1532
  if (Is.empty(entity)) {
1571
1533
  throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
1572
1534
  }
1573
- if (entity.tenantId !== tenantId) {
1574
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
1535
+ if (entity.organizationIdentity !== organizationId) {
1536
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongOrganization");
1575
1537
  }
1576
1538
  return {
1577
1539
  id: entity.id,
@@ -1582,20 +1544,18 @@ export class DataspaceControlPlaneService {
1582
1544
  };
1583
1545
  }
1584
1546
  /**
1585
- * List the dataspace app datasets owned by the calling tenant.
1547
+ * List the dataspace app datasets owned by the calling organization.
1586
1548
  * @param cursor Optional pagination cursor.
1587
1549
  * @param limit Optional maximum number of entries to return.
1588
1550
  * @returns The stored datasets and the next-page cursor if more exist.
1589
1551
  */
1590
1552
  async listAppDatasets(cursor, limit) {
1591
- const tenantId = await this.resolveCallingTenantId();
1592
- const page = await this._dataspaceAppDatasetStorage.query(Is.stringValue(tenantId)
1593
- ? {
1594
- property: "tenantId",
1595
- value: tenantId,
1596
- comparison: ComparisonOperator.Equals
1597
- }
1598
- : undefined, undefined, undefined, cursor, limit);
1553
+ const organizationIdentity = await this.resolveContextOrganizationId();
1554
+ const page = await this._dataspaceAppDatasetStorage.query({
1555
+ property: "organizationIdentity",
1556
+ value: organizationIdentity,
1557
+ comparison: ComparisonOperator.Equals
1558
+ }, undefined, undefined, cursor, limit);
1599
1559
  const entities = (page.entities ?? []).map(entity => ({
1600
1560
  id: entity.id,
1601
1561
  appId: entity.appId,
@@ -1609,7 +1569,7 @@ export class DataspaceControlPlaneService {
1609
1569
  };
1610
1570
  }
1611
1571
  /**
1612
- * Update a dataset record owned by the calling tenant.
1572
+ * Update a dataset record owned by the calling organization.
1613
1573
  * @param id The stored dataset id.
1614
1574
  * @param appId The dataspace app this dataset belongs to.
1615
1575
  * @param dataset The dataset payload.
@@ -1618,13 +1578,13 @@ export class DataspaceControlPlaneService {
1618
1578
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
1619
1579
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "appId", appId);
1620
1580
  Guards.object(DataspaceControlPlaneService.CLASS_NAME, "dataset", dataset);
1621
- const tenantId = await this.resolveCallingTenantId();
1581
+ const organizationId = await this.resolveContextOrganizationId();
1622
1582
  const existing = await this._dataspaceAppDatasetStorage.get(id);
1623
1583
  if (Is.empty(existing)) {
1624
1584
  throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
1625
1585
  }
1626
- if (existing.tenantId !== tenantId) {
1627
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
1586
+ if (existing.organizationIdentity !== organizationId) {
1587
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongOrganization");
1628
1588
  }
1629
1589
  const updated = {
1630
1590
  ...existing,
@@ -1637,20 +1597,20 @@ export class DataspaceControlPlaneService {
1637
1597
  await this._dataspaceAppDatasetStorage.set(updated);
1638
1598
  }
1639
1599
  /**
1640
- * Delete a dataspace app dataset owned by the calling tenant.
1600
+ * Delete a dataspace app dataset owned by the calling organization.
1641
1601
  * @param id The stored app dataset id.
1642
1602
  */
1643
1603
  async deleteAppDataset(id) {
1644
1604
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
1645
- const tenantId = await this.resolveCallingTenantId();
1605
+ const organizationId = await this.resolveContextOrganizationId();
1646
1606
  const existing = await this._dataspaceAppDatasetStorage.get(id);
1647
1607
  if (Is.empty(existing)) {
1648
1608
  throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
1649
1609
  }
1650
- if (existing.tenantId !== tenantId) {
1651
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
1610
+ if (existing.organizationIdentity !== organizationId) {
1611
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongOrganization");
1652
1612
  }
1653
- const localTrustPayload = await this.generateLocalTrustPayload(existing.tenantId);
1613
+ const localTrustPayload = await this._trustComponent.generate(existing.organizationIdentity, this._overrideTrustGeneratorType, {});
1654
1614
  const removeResult = await this._federatedCatalogueComponent.remove(id, localTrustPayload);
1655
1615
  if (isCatalogError(removeResult)) {
1656
1616
  throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "datasetRemoveFailed", {
@@ -1730,7 +1690,7 @@ export class DataspaceControlPlaneService {
1730
1690
  providerIdentity: storageEntity.providerIdentity,
1731
1691
  format: storageEntity.format,
1732
1692
  callbackAddress: storageEntity.callbackAddress,
1733
- tenantId: storageEntity.tenantId,
1693
+ organizationIdentity: storageEntity.organizationIdentity,
1734
1694
  dateCreated: new Date(storageEntity.dateCreated),
1735
1695
  dateModified: new Date(storageEntity.dateModified),
1736
1696
  policies: storageEntity.policies,
@@ -1756,7 +1716,7 @@ export class DataspaceControlPlaneService {
1756
1716
  providerIdentity: entity.providerIdentity,
1757
1717
  format: entity.format,
1758
1718
  callbackAddress: entity.callbackAddress,
1759
- tenantId: entity.tenantId,
1719
+ organizationIdentity: entity.organizationIdentity,
1760
1720
  dateCreated: entity.dateCreated.toISOString(),
1761
1721
  dateModified: entity.dateModified.toISOString(),
1762
1722
  policies: entity.policies,
@@ -1783,57 +1743,6 @@ export class DataspaceControlPlaneService {
1783
1743
  }
1784
1744
  return agreement;
1785
1745
  }
1786
- /**
1787
- * Build the caller's composite identity (`nodeDid:tenantIdHash`) from a
1788
- * verified trust payload.
1789
- * @param trustInfo The verification info from `TrustHelper.verifyTrust`.
1790
- * @returns The composite identity.
1791
- * @internal
1792
- */
1793
- buildCallerComposite(trustInfo) {
1794
- Guards.object(DataspaceControlPlaneService.CLASS_NAME, "trustInfo", trustInfo);
1795
- Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "trustInfo.identity", trustInfo.identity);
1796
- return Is.stringValue(trustInfo.tenantId)
1797
- ? `${trustInfo.identity}:${trustInfo.tenantId}`
1798
- : trustInfo.identity;
1799
- }
1800
- /**
1801
- * Validate that the caller's verified identity matches the consumer of a transfer process.
1802
- * @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
1803
- * @param entity The transfer process entity.
1804
- * @throws UnauthorizedError if the caller is not the consumer.
1805
- * @internal
1806
- */
1807
- validateCallerIsConsumer(callerComposite, entity) {
1808
- if (callerComposite !== entity.consumerIdentity) {
1809
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsConsumer");
1810
- }
1811
- }
1812
- /**
1813
- * Validate that the caller's verified identity matches the provider of a transfer process.
1814
- * @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
1815
- * @param entity The transfer process entity.
1816
- * @throws UnauthorizedError if the caller is not the provider.
1817
- * @internal
1818
- */
1819
- validateCallerIsProvider(callerComposite, entity) {
1820
- if (callerComposite !== entity.providerIdentity) {
1821
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedAsProvider");
1822
- }
1823
- }
1824
- /**
1825
- * Validate that the caller's verified identity matches either the consumer or provider of a transfer process.
1826
- * @param callerComposite The caller's composite identity (`nodeDid:tenantIdHash`).
1827
- * @param entity The transfer process entity.
1828
- * @throws UnauthorizedError if the caller is not a party to the transfer.
1829
- * @internal
1830
- */
1831
- validateCallerIsTransferParty(callerComposite, entity) {
1832
- if (callerComposite !== entity.consumerIdentity &&
1833
- callerComposite !== entity.providerIdentity) {
1834
- throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForTransfer");
1835
- }
1836
- }
1837
1746
  /**
1838
1747
  * Extract dataset ID from Agreement target.
1839
1748
  * @param format Agreement.
@@ -1882,8 +1791,8 @@ export class DataspaceControlPlaneService {
1882
1791
  async validateCatalogDataset(datasetId, agreement) {
1883
1792
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "datasetId", datasetId);
1884
1793
  Guards.object(DataspaceControlPlaneService.CLASS_NAME, "agreement", agreement);
1885
- // Lookup dataset in catalog
1886
- const localTrustPayload = await this.generateLocalTrustPayload();
1794
+ const organizationId = await this.resolveContextOrganizationId();
1795
+ const localTrustPayload = await this._trustComponent.generate(organizationId, this._overrideTrustGeneratorType, {});
1887
1796
  const catalogResult = await this._federatedCatalogueComponent.get(datasetId, localTrustPayload);
1888
1797
  if (isCatalogError(catalogResult)) {
1889
1798
  if (isCatalogErrorName(catalogResult, NotFoundError.CLASS_NAME)) {
@@ -2090,52 +1999,11 @@ export class DataspaceControlPlaneService {
2090
1999
  return dataset;
2091
2000
  }
2092
2001
  const contextIds = await ContextIdStore.getContextIds();
2093
- // The publisher attribution is always the composite identifier
2094
- const compositePublisher = this.buildTenantIdentifier(contextIds?.[ContextIdKeys.Node], contextIds?.[ContextIdKeys.Tenant]);
2095
- if (compositePublisher) {
2096
- return { ...dataset, "dcterms:publisher": compositePublisher };
2002
+ if (contextIds?.[ContextIdKeys.Organization]) {
2003
+ return { ...dataset, "dcterms:publisher": contextIds[ContextIdKeys.Organization] };
2097
2004
  }
2098
2005
  return dataset;
2099
2006
  }
2100
- /**
2101
- * Build the tenant-attributed identifier.
2102
- * @param nodeId The node identity (from `ContextIdKeys.Node`).
2103
- * @param tenantId The plaintext tenant id (from `ContextIdKeys.Tenant`), if any. Hashed before insertion into the composite.
2104
- * @returns Composite identifier, or undefined if nodeId is missing.
2105
- * @internal
2106
- */
2107
- buildTenantIdentifier(nodeId, tenantId) {
2108
- if (!Is.stringValue(nodeId)) {
2109
- return undefined;
2110
- }
2111
- return Is.stringValue(tenantId) ? `${nodeId}:${TrustHelper.hashTenantId(tenantId)}` : nodeId;
2112
- }
2113
- /**
2114
- * Parse a composite tenant identifier into its node and tenant-hash portions.
2115
- * @param composite The composite identifier (`nodeDid` or `nodeDid:hash`).
2116
- * @returns The parsed parts. `tenantIdHash` is undefined when the composite has no tenant portion (single-tenant node form).
2117
- * @throws GuardError if the input is not a non-empty string.
2118
- * @throws GeneralError if the input does not start with `did:` — the only valid composite identifier shapes carry a DID as the leading portion.
2119
- * @internal
2120
- */
2121
- parseTenantIdentifier(composite) {
2122
- Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "composite", composite);
2123
- if (!composite.startsWith("did:")) {
2124
- throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "invalidCompositeIdentifier", { composite });
2125
- }
2126
- const lastColon = composite.lastIndexOf(":");
2127
- if (lastColon === -1) {
2128
- return { nodeDid: composite };
2129
- }
2130
- const candidateHash = composite.slice(lastColon + 1);
2131
- if (DataspaceControlPlaneService._TENANT_HASH_PATTERN.test(candidateHash)) {
2132
- return {
2133
- nodeDid: composite.slice(0, lastColon),
2134
- tenantIdHash: candidateHash
2135
- };
2136
- }
2137
- return { nodeDid: composite };
2138
- }
2139
2007
  /**
2140
2008
  * Resolve the data plane component or throw if it isn't registered. Push-mode transfers
2141
2009
  * require the data plane; pull-only deployments may run without it.
@@ -2315,26 +2183,22 @@ export class DataspaceControlPlaneService {
2315
2183
  }
2316
2184
  // DATASPACE APP DATASET HANDLERS
2317
2185
  /**
2318
- * Publish a single dataspace app dataset to the federated catalogue in its owning tenant's context.
2186
+ * Publish a single dataspace app dataset to the federated catalogue.
2319
2187
  * @param appDataset The stored dataspace app dataset entity.
2320
2188
  * @internal
2321
2189
  */
2322
2190
  async publishAppDataset(appDataset) {
2323
- // Override `Tenant` in the context wrap so federated catalogue captures the
2324
- // appDataset's owning tenant. On single-tenant nodes the appDataset has no tenantId,
2325
- // so the assignment writes `Tenant: undefined` equivalent to no override.
2326
- const contextIds = (await ContextIdStore.getContextIds()) ?? {};
2327
- const localTrustPayload = await this.generateLocalTrustPayload(appDataset.tenantId);
2328
- const wrappedContextIds = {
2329
- ...contextIds,
2330
- [ContextIdKeys.Tenant]: appDataset.tenantId
2331
- };
2332
- await ContextIdStore.run(wrappedContextIds, async () => {
2191
+ const localTrustPayload = await this._trustComponent.generate(appDataset.organizationIdentity, this._overrideTrustGeneratorType);
2192
+ // Ensure that any dataspace app operations are run with the tenant id
2193
+ // of the dataset owner, so that propagates to any operations it performs
2194
+ const contextIds = await ContextIdStore.getContextIds();
2195
+ const tenantContextIds = { ...contextIds, [ContextIdKeys.Tenant]: appDataset.tenantId };
2196
+ await ContextIdStore.run(tenantContextIds, async () => {
2333
2197
  const app = DataspaceAppFactory.get(appDataset.appId);
2334
2198
  const datasetPayload = this.restampDatasetId(appDataset.dataset, appDataset.id);
2335
2199
  const overrideHandler = app.datasetsHandled?.bind(app);
2336
2200
  const rawDatasets = overrideHandler
2337
- ? await overrideHandler(datasetPayload, appDataset.tenantId ?? "")
2201
+ ? await overrideHandler(datasetPayload)
2338
2202
  : [datasetPayload];
2339
2203
  const datasets = await Promise.all(rawDatasets.map(async (d) => this.populateDefaults(d)));
2340
2204
  for (const dataset of datasets) {
@@ -2351,38 +2215,23 @@ export class DataspaceControlPlaneService {
2351
2215
  });
2352
2216
  }
2353
2217
  /**
2354
- * Resolve the calling tenant from the request context.
2355
- * @returns The owning tenant id, or undefined for single-tenant nodes.
2218
+ * Resolve the calling organization from the request context.
2219
+ * @returns The owning organization identity.
2356
2220
  * @internal
2357
2221
  */
2358
- async resolveCallingTenantId() {
2222
+ async resolveContextOrganizationId() {
2359
2223
  const contextIds = await ContextIdStore.getContextIds();
2360
- const tenantId = contextIds?.[ContextIdKeys.Tenant];
2361
- return Is.stringValue(tenantId) ? tenantId : undefined;
2224
+ ContextIdHelper.guard(contextIds, ContextIdKeys.Organization);
2225
+ return contextIds[ContextIdKeys.Organization];
2362
2226
  }
2363
2227
  /**
2364
- * Resolve the node identity from the current context, throwing if absent.
2365
- * @returns The owning node identity.
2228
+ * Resolve the tenant identity from the current context.
2229
+ * @returns The owning tenant identity.
2366
2230
  * @internal
2367
2231
  */
2368
- async resolveNodeIdentity() {
2232
+ async resolveContextTenantId() {
2369
2233
  const contextIds = await ContextIdStore.getContextIds();
2370
- const nodeId = contextIds?.[ContextIdKeys.Node];
2371
- if (!Is.stringValue(nodeId)) {
2372
- throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "datasetNodeContextRequired");
2373
- }
2374
- return nodeId;
2375
- }
2376
- /**
2377
- * Generate a trust payload representing this node for internal FederatedCatalogue calls.
2378
- * @param tenantId Optional tenant ID to embed in the token.
2379
- * @returns The generated trust payload.
2380
- * @internal
2381
- */
2382
- async generateLocalTrustPayload(tenantId) {
2383
- const nodeId = await this.resolveNodeIdentity();
2384
- const contextIds = (await ContextIdStore.getContextIds()) ?? {};
2385
- return this._trustComponent.generate(nodeId, this._overrideTrustGeneratorType, {}, Is.stringValue(tenantId) ? TrustHelper.hashTenantId(tenantId) : undefined, contextIds[ContextIdKeys.Organization]);
2234
+ return contextIds?.[ContextIdKeys.Tenant];
2386
2235
  }
2387
2236
  }
2388
2237
  //# sourceMappingURL=dataspaceControlPlaneService.js.map