@becklyn/deployment-protection 0.0.1

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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/cjs/bypass.d.ts +5 -0
  4. package/dist/cjs/bypass.d.ts.map +1 -0
  5. package/dist/cjs/bypass.js +51 -0
  6. package/dist/cjs/config.d.ts +13 -0
  7. package/dist/cjs/config.d.ts.map +1 -0
  8. package/dist/cjs/config.js +68 -0
  9. package/dist/cjs/constants.d.ts +15 -0
  10. package/dist/cjs/constants.d.ts.map +1 -0
  11. package/dist/cjs/constants.js +19 -0
  12. package/dist/cjs/crypto.d.ts +7 -0
  13. package/dist/cjs/crypto.d.ts.map +1 -0
  14. package/dist/cjs/crypto.js +56 -0
  15. package/dist/cjs/handler.d.ts +6 -0
  16. package/dist/cjs/handler.d.ts.map +1 -0
  17. package/dist/cjs/handler.js +212 -0
  18. package/dist/cjs/index.d.ts +6 -0
  19. package/dist/cjs/index.d.ts.map +1 -0
  20. package/dist/cjs/index.js +17 -0
  21. package/dist/cjs/login-page.d.ts +8 -0
  22. package/dist/cjs/login-page.d.ts.map +1 -0
  23. package/dist/cjs/login-page.js +176 -0
  24. package/dist/cjs/middleware.d.ts +25 -0
  25. package/dist/cjs/middleware.d.ts.map +1 -0
  26. package/dist/cjs/middleware.js +63 -0
  27. package/dist/cjs/password.d.ts +3 -0
  28. package/dist/cjs/password.d.ts.map +1 -0
  29. package/dist/cjs/password.js +12 -0
  30. package/dist/cjs/safe-return-to.d.ts +8 -0
  31. package/dist/cjs/safe-return-to.d.ts.map +1 -0
  32. package/dist/cjs/safe-return-to.js +60 -0
  33. package/dist/cjs/session.d.ts +11 -0
  34. package/dist/cjs/session.d.ts.map +1 -0
  35. package/dist/cjs/session.js +64 -0
  36. package/dist/cjs/types.d.ts +35 -0
  37. package/dist/cjs/types.d.ts.map +1 -0
  38. package/dist/cjs/types.js +2 -0
  39. package/dist/cjs/vercel-oauth.d.ts +30 -0
  40. package/dist/cjs/vercel-oauth.d.ts.map +1 -0
  41. package/dist/cjs/vercel-oauth.js +166 -0
  42. package/dist/es/bypass.d.ts +5 -0
  43. package/dist/es/bypass.d.ts.map +1 -0
  44. package/dist/es/bypass.js +45 -0
  45. package/dist/es/config.d.ts +13 -0
  46. package/dist/es/config.d.ts.map +1 -0
  47. package/dist/es/config.js +62 -0
  48. package/dist/es/constants.d.ts +15 -0
  49. package/dist/es/constants.d.ts.map +1 -0
  50. package/dist/es/constants.js +16 -0
  51. package/dist/es/crypto.d.ts +7 -0
  52. package/dist/es/crypto.d.ts.map +1 -0
  53. package/dist/es/crypto.js +48 -0
  54. package/dist/es/handler.d.ts +6 -0
  55. package/dist/es/handler.d.ts.map +1 -0
  56. package/dist/es/handler.js +209 -0
  57. package/dist/es/index.d.ts +6 -0
  58. package/dist/es/index.d.ts.map +1 -0
  59. package/dist/es/index.js +4 -0
  60. package/dist/es/login-page.d.ts +8 -0
  61. package/dist/es/login-page.d.ts.map +1 -0
  62. package/dist/es/login-page.js +173 -0
  63. package/dist/es/middleware.d.ts +25 -0
  64. package/dist/es/middleware.d.ts.map +1 -0
  65. package/dist/es/middleware.js +59 -0
  66. package/dist/es/password.d.ts +3 -0
  67. package/dist/es/password.d.ts.map +1 -0
  68. package/dist/es/password.js +9 -0
  69. package/dist/es/safe-return-to.d.ts +8 -0
  70. package/dist/es/safe-return-to.d.ts.map +1 -0
  71. package/dist/es/safe-return-to.js +57 -0
  72. package/dist/es/session.d.ts +11 -0
  73. package/dist/es/session.d.ts.map +1 -0
  74. package/dist/es/session.js +59 -0
  75. package/dist/es/types.d.ts +35 -0
  76. package/dist/es/types.d.ts.map +1 -0
  77. package/dist/es/types.js +1 -0
  78. package/dist/es/vercel-oauth.d.ts +30 -0
  79. package/dist/es/vercel-oauth.d.ts.map +1 -0
  80. package/dist/es/vercel-oauth.js +156 -0
  81. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Becklyn Studios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # `@becklyn/deployment-protection`
