@lympid/embed 0.0.0-stage → 1.0.0-rc.2

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,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../cjs/protocol.js", "../cjs/bridge.js", "../cjs/modal.js", "../cjs/embed.js", "../cjs/index.js"],
4
+ "sourcesContent": ["\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.WIDGET_TO_HOST_SOURCE = exports.HOST_TO_WIDGET_SOURCE = void 0;\nexports.isWidgetToHostMessage = isWidgetToHostMessage;\n/**\n * `postMessage` wire protocol between the host page (this SDK) and the\n * widget iframe (the whitelabel app running in embed mode \u2014 see\n * `whitelabel-embed-mode` in the Phase 1 plan).\n *\n * Both directions are only ever accepted/sent against the exact origin\n * configured via `InitOptions.baseUrl` \u2014 see `createBridge()` in\n * `./bridge.ts`. This file only defines the message *shapes*; it has\n * zero DOM/`window` dependencies so it can be unit-tested directly.\n */\n/** Discriminator so a `postMessage` listener can cheaply ignore every\n * unrelated message landing on the same `window` (browser extensions,\n * other SDKs, etc.) before even checking the origin. */\nexports.HOST_TO_WIDGET_SOURCE = 'lympid-embed-host';\nexports.WIDGET_TO_HOST_SOURCE = 'lympid-embed-widget';\n/** Narrows an arbitrary `MessageEvent.data` payload to a\n * `WidgetToHostMessage`, without throwing on malformed input \u2014 a\n * `postMessage` listener must never crash the host page just because\n * some unrelated script on the same window posted something shaped\n * differently. */\nfunction isWidgetToHostMessage(data) {\n return (typeof data === 'object' &&\n data !== null &&\n data.source === exports.WIDGET_TO_HOST_SOURCE &&\n typeof data.type === 'string');\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.createBridge = createBridge;\nconst protocol_1 = require(\"./protocol\");\n/**\n * @param iframe The widget iframe this bridge talks to.\n * @param targetOrigin The exact origin the iframe was loaded from\n * (derived from `InitOptions.baseUrl`, never inferred from the\n * iframe's `src` string, which a compromised/misconfigured iframe\n * could have redirected away from).\n * @param onMessage Called for every well-formed `WidgetToHostMessage`\n * received from exactly `targetOrigin`. Messages from any other\n * origin, or that don't match the `WidgetToHostMessage` shape, are\n * silently dropped.\n */\nfunction createBridge(iframe, targetOrigin, onMessage) {\n const listener = (domEvent) => {\n // Origin check FIRST, before touching `data` at all \u2014 this is the\n // entire security boundary of the bridge. A mismatched origin is\n // dropped unconditionally, regardless of how well-formed the\n // payload looks.\n if (domEvent.origin !== targetOrigin) {\n return;\n }\n if (domEvent.source !== iframe.contentWindow) {\n return;\n }\n if (!(0, protocol_1.isWidgetToHostMessage)(domEvent.data)) {\n return;\n }\n onMessage(domEvent.data);\n };\n window.addEventListener('message', listener);\n return {\n send: (message) => {\n if (!iframe.contentWindow) {\n return;\n }\n iframe.contentWindow.postMessage({ ...message, source: protocol_1.HOST_TO_WIDGET_SOURCE }, targetOrigin);\n },\n teardown: () => {\n window.removeEventListener('message', listener);\n },\n };\n}\n", "\"use strict\";\n/**\n * Modal overlay + iframe DOM construction for the widget.\n *\n * Deliberately framework-agnostic (no React) and dependency-free \u2014 a\n * handful of inline-styled `div`/`iframe` nodes is simpler, smaller, and\n * safer than reaching for a UI library inside a script a partner embeds\n * on their own page (no risk of CSS/class-name collisions with the\n * partner's own stylesheet).\n */\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.createModal = createModal;\nconst Z_INDEX = 2147483000; // Comfortably above virtually any host-page UI.\nconst DEFAULT_HEIGHT_PX = 600;\n/** Light/dark panel background, matching the whitelabel app's own\n * `LEDGER_PAPER` surface token (`packages/shared-types/src/\n * design-tokens.ts`) so the modal's own chrome doesn't flash a white\n * box around a dark-themed widget. The host page has no visibility\n * into the iframe's actual in-app theme setting (cross-origin, and the\n * whitelabel app can toggle independently of OS preference), so this\n * follows the OS-level `prefers-color-scheme` media query as the best\n * available signal without a cross-origin theme-sync protocol. */\nconst PANEL_BACKGROUND_LIGHT = '#ffffff';\nconst PANEL_BACKGROUND_DARK = '#161B22';\nfunction prefersDarkColorScheme() {\n return (typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-color-scheme: dark)').matches);\n}\n/** Close glyph centered in the 32px button. Path matches\n * `apps/whitelabel/src/assets/images/svg/24/close.svg`. */\nfunction createCloseIcon() {\n const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n icon.setAttribute('viewBox', '0 0 24 24');\n icon.setAttribute('width', '20');\n icon.setAttribute('height', '20');\n icon.setAttribute('fill', 'none');\n icon.setAttribute('aria-hidden', 'true');\n const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n path.setAttribute('d', 'M17 7 7 17M7 7l10 10');\n path.setAttribute('stroke', 'currentColor');\n path.setAttribute('stroke-width', '1.3');\n path.setAttribute('stroke-linecap', 'round');\n path.setAttribute('stroke-linejoin', 'round');\n icon.appendChild(path);\n return icon;\n}\n/**\n * Builds and appends the backdrop + iframe modal to `container`.\n *\n * @param container Parent node to append the overlay to.\n * @param src The widget iframe's `src` URL (the partner's whitelabel\n * origin + `/embed` route + flow query param).\n * @param onBackdropClick Called when the end user clicks the backdrop\n * or the close button \u2014 the caller decides whether that closes the\n * modal (it always should; kept as a callback so `embed.ts` stays\n * the single owner of close/`onClose` semantics).\n */\nfunction createModal(container, src, onBackdropClick) {\n const overlay = document.createElement('div');\n overlay.setAttribute('data-lympid-embed', 'overlay');\n overlay.style.position = 'fixed';\n overlay.style.inset = '0';\n overlay.style.zIndex = String(Z_INDEX);\n overlay.style.display = 'flex';\n overlay.style.alignItems = 'center';\n overlay.style.justifyContent = 'center';\n overlay.style.backgroundColor = 'rgba(15, 23, 42, 0.6)';\n const panel = document.createElement('div');\n panel.setAttribute('data-lympid-embed', 'panel');\n panel.style.position = 'relative';\n panel.style.width = 'min(420px, calc(100vw - 32px))';\n panel.style.height = `min(${DEFAULT_HEIGHT_PX}px, calc(100vh - 32px))`;\n panel.style.maxHeight = 'calc(100vh - 32px)';\n panel.style.borderRadius = '16px';\n panel.style.overflow = 'hidden';\n panel.style.boxShadow = '0 20px 60px rgba(0, 0, 0, 0.35)';\n panel.style.backgroundColor = prefersDarkColorScheme()\n ? PANEL_BACKGROUND_DARK\n : PANEL_BACKGROUND_LIGHT;\n panel.style.transition = 'height 0.2s ease';\n // Keep the panel's background in step if the OS-level preference\n // changes while the modal is open (e.g. a scheduled light/dark\n // switch, or the user flips it in their OS settings mid-session).\n let colorSchemeMedia = null;\n let handleColorSchemeChange = null;\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');\n handleColorSchemeChange = (event) => {\n panel.style.backgroundColor = event.matches\n ? PANEL_BACKGROUND_DARK\n : PANEL_BACKGROUND_LIGHT;\n };\n colorSchemeMedia.addEventListener('change', handleColorSchemeChange);\n }\n const closeButton = document.createElement('button');\n closeButton.setAttribute('data-lympid-embed', 'close');\n closeButton.setAttribute('aria-label', 'Close');\n closeButton.type = 'button';\n closeButton.style.position = 'absolute';\n closeButton.style.top = '8px';\n closeButton.style.right = '8px';\n closeButton.style.zIndex = String(Z_INDEX + 1);\n closeButton.style.display = 'flex';\n closeButton.style.alignItems = 'center';\n closeButton.style.justifyContent = 'center';\n closeButton.style.width = '32px';\n closeButton.style.height = '32px';\n closeButton.style.margin = '0';\n closeButton.style.padding = '0';\n closeButton.style.border = 'none';\n closeButton.style.borderRadius = '50%';\n closeButton.style.backgroundColor = 'rgba(15, 23, 42, 0.06)';\n closeButton.style.color = '#585858';\n closeButton.style.cursor = 'pointer';\n closeButton.style.appearance = 'none';\n closeButton.style.setProperty('-webkit-appearance', 'none');\n // Same stroke as the whitelabel `Icon` named `close`\n // (`src/assets/images/svg/24/close.svg`), which every other modal\n // uses. A text \"\u00D7\" sits on the font baseline, so it looks high, and\n // a host `button { padding }` rule shifts it further. Flex + a reset\n // padding keeps the glyph centered on the partner page.\n closeButton.appendChild(createCloseIcon());\n const iframe = document.createElement('iframe');\n iframe.setAttribute('data-lympid-embed', 'iframe');\n iframe.src = src;\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.title = 'Lympid';\n // Phase 1 flows (connect/sign-in, KYC) need neither top-level\n // navigation nor same-origin escalation beyond what the whitelabel\n // app itself needs to run (forms, scripts, camera for KYC).\n iframe.setAttribute('allow', 'camera; microphone; clipboard-write');\n iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');\n closeButton.addEventListener('click', onBackdropClick);\n overlay.addEventListener('click', (domEvent) => {\n if (domEvent.target === overlay) {\n onBackdropClick();\n }\n });\n panel.appendChild(closeButton);\n panel.appendChild(iframe);\n overlay.appendChild(panel);\n container.appendChild(overlay);\n return {\n iframe,\n setHeight: (height) => {\n const clamped = Math.max(240, Math.min(height, (typeof window !== 'undefined' ? window.innerHeight : height) - 32));\n panel.style.height = `${clamped}px`;\n },\n remove: () => {\n if (colorSchemeMedia && handleColorSchemeChange) {\n colorSchemeMedia.removeEventListener('change', handleColorSchemeChange);\n }\n overlay.remove();\n },\n };\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.init = init;\nconst bridge_1 = require(\"./bridge\");\nconst modal_1 = require(\"./modal\");\nconst protocol_1 = require(\"./protocol\");\n/** Normalises a configured `baseUrl` into a bare origin string\n * (`\"https://host:port\"`, no path/trailing slash) suitable both for\n * building the iframe `src` and for `postMessage` origin checks. Throws\n * early \u2014 at `init()` time, not at `open()` time \u2014 on a malformed URL,\n * since a bad `baseUrl` is a partner-integration mistake that should\n * fail loudly immediately rather than silently drop every widget\n * message later. */\nfunction normalizeOrigin(baseUrl) {\n const url = new URL(baseUrl);\n return url.origin;\n}\nfunction buildIframeSrc(origin, flow, partnerId, rwaId) {\n const params = new URLSearchParams({ flow });\n if (partnerId) {\n params.set('partnerId', partnerId);\n }\n if (rwaId) {\n params.set('rwaId', rwaId);\n }\n return `${origin}/embed?${params.toString()}`;\n}\n/**\n * Creates a `LympidEmbedInstance` bound to one partner's whitelabel\n * origin. See `InitOptions`/`OpenOptions` in `./types.ts` for the full\n * contract, and `apps/whitelabel/src/app/embed/` (whitelabel-embed-mode)\n * for the counterpart that runs inside the iframe.\n */\nfunction init(options) {\n const origin = normalizeOrigin(options.baseUrl);\n const container = options.container ?? document.body;\n let current = null;\n let destroyed = false;\n /** Tears down the currently-open modal/bridge, if any, optionally\n * firing the `onClose` callback that was passed to the `open()` call\n * being torn down. */\n function teardownCurrent(fireOnClose) {\n if (!current) {\n return;\n }\n const { modal, bridge, onClose } = current;\n bridge.teardown();\n modal.remove();\n current = null;\n if (fireOnClose && onClose) {\n onClose();\n }\n }\n function open(openOptions) {\n if (destroyed) {\n throw new Error('This LympidEmbedInstance has been destroyed and cannot be reused \u2014 call init() again.');\n }\n // Replace any already-open modal from this instance first \u2014 only\n // one widget flow can sensibly be open at a time per instance.\n teardownCurrent(true);\n const src = buildIframeSrc(origin, openOptions.flow, options.partnerId, openOptions.rwaId);\n const modal = (0, modal_1.createModal)(container, src, () => teardownCurrent(true));\n const bridge = (0, bridge_1.createBridge)(modal.iframe, origin, (message) => {\n switch (message.type) {\n case 'ready': {\n bridge.send({\n source: protocol_1.HOST_TO_WIDGET_SOURCE,\n type: 'init',\n flow: openOptions.flow,\n hasSessionCodeProvider: !!openOptions.getSessionCode,\n partnerId: options.partnerId,\n rwaId: openOptions.rwaId,\n });\n break;\n }\n case 'request-session-code': {\n void handleSessionCodeRequest(bridge, openOptions, message.requestId);\n break;\n }\n case 'resize': {\n modal.setHeight(message.height);\n break;\n }\n case 'event': {\n openOptions.onEvent?.(message.event);\n break;\n }\n case 'close': {\n teardownCurrent(true);\n break;\n }\n default:\n break;\n }\n });\n current = { modal, bridge, onClose: openOptions.onClose };\n }\n function close() {\n teardownCurrent(true);\n }\n function destroy() {\n teardownCurrent(true);\n destroyed = true;\n }\n return { open, close, destroy };\n}\n/**\n * Resolves `openOptions.getSessionCode()` (if configured) and relays\n * the result back to the widget as `session-code`/`session-code-error`.\n * Extracted so `init()`'s `open()` switch stays a flat dispatch table.\n *\n * @param requestId Correlation id the widget attached to its\n * `request-session-code` message \u2014 echoed back unchanged so the\n * widget can match the response to the right in-flight request even\n * if several overlap.\n */\nasync function handleSessionCodeRequest(bridge, openOptions, requestId) {\n const provider = openOptions.getSessionCode;\n if (!provider) {\n // The widget asked for a session code on a partner that never\n // configured `getSessionCode` \u2014 this is an integration mistake\n // (likely a `connected`-model partner whose host page forgot the\n // callback). Surface it both as an event (for the host page's own\n // logging/monitoring) and back to the widget (so it can show the\n // end user a retry/contact-support state instead of hanging).\n const error = 'getSessionCode was not provided to open()';\n openOptions.onEvent?.({ type: 'session-code-error', requestId, error });\n bridge.send({\n source: protocol_1.HOST_TO_WIDGET_SOURCE,\n type: 'session-code-error',\n requestId,\n error,\n });\n return;\n }\n try {\n const code = await provider();\n bridge.send({\n source: protocol_1.HOST_TO_WIDGET_SOURCE,\n type: 'session-code',\n requestId,\n code,\n });\n }\n catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n bridge.send({\n source: protocol_1.HOST_TO_WIDGET_SOURCE,\n type: 'session-code-error',\n requestId,\n error: errorMessage,\n });\n }\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.init = void 0;\n/**\n * `@lympid/embed` \u2014 public entry point.\n *\n * ```ts\n * import { init } from '@lympid/embed';\n *\n * const widget = init({ baseUrl: 'https://invest.yourpartnerdomain.com' });\n *\n * widget.open({\n * flow: 'connect',\n * // Only needed for partners using the \"connected\" login model \u2014\n * // call YOUR OWN backend here, which in turn calls\n * // POST /api/v1/embed-sessions with your x-api-key.\n * getSessionCode: async () => {\n * const res = await fetch('/api/lympid/session-code', { method: 'POST' });\n * const { handoffCode } = await res.json();\n * return handoffCode;\n * },\n * onEvent: (event) => console.log('lympid event', event),\n * onClose: () => console.log('lympid widget closed'),\n * });\n * ```\n */\nvar embed_1 = require(\"./embed\");\nObject.defineProperty(exports, \"init\", { enumerable: true, get: function () { return embed_1.init; } });\n"],
5
+ "mappings": "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",
6
+ "names": ["require_protocol", "__commonJSMin", "exports", "isWidgetToHostMessage", "data", "require_bridge", "__commonJSMin", "exports", "createBridge", "protocol_1", "iframe", "targetOrigin", "onMessage", "listener", "domEvent", "message", "require_modal", "__commonJSMin", "exports", "createModal", "Z_INDEX", "DEFAULT_HEIGHT_PX", "PANEL_BACKGROUND_LIGHT", "PANEL_BACKGROUND_DARK", "prefersDarkColorScheme", "createCloseIcon", "icon", "path", "container", "src", "onBackdropClick", "overlay", "panel", "colorSchemeMedia", "handleColorSchemeChange", "event", "closeButton", "iframe", "domEvent", "height", "clamped", "require_embed", "__commonJSMin", "exports", "init", "bridge_1", "modal_1", "protocol_1", "normalizeOrigin", "baseUrl", "buildIframeSrc", "origin", "flow", "partnerId", "rwaId", "params", "options", "container", "current", "destroyed", "teardownCurrent", "fireOnClose", "modal", "bridge", "onClose", "open", "openOptions", "src", "message", "handleSessionCodeRequest", "close", "destroy", "requestId", "provider", "error", "code", "errorMessage", "require_index", "__commonJSMin", "exports", "embed_1"]
7
+ }
@@ -0,0 +1,31 @@
1
+ import { type HostToWidgetMessage, type WidgetToHostMessage } from './protocol';
2
+ /**
3
+ * The `postMessage` bridge between the host page and one open widget
4
+ * iframe, restricted in both directions to the exact origin configured
5
+ * via `InitOptions.baseUrl`.
6
+ *
7
+ * This is the only place in the SDK that touches `window.postMessage`
8
+ * or `window.addEventListener('message', ...)` — keeping it isolated
9
+ * makes the origin-restriction invariant easy to audit in one file.
10
+ */
11
+ export interface Bridge {
12
+ /** Sends a message to the widget iframe, targeted at `targetOrigin`
13
+ * only (never `'*'`). */
14
+ send: (message: HostToWidgetMessage) => void;
15
+ /** Tears down the `message` listener. Call once when the modal
16
+ * closes — a leaked listener would otherwise keep reacting to
17
+ * messages from a since-removed iframe. */
18
+ teardown: () => void;
19
+ }
20
+ /**
21
+ * @param iframe The widget iframe this bridge talks to.
22
+ * @param targetOrigin The exact origin the iframe was loaded from
23
+ * (derived from `InitOptions.baseUrl`, never inferred from the
24
+ * iframe's `src` string, which a compromised/misconfigured iframe
25
+ * could have redirected away from).
26
+ * @param onMessage Called for every well-formed `WidgetToHostMessage`
27
+ * received from exactly `targetOrigin`. Messages from any other
28
+ * origin, or that don't match the `WidgetToHostMessage` shape, are
29
+ * silently dropped.
30
+ */
31
+ export declare function createBridge(iframe: HTMLIFrameElement, targetOrigin: string, onMessage: (message: WidgetToHostMessage) => void): Bridge;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBridge = createBridge;
4
+ const protocol_1 = require("./protocol");
5
+ /**
6
+ * @param iframe The widget iframe this bridge talks to.
7
+ * @param targetOrigin The exact origin the iframe was loaded from
8
+ * (derived from `InitOptions.baseUrl`, never inferred from the
9
+ * iframe's `src` string, which a compromised/misconfigured iframe
10
+ * could have redirected away from).
11
+ * @param onMessage Called for every well-formed `WidgetToHostMessage`
12
+ * received from exactly `targetOrigin`. Messages from any other
13
+ * origin, or that don't match the `WidgetToHostMessage` shape, are
14
+ * silently dropped.
15
+ */
16
+ function createBridge(iframe, targetOrigin, onMessage) {
17
+ const listener = (domEvent) => {
18
+ // Origin check FIRST, before touching `data` at all — this is the
19
+ // entire security boundary of the bridge. A mismatched origin is
20
+ // dropped unconditionally, regardless of how well-formed the
21
+ // payload looks.
22
+ if (domEvent.origin !== targetOrigin) {
23
+ return;
24
+ }
25
+ if (domEvent.source !== iframe.contentWindow) {
26
+ return;
27
+ }
28
+ if (!(0, protocol_1.isWidgetToHostMessage)(domEvent.data)) {
29
+ return;
30
+ }
31
+ onMessage(domEvent.data);
32
+ };
33
+ window.addEventListener('message', listener);
34
+ return {
35
+ send: (message) => {
36
+ if (!iframe.contentWindow) {
37
+ return;
38
+ }
39
+ iframe.contentWindow.postMessage({ ...message, source: protocol_1.HOST_TO_WIDGET_SOURCE }, targetOrigin);
40
+ },
41
+ teardown: () => {
42
+ window.removeEventListener('message', listener);
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,8 @@
1
+ import type { InitOptions, LympidEmbedInstance } from './types';
2
+ /**
3
+ * Creates a `LympidEmbedInstance` bound to one partner's whitelabel
4
+ * origin. See `InitOptions`/`OpenOptions` in `./types.ts` for the full
5
+ * contract, and `apps/whitelabel/src/app/embed/` (whitelabel-embed-mode)
6
+ * for the counterpart that runs inside the iframe.
7
+ */
8
+ export declare function init(options: InitOptions): LympidEmbedInstance;
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init = init;
4
+ const bridge_1 = require("./bridge");
5
+ const modal_1 = require("./modal");
6
+ const protocol_1 = require("./protocol");
7
+ /** Normalises a configured `baseUrl` into a bare origin string
8
+ * (`"https://host:port"`, no path/trailing slash) suitable both for
9
+ * building the iframe `src` and for `postMessage` origin checks. Throws
10
+ * early — at `init()` time, not at `open()` time — on a malformed URL,
11
+ * since a bad `baseUrl` is a partner-integration mistake that should
12
+ * fail loudly immediately rather than silently drop every widget
13
+ * message later. */
14
+ function normalizeOrigin(baseUrl) {
15
+ const url = new URL(baseUrl);
16
+ return url.origin;
17
+ }
18
+ function buildIframeSrc(origin, flow, partnerId, rwaId) {
19
+ const params = new URLSearchParams({ flow });
20
+ if (partnerId) {
21
+ params.set('partnerId', partnerId);
22
+ }
23
+ if (rwaId) {
24
+ params.set('rwaId', rwaId);
25
+ }
26
+ return `${origin}/embed?${params.toString()}`;
27
+ }
28
+ /**
29
+ * Creates a `LympidEmbedInstance` bound to one partner's whitelabel
30
+ * origin. See `InitOptions`/`OpenOptions` in `./types.ts` for the full
31
+ * contract, and `apps/whitelabel/src/app/embed/` (whitelabel-embed-mode)
32
+ * for the counterpart that runs inside the iframe.
33
+ */
34
+ function init(options) {
35
+ const origin = normalizeOrigin(options.baseUrl);
36
+ const container = options.container ?? document.body;
37
+ let current = null;
38
+ let destroyed = false;
39
+ /** Tears down the currently-open modal/bridge, if any, optionally
40
+ * firing the `onClose` callback that was passed to the `open()` call
41
+ * being torn down. */
42
+ function teardownCurrent(fireOnClose) {
43
+ if (!current) {
44
+ return;
45
+ }
46
+ const { modal, bridge, onClose } = current;
47
+ bridge.teardown();
48
+ modal.remove();
49
+ current = null;
50
+ if (fireOnClose && onClose) {
51
+ onClose();
52
+ }
53
+ }
54
+ function open(openOptions) {
55
+ if (destroyed) {
56
+ throw new Error('This LympidEmbedInstance has been destroyed and cannot be reused — call init() again.');
57
+ }
58
+ // Replace any already-open modal from this instance first — only
59
+ // one widget flow can sensibly be open at a time per instance.
60
+ teardownCurrent(true);
61
+ const src = buildIframeSrc(origin, openOptions.flow, options.partnerId, openOptions.rwaId);
62
+ const modal = (0, modal_1.createModal)(container, src, () => teardownCurrent(true));
63
+ const bridge = (0, bridge_1.createBridge)(modal.iframe, origin, (message) => {
64
+ switch (message.type) {
65
+ case 'ready': {
66
+ bridge.send({
67
+ source: protocol_1.HOST_TO_WIDGET_SOURCE,
68
+ type: 'init',
69
+ flow: openOptions.flow,
70
+ hasSessionCodeProvider: !!openOptions.getSessionCode,
71
+ partnerId: options.partnerId,
72
+ rwaId: openOptions.rwaId,
73
+ });
74
+ break;
75
+ }
76
+ case 'request-session-code': {
77
+ void handleSessionCodeRequest(bridge, openOptions, message.requestId);
78
+ break;
79
+ }
80
+ case 'resize': {
81
+ modal.setHeight(message.height);
82
+ break;
83
+ }
84
+ case 'event': {
85
+ openOptions.onEvent?.(message.event);
86
+ break;
87
+ }
88
+ case 'close': {
89
+ teardownCurrent(true);
90
+ break;
91
+ }
92
+ default:
93
+ break;
94
+ }
95
+ });
96
+ current = { modal, bridge, onClose: openOptions.onClose };
97
+ }
98
+ function close() {
99
+ teardownCurrent(true);
100
+ }
101
+ function destroy() {
102
+ teardownCurrent(true);
103
+ destroyed = true;
104
+ }
105
+ return { open, close, destroy };
106
+ }
107
+ /**
108
+ * Resolves `openOptions.getSessionCode()` (if configured) and relays
109
+ * the result back to the widget as `session-code`/`session-code-error`.
110
+ * Extracted so `init()`'s `open()` switch stays a flat dispatch table.
111
+ *
112
+ * @param requestId Correlation id the widget attached to its
113
+ * `request-session-code` message — echoed back unchanged so the
114
+ * widget can match the response to the right in-flight request even
115
+ * if several overlap.
116
+ */
117
+ async function handleSessionCodeRequest(bridge, openOptions, requestId) {
118
+ const provider = openOptions.getSessionCode;
119
+ if (!provider) {
120
+ // The widget asked for a session code on a partner that never
121
+ // configured `getSessionCode` — this is an integration mistake
122
+ // (likely a `connected`-model partner whose host page forgot the
123
+ // callback). Surface it both as an event (for the host page's own
124
+ // logging/monitoring) and back to the widget (so it can show the
125
+ // end user a retry/contact-support state instead of hanging).
126
+ const error = 'getSessionCode was not provided to open()';
127
+ openOptions.onEvent?.({ type: 'session-code-error', requestId, error });
128
+ bridge.send({
129
+ source: protocol_1.HOST_TO_WIDGET_SOURCE,
130
+ type: 'session-code-error',
131
+ requestId,
132
+ error,
133
+ });
134
+ return;
135
+ }
136
+ try {
137
+ const code = await provider();
138
+ bridge.send({
139
+ source: protocol_1.HOST_TO_WIDGET_SOURCE,
140
+ type: 'session-code',
141
+ requestId,
142
+ code,
143
+ });
144
+ }
145
+ catch (error) {
146
+ const errorMessage = error instanceof Error ? error.message : String(error);
147
+ bridge.send({
148
+ source: protocol_1.HOST_TO_WIDGET_SOURCE,
149
+ type: 'session-code-error',
150
+ requestId,
151
+ error: errorMessage,
152
+ });
153
+ }
154
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@lympid/embed` — public entry point.
3
+ *
4
+ * ```ts
5
+ * import { init } from '@lympid/embed';
6
+ *
7
+ * const widget = init({ baseUrl: 'https://invest.yourpartnerdomain.com' });
8
+ *
9
+ * widget.open({
10
+ * flow: 'connect',
11
+ * // Only needed for partners using the "connected" login model —
12
+ * // call YOUR OWN backend here, which in turn calls
13
+ * // POST /api/v1/embed-sessions with your x-api-key.
14
+ * getSessionCode: async () => {
15
+ * const res = await fetch('/api/lympid/session-code', { method: 'POST' });
16
+ * const { handoffCode } = await res.json();
17
+ * return handoffCode;
18
+ * },
19
+ * onEvent: (event) => console.log('lympid event', event),
20
+ * onClose: () => console.log('lympid widget closed'),
21
+ * });
22
+ * ```
23
+ */
24
+ export { init } from './embed';
25
+ export type { EmbedFlow, InitOptions, LympidEmbedEvent, LympidEmbedInstance, OpenOptions, } from './types';
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init = void 0;
4
+ /**
5
+ * `@lympid/embed` — public entry point.
6
+ *
7
+ * ```ts
8
+ * import { init } from '@lympid/embed';
9
+ *
10
+ * const widget = init({ baseUrl: 'https://invest.yourpartnerdomain.com' });
11
+ *
12
+ * widget.open({
13
+ * flow: 'connect',
14
+ * // Only needed for partners using the "connected" login model —
15
+ * // call YOUR OWN backend here, which in turn calls
16
+ * // POST /api/v1/embed-sessions with your x-api-key.
17
+ * getSessionCode: async () => {
18
+ * const res = await fetch('/api/lympid/session-code', { method: 'POST' });
19
+ * const { handoffCode } = await res.json();
20
+ * return handoffCode;
21
+ * },
22
+ * onEvent: (event) => console.log('lympid event', event),
23
+ * onClose: () => console.log('lympid widget closed'),
24
+ * });
25
+ * ```
26
+ */
27
+ var embed_1 = require("./embed");
28
+ Object.defineProperty(exports, "init", { enumerable: true, get: function () { return embed_1.init; } });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Modal overlay + iframe DOM construction for the widget.
3
+ *
4
+ * Deliberately framework-agnostic (no React) and dependency-free — a
5
+ * handful of inline-styled `div`/`iframe` nodes is simpler, smaller, and
6
+ * safer than reaching for a UI library inside a script a partner embeds
7
+ * on their own page (no risk of CSS/class-name collisions with the
8
+ * partner's own stylesheet).
9
+ */
10
+ export interface ModalHandle {
11
+ /** The widget's iframe element. */
12
+ iframe: HTMLIFrameElement;
13
+ /** Resizes the iframe (and its backdrop) to the widget's reported
14
+ * content height, clamped to the viewport. */
15
+ setHeight: (height: number) => void;
16
+ /** Removes every DOM node this modal created. */
17
+ remove: () => void;
18
+ }
19
+ /**
20
+ * Builds and appends the backdrop + iframe modal to `container`.
21
+ *
22
+ * @param container Parent node to append the overlay to.
23
+ * @param src The widget iframe's `src` URL (the partner's whitelabel
24
+ * origin + `/embed` route + flow query param).
25
+ * @param onBackdropClick Called when the end user clicks the backdrop
26
+ * or the close button — the caller decides whether that closes the
27
+ * modal (it always should; kept as a callback so `embed.ts` stays
28
+ * the single owner of close/`onClose` semantics).
29
+ */
30
+ export declare function createModal(container: HTMLElement, src: string, onBackdropClick: () => void): ModalHandle;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * Modal overlay + iframe DOM construction for the widget.
4
+ *
5
+ * Deliberately framework-agnostic (no React) and dependency-free — a
6
+ * handful of inline-styled `div`/`iframe` nodes is simpler, smaller, and
7
+ * safer than reaching for a UI library inside a script a partner embeds
8
+ * on their own page (no risk of CSS/class-name collisions with the
9
+ * partner's own stylesheet).
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.createModal = createModal;
13
+ const Z_INDEX = 2147483000; // Comfortably above virtually any host-page UI.
14
+ const DEFAULT_HEIGHT_PX = 600;
15
+ /** Light/dark panel background, matching the whitelabel app's own
16
+ * `LEDGER_PAPER` surface token (`packages/shared-types/src/
17
+ * design-tokens.ts`) so the modal's own chrome doesn't flash a white
18
+ * box around a dark-themed widget. The host page has no visibility
19
+ * into the iframe's actual in-app theme setting (cross-origin, and the
20
+ * whitelabel app can toggle independently of OS preference), so this
21
+ * follows the OS-level `prefers-color-scheme` media query as the best
22
+ * available signal without a cross-origin theme-sync protocol. */
23
+ const PANEL_BACKGROUND_LIGHT = '#ffffff';
24
+ const PANEL_BACKGROUND_DARK = '#161B22';
25
+ function prefersDarkColorScheme() {
26
+ return (typeof window !== 'undefined' &&
27
+ typeof window.matchMedia === 'function' &&
28
+ window.matchMedia('(prefers-color-scheme: dark)').matches);
29
+ }
30
+ /** Close glyph centered in the 32px button. Path matches
31
+ * `apps/whitelabel/src/assets/images/svg/24/close.svg`. */
32
+ function createCloseIcon() {
33
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
34
+ icon.setAttribute('viewBox', '0 0 24 24');
35
+ icon.setAttribute('width', '20');
36
+ icon.setAttribute('height', '20');
37
+ icon.setAttribute('fill', 'none');
38
+ icon.setAttribute('aria-hidden', 'true');
39
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
40
+ path.setAttribute('d', 'M17 7 7 17M7 7l10 10');
41
+ path.setAttribute('stroke', 'currentColor');
42
+ path.setAttribute('stroke-width', '1.3');
43
+ path.setAttribute('stroke-linecap', 'round');
44
+ path.setAttribute('stroke-linejoin', 'round');
45
+ icon.appendChild(path);
46
+ return icon;
47
+ }
48
+ /**
49
+ * Builds and appends the backdrop + iframe modal to `container`.
50
+ *
51
+ * @param container Parent node to append the overlay to.
52
+ * @param src The widget iframe's `src` URL (the partner's whitelabel
53
+ * origin + `/embed` route + flow query param).
54
+ * @param onBackdropClick Called when the end user clicks the backdrop
55
+ * or the close button — the caller decides whether that closes the
56
+ * modal (it always should; kept as a callback so `embed.ts` stays
57
+ * the single owner of close/`onClose` semantics).
58
+ */
59
+ function createModal(container, src, onBackdropClick) {
60
+ const overlay = document.createElement('div');
61
+ overlay.setAttribute('data-lympid-embed', 'overlay');
62
+ overlay.style.position = 'fixed';
63
+ overlay.style.inset = '0';
64
+ overlay.style.zIndex = String(Z_INDEX);
65
+ overlay.style.display = 'flex';
66
+ overlay.style.alignItems = 'center';
67
+ overlay.style.justifyContent = 'center';
68
+ overlay.style.backgroundColor = 'rgba(15, 23, 42, 0.6)';
69
+ const panel = document.createElement('div');
70
+ panel.setAttribute('data-lympid-embed', 'panel');
71
+ panel.style.position = 'relative';
72
+ panel.style.width = 'min(420px, calc(100vw - 32px))';
73
+ panel.style.height = `min(${DEFAULT_HEIGHT_PX}px, calc(100vh - 32px))`;
74
+ panel.style.maxHeight = 'calc(100vh - 32px)';
75
+ panel.style.borderRadius = '16px';
76
+ panel.style.overflow = 'hidden';
77
+ panel.style.boxShadow = '0 20px 60px rgba(0, 0, 0, 0.35)';
78
+ panel.style.backgroundColor = prefersDarkColorScheme()
79
+ ? PANEL_BACKGROUND_DARK
80
+ : PANEL_BACKGROUND_LIGHT;
81
+ panel.style.transition = 'height 0.2s ease';
82
+ // Keep the panel's background in step if the OS-level preference
83
+ // changes while the modal is open (e.g. a scheduled light/dark
84
+ // switch, or the user flips it in their OS settings mid-session).
85
+ let colorSchemeMedia = null;
86
+ let handleColorSchemeChange = null;
87
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
88
+ colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
89
+ handleColorSchemeChange = (event) => {
90
+ panel.style.backgroundColor = event.matches
91
+ ? PANEL_BACKGROUND_DARK
92
+ : PANEL_BACKGROUND_LIGHT;
93
+ };
94
+ colorSchemeMedia.addEventListener('change', handleColorSchemeChange);
95
+ }
96
+ const closeButton = document.createElement('button');
97
+ closeButton.setAttribute('data-lympid-embed', 'close');
98
+ closeButton.setAttribute('aria-label', 'Close');
99
+ closeButton.type = 'button';
100
+ closeButton.style.position = 'absolute';
101
+ closeButton.style.top = '8px';
102
+ closeButton.style.right = '8px';
103
+ closeButton.style.zIndex = String(Z_INDEX + 1);
104
+ closeButton.style.display = 'flex';
105
+ closeButton.style.alignItems = 'center';
106
+ closeButton.style.justifyContent = 'center';
107
+ closeButton.style.width = '32px';
108
+ closeButton.style.height = '32px';
109
+ closeButton.style.margin = '0';
110
+ closeButton.style.padding = '0';
111
+ closeButton.style.border = 'none';
112
+ closeButton.style.borderRadius = '50%';
113
+ closeButton.style.backgroundColor = 'rgba(15, 23, 42, 0.06)';
114
+ closeButton.style.color = '#585858';
115
+ closeButton.style.cursor = 'pointer';
116
+ closeButton.style.appearance = 'none';
117
+ closeButton.style.setProperty('-webkit-appearance', 'none');
118
+ // Same stroke as the whitelabel `Icon` named `close`
119
+ // (`src/assets/images/svg/24/close.svg`), which every other modal
120
+ // uses. A text "×" sits on the font baseline, so it looks high, and
121
+ // a host `button { padding }` rule shifts it further. Flex + a reset
122
+ // padding keeps the glyph centered on the partner page.
123
+ closeButton.appendChild(createCloseIcon());
124
+ const iframe = document.createElement('iframe');
125
+ iframe.setAttribute('data-lympid-embed', 'iframe');
126
+ iframe.src = src;
127
+ iframe.style.width = '100%';
128
+ iframe.style.height = '100%';
129
+ iframe.style.border = 'none';
130
+ iframe.title = 'Lympid';
131
+ // Phase 1 flows (connect/sign-in, KYC) need neither top-level
132
+ // navigation nor same-origin escalation beyond what the whitelabel
133
+ // app itself needs to run (forms, scripts, camera for KYC).
134
+ iframe.setAttribute('allow', 'camera; microphone; clipboard-write');
135
+ iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
136
+ closeButton.addEventListener('click', onBackdropClick);
137
+ overlay.addEventListener('click', (domEvent) => {
138
+ if (domEvent.target === overlay) {
139
+ onBackdropClick();
140
+ }
141
+ });
142
+ panel.appendChild(closeButton);
143
+ panel.appendChild(iframe);
144
+ overlay.appendChild(panel);
145
+ container.appendChild(overlay);
146
+ return {
147
+ iframe,
148
+ setHeight: (height) => {
149
+ const clamped = Math.max(240, Math.min(height, (typeof window !== 'undefined' ? window.innerHeight : height) - 32));
150
+ panel.style.height = `${clamped}px`;
151
+ },
152
+ remove: () => {
153
+ if (colorSchemeMedia && handleColorSchemeChange) {
154
+ colorSchemeMedia.removeEventListener('change', handleColorSchemeChange);
155
+ }
156
+ overlay.remove();
157
+ },
158
+ };
159
+ }
@@ -0,0 +1,71 @@
1
+ import type { EmbedFlow, LympidEmbedEvent } from './types';
2
+ /**
3
+ * `postMessage` wire protocol between the host page (this SDK) and the
4
+ * widget iframe (the whitelabel app running in embed mode — see
5
+ * `whitelabel-embed-mode` in the Phase 1 plan).
6
+ *
7
+ * Both directions are only ever accepted/sent against the exact origin
8
+ * configured via `InitOptions.baseUrl` — see `createBridge()` in
9
+ * `./bridge.ts`. This file only defines the message *shapes*; it has
10
+ * zero DOM/`window` dependencies so it can be unit-tested directly.
11
+ */
12
+ /** Discriminator so a `postMessage` listener can cheaply ignore every
13
+ * unrelated message landing on the same `window` (browser extensions,
14
+ * other SDKs, etc.) before even checking the origin. */
15
+ export declare const HOST_TO_WIDGET_SOURCE = "lympid-embed-host";
16
+ export declare const WIDGET_TO_HOST_SOURCE = "lympid-embed-widget";
17
+ export type HostToWidgetMessage = {
18
+ source: typeof HOST_TO_WIDGET_SOURCE;
19
+ type: 'init';
20
+ flow: EmbedFlow;
21
+ /** True when the host page supplied a `getSessionCode` callback,
22
+ * i.e. this partner uses the `connected` login model. The widget
23
+ * uses this to decide whether to request a session code or run
24
+ * its own `lympid-code` email/phone + OTP UI. */
25
+ hasSessionCodeProvider: boolean;
26
+ /** `InitOptions.partnerId`, if the host page supplied one. Only
27
+ * required for `flow: 'register'` — see that field's own doc
28
+ * comment in `types.ts` for why registration needs this and the
29
+ * other flows don't. An untrusted hint, like the iframe URL's own
30
+ * `partnerId` query param; the widget's registration calls carry
31
+ * it to the API, which re-validates it server-side. */
32
+ partnerId?: string;
33
+ /** `OpenOptions.rwaId`, if the host page supplied one. Only
34
+ * meaningful for `flow: 'invest'`. */
35
+ rwaId?: string;
36
+ } | {
37
+ source: typeof HOST_TO_WIDGET_SOURCE;
38
+ type: 'session-code';
39
+ requestId: string;
40
+ code: string;
41
+ } | {
42
+ source: typeof HOST_TO_WIDGET_SOURCE;
43
+ type: 'session-code-error';
44
+ requestId: string;
45
+ error: string;
46
+ };
47
+ export type WidgetToHostMessage = {
48
+ source: typeof WIDGET_TO_HOST_SOURCE;
49
+ type: 'ready';
50
+ } | {
51
+ source: typeof WIDGET_TO_HOST_SOURCE;
52
+ type: 'request-session-code';
53
+ requestId: string;
54
+ } | {
55
+ source: typeof WIDGET_TO_HOST_SOURCE;
56
+ type: 'resize';
57
+ height: number;
58
+ } | {
59
+ source: typeof WIDGET_TO_HOST_SOURCE;
60
+ type: 'event';
61
+ event: LympidEmbedEvent;
62
+ } | {
63
+ source: typeof WIDGET_TO_HOST_SOURCE;
64
+ type: 'close';
65
+ };
66
+ /** Narrows an arbitrary `MessageEvent.data` payload to a
67
+ * `WidgetToHostMessage`, without throwing on malformed input — a
68
+ * `postMessage` listener must never crash the host page just because
69
+ * some unrelated script on the same window posted something shaped
70
+ * differently. */
71
+ export declare function isWidgetToHostMessage(data: unknown): data is WidgetToHostMessage;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WIDGET_TO_HOST_SOURCE = exports.HOST_TO_WIDGET_SOURCE = void 0;
4
+ exports.isWidgetToHostMessage = isWidgetToHostMessage;
5
+ /**
6
+ * `postMessage` wire protocol between the host page (this SDK) and the
7
+ * widget iframe (the whitelabel app running in embed mode — see
8
+ * `whitelabel-embed-mode` in the Phase 1 plan).
9
+ *
10
+ * Both directions are only ever accepted/sent against the exact origin
11
+ * configured via `InitOptions.baseUrl` — see `createBridge()` in
12
+ * `./bridge.ts`. This file only defines the message *shapes*; it has
13
+ * zero DOM/`window` dependencies so it can be unit-tested directly.
14
+ */
15
+ /** Discriminator so a `postMessage` listener can cheaply ignore every
16
+ * unrelated message landing on the same `window` (browser extensions,
17
+ * other SDKs, etc.) before even checking the origin. */
18
+ exports.HOST_TO_WIDGET_SOURCE = 'lympid-embed-host';
19
+ exports.WIDGET_TO_HOST_SOURCE = 'lympid-embed-widget';
20
+ /** Narrows an arbitrary `MessageEvent.data` payload to a
21
+ * `WidgetToHostMessage`, without throwing on malformed input — a
22
+ * `postMessage` listener must never crash the host page just because
23
+ * some unrelated script on the same window posted something shaped
24
+ * differently. */
25
+ function isWidgetToHostMessage(data) {
26
+ return (typeof data === 'object' &&
27
+ data !== null &&
28
+ data.source === exports.WIDGET_TO_HOST_SOURCE &&
29
+ typeof data.type === 'string');
30
+ }