@declarion/embed 0.6.3 → 0.6.6

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
@@ -43,6 +43,26 @@ least-privilege `dk:_global` key whose service-account owner holds only
43
43
  `action:auth.create_embed_session`; tenant-scoped keys can mint only in their
44
44
  own tenant.
45
45
 
46
+ ### Token model and the persistent iframe
47
+
48
+ The embed token is minted per **(user, tenant)** and carries that user's full
49
+ effective permissions. ONE token serves every screen the user can open - it is
50
+ NOT per screen. Mint it once per identity and reuse it; `getToken` should be
51
+ bound to the identity, not the current screen.
52
+
53
+ Switching screens is fast because the iframe is **persistent**: changing the
54
+ `route` (the `route` prop, or `handle.navigate(route)`) posts a soft
55
+ SPA-navigation to the existing iframe. The app keeps its loaded schema and
56
+ session - it does NOT reload or re-fetch the schema per screen. The first load
57
+ fetches the schema once; later screen switches are frontend-speed.
58
+
59
+ Changing the embedded **identity** (a different user or tenant) MUST rebuild
60
+ the iframe, because the in-iframe schema cache is authority-shaped. With the
61
+ React binding, pass a stable `identityKey` (e.g. `` `${tenant}|${user}` ``);
62
+ changing it rebuilds the iframe and re-mints. With the core, create a new embed
63
+ for the new identity. Per-screen access is enforced inside the iframe at
64
+ navigate time, never by narrowing the token.
65
+
46
66
  ## Quickstart: browser core
47
67
 
48
68
  ```ts
@@ -66,11 +86,15 @@ const embed = createDeclarionEmbed({
66
86
  ```tsx
67
87
  import { DeclarionEmbed } from "@declarion/embed/react";
68
88
 