2
+
3
+ Simple, shared deployment gate for Next.js apps on Vercel.
4
+
5
+ Use this instead of (or after disabling) Vercel platform Deployment Protection when you need:
6
+
7
+ - a **shared username/password** from env vars
8
+ - optional **Sign in with Vercel** for team members
9
+ - always-on **automation bypass** via `x-vercel-protection-bypass` / `VERCEL_AUTOMATION_BYPASS_SECRET`
10
+ - a kill switch (`DEPLOYMENT_PROTECTION_ENABLED`, on by default)
11
+
12
+ Protection runs in Next.js **middleware** (Next ≤15) or **proxy** (Next 16+).
13
+
14
+ > **Note:** Vercel Connect is unrelated (third-party API tokens). Platform Vercel Authentication cookies are not visible to your app once platform protection is off — team members use the **Sign in with Vercel** button instead.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm i @becklyn/deployment-protection
20
+ ```
21
+
22
+ ## Quick setup
23
+
24
+ ### 1. Middleware / proxy
25
+
26
+ **Next.js 16+ (`proxy.ts`):**
27
+
28
+ ```ts
29
+ import {
30
+ deploymentProtectionMatcher,
31
+ withDeploymentProtection,
32
+ } from "@becklyn/deployment-protection";
33
+
34
+ export const proxy = withDeploymentProtection();
35
+
36
+ export const config = {
37
+ matcher: deploymentProtectionMatcher,
38
+ };
39
+ ```
40
+
41
+ **Next.js ≤15 (`middleware.ts`):**
42
+
43
+ ```ts
44
+ import {
45
+ deploymentProtectionMatcher,
46
+ withDeploymentProtection,
47
+ } from "@becklyn/deployment-protection";
48
+
49
+ export default withDeploymentProtection();
50
+
51
+ export const config = {
52
+ matcher: deploymentProtectionMatcher,
53
+ };
54
+ ```
55
+
56
+ Compose with existing middleware:
57
+
58
+ ```ts
59
+ export default withDeploymentProtection(async request => {
60
+ // your logic…
61
+ return NextResponse.next();
62
+ });
63
+ ```
64
+
65
+ ### 2. Environment variables
66
+
67
+ | Variable | Required | Default | Description |
68
+ | -------------------------------------------- | ----------------- | ------------------------- | ------------------------------------------------------ |
69
+ | `DEPLOYMENT_PROTECTION_ENABLED` | no | `true` | Set `false` / `0` / `off` to disable |
70
+ | `DEPLOYMENT_PROTECTION_USERNAME` | for password auth | — | Shared username |
71
+ | `DEPLOYMENT_PROTECTION_PASSWORD` | for password auth | — | Shared password |
72
+ | `DEPLOYMENT_PROTECTION_SECRET` | recommended | derived from user+pass | HMAC secret for session cookies |
73
+ | `VERCEL_AUTOMATION_BYPASS_SECRET` | no | — | Same secret as Vercel Protection Bypass for Automation |
74
+ | `DEPLOYMENT_PROTECTION_VERCEL_CLIENT_ID` | for Vercel login | — | Or `NEXT_PUBLIC_VERCEL_APP_CLIENT_ID` |
75
+ | `DEPLOYMENT_PROTECTION_VERCEL_CLIENT_SECRET` | for Vercel login | — | Or `VERCEL_APP_CLIENT_SECRET` |
76
+ | `DEPLOYMENT_PROTECTION_FORM_TITLE` | no | `Authentication required` | Login heading |
77
+ | `DEPLOYMENT_PROTECTION_FORM_DESCRIPTION` | no | … | Login description |
78
+
79
+ At least one of password auth, Vercel OAuth, or a bypass secret must be configured while enabled. If nothing is configured, the gate stays inactive (fail-open) so local dev is not bricked.
80
+
81
+ ### 3. Sign in with Vercel (optional)
82
+
83
+ 1. Create a [Sign in with Vercel](https://vercel.com/docs/sign-in-with-vercel) app in the Vercel dashboard.
84
+ 2. Generate a client secret.
85
+ 3. Add authorization callback URLs for each project host, pointing at:
86
+
87
+ ```text
88
+ https://<your-host>/_becklyn/deployment-protection/vercel/callback
89
+ http://localhost:3000/_becklyn/deployment-protection/vercel/callback
90
+ ```
91
+
92
+ You can attach the path to preview + production domains of each project from the dashboard.
93
+
94
+ 4. Set client id/secret env vars on the project(s). Prefer team-level shared env values.
95
+
96
+ Scopes used: `openid email profile`.
97
+
98
+ ### 4. Automation bypass
99
+
100
+ Matches Vercel’s Protection Bypass for Automation:
101
+
102
+ ```http
103
+ GET /api/health
104
+ x-vercel-protection-bypass: <VERCEL_AUTOMATION_BYPASS_SECRET>
105
+ ```
106
+
107
+ Or query:
108
+
109
+ ```text
110
+ https://app.example/?x-vercel-protection-bypass=<secret>&x-vercel-set-bypass-cookie=true
111
+ ```
112
+
113
+ When platform Deployment Protection is disabled, **this package** enforces the bypass secret so CI/Playwright keep working the same way.
114
+
115
+ ## Behaviour
116
+
117
+ 1. Disabled / misconfigured → pass through
118
+ 2. Internal Vercel OAuth routes → handled by the package
119
+ 3. Login form `POST` → validate username/password, set signed HttpOnly session cookie
120
+ 4. Valid bypass header/query/cookie → allow (optionally set cookies)
121
+ 5. Valid session cookie → allow
122
+ 6. Else → HTML login page (password and/or Sign in with Vercel)
123
+
124
+ Session cookie: `__becklyn_dp_session` (HMAC-SHA256, 14 days by default).
125
+
126
+ ## Disable without code changes
127
+
128
+ ```bash
129
+ DEPLOYMENT_PROTECTION_ENABLED=false
130
+ ```
131
+
132
+ Redeploy. Middleware stays installed but is a no-op.
133
+
134
+ ## API
135
+
136
+ ```ts
137
+ import {
138
+ deploymentProtectionMatcher,
139
+ handleDeploymentProtection,
140
+ resolveConfig,
141
+ withDeploymentProtection,
142
+ } from "@becklyn/deployment-protection";
143
+ ```
144
+
145
+ - `withDeploymentProtection(options?)` / `withDeploymentProtection(next, options?)` — middleware/proxy factory
146
+ - `handleDeploymentProtection(request, options?)` — framework-agnostic core (`null` = continue)
147
+ - `deploymentProtectionMatcher` — recommended matcher array
148
+
149
+ ## Security notes
150
+
151
+ - This is a **shared-secret gate**, not per-user product auth.
152
+ - Prefer a dedicated `DEPLOYMENT_PROTECTION_SECRET` in production.
153
+ - Turn off paid Vercel Password Protection / seat-heavy sharing once this is live; keep `VERCEL_AUTOMATION_BYPASS_SECRET` configured for tooling.
154
+ - Middleware is a convenience edge check — do not treat it as the only control for highly sensitive data.
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,5 @@
1
+ export declare function readBypassToken(request: Request): string | null;
2
+ export declare function readBypassCookie(request: Request): string | null;
3
+ export declare function isValidBypass(provided: string | null | undefined, secret: string | null): Promise<boolean>;
4
+ export declare function shouldSetBypassCookie(request: Request): boolean | "samesitenone";
5
+ //# sourceMappingURL=bypass.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bypass.d.ts","sourceRoot":"","sources":["../../src/bypass.ts"],"names":[],"mappings":"AAGA,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAe/D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAehE;AAED,wBAAsB,aAAa,CAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,MAAM,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,cAAc,CAehF"}
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readBypassToken = readBypassToken;
4
+ exports.readBypassCookie = readBypassCookie;
5
+ exports.isValidBypass = isValidBypass;
6
+ exports.shouldSetBypassCookie = shouldSetBypassCookie;
7
+ const constants_1 = require("./constants");
8
+ const crypto_1 = require("./crypto");
9
+ function readBypassToken(request) {
10
+ const header = request.headers.get(constants_1.BYPASS_HEADER)?.trim();
11
+ if (header) {
12
+ return header;
13
+ }
14
+ const url = new URL(request.url);
15
+ const query = url.searchParams.get(constants_1.BYPASS_HEADER)?.trim();
16
+ if (query) {
17
+ return query;
18
+ }
19
+ return null;
20
+ }
21
+ function readBypassCookie(request) {
22
+ const cookieHeader = request.headers.get("cookie");
23
+ if (!cookieHeader) {
24
+ return null;
25
+ }
26
+ for (const part of cookieHeader.split(";")) {
27
+ const [rawName, ...rest] = part.trim().split("=");
28
+ if (rawName === constants_1.BYPASS_COOKIE_NAME) {
29
+ return decodeURIComponent(rest.join("="));
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ async function isValidBypass(provided, secret) {
35
+ if (!provided || !secret) {
36
+ return false;
37
+ }
38
+ return (0, crypto_1.timingSafeEqualString)(provided, secret);
39
+ }
40
+ function shouldSetBypassCookie(request) {
41
+ const url = new URL(request.url);
42
+ const value = request.headers.get(constants_1.SET_BYPASS_COOKIE_HEADER) ??
43
+ url.searchParams.get(constants_1.SET_BYPASS_COOKIE_HEADER);
44
+ if (!value) {
45
+ return false;
46
+ }
47
+ if (value.toLowerCase() === "samesitenone") {
48
+ return "samesitenone";
49
+ }
50
+ return value === "true" || value === "1";
51
+ }
@@ -0,0 +1,13 @@
1
+ import type { DeploymentProtectionConfig, DeploymentProtectionOptions } from "./types";
2
+ /**
3
+ * Resolve runtime configuration from env (+ optional overrides).
4
+ */
5
+ export declare function resolveConfig(options?: DeploymentProtectionOptions): DeploymentProtectionConfig;
6
+ export declare function hasPasswordAuth(config: DeploymentProtectionConfig): boolean;
7
+ export declare function hasVercelAuth(config: DeploymentProtectionConfig): boolean;
8
+ /**
9
+ * Protection only runs when enabled and at least one auth method is configured.
10
+ * Misconfigured+enabled is treated as inactive (fail-open) so local/dev isn't bricked.
11
+ */
12
+ export declare function isProtectionActive(config: DeploymentProtectionConfig): boolean;
13
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,2BAA2B,EAAU,MAAM,SAAS,CAAC;AA0B/F;;GAEG;AACH,wBAAgB,aAAa,CACzB,OAAO,GAAE,2BAAgC,GAC1C,0BAA0B,CA6C5B;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAE3E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAEzE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAK9E"}
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveConfig = resolveConfig;
4
+ exports.hasPasswordAuth = hasPasswordAuth;
5
+ exports.hasVercelAuth = hasVercelAuth;
6
+ exports.isProtectionActive = isProtectionActive;
7
+ const FALSEY = new Set(["0", "false", "no", "off"]);
8
+ function read(env, ...keys) {
9
+ for (const key of keys) {
10
+ const value = env[key]?.trim();
11
+ if (value) {
12
+ return value;
13
+ }
14
+ }
15
+ return null;
16
+ }
17
+ function isEnabled(env) {
18
+ const raw = read(env, "DEPLOYMENT_PROTECTION_ENABLED");
19
+ if (!raw) {
20
+ return true;
21
+ }
22
+ return !FALSEY.has(raw.toLowerCase());
23
+ }
24
+ /**
25
+ * Resolve runtime configuration from env (+ optional overrides).
26
+ */
27
+ function resolveConfig(options = {}) {
28
+ const env = {
29
+ ...(typeof process !== "undefined" ? process.env : {}),
30
+ ...options.env,
31
+ };
32
+ const username = read(env, "DEPLOYMENT_PROTECTION_USERNAME");
33
+ const password = read(env, "DEPLOYMENT_PROTECTION_PASSWORD");
34
+ const explicitSecret = read(env, "DEPLOYMENT_PROTECTION_SECRET");
35
+ // Prefer an explicit secret; otherwise derive a stable key from credentials so
36
+ // projects can stay zero-config beyond username/password.
37
+ const secret = explicitSecret ?? (username && password ? `dp:${username}:${password}` : null);
38
+ return {
39
+ enabled: isEnabled(env),
40
+ username,
41
+ password,
42
+ secret,
43
+ bypassSecret: read(env, "VERCEL_AUTOMATION_BYPASS_SECRET", "DEPLOYMENT_PROTECTION_BYPASS_SECRET"),
44
+ vercelClientId: read(env, "DEPLOYMENT_PROTECTION_VERCEL_CLIENT_ID", "NEXT_PUBLIC_VERCEL_APP_CLIENT_ID", "VERCEL_APP_CLIENT_ID"),
45
+ vercelClientSecret: read(env, "DEPLOYMENT_PROTECTION_VERCEL_CLIENT_SECRET", "VERCEL_APP_CLIENT_SECRET"),
46
+ sessionTtlSeconds: options.sessionTtlSeconds ?? 60 * 60 * 24 * 14,
47
+ formTitle: options.form?.title ??
48
+ read(env, "DEPLOYMENT_PROTECTION_FORM_TITLE") ??
49
+ "Authentication required",
50
+ formDescription: options.form?.description ??
51
+ read(env, "DEPLOYMENT_PROTECTION_FORM_DESCRIPTION") ??
52
+ "Enter the shared credentials to continue.",
53
+ };
54
+ }
55
+ function hasPasswordAuth(config) {
56
+ return Boolean(config.username && config.password && config.secret);
57
+ }
58
+ function hasVercelAuth(config) {
59
+ return Boolean(config.vercelClientId && config.vercelClientSecret && config.secret);
60
+ }
61
+ /**
62
+ * Protection only runs when enabled and at least one auth method is configured.
63
+ * Misconfigured+enabled is treated as inactive (fail-open) so local/dev isn't bricked.
64
+ */
65
+ function isProtectionActive(config) {
66
+ return (config.enabled &&
67
+ (hasPasswordAuth(config) || hasVercelAuth(config) || Boolean(config.bypassSecret)));
68
+ }
@@ -0,0 +1,15 @@
1
+ export declare const SESSION_COOKIE_NAME = "__becklyn_dp_session";
2
+ export declare const BYPASS_COOKIE_NAME = "__becklyn_dp_bypass";
3
+ export declare const OAUTH_STATE_COOKIE = "__becklyn_dp_oauth_state";
4
+ export declare const OAUTH_NONCE_COOKIE = "__becklyn_dp_oauth_nonce";
5
+ export declare const OAUTH_VERIFIER_COOKIE = "__becklyn_dp_oauth_verifier";
6
+ export declare const OAUTH_RETURN_COOKIE = "__becklyn_dp_oauth_return";
7
+ export declare const INTERNAL_PATH_PREFIX = "/_becklyn/deployment-protection";
8
+ export declare const VERCEL_AUTHORIZE_PATH = "/_becklyn/deployment-protection/vercel";
9
+ export declare const VERCEL_CALLBACK_PATH = "/_becklyn/deployment-protection/vercel/callback";
10
+ export declare const BYPASS_HEADER = "x-vercel-protection-bypass";
11
+ export declare const SET_BYPASS_COOKIE_HEADER = "x-vercel-set-bypass-cookie";
12
+ export declare const DEFAULT_SESSION_TTL_SECONDS: number;
13
+ /** Recommended Next.js matcher for full-site protection. */
14
+ export declare const deploymentProtectionMatcher: string[];
15
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAC1D,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AACxD,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAC7D,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAC7D,eAAO,MAAM,qBAAqB,gCAAgC,CAAC;AACnE,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,eAAO,MAAM,oBAAoB,oCAAoC,CAAC;AACtE,eAAO,MAAM,qBAAqB,2CAAmC,CAAC;AACtE,eAAO,MAAM,oBAAoB,oDAA4C,CAAC;AAE9E,eAAO,MAAM,aAAa,+BAA+B,CAAC;AAC1D,eAAO,MAAM,wBAAwB,+BAA+B,CAAC;AAErE,eAAO,MAAM,2BAA2B,QAAoB,CAAC;AAE7D,4DAA4D;AAC5D,eAAO,MAAM,2BAA2B,UAEvC,CAAC"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deploymentProtectionMatcher = exports.DEFAULT_SESSION_TTL_SECONDS = exports.SET_BYPASS_COOKIE_HEADER = exports.BYPASS_HEADER = exports.VERCEL_CALLBACK_PATH = exports.VERCEL_AUTHORIZE_PATH = exports.INTERNAL_PATH_PREFIX = exports.OAUTH_RETURN_COOKIE = exports.OAUTH_VERIFIER_COOKIE = exports.OAUTH_NONCE_COOKIE = exports.OAUTH_STATE_COOKIE = exports.BYPASS_COOKIE_NAME = exports.SESSION_COOKIE_NAME = void 0;
4
+ exports.SESSION_COOKIE_NAME = "__becklyn_dp_session";
5
+ exports.BYPASS_COOKIE_NAME = "__becklyn_dp_bypass";
6
+ exports.OAUTH_STATE_COOKIE = "__becklyn_dp_oauth_state";
7
+ exports.OAUTH_NONCE_COOKIE = "__becklyn_dp_oauth_nonce";
8
+ exports.OAUTH_VERIFIER_COOKIE = "__becklyn_dp_oauth_verifier";
9
+ exports.OAUTH_RETURN_COOKIE = "__becklyn_dp_oauth_return";
10
+ exports.INTERNAL_PATH_PREFIX = "/_becklyn/deployment-protection";
11
+ exports.VERCEL_AUTHORIZE_PATH = `${exports.INTERNAL_PATH_PREFIX}/vercel`;
12
+ exports.VERCEL_CALLBACK_PATH = `${exports.INTERNAL_PATH_PREFIX}/vercel/callback`;
13
+ exports.BYPASS_HEADER = "x-vercel-protection-bypass";
14
+ exports.SET_BYPASS_COOKIE_HEADER = "x-vercel-set-bypass-cookie";
15
+ exports.DEFAULT_SESSION_TTL_SECONDS = 60 * 60 * 24 * 14; // 14 days
16
+ /** Recommended Next.js matcher for full-site protection. */
17
+ exports.deploymentProtectionMatcher = [
18
+ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|woff2?)$).*)",
19
+ ];
@@ -0,0 +1,7 @@
1
+ export declare function bytesToBase64Url(bytes: ArrayBuffer | Uint8Array): string;
2
+ export declare function base64UrlToBytes(value: string): Uint8Array;
3
+ export declare function sha256Base64Url(value: string): Promise<string>;
4
+ export declare function hmacSign(secret: string, payload: string): Promise<string>;
5
+ export declare function timingSafeEqualString(a: string, b: string): Promise<boolean>;
6
+ export declare function randomToken(byteLength?: number): string;
7
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,CASxE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAY1D;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGpE;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAU/E;AAED,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiBlF;AAED,wBAAgB,WAAW,CAAC,UAAU,SAAK,GAAG,MAAM,CAInD"}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bytesToBase64Url = bytesToBase64Url;
4
+ exports.base64UrlToBytes = base64UrlToBytes;
5
+ exports.sha256Base64Url = sha256Base64Url;
6
+ exports.hmacSign = hmacSign;
7
+ exports.timingSafeEqualString = timingSafeEqualString;
8
+ exports.randomToken = randomToken;
9
+ const textEncoder = new TextEncoder();
10
+ function bytesToBase64Url(bytes) {
11
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
12
+ let binary = "";
13
+ for (const byte of view) {
14
+ binary += String.fromCharCode(byte);
15
+ }
16
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
17
+ }
18
+ function base64UrlToBytes(value) {
19
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
20
+ const padLength = (4 - (padded.length % 4)) % 4;
21
+ const base64 = padded + "=".repeat(padLength);
22
+ const binary = atob(base64);
23
+ const bytes = new Uint8Array(binary.length);
24
+ for (let i = 0; i < binary.length; i++) {
25
+ bytes[i] = binary.charCodeAt(i);
26
+ }
27
+ return bytes;
28
+ }
29
+ async function sha256Base64Url(value) {
30
+ const digest = await crypto.subtle.digest("SHA-256", textEncoder.encode(value));
31
+ return bytesToBase64Url(digest);
32
+ }
33
+ async function hmacSign(secret, payload) {
34
+ const key = await crypto.subtle.importKey("raw", textEncoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
35
+ const signature = await crypto.subtle.sign("HMAC", key, textEncoder.encode(payload));
36
+ return bytesToBase64Url(signature);
37
+ }
38
+ async function timingSafeEqualString(a, b) {
39
+ const aBytes = textEncoder.encode(a);
40
+ const bBytes = textEncoder.encode(b);
41
+ if (aBytes.length !== bBytes.length) {
42
+ // Compare against itself to keep runtime roughly constant on length mismatch.
43
+ await crypto.subtle.digest("SHA-256", aBytes);
44
+ return false;
45
+ }
46
+ let mismatch = 0;
47
+ for (let i = 0; i < aBytes.length; i++) {
48
+ mismatch |= (aBytes[i] ?? 0) ^ (bBytes[i] ?? 0);
49
+ }
50
+ return mismatch === 0;
51
+ }
52
+ function randomToken(byteLength = 32) {
53
+ const bytes = new Uint8Array(byteLength);
54
+ crypto.getRandomValues(bytes);
55
+ return bytesToBase64Url(bytes);
56
+ }
@@ -0,0 +1,6 @@
1
+ import type { DeploymentProtectionOptions } from "./types";
2
+ /**
3
+ * Core request handler. Returns `null` when the request may continue to the app.
4
+ */
5
+ export declare function handleDeploymentProtection(request: Request, options?: DeploymentProtectionOptions): Promise<Response | null>;
6
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/handler.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAA8B,2BAA2B,EAAE,MAAM,SAAS,CAAC;AA2OvF;;GAEG;AACH,wBAAsB,0BAA0B,CAC5C,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAqD1B"}
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleDeploymentProtection = handleDeploymentProtection;
4
+ const bypass_1 = require("./bypass");
5
+ const config_1 = require("./config");
6
+ const constants_1 = require("./constants");
7
+ const crypto_1 = require("./crypto");
8
+ const login_page_1 = require("./login-page");
9
+ const password_1 = require("./password");
10
+ const safe_return_to_1 = require("./safe-return-to");
11
+ const session_1 = require("./session");
12
+ const vercel_oauth_1 = require("./vercel-oauth");
13
+ function isSecureRequest(request) {
14
+ return new URL(request.url).protocol === "https:";
15
+ }
16
+ function getPathname(request) {
17
+ return new URL(request.url).pathname;
18
+ }
19
+ function htmlResponse(html, status = 401) {
20
+ return new Response(html, {
21
+ status,
22
+ headers: {
23
+ "Content-Type": "text/html; charset=utf-8",
24
+ "Cache-Control": "no-store",
25
+ },
26
+ });
27
+ }
28
+ function redirect(location, status = 302) {
29
+ return new Response(null, {
30
+ status,
31
+ headers: {
32
+ Location: location,
33
+ "Cache-Control": "no-store",
34
+ },
35
+ });
36
+ }
37
+ async function attachSession(response, config, method, subject, secure) {
38
+ if (!config.secret) {
39
+ return response;
40
+ }
41
+ const token = await (0, session_1.createSessionToken)(config.secret, method, subject, config.sessionTtlSeconds);
42
+ const opts = (0, session_1.sessionCookieOptions)(config.sessionTtlSeconds, secure);
43
+ (0, vercel_oauth_1.appendSetCookie)(response, constants_1.SESSION_COOKIE_NAME, token, opts);
44
+ return response;
45
+ }
46
+ async function handleBypass(request, config) {
47
+ if (!config.bypassSecret) {
48
+ return { kind: "miss" };
49
+ }
50
+ const token = (0, bypass_1.readBypassToken)(request) ?? (0, bypass_1.readBypassCookie)(request);
51
+ if (!(await (0, bypass_1.isValidBypass)(token, config.bypassSecret))) {
52
+ return { kind: "miss" };
53
+ }
54
+ const setCookieMode = (0, bypass_1.shouldSetBypassCookie)(request);
55
+ const url = new URL(request.url);
56
+ const hadQueryBypass = url.searchParams.has(constants_1.BYPASS_HEADER);
57
+ const hadSetCookieQuery = url.searchParams.has("x-vercel-set-bypass-cookie");
58
+ if (hadQueryBypass || hadSetCookieQuery || setCookieMode) {
59
+ url.searchParams.delete(constants_1.BYPASS_HEADER);
60
+ url.searchParams.delete("x-vercel-set-bypass-cookie");
61
+ const response = redirect(url.pathname + url.search + url.hash);
62
+ const secure = isSecureRequest(request);
63
+ if (setCookieMode || hadSetCookieQuery) {
64
+ (0, vercel_oauth_1.appendSetCookie)(response, constants_1.BYPASS_COOKIE_NAME, config.bypassSecret, {
65
+ httpOnly: true,
66
+ secure,
67
+ path: "/",
68
+ maxAge: config.sessionTtlSeconds,
69
+ sameSite: setCookieMode === "samesitenone" ? "none" : "lax",
70
+ });
71
+ }
72
+ await attachSession(response, config, "bypass", "automation", secure);
73
+ return { kind: "respond", response };
74
+ }
75
+ return { kind: "allow" };
76
+ }
77
+ async function handlePasswordPost(request, config) {
78
+ if (request.method !== "POST" || !(0, config_1.hasPasswordAuth)(config)) {
79
+ return null;
80
+ }
81
+ const contentType = request.headers.get("content-type") ?? "";
82
+ if (!contentType.includes("application/x-www-form-urlencoded") &&
83
+ !contentType.includes("multipart/form-data")) {
84
+ return null;
85
+ }
86
+ let form;
87
+ try {
88
+ form = await request.formData();
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ if (form.get("__becklyn_dp") !== "1") {
94
+ return null;
95
+ }
96
+ const username = String(form.get("username") ?? "");
97
+ const password = String(form.get("password") ?? "");
98
+ const returnTo = (0, safe_return_to_1.safeReturnTo)(String(form.get("return_to") ?? getPathname(request)));
99
+ if (!(await (0, password_1.validatePasswordCredentials)(config, username, password))) {
100
+ return htmlResponse((0, login_page_1.renderLoginPage)(config, {
101
+ error: "Invalid username or password.",
102
+ returnTo,
103
+ showPassword: true,
104
+ showVercel: (0, config_1.hasVercelAuth)(config),
105
+ }));
106
+ }
107
+ const response = redirect(returnTo);
108
+ return attachSession(response, config, "password", username, isSecureRequest(request));
109
+ }
110
+ async function handleVercelAuthorize(request, config) {
111
+ if (getPathname(request) !== constants_1.VERCEL_AUTHORIZE_PATH) {
112
+ return null;
113
+ }
114
+ if (!(0, config_1.hasVercelAuth)(config)) {
115
+ return new Response("Sign in with Vercel is not configured", { status: 500 });
116
+ }
117
+ const returnTo = (0, safe_return_to_1.safeReturnTo)(new URL(request.url).searchParams.get("return_to"));
118
+ return (0, vercel_oauth_1.buildVercelAuthorizeRedirect)(request, config, returnTo);
119
+ }
120
+ async function handleVercelCallback(request, config) {
121
+ if (getPathname(request) !== constants_1.VERCEL_CALLBACK_PATH) {
122
+ return null;
123
+ }
124
+ if (!(0, config_1.hasVercelAuth)(config) || !config.secret) {
125
+ return new Response("Sign in with Vercel is not configured", { status: 500 });
126
+ }
127
+ const url = new URL(request.url);
128
+ const code = url.searchParams.get("code");
129
+ const state = url.searchParams.get("state");
130
+ const secure = isSecureRequest(request);
131
+ const returnTo = (0, safe_return_to_1.safeReturnTo)((0, vercel_oauth_1.readCookie)(request, constants_1.OAUTH_RETURN_COOKIE));
132
+ if (!code || !(await (0, vercel_oauth_1.assertOAuthState)(request, state))) {
133
+ const response = htmlResponse((0, login_page_1.renderLoginPage)(config, {
134
+ error: "Vercel sign-in failed (invalid state).",
135
+ returnTo,
136
+ showPassword: (0, config_1.hasPasswordAuth)(config),
137
+ showVercel: true,
138
+ }));
139
+ (0, vercel_oauth_1.clearOAuthCookies)(response, secure);
140
+ return response;
141
+ }
142
+ try {
143
+ const codeVerifier = (0, vercel_oauth_1.readCookie)(request, constants_1.OAUTH_VERIFIER_COOKIE);
144
+ if (!codeVerifier) {
145
+ throw new Error("Missing PKCE verifier");
146
+ }
147
+ const tokenData = await (0, vercel_oauth_1.exchangeVercelCode)(request, config, code, codeVerifier);
148
+ const storedNonce = (0, vercel_oauth_1.readCookie)(request, constants_1.OAUTH_NONCE_COOKIE);
149
+ const tokenNonce = (0, vercel_oauth_1.decodeIdTokenNonce)(tokenData.id_token);
150
+ if (storedNonce && tokenNonce && !(await (0, crypto_1.timingSafeEqualString)(storedNonce, tokenNonce))) {
151
+ throw new Error("Nonce mismatch");
152
+ }
153
+ const user = await (0, vercel_oauth_1.fetchVercelUserInfo)(tokenData.access_token);
154
+ const subject = user.preferred_username || user.email || user.sub || "vercel-user";
155
+ const response = redirect(returnTo);
156
+ (0, vercel_oauth_1.clearOAuthCookies)(response, secure);
157
+ return attachSession(response, config, "vercel", subject, secure);
158
+ }
159
+ catch {
160
+ const response = htmlResponse((0, login_page_1.renderLoginPage)(config, {
161
+ error: "Vercel sign-in failed.",
162
+ returnTo,
163
+ showPassword: (0, config_1.hasPasswordAuth)(config),
164
+ showVercel: true,
165
+ }));
166
+ (0, vercel_oauth_1.clearOAuthCookies)(response, secure);
167
+ return response;
168
+ }
169
+ }
170
+ /**
171
+ * Core request handler. Returns `null` when the request may continue to the app.
172
+ */
173
+ async function handleDeploymentProtection(request, options = {}) {
174
+ const config = (0, config_1.resolveConfig)(options);
175
+ if (!(0, config_1.isProtectionActive)(config)) {
176
+ return null;
177
+ }
178
+ const vercelAuthorize = await handleVercelAuthorize(request, config);
179
+ if (vercelAuthorize) {
180
+ return vercelAuthorize;
181
+ }
182
+ const vercelCallback = await handleVercelCallback(request, config);
183
+ if (vercelCallback) {
184
+ return vercelCallback;
185
+ }
186
+ const passwordResponse = await handlePasswordPost(request, config);
187
+ if (passwordResponse) {
188
+ return passwordResponse;
189
+ }
190
+ const bypass = await handleBypass(request, config);
191
+ if (bypass.kind === "respond") {
192
+ return bypass.response;
193
+ }
194
+ if (bypass.kind === "allow") {
195
+ return null;
196
+ }
197
+ if (config.secret) {
198
+ const session = await (0, session_1.verifySessionToken)(config.secret, (0, vercel_oauth_1.readCookie)(request, constants_1.SESSION_COOKIE_NAME));
199
+ if (session) {
200
+ return null;
201
+ }
202
+ }
203
+ if (!(0, config_1.hasPasswordAuth)(config) && !(0, config_1.hasVercelAuth)(config)) {
204
+ return new Response("Unauthorized", { status: 401 });
205
+ }
206
+ const returnTo = (0, safe_return_to_1.safeReturnTo)(getPathname(request) + new URL(request.url).search);
207
+ return htmlResponse((0, login_page_1.renderLoginPage)(config, {
208
+ returnTo,
209
+ showPassword: (0, config_1.hasPasswordAuth)(config),
210
+ showVercel: (0, config_1.hasVercelAuth)(config),
211
+ }));
212
+ }