@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 +56 -14
- package/dist/core.js +61 -50
- package/dist/core.js.map +1 -1
- package/dist/declarion-embed.iife.js +1 -1
- package/dist/declarion-embed.iife.js.map +1 -1
- package/dist/react.d.ts +14 -0
- package/dist/react.js +11 -3
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
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=
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- `navigation: "delegated"` - the iframe never moves; it
|
|
123
|
-
`onNavigate({ mode: "delegated", route, screenCode, ... })` and the
|
|
124
|
-
|
|
125
|
-
|
|
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)
|
|
174
|
-
|
|
175
|
-
iframe
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
|
|
107
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
}),
|
|
134
|
-
},
|
|
135
|
-
|
|
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
|
-
},
|
|
140
|
-
|
|
144
|
+
}, k = (t) => {
|
|
145
|
+
b("<- iframe dirty-changed", t), e.onEvent?.({
|
|
141
146
|
type: n.dirtyChanged,
|
|
142
147
|
payload: t
|
|
143
148
|
});
|
|
144
|
-
},
|
|
145
|
-
|
|
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
|
-
},
|
|
158
|
-
if (typeof t?.route != "string" || t.route.length === 0 || !
|
|
159
|
-
|
|
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
|
-
|
|
167
|
+
b("<- iframe navigation-failed", t), e.onEvent?.({
|
|
163
168
|
type: n.navigationFailed,
|
|
164
169
|
payload: t
|
|
165
170
|
}), e.onNavigationFailed?.(t);
|
|
166
|
-
},
|
|
167
|
-
m || (m = !0,
|
|
168
|
-
},
|
|
171
|
+
}, N = () => {
|
|
172
|
+
m || (m = !0, b("handshake complete - iframe authenticated"), e.onReady?.());
|
|
173
|
+
}, P = () => {
|
|
169
174
|
c !== null && (clearTimeout(c), c = null);
|
|
170
|
-
},
|
|
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
|
-
|
|
186
|
+
T();
|
|
182
187
|
break;
|
|
183
188
|
case n.tokenExpired:
|
|
184
|
-
|
|
189
|
+
E();
|
|
185
190
|
break;
|
|
186
191
|
case n.reloadRequired:
|
|
187
|
-
|
|
192
|
+
D(a.payload);
|
|
188
193
|
break;
|
|
189
194
|
case n.resized:
|
|
190
|
-
|
|
195
|
+
O(a.payload);
|
|
191
196
|
break;
|
|
192
197
|
case n.navigated:
|
|
193
|
-
|
|
198
|
+
A(n.navigated, a.payload);
|
|
194
199
|
break;
|
|
195
200
|
case n.navigationRequested:
|
|
196
|
-
|
|
201
|
+
A(n.navigationRequested, a.payload);
|
|
197
202
|
break;
|
|
198
203
|
case n.dirtyChanged:
|
|
199
|
-
|
|
204
|
+
k(a.payload);
|
|
200
205
|
break;
|
|
201
206
|
case n.navigationFailed:
|
|
202
|
-
|
|
207
|
+
M(a.payload);
|
|
203
208
|
break;
|
|
204
209
|
default: break;
|
|
205
210
|
}
|
|
206
|
-
},
|
|
207
|
-
if (
|
|
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
|
|
217
|
+
let L = e.navigation ?? "self", R = window.location.origin, z;
|
|
213
218
|
try {
|
|
214
|
-
|
|
219
|
+
z = u({
|
|
215
220
|
declarionOrigin: e.declarionOrigin,
|
|
216
221
|
route: e.route,
|
|
217
|
-
parentOrigin:
|
|
218
|
-
navigation:
|
|
222
|
+
parentOrigin: R,
|
|
223
|
+
navigation: L,
|
|
219
224
|
theme: e.theme
|
|
220
225
|
});
|
|
221
226
|
} catch (t) {
|
|
222
|
-
return
|
|
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 =
|
|
229
|
-
c = null, !l &&
|
|
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(
|
|
232
|
-
|
|
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 ||
|
|
246
|
+
t || S(n.setTheme, { theme: e });
|
|
236
247
|
},
|
|
237
248
|
destroy() {
|
|
238
|
-
t || (t = !0,
|
|
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":";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,IAGnB,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,UAAsB;EAK1B,AAJA,IAAgB,IAChB,EAAoB,GACpB,EAAS,iBAAiB,GAC1B,EAAQ,UAAU,EAAE,MAAM,EAAoB,MAAM,CAAC,GACrD,EAA4B;CAC9B,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,KAEJ,EAAa,EAAoB,UAAU,EACzC,OAAO,EAAa,EAAQ,iBAAiB,CAAK,EACpD,CAAC;EACH;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
|
+
{"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":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var DeclarionEmbed=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`declarion-embed`,n=1,r={ready:`ready`,setToken:`set-token`,tokenExpired:`token-expired`,reloadRequired:`reload-required`,resized:`resized`,navigated:`navigated`,navigationRequested:`navigation-requested`,dirtyChanged:`dirty-changed`,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))},
|
|
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":"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"}
|
|
1
|
+
{"version":3,"file":"declarion-embed.iife.js","names":[],"sources":["../src/protocol.ts","../src/inbound.ts","../src/errors.ts","../src/url.ts","../src/core.ts"],"sourcesContent":["// Embed postMessage protocol - host-side contract.\n//\n// `@declarion/embed` is a SEPARATE npm package from `@declarion/react` and\n// MUST NOT depend on it (a host app must not pull the full Declarion UI SDK\n// to host an iframe). The protocol contract is therefore re-declared here,\n// independently, and MUST match the iframe side EXACTLY:\n// typescript/packages/react/src/embed/protocol.ts\n// A divergence breaks the handshake. Any change to the wire envelope, the\n// message-type set, or a payload shape MUST land in both files together.\n//\n// Wire envelope: { source: \"declarion-embed\", protocol: 1, type, payload }.\n//\n// Naming contract (developer-facing - keep it consistent):\n// - A message the iframe SENDS to the host is an EVENT, named in the past\n// tense - \"this happened\": `resized`, `navigated`, `navigation-requested`,\n// `dirty-changed`, `token-expired`, `reload-required`, `ready`.\n// - A message the host SENDS to the iframe is a COMMAND, named as an\n// imperative - \"do this\": `set-token`, `navigate`, `set-theme`.\n//\n// Security model: the host SDK validates EVERY inbound frame on two axes -\n// exact origin equality against the configured `declarionOrigin`, and the\n// envelope shape (`source` discriminator + numeric `protocol`). A frame that\n// fails either check is dropped SILENTLY; an untrusted page that re-framed\n// the iframe must learn nothing. Outbound host frames target the EXACT\n// `declarionOrigin`, never `\"*\"`.\n\n/**\n * The `source` discriminator stamped on every embed frame. Both the host SDK\n * and the iframe filter inbound traffic on this value so unrelated\n * `postMessage` frames (browser extensions, other widgets) are ignored.\n */\nexport const EMBED_MESSAGE_SOURCE = \"declarion-embed\" as const;\n\n/**\n * The protocol version this SDK speaks. Bumped only on a breaking\n * envelope/payload change. The SDK warns when the iframe reports a different\n * version (see the diagnostics path).\n */\nexport const EMBED_PROTOCOL_VERSION = 1 as const;\n\n/**\n * Every protocol message `type`. The object key is the camelCase name used in\n * code; the value is the on-wire string. Events (iframe -> host) are past\n * tense; commands (host -> iframe) are imperative. This SDK is the HOST side:\n * it RECEIVES the iframe-to-host events and SENDS the host-to-iframe commands.\n */\nexport const EMBED_MESSAGE_TYPES = {\n /** iframe -> host: SDK mounted, requests the first token. */\n ready: \"ready\",\n /** host -> iframe: deliver or refresh the embed token. */\n setToken: \"set-token\",\n /** iframe -> host: token rejected or near expiry. */\n tokenExpired: \"token-expired\",\n /** iframe -> host: the iframe must be reloaded. */\n reloadRequired: \"reload-required\",\n /** iframe -> host: embed content height changed. */\n resized: \"resized\",\n /** iframe -> host: `self` mode internal navigation happened. */\n navigated: \"navigated\",\n /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */\n navigationRequested: \"navigation-requested\",\n /** iframe -> host: the embedded screen's unsaved-edits state flipped. */\n dirtyChanged: \"dirty-changed\",\n /** iframe -> host: an in-frame navigation could not be handled (unknown\n * route, or a 404 the host should recover from). The iframe stayed put. */\n navigationFailed: \"navigation-failed\",\n /** host -> iframe: drive the iframe to a screen (deep-linking). */\n navigate: \"navigate\",\n /** host -> iframe: runtime theme switch. */\n setTheme: \"set-theme\",\n} as const;\n\n/** The union of all on-wire message `type` strings. */\nexport type EmbedMessageType =\n (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];\n\n// --- Payload interfaces, one per message type ---\n\n/** `ready` payload: empty - the frame itself is the signal. */\nexport type EmbedReadyPayload = Record<string, never>;\n\n/** `set-token` payload: the embed token and its absolute expiry. */\nexport interface EmbedSetTokenPayload {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** RFC 3339 expiry timestamp of the token. */\n readonly expires_at: string;\n}\n\n/** `token-expired` payload: empty - the host re-mints via `getToken`. */\nexport type EmbedTokenExpiredPayload = Record<string, never>;\n\n/** `reload-required` payload: a human-readable reason for the reload. */\nexport interface EmbedReloadRequiredPayload {\n /** Why the iframe must be reloaded (asset drift, terminal auth failure). */\n readonly reason: string;\n}\n\n/** `resized` payload: the measured content height in CSS pixels. */\nexport interface EmbedResizedPayload {\n /** Content height in CSS pixels. */\n readonly height: number;\n}\n\n/**\n * `navigated` / `navigation-requested` payload: the screen route and, when\n * resolvable, the matched screen code, bound entity code, and record id.\n *\n * `screenCode`, `entity`, and `recordId` are optional: an unregistered path\n * resolves to none of them, a `custom` screen has no entity, and a list route\n * has no record id. The host needs `screenCode` to mint a scoped embed token\n * for the target screen when it executes a `delegated` navigation intent.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /**\n * The matched screen's code (its key in the schema screen map), when a\n * registered screen owns the route. Absent for an unresolved path. The host\n * passes this as `screen_code` to `auth.create_embed_session`.\n */\n readonly screenCode?: string;\n /** The bound entity code, when the route resolves to one. */\n readonly entity?: string;\n /** The record id, when the route is a detail route with an id. */\n readonly recordId?: string;\n}\n\n/**\n * `dirty-changed` payload: whether the embedded screen currently has unsaved\n * edits. The host tracks this so it can guard its own navigation (its menu,\n * a host-initiated `navigate`) before moving the iframe away from a dirty\n * screen - the iframe never pops a dialog for host-driven navigation.\n */\nexport interface EmbedDirtyChangedPayload {\n /** True when the embedded screen has unsaved edits. */\n readonly dirty: boolean;\n}\n\n/**\n * `navigation-failed` payload: an in-frame navigation the iframe refused or\n * could not resolve. The iframe stayed put; the host owns recovery (show its\n * own not-found, reset its sidebar selection, etc.).\n *\n * `reason` is a closed set, each with a real producer: `not_found` from the\n * embedded 404 catch-all, `unresolved` from the navigation chokepoint when the\n * target route matches no registered screen.\n */\nexport interface EmbedNavigationFailedPayload {\n /** The route that could not be navigated to. */\n readonly route: string;\n /** Why it failed. */\n readonly reason: \"not_found\" | \"unresolved\";\n}\n\n/** `navigate` payload: the route the host drives the iframe to. */\nexport interface EmbedNavigatePayload {\n /** The Declarion screen route the host wants opened. */\n readonly route: string;\n}\n\n/** `set-theme` payload: the theme the host switches the iframe to. */\nexport interface EmbedSetThemePayload {\n /** The requested theme. */\n readonly theme: EmbedTheme;\n}\n\n/** The theme hint carried on the `theme` URL param and `set-theme` frame. */\nexport type EmbedTheme = \"light\" | \"dark\";\n\n/**\n * Maps each message type to its payload shape. Used to type the inbound\n * classifier and the outbound sender so the payload is checked against the\n * message type.\n */\nexport interface EmbedMessagePayloadMap {\n [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;\n [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;\n [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;\n [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;\n [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;\n [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;\n [EMBED_MESSAGE_TYPES.navigationFailed]: EmbedNavigationFailedPayload;\n [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;\n [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;\n}\n\n/**\n * The wire envelope. Every embed frame is exactly this shape: a fixed\n * `source` + `protocol` pair, a discriminating `type`, and the typed\n * `payload` for that type.\n */\nexport interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {\n readonly source: typeof EMBED_MESSAGE_SOURCE;\n readonly protocol: typeof EMBED_PROTOCOL_VERSION;\n readonly type: T;\n readonly payload: EmbedMessagePayloadMap[T];\n}\n","// Inbound `postMessage` validation - host side.\n//\n// The host SDK receives the iframe-to-parent frames. EVERY inbound frame is\n// validated on two axes before it is acted on:\n// 1. `event.origin === declarionOrigin` - exact string equality, no\n// prefix, no wildcard. A frame from any other origin is dropped.\n// 2. the envelope shape - `source === \"declarion-embed\"` and a numeric\n// `protocol`.\n// A frame failing either check is dropped SILENTLY (security): an untrusted\n// page that re-framed the iframe must learn nothing.\n//\n// A frame that passes origin + source but carries a DIFFERENT `protocol`\n// version is classified `protocol-mismatch`: that frame IS the trusted\n// iframe, just on a mismatched SDK version. The caller surfaces it as a\n// clear console warning naming both versions, never a silent drop.\n//\n// This mirrors the iframe-side classifier:\n// typescript/packages/react/src/embed/handshake.ts\n\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n type EmbedMessage,\n} from \"./protocol\";\n\n/**\n * The classification of an inbound `message` event after validation.\n *\n * - `valid`: a trusted, shape-correct, version-matched embed frame.\n * - `protocol-mismatch`: origin + source are trusted, but the `protocol`\n * version differs. The caller logs a warning; the frame is not acted on.\n * - `rejected`: not a trusted embed frame (wrong origin, missing/foreign\n * `source`, malformed envelope). The caller drops it silently.\n */\nexport type InboundClassification =\n | { kind: \"valid\"; message: EmbedMessage }\n | { kind: \"protocol-mismatch\"; received: unknown }\n | { kind: \"rejected\" };\n\n/**\n * Validate and classify an inbound `message` event against the trusted\n * `declarionOrigin`.\n *\n * Returns `rejected` for anything that is not a trusted embed frame - the\n * caller drops those with no logging. Returns `protocol-mismatch` when the\n * frame is trusted but on a different protocol version. Returns `valid` with\n * the typed envelope otherwise.\n */\nexport function classifyInboundMessage(\n event: MessageEvent,\n declarionOrigin: string,\n): InboundClassification {\n // Axis 1: exact origin match. A trailing slash, a subdomain, a different\n // port - all fail this equality and the frame is dropped.\n if (event.origin !== declarionOrigin) {\n return { kind: \"rejected\" };\n }\n\n // Axis 2: envelope shape. The data must be an object carrying our\n // `source` discriminator and a numeric `protocol`.\n const data = event.data as unknown;\n if (typeof data !== \"object\" || data === null) {\n return { kind: \"rejected\" };\n }\n const envelope = data as Partial<EmbedMessage>;\n if (envelope.source !== EMBED_MESSAGE_SOURCE) {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.protocol !== \"number\") {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.type !== \"string\") {\n return { kind: \"rejected\" };\n }\n\n // Trusted iframe, our envelope, but a different protocol version. Surface\n // it loudly - this is the iframe on a mismatched version, not an attacker.\n if (envelope.protocol !== EMBED_PROTOCOL_VERSION) {\n return { kind: \"protocol-mismatch\", received: envelope.protocol };\n }\n\n return { kind: \"valid\", message: envelope as EmbedMessage };\n}\n","// Typed, actionable embed diagnostics.\n//\n// A silently blank iframe is the worst embedding failure. This module is the\n// single definition of every developer-facing error the SDK can raise. Each\n// error carries a stable `code` (for programmatic handling) and a human\n// message written to be ACTIONABLE - it names the option to fix and, where\n// relevant, the deployment config.\n//\n// Two failure classes, opposite requirements (Decision 21):\n// - Untrusted cross-origin `postMessage` frames: dropped SILENTLY. They are\n// a security concern; never surfaced. The SDK does this in the inbound\n// classifier and never constructs an EmbedError for them.\n// - Developer misconfiguration: surfaced LOUDLY via `onError` AND\n// `console.error`. Every such case is one of the codes below.\n\n/**\n * Stable, machine-readable embed error codes. A host may branch on\n * `error.code`; the strings are part of the public contract.\n */\nexport const EMBED_ERROR_CODES = {\n /**\n * A required `createDeclarionEmbed` option is missing or malformed\n * (`container`, `declarionOrigin`, `route`, `getToken`). Raised\n * synchronously before the iframe is created.\n */\n invalidOptions: \"invalid-options\",\n /**\n * The host's `getToken` callback rejected, threw, or resolved to a value\n * that is not `{ token: string, expires_at: string }`.\n */\n getTokenFailed: \"get-token-failed\",\n /**\n * No `ready` frame arrived from the iframe within the post-mount timeout.\n * The usual causes are a `declarionOrigin` mismatch (the iframe loaded a\n * different origin, or never loaded) or framing denied by the Declarion CSP\n * (the host origin is not in `DECLARION_FRAME_ANCESTORS`). A slow `getToken`\n * does NOT cause this - the timer is cleared as soon as `ready` arrives.\n */\n handshakeTimeout: \"handshake-timeout\",\n /**\n * The iframe asked the host to reload it (`reload-required`) - asset drift\n * or a terminal auth failure inside the iframe.\n */\n reloadRequired: \"reload-required\",\n} as const;\n\n/** The union of all embed error code strings. */\nexport type EmbedErrorCode =\n (typeof EMBED_ERROR_CODES)[keyof typeof EMBED_ERROR_CODES];\n\n/**\n * A typed embed error. Always passed to the host `onError` callback and\n * always also written to `console.error` with the `[declarion-embed]`\n * prefix, so a misconfiguration is never silent.\n *\n * `cause` carries the originating error when one exists (e.g. the rejection\n * value from `getToken`), preserving the stack for debugging.\n */\nexport class EmbedError extends Error {\n /** The stable, machine-readable error code. */\n readonly code: EmbedErrorCode;\n\n constructor(code: EmbedErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"EmbedError\";\n this.code = code;\n // Restore the prototype chain: `extends Error` across the ES5 transpile\n // target otherwise breaks `instanceof EmbedError`.\n Object.setPrototypeOf(this, EmbedError.prototype);\n }\n}\n","// Embed iframe `src` construction.\n//\n// The host SDK builds the iframe URL the Declarion deployment parses. The\n// param grammar MUST match the iframe-side parser EXACTLY:\n// typescript/packages/react/src/embed/params.ts\n// The iframe reads `embed=1`, `parent_origin`, `theme`, `nav`; any drift\n// here silently changes how the iframe boots.\n\nimport type { EmbedNavigationContract, EmbedTheme } from \"./types\";\n\n/** Query-param names that make up the embed URL contract. */\nexport const EMBED_PARAM_EMBED = \"embed\";\nexport const EMBED_PARAM_PARENT_ORIGIN = \"parent_origin\";\nexport const EMBED_PARAM_THEME = \"theme\";\nexport const EMBED_PARAM_NAV = \"nav\";\n\n/** The on-wire value of `embed` that enables shellless render. */\nconst EMBED_PARAM_EMBED_ENABLED = \"1\";\n\n/** Default navigation contract when the host does not set `navigation`. */\nexport const DEFAULT_NAVIGATION_CONTRACT: EmbedNavigationContract = \"self\";\n\n/** Inputs needed to build the iframe `src`. */\nexport interface BuildEmbedSrcInput {\n /** The Declarion deployment origin (`https://app.example.com`). */\n readonly declarionOrigin: string;\n /** The Declarion screen route to embed. */\n readonly route: string;\n /**\n * The host page's own origin. Becomes `parent_origin` so the iframe knows\n * exactly which origin may exchange `postMessage` frames with it.\n */\n readonly parentOrigin: string;\n /** Navigation contract; becomes the `nav` param. */\n readonly navigation: EmbedNavigationContract;\n /** Optional initial theme; becomes the `theme` param when set. */\n readonly theme?: EmbedTheme;\n}\n\n/**\n * Build the absolute iframe `src` URL for an embedded Declarion screen.\n *\n * The `route` is resolved as a path against `declarionOrigin`; the four\n * embed params are appended. `parent_origin` is the host's own origin so the\n * iframe restricts its `postMessage` traffic to exactly that origin.\n *\n * Throws when `declarionOrigin` is not a parseable absolute origin - callers\n * convert that into a typed `EmbedError` before raising it to the host.\n */\nexport function buildEmbedSrc(input: BuildEmbedSrcInput): string {\n // `route` may be a bare path (`/cases`) or already carry a query/hash.\n // Resolving it against the origin keeps any route-level query intact while\n // the embed params are layered on top.\n const url = new URL(input.route, input.declarionOrigin);\n url.searchParams.set(EMBED_PARAM_EMBED, EMBED_PARAM_EMBED_ENABLED);\n url.searchParams.set(EMBED_PARAM_PARENT_ORIGIN, input.parentOrigin);\n url.searchParams.set(EMBED_PARAM_NAV, input.navigation);\n if (input.theme) {\n url.searchParams.set(EMBED_PARAM_THEME, input.theme);\n }\n return url.toString();\n}\n\n/**\n * Resolve a Declarion screen route to an absolute URL for a runtime\n * `navigate` frame. The host passes a route string; the iframe consumes the\n * route as-is, so this only normalizes it against the deployment origin for\n * the host's own bookkeeping. Returns the input unchanged when it cannot be\n * resolved (the iframe tolerates a relative route).\n */\nexport function resolveRoute(declarionOrigin: string, route: string): string {\n try {\n const url = new URL(route, declarionOrigin);\n // Keep the hash so a host can deep-link to an in-page anchor; dropping it\n // would silently break `handle.navigate(\"/cases/42#notes\")`.\n return url.pathname + url.search + url.hash;\n } catch {\n return route;\n }\n}\n","// `createDeclarionEmbed` - the framework-agnostic, dependency-free embed core.\n//\n// Builds the iframe, runs the `ready` -> `auth` handshake, owns token refresh\n// through the host `getToken` callback, auto-applies `resize`, mirrors\n// navigation, and surfaces misconfiguration loudly. The React binding\n// (`./react`) and the demo host both build on this single core.\n\nimport {\n EMBED_MESSAGE_TYPES,\n EMBED_PROTOCOL_VERSION,\n type EmbedSetTokenPayload,\n type EmbedMessage,\n type EmbedMessagePayloadMap,\n type EmbedMessageType,\n type EmbedDirtyChangedPayload,\n type EmbedNavigationFailedPayload,\n type EmbedNavigationPayload,\n type EmbedReloadRequiredPayload,\n type EmbedResizedPayload,\n} from \"./protocol\";\nimport { classifyInboundMessage } from \"./inbound\";\nimport {\n EMBED_ERROR_CODES,\n EmbedError,\n type EmbedErrorCode,\n} from \"./errors\";\nimport {\n DEFAULT_NAVIGATION_CONTRACT,\n buildEmbedSrc,\n resolveRoute,\n} from \"./url\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n EmbedNavigateEvent,\n EmbedToken,\n} from \"./types\";\n\n/**\n * How long the SDK waits for the first `auth` frame to be requested by the\n * iframe after it loads. The iframe emits `ready`; if no `ready` arrives -\n * usually a `declarionOrigin` mismatch or framing denied by the Declarion\n * CSP - the SDK raises a `handshake-timeout` error rather than leave a\n * silently blank iframe.\n */\nconst HANDSHAKE_TIMEOUT_MS = 20_000;\n\n/** Default iframe `title` when the host does not supply one. */\nconst DEFAULT_IFRAME_TITLE = \"Declarion embedded screen\";\n\n/**\n * Default iframe `sandbox` tokens. Comprehensive enough to run the full app\n * (scripts, same-origin storage/cookies, forms, modals, downloads, and popups\n * that escape the sandbox so external links open as normal windows), but\n * deliberately OMITTING the top-navigation tokens so the framed app cannot\n * navigate the host's top window.\n */\nconst DEFAULT_SANDBOX_TOKENS: readonly string[] = [\n \"allow-scripts\",\n \"allow-same-origin\",\n \"allow-forms\",\n \"allow-modals\",\n \"allow-downloads\",\n \"allow-popups\",\n \"allow-popups-to-escape-sandbox\",\n];\n\n/** Tokens a host may never add - they would defeat the top-nav containment. */\nconst FORBIDDEN_SANDBOX_TOKENS: ReadonlySet<string> = new Set([\n \"allow-top-navigation\",\n \"allow-top-navigation-by-user-activation\",\n]);\n\n/**\n * Build the iframe `sandbox` value: the default token set plus any host\n * `extraSandbox` tokens, with the forbidden top-navigation tokens stripped so\n * a host can widen the sandbox but never re-enable a top-frame escape.\n */\nfunction buildSandbox(extra?: readonly string[]): string {\n const tokens = new Set(DEFAULT_SANDBOX_TOKENS);\n for (const token of extra ?? []) {\n const trimmed = token.trim();\n if (trimmed && !FORBIDDEN_SANDBOX_TOKENS.has(trimmed)) tokens.add(trimmed);\n }\n return Array.from(tokens).join(\" \");\n}\n\n/** Console prefix for every SDK diagnostic line. */\nconst LOG_PREFIX = \"[declarion-embed]\";\n\n/**\n * Validate `createDeclarionEmbed` options. Returns a typed `EmbedError` for\n * the first problem found, or `null` when the options are well-formed.\n *\n * Separated from `createDeclarionEmbed` so the React binding can reuse the\n * exact same validation.\n */\nfunction validateOptions(options: DeclarionEmbedOptions): EmbedError | null {\n const fail = (message: string): EmbedError =>\n new EmbedError(EMBED_ERROR_CODES.invalidOptions, message);\n\n if (!options.container || typeof options.container.appendChild !== \"function\") {\n return fail(\n \"`container` must be a DOM element that can receive the iframe.\",\n );\n }\n if (typeof options.declarionOrigin !== \"string\" || options.declarionOrigin === \"\") {\n return fail(\n \"`declarionOrigin` is required and must be the exact origin of the \" +\n \"Declarion deployment, e.g. \\\"https://app.example.com\\\".\",\n );\n }\n let parsedOrigin: URL;\n try {\n parsedOrigin = new URL(options.declarionOrigin);\n } catch {\n return fail(\n `\\`declarionOrigin\\` is not a valid URL: \"${options.declarionOrigin}\". ` +\n 'Pass an exact origin, e.g. \"https://app.example.com\".',\n );\n }\n if (parsedOrigin.origin !== options.declarionOrigin) {\n return fail(\n `\\`declarionOrigin\\` must be exactly an origin with no path, query, ` +\n `or trailing slash. Got \"${options.declarionOrigin}\"; expected ` +\n `\"${parsedOrigin.origin}\".`,\n );\n }\n if (typeof options.route !== \"string\" || options.route === \"\") {\n return fail(\"`route` is required and must be a Declarion screen route.\");\n }\n if (typeof options.getToken !== \"function\") {\n return fail(\n \"`getToken` is required and must be an async function returning \" +\n \"{ token, expires_at }.\",\n );\n }\n return null;\n}\n\n/**\n * Validate the value a host `getToken` callback resolved to. The host owns\n * this code; a malformed return is a developer mistake, surfaced loudly.\n */\nfunction isValidToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Create an embedded Declarion screen inside `options.container`.\n *\n * Returns a `DeclarionEmbedHandle` exposing `navigate`, `setTheme`, and\n * `destroy`. On a misconfiguration the SDK still returns a handle (so\n * `destroy` is always callable) but reports the problem through `onError`\n * and `console.error`; the iframe is not created in that case.\n */\nexport function createDeclarionEmbed(\n options: DeclarionEmbedOptions,\n): DeclarionEmbedHandle {\n let destroyed = false;\n let iframe: HTMLIFrameElement | null = null;\n let messageListener: ((event: MessageEvent) => void) | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let readyReceived = false;\n let firstAuthDelivered = false;\n // 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
|
-
|
|
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)])
|
|
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
|
|
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"}
|