@declarion/embed 0.1.92 → 0.2.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.
package/README.md CHANGED
@@ -153,14 +153,23 @@ cross-origin `postMessage` frames are dropped silently. The SDK detects:
153
153
  A protocol version mismatch between this SDK and the Declarion deployment
154
154
  produces a clear `console.warn` naming both versions.
155
155
 
156
- ## Reference integration
157
-
158
- A complete, runnable foreign-host app lives at
159
- [`typescript/examples/embed-host`](../../examples/embed-host) in the Declarion
160
- Core monorepo. It embeds list, detail, and action screens, demonstrates both
161
- navigation modes, and mints tokens with `@declarion/embed/server` from a host
162
- backend that holds the `dk:` API key server-side only. Use it as the
163
- copy-from template for a real integration.
156
+ ## Putting it together
157
+
158
+ A real integration is two halves, and the snippets above are the whole
159
+ contract:
160
+
161
+ - **Host frontend** - renders `<DeclarionEmbed />` (React) or calls
162
+ `createDeclarionEmbed` (any framework) and supplies the async `getToken`
163
+ callback. The SDK owns the iframe, the `ready` -> `set-token` handshake,
164
+ resize, navigation, and token refresh.
165
+ - **Host backend** - holds the `dk:` API key (server-side only) and mints
166
+ short-lived, scoped embed tokens with `createEmbedSession` from
167
+ `@declarion/embed/server`. Your `getToken` callback fetches one from your
168
+ own backend endpoint.
169
+
170
+ The `dk:` key never leaves your server; the browser only ever holds a
171
+ short-lived, scoped embed token. That split is the entire security model -
172
+ keep `@declarion/embed/server` out of any browser bundle.
164
173
 
165
174
  ## License
166
175
 
package/dist/core.js CHANGED
@@ -114,7 +114,7 @@ function _(e) {
114
114
  payload: t
115
115
  }), y(i.reloadRequired, `The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`);
