@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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Stable, machine-readable embed error codes. A host may branch on
3
+ * `error.code`; the strings are part of the public contract.
4
+ */
5
+ export declare const EMBED_ERROR_CODES: {
6
+ /**
7
+ * A required `createDeclarionEmbed` option is missing or malformed
8
+ * (`container`, `declarionOrigin`, `route`, `getToken`). Raised
9
+ * synchronously before the iframe is created.
10
+ */
11
+ readonly invalidOptions: "invalid-options";
12
+ /**
13
+ * The host's `getToken` callback rejected, threw, or resolved to a value
14
+ * that is not `{ token: string, expires_at: string }`.
15
+ */
16
+ readonly getTokenFailed: "get-token-failed";
17
+ /**
18
+ * No `ready` frame arrived from the iframe within the post-mount timeout.
19
+ * The usual causes are a `declarionOrigin` mismatch (the iframe loaded a
20
+ * different origin, or never loaded) or framing denied by the Declarion CSP
21
+ * (the host origin is not in `DECLARION_FRAME_ANCESTORS`). A slow `getToken`
22
+ * does NOT cause this - the timer is cleared as soon as `ready` arrives.
23
+ */
24
+ readonly handshakeTimeout: "handshake-timeout";
25
+ /**
26
+ * The iframe asked the host to reload it (`reload-required`) - asset drift
27
+ * or a terminal auth failure inside the iframe.
28
+ */
29
+ readonly reloadRequired: "reload-required";
30
+ };
31
+ /** The union of all embed error code strings. */
32
+ export type EmbedErrorCode = (typeof EMBED_ERROR_CODES)[keyof typeof EMBED_ERROR_CODES];
33
+ /**
34
+ * A typed embed error. Always passed to the host `onError` callback and
35
+ * always also written to `console.error` with the `[declarion-embed]`
36
+ * prefix, so a misconfiguration is never silent.
37
+ *
38
+ * `cause` carries the originating error when one exists (e.g. the rejection
39
+ * value from `getToken`), preserving the stack for debugging.
40
+ */
41
+ export declare class EmbedError extends Error {
42
+ /** The stable, machine-readable error code. */
43
+ readonly code: EmbedErrorCode;
44
+ constructor(code: EmbedErrorCode, message: string, options?: {
45
+ cause?: unknown;
46
+ });
47
+ }
@@ -0,0 +1,29 @@
1
+ import { type EmbedMessage } from "./protocol";
2
+ /**
3
+ * The classification of an inbound `message` event after validation.
4
+ *
5
+ * - `valid`: a trusted, shape-correct, version-matched embed frame.
6
+ * - `protocol-mismatch`: origin + source are trusted, but the `protocol`
7
+ * version differs. The caller logs a warning; the frame is not acted on.
8
+ * - `rejected`: not a trusted embed frame (wrong origin, missing/foreign
9
+ * `source`, malformed envelope). The caller drops it silently.
10
+ */
11
+ export type InboundClassification = {
12
+ kind: "valid";
13
+ message: EmbedMessage;
14
+ } | {
15
+ kind: "protocol-mismatch";
16
+ received: unknown;
17
+ } | {
18
+ kind: "rejected";
19
+ };
20
+ /**
21
+ * Validate and classify an inbound `message` event against the trusted
22
+ * `declarionOrigin`.
23
+ *
24
+ * Returns `rejected` for anything that is not a trusted embed frame - the
25
+ * caller drops those with no logging. Returns `protocol-mismatch` when the
26
+ * frame is trusted but on a different protocol version. Returns `valid` with
27
+ * the typed envelope otherwise.
28
+ */
29
+ export declare function classifyInboundMessage(event: MessageEvent, declarionOrigin: string): InboundClassification;
@@ -0,0 +1,6 @@
1
+ export { createDeclarionEmbed } from "./core";
2
+ export type { DeclarionEmbedOptions, DeclarionEmbedHandle, EmbedNavigationContract, EmbedNavigateEvent, EmbedEvent, EmbedToken, EmbedGetToken, EmbedTheme, } from "./types";
3
+ export { EmbedError, EMBED_ERROR_CODES } from "./errors";
4
+ export type { EmbedErrorCode } from "./errors";
5
+ export { EMBED_MESSAGE_SOURCE, EMBED_MESSAGE_TYPES, EMBED_PROTOCOL_VERSION, } from "./protocol";
6
+ export type { EmbedMessage, EmbedMessageType, EmbedMessagePayloadMap, EmbedReadyPayload, EmbedSetTokenPayload, EmbedTokenExpiredPayload, EmbedReloadRequiredPayload, EmbedResizedPayload, EmbedNavigationPayload, EmbedDirtyChangedPayload, EmbedNavigatePayload, EmbedSetThemePayload, } from "./protocol";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { a as e, i as t, n, o as r, r as i, t as a } from "./core.js";
2
+ export { n as EMBED_ERROR_CODES, t as EMBED_MESSAGE_SOURCE, e as EMBED_MESSAGE_TYPES, r as EMBED_PROTOCOL_VERSION, i as EmbedError, a as createDeclarionEmbed };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The `source` discriminator stamped on every embed frame. Both the host SDK
3
+ * and the iframe filter inbound traffic on this value so unrelated
4
+ * `postMessage` frames (browser extensions, other widgets) are ignored.
5
+ */
6
+ export declare const EMBED_MESSAGE_SOURCE: "declarion-embed";
7
+ /**
8
+ * The protocol version this SDK speaks. Bumped only on a breaking
9
+ * envelope/payload change. The SDK warns when the iframe reports a different
10
+ * version (see the diagnostics path).
11
+ */
12
+ export declare const EMBED_PROTOCOL_VERSION: 1;
13
+ /**
14
+ * Every protocol message `type`. The object key is the camelCase name used in
15
+ * code; the value is the on-wire string. Events (iframe -> host) are past
16
+ * tense; commands (host -> iframe) are imperative. This SDK is the HOST side:
17
+ * it RECEIVES the iframe-to-host events and SENDS the host-to-iframe commands.
18
+ */
19
+ export declare const EMBED_MESSAGE_TYPES: {
20
+ /** iframe -> host: SDK mounted, requests the first token. */
21
+ readonly ready: "ready";
22
+ /** host -> iframe: deliver or refresh the embed token. */
23
+ readonly setToken: "set-token";
24
+ /** iframe -> host: token rejected or near expiry. */
25
+ readonly tokenExpired: "token-expired";
26
+ /** iframe -> host: the iframe must be reloaded. */
27
+ readonly reloadRequired: "reload-required";
28
+ /** iframe -> host: embed content height changed. */
29
+ readonly resized: "resized";
30
+ /** iframe -> host: `self` mode internal navigation happened. */
31
+ readonly navigated: "navigated";
32
+ /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */
33
+ readonly navigationRequested: "navigation-requested";
34
+ /** iframe -> host: the embedded screen's unsaved-edits state flipped. */
35
+ readonly dirtyChanged: "dirty-changed";
36
+ /** host -> iframe: drive the iframe to a screen (deep-linking). */
37
+ readonly navigate: "navigate";
38
+ /** host -> iframe: runtime theme switch. */
39
+ readonly setTheme: "set-theme";
40
+ };
41
+ /** The union of all on-wire message `type` strings. */
42
+ export type EmbedMessageType = (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];
43
+ /** `ready` payload: empty - the frame itself is the signal. */
44
+ export type EmbedReadyPayload = Record<string, never>;
45
+ /** `set-token` payload: the embed token and its absolute expiry. */
46
+ export interface EmbedSetTokenPayload {
47
+ /** The scoped, refresh-less embed JWT. */
48
+ readonly token: string;
49
+ /** RFC 3339 expiry timestamp of the token. */
50
+ readonly expires_at: string;
51
+ }
52
+ /** `token-expired` payload: empty - the host re-mints via `getToken`. */
53
+ export type EmbedTokenExpiredPayload = Record<string, never>;
54
+ /** `reload-required` payload: a human-readable reason for the reload. */
55
+ export interface EmbedReloadRequiredPayload {
56
+ /** Why the iframe must be reloaded (asset drift, terminal auth failure). */
57
+ readonly reason: string;
58
+ }
59
+ /** `resized` payload: the measured content height in CSS pixels. */
60
+ export interface EmbedResizedPayload {
61
+ /** Content height in CSS pixels. */
62
+ readonly height: number;
63
+ }
64
+ /**
65
+ * `navigated` / `navigation-requested` payload: the screen route and, when
66
+ * resolvable, the bound entity code and record id.
67
+ *
68
+ * `entity` and `recordId` are optional: a `custom` screen has no entity, and
69
+ * a list route has no record id.
70
+ */
71
+ export interface EmbedNavigationPayload {
72
+ /** The Declarion screen route path the navigation targets. */
73
+ readonly route: string;
74
+ /** The bound entity code, when the route resolves to one. */
75
+ readonly entity?: string;
76
+ /** The record id, when the route is a detail route with an id. */
77
+ readonly recordId?: string;
78
+ }
79
+ /**
80
+ * `dirty-changed` payload: whether the embedded screen currently has unsaved
81
+ * edits. The host tracks this so it can guard its own navigation (its menu,
82
+ * a host-initiated `navigate`) before moving the iframe away from a dirty
83
+ * screen - the iframe never pops a dialog for host-driven navigation.
84
+ */
85
+ export interface EmbedDirtyChangedPayload {
86
+ /** True when the embedded screen has unsaved edits. */
87
+ readonly dirty: boolean;
88
+ }
89
+ /** `navigate` payload: the route the host drives the iframe to. */
90
+ export interface EmbedNavigatePayload {
91
+ /** The Declarion screen route the host wants opened. */
92
+ readonly route: string;
93
+ }
94
+ /** `set-theme` payload: the theme the host switches the iframe to. */
95
+ export interface EmbedSetThemePayload {
96
+ /** The requested theme. */
97
+ readonly theme: EmbedTheme;
98
+ }
99
+ /** The theme hint carried on the `theme` URL param and `set-theme` frame. */
100
+ export type EmbedTheme = "light" | "dark";
101
+ /**
102
+ * Maps each message type to its payload shape. Used to type the inbound
103
+ * classifier and the outbound sender so the payload is checked against the
104
+ * message type.
105
+ */
106
+ export interface EmbedMessagePayloadMap {
107
+ [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;
108
+ [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;
109
+ [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;
110
+ [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;
111
+ [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;
112
+ [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;
113
+ [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;
114
+ [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;
115
+ [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;
116
+ [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;
117
+ }
118
+ /**
119
+ * The wire envelope. Every embed frame is exactly this shape: a fixed
120
+ * `source` + `protocol` pair, a discriminating `type`, and the typed
121
+ * `payload` for that type.
122
+ */
123
+ export interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {
124
+ readonly source: typeof EMBED_MESSAGE_SOURCE;
125
+ readonly protocol: typeof EMBED_PROTOCOL_VERSION;
126
+ readonly type: T;
127
+ readonly payload: EmbedMessagePayloadMap[T];
128
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactElement, Ref } from "react";
2
+ import type { DeclarionEmbedHandle, DeclarionEmbedOptions } from "./types";
3
+ /**
4
+ * Props for `<DeclarionEmbed />`.
5
+ *
6
+ * Every `createDeclarionEmbed` option except `container` is accepted as a
7
+ * prop - the component owns the container element. `className` and `style`
8
+ * style that container `<div>`.
9
+ */
10
+ export interface DeclarionEmbedProps extends Omit<DeclarionEmbedOptions, "container"> {
11
+ /** Optional class applied to the container element wrapping the iframe. */
12
+ readonly className?: string;
13
+ /** Optional inline style applied to the container element. */
14
+ readonly style?: React.CSSProperties;
15
+ }
16
+ /**
17
+ * `<DeclarionEmbed />` - the React binding for `@declarion/embed`.
18
+ *
19
+ * Accepts the same options as `createDeclarionEmbed` (minus `container`,
20
+ * which the component owns) and forwards a `DeclarionEmbedHandle` ref.
21
+ */
22
+ export declare const DeclarionEmbed: (props: DeclarionEmbedProps & {
23
+ ref?: Ref<DeclarionEmbedHandle>;
24
+ }) => ReactElement;
25
+ export type { DeclarionEmbedOptions, DeclarionEmbedHandle, EmbedNavigationContract, EmbedNavigateEvent, EmbedEvent, EmbedToken, EmbedGetToken, EmbedTheme, } from "./types";
26
+ export { EmbedError, EMBED_ERROR_CODES } from "./errors";
27
+ export type { EmbedErrorCode } from "./errors";
package/dist/react.js ADDED
@@ -0,0 +1,50 @@
1
+ import { n as e, r as t, t as n } from "./core.js";
2
+ import { forwardRef as r, useEffect as i, useImperativeHandle as a, useRef as o } from "react";
3
+ import { jsx as s } from "react/jsx-runtime";
4
+ //#region src/react.tsx
5
+ function c(e) {
6
+ return [
7
+ e.declarionOrigin,
8
+ e.route,
9
+ e.navigation ?? "self"
10
+ ].join("\0");
11
+ }
12
+ function l(e, t) {
13
+ let { className: r, style: l } = e, u = o(null), d = o(null), f = o(e);
14
+ return f.current = e, i(() => {
15
+ let e = u.current;
16
+ if (!e) return;
17
+ let t = n({
18
+ container: e,
19
+ declarionOrigin: f.current.declarionOrigin,
20
+ route: f.current.route,
21
+ navigation: f.current.navigation,
22
+ theme: f.current.theme,
23
+ title: f.current.title,
24
+ debug: f.current.debug,
25
+ getToken: () => f.current.getToken(),
26
+ onReady: () => f.current.onReady?.(),
27
+ onError: (e) => f.current.onError?.(e),
28
+ onNavigate: (e) => f.current.onNavigate?.(e),
29
+ onEvent: (e) => f.current.onEvent?.(e)
30
+ });
31
+ return d.current = t, () => {
32
+ t.destroy(), d.current = null;
33
+ };
34
+ }, [c(e)]), i(() => {
35
+ e.theme && d.current?.setTheme(e.theme);
36
+ }, [e.theme]), a(t, () => ({
37
+ navigate: (e) => d.current?.navigate(e),
38
+ setTheme: (e) => d.current?.setTheme(e),
39
+ destroy: () => d.current?.destroy()
40
+ }), []), /* @__PURE__ */ s("div", {
41
+ ref: u,
42
+ className: r,
43
+ style: l
44
+ });
45
+ }
46
+ var u = r(l);
47
+ //#endregion
48
+ export { u as DeclarionEmbed, e as EMBED_ERROR_CODES, t as EmbedError };
49
+
50
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","names":[],"sources":["../src/react.tsx"],"sourcesContent":["// `@declarion/embed/react` - the React binding.\n//\n// `<DeclarionEmbed />` wraps `createDeclarionEmbed` (the dependency-free\n// core) in a React component, managing the embed lifecycle across mount,\n// prop change, and unmount. React is a peer dependency of THIS entry only;\n// the core entry (`@declarion/embed`) stays dependency-free.\n\nimport { useEffect, useImperativeHandle, useRef } from \"react\";\nimport type { ForwardedRef, ReactElement, Ref } from \"react\";\nimport { forwardRef } from \"react\";\nimport { createDeclarionEmbed } from \"./core\";\nimport type {\n DeclarionEmbedHandle,\n DeclarionEmbedOptions,\n} from \"./types\";\n\n/**\n * Props for `<DeclarionEmbed />`.\n *\n * Every `createDeclarionEmbed` option except `container` is accepted as a\n * prop - the component owns the container element. `className` and `style`\n * style that container `<div>`.\n */\nexport interface DeclarionEmbedProps\n extends Omit<DeclarionEmbedOptions, \"container\"> {\n /** Optional class applied to the container element wrapping the iframe. */\n readonly className?: string;\n /** Optional inline style applied to the container element. */\n readonly style?: React.CSSProperties;\n}\n\n/**\n * The set of props that, when changed, MUST rebuild the embed: a different\n * Declarion deployment, route, navigation contract, or token source means a\n * different iframe. Theme is intentionally excluded - it is applied live via\n * the handle's `setTheme` without a rebuild.\n */\nfunction rebuildKey(props: DeclarionEmbedProps): string {\n return [\n props.declarionOrigin,\n props.route,\n props.navigation ?? \"self\",\n ].join(\"\\u0000\");\n}\n\n/**\n * Embed a Declarion screen as a white-label iframe.\n *\n * The component creates the embed on mount, rebuilds it when the deployment,\n * route, or navigation contract changes, and destroys it on unmount. A\n * `ref` exposes the imperative `DeclarionEmbedHandle` (`navigate`,\n * `setTheme`, `destroy`) for hosts that drive the iframe directly.\n *\n * `getToken` and the callback props are read through a ref, so passing a\n * fresh inline `getToken` on every render does NOT rebuild the iframe.\n */\nfunction DeclarionEmbedComponent(\n props: DeclarionEmbedProps,\n ref: ForwardedRef<DeclarionEmbedHandle>,\n): ReactElement {\n const { className, style } = props;\n const containerRef = useRef<HTMLDivElement | null>(null);\n const handleRef = useRef<DeclarionEmbedHandle | null>(null);\n\n // Latest props, read by the stable callbacks handed to the core. This\n // keeps `getToken` / `onReady` / `onError` / `onNavigate` / `onEvent`\n // current without rebuilding the iframe when an inline closure changes.\n const propsRef = useRef(props);\n propsRef.current = props;\n\n const key = rebuildKey(props);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return undefined;\n\n const handle = createDeclarionEmbed({\n container,\n // `propsRef.current` is always the latest props snapshot - React updates\n // the ref during render, before this effect runs - so these read\n // correctly on both the initial mount and a key-triggered rebuild.\n declarionOrigin: propsRef.current.declarionOrigin,\n route: propsRef.current.route,\n navigation: propsRef.current.navigation,\n theme: propsRef.current.theme,\n title: propsRef.current.title,\n debug: propsRef.current.debug,\n // The callbacks delegate through `propsRef` so the latest closure\n // always runs, even though the embed was built once.\n getToken: () => propsRef.current.getToken(),\n onReady: () => propsRef.current.onReady?.(),\n onError: (error) => propsRef.current.onError?.(error),\n onNavigate: (event) => propsRef.current.onNavigate?.(event),\n onEvent: (event) => propsRef.current.onEvent?.(event),\n });\n handleRef.current = handle;\n\n return () => {\n handle.destroy();\n handleRef.current = null;\n };\n // `key` changes only when the deployment, route, or navigation contract\n // changes - exactly the props that require a fresh iframe.\n }, [key]);\n\n // Apply a live theme change without a rebuild.\n useEffect(() => {\n if (props.theme) {\n handleRef.current?.setTheme(props.theme);\n }\n }, [props.theme]);\n\n // Expose the imperative handle. The wrapper stays valid across rebuilds:\n // it always delegates to the current `handleRef`.\n useImperativeHandle(\n ref,\n (): DeclarionEmbedHandle => ({\n navigate: (route) => handleRef.current?.navigate(route),\n setTheme: (theme) => handleRef.current?.setTheme(theme),\n destroy: () => handleRef.current?.destroy(),\n }),\n [],\n );\n\n return <div ref={containerRef} className={className} style={style} />;\n}\n\n/**\n * `<DeclarionEmbed />` - the React binding for `@declarion/embed`.\n *\n * Accepts the same options as `createDeclarionEmbed` (minus `container`,\n * which the component owns) and forwards a `DeclarionEmbedHandle` ref.\n */\nexport const DeclarionEmbed = forwardRef(DeclarionEmbedComponent) as (\n props: DeclarionEmbedProps & { ref?: Ref<DeclarionEmbedHandle> },\n) => ReactElement;\n\n// Re-export the option, handle, event, and protocol types so a React host\n// imports everything from the single `@declarion/embed/react` entry.\nexport type {\n DeclarionEmbedOptions,\n DeclarionEmbedHandle,\n EmbedNavigationContract,\n EmbedNavigateEvent,\n EmbedEvent,\n EmbedToken,\n EmbedGetToken,\n EmbedTheme,\n} from \"./types\";\nexport { EmbedError, EMBED_ERROR_CODES } from \"./errors\";\nexport type { EmbedErrorCode } from \"./errors\";\n"],"mappings":";;;;AAqCA,SAAS,EAAW,GAAoC;CACtD,OAAO;EACL,EAAM;EACN,EAAM;EACN,EAAM,cAAc;CACtB,EAAE,KAAK,IAAQ;AACjB;AAaA,SAAS,EACP,GACA,GACc;CACd,IAAM,EAAE,cAAW,aAAU,GACvB,IAAe,EAA8B,IAAI,GACjD,IAAY,EAAoC,IAAI,GAKpD,IAAW,EAAO,CAAK;CAyD7B,OAxDA,EAAS,UAAU,GAInB,QAAgB;EACd,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAS,EAAqB;GAClC;GAIA,iBAAiB,EAAS,QAAQ;GAClC,OAAO,EAAS,QAAQ;GACxB,YAAY,EAAS,QAAQ;GAC7B,OAAO,EAAS,QAAQ;GACxB,OAAO,EAAS,QAAQ;GACxB,OAAO,EAAS,QAAQ;GAGxB,gBAAgB,EAAS,QAAQ,SAAS;GAC1C,eAAe,EAAS,QAAQ,UAAU;GAC1C,UAAU,MAAU,EAAS,QAAQ,UAAU,CAAK;GACpD,aAAa,MAAU,EAAS,QAAQ,aAAa,CAAK;GAC1D,UAAU,MAAU,EAAS,QAAQ,UAAU,CAAK;EACtD,CAAC;EAGD,OAFA,EAAU,UAAU,SAEP;GAEX,AADA,EAAO,QAAQ,GACf,EAAU,UAAU;EACtB;CAGF,GAAG,CAjCS,EAAW,CAiCnB,CAAG,CAAC,GAGR,QAAgB;EACd,AAAI,EAAM,SACR,EAAU,SAAS,SAAS,EAAM,KAAK;CAE3C,GAAG,CAAC,EAAM,KAAK,CAAC,GAIhB,EACE,UAC6B;EAC3B,WAAW,MAAU,EAAU,SAAS,SAAS,CAAK;EACtD,WAAW,MAAU,EAAU,SAAS,SAAS,CAAK;EACtD,eAAe,EAAU,SAAS,QAAQ;CAC5C,IACA,CAAC,CACH,GAEO,kBAAC,OAAD;EAAK,KAAK;EAAyB;EAAkB;CAAQ,CAAA;AACtE;AAQA,IAAa,IAAiB,EAAW,CAAuB"}
@@ -0,0 +1,66 @@
1
+ import type { EmbedToken } from "./types";
2
+ /**
3
+ * Options for `createEmbedSession`. `apiKey`, `declarionOrigin`, and the
4
+ * three identity fields are required; `ttlSeconds` and `permissions` are
5
+ * optional and forwarded to the action unchanged.
6
+ */
7
+ export interface CreateEmbedSessionOptions {
8
+ /**
9
+ * The exact origin of the Declarion deployment, e.g.
10
+ * `https://app.example.com`. The action call is `${declarionOrigin}` +
11
+ * `/api/actions/auth.create_embed_session`.
12
+ */
13
+ readonly declarionOrigin: string;
14
+ /**
15
+ * The `dk:`-prefixed Declarion API key. The trust anchor for the mint
16
+ * call. MUST be held server-side only; never ship it to a browser.
17
+ */
18
+ readonly apiKey: string;
19
+ /** Tenant code; MUST match the API key's active tenant. */
20
+ readonly tenantCode: string;
21
+ /** Email of the target user the iframe runs as (host identity assertion). */
22
+ readonly userEmail: string;
23
+ /** Code of the Declarion screen to embed. */
24
+ readonly screenCode: string;
25
+ /**
26
+ * Token lifetime in seconds. Defaults to 600. The deployment caps it at
27
+ * 900 and raises values below 60 to 60.
28
+ */
29
+ readonly ttlSeconds?: number;
30
+ /**
31
+ * Extra permission allow-list. Every entry MUST be held by the target
32
+ * user; wildcards are rejected by the deployment.
33
+ */
34
+ readonly permissions?: readonly string[];
35
+ /**
36
+ * Optional `AbortSignal` to cancel the mint request (e.g. a host request
37
+ * timeout). Forwarded to `fetch`.
38
+ */
39
+ readonly signal?: AbortSignal;
40
+ }
41
+ /**
42
+ * The error thrown when `auth.create_embed_session` does not return a
43
+ * session. Carries the deployment's machine-readable error `code` (e.g.
44
+ * `EMBED_NOT_API_KEY`, `FORBIDDEN`) and the HTTP status so the host backend
45
+ * can branch on the failure.
46
+ */
47
+ export declare class EmbedSessionError extends Error {
48
+ /** The Declarion error code, or `null` when the response had none. */
49
+ readonly code: string | null;
50
+ /** The HTTP status of the action response. */
51
+ readonly status: number;
52
+ constructor(message: string, status: number, code: string | null);
53
+ }
54
+ /**
55
+ * Mint a short-lived, scoped embed session token for a host-asserted user.
56
+ *
57
+ * Calls `POST /api/actions/auth.create_embed_session` on the Declarion
58
+ * deployment, authenticating with the server-held `dk:` API key, and returns
59
+ * the `{ token, expires_at }` the host hands to the iframe (typically as the
60
+ * `getToken` result of `createDeclarionEmbed` / `<DeclarionEmbed />`).
61
+ *
62
+ * Throws `EmbedSessionError` on any non-success response, carrying the
63
+ * deployment's error code and HTTP status.
64
+ */
65
+ export declare function createEmbedSession(options: CreateEmbedSessionOptions): Promise<EmbedToken>;
66
+ export type { EmbedToken } from "./types";
package/dist/server.js ADDED
@@ -0,0 +1,50 @@
1
+ //#region src/server.ts
2
+ var e = "auth.create_embed_session", t = "/api/actions", n = 600, r = class e extends Error {
3
+ code;
4
+ status;
5
+ constructor(t, n, r) {
6
+ super(t), this.name = "EmbedSessionError", this.code = r, this.status = n, Object.setPrototypeOf(this, e.prototype);
7
+ }
8
+ };
9
+ function i(e) {
10
+ if (typeof e != "object" || !e) return !1;
11
+ let t = e;
12
+ return typeof t.token == "string" && t.token !== "" && typeof t.expires_at == "string" && t.expires_at !== "";
13
+ }
14
+ async function a(a) {
15
+ let o = `${new URL(a.declarionOrigin).origin}${t}/${e}`, s = {
16
+ tenant_code: a.tenantCode,
17
+ user_email: a.userEmail,
18
+ screen_code: a.screenCode,
19
+ ttl_seconds: a.ttlSeconds ?? n
20
+ };
21
+ a.permissions && a.permissions.length > 0 && (s.permissions = a.permissions);
22
+ let c = await fetch(o, {
23
+ method: "POST",
24
+ headers: {
25
+ "content-type": "application/json",
26
+ authorization: a.apiKey
27
+ },
28
+ body: JSON.stringify(s),
29
+ signal: a.signal
30
+ }), l;
31
+ try {
32
+ l = await c.json();
33
+ } catch {
34
+ throw new r(`auth.create_embed_session returned a non-JSON response (HTTP ${c.status}).`, c.status, null);
35
+ }
36
+ if (!c.ok) {
37
+ let e = l, t = e.error?.code ?? null;
38
+ throw new r(e.error?.message ?? `auth.create_embed_session failed with HTTP ${c.status}.`, c.status, t);
39
+ }
40
+ let u = l.result;
41
+ if (!i(u)) throw new r("auth.create_embed_session returned a malformed result: expected { token, expires_at }.", c.status, null);
42
+ return {
43
+ token: u.token,
44
+ expires_at: u.expires_at
45
+ };
46
+ }
47
+ //#endregion
48
+ export { r as EmbedSessionError, a as createEmbedSession };
49
+
50
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["// `@declarion/embed/server` - the Node-side mint helper.\n//\n// `createEmbedSession` performs the authenticated\n// `POST /api/actions/auth.create_embed_session` call so the host backend\n// writes one typed call instead of a hand-rolled HTTP request.\n//\n// This entry is SEPARATE from the browser core (`@declarion/embed`) and the\n// React binding (`@declarion/embed/react`) for one security reason: the\n// `dk:` API key is a backend-to-backend credential and MUST NEVER reach a\n// browser. Keeping it in its own entry means a frontend bundle that imports\n// `@declarion/embed` or `@declarion/embed/react` cannot pull the key path.\n//\n// The helper uses the global `fetch` (Node 18+, the platform's pinned Node\n// is 24) so it stays dependency-free.\n\nimport type { EmbedToken } from \"./types\";\n\n/** The fully-qualified action code for minting an embed session. */\nconst EMBED_SESSION_ACTION = \"auth.create_embed_session\";\n\n/** Path of the generic action dispatcher on a Declarion deployment. */\nconst ACTIONS_PATH = \"/api/actions\";\n\n/** Default token lifetime (seconds) when the host does not request one. */\nconst DEFAULT_TTL_SECONDS = 600;\n\n/**\n * Options for `createEmbedSession`. `apiKey`, `declarionOrigin`, and the\n * three identity fields are required; `ttlSeconds` and `permissions` are\n * optional and forwarded to the action unchanged.\n */\nexport interface CreateEmbedSessionOptions {\n /**\n * The exact origin of the Declarion deployment, e.g.\n * `https://app.example.com`. The action call is `${declarionOrigin}` +\n * `/api/actions/auth.create_embed_session`.\n */\n readonly declarionOrigin: string;\n /**\n * The `dk:`-prefixed Declarion API key. The trust anchor for the mint\n * call. MUST be held server-side only; never ship it to a browser.\n */\n readonly apiKey: string;\n /** Tenant code; MUST match the API key's active tenant. */\n readonly tenantCode: string;\n /** Email of the target user the iframe runs as (host identity assertion). */\n readonly userEmail: string;\n /** Code of the Declarion screen to embed. */\n readonly screenCode: string;\n /**\n * Token lifetime in seconds. Defaults to 600. The deployment caps it at\n * 900 and raises values below 60 to 60.\n */\n readonly ttlSeconds?: number;\n /**\n * Extra permission allow-list. Every entry MUST be held by the target\n * user; wildcards are rejected by the deployment.\n */\n readonly permissions?: readonly string[];\n /**\n * Optional `AbortSignal` to cancel the mint request (e.g. a host request\n * timeout). Forwarded to `fetch`.\n */\n readonly signal?: AbortSignal;\n}\n\n/**\n * The error thrown when `auth.create_embed_session` does not return a\n * session. Carries the deployment's machine-readable error `code` (e.g.\n * `EMBED_NOT_API_KEY`, `FORBIDDEN`) and the HTTP status so the host backend\n * can branch on the failure.\n */\nexport class EmbedSessionError extends Error {\n /** The Declarion error code, or `null` when the response had none. */\n readonly code: string | null;\n /** The HTTP status of the action response. */\n readonly status: number;\n\n constructor(message: string, status: number, code: string | null) {\n super(message);\n this.name = \"EmbedSessionError\";\n this.code = code;\n this.status = status;\n Object.setPrototypeOf(this, EmbedSessionError.prototype);\n }\n}\n\n/** The action dispatcher's success envelope: `{ status, result, ... }`. */\ninterface ActionSuccessEnvelope {\n readonly status?: string;\n readonly result?: unknown;\n}\n\n/** The action dispatcher's error envelope: `{ error: { message, code } }`. */\ninterface ActionErrorEnvelope {\n readonly error?: { readonly message?: string; readonly code?: string };\n}\n\n/** Narrow an unknown parsed body to a valid `{ token, expires_at }` result. */\nfunction isEmbedToken(value: unknown): value is EmbedToken {\n if (typeof value !== \"object\" || value === null) return false;\n const token = value as Partial<EmbedToken>;\n return (\n typeof token.token === \"string\" &&\n token.token !== \"\" &&\n typeof token.expires_at === \"string\" &&\n token.expires_at !== \"\"\n );\n}\n\n/**\n * Mint a short-lived, scoped embed session token for a host-asserted user.\n *\n * Calls `POST /api/actions/auth.create_embed_session` on the Declarion\n * deployment, authenticating with the server-held `dk:` API key, and returns\n * the `{ token, expires_at }` the host hands to the iframe (typically as the\n * `getToken` result of `createDeclarionEmbed` / `<DeclarionEmbed />`).\n *\n * Throws `EmbedSessionError` on any non-success response, carrying the\n * deployment's error code and HTTP status.\n */\nexport async function createEmbedSession(\n options: CreateEmbedSessionOptions,\n): Promise<EmbedToken> {\n const origin = new URL(options.declarionOrigin).origin;\n const url = `${origin}${ACTIONS_PATH}/${EMBED_SESSION_ACTION}`;\n\n const body: Record<string, unknown> = {\n tenant_code: options.tenantCode,\n user_email: options.userEmail,\n screen_code: options.screenCode,\n ttl_seconds: options.ttlSeconds ?? DEFAULT_TTL_SECONDS,\n };\n if (options.permissions && options.permissions.length > 0) {\n body.permissions = options.permissions;\n }\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n // The `dk:` API key travels in the Authorization header. The action\n // handler requires `AuthMethod == \"apikey\"`; a cookie or Basic auth\n // cannot mint an embed session.\n authorization: options.apiKey,\n },\n body: JSON.stringify(body),\n signal: options.signal,\n });\n\n // Parse the JSON body once; both the success and the error envelope are\n // JSON. A non-JSON body (a proxy error page) is surfaced as a generic\n // failure carrying the HTTP status.\n let parsed: unknown;\n try {\n parsed = await response.json();\n } catch {\n throw new EmbedSessionError(\n `auth.create_embed_session returned a non-JSON response ` +\n `(HTTP ${response.status}).`,\n response.status,\n null,\n );\n }\n\n if (!response.ok) {\n const errorBody = parsed as ActionErrorEnvelope;\n const code = errorBody.error?.code ?? null;\n const message =\n errorBody.error?.message ??\n `auth.create_embed_session failed with HTTP ${response.status}.`;\n throw new EmbedSessionError(message, response.status, code);\n }\n\n const result = (parsed as ActionSuccessEnvelope).result;\n if (!isEmbedToken(result)) {\n throw new EmbedSessionError(\n \"auth.create_embed_session returned a malformed result: expected \" +\n \"{ token, expires_at }.\",\n response.status,\n null,\n );\n }\n\n return { token: result.token, expires_at: result.expires_at };\n}\n\nexport type { EmbedToken } from \"./types\";\n"],"mappings":";AAkBA,IAAM,IAAuB,6BAGvB,IAAe,gBAGf,IAAsB,KAgDf,IAAb,MAAa,UAA0B,MAAM;CAE3C;CAEA;CAEA,YAAY,GAAiB,GAAgB,GAAqB;EAKhE,AAJA,MAAM,CAAO,GACb,KAAK,OAAO,qBACZ,KAAK,OAAO,GACZ,KAAK,SAAS,GACd,OAAO,eAAe,MAAM,EAAkB,SAAS;CACzD;AACF;AAcA,SAAS,EAAa,GAAqC;CACzD,IAAI,OAAO,KAAU,aAAY,GAAgB,OAAO;CACxD,IAAM,IAAQ;CACd,OACE,OAAO,EAAM,SAAU,YACvB,EAAM,UAAU,MAChB,OAAO,EAAM,cAAe,YAC5B,EAAM,eAAe;AAEzB;AAaA,eAAsB,EACpB,GACqB;CAErB,IAAM,IAAM,GADG,IAAI,IAAI,EAAQ,eAAe,EAAE,SACxB,EAAa,GAAG,KAElC,IAAgC;EACpC,aAAa,EAAQ;EACrB,YAAY,EAAQ;EACpB,aAAa,EAAQ;EACrB,aAAa,EAAQ,cAAc;CACrC;CACA,AAAI,EAAQ,eAAe,EAAQ,YAAY,SAAS,MACtD,EAAK,cAAc,EAAQ;CAG7B,IAAM,IAAW,MAAM,MAAM,GAAK;EAChC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAIhB,eAAe,EAAQ;EACzB;EACA,MAAM,KAAK,UAAU,CAAI;EACzB,QAAQ,EAAQ;CAClB,CAAC,GAKG;CACJ,IAAI;EACF,IAAS,MAAM,EAAS,KAAK;CAC/B,QAAQ;EACN,MAAM,IAAI,EACR,gEACW,EAAS,OAAO,KAC3B,EAAS,QACT,IACF;CACF;CAEA,IAAI,CAAC,EAAS,IAAI;EAChB,IAAM,IAAY,GACZ,IAAO,EAAU,OAAO,QAAQ;EAItC,MAAM,IAAI,EAFR,EAAU,OAAO,WACjB,8CAA8C,EAAS,OAAO,IAC3B,EAAS,QAAQ,CAAI;CAC5D;CAEA,IAAM,IAAU,EAAiC;CACjD,IAAI,CAAC,EAAa,CAAM,GACtB,MAAM,IAAI,EACR,0FAEA,EAAS,QACT,IACF;CAGF,OAAO;EAAE,OAAO,EAAO;EAAO,YAAY,EAAO;CAAW;AAC9D"}
@@ -0,0 +1,136 @@
1
+ import type { EmbedDirtyChangedPayload, EmbedNavigationPayload, EmbedReloadRequiredPayload, EmbedResizedPayload, EmbedTheme } from "./protocol";
2
+ import type { EmbedError } from "./errors";
3
+ /**
4
+ * The navigation contract for an embedded iframe (Section 1, Decision 16).
5
+ *
6
+ * - `self` (default): the iframe navigates internally between Declarion
7
+ * screen routes and emits `navigated` so the host can mirror its own URL,
8
+ * breadcrumb, and back button.
9
+ * - `delegated`: the iframe never moves; it emits `navigation-requested` and
10
+ * the host decides what to open.
11
+ */
12
+ export type EmbedNavigationContract = "self" | "delegated";
13
+ /**
14
+ * A token the host hands to the embedded iframe. Returned by `getToken` and
15
+ * delivered to the iframe in the `set-token` frame.
16
+ */
17
+ export interface EmbedToken {
18
+ /** The scoped, refresh-less embed JWT minted by `auth.create_embed_session`. */
19
+ readonly token: string;
20
+ /** RFC 3339 absolute expiry timestamp of the token. */
21
+ readonly expires_at: string;
22
+ }
23
+ /**
24
+ * The async host callback that owns the entire embed token lifecycle
25
+ * (Decision 23). The SDK calls it on mount and again on every
26
+ * `token-expired` frame; the host backend mints a fresh token via
27
+ * `auth.create_embed_session` and the SDK delivers it to the iframe. The
28
+ * developer never handles token refresh manually.
29
+ */
30
+ export type EmbedGetToken = () => Promise<EmbedToken>;
31
+ /**
32
+ * A protocol event surfaced to the generic `onEvent` callback. One variant
33
+ * per inbound iframe-to-host message the host may observe. The host-to-iframe
34
+ * frames (`set-token`, `navigate`, `set-theme`) are SDK outputs, not events,
35
+ * and are intentionally absent.
36
+ */
37
+ export type EmbedEvent = {
38
+ readonly type: "ready";
39
+ } | {
40
+ readonly type: "token-expired";
41
+ } | {
42
+ readonly type: "reload-required";
43
+ readonly payload: EmbedReloadRequiredPayload;
44
+ } | {
45
+ readonly type: "resized";
46
+ readonly payload: EmbedResizedPayload;
47
+ } | {
48
+ readonly type: "navigated";
49
+ readonly payload: EmbedNavigationPayload;
50
+ } | {
51
+ readonly type: "navigation-requested";
52
+ readonly payload: EmbedNavigationPayload;
53
+ } | {
54
+ readonly type: "dirty-changed";
55
+ readonly payload: EmbedDirtyChangedPayload;
56
+ };
57
+ /**
58
+ * A navigation event surfaced to the `onNavigate` callback. `mode` records
59
+ * which protocol frame produced it: `self` for `navigated` (the
60
+ * iframe already moved), `delegated` for `navigation-requested` (the iframe
61
+ * stayed and the host must open the route).
62
+ */
63
+ export interface EmbedNavigateEvent {
64
+ /** Which navigation frame produced this event. */
65
+ readonly mode: EmbedNavigationContract;
66
+ /** The Declarion screen route the navigation targets. */
67
+ readonly route: string;
68
+ /** The bound entity code, when the route resolves to one. */
69
+ readonly entity?: string;
70
+ /** The record id, when the route is a detail route with an id. */
71
+ readonly recordId?: string;
72
+ }
73
+ /**
74
+ * The options accepted by `createDeclarionEmbed` and the `<DeclarionEmbed />`
75
+ * React binding (Section 7 Core API table).
76
+ */
77
+ export interface DeclarionEmbedOptions {
78
+ /** DOM element that receives the iframe. The SDK appends the iframe to it. */
79
+ readonly container: HTMLElement;
80
+ /**
81
+ * The exact origin the Declarion deployment is served from
82
+ * (`https://app.example.com`). Used to build the iframe `src` and to
83
+ * exact-match every inbound `postMessage` frame.
84
+ */
85
+ readonly declarionOrigin: string;
86
+ /** The Declarion screen route to embed (the iframe's initial route). */
87
+ readonly route: string;
88
+ /**
89
+ * The async host callback returning a fresh embed token. Called on mount
90
+ * and on every `token-expired` frame. See `EmbedGetToken`.
91
+ */
92
+ readonly getToken: EmbedGetToken;
93
+ /**
94
+ * Navigation contract for this iframe. `"self"` (default) or
95
+ * `"delegated"`; sets the `nav` URL param.
96
+ */
97
+ readonly navigation?: EmbedNavigationContract;
98
+ /** Initial theme hint, applied before the iframe's first paint. */
99
+ readonly theme?: EmbedTheme;
100
+ /** The iframe `title` attribute. Defaults to a sensible label. */
101
+ readonly title?: string;
102
+ /** When true, logs the handshake and every protocol frame to the console. */
103
+ readonly debug?: boolean;
104
+ /**
105
+ * Fired once the embed has authenticated - the first token has been
106
+ * delivered to the iframe after the `ready` handshake. The SDK has no
107
+ * iframe-render signal, so this is the earliest point the host can treat
108
+ * the embedded screen as live.
109
+ */
110
+ readonly onReady?: () => void;
111
+ /** Fired on misconfiguration with a typed, actionable `EmbedError`. */
112
+ readonly onError?: (error: EmbedError) => void;
113
+ /** Fired for `navigated` (`self`) and `navigation-requested` (`delegated`). */
114
+ readonly onNavigate?: (event: EmbedNavigateEvent) => void;
115
+ /** Generic callback fired for every inbound protocol event. */
116
+ readonly onEvent?: (event: EmbedEvent) => void;
117
+ }
118
+ /**
119
+ * The imperative handle returned by `createDeclarionEmbed`. The host keeps
120
+ * it to drive the iframe and to tear it down.
121
+ */
122
+ export interface DeclarionEmbedHandle {
123
+ /**
124
+ * Drive the iframe to a Declarion screen route (deep-linking). Works in
125
+ * both navigation contracts.
126
+ */
127
+ navigate(route: string): void;
128
+ /** Switch the iframe theme at runtime. */
129
+ setTheme(theme: EmbedTheme): void;
130
+ /**
131
+ * Tear down the embed: detach the `message` listener, stop pending timers,
132
+ * and remove the iframe from the container. Idempotent.
133
+ */
134
+ destroy(): void;
135
+ }
136
+ export type { EmbedTheme } from "./protocol";
package/dist/url.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { EmbedNavigationContract, EmbedTheme } from "./types";
2
+ /** Query-param names that make up the embed URL contract. */
3
+ export declare const EMBED_PARAM_EMBED = "embed";
4
+ export declare const EMBED_PARAM_PARENT_ORIGIN = "parent_origin";
5
+ export declare const EMBED_PARAM_THEME = "theme";
6
+ export declare const EMBED_PARAM_NAV = "nav";
7
+ /** Default navigation contract when the host does not set `navigation`. */
8
+ export declare const DEFAULT_NAVIGATION_CONTRACT: EmbedNavigationContract;
9
+ /** Inputs needed to build the iframe `src`. */
10
+ export interface BuildEmbedSrcInput {
11
+ /** The Declarion deployment origin (`https://app.example.com`). */
12
+ readonly declarionOrigin: string;
13
+ /** The Declarion screen route to embed. */
14
+ readonly route: string;
15
+ /**
16
+ * The host page's own origin. Becomes `parent_origin` so the iframe knows
17
+ * exactly which origin may exchange `postMessage` frames with it.
18
+ */
19
+ readonly parentOrigin: string;
20
+ /** Navigation contract; becomes the `nav` param. */
21
+ readonly navigation: EmbedNavigationContract;
22
+ /** Optional initial theme; becomes the `theme` param when set. */
23
+ readonly theme?: EmbedTheme;
24
+ }
25
+ /**
26
+ * Build the absolute iframe `src` URL for an embedded Declarion screen.
27
+ *
28
+ * The `route` is resolved as a path against `declarionOrigin`; the four
29
+ * embed params are appended. `parent_origin` is the host's own origin so the
30
+ * iframe restricts its `postMessage` traffic to exactly that origin.
31
+ *
32
+ * Throws when `declarionOrigin` is not a parseable absolute origin - callers
33
+ * convert that into a typed `EmbedError` before raising it to the host.
34
+ */
35
+ export declare function buildEmbedSrc(input: BuildEmbedSrcInput): string;
36
+ /**
37
+ * Resolve a Declarion screen route to an absolute URL for a runtime
38
+ * `navigate` frame. The host passes a route string; the iframe consumes the
39
+ * route as-is, so this only normalizes it against the deployment origin for
40
+ * the host's own bookkeeping. Returns the input unchanged when it cannot be
41
+ * resolved (the iframe tolerates a relative route).
42
+ */
43
+ export declare function resolveRoute(declarionOrigin: string, route: string): string;