@twin.org/dataspace-control-plane-service 0.0.3-next.48 → 0.0.3-next.50

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.
@@ -4,12 +4,14 @@ import { HttpContextIdKeys, HttpUrlHelper } from "@twin.org/api-models";
4
4
  import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@twin.org/context";
5
5
  import { AlreadyExistsError, ArrayHelper, BaseError, ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, ObjectHelper, RandomHelper, StringHelper, UnauthorizedError, Url, Urn, ValidationError } from "@twin.org/core";
6
6
  import { JsonLdHelper } from "@twin.org/data-json-ld";
7
- import { DataspaceAppFactory, DataspaceTransferFormat, TransferProcessRole, getJsonLdId, getJsonLdType } from "@twin.org/dataspace-models";
7
+ import { DataspaceControlPlaneMetricIds, DataspaceControlPlaneMetrics, DataspaceTransferFormat, TransferProcessRole, getJsonLdId, getJsonLdType } from "@twin.org/dataspace-models";
8
8
  import { EngineCoreFactory } from "@twin.org/engine-models";
9
9
  import { ComparisonOperator } from "@twin.org/entity";
10
10
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
11
11
  import { OdrlPolicyHelper, PolicyRequesterFactory } from "@twin.org/rights-management-models";
12
12
  import { DataspaceProtocolContexts, DataspaceProtocolContractNegotiationTypes, DataspaceProtocolEndpointType, DataspaceProtocolHelper, DataspaceProtocolTransferProcessStateType, DataspaceProtocolTransferProcessTypes } from "@twin.org/standards-dataspace-protocol";
13
+ import { OdrlPolicyType } from "@twin.org/standards-w3c-odrl";
14
+ import { MetricHelper } from "@twin.org/telemetry-models";
13
15
  import { TrustHelper } from "@twin.org/trust-models";
14
16
  import { DataspaceControlPlanePolicyRequester } from "./dataspaceControlPlanePolicyRequester.js";
15
17
  import { EndpointProperties } from "./models/endpointProperties.js";
@@ -120,6 +122,11 @@ export class DataspaceControlPlaneService {
120
122
  * @internal
121
123
  */
122
124
  _platformComponent;
125
+ /**
126
+ * The optional telemetry component for metrics.
127
+ * @internal
128
+ */
129
+ _telemetryComponent;
123
130
  /**
124
131
  * Registered negotiation callbacks from upstream callers, keyed by registration key.
125
132
  * @internal
@@ -169,6 +176,7 @@ export class DataspaceControlPlaneService {
169
176
  : undefined;
170
177
  this._taskScheduler = ComponentFactory.getIfExists(options?.taskSchedulerComponentType ?? "task-scheduler");
171
178
  this._platformComponent = ComponentFactory.getIfExists(options?.platformComponentType ?? "platform");
179
+ this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
172
180
  // Data plane component is optional and resolved lazily. The control plane is initialised
173
181
  // BEFORE the data plane in engine startup order, so `getRegisteredInstanceTypeOptional`
174
182
  // returns undefined when the engine constructs us — the wiring override can't fire here.
@@ -235,6 +243,7 @@ export class DataspaceControlPlaneService {
235
243
  * @returns A promise that resolves when the federated catalogue is populated and the cleanup task is scheduled.
236
244
  */
237
245
  async start(nodeLoggingComponentType) {
246
+ await MetricHelper.createMetrics(this._telemetryComponent, DataspaceControlPlaneMetrics);
238
247
  const engine = EngineCoreFactory.getIfExists("engine");
239
248
  // Skip if no engine exists OR if this is a clone instance
240
249
  if (Is.empty(engine) || engine.isClone()) {
@@ -344,7 +353,7 @@ export class DataspaceControlPlaneService {
344
353
  * @param options Request options.
345
354
  * @param options.autoStart When true, the provider immediately starts the requested transfer (scheduled
346
355
  * on the next tick); when omitted/false the provider start must be triggered explicitly.
347
- * @param trustPayload Trust payload containing authorization information (Base64-encoded token).
356
+ * @param trustPayload Trust payload containing authorization information.
348
357
  * @returns Transfer Process (DSP compliant) with state REQUESTED, or TransferError if the operation fails.
349
358
  *
350
359
  * Role Performed: Provider
@@ -377,14 +386,10 @@ export class DataspaceControlPlaneService {
377
386
  ts: Date.now(),
378
387
  message: "agreementNotFound",
379
388
  data: {
380
- agreementId: request.agreementId,
381
- hint: "Agreement must exist before transfer. Use contract negotiation to create agreement first."
389
+ notFoundId: request.agreementId
382
390
  }
383
391
  });
384
- throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "agreementNotFound", request.agreementId, {
385
- agreementId: request.agreementId,
386
- hint: "Perform contract negotiation first to create an agreement"
387
- });
392
+ throw new NotFoundError(DataspaceControlPlaneService.CLASS_NAME, "agreementNotFound", request.agreementId);
388
393
  }
389
394
  const assigneeIdentity = OdrlPolicyHelper.extractAssigneeIdentity(agreement);
390
395
  const assigneeIds = ArrayHelper.fromObjectOrArray(assigneeIdentity);
@@ -453,6 +458,7 @@ export class DataspaceControlPlaneService {
453
458
  await this.runProviderStart(consumerPid, contextIds?.[HttpContextIdKeys.PublicOrigin]);
454
459
  }, 0);
