@declarion/embed 0.1.92
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/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +213 -0
- package/dist/core.js.map +1 -0
- package/dist/declarion-embed.iife.js +2 -0
- package/dist/declarion-embed.iife.js.map +1 -0
- package/dist/errors.d.ts +47 -0
- package/dist/inbound.d.ts +29 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -0
- package/dist/protocol.d.ts +128 -0
- package/dist/react.d.ts +27 -0
- package/dist/react.js +50 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +50 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/url.d.ts +43 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Disciplinedware LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @declarion/embed
|
|
2
|
+
|
|
3
|
+
Host integration SDK for embedding [Declarion](https://declarion.io) screens
|
|
4
|
+
as white-label iframes in a third-party app.
|
|
5
|
+
|
|
6
|
+
A Declarion screen route renders without Declarion shell chrome when loaded
|
|
7
|
+
with `?embed=1`. The host app owns the outer shell, navigation, and identity;
|
|
8
|
+
Declarion owns screen rendering, data access, and tenant isolation. This
|
|
9
|
+
package is the typed integration layer for both sides:
|
|
10
|
+
|
|
11
|
+
- **Browser core** (`@declarion/embed`) - dependency-free. Creates the
|
|
12
|
+
iframe, runs the auth handshake, auto-resizes, mirrors navigation.
|
|
13
|
+
- **React binding** (`@declarion/embed/react`) - `<DeclarionEmbed />`.
|
|
14
|
+
- **Server helper** (`@declarion/embed/server`) - `createEmbedSession`, the
|
|
15
|
+
Node-side mint call. Keeps the `dk:` API key out of browser code.
|
|
16
|
+
|
|
17
|
+
The host installs only `@declarion/embed`; it does **not** depend on
|
|
18
|
+
`@declarion/react` (the full Declarion UI SDK).
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @declarion/embed
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`react` and `react-dom` are optional peer dependencies, required only by the
|
|
27
|
+
`@declarion/embed/react` entry.
|
|
28
|
+
|
|
29
|
+
## How embedding works
|
|
30
|
+
|
|
31
|
+
1. The host backend mints a short-lived, scoped token for a user with
|
|
32
|
+
`createEmbedSession` (`@declarion/embed/server`), which calls the
|
|
33
|
+
`auth.create_embed_session` action with a server-held `dk:` API key.
|
|
34
|
+
2. The host frontend embeds a screen with `createDeclarionEmbed` or
|
|
35
|
+
`<DeclarionEmbed />`, passing a `getToken` callback that fetches a token
|
|
36
|
+
from the host backend.
|
|
37
|
+
3. The SDK runs a `postMessage` handshake: the iframe asks for a token, the
|
|
38
|
+
SDK delivers it, and refreshes it whenever the iframe reports expiry.
|
|
39
|
+
|
|
40
|
+
The `dk:` API key never leaves the host backend.
|
|
41
|
+
|
|
42
|
+
## Quickstart: browser core
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createDeclarionEmbed } from "@declarion/embed";
|
|
46
|
+
|
|
47
|
+
const embed = createDeclarionEmbed({
|
|
48
|
+
container: document.getElementById("declarion")!,
|
|
49
|
+
declarionOrigin: "https://app.example.com",
|
|
50
|
+
route: "/cases",
|
|
51
|
+
getToken: async () => {
|
|
52
|
+
const res = await fetch("/host-api/embed-token");
|
|
53
|
+
return res.json(); // { token, expires_at }
|
|
54
|
+
},
|
|
55
|
+
onError: (err) => console.error(err.code, err.message),
|
|
56
|
+
});
|
|
57
|
+
// Later: embed.navigate("/cases/42"); embed.setTheme("dark"); embed.destroy();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quickstart: React
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { DeclarionEmbed } from "@declarion/embed/react";
|
|
64
|
+
|
|
65
|
+
export function CasesPanel() {
|
|
66
|
+
return (
|
|
67
|
+
<DeclarionEmbed
|
|
68
|
+
declarionOrigin="https://app.example.com"
|
|
69
|
+
route="/cases"
|
|
70
|
+
getToken={async () => (await fetch("/host-api/embed-token")).json()}
|
|
71
|
+
onError={(err) => console.error(err.code, err.message)}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quickstart: server helper (Node)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { createEmbedSession } from "@declarion/embed/server";
|
|
81
|
+
|
|
82
|
+
// In a host backend route. The dk: API key is read from server config.
|
|
83
|
+
const session = await createEmbedSession({
|
|
84
|
+
declarionOrigin: "https://app.example.com",
|
|
85
|
+
apiKey: process.env.DECLARION_API_KEY!, // dk:... - server-side only
|
|
86
|
+
tenantCode: "acme",
|
|
87
|
+
userEmail: "person@example.com",
|
|
88
|
+
screenCode: "cases_list",
|
|
89
|
+
});
|
|
90
|
+
// Return `session` ({ token, expires_at }) to the host frontend's getToken.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Hosts on a non-Node backend call the
|
|
94
|
+
`POST /api/actions/auth.create_embed_session` action over HTTP directly with
|
|
95
|
+
the same `dk:` API key in the `Authorization` header.
|
|
96
|
+
|
|
97
|
+
## Script tag (no build step)
|
|
98
|
+
|
|
99
|
+
The package also ships an IIFE bundle of the core. Load it and use
|
|
100
|
+
`window.DeclarionEmbed.createDeclarionEmbed`:
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<script src="https://unpkg.com/@declarion/embed/dist/declarion-embed.iife.js"></script>
|
|
104
|
+
<script>
|
|
105
|
+
DeclarionEmbed.createDeclarionEmbed({ /* options */ });
|
|
106
|
+
</script>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Navigation modes
|
|
110
|
+
|
|
111
|
+
- `navigation: "self"` (default) - the iframe navigates internally and emits
|
|
112
|
+
`onNavigate({ mode: "self", ... })` so the host can mirror its URL.
|
|
113
|
+
- `navigation: "delegated"` - the iframe never moves; it emits
|
|
114
|
+
`onNavigate({ mode: "delegated", ... })` and the host opens the route.
|
|
115
|
+
|
|
116
|
+
## Unsaved changes
|
|
117
|
+
|
|
118
|
+
A Declarion screen can hold unsaved edits. When the host navigates the iframe
|
|
119
|
+
away from such a screen - via its own menu, or `handle.navigate()` - it should
|
|
120
|
+
confirm with the user first. The iframe reports its edit state through the
|
|
121
|
+
`onEvent` callback:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
let embedDirty = false;
|
|
125
|
+
createDeclarionEmbed({
|
|
126
|
+
// ...
|
|
127
|
+
onEvent(event) {
|
|
128
|
+
if (event.type === "dirty-changed") embedDirty = event.payload.dirty;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Before the host moves the iframe away:
|
|
133
|
+
if (embedDirty && !confirm("Discard unsaved changes?")) return;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Navigation *inside* the iframe (a row click, a Back button) is already guarded
|
|
137
|
+
by Declarion itself; this is only for navigation the host initiates, which the
|
|
138
|
+
iframe cannot intercept.
|
|
139
|
+
|
|
140
|
+
## Diagnostics
|
|
141
|
+
|
|
142
|
+
A misconfiguration is surfaced loudly through `onError` (a typed
|
|
143
|
+
`EmbedError` with a stable `code`) **and** `console.error`. Untrusted
|
|
144
|
+
cross-origin `postMessage` frames are dropped silently. The SDK detects:
|
|
145
|
+
|
|
146
|
+
- `invalid-options` - a required option is missing or malformed.
|
|
147
|
+
- `get-token-failed` - `getToken` rejected or returned a malformed result.
|
|
148
|
+
- `handshake-timeout` - no handshake within the post-load window, usually a
|
|
149
|
+
`declarionOrigin` mismatch or framing denied by the Declarion CSP
|
|
150
|
+
(`DECLARION_FRAME_ANCESTORS` does not list the host origin).
|
|
151
|
+
- `reload-required` - the iframe asked the host to reload it.
|
|
152
|
+
|
|
153
|
+
A protocol version mismatch between this SDK and the Declarion deployment
|
|
154
|
+
produces a clear `console.warn` naming both versions.
|
|
155
|
+
|
|
156
|
+
## Reference integration
|
|
157
|
+
|
|
158
|
+
A complete, runnable foreign-host app lives at
|
|
159
|
+
[`typescript/examples/embed-host`](../../examples/embed-host) in the Declarion
|
|
160
|
+
Core monorepo. It embeds list, detail, and action screens, demonstrates both
|
|
161
|
+
navigation modes, and mints tokens with `@declarion/embed/server` from a host
|
|
162
|
+
backend that holds the `dk:` API key server-side only. Use it as the
|
|
163
|
+
copy-from template for a real integration.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DeclarionEmbedHandle, DeclarionEmbedOptions } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Create an embedded Declarion screen inside `options.container`.
|
|
4
|
+
*
|
|
5
|
+
* Returns a `DeclarionEmbedHandle` exposing `navigate`, `setTheme`, and
|
|
6
|
+
* `destroy`. On a misconfiguration the SDK still returns a handle (so
|
|
7
|
+
* `destroy` is always callable) but reports the problem through `onError`
|
|
8
|
+
* and `console.error`; the iframe is not created in that case.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createDeclarionEmbed(options: DeclarionEmbedOptions): DeclarionEmbedHandle;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
//#region src/protocol.ts
|
|
2
|
+
var e = "declarion-embed", t = 1, n = {
|
|
3
|
+
ready: "ready",
|
|
4
|
+
setToken: "set-token",
|
|
5
|
+
tokenExpired: "token-expired",
|
|
6
|
+
reloadRequired: "reload-required",
|
|
7
|
+
resized: "resized",
|
|
8
|
+
navigated: "navigated",
|
|
9
|
+
navigationRequested: "navigation-requested",
|
|
10
|
+
dirtyChanged: "dirty-changed",
|
|
11
|
+
navigate: "navigate",
|
|
12
|
+
setTheme: "set-theme"
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/inbound.ts
|
|
16
|
+
function r(e, t) {
|
|
17
|
+
if (e.origin !== t) return { kind: "rejected" };
|
|
18
|
+
let n = e.data;
|
|
19
|
+
if (typeof n != "object" || !n) return { kind: "rejected" };
|
|
20
|
+
let r = n;
|
|
21
|
+
return r.source !== "declarion-embed" || typeof r.protocol != "number" || typeof r.type != "string" ? { kind: "rejected" } : r.protocol === 1 ? {
|
|
22
|
+
kind: "valid",
|
|
23
|
+
message: r
|
|
24
|
+
} : {
|
|
25
|
+
kind: "protocol-mismatch",
|
|
26
|
+
received: r.protocol
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/errors.ts
|
|
31
|
+
var i = {
|
|
32
|
+
invalidOptions: "invalid-options",
|
|
33
|
+
getTokenFailed: "get-token-failed",
|
|
34
|
+
handshakeTimeout: "handshake-timeout",
|
|
35
|
+
reloadRequired: "reload-required"
|
|
36
|
+
}, a = class e extends Error {
|
|
37
|
+
code;
|
|
38
|
+
constructor(t, n, r) {
|
|
39
|
+
super(n, r), this.name = "EmbedError", this.code = t, Object.setPrototypeOf(this, e.prototype);
|
|
40
|
+
}
|
|
41
|
+
}, o = "embed", s = "parent_origin", c = "theme", l = "1";
|
|
42
|
+
function u(e) {
|
|
43
|
+
let t = new URL(e.route, e.declarionOrigin);
|
|
44
|
+
return t.searchParams.set(o, l), t.searchParams.set(s, e.parentOrigin), t.searchParams.set("nav", e.navigation), e.theme && t.searchParams.set(c, e.theme), t.toString();
|
|
45
|
+
}
|
|
46
|
+
function d(e, t) {
|
|
47
|
+
try {
|
|
48
|
+
let n = new URL(t, e);
|
|
49
|
+
return n.pathname + n.search + n.hash;
|
|
50
|
+
} catch {
|
|
51
|
+
return t;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/core.ts
|
|
56
|
+
var f = 2e4, p = "Declarion embedded screen", m = "[declarion-embed]";
|
|
57
|
+
function h(e) {
|
|
58
|
+
let t = (e) => new a(i.invalidOptions, e);
|
|
59
|
+
if (!e.container || typeof e.container.appendChild != "function") return t("`container` must be a DOM element that can receive the iframe.");
|
|
60
|
+
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\".");
|
|
61
|
+
let n;
|
|
62
|
+
try {
|
|
63
|
+
n = new URL(e.declarionOrigin);
|
|
64
|
+
} catch {
|
|
65
|
+
return t(`\`declarionOrigin\` is not a valid URL: "${e.declarionOrigin}". Pass an exact origin, e.g. "https://app.example.com".`);
|
|
66
|
+
}
|
|
67
|
+
return n.origin === e.declarionOrigin ? typeof e.route != "string" || e.route === "" ? t("`route` is required and must be a Declarion screen route.") : typeof e.getToken == "function" ? null : t("`getToken` is required and must be an async function returning { token, expires_at }.") : t(`\`declarionOrigin\` must be exactly an origin with no path, query, or trailing slash. Got "${e.declarionOrigin}"; expected "${n.origin}".`);
|
|
68
|
+
}
|
|
69
|
+
function g(e) {
|
|
70
|
+
if (typeof e != "object" || !e) return !1;
|
|
71
|
+
let t = e;
|
|
72
|
+
return typeof t.token == "string" && t.token !== "" && typeof t.expires_at == "string" && t.expires_at !== "";
|
|
73
|
+
}
|
|
74
|
+
function _(e) {
|
|
75
|
+
let t = !1, o = null, s = null, c = null, l = !1, _ = !1, v = (t, n) => {
|
|
76
|
+
e.debug && (n === void 0 ? console.info(`${m} ${t}`) : console.info(`${m} ${t}`, n));
|
|
77
|
+
}, y = (t, n, r) => {
|
|
78
|
+
let i = new a(t, n, r ? { cause: r } : void 0);
|
|
79
|
+
console.error(`${m} ${n}`, r ?? ""), e.onError?.(i);
|
|
80
|
+
}, b = (t, n) => {
|
|
81
|
+
if (!o?.contentWindow) return;
|
|
82
|
+
let r = {
|
|
83
|
+
source: "declarion-embed",
|
|
84
|
+
protocol: 1,
|
|
85
|
+
type: t,
|
|
86
|
+
payload: n
|
|
87
|
+
};
|
|
88
|
+
v(`-> iframe ${t}`, n), o.contentWindow.postMessage(r, e.declarionOrigin);
|
|
89
|
+
}, x = async () => {
|
|
90
|
+
let r;
|
|
91
|
+
try {
|
|
92
|
+
r = await e.getToken();
|
|
93
|
+
} catch (e) {
|
|
94
|
+
y(i.getTokenFailed, "`getToken` rejected. The host backend must mint a token via auth.create_embed_session and return { token, expires_at }.", e);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!g(r)) {
|
|
98
|
+
y(i.getTokenFailed, "`getToken` resolved to a malformed value. Expected { token: string, expires_at: string }.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (t) return;
|
|
102
|
+
let a = {
|
|
103
|
+
token: r.token,
|
|
104
|
+
expires_at: r.expires_at
|
|
105
|
+
};
|
|
106
|
+
b(n.setToken, a), O();
|
|
107
|
+
}, S = () => {
|
|
108
|
+
l = !0, k(), v("<- iframe ready"), e.onEvent?.({ type: n.ready }), x();
|
|
109
|
+
}, C = () => {
|
|
110
|
+
v("<- iframe token-expired"), e.onEvent?.({ type: n.tokenExpired }), x();
|
|
111
|
+
}, w = (t) => {
|
|
112
|
+
v("<- iframe reload-required", t), e.onEvent?.({
|
|
113
|
+
type: n.reloadRequired,
|
|
114
|
+
payload: t
|
|
115
|
+
}), y(i.reloadRequired, `The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`);
|
|
116
|
+
}, T = (t) => {
|
|
117
|
+
v("<- iframe resized", t), o && Number.isFinite(t.height) && t.height > 0 && (o.style.height = `${t.height}px`), e.onEvent?.({
|
|
118
|
+
type: n.resized,
|
|
119
|
+
payload: t
|
|
120
|
+
});
|
|
121
|
+
}, E = (t) => {
|
|
122
|
+
v("<- iframe dirty-changed", t), e.onEvent?.({
|
|
123
|
+
type: n.dirtyChanged,
|
|
124
|
+
payload: t
|
|
125
|
+
});
|
|
126
|
+
}, D = (t, r) => {
|
|
127
|
+
v(`<- iframe ${t}`, r), e.onEvent?.({
|
|
128
|
+
type: t,
|
|
129
|
+
payload: r
|
|
130
|
+
});
|
|
131
|
+
let i = {
|
|
132
|
+
mode: t === n.navigated ? "self" : "delegated",
|
|
133
|
+
route: r.route,
|
|
134
|
+
entity: r.entity,
|
|
135
|
+
recordId: r.recordId
|
|
136
|
+
};
|
|
137
|
+
e.onNavigate?.(i);
|
|
138
|
+
}, O = () => {
|
|
139
|
+
_ || (_ = !0, v("handshake complete - iframe authenticated"), e.onReady?.());
|
|
140
|
+
}, k = () => {
|
|
141
|
+
c !== null && (clearTimeout(c), c = null);
|
|
142
|
+
}, A = (t) => {
|
|
143
|
+
let i = r(t, e.declarionOrigin);
|
|
144
|
+
if (i.kind === "rejected") return;
|
|
145
|
+
if (i.kind === "protocol-mismatch") {
|
|
146
|
+
console.warn(`${m} protocol version mismatch: this SDK speaks protocol 1, the iframe reported protocol ${String(i.received)}. Update @declarion/embed and the Declarion deployment to matching versions.`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let { message: a } = i;
|
|
150
|
+
switch (a.type) {
|
|
151
|
+
case n.ready:
|
|
152
|
+
S();
|
|
153
|
+
break;
|
|
154
|
+
case n.tokenExpired:
|
|
155
|
+
C();
|
|
156
|
+
break;
|
|
157
|
+
case n.reloadRequired:
|
|
158
|
+
w(a.payload);
|
|
159
|
+
break;
|
|
160
|
+
case n.resized:
|
|
161
|
+
T(a.payload);
|
|
162
|
+
break;
|
|
163
|
+
case n.navigated:
|
|
164
|
+
D(n.navigated, a.payload);
|
|
165
|
+
break;
|
|
166
|
+
case n.navigationRequested:
|
|
167
|
+
D(n.navigationRequested, a.payload);
|
|
168
|
+
break;
|
|
169
|
+
case n.dirtyChanged:
|
|
170
|
+
E(a.payload);
|
|
171
|
+
break;
|
|
172
|
+
default: break;
|
|
173
|
+
}
|
|
174
|
+
}, j = h(e);
|
|
175
|
+
if (j) return console.error(`${m} ${j.message}`), e.onError?.(j), {
|
|
176
|
+
navigate: () => void 0,
|
|
177
|
+
setTheme: () => void 0,
|
|
178
|
+
destroy: () => void 0
|
|
179
|
+
};
|
|
180
|
+
let M = e.navigation ?? "self", N = window.location.origin, P;
|
|
181
|
+
try {
|
|
182
|
+
P = u({
|
|
183
|
+
declarionOrigin: e.declarionOrigin,
|
|
184
|
+
route: e.route,
|
|
185
|
+
parentOrigin: N,
|
|
186
|
+
navigation: M,
|
|
187
|
+
theme: e.theme
|
|
188
|
+
});
|
|
189
|
+
} catch (t) {
|
|
190
|
+
return y(i.invalidOptions, `Could not build the iframe URL from route "${e.route}". \`route\` must be a valid Declarion screen route.`, t), {
|
|
191
|
+
navigate: () => void 0,
|
|
192
|
+
setTheme: () => void 0,
|
|
193
|
+
destroy: () => void 0
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return o = document.createElement("iframe"), o.src = P, o.title = e.title ?? p, o.style.width = "100%", o.style.border = "0", o.style.display = "block", o.style.height || (o.style.height = "150px"), s = A, window.addEventListener("message", s), e.container.appendChild(o), v("iframe created", { src: P }), c = setTimeout(() => {
|
|
197
|
+
c = null, !l && y(i.handshakeTimeout, `No handshake from the iframe within ${f}ms. Check that \`declarionOrigin\` exactly matches the Declarion deployment origin, that the host origin is allow-listed in the deployment's DECLARION_FRAME_ANCESTORS, and that \`route\` resolves to a real screen.`);
|
|
198
|
+
}, f), {
|
|
199
|
+
navigate(r) {
|
|
200
|
+
t || b(n.navigate, { route: d(e.declarionOrigin, r) });
|
|
201
|
+
},
|
|
202
|
+
setTheme(e) {
|
|
203
|
+
t || b(n.setTheme, { theme: e });
|
|
204
|
+
},
|
|
205
|
+
destroy() {
|
|
206
|
+
t || (t = !0, k(), s &&= (window.removeEventListener("message", s), null), o?.parentNode && o.parentNode.removeChild(o), o = null, v("embed destroyed"));
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
export { n as a, e as i, i as n, t as o, a as r, _ as t };
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=core.js.map
|
package/dist/core.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","names":[],"sources":["../src/protocol.ts","../src/inbound.ts","../src/errors.ts","../src/url.ts","../src/core.ts"],"sourcesContent":["// Embed postMessage protocol - host-side contract.\n//\n// `@declarion/embed` is a SEPARATE npm package from `@declarion/react` and\n// MUST NOT depend on it (a host app must not pull the full Declarion UI SDK\n// to host an iframe). The protocol contract is therefore re-declared here,\n// independently, and MUST match the iframe side EXACTLY:\n// typescript/packages/react/src/embed/protocol.ts\n// A divergence breaks the handshake. Any change to the wire envelope, the\n// message-type set, or a payload shape MUST land in both files together.\n//\n// Wire envelope: { source: \"declarion-embed\", protocol: 1, type, payload }.\n//\n// Naming contract (developer-facing - keep it consistent):\n// - A message the iframe SENDS to the host is an EVENT, named in the past\n// tense - \"this happened\": `resized`, `navigated`, `navigation-requested`,\n// `dirty-changed`, `token-expired`, `reload-required`, `ready`.\n// - A message the host SENDS to the iframe is a COMMAND, named as an\n// imperative - \"do this\": `set-token`, `navigate`, `set-theme`.\n//\n// Security model: the host SDK validates EVERY inbound frame on two axes -\n// exact origin equality against the configured `declarionOrigin`, and the\n// envelope shape (`source` discriminator + numeric `protocol`). A frame that\n// fails either check is dropped SILENTLY; an untrusted page that re-framed\n// the iframe must learn nothing. Outbound host frames target the EXACT\n// `declarionOrigin`, never `\"*\"`.\n\n/**\n * The `source` discriminator stamped on every embed frame. Both the host SDK\n * and the iframe filter inbound traffic on this value so unrelated\n * `postMessage` frames (browser extensions, other widgets) are ignored.\n */\nexport const EMBED_MESSAGE_SOURCE = \"declarion-embed\" as const;\n\n/**\n * The protocol version this SDK speaks. Bumped only on a breaking\n * envelope/payload change. The SDK warns when the iframe reports a different\n * version (see the diagnostics path).\n */\nexport const EMBED_PROTOCOL_VERSION = 1 as const;\n\n/**\n * Every protocol message `type`. The object key is the camelCase name used in\n * code; the value is the on-wire string. Events (iframe -> host) are past\n * tense; commands (host -> iframe) are imperative. This SDK is the HOST side:\n * it RECEIVES the iframe-to-host events and SENDS the host-to-iframe commands.\n */\nexport const EMBED_MESSAGE_TYPES = {\n /** iframe -> host: SDK mounted, requests the first token. */\n ready: \"ready\",\n /** host -> iframe: deliver or refresh the embed token. */\n setToken: \"set-token\",\n /** iframe -> host: token rejected or near expiry. */\n tokenExpired: \"token-expired\",\n /** iframe -> host: the iframe must be reloaded. */\n reloadRequired: \"reload-required\",\n /** iframe -> host: embed content height changed. */\n resized: \"resized\",\n /** iframe -> host: `self` mode internal navigation happened. */\n navigated: \"navigated\",\n /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */\n navigationRequested: \"navigation-requested\",\n /** iframe -> host: the embedded screen's unsaved-edits state flipped. */\n dirtyChanged: \"dirty-changed\",\n /** host -> iframe: drive the iframe to a screen (deep-linking). */\n navigate: \"navigate\",\n /** host -> iframe: runtime theme switch. */\n setTheme: \"set-theme\",\n} as const;\n\n/** The union of all on-wire message `type` strings. */\nexport type EmbedMessageType =\n (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];\n\n// --- Payload interfaces, one per message type ---\n\n/** `ready` payload: empty - the frame itself is the signal. */\nexport type EmbedReadyPayload = Record<string, never>;\n\n/** `set-token` payload: the embed token and its absolute expiry. */\nexport interface EmbedSetTokenPayload {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** RFC 3339 expiry timestamp of the token. */\n readonly expires_at: string;\n}\n\n/** `token-expired` payload: empty - the host re-mints via `getToken`. */\nexport type EmbedTokenExpiredPayload = Record<string, never>;\n\n/** `reload-required` payload: a human-readable reason for the reload. */\nexport interface EmbedReloadRequiredPayload {\n /** Why the iframe must be reloaded (asset drift, terminal auth failure). */\n readonly reason: string;\n}\n\n/** `resized` payload: the measured content height in CSS pixels. */\nexport interface EmbedResizedPayload {\n /** Content height in CSS pixels. */\n readonly height: number;\n}\n\n/**\n * `navigated` / `navigation-requested` payload: the screen route and, when\n * resolvable, the bound entity code and record id.\n *\n * `entity` and `recordId` are optional: a `custom` screen has no entity, and\n * a list route has no record id.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /** The bound entity code, when the route resolves to one. */\n readonly entity?: string;\n /** The record id, when the route is a detail route with an id. */\n readonly recordId?: string;\n}\n\n/**\n * `dirty-changed` payload: whether the embedded screen currently has unsaved\n * edits. The host tracks this so it can guard its own navigation (its menu,\n * a host-initiated `navigate`) before moving the iframe away from a dirty\n * screen - the iframe never pops a dialog for host-driven navigation.\n */\nexport interface EmbedDirtyChangedPayload {\n /** True when the embedded screen has unsaved edits. */\n readonly dirty: boolean;\n}\n\n/** `navigate` payload: the route the host drives the iframe to. */\nexport interface EmbedNavigatePayload {\n /** The Declarion screen route the host wants opened. */\n readonly route: string;\n}\n\n/** `set-theme` payload: the theme the host switches the iframe to. */\nexport interface EmbedSetThemePayload {\n /** The requested theme. */\n readonly theme: EmbedTheme;\n}\n\n/** The theme hint carried on the `theme` URL param and `set-theme` frame. */\nexport type EmbedTheme = \"light\" | \"dark\";\n\n/**\n * Maps each message type to its payload shape. Used to type the inbound\n * classifier and the outbound sender so the payload is checked against the\n * message type.\n */\nexport interface EmbedMessagePayloadMap {\n [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;\n [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;\n [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;\n [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;\n [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;\n [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;\n [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;\n [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;\n}\n\n/**\n * The wire envelope. Every embed frame is exactly this shape: a fixed\n * `source` + `protocol` pair, a discriminating `type`, and the typed\n * `payload` for that type.\n */\nexport interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {\n readonly source: typeof EMBED_MESSAGE_SOURCE;\n readonly protocol: typeof EMBED_PROTOCOL_VERSION;\n readonly type: T;\n readonly payload: EmbedMessagePayloadMap[T];\n}\n","// Inbound `postMessage` validation - host side.\n//\n// The host SDK receives the iframe-to-parent frames. EVERY inbound frame is\n// validated on two axes before it is acted on:\n// 1. `event.origin === declarionOrigin` - exact string equality, no\n// prefix, no wildcard. A frame from any other origin is dropped.\n// 2. the envelope shape - `source === \"declarion-embed\"` and a numeric\n// `protocol`.\n// A frame failing either check is dropped SILENTLY (security): an untrusted\n// page that re-framed the iframe must learn nothing.\n//\n// A frame that passes origin + source but carries a DIFFERENT `protocol`\n// version is classified `protocol-mismatch`: that frame IS the trusted\n// iframe, just on a mismatched SDK version. The caller surfaces it as a\n// clear console warning naming both versions, never a silent drop.\n//\n// This mirrors the iframe-side classifier:\n// typescript/packages/react/src/embed/handshake.ts\n\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n type EmbedMessage,\n} from \"./protocol\";\n\n/**\n * The classification of an inbound `message` event after validation.\n *\n * - `valid`: a trusted, shape-correct, version-matched embed frame.\n * - `protocol-mismatch`: origin + source are trusted, but the `protocol`\n * version differs. The caller logs a warning; the frame is not acted on.\n * - `rejected`: not a trusted embed frame (wrong origin, missing/foreign\n * `source`, malformed envelope). The caller drops it silently.\n */\nexport type InboundClassification =\n | { kind: \"valid\"; message: EmbedMessage }\n | { kind: \"protocol-mismatch\"; received: unknown }\n | { kind: \"rejected\" };\n\n/**\n * Validate and classify an inbound `message` event against the trusted\n * `declarionOrigin`.\n *\n * Returns `rejected` for anything that is not a trusted embed frame - the\n * caller drops those with no logging. Returns `protocol-mismatch` when the\n * frame is trusted but on a different protocol version. Returns `valid` with\n * the typed envelope otherwise.\n */\nexport function classifyInboundMessage(\n event: MessageEvent,\n declarionOrigin: string,\n): InboundClassification {\n // Axis 1: exact origin match. A trailing slash, a subdomain, a different\n // port - all fail this equality and the frame is dropped.\n if (event.origin !== declarionOrigin) {\n return { kind: \"rejected\" };\n }\n\n // Axis 2: envelope shape. The data must be an object carrying our\n // `source` discriminator and a numeric `protocol`.\n const data = event.data as unknown;\n if (typeof data !== \"object\" || data === null) {\n return { kind: \"rejected\" };\n }\n const envelope = data as Partial<EmbedMessage>;\n if (envelope.source !== EMBED_MESSAGE_SOURCE) {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.protocol !== \"number\") {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.type !== \"string\") {\n return { kind: \"rejected\" };\n }\n\n // Trusted iframe, our envelope, but a different protocol version. Surface\n // it loudly - this is the iframe on a mismatched version, not an attacker.\n if (envelope.protocol !== EMBED_PROTOCOL_VERSION) {\n return { kind: \"protocol-mismatch\", received: envelope.protocol };\n }\n\n return { kind: \"valid\", message: envelope as EmbedMessage };\n}\n","// Typed, actionable embed diagnostics.\n//\n// A silently blank iframe is the worst embedding failure. This module is the\n// single definition of every developer-facing error the SDK can raise. Each\n// error carries a stable `code` (for programmatic handling) and a human\n// message written to be ACTIONABLE - it names the option to fix and, where\n// relevant, the deployment config.\n//\n// Two failure classes, opposite requirements (Decision 21):\n// - Untrusted cross-origin `postMessage` frames: dropped SILENTLY. They are\n// a security concern; never surfaced. The SDK does this in the inbound\n// classifier and never constructs an EmbedError for them.\n// - Developer misconfiguration: surfaced LOUDLY via `onError` AND\n// `console.error`. Every such case is one of the codes below.\n\n/**\n * Stable, machine-readable embed error codes. A host may branch on\n * `error.code`; the strings are part of the public contract.\n */\nexport const EMBED_ERROR_CODES = {\n /**\n * A required `createDeclarionEmbed` option is missing or malformed\n * (`container`, `declarionOrigin`, `route`, `getToken`). Raised\n * synchronously before the iframe is created.\n */\n invalidOptions: \"invalid-options\",\n /**\n * The host's `getToken` callback rejected, threw, or resolved to a value\n * that is not `{ token: string, expires_at: string }`.\n */\n getTokenFailed: \"get-token-failed\",\n /**\n * No `ready` frame arrived from the iframe within the post-mount timeout.\n * The usual causes are a `declarionOrigin` mismatch (the iframe loaded a\n * different origin, or never loaded) or framing denied by the Declarion CSP\n * (the host origin is not in `DECLARION_FRAME_ANCESTORS`). A slow `getToken`\n * does NOT cause this - the timer is cleared as soon as `ready` arrives.\n */\n handshakeTimeout: \"handshake-timeout\",\n /**\n * The iframe asked the host to reload it (`reload-required`) - asset drift\n * or a terminal auth failure inside the iframe.\n */\n reloadRequired: \"reload-required\",\n} as const;\n\n/** The union of all embed error code strings. */\nexport type EmbedErrorCode =\n (typeof EMBED_ERROR_CODES)[keyof typeof EMBED_ERROR_CODES];\n\n/**\n * A typed embed error. Always passed to the host `onError` callback and\n * always also written to `console.error` with the `[declarion-embed]`\n * prefix, so a misconfiguration is never silent.\n *\n * `cause` carries the originating error when one exists (e.g. the rejection\n * value from `getToken`), preserving the stack for debugging.\n */\nexport class EmbedError extends Error {\n /** The stable, machine-readable error code. */\n readonly code: EmbedErrorCode;\n\n constructor(code: EmbedErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"EmbedError\";\n this.code = code;\n // Restore the prototype chain: `extends Error` across the ES5 transpile\n // target otherwise breaks `instanceof EmbedError`.\n Object.setPrototypeOf(this, EmbedError.prototype);\n }\n}\n","// Embed iframe `src` construction.\n//\n// The host SDK builds the iframe URL the Declarion deployment parses. The\n// param grammar MUST match the iframe-side parser EXACTLY:\n// typescript/packages/react/src/embed/params.ts\n// The iframe reads `embed=1`, `parent_origin`, `theme`, `nav`; any drift\n// here silently changes how the iframe boots.\n\nimport type { EmbedNavigationContract, EmbedTheme } from \"./types\";\n\n/** Query-param names that make up the embed URL contract. */\nexport const EMBED_PARAM_EMBED = \"embed\";\nexport const EMBED_PARAM_PARENT_ORIGIN = \"parent_origin\";\nexport const EMBED_PARAM_THEME = \"theme\";\nexport const EMBED_PARAM_NAV = \"nav\";\n\n/** The on-wire value of `embed` that enables shellless render. */\nconst EMBED_PARAM_EMBED_ENABLED = \"1\";\n\n/** Default navigation contract when the host does not set `navigation`. */\nexport const DEFAULT_NAVIGATION_CONTRACT: EmbedNavigationContract = \"self\";\n\n/** Inputs needed to build the iframe `src`. */\nexport interface BuildEmbedSrcInput {\n /** The Declarion deployment origin (`https://app.example.com`). */\n readonly declarionOrigin: string;\n /** The Declarion screen route to embed. */\n readonly route: string;\n /**\n * The host page's own origin. Becomes `parent_origin` so the iframe knows\n * exactly which origin may exchange `postMessage` frames with it.\n */\n readonly parentOrigin: string;\n /** Navigation contract; becomes the `nav` param. */\n readonly navigation: EmbedNavigationContract;\n /** Optional initial theme; becomes the `theme` param when set. */\n readonly theme?: EmbedTheme;\n}\n\n/**\n * Build the absolute iframe `src` URL for an embedded Declarion screen.\n *\n * The `route` is resolved as a path against `declarionOrigin`; the four\n * embed params are appended. `parent_origin` is the host's own origin so the\n * iframe restricts its `postMessage` traffic to exactly that origin.\n *\n * Throws when `declarionOrigin` is not a parseable absolute origin - callers\n * convert that into a typed `EmbedError` before raising it to the host.\n */\nexport function buildEmbedSrc(input: BuildEmbedSrcInput): string {\n // `route` may be a bare path (`/cases`) or already carry a query/hash.\n // Resolving it against the origin keeps any route-level query intact while\n // the embed params are layered on top.\n const url = new URL(input.route, input.declarionOrigin);\n url.searchParams.set(EMBED_PARAM_EMBED, EMBED_PARAM_EMBED_ENABLED);\n url.searchParams.set(EMBED_PARAM_PARENT_ORIGIN, input.parentOrigin);\n url.searchParams.set(EMBED_PARAM_NAV, input.navigation);\n if (input.theme) {\n url.searchParams.set(EMBED_PARAM_THEME, input.theme);\n }\n return url.toString();\n}\n\n/**\n * Resolve a Declarion screen route to an absolute URL for a runtime\n * `navigate` frame. The host passes a route string; the iframe consumes the\n * route as-is, so this only normalizes it against the deployment origin for\n * the host's own bookkeeping. Returns the input unchanged when it cannot be\n * resolved (the iframe tolerates a relative route).\n */\nexport function resolveRoute(declarionOrigin: string, route: string): string {\n try {\n const url = new URL(route, declarionOrigin);\n // Keep the hash so a host can deep-link to an in-page anchor; dropping it\n // would silently break `handle.navigate(\"/cases/42#notes\")`.\n return url.pathname + url.search + url.hash;\n } catch {\n return route;\n }\n}\n","// `createDeclarionEmbed` - the framework-agnostic, dependency-free embed core.\n//\n// Builds the iframe, runs the `ready` -> `auth` handshake, owns token refresh\n// through the host `getToken` callback, auto-applies `resize`, mirrors\n// navigation, and surfaces misconfiguration loudly. The React binding\n// (`./react`) and the demo host both build on this single core.\n\nimport {\n EMBED_MESSAGE_TYPES,\n EMBED_PROTOCOL_VERSION,\n type EmbedSetTokenPayload,\n type EmbedMessage,\n type EmbedMessagePayloadMap,\n type EmbedMessageType,\n type EmbedDirtyChangedPayload,\n type EmbedNavigationPayload,\n type EmbedReloadRequiredPayload,\n type EmbedResizedPayload,\n} from \"./protocol\";\nimport { classifyInboundMessage } from \"./inbound\";\nimport {\n EMBED_ERROR_CODES,\n EmbedError,\n type EmbedErrorCode,\n} from \"./errors\";\nimport {\n DEFAULT_NAVIGATION_CONTRACT,\n buildEmbedSrc,\n resolveRoute,\n} from \"./url\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n EmbedNavigateEvent,\n EmbedToken,\n} from \"./types\";\n\n/**\n * How long the SDK waits for the first `auth` frame to be requested by the\n * iframe after it loads. The iframe emits `ready`; if no `ready` arrives -\n * usually a `declarionOrigin` mismatch or framing denied by the Declarion\n * CSP - the SDK raises a `handshake-timeout` error rather than leave a\n * silently blank iframe.\n */\nconst HANDSHAKE_TIMEOUT_MS = 20_000;\n\n/** Default iframe `title` when the host does not supply one. */\nconst DEFAULT_IFRAME_TITLE = \"Declarion embedded screen\";\n\n/** Console prefix for every SDK diagnostic line. */\nconst LOG_PREFIX = \"[declarion-embed]\";\n\n/**\n * Validate `createDeclarionEmbed` options. Returns a typed `EmbedError` for\n * the first problem found, or `null` when the options are well-formed.\n *\n * Separated from `createDeclarionEmbed` so the React binding can reuse the\n * exact same validation.\n */\nfunction validateOptions(options: DeclarionEmbedOptions): EmbedError | null {\n const fail = (message: string): EmbedError =>\n new EmbedError(EMBED_ERROR_CODES.invalidOptions, message);\n\n if (!options.container || typeof options.container.appendChild !== \"function\") {\n return fail(\n \"`container` must be a DOM element that can receive the iframe.\",\n );\n }\n if (typeof options.declarionOrigin !== \"string\" || options.declarionOrigin === \"\") {\n return fail(\n \"`declarionOrigin` is required and must be the exact origin of the \" +\n \"Declarion deployment, e.g. \\\"https://app.example.com\\\".\",\n );\n }\n let parsedOrigin: URL;\n try {\n parsedOrigin = new URL(options.declarionOrigin);\n } catch {\n return fail(\n `\\`declarionOrigin\\` is not a valid URL: \"${options.declarionOrigin}\". ` +\n 'Pass an exact origin, e.g. \"https://app.example.com\".',\n );\n }\n if (parsedOrigin.origin !== options.declarionOrigin) {\n return fail(\n `\\`declarionOrigin\\` must be exactly an origin with no path, query, ` +\n `or trailing slash. Got \"${options.declarionOrigin}\"; expected ` +\n `\"${parsedOrigin.origin}\".`,\n );\n }\n if (typeof options.route !== \"string\" || options.route === \"\") {\n return fail(\"`route` is required and must be a Declarion screen route.\");\n }\n if (typeof options.getToken !== \"function\") {\n return fail(\n \"`getToken` is required and must be an async function returning \" +\n \"{ token, expires_at }.\",\n );\n }\n return null;\n}\n\n/**\n * Validate the value a host `getToken` callback resolved to. The host owns\n * this code; a malformed return is a developer mistake, surfaced loudly.\n */\nfunction isValidToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Create an embedded Declarion screen inside `options.container`.\n *\n * Returns a `DeclarionEmbedHandle` exposing `navigate`, `setTheme`, and\n * `destroy`. On a misconfiguration the SDK still returns a handle (so\n * `destroy` is always callable) but reports the problem through `onError`\n * and `console.error`; the iframe is not created in that case.\n */\nexport function createDeclarionEmbed(\n options: DeclarionEmbedOptions,\n): DeclarionEmbedHandle {\n let destroyed = false;\n let iframe: HTMLIFrameElement | null = null;\n let messageListener: ((event: MessageEvent) => void) | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let readyReceived = false;\n let firstAuthDelivered = false;\n\n /** Emit a debug line when `debug` is on. */\n const logDebug = (message: string, detail?: unknown): void => {\n if (!options.debug) return;\n if (detail === undefined) {\n console.info(`${LOG_PREFIX} ${message}`);\n } else {\n console.info(`${LOG_PREFIX} ${message}`, detail);\n }\n };\n\n /**\n * Report a developer-facing error. Always written to `console.error` AND\n * handed to `onError`, so a misconfiguration is never silent (Decision 21).\n */\n const reportError = (\n code: EmbedErrorCode,\n message: string,\n cause?: unknown,\n ): void => {\n const error = new EmbedError(code, message, cause ? { cause } : undefined);\n console.error(`${LOG_PREFIX} ${message}`, cause ?? \"\");\n options.onError?.(error);\n };\n\n /** Post an enveloped frame to the iframe, targeting the exact origin. */\n const postToIframe = <T extends EmbedMessageType>(\n type: T,\n payload: EmbedMessagePayloadMap[T],\n ): void => {\n if (!iframe?.contentWindow) return;\n const message: EmbedMessage<T> = {\n source: \"declarion-embed\",\n protocol: EMBED_PROTOCOL_VERSION,\n type,\n payload,\n };\n logDebug(`-> iframe ${type}`, payload);\n iframe.contentWindow.postMessage(message, options.declarionOrigin);\n };\n\n /**\n * Call the host `getToken` callback and deliver the token to the iframe.\n * Surfaces a `get-token-failed` error when the callback rejects, throws,\n * or resolves to a malformed value.\n */\n const requestAndDeliverToken = async (): Promise<void> => {\n let result: unknown;\n try {\n result = await options.getToken();\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` rejected. The host backend must mint a token via \" +\n \"auth.create_embed_session and return { token, expires_at }.\",\n cause,\n );\n return;\n }\n if (!isValidToken(result)) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` resolved to a malformed value. Expected \" +\n \"{ token: string, expires_at: string }.\",\n );\n return;\n }\n if (destroyed) return;\n const payload: EmbedSetTokenPayload = {\n token: result.token,\n expires_at: result.expires_at,\n };\n postToIframe(EMBED_MESSAGE_TYPES.setToken, payload);\n onAuthDelivered();\n };\n\n /** Handle the iframe `ready` frame: the iframe requests its first token. */\n const onReady = (): void => {\n readyReceived = true;\n clearHandshakeTimer();\n logDebug(\"<- iframe ready\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.ready });\n void requestAndDeliverToken();\n };\n\n /** Handle a `token-expired` frame: re-run `getToken` and deliver again. */\n const onTokenExpired = (): void => {\n logDebug(\"<- iframe token-expired\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.tokenExpired });\n void requestAndDeliverToken();\n };\n\n /** Handle a `reload-required` frame: surface it and notify the host. */\n const onReloadRequired = (payload: EmbedReloadRequiredPayload): void => {\n logDebug(\"<- iframe reload-required\", payload);\n options.onEvent?.({\n type: EMBED_MESSAGE_TYPES.reloadRequired,\n payload,\n });\n reportError(\n EMBED_ERROR_CODES.reloadRequired,\n `The embedded iframe requested a reload: ${payload.reason}. ` +\n \"Reload the iframe to recover.\",\n );\n };\n\n /** Handle a `resized` frame: auto-apply the reported height to the iframe. */\n const onResize = (payload: EmbedResizedPayload): void => {\n logDebug(\"<- iframe resized\", payload);\n if (iframe && Number.isFinite(payload.height) && payload.height > 0) {\n iframe.style.height = `${payload.height}px`;\n }\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.resized, payload });\n };\n\n /**\n * Handle a `dirty-changed` frame: surface the embedded screen's\n * unsaved-edits state. The SDK does not act on it; the host tracks it and\n * guards its own navigation (its menu, a host-initiated `navigate`) before\n * moving the iframe off a dirty screen.\n */\n const onDirtyChanged = (payload: EmbedDirtyChangedPayload): void => {\n logDebug(\"<- iframe dirty-changed\", payload);\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.dirtyChanged, payload });\n };\n\n /**\n * Handle a navigation frame. `navigated` (the iframe moved, `self` mode)\n * and `navigation-requested` (the iframe stayed, `delegated` mode) both\n * surface through `onNavigate`; `mode` records which.\n */\n const onNavigationFrame = (\n type:\n | typeof EMBED_MESSAGE_TYPES.navigated\n | typeof EMBED_MESSAGE_TYPES.navigationRequested,\n payload: EmbedNavigationPayload,\n ): void => {\n logDebug(`<- iframe ${type}`, payload);\n options.onEvent?.({ type, payload });\n const event: EmbedNavigateEvent = {\n mode: type === EMBED_MESSAGE_TYPES.navigated ? \"self\" : \"delegated\",\n route: payload.route,\n entity: payload.entity,\n recordId: payload.recordId,\n };\n options.onNavigate?.(event);\n };\n\n /**\n * Mark the embed ready once the first token is delivered to the iframe.\n * The SDK has no separate \"screen rendered\" signal; the first successful\n * `set-token` delivery after `ready` is the earliest reliable point the\n * host can treat the embedded screen as authenticated.\n */\n const onAuthDelivered = (): void => {\n if (!firstAuthDelivered) {\n firstAuthDelivered = true;\n logDebug(\"handshake complete - iframe authenticated\");\n options.onReady?.();\n }\n };\n\n /** Stop the post-load handshake timeout. */\n const clearHandshakeTimer = (): void => {\n if (handshakeTimer !== null) {\n clearTimeout(handshakeTimer);\n handshakeTimer = null;\n }\n };\n\n /**\n * The single `message` listener. Validates every frame via\n * `classifyInboundMessage`: untrusted frames are dropped silently, a\n * protocol mismatch warns loudly, a valid frame is dispatched.\n */\n const handleMessage = (event: MessageEvent): void => {\n const classified = classifyInboundMessage(event, options.declarionOrigin);\n\n if (classified.kind === \"rejected\") {\n // Untrusted or malformed frame. Dropped silently (security): a page\n // that re-framed the iframe must learn nothing from the host logs.\n return;\n }\n\n if (classified.kind === \"protocol-mismatch\") {\n // The trusted iframe on a mismatched protocol version. Warn loudly,\n // naming both versions - a silent drop here is a blank iframe.\n console.warn(\n `${LOG_PREFIX} protocol version mismatch: this SDK speaks protocol ` +\n `${EMBED_PROTOCOL_VERSION}, the iframe reported protocol ` +\n `${String(classified.received)}. Update @declarion/embed and the ` +\n \"Declarion deployment to matching versions.\",\n );\n return;\n }\n\n const { message } = classified;\n switch (message.type as EmbedMessageType) {\n case EMBED_MESSAGE_TYPES.ready:\n onReady();\n break;\n case EMBED_MESSAGE_TYPES.tokenExpired:\n onTokenExpired();\n break;\n case EMBED_MESSAGE_TYPES.reloadRequired:\n onReloadRequired(message.payload as EmbedReloadRequiredPayload);\n break;\n case EMBED_MESSAGE_TYPES.resized:\n onResize(message.payload as EmbedResizedPayload);\n break;\n case EMBED_MESSAGE_TYPES.navigated:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigated,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.navigationRequested:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigationRequested,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.dirtyChanged:\n onDirtyChanged(message.payload as EmbedDirtyChangedPayload);\n break;\n default:\n // `set-token`, `navigate`, `set-theme` are host -> iframe; the iframe\n // does not send them back. Any other type is not consumed. Ignore.\n break;\n }\n };\n\n // --- Construction ------------------------------------------------------\n\n const optionsError = validateOptions(options);\n if (optionsError) {\n console.error(`${LOG_PREFIX} ${optionsError.message}`);\n options.onError?.(optionsError);\n // Return an inert handle: the iframe was never created, so navigate /\n // setTheme are no-ops and destroy has nothing to tear down.\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n const navigation = options.navigation ?? DEFAULT_NAVIGATION_CONTRACT;\n // The host's own origin. The iframe restricts every `postMessage` it sends\n // to exactly this origin.\n const parentOrigin = window.location.origin;\n\n let src: string;\n try {\n src = buildEmbedSrc({\n declarionOrigin: options.declarionOrigin,\n route: options.route,\n parentOrigin,\n navigation,\n theme: options.theme,\n });\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.invalidOptions,\n `Could not build the iframe URL from route \"${options.route}\". ` +\n \"`route` must be a valid Declarion screen route.\",\n cause,\n );\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n iframe = document.createElement(\"iframe\");\n iframe.src = src;\n iframe.title = options.title ?? DEFAULT_IFRAME_TITLE;\n // The screen sizes itself; the host receives `resize` frames and the SDK\n // applies the height. A sensible non-zero starting height avoids a\n // zero-height flash before the first `resize` arrives.\n iframe.style.width = \"100%\";\n iframe.style.border = \"0\";\n iframe.style.display = \"block\";\n if (!iframe.style.height) {\n iframe.style.height = \"150px\";\n }\n\n messageListener = handleMessage;\n window.addEventListener(\"message\", messageListener);\n options.container.appendChild(iframe);\n logDebug(\"iframe created\", { src });\n\n // Bound the wait for the iframe's `ready` frame. No `ready` means the\n // iframe never loaded the embed runtime: a `declarionOrigin` mismatch,\n // framing denied by the Declarion CSP, or an unreachable route.\n handshakeTimer = setTimeout(() => {\n handshakeTimer = null;\n if (readyReceived) return;\n reportError(\n EMBED_ERROR_CODES.handshakeTimeout,\n `No handshake from the iframe within ${HANDSHAKE_TIMEOUT_MS}ms. ` +\n \"Check that `declarionOrigin` exactly matches the Declarion \" +\n \"deployment origin, that the host origin is allow-listed in the \" +\n \"deployment's DECLARION_FRAME_ANCESTORS, and that `route` resolves \" +\n \"to a real screen.\",\n );\n }, HANDSHAKE_TIMEOUT_MS);\n\n return {\n navigate(route: string): void {\n if (destroyed) return;\n // Drive the iframe to a screen route (deep-linking, both nav modes).\n postToIframe(EMBED_MESSAGE_TYPES.navigate, {\n route: resolveRoute(options.declarionOrigin, route),\n });\n },\n setTheme(theme): void {\n if (destroyed) return;\n postToIframe(EMBED_MESSAGE_TYPES.setTheme, { theme });\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearHandshakeTimer();\n if (messageListener) {\n window.removeEventListener(\"message\", messageListener);\n messageListener = null;\n }\n if (iframe?.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n iframe = null;\n logDebug(\"embed destroyed\");\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var DeclarionEmbed=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`declarion-embed`,n=1,r={ready:`ready`,setToken:`set-token`,tokenExpired:`token-expired`,reloadRequired:`reload-required`,resized:`resized`,navigated:`navigated`,navigationRequested:`navigation-requested`,dirtyChanged:`dirty-changed`,navigate:`navigate`,setTheme:`set-theme`};function i(e,t){if(e.origin!==t)return{kind:`rejected`};let n=e.data;if(typeof n!=`object`||!n)return{kind:`rejected`};let r=n;return r.source!==`declarion-embed`||typeof r.protocol!=`number`||typeof r.type!=`string`?{kind:`rejected`}:r.protocol===1?{kind:`valid`,message:r}:{kind:`protocol-mismatch`,received:r.protocol}}var a={invalidOptions:`invalid-options`,getTokenFailed:`get-token-failed`,handshakeTimeout:`handshake-timeout`,reloadRequired:`reload-required`},o=class e extends Error{code;constructor(t,n,r){super(n,r),this.name=`EmbedError`,this.code=t,Object.setPrototypeOf(this,e.prototype)}},s=`embed`,c=`parent_origin`,l=`theme`,u=`1`;function d(e){let t=new URL(e.route,e.declarionOrigin);return t.searchParams.set(s,u),t.searchParams.set(c,e.parentOrigin),t.searchParams.set(`nav`,e.navigation),e.theme&&t.searchParams.set(l,e.theme),t.toString()}function f(e,t){try{let n=new URL(t,e);return n.pathname+n.search+n.hash}catch{return t}}var p=2e4,m=`Declarion embedded screen`,h=`[declarion-embed]`;function g(e){let t=e=>new o(a.invalidOptions,e);if(!e.container||typeof e.container.appendChild!=`function`)return t("`container` must be a DOM element that can receive the iframe.");if(typeof e.declarionOrigin!=`string`||e.declarionOrigin===``)return t('`declarionOrigin` is required and must be the exact origin of the Declarion deployment, e.g. "https://app.example.com".');let n;try{n=new URL(e.declarionOrigin)}catch{return t(`\`declarionOrigin\` is not a valid URL: "${e.declarionOrigin}". Pass an exact origin, e.g. "https://app.example.com".`)}return n.origin===e.declarionOrigin?typeof e.route!=`string`||e.route===``?t("`route` is required and must be a Declarion screen route."):typeof e.getToken==`function`?null:t("`getToken` is required and must be an async function returning { token, expires_at }."):t(`\`declarionOrigin\` must be exactly an origin with no path, query, or trailing slash. Got "${e.declarionOrigin}"; expected "${n.origin}".`)}function _(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.token==`string`&&t.token!==``&&typeof t.expires_at==`string`&&t.expires_at!==``}function v(e){let t=!1,n=null,s=null,c=null,l=!1,u=!1,v=(t,n)=>{e.debug&&(n===void 0?console.info(`${h} ${t}`):console.info(`${h} ${t}`,n))},y=(t,n,r)=>{let i=new o(t,n,r?{cause:r}:void 0);console.error(`${h} ${n}`,r??``),e.onError?.(i)},b=(t,r)=>{if(!n?.contentWindow)return;let i={source:`declarion-embed`,protocol:1,type:t,payload:r};v(`-> iframe ${t}`,r),n.contentWindow.postMessage(i,e.declarionOrigin)},x=async()=>{let n;try{n=await e.getToken()}catch(e){y(a.getTokenFailed,"`getToken` rejected. The host backend must mint a token via auth.create_embed_session and return { token, expires_at }.",e);return}if(!_(n)){y(a.getTokenFailed,"`getToken` resolved to a malformed value. Expected { token: string, expires_at: string }.");return}if(t)return;let i={token:n.token,expires_at:n.expires_at};b(r.setToken,i),O()},S=()=>{l=!0,k(),v(`<- iframe ready`),e.onEvent?.({type:r.ready}),x()},C=()=>{v(`<- iframe token-expired`),e.onEvent?.({type:r.tokenExpired}),x()},w=t=>{v(`<- iframe reload-required`,t),e.onEvent?.({type:r.reloadRequired,payload:t}),y(a.reloadRequired,`The embedded iframe requested a reload: ${t.reason}. Reload the iframe to recover.`)},T=t=>{v(`<- iframe resized`,t),n&&Number.isFinite(t.height)&&t.height>0&&(n.style.height=`${t.height}px`),e.onEvent?.({type:r.resized,payload:t})},E=t=>{v(`<- iframe dirty-changed`,t),e.onEvent?.({type:r.dirtyChanged,payload:t})},D=(t,n)=>{v(`<- iframe ${t}`,n),e.onEvent?.({type:t,payload:n});let i={mode:t===r.navigated?`self`:`delegated`,route:n.route,entity:n.entity,recordId:n.recordId};e.onNavigate?.(i)},O=()=>{u||(u=!0,v(`handshake complete - iframe authenticated`),e.onReady?.())},k=()=>{c!==null&&(clearTimeout(c),c=null)},A=t=>{let n=i(t,e.declarionOrigin);if(n.kind===`rejected`)return;if(n.kind===`protocol-mismatch`){console.warn(`${h} protocol version mismatch: this SDK speaks protocol 1, the iframe reported protocol ${String(n.received)}. Update @declarion/embed and the Declarion deployment to matching versions.`);return}let{message:a}=n;switch(a.type){case r.ready:S();break;case r.tokenExpired:C();break;case r.reloadRequired:w(a.payload);break;case r.resized:T(a.payload);break;case r.navigated:D(r.navigated,a.payload);break;case r.navigationRequested:D(r.navigationRequested,a.payload);break;case r.dirtyChanged:E(a.payload);break;default:break}},j=g(e);if(j)return console.error(`${h} ${j.message}`),e.onError?.(j),{navigate:()=>void 0,setTheme:()=>void 0,destroy:()=>void 0};let M=e.navigation??`self`,N=window.location.origin,P;try{P=d({declarionOrigin:e.declarionOrigin,route:e.route,parentOrigin:N,navigation:M,theme:e.theme})}catch(t){return y(a.invalidOptions,`Could not build the iframe URL from route "${e.route}". \`route\` must be a valid Declarion screen route.`,t),{navigate:()=>void 0,setTheme:()=>void 0,destroy:()=>void 0}}return n=document.createElement(`iframe`),n.src=P,n.title=e.title??m,n.style.width=`100%`,n.style.border=`0`,n.style.display=`block`,n.style.height||(n.style.height=`150px`),s=A,window.addEventListener(`message`,s),e.container.appendChild(n),v(`iframe created`,{src:P}),c=setTimeout(()=>{c=null,!l&&y(a.handshakeTimeout,`No handshake from the iframe within ${p}ms. Check that \`declarionOrigin\` exactly matches the Declarion deployment origin, that the host origin is allow-listed in the deployment's DECLARION_FRAME_ANCESTORS, and that \`route\` resolves to a real screen.`)},p),{navigate(n){t||b(r.navigate,{route:f(e.declarionOrigin,n)})},setTheme(e){t||b(r.setTheme,{theme:e})},destroy(){t||(t=!0,k(),s&&=(window.removeEventListener(`message`,s),null),n?.parentNode&&n.parentNode.removeChild(n),n=null,v(`embed destroyed`))}}}return e.EMBED_ERROR_CODES=a,e.EMBED_MESSAGE_SOURCE=t,e.EMBED_MESSAGE_TYPES=r,e.EMBED_PROTOCOL_VERSION=n,e.EmbedError=o,e.createDeclarionEmbed=v,e})({});
|
|
2
|
+
//# sourceMappingURL=declarion-embed.iife.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declarion-embed.iife.js","names":[],"sources":["../src/protocol.ts","../src/inbound.ts","../src/errors.ts","../src/url.ts","../src/core.ts"],"sourcesContent":["// Embed postMessage protocol - host-side contract.\n//\n// `@declarion/embed` is a SEPARATE npm package from `@declarion/react` and\n// MUST NOT depend on it (a host app must not pull the full Declarion UI SDK\n// to host an iframe). The protocol contract is therefore re-declared here,\n// independently, and MUST match the iframe side EXACTLY:\n// typescript/packages/react/src/embed/protocol.ts\n// A divergence breaks the handshake. Any change to the wire envelope, the\n// message-type set, or a payload shape MUST land in both files together.\n//\n// Wire envelope: { source: \"declarion-embed\", protocol: 1, type, payload }.\n//\n// Naming contract (developer-facing - keep it consistent):\n// - A message the iframe SENDS to the host is an EVENT, named in the past\n// tense - \"this happened\": `resized`, `navigated`, `navigation-requested`,\n// `dirty-changed`, `token-expired`, `reload-required`, `ready`.\n// - A message the host SENDS to the iframe is a COMMAND, named as an\n// imperative - \"do this\": `set-token`, `navigate`, `set-theme`.\n//\n// Security model: the host SDK validates EVERY inbound frame on two axes -\n// exact origin equality against the configured `declarionOrigin`, and the\n// envelope shape (`source` discriminator + numeric `protocol`). A frame that\n// fails either check is dropped SILENTLY; an untrusted page that re-framed\n// the iframe must learn nothing. Outbound host frames target the EXACT\n// `declarionOrigin`, never `\"*\"`.\n\n/**\n * The `source` discriminator stamped on every embed frame. Both the host SDK\n * and the iframe filter inbound traffic on this value so unrelated\n * `postMessage` frames (browser extensions, other widgets) are ignored.\n */\nexport const EMBED_MESSAGE_SOURCE = \"declarion-embed\" as const;\n\n/**\n * The protocol version this SDK speaks. Bumped only on a breaking\n * envelope/payload change. The SDK warns when the iframe reports a different\n * version (see the diagnostics path).\n */\nexport const EMBED_PROTOCOL_VERSION = 1 as const;\n\n/**\n * Every protocol message `type`. The object key is the camelCase name used in\n * code; the value is the on-wire string. Events (iframe -> host) are past\n * tense; commands (host -> iframe) are imperative. This SDK is the HOST side:\n * it RECEIVES the iframe-to-host events and SENDS the host-to-iframe commands.\n */\nexport const EMBED_MESSAGE_TYPES = {\n /** iframe -> host: SDK mounted, requests the first token. */\n ready: \"ready\",\n /** host -> iframe: deliver or refresh the embed token. */\n setToken: \"set-token\",\n /** iframe -> host: token rejected or near expiry. */\n tokenExpired: \"token-expired\",\n /** iframe -> host: the iframe must be reloaded. */\n reloadRequired: \"reload-required\",\n /** iframe -> host: embed content height changed. */\n resized: \"resized\",\n /** iframe -> host: `self` mode internal navigation happened. */\n navigated: \"navigated\",\n /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */\n navigationRequested: \"navigation-requested\",\n /** iframe -> host: the embedded screen's unsaved-edits state flipped. */\n dirtyChanged: \"dirty-changed\",\n /** host -> iframe: drive the iframe to a screen (deep-linking). */\n navigate: \"navigate\",\n /** host -> iframe: runtime theme switch. */\n setTheme: \"set-theme\",\n} as const;\n\n/** The union of all on-wire message `type` strings. */\nexport type EmbedMessageType =\n (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];\n\n// --- Payload interfaces, one per message type ---\n\n/** `ready` payload: empty - the frame itself is the signal. */\nexport type EmbedReadyPayload = Record<string, never>;\n\n/** `set-token` payload: the embed token and its absolute expiry. */\nexport interface EmbedSetTokenPayload {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** RFC 3339 expiry timestamp of the token. */\n readonly expires_at: string;\n}\n\n/** `token-expired` payload: empty - the host re-mints via `getToken`. */\nexport type EmbedTokenExpiredPayload = Record<string, never>;\n\n/** `reload-required` payload: a human-readable reason for the reload. */\nexport interface EmbedReloadRequiredPayload {\n /** Why the iframe must be reloaded (asset drift, terminal auth failure). */\n readonly reason: string;\n}\n\n/** `resized` payload: the measured content height in CSS pixels. */\nexport interface EmbedResizedPayload {\n /** Content height in CSS pixels. */\n readonly height: number;\n}\n\n/**\n * `navigated` / `navigation-requested` payload: the screen route and, when\n * resolvable, the bound entity code and record id.\n *\n * `entity` and `recordId` are optional: a `custom` screen has no entity, and\n * a list route has no record id.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /** The bound entity code, when the route resolves to one. */\n readonly entity?: string;\n /** The record id, when the route is a detail route with an id. */\n readonly recordId?: string;\n}\n\n/**\n * `dirty-changed` payload: whether the embedded screen currently has unsaved\n * edits. The host tracks this so it can guard its own navigation (its menu,\n * a host-initiated `navigate`) before moving the iframe away from a dirty\n * screen - the iframe never pops a dialog for host-driven navigation.\n */\nexport interface EmbedDirtyChangedPayload {\n /** True when the embedded screen has unsaved edits. */\n readonly dirty: boolean;\n}\n\n/** `navigate` payload: the route the host drives the iframe to. */\nexport interface EmbedNavigatePayload {\n /** The Declarion screen route the host wants opened. */\n readonly route: string;\n}\n\n/** `set-theme` payload: the theme the host switches the iframe to. */\nexport interface EmbedSetThemePayload {\n /** The requested theme. */\n readonly theme: EmbedTheme;\n}\n\n/** The theme hint carried on the `theme` URL param and `set-theme` frame. */\nexport type EmbedTheme = \"light\" | \"dark\";\n\n/**\n * Maps each message type to its payload shape. Used to type the inbound\n * classifier and the outbound sender so the payload is checked against the\n * message type.\n */\nexport interface EmbedMessagePayloadMap {\n [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;\n [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;\n [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;\n [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;\n [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;\n [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;\n [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;\n [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;\n}\n\n/**\n * The wire envelope. Every embed frame is exactly this shape: a fixed\n * `source` + `protocol` pair, a discriminating `type`, and the typed\n * `payload` for that type.\n */\nexport interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {\n readonly source: typeof EMBED_MESSAGE_SOURCE;\n readonly protocol: typeof EMBED_PROTOCOL_VERSION;\n readonly type: T;\n readonly payload: EmbedMessagePayloadMap[T];\n}\n","// Inbound `postMessage` validation - host side.\n//\n// The host SDK receives the iframe-to-parent frames. EVERY inbound frame is\n// validated on two axes before it is acted on:\n// 1. `event.origin === declarionOrigin` - exact string equality, no\n// prefix, no wildcard. A frame from any other origin is dropped.\n// 2. the envelope shape - `source === \"declarion-embed\"` and a numeric\n// `protocol`.\n// A frame failing either check is dropped SILENTLY (security): an untrusted\n// page that re-framed the iframe must learn nothing.\n//\n// A frame that passes origin + source but carries a DIFFERENT `protocol`\n// version is classified `protocol-mismatch`: that frame IS the trusted\n// iframe, just on a mismatched SDK version. The caller surfaces it as a\n// clear console warning naming both versions, never a silent drop.\n//\n// This mirrors the iframe-side classifier:\n// typescript/packages/react/src/embed/handshake.ts\n\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n type EmbedMessage,\n} from \"./protocol\";\n\n/**\n * The classification of an inbound `message` event after validation.\n *\n * - `valid`: a trusted, shape-correct, version-matched embed frame.\n * - `protocol-mismatch`: origin + source are trusted, but the `protocol`\n * version differs. The caller logs a warning; the frame is not acted on.\n * - `rejected`: not a trusted embed frame (wrong origin, missing/foreign\n * `source`, malformed envelope). The caller drops it silently.\n */\nexport type InboundClassification =\n | { kind: \"valid\"; message: EmbedMessage }\n | { kind: \"protocol-mismatch\"; received: unknown }\n | { kind: \"rejected\" };\n\n/**\n * Validate and classify an inbound `message` event against the trusted\n * `declarionOrigin`.\n *\n * Returns `rejected` for anything that is not a trusted embed frame - the\n * caller drops those with no logging. Returns `protocol-mismatch` when the\n * frame is trusted but on a different protocol version. Returns `valid` with\n * the typed envelope otherwise.\n */\nexport function classifyInboundMessage(\n event: MessageEvent,\n declarionOrigin: string,\n): InboundClassification {\n // Axis 1: exact origin match. A trailing slash, a subdomain, a different\n // port - all fail this equality and the frame is dropped.\n if (event.origin !== declarionOrigin) {\n return { kind: \"rejected\" };\n }\n\n // Axis 2: envelope shape. The data must be an object carrying our\n // `source` discriminator and a numeric `protocol`.\n const data = event.data as unknown;\n if (typeof data !== \"object\" || data === null) {\n return { kind: \"rejected\" };\n }\n const envelope = data as Partial<EmbedMessage>;\n if (envelope.source !== EMBED_MESSAGE_SOURCE) {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.protocol !== \"number\") {\n return { kind: \"rejected\" };\n }\n if (typeof envelope.type !== \"string\") {\n return { kind: \"rejected\" };\n }\n\n // Trusted iframe, our envelope, but a different protocol version. Surface\n // it loudly - this is the iframe on a mismatched version, not an attacker.\n if (envelope.protocol !== EMBED_PROTOCOL_VERSION) {\n return { kind: \"protocol-mismatch\", received: envelope.protocol };\n }\n\n return { kind: \"valid\", message: envelope as EmbedMessage };\n}\n","// Typed, actionable embed diagnostics.\n//\n// A silently blank iframe is the worst embedding failure. This module is the\n// single definition of every developer-facing error the SDK can raise. Each\n// error carries a stable `code` (for programmatic handling) and a human\n// message written to be ACTIONABLE - it names the option to fix and, where\n// relevant, the deployment config.\n//\n// Two failure classes, opposite requirements (Decision 21):\n// - Untrusted cross-origin `postMessage` frames: dropped SILENTLY. They are\n// a security concern; never surfaced. The SDK does this in the inbound\n// classifier and never constructs an EmbedError for them.\n// - Developer misconfiguration: surfaced LOUDLY via `onError` AND\n// `console.error`. Every such case is one of the codes below.\n\n/**\n * Stable, machine-readable embed error codes. A host may branch on\n * `error.code`; the strings are part of the public contract.\n */\nexport const EMBED_ERROR_CODES = {\n /**\n * A required `createDeclarionEmbed` option is missing or malformed\n * (`container`, `declarionOrigin`, `route`, `getToken`). Raised\n * synchronously before the iframe is created.\n */\n invalidOptions: \"invalid-options\",\n /**\n * The host's `getToken` callback rejected, threw, or resolved to a value\n * that is not `{ token: string, expires_at: string }`.\n */\n getTokenFailed: \"get-token-failed\",\n /**\n * No `ready` frame arrived from the iframe within the post-mount timeout.\n * The usual causes are a `declarionOrigin` mismatch (the iframe loaded a\n * different origin, or never loaded) or framing denied by the Declarion CSP\n * (the host origin is not in `DECLARION_FRAME_ANCESTORS`). A slow `getToken`\n * does NOT cause this - the timer is cleared as soon as `ready` arrives.\n */\n handshakeTimeout: \"handshake-timeout\",\n /**\n * The iframe asked the host to reload it (`reload-required`) - asset drift\n * or a terminal auth failure inside the iframe.\n */\n reloadRequired: \"reload-required\",\n} as const;\n\n/** The union of all embed error code strings. */\nexport type EmbedErrorCode =\n (typeof EMBED_ERROR_CODES)[keyof typeof EMBED_ERROR_CODES];\n\n/**\n * A typed embed error. Always passed to the host `onError` callback and\n * always also written to `console.error` with the `[declarion-embed]`\n * prefix, so a misconfiguration is never silent.\n *\n * `cause` carries the originating error when one exists (e.g. the rejection\n * value from `getToken`), preserving the stack for debugging.\n */\nexport class EmbedError extends Error {\n /** The stable, machine-readable error code. */\n readonly code: EmbedErrorCode;\n\n constructor(code: EmbedErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"EmbedError\";\n this.code = code;\n // Restore the prototype chain: `extends Error` across the ES5 transpile\n // target otherwise breaks `instanceof EmbedError`.\n Object.setPrototypeOf(this, EmbedError.prototype);\n }\n}\n","// Embed iframe `src` construction.\n//\n// The host SDK builds the iframe URL the Declarion deployment parses. The\n// param grammar MUST match the iframe-side parser EXACTLY:\n// typescript/packages/react/src/embed/params.ts\n// The iframe reads `embed=1`, `parent_origin`, `theme`, `nav`; any drift\n// here silently changes how the iframe boots.\n\nimport type { EmbedNavigationContract, EmbedTheme } from \"./types\";\n\n/** Query-param names that make up the embed URL contract. */\nexport const EMBED_PARAM_EMBED = \"embed\";\nexport const EMBED_PARAM_PARENT_ORIGIN = \"parent_origin\";\nexport const EMBED_PARAM_THEME = \"theme\";\nexport const EMBED_PARAM_NAV = \"nav\";\n\n/** The on-wire value of `embed` that enables shellless render. */\nconst EMBED_PARAM_EMBED_ENABLED = \"1\";\n\n/** Default navigation contract when the host does not set `navigation`. */\nexport const DEFAULT_NAVIGATION_CONTRACT: EmbedNavigationContract = \"self\";\n\n/** Inputs needed to build the iframe `src`. */\nexport interface BuildEmbedSrcInput {\n /** The Declarion deployment origin (`https://app.example.com`). */\n readonly declarionOrigin: string;\n /** The Declarion screen route to embed. */\n readonly route: string;\n /**\n * The host page's own origin. Becomes `parent_origin` so the iframe knows\n * exactly which origin may exchange `postMessage` frames with it.\n */\n readonly parentOrigin: string;\n /** Navigation contract; becomes the `nav` param. */\n readonly navigation: EmbedNavigationContract;\n /** Optional initial theme; becomes the `theme` param when set. */\n readonly theme?: EmbedTheme;\n}\n\n/**\n * Build the absolute iframe `src` URL for an embedded Declarion screen.\n *\n * The `route` is resolved as a path against `declarionOrigin`; the four\n * embed params are appended. `parent_origin` is the host's own origin so the\n * iframe restricts its `postMessage` traffic to exactly that origin.\n *\n * Throws when `declarionOrigin` is not a parseable absolute origin - callers\n * convert that into a typed `EmbedError` before raising it to the host.\n */\nexport function buildEmbedSrc(input: BuildEmbedSrcInput): string {\n // `route` may be a bare path (`/cases`) or already carry a query/hash.\n // Resolving it against the origin keeps any route-level query intact while\n // the embed params are layered on top.\n const url = new URL(input.route, input.declarionOrigin);\n url.searchParams.set(EMBED_PARAM_EMBED, EMBED_PARAM_EMBED_ENABLED);\n url.searchParams.set(EMBED_PARAM_PARENT_ORIGIN, input.parentOrigin);\n url.searchParams.set(EMBED_PARAM_NAV, input.navigation);\n if (input.theme) {\n url.searchParams.set(EMBED_PARAM_THEME, input.theme);\n }\n return url.toString();\n}\n\n/**\n * Resolve a Declarion screen route to an absolute URL for a runtime\n * `navigate` frame. The host passes a route string; the iframe consumes the\n * route as-is, so this only normalizes it against the deployment origin for\n * the host's own bookkeeping. Returns the input unchanged when it cannot be\n * resolved (the iframe tolerates a relative route).\n */\nexport function resolveRoute(declarionOrigin: string, route: string): string {\n try {\n const url = new URL(route, declarionOrigin);\n // Keep the hash so a host can deep-link to an in-page anchor; dropping it\n // would silently break `handle.navigate(\"/cases/42#notes\")`.\n return url.pathname + url.search + url.hash;\n } catch {\n return route;\n }\n}\n","// `createDeclarionEmbed` - the framework-agnostic, dependency-free embed core.\n//\n// Builds the iframe, runs the `ready` -> `auth` handshake, owns token refresh\n// through the host `getToken` callback, auto-applies `resize`, mirrors\n// navigation, and surfaces misconfiguration loudly. The React binding\n// (`./react`) and the demo host both build on this single core.\n\nimport {\n EMBED_MESSAGE_TYPES,\n EMBED_PROTOCOL_VERSION,\n type EmbedSetTokenPayload,\n type EmbedMessage,\n type EmbedMessagePayloadMap,\n type EmbedMessageType,\n type EmbedDirtyChangedPayload,\n type EmbedNavigationPayload,\n type EmbedReloadRequiredPayload,\n type EmbedResizedPayload,\n} from \"./protocol\";\nimport { classifyInboundMessage } from \"./inbound\";\nimport {\n EMBED_ERROR_CODES,\n EmbedError,\n type EmbedErrorCode,\n} from \"./errors\";\nimport {\n DEFAULT_NAVIGATION_CONTRACT,\n buildEmbedSrc,\n resolveRoute,\n} from \"./url\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n EmbedNavigateEvent,\n EmbedToken,\n} from \"./types\";\n\n/**\n * How long the SDK waits for the first `auth` frame to be requested by the\n * iframe after it loads. The iframe emits `ready`; if no `ready` arrives -\n * usually a `declarionOrigin` mismatch or framing denied by the Declarion\n * CSP - the SDK raises a `handshake-timeout` error rather than leave a\n * silently blank iframe.\n */\nconst HANDSHAKE_TIMEOUT_MS = 20_000;\n\n/** Default iframe `title` when the host does not supply one. */\nconst DEFAULT_IFRAME_TITLE = \"Declarion embedded screen\";\n\n/** Console prefix for every SDK diagnostic line. */\nconst LOG_PREFIX = \"[declarion-embed]\";\n\n/**\n * Validate `createDeclarionEmbed` options. Returns a typed `EmbedError` for\n * the first problem found, or `null` when the options are well-formed.\n *\n * Separated from `createDeclarionEmbed` so the React binding can reuse the\n * exact same validation.\n */\nfunction validateOptions(options: DeclarionEmbedOptions): EmbedError | null {\n const fail = (message: string): EmbedError =>\n new EmbedError(EMBED_ERROR_CODES.invalidOptions, message);\n\n if (!options.container || typeof options.container.appendChild !== \"function\") {\n return fail(\n \"`container` must be a DOM element that can receive the iframe.\",\n );\n }\n if (typeof options.declarionOrigin !== \"string\" || options.declarionOrigin === \"\") {\n return fail(\n \"`declarionOrigin` is required and must be the exact origin of the \" +\n \"Declarion deployment, e.g. \\\"https://app.example.com\\\".\",\n );\n }\n let parsedOrigin: URL;\n try {\n parsedOrigin = new URL(options.declarionOrigin);\n } catch {\n return fail(\n `\\`declarionOrigin\\` is not a valid URL: \"${options.declarionOrigin}\". ` +\n 'Pass an exact origin, e.g. \"https://app.example.com\".',\n );\n }\n if (parsedOrigin.origin !== options.declarionOrigin) {\n return fail(\n `\\`declarionOrigin\\` must be exactly an origin with no path, query, ` +\n `or trailing slash. Got \"${options.declarionOrigin}\"; expected ` +\n `\"${parsedOrigin.origin}\".`,\n );\n }\n if (typeof options.route !== \"string\" || options.route === \"\") {\n return fail(\"`route` is required and must be a Declarion screen route.\");\n }\n if (typeof options.getToken !== \"function\") {\n return fail(\n \"`getToken` is required and must be an async function returning \" +\n \"{ token, expires_at }.\",\n );\n }\n return null;\n}\n\n/**\n * Validate the value a host `getToken` callback resolved to. The host owns\n * this code; a malformed return is a developer mistake, surfaced loudly.\n */\nfunction isValidToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Create an embedded Declarion screen inside `options.container`.\n *\n * Returns a `DeclarionEmbedHandle` exposing `navigate`, `setTheme`, and\n * `destroy`. On a misconfiguration the SDK still returns a handle (so\n * `destroy` is always callable) but reports the problem through `onError`\n * and `console.error`; the iframe is not created in that case.\n */\nexport function createDeclarionEmbed(\n options: DeclarionEmbedOptions,\n): DeclarionEmbedHandle {\n let destroyed = false;\n let iframe: HTMLIFrameElement | null = null;\n let messageListener: ((event: MessageEvent) => void) | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let readyReceived = false;\n let firstAuthDelivered = false;\n\n /** Emit a debug line when `debug` is on. */\n const logDebug = (message: string, detail?: unknown): void => {\n if (!options.debug) return;\n if (detail === undefined) {\n console.info(`${LOG_PREFIX} ${message}`);\n } else {\n console.info(`${LOG_PREFIX} ${message}`, detail);\n }\n };\n\n /**\n * Report a developer-facing error. Always written to `console.error` AND\n * handed to `onError`, so a misconfiguration is never silent (Decision 21).\n */\n const reportError = (\n code: EmbedErrorCode,\n message: string,\n cause?: unknown,\n ): void => {\n const error = new EmbedError(code, message, cause ? { cause } : undefined);\n console.error(`${LOG_PREFIX} ${message}`, cause ?? \"\");\n options.onError?.(error);\n };\n\n /** Post an enveloped frame to the iframe, targeting the exact origin. */\n const postToIframe = <T extends EmbedMessageType>(\n type: T,\n payload: EmbedMessagePayloadMap[T],\n ): void => {\n if (!iframe?.contentWindow) return;\n const message: EmbedMessage<T> = {\n source: \"declarion-embed\",\n protocol: EMBED_PROTOCOL_VERSION,\n type,\n payload,\n };\n logDebug(`-> iframe ${type}`, payload);\n iframe.contentWindow.postMessage(message, options.declarionOrigin);\n };\n\n /**\n * Call the host `getToken` callback and deliver the token to the iframe.\n * Surfaces a `get-token-failed` error when the callback rejects, throws,\n * or resolves to a malformed value.\n */\n const requestAndDeliverToken = async (): Promise<void> => {\n let result: unknown;\n try {\n result = await options.getToken();\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` rejected. The host backend must mint a token via \" +\n \"auth.create_embed_session and return { token, expires_at }.\",\n cause,\n );\n return;\n }\n if (!isValidToken(result)) {\n reportError(\n EMBED_ERROR_CODES.getTokenFailed,\n \"`getToken` resolved to a malformed value. Expected \" +\n \"{ token: string, expires_at: string }.\",\n );\n return;\n }\n if (destroyed) return;\n const payload: EmbedSetTokenPayload = {\n token: result.token,\n expires_at: result.expires_at,\n };\n postToIframe(EMBED_MESSAGE_TYPES.setToken, payload);\n onAuthDelivered();\n };\n\n /** Handle the iframe `ready` frame: the iframe requests its first token. */\n const onReady = (): void => {\n readyReceived = true;\n clearHandshakeTimer();\n logDebug(\"<- iframe ready\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.ready });\n void requestAndDeliverToken();\n };\n\n /** Handle a `token-expired` frame: re-run `getToken` and deliver again. */\n const onTokenExpired = (): void => {\n logDebug(\"<- iframe token-expired\");\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.tokenExpired });\n void requestAndDeliverToken();\n };\n\n /** Handle a `reload-required` frame: surface it and notify the host. */\n const onReloadRequired = (payload: EmbedReloadRequiredPayload): void => {\n logDebug(\"<- iframe reload-required\", payload);\n options.onEvent?.({\n type: EMBED_MESSAGE_TYPES.reloadRequired,\n payload,\n });\n reportError(\n EMBED_ERROR_CODES.reloadRequired,\n `The embedded iframe requested a reload: ${payload.reason}. ` +\n \"Reload the iframe to recover.\",\n );\n };\n\n /** Handle a `resized` frame: auto-apply the reported height to the iframe. */\n const onResize = (payload: EmbedResizedPayload): void => {\n logDebug(\"<- iframe resized\", payload);\n if (iframe && Number.isFinite(payload.height) && payload.height > 0) {\n iframe.style.height = `${payload.height}px`;\n }\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.resized, payload });\n };\n\n /**\n * Handle a `dirty-changed` frame: surface the embedded screen's\n * unsaved-edits state. The SDK does not act on it; the host tracks it and\n * guards its own navigation (its menu, a host-initiated `navigate`) before\n * moving the iframe off a dirty screen.\n */\n const onDirtyChanged = (payload: EmbedDirtyChangedPayload): void => {\n logDebug(\"<- iframe dirty-changed\", payload);\n options.onEvent?.({ type: EMBED_MESSAGE_TYPES.dirtyChanged, payload });\n };\n\n /**\n * Handle a navigation frame. `navigated` (the iframe moved, `self` mode)\n * and `navigation-requested` (the iframe stayed, `delegated` mode) both\n * surface through `onNavigate`; `mode` records which.\n */\n const onNavigationFrame = (\n type:\n | typeof EMBED_MESSAGE_TYPES.navigated\n | typeof EMBED_MESSAGE_TYPES.navigationRequested,\n payload: EmbedNavigationPayload,\n ): void => {\n logDebug(`<- iframe ${type}`, payload);\n options.onEvent?.({ type, payload });\n const event: EmbedNavigateEvent = {\n mode: type === EMBED_MESSAGE_TYPES.navigated ? \"self\" : \"delegated\",\n route: payload.route,\n entity: payload.entity,\n recordId: payload.recordId,\n };\n options.onNavigate?.(event);\n };\n\n /**\n * Mark the embed ready once the first token is delivered to the iframe.\n * The SDK has no separate \"screen rendered\" signal; the first successful\n * `set-token` delivery after `ready` is the earliest reliable point the\n * host can treat the embedded screen as authenticated.\n */\n const onAuthDelivered = (): void => {\n if (!firstAuthDelivered) {\n firstAuthDelivered = true;\n logDebug(\"handshake complete - iframe authenticated\");\n options.onReady?.();\n }\n };\n\n /** Stop the post-load handshake timeout. */\n const clearHandshakeTimer = (): void => {\n if (handshakeTimer !== null) {\n clearTimeout(handshakeTimer);\n handshakeTimer = null;\n }\n };\n\n /**\n * The single `message` listener. Validates every frame via\n * `classifyInboundMessage`: untrusted frames are dropped silently, a\n * protocol mismatch warns loudly, a valid frame is dispatched.\n */\n const handleMessage = (event: MessageEvent): void => {\n const classified = classifyInboundMessage(event, options.declarionOrigin);\n\n if (classified.kind === \"rejected\") {\n // Untrusted or malformed frame. Dropped silently (security): a page\n // that re-framed the iframe must learn nothing from the host logs.\n return;\n }\n\n if (classified.kind === \"protocol-mismatch\") {\n // The trusted iframe on a mismatched protocol version. Warn loudly,\n // naming both versions - a silent drop here is a blank iframe.\n console.warn(\n `${LOG_PREFIX} protocol version mismatch: this SDK speaks protocol ` +\n `${EMBED_PROTOCOL_VERSION}, the iframe reported protocol ` +\n `${String(classified.received)}. Update @declarion/embed and the ` +\n \"Declarion deployment to matching versions.\",\n );\n return;\n }\n\n const { message } = classified;\n switch (message.type as EmbedMessageType) {\n case EMBED_MESSAGE_TYPES.ready:\n onReady();\n break;\n case EMBED_MESSAGE_TYPES.tokenExpired:\n onTokenExpired();\n break;\n case EMBED_MESSAGE_TYPES.reloadRequired:\n onReloadRequired(message.payload as EmbedReloadRequiredPayload);\n break;\n case EMBED_MESSAGE_TYPES.resized:\n onResize(message.payload as EmbedResizedPayload);\n break;\n case EMBED_MESSAGE_TYPES.navigated:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigated,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.navigationRequested:\n onNavigationFrame(\n EMBED_MESSAGE_TYPES.navigationRequested,\n message.payload as EmbedNavigationPayload,\n );\n break;\n case EMBED_MESSAGE_TYPES.dirtyChanged:\n onDirtyChanged(message.payload as EmbedDirtyChangedPayload);\n break;\n default:\n // `set-token`, `navigate`, `set-theme` are host -> iframe; the iframe\n // does not send them back. Any other type is not consumed. Ignore.\n break;\n }\n };\n\n // --- Construction ------------------------------------------------------\n\n const optionsError = validateOptions(options);\n if (optionsError) {\n console.error(`${LOG_PREFIX} ${optionsError.message}`);\n options.onError?.(optionsError);\n // Return an inert handle: the iframe was never created, so navigate /\n // setTheme are no-ops and destroy has nothing to tear down.\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n const navigation = options.navigation ?? DEFAULT_NAVIGATION_CONTRACT;\n // The host's own origin. The iframe restricts every `postMessage` it sends\n // to exactly this origin.\n const parentOrigin = window.location.origin;\n\n let src: string;\n try {\n src = buildEmbedSrc({\n declarionOrigin: options.declarionOrigin,\n route: options.route,\n parentOrigin,\n navigation,\n theme: options.theme,\n });\n } catch (cause) {\n reportError(\n EMBED_ERROR_CODES.invalidOptions,\n `Could not build the iframe URL from route \"${options.route}\". ` +\n \"`route` must be a valid Declarion screen route.\",\n cause,\n );\n return {\n navigate: () => undefined,\n setTheme: () => undefined,\n destroy: () => undefined,\n };\n }\n\n iframe = document.createElement(\"iframe\");\n iframe.src = src;\n iframe.title = options.title ?? DEFAULT_IFRAME_TITLE;\n // The screen sizes itself; the host receives `resize` frames and the SDK\n // applies the height. A sensible non-zero starting height avoids a\n // zero-height flash before the first `resize` arrives.\n iframe.style.width = \"100%\";\n iframe.style.border = \"0\";\n iframe.style.display = \"block\";\n if (!iframe.style.height) {\n iframe.style.height = \"150px\";\n }\n\n messageListener = handleMessage;\n window.addEventListener(\"message\", messageListener);\n options.container.appendChild(iframe);\n logDebug(\"iframe created\", { src });\n\n // Bound the wait for the iframe's `ready` frame. No `ready` means the\n // iframe never loaded the embed runtime: a `declarionOrigin` mismatch,\n // framing denied by the Declarion CSP, or an unreachable route.\n handshakeTimer = setTimeout(() => {\n handshakeTimer = null;\n if (readyReceived) return;\n reportError(\n EMBED_ERROR_CODES.handshakeTimeout,\n `No handshake from the iframe within ${HANDSHAKE_TIMEOUT_MS}ms. ` +\n \"Check that `declarionOrigin` exactly matches the Declarion \" +\n \"deployment origin, that the host origin is allow-listed in the \" +\n \"deployment's DECLARION_FRAME_ANCESTORS, and that `route` resolves \" +\n \"to a real screen.\",\n );\n }, HANDSHAKE_TIMEOUT_MS);\n\n return {\n navigate(route: string): void {\n if (destroyed) return;\n // Drive the iframe to a screen route (deep-linking, both nav modes).\n postToIframe(EMBED_MESSAGE_TYPES.navigate, {\n route: resolveRoute(options.declarionOrigin, route),\n });\n },\n setTheme(theme): void {\n if (destroyed) return;\n postToIframe(EMBED_MESSAGE_TYPES.setTheme, { theme });\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearHandshakeTimer();\n if (messageListener) {\n window.removeEventListener(\"message\", messageListener);\n messageListener = null;\n }\n if (iframe?.parentNode) {\n iframe.parentNode.removeChild(iframe);\n }\n iframe = null;\n logDebug(\"embed destroyed\");\n },\n };\n}\n"],"mappings":"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"}
|