/** * EcosystemBridge — Loads external r-ecosystem app manifests and dynamically * imports their Web Component modules for canvas embedding. * * Two embedding modes: * 1. Trusted: dynamic import() → registers Web Component → shares CRDT + ports + events * 2. Sandboxed: creates iframe with sandbox attr → postMessage bridge → origin-validated * * Usage: * const bridge = EcosystemBridge.getInstance(); * const manifest = await bridge.loadManifest("rwallet"); * await bridge.loadModule(manifest); // registers shapes in shapeRegistry * // or * const iframe = bridge.createSandboxedEmbed(manifest, shapeDesc, container); */ import type { EcosystemManifest, ResolvedManifest, EcosystemShapeDescriptor, } from "../shared/ecosystem-manifest"; import { ECOSYSTEM_PROTOCOL_VERSION } from "../shared/ecosystem-manifest"; import { shapeRegistry } from "./shape-registry"; // ── Types ── interface CachedManifest { manifest: ResolvedManifest; fetchedAt: number; } interface SandboxedEmbed { iframe: HTMLIFrameElement; appId: string; origin: string; destroy: () => void; } // ── Singleton ── let instance: EcosystemBridge | null = null; export class EcosystemBridge { /** Cached manifests keyed by appId */ #manifests = new Map(); /** Loaded module URLs (to avoid double-import) */ #loadedModules = new Set(); /** Active sandboxed embeds */ #sandboxedEmbeds = new Map(); /** postMessage handler bound reference for cleanup */ #messageHandler: ((e: MessageEvent) => void) | null = null; /** Cache TTL for manifests (1 hour) */ static MANIFEST_TTL = 3600_000; static getInstance(): EcosystemBridge { if (!instance) { instance = new EcosystemBridge(); } return instance; } constructor() { // Listen for postMessage from sandboxed iframes this.#messageHandler = this.#handleSandboxMessage.bind(this); if (typeof window !== "undefined") { window.addEventListener("message", this.#messageHandler); } } // ── Manifest loading ── /** * Load an ecosystem app's manifest via the server proxy (avoids CORS). * Results are cached for MANIFEST_TTL. */ async loadManifest(appId: string): Promise { // Check cache const cached = this.#manifests.get(appId); if (cached && Date.now() - cached.fetchedAt < EcosystemBridge.MANIFEST_TTL) { return cached.manifest; } const res = await fetch(`/api/ecosystem/${encodeURIComponent(appId)}/manifest`); if (!res.ok) { throw new Error(`Failed to load manifest for ${appId}: ${res.status}`); } const data: ResolvedManifest = await res.json(); // Validate protocol version if (data.minProtocolVersion && data.minProtocolVersion > ECOSYSTEM_PROTOCOL_VERSION) { throw new Error( `App ${appId} requires protocol v${data.minProtocolVersion}, ` + `but this rSpace supports v${ECOSYSTEM_PROTOCOL_VERSION}` ); } this.#manifests.set(appId, { manifest: data, fetchedAt: Date.now() }); return data; } /** Get a cached manifest without fetching. */ getCachedManifest(appId: string): ResolvedManifest | null { return this.#manifests.get(appId)?.manifest ?? null; } /** Get all cached manifests. */ getAllCachedManifests(): ResolvedManifest[] { return Array.from(this.#manifests.values()).map((c) => c.manifest); } // ── Module loading (Trusted mode) ── /** * Dynamically import an ecosystem module and register its shapes. * Uses the server module proxy to avoid CORS. * * The imported module is expected to: * - Define and register custom elements (Web Components) * - Export shape classes that extend FolkShape or HTMLElement */ async loadModule(manifest: ResolvedManifest): Promise { const moduleUrl = `/api/ecosystem/${encodeURIComponent(manifest.appId)}/module`; if (this.#loadedModules.has(manifest.appId)) { return; // Already loaded } try { // Dynamic import via blob URL to execute the proxied JS as a module const res = await fetch(moduleUrl); if (!res.ok) throw new Error(`Module fetch failed: ${res.status}`); const js = await res.text(); const blob = new Blob([js], { type: "application/javascript" }); const blobUrl = URL.createObjectURL(blob); try { await import(/* @vite-ignore */ blobUrl); } finally { URL.revokeObjectURL(blobUrl); } this.#loadedModules.add(manifest.appId); // Register shapes in the shape registry (if not already registered by the module itself) this.registerShapes(manifest); // Notify service worker to cache the module this.#notifySwCache(manifest); } catch (err) { console.error(`[EcosystemBridge] Failed to load module for ${manifest.appId}:`, err); throw err; } } /** * Register manifest shapes in the shape registry. * Called automatically by loadModule(), but can also be called manually * if the module was loaded via another mechanism (e.g.