@becklyn/deployment-protection 0.2.0 → 0.3.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 (46) hide show
  1. package/README.md +81 -12
  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/handler.d.ts.map +1 -1
  12. package/dist/cjs/handler.js +38 -1
  13. package/dist/cjs/handoff.d.ts +46 -0
  14. package/dist/cjs/handoff.d.ts.map +1 -0
  15. package/dist/cjs/handoff.js +199 -0
  16. package/dist/cjs/index.d.ts +2 -1
  17. package/dist/cjs/index.d.ts.map +1 -1
  18. package/dist/cjs/index.js +8 -1
  19. package/dist/cjs/types.d.ts +15 -0
  20. package/dist/cjs/types.d.ts.map +1 -1
  21. package/dist/cjs/vercel-oauth.d.ts +9 -2
  22. package/dist/cjs/vercel-oauth.d.ts.map +1 -1
  23. package/dist/cjs/vercel-oauth.js +8 -4
  24. package/dist/es/auth-proxy.d.ts +15 -0
  25. package/dist/es/auth-proxy.d.ts.map +1 -0
  26. package/dist/es/auth-proxy.js +130 -0
  27. package/dist/es/config.d.ts +7 -0
  28. package/dist/es/config.d.ts.map +1 -1
  29. package/dist/es/config.js +18 -1
  30. package/dist/es/constants.d.ts +1 -0
  31. package/dist/es/constants.d.ts.map +1 -1
  32. package/dist/es/constants.js +1 -0
  33. package/dist/es/handler.d.ts.map +1 -1
  34. package/dist/es/handler.js +39 -2
  35. package/dist/es/handoff.d.ts +46 -0
  36. package/dist/es/handoff.d.ts.map +1 -0
  37. package/dist/es/handoff.js +187 -0
  38. package/dist/es/index.d.ts +2 -1
  39. package/dist/es/index.d.ts.map +1 -1
  40. package/dist/es/index.js +2 -1
  41. package/dist/es/types.d.ts +15 -0
  42. package/dist/es/types.d.ts.map +1 -1
  43. package/dist/es/vercel-oauth.d.ts +9 -2
  44. package/dist/es/vercel-oauth.d.ts.map +1 -1
  45. package/dist/es/vercel-oauth.js +9 -5
  46. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { isValidBypass, readBypassCookie, readBypassToken, shouldSetBypassCookie } from "./bypass";
2
- import { hasPasswordAuth, hasVercelAuth, isProtectionActive, resolveConfig } from "./config";
2
+ import { hasPasswordAuth, hasVercelAuth, hasVercelDirectAuth, hasVercelProxyAuth, isProtectionActive, resolveConfig, } from "./config";
3
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";
4
4
  import { timingSafeEqualString } from "./crypto";
5
+ import { buildProxyStartUrl, verifyHandoffToken } from "./handoff";
5
6
  import { renderLoginPage } from "./login-page";
6
7
  import { validatePasswordCredentials } from "./password";
7
8
  import { safeReturnTo } from "./safe-return-to";
@@ -114,6 +115,19 @@ async function handleVercelAuthorize(request, config) {
114
115
  return new Response("Sign in with Vercel is not configured", { status: 500 });
115
116
  }
116
117
  const returnTo = safeReturnTo(new URL(request.url).searchParams.get("return_to"));
118
+ // Prefer central auth-proxy so apps never register per-host OAuth callbacks.
119
+ if (hasVercelProxyAuth(config) && config.authProxyUrl && config.handoffSecret) {
120
+ const startUrl = await buildProxyStartUrl({
121
+ authProxyUrl: config.authProxyUrl,
122
+ secret: config.handoffSecret,
123
+ returnOrigin: new URL(request.url).origin,
124
+ returnPath: returnTo,
125
+ });
126
+ return redirect(request, startUrl);
127
+ }
128
+ if (!hasVercelDirectAuth(config)) {
129
+ return new Response("Sign in with Vercel is not configured", { status: 500 });
130
+ }
117
131
  return buildVercelAuthorizeRedirect(request, config, returnTo);
118
132
  }
