@declarion/embed 0.5.0 → 0.6.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
@@ -114,10 +114,41 @@ The package also ships an IIFE bundle of the core. Load it and use
114
114
 
115
115
  ## Navigation modes
116
116
 
117
- - `navigation: "self"` (default) - the iframe navigates internally and emits
118
- `onNavigate({ mode: "self", ... })` so the host can mirror its URL.
117
+ - `navigation: "self"` (default) - the iframe navigates internally for a
118
+ same-surface move (same screen, or a list-to-detail drill-down of the same
119
+ entity) and emits `onNavigate({ mode: "self", route, screenCode, ... })` so
120
+ the host can mirror its URL. A cross-surface jump is delegated even in self
121
+ mode, so the host's native navigation never desyncs.
119
122
  - `navigation: "delegated"` - the iframe never moves; it emits
120
- `onNavigate({ mode: "delegated", ... })` and the host opens the route.
123
+ `onNavigate({ mode: "delegated", route, screenCode, ... })` and the host
124
+ opens the route. Pass `screenCode` to `createEmbedSession` to mint a scoped
125
+ token for the target screen, then remount/redirect the iframe to it.
126
+
127
+ The `onNavigate` event carries `{ mode, route, screenCode?, entity?, recordId? }`.
128
+
129
+ ## Navigation failures
130
+
131
+ When an in-frame navigation cannot be handled - an unknown route, or a 404 -
132
+ the iframe stays put and emits `onNavigationFailed({ route, reason })`
133
+ (`reason` is `not_found` or `unresolved`). The host owns recovery: show its own
134
+ not-found, or reset its sidebar selection. Never break the frame.
135
+
136
+ ```ts
137
+ createDeclarionEmbed({
138
+ // ...
139
+ onNavigationFailed({ route, reason }) {
140
+ hostRouter.showNotFound(route, reason);
141
+ },
142
+ });
143
+ ```
144
+
145
+ ## Iframe sandbox
146
+
147
+ The SDK sets a `sandbox` on the iframe by default that runs the full app but
148
+ omits the top-navigation tokens, so the framed app cannot navigate the host's
149
+ top window. Widen it with the `extraSandbox` option if needed - the
150
+ top-navigation tokens are stripped from it and can never be re-added. Keep your
151
+ CSP `frame-ancestors` configured to control *who* may frame the app.
121
152
 
122
153
  ## Unsaved changes
123
154
 
package/dist/core.js CHANGED
@@ -8,6 +8,7 @@ var e = "declarion-embed", t = 1, n = {
8
8
  navigated: "navigated",
9
9
  navigationRequested: "navigation-requested",
10
10
  dirtyChanged: "dirty-changed",
11
+ navigationFailed: "navigation-failed",
11
12
  navigate: "navigate",
12
13
  setTheme: "set-theme"
13
14
  };
@@ -53,8 +54,25 @@ function d(e, t) {
53
54
  }
54
55
  //#endregion
55
56
  //#region src/core.ts
56
- var f = 2e4, p = "Declarion embedded screen", m = "[declarion-embed]";
57
- function h(e) {
57
+ var f = 2e4, p = "Declarion embedded screen", m = [
58
+ "allow-scripts",
59
+ "allow-same-origin",
60
+ "allow-forms",
61
+ "allow-modals",
62
+ "allow-downloads",
63
+ "allow-popups",
64
+ "allow-popups-to-escape-sandbox"
65
+ ], h = new Set(["allow-top-navigation", "allow-top-navigation-by-user-activation"]);
66
+ function g(e) {
67
+ let t = new Set(m);
68
+ for (let n of e ?? []) {
69
+ let e = n.trim();
70
+ e && !h.has(e) && t.add(e);
71
+ }
72
+ return Array.from(t).join(" ");
73
+ }
74
+ var _ = "[declarion-embed]";
75
+ function v(e) {
58
76
  let t = (e) => new a(i.invalidOptions, e);
59
77
  if (!e.container || typeof e.container.appendChild != "function") return t("`container` must be a DOM element that can receive the iframe.");
60
78
  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\".");
@@ -66,18 +84,18 @@ function h(e) {
66
84
  }
67
85
  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}".`);
68
86
  }