116
116
  }, T = (t) => {
117
- v("<- iframe resized", t), o && Number.isFinite(t.height) && t.height > 0 && (o.style.height = `${t.height}px`), e.onEvent?.({
117
+ v("<- iframe resized", t), !e.height && o && Number.isFinite(t.height) && t.height > 0 && (o.style.height = `${t.height}px`), e.onEvent?.({
118
118
  type: n.resized,
119
119
  payload: t
120
120
  });
@@ -140,6 +140,7 @@ function _(e) {
140
140
  }, k = () => {
141
141
  c !== null && (clearTimeout(c), c = null);
142
142
  }, A = (t) => {
143
+ if (!o || t.source !== o.contentWindow) return;
143
144
  let i = r(t, e.declarionOrigin);
144
145
  if (i.kind === "rejected") return;
145
146
  if (i.kind === "protocol-mismatch") {
@@ -193,7 +194,7 @@ function _(e) {
193
194
  destroy: () => void 0
194
195
  };
195
196
  }
196
- return o = document.createElement("iframe"), o.src = P, o.title = e.title ?? p, o.style.width = "100%", o.style.border = "0", o.style.display = "block", o.style.height || (o.style.height = "150px"), s = A, window.addEventListener("message", s), e.container.appendChild(o), v("iframe created", { src: P }), c = setTimeout(() => {
197
+ return o = document.createElement("iframe"), o.src = P, o.title = e.title ?? p, o.style.width = "100%", o.style.border = "0", o.style.display = "block", e.height ? o.style.height = e.height : o.style.height || (o.style.height = "150px"), s = A, window.addEventListener("message", s), e.container.appendChild(o), v("iframe created", { src: P }), c = setTimeout(() => {
197
198
  c = null, !l && y(i.handshakeTimeout, `No handshake from the iframe within ${f}ms. Check that \`declarionOrigin\` exactly matches the Declarion deployment origin, that the host origin is allow-listed in the deployment's DECLARION_FRAME_ANCESTORS, and that \`route\` resolves to a real screen.`);
198
199
  }, f), {
199
200
  navigate(r) {
package/dist/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","names":[],"sources":["../src/protocol.ts","../src/inbound.ts","../src/errors.ts","../src/url.ts","../src/core.ts"],"sourcesContent":["// Embed postMessage protocol - host-side contract.\n//\n// `@declarion/embed` is a SEPARATE npm package from `@declarion/react` and\n// MUST NOT depend on it (a host app must not pull the full Declarion UI SDK\n// to host an iframe). The protocol contract is therefore re-declared here,\n// independently, and MUST match the iframe side EXACTLY:\n// typescript/packages/react/src/embed/protocol.ts\n// A divergence breaks the handshake. Any change to the wire envelope, the\n// message-type set, or a payload shape MUST land in both files together.\n//\n// Wire envelope: { source: \"declarion-embed\", protocol: 1, type, payload }.\n//\n// Naming contract (developer-facing - keep it consistent):\n// - A message the iframe SENDS to the host is an EVENT, named in the past\n// tense - \"this happened\": `resized`, `navigated`, `navigation-requested`,\n// `dirty-changed`, `token-expired`, `reload-required`, `ready`.\n// - A message the host SENDS to the iframe is a COMMAND, named as an\n// imperative - \"do this\": `set-token`, `navigate`, `set-theme`.\n//\n// Security model: the host SDK validates EVERY inbound frame on two axes -\n// exact origin equality against the configured `declarionOrigin`, and the\n// envelope shape (`source` discriminator + numeric `protocol`). A frame that\n// fails either check is dropped SILENTLY; an untrusted page that re-framed\n// the iframe must learn nothing. Outbound host frames target the EXACT\n// `declarionOrigin`, never `\"*\"`.\n\n/**\n * The `source` discriminator stamped on every embed frame. Both the host SDK\n * and the iframe filter inbound traffic on this value so unrelated\n * `postMessage` frames (browser extensions, other widgets) are ignored.\n */\nexport const EMBED_MESSAGE_SOURCE = \"declarion-embed\" as const;\n\n/**\n * The protocol version this SDK speaks. Bumped only on a breaking\n * envelope/payload change. The SDK warns when the iframe reports a different\n * version (see the diagnostics path).\n */\nexport const EMBED_PROTOCOL_VERSION = 1 as const;\n\n/**\n * Every protocol message `type`. The object key is the camelCase name used in\n * code; the value is the on-wire string. Events (iframe -> host) are past\n * tense; commands (host -> iframe) are imperative. This SDK is the HOST side:\n * it RECEIVES the iframe-to-host events and SENDS the host-to-iframe commands.\n */\nexport const EMBED_MESSAGE_TYPES = {\n /** iframe -> host: SDK mounted, requests the first token. */\n ready: \"ready\",\n /** host -> iframe: deliver or refresh the embed token. */\n setToken: \"set-token\",\n /** iframe -> host: token rejected or near expiry. */\n tokenExpired: \"token-expired\",\n /** iframe -> host: the iframe must be reloaded. */\n reloadRequired: \"reload-required\",\n /** iframe -> host: embed content height changed. */\n resized: \"resized\",\n /** iframe -> host: `self` mode internal navigation happened. */\n navigated: \"navigated\",\n /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */\n navigationRequested: \"navigation-requested\",\n /** iframe -> host: the embedded screen's unsaved-edits state flipped. */\n dirtyChanged: \"dirty-changed\",\n /** host -> iframe: drive the iframe to a screen (deep-linking). */\n navigate: \"navigate\",\n /** host -> iframe: runtime theme switch. */\n setTheme: \"set-theme\",\n} as const;\n\n/** The union of all on-wire message `type` strings. */\nexport type EmbedMessageType =\n (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];\n\n// --- Payload interfaces, one per message type ---\n\n/** `ready` payload: empty - the frame itself is the signal. */\nexport type EmbedReadyPayload = Record<string, never>;\n\n/** `set-token` payload: the embed token and its absolute expiry. */\nexport interface EmbedSetTokenPayload {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** RFC 3339 expiry timestamp of the token. */\n readonly expires_at: string;\n}\n\n/** `token-expired` payload: empty - the host re-mints via `getToken`. */\nexport type EmbedTokenExpiredPayload = Record<string, never>;\n\n/** `reload-required` payload: a human-readable reason for the reload. */\nexport interface EmbedReloadRequiredPayload {\n /** Why the iframe must be reloaded (asset drift, terminal auth failure). */\n readonly reason: string;\n}\n\n/** `resized` payload: the measured content height in CSS pixels. */\nexport interface EmbedResizedPayload {\n /** Content height in CSS pixels. */\n readonly height: number;\n}\n\n/**\n * `navigated` / `navigation-requested` payload: the screen route and, when\n * resolvable, the bound entity code and record id.\n *\n * `entity` and `recordId` are optional: a `custom` screen has no entity, and\n * a list route has no record id.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /** The bound entity code, when the route resolves to one. */\n readonly entity?: string;\n /** The record id, when the route is a detail route with an id. */\n readonly recordId?: string;\n}\n\n/**\n * `dirty-changed` payload: whether the embedded screen currently has unsaved\n * edits. The host tracks this so it can guard its own navigation (its menu,\n * a host-initiated `navigate`) before moving the iframe away from a dirty\n * screen - the iframe never pops a dialog for host-driven navigation.\n */\nexport interface EmbedDirtyChangedPayload {\n /** True when the embedded screen has unsaved edits. */\n readonly dirty: boolean;\n}\n\n/** `navigate` payload: the route the host drives the iframe to. */\nexport interface EmbedNavigatePayload {\n /** The Declarion screen route the host wants opened. */\n readonly route: string;\n}\n\n/** `set-theme` payload: the theme the host switches the iframe to. */\nexport interface EmbedSetThemePayload {\n /** The requested theme. */\n readonly theme: EmbedTheme;\n}\n\n/** The theme hint carried on the `theme` URL param and `set-theme` frame. */\nexport type EmbedTheme = \"light\" | \"dark\";\n\n/**\n * Maps each message type to its payload shape. Used to type the inbound\n * classifier and the outbound sender so the payload is checked against the\n * message type.\n */\nexport interface EmbedMessagePayloadMap {\n [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;\n [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;\n [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;\n [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;\n [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;\n [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;\n [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;\n [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;\n}\n\n/**\n * The wire envelope. Every embed frame is exactly this shape: a fixed\n * `source` + `protocol` pair, a discriminating `type`, and the typed\n * `payload` for that type.\n */\nexport interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {\n readonly source: typeof EMBED_MESSAGE_SOURCE;\n readonly protocol: typeof EMBED_PROTOCOL_VERSION;\n readonly type: T;\n readonly payload: EmbedMessagePayloadMap[T];\n}\n","// Inbound `postMessage` validation - host side.\n//\n// The host SDK receives the iframe-to-parent frames. EVERY inbound frame is\n// validated on two axes before it is acted on:\n// 1. `event.origin === declarionOrigin` - exact string equality, no\n// prefix, no wildcard. A frame from any other origin is dropped.\n// 2. the envelope shape - `source === \"declarion-embed\"` and a numeric\n// `protocol`.\n// A frame failing either check is dropped SILENTLY (security): an untrusted\n// page that re-framed the iframe must learn nothing.\n//\n// A frame that passes origin + source but carries a DIFFERENT `protocol`\n// version is classified `protocol-mismatch`: that frame IS the trusted\n// iframe, just on a mismatched SDK version. The caller surfaces it as a\n// clear console warning naming both versions, never a silent drop.\n//\n// This mirrors the iframe-side classifier:\n// typescript/packages/react/src/embed/handshake.ts\n\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n type EmbedMessage,\n} from \"./protocol\";\n\n/**\n * The classification of an inbound `message` event after validation.\n *\n * - `valid`: a trusted, shape-correct, version-matched embed frame.\n * - `protocol-mismatch`: origin + source are trusted, but the `protocol`\n * version differs. The caller logs a warning; the frame is not acted on.\n * - `rejected`: not a trusted embed frame (wrong origin, missing/foreign\n * `source`, malformed envelope). The caller drops it silently.\n */\nexport type InboundClassification =\n | { kind: \"valid\"; message: EmbedMessage }\n | { kind: \"protocol-mismatch\"; received: unknown }\n | { kind: \"rejected\" };\n\n/**\n * Validate and classify an inbound `message` event against the trusted\n * `declarionOrigin`.\n *\n * Returns `rejected` for anything that is not a trusted embed frame - the\n * caller drops those with no logging. Returns `protocol-mismatch` when the\n * frame is trusted but on a different protocol version. Returns `valid` with\n * the typed envelope otherwise.\n */\nexport function classifyInboundMessage(\n event: MessageEvent,\n declarionOrigin: string,\n): InboundClassification {\n // Axis 1: exact origin match. A trailing slash, a subdomain, a different\n // port - all fail this equality and the frame is dropped.\n if (event.origin !== declarionOrigin) {\n return { kind: \"rejected\" };\n }\n\n // Axis 2: envelope shape. The data must be an object carrying our\n // `source` discriminator and a numeric `protocol`.\n const data = event.data as unknown;\n if (typeof data !== \"object\" || data === null) {\n return { kind: \"rejected\" };\n }\n const envelope = data as Partial<EmbedMessage>;\n if (envelope.source !== EMBED_MESSAGE_SOURCE) {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.protocol !== \"number\") {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.type !== \"string\") {\n return { kind: \"rejected\" };\n }\n\n // Trusted iframe, our envelope, but a different protocol version. Surface\n // it loudly - this is the iframe on a mismatched version, not an attacker.\n if (envelope.protocol !== EMBED_PROTOCOL_VERSION) {\n return { kind: \"protocol-mismatch\", received: envelope.protocol };\n }\n\n return { kind: \"valid\", message: envelope as EmbedMessage };\n}\n","// Typed, actionable embed diagnostics.\n//\n// A silently blank iframe is the worst embedding failure. This module is the\n// single definition of every developer-facing error the SDK can raise. Each\n// error carries a stable `code` (for programmatic handling) and a human\n// message written to be ACTIONABLE - it names the option to fix and, where\n// relevant, the deployment config.\n//\n// Two failure classes, opposite requirements (Decision 21):\n// - Untrusted cross-origin `postMessage` frames: dropped SILENTLY. They are\n// a security concern; never surfaced. The SDK does this in the inbound\n// classifier and never constructs an EmbedError for them.\n// - Developer misconfiguration: surfaced LOUDLY via `onError` AND\n// `console.error`. Every such case is one of the codes below.\n\n/**\n * Stable, machine-readable embed error codes. A host may branch on\n * `error.code`; the strings are part of the public contract.\n */\nexport const EMBED_ERROR_CODES = {\n /**\n * A required `createDeclarionEmbed` option is missing or malformed\n * (`container`, `declarionOrigin`, `route`, `getToken`). Raised\n * synchronously before the iframe is created.\n */\n invalidOptions: \"invalid-options\",\n /**\n * The host's `getToken` callback rejected, threw, or resolved to a value\n * that is not `{ token: string, expires_at: string }`.\n */\n getTokenFailed: \"get-token-failed\",\n /**\n * No `ready` frame arrived from the iframe within the post-mount timeout.\n * The usual causes are a `declarionOrigin` mismatch (the iframe loaded a\n * different origin, or never loaded) or framing denied by the Declarion CSP\n * (the host origin is not in `DECLARION_FRAME_ANCESTORS`). A slow `getToken`\n * does NOT cause this - the timer is cleared as soon as `ready` arrives.\n */\n handshakeTimeout: \"handshake-timeout\",\n /**\n * The iframe asked the host to reload it (`reload-required`) - asset drift\n * or a terminal auth failure inside the iframe.\n */\n reloadRequired: \"reload-required\",\n} as const;\n\n/** The union of all embed error code strings. */\nexport type EmbedErrorCode =\n (typeof EMBED_ERROR_CODES)[keyof typeof EMBED_ERROR_CODES];\n\n/**\n * A typed embed error. Always passed to the host `onError` callback and\n * always also written to `console.error` with the `[declarion-embed]`\n * prefix, so a misconfiguration is never silent.\n *\n * `cause` carries the originating error when one exists (e.g. the rejection\n * value from `getToken`), preserving the stack for debugging.\n */\nexport class EmbedError extends Error {\n /** The stable, machine-readable error code. */\n readonly code: EmbedErrorCode;\n\n constructor(code: EmbedErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"EmbedError\";\n this.code = code;\n // Restore the prototype chain: `extends Error` across the ES5 transpile\n // target otherwise breaks `instanceof EmbedError`.\n Object.setPrototypeOf(this, EmbedError.prototype);\n }\n}\n","// Embed iframe `src` construction.\n//\n// The host SDK builds the iframe URL the Declarion deployment parses. The\n// param grammar MUST match the iframe-side parser EXACTLY:\n// typescript/packages/react/src/embed/params.ts\n// The iframe reads `embed=1`, `parent_origin`, `theme`, `nav`; any drift\n// here silently changes how the iframe boots.\n\nimport type { EmbedNavigationContract, EmbedTheme } from \"./types\";\n\n/** Query-param names that make up the embed URL contract. */\nexport const EMBED_PARAM_EMBED = \"embed\";\nexport const EMBED_PARAM_PARENT_ORIGIN = \"parent_origin\";\nexport const EMBED_PARAM_THEME = \"theme\";\nexport const EMBED_PARAM_NAV = \"nav\";\n\n/** The on-wire value of `embed` that enables shellless render. */\nconst EMBED_PARAM_EMBED_ENABLED = \"1\";\n\n/** Default navigation contract when the host does not set `navigation`. */\nexport const DEFAULT_NAVIGATION_CONTRACT: EmbedNavigationContract = \"self\";\n\n/** Inputs needed to build the iframe `src`. */\nexport interface BuildEmbedSrcInput {\n /** The Declarion deployment origin (`https://app.example.com`). */\n readonly declarionOrigin: string;\n /** The Declarion screen route to embed. */\n readonly route: string;\n /**\n * The host page's own origin. Becomes `parent_origin` so the iframe knows\n * exactly which origin may exchange `postMessage` frames with it.\n */\n readonly parentOrigin: string;\n /** Navigation contract; becomes the `nav` param. */\n readonly navigation: EmbedNavigationContract;\n /** Optional initial theme; becomes the `theme` param when set. */\n readonly theme?: EmbedTheme;\n}\n\n/**\n * Build the absolute iframe `src` URL for an embedded Declarion screen.\n *\n * The `route` is resolved as a path against `declarionOrigin`; the four\n * embed params are appended. `parent_origin` is the host's own origin so the\n * iframe restricts its `postMessage` traffic to exactly that origin.\n *\n * Throws when `declarionOrigin` is not a parseable absolute origin - callers\n * convert that into a typed `EmbedError` before raising it to the host.\n */\nexport function buildEmbedSrc(input: BuildEmbedSrcInput): string {\n // `route` may be a bare path (`/cases`) or already carry a query/hash.\n // Resolving it against the origin keeps any route-level query intact while\n // the embed params are layered on top.\n const url = new URL(input.route, input.declarionOrigin);\n url.searchParams.set(EMBED_PARAM_EMBED, EMBED_PARAM_EMBED_ENABLED);\n url.searchParams.set(EMBED_PARAM_PARENT_ORIGIN, input.parentOrigin);\n url.searchParams.set(EMBED_PARAM_NAV, input.navigation);\n if (input.theme) {\n url.searchParams.set(EMBED_PARAM_THEME, input.theme);\n }\n return url.toString();\n}\n\n/**\n * Resolve a Declarion screen route to an absolute URL for a runtime\n * `navigate` frame. The host passes a route string; the iframe consumes the\n * route as-is, so this only normalizes it against the deployment origin for\n * the host's own bookkeeping. Returns the input unchanged when it cannot be\n * resolved (the iframe tolerates a relative route).\n */\nexport function resolveRoute(declarionOrigin: string, route: string): string {\n try {\n const url = new URL(route, declarionOrigin);\n // Keep the hash so a host can deep-link to an in-page anchor; dropping it\n // would silently break `handle.navigate(\"/cases/42#notes\")`.\n return url.pathname + url.search + url.hash;\n } catch {\n return route;\n }\n}\n","// `createDeclarionEmbed` - the framework-agnostic, dependency-free embed core.\n//\n// Builds the iframe, runs the `ready` -> `auth` handshake, owns token refresh\n// through the host `getToken` callback, auto-applies `resize`, mirrors\n// navigation, and surfaces misconfiguration loudly. The React binding\n// (`./react`) and the demo host both build on this single core.\n\nimport {\n EMBED_MESSAGE_TYPES,\n EMBED_PROTOCOL_VERSION,\n type EmbedSetTokenPayload,\n type EmbedMessage,\n type EmbedMessagePayloadMap,\n type EmbedMessageType,\n type EmbedDirtyChangedPayload,\n type EmbedNavigationPayload,\n type EmbedReloadRequiredPayload,\n type EmbedResizedPayload,\n} from \"./protocol\";\nimport { classifyInboundMessage } from \"./inbound\";\nimport {\n EMBED_ERROR_CODES,\n EmbedError,\n type EmbedErrorCode,\n} from \"./errors\";\nimport {\n DEFAULT_NAVIGATION_CONTRACT,\n buildEmbedSrc,\n resolveRoute,\n} from \"./url\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n EmbedNavigateEvent,\n EmbedToken,\n} from \"./types\";\n\n/**\n * How long the SDK waits for the first `auth` frame to be requested by the\n * iframe after it loads. The iframe emits `ready`; if no `ready` arrives -\n * usually a `declarionOrigin` mismatch or framing denied by the Declarion\n * CSP - the SDK raises a `handshake-timeout` error rather than leave a\n * silently blank iframe.\n */\nconst HANDSHAKE_TIMEOUT_MS = 20_000;\n\n/** Default iframe `title` when the host does not supply one. */\nconst DEFAULT_IFRAME_TITLE = \"Declarion embedded screen\";\n\n/** Console prefix for every SDK diagnostic line. */\nconst LOG_PREFIX = \"[declarion-embed]\";\n\n/**\n * Validate `createDeclarionEmbed` options. Returns a typed `EmbedError` for\n * the first problem found, or `null` when the options are well-formed.\n *\n * Separated from `createDeclarionEmbed` so the React binding can reuse the\n * exact same validation.\n */\nfunction validateOptions(options: DeclarionEmbedOptions): EmbedError | null {\n const fail = (message: string): EmbedError =>\n new EmbedError(EMBED_ERROR_CODES.invalidOptions, message);\n\n if (!options.container || typeof options.container.appendChild !== \"function\") {\n return fail(\n \"`container` must be a DOM element that can receive the iframe.\",\n );\n }\n if (typeof options.declarionOrigin !== \"string\" || options.declarionOrigin === \"\") {\n return fail(\n \"`declarionOrigin` is required and must be the exact origin of the \" +\n \"Declarion deployment, e.g. \\\"https://app.example.com\\\".\",\n );\n }\n let parsedOrigin: URL;\n try {\n parsedOrigin = new URL(options.declarionOrigin);\n } catch {\n return fail(\n `\\`declarionOrigin\\` is not a valid URL: \"${options.declarionOrigin}\". ` +\n 'Pass an exact origin, e.g. \"https://app.example.com\".',\n );\n }\n if (parsedOrigin.origin !== options.declarionOrigin) {\n return fail(\n `\\`declarionOrigin\\` must be exactly an origin with no path, query, ` +\n `or trailing slash. Got \"${options.declarionOrigin}\"; expected ` +\n `\"${parsedOrigin.origin}\".`,\n );\n }\n if (typeof options.route !== \"string\" || options.route === \"\") {\n return fail(\"`route` is required and must be a Declarion screen route.\");\n }\n if (typeof options.getToken !== \"function\") {\n return fail(\n \"`getToken` is required and must be an async function returning \" +\n \"{ token, expires_at }.\",\n );\n }\n return null;\n}\n\n/**\n * Validate the value a host `getToken` callback resolved to. The host owns\n * this code; a malformed return is a developer mistake, surfaced loudly.\n */\nfunction isValidToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Create an embedded Declarion screen inside `options.container`.\n *\n * Returns a `DeclarionEmbedHandle` exposing `navigate`, `setTheme`, and\n * `destroy`. On a misconfiguration the SDK still returns a handle (so\n * `destroy` is always callable) but reports the problem through `onError`\n * and `console.error`; the iframe is not created in that case.\n */\nexport function createDeclarionEmbed(\n options: DeclarionEmbedOptions,\n): DeclarionEmbedHandle {\n let destroyed = false;\n let iframe: HTMLIFrameElement | null = null;\n let messageListener: ((event: MessageEvent) => void) | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let readyReceived = false;\n let firstAuthDelivered = false;\n\n /** Emit a debug line when `debug` is on. */\n const logDebug = (message: string, detail?: unknown): void => {\n if (!options.debug) return;\n if (detail === undefined) {\n console.info(`${LOG_PREFIX} ${message}`);\n } else {\n console.info(`${LOG_PREFIX} ${message}`, detail);\n }\n };\n\n /**\n * Report a developer-facing error. Always written to `console.error` AND\n * handed to `onError`, so a misconfiguration is never silent (Decision 21).\n */\n const reportError = (\n code: EmbedErrorCode,\n message: string,\n cause?: unknown,\n ): void => {\n const error = new EmbedError(code, message, cause ? { cause } : undefined);\n console.error(`${LOG_PREFIX} ${message}`, cause ?? \"\");\n options.onError?.(error);\n };\n\n /** Post an enveloped frame to the iframe, targeting the exact origin. */\n const postToIframe = <T extends EmbedMessageType>(\n type: T,\n payload: EmbedMessagePayloadMap[T],\n ): void => {\n if (!iframe?.contentWindow) return;\n const message: EmbedMessage<T> = {\n source: \"declarion-embed\",\n protocol: EMBED_PROTOCOL_VERSION,\n type,\n payload,\n };\n logDebug(`-> iframe ${type}`, payload);\n iframe.contentWindow.postMessage(message, options.declarionOrigin);\n };\n\n /**\n * Call the host `getToken` callback and deliver the token to the iframe.\n * Surfaces a `get-token-failed` error when the callback rejects, throws,\n * or resolves to a malformed value.\n */\n const requestAndDeliverToken = async (): Promise<void> => {\n let result: unknown;\n try {\n result = await options.getToken();\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` rejected. The host backend must mint a token via \" +\n \"auth.create_embed_session and return { token, expires_at }.\",\n cause,\n );\n return;\n }\n if (!isValidToken(result)) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` resolved to a malformed value. Expected \" +\n \"{ token: string, expires_at: string }.\",\n );\n return;\n }\n if (destroyed) return;\n const payload: EmbedSetTokenPayload = {\n token: result.token,\n expires_at: result.expires_at,\n };\n postToIframe(EMBED_MESSAGE_TYPES.setToken, payload);\n onAuthDelivered();\n };\n\n /** Handle the iframe `ready` frame: the iframe requests its first token. */\n const onReady = (): void => {\n readyReceived = true;\n clearHandshakeTimer();\n logDebug(\"<- iframe ready\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.ready });\n void requestAndDeliverToken();\n };\n\n /** Handle a `token-expired` frame: re-run `getToken` and deliver again. */\n const onTokenExpired = (): void => {\n logDebug(\"<- iframe token-expired\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.tokenExpired });\n void requestAndDeliverToken();\n };\n\n /** Handle a `reload-required` frame: surface it and notify the host. */\n const onReloadRequired = (payload: EmbedReloadRequiredPayload): void => {\n logDebug(\"<- iframe reload-required\", payload);\n options.onEvent?.({\n type: EMBED_MESSAGE_TYPES.reloadRequired,\n payload,\n });\n reportError(\n EMBED_ERROR_CODES.reloadRequired,\n `The embedded iframe requested a reload: ${payload.reason}. ` +\n \"Reload the iframe to recover.\",\n );\n };\n\n /** Handle a `resized` frame: auto-apply the reported height to the iframe. */\n const onResize = (payload: EmbedResizedPayload): void => {\n logDebug(\"<- iframe resized\", payload);\n if (iframe && Number.isFinite(payload.height) && payload.height > 0) {\n iframe.style.height = `${payload.height}px`;\n }\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.resized, payload });\n };\n\n /**\n * Handle a `dirty-changed` frame: surface the embedded screen's\n * unsaved-edits state. The SDK does not act on it; the host tracks it and\n * guards its own navigation (its menu, a host-initiated `navigate`) before\n * moving the iframe off a dirty screen.\n */\n const onDirtyChanged = (payload: EmbedDirtyChangedPayload): void => {\n logDebug(\"<- iframe dirty-changed\", payload);\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.dirtyChanged, payload });\n };\n\n /**\n * Handle a navigation frame. `navigated` (the iframe moved, `self` mode)\n * and `navigation-requested` (the iframe stayed, `delegated` mode) both\n * surface through `onNavigate`; `mode` records which.\n */\n const onNavigationFrame = (\n type:\n | typeof EMBED_MESSAGE_TYPES.navigated\n | typeof EMBED_MESSAGE_TYPES.navigationRequested,\n payload: EmbedNavigationPayload,\n ): void => {\n logDebug(`<- iframe ${type}`, payload);\n options.onEvent?.({ type, payload });\n const event: EmbedNavigateEvent = {\n mode: type === EMBED_MESSAGE_TYPES.navigated ? \"self\" : \"delegated\",\n route: payload.route,\n entity: payload.entity,\n recordId: payload.recordId,\n };\n options.onNavigate?.(event);\n };\n\n /**\n * Mark the embed ready once the first token is delivered to the iframe.\n * The SDK has no separate \"screen rendered\" signal; the first successful\n * `set-token` delivery after `ready` is the earliest reliable point the\n * host can treat the embedded screen as authenticated.\n */\n const onAuthDelivered = (): void => {\n if (!firstAuthDelivered) {\n firstAuthDelivered = true;\n logDebug(\"handshake complete - iframe authenticated\");\n options.onReady?.();\n }\n };\n\n /** Stop the post-load handshake timeout. */\n const clearHandshakeTimer = (): void => {\n if (handshakeTimer !== null) {\n clearTimeout(handshakeTimer);\n handshakeTimer = null;\n }\n };\n\n /**\n * The single `message` listener. Validates every frame via\n * `classifyInboundMessage`: untrusted frames are dropped silently, a\n * protocol mismatch warns loudly, a valid frame is dispatched.\n */\n const handleMessage = (event: MessageEvent): void => {\n const classified = classifyInboundMessage(event, options.declarionOrigin);\n\n if (classified.kind === \"rejected\") {\n // Untrusted or malformed frame. Dropped silently (security): a page\n // that re-framed the iframe must learn nothing from the host logs.\n return;\n }\n\n if (classified.kind === \"protocol-mismatch\") {\n // The trusted iframe on a mismatched protocol version. Warn loudly,\n // naming both versions - a silent drop here is a blank iframe.\n console.warn(\n `${LOG_PREFIX} protocol version mismatch: this SDK speaks protocol ` +\n `${EMBED_PROTOCOL_VERSION}, the iframe reported protocol ` +\n `${String(classified.received)}. Update @declarion/embed and the ` +\n \"Declarion deployment to matching versions.\",\n );\n return;\n }\n\n const { message } = classified;\n switch (message.type as EmbedMessageType) {\n case EMBED_MESSAGE_TYPES.ready:\n onReady();\n break;\n case EMBED_MESSAGE_TYPES.tokenExpired:\n onTokenExpired();\n break;\n case EMBED_MESSAGE_TYPES.reloadRequired:\n onReloadRequired(message.payload as EmbedReloadRequiredPayload);\n break;\n case EMBED_MESSAGE_TYPES.resized:\n onResize(message.payload as EmbedResizedPayload);\n break;\n case EMBED_MESSAGE_TYPES.navigated:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigated,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.navigationRequested:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigationRequested,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.dirtyChanged:\n onDirtyChanged(message.payload as EmbedDirtyChangedPayload);\n break;\n default:\n // `set-token`, `navigate`, `set-theme` are host -> iframe; the iframe\n // does not send them back. Any other type is not consumed. Ignore.\n break;\n }\n };\n\n // --- Construction ------------------------------------------------------\n\n const optionsError = validateOptions(options);\n if (optionsError) {\n console.error(`${LOG_PREFIX} ${optionsError.message}`);\n options.onError?.(optionsError);\n // Return an inert handle: the iframe was never created, so navigate /\n // setTheme are no-ops and destroy has nothing to tear down.\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n const navigation = options.navigation ?? DEFAULT_NAVIGATION_CONTRACT;\n // The host's own origin. The iframe restricts every `postMessage` it sends\n // to exactly this origin.\n const parentOrigin = window.location.origin;\n\n let src: string;\n try {\n src = buildEmbedSrc({\n declarionOrigin: options.declarionOrigin,\n route: options.route,\n parentOrigin,\n navigation,\n theme: options.theme,\n });\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.invalidOptions,\n `Could not build the iframe URL from route \"${options.route}\". ` +\n \"`route` must be a valid Declarion screen route.\",\n cause,\n );\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n iframe = document.createElement(\"iframe\");\n iframe.src = src;\n iframe.title = options.title ?? DEFAULT_IFRAME_TITLE;\n // The screen sizes itself; the host receives `resize` frames and the SDK\n // applies the height. A sensible non-zero starting height avoids a\n // zero-height flash before the first `resize` arrives.\n iframe.style.width = \"100%\";\n iframe.style.border = \"0\";\n iframe.style.display = \"block\";\n if (!iframe.style.height) {\n iframe.style.height = \"150px\";\n }\n\n messageListener = handleMessage;\n window.addEventListener(\"message\", messageListener);\n options.container.appendChild(iframe);\n logDebug(\"iframe created\", { src });\n\n // Bound the wait for the iframe's `ready` frame. No `ready` means the\n // iframe never loaded the embed runtime: a `declarionOrigin` mismatch,\n // framing denied by the Declarion CSP, or an unreachable route.\n handshakeTimer = setTimeout(() => {\n handshakeTimer = null;\n if (readyReceived) return;\n reportError(\n EMBED_ERROR_CODES.handshakeTimeout,\n `No handshake from the iframe within ${HANDSHAKE_TIMEOUT_MS}ms. ` +\n \"Check that `declarionOrigin` exactly matches the Declarion \" +\n \"deployment origin, that the host origin is allow-listed in the \" +\n \"deployment's DECLARION_FRAME_ANCESTORS, and that `route` resolves \" +\n \"to a real screen.\",\n );\n }, HANDSHAKE_TIMEOUT_MS);\n\n return {\n navigate(route: string): void {\n if (destroyed) return;\n // Drive the iframe to a screen route (deep-linking, both nav modes).\n postToIframe(EMBED_MESSAGE_TYPES.navigate, {\n route: resolveRoute(options.declarionOrigin, route),\n });\n },\n setTheme(theme): void {\n if (destroyed) return;\n postToIframe(EMBED_MESSAGE_TYPES.setTheme, { theme });\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearHandshakeTimer();\n if (messageListener) {\n window.removeEventListener(\"message\", messageListener);\n messageListener = null;\n }\n if (iframe?.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n iframe = null;\n logDebug(\"embed destroyed\");\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"core.js","names":[],"sources":["../src/protocol.ts","../src/inbound.ts","../src/errors.ts","../src/url.ts","../src/core.ts"],"sourcesContent":["// Embed postMessage protocol - host-side contract.\n//\n// `@declarion/embed` is a SEPARATE npm package from `@declarion/react` and\n// MUST NOT depend on it (a host app must not pull the full Declarion UI SDK\n// to host an iframe). The protocol contract is therefore re-declared here,\n// independently, and MUST match the iframe side EXACTLY:\n// typescript/packages/react/src/embed/protocol.ts\n// A divergence breaks the handshake. Any change to the wire envelope, the\n// message-type set, or a payload shape MUST land in both files together.\n//\n// Wire envelope: { source: \"declarion-embed\", protocol: 1, type, payload }.\n//\n// Naming contract (developer-facing - keep it consistent):\n// - A message the iframe SENDS to the host is an EVENT, named in the past\n// tense - \"this happened\": `resized`, `navigated`, `navigation-requested`,\n// `dirty-changed`, `token-expired`, `reload-required`, `ready`.\n// - A message the host SENDS to the iframe is a COMMAND, named as an\n// imperative - \"do this\": `set-token`, `navigate`, `set-theme`.\n//\n// Security model: the host SDK validates EVERY inbound frame on two axes -\n// exact origin equality against the configured `declarionOrigin`, and the\n// envelope shape (`source` discriminator + numeric `protocol`). A frame that\n// fails either check is dropped SILENTLY; an untrusted page that re-framed\n// the iframe must learn nothing. Outbound host frames target the EXACT\n// `declarionOrigin`, never `\"*\"`.\n\n/**\n * The `source` discriminator stamped on every embed frame. Both the host SDK\n * and the iframe filter inbound traffic on this value so unrelated\n * `postMessage` frames (browser extensions, other widgets) are ignored.\n */\nexport const EMBED_MESSAGE_SOURCE = \"declarion-embed\" as const;\n\n/**\n * The protocol version this SDK speaks. Bumped only on a breaking\n * envelope/payload change. The SDK warns when the iframe reports a different\n * version (see the diagnostics path).\n */\nexport const EMBED_PROTOCOL_VERSION = 1 as const;\n\n/**\n * Every protocol message `type`. The object key is the camelCase name used in\n * code; the value is the on-wire string. Events (iframe -> host) are past\n * tense; commands (host -> iframe) are imperative. This SDK is the HOST side:\n * it RECEIVES the iframe-to-host events and SENDS the host-to-iframe commands.\n */\nexport const EMBED_MESSAGE_TYPES = {\n /** iframe -> host: SDK mounted, requests the first token. */\n ready: \"ready\",\n /** host -> iframe: deliver or refresh the embed token. */\n setToken: \"set-token\",\n /** iframe -> host: token rejected or near expiry. */\n tokenExpired: \"token-expired\",\n /** iframe -> host: the iframe must be reloaded. */\n reloadRequired: \"reload-required\",\n /** iframe -> host: embed content height changed. */\n resized: \"resized\",\n /** iframe -> host: `self` mode internal navigation happened. */\n navigated: \"navigated\",\n /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */\n navigationRequested: \"navigation-requested\",\n /** iframe -> host: the embedded screen's unsaved-edits state flipped. */\n dirtyChanged: \"dirty-changed\",\n /** host -> iframe: drive the iframe to a screen (deep-linking). */\n navigate: \"navigate\",\n /** host -> iframe: runtime theme switch. */\n setTheme: \"set-theme\",\n} as const;\n\n/** The union of all on-wire message `type` strings. */\nexport type EmbedMessageType =\n (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];\n\n// --- Payload interfaces, one per message type ---\n\n/** `ready` payload: empty - the frame itself is the signal. */\nexport type EmbedReadyPayload = Record<string, never>;\n\n/** `set-token` payload: the embed token and its absolute expiry. */\nexport interface EmbedSetTokenPayload {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** RFC 3339 expiry timestamp of the token. */\n readonly expires_at: string;\n}\n\n/** `token-expired` payload: empty - the host re-mints via `getToken`. */\nexport type EmbedTokenExpiredPayload = Record<string, never>;\n\n/** `reload-required` payload: a human-readable reason for the reload. */\nexport interface EmbedReloadRequiredPayload {\n /** Why the iframe must be reloaded (asset drift, terminal auth failure). */\n readonly reason: string;\n}\n\n/** `resized` payload: the measured content height in CSS pixels. */\nexport interface EmbedResizedPayload {\n /** Content height in CSS pixels. */\n readonly height: number;\n}\n\n/**\n * `navigated` / `navigation-requested` payload: the screen route and, when\n * resolvable, the bound entity code and record id.\n *\n * `entity` and `recordId` are optional: a `custom` screen has no entity, and\n * a list route has no record id.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /** The bound entity code, when the route resolves to one. */\n readonly entity?: string;\n /** The record id, when the route is a detail route with an id. */\n readonly recordId?: string;\n}\n\n/**\n * `dirty-changed` payload: whether the embedded screen currently has unsaved\n * edits. The host tracks this so it can guard its own navigation (its menu,\n * a host-initiated `navigate`) before moving the iframe away from a dirty\n * screen - the iframe never pops a dialog for host-driven navigation.\n */\nexport interface EmbedDirtyChangedPayload {\n /** True when the embedded screen has unsaved edits. */\n readonly dirty: boolean;\n}\n\n/** `navigate` payload: the route the host drives the iframe to. */\nexport interface EmbedNavigatePayload {\n /** The Declarion screen route the host wants opened. */\n readonly route: string;\n}\n\n/** `set-theme` payload: the theme the host switches the iframe to. */\nexport interface EmbedSetThemePayload {\n /** The requested theme. */\n readonly theme: EmbedTheme;\n}\n\n/** The theme hint carried on the `theme` URL param and `set-theme` frame. */\nexport type EmbedTheme = \"light\" | \"dark\";\n\n/**\n * Maps each message type to its payload shape. Used to type the inbound\n * classifier and the outbound sender so the payload is checked against the\n * message type.\n */\nexport interface EmbedMessagePayloadMap {\n [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;\n [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;\n [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;\n [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;\n [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;\n [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;\n [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;\n [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;\n}\n\n/**\n * The wire envelope. Every embed frame is exactly this shape: a fixed\n * `source` + `protocol` pair, a discriminating `type`, and the typed\n * `payload` for that type.\n */\nexport interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {\n readonly source: typeof EMBED_MESSAGE_SOURCE;\n readonly protocol: typeof EMBED_PROTOCOL_VERSION;\n readonly type: T;\n readonly payload: EmbedMessagePayloadMap[T];\n}\n","// Inbound `postMessage` validation - host side.\n//\n// The host SDK receives the iframe-to-parent frames. EVERY inbound frame is\n// validated on two axes before it is acted on:\n// 1. `event.origin === declarionOrigin` - exact string equality, no\n// prefix, no wildcard. A frame from any other origin is dropped.\n// 2. the envelope shape - `source === \"declarion-embed\"` and a numeric\n// `protocol`.\n// A frame failing either check is dropped SILENTLY (security): an untrusted\n// page that re-framed the iframe must learn nothing.\n//\n// A frame that passes origin + source but carries a DIFFERENT `protocol`\n// version is classified `protocol-mismatch`: that frame IS the trusted\n// iframe, just on a mismatched SDK version. The caller surfaces it as a\n// clear console warning naming both versions, never a silent drop.\n//\n// This mirrors the iframe-side classifier:\n// typescript/packages/react/src/embed/handshake.ts\n\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n type EmbedMessage,\n} from \"./protocol\";\n\n/**\n * The classification of an inbound `message` event after validation.\n *\n * - `valid`: a trusted, shape-correct, version-matched embed frame.\n * - `protocol-mismatch`: origin + source are trusted, but the `protocol`\n * version differs. The caller logs a warning; the frame is not acted on.\n * - `rejected`: not a trusted embed frame (wrong origin, missing/foreign\n * `source`, malformed envelope). The caller drops it silently.\n */\nexport type InboundClassification =\n | { kind: \"valid\"; message: EmbedMessage }\n | { kind: \"protocol-mismatch\"; received: unknown }\n | { kind: \"rejected\" };\n\n/**\n * Validate and classify an inbound `message` event against the trusted\n * `declarionOrigin`.\n *\n * Returns `rejected` for anything that is not a trusted embed frame - the\n * caller drops those with no logging. Returns `protocol-mismatch` when the\n * frame is trusted but on a different protocol version. Returns `valid` with\n * the typed envelope otherwise.\n */\nexport function classifyInboundMessage(\n event: MessageEvent,\n declarionOrigin: string,\n): InboundClassification {\n // Axis 1: exact origin match. A trailing slash, a subdomain, a different\n // port - all fail this equality and the frame is dropped.\n if (event.origin !== declarionOrigin) {\n return { kind: \"rejected\" };\n }\n\n // Axis 2: envelope shape. The data must be an object carrying our\n // `source` discriminator and a numeric `protocol`.\n const data = event.data as unknown;\n if (typeof data !== \"object\" || data === null) {\n return { kind: \"rejected\" };\n }\n const envelope = data as Partial<EmbedMessage>;\n if (envelope.source !== EMBED_MESSAGE_SOURCE) {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.protocol !== \"number\") {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.type !== \"string\") {\n return { kind: \"rejected\" };\n }\n\n // Trusted iframe, our envelope, but a different protocol version. Surface\n // it loudly - this is the iframe on a mismatched version, not an attacker.\n if (envelope.protocol !== EMBED_PROTOCOL_VERSION) {\n return { kind: \"protocol-mismatch\", received: envelope.protocol };\n }\n\n return { kind: \"valid\", message: envelope as EmbedMessage };\n}\n","// Typed, actionable embed diagnostics.\n//\n// A silently blank iframe is the worst embedding failure. This module is the\n// single definition of every developer-facing error the SDK can raise. Each\n// error carries a stable `code` (for programmatic handling) and a human\n// message written to be ACTIONABLE - it names the option to fix and, where\n// relevant, the deployment config.\n//\n// Two failure classes, opposite requirements (Decision 21):\n// - Untrusted cross-origin `postMessage` frames: dropped SILENTLY. They are\n// a security concern; never surfaced. The SDK does this in the inbound\n// classifier and never constructs an EmbedError for them.\n// - Developer misconfiguration: surfaced LOUDLY via `onError` AND\n// `console.error`. Every such case is one of the codes below.\n\n/**\n * Stable, machine-readable embed error codes. A host may branch on\n * `error.code`; the strings are part of the public contract.\n */\nexport const EMBED_ERROR_CODES = {\n /**\n * A required `createDeclarionEmbed` option is missing or malformed\n * (`container`, `declarionOrigin`, `route`, `getToken`). Raised\n * synchronously before the iframe is created.\n */\n invalidOptions: \"invalid-options\",\n /**\n * The host's `getToken` callback rejected, threw, or resolved to a value\n * that is not `{ token: string, expires_at: string }`.\n */\n getTokenFailed: \"get-token-failed\",\n /**\n * No `ready` frame arrived from the iframe within the post-mount timeout.\n * The usual causes are a `declarionOrigin` mismatch (the iframe loaded a\n * different origin, or never loaded) or framing denied by the Declarion CSP\n * (the host origin is not in `DECLARION_FRAME_ANCESTORS`). A slow `getToken`\n * does NOT cause this - the timer is cleared as soon as `ready` arrives.\n */\n handshakeTimeout: \"handshake-timeout\",\n /**\n * The iframe asked the host to reload it (`reload-required`) - asset drift\n * or a terminal auth failure inside the iframe.\n */\n reloadRequired: \"reload-required\",\n} as const;\n\n/** The union of all embed error code strings. */\nexport type EmbedErrorCode =\n (typeof EMBED_ERROR_CODES)[keyof typeof EMBED_ERROR_CODES];\n\n/**\n * A typed embed error. Always passed to the host `onError` callback and\n * always also written to `console.error` with the `[declarion-embed]`\n * prefix, so a misconfiguration is never silent.\n *\n * `cause` carries the originating error when one exists (e.g. the rejection\n * value from `getToken`), preserving the stack for debugging.\n */\nexport class EmbedError extends Error {\n /** The stable, machine-readable error code. */\n readonly code: EmbedErrorCode;\n\n constructor(code: EmbedErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"EmbedError\";\n this.code = code;\n // Restore the prototype chain: `extends Error` across the ES5 transpile\n // target otherwise breaks `instanceof EmbedError`.\n Object.setPrototypeOf(this, EmbedError.prototype);\n }\n}\n","// Embed iframe `src` construction.\n//\n// The host SDK builds the iframe URL the Declarion deployment parses. The\n// param grammar MUST match the iframe-side parser EXACTLY:\n// typescript/packages/react/src/embed/params.ts\n// The iframe reads `embed=1`, `parent_origin`, `theme`, `nav`; any drift\n// here silently changes how the iframe boots.\n\nimport type { EmbedNavigationContract, EmbedTheme } from \"./types\";\n\n/** Query-param names that make up the embed URL contract. */\nexport const EMBED_PARAM_EMBED = \"embed\";\nexport const EMBED_PARAM_PARENT_ORIGIN = \"parent_origin\";\nexport const EMBED_PARAM_THEME = \"theme\";\nexport const EMBED_PARAM_NAV = \"nav\";\n\n/** The on-wire value of `embed` that enables shellless render. */\nconst EMBED_PARAM_EMBED_ENABLED = \"1\";\n\n/** Default navigation contract when the host does not set `navigation`. */\nexport const DEFAULT_NAVIGATION_CONTRACT: EmbedNavigationContract = \"self\";\n\n/** Inputs needed to build the iframe `src`. */\nexport interface BuildEmbedSrcInput {\n /** The Declarion deployment origin (`https://app.example.com`). */\n readonly declarionOrigin: string;\n /** The Declarion screen route to embed. */\n readonly route: string;\n /**\n * The host page's own origin. Becomes `parent_origin` so the iframe knows\n * exactly which origin may exchange `postMessage` frames with it.\n */\n readonly parentOrigin: string;\n /** Navigation contract; becomes the `nav` param. */\n readonly navigation: EmbedNavigationContract;\n /** Optional initial theme; becomes the `theme` param when set. */\n readonly theme?: EmbedTheme;\n}\n\n/**\n * Build the absolute iframe `src` URL for an embedded Declarion screen.\n *\n * The `route` is resolved as a path against `declarionOrigin`; the four\n * embed params are appended. `parent_origin` is the host's own origin so the\n * iframe restricts its `postMessage` traffic to exactly that origin.\n *\n * Throws when `declarionOrigin` is not a parseable absolute origin - callers\n * convert that into a typed `EmbedError` before raising it to the host.\n */\nexport function buildEmbedSrc(input: BuildEmbedSrcInput): string {\n // `route` may be a bare path (`/cases`) or already carry a query/hash.\n // Resolving it against the origin keeps any route-level query intact while\n // the embed params are layered on top.\n const url = new URL(input.route, input.declarionOrigin);\n url.searchParams.set(EMBED_PARAM_EMBED, EMBED_PARAM_EMBED_ENABLED);\n url.searchParams.set(EMBED_PARAM_PARENT_ORIGIN, input.parentOrigin);\n url.searchParams.set(EMBED_PARAM_NAV, input.navigation);\n if (input.theme) {\n url.searchParams.set(EMBED_PARAM_THEME, input.theme);\n }\n return url.toString();\n}\n\n/**\n * Resolve a Declarion screen route to an absolute URL for a runtime\n * `navigate` frame. The host passes a route string; the iframe consumes the\n * route as-is, so this only normalizes it against the deployment origin for\n * the host's own bookkeeping. Returns the input unchanged when it cannot be\n * resolved (the iframe tolerates a relative route).\n */\nexport function resolveRoute(declarionOrigin: string, route: string): string {\n try {\n const url = new URL(route, declarionOrigin);\n // Keep the hash so a host can deep-link to an in-page anchor; dropping it\n // would silently break `handle.navigate(\"/cases/42#notes\")`.\n return url.pathname + url.search + url.hash;\n } catch {\n return route;\n }\n}\n","// `createDeclarionEmbed` - the framework-agnostic, dependency-free embed core.\n//\n// Builds the iframe, runs the `ready` -> `auth` handshake, owns token refresh\n// through the host `getToken` callback, auto-applies `resize`, mirrors\n// navigation, and surfaces misconfiguration loudly. The React binding\n// (`./react`) and the demo host both build on this single core.\n\nimport {\n EMBED_MESSAGE_TYPES,\n EMBED_PROTOCOL_VERSION,\n type EmbedSetTokenPayload,\n type EmbedMessage,\n type EmbedMessagePayloadMap,\n type EmbedMessageType,\n type EmbedDirtyChangedPayload,\n type EmbedNavigationPayload,\n type EmbedReloadRequiredPayload,\n type EmbedResizedPayload,\n} from \"./protocol\";\nimport { classifyInboundMessage } from \"./inbound\";\nimport {\n EMBED_ERROR_CODES,\n EmbedError,\n type EmbedErrorCode,\n} from \"./errors\";\nimport {\n DEFAULT_NAVIGATION_CONTRACT,\n buildEmbedSrc,\n resolveRoute,\n} from \"./url\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n EmbedNavigateEvent,\n EmbedToken,\n} from \"./types\";\n\n/**\n * How long the SDK waits for the first `auth` frame to be requested by the\n * iframe after it loads. The iframe emits `ready`; if no `ready` arrives -\n * usually a `declarionOrigin` mismatch or framing denied by the Declarion\n * CSP - the SDK raises a `handshake-timeout` error rather than leave a\n * silently blank iframe.\n */\nconst HANDSHAKE_TIMEOUT_MS = 20_000;\n\n/** Default iframe `title` when the host does not supply one. */\nconst DEFAULT_IFRAME_TITLE = \"Declarion embedded screen\";\n\n/** Console prefix for every SDK diagnostic line. */\nconst LOG_PREFIX = \"[declarion-embed]\";\n\n/**\n * Validate `createDeclarionEmbed` options. Returns a typed `EmbedError` for\n * the first problem found, or `null` when the options are well-formed.\n *\n * Separated from `createDeclarionEmbed` so the React binding can reuse the\n * exact same validation.\n */\nfunction validateOptions(options: DeclarionEmbedOptions): EmbedError | null {\n const fail = (message: string): EmbedError =>\n new EmbedError(EMBED_ERROR_CODES.invalidOptions, message);\n\n if (!options.container || typeof options.container.appendChild !== \"function\") {\n return fail(\n \"`container` must be a DOM element that can receive the iframe.\",\n );\n }\n if (typeof options.declarionOrigin !== \"string\" || options.declarionOrigin === \"\") {\n return fail(\n \"`declarionOrigin` is required and must be the exact origin of the \" +\n \"Declarion deployment, e.g. \\\"https://app.example.com\\\".\",\n );\n }\n let parsedOrigin: URL;\n try {\n parsedOrigin = new URL(options.declarionOrigin);\n } catch {\n return fail(\n `\\`declarionOrigin\\` is not a valid URL: \"${options.declarionOrigin}\". ` +\n 'Pass an exact origin, e.g. \"https://app.example.com\".',\n );\n }\n if (parsedOrigin.origin !== options.declarionOrigin) {\n return fail(\n `\\`declarionOrigin\\` must be exactly an origin with no path, query, ` +\n `or trailing slash. Got \"${options.declarionOrigin}\"; expected ` +\n `\"${parsedOrigin.origin}\".`,\n );\n }\n if (typeof options.route !== \"string\" || options.route === \"\") {\n return fail(\"`route` is required and must be a Declarion screen route.\");\n }\n if (typeof options.getToken !== \"function\") {\n return fail(\n \"`getToken` is required and must be an async function returning \" +\n \"{ token, expires_at }.\",\n );\n }\n return null;\n}\n\n/**\n * Validate the value a host `getToken` callback resolved to. The host owns\n * this code; a malformed return is a developer mistake, surfaced loudly.\n */\nfunction isValidToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Create an embedded Declarion screen inside `options.container`.\n *\n * Returns a `DeclarionEmbedHandle` exposing `navigate`, `setTheme`, and\n * `destroy`. On a misconfiguration the SDK still returns a handle (so\n * `destroy` is always callable) but reports the problem through `onError`\n * and `console.error`; the iframe is not created in that case.\n */\nexport function createDeclarionEmbed(\n options: DeclarionEmbedOptions,\n): DeclarionEmbedHandle {\n let destroyed = false;\n let iframe: HTMLIFrameElement | null = null;\n let messageListener: ((event: MessageEvent) => void) | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let readyReceived = false;\n let firstAuthDelivered = false;\n\n /** Emit a debug line when `debug` is on. */\n const logDebug = (message: string, detail?: unknown): void => {\n if (!options.debug) return;\n if (detail === undefined) {\n console.info(`${LOG_PREFIX} ${message}`);\n } else {\n console.info(`${LOG_PREFIX} ${message}`, detail);\n }\n };\n\n /**\n * Report a developer-facing error. Always written to `console.error` AND\n * handed to `onError`, so a misconfiguration is never silent (Decision 21).\n */\n const reportError = (\n code: EmbedErrorCode,\n message: string,\n cause?: unknown,\n ): void => {\n const error = new EmbedError(code, message, cause ? { cause } : undefined);\n console.error(`${LOG_PREFIX} ${message}`, cause ?? \"\");\n options.onError?.(error);\n };\n\n /** Post an enveloped frame to the iframe, targeting the exact origin. */\n const postToIframe = <T extends EmbedMessageType>(\n type: T,\n payload: EmbedMessagePayloadMap[T],\n ): void => {\n if (!iframe?.contentWindow) return;\n const message: EmbedMessage<T> = {\n source: \"declarion-embed\",\n protocol: EMBED_PROTOCOL_VERSION,\n type,\n payload,\n };\n logDebug(`-> iframe ${type}`, payload);\n iframe.contentWindow.postMessage(message, options.declarionOrigin);\n };\n\n /**\n * Call the host `getToken` callback and deliver the token to the iframe.\n * Surfaces a `get-token-failed` error when the callback rejects, throws,\n * or resolves to a malformed value.\n */\n const requestAndDeliverToken = async (): Promise<void> => {\n let result: unknown;\n try {\n result = await options.getToken();\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` rejected. The host backend must mint a token via \" +\n \"auth.create_embed_session and return { token, expires_at }.\",\n cause,\n );\n return;\n }\n if (!isValidToken(result)) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` resolved to a malformed value. Expected \" +\n \"{ token: string, expires_at: string }.\",\n );\n return;\n }\n if (destroyed) return;\n const payload: EmbedSetTokenPayload = {\n token: result.token,\n expires_at: result.expires_at,\n };\n postToIframe(EMBED_MESSAGE_TYPES.setToken, payload);\n onAuthDelivered();\n };\n\n /** Handle the iframe `ready` frame: the iframe requests its first token. */\n const onReady = (): void => {\n readyReceived = true;\n clearHandshakeTimer();\n logDebug(\"<- iframe ready\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.ready });\n void requestAndDeliverToken();\n };\n\n /** Handle a `token-expired` frame: re-run `getToken` and deliver again. */\n const onTokenExpired = (): void => {\n logDebug(\"<- iframe token-expired\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.tokenExpired });\n void requestAndDeliverToken();\n };\n\n /** Handle a `reload-required` frame: surface it and notify the host. */\n const onReloadRequired = (payload: EmbedReloadRequiredPayload): void => {\n logDebug(\"<- iframe reload-required\", payload);\n options.onEvent?.({\n type: EMBED_MESSAGE_TYPES.reloadRequired,\n payload,\n });\n reportError(\n EMBED_ERROR_CODES.reloadRequired,\n `The embedded iframe requested a reload: ${payload.reason}. ` +\n \"Reload the iframe to recover.\",\n );\n };\n\n /**\n * Handle a `resized` frame: auto-apply the reported height to the iframe.\n * Skipped entirely in fixed-height mode (`options.height` set) - there the\n * host owns the iframe height and the embedded screen scrolls internally.\n */\n const onResize = (payload: EmbedResizedPayload): void => {\n logDebug(\"<- iframe resized\", payload);\n if (!options.height && iframe && Number.isFinite(payload.height) && payload.height > 0) {\n iframe.style.height = `${payload.height}px`;\n }\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.resized, payload });\n };\n\n /**\n * Handle a `dirty-changed` frame: surface the embedded screen's\n * unsaved-edits state. The SDK does not act on it; the host tracks it and\n * guards its own navigation (its menu, a host-initiated `navigate`) before\n * moving the iframe off a dirty screen.\n */\n const onDirtyChanged = (payload: EmbedDirtyChangedPayload): void => {\n logDebug(\"<- iframe dirty-changed\", payload);\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.dirtyChanged, payload });\n };\n\n /**\n * Handle a navigation frame. `navigated` (the iframe moved, `self` mode)\n * and `navigation-requested` (the iframe stayed, `delegated` mode) both\n * surface through `onNavigate`; `mode` records which.\n */\n const onNavigationFrame = (\n type:\n | typeof EMBED_MESSAGE_TYPES.navigated\n | typeof EMBED_MESSAGE_TYPES.navigationRequested,\n payload: EmbedNavigationPayload,\n ): void => {\n logDebug(`<- iframe ${type}`, payload);\n options.onEvent?.({ type, payload });\n const event: EmbedNavigateEvent = {\n mode: type === EMBED_MESSAGE_TYPES.navigated ? \"self\" : \"delegated\",\n route: payload.route,\n entity: payload.entity,\n recordId: payload.recordId,\n };\n options.onNavigate?.(event);\n };\n\n /**\n * Mark the embed ready once the first token is delivered to the iframe.\n * The SDK has no separate \"screen rendered\" signal; the first successful\n * `set-token` delivery after `ready` is the earliest reliable point the\n * host can treat the embedded screen as authenticated.\n */\n const onAuthDelivered = (): void => {\n if (!firstAuthDelivered) {\n firstAuthDelivered = true;\n logDebug(\"handshake complete - iframe authenticated\");\n options.onReady?.();\n }\n };\n\n /** Stop the post-load handshake timeout. */\n const clearHandshakeTimer = (): void => {\n if (handshakeTimer !== null) {\n clearTimeout(handshakeTimer);\n handshakeTimer = null;\n }\n };\n\n /**\n * The single `message` listener. Validates every frame via\n * `classifyInboundMessage`: untrusted frames are dropped silently, a\n * protocol mismatch warns loudly, a valid frame is dispatched.\n */\n const handleMessage = (event: MessageEvent): void => {\n // Scope every inbound frame to THIS instance's iframe. A host page may\n // embed several Declarion screens at once; each instance adds its own\n // `window` `message` listener, and every iframe posts from the same\n // `declarionOrigin`, so origin alone cannot tell them apart. A frame\n // whose `source` is not this iframe's `contentWindow` belongs to another\n // embed (or another widget) - drop it silently so instances never\n // cross-talk.\n if (!iframe || event.source !== iframe.contentWindow) {\n return;\n }\n\n const classified = classifyInboundMessage(event, options.declarionOrigin);\n\n if (classified.kind === \"rejected\") {\n // Untrusted or malformed frame. Dropped silently (security): a page\n // that re-framed the iframe must learn nothing from the host logs.\n return;\n }\n\n if (classified.kind === \"protocol-mismatch\") {\n // The trusted iframe on a mismatched protocol version. Warn loudly,\n // naming both versions - a silent drop here is a blank iframe.\n console.warn(\n `${LOG_PREFIX} protocol version mismatch: this SDK speaks protocol ` +\n `${EMBED_PROTOCOL_VERSION}, the iframe reported protocol ` +\n `${String(classified.received)}. Update @declarion/embed and the ` +\n \"Declarion deployment to matching versions.\",\n );\n return;\n }\n\n const { message } = classified;\n switch (message.type as EmbedMessageType) {\n case EMBED_MESSAGE_TYPES.ready:\n onReady();\n break;\n case EMBED_MESSAGE_TYPES.tokenExpired:\n onTokenExpired();\n break;\n case EMBED_MESSAGE_TYPES.reloadRequired:\n onReloadRequired(message.payload as EmbedReloadRequiredPayload);\n break;\n case EMBED_MESSAGE_TYPES.resized:\n onResize(message.payload as EmbedResizedPayload);\n break;\n case EMBED_MESSAGE_TYPES.navigated:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigated,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.navigationRequested:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigationRequested,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.dirtyChanged:\n onDirtyChanged(message.payload as EmbedDirtyChangedPayload);\n break;\n default:\n // `set-token`, `navigate`, `set-theme` are host -> iframe; the iframe\n // does not send them back. Any other type is not consumed. Ignore.\n break;\n }\n };\n\n // --- Construction ------------------------------------------------------\n\n const optionsError = validateOptions(options);\n if (optionsError) {\n console.error(`${LOG_PREFIX} ${optionsError.message}`);\n options.onError?.(optionsError);\n // Return an inert handle: the iframe was never created, so navigate /\n // setTheme are no-ops and destroy has nothing to tear down.\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n const navigation = options.navigation ?? DEFAULT_NAVIGATION_CONTRACT;\n // The host's own origin. The iframe restricts every `postMessage` it sends\n // to exactly this origin.\n const parentOrigin = window.location.origin;\n\n let src: string;\n try {\n src = buildEmbedSrc({\n declarionOrigin: options.declarionOrigin,\n route: options.route,\n parentOrigin,\n navigation,\n theme: options.theme,\n });\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.invalidOptions,\n `Could not build the iframe URL from route \"${options.route}\". ` +\n \"`route` must be a valid Declarion screen route.\",\n cause,\n );\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n iframe = document.createElement(\"iframe\");\n iframe.src = src;\n iframe.title = options.title ?? DEFAULT_IFRAME_TITLE;\n iframe.style.width = \"100%\";\n iframe.style.border = \"0\";\n iframe.style.display = \"block\";\n // Fixed-height mode: the host owns the height and the SDK never auto-\n // resizes (see onResize). Otherwise a sensible non-zero starting height\n // avoids a zero-height flash before the first `resize` frame arrives.\n if (options.height) {\n iframe.style.height = options.height;\n } else if (!iframe.style.height) {\n iframe.style.height = \"150px\";\n }\n\n messageListener = handleMessage;\n window.addEventListener(\"message\", messageListener);\n options.container.appendChild(iframe);\n logDebug(\"iframe created\", { src });\n\n // Bound the wait for the iframe's `ready` frame. No `ready` means the\n // iframe never loaded the embed runtime: a `declarionOrigin` mismatch,\n // framing denied by the Declarion CSP, or an unreachable route.\n handshakeTimer = setTimeout(() => {\n handshakeTimer = null;\n if (readyReceived) return;\n reportError(\n EMBED_ERROR_CODES.handshakeTimeout,\n `No handshake from the iframe within ${HANDSHAKE_TIMEOUT_MS}ms. ` +\n \"Check that `declarionOrigin` exactly matches the Declarion \" +\n \"deployment origin, that the host origin is allow-listed in the \" +\n \"deployment's DECLARION_FRAME_ANCESTORS, and that `route` resolves \" +\n \"to a real screen.\",\n );\n }, HANDSHAKE_TIMEOUT_MS);\n\n return {\n navigate(route: string): void {\n if (destroyed) return;\n // Drive the iframe to a screen route (deep-linking, both nav modes).\n postToIframe(EMBED_MESSAGE_TYPES.navigate, {\n route: resolveRoute(options.declarionOrigin, route),\n });\n },\n setTheme(theme): void {\n if (destroyed) return;\n postToIframe(EMBED_MESSAGE_TYPES.setTheme, { theme });\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearHandshakeTimer();\n if (messageListener) {\n window.removeEventListener(\"message\", messageListener);\n messageListener = null;\n }\n if (iframe?.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n iframe = null;\n logDebug(\"embed destroyed\");\n },\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- var DeclarionEmbed=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`declarion-embed`,n=1,r={ready:`ready`,setToken:`set-token`,tokenExpired:`token-expired`,reloadRequired:`reload-required`,resized:`resized`,navigated:`navigated`,navigationRequested:`navigation-requested`,dirtyChanged:`dirty-changed`,navigate:`navigate`,setTheme:`set-theme`};function i(e,t){if(e.origin!==t)return{kind:`rejected`};let n=e.data;if(typeof n!=`object`||!n)return{kind:`rejected`};let r=n;return r.source!==`declarion-embed`||typeof r.protocol!=`number`||typeof r.type!=`string`?{kind:`rejected`}:r.protocol===1?{kind:`valid`,message:r}:{kind:`protocol-mismatch`,received:r.protocol}}var a={invalidOptions:`invalid-options`,getTokenFailed:`get-token-failed`,handshakeTimeout:`handshake-timeout`,reloadRequired:`reload-required`},o=class e extends Error{code;constructor(t,n,r){super(n,r),this.name=`EmbedError`,this.code=t,Object.setPrototypeOf(this,e.prototype)}},s=`embed`,c=`parent_origin`,l=`theme`,u=`1`;function d(e){let t=new URL(e.route,e.declarionOrigin);return t.searchParams.set(s,u),t.searchParams.set(c,e.parentOrigin),t.searchParams.set(`nav`,e.navigation),e.theme&&t.searchParams.set(l,e.theme),t.toString()}function f(e,t){try{let n=new URL(t,e);return n.pathname+n.search+n.hash}catch{return t}}var p=2e4,m=`Declarion embedded screen`,h=`[declarion-embed]`;function g(e){let t=e=>new o(a.invalidOptions,e);if(!e.container||typeof e.container.appendChild!=`function`)return t("`container` must be a DOM element that can receive the iframe.");if(typeof e.declarionOrigin!=`string`||e.declarionOrigin===``)return t('`declarionOrigin` is required and must be the exact origin of the Declarion deployment, e.g. "https://app.example.com".');let n;try{n=new URL(e.declarionOrigin)}catch{return t(`\`declarionOrigin\` is not a valid URL: "${e.declarionOrigin}". Pass an exact origin, e.g. "https://app.example.com".`)}return n.origin===e.declarionOrigin?typeof e.route!=`string`||e.route===``?t("`route` is required and must be a Declarion screen route."):typeof e.getToken==`function`?null:t("`getToken` is required and must be an async function returning { token, expires_at }."):t(`\`declarionOrigin\` must be exactly an origin with no path, query, or trailing slash. Got "${e.declarionOrigin}"; expected "${n.origin}".`)}function _(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.token==`string`&&t.token!==``&&typeof t.expires_at==`string`&&t.expires_at!==``}function v(e){let t=!1,n=null,s=null,c=null,l=!1,u=!1,v=(t,n)=>{e.debug&&(n===void 0?console.info(`${h} ${t}`):console.info(`${h} ${t}`,n))},y=(t,n,r)=>{let i=new o(t,n,r?{cause:r}:void 0);console.error(`${h} ${n}`,r??``),e.onError?.(i)},b=(t,r)=>{if(!n?.contentWindow)return;let i={source:`declarion-embed`,protocol:1,type:t,payload:r};v(`-> iframe ${t}`,r),n.contentWindow.postMessage(i,e.declarionOrigin)},x=async()=>{let n;try{n=await e.getToken()}catch(e){y(a.getTokenFailed,"`getToken` rejected. The host backend must mint a token via auth.create_embed_session and return { token, expires_at }.",e);return}if(!_(n)){y(a.getTokenFailed,"`getToken` resolved to a malformed value. Expected { token: string, expires_at: string }.");return}if(t)return;let i={token:n.token,expires_at:n.expires_at};b(r.setToken,i),O()},S=()=>{l=!0,k(),v(`<- iframe ready`),e.onEvent?.({type:r.ready}),x()},C=()=>{v(`<- iframe token-expired`),e.onEvent?.({type:r.tokenExpired}),x()},w=t=>{v(`<- iframe reload-required`,t),e.onEvent?.({type:r.reloadRequired,payload:t}),y(a.reloadRequired,`The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`)},T=t=>{v(`<- iframe resized`,t),n&&Number.isFinite(t.height)&&t.height>0&&(n.style.height=`${t.height}px`),e.onEvent?.({type:r.resized,payload:t})},E=t=>{v(`<- iframe dirty-changed`,t),e.onEvent?.({type:r.dirtyChanged,payload:t})},D=(t,n)=>{v(`<- iframe ${t}`,n),e.onEvent?.({type:t,payload:n});let i={mode:t===r.navigated?`self`:`delegated`,route:n.route,entity:n.entity,recordId:n.recordId};e.onNavigate?.(i)},O=()=>{u||(u=!0,v(`handshake complete - iframe authenticated`),e.onReady?.())},k=()=>{c!==null&&(clearTimeout(c),c=null)},A=t=>{let n=i(t,e.declarionOrigin);if(n.kind===`rejected`)return;if(n.kind===`protocol-mismatch`){console.warn(`${h} protocol version mismatch: this SDK speaks protocol 1, the iframe reported protocol ${String(n.received)}. Update @declarion/embed and the Declarion deployment to matching versions.`);return}let{message:a}=n;switch(a.type){case r.ready:S();break;case r.tokenExpired:C();break;case r.reloadRequired:w(a.payload);break;case r.resized:T(a.payload);break;case r.navigated:D(r.navigated,a.payload);break;case r.navigationRequested:D(r.navigationRequested,a.payload);break;case r.dirtyChanged:E(a.payload);break;default:break}},j=g(e);if(j)return console.error(`${h} ${j.message}`),e.onError?.(j),{navigate:()=>void 0,setTheme:()=>void 0,destroy:()=>void 0};let M=e.navigation??`self`,N=window.location.origin,P;try{P=d({declarionOrigin:e.declarionOrigin,route:e.route,parentOrigin:N,navigation:M,theme:e.theme})}catch(t){return y(a.invalidOptions,`Could not build the iframe URL from route "${e.route}". \`route\` must be a valid Declarion screen route.`,t),{navigate:()=>void 0,setTheme:()=>void 0,destroy:()=>void 0}}return n=document.createElement(`iframe`),n.src=P,n.title=e.title??m,n.style.width=`100%`,n.style.border=`0`,n.style.display=`block`,n.style.height||(n.style.height=`150px`),s=A,window.addEventListener(`message`,s),e.container.appendChild(n),v(`iframe created`,{src:P}),c=setTimeout(()=>{c=null,!l&&y(a.handshakeTimeout,`No handshake from the iframe within ${p}ms. Check that \`declarionOrigin\` exactly matches the Declarion deployment origin, that the host origin is allow-listed in the deployment's DECLARION_FRAME_ANCESTORS, and that \`route\` resolves to a real screen.`)},p),{navigate(n){t||b(r.navigate,{route:f(e.declarionOrigin,n)})},setTheme(e){t||b(r.setTheme,{theme:e})},destroy(){t||(t=!0,k(),s&&=(window.removeEventListener(`message`,s),null),n?.parentNode&&n.parentNode.removeChild(n),n=null,v(`embed destroyed`))}}}return e.EMBED_ERROR_CODES=a,e.EMBED_MESSAGE_SOURCE=t,e.EMBED_MESSAGE_TYPES=r,e.EMBED_PROTOCOL_VERSION=n,e.EmbedError=o,e.createDeclarionEmbed=v,e})({});
1
+ var DeclarionEmbed=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`declarion-embed`,n=1,r={ready:`ready`,setToken:`set-token`,tokenExpired:`token-expired`,reloadRequired:`reload-required`,resized:`resized`,navigated:`navigated`,navigationRequested:`navigation-requested`,dirtyChanged:`dirty-changed`,navigate:`navigate`,setTheme:`set-theme`};function i(e,t){if(e.origin!==t)return{kind:`rejected`};let n=e.data;if(typeof n!=`object`||!n)return{kind:`rejected`};let r=n;return r.source!==`declarion-embed`||typeof r.protocol!=`number`||typeof r.type!=`string`?{kind:`rejected`}:r.protocol===1?{kind:`valid`,message:r}:{kind:`protocol-mismatch`,received:r.protocol}}var a={invalidOptions:`invalid-options`,getTokenFailed:`get-token-failed`,handshakeTimeout:`handshake-timeout`,reloadRequired:`reload-required`},o=class e extends Error{code;constructor(t,n,r){super(n,r),this.name=`EmbedError`,this.code=t,Object.setPrototypeOf(this,e.prototype)}},s=`embed`,c=`parent_origin`,l=`theme`,u=`1`;function d(e){let t=new URL(e.route,e.declarionOrigin);return t.searchParams.set(s,u),t.searchParams.set(c,e.parentOrigin),t.searchParams.set(`nav`,e.navigation),e.theme&&t.searchParams.set(l,e.theme),t.toString()}function f(e,t){try{let n=new URL(t,e);return n.pathname+n.search+n.hash}catch{return t}}var p=2e4,m=`Declarion embedded screen`,h=`[declarion-embed]`;function g(e){let t=e=>new o(a.invalidOptions,e);if(!e.container||typeof e.container.appendChild!=`function`)return t("`container` must be a DOM element that can receive the iframe.");if(typeof e.declarionOrigin!=`string`||e.declarionOrigin===``)return t('`declarionOrigin` is required and must be the exact origin of the Declarion deployment, e.g. "https://app.example.com".');let n;try{n=new URL(e.declarionOrigin)}catch{return t(`\`declarionOrigin\` is not a valid URL: "${e.declarionOrigin}". Pass an exact origin, e.g. "https://app.example.com".`)}return n.origin===e.declarionOrigin?typeof e.route!=`string`||e.route===``?t("`route` is required and must be a Declarion screen route."):typeof e.getToken==`function`?null:t("`getToken` is required and must be an async function returning { token, expires_at }."):t(`\`declarionOrigin\` must be exactly an origin with no path, query, or trailing slash. Got "${e.declarionOrigin}"; expected "${n.origin}".`)}function _(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.token==`string`&&t.token!==``&&typeof t.expires_at==`string`&&t.expires_at!==``}function v(e){let t=!1,n=null,s=null,c=null,l=!1,u=!1,v=(t,n)=>{e.debug&&(n===void 0?console.info(`${h} ${t}`):console.info(`${h} ${t}`,n))},y=(t,n,r)=>{let i=new o(t,n,r?{cause:r}:void 0);console.error(`${h} ${n}`,r??``),e.onError?.(i)},b=(t,r)=>{if(!n?.contentWindow)return;let i={source:`declarion-embed`,protocol:1,type:t,payload:r};v(`-> iframe ${t}`,r),n.contentWindow.postMessage(i,e.declarionOrigin)},x=async()=>{let n;try{n=await e.getToken()}catch(e){y(a.getTokenFailed,"`getToken` rejected. The host backend must mint a token via auth.create_embed_session and return { token, expires_at }.",e);return}if(!_(n)){y(a.getTokenFailed,"`getToken` resolved to a malformed value. Expected { token: string, expires_at: string }.");return}if(t)return;let i={token:n.token,expires_at:n.expires_at};b(r.setToken,i),O()},S=()=>{l=!0,k(),v(`<- iframe ready`),e.onEvent?.({type:r.ready}),x()},C=()=>{v(`<- iframe token-expired`),e.onEvent?.({type:r.tokenExpired}),x()},w=t=>{v(`<- iframe reload-required`,t),e.onEvent?.({type:r.reloadRequired,payload:t}),y(a.reloadRequired,`The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`)},T=t=>{v(`<- iframe resized`,t),!e.height&&n&&Number.isFinite(t.height)&&t.height>0&&(n.style.height=`${t.height}px`),e.onEvent?.({type:r.resized,payload:t})},E=t=>{v(`<- iframe dirty-changed`,t),e.onEvent?.({type:r.dirtyChanged,payload:t})},D=(t,n)=>{v(`<- iframe ${t}`,n),e.onEvent?.({type:t,payload:n});let i={mode:t===r.navigated?`self`:`delegated`,route:n.route,entity:n.entity,recordId:n.recordId};e.onNavigate?.(i)},O=()=>{u||(u=!0,v(`handshake complete - iframe authenticated`),e.onReady?.())},k=()=>{c!==null&&(clearTimeout(c),c=null)},A=t=>{if(!n||t.source!==n.contentWindow)return;let a=i(t,e.declarionOrigin);if(a.kind===`rejected`)return;if(a.kind===`protocol-mismatch`){console.warn(`${h} protocol version mismatch: this SDK speaks protocol 1, the iframe reported protocol ${String(a.received)}. Update @declarion/embed and the Declarion deployment to matching versions.`);return}let{message:o}=a;switch(o.type){case r.ready:S();break;case r.tokenExpired:C();break;case r.reloadRequired:w(o.payload);break;case r.resized:T(o.payload);break;case r.navigated:D(r.navigated,o.payload);break;case r.navigationRequested:D(r.navigationRequested,o.payload);break;case r.dirtyChanged:E(o.payload);break;default:break}},j=g(e);if(j)return console.error(`${h} ${j.message}`),e.onError?.(j),{navigate:()=>void 0,setTheme:()=>void 0,destroy:()=>void 0};let M=e.navigation??`self`,N=window.location.origin,P;try{P=d({declarionOrigin:e.declarionOrigin,route:e.route,parentOrigin:N,navigation:M,theme:e.theme})}catch(t){return y(a.invalidOptions,`Could not build the iframe URL from route "${e.route}". \`route\` must be a valid Declarion screen route.`,t),{navigate:()=>void 0,setTheme:()=>void 0,destroy:()=>void 0}}return n=document.createElement(`iframe`),n.src=P,n.title=e.title??m,n.style.width=`100%`,n.style.border=`0`,n.style.display=`block`,e.height?n.style.height=e.height:n.style.height||(n.style.height=`150px`),s=A,window.addEventListener(`message`,s),e.container.appendChild(n),v(`iframe created`,{src:P}),c=setTimeout(()=>{c=null,!l&&y(a.handshakeTimeout,`No handshake from the iframe within ${p}ms. Check that \`declarionOrigin\` exactly matches the Declarion deployment origin, that the host origin is allow-listed in the deployment's DECLARION_FRAME_ANCESTORS, and that \`route\` resolves to a real screen.`)},p),{navigate(n){t||b(r.navigate,{route:f(e.declarionOrigin,n)})},setTheme(e){t||b(r.setTheme,{theme:e})},destroy(){t||(t=!0,k(),s&&=(window.removeEventListener(`message`,s),null),n?.parentNode&&n.parentNode.removeChild(n),n=null,v(`embed destroyed`))}}}return e.EMBED_ERROR_CODES=a,e.EMBED_MESSAGE_SOURCE=t,e.EMBED_MESSAGE_TYPES=r,e.EMBED_PROTOCOL_VERSION=n,e.EmbedError=o,e.createDeclarionEmbed=v,e})({});
2
2
  //# sourceMappingURL=declarion-embed.iife.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"declarion-embed.iife.js","names":[],"sources":["../src/protocol.ts","../src/inbound.ts","../src/errors.ts","../src/url.ts","../src/core.ts"],"sourcesContent":["// Embed postMessage protocol - host-side contract.\n//\n// `@declarion/embed` is a SEPARATE npm package from `@declarion/react` and\n// MUST NOT depend on it (a host app must not pull the full Declarion UI SDK\n// to host an iframe). The protocol contract is therefore re-declared here,\n// independently, and MUST match the iframe side EXACTLY:\n// typescript/packages/react/src/embed/protocol.ts\n// A divergence breaks the handshake. Any change to the wire envelope, the\n// message-type set, or a payload shape MUST land in both files together.\n//\n// Wire envelope: { source: \"declarion-embed\", protocol: 1, type, payload }.\n//\n// Naming contract (developer-facing - keep it consistent):\n// - A message the iframe SENDS to the host is an EVENT, named in the past\n// tense - \"this happened\": `resized`, `navigated`, `navigation-requested`,\n// `dirty-changed`, `token-expired`, `reload-required`, `ready`.\n// - A message the host SENDS to the iframe is a COMMAND, named as an\n// imperative - \"do this\": `set-token`, `navigate`, `set-theme`.\n//\n// Security model: the host SDK validates EVERY inbound frame on two axes -\n// exact origin equality against the configured `declarionOrigin`, and the\n// envelope shape (`source` discriminator + numeric `protocol`). A frame that\n// fails either check is dropped SILENTLY; an untrusted page that re-framed\n// the iframe must learn nothing. Outbound host frames target the EXACT\n// `declarionOrigin`, never `\"*\"`.\n\n/**\n * The `source` discriminator stamped on every embed frame. Both the host SDK\n * and the iframe filter inbound traffic on this value so unrelated\n * `postMessage` frames (browser extensions, other widgets) are ignored.\n */\nexport const EMBED_MESSAGE_SOURCE = \"declarion-embed\" as const;\n\n/**\n * The protocol version this SDK speaks. Bumped only on a breaking\n * envelope/payload change. The SDK warns when the iframe reports a different\n * version (see the diagnostics path).\n */\nexport const EMBED_PROTOCOL_VERSION = 1 as const;\n\n/**\n * Every protocol message `type`. The object key is the camelCase name used in\n * code; the value is the on-wire string. Events (iframe -> host) are past\n * tense; commands (host -> iframe) are imperative. This SDK is the HOST side:\n * it RECEIVES the iframe-to-host events and SENDS the host-to-iframe commands.\n */\nexport const EMBED_MESSAGE_TYPES = {\n /** iframe -> host: SDK mounted, requests the first token. */\n ready: \"ready\",\n /** host -> iframe: deliver or refresh the embed token. */\n setToken: \"set-token\",\n /** iframe -> host: token rejected or near expiry. */\n tokenExpired: \"token-expired\",\n /** iframe -> host: the iframe must be reloaded. */\n reloadRequired: \"reload-required\",\n /** iframe -> host: embed content height changed. */\n resized: \"resized\",\n /** iframe -> host: `self` mode internal navigation happened. */\n navigated: \"navigated\",\n /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */\n navigationRequested: \"navigation-requested\",\n /** iframe -> host: the embedded screen's unsaved-edits state flipped. */\n dirtyChanged: \"dirty-changed\",\n /** host -> iframe: drive the iframe to a screen (deep-linking). */\n navigate: \"navigate\",\n /** host -> iframe: runtime theme switch. */\n setTheme: \"set-theme\",\n} as const;\n\n/** The union of all on-wire message `type` strings. */\nexport type EmbedMessageType =\n (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];\n\n// --- Payload interfaces, one per message type ---\n\n/** `ready` payload: empty - the frame itself is the signal. */\nexport type EmbedReadyPayload = Record<string, never>;\n\n/** `set-token` payload: the embed token and its absolute expiry. */\nexport interface EmbedSetTokenPayload {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** RFC 3339 expiry timestamp of the token. */\n readonly expires_at: string;\n}\n\n/** `token-expired` payload: empty - the host re-mints via `getToken`. */\nexport type EmbedTokenExpiredPayload = Record<string, never>;\n\n/** `reload-required` payload: a human-readable reason for the reload. */\nexport interface EmbedReloadRequiredPayload {\n /** Why the iframe must be reloaded (asset drift, terminal auth failure). */\n readonly reason: string;\n}\n\n/** `resized` payload: the measured content height in CSS pixels. */\nexport interface EmbedResizedPayload {\n /** Content height in CSS pixels. */\n readonly height: number;\n}\n\n/**\n * `navigated` / `navigation-requested` payload: the screen route and, when\n * resolvable, the bound entity code and record id.\n *\n * `entity` and `recordId` are optional: a `custom` screen has no entity, and\n * a list route has no record id.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /** The bound entity code, when the route resolves to one. */\n readonly entity?: string;\n /** The record id, when the route is a detail route with an id. */\n readonly recordId?: string;\n}\n\n/**\n * `dirty-changed` payload: whether the embedded screen currently has unsaved\n * edits. The host tracks this so it can guard its own navigation (its menu,\n * a host-initiated `navigate`) before moving the iframe away from a dirty\n * screen - the iframe never pops a dialog for host-driven navigation.\n */\nexport interface EmbedDirtyChangedPayload {\n /** True when the embedded screen has unsaved edits. */\n readonly dirty: boolean;\n}\n\n/** `navigate` payload: the route the host drives the iframe to. */\nexport interface EmbedNavigatePayload {\n /** The Declarion screen route the host wants opened. */\n readonly route: string;\n}\n\n/** `set-theme` payload: the theme the host switches the iframe to. */\nexport interface EmbedSetThemePayload {\n /** The requested theme. */\n readonly theme: EmbedTheme;\n}\n\n/** The theme hint carried on the `theme` URL param and `set-theme` frame. */\nexport type EmbedTheme = \"light\" | \"dark\";\n\n/**\n * Maps each message type to its payload shape. Used to type the inbound\n * classifier and the outbound sender so the payload is checked against the\n * message type.\n */\nexport interface EmbedMessagePayloadMap {\n [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;\n [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;\n [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;\n [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;\n [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;\n [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;\n [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;\n [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;\n}\n\n/**\n * The wire envelope. Every embed frame is exactly this shape: a fixed\n * `source` + `protocol` pair, a discriminating `type`, and the typed\n * `payload` for that type.\n */\nexport interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {\n readonly source: typeof EMBED_MESSAGE_SOURCE;\n readonly protocol: typeof EMBED_PROTOCOL_VERSION;\n readonly type: T;\n readonly payload: EmbedMessagePayloadMap[T];\n}\n","// Inbound `postMessage` validation - host side.\n//\n// The host SDK receives the iframe-to-parent frames. EVERY inbound frame is\n// validated on two axes before it is acted on:\n// 1. `event.origin === declarionOrigin` - exact string equality, no\n// prefix, no wildcard. A frame from any other origin is dropped.\n// 2. the envelope shape - `source === \"declarion-embed\"` and a numeric\n// `protocol`.\n// A frame failing either check is dropped SILENTLY (security): an untrusted\n// page that re-framed the iframe must learn nothing.\n//\n// A frame that passes origin + source but carries a DIFFERENT `protocol`\n// version is classified `protocol-mismatch`: that frame IS the trusted\n// iframe, just on a mismatched SDK version. The caller surfaces it as a\n// clear console warning naming both versions, never a silent drop.\n//\n// This mirrors the iframe-side classifier:\n// typescript/packages/react/src/embed/handshake.ts\n\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n type EmbedMessage,\n} from \"./protocol\";\n\n/**\n * The classification of an inbound `message` event after validation.\n *\n * - `valid`: a trusted, shape-correct, version-matched embed frame.\n * - `protocol-mismatch`: origin + source are trusted, but the `protocol`\n * version differs. The caller logs a warning; the frame is not acted on.\n * - `rejected`: not a trusted embed frame (wrong origin, missing/foreign\n * `source`, malformed envelope). The caller drops it silently.\n */\nexport type InboundClassification =\n | { kind: \"valid\"; message: EmbedMessage }\n | { kind: \"protocol-mismatch\"; received: unknown }\n | { kind: \"rejected\" };\n\n/**\n * Validate and classify an inbound `message` event against the trusted\n * `declarionOrigin`.\n *\n * Returns `rejected` for anything that is not a trusted embed frame - the\n * caller drops those with no logging. Returns `protocol-mismatch` when the\n * frame is trusted but on a different protocol version. Returns `valid` with\n * the typed envelope otherwise.\n */\nexport function classifyInboundMessage(\n event: MessageEvent,\n declarionOrigin: string,\n): InboundClassification {\n // Axis 1: exact origin match. A trailing slash, a subdomain, a different\n // port - all fail this equality and the frame is dropped.\n if (event.origin !== declarionOrigin) {\n return { kind: \"rejected\" };\n }\n\n // Axis 2: envelope shape. The data must be an object carrying our\n // `source` discriminator and a numeric `protocol`.\n const data = event.data as unknown;\n if (typeof data !== \"object\" || data === null) {\n return { kind: \"rejected\" };\n }\n const envelope = data as Partial<EmbedMessage>;\n if (envelope.source !== EMBED_MESSAGE_SOURCE) {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.protocol !== \"number\") {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.type !== \"string\") {\n return { kind: \"rejected\" };\n }\n\n // Trusted iframe, our envelope, but a different protocol version. Surface\n // it loudly - this is the iframe on a mismatched version, not an attacker.\n if (envelope.protocol !== EMBED_PROTOCOL_VERSION) {\n return { kind: \"protocol-mismatch\", received: envelope.protocol };\n }\n\n return { kind: \"valid\", message: envelope as EmbedMessage };\n}\n","// Typed, actionable embed diagnostics.\n//\n// A silently blank iframe is the worst embedding failure. This module is the\n// single definition of every developer-facing error the SDK can raise. Each\n// error carries a stable `code` (for programmatic handling) and a human\n// message written to be ACTIONABLE - it names the option to fix and, where\n// relevant, the deployment config.\n//\n// Two failure classes, opposite requirements (Decision 21):\n// - Untrusted cross-origin `postMessage` frames: dropped SILENTLY. They are\n// a security concern; never surfaced. The SDK does this in the inbound\n// classifier and never constructs an EmbedError for them.\n// - Developer misconfiguration: surfaced LOUDLY via `onError` AND\n// `console.error`. Every such case is one of the codes below.\n\n/**\n * Stable, machine-readable embed error codes. A host may branch on\n * `error.code`; the strings are part of the public contract.\n */\nexport const EMBED_ERROR_CODES = {\n /**\n * A required `createDeclarionEmbed` option is missing or malformed\n * (`container`, `declarionOrigin`, `route`, `getToken`). Raised\n * synchronously before the iframe is created.\n */\n invalidOptions: \"invalid-options\",\n /**\n * The host's `getToken` callback rejected, threw, or resolved to a value\n * that is not `{ token: string, expires_at: string }`.\n */\n getTokenFailed: \"get-token-failed\",\n /**\n * No `ready` frame arrived from the iframe within the post-mount timeout.\n * The usual causes are a `declarionOrigin` mismatch (the iframe loaded a\n * different origin, or never loaded) or framing denied by the Declarion CSP\n * (the host origin is not in `DECLARION_FRAME_ANCESTORS`). A slow `getToken`\n * does NOT cause this - the timer is cleared as soon as `ready` arrives.\n */\n handshakeTimeout: \"handshake-timeout\",\n /**\n * The iframe asked the host to reload it (`reload-required`) - asset drift\n * or a terminal auth failure inside the iframe.\n */\n reloadRequired: \"reload-required\",\n} as const;\n\n/** The union of all embed error code strings. */\nexport type EmbedErrorCode =\n (typeof EMBED_ERROR_CODES)[keyof typeof EMBED_ERROR_CODES];\n\n/**\n * A typed embed error. Always passed to the host `onError` callback and\n * always also written to `console.error` with the `[declarion-embed]`\n * prefix, so a misconfiguration is never silent.\n *\n * `cause` carries the originating error when one exists (e.g. the rejection\n * value from `getToken`), preserving the stack for debugging.\n */\nexport class EmbedError extends Error {\n /** The stable, machine-readable error code. */\n readonly code: EmbedErrorCode;\n\n constructor(code: EmbedErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"EmbedError\";\n this.code = code;\n // Restore the prototype chain: `extends Error` across the ES5 transpile\n // target otherwise breaks `instanceof EmbedError`.\n Object.setPrototypeOf(this, EmbedError.prototype);\n }\n}\n","// Embed iframe `src` construction.\n//\n// The host SDK builds the iframe URL the Declarion deployment parses. The\n// param grammar MUST match the iframe-side parser EXACTLY:\n// typescript/packages/react/src/embed/params.ts\n// The iframe reads `embed=1`, `parent_origin`, `theme`, `nav`; any drift\n// here silently changes how the iframe boots.\n\nimport type { EmbedNavigationContract, EmbedTheme } from \"./types\";\n\n/** Query-param names that make up the embed URL contract. */\nexport const EMBED_PARAM_EMBED = \"embed\";\nexport const EMBED_PARAM_PARENT_ORIGIN = \"parent_origin\";\nexport const EMBED_PARAM_THEME = \"theme\";\nexport const EMBED_PARAM_NAV = \"nav\";\n\n/** The on-wire value of `embed` that enables shellless render. */\nconst EMBED_PARAM_EMBED_ENABLED = \"1\";\n\n/** Default navigation contract when the host does not set `navigation`. */\nexport const DEFAULT_NAVIGATION_CONTRACT: EmbedNavigationContract = \"self\";\n\n/** Inputs needed to build the iframe `src`. */\nexport interface BuildEmbedSrcInput {\n /** The Declarion deployment origin (`https://app.example.com`). */\n readonly declarionOrigin: string;\n /** The Declarion screen route to embed. */\n readonly route: string;\n /**\n * The host page's own origin. Becomes `parent_origin` so the iframe knows\n * exactly which origin may exchange `postMessage` frames with it.\n */\n readonly parentOrigin: string;\n /** Navigation contract; becomes the `nav` param. */\n readonly navigation: EmbedNavigationContract;\n /** Optional initial theme; becomes the `theme` param when set. */\n readonly theme?: EmbedTheme;\n}\n\n/**\n * Build the absolute iframe `src` URL for an embedded Declarion screen.\n *\n * The `route` is resolved as a path against `declarionOrigin`; the four\n * embed params are appended. `parent_origin` is the host's own origin so the\n * iframe restricts its `postMessage` traffic to exactly that origin.\n *\n * Throws when `declarionOrigin` is not a parseable absolute origin - callers\n * convert that into a typed `EmbedError` before raising it to the host.\n */\nexport function buildEmbedSrc(input: BuildEmbedSrcInput): string {\n // `route` may be a bare path (`/cases`) or already carry a query/hash.\n // Resolving it against the origin keeps any route-level query intact while\n // the embed params are layered on top.\n const url = new URL(input.route, input.declarionOrigin);\n url.searchParams.set(EMBED_PARAM_EMBED, EMBED_PARAM_EMBED_ENABLED);\n url.searchParams.set(EMBED_PARAM_PARENT_ORIGIN, input.parentOrigin);\n url.searchParams.set(EMBED_PARAM_NAV, input.navigation);\n if (input.theme) {\n url.searchParams.set(EMBED_PARAM_THEME, input.theme);\n }\n return url.toString();\n}\n\n/**\n * Resolve a Declarion screen route to an absolute URL for a runtime\n * `navigate` frame. The host passes a route string; the iframe consumes the\n * route as-is, so this only normalizes it against the deployment origin for\n * the host's own bookkeeping. Returns the input unchanged when it cannot be\n * resolved (the iframe tolerates a relative route).\n */\nexport function resolveRoute(declarionOrigin: string, route: string): string {\n try {\n const url = new URL(route, declarionOrigin);\n // Keep the hash so a host can deep-link to an in-page anchor; dropping it\n // would silently break `handle.navigate(\"/cases/42#notes\")`.\n return url.pathname + url.search + url.hash;\n } catch {\n return route;\n }\n}\n","// `createDeclarionEmbed` - the framework-agnostic, dependency-free embed core.\n//\n// Builds the iframe, runs the `ready` -> `auth` handshake, owns token refresh\n// through the host `getToken` callback, auto-applies `resize`, mirrors\n// navigation, and surfaces misconfiguration loudly. The React binding\n// (`./react`) and the demo host both build on this single core.\n\nimport {\n EMBED_MESSAGE_TYPES,\n EMBED_PROTOCOL_VERSION,\n type EmbedSetTokenPayload,\n type EmbedMessage,\n type EmbedMessagePayloadMap,\n type EmbedMessageType,\n type EmbedDirtyChangedPayload,\n type EmbedNavigationPayload,\n type EmbedReloadRequiredPayload,\n type EmbedResizedPayload,\n} from \"./protocol\";\nimport { classifyInboundMessage } from \"./inbound\";\nimport {\n EMBED_ERROR_CODES,\n EmbedError,\n type EmbedErrorCode,\n} from \"./errors\";\nimport {\n DEFAULT_NAVIGATION_CONTRACT,\n buildEmbedSrc,\n resolveRoute,\n} from \"./url\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n EmbedNavigateEvent,\n EmbedToken,\n} from \"./types\";\n\n/**\n * How long the SDK waits for the first `auth` frame to be requested by the\n * iframe after it loads. The iframe emits `ready`; if no `ready` arrives -\n * usually a `declarionOrigin` mismatch or framing denied by the Declarion\n * CSP - the SDK raises a `handshake-timeout` error rather than leave a\n * silently blank iframe.\n */\nconst HANDSHAKE_TIMEOUT_MS = 20_000;\n\n/** Default iframe `title` when the host does not supply one. */\nconst DEFAULT_IFRAME_TITLE = \"Declarion embedded screen\";\n\n/** Console prefix for every SDK diagnostic line. */\nconst LOG_PREFIX = \"[declarion-embed]\";\n\n/**\n * Validate `createDeclarionEmbed` options. Returns a typed `EmbedError` for\n * the first problem found, or `null` when the options are well-formed.\n *\n * Separated from `createDeclarionEmbed` so the React binding can reuse the\n * exact same validation.\n */\nfunction validateOptions(options: DeclarionEmbedOptions): EmbedError | null {\n const fail = (message: string): EmbedError =>\n new EmbedError(EMBED_ERROR_CODES.invalidOptions, message);\n\n if (!options.container || typeof options.container.appendChild !== \"function\") {\n return fail(\n \"`container` must be a DOM element that can receive the iframe.\",\n );\n }\n if (typeof options.declarionOrigin !== \"string\" || options.declarionOrigin === \"\") {\n return fail(\n \"`declarionOrigin` is required and must be the exact origin of the \" +\n \"Declarion deployment, e.g. \\\"https://app.example.com\\\".\",\n );\n }\n let parsedOrigin: URL;\n try {\n parsedOrigin = new URL(options.declarionOrigin);\n } catch {\n return fail(\n `\\`declarionOrigin\\` is not a valid URL: \"${options.declarionOrigin}\". ` +\n 'Pass an exact origin, e.g. \"https://app.example.com\".',\n );\n }\n if (parsedOrigin.origin !== options.declarionOrigin) {\n return fail(\n `\\`declarionOrigin\\` must be exactly an origin with no path, query, ` +\n `or trailing slash. Got \"${options.declarionOrigin}\"; expected ` +\n `\"${parsedOrigin.origin}\".`,\n );\n }\n if (typeof options.route !== \"string\" || options.route === \"\") {\n return fail(\"`route` is required and must be a Declarion screen route.\");\n }\n if (typeof options.getToken !== \"function\") {\n return fail(\n \"`getToken` is required and must be an async function returning \" +\n \"{ token, expires_at }.\",\n );\n }\n return null;\n}\n\n/**\n * Validate the value a host `getToken` callback resolved to. The host owns\n * this code; a malformed return is a developer mistake, surfaced loudly.\n */\nfunction isValidToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Create an embedded Declarion screen inside `options.container`.\n *\n * Returns a `DeclarionEmbedHandle` exposing `navigate`, `setTheme`, and\n * `destroy`. On a misconfiguration the SDK still returns a handle (so\n * `destroy` is always callable) but reports the problem through `onError`\n * and `console.error`; the iframe is not created in that case.\n */\nexport function createDeclarionEmbed(\n options: DeclarionEmbedOptions,\n): DeclarionEmbedHandle {\n let destroyed = false;\n let iframe: HTMLIFrameElement | null = null;\n let messageListener: ((event: MessageEvent) => void) | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let readyReceived = false;\n let firstAuthDelivered = false;\n\n /** Emit a debug line when `debug` is on. */\n const logDebug = (message: string, detail?: unknown): void => {\n if (!options.debug) return;\n if (detail === undefined) {\n console.info(`${LOG_PREFIX} ${message}`);\n } else {\n console.info(`${LOG_PREFIX} ${message}`, detail);\n }\n };\n\n /**\n * Report a developer-facing error. Always written to `console.error` AND\n * handed to `onError`, so a misconfiguration is never silent (Decision 21).\n */\n const reportError = (\n code: EmbedErrorCode,\n message: string,\n cause?: unknown,\n ): void => {\n const error = new EmbedError(code, message, cause ? { cause } : undefined);\n console.error(`${LOG_PREFIX} ${message}`, cause ?? \"\");\n options.onError?.(error);\n };\n\n /** Post an enveloped frame to the iframe, targeting the exact origin. */\n const postToIframe = <T extends EmbedMessageType>(\n type: T,\n payload: EmbedMessagePayloadMap[T],\n ): void => {\n if (!iframe?.contentWindow) return;\n const message: EmbedMessage<T> = {\n source: \"declarion-embed\",\n protocol: EMBED_PROTOCOL_VERSION,\n type,\n payload,\n };\n logDebug(`-> iframe ${type}`, payload);\n iframe.contentWindow.postMessage(message, options.declarionOrigin);\n };\n\n /**\n * Call the host `getToken` callback and deliver the token to the iframe.\n * Surfaces a `get-token-failed` error when the callback rejects, throws,\n * or resolves to a malformed value.\n */\n const requestAndDeliverToken = async (): Promise<void> => {\n let result: unknown;\n try {\n result = await options.getToken();\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` rejected. The host backend must mint a token via \" +\n \"auth.create_embed_session and return { token, expires_at }.\",\n cause,\n );\n return;\n }\n if (!isValidToken(result)) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` resolved to a malformed value. Expected \" +\n \"{ token: string, expires_at: string }.\",\n );\n return;\n }\n if (destroyed) return;\n const payload: EmbedSetTokenPayload = {\n token: result.token,\n expires_at: result.expires_at,\n };\n postToIframe(EMBED_MESSAGE_TYPES.setToken, payload);\n onAuthDelivered();\n };\n\n /** Handle the iframe `ready` frame: the iframe requests its first token. */\n const onReady = (): void => {\n readyReceived = true;\n clearHandshakeTimer();\n logDebug(\"<- iframe ready\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.ready });\n void requestAndDeliverToken();\n };\n\n /** Handle a `token-expired` frame: re-run `getToken` and deliver again. */\n const onTokenExpired = (): void => {\n logDebug(\"<- iframe token-expired\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.tokenExpired });\n void requestAndDeliverToken();\n };\n\n /** Handle a `reload-required` frame: surface it and notify the host. */\n const onReloadRequired = (payload: EmbedReloadRequiredPayload): void => {\n logDebug(\"<- iframe reload-required\", payload);\n options.onEvent?.({\n type: EMBED_MESSAGE_TYPES.reloadRequired,\n payload,\n });\n reportError(\n EMBED_ERROR_CODES.reloadRequired,\n `The embedded iframe requested a reload: ${payload.reason}. ` +\n \"Reload the iframe to recover.\",\n );\n };\n\n /** Handle a `resized` frame: auto-apply the reported height to the iframe. */\n const onResize = (payload: EmbedResizedPayload): void => {\n logDebug(\"<- iframe resized\", payload);\n if (iframe && Number.isFinite(payload.height) && payload.height > 0) {\n iframe.style.height = `${payload.height}px`;\n }\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.resized, payload });\n };\n\n /**\n * Handle a `dirty-changed` frame: surface the embedded screen's\n * unsaved-edits state. The SDK does not act on it; the host tracks it and\n * guards its own navigation (its menu, a host-initiated `navigate`) before\n * moving the iframe off a dirty screen.\n */\n const onDirtyChanged = (payload: EmbedDirtyChangedPayload): void => {\n logDebug(\"<- iframe dirty-changed\", payload);\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.dirtyChanged, payload });\n };\n\n /**\n * Handle a navigation frame. `navigated` (the iframe moved, `self` mode)\n * and `navigation-requested` (the iframe stayed, `delegated` mode) both\n * surface through `onNavigate`; `mode` records which.\n */\n const onNavigationFrame = (\n type:\n | typeof EMBED_MESSAGE_TYPES.navigated\n | typeof EMBED_MESSAGE_TYPES.navigationRequested,\n payload: EmbedNavigationPayload,\n ): void => {\n logDebug(`<- iframe ${type}`, payload);\n options.onEvent?.({ type, payload });\n const event: EmbedNavigateEvent = {\n mode: type === EMBED_MESSAGE_TYPES.navigated ? \"self\" : \"delegated\",\n route: payload.route,\n entity: payload.entity,\n recordId: payload.recordId,\n };\n options.onNavigate?.(event);\n };\n\n /**\n * Mark the embed ready once the first token is delivered to the iframe.\n * The SDK has no separate \"screen rendered\" signal; the first successful\n * `set-token` delivery after `ready` is the earliest reliable point the\n * host can treat the embedded screen as authenticated.\n */\n const onAuthDelivered = (): void => {\n if (!firstAuthDelivered) {\n firstAuthDelivered = true;\n logDebug(\"handshake complete - iframe authenticated\");\n options.onReady?.();\n }\n };\n\n /** Stop the post-load handshake timeout. */\n const clearHandshakeTimer = (): void => {\n if (handshakeTimer !== null) {\n clearTimeout(handshakeTimer);\n handshakeTimer = null;\n }\n };\n\n /**\n * The single `message` listener. Validates every frame via\n * `classifyInboundMessage`: untrusted frames are dropped silently, a\n * protocol mismatch warns loudly, a valid frame is dispatched.\n */\n const handleMessage = (event: MessageEvent): void => {\n const classified = classifyInboundMessage(event, options.declarionOrigin);\n\n if (classified.kind === \"rejected\") {\n // Untrusted or malformed frame. Dropped silently (security): a page\n // that re-framed the iframe must learn nothing from the host logs.\n return;\n }\n\n if (classified.kind === \"protocol-mismatch\") {\n // The trusted iframe on a mismatched protocol version. Warn loudly,\n // naming both versions - a silent drop here is a blank iframe.\n console.warn(\n `${LOG_PREFIX} protocol version mismatch: this SDK speaks protocol ` +\n `${EMBED_PROTOCOL_VERSION}, the iframe reported protocol ` +\n `${String(classified.received)}. Update @declarion/embed and the ` +\n \"Declarion deployment to matching versions.\",\n );\n return;\n }\n\n const { message } = classified;\n switch (message.type as EmbedMessageType) {\n case EMBED_MESSAGE_TYPES.ready:\n onReady();\n break;\n case EMBED_MESSAGE_TYPES.tokenExpired:\n onTokenExpired();\n break;\n case EMBED_MESSAGE_TYPES.reloadRequired:\n onReloadRequired(message.payload as EmbedReloadRequiredPayload);\n break;\n case EMBED_MESSAGE_TYPES.resized:\n onResize(message.payload as EmbedResizedPayload);\n break;\n case EMBED_MESSAGE_TYPES.navigated:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigated,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.navigationRequested:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigationRequested,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.dirtyChanged:\n onDirtyChanged(message.payload as EmbedDirtyChangedPayload);\n break;\n default:\n // `set-token`, `navigate`, `set-theme` are host -> iframe; the iframe\n // does not send them back. Any other type is not consumed. Ignore.\n break;\n }\n };\n\n // --- Construction ------------------------------------------------------\n\n const optionsError = validateOptions(options);\n if (optionsError) {\n console.error(`${LOG_PREFIX} ${optionsError.message}`);\n options.onError?.(optionsError);\n // Return an inert handle: the iframe was never created, so navigate /\n // setTheme are no-ops and destroy has nothing to tear down.\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n const navigation = options.navigation ?? DEFAULT_NAVIGATION_CONTRACT;\n // The host's own origin. The iframe restricts every `postMessage` it sends\n // to exactly this origin.\n const parentOrigin = window.location.origin;\n\n let src: string;\n try {\n src = buildEmbedSrc({\n declarionOrigin: options.declarionOrigin,\n route: options.route,\n parentOrigin,\n navigation,\n theme: options.theme,\n });\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.invalidOptions,\n `Could not build the iframe URL from route \"${options.route}\". ` +\n \"`route` must be a valid Declarion screen route.\",\n cause,\n );\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n iframe = document.createElement(\"iframe\");\n iframe.src = src;\n iframe.title = options.title ?? DEFAULT_IFRAME_TITLE;\n // The screen sizes itself; the host receives `resize` frames and the SDK\n // applies the height. A sensible non-zero starting height avoids a\n // zero-height flash before the first `resize` arrives.\n iframe.style.width = \"100%\";\n iframe.style.border = \"0\";\n iframe.style.display = \"block\";\n if (!iframe.style.height) {\n iframe.style.height = \"150px\";\n }\n\n messageListener = handleMessage;\n window.addEventListener(\"message\", messageListener);\n options.container.appendChild(iframe);\n logDebug(\"iframe created\", { src });\n\n // Bound the wait for the iframe's `ready` frame. No `ready` means the\n // iframe never loaded the embed runtime: a `declarionOrigin` mismatch,\n // framing denied by the Declarion CSP, or an unreachable route.\n handshakeTimer = setTimeout(() => {\n handshakeTimer = null;\n if (readyReceived) return;\n reportError(\n EMBED_ERROR_CODES.handshakeTimeout,\n `No handshake from the iframe within ${HANDSHAKE_TIMEOUT_MS}ms. ` +\n \"Check that `declarionOrigin` exactly matches the Declarion \" +\n \"deployment origin, that the host origin is allow-listed in the \" +\n \"deployment's DECLARION_FRAME_ANCESTORS, and that `route` resolves \" +\n \"to a real screen.\",\n );\n }, HANDSHAKE_TIMEOUT_MS);\n\n return {\n navigate(route: string): void {\n if (destroyed) return;\n // Drive the iframe to a screen route (deep-linking, both nav modes).\n postToIframe(EMBED_MESSAGE_TYPES.navigate, {\n route: resolveRoute(options.declarionOrigin, route),\n });\n },\n setTheme(theme): void {\n if (destroyed) return;\n postToIframe(EMBED_MESSAGE_TYPES.setTheme, { theme });\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearHandshakeTimer();\n if (messageListener) {\n window.removeEventListener(\"message\", messageListener);\n messageListener = null;\n }\n if (iframe?.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n iframe = null;\n logDebug(\"embed destroyed\");\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"declarion-embed.iife.js","names":[],"sources":["../src/protocol.ts","../src/inbound.ts","../src/errors.ts","../src/url.ts","../src/core.ts"],"sourcesContent":["// Embed postMessage protocol - host-side contract.\n//\n// `@declarion/embed` is a SEPARATE npm package from `@declarion/react` and\n// MUST NOT depend on it (a host app must not pull the full Declarion UI SDK\n// to host an iframe). The protocol contract is therefore re-declared here,\n// independently, and MUST match the iframe side EXACTLY:\n// typescript/packages/react/src/embed/protocol.ts\n// A divergence breaks the handshake. Any change to the wire envelope, the\n// message-type set, or a payload shape MUST land in both files together.\n//\n// Wire envelope: { source: \"declarion-embed\", protocol: 1, type, payload }.\n//\n// Naming contract (developer-facing - keep it consistent):\n// - A message the iframe SENDS to the host is an EVENT, named in the past\n// tense - \"this happened\": `resized`, `navigated`, `navigation-requested`,\n// `dirty-changed`, `token-expired`, `reload-required`, `ready`.\n// - A message the host SENDS to the iframe is a COMMAND, named as an\n// imperative - \"do this\": `set-token`, `navigate`, `set-theme`.\n//\n// Security model: the host SDK validates EVERY inbound frame on two axes -\n// exact origin equality against the configured `declarionOrigin`, and the\n// envelope shape (`source` discriminator + numeric `protocol`). A frame that\n// fails either check is dropped SILENTLY; an untrusted page that re-framed\n// the iframe must learn nothing. Outbound host frames target the EXACT\n// `declarionOrigin`, never `\"*\"`.\n\n/**\n * The `source` discriminator stamped on every embed frame. Both the host SDK\n * and the iframe filter inbound traffic on this value so unrelated\n * `postMessage` frames (browser extensions, other widgets) are ignored.\n */\nexport const EMBED_MESSAGE_SOURCE = \"declarion-embed\" as const;\n\n/**\n * The protocol version this SDK speaks. Bumped only on a breaking\n * envelope/payload change. The SDK warns when the iframe reports a different\n * version (see the diagnostics path).\n */\nexport const EMBED_PROTOCOL_VERSION = 1 as const;\n\n/**\n * Every protocol message `type`. The object key is the camelCase name used in\n * code; the value is the on-wire string. Events (iframe -> host) are past\n * tense; commands (host -> iframe) are imperative. This SDK is the HOST side:\n * it RECEIVES the iframe-to-host events and SENDS the host-to-iframe commands.\n */\nexport const EMBED_MESSAGE_TYPES = {\n /** iframe -> host: SDK mounted, requests the first token. */\n ready: \"ready\",\n /** host -> iframe: deliver or refresh the embed token. */\n setToken: \"set-token\",\n /** iframe -> host: token rejected or near expiry. */\n tokenExpired: \"token-expired\",\n /** iframe -> host: the iframe must be reloaded. */\n reloadRequired: \"reload-required\",\n /** iframe -> host: embed content height changed. */\n resized: \"resized\",\n /** iframe -> host: `self` mode internal navigation happened. */\n navigated: \"navigated\",\n /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */\n navigationRequested: \"navigation-requested\",\n /** iframe -> host: the embedded screen's unsaved-edits state flipped. */\n dirtyChanged: \"dirty-changed\",\n /** host -> iframe: drive the iframe to a screen (deep-linking). */\n navigate: \"navigate\",\n /** host -> iframe: runtime theme switch. */\n setTheme: \"set-theme\",\n} as const;\n\n/** The union of all on-wire message `type` strings. */\nexport type EmbedMessageType =\n (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];\n\n// --- Payload interfaces, one per message type ---\n\n/** `ready` payload: empty - the frame itself is the signal. */\nexport type EmbedReadyPayload = Record<string, never>;\n\n/** `set-token` payload: the embed token and its absolute expiry. */\nexport interface EmbedSetTokenPayload {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** RFC 3339 expiry timestamp of the token. */\n readonly expires_at: string;\n}\n\n/** `token-expired` payload: empty - the host re-mints via `getToken`. */\nexport type EmbedTokenExpiredPayload = Record<string, never>;\n\n/** `reload-required` payload: a human-readable reason for the reload. */\nexport interface EmbedReloadRequiredPayload {\n /** Why the iframe must be reloaded (asset drift, terminal auth failure). */\n readonly reason: string;\n}\n\n/** `resized` payload: the measured content height in CSS pixels. */\nexport interface EmbedResizedPayload {\n /** Content height in CSS pixels. */\n readonly height: number;\n}\n\n/**\n * `navigated` / `navigation-requested` payload: the screen route and, when\n * resolvable, the bound entity code and record id.\n *\n * `entity` and `recordId` are optional: a `custom` screen has no entity, and\n * a list route has no record id.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /** The bound entity code, when the route resolves to one. */\n readonly entity?: string;\n /** The record id, when the route is a detail route with an id. */\n readonly recordId?: string;\n}\n\n/**\n * `dirty-changed` payload: whether the embedded screen currently has unsaved\n * edits. The host tracks this so it can guard its own navigation (its menu,\n * a host-initiated `navigate`) before moving the iframe away from a dirty\n * screen - the iframe never pops a dialog for host-driven navigation.\n */\nexport interface EmbedDirtyChangedPayload {\n /** True when the embedded screen has unsaved edits. */\n readonly dirty: boolean;\n}\n\n/** `navigate` payload: the route the host drives the iframe to. */\nexport interface EmbedNavigatePayload {\n /** The Declarion screen route the host wants opened. */\n readonly route: string;\n}\n\n/** `set-theme` payload: the theme the host switches the iframe to. */\nexport interface EmbedSetThemePayload {\n /** The requested theme. */\n readonly theme: EmbedTheme;\n}\n\n/** The theme hint carried on the `theme` URL param and `set-theme` frame. */\nexport type EmbedTheme = \"light\" | \"dark\";\n\n/**\n * Maps each message type to its payload shape. Used to type the inbound\n * classifier and the outbound sender so the payload is checked against the\n * message type.\n */\nexport interface EmbedMessagePayloadMap {\n [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;\n [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;\n [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;\n [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;\n [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;\n [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;\n [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;\n [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;\n}\n\n/**\n * The wire envelope. Every embed frame is exactly this shape: a fixed\n * `source` + `protocol` pair, a discriminating `type`, and the typed\n * `payload` for that type.\n */\nexport interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {\n readonly source: typeof EMBED_MESSAGE_SOURCE;\n readonly protocol: typeof EMBED_PROTOCOL_VERSION;\n readonly type: T;\n readonly payload: EmbedMessagePayloadMap[T];\n}\n","// Inbound `postMessage` validation - host side.\n//\n// The host SDK receives the iframe-to-parent frames. EVERY inbound frame is\n// validated on two axes before it is acted on:\n// 1. `event.origin === declarionOrigin` - exact string equality, no\n// prefix, no wildcard. A frame from any other origin is dropped.\n// 2. the envelope shape - `source === \"declarion-embed\"` and a numeric\n// `protocol`.\n// A frame failing either check is dropped SILENTLY (security): an untrusted\n// page that re-framed the iframe must learn nothing.\n//\n// A frame that passes origin + source but carries a DIFFERENT `protocol`\n// version is classified `protocol-mismatch`: that frame IS the trusted\n// iframe, just on a mismatched SDK version. The caller surfaces it as a\n// clear console warning naming both versions, never a silent drop.\n//\n// This mirrors the iframe-side classifier:\n// typescript/packages/react/src/embed/handshake.ts\n\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n type EmbedMessage,\n} from \"./protocol\";\n\n/**\n * The classification of an inbound `message` event after validation.\n *\n * - `valid`: a trusted, shape-correct, version-matched embed frame.\n * - `protocol-mismatch`: origin + source are trusted, but the `protocol`\n * version differs. The caller logs a warning; the frame is not acted on.\n * - `rejected`: not a trusted embed frame (wrong origin, missing/foreign\n * `source`, malformed envelope). The caller drops it silently.\n */\nexport type InboundClassification =\n | { kind: \"valid\"; message: EmbedMessage }\n | { kind: \"protocol-mismatch\"; received: unknown }\n | { kind: \"rejected\" };\n\n/**\n * Validate and classify an inbound `message` event against the trusted\n * `declarionOrigin`.\n *\n * Returns `rejected` for anything that is not a trusted embed frame - the\n * caller drops those with no logging. Returns `protocol-mismatch` when the\n * frame is trusted but on a different protocol version. Returns `valid` with\n * the typed envelope otherwise.\n */\nexport function classifyInboundMessage(\n event: MessageEvent,\n declarionOrigin: string,\n): InboundClassification {\n // Axis 1: exact origin match. A trailing slash, a subdomain, a different\n // port - all fail this equality and the frame is dropped.\n if (event.origin !== declarionOrigin) {\n return { kind: \"rejected\" };\n }\n\n // Axis 2: envelope shape. The data must be an object carrying our\n // `source` discriminator and a numeric `protocol`.\n const data = event.data as unknown;\n if (typeof data !== \"object\" || data === null) {\n return { kind: \"rejected\" };\n }\n const envelope = data as Partial<EmbedMessage>;\n if (envelope.source !== EMBED_MESSAGE_SOURCE) {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.protocol !== \"number\") {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.type !== \"string\") {\n return { kind: \"rejected\" };\n }\n\n // Trusted iframe, our envelope, but a different protocol version. Surface\n // it loudly - this is the iframe on a mismatched version, not an attacker.\n if (envelope.protocol !== EMBED_PROTOCOL_VERSION) {\n return { kind: \"protocol-mismatch\", received: envelope.protocol };\n }\n\n return { kind: \"valid\", message: envelope as EmbedMessage };\n}\n","// Typed, actionable embed diagnostics.\n//\n// A silently blank iframe is the worst embedding failure. This module is the\n// single definition of every developer-facing error the SDK can raise. Each\n// error carries a stable `code` (for programmatic handling) and a human\n// message written to be ACTIONABLE - it names the option to fix and, where\n// relevant, the deployment config.\n//\n// Two failure classes, opposite requirements (Decision 21):\n// - Untrusted cross-origin `postMessage` frames: dropped SILENTLY. They are\n// a security concern; never surfaced. The SDK does this in the inbound\n// classifier and never constructs an EmbedError for them.\n// - Developer misconfiguration: surfaced LOUDLY via `onError` AND\n// `console.error`. Every such case is one of the codes below.\n\n/**\n * Stable, machine-readable embed error codes. A host may branch on\n * `error.code`; the strings are part of the public contract.\n */\nexport const EMBED_ERROR_CODES = {\n /**\n * A required `createDeclarionEmbed` option is missing or malformed\n * (`container`, `declarionOrigin`, `route`, `getToken`). Raised\n * synchronously before the iframe is created.\n */\n invalidOptions: \"invalid-options\",\n /**\n * The host's `getToken` callback rejected, threw, or resolved to a value\n * that is not `{ token: string, expires_at: string }`.\n */\n getTokenFailed: \"get-token-failed\",\n /**\n * No `ready` frame arrived from the iframe within the post-mount timeout.\n * The usual causes are a `declarionOrigin` mismatch (the iframe loaded a\n * different origin, or never loaded) or framing denied by the Declarion CSP\n * (the host origin is not in `DECLARION_FRAME_ANCESTORS`). A slow `getToken`\n * does NOT cause this - the timer is cleared as soon as `ready` arrives.\n */\n handshakeTimeout: \"handshake-timeout\",\n /**\n * The iframe asked the host to reload it (`reload-required`) - asset drift\n * or a terminal auth failure inside the iframe.\n */\n reloadRequired: \"reload-required\",\n} as const;\n\n/** The union of all embed error code strings. */\nexport type EmbedErrorCode =\n (typeof EMBED_ERROR_CODES)[keyof typeof EMBED_ERROR_CODES];\n\n/**\n * A typed embed error. Always passed to the host `onError` callback and\n * always also written to `console.error` with the `[declarion-embed]`\n * prefix, so a misconfiguration is never silent.\n *\n * `cause` carries the originating error when one exists (e.g. the rejection\n * value from `getToken`), preserving the stack for debugging.\n */\nexport class EmbedError extends Error {\n /** The stable, machine-readable error code. */\n readonly code: EmbedErrorCode;\n\n constructor(code: EmbedErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"EmbedError\";\n this.code = code;\n // Restore the prototype chain: `extends Error` across the ES5 transpile\n // target otherwise breaks `instanceof EmbedError`.\n Object.setPrototypeOf(this, EmbedError.prototype);\n }\n}\n","// Embed iframe `src` construction.\n//\n// The host SDK builds the iframe URL the Declarion deployment parses. The\n// param grammar MUST match the iframe-side parser EXACTLY:\n// typescript/packages/react/src/embed/params.ts\n// The iframe reads `embed=1`, `parent_origin`, `theme`, `nav`; any drift\n// here silently changes how the iframe boots.\n\nimport type { EmbedNavigationContract, EmbedTheme } from \"./types\";\n\n/** Query-param names that make up the embed URL contract. */\nexport const EMBED_PARAM_EMBED = \"embed\";\nexport const EMBED_PARAM_PARENT_ORIGIN = \"parent_origin\";\nexport const EMBED_PARAM_THEME = \"theme\";\nexport const EMBED_PARAM_NAV = \"nav\";\n\n/** The on-wire value of `embed` that enables shellless render. */\nconst EMBED_PARAM_EMBED_ENABLED = \"1\";\n\n/** Default navigation contract when the host does not set `navigation`. */\nexport const DEFAULT_NAVIGATION_CONTRACT: EmbedNavigationContract = \"self\";\n\n/** Inputs needed to build the iframe `src`. */\nexport interface BuildEmbedSrcInput {\n /** The Declarion deployment origin (`https://app.example.com`). */\n readonly declarionOrigin: string;\n /** The Declarion screen route to embed. */\n readonly route: string;\n /**\n * The host page's own origin. Becomes `parent_origin` so the iframe knows\n * exactly which origin may exchange `postMessage` frames with it.\n */\n readonly parentOrigin: string;\n /** Navigation contract; becomes the `nav` param. */\n readonly navigation: EmbedNavigationContract;\n /** Optional initial theme; becomes the `theme` param when set. */\n readonly theme?: EmbedTheme;\n}\n\n/**\n * Build the absolute iframe `src` URL for an embedded Declarion screen.\n *\n * The `route` is resolved as a path against `declarionOrigin`; the four\n * embed params are appended. `parent_origin` is the host's own origin so the\n * iframe restricts its `postMessage` traffic to exactly that origin.\n *\n * Throws when `declarionOrigin` is not a parseable absolute origin - callers\n * convert that into a typed `EmbedError` before raising it to the host.\n */\nexport function buildEmbedSrc(input: BuildEmbedSrcInput): string {\n // `route` may be a bare path (`/cases`) or already carry a query/hash.\n // Resolving it against the origin keeps any route-level query intact while\n // the embed params are layered on top.\n const url = new URL(input.route, input.declarionOrigin);\n url.searchParams.set(EMBED_PARAM_EMBED, EMBED_PARAM_EMBED_ENABLED);\n url.searchParams.set(EMBED_PARAM_PARENT_ORIGIN, input.parentOrigin);\n url.searchParams.set(EMBED_PARAM_NAV, input.navigation);\n if (input.theme) {\n url.searchParams.set(EMBED_PARAM_THEME, input.theme);\n }\n return url.toString();\n}\n\n/**\n * Resolve a Declarion screen route to an absolute URL for a runtime\n * `navigate` frame. The host passes a route string; the iframe consumes the\n * route as-is, so this only normalizes it against the deployment origin for\n * the host's own bookkeeping. Returns the input unchanged when it cannot be\n * resolved (the iframe tolerates a relative route).\n */\nexport function resolveRoute(declarionOrigin: string, route: string): string {\n try {\n const url = new URL(route, declarionOrigin);\n // Keep the hash so a host can deep-link to an in-page anchor; dropping it\n // would silently break `handle.navigate(\"/cases/42#notes\")`.\n return url.pathname + url.search + url.hash;\n } catch {\n return route;\n }\n}\n","// `createDeclarionEmbed` - the framework-agnostic, dependency-free embed core.\n//\n// Builds the iframe, runs the `ready` -> `auth` handshake, owns token refresh\n// through the host `getToken` callback, auto-applies `resize`, mirrors\n// navigation, and surfaces misconfiguration loudly. The React binding\n// (`./react`) and the demo host both build on this single core.\n\nimport {\n EMBED_MESSAGE_TYPES,\n EMBED_PROTOCOL_VERSION,\n type EmbedSetTokenPayload,\n type EmbedMessage,\n type EmbedMessagePayloadMap,\n type EmbedMessageType,\n type EmbedDirtyChangedPayload,\n type EmbedNavigationPayload,\n type EmbedReloadRequiredPayload,\n type EmbedResizedPayload,\n} from \"./protocol\";\nimport { classifyInboundMessage } from \"./inbound\";\nimport {\n EMBED_ERROR_CODES,\n EmbedError,\n type EmbedErrorCode,\n} from \"./errors\";\nimport {\n DEFAULT_NAVIGATION_CONTRACT,\n buildEmbedSrc,\n resolveRoute,\n} from \"./url\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n EmbedNavigateEvent,\n EmbedToken,\n} from \"./types\";\n\n/**\n * How long the SDK waits for the first `auth` frame to be requested by the\n * iframe after it loads. The iframe emits `ready`; if no `ready` arrives -\n * usually a `declarionOrigin` mismatch or framing denied by the Declarion\n * CSP - the SDK raises a `handshake-timeout` error rather than leave a\n * silently blank iframe.\n */\nconst HANDSHAKE_TIMEOUT_MS = 20_000;\n\n/** Default iframe `title` when the host does not supply one. */\nconst DEFAULT_IFRAME_TITLE = \"Declarion embedded screen\";\n\n/** Console prefix for every SDK diagnostic line. */\nconst LOG_PREFIX = \"[declarion-embed]\";\n\n/**\n * Validate `createDeclarionEmbed` options. Returns a typed `EmbedError` for\n * the first problem found, or `null` when the options are well-formed.\n *\n * Separated from `createDeclarionEmbed` so the React binding can reuse the\n * exact same validation.\n */\nfunction validateOptions(options: DeclarionEmbedOptions): EmbedError | null {\n const fail = (message: string): EmbedError =>\n new EmbedError(EMBED_ERROR_CODES.invalidOptions, message);\n\n if (!options.container || typeof options.container.appendChild !== \"function\") {\n return fail(\n \"`container` must be a DOM element that can receive the iframe.\",\n );\n }\n if (typeof options.declarionOrigin !== \"string\" || options.declarionOrigin === \"\") {\n return fail(\n \"`declarionOrigin` is required and must be the exact origin of the \" +\n \"Declarion deployment, e.g. \\\"https://app.example.com\\\".\",\n );\n }\n let parsedOrigin: URL;\n try {\n parsedOrigin = new URL(options.declarionOrigin);\n } catch {\n return fail(\n `\\`declarionOrigin\\` is not a valid URL: \"${options.declarionOrigin}\". ` +\n 'Pass an exact origin, e.g. \"https://app.example.com\".',\n );\n }\n if (parsedOrigin.origin !== options.declarionOrigin) {\n return fail(\n `\\`declarionOrigin\\` must be exactly an origin with no path, query, ` +\n `or trailing slash. Got \"${options.declarionOrigin}\"; expected ` +\n `\"${parsedOrigin.origin}\".`,\n );\n }\n if (typeof options.route !== \"string\" || options.route === \"\") {\n return fail(\"`route` is required and must be a Declarion screen route.\");\n }\n if (typeof options.getToken !== \"function\") {\n return fail(\n \"`getToken` is required and must be an async function returning \" +\n \"{ token, expires_at }.\",\n );\n }\n return null;\n}\n\n/**\n * Validate the value a host `getToken` callback resolved to. The host owns\n * this code; a malformed return is a developer mistake, surfaced loudly.\n */\nfunction isValidToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Create an embedded Declarion screen inside `options.container`.\n *\n * Returns a `DeclarionEmbedHandle` exposing `navigate`, `setTheme`, and\n * `destroy`. On a misconfiguration the SDK still returns a handle (so\n * `destroy` is always callable) but reports the problem through `onError`\n * and `console.error`; the iframe is not created in that case.\n */\nexport function createDeclarionEmbed(\n options: DeclarionEmbedOptions,\n): DeclarionEmbedHandle {\n let destroyed = false;\n let iframe: HTMLIFrameElement | null = null;\n let messageListener: ((event: MessageEvent) => void) | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let readyReceived = false;\n let firstAuthDelivered = false;\n\n /** Emit a debug line when `debug` is on. */\n const logDebug = (message: string, detail?: unknown): void => {\n if (!options.debug) return;\n if (detail === undefined) {\n console.info(`${LOG_PREFIX} ${message}`);\n } else {\n console.info(`${LOG_PREFIX} ${message}`, detail);\n }\n };\n\n /**\n * Report a developer-facing error. Always written to `console.error` AND\n * handed to `onError`, so a misconfiguration is never silent (Decision 21).\n */\n const reportError = (\n code: EmbedErrorCode,\n message: string,\n cause?: unknown,\n ): void => {\n const error = new EmbedError(code, message, cause ? { cause } : undefined);\n console.error(`${LOG_PREFIX} ${message}`, cause ?? \"\");\n options.onError?.(error);\n };\n\n /** Post an enveloped frame to the iframe, targeting the exact origin. */\n const postToIframe = <T extends EmbedMessageType>(\n type: T,\n payload: EmbedMessagePayloadMap[T],\n ): void => {\n if (!iframe?.contentWindow) return;\n const message: EmbedMessage<T> = {\n source: \"declarion-embed\",\n protocol: EMBED_PROTOCOL_VERSION,\n type,\n payload,\n };\n logDebug(`-> iframe ${type}`, payload);\n iframe.contentWindow.postMessage(message, options.declarionOrigin);\n };\n\n /**\n * Call the host `getToken` callback and deliver the token to the iframe.\n * Surfaces a `get-token-failed` error when the callback rejects, throws,\n * or resolves to a malformed value.\n */\n const requestAndDeliverToken = async (): Promise<void> => {\n let result: unknown;\n try {\n result = await options.getToken();\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` rejected. The host backend must mint a token via \" +\n \"auth.create_embed_session and return { token, expires_at }.\",\n cause,\n );\n return;\n }\n if (!isValidToken(result)) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` resolved to a malformed value. Expected \" +\n \"{ token: string, expires_at: string }.\",\n );\n return;\n }\n if (destroyed) return;\n const payload: EmbedSetTokenPayload = {\n token: result.token,\n expires_at: result.expires_at,\n };\n postToIframe(EMBED_MESSAGE_TYPES.setToken, payload);\n onAuthDelivered();\n };\n\n /** Handle the iframe `ready` frame: the iframe requests its first token. */\n const onReady = (): void => {\n readyReceived = true;\n clearHandshakeTimer();\n logDebug(\"<- iframe ready\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.ready });\n void requestAndDeliverToken();\n };\n\n /** Handle a `token-expired` frame: re-run `getToken` and deliver again. */\n const onTokenExpired = (): void => {\n logDebug(\"<- iframe token-expired\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.tokenExpired });\n void requestAndDeliverToken();\n };\n\n /** Handle a `reload-required` frame: surface it and notify the host. */\n const onReloadRequired = (payload: EmbedReloadRequiredPayload): void => {\n logDebug(\"<- iframe reload-required\", payload);\n options.onEvent?.({\n type: EMBED_MESSAGE_TYPES.reloadRequired,\n payload,\n });\n reportError(\n EMBED_ERROR_CODES.reloadRequired,\n `The embedded iframe requested a reload: ${payload.reason}. ` +\n \"Reload the iframe to recover.\",\n );\n };\n\n /**\n * Handle a `resized` frame: auto-apply the reported height to the iframe.\n * Skipped entirely in fixed-height mode (`options.height` set) - there the\n * host owns the iframe height and the embedded screen scrolls internally.\n */\n const onResize = (payload: EmbedResizedPayload): void => {\n logDebug(\"<- iframe resized\", payload);\n if (!options.height && iframe && Number.isFinite(payload.height) && payload.height > 0) {\n iframe.style.height = `${payload.height}px`;\n }\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.resized, payload });\n };\n\n /**\n * Handle a `dirty-changed` frame: surface the embedded screen's\n * unsaved-edits state. The SDK does not act on it; the host tracks it and\n * guards its own navigation (its menu, a host-initiated `navigate`) before\n * moving the iframe off a dirty screen.\n */\n const onDirtyChanged = (payload: EmbedDirtyChangedPayload): void => {\n logDebug(\"<- iframe dirty-changed\", payload);\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.dirtyChanged, payload });\n };\n\n /**\n * Handle a navigation frame. `navigated` (the iframe moved, `self` mode)\n * and `navigation-requested` (the iframe stayed, `delegated` mode) both\n * surface through `onNavigate`; `mode` records which.\n */\n const onNavigationFrame = (\n type:\n | typeof EMBED_MESSAGE_TYPES.navigated\n | typeof EMBED_MESSAGE_TYPES.navigationRequested,\n payload: EmbedNavigationPayload,\n ): void => {\n logDebug(`<- iframe ${type}`, payload);\n options.onEvent?.({ type, payload });\n const event: EmbedNavigateEvent = {\n mode: type === EMBED_MESSAGE_TYPES.navigated ? \"self\" : \"delegated\",\n route: payload.route,\n entity: payload.entity,\n recordId: payload.recordId,\n };\n options.onNavigate?.(event);\n };\n\n /**\n * Mark the embed ready once the first token is delivered to the iframe.\n * The SDK has no separate \"screen rendered\" signal; the first successful\n * `set-token` delivery after `ready` is the earliest reliable point the\n * host can treat the embedded screen as authenticated.\n */\n const onAuthDelivered = (): void => {\n if (!firstAuthDelivered) {\n firstAuthDelivered = true;\n logDebug(\"handshake complete - iframe authenticated\");\n options.onReady?.();\n }\n };\n\n /** Stop the post-load handshake timeout. */\n const clearHandshakeTimer = (): void => {\n if (handshakeTimer !== null) {\n clearTimeout(handshakeTimer);\n handshakeTimer = null;\n }\n };\n\n /**\n * The single `message` listener. Validates every frame via\n * `classifyInboundMessage`: untrusted frames are dropped silently, a\n * protocol mismatch warns loudly, a valid frame is dispatched.\n */\n const handleMessage = (event: MessageEvent): void => {\n // Scope every inbound frame to THIS instance's iframe. A host page may\n // embed several Declarion screens at once; each instance adds its own\n // `window` `message` listener, and every iframe posts from the same\n // `declarionOrigin`, so origin alone cannot tell them apart. A frame\n // whose `source` is not this iframe's `contentWindow` belongs to another\n // embed (or another widget) - drop it silently so instances never\n // cross-talk.\n if (!iframe || event.source !== iframe.contentWindow) {\n return;\n }\n\n const classified = classifyInboundMessage(event, options.declarionOrigin);\n\n if (classified.kind === \"rejected\") {\n // Untrusted or malformed frame. Dropped silently (security): a page\n // that re-framed the iframe must learn nothing from the host logs.\n return;\n }\n\n if (classified.kind === \"protocol-mismatch\") {\n // The trusted iframe on a mismatched protocol version. Warn loudly,\n // naming both versions - a silent drop here is a blank iframe.\n console.warn(\n `${LOG_PREFIX} protocol version mismatch: this SDK speaks protocol ` +\n `${EMBED_PROTOCOL_VERSION}, the iframe reported protocol ` +\n `${String(classified.received)}. Update @declarion/embed and the ` +\n \"Declarion deployment to matching versions.\",\n );\n return;\n }\n\n const { message } = classified;\n switch (message.type as EmbedMessageType) {\n case EMBED_MESSAGE_TYPES.ready:\n onReady();\n break;\n case EMBED_MESSAGE_TYPES.tokenExpired:\n onTokenExpired();\n break;\n case EMBED_MESSAGE_TYPES.reloadRequired:\n onReloadRequired(message.payload as EmbedReloadRequiredPayload);\n break;\n case EMBED_MESSAGE_TYPES.resized:\n onResize(message.payload as EmbedResizedPayload);\n break;\n case EMBED_MESSAGE_TYPES.navigated:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigated,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.navigationRequested:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigationRequested,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.dirtyChanged:\n onDirtyChanged(message.payload as EmbedDirtyChangedPayload);\n break;\n default:\n // `set-token`, `navigate`, `set-theme` are host -> iframe; the iframe\n // does not send them back. Any other type is not consumed. Ignore.\n break;\n }\n };\n\n // --- Construction ------------------------------------------------------\n\n const optionsError = validateOptions(options);\n if (optionsError) {\n console.error(`${LOG_PREFIX} ${optionsError.message}`);\n options.onError?.(optionsError);\n // Return an inert handle: the iframe was never created, so navigate /\n // setTheme are no-ops and destroy has nothing to tear down.\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n const navigation = options.navigation ?? DEFAULT_NAVIGATION_CONTRACT;\n // The host's own origin. The iframe restricts every `postMessage` it sends\n // to exactly this origin.\n const parentOrigin = window.location.origin;\n\n let src: string;\n try {\n src = buildEmbedSrc({\n declarionOrigin: options.declarionOrigin,\n route: options.route,\n parentOrigin,\n navigation,\n theme: options.theme,\n });\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.invalidOptions,\n `Could not build the iframe URL from route \"${options.route}\". ` +\n \"`route` must be a valid Declarion screen route.\",\n cause,\n );\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n iframe = document.createElement(\"iframe\");\n iframe.src = src;\n iframe.title = options.title ?? DEFAULT_IFRAME_TITLE;\n iframe.style.width = \"100%\";\n iframe.style.border = \"0\";\n iframe.style.display = \"block\";\n // Fixed-height mode: the host owns the height and the SDK never auto-\n // resizes (see onResize). Otherwise a sensible non-zero starting height\n // avoids a zero-height flash before the first `resize` frame arrives.\n if (options.height) {\n iframe.style.height = options.height;\n } else if (!iframe.style.height) {\n iframe.style.height = \"150px\";\n }\n\n messageListener = handleMessage;\n window.addEventListener(\"message\", messageListener);\n options.container.appendChild(iframe);\n logDebug(\"iframe created\", { src });\n\n // Bound the wait for the iframe's `ready` frame. No `ready` means the\n // iframe never loaded the embed runtime: a `declarionOrigin` mismatch,\n // framing denied by the Declarion CSP, or an unreachable route.\n handshakeTimer = setTimeout(() => {\n handshakeTimer = null;\n if (readyReceived) return;\n reportError(\n EMBED_ERROR_CODES.handshakeTimeout,\n `No handshake from the iframe within ${HANDSHAKE_TIMEOUT_MS}ms. ` +\n \"Check that `declarionOrigin` exactly matches the Declarion \" +\n \"deployment origin, that the host origin is allow-listed in the \" +\n \"deployment's DECLARION_FRAME_ANCESTORS, and that `route` resolves \" +\n \"to a real screen.\",\n );\n }, HANDSHAKE_TIMEOUT_MS);\n\n return {\n navigate(route: string): void {\n if (destroyed) return;\n // Drive the iframe to a screen route (deep-linking, both nav modes).\n postToIframe(EMBED_MESSAGE_TYPES.navigate, {\n route: resolveRoute(options.declarionOrigin, route),\n });\n },\n setTheme(theme): void {\n if (destroyed) return;\n postToIframe(EMBED_MESSAGE_TYPES.setTheme, { theme });\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearHandshakeTimer();\n if (messageListener) {\n window.removeEventListener(\"message\", messageListener);\n messageListener = null;\n }\n if (iframe?.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n iframe = null;\n logDebug(\"embed destroyed\");\n },\n };\n}\n"],"mappings":"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"}
package/dist/react.js CHANGED
@@ -6,7 +6,8 @@ function c(e) {
6
6
  return [
7
7
  e.declarionOrigin,
8
8
  e.route,
9
- e.navigation ?? "self"
9
+ e.navigation ?? "self",
10
+ e.height ?? ""
10
11
  ].join("\0");
11
12
  }
12
13
  function l(e, t) {
@@ -19,6 +20,7 @@ function l(e, t) {
19
20
  declarionOrigin: f.current.declarionOrigin,
20
21
  route: f.current.route,
21
22
  navigation: f.current.navigation,
23
+ height: f.current.height,
22
24
  theme: f.current.theme,
23
25
  title: f.current.title,
24
26
  debug: f.current.debug,
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","names":[],"sources":["../src/react.tsx"],"sourcesContent":["// `@declarion/embed/react` - the React binding.\n//\n// `<DeclarionEmbed />` wraps `createDeclarionEmbed` (the dependency-free\n// core) in a React component, managing the embed lifecycle across mount,\n// prop change, and unmount. React is a peer dependency of THIS entry only;\n// the core entry (`@declarion/embed`) stays dependency-free.\n\nimport { useEffect, useImperativeHandle, useRef } from \"react\";\nimport type { ForwardedRef, ReactElement, Ref } from \"react\";\nimport { forwardRef } from \"react\";\nimport { createDeclarionEmbed } from \"./core\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n} from \"./types\";\n\n/**\n * Props for `<DeclarionEmbed />`.\n *\n * Every `createDeclarionEmbed` option except `container` is accepted as a\n * prop - the component owns the container element. `className` and `style`\n * style that container `<div>`.\n */\nexport interface DeclarionEmbedProps\n extends Omit<DeclarionEmbedOptions, \"container\"> {\n /** Optional class applied to the container element wrapping the iframe. */\n readonly className?: string;\n /** Optional inline style applied to the container element. */\n readonly style?: React.CSSProperties;\n}\n\n/**\n * The set of props that, when changed, MUST rebuild the embed: a different\n * Declarion deployment, route, navigation contract, or token source means a\n * different iframe. Theme is intentionally excluded - it is applied live via\n * the handle's `setTheme` without a rebuild.\n */\nfunction rebuildKey(props: DeclarionEmbedProps): string {\n return [\n props.declarionOrigin,\n props.route,\n props.navigation ?? \"self\",\n ].join(\"\\u0000\");\n}\n\n/**\n * Embed a Declarion screen as a white-label iframe.\n *\n * The component creates the embed on mount, rebuilds it when the deployment,\n * route, or navigation contract changes, and destroys it on unmount. A\n * `ref` exposes the imperative `DeclarionEmbedHandle` (`navigate`,\n * `setTheme`, `destroy`) for hosts that drive the iframe directly.\n *\n * `getToken` and the callback props are read through a ref, so passing a\n * fresh inline `getToken` on every render does NOT rebuild the iframe.\n */\nfunction DeclarionEmbedComponent(\n props: DeclarionEmbedProps,\n ref: ForwardedRef<DeclarionEmbedHandle>,\n): ReactElement {\n const { className, style } = props;\n const containerRef = useRef<HTMLDivElement | null>(null);\n const handleRef = useRef<DeclarionEmbedHandle | null>(null);\n\n // Latest props, read by the stable callbacks handed to the core. This\n // keeps `getToken` / `onReady` / `onError` / `onNavigate` / `onEvent`\n // current without rebuilding the iframe when an inline closure changes.\n const propsRef = useRef(props);\n propsRef.current = props;\n\n const key = rebuildKey(props);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return undefined;\n\n const handle = createDeclarionEmbed({\n container,\n // `propsRef.current` is always the latest props snapshot - React updates\n // the ref during render, before this effect runs - so these read\n // correctly on both the initial mount and a key-triggered rebuild.\n declarionOrigin: propsRef.current.declarionOrigin,\n route: propsRef.current.route,\n navigation: propsRef.current.navigation,\n theme: propsRef.current.theme,\n title: propsRef.current.title,\n debug: propsRef.current.debug,\n // The callbacks delegate through `propsRef` so the latest closure\n // always runs, even though the embed was built once.\n getToken: () => propsRef.current.getToken(),\n onReady: () => propsRef.current.onReady?.(),\n onError: (error) => propsRef.current.onError?.(error),\n onNavigate: (event) => propsRef.current.onNavigate?.(event),\n onEvent: (event) => propsRef.current.onEvent?.(event),\n });\n handleRef.current = handle;\n\n return () => {\n handle.destroy();\n handleRef.current = null;\n };\n // `key` changes only when the deployment, route, or navigation contract\n // changes - exactly the props that require a fresh iframe.\n }, [key]);\n\n // Apply a live theme change without a rebuild.\n useEffect(() => {\n if (props.theme) {\n handleRef.current?.setTheme(props.theme);\n }\n }, [props.theme]);\n\n // Expose the imperative handle. The wrapper stays valid across rebuilds:\n // it always delegates to the current `handleRef`.\n useImperativeHandle(\n ref,\n (): DeclarionEmbedHandle => ({\n navigate: (route) => handleRef.current?.navigate(route),\n setTheme: (theme) => handleRef.current?.setTheme(theme),\n destroy: () => handleRef.current?.destroy(),\n }),\n [],\n );\n\n return <div ref={containerRef} className={className} style={style} />;\n}\n\n/**\n * `<DeclarionEmbed />` - the React binding for `@declarion/embed`.\n *\n * Accepts the same options as `createDeclarionEmbed` (minus `container`,\n * which the component owns) and forwards a `DeclarionEmbedHandle` ref.\n */\nexport const DeclarionEmbed = forwardRef(DeclarionEmbedComponent) as (\n props: DeclarionEmbedProps & { ref?: Ref<DeclarionEmbedHandle> },\n) => ReactElement;\n\n// Re-export the option, handle, event, and protocol types so a React host\n// imports everything from the single `@declarion/embed/react` entry.\nexport type {\n DeclarionEmbedOptions,\n DeclarionEmbedHandle,\n EmbedNavigationContract,\n EmbedNavigateEvent,\n EmbedEvent,\n EmbedToken,\n EmbedGetToken,\n EmbedTheme,\n} from \"./types\";\nexport { EmbedError, EMBED_ERROR_CODES } from \"./errors\";\nexport type { EmbedErrorCode } from \"./errors\";\n"],"mappings":";;;;AAqCA,SAAS,EAAW,GAAoC;CACtD,OAAO;EACL,EAAM;EACN,EAAM;EACN,EAAM,cAAc;CACtB,EAAE,KAAK,IAAQ;AACjB;AAaA,SAAS,EACP,GACA,GACc;CACd,IAAM,EAAE,cAAW,aAAU,GACvB,IAAe,EAA8B,IAAI,GACjD,IAAY,EAAoC,IAAI,GAKpD,IAAW,EAAO,CAAK;CAyD7B,OAxDA,EAAS,UAAU,GAInB,QAAgB;EACd,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAS,EAAqB;GAClC;GAIA,iBAAiB,EAAS,QAAQ;GAClC,OAAO,EAAS,QAAQ;GACxB,YAAY,EAAS,QAAQ;GAC7B,OAAO,EAAS,QAAQ;GACxB,OAAO,EAAS,QAAQ;GACxB,OAAO,EAAS,QAAQ;GAGxB,gBAAgB,EAAS,QAAQ,SAAS;GAC1C,eAAe,EAAS,QAAQ,UAAU;GAC1C,UAAU,MAAU,EAAS,QAAQ,UAAU,CAAK;GACpD,aAAa,MAAU,EAAS,QAAQ,aAAa,CAAK;GAC1D,UAAU,MAAU,EAAS,QAAQ,UAAU,CAAK;EACtD,CAAC;EAGD,OAFA,EAAU,UAAU,SAEP;GAEX,AADA,EAAO,QAAQ,GACf,EAAU,UAAU;EACtB;CAGF,GAAG,CAjCS,EAAW,CAiCnB,CAAG,CAAC,GAGR,QAAgB;EACd,AAAI,EAAM,SACR,EAAU,SAAS,SAAS,EAAM,KAAK;CAE3C,GAAG,CAAC,EAAM,KAAK,CAAC,GAIhB,EACE,UAC6B;EAC3B,WAAW,MAAU,EAAU,SAAS,SAAS,CAAK;EACtD,WAAW,MAAU,EAAU,SAAS,SAAS,CAAK;EACtD,eAAe,EAAU,SAAS,QAAQ;CAC5C,IACA,CAAC,CACH,GAEO,kBAAC,OAAD;EAAK,KAAK;EAAyB;EAAkB;CAAQ,CAAA;AACtE;AAQA,IAAa,IAAiB,EAAW,CAAuB"}
1
+ {"version":3,"file":"react.js","names":[],"sources":["../src/react.tsx"],"sourcesContent":["// `@declarion/embed/react` - the React binding.\n//\n// `<DeclarionEmbed />` wraps `createDeclarionEmbed` (the dependency-free\n// core) in a React component, managing the embed lifecycle across mount,\n// prop change, and unmount. React is a peer dependency of THIS entry only;\n// the core entry (`@declarion/embed`) stays dependency-free.\n\nimport { useEffect, useImperativeHandle, useRef } from \"react\";\nimport type { ForwardedRef, ReactElement, Ref } from \"react\";\nimport { forwardRef } from \"react\";\nimport { createDeclarionEmbed } from \"./core\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n} from \"./types\";\n\n/**\n * Props for `<DeclarionEmbed />`.\n *\n * Every `createDeclarionEmbed` option except `container` is accepted as a\n * prop - the component owns the container element. `className` and `style`\n * style that container `<div>`.\n */\nexport interface DeclarionEmbedProps\n extends Omit<DeclarionEmbedOptions, \"container\"> {\n /** Optional class applied to the container element wrapping the iframe. */\n readonly className?: string;\n /** Optional inline style applied to the container element. */\n readonly style?: React.CSSProperties;\n}\n\n/**\n * The set of props that, when changed, MUST rebuild the embed: a different\n * Declarion deployment, route, navigation contract, or fixed height means a\n * different iframe. Theme is intentionally excluded - it is applied live via\n * the handle's `setTheme` without a rebuild.\n */\nfunction rebuildKey(props: DeclarionEmbedProps): string {\n return [\n props.declarionOrigin,\n props.route,\n props.navigation ?? \"self\",\n props.height ?? \"\",\n ].join(\"\\u0000\");\n}\n\n/**\n * Embed a Declarion screen as a white-label iframe.\n *\n * The component creates the embed on mount, rebuilds it when the deployment,\n * route, navigation contract, or fixed height changes, and destroys it on\n * unmount. A\n * `ref` exposes the imperative `DeclarionEmbedHandle` (`navigate`,\n * `setTheme`, `destroy`) for hosts that drive the iframe directly.\n *\n * `getToken` and the callback props are read through a ref, so passing a\n * fresh inline `getToken` on every render does NOT rebuild the iframe.\n */\nfunction DeclarionEmbedComponent(\n props: DeclarionEmbedProps,\n ref: ForwardedRef<DeclarionEmbedHandle>,\n): ReactElement {\n const { className, style } = props;\n const containerRef = useRef<HTMLDivElement | null>(null);\n const handleRef = useRef<DeclarionEmbedHandle | null>(null);\n\n // Latest props, read by the stable callbacks handed to the core. This\n // keeps `getToken` / `onReady` / `onError` / `onNavigate` / `onEvent`\n // current without rebuilding the iframe when an inline closure changes.\n const propsRef = useRef(props);\n propsRef.current = props;\n\n const key = rebuildKey(props);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return undefined;\n\n const handle = createDeclarionEmbed({\n container,\n // `propsRef.current` is always the latest props snapshot - React updates\n // the ref during render, before this effect runs - so these read\n // correctly on both the initial mount and a key-triggered rebuild.\n declarionOrigin: propsRef.current.declarionOrigin,\n route: propsRef.current.route,\n navigation: propsRef.current.navigation,\n height: propsRef.current.height,\n theme: propsRef.current.theme,\n title: propsRef.current.title,\n debug: propsRef.current.debug,\n // The callbacks delegate through `propsRef` so the latest closure\n // always runs, even though the embed was built once.\n getToken: () => propsRef.current.getToken(),\n onReady: () => propsRef.current.onReady?.(),\n onError: (error) => propsRef.current.onError?.(error),\n onNavigate: (event) => propsRef.current.onNavigate?.(event),\n onEvent: (event) => propsRef.current.onEvent?.(event),\n });\n handleRef.current = handle;\n\n return () => {\n handle.destroy();\n handleRef.current = null;\n };\n // `key` changes only when the deployment, route, navigation contract, or\n // fixed height changes - exactly the props that require a fresh iframe.\n }, [key]);\n\n // Apply a live theme change without a rebuild.\n useEffect(() => {\n if (props.theme) {\n handleRef.current?.setTheme(props.theme);\n }\n }, [props.theme]);\n\n // Expose the imperative handle. The wrapper stays valid across rebuilds:\n // it always delegates to the current `handleRef`.\n useImperativeHandle(\n ref,\n (): DeclarionEmbedHandle => ({\n navigate: (route) => handleRef.current?.navigate(route),\n setTheme: (theme) => handleRef.current?.setTheme(theme),\n destroy: () => handleRef.current?.destroy(),\n }),\n [],\n );\n\n return <div ref={containerRef} className={className} style={style} />;\n}\n\n/**\n * `<DeclarionEmbed />` - the React binding for `@declarion/embed`.\n *\n * Accepts the same options as `createDeclarionEmbed` (minus `container`,\n * which the component owns) and forwards a `DeclarionEmbedHandle` ref.\n */\nexport const DeclarionEmbed = forwardRef(DeclarionEmbedComponent) as (\n props: DeclarionEmbedProps & { ref?: Ref<DeclarionEmbedHandle> },\n) => ReactElement;\n\n// Re-export the option, handle, event, and protocol types so a React host\n// imports everything from the single `@declarion/embed/react` entry.\nexport type {\n DeclarionEmbedOptions,\n DeclarionEmbedHandle,\n EmbedNavigationContract,\n EmbedNavigateEvent,\n EmbedEvent,\n EmbedToken,\n EmbedGetToken,\n EmbedTheme,\n} from \"./types\";\nexport { EmbedError, EMBED_ERROR_CODES } from \"./errors\";\nexport type { EmbedErrorCode } from \"./errors\";\n"],"mappings":";;;;AAqCA,SAAS,EAAW,GAAoC;CACtD,OAAO;EACL,EAAM;EACN,EAAM;EACN,EAAM,cAAc;EACpB,EAAM,UAAU;CAClB,EAAE,KAAK,IAAQ;AACjB;AAcA,SAAS,EACP,GACA,GACc;CACd,IAAM,EAAE,cAAW,aAAU,GACvB,IAAe,EAA8B,IAAI,GACjD,IAAY,EAAoC,IAAI,GAKpD,IAAW,EAAO,CAAK;CA0D7B,OAzDA,EAAS,UAAU,GAInB,QAAgB;EACd,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAS,EAAqB;GAClC;GAIA,iBAAiB,EAAS,QAAQ;GAClC,OAAO,EAAS,QAAQ;GACxB,YAAY,EAAS,QAAQ;GAC7B,QAAQ,EAAS,QAAQ;GACzB,OAAO,EAAS,QAAQ;GACxB,OAAO,EAAS,QAAQ;GACxB,OAAO,EAAS,QAAQ;GAGxB,gBAAgB,EAAS,QAAQ,SAAS;GAC1C,eAAe,EAAS,QAAQ,UAAU;GAC1C,UAAU,MAAU,EAAS,QAAQ,UAAU,CAAK;GACpD,aAAa,MAAU,EAAS,QAAQ,aAAa,CAAK;GAC1D,UAAU,MAAU,EAAS,QAAQ,UAAU,CAAK;EACtD,CAAC;EAGD,OAFA,EAAU,UAAU,SAEP;GAEX,AADA,EAAO,QAAQ,GACf,EAAU,UAAU;EACtB;CAGF,GAAG,CAlCS,EAAW,CAkCnB,CAAG,CAAC,GAGR,QAAgB;EACd,AAAI,EAAM,SACR,EAAU,SAAS,SAAS,EAAM,KAAK;CAE3C,GAAG,CAAC,EAAM,KAAK,CAAC,GAIhB,EACE,UAC6B;EAC3B,WAAW,MAAU,EAAU,SAAS,SAAS,CAAK;EACtD,WAAW,MAAU,EAAU,SAAS,SAAS,CAAK;EACtD,eAAe,EAAU,SAAS,QAAQ;CAC5C,IACA,CAAC,CACH,GAEO,kBAAC,OAAD;EAAK,KAAK;EAAyB;EAAkB;CAAQ,CAAA;AACtE;AAQA,IAAa,IAAiB,EAAW,CAAuB"}
package/dist/server.d.ts CHANGED
@@ -16,7 +16,11 @@ export interface CreateEmbedSessionOptions {
16
16
  * call. MUST be held server-side only; never ship it to a browser.
17
17
  */
18
18
  readonly apiKey: string;
19
- /** Tenant code; MUST match the API key's active tenant. */
19
+ /**
20
+ * The target tenant to mint the embed session in. A tenant-scoped `dk:`
21
+ * key may name only its own tenant; a `_global` `dk:` key may name any
22
+ * tenant (one key for a multi-tenant host).
23
+ */
20
24
  readonly tenantCode: string;
21
25
  /** Email of the target user the iframe runs as (host identity assertion). */
22
26
  readonly userEmail: string;
@@ -28,8 +32,12 @@ export interface CreateEmbedSessionOptions {
28
32
  */
29
33
  readonly ttlSeconds?: number;
30
34
  /**
31
- * Extra permission allow-list. Every entry MUST be held by the target
32
- * user; wildcards are rejected by the deployment.
35
+ * Optional permission RESTRICTION. When omitted, the embed token carries
36
+ * the target user's full effective permissions - the embedded screen lets
37
+ * the user do exactly what they are allowed. When provided, the token is
38
+ * restricted to this explicit allow-list: wildcards are rejected and every
39
+ * entry MUST be held by the target user. Use it to harden a deliberately
40
+ * limited iframe (for example a public, read-only embed).
33
41
  */
34
42
  readonly permissions?: readonly string[];
35
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["// `@declarion/embed/server` - the Node-side mint helper.\n//\n// `createEmbedSession` performs the authenticated\n// `POST /api/actions/auth.create_embed_session` call so the host backend\n// writes one typed call instead of a hand-rolled HTTP request.\n//\n// This entry is SEPARATE from the browser core (`@declarion/embed`) and the\n// React binding (`@declarion/embed/react`) for one security reason: the\n// `dk:` API key is a backend-to-backend credential and MUST NEVER reach a\n// browser. Keeping it in its own entry means a frontend bundle that imports\n// `@declarion/embed` or `@declarion/embed/react` cannot pull the key path.\n//\n// The helper uses the global `fetch` (Node 18+, the platform's pinned Node\n// is 24) so it stays dependency-free.\n\nimport type { EmbedToken } from \"./types\";\n\n/** The fully-qualified action code for minting an embed session. */\nconst EMBED_SESSION_ACTION = \"auth.create_embed_session\";\n\n/** Path of the generic action dispatcher on a Declarion deployment. */\nconst ACTIONS_PATH = \"/api/actions\";\n\n/** Default token lifetime (seconds) when the host does not request one. */\nconst DEFAULT_TTL_SECONDS = 600;\n\n/**\n * Options for `createEmbedSession`. `apiKey`, `declarionOrigin`, and the\n * three identity fields are required; `ttlSeconds` and `permissions` are\n * optional and forwarded to the action unchanged.\n */\nexport interface CreateEmbedSessionOptions {\n /**\n * The exact origin of the Declarion deployment, e.g.\n * `https://app.example.com`. The action call is `${declarionOrigin}` +\n * `/api/actions/auth.create_embed_session`.\n */\n readonly declarionOrigin: string;\n /**\n * The `dk:`-prefixed Declarion API key. The trust anchor for the mint\n * call. MUST be held server-side only; never ship it to a browser.\n */\n readonly apiKey: string;\n /** Tenant code; MUST match the API key's active tenant. */\n readonly tenantCode: string;\n /** Email of the target user the iframe runs as (host identity assertion). */\n readonly userEmail: string;\n /** Code of the Declarion screen to embed. */\n readonly screenCode: string;\n /**\n * Token lifetime in seconds. Defaults to 600. The deployment caps it at\n * 900 and raises values below 60 to 60.\n */\n readonly ttlSeconds?: number;\n /**\n * Extra permission allow-list. Every entry MUST be held by the target\n * user; wildcards are rejected by the deployment.\n */\n readonly permissions?: readonly string[];\n /**\n * Optional `AbortSignal` to cancel the mint request (e.g. a host request\n * timeout). Forwarded to `fetch`.\n */\n readonly signal?: AbortSignal;\n}\n\n/**\n * The error thrown when `auth.create_embed_session` does not return a\n * session. Carries the deployment's machine-readable error `code` (e.g.\n * `EMBED_NOT_API_KEY`, `FORBIDDEN`) and the HTTP status so the host backend\n * can branch on the failure.\n */\nexport class EmbedSessionError extends Error {\n /** The Declarion error code, or `null` when the response had none. */\n readonly code: string | null;\n /** The HTTP status of the action response. */\n readonly status: number;\n\n constructor(message: string, status: number, code: string | null) {\n super(message);\n this.name = \"EmbedSessionError\";\n this.code = code;\n this.status = status;\n Object.setPrototypeOf(this, EmbedSessionError.prototype);\n }\n}\n\n/** The action dispatcher's success envelope: `{ status, result, ... }`. */\ninterface ActionSuccessEnvelope {\n readonly status?: string;\n readonly result?: unknown;\n}\n\n/** The action dispatcher's error envelope: `{ error: { message, code } }`. */\ninterface ActionErrorEnvelope {\n readonly error?: { readonly message?: string; readonly code?: string };\n}\n\n/** Narrow an unknown parsed body to a valid `{ token, expires_at }` result. */\nfunction isEmbedToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Mint a short-lived, scoped embed session token for a host-asserted user.\n *\n * Calls `POST /api/actions/auth.create_embed_session` on the Declarion\n * deployment, authenticating with the server-held `dk:` API key, and returns\n * the `{ token, expires_at }` the host hands to the iframe (typically as the\n * `getToken` result of `createDeclarionEmbed` / `<DeclarionEmbed />`).\n *\n * Throws `EmbedSessionError` on any non-success response, carrying the\n * deployment's error code and HTTP status.\n */\nexport async function createEmbedSession(\n options: CreateEmbedSessionOptions,\n): Promise<EmbedToken> {\n const origin = new URL(options.declarionOrigin).origin;\n const url = `${origin}${ACTIONS_PATH}/${EMBED_SESSION_ACTION}`;\n\n const body: Record<string, unknown> = {\n tenant_code: options.tenantCode,\n user_email: options.userEmail,\n screen_code: options.screenCode,\n ttl_seconds: options.ttlSeconds ?? DEFAULT_TTL_SECONDS,\n };\n if (options.permissions && options.permissions.length > 0) {\n body.permissions = options.permissions;\n }\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n // The `dk:` API key travels in the Authorization header. The action\n // handler requires `AuthMethod == \"apikey\"`; a cookie or Basic auth\n // cannot mint an embed session.\n authorization: options.apiKey,\n },\n body: JSON.stringify(body),\n signal: options.signal,\n });\n\n // Parse the JSON body once; both the success and the error envelope are\n // JSON. A non-JSON body (a proxy error page) is surfaced as a generic\n // failure carrying the HTTP status.\n let parsed: unknown;\n try {\n parsed = await response.json();\n } catch {\n throw new EmbedSessionError(\n `auth.create_embed_session returned a non-JSON response ` +\n `(HTTP ${response.status}).`,\n response.status,\n null,\n );\n }\n\n if (!response.ok) {\n const errorBody = parsed as ActionErrorEnvelope;\n const code = errorBody.error?.code ?? null;\n const message =\n errorBody.error?.message ??\n `auth.create_embed_session failed with HTTP ${response.status}.`;\n throw new EmbedSessionError(message, response.status, code);\n }\n\n const result = (parsed as ActionSuccessEnvelope).result;\n if (!isEmbedToken(result)) {\n throw new EmbedSessionError(\n \"auth.create_embed_session returned a malformed result: expected \" +\n \"{ token, expires_at }.\",\n response.status,\n null,\n );\n }\n\n return { token: result.token, expires_at: result.expires_at };\n}\n\nexport type { EmbedToken } from \"./types\";\n"],"mappings":";AAkBA,IAAM,IAAuB,6BAGvB,IAAe,gBAGf,IAAsB,KAgDf,IAAb,MAAa,UAA0B,MAAM;CAE3C;CAEA;CAEA,YAAY,GAAiB,GAAgB,GAAqB;EAKhE,AAJA,MAAM,CAAO,GACb,KAAK,OAAO,qBACZ,KAAK,OAAO,GACZ,KAAK,SAAS,GACd,OAAO,eAAe,MAAM,EAAkB,SAAS;CACzD;AACF;AAcA,SAAS,EAAa,GAAqC;CACzD,IAAI,OAAO,KAAU,aAAY,GAAgB,OAAO;CACxD,IAAM,IAAQ;CACd,OACE,OAAO,EAAM,SAAU,YACvB,EAAM,UAAU,MAChB,OAAO,EAAM,cAAe,YAC5B,EAAM,eAAe;AAEzB;AAaA,eAAsB,EACpB,GACqB;CAErB,IAAM,IAAM,GADG,IAAI,IAAI,EAAQ,eAAe,EAAE,SACxB,EAAa,GAAG,KAElC,IAAgC;EACpC,aAAa,EAAQ;EACrB,YAAY,EAAQ;EACpB,aAAa,EAAQ;EACrB,aAAa,EAAQ,cAAc;CACrC;CACA,AAAI,EAAQ,eAAe,EAAQ,YAAY,SAAS,MACtD,EAAK,cAAc,EAAQ;CAG7B,IAAM,IAAW,MAAM,MAAM,GAAK;EAChC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAIhB,eAAe,EAAQ;EACzB;EACA,MAAM,KAAK,UAAU,CAAI;EACzB,QAAQ,EAAQ;CAClB,CAAC,GAKG;CACJ,IAAI;EACF,IAAS,MAAM,EAAS,KAAK;CAC/B,QAAQ;EACN,MAAM,IAAI,EACR,gEACW,EAAS,OAAO,KAC3B,EAAS,QACT,IACF;CACF;CAEA,IAAI,CAAC,EAAS,IAAI;EAChB,IAAM,IAAY,GACZ,IAAO,EAAU,OAAO,QAAQ;EAItC,MAAM,IAAI,EAFR,EAAU,OAAO,WACjB,8CAA8C,EAAS,OAAO,IAC3B,EAAS,QAAQ,CAAI;CAC5D;CAEA,IAAM,IAAU,EAAiC;CACjD,IAAI,CAAC,EAAa,CAAM,GACtB,MAAM,IAAI,EACR,0FAEA,EAAS,QACT,IACF;CAGF,OAAO;EAAE,OAAO,EAAO;EAAO,YAAY,EAAO;CAAW;AAC9D"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["// `@declarion/embed/server` - the Node-side mint helper.\n//\n// `createEmbedSession` performs the authenticated\n// `POST /api/actions/auth.create_embed_session` call so the host backend\n// writes one typed call instead of a hand-rolled HTTP request.\n//\n// This entry is SEPARATE from the browser core (`@declarion/embed`) and the\n// React binding (`@declarion/embed/react`) for one security reason: the\n// `dk:` API key is a backend-to-backend credential and MUST NEVER reach a\n// browser. Keeping it in its own entry means a frontend bundle that imports\n// `@declarion/embed` or `@declarion/embed/react` cannot pull the key path.\n//\n// The helper uses the global `fetch` (Node 18+, the platform's pinned Node\n// is 24) so it stays dependency-free.\n\nimport type { EmbedToken } from \"./types\";\n\n/** The fully-qualified action code for minting an embed session. */\nconst EMBED_SESSION_ACTION = \"auth.create_embed_session\";\n\n/** Path of the generic action dispatcher on a Declarion deployment. */\nconst ACTIONS_PATH = \"/api/actions\";\n\n/** Default token lifetime (seconds) when the host does not request one. */\nconst DEFAULT_TTL_SECONDS = 600;\n\n/**\n * Options for `createEmbedSession`. `apiKey`, `declarionOrigin`, and the\n * three identity fields are required; `ttlSeconds` and `permissions` are\n * optional and forwarded to the action unchanged.\n */\nexport interface CreateEmbedSessionOptions {\n /**\n * The exact origin of the Declarion deployment, e.g.\n * `https://app.example.com`. The action call is `${declarionOrigin}` +\n * `/api/actions/auth.create_embed_session`.\n */\n readonly declarionOrigin: string;\n /**\n * The `dk:`-prefixed Declarion API key. The trust anchor for the mint\n * call. MUST be held server-side only; never ship it to a browser.\n */\n readonly apiKey: string;\n /**\n * The target tenant to mint the embed session in. A tenant-scoped `dk:`\n * key may name only its own tenant; a `_global` `dk:` key may name any\n * tenant (one key for a multi-tenant host).\n */\n readonly tenantCode: string;\n /** Email of the target user the iframe runs as (host identity assertion). */\n readonly userEmail: string;\n /** Code of the Declarion screen to embed. */\n readonly screenCode: string;\n /**\n * Token lifetime in seconds. Defaults to 600. The deployment caps it at\n * 900 and raises values below 60 to 60.\n */\n readonly ttlSeconds?: number;\n /**\n * Optional permission RESTRICTION. When omitted, the embed token carries\n * the target user's full effective permissions - the embedded screen lets\n * the user do exactly what they are allowed. When provided, the token is\n * restricted to this explicit allow-list: wildcards are rejected and every\n * entry MUST be held by the target user. Use it to harden a deliberately\n * limited iframe (for example a public, read-only embed).\n */\n readonly permissions?: readonly string[];\n /**\n * Optional `AbortSignal` to cancel the mint request (e.g. a host request\n * timeout). Forwarded to `fetch`.\n */\n readonly signal?: AbortSignal;\n}\n\n/**\n * The error thrown when `auth.create_embed_session` does not return a\n * session. Carries the deployment's machine-readable error `code` (e.g.\n * `EMBED_NOT_API_KEY`, `FORBIDDEN`) and the HTTP status so the host backend\n * can branch on the failure.\n */\nexport class EmbedSessionError extends Error {\n /** The Declarion error code, or `null` when the response had none. */\n readonly code: string | null;\n /** The HTTP status of the action response. */\n readonly status: number;\n\n constructor(message: string, status: number, code: string | null) {\n super(message);\n this.name = \"EmbedSessionError\";\n this.code = code;\n this.status = status;\n Object.setPrototypeOf(this, EmbedSessionError.prototype);\n }\n}\n\n/** The action dispatcher's success envelope: `{ status, result, ... }`. */\ninterface ActionSuccessEnvelope {\n readonly status?: string;\n readonly result?: unknown;\n}\n\n/** The action dispatcher's error envelope: `{ error: { message, code } }`. */\ninterface ActionErrorEnvelope {\n readonly error?: { readonly message?: string; readonly code?: string };\n}\n\n/** Narrow an unknown parsed body to a valid `{ token, expires_at }` result. */\nfunction isEmbedToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Mint a short-lived, scoped embed session token for a host-asserted user.\n *\n * Calls `POST /api/actions/auth.create_embed_session` on the Declarion\n * deployment, authenticating with the server-held `dk:` API key, and returns\n * the `{ token, expires_at }` the host hands to the iframe (typically as the\n * `getToken` result of `createDeclarionEmbed` / `<DeclarionEmbed />`).\n *\n * Throws `EmbedSessionError` on any non-success response, carrying the\n * deployment's error code and HTTP status.\n */\nexport async function createEmbedSession(\n options: CreateEmbedSessionOptions,\n): Promise<EmbedToken> {\n const origin = new URL(options.declarionOrigin).origin;\n const url = `${origin}${ACTIONS_PATH}/${EMBED_SESSION_ACTION}`;\n\n const body: Record<string, unknown> = {\n tenant_code: options.tenantCode,\n user_email: options.userEmail,\n screen_code: options.screenCode,\n ttl_seconds: options.ttlSeconds ?? DEFAULT_TTL_SECONDS,\n };\n if (options.permissions && options.permissions.length > 0) {\n body.permissions = options.permissions;\n }\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n // The `dk:` API key travels in the Authorization header. The action\n // handler requires `AuthMethod == \"apikey\"`; a cookie or Basic auth\n // cannot mint an embed session.\n authorization: options.apiKey,\n },\n body: JSON.stringify(body),\n signal: options.signal,\n });\n\n // Parse the JSON body once; both the success and the error envelope are\n // JSON. A non-JSON body (a proxy error page) is surfaced as a generic\n // failure carrying the HTTP status.\n let parsed: unknown;\n try {\n parsed = await response.json();\n } catch {\n throw new EmbedSessionError(\n `auth.create_embed_session returned a non-JSON response ` +\n `(HTTP ${response.status}).`,\n response.status,\n null,\n );\n }\n\n if (!response.ok) {\n const errorBody = parsed as ActionErrorEnvelope;\n const code = errorBody.error?.code ?? null;\n const message =\n errorBody.error?.message ??\n `auth.create_embed_session failed with HTTP ${response.status}.`;\n throw new EmbedSessionError(message, response.status, code);\n }\n\n const result = (parsed as ActionSuccessEnvelope).result;\n if (!isEmbedToken(result)) {\n throw new EmbedSessionError(\n \"auth.create_embed_session returned a malformed result: expected \" +\n \"{ token, expires_at }.\",\n response.status,\n null,\n );\n }\n\n return { token: result.token, expires_at: result.expires_at };\n}\n\nexport type { EmbedToken } from \"./types\";\n"],"mappings":";AAkBA,IAAM,IAAuB,6BAGvB,IAAe,gBAGf,IAAsB,KAwDf,IAAb,MAAa,UAA0B,MAAM;CAE3C;CAEA;CAEA,YAAY,GAAiB,GAAgB,GAAqB;EAKhE,AAJA,MAAM,CAAO,GACb,KAAK,OAAO,qBACZ,KAAK,OAAO,GACZ,KAAK,SAAS,GACd,OAAO,eAAe,MAAM,EAAkB,SAAS;CACzD;AACF;AAcA,SAAS,EAAa,GAAqC;CACzD,IAAI,OAAO,KAAU,aAAY,GAAgB,OAAO;CACxD,IAAM,IAAQ;CACd,OACE,OAAO,EAAM,SAAU,YACvB,EAAM,UAAU,MAChB,OAAO,EAAM,cAAe,YAC5B,EAAM,eAAe;AAEzB;AAaA,eAAsB,EACpB,GACqB;CAErB,IAAM,IAAM,GADG,IAAI,IAAI,EAAQ,eAAe,EAAE,SACxB,EAAa,GAAG,KAElC,IAAgC;EACpC,aAAa,EAAQ;EACrB,YAAY,EAAQ;EACpB,aAAa,EAAQ;EACrB,aAAa,EAAQ,cAAc;CACrC;CACA,AAAI,EAAQ,eAAe,EAAQ,YAAY,SAAS,MACtD,EAAK,cAAc,EAAQ;CAG7B,IAAM,IAAW,MAAM,MAAM,GAAK;EAChC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAIhB,eAAe,EAAQ;EACzB;EACA,MAAM,KAAK,UAAU,CAAI;EACzB,QAAQ,EAAQ;CAClB,CAAC,GAKG;CACJ,IAAI;EACF,IAAS,MAAM,EAAS,KAAK;CAC/B,QAAQ;EACN,MAAM,IAAI,EACR,gEACW,EAAS,OAAO,KAC3B,EAAS,QACT,IACF;CACF;CAEA,IAAI,CAAC,EAAS,IAAI;EAChB,IAAM,IAAY,GACZ,IAAO,EAAU,OAAO,QAAQ;EAItC,MAAM,IAAI,EAFR,EAAU,OAAO,WACjB,8CAA8C,EAAS,OAAO,IAC3B,EAAS,QAAQ,CAAI;CAC5D;CAEA,IAAM,IAAU,EAAiC;CACjD,IAAI,CAAC,EAAa,CAAM,GACtB,MAAM,IAAI,EACR,0FAEA,EAAS,QACT,IACF;CAGF,OAAO;EAAE,OAAO,EAAO;EAAO,YAAY,EAAO;CAAW;AAC9D"}
package/dist/types.d.ts CHANGED
@@ -99,6 +99,19 @@ export interface DeclarionEmbedOptions {
99
99
  readonly theme?: EmbedTheme;
100
100
  /** The iframe `title` attribute. Defaults to a sensible label. */
101
101
  readonly title?: string;
102
+ /**
103
+ * A fixed iframe height (any CSS length, e.g. `"640px"` or `"75vh"`).
104
+ *
105
+ * When set, the SDK sizes the iframe to it and STOPS auto-resizing - the
106
+ * `resized` frames from the iframe are ignored. This is REQUIRED for
107
+ * screens that fill their own viewport with internal scroll, chiefly list
108
+ * screens: their virtualized table needs a real, bounded viewport, which a
109
+ * content-driven auto-resizing iframe never provides.
110
+ *
111
+ * When omitted (default), the SDK auto-resizes the iframe to the embedded
112
+ * content's height - correct for intrinsic-height screens (forms, detail).
113
+ */
114
+ readonly height?: string;
102
115
  /** When true, logs the handshake and every protocol frame to the console. */
103
116
  readonly debug?: boolean;
104
117
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declarion/embed",
3
- "version": "0.1.92",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Host integration SDK for embedding Declarion screens as white-label iframes.",