@codelitdev/oauth-server-kit 0.1.0-alpha.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.
@@ -0,0 +1,366 @@
1
+ 'use strict';
2
+
3
+ var express = require('express');
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __defProps = Object.defineProperties;
7
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
8
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
11
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
12
+ var __spreadValues = (a, b) => {
13
+ for (var prop in b || (b = {}))
14
+ if (__hasOwnProp.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ if (__getOwnPropSymbols)
17
+ for (var prop of __getOwnPropSymbols(b)) {
18
+ if (__propIsEnum.call(b, prop))
19
+ __defNormalProp(a, prop, b[prop]);
20
+ }
21
+ return a;
22
+ };
23
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
24
+
25
+ // src/verify-access-token.ts
26
+ function requireAbsoluteUrl(value, field) {
27
+ let url;
28
+ try {
29
+ url = new URL(value);
30
+ } catch (e) {
31
+ throw new Error(`${field} must be an absolute URL`);
32
+ }
33
+ if (!["http:", "https:"].includes(url.protocol)) {
34
+ throw new Error(`${field} must use http or https`);
35
+ }
36
+ if (process.env.NODE_ENV === "production" && url.protocol !== "https:") {
37
+ throw new Error(`${field} must use https in production`);
38
+ }
39
+ return url.toString().replace(/\/$/, "");
40
+ }
41
+ function normalizedUniqueStrings(values) {
42
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
43
+ }
44
+ function claimStrings(value) {
45
+ if (typeof value === "string") return [value];
46
+ if (!Array.isArray(value)) return [];
47
+ return value.filter((item) => typeof item === "string");
48
+ }
49
+ function optionalString(value) {
50
+ return typeof value === "string" && value.length > 0 ? value : void 0;
51
+ }
52
+ function validateOAuthVerificationOptions(options) {
53
+ var _a;
54
+ requireAbsoluteUrl(options.issuer, "issuer");
55
+ if (normalizedUniqueStrings(options.audiences).length === 0) {
56
+ throw new Error("audiences must contain at least one value");
57
+ }
58
+ for (const value of Object.values((_a = options.resourceMetadataMappings) != null ? _a : {})) {
59
+ requireAbsoluteUrl(value, "resourceMetadataMappings");
60
+ }
61
+ }
62
+ async function verifyOAuthAccessToken(options, token) {
63
+ var _a, _b;
64
+ validateOAuthVerificationOptions(options);
65
+ const issuer = requireAbsoluteUrl(options.issuer, "issuer");
66
+ const audiences = normalizedUniqueStrings(options.audiences);
67
+ if (!token.trim()) return { status: "invalid_token" };
68
+ try {
69
+ const claims = await options.oauthResourceClient.getActions().verifyAccessToken(token, {
70
+ verifyOptions: { issuer, audience: audiences },
71
+ resourceMetadataMappings: (_a = options.resourceMetadataMappings) != null ? _a : {}
72
+ });
73
+ const subject = optionalString(claims.sub);
74
+ const claimIssuer = optionalString(claims.iss);
75
+ const clientId = (_b = optionalString(claims.azp)) != null ? _b : optionalString(claims.client_id);
76
+ const tokenAudiences = normalizedUniqueStrings(
77
+ claimStrings(claims.aud)
78
+ );
79
+ const validatedAudiences = tokenAudiences.filter(
80
+ (audience) => audiences.includes(audience)
81
+ );
82
+ if (!subject || (claimIssuer == null ? void 0 : claimIssuer.replace(/\/$/, "")) !== issuer || !clientId || validatedAudiences.length === 0) {
83
+ return { status: "invalid_token" };
84
+ }
85
+ const scopeValue = claims.scope;
86
+ const scopes = normalizedUniqueStrings(
87
+ Array.isArray(scopeValue) ? claimStrings(scopeValue) : typeof scopeValue === "string" ? scopeValue.split(/\s+/) : []
88
+ );
89
+ const email = optionalString(claims.email);
90
+ const name = optionalString(claims.name);
91
+ return {
92
+ status: "authenticated",
93
+ identity: __spreadProps(__spreadValues(__spreadValues({
94
+ method: "oauth",
95
+ issuer,
96
+ subject
97
+ }, email ? { email } : {}), name ? { name } : {}), {
98
+ clientId,
99
+ scopes,
100
+ audiences: validatedAudiences
101
+ })
102
+ };
103
+ } catch (e) {
104
+ return { status: "invalid_token" };
105
+ }
106
+ }
107
+
108
+ // src/express.ts
109
+ function setBearerChallenge(response, resourceMetadataUrl) {
110
+ if (resourceMetadataUrl) {
111
+ response.setHeader(
112
+ "WWW-Authenticate",
113
+ `Bearer resource_metadata="${resourceMetadataUrl}"`
114
+ );
115
+ return;
116
+ }
117
+ response.setHeader("WWW-Authenticate", "Bearer");
118
+ }
119
+ function sendAuthenticationError(response, result, resourceMetadataUrl) {
120
+ if (result.status === "unavailable") {
121
+ response.status(503).json({ error: "authentication_unavailable" });
122
+ return;
123
+ }
124
+ setBearerChallenge(response, resourceMetadataUrl);
125
+ if (result.status === "invalid_token") {
126
+ response.status(401).json({
127
+ error: "invalid_token",
128
+ error_description: "The access token is invalid or expired"
129
+ });
130
+ return;
131
+ }
132
+ response.status(401).json({
133
+ error: "unauthorized",
134
+ error_description: "A Bearer access token is required"
135
+ });
136
+ }
137
+ function bearerToken(request) {
138
+ var _a;
139
+ const value = request.headers.authorization;
140
+ if (!value) return null;
141
+ const match = value.match(/^Bearer\s+(.+)$/i);
142
+ return ((_a = match == null ? void 0 : match[1]) == null ? void 0 : _a.trim()) || null;
143
+ }
144
+ function createOAuthBearerMiddleware(options) {
145
+ validateOAuthVerificationOptions(options);
146
+ if (options.resourceMetadataUrl) {
147
+ let metadataUrl;
148
+ try {
149
+ metadataUrl = new URL(options.resourceMetadataUrl);
150
+ } catch (e) {
151
+ throw new Error("resourceMetadataUrl must be an absolute URL");
152
+ }
153
+ if (!["http:", "https:"].includes(metadataUrl.protocol)) {
154
+ throw new Error("resourceMetadataUrl must use http or https");
155
+ }
156
+ if (process.env.NODE_ENV === "production" && metadataUrl.protocol !== "https:") {
157
+ throw new Error("resourceMetadataUrl must use https in production");
158
+ }
159
+ }
160
+ return async function oauthBearerMiddleware(request, response, next) {
161
+ const token = bearerToken(request);
162
+ if (!token) {
163
+ sendAuthenticationError(
164
+ response,
165
+ { status: "missing" },
166
+ options.resourceMetadataUrl
167
+ );
168
+ return;
169
+ }
170
+ const result = await verifyOAuthAccessToken(options, token);
171
+ if (result.status !== "authenticated") {
172
+ sendAuthenticationError(
173
+ response,
174
+ result,
175
+ options.resourceMetadataUrl
176
+ );
177
+ return;
178
+ }
179
+ request.auth = result.identity;
180
+ next();
181
+ };
182
+ }
183
+ function escapeHtml(value) {
184
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
185
+ }
186
+ function scriptLiteral(value) {
187
+ return JSON.stringify(value).replace(/</g, "\\u003c");
188
+ }
189
+ function frameProtection(response) {
190
+ response.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
191
+ response.setHeader("X-Frame-Options", "DENY");
192
+ response.setHeader("X-Content-Type-Options", "nosniff");
193
+ }
194
+ function absoluteOrigin(value, field) {
195
+ let url;
196
+ try {
197
+ url = new URL(value);
198
+ } catch (e) {
199
+ throw new Error(`${field} must be an absolute URL`);
200
+ }
201
+ if (!["http:", "https:"].includes(url.protocol)) {
202
+ throw new Error(`${field} must use http or https`);
203
+ }
204
+ if (process.env.NODE_ENV === "production" && url.protocol !== "https:") {
205
+ throw new Error(`${field} must use https in production`);
206
+ }
207
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
208
+ throw new Error(`${field} must contain origins without paths`);
209
+ }
210
+ return url.origin;
211
+ }
212
+ function normalizeOptions(options) {
213
+ var _a;
214
+ if (!options.appName.trim()) throw new Error("appName is required");
215
+ if (!options.authBasePath.startsWith("/") || options.authBasePath.startsWith("//")) {
216
+ throw new Error("authBasePath must be an absolute path");
217
+ }
218
+ const authBasePath = options.authBasePath === "/" ? "" : options.authBasePath.replace(/\/+$/, "");
219
+ if (options.authBasePath.includes("?") || options.authBasePath.includes("#") || /\s/.test(options.authBasePath)) {
220
+ throw new Error("authBasePath must be an absolute path");
221
+ }
222
+ if (options.allowedRedirectOrigins.length === 0) {
223
+ throw new Error("allowedRedirectOrigins must not be empty");
224
+ }
225
+ const allowedRedirectOrigins = new Set(
226
+ options.allowedRedirectOrigins.map(
227
+ (origin) => absoluteOrigin(origin, "allowedRedirectOrigins")
228
+ )
229
+ );
230
+ let defaultRedirect;
231
+ try {
232
+ defaultRedirect = new URL(options.defaultRedirectUrl);
233
+ } catch (e) {
234
+ throw new Error("defaultRedirectUrl must be an absolute URL");
235
+ }
236
+ if (!allowedRedirectOrigins.has(defaultRedirect.origin)) {
237
+ throw new Error("defaultRedirectUrl must use an allowed origin");
238
+ }
239
+ if (options.loginMethods.length === 0) {
240
+ throw new Error("loginMethods must contain at least one method");
241
+ }
242
+ const methodIds = options.loginMethods.map(
243
+ (method) => method.type === "email-otp" ? "email-otp" : `social:${method.providerId}`
244
+ );
245
+ if (new Set(methodIds).size !== methodIds.length) {
246
+ throw new Error("loginMethods must have unique method identifiers");
247
+ }
248
+ for (const method of options.loginMethods) {
249
+ if (method.type === "social") {
250
+ if (!method.providerId.trim() || !method.label.trim()) {
251
+ throw new Error(
252
+ "social login methods require providerId and label"
253
+ );
254
+ }
255
+ }
256
+ }
257
+ for (const name of (_a = options.legacyHostOnlySessionCookieNames) != null ? _a : []) {
258
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
259
+ throw new Error("legacy cookie names contain invalid characters");
260
+ }
261
+ }
262
+ return __spreadProps(__spreadValues({}, options), {
263
+ appName: options.appName.trim(),
264
+ authBasePath,
265
+ allowedRedirectOrigins,
266
+ defaultRedirectUrl: defaultRedirect.toString()
267
+ });
268
+ }
269
+ var STYLES = `
270
+ *{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#0b0b0b;color:#fff;font-family:system-ui,sans-serif}
271
+ .card{width:100%;max-width:400px;padding:32px;border:1px solid #292929;border-radius:14px;background:#111}.logo{width:38px;height:38px;margin:0 auto 20px;display:grid;place-items:center;border-radius:50%;background:#fff;color:#111;font-weight:700}
272
+ h1{margin:0 0 8px;text-align:center;font-size:22px}.sub{margin:0 0 24px;text-align:center;color:#aaa;font-size:13px;line-height:1.5}.error{display:none;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#3a1717;color:#ff9a9a;font-size:13px}
273
+ form{margin:0}.field{margin-bottom:14px}label{display:block;margin-bottom:6px;font-size:12px}input{width:100%;padding:11px 13px;border:1px solid #333;border-radius:8px;background:#181818;color:#fff;font-size:15px}
274
+ button{width:100%;margin-top:8px;padding:11px;border:1px solid #333;border-radius:8px;background:#1b1b1b;color:#fff;font-size:14px;cursor:pointer}.primary{border-color:#8c7a6b;background:#8c7a6b}.secondary{color:#bbb}.divider{display:flex;align-items:center;gap:10px;margin:18px 0;color:#777;font-size:12px}.divider:before,.divider:after{content:"";flex:1;height:1px;background:#2a2a2a}
275
+ button:disabled{opacity:.55;cursor:not-allowed}.client{margin:20px 0;padding:14px;border:1px solid #2a2a2a;border-radius:9px;background:#181818}.client-name{font-weight:600;word-break:break-all}.scopes{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.scope{padding:4px 7px;border:1px solid #333;border-radius:6px;color:#bbb;font-size:12px}.actions{display:flex;gap:8px}.actions button{margin:0}
276
+ `;
277
+ function page(title, body) {
278
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${STYLES}</style></head><body><main class="card">${body}</main></body></html>`;
279
+ }
280
+ function clearLegacyCookies(response, names) {
281
+ for (const name of names != null ? names : []) {
282
+ response.cookie(name, "", {
283
+ path: "/",
284
+ maxAge: 0,
285
+ httpOnly: true,
286
+ secure: true,
287
+ sameSite: "lax"
288
+ });
289
+ }
290
+ }
291
+ function loginMarkup(options) {
292
+ var _a, _b;
293
+ const emailMethod = options.loginMethods.find(
294
+ (method) => method.type === "email-otp"
295
+ );
296
+ const socialMethods = options.loginMethods.filter(
297
+ (method) => method.type === "social"
298
+ );
299
+ const email = emailMethod ? `<form id="email-form"><div class="field"><label for="email">Email</label><input id="email" type="email" required autocomplete="email"></div><button class="primary" type="submit">${escapeHtml((_a = emailMethod.label) != null ? _a : "Continue with email")}</button></form><form id="otp-form" hidden><div class="field"><label for="otp">Verification code</label><input id="otp" inputmode="numeric" required autocomplete="one-time-code"></div><button class="primary" type="submit">Sign in</button><button class="secondary" type="button" id="change-email">Use a different email</button></form>` : "";
300
+ const social = socialMethods.map(
301
+ (method) => `<button type="button" class="social" data-provider="${escapeHtml(method.providerId)}">${escapeHtml(method.label)}</button>`
302
+ ).join("");
303
+ const divider = email && social ? '<div class="divider">or</div>' : "";
304
+ return `<div class="logo">${escapeHtml((_b = options.appName[0]) != null ? _b : "?")}</div><h1>Sign in to ${escapeHtml(options.appName)}</h1><p class="sub">Use one of the configured sign-in methods.</p><div class="error" id="error"></div>${email}${divider}${social}`;
305
+ }
306
+ function loginScript(options, oauth, redirect) {
307
+ const authBasePath = scriptLiteral(options.authBasePath);
308
+ const redirectTarget = scriptLiteral(redirect != null ? redirect : "");
309
+ return `<script>(function(){
310
+ var authBasePath=${authBasePath};var oauth=${oauth};var oauthQuery=location.search.slice(1);var redirectTarget=${redirectTarget};var errorEl=document.getElementById("error");
311
+ function showError(message){errorEl.textContent=message||"Sign-in failed. Please try again.";errorEl.style.display="block"}function clearError(){errorEl.style.display="none"}
312
+ async function post(path,body){var response=await fetch(authBasePath+path,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify(body)});var data={};try{data=await response.json()}catch{}if(!response.ok)throw new Error("Sign-in failed. Please try again.");return data}
313
+ function follow(data){var target=oauth?(data.url||data.redirect_uri):redirectTarget;if(!target)throw new Error("Sign-in failed. Please try again.");location.assign(target)}
314
+ var emailForm=document.getElementById("email-form"),otpForm=document.getElementById("otp-form"),email="";
315
+ if(emailForm){emailForm.addEventListener("submit",async function(event){event.preventDefault();clearError();email=emailForm.querySelector("input").value;try{await post("/email-otp/send-verification-otp",{email:email,type:"sign-in",...(oauth?{oauth_query:oauthQuery}:{})});emailForm.hidden=true;otpForm.hidden=false}catch(error){showError(error.message)}});otpForm.addEventListener("submit",async function(event){event.preventDefault();clearError();try{var data=await post("/sign-in/email-otp",{email:email,otp:document.getElementById("otp").value,name:email.split("@")[0],...(oauth?{oauth_query:oauthQuery}:{})});follow(data)}catch(error){showError(error.message)}});document.getElementById("change-email").addEventListener("click",function(){otpForm.hidden=true;emailForm.hidden=false;clearError()})}
316
+ document.querySelectorAll("button.social").forEach(function(button){button.addEventListener("click",async function(){clearError();button.disabled=true;try{var body={provider:button.dataset.provider,...(oauth?{oauth_query:oauthQuery}:{callbackURL:redirectTarget,errorCallbackURL:location.href})};follow(await post("/sign-in/social",body))}catch(error){showError(error.message);button.disabled=false}})});
317
+ })();</script>`;
318
+ }
319
+ function allowedRedirect(requested, options) {
320
+ if (typeof requested !== "string") return options.defaultRedirectUrl;
321
+ try {
322
+ const url = new URL(requested);
323
+ return options.allowedRedirectOrigins.has(url.origin) ? url.toString() : options.defaultRedirectUrl;
324
+ } catch (e) {
325
+ return options.defaultRedirectUrl;
326
+ }
327
+ }
328
+ function createOAuthPagesRouter(rawOptions) {
329
+ const options = normalizeOptions(rawOptions);
330
+ const router = express.Router();
331
+ router.get("/login", (request, response) => {
332
+ frameProtection(response);
333
+ clearLegacyCookies(response, options.legacyHostOnlySessionCookieNames);
334
+ const redirect = allowedRedirect(request.query.redirect, options);
335
+ response.type("html").send(
336
+ page(
337
+ `Sign in to ${options.appName}`,
338
+ loginMarkup(options) + loginScript(options, false, redirect)
339
+ )
340
+ );
341
+ });
342
+ router.get("/oauth/login", (_request, response) => {
343
+ frameProtection(response);
344
+ clearLegacyCookies(response, options.legacyHostOnlySessionCookieNames);
345
+ response.type("html").send(
346
+ page(
347
+ `Sign in to ${options.appName}`,
348
+ loginMarkup(options) + loginScript(options, true)
349
+ )
350
+ );
351
+ });
352
+ router.get("/oauth/consent", (request, response) => {
353
+ var _a;
354
+ frameProtection(response);
355
+ const clientId = typeof request.query.client_id === "string" ? request.query.client_id : "OAuth client";
356
+ const scope = typeof request.query.scope === "string" ? request.query.scope : "";
357
+ const scopes = scope.split(/\s+/).filter(Boolean);
358
+ const badges = scopes.map((item) => `<span class="scope">${escapeHtml(item)}</span>`).join("");
359
+ const body = `<div class="logo">${escapeHtml((_a = options.appName[0]) != null ? _a : "?")}</div><h1>Authorize ${escapeHtml(options.appName)} access</h1><p class="sub">Review the client and requested scopes.</p><div class="error" id="error"></div><div class="client"><div class="client-name">${escapeHtml(clientId)}</div>${badges ? `<div class="scopes">${badges}</div>` : ""}</div><div class="actions"><button class="primary" id="allow" type="button">Allow</button><button id="deny" type="button">Deny</button></div><script>(function(){var authBasePath=${scriptLiteral(options.authBasePath)},oauthQuery=location.search.slice(1),errorEl=document.getElementById("error"),buttons=[document.getElementById("allow"),document.getElementById("deny")];async function decide(accept){buttons.forEach(function(button){button.disabled=true});errorEl.style.display="none";try{var response=await fetch(authBasePath+"/oauth2/consent",{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({accept:accept,oauth_query:oauthQuery})});var data={};try{data=await response.json()}catch{}var target=data.url||data.redirect_uri;if(!response.ok||!target)throw new Error();location.assign(target)}catch{errorEl.textContent="Could not complete authorization.";errorEl.style.display="block";buttons.forEach(function(button){button.disabled=false})}}buttons[0].addEventListener("click",function(){decide(true)});buttons[1].addEventListener("click",function(){decide(false)})})();</script>`;
360
+ response.type("html").send(page(`Authorize ${options.appName} access`, body));
361
+ });
362
+ return router;
363
+ }
364
+
365
+ exports.createOAuthBearerMiddleware = createOAuthBearerMiddleware;
366
+ exports.createOAuthPagesRouter = createOAuthPagesRouter;
@@ -0,0 +1,262 @@
1
+ import { validateOAuthVerificationOptions, verifyOAuthAccessToken } from './chunk-RJ4ABQXG.mjs';
2
+ import { __spreadProps, __spreadValues } from './chunk-I4E63NIC.mjs';
3
+ import { Router } from 'express';
4
+
5
+ // src/express.ts
6
+ function setBearerChallenge(response, resourceMetadataUrl) {
7
+ if (resourceMetadataUrl) {
8
+ response.setHeader(
9
+ "WWW-Authenticate",
10
+ `Bearer resource_metadata="${resourceMetadataUrl}"`
11
+ );
12
+ return;
13
+ }
14
+ response.setHeader("WWW-Authenticate", "Bearer");
15
+ }
16
+ function sendAuthenticationError(response, result, resourceMetadataUrl) {
17
+ if (result.status === "unavailable") {
18
+ response.status(503).json({ error: "authentication_unavailable" });
19
+ return;
20
+ }
21
+ setBearerChallenge(response, resourceMetadataUrl);
22
+ if (result.status === "invalid_token") {
23
+ response.status(401).json({
24
+ error: "invalid_token",
25
+ error_description: "The access token is invalid or expired"
26
+ });
27
+ return;
28
+ }
29
+ response.status(401).json({
30
+ error: "unauthorized",
31
+ error_description: "A Bearer access token is required"
32
+ });
33
+ }
34
+ function bearerToken(request) {
35
+ var _a;
36
+ const value = request.headers.authorization;
37
+ if (!value) return null;
38
+ const match = value.match(/^Bearer\s+(.+)$/i);
39
+ return ((_a = match == null ? void 0 : match[1]) == null ? void 0 : _a.trim()) || null;
40
+ }
41
+ function createOAuthBearerMiddleware(options) {
42
+ validateOAuthVerificationOptions(options);
43
+ if (options.resourceMetadataUrl) {
44
+ let metadataUrl;
45
+ try {
46
+ metadataUrl = new URL(options.resourceMetadataUrl);
47
+ } catch (e) {
48
+ throw new Error("resourceMetadataUrl must be an absolute URL");
49
+ }
50
+ if (!["http:", "https:"].includes(metadataUrl.protocol)) {
51
+ throw new Error("resourceMetadataUrl must use http or https");
52
+ }
53
+ if (process.env.NODE_ENV === "production" && metadataUrl.protocol !== "https:") {
54
+ throw new Error("resourceMetadataUrl must use https in production");
55
+ }
56
+ }
57
+ return async function oauthBearerMiddleware(request, response, next) {
58
+ const token = bearerToken(request);
59
+ if (!token) {
60
+ sendAuthenticationError(
61
+ response,
62
+ { status: "missing" },
63
+ options.resourceMetadataUrl
64
+ );
65
+ return;
66
+ }
67
+ const result = await verifyOAuthAccessToken(options, token);
68
+ if (result.status !== "authenticated") {
69
+ sendAuthenticationError(
70
+ response,
71
+ result,
72
+ options.resourceMetadataUrl
73
+ );
74
+ return;
75
+ }
76
+ request.auth = result.identity;
77
+ next();
78
+ };
79
+ }
80
+ function escapeHtml(value) {
81
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
82
+ }
83
+ function scriptLiteral(value) {
84
+ return JSON.stringify(value).replace(/</g, "\\u003c");
85
+ }
86
+ function frameProtection(response) {
87
+ response.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
88
+ response.setHeader("X-Frame-Options", "DENY");
89
+ response.setHeader("X-Content-Type-Options", "nosniff");
90
+ }
91
+ function absoluteOrigin(value, field) {
92
+ let url;
93
+ try {
94
+ url = new URL(value);
95
+ } catch (e) {
96
+ throw new Error(`${field} must be an absolute URL`);
97
+ }
98
+ if (!["http:", "https:"].includes(url.protocol)) {
99
+ throw new Error(`${field} must use http or https`);
100
+ }
101
+ if (process.env.NODE_ENV === "production" && url.protocol !== "https:") {
102
+ throw new Error(`${field} must use https in production`);
103
+ }
104
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
105
+ throw new Error(`${field} must contain origins without paths`);
106
+ }
107
+ return url.origin;
108
+ }
109
+ function normalizeOptions(options) {
110
+ var _a;
111
+ if (!options.appName.trim()) throw new Error("appName is required");
112
+ if (!options.authBasePath.startsWith("/") || options.authBasePath.startsWith("//")) {
113
+ throw new Error("authBasePath must be an absolute path");
114
+ }
115
+ const authBasePath = options.authBasePath === "/" ? "" : options.authBasePath.replace(/\/+$/, "");
116
+ if (options.authBasePath.includes("?") || options.authBasePath.includes("#") || /\s/.test(options.authBasePath)) {
117
+ throw new Error("authBasePath must be an absolute path");
118
+ }
119
+ if (options.allowedRedirectOrigins.length === 0) {
120
+ throw new Error("allowedRedirectOrigins must not be empty");
121
+ }
122
+ const allowedRedirectOrigins = new Set(
123
+ options.allowedRedirectOrigins.map(
124
+ (origin) => absoluteOrigin(origin, "allowedRedirectOrigins")
125
+ )
126
+ );
127
+ let defaultRedirect;
128
+ try {
129
+ defaultRedirect = new URL(options.defaultRedirectUrl);
130
+ } catch (e) {
131
+ throw new Error("defaultRedirectUrl must be an absolute URL");
132
+ }
133
+ if (!allowedRedirectOrigins.has(defaultRedirect.origin)) {
134
+ throw new Error("defaultRedirectUrl must use an allowed origin");
135
+ }
136
+ if (options.loginMethods.length === 0) {
137
+ throw new Error("loginMethods must contain at least one method");
138
+ }
139
+ const methodIds = options.loginMethods.map(
140
+ (method) => method.type === "email-otp" ? "email-otp" : `social:${method.providerId}`
141
+ );
142
+ if (new Set(methodIds).size !== methodIds.length) {
143
+ throw new Error("loginMethods must have unique method identifiers");
144
+ }
145
+ for (const method of options.loginMethods) {
146
+ if (method.type === "social") {
147
+ if (!method.providerId.trim() || !method.label.trim()) {
148
+ throw new Error(
149
+ "social login methods require providerId and label"
150
+ );
151
+ }
152
+ }
153
+ }
154
+ for (const name of (_a = options.legacyHostOnlySessionCookieNames) != null ? _a : []) {
155
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
156
+ throw new Error("legacy cookie names contain invalid characters");
157
+ }
158
+ }
159
+ return __spreadProps(__spreadValues({}, options), {
160
+ appName: options.appName.trim(),
161
+ authBasePath,
162
+ allowedRedirectOrigins,
163
+ defaultRedirectUrl: defaultRedirect.toString()
164
+ });
165
+ }
166
+ var STYLES = `
167
+ *{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#0b0b0b;color:#fff;font-family:system-ui,sans-serif}
168
+ .card{width:100%;max-width:400px;padding:32px;border:1px solid #292929;border-radius:14px;background:#111}.logo{width:38px;height:38px;margin:0 auto 20px;display:grid;place-items:center;border-radius:50%;background:#fff;color:#111;font-weight:700}
169
+ h1{margin:0 0 8px;text-align:center;font-size:22px}.sub{margin:0 0 24px;text-align:center;color:#aaa;font-size:13px;line-height:1.5}.error{display:none;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#3a1717;color:#ff9a9a;font-size:13px}
170
+ form{margin:0}.field{margin-bottom:14px}label{display:block;margin-bottom:6px;font-size:12px}input{width:100%;padding:11px 13px;border:1px solid #333;border-radius:8px;background:#181818;color:#fff;font-size:15px}
171
+ button{width:100%;margin-top:8px;padding:11px;border:1px solid #333;border-radius:8px;background:#1b1b1b;color:#fff;font-size:14px;cursor:pointer}.primary{border-color:#8c7a6b;background:#8c7a6b}.secondary{color:#bbb}.divider{display:flex;align-items:center;gap:10px;margin:18px 0;color:#777;font-size:12px}.divider:before,.divider:after{content:"";flex:1;height:1px;background:#2a2a2a}
172
+ button:disabled{opacity:.55;cursor:not-allowed}.client{margin:20px 0;padding:14px;border:1px solid #2a2a2a;border-radius:9px;background:#181818}.client-name{font-weight:600;word-break:break-all}.scopes{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.scope{padding:4px 7px;border:1px solid #333;border-radius:6px;color:#bbb;font-size:12px}.actions{display:flex;gap:8px}.actions button{margin:0}
173
+ `;
174
+ function page(title, body) {
175
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${STYLES}</style></head><body><main class="card">${body}</main></body></html>`;
176
+ }
177
+ function clearLegacyCookies(response, names) {
178
+ for (const name of names != null ? names : []) {
179
+ response.cookie(name, "", {
180
+ path: "/",
181
+ maxAge: 0,
182
+ httpOnly: true,
183
+ secure: true,
184
+ sameSite: "lax"
185
+ });
186
+ }
187
+ }
188
+ function loginMarkup(options) {
189
+ var _a, _b;
190
+ const emailMethod = options.loginMethods.find(
191
+ (method) => method.type === "email-otp"
192
+ );
193
+ const socialMethods = options.loginMethods.filter(
194
+ (method) => method.type === "social"
195
+ );
196
+ const email = emailMethod ? `<form id="email-form"><div class="field"><label for="email">Email</label><input id="email" type="email" required autocomplete="email"></div><button class="primary" type="submit">${escapeHtml((_a = emailMethod.label) != null ? _a : "Continue with email")}</button></form><form id="otp-form" hidden><div class="field"><label for="otp">Verification code</label><input id="otp" inputmode="numeric" required autocomplete="one-time-code"></div><button class="primary" type="submit">Sign in</button><button class="secondary" type="button" id="change-email">Use a different email</button></form>` : "";
197
+ const social = socialMethods.map(
198
+ (method) => `<button type="button" class="social" data-provider="${escapeHtml(method.providerId)}">${escapeHtml(method.label)}</button>`
199
+ ).join("");
200
+ const divider = email && social ? '<div class="divider">or</div>' : "";
201
+ return `<div class="logo">${escapeHtml((_b = options.appName[0]) != null ? _b : "?")}</div><h1>Sign in to ${escapeHtml(options.appName)}</h1><p class="sub">Use one of the configured sign-in methods.</p><div class="error" id="error"></div>${email}${divider}${social}`;
202
+ }
203
+ function loginScript(options, oauth, redirect) {
204
+ const authBasePath = scriptLiteral(options.authBasePath);
205
+ const redirectTarget = scriptLiteral(redirect != null ? redirect : "");
206
+ return `<script>(function(){
207
+ var authBasePath=${authBasePath};var oauth=${oauth};var oauthQuery=location.search.slice(1);var redirectTarget=${redirectTarget};var errorEl=document.getElementById("error");
208
+ function showError(message){errorEl.textContent=message||"Sign-in failed. Please try again.";errorEl.style.display="block"}function clearError(){errorEl.style.display="none"}
209
+ async function post(path,body){var response=await fetch(authBasePath+path,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify(body)});var data={};try{data=await response.json()}catch{}if(!response.ok)throw new Error("Sign-in failed. Please try again.");return data}
210
+ function follow(data){var target=oauth?(data.url||data.redirect_uri):redirectTarget;if(!target)throw new Error("Sign-in failed. Please try again.");location.assign(target)}
211
+ var emailForm=document.getElementById("email-form"),otpForm=document.getElementById("otp-form"),email="";
212
+ if(emailForm){emailForm.addEventListener("submit",async function(event){event.preventDefault();clearError();email=emailForm.querySelector("input").value;try{await post("/email-otp/send-verification-otp",{email:email,type:"sign-in",...(oauth?{oauth_query:oauthQuery}:{})});emailForm.hidden=true;otpForm.hidden=false}catch(error){showError(error.message)}});otpForm.addEventListener("submit",async function(event){event.preventDefault();clearError();try{var data=await post("/sign-in/email-otp",{email:email,otp:document.getElementById("otp").value,name:email.split("@")[0],...(oauth?{oauth_query:oauthQuery}:{})});follow(data)}catch(error){showError(error.message)}});document.getElementById("change-email").addEventListener("click",function(){otpForm.hidden=true;emailForm.hidden=false;clearError()})}
213
+ document.querySelectorAll("button.social").forEach(function(button){button.addEventListener("click",async function(){clearError();button.disabled=true;try{var body={provider:button.dataset.provider,...(oauth?{oauth_query:oauthQuery}:{callbackURL:redirectTarget,errorCallbackURL:location.href})};follow(await post("/sign-in/social",body))}catch(error){showError(error.message);button.disabled=false}})});
214
+ })();</script>`;
215
+ }
216
+ function allowedRedirect(requested, options) {
217
+ if (typeof requested !== "string") return options.defaultRedirectUrl;
218
+ try {
219
+ const url = new URL(requested);
220
+ return options.allowedRedirectOrigins.has(url.origin) ? url.toString() : options.defaultRedirectUrl;
221
+ } catch (e) {
222
+ return options.defaultRedirectUrl;
223
+ }
224
+ }
225
+ function createOAuthPagesRouter(rawOptions) {
226
+ const options = normalizeOptions(rawOptions);
227
+ const router = Router();
228
+ router.get("/login", (request, response) => {
229
+ frameProtection(response);
230
+ clearLegacyCookies(response, options.legacyHostOnlySessionCookieNames);
231
+ const redirect = allowedRedirect(request.query.redirect, options);
232
+ response.type("html").send(
233
+ page(
234
+ `Sign in to ${options.appName}`,
235
+ loginMarkup(options) + loginScript(options, false, redirect)
236
+ )
237
+ );
238
+ });
239
+ router.get("/oauth/login", (_request, response) => {
240
+ frameProtection(response);
241
+ clearLegacyCookies(response, options.legacyHostOnlySessionCookieNames);
242
+ response.type("html").send(
243
+ page(
244
+ `Sign in to ${options.appName}`,
245
+ loginMarkup(options) + loginScript(options, true)
246
+ )
247
+ );
248
+ });
249
+ router.get("/oauth/consent", (request, response) => {
250
+ var _a;
251
+ frameProtection(response);
252
+ const clientId = typeof request.query.client_id === "string" ? request.query.client_id : "OAuth client";
253
+ const scope = typeof request.query.scope === "string" ? request.query.scope : "";
254
+ const scopes = scope.split(/\s+/).filter(Boolean);
255
+ const badges = scopes.map((item) => `<span class="scope">${escapeHtml(item)}</span>`).join("");
256
+ const body = `<div class="logo">${escapeHtml((_a = options.appName[0]) != null ? _a : "?")}</div><h1>Authorize ${escapeHtml(options.appName)} access</h1><p class="sub">Review the client and requested scopes.</p><div class="error" id="error"></div><div class="client"><div class="client-name">${escapeHtml(clientId)}</div>${badges ? `<div class="scopes">${badges}</div>` : ""}</div><div class="actions"><button class="primary" id="allow" type="button">Allow</button><button id="deny" type="button">Deny</button></div><script>(function(){var authBasePath=${scriptLiteral(options.authBasePath)},oauthQuery=location.search.slice(1),errorEl=document.getElementById("error"),buttons=[document.getElementById("allow"),document.getElementById("deny")];async function decide(accept){buttons.forEach(function(button){button.disabled=true});errorEl.style.display="none";try{var response=await fetch(authBasePath+"/oauth2/consent",{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({accept:accept,oauth_query:oauthQuery})});var data={};try{data=await response.json()}catch{}var target=data.url||data.redirect_uri;if(!response.ok||!target)throw new Error();location.assign(target)}catch{errorEl.textContent="Could not complete authorization.";errorEl.style.display="block";buttons.forEach(function(button){button.disabled=false})}}buttons[0].addEventListener("click",function(){decide(true)});buttons[1].addEventListener("click",function(){decide(false)})})();</script>`;
257
+ response.type("html").send(page(`Authorize ${options.appName} access`, body));
258
+ });
259
+ return router;
260
+ }
261
+
262
+ export { createOAuthBearerMiddleware, createOAuthPagesRouter };
@@ -0,0 +1,17 @@
1
+ import { O as OAuthResourceClient, A as AuthenticationResult } from './types-DJxCmhYu.mjs';
2
+ export { a as AuthenticatedIdentity } from './types-DJxCmhYu.mjs';
3
+ import '@better-auth/oauth-provider/resource-client';
4
+
5
+ interface VerifyOAuthAccessTokenOptions {
6
+ oauthResourceClient: OAuthResourceClient;
7
+ issuer: string;
8
+ audiences: readonly string[];
9
+ resourceMetadataMappings?: Readonly<Record<string, string>>;
10
+ }
11
+ /**
12
+ * Verifies a Better Auth OAuth access token and returns protocol identity only.
13
+ * Product claims are deliberately discarded.
14
+ */
15
+ declare function verifyOAuthAccessToken(options: VerifyOAuthAccessTokenOptions, token: string): Promise<AuthenticationResult>;
16
+
17
+ export { AuthenticationResult, OAuthResourceClient, type VerifyOAuthAccessTokenOptions, verifyOAuthAccessToken };