@becklyn/deployment-protection 0.2.0 → 0.4.0

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 (58) hide show
  1. package/README.md +129 -16
  2. package/dist/cjs/auth-proxy.d.ts +15 -0
  3. package/dist/cjs/auth-proxy.d.ts.map +1 -0
  4. package/dist/cjs/auth-proxy.js +135 -0
  5. package/dist/cjs/config.d.ts +7 -0
  6. package/dist/cjs/config.d.ts.map +1 -1
  7. package/dist/cjs/config.js +20 -1
  8. package/dist/cjs/constants.d.ts +1 -0
  9. package/dist/cjs/constants.d.ts.map +1 -1
  10. package/dist/cjs/constants.js +2 -1
  11. package/dist/cjs/edge.d.ts +14 -0
  12. package/dist/cjs/edge.d.ts.map +1 -0
  13. package/dist/cjs/edge.js +32 -0
  14. package/dist/cjs/handler.d.ts.map +1 -1
  15. package/dist/cjs/handler.js +38 -1
  16. package/dist/cjs/handoff.d.ts +46 -0
  17. package/dist/cjs/handoff.d.ts.map +1 -0
  18. package/dist/cjs/handoff.js +199 -0
  19. package/dist/cjs/index.d.ts +3 -1
  20. package/dist/cjs/index.d.ts.map +1 -1
  21. package/dist/cjs/index.js +11 -1
  22. package/dist/cjs/storybook.d.ts +33 -0
  23. package/dist/cjs/storybook.d.ts.map +1 -0
  24. package/dist/cjs/storybook.js +36 -0
  25. package/dist/cjs/types.d.ts +15 -0
  26. package/dist/cjs/types.d.ts.map +1 -1
  27. package/dist/cjs/vercel-oauth.d.ts +9 -2
  28. package/dist/cjs/vercel-oauth.d.ts.map +1 -1
  29. package/dist/cjs/vercel-oauth.js +8 -4
  30. package/dist/es/auth-proxy.d.ts +15 -0
  31. package/dist/es/auth-proxy.d.ts.map +1 -0
  32. package/dist/es/auth-proxy.js +130 -0
  33. package/dist/es/config.d.ts +7 -0
  34. package/dist/es/config.d.ts.map +1 -1
  35. package/dist/es/config.js +18 -1
  36. package/dist/es/constants.d.ts +1 -0
  37. package/dist/es/constants.d.ts.map +1 -1
  38. package/dist/es/constants.js +1 -0
  39. package/dist/es/edge.d.ts +14 -0
  40. package/dist/es/edge.d.ts.map +1 -0
  41. package/dist/es/edge.js +28 -0
  42. package/dist/es/handler.d.ts.map +1 -1
  43. package/dist/es/handler.js +39 -2
  44. package/dist/es/handoff.d.ts +46 -0
  45. package/dist/es/handoff.d.ts.map +1 -0
  46. package/dist/es/handoff.js +187 -0
  47. package/dist/es/index.d.ts +3 -1
  48. package/dist/es/index.d.ts.map +1 -1
  49. package/dist/es/index.js +3 -1
  50. package/dist/es/storybook.d.ts +33 -0
  51. package/dist/es/storybook.d.ts.map +1 -0
  52. package/dist/es/storybook.js +31 -0
  53. package/dist/es/types.d.ts +15 -0
  54. package/dist/es/types.d.ts.map +1 -1
  55. package/dist/es/vercel-oauth.d.ts +9 -2
  56. package/dist/es/vercel-oauth.d.ts.map +1 -1
  57. package/dist/es/vercel-oauth.js +9 -5
  58. package/package.json +19 -2
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROXY_START_TTL_SECONDS = exports.HANDOFF_TTL_SECONDS = void 0;
4
+ exports.canonicalProxyStartPayload = canonicalProxyStartPayload;
5
+ exports.signProxyStart = signProxyStart;
6
+ exports.verifyProxyStartSignature = verifyProxyStartSignature;
7
+ exports.buildProxyStartUrl = buildProxyStartUrl;
8
+ exports.createHandoffToken = createHandoffToken;
9
+ exports.verifyHandoffToken = verifyHandoffToken;
10
+ exports.normalizeReturnOrigin = normalizeReturnOrigin;
11
+ exports.isReturnOriginAllowed = isReturnOriginAllowed;
12
+ exports.parseAllowlist = parseAllowlist;
13
+ const crypto_1 = require("./crypto");
14
+ const safe_return_to_1 = require("./safe-return-to");
15
+ exports.HANDOFF_TTL_SECONDS = 60;
16
+ exports.PROXY_START_TTL_SECONDS = 5 * 60;
17
+ function encodeJson(value) {
18
+ return (0, crypto_1.bytesToBase64Url)(new TextEncoder().encode(JSON.stringify(value)));
19
+ }
20
+ function decodeJson(encoded) {
21
+ try {
22
+ const json = new TextDecoder().decode((0, crypto_1.base64UrlToBytes)(encoded));
23
+ return JSON.parse(json);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Canonical payload for HMAC over the proxy start request.
31
+ * Field order is fixed so app and proxy produce the same signature.
32
+ */
33
+ function canonicalProxyStartPayload(params) {
34
+ return `v1\n${params.returnOrigin}\n${params.returnPath}\n${params.exp}\n${params.nonce}`;
35
+ }
36
+ async function signProxyStart(secret, params) {
37
+ return (0, crypto_1.hmacSign)(secret, canonicalProxyStartPayload(params));
38
+ }
39
+ async function verifyProxyStartSignature(secret, params, signature) {
40
+ if (!signature) {
41
+ return false;
42
+ }
43
+ if (params.exp < Math.floor(Date.now() / 1000)) {
44
+ return false;
45
+ }
46
+ const expected = await signProxyStart(secret, params);
47
+ return (0, crypto_1.timingSafeEqualString)(signature, expected);
48
+ }
49
+ async function buildProxyStartUrl(options) {
50
+ const returnPath = (0, safe_return_to_1.safeReturnTo)(options.returnPath);
51
+ const params = {
52
+ returnOrigin: options.returnOrigin.replace(/\/$/, ""),
53
+ returnPath,
54
+ exp: Math.floor(Date.now() / 1000) + (options.ttlSeconds ?? exports.PROXY_START_TTL_SECONDS),
55
+ nonce: (0, crypto_1.randomToken)(16),
56
+ };
57
+ const sig = await signProxyStart(options.secret, params);
58
+ const url = new URL("/start", ensureTrailingSlashBase(options.authProxyUrl));
59
+ url.searchParams.set("return_origin", params.returnOrigin);
60
+ url.searchParams.set("return_path", params.returnPath);
61
+ url.searchParams.set("exp", String(params.exp));
62
+ url.searchParams.set("nonce", params.nonce);
63
+ url.searchParams.set("sig", sig);
64
+ return url.toString();
65
+ }
66
+ function ensureTrailingSlashBase(value) {
67
+ try {
68
+ const url = new URL(value);
69
+ return url.toString().endsWith("/") ? url.toString() : `${url.toString()}/`;
70
+ }
71
+ catch {
72
+ return value.endsWith("/") ? value : `${value}/`;
73
+ }
74
+ }
75
+ async function createHandoffToken(secret, audienceOrigin, subject, ttlSeconds = exports.HANDOFF_TTL_SECONDS) {
76
+ const payload = {
77
+ exp: Math.floor(Date.now() / 1000) + ttlSeconds,
78
+ aud: audienceOrigin.replace(/\/$/, ""),
79
+ subject,
80
+ method: "vercel",
81
+ };
82
+ const body = encodeJson(payload);
83
+ const signature = await (0, crypto_1.hmacSign)(secret, body);
84
+ return `${body}.${signature}`;
85
+ }
86
+ async function verifyHandoffToken(secret, token, expectedAudienceOrigin) {
87
+ if (!token) {
88
+ return null;
89
+ }
90
+ const [body, signature] = token.split(".");
91
+ if (!body || !signature) {
92
+ return null;
93
+ }
94
+ const expected = await (0, crypto_1.hmacSign)(secret, body);
95
+ if (!(await (0, crypto_1.timingSafeEqualString)(signature, expected))) {
96
+ return null;
97
+ }
98
+ const payload = decodeJson(body);
99
+ if (!payload ||
100
+ typeof payload.exp !== "number" ||
101
+ typeof payload.aud !== "string" ||
102
+ typeof payload.subject !== "string" ||
103
+ payload.method !== "vercel") {
104
+ return null;
105
+ }
106
+ if (payload.exp < Math.floor(Date.now() / 1000)) {
107
+ return null;
108
+ }
109
+ const expectedAud = expectedAudienceOrigin.replace(/\/$/, "");
110
+ if (!(await (0, crypto_1.timingSafeEqualString)(payload.aud, expectedAud))) {
111
+ return null;
112
+ }
113
+ return payload;
114
+ }
115
+ /**
116
+ * Validate a post-login return origin for the auth proxy.
117
+ * Only https (and http://localhost / 127.0.0.1) are accepted.
118
+ */
119
+ function normalizeReturnOrigin(value) {
120
+ if (typeof value !== "string" || value.length === 0 || value.length > 512) {
121
+ return null;
122
+ }
123
+ let url;
124
+ try {
125
+ url = new URL(value);
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ if (url.username || url.password) {
131
+ return null;
132
+ }
133
+ const host = url.hostname.toLowerCase();
134
+ const isLocalHttp = url.protocol === "http:" &&
135
+ (host === "localhost" || host === "127.0.0.1" || host === "[::1]");
136
+ if (url.protocol !== "https:" && !isLocalHttp) {
137
+ return null;
138
+ }
139
+ // Origin only — reject unexpected path/query/hash noise by normalizing.
140
+ return url.origin;
141
+ }
142
+ /**
143
+ * Optional allowlist. Entries may be:
144
+ * - full origins (`https://app.example.com`)
145
+ * - hostname suffixes (`.vercel.app`, `example.com`)
146
+ * - `localhost` (any localhost / 127.0.0.1 http(s) origin)
147
+ *
148
+ * Empty allowlist → any origin that passes {@link normalizeReturnOrigin}.
149
+ */
150
+ function isReturnOriginAllowed(origin, allowlist) {
151
+ if (allowlist.length === 0) {
152
+ return true;
153
+ }
154
+ let url;
155
+ try {
156
+ url = new URL(origin);
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ const host = url.hostname.toLowerCase();
162
+ for (const raw of allowlist) {
163
+ const entry = raw.trim().toLowerCase();
164
+ if (!entry) {
165
+ continue;
166
+ }
167
+ if (entry === "localhost") {
168
+ if (host === "localhost" || host === "127.0.0.1" || host === "[::1]") {
169
+ return true;
170
+ }
171
+ continue;
172
+ }
173
+ if (entry.startsWith("http://") || entry.startsWith("https://")) {
174
+ try {
175
+ if (new URL(entry).origin.toLowerCase() === origin.toLowerCase()) {
176
+ return true;
177
+ }
178
+ }
179
+ catch {
180
+ // ignore invalid allowlist entries
181
+ }
182
+ continue;
183
+ }
184
+ const suffix = entry.startsWith(".") ? entry : `.${entry}`;
185
+ if (host === entry || host === entry.replace(/^\./, "") || host.endsWith(suffix)) {
186
+ return true;
187
+ }
188
+ }
189
+ return false;
190
+ }
191
+ function parseAllowlist(value) {
192
+ if (!value) {
193
+ return [];
194
+ }
195
+ return value
196
+ .split(",")
197
+ .map(part => part.trim())
198
+ .filter(Boolean);
199
+ }
@@ -1,6 +1,8 @@
1
1
  export { INTERNAL_PATH_PREFIX, VERCEL_AUTHORIZE_PATH, VERCEL_CALLBACK_PATH } from "./constants";
2
+ export { AUTH_PROXY_CALLBACK_PATH, AUTH_PROXY_START_PATH, handleAuthProxyCallback, handleAuthProxyStart, } from "./auth-proxy";
2
3
  export { handleDeploymentProtection } from "./handler";
3
4
  export { withDeploymentProtection } from "./middleware";
4
- export { resolveConfig, hasPasswordAuth, hasVercelAuth, isProtectionActive } from "./config";
5
+ export { middlewarePassThrough, withEdgeDeploymentProtection } from "./edge";
6
+ export { resolveConfig, hasPasswordAuth, hasVercelAuth, hasVercelDirectAuth, hasVercelProxyAuth, isProtectionActive, } from "./config";
5
7
  export type { AuthMethod, DeploymentProtectionConfig, DeploymentProtectionOptions, FormOptions, SessionPayload, } from "./types";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAChG,OAAO,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC7F,YAAY,EACR,UAAU,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,WAAW,EACX,cAAc,GACjB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAChG,OAAO,EACH,wBAAwB,EACxB,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,QAAQ,CAAC;AAC7E,OAAO,EACH,aAAa,EACb,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,GACrB,MAAM,UAAU,CAAC;AAClB,YAAY,EACR,UAAU,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,WAAW,EACX,cAAc,GACjB,MAAM,SAAS,CAAC"}
package/dist/cjs/index.js CHANGED
@@ -1,16 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isProtectionActive = exports.hasVercelAuth = exports.hasPasswordAuth = exports.resolveConfig = exports.withDeploymentProtection = exports.handleDeploymentProtection = exports.VERCEL_CALLBACK_PATH = exports.VERCEL_AUTHORIZE_PATH = exports.INTERNAL_PATH_PREFIX = void 0;
3
+ exports.isProtectionActive = exports.hasVercelProxyAuth = exports.hasVercelDirectAuth = exports.hasVercelAuth = exports.hasPasswordAuth = exports.resolveConfig = exports.withEdgeDeploymentProtection = exports.middlewarePassThrough = exports.withDeploymentProtection = exports.handleDeploymentProtection = exports.handleAuthProxyStart = exports.handleAuthProxyCallback = exports.AUTH_PROXY_START_PATH = exports.AUTH_PROXY_CALLBACK_PATH = exports.VERCEL_CALLBACK_PATH = exports.VERCEL_AUTHORIZE_PATH = exports.INTERNAL_PATH_PREFIX = void 0;
4
4
  var constants_1 = require("./constants");
5
5
  Object.defineProperty(exports, "INTERNAL_PATH_PREFIX", { enumerable: true, get: function () { return constants_1.INTERNAL_PATH_PREFIX; } });
6
6
  Object.defineProperty(exports, "VERCEL_AUTHORIZE_PATH", { enumerable: true, get: function () { return constants_1.VERCEL_AUTHORIZE_PATH; } });
7
7
  Object.defineProperty(exports, "VERCEL_CALLBACK_PATH", { enumerable: true, get: function () { return constants_1.VERCEL_CALLBACK_PATH; } });
8
+ var auth_proxy_1 = require("./auth-proxy");
9
+ Object.defineProperty(exports, "AUTH_PROXY_CALLBACK_PATH", { enumerable: true, get: function () { return auth_proxy_1.AUTH_PROXY_CALLBACK_PATH; } });
10
+ Object.defineProperty(exports, "AUTH_PROXY_START_PATH", { enumerable: true, get: function () { return auth_proxy_1.AUTH_PROXY_START_PATH; } });
11
+ Object.defineProperty(exports, "handleAuthProxyCallback", { enumerable: true, get: function () { return auth_proxy_1.handleAuthProxyCallback; } });
12
+ Object.defineProperty(exports, "handleAuthProxyStart", { enumerable: true, get: function () { return auth_proxy_1.handleAuthProxyStart; } });
8
13
  var handler_1 = require("./handler");
9
14
  Object.defineProperty(exports, "handleDeploymentProtection", { enumerable: true, get: function () { return handler_1.handleDeploymentProtection; } });
10
15
  var middleware_1 = require("./middleware");
11
16
  Object.defineProperty(exports, "withDeploymentProtection", { enumerable: true, get: function () { return middleware_1.withDeploymentProtection; } });
17
+ var edge_1 = require("./edge");
18
+ Object.defineProperty(exports, "middlewarePassThrough", { enumerable: true, get: function () { return edge_1.middlewarePassThrough; } });
19
+ Object.defineProperty(exports, "withEdgeDeploymentProtection", { enumerable: true, get: function () { return edge_1.withEdgeDeploymentProtection; } });
12
20
  var config_1 = require("./config");
13
21
  Object.defineProperty(exports, "resolveConfig", { enumerable: true, get: function () { return config_1.resolveConfig; } });
14
22
  Object.defineProperty(exports, "hasPasswordAuth", { enumerable: true, get: function () { return config_1.hasPasswordAuth; } });
15
23
  Object.defineProperty(exports, "hasVercelAuth", { enumerable: true, get: function () { return config_1.hasVercelAuth; } });
24
+ Object.defineProperty(exports, "hasVercelDirectAuth", { enumerable: true, get: function () { return config_1.hasVercelDirectAuth; } });
25
+ Object.defineProperty(exports, "hasVercelProxyAuth", { enumerable: true, get: function () { return config_1.hasVercelProxyAuth; } });
16
26
  Object.defineProperty(exports, "isProtectionActive", { enumerable: true, get: function () { return config_1.isProtectionActive; } });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Storybook deployment protection for Vercel.
3
+ *
4
+ * Built Storybooks are static sites, so protection runs as **Vercel Edge Middleware**
5
+ * (not Storybook config). Drop a `middleware.ts` next to the Vercel project root that
6
+ * serves `storybook-static` and set the same env vars as the Next.js integration.
7
+ *
8
+ * @example middleware.ts
9
+ * ```ts
10
+ * import { withStorybookDeploymentProtection } from "@becklyn/deployment-protection/storybook";
11
+ *
12
+ * export default withStorybookDeploymentProtection();
13
+ *
14
+ * // Matcher must be a string literal in this file (Vercel/Next static analysis).
15
+ * export const config = {
16
+ * matcher: [
17
+ * "/((?!.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|woff2?|mjs)$).*)",
18
+ * ],
19
+ * };
20
+ * ```
21
+ *
22
+ * Optional `vercel.json` for a Storybook-only project:
23
+ * ```json
24
+ * {
25
+ * "buildCommand": "npm run build-storybook",
26
+ * "outputDirectory": "storybook-static",
27
+ * "framework": null
28
+ * }
29
+ * ```
30
+ */
31
+ export { middlewarePassThrough, withEdgeDeploymentProtection as withStorybookDeploymentProtection, } from "./edge";
32
+ export type { DeploymentProtectionOptions } from "./types";
33
+ //# sourceMappingURL=storybook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storybook.d.ts","sourceRoot":"","sources":["../../src/storybook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EACH,qBAAqB,EACrB,4BAA4B,IAAI,iCAAiC,GACpE,MAAM,QAAQ,CAAC;AAChB,YAAY,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withStorybookDeploymentProtection = exports.middlewarePassThrough = void 0;
4
+ /**
5
+ * Storybook deployment protection for Vercel.
6
+ *
7
+ * Built Storybooks are static sites, so protection runs as **Vercel Edge Middleware**
8
+ * (not Storybook config). Drop a `middleware.ts` next to the Vercel project root that
9
+ * serves `storybook-static` and set the same env vars as the Next.js integration.
10
+ *
11
+ * @example middleware.ts
12
+ * ```ts
13
+ * import { withStorybookDeploymentProtection } from "@becklyn/deployment-protection/storybook";
14
+ *
15
+ * export default withStorybookDeploymentProtection();
16
+ *
17
+ * // Matcher must be a string literal in this file (Vercel/Next static analysis).
18
+ * export const config = {
19
+ * matcher: [
20
+ * "/((?!.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|woff2?|mjs)$).*)",
21
+ * ],
22
+ * };
23
+ * ```
24
+ *
25
+ * Optional `vercel.json` for a Storybook-only project:
26
+ * ```json
27
+ * {
28
+ * "buildCommand": "npm run build-storybook",
29
+ * "outputDirectory": "storybook-static",
30
+ * "framework": null
31
+ * }
32
+ * ```
33
+ */
34
+ var edge_1 = require("./edge");
35
+ Object.defineProperty(exports, "middlewarePassThrough", { enumerable: true, get: function () { return edge_1.middlewarePassThrough; } });
36
+ Object.defineProperty(exports, "withStorybookDeploymentProtection", { enumerable: true, get: function () { return edge_1.withEdgeDeploymentProtection; } });
@@ -4,7 +4,22 @@ export interface DeploymentProtectionConfig {
4
4
  username: string | null;
5
5
  password: string | null;
6
6
  secret: string | null;
7
+ /**
8
+ * Shared secret used to sign proxy start requests and verify handoff tokens.
9
+ * Falls back to {@link secret} when unset.
10
+ */
11
+ handoffSecret: string | null;
7
12
  bypassSecret: string | null;
13
+ /**
14
+ * Base URL of the central auth-proxy app (e.g. https://dp-auth.example.com).
15
+ * When set, protected apps no longer need per-host Vercel OAuth callback URLs.
16
+ */
17
+ authProxyUrl: string | null;
18
+ /**
19
+ * Optional allowlist for auth-proxy return origins (proxy side).
20
+ * See README for entry formats.
21
+ */
22
+ allowedReturnOrigins: string[];
8
23
  vercelClientId: string | null;
9
24
  vercelClientSecret: string | null;
10
25
  sessionTtlSeconds: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,0BAA0B;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA2B;IACxC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,0BAA0B;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;;OAGG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA2B;IACxC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC"}
@@ -13,8 +13,15 @@ export interface VercelUserInfo {
13
13
  email?: string;
14
14
  preferred_username?: string;
15
15
  }
16
- export declare function buildVercelAuthorizeRedirect(request: Request, config: DeploymentProtectionConfig, returnTo: string): Promise<Response>;
17
- export declare function exchangeVercelCode(request: Request, config: DeploymentProtectionConfig, code: string, codeVerifier: string): Promise<VercelTokenResponse>;
16
+ export interface VercelOAuthPathOptions {
17
+ /**
18
+ * Absolute path used as the OAuth redirect_uri path on the current origin.
19
+ * Defaults to the protected-app callback path. Auth-proxy uses `/callback`.
20
+ */
21
+ callbackPath?: string;
22
+ }
23
+ export declare function buildVercelAuthorizeRedirect(request: Request, config: DeploymentProtectionConfig, returnTo: string, options?: VercelOAuthPathOptions): Promise<Response>;
24
+ export declare function exchangeVercelCode(request: Request, config: DeploymentProtectionConfig, code: string, codeVerifier: string, options?: VercelOAuthPathOptions): Promise<VercelTokenResponse>;
18
25
  export declare function fetchVercelUserInfo(accessToken: string): Promise<VercelUserInfo>;
19
26
  export declare function readCookie(request: Request, name: string): string | null;
20
27
  export declare function assertOAuthState(request: Request, state: string | null): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"vercel-oauth.d.ts","sourceRoot":"","sources":["../../src/vercel-oauth.ts"],"names":[],"mappings":"AAQA,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;AAYD,wBAAsB,4BAA4B,CAC9C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,QAAQ,EAAE,MAAM,GACjB,OAAO,CAAC,QAAQ,CAAC,CAsCnB;AAED,wBAAsB,kBAAkB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,0BAA0B,EAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACrB,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,CAiB3E;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;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"}
@@ -19,7 +19,10 @@ function cookieOptions(secure) {
19
19
  maxAge: 10 * 60,
20
20
  };
21
21
  }
22
- async function buildVercelAuthorizeRedirect(request, config, returnTo) {
22
+ function resolveCallbackPath(options) {
23
+ return options?.callbackPath ?? constants_1.VERCEL_CALLBACK_PATH;
24
+ }
25
+ async function buildVercelAuthorizeRedirect(request, config, returnTo, options) {
23
26
  if (!config.vercelClientId) {
24
27
  return new Response("Vercel OAuth is not configured", { status: 500 });
25
28
  }
@@ -28,7 +31,7 @@ async function buildVercelAuthorizeRedirect(request, config, returnTo) {
28
31
  const codeVerifier = (0, crypto_1.randomToken)(48);
29
32
  const codeChallenge = await (0, crypto_1.sha256Base64Url)(codeVerifier);
30
33
  const origin = new URL(request.url).origin;
31
- const redirectUri = `${origin}${constants_1.VERCEL_CALLBACK_PATH}`;
34
+ const redirectUri = `${origin}${resolveCallbackPath(options)}`;
32
35
  const secure = origin.startsWith("https://");
33
36
  const params = new URLSearchParams({
34
37
  client_id: config.vercelClientId,
@@ -53,7 +56,7 @@ async function buildVercelAuthorizeRedirect(request, config, returnTo) {
53
56
  appendSetCookie(response, constants_1.OAUTH_RETURN_COOKIE, returnTo || "/", opts);
54
57
  return response;
55
58
  }
56
- async function exchangeVercelCode(request, config, code, codeVerifier) {
59
+ async function exchangeVercelCode(request, config, code, codeVerifier, options) {
57
60
  if (!config.vercelClientId || !config.vercelClientSecret) {
58
61
  throw new Error("Vercel OAuth is not configured");
59
62
  }
@@ -64,7 +67,7 @@ async function exchangeVercelCode(request, config, code, codeVerifier) {
64
67
  client_secret: config.vercelClientSecret,
65
68
  code,
66
69
  code_verifier: codeVerifier,
67
- redirect_uri: `${origin}${constants_1.VERCEL_CALLBACK_PATH}`,
70
+ redirect_uri: `${origin}${resolveCallbackPath(options)}`,
68
71
  });
69
72
  const response = await fetch("https://api.vercel.com/login/oauth/token", {
70
73
  method: "POST",
@@ -143,6 +146,7 @@ function clearOAuthCookies(response, secure) {
143
146
  constants_1.OAUTH_NONCE_COOKIE,
144
147
  constants_1.OAUTH_VERIFIER_COOKIE,
145
148
  constants_1.OAUTH_RETURN_COOKIE,
149
+ constants_1.OAUTH_RETURN_ORIGIN_COOKIE,
146
150
  ]) {
147
151
  appendSetCookie(response, name, "", expired);
148
152
  }
@@ -0,0 +1,15 @@
1
+ import type { DeploymentProtectionOptions } from "./types";
2
+ /** Fixed OAuth callback path registered once on the Vercel OAuth app. */
3
+ export declare const AUTH_PROXY_CALLBACK_PATH = "/callback";
4
+ export declare const AUTH_PROXY_START_PATH = "/start";
5
+ /**
6
+ * Auth-proxy entry: verify the signed start request from a protected app, then
7
+ * begin Sign in with Vercel against this proxy's fixed callback URL.
8
+ */
9
+ export declare function handleAuthProxyStart(request: Request, options?: DeploymentProtectionOptions): Promise<Response>;
10
+ /**
11
+ * Auth-proxy Vercel OAuth callback: exchange the code, mint a short-lived handoff
12
+ * token, and redirect back to the protected app callback.
13
+ */
14
+ export declare function handleAuthProxyCallback(request: Request, options?: DeploymentProtectionOptions): Promise<Response>;
15
+ //# sourceMappingURL=auth-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-proxy.d.ts","sourceRoot":"","sources":["../../src/auth-proxy.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAY3D,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,cAAc,CAAC;AACpD,eAAO,MAAM,qBAAqB,WAAW,CAAC;AA0B9C;;;GAGG;AACH,wBAAsB,oBAAoB,CACtC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,CAAC,CAwCnB;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CACzC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,CAAC,CAqEnB"}
@@ -0,0 +1,130 @@
1
+ import { hasVercelDirectAuth, resolveConfig } from "./config";
2
+ import { OAUTH_NONCE_COOKIE, OAUTH_RETURN_COOKIE, OAUTH_RETURN_ORIGIN_COOKIE, OAUTH_VERIFIER_COOKIE, VERCEL_CALLBACK_PATH, } from "./constants";
3
+ import { timingSafeEqualString } from "./crypto";
4
+ import { createHandoffToken, isReturnOriginAllowed, normalizeReturnOrigin, verifyProxyStartSignature, } from "./handoff";
5
+ import { safeReturnTo } from "./safe-return-to";
6
+ import { appendSetCookie, assertOAuthState, buildVercelAuthorizeRedirect, clearOAuthCookies, decodeIdTokenNonce, exchangeVercelCode, fetchVercelUserInfo, readCookie, } from "./vercel-oauth";
7
+ /** Fixed OAuth callback path registered once on the Vercel OAuth app. */
8
+ export const AUTH_PROXY_CALLBACK_PATH = "/callback";
9
+ export const AUTH_PROXY_START_PATH = "/start";
10
+ function isSecureRequest(request) {
11
+ return new URL(request.url).protocol === "https:";
12
+ }
13
+ function textResponse(message, status) {
14
+ return new Response(message, {
15
+ status,
16
+ headers: {
17
+ "Content-Type": "text/plain; charset=utf-8",
18
+ "Cache-Control": "no-store",
19
+ },
20
+ });
21
+ }
22
+ function cookieOptions(secure) {
23
+ return {
24
+ httpOnly: true,
25
+ sameSite: "lax",
26
+ secure,
27
+ path: "/",
28
+ maxAge: 10 * 60,
29
+ };
30
+ }
31
+ /**
32
+ * Auth-proxy entry: verify the signed start request from a protected app, then
33
+ * begin Sign in with Vercel against this proxy's fixed callback URL.
34
+ */
35
+ export async function handleAuthProxyStart(request, options = {}) {
36
+ const config = resolveConfig(options);
37
+ if (!hasVercelDirectAuth(config) || !config.handoffSecret) {
38
+ return textResponse("Auth proxy is not configured", 500);
39
+ }
40
+ const url = new URL(request.url);
41
+ const returnOrigin = normalizeReturnOrigin(url.searchParams.get("return_origin"));
42
+ const returnPath = safeReturnTo(url.searchParams.get("return_path"));
43
+ const expRaw = url.searchParams.get("exp");
44
+ const nonce = url.searchParams.get("nonce");
45
+ const sig = url.searchParams.get("sig");
46
+ const exp = expRaw ? Number(expRaw) : NaN;
47
+ if (!returnOrigin || !nonce || !Number.isFinite(exp)) {
48
+ return textResponse("Invalid start request", 400);
49
+ }
50
+ if (!isReturnOriginAllowed(returnOrigin, config.allowedReturnOrigins)) {
51
+ return textResponse("Return origin is not allowed", 403);
52
+ }
53
+ const params = {
54
+ returnOrigin,
55
+ returnPath,
56
+ exp,
57
+ nonce,
58
+ };
59
+ if (!(await verifyProxyStartSignature(config.handoffSecret, params, sig))) {
60
+ return textResponse("Invalid or expired start signature", 403);
61
+ }
62
+ const response = await buildVercelAuthorizeRedirect(request, config, returnPath, {
63
+ callbackPath: AUTH_PROXY_CALLBACK_PATH,
64
+ });
65
+ const secure = isSecureRequest(request);
66
+ appendSetCookie(response, OAUTH_RETURN_ORIGIN_COOKIE, returnOrigin, cookieOptions(secure));
67
+ return response;
68
+ }
69
+ /**
70
+ * Auth-proxy Vercel OAuth callback: exchange the code, mint a short-lived handoff
71
+ * token, and redirect back to the protected app callback.
72
+ */
73
+ export async function handleAuthProxyCallback(request, options = {}) {
74
+ const config = resolveConfig(options);
75
+ if (!hasVercelDirectAuth(config) || !config.handoffSecret) {
76
+ return textResponse("Auth proxy is not configured", 500);
77
+ }
78
+ const url = new URL(request.url);
79
+ const code = url.searchParams.get("code");
80
+ const state = url.searchParams.get("state");
81
+ const secure = isSecureRequest(request);
82
+ const returnOrigin = normalizeReturnOrigin(readCookie(request, OAUTH_RETURN_ORIGIN_COOKIE));
83
+ const returnPath = safeReturnTo(readCookie(request, OAUTH_RETURN_COOKIE));
84
+ const fail = (message, status = 401) => {
85
+ const response = textResponse(message, status);
86
+ clearOAuthCookies(response, secure);
87
+ return response;
88
+ };
89
+ if (!returnOrigin) {
90
+ return fail("Missing return origin", 400);
91
+ }
92
+ if (!isReturnOriginAllowed(returnOrigin, config.allowedReturnOrigins)) {
93
+ return fail("Return origin is not allowed", 403);
94
+ }
95
+ if (!code || !(await assertOAuthState(request, state))) {
96
+ return fail("Vercel sign-in failed (invalid state)");
97
+ }
98
+ try {
99
+ const codeVerifier = readCookie(request, OAUTH_VERIFIER_COOKIE);
100
+ if (!codeVerifier) {
101
+ throw new Error("Missing PKCE verifier");
102
+ }
103
+ const tokenData = await exchangeVercelCode(request, config, code, codeVerifier, {
104
+ callbackPath: AUTH_PROXY_CALLBACK_PATH,
105
+ });
106
+ const storedNonce = readCookie(request, OAUTH_NONCE_COOKIE);
107
+ const tokenNonce = decodeIdTokenNonce(tokenData.id_token);
108
+ if (storedNonce && tokenNonce && !(await timingSafeEqualString(storedNonce, tokenNonce))) {
109
+ throw new Error("Nonce mismatch");
110
+ }
111
+ const user = await fetchVercelUserInfo(tokenData.access_token);
112
+ const subject = user.preferred_username || user.email || user.sub || "vercel-user";
113
+ const handoff = await createHandoffToken(config.handoffSecret, returnOrigin, subject);
114
+ const target = new URL(VERCEL_CALLBACK_PATH, `${returnOrigin}/`);
115
+ target.searchParams.set("handoff", handoff);
116
+ target.searchParams.set("return_to", returnPath);
117
+ const response = new Response(null, {
118
+ status: 302,
119
+ headers: {
120
+ Location: target.toString(),
121
+ "Cache-Control": "no-store",
122
+ },
123
+ });
124
+ clearOAuthCookies(response, secure);
125
+ return response;
126
+ }
127
+ catch {
128
+ return fail("Vercel sign-in failed");
129
+ }
130
+ }
@@ -4,6 +4,13 @@ import type { DeploymentProtectionConfig, DeploymentProtectionOptions } from "./
4
4
  */
5
5
  export declare function resolveConfig(options?: DeploymentProtectionOptions): DeploymentProtectionConfig;
6
6
  export declare function hasPasswordAuth(config: DeploymentProtectionConfig): boolean;
7
+ /** Direct Vercel OAuth on the protected app (legacy / single-project setup). */
8
+ export declare function hasVercelDirectAuth(config: DeploymentProtectionConfig): boolean;
9
+ /**
10
+ * Central auth-proxy mode: apps only need the proxy URL + shared handoff secret.
11
+ * Vercel client credentials live solely on the proxy.
12
+ */
13
+ export declare function hasVercelProxyAuth(config: DeploymentProtectionConfig): boolean;
7
14
  export declare function hasVercelAuth(config: DeploymentProtectionConfig): boolean;
8
15
  /**
9
16
  * Protection only runs when enabled and at least one auth method is configured.
@@ -1 +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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAE,2BAA2B,EAAU,MAAM,SAAS,CAAC;AA0B/F;;GAEG;AACH,wBAAgB,aAAa,CACzB,OAAO,GAAE,2BAAgC,GAC1C,0BAA0B,CA0D5B;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAE3E;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAE/E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAE9E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAEzE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAK9E"}
package/dist/es/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseAllowlist } from "./handoff";
1
2
  const FALSEY = new Set(["0", "false", "no", "off"]);
2
3
  function read(env, ...keys) {
3
4
  for (const key of keys) {
@@ -29,12 +30,17 @@ export function resolveConfig(options = {}) {
29
30
  // Prefer an explicit secret; otherwise derive a stable key from credentials so
30
31
  // projects can stay zero-config beyond username/password.
31
32
  const secret = explicitSecret ?? (username && password ? `dp:${username}:${password}` : null);
33
+ const handoffSecret = read(env, "DEPLOYMENT_PROTECTION_HANDOFF_SECRET") ?? explicitSecret ?? secret;
34
+ const authProxyUrl = read(env, "DEPLOYMENT_PROTECTION_AUTH_PROXY_URL")?.replace(/\/$/, "") ?? null;
32
35
  return {
33
36
  enabled: isEnabled(env),
34
37
  username,
35
38
  password,
36
39
  secret,
40
+ handoffSecret,
37
41
  bypassSecret: read(env, "VERCEL_AUTOMATION_BYPASS_SECRET", "DEPLOYMENT_PROTECTION_BYPASS_SECRET"),
42
+ authProxyUrl,
43
+ allowedReturnOrigins: parseAllowlist(read(env, "DEPLOYMENT_PROTECTION_ALLOWED_ORIGINS", "DEPLOYMENT_PROTECTION_AUTH_PROXY_ALLOWED_ORIGINS")),
38
44
  vercelClientId: read(env, "DEPLOYMENT_PROTECTION_VERCEL_CLIENT_ID", "NEXT_PUBLIC_VERCEL_APP_CLIENT_ID", "VERCEL_APP_CLIENT_ID"),
39
45
  vercelClientSecret: read(env, "DEPLOYMENT_PROTECTION_VERCEL_CLIENT_SECRET", "VERCEL_APP_CLIENT_SECRET"),
40
46
  sessionTtlSeconds: options.sessionTtlSeconds ?? 60 * 60 * 24 * 14,
@@ -49,9 +55,20 @@ export function resolveConfig(options = {}) {
49
55
  export function hasPasswordAuth(config) {
50
56
  return Boolean(config.username && config.password && config.secret);
51
57
  }
52
- export function hasVercelAuth(config) {
58
+ /** Direct Vercel OAuth on the protected app (legacy / single-project setup). */
59
+ export function hasVercelDirectAuth(config) {
53
60
  return Boolean(config.vercelClientId && config.vercelClientSecret && config.secret);
54
61
  }
62
+ /**
63
+ * Central auth-proxy mode: apps only need the proxy URL + shared handoff secret.
64
+ * Vercel client credentials live solely on the proxy.
65
+ */
66
+ export function hasVercelProxyAuth(config) {
67
+ return Boolean(config.authProxyUrl && config.handoffSecret && config.secret);
68
+ }
69
+ export function hasVercelAuth(config) {
70
+ return hasVercelDirectAuth(config) || hasVercelProxyAuth(config);
71
+ }
55
72
  /**
56
73
  * Protection only runs when enabled and at least one auth method is configured.
57
74
  * Misconfigured+enabled is treated as inactive (fail-open) so local/dev isn't bricked.
@@ -4,6 +4,7 @@ export declare const OAUTH_STATE_COOKIE = "__becklyn_dp_oauth_state";
4
4
  export declare const OAUTH_NONCE_COOKIE = "__becklyn_dp_oauth_nonce";
5
5
  export declare const OAUTH_VERIFIER_COOKIE = "__becklyn_dp_oauth_verifier";
6
6
  export declare const OAUTH_RETURN_COOKIE = "__becklyn_dp_oauth_return";
7
+ export declare const OAUTH_RETURN_ORIGIN_COOKIE = "__becklyn_dp_oauth_return_origin";
7
8
  export declare const INTERNAL_PATH_PREFIX = "/_becklyn/deployment-protection";
8
9
  export declare const VERCEL_AUTHORIZE_PATH = "/_becklyn/deployment-protection/vercel";
9
10
  export declare const VERCEL_CALLBACK_PATH = "/_becklyn/deployment-protection/vercel/callback";