@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 +1 -1
- package/src/preview-route.ts +62 -6
package/package.json
CHANGED
package/src/preview-route.ts
CHANGED
|
@@ -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
|
-
|
|
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<
|
|
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)
|
|
78
|
-
|
|
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
|
|