@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 +17 -4
- package/dist/cjs/handler.d.ts.map +1 -1
- package/dist/cjs/handler.js +19 -6
- package/dist/cjs/middleware.d.ts.map +1 -1
- package/dist/cjs/middleware.js +85 -10
- package/dist/cjs/session.d.ts +1 -1
- package/dist/cjs/session.d.ts.map +1 -1
- package/dist/cjs/session.js +3 -1
- package/dist/cjs/vercel-oauth.d.ts.map +1 -1
- package/dist/cjs/vercel-oauth.js +6 -1
- package/dist/edge.mjs +20 -18
- package/dist/es/handler.d.ts.map +1 -1
- package/dist/es/handler.js +20 -7
- package/dist/es/middleware.d.ts.map +1 -1
- package/dist/es/middleware.js +85 -10
- package/dist/es/session.d.ts +1 -1
- package/dist/es/session.d.ts.map +1 -1
- package/dist/es/session.js +3 -1
- package/dist/es/vercel-oauth.d.ts.map +1 -1
- package/dist/es/vercel-oauth.js +6 -1
- package/dist/storybook.mjs +20 -18
- package/package.json +3 -3
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
|
|
229
|
-
5. Valid
|
|
230
|
-
6.
|
|
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":"
|
|
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"}
|
package/dist/cjs/handler.js
CHANGED
|
@@ -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
|
-
|
|
49
|
+
const secret = signingSecret(config);
|
|
50
|
+
if (!secret) {
|
|
42
51
|
return response;
|
|
43
52
|
}
|
|
44
|
-
const token = await (0, session_1.createSessionToken)(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
237
|
-
|
|
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;
|
|
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"}
|
package/dist/cjs/middleware.js
CHANGED
|
@@ -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
|
-
//
|
|
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") {
|
package/dist/cjs/session.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/cjs/session.js
CHANGED
|
@@ -56,7 +56,9 @@ async function verifySessionToken(secret, token) {
|
|
|
56
56
|
function sessionCookieOptions(maxAge, secure) {
|
|
57
57
|
return {
|
|
58
58
|
httpOnly: true,
|
|
59
|
-
|
|
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;
|
|
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"}
|
package/dist/cjs/vercel-oauth.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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/dist/es/handler.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/handler.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/es/handler.js
CHANGED
|
@@ -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
|
-
|
|
46
|
+
const secret = signingSecret(config);
|
|
47
|
+
if (!secret) {
|
|
39
48
|
return response;
|
|
40
49
|
}
|
|
41
|
-
const token = await createSessionToken(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
234
|
-
|
|
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;
|
|
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"}
|
package/dist/es/middleware.js
CHANGED
|
@@ -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
|
-
//
|
|
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") {
|
package/dist/es/session.d.ts
CHANGED
|
@@ -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;
|
package/dist/es/session.d.ts.map
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/es/session.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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"}
|
package/dist/es/vercel-oauth.js
CHANGED
|
@@ -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
|
|
162
|
+
parts.push(`SameSite=${SAME_SITE_LABEL[options.sameSite]}`);
|
|
158
163
|
}
|
|
159
164
|
response.headers.append("Set-Cookie", parts.join("; "));
|
|
160
165
|
}
|
package/dist/storybook.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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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.
|
|
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": "
|
|
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": "*",
|