New:Socket for Asana Is Now Available.Learn more
Sign In

@dodomain/connect

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@dodomain/connect - npm Package Compare versions

Comparing version
0.2.0
to
0.2.1
+6
-0
dist/index.cjs

@@ -74,2 +74,8 @@ "use strict";

Object.assign(frame.style, {
// content-box is load-bearing: host pages routinely reset every element
// to border-box (Tailwind Preflight et al), which would make the 1px
// borders eat into the height applyReportedHeight sets — the inner
// viewport lands 2px short of the reported content and the sheet grows a
// permanent scrollbar (found live on Uptimely, 2026-08-04).
boxSizing: "content-box",
width: "min(560px, 94vw)",

@@ -76,0 +82,0 @@ height: "min(480px, 92vh)",

+1
-1

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/index.ts","../../core/src/origin.ts","../../core/src/message-types.ts"],"sourcesContent":["// @dodomain/connect — the embeddable browser widget.\n// Opens the hosted connect page in a modal iframe and relays lifecycle events.\n// Integrator branding (App.name/logoUrl/brandColor, 2026-07-21) reaches the\n// end user THROUGH the hosted flow this iframe renders — the widget draws no\n// flow chrome of its own, so it needs no branding API and no postMessage\n// contract change (message-types stays as-is).\n//\n// import { showDoDomain } from \"@dodomain/connect\";\n// const session = await fetch(\"/my-api/create-session\").then(r => r.json());\n// showDoDomain({ token: session.token, onVerified: () => refetch() });\n//\n// FIX(F-008, split F-010): imports the message-type constants + a type-only\n// contract from @dodomain/core/message-types — a ZERO-IMPORT module — never\n// zod at runtime (R5: this widget ships into an INTEGRATOR's page bundle, so\n// it stays dependency-free). F-008 originally imported the plain-const half\n// of @dodomain/core/messages (which ALSO imports zod, for zDoDomainMessage),\n// betting a bundler's tree-shaking would drop the unused zod graph. F-010\n// verified that bet against a real tsup build and it did NOT hold (esbuild's\n// default AND Rollup's tree-shaking both left zod's full runtime in dist,\n// confirmed via test/build.smoke.test.ts) — so the plain consts now live in\n// their own zod-free module (messages.ts's header has the full history) and\n// this package imports ONLY from there, guaranteeing zod can never reach\n// this bundle regardless of any bundler's tree-shaking sophistication.\nimport { DODOMAIN_DEFAULT_ORIGIN } from \"@dodomain/core/origin\";\nimport {\n EMBED_PARAM,\n EMBED_VALUE,\n MESSAGE_TYPES,\n ORIGIN_PARAM,\n THEME_PARAM,\n type DoDomainMessage,\n} from \"@dodomain/core/message-types\";\n\nexport interface ShowDoDomainOptions {\n /** Session token from POST /api/v1/sessions (dd_sess_…). */\n token: string;\n /** DoDomain origin. Defaults to https://app.dodomain.io. */\n baseUrl?: string;\n onVerified?: (detail: { domain?: string }) => void;\n onClose?: () => void;\n /**\n * FIX(F-010): fires when the hosted flow fails to load or reports a\n * session error — a cross-origin iframe's HTTP 404/500 exposes neither\n * `onerror` nor readable content by default, so before this fix a broken\n * embed just sat there silently. See DoDomainWidgetError's own doc for the\n * three cases.\n */\n onError?: (detail: DoDomainWidgetError) => void;\n /**\n * FIX(F-010): milliseconds to wait for the hosted flow's `dodomain:ready`\n * handshake before treating the embed as failed-to-load. Default 15000.\n */\n loadTimeoutMs?: number;\n /**\n * Host-page theme (2026-08-04 embed polish). Pass the theme YOUR page is\n * currently rendering so the embedded sheet matches it — the hosted flow\n * adopts it and hides its own theme toggle. Omitted ⇒ the flow resolves\n * its own theme (prefers-color-scheme / its visitor preference).\n */\n theme?: \"light\" | \"dark\";\n}\n\n/**\n * FIX(F-010): the three ways `onError` can fire.\n * - `load-timeout` — no `dodomain:ready`/`dodomain:verified` arrived within\n * `loadTimeoutMs` (covers a 404/DNS failure/hung load — anything that\n * never gets far enough to run the hosted flow's own JS).\n * - `load-error` — the iframe's own `error` event fired (best-effort;\n * browsers rarely fire this for a cross-origin navigation, but it's free\n * to listen for).\n * - `session-error` — the hosted flow mounted and posted `dodomain:error`\n * with a `code` (e.g. an expired/not-found token, or a verify() failure —\n * see connect-flow.tsx).\n */\nexport type DoDomainWidgetError =\n { type: \"load-timeout\" } | { type: \"load-error\" } | { type: \"session-error\"; code: string };\n\nexport interface DoDomainHandle {\n close: () => void;\n}\n\nconst DEFAULT_BASE = DODOMAIN_DEFAULT_ORIGIN;\nconst DEFAULT_LOAD_TIMEOUT_MS = 15_000;\n\nexport function showDoDomain(opts: ShowDoDomainOptions): DoDomainHandle {\n if (typeof document === \"undefined\") {\n throw new Error(\"showDoDomain must run in a browser\");\n }\n const base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/$/, \"\");\n const origin = new URL(base).origin;\n\n const backdrop = document.createElement(\"div\");\n backdrop.setAttribute(\"data-dodomain\", \"backdrop\");\n // Graphite & Pine (docs/DESIGN.md): graphite-ink scrim (#17201C at 55%) — no\n // backdrop-blur (the system bans glassmorphism chrome) and no blue-grays.\n Object.assign(backdrop.style, {\n position: \"fixed\",\n inset: \"0\",\n background: \"rgba(23,32,28,0.55)\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n zIndex: \"2147483647\",\n } as CSSStyleDeclaration);\n\n const frame = document.createElement(\"iframe\");\n // FIX(F-008/§10.1 origin scoping): appends this page's own origin so the\n // hosted flow can scope postMessage's targetOrigin to it instead of \"*\" —\n // see connect-flow.tsx for the producer side of this handshake. The theme\n // param (2026-08-04 embed polish) hands the HOST page's theme to the flow\n // so the sheet matches the page around it.\n frame.src =\n `${base}/connect/${encodeURIComponent(opts.token)}` +\n `?${EMBED_PARAM}=${EMBED_VALUE}&${ORIGIN_PARAM}=${encodeURIComponent(window.location.origin)}` +\n (opts.theme ? `&${THEME_PARAM}=${opts.theme}` : \"\");\n frame.setAttribute(\"title\", \"Connect your domain\");\n // Graphite & Pine card: surface-1 + 1px hairline, card radius 14px,\n // level-3 (modal) graphite shadow. The background pre-paints the hosted\n // flow's canvas IN THE HANDED-OVER THEME, so a slow load never flashes the\n // wrong brightness. Height starts compact and then HUGS THE CONTENT: the\n // flow reports its natural height via `dodomain:height` (onMessage below)\n // and the frame follows — a fixed-height box left a dead slab of empty\n // canvas under short content (2026-08-04 embed polish).\n const dark = opts.theme === \"dark\";\n Object.assign(frame.style, {\n width: \"min(560px, 94vw)\",\n height: \"min(480px, 92vh)\",\n border: dark ? \"1px solid #2a352f\" : \"1px solid #e5e9e7\",\n borderRadius: \"14px\",\n boxShadow: \"0 1px 2px rgba(23,32,28,0.05), 0 12px 32px rgba(23,32,28,0.14)\",\n background: dark ? \"#17201c\" : \"#ffffff\",\n transition: \"height 180ms ease\",\n } as CSSStyleDeclaration);\n\n function applyReportedHeight(height: number) {\n if (!Number.isFinite(height) || height <= 0) return;\n const max = Math.floor(window.innerHeight * 0.92);\n const clamped = Math.max(280, Math.min(Math.ceil(height), max));\n frame.style.height = `${clamped}px`;\n }\n\n // FIX(F-010): the only reliable \"did the flow actually come up?\" signal —\n // a cross-origin iframe's 404/500 fires neither `onerror` nor exposes\n // readable content. Cleared by the first `dodomain:ready`/`dodomain:verified`\n // (onMessage below); otherwise fires onError({type:\"load-timeout\"}).\n let loadTimer: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {\n loadTimer = undefined;\n opts.onError?.({ type: \"load-timeout\" });\n }, opts.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS);\n\n function clearLoadTimer() {\n if (loadTimer !== undefined) {\n clearTimeout(loadTimer);\n loadTimer = undefined;\n }\n }\n\n // FIX(F-010): best-effort network-level signal (rarely fires for a\n // cross-origin navigation, but free to listen for) — the load-timeout\n // above is the primary detector.\n function onFrameError() {\n clearLoadTimer();\n opts.onError?.({ type: \"load-error\" });\n }\n frame.addEventListener(\"error\", onFrameError);\n\n let closed = false;\n function teardown() {\n if (closed) return;\n closed = true;\n clearLoadTimer();\n window.removeEventListener(\"message\", onMessage);\n frame.removeEventListener(\"error\", onFrameError);\n backdrop.remove();\n }\n function close() {\n teardown();\n opts.onClose?.();\n }\n\n function onMessage(e: MessageEvent) {\n if (e.origin !== origin) return;\n // Cheap runtime guard (no zod, per R5 — see the module-level fix note\n // above): a `MessageEvent.data` narrowing, not a full schema parse.\n const data = e.data as DoDomainMessage | undefined;\n if (!data || typeof data.type !== \"string\") return;\n if (data.type === MESSAGE_TYPES.VERIFIED) {\n clearLoadTimer();\n opts.onVerified?.({ domain: data.domain });\n } else if (data.type === MESSAGE_TYPES.READY) {\n clearLoadTimer();\n } else if (data.type === MESSAGE_TYPES.ERROR) {\n clearLoadTimer();\n opts.onError?.({ type: \"session-error\", code: data.code });\n } else if (data.type === MESSAGE_TYPES.HEIGHT) {\n applyReportedHeight(data.height);\n } else if (data.type === MESSAGE_TYPES.CLOSE) {\n close();\n }\n }\n\n backdrop.addEventListener(\"click\", (e) => {\n if (e.target === backdrop) close();\n });\n window.addEventListener(\"message\", onMessage);\n\n backdrop.appendChild(frame);\n document.body.appendChild(backdrop);\n\n return { close };\n}\n","// The single canonical public origin for the DoDomain hosted app (F-010) —\n// the production home for BOTH the REST API (`/api/v1/*`) and the hosted\n// connect flow (`/connect/:token`). See root README.md's \"Origins\" section\n// for the topology: `api.dodomain.io` / `connect.dodomain.io` are cosmetic\n// subdomain names for this same apps/web deployment, not separate hosts,\n// until ops splits them onto distinct deployments.\n//\n// Zero imports, framework-free — the one literal both the node SDK\n// (packages/node) and the embeddable widget (packages/connect) default to,\n// so a shipped SDK and a shipped widget can never re-diverge on the prod\n// origin the way they did before this fix (node defaulted to the unregistered\n// `api.dodomain.io`; connect defaulted to the unregistered `connect.dodomain.io`\n// — neither actually resolves, so the widget's iframe would 404 with zero\n// error surface). apps/web's own `env.ts` `APP_ORIGIN` stays a REQUIRED,\n// no-default env var by design (F-015, fail-closed) — this constant is a\n// client-facing SDK/widget default only, never an env fallback.\nexport const DODOMAIN_DEFAULT_ORIGIN = \"https://app.dodomain.io\";\n","// The zod-FREE half of the widget <-> hosted-flow postMessage contract\n// (F-010 split — see messages.ts's header for the full history). Zero\n// imports, so nothing here can ever pull zod into a consuming bundle,\n// regardless of tree-shaking. @dodomain/connect (bundle-size-sensitive — it\n// ships into an INTEGRATOR's page, not DoDomain's own) imports ONLY from\n// this file, never from messages.ts.\n//\n// messages.ts re-exports everything below unchanged, so existing\n// `from \"@dodomain/core/messages\"` imports (apps/web's connect-flow.tsx)\n// keep working without any change — messages.ts is still the one place that\n// ALSO exports the zod validator (zDoDomainMessage) for zod-tolerant\n// consumers.\n\n/**\n * postMessage type discriminants for the widget <-> hosted-flow contract.\n *\n * READY/ERROR are the load-detection handshake — a cross-origin iframe's\n * HTTP 404/500 fires neither `onerror` nor exposes readable content, so a\n * handshake postMessage from the flow is the only reliable \"did this\n * actually load?\" signal. The hosted flow (connect-flow.tsx) posts READY on\n * mount; the widget (packages/connect) starts a `loadTimeoutMs` timer on\n * show and clears it on the first READY/VERIFIED, else calls\n * `onError({type:\"load-timeout\"})`. ERROR carries a `code` (the same\n * verify()-failure vocabulary connect-flow.tsx already renders in its own\n * in-page banner) so the widget can call `onError({type:\"session-error\",code})`\n * — additive: an older widget build safely ignores both unknown types.\n */\nexport const MESSAGE_TYPES = {\n VERIFIED: \"dodomain:verified\",\n CLOSE: \"dodomain:close\",\n READY: \"dodomain:ready\",\n ERROR: \"dodomain:error\",\n // Content-height report (2026-08-04 embed polish): the hosted flow posts\n // its natural content height on mount and on every resize so the widget's\n // iframe can hug the content instead of sitting at a fixed height with\n // dead space below the footer. Additive — an older widget build safely\n // ignores the unknown type, and an older flow simply never posts it (the\n // widget keeps its initial height).\n HEIGHT: \"dodomain:height\",\n} as const;\n\n// ── The iframe URL contract ──────────────────────────────────────────────\n// packages/connect builds `${base}/connect/${token}?${EMBED_PARAM}=${EMBED_VALUE}\n// &${ORIGIN_PARAM}=<its own origin>`; the hosted connect page\n// (apps/web/src/app/connect/[token]/connect-flow.tsx) reads both params — ONE\n// set of query-param names instead of \"embed\"/\"origin\" string literals\n// hand-typed on both sides. `ORIGIN_PARAM` carries the embedding integrator's\n// origin so the hosted flow can scope postMessage's targetOrigin to it\n// instead of \"*\" (PLAN-F-008 §2/§10.1 — see connect-flow.tsx for the\n// documented \"*\" fallback when the param is absent).\nexport const EMBED_PARAM = \"embed\";\nexport const EMBED_VALUE = \"1\";\nexport const ORIGIN_PARAM = \"origin\";\n// Host-app theme handoff (2026-08-04 embed polish): the widget passes the\n// integrator page's theme so the embedded sheet matches it — a theme toggle\n// inside someone else's modal is chrome noise, so the hosted flow hides its\n// own toggle in embed mode and adopts this value instead. Only \"light\" and\n// \"dark\" are honored; anything else falls back to the flow's own resolution.\nexport const THEME_PARAM = \"theme\";\n\n// Hand-written (not `z.infer<typeof zDoDomainMessage>`, unlike before the\n// split — that schema now lives in messages.ts, which imports zod, and this\n// file must not). messages.ts's zDoDomainMessage is annotated\n// `z.ZodType<DoDomainMessage>` against THIS type, so if the two shapes ever\n// drift, messages.ts fails to typecheck — compiler-enforced sync, not just a\n// documentation promise.\nexport type DoDomainMessage =\n | { type: typeof MESSAGE_TYPES.VERIFIED; domain?: string }\n | { type: typeof MESSAGE_TYPES.CLOSE }\n | { type: typeof MESSAGE_TYPES.READY }\n | { type: typeof MESSAGE_TYPES.ERROR; code: string }\n | { type: typeof MESSAGE_TYPES.HEIGHT; height: number };\n"],"mappings":"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","names":[]}
{"version":3,"sources":["../src/index.ts","../../core/src/origin.ts","../../core/src/message-types.ts"],"sourcesContent":["// @dodomain/connect — the embeddable browser widget.\n// Opens the hosted connect page in a modal iframe and relays lifecycle events.\n// Integrator branding (App.name/logoUrl/brandColor, 2026-07-21) reaches the\n// end user THROUGH the hosted flow this iframe renders — the widget draws no\n// flow chrome of its own, so it needs no branding API and no postMessage\n// contract change (message-types stays as-is).\n//\n// import { showDoDomain } from \"@dodomain/connect\";\n// const session = await fetch(\"/my-api/create-session\").then(r => r.json());\n// showDoDomain({ token: session.token, onVerified: () => refetch() });\n//\n// FIX(F-008, split F-010): imports the message-type constants + a type-only\n// contract from @dodomain/core/message-types — a ZERO-IMPORT module — never\n// zod at runtime (R5: this widget ships into an INTEGRATOR's page bundle, so\n// it stays dependency-free). F-008 originally imported the plain-const half\n// of @dodomain/core/messages (which ALSO imports zod, for zDoDomainMessage),\n// betting a bundler's tree-shaking would drop the unused zod graph. F-010\n// verified that bet against a real tsup build and it did NOT hold (esbuild's\n// default AND Rollup's tree-shaking both left zod's full runtime in dist,\n// confirmed via test/build.smoke.test.ts) — so the plain consts now live in\n// their own zod-free module (messages.ts's header has the full history) and\n// this package imports ONLY from there, guaranteeing zod can never reach\n// this bundle regardless of any bundler's tree-shaking sophistication.\nimport { DODOMAIN_DEFAULT_ORIGIN } from \"@dodomain/core/origin\";\nimport {\n EMBED_PARAM,\n EMBED_VALUE,\n MESSAGE_TYPES,\n ORIGIN_PARAM,\n THEME_PARAM,\n type DoDomainMessage,\n} from \"@dodomain/core/message-types\";\n\nexport interface ShowDoDomainOptions {\n /** Session token from POST /api/v1/sessions (dd_sess_…). */\n token: string;\n /** DoDomain origin. Defaults to https://app.dodomain.io. */\n baseUrl?: string;\n onVerified?: (detail: { domain?: string }) => void;\n onClose?: () => void;\n /**\n * FIX(F-010): fires when the hosted flow fails to load or reports a\n * session error — a cross-origin iframe's HTTP 404/500 exposes neither\n * `onerror` nor readable content by default, so before this fix a broken\n * embed just sat there silently. See DoDomainWidgetError's own doc for the\n * three cases.\n */\n onError?: (detail: DoDomainWidgetError) => void;\n /**\n * FIX(F-010): milliseconds to wait for the hosted flow's `dodomain:ready`\n * handshake before treating the embed as failed-to-load. Default 15000.\n */\n loadTimeoutMs?: number;\n /**\n * Host-page theme (2026-08-04 embed polish). Pass the theme YOUR page is\n * currently rendering so the embedded sheet matches it — the hosted flow\n * adopts it and hides its own theme toggle. Omitted ⇒ the flow resolves\n * its own theme (prefers-color-scheme / its visitor preference).\n */\n theme?: \"light\" | \"dark\";\n}\n\n/**\n * FIX(F-010): the three ways `onError` can fire.\n * - `load-timeout` — no `dodomain:ready`/`dodomain:verified` arrived within\n * `loadTimeoutMs` (covers a 404/DNS failure/hung load — anything that\n * never gets far enough to run the hosted flow's own JS).\n * - `load-error` — the iframe's own `error` event fired (best-effort;\n * browsers rarely fire this for a cross-origin navigation, but it's free\n * to listen for).\n * - `session-error` — the hosted flow mounted and posted `dodomain:error`\n * with a `code` (e.g. an expired/not-found token, or a verify() failure —\n * see connect-flow.tsx).\n */\nexport type DoDomainWidgetError =\n { type: \"load-timeout\" } | { type: \"load-error\" } | { type: \"session-error\"; code: string };\n\nexport interface DoDomainHandle {\n close: () => void;\n}\n\nconst DEFAULT_BASE = DODOMAIN_DEFAULT_ORIGIN;\nconst DEFAULT_LOAD_TIMEOUT_MS = 15_000;\n\nexport function showDoDomain(opts: ShowDoDomainOptions): DoDomainHandle {\n if (typeof document === \"undefined\") {\n throw new Error(\"showDoDomain must run in a browser\");\n }\n const base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/$/, \"\");\n const origin = new URL(base).origin;\n\n const backdrop = document.createElement(\"div\");\n backdrop.setAttribute(\"data-dodomain\", \"backdrop\");\n // Graphite & Pine (docs/DESIGN.md): graphite-ink scrim (#17201C at 55%) — no\n // backdrop-blur (the system bans glassmorphism chrome) and no blue-grays.\n Object.assign(backdrop.style, {\n position: \"fixed\",\n inset: \"0\",\n background: \"rgba(23,32,28,0.55)\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n zIndex: \"2147483647\",\n } as CSSStyleDeclaration);\n\n const frame = document.createElement(\"iframe\");\n // FIX(F-008/§10.1 origin scoping): appends this page's own origin so the\n // hosted flow can scope postMessage's targetOrigin to it instead of \"*\" —\n // see connect-flow.tsx for the producer side of this handshake. The theme\n // param (2026-08-04 embed polish) hands the HOST page's theme to the flow\n // so the sheet matches the page around it.\n frame.src =\n `${base}/connect/${encodeURIComponent(opts.token)}` +\n `?${EMBED_PARAM}=${EMBED_VALUE}&${ORIGIN_PARAM}=${encodeURIComponent(window.location.origin)}` +\n (opts.theme ? `&${THEME_PARAM}=${opts.theme}` : \"\");\n frame.setAttribute(\"title\", \"Connect your domain\");\n // Graphite & Pine card: surface-1 + 1px hairline, card radius 14px,\n // level-3 (modal) graphite shadow. The background pre-paints the hosted\n // flow's canvas IN THE HANDED-OVER THEME, so a slow load never flashes the\n // wrong brightness. Height starts compact and then HUGS THE CONTENT: the\n // flow reports its natural height via `dodomain:height` (onMessage below)\n // and the frame follows — a fixed-height box left a dead slab of empty\n // canvas under short content (2026-08-04 embed polish).\n const dark = opts.theme === \"dark\";\n Object.assign(frame.style, {\n // content-box is load-bearing: host pages routinely reset every element\n // to border-box (Tailwind Preflight et al), which would make the 1px\n // borders eat into the height applyReportedHeight sets — the inner\n // viewport lands 2px short of the reported content and the sheet grows a\n // permanent scrollbar (found live on Uptimely, 2026-08-04).\n boxSizing: \"content-box\",\n width: \"min(560px, 94vw)\",\n height: \"min(480px, 92vh)\",\n border: dark ? \"1px solid #2a352f\" : \"1px solid #e5e9e7\",\n borderRadius: \"14px\",\n boxShadow: \"0 1px 2px rgba(23,32,28,0.05), 0 12px 32px rgba(23,32,28,0.14)\",\n background: dark ? \"#17201c\" : \"#ffffff\",\n transition: \"height 180ms ease\",\n } as CSSStyleDeclaration);\n\n function applyReportedHeight(height: number) {\n if (!Number.isFinite(height) || height <= 0) return;\n const max = Math.floor(window.innerHeight * 0.92);\n const clamped = Math.max(280, Math.min(Math.ceil(height), max));\n frame.style.height = `${clamped}px`;\n }\n\n // FIX(F-010): the only reliable \"did the flow actually come up?\" signal —\n // a cross-origin iframe's 404/500 fires neither `onerror` nor exposes\n // readable content. Cleared by the first `dodomain:ready`/`dodomain:verified`\n // (onMessage below); otherwise fires onError({type:\"load-timeout\"}).\n let loadTimer: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {\n loadTimer = undefined;\n opts.onError?.({ type: \"load-timeout\" });\n }, opts.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS);\n\n function clearLoadTimer() {\n if (loadTimer !== undefined) {\n clearTimeout(loadTimer);\n loadTimer = undefined;\n }\n }\n\n // FIX(F-010): best-effort network-level signal (rarely fires for a\n // cross-origin navigation, but free to listen for) — the load-timeout\n // above is the primary detector.\n function onFrameError() {\n clearLoadTimer();\n opts.onError?.({ type: \"load-error\" });\n }\n frame.addEventListener(\"error\", onFrameError);\n\n let closed = false;\n function teardown() {\n if (closed) return;\n closed = true;\n clearLoadTimer();\n window.removeEventListener(\"message\", onMessage);\n frame.removeEventListener(\"error\", onFrameError);\n backdrop.remove();\n }\n function close() {\n teardown();\n opts.onClose?.();\n }\n\n function onMessage(e: MessageEvent) {\n if (e.origin !== origin) return;\n // Cheap runtime guard (no zod, per R5 — see the module-level fix note\n // above): a `MessageEvent.data` narrowing, not a full schema parse.\n const data = e.data as DoDomainMessage | undefined;\n if (!data || typeof data.type !== \"string\") return;\n if (data.type === MESSAGE_TYPES.VERIFIED) {\n clearLoadTimer();\n opts.onVerified?.({ domain: data.domain });\n } else if (data.type === MESSAGE_TYPES.READY) {\n clearLoadTimer();\n } else if (data.type === MESSAGE_TYPES.ERROR) {\n clearLoadTimer();\n opts.onError?.({ type: \"session-error\", code: data.code });\n } else if (data.type === MESSAGE_TYPES.HEIGHT) {\n applyReportedHeight(data.height);\n } else if (data.type === MESSAGE_TYPES.CLOSE) {\n close();\n }\n }\n\n backdrop.addEventListener(\"click\", (e) => {\n if (e.target === backdrop) close();\n });\n window.addEventListener(\"message\", onMessage);\n\n backdrop.appendChild(frame);\n document.body.appendChild(backdrop);\n\n return { close };\n}\n","// The single canonical public origin for the DoDomain hosted app (F-010) —\n// the production home for BOTH the REST API (`/api/v1/*`) and the hosted\n// connect flow (`/connect/:token`). See root README.md's \"Origins\" section\n// for the topology: `api.dodomain.io` / `connect.dodomain.io` are cosmetic\n// subdomain names for this same apps/web deployment, not separate hosts,\n// until ops splits them onto distinct deployments.\n//\n// Zero imports, framework-free — the one literal both the node SDK\n// (packages/node) and the embeddable widget (packages/connect) default to,\n// so a shipped SDK and a shipped widget can never re-diverge on the prod\n// origin the way they did before this fix (node defaulted to the unregistered\n// `api.dodomain.io`; connect defaulted to the unregistered `connect.dodomain.io`\n// — neither actually resolves, so the widget's iframe would 404 with zero\n// error surface). apps/web's own `env.ts` `APP_ORIGIN` stays a REQUIRED,\n// no-default env var by design (F-015, fail-closed) — this constant is a\n// client-facing SDK/widget default only, never an env fallback.\nexport const DODOMAIN_DEFAULT_ORIGIN = \"https://app.dodomain.io\";\n","// The zod-FREE half of the widget <-> hosted-flow postMessage contract\n// (F-010 split — see messages.ts's header for the full history). Zero\n// imports, so nothing here can ever pull zod into a consuming bundle,\n// regardless of tree-shaking. @dodomain/connect (bundle-size-sensitive — it\n// ships into an INTEGRATOR's page, not DoDomain's own) imports ONLY from\n// this file, never from messages.ts.\n//\n// messages.ts re-exports everything below unchanged, so existing\n// `from \"@dodomain/core/messages\"` imports (apps/web's connect-flow.tsx)\n// keep working without any change — messages.ts is still the one place that\n// ALSO exports the zod validator (zDoDomainMessage) for zod-tolerant\n// consumers.\n\n/**\n * postMessage type discriminants for the widget <-> hosted-flow contract.\n *\n * READY/ERROR are the load-detection handshake — a cross-origin iframe's\n * HTTP 404/500 fires neither `onerror` nor exposes readable content, so a\n * handshake postMessage from the flow is the only reliable \"did this\n * actually load?\" signal. The hosted flow (connect-flow.tsx) posts READY on\n * mount; the widget (packages/connect) starts a `loadTimeoutMs` timer on\n * show and clears it on the first READY/VERIFIED, else calls\n * `onError({type:\"load-timeout\"})`. ERROR carries a `code` (the same\n * verify()-failure vocabulary connect-flow.tsx already renders in its own\n * in-page banner) so the widget can call `onError({type:\"session-error\",code})`\n * — additive: an older widget build safely ignores both unknown types.\n */\nexport const MESSAGE_TYPES = {\n VERIFIED: \"dodomain:verified\",\n CLOSE: \"dodomain:close\",\n READY: \"dodomain:ready\",\n ERROR: \"dodomain:error\",\n // Content-height report (2026-08-04 embed polish): the hosted flow posts\n // its natural content height on mount and on every resize so the widget's\n // iframe can hug the content instead of sitting at a fixed height with\n // dead space below the footer. Additive — an older widget build safely\n // ignores the unknown type, and an older flow simply never posts it (the\n // widget keeps its initial height).\n HEIGHT: \"dodomain:height\",\n} as const;\n\n// ── The iframe URL contract ──────────────────────────────────────────────\n// packages/connect builds `${base}/connect/${token}?${EMBED_PARAM}=${EMBED_VALUE}\n// &${ORIGIN_PARAM}=<its own origin>`; the hosted connect page\n// (apps/web/src/app/connect/[token]/connect-flow.tsx) reads both params — ONE\n// set of query-param names instead of \"embed\"/\"origin\" string literals\n// hand-typed on both sides. `ORIGIN_PARAM` carries the embedding integrator's\n// origin so the hosted flow can scope postMessage's targetOrigin to it\n// instead of \"*\" (PLAN-F-008 §2/§10.1 — see connect-flow.tsx for the\n// documented \"*\" fallback when the param is absent).\nexport const EMBED_PARAM = \"embed\";\nexport const EMBED_VALUE = \"1\";\nexport const ORIGIN_PARAM = \"origin\";\n// Host-app theme handoff (2026-08-04 embed polish): the widget passes the\n// integrator page's theme so the embedded sheet matches it — a theme toggle\n// inside someone else's modal is chrome noise, so the hosted flow hides its\n// own toggle in embed mode and adopts this value instead. Only \"light\" and\n// \"dark\" are honored; anything else falls back to the flow's own resolution.\nexport const THEME_PARAM = \"theme\";\n\n// Hand-written (not `z.infer<typeof zDoDomainMessage>`, unlike before the\n// split — that schema now lives in messages.ts, which imports zod, and this\n// file must not). messages.ts's zDoDomainMessage is annotated\n// `z.ZodType<DoDomainMessage>` against THIS type, so if the two shapes ever\n// drift, messages.ts fails to typecheck — compiler-enforced sync, not just a\n// documentation promise.\nexport type DoDomainMessage =\n | { type: typeof MESSAGE_TYPES.VERIFIED; domain?: string }\n | { type: typeof MESSAGE_TYPES.CLOSE }\n | { type: typeof MESSAGE_TYPES.READY }\n | { type: typeof MESSAGE_TYPES.ERROR; code: string }\n | { type: typeof MESSAGE_TYPES.HEIGHT; height: number };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgBO,IAAM,0BAA0B;;;ACWhC,IAAM,gBAAgB;AAAA,EAC3B,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOP,QAAQ;AACV;AAWO,IAAM,cAAc;AACpB,IAAM,cAAc;AACpB,IAAM,eAAe;AAMrB,IAAM,cAAc;;;AFuB3B,IAAM,eAAe;AACrB,IAAM,0BAA0B;AAEzB,SAAS,aAAa,MAA2C;AACtE,MAAI,OAAO,aAAa,aAAa;AACnC,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,QAAM,QAAQ,KAAK,WAAW,cAAc,QAAQ,OAAO,EAAE;AAC7D,QAAM,SAAS,IAAI,IAAI,IAAI,EAAE;AAE7B,QAAM,WAAW,SAAS,cAAc,KAAK;AAC7C,WAAS,aAAa,iBAAiB,UAAU;AAGjD,SAAO,OAAO,SAAS,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV,CAAwB;AAExB,QAAM,QAAQ,SAAS,cAAc,QAAQ;AAM7C,QAAM,MACJ,GAAG,IAAI,YAAY,mBAAmB,KAAK,KAAK,CAAC,IAC7C,WAAW,IAAI,WAAW,IAAI,YAAY,IAAI,mBAAmB,OAAO,SAAS,MAAM,CAAC,MAC3F,KAAK,QAAQ,IAAI,WAAW,IAAI,KAAK,KAAK,KAAK;AAClD,QAAM,aAAa,SAAS,qBAAqB;AAQjD,QAAM,OAAO,KAAK,UAAU;AAC5B,SAAO,OAAO,MAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMzB,WAAW;AAAA,IACX,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ,OAAO,sBAAsB;AAAA,IACrC,cAAc;AAAA,IACd,WAAW;AAAA,IACX,YAAY,OAAO,YAAY;AAAA,IAC/B,YAAY;AAAA,EACd,CAAwB;AAExB,WAAS,oBAAoB,QAAgB;AAC3C,QAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG;AAC7C,UAAM,MAAM,KAAK,MAAM,OAAO,cAAc,IAAI;AAChD,UAAM,UAAU,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC;AAC9D,UAAM,MAAM,SAAS,GAAG,OAAO;AAAA,EACjC;AAMA,MAAI,YAAuD,WAAW,MAAM;AAC1E,gBAAY;AACZ,SAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAAA,EACzC,GAAG,KAAK,iBAAiB,uBAAuB;AAEhD,WAAS,iBAAiB;AACxB,QAAI,cAAc,QAAW;AAC3B,mBAAa,SAAS;AACtB,kBAAY;AAAA,IACd;AAAA,EACF;AAKA,WAAS,eAAe;AACtB,mBAAe;AACf,SAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAAA,EACvC;AACA,QAAM,iBAAiB,SAAS,YAAY;AAE5C,MAAI,SAAS;AACb,WAAS,WAAW;AAClB,QAAI,OAAQ;AACZ,aAAS;AACT,mBAAe;AACf,WAAO,oBAAoB,WAAW,SAAS;AAC/C,UAAM,oBAAoB,SAAS,YAAY;AAC/C,aAAS,OAAO;AAAA,EAClB;AACA,WAAS,QAAQ;AACf,aAAS;AACT,SAAK,UAAU;AAAA,EACjB;AAEA,WAAS,UAAU,GAAiB;AAClC,QAAI,EAAE,WAAW,OAAQ;AAGzB,UAAM,OAAO,EAAE;AACf,QAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,SAAU;AAC5C,QAAI,KAAK,SAAS,cAAc,UAAU;AACxC,qBAAe;AACf,WAAK,aAAa,EAAE,QAAQ,KAAK,OAAO,CAAC;AAAA,IAC3C,WAAW,KAAK,SAAS,cAAc,OAAO;AAC5C,qBAAe;AAAA,IACjB,WAAW,KAAK,SAAS,cAAc,OAAO;AAC5C,qBAAe;AACf,WAAK,UAAU,EAAE,MAAM,iBAAiB,MAAM,KAAK,KAAK,CAAC;AAAA,IAC3D,WAAW,KAAK,SAAS,cAAc,QAAQ;AAC7C,0BAAoB,KAAK,MAAM;AAAA,IACjC,WAAW,KAAK,SAAS,cAAc,OAAO;AAC5C,YAAM;AAAA,IACR;AAAA,EACF;AAEA,WAAS,iBAAiB,SAAS,CAAC,MAAM;AACxC,QAAI,EAAE,WAAW,SAAU,OAAM;AAAA,EACnC,CAAC;AACD,SAAO,iBAAiB,WAAW,SAAS;AAE5C,WAAS,YAAY,KAAK;AAC1B,WAAS,KAAK,YAAY,QAAQ;AAElC,SAAO,EAAE,MAAM;AACjB;","names":[]}

@@ -48,2 +48,8 @@ // ../core/src/origin.ts

Object.assign(frame.style, {
// content-box is load-bearing: host pages routinely reset every element
// to border-box (Tailwind Preflight et al), which would make the 1px
// borders eat into the height applyReportedHeight sets — the inner
// viewport lands 2px short of the reported content and the sheet grows a
// permanent scrollbar (found live on Uptimely, 2026-08-04).
boxSizing: "content-box",
width: "min(560px, 94vw)",

@@ -50,0 +56,0 @@ height: "min(480px, 92vh)",

@@ -1,1 +0,1 @@

{"version":3,"sources":["../../core/src/origin.ts","../../core/src/message-types.ts","../src/index.ts"],"sourcesContent":["// The single canonical public origin for the DoDomain hosted app (F-010) —\n// the production home for BOTH the REST API (`/api/v1/*`) and the hosted\n// connect flow (`/connect/:token`). See root README.md's \"Origins\" section\n// for the topology: `api.dodomain.io` / `connect.dodomain.io` are cosmetic\n// subdomain names for this same apps/web deployment, not separate hosts,\n// until ops splits them onto distinct deployments.\n//\n// Zero imports, framework-free — the one literal both the node SDK\n// (packages/node) and the embeddable widget (packages/connect) default to,\n// so a shipped SDK and a shipped widget can never re-diverge on the prod\n// origin the way they did before this fix (node defaulted to the unregistered\n// `api.dodomain.io`; connect defaulted to the unregistered `connect.dodomain.io`\n// — neither actually resolves, so the widget's iframe would 404 with zero\n// error surface). apps/web's own `env.ts` `APP_ORIGIN` stays a REQUIRED,\n// no-default env var by design (F-015, fail-closed) — this constant is a\n// client-facing SDK/widget default only, never an env fallback.\nexport const DODOMAIN_DEFAULT_ORIGIN = \"https://app.dodomain.io\";\n","// The zod-FREE half of the widget <-> hosted-flow postMessage contract\n// (F-010 split — see messages.ts's header for the full history). Zero\n// imports, so nothing here can ever pull zod into a consuming bundle,\n// regardless of tree-shaking. @dodomain/connect (bundle-size-sensitive — it\n// ships into an INTEGRATOR's page, not DoDomain's own) imports ONLY from\n// this file, never from messages.ts.\n//\n// messages.ts re-exports everything below unchanged, so existing\n// `from \"@dodomain/core/messages\"` imports (apps/web's connect-flow.tsx)\n// keep working without any change — messages.ts is still the one place that\n// ALSO exports the zod validator (zDoDomainMessage) for zod-tolerant\n// consumers.\n\n/**\n * postMessage type discriminants for the widget <-> hosted-flow contract.\n *\n * READY/ERROR are the load-detection handshake — a cross-origin iframe's\n * HTTP 404/500 fires neither `onerror` nor exposes readable content, so a\n * handshake postMessage from the flow is the only reliable \"did this\n * actually load?\" signal. The hosted flow (connect-flow.tsx) posts READY on\n * mount; the widget (packages/connect) starts a `loadTimeoutMs` timer on\n * show and clears it on the first READY/VERIFIED, else calls\n * `onError({type:\"load-timeout\"})`. ERROR carries a `code` (the same\n * verify()-failure vocabulary connect-flow.tsx already renders in its own\n * in-page banner) so the widget can call `onError({type:\"session-error\",code})`\n * — additive: an older widget build safely ignores both unknown types.\n */\nexport const MESSAGE_TYPES = {\n VERIFIED: \"dodomain:verified\",\n CLOSE: \"dodomain:close\",\n READY: \"dodomain:ready\",\n ERROR: \"dodomain:error\",\n // Content-height report (2026-08-04 embed polish): the hosted flow posts\n // its natural content height on mount and on every resize so the widget's\n // iframe can hug the content instead of sitting at a fixed height with\n // dead space below the footer. Additive — an older widget build safely\n // ignores the unknown type, and an older flow simply never posts it (the\n // widget keeps its initial height).\n HEIGHT: \"dodomain:height\",\n} as const;\n\n// ── The iframe URL contract ──────────────────────────────────────────────\n// packages/connect builds `${base}/connect/${token}?${EMBED_PARAM}=${EMBED_VALUE}\n// &${ORIGIN_PARAM}=<its own origin>`; the hosted connect page\n// (apps/web/src/app/connect/[token]/connect-flow.tsx) reads both params — ONE\n// set of query-param names instead of \"embed\"/\"origin\" string literals\n// hand-typed on both sides. `ORIGIN_PARAM` carries the embedding integrator's\n// origin so the hosted flow can scope postMessage's targetOrigin to it\n// instead of \"*\" (PLAN-F-008 §2/§10.1 — see connect-flow.tsx for the\n// documented \"*\" fallback when the param is absent).\nexport const EMBED_PARAM = \"embed\";\nexport const EMBED_VALUE = \"1\";\nexport const ORIGIN_PARAM = \"origin\";\n// Host-app theme handoff (2026-08-04 embed polish): the widget passes the\n// integrator page's theme so the embedded sheet matches it — a theme toggle\n// inside someone else's modal is chrome noise, so the hosted flow hides its\n// own toggle in embed mode and adopts this value instead. Only \"light\" and\n// \"dark\" are honored; anything else falls back to the flow's own resolution.\nexport const THEME_PARAM = \"theme\";\n\n// Hand-written (not `z.infer<typeof zDoDomainMessage>`, unlike before the\n// split — that schema now lives in messages.ts, which imports zod, and this\n// file must not). messages.ts's zDoDomainMessage is annotated\n// `z.ZodType<DoDomainMessage>` against THIS type, so if the two shapes ever\n// drift, messages.ts fails to typecheck — compiler-enforced sync, not just a\n// documentation promise.\nexport type DoDomainMessage =\n | { type: typeof MESSAGE_TYPES.VERIFIED; domain?: string }\n | { type: typeof MESSAGE_TYPES.CLOSE }\n | { type: typeof MESSAGE_TYPES.READY }\n | { type: typeof MESSAGE_TYPES.ERROR; code: string }\n | { type: typeof MESSAGE_TYPES.HEIGHT; height: number };\n","// @dodomain/connect — the embeddable browser widget.\n// Opens the hosted connect page in a modal iframe and relays lifecycle events.\n// Integrator branding (App.name/logoUrl/brandColor, 2026-07-21) reaches the\n// end user THROUGH the hosted flow this iframe renders — the widget draws no\n// flow chrome of its own, so it needs no branding API and no postMessage\n// contract change (message-types stays as-is).\n//\n// import { showDoDomain } from \"@dodomain/connect\";\n// const session = await fetch(\"/my-api/create-session\").then(r => r.json());\n// showDoDomain({ token: session.token, onVerified: () => refetch() });\n//\n// FIX(F-008, split F-010): imports the message-type constants + a type-only\n// contract from @dodomain/core/message-types — a ZERO-IMPORT module — never\n// zod at runtime (R5: this widget ships into an INTEGRATOR's page bundle, so\n// it stays dependency-free). F-008 originally imported the plain-const half\n// of @dodomain/core/messages (which ALSO imports zod, for zDoDomainMessage),\n// betting a bundler's tree-shaking would drop the unused zod graph. F-010\n// verified that bet against a real tsup build and it did NOT hold (esbuild's\n// default AND Rollup's tree-shaking both left zod's full runtime in dist,\n// confirmed via test/build.smoke.test.ts) — so the plain consts now live in\n// their own zod-free module (messages.ts's header has the full history) and\n// this package imports ONLY from there, guaranteeing zod can never reach\n// this bundle regardless of any bundler's tree-shaking sophistication.\nimport { DODOMAIN_DEFAULT_ORIGIN } from \"@dodomain/core/origin\";\nimport {\n EMBED_PARAM,\n EMBED_VALUE,\n MESSAGE_TYPES,\n ORIGIN_PARAM,\n THEME_PARAM,\n type DoDomainMessage,\n} from \"@dodomain/core/message-types\";\n\nexport interface ShowDoDomainOptions {\n /** Session token from POST /api/v1/sessions (dd_sess_…). */\n token: string;\n /** DoDomain origin. Defaults to https://app.dodomain.io. */\n baseUrl?: string;\n onVerified?: (detail: { domain?: string }) => void;\n onClose?: () => void;\n /**\n * FIX(F-010): fires when the hosted flow fails to load or reports a\n * session error — a cross-origin iframe's HTTP 404/500 exposes neither\n * `onerror` nor readable content by default, so before this fix a broken\n * embed just sat there silently. See DoDomainWidgetError's own doc for the\n * three cases.\n */\n onError?: (detail: DoDomainWidgetError) => void;\n /**\n * FIX(F-010): milliseconds to wait for the hosted flow's `dodomain:ready`\n * handshake before treating the embed as failed-to-load. Default 15000.\n */\n loadTimeoutMs?: number;\n /**\n * Host-page theme (2026-08-04 embed polish). Pass the theme YOUR page is\n * currently rendering so the embedded sheet matches it — the hosted flow\n * adopts it and hides its own theme toggle. Omitted ⇒ the flow resolves\n * its own theme (prefers-color-scheme / its visitor preference).\n */\n theme?: \"light\" | \"dark\";\n}\n\n/**\n * FIX(F-010): the three ways `onError` can fire.\n * - `load-timeout` — no `dodomain:ready`/`dodomain:verified` arrived within\n * `loadTimeoutMs` (covers a 404/DNS failure/hung load — anything that\n * never gets far enough to run the hosted flow's own JS).\n * - `load-error` — the iframe's own `error` event fired (best-effort;\n * browsers rarely fire this for a cross-origin navigation, but it's free\n * to listen for).\n * - `session-error` — the hosted flow mounted and posted `dodomain:error`\n * with a `code` (e.g. an expired/not-found token, or a verify() failure —\n * see connect-flow.tsx).\n */\nexport type DoDomainWidgetError =\n { type: \"load-timeout\" } | { type: \"load-error\" } | { type: \"session-error\"; code: string };\n\nexport interface DoDomainHandle {\n close: () => void;\n}\n\nconst DEFAULT_BASE = DODOMAIN_DEFAULT_ORIGIN;\nconst DEFAULT_LOAD_TIMEOUT_MS = 15_000;\n\nexport function showDoDomain(opts: ShowDoDomainOptions): DoDomainHandle {\n if (typeof document === \"undefined\") {\n throw new Error(\"showDoDomain must run in a browser\");\n }\n const base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/$/, \"\");\n const origin = new URL(base).origin;\n\n const backdrop = document.createElement(\"div\");\n backdrop.setAttribute(\"data-dodomain\", \"backdrop\");\n // Graphite & Pine (docs/DESIGN.md): graphite-ink scrim (#17201C at 55%) — no\n // backdrop-blur (the system bans glassmorphism chrome) and no blue-grays.\n Object.assign(backdrop.style, {\n position: \"fixed\",\n inset: \"0\",\n background: \"rgba(23,32,28,0.55)\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n zIndex: \"2147483647\",\n } as CSSStyleDeclaration);\n\n const frame = document.createElement(\"iframe\");\n // FIX(F-008/§10.1 origin scoping): appends this page's own origin so the\n // hosted flow can scope postMessage's targetOrigin to it instead of \"*\" —\n // see connect-flow.tsx for the producer side of this handshake. The theme\n // param (2026-08-04 embed polish) hands the HOST page's theme to the flow\n // so the sheet matches the page around it.\n frame.src =\n `${base}/connect/${encodeURIComponent(opts.token)}` +\n `?${EMBED_PARAM}=${EMBED_VALUE}&${ORIGIN_PARAM}=${encodeURIComponent(window.location.origin)}` +\n (opts.theme ? `&${THEME_PARAM}=${opts.theme}` : \"\");\n frame.setAttribute(\"title\", \"Connect your domain\");\n // Graphite & Pine card: surface-1 + 1px hairline, card radius 14px,\n // level-3 (modal) graphite shadow. The background pre-paints the hosted\n // flow's canvas IN THE HANDED-OVER THEME, so a slow load never flashes the\n // wrong brightness. Height starts compact and then HUGS THE CONTENT: the\n // flow reports its natural height via `dodomain:height` (onMessage below)\n // and the frame follows — a fixed-height box left a dead slab of empty\n // canvas under short content (2026-08-04 embed polish).\n const dark = opts.theme === \"dark\";\n Object.assign(frame.style, {\n width: \"min(560px, 94vw)\",\n height: \"min(480px, 92vh)\",\n border: dark ? \"1px solid #2a352f\" : \"1px solid #e5e9e7\",\n borderRadius: \"14px\",\n boxShadow: \"0 1px 2px rgba(23,32,28,0.05), 0 12px 32px rgba(23,32,28,0.14)\",\n background: dark ? \"#17201c\" : \"#ffffff\",\n transition: \"height 180ms ease\",\n } as CSSStyleDeclaration);\n\n function applyReportedHeight(height: number) {\n if (!Number.isFinite(height) || height <= 0) return;\n const max = Math.floor(window.innerHeight * 0.92);\n const clamped = Math.max(280, Math.min(Math.ceil(height), max));\n frame.style.height = `${clamped}px`;\n }\n\n // FIX(F-010): the only reliable \"did the flow actually come up?\" signal —\n // a cross-origin iframe's 404/500 fires neither `onerror` nor exposes\n // readable content. Cleared by the first `dodomain:ready`/`dodomain:verified`\n // (onMessage below); otherwise fires onError({type:\"load-timeout\"}).\n let loadTimer: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {\n loadTimer = undefined;\n opts.onError?.({ type: \"load-timeout\" });\n }, opts.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS);\n\n function clearLoadTimer() {\n if (loadTimer !== undefined) {\n clearTimeout(loadTimer);\n loadTimer = undefined;\n }\n }\n\n // FIX(F-010): best-effort network-level signal (rarely fires for a\n // cross-origin navigation, but free to listen for) — the load-timeout\n // above is the primary detector.\n function onFrameError() {\n clearLoadTimer();\n opts.onError?.({ type: \"load-error\" });\n }\n frame.addEventListener(\"error\", onFrameError);\n\n let closed = false;\n function teardown() {\n if (closed) return;\n closed = true;\n clearLoadTimer();\n window.removeEventListener(\"message\", onMessage);\n frame.removeEventListener(\"error\", onFrameError);\n backdrop.remove();\n }\n function close() {\n teardown();\n opts.onClose?.();\n }\n\n function onMessage(e: MessageEvent) {\n if (e.origin !== origin) return;\n // Cheap runtime guard (no zod, per R5 — see the module-level fix note\n // above): a `MessageEvent.data` narrowing, not a full schema parse.\n const data = e.data as DoDomainMessage | undefined;\n if (!data || typeof data.type !== \"string\") return;\n if (data.type === MESSAGE_TYPES.VERIFIED) {\n clearLoadTimer();\n opts.onVerified?.({ domain: data.domain });\n } else if (data.type === MESSAGE_TYPES.READY) {\n clearLoadTimer();\n } else if (data.type === MESSAGE_TYPES.ERROR) {\n clearLoadTimer();\n opts.onError?.({ type: \"session-error\", code: data.code });\n } else if (data.type === MESSAGE_TYPES.HEIGHT) {\n applyReportedHeight(data.height);\n } else if (data.type === MESSAGE_TYPES.CLOSE) {\n close();\n }\n }\n\n backdrop.addEventListener(\"click\", (e) => {\n if (e.target === backdrop) close();\n });\n window.addEventListener(\"message\", onMessage);\n\n backdrop.appendChild(frame);\n document.body.appendChild(backdrop);\n\n return { close };\n}\n"],"mappings":"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","names":[]}
{"version":3,"sources":["../../core/src/origin.ts","../../core/src/message-types.ts","../src/index.ts"],"sourcesContent":["// The single canonical public origin for the DoDomain hosted app (F-010) —\n// the production home for BOTH the REST API (`/api/v1/*`) and the hosted\n// connect flow (`/connect/:token`). See root README.md's \"Origins\" section\n// for the topology: `api.dodomain.io` / `connect.dodomain.io` are cosmetic\n// subdomain names for this same apps/web deployment, not separate hosts,\n// until ops splits them onto distinct deployments.\n//\n// Zero imports, framework-free — the one literal both the node SDK\n// (packages/node) and the embeddable widget (packages/connect) default to,\n// so a shipped SDK and a shipped widget can never re-diverge on the prod\n// origin the way they did before this fix (node defaulted to the unregistered\n// `api.dodomain.io`; connect defaulted to the unregistered `connect.dodomain.io`\n// — neither actually resolves, so the widget's iframe would 404 with zero\n// error surface). apps/web's own `env.ts` `APP_ORIGIN` stays a REQUIRED,\n// no-default env var by design (F-015, fail-closed) — this constant is a\n// client-facing SDK/widget default only, never an env fallback.\nexport const DODOMAIN_DEFAULT_ORIGIN = \"https://app.dodomain.io\";\n","// The zod-FREE half of the widget <-> hosted-flow postMessage contract\n// (F-010 split — see messages.ts's header for the full history). Zero\n// imports, so nothing here can ever pull zod into a consuming bundle,\n// regardless of tree-shaking. @dodomain/connect (bundle-size-sensitive — it\n// ships into an INTEGRATOR's page, not DoDomain's own) imports ONLY from\n// this file, never from messages.ts.\n//\n// messages.ts re-exports everything below unchanged, so existing\n// `from \"@dodomain/core/messages\"` imports (apps/web's connect-flow.tsx)\n// keep working without any change — messages.ts is still the one place that\n// ALSO exports the zod validator (zDoDomainMessage) for zod-tolerant\n// consumers.\n\n/**\n * postMessage type discriminants for the widget <-> hosted-flow contract.\n *\n * READY/ERROR are the load-detection handshake — a cross-origin iframe's\n * HTTP 404/500 fires neither `onerror` nor exposes readable content, so a\n * handshake postMessage from the flow is the only reliable \"did this\n * actually load?\" signal. The hosted flow (connect-flow.tsx) posts READY on\n * mount; the widget (packages/connect) starts a `loadTimeoutMs` timer on\n * show and clears it on the first READY/VERIFIED, else calls\n * `onError({type:\"load-timeout\"})`. ERROR carries a `code` (the same\n * verify()-failure vocabulary connect-flow.tsx already renders in its own\n * in-page banner) so the widget can call `onError({type:\"session-error\",code})`\n * — additive: an older widget build safely ignores both unknown types.\n */\nexport const MESSAGE_TYPES = {\n VERIFIED: \"dodomain:verified\",\n CLOSE: \"dodomain:close\",\n READY: \"dodomain:ready\",\n ERROR: \"dodomain:error\",\n // Content-height report (2026-08-04 embed polish): the hosted flow posts\n // its natural content height on mount and on every resize so the widget's\n // iframe can hug the content instead of sitting at a fixed height with\n // dead space below the footer. Additive — an older widget build safely\n // ignores the unknown type, and an older flow simply never posts it (the\n // widget keeps its initial height).\n HEIGHT: \"dodomain:height\",\n} as const;\n\n// ── The iframe URL contract ──────────────────────────────────────────────\n// packages/connect builds `${base}/connect/${token}?${EMBED_PARAM}=${EMBED_VALUE}\n// &${ORIGIN_PARAM}=<its own origin>`; the hosted connect page\n// (apps/web/src/app/connect/[token]/connect-flow.tsx) reads both params — ONE\n// set of query-param names instead of \"embed\"/\"origin\" string literals\n// hand-typed on both sides. `ORIGIN_PARAM` carries the embedding integrator's\n// origin so the hosted flow can scope postMessage's targetOrigin to it\n// instead of \"*\" (PLAN-F-008 §2/§10.1 — see connect-flow.tsx for the\n// documented \"*\" fallback when the param is absent).\nexport const EMBED_PARAM = \"embed\";\nexport const EMBED_VALUE = \"1\";\nexport const ORIGIN_PARAM = \"origin\";\n// Host-app theme handoff (2026-08-04 embed polish): the widget passes the\n// integrator page's theme so the embedded sheet matches it — a theme toggle\n// inside someone else's modal is chrome noise, so the hosted flow hides its\n// own toggle in embed mode and adopts this value instead. Only \"light\" and\n// \"dark\" are honored; anything else falls back to the flow's own resolution.\nexport const THEME_PARAM = \"theme\";\n\n// Hand-written (not `z.infer<typeof zDoDomainMessage>`, unlike before the\n// split — that schema now lives in messages.ts, which imports zod, and this\n// file must not). messages.ts's zDoDomainMessage is annotated\n// `z.ZodType<DoDomainMessage>` against THIS type, so if the two shapes ever\n// drift, messages.ts fails to typecheck — compiler-enforced sync, not just a\n// documentation promise.\nexport type DoDomainMessage =\n | { type: typeof MESSAGE_TYPES.VERIFIED; domain?: string }\n | { type: typeof MESSAGE_TYPES.CLOSE }\n | { type: typeof MESSAGE_TYPES.READY }\n | { type: typeof MESSAGE_TYPES.ERROR; code: string }\n | { type: typeof MESSAGE_TYPES.HEIGHT; height: number };\n","// @dodomain/connect — the embeddable browser widget.\n// Opens the hosted connect page in a modal iframe and relays lifecycle events.\n// Integrator branding (App.name/logoUrl/brandColor, 2026-07-21) reaches the\n// end user THROUGH the hosted flow this iframe renders — the widget draws no\n// flow chrome of its own, so it needs no branding API and no postMessage\n// contract change (message-types stays as-is).\n//\n// import { showDoDomain } from \"@dodomain/connect\";\n// const session = await fetch(\"/my-api/create-session\").then(r => r.json());\n// showDoDomain({ token: session.token, onVerified: () => refetch() });\n//\n// FIX(F-008, split F-010): imports the message-type constants + a type-only\n// contract from @dodomain/core/message-types — a ZERO-IMPORT module — never\n// zod at runtime (R5: this widget ships into an INTEGRATOR's page bundle, so\n// it stays dependency-free). F-008 originally imported the plain-const half\n// of @dodomain/core/messages (which ALSO imports zod, for zDoDomainMessage),\n// betting a bundler's tree-shaking would drop the unused zod graph. F-010\n// verified that bet against a real tsup build and it did NOT hold (esbuild's\n// default AND Rollup's tree-shaking both left zod's full runtime in dist,\n// confirmed via test/build.smoke.test.ts) — so the plain consts now live in\n// their own zod-free module (messages.ts's header has the full history) and\n// this package imports ONLY from there, guaranteeing zod can never reach\n// this bundle regardless of any bundler's tree-shaking sophistication.\nimport { DODOMAIN_DEFAULT_ORIGIN } from \"@dodomain/core/origin\";\nimport {\n EMBED_PARAM,\n EMBED_VALUE,\n MESSAGE_TYPES,\n ORIGIN_PARAM,\n THEME_PARAM,\n type DoDomainMessage,\n} from \"@dodomain/core/message-types\";\n\nexport interface ShowDoDomainOptions {\n /** Session token from POST /api/v1/sessions (dd_sess_…). */\n token: string;\n /** DoDomain origin. Defaults to https://app.dodomain.io. */\n baseUrl?: string;\n onVerified?: (detail: { domain?: string }) => void;\n onClose?: () => void;\n /**\n * FIX(F-010): fires when the hosted flow fails to load or reports a\n * session error — a cross-origin iframe's HTTP 404/500 exposes neither\n * `onerror` nor readable content by default, so before this fix a broken\n * embed just sat there silently. See DoDomainWidgetError's own doc for the\n * three cases.\n */\n onError?: (detail: DoDomainWidgetError) => void;\n /**\n * FIX(F-010): milliseconds to wait for the hosted flow's `dodomain:ready`\n * handshake before treating the embed as failed-to-load. Default 15000.\n */\n loadTimeoutMs?: number;\n /**\n * Host-page theme (2026-08-04 embed polish). Pass the theme YOUR page is\n * currently rendering so the embedded sheet matches it — the hosted flow\n * adopts it and hides its own theme toggle. Omitted ⇒ the flow resolves\n * its own theme (prefers-color-scheme / its visitor preference).\n */\n theme?: \"light\" | \"dark\";\n}\n\n/**\n * FIX(F-010): the three ways `onError` can fire.\n * - `load-timeout` — no `dodomain:ready`/`dodomain:verified` arrived within\n * `loadTimeoutMs` (covers a 404/DNS failure/hung load — anything that\n * never gets far enough to run the hosted flow's own JS).\n * - `load-error` — the iframe's own `error` event fired (best-effort;\n * browsers rarely fire this for a cross-origin navigation, but it's free\n * to listen for).\n * - `session-error` — the hosted flow mounted and posted `dodomain:error`\n * with a `code` (e.g. an expired/not-found token, or a verify() failure —\n * see connect-flow.tsx).\n */\nexport type DoDomainWidgetError =\n { type: \"load-timeout\" } | { type: \"load-error\" } | { type: \"session-error\"; code: string };\n\nexport interface DoDomainHandle {\n close: () => void;\n}\n\nconst DEFAULT_BASE = DODOMAIN_DEFAULT_ORIGIN;\nconst DEFAULT_LOAD_TIMEOUT_MS = 15_000;\n\nexport function showDoDomain(opts: ShowDoDomainOptions): DoDomainHandle {\n if (typeof document === \"undefined\") {\n throw new Error(\"showDoDomain must run in a browser\");\n }\n const base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/$/, \"\");\n const origin = new URL(base).origin;\n\n const backdrop = document.createElement(\"div\");\n backdrop.setAttribute(\"data-dodomain\", \"backdrop\");\n // Graphite & Pine (docs/DESIGN.md): graphite-ink scrim (#17201C at 55%) — no\n // backdrop-blur (the system bans glassmorphism chrome) and no blue-grays.\n Object.assign(backdrop.style, {\n position: \"fixed\",\n inset: \"0\",\n background: \"rgba(23,32,28,0.55)\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n zIndex: \"2147483647\",\n } as CSSStyleDeclaration);\n\n const frame = document.createElement(\"iframe\");\n // FIX(F-008/§10.1 origin scoping): appends this page's own origin so the\n // hosted flow can scope postMessage's targetOrigin to it instead of \"*\" —\n // see connect-flow.tsx for the producer side of this handshake. The theme\n // param (2026-08-04 embed polish) hands the HOST page's theme to the flow\n // so the sheet matches the page around it.\n frame.src =\n `${base}/connect/${encodeURIComponent(opts.token)}` +\n `?${EMBED_PARAM}=${EMBED_VALUE}&${ORIGIN_PARAM}=${encodeURIComponent(window.location.origin)}` +\n (opts.theme ? `&${THEME_PARAM}=${opts.theme}` : \"\");\n frame.setAttribute(\"title\", \"Connect your domain\");\n // Graphite & Pine card: surface-1 + 1px hairline, card radius 14px,\n // level-3 (modal) graphite shadow. The background pre-paints the hosted\n // flow's canvas IN THE HANDED-OVER THEME, so a slow load never flashes the\n // wrong brightness. Height starts compact and then HUGS THE CONTENT: the\n // flow reports its natural height via `dodomain:height` (onMessage below)\n // and the frame follows — a fixed-height box left a dead slab of empty\n // canvas under short content (2026-08-04 embed polish).\n const dark = opts.theme === \"dark\";\n Object.assign(frame.style, {\n // content-box is load-bearing: host pages routinely reset every element\n // to border-box (Tailwind Preflight et al), which would make the 1px\n // borders eat into the height applyReportedHeight sets — the inner\n // viewport lands 2px short of the reported content and the sheet grows a\n // permanent scrollbar (found live on Uptimely, 2026-08-04).\n boxSizing: \"content-box\",\n width: \"min(560px, 94vw)\",\n height: \"min(480px, 92vh)\",\n border: dark ? \"1px solid #2a352f\" : \"1px solid #e5e9e7\",\n borderRadius: \"14px\",\n boxShadow: \"0 1px 2px rgba(23,32,28,0.05), 0 12px 32px rgba(23,32,28,0.14)\",\n background: dark ? \"#17201c\" : \"#ffffff\",\n transition: \"height 180ms ease\",\n } as CSSStyleDeclaration);\n\n function applyReportedHeight(height: number) {\n if (!Number.isFinite(height) || height <= 0) return;\n const max = Math.floor(window.innerHeight * 0.92);\n const clamped = Math.max(280, Math.min(Math.ceil(height), max));\n frame.style.height = `${clamped}px`;\n }\n\n // FIX(F-010): the only reliable \"did the flow actually come up?\" signal —\n // a cross-origin iframe's 404/500 fires neither `onerror` nor exposes\n // readable content. Cleared by the first `dodomain:ready`/`dodomain:verified`\n // (onMessage below); otherwise fires onError({type:\"load-timeout\"}).\n let loadTimer: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {\n loadTimer = undefined;\n opts.onError?.({ type: \"load-timeout\" });\n }, opts.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS);\n\n function clearLoadTimer() {\n if (loadTimer !== undefined) {\n clearTimeout(loadTimer);\n loadTimer = undefined;\n }\n }\n\n // FIX(F-010): best-effort network-level signal (rarely fires for a\n // cross-origin navigation, but free to listen for) — the load-timeout\n // above is the primary detector.\n function onFrameError() {\n clearLoadTimer();\n opts.onError?.({ type: \"load-error\" });\n }\n frame.addEventListener(\"error\", onFrameError);\n\n let closed = false;\n function teardown() {\n if (closed) return;\n closed = true;\n clearLoadTimer();\n window.removeEventListener(\"message\", onMessage);\n frame.removeEventListener(\"error\", onFrameError);\n backdrop.remove();\n }\n function close() {\n teardown();\n opts.onClose?.();\n }\n\n function onMessage(e: MessageEvent) {\n if (e.origin !== origin) return;\n // Cheap runtime guard (no zod, per R5 — see the module-level fix note\n // above): a `MessageEvent.data` narrowing, not a full schema parse.\n const data = e.data as DoDomainMessage | undefined;\n if (!data || typeof data.type !== \"string\") return;\n if (data.type === MESSAGE_TYPES.VERIFIED) {\n clearLoadTimer();\n opts.onVerified?.({ domain: data.domain });\n } else if (data.type === MESSAGE_TYPES.READY) {\n clearLoadTimer();\n } else if (data.type === MESSAGE_TYPES.ERROR) {\n clearLoadTimer();\n opts.onError?.({ type: \"session-error\", code: data.code });\n } else if (data.type === MESSAGE_TYPES.HEIGHT) {\n applyReportedHeight(data.height);\n } else if (data.type === MESSAGE_TYPES.CLOSE) {\n close();\n }\n }\n\n backdrop.addEventListener(\"click\", (e) => {\n if (e.target === backdrop) close();\n });\n window.addEventListener(\"message\", onMessage);\n\n backdrop.appendChild(frame);\n document.body.appendChild(backdrop);\n\n return { close };\n}\n"],"mappings":"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","names":[]}
{
"name": "@dodomain/connect",
"version": "0.2.0",
"version": "0.2.1",
"description": "Official browser widget for DoDomain — opens the hosted domain-connect flow in a modal iframe and relays its lifecycle events.",

@@ -5,0 +5,0 @@ "license": "MIT",