import { formatDate } from "@helpers"; // import { DSPContractStorage } from "@helpers/dsp/DSPContractStorage"; import { localized, msg } from "@lit/localize"; import type { TemplateResultOrSymbol } from "@src/component"; import { DSPContractStorage, offerKindActionHandler, offerKindHandler, rdf, TemsObjectHandler, } from "@startinblox/solid-tems-shared"; import ModalStyle from "@styles/modal/tems-modal.scss?inline"; import { css, html, nothing, type TemplateResult, unsafeCSS } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; export type TemsModalProps = rdf.ValidM18Object; @customElement("tems-modal") @localized() export class TemsModal extends TemsObjectHandler { static styles = css` ${unsafeCSS(ModalStyle)} `; @property({ attribute: false, type: Object }) object: TemsModalProps["object"] = { "@id": "" }; @property({ attribute: false }) dspStore?: any; @property({ attribute: false }) participantId?: string; @property({ attribute: false }) participantConnectorUri?: string; @property({ attribute: false, type: Boolean }) displayServiceTest = true; @state() negotiationStatus: | "idle" | "negotiating" | "pending" | "granted" | "failed" | "transferring" = "idle"; @state() negotiationError?: string; @state() contractId?: string; @state() negotiationId?: string; @state() currentState?: string; @state() attempt?: number; @state() maxAttempts?: number; @state() gettingToken = false; @state() testingService = false; @state() testResult?: any; @state() existingAgreementChecked = false; @state() transferId?: string; @state() edrToken?: string; @state() edrEndpoint?: string; @state() showPolicySelection = false; @state() selectedPolicyIndex?: number; @state() availablePolicies?: any[]; @state() transferError?: string; @state() gettingEDR = false; @state() accessingData = false; @state() dataAccessAttempt?: number; @state() dataAccessMaxAttempts?: number; @state() countdown?: number; @state() dataResult?: any; @state() dataAccessError?: string; /** * Check for existing agreement when component connects */ connectedCallback() { super.connectedCallback(); this._checkExistingAgreement(); } /** * Get localStorage key for this asset * Uses combination of provider ID and dataset ID for uniqueness across providers */ private _getStorageKey(): string { const obj = this.object as any; const datasetId = obj.datasetId || obj.assetId; // Include provider ID to differentiate assets with same ID from different providers const providerId = obj.counterPartyId || obj._providerParticipantId || obj._provider || ""; // DEBUG: Log what provider info we're seeing if (!datasetId) return ""; // Create composite key: provider-assetId const key = providerId ? `dsp-agreement-${providerId}-${datasetId}` : `dsp-agreement-${datasetId}`; return key; } /** * Save agreement info to localStorage */ private _saveAgreementInfo( contractId: string, negotiationId: string, timestamp: number, ) { const key = this._getStorageKey(); if (key) { const obj = this.object as any; const agreementInfo = { contractId, negotiationId, timestamp, assetId: obj.datasetId || obj.assetId, providerId: obj.counterPartyId || obj._providerParticipantId || obj._provider || "", providerAddress: obj.counterPartyAddress || obj._providerAddress || "", }; localStorage.setItem(key, JSON.stringify(agreementInfo)); } } /** * Load agreement info from localStorage */ private _loadAgreementInfo(): { contractId: string; negotiationId: string; timestamp: number; assetId: string; } | null { const key = this._getStorageKey(); if (!key) return null; try { const stored = localStorage.getItem(key); if (stored) { const info = JSON.parse(stored); return info; } } catch (error) { console.error("Failed to load agreement info:", error); } return null; } /** * Save initial contract state when negotiation starts */ private _saveInitialContractState(negotiationId: string) { try { const obj = this.object as any; // Check if contract already exists for this asset from this provider const providerId = obj.counterPartyId || obj._providerParticipantId || ""; const existingContracts = DSPContractStorage.getByAssetAndProvider( obj.assetId || obj.datasetId, providerId, ); const existingContract = existingContracts.find( (c) => c.contractId === negotiationId, ); if (!existingContract) { // Debug: log asset object to see available properties // Extract index endpoint URL from asset (dcat:endpointUrl) const indexEndpointUrl = obj["dcat:endpointUrl"] || obj.endpointUrl || obj["endpointUrl"]; const assetName = obj.name || obj.assetId || "Unknown Asset"; // Detect if this is an index asset const isIndexAsset = assetName.toLowerCase().includes("index"); // Create new contract in REQUESTED state DSPContractStorage.create({ assetId: obj.assetId || obj.datasetId, datasetId: obj.datasetId || obj.assetId, assetName, assetDescription: obj.description, providerName: obj._provider || obj.provider?.name || "Unknown Provider", providerAddress: obj.counterPartyAddress || obj._providerAddress || "", providerParticipantId: obj.counterPartyId || obj._providerParticipantId || "", providerColor: obj._providerColor, policy: obj.policy, state: "REQUESTED", contractId: negotiationId, // Index-specific fields isIndexAsset, indexEndpointUrl, }); } } catch (error) { console.error( "[DSP Contract Catalog] Failed to save initial contract state:", error, ); } } /** * Save contract to DSP Contract Catalog for history tracking */ private _saveToContractCatalog(contractId: string, negotiationId: string) { try { const obj = this.object as any; // Debug: log asset object to see available properties for endpoint URL // Check if contract already exists - search by contractId (negotiationId) first, // then by agreementId as fallback. Filter by provider to avoid cross-provider confusion. const providerId = obj.counterPartyId || obj._providerParticipantId || ""; const existingContracts = DSPContractStorage.getByAssetAndProvider( obj.assetId || obj.datasetId, providerId, ); const existingContract = existingContracts.find( (c) => c.contractId === negotiationId || c.agreementId === contractId, ); // Extract index endpoint URL from asset (dcat:endpointUrl) // Try multiple possible property names - the mapping config adds 'indexEndpointUrl' const indexEndpointUrl = obj.indexEndpointUrl || obj["dcat:endpointUrl"] || obj["dcat:endpointURL"] || obj.endpointUrl || obj["endpointUrl"] || obj.endpointURL; const assetName = obj.name || obj.assetId || "Unknown Asset"; // Detect if this is an index asset const isIndexAsset = assetName.toLowerCase().includes("index"); if (existingContract) { // Update existing contract with index metadata DSPContractStorage.updateState(existingContract.id, "FINALIZED", { agreementId: contractId, contractId: negotiationId, isIndexAsset, indexEndpointUrl, }); } else { // Create new contract entry DSPContractStorage.create({ assetId: obj.assetId || obj.datasetId, datasetId: obj.datasetId || obj.assetId, assetName, assetDescription: obj.description, providerName: obj._provider || obj.provider?.name || "Unknown Provider", providerAddress: obj.counterPartyAddress || obj._providerAddress || "", providerParticipantId: obj.counterPartyId || obj._providerParticipantId || "", providerColor: obj._providerColor, policy: obj.policy, state: "FINALIZED", contractId: negotiationId, agreementId: contractId, // Index-specific fields isIndexAsset, indexEndpointUrl, }); } } catch (error) { console.error("[DSP Contract Catalog] Failed to save contract:", error); } } /** * Update contract state in catalog (for failures) */ private _updateContractState( negotiationId: string, state: "FAILED" | "TERMINATED", error?: string, ) { try { const obj = this.object as any; const providerId = obj.counterPartyId || obj._providerParticipantId || ""; const existingContracts = DSPContractStorage.getByAssetAndProvider( obj.assetId || obj.datasetId, providerId, ); const existingContract = existingContracts.find( (c) => c.contractId === negotiationId, ); if (existingContract) { DSPContractStorage.updateState(existingContract.id, state, { error }); } } catch (error) { console.error( "[DSP Contract Catalog] Failed to update contract state:", error, ); } } /** * Check if there's an existing agreement for this asset */ private async _checkExistingAgreement() { if (this.existingAgreementChecked) return; this.existingAgreementChecked = true; // Try to load from localStorage const storedInfo = this._loadAgreementInfo(); if (storedInfo) { this.contractId = storedInfo.contractId; this.negotiationId = storedInfo.negotiationId; this.negotiationStatus = "granted"; this.requestUpdate(); } // Also check if DSP store has the agreement try { if (this.dspStore && storedInfo?.negotiationId) { // Verify the agreement still exists in the store try { const obj = this.object as any; const providerId = obj.counterPartyId || obj._providerParticipantId || obj._provider || ""; await this.dspStore.getContractAgreement( storedInfo.negotiationId, providerId, ); } catch (error) { console.warn("Could not verify agreement in store:", error); } } } catch (error) { console.warn("Error checking DSP store for existing agreement:", error); } } /** * Clear existing agreement and allow renegotiation */ private _renewContract() { if ( !confirm( msg( "This will delete the current contract and start a fresh negotiation. Continue?", ), ) ) { return; } const key = this._getStorageKey(); if (key) { localStorage.removeItem(key); } // Also delete from DSPContractStorage - only for this provider's contract const obj = this.object as any; const assetId = obj?.assetId || obj?.datasetId; const providerId = obj?.counterPartyId || obj?._providerParticipantId || ""; if (assetId) { const existingContracts = DSPContractStorage.getByAssetAndProvider( assetId, providerId, ); for (const contract of existingContracts) { DSPContractStorage.delete(contract.id); } } // Reset state to idle this.negotiationStatus = "idle"; this.contractId = undefined; this.negotiationId = undefined; this.negotiationError = undefined; this.gettingToken = false; this.testingService = false; this.testResult = undefined; this.existingAgreementChecked = false; this.requestUpdate(); } _selectPolicy(index: number) { this.selectedPolicyIndex = index; this.showPolicySelection = false; this.requestUpdate(); // Automatically proceed with negotiation after selection this._negotiateAccess(); } _cancelPolicySelection() { this.showPolicySelection = false; this.selectedPolicyIndex = undefined; this.availablePolicies = undefined; this.requestUpdate(); } _formatPolicyDetails(policy: any): string { if (!policy) return "No policy details available"; const parts: string[] = []; // Policy ID if (policy["@id"]) { parts.push( `
Policy ID: ${policy["@id"]}
`, ); } // Policy Type if (policy["@type"]) { parts.push( `
Type: ${policy["@type"]}
`, ); } // Permissions const permissions = policy["odrl:permission"]; if (permissions) { const permArray = Array.isArray(permissions) ? permissions : [permissions]; if (permArray.length > 0) { parts.push( '
Permissions:
"); } } // Prohibitions const prohibitions = policy["odrl:prohibition"]; if (prohibitions) { const prohibArray = Array.isArray(prohibitions) ? prohibitions : [prohibitions]; if (prohibArray.length > 0) { parts.push( '
Prohibitions:
"); } } // Obligations const obligations = policy["odrl:obligation"]; if (obligations) { const obligArray = Array.isArray(obligations) ? obligations : [obligations]; if (obligArray.length > 0) { parts.push( '
Obligations:
"); } } // Target if (policy.target) { parts.push( `
Target Asset: ${policy.target}
`, ); } // Assigner if (policy.assigner) { parts.push( `
Assigner: ${policy.assigner}
`, ); } return parts.length > 0 ? parts.join("") : "No policy details available"; } async _negotiateAccess() { try { // Use the DSP store passed as property if (!this.dspStore) { throw new Error( "DSP connector not configured. Please provide participant-connector-uri and participant-api-key attributes.", ); } const dspStore = this.dspStore; // DEBUG: Log store configuration to verify correct store is being used // Use pre-processed contract negotiation fields from the mapped Destination object // These fields are extracted and processed by FederatedCatalogueStore.mapSourceToDestination() const obj = this.object as any; const counterPartyAddress = obj.counterPartyAddress; const counterPartyId = obj.counterPartyId || this.participantId; const datasetId = obj.datasetId; // DEFENSIVE: Handle case where obj.policy might be an array or have numeric keys let policies = obj.policies; let rawPolicy = obj.policy; // If obj.policy is an array, convert it to policies array if (Array.isArray(rawPolicy)) { console.warn( "[tems-modal] obj.policy is an array! Converting to policies array.", ); // Check if array object has a "target" property const target = (rawPolicy as any).target; // Filter out non-policy properties (like "target") policies = rawPolicy.filter( (item: any) => item && typeof item === "object" && item["@id"], ); // Add target to each policy if it exists and policy doesn't have one if (target) { policies = policies.map((p: any) => { if (!p.target && !p["odrl:target"]) { return { ...p, target, "odrl:target": target }; } return p; }); } rawPolicy = policies.length > 0 ? policies[0] : rawPolicy[0]; // Use first valid policy as default } // If obj.policy is an object with numeric keys (array-like object) else if (rawPolicy && typeof rawPolicy === "object") { const keys = Object.keys(rawPolicy); const hasNumericKeys = keys.some((k) => /^\d+$/.test(k)); if (hasNumericKeys) { console.warn( "[tems-modal] obj.policy has numeric keys! Extracting policies array.", ); // Check if object has a "target" property const target = rawPolicy.target; // Extract policies from numeric keys const extractedPolicies = []; for (const key in rawPolicy) { if (/^\d+$/.test(key)) { let policy = rawPolicy[key]; // Add target if it exists and policy doesn't have one if (target && !policy.target && !policy["odrl:target"]) { policy = { ...policy, target, "odrl:target": target }; } extractedPolicies.push(policy); } } if (extractedPolicies.length > 0) { policies = extractedPolicies; rawPolicy = extractedPolicies[0]; // Use first as default } } } // Check if there are multiple policies available if ( policies && policies.length > 1 && this.selectedPolicyIndex === undefined ) { // Store policies in state for the modal to access this.availablePolicies = policies; // Show policy selection UI this.showPolicySelection = true; this.requestUpdate(); return; } // Use selected policy or the single policy const policy = this.selectedPolicyIndex !== undefined && this.availablePolicies ? this.availablePolicies[this.selectedPolicyIndex] : this.selectedPolicyIndex !== undefined && policies ? policies[this.selectedPolicyIndex] : rawPolicy; this.selectedPolicyIndex !== undefined && this.availablePolicies ? "availablePolicies[index]" : this.selectedPolicyIndex !== undefined && policies ? "policies[index]" : "rawPolicy (fallback)"; // Validate required fields if (!counterPartyAddress) { throw new Error( "No provider endpoint URL (counterPartyAddress) found in service object", ); } if (!datasetId) { throw new Error("No dataset ID found in service object"); } if (!policy) { throw new Error("No policy found for dataset"); } // FINAL SAFEGUARD: Ensure policy doesn't have numeric keys if (policy && typeof policy === "object") { const policyKeys = Object.keys(policy); const hasNumericKeys = policyKeys.some((k) => /^\d+$/.test(k)); if (hasNumericKeys) { console.error( "[tems-modal] ERROR: Policy still has numeric keys after processing!", policy, ); throw new Error( "Invalid policy structure detected. Policy must be a single object, not an array.", ); } } if (!counterPartyId) { throw new Error( "No participant ID configured. Please provide participant-id attribute or ensure dspace:participantId is in the service data.", ); } // Start negotiation this.negotiationStatus = "negotiating"; this.negotiationError = undefined; this.requestUpdate(); // The policy already has the target field set by FederatedCatalogueStore // and all urn:tems: prefixes have been stripped const processedPolicy = policy; // Initiate contract negotiation const negotiationId = await dspStore.negotiateContract( counterPartyAddress, datasetId, processedPolicy, counterPartyId, ); this.negotiationId = negotiationId; this.negotiationStatus = "pending"; this.requestUpdate(); // Save initial contract state to catalog this._saveInitialContractState(negotiationId); // Poll for negotiation status await this._pollNegotiationStatus(dspStore, negotiationId); } catch (error) { console.error("Contract negotiation failed:", error); this.negotiationStatus = "failed"; this.negotiationError = (error as Error).message; // Update contract state if negotiation was initiated if (this.negotiationId) { this._updateContractState( this.negotiationId, "FAILED", this.negotiationError, ); } this.requestUpdate(); } } async _pollNegotiationStatus(dspStore: any, negotiationId: string) { const maxAttempts = 8; const pollInterval = 5000; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const status = await dspStore.getNegotiationStatus(negotiationId); this.currentState = status.state; this.attempt = attempt + 1; this.maxAttempts = maxAttempts; this.requestUpdate(); if (status.state === "FINALIZED" || status.state === "AGREED") { // Retrieve contract agreement (pass providerId to properly key the agreement) const obj = this.object as any; const providerId = obj.counterPartyId || obj._providerParticipantId || obj._provider || ""; try { const agreement = await dspStore.getContractAgreement( negotiationId, providerId, ); this.contractId = agreement ? agreement["@id"] : status.contractAgreementId || negotiationId; } catch (error) { console.error("Failed to retrieve contract agreement:", error); this.contractId = status.contractAgreementId || negotiationId; } // Save agreement info to localStorage for persistence if (this.contractId && negotiationId) { this._saveAgreementInfo(this.contractId, negotiationId, Date.now()); } // Save contract to DSP Contract Storage for catalog display if (this.contractId) { this._saveToContractCatalog(this.contractId, negotiationId); } this.negotiationStatus = "granted"; this.requestUpdate(); return; } if (status.state === "TERMINATED") { this.negotiationStatus = "failed"; this.negotiationError = status.errorDetail || "Negotiation terminated"; this._updateContractState( negotiationId, "TERMINATED", this.negotiationError, ); this.requestUpdate(); return; } await new Promise((resolve) => setTimeout(resolve, pollInterval)); } catch (error) { console.error( `Error polling negotiation status (attempt ${attempt + 1}):`, error, ); await new Promise((resolve) => setTimeout(resolve, pollInterval)); } } // Timeout this.negotiationStatus = "failed"; this.negotiationError = "Negotiation timeout after 40 seconds - may still be processing on provider side"; this._updateContractState(negotiationId, "FAILED", this.negotiationError); this.requestUpdate(); } // ============================================================================ // EDR (Endpoint Data Reference) Data Access Methods // ============================================================================ // These methods provide UI coordination for EDR-based data access. // They delegate business logic to the DataspaceConnectorStore (sib-core) // and focus on UI state management. // // Architecture: // - tems-modal: UI layer (state, progress, errors, user interaction) // - dspStore (DataspaceConnectorStore): Business logic (HTTP calls, polling, auth) // // Flow: // 1. _initiateEDRTransfer() → dspStore.initiateEDRTransfer() + getEDRToken() // 2. _accessData() → _fetchDataWithLongPolling() → dspStore.fetchWithEDRToken() // ============================================================================ /** * Initiate EDR transfer for HTTP Pull data access * Delegates to the DataspaceConnectorStore */ async _initiateEDRTransfer() { try { this.gettingEDR = true; this.transferError = undefined; this.requestUpdate(); // Use the DSP store passed as property if (!this.dspStore) { throw new Error("DSP connector not configured."); } const obj = this.object as any; const assetId = obj.datasetId || obj.assetId; const counterPartyAddress = obj.counterPartyAddress; if (!assetId) { throw new Error("No asset ID found in service object"); } if (!counterPartyAddress) { throw new Error("No provider endpoint address found"); } if (!this.contractId) { throw new Error( "No contract agreement available. Please complete contract negotiation first.", ); } // Get providerParticipantId to properly key the agreement mapping const providerId = obj.counterPartyId || obj._providerParticipantId || obj._provider || ""; // Store delegates to DataspaceConnectorStore.initiateEDRTransfer() // which handles the transfer process creation const transferId = await this.dspStore.initiateEDRTransfer( assetId, counterPartyAddress, this.contractId, providerId, ); // Store delegates to DataspaceConnectorStore.getEDRToken() // which handles polling (10 attempts × 2s) and returns EDR data address const edrDataAddress = await this.dspStore.getEDRToken(transferId); if (!edrDataAddress) { throw new Error("Failed to retrieve EDR token"); } // Transform localhost endpoint to public provider address if needed let transformedEndpoint = edrDataAddress.endpoint; if ( transformedEndpoint.includes("localhost") || transformedEndpoint.includes("127.0.0.1") ) { const providerBase = counterPartyAddress.replace("/protocol", ""); const localUrl = new URL(transformedEndpoint); transformedEndpoint = `${providerBase}${localUrl.pathname}${localUrl.search}`; } // Store the EDR information for data access this.transferId = transferId; this.edrToken = edrDataAddress.authorization; this.edrEndpoint = transformedEndpoint; } catch (error) { console.error("❌ EDR transfer failed:", error); this.transferError = (error as Error).message; } finally { this.gettingEDR = false; this.requestUpdate(); } } /** * Access data using EDR token with long-polling strategy * Delegates to the DataspaceConnectorStore for actual data fetching */ async _accessData() { try { this.accessingData = true; this.dataAccessError = undefined; this.dataResult = undefined; this.requestUpdate(); if (!this.dspStore) { throw new Error("DSP connector not configured."); } if (!this.edrToken || !this.edrEndpoint) { throw new Error( 'No EDR token available. Please click "Get EDR Token" first.', ); } const obj = this.object as any; const counterPartyAddress = obj.counterPartyAddress; // Transform localhost endpoint to public provider address if needed let transformedEndpoint = this.edrEndpoint; if ( transformedEndpoint.includes("localhost") || transformedEndpoint.includes("127.0.0.1") ) { const providerBase = counterPartyAddress.replace("/protocol", ""); const localUrl = new URL(transformedEndpoint); transformedEndpoint = `${providerBase}${localUrl.pathname}${localUrl.search}`; } // Build EDR data address object for the store const edrDataAddress = { endpoint: transformedEndpoint, authorization: this.edrToken, authType: "bearer", type: "https://w3id.org/idsa/v4.1/HTTP", endpointType: "https://w3id.org/idsa/v4.1/HTTP", }; // Implement UI-level long-polling with progress feedback // The store's fetchWithEDRToken handles the actual HTTP request const data = await this._fetchDataWithLongPolling(edrDataAddress); this.dataResult = data; } catch (error) { console.error("❌ Data access failed:", error); this.dataAccessError = (error as Error).message; } finally { this.accessingData = false; this.dataAccessAttempt = undefined; this.dataAccessMaxAttempts = undefined; this.countdown = undefined; this.requestUpdate(); } } /** * Fetch data with long-polling retry logic and UI progress updates * Delegates actual fetching to DataspaceConnectorStore.fetchWithEDRToken() */ async _fetchDataWithLongPolling( edrDataAddress: any, maxAttempts = 12, // 12 attempts over 1 minute pollInterval = 5000, // 5 seconds between attempts ): Promise { this.dataAccessMaxAttempts = maxAttempts; for (let attempt = 1; attempt <= maxAttempts; attempt++) { this.dataAccessAttempt = attempt; this.requestUpdate(); try { // Delegate to store's fetchWithEDRToken method // Store handles the actual HTTP request with proper headers const data = await this.dspStore.fetchWithEDRToken(edrDataAddress); if (data) { return data; } } catch (error) { const errorMessage = (error as Error).message; console.warn( `⚠️ Data access attempt ${attempt}/${maxAttempts} failed:`, errorMessage, ); // Check for specific error types that might resolve with waiting const isRetryableError = errorMessage.includes("404") || errorMessage.includes("503") || errorMessage.includes("502") || errorMessage.includes("timeout") || errorMessage.includes("not ready") || errorMessage.includes("processing"); // If this is the last attempt or not a retryable error, throw if (attempt === maxAttempts || !isRetryableError) { console.error(`❌ Final data access attempt failed: ${errorMessage}`); throw error; } } // Wait before next attempt (except on the last iteration) if (attempt < maxAttempts) { // Show countdown in UI during wait for (let countdown = pollInterval / 1000; countdown > 0; countdown--) { this.countdown = countdown; this.requestUpdate(); await new Promise((resolve) => setTimeout(resolve, 1000)); } this.countdown = undefined; } } throw new Error( `Data access failed after ${maxAttempts} attempts over ${(maxAttempts * pollInterval) / 1000} seconds`, ); } _renderBoolean(field: boolean): TemplateResultOrSymbol { if (field) { return html``; } return html``; } _renderDivision(type: string, label: string): TemplateResult { return html`
${unsafeHTML(String(label))}
`; } _renderBadge(type?: string, label?: string): TemplateResultOrSymbol { if (!label) return nothing; return html``; } _renderButton( iconLeft?: TemplateResult, size?: string, label?: string, type?: string, url?: string, iconRight?: TemplateResult, disabled?: boolean, ): TemplateResultOrSymbol { if (!label) return nothing; return html``; } _renderIframe(url: string): TemplateResult { return html``; } _renderKindBadgeComponent( object: rdf.DataOffer | undefined = undefined, ): TemplateResultOrSymbol { const data_offer = object || this.object; if (!data_offer.offers || data_offer.offers.length === 0) return nothing; return html`
${data_offer.offers.map((offer: rdf.Offer) => this._renderBadge("information", offerKindHandler(offer.kind)), )}
`; } _renderCategoryBadgeComponent(): TemplateResultOrSymbol { const badgeType: string = this.isType(rdf.RDFTYPE_DATAOFFER) ? "default" : "information"; if (!this.object.categories || this.object.categories.length === 0) return nothing; return html`
${this.object.categories.length === 0 ? this._renderBadge(badgeType, msg("No category")) : this.object.categories.map((category: rdf.NamedResource) => this._renderBadge(badgeType, category.name || ""), )}
`; } _renderDescription(): TemplateResult { return this._renderDivision("body-m", this.object.description); } _renderTitleValueDivision( title: string, value: string | undefined, ): TemplateResultOrSymbol { if (!value) return nothing; return html`${this._renderDivision("h4", title)} ${this._renderDivision("body-m", value)}`; } _renderLicences(): TemplateResult { return html`
${this.object.licences.length !== 0 ? html`${this._renderDivision("h4", msg("Licences"))} ${this.object.licences.map((licence: rdf.Licence) => { if (!licence.name) return nothing; return html` ${licence.url ? html`${licence.name || msg("See more")} ` : html`${licence.name}`} `; })}` : html`${this._renderDivision("h4", msg("Licences"))} -`}
`; } _renderBgImg(imgSrc: string, className: string) { if (!imgSrc) { return nothing; } return html`
`; } _renderImageSingle(): TemplateResultOrSymbol { if (!this.object.image && !this.object.images) { return nothing; } const images = []; if (this.object.image) { images.push(this.object.image); } if (this.object.images) { images.push(...this.object.images); } return html`
${images.map((image: rdf.Image) => { if (image.iframe && image.url) { return html`${this._renderIframe(image.url)}`; } return html`${ifDefined(image.name)}
`; })} `; } _renderImageArray(): TemplateResultOrSymbol { const iframe = this.object.images.filter( (image: rdf.Image) => image.iframe && image.url, ); if (iframe.length > 0) { return html`${this._renderIframe(iframe[0].url)}`; } const filteredImages = this.object.images.filter( (image: rdf.Image) => !image.iframe && image.url, ); const imgCount = filteredImages.length; switch (imgCount) { case 0: return nothing; case 1: return html`
`; case 2: return html`
${this._renderBgImg(filteredImages[0].url, "full-width")} ${this._renderBgImg(filteredImages[1].url, "full-width")}
`; case 3: return html`
${this._renderBgImg(filteredImages[0].url, "full-width")}
${this._renderBgImg(filteredImages[1].url, "")} ${this._renderBgImg(filteredImages[2].url, "")}
`; default: return html`
${this._renderBgImg(filteredImages[0].url, "full-width")}
${this._renderBgImg(filteredImages[1].url, "")} ${this._renderBgImg(filteredImages[2].url, "")}
${this._renderBgImg(filteredImages[3].url, "last-img")}
`; } } _renderAboutProvider(): TemplateResultOrSymbol { if (this.object.providers.length === 0) return nothing; return html`${this._renderDivision("h4", msg("Providers"))} ${this.object.providers.map( (provider: rdf.Provider) => html`
${provider.name}
${this._renderTitleValueDivision( msg("About the providers"), provider.description || msg("No description provided"), )}`, )}`; } _renderCompatibleServices(): TemplateResultOrSymbol { if (this.object.services.length === 0) return nothing; return html`${this._renderDivision( "h4", this.isType(rdf.RDFTYPE_PROVIDER) ? msg("Available Services") : msg("Compatible Services"), )} ${this.object.services.map( (service: rdf.Service) => html``, )}`; } _renderCompatibleDataOffers(): TemplateResultOrSymbol { if (this.object.data_offers.length === 0) return nothing; return html`${this._renderDivision("h4", msg("Available Data Offers"))} ${this.object.data_offers.map( (data_offer: rdf.DataOffer) => html``, )}`; } // tags=${this._renderKindBadgeComponent(data_offer)} _renderOffers(): TemplateResult { return html`${this._renderDivision("h4", msg("Offers"))} ${this.object.offers.map((offer: rdf.Offer) => { const msgSubscribe: string = offerKindActionHandler(offer.kind); if (!msgSubscribe) return nothing; return html`
${this._renderButton( undefined, "sm", msgSubscribe, "primary", undefined, undefined, true, )}
`; })}`; } _renderDataOfferBadgeRow(): TemplateResult { return html`
${this.renderTemplateWhenWith(["offers"], this._renderKindBadgeComponent)} ${this.renderTemplateWhenWith( ["categories"], this._renderCategoryBadgeComponent, )}
`; } _renderColumns(...columns: TemplateResultOrSymbol[]): TemplateResultOrSymbol { const filteredColumns = columns.filter((col) => col !== nothing); if (filteredColumns.length === 1) { return columns[0]; } return html`
${filteredColumns.map( (col) => html`
${col}
`, )}
`; } _renderApiAccessGuide(): TemplateResultOrSymbol { if (this.dspStore) { return this._renderEDRAccessGuide(); } return nothing; } _renderEDRAccessGuide(): TemplateResultOrSymbol { const obj = this.object as any; const connectorUri = obj.counterPartyAddress || ""; const agreementId = this.contractId || ""; const assetId = obj.datasetId || ""; return html`
${this._renderDivision("h4", msg("Dataspace Protocol Access Guide"))}
${msg("Data is accessed via the Eclipse Dataspace Protocol (HTTP Pull). After contract negotiation, initiate a transfer to obtain an EDR (Endpoint Data Reference) token for direct data access.")}
${msg("Step 1: Initiate Transfer Process")}
curl -X POST '<consumer-connector>/v3/transferprocesses' \\
  -H 'Content-Type: application/json' \\
  -H 'X-Api-Key: <your-api-key>' \\
  -d '{
    "@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" },
    "@type": "TransferRequest",
    "assetId": "${assetId}",
    "protocol": "dataspace-protocol-http",
    "counterPartyAddress": "${connectorUri}",
    "contractId": "${agreementId}",
    "transferType": "HttpData-PULL",
    "dataDestination": { "type": "HttpProxy" }
  }'