69
- function g(e) {
87
+ function y(e) {
70
88
  if (typeof e != "object" || !e) return !1;
71
89
  let t = e;
72
90
  return typeof t.token == "string" && t.token !== "" && typeof t.expires_at == "string" && t.expires_at !== "";
73
91
  }
74
- function _(e) {
75
- let t = !1, o = null, s = null, c = null, l = !1, _ = !1, v = (t, n) => {
76
- e.debug && (n === void 0 ? console.info(`${m} ${t}`) : console.info(`${m} ${t}`, n));
77
- }, y = (t, n, r) => {
92
+ function b(e) {
93
+ let t = !1, o = null, s = null, c = null, l = !1, m = !1, h = (t, n) => {
94
+ e.debug && (n === void 0 ? console.info(`${_} ${t}`) : console.info(`${_} ${t}`, n));
95
+ }, b = (t, n, r) => {
78
96
  let i = new a(t, n, r ? { cause: r } : void 0);
79
- console.error(`${m} ${n}`, r ?? ""), e.onError?.(i);
80
- }, b = (t, n) => {
97
+ console.error(`${_} ${n}`, r ?? ""), e.onError?.(i);
98
+ }, x = (t, n) => {
81
99
  if (!o?.contentWindow) return;
82
100
  let r = {
83
101
  source: "declarion-embed",
@@ -85,17 +103,17 @@ function _(e) {
85
103
  type: t,
86
104
  payload: n
87
105
  };
88
- v(`-> iframe ${t}`, n), o.contentWindow.postMessage(r, e.declarionOrigin);
89
- }, x = async () => {
106
+ h(`-> iframe ${t}`, n), o.contentWindow.postMessage(r, e.declarionOrigin);
107
+ }, S = async () => {
90
108
  let r;
91
109
  try {
92
110
  r = await e.getToken();
93
111
  } catch (e) {
94
- y(i.getTokenFailed, "`getToken` rejected. The host backend must mint a token via auth.create_embed_session and return { token, expires_at }.", e);
112
+ b(i.getTokenFailed, "`getToken` rejected. The host backend must mint a token via auth.create_embed_session and return { token, expires_at }.", e);
95
113
  return;
96
114
  }
97
- if (!g(r)) {
98
- y(i.getTokenFailed, "`getToken` resolved to a malformed value. Expected { token: string, expires_at: string }.");
115
+ if (!y(r)) {
116
+ b(i.getTokenFailed, "`getToken` resolved to a malformed value. Expected { token: string, expires_at: string }.");
99
117
  return;
100
118
  }
101
119
  if (t) return;
@@ -103,112 +121,125 @@ function _(e) {
103
121
  token: r.token,
104
122
  expires_at: r.expires_at
105
123
  };
106
- b(n.setToken, a), O();
107
- }, S = () => {
108
- l = !0, k(), v("<- iframe ready"), e.onEvent?.({ type: n.ready }), x();
124
+ x(n.setToken, a), j();
109
125
  }, C = () => {
110
- v("<- iframe token-expired"), e.onEvent?.({ type: n.tokenExpired }), x();
111
- }, w = (t) => {
112
- v("<- iframe reload-required", t), e.onEvent?.({
126
+ l = !0, M(), h("<- iframe ready"), e.onEvent?.({ type: n.ready }), S();
127
+ }, w = () => {
128
+ h("<- iframe token-expired"), e.onEvent?.({ type: n.tokenExpired }), S();
129
+ }, T = (t) => {
130
+ h("<- iframe reload-required", t), e.onEvent?.({
113
131
  type: n.reloadRequired,
114
132
  payload: t
115
- }), y(i.reloadRequired, `The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`);
116
- }, T = (t) => {
117
- v("<- iframe resized", t), !e.height && o && Number.isFinite(t.height) && t.height > 0 && (o.style.height = `${t.height}px`), e.onEvent?.({
133
+ }), b(i.reloadRequired, `The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`);
134
+ }, E = (t) => {
135
+ h("<- iframe resized", t), !e.height && o && Number.isFinite(t.height) && t.height > 0 && (o.style.height = `${t.height}px`), e.onEvent?.({
118
136
  type: n.resized,
119
137
  payload: t
120
138
  });
121
- }, E = (t) => {
122
- v("<- iframe dirty-changed", t), e.onEvent?.({
139
+ }, D = (t) => {
140
+ h("<- iframe dirty-changed", t), e.onEvent?.({
123
141
  type: n.dirtyChanged,
124
142
  payload: t
125
143
  });
126
- }, D = (t, r) => {
127
- v(`<- iframe ${t}`, r), e.onEvent?.({
144
+ }, O = (t, r) => {
145
+ h(`<- iframe ${t}`, r), e.onEvent?.({
128
146
  type: t,
129
147
  payload: r
130
148
  });
131
149
  let i = {
132
150
  mode: t === n.navigated ? "self" : "delegated",
133
151
  route: r.route,
152
+ screenCode: r.screenCode,
134
153
  entity: r.entity,
135
154
  recordId: r.recordId
136
155
  };
137
156
  e.onNavigate?.(i);
138
- }, O = () => {
139
- _ || (_ = !0, v("handshake complete - iframe authenticated"), e.onReady?.());
140
- }, k = () => {
157
+ }, k = new Set(["not_found", "unresolved"]), A = (t) => {
158
+ if (typeof t?.route != "string" || t.route.length === 0 || !k.has(t.reason)) {
159
+ h("<- iframe navigation-failed (dropped: invalid payload)", t);
160
+ return;
161
+ }
162
+ h("<- iframe navigation-failed", t), e.onEvent?.({
163
+ type: n.navigationFailed,
164
+ payload: t
165
+ }), e.onNavigationFailed?.(t);
166
+ }, j = () => {
167
+ m || (m = !0, h("handshake complete - iframe authenticated"), e.onReady?.());
168
+ }, M = () => {
141
169
  c !== null && (clearTimeout(c), c = null);
142
- }, A = (t) => {
170
+ }, N = (t) => {
143
171
  if (!o || t.source !== o.contentWindow) return;
144
172
  let i = r(t, e.declarionOrigin);
145
173
  if (i.kind === "rejected") return;
146
174
  if (i.kind === "protocol-mismatch") {
147
- console.warn(`${m} protocol version mismatch: this SDK speaks protocol 1, the iframe reported protocol ${String(i.received)}. Update @declarion/embed and the Declarion deployment to matching versions.`);
175
+ console.warn(`${_} protocol version mismatch: this SDK speaks protocol 1, the iframe reported protocol ${String(i.received)}. Update @declarion/embed and the Declarion deployment to matching versions.`);
148
176
  return;
149
177
  }
150
178
  let { message: a } = i;
151
179
  switch (a.type) {
152
180
  case n.ready:
153
- S();
181
+ C();
154
182
  break;
155
183
  case n.tokenExpired:
156
- C();
184
+ w();
157
185
  break;
158
186
  case n.reloadRequired:
159
- w(a.payload);
187
+ T(a.payload);
160
188
  break;
161
189
  case n.resized:
162
- T(a.payload);
190
+ E(a.payload);
163
191
  break;
164
192
  case n.navigated:
165
- D(n.navigated, a.payload);
193
+ O(n.navigated, a.payload);
166
194
  break;
167
195
  case n.navigationRequested:
168
- D(n.navigationRequested, a.payload);
196
+ O(n.navigationRequested, a.payload);
169
197
  break;
170
198
  case n.dirtyChanged:
171
- E(a.payload);
199
+ D(a.payload);
200
+ break;
201
+ case n.navigationFailed:
202
+ A(a.payload);
172
203
  break;
173
204
  default: break;
174
205
  }
175
- }, j = h(e);
176
- if (j) return console.error(`${m} ${j.message}`), e.onError?.(j), {
206
+ }, P = v(e);
207
+ if (P) return console.error(`${_} ${P.message}`), e.onError?.(P), {
177
208
  navigate: () => void 0,
178
209
  setTheme: () => void 0,
179
210
  destroy: () => void 0
180
211
  };
181
- let M = e.navigation ?? "self", N = window.location.origin, P;
212
+ let F = e.navigation ?? "self", I = window.location.origin, L;
182
213
  try {
183
- P = u({
214
+ L = u({
184
215
  declarionOrigin: e.declarionOrigin,
185
216
  route: e.route,
186
- parentOrigin: N,
187
- navigation: M,
217
+ parentOrigin: I,
218
+ navigation: F,
188
219
  theme: e.theme
189
220
  });
190
221
  } catch (t) {
191
- return y(i.invalidOptions, `Could not build the iframe URL from route "${e.route}". \`route\` must be a valid Declarion screen route.`, t), {
222
+ return b(i.invalidOptions, `Could not build the iframe URL from route "${e.route}". \`route\` must be a valid Declarion screen route.`, t), {
192
223
  navigate: () => void 0,
193
224
  setTheme: () => void 0,
194
225
  destroy: () => void 0
195
226
  };
196
227
  }
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(() => {
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.`);
228
+ return o = document.createElement("iframe"), o.src = L, o.title = e.title ?? p, o.setAttribute("sandbox", g(e.extraSandbox)), 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 = N, window.addEventListener("message", s), e.container.appendChild(o), h("iframe created", { src: L }), c = setTimeout(() => {
229
+ c = null, !l && b(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.`);
199
230
  }, f), {
200
231
  navigate(r) {
201
- t || b(n.navigate, { route: d(e.declarionOrigin, r) });
232
+ t || x(n.navigate, { route: d(e.declarionOrigin, r) });
202
233
  },
203
234
  setTheme(e) {
204
- t || b(n.setTheme, { theme: e });
235
+ t || x(n.setTheme, { theme: e });
205
236
  },
206
237
  destroy() {
207
- t || (t = !0, k(), s &&= (window.removeEventListener("message", s), null), o?.parentNode && o.parentNode.removeChild(o), o = null, v("embed destroyed"));
238
+ t || (t = !0, M(), s &&= (window.removeEventListener("message", s), null), o?.parentNode && o.parentNode.removeChild(o), o = null, h("embed destroyed"));
208
239
  }
209
240
  };
210
241
  }
211
242
  //#endregion
212
- export { n as a, e as i, i as n, t as o, a as r, _ as t };
243
+ export { n as a, e as i, i as n, t as o, a as r, b as t };
213
244
 
214
245
  //# sourceMappingURL=core.js.map
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 /**\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
+ {"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 /** iframe -> host: an in-frame navigation could not be handled (unknown\n * route, or a 404 the host should recover from). The iframe stayed put. */\n navigationFailed: \"navigation-failed\",\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 matched screen code, bound entity code, and record id.\n *\n * `screenCode`, `entity`, and `recordId` are optional: an unregistered path\n * resolves to none of them, a `custom` screen has no entity, and a list route\n * has no record id. The host needs `screenCode` to mint a scoped embed token\n * for the target screen when it executes a `delegated` navigation intent.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /**\n * The matched screen's code (its key in the schema screen map), when a\n * registered screen owns the route. Absent for an unresolved path. The host\n * passes this as `screen_code` to `auth.create_embed_session`.\n */\n readonly screenCode?: 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/**\n * `navigation-failed` payload: an in-frame navigation the iframe refused or\n * could not resolve. The iframe stayed put; the host owns recovery (show its\n * own not-found, reset its sidebar selection, etc.).\n *\n * `reason` is a closed set, each with a real producer: `not_found` from the\n * embedded 404 catch-all, `unresolved` from the navigation chokepoint when the\n * target route matches no registered screen.\n */\nexport interface EmbedNavigationFailedPayload {\n /** The route that could not be navigated to. */\n readonly route: string;\n /** Why it failed. */\n readonly reason: \"not_found\" | \"unresolved\";\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.navigationFailed]: EmbedNavigationFailedPayload;\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 EmbedNavigationFailedPayload,\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/**\n * Default iframe `sandbox` tokens. Comprehensive enough to run the full app\n * (scripts, same-origin storage/cookies, forms, modals, downloads, and popups\n * that escape the sandbox so external links open as normal windows), but\n * deliberately OMITTING the top-navigation tokens so the framed app cannot\n * navigate the host's top window.\n */\nconst DEFAULT_SANDBOX_TOKENS: readonly string[] = [\n \"allow-scripts\",\n \"allow-same-origin\",\n \"allow-forms\",\n \"allow-modals\",\n \"allow-downloads\",\n \"allow-popups\",\n \"allow-popups-to-escape-sandbox\",\n];\n\n/** Tokens a host may never add - they would defeat the top-nav containment. */\nconst FORBIDDEN_SANDBOX_TOKENS: ReadonlySet<string> = new Set([\n \"allow-top-navigation\",\n \"allow-top-navigation-by-user-activation\",\n]);\n\n/**\n * Build the iframe `sandbox` value: the default token set plus any host\n * `extraSandbox` tokens, with the forbidden top-navigation tokens stripped so\n * a host can widen the sandbox but never re-enable a top-frame escape.\n */\nfunction buildSandbox(extra?: readonly string[]): string {\n const tokens = new Set(DEFAULT_SANDBOX_TOKENS);\n for (const token of extra ?? []) {\n const trimmed = token.trim();\n if (trimmed && !FORBIDDEN_SANDBOX_TOKENS.has(trimmed)) tokens.add(trimmed);\n }\n return Array.from(tokens).join(\" \");\n}\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 screenCode: payload.screenCode,\n entity: payload.entity,\n recordId: payload.recordId,\n };\n options.onNavigate?.(event);\n };\n\n /**\n * Handle a `navigation-failed` frame: the iframe could not navigate to a\n * target (unknown route, or a 404) and stayed put. The host owns recovery.\n *\n * The envelope is already trusted (origin + source + protocol validated by\n * `classifyInboundMessage`); this is the only per-payload validator in the\n * SDK, scoped to this event - a malformed payload (missing route, unknown\n * reason) is dropped rather than surfaced, so a buggy or hostile frame\n * cannot hand the host an ill-typed object.\n */\n const VALID_NAV_FAILED_REASONS = new Set([\"not_found\", \"unresolved\"]);\n const onNavigationFailed = (payload: EmbedNavigationFailedPayload): void => {\n if (\n typeof payload?.route !== \"string\" ||\n payload.route.length === 0 ||\n !VALID_NAV_FAILED_REASONS.has(payload.reason)\n ) {\n logDebug(\"<- iframe navigation-failed (dropped: invalid payload)\", payload);\n return;\n }\n logDebug(\"<- iframe navigation-failed\", payload);\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.navigationFailed, payload });\n options.onNavigationFailed?.(payload);\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 case EMBED_MESSAGE_TYPES.navigationFailed:\n onNavigationFailed(message.payload as EmbedNavigationFailedPayload);\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 // Browser-enforced floor: the SDK owns iframe creation, so the sandbox\n // default lives here (not only in a reference host) - a consumer who never\n // copies the demo still gets it. The set OMITS `allow-top-navigation` and\n // `allow-top-navigation-by-user-activation`, so the framed app physically\n // cannot navigate the TOP window (Atlassian Forge's posture). It does NOT\n // stop the iframe navigating ITSELF - that is the in-app interceptor +\n // reverse-framing guard's job; this is the floor against top-frame hijack\n // and a backstop for anything that escapes. `allow-same-origin` is safe here\n // because the framed document is the Declarion origin, DIFFERENT from the\n // (cross-origin) host page, so the framed code cannot reach this iframe\n // element to drop its own sandbox. A host may ADD tokens via `extraSandbox`;\n // the top-navigation tokens can never be added back.\n iframe.setAttribute(\"sandbox\", buildSandbox(options.extraSandbox));\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),!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})({});
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`,navigationFailed:`navigation-failed`,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=[`allow-scripts`,`allow-same-origin`,`allow-forms`,`allow-modals`,`allow-downloads`,`allow-popups`,`allow-popups-to-escape-sandbox`],g=new Set([`allow-top-navigation`,`allow-top-navigation-by-user-activation`]);function _(e){let t=new Set(h);for(let n of e??[]){let e=n.trim();e&&!g.has(e)&&t.add(e)}return Array.from(t).join(` `)}var v=`[declarion-embed]`;function y(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 b(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 x(e){let t=!1,n=null,s=null,c=null,l=!1,u=!1,h=(t,n)=>{e.debug&&(n===void 0?console.info(`${v} ${t}`):console.info(`${v} ${t}`,n))},g=(t,n,r)=>{let i=new o(t,n,r?{cause:r}:void 0);console.error(`${v} ${n}`,r??``),e.onError?.(i)},x=(t,r)=>{if(!n?.contentWindow)return;let i={source:`declarion-embed`,protocol:1,type:t,payload:r};h(`-> iframe ${t}`,r),n.contentWindow.postMessage(i,e.declarionOrigin)},S=async()=>{let n;try{n=await e.getToken()}catch(e){g(a.getTokenFailed,"`getToken` rejected. The host backend must mint a token via auth.create_embed_session and return { token, expires_at }.",e);return}if(!b(n)){g(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};x(r.setToken,i),j()},C=()=>{l=!0,M(),h(`<- iframe ready`),e.onEvent?.({type:r.ready}),S()},w=()=>{h(`<- iframe token-expired`),e.onEvent?.({type:r.tokenExpired}),S()},T=t=>{h(`<- iframe reload-required`,t),e.onEvent?.({type:r.reloadRequired,payload:t}),g(a.reloadRequired,`The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`)},E=t=>{h(`<- 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})},D=t=>{h(`<- iframe dirty-changed`,t),e.onEvent?.({type:r.dirtyChanged,payload:t})},O=(t,n)=>{h(`<- iframe ${t}`,n),e.onEvent?.({type:t,payload:n});let i={mode:t===r.navigated?`self`:`delegated`,route:n.route,screenCode:n.screenCode,entity:n.entity,recordId:n.recordId};e.onNavigate?.(i)},k=new Set([`not_found`,`unresolved`]),A=t=>{if(typeof t?.route!=`string`||t.route.length===0||!k.has(t.reason)){h(`<- iframe navigation-failed (dropped: invalid payload)`,t);return}h(`<- iframe navigation-failed`,t),e.onEvent?.({type:r.navigationFailed,payload:t}),e.onNavigationFailed?.(t)},j=()=>{u||(u=!0,h(`handshake complete - iframe authenticated`),e.onReady?.())},M=()=>{c!==null&&(clearTimeout(c),c=null)},N=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(`${v} 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:C();break;case r.tokenExpired:w();break;case r.reloadRequired:T(o.payload);break;case r.resized:E(o.payload);break;case r.navigated:O(r.navigated,o.payload);break;case r.navigationRequested:O(r.navigationRequested,o.payload);break;case r.dirtyChanged:D(o.payload);break;case r.navigationFailed:A(o.payload);break;default:break}},P=y(e);if(P)return console.error(`${v} ${P.message}`),e.onError?.(P),{navigate:()=>void 0,setTheme:()=>void 0,destroy:()=>void 0};let F=e.navigation??`self`,I=window.location.origin,L;try{L=d({declarionOrigin:e.declarionOrigin,route:e.route,parentOrigin:I,navigation:F,theme:e.theme})}catch(t){return g(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=L,n.title=e.title??m,n.setAttribute(`sandbox`,_(e.extraSandbox)),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=N,window.addEventListener(`message`,s),e.container.appendChild(n),h(`iframe created`,{src:L}),c=setTimeout(()=>{c=null,!l&&g(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||x(r.navigate,{route:f(e.declarionOrigin,n)})},setTheme(e){t||x(r.setTheme,{theme:e})},destroy(){t||(t=!0,M(),s&&=(window.removeEventListener(`message`,s),null),n?.parentNode&&n.parentNode.removeChild(n),n=null,h(`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=x,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 /**\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
+ {"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 /** iframe -> host: an in-frame navigation could not be handled (unknown\n * route, or a 404 the host should recover from). The iframe stayed put. */\n navigationFailed: \"navigation-failed\",\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 matched screen code, bound entity code, and record id.\n *\n * `screenCode`, `entity`, and `recordId` are optional: an unregistered path\n * resolves to none of them, a `custom` screen has no entity, and a list route\n * has no record id. The host needs `screenCode` to mint a scoped embed token\n * for the target screen when it executes a `delegated` navigation intent.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /**\n * The matched screen's code (its key in the schema screen map), when a\n * registered screen owns the route. Absent for an unresolved path. The host\n * passes this as `screen_code` to `auth.create_embed_session`.\n */\n readonly screenCode?: 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/**\n * `navigation-failed` payload: an in-frame navigation the iframe refused or\n * could not resolve. The iframe stayed put; the host owns recovery (show its\n * own not-found, reset its sidebar selection, etc.).\n *\n * `reason` is a closed set, each with a real producer: `not_found` from the\n * embedded 404 catch-all, `unresolved` from the navigation chokepoint when the\n * target route matches no registered screen.\n */\nexport interface EmbedNavigationFailedPayload {\n /** The route that could not be navigated to. */\n readonly route: string;\n /** Why it failed. */\n readonly reason: \"not_found\" | \"unresolved\";\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.navigationFailed]: EmbedNavigationFailedPayload;\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 EmbedNavigationFailedPayload,\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/**\n * Default iframe `sandbox` tokens. Comprehensive enough to run the full app\n * (scripts, same-origin storage/cookies, forms, modals, downloads, and popups\n * that escape the sandbox so external links open as normal windows), but\n * deliberately OMITTING the top-navigation tokens so the framed app cannot\n * navigate the host's top window.\n */\nconst DEFAULT_SANDBOX_TOKENS: readonly string[] = [\n \"allow-scripts\",\n \"allow-same-origin\",\n \"allow-forms\",\n \"allow-modals\",\n \"allow-downloads\",\n \"allow-popups\",\n \"allow-popups-to-escape-sandbox\",\n];\n\n/** Tokens a host may never add - they would defeat the top-nav containment. */\nconst FORBIDDEN_SANDBOX_TOKENS: ReadonlySet<string> = new Set([\n \"allow-top-navigation\",\n \"allow-top-navigation-by-user-activation\",\n]);\n\n/**\n * Build the iframe `sandbox` value: the default token set plus any host\n * `extraSandbox` tokens, with the forbidden top-navigation tokens stripped so\n * a host can widen the sandbox but never re-enable a top-frame escape.\n */\nfunction buildSandbox(extra?: readonly string[]): string {\n const tokens = new Set(DEFAULT_SANDBOX_TOKENS);\n for (const token of extra ?? []) {\n const trimmed = token.trim();\n if (trimmed && !FORBIDDEN_SANDBOX_TOKENS.has(trimmed)) tokens.add(trimmed);\n }\n return Array.from(tokens).join(\" \");\n}\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 screenCode: payload.screenCode,\n entity: payload.entity,\n recordId: payload.recordId,\n };\n options.onNavigate?.(event);\n };\n\n /**\n * Handle a `navigation-failed` frame: the iframe could not navigate to a\n * target (unknown route, or a 404) and stayed put. The host owns recovery.\n *\n * The envelope is already trusted (origin + source + protocol validated by\n * `classifyInboundMessage`); this is the only per-payload validator in the\n * SDK, scoped to this event - a malformed payload (missing route, unknown\n * reason) is dropped rather than surfaced, so a buggy or hostile frame\n * cannot hand the host an ill-typed object.\n */\n const VALID_NAV_FAILED_REASONS = new Set([\"not_found\", \"unresolved\"]);\n const onNavigationFailed = (payload: EmbedNavigationFailedPayload): void => {\n if (\n typeof payload?.route !== \"string\" ||\n payload.route.length === 0 ||\n !VALID_NAV_FAILED_REASONS.has(payload.reason)\n ) {\n logDebug(\"<- iframe navigation-failed (dropped: invalid payload)\", payload);\n return;\n }\n logDebug(\"<- iframe navigation-failed\", payload);\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.navigationFailed, payload });\n options.onNavigationFailed?.(payload);\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 case EMBED_MESSAGE_TYPES.navigationFailed:\n onNavigationFailed(message.payload as EmbedNavigationFailedPayload);\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 // Browser-enforced floor: the SDK owns iframe creation, so the sandbox\n // default lives here (not only in a reference host) - a consumer who never\n // copies the demo still gets it. The set OMITS `allow-top-navigation` and\n // `allow-top-navigation-by-user-activation`, so the framed app physically\n // cannot navigate the TOP window (Atlassian Forge's posture). It does NOT\n // stop the iframe navigating ITSELF - that is the in-app interceptor +\n // reverse-framing guard's job; this is the floor against top-frame hijack\n // and a backstop for anything that escapes. `allow-same-origin` is safe here\n // because the framed document is the Declarion origin, DIFFERENT from the\n // (cross-origin) host page, so the framed code cannot reach this iframe\n // element to drop its own sandbox. A host may ADD tokens via `extraSandbox`;\n // the top-navigation tokens can never be added back.\n iframe.setAttribute(\"sandbox\", buildSandbox(options.extraSandbox));\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":"6FA+BA,IAAa,EAAuB,kBAOvB,EAAyB,EAQzB,EAAsB,CAEjC,MAAO,QAEP,SAAU,YAEV,aAAc,gBAEd,eAAgB,kBAEhB,QAAS,UAET,UAAW,YAEX,oBAAqB,uBAErB,aAAc,gBAGd,iBAAkB,oBAElB,SAAU,WAEV,SAAU,WACZ,ECtBA,SAAgB,EACd,EACA,EACuB,CAGvB,GAAI,EAAM,SAAW,EACnB,MAAO,CAAE,KAAM,UAAW,EAK5B,IAAM,EAAO,EAAM,KACnB,GAAI,OAAO,GAAS,WAAY,EAC9B,MAAO,CAAE,KAAM,UAAW,EAE5B,IAAM,EAAW,EAiBjB,OAhBI,EAAS,SAAA,mBAGT,OAAO,EAAS,UAAa,UAG7B,OAAO,EAAS,MAAS,SACpB,CAAE,KAAM,UAAW,EAKxB,EAAS,WAAA,EAIN,CAAE,KAAM,QAAS,QAAS,CAAyB,EAHjD,CAAE,KAAM,oBAAqB,SAAU,EAAS,QAAS,CAIpE,CC/DA,IAAa,EAAoB,CAM/B,eAAgB,kBAKhB,eAAgB,mBAQhB,iBAAkB,oBAKlB,eAAgB,iBAClB,EAca,EAAb,MAAa,UAAmB,KAAM,CAEpC,KAEA,YAAY,EAAsB,EAAiB,EAA+B,CAChF,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,aACZ,KAAK,KAAO,EAGZ,OAAO,eAAe,KAAM,EAAW,SAAS,CAClD,CACF,EC3Da,EAAoB,QACpB,EAA4B,gBAC5B,EAAoB,QAI3B,EAA4B,IAgClC,SAAgB,EAAc,EAAmC,CAI/D,IAAM,EAAM,IAAI,IAAI,EAAM,MAAO,EAAM,eAAe,EAOtD,OANA,EAAI,aAAa,IAAI,EAAmB,CAAyB,EACjE,EAAI,aAAa,IAAI,EAA2B,EAAM,YAAY,EAClE,EAAI,aAAa,IAAA,MAAqB,EAAM,UAAU,EAClD,EAAM,OACR,EAAI,aAAa,IAAI,EAAmB,EAAM,KAAK,EAE9C,EAAI,SAAS,CACtB,CASA,SAAgB,EAAa,EAAyB,EAAuB,CAC3E,GAAI,CACF,IAAM,EAAM,IAAI,IAAI,EAAO,CAAe,EAG1C,OAAO,EAAI,SAAW,EAAI,OAAS,EAAI,IACzC,MAAQ,CACN,OAAO,CACT,CACF,CClCA,IAAM,EAAuB,IAGvB,EAAuB,4BASvB,EAA4C,CAChD,gBACA,oBACA,cACA,eACA,kBACA,eACA,gCACF,EAGM,EAAgD,IAAI,IAAI,CAC5D,uBACA,yCACF,CAAC,EAOD,SAAS,EAAa,EAAmC,CACvD,IAAM,EAAS,IAAI,IAAI,CAAsB,EAC7C,IAAK,IAAM,KAAS,GAAS,CAAC,EAAG,CAC/B,IAAM,EAAU,EAAM,KAAK,EACvB,GAAW,CAAC,EAAyB,IAAI,CAAO,GAAG,EAAO,IAAI,CAAO,CAC3E,CACA,OAAO,MAAM,KAAK,CAAM,EAAE,KAAK,GAAG,CACpC,CAGA,IAAM,EAAa,oBASnB,SAAS,EAAgB,EAAmD,CAC1E,IAAM,EAAQ,GACZ,IAAI,EAAW,EAAkB,eAAgB,CAAO,EAE1D,GAAI,CAAC,EAAQ,WAAa,OAAO,EAAQ,UAAU,aAAgB,WACjE,OAAO,EACL,gEACF,EAEF,GAAI,OAAO,EAAQ,iBAAoB,UAAY,EAAQ,kBAAoB,GAC7E,OAAO,EACL,yHAEF,EAEF,IAAI,EACJ,GAAI,CACF,EAAe,IAAI,IAAI,EAAQ,eAAe,CAChD,MAAQ,CACN,OAAO,EACL,4CAA4C,EAAQ,gBAAgB,yDAEtE,CACF,CAiBA,OAhBI,EAAa,SAAW,EAAQ,gBAOhC,OAAO,EAAQ,OAAU,UAAY,EAAQ,QAAU,GAClD,EAAK,2DAA2D,EAErE,OAAO,EAAQ,UAAa,WAMzB,KALE,EACL,uFAEF,EAbO,EACL,8FAC6B,EAAQ,gBAAgB,eAC/C,EAAa,OAAO,GAC5B,CAYJ,CAMA,SAAS,EAAa,EAAqC,CACzD,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAQ,EACd,OACE,OAAO,EAAM,OAAU,UACvB,EAAM,QAAU,IAChB,OAAO,EAAM,YAAe,UAC5B,EAAM,aAAe,EAEzB,CAUA,SAAgB,EACd,EACsB,CACtB,IAAI,EAAY,GACZ,EAAmC,KACnC,EAA0D,KAC1D,EAAuD,KACvD,EAAgB,GAChB,EAAqB,GAGnB,GAAY,EAAiB,IAA2B,CACvD,EAAQ,QACT,IAAW,IAAA,GACb,QAAQ,KAAK,GAAG,EAAW,GAAG,GAAS,EAEvC,QAAQ,KAAK,GAAG,EAAW,GAAG,IAAW,CAAM,EAEnD,EAMM,GACJ,EACA,EACA,IACS,CACT,IAAM,EAAQ,IAAI,EAAW,EAAM,EAAS,EAAQ,CAAE,OAAM,EAAI,IAAA,EAAS,EACzE,QAAQ,MAAM,GAAG,EAAW,GAAG,IAAW,GAAS,EAAE,EACrD,EAAQ,UAAU,CAAK,CACzB,EAGM,GACJ,EACA,IACS,CACT,GAAI,CAAC,GAAQ,cAAe,OAC5B,IAAM,EAA2B,CAC/B,OAAQ,kBACR,SAAA,EACA,OACA,SACF,EACA,EAAS,aAAa,IAAQ,CAAO,EACrC,EAAO,cAAc,YAAY,EAAS,EAAQ,eAAe,CACnE,EAOM,EAAyB,SAA2B,CACxD,IAAI,EACJ,GAAI,CACF,EAAS,MAAM,EAAQ,SAAS,CAClC,OAAS,EAAO,CACd,EACE,EAAkB,eAClB,0HAEA,CACF,EACA,MACF,CACA,GAAI,CAAC,EAAa,CAAM,EAAG,CACzB,EACE,EAAkB,eAClB,2FAEF,EACA,MACF,CACA,GAAI,EAAW,OACf,IAAM,EAAgC,CACpC,MAAO,EAAO,MACd,WAAY,EAAO,UACrB,EACA,EAAa,EAAoB,SAAU,CAAO,EAClD,EAAgB,CAClB,EAGM,MAAsB,CAC1B,EAAgB,GAChB,EAAoB,EACpB,EAAS,iBAAiB,EAC1B,EAAQ,UAAU,CAAE,KAAM,EAAoB,KAAM,CAAC,EACrD,EAA4B,CAC9B,EAGM,MAA6B,CACjC,EAAS,yBAAyB,EAClC,EAAQ,UAAU,CAAE,KAAM,EAAoB,YAAa,CAAC,EAC5D,EAA4B,CAC9B,EAGM,EAAoB,GAA8C,CACtE,EAAS,4BAA6B,CAAO,EAC7C,EAAQ,UAAU,CAChB,KAAM,EAAoB,eAC1B,SACF,CAAC,EACD,EACE,EAAkB,eAClB,2CAA2C,EAAQ,OAAO,gCAE5D,CACF,EAOM,EAAY,GAAuC,CACvD,EAAS,oBAAqB,CAAO,EACjC,CAAC,EAAQ,QAAU,GAAU,OAAO,SAAS,EAAQ,MAAM,GAAK,EAAQ,OAAS,IACnF,EAAO,MAAM,OAAS,GAAG,EAAQ,OAAO,KAE1C,EAAQ,UAAU,CAAE,KAAM,EAAoB,QAAS,SAAQ,CAAC,CAClE,EAQM,EAAkB,GAA4C,CAClE,EAAS,0BAA2B,CAAO,EAC3C,EAAQ,UAAU,CAAE,KAAM,EAAoB,aAAc,SAAQ,CAAC,CACvE,EAOM,GACJ,EAGA,IACS,CACT,EAAS,aAAa,IAAQ,CAAO,EACrC,EAAQ,UAAU,CAAE,OAAM,SAAQ,CAAC,EACnC,IAAM,EAA4B,CAChC,KAAM,IAAS,EAAoB,UAAY,OAAS,YACxD,MAAO,EAAQ,MACf,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,SAAU,EAAQ,QACpB,EACA,EAAQ,aAAa,CAAK,CAC5B,EAYM,EAA2B,IAAI,IAAI,CAAC,YAAa,YAAY,CAAC,EAC9D,EAAsB,GAAgD,CAC1E,GACE,OAAO,GAAS,OAAU,UAC1B,EAAQ,MAAM,SAAW,GACzB,CAAC,EAAyB,IAAI,EAAQ,MAAM,EAC5C,CACA,EAAS,yDAA0D,CAAO,EAC1E,MACF,CACA,EAAS,8BAA+B,CAAO,EAC/C,EAAQ,UAAU,CAAE,KAAM,EAAoB,iBAAkB,SAAQ,CAAC,EACzE,EAAQ,qBAAqB,CAAO,CACtC,EAQM,MAA8B,CAC7B,IACH,EAAqB,GACrB,EAAS,2CAA2C,EACpD,EAAQ,UAAU,EAEtB,EAGM,MAAkC,CAClC,IAAmB,OACrB,aAAa,CAAc,EAC3B,EAAiB,KAErB,EAOM,EAAiB,GAA8B,CAQnD,GAAI,CAAC,GAAU,EAAM,SAAW,EAAO,cACrC,OAGF,IAAM,EAAa,EAAuB,EAAO,EAAQ,eAAe,EAExE,GAAI,EAAW,OAAS,WAGtB,OAGF,GAAI,EAAW,OAAS,oBAAqB,CAG3C,QAAQ,KACN,GAAG,EAAW,uFAET,OAAO,EAAW,QAAQ,EAAE,6EAEnC,EACA,MACF,CAEA,GAAM,CAAE,WAAY,EACpB,OAAQ,EAAQ,KAAhB,CACE,KAAK,EAAoB,MACvB,EAAQ,EACR,MACF,KAAK,EAAoB,aACvB,EAAe,EACf,MACF,KAAK,EAAoB,eACvB,EAAiB,EAAQ,OAAqC,EAC9D,MACF,KAAK,EAAoB,QACvB,EAAS,EAAQ,OAA8B,EAC/C,MACF,KAAK,EAAoB,UACvB,EACE,EAAoB,UACpB,EAAQ,OACV,EACA,MACF,KAAK,EAAoB,oBACvB,EACE,EAAoB,oBACpB,EAAQ,OACV,EACA,MACF,KAAK,EAAoB,aACvB,EAAe,EAAQ,OAAmC,EAC1D,MACF,KAAK,EAAoB,iBACvB,EAAmB,EAAQ,OAAuC,EAClE,MACF,QAGE,KACJ,CACF,EAIM,EAAe,EAAgB,CAAO,EAC5C,GAAI,EAKF,OAJA,QAAQ,MAAM,GAAG,EAAW,GAAG,EAAa,SAAS,EACrD,EAAQ,UAAU,CAAY,EAGvB,CACL,aAAgB,IAAA,GAChB,aAAgB,IAAA,GAChB,YAAe,IAAA,EACjB,EAGF,IAAM,EAAa,EAAQ,YAAA,OAGrB,EAAe,OAAO,SAAS,OAEjC,EACJ,GAAI,CACF,EAAM,EAAc,CAClB,gBAAiB,EAAQ,gBACzB,MAAO,EAAQ,MACf,eACA,aACA,MAAO,EAAQ,KACjB,CAAC,CACH,OAAS,EAAO,CAOd,OANA,EACE,EAAkB,eAClB,8CAA8C,EAAQ,MAAM,sDAE5D,CACF,EACO,CACL,aAAgB,IAAA,GAChB,aAAgB,IAAA,GAChB,YAAe,IAAA,EACjB,CACF,CAmDA,MAjDA,GAAS,SAAS,cAAc,QAAQ,EACxC,EAAO,IAAM,EACb,EAAO,MAAQ,EAAQ,OAAS,EAahC,EAAO,aAAa,UAAW,EAAa,EAAQ,YAAY,CAAC,EACjE,EAAO,MAAM,MAAQ,OACrB,EAAO,MAAM,OAAS,IACtB,EAAO,MAAM,QAAU,QAInB,EAAQ,OACV,EAAO,MAAM,OAAS,EAAQ,OACpB,EAAO,MAAM,SACvB,EAAO,MAAM,OAAS,SAGxB,EAAkB,EAClB,OAAO,iBAAiB,UAAW,CAAe,EAClD,EAAQ,UAAU,YAAY,CAAM,EACpC,EAAS,iBAAkB,CAAE,KAAI,CAAC,EAKlC,EAAiB,eAAiB,CAChC,EAAiB,KACb,IACJ,EACE,EAAkB,iBAClB,uCAAuC,EAAqB,sNAK9D,CACF,EAAG,CAAoB,EAEhB,CACL,SAAS,EAAqB,CACxB,GAEJ,EAAa,EAAoB,SAAU,CACzC,MAAO,EAAa,EAAQ,gBAAiB,CAAK,CACpD,CAAC,CACH,EACA,SAAS,EAAa,CAChB,GACJ,EAAa,EAAoB,SAAU,CAAE,OAAM,CAAC,CACtD,EACA,SAAgB,CACV,IACJ,EAAY,GACZ,EAAoB,EACpB,AAEE,KADA,OAAO,oBAAoB,UAAW,CAAe,EACnC,MAEhB,GAAQ,YACV,EAAO,WAAW,YAAY,CAAM,EAEtC,EAAS,KACT,EAAS,iBAAiB,EAC5B,CACF,CACF"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export type { DeclarionEmbedOptions, DeclarionEmbedHandle, EmbedNavigationContra
3
3
  export { EmbedError, EMBED_ERROR_CODES } from "./errors";
4
4
  export type { EmbedErrorCode } from "./errors";
5
5
  export { EMBED_MESSAGE_SOURCE, EMBED_MESSAGE_TYPES, EMBED_PROTOCOL_VERSION, } from "./protocol";
6
- export type { EmbedMessage, EmbedMessageType, EmbedMessagePayloadMap, EmbedReadyPayload, EmbedSetTokenPayload, EmbedTokenExpiredPayload, EmbedReloadRequiredPayload, EmbedResizedPayload, EmbedNavigationPayload, EmbedDirtyChangedPayload, EmbedNavigatePayload, EmbedSetThemePayload, } from "./protocol";
6
+ export type { EmbedMessage, EmbedMessageType, EmbedMessagePayloadMap, EmbedReadyPayload, EmbedSetTokenPayload, EmbedTokenExpiredPayload, EmbedReloadRequiredPayload, EmbedResizedPayload, EmbedNavigationPayload, EmbedDirtyChangedPayload, EmbedNavigationFailedPayload, EmbedNavigatePayload, EmbedSetThemePayload, } from "./protocol";
@@ -33,6 +33,9 @@ export declare const EMBED_MESSAGE_TYPES: {
33
33
  readonly navigationRequested: "navigation-requested";
34
34
  /** iframe -> host: the embedded screen's unsaved-edits state flipped. */
35
35
  readonly dirtyChanged: "dirty-changed";
36
+ /** iframe -> host: an in-frame navigation could not be handled (unknown
37
+ * route, or a 404 the host should recover from). The iframe stayed put. */
38
+ readonly navigationFailed: "navigation-failed";
36
39
  /** host -> iframe: drive the iframe to a screen (deep-linking). */
37
40
  readonly navigate: "navigate";
38
41
  /** host -> iframe: runtime theme switch. */
@@ -63,14 +66,22 @@ export interface EmbedResizedPayload {
63
66
  }
64
67
  /**
65
68
  * `navigated` / `navigation-requested` payload: the screen route and, when
66
- * resolvable, the bound entity code and record id.
69
+ * resolvable, the matched screen code, bound entity code, and record id.
67
70
  *
68
- * `entity` and `recordId` are optional: a `custom` screen has no entity, and
69
- * a list route has no record id.
71
+ * `screenCode`, `entity`, and `recordId` are optional: an unregistered path
72
+ * resolves to none of them, a `custom` screen has no entity, and a list route
73
+ * has no record id. The host needs `screenCode` to mint a scoped embed token
74
+ * for the target screen when it executes a `delegated` navigation intent.
70
75
  */
71
76
  export interface EmbedNavigationPayload {
72
77
  /** The Declarion screen route path the navigation targets. */
73
78
  readonly route: string;
79
+ /**
80
+ * The matched screen's code (its key in the schema screen map), when a
81
+ * registered screen owns the route. Absent for an unresolved path. The host
82
+ * passes this as `screen_code` to `auth.create_embed_session`.
83
+ */
84
+ readonly screenCode?: string;
74
85
  /** The bound entity code, when the route resolves to one. */
75
86
  readonly entity?: string;
76
87
  /** The record id, when the route is a detail route with an id. */
@@ -86,6 +97,21 @@ export interface EmbedDirtyChangedPayload {
86
97
  /** True when the embedded screen has unsaved edits. */
87
98
  readonly dirty: boolean;
88
99
  }
100
+ /**
101
+ * `navigation-failed` payload: an in-frame navigation the iframe refused or
102
+ * could not resolve. The iframe stayed put; the host owns recovery (show its
103
+ * own not-found, reset its sidebar selection, etc.).
104
+ *
105
+ * `reason` is a closed set, each with a real producer: `not_found` from the
106
+ * embedded 404 catch-all, `unresolved` from the navigation chokepoint when the
107
+ * target route matches no registered screen.
108
+ */
109
+ export interface EmbedNavigationFailedPayload {
110
+ /** The route that could not be navigated to. */
111
+ readonly route: string;
112
+ /** Why it failed. */
113
+ readonly reason: "not_found" | "unresolved";
114
+ }
89
115
  /** `navigate` payload: the route the host drives the iframe to. */
90
116
  export interface EmbedNavigatePayload {
91
117
  /** The Declarion screen route the host wants opened. */
@@ -112,6 +138,7 @@ export interface EmbedMessagePayloadMap {
112
138
  [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;
113
139
  [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;
114
140
  [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;
141
+ [EMBED_MESSAGE_TYPES.navigationFailed]: EmbedNavigationFailedPayload;
115
142
  [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;
116
143
  [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;
117
144
  }
package/dist/react.js CHANGED
@@ -7,7 +7,8 @@ function c(e) {
7
7
  e.declarionOrigin,
8
8
  e.route,
9
9
  e.navigation ?? "self",
10
- e.height ?? ""
10
+ e.height ?? "",
11
+ (e.extraSandbox ?? []).join(",")
11
12
  ].join("\0");
12
13
  }
13
14
  function l(e, t) {
@@ -24,10 +25,12 @@ function l(e, t) {
24
25
  theme: f.current.theme,
25
26
  title: f.current.title,
26
27
  debug: f.current.debug,
28
+ extraSandbox: f.current.extraSandbox,
27
29
  getToken: () => f.current.getToken(),
28
30
  onReady: () => f.current.onReady?.(),
29
31
  onError: (e) => f.current.onError?.(e),
30
32
  onNavigate: (e) => f.current.onNavigate?.(e),
33
+ onNavigationFailed: (e) => f.current.onNavigationFailed?.(e),
31
34
  onEvent: (e) => f.current.onEvent?.(e)
32
35
  });
33
36
  return d.current = t, () => {
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 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"}
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 // extraSandbox sets the iframe sandbox attribute at build time, so a\n // change requires a fresh iframe.\n (props.extraSandbox ?? []).join(\",\"),\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 // Static sandbox extension is read at build time (it sets the iframe\n // sandbox attribute); a change rebuilds via rebuildKey.\n extraSandbox: propsRef.current.extraSandbox,\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 onNavigationFailed: (payload) =>\n propsRef.current.onNavigationFailed?.(payload),\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;GAGf,EAAM,gBAAgB,CAAC,GAAG,KAAK,GAAG;CACrC,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;CA+D7B,OA9DA,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,cAAc,EAAS,QAAQ;GAG/B,gBAAgB,EAAS,QAAQ,SAAS;GAC1C,eAAe,EAAS,QAAQ,UAAU;GAC1C,UAAU,MAAU,EAAS,QAAQ,UAAU,CAAK;GACpD,aAAa,MAAU,EAAS,QAAQ,aAAa,CAAK;GAC1D,qBAAqB,MACnB,EAAS,QAAQ,qBAAqB,CAAO;GAC/C,UAAU,MAAU,EAAS,QAAQ,UAAU,CAAK;EACtD,CAAC;EAGD,OAFA,EAAU,UAAU,SAEP;GAEX,AADA,EAAO,QAAQ,GACf,EAAU,UAAU;EACtB;CAGF,GAAG,CAvCS,EAAW,CAuCnB,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/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { EmbedDirtyChangedPayload, EmbedNavigationPayload, EmbedReloadRequiredPayload, EmbedResizedPayload, EmbedTheme } from "./protocol";
1
+ import type { EmbedDirtyChangedPayload, EmbedNavigationFailedPayload, EmbedNavigationPayload, EmbedReloadRequiredPayload, EmbedResizedPayload, EmbedTheme } from "./protocol";
2
2
  import type { EmbedError } from "./errors";
3
3
  /**
4
4
  * The navigation contract for an embedded iframe (Section 1, Decision 16).
@@ -53,6 +53,9 @@ export type EmbedEvent = {
53
53
  } | {
54
54
  readonly type: "dirty-changed";
55
55
  readonly payload: EmbedDirtyChangedPayload;
56
+ } | {
57
+ readonly type: "navigation-failed";
58
+ readonly payload: EmbedNavigationFailedPayload;
56
59
  };
57
60
  /**
58
61
  * A navigation event surfaced to the `onNavigate` callback. `mode` records
@@ -65,6 +68,12 @@ export interface EmbedNavigateEvent {
65
68
  readonly mode: EmbedNavigationContract;
66
69
  /** The Declarion screen route the navigation targets. */
67
70
  readonly route: string;
71
+ /**
72
+ * The matched screen's code, when a registered screen owns the route. A
73
+ * `delegated` host passes this as `screen_code` to
74
+ * `auth.create_embed_session` to mint a scoped token for the target screen.
75
+ */
76
+ readonly screenCode?: string;
68
77
  /** The bound entity code, when the route resolves to one. */
69
78
  readonly entity?: string;
70
79
  /** The record id, when the route is a detail route with an id. */
@@ -99,6 +108,15 @@ export interface DeclarionEmbedOptions {
99
108
  readonly theme?: EmbedTheme;
100
109
  /** The iframe `title` attribute. Defaults to a sensible label. */
101
110
  readonly title?: string;
111
+ /**
112
+ * Extra iframe `sandbox` tokens to ADD to the SDK default. The default set
113
+ * runs the full app while omitting the top-navigation tokens, so the framed
114
+ * app cannot navigate the host's top window. The top-navigation tokens
115
+ * (`allow-top-navigation`, `allow-top-navigation-by-user-activation`) are
116
+ * stripped from this list - a host can widen the sandbox but never re-enable
117
+ * a top-frame escape.
118
+ */
119
+ readonly extraSandbox?: readonly string[];
102
120
  /**
103
121
  * A fixed iframe height (any CSS length, e.g. `"640px"` or `"75vh"`).
104
122
  *
@@ -125,6 +143,12 @@ export interface DeclarionEmbedOptions {
125
143
  readonly onError?: (error: EmbedError) => void;
126
144
  /** Fired for `navigated` (`self`) and `navigation-requested` (`delegated`). */
127
145
  readonly onNavigate?: (event: EmbedNavigateEvent) => void;
146
+ /**
147
+ * Fired when the embedded screen could not navigate to a target (an unknown
148
+ * route, or a 404). The iframe stayed put; the host owns recovery - show its
149
+ * own not-found, reset its sidebar selection, etc. Never break the frame.
150
+ */
151
+ readonly onNavigationFailed?: (payload: EmbedNavigationFailedPayload) => void;
128
152
  /** Generic callback fired for every inbound protocol event. */
129
153
  readonly onEvent?: (event: EmbedEvent) => void;
130
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declarion/embed",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Host integration SDK for embedding Declarion screens as white-label iframes.",