455
460
  }
461
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.TransfersRequested);
456
462
  return {
457
463
  "@context": [DataspaceProtocolContexts.Context],
458
464
  "@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
@@ -601,6 +607,7 @@ export class DataspaceControlPlaneService {
601
607
  format
602
608
  }
603
609
  });
610
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.TransfersPrepared);
604
611
  return { consumerPid };
605
612
  }
606
613
  // ----------------------------------------------------------------------------
@@ -610,7 +617,7 @@ export class DataspaceControlPlaneService {
610
617
  * Start a Transfer Process.
611
618
  * Transitions Transfer Process from REQUESTED to STARTED state or resumes from SUSPENDED state.
612
619
  * @param message Transfer start message (DSP compliant).
613
- * @param trustPayload Trust payload containing authorization information (Base64-encoded token).
620
+ * @param trustPayload Trust payload containing authorization information.
614
621
  * @returns Transfer Start Message (DSP compliant) with dataAddress for PULL transfers, or TransferError if the operation fails.
615
622
  *
616
623
  * Role Performed: Provider / Consumer
@@ -668,6 +675,7 @@ export class DataspaceControlPlaneService {
668
675
  });
669
676
  await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.STARTED);
670
677
  await this._internalTransferCallback.onStarted(entity.consumerPid, message);
678
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.TransfersStarted);
671
679
  return message;
672
680
  }
673
681
  // PROVIDER-ACT (role === Provider): build the dataAddress, transition to STARTED, deliver below.
@@ -890,6 +898,7 @@ export class DataspaceControlPlaneService {
890
898
  // Notify the consumer by POSTing the TransferStart (with its dataAddress) to the callback.
891
899
  // Best-effort: a delivery failure must not roll back the STARTED transition.
892
900
  await this.deliverToConsumerCallback(entity, "start", async (remoteControlPlane, token) => remoteControlPlane.startTransfer(response, token));
901
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.TransfersStarted);
893
902
  return response;
894
903
  }
