@sogelink-research/dataspace-sdk
v1.1.0
Published
Dataspace protocol abstraction with TSG and EDC implementations
Readme
@sogelink-research/dataspace-sdk
TypeScript SDK for the Dataspace Protocol, with built-in support for TSG (Trust Service Gateway) and Eclipse Dataspace Connector (EDC).
Write dataspace integration code once, swap out the connector implementation without changing business logic.
Features
- Protocol-agnostic — abstract
IDataspaceClientclass works identically across TSG and EDC - Lifecycle wrappers —
NegotiationandTransferclasses with state guards, actions, and notification hooks - Pluggable HTTP adapters — same client code runs server-side (direct auth) or client-side (via proxy)
- Typed everywhere — base types shared across ecosystems, with TSG/EDC-specific extensions for raw API access
- ESM-only — ships as ES modules with full TypeScript declarations
Install
npm install @sogelink-research/dataspace-sdkQuick Start
TSG — Server-side (OAuth2)
import { TSGClient, TSGDirectHttpAdapter } from "@sogelink-research/dataspace-sdk";
const adapter = new TSGDirectHttpAdapter({
baseUrl: "https://lelystad.tsg.beta.geodan.nl",
clientId: "my-client-id",
clientSecret: "my-client-secret",
participant: "lelystad",
});
const client = new TSGClient("lelystad", adapter);
const catalog = await client.getCatalog();
const negotiations = await client.getNegotiations();EDC — Server-side (API key)
import { EDCClient, EDCDirectHttpAdapter } from "@sogelink-research/dataspace-sdk";
const adapter = new EDCDirectHttpAdapter({
baseUrl: "https://edc.example.com/management",
apiKey: "my-api-key",
participant: "provider-1",
});
const client = new EDCClient("provider-1", "urn:connector:provider-1", adapter);
const negotiations = await client.getNegotiations();
const transfers = await client.getTransfers();Client-side (via proxy)
On the client side, use ProxyHttpAdapter to route requests through your server-side proxy that handles authentication:
import { TSGClient, ProxyHttpAdapter } from "@sogelink-research/dataspace-sdk";
const adapter = new ProxyHttpAdapter("/dataspace/lelystad");
const client = new TSGClient("lelystad", adapter);This works with any client — TSG, EDC, or future implementations. The proxy prefix is passed as-is, so your server just needs to forward requests and inject credentials.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ DataspaceManager ──────────────────────────────────────┐ │
│ (orchestration) IDataspaceClient Negotiation Transfer │
│ (abstract) (wrapper) (wrapper) │
└──────────┬──────────────────────────────────────────┬────────┘
│ │
┌───────┴───────┐ ┌───────┴───────┐
│ TSGClient │ │ EDCClient │
└───────┬───────┘ └───────┬───────┘
│ │
┌───────┴────────────┐ ┌──────────┴─────────┐
│ IHttpAdapter │ │ IHttpAdapter │
│ ┌───────────────┐ │ │ ┌────────────────┐ │
│ │ TSGDirect │ │ │ │ EDCDirect │ │
│ │ (OAuth2) │ │ │ │ (X-Api-Key) │ │
│ ├───────────────┤ │ │ ├────────────────┤ │
│ │ ProxyHttp │ │ │ │ ProxyHttp │ │
│ │ (via proxy) │ │ │ │ (via proxy) │ │
│ └───────────────┘ │ │ └────────────────┘ │
└────────────────────┘ └────────────────────┘Core Concepts
IDataspaceClient
The abstract base class all connector implementations extend. Defines the full contract:
| Category | Methods |
|----------|---------|
| Catalog | refreshRegistry(), getCatalog() |
| Data Planes | getDataPlanes(), getDataPlaneDatasets(), addDatasetToDataPlane(), deleteDataset(), deleteDatasetViaControlPlane(), updateDataPlaneConfiguration(), getDatasetById(), getDataset(), updateDataset() |
| Negotiations | getNegotiations(), getNegotiation(), getNegotiationDataset(), requestNegotiation(), agreeNegotiation(), verifyNegotiation(), finalizeNegotiation(), terminateNegotiation() |
| Transfers | getTransfers(), getTransfer(), requestTransfer(), startTransfer(), terminateTransfer(), suspendTransfer(), completeTransfer(), transferDataProxied(), transferDataDirect() |
| Logging | getIngressLogs(), getEgressLogs() |
Negotiation Wrapper
Wraps a DataspaceNegotiation with state guards and lifecycle actions:
import { Negotiation } from "@sogelink-research/dataspace-sdk";
const negotiations = await client.getNegotiations();
const wrapped = negotiations.map(n =>
new Negotiation(n, client, refreshNegotiations, refreshTransfers, notifier)
);
for (const neg of wrapped) {
console.log(neg.id, neg.state, neg.role);
if (neg.canAgree) await neg.agree(); // provider: REQUESTED → AGREED
if (neg.canVerify) await neg.verify(); // consumer: AGREED → VERIFIED
if (neg.canFinalize) await neg.finalize(); // provider: VERIFIED → FINALIZED
if (neg.canRequestTransfer) await neg.requestTransfer();
}State helpers: isRequested, isOffered, isAccepted, isAgreed, isVerified, isFinalized, isTerminated, isActive.
Transfer Wrapper
Wraps a DataspaceTransfer with guarded lifecycle methods:
import { Transfer } from "@sogelink-research/dataspace-sdk";
const transfers = await client.getTransfers();
const wrapped = transfers.map(t =>
new Transfer(t, client, refreshTransfers, notifier)
);
for (const t of wrapped) {
if (t.canStart) await t.start();
if (t.canTest) {
const data = await t.fetchData("items"); // fetch via data plane
}
}State helpers: isRequested, isStarted, isSuspended, isTerminated, isCompleted, isTerminal.
Notifications
Both wrappers accept an optional IDataspaceNotifier for UI notifications or logging.
The refresh callbacks (onRefreshNegotiations, onRefreshTransfers, onRefresh) are also optional — they default to no-ops when omitted:
// Minimal — no refresh callbacks, no notifier
const neg = new Negotiation(data, client);
const tfr = new Transfer(data, client);
// Full — with refresh hooks and notifier
const neg = new Negotiation(data, client, refreshNeg, refreshTfr, notifier);
const tfr = new Transfer(data, client, refreshTfr, notifier);Custom notifier example:
const notifier: IDataspaceNotifier = {
onSuccess(title, message) { console.log(`✓ ${title}: ${message}`); },
onError(title, message) { console.error(`✗ ${title}: ${message}`); },
onWarning(title, message) { console.warn(`⚠ ${title}: ${message}`); },
};DataspaceManager
Framework-agnostic orchestration layer that ties together a client, negotiations, transfers, catalog, and data planes. Designed to be extended in UI frameworks (e.g. with Svelte $state runes or React state):
import { DataspaceManager, TSGClient, ProxyHttpAdapter } from "@sogelink-research/dataspace-sdk";
// Immediate initialization
const adapter = new ProxyHttpAdapter("/dataspace/lelystad");
const client = new TSGClient("lelystad", adapter);
const manager = new DataspaceManager(client);
await manager.loadAll();
console.log(manager.catalog); // CatalogEntry[]
console.log(manager.negotiations); // Negotiation[] (wrapped)
console.log(manager.transfers); // Transfer[] (wrapped)
console.log(manager.providerNegotiations); // filtered by role
console.log(manager.activeTransfers); // non-terminal transfersLazy initialization is also supported — useful when the client is created after construction:
const manager = new DataspaceManager();
// Later, when config is available:
manager.setClient(new TSGClient("lelystad", adapter));
await manager.loadAll();Subclass to add framework reactivity or app-specific logic:
class AppDataspaceManager extends DataspaceManager {
// Override with Svelte $state, MobX observable, Vue ref, etc.
public override negotiations: Array<Negotiation> = $state([]);
public override transfers: Array<Transfer> = $state([]);
// Override to add filtering, extra logging, etc.
public override async updateNegotiations(): Promise<void> {
if (!this.client) return;
const raw = await this.client.getNegotiations();
this.negotiations = raw
.filter(n => this.allowedParticipants.includes(n.remoteParty))
.map(n => this.wrapNegotiation(n));
}
}Built-in computed getters: remoteCatalog, providerNegotiations, consumerNegotiations, activeNegotiations, terminatedNegotiations, finalizedNegotiations, activeTransfers, providerTransfers, consumerTransfers.
Lookup helpers: resolveDatasetName(id), resolvePartyName(id), getFinalizedNegotiationForDataset(id), getNegotiationForDataset(id), getNegotiationsByRole(role).
Type System
The SDK uses a layered type system:
Base types (shared across all ecosystems)
import type {
DataspaceNegotiation, // id, state, role, remoteParty, remoteAddress, agreementId, datasetId, modifiedDate
DataspaceTransfer, // id, state, role, remoteParty, remoteAddress, agreementId, modifiedDate
CatalogEntry, // DCAT catalog from remote participants
CatalogDataset, // dataset within a catalog entry
DatasetPolicy, // ODRL offer
NegotiationState, // "REQUESTED" | "OFFERED" | "ACCEPTED" | "AGREED" | "VERIFIED" | "FINALIZED" | "TERMINATED"
TransferState, // "REQUESTED" | "STARTED" | "TERMINATED" | "COMPLETED" | "SUSPENDED"
} from "@sogelink-research/dataspace-sdk";TSG-specific types
import type {
TsgNegotiation, // extends DataspaceNegotiation + dataSet, agreementDao, events, ...
TsgTransfer, // extends DataspaceTransfer + remoteId, format
TsgDataPlane, // data plane with health, datasets, management address
TsgDataPlaneDataset, // dataset within a data plane
TsgDataPlaneLogEntry, // ingress/egress log entry
TsgConnectedDataset, // external dataset with active transfer
TsgExposableDataset, // dataset that can be published
} from "@sogelink-research/dataspace-sdk";EDC-specific types
import type {
EdcNegotiation, // extends DataspaceNegotiation + counterPartyId, contractAgreementId, ...
EdcTransfer, // extends DataspaceTransfer + assetId, dataDestination, ...
EdcContractAgreement, // finalized contract agreement
EdcAssetOutput, // EDC asset
EdcDataPlaneInstance, // EDC data plane instance
EdcCatalog, // raw DCAT catalog from EDC
} from "@sogelink-research/dataspace-sdk";The base types are always available on every client. Ecosystem-specific types are returned by the concrete client and extend the base types with raw API fields.
HTTP Adapters
| Adapter | Auth Method | Use Case |
|---------|-------------|----------|
| TSGDirectHttpAdapter | OAuth2 client-credentials | Server-side TSG access |
| EDCDirectHttpAdapter | X-Api-Key header | Server-side EDC access |
| ProxyHttpAdapter | None (proxy handles auth) | Client-side access via any backend proxy |
You can implement IHttpAdapter for custom auth schemes:
import type { IHttpAdapter } from "@sogelink-research/dataspace-sdk";
class MyCustomAdapter implements IHttpAdapter {
async get(url: string): Promise<Response> { /* ... */ }
async post(url: string, body: any): Promise<Response> { /* ... */ }
async put(url: string, body: any): Promise<Response> { /* ... */ }
async delete(url: string): Promise<Response> { /* ... */ }
}TSG vs EDC Differences
| Feature | TSG | EDC | |---------|-----|-----| | Authentication | OAuth2 client-credentials | X-Api-Key | | Catalog | Federated registry with refresh | Per-connector catalog request | | Negotiation lifecycle | Manual (agree → verify → finalize) | Automatic (only terminate available) | | Data planes | First-class with typed datasets | Assets + policy/contract definitions | | Logging | Ingress/egress logs via API | Not available via management API | | Transfers | Start via data plane endpoint | Resume/deprovision via management API |
The IDataspaceClient abstraction normalizes these differences. Methods that are not applicable to a specific connector throw descriptive errors (e.g., agreeNegotiation() on EDC).
