@twin.org/dataspace-control-plane-service 0.0.3-next.26 → 0.0.3-next.28

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.
@@ -3,6 +3,7 @@ import { ArrayHelper, BaseError, ComponentFactory, Converter, GeneralError, Guar
3
3
  import { JsonLdHelper } from "@twin.org/data-json-ld";
4
4
  import { DataspaceAppFactory, TransferProcessRole } from "@twin.org/dataspace-models";
5
5
  import { EngineCoreFactory } from "@twin.org/engine-models";
6
+ import { ComparisonOperator } from "@twin.org/entity";
6
7
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
7
8
  import { OdrlPolicyHelper, PolicyRequesterFactory } from "@twin.org/rights-management-models";
8
9
  import { DataspaceProtocolContexts, DataspaceProtocolContractNegotiationTypes, DataspaceProtocolEndpointType, DataspaceProtocolHelper, DataspaceProtocolTransferProcessStateType, DataspaceProtocolTransferProcessTypes } from "@twin.org/standards-dataspace-protocol";
@@ -69,6 +70,11 @@ export class DataspaceControlPlaneService {
69
70
  * @internal
70
71
  */
71
72
  _transferProcessStorage;
73
+ /**
74
+ * Entity storage for tenant-supplied Dataspace App Dataset entities.
75
+ * @internal
76
+ */
77
+ _dataspaceAppDatasetStorage;
72
78
  /**
73
79
  * The trust component for token verification and generation.
74
80
  * @internal
@@ -101,6 +107,11 @@ export class DataspaceControlPlaneService {
101
107
  * @internal
102
108
  */
103
109
  _negotiationCallbacks;
110
+ /**
111
+ * The component type name for the hosting component.
112
+ * @internal
113
+ */
114
+ _urlTransformerComponent;
104
115
  /**
105
116
  * Create a new instance of DataspaceControlPlaneService.
106
117
  * @param options The options for the service.
@@ -118,12 +129,14 @@ export class DataspaceControlPlaneService {
118
129
  // Retrieve Federated Catalogue component with default
119
130
  this._federatedCatalogueComponent = ComponentFactory.get(options?.federatedCatalogueComponentType ?? "federated-catalogue");
120
131
  this._transferProcessStorage = EntityStorageConnectorFactory.get(options?.transferProcessEntityStorageType ?? "transfer-process");
132
+ this._dataspaceAppDatasetStorage = EntityStorageConnectorFactory.get(options?.dataspaceAppDatasetEntityStorageType ?? "dataspace-app-dataset");
121
133
  this._trustComponent = ComponentFactory.get(options?.trustComponentType ?? "trust");
122
134
  this._overrideTrustGeneratorType = options?.config?.overrideTrustGeneratorType;
123
135
  this._dataPlanePath = Is.stringValue(options?.config?.dataPlanePath)
124
136
  ? StringHelper.trimLeadingSlashes(options.config.dataPlanePath)
125
137
  : undefined;
126
138
  this._taskScheduler = ComponentFactory.getIfExists(options?.taskSchedulerComponentType ?? "task-scheduler");
139
+ this._urlTransformerComponent = ComponentFactory.get(options?.urlTransformerComponentType ?? "url-transformer");
127
140
  this._negotiationCallbacks = new Map();
128
141
  const internalCallback = {
129
142
  onStateChanged: async (negotiationId, state, data) => {
@@ -206,7 +219,9 @@ export class DataspaceControlPlaneService {
206
219
  /**
207
220
  * The service needs to be started when the application is initialized.
208
221
  * Populates the Federated Catalogue with datasets from registered apps
209
- * and starts the stalled negotiation cleanup task.
222
+ * and starts the stalled negotiation cleanup task. Also captures the node
223
+ * identity from ContextIdStore when tenant-token encryption is configured
224
+ * (required to derive the vault key name `${nodeId}/${signingKeyName}`).
210
225
  * @param nodeLoggingComponentType The node logging component type.
211
226
  */
212
227
  async start(nodeLoggingComponentType) {
@@ -227,28 +242,20 @@ export class DataspaceControlPlaneService {
227
242
  source: DataspaceControlPlaneService.CLASS_NAME,
228
243
  message: "populatingFederatedCatalogue"
229
244
  });
230
- // Get all registered apps
231
- const appNames = DataspaceAppFactory.names();
232
- await this._loggingComponent?.log({
233
- level: "debug",
234
- ts: Date.now(),
235
- source: DataspaceControlPlaneService.CLASS_NAME,
236
- message: "discoveredApps",
237
- data: { count: appNames.length, apps: appNames }
238
- });
239
245
  let registeredCount = 0;
240
246
  let errorCount = 0;
241
- // Collect and register datasets from each app
242
- for (const appName of appNames) {
243
- try {
244
- const app = DataspaceAppFactory.get(appName);
245
- const datasets = await app.datasetsHandled();
246
- for (const dataset of datasets) {
247
+ let totalDatasets = 0;
248
+ // Walk stored dataspace app datasets one page at a time and publish each in its
249
+ // owning tenant's context. Memory stays bounded to one page.
250
+ let cursor;
251
+ do {
252
+ const page = await this._dataspaceAppDatasetStorage.query(undefined, undefined, undefined, cursor);
253
+ if (Is.arrayValue(page.entities)) {
254
+ for (const entity of page.entities) {
255
+ const appDataset = entity;
256
+ totalDatasets++;
247
257
  try {
248
- // Ensure publisher is set (required for Federated Catalogue)
249
- const datasetWithPublisher = await this.ensurePublisher(dataset);
250
- // Register with Federated Catalogue
251
- await this._federatedCatalogueComponent.set(datasetWithPublisher);
258
+ await this.publishAppDataset(appDataset);
252
259
  registeredCount++;
253
260
  await this._loggingComponent?.log({
254
261
  level: "debug",
@@ -256,8 +263,9 @@ export class DataspaceControlPlaneService {
256
263
  source: DataspaceControlPlaneService.CLASS_NAME,
257
264
  message: "datasetRegistered",
258
265
  data: {
259
- datasetId: getJsonLdId(datasetWithPublisher) ?? "",
260
- appName
266
+ datasetId: appDataset.id,
267
+ appId: appDataset.appId,
268
+ tenantId: appDataset.tenantId
261
269
  }
262
270
  });
263
271
  }
@@ -267,27 +275,19 @@ export class DataspaceControlPlaneService {
267
275
  level: "error",
268
276
  ts: Date.now(),
269
277
  source: DataspaceControlPlaneService.CLASS_NAME,
270
- message: "datasetRegistrationFailed",
278
+ message: "datasetPublishFailed",
271
279
  error: BaseError.fromError(error),
272
280
  data: {
273
- datasetId: getJsonLdId(dataset) ?? "",
274
- appName
281
+ datasetRecordId: appDataset.id,
282
+ appId: appDataset.appId,
283
+ tenantId: appDataset.tenantId
275
284
  }
276
285
  });
277
286
  }
278
287
  }
279
288
  }
280
- catch (error) {
281
- await this._loggingComponent?.log({
282
- level: "error",
283
- ts: Date.now(),
284
- source: DataspaceControlPlaneService.CLASS_NAME,
285
- message: "appDatasetsRetrievalFailed",
286
- error: BaseError.fromError(error),
287
- data: { appName }
288
- });
289
- }
290
- }
289
+ cursor = page.cursor;
290
+ } while (Is.stringValue(cursor));
291
291
  await this._loggingComponent?.log({
292
292
  level: "info",
293
293
  ts: Date.now(),
@@ -296,7 +296,7 @@ export class DataspaceControlPlaneService {
296
296
  data: {
297
297
  registeredCount,
298
298
  errorCount,
299
- totalApps: appNames.length
299
+ totalDatasets
300
300
  }
301
301
  });
302
302
  if (this._taskScheduler) {
@@ -336,7 +336,7 @@ export class DataspaceControlPlaneService {
336
336
  async requestTransfer(request, trustPayload) {
337
337
  const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "requestTransfer");
338
338
  const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(request));
339
- if (validationFailures.length > 0) {
339
+ if (Is.arrayValue(validationFailures)) {
340
340
  await this._loggingComponent?.log({
341
341
  level: "error",
342
342
  source: DataspaceControlPlaneService.CLASS_NAME,
@@ -350,7 +350,7 @@ export class DataspaceControlPlaneService {
350
350
  let datasetId;
351
351
  let consumerIdentity;
352
352
  let providerIdentity;
353
- let policies = [];
353
+ let policies;
354
354
  try {
355
355
  const agreement = await this.lookupAgreement(request.agreementId);
356
356
  if (!agreement) {
@@ -445,7 +445,7 @@ export class DataspaceControlPlaneService {
445
445
  async startTransfer(message, publicOrigin, trustPayload) {
446
446
  const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "startTransfer");
447
447
  const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
448
- if (validationFailures.length > 0) {
448
+ if (Is.arrayValue(validationFailures)) {
449
449
  await this._loggingComponent?.log({
450
450
  level: "error",
451
451
  source: DataspaceControlPlaneService.CLASS_NAME,
@@ -527,7 +527,12 @@ export class DataspaceControlPlaneService {
527
527
  }
528
528
  });
529
529
  const tokenString = accessToken;
530
- const fullEndpoint = `${publicOrigin}/${this._dataPlanePath}`;
530
+ let fullEndpoint = `${publicOrigin}/${this._dataPlanePath}`;
531
+ const contextIds = await ContextIdStore.getContextIds();
532
+ const tenantId = contextIds?.[ContextIdKeys.Tenant];
533
+ if (Is.stringValue(tenantId)) {
534
+ fullEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(fullEndpoint, "tenant", tenantId);
535
+ }
531
536
  response.dataAddress = {
532
537
  "@type": DataspaceProtocolTransferProcessTypes.DataAddress,
533
538
  endpointType: DataspaceProtocolEndpointType.HttpsQueryEndpoint,
@@ -607,7 +612,7 @@ export class DataspaceControlPlaneService {
607
612
  async completeTransfer(message, trustPayload) {
608
613
  const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "completeTransfer");
609
614
  const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
610
- if (validationFailures.length > 0) {
615
+ if (Is.arrayValue(validationFailures)) {
611
616
  await this._loggingComponent?.log({
612
617
  level: "error",
613
618
  source: DataspaceControlPlaneService.CLASS_NAME,
@@ -662,7 +667,7 @@ export class DataspaceControlPlaneService {
662
667
  async suspendTransfer(message, trustPayload) {
663
668
  const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "suspendTransfer");
664
669
  const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
665
- if (validationFailures.length > 0) {
670
+ if (Is.arrayValue(validationFailures)) {
666
671
  await this._loggingComponent?.log({
667
672
  level: "error",
668
673
  source: DataspaceControlPlaneService.CLASS_NAME,
@@ -718,7 +723,7 @@ export class DataspaceControlPlaneService {
718
723
  async terminateTransfer(message, trustPayload) {
719
724
  const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "terminateTransfer");
720
725
  const validationFailures = await DataspaceProtocolHelper.validate(JsonLdHelper.toNodeObject(message));
721
- if (validationFailures.length > 0) {
726
+ if (Is.arrayValue(validationFailures)) {
722
727
  await this._loggingComponent?.log({
723
728
  level: "error",
724
729
  source: DataspaceControlPlaneService.CLASS_NAME,
@@ -1115,6 +1120,144 @@ export class DataspaceControlPlaneService {
1115
1120
  dataAddress: entity.dataAddress
1116
1121
  };
1117
1122
  }
1123
+ /**
1124
+ * Register a dataset for a dataspace app, owned by the calling tenant.
1125
+ * @param id Optional explicit id. If omitted, derived from `dataset["@id"]`
1126
+ * or generated.
1127
+ * @param appId The dataspace app this dataset belongs to.
1128
+ * @param dataset The dataset payload.
1129
+ * @returns The resolved dataset id.
1130
+ */
1131
+ async createAppDataset(id, appId, dataset) {
1132
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "appId", appId);
1133
+ Guards.object(DataspaceControlPlaneService.CLASS_NAME, "dataset", dataset);
1134
+ const resolvedId = id ??
1135
+ (Is.stringValue(dataset["@id"]) ? dataset["@id"] : RandomHelper.generateUuidV7("compact"));
1136
+ const tenantId = await this.resolveCallingTenantId();
1137
+ const nodeIdentity = await this.resolveNodeIdentity();
1138
+ const existing = await this._dataspaceAppDatasetStorage.get(resolvedId);
1139
+ if (!Is.empty(existing)) {
1140
+ throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "datasetAlreadyExists", {
1141
+ id: resolvedId
1142
+ });
1143
+ }
1144
+ const now = new Date().toISOString();
1145
+ const entity = {
1146
+ id: resolvedId,
1147
+ nodeIdentity,
1148
+ tenantId,
1149
+ appId,
1150
+ dataset: this.stripDatasetId(dataset),
1151
+ dateCreated: now,
1152
+ dateModified: now
1153
+ };
1154
+ // Side effect first, primary storage last
1155
+ await this.publishAppDataset(entity);
1156
+ await this._dataspaceAppDatasetStorage.set(entity);
1157
+ return resolvedId;
1158
+ }
1159
+ /**
1160
+ * Get a dataset record owned by the calling tenant.
1161
+ * @param id The stored dataset id.
1162
+ * @returns The stored dataset record.
1163
+ */
1164
+ async getAppDataset(id) {
1165
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
1166
+ const tenantId = await this.resolveCallingTenantId();
1167
+ const entity = await this._dataspaceAppDatasetStorage.get(id);
1168
+ if (Is.empty(entity)) {
1169
+ throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
1170
+ }
1171
+ if (entity.tenantId !== tenantId) {
1172
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
1173
+ }
1174
+ return {
1175
+ id: entity.id,
1176
+ appId: entity.appId,
1177
+ dataset: this.restampDatasetId(entity.dataset, entity.id),
1178
+ dateCreated: entity.dateCreated,
1179
+ dateModified: entity.dateModified
1180
+ };
1181
+ }
1182
+ /**
1183
+ * List the dataspace app datasets owned by the calling tenant.
1184
+ * @param cursor Optional pagination cursor.
1185
+ * @param limit Optional maximum number of entries to return.
1186
+ * @returns The stored datasets and the next-page cursor if more exist.
1187
+ */
1188
+ async listAppDatasets(cursor, limit) {
1189
+ const tenantId = await this.resolveCallingTenantId();
1190
+ const page = await this._dataspaceAppDatasetStorage.query(Is.stringValue(tenantId)
1191
+ ? {
1192
+ property: "tenantId",
1193
+ value: tenantId,
1194
+ comparison: ComparisonOperator.Equals
1195
+ }
1196
+ : undefined, undefined, undefined, cursor, limit);
1197
+ const entities = (page.entities ?? []).map(entity => ({
1198
+ id: entity.id ?? "",
1199
+ appId: entity.appId ?? "",
1200
+ dataset: this.restampDatasetId(entity.dataset ?? {}, entity.id ?? ""),
1201
+ dateCreated: entity.dateCreated ?? "",
1202
+ dateModified: entity.dateModified ?? ""
1203
+ }));
1204
+ return {
1205
+ entities,
1206
+ cursor: page.cursor
1207
+ };
1208
+ }
1209
+ /**
1210
+ * Update a dataset record owned by the calling tenant.
1211
+ * @param id The stored dataset id.
1212
+ * @param appId The dataspace app this dataset belongs to.
1213
+ * @param dataset The dataset payload.
1214
+ */
1215
+ async updateAppDataset(id, appId, dataset) {
1216
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
1217
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "appId", appId);
1218
+ Guards.object(DataspaceControlPlaneService.CLASS_NAME, "dataset", dataset);
1219
+ const tenantId = await this.resolveCallingTenantId();
1220
+ const existing = await this._dataspaceAppDatasetStorage.get(id);
1221
+ if (Is.empty(existing)) {
1222
+ throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
1223
+ }
1224
+ if (existing.tenantId !== tenantId) {
1225
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
1226
+ }
1227
+ const updated = {
1228
+ ...existing,
1229
+ appId,
1230
+ dataset: this.stripDatasetId(dataset),
1231
+ dateModified: new Date().toISOString()
1232
+ };
1233
+ // Side effect first, primary storage last
1234
+ await this.publishAppDataset(updated);
1235
+ await this._dataspaceAppDatasetStorage.set(updated);
1236
+ }
1237
+ /**
1238
+ * Delete a dataspace app dataset owned by the calling tenant.
1239
+ * @param id The stored app dataset id.
1240
+ */
1241
+ async deleteAppDataset(id) {
1242
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "id", id);
1243
+ const tenantId = await this.resolveCallingTenantId();
1244
+ const existing = await this._dataspaceAppDatasetStorage.get(id);
1245
+ if (Is.empty(existing)) {
1246
+ throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "datasetNotFound", id);
1247
+ }
1248
+ if (existing.tenantId !== tenantId) {
1249
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "datasetWrongTenant");
1250
+ }
1251
+ // Side effect first, primary storage last
1252
+ const wrappedContextIds = {
1253
+ ...((await ContextIdStore.getContextIds()) ?? {}),
1254
+ [ContextIdKeys.Tenant]: existing.tenantId
1255
+ };
1256
+ await ContextIdStore.run(wrappedContextIds, async () => {
1257
+ await this._federatedCatalogueComponent.remove(id);
1258
+ });
1259
+ await this._dataspaceAppDatasetStorage.remove(id);
1260
+ }
1118
1261
  // ============================================================================
1119
1262
  // PRIVATE HELPER METHODS
1120
1263
  // ============================================================================
@@ -1155,7 +1298,7 @@ export class DataspaceControlPlaneService {
1155
1298
  }
1156
1299
  }
1157
1300
  }
1158
- if (stalled.length > 0) {
1301
+ if (Is.arrayValue(stalled)) {
1159
1302
  await this._loggingComponent?.log({
1160
1303
  level: "info",
1161
1304
  source: DataspaceControlPlaneService.CLASS_NAME,
@@ -1283,20 +1426,21 @@ export class DataspaceControlPlaneService {
1283
1426
  agreementId: OdrlPolicyHelper.getUid(agreement)
1284
1427
  });
1285
1428
  }
1286
- const targetIds = OdrlPolicyHelper.getTargets(agreement);
1287
- if (targetIds.length === 0) {
1429
+ // Top-level target identifies the dataset; rule-level targets are constraint
1430
+ // scopes (refinements, JSONPath filters, AssetCollections) and aren't datasets.
1431
+ const datasetTargets = OdrlPolicyHelper.getDatasetTargets(agreement);
1432
+ if (!Is.arrayValue(datasetTargets)) {
1288
1433
  throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "agreementTargetMissingUid", {
1289
1434
  agreementId: OdrlPolicyHelper.getUid(agreement)
1290
1435
  });
1291
1436
  }
1292
- // Validate single target, multiple targets are not currently supported
1293
- if (targetIds.length > 1) {
1437
+ if (datasetTargets.length > 1) {
1294
1438
  throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "agreementMultipleTargetsNotSupported", {
1295
1439
  agreementId: OdrlPolicyHelper.getUid(agreement),
1296
- targetCount: targetIds.length
1440
+ targetCount: datasetTargets.length
1297
1441
  });
1298
1442
  }
1299
- return targetIds[0];
1443
+ return datasetTargets[0];
1300
1444
  }
1301
1445
  /**
1302
1446
  * Validate that the dataset exists in the Federated Catalogue.
@@ -1478,12 +1622,12 @@ export class DataspaceControlPlaneService {
1478
1622
  // the target is implicitly the Dataset. When the catalogue offer has no targets,
1479
1623
  // skip the target comparison entirely (the agreement's target is the dataset itself).
1480
1624
  // Only reject if both have explicit targets that don't overlap.
1481
- if (offerTargets.length > 0 && agreementTargets.length > 0) {
1625
+ if (Is.arrayValue(offerTargets) && Is.arrayValue(agreementTargets)) {
1482
1626
  if (!agreementTargets.some((agreementTarget) => offerTargets.includes(agreementTarget))) {
1483
1627
  return false;
1484
1628
  }
1485
1629
  }
1486
- else if (offerTargets.length > 0 && agreementTargets.length === 0) {
1630
+ else if (Is.arrayValue(offerTargets) && !Is.arrayValue(agreementTargets)) {
1487
1631
  // Offer has targets but agreement doesn't — mismatch
1488
1632
  return false;
1489
1633
  }
@@ -1502,24 +1646,92 @@ export class DataspaceControlPlaneService {
1502
1646
  return true;
1503
1647
  }
1504
1648
  /**
1505
- * Ensure the dataset has a publisher set.
1506
- * @param dataset The dataset.
1507
- * @returns The dataset with publisher set.
1649
+ * Populate the system-controlled fields of a dataset payload.
1650
+ * @param dataset The user-supplied dataset payload.
1651
+ * @returns The populated dataset.
1508
1652
  * @internal
1509
1653
  */
1510
- async ensurePublisher(dataset) {
1654
+ async populateDefaults(dataset) {
1511
1655
  if (dataset["dcterms:publisher"]) {
1512
1656
  return dataset;
1513
1657
  }
1514
1658
  const contextIds = await ContextIdStore.getContextIds();
1515
1659
  const orgId = contextIds?.[ContextIdKeys.Organization];
1516
1660
  if (orgId) {
1517
- return {
1518
- ...dataset,
1519
- "dcterms:publisher": orgId
1520
- };
1661
+ return { ...dataset, "dcterms:publisher": orgId };
1521
1662
  }
1522
1663
  return dataset;
1523
1664
  }
1665
+ /**
1666
+ * Strip `@id` from a dataset payload before storing it.
1667
+ * @param dataset The dataset payload.
1668
+ * @returns The dataset payload with `@id` removed.
1669
+ * @internal
1670
+ */
1671
+ stripDatasetId(dataset) {
1672
+ const copy = { ...dataset };
1673
+ delete copy["@id"];
1674
+ return copy;
1675
+ }
1676
+ /**
1677
+ * Re-stamp `@id` onto a stored payload blob using the entity primary key.
1678
+ * @param payload The stored payload blob (without `@id`).
1679
+ * @param id The entity id (becomes the dataset's `@id`).
1680
+ * @returns The dataset payload with `@id` populated.
1681
+ * @internal
1682
+ */
1683
+ restampDatasetId(payload, id) {
1684
+ return { ...payload, "@id": id };
1685
+ }
1686
+ // DATASPACE APP DATASET HANDLERS
1687
+ /**
1688
+ * Publish a single dataspace app dataset to the federated catalogue in its owning tenant's context.
1689
+ * @param appDataset The stored dataspace app dataset entity.
1690
+ * @internal
1691
+ */
1692
+ async publishAppDataset(appDataset) {
1693
+ // Override `Tenant` in the context wrap so federated catalogue captures the
1694
+ // appDataset's owning tenant. On single-tenant nodes the appDataset has no tenantId,
1695
+ // so the assignment writes `Tenant: undefined` — equivalent to no override.
1696
+ const wrappedContextIds = {
1697
+ ...((await ContextIdStore.getContextIds()) ?? {}),
1698
+ [ContextIdKeys.Tenant]: appDataset.tenantId
1699
+ };
1700
+ await ContextIdStore.run(wrappedContextIds, async () => {
1701
+ const app = DataspaceAppFactory.get(appDataset.appId);
1702
+ const datasetPayload = this.restampDatasetId(appDataset.dataset, appDataset.id);
1703
+ const overrideHandler = app.datasetsHandled?.bind(app);
1704
+ const rawDatasets = overrideHandler
1705
+ ? await overrideHandler(datasetPayload, appDataset.tenantId ?? "")
1706
+ : [datasetPayload];
1707
+ const datasets = await Promise.all(rawDatasets.map(async (d) => this.populateDefaults(d)));
1708
+ for (const dataset of datasets) {
1709
+ await this._federatedCatalogueComponent.set(dataset);
1710
+ }
1711
+ });
1712
+ }
1713
+ /**
1714
+ * Resolve the calling tenant from the request context.
1715
+ * @returns The owning tenant id, or undefined for single-tenant nodes.
1716
+ * @internal
1717
+ */
1718
+ async resolveCallingTenantId() {
1719
+ const contextIds = await ContextIdStore.getContextIds();
1720
+ const tenantId = contextIds?.[ContextIdKeys.Tenant];
1721
+ return Is.stringValue(tenantId) ? tenantId : undefined;
1722
+ }
1723
+ /**
1724
+ * Resolve the node identity from the current context, throwing if absent.
1725
+ * @returns The owning node identity.
1726
+ * @internal
1727
+ */
1728
+ async resolveNodeIdentity() {
1729
+ const contextIds = await ContextIdStore.getContextIds();
1730
+ const nodeId = contextIds?.[ContextIdKeys.Node];
1731
+ if (!Is.stringValue(nodeId)) {
1732
+ throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "datasetNodeContextRequired");
1733
+ }
1734
+ return nodeId;
1735
+ }
1524
1736
  }
1525
1737
  //# sourceMappingURL=dataspaceControlPlaneService.js.map