@twin.org/dataspace-control-plane-service 0.0.3-next.32 → 0.0.3-next.34

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.
@@ -168,7 +168,8 @@ export class DataspaceControlPlanePolicyRequester {
168
168
  }
169
169
  });
170
170
  if (agreementId) {
171
- await this._callback?.onCompleted(negotiationId, agreementId);
171
+ await this._callback?.onStateChanged(negotiationId, DataspaceProtocolContractNegotiationStateType.FINALIZED, { agreement: negotiation.agreement });
172
+ await this._callback?.onFinalized(negotiationId, agreementId);
172
173
  }
173
174
  else {
174
175
  await this._callback?.onFailed(negotiationId, "negotiationFinalizedNoAgreement");
@@ -1 +1 @@
1
- {"version":3,"file":"dataspaceControlPlanePolicyRequester.js","sourceRoot":"","sources":["../../src/dataspaceControlPlanePolicyRequester.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { ComponentFactory } from \"@twin.org/core\";\nimport type { INegotiationCallback } from \"@twin.org/dataspace-models\";\nimport type { ILoggingComponent } from \"@twin.org/logging-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { OdrlPolicyHelper, type IPolicyRequester } from \"@twin.org/rights-management-models\";\nimport {\n\tDataspaceProtocolContractNegotiationStateType,\n\ttype IDataspaceProtocolAgreement,\n\ttype IDataspaceProtocolOffer\n} from \"@twin.org/standards-dataspace-protocol\";\nimport type { INegotiationState } from \"./models/INegotiationState.js\";\n\n/**\n * Policy Requester for Dataspace Control Plane.\n *\n * Handles contract negotiation callbacks from PNP and forwards state changes\n * to the control plane service via the INegotiationCallback interface.\n *\n * Callback Flow:\n * 1. Control Plane initiates negotiation via PNP.sendRequestToProvider()\n * 2. PNP calls offer() → auto-accept, notify control plane\n * 3. PNP calls agreement() → store agreement, notify control plane\n * 4. PNP calls finalised() → notify control plane with agreementId\n * 5. Control plane notifies upstream caller (e.g. supply-chain)\n */\nexport class DataspaceControlPlanePolicyRequester implements IPolicyRequester {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<DataspaceControlPlanePolicyRequester>();\n\n\t/**\n\t * The logging component.\n\t * @internal\n\t */\n\tprivate readonly _loggingComponent?: ILoggingComponent;\n\n\t/**\n\t * Callback interface for notifying the control plane of negotiation state changes.\n\t * @internal\n\t */\n\tprivate readonly _callback?: INegotiationCallback;\n\n\t/**\n\t * Active negotiations tracked in memory.\n\t * Key: negotiationId\n\t * Value: Negotiation state\n\t * @internal\n\t */\n\tprivate readonly _negotiations: Map<string, INegotiationState>;\n\n\t/**\n\t * Create a new instance of DataspaceControlPlanePolicyRequester.\n\t * @param loggingComponentType Optional logging component type.\n\t * @param callback Optional callback interface for state change notifications.\n\t */\n\tconstructor(loggingComponentType?: string, callback?: INegotiationCallback) {\n\t\tthis._loggingComponent = ComponentFactory.getIfExists<ILoggingComponent>(\n\t\t\tloggingComponentType ?? \"logging\"\n\t\t);\n\t\tthis._callback = callback;\n\t\tthis._negotiations = new Map();\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn DataspaceControlPlanePolicyRequester.CLASS_NAME;\n\t}\n\n\t/**\n\t * Register a negotiation for tracking.\n\t * Called by the control plane service after initiating a negotiation via PNP.\n\t * @param negotiationId The negotiation ID returned by PNP.sendRequestToProvider().\n\t */\n\tpublic trackNegotiation(negotiationId: string): void {\n\t\tconst now = Date.now();\n\t\tthis._negotiations.set(negotiationId, {\n\t\t\tnegotiationId,\n\t\t\tstate: DataspaceProtocolContractNegotiationStateType.REQUESTED,\n\t\t\tstartedAt: now,\n\t\t\tupdatedAt: now\n\t\t});\n\t}\n\n\t/**\n\t * Get all active negotiations (for stalled cleanup).\n\t * @returns Map of negotiationId to negotiation state.\n\t */\n\tpublic getActiveNegotiations(): Map<string, INegotiationState> {\n\t\treturn this._negotiations;\n\t}\n\n\t/**\n\t * Remove a negotiation from tracking (for cleanup).\n\t * @param negotiationId The negotiation ID to remove.\n\t */\n\tpublic removeNegotiation(negotiationId: string): void {\n\t\tthis._negotiations.delete(negotiationId);\n\t}\n\n\t/**\n\t * A policy has been offered by a provider.\n\t * Called by PNP when provider sends an OfferMessage.\n\t * @param negotiationId The id of the negotiation.\n\t * @param offer The offer sent by the provider.\n\t * @returns True if the offer was accepted, false otherwise.\n\t */\n\tpublic async offer(negotiationId: string, offer: IDataspaceProtocolOffer): Promise<boolean> {\n\t\tawait this._loggingComponent?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\tts: Date.now(),\n\t\t\tmessage: \"offerReceived\",\n\t\t\tdata: {\n\t\t\t\tnegotiationId,\n\t\t\t\tofferId: offer[\"@id\"]\n\t\t\t}\n\t\t});\n\n\t\tconst negotiation = this._negotiations.get(negotiationId);\n\t\tif (negotiation) {\n\t\t\tnegotiation.state = DataspaceProtocolContractNegotiationStateType.OFFERED;\n\t\t\tnegotiation.updatedAt = Date.now();\n\t\t}\n\n\t\tawait this._callback?.onStateChanged(\n\t\t\tnegotiationId,\n\t\t\tDataspaceProtocolContractNegotiationStateType.OFFERED,\n\t\t\t{ offer }\n\t\t);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * A policy agreement has been sent by a provider.\n\t * Called by PNP when provider sends an AgreementMessage.\n\t * @param negotiationId The id of the negotiation.\n\t * @param agreement The agreement sent by the provider.\n\t * @returns True if the agreement was accepted, false otherwise.\n\t */\n\tpublic async agreement(\n\t\tnegotiationId: string,\n\t\tagreement: IDataspaceProtocolAgreement\n\t): Promise<boolean> {\n\t\tawait this._loggingComponent?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\tts: Date.now(),\n\t\t\tmessage: \"agreementReceived\",\n\t\t\tdata: {\n\t\t\t\tnegotiationId,\n\t\t\t\tagreementId: OdrlPolicyHelper.getUid(agreement) ?? \"\"\n\t\t\t}\n\t\t});\n\n\t\tconst negotiation = this._negotiations.get(negotiationId);\n\t\tif (negotiation) {\n\t\t\tnegotiation.state = DataspaceProtocolContractNegotiationStateType.AGREED;\n\t\t\tnegotiation.agreement = agreement;\n\t\t\tnegotiation.updatedAt = Date.now();\n\t\t}\n\n\t\tawait this._callback?.onStateChanged(\n\t\t\tnegotiationId,\n\t\t\tDataspaceProtocolContractNegotiationStateType.AGREED,\n\t\t\t{ agreement }\n\t\t);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * A policy finalisation has been sent by a provider.\n\t * Called by PNP when provider sends a FinalizedEvent.\n\t * @param negotiationId The id of the negotiation.\n\t * @returns Nothing.\n\t */\n\tpublic async finalised(negotiationId: string): Promise<void> {\n\t\tawait this._loggingComponent?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\tts: Date.now(),\n\t\t\tmessage: \"negotiationFinalized\",\n\t\t\tdata: { negotiationId }\n\t\t});\n\n\t\tconst negotiation = this._negotiations.get(negotiationId);\n\t\tif (negotiation) {\n\t\t\tnegotiation.state = DataspaceProtocolContractNegotiationStateType.FINALIZED;\n\t\t\tnegotiation.updatedAt = Date.now();\n\n\t\t\tconst agreementId = OdrlPolicyHelper.getUid(negotiation.agreement);\n\t\t\tthis._negotiations.delete(negotiationId);\n\n\t\t\tawait this._loggingComponent?.log({\n\t\t\t\tlevel: \"info\",\n\t\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\t\tts: Date.now(),\n\t\t\t\tmessage: \"negotiationCompleted\",\n\t\t\t\tdata: {\n\t\t\t\t\tnegotiationId,\n\t\t\t\t\tagreementId,\n\t\t\t\t\tdurationMs: Date.now() - negotiation.startedAt\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tif (agreementId) {\n\t\t\t\tawait this._callback?.onCompleted(negotiationId, agreementId);\n\t\t\t} else {\n\t\t\t\tawait this._callback?.onFailed(negotiationId, \"negotiationFinalizedNoAgreement\");\n\t\t\t}\n\t\t} else {\n\t\t\tawait this._loggingComponent?.log({\n\t\t\t\tlevel: \"warn\",\n\t\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\t\tts: Date.now(),\n\t\t\t\tmessage: \"unknownNegotiationFinalized\",\n\t\t\t\tdata: { negotiationId }\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * A policy termination has been sent by a provider.\n\t * Called by PNP when provider sends a TerminatedMessage or negotiation fails.\n\t * @param negotiationId The id of the negotiation.\n\t * @returns Nothing.\n\t */\n\tpublic async terminated(negotiationId: string): Promise<void> {\n\t\tawait this._loggingComponent?.log({\n\t\t\tlevel: \"warn\",\n\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\tts: Date.now(),\n\t\t\tmessage: \"negotiationTerminated\",\n\t\t\tdata: { negotiationId }\n\t\t});\n\n\t\tconst negotiation = this._negotiations.get(negotiationId);\n\t\tif (negotiation) {\n\t\t\tnegotiation.state = DataspaceProtocolContractNegotiationStateType.TERMINATED;\n\t\t\tnegotiation.updatedAt = Date.now();\n\n\t\t\tthis._negotiations.delete(negotiationId);\n\n\t\t\tawait this._loggingComponent?.log({\n\t\t\t\tlevel: \"info\",\n\t\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\t\tts: Date.now(),\n\t\t\t\tmessage: \"negotiationRejected\",\n\t\t\t\tdata: {\n\t\t\t\t\tnegotiationId,\n\t\t\t\t\tdurationMs: Date.now() - negotiation.startedAt\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tawait this._callback?.onFailed(negotiationId, \"negotiationTerminatedByProvider\");\n\t\t} else {\n\t\t\tawait this._loggingComponent?.log({\n\t\t\t\tlevel: \"warn\",\n\t\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\t\tts: Date.now(),\n\t\t\t\tmessage: \"unknownNegotiationTerminated\",\n\t\t\t\tdata: { negotiationId }\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"dataspaceControlPlanePolicyRequester.js","sourceRoot":"","sources":["../../src/dataspaceControlPlanePolicyRequester.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIlD,OAAO,EAAE,gBAAgB,EAAyB,MAAM,oCAAoC,CAAC;AAC7F,OAAO,EACN,6CAA6C,EAG7C,MAAM,wCAAwC,CAAC;AAGhD;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,oCAAoC;IAChD;;OAEG;IACI,MAAM,CAAU,UAAU,0CAA0D;IAE3F;;;OAGG;IACc,iBAAiB,CAAqB;IAEvD;;;OAGG;IACc,SAAS,CAAwB;IAElD;;;;;OAKG;IACc,aAAa,CAAiC;IAE/D;;;;OAIG;IACH,YAAY,oBAA6B,EAAE,QAA+B;QACzE,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC,WAAW,CACpD,oBAAoB,IAAI,SAAS,CACjC,CAAC;QACF,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;QAC1B,IAAI,CAAC,aAAa,GAAG,IAAI,GAAG,EAAE,CAAC;IAChC,CAAC;IAED;;;OAGG;IACI,SAAS;QACf,OAAO,oCAAoC,CAAC,UAAU,CAAC;IACxD,CAAC;IAED;;;;OAIG;IACI,gBAAgB,CAAC,aAAqB;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,aAAa,EAAE;YACrC,aAAa;YACb,KAAK,EAAE,6CAA6C,CAAC,SAAS;YAC9D,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACd,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACI,qBAAqB;QAC3B,OAAO,IAAI,CAAC,aAAa,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACI,iBAAiB,CAAC,aAAqB;QAC7C,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAC1C,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,KAAK,CAAC,aAAqB,EAAE,KAA8B;QACvE,MAAM,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;YACjC,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,oCAAoC,CAAC,UAAU;YACvD,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;YACd,OAAO,EAAE,eAAe;YACxB,IAAI,EAAE;gBACL,aAAa;gBACb,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC;aACrB;SACD,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC1D,IAAI,WAAW,EAAE,CAAC;YACjB,WAAW,CAAC,KAAK,GAAG,6CAA6C,CAAC,OAAO,CAAC;YAC1E,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACpC,CAAC;QAED,MAAM,IAAI,CAAC,SAAS,EAAE,cAAc,CACnC,aAAa,EACb,6CAA6C,CAAC,OAAO,EACrD,EAAE,KAAK,EAAE,CACT,CAAC;QAEF,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,SAAS,CACrB,aAAqB,EACrB,SAAsC;QAEtC,MAAM,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;YACjC,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,oCAAoC,CAAC,UAAU;YACvD,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;YACd,OAAO,EAAE,mBAAmB;YAC5B,IAAI,EAAE;gBACL,aAAa;gBACb,WAAW,EAAE,gBAAgB,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE;aACrD;SACD,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC1D,IAAI,WAAW,EAAE,CAAC;YACjB,WAAW,CAAC,KAAK,GAAG,6CAA6C,CAAC,MAAM,CAAC;YACzE,WAAW,CAAC,SAAS,GAAG,SAAS,CAAC;YAClC,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACpC,CAAC;QAED,MAAM,IAAI,CAAC,SAAS,EAAE,cAAc,CACnC,aAAa,EACb,6CAA6C,CAAC,MAAM,EACpD,EAAE,SAAS,EAAE,CACb,CAAC;QAEF,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,SAAS,CAAC,aAAqB;QAC3C,MAAM,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;YACjC,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,oCAAoC,CAAC,UAAU;YACvD,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;YACd,OAAO,EAAE,sBAAsB;YAC/B,IAAI,EAAE,EAAE,aAAa,EAAE;SACvB,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC1D,IAAI,WAAW,EAAE,CAAC;YACjB,WAAW,CAAC,KAAK,GAAG,6CAA6C,CAAC,SAAS,CAAC;YAC5E,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAEnC,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YACnE,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YAEzC,MAAM,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;gBACjC,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,oCAAoC,CAAC,UAAU;gBACvD,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;gBACd,OAAO,EAAE,sBAAsB;gBAC/B,IAAI,EAAE;oBACL,aAAa;oBACb,WAAW;oBACX,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,CAAC,SAAS;iBAC9C;aACD,CAAC,CAAC;YAEH,IAAI,WAAW,EAAE,CAAC;gBACjB,MAAM,IAAI,CAAC,SAAS,EAAE,cAAc,CACnC,aAAa,EACb,6CAA6C,CAAC,SAAS,EACvD,EAAE,SAAS,EAAE,WAAW,CAAC,SAAS,EAAE,CACpC,CAAC;gBAEF,MAAM,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;YAC/D,CAAC;iBAAM,CAAC;gBACP,MAAM,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,aAAa,EAAE,iCAAiC,CAAC,CAAC;YAClF,CAAC;QACF,CAAC;aAAM,CAAC;YACP,MAAM,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;gBACjC,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,oCAAoC,CAAC,UAAU;gBACvD,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;gBACd,OAAO,EAAE,6BAA6B;gBACtC,IAAI,EAAE,EAAE,aAAa,EAAE;aACvB,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,UAAU,CAAC,aAAqB;QAC5C,MAAM,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;YACjC,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,oCAAoC,CAAC,UAAU;YACvD,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;YACd,OAAO,EAAE,uBAAuB;YAChC,IAAI,EAAE,EAAE,aAAa,EAAE;SACvB,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC1D,IAAI,WAAW,EAAE,CAAC;YACjB,WAAW,CAAC,KAAK,GAAG,6CAA6C,CAAC,UAAU,CAAC;YAC7E,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAEnC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YAEzC,MAAM,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;gBACjC,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,oCAAoC,CAAC,UAAU;gBACvD,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;gBACd,OAAO,EAAE,qBAAqB;gBAC9B,IAAI,EAAE;oBACL,aAAa;oBACb,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,CAAC,SAAS;iBAC9C;aACD,CAAC,CAAC;YAEH,MAAM,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,aAAa,EAAE,iCAAiC,CAAC,CAAC;QAClF,CAAC;aAAM,CAAC;YACP,MAAM,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;gBACjC,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,oCAAoC,CAAC,UAAU;gBACvD,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;gBACd,OAAO,EAAE,8BAA8B;gBACvC,IAAI,EAAE,EAAE,aAAa,EAAE;aACvB,CAAC,CAAC;QACJ,CAAC;IACF,CAAC","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { ComponentFactory } from \"@twin.org/core\";\nimport type { INegotiationCallback } from \"@twin.org/dataspace-models\";\nimport type { ILoggingComponent } from \"@twin.org/logging-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { OdrlPolicyHelper, type IPolicyRequester } from \"@twin.org/rights-management-models\";\nimport {\n\tDataspaceProtocolContractNegotiationStateType,\n\ttype IDataspaceProtocolAgreement,\n\ttype IDataspaceProtocolOffer\n} from \"@twin.org/standards-dataspace-protocol\";\nimport type { INegotiationState } from \"./models/INegotiationState.js\";\n\n/**\n * Policy Requester for Dataspace Control Plane.\n *\n * Handles contract negotiation callbacks from PNP and forwards state changes\n * to the control plane service via the INegotiationCallback interface.\n *\n * Callback Flow:\n * 1. Control Plane initiates negotiation via PNP.sendRequestToProvider()\n * 2. PNP calls offer() → auto-accept, notify control plane\n * 3. PNP calls agreement() → store agreement, notify control plane\n * 4. PNP calls finalised() → notify control plane with agreementId\n * 5. Control plane notifies upstream caller (e.g. supply-chain)\n */\nexport class DataspaceControlPlanePolicyRequester implements IPolicyRequester {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<DataspaceControlPlanePolicyRequester>();\n\n\t/**\n\t * The logging component.\n\t * @internal\n\t */\n\tprivate readonly _loggingComponent?: ILoggingComponent;\n\n\t/**\n\t * Callback interface for notifying the control plane of negotiation state changes.\n\t * @internal\n\t */\n\tprivate readonly _callback?: INegotiationCallback;\n\n\t/**\n\t * Active negotiations tracked in memory.\n\t * Key: negotiationId\n\t * Value: Negotiation state\n\t * @internal\n\t */\n\tprivate readonly _negotiations: Map<string, INegotiationState>;\n\n\t/**\n\t * Create a new instance of DataspaceControlPlanePolicyRequester.\n\t * @param loggingComponentType Optional logging component type.\n\t * @param callback Optional callback interface for state change notifications.\n\t */\n\tconstructor(loggingComponentType?: string, callback?: INegotiationCallback) {\n\t\tthis._loggingComponent = ComponentFactory.getIfExists<ILoggingComponent>(\n\t\t\tloggingComponentType ?? \"logging\"\n\t\t);\n\t\tthis._callback = callback;\n\t\tthis._negotiations = new Map();\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn DataspaceControlPlanePolicyRequester.CLASS_NAME;\n\t}\n\n\t/**\n\t * Register a negotiation for tracking.\n\t * Called by the control plane service after initiating a negotiation via PNP.\n\t * @param negotiationId The negotiation ID returned by PNP.sendRequestToProvider().\n\t */\n\tpublic trackNegotiation(negotiationId: string): void {\n\t\tconst now = Date.now();\n\t\tthis._negotiations.set(negotiationId, {\n\t\t\tnegotiationId,\n\t\t\tstate: DataspaceProtocolContractNegotiationStateType.REQUESTED,\n\t\t\tstartedAt: now,\n\t\t\tupdatedAt: now\n\t\t});\n\t}\n\n\t/**\n\t * Get all active negotiations (for stalled cleanup).\n\t * @returns Map of negotiationId to negotiation state.\n\t */\n\tpublic getActiveNegotiations(): Map<string, INegotiationState> {\n\t\treturn this._negotiations;\n\t}\n\n\t/**\n\t * Remove a negotiation from tracking (for cleanup).\n\t * @param negotiationId The negotiation ID to remove.\n\t */\n\tpublic removeNegotiation(negotiationId: string): void {\n\t\tthis._negotiations.delete(negotiationId);\n\t}\n\n\t/**\n\t * A policy has been offered by a provider.\n\t * Called by PNP when provider sends an OfferMessage.\n\t * @param negotiationId The id of the negotiation.\n\t * @param offer The offer sent by the provider.\n\t * @returns True if the offer was accepted, false otherwise.\n\t */\n\tpublic async offer(negotiationId: string, offer: IDataspaceProtocolOffer): Promise<boolean> {\n\t\tawait this._loggingComponent?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\tts: Date.now(),\n\t\t\tmessage: \"offerReceived\",\n\t\t\tdata: {\n\t\t\t\tnegotiationId,\n\t\t\t\tofferId: offer[\"@id\"]\n\t\t\t}\n\t\t});\n\n\t\tconst negotiation = this._negotiations.get(negotiationId);\n\t\tif (negotiation) {\n\t\t\tnegotiation.state = DataspaceProtocolContractNegotiationStateType.OFFERED;\n\t\t\tnegotiation.updatedAt = Date.now();\n\t\t}\n\n\t\tawait this._callback?.onStateChanged(\n\t\t\tnegotiationId,\n\t\t\tDataspaceProtocolContractNegotiationStateType.OFFERED,\n\t\t\t{ offer }\n\t\t);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * A policy agreement has been sent by a provider.\n\t * Called by PNP when provider sends an AgreementMessage.\n\t * @param negotiationId The id of the negotiation.\n\t * @param agreement The agreement sent by the provider.\n\t * @returns True if the agreement was accepted, false otherwise.\n\t */\n\tpublic async agreement(\n\t\tnegotiationId: string,\n\t\tagreement: IDataspaceProtocolAgreement\n\t): Promise<boolean> {\n\t\tawait this._loggingComponent?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\tts: Date.now(),\n\t\t\tmessage: \"agreementReceived\",\n\t\t\tdata: {\n\t\t\t\tnegotiationId,\n\t\t\t\tagreementId: OdrlPolicyHelper.getUid(agreement) ?? \"\"\n\t\t\t}\n\t\t});\n\n\t\tconst negotiation = this._negotiations.get(negotiationId);\n\t\tif (negotiation) {\n\t\t\tnegotiation.state = DataspaceProtocolContractNegotiationStateType.AGREED;\n\t\t\tnegotiation.agreement = agreement;\n\t\t\tnegotiation.updatedAt = Date.now();\n\t\t}\n\n\t\tawait this._callback?.onStateChanged(\n\t\t\tnegotiationId,\n\t\t\tDataspaceProtocolContractNegotiationStateType.AGREED,\n\t\t\t{ agreement }\n\t\t);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * A policy finalisation has been sent by a provider.\n\t * Called by PNP when provider sends a FinalizedEvent.\n\t * @param negotiationId The id of the negotiation.\n\t * @returns Nothing.\n\t */\n\tpublic async finalised(negotiationId: string): Promise<void> {\n\t\tawait this._loggingComponent?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\tts: Date.now(),\n\t\t\tmessage: \"negotiationFinalized\",\n\t\t\tdata: { negotiationId }\n\t\t});\n\n\t\tconst negotiation = this._negotiations.get(negotiationId);\n\t\tif (negotiation) {\n\t\t\tnegotiation.state = DataspaceProtocolContractNegotiationStateType.FINALIZED;\n\t\t\tnegotiation.updatedAt = Date.now();\n\n\t\t\tconst agreementId = OdrlPolicyHelper.getUid(negotiation.agreement);\n\t\t\tthis._negotiations.delete(negotiationId);\n\n\t\t\tawait this._loggingComponent?.log({\n\t\t\t\tlevel: \"info\",\n\t\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\t\tts: Date.now(),\n\t\t\t\tmessage: \"negotiationCompleted\",\n\t\t\t\tdata: {\n\t\t\t\t\tnegotiationId,\n\t\t\t\t\tagreementId,\n\t\t\t\t\tdurationMs: Date.now() - negotiation.startedAt\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tif (agreementId) {\n\t\t\t\tawait this._callback?.onStateChanged(\n\t\t\t\t\tnegotiationId,\n\t\t\t\t\tDataspaceProtocolContractNegotiationStateType.FINALIZED,\n\t\t\t\t\t{ agreement: negotiation.agreement }\n\t\t\t\t);\n\n\t\t\t\tawait this._callback?.onFinalized(negotiationId, agreementId);\n\t\t\t} else {\n\t\t\t\tawait this._callback?.onFailed(negotiationId, \"negotiationFinalizedNoAgreement\");\n\t\t\t}\n\t\t} else {\n\t\t\tawait this._loggingComponent?.log({\n\t\t\t\tlevel: \"warn\",\n\t\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\t\tts: Date.now(),\n\t\t\t\tmessage: \"unknownNegotiationFinalized\",\n\t\t\t\tdata: { negotiationId }\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * A policy termination has been sent by a provider.\n\t * Called by PNP when provider sends a TerminatedMessage or negotiation fails.\n\t * @param negotiationId The id of the negotiation.\n\t * @returns Nothing.\n\t */\n\tpublic async terminated(negotiationId: string): Promise<void> {\n\t\tawait this._loggingComponent?.log({\n\t\t\tlevel: \"warn\",\n\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\tts: Date.now(),\n\t\t\tmessage: \"negotiationTerminated\",\n\t\t\tdata: { negotiationId }\n\t\t});\n\n\t\tconst negotiation = this._negotiations.get(negotiationId);\n\t\tif (negotiation) {\n\t\t\tnegotiation.state = DataspaceProtocolContractNegotiationStateType.TERMINATED;\n\t\t\tnegotiation.updatedAt = Date.now();\n\n\t\t\tthis._negotiations.delete(negotiationId);\n\n\t\t\tawait this._loggingComponent?.log({\n\t\t\t\tlevel: \"info\",\n\t\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\t\tts: Date.now(),\n\t\t\t\tmessage: \"negotiationRejected\",\n\t\t\t\tdata: {\n\t\t\t\t\tnegotiationId,\n\t\t\t\t\tdurationMs: Date.now() - negotiation.startedAt\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tawait this._callback?.onFailed(negotiationId, \"negotiationTerminatedByProvider\");\n\t\t} else {\n\t\t\tawait this._loggingComponent?.log({\n\t\t\t\tlevel: \"warn\",\n\t\t\t\tsource: DataspaceControlPlanePolicyRequester.CLASS_NAME,\n\t\t\t\tts: Date.now(),\n\t\t\t\tmessage: \"unknownNegotiationTerminated\",\n\t\t\t\tdata: { negotiationId }\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
@@ -120,6 +120,22 @@ export class DataspaceControlPlaneService {
120
120
  * @internal
121
121
  */
122
122
  _negotiationCallbacks;
123
+ /**
124
+ * Registered transfer callbacks from upstream callers, keyed by registration key.
125
+ * @internal
126
+ */
127
+ _transferCallbacks;
128
+ /**
129
+ * Internal transfer callback that fans out to all registered transfer callbacks.
130
+ * @internal
131
+ */
132
+ _internalTransferCallback;
133
+ /**
134
+ * Factory key used to create remote control plane REST client instances for outbound
135
+ * DSP transfer requests. Resolved via ComponentFactory.create() at call time.
136
+ * @internal
137
+ */
138
+ _remoteControlPlaneComponentType;
123
139
  /**
124
140
  * The component type name for the hosting component.
125
141
  * @internal
@@ -159,6 +175,10 @@ export class DataspaceControlPlaneService {
159
175
  options?.dataPlaneComponentType ?? "dataspace-data-plane-service";
160
176
  this._urlTransformerComponent = ComponentFactory.get(options?.urlTransformerComponentType ?? "url-transformer");
161
177
  this._negotiationCallbacks = new Map();
178
+ this._transferCallbacks = new Map();
179
+ this._internalTransferCallback = this.createInternalTransferCallback();
180
+ this._remoteControlPlaneComponentType =
181
+ options?.remoteControlPlaneComponentType ?? "dataspace-control-plane-rest-client";
162
182
  const internalCallback = this.createInternalCallback();
163
183
  this._policyRequester = new DataspaceControlPlanePolicyRequester(options?.loggingComponentType ?? "logging", internalCallback);
164
184
  PolicyRequesterFactory.register(DataspaceControlPlaneService._REQUESTER_TYPE, () => this._policyRequester);
@@ -188,6 +208,24 @@ export class DataspaceControlPlaneService {
188
208
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "key", key);
189
209
  this._negotiationCallbacks.delete(key);
190
210
  }
211
+ /**
212
+ * Register a callback to receive transfer process state change notifications.
213
+ * @param key A unique key identifying this callback registration.
214
+ * @param callback The callback interface to register.
215
+ */
216
+ registerTransferCallback(key, callback) {
217
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "key", key);
218
+ Guards.object(DataspaceControlPlaneService.CLASS_NAME, "callback", callback);
219
+ this._transferCallbacks.set(key, callback);
220
+ }
221
+ /**
222
+ * Unregister a previously registered transfer callback.
223
+ * @param key The key used when registering the callback.
224
+ */
225
+ unregisterTransferCallback(key) {
226
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "key", key);
227
+ this._transferCallbacks.delete(key);
228
+ }
191
229
  /**
192
230
  * The service needs to be started when the application is initialized.
193
231
  * Populates the Federated Catalogue with datasets from registered apps
@@ -406,6 +444,142 @@ export class DataspaceControlPlaneService {
406
444
  state: entity.state
407
445
  };
408
446
  }
447
+ /**
448
+ * Start a data transfer as a Consumer.
449
+ * Generates a consumerPid, POSTs a TransferRequestMessage to the provider's DSP endpoint,
450
+ * and (only if the provider accepts) persists a local TransferProcess in REQUESTED state.
451
+ * @param agreementId The finalized agreement ID from contract negotiation.
452
+ * @param providerEndpoint The provider's DSP control plane base URL.
453
+ * @param publicOrigin The public origin URL of this control plane (used as callbackAddress).
454
+ * @param format The transfer format (e.g. "HttpProxy-PULL", "HttpProxy-PUSH").
455
+ * @param trustPayload Trust payload for authenticating this call.
456
+ * @returns The consumerPid of the newly created TransferProcess.
457
+ *
458
+ * **Engine configuration requirement:** The outbound call to the provider uses
459
+ * `ComponentFactory.create(remoteControlPlaneComponentType, { endpoint, pathPrefix })`.
460
+ * For the runtime `providerEndpoint` to be forwarded correctly, the engine **must** register
461
+ * the component type (default: `dataspace-control-plane-rest-client`) as a
462
+ * **multi-instance** component (`isMultiInstance: true` in engine config). A singleton
463
+ * registration ignores the runtime `endpoint` arg and silently POSTs to its
464
+ * static endpoint instead.
465
+ */
466
+ async startDataTransfer(agreementId, providerEndpoint, publicOrigin, format, trustPayload) {
467
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "agreementId", agreementId);
468
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "providerEndpoint", providerEndpoint);
469
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "publicOrigin", publicOrigin);
470
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "format", format);
471
+ if (!Object.values(DataspaceTransferFormat).includes(format)) {
472
+ throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "unsupportedTransferFormat", {
473
+ format,
474
+ supported: Object.values(DataspaceTransferFormat)
475
+ });
476
+ }
477
+ const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "startDataTransfer");
478
+ await this._loggingComponent?.log({
479
+ level: "info",
480
+ source: DataspaceControlPlaneService.CLASS_NAME,
481
+ ts: Date.now(),
482
+ message: "startingDataTransfer",
483
+ data: { agreementId, providerEndpoint, format, identity: trustInfo.identity }
484
+ });
485
+ const agreement = await this.lookupAgreement(agreementId);
486
+ const assigneeIdentity = OdrlPolicyHelper.extractAssigneeIdentity(agreement);
487
+ const assigneeIds = ArrayHelper.fromObjectOrArray(assigneeIdentity);
488
+ const callerComposite = this.buildCallerComposite(trustInfo);
489
+ if (!assigneeIds.includes(callerComposite)) {
490
+ throw new UnauthorizedError(DataspaceControlPlaneService.CLASS_NAME, "callerNotAuthorizedForAgreement");
491
+ }
492
+ const assignerIdentity = OdrlPolicyHelper.extractAssignerIdentity(agreement);
493
+ const assignerIds = ArrayHelper.fromObjectOrArray(assignerIdentity);
494
+ if (assignerIds.length > 1) {
495
+ throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "multipleAssignersNotSupported");
496
+ }
497
+ const providerIdentity = assignerIds[0];
498
+ const datasetId = this.extractDatasetId(agreement);
499
+ const consumerPid = `urn:uuid:${RandomHelper.generateUuidV7()}`;
500
+ const callbackAddress = StringHelper.trimTrailingSlashes(publicOrigin);
501
+ // Fetch context once and reuse across the PUSH dataAddress, trust token, and storage entity.
502
+ const contextIds = await ContextIdStore.getContextIds();
503
+ const nodeIdentity = contextIds?.[ContextIdKeys.Node];
504
+ Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "nodeIdentity", nodeIdentity);
505
+ const tenantId = contextIds?.[ContextIdKeys.Tenant];
506
+ const organizationIdentity = contextIds?.[ContextIdKeys.Organization];
507
+ const transferRequestMessage = {
508
+ "@context": [DataspaceProtocolContexts.Context],
509
+ "@type": DataspaceProtocolTransferProcessTypes.TransferRequestMessage,
510
+ consumerPid,
511
+ agreementId,
512
+ callbackAddress,
513
+ format
514
+ };
515
+ // For consumer-initiated PUSH transfers the consumer must supply its /inbox endpoint as
516
+ // dataAddress so the provider knows where to push ActivityStreams objects. Without it the
517
+ // provider silently falls through to PULL mode on startTransfer.
518
+ if (format === DataspaceTransferFormat.HttpProxyPush) {
519
+ if (!Is.stringValue(this._dataPlanePath)) {
520
+ throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "pushTransferDataPathNotConfigured", { consumerPid });
521
+ }
522
+ let inboxEndpoint = `${callbackAddress}/${this._dataPlanePath}/inbox`;
523
+ if (Is.stringValue(tenantId)) {
524
+ inboxEndpoint = await this._urlTransformerComponent.addEncryptedQueryParamToUrl(inboxEndpoint, "tenant", tenantId);
525
+ }
526
+ transferRequestMessage.dataAddress = {
527
+ "@type": DataspaceProtocolTransferProcessTypes.DataAddress,
528
+ endpointType: DataspaceProtocolEndpointType.HttpsActivityStreamEndpoint,
529
+ endpoint: inboxEndpoint
530
+ };
531
+ }
532
+ // Generate outbound trust token to authenticate this node to the provider.
533
+ const outboundToken = await this._trustComponent.generate(nodeIdentity, this._overrideTrustGeneratorType, { subject: { consumerPid, agreementId } }, Is.stringValue(tenantId) ? TrustHelper.hashTenantId(tenantId) : undefined, organizationIdentity);
534
+ // Create a remote REST client pointed at the provider endpoint and call requestTransfer.
535
+ const remoteControlPlane = ComponentFactory.create(this._remoteControlPlaneComponentType, { endpoint: providerEndpoint, pathPrefix: "" });
536
+ const result = await remoteControlPlane.requestTransfer(transferRequestMessage, outboundToken);
537
+ if (getJsonLdType(result) === DataspaceProtocolTransferProcessTypes.TransferError) {
538
+ const transferError = result;
539
+ throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "transferRequestRejectedByProvider", {
540
+ agreementId,
541
+ providerEndpoint,
542
+ code: transferError.code
543
+ });
544
+ }
545
+ const transferProcess = result;
546
+ if (!Is.stringValue(transferProcess.providerPid)) {
547
+ throw new GeneralError(DataspaceControlPlaneService.CLASS_NAME, "providerPidMissingInResponse", { consumerPid });
548
+ }
549
+ const now = new Date();
550
+ const storageEntity = {
551
+ id: Converter.bytesToHex(RandomHelper.generate(32)),
552
+ consumerPid,
553
+ providerPid: transferProcess.providerPid,
554
+ state: DataspaceProtocolTransferProcessStateType.REQUESTED,
555
+ agreementId,
556
+ datasetId,
557
+ consumerIdentity: callerComposite,
558
+ providerIdentity,
559
+ offerId: agreementId,
560
+ policies: [agreement],
561
+ callbackAddress,
562
+ format,
563
+ dataAddress: transferRequestMessage.dataAddress,
564
+ tenantId: Is.stringValue(tenantId) ? tenantId : undefined,
565
+ dateCreated: now.toISOString(),
566
+ dateModified: now.toISOString()
567
+ };
568
+ await this._transferProcessStorage.set(storageEntity);
569
+ await this._loggingComponent?.log({
570
+ level: "info",
571
+ source: DataspaceControlPlaneService.CLASS_NAME,
572
+ ts: Date.now(),
573
+ message: "dataTransferStarted",
574
+ data: {
575
+ consumerPid,
576
+ providerPid: storageEntity.providerPid,
577
+ agreementId,
578
+ format
579
+ }
580
+ });
581
+ return { consumerPid };
582
+ }
409
583
  // ----------------------------------------------------------------------------