${msg("Response")}: { "@id": "<transfer-id>", ... }
${msg("Step 2: Get EDR Token (poll until ready)")}
curl -X GET '<consumer-connector>/v3/edrs/<transfer-id>/dataaddress' \\
  -H 'X-Api-Key: <your-api-key>'
${msg("Response")}: { "endpoint": "https://...", "authorization": "eyJ..." }
${msg("Step 3: Access Data")}
curl -X GET '<endpoint-from-step-2>' \\
  -H 'Authorization: <authorization-token-from-step-2>'
${this.negotiationStatus === "granted" && this.contractId ? html`✅ ${msg("Agreement ID")}: ${this.contractId}` : html`⚠️ ${msg("You need to complete contract negotiation first to get an agreement ID.")}` }
`; } _renderServiceSpecificModal(): TemplateResultOrSymbol { return html` ${this._renderColumns( html`${this.renderTemplateWhenWith(["long_description"], () => this._renderTitleValueDivision( msg("Features & Functionalities"), this.object.long_description, ), )}${this.renderTemplateWhenWith( [["is_in_app", "is_external", "is_api"]], () => html`${this._renderDivision("h4", msg("Installation Possible"))}
${this.renderTemplateWhenWith( ["is_in_app"], () => html`${this._renderBadge("success", msg("In-App"))}
`, )} ${this.renderTemplateWhenWith( ["is_external"], () => html`${this._renderBadge("success", msg("External"))}`, )} ${this.renderTemplateWhenWith( ["is_api"], () => html`${this._renderBadge("success", msg("API"))}`, )} `, )}${this.renderTemplateWhenWith( ["developper", "developper.url"], () => html`${this._renderDivision("h4", msg("Developper"))} ${this.object.developper.name}`, )}${this.renderTemplateWhenWith(["release_date"], () => this._renderTitleValueDivision( msg("Release Date"), formatDate(this.object.release_date), ), )}${this.renderTemplateWhenWith(["last_update"], () => this._renderTitleValueDivision( msg("Last Update"), formatDate(this.object.last_update), ), )}${this.renderTemplateWhenWith(["url"], () => this._renderButton( undefined, "sm", "Access the service", "outline-gray", this.object.url, ), )}${this.renderTemplateWhenWith(["documentation_url"], () => this._renderButton( undefined, "sm", "Read the full documentation", "outline-gray", this.object.documentation_url, ), )} `, )}`; } _isOwnOffering(): boolean { const obj = this.object as any; const counterPartyId: string = obj?.counterPartyId || obj?._provider?.participantId || ""; const counterPartyAddress: string = obj?.counterPartyAddress || obj?._provider?.address || ""; if (this.participantId && counterPartyId) { if (counterPartyId === this.participantId) return true; if (typeof counterPartyId === "string" && counterPartyId.includes(this.participantId)) { return true; } } if (this.participantConnectorUri && counterPartyAddress) { try { const a = new URL(counterPartyAddress); const b = new URL(this.participantConnectorUri); if (a.hostname === b.hostname && a.port === b.port) return true; } catch {} } return false; } _renderNegotiationSection(): TemplateResultOrSymbol { return html` ${this._renderPolicyDescription()} ${this._renderAgreementInfo()} ${this._renderApiAccessGuide()} ${this._renderEDRDataAccessSection()} `; } _renderPolicyDescription(): TemplateResultOrSymbol { const obj = this.object as any; let policy = obj.policy; let policies = obj.policies; // DEFENSIVE: Handle case where obj.policy might be an array if (Array.isArray(policy)) { // Extract valid policies from array const extractedPolicies = policy.filter( (item: any) => item && typeof item === "object" && item["@id"], ); if (extractedPolicies.length > 0) { policies = extractedPolicies; policy = extractedPolicies[0]; } } // DEFENSIVE: Handle case where obj.policy has numeric keys else if (policy && typeof policy === "object") { const keys = Object.keys(policy); const hasNumericKeys = keys.some((k) => /^\d+$/.test(k)); if (hasNumericKeys) { const extractedPolicies = []; for (const key in policy) { if (/^\d+$/.test(key)) { extractedPolicies.push(policy[key]); } } if (extractedPolicies.length > 0) { policies = extractedPolicies; policy = extractedPolicies[0]; } } } // Check if we have multiple policies const hasMultiplePolicies = policies && Array.isArray(policies) && policies.length > 1; // Only show if there's a policy if (!policy && (!policies || policies.length === 0)) { return nothing; } return html`
${hasMultiplePolicies ? html` ${this._renderDivision("h4", msg("Access Policies"))}
${msg("Multiple contract policies available for this asset")} (${policies.length})
${policies.map( (p: any, index: number) => html`
${msg("Policy")} ${index + 1} ${p["@id"] ? html` ${p["@id"]} ` : nothing}
`, )} ` : html` ${this._renderDivision("h4", msg("Access Policy"))} `}
`; } _renderAgreementInfo(): TemplateResultOrSymbol { // Show agreement info after successful negotiation if (this.negotiationStatus !== "granted" || !this.contractId) { return nothing; } const storedInfo = this._loadAgreementInfo(); const agreementDate = storedInfo?.timestamp ? new Date(storedInfo.timestamp).toLocaleString() : null; // Get endpoint URL from asset const obj = this.object as any; const endpointUrl = obj?.endpointUrl || obj?.["dcat:endpointURL"] || obj?.distribution?.endpointUrl; const assetId = obj?.datasetId || obj?.assetId; return html`
${this._renderDivision("h4", msg("Contract Agreement"))}
✅ ${msg("Agreement ID:")}
${this.contractId}
${assetId ? html`
${msg("Asset ID:")}
${assetId}
` : nothing} ${endpointUrl ? html`
🔗 ${msg("Endpoint URL:")}
${endpointUrl}
` : nothing} ${agreementDate ? html`
${msg("Agreed on:")} ${agreementDate}
` : nothing}
ℹ️ ${msg("Note:")}
${msg( "You can now use this agreement ID to access the service through the provider's API or data gateway.", )}
${storedInfo ? html`
` : nothing}
`; } _renderEDRDataAccessSection(): TemplateResultOrSymbol { // Only show for services with negotiated access if (this.negotiationStatus !== "granted") { return nothing; } const storedInfo = this._loadAgreementInfo(); const agreementDate = storedInfo?.timestamp ? new Date(storedInfo.timestamp).toLocaleString() : null; return html`
${this._renderDivision("h4", msg("EDR Data Access (HTTP Pull)"))} ${this.contractId ? html`
Agreement ID: ${this.contractId}
${agreementDate ? html`
Agreed on: ${agreementDate}
` : nothing} ${this.transferId ? html`
Transfer ID: ${this.transferId}
` : nothing}
` : nothing}
${this.gettingEDR ? html` ${msg("Getting EDR token...")} ` : !this.edrToken ? html` 🚀 ${msg("Get EDR Token")} ` : html`
✅ ${msg("EDR Token Ready")} ${this.edrEndpoint ? html`
Endpoint: ${this.edrEndpoint}
` : nothing} ${this.edrToken ? html`
${msg("Token")}: ${this.edrToken}
` : nothing}
`} ${this.transferError ? html`
⚠️ ${this.transferError} 🔄 ${msg("Retry")}
` : nothing} ${this.displayServiceTest && this.edrToken ? this.accessingData ? html` 📡 ${msg("Accessing data")} ${this.dataAccessAttempt ? ` (${this.dataAccessAttempt}/${this.dataAccessMaxAttempts})` : "..."} ${this.countdown ? html`
⏳ Next retry in ${this.countdown}s` : nothing}
` : html` 📁 ${msg("Access Data")} ` : nothing} ${this.displayServiceTest && this.dataAccessError ? html`
⚠️ ${this.dataAccessError}
` : nothing} ${this.displayServiceTest && this.dataResult ? html`
✅ ${msg("Data retrieved successfully:")}
${JSON.stringify(this.dataResult, null, 2)}
` : nothing}
ℹ️ ${msg("About EDR (Endpoint Data Reference)")}
${msg( "EDR is the Eclipse Dataspace Protocol's HTTP Pull mechanism for accessing data. The EDR token provides temporary, authorized access to the provider's data endpoint.", )}
${storedInfo ? html`
` : nothing}
`; } _renderModal(): TemplateResultOrSymbol { if (!this.object || !this.object["@type"]) { return nothing; } return html`${this.renderTemplateWhenWith( [rdf.RDFTYPE_OBJECT, "images"], this._renderImageArray, )} `; } _closeModal() { this.dispatchEvent(new CustomEvent("close")); } _addBookmark() { this.dispatchEvent( new CustomEvent("bookmark", { detail: { add: true, object: this.object }, }), ); } _removeBookmark() { this.dispatchEvent( new CustomEvent("bookmark", { detail: { add: false, object: this.object }, }), ); } _purchase() { console.warn(msg("Disabled for POC")); this.dispatchEvent(new CustomEvent("purchase")); } _renderPolicySelection(): TemplateResult { const policies = this.availablePolicies || []; return html`

${msg("Select a Policy")}

${msg( "Multiple policies are available for this dataset. Please select one to proceed with the negotiation.", )}

${policies.map( (policy: any, index: number) => html`
this._selectPolicy(index)} >
${msg("Policy")} ${index + 1} ${policy["@id"] ? html`${policy["@id"]}` : nothing}
${unsafeHTML(this._formatPolicyDetails(policy))}
`, )}
${msg("Cancel")}
`; } _renderNegotiationButton(): TemplateResultOrSymbol { if (!this.dspStore) { return nothing; } // Show policy selection if needed if (this.showPolicySelection) { return this._renderPolicySelection(); } switch (this.negotiationStatus) { case "idle": return html`${msg("Negotiate access")}`; case "negotiating": return html` ${msg("Negotiating...")} `; case "pending": return html` ${this.currentState || msg("Pending")} ${this.attempt ? `(${this.attempt}/${this.maxAttempts})` : ""} `; case "granted": { return html` ✅ ${msg("Access Granted")} `; } case "failed": return html`
❌ ${msg("Failed")}: ${this.negotiationError || msg("Unknown error")} ${msg("Retry")}
`; default: return html`${msg("Activate this service")}`; } } render() { return html``; } }