@becklyn/deployment-protection 0.4.1 → 0.4.3

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 CHANGED
@@ -214,10 +214,20 @@ x-vercel-protection-bypass: <VERCEL_AUTOMATION_BYPASS_SECRET>
214
214
 
215
215
  Or query:
216
216
 
217
+ ```text
218
+ https://app.example/?x-vercel-protection-bypass=<secret>
219
+ ```
220
+
221
+ A valid bypass query param redirects to the same path without the secret and sets the signed `__becklyn_dp_session` cookie, so later requests stay authenticated. You do not need `x-vercel-set-bypass-cookie` for that.
222
+
223
+ Optional Vercel-compatible helper — also persist the raw bypass secret as `__becklyn_dp_bypass`:
224
+
217
225
  ```text
218
226
  https://app.example/?x-vercel-protection-bypass=<secret>&x-vercel-set-bypass-cookie=true
219
227
  ```
220
228
 
229
+ The header form stays one-shot (no cookies) so CI/Playwright can send it on each request.
230
+
221
231
  When platform Deployment Protection is disabled, **this package** enforces the bypass secret so CI/Playwright keep working the same way.
222
232
 
223
233
  ## Behaviour
@@ -225,11 +235,12 @@ When platform Deployment Protection is disabled, **this package** enforces the b
225
235
  1. Disabled / misconfigured → pass through
226
236
  2. Internal Vercel OAuth routes → handled by the package (proxy start or direct OAuth / handoff)
227
237
  3. Login form `POST` → validate username/password, set signed HttpOnly session cookie
228
- 4. Valid bypass header/query/cookie → allow (optionally set cookies)
229
- 5. Valid session cookieallow
230
- 6. Else HTML login page (password and/or Sign in with Vercel)
238
+ 4. Valid bypass header → allow this request
239
+ 5. Valid bypass query param set `__becklyn_dp_session`, redirect to the cleaned URL
240
+ 6. Valid session or bypass cookie allow
241
+ 7. Else → HTML login page (password and/or Sign in with Vercel)
231
242
 
232
- Session cookie: `__becklyn_dp_session` (HMAC-SHA256, 14 days by default).
243
+ Session cookie: `__becklyn_dp_session` (HMAC-SHA256, 14 days by default). On HTTPS it is `HttpOnly; Secure; SameSite=None` so a logged-in session is sent when the preview is embedded in a third-party iframe (e.g. Contentful live preview). On HTTP (localhost) it stays `SameSite=Lax` because browsers reject `SameSite=None` without `Secure`.
233
244
 
234
245
  ## Disable without code changes
235
246
 
@@ -272,6 +283,8 @@ Recommended matcher (must be pasted as a string literal in your middleware/proxy
272
283
  - Do not put the raw handoff secret in query strings — only signatures/tokens.
273
284
  - Turn off paid Vercel Password Protection / seat-heavy sharing once this is live; keep `VERCEL_AUTOMATION_BYPASS_SECRET` configured for tooling.
274
285
  - Middleware is a convenience edge check — do not treat it as the only control for highly sensitive data.
286
+ - `__becklyn_dp_session` is `SameSite=None` on HTTPS so third-party iframes can send it. That cookie only decides whether the preview may load; apps must still CSRF-protect their own state-changing endpoints. OAuth state/PKCE cookies stay `SameSite=Lax`.
287
+ - Safari (and some Chrome third-party-cookie settings) may still block unpartitioned third-party cookies even with `SameSite=None`.
275
288
 
276
289
  ## License
277
290
 
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/handler.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAA8B,2BAA2B,EAAE,MAAM,SAAS,CAAC;AA4RvF;;GAEG;AACH,wBAAsB,0BAA0B,CAC5C,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAqD1B"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/handler.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAA8B,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAqSvF;;GAEG;AACH,wBAAsB,0BAA0B,CAC5C,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAoD1B"}
@@ -37,11 +37,20 @@ function redirect(request, location, status = 302) {
37
37
  },
38
38
  });
39
39
  }
