@tangle-network/blueprint-ui 0.5.0 → 0.5.1

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.
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { T as TangleIframeClient, W as WalletSnapshot, S as ServiceSnapshot, J as JobInvocation } from '../tangleIframeClient-DES8FDF0.js';
4
- export { C as ClientEventMap, a as TangleIframeClientOptions } from '../tangleIframeClient-DES8FDF0.js';
3
+ import { T as TangleIframeClient, W as WalletSnapshot, S as ServiceSnapshot, J as JobInvocation } from '../tangleIframeClient-CAyUr99p.js';
4
+ export { C as ClientEventMap, a as TangleIframeClientOptions } from '../tangleIframeClient-CAyUr99p.js';
5
5
  import { Address, PublicClient, Hex } from 'viem';
6
6
  import { J as JobInputs, b as ChainContext, m as SignTypedDataRequest } from '../parentBridgeProtocol-BS2zbIvX.js';
7
7
  export { C as CallJobRequest, d as JobResultEvent, e as JobResultStatus, S as ServiceContextBroadcast, g as ServiceContextJob, h as ServiceContextOperator, n as SignTypedDataResult } from '../parentBridgeProtocol-BS2zbIvX.js';
@@ -18,6 +18,8 @@ import {
18
18
 
19
19
  // src/iframe/tangleIframeClient.ts
20
20
  var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
21
+ var HANDSHAKE_RETRY_MS = 250;
22
+ var HANDSHAKE_RETRY_BUDGET_MS = 1e4;
21
23
  var NULL_WALLET = {
22
24
  address: null,
23
25
  chainId: null,
@@ -41,6 +43,7 @@ var TangleIframeClient = class {
41
43
  handshakeAcked = false;
42
44
  handshakeWaiters = [];
43
45
  installed = false;
46
+ handshakeRetry = null;
44
47
  listeners = {
45
48
  wallet: /* @__PURE__ */ new Set(),
46
49
  service: /* @__PURE__ */ new Set(),
@@ -53,10 +56,22 @@ var TangleIframeClient = class {
53
56
  this.installed = true;
54
57
  window.addEventListener("message", this.handleParentMessage);
55
58
  this.postHandshake();
59
+ if (this.handshakeRetry === null) {
60
+ let elapsed = 0;
61
+ this.handshakeRetry = setInterval(() => {
62
+ elapsed += HANDSHAKE_RETRY_MS;
63
+ if (this.handshakeAcked || elapsed >= HANDSHAKE_RETRY_BUDGET_MS) {
64
+ this.clearHandshakeRetry();
65
+ return;
66
+ }
67
+ this.postHandshake();
68
+ }, HANDSHAKE_RETRY_MS);
69
+ }
56
70
  }
57
71
  uninstall() {
58
72
  if (!this.installed || typeof window === "undefined") return;
59
73
  this.installed = false;
74
+ this.clearHandshakeRetry();
60
75
  window.removeEventListener("message", this.handleParentMessage);
61
76
  for (const [, pending] of this.pendingJobs) {
62
77
  clearTimeout(pending.timer);
@@ -164,6 +179,12 @@ var TangleIframeClient = class {
164
179
  });
165
180
  }
166
181
  // ── Internals ───────────────────────────────────────────────────────────
182
+ clearHandshakeRetry() {
183
+ if (this.handshakeRetry !== null) {
184
+ clearInterval(this.handshakeRetry);
185
+ this.handshakeRetry = null;
186
+ }
187
+ }
167
188
  postHandshake() {
168
189
  this.postToParent({
169
190
  kind: "tangle.app.handshake",
@@ -186,6 +207,7 @@ var TangleIframeClient = class {
186
207
  switch (message.kind) {
187
208
  case "tangle.app.handshakeAck":
188
209
  this.handshakeAcked = true;
210
+ this.clearHandshakeRetry();
189
211
  for (const resolve of this.handshakeWaiters) resolve();
190
212
  this.handshakeWaiters = [];
191
213
  return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/iframe/TangleIframeProvider.tsx","../../src/iframe/tangleIframeClient.ts","../../src/iframe/hooks.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport {\n TangleIframeClient,\n type ServiceSnapshot,\n type TangleIframeClientOptions,\n type WalletSnapshot,\n} from './tangleIframeClient';\nimport {\n detectTangleCloudParentOrigin,\n TANGLE_CLOUD_ORIGINS_DEFAULT,\n} from '../wallet/detectParentOrigin';\n\ntype Props = {\n appId: string;\n /** Override the detected parent origin (e.g. dev/staging deploys). */\n parentOrigin?: string;\n /** Extra trusted origins for `detectTangleCloudParentOrigin`. */\n extraOrigins?: readonly string[];\n /**\n * Override the bootstrap behavior. When `'auto'` (default), the SDK\n * sniffs the embed context: real parent → install the bridge, top-frame\n * → drop into dev mode. `'bridge'` forces real-parent mode and throws\n * if no parent is detected. `'dev'` forces dev mode even when embedded\n * — useful for component-level tests.\n */\n mode?: 'auto' | 'bridge' | 'dev';\n children: ReactNode;\n};\n\ntype ContextValue = {\n readonly client: TangleIframeClient | null;\n readonly wallet: WalletSnapshot;\n readonly service: ServiceSnapshot;\n readonly mode: 'bridge' | 'dev';\n readonly isReady: boolean;\n};\n\nconst TangleIframeContext = createContext<ContextValue | null>(null);\n\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n chain: null,\n};\n\n/**\n * Iframe-blueprint root provider. Wrap your app once at the entry point.\n *\n * In `auto` mode (default) the SDK detects whether the app is embedded by a\n * trusted Tangle Cloud parent. If yes → installs the postMessage bridge.\n * If no (running standalone at `localhost:5173` etc.) → enters **dev mode**\n * with an in-memory state machine that the developer can drive via the\n * exported debug controls. Dev mode keeps the hook surface identical to\n * production so component code never branches on embed-vs-not.\n *\n * Three lifecycle stages:\n *\n * 1. Mount — `client` is created, mode is decided.\n * 2. Bootstrap — handshake (bridge) or first-paint setup (dev). The\n * `isReady` flag flips to true.\n * 3. Active — wallet + service snapshots flow in via subscriptions.\n */\nexport function TangleIframeProvider({\n appId,\n parentOrigin: explicitOrigin,\n extraOrigins,\n mode: requestedMode = 'auto',\n children,\n}: Props) {\n // Resolve the effective mode once at mount. Switching modes mid-session\n // would tear down the bridge / dev state inconsistently; restart instead.\n const resolution = useMemo(() => {\n if (requestedMode === 'dev') {\n return { mode: 'dev' as const, parentOrigin: null };\n }\n const detected =\n explicitOrigin ?? detectTangleCloudParentOrigin({ extraOrigins });\n if (requestedMode === 'bridge') {\n if (!detected) {\n // eslint-disable-next-line no-console\n console.error(\n '[TangleIframeProvider] mode=\"bridge\" but no trusted parent was detected. Falling back to dev mode.',\n );\n return { mode: 'dev' as const, parentOrigin: null };\n }\n return { mode: 'bridge' as const, parentOrigin: detected };\n }\n // auto: bridge when detected, dev otherwise.\n return detected\n ? { mode: 'bridge' as const, parentOrigin: detected }\n : { mode: 'dev' as const, parentOrigin: null };\n }, [requestedMode, explicitOrigin, extraOrigins]);\n\n const clientRef = useRef<TangleIframeClient | null>(null);\n const [wallet, setWallet] = useState<WalletSnapshot>(NULL_WALLET);\n const [service, setService] = useState<ServiceSnapshot>(NULL_SERVICE);\n const [isReady, setIsReady] = useState(false);\n\n useEffect(() => {\n if (resolution.mode === 'dev') {\n // Dev mode: no bridge. The DevHarness component (or a test) seeds\n // wallet + service via `setDevWallet` / `setDevService` on the\n // returned context. Mark ready immediately so app code unblocks.\n setIsReady(true);\n return undefined;\n }\n // Bridge mode\n const options: TangleIframeClientOptions = {\n parentOrigin: resolution.parentOrigin,\n appId,\n };\n const client = new TangleIframeClient(options);\n clientRef.current = client;\n const unsubWallet = client.subscribe('wallet', setWallet);\n const unsubService = client.subscribe('service', setService);\n client.install();\n setIsReady(true);\n return () => {\n unsubWallet();\n unsubService();\n client.uninstall();\n clientRef.current = null;\n setIsReady(false);\n };\n }, [resolution, appId]);\n\n const value = useMemo<ContextValue>(\n () => ({\n client: clientRef.current,\n wallet,\n service,\n mode: resolution.mode,\n isReady,\n }),\n [wallet, service, resolution.mode, isReady],\n );\n\n return (\n <TangleIframeContext.Provider value={value}>\n {children}\n </TangleIframeContext.Provider>\n );\n}\n\nexport function useTangleIframeContext(): ContextValue {\n const ctx = useContext(TangleIframeContext);\n if (!ctx) {\n throw new Error(\n 'useTangleIframeContext must be used inside <TangleIframeProvider>.',\n );\n }\n return ctx;\n}\n\nexport { TANGLE_CLOUD_ORIGINS_DEFAULT };\n","// Thin-iframe SDK client — the framework-agnostic state machine that talks\n// to a Tangle Cloud parent dapp over postMessage. React hooks (below) are\n// thin wrappers around an instance of this class.\n//\n// Why a class, not a bag of functions: the iframe lifecycle is stateful —\n// handshake, account changes, service-context broadcasts, in-flight job\n// requests. The class owns that state once; hooks subscribe via listeners.\n// Testing the protocol shape doesn't require React.\n\nimport type { Address, Hex } from 'viem';\n\nimport {\n makeCorrelationId,\n NO_WALLET_ADDRESS,\n TANGLE_IFRAME_PROTOCOL_VERSION,\n type CallJobRequest,\n type ChainContext,\n type JobInputs,\n type JobResultEvent,\n type JobResultStatus,\n type ParentMessage,\n type ServiceContextBroadcast,\n type ServiceContextJob,\n type ServiceContextOperator,\n type SignTypedDataRequest,\n} from '../wallet/parentBridgeProtocol';\n\nexport type WalletSnapshot = {\n readonly address: Address | null;\n readonly chainId: number | null;\n readonly isConnected: boolean;\n};\n\nexport type ServiceSnapshot = {\n readonly blueprintId: string | null;\n readonly serviceId: string | null;\n readonly operators: readonly ServiceContextOperator[];\n readonly jobs: readonly ServiceContextJob[];\n readonly mode: string | null;\n /** Chain context broadcast by the parent — drives `useTanglePublicClient`.\n * `null` when the parent hasn't sent one (older parent or dev mode). */\n readonly chain: ChainContext | null;\n};\n\nexport type JobInvocation = {\n readonly correlationId: string;\n readonly status: JobResultStatus;\n readonly data?: unknown;\n readonly chunks: readonly unknown[];\n readonly error?: string;\n readonly progress?: { readonly percent?: number; readonly eta_ms?: number };\n};\n\nexport type ClientEventMap = {\n wallet: WalletSnapshot;\n service: ServiceSnapshot;\n job: JobInvocation;\n};\n\ntype Listener<K extends keyof ClientEventMap> = (\n value: ClientEventMap[K],\n) => void;\n\nexport type TangleIframeClientOptions = {\n /**\n * Origin of the parent dapp. The client posts every message with this\n * exact `targetOrigin` and rejects inbound messages from any other origin.\n * Pass `'*'` only in dev — production must pin to the real parent\n * (`https://cloud.tangle.tools` etc.).\n */\n parentOrigin: string;\n /**\n * Stable identifier for this iframe app. The parent surfaces it in\n * handshake logs + uses it for permission scoping.\n */\n appId: string;\n /**\n * Per-request timeout. Defaults to 60s — long enough for a user to\n * read + approve a signing prompt in the parent. Long-running jobs\n * stream progress events; the request \"completes\" only on terminal\n * status, so the timeout protects against parents that drop replies\n * entirely.\n */\n requestTimeoutMs?: number;\n};\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000;\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n chain: null,\n};\n\ntype PendingJob = {\n resolve: (value: JobInvocation) => void;\n reject: (reason: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n invocation: JobInvocation;\n};\n\nexport class TangleIframeClient {\n private wallet: WalletSnapshot = NULL_WALLET;\n private service: ServiceSnapshot = NULL_SERVICE;\n private handshakeAcked = false;\n private handshakeWaiters: Array<() => void> = [];\n private installed = false;\n private listeners: {\n [K in keyof ClientEventMap]: Set<Listener<K>>;\n } = {\n wallet: new Set(),\n service: new Set(),\n job: new Set(),\n };\n private pendingJobs = new Map<string, PendingJob>();\n\n constructor(private readonly options: TangleIframeClientOptions) {}\n\n /** Wire the global message listener + initial handshake. Idempotent. */\n install(): void {\n if (this.installed || typeof window === 'undefined') return;\n this.installed = true;\n window.addEventListener('message', this.handleParentMessage);\n this.postHandshake();\n }\n\n uninstall(): void {\n if (!this.installed || typeof window === 'undefined') return;\n this.installed = false;\n window.removeEventListener('message', this.handleParentMessage);\n for (const [, pending] of this.pendingJobs) {\n clearTimeout(pending.timer);\n pending.reject(new Error('Tangle iframe client uninstalled'));\n }\n this.pendingJobs.clear();\n }\n\n // ── State accessors ─────────────────────────────────────────────────────\n\n getWallet(): WalletSnapshot {\n return this.wallet;\n }\n getService(): ServiceSnapshot {\n return this.service;\n }\n\n // ── Subscription API (used by React hooks) ──────────────────────────────\n\n subscribe<K extends keyof ClientEventMap>(\n event: K,\n listener: Listener<K>,\n ): () => void {\n this.listeners[event].add(listener);\n return () => {\n this.listeners[event].delete(listener);\n };\n }\n\n // ── Wallet operations ───────────────────────────────────────────────────\n\n async signMessage(message: string): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signMessage', {\n chainId: this.wallet.chainId ?? 0,\n message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n async sendTransaction(tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTransaction', {\n chainId: this.wallet.chainId ?? 0,\n to: tx.to,\n data: tx.data,\n ...(tx.value !== undefined ? { value: tx.value.toString(10) } : {}),\n }).then((data) => (data as { txHash: Hex }).txHash);\n }\n\n async switchChain(chainId: number): Promise<number> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.switchChain', { chainId }).then(\n (data) => (data as { chainId: number }).chainId,\n );\n }\n\n /**\n * EIP-712 typed-data signing. The parent renders the typed-data fields in\n * its approval modal; the user audits what they're signing. Use for\n * operator envelopes, off-chain attestations, anything that needs a\n * signature outside the standard blueprint-job RFQ flow.\n *\n * Shape mirrors viem's `signTypedData` argument. Do not include the\n * EIP712Domain entry in `types` — the parent injects it from `domain`.\n */\n async signTypedData(args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTypedData', {\n chainId: this.wallet.chainId ?? 0,\n domain: args.domain,\n types: args.types,\n primaryType: args.primaryType,\n message: args.message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n // ── Job invocation ──────────────────────────────────────────────────────\n\n /**\n * Invoke a blueprint job. Returns a Promise that resolves on terminal\n * status (`success` or `error`); subscribe to the `job` event for\n * intermediate streaming chunks.\n *\n * Streaming opt-in: pass `stream: true` if the publisher's job emits\n * chunks (LLM generation, video encoding). One-shot jobs (embeddings,\n * classifications) skip the streaming machinery.\n */\n async callJob(args: {\n jobIndex: number;\n inputs: JobInputs;\n stream?: boolean;\n }): Promise<JobInvocation> {\n await this.ensureBootstrapped();\n const correlationId = makeCorrelationId('tangle.app.callJob');\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n return new Promise<JobInvocation>((resolve, reject) => {\n const invocation: JobInvocation = {\n correlationId,\n status: 'pending',\n chunks: [],\n };\n const timer = setTimeout(() => {\n this.pendingJobs.delete(correlationId);\n reject(\n bridgeError(4900, `Job did not respond within ${timeout}ms`),\n );\n }, timeout);\n this.pendingJobs.set(correlationId, {\n resolve,\n reject,\n timer,\n invocation,\n });\n const message: CallJobRequest = {\n kind: 'tangle.app.callJob',\n correlationId,\n jobIndex: args.jobIndex,\n inputs: args.inputs,\n ...(args.stream !== undefined ? { stream: args.stream } : {}),\n };\n this.postToParent(message);\n // Emit pending immediately so consumer UIs can show optimistic state.\n this.emit('job', invocation);\n });\n }\n\n // ── Internals ───────────────────────────────────────────────────────────\n\n private postHandshake(): void {\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }\n\n private postToParent(message: object): void {\n if (typeof window === 'undefined') return;\n try {\n window.parent.postMessage(message, this.options.parentOrigin);\n } catch {\n // Cross-origin / sandboxed; defensive only — postMessage shouldn't throw.\n }\n }\n\n private handleParentMessage = (event: MessageEvent): void => {\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const message = data as ParentMessage;\n switch (message.kind) {\n case 'tangle.app.handshakeAck':\n this.handshakeAcked = true;\n for (const resolve of this.handshakeWaiters) resolve();\n this.handshakeWaiters = [];\n return;\n case 'tangle.app.readAccountResult':\n if (message.ok) {\n this.updateWallet({\n address:\n message.data.account === NO_WALLET_ADDRESS\n ? null\n : message.data.account,\n chainId: message.data.chainId,\n isConnected: message.data.account !== NO_WALLET_ADDRESS,\n });\n }\n return;\n case 'tangle.app.accountChanged':\n this.updateWallet({\n address: message.account,\n chainId: this.wallet.chainId,\n isConnected: message.account !== null,\n });\n return;\n case 'tangle.app.chainChanged':\n this.updateWallet({\n address: this.wallet.address,\n chainId: message.chainId,\n isConnected: this.wallet.isConnected,\n });\n return;\n case 'tangle.app.serviceContext':\n this.updateService(message);\n return;\n case 'tangle.app.jobResult':\n this.handleJobResult(message);\n return;\n // Wallet-shape responses (signMessageResult etc.) are routed by\n // dispatchWallet's promise resolver, not here.\n default:\n return;\n }\n };\n\n private async dispatchWallet(\n kind:\n | 'tangle.app.signMessage'\n | 'tangle.app.signTransaction'\n | 'tangle.app.signTypedData'\n | 'tangle.app.switchChain',\n payload: Record<string, unknown>,\n ): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const correlationId = makeCorrelationId(kind);\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const expectedKind = (\n {\n 'tangle.app.signMessage': 'tangle.app.signMessageResult',\n 'tangle.app.signTransaction': 'tangle.app.signTransactionResult',\n 'tangle.app.signTypedData': 'tangle.app.signTypedDataResult',\n 'tangle.app.switchChain': 'tangle.app.switchChainResult',\n } as const\n )[kind];\n const timer = setTimeout(() => {\n window.removeEventListener('message', listener);\n reject(bridgeError(4900, `Parent did not respond to ${kind} in ${timeout}ms`));\n }, timeout);\n const listener = (event: MessageEvent) => {\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const msg = data as ParentMessage;\n if (\n msg.kind !== expectedKind ||\n !('correlationId' in msg) ||\n msg.correlationId !== correlationId\n ) {\n return;\n }\n clearTimeout(timer);\n window.removeEventListener('message', listener);\n // Narrow the type — expectedKind is the wallet-shape `{ok, data|error}` envelope\n const env = msg as {\n ok: boolean;\n data?: unknown;\n error?: string;\n };\n if (env.ok) {\n resolve(env.data);\n } else {\n reject(bridgeError(4001, env.error ?? 'Parent rejected request'));\n }\n };\n window.addEventListener('message', listener);\n this.postToParent({ kind, correlationId, ...payload });\n });\n }\n\n private handleJobResult(message: JobResultEvent): void {\n const pending = this.pendingJobs.get(message.correlationId);\n if (!pending) return;\n const updated: JobInvocation = {\n correlationId: message.correlationId,\n status: message.status,\n chunks:\n message.chunk !== undefined\n ? [...pending.invocation.chunks, message.chunk]\n : pending.invocation.chunks,\n ...(message.data !== undefined ? { data: message.data } : {}),\n ...(message.error !== undefined ? { error: message.error } : {}),\n ...(message.progress !== undefined ? { progress: message.progress } : {}),\n };\n pending.invocation = updated;\n this.emit('job', updated);\n if (message.status === 'success' || message.status === 'error') {\n clearTimeout(pending.timer);\n this.pendingJobs.delete(message.correlationId);\n if (message.status === 'success') {\n pending.resolve(updated);\n } else {\n pending.reject(bridgeError(4001, message.error ?? 'Job failed'));\n }\n }\n }\n\n private updateWallet(next: WalletSnapshot): void {\n if (\n this.wallet.address === next.address &&\n this.wallet.chainId === next.chainId &&\n this.wallet.isConnected === next.isConnected\n ) {\n return;\n }\n this.wallet = next;\n this.emit('wallet', next);\n }\n\n private updateService(broadcast: ServiceContextBroadcast): void {\n const next: ServiceSnapshot = {\n blueprintId: broadcast.blueprintId,\n serviceId: broadcast.serviceId,\n operators: broadcast.operators,\n jobs: broadcast.jobs,\n mode: broadcast.mode,\n chain: broadcast.chain ?? null,\n };\n this.service = next;\n this.emit('service', next);\n }\n\n private emit<K extends keyof ClientEventMap>(\n event: K,\n value: ClientEventMap[K],\n ): void {\n for (const listener of [...this.listeners[event]]) {\n try {\n (listener as Listener<K>)(value);\n } catch {\n // Listener bugs shouldn't break the bridge.\n }\n }\n }\n\n private async ensureBootstrapped(): Promise<void> {\n if (this.handshakeAcked) return;\n this.install();\n await new Promise<void>((resolve) => {\n this.handshakeWaiters.push(resolve);\n const retry = setInterval(() => {\n if (this.handshakeAcked) {\n clearInterval(retry);\n return;\n }\n this.postHandshake();\n }, 500);\n setTimeout(() => clearInterval(retry), 10_000);\n });\n }\n}\n\nfunction bridgeError(code: number, message: string): Error {\n const err = new Error(message) as Error & { code?: number };\n err.code = code;\n return err;\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport {\n createPublicClient,\n http,\n type Address,\n type Chain,\n type Hex,\n type PublicClient,\n} from 'viem';\n\nimport { useTangleIframeContext } from './TangleIframeProvider';\nimport type {\n JobInvocation,\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type {\n ChainContext,\n JobInputs,\n SignTypedDataRequest,\n} from '../wallet/parentBridgeProtocol';\n\n/**\n * Read-only view of the connected wallet, plus the operations the iframe\n * can request the parent to perform.\n *\n * The iframe never holds a private key, never sees `window.ethereum`, never\n * imports wagmi. All wallet work happens upstream in the Tangle Cloud\n * dapp's wagmi config + ConnectKit modal.\n */\nexport function useTangleWallet(): WalletSnapshot & {\n signMessage: (message: string) => Promise<Hex>;\n sendTransaction: (tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }) => Promise<Hex>;\n signTypedData: (args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }) => Promise<Hex>;\n switchChain: (chainId: number) => Promise<number>;\n} {\n const { client, wallet } = useTangleIframeContext();\n const signMessage = useCallback(\n (message: string) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signMessage(message);\n },\n [client],\n );\n const sendTransaction = useCallback(\n (tx: { to: Address; data: Hex; value?: bigint }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.sendTransaction(tx);\n },\n [client],\n );\n const signTypedData = useCallback(\n (args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signTypedData(args);\n },\n [client],\n );\n const switchChain = useCallback(\n (chainId: number) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.switchChain(chainId);\n },\n [client],\n );\n return {\n ...wallet,\n signMessage,\n sendTransaction,\n signTypedData,\n switchChain,\n };\n}\n\n/**\n * Chain configuration broadcast by the parent: chain id, name, RPC URL,\n * block explorer, native currency. Returns `null` until the parent has\n * sent its first `serviceContext` broadcast (or in dev mode without a\n * seeded harness).\n *\n * Use this when you want to display chain-aware info (block explorer\n * links, native currency labels) or when you want to build your own viem\n * client with the parent's RPC URL. For a pre-built read-only client,\n * see `useTanglePublicClient()`.\n */\nexport function useChainContext(): ChainContext | null {\n return useTangleIframeContext().service.chain;\n}\n\n/**\n * Read-only viem `PublicClient` pinned to the chain the parent dapp is\n * connected to. Useful for `readContract`, `getBalance`, `multicall`, etc.\n *\n * Returns `null` until the parent broadcasts a chain context. Iframes that\n * need to read from chains *other* than the active one should bring their\n * own client — this hook is a convenience for the common case, not a\n * constraint. Multi-chain dashboards just create additional clients\n * directly via `createPublicClient`.\n *\n * Memoized per chain id + RPC URL, so consumers get a stable identity\n * across re-renders.\n */\nexport function useTanglePublicClient(): PublicClient | null {\n const chain = useChainContext();\n return useMemo(() => {\n if (!chain) return null;\n const chainConfig: Chain = {\n id: chain.id,\n name: chain.name,\n nativeCurrency:\n chain.nativeCurrency !== undefined\n ? { ...chain.nativeCurrency }\n : { name: 'Ether', symbol: 'ETH', decimals: 18 },\n rpcUrls: {\n default: { http: [chain.rpcUrl] },\n },\n ...(chain.blockExplorerUrl\n ? {\n blockExplorers: {\n default: { name: 'Explorer', url: chain.blockExplorerUrl },\n },\n }\n : {}),\n } as Chain;\n return createPublicClient({\n chain: chainConfig,\n transport: http(chain.rpcUrl),\n });\n }, [chain]);\n}\n\n/**\n * The service the iframe is currently rendering for. Broadcast by the\n * parent dapp on mount + every time the service/mode changes — the iframe\n * never queries the chain or the indexer itself.\n *\n * `serviceId === null` means the operator hasn't deployed an instance yet;\n * the iframe should render its deploy-ready / configuration surface.\n */\nexport function useTangleService(): ServiceSnapshot {\n return useTangleIframeContext().service;\n}\n\n/**\n * Invoke a blueprint job. Returns a callable + a snapshot of the most\n * recent invocation (or null if none yet).\n *\n * Streaming jobs (LLM, video, audio) opt in via `stream: true`. The hook's\n * `invocation.chunks` accumulates each streaming chunk so the UI can render\n * progressive output. For one-shot jobs (embeddings, classification), use\n * the `invocation.data` once `status === 'success'`.\n *\n * Multiple in-flight invocations are supported — each `call()` returns its\n * own correlationId. The hook tracks only the *latest* invocation in its\n * state; consumers that need all history can subscribe to the client's\n * `job` event directly.\n */\nexport function useCallJob() {\n const { client } = useTangleIframeContext();\n const [invocation, setInvocation] = useState<JobInvocation | null>(null);\n const [latestId, setLatestId] = useState<string | null>(null);\n\n useEffect(() => {\n if (!client) return undefined;\n return client.subscribe('job', (next) => {\n // Only update if this is the latest invocation, or no latest tracked.\n setLatestId((prevLatest) => {\n if (prevLatest === null || prevLatest === next.correlationId) {\n setInvocation(next);\n return next.correlationId;\n }\n return prevLatest;\n });\n });\n }, [client]);\n\n const call = useCallback(\n async (args: { jobIndex: number; inputs: JobInputs; stream?: boolean }) => {\n if (!client) {\n throw new Error(\n 'Job invocation not available in dev mode without a configured stub. See `setDevJobHandler` in the testing harness.',\n );\n }\n // Clear prior invocation state when starting a new call.\n setInvocation(null);\n const result = await client.callJob(args);\n setLatestId(result.correlationId);\n return result;\n },\n [client],\n );\n\n const reset = useCallback(() => {\n setInvocation(null);\n setLatestId(null);\n }, []);\n\n return useMemo(\n () => ({ call, invocation, reset, isPending: invocation?.status === 'pending' || invocation?.status === 'streaming' }),\n [call, invocation, reset],\n );\n}\n\n/**\n * Convenience: returns just the address when connected, or `null`. Most\n * iframe components only care about the address.\n */\nexport function useTangleAddress(): Address | null {\n return useTangleIframeContext().wallet.address;\n}\n\n/** Whether the iframe has completed its parent-handshake (or is in dev mode). */\nexport function useTangleReady(): boolean {\n return useTangleIframeContext().isReady;\n}\n\n/** Resolved mode — `'bridge'` (real parent) or `'dev'` (standalone). */\nexport function useTangleMode(): 'bridge' | 'dev' {\n return useTangleIframeContext().mode;\n}\n"],"mappings":"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","names":["NULL_WALLET","NULL_SERVICE","useEffect","useMemo","useState","useMemo","useState","useEffect"]}
1
+ {"version":3,"sources":["../../src/iframe/TangleIframeProvider.tsx","../../src/iframe/tangleIframeClient.ts","../../src/iframe/hooks.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport {\n TangleIframeClient,\n type ServiceSnapshot,\n type TangleIframeClientOptions,\n type WalletSnapshot,\n} from './tangleIframeClient';\nimport {\n detectTangleCloudParentOrigin,\n TANGLE_CLOUD_ORIGINS_DEFAULT,\n} from '../wallet/detectParentOrigin';\n\ntype Props = {\n appId: string;\n /** Override the detected parent origin (e.g. dev/staging deploys). */\n parentOrigin?: string;\n /** Extra trusted origins for `detectTangleCloudParentOrigin`. */\n extraOrigins?: readonly string[];\n /**\n * Override the bootstrap behavior. When `'auto'` (default), the SDK\n * sniffs the embed context: real parent → install the bridge, top-frame\n * → drop into dev mode. `'bridge'` forces real-parent mode and throws\n * if no parent is detected. `'dev'` forces dev mode even when embedded\n * — useful for component-level tests.\n */\n mode?: 'auto' | 'bridge' | 'dev';\n children: ReactNode;\n};\n\ntype ContextValue = {\n readonly client: TangleIframeClient | null;\n readonly wallet: WalletSnapshot;\n readonly service: ServiceSnapshot;\n readonly mode: 'bridge' | 'dev';\n readonly isReady: boolean;\n};\n\nconst TangleIframeContext = createContext<ContextValue | null>(null);\n\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n chain: null,\n};\n\n/**\n * Iframe-blueprint root provider. Wrap your app once at the entry point.\n *\n * In `auto` mode (default) the SDK detects whether the app is embedded by a\n * trusted Tangle Cloud parent. If yes → installs the postMessage bridge.\n * If no (running standalone at `localhost:5173` etc.) → enters **dev mode**\n * with an in-memory state machine that the developer can drive via the\n * exported debug controls. Dev mode keeps the hook surface identical to\n * production so component code never branches on embed-vs-not.\n *\n * Three lifecycle stages:\n *\n * 1. Mount — `client` is created, mode is decided.\n * 2. Bootstrap — handshake (bridge) or first-paint setup (dev). The\n * `isReady` flag flips to true.\n * 3. Active — wallet + service snapshots flow in via subscriptions.\n */\nexport function TangleIframeProvider({\n appId,\n parentOrigin: explicitOrigin,\n extraOrigins,\n mode: requestedMode = 'auto',\n children,\n}: Props) {\n // Resolve the effective mode once at mount. Switching modes mid-session\n // would tear down the bridge / dev state inconsistently; restart instead.\n const resolution = useMemo(() => {\n if (requestedMode === 'dev') {\n return { mode: 'dev' as const, parentOrigin: null };\n }\n const detected =\n explicitOrigin ?? detectTangleCloudParentOrigin({ extraOrigins });\n if (requestedMode === 'bridge') {\n if (!detected) {\n // eslint-disable-next-line no-console\n console.error(\n '[TangleIframeProvider] mode=\"bridge\" but no trusted parent was detected. Falling back to dev mode.',\n );\n return { mode: 'dev' as const, parentOrigin: null };\n }\n return { mode: 'bridge' as const, parentOrigin: detected };\n }\n // auto: bridge when detected, dev otherwise.\n return detected\n ? { mode: 'bridge' as const, parentOrigin: detected }\n : { mode: 'dev' as const, parentOrigin: null };\n }, [requestedMode, explicitOrigin, extraOrigins]);\n\n const clientRef = useRef<TangleIframeClient | null>(null);\n const [wallet, setWallet] = useState<WalletSnapshot>(NULL_WALLET);\n const [service, setService] = useState<ServiceSnapshot>(NULL_SERVICE);\n const [isReady, setIsReady] = useState(false);\n\n useEffect(() => {\n if (resolution.mode === 'dev') {\n // Dev mode: no bridge. The DevHarness component (or a test) seeds\n // wallet + service via `setDevWallet` / `setDevService` on the\n // returned context. Mark ready immediately so app code unblocks.\n setIsReady(true);\n return undefined;\n }\n // Bridge mode\n const options: TangleIframeClientOptions = {\n parentOrigin: resolution.parentOrigin,\n appId,\n };\n const client = new TangleIframeClient(options);\n clientRef.current = client;\n const unsubWallet = client.subscribe('wallet', setWallet);\n const unsubService = client.subscribe('service', setService);\n client.install();\n setIsReady(true);\n return () => {\n unsubWallet();\n unsubService();\n client.uninstall();\n clientRef.current = null;\n setIsReady(false);\n };\n }, [resolution, appId]);\n\n const value = useMemo<ContextValue>(\n () => ({\n client: clientRef.current,\n wallet,\n service,\n mode: resolution.mode,\n isReady,\n }),\n [wallet, service, resolution.mode, isReady],\n );\n\n return (\n <TangleIframeContext.Provider value={value}>\n {children}\n </TangleIframeContext.Provider>\n );\n}\n\nexport function useTangleIframeContext(): ContextValue {\n const ctx = useContext(TangleIframeContext);\n if (!ctx) {\n throw new Error(\n 'useTangleIframeContext must be used inside <TangleIframeProvider>.',\n );\n }\n return ctx;\n}\n\nexport { TANGLE_CLOUD_ORIGINS_DEFAULT };\n","// Thin-iframe SDK client — the framework-agnostic state machine that talks\n// to a Tangle Cloud parent dapp over postMessage. React hooks (below) are\n// thin wrappers around an instance of this class.\n//\n// Why a class, not a bag of functions: the iframe lifecycle is stateful —\n// handshake, account changes, service-context broadcasts, in-flight job\n// requests. The class owns that state once; hooks subscribe via listeners.\n// Testing the protocol shape doesn't require React.\n\nimport type { Address, Hex } from 'viem';\n\nimport {\n makeCorrelationId,\n NO_WALLET_ADDRESS,\n TANGLE_IFRAME_PROTOCOL_VERSION,\n type CallJobRequest,\n type ChainContext,\n type JobInputs,\n type JobResultEvent,\n type JobResultStatus,\n type ParentMessage,\n type ServiceContextBroadcast,\n type ServiceContextJob,\n type ServiceContextOperator,\n type SignTypedDataRequest,\n} from '../wallet/parentBridgeProtocol';\n\nexport type WalletSnapshot = {\n readonly address: Address | null;\n readonly chainId: number | null;\n readonly isConnected: boolean;\n};\n\nexport type ServiceSnapshot = {\n readonly blueprintId: string | null;\n readonly serviceId: string | null;\n readonly operators: readonly ServiceContextOperator[];\n readonly jobs: readonly ServiceContextJob[];\n readonly mode: string | null;\n /** Chain context broadcast by the parent — drives `useTanglePublicClient`.\n * `null` when the parent hasn't sent one (older parent or dev mode). */\n readonly chain: ChainContext | null;\n};\n\nexport type JobInvocation = {\n readonly correlationId: string;\n readonly status: JobResultStatus;\n readonly data?: unknown;\n readonly chunks: readonly unknown[];\n readonly error?: string;\n readonly progress?: { readonly percent?: number; readonly eta_ms?: number };\n};\n\nexport type ClientEventMap = {\n wallet: WalletSnapshot;\n service: ServiceSnapshot;\n job: JobInvocation;\n};\n\ntype Listener<K extends keyof ClientEventMap> = (\n value: ClientEventMap[K],\n) => void;\n\nexport type TangleIframeClientOptions = {\n /**\n * Origin of the parent dapp. The client posts every message with this\n * exact `targetOrigin` and rejects inbound messages from any other origin.\n * Pass `'*'` only in dev — production must pin to the real parent\n * (`https://cloud.tangle.tools` etc.).\n */\n parentOrigin: string;\n /**\n * Stable identifier for this iframe app. The parent surfaces it in\n * handshake logs + uses it for permission scoping.\n */\n appId: string;\n /**\n * Per-request timeout. Defaults to 60s — long enough for a user to\n * read + approve a signing prompt in the parent. Long-running jobs\n * stream progress events; the request \"completes\" only on terminal\n * status, so the timeout protects against parents that drop replies\n * entirely.\n */\n requestTimeoutMs?: number;\n};\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000;\nconst HANDSHAKE_RETRY_MS = 250;\nconst HANDSHAKE_RETRY_BUDGET_MS = 10_000;\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n chain: null,\n};\n\ntype PendingJob = {\n resolve: (value: JobInvocation) => void;\n reject: (reason: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n invocation: JobInvocation;\n};\n\nexport class TangleIframeClient {\n private wallet: WalletSnapshot = NULL_WALLET;\n private service: ServiceSnapshot = NULL_SERVICE;\n private handshakeAcked = false;\n private handshakeWaiters: Array<() => void> = [];\n private installed = false;\n private handshakeRetry: ReturnType<typeof setInterval> | null = null;\n private listeners: {\n [K in keyof ClientEventMap]: Set<Listener<K>>;\n } = {\n wallet: new Set(),\n service: new Set(),\n job: new Set(),\n };\n private pendingJobs = new Map<string, PendingJob>();\n\n constructor(private readonly options: TangleIframeClientOptions) {}\n\n /** Wire the global message listener + initial handshake. Idempotent. */\n install(): void {\n if (this.installed || typeof window === 'undefined') return;\n this.installed = true;\n window.addEventListener('message', this.handleParentMessage);\n this.postHandshake();\n // Stand up a bounded retry. The parent may attach its listener slightly\n // after the iframe loads (React mounts child effects before parent\n // effects; a real parent may create the frame before its handler is\n // ready), so a single handshake can be dropped. Retry until acked.\n if (this.handshakeRetry === null) {\n let elapsed = 0;\n this.handshakeRetry = setInterval(() => {\n elapsed += HANDSHAKE_RETRY_MS;\n if (this.handshakeAcked || elapsed >= HANDSHAKE_RETRY_BUDGET_MS) {\n this.clearHandshakeRetry();\n return;\n }\n this.postHandshake();\n }, HANDSHAKE_RETRY_MS);\n }\n }\n\n uninstall(): void {\n if (!this.installed || typeof window === 'undefined') return;\n this.installed = false;\n this.clearHandshakeRetry();\n window.removeEventListener('message', this.handleParentMessage);\n for (const [, pending] of this.pendingJobs) {\n clearTimeout(pending.timer);\n pending.reject(new Error('Tangle iframe client uninstalled'));\n }\n this.pendingJobs.clear();\n }\n\n // ── State accessors ─────────────────────────────────────────────────────\n\n getWallet(): WalletSnapshot {\n return this.wallet;\n }\n getService(): ServiceSnapshot {\n return this.service;\n }\n\n // ── Subscription API (used by React hooks) ──────────────────────────────\n\n subscribe<K extends keyof ClientEventMap>(\n event: K,\n listener: Listener<K>,\n ): () => void {\n this.listeners[event].add(listener);\n return () => {\n this.listeners[event].delete(listener);\n };\n }\n\n // ── Wallet operations ───────────────────────────────────────────────────\n\n async signMessage(message: string): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signMessage', {\n chainId: this.wallet.chainId ?? 0,\n message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n async sendTransaction(tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTransaction', {\n chainId: this.wallet.chainId ?? 0,\n to: tx.to,\n data: tx.data,\n ...(tx.value !== undefined ? { value: tx.value.toString(10) } : {}),\n }).then((data) => (data as { txHash: Hex }).txHash);\n }\n\n async switchChain(chainId: number): Promise<number> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.switchChain', { chainId }).then(\n (data) => (data as { chainId: number }).chainId,\n );\n }\n\n /**\n * EIP-712 typed-data signing. The parent renders the typed-data fields in\n * its approval modal; the user audits what they're signing. Use for\n * operator envelopes, off-chain attestations, anything that needs a\n * signature outside the standard blueprint-job RFQ flow.\n *\n * Shape mirrors viem's `signTypedData` argument. Do not include the\n * EIP712Domain entry in `types` — the parent injects it from `domain`.\n */\n async signTypedData(args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTypedData', {\n chainId: this.wallet.chainId ?? 0,\n domain: args.domain,\n types: args.types,\n primaryType: args.primaryType,\n message: args.message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n // ── Job invocation ──────────────────────────────────────────────────────\n\n /**\n * Invoke a blueprint job. Returns a Promise that resolves on terminal\n * status (`success` or `error`); subscribe to the `job` event for\n * intermediate streaming chunks.\n *\n * Streaming opt-in: pass `stream: true` if the publisher's job emits\n * chunks (LLM generation, video encoding). One-shot jobs (embeddings,\n * classifications) skip the streaming machinery.\n */\n async callJob(args: {\n jobIndex: number;\n inputs: JobInputs;\n stream?: boolean;\n }): Promise<JobInvocation> {\n await this.ensureBootstrapped();\n const correlationId = makeCorrelationId('tangle.app.callJob');\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n return new Promise<JobInvocation>((resolve, reject) => {\n const invocation: JobInvocation = {\n correlationId,\n status: 'pending',\n chunks: [],\n };\n const timer = setTimeout(() => {\n this.pendingJobs.delete(correlationId);\n reject(\n bridgeError(4900, `Job did not respond within ${timeout}ms`),\n );\n }, timeout);\n this.pendingJobs.set(correlationId, {\n resolve,\n reject,\n timer,\n invocation,\n });\n const message: CallJobRequest = {\n kind: 'tangle.app.callJob',\n correlationId,\n jobIndex: args.jobIndex,\n inputs: args.inputs,\n ...(args.stream !== undefined ? { stream: args.stream } : {}),\n };\n this.postToParent(message);\n // Emit pending immediately so consumer UIs can show optimistic state.\n this.emit('job', invocation);\n });\n }\n\n // ── Internals ───────────────────────────────────────────────────────────\n\n private clearHandshakeRetry(): void {\n if (this.handshakeRetry !== null) {\n clearInterval(this.handshakeRetry);\n this.handshakeRetry = null;\n }\n }\n\n private postHandshake(): void {\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }\n\n private postToParent(message: object): void {\n if (typeof window === 'undefined') return;\n try {\n window.parent.postMessage(message, this.options.parentOrigin);\n } catch {\n // Cross-origin / sandboxed; defensive only — postMessage shouldn't throw.\n }\n }\n\n private handleParentMessage = (event: MessageEvent): void => {\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const message = data as ParentMessage;\n switch (message.kind) {\n case 'tangle.app.handshakeAck':\n this.handshakeAcked = true;\n this.clearHandshakeRetry();\n for (const resolve of this.handshakeWaiters) resolve();\n this.handshakeWaiters = [];\n return;\n case 'tangle.app.readAccountResult':\n if (message.ok) {\n this.updateWallet({\n address:\n message.data.account === NO_WALLET_ADDRESS\n ? null\n : message.data.account,\n chainId: message.data.chainId,\n isConnected: message.data.account !== NO_WALLET_ADDRESS,\n });\n }\n return;\n case 'tangle.app.accountChanged':\n this.updateWallet({\n address: message.account,\n chainId: this.wallet.chainId,\n isConnected: message.account !== null,\n });\n return;\n case 'tangle.app.chainChanged':\n this.updateWallet({\n address: this.wallet.address,\n chainId: message.chainId,\n isConnected: this.wallet.isConnected,\n });\n return;\n case 'tangle.app.serviceContext':\n this.updateService(message);\n return;\n case 'tangle.app.jobResult':\n this.handleJobResult(message);\n return;\n // Wallet-shape responses (signMessageResult etc.) are routed by\n // dispatchWallet's promise resolver, not here.\n default:\n return;\n }\n };\n\n private async dispatchWallet(\n kind:\n | 'tangle.app.signMessage'\n | 'tangle.app.signTransaction'\n | 'tangle.app.signTypedData'\n | 'tangle.app.switchChain',\n payload: Record<string, unknown>,\n ): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const correlationId = makeCorrelationId(kind);\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const expectedKind = (\n {\n 'tangle.app.signMessage': 'tangle.app.signMessageResult',\n 'tangle.app.signTransaction': 'tangle.app.signTransactionResult',\n 'tangle.app.signTypedData': 'tangle.app.signTypedDataResult',\n 'tangle.app.switchChain': 'tangle.app.switchChainResult',\n } as const\n )[kind];\n const timer = setTimeout(() => {\n window.removeEventListener('message', listener);\n reject(bridgeError(4900, `Parent did not respond to ${kind} in ${timeout}ms`));\n }, timeout);\n const listener = (event: MessageEvent) => {\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const msg = data as ParentMessage;\n if (\n msg.kind !== expectedKind ||\n !('correlationId' in msg) ||\n msg.correlationId !== correlationId\n ) {\n return;\n }\n clearTimeout(timer);\n window.removeEventListener('message', listener);\n // Narrow the type — expectedKind is the wallet-shape `{ok, data|error}` envelope\n const env = msg as {\n ok: boolean;\n data?: unknown;\n error?: string;\n };\n if (env.ok) {\n resolve(env.data);\n } else {\n reject(bridgeError(4001, env.error ?? 'Parent rejected request'));\n }\n };\n window.addEventListener('message', listener);\n this.postToParent({ kind, correlationId, ...payload });\n });\n }\n\n private handleJobResult(message: JobResultEvent): void {\n const pending = this.pendingJobs.get(message.correlationId);\n if (!pending) return;\n const updated: JobInvocation = {\n correlationId: message.correlationId,\n status: message.status,\n chunks:\n message.chunk !== undefined\n ? [...pending.invocation.chunks, message.chunk]\n : pending.invocation.chunks,\n ...(message.data !== undefined ? { data: message.data } : {}),\n ...(message.error !== undefined ? { error: message.error } : {}),\n ...(message.progress !== undefined ? { progress: message.progress } : {}),\n };\n pending.invocation = updated;\n this.emit('job', updated);\n if (message.status === 'success' || message.status === 'error') {\n clearTimeout(pending.timer);\n this.pendingJobs.delete(message.correlationId);\n if (message.status === 'success') {\n pending.resolve(updated);\n } else {\n pending.reject(bridgeError(4001, message.error ?? 'Job failed'));\n }\n }\n }\n\n private updateWallet(next: WalletSnapshot): void {\n if (\n this.wallet.address === next.address &&\n this.wallet.chainId === next.chainId &&\n this.wallet.isConnected === next.isConnected\n ) {\n return;\n }\n this.wallet = next;\n this.emit('wallet', next);\n }\n\n private updateService(broadcast: ServiceContextBroadcast): void {\n const next: ServiceSnapshot = {\n blueprintId: broadcast.blueprintId,\n serviceId: broadcast.serviceId,\n operators: broadcast.operators,\n jobs: broadcast.jobs,\n mode: broadcast.mode,\n chain: broadcast.chain ?? null,\n };\n this.service = next;\n this.emit('service', next);\n }\n\n private emit<K extends keyof ClientEventMap>(\n event: K,\n value: ClientEventMap[K],\n ): void {\n for (const listener of [...this.listeners[event]]) {\n try {\n (listener as Listener<K>)(value);\n } catch {\n // Listener bugs shouldn't break the bridge.\n }\n }\n }\n\n private async ensureBootstrapped(): Promise<void> {\n if (this.handshakeAcked) return;\n this.install();\n await new Promise<void>((resolve) => {\n this.handshakeWaiters.push(resolve);\n const retry = setInterval(() => {\n if (this.handshakeAcked) {\n clearInterval(retry);\n return;\n }\n this.postHandshake();\n }, 500);\n setTimeout(() => clearInterval(retry), 10_000);\n });\n }\n}\n\nfunction bridgeError(code: number, message: string): Error {\n const err = new Error(message) as Error & { code?: number };\n err.code = code;\n return err;\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport {\n createPublicClient,\n http,\n type Address,\n type Chain,\n type Hex,\n type PublicClient,\n} from 'viem';\n\nimport { useTangleIframeContext } from './TangleIframeProvider';\nimport type {\n JobInvocation,\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type {\n ChainContext,\n JobInputs,\n SignTypedDataRequest,\n} from '../wallet/parentBridgeProtocol';\n\n/**\n * Read-only view of the connected wallet, plus the operations the iframe\n * can request the parent to perform.\n *\n * The iframe never holds a private key, never sees `window.ethereum`, never\n * imports wagmi. All wallet work happens upstream in the Tangle Cloud\n * dapp's wagmi config + ConnectKit modal.\n */\nexport function useTangleWallet(): WalletSnapshot & {\n signMessage: (message: string) => Promise<Hex>;\n sendTransaction: (tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }) => Promise<Hex>;\n signTypedData: (args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }) => Promise<Hex>;\n switchChain: (chainId: number) => Promise<number>;\n} {\n const { client, wallet } = useTangleIframeContext();\n const signMessage = useCallback(\n (message: string) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signMessage(message);\n },\n [client],\n );\n const sendTransaction = useCallback(\n (tx: { to: Address; data: Hex; value?: bigint }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.sendTransaction(tx);\n },\n [client],\n );\n const signTypedData = useCallback(\n (args: {\n domain: SignTypedDataRequest['domain'];\n types: SignTypedDataRequest['types'];\n primaryType: string;\n message: Readonly<Record<string, unknown>>;\n }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signTypedData(args);\n },\n [client],\n );\n const switchChain = useCallback(\n (chainId: number) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.switchChain(chainId);\n },\n [client],\n );\n return {\n ...wallet,\n signMessage,\n sendTransaction,\n signTypedData,\n switchChain,\n };\n}\n\n/**\n * Chain configuration broadcast by the parent: chain id, name, RPC URL,\n * block explorer, native currency. Returns `null` until the parent has\n * sent its first `serviceContext` broadcast (or in dev mode without a\n * seeded harness).\n *\n * Use this when you want to display chain-aware info (block explorer\n * links, native currency labels) or when you want to build your own viem\n * client with the parent's RPC URL. For a pre-built read-only client,\n * see `useTanglePublicClient()`.\n */\nexport function useChainContext(): ChainContext | null {\n return useTangleIframeContext().service.chain;\n}\n\n/**\n * Read-only viem `PublicClient` pinned to the chain the parent dapp is\n * connected to. Useful for `readContract`, `getBalance`, `multicall`, etc.\n *\n * Returns `null` until the parent broadcasts a chain context. Iframes that\n * need to read from chains *other* than the active one should bring their\n * own client — this hook is a convenience for the common case, not a\n * constraint. Multi-chain dashboards just create additional clients\n * directly via `createPublicClient`.\n *\n * Memoized per chain id + RPC URL, so consumers get a stable identity\n * across re-renders.\n */\nexport function useTanglePublicClient(): PublicClient | null {\n const chain = useChainContext();\n return useMemo(() => {\n if (!chain) return null;\n const chainConfig: Chain = {\n id: chain.id,\n name: chain.name,\n nativeCurrency:\n chain.nativeCurrency !== undefined\n ? { ...chain.nativeCurrency }\n : { name: 'Ether', symbol: 'ETH', decimals: 18 },\n rpcUrls: {\n default: { http: [chain.rpcUrl] },\n },\n ...(chain.blockExplorerUrl\n ? {\n blockExplorers: {\n default: { name: 'Explorer', url: chain.blockExplorerUrl },\n },\n }\n : {}),\n } as Chain;\n return createPublicClient({\n chain: chainConfig,\n transport: http(chain.rpcUrl),\n });\n }, [chain]);\n}\n\n/**\n * The service the iframe is currently rendering for. Broadcast by the\n * parent dapp on mount + every time the service/mode changes — the iframe\n * never queries the chain or the indexer itself.\n *\n * `serviceId === null` means the operator hasn't deployed an instance yet;\n * the iframe should render its deploy-ready / configuration surface.\n */\nexport function useTangleService(): ServiceSnapshot {\n return useTangleIframeContext().service;\n}\n\n/**\n * Invoke a blueprint job. Returns a callable + a snapshot of the most\n * recent invocation (or null if none yet).\n *\n * Streaming jobs (LLM, video, audio) opt in via `stream: true`. The hook's\n * `invocation.chunks` accumulates each streaming chunk so the UI can render\n * progressive output. For one-shot jobs (embeddings, classification), use\n * the `invocation.data` once `status === 'success'`.\n *\n * Multiple in-flight invocations are supported — each `call()` returns its\n * own correlationId. The hook tracks only the *latest* invocation in its\n * state; consumers that need all history can subscribe to the client's\n * `job` event directly.\n */\nexport function useCallJob() {\n const { client } = useTangleIframeContext();\n const [invocation, setInvocation] = useState<JobInvocation | null>(null);\n const [latestId, setLatestId] = useState<string | null>(null);\n\n useEffect(() => {\n if (!client) return undefined;\n return client.subscribe('job', (next) => {\n // Only update if this is the latest invocation, or no latest tracked.\n setLatestId((prevLatest) => {\n if (prevLatest === null || prevLatest === next.correlationId) {\n setInvocation(next);\n return next.correlationId;\n }\n return prevLatest;\n });\n });\n }, [client]);\n\n const call = useCallback(\n async (args: { jobIndex: number; inputs: JobInputs; stream?: boolean }) => {\n if (!client) {\n throw new Error(\n 'Job invocation not available in dev mode without a configured stub. See `setDevJobHandler` in the testing harness.',\n );\n }\n // Clear prior invocation state when starting a new call.\n setInvocation(null);\n const result = await client.callJob(args);\n setLatestId(result.correlationId);\n return result;\n },\n [client],\n );\n\n const reset = useCallback(() => {\n setInvocation(null);\n setLatestId(null);\n }, []);\n\n return useMemo(\n () => ({ call, invocation, reset, isPending: invocation?.status === 'pending' || invocation?.status === 'streaming' }),\n [call, invocation, reset],\n );\n}\n\n/**\n * Convenience: returns just the address when connected, or `null`. Most\n * iframe components only care about the address.\n */\nexport function useTangleAddress(): Address | null {\n return useTangleIframeContext().wallet.address;\n}\n\n/** Whether the iframe has completed its parent-handshake (or is in dev mode). */\nexport function useTangleReady(): boolean {\n return useTangleIframeContext().isReady;\n}\n\n/** Resolved mode — `'bridge'` (real parent) or `'dev'` (standalone). */\nexport function useTangleMode(): 'bridge' | 'dev' {\n return useTangleIframeContext().mode;\n}\n"],"mappings":";;;;;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;;;AC8EP,IAAM,6BAA6B;AACnC,IAAM,qBAAqB;AAC3B,IAAM,4BAA4B;AAClC,IAAM,cAA8B;AAAA,EAClC,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AACA,IAAM,eAAgC;AAAA,EACpC,aAAa;AAAA,EACb,WAAW;AAAA,EACX,WAAW,CAAC;AAAA,EACZ,MAAM,CAAC;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AASO,IAAM,qBAAN,MAAyB;AAAA,EAgB9B,YAA6B,SAAoC;AAApC;AAAA,EAAqC;AAAA,EAArC;AAAA,EAfrB,SAAyB;AAAA,EACzB,UAA2B;AAAA,EAC3B,iBAAiB;AAAA,EACjB,mBAAsC,CAAC;AAAA,EACvC,YAAY;AAAA,EACZ,iBAAwD;AAAA,EACxD,YAEJ;AAAA,IACF,QAAQ,oBAAI,IAAI;AAAA,IAChB,SAAS,oBAAI,IAAI;AAAA,IACjB,KAAK,oBAAI,IAAI;AAAA,EACf;AAAA,EACQ,cAAc,oBAAI,IAAwB;AAAA;AAAA,EAKlD,UAAgB;AACd,QAAI,KAAK,aAAa,OAAO,WAAW,YAAa;AACrD,SAAK,YAAY;AACjB,WAAO,iBAAiB,WAAW,KAAK,mBAAmB;AAC3D,SAAK,cAAc;AAKnB,QAAI,KAAK,mBAAmB,MAAM;AAChC,UAAI,UAAU;AACd,WAAK,iBAAiB,YAAY,MAAM;AACtC,mBAAW;AACX,YAAI,KAAK,kBAAkB,WAAW,2BAA2B;AAC/D,eAAK,oBAAoB;AACzB;AAAA,QACF;AACA,aAAK,cAAc;AAAA,MACrB,GAAG,kBAAkB;AAAA,IACvB;AAAA,EACF;AAAA,EAEA,YAAkB;AAChB,QAAI,CAAC,KAAK,aAAa,OAAO,WAAW,YAAa;AACtD,SAAK,YAAY;AACjB,SAAK,oBAAoB;AACzB,WAAO,oBAAoB,WAAW,KAAK,mBAAmB;AAC9D,eAAW,CAAC,EAAE,OAAO,KAAK,KAAK,aAAa;AAC1C,mBAAa,QAAQ,KAAK;AAC1B,cAAQ,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAAA,IAC9D;AACA,SAAK,YAAY,MAAM;AAAA,EACzB;AAAA;AAAA,EAIA,YAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EACA,aAA8B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIA,UACE,OACA,UACY;AACZ,SAAK,UAAU,KAAK,EAAE,IAAI,QAAQ;AAClC,WAAO,MAAM;AACX,WAAK,UAAU,KAAK,EAAE,OAAO,QAAQ;AAAA,IACvC;AAAA,EACF;AAAA;AAAA,EAIA,MAAM,YAAY,SAA+B;AAC/C,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,0BAA0B;AAAA,MACnD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC;AAAA,IACF,CAAC,EAAE,KAAK,CAAC,SAAU,KAA4B,SAAS;AAAA,EAC1D;AAAA,EAEA,MAAM,gBAAgB,IAIL;AACf,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,8BAA8B;AAAA,MACvD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC,IAAI,GAAG;AAAA,MACP,MAAM,GAAG;AAAA,MACT,GAAI,GAAG,UAAU,SAAY,EAAE,OAAO,GAAG,MAAM,SAAS,EAAE,EAAE,IAAI,CAAC;AAAA,IACnE,CAAC,EAAE,KAAK,CAAC,SAAU,KAAyB,MAAM;AAAA,EACpD;AAAA,EAEA,MAAM,YAAY,SAAkC;AAClD,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,0BAA0B,EAAE,QAAQ,CAAC,EAAE;AAAA,MAChE,CAAC,SAAU,KAA6B;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,cAAc,MAKH;AACf,UAAM,KAAK,mBAAmB;AAC9B,WAAO,KAAK,eAAe,4BAA4B;AAAA,MACrD,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC,QAAQ,KAAK;AAAA,MACb,OAAO,KAAK;AAAA,MACZ,aAAa,KAAK;AAAA,MAClB,SAAS,KAAK;AAAA,IAChB,CAAC,EAAE,KAAK,CAAC,SAAU,KAA4B,SAAS;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,QAAQ,MAIa;AACzB,UAAM,KAAK,mBAAmB;AAC9B,UAAM,gBAAgB,kBAAkB,oBAAoB;AAC5D,UAAM,UACJ,KAAK,QAAQ,oBAAoB;AACnC,WAAO,IAAI,QAAuB,CAAC,SAAS,WAAW;AACrD,YAAM,aAA4B;AAAA,QAChC;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,CAAC;AAAA,MACX;AACA,YAAM,QAAQ,WAAW,MAAM;AAC7B,aAAK,YAAY,OAAO,aAAa;AACrC;AAAA,UACE,YAAY,MAAM,8BAA8B,OAAO,IAAI;AAAA,QAC7D;AAAA,MACF,GAAG,OAAO;AACV,WAAK,YAAY,IAAI,eAAe;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,YAAM,UAA0B;AAAA,QAC9B,MAAM;AAAA,QACN;AAAA,QACA,UAAU,KAAK;AAAA,QACf,QAAQ,KAAK;AAAA,QACb,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,MAC7D;AACA,WAAK,aAAa,OAAO;AAEzB,WAAK,KAAK,OAAO,UAAU;AAAA,IAC7B,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,sBAA4B;AAClC,QAAI,KAAK,mBAAmB,MAAM;AAChC,oBAAc,KAAK,cAAc;AACjC,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,SAAK,aAAa;AAAA,MAChB,MAAM;AAAA,MACN,OAAO,KAAK,QAAQ;AAAA,MACpB,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,SAAuB;AAC1C,QAAI,OAAO,WAAW,YAAa;AACnC,QAAI;AACF,aAAO,OAAO,YAAY,SAAS,KAAK,QAAQ,YAAY;AAAA,IAC9D,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,sBAAsB,CAAC,UAA8B;AAC3D,QAAI,MAAM,WAAW,KAAK,QAAQ,aAAc;AAChD,UAAM,OAAO,MAAM;AACnB,QAAI,OAAO,SAAS,YAAY,SAAS,KAAM;AAC/C,UAAM,UAAU;AAChB,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,aAAK,iBAAiB;AACtB,aAAK,oBAAoB;AACzB,mBAAW,WAAW,KAAK,iBAAkB,SAAQ;AACrD,aAAK,mBAAmB,CAAC;AACzB;AAAA,MACF,KAAK;AACH,YAAI,QAAQ,IAAI;AACd,eAAK,aAAa;AAAA,YAChB,SACE,QAAQ,KAAK,YAAY,oBACrB,OACA,QAAQ,KAAK;AAAA,YACnB,SAAS,QAAQ,KAAK;AAAA,YACtB,aAAa,QAAQ,KAAK,YAAY;AAAA,UACxC,CAAC;AAAA,QACH;AACA;AAAA,MACF,KAAK;AACH,aAAK,aAAa;AAAA,UAChB,SAAS,QAAQ;AAAA,UACjB,SAAS,KAAK,OAAO;AAAA,UACrB,aAAa,QAAQ,YAAY;AAAA,QACnC,CAAC;AACD;AAAA,MACF,KAAK;AACH,aAAK,aAAa;AAAA,UAChB,SAAS,KAAK,OAAO;AAAA,UACrB,SAAS,QAAQ;AAAA,UACjB,aAAa,KAAK,OAAO;AAAA,QAC3B,CAAC;AACD;AAAA,MACF,KAAK;AACH,aAAK,cAAc,OAAO;AAC1B;AAAA,MACF,KAAK;AACH,aAAK,gBAAgB,OAAO;AAC5B;AAAA;AAAA;AAAA,MAGF;AACE;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,MAAc,eACZ,MAKA,SACkB;AAClB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,gBAAgB,kBAAkB,IAAI;AAC5C,YAAM,UACJ,KAAK,QAAQ,oBAAoB;AACnC,YAAM,eACJ;AAAA,QACE,0BAA0B;AAAA,QAC1B,8BAA8B;AAAA,QAC9B,4BAA4B;AAAA,QAC5B,0BAA0B;AAAA,MAC5B,EACA,IAAI;AACN,YAAM,QAAQ,WAAW,MAAM;AAC7B,eAAO,oBAAoB,WAAW,QAAQ;AAC9C,eAAO,YAAY,MAAM,6BAA6B,IAAI,OAAO,OAAO,IAAI,CAAC;AAAA,MAC/E,GAAG,OAAO;AACV,YAAM,WAAW,CAAC,UAAwB;AACxC,YAAI,MAAM,WAAW,KAAK,QAAQ,aAAc;AAChD,cAAM,OAAO,MAAM;AACnB,YAAI,OAAO,SAAS,YAAY,SAAS,KAAM;AAC/C,cAAM,MAAM;AACZ,YACE,IAAI,SAAS,gBACb,EAAE,mBAAmB,QACrB,IAAI,kBAAkB,eACtB;AACA;AAAA,QACF;AACA,qBAAa,KAAK;AAClB,eAAO,oBAAoB,WAAW,QAAQ;AAE9C,cAAM,MAAM;AAKZ,YAAI,IAAI,IAAI;AACV,kBAAQ,IAAI,IAAI;AAAA,QAClB,OAAO;AACL,iBAAO,YAAY,MAAM,IAAI,SAAS,yBAAyB,CAAC;AAAA,QAClE;AAAA,MACF;AACA,aAAO,iBAAiB,WAAW,QAAQ;AAC3C,WAAK,aAAa,EAAE,MAAM,eAAe,GAAG,QAAQ,CAAC;AAAA,IACvD,CAAC;AAAA,EACH;AAAA,EAEQ,gBAAgB,SAA+B;AACrD,UAAM,UAAU,KAAK,YAAY,IAAI,QAAQ,aAAa;AAC1D,QAAI,CAAC,QAAS;AACd,UAAM,UAAyB;AAAA,MAC7B,eAAe,QAAQ;AAAA,MACvB,QAAQ,QAAQ;AAAA,MAChB,QACE,QAAQ,UAAU,SACd,CAAC,GAAG,QAAQ,WAAW,QAAQ,QAAQ,KAAK,IAC5C,QAAQ,WAAW;AAAA,MACzB,GAAI,QAAQ,SAAS,SAAY,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC3D,GAAI,QAAQ,UAAU,SAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,MAC9D,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IACzE;AACA,YAAQ,aAAa;AACrB,SAAK,KAAK,OAAO,OAAO;AACxB,QAAI,QAAQ,WAAW,aAAa,QAAQ,WAAW,SAAS;AAC9D,mBAAa,QAAQ,KAAK;AAC1B,WAAK,YAAY,OAAO,QAAQ,aAAa;AAC7C,UAAI,QAAQ,WAAW,WAAW;AAChC,gBAAQ,QAAQ,OAAO;AAAA,MACzB,OAAO;AACL,gBAAQ,OAAO,YAAY,MAAM,QAAQ,SAAS,YAAY,CAAC;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,MAA4B;AAC/C,QACE,KAAK,OAAO,YAAY,KAAK,WAC7B,KAAK,OAAO,YAAY,KAAK,WAC7B,KAAK,OAAO,gBAAgB,KAAK,aACjC;AACA;AAAA,IACF;AACA,SAAK,SAAS;AACd,SAAK,KAAK,UAAU,IAAI;AAAA,EAC1B;AAAA,EAEQ,cAAc,WAA0C;AAC9D,UAAM,OAAwB;AAAA,MAC5B,aAAa,UAAU;AAAA,MACvB,WAAW,UAAU;AAAA,MACrB,WAAW,UAAU;AAAA,MACrB,MAAM,UAAU;AAAA,MAChB,MAAM,UAAU;AAAA,MAChB,OAAO,UAAU,SAAS;AAAA,IAC5B;AACA,SAAK,UAAU;AACf,SAAK,KAAK,WAAW,IAAI;AAAA,EAC3B;AAAA,EAEQ,KACN,OACA,OACM;AACN,eAAW,YAAY,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC,GAAG;AACjD,UAAI;AACF,QAAC,SAAyB,KAAK;AAAA,MACjC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,qBAAoC;AAChD,QAAI,KAAK,eAAgB;AACzB,SAAK,QAAQ;AACb,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,WAAK,iBAAiB,KAAK,OAAO;AAClC,YAAM,QAAQ,YAAY,MAAM;AAC9B,YAAI,KAAK,gBAAgB;AACvB,wBAAc,KAAK;AACnB;AAAA,QACF;AACA,aAAK,cAAc;AAAA,MACrB,GAAG,GAAG;AACN,iBAAW,MAAM,cAAc,KAAK,GAAG,GAAM;AAAA,IAC/C,CAAC;AAAA,EACH;AACF;AAEA,SAAS,YAAY,MAAc,SAAwB;AACzD,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,MAAI,OAAO;AACX,SAAO;AACT;;;ADlWI;AA7GJ,IAAM,sBAAsB,cAAmC,IAAI;AAEnE,IAAMA,eAA8B;AAAA,EAClC,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AACA,IAAMC,gBAAgC;AAAA,EACpC,aAAa;AAAA,EACb,WAAW;AAAA,EACX,WAAW,CAAC;AAAA,EACZ,MAAM,CAAC;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AAmBO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,MAAM,gBAAgB;AAAA,EACtB;AACF,GAAU;AAGR,QAAM,aAAa,QAAQ,MAAM;AAC/B,QAAI,kBAAkB,OAAO;AAC3B,aAAO,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,IACpD;AACA,UAAM,WACJ,kBAAkB,8BAA8B,EAAE,aAAa,CAAC;AAClE,QAAI,kBAAkB,UAAU;AAC9B,UAAI,CAAC,UAAU;AAEb,gBAAQ;AAAA,UACN;AAAA,QACF;AACA,eAAO,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,MACpD;AACA,aAAO,EAAE,MAAM,UAAmB,cAAc,SAAS;AAAA,IAC3D;AAEA,WAAO,WACH,EAAE,MAAM,UAAmB,cAAc,SAAS,IAClD,EAAE,MAAM,OAAgB,cAAc,KAAK;AAAA,EACjD,GAAG,CAAC,eAAe,gBAAgB,YAAY,CAAC;AAEhD,QAAM,YAAY,OAAkC,IAAI;AACxD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyBD,YAAW;AAChE,QAAM,CAAC,SAAS,UAAU,IAAI,SAA0BC,aAAY;AACpE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,QAAI,WAAW,SAAS,OAAO;AAI7B,iBAAW,IAAI;AACf,aAAO;AAAA,IACT;AAEA,UAAM,UAAqC;AAAA,MACzC,cAAc,WAAW;AAAA,MACzB;AAAA,IACF;AACA,UAAM,SAAS,IAAI,mBAAmB,OAAO;AAC7C,cAAU,UAAU;AACpB,UAAM,cAAc,OAAO,UAAU,UAAU,SAAS;AACxD,UAAM,eAAe,OAAO,UAAU,WAAW,UAAU;AAC3D,WAAO,QAAQ;AACf,eAAW,IAAI;AACf,WAAO,MAAM;AACX,kBAAY;AACZ,mBAAa;AACb,aAAO,UAAU;AACjB,gBAAU,UAAU;AACpB,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,YAAY,KAAK,CAAC;AAEtB,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL,QAAQ,UAAU;AAAA,MAClB;AAAA,MACA;AAAA,MACA,MAAM,WAAW;AAAA,MACjB;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,SAAS,WAAW,MAAM,OAAO;AAAA,EAC5C;AAEA,SACE,oBAAC,oBAAoB,UAApB,EAA6B,OAC3B,UACH;AAEJ;AAEO,SAAS,yBAAuC;AACrD,QAAM,MAAM,WAAW,mBAAmB;AAC1C,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;AEzKA,SAAS,aAAa,aAAAC,YAAW,WAAAC,UAAS,YAAAC,iBAAgB;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AAsBA,SAAS,kBAcd;AACA,QAAM,EAAE,QAAQ,OAAO,IAAI,uBAAuB;AAClD,QAAM,cAAc;AAAA,IAClB,CAAC,YAAoB;AACnB,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,YAAY,OAAO;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,kBAAkB;AAAA,IACtB,CAAC,OAAmD;AAClD,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,gBAAgB,EAAE;AAAA,IAClC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,gBAAgB;AAAA,IACpB,CAAC,SAKK;AACJ,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,cAAc,IAAI;AAAA,IAClC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,cAAc;AAAA,IAClB,CAAC,YAAoB;AACnB,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChE,aAAO,OAAO,YAAY,OAAO;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAaO,SAAS,kBAAuC;AACrD,SAAO,uBAAuB,EAAE,QAAQ;AAC1C;AAeO,SAAS,wBAA6C;AAC3D,QAAM,QAAQ,gBAAgB;AAC9B,SAAOC,SAAQ,MAAM;AACnB,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,cAAqB;AAAA,MACzB,IAAI,MAAM;AAAA,MACV,MAAM,MAAM;AAAA,MACZ,gBACE,MAAM,mBAAmB,SACrB,EAAE,GAAG,MAAM,eAAe,IAC1B,EAAE,MAAM,SAAS,QAAQ,OAAO,UAAU,GAAG;AAAA,MACnD,SAAS;AAAA,QACP,SAAS,EAAE,MAAM,CAAC,MAAM,MAAM,EAAE;AAAA,MAClC;AAAA,MACA,GAAI,MAAM,mBACN;AAAA,QACE,gBAAgB;AAAA,UACd,SAAS,EAAE,MAAM,YAAY,KAAK,MAAM,iBAAiB;AAAA,QAC3D;AAAA,MACF,IACA,CAAC;AAAA,IACP;AACA,WAAO,mBAAmB;AAAA,MACxB,OAAO;AAAA,MACP,WAAW,KAAK,MAAM,MAAM;AAAA,IAC9B,CAAC;AAAA,EACH,GAAG,CAAC,KAAK,CAAC;AACZ;AAUO,SAAS,mBAAoC;AAClD,SAAO,uBAAuB,EAAE;AAClC;AAgBO,SAAS,aAAa;AAC3B,QAAM,EAAE,OAAO,IAAI,uBAAuB;AAC1C,QAAM,CAAC,YAAY,aAAa,IAAIC,UAA+B,IAAI;AACvE,QAAM,CAAC,UAAU,WAAW,IAAIA,UAAwB,IAAI;AAE5D,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,OAAO,UAAU,OAAO,CAAC,SAAS;AAEvC,kBAAY,CAAC,eAAe;AAC1B,YAAI,eAAe,QAAQ,eAAe,KAAK,eAAe;AAC5D,wBAAc,IAAI;AAClB,iBAAO,KAAK;AAAA,QACd;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH,CAAC;AAAA,EACH,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,OAAO;AAAA,IACX,OAAO,SAAoE;AACzE,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAEA,oBAAc,IAAI;AAClB,YAAM,SAAS,MAAM,OAAO,QAAQ,IAAI;AACxC,kBAAY,OAAO,aAAa;AAChC,aAAO;AAAA,IACT;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,kBAAc,IAAI;AAClB,gBAAY,IAAI;AAAA,EAClB,GAAG,CAAC,CAAC;AAEL,SAAOF;AAAA,IACL,OAAO,EAAE,MAAM,YAAY,OAAO,WAAW,YAAY,WAAW,aAAa,YAAY,WAAW,YAAY;AAAA,IACpH,CAAC,MAAM,YAAY,KAAK;AAAA,EAC1B;AACF;AAMO,SAAS,mBAAmC;AACjD,SAAO,uBAAuB,EAAE,OAAO;AACzC;AAGO,SAAS,iBAA0B;AACxC,SAAO,uBAAuB,EAAE;AAClC;AAGO,SAAS,gBAAkC;AAChD,SAAO,uBAAuB,EAAE;AAClC;","names":["NULL_WALLET","NULL_SERVICE","useEffect","useMemo","useState","useMemo","useState","useEffect"]}
@@ -1,7 +1,7 @@
1
1
  import { C as CallJobRequest, h as ServiceContextOperator, g as ServiceContextJob, b as ChainContext } from '../parentBridgeProtocol-BS2zbIvX.js';
2
2
  import { FC, ReactNode } from 'react';
3
3
  import { Address } from 'viem';
4
- import { W as WalletSnapshot, S as ServiceSnapshot } from '../tangleIframeClient-DES8FDF0.js';
4
+ import { W as WalletSnapshot, S as ServiceSnapshot } from '../tangleIframeClient-CAyUr99p.js';
5
5
 
6
6
  type MockWalletInput = Partial<{
7
7
  address: Address | null;
@@ -83,9 +83,7 @@ var TangleParentHarness = ({
83
83
  });
84
84
  }
85
85
  };
86
- const handler = async (event) => {
87
- if (event.origin === HARNESS_ORIGIN) return;
88
- const data = event.data;
86
+ const handleInbound = async (data) => {
89
87
  if (typeof data !== "object" || data === null) return;
90
88
  const message = data;
91
89
  switch (message.kind) {
@@ -118,8 +116,8 @@ var TangleParentHarness = ({
118
116
  if (typeof message.correlationId !== "string") return;
119
117
  const request = message;
120
118
  setCallLog((prev) => [...prev, request]);
121
- const handler2 = callJobHandler.current;
122
- if (!handler2) {
119
+ const handler = callJobHandler.current;
120
+ if (!handler) {
123
121
  const result = {
124
122
  kind: "tangle.app.jobResult",
125
123
  correlationId: request.correlationId,
@@ -130,7 +128,7 @@ var TangleParentHarness = ({
130
128
  return;
131
129
  }
132
130
  try {
133
- const outcome = await handler2(request);
131
+ const outcome = await handler(request);
134
132
  for (const chunk of outcome.chunks ?? []) {
135
133
  reply({
136
134
  kind: "tangle.app.jobResult",
@@ -206,8 +204,22 @@ var TangleParentHarness = ({
206
204
  }
207
205
  }
208
206
  };
209
- window.addEventListener("message", handler);
210
- return () => window.removeEventListener("message", handler);
207
+ const originalParent = window.parent;
208
+ const proxyParent = {
209
+ postMessage: (message) => {
210
+ void handleInbound(message);
211
+ }
212
+ };
213
+ Object.defineProperty(window, "parent", {
214
+ configurable: true,
215
+ get: () => proxyParent
216
+ });
217
+ return () => {
218
+ Object.defineProperty(window, "parent", {
219
+ configurable: true,
220
+ value: originalParent
221
+ });
222
+ };
211
223
  }, [appId, currentWallet, currentService]);
212
224
  useEffect(() => {
213
225
  if (!seenHandshake.current) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/iframe/testing.tsx"],"sourcesContent":["// Testing harness for iframe blueprints. The promise of the SDK is that\n// publishers can iterate on their UI without running the Tangle Cloud dapp\n// — these utilities are what makes that true.\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport type { Address } from 'viem';\n\nimport type {\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type {\n CallJobRequest,\n JobInputs,\n JobResultEvent,\n ParentMessage,\n ServiceContextBroadcast,\n ServiceContextJob,\n ServiceContextOperator,\n} from '../wallet/parentBridgeProtocol';\n\nexport type MockWalletInput = Partial<{\n address: Address | null;\n chainId: number;\n isConnected: boolean;\n}>;\n\nexport type MockServiceInput = Partial<{\n blueprintId: string;\n serviceId: string | null;\n operators: readonly ServiceContextOperator[];\n jobs: readonly ServiceContextJob[];\n mode: string | null;\n chain: import('../wallet/parentBridgeProtocol').ChainContext | null;\n}>;\n\n/**\n * Construct a deterministic wallet snapshot for tests. Defaults:\n * connected, vitalik.eth's address, Base Sepolia (84532).\n */\nexport function mockWallet(input: MockWalletInput = {}): WalletSnapshot {\n return {\n address:\n input.address === undefined\n ? '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'\n : input.address,\n chainId: input.chainId ?? 84532,\n isConnected: input.isConnected ?? input.address !== null,\n };\n}\n\n/**\n * Construct a deterministic service snapshot for tests. Defaults: blueprint\n * id `0`, no service deployed yet (serviceId null), single mock operator on\n * the canonical local sidecar URL.\n */\nexport function mockServiceContext(\n input: MockServiceInput = {},\n): ServiceSnapshot {\n return {\n blueprintId: input.blueprintId ?? '0',\n serviceId: input.serviceId === undefined ? null : input.serviceId,\n operators:\n input.operators ?? [\n {\n address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',\n rpcAddress: 'http://localhost:8545',\n status: 'active',\n },\n ],\n jobs:\n input.jobs ?? [\n { index: 0, name: 'invoke' },\n ],\n mode: input.mode ?? null,\n chain:\n input.chain === undefined\n ? {\n id: 84532,\n name: 'Base Sepolia',\n rpcUrl: 'https://sepolia.base.org',\n blockExplorerUrl: 'https://sepolia.basescan.org',\n nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 },\n }\n : input.chain,\n };\n}\n\nexport type CallJobHandler = (\n request: CallJobRequest,\n) => Promise<{\n status: 'success' | 'error';\n data?: unknown;\n error?: string;\n /** Streaming chunks emitted in order before the terminal status. */\n chunks?: readonly unknown[];\n}>;\n\ntype HarnessProps = {\n appId?: string;\n wallet?: WalletSnapshot;\n service?: ServiceSnapshot;\n /** Override callJob behavior. Default: returns a static `{ ok: true }`. */\n onCallJob?: CallJobHandler;\n /** Surface a floating debug panel that lets the developer flip state at runtime. */\n showDebugPanel?: boolean;\n children: ReactNode;\n};\n\n/**\n * Drop-in parent simulator for tests + storybook + standalone dev. Wraps\n * children in a fake parent that:\n *\n * - Acks the iframe's handshake immediately\n * - Broadcasts the configured wallet + service context on mount\n * - Intercepts `callJob` requests and routes them through `onCallJob`\n * - (Optional) Mounts a floating debug panel so the developer can\n * mutate state at runtime: change account, switch chain, set\n * serviceId, fire a custom job\n *\n * The harness runs in the same JS context as the iframe app — there's no\n * cross-frame postMessage, just same-window event dispatch. That keeps it\n * fully synchronous + assertable, but the messages still flow through the\n * exact same protocol surface the production bridge uses.\n *\n * Usage:\n *\n * <TangleParentHarness wallet={mockWallet()} service={mockServiceContext()}>\n * <TangleIframeProvider appId=\"my-app\" mode=\"bridge\" parentOrigin=\"harness://\">\n * <App />\n * </TangleIframeProvider>\n * </TangleParentHarness>\n *\n * Set `mode=\"bridge\"` + `parentOrigin=\"harness://\"` on the provider so it\n * matches the harness's synthetic origin. In production, use `mode=\"auto\"`\n * (the default).\n */\nexport const TangleParentHarness: FC<HarnessProps> = ({\n appId = 'harness',\n wallet = mockWallet(),\n service = mockServiceContext(),\n onCallJob,\n showDebugPanel = false,\n children,\n}) => {\n const [currentWallet, setCurrentWallet] = useState<WalletSnapshot>(wallet);\n const [currentService, setCurrentService] =\n useState<ServiceSnapshot>(service);\n const [callLog, setCallLog] = useState<CallJobRequest[]>([]);\n const callJobHandler = useRef<CallJobHandler | undefined>(onCallJob);\n callJobHandler.current = onCallJob;\n const seenHandshake = useRef(false);\n\n // Listen for iframe → \"parent\" messages. Since the harness shares the\n // window, `window.postMessage` with the synthetic origin is the easiest\n // wire — the iframe SDK posts to `window.parent`, which in same-window\n // mode IS this listener.\n useEffect(() => {\n const reply = (message: ParentMessage) => {\n window.dispatchEvent(\n new MessageEvent('message', {\n data: message,\n origin: HARNESS_ORIGIN,\n }),\n );\n };\n\n const broadcast = () => {\n const broadcastMsg: ServiceContextBroadcast = {\n kind: 'tangle.app.serviceContext',\n blueprintId: currentService.blueprintId ?? '0',\n serviceId: currentService.serviceId,\n operators: currentService.operators,\n jobs: currentService.jobs,\n mode: currentService.mode,\n ...(currentService.chain !== null\n ? { chain: currentService.chain }\n : {}),\n };\n reply(broadcastMsg);\n // Also broadcast wallet — combined into accountChanged + chainChanged.\n reply({\n kind: 'tangle.app.accountChanged',\n account: currentWallet.address,\n });\n if (currentWallet.chainId !== null) {\n reply({\n kind: 'tangle.app.chainChanged',\n chainId: currentWallet.chainId,\n });\n }\n };\n\n const handler = async (event: MessageEvent) => {\n // The iframe posts via `window.parent.postMessage(msg, parentOrigin)`.\n // In same-window mode, that fires a message event on this same window\n // with origin = parentOrigin. Filter out events the harness itself\n // dispatched (origin === HARNESS_ORIGIN) — those are replies.\n if (event.origin === HARNESS_ORIGIN) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const message = data as { kind?: string; correlationId?: string };\n\n switch (message.kind) {\n case 'tangle.app.handshake': {\n if (!seenHandshake.current) {\n seenHandshake.current = true;\n reply({\n kind: 'tangle.app.handshakeAck',\n appId,\n protocolVersion: '1',\n });\n broadcast();\n }\n return;\n }\n case 'tangle.app.readAccount': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.readAccountResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n account:\n currentWallet.address ??\n ('0x0000000000000000000000000000000000000000' as Address),\n chainId: currentWallet.chainId ?? 0,\n },\n });\n return;\n }\n case 'tangle.app.callJob': {\n if (typeof message.correlationId !== 'string') return;\n const request = message as unknown as CallJobRequest;\n setCallLog((prev) => [...prev, request]);\n // Default behavior when no handler: emit a single `success` with\n // a echo of the inputs so UIs render *something* in dev mode.\n const handler = callJobHandler.current;\n if (!handler) {\n const result: JobResultEvent = {\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'success',\n data: { echo: request.inputs },\n };\n reply(result);\n return;\n }\n try {\n const outcome = await handler(request);\n for (const chunk of outcome.chunks ?? []) {\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'streaming',\n chunk,\n });\n }\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: outcome.status,\n ...(outcome.data !== undefined ? { data: outcome.data } : {}),\n ...(outcome.error !== undefined ? { error: outcome.error } : {}),\n });\n } catch (err) {\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'error',\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n // Wallet ops respond optimistically — tests that want to assert\n // specific signatures should pre-set them via the dev handler.\n case 'tangle.app.signMessage': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.signMessageResult',\n correlationId: message.correlationId,\n ok: true,\n data: { signature: '0xdeadbeef' as `0x${string}` },\n });\n return;\n }\n case 'tangle.app.signTypedData': {\n if (typeof message.correlationId !== 'string') return;\n // The harness signs deterministically — production parents show\n // an approval modal first. Tests that need to assert the\n // typed-data payload should inspect callLog (extend later if\n // needed) or pass a custom onSignTypedData handler.\n reply({\n kind: 'tangle.app.signTypedDataResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n signature: ('0x' + '11'.repeat(65)) as `0x${string}`,\n },\n });\n return;\n }\n case 'tangle.app.signTransaction': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.signTransactionResult',\n correlationId: message.correlationId,\n ok: true,\n data: { txHash: ('0x' + '00'.repeat(32)) as `0x${string}` },\n });\n return;\n }\n case 'tangle.app.switchChain': {\n if (\n typeof message.correlationId !== 'string' ||\n typeof (message as unknown as { chainId?: number }).chainId !== 'number'\n ) {\n return;\n }\n const chainId = (message as unknown as { chainId: number }).chainId;\n setCurrentWallet((w) => ({ ...w, chainId }));\n reply({\n kind: 'tangle.app.switchChainResult',\n correlationId: message.correlationId,\n ok: true,\n data: { chainId },\n });\n return;\n }\n }\n };\n window.addEventListener('message', handler);\n return () => window.removeEventListener('message', handler);\n }, [appId, currentWallet, currentService]);\n\n // Re-broadcast when state changes.\n useEffect(() => {\n if (!seenHandshake.current) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.accountChanged',\n account: currentWallet.address,\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentWallet.address]);\n\n useEffect(() => {\n if (!seenHandshake.current || currentWallet.chainId === null) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.chainChanged',\n chainId: currentWallet.chainId,\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentWallet.chainId]);\n\n useEffect(() => {\n if (!seenHandshake.current) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.serviceContext',\n blueprintId: currentService.blueprintId ?? '0',\n serviceId: currentService.serviceId,\n operators: currentService.operators,\n jobs: currentService.jobs,\n mode: currentService.mode,\n ...(currentService.chain !== null\n ? { chain: currentService.chain }\n : {}),\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentService]);\n\n const debugApi = useMemo(\n () => ({\n setWallet: setCurrentWallet,\n setService: setCurrentService,\n callLog,\n }),\n [callLog],\n );\n\n return (\n <>\n {children}\n {showDebugPanel && <DebugPanel api={debugApi} />}\n </>\n );\n};\n\n/**\n * Synthetic origin every harness instance uses. Stable across tests so the\n * iframe SDK + the harness can pin to the same string.\n */\nexport const HARNESS_ORIGIN = 'harness://tangle.local';\n\n// ── Debug panel ──────────────────────────────────────────────────────────────\n\nconst DebugPanel: FC<{\n api: {\n setWallet: (w: WalletSnapshot | ((prev: WalletSnapshot) => WalletSnapshot)) => void;\n setService: (\n s: ServiceSnapshot | ((prev: ServiceSnapshot) => ServiceSnapshot),\n ) => void;\n callLog: readonly CallJobRequest[];\n };\n}> = ({ api }) => {\n const [open, setOpen] = useState(true);\n const [tab, setTab] = useState<'wallet' | 'service' | 'log'>('wallet');\n if (!open) {\n return (\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n style={debugStyles.collapsedTrigger}\n >\n Debug\n </button>\n );\n }\n return (\n <div style={debugStyles.panel}>\n <header style={debugStyles.header}>\n <strong style={{ fontSize: 11 }}>TANGLE DEV HARNESS</strong>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n style={debugStyles.closeButton}\n aria-label=\"Close debug panel\"\n >\n ×\n </button>\n </header>\n <nav style={debugStyles.tabs}>\n {(['wallet', 'service', 'log'] as const).map((t) => (\n <button\n key={t}\n type=\"button\"\n onClick={() => setTab(t)}\n style={{\n ...debugStyles.tab,\n ...(tab === t ? debugStyles.tabActive : {}),\n }}\n >\n {t}\n </button>\n ))}\n </nav>\n <div style={debugStyles.body}>\n {tab === 'wallet' && <WalletTab api={api} />}\n {tab === 'service' && <ServiceTab api={api} />}\n {tab === 'log' && <CallLogTab callLog={api.callLog} />}\n </div>\n </div>\n );\n};\n\nconst WalletTab: FC<{\n api: { setWallet: (w: WalletSnapshot | ((prev: WalletSnapshot) => WalletSnapshot)) => void };\n}> = ({ api }) => {\n const [address, setAddressInput] = useState(\n '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',\n );\n const [chainId, setChainIdInput] = useState('84532');\n const applyConnect = useCallback(() => {\n api.setWallet({\n address: address as Address,\n chainId: Number(chainId) || null,\n isConnected: true,\n });\n }, [address, chainId, api]);\n const disconnect = useCallback(() => {\n api.setWallet({ address: null, chainId: null, isConnected: false });\n }, [api]);\n return (\n <div>\n <label style={debugStyles.label}>address</label>\n <input\n value={address}\n onChange={(e) => setAddressInput(e.target.value)}\n style={debugStyles.input}\n />\n <label style={debugStyles.label}>chain id</label>\n <input\n value={chainId}\n onChange={(e) => setChainIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <div style={debugStyles.buttonRow}>\n <button type=\"button\" onClick={applyConnect} style={debugStyles.primary}>\n Set connected\n </button>\n <button type=\"button\" onClick={disconnect} style={debugStyles.secondary}>\n Disconnect\n </button>\n </div>\n </div>\n );\n};\n\nconst ServiceTab: FC<{\n api: {\n setService: (\n s: ServiceSnapshot | ((prev: ServiceSnapshot) => ServiceSnapshot),\n ) => void;\n };\n}> = ({ api }) => {\n const [serviceId, setServiceIdInput] = useState('1');\n const [blueprintId, setBlueprintIdInput] = useState('0');\n const apply = useCallback(() => {\n api.setService((prev) => ({\n ...prev,\n serviceId: serviceId || null,\n blueprintId,\n }));\n }, [api, serviceId, blueprintId]);\n const clearService = useCallback(() => {\n api.setService((prev) => ({ ...prev, serviceId: null }));\n }, [api]);\n return (\n <div>\n <label style={debugStyles.label}>blueprint id</label>\n <input\n value={blueprintId}\n onChange={(e) => setBlueprintIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <label style={debugStyles.label}>service id (empty = not deployed)</label>\n <input\n value={serviceId}\n onChange={(e) => setServiceIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <div style={debugStyles.buttonRow}>\n <button type=\"button\" onClick={apply} style={debugStyles.primary}>\n Apply\n </button>\n <button type=\"button\" onClick={clearService} style={debugStyles.secondary}>\n Clear service\n </button>\n </div>\n </div>\n );\n};\n\nconst CallLogTab: FC<{ callLog: readonly CallJobRequest[] }> = ({ callLog }) => {\n if (callLog.length === 0) {\n return <p style={debugStyles.empty}>No callJob requests yet.</p>;\n }\n return (\n <ol style={debugStyles.log}>\n {callLog.map((entry) => (\n <li key={entry.correlationId} style={debugStyles.logEntry}>\n <strong>job {entry.jobIndex}</strong>\n <pre style={debugStyles.pre}>\n {JSON.stringify(entry.inputs, null, 2)}\n </pre>\n </li>\n ))}\n </ol>\n );\n};\n\n// Inline styles keep the harness style-system-agnostic — consumers may not\n// ship Tailwind, and the panel shouldn't add a dependency.\nconst debugStyles = {\n panel: {\n position: 'fixed' as const,\n right: 12,\n top: 12,\n width: 280,\n zIndex: 99999,\n background: '#0b0b14',\n color: '#fff',\n border: '1px solid #3a3a52',\n borderRadius: 10,\n boxShadow: '0 14px 32px rgba(0,0,0,0.4)',\n fontFamily:\n 'ui-monospace, SFMono-Regular, Menlo, Monaco, \"Cascadia Code\", monospace',\n fontSize: 12,\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '8px 10px',\n borderBottom: '1px solid #2a2a3e',\n },\n closeButton: {\n background: 'none',\n border: 'none',\n color: '#fff',\n fontSize: 18,\n cursor: 'pointer',\n lineHeight: 1,\n },\n tabs: {\n display: 'flex',\n borderBottom: '1px solid #2a2a3e',\n },\n tab: {\n flex: 1,\n background: 'none',\n border: 'none',\n color: '#a0a0c0',\n padding: '6px 8px',\n cursor: 'pointer',\n fontSize: 11,\n textTransform: 'uppercase' as const,\n },\n tabActive: {\n color: '#fff',\n borderBottom: '2px solid #818cf8',\n },\n body: {\n padding: 10,\n maxHeight: 320,\n overflow: 'auto' as const,\n },\n label: {\n display: 'block',\n color: '#a0a0c0',\n fontSize: 10,\n marginBottom: 4,\n marginTop: 6,\n textTransform: 'uppercase' as const,\n },\n input: {\n width: '100%',\n background: '#15152a',\n border: '1px solid #2a2a3e',\n color: '#fff',\n padding: '6px 8px',\n borderRadius: 4,\n fontFamily: 'inherit',\n fontSize: 11,\n boxSizing: 'border-box' as const,\n },\n buttonRow: { display: 'flex', gap: 6, marginTop: 8 },\n primary: {\n flex: 1,\n background: '#4f46e5',\n color: '#fff',\n border: 'none',\n padding: '6px 8px',\n borderRadius: 4,\n cursor: 'pointer',\n fontSize: 11,\n fontFamily: 'inherit',\n },\n secondary: {\n flex: 1,\n background: 'transparent',\n color: '#a0a0c0',\n border: '1px solid #3a3a52',\n padding: '6px 8px',\n borderRadius: 4,\n cursor: 'pointer',\n fontSize: 11,\n fontFamily: 'inherit',\n },\n collapsedTrigger: {\n position: 'fixed' as const,\n right: 12,\n top: 12,\n zIndex: 99999,\n padding: '6px 10px',\n background: '#0b0b14',\n border: '1px solid #3a3a52',\n color: '#fff',\n borderRadius: 6,\n fontFamily: 'inherit',\n fontSize: 11,\n cursor: 'pointer',\n },\n log: { listStyle: 'none', padding: 0, margin: 0 },\n logEntry: {\n padding: 6,\n borderBottom: '1px solid #2a2a3e',\n fontSize: 11,\n },\n pre: {\n margin: '4px 0 0',\n color: '#a0a0c0',\n fontSize: 10,\n whiteSpace: 'pre-wrap' as const,\n wordBreak: 'break-word' as const,\n },\n empty: { color: '#a0a0c0', fontSize: 11, margin: 0 },\n} as const;\n\nexport type { JobInputs };\n"],"mappings":"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","names":["handler"]}
1
+ {"version":3,"sources":["../../src/iframe/testing.tsx"],"sourcesContent":["// Testing harness for iframe blueprints. The promise of the SDK is that\n// publishers can iterate on their UI without running the Tangle Cloud dapp\n// — these utilities are what makes that true.\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport type { Address } from 'viem';\n\nimport type {\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type {\n CallJobRequest,\n JobInputs,\n JobResultEvent,\n ParentMessage,\n ServiceContextBroadcast,\n ServiceContextJob,\n ServiceContextOperator,\n} from '../wallet/parentBridgeProtocol';\n\nexport type MockWalletInput = Partial<{\n address: Address | null;\n chainId: number;\n isConnected: boolean;\n}>;\n\nexport type MockServiceInput = Partial<{\n blueprintId: string;\n serviceId: string | null;\n operators: readonly ServiceContextOperator[];\n jobs: readonly ServiceContextJob[];\n mode: string | null;\n chain: import('../wallet/parentBridgeProtocol').ChainContext | null;\n}>;\n\n/**\n * Construct a deterministic wallet snapshot for tests. Defaults:\n * connected, vitalik.eth's address, Base Sepolia (84532).\n */\nexport function mockWallet(input: MockWalletInput = {}): WalletSnapshot {\n return {\n address:\n input.address === undefined\n ? '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'\n : input.address,\n chainId: input.chainId ?? 84532,\n isConnected: input.isConnected ?? input.address !== null,\n };\n}\n\n/**\n * Construct a deterministic service snapshot for tests. Defaults: blueprint\n * id `0`, no service deployed yet (serviceId null), single mock operator on\n * the canonical local sidecar URL.\n */\nexport function mockServiceContext(\n input: MockServiceInput = {},\n): ServiceSnapshot {\n return {\n blueprintId: input.blueprintId ?? '0',\n serviceId: input.serviceId === undefined ? null : input.serviceId,\n operators:\n input.operators ?? [\n {\n address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',\n rpcAddress: 'http://localhost:8545',\n status: 'active',\n },\n ],\n jobs:\n input.jobs ?? [\n { index: 0, name: 'invoke' },\n ],\n mode: input.mode ?? null,\n chain:\n input.chain === undefined\n ? {\n id: 84532,\n name: 'Base Sepolia',\n rpcUrl: 'https://sepolia.base.org',\n blockExplorerUrl: 'https://sepolia.basescan.org',\n nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 },\n }\n : input.chain,\n };\n}\n\nexport type CallJobHandler = (\n request: CallJobRequest,\n) => Promise<{\n status: 'success' | 'error';\n data?: unknown;\n error?: string;\n /** Streaming chunks emitted in order before the terminal status. */\n chunks?: readonly unknown[];\n}>;\n\ntype HarnessProps = {\n appId?: string;\n wallet?: WalletSnapshot;\n service?: ServiceSnapshot;\n /** Override callJob behavior. Default: returns a static `{ ok: true }`. */\n onCallJob?: CallJobHandler;\n /** Surface a floating debug panel that lets the developer flip state at runtime. */\n showDebugPanel?: boolean;\n children: ReactNode;\n};\n\n/**\n * Drop-in parent simulator for tests + storybook + standalone dev. Wraps\n * children in a fake parent that:\n *\n * - Acks the iframe's handshake immediately\n * - Broadcasts the configured wallet + service context on mount\n * - Intercepts `callJob` requests and routes them through `onCallJob`\n * - (Optional) Mounts a floating debug panel so the developer can\n * mutate state at runtime: change account, switch chain, set\n * serviceId, fire a custom job\n *\n * The harness runs in the same JS context as the iframe app — there's no\n * cross-frame postMessage, just same-window event dispatch. That keeps it\n * fully synchronous + assertable, but the messages still flow through the\n * exact same protocol surface the production bridge uses.\n *\n * Usage:\n *\n * <TangleParentHarness wallet={mockWallet()} service={mockServiceContext()}>\n * <TangleIframeProvider appId=\"my-app\" mode=\"bridge\" parentOrigin=\"harness://\">\n * <App />\n * </TangleIframeProvider>\n * </TangleParentHarness>\n *\n * Set `mode=\"bridge\"` + `parentOrigin=\"harness://\"` on the provider so it\n * matches the harness's synthetic origin. In production, use `mode=\"auto\"`\n * (the default).\n */\nexport const TangleParentHarness: FC<HarnessProps> = ({\n appId = 'harness',\n wallet = mockWallet(),\n service = mockServiceContext(),\n onCallJob,\n showDebugPanel = false,\n children,\n}) => {\n const [currentWallet, setCurrentWallet] = useState<WalletSnapshot>(wallet);\n const [currentService, setCurrentService] =\n useState<ServiceSnapshot>(service);\n const [callLog, setCallLog] = useState<CallJobRequest[]>([]);\n const callJobHandler = useRef<CallJobHandler | undefined>(onCallJob);\n callJobHandler.current = onCallJob;\n const seenHandshake = useRef(false);\n\n // Bridge the iframe → \"parent\" channel. The SDK posts via\n // `window.parent.postMessage(msg, parentOrigin)`. In a real iframe that\n // crosses a window boundary; in the harness both live in one window, where\n // a same-window `postMessage` with a synthetic targetOrigin is *not*\n // delivered (the origin won't match the document's real origin in jsdom /\n // happy-dom / a real browser). So we intercept `window.parent.postMessage`\n // directly — identical to how production parents receive frames, minus the\n // window hop. Replies still travel back as dispatched `message` events\n // tagged with HARNESS_ORIGIN, which the SDK's listener filters for.\n useEffect(() => {\n const reply = (message: ParentMessage) => {\n window.dispatchEvent(\n new MessageEvent('message', {\n data: message,\n origin: HARNESS_ORIGIN,\n }),\n );\n };\n\n const broadcast = () => {\n const broadcastMsg: ServiceContextBroadcast = {\n kind: 'tangle.app.serviceContext',\n blueprintId: currentService.blueprintId ?? '0',\n serviceId: currentService.serviceId,\n operators: currentService.operators,\n jobs: currentService.jobs,\n mode: currentService.mode,\n ...(currentService.chain !== null\n ? { chain: currentService.chain }\n : {}),\n };\n reply(broadcastMsg);\n // Also broadcast wallet — combined into accountChanged + chainChanged.\n reply({\n kind: 'tangle.app.accountChanged',\n account: currentWallet.address,\n });\n if (currentWallet.chainId !== null) {\n reply({\n kind: 'tangle.app.chainChanged',\n chainId: currentWallet.chainId,\n });\n }\n };\n\n const handleInbound = async (data: unknown) => {\n if (typeof data !== 'object' || data === null) return;\n const message = data as { kind?: string; correlationId?: string };\n\n switch (message.kind) {\n case 'tangle.app.handshake': {\n if (!seenHandshake.current) {\n seenHandshake.current = true;\n reply({\n kind: 'tangle.app.handshakeAck',\n appId,\n protocolVersion: '1',\n });\n broadcast();\n }\n return;\n }\n case 'tangle.app.readAccount': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.readAccountResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n account:\n currentWallet.address ??\n ('0x0000000000000000000000000000000000000000' as Address),\n chainId: currentWallet.chainId ?? 0,\n },\n });\n return;\n }\n case 'tangle.app.callJob': {\n if (typeof message.correlationId !== 'string') return;\n const request = message as unknown as CallJobRequest;\n setCallLog((prev) => [...prev, request]);\n // Default behavior when no handler: emit a single `success` with\n // a echo of the inputs so UIs render *something* in dev mode.\n const handler = callJobHandler.current;\n if (!handler) {\n const result: JobResultEvent = {\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'success',\n data: { echo: request.inputs },\n };\n reply(result);\n return;\n }\n try {\n const outcome = await handler(request);\n for (const chunk of outcome.chunks ?? []) {\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'streaming',\n chunk,\n });\n }\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: outcome.status,\n ...(outcome.data !== undefined ? { data: outcome.data } : {}),\n ...(outcome.error !== undefined ? { error: outcome.error } : {}),\n });\n } catch (err) {\n reply({\n kind: 'tangle.app.jobResult',\n correlationId: request.correlationId,\n status: 'error',\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n // Wallet ops respond optimistically — tests that want to assert\n // specific signatures should pre-set them via the dev handler.\n case 'tangle.app.signMessage': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.signMessageResult',\n correlationId: message.correlationId,\n ok: true,\n data: { signature: '0xdeadbeef' as `0x${string}` },\n });\n return;\n }\n case 'tangle.app.signTypedData': {\n if (typeof message.correlationId !== 'string') return;\n // The harness signs deterministically — production parents show\n // an approval modal first. Tests that need to assert the\n // typed-data payload should inspect callLog (extend later if\n // needed) or pass a custom onSignTypedData handler.\n reply({\n kind: 'tangle.app.signTypedDataResult',\n correlationId: message.correlationId,\n ok: true,\n data: {\n signature: ('0x' + '11'.repeat(65)) as `0x${string}`,\n },\n });\n return;\n }\n case 'tangle.app.signTransaction': {\n if (typeof message.correlationId !== 'string') return;\n reply({\n kind: 'tangle.app.signTransactionResult',\n correlationId: message.correlationId,\n ok: true,\n data: { txHash: ('0x' + '00'.repeat(32)) as `0x${string}` },\n });\n return;\n }\n case 'tangle.app.switchChain': {\n if (\n typeof message.correlationId !== 'string' ||\n typeof (message as unknown as { chainId?: number }).chainId !== 'number'\n ) {\n return;\n }\n const chainId = (message as unknown as { chainId: number }).chainId;\n setCurrentWallet((w) => ({ ...w, chainId }));\n reply({\n kind: 'tangle.app.switchChainResult',\n correlationId: message.correlationId,\n ok: true,\n data: { chainId },\n });\n return;\n }\n }\n };\n\n // Route the iframe's outbound posts straight into the handler. We expose\n // only `postMessage` — the SDK never touches other `window.parent`\n // members — and restore the original on teardown.\n const originalParent = window.parent;\n const proxyParent = {\n postMessage: (message: unknown) => {\n void handleInbound(message);\n },\n } as unknown as Window;\n Object.defineProperty(window, 'parent', {\n configurable: true,\n get: () => proxyParent,\n });\n return () => {\n Object.defineProperty(window, 'parent', {\n configurable: true,\n value: originalParent,\n });\n };\n }, [appId, currentWallet, currentService]);\n\n // Re-broadcast when state changes.\n useEffect(() => {\n if (!seenHandshake.current) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.accountChanged',\n account: currentWallet.address,\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentWallet.address]);\n\n useEffect(() => {\n if (!seenHandshake.current || currentWallet.chainId === null) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.chainChanged',\n chainId: currentWallet.chainId,\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentWallet.chainId]);\n\n useEffect(() => {\n if (!seenHandshake.current) return;\n window.dispatchEvent(\n new MessageEvent('message', {\n data: {\n kind: 'tangle.app.serviceContext',\n blueprintId: currentService.blueprintId ?? '0',\n serviceId: currentService.serviceId,\n operators: currentService.operators,\n jobs: currentService.jobs,\n mode: currentService.mode,\n ...(currentService.chain !== null\n ? { chain: currentService.chain }\n : {}),\n },\n origin: HARNESS_ORIGIN,\n }),\n );\n }, [currentService]);\n\n const debugApi = useMemo(\n () => ({\n setWallet: setCurrentWallet,\n setService: setCurrentService,\n callLog,\n }),\n [callLog],\n );\n\n return (\n <>\n {children}\n {showDebugPanel && <DebugPanel api={debugApi} />}\n </>\n );\n};\n\n/**\n * Synthetic origin every harness instance uses. Stable across tests so the\n * iframe SDK + the harness can pin to the same string.\n */\nexport const HARNESS_ORIGIN = 'harness://tangle.local';\n\n// ── Debug panel ──────────────────────────────────────────────────────────────\n\nconst DebugPanel: FC<{\n api: {\n setWallet: (w: WalletSnapshot | ((prev: WalletSnapshot) => WalletSnapshot)) => void;\n setService: (\n s: ServiceSnapshot | ((prev: ServiceSnapshot) => ServiceSnapshot),\n ) => void;\n callLog: readonly CallJobRequest[];\n };\n}> = ({ api }) => {\n const [open, setOpen] = useState(true);\n const [tab, setTab] = useState<'wallet' | 'service' | 'log'>('wallet');\n if (!open) {\n return (\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n style={debugStyles.collapsedTrigger}\n >\n Debug\n </button>\n );\n }\n return (\n <div style={debugStyles.panel}>\n <header style={debugStyles.header}>\n <strong style={{ fontSize: 11 }}>TANGLE DEV HARNESS</strong>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n style={debugStyles.closeButton}\n aria-label=\"Close debug panel\"\n >\n ×\n </button>\n </header>\n <nav style={debugStyles.tabs}>\n {(['wallet', 'service', 'log'] as const).map((t) => (\n <button\n key={t}\n type=\"button\"\n onClick={() => setTab(t)}\n style={{\n ...debugStyles.tab,\n ...(tab === t ? debugStyles.tabActive : {}),\n }}\n >\n {t}\n </button>\n ))}\n </nav>\n <div style={debugStyles.body}>\n {tab === 'wallet' && <WalletTab api={api} />}\n {tab === 'service' && <ServiceTab api={api} />}\n {tab === 'log' && <CallLogTab callLog={api.callLog} />}\n </div>\n </div>\n );\n};\n\nconst WalletTab: FC<{\n api: { setWallet: (w: WalletSnapshot | ((prev: WalletSnapshot) => WalletSnapshot)) => void };\n}> = ({ api }) => {\n const [address, setAddressInput] = useState(\n '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',\n );\n const [chainId, setChainIdInput] = useState('84532');\n const applyConnect = useCallback(() => {\n api.setWallet({\n address: address as Address,\n chainId: Number(chainId) || null,\n isConnected: true,\n });\n }, [address, chainId, api]);\n const disconnect = useCallback(() => {\n api.setWallet({ address: null, chainId: null, isConnected: false });\n }, [api]);\n return (\n <div>\n <label style={debugStyles.label}>address</label>\n <input\n value={address}\n onChange={(e) => setAddressInput(e.target.value)}\n style={debugStyles.input}\n />\n <label style={debugStyles.label}>chain id</label>\n <input\n value={chainId}\n onChange={(e) => setChainIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <div style={debugStyles.buttonRow}>\n <button type=\"button\" onClick={applyConnect} style={debugStyles.primary}>\n Set connected\n </button>\n <button type=\"button\" onClick={disconnect} style={debugStyles.secondary}>\n Disconnect\n </button>\n </div>\n </div>\n );\n};\n\nconst ServiceTab: FC<{\n api: {\n setService: (\n s: ServiceSnapshot | ((prev: ServiceSnapshot) => ServiceSnapshot),\n ) => void;\n };\n}> = ({ api }) => {\n const [serviceId, setServiceIdInput] = useState('1');\n const [blueprintId, setBlueprintIdInput] = useState('0');\n const apply = useCallback(() => {\n api.setService((prev) => ({\n ...prev,\n serviceId: serviceId || null,\n blueprintId,\n }));\n }, [api, serviceId, blueprintId]);\n const clearService = useCallback(() => {\n api.setService((prev) => ({ ...prev, serviceId: null }));\n }, [api]);\n return (\n <div>\n <label style={debugStyles.label}>blueprint id</label>\n <input\n value={blueprintId}\n onChange={(e) => setBlueprintIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <label style={debugStyles.label}>service id (empty = not deployed)</label>\n <input\n value={serviceId}\n onChange={(e) => setServiceIdInput(e.target.value)}\n style={debugStyles.input}\n />\n <div style={debugStyles.buttonRow}>\n <button type=\"button\" onClick={apply} style={debugStyles.primary}>\n Apply\n </button>\n <button type=\"button\" onClick={clearService} style={debugStyles.secondary}>\n Clear service\n </button>\n </div>\n </div>\n );\n};\n\nconst CallLogTab: FC<{ callLog: readonly CallJobRequest[] }> = ({ callLog }) => {\n if (callLog.length === 0) {\n return <p style={debugStyles.empty}>No callJob requests yet.</p>;\n }\n return (\n <ol style={debugStyles.log}>\n {callLog.map((entry) => (\n <li key={entry.correlationId} style={debugStyles.logEntry}>\n <strong>job {entry.jobIndex}</strong>\n <pre style={debugStyles.pre}>\n {JSON.stringify(entry.inputs, null, 2)}\n </pre>\n </li>\n ))}\n </ol>\n );\n};\n\n// Inline styles keep the harness style-system-agnostic — consumers may not\n// ship Tailwind, and the panel shouldn't add a dependency.\nconst debugStyles = {\n panel: {\n position: 'fixed' as const,\n right: 12,\n top: 12,\n width: 280,\n zIndex: 99999,\n background: '#0b0b14',\n color: '#fff',\n border: '1px solid #3a3a52',\n borderRadius: 10,\n boxShadow: '0 14px 32px rgba(0,0,0,0.4)',\n fontFamily:\n 'ui-monospace, SFMono-Regular, Menlo, Monaco, \"Cascadia Code\", monospace',\n fontSize: 12,\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '8px 10px',\n borderBottom: '1px solid #2a2a3e',\n },\n closeButton: {\n background: 'none',\n border: 'none',\n color: '#fff',\n fontSize: 18,\n cursor: 'pointer',\n lineHeight: 1,\n },\n tabs: {\n display: 'flex',\n borderBottom: '1px solid #2a2a3e',\n },\n tab: {\n flex: 1,\n background: 'none',\n border: 'none',\n color: '#a0a0c0',\n padding: '6px 8px',\n cursor: 'pointer',\n fontSize: 11,\n textTransform: 'uppercase' as const,\n },\n tabActive: {\n color: '#fff',\n borderBottom: '2px solid #818cf8',\n },\n body: {\n padding: 10,\n maxHeight: 320,\n overflow: 'auto' as const,\n },\n label: {\n display: 'block',\n color: '#a0a0c0',\n fontSize: 10,\n marginBottom: 4,\n marginTop: 6,\n textTransform: 'uppercase' as const,\n },\n input: {\n width: '100%',\n background: '#15152a',\n border: '1px solid #2a2a3e',\n color: '#fff',\n padding: '6px 8px',\n borderRadius: 4,\n fontFamily: 'inherit',\n fontSize: 11,\n boxSizing: 'border-box' as const,\n },\n buttonRow: { display: 'flex', gap: 6, marginTop: 8 },\n primary: {\n flex: 1,\n background: '#4f46e5',\n color: '#fff',\n border: 'none',\n padding: '6px 8px',\n borderRadius: 4,\n cursor: 'pointer',\n fontSize: 11,\n fontFamily: 'inherit',\n },\n secondary: {\n flex: 1,\n background: 'transparent',\n color: '#a0a0c0',\n border: '1px solid #3a3a52',\n padding: '6px 8px',\n borderRadius: 4,\n cursor: 'pointer',\n fontSize: 11,\n fontFamily: 'inherit',\n },\n collapsedTrigger: {\n position: 'fixed' as const,\n right: 12,\n top: 12,\n zIndex: 99999,\n padding: '6px 10px',\n background: '#0b0b14',\n border: '1px solid #3a3a52',\n color: '#fff',\n borderRadius: 6,\n fontFamily: 'inherit',\n fontSize: 11,\n cursor: 'pointer',\n },\n log: { listStyle: 'none', padding: 0, margin: 0 },\n logEntry: {\n padding: 6,\n borderBottom: '1px solid #2a2a3e',\n fontSize: 11,\n },\n pre: {\n margin: '4px 0 0',\n color: '#a0a0c0',\n fontSize: 10,\n whiteSpace: 'pre-wrap' as const,\n wordBreak: 'break-word' as const,\n },\n empty: { color: '#a0a0c0', fontSize: 11, margin: 0 },\n} as const;\n\nexport type { JobInputs };\n"],"mappings":"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","names":[]}
@@ -62,6 +62,7 @@ declare class TangleIframeClient {
62
62
  private handshakeAcked;
63
63
  private handshakeWaiters;
64
64
  private installed;
65
+ private handshakeRetry;
65
66
  private listeners;
66
67
  private pendingJobs;
67
68
  constructor(options: TangleIframeClientOptions);
@@ -107,6 +108,7 @@ declare class TangleIframeClient {
107
108
  inputs: JobInputs;
108
109
  stream?: boolean;
109
110
  }): Promise<JobInvocation>;
111
+ private clearHandshakeRetry;
110
112
  private postHandshake;
111
113
  private postToParent;
112
114
  private handleParentMessage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/blueprint-ui",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Shared blueprint UI components, hooks, and contract utilities for Tangle Network apps",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,91 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { TangleIframeProvider } from './TangleIframeProvider';
5
+ import {
6
+ useChainContext,
7
+ useTangleService,
8
+ useTangleWallet,
9
+ } from './hooks';
10
+ import {
11
+ HARNESS_ORIGIN,
12
+ TangleParentHarness,
13
+ mockServiceContext,
14
+ mockWallet,
15
+ } from './testing';
16
+
17
+ /**
18
+ * End-to-end: a consumer app mounts `TangleIframeProvider` (forced bridge
19
+ * mode) *inside* `TangleParentHarness`. This is exactly how a downstream
20
+ * blueprint tests itself, and it exercises the parts that unit tests of the
21
+ * client alone can't:
22
+ * - React mounts child effects before parent effects, so the provider's
23
+ * `install()` posts its first handshake before the harness has wired its
24
+ * `window.parent.postMessage` interceptor. The client's standing retry
25
+ * must recover from that dropped handshake.
26
+ * - The harness must receive iframe→parent posts via the `window.parent`
27
+ * override (same-window `postMessage` with a synthetic origin is dropped
28
+ * by the DOM), and replies must flow back as dispatched `message` events.
29
+ * A regression in either path makes wallet/service context never arrive.
30
+ */
31
+ function Probe() {
32
+ const wallet = useTangleWallet();
33
+ const service = useTangleService();
34
+ const chain = useChainContext();
35
+ return (
36
+ <div>
37
+ <span data-testid="address">{wallet.address ?? 'none'}</span>
38
+ <span data-testid="connected">{String(wallet.isConnected)}</span>
39
+ <span data-testid="chainId">{String(wallet.chainId)}</span>
40
+ <span data-testid="serviceId">{service.serviceId ?? 'none'}</span>
41
+ <span data-testid="operators">{String(service.operators.length)}</span>
42
+ <span data-testid="chainName">{chain?.name ?? 'none'}</span>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ function mount(opts?: {
48
+ address?: `0x${string}` | null;
49
+ serviceId?: string | null;
50
+ }) {
51
+ return render(
52
+ <TangleParentHarness
53
+ appId="probe-app"
54
+ wallet={mockWallet({ address: opts?.address })}
55
+ service={mockServiceContext({ serviceId: opts?.serviceId ?? '7' })}
56
+ >
57
+ <TangleIframeProvider
58
+ appId="probe-app"
59
+ mode="bridge"
60
+ parentOrigin={HARNESS_ORIGIN}
61
+ >
62
+ <Probe />
63
+ </TangleIframeProvider>
64
+ </TangleParentHarness>,
65
+ );
66
+ }
67
+
68
+ describe('TangleIframeProvider ↔ TangleParentHarness integration', () => {
69
+ it('propagates wallet + chain + service context after handshake', async () => {
70
+ mount();
71
+ await waitFor(() => {
72
+ expect(screen.getByTestId('connected').textContent).toBe('true');
73
+ });
74
+ expect(screen.getByTestId('address').textContent).toBe(
75
+ '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
76
+ );
77
+ expect(screen.getByTestId('chainId').textContent).toBe('84532');
78
+ expect(screen.getByTestId('serviceId').textContent).toBe('7');
79
+ expect(screen.getByTestId('operators').textContent).toBe('1');
80
+ expect(screen.getByTestId('chainName').textContent).toBe('Base Sepolia');
81
+ });
82
+
83
+ it('reports a disconnected wallet when the parent has no account', async () => {
84
+ mount({ address: null });
85
+ await waitFor(() => {
86
+ expect(screen.getByTestId('serviceId').textContent).toBe('7');
87
+ });
88
+ expect(screen.getByTestId('connected').textContent).toBe('false');
89
+ expect(screen.getByTestId('address').textContent).toBe('none');
90
+ });
91
+ });
@@ -85,6 +85,8 @@ export type TangleIframeClientOptions = {
85
85
  };
86
86
 
87
87
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
88
+ const HANDSHAKE_RETRY_MS = 250;
89
+ const HANDSHAKE_RETRY_BUDGET_MS = 10_000;
88
90
  const NULL_WALLET: WalletSnapshot = {
89
91
  address: null,
90
92
  chainId: null,
@@ -112,6 +114,7 @@ export class TangleIframeClient {
112
114
  private handshakeAcked = false;
113
115
  private handshakeWaiters: Array<() => void> = [];
114
116
  private installed = false;
117
+ private handshakeRetry: ReturnType<typeof setInterval> | null = null;
115
118
  private listeners: {
116
119
  [K in keyof ClientEventMap]: Set<Listener<K>>;
117
120
  } = {
@@ -129,11 +132,27 @@ export class TangleIframeClient {
129
132
  this.installed = true;
130
133
  window.addEventListener('message', this.handleParentMessage);
131
134
  this.postHandshake();
135
+ // Stand up a bounded retry. The parent may attach its listener slightly
136
+ // after the iframe loads (React mounts child effects before parent
137
+ // effects; a real parent may create the frame before its handler is
138
+ // ready), so a single handshake can be dropped. Retry until acked.
139
+ if (this.handshakeRetry === null) {
140
+ let elapsed = 0;
141
+ this.handshakeRetry = setInterval(() => {
142
+ elapsed += HANDSHAKE_RETRY_MS;
143
+ if (this.handshakeAcked || elapsed >= HANDSHAKE_RETRY_BUDGET_MS) {
144
+ this.clearHandshakeRetry();
145
+ return;
146
+ }
147
+ this.postHandshake();
148
+ }, HANDSHAKE_RETRY_MS);
149
+ }
132
150
  }
133
151
 
134
152
  uninstall(): void {
135
153
  if (!this.installed || typeof window === 'undefined') return;
136
154
  this.installed = false;
155
+ this.clearHandshakeRetry();
137
156
  window.removeEventListener('message', this.handleParentMessage);
138
157
  for (const [, pending] of this.pendingJobs) {
139
158
  clearTimeout(pending.timer);
@@ -272,6 +291,13 @@ export class TangleIframeClient {
272
291
 
273
292
  // ── Internals ───────────────────────────────────────────────────────────
274
293
 
294
+ private clearHandshakeRetry(): void {
295
+ if (this.handshakeRetry !== null) {
296
+ clearInterval(this.handshakeRetry);
297
+ this.handshakeRetry = null;
298
+ }
299
+ }
300
+
275
301
  private postHandshake(): void {
276
302
  this.postToParent({
277
303
  kind: 'tangle.app.handshake',
@@ -297,6 +323,7 @@ export class TangleIframeClient {
297
323
  switch (message.kind) {
298
324
  case 'tangle.app.handshakeAck':
299
325
  this.handshakeAcked = true;
326
+ this.clearHandshakeRetry();
300
327
  for (const resolve of this.handshakeWaiters) resolve();
301
328
  this.handshakeWaiters = [];
302
329
  return;
@@ -159,10 +159,15 @@ export const TangleParentHarness: FC<HarnessProps> = ({
159
159
  callJobHandler.current = onCallJob;
160
160
  const seenHandshake = useRef(false);
161
161
 
162
- // Listen for iframe → "parent" messages. Since the harness shares the
163
- // window, `window.postMessage` with the synthetic origin is the easiest
164
- // wire the iframe SDK posts to `window.parent`, which in same-window
165
- // mode IS this listener.
162
+ // Bridge the iframe → "parent" channel. The SDK posts via
163
+ // `window.parent.postMessage(msg, parentOrigin)`. In a real iframe that
164
+ // crosses a window boundary; in the harness both live in one window, where
165
+ // a same-window `postMessage` with a synthetic targetOrigin is *not*
166
+ // delivered (the origin won't match the document's real origin in jsdom /
167
+ // happy-dom / a real browser). So we intercept `window.parent.postMessage`
168
+ // directly — identical to how production parents receive frames, minus the
169
+ // window hop. Replies still travel back as dispatched `message` events
170
+ // tagged with HARNESS_ORIGIN, which the SDK's listener filters for.
166
171
  useEffect(() => {
167
172
  const reply = (message: ParentMessage) => {
168
173
  window.dispatchEvent(
@@ -199,13 +204,7 @@ export const TangleParentHarness: FC<HarnessProps> = ({
199
204
  }
200
205
  };
201
206
 
202
- const handler = async (event: MessageEvent) => {
203
- // The iframe posts via `window.parent.postMessage(msg, parentOrigin)`.
204
- // In same-window mode, that fires a message event on this same window
205
- // with origin = parentOrigin. Filter out events the harness itself
206
- // dispatched (origin === HARNESS_ORIGIN) — those are replies.
207
- if (event.origin === HARNESS_ORIGIN) return;
208
- const data = event.data;
207
+ const handleInbound = async (data: unknown) => {
209
208
  if (typeof data !== 'object' || data === null) return;
210
209
  const message = data as { kind?: string; correlationId?: string };
211
210
 
@@ -338,8 +337,26 @@ export const TangleParentHarness: FC<HarnessProps> = ({
338
337
  }
339
338
  }
340
339
  };
341
- window.addEventListener('message', handler);
342
- return () => window.removeEventListener('message', handler);
340
+
341
+ // Route the iframe's outbound posts straight into the handler. We expose
342
+ // only `postMessage` — the SDK never touches other `window.parent`
343
+ // members — and restore the original on teardown.
344
+ const originalParent = window.parent;
345
+ const proxyParent = {
346
+ postMessage: (message: unknown) => {
347
+ void handleInbound(message);
348
+ },
349
+ } as unknown as Window;
350
+ Object.defineProperty(window, 'parent', {
351
+ configurable: true,
352
+ get: () => proxyParent,
353
+ });
354
+ return () => {
355
+ Object.defineProperty(window, 'parent', {
356
+ configurable: true,
357
+ value: originalParent,
358
+ });
359
+ };
343
360
  }, [appId, currentWallet, currentService]);
344
361
 
345
362
  // Re-broadcast when state changes.