@server/next 0.46.0 → 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.
- package/index.d.ts +94 -53
- package/index.js +728 -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,727 @@ 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
|
+
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();
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
);
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
return
|
|
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}`);
|
|
1372
1451
|
};
|
|
1373
|
-
return {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
+
}
|
|
1434
1513
|
};
|
|
1435
|
-
return finishLogin(ctx, {
|
|
1436
|
-
provider: "email",
|
|
1437
|
-
key: email,
|
|
1438
|
-
email,
|
|
1439
|
-
user
|
|
1440
|
-
});
|
|
1441
1514
|
}
|
|
1442
|
-
async function emailResetPassword() {
|
|
1443
|
-
}
|
|
1444
|
-
async function emailUpdatePassword(ctx) {
|
|
1445
|
-
const passwords = ctx.body;
|
|
1446
|
-
const fullUser = await ctx.options.auth.users.get(ctx.user.email);
|
|
1447
|
-
if (!fullUser) throw ServerError_default.AUTH_NO_USER();
|
|
1448
|
-
const isValid = await verify2(passwords.previous, fullUser.password);
|
|
1449
|
-
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
1450
|
-
fullUser.password = await hash2(passwords.updated);
|
|
1451
|
-
await updateUser(fullUser, ctx.user, ctx.options.auth.users);
|
|
1452
|
-
return 200;
|
|
1453
|
-
}
|
|
1454
|
-
var email_default = {
|
|
1455
|
-
login: emailLogin,
|
|
1456
|
-
register: emailRegister,
|
|
1457
|
-
reset: emailResetPassword,
|
|
1458
|
-
password: emailUpdatePassword
|
|
1459
|
-
};
|
|
1460
1515
|
|
|
1461
|
-
// src/auth/
|
|
1462
|
-
var
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
|
1490
|
-
};
|
|
1491
|
-
for (const [key, value] of Object.entries(extra)) {
|
|
1492
|
-
if (value) params[key] = value;
|
|
1493
|
-
}
|
|
1494
|
-
const res = await fch("https://github.com/login/oauth/access_token", {
|
|
1495
|
-
method: "post",
|
|
1496
|
-
body: JSON.stringify(params)
|
|
1497
|
-
});
|
|
1498
|
-
return (path) => {
|
|
1499
|
-
return fch(`https://api.github.com${path}`, {
|
|
1500
|
-
headers: { Authorization: `Bearer ${res.access_token}` }
|
|
1501
|
-
});
|
|
1502
|
-
};
|
|
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;
|
|
1503
1526
|
};
|
|
1504
|
-
var
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
if (value) search.set(key, value);
|
|
1511
|
-
}
|
|
1512
|
-
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" }
|
|
1513
1533
|
};
|
|
1514
|
-
var
|
|
1515
|
-
|
|
1516
|
-
|
|
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);
|
|
1517
1556
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
const [
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
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;
|
|
1529
1567
|
};
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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}`)
|
|
1551
1616
|
);
|
|
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
|
-
});
|
|
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
|
+
}
|
|
1597
1631
|
|
|
1598
|
-
// src/auth/
|
|
1599
|
-
var
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
+
}
|
|
1607
1646
|
};
|
|
1647
|
+
var vendors_default = VENDORS;
|
|
1608
1648
|
|
|
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) {
|
|
1649
|
+
// src/auth/parse.ts
|
|
1650
|
+
function parseAuth(auth2) {
|
|
1617
1651
|
if (!auth2) return null;
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
+
);
|
|
1621
1662
|
}
|
|
1622
|
-
if (!
|
|
1623
|
-
throw new Error(
|
|
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
|
+
);
|
|
1624
1667
|
}
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
+
);
|
|
1629
1673
|
}
|
|
1630
|
-
const
|
|
1631
|
-
if (
|
|
1674
|
+
const audience = globalThis.env[`${KEY}_AUDIENCE`];
|
|
1675
|
+
if (!audience) {
|
|
1632
1676
|
throw new Error(
|
|
1633
|
-
|
|
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.`
|
|
1634
1678
|
);
|
|
1635
1679
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
}
|
|
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
|
+
);
|
|
1654
1736
|
}
|
|
1655
1737
|
|
|
1656
1738
|
// src/helpers/color.ts
|
|
@@ -1715,16 +1797,16 @@ var STATUS_TEXT = {
|
|
|
1715
1797
|
502: "Bad Gateway",
|
|
1716
1798
|
503: "Service Unavailable"
|
|
1717
1799
|
};
|
|
1718
|
-
var
|
|
1800
|
+
var UNITS3 = ["b", "kb", "mb", "gb", "tb"];
|
|
1719
1801
|
function formatBytes(bytes) {
|
|
1720
1802
|
if (!bytes || bytes < 0) return "0b";
|
|
1721
1803
|
const i = Math.min(
|
|
1722
1804
|
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
1723
|
-
|
|
1805
|
+
UNITS3.length - 1
|
|
1724
1806
|
);
|
|
1725
1807
|
const value = bytes / 1024 ** i;
|
|
1726
1808
|
const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
1727
|
-
return `${rounded}${
|
|
1809
|
+
return `${rounded}${UNITS3[i]}`;
|
|
1728
1810
|
}
|
|
1729
1811
|
var SCOPE_COLORS = {
|
|
1730
1812
|
start: "green",
|
|
@@ -1909,31 +1991,13 @@ function config(options = {}) {
|
|
|
1909
1991
|
settings.uploads = resolveUploads(options.uploads);
|
|
1910
1992
|
const production = env2.NODE_ENV === "production";
|
|
1911
1993
|
if (options.auth || env2.AUTH) {
|
|
1912
|
-
settings.auth =
|
|
1994
|
+
settings.auth = parseAuth(
|
|
1913
1995
|
options.auth || env2.AUTH || null
|
|
1914
1996
|
);
|
|
1915
1997
|
}
|
|
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-")) {
|
|
1998
|
+
if (settings.auth?.some((one) => one.name === "flow") && settings.secrets[0].startsWith("unsafe-")) {
|
|
1935
1999
|
console.warn(
|
|
1936
|
-
"[server:auth]
|
|
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)."
|
|
1937
2001
|
);
|
|
1938
2002
|
}
|
|
1939
2003
|
if (options.openapi) {
|
|
@@ -1950,7 +2014,8 @@ function config(options = {}) {
|
|
|
1950
2014
|
settings.onResponse = options.onResponse;
|
|
1951
2015
|
const loc = (v) => typeof v === "string" ? v : "enabled";
|
|
1952
2016
|
if (settings.auth) {
|
|
1953
|
-
|
|
2017
|
+
const names = settings.auth.map((one) => one.name).join(", ");
|
|
2018
|
+
log.message("auth", ` enabled`);
|
|
1954
2019
|
}
|
|
1955
2020
|
if (settings.public) log.message("public", loc(options.public));
|
|
1956
2021
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
@@ -2166,9 +2231,9 @@ async function validateResponse(out, options) {
|
|
|
2166
2231
|
if (out?.constructor !== Object && !Array.isArray(out)) return out;
|
|
2167
2232
|
return await run(options.response, out, "response");
|
|
2168
2233
|
}
|
|
2169
|
-
function replace2(
|
|
2170
|
-
for (const key of Object.keys(
|
|
2171
|
-
Object.assign(
|
|
2234
|
+
function replace2(target2, values) {
|
|
2235
|
+
for (const key of Object.keys(target2)) delete target2[key];
|
|
2236
|
+
Object.assign(target2, values);
|
|
2172
2237
|
}
|
|
2173
2238
|
|
|
2174
2239
|
// src/helpers/handleRequest.ts
|
|
@@ -2236,37 +2301,6 @@ async function getResponse(app, ctx) {
|
|
|
2236
2301
|
}
|
|
2237
2302
|
}
|
|
2238
2303
|
|
|
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
2304
|
// src/helpers/iteratorAsyncToReadable.ts
|
|
2271
2305
|
function iteratorAsyncToReadable(asyncGenerator) {
|
|
2272
2306
|
let cancelled = false;
|
|
@@ -2357,142 +2391,19 @@ function toWeb(nodeStream) {
|
|
|
2357
2391
|
});
|
|
2358
2392
|
}
|
|
2359
2393
|
|
|
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
2394
|
// src/auth/index.ts
|
|
2458
|
-
var oauth2 = [
|
|
2459
|
-
"github",
|
|
2460
|
-
"google",
|
|
2461
|
-
"microsoft",
|
|
2462
|
-
"discord",
|
|
2463
|
-
"facebook"
|
|
2464
|
-
];
|
|
2465
2395
|
function auth(app) {
|
|
2396
|
+
const entries = app.settings.auth;
|
|
2466
2397
|
app.use(async function middle(ctx) {
|
|
2467
|
-
|
|
2398
|
+
for (const entry3 of entries) {
|
|
2399
|
+
const user = await entry3.user(ctx);
|
|
2400
|
+
if (user) {
|
|
2401
|
+
ctx.user = user;
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2468
2405
|
});
|
|
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
|
-
}
|
|
2406
|
+
for (const entry3 of entries) entry3.routes?.(app);
|
|
2496
2407
|
}
|
|
2497
2408
|
|
|
2498
2409
|
// src/helpers/parseRange.ts
|
|
@@ -2528,35 +2439,35 @@ async function assets(ctx) {
|
|
|
2528
2439
|
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
2529
2440
|
const file2 = ctx.options.public.file(key);
|
|
2530
2441
|
const info = file2.info?.bind(file2);
|
|
2531
|
-
const
|
|
2532
|
-
if (info ? !
|
|
2442
|
+
const meta2 = info ? await info() : null;
|
|
2443
|
+
if (info ? !meta2 : !await file2.exists()) return;
|
|
2533
2444
|
const ext = ctx.url.pathname.split(".").pop()?.toLowerCase();
|
|
2534
|
-
const ctype = ext && mimes_default[ext] ||
|
|
2445
|
+
const ctype = ext && mimes_default[ext] || meta2?.type || ext;
|
|
2535
2446
|
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2536
2447
|
let tag;
|
|
2537
|
-
if (
|
|
2538
|
-
const stamp =
|
|
2539
|
-
tag = `W/"${
|
|
2448
|
+
if (meta2) {
|
|
2449
|
+
const stamp = meta2.modified ? meta2.modified.getTime() : 0;
|
|
2450
|
+
tag = `W/"${meta2.size.toString(16)}-${stamp.toString(16)}"`;
|
|
2540
2451
|
headers2.etag = tag;
|
|
2541
|
-
if (
|
|
2452
|
+
if (meta2.modified) headers2["last-modified"] = meta2.modified.toUTCString();
|
|
2542
2453
|
}
|
|
2543
|
-
const canRange = !!(
|
|
2454
|
+
const canRange = !!(meta2 && file2.slice);
|
|
2544
2455
|
if (canRange) headers2["accept-ranges"] = "bytes";
|
|
2545
2456
|
if (tag && ctx.headers["if-none-match"] === tag) {
|
|
2546
2457
|
return status(304).headers(headers2).send();
|
|
2547
2458
|
}
|
|
2548
2459
|
const rangeHeader = ctx.headers.range;
|
|
2549
2460
|
const ifRange = ctx.headers["if-range"];
|
|
2550
|
-
if (
|
|
2551
|
-
const range = parseRange(rangeHeader,
|
|
2461
|
+
if (meta2 && file2.slice && rangeHeader && (!ifRange || ifRange === tag)) {
|
|
2462
|
+
const range = parseRange(rangeHeader, meta2.size);
|
|
2552
2463
|
if (range === "unsatisfiable") {
|
|
2553
|
-
return status(416).headers({ ...headers2, "content-range": `bytes */${
|
|
2464
|
+
return status(416).headers({ ...headers2, "content-range": `bytes */${meta2.size}` }).send();
|
|
2554
2465
|
}
|
|
2555
2466
|
if (range) {
|
|
2556
2467
|
const { start, end } = range;
|
|
2557
2468
|
return type(ctype).status(206).headers({
|
|
2558
2469
|
...headers2,
|
|
2559
|
-
"content-range": `bytes ${start}-${end}/${
|
|
2470
|
+
"content-range": `bytes ${start}-${end}/${meta2.size}`,
|
|
2560
2471
|
"content-length": String(end - start + 1)
|
|
2561
2472
|
}).send(file2.slice(start, end + 1).stream());
|
|
2562
2473
|
}
|
|
@@ -2627,7 +2538,7 @@ var generateOpenApiPaths = async (handlers, specPath) => {
|
|
|
2627
2538
|
for (const [method, routes] of Object.entries(handlers)) {
|
|
2628
2539
|
for (const route of routes) {
|
|
2629
2540
|
const path = route.path;
|
|
2630
|
-
const
|
|
2541
|
+
const meta2 = route.options ?? {};
|
|
2631
2542
|
const config2 = getConfig(route.options?.schema);
|
|
2632
2543
|
if (typeof path !== "string" || path === "*" || path === specPath) {
|
|
2633
2544
|
continue;
|
|
@@ -2638,13 +2549,13 @@ var generateOpenApiPaths = async (handlers, specPath) => {
|
|
|
2638
2549
|
paths[normalizedPath] = {};
|
|
2639
2550
|
}
|
|
2640
2551
|
let requestBody;
|
|
2641
|
-
if (
|
|
2642
|
-
const schema = await toJsonSchema(
|
|
2552
|
+
if (meta2?.body) {
|
|
2553
|
+
const schema = await toJsonSchema(meta2.body);
|
|
2643
2554
|
requestBody = { content: { "application/json": { schema } } };
|
|
2644
2555
|
}
|
|
2645
2556
|
let responses;
|
|
2646
|
-
if (
|
|
2647
|
-
const schema = await toJsonSchema(
|
|
2557
|
+
if (meta2?.response) {
|
|
2558
|
+
const schema = await toJsonSchema(meta2.response);
|
|
2648
2559
|
responses = {
|
|
2649
2560
|
200: { description: "OK", content: { "application/json": { schema } } }
|
|
2650
2561
|
};
|
|
@@ -2660,8 +2571,8 @@ var generateOpenApiPaths = async (handlers, specPath) => {
|
|
|
2660
2571
|
schema: { type: type2 }
|
|
2661
2572
|
});
|
|
2662
2573
|
});
|
|
2663
|
-
if (
|
|
2664
|
-
const schema = await toJsonSchema(
|
|
2574
|
+
if (meta2?.query) {
|
|
2575
|
+
const schema = await toJsonSchema(meta2.query);
|
|
2665
2576
|
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
|
|
2666
2577
|
parameters.push({
|
|
2667
2578
|
name,
|
|
@@ -2734,7 +2645,10 @@ function timer(ctx) {
|
|
|
2734
2645
|
async function socketUser(app, headers2, cookies2) {
|
|
2735
2646
|
if (!app.settings.auth) return void 0;
|
|
2736
2647
|
const ctx = { options: app.settings, headers: headers2, cookies: cookies2 };
|
|
2737
|
-
|
|
2648
|
+
for (const entry3 of app.settings.auth) {
|
|
2649
|
+
const user = await entry3.user(ctx);
|
|
2650
|
+
if (user) return user;
|
|
2651
|
+
}
|
|
2738
2652
|
}
|
|
2739
2653
|
|
|
2740
2654
|
// src/helpers/wsNode.ts
|
|
@@ -3233,8 +3147,7 @@ function ServerTest(app) {
|
|
|
3233
3147
|
}
|
|
3234
3148
|
|
|
3235
3149
|
// src/index.ts
|
|
3236
|
-
import { default as default2 } from "
|
|
3237
|
-
import { default as default3 } from "bucket";
|
|
3150
|
+
import { default as default2 } from "bucket";
|
|
3238
3151
|
var Server = class extends Router {
|
|
3239
3152
|
settings;
|
|
3240
3153
|
platform;
|
|
@@ -3299,7 +3212,7 @@ export {
|
|
|
3299
3212
|
Server,
|
|
3300
3213
|
ServerError_default as ServerError,
|
|
3301
3214
|
ValidationError,
|
|
3302
|
-
|
|
3215
|
+
default2 as bucket,
|
|
3303
3216
|
cache,
|
|
3304
3217
|
cookies,
|
|
3305
3218
|
server as default,
|
|
@@ -3307,7 +3220,6 @@ export {
|
|
|
3307
3220
|
file,
|
|
3308
3221
|
headers,
|
|
3309
3222
|
json,
|
|
3310
|
-
default2 as kv,
|
|
3311
3223
|
redirect,
|
|
3312
3224
|
router,
|
|
3313
3225
|
send,
|