@preprio/toolkit 0.1.0-beta.3 → 0.1.0-beta.5

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/index.cjs CHANGED
@@ -43,7 +43,7 @@ __export(src_exports, {
43
43
  module.exports = __toCommonJS(src_exports);
44
44
 
45
45
  // src/version.ts
46
- var VERSION = "0.1.0-beta.3";
46
+ var VERSION = "0.1.0-beta.5";
47
47
 
48
48
  // src/core/constants.ts
49
49
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -67,16 +67,34 @@ function getCookie(name) {
67
67
  }
68
68
  function setCookie(name, value, options = {}) {
69
69
  if (typeof document === "undefined") return;
70
- const { maxAge, path = "/" } = options;
70
+ const { maxAge, path = "/", sameSite, secure } = options;
71
71
  let cookie = `${name}=${encodeURIComponent(value)};path=${path}`;
72
72
  if (typeof maxAge === "number") {
73
73
  cookie += `;max-age=${maxAge}`;
74
74
  }
75
+ if (sameSite) {
76
+ cookie += `;samesite=${sameSite}`;
77
+ }
78
+ if (secure) {
79
+ cookie += ";secure";
80
+ }
75
81
  document.cookie = cookie;
76
82
  }
77
- function removeCookie(name, path = "/") {
83
+ function removeCookie(name, path = "/", options = {}) {
78
84
  if (typeof document === "undefined") return;
79
- document.cookie = `${name}=;path=${path};expires=Thu, 01 Jan 1970 00:00:00 GMT`;
85
+ const { sameSite, secure } = options;
86
+ let cookie = `${name}=;path=${path};expires=Thu, 01 Jan 1970 00:00:00 GMT`;
87
+ if (sameSite) {
88
+ cookie += `;samesite=${sameSite}`;
89
+ }
90
+ if (secure) {
91
+ cookie += ";secure";
92
+ }
93
+ document.cookie = cookie;
94
+ }
95
+ function crossSiteCookieOptions() {
96
+ if (typeof window === "undefined" || !window.isSecureContext) return {};
97
+ return { sameSite: "None", secure: true };
80
98
  }
81
99
 
82
100
  // src/core/locales/en.json
@@ -170,7 +188,11 @@ function t(key, locale, vars) {
170
188
  }
171
189
 
172
190
  // src/core/utils.ts
173
- function sendPreprEvent(event, data) {
191
+ var trustedParentOrigin = null;
192
+ function setTrustedParentOrigin(origin) {
193
+ trustedParentOrigin = origin;
194
+ }
195
+ function sendPreprEvent(event, data, options) {
174
196
  if (typeof window !== "undefined") {
175
197
  const message = {
176
198
  name: "prepr_preview_bar",
@@ -181,7 +203,11 @@ function sendPreprEvent(event, data) {
181
203
  new CustomEvent("prepr_preview_bar", { detail: message })
182
204
  );
183
205
  if (window.parent && window.parent !== window) {
184
- window.parent.postMessage(message, "*");
206
+ if (trustedParentOrigin) {
207
+ window.parent.postMessage(message, trustedParentOrigin);
208
+ } else if (options?.allowUntrustedTarget) {
209
+ window.parent.postMessage(message, "*");
210
+ }
185
211
  }
186
212
  }
187
213
  }
@@ -267,7 +293,23 @@ function createElementCache(query, ttl = 1e3) {
267
293
  }
268
294
 
269
295
  // src/core/iframe-bridge.ts
270
- function createIframeBridge(store) {
296
+ var EDITOR_BASE_DOMAIN = "prepr.io";
297
+ function isAllowedEditorOrigin(origin, allowed) {
298
+ if (allowed) return allowed.includes(origin);
299
+ let url;
300
+ try {
301
+ url = new URL(origin);
302
+ } catch {
303
+ return false;
304
+ }
305
+ if (url.protocol !== "https:" || url.port !== "") return false;
306
+ const suffix = `.${EDITOR_BASE_DOMAIN}`;
307
+ if (!url.hostname.endsWith(suffix)) return false;
308
+ const label = url.hostname.slice(0, -suffix.length);
309
+ return label.length > 0 && !label.includes(".");
310
+ }
311
+ function createIframeBridge(store, options = {}) {
312
+ const allowedOrigins = options.allowedEditorOrigins;
271
313
  let parentOrigin = null;
272
314
  const onKeyDown = (event) => {
273
315
  const key = event.key.toLowerCase();
@@ -277,14 +319,16 @@ function createIframeBridge(store) {
277
319
  const onMessage = (evt) => {
278
320
  const data = evt?.data;
279
321
  if (data?.event === "prepr:initVE" && !parentOrigin) {
322
+ if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) return;
280
323
  parentOrigin = evt.origin;
324
+ setTrustedParentOrigin(parentOrigin);
281
325
  if (data.scrollPosition != null) {
282
326
  const top = data.scrollPosition;
283
327
  setTimeout(() => window.scrollTo(0, top), 1);
284
328
  }
285
329
  store.set({ previewMode: true, editMode: data.editMode ?? true });
286
330
  }
287
- if (evt.origin !== parentOrigin) return;
331
+ if (!parentOrigin || evt.origin !== parentOrigin) return;
288
332
  if (data?.event === "prepr:getScrollPosition") {
289
333
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
290
334
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
@@ -292,13 +336,15 @@ function createIframeBridge(store) {
292
336
  };
293
337
  return {
294
338
  start() {
295
- sendPreprEvent("loaded");
339
+ sendPreprEvent("loaded", void 0, { allowUntrustedTarget: true });
296
340
  window.addEventListener("keydown", onKeyDown);
297
341
  window.addEventListener("message", onMessage);
298
342
  },
299
343
  stop() {
300
344
  window.removeEventListener("keydown", onKeyDown);
301
345
  window.removeEventListener("message", onMessage);
346
+ parentOrigin = null;
347
+ setTrustedParentOrigin(null);
302
348
  }
303
349
  };
304
350
  }
@@ -933,43 +979,55 @@ function createToolbarStore(initial = {}) {
933
979
  }
934
980
 
935
981
  // src/core/toolbar-change-handler.ts
936
- var COOKIE_OPTS = { maxAge: ONE_YEAR_SECONDS, path: "/" };
982
+ function cookieOpts() {
983
+ return {
984
+ maxAge: ONE_YEAR_SECONDS,
985
+ path: "/",
986
+ ...crossSiteCookieOptions()
987
+ };
988
+ }
937
989
  function createChangeHandler(deps) {
938
990
  const { store, stega, syncAutoClean } = deps;
939
- function updateParam(name, value) {
991
+ function updateParams(patch) {
940
992
  const [path, existing] = splitPath(deps.currentPath());
941
993
  const params = new URLSearchParams(existing);
942
- if (value === null) {
943
- params.delete(name);
944
- } else {
945
- params.set(name, value);
994
+ for (const [name, value] of Object.entries(patch)) {
995
+ if (value === null) {
996
+ params.delete(name);
997
+ } else {
998
+ params.set(name, value);
999
+ }
946
1000
  }
947
1001
  const query = params.toString();
948
1002
  deps.navigate(query ? `${path}?${query}` : path);
949
1003
  }
950
1004
  return function handleChange(before, after) {
1005
+ const paramPatch = {};
951
1006
  if (before.selectedSegment !== after.selectedSegment) {
952
1007
  if (after.selectedSegment === null) {
953
- removeCookie(COOKIE_SEGMENT);
1008
+ removeCookie(COOKIE_SEGMENT, "/", crossSiteCookieOptions());
954
1009
  } else {
955
- setCookie(COOKIE_SEGMENT, after.selectedSegment, COOKIE_OPTS);
1010
+ setCookie(COOKIE_SEGMENT, after.selectedSegment, cookieOpts());
956
1011
  }
957
- updateParam(PARAM_SEGMENT, after.selectedSegment);
1012
+ paramPatch[PARAM_SEGMENT] = after.selectedSegment;
958
1013
  sendPreprEvent("segment_changed", {
959
1014
  segment: after.selectedSegment ?? void 0
960
1015
  });
961
1016
  }
962
1017
  if (before.selectedVariant !== after.selectedVariant) {
963
1018
  if (after.selectedVariant === null) {
964
- removeCookie(COOKIE_VARIANT);
1019
+ removeCookie(COOKIE_VARIANT, "/", crossSiteCookieOptions());
965
1020
  } else {
966
- setCookie(COOKIE_VARIANT, after.selectedVariant, COOKIE_OPTS);
1021
+ setCookie(COOKIE_VARIANT, after.selectedVariant, cookieOpts());
967
1022
  }
968
- updateParam(PARAM_VARIANT, after.selectedVariant);
1023
+ paramPatch[PARAM_VARIANT] = after.selectedVariant;
969
1024
  sendPreprEvent("variant_changed", {
970
1025
  variant: after.selectedVariant ?? void 0
971
1026
  });
972
1027
  }
1028
+ if (Object.keys(paramPatch).length > 0) {
1029
+ updateParams(paramPatch);
1030
+ }
973
1031
  if (before.editMode !== after.editMode) {
974
1032
  if (after.editMode) {
975
1033
  stega.start();
@@ -989,8 +1047,8 @@ function createChangeHandler(deps) {
989
1047
  if (Object.keys(coupled).length > 0) {
990
1048
  store.set(coupled);
991
1049
  }
992
- setCookie(COOKIE_PREVIEW_MODE, String(after.previewMode), COOKIE_OPTS);
993
- setCookie(COOKIE_TOOLBAR_OPEN, "false", COOKIE_OPTS);
1050
+ setCookie(COOKIE_PREVIEW_MODE, String(after.previewMode), cookieOpts());
1051
+ setCookie(COOKIE_TOOLBAR_OPEN, "false", cookieOpts());
994
1052
  sendPreprEvent("preview_mode_toggled", { previewMode: after.previewMode });
995
1053
  syncAutoClean(after.previewMode);
996
1054
  const [path, existing] = splitPath(deps.currentPath());
@@ -1005,7 +1063,7 @@ function createChangeHandler(deps) {
1005
1063
  }
1006
1064
  }
1007
1065
  if (before.toolbarOpen !== after.toolbarOpen) {
1008
- setCookie(COOKIE_TOOLBAR_OPEN, String(after.toolbarOpen), COOKIE_OPTS);
1066
+ setCookie(COOKIE_TOOLBAR_OPEN, String(after.toolbarOpen), cookieOpts());
1009
1067
  }
1010
1068
  };
1011
1069
  }
@@ -2075,7 +2133,9 @@ function createPreprToolbar(opts) {
2075
2133
  });
2076
2134
  syncAutoClean(store.get().previewMode);
2077
2135
  const bridge = createIframeBridge(store);
2078
- sendPreprEvent("getScrollPosition", { value: 0 });
2136
+ sendPreprEvent("getScrollPosition", { value: 0 }, {
2137
+ allowUntrustedTarget: true
2138
+ });
2079
2139
  if (isIframe) {
2080
2140
  bridge.start();
2081
2141
  }
@@ -2271,7 +2331,11 @@ function processPreprRequest(request, opts) {
2271
2331
  const requestHeaders = new Headers(request.headers);
2272
2332
  const responseCookies = [];
2273
2333
  const requestCookies = parseCookies(request);
2274
- const searchParams = new URL(request.url).searchParams;
2334
+ const url = new URL(request.url);
2335
+ const searchParams = url.searchParams;
2336
+ const forwardedProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
2337
+ const isSecure = forwardedProto ? forwardedProto === "https" : url.protocol === "https:";
2338
+ const crossSite = isSecure ? { sameSite: "None", secure: true } : {};
2275
2339
  for (const key of UTM_PARAMS) {
2276
2340
  const value = searchParams.get(key);
2277
2341
  if (value !== null) {
@@ -2320,6 +2384,15 @@ function processPreprRequest(request, opts) {
2320
2384
  }
2321
2385
  requestHeaders.set("Prepr-Preview-Bar", "true");
2322
2386
  const isLivePreview = searchParams.get(PARAM_HIDE_BAR) === "true";
2387
+ if (!isLivePreview && !requestCookies.has(COOKIE_PREVIEW_MODE)) {
2388
+ responseCookies.push({
2389
+ name: COOKIE_PREVIEW_MODE,
2390
+ value: "true",
2391
+ maxAge: ONE_YEAR_SECONDS,
2392
+ path: "/",
2393
+ ...crossSite
2394
+ });
2395
+ }
2323
2396
  if (!isLivePreview) {
2324
2397
  const segmentCookie = requestCookies.get(COOKIE_SEGMENT);
2325
2398
  if (segmentCookie) {
@@ -2338,7 +2411,8 @@ function processPreprRequest(request, opts) {
2338
2411
  name: COOKIE_VARIANT,
2339
2412
  value: previewAb,
2340
2413
  maxAge: ONE_YEAR_SECONDS,
2341
- path: "/"
2414
+ path: "/",
2415
+ ...crossSite
2342
2416
  });
2343
2417
  }
2344
2418
  }
@@ -2350,7 +2424,8 @@ function processPreprRequest(request, opts) {
2350
2424
  name: COOKIE_SEGMENT,
2351
2425
  value: previewSegment,
2352
2426
  maxAge: ONE_YEAR_SECONDS,
2353
- path: "/"
2427
+ path: "/",
2428
+ ...crossSite
2354
2429
  });
2355
2430
  }
2356
2431
  }
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ export { f as PreprEnvironment, g as PreprEventType, h as PreprHeaderName, i as
5
5
  * Package version; must match package.json. Lives in its own leaf module so
6
6
  * core can import it without cycling through the barrel.
7
7
  */
8
- declare const VERSION = "0.1.0-beta.3";
8
+ declare const VERSION = "0.1.0-beta.5";
9
9
 
10
10
  interface ToolbarState {
11
11
  locale: Locale;
@@ -160,6 +160,15 @@ interface CookieSpec {
160
160
  value: string;
161
161
  maxAge: number;
162
162
  path: string;
163
+ /**
164
+ * Set to `'None'` for cookies that must survive the Prepr editor's
165
+ * cross-site preview iframe — browsers drop the browser-default `Lax`
166
+ * cookies there. Always paired with `secure`, which browsers require for
167
+ * `SameSite=None`.
168
+ */
169
+ sameSite?: 'Strict' | 'Lax' | 'None';
170
+ /** Adds `Secure`. Required by browsers alongside `SameSite=None`. */
171
+ secure?: boolean;
163
172
  }
164
173
  interface PreprMiddlewareResult {
165
174
  requestHeaders: Headers;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { f as PreprEnvironment, g as PreprEventType, h as PreprHeaderName, i as
5
5
  * Package version; must match package.json. Lives in its own leaf module so
6
6
  * core can import it without cycling through the barrel.
7
7
  */
8
- declare const VERSION = "0.1.0-beta.3";
8
+ declare const VERSION = "0.1.0-beta.5";
9
9
 
10
10
  interface ToolbarState {
11
11
  locale: Locale;
@@ -160,6 +160,15 @@ interface CookieSpec {
160
160
  value: string;
161
161
  maxAge: number;
162
162
  path: string;
163
+ /**
164
+ * Set to `'None'` for cookies that must survive the Prepr editor's
165
+ * cross-site preview iframe — browsers drop the browser-default `Lax`
166
+ * cookies there. Always paired with `secure`, which browsers require for
167
+ * `SameSite=None`.
168
+ */
169
+ sameSite?: 'Strict' | 'Lax' | 'None';
170
+ /** Adds `Secure`. Required by browsers alongside `SameSite=None`. */
171
+ secure?: boolean;
163
172
  }
164
173
  interface PreprMiddlewareResult {
165
174
  requestHeaders: Headers;
package/dist/index.js CHANGED
@@ -16,10 +16,10 @@ import {
16
16
  setTrackingParam,
17
17
  stegaClean,
18
18
  trackEvent
19
- } from "./chunk-5JGPWQVA.js";
19
+ } from "./chunk-NLQIRN5O.js";
20
20
 
21
21
  // src/version.ts
22
- var VERSION = "0.1.0-beta.3";
22
+ var VERSION = "0.1.0-beta.5";
23
23
 
24
24
  // src/core/server.ts
25
25
  var PreprError = class extends Error {
@@ -198,7 +198,11 @@ function processPreprRequest(request, opts) {
198
198
  const requestHeaders = new Headers(request.headers);
199
199
  const responseCookies = [];
200
200
  const requestCookies = parseCookies(request);
201
- const searchParams = new URL(request.url).searchParams;
201
+ const url = new URL(request.url);
202
+ const searchParams = url.searchParams;
203
+ const forwardedProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
204
+ const isSecure = forwardedProto ? forwardedProto === "https" : url.protocol === "https:";
205
+ const crossSite = isSecure ? { sameSite: "None", secure: true } : {};
202
206
  for (const key of UTM_PARAMS) {
203
207
  const value = searchParams.get(key);
204
208
  if (value !== null) {
@@ -247,6 +251,15 @@ function processPreprRequest(request, opts) {
247
251
  }
248
252
  requestHeaders.set("Prepr-Preview-Bar", "true");
249
253
  const isLivePreview = searchParams.get(PARAM_HIDE_BAR) === "true";
254
+ if (!isLivePreview && !requestCookies.has(COOKIE_PREVIEW_MODE)) {
255
+ responseCookies.push({
256
+ name: COOKIE_PREVIEW_MODE,
257
+ value: "true",
258
+ maxAge: ONE_YEAR_SECONDS,
259
+ path: "/",
260
+ ...crossSite
261
+ });
262
+ }
250
263
  if (!isLivePreview) {
251
264
  const segmentCookie = requestCookies.get(COOKIE_SEGMENT);
252
265
  if (segmentCookie) {
@@ -265,7 +278,8 @@ function processPreprRequest(request, opts) {
265
278
  name: COOKIE_VARIANT,
266
279
  value: previewAb,
267
280
  maxAge: ONE_YEAR_SECONDS,
268
- path: "/"
281
+ path: "/",
282
+ ...crossSite
269
283
  });
270
284
  }
271
285
  }
@@ -277,7 +291,8 @@ function processPreprRequest(request, opts) {
277
291
  name: COOKIE_SEGMENT,
278
292
  value: previewSegment,
279
293
  maxAge: ONE_YEAR_SECONDS,
280
- path: "/"
294
+ path: "/",
295
+ ...crossSite
281
296
  });
282
297
  }
283
298
  }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/core/json-script.ts
21
+ var json_script_exports = {};
22
+ __export(json_script_exports, {
23
+ serializeForScriptTag: () => serializeForScriptTag
24
+ });
25
+ module.exports = __toCommonJS(json_script_exports);
26
+ function serializeForScriptTag(value) {
27
+ return JSON.stringify(value ?? null).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
28
+ }
29
+ // Annotate the CommonJS export names for ESM import in node:
30
+ 0 && (module.exports = {
31
+ serializeForScriptTag
32
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Serialize a value for embedding inside `<script type="application/json">`.
3
+ *
4
+ * `JSON.stringify` does not escape `<`, `>` or `&`, so a string value
5
+ * containing `</script>` would close the tag early and let the remainder parse
6
+ * as HTML — stored XSS whenever any part of the payload is author-controlled
7
+ * (CMS segment names reach these components verbatim).
8
+ *
9
+ * Escaping `<` and `>` as `\uXXXX` keeps the output valid JSON that parses back
10
+ * to the identical value, while making an early tag close impossible. `&` is
11
+ * escaped too so the payload is also safe inside HTML-escaping contexts, and
12
+ * U+2028/U+2029 because they are literal line terminators in JS source but
13
+ * legal unescaped inside a JSON string.
14
+ */
15
+ declare function serializeForScriptTag(value: unknown): string;
16
+
17
+ export { serializeForScriptTag };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Serialize a value for embedding inside `<script type="application/json">`.
3
+ *
4
+ * `JSON.stringify` does not escape `<`, `>` or `&`, so a string value
5
+ * containing `</script>` would close the tag early and let the remainder parse
6
+ * as HTML — stored XSS whenever any part of the payload is author-controlled
7
+ * (CMS segment names reach these components verbatim).
8
+ *
9
+ * Escaping `<` and `>` as `\uXXXX` keeps the output valid JSON that parses back
10
+ * to the identical value, while making an early tag close impossible. `&` is
11
+ * escaped too so the payload is also safe inside HTML-escaping contexts, and
12
+ * U+2028/U+2029 because they are literal line terminators in JS source but
13
+ * legal unescaped inside a JSON string.
14
+ */
15
+ declare function serializeForScriptTag(value: unknown): string;
16
+
17
+ export { serializeForScriptTag };
@@ -0,0 +1,7 @@
1
+ // src/core/json-script.ts
2
+ function serializeForScriptTag(value) {
3
+ return JSON.stringify(value ?? null).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
4
+ }
5
+ export {
6
+ serializeForScriptTag
7
+ };