@server/next 0.46.0 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +94 -53
- package/index.js +747 -816
- 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
|
|
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(
|
|
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));
|
|
@@ -1062,595 +1012,746 @@ async function verifyJwt(token, secret) {
|
|
|
1062
1012
|
return payload;
|
|
1063
1013
|
}
|
|
1064
1014
|
|
|
1065
|
-
// src/auth/
|
|
1066
|
-
var
|
|
1067
|
-
|
|
1068
|
-
|
|
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) {
|
|
1069
1030
|
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
1070
1031
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
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));
|
|
1135
|
+
}
|
|
1136
|
+
return `${base}?${query}`;
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/auth/providers/antarctic.ts
|
|
1140
|
+
var nowhere = {
|
|
1141
|
+
get: async () => null,
|
|
1142
|
+
set: async () => {
|
|
1143
|
+
},
|
|
1144
|
+
del: async () => {
|
|
1073
1145
|
}
|
|
1074
|
-
return id;
|
|
1075
1146
|
};
|
|
1076
|
-
function
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
provider: input.provider,
|
|
1092
|
-
created: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
|
|
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
|
+
});
|
|
1093
1162
|
};
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
+
}
|
|
1098
1187
|
};
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
+
fortytwo: FortyTwo
|
|
1255
|
+
};
|
|
1256
|
+
var ALIASES = {
|
|
1257
|
+
cognito: "amazoncognito",
|
|
1258
|
+
entra: "microsoftentraid",
|
|
1259
|
+
microsoft: "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] = antarcticProvider(alias, CLASSES[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();
|
|
1134
1284
|
});
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
assertUser(exposed, "onUser");
|
|
1138
|
-
return reply.status(201).json(exposed);
|
|
1139
|
-
}
|
|
1140
|
-
return reply.redirect(settings.redirect);
|
|
1285
|
+
doc.catch(() => discovered.delete(issuer));
|
|
1286
|
+
discovered.set(issuer, doc);
|
|
1141
1287
|
}
|
|
1142
|
-
|
|
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
|
+
};
|
|
1143
1345
|
}
|
|
1144
1346
|
|
|
1145
1347
|
// src/auth/state.ts
|
|
1146
|
-
var
|
|
1147
|
-
|
|
1148
|
-
|
|
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);
|
|
1149
1352
|
return {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
secure: crossSite || ctx.platform.production,
|
|
1157
|
-
sameSite: crossSite ? "None" : "Lax"
|
|
1158
|
-
}
|
|
1353
|
+
value,
|
|
1354
|
+
path: "/",
|
|
1355
|
+
expires: EXPIRES,
|
|
1356
|
+
httpOnly: true,
|
|
1357
|
+
secure: ctx.platform.production,
|
|
1358
|
+
sameSite: "Lax"
|
|
1159
1359
|
};
|
|
1160
1360
|
}
|
|
1161
|
-
function
|
|
1162
|
-
const
|
|
1163
|
-
if (!
|
|
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) {
|
|
1164
1366
|
throw ServerError_default.AUTH_INVALID_STATE();
|
|
1165
1367
|
}
|
|
1166
|
-
|
|
1167
|
-
function clearState() {
|
|
1168
|
-
return createCookies(NAME, { value: null });
|
|
1368
|
+
return pending;
|
|
1169
1369
|
}
|
|
1170
1370
|
|
|
1171
|
-
// src/auth/
|
|
1172
|
-
var
|
|
1173
|
-
var TOKEN = "https://appleid.apple.com/auth/token";
|
|
1174
|
-
var b64url2 = (data) => {
|
|
1175
|
-
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1176
|
-
let bin = "";
|
|
1177
|
-
for (const byte of bytes) bin += String.fromCharCode(byte);
|
|
1178
|
-
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1179
|
-
};
|
|
1180
|
-
var b64urlJson = (segment) => {
|
|
1181
|
-
let b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
1182
|
-
b64 += "=".repeat((4 - b64.length % 4) % 4);
|
|
1183
|
-
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
1184
|
-
return JSON.parse(new TextDecoder().decode(bytes));
|
|
1185
|
-
};
|
|
1186
|
-
var clientSecret = async () => {
|
|
1187
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1188
|
-
const header = { alg: "ES256", kid: env.APPLE_KEY_ID, typ: "JWT" };
|
|
1189
|
-
const payload = {
|
|
1190
|
-
iss: env.APPLE_TEAM_ID,
|
|
1191
|
-
iat: now,
|
|
1192
|
-
exp: now + 3600,
|
|
1193
|
-
aud: "https://appleid.apple.com",
|
|
1194
|
-
sub: env.APPLE_ID
|
|
1195
|
-
};
|
|
1196
|
-
const data = `${b64url2(JSON.stringify(header))}.${b64url2(JSON.stringify(payload))}`;
|
|
1197
|
-
const pem = String(env.APPLE_PRIVATE_KEY).replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
|
1198
|
-
const der = Uint8Array.from(atob(pem), (c) => c.charCodeAt(0));
|
|
1199
|
-
const key = await crypto.subtle.importKey(
|
|
1200
|
-
"pkcs8",
|
|
1201
|
-
der,
|
|
1202
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1203
|
-
false,
|
|
1204
|
-
["sign"]
|
|
1205
|
-
);
|
|
1206
|
-
const sig = await crypto.subtle.sign(
|
|
1207
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
1208
|
-
key,
|
|
1209
|
-
new TextEncoder().encode(data)
|
|
1210
|
-
);
|
|
1211
|
-
return `${data}.${b64url2(new Uint8Array(sig))}`;
|
|
1212
|
-
};
|
|
1213
|
-
var login = (ctx) => {
|
|
1214
|
-
const { state, cookie } = startState(ctx, true);
|
|
1215
|
-
const params = new URLSearchParams({
|
|
1216
|
-
client_id: env.APPLE_ID,
|
|
1217
|
-
redirect_uri: `${ctx.url.origin}/auth/callback/apple`,
|
|
1218
|
-
response_type: "code",
|
|
1219
|
-
scope: "name email",
|
|
1220
|
-
// Requesting scopes forces Apple to POST the result back (form_post)
|
|
1221
|
-
response_mode: "form_post",
|
|
1222
|
-
state
|
|
1223
|
-
});
|
|
1224
|
-
return cookies("oauth_state", cookie).redirect(`${AUTHORIZE}?${params}`);
|
|
1225
|
-
};
|
|
1226
|
-
var exchange = async (code, redirectUri, user) => {
|
|
1227
|
-
const params = new URLSearchParams({
|
|
1228
|
-
client_id: env.APPLE_ID,
|
|
1229
|
-
client_secret: await clientSecret(),
|
|
1230
|
-
code: code ?? "",
|
|
1231
|
-
grant_type: "authorization_code"
|
|
1232
|
-
});
|
|
1233
|
-
if (redirectUri) params.set("redirect_uri", redirectUri);
|
|
1234
|
-
const tokenRes = await fetch(TOKEN, {
|
|
1235
|
-
method: "POST",
|
|
1236
|
-
headers: {
|
|
1237
|
-
accept: "application/json",
|
|
1238
|
-
"content-type": "application/x-www-form-urlencoded"
|
|
1239
|
-
},
|
|
1240
|
-
body: params
|
|
1241
|
-
});
|
|
1242
|
-
if (!tokenRes.ok) throw new Error("apple: token exchange failed");
|
|
1243
|
-
const token = await tokenRes.json();
|
|
1244
|
-
const claims = b64urlJson(token.id_token.split(".")[1]);
|
|
1245
|
-
let name;
|
|
1246
|
-
if (user) {
|
|
1247
|
-
const parsed = JSON.parse(user).name;
|
|
1248
|
-
if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
|
|
1249
|
-
}
|
|
1250
|
-
return { ...claims, name };
|
|
1251
|
-
};
|
|
1252
|
-
var finish = async (ctx, raw, opts) => {
|
|
1253
|
-
const { onProfile } = ctx.options.auth;
|
|
1254
|
-
const profile = onProfile ? await onProfile(raw, "apple") : { id: raw.sub, name: raw.name, email: raw.email };
|
|
1255
|
-
assertUser(profile, "onProfile");
|
|
1256
|
-
return finishLogin(
|
|
1257
|
-
ctx,
|
|
1258
|
-
{
|
|
1259
|
-
provider: "apple",
|
|
1260
|
-
key: profile.id,
|
|
1261
|
-
email: profile.email,
|
|
1262
|
-
user: profile
|
|
1263
|
-
},
|
|
1264
|
-
opts
|
|
1265
|
-
);
|
|
1266
|
-
};
|
|
1267
|
-
var callback = async (ctx) => {
|
|
1268
|
-
const body = ctx.body || {};
|
|
1269
|
-
checkState(ctx, body.state);
|
|
1270
|
-
const url = `${ctx.url.origin}/auth/callback/apple`;
|
|
1271
|
-
const raw = await exchange(body.code, url, body.user);
|
|
1272
|
-
const res = await finish(ctx, raw);
|
|
1273
|
-
res.headers.append("set-cookie", clearState());
|
|
1274
|
-
return res;
|
|
1275
|
-
};
|
|
1276
|
-
var verify = async (ctx) => {
|
|
1277
|
-
const { code, redirect_uri, user } = ctx.body ?? {};
|
|
1278
|
-
if (!code) throw ServerError_default.AUTH_NO_CODE();
|
|
1279
|
-
const raw = await exchange(code, redirect_uri, user);
|
|
1280
|
-
return finish(ctx, raw, { json: true });
|
|
1281
|
-
};
|
|
1282
|
-
var apple_default = { login, callback, verify };
|
|
1283
|
-
|
|
1284
|
-
// src/auth/providers/oauth.ts
|
|
1371
|
+
// src/auth/flow.ts
|
|
1372
|
+
var SPEC = { schema: { tags: "auth" } };
|
|
1285
1373
|
var wantsJson = (ctx) => String(ctx.headers.accept || "").includes("application/json");
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
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
|
+
);
|
|
1303
1390
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
+
);
|
|
1309
1404
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
if (value) body.set(key, value);
|
|
1323
|
-
}
|
|
1324
|
-
const tokenRes = await fetch(config2.tokenUrl, {
|
|
1325
|
-
method: "POST",
|
|
1326
|
-
headers: {
|
|
1327
|
-
accept: "application/json",
|
|
1328
|
-
"content-type": "application/x-www-form-urlencoded"
|
|
1329
|
-
},
|
|
1330
|
-
body
|
|
1331
|
-
});
|
|
1332
|
-
if (!tokenRes.ok) throw new Error(`${config2.name}: token exchange failed`);
|
|
1333
|
-
const token = await tokenRes.json();
|
|
1334
|
-
const profileRes = await fetch(config2.profileUrl, {
|
|
1335
|
-
headers: {
|
|
1336
|
-
accept: "application/json",
|
|
1337
|
-
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
|
+
);
|
|
1338
1417
|
}
|
|
1339
|
-
})
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
return finishLogin(
|
|
1348
|
-
ctx,
|
|
1349
|
-
{
|
|
1350
|
-
provider: config2.name,
|
|
1351
|
-
key: profile.id,
|
|
1352
|
-
email: profile.email,
|
|
1353
|
-
user: profile
|
|
1354
|
-
},
|
|
1355
|
-
opts
|
|
1356
|
-
);
|
|
1357
|
-
};
|
|
1358
|
-
const callback3 = async (ctx) => {
|
|
1359
|
-
checkState(ctx, ctx.url.query.state);
|
|
1360
|
-
const raw = await exchange2(ctx, ctx.url.query.code, {
|
|
1361
|
-
redirect_uri: callbackUrl(ctx)
|
|
1362
|
-
});
|
|
1363
|
-
const res = await finish3(ctx, raw);
|
|
1364
|
-
res.headers.append("set-cookie", clearState());
|
|
1365
|
-
return res;
|
|
1366
|
-
};
|
|
1367
|
-
const verify4 = async (ctx) => {
|
|
1368
|
-
const { code, redirect_uri, code_verifier } = ctx.body ?? {};
|
|
1369
|
-
if (!code) throw ServerError_default.AUTH_NO_CODE();
|
|
1370
|
-
const raw = await exchange2(ctx, code, { redirect_uri, code_verifier });
|
|
1371
|
-
return finish3(ctx, raw, { json: true });
|
|
1372
|
-
};
|
|
1373
|
-
return { login: login3, callback: callback3, verify: verify4 };
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// src/auth/providers/discord.ts
|
|
1377
|
-
var discord_default = oauthProvider({
|
|
1378
|
-
name: "discord",
|
|
1379
|
-
authorizeUrl: "https://discord.com/oauth2/authorize",
|
|
1380
|
-
tokenUrl: "https://discord.com/api/oauth2/token",
|
|
1381
|
-
profileUrl: "https://discord.com/api/users/@me",
|
|
1382
|
-
scope: "identify email",
|
|
1383
|
-
profile: (p) => ({
|
|
1384
|
-
id: p.id,
|
|
1385
|
-
email: p.email,
|
|
1386
|
-
name: p.global_name || p.username,
|
|
1387
|
-
picture: p.avatar ? `https://cdn.discordapp.com/avatars/${p.id}/${p.avatar}.png` : void 0
|
|
1388
|
-
})
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
|
-
// src/auth/updateUser.ts
|
|
1392
|
-
async function updateUser(user, auth2, store) {
|
|
1393
|
-
if (auth2.provider === "email") {
|
|
1394
|
-
return await store.set(auth2.email, user);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// src/auth/providers/email.ts
|
|
1399
|
-
async function emailLogin(ctx) {
|
|
1400
|
-
const { email, password } = ctx.body;
|
|
1401
|
-
if (!email) throw ServerError_default.LOGIN_NO_EMAIL();
|
|
1402
|
-
if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
|
|
1403
|
-
if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
|
|
1404
|
-
if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
|
|
1405
|
-
const users = ctx.options.auth.users;
|
|
1406
|
-
if (!await users.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
|
|
1407
|
-
const user = await users.get(email);
|
|
1408
|
-
const isValid = await verify2(password, user.password);
|
|
1409
|
-
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
1410
|
-
return finishLogin(ctx, {
|
|
1411
|
-
provider: "email",
|
|
1412
|
-
key: user.email,
|
|
1413
|
-
email: user.email,
|
|
1414
|
-
user
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
async function emailRegister(ctx) {
|
|
1418
|
-
const { email, password, ...data } = ctx.body;
|
|
1419
|
-
if (!email) throw ServerError_default.REGISTER_NO_EMAIL();
|
|
1420
|
-
if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
|
|
1421
|
-
if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
|
|
1422
|
-
if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
|
|
1423
|
-
const users = ctx.options.auth.users;
|
|
1424
|
-
if (await users.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
|
|
1425
|
-
const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
|
|
1426
|
-
const user = {
|
|
1427
|
-
id: createId(email),
|
|
1428
|
-
strategy: ctx.options.auth.strategy,
|
|
1429
|
-
provider: "email",
|
|
1430
|
-
email,
|
|
1431
|
-
password: await hash2(password),
|
|
1432
|
-
time,
|
|
1433
|
-
...data
|
|
1434
|
-
};
|
|
1435
|
-
return finishLogin(ctx, {
|
|
1436
|
-
provider: "email",
|
|
1437
|
-
key: email,
|
|
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 publicProfile = ({ id, email, name, avatar }) => ({
|
|
1425
|
+
id,
|
|
1438
1426
|
email,
|
|
1439
|
-
|
|
1427
|
+
name,
|
|
1428
|
+
avatar
|
|
1440
1429
|
});
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
profile: (p) => ({
|
|
1469
|
-
id: p.id,
|
|
1470
|
-
email: p.email,
|
|
1471
|
-
name: p.name,
|
|
1472
|
-
picture: p.picture?.data?.url
|
|
1473
|
-
})
|
|
1474
|
-
});
|
|
1475
|
-
|
|
1476
|
-
// src/auth/providers/github.ts
|
|
1477
|
-
var AUTHORIZE2 = "https://github.com/login/oauth/authorize";
|
|
1478
|
-
var oauth = async (code, extra) => {
|
|
1479
|
-
const fch = async (url, { body, headers: headers2 = {}, ...rest } = {}) => {
|
|
1480
|
-
headers2.accept = "application/json";
|
|
1481
|
-
headers2["content-type"] = "application/json";
|
|
1482
|
-
const res2 = await fetch(url, { ...rest, body, headers: headers2 });
|
|
1483
|
-
if (!res2.ok) throw new Error("Invalid request");
|
|
1484
|
-
return res2.json();
|
|
1485
|
-
};
|
|
1486
|
-
const params = {
|
|
1487
|
-
client_id: env.GITHUB_ID,
|
|
1488
|
-
client_secret: env.GITHUB_SECRET,
|
|
1489
|
-
code
|
|
1430
|
+
const redirects = typeof config2.redirect === "object" ? config2.redirect : {};
|
|
1431
|
+
const loginTo = typeof config2.redirect === "object" ? redirects.login : config2.redirect;
|
|
1432
|
+
const finish = async (ctx, profile) => {
|
|
1433
|
+
const payload = getUser ? await (async () => {
|
|
1434
|
+
const id = await onLogin(profile, ctx);
|
|
1435
|
+
if (id === void 0 || id === null) {
|
|
1436
|
+
throw new Error("`onLogin` must return the id the credential points at");
|
|
1437
|
+
}
|
|
1438
|
+
if (!isSigned(strategies[0])) return { sub: String(id) };
|
|
1439
|
+
const user2 = await getUser(String(id), ctx);
|
|
1440
|
+
return { user: await toPublicUser(user2) };
|
|
1441
|
+
})() : { user: publicProfile(profile) };
|
|
1442
|
+
const signed = { ...payload, provider: profile.provider };
|
|
1443
|
+
const token = await issue(ctx, signed, expires);
|
|
1444
|
+
const user = signed.user ?? await getUser(signed.sub, ctx);
|
|
1445
|
+
const to = await target(loginTo, "/", user, ctx);
|
|
1446
|
+
if (inCookie(strategies[0])) {
|
|
1447
|
+
return cookies("session", {
|
|
1448
|
+
value: token,
|
|
1449
|
+
path: "/",
|
|
1450
|
+
expires,
|
|
1451
|
+
httpOnly: true,
|
|
1452
|
+
secure: ctx.platform.production,
|
|
1453
|
+
sameSite: "Lax"
|
|
1454
|
+
}).redirect(to);
|
|
1455
|
+
}
|
|
1456
|
+
return redirect(`${to}#token=${token}`);
|
|
1490
1457
|
};
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
}
|
|
1458
|
+
return {
|
|
1459
|
+
name: "flow",
|
|
1460
|
+
async user(ctx) {
|
|
1461
|
+
const found = await read(ctx, strategies);
|
|
1462
|
+
if (!found) return;
|
|
1463
|
+
const { payload, strategy } = found;
|
|
1464
|
+
ctx.auth = meta(payload, strategy);
|
|
1465
|
+
if (payload.user) return payload.user;
|
|
1466
|
+
if (!payload.sub) return;
|
|
1467
|
+
return getUser(payload.sub, ctx);
|
|
1468
|
+
},
|
|
1469
|
+
routes(app) {
|
|
1470
|
+
for (const { name, options, provider } of list) {
|
|
1471
|
+
app.get(`/auth/login/${name}`, SPEC, async (ctx) => {
|
|
1472
|
+
const { url, state, payload } = await provider.authorize(ctx, options);
|
|
1473
|
+
const cookie = await startState(ctx, { state, payload });
|
|
1474
|
+
if (wantsJson(ctx)) {
|
|
1475
|
+
return cookies(NAME2, cookie).json({ url });
|
|
1476
|
+
}
|
|
1477
|
+
return cookies(NAME2, cookie).redirect(url);
|
|
1478
|
+
});
|
|
1479
|
+
const callback = async (ctx) => {
|
|
1480
|
+
const query = ctx.url.query;
|
|
1481
|
+
if (query.error) {
|
|
1482
|
+
const to = await target(redirects.error, "/", null, ctx);
|
|
1483
|
+
return redirect(`${to}?error=${encodeURIComponent(query.error)}`);
|
|
1484
|
+
}
|
|
1485
|
+
const pending = await readState(ctx, query.state);
|
|
1486
|
+
if (!query.code) throw ServerError_default.AUTH_NO_CODE();
|
|
1487
|
+
let res;
|
|
1488
|
+
try {
|
|
1489
|
+
const profile = await provider.exchange(
|
|
1490
|
+
ctx,
|
|
1491
|
+
options,
|
|
1492
|
+
query.code,
|
|
1493
|
+
pending
|
|
1494
|
+
);
|
|
1495
|
+
res = await finish(ctx, profile);
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
const to = await target(redirects.error, "/", null, ctx);
|
|
1498
|
+
const message = error.message;
|
|
1499
|
+
res = redirect(`${to}?error=${encodeURIComponent(message)}`);
|
|
1500
|
+
}
|
|
1501
|
+
res.headers.append(
|
|
1502
|
+
"set-cookie",
|
|
1503
|
+
`${NAME2}=; Path=/; Max-Age=0; HttpOnly`
|
|
1504
|
+
);
|
|
1505
|
+
return res;
|
|
1506
|
+
};
|
|
1507
|
+
app.get(`/auth/callback/${name}`, SPEC, callback);
|
|
1508
|
+
}
|
|
1509
|
+
app.post("/auth/logout", SPEC, async (ctx) => {
|
|
1510
|
+
const found = await read(ctx, strategies).catch(() => void 0);
|
|
1511
|
+
if (onLogout && found?.payload.sub) {
|
|
1512
|
+
await onLogout(found.payload.sub, ctx);
|
|
1513
|
+
}
|
|
1514
|
+
const to = await target(redirects.logout, "/", null, ctx);
|
|
1515
|
+
if (!inCookie(strategies[0])) return status(204);
|
|
1516
|
+
return cookies("session", { value: null }).redirect(to);
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1502
1519
|
};
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/auth/verify.ts
|
|
1523
|
+
var enc2 = new TextEncoder();
|
|
1524
|
+
var dec2 = new TextDecoder();
|
|
1525
|
+
var unb64url2 = (seg) => {
|
|
1526
|
+
let b64 = seg.replace(/-/g, "+").replace(/_/g, "/");
|
|
1527
|
+
b64 += "=".repeat((4 - b64.length % 4) % 4);
|
|
1528
|
+
const bin = atob(b64);
|
|
1529
|
+
const bytes = new Uint8Array(bin.length);
|
|
1530
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1531
|
+
return bytes;
|
|
1503
1532
|
};
|
|
1504
|
-
var
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
if (value) search.set(key, value);
|
|
1511
|
-
}
|
|
1512
|
-
return `${AUTHORIZE2}?${search}`;
|
|
1533
|
+
var ALGS = {
|
|
1534
|
+
RS256: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
1535
|
+
RS384: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" },
|
|
1536
|
+
RS512: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
|
|
1537
|
+
ES256: { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" },
|
|
1538
|
+
ES384: { name: "ECDSA", namedCurve: "P-384", hash: "SHA-384" }
|
|
1513
1539
|
};
|
|
1514
|
-
var
|
|
1515
|
-
|
|
1516
|
-
|
|
1540
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
1541
|
+
function keysOf(issuer, refresh = false) {
|
|
1542
|
+
let entry3 = cache2.get(issuer);
|
|
1543
|
+
if (!entry3 || refresh && Date.now() - entry3.at > 6e4) {
|
|
1544
|
+
const keys = (async () => {
|
|
1545
|
+
const url = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
|
|
1546
|
+
const discovery = await fetch(url).then((r2) => r2.json());
|
|
1547
|
+
const set = await fetch(discovery.jwks_uri).then((r2) => r2.json());
|
|
1548
|
+
const out = /* @__PURE__ */ new Map();
|
|
1549
|
+
for (const jwk of set.keys ?? []) {
|
|
1550
|
+
const algorithm = ALGS[jwk.alg];
|
|
1551
|
+
if (!algorithm) continue;
|
|
1552
|
+
out.set(
|
|
1553
|
+
jwk.kid,
|
|
1554
|
+
await crypto.subtle.importKey("jwk", jwk, algorithm, false, ["verify"])
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
return out;
|
|
1558
|
+
})();
|
|
1559
|
+
keys.catch(() => cache2.delete(issuer));
|
|
1560
|
+
entry3 = { at: Date.now(), keys };
|
|
1561
|
+
cache2.set(issuer, entry3);
|
|
1517
1562
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
const [
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
return { ...profile, email };
|
|
1563
|
+
return entry3.keys;
|
|
1564
|
+
}
|
|
1565
|
+
var bearer2 = (ctx) => {
|
|
1566
|
+
const header = ctx.headers.authorization;
|
|
1567
|
+
if (!header) return;
|
|
1568
|
+
const [type2, token] = header.trim().split(" ");
|
|
1569
|
+
if (type2?.toLowerCase() !== "bearer" || !token) {
|
|
1570
|
+
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
1571
|
+
}
|
|
1572
|
+
return token;
|
|
1529
1573
|
};
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1574
|
+
function entry2(options) {
|
|
1575
|
+
const { verify: issuer, audience } = options;
|
|
1576
|
+
const claimNames = options.audienceClaim ? Array.isArray(options.audienceClaim) ? options.audienceClaim : [options.audienceClaim] : ["aud"];
|
|
1577
|
+
if (!audience) {
|
|
1578
|
+
throw new Error(
|
|
1579
|
+
"`verify` needs an `audience`: one issuer serves many applications, and without it a token minted for another one is accepted here."
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
const allowed = Array.isArray(audience) ? audience : [audience];
|
|
1583
|
+
return {
|
|
1584
|
+
name: `verify:${issuer}`,
|
|
1585
|
+
async user(ctx) {
|
|
1586
|
+
const token = options.cookie ? ctx.cookies[options.cookie] : bearer2(ctx);
|
|
1587
|
+
if (!token) return;
|
|
1588
|
+
const claims2 = await check(token, issuer, allowed, claimNames);
|
|
1589
|
+
ctx.auth = {
|
|
1590
|
+
issuedAt: new Date((claims2.iat ?? 0) * 1e3),
|
|
1591
|
+
expiresAt: claims2.exp ? new Date(claims2.exp * 1e3) : void 0,
|
|
1592
|
+
strategy: options.cookie ? "cookie" : "jwt",
|
|
1593
|
+
provider: issuer
|
|
1594
|
+
};
|
|
1595
|
+
if (!options.getUser) return claims2;
|
|
1596
|
+
return options.getUser(claims2.sub, ctx);
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
async function check(token, issuer, allowed, claimNames) {
|
|
1601
|
+
const parts = token.split(".");
|
|
1602
|
+
if (parts.length !== 3) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1603
|
+
const [head, body, sig] = parts;
|
|
1604
|
+
let header;
|
|
1605
|
+
let claims2;
|
|
1606
|
+
try {
|
|
1607
|
+
header = JSON.parse(dec2.decode(unb64url2(head)));
|
|
1608
|
+
claims2 = JSON.parse(dec2.decode(unb64url2(body)));
|
|
1609
|
+
} catch {
|
|
1610
|
+
throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1611
|
+
}
|
|
1612
|
+
const algorithm = ALGS[header?.alg];
|
|
1613
|
+
if (!algorithm) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1614
|
+
let key = (await keysOf(issuer)).get(header.kid);
|
|
1615
|
+
if (!key) key = (await keysOf(issuer, true)).get(header.kid);
|
|
1616
|
+
if (!key) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1617
|
+
const ok = await crypto.subtle.verify(
|
|
1618
|
+
algorithm.name === "ECDSA" ? { name: "ECDSA", hash: algorithm.hash } : algorithm,
|
|
1619
|
+
key,
|
|
1620
|
+
unb64url2(sig),
|
|
1621
|
+
enc2.encode(`${head}.${body}`)
|
|
1551
1622
|
);
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
return
|
|
1565
|
-
}
|
|
1566
|
-
var github_default = { login: login2, callback: callback2, verify: verify3 };
|
|
1567
|
-
|
|
1568
|
-
// src/auth/providers/google.ts
|
|
1569
|
-
var google_default = oauthProvider({
|
|
1570
|
-
name: "google",
|
|
1571
|
-
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1572
|
-
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1573
|
-
profileUrl: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
1574
|
-
scope: "openid email profile",
|
|
1575
|
-
profile: (p) => ({
|
|
1576
|
-
id: p.sub,
|
|
1577
|
-
email: p.email,
|
|
1578
|
-
name: p.name,
|
|
1579
|
-
picture: p.picture
|
|
1580
|
-
})
|
|
1581
|
-
});
|
|
1582
|
-
|
|
1583
|
-
// src/auth/providers/microsoft.ts
|
|
1584
|
-
var microsoft_default = oauthProvider({
|
|
1585
|
-
name: "microsoft",
|
|
1586
|
-
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
1587
|
-
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
1588
|
-
profileUrl: "https://graph.microsoft.com/v1.0/me",
|
|
1589
|
-
scope: "openid email profile User.Read",
|
|
1590
|
-
profile: (p) => ({
|
|
1591
|
-
// Personal accounts expose `userPrincipalName` rather than `mail`
|
|
1592
|
-
id: p.id,
|
|
1593
|
-
email: p.mail || p.userPrincipalName,
|
|
1594
|
-
name: p.displayName
|
|
1595
|
-
})
|
|
1596
|
-
});
|
|
1623
|
+
if (!ok) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1624
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1625
|
+
if (claims2.exp && now >= claims2.exp) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1626
|
+
if (claims2.nbf && now < claims2.nbf) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1627
|
+
if (claims2.iss !== issuer) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1628
|
+
const name = claimNames.find((one) => claims2[one] !== void 0);
|
|
1629
|
+
if (!name) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1630
|
+
const value = claims2[name];
|
|
1631
|
+
const aud = Array.isArray(value) ? value : [value];
|
|
1632
|
+
if (!aud.some((one) => allowed.includes(one))) {
|
|
1633
|
+
throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1634
|
+
}
|
|
1635
|
+
return claims2;
|
|
1636
|
+
}
|
|
1597
1637
|
|
|
1598
|
-
// src/auth/
|
|
1599
|
-
var
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1638
|
+
// src/auth/vendors.ts
|
|
1639
|
+
var VENDORS = {
|
|
1640
|
+
clerk: {
|
|
1641
|
+
cookie: "__session",
|
|
1642
|
+
// Clerk session tokens carry no `aud`: the authorized party (your
|
|
1643
|
+
// frontend origin) is in `azp`, which is what their own SDK checks
|
|
1644
|
+
audience: "your frontend origin, like https://app.example.com",
|
|
1645
|
+
claim: "azp",
|
|
1646
|
+
docs: "https://clerk.com/docs/backend-requests/resources/session-tokens"
|
|
1647
|
+
},
|
|
1648
|
+
firebase: {
|
|
1649
|
+
// The client SDK holds the token and sends it as a header, so no cookie.
|
|
1650
|
+
// Both halves are the project id: the issuer is per-project, and it is
|
|
1651
|
+
// what Firebase puts in `aud`.
|
|
1652
|
+
audience: "your Firebase project id",
|
|
1653
|
+
docs: "https://firebase.google.com/docs/auth/admin/verify-id-tokens"
|
|
1654
|
+
},
|
|
1655
|
+
// Google Cloud Identity Platform is the same service, and the same tokens,
|
|
1656
|
+
// under its enterprise name
|
|
1657
|
+
gcip: {
|
|
1658
|
+
audience: "your Google Cloud project id",
|
|
1659
|
+
docs: "https://cloud.google.com/identity-platform/docs/how-to-verify-tokens"
|
|
1660
|
+
},
|
|
1661
|
+
supabase: {
|
|
1662
|
+
audience: '"authenticated"',
|
|
1663
|
+
docs: "https://supabase.com/docs/guides/auth/jwts"
|
|
1664
|
+
}
|
|
1607
1665
|
};
|
|
1666
|
+
var vendors_default = VENDORS;
|
|
1608
1667
|
|
|
1609
|
-
// src/auth/
|
|
1610
|
-
|
|
1611
|
-
function defaultOnUser(fullUser) {
|
|
1612
|
-
const { password: _password, ...user } = fullUser;
|
|
1613
|
-
return user;
|
|
1614
|
-
}
|
|
1615
|
-
var available = Object.keys(providers_default);
|
|
1616
|
-
function parseAuthOptions(auth2) {
|
|
1668
|
+
// src/auth/parse.ts
|
|
1669
|
+
function parseAuth(auth2) {
|
|
1617
1670
|
if (!auth2) return null;
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1671
|
+
const list = Array.isArray(auth2) ? auth2 : [auth2];
|
|
1672
|
+
return list.flatMap((one) => toEntry(one));
|
|
1673
|
+
}
|
|
1674
|
+
function vendorEntry(strategy, name) {
|
|
1675
|
+
const vendor = vendors_default[name];
|
|
1676
|
+
const KEY = name.toUpperCase();
|
|
1677
|
+
if (strategy !== "jwt" && strategy !== "cookie") {
|
|
1678
|
+
throw new Error(
|
|
1679
|
+
`"${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.`
|
|
1680
|
+
);
|
|
1621
1681
|
}
|
|
1622
|
-
if (!
|
|
1623
|
-
throw new Error(
|
|
1682
|
+
if (strategy === "cookie" && !vendor.cookie) {
|
|
1683
|
+
throw new Error(
|
|
1684
|
+
`"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 }.`
|
|
1685
|
+
);
|
|
1624
1686
|
}
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1687
|
+
const issuer = globalThis.env[`${KEY}_ISSUER`];
|
|
1688
|
+
if (!issuer) {
|
|
1689
|
+
throw new Error(
|
|
1690
|
+
`${KEY}_ISSUER is not set, and it differs per account, so it cannot be guessed. See ${vendor.docs}`
|
|
1691
|
+
);
|
|
1629
1692
|
}
|
|
1630
|
-
const
|
|
1631
|
-
if (
|
|
1693
|
+
const audience = globalThis.env[`${KEY}_AUDIENCE`];
|
|
1694
|
+
if (!audience) {
|
|
1632
1695
|
throw new Error(
|
|
1633
|
-
|
|
1696
|
+
`${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.`
|
|
1634
1697
|
);
|
|
1635
1698
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
}
|
|
1699
|
+
return entry2({
|
|
1700
|
+
verify: issuer,
|
|
1701
|
+
audience,
|
|
1702
|
+
...vendor.claim ? { audienceClaim: vendor.claim } : {},
|
|
1703
|
+
...strategy === "cookie" ? { cookie: vendor.cookie } : {}
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
function toEntry(auth2) {
|
|
1707
|
+
if (typeof auth2 === "string") {
|
|
1708
|
+
const [strategy, name] = auth2.split(":");
|
|
1709
|
+
if (!name) {
|
|
1710
|
+
throw new Error(
|
|
1711
|
+
`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.`
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
if (vendors_default[name]) return [vendorEntry(strategy, name)];
|
|
1715
|
+
return [entry({ strategy, providers: name })];
|
|
1716
|
+
}
|
|
1717
|
+
if (typeof auth2 === "function") {
|
|
1718
|
+
return [{ name: "function", user: async (ctx) => auth2(ctx) }];
|
|
1719
|
+
}
|
|
1720
|
+
if (auth2 && typeof auth2 === "object") {
|
|
1721
|
+
if ("verify" in auth2) return [entry2(auth2)];
|
|
1722
|
+
if ("providers" in auth2) return [entry(auth2)];
|
|
1723
|
+
if ("handler" in auth2) {
|
|
1724
|
+
const instance = auth2;
|
|
1725
|
+
const path = (instance.path ?? "/api/auth").replace(/\/$/, "");
|
|
1726
|
+
const raw = { parser: "stream" };
|
|
1727
|
+
const forward = (ctx) => instance.handler(
|
|
1728
|
+
new Request(ctx.url.href, {
|
|
1729
|
+
method: ctx.method,
|
|
1730
|
+
headers: ctx.headers,
|
|
1731
|
+
body: ctx.body,
|
|
1732
|
+
// Required by fetch whenever a body is a stream
|
|
1733
|
+
...ctx.body ? { duplex: "half" } : {}
|
|
1734
|
+
})
|
|
1735
|
+
);
|
|
1736
|
+
return [
|
|
1737
|
+
{
|
|
1738
|
+
name: `instance:${path}`,
|
|
1739
|
+
user: async (ctx) => instance.user?.(ctx),
|
|
1740
|
+
routes: (app) => {
|
|
1741
|
+
const wildcard = `${path}/*`;
|
|
1742
|
+
app.get(wildcard, raw, forward);
|
|
1743
|
+
app.post(wildcard, raw, forward);
|
|
1744
|
+
app.put(wildcard, raw, forward);
|
|
1745
|
+
app.patch(wildcard, raw, forward);
|
|
1746
|
+
app.delete(wildcard, raw, forward);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
];
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
throw new Error(
|
|
1753
|
+
"Invalid `auth`: it takes a string, a function, `{ providers }`, `{ verify, audience }`, a library instance, or an array of those."
|
|
1754
|
+
);
|
|
1654
1755
|
}
|
|
1655
1756
|
|
|
1656
1757
|
// src/helpers/color.ts
|
|
@@ -1715,16 +1816,16 @@ var STATUS_TEXT = {
|
|
|
1715
1816
|
502: "Bad Gateway",
|
|
1716
1817
|
503: "Service Unavailable"
|
|
1717
1818
|
};
|
|
1718
|
-
var
|
|
1819
|
+
var UNITS3 = ["b", "kb", "mb", "gb", "tb"];
|
|
1719
1820
|
function formatBytes(bytes) {
|
|
1720
1821
|
if (!bytes || bytes < 0) return "0b";
|
|
1721
1822
|
const i = Math.min(
|
|
1722
1823
|
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
1723
|
-
|
|
1824
|
+
UNITS3.length - 1
|
|
1724
1825
|
);
|
|
1725
1826
|
const value = bytes / 1024 ** i;
|
|
1726
1827
|
const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
1727
|
-
return `${rounded}${
|
|
1828
|
+
return `${rounded}${UNITS3[i]}`;
|
|
1728
1829
|
}
|
|
1729
1830
|
var SCOPE_COLORS = {
|
|
1730
1831
|
start: "green",
|
|
@@ -1909,31 +2010,13 @@ function config(options = {}) {
|
|
|
1909
2010
|
settings.uploads = resolveUploads(options.uploads);
|
|
1910
2011
|
const production = env2.NODE_ENV === "production";
|
|
1911
2012
|
if (options.auth || env2.AUTH) {
|
|
1912
|
-
settings.auth =
|
|
2013
|
+
settings.auth = parseAuth(
|
|
1913
2014
|
options.auth || env2.AUTH || null
|
|
1914
2015
|
);
|
|
1915
2016
|
}
|
|
1916
|
-
if (settings.auth) {
|
|
1917
|
-
if (!settings.auth.users) {
|
|
1918
|
-
if (production) {
|
|
1919
|
-
throw new Error(
|
|
1920
|
-
"Auth in production needs a persistent `users` store, like auth: { ..., users: kv(redis).prefix('user:') }."
|
|
1921
|
-
);
|
|
1922
|
-
}
|
|
1923
|
-
settings.auth.users = toStore(/* @__PURE__ */ new Map());
|
|
1924
|
-
}
|
|
1925
|
-
if (!settings.auth.sessions && !settings.auth.strategy.includes("jwt")) {
|
|
1926
|
-
if (production) {
|
|
1927
|
-
throw new Error(
|
|
1928
|
-
"Auth in production needs a persistent `sessions` store, like auth: { ..., sessions: kv(redis).prefix('session:') }."
|
|
1929
|
-
);
|
|
1930
|
-
}
|
|
1931
|
-
settings.auth.sessions = toStoreExpiring(/* @__PURE__ */ new Map(), "1w");
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
if (settings.auth?.strategy.includes("jwt") && settings.secrets[0].startsWith("unsafe-")) {
|
|
2017
|
+
if (settings.auth?.some((one) => one.name === "flow") && settings.secrets[0].startsWith("unsafe-")) {
|
|
1935
2018
|
console.warn(
|
|
1936
|
-
"[server:auth]
|
|
2019
|
+
"[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)."
|
|
1937
2020
|
);
|
|
1938
2021
|
}
|
|
1939
2022
|
if (options.openapi) {
|
|
@@ -1950,7 +2033,8 @@ function config(options = {}) {
|
|
|
1950
2033
|
settings.onResponse = options.onResponse;
|
|
1951
2034
|
const loc = (v) => typeof v === "string" ? v : "enabled";
|
|
1952
2035
|
if (settings.auth) {
|
|
1953
|
-
|
|
2036
|
+
const names = settings.auth.map((one) => one.name).join(", ");
|
|
2037
|
+
log.message("auth", ` enabled`);
|
|
1954
2038
|
}
|
|
1955
2039
|
if (settings.public) log.message("public", loc(options.public));
|
|
1956
2040
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
@@ -2166,9 +2250,9 @@ async function validateResponse(out, options) {
|
|
|
2166
2250
|
if (out?.constructor !== Object && !Array.isArray(out)) return out;
|
|
2167
2251
|
return await run(options.response, out, "response");
|
|
2168
2252
|
}
|
|
2169
|
-
function replace2(
|
|
2170
|
-
for (const key of Object.keys(
|
|
2171
|
-
Object.assign(
|
|
2253
|
+
function replace2(target2, values) {
|
|
2254
|
+
for (const key of Object.keys(target2)) delete target2[key];
|
|
2255
|
+
Object.assign(target2, values);
|
|
2172
2256
|
}
|
|
2173
2257
|
|
|
2174
2258
|
// src/helpers/handleRequest.ts
|
|
@@ -2236,37 +2320,6 @@ async function getResponse(app, ctx) {
|
|
|
2236
2320
|
}
|
|
2237
2321
|
}
|
|
2238
2322
|
|
|
2239
|
-
// src/helpers/hash.ts
|
|
2240
|
-
import * as crypto2 from "crypto";
|
|
2241
|
-
import { getRandomValues } from "crypto";
|
|
2242
|
-
import { promisify } from "util";
|
|
2243
|
-
async function hash2(password) {
|
|
2244
|
-
if ("Bun" in globalThis) {
|
|
2245
|
-
return await Bun.password.hash(password, {
|
|
2246
|
-
algorithm: "argon2id",
|
|
2247
|
-
memoryCost: 65536,
|
|
2248
|
-
timeCost: 3
|
|
2249
|
-
});
|
|
2250
|
-
}
|
|
2251
|
-
if (!("argon2" in crypto2)) {
|
|
2252
|
-
throw new Error(
|
|
2253
|
-
"Password hashing needs argon2: run on Bun, or on Node 24+ where node:crypto provides it."
|
|
2254
|
-
);
|
|
2255
|
-
}
|
|
2256
|
-
const nonce = getRandomValues(new Uint8Array(32));
|
|
2257
|
-
const argon23 = promisify(crypto2.argon2);
|
|
2258
|
-
const buf = await argon23("argon2id", {
|
|
2259
|
-
message: Buffer.from(password),
|
|
2260
|
-
nonce,
|
|
2261
|
-
parallelism: 1,
|
|
2262
|
-
tagLength: 32,
|
|
2263
|
-
memory: 65536,
|
|
2264
|
-
passes: 3
|
|
2265
|
-
});
|
|
2266
|
-
const b64 = (bytes) => Buffer.from(bytes).toString("base64").replace(/=+$/, "");
|
|
2267
|
-
return `$argon2id$v=19$m=65536,t=3,p=1$${b64(nonce)}$${b64(buf)}`;
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
2323
|
// src/helpers/iteratorAsyncToReadable.ts
|
|
2271
2324
|
function iteratorAsyncToReadable(asyncGenerator) {
|
|
2272
2325
|
let cancelled = false;
|
|
@@ -2357,142 +2410,19 @@ function toWeb(nodeStream) {
|
|
|
2357
2410
|
});
|
|
2358
2411
|
}
|
|
2359
2412
|
|
|
2360
|
-
// src/helpers/verify.ts
|
|
2361
|
-
import * as crypto3 from "crypto";
|
|
2362
|
-
async function verify2(password, hash3) {
|
|
2363
|
-
if ("Bun" in globalThis) {
|
|
2364
|
-
return Bun.password.verify(password, hash3, "argon2id");
|
|
2365
|
-
}
|
|
2366
|
-
const match = /^\$argon2(id|i|d)\$v=(\d+)\$m=(\d+),t=(\d+),p=(\d+)\$([^$]+)\$([^$]+)$/.exec(
|
|
2367
|
-
hash3
|
|
2368
|
-
);
|
|
2369
|
-
if (!match) throw new Error("Invalid Argon2 hash format");
|
|
2370
|
-
const [, variant, , memory, passes, parallelism, saltB64, hashB64] = match;
|
|
2371
|
-
const nonce = Buffer.from(saltB64, "base64");
|
|
2372
|
-
const expected = Buffer.from(hashB64, "base64");
|
|
2373
|
-
return new Promise((resolve, reject) => {
|
|
2374
|
-
crypto3.argon2(
|
|
2375
|
-
`argon2${variant}`,
|
|
2376
|
-
{
|
|
2377
|
-
message: password,
|
|
2378
|
-
nonce,
|
|
2379
|
-
memory: parseInt(memory, 10),
|
|
2380
|
-
passes: parseInt(passes, 10),
|
|
2381
|
-
parallelism: parseInt(parallelism, 10),
|
|
2382
|
-
tagLength: expected.length
|
|
2383
|
-
},
|
|
2384
|
-
(err, derivedKey) => {
|
|
2385
|
-
if (err) return reject(err);
|
|
2386
|
-
if (derivedKey.length !== expected.length) return resolve(false);
|
|
2387
|
-
resolve(crypto3.timingSafeEqual(derivedKey, expected));
|
|
2388
|
-
}
|
|
2389
|
-
);
|
|
2390
|
-
});
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
// src/auth/getUser.ts
|
|
2394
|
-
async function getJwtUser(ctx) {
|
|
2395
|
-
const header = ctx.headers.authorization;
|
|
2396
|
-
if (!header) return;
|
|
2397
|
-
const [type2, token] = header.trim().split(" ");
|
|
2398
|
-
if (type2?.toLowerCase() !== "bearer" || !token) {
|
|
2399
|
-
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2400
|
-
}
|
|
2401
|
-
const payload = await verifyJwt(token, ctx.options.secrets);
|
|
2402
|
-
if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2403
|
-
const { iat, exp, ...claims } = payload;
|
|
2404
|
-
if (!claims.id || !claims.email) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2405
|
-
if (!ctx.options.auth.providers.includes(claims.provider)) {
|
|
2406
|
-
throw ServerError_default.AUTH_INVALID_PROVIDER({
|
|
2407
|
-
provider: claims.provider,
|
|
2408
|
-
valid: ctx.options.auth.providers
|
|
2409
|
-
});
|
|
2410
|
-
}
|
|
2411
|
-
const exposed = await ctx.options.auth.onUser(claims, ctx);
|
|
2412
|
-
assertUser(exposed, "onUser");
|
|
2413
|
-
return exposed;
|
|
2414
|
-
}
|
|
2415
|
-
async function getAuthSession(ctx) {
|
|
2416
|
-
const id = findSessionId(ctx);
|
|
2417
|
-
if (!id) return;
|
|
2418
|
-
const session = await ctx.options.auth.sessions.get(id);
|
|
2419
|
-
return session?.user ? session : void 0;
|
|
2420
|
-
}
|
|
2421
|
-
async function getUser(ctx) {
|
|
2422
|
-
if (!ctx.options.auth) return;
|
|
2423
|
-
const options = ctx.options.auth;
|
|
2424
|
-
if (options.strategy.includes("jwt")) return getJwtUser(ctx);
|
|
2425
|
-
const auth2 = await getAuthSession(ctx);
|
|
2426
|
-
if (!auth2) return;
|
|
2427
|
-
if (!options.providers.includes(auth2.provider)) {
|
|
2428
|
-
throw ServerError_default.AUTH_INVALID_PROVIDER({
|
|
2429
|
-
provider: auth2.provider,
|
|
2430
|
-
valid: options.providers
|
|
2431
|
-
});
|
|
2432
|
-
}
|
|
2433
|
-
const user = await options.users.get(auth2.user);
|
|
2434
|
-
if (!user) throw ServerError_default.AUTH_NO_USER();
|
|
2435
|
-
const exposed = await options.onUser(user, ctx);
|
|
2436
|
-
assertUser(exposed, "onUser");
|
|
2437
|
-
return exposed;
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
// src/auth/logout.ts
|
|
2441
|
-
async function logout(ctx) {
|
|
2442
|
-
const { strategy } = ctx.options.auth;
|
|
2443
|
-
if (!strategy.includes("jwt")) {
|
|
2444
|
-
const prev = findSessionId(ctx);
|
|
2445
|
-
if (prev) await ctx.options.auth.sessions.del(prev);
|
|
2446
|
-
}
|
|
2447
|
-
if (ctx.options.auth.onLogout) await ctx.options.auth.onLogout(ctx);
|
|
2448
|
-
if (strategy.includes("token") || strategy.includes("jwt")) {
|
|
2449
|
-
return { token: null };
|
|
2450
|
-
}
|
|
2451
|
-
if (strategy.includes("cookie")) {
|
|
2452
|
-
return cookies({ session: null }).redirect("/");
|
|
2453
|
-
}
|
|
2454
|
-
throw new Error("Unknown auth type");
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
2413
|
// src/auth/index.ts
|
|
2458
|
-
var oauth2 = [
|
|
2459
|
-
"github",
|
|
2460
|
-
"google",
|
|
2461
|
-
"microsoft",
|
|
2462
|
-
"discord",
|
|
2463
|
-
"facebook"
|
|
2464
|
-
];
|
|
2465
2414
|
function auth(app) {
|
|
2415
|
+
const entries = app.settings.auth;
|
|
2466
2416
|
app.use(async function middle(ctx) {
|
|
2467
|
-
|
|
2417
|
+
for (const entry3 of entries) {
|
|
2418
|
+
const user = await entry3.user(ctx);
|
|
2419
|
+
if (user) {
|
|
2420
|
+
ctx.user = user;
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2468
2424
|
});
|
|
2469
|
-
const
|
|
2470
|
-
app.post("/auth/logout", spec, logout);
|
|
2471
|
-
const enabled = app.settings.auth.providers;
|
|
2472
|
-
for (const name of oauth2) {
|
|
2473
|
-
if (!enabled.includes(name)) continue;
|
|
2474
|
-
const key = name.toUpperCase();
|
|
2475
|
-
if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
|
|
2476
|
-
if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
|
|
2477
|
-
app.get(`/auth/login/${name}`, spec, providers_default[name].login);
|
|
2478
|
-
app.get(`/auth/callback/${name}`, spec, providers_default[name].callback);
|
|
2479
|
-
app.post(`/auth/verify/${name}`, spec, providers_default[name].verify);
|
|
2480
|
-
}
|
|
2481
|
-
if (enabled.includes("apple")) {
|
|
2482
|
-
const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
|
|
2483
|
-
for (const key of keys) {
|
|
2484
|
-
if (!env[key]) throw new Error(`${key} not defined`);
|
|
2485
|
-
}
|
|
2486
|
-
app.get("/auth/login/apple", spec, providers_default.apple.login);
|
|
2487
|
-
app.post("/auth/callback/apple", spec, providers_default.apple.callback);
|
|
2488
|
-
app.post("/auth/verify/apple", spec, providers_default.apple.verify);
|
|
2489
|
-
}
|
|
2490
|
-
if (enabled.includes("email")) {
|
|
2491
|
-
app.post("/auth/register/email", spec, providers_default.email.register);
|
|
2492
|
-
app.post("/auth/login/email", spec, providers_default.email.login);
|
|
2493
|
-
app.put("/auth/password/email", spec, providers_default.email.password);
|
|
2494
|
-
app.put("/auth/reset/email", spec, providers_default.email.reset);
|
|
2495
|
-
}
|
|
2425
|
+
for (const entry3 of entries) entry3.routes?.(app);
|
|
2496
2426
|
}
|
|
2497
2427
|
|
|
2498
2428
|
// src/helpers/parseRange.ts
|
|
@@ -2528,35 +2458,35 @@ async function assets(ctx) {
|
|
|
2528
2458
|
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
2529
2459
|
const file2 = ctx.options.public.file(key);
|
|
2530
2460
|
const info = file2.info?.bind(file2);
|
|
2531
|
-
const
|
|
2532
|
-
if (info ? !
|
|
2461
|
+
const meta2 = info ? await info() : null;
|
|
2462
|
+
if (info ? !meta2 : !await file2.exists()) return;
|
|
2533
2463
|
const ext = ctx.url.pathname.split(".").pop()?.toLowerCase();
|
|
2534
|
-
const ctype = ext && mimes_default[ext] ||
|
|
2464
|
+
const ctype = ext && mimes_default[ext] || meta2?.type || ext;
|
|
2535
2465
|
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2536
2466
|
let tag;
|
|
2537
|
-
if (
|
|
2538
|
-
const stamp =
|
|
2539
|
-
tag = `W/"${
|
|
2467
|
+
if (meta2) {
|
|
2468
|
+
const stamp = meta2.modified ? meta2.modified.getTime() : 0;
|
|
2469
|
+
tag = `W/"${meta2.size.toString(16)}-${stamp.toString(16)}"`;
|
|
2540
2470
|
headers2.etag = tag;
|
|
2541
|
-
if (
|
|
2471
|
+
if (meta2.modified) headers2["last-modified"] = meta2.modified.toUTCString();
|
|
2542
2472
|
}
|
|
2543
|
-
const canRange = !!(
|
|
2473
|
+
const canRange = !!(meta2 && file2.slice);
|
|
2544
2474
|
if (canRange) headers2["accept-ranges"] = "bytes";
|
|
2545
2475
|
if (tag && ctx.headers["if-none-match"] === tag) {
|
|
2546
2476
|
return status(304).headers(headers2).send();
|
|
2547
2477
|
}
|
|
2548
2478
|
const rangeHeader = ctx.headers.range;
|
|
2549
2479
|
const ifRange = ctx.headers["if-range"];
|
|
2550
|
-
if (
|
|
2551
|
-
const range = parseRange(rangeHeader,
|
|
2480
|
+
if (meta2 && file2.slice && rangeHeader && (!ifRange || ifRange === tag)) {
|
|
2481
|
+
const range = parseRange(rangeHeader, meta2.size);
|
|
2552
2482
|
if (range === "unsatisfiable") {
|
|
2553
|
-
return status(416).headers({ ...headers2, "content-range": `bytes */${
|
|
2483
|
+
return status(416).headers({ ...headers2, "content-range": `bytes */${meta2.size}` }).send();
|
|
2554
2484
|
}
|
|
2555
2485
|
if (range) {
|
|
2556
2486
|
const { start, end } = range;
|
|
2557
2487
|
return type(ctype).status(206).headers({
|
|
2558
2488
|
...headers2,
|
|
2559
|
-
"content-range": `bytes ${start}-${end}/${
|
|
2489
|
+
"content-range": `bytes ${start}-${end}/${meta2.size}`,
|
|
2560
2490
|
"content-length": String(end - start + 1)
|
|
2561
2491
|
}).send(file2.slice(start, end + 1).stream());
|
|
2562
2492
|
}
|
|
@@ -2627,7 +2557,7 @@ var generateOpenApiPaths = async (handlers, specPath) => {
|
|
|
2627
2557
|
for (const [method, routes] of Object.entries(handlers)) {
|
|
2628
2558
|
for (const route of routes) {
|
|
2629
2559
|
const path = route.path;
|
|
2630
|
-
const
|
|
2560
|
+
const meta2 = route.options ?? {};
|
|
2631
2561
|
const config2 = getConfig(route.options?.schema);
|
|
2632
2562
|
if (typeof path !== "string" || path === "*" || path === specPath) {
|
|
2633
2563
|
continue;
|
|
@@ -2638,13 +2568,13 @@ var generateOpenApiPaths = async (handlers, specPath) => {
|
|
|
2638
2568
|
paths[normalizedPath] = {};
|
|
2639
2569
|
}
|
|
2640
2570
|
let requestBody;
|
|
2641
|
-
if (
|
|
2642
|
-
const schema = await toJsonSchema(
|
|
2571
|
+
if (meta2?.body) {
|
|
2572
|
+
const schema = await toJsonSchema(meta2.body);
|
|
2643
2573
|
requestBody = { content: { "application/json": { schema } } };
|
|
2644
2574
|
}
|
|
2645
2575
|
let responses;
|
|
2646
|
-
if (
|
|
2647
|
-
const schema = await toJsonSchema(
|
|
2576
|
+
if (meta2?.response) {
|
|
2577
|
+
const schema = await toJsonSchema(meta2.response);
|
|
2648
2578
|
responses = {
|
|
2649
2579
|
200: { description: "OK", content: { "application/json": { schema } } }
|
|
2650
2580
|
};
|
|
@@ -2660,8 +2590,8 @@ var generateOpenApiPaths = async (handlers, specPath) => {
|
|
|
2660
2590
|
schema: { type: type2 }
|
|
2661
2591
|
});
|
|
2662
2592
|
});
|
|
2663
|
-
if (
|
|
2664
|
-
const schema = await toJsonSchema(
|
|
2593
|
+
if (meta2?.query) {
|
|
2594
|
+
const schema = await toJsonSchema(meta2.query);
|
|
2665
2595
|
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
|
|
2666
2596
|
parameters.push({
|
|
2667
2597
|
name,
|
|
@@ -2734,7 +2664,10 @@ function timer(ctx) {
|
|
|
2734
2664
|
async function socketUser(app, headers2, cookies2) {
|
|
2735
2665
|
if (!app.settings.auth) return void 0;
|
|
2736
2666
|
const ctx = { options: app.settings, headers: headers2, cookies: cookies2 };
|
|
2737
|
-
|
|
2667
|
+
for (const entry3 of app.settings.auth) {
|
|
2668
|
+
const user = await entry3.user(ctx);
|
|
2669
|
+
if (user) return user;
|
|
2670
|
+
}
|
|
2738
2671
|
}
|
|
2739
2672
|
|
|
2740
2673
|
// src/helpers/wsNode.ts
|
|
@@ -3233,8 +3166,7 @@ function ServerTest(app) {
|
|
|
3233
3166
|
}
|
|
3234
3167
|
|
|
3235
3168
|
// src/index.ts
|
|
3236
|
-
import { default as default2 } from "
|
|
3237
|
-
import { default as default3 } from "bucket";
|
|
3169
|
+
import { default as default2 } from "bucket";
|
|
3238
3170
|
var Server = class extends Router {
|
|
3239
3171
|
settings;
|
|
3240
3172
|
platform;
|
|
@@ -3299,7 +3231,7 @@ export {
|
|
|
3299
3231
|
Server,
|
|
3300
3232
|
ServerError_default as ServerError,
|
|
3301
3233
|
ValidationError,
|
|
3302
|
-
|
|
3234
|
+
default2 as bucket,
|
|
3303
3235
|
cache,
|
|
3304
3236
|
cookies,
|
|
3305
3237
|
server as default,
|
|
@@ -3307,7 +3239,6 @@ export {
|
|
|
3307
3239
|
file,
|
|
3308
3240
|
headers,
|
|
3309
3241
|
json,
|
|
3310
|
-
default2 as kv,
|
|
3311
3242
|
redirect,
|
|
3312
3243
|
router,
|
|
3313
3244
|
send,
|