410
584
  // PROVIDER SIDE OPERATIONS
411
585
  // ----------------------------------------------------------------------------
@@ -670,6 +844,10 @@ export class DataspaceControlPlaneService {
670
844
  throw setupError;
671
845
  }
672
846
  }
847
+ if (role === TransferProcessRole.Consumer) {
848
+ await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.STARTED);
849
+ await this._internalTransferCallback.onStarted(entity.consumerPid, response);
850
+ }
673
851
  return response;
674
852
  }
675
853
  catch (error) {
@@ -757,6 +935,10 @@ export class DataspaceControlPlaneService {
757
935
  throw teardownError;
758
936
  }
759
937
  }
938
+ if (role === TransferProcessRole.Consumer) {
939
+ await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.COMPLETED);
940
+ await this._internalTransferCallback.onCompleted(entity.consumerPid);
941
+ }
760
942
  return {
761
943
  "@context": [DataspaceProtocolContexts.Context],
762
944
  "@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
@@ -846,6 +1028,15 @@ export class DataspaceControlPlaneService {
846
1028
  throw suspendError;
847
1029
  }
848
1030
  }
1031
+ if (role === TransferProcessRole.Consumer) {
1032
+ await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.SUSPENDED);
1033
+ // DSP reason is typed any[] — forward only the first entry since the callback
1034
+ // contract is reason?: string. Additional entries are intentionally dropped.
1035
+ const suspendReason = Array.isArray(message.reason)
1036
+ ? message.reason[0]
1037
+ : message.reason;
1038
+ await this._internalTransferCallback.onSuspended(entity.consumerPid, suspendReason);
1039
+ }
849
1040
  return {
850
1041
  "@context": [DataspaceProtocolContexts.Context],
851
1042
  "@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
@@ -929,6 +1120,15 @@ export class DataspaceControlPlaneService {
929
1120
  throw teardownError;
930
1121
  }
931
1122
  }