40
+ /**
41
+ * Prefer the dedicated session secret; fall back to the automation bypass secret
42
+ * so a query-param bypass can still persist `__becklyn_dp_session` when no
43
+ * username/password or explicit HMAC secret is configured.
44
+ */
45
+ function signingSecret(config) {
46
+ return config.secret ?? config.bypassSecret;
47
+ }
40
48
  async function attachSession(response, config, method, subject, secure) {
41
- if (!config.secret) {
49
+ const secret = signingSecret(config);
50
+ if (!secret) {
42
51
  return response;
43
52
  }
44
- const token = await (0, session_1.createSessionToken)(config.secret, method, subject, config.sessionTtlSeconds);
53
+ const token = await (0, session_1.createSessionToken)(secret, method, subject, config.sessionTtlSeconds);
45
54
  const opts = (0, session_1.sessionCookieOptions)(config.sessionTtlSeconds, secure);
46
55
  (0, vercel_oauth_1.appendSetCookie)(response, constants_1.SESSION_COOKIE_NAME, token, opts);
47
56
  return response;
@@ -57,10 +66,12 @@ async function handleBypass(request, config) {
57
66
  const setCookieMode = (0, bypass_1.shouldSetBypassCookie)(request);
58
67
  const url = new URL(request.url);
59
68
  const hadQueryBypass = url.searchParams.has(constants_1.BYPASS_HEADER);
60
- const hadSetCookieQuery = url.searchParams.has("x-vercel-set-bypass-cookie");
69
+ const hadSetCookieQuery = url.searchParams.has(constants_1.SET_BYPASS_COOKIE_HEADER);
70
+ // Query-param bypass (and the Vercel set-cookie helper) persist auth, then
71
+ // strip secrets from the URL. Header-only automation stays one-shot.
61
72
  if (hadQueryBypass || hadSetCookieQuery || setCookieMode) {
62
73
  url.searchParams.delete(constants_1.BYPASS_HEADER);
63
- url.searchParams.delete("x-vercel-set-bypass-cookie");
74
+ url.searchParams.delete(constants_1.SET_BYPASS_COOKIE_HEADER);
64
75
  const response = redirect(request, url.pathname + url.search + url.hash);
65
76
  const secure = isSecureRequest(request);
66
77
  if (setCookieMode || hadSetCookieQuery) {
@@ -72,6 +83,7 @@ async function handleBypass(request, config) {
72
83
  sameSite: setCookieMode === "samesitenone" ? "none" : "lax",
73
84
  });
74
85
  }
86
+ // Always persist a signed session when the bypass secret arrived via query.
75
87
  await attachSession(response, config, "bypass", "automation", secure);
76
88
  return { kind: "respond", response };
77
89
  }
@@ -233,8 +245,9 @@ async function handleDeploymentProtection(request, options = {}) {
233
245
  if (bypass.kind === "allow") {
234
246
  return null;
235
247
  }
236
- if (config.secret) {
237
- const session = await (0, session_1.verifySessionToken)(config.secret, (0, vercel_oauth_1.readCookie)(request, constants_1.SESSION_COOKIE_NAME));
248
+ const secret = signingSecret(config);
249
+ if (secret) {
250
+ const session = await (0, session_1.verifySessionToken)(secret, (0, vercel_oauth_1.readCookie)(request, constants_1.SESSION_COOKIE_NAME));
238
251
  if (session) {
239
252
  return null;
240
253
  }
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAE3D,KAAK,cAAc,GAAG,CAClB,OAAO,EAAE,WAAW,EACpB,KAAK,CAAC,EAAE,OAAO,KAEb,QAAQ,GACR,YAAY,GACZ,OAAO,CAAC,QAAQ,GAAG,YAAY,GAAG,SAAS,GAAG,IAAI,CAAC,GACnD,SAAS,GACT,IAAI,CAAC;AAEX,MAAM,MAAM,4BAA4B,GAClC,EAAE,GACF,CAAC,2BAA2B,CAAC,GAC7B,CAAC,cAAc,CAAC,GAChB,CAAC,cAAc,EAAE,2BAA2B,CAAC,CAAC;AA8DpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,IAAI,EAAE,4BAA4B,IAgBtE,SAAS,WAAW,EACpB,QAAQ,OAAO,KAChB,OAAO,CAAC,QAAQ,GAAG,YAAY,CAAC,CActC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAE3D,KAAK,cAAc,GAAG,CAClB,OAAO,EAAE,WAAW,EACpB,KAAK,CAAC,EAAE,OAAO,KAEb,QAAQ,GACR,YAAY,GACZ,OAAO,CAAC,QAAQ,GAAG,YAAY,GAAG,SAAS,GAAG,IAAI,CAAC,GACnD,SAAS,GACT,IAAI,CAAC;AAEX,MAAM,MAAM,4BAA4B,GAClC,EAAE,GACF,CAAC,2BAA2B,CAAC,GAC7B,CAAC,cAAc,CAAC,GAChB,CAAC,cAAc,EAAE,2BAA2B,CAAC,CAAC;AA8JpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,IAAI,EAAE,4BAA4B,IAgBtE,SAAS,WAAW,EACpB,QAAQ,OAAO,KAChB,OAAO,CAAC,QAAQ,GAAG,YAAY,CAAC,CActC"}
@@ -9,12 +9,95 @@ function isOptions(value) {
9
9
  !Array.isArray(value) &&
10
10
  typeof value !== "function");
11
11
  }
12
+ function decodeCookieValue(value) {
13
+ try {
14
+ return decodeURIComponent(value);
15
+ }
16
+ catch {
17
+ return value;
18
+ }
19
+ }
20
+ function parseSetCookie(header) {
21
+ const parts = header
22
+ .split(";")
23
+ .map(part => part.trim())
24
+ .filter(Boolean);
25
+ const first = parts.shift();
26
+ if (!first) {
27
+ return null;
28
+ }
29
+ const separator = first.indexOf("=");
30
+ if (separator <= 0) {
31
+ return null;
32
+ }
33
+ const parsed = {
34
+ name: first.slice(0, separator),
35
+ value: decodeCookieValue(first.slice(separator + 1)),
36
+ };
37
+ for (const part of parts) {
38
+ const [rawKey, ...rest] = part.split("=");
39
+ const key = rawKey?.toLowerCase();
40
+ const attrValue = rest.join("=");
41
+ if (key === "httponly") {
42
+ parsed.httpOnly = true;
43
+ }
44
+ else if (key === "secure") {
45
+ parsed.secure = true;
46
+ }
47
+ else if (key === "path") {
48
+ parsed.path = attrValue;
49
+ }
50
+ else if (key === "max-age") {
51
+ const maxAge = Number(attrValue);
52
+ if (!Number.isNaN(maxAge)) {
53
+ parsed.maxAge = maxAge;
54
+ }
55
+ }
56
+ else if (key === "samesite") {
57
+ const sameSite = attrValue.toLowerCase();
58
+ if (sameSite === "lax" || sameSite === "strict" || sameSite === "none") {
59
+ parsed.sameSite = sameSite;
60
+ }
61
+ }
62
+ }
63
+ return parsed;
64
+ }
65
+ function readSetCookieHeaders(headers) {
66
+ if (typeof headers.getSetCookie === "function") {
67
+ const cookies = headers.getSetCookie();
68
+ if (cookies.length > 0) {
69
+ return cookies;
70
+ }
71
+ }
72
+ const single = headers.get("set-cookie");
73
+ return single ? [single] : [];
74
+ }
75
+ /**
76
+ * Next.js middleware only reliably applies cookies set via `NextResponse.cookies`.
77
+ * Copying raw `Set-Cookie` headers onto a redirect is ignored by the runtime.
78
+ */
79
+ function applyCookies(from, to) {
80
+ for (const header of readSetCookieHeaders(from.headers)) {
81
+ const cookie = parseSetCookie(header);
82
+ if (!cookie) {
83
+ continue;
84
+ }
85
+ to.cookies.set(cookie.name, cookie.value, {
86
+ httpOnly: cookie.httpOnly,
87
+ secure: cookie.secure,
88
+ path: cookie.path,
89
+ maxAge: cookie.maxAge,
90
+ sameSite: cookie.sameSite,
91
+ });
92
+ }
93
+ }
12
94
  function toNextResponse(request, response) {
13
95
  const location = response.headers.get("Location");
14
96
  if (location && response.status >= 300 && response.status < 400) {
15
97
  // Always resolve against the incoming request so relative Locations never reach Next.
16
98
  const redirectResponse = server_1.NextResponse.redirect(new URL(location, request.url), response.status);
17
99
  copyHeaders(response, redirectResponse, /* skipLocation */ true);
100
+ applyCookies(response, redirectResponse);
18
101
  return redirectResponse;
19
102
  }
20
103
  const nextResponse = new server_1.NextResponse(response.body, {
@@ -22,22 +105,14 @@ function toNextResponse(request, response) {
22
105
  statusText: response.statusText,
23
106
  });
24
107
  copyHeaders(response, nextResponse, false);
108
+ applyCookies(response, nextResponse);
25
109
  return nextResponse;
26
110
  }
27
111
  function copyHeaders(from, to, skipLocation) {
28
- const setCookies = typeof from.headers.getSetCookie === "function" ? from.headers.getSetCookie() : [];
29
- if (setCookies.length > 0) {
30
- for (const cookie of setCookies) {
31
- to.headers.append("Set-Cookie", cookie);
32
- }
33
- }
34
112
  from.headers.forEach((value, key) => {
35
113
  const lower = key.toLowerCase();
36
114
  if (lower === "set-cookie") {
37
- // Already copied via getSetCookie when available; fall back otherwise.
38
- if (setCookies.length === 0) {
39
- to.headers.append("Set-Cookie", value);
40
- }
115
+ // Applied via NextResponse.cookies so the middleware runtime honors them.
41
116
  return;
42
117
  }
43
118
  if (skipLocation && lower === "location") {
@@ -3,7 +3,7 @@ export declare function createSessionToken(secret: string, method: AuthMethod, s
3
3
  export declare function verifySessionToken(secret: string, token: string | undefined | null): Promise<SessionPayload | null>;
4
4
  export declare function sessionCookieOptions(maxAge: number, secure: boolean): {
5
5
  httpOnly: boolean;
6
- sameSite: "lax";
6
+ sameSite: "none" | "lax";
7
7
  secure: boolean;
8
8
  path: string;
9
9
  maxAge: number;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAyB1D,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GACjC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA4BhC;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;;;;;;EAQnE"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAyB1D,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GACjC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA4BhC;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;;cAKrB,MAAM,GAAG,KAAK;;;;EAK5D"}
@@ -56,7 +56,9 @@ async function verifySessionToken(secret, token) {
56
56
  function sessionCookieOptions(maxAge, secure) {
57
57
  return {
58
58
  httpOnly: true,
59
- sameSite: "lax",
59
+ // SameSite=None; Secure is required for third-party iframes (e.g. Contentful).
60
+ // Browsers reject SameSite=None without Secure, so HTTP (localhost) stays Lax.
61
+ sameSite: (secure ? "none" : "lax"),
60
62
  secure,
61
63
  path: "/",
62
64
  maxAge,
@@ -1 +1 @@
1
- {"version":3,"file":"vercel-oauth.d.ts","sourceRoot":"","sources":["../../src/vercel-oauth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAC;AAE1D,MAAM,WAAW,mBAAmB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAgBD,wBAAsB,4BAA4B,CAC9C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAsCnB;AAED,wBAAsB,kBAAkB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA6B9B;AAED,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAYtF;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBxE;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAO/F;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAqB7E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAkB3E;AAED,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IACL,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,GACF,IAAI,CAwBN"}
1
+ {"version":3,"file":"vercel-oauth.d.ts","sourceRoot":"","sources":["../../src/vercel-oauth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAC;AAE1D,MAAM,WAAW,mBAAmB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAgBD,wBAAsB,4BAA4B,CAC9C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAsCnB;AAED,wBAAsB,kBAAkB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA6B9B;AAED,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAYtF;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBxE;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAO/F;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAqB7E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAkB3E;AAQD,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IACL,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,GACF,IAAI,CAsBN"}
@@ -151,6 +151,11 @@ function clearOAuthCookies(response, secure) {
151
151
  appendSetCookie(response, name, "", expired);
152
152
  }
153
153
  }
154
+ const SAME_SITE_LABEL = {
155
+ lax: "Lax",
156
+ strict: "Strict",
157
+ none: "None",
158
+ };
154
159
  function appendSetCookie(response, name, value, options) {
155
160
  const parts = [`${name}=${encodeURIComponent(value)}`];
156
161
  if (options.maxAge !== undefined) {
@@ -164,7 +169,7 @@ function appendSetCookie(response, name, value, options) {
164
169
  parts.push("Secure");
165
170
  }
166
171
  if (options.sameSite) {
167
- parts.push(`SameSite=${options.sameSite === "none" ? "None" : options.sameSite[0].toUpperCase()}${options.sameSite.slice(1)}`);
172
+ parts.push(`SameSite=${SAME_SITE_LABEL[options.sameSite]}`);
168
173
  }
169
174
  response.headers.append("Set-Cookie", parts.join("; "));
170
175
  }
package/dist/edge.mjs CHANGED
@@ -534,7 +534,9 @@ async function verifySessionToken(secret, token) {
534
534
  function sessionCookieOptions(maxAge, secure) {
535
535
  return {
536
536
  httpOnly: true,
537
- sameSite: "lax",
537
+ // SameSite=None; Secure is required for third-party iframes (e.g. Contentful).
538
+ // Browsers reject SameSite=None without Secure, so HTTP (localhost) stays Lax.
539
+ sameSite: secure ? "none" : "lax",
538
540
  secure,
539
541
  path: "/",
540
542
  maxAge
@@ -682,6 +684,11 @@ function clearOAuthCookies(response, secure) {
682
684
  appendSetCookie(response, name, "", expired);
683
685
  }
684
686
  }
687
+ var SAME_SITE_LABEL = {
688
+ lax: "Lax",
689
+ strict: "Strict",
690
+ none: "None"
691
+ };
685
692
  function appendSetCookie(response, name, value, options) {
686
693
  const parts = [`${name}=${encodeURIComponent(value)}`];
687
694
  if (options.maxAge !== void 0) {
@@ -695,9 +702,7 @@ function appendSetCookie(response, name, value, options) {
695
702
  parts.push("Secure");
696
703
  }
697
704
  if (options.sameSite) {
698
- parts.push(
699
- `SameSite=${options.sameSite === "none" ? "None" : options.sameSite[0].toUpperCase()}${options.sameSite.slice(1)}`
700
- );
705
+ parts.push(`SameSite=${SAME_SITE_LABEL[options.sameSite]}`);
701
706
  }
702
707
  response.headers.append("Set-Cookie", parts.join("; "));
703
708
  }
@@ -728,16 +733,15 @@ function redirect(request, location, status = 302) {
728
733
  }
729
734
  });
730
735
  }
736
+ function signingSecret(config) {
737
+ return config.secret ?? config.bypassSecret;
738
+ }
731
739
  async function attachSession(response, config, method, subject, secure) {
732
- if (!config.secret) {
740
+ const secret = signingSecret(config);
741
+ if (!secret) {
733
742
  return response;
734
743
  }
735
- const token = await createSessionToken(
736
- config.secret,
737
- method,
738
- subject,
739
- config.sessionTtlSeconds
740
- );
744
+ const token = await createSessionToken(secret, method, subject, config.sessionTtlSeconds);
741
745
  const opts = sessionCookieOptions(config.sessionTtlSeconds, secure);
742
746
  appendSetCookie(response, SESSION_COOKIE_NAME, token, opts);
743
747
  return response;
@@ -753,10 +757,10 @@ async function handleBypass(request, config) {
753
757
  const setCookieMode = shouldSetBypassCookie(request);
754
758
  const url = new URL(request.url);
755
759
  const hadQueryBypass = url.searchParams.has(BYPASS_HEADER);
756
- const hadSetCookieQuery = url.searchParams.has("x-vercel-set-bypass-cookie");
760
+ const hadSetCookieQuery = url.searchParams.has(SET_BYPASS_COOKIE_HEADER);
757
761
  if (hadQueryBypass || hadSetCookieQuery || setCookieMode) {
758
762
  url.searchParams.delete(BYPASS_HEADER);
759
- url.searchParams.delete("x-vercel-set-bypass-cookie");
763
+ url.searchParams.delete(SET_BYPASS_COOKIE_HEADER);
760
764
  const response = redirect(request, url.pathname + url.search + url.hash);
761
765
  const secure = isSecureRequest(request);
762
766
  if (setCookieMode || hadSetCookieQuery) {
@@ -928,11 +932,9 @@ async function handleDeploymentProtection(request, options = {}) {
928
932
  if (bypass.kind === "allow") {
929
933
  return null;
930
934
  }
931
- if (config.secret) {
932
- const session = await verifySessionToken(
933
- config.secret,
934
- readCookie(request, SESSION_COOKIE_NAME)
935
- );
935
+ const secret = signingSecret(config);
936
+ if (secret) {
937
+ const session = await verifySessionToken(secret, readCookie(request, SESSION_COOKIE_NAME));
936
938
  if (session) {
937
939
  return null;
938
940
  }
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/handler.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAA8B,2BAA2B,EAAE,MAAM,SAAS,CAAC;AA4RvF;;GAEG;AACH,wBAAsB,0BAA0B,CAC5C,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAqD1B"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/handler.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAA8B,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAqSvF;;GAEG;AACH,wBAAsB,0BAA0B,CAC5C,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAoD1B"}
@@ -1,6 +1,6 @@
1
1
  import { isValidBypass, readBypassCookie, readBypassToken, shouldSetBypassCookie } from "./bypass";
2
2
  import { hasPasswordAuth, hasVercelAuth, hasVercelDirectAuth, hasVercelProxyAuth, isProtectionActive, resolveConfig, } from "./config";
3
- import { BYPASS_COOKIE_NAME, BYPASS_HEADER, OAUTH_NONCE_COOKIE, OAUTH_RETURN_COOKIE, OAUTH_VERIFIER_COOKIE, SESSION_COOKIE_NAME, VERCEL_AUTHORIZE_PATH, VERCEL_CALLBACK_PATH, } from "./constants";
3
+ import { BYPASS_COOKIE_NAME, BYPASS_HEADER, OAUTH_NONCE_COOKIE, OAUTH_RETURN_COOKIE, OAUTH_VERIFIER_COOKIE, SESSION_COOKIE_NAME, SET_BYPASS_COOKIE_HEADER, VERCEL_AUTHORIZE_PATH, VERCEL_CALLBACK_PATH, } from "./constants";
4
4
  import { timingSafeEqualString } from "./crypto";
5
5
  import { buildProxyStartUrl, verifyHandoffToken } from "./handoff";
6
6
  import { renderLoginPage } from "./login-page";
@@ -34,11 +34,20 @@ function redirect(request, location, status = 302) {
34
34
  },
35
35
  });
36
36
  }
37
+ /**
38
+ * Prefer the dedicated session secret; fall back to the automation bypass secret
39
+ * so a query-param bypass can still persist `__becklyn_dp_session` when no
40
+ * username/password or explicit HMAC secret is configured.
41
+ */
42
+ function signingSecret(config) {
43
+ return config.secret ?? config.bypassSecret;
44
+ }
37
45
  async function attachSession(response, config, method, subject, secure) {
38
- if (!config.secret) {
46
+ const secret = signingSecret(config);
47
+ if (!secret) {
39
48
  return response;
40
49
  }
41
- const token = await createSessionToken(config.secret, method, subject, config.sessionTtlSeconds);
50
+ const token = await createSessionToken(secret, method, subject, config.sessionTtlSeconds);
42
51
  const opts = sessionCookieOptions(config.sessionTtlSeconds, secure);
43
52
  appendSetCookie(response, SESSION_COOKIE_NAME, token, opts);
44
53
  return response;
@@ -54,10 +63,12 @@ async function handleBypass(request, config) {
54
63
  const setCookieMode = shouldSetBypassCookie(request);
55
64
  const url = new URL(request.url);
56
65
  const hadQueryBypass = url.searchParams.has(BYPASS_HEADER);
57
- const hadSetCookieQuery = url.searchParams.has("x-vercel-set-bypass-cookie");
66
+ const hadSetCookieQuery = url.searchParams.has(SET_BYPASS_COOKIE_HEADER);
67
+ // Query-param bypass (and the Vercel set-cookie helper) persist auth, then
68
+ // strip secrets from the URL. Header-only automation stays one-shot.
58
69
  if (hadQueryBypass || hadSetCookieQuery || setCookieMode) {
59
70
  url.searchParams.delete(BYPASS_HEADER);
60
- url.searchParams.delete("x-vercel-set-bypass-cookie");
71
+ url.searchParams.delete(SET_BYPASS_COOKIE_HEADER);
61
72
  const response = redirect(request, url.pathname + url.search + url.hash);
62
73
  const secure = isSecureRequest(request);
63
74
  if (setCookieMode || hadSetCookieQuery) {
@@ -69,6 +80,7 @@ async function handleBypass(request, config) {
69
80
  sameSite: setCookieMode === "samesitenone" ? "none" : "lax",
70
81
  });
71
82
  }
83
+ // Always persist a signed session when the bypass secret arrived via query.
72
84
  await attachSession(response, config, "bypass", "automation", secure);
73
85
  return { kind: "respond", response };
74
86
  }
@@ -230,8 +242,9 @@ export async function handleDeploymentProtection(request, options = {}) {
230
242
  if (bypass.kind === "allow") {
231
243
  return null;
232
244
  }
233
- if (config.secret) {
234
- const session = await verifySessionToken(config.secret, readCookie(request, SESSION_COOKIE_NAME));
245
+ const secret = signingSecret(config);
246
+ if (secret) {
247
+ const session = await verifySessionToken(secret, readCookie(request, SESSION_COOKIE_NAME));
235
248
  if (session) {
236
249
  return null;
237
250
  }
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAE3D,KAAK,cAAc,GAAG,CAClB,OAAO,EAAE,WAAW,EACpB,KAAK,CAAC,EAAE,OAAO,KAEb,QAAQ,GACR,YAAY,GACZ,OAAO,CAAC,QAAQ,GAAG,YAAY,GAAG,SAAS,GAAG,IAAI,CAAC,GACnD,SAAS,GACT,IAAI,CAAC;AAEX,MAAM,MAAM,4BAA4B,GAClC,EAAE,GACF,CAAC,2BAA2B,CAAC,GAC7B,CAAC,cAAc,CAAC,GAChB,CAAC,cAAc,EAAE,2BAA2B,CAAC,CAAC;AA8DpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,IAAI,EAAE,4BAA4B,IAgBtE,SAAS,WAAW,EACpB,QAAQ,OAAO,KAChB,OAAO,CAAC,QAAQ,GAAG,YAAY,CAAC,CActC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAE3D,KAAK,cAAc,GAAG,CAClB,OAAO,EAAE,WAAW,EACpB,KAAK,CAAC,EAAE,OAAO,KAEb,QAAQ,GACR,YAAY,GACZ,OAAO,CAAC,QAAQ,GAAG,YAAY,GAAG,SAAS,GAAG,IAAI,CAAC,GACnD,SAAS,GACT,IAAI,CAAC;AAEX,MAAM,MAAM,4BAA4B,GAClC,EAAE,GACF,CAAC,2BAA2B,CAAC,GAC7B,CAAC,cAAc,CAAC,GAChB,CAAC,cAAc,EAAE,2BAA2B,CAAC,CAAC;AA8JpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,IAAI,EAAE,4BAA4B,IAgBtE,SAAS,WAAW,EACpB,QAAQ,OAAO,KAChB,OAAO,CAAC,QAAQ,GAAG,YAAY,CAAC,CActC"}
@@ -6,12 +6,95 @@ function isOptions(value) {
6
6
  !Array.isArray(value) &&
7
7
  typeof value !== "function");
8
8
  }
9
+ function decodeCookieValue(value) {
10
+ try {
11
+ return decodeURIComponent(value);
12
+ }
13
+ catch {
14
+ return value;
15
+ }
16
+ }
17
+ function parseSetCookie(header) {
18
+ const parts = header
19
+ .split(";")
20
+ .map(part => part.trim())
21
+ .filter(Boolean);
22
+ const first = parts.shift();
23
+ if (!first) {
24
+ return null;
25
+ }
26
+ const separator = first.indexOf("=");
27
+ if (separator <= 0) {
28
+ return null;
29
+ }
30
+ const parsed = {
31
+ name: first.slice(0, separator),
32
+ value: decodeCookieValue(first.slice(separator + 1)),
33
+ };
34
+ for (const part of parts) {
35
+ const [rawKey, ...rest] = part.split("=");
36
+ const key = rawKey?.toLowerCase();
37
+ const attrValue = rest.join("=");
38
+ if (key === "httponly") {
39
+ parsed.httpOnly = true;
40
+ }
41
+ else if (key === "secure") {
42
+ parsed.secure = true;
43
+ }
44
+ else if (key === "path") {
45
+ parsed.path = attrValue;
46
+ }
47
+ else if (key === "max-age") {
48
+ const maxAge = Number(attrValue);
49
+ if (!Number.isNaN(maxAge)) {
50
+ parsed.maxAge = maxAge;
51
+ }
52
+ }
53
+ else if (key === "samesite") {
54
+ const sameSite = attrValue.toLowerCase();
55
+ if (sameSite === "lax" || sameSite === "strict" || sameSite === "none") {
56
+ parsed.sameSite = sameSite;
57
+ }
58
+ }
59
+ }
60
+ return parsed;
61
+ }
62
+ function readSetCookieHeaders(headers) {
63
+ if (typeof headers.getSetCookie === "function") {
64
+ const cookies = headers.getSetCookie();
65
+ if (cookies.length > 0) {
66
+ return cookies;
67
+ }
68
+ }
69
+ const single = headers.get("set-cookie");
70
+ return single ? [single] : [];
71
+ }
72
+ /**
73
+ * Next.js middleware only reliably applies cookies set via `NextResponse.cookies`.
74
+ * Copying raw `Set-Cookie` headers onto a redirect is ignored by the runtime.
75
+ */
76
+ function applyCookies(from, to) {
77
+ for (const header of readSetCookieHeaders(from.headers)) {
78
+ const cookie = parseSetCookie(header);
79
+ if (!cookie) {
80
+ continue;
81
+ }
82
+ to.cookies.set(cookie.name, cookie.value, {
83
+ httpOnly: cookie.httpOnly,
84
+ secure: cookie.secure,
85
+ path: cookie.path,
86
+ maxAge: cookie.maxAge,
87
+ sameSite: cookie.sameSite,
88
+ });
89
+ }
90
+ }
9
91
  function toNextResponse(request, response) {
10
92
  const location = response.headers.get("Location");
11
93
  if (location && response.status >= 300 && response.status < 400) {
12
94
  // Always resolve against the incoming request so relative Locations never reach Next.
13
95
  const redirectResponse = NextResponse.redirect(new URL(location, request.url), response.status);
14
96
  copyHeaders(response, redirectResponse, /* skipLocation */ true);
97
+ applyCookies(response, redirectResponse);
15
98
  return redirectResponse;
16
99
  }
17
100
  const nextResponse = new NextResponse(response.body, {
@@ -19,22 +102,14 @@ function toNextResponse(request, response) {
19
102
  statusText: response.statusText,
20
103
  });
21
104
  copyHeaders(response, nextResponse, false);
105
+ applyCookies(response, nextResponse);
22
106
  return nextResponse;
23
107
  }
24
108
  function copyHeaders(from, to, skipLocation) {
25
- const setCookies = typeof from.headers.getSetCookie === "function" ? from.headers.getSetCookie() : [];
26
- if (setCookies.length > 0) {
27
- for (const cookie of setCookies) {
28
- to.headers.append("Set-Cookie", cookie);
29
- }
30
- }
31
109
  from.headers.forEach((value, key) => {
32
110
  const lower = key.toLowerCase();
33
111
  if (lower === "set-cookie") {
34
- // Already copied via getSetCookie when available; fall back otherwise.
35
- if (setCookies.length === 0) {
36
- to.headers.append("Set-Cookie", value);
37
- }
112
+ // Applied via NextResponse.cookies so the middleware runtime honors them.
38
113
  return;
39
114
  }
40
115
  if (skipLocation && lower === "location") {
@@ -3,7 +3,7 @@ export declare function createSessionToken(secret: string, method: AuthMethod, s
3
3
  export declare function verifySessionToken(secret: string, token: string | undefined | null): Promise<SessionPayload | null>;
4
4
  export declare function sessionCookieOptions(maxAge: number, secure: boolean): {
5
5
  httpOnly: boolean;
6
- sameSite: "lax";
6
+ sameSite: "none" | "lax";
7
7
  secure: boolean;
8
8
  path: string;
9
9
  maxAge: number;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAyB1D,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GACjC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA4BhC;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;;;;;;EAQnE"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAyB1D,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GACjC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA4BhC;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;;cAKrB,MAAM,GAAG,KAAK;;;;EAK5D"}
@@ -51,7 +51,9 @@ export async function verifySessionToken(secret, token) {
51
51
  export function sessionCookieOptions(maxAge, secure) {
52
52
  return {
53
53
  httpOnly: true,
54
- sameSite: "lax",
54
+ // SameSite=None; Secure is required for third-party iframes (e.g. Contentful).
55
+ // Browsers reject SameSite=None without Secure, so HTTP (localhost) stays Lax.
56
+ sameSite: (secure ? "none" : "lax"),
55
57
  secure,
56
58
  path: "/",
57
59
  maxAge,
@@ -1 +1 @@
1
- {"version":3,"file":"vercel-oauth.d.ts","sourceRoot":"","sources":["../../src/vercel-oauth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAC;AAE1D,MAAM,WAAW,mBAAmB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAgBD,wBAAsB,4BAA4B,CAC9C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAsCnB;AAED,wBAAsB,kBAAkB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA6B9B;AAED,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAYtF;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBxE;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAO/F;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAqB7E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAkB3E;AAED,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IACL,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,GACF,IAAI,CAwBN"}
1
+ {"version":3,"file":"vercel-oauth.d.ts","sourceRoot":"","sources":["../../src/vercel-oauth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAC;AAE1D,MAAM,WAAW,mBAAmB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAgBD,wBAAsB,4BAA4B,CAC9C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAsCnB;AAED,wBAAsB,kBAAkB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA6B9B;AAED,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAYtF;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBxE;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAO/F;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAqB7E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAkB3E;AAQD,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IACL,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,GACF,IAAI,CAsBN"}
@@ -141,6 +141,11 @@ export function clearOAuthCookies(response, secure) {
141
141
  appendSetCookie(response, name, "", expired);
142
142
  }
143
143
  }
144
+ const SAME_SITE_LABEL = {
145
+ lax: "Lax",
146
+ strict: "Strict",
147
+ none: "None",
148
+ };
144
149
  export function appendSetCookie(response, name, value, options) {
145
150
  const parts = [`${name}=${encodeURIComponent(value)}`];
146
151
  if (options.maxAge !== undefined) {
@@ -154,7 +159,7 @@ export function appendSetCookie(response, name, value, options) {
154
159
  parts.push("Secure");
155
160
  }
156
161
  if (options.sameSite) {
157
- parts.push(`SameSite=${options.sameSite === "none" ? "None" : options.sameSite[0].toUpperCase()}${options.sameSite.slice(1)}`);
162
+ parts.push(`SameSite=${SAME_SITE_LABEL[options.sameSite]}`);
158
163
  }
159
164
  response.headers.append("Set-Cookie", parts.join("; "));
160
165
  }
@@ -534,7 +534,9 @@ async function verifySessionToken(secret, token) {
534
534
  function sessionCookieOptions(maxAge, secure) {
535
535
  return {
536
536
  httpOnly: true,
537
- sameSite: "lax",
537
+ // SameSite=None; Secure is required for third-party iframes (e.g. Contentful).
538
+ // Browsers reject SameSite=None without Secure, so HTTP (localhost) stays Lax.
539
+ sameSite: secure ? "none" : "lax",
538
540
  secure,
539
541
  path: "/",
540
542
  maxAge
@@ -682,6 +684,11 @@ function clearOAuthCookies(response, secure) {
682
684
  appendSetCookie(response, name, "", expired);
683
685
  }
684
686
  }
687
+ var SAME_SITE_LABEL = {
688
+ lax: "Lax",
689
+ strict: "Strict",
690
+ none: "None"
691
+ };
685
692
  function appendSetCookie(response, name, value, options) {
686
693
  const parts = [`${name}=${encodeURIComponent(value)}`];
687
694
  if (options.maxAge !== void 0) {
@@ -695,9 +702,7 @@ function appendSetCookie(response, name, value, options) {
695
702
  parts.push("Secure");
696
703
  }
697
704
  if (options.sameSite) {
698
- parts.push(
699
- `SameSite=${options.sameSite === "none" ? "None" : options.sameSite[0].toUpperCase()}${options.sameSite.slice(1)}`
700
- );
705
+ parts.push(`SameSite=${SAME_SITE_LABEL[options.sameSite]}`);
701
706
  }
702
707
  response.headers.append("Set-Cookie", parts.join("; "));
703
708
  }
@@ -728,16 +733,15 @@ function redirect(request, location, status = 302) {
728
733
  }
729
734
  });
730
735
  }
736
+ function signingSecret(config) {
737
+ return config.secret ?? config.bypassSecret;
738
+ }
731
739
  async function attachSession(response, config, method, subject, secure) {
732
- if (!config.secret) {
740
+ const secret = signingSecret(config);
741
+ if (!secret) {
733
742
  return response;
734
743
  }
735
- const token = await createSessionToken(
736
- config.secret,
737
- method,
738
- subject,
739
- config.sessionTtlSeconds
740
- );
744
+ const token = await createSessionToken(secret, method, subject, config.sessionTtlSeconds);
741
745
  const opts = sessionCookieOptions(config.sessionTtlSeconds, secure);
742
746
  appendSetCookie(response, SESSION_COOKIE_NAME, token, opts);
743
747
  return response;
@@ -753,10 +757,10 @@ async function handleBypass(request, config) {
753
757
  const setCookieMode = shouldSetBypassCookie(request);
754
758
  const url = new URL(request.url);
755
759
  const hadQueryBypass = url.searchParams.has(BYPASS_HEADER);
756
- const hadSetCookieQuery = url.searchParams.has("x-vercel-set-bypass-cookie");
760
+ const hadSetCookieQuery = url.searchParams.has(SET_BYPASS_COOKIE_HEADER);
757
761
  if (hadQueryBypass || hadSetCookieQuery || setCookieMode) {
758
762
  url.searchParams.delete(BYPASS_HEADER);
759
- url.searchParams.delete("x-vercel-set-bypass-cookie");
763
+ url.searchParams.delete(SET_BYPASS_COOKIE_HEADER);
760
764
  const response = redirect(request, url.pathname + url.search + url.hash);
761
765
  const secure = isSecureRequest(request);
762
766
  if (setCookieMode || hadSetCookieQuery) {
@@ -928,11 +932,9 @@ async function handleDeploymentProtection(request, options = {}) {
928
932
  if (bypass.kind === "allow") {
929
933
  return null;
930
934
  }
931
- if (config.secret) {
932
- const session = await verifySessionToken(
933
- config.secret,
934
- readCookie(request, SESSION_COOKIE_NAME)
935
- );
935
+ const secret = signingSecret(config);
936
+ if (secret) {
937
+ const session = await verifySessionToken(secret, readCookie(request, SESSION_COOKIE_NAME));
936
938
  if (session) {
937
939
  return null;
938
940
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@becklyn/deployment-protection",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "license": "MIT",
5
5
  "description": "Simple shared username/password + Sign in with Vercel gate for Next.js and Storybook deployments",
6
6
  "homepage": "https://github.com/Becklyn-Studios/ts-libs/tree/main/packages/deployment-protection",
@@ -11,9 +11,9 @@
11
11
  "scripts": {
12
12
  "lint": "eslint . --max-warnings 0 && prettier --check \"./**/*.{ts,tsx,md,json,mjs}\"",
13
13
  "fix": "eslint . --fix && prettier --write \"./**/*.{ts,tsx,md,json,mjs}\"",
14
- "test": "npm run build && vitest run",
14
+ "test": "vitest run",
15
15
  "build": "rm -rf dist && tsc -p tsconfig-es.json && tsc -p tsconfig-cjs.json && node ./scripts/build-edge-bundles.mjs",
16
- "prepublishOnly": "npm run lint && npm run test"
16
+ "prepublishOnly": "npm run lint && npm run build && npm run test"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@becklyn/eslint": "*",