@backmatter/next 0.0.35 → 0.0.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/next",
3
- "version": "0.0.35",
3
+ "version": "0.0.36",
4
4
  "description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -20,7 +20,12 @@ import { MARKER_COOKIE, MARKER_TTL_S, safeReturnPath } from "./gate";
20
20
  import { requestUrlFrom } from "./request-url";
21
21
 
22
22
  export type PreviewRouteDeps = {
23
- enableDraft?: () => void | Promise<void>;
23
+ /** Returns Next's preview bypass id when draft was just enabled. */
24
+ enableDraft?: () =>
25
+ | void
26
+ | string
27
+ | undefined
28
+ | Promise<void | string | undefined>;
24
29
  disableDraft?: () => void | Promise<void>;
25
30
  isDraftEnabled?: () => boolean | Promise<boolean>;
26
31
  now?: () => number;
@@ -51,6 +56,44 @@ function markerCookieHeader(secure: boolean, on: boolean): string {
51
56
  return parts.join("; ");
52
57
  }
53
58
 
59
+ /** Next's static-bypass cookie. Same name Next sets; Partitioned so a cross-site frame keeps it. */
60
+ const DRAFT_BYPASS_COOKIE = "__prerender_bypass";
61
+
62
+ function partitionedDraftBypass(value: string): string | null {
63
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
64
+ return [
65
+ `${DRAFT_BYPASS_COOKIE}=${value}`,
66
+ "Path=/",
67
+ "HttpOnly",
68
+ "Secure",
69
+ "SameSite=None",
70
+ "Partitioned",
71
+ ].join("; ");
72
+ }
73
+
74
+ function clearPartitionedDraftBypass(): string {
75
+ return [
76
+ `${DRAFT_BYPASS_COOKIE}=`,
77
+ "Path=/",
78
+ "HttpOnly",
79
+ "Secure",
80
+ "SameSite=None",
81
+ "Partitioned",
82
+ "Max-Age=0",
83
+ ].join("; ");
84
+ }
85
+
86
+ function appendPartitionedBypass(headers: Headers, secure: boolean, bypass: unknown) {
87
+ if (!secure || typeof bypass !== "string") return;
88
+ const cookie = partitionedDraftBypass(bypass);
89
+ if (cookie) headers.append("Set-Cookie", cookie);
90
+ }
91
+
92
+ function appendClearPartitionedBypass(headers: Headers, secure: boolean) {
93
+ if (!secure) return;
94
+ headers.append("Set-Cookie", clearPartitionedDraftBypass());
95
+ }
96
+
54
97
  function cookieBase(name: string, secure: boolean, maxAge: number, value: string) {
55
98
  // HTTPS is cross-site from the CMS iframe (dash → customer origin). Lax is
56
99
  // not sent there, so draftMode can be on while the checkout cookie is missing
@@ -69,13 +112,21 @@ function cookieBase(name: string, secure: boolean, maxAge: number, value: string
69
112
  return parts.join("; ");
70
113
  }
71
114
 
72
- async function nextDraft(enable: boolean): Promise<void> {
115
+ async function nextDraft(enable: boolean): Promise<string | undefined> {
116
+ // Same jar as draft.enable(): route phase is "action", so cookies()
117
+ // reads requestStore.mutableCookies. Do not cookies().set this name;
118
+ // the jar is keyed by name and a second set would replace Next's cookie.
73
119
  const mod = (await import("next/headers" as string)) as {
74
120
  draftMode: () => Promise<{ enable: () => void; disable: () => void }>;
121
+ cookies: () => Promise<{ get: (name: string) => { value: string } | undefined }>;
75
122
  };
76
123
  const draft = await mod.draftMode();
77
- if (enable) draft.enable();
78
- else draft.disable();
124
+ if (!enable) {
125
+ draft.disable();
126
+ return undefined;
127
+ }
128
+ draft.enable();
129
+ return (await mod.cookies()).get(DRAFT_BYPASS_COOKIE)?.value;
79
130
  }
80
131
 
81
132
  async function nextDraftEnabled(): Promise<boolean> {
@@ -161,6 +212,7 @@ export function createPreviewRoute(
161
212
  headers.append("Set-Cookie", cookieBase(PREVIEW_COOKIE, secure, 0, ""));
162
213
  headers.append("Set-Cookie", cookieBase(PREVIEW_SESSION_COOKIE, secure, 0, ""));
163
214
  headers.append("Set-Cookie", markerCookieHeader(secure, false));
215
+ appendClearPartitionedBypass(headers, secure);
164
216
  return Response.json({ mode: "live", reason: "invalid" }, { headers });
165
217
  };
166
218
  if (!claims || !token || !workspaceOk) {
@@ -275,7 +327,7 @@ export function createPreviewRoute(
275
327
  tree: payload.tree,
276
328
  exp: now() + PREVIEW_COOKIE_TTL_MS,
277
329
  });
278
- await (deps.enableDraft ?? (() => nextDraft(true)))();
330
+ const bypass = await (deps.enableDraft ?? (() => nextDraft(true)))();
279
331
  const location = previewLocationWithCheckout(url.origin, payload.redirect, value);
280
332
  const headers = new Headers({ Location: location });
281
333
  headers.append(
@@ -295,6 +347,7 @@ export function createPreviewRoute(
295
347
  }
296
348
  // Entry opts the visitor in, so the bar shows on later pages without a param.
297
349
  headers.append("Set-Cookie", markerCookieHeader(secure, true));
350
+ appendPartitionedBypass(headers, secure, bypass);
298
351
  return new Response(null, { status: 302, headers });
299
352
  };
300
353
 
@@ -333,6 +386,7 @@ export function createPreviewRoute(
333
386
  await (deps.disableDraft ?? (() => nextDraft(false)))();
334
387
  const headers = new Headers();
335
388
  headers.append("Set-Cookie", cookieBase(PREVIEW_COOKIE, secure, 0, ""));
389
+ appendClearPartitionedBypass(headers, secure);
336
390
  return new Response(null, { status: 204, headers });
337
391
  }
338
392
 
@@ -353,13 +407,14 @@ export function createPreviewRoute(
353
407
  tree: claims.tree,
354
408
  exp: now() + PREVIEW_COOKIE_TTL_MS,
355
409
  });
356
- await (deps.enableDraft ?? (() => nextDraft(true)))();
410
+ const bypass = await (deps.enableDraft ?? (() => nextDraft(true)))();
357
411
  const headers = new Headers();
358
412
  headers.append(
359
413
  "Set-Cookie",
360
414
  cookieBase(PREVIEW_COOKIE, secure, Math.floor(PREVIEW_COOKIE_TTL_MS / 1000), value),
361
415
  );
362
416
  headers.append("Set-Cookie", markerCookieHeader(secure, true));
417
+ appendPartitionedBypass(headers, secure, bypass);
363
418
  return new Response(null, { status: 204, headers });
364
419
  }
365
420
 
@@ -369,6 +424,7 @@ export function createPreviewRoute(
369
424
  headers.append("Set-Cookie", cookieBase(PREVIEW_SESSION_COOKIE, secure, 0, ""));
370
425
  // Exit also drops the marker; otherwise the next page would bounce straight back in.
371
426
  headers.append("Set-Cookie", markerCookieHeader(secure, false));
427
+ appendClearPartitionedBypass(headers, secure);
372
428
  return new Response(null, { status: 204, headers });
373
429
  };
374
430