1123
+ if (role === TransferProcessRole.Consumer) {
1124
+ await this._internalTransferCallback.onStateChanged(entity.consumerPid, DataspaceProtocolTransferProcessStateType.TERMINATED);
1125
+ // DSP reason is typed any[] — forward only the first entry since the callback
1126
+ // contract is reason?: string. Additional entries are intentionally dropped.
1127
+ const terminateReason = Array.isArray(message.reason)
1128
+ ? message.reason[0]
1129
+ : message.reason;
1130
+ await this._internalTransferCallback.onTerminated(entity.consumerPid, terminateReason);
1131
+ }
932
1132
  return {
933
1133
  "@context": [DataspaceProtocolContexts.Context],
934
1134
  "@type": DataspaceProtocolTransferProcessTypes.TransferProcess,
@@ -995,12 +1195,19 @@ export class DataspaceControlPlaneService {
995
1195
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "offerId", offerId);
996
1196
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "providerEndpoint", providerEndpoint);
997
1197
  Guards.stringValue(DataspaceControlPlaneService.CLASS_NAME, "publicOrigin", publicOrigin);
1198
+ const trustInfo = await TrustHelper.verifyTrust(this._trustComponent, trustPayload, "negotiateAgreement");
998
1199
  await this._loggingComponent?.log({
999
1200
  level: "info",
1000
1201
  source: DataspaceControlPlaneService.CLASS_NAME,
1001
1202
  ts: Date.now(),
1002
1203
  message: "startingContractNegotiation",
1003
- data: { datasetId, offerId, providerEndpoint, publicOrigin }
1204
+ data: {
1205
+ datasetId,
1206
+ offerId,
1207
+ providerEndpoint,
1208
+ publicOrigin,
1209
+ verifiedIdentity: trustInfo.identity
1210
+ }
1004
1211
  });
