@keypuncherlabs/live-preview 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @keypuncherlabs/live-preview
2
+
3
+ Push **live, validated CSS** from a parent application into an embedded preview
4
+ (such as a Storybook hosted on a different subdomain or origin) over
5
+ `window.postMessage`.
6
+
7
+ It is framework-agnostic, has no runtime dependencies, and is built
8
+ security-first — previews are often hosted on origins you do not fully control,
9
+ so every message is gated by an origin allowlist and the CSS is validated at
10
+ every hop before it is injected.
11
+
12
+ ## Why
13
+
14
+ When the preview lives on a different origin, you cannot reach into its DOM
15
+ directly. This library gives you a tiny, safe channel:
16
+
17
+ - **Sender** (parent app): validate CSS, then post it to a specific iframe and
18
+ origin.
19
+ - **Receiver** (embedded app): accept CSS only from allowlisted origins,
20
+ re-validate it, and inject it into a single `<style>` element via
21
+ `textContent` (never `innerHTML`).
22
+ - **Relay** (nested-iframe host, e.g. a Storybook *manager*): forward CSS one hop
23
+ down to the real preview frame, since a grandparent window can only post to its
24
+ direct child.
25
+
26
+ Message type on the wire: `live-preview-css`. Receivers/relays announce
27
+ readiness with `live-preview-ready` so the parent can (re)send without racing
28
+ startup.
29
+
30
+ ## Security model
31
+
32
+ - **Origin allowlist is required.** The receiver and relay reject any message
33
+ whose `event.origin` is not listed. Pass `['*']` only to deliberately opt out
34
+ (e.g. local development).
35
+ - **Explicit target origin.** The sender never posts to `'*'`; it always
36
+ addresses a concrete origin so a swapped/navigated iframe cannot receive CSS.
37
+ - **CSS is validated at every hop.** `validateCss` rejects (does not "clean")
38
+ anything dangerous: `<style>`/`<script>`/HTML markup (breakout attempts),
39
+ `expression()`, `javascript:`/`vbscript:`, `-moz-binding`, `behavior:`, control
40
+ characters, `@import` (off by default), non-`https`/`data` `url()` schemes (off
41
+ by default), and oversized payloads.
42
+ - **Safe injection.** CSS is written with `textContent` into one reused `<style>`
43
+ node, so markup cannot be injected and the DOM does not grow.
44
+
45
+ ## Usage
46
+
47
+ ### Parent app (sender)
48
+
49
+ ```ts
50
+ import { createLivePreviewSender } from '@keypuncherlabs/live-preview';
51
+
52
+ const iframe = document.querySelector('iframe')!;
53
+ const sender = createLivePreviewSender({
54
+ targetWindow: iframe.contentWindow!,
55
+ targetOrigin: new URL(iframe.src).origin, // never '*'
56
+ });
57
+
58
+ // Resend whenever the preview reports it is ready.
59
+ window.addEventListener('message', (e) => {
60
+ if (e.origin === new URL(iframe.src).origin && e.data?.type === 'live-preview-ready') {
61
+ sender.sendCss(currentCss);
62
+ }
63
+ });
64
+
65
+ const result = sender.sendCss(':root { --color-primary: #06f; }');
66
+ if (!result.valid) console.warn(result.errors);
67
+ ```
68
+
69
+ ### Embedded app (receiver)
70
+
71
+ ```ts
72
+ import { startLivePreviewReceiver } from '@keypuncherlabs/live-preview';
73
+
74
+ startLivePreviewReceiver({
75
+ allowedOrigins: ['https://app.example.com'],
76
+ styleId: 'live-preview-styles',
77
+ onReject: (reason) => console.warn(reason),
78
+ });
79
+ ```
80
+
81
+ ### Nested iframe host (relay)
82
+
83
+ ```ts
84
+ import { createLivePreviewRelay } from '@keypuncherlabs/live-preview';
85
+
86
+ createLivePreviewRelay({
87
+ allowedOrigins: ['https://app.example.com'],
88
+ getTargetWindow: () =>
89
+ (document.getElementById('preview-iframe') as HTMLIFrameElement | null)?.contentWindow,
90
+ targetOrigin: window.location.origin,
91
+ });
92
+ ```
93
+
94
+ ## Validation options
95
+
96
+ `validateCss(input, options)` and the `validation` option on the sender/receiver/
97
+ relay accept:
98
+
99
+ - `maxLength` — size cap (default 100,000 chars).
100
+ - `allowAtImport` — permit `@import` (default `false`).
101
+ - `allowExternalUrls` — permit any `url()` scheme except `javascript:`/`vbscript:`/
102
+ `file:` (default `false`).
103
+ - `allowedUrlSchemes` — schemes allowed when `allowExternalUrls` is off (default
104
+ `['https', 'data']`; relative URLs and fragments always pass).
105
+
106
+ ## Building
107
+
108
+ Run `nx build live-preview` to build the library.
109
+
110
+ ## Running unit tests
111
+
112
+ Run `nx test live-preview` to execute the unit tests via [Jest](https://jestjs.io).
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@keypuncherlabs/live-preview",
3
+ "version": "1.1.0",
4
+ "type": "commonjs",
5
+ "description": "Send validated CSS into an embedded preview (e.g. a Storybook on another origin) over postMessage. Framework-agnostic, dependency-free, security-first.",
6
+ "keywords": [
7
+ "live-preview",
8
+ "postmessage",
9
+ "iframe",
10
+ "css-injection",
11
+ "storybook",
12
+ "cross-origin",
13
+ "design-tokens"
14
+ ],
15
+ "license": "MIT",
16
+ "sideEffects": false,
17
+ "main": "./src/index.js",
18
+ "types": "./src/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./src/index.d.ts",
22
+ "default": "./src/index.js"
23
+ }
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "dependencies": {
32
+ "tslib": "^2.3.0"
33
+ }
34
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './lib/protocol';
2
+ export * from './lib/validate-css';
3
+ export * from './lib/sender';
4
+ export * from './lib/receiver';
5
+ export * from './lib/relay';
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./lib/protocol"), exports);
5
+ tslib_1.__exportStar(require("./lib/validate-css"), exports);
6
+ tslib_1.__exportStar(require("./lib/sender"), exports);
7
+ tslib_1.__exportStar(require("./lib/receiver"), exports);
8
+ tslib_1.__exportStar(require("./lib/relay"), exports);
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../libs/public/keypuncherlabs/live-preview/src/index.ts"],"names":[],"mappings":";;;AAAA,yDAA+B;AAC/B,6DAAmC;AACnC,uDAA6B;AAC7B,yDAA+B;AAC/B,sDAA4B"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Wire protocol shared by the sender (parent app), the receiver (embedded app),
3
+ * and the relay (a nested-iframe host such as a Storybook manager).
4
+ *
5
+ * Messages travel over `window.postMessage`, so they must be plain,
6
+ * structured-clonable objects. The `type` discriminator is namespaced with the
7
+ * `live-preview-` prefix so it is easy to identify and unlikely to collide with
8
+ * other postMessage traffic on the same window.
9
+ */
10
+ /** Message type for pushing a CSS payload into an embedded preview. */
11
+ export declare const LIVE_PREVIEW_CSS_TYPE: "live-preview-css";
12
+ /** Message type a receiver/relay emits upward once it is ready to apply CSS. */
13
+ export declare const LIVE_PREVIEW_READY_TYPE: "live-preview-ready";
14
+ /** Carries a CSS string from a parent app to an embedded preview. */
15
+ export interface LivePreviewCssMessage {
16
+ type: typeof LIVE_PREVIEW_CSS_TYPE;
17
+ css: string;
18
+ }
19
+ /**
20
+ * Announces that a receiver (or relay) has attached its listener. A parent app
21
+ * can wait for this before sending so the initial CSS is not lost to a startup
22
+ * race.
23
+ */
24
+ export interface LivePreviewReadyMessage {
25
+ type: typeof LIVE_PREVIEW_READY_TYPE;
26
+ }
27
+ export type LivePreviewMessage = LivePreviewCssMessage | LivePreviewReadyMessage;
28
+ /** Narrows arbitrary `postMessage` data to a CSS message. */
29
+ export declare function isCssMessage(value: unknown): value is LivePreviewCssMessage;
30
+ /** Narrows arbitrary `postMessage` data to a ready message. */
31
+ export declare function isReadyMessage(value: unknown): value is LivePreviewReadyMessage;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ /**
3
+ * Wire protocol shared by the sender (parent app), the receiver (embedded app),
4
+ * and the relay (a nested-iframe host such as a Storybook manager).
5
+ *
6
+ * Messages travel over `window.postMessage`, so they must be plain,
7
+ * structured-clonable objects. The `type` discriminator is namespaced with the
8
+ * `live-preview-` prefix so it is easy to identify and unlikely to collide with
9
+ * other postMessage traffic on the same window.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.LIVE_PREVIEW_READY_TYPE = exports.LIVE_PREVIEW_CSS_TYPE = void 0;
13
+ exports.isCssMessage = isCssMessage;
14
+ exports.isReadyMessage = isReadyMessage;
15
+ /** Message type for pushing a CSS payload into an embedded preview. */
16
+ exports.LIVE_PREVIEW_CSS_TYPE = 'live-preview-css';
17
+ /** Message type a receiver/relay emits upward once it is ready to apply CSS. */
18
+ exports.LIVE_PREVIEW_READY_TYPE = 'live-preview-ready';
19
+ /** Narrows arbitrary `postMessage` data to a CSS message. */
20
+ function isCssMessage(value) {
21
+ return (typeof value === 'object' &&
22
+ value !== null &&
23
+ value.type === exports.LIVE_PREVIEW_CSS_TYPE &&
24
+ typeof value.css === 'string');
25
+ }
26
+ /** Narrows arbitrary `postMessage` data to a ready message. */
27
+ function isReadyMessage(value) {
28
+ return (typeof value === 'object' &&
29
+ value !== null &&
30
+ value.type === exports.LIVE_PREVIEW_READY_TYPE);
31
+ }
32
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../../../../../libs/public/keypuncherlabs/live-preview/src/lib/protocol.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA0BH,oCAOC;AAGD,wCAMC;AAxCD,uEAAuE;AAC1D,QAAA,qBAAqB,GAAG,kBAA2B,CAAC;AAEjE,gFAAgF;AACnE,QAAA,uBAAuB,GAAG,oBAA6B,CAAC;AAmBrE,6DAA6D;AAC7D,SAAgB,YAAY,CAAC,KAAc;IACzC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAA4B,CAAC,IAAI,KAAK,6BAAqB;QAC5D,OAAQ,KAA2B,CAAC,GAAG,KAAK,QAAQ,CACrD,CAAC;AACJ,CAAC;AAED,+DAA+D;AAC/D,SAAgB,cAAc,CAAC,KAAc;IAC3C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAA4B,CAAC,IAAI,KAAK,+BAAuB,CAC/D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Embedded-app side of the live preview channel.
3
+ *
4
+ * The receiver listens for `live-preview-css` messages and injects the CSS into
5
+ * a single dedicated `<style>` element. Because the embedding page may live on a
6
+ * different origin than the parent app, two trust gates apply to every message:
7
+ *
8
+ * 1. `event.origin` must be in the configured allowlist. The allowlist is
9
+ * required — there is no implicit default. Pass `['*']` only to deliberately
10
+ * opt out (e.g. local development).
11
+ * 2. The CSS is re-validated here even though the sender already validated it;
12
+ * this layer never assumes an upstream hop is trustworthy.
13
+ *
14
+ * Injection uses `textContent` (never `innerHTML`) and reuses one style node, so
15
+ * markup cannot be injected and the DOM does not grow on repeated updates.
16
+ */
17
+ import { type CssValidationOptions } from './validate-css';
18
+ export declare const DEFAULT_STYLE_ID = "live-preview-styles";
19
+ export interface LivePreviewReceiverOptions {
20
+ /**
21
+ * Origins permitted to send CSS, e.g. `['https://app.example.com']`. Required.
22
+ * Use `['*']` only to intentionally accept any origin (development).
23
+ */
24
+ allowedOrigins: string[];
25
+ /** Document to inject into. Defaults to the ambient `document`. */
26
+ target?: Document;
27
+ /** Window to attach the message listener to. Defaults to the ambient `window`. */
28
+ source?: Window;
29
+ /** Id of the injected `<style>` element. Defaults to {@link DEFAULT_STYLE_ID}. */
30
+ styleId?: string;
31
+ /** Validation overrides applied to incoming CSS. */
32
+ validation?: CssValidationOptions;
33
+ /** Called after CSS is successfully applied. */
34
+ onApply?: (css: string) => void;
35
+ /** Called when a message is rejected, with a human-readable reason. */
36
+ onReject?: (reason: string) => void;
37
+ }
38
+ export interface LivePreviewReceiver {
39
+ /** Detach the listener. */
40
+ stop: () => void;
41
+ }
42
+ export declare function startLivePreviewReceiver(options: LivePreviewReceiverOptions): LivePreviewReceiver;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * Embedded-app side of the live preview channel.
4
+ *
5
+ * The receiver listens for `live-preview-css` messages and injects the CSS into
6
+ * a single dedicated `<style>` element. Because the embedding page may live on a
7
+ * different origin than the parent app, two trust gates apply to every message:
8
+ *
9
+ * 1. `event.origin` must be in the configured allowlist. The allowlist is
10
+ * required — there is no implicit default. Pass `['*']` only to deliberately
11
+ * opt out (e.g. local development).
12
+ * 2. The CSS is re-validated here even though the sender already validated it;
13
+ * this layer never assumes an upstream hop is trustworthy.
14
+ *
15
+ * Injection uses `textContent` (never `innerHTML`) and reuses one style node, so
16
+ * markup cannot be injected and the DOM does not grow on repeated updates.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.DEFAULT_STYLE_ID = void 0;
20
+ exports.startLivePreviewReceiver = startLivePreviewReceiver;
21
+ const protocol_1 = require("./protocol");
22
+ const validate_css_1 = require("./validate-css");
23
+ exports.DEFAULT_STYLE_ID = 'live-preview-styles';
24
+ const isOriginAllowed = (origin, allowed) => allowed.includes('*') || allowed.includes(origin);
25
+ const getOrCreateStyle = (doc, styleId) => {
26
+ const existing = doc.getElementById(styleId);
27
+ if (existing instanceof HTMLStyleElement) {
28
+ return existing;
29
+ }
30
+ const style = doc.createElement('style');
31
+ style.id = styleId;
32
+ style.setAttribute('data-live-preview', '');
33
+ doc.head.appendChild(style);
34
+ return style;
35
+ };
36
+ function startLivePreviewReceiver(options) {
37
+ const { allowedOrigins, target = document, source = window, styleId = exports.DEFAULT_STYLE_ID, validation, onApply, onReject, } = options;
38
+ if (!allowedOrigins || allowedOrigins.length === 0) {
39
+ throw new Error('startLivePreviewReceiver requires a non-empty allowedOrigins list');
40
+ }
41
+ const handleMessage = (event) => {
42
+ if (!isOriginAllowed(event.origin, allowedOrigins)) {
43
+ return; // Silently ignore untrusted origins — not actionable to the page.
44
+ }
45
+ if (!(0, protocol_1.isCssMessage)(event.data)) {
46
+ return;
47
+ }
48
+ const result = (0, validate_css_1.validateCss)(event.data.css, validation);
49
+ if (!result.valid) {
50
+ onReject === null || onReject === void 0 ? void 0 : onReject(`rejected css from ${event.origin}: ${result.errors.join('; ')}`);
51
+ return;
52
+ }
53
+ getOrCreateStyle(target, styleId).textContent = result.css;
54
+ onApply === null || onApply === void 0 ? void 0 : onApply(result.css);
55
+ };
56
+ source.addEventListener('message', handleMessage);
57
+ // Announce readiness upward so a parent can (re)send the current CSS without
58
+ // racing this listener's attachment.
59
+ if (source.parent && source.parent !== source) {
60
+ const ready = { type: protocol_1.LIVE_PREVIEW_READY_TYPE };
61
+ source.parent.postMessage(ready, '*');
62
+ }
63
+ return {
64
+ stop() {
65
+ source.removeEventListener('message', handleMessage);
66
+ },
67
+ };
68
+ }
69
+ //# sourceMappingURL=receiver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../../../../../../libs/public/keypuncherlabs/live-preview/src/lib/receiver.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAmDH,4DAiDC;AAlGD,yCAIoB;AACpB,iDAAwE;AAE3D,QAAA,gBAAgB,GAAG,qBAAqB,CAAC;AA2BtD,MAAM,eAAe,GAAG,CAAC,MAAc,EAAE,OAAiB,EAAW,EAAE,CACrE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEpD,MAAM,gBAAgB,GAAG,CAAC,GAAa,EAAE,OAAe,EAAoB,EAAE;IAC5E,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,QAAQ,YAAY,gBAAgB,EAAE,CAAC;QACzC,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC;IACnB,KAAK,CAAC,YAAY,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;IAC5C,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,SAAgB,wBAAwB,CACtC,OAAmC;IAEnC,MAAM,EACJ,cAAc,EACd,MAAM,GAAG,QAAQ,EACjB,MAAM,GAAG,MAAM,EACf,OAAO,GAAG,wBAAgB,EAC1B,UAAU,EACV,OAAO,EACP,QAAQ,GACT,GAAG,OAAO,CAAC;IAEZ,IAAI,CAAC,cAAc,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACvF,CAAC;IAED,MAAM,aAAa,GAAG,CAAC,KAAmB,EAAQ,EAAE;QAClD,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC;YACnD,OAAO,CAAC,kEAAkE;QAC5E,CAAC;QACD,IAAI,CAAC,IAAA,uBAAY,EAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,IAAA,0BAAW,EAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAClB,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAG,qBAAqB,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7E,OAAO;QACT,CAAC;QAED,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC;QAC3D,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC,CAAC;IAEF,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IAElD,6EAA6E;IAC7E,qCAAqC;IACrC,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9C,MAAM,KAAK,GAA4B,EAAE,IAAI,EAAE,kCAAuB,EAAE,CAAC;QACzE,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,OAAO;QACL,IAAI;YACF,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QACvD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Nested-iframe glue for hosts that sit between the parent app and the actual
3
+ * preview document — most notably a Storybook *manager*, which the parent posts
4
+ * to but which renders stories in a nested `#storybook-preview-iframe`.
5
+ *
6
+ * A grandparent window can only `postMessage` its direct child, so CSS has to be
7
+ * relayed one hop down. The relay:
8
+ *
9
+ * - accepts `live-preview-css` only from allowlisted parent origins,
10
+ * - re-validates the CSS (defense in depth) before forwarding,
11
+ * - forwards it to a child window resolved lazily (the child may not exist yet
12
+ * when the relay starts), and
13
+ * - forwards the child's `live-preview-ready` ping back up to its own parent so
14
+ * the original sender's readiness handshake works end to end.
15
+ */
16
+ import { type CssValidationOptions } from './validate-css';
17
+ export interface LivePreviewRelayOptions {
18
+ /** Parent origins permitted to send CSS. Required. `['*']` opts out (dev). */
19
+ allowedOrigins: string[];
20
+ /** Resolves the child window to forward to (e.g. the nested preview iframe). */
21
+ getTargetWindow: () => Window | null | undefined;
22
+ /** Exact origin of the child window. Usually `window.location.origin`. */
23
+ targetOrigin: string;
24
+ /** Window to listen on. Defaults to the ambient `window`. */
25
+ source?: Window;
26
+ /** Validation overrides applied to relayed CSS. */
27
+ validation?: CssValidationOptions;
28
+ /** Called when CSS is rejected before forwarding, with a reason. */
29
+ onReject?: (reason: string) => void;
30
+ }
31
+ export interface LivePreviewRelay {
32
+ /** Detach the listener. */
33
+ stop: () => void;
34
+ }
35
+ export declare function createLivePreviewRelay(options: LivePreviewRelayOptions): LivePreviewRelay;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * Nested-iframe glue for hosts that sit between the parent app and the actual
4
+ * preview document — most notably a Storybook *manager*, which the parent posts
5
+ * to but which renders stories in a nested `#storybook-preview-iframe`.
6
+ *
7
+ * A grandparent window can only `postMessage` its direct child, so CSS has to be
8
+ * relayed one hop down. The relay:
9
+ *
10
+ * - accepts `live-preview-css` only from allowlisted parent origins,
11
+ * - re-validates the CSS (defense in depth) before forwarding,
12
+ * - forwards it to a child window resolved lazily (the child may not exist yet
13
+ * when the relay starts), and
14
+ * - forwards the child's `live-preview-ready` ping back up to its own parent so
15
+ * the original sender's readiness handshake works end to end.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.createLivePreviewRelay = createLivePreviewRelay;
19
+ const protocol_1 = require("./protocol");
20
+ const validate_css_1 = require("./validate-css");
21
+ const isOriginAllowed = (origin, allowed) => allowed.includes('*') || allowed.includes(origin);
22
+ function createLivePreviewRelay(options) {
23
+ const { allowedOrigins, getTargetWindow, targetOrigin, source = window, validation, onReject, } = options;
24
+ if (!allowedOrigins || allowedOrigins.length === 0) {
25
+ throw new Error('createLivePreviewRelay requires a non-empty allowedOrigins list');
26
+ }
27
+ if (!targetOrigin || targetOrigin === '*') {
28
+ throw new Error('createLivePreviewRelay requires an explicit targetOrigin (cannot be "*")');
29
+ }
30
+ const handleMessage = (event) => {
31
+ // A ready ping bubbling up from the child preview: pass it to our parent so
32
+ // the original sender can complete its handshake.
33
+ if ((0, protocol_1.isReadyMessage)(event.data)) {
34
+ if (source.parent && source.parent !== source) {
35
+ source.parent.postMessage(event.data, '*');
36
+ }
37
+ return;
38
+ }
39
+ if (!isOriginAllowed(event.origin, allowedOrigins)) {
40
+ return;
41
+ }
42
+ if (!(0, protocol_1.isCssMessage)(event.data)) {
43
+ return;
44
+ }
45
+ const result = (0, validate_css_1.validateCss)(event.data.css, validation);
46
+ if (!result.valid) {
47
+ onReject === null || onReject === void 0 ? void 0 : onReject(`relay rejected css from ${event.origin}: ${result.errors.join('; ')}`);
48
+ return;
49
+ }
50
+ const child = getTargetWindow();
51
+ if (!child) {
52
+ return; // Preview frame not mounted yet; the sender resends on ready.
53
+ }
54
+ const message = { type: protocol_1.LIVE_PREVIEW_CSS_TYPE, css: result.css };
55
+ child.postMessage(message, targetOrigin);
56
+ };
57
+ source.addEventListener('message', handleMessage);
58
+ return {
59
+ stop() {
60
+ source.removeEventListener('message', handleMessage);
61
+ },
62
+ };
63
+ }
64
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.js","sourceRoot":"","sources":["../../../../../../../libs/public/keypuncherlabs/live-preview/src/lib/relay.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAiCH,wDAwDC;AAvFD,yCAKoB;AACpB,iDAAwE;AAsBxE,MAAM,eAAe,GAAG,CAAC,MAAc,EAAE,OAAiB,EAAW,EAAE,CACrE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEpD,SAAgB,sBAAsB,CAAC,OAAgC;IACrE,MAAM,EACJ,cAAc,EACd,eAAe,EACf,YAAY,EACZ,MAAM,GAAG,MAAM,EACf,UAAU,EACV,QAAQ,GACT,GAAG,OAAO,CAAC;IAEZ,IAAI,CAAC,cAAc,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACrF,CAAC;IACD,IAAI,CAAC,YAAY,IAAI,YAAY,KAAK,GAAG,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,CAAC;IAED,MAAM,aAAa,GAAG,CAAC,KAAmB,EAAQ,EAAE;QAClD,4EAA4E;QAC5E,kDAAkD;QAClD,IAAI,IAAA,yBAAc,EAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;gBAC9C,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC7C,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC;YACnD,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAA,uBAAY,EAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,IAAA,0BAAW,EAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAClB,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAG,2BAA2B,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnF,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,8DAA8D;QACxE,CAAC;QAED,MAAM,OAAO,GAA0B,EAAE,IAAI,EAAE,gCAAqB,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;QACxF,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC3C,CAAC,CAAC;IAEF,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IAElD,OAAO;QACL,IAAI;YACF,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QACvD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Parent-app side of the live preview channel.
3
+ *
4
+ * A sender is bound to one target window (typically an embedded `<iframe>`'s
5
+ * `contentWindow`) and one explicit target origin. CSS is validated before it
6
+ * leaves this window, and the post is always addressed to a concrete origin —
7
+ * never `'*'` — so a navigated-away or swapped iframe can never receive it.
8
+ */
9
+ import { type CssValidationOptions, type CssValidationResult } from './validate-css';
10
+ export interface LivePreviewSenderOptions {
11
+ /** The window to post to, e.g. `iframe.contentWindow`. */
12
+ targetWindow: Window;
13
+ /**
14
+ * The exact origin of the embedded preview, e.g. `https://preview.example.com`.
15
+ * Wildcards are rejected: posting CSS to `'*'` would leak it to whatever
16
+ * document currently occupies the frame.
17
+ */
18
+ targetOrigin: string;
19
+ /** Validation overrides applied before sending. */
20
+ validation?: CssValidationOptions;
21
+ }
22
+ export interface LivePreviewSender {
23
+ /**
24
+ * Validate and post a CSS payload. Returns the validation result; when
25
+ * invalid, nothing is sent and `result.errors` explains why.
26
+ */
27
+ sendCss(css: string): CssValidationResult;
28
+ }
29
+ export declare function createLivePreviewSender(options: LivePreviewSenderOptions): LivePreviewSender;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * Parent-app side of the live preview channel.
4
+ *
5
+ * A sender is bound to one target window (typically an embedded `<iframe>`'s
6
+ * `contentWindow`) and one explicit target origin. CSS is validated before it
7
+ * leaves this window, and the post is always addressed to a concrete origin —
8
+ * never `'*'` — so a navigated-away or swapped iframe can never receive it.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createLivePreviewSender = createLivePreviewSender;
12
+ const protocol_1 = require("./protocol");
13
+ const validate_css_1 = require("./validate-css");
14
+ function createLivePreviewSender(options) {
15
+ const { targetWindow, targetOrigin, validation } = options;
16
+ if (!targetOrigin || targetOrigin === '*') {
17
+ throw new Error('createLivePreviewSender requires an explicit targetOrigin (cannot be "*")');
18
+ }
19
+ return {
20
+ sendCss(css) {
21
+ const result = (0, validate_css_1.validateCss)(css, validation);
22
+ if (!result.valid) {
23
+ return result;
24
+ }
25
+ const message = { type: protocol_1.LIVE_PREVIEW_CSS_TYPE, css: result.css };
26
+ targetWindow.postMessage(message, targetOrigin);
27
+ return result;
28
+ },
29
+ };
30
+ }
31
+ //# sourceMappingURL=sender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sender.js","sourceRoot":"","sources":["../../../../../../../libs/public/keypuncherlabs/live-preview/src/lib/sender.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AA0BH,0DAqBC;AA7CD,yCAA+E;AAC/E,iDAAkG;AAuBlG,SAAgB,uBAAuB,CAAC,OAAiC;IACvE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IAE3D,IAAI,CAAC,YAAY,IAAI,YAAY,KAAK,GAAG,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CACb,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IAED,OAAO;QACL,OAAO,CAAC,GAAW;YACjB,MAAM,MAAM,GAAG,IAAA,0BAAW,EAAC,GAAG,EAAE,UAAU,CAAC,CAAC;YAC5C,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;gBAClB,OAAO,MAAM,CAAC;YAChB,CAAC;YAED,MAAM,OAAO,GAA0B,EAAE,IAAI,EAAE,gCAAqB,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;YACxF,YAAY,CAAC,WAAW,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YAChD,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Safety validation for CSS that crosses an origin boundary.
3
+ *
4
+ * This util is meant to be embedded in pages on origins you do not fully
5
+ * control, so the CSS arriving over `postMessage` must be treated as untrusted
6
+ * input. `validateCss` is a pure, DOM-free string check (it runs in Node too)
7
+ * and is applied at every hop — by the sender before posting and again by the
8
+ * receiver/relay before injecting — so a single trusted layer is never assumed.
9
+ *
10
+ * It is intentionally conservative: it rejects rather than tries to "clean" the
11
+ * input. Anything that looks like an attempt to break out of a `<style>` element
12
+ * or pull in active/external content is refused. The companion injection step
13
+ * still uses `textContent` (never `innerHTML`), so even valid CSS cannot inject
14
+ * markup.
15
+ */
16
+ /** Default ceiling on payload size, guarding against memory-exhaustion DoS. */
17
+ export declare const DEFAULT_MAX_CSS_LENGTH = 100000;
18
+ /** URL schemes allowed inside `url(...)` by default (plus relative / `#`). */
19
+ export declare const DEFAULT_ALLOWED_URL_SCHEMES: readonly ["https", "data"];
20
+ export interface CssValidationOptions {
21
+ /** Max allowed length in characters. Defaults to {@link DEFAULT_MAX_CSS_LENGTH}. */
22
+ maxLength?: number;
23
+ /** Allow `@import` rules. Off by default (they trigger external fetches). */
24
+ allowAtImport?: boolean;
25
+ /**
26
+ * Allow any `url(...)` scheme except the always-forbidden ones. Off by
27
+ * default. When off, only {@link allowedUrlSchemes} (plus relative/`#`) pass.
28
+ */
29
+ allowExternalUrls?: boolean;
30
+ /** Permitted `url(...)` schemes when `allowExternalUrls` is off. */
31
+ allowedUrlSchemes?: readonly string[];
32
+ }
33
+ export interface CssValidationResult {
34
+ /** True when the input is safe to inject as-is. */
35
+ valid: boolean;
36
+ /** The original CSS when valid, otherwise an empty string. */
37
+ css: string;
38
+ /** Human-readable reasons the input was rejected (empty when valid). */
39
+ errors: string[];
40
+ }
41
+ /**
42
+ * Validate a CSS string. Never throws; returns a result describing why the
43
+ * input was rejected.
44
+ */
45
+ export declare function validateCss(input: unknown, options?: CssValidationOptions): CssValidationResult;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * Safety validation for CSS that crosses an origin boundary.
4
+ *
5
+ * This util is meant to be embedded in pages on origins you do not fully
6
+ * control, so the CSS arriving over `postMessage` must be treated as untrusted
7
+ * input. `validateCss` is a pure, DOM-free string check (it runs in Node too)
8
+ * and is applied at every hop — by the sender before posting and again by the
9
+ * receiver/relay before injecting — so a single trusted layer is never assumed.
10
+ *
11
+ * It is intentionally conservative: it rejects rather than tries to "clean" the
12
+ * input. Anything that looks like an attempt to break out of a `<style>` element
13
+ * or pull in active/external content is refused. The companion injection step
14
+ * still uses `textContent` (never `innerHTML`), so even valid CSS cannot inject
15
+ * markup.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.DEFAULT_ALLOWED_URL_SCHEMES = exports.DEFAULT_MAX_CSS_LENGTH = void 0;
19
+ exports.validateCss = validateCss;
20
+ /** Default ceiling on payload size, guarding against memory-exhaustion DoS. */
21
+ exports.DEFAULT_MAX_CSS_LENGTH = 100000;
22
+ /** URL schemes allowed inside `url(...)` by default (plus relative / `#`). */
23
+ exports.DEFAULT_ALLOWED_URL_SCHEMES = ['https', 'data'];
24
+ /** Schemes that are never allowed inside `url(...)`, even with opt-outs. */
25
+ const FORBIDDEN_URL_SCHEMES = ['javascript', 'vbscript', 'file'];
26
+ // Matches any ASCII control character except tab (\t \x09), newline (\n \x0a)
27
+ // and carriage return (\r \x0d), which are legitimate whitespace in CSS. Built
28
+ // from escapes so no literal control bytes live in this source file.
29
+ const CONTROL_CHARS = new RegExp('[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]');
30
+ // Active-content / breakout constructs that have no place in injected CSS.
31
+ const FORBIDDEN_PATTERNS = [
32
+ { pattern: /<\/?\s*style/i, message: 'contains a <style> tag (possible breakout)' },
33
+ { pattern: /<\s*script/i, message: 'contains a <script> tag' },
34
+ { pattern: /<\/?\s*[a-z]/i, message: 'contains HTML-like markup' },
35
+ { pattern: /expression\s*\(/i, message: 'contains an expression() call' },
36
+ { pattern: /javascript\s*:/i, message: 'contains a javascript: reference' },
37
+ { pattern: /vbscript\s*:/i, message: 'contains a vbscript: reference' },
38
+ { pattern: /-moz-binding/i, message: 'contains a -moz-binding declaration' },
39
+ { pattern: /behavior\s*:/i, message: 'contains a behavior: declaration' },
40
+ { pattern: CONTROL_CHARS, message: 'contains control characters' },
41
+ ];
42
+ // Captures the scheme of every url(...) target so each can be checked.
43
+ const URL_PATTERN = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi;
44
+ const schemeOf = (target) => {
45
+ const match = /^\s*([a-z][a-z0-9+.-]*)\s*:/i.exec(target);
46
+ return match ? match[1].toLowerCase() : null;
47
+ };
48
+ /**
49
+ * Validate a CSS string. Never throws; returns a result describing why the
50
+ * input was rejected.
51
+ */
52
+ function validateCss(input, options = {}) {
53
+ const errors = [];
54
+ const { maxLength = exports.DEFAULT_MAX_CSS_LENGTH, allowAtImport = false, allowExternalUrls = false, allowedUrlSchemes = exports.DEFAULT_ALLOWED_URL_SCHEMES, } = options;
55
+ if (typeof input !== 'string') {
56
+ return { valid: false, css: '', errors: ['css must be a string'] };
57
+ }
58
+ if (input.length > maxLength) {
59
+ errors.push(`css exceeds the maximum length of ${maxLength} characters`);
60
+ }
61
+ for (const { pattern, message } of FORBIDDEN_PATTERNS) {
62
+ if (pattern.test(input)) {
63
+ errors.push(`css ${message}`);
64
+ }
65
+ }
66
+ if (!allowAtImport && /@import\b/i.test(input)) {
67
+ errors.push('css contains an @import rule');
68
+ }
69
+ if (!allowExternalUrls) {
70
+ const allowed = allowedUrlSchemes.map((scheme) => scheme.toLowerCase());
71
+ for (const match of input.matchAll(URL_PATTERN)) {
72
+ const scheme = schemeOf(match[2]);
73
+ // No scheme => relative URL or fragment, which is fine.
74
+ if (scheme !== null && !allowed.includes(scheme)) {
75
+ errors.push(`css contains a disallowed url() scheme: ${scheme}:`);
76
+ }
77
+ }
78
+ }
79
+ else {
80
+ for (const match of input.matchAll(URL_PATTERN)) {
81
+ const scheme = schemeOf(match[2]);
82
+ if (scheme !== null && FORBIDDEN_URL_SCHEMES.includes(scheme)) {
83
+ errors.push(`css contains a forbidden url() scheme: ${scheme}:`);
84
+ }
85
+ }
86
+ }
87
+ if (errors.length > 0) {
88
+ return { valid: false, css: '', errors };
89
+ }
90
+ return { valid: true, css: input, errors: [] };
91
+ }
92
+ //# sourceMappingURL=validate-css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-css.js","sourceRoot":"","sources":["../../../../../../../libs/public/keypuncherlabs/live-preview/src/lib/validate-css.ts"],"names":[],"mappings":"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"}