119
133
  async function handleVercelCallback(request, config) {
@@ -124,9 +138,32 @@ async function handleVercelCallback(request, config) {
124
138
  return new Response("Sign in with Vercel is not configured", { status: 500 });
125
139
  }
126
140
  const url = new URL(request.url);
141
+ const handoff = url.searchParams.get("handoff");
142
+ const secure = isSecureRequest(request);
143
+ // Auth-proxy handoff: short-lived signed token, no Vercel client secret on the app.
144
+ if (handoff) {
145
+ if (!config.handoffSecret) {
146
+ return new Response("Sign in with Vercel is not configured", { status: 500 });
147
+ }
148
+ const returnTo = safeReturnTo(url.searchParams.get("return_to"));
149
+ const payload = await verifyHandoffToken(config.handoffSecret, handoff, url.origin);
150
+ if (!payload) {
151
+ return htmlResponse(renderLoginPage(config, {
152
+ error: "Vercel sign-in failed (invalid handoff).",
153
+ returnTo,
154
+ showPassword: hasPasswordAuth(config),
155
+ showVercel: true,
156
+ }));
157
+ }
158
+ const response = redirect(request, returnTo);
159
+ return attachSession(response, config, "vercel", payload.subject, secure);
160
+ }
161
+ // Legacy direct OAuth callback on the protected app.
162
+ if (!hasVercelDirectAuth(config)) {
163
+ return new Response("Sign in with Vercel is not configured", { status: 500 });
164
+ }
127
165
  const code = url.searchParams.get("code");
128
166
  const state = url.searchParams.get("state");
129
- const secure = isSecureRequest(request);
130
167
  const returnTo = safeReturnTo(readCookie(request, OAUTH_RETURN_COOKIE));
131
168
  if (!code || !(await assertOAuthState(request, state))) {
132
169
  const response = htmlResponse(renderLoginPage(config, {
@@ -0,0 +1,46 @@
1
+ export declare const HANDOFF_TTL_SECONDS = 60;
2
+ export declare const PROXY_START_TTL_SECONDS: number;
3
+ export interface ProxyStartParams {
4
+ returnOrigin: string;
5
+ returnPath: string;
6
+ exp: number;
7
+ nonce: string;
8
+ }
9
+ export interface HandoffPayload {
10
+ exp: number;
11
+ aud: string;
12
+ subject: string;
13
+ method: "vercel";
14
+ }
15
+ /**
16
+ * Canonical payload for HMAC over the proxy start request.
17
+ * Field order is fixed so app and proxy produce the same signature.
18
+ */
19
+ export declare function canonicalProxyStartPayload(params: ProxyStartParams): string;
20
+ export declare function signProxyStart(secret: string, params: ProxyStartParams): Promise<string>;
21
+ export declare function verifyProxyStartSignature(secret: string, params: ProxyStartParams, signature: string | null | undefined): Promise<boolean>;
22
+ export declare function buildProxyStartUrl(options: {
23
+ authProxyUrl: string;
24
+ secret: string;
25
+ returnOrigin: string;
26
+ returnPath: string;
27
+ ttlSeconds?: number;
28
+ }): Promise<string>;
29
+ export declare function createHandoffToken(secret: string, audienceOrigin: string, subject: string, ttlSeconds?: number): Promise<string>;
30
+ export declare function verifyHandoffToken(secret: string, token: string | null | undefined, expectedAudienceOrigin: string): Promise<HandoffPayload | null>;
31
+ /**
32
+ * Validate a post-login return origin for the auth proxy.
33
+ * Only https (and http://localhost / 127.0.0.1) are accepted.
34
+ */
35
+ export declare function normalizeReturnOrigin(value: string | null | undefined): string | null;
36
+ /**
37
+ * Optional allowlist. Entries may be:
38
+ * - full origins (`https://app.example.com`)
39
+ * - hostname suffixes (`.vercel.app`, `example.com`)
40
+ * - `localhost` (any localhost / 127.0.0.1 http(s) origin)
41
+ *
42
+ * Empty allowlist → any origin that passes {@link normalizeReturnOrigin}.
43
+ */
44
+ export declare function isReturnOriginAllowed(origin: string, allowlist: string[]): boolean;
45
+ export declare function parseAllowlist(value: string | null | undefined): string[];
46
+ //# sourceMappingURL=handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff.d.ts","sourceRoot":"","sources":["../../src/handoff.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;CACpB;AAeD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAE3E;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAE9F;AAED,wBAAsB,yBAAyB,CAC3C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,OAAO,CAAC,OAAO,CAAC,CAWlB;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBlB;AAWD,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,UAAU,SAAsB,GACjC,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,sBAAsB,EAAE,MAAM,GAC/B,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAwChC;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CA4BrF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAgDlF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CASzE"}
@@ -0,0 +1,187 @@
1
+ import { base64UrlToBytes, bytesToBase64Url, hmacSign, randomToken, timingSafeEqualString, } from "./crypto";
2
+ import { safeReturnTo } from "./safe-return-to";
3
+ export const HANDOFF_TTL_SECONDS = 60;
4
+ export const PROXY_START_TTL_SECONDS = 5 * 60;
5
+ function encodeJson(value) {
6
+ return bytesToBase64Url(new TextEncoder().encode(JSON.stringify(value)));
7
+ }
8
+ function decodeJson(encoded) {
9
+ try {
10
+ const json = new TextDecoder().decode(base64UrlToBytes(encoded));
11
+ return JSON.parse(json);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ /**
18
+ * Canonical payload for HMAC over the proxy start request.
19
+ * Field order is fixed so app and proxy produce the same signature.
20
+ */
21
+ export function canonicalProxyStartPayload(params) {
22
+ return `v1\n${params.returnOrigin}\n${params.returnPath}\n${params.exp}\n${params.nonce}`;
23
+ }
24
+ export async function signProxyStart(secret, params) {
25
+ return hmacSign(secret, canonicalProxyStartPayload(params));
26
+ }
27
+ export async function verifyProxyStartSignature(secret, params, signature) {
28
+ if (!signature) {
29
+ return false;
30
+ }
31
+ if (params.exp < Math.floor(Date.now() / 1000)) {
32
+ return false;
33
+ }
34
+ const expected = await signProxyStart(secret, params);
35
+ return timingSafeEqualString(signature, expected);
36
+ }
37
+ export async function buildProxyStartUrl(options) {
38
+ const returnPath = safeReturnTo(options.returnPath);
39
+ const params = {
40
+ returnOrigin: options.returnOrigin.replace(/\/$/, ""),
41
+ returnPath,
42
+ exp: Math.floor(Date.now() / 1000) + (options.ttlSeconds ?? PROXY_START_TTL_SECONDS),
43
+ nonce: randomToken(16),
44
+ };
45
+ const sig = await signProxyStart(options.secret, params);
46
+ const url = new URL("/start", ensureTrailingSlashBase(options.authProxyUrl));
47
+ url.searchParams.set("return_origin", params.returnOrigin);
48
+ url.searchParams.set("return_path", params.returnPath);
49
+ url.searchParams.set("exp", String(params.exp));
50
+ url.searchParams.set("nonce", params.nonce);
51
+ url.searchParams.set("sig", sig);
52
+ return url.toString();
53
+ }
54
+ function ensureTrailingSlashBase(value) {
55
+ try {
56
+ const url = new URL(value);
57
+ return url.toString().endsWith("/") ? url.toString() : `${url.toString()}/`;
58
+ }
59
+ catch {
60
+ return value.endsWith("/") ? value : `${value}/`;
61
+ }
62
+ }
63
+ export async function createHandoffToken(secret, audienceOrigin, subject, ttlSeconds = HANDOFF_TTL_SECONDS) {
64
+ const payload = {
65
+ exp: Math.floor(Date.now() / 1000) + ttlSeconds,
66
+ aud: audienceOrigin.replace(/\/$/, ""),
67
+ subject,
68
+ method: "vercel",
69
+ };
70
+ const body = encodeJson(payload);
71
+ const signature = await hmacSign(secret, body);
72
+ return `${body}.${signature}`;
73
+ }
74
+ export async function verifyHandoffToken(secret, token, expectedAudienceOrigin) {
75
+ if (!token) {
76
+ return null;
77
+ }
78
+ const [body, signature] = token.split(".");
79
+ if (!body || !signature) {
80
+ return null;
81
+ }
82
+ const expected = await hmacSign(secret, body);
83
+ if (!(await timingSafeEqualString(signature, expected))) {
84
+ return null;
85
+ }
86
+ const payload = decodeJson(body);
87
+ if (!payload ||
88
+ typeof payload.exp !== "number" ||
89
+ typeof payload.aud !== "string" ||
90
+ typeof payload.subject !== "string" ||
91
+ payload.method !== "vercel") {
92
+ return null;
93
+ }
94
+ if (payload.exp < Math.floor(Date.now() / 1000)) {
95
+ return null;
96
+ }
97
+ const expectedAud = expectedAudienceOrigin.replace(/\/$/, "");
98
+ if (!(await timingSafeEqualString(payload.aud, expectedAud))) {
99
+ return null;
100
+ }
101
+ return payload;
102
+ }
103
+ /**
104
+ * Validate a post-login return origin for the auth proxy.
105
+ * Only https (and http://localhost / 127.0.0.1) are accepted.
106
+ */
107
+ export function normalizeReturnOrigin(value) {
108
+ if (typeof value !== "string" || value.length === 0 || value.length > 512) {
109
+ return null;
110
+ }
111
+ let url;
112
+ try {
113
+ url = new URL(value);
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ if (url.username || url.password) {
119
+ return null;
120
+ }
121
+ const host = url.hostname.toLowerCase();
122
+ const isLocalHttp = url.protocol === "http:" &&
123
+ (host === "localhost" || host === "127.0.0.1" || host === "[::1]");
124
+ if (url.protocol !== "https:" && !isLocalHttp) {
125
+ return null;
126
+ }
127
+ // Origin only — reject unexpected path/query/hash noise by normalizing.
128
+ return url.origin;
129
+ }
130
+ /**
131
+ * Optional allowlist. Entries may be:
132
+ * - full origins (`https://app.example.com`)
133
+ * - hostname suffixes (`.vercel.app`, `example.com`)
134
+ * - `localhost` (any localhost / 127.0.0.1 http(s) origin)
135
+ *
136
+ * Empty allowlist → any origin that passes {@link normalizeReturnOrigin}.
137
+ */
138
+ export function isReturnOriginAllowed(origin, allowlist) {
139
+ if (allowlist.length === 0) {
140
+ return true;
141
+ }
142
+ let url;
143
+ try {
144
+ url = new URL(origin);
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ const host = url.hostname.toLowerCase();
150
+ for (const raw of allowlist) {
151
+ const entry = raw.trim().toLowerCase();
152
+ if (!entry) {
153
+ continue;
154
+ }
155
+ if (entry === "localhost") {
156
+ if (host === "localhost" || host === "127.0.0.1" || host === "[::1]") {
157
+ return true;
158
+ }
159
+ continue;
160
+ }
161
+ if (entry.startsWith("http://") || entry.startsWith("https://")) {
162
+ try {
163
+ if (new URL(entry).origin.toLowerCase() === origin.toLowerCase()) {
164
+ return true;
165
+ }
166
+ }
167
+ catch {
168
+ // ignore invalid allowlist entries
169
+ }
170
+ continue;
171
+ }
172
+ const suffix = entry.startsWith(".") ? entry : `.${entry}`;
173
+ if (host === entry || host === entry.replace(/^\./, "") || host.endsWith(suffix)) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
178
+ }
179
+ export function parseAllowlist(value) {
180
+ if (!value) {
181
+ return [];
182
+ }
183
+ return value
184
+ .split(",")
185
+ .map(part => part.trim())
186
+ .filter(Boolean);
187
+ }
@@ -1,6 +1,7 @@
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 { resolveConfig, hasPasswordAuth, hasVercelAuth, hasVercelDirectAuth, hasVercelProxyAuth, isProtectionActive, } from "./config";
5
6
  export type { AuthMethod, DeploymentProtectionConfig, DeploymentProtectionOptions, FormOptions, SessionPayload, } from "./types";
6
7
  //# 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,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/es/index.js CHANGED
@@ -1,4 +1,5 @@
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 { resolveConfig, hasPasswordAuth, hasVercelAuth, hasVercelDirectAuth, hasVercelProxyAuth, isProtectionActive, } from "./config";
@@ -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"}
@@ -1,4 +1,4 @@
1
- import { OAUTH_NONCE_COOKIE, OAUTH_RETURN_COOKIE, OAUTH_STATE_COOKIE, OAUTH_VERIFIER_COOKIE, VERCEL_CALLBACK_PATH, } from "./constants";
1
+ import { OAUTH_NONCE_COOKIE, OAUTH_RETURN_COOKIE, OAUTH_RETURN_ORIGIN_COOKIE, OAUTH_STATE_COOKIE, OAUTH_VERIFIER_COOKIE, VERCEL_CALLBACK_PATH, } from "./constants";
2
2
  import { randomToken, sha256Base64Url, timingSafeEqualString } from "./crypto";
3
3
  function cookieOptions(secure) {
4
4
  return {
@@ -9,7 +9,10 @@ function cookieOptions(secure) {
9
9
  maxAge: 10 * 60,
10
10
  };
11
11
  }
12
- export async function buildVercelAuthorizeRedirect(request, config, returnTo) {
12
+ function resolveCallbackPath(options) {
13
+ return options?.callbackPath ?? VERCEL_CALLBACK_PATH;
14
+ }
15
+ export async function buildVercelAuthorizeRedirect(request, config, returnTo, options) {
13
16
  if (!config.vercelClientId) {
14
17
  return new Response("Vercel OAuth is not configured", { status: 500 });
15
18
  }
@@ -18,7 +21,7 @@ export async function buildVercelAuthorizeRedirect(request, config, returnTo) {
18
21
  const codeVerifier = randomToken(48);
19
22
  const codeChallenge = await sha256Base64Url(codeVerifier);
20
23
  const origin = new URL(request.url).origin;
21
- const redirectUri = `${origin}${VERCEL_CALLBACK_PATH}`;
24
+ const redirectUri = `${origin}${resolveCallbackPath(options)}`;
22
25
  const secure = origin.startsWith("https://");
23
26
  const params = new URLSearchParams({
24
27
  client_id: config.vercelClientId,
@@ -43,7 +46,7 @@ export async function buildVercelAuthorizeRedirect(request, config, returnTo) {
43
46
  appendSetCookie(response, OAUTH_RETURN_COOKIE, returnTo || "/", opts);
44
47
  return response;
45
48
  }
46
- export async function exchangeVercelCode(request, config, code, codeVerifier) {
49
+ export async function exchangeVercelCode(request, config, code, codeVerifier, options) {
47
50
  if (!config.vercelClientId || !config.vercelClientSecret) {
48
51
  throw new Error("Vercel OAuth is not configured");
49
52
  }
@@ -54,7 +57,7 @@ export async function exchangeVercelCode(request, config, code, codeVerifier) {
54
57
  client_secret: config.vercelClientSecret,
55
58
  code,
56
59
  code_verifier: codeVerifier,
57
- redirect_uri: `${origin}${VERCEL_CALLBACK_PATH}`,
60
+ redirect_uri: `${origin}${resolveCallbackPath(options)}`,
58
61
  });
59
62
  const response = await fetch("https://api.vercel.com/login/oauth/token", {
60
63
  method: "POST",
@@ -133,6 +136,7 @@ export function clearOAuthCookies(response, secure) {
133
136
  OAUTH_NONCE_COOKIE,
134
137
  OAUTH_VERIFIER_COOKIE,
135
138
  OAUTH_RETURN_COOKIE,
139
+ OAUTH_RETURN_ORIGIN_COOKIE,
136
140
  ]) {
137
141
  appendSetCookie(response, name, "", expired);
138
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@becklyn/deployment-protection",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "Simple shared username/password + Sign in with Vercel gate for Next.js deployments",
6
6
  "homepage": "https://github.com/Becklyn-Studios/ts-libs/tree/main/packages/deployment-protection",