@joryio/web-sdk 1.3.0 → 1.3.1-beta.1

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/dist/joryio.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Joryio Web SDK v1.3.0
2
+ * Joryio Web SDK v1.3.1-beta.1
3
3
  * (c) 2026 Joryio. All rights reserved.
4
4
  * @license Commercial - see https://github.com/joryio/joryio-web-sdk/blob/main/LICENSE
5
5
  */
6
- const SDK_VERSION = '1.3.0';
6
+ const SDK_VERSION = '1.3.1';
7
7
  const DEFAULT_API_ENDPOINT = 'https://api-eu1.joryio.com/api';
8
8
  function normalizeApiEndpoint(raw) {
9
9
  const trimmed = (raw || '').trim();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Joryio Web SDK v1.3.0
2
+ * Joryio Web SDK v1.3.1-beta.1
3
3
  * (c) 2026 Joryio. All rights reserved.
4
4
  * @license Commercial - see https://github.com/joryio/joryio-web-sdk/blob/main/LICENSE
5
5
  */
@@ -9,7 +9,7 @@
9
9
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JoryioSDK = {}));
10
10
  })(this, (function (exports) { 'use strict';
11
11
 
12
- const SDK_VERSION = '1.3.0';
12
+ const SDK_VERSION = '1.3.1';
13
13
  const DEFAULT_API_ENDPOINT = 'https://api-eu1.joryio.com/api';
14
14
  function normalizeApiEndpoint(raw) {
15
15
  const trimmed = (raw || '').trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joryio/web-sdk",
3
- "version": "1.3.0",
3
+ "version": "1.3.1-beta.1",
4
4
  "description": "Joryio Web Tracking SDK - Lightweight analytics and event tracking",
5
5
  "main": "dist/joryio.cjs.js",
6
6
  "module": "dist/joryio.mjs",
@@ -2,7 +2,7 @@
2
2
  * SDK Configuration Types and Defaults
3
3
  */
4
4
 
5
- export const SDK_VERSION = '1.3.0'; // keep in sync with package.json — the backend serves this in X-SDK-Version headers
5
+ export const SDK_VERSION = '1.3.1'; // keep in sync with package.json — the backend serves this in X-SDK-Version headers
6
6
  export const DEFAULT_API_ENDPOINT = 'https://api-eu1.joryio.com/api';
7
7
 
8
8
  /**
@@ -88,17 +88,20 @@ export interface InAppOptions {
88
88
  *
89
89
  * What actually happens, because the name alone cannot carry it:
90
90
  *
91
- * - The server strips `<script>` and every `on*` handler from the authored
92
- * HTML before it is sent, so author JavaScript does NOT run in normal
93
- * operation.
94
- * - The message is still rendered in an iframe with `allow-scripts
95
- * allow-same-origin`, because this SDK's own bootstrap runs there (height
91
+ * - The server keeps the authored HTML as written (only `<base>` and
92
+ * `<meta http-equiv="refresh">` are removed), so `<script>` and `on*`
93
+ * handlers reach the browser - and do NOT run: the frame carries a fresh
94
+ * nonce Content-Security-Policy per message, and only the SDK's bootstrap
95
+ * has the nonce. Buttons and forms work through that bootstrap
96
+ * (`data-joryio-action`, `data-jry-form`), not through author script.
97
+ * - The message is rendered in an iframe with `allow-scripts
98
+ * allow-same-origin`, because the bootstrap runs there (height
96
99
  * reporting, click logging, the host bridge). That frame therefore CAN
97
100
  * execute script, at your page's origin.
98
- * - So the residual risk is a sanitiser bypass: if one were ever found,
99
- * injected script would run with your origin's privileges. That is why
100
- * this is opt-in and why the name keeps `Js` - `allowHtmlInAppMessages`
101
- * alone would read like a formatting preference.
101
+ * - So the residual risk is a gap in the CSP (a browser that does not
102
+ * enforce it): author script would then run with your origin's
103
+ * privileges. That is why this is opt-in and why the name keeps `Js` -
104
+ * `allowHtmlInAppMessages` alone would read like a formatting preference.
102
105
  *
103
106
  * Leaving it off does not disable in-app messaging: NATIVE messages still
104
107
  * display, because they are data rendered into DOM text nodes with no
package/src/core/inapp.ts CHANGED
@@ -381,10 +381,13 @@ export class InAppMessagingManager {
381
381
  private iframes: Map<string, HTMLIFrameElement> = new Map();
382
382
 
383
383
  /**
384
- * Whether HTML in-app messages may run. Off by default, because an HTML
385
- * message executes author-supplied JavaScript on the host page - which is
386
- * exactly why it is rendered in a sandboxed iframe. Native messages are
387
- * unaffected: they are data rendered as DOM, with no interpreter.
384
+ * Whether HTML in-app messages are displayed. Off by default. The markup
385
+ * is kept as written by the server, and author scripts in it do NOT run:
386
+ * the frame's per-message nonce CSP (buildSrcdoc) lets only the SDK's own
387
+ * bridge bootstrap execute. But that frame can execute script at the host
388
+ * page's origin, so a gap in the CSP would be XSS - hence opt-in. Native
389
+ * messages are unaffected: they are data rendered as DOM, with no
390
+ * interpreter.
388
391
  */
389
392
  private allowHtmlJsInAppMessages = false;
390
393
 
@@ -1203,8 +1206,9 @@ export class InAppMessagingManager {
1203
1206
 
1204
1207
  if (kind === 'html') {
1205
1208
  if (!renderedHtml.trim()) return 'its `content.html` is empty';
1206
- // A SKIP, not a failure: HTML runs author-supplied JavaScript on the host
1207
- // page, so an integration that has not opted in never creates the iframe.
1209
+ // A SKIP, not a failure: HTML is shown in a frame that can execute script
1210
+ // at the host page's origin (only the SDK bridge, under the nonce CSP),
1211
+ // so an integration that has not opted in never creates the iframe.
1208
1212
  if (!this.allowHtmlJsInAppMessages) {
1209
1213
  return 'it is an HTML message and allowHtmlJsInAppMessages is false';
1210
1214
  }
@@ -1459,11 +1463,14 @@ export class InAppMessagingManager {
1459
1463
  // Render the message HTML inside a SAME-ORIGIN SANDBOXED iframe. The sandbox
1460
1464
  // (allow-scripts allow-same-origin) is the isolation/encapsulation boundary
1461
1465
  // — message CSS can't leak to the host and host CSS can't bleed in — and it
1462
- // lets the message's own JS run AND reach `joryioBridge`. Same-origin is
1463
- // required so `joryioBridge.requestPushPermission()` keeps the click's
1466
+ // lets the SDK's bridge bootstrap run AND reach `joryioBridge`. Same-origin
1467
+ // is required so `joryioBridge.requestPushPermission()` keeps the click's
1464
1468
  // user-gesture activation (a cross-origin postMessage hop would lose it).
1465
- // Content is first-party (authored in-dashboard + sanitized server-side on
1466
- // save); the sandbox is defense-in-depth, not the only line.
1469
+ // Author scripts and on* handlers do NOT run: the server keeps the markup
1470
+ // as written (sanitizeInAppHtml drops only <base> and meta refresh), and
1471
+ // the srcdoc's nonce CSP (buildSrcdoc) is what stops them. The sandbox and
1472
+ // the navigation rules contain the rest; there is no server sanitiser in
1473
+ // front of this.
1467
1474
  const iframe = document.createElement('iframe');
1468
1475
  iframe.id = `joryio-iframe-${campaign.id}`;
1469
1476
  // ── Native content: build DOM directly, no iframe ───────────────────────
@@ -1649,9 +1656,9 @@ export class InAppMessagingManager {
1649
1656
  let isDismissing = false;
1650
1657
 
1651
1658
  if (campaign.content?.kind !== 'native') {
1652
- // HTML runs author-supplied JavaScript on this page - which is why the
1653
- // iframe exists at all. An integration that has not opted in never creates
1654
- // one. This is a SKIP, not a failure: native messages still display,
1659
+ // HTML is shown in an iframe that can execute script at this page's
1660
+ // origin (only the SDK bridge - the nonce CSP blocks author script). An
1661
+ // integration that has not opted in never creates one. This is a SKIP, not a failure: native messages still display,
1655
1662
  // because they are data rendered as DOM with no interpreter.
1656
1663
  if (!this.allowHtmlJsInAppMessages) {
1657
1664
  this.logger.warn(
@@ -1661,7 +1668,8 @@ export class InAppMessagingManager {
1661
1668
  return;
1662
1669
  }
1663
1670
 
1664
- // allow-scripts/allow-same-origin: run the message JS + reach the bridge.
1671
+ // allow-scripts/allow-same-origin: run the SDK bridge bootstrap (the
1672
+ // nonce CSP blocks every other script) + reach the host bridge.
1665
1673
  // allow-popups: a CTA may open a new tab. NOT allow-top-navigation: every
1666
1674
  // link and redirect goes through the bridge's `navigate`, which the HOST
1667
1675
  // page carries out (rule 57) - the message document itself may never
@@ -1800,8 +1808,9 @@ export class InAppMessagingManager {
1800
1808
  * event name, received `undefined`, and the host guard dropped it - a control
1801
1809
  * that looked like it worked and logged nothing. Verbs taking two arguments
1802
1810
  * (`navigate`) still cannot be driven declaratively.
1803
- * NOT sanitized here — the sandbox is the boundary and the HTML is first-party
1804
- * (dashboard-authored + sanitized server-side on save). campaignId + the host
1811
+ * NOT sanitized here, nor by the server beyond <base> and meta refresh: the
1812
+ * nonce CSP below is what keeps author script from running, and the sandbox
1813
+ * and navigation rules contain the document. campaignId + the host
1805
1814
  * key are injected as JSON literals (our own values).
1806
1815
  */
1807
1816
  private buildSrcdoc(
@@ -1880,22 +1889,20 @@ export class InAppMessagingManager {
1880
1889
  /*
1881
1890
  * A per-document CSP, and the nonce is the point.
1882
1891
  *
1883
- * The server strips <script> and every on* handler before this HTML is
1884
- * sent, so no author JavaScript runs today. The residual risk is a
1885
- * SANITISER BYPASS - mXSS in HTML sanitisers is a recurring class - and
1886
- * because this iframe is `allow-scripts allow-same-origin`, injected script
1887
- * would run at the HOST PAGE'S ORIGIN. That is XSS on a customer's site,
1888
- * reachable from a compromised marketing account.
1892
+ * The server keeps the author's markup as written (sanitizeInAppHtml drops
1893
+ * only <base> and meta refresh), so author <script> blocks and on*
1894
+ * handlers ARE in this document. Because this iframe is `allow-scripts
1895
+ * allow-same-origin`, they would run at the HOST PAGE'S ORIGIN - XSS on a
1896
+ * customer's site, reachable from a compromised marketing account.
1889
1897
  *
1890
- * `script-src 'nonce-...'` closes that: only the bootstrap below carries
1891
- * the nonce, so anything that survives sanitisation still cannot execute.
1892
- * Two independent controls instead of one, which matters when the first is
1893
- * a parser fighting an adversary.
1898
+ * `script-src 'nonce-...'` is the control: only the bootstrap below carries
1899
+ * the nonce, so nothing the author wrote executes. There is no server
1900
+ * sanitiser in front of it; this CSP is the first line, not a backstop.
1894
1901
  *
1895
1902
  * The rest is least-privilege for content that is only ever a message:
1896
1903
  * default-src 'none' nothing is allowed unless named below
1897
- * style-src unsafe-inline the sanitiser ALLOWS style="" attributes, so
1898
- * this must stay permissive; styles cannot execute
1904
+ * style-src unsafe-inline author style="" attributes are kept, so this
1905
+ * must stay permissive; styles cannot execute
1899
1906
  * img/media/font https: data: no http subresources - the mixed-content
1900
1907
  * control Android enforced and iOS could not
1901
1908
  * upgrade-insecure-requests an author's http image is upgraded rather