@server/next 0.45.2 → 0.47.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 (3) hide show
  1. package/index.d.ts +96 -55
  2. package/index.js +757 -820
  3. package/package.json +3 -3
package/index.js CHANGED
@@ -43,7 +43,6 @@ ServerError_default.extend({
43
43
  status: 400,
44
44
  message: "The route param '{param}' tries to climb the path ('{value}'). If this route legitimately receives paths, set security: { traversalProtection: false }"
45
45
  },
46
- AUTH_ARGON_NEEDED: "Argon2 is needed for the auth module, please install it with 'npm i argon2'",
47
46
  AUTH_INVALID_TOKEN: { status: 401, message: "Invalid Authorization token" },
48
47
  AUTH_NO_CODE: {
49
48
  status: 400,
@@ -53,32 +52,7 @@ ServerError_default.extend({
53
52
  status: 401,
54
53
  message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
55
54
  },
56
- AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" },
57
- AUTH_NO_PROVIDER: "No provider passed to the option 'auth.providers'",
58
- AUTH_INVALID_PROVIDER: {
59
- status: 401,
60
- message: "Invalid provider '{provider}', valid ones are: '{valid}'"
61
- },
62
- AUTH_NO_SESSION: { status: 401, message: "Invalid session" },
63
- AUTH_NO_USER: {
64
- status: 401,
65
- message: "Credentials do not correspond to a user"
66
- },
67
- AUTH_INVALID_USER: {
68
- status: 500,
69
- message: "{callback} must return a user with an 'id' and an 'email'"
70
- },
71
- LOGIN_NO_EMAIL: "The email is required to log in",
72
- LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
73
- LOGIN_NO_PASSWORD: "The email is required to log in",
74
- LOGIN_INVALID_PASSWORD: "The password you wrote is not correct",
75
- LOGIN_WRONG_ACCOUNT: "That email does not correspond to any account",
76
- LOGIN_WRONG_PASSWORD: "That is not the valid password",
77
- REGISTER_NO_EMAIL: "Email needed",
78
- REGISTER_INVALID_EMAIL: "The email you wrote is not correct",
79
- REGISTER_NO_PASSWORD: "Password needed",
80
- REGISTER_INVALID_PASSWORD: "The password you wrote is not correct",
81
- REGISTER_EMAIL_EXISTS: "Email is already registered"
55
+ AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" }
82
56
  });
83
57
  var errors_default = ServerError_default;
84
58
 
@@ -744,23 +718,6 @@ function clientIp(headers2, opts = {}) {
744
718
  return normalize(remoteAddress);
745
719
  }
746
720
 
747
- // src/helpers/store.ts
748
- import kv from "polystore";
749
- function isStore(source) {
750
- const store = source;
751
- return Boolean(
752
- store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function"
753
- );
754
- }
755
- function toStore(source) {
756
- if (isStore(source)) return source;
757
- return kv(source);
758
- }
759
- function toStoreExpiring(source, expires) {
760
- if (isStore(source)) return source;
761
- return kv(source).expires(expires);
762
- }
763
-
764
721
  // src/helpers/disposition.ts
765
722
  var encodeExt = (name) => encodeURIComponent(name).replace(
766
723
  /['()*]/g,
@@ -984,13 +941,6 @@ var json = (...args) => r().json(...args);
984
941
  var file = (...args) => r().file(...args);
985
942
  var redirect = (...args) => r().redirect(...args);
986
943
 
987
- // src/auth/assertUser.ts
988
- function assertUser(user, callback3) {
989
- if (!user || typeof user !== "object" || user.id == null || !user.email) {
990
- throw ServerError_default.AUTH_INVALID_USER({ callback: callback3 });
991
- }
992
- }
993
-
994
944
  // src/helpers/jwt.ts
995
945
  var enc = new TextEncoder();
996
946
  var dec = new TextDecoder();
@@ -1017,13 +967,13 @@ var hmacKey = (secret) => crypto.subtle.importKey(
1017
967
  );
1018
968
  async function signJwt(payload, secret, expires) {
1019
969
  const now = Math.floor(Date.now() / 1e3);
1020
- const claims = {
970
+ const claims2 = {
1021
971
  iat: now,
1022
972
  ...expires ? { exp: now + expires } : {},
1023
973
  ...payload
1024
974
  };
1025
975
  const head = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
1026
- const body = b64url(JSON.stringify(claims));
976
+ const body = b64url(JSON.stringify(claims2));
1027
977
  const data = `${head}.${body}`;
1028
978
  const key = await hmacKey(secret);
1029
979
  const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
@@ -1040,13 +990,17 @@ async function verifyJwt(token, secret) {
1040
990
  return null;
1041
991
  }
1042
992
  if (header?.alg !== "HS256") return null;
1043
- const key = await hmacKey(secret);
1044
- const ok = await crypto.subtle.verify(
1045
- "HMAC",
1046
- key,
1047
- unb64url(sig),
1048
- enc.encode(`${head}.${body}`)
1049
- );
993
+ let ok = false;
994
+ for (const candidate of Array.isArray(secret) ? secret : [secret]) {
995
+ const key = await hmacKey(candidate);
996
+ ok = await crypto.subtle.verify(
997
+ "HMAC",
998
+ key,
999
+ unb64url(sig),
1000
+ enc.encode(`${head}.${body}`)
1001
+ );
1002
+ if (ok) break;
1003
+ }
1050
1004
  if (!ok) return null;
1051
1005
  let payload;
1052
1006
  try {
@@ -1058,591 +1012,727 @@ async function verifyJwt(token, secret) {
1058
1012
  return payload;
1059
1013
  }
1060
1014
 
1061
- // src/auth/findSessionId.ts
1062
- var validateToken = (authorization) => {
1063
- const [type2, id] = authorization.trim().split(" ");
1064
- if (type2?.toLowerCase() !== "bearer") {
1015
+ // src/auth/credential.ts
1016
+ var NAME = "session";
1017
+ var inCookie = (s) => s === "session" || s === "cookie";
1018
+ var isSigned = (s) => s === "cookie" || s === "jwt";
1019
+ var UNITS2 = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 };
1020
+ function seconds(expires) {
1021
+ const match = /^(\d+)([smhdw])$/.exec(expires);
1022
+ if (!match) throw new Error(`Invalid \`expires\`: "${expires}"`);
1023
+ return Number(match[1]) * UNITS2[match[2]];
1024
+ }
1025
+ var bearer = (ctx) => {
1026
+ const header = ctx.headers.authorization;
1027
+ if (!header) return;
1028
+ const [type2, token] = header.trim().split(" ");
1029
+ if (type2?.toLowerCase() !== "bearer" || !token) {
1065
1030
  throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1066
1031
  }
1067
- if (id?.length !== 16) {
1068
- throw ServerError_default.AUTH_INVALID_TOKEN();
1032
+ return token;
1033
+ };
1034
+ async function read(ctx, strategies) {
1035
+ for (const strategy of strategies) {
1036
+ const token = inCookie(strategy) ? ctx.cookies[NAME] : bearer(ctx);
1037
+ if (!token) continue;
1038
+ const payload = await verifyJwt(token, ctx.options.secrets);
1039
+ if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
1040
+ return { payload, strategy };
1041
+ }
1042
+ }
1043
+ var meta = (payload, strategy) => ({
1044
+ issuedAt: new Date(payload.iat * 1e3),
1045
+ expiresAt: payload.exp ? new Date(payload.exp * 1e3) : void 0,
1046
+ strategy,
1047
+ provider: payload.provider
1048
+ });
1049
+ var issue = (ctx, payload, expires) => signJwt(payload, ctx.options.secrets[0], seconds(expires));
1050
+
1051
+ // src/auth/providers/index.ts
1052
+ import {
1053
+ AmazonCognito,
1054
+ AniList,
1055
+ Apple,
1056
+ Atlassian,
1057
+ Auth0,
1058
+ Authentik,
1059
+ Autodesk,
1060
+ BattleNet,
1061
+ Bitbucket,
1062
+ Box,
1063
+ Bungie,
1064
+ Coinbase,
1065
+ Discord,
1066
+ DonationAlerts,
1067
+ Dribbble,
1068
+ Dropbox,
1069
+ Etsy,
1070
+ EpicGames,
1071
+ Facebook,
1072
+ Figma,
1073
+ Gitea,
1074
+ GitHub,
1075
+ GitLab,
1076
+ Google,
1077
+ Intuit,
1078
+ Kakao,
1079
+ Kick,
1080
+ KeyCloak,
1081
+ Lichess,
1082
+ Line,
1083
+ Linear,
1084
+ LinkedIn,
1085
+ Mastodon,
1086
+ MercadoLibre,
1087
+ MercadoPago,
1088
+ MicrosoftEntraId,
1089
+ MyAnimeList,
1090
+ Naver,
1091
+ Notion,
1092
+ Okta,
1093
+ Osu,
1094
+ Patreon,
1095
+ Polar,
1096
+ Reddit,
1097
+ Roblox,
1098
+ Salesforce,
1099
+ Shikimori,
1100
+ Slack,
1101
+ Spotify,
1102
+ StartGG,
1103
+ Strava,
1104
+ TikTok,
1105
+ Tiltify,
1106
+ Tumblr,
1107
+ Twitch,
1108
+ Twitter,
1109
+ VK,
1110
+ Withings,
1111
+ WorkOS,
1112
+ Yahoo,
1113
+ Yandex,
1114
+ Zoom,
1115
+ FortyTwo
1116
+ } from "antarctic";
1117
+
1118
+ // src/auth/providers/oauth.ts
1119
+ var credentials = (name, options) => ({
1120
+ id: options.id ?? env[`${name.toUpperCase()}_ID`],
1121
+ secret: options.secret ?? env[`${name.toUpperCase()}_SECRET`]
1122
+ });
1123
+ var passthrough = (options) => {
1124
+ const { id, secret, scope, issuer, ...rest } = options;
1125
+ return rest;
1126
+ };
1127
+ var scopeOf = (options, fallback) => {
1128
+ const scope = options.scope ?? fallback;
1129
+ return Array.isArray(scope) ? scope.join(" ") : scope;
1130
+ };
1131
+ var search = (base, params) => {
1132
+ const query = new URLSearchParams();
1133
+ for (const [key, value] of Object.entries(params)) {
1134
+ if (value) query.set(key, String(value));
1069
1135
  }
1070
- return id;
1136
+ return `${base}?${query}`;
1071
1137
  };
1072
- function findSessionId(ctx) {
1073
- if (ctx.options.auth?.strategy.includes("token")) {
1074
- if (!ctx.headers.authorization) return;
1075
- return validateToken(ctx.headers.authorization);
1076
- }
1077
- return ctx.cookies.session || void 0;
1078
- }
1079
-
1080
- // src/auth/finishLogin.ts
1081
- async function finishLogin(ctx, input, opts = {}) {
1082
- const settings = ctx.options.auth;
1083
- const { strategy, onLogin, onUser, onToken } = settings;
1084
- const key = String(input.key);
1085
- const auth2 = {
1086
- user: key,
1087
- provider: input.provider,
1088
- created: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
1138
+
1139
+ // src/auth/providers/antarctic.ts
1140
+ var nowhere = {
1141
+ get: async () => null,
1142
+ set: async () => {
1143
+ },
1144
+ del: async () => {
1145
+ }
1146
+ };
1147
+ function antarcticProvider(name, Client) {
1148
+ const client = (ctx, options) => {
1149
+ const { id, secret } = credentials(name, options);
1150
+ if (!id) throw new Error(`${name.toUpperCase()}_ID is not set`);
1151
+ return new Client({
1152
+ // Whatever that provider needs beyond the standard four: Auth0 takes a
1153
+ // `domain`, Keycloak a `realm`, Gitea a `baseURL`, Mastodon an
1154
+ // `instance`. Unknown keys go straight through.
1155
+ ...passthrough(options),
1156
+ clientId: id,
1157
+ clientSecret: secret,
1158
+ redirectURI: `${ctx.url.origin}/auth/callback/${name}`,
1159
+ scopes: options.scope ? Array.isArray(options.scope) ? options.scope : options.scope.split(" ") : void 0,
1160
+ store: nowhere
1161
+ });
1089
1162
  };
1090
- const loginUser = {
1091
- ...input.user,
1092
- provider: input.provider,
1093
- strategy
1163
+ return {
1164
+ async authorize(ctx, options) {
1165
+ const { url, state, payload } = await client(
1166
+ ctx,
1167
+ options
1168
+ ).getAuthorizationURL();
1169
+ return { url: String(url), state, payload };
1170
+ },
1171
+ async exchange(ctx, options, code, pending) {
1172
+ const user = await client(ctx, options).getUser(
1173
+ { code, state: pending.state },
1174
+ pending
1175
+ );
1176
+ return {
1177
+ provider: name,
1178
+ id: String(user.id),
1179
+ email: user.email ?? "",
1180
+ name: user.name ?? void 0,
1181
+ avatar: user.image ?? void 0,
1182
+ accessToken: user.accessToken,
1183
+ refreshToken: user.refreshToken ?? void 0,
1184
+ raw: user.raw ?? {}
1185
+ };
1186
+ }
1094
1187
  };
1095
- const existingUser = await settings.users.get(key) ?? null;
1096
- const user = onLogin ? await onLogin(loginUser, existingUser, ctx) : { ...existingUser ?? {}, ...loginUser };
1097
- assertUser(user, "onLogin");
1098
- await settings.users.set(key, user);
1099
- if (strategy.includes("jwt")) {
1100
- const payload = {
1101
- ...await onToken(user, ctx),
1102
- provider: input.provider
1103
- };
1104
- assertUser(payload, "onToken");
1105
- const token = await signJwt(payload, ctx.options.secret, 7 * 24 * 60 * 60);
1106
- const exposed = await onUser(payload, ctx);
1107
- assertUser(exposed, "onUser");
1108
- return status(201).json({ ...exposed, token });
1109
- }
1110
- const prev = findSessionId(ctx);
1111
- if (prev) await settings.sessions.del(prev);
1112
- const id = createId();
1113
- await settings.sessions.set(id, auth2);
1114
- if (strategy.includes("token")) {
1115
- const exposed = await onUser(user, ctx);
1116
- assertUser(exposed, "onUser");
1117
- return status(201).json({ ...exposed, token: id });
1118
- }
1119
- if (strategy.includes("cookie")) {
1120
- const reply = cookies("session", {
1121
- value: id,
1122
- path: "/",
1123
- httpOnly: true,
1124
- secure: ctx.platform.production,
1125
- sameSite: "Lax"
1188
+ }
1189
+
1190
+ // src/auth/providers/index.ts
1191
+ var CLASSES = {
1192
+ amazoncognito: AmazonCognito,
1193
+ anilist: AniList,
1194
+ apple: Apple,
1195
+ atlassian: Atlassian,
1196
+ auth0: Auth0,
1197
+ authentik: Authentik,
1198
+ autodesk: Autodesk,
1199
+ battlenet: BattleNet,
1200
+ bitbucket: Bitbucket,
1201
+ box: Box,
1202
+ bungie: Bungie,
1203
+ coinbase: Coinbase,
1204
+ discord: Discord,
1205
+ donationalerts: DonationAlerts,
1206
+ dribbble: Dribbble,
1207
+ dropbox: Dropbox,
1208
+ etsy: Etsy,
1209
+ epicgames: EpicGames,
1210
+ facebook: Facebook,
1211
+ figma: Figma,
1212
+ gitea: Gitea,
1213
+ github: GitHub,
1214
+ gitlab: GitLab,
1215
+ google: Google,
1216
+ intuit: Intuit,
1217
+ kakao: Kakao,
1218
+ kick: Kick,
1219
+ keycloak: KeyCloak,
1220
+ lichess: Lichess,
1221
+ line: Line,
1222
+ linear: Linear,
1223
+ linkedin: LinkedIn,
1224
+ mastodon: Mastodon,
1225
+ mercadolibre: MercadoLibre,
1226
+ mercadopago: MercadoPago,
1227
+ microsoftentraid: MicrosoftEntraId,
1228
+ myanimelist: MyAnimeList,
1229
+ naver: Naver,
1230
+ notion: Notion,
1231
+ okta: Okta,
1232
+ osu: Osu,
1233
+ patreon: Patreon,
1234
+ polar: Polar,
1235
+ reddit: Reddit,
1236
+ roblox: Roblox,
1237
+ salesforce: Salesforce,
1238
+ shikimori: Shikimori,
1239
+ slack: Slack,
1240
+ spotify: Spotify,
1241
+ startgg: StartGG,
1242
+ strava: Strava,
1243
+ tiktok: TikTok,
1244
+ tiltify: Tiltify,
1245
+ tumblr: Tumblr,
1246
+ twitch: Twitch,
1247
+ twitter: Twitter,
1248
+ vk: VK,
1249
+ withings: Withings,
1250
+ workos: WorkOS,
1251
+ yahoo: Yahoo,
1252
+ yandex: Yandex,
1253
+ zoom: Zoom,
1254
+ 42: FortyTwo
1255
+ };
1256
+ var ALIASES = {
1257
+ microsoft: "microsoftentraid",
1258
+ cognito: "amazoncognito",
1259
+ entra: "microsoftentraid"
1260
+ };
1261
+ var providers = Object.fromEntries(
1262
+ Object.entries(CLASSES).map(([name, Client]) => [
1263
+ name,
1264
+ antarcticProvider(name, Client)
1265
+ ])
1266
+ );
1267
+ for (const [alias, target2] of Object.entries(ALIASES)) {
1268
+ providers[alias] = providers[target2];
1269
+ }
1270
+ var ISSUERS = {
1271
+ paypal: "https://www.paypal.com"
1272
+ };
1273
+ var providers_default = providers;
1274
+
1275
+ // src/auth/providers/oidc.ts
1276
+ var discovered = /* @__PURE__ */ new Map();
1277
+ function discover(issuer) {
1278
+ let doc = discovered.get(issuer);
1279
+ if (!doc) {
1280
+ const url = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
1281
+ doc = fetch(url).then((r2) => {
1282
+ if (!r2.ok) throw new Error(`Cannot reach the OIDC issuer at ${url}`);
1283
+ return r2.json();
1126
1284
  });
1127
- if (opts.json) {
1128
- const exposed = await onUser(user, ctx);
1129
- assertUser(exposed, "onUser");
1130
- return reply.status(201).json(exposed);
1131
- }
1132
- return reply.redirect(settings.redirect);
1285
+ doc.catch(() => discovered.delete(issuer));
1286
+ discovered.set(issuer, doc);
1133
1287
  }
1134
- throw new Error("Unknown auth type");
1288
+ return doc;
1289
+ }
1290
+ var claims = (token) => {
1291
+ const body = token.split(".")[1];
1292
+ if (!body) throw new Error("The issuer returned no usable id_token");
1293
+ let b64 = body.replace(/-/g, "+").replace(/_/g, "/");
1294
+ b64 += "=".repeat((4 - b64.length % 4) % 4);
1295
+ return JSON.parse(atob(b64));
1296
+ };
1297
+ function oidcProvider(name) {
1298
+ return {
1299
+ async authorize(ctx, options) {
1300
+ const doc = await discover(options.issuer);
1301
+ const state = createId();
1302
+ const url = search(doc.authorization_endpoint, {
1303
+ client_id: credentials(name, options).id,
1304
+ response_type: "code",
1305
+ scope: scopeOf(options, "openid email profile"),
1306
+ redirect_uri: `${ctx.url.origin}/auth/callback/${name}`,
1307
+ state,
1308
+ ...passthrough(options)
1309
+ });
1310
+ return { url, state };
1311
+ },
1312
+ async exchange(ctx, options, code) {
1313
+ const doc = await discover(options.issuer);
1314
+ const { id, secret } = credentials(name, options);
1315
+ const body = new URLSearchParams({
1316
+ client_id: id,
1317
+ client_secret: secret,
1318
+ code,
1319
+ grant_type: "authorization_code"
1320
+ });
1321
+ body.set("redirect_uri", `${ctx.url.origin}/auth/callback/${name}`);
1322
+ const res = await fetch(doc.token_endpoint, {
1323
+ method: "POST",
1324
+ headers: {
1325
+ accept: "application/json",
1326
+ "content-type": "application/x-www-form-urlencoded"
1327
+ },
1328
+ body
1329
+ });
1330
+ if (!res.ok) throw new Error(`${name}: token exchange failed`);
1331
+ const token = await res.json();
1332
+ const raw = claims(token.id_token);
1333
+ return {
1334
+ provider: name,
1335
+ id: String(raw.sub),
1336
+ email: raw.email,
1337
+ name: raw.name,
1338
+ avatar: raw.picture,
1339
+ accessToken: token.access_token,
1340
+ refreshToken: token.refresh_token,
1341
+ raw
1342
+ };
1343
+ }
1344
+ };
1135
1345
  }
1136
1346
 
1137
1347
  // src/auth/state.ts
1138
- var NAME = "oauth_state";
1139
- function startState(ctx, crossSite = false) {
1140
- const state = createId();
1348
+ var NAME2 = "oauth_state";
1349
+ var EXPIRES = "10m";
1350
+ async function startState(ctx, pending) {
1351
+ const value = await signJwt(pending, ctx.options.secrets[0], 10 * 60);
1141
1352
  return {
1142
- state,
1143
- cookie: {
1144
- value: state,
1145
- path: "/",
1146
- expires: "10m",
1147
- httpOnly: true,
1148
- secure: crossSite || ctx.platform.production,
1149
- sameSite: crossSite ? "None" : "Lax"
1150
- }
1353
+ value,
1354
+ path: "/",
1355
+ expires: EXPIRES,
1356
+ httpOnly: true,
1357
+ secure: ctx.platform.production,
1358
+ sameSite: "Lax"
1151
1359
  };
1152
1360
  }
1153
- function checkState(ctx, received) {
1154
- const expected = ctx.cookies[NAME];
1155
- if (!expected || !received || expected !== received) {
1361
+ async function readState(ctx, received) {
1362
+ const cookie = ctx.cookies[NAME2];
1363
+ if (!cookie || !received) throw ServerError_default.AUTH_INVALID_STATE();
1364
+ const pending = await verifyJwt(cookie, ctx.options.secrets);
1365
+ if (!pending || pending.state !== received) {
1156
1366
  throw ServerError_default.AUTH_INVALID_STATE();
1157
1367
  }
1158
- }
1159
- function clearState() {
1160
- return createCookies(NAME, { value: null });
1368
+ return pending;
1161
1369
  }
1162
1370
 
1163
- // src/auth/providers/apple.ts
1164
- var AUTHORIZE = "https://appleid.apple.com/auth/authorize";
1165
- var TOKEN = "https://appleid.apple.com/auth/token";
1166
- var b64url2 = (data) => {
1167
- const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
1168
- let bin = "";
1169
- for (const byte of bytes) bin += String.fromCharCode(byte);
1170
- return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1171
- };
1172
- var b64urlJson = (segment) => {
1173
- let b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
1174
- b64 += "=".repeat((4 - b64.length % 4) % 4);
1175
- const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
1176
- return JSON.parse(new TextDecoder().decode(bytes));
1177
- };
1178
- var clientSecret = async () => {
1179
- const now = Math.floor(Date.now() / 1e3);
1180
- const header = { alg: "ES256", kid: env.APPLE_KEY_ID, typ: "JWT" };
1181
- const payload = {
1182
- iss: env.APPLE_TEAM_ID,
1183
- iat: now,
1184
- exp: now + 3600,
1185
- aud: "https://appleid.apple.com",
1186
- sub: env.APPLE_ID
1187
- };
1188
- const data = `${b64url2(JSON.stringify(header))}.${b64url2(JSON.stringify(payload))}`;
1189
- const pem = String(env.APPLE_PRIVATE_KEY).replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
1190
- const der = Uint8Array.from(atob(pem), (c) => c.charCodeAt(0));
1191
- const key = await crypto.subtle.importKey(
1192
- "pkcs8",
1193
- der,
1194
- { name: "ECDSA", namedCurve: "P-256" },
1195
- false,
1196
- ["sign"]
1197
- );
1198
- const sig = await crypto.subtle.sign(
1199
- { name: "ECDSA", hash: "SHA-256" },
1200
- key,
1201
- new TextEncoder().encode(data)
1202
- );
1203
- return `${data}.${b64url2(new Uint8Array(sig))}`;
1204
- };
1205
- var login = (ctx) => {
1206
- const { state, cookie } = startState(ctx, true);
1207
- const params = new URLSearchParams({
1208
- client_id: env.APPLE_ID,
1209
- redirect_uri: `${ctx.url.origin}/auth/callback/apple`,
1210
- response_type: "code",
1211
- scope: "name email",
1212
- // Requesting scopes forces Apple to POST the result back (form_post)
1213
- response_mode: "form_post",
1214
- state
1215
- });
1216
- return cookies("oauth_state", cookie).redirect(`${AUTHORIZE}?${params}`);
1217
- };
1218
- var exchange = async (code, redirectUri, user) => {
1219
- const params = new URLSearchParams({
1220
- client_id: env.APPLE_ID,
1221
- client_secret: await clientSecret(),
1222
- code: code ?? "",
1223
- grant_type: "authorization_code"
1224
- });
1225
- if (redirectUri) params.set("redirect_uri", redirectUri);
1226
- const tokenRes = await fetch(TOKEN, {
1227
- method: "POST",
1228
- headers: {
1229
- accept: "application/json",
1230
- "content-type": "application/x-www-form-urlencoded"
1231
- },
1232
- body: params
1233
- });
1234
- if (!tokenRes.ok) throw new Error("apple: token exchange failed");
1235
- const token = await tokenRes.json();
1236
- const claims = b64urlJson(token.id_token.split(".")[1]);
1237
- let name;
1238
- if (user) {
1239
- const parsed = JSON.parse(user).name;
1240
- if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
1241
- }
1242
- return { ...claims, name };
1243
- };
1244
- var finish = async (ctx, raw, opts) => {
1245
- const { onProfile } = ctx.options.auth;
1246
- const profile = onProfile ? await onProfile(raw, "apple") : { id: raw.sub, name: raw.name, email: raw.email };
1247
- assertUser(profile, "onProfile");
1248
- return finishLogin(
1249
- ctx,
1250
- {
1251
- provider: "apple",
1252
- key: profile.id,
1253
- email: profile.email,
1254
- user: profile
1255
- },
1256
- opts
1257
- );
1258
- };
1259
- var callback = async (ctx) => {
1260
- const body = ctx.body || {};
1261
- checkState(ctx, body.state);
1262
- const url = `${ctx.url.origin}/auth/callback/apple`;
1263
- const raw = await exchange(body.code, url, body.user);
1264
- const res = await finish(ctx, raw);
1265
- res.headers.append("set-cookie", clearState());
1266
- return res;
1267
- };
1268
- var verify = async (ctx) => {
1269
- const { code, redirect_uri, user } = ctx.body ?? {};
1270
- if (!code) throw ServerError_default.AUTH_NO_CODE();
1271
- const raw = await exchange(code, redirect_uri, user);
1272
- return finish(ctx, raw, { json: true });
1273
- };
1274
- var apple_default = { login, callback, verify };
1275
-
1276
- // src/auth/providers/oauth.ts
1371
+ // src/auth/flow.ts
1372
+ var SPEC = { schema: { tags: "auth" } };
1277
1373
  var wantsJson = (ctx) => String(ctx.headers.accept || "").includes("application/json");
1278
- var clientParams = (source) => ({
1279
- redirect_uri: source.redirect_uri,
1280
- state: source.state,
1281
- code_challenge: source.code_challenge,
1282
- code_challenge_method: source.code_challenge ? "S256" : void 0
1283
- });
1284
- function oauthProvider(config2) {
1285
- const KEY = config2.name.toUpperCase();
1286
- const callbackUrl = (ctx) => `${ctx.url.origin}/auth/callback/${config2.name}`;
1287
- const authorizeUrl2 = (params) => {
1288
- const search = new URLSearchParams({
1289
- client_id: env[`${KEY}_ID`],
1290
- response_type: "code",
1291
- scope: config2.scope
1292
- });
1293
- for (const [key, value] of Object.entries(params)) {
1294
- if (value) search.set(key, value);
1374
+ function parseProviders(given) {
1375
+ const map2 = typeof given === "string" ? { [given]: {} } : Array.isArray(given) ? Object.fromEntries(given.map((name) => [name, {}])) : { ...given };
1376
+ const out = [];
1377
+ for (const [name, raw] of Object.entries(map2)) {
1378
+ const options = typeof raw === "string" ? { issuer: raw } : { ...raw };
1379
+ if (!options.issuer && !providers_default[name] && ISSUERS[name]) {
1380
+ options.issuer = ISSUERS[name];
1381
+ }
1382
+ if (options.issuer) {
1383
+ out.push({ name, options, provider: oidcProvider(name) });
1384
+ } else if (providers_default[name]) {
1385
+ out.push({ name, options, provider: providers_default[name] });
1386
+ } else {
1387
+ throw new Error(
1388
+ `Unknown provider "${name}". Give it an \`issuer\` to use any OIDC provider, or pick one of "${Object.keys(providers_default).join('", "')}".`
1389
+ );
1295
1390
  }
1296
- return `${config2.authorizeUrl}?${search}`;
1297
- };
1298
- const login3 = (ctx) => {
1299
- if (wantsJson(ctx)) {
1300
- return json({ url: authorizeUrl2(clientParams(ctx.url.query)) });
1391
+ }
1392
+ if (!out.length) throw new Error("Auth needs at least one provider");
1393
+ return out;
1394
+ }
1395
+ var target = async (where, fallback, user, ctx) => typeof where === "function" ? where(user, ctx) : where ?? fallback;
1396
+ function entry(config2) {
1397
+ const list = parseProviders(config2.providers);
1398
+ const strategies = Array.isArray(config2.strategy) ? config2.strategy : [config2.strategy ?? "session"];
1399
+ for (const one of strategies) {
1400
+ if (!["session", "cookie", "token", "jwt"].includes(one)) {
1401
+ throw new Error(
1402
+ `Unknown strategy "${one}"; it takes 'session', 'cookie', 'token' or 'jwt'.`
1403
+ );
1301
1404
  }
1302
- const { state, cookie } = startState(ctx);
1303
- const url = authorizeUrl2({ redirect_uri: callbackUrl(ctx), state });
1304
- return cookies("oauth_state", cookie).redirect(url);
1305
- };
1306
- const exchange2 = async (ctx, code, extra) => {
1307
- const body = new URLSearchParams({
1308
- client_id: env[`${KEY}_ID`],
1309
- client_secret: env[`${KEY}_SECRET`],
1310
- code,
1311
- grant_type: "authorization_code"
1312
- });
1313
- for (const [key, value] of Object.entries(extra)) {
1314
- if (value) body.set(key, value);
1315
- }
1316
- const tokenRes = await fetch(config2.tokenUrl, {
1317
- method: "POST",
1318
- headers: {
1319
- accept: "application/json",
1320
- "content-type": "application/x-www-form-urlencoded"
1321
- },
1322
- body
1323
- });
1324
- if (!tokenRes.ok) throw new Error(`${config2.name}: token exchange failed`);
1325
- const token = await tokenRes.json();
1326
- const profileRes = await fetch(config2.profileUrl, {
1327
- headers: {
1328
- accept: "application/json",
1329
- authorization: `Bearer ${token.access_token}`
1405
+ }
1406
+ const expires = config2.expires ?? "30d";
1407
+ const { onLogin, getUser, toPublicUser, onLogout } = config2;
1408
+ if (onLogin && !getUser) {
1409
+ throw new Error("`onLogin` needs a `getUser`: something has to resolve the id it returns.");
1410
+ }
1411
+ for (const strategy of strategies) {
1412
+ if (isSigned(strategy)) {
1413
+ if (getUser && !toPublicUser) {
1414
+ throw new Error(
1415
+ `The \`${strategy}\` strategy signs the user into the credential, so it needs a \`toPublicUser\` to say what goes in. Signing the whole row would publish whatever else is on it.`
1416
+ );
1330
1417
  }
1331
- });
1332
- if (!profileRes.ok) throw new Error(`${config2.name}: profile fetch failed`);
1333
- return profileRes.json();
1334
- };
1335
- const finish3 = async (ctx, raw, opts) => {
1336
- const { onProfile } = ctx.options.auth;
1337
- const profile = onProfile ? await onProfile(raw, config2.name) : config2.profile(raw);
1338
- assertUser(profile, "onProfile");
1339
- return finishLogin(
1340
- ctx,
1341
- {
1342
- provider: config2.name,
1343
- key: profile.id,
1344
- email: profile.email,
1345
- user: profile
1346
- },
1347
- opts
1348
- );
1349
- };
1350
- const callback3 = async (ctx) => {
1351
- checkState(ctx, ctx.url.query.state);
1352
- const raw = await exchange2(ctx, ctx.url.query.code, {
1353
- redirect_uri: callbackUrl(ctx)
1354
- });
1355
- const res = await finish3(ctx, raw);
1356
- res.headers.append("set-cookie", clearState());
1357
- return res;
1418
+ } else if (!getUser) {
1419
+ throw new Error(
1420
+ `The \`${strategy}\` strategy puts an id in the credential, so it needs a \`getUser\` to resolve it. With no database, use \`cookie\` or \`jwt\`.`
1421
+ );
1422
+ }
1423
+ }
1424
+ const redirects = typeof config2.redirect === "object" ? config2.redirect : {};
1425
+ const loginTo = typeof config2.redirect === "object" ? redirects.login : config2.redirect;
1426
+ const finish = async (ctx, profile) => {
1427
+ const payload = getUser ? await (async () => {
1428
+ const id = await onLogin(profile, ctx);
1429
+ if (id === void 0 || id === null) {
1430
+ throw new Error("`onLogin` must return the id the credential points at");
1431
+ }
1432
+ if (!isSigned(strategies[0])) return { sub: String(id) };
1433
+ const user2 = await getUser(String(id), ctx);
1434
+ return { user: await toPublicUser(user2) };
1435
+ })() : { user: profile };
1436
+ const signed = { ...payload, provider: profile.provider };
1437
+ const token = await issue(ctx, signed, expires);
1438
+ const user = signed.user ?? await getUser(signed.sub, ctx);
1439
+ const to = await target(loginTo, "/", user, ctx);
1440
+ if (inCookie(strategies[0])) {
1441
+ return cookies("session", {
1442
+ value: token,
1443
+ path: "/",
1444
+ expires,
1445
+ httpOnly: true,
1446
+ secure: ctx.platform.production,
1447
+ sameSite: "Lax"
1448
+ }).redirect(to);
1449
+ }
1450
+ return redirect(`${to}#token=${token}`);
1358
1451
  };
1359
- const verify4 = async (ctx) => {
1360
- const { code, redirect_uri, code_verifier } = ctx.body ?? {};
1361
- if (!code) throw ServerError_default.AUTH_NO_CODE();
1362
- const raw = await exchange2(ctx, code, { redirect_uri, code_verifier });
1363
- return finish3(ctx, raw, { json: true });
1452
+ return {
1453
+ name: "flow",
1454
+ async user(ctx) {
1455
+ const found = await read(ctx, strategies);
1456
+ if (!found) return;
1457
+ const { payload, strategy } = found;
1458
+ ctx.auth = meta(payload, strategy);
1459
+ if (payload.user) return payload.user;
1460
+ if (!payload.sub) return;
1461
+ return getUser(payload.sub, ctx);
1462
+ },
1463
+ routes(app) {
1464
+ for (const { name, options, provider } of list) {
1465
+ app.get(`/auth/login/${name}`, SPEC, async (ctx) => {
1466
+ const { url, state, payload } = await provider.authorize(ctx, options);
1467
+ const cookie = await startState(ctx, { state, payload });
1468
+ if (wantsJson(ctx)) {
1469
+ return cookies(NAME2, cookie).json({ url });
1470
+ }
1471
+ return cookies(NAME2, cookie).redirect(url);
1472
+ });
1473
+ const callback = async (ctx) => {
1474
+ const query = ctx.url.query;
1475
+ if (query.error) {
1476
+ const to = await target(redirects.error, "/", null, ctx);
1477
+ return redirect(`${to}?error=${encodeURIComponent(query.error)}`);
1478
+ }
1479
+ const pending = await readState(ctx, query.state);
1480
+ if (!query.code) throw ServerError_default.AUTH_NO_CODE();
1481
+ let res;
1482
+ try {
1483
+ const profile = await provider.exchange(
1484
+ ctx,
1485
+ options,
1486
+ query.code,
1487
+ pending
1488
+ );
1489
+ res = await finish(ctx, profile);
1490
+ } catch (error) {
1491
+ const to = await target(redirects.error, "/", null, ctx);
1492
+ const message = error.message;
1493
+ res = redirect(`${to}?error=${encodeURIComponent(message)}`);
1494
+ }
1495
+ res.headers.append(
1496
+ "set-cookie",
1497
+ `${NAME2}=; Path=/; Max-Age=0; HttpOnly`
1498
+ );
1499
+ return res;
1500
+ };
1501
+ app.get(`/auth/callback/${name}`, SPEC, callback);
1502
+ }
1503
+ app.post("/auth/logout", SPEC, async (ctx) => {
1504
+ const found = await read(ctx, strategies).catch(() => void 0);
1505
+ if (onLogout && found?.payload.sub) {
1506
+ await onLogout(found.payload.sub, ctx);
1507
+ }
1508
+ const to = await target(redirects.logout, "/", null, ctx);
1509
+ if (!inCookie(strategies[0])) return status(204);
1510
+ return cookies("session", { value: null }).redirect(to);
1511
+ });
1512
+ }
1364
1513
  };
1365
- return { login: login3, callback: callback3, verify: verify4 };
1366
- }
1367
-
1368
- // src/auth/providers/discord.ts
1369
- var discord_default = oauthProvider({
1370
- name: "discord",
1371
- authorizeUrl: "https://discord.com/oauth2/authorize",
1372
- tokenUrl: "https://discord.com/api/oauth2/token",
1373
- profileUrl: "https://discord.com/api/users/@me",
1374
- scope: "identify email",
1375
- profile: (p) => ({
1376
- id: p.id,
1377
- email: p.email,
1378
- name: p.global_name || p.username,
1379
- picture: p.avatar ? `https://cdn.discordapp.com/avatars/${p.id}/${p.avatar}.png` : void 0
1380
- })
1381
- });
1382
-
1383
- // src/auth/updateUser.ts
1384
- async function updateUser(user, auth2, store) {
1385
- if (auth2.provider === "email") {
1386
- return await store.set(auth2.email, user);
1387
- }
1388
- }
1389
-
1390
- // src/auth/providers/email.ts
1391
- async function emailLogin(ctx) {
1392
- const { email, password } = ctx.body;
1393
- if (!email) throw ServerError_default.LOGIN_NO_EMAIL();
1394
- if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
1395
- if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
1396
- if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
1397
- const users = ctx.options.auth.users;
1398
- if (!await users.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
1399
- const user = await users.get(email);
1400
- const isValid = await verify2(password, user.password);
1401
- if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
1402
- return finishLogin(ctx, {
1403
- provider: "email",
1404
- key: user.email,
1405
- email: user.email,
1406
- user
1407
- });
1408
1514
  }
1409
- async function emailRegister(ctx) {
1410
- const { email, password, ...data } = ctx.body;
1411
- if (!email) throw ServerError_default.REGISTER_NO_EMAIL();
1412
- if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
1413
- if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
1414
- if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
1415
- const users = ctx.options.auth.users;
1416
- if (await users.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
1417
- const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
1418
- const user = {
1419
- id: createId(email),
1420
- strategy: ctx.options.auth.strategy,
1421
- provider: "email",
1422
- email,
1423
- password: await hash2(password),
1424
- time,
1425
- ...data
1426
- };
1427
- return finishLogin(ctx, {
1428
- provider: "email",
1429
- key: email,
1430
- email,
1431
- user
1432
- });
1433
- }
1434
- async function emailResetPassword() {
1435
- }
1436
- async function emailUpdatePassword(ctx) {
1437
- const passwords = ctx.body;
1438
- const fullUser = await ctx.options.auth.users.get(ctx.user.email);
1439
- if (!fullUser) throw ServerError_default.AUTH_NO_USER();
1440
- const isValid = await verify2(passwords.previous, fullUser.password);
1441
- if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
1442
- fullUser.password = await hash2(passwords.updated);
1443
- await updateUser(fullUser, ctx.user, ctx.options.auth.users);
1444
- return 200;
1445
- }
1446
- var email_default = {
1447
- login: emailLogin,
1448
- register: emailRegister,
1449
- reset: emailResetPassword,
1450
- password: emailUpdatePassword
1451
- };
1452
1515
 
1453
- // src/auth/providers/facebook.ts
1454
- var facebook_default = oauthProvider({
1455
- name: "facebook",
1456
- authorizeUrl: "https://www.facebook.com/v18.0/dialog/oauth",
1457
- tokenUrl: "https://graph.facebook.com/v18.0/oauth/access_token",
1458
- profileUrl: "https://graph.facebook.com/me?fields=id,name,email,picture",
1459
- scope: "email public_profile",
1460
- profile: (p) => ({
1461
- id: p.id,
1462
- email: p.email,
1463
- name: p.name,
1464
- picture: p.picture?.data?.url
1465
- })
1466
- });
1467
-
1468
- // src/auth/providers/github.ts
1469
- var AUTHORIZE2 = "https://github.com/login/oauth/authorize";
1470
- var oauth = async (code, extra) => {
1471
- const fch = async (url, { body, headers: headers2 = {}, ...rest } = {}) => {
1472
- headers2.accept = "application/json";
1473
- headers2["content-type"] = "application/json";
1474
- const res2 = await fetch(url, { ...rest, body, headers: headers2 });
1475
- if (!res2.ok) throw new Error("Invalid request");
1476
- return res2.json();
1477
- };
1478
- const params = {
1479
- client_id: env.GITHUB_ID,
1480
- client_secret: env.GITHUB_SECRET,
1481
- code
1482
- };
1483
- for (const [key, value] of Object.entries(extra)) {
1484
- if (value) params[key] = value;
1485
- }
1486
- const res = await fch("https://github.com/login/oauth/access_token", {
1487
- method: "post",
1488
- body: JSON.stringify(params)
1489
- });
1490
- return (path) => {
1491
- return fch(`https://api.github.com${path}`, {
1492
- headers: { Authorization: `Bearer ${res.access_token}` }
1493
- });
1494
- };
1516
+ // src/auth/verify.ts
1517
+ var enc2 = new TextEncoder();
1518
+ var dec2 = new TextDecoder();
1519
+ var unb64url2 = (seg) => {
1520
+ let b64 = seg.replace(/-/g, "+").replace(/_/g, "/");
1521
+ b64 += "=".repeat((4 - b64.length % 4) % 4);
1522
+ const bin = atob(b64);
1523
+ const bytes = new Uint8Array(bin.length);
1524
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1525
+ return bytes;
1495
1526
  };
1496
- var authorizeUrl = (params) => {
1497
- const search = new URLSearchParams({
1498
- client_id: env.GITHUB_ID,
1499
- scope: "user:email"
1500
- });
1501
- for (const [key, value] of Object.entries(params)) {
1502
- if (value) search.set(key, value);
1503
- }
1504
- return `${AUTHORIZE2}?${search}`;
1527
+ var ALGS = {
1528
+ RS256: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
1529
+ RS384: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" },
1530
+ RS512: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
1531
+ ES256: { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" },
1532
+ ES384: { name: "ECDSA", namedCurve: "P-384", hash: "SHA-384" }
1505
1533
  };
1506
- var login2 = (ctx) => {
1507
- if (wantsJson(ctx)) {
1508
- return json({ url: authorizeUrl(clientParams(ctx.url.query)) });
1534
+ var cache2 = /* @__PURE__ */ new Map();
1535
+ function keysOf(issuer, refresh = false) {
1536
+ let entry3 = cache2.get(issuer);
1537
+ if (!entry3 || refresh && Date.now() - entry3.at > 6e4) {
1538
+ const keys = (async () => {
1539
+ const url = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
1540
+ const discovery = await fetch(url).then((r2) => r2.json());
1541
+ const set = await fetch(discovery.jwks_uri).then((r2) => r2.json());
1542
+ const out = /* @__PURE__ */ new Map();
1543
+ for (const jwk of set.keys ?? []) {
1544
+ const algorithm = ALGS[jwk.alg];
1545
+ if (!algorithm) continue;
1546
+ out.set(
1547
+ jwk.kid,
1548
+ await crypto.subtle.importKey("jwk", jwk, algorithm, false, ["verify"])
1549
+ );
1550
+ }
1551
+ return out;
1552
+ })();
1553
+ keys.catch(() => cache2.delete(issuer));
1554
+ entry3 = { at: Date.now(), keys };
1555
+ cache2.set(issuer, entry3);
1509
1556
  }
1510
- const { state, cookie } = startState(ctx);
1511
- return cookies("oauth_state", cookie).redirect(authorizeUrl({ state }));
1512
- };
1513
- var getUserProfile = async (code, extra = {}) => {
1514
- const api = await oauth(code, extra);
1515
- const [profile, emails] = await Promise.all([
1516
- api("/user"),
1517
- api("/user/emails")
1518
- ]);
1519
- const email = emails.sort((a) => a.primary ? -1 : 1)[0]?.email;
1520
- return { ...profile, email };
1557
+ return entry3.keys;
1558
+ }
1559
+ var bearer2 = (ctx) => {
1560
+ const header = ctx.headers.authorization;
1561
+ if (!header) return;
1562
+ const [type2, token] = header.trim().split(" ");
1563
+ if (type2?.toLowerCase() !== "bearer" || !token) {
1564
+ throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1565
+ }
1566
+ return token;
1521
1567
  };
1522
- var defaultProfile = (raw) => ({
1523
- id: raw.id,
1524
- name: raw.name,
1525
- email: raw.email,
1526
- picture: raw.avatar_url,
1527
- location: raw.location,
1528
- created: raw.created_at
1529
- });
1530
- var finish2 = async (ctx, raw, opts) => {
1531
- const { onProfile } = ctx.options.auth;
1532
- const profile = onProfile ? await onProfile(raw, "github") : defaultProfile(raw);
1533
- assertUser(profile, "onProfile");
1534
- return finishLogin(
1535
- ctx,
1536
- {
1537
- provider: "github",
1538
- key: profile.id,
1539
- email: profile.email,
1540
- user: profile
1541
- },
1542
- opts
1568
+ function entry2(options) {
1569
+ const { verify: issuer, audience } = options;
1570
+ const claimNames = options.audienceClaim ? Array.isArray(options.audienceClaim) ? options.audienceClaim : [options.audienceClaim] : ["aud"];
1571
+ if (!audience) {
1572
+ throw new Error(
1573
+ "`verify` needs an `audience`: one issuer serves many applications, and without it a token minted for another one is accepted here."
1574
+ );
1575
+ }
1576
+ const allowed = Array.isArray(audience) ? audience : [audience];
1577
+ return {
1578
+ name: `verify:${issuer}`,
1579
+ async user(ctx) {
1580
+ const token = options.cookie ? ctx.cookies[options.cookie] : bearer2(ctx);
1581
+ if (!token) return;
1582
+ const claims2 = await check(token, issuer, allowed, claimNames);
1583
+ ctx.auth = {
1584
+ issuedAt: new Date((claims2.iat ?? 0) * 1e3),
1585
+ expiresAt: claims2.exp ? new Date(claims2.exp * 1e3) : void 0,
1586
+ strategy: options.cookie ? "cookie" : "jwt",
1587
+ provider: issuer
1588
+ };
1589
+ if (!options.getUser) return claims2;
1590
+ return options.getUser(claims2.sub, ctx);
1591
+ }
1592
+ };
1593
+ }
1594
+ async function check(token, issuer, allowed, claimNames) {
1595
+ const parts = token.split(".");
1596
+ if (parts.length !== 3) throw ServerError_default.AUTH_INVALID_TOKEN();
1597
+ const [head, body, sig] = parts;
1598
+ let header;
1599
+ let claims2;
1600
+ try {
1601
+ header = JSON.parse(dec2.decode(unb64url2(head)));
1602
+ claims2 = JSON.parse(dec2.decode(unb64url2(body)));
1603
+ } catch {
1604
+ throw ServerError_default.AUTH_INVALID_TOKEN();
1605
+ }
1606
+ const algorithm = ALGS[header?.alg];
1607
+ if (!algorithm) throw ServerError_default.AUTH_INVALID_TOKEN();
1608
+ let key = (await keysOf(issuer)).get(header.kid);
1609
+ if (!key) key = (await keysOf(issuer, true)).get(header.kid);
1610
+ if (!key) throw ServerError_default.AUTH_INVALID_TOKEN();
1611
+ const ok = await crypto.subtle.verify(
1612
+ algorithm.name === "ECDSA" ? { name: "ECDSA", hash: algorithm.hash } : algorithm,
1613
+ key,
1614
+ unb64url2(sig),
1615
+ enc2.encode(`${head}.${body}`)
1543
1616
  );
1544
- };
1545
- var callback2 = async (ctx) => {
1546
- checkState(ctx, ctx.url.query.state);
1547
- const raw = await getUserProfile(ctx.url.query.code);
1548
- const res = await finish2(ctx, raw);
1549
- res.headers.append("set-cookie", clearState());
1550
- return res;
1551
- };
1552
- var verify3 = async (ctx) => {
1553
- const { code, redirect_uri, code_verifier } = ctx.body ?? {};
1554
- if (!code) throw ServerError_default.AUTH_NO_CODE();
1555
- const raw = await getUserProfile(code, { redirect_uri, code_verifier });
1556
- return finish2(ctx, raw, { json: true });
1557
- };
1558
- var github_default = { login: login2, callback: callback2, verify: verify3 };
1559
-
1560
- // src/auth/providers/google.ts
1561
- var google_default = oauthProvider({
1562
- name: "google",
1563
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1564
- tokenUrl: "https://oauth2.googleapis.com/token",
1565
- profileUrl: "https://openidconnect.googleapis.com/v1/userinfo",
1566
- scope: "openid email profile",
1567
- profile: (p) => ({
1568
- id: p.sub,
1569
- email: p.email,
1570
- name: p.name,
1571
- picture: p.picture
1572
- })
1573
- });
1574
-
1575
- // src/auth/providers/microsoft.ts
1576
- var microsoft_default = oauthProvider({
1577
- name: "microsoft",
1578
- authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
1579
- tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
1580
- profileUrl: "https://graph.microsoft.com/v1.0/me",
1581
- scope: "openid email profile User.Read",
1582
- profile: (p) => ({
1583
- // Personal accounts expose `userPrincipalName` rather than `mail`
1584
- id: p.id,
1585
- email: p.mail || p.userPrincipalName,
1586
- name: p.displayName
1587
- })
1588
- });
1617
+ if (!ok) throw ServerError_default.AUTH_INVALID_TOKEN();
1618
+ const now = Math.floor(Date.now() / 1e3);
1619
+ if (claims2.exp && now >= claims2.exp) throw ServerError_default.AUTH_INVALID_TOKEN();
1620
+ if (claims2.nbf && now < claims2.nbf) throw ServerError_default.AUTH_INVALID_TOKEN();
1621
+ if (claims2.iss !== issuer) throw ServerError_default.AUTH_INVALID_TOKEN();
1622
+ const name = claimNames.find((one) => claims2[one] !== void 0);
1623
+ if (!name) throw ServerError_default.AUTH_INVALID_TOKEN();
1624
+ const value = claims2[name];
1625
+ const aud = Array.isArray(value) ? value : [value];
1626
+ if (!aud.some((one) => allowed.includes(one))) {
1627
+ throw ServerError_default.AUTH_INVALID_TOKEN();
1628
+ }
1629
+ return claims2;
1630
+ }
1589
1631
 
1590
- // src/auth/providers/index.ts
1591
- var providers_default = {
1592
- apple: apple_default,
1593
- discord: discord_default,
1594
- email: email_default,
1595
- facebook: facebook_default,
1596
- github: github_default,
1597
- google: google_default,
1598
- microsoft: microsoft_default
1632
+ // src/auth/vendors.ts
1633
+ var VENDORS = {
1634
+ clerk: {
1635
+ cookie: "__session",
1636
+ // Clerk session tokens carry no `aud`: the authorized party (your
1637
+ // frontend origin) is in `azp`, which is what their own SDK checks
1638
+ audience: "your frontend origin, like https://app.example.com",
1639
+ claim: "azp",
1640
+ docs: "https://clerk.com/docs/backend-requests/resources/session-tokens"
1641
+ },
1642
+ supabase: {
1643
+ audience: '"authenticated"',
1644
+ docs: "https://supabase.com/docs/guides/auth/jwts"
1645
+ }
1599
1646
  };
1647
+ var vendors_default = VENDORS;
1600
1648
 
1601
- // src/auth/parseAuthOptions.ts
1602
- var defaultRedirect = "/user";
1603
- function defaultOnUser(fullUser) {
1604
- const { password: _password, ...user } = fullUser;
1605
- return user;
1606
- }
1607
- var available = Object.keys(providers_default);
1608
- function parseAuthOptions(auth2) {
1649
+ // src/auth/parse.ts
1650
+ function parseAuth(auth2) {
1609
1651
  if (!auth2) return null;
1610
- if (typeof auth2 === "string") {
1611
- const [strategy2, provider] = auth2.split(":");
1612
- auth2 = { strategy: strategy2, providers: provider ? [provider] : [] };
1652
+ const list = Array.isArray(auth2) ? auth2 : [auth2];
1653
+ return list.flatMap((one) => toEntry(one));
1654
+ }
1655
+ function vendorEntry(strategy, name) {
1656
+ const vendor = vendors_default[name];
1657
+ const KEY = name.toUpperCase();
1658
+ if (strategy !== "jwt" && strategy !== "cookie") {
1659
+ throw new Error(
1660
+ `"${strategy}:${name}" is not possible: ${name} issues a signed token, and "${strategy}" means an opaque id resolved through a \`getUser\` of yours. Use "jwt:${name}", or "cookie:${name}" for a same-origin app.`
1661
+ );
1613
1662
  }
1614
- if (!auth2.strategy?.length) {
1615
- throw new Error("Auth options needs a strategy");
1663
+ if (strategy === "cookie" && !vendor.cookie) {
1664
+ throw new Error(
1665
+ `"cookie:${name}" is not possible: ${name} does not store its token in a cookie with a fixed name. Use "jwt:${name}", or name the cookie yourself with { verify, audience, cookie }.`
1666
+ );
1616
1667
  }
1617
- const strategy = auth2.strategy;
1618
- const list = Array.isArray(auth2.providers) ? auth2.providers : auth2.providers ? [auth2.providers] : [];
1619
- if (!list.length) {
1620
- throw new Error("Auth options needs a provider");
1668
+ const issuer = globalThis.env[`${KEY}_ISSUER`];
1669
+ if (!issuer) {
1670
+ throw new Error(
1671
+ `${KEY}_ISSUER is not set, and it differs per account, so it cannot be guessed. See ${vendor.docs}`
1672
+ );
1621
1673
  }
1622
- const invalid = list.find((p) => !available.includes(p));
1623
- if (invalid) {
1674
+ const audience = globalThis.env[`${KEY}_AUDIENCE`];
1675
+ if (!audience) {
1624
1676
  throw new Error(
1625
- `Provider "${invalid}" not found, available ones are "${available.join('", "')}"`
1677
+ `${KEY}_AUDIENCE is not set. It should be ${vendor.audience}. One issuer serves many applications, all signed with the same keys, so without it a token minted for another one is accepted here.`
1626
1678
  );
1627
1679
  }
1628
- const redirect2 = auth2.redirect || defaultRedirect;
1629
- const { onProfile, onLogin, onLogout } = auth2;
1630
- const onUser = auth2.onUser || defaultOnUser;
1631
- const onToken = auth2.onToken || defaultOnUser;
1632
- const users = auth2.users ? toStore(auth2.users) : null;
1633
- const sessions = auth2.sessions ? toStoreExpiring(auth2.sessions, "1w") : null;
1634
- return {
1635
- strategy,
1636
- providers: list,
1637
- redirect: redirect2,
1638
- onProfile,
1639
- onLogin,
1640
- onUser,
1641
- onToken,
1642
- onLogout,
1643
- users,
1644
- sessions
1645
- };
1680
+ return entry2({
1681
+ verify: issuer,
1682
+ audience,
1683
+ ...vendor.claim ? { audienceClaim: vendor.claim } : {},
1684
+ ...strategy === "cookie" ? { cookie: vendor.cookie } : {}
1685
+ });
1686
+ }
1687
+ function toEntry(auth2) {
1688
+ if (typeof auth2 === "string") {
1689
+ const [strategy, name] = auth2.split(":");
1690
+ if (!name) {
1691
+ throw new Error(
1692
+ `Invalid auth "${auth2}": the string form is "<strategy>:<name>", like "cookie:github" to log people in, or "jwt:clerk" to check a token a vendor issued.`
1693
+ );
1694
+ }
1695
+ if (vendors_default[name]) return [vendorEntry(strategy, name)];
1696
+ return [entry({ strategy, providers: name })];
1697
+ }
1698
+ if (typeof auth2 === "function") {
1699
+ return [{ name: "function", user: async (ctx) => auth2(ctx) }];
1700
+ }
1701
+ if (auth2 && typeof auth2 === "object") {
1702
+ if ("verify" in auth2) return [entry2(auth2)];
1703
+ if ("providers" in auth2) return [entry(auth2)];
1704
+ if ("handler" in auth2) {
1705
+ const instance = auth2;
1706
+ const path = (instance.path ?? "/api/auth").replace(/\/$/, "");
1707
+ const raw = { parser: "stream" };
1708
+ const forward = (ctx) => instance.handler(
1709
+ new Request(ctx.url.href, {
1710
+ method: ctx.method,
1711
+ headers: ctx.headers,
1712
+ body: ctx.body,
1713
+ // Required by fetch whenever a body is a stream
1714
+ ...ctx.body ? { duplex: "half" } : {}
1715
+ })
1716
+ );
1717
+ return [
1718
+ {
1719
+ name: `instance:${path}`,
1720
+ user: async (ctx) => instance.user?.(ctx),
1721
+ routes: (app) => {
1722
+ const wildcard = `${path}/*`;
1723
+ app.get(wildcard, raw, forward);
1724
+ app.post(wildcard, raw, forward);
1725
+ app.put(wildcard, raw, forward);
1726
+ app.patch(wildcard, raw, forward);
1727
+ app.delete(wildcard, raw, forward);
1728
+ }
1729
+ }
1730
+ ];
1731
+ }
1732
+ }
1733
+ throw new Error(
1734
+ "Invalid `auth`: it takes a string, a function, `{ providers }`, `{ verify, audience }`, a library instance, or an array of those."
1735
+ );
1646
1736
  }
1647
1737
 
1648
1738
  // src/helpers/color.ts
@@ -1707,16 +1797,16 @@ var STATUS_TEXT = {
1707
1797
  502: "Bad Gateway",
1708
1798
  503: "Service Unavailable"
1709
1799
  };
1710
- var UNITS2 = ["b", "kb", "mb", "gb", "tb"];
1800
+ var UNITS3 = ["b", "kb", "mb", "gb", "tb"];
1711
1801
  function formatBytes(bytes) {
1712
1802
  if (!bytes || bytes < 0) return "0b";
1713
1803
  const i = Math.min(
1714
1804
  Math.floor(Math.log(bytes) / Math.log(1024)),
1715
- UNITS2.length - 1
1805
+ UNITS3.length - 1
1716
1806
  );
1717
1807
  const value = bytes / 1024 ** i;
1718
1808
  const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
1719
- return `${rounded}${UNITS2[i]}`;
1809
+ return `${rounded}${UNITS3[i]}`;
1720
1810
  }
1721
1811
  var SCOPE_COLORS = {
1722
1812
  start: "green",
@@ -1754,6 +1844,13 @@ function createLogger(level) {
1754
1844
  };
1755
1845
  }
1756
1846
 
1847
+ // src/helpers/secrets.ts
1848
+ function resolveSecrets(option) {
1849
+ const given = option ?? globalThis.env.SECRETS?.split(",");
1850
+ const list = (Array.isArray(given) ? given : [given]).map((one) => one?.trim()).filter(Boolean);
1851
+ return list.length ? list : [`unsafe-${createId()}`];
1852
+ }
1853
+
1757
1854
  // src/helpers/security.ts
1758
1855
  function resolveSecurity(security) {
1759
1856
  const off = security === false;
@@ -1827,13 +1924,23 @@ function config(options = {}) {
1827
1924
  );
1828
1925
  }
1829
1926
  }
1927
+ if (opts.secret !== void 0) {
1928
+ throw new Error(
1929
+ "The `secret` option is now `secrets`, and takes one key or several: `secrets: [current, previous]` signs with the first and verifies with any, so rotating a key no longer signs everyone out."
1930
+ );
1931
+ }
1932
+ if (env2.SECRET && !env2.SECRETS) {
1933
+ throw new Error(
1934
+ "The SECRET environment variable is now SECRETS, a comma-separated list. Rename it, or every token signed with the old key breaks."
1935
+ );
1936
+ }
1830
1937
  const raw = options.log ?? env2.LOG_LEVEL;
1831
1938
  const level = raw === true ? "info" : raw === false ? void 0 : raw;
1832
1939
  const log = createLogger(level);
1833
1940
  const settings = {
1834
1941
  // `env.PORT` is a string, so coerce it: `settings.port` is a number
1835
1942
  port: options.port || Number(env2.PORT) || 3e3,
1836
- secret: options.secret || env2.SECRET || `unsafe-${createId()}`,
1943
+ secrets: resolveSecrets(options.secrets),
1837
1944
  log,
1838
1945
  // How request bodies are read: parsed into ctx.body by default; `raw` keeps
1839
1946
  // the Buffer, `stream` hands the handler the unread web ReadableStream.
@@ -1884,31 +1991,13 @@ function config(options = {}) {
1884
1991
  settings.uploads = resolveUploads(options.uploads);
1885
1992
  const production = env2.NODE_ENV === "production";
1886
1993
  if (options.auth || env2.AUTH) {
1887
- settings.auth = parseAuthOptions(
1994
+ settings.auth = parseAuth(
1888
1995
  options.auth || env2.AUTH || null
1889
1996
  );
1890
1997
  }
1891
- if (settings.auth) {
1892
- if (!settings.auth.users) {
1893
- if (production) {
1894
- throw new Error(
1895
- "Auth in production needs a persistent `users` store, like auth: { ..., users: kv(redis).prefix('user:') }."
1896
- );
1897
- }
1898
- settings.auth.users = toStore(/* @__PURE__ */ new Map());
1899
- }
1900
- if (!settings.auth.sessions && !settings.auth.strategy.includes("jwt")) {
1901
- if (production) {
1902
- throw new Error(
1903
- "Auth in production needs a persistent `sessions` store, like auth: { ..., sessions: kv(redis).prefix('session:') }."
1904
- );
1905
- }
1906
- settings.auth.sessions = toStoreExpiring(/* @__PURE__ */ new Map(), "1w");
1907
- }
1908
- }
1909
- if (settings.auth?.strategy.includes("jwt") && settings.secret.startsWith("unsafe-")) {
1998
+ if (settings.auth?.some((one) => one.name === "flow") && settings.secrets[0].startsWith("unsafe-")) {
1910
1999
  console.warn(
1911
- "[server:auth] jwt strategy with no SECRET set: tokens are signed with a random per-process secret, so they break on restart and across instances. Set the SECRET environment variable (or the `secret` option)."
2000
+ "[server:auth] auth with no SECRETS set: credentials are signed with a random per-process secret, so they break on restart and across instances. Set the SECRETS environment variable (or the `secrets` option)."
1912
2001
  );
1913
2002
  }
1914
2003
  if (options.openapi) {
@@ -1925,7 +2014,8 @@ function config(options = {}) {
1925
2014
  settings.onResponse = options.onResponse;
1926
2015
  const loc = (v) => typeof v === "string" ? v : "enabled";
1927
2016
  if (settings.auth) {
1928
- log.message("auth", `${settings.auth.providers.join(", ")} auth enabled`);
2017
+ const names = settings.auth.map((one) => one.name).join(", ");
2018
+ log.message("auth", ` enabled`);
1929
2019
  }
1930
2020
  if (settings.public) log.message("public", loc(options.public));
1931
2021
  if (settings.uploads) log.message("uploads", loc(options.uploads));
@@ -2141,9 +2231,9 @@ async function validateResponse(out, options) {
2141
2231
  if (out?.constructor !== Object && !Array.isArray(out)) return out;
2142
2232
  return await run(options.response, out, "response");
2143
2233
  }
2144
- function replace2(target, values) {
2145
- for (const key of Object.keys(target)) delete target[key];
2146
- Object.assign(target, values);
2234
+ function replace2(target2, values) {
2235
+ for (const key of Object.keys(target2)) delete target2[key];
2236
+ Object.assign(target2, values);
2147
2237
  }
2148
2238
 
2149
2239
  // src/helpers/handleRequest.ts
@@ -2211,37 +2301,6 @@ async function getResponse(app, ctx) {
2211
2301
  }
2212
2302
  }
2213
2303
 
2214
- // src/helpers/hash.ts
2215
- import * as crypto2 from "crypto";
2216
- import { getRandomValues } from "crypto";
2217
- import { promisify } from "util";
2218
- async function hash2(password) {
2219
- if ("Bun" in globalThis) {
2220
- return await Bun.password.hash(password, {
2221
- algorithm: "argon2id",
2222
- memoryCost: 65536,
2223
- timeCost: 3
2224
- });
2225
- }
2226
- if (!("argon2" in crypto2)) {
2227
- throw new Error(
2228
- "Password hashing needs argon2: run on Bun, or on Node 24+ where node:crypto provides it."
2229
- );
2230
- }
2231
- const nonce = getRandomValues(new Uint8Array(32));
2232
- const argon23 = promisify(crypto2.argon2);
2233
- const buf = await argon23("argon2id", {
2234
- message: Buffer.from(password),
2235
- nonce,
2236
- parallelism: 1,
2237
- tagLength: 32,
2238
- memory: 65536,
2239
- passes: 3
2240
- });
2241
- const b64 = (bytes) => Buffer.from(bytes).toString("base64").replace(/=+$/, "");
2242
- return `$argon2id$v=19$m=65536,t=3,p=1$${b64(nonce)}$${b64(buf)}`;
2243
- }
2244
-
2245
2304
  // src/helpers/iteratorAsyncToReadable.ts
2246
2305
  function iteratorAsyncToReadable(asyncGenerator) {
2247
2306
  let cancelled = false;
@@ -2332,142 +2391,19 @@ function toWeb(nodeStream) {
2332
2391
  });
2333
2392
  }
2334
2393
 
2335
- // src/helpers/verify.ts
2336
- import * as crypto3 from "crypto";
2337
- async function verify2(password, hash3) {
2338
- if ("Bun" in globalThis) {
2339
- return Bun.password.verify(password, hash3, "argon2id");
2340
- }
2341
- const match = /^\$argon2(id|i|d)\$v=(\d+)\$m=(\d+),t=(\d+),p=(\d+)\$([^$]+)\$([^$]+)$/.exec(
2342
- hash3
2343
- );
2344
- if (!match) throw new Error("Invalid Argon2 hash format");
2345
- const [, variant, , memory, passes, parallelism, saltB64, hashB64] = match;
2346
- const nonce = Buffer.from(saltB64, "base64");
2347
- const expected = Buffer.from(hashB64, "base64");
2348
- return new Promise((resolve, reject) => {
2349
- crypto3.argon2(
2350
- `argon2${variant}`,
2351
- {
2352
- message: password,
2353
- nonce,
2354
- memory: parseInt(memory, 10),
2355
- passes: parseInt(passes, 10),
2356
- parallelism: parseInt(parallelism, 10),
2357
- tagLength: expected.length
2358
- },
2359
- (err, derivedKey) => {
2360
- if (err) return reject(err);
2361
- if (derivedKey.length !== expected.length) return resolve(false);
2362
- resolve(crypto3.timingSafeEqual(derivedKey, expected));
2363
- }
2364
- );
2365
- });
2366
- }
2367
-
2368
- // src/auth/getUser.ts
2369
- async function getJwtUser(ctx) {
2370
- const header = ctx.headers.authorization;
2371
- if (!header) return;
2372
- const [type2, token] = header.trim().split(" ");
2373
- if (type2?.toLowerCase() !== "bearer" || !token) {
2374
- throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
2375
- }
2376
- const payload = await verifyJwt(token, ctx.options.secret);
2377
- if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
2378
- const { iat, exp, ...claims } = payload;
2379
- if (!claims.id || !claims.email) throw ServerError_default.AUTH_INVALID_TOKEN();
2380
- if (!ctx.options.auth.providers.includes(claims.provider)) {
2381
- throw ServerError_default.AUTH_INVALID_PROVIDER({
2382
- provider: claims.provider,
2383
- valid: ctx.options.auth.providers
2384
- });
2385
- }
2386
- const exposed = await ctx.options.auth.onUser(claims, ctx);
2387
- assertUser(exposed, "onUser");
2388
- return exposed;
2389
- }
2390
- async function getAuthSession(ctx) {
2391
- const id = findSessionId(ctx);
2392
- if (!id) return;
2393
- const session = await ctx.options.auth.sessions.get(id);
2394
- return session?.user ? session : void 0;
2395
- }
2396
- async function getUser(ctx) {
2397
- if (!ctx.options.auth) return;
2398
- const options = ctx.options.auth;
2399
- if (options.strategy.includes("jwt")) return getJwtUser(ctx);
2400
- const auth2 = await getAuthSession(ctx);
2401
- if (!auth2) return;
2402
- if (!options.providers.includes(auth2.provider)) {
2403
- throw ServerError_default.AUTH_INVALID_PROVIDER({
2404
- provider: auth2.provider,
2405
- valid: options.providers
2406
- });
2407
- }
2408
- const user = await options.users.get(auth2.user);
2409
- if (!user) throw ServerError_default.AUTH_NO_USER();
2410
- const exposed = await options.onUser(user, ctx);
2411
- assertUser(exposed, "onUser");
2412
- return exposed;
2413
- }
2414
-
2415
- // src/auth/logout.ts
2416
- async function logout(ctx) {
2417
- const { strategy } = ctx.options.auth;
2418
- if (!strategy.includes("jwt")) {
2419
- const prev = findSessionId(ctx);
2420
- if (prev) await ctx.options.auth.sessions.del(prev);
2421
- }
2422
- if (ctx.options.auth.onLogout) await ctx.options.auth.onLogout(ctx);
2423
- if (strategy.includes("token") || strategy.includes("jwt")) {
2424
- return { token: null };
2425
- }
2426
- if (strategy.includes("cookie")) {
2427
- return cookies({ session: null }).redirect("/");
2428
- }
2429
- throw new Error("Unknown auth type");
2430
- }
2431
-
2432
2394
  // src/auth/index.ts
2433
- var oauth2 = [
2434
- "github",
2435
- "google",
2436
- "microsoft",
2437
- "discord",
2438
- "facebook"
2439
- ];
2440
2395
  function auth(app) {
2396
+ const entries = app.settings.auth;
2441
2397
  app.use(async function middle(ctx) {
2442
- ctx.user = await getUser(ctx);
2398
+ for (const entry3 of entries) {
2399
+ const user = await entry3.user(ctx);
2400
+ if (user) {
2401
+ ctx.user = user;
2402
+ return;
2403
+ }
2404
+ }
2443
2405
  });
2444
- const spec = { schema: { tags: "auth" } };
2445
- app.post("/auth/logout", spec, logout);
2446
- const enabled = app.settings.auth.providers;
2447
- for (const name of oauth2) {
2448
- if (!enabled.includes(name)) continue;
2449
- const key = name.toUpperCase();
2450
- if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
2451
- if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
2452
- app.get(`/auth/login/${name}`, spec, providers_default[name].login);
2453
- app.get(`/auth/callback/${name}`, spec, providers_default[name].callback);
2454
- app.post(`/auth/verify/${name}`, spec, providers_default[name].verify);
2455
- }
2456
- if (enabled.includes("apple")) {
2457
- const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
2458
- for (const key of keys) {
2459
- if (!env[key]) throw new Error(`${key} not defined`);
2460
- }
2461
- app.get("/auth/login/apple", spec, providers_default.apple.login);
2462
- app.post("/auth/callback/apple", spec, providers_default.apple.callback);
2463
- app.post("/auth/verify/apple", spec, providers_default.apple.verify);
2464
- }
2465
- if (enabled.includes("email")) {
2466
- app.post("/auth/register/email", spec, providers_default.email.register);
2467
- app.post("/auth/login/email", spec, providers_default.email.login);
2468
- app.put("/auth/password/email", spec, providers_default.email.password);
2469
- app.put("/auth/reset/email", spec, providers_default.email.reset);
2470
- }
2406
+ for (const entry3 of entries) entry3.routes?.(app);
2471
2407
  }
2472
2408
 
2473
2409
  // src/helpers/parseRange.ts
@@ -2503,35 +2439,35 @@ async function assets(ctx) {
2503
2439
  const key = ctx.url.pathname.replace(/^\/+/, "");
2504
2440
  const file2 = ctx.options.public.file(key);
2505
2441
  const info = file2.info?.bind(file2);
2506
- const meta = info ? await info() : null;
2507
- if (info ? !meta : !await file2.exists()) return;
2442
+ const meta2 = info ? await info() : null;
2443
+ if (info ? !meta2 : !await file2.exists()) return;
2508
2444
  const ext = ctx.url.pathname.split(".").pop()?.toLowerCase();
2509
- const ctype = ext && mimes_default[ext] || meta?.type || ext;
2445
+ const ctype = ext && mimes_default[ext] || meta2?.type || ext;
2510
2446
  const headers2 = { "cache-control": CACHE_CONTROL };
2511
2447
  let tag;
2512
- if (meta) {
2513
- const stamp = meta.modified ? meta.modified.getTime() : 0;
2514
- tag = `W/"${meta.size.toString(16)}-${stamp.toString(16)}"`;
2448
+ if (meta2) {
2449
+ const stamp = meta2.modified ? meta2.modified.getTime() : 0;
2450
+ tag = `W/"${meta2.size.toString(16)}-${stamp.toString(16)}"`;
2515
2451
  headers2.etag = tag;
2516
- if (meta.modified) headers2["last-modified"] = meta.modified.toUTCString();
2452
+ if (meta2.modified) headers2["last-modified"] = meta2.modified.toUTCString();
2517
2453
  }
2518
- const canRange = !!(meta && file2.slice);
2454
+ const canRange = !!(meta2 && file2.slice);
2519
2455
  if (canRange) headers2["accept-ranges"] = "bytes";
2520
2456
  if (tag && ctx.headers["if-none-match"] === tag) {
2521
2457
  return status(304).headers(headers2).send();
2522
2458
  }
2523
2459
  const rangeHeader = ctx.headers.range;
2524
2460
  const ifRange = ctx.headers["if-range"];
2525
- if (meta && file2.slice && rangeHeader && (!ifRange || ifRange === tag)) {
2526
- const range = parseRange(rangeHeader, meta.size);
2461
+ if (meta2 && file2.slice && rangeHeader && (!ifRange || ifRange === tag)) {
2462
+ const range = parseRange(rangeHeader, meta2.size);
2527
2463
  if (range === "unsatisfiable") {
2528
- return status(416).headers({ ...headers2, "content-range": `bytes */${meta.size}` }).send();
2464
+ return status(416).headers({ ...headers2, "content-range": `bytes */${meta2.size}` }).send();
2529
2465
  }
2530
2466
  if (range) {
2531
2467
  const { start, end } = range;
2532
2468
  return type(ctype).status(206).headers({
2533
2469
  ...headers2,
2534
- "content-range": `bytes ${start}-${end}/${meta.size}`,
2470
+ "content-range": `bytes ${start}-${end}/${meta2.size}`,
2535
2471
  "content-length": String(end - start + 1)
2536
2472
  }).send(file2.slice(start, end + 1).stream());
2537
2473
  }
@@ -2602,7 +2538,7 @@ var generateOpenApiPaths = async (handlers, specPath) => {
2602
2538
  for (const [method, routes] of Object.entries(handlers)) {
2603
2539
  for (const route of routes) {
2604
2540
  const path = route.path;
2605
- const meta = route.options ?? {};
2541
+ const meta2 = route.options ?? {};
2606
2542
  const config2 = getConfig(route.options?.schema);
2607
2543
  if (typeof path !== "string" || path === "*" || path === specPath) {
2608
2544
  continue;
@@ -2613,13 +2549,13 @@ var generateOpenApiPaths = async (handlers, specPath) => {
2613
2549
  paths[normalizedPath] = {};
2614
2550
  }
2615
2551
  let requestBody;
2616
- if (meta?.body) {
2617
- const schema = await toJsonSchema(meta.body);
2552
+ if (meta2?.body) {
2553
+ const schema = await toJsonSchema(meta2.body);
2618
2554
  requestBody = { content: { "application/json": { schema } } };
2619
2555
  }
2620
2556
  let responses;
2621
- if (meta?.response) {
2622
- const schema = await toJsonSchema(meta.response);
2557
+ if (meta2?.response) {
2558
+ const schema = await toJsonSchema(meta2.response);
2623
2559
  responses = {
2624
2560
  200: { description: "OK", content: { "application/json": { schema } } }
2625
2561
  };
@@ -2635,8 +2571,8 @@ var generateOpenApiPaths = async (handlers, specPath) => {
2635
2571
  schema: { type: type2 }
2636
2572
  });
2637
2573
  });
2638
- if (meta?.query) {
2639
- const schema = await toJsonSchema(meta.query);
2574
+ if (meta2?.query) {
2575
+ const schema = await toJsonSchema(meta2.query);
2640
2576
  for (const [name, prop] of Object.entries(schema.properties ?? {})) {
2641
2577
  parameters.push({
2642
2578
  name,
@@ -2709,7 +2645,10 @@ function timer(ctx) {
2709
2645
  async function socketUser(app, headers2, cookies2) {
2710
2646
  if (!app.settings.auth) return void 0;
2711
2647
  const ctx = { options: app.settings, headers: headers2, cookies: cookies2 };
2712
- return getUser(ctx);
2648
+ for (const entry3 of app.settings.auth) {
2649
+ const user = await entry3.user(ctx);
2650
+ if (user) return user;
2651
+ }
2713
2652
  }
2714
2653
 
2715
2654
  // src/helpers/wsNode.ts
@@ -3208,8 +3147,7 @@ function ServerTest(app) {
3208
3147
  }
3209
3148
 
3210
3149
  // src/index.ts
3211
- import { default as default2 } from "polystore";
3212
- import { default as default3 } from "bucket";
3150
+ import { default as default2 } from "bucket";
3213
3151
  var Server = class extends Router {
3214
3152
  settings;
3215
3153
  platform;
@@ -3274,7 +3212,7 @@ export {
3274
3212
  Server,
3275
3213
  ServerError_default as ServerError,
3276
3214
  ValidationError,
3277
- default3 as bucket,
3215
+ default2 as bucket,
3278
3216
  cache,
3279
3217
  cookies,
3280
3218
  server as default,
@@ -3282,7 +3220,6 @@ export {
3282
3220
  file,
3283
3221
  headers,
3284
3222
  json,
3285
- default2 as kv,
3286
3223
  redirect,
3287
3224
  router,
3288
3225
  send,