1005
1212
  const catalogResult = await this._federatedCatalogueComponent.get(datasetId);
1006
1213
  if (isCatalogError(catalogResult)) {
@@ -1955,10 +2162,10 @@ export class DataspaceControlPlaneService {
1955
2162
  }
1956
2163
  }
1957
2164
  },
1958
- onCompleted: async (negotiationId, agreementId) => {
2165
+ onFinalized: async (negotiationId, agreementId) => {
1959
2166
  for (const [key, cb] of this._negotiationCallbacks.entries()) {
1960
2167
  try {
1961
- await cb.onCompleted(negotiationId, agreementId);
2168
+ await cb.onFinalized(negotiationId, agreementId);
1962
2169
  }
1963
2170
  catch (error) {
1964
2171
  await this._loggingComponent?.log({
@@ -1989,6 +2196,101 @@ export class DataspaceControlPlaneService {
1989
2196
  }
1990
2197
  };
1991
2198
  }
2199
+ /**
2200
+ * Creates the internal ITransferCallback that fans out to all registered transfer callbacks.
2201
+ * Errors thrown by individual callbacks are logged and swallowed so they cannot affect
2202
+ * the DSP protocol state machine or other registrants.
2203
+ * @returns The internal transfer callback.
2204
+ */
2205
+ createInternalTransferCallback() {
2206
+ return {
2207
+ onStateChanged: async (consumerPid, state) => {
2208
+ for (const [key, cb] of this._transferCallbacks.entries()) {
2209
+ try {
2210
+ await cb.onStateChanged(consumerPid, state);
2211
+ }
2212
+ catch (error) {
2213
+ await this._loggingComponent?.log({
2214
+ level: "error",
2215
+ source: DataspaceControlPlaneService.CLASS_NAME,
2216
+ ts: Date.now(),
2217
+ message: "transferCallbackError",
2218
+ error: BaseError.fromError(error),
2219
+ data: { key, consumerPid, state, method: "onStateChanged" }
2220
+ });
2221
+ }
2222
+ }
2223
+ },
2224
+ onStarted: async (consumerPid, message) => {
2225
+ for (const [key, cb] of this._transferCallbacks.entries()) {
2226
+ try {
2227
+ await cb.onStarted(consumerPid, message);
2228
+ }
2229
+ catch (error) {
2230
+ await this._loggingComponent?.log({
2231
+ level: "error",
2232
+ source: DataspaceControlPlaneService.CLASS_NAME,
2233
+ ts: Date.now(),
2234
+ message: "transferCallbackError",
2235
+ error: BaseError.fromError(error),
2236
+ data: { key, consumerPid, method: "onStarted" }
2237
+ });
2238
+ }
2239
+ }
2240
+ },
2241
+ onCompleted: async (consumerPid) => {
2242
+ for (const [key, cb] of this._transferCallbacks.entries()) {
2243
+ try {
2244
+ await cb.onCompleted(consumerPid);
2245
+ }
2246
+ catch (error) {
2247
+ await this._loggingComponent?.log({
2248
+ level: "error",
2249
+ source: DataspaceControlPlaneService.CLASS_NAME,
2250
+ ts: Date.now(),
2251
+ message: "transferCallbackError",
2252
+ error: BaseError.fromError(error),
2253
+ data: { key, consumerPid, method: "onCompleted" }
2254
+ });
2255
+ }
2256
+ }
2257
+ },
2258
+ onSuspended: async (consumerPid, reason) => {
2259
+ for (const [key, cb] of this._transferCallbacks.entries()) {
2260
+ try {
2261
+ await cb.onSuspended(consumerPid, reason);
2262
+ }
2263
+ catch (error) {
2264
+ await this._loggingComponent?.log({
2265
+ level: "error",
2266
+ source: DataspaceControlPlaneService.CLASS_NAME,
2267
+ ts: Date.now(),
2268
+ message: "transferCallbackError",
2269
+ error: BaseError.fromError(error),
2270
+ data: { key, consumerPid, method: "onSuspended" }
2271
+ });
2272
+ }
2273
+ }
2274
+ },
2275
+ onTerminated: async (consumerPid, reason) => {
2276
+ for (const [key, cb] of this._transferCallbacks.entries()) {
2277
+ try {
2278
+ await cb.onTerminated(consumerPid, reason);
2279
+ }
2280
+ catch (error) {
2281
+ await this._loggingComponent?.log({
2282
+ level: "error",
2283
+ source: DataspaceControlPlaneService.CLASS_NAME,
2284
+ ts: Date.now(),
2285
+ message: "transferCallbackError",
2286
+ error: BaseError.fromError(error),
2287
+ data: { key, consumerPid, method: "onTerminated" }
2288
+ });
2289
+ }
2290
+ }
2291
+ }
2292
+ };
2293
+ }
1992
2294
  /**
1993
2295
  * Re-stamp `@id` onto a stored payload blob using the entity primary key.
1994
2296
  * @param payload The stored payload blob (without `@id`).