@tangle-network/blueprint-ui 0.3.0 → 0.4.0

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.
@@ -0,0 +1,57 @@
1
+ // src/wallet/detectParentOrigin.ts
2
+ var TANGLE_CLOUD_ORIGINS_DEFAULT = Object.freeze([
3
+ "https://cloud.tangle.tools",
4
+ "https://develop.cloud.tangle.tools",
5
+ // Local dev (Vite default port for tangle-cloud + Netlify dev preview).
6
+ "http://localhost:4300",
7
+ "http://localhost:8888"
8
+ ]);
9
+ function originFromReferrer() {
10
+ if (typeof document === "undefined") return null;
11
+ const ref = document.referrer;
12
+ if (!ref) return null;
13
+ try {
14
+ return new URL(ref).origin;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function detectTangleCloudParentOrigin(options = {}) {
20
+ if (typeof window === "undefined" || window.parent === window) {
21
+ return null;
22
+ }
23
+ const allowlist = /* @__PURE__ */ new Set([
24
+ ...TANGLE_CLOUD_ORIGINS_DEFAULT,
25
+ ...options.extraOrigins ?? []
26
+ ]);
27
+ const referrerOrigin = originFromReferrer();
28
+ if (referrerOrigin && allowlist.has(referrerOrigin)) {
29
+ return referrerOrigin;
30
+ }
31
+ try {
32
+ const url = new URL(window.location.href);
33
+ const explicit = url.searchParams.get("parent");
34
+ if (explicit && allowlist.has(explicit)) return explicit;
35
+ } catch {
36
+ }
37
+ return null;
38
+ }
39
+
40
+ // src/wallet/parentBridgeProtocol.ts
41
+ var TANGLE_IFRAME_PROTOCOL_VERSION = "1";
42
+ var TANGLE_IFRAME_PROTOCOL_PREFIX = "tangle.app.";
43
+ var NO_WALLET_ADDRESS = "0x0000000000000000000000000000000000000000";
44
+ function makeCorrelationId(prefix) {
45
+ const random = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36);
46
+ return `${prefix}.${random}`;
47
+ }
48
+
49
+ export {
50
+ TANGLE_CLOUD_ORIGINS_DEFAULT,
51
+ detectTangleCloudParentOrigin,
52
+ TANGLE_IFRAME_PROTOCOL_VERSION,
53
+ TANGLE_IFRAME_PROTOCOL_PREFIX,
54
+ NO_WALLET_ADDRESS,
55
+ makeCorrelationId
56
+ };
57
+ //# sourceMappingURL=chunk-BLXSBQU4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/wallet/detectParentOrigin.ts","../src/wallet/parentBridgeProtocol.ts"],"sourcesContent":["// Determine which origin to trust as the parent dapp.\n//\n// `document.referrer` is the *initial* embedder — it's set when the iframe is\n// first loaded and survives reloads (though it can be cleared by `referrerpolicy`\n// or by the embedder). The Tangle Cloud iframe wrapper deliberately omits\n// `referrerpolicy=\"no-referrer\"` so we get the embedder's origin here.\n//\n// We compare it against an allowlist of known Tangle Cloud origins. If it\n// matches, that's the parent. Otherwise the iframe is being loaded directly\n// (standalone domain visit, dev server, untrusted embedder) and the bridge\n// stays disabled — the app falls back to its normal injected/WC wallet path.\n\n/**\n * Default Tangle Cloud origins. Consumers (agent-sandbox UI,\n * trading-arena, future iframe blueprints) pass app-specific additions\n * via `extraOrigins` rather than mutating this list.\n */\nexport const TANGLE_CLOUD_ORIGINS_DEFAULT = Object.freeze([\n 'https://cloud.tangle.tools',\n 'https://develop.cloud.tangle.tools',\n // Local dev (Vite default port for tangle-cloud + Netlify dev preview).\n 'http://localhost:4300',\n 'http://localhost:8888',\n] as const);\n\nfunction originFromReferrer(): string | null {\n if (typeof document === 'undefined') return null;\n const ref = document.referrer;\n if (!ref) return null;\n try {\n return new URL(ref).origin;\n } catch {\n return null;\n }\n}\n\n/**\n * Returns the parent origin to bridge to, or null when no trusted parent is\n * detected. Caller should skip installing the bridge connector when this\n * returns null.\n *\n * `extraOrigins` is the application's escape hatch for staging or dev\n * deploys not covered by the default list. The library deliberately does\n * not read environment variables itself (consumers may bundle for non-Vite\n * runtimes); the consuming app threads `import.meta.env.VITE_*` or\n * `process.env.*` in itself.\n *\n * Falls back to a `?parent=<origin>` query parameter when no referrer is\n * present (some browsers strip referrer from cross-origin loads). Useful\n * for dev embedding flows.\n */\nexport function detectTangleCloudParentOrigin(\n options: { extraOrigins?: readonly string[] } = {},\n): string | null {\n if (typeof window === 'undefined' || window.parent === window) {\n return null;\n }\n const allowlist = new Set<string>([\n ...TANGLE_CLOUD_ORIGINS_DEFAULT,\n ...(options.extraOrigins ?? []),\n ]);\n const referrerOrigin = originFromReferrer();\n if (referrerOrigin && allowlist.has(referrerOrigin)) {\n return referrerOrigin;\n }\n try {\n const url = new URL(window.location.href);\n const explicit = url.searchParams.get('parent');\n if (explicit && allowlist.has(explicit)) return explicit;\n } catch {\n // ignore\n }\n return null;\n}\n","// Tangle Cloud iframe ↔ parent dapp protocol — must mirror the parent's\n// spec at `apps/tangle-cloud/src/blueprintApps/iframe/protocol.ts`. Bump the\n// version constant in lockstep when either side adds a request kind.\n\nimport type { Address, Hex } from 'viem';\n\nexport const TANGLE_IFRAME_PROTOCOL_VERSION = '1' as const;\nexport const TANGLE_IFRAME_PROTOCOL_PREFIX = 'tangle.app.';\n\n// ─── Iframe → Parent requests ────────────────────────────────────────────────\n\nexport type HandshakeRequest = {\n kind: 'tangle.app.handshake';\n appId: string;\n version: typeof TANGLE_IFRAME_PROTOCOL_VERSION;\n};\n\nexport type ReadAccountRequest = {\n kind: 'tangle.app.readAccount';\n correlationId: string;\n};\n\nexport type SwitchChainRequest = {\n kind: 'tangle.app.switchChain';\n correlationId: string;\n chainId: number;\n};\n\nexport type SignMessageRequest = {\n kind: 'tangle.app.signMessage';\n correlationId: string;\n chainId: number;\n message: string;\n};\n\nexport type SignTransactionRequest = {\n kind: 'tangle.app.signTransaction';\n correlationId: string;\n chainId: number;\n to: Address;\n data: Hex;\n value?: string;\n};\n\n// ─── Parent → Iframe messages ────────────────────────────────────────────────\n\nexport type HandshakeAck = {\n kind: 'tangle.app.handshakeAck';\n appId: string;\n protocolVersion: typeof TANGLE_IFRAME_PROTOCOL_VERSION;\n};\n\nexport type ResultEnvelope<T> = { correlationId: string } & (\n | { ok: true; data: T }\n | { ok: false; error: string }\n);\n\nexport type ReadAccountResult = {\n kind: 'tangle.app.readAccountResult';\n} & ResultEnvelope<{ account: Address; chainId: number }>;\n\nexport type SwitchChainResult = {\n kind: 'tangle.app.switchChainResult';\n} & ResultEnvelope<{ chainId: number }>;\n\nexport type SignMessageResult = {\n kind: 'tangle.app.signMessageResult';\n} & ResultEnvelope<{ signature: Hex }>;\n\nexport type SignTransactionResult = {\n kind: 'tangle.app.signTransactionResult';\n} & ResultEnvelope<{ txHash: Hex }>;\n\nexport type AccountChanged = {\n kind: 'tangle.app.accountChanged';\n account: Address | null;\n};\n\nexport type ChainChanged = {\n kind: 'tangle.app.chainChanged';\n chainId: number;\n};\n\n// ─── Service context (parent → iframe) ──────────────────────────────────────\n//\n// Iframe blueprints embedded by Tangle Cloud need to know which service +\n// blueprint they're rendering for, plus which operators are quoted. The\n// parent broadcasts this on mount and on every change (mode picker swap,\n// new service activation, operator delta). The iframe just reads — it\n// doesn't query the chain itself.\n//\n// The thin-iframe SDK exposes this as `useTangleService()`. Iframes that\n// use the full wagmi connector path can still listen to `serviceContext`\n// for routing convenience.\n\nexport type ServiceContextOperator = {\n readonly address: Address;\n readonly rpcAddress: string | undefined;\n readonly status: 'active' | 'inactive' | 'unknown';\n};\n\nexport type ServiceContextJob = {\n readonly index: number;\n readonly name: string;\n readonly inputSchema?: unknown;\n};\n\nexport type ServiceContextBroadcast = {\n kind: 'tangle.app.serviceContext';\n readonly blueprintId: string;\n readonly serviceId: string | null;\n readonly operators: readonly ServiceContextOperator[];\n readonly jobs: readonly ServiceContextJob[];\n readonly mode: string | null;\n};\n\n// ─── Job invocation (iframe ↔ parent) ────────────────────────────────────────\n//\n// Instead of the iframe wiring up its own EIP-712 quote / sign / submit\n// flow, it sends a single CallJob request upstream. The parent does the\n// whole dance (fetch RFQ quote, build typed data, request user signature,\n// submit on-chain) and streams results back. The iframe never touches\n// chain logic.\n\nexport type JobInputs = Readonly<Record<string, unknown>>;\n\nexport type CallJobRequest = {\n kind: 'tangle.app.callJob';\n correlationId: string;\n /** Job index within the blueprint, e.g. 0 for the primary entry-point. */\n jobIndex: number;\n /** Free-form inputs validated by the parent against the on-chain ABI. */\n inputs: JobInputs;\n /**\n * Whether the publisher wants intermediate progress (streaming chunks)\n * or just the terminal result. Streaming jobs (LLM generation, video\n * encode) opt in; one-shots (embeddings, classifications) don't.\n */\n stream?: boolean;\n};\n\nexport type JobResultStatus = 'pending' | 'streaming' | 'success' | 'error';\n\nexport type JobResultEvent = {\n kind: 'tangle.app.jobResult';\n correlationId: string;\n status: JobResultStatus;\n /** Present on `streaming` and `success`. Shape is publisher-defined. */\n data?: unknown;\n /** Present on `streaming` only — incremental chunk for live UI. */\n chunk?: unknown;\n /** Present on `error`. Human-readable. */\n error?: string;\n /** Optional progress metadata (e.g. `{ percent: 0.42, eta_ms: 8000 }`). */\n progress?: { readonly percent?: number; readonly eta_ms?: number };\n};\n\nexport type ParentMessage =\n | HandshakeAck\n | ReadAccountResult\n | SwitchChainResult\n | SignMessageResult\n | SignTransactionResult\n | AccountChanged\n | ChainChanged\n | ServiceContextBroadcast\n | JobResultEvent;\n\nexport type IframeRequest =\n | HandshakeRequest\n | ReadAccountRequest\n | SwitchChainRequest\n | SignMessageRequest\n | SignTransactionRequest\n | CallJobRequest;\n\n// The zero address used by the parent when no wallet is connected. The parent\n// always responds to readAccount with an address; this sentinel means \"no\n// wallet\" without making the response type a union of result shapes.\nexport const NO_WALLET_ADDRESS = '0x0000000000000000000000000000000000000000';\n\n/**\n * Cryptographically-random ASCII correlation id matching the parent's\n * validator regex (`/^[\\w.\\-:]+$/`, max length 128). The connector keeps a\n * Map<correlationId, Resolver> so each request resolves independently.\n */\nexport function makeCorrelationId(prefix: string): string {\n const random =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : Math.random().toString(36).slice(2) + Date.now().toString(36);\n return `${prefix}.${random}`;\n}\n"],"mappings":";AAiBO,IAAM,+BAA+B,OAAO,OAAO;AAAA,EACxD;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF,CAAU;AAEV,SAAS,qBAAoC;AAC3C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,MAAM,SAAS;AACrB,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAiBO,SAAS,8BACd,UAAgD,CAAC,GAClC;AACf,MAAI,OAAO,WAAW,eAAe,OAAO,WAAW,QAAQ;AAC7D,WAAO;AAAA,EACT;AACA,QAAM,YAAY,oBAAI,IAAY;AAAA,IAChC,GAAG;AAAA,IACH,GAAI,QAAQ,gBAAgB,CAAC;AAAA,EAC/B,CAAC;AACD,QAAM,iBAAiB,mBAAmB;AAC1C,MAAI,kBAAkB,UAAU,IAAI,cAAc,GAAG;AACnD,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,UAAM,WAAW,IAAI,aAAa,IAAI,QAAQ;AAC9C,QAAI,YAAY,UAAU,IAAI,QAAQ,EAAG,QAAO;AAAA,EAClD,QAAQ;AAAA,EAER;AACA,SAAO;AACT;;;ACnEO,IAAM,iCAAiC;AACvC,IAAM,gCAAgC;AA4KtC,IAAM,oBAAoB;AAO1B,SAAS,kBAAkB,QAAwB;AACxD,QAAM,SACJ,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,aAC1D,OAAO,WAAW,IAClB,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE;AAClE,SAAO,GAAG,MAAM,IAAI,MAAM;AAC5B;","names":[]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Default Tangle Cloud origins. Consumers (agent-sandbox UI,
3
+ * trading-arena, future iframe blueprints) pass app-specific additions
4
+ * via `extraOrigins` rather than mutating this list.
5
+ */
6
+ declare const TANGLE_CLOUD_ORIGINS_DEFAULT: readonly ["https://cloud.tangle.tools", "https://develop.cloud.tangle.tools", "http://localhost:4300", "http://localhost:8888"];
7
+ /**
8
+ * Returns the parent origin to bridge to, or null when no trusted parent is
9
+ * detected. Caller should skip installing the bridge connector when this
10
+ * returns null.
11
+ *
12
+ * `extraOrigins` is the application's escape hatch for staging or dev
13
+ * deploys not covered by the default list. The library deliberately does
14
+ * not read environment variables itself (consumers may bundle for non-Vite
15
+ * runtimes); the consuming app threads `import.meta.env.VITE_*` or
16
+ * `process.env.*` in itself.
17
+ *
18
+ * Falls back to a `?parent=<origin>` query parameter when no referrer is
19
+ * present (some browsers strip referrer from cross-origin loads). Useful
20
+ * for dev embedding flows.
21
+ */
22
+ declare function detectTangleCloudParentOrigin(options?: {
23
+ extraOrigins?: readonly string[];
24
+ }): string | null;
25
+
26
+ export { TANGLE_CLOUD_ORIGINS_DEFAULT as T, detectTangleCloudParentOrigin as d };
@@ -0,0 +1,113 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { T as TangleIframeClient, W as WalletSnapshot, S as ServiceSnapshot, J as JobInvocation } from '../tangleIframeClient-D-PP-KhN.js';
4
+ export { C as ClientEventMap, a as TangleIframeClientOptions } from '../tangleIframeClient-D-PP-KhN.js';
5
+ import { Address, Hex } from 'viem';
6
+ import { J as JobInputs } from '../parentBridgeProtocol-CqK9e6Fk.js';
7
+ export { C as CallJobRequest, c as JobResultEvent, d as JobResultStatus, S as ServiceContextBroadcast, f as ServiceContextJob, g as ServiceContextOperator } from '../parentBridgeProtocol-CqK9e6Fk.js';
8
+ export { T as TANGLE_CLOUD_ORIGINS_DEFAULT } from '../detectParentOrigin-BYruoIdc.js';
9
+
10
+ type Props = {
11
+ appId: string;
12
+ /** Override the detected parent origin (e.g. dev/staging deploys). */
13
+ parentOrigin?: string;
14
+ /** Extra trusted origins for `detectTangleCloudParentOrigin`. */
15
+ extraOrigins?: readonly string[];
16
+ /**
17
+ * Override the bootstrap behavior. When `'auto'` (default), the SDK
18
+ * sniffs the embed context: real parent → install the bridge, top-frame
19
+ * → drop into dev mode. `'bridge'` forces real-parent mode and throws
20
+ * if no parent is detected. `'dev'` forces dev mode even when embedded
21
+ * — useful for component-level tests.
22
+ */
23
+ mode?: 'auto' | 'bridge' | 'dev';
24
+ children: ReactNode;
25
+ };
26
+ type ContextValue = {
27
+ readonly client: TangleIframeClient | null;
28
+ readonly wallet: WalletSnapshot;
29
+ readonly service: ServiceSnapshot;
30
+ readonly mode: 'bridge' | 'dev';
31
+ readonly isReady: boolean;
32
+ };
33
+ /**
34
+ * Iframe-blueprint root provider. Wrap your app once at the entry point.
35
+ *
36
+ * In `auto` mode (default) the SDK detects whether the app is embedded by a
37
+ * trusted Tangle Cloud parent. If yes → installs the postMessage bridge.
38
+ * If no (running standalone at `localhost:5173` etc.) → enters **dev mode**
39
+ * with an in-memory state machine that the developer can drive via the
40
+ * exported debug controls. Dev mode keeps the hook surface identical to
41
+ * production so component code never branches on embed-vs-not.
42
+ *
43
+ * Three lifecycle stages:
44
+ *
45
+ * 1. Mount — `client` is created, mode is decided.
46
+ * 2. Bootstrap — handshake (bridge) or first-paint setup (dev). The
47
+ * `isReady` flag flips to true.
48
+ * 3. Active — wallet + service snapshots flow in via subscriptions.
49
+ */
50
+ declare function TangleIframeProvider({ appId, parentOrigin: explicitOrigin, extraOrigins, mode: requestedMode, children, }: Props): react_jsx_runtime.JSX.Element;
51
+ declare function useTangleIframeContext(): ContextValue;
52
+
53
+ /**
54
+ * Read-only view of the connected wallet, plus the operations the iframe
55
+ * can request the parent to perform.
56
+ *
57
+ * The iframe never holds a private key, never sees `window.ethereum`, never
58
+ * imports wagmi. All wallet work happens upstream in the Tangle Cloud
59
+ * dapp's wagmi config + ConnectKit modal.
60
+ */
61
+ declare function useTangleWallet(): WalletSnapshot & {
62
+ signMessage: (message: string) => Promise<Hex>;
63
+ sendTransaction: (tx: {
64
+ to: Address;
65
+ data: Hex;
66
+ value?: bigint;
67
+ }) => Promise<Hex>;
68
+ switchChain: (chainId: number) => Promise<number>;
69
+ };
70
+ /**
71
+ * The service the iframe is currently rendering for. Broadcast by the
72
+ * parent dapp on mount + every time the service/mode changes — the iframe
73
+ * never queries the chain or the indexer itself.
74
+ *
75
+ * `serviceId === null` means the operator hasn't deployed an instance yet;
76
+ * the iframe should render its deploy-ready / configuration surface.
77
+ */
78
+ declare function useTangleService(): ServiceSnapshot;
79
+ /**
80
+ * Invoke a blueprint job. Returns a callable + a snapshot of the most
81
+ * recent invocation (or null if none yet).
82
+ *
83
+ * Streaming jobs (LLM, video, audio) opt in via `stream: true`. The hook's
84
+ * `invocation.chunks` accumulates each streaming chunk so the UI can render
85
+ * progressive output. For one-shot jobs (embeddings, classification), use
86
+ * the `invocation.data` once `status === 'success'`.
87
+ *
88
+ * Multiple in-flight invocations are supported — each `call()` returns its
89
+ * own correlationId. The hook tracks only the *latest* invocation in its
90
+ * state; consumers that need all history can subscribe to the client's
91
+ * `job` event directly.
92
+ */
93
+ declare function useCallJob(): {
94
+ call: (args: {
95
+ jobIndex: number;
96
+ inputs: JobInputs;
97
+ stream?: boolean;
98
+ }) => Promise<JobInvocation>;
99
+ invocation: JobInvocation | null;
100
+ reset: () => void;
101
+ isPending: boolean;
102
+ };
103
+ /**
104
+ * Convenience: returns just the address when connected, or `null`. Most
105
+ * iframe components only care about the address.
106
+ */
107
+ declare function useTangleAddress(): Address | null;
108
+ /** Whether the iframe has completed its parent-handshake (or is in dev mode). */
109
+ declare function useTangleReady(): boolean;
110
+ /** Resolved mode — `'bridge'` (real parent) or `'dev'` (standalone). */
111
+ declare function useTangleMode(): 'bridge' | 'dev';
112
+
113
+ export { JobInputs, JobInvocation, ServiceSnapshot, TangleIframeClient, TangleIframeProvider, WalletSnapshot, useCallJob, useTangleAddress, useTangleIframeContext, useTangleMode, useTangleReady, useTangleService, useTangleWallet };