69
- export function CasesPanel() {
89
+ export function CasesPanel({ tenant, user, route }) {
70
90
  return (
71
91
  <DeclarionEmbed
72
92
  declarionOrigin="https://app.example.com"
73
- route="/cases"
93
+ route={route}
94
+ // Stable per identity. Changing `route` soft-navigates the SAME iframe;
95
+ // changing `identityKey` rebuilds it (a different user must not reuse the
96
+ // prior user's authority-shaped schema).
97
+ identityKey={`${tenant}|${user}`}
74
98
  getToken={async () => (await fetch("/host-api/embed-token")).json()}
75
99
  onError={(err) => console.error(err.code, err.message)}
76
100
  />
@@ -114,18 +138,23 @@ The package also ships an IIFE bundle of the core. Load it and use
114
138
 
115
139
  ## Navigation modes
116
140
 
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.
122
- - `navigation: "delegated"` - the iframe never moves; it emits
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.
141
+ - `navigation: "self"` (default) - the iframe routes EVERY in-frame link
142
+ internally (the token is identity-scoped, so the iframe is already authorized
143
+ to open anywhere the user can go) and emits
144
+ `onNavigate({ mode: "self", route, screenCode, ... })` so the host can mirror
145
+ its URL.
146
+ - `navigation: "delegated"` - the iframe never moves on an in-frame link; it
147
+ emits `onNavigate({ mode: "delegated", route, screenCode, ... })` and the
148
+ host decides what to open (drive it back via the `route` prop /
149
+ `handle.navigate`). Use this to pin the embed to a single screen.
126
150
 
127
151
  The `onNavigate` event carries `{ mode, route, screenCode?, entity?, recordId? }`.
128
152
 
153
+ A host-driven `navigate` (the `route` prop changing, or `handle.navigate`) is a
154
+ deep-link command: it MOVES the iframe in BOTH modes (it is the host asking),
155
+ as a soft SPA navigation - only in-frame LINKS are intercepted/delegated. The
156
+ same token authorizes every screen, so no re-mint is needed to switch.
157
+
129
158
  ## Navigation failures
130
159
 
131
160
  When an in-frame navigation cannot be handled - an unknown route, or a 404 -
@@ -170,9 +199,22 @@ createDeclarionEmbed({
170
199
  if (embedDirty && !confirm("Discard unsaved changes?")) return;
171
200
  ```
172
201
 
173
- Navigation *inside* the iframe (a row click, a Back button) is already guarded
174
- by Declarion itself; this is only for navigation the host initiates, which the
175
- iframe cannot intercept.
202
+ Navigation *inside* the iframe (a row click, a Back button) AND a host-driven
203
+ `route` change / `handle.navigate` are both already guarded by Declarion: the
204
+ iframe shows its own confirm dialog before discarding edits. The `dirty-changed`
205
+ event is for host actions the iframe CANNOT intercept - the host leaving the
206
+ embed via its own page navigation, or replacing the embed on an identity
207
+ switch. Use it to guard those.
208
+
209
+ ## Staying current while embedded
210
+
211
+ When the Declarion deployment is redeployed while a screen is embedded, the
212
+ iframe does NOT show Declarion's own reload chrome (that would be wrong inside a
213
+ white-label host). Instead it asks the host to reload it via a `reload-required`
214
+ event (surfaced through `onError` with code `reload-required` and through
215
+ `onEvent`). The host decides: reload the iframe, show its own banner, or ignore.
216
+ The persistent iframe otherwise keeps its loaded schema across screen switches
217
+ and only re-validates it on a real version change or SSE session refresh.
176
218
 
177
219
  ## Diagnostics
178
220
 
package/dist/core.js CHANGED
@@ -90,12 +90,12 @@ function y(e) {
90
90
  return typeof t.token == "string" && t.token !== "" && typeof t.expires_at == "string" && t.expires_at !== "";
91
91
  }
92
92
  function b(e) {
93
- let t = !1, o = null, s = null, c = null, l = !1, m = !1, h = (t, n) => {
93
+ let t = !1, o = null, s = null, c = null, l = !1, m = !1, h = null, b = (t, n) => {
94
94
  e.debug && (n === void 0 ? console.info(`${_} ${t}`) : console.info(`${_} ${t}`, n));
95
- }, b = (t, n, r) => {
95
+ }, x = (t, n, r) => {
96
96
  let i = new a(t, n, r ? { cause: r } : void 0);
97
97
  console.error(`${_} ${n}`, r ?? ""), e.onError?.(i);
98
- }, x = (t, n) => {
98
+ }, S = (t, n) => {
99
99
  if (!o?.contentWindow) return;
100
100
  let r = {
101
101
  source: "declarion-embed",
@@ -103,17 +103,17 @@ function b(e) {
103
103
  type: t,
104
104
  payload: n
105
105
  };
106
- h(`-> iframe ${t}`, n), o.contentWindow.postMessage(r, e.declarionOrigin);
107
- }, S = async () => {
106
+ b(`-> iframe ${t}`, n), o.contentWindow.postMessage(r, e.declarionOrigin);
107
+ }, C = async () => {
108
108
  let r;
109
109
  try {
110
110
  r = await e.getToken();
111
111
  } catch (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);
112
+ x(i.getTokenFailed, "`getToken` rejected. The host backend must mint a token via auth.create_embed_session and return { token, expires_at }.", e);
113
113
  return;
114
114
  }
115
115
  if (!y(r)) {
116
- b(i.getTokenFailed, "`getToken` resolved to a malformed value. Expected { token: string, expires_at: string }.");
116
+ x(i.getTokenFailed, "`getToken` resolved to a malformed value. Expected { token: string, expires_at: string }.");
117
117
  return;
118
118
  }
119
119
  if (t) return;
@@ -121,28 +121,33 @@ function b(e) {
121
121
  token: r.token,
122
122
  expires_at: r.expires_at
123
123
  };
124
- x(n.setToken, a), j();
125
- }, C = () => {
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?.({
124
+ S(n.setToken, a), N();
125
+ }, w = (t) => {
126
+ S(n.navigate, { route: d(e.declarionOrigin, t) });
127
+ }, T = () => {
128
+ if (l = !0, P(), b("<- iframe ready"), e.onEvent?.({ type: n.ready }), C(), h !== null) {
129
+ let e = h;
130
+ h = null, w(e);
131
+ }
132
+ }, E = () => {
133
+ b("<- iframe token-expired"), e.onEvent?.({ type: n.tokenExpired }), C();
134
+ }, D = (t) => {
135
+ b("<- iframe reload-required", t), e.onEvent?.({
131
136
  type: n.reloadRequired,
132
137
  payload: t
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?.({
138
+ }), x(i.reloadRequired, `The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`);
139
+ }, O = (t) => {
140
+ b("<- iframe resized", t), !e.height && o && Number.isFinite(t.height) && t.height > 0 && (o.style.height = `${t.height}px`), e.onEvent?.({
136
141
  type: n.resized,
137
142
  payload: t
138
143
  });
139
- }, D = (t) => {
140
- h("<- iframe dirty-changed", t), e.onEvent?.({
144
+ }, k = (t) => {
145
+ b("<- iframe dirty-changed", t), e.onEvent?.({
141
146
  type: n.dirtyChanged,
142
147
  payload: t
143
148
  });
144
- }, O = (t, r) => {
145
- h(`<- iframe ${t}`, r), e.onEvent?.({
149
+ }, A = (t, r) => {
150
+ b(`<- iframe ${t}`, r), e.onEvent?.({
146
151
  type: t,
147
152
  payload: r
148
153
  });
@@ -154,20 +159,20 @@ function b(e) {
154
159
  recordId: r.recordId
155
160
  };
156
161
  e.onNavigate?.(i);
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);
162
+ }, j = new Set(["not_found", "unresolved"]), M = (t) => {
163
+ if (typeof t?.route != "string" || t.route.length === 0 || !j.has(t.reason)) {
164
+ b("<- iframe navigation-failed (dropped: invalid payload)", t);
160
165
  return;
161
166
  }
162
- h("<- iframe navigation-failed", t), e.onEvent?.({
167
+ b("<- iframe navigation-failed", t), e.onEvent?.({
163
168
  type: n.navigationFailed,
164
169
  payload: t
165
170
  }), e.onNavigationFailed?.(t);
166
- }, j = () => {
167
- m || (m = !0, h("handshake complete - iframe authenticated"), e.onReady?.());
168
- }, M = () => {
171
+ }, N = () => {
172
+ m || (m = !0, b("handshake complete - iframe authenticated"), e.onReady?.());
173
+ }, P = () => {
169
174
  c !== null && (clearTimeout(c), c = null);
170
- }, N = (t) => {
175
+ }, F = (t) => {
171
176
  if (!o || t.source !== o.contentWindow) return;
172
177
  let i = r(t, e.declarionOrigin);
173
178
  if (i.kind === "rejected") return;
@@ -178,64 +183,70 @@ function b(e) {
178
183
  let { message: a } = i;
179
184
  switch (a.type) {
180
185
  case n.ready:
181
- C();
186
+ T();
182
187
  break;
183
188
  case n.tokenExpired:
184
- w();
189
+ E();
185
190
  break;
186
191
  case n.reloadRequired:
187
- T(a.payload);
192
+ D(a.payload);
188
193
  break;
189
194
  case n.resized:
190
- E(a.payload);
195
+ O(a.payload);
191
196
  break;
192
197
  case n.navigated:
193
- O(n.navigated, a.payload);
198
+ A(n.navigated, a.payload);
194
199
  break;
195
200
  case n.navigationRequested:
196
- O(n.navigationRequested, a.payload);
201
+ A(n.navigationRequested, a.payload);
197
202
  break;
198
203
  case n.dirtyChanged:
199
- D(a.payload);
204
+ k(a.payload);
200
205
  break;
201
206
  case n.navigationFailed:
202
- A(a.payload);
207
+ M(a.payload);
203
208
  break;
204
209
  default: break;
205
210
  }
206
- }, P = v(e);
207
- if (P) return console.error(`${_} ${P.message}`), e.onError?.(P), {
211
+ }, I = v(e);
212
+ if (I) return console.error(`${_} ${I.message}`), e.onError?.(I), {
208
213
  navigate: () => void 0,
209
214
  setTheme: () => void 0,
210
215
  destroy: () => void 0
211
216
  };
212
- let F = e.navigation ?? "self", I = window.location.origin, L;
217
+ let L = e.navigation ?? "self", R = window.location.origin, z;
213
218
  try {
214
- L = u({
219
+ z = u({
215
220
  declarionOrigin: e.declarionOrigin,
216
221
  route: e.route,
217
- parentOrigin: I,
218
- navigation: F,
222
+ parentOrigin: R,
223
+ navigation: L,
219
224
  theme: e.theme
220
225
  });
221
226
  } catch (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), {
227
+ return x(i.invalidOptions, `Could not build the iframe URL from route "${e.route}". \`route\` must be a valid Declarion screen route.`, t), {
223
228
  navigate: () => void 0,
224
229
  setTheme: () => void 0,
225
230
  destroy: () => void 0
226
231
  };
227
232
  }
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.`);
233
+ return o = document.createElement("iframe"), o.src = z, 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 = F, window.addEventListener("message", s), e.container.appendChild(o), b("iframe created", { src: z }), c = setTimeout(() => {
234
+ c = null, !l && x(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.`);
230
235
  }, f), {
231
- navigate(r) {
232
- t || x(n.navigate, { route: d(e.declarionOrigin, r) });
236
+ navigate(e) {
237
+ if (!t) {
238
+ if (!l) {
239
+ h = e;
240
+ return;
241
+ }
242
+ w(e);
243
+ }
233
244
  },
234
245
  setTheme(e) {
235
- t || x(n.setTheme, { theme: e });
246
+ t || S(n.setTheme, { theme: e });
236
247
  },
237
248
  destroy() {
238
- t || (t = !0, M(), s &&= (window.removeEventListener("message", s), null), o?.parentNode && o.parentNode.removeChild(o), o = null, h("embed destroyed"));
249
+ t || (t = !0, P(), s &&= (window.removeEventListener("message", s), null), o?.parentNode && o.parentNode.removeChild(o), o = null, b("embed destroyed"));
239
250
  }
240
251
  };
241
252
  }
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 /** 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
+ {"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 // A navigate requested before the iframe `ready` handshake. The iframe app\n // is not listening for postMessage until it boots and emits `ready`, so a\n // pre-ready navigate would be lost; buffer the latest route (last wins) and\n // flush it on `ready`.\n let pendingNavigateRoute: string | null = null;\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 /** Post a navigate frame, resolving the route to a path on the origin. */\n const postNavigate = (route: string): void => {\n postToIframe(EMBED_MESSAGE_TYPES.navigate, {\n route: resolveRoute(options.declarionOrigin, route),\n });\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 // Flush a navigate buffered before the handshake completed (last wins).\n if (pendingNavigateRoute !== null) {\n const route = pendingNavigateRoute;\n pendingNavigateRoute = null;\n postNavigate(route);\n }\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 // Before `ready` the iframe app is not yet listening; buffer the route\n // and flush it on the handshake so a navigate immediately after mount\n // is not lost.\n if (!readyReceived) {\n pendingNavigateRoute = route;\n return;\n }\n postNavigate(route);\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":";AA+BA,IAAa,IAAuB,mBAOvB,IAAyB,GAQzB,IAAsB;CAEjC,OAAO;CAEP,UAAU;CAEV,cAAc;CAEd,gBAAgB;CAEhB,SAAS;CAET,WAAW;CAEX,qBAAqB;CAErB,cAAc;CAGd,kBAAkB;CAElB,UAAU;CAEV,UAAU;AACZ;;;ACtBA,SAAgB,EACd,GACA,GACuB;CAGvB,IAAI,EAAM,WAAW,GACnB,OAAO,EAAE,MAAM,WAAW;CAK5B,IAAM,IAAO,EAAM;CACnB,IAAI,OAAO,KAAS,aAAY,GAC9B,OAAO,EAAE,MAAM,WAAW;CAE5B,IAAM,IAAW;CAiBjB,OAhBI,EAAS,WAAA,qBAGT,OAAO,EAAS,YAAa,YAG7B,OAAO,EAAS,QAAS,WACpB,EAAE,MAAM,WAAW,IAKxB,EAAS,aAAA,IAIN;EAAE,MAAM;EAAS,SAAS;CAAyB,IAHjD;EAAE,MAAM;EAAqB,UAAU,EAAS;CAAS;AAIpE;;;AC/DA,IAAa,IAAoB;CAM/B,gBAAgB;CAKhB,gBAAgB;CAQhB,kBAAkB;CAKlB,gBAAgB;AAClB,GAca,IAAb,MAAa,UAAmB,MAAM;CAEpC;CAEA,YAAY,GAAsB,GAAiB,GAA+B;EAMhF,AALA,MAAM,GAAS,CAAO,GACtB,KAAK,OAAO,cACZ,KAAK,OAAO,GAGZ,OAAO,eAAe,MAAM,EAAW,SAAS;CAClD;AACF,GC3Da,IAAoB,SACpB,IAA4B,iBAC5B,IAAoB,SAI3B,IAA4B;AAgClC,SAAgB,EAAc,GAAmC;CAI/D,IAAM,IAAM,IAAI,IAAI,EAAM,OAAO,EAAM,eAAe;CAOtD,OANA,EAAI,aAAa,IAAI,GAAmB,CAAyB,GACjE,EAAI,aAAa,IAAI,GAA2B,EAAM,YAAY,GAClE,EAAI,aAAa,IAAA,OAAqB,EAAM,UAAU,GAClD,EAAM,SACR,EAAI,aAAa,IAAI,GAAmB,EAAM,KAAK,GAE9C,EAAI,SAAS;AACtB;AASA,SAAgB,EAAa,GAAyB,GAAuB;CAC3E,IAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAO,CAAe;EAG1C,OAAO,EAAI,WAAW,EAAI,SAAS,EAAI;CACzC,QAAQ;EACN,OAAO;CACT;AACF;;;AClCA,IAAM,IAAuB,KAGvB,IAAuB,6BASvB,IAA4C;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;AACF,GAGM,IAAgD,IAAI,IAAI,CAC5D,wBACA,yCACF,CAAC;AAOD,SAAS,EAAa,GAAmC;CACvD,IAAM,IAAS,IAAI,IAAI,CAAsB;CAC7C,KAAK,IAAM,KAAS,KAAS,CAAC,GAAG;EAC/B,IAAM,IAAU,EAAM,KAAK;EAC3B,AAAI,KAAW,CAAC,EAAyB,IAAI,CAAO,KAAG,EAAO,IAAI,CAAO;CAC3E;CACA,OAAO,MAAM,KAAK,CAAM,EAAE,KAAK,GAAG;AACpC;AAGA,IAAM,IAAa;AASnB,SAAS,EAAgB,GAAmD;CAC1E,IAAM,KAAQ,MACZ,IAAI,EAAW,EAAkB,gBAAgB,CAAO;CAE1D,IAAI,CAAC,EAAQ,aAAa,OAAO,EAAQ,UAAU,eAAgB,YACjE,OAAO,EACL,gEACF;CAEF,IAAI,OAAO,EAAQ,mBAAoB,YAAY,EAAQ,oBAAoB,IAC7E,OAAO,EACL,2HAEF;CAEF,IAAI;CACJ,IAAI;EACF,IAAe,IAAI,IAAI,EAAQ,eAAe;CAChD,QAAQ;EACN,OAAO,EACL,4CAA4C,EAAQ,gBAAgB,yDAEtE;CACF;CAiBA,OAhBI,EAAa,WAAW,EAAQ,kBAOhC,OAAO,EAAQ,SAAU,YAAY,EAAQ,UAAU,KAClD,EAAK,2DAA2D,IAErE,OAAO,EAAQ,YAAa,aAMzB,OALE,EACL,uFAEF,IAbO,EACL,8FAC6B,EAAQ,gBAAgB,eAC/C,EAAa,OAAO,GAC5B;AAYJ;AAMA,SAAS,EAAa,GAAqC;CACzD,IAAI,OAAO,KAAU,aAAY,GAAgB,OAAO;CACxD,IAAM,IAAQ;CACd,OACE,OAAO,EAAM,SAAU,YACvB,EAAM,UAAU,MAChB,OAAO,EAAM,cAAe,YAC5B,EAAM,eAAe;AAEzB;AAUA,SAAgB,EACd,GACsB;CACtB,IAAI,IAAY,IACZ,IAAmC,MACnC,IAA0D,MAC1D,IAAuD,MACvD,IAAgB,IAChB,IAAqB,IAKrB,IAAsC,MAGpC,KAAY,GAAiB,MAA2B;EACvD,EAAQ,UACT,MAAW,KAAA,IACb,QAAQ,KAAK,GAAG,EAAW,GAAG,GAAS,IAEvC,QAAQ,KAAK,GAAG,EAAW,GAAG,KAAW,CAAM;CAEnD,GAMM,KACJ,GACA,GACA,MACS;EACT,IAAM,IAAQ,IAAI,EAAW,GAAM,GAAS,IAAQ,EAAE,SAAM,IAAI,KAAA,CAAS;EAEzE,AADA,QAAQ,MAAM,GAAG,EAAW,GAAG,KAAW,KAAS,EAAE,GACrD,EAAQ,UAAU,CAAK;CACzB,GAGM,KACJ,GACA,MACS;EACT,IAAI,CAAC,GAAQ,eAAe;EAC5B,IAAM,IAA2B;GAC/B,QAAQ;GACR,UAAA;GACA;GACA;EACF;EAEA,AADA,EAAS,aAAa,KAAQ,CAAO,GACrC,EAAO,cAAc,YAAY,GAAS,EAAQ,eAAe;CACnE,GAOM,IAAyB,YAA2B;EACxD,IAAI;EACJ,IAAI;GACF,IAAS,MAAM,EAAQ,SAAS;EAClC,SAAS,GAAO;GACd,EACE,EAAkB,gBAClB,2HAEA,CACF;GACA;EACF;EACA,IAAI,CAAC,EAAa,CAAM,GAAG;GACzB,EACE,EAAkB,gBAClB,2FAEF;GACA;EACF;EACA,IAAI,GAAW;EACf,IAAM,IAAgC;GACpC,OAAO,EAAO;GACd,YAAY,EAAO;EACrB;EAEA,AADA,EAAa,EAAoB,UAAU,CAAO,GAClD,EAAgB;CAClB,GAGM,KAAgB,MAAwB;EAC5C,EAAa,EAAoB,UAAU,EACzC,OAAO,EAAa,EAAQ,iBAAiB,CAAK,EACpD,CAAC;CACH,GAGM,UAAsB;EAO1B,IANA,IAAgB,IAChB,EAAoB,GACpB,EAAS,iBAAiB,GAC1B,EAAQ,UAAU,EAAE,MAAM,EAAoB,MAAM,CAAC,GACrD,EAA4B,GAExB,MAAyB,MAAM;GACjC,IAAM,IAAQ;GAEd,AADA,IAAuB,MACvB,EAAa,CAAK;EACpB;CACF,GAGM,UAA6B;EAGjC,AAFA,EAAS,yBAAyB,GAClC,EAAQ,UAAU,EAAE,MAAM,EAAoB,aAAa,CAAC,GAC5D,EAA4B;CAC9B,GAGM,KAAoB,MAA8C;EAMtE,AALA,EAAS,6BAA6B,CAAO,GAC7C,EAAQ,UAAU;GAChB,MAAM,EAAoB;GAC1B;EACF,CAAC,GACD,EACE,EAAkB,gBAClB,2CAA2C,EAAQ,OAAO,gCAE5D;CACF,GAOM,KAAY,MAAuC;EAKvD,AAJA,EAAS,qBAAqB,CAAO,GACjC,CAAC,EAAQ,UAAU,KAAU,OAAO,SAAS,EAAQ,MAAM,KAAK,EAAQ,SAAS,MACnF,EAAO,MAAM,SAAS,GAAG,EAAQ,OAAO,MAE1C,EAAQ,UAAU;GAAE,MAAM,EAAoB;GAAS;EAAQ,CAAC;CAClE,GAQM,KAAkB,MAA4C;EAElE,AADA,EAAS,2BAA2B,CAAO,GAC3C,EAAQ,UAAU;GAAE,MAAM,EAAoB;GAAc;EAAQ,CAAC;CACvE,GAOM,KACJ,GAGA,MACS;EAET,AADA,EAAS,aAAa,KAAQ,CAAO,GACrC,EAAQ,UAAU;GAAE;GAAM;EAAQ,CAAC;EACnC,IAAM,IAA4B;GAChC,MAAM,MAAS,EAAoB,YAAY,SAAS;GACxD,OAAO,EAAQ;GACf,YAAY,EAAQ;GACpB,QAAQ,EAAQ;GAChB,UAAU,EAAQ;EACpB;EACA,EAAQ,aAAa,CAAK;CAC5B,GAYM,IAA2B,IAAI,IAAI,CAAC,aAAa,YAAY,CAAC,GAC9D,KAAsB,MAAgD;EAC1E,IACE,OAAO,GAAS,SAAU,YAC1B,EAAQ,MAAM,WAAW,KACzB,CAAC,EAAyB,IAAI,EAAQ,MAAM,GAC5C;GACA,EAAS,0DAA0D,CAAO;GAC1E;EACF;EAGA,AAFA,EAAS,+BAA+B,CAAO,GAC/C,EAAQ,UAAU;GAAE,MAAM,EAAoB;GAAkB;EAAQ,CAAC,GACzE,EAAQ,qBAAqB,CAAO;CACtC,GAQM,UAA8B;EAClC,AAAK,MACH,IAAqB,IACrB,EAAS,2CAA2C,GACpD,EAAQ,UAAU;CAEtB,GAGM,UAAkC;EACtC,AAAI,MAAmB,SACrB,aAAa,CAAc,GAC3B,IAAiB;CAErB,GAOM,KAAiB,MAA8B;EAQnD,IAAI,CAAC,KAAU,EAAM,WAAW,EAAO,eACrC;EAGF,IAAM,IAAa,EAAuB,GAAO,EAAQ,eAAe;EAExE,IAAI,EAAW,SAAS,YAGtB;EAGF,IAAI,EAAW,SAAS,qBAAqB;GAG3C,QAAQ,KACN,GAAG,EAAW,uFAET,OAAO,EAAW,QAAQ,EAAE,6EAEnC;GACA;EACF;EAEA,IAAM,EAAE,eAAY;EACpB,QAAQ,EAAQ,MAAhB;GACE,KAAK,EAAoB;IACvB,EAAQ;IACR;GACF,KAAK,EAAoB;IACvB,EAAe;IACf;GACF,KAAK,EAAoB;IACvB,EAAiB,EAAQ,OAAqC;IAC9D;GACF,KAAK,EAAoB;IACvB,EAAS,EAAQ,OAA8B;IAC/C;GACF,KAAK,EAAoB;IACvB,EACE,EAAoB,WACpB,EAAQ,OACV;IACA;GACF,KAAK,EAAoB;IACvB,EACE,EAAoB,qBACpB,EAAQ,OACV;IACA;GACF,KAAK,EAAoB;IACvB,EAAe,EAAQ,OAAmC;IAC1D;GACF,KAAK,EAAoB;IACvB,EAAmB,EAAQ,OAAuC;IAClE;GACF,SAGE;EACJ;CACF,GAIM,IAAe,EAAgB,CAAO;CAC5C,IAAI,GAKF,OAJA,QAAQ,MAAM,GAAG,EAAW,GAAG,EAAa,SAAS,GACrD,EAAQ,UAAU,CAAY,GAGvB;EACL,gBAAgB,KAAA;EAChB,gBAAgB,KAAA;EAChB,eAAe,KAAA;CACjB;CAGF,IAAM,IAAa,EAAQ,cAAA,QAGrB,IAAe,OAAO,SAAS,QAEjC;CACJ,IAAI;EACF,IAAM,EAAc;GAClB,iBAAiB,EAAQ;GACzB,OAAO,EAAQ;GACf;GACA;GACA,OAAO,EAAQ;EACjB,CAAC;CACH,SAAS,GAAO;EAOd,OANA,EACE,EAAkB,gBAClB,8CAA8C,EAAQ,MAAM,uDAE5D,CACF,GACO;GACL,gBAAgB,KAAA;GAChB,gBAAgB,KAAA;GAChB,eAAe,KAAA;EACjB;CACF;CAmDA,OAjDA,IAAS,SAAS,cAAc,QAAQ,GACxC,EAAO,MAAM,GACb,EAAO,QAAQ,EAAQ,SAAS,GAahC,EAAO,aAAa,WAAW,EAAa,EAAQ,YAAY,CAAC,GACjE,EAAO,MAAM,QAAQ,QACrB,EAAO,MAAM,SAAS,KACtB,EAAO,MAAM,UAAU,SAInB,EAAQ,SACV,EAAO,MAAM,SAAS,EAAQ,SACpB,EAAO,MAAM,WACvB,EAAO,MAAM,SAAS,UAGxB,IAAkB,GAClB,OAAO,iBAAiB,WAAW,CAAe,GAClD,EAAQ,UAAU,YAAY,CAAM,GACpC,EAAS,kBAAkB,EAAE,OAAI,CAAC,GAKlC,IAAiB,iBAAiB;EAChC,IAAiB,MACb,MACJ,EACE,EAAkB,kBAClB,uCAAuC,EAAqB,sNAK9D;CACF,GAAG,CAAoB,GAEhB;EACL,SAAS,GAAqB;GACxB,QAKJ;QAAI,CAAC,GAAe;KAClB,IAAuB;KACvB;IACF;IACA,EAAa,CAAK;GADlB;EAEF;EACA,SAAS,GAAa;GAChB,KACJ,EAAa,EAAoB,UAAU,EAAE,SAAM,CAAC;EACtD;EACA,UAAgB;GACV,MACJ,IAAY,IACZ,EAAoB,GACpB,AAEE,OADA,OAAO,oBAAoB,WAAW,CAAe,GACnC,OAEhB,GAAQ,cACV,EAAO,WAAW,YAAY,CAAM,GAEtC,IAAS,MACT,EAAS,iBAAiB;EAC5B;CACF;AACF"}
@@ -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`,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})({});
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=null,g=(t,n)=>{e.debug&&(n===void 0?console.info(`${v} ${t}`):console.info(`${v} ${t}`,n))},x=(t,n,r)=>{let i=new o(t,n,r?{cause:r}:void 0);console.error(`${v} ${n}`,r??``),e.onError?.(i)},S=(t,r)=>{if(!n?.contentWindow)return;let i={source:`declarion-embed`,protocol:1,type:t,payload:r};g(`-> iframe ${t}`,r),n.contentWindow.postMessage(i,e.declarionOrigin)},C=async()=>{let n;try{n=await e.getToken()}catch(e){x(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)){x(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};S(r.setToken,i),N()},w=t=>{S(r.navigate,{route:f(e.declarionOrigin,t)})},T=()=>{if(l=!0,P(),g(`<- iframe ready`),e.onEvent?.({type:r.ready}),C(),h!==null){let e=h;h=null,w(e)}},E=()=>{g(`<- iframe token-expired`),e.onEvent?.({type:r.tokenExpired}),C()},D=t=>{g(`<- iframe reload-required`,t),e.onEvent?.({type:r.reloadRequired,payload:t}),x(a.reloadRequired,`The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`)},O=t=>{g(`<- 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})},k=t=>{g(`<- iframe dirty-changed`,t),e.onEvent?.({type:r.dirtyChanged,payload:t})},A=(t,n)=>{g(`<- 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)},j=new Set([`not_found`,`unresolved`]),M=t=>{if(typeof t?.route!=`string`||t.route.length===0||!j.has(t.reason)){g(`<- iframe navigation-failed (dropped: invalid payload)`,t);return}g(`<- iframe navigation-failed`,t),e.onEvent?.({type:r.navigationFailed,payload:t}),e.onNavigationFailed?.(t)},N=()=>{u||(u=!0,g(`handshake complete - iframe authenticated`),e.onReady?.())},P=()=>{c!==null&&(clearTimeout(c),c=null)},F=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:T();break;case r.tokenExpired:E();break;case r.reloadRequired:D(o.payload);break;case r.resized:O(o.payload);break;case r.navigated:A(r.navigated,o.payload);break;case r.navigationRequested:A(r.navigationRequested,o.payload);break;case r.dirtyChanged:k(o.payload);break;case r.navigationFailed:M(o.payload);break;default:break}},I=y(e);if(I)return console.error(`${v} ${I.message}`),e.onError?.(I),{navigate:()=>void 0,setTheme:()=>void 0,destroy:()=>void 0};let L=e.navigation??`self`,R=window.location.origin,z;try{z=d({declarionOrigin:e.declarionOrigin,route:e.route,parentOrigin:R,navigation:L,theme:e.theme})}catch(t){return x(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=z,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=F,window.addEventListener(`message`,s),e.container.appendChild(n),g(`iframe created`,{src:z}),c=setTimeout(()=>{c=null,!l&&x(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(e){if(!t){if(!l){h=e;return}w(e)}},setTheme(e){t||S(r.setTheme,{theme:e})},destroy(){t||(t=!0,P(),s&&=(window.removeEventListener(`message`,s),null),n?.parentNode&&n.parentNode.removeChild(n),n=null,g(`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 /** 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"}
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 // A navigate requested before the iframe `ready` handshake. The iframe app\n // is not listening for postMessage until it boots and emits `ready`, so a\n // pre-ready navigate would be lost; buffer the latest route (last wins) and\n // flush it on `ready`.\n let pendingNavigateRoute: string | null = null;\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 /** Post a navigate frame, resolving the route to a path on the origin. */\n const postNavigate = (route: string): void => {\n postToIframe(EMBED_MESSAGE_TYPES.navigate, {\n route: resolveRoute(options.declarionOrigin, route),\n });\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 // Flush a navigate buffered before the handshake completed (last wins).\n if (pendingNavigateRoute !== null) {\n const route = pendingNavigateRoute;\n pendingNavigateRoute = null;\n postNavigate(route);\n }\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 // Before `ready` the iframe app is not yet listening; buffer the route\n // and flush it on the handshake so a navigate immediately after mount\n // is not lost.\n if (!readyReceived) {\n pendingNavigateRoute = route;\n return;\n }\n postNavigate(route);\n },\n setTheme(theme): void {\n if (destroyed) return;\n postToIframe(EMBED_MESSAGE_TYPES.setTheme, { theme });\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearHandshakeTimer();\n if (messageListener) {\n window.removeEventListener(\"message\", messageListener);\n messageListener = null;\n }\n if (iframe?.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n iframe = null;\n logDebug(\"embed destroyed\");\n },\n };\n}\n"],"mappings":"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"}
package/dist/react.d.ts CHANGED
@@ -12,6 +12,20 @@ export interface DeclarionEmbedProps extends Omit<DeclarionEmbedOptions, "contai
12
12
  readonly className?: string;
13
13
  /** Optional inline style applied to the container element. */
14
14
  readonly style?: React.CSSProperties;
15
+ /**
16
+ * An opaque key for the embedded IDENTITY (the user + tenant the `getToken`
17
+ * callback mints for). Changing it REBUILDS the iframe.
18
+ *
19
+ * The embed token is per (user, tenant) and the in-iframe `/api/schema`
20
+ * cache is authority-shaped but keyed only on "authenticated", so a
21
+ * persistent iframe whose `getToken` starts returning a DIFFERENT user's
22
+ * token would keep serving the FIRST user's schema. Set this to a stable
23
+ * string for the identity (e.g. `` `${tenant}|${user}` ``) so an identity
24
+ * switch forces a fresh iframe + handshake. Leave it unset only when the
25
+ * identity never changes for the life of the component. A `route` change is
26
+ * NOT an identity change and must not go here - that is a soft navigate.
27
+ */
28
+ readonly identityKey?: string;
15
29
  }
16
30
  /**
17
31
  * `<DeclarionEmbed />` - the React binding for `@declarion/embed`.
package/dist/react.js CHANGED
@@ -5,15 +5,15 @@ import { jsx as s } from "react/jsx-runtime";
5
5
  function c(e) {
6
6
  return [
7
7
  e.declarionOrigin,
8
- e.route,
9
8
  e.navigation ?? "self",
10
9
  e.height ?? "",
10
+ e.identityKey ?? "",
11
11
  (e.extraSandbox ?? []).join(",")
12
12
  ].join("\0");
13
13
  }
14
14
  function l(e, t) {
15
15
  let { className: r, style: l } = e, u = o(null), d = o(null), f = o(e);
16
- return f.current = e, i(() => {
16
+ f.current = e, i(() => {
17
17
  let e = u.current;
18
18
  if (!e) return;
19
19
  let t = n({
@@ -36,7 +36,15 @@ function l(e, t) {
36
36
  return d.current = t, () => {
37
37
  t.destroy(), d.current = null;
38
38
  };
39
- }, [c(e)]), i(() => {
39
+ }, [c(e)]);
40
+ let p = o(!0);
41
+ return i(() => {
42
+ if (p.current) {
43
+ p.current = !1;
44
+ return;
45
+ }
46
+ d.current?.navigate(e.route);
47
+ }, [e.route]), i(() => {
40
48
  e.theme && d.current?.setTheme(e.theme);
41
49
  }, [e.theme]), a(t, () => ({
42
50
  navigate: (e) => d.current?.navigate(e),
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 // 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"}
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 * An opaque key for the embedded IDENTITY (the user + tenant the `getToken`\n * callback mints for). Changing it REBUILDS the iframe.\n *\n * The embed token is per (user, tenant) and the in-iframe `/api/schema`\n * cache is authority-shaped but keyed only on \"authenticated\", so a\n * persistent iframe whose `getToken` starts returning a DIFFERENT user's\n * token would keep serving the FIRST user's schema. Set this to a stable\n * string for the identity (e.g. `` `${tenant}|${user}` ``) so an identity\n * switch forces a fresh iframe + handshake. Leave it unset only when the\n * identity never changes for the life of the component. A `route` change is\n * NOT an identity change and must not go here - that is a soft navigate.\n */\n readonly identityKey?: string;\n}\n\n/**\n * The set of props that, when changed, MUST rebuild the embed (a fresh\n * iframe): a different Declarion deployment, navigation contract, fixed\n * height, or IDENTITY (`identityKey`). `route` is deliberately EXCLUDED - a\n * route change is a soft SPA navigate posted to the existing iframe (see the\n * route effect below), not an iframe re-create. Theme is also excluded - it is\n * applied live via the handle's `setTheme`.\n *\n * `identityKey` is in the rebuild set because the in-iframe schema cache is\n * authority-shaped: switching identity on a persistent iframe would otherwise\n * serve the prior user's schema (see the prop's doc).\n */\nfunction rebuildKey(props: DeclarionEmbedProps): string {\n return [\n props.declarionOrigin,\n props.navigation ?? \"self\",\n props.height ?? \"\",\n // A change of embedded identity (user + tenant) must rebuild - the schema\n // cache is authority-shaped and would otherwise leak across identities.\n props.identityKey ?? \"\",\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 * navigation contract, or fixed height changes, soft-navigates the existing\n * iframe when only `route` changes, and destroys it on unmount. A\n * `ref` exposes the imperative `DeclarionEmbedHandle` (`navigate`,\n * `setTheme`, `destroy`) for hosts that drive the iframe directly.\n *\n * `getToken` and the callback props are read through a ref, so passing a\n * fresh inline `getToken` on every render does NOT rebuild the iframe.\n */\nfunction DeclarionEmbedComponent(\n props: DeclarionEmbedProps,\n ref: ForwardedRef<DeclarionEmbedHandle>,\n): ReactElement {\n const { className, style } = props;\n const containerRef = useRef<HTMLDivElement | null>(null);\n const handleRef = useRef<DeclarionEmbedHandle | null>(null);\n\n // Latest props, read by the stable callbacks handed to the core. This\n // keeps `getToken` / `onReady` / `onError` / `onNavigate` / `onEvent`\n // current without rebuilding the iframe when an inline closure changes.\n const propsRef = useRef(props);\n propsRef.current = props;\n\n const key = rebuildKey(props);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return undefined;\n\n const handle = createDeclarionEmbed({\n container,\n // `propsRef.current` is always the latest props snapshot - React updates\n // the ref during render, before this effect runs - so these read\n // correctly on both the initial mount and a key-triggered rebuild.\n declarionOrigin: propsRef.current.declarionOrigin,\n route: propsRef.current.route,\n navigation: propsRef.current.navigation,\n 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 // A `route` change is a soft SPA navigate, not a rebuild: post a `navigate`\n // frame to the existing iframe. The first run is skipped - the iframe is\n // created at the initial route via its `src` - so this fires only on a\n // genuine route change after mount. On a hard-dim rebuild that also changes\n // the route, the new iframe loads at the new route via `src` and this posts\n // a redundant navigate to the same route (a harmless no-op).\n const isFirstRouteEffect = useRef(true);\n useEffect(() => {\n if (isFirstRouteEffect.current) {\n isFirstRouteEffect.current = false;\n return;\n }\n handleRef.current?.navigate(props.route);\n }, [props.route]);\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":";;;;AAyDA,SAAS,EAAW,GAAoC;CACtD,OAAO;EACL,EAAM;EACN,EAAM,cAAc;EACpB,EAAM,UAAU;EAGhB,EAAM,eAAe;GAGpB,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;CAK7B,AAJA,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;CAQR,IAAM,IAAqB,EAAO,EAAI;CA4BtC,OA3BA,QAAgB;EACd,IAAI,EAAmB,SAAS;GAC9B,EAAmB,UAAU;GAC7B;EACF;EACA,EAAU,SAAS,SAAS,EAAM,KAAK;CACzC,GAAG,CAAC,EAAM,KAAK,CAAC,GAGhB,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declarion/embed",
3
- "version": "0.6.3",
3
+ "version": "0.6.6",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Host integration SDK for embedding Declarion screens as white-label iframes.",