895
904
  catch (error) {
@@ -945,7 +954,7 @@ export class DataspaceControlPlaneService {
945
954
  /**
946
955
  * Complete a Transfer Process.
947
956
  * @param message Transfer completion message (DSP compliant).
948
- * @param trustPayload Trust payload containing authorization information (Base64-encoded token).
957
+ * @param trustPayload Trust payload containing authorization information.
949
958
  * @returns Transfer Process (DSP compliant) with state COMPLETED, or TransferError if the operation fails.
950
959
  */
951
960
  async completeTransfer(message, trustPayload) {
@@ -1025,6 +1034,7 @@ export class DataspaceControlPlaneService {
1025
1034
  await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.COMPLETED);
1026
1035
  await this._internalTransferCallback.onCompleted(entity.consumerPid);
1027
1036
  }
1037
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.TransfersCompleted);
1028
1038
  return {
1029
1039
  "@context": [DataspaceProtocolContexts.Context],
1030
1040
  "@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
@@ -1040,7 +1050,7 @@ export class DataspaceControlPlaneService {
1040
1050
  /**
1041
1051
  * Suspend a Transfer Process.
1042
1052
  * @param message Transfer suspension message (DSP compliant).
1043
- * @param trustPayload Trust payload containing authorization information (Base64-encoded token).
1053
+ * @param trustPayload Trust payload containing authorization information.
1044
1054
  * @returns Transfer Process (DSP compliant) with state SUSPENDED, or TransferError if the operation fails.
1045
1055
  */
1046
1056
  async suspendTransfer(message, trustPayload) {
@@ -1127,6 +1137,7 @@ export class DataspaceControlPlaneService {
1127
1137
  // PROVIDER-ACT: forward the suspension (with its reason) to the consumer callback.
1128
1138
  await this.deliverToConsumerCallback(entity, "suspension", async (remoteControlPlane, token) => remoteControlPlane.suspendTransfer(message, token));
1129
1139
  }
1140
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.TransfersSuspended);
1130
1141
  return {
1131
1142
  "@context": [DataspaceProtocolContexts.Context],
1132
1143
  "@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
@@ -1142,7 +1153,7 @@ export class DataspaceControlPlaneService {
1142
1153
  /**
1143
1154
  * Terminate a Transfer Process.
1144
1155
  * @param message Transfer termination message (DSP compliant).
1145
- * @param trustPayload Trust payload containing authorization information (Base64-encoded token).
1156
+ * @param trustPayload Trust payload containing authorization information.
1146
1157
  * @returns Transfer Process (DSP compliant) with state TERMINATED, or TransferError if the operation fails.
1147
1158
  */
1148
1159
  async terminateTransfer(message, trustPayload) {
@@ -1223,6 +1234,7 @@ export class DataspaceControlPlaneService {
1223
1234
  // PROVIDER-ACT: forward the termination (with its reason) to the consumer callback.
1224
1235
  await this.deliverToConsumerCallback(entity, "termination", async (remoteControlPlane, token) => remoteControlPlane.terminateTransfer(message, token));
1225
1236
  }
1237
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.TransfersTerminated);
1226
1238
  return {
1227
1239
  "@context": [DataspaceProtocolContexts.Context],
1228
1240
  "@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
@@ -1238,7 +1250,7 @@ export class DataspaceControlPlaneService {
1238
1250
  /**
1239
1251
  * Get Transfer Process state.
1240
1252
  * @param pid Process ID (consumerPid or providerPid).
1241
- * @param trustPayload Trust payload containing authorization information (Base64-encoded token).
1253
+ * @param trustPayload Trust payload containing authorization information.
1242
1254
  * @returns Transfer Process (DSP compliant) with current state, or TransferError if the operation fails.
1243
1255
  */
1244
1256
  async getTransferProcess(pid, trustPayload) {
@@ -1284,7 +1296,7 @@ export class DataspaceControlPlaneService {
1284
1296
  * @param offerId The offer ID from the provider's catalog.
1285
1297
  * @param providerEndpoint The provider's contract negotiation endpoint URL.
1286
1298
  * @param trustPayload The trust payload for authentication.
1287
- * @returns The negotiation ID. Use the registered callback for completion notification.
1299
+ * @returns For implicit trust: `{ agreementId }`. For external negotiation: `{ negotiationId }`.
1288
1300
  */
1289
1301
  async negotiateAgreement(datasetId, offerId, providerEndpoint, trustPayload) {
1290
1302
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "datasetId", datasetId);
@@ -1364,6 +1376,9 @@ export class DataspaceControlPlaneService {
1364
1376
  offerType: getJsonLdType(matchingOffer)
1365
1377
  }
1366
1378
  });
1379
+ if (trustInfo.identity === organizationId) {
1380
+ return this.negotiateImplicitTrustAgreement(organizationId, datasetId);
1381
+ }
1367
1382
  const negotiationId = await this._policyNegotiationPointComponent.sendRequestToProvider(providerEndpoint, DataspaceControlPlaneService._REQUESTER_TYPE, offerId, publicOrigin);
1368
1383
  if (!negotiationId) {
1369
1384
  throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "negotiationInitiationFailed", {
@@ -1372,6 +1387,7 @@ export class DataspaceControlPlaneService {
1372
1387
  });
1373
1388
  }
1374
1389
  this._policyRequester.trackNegotiation(negotiationId);
1390
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.NegotiationsInitiated);
1375
1391
  await this._loggingComponent?.log({
1376
1392
  level: "info",
1377
1393
  source: DataspaceControlPlaneService.CLASS_NAME,
@@ -1464,7 +1480,7 @@ export class DataspaceControlPlaneService {
1464
1480
  /**
1465
1481
  * Resolve consumerPid to Transfer Context.
1466
1482
  * @param consumerPid Consumer Process ID.
1467
- * @param trustPayload Trust payload containing authorization information (Base64-encoded token).
1483
+ * @param trustPayload Trust payload containing authorization information.
1468
1484
  * @returns Transfer Context with Agreement, datasetId, and Transfer Process metadata.
1469
1485
  */
1470
1486
  async resolveConsumerPid(consumerPid, trustPayload) {
@@ -1521,7 +1537,7 @@ export class DataspaceControlPlaneService {
1521
1537
  /**
1522
1538
  * Resolve providerPid to Transfer Context.
1523
1539
  * @param providerPid Provider Process ID.
1524
- * @param trustPayload Trust payload containing authorization information (Base64-encoded token).
1540
+ * @param trustPayload Trust payload containing authorization information.
1525
1541
  * @returns Transfer Context with Agreement, datasetId, and Transfer Process metadata.
1526
1542
  */
1527
1543
  async resolveProviderPid(providerPid, trustPayload) {
@@ -1631,6 +1647,7 @@ export class DataspaceControlPlaneService {
1631
1647
  // Side effect first, primary storage last
1632
1648
  await this.publishAppDataset(entity);
1633
1649
  await this._dataspaceAppDatasetStorage.set(entity);
1650
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.AppDatasetsCreated);
1634
1651
  return resolvedId;
1635
1652
  }
1636
1653
  /**
@@ -1709,6 +1726,7 @@ export class DataspaceControlPlaneService {
1709
1726
  // Side effect first, primary storage last
1710
1727
  await this.publishAppDataset(updated);
1711
1728
  await this._dataspaceAppDatasetStorage.set(updated);
1729
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.AppDatasetsUpdated);
1712
1730
  }
1713
1731
  /**
1714
1732
  * Delete a dataspace app dataset owned by the calling organization.
@@ -1735,6 +1753,7 @@ export class DataspaceControlPlaneService {
1735
1753
  });
1736
1754
  }
1737
1755
  await this._dataspaceAppDatasetStorage.remove(id);
1756
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.AppDatasetsDeleted);
1738
1757
  }
1739
1758
  // ============================================================================
1740
1759
  // PRIVATE HELPER METHODS
@@ -2438,12 +2457,8 @@ export class DataspaceControlPlaneService {
2438
2457
  const contextIds = await ContextIdStore.getContextIds();
2439
2458
  const tenantContextIds = { ...contextIds, [ContextIdKeys.Tenant]: appDataset.tenantId };
2440
2459
  await ContextIdStore.run(tenantContextIds, async () => {
2441
- const app = DataspaceAppFactory.get(appDataset.appId);
2442
2460
  const datasetPayload = this.restampDatasetId(appDataset.dataset, appDataset.id);
2443
- const overrideHandler = app.datasetsHandled?.bind(app);
2444
- const rawDatasets = overrideHandler
2445
- ? await overrideHandler(datasetPayload)
2446
- : [datasetPayload];
2461
+ const rawDatasets = [datasetPayload];
2447
2462
  const datasets = await Promise.all(rawDatasets.map(async (d) => this.populateDefaults(d)));
2448
2463
  for (const dataset of datasets) {
2449
2464
  const publishResult = await this._federatedCatalogueComponent.set(dataset, localTrustPayload);
@@ -2477,5 +2492,67 @@ export class DataspaceControlPlaneService {
2477
2492
  const contextIds = await ContextIdStore.getContextIds();
2478
2493
  return contextIds?.[ContextIdKeys.Tenant];
2479
2494
  }
2495
+ /**
2496
+ * Return an existing full-access agreement for the same-organization (implicit trust) case,
2497
+ * or create and store one if none exists. Fires onFinalized on all registered callbacks in
2498
+ * both cases.
2499
+ * @param organizationId The local organization ID (both assigner and assignee).
2500
+ * @param datasetId The dataset being granted access to.
2501
+ * @returns The agreement ID.
2502
+ * @internal
2503
+ */
2504
+ async negotiateImplicitTrustAgreement(organizationId, datasetId) {
2505
+ const { policies } = await this._policyAdministrationPointComponent.query({
2506
+ type: OdrlPolicyType.Agreement,
2507
+ assigner: organizationId,
2508
+ assignee: organizationId,
2509
+ target: datasetId
2510
+ });
2511
+ let agreementId;
2512
+ if (policies.length > 0) {
2513
+ agreementId = OdrlPolicyHelper.getUid(policies[0]);
2514
+ await this._loggingComponent?.log({
2515
+ level: "info",
2516
+ source: DataspaceControlPlaneService.CLASS_NAME,
2517
+ ts: Date.now(),
2518
+ message: "implicitTrustAgreementReused",
2519
+ data: { agreementId, datasetId, organizationId }
2520
+ });
2521
+ }
2522
+ else {
2523
+ const implicitAgreement = {
2524
+ "@context": "http://www.w3.org/ns/odrl.jsonld",
2525
+ "@type": "Agreement",
2526
+ assigner: organizationId,
2527
+ assignee: organizationId,
2528
+ target: datasetId,
2529
+ permission: [{ action: "use" }]
2530
+ };
2531
+ agreementId = await this._policyAdministrationPointComponent.create(implicitAgreement);
2532
+ await MetricHelper.metricIncrement(this._telemetryComponent, DataspaceControlPlaneMetricIds.NegotiationsInitiated);
2533
+ await this._loggingComponent?.log({
2534
+ level: "info",
2535
+ source: DataspaceControlPlaneService.CLASS_NAME,
2536
+ ts: Date.now(),
2537
+ message: "implicitTrustAgreementCreated",
2538
+ data: { agreementId, datasetId, organizationId }
2539
+ });
2540
+ }
2541
+ for (const [key, cb] of this._negotiationCallbacks.entries()) {
2542
+ try {
2543
+ await cb.onFinalized(undefined, agreementId);
2544
+ }
2545
+ catch (error) {
2546
+ await this._loggingComponent?.log({
2547
+ level: "error",
2548
+ source: DataspaceControlPlaneService.CLASS_NAME,
2549
+ ts: Date.now(),
2550
+ message: "negotiationCallbackError",
2551
+ data: { key, negotiationId: agreementId, method: "onFinalized", error }
2552
+ });
2553
+ }
2554
+ }
2555
+ return { agreementId };
2556
+ }
2480
2557
  }
2481
2558
  //# sourceMappingURL=dataspaceControlPlaneService.js.map