@server/next 0.28.16 → 0.30.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 +29 -3
- package/index.js +733 -214
- package/package.json +19 -25
- package/readme.md +1 -1
- package/src/jsx/jsx-runtime.js +1 -1
package/index.js
CHANGED
|
@@ -43,12 +43,22 @@ ServerError_default.extend({
|
|
|
43
43
|
NO_STORE_WRITE: "You need a 'store' to write 'ctx.session.{key}'",
|
|
44
44
|
NO_STORE_READ: "You need a 'store' to read 'ctx.session.{key}'",
|
|
45
45
|
AUTH_ARGON_NEEDED: "Argon2 is needed for the auth module, please install it with 'npm i argon2'",
|
|
46
|
-
AUTH_INVALID_TOKEN: "Invalid Authorization token",
|
|
47
|
-
AUTH_INVALID_COOKIE: "Invalid Authorization cookie",
|
|
48
|
-
AUTH_INVALID_HEADER:
|
|
49
|
-
|
|
46
|
+
AUTH_INVALID_TOKEN: { status: 401, message: "Invalid Authorization token" },
|
|
47
|
+
AUTH_INVALID_COOKIE: { status: 401, message: "Invalid Authorization cookie" },
|
|
48
|
+
AUTH_INVALID_HEADER: {
|
|
49
|
+
status: 401,
|
|
50
|
+
message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
|
|
51
|
+
},
|
|
52
|
+
AUTH_INVALID_STRATEGY: {
|
|
53
|
+
status: 401,
|
|
54
|
+
message: "Invalid Authorization type '{strategy}', valid one is '{valid}'"
|
|
55
|
+
},
|
|
56
|
+
AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" },
|
|
50
57
|
AUTH_NO_PROVIDER: "No provider passed to the option 'auth.provider'",
|
|
51
|
-
AUTH_INVALID_PROVIDER:
|
|
58
|
+
AUTH_INVALID_PROVIDER: {
|
|
59
|
+
status: 401,
|
|
60
|
+
message: "Invalid provider '{provider}', valid ones are: '{valid}'"
|
|
61
|
+
},
|
|
52
62
|
AUTH_NO_SESSION: { status: 401, message: "Invalid session" },
|
|
53
63
|
AUTH_NO_USER: {
|
|
54
64
|
status: 401,
|
|
@@ -79,97 +89,23 @@ if (typeof process !== "undefined") {
|
|
|
79
89
|
Object.assign(globalThis.env, process.env);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
// src/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
provider,
|
|
99
|
-
email: user.email
|
|
100
|
-
};
|
|
101
|
-
await session2.set(
|
|
102
|
-
id,
|
|
103
|
-
{ id, strategy, provider, user: user.email },
|
|
104
|
-
{ expires: "1w" }
|
|
105
|
-
);
|
|
106
|
-
if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
|
|
107
|
-
if (strategy.includes("token")) {
|
|
108
|
-
return status(201).json({ ...user, token: id });
|
|
109
|
-
}
|
|
110
|
-
if (strategy.includes("cookie")) {
|
|
111
|
-
return status(302).cookies({ authentication: id }).redirect(redirect2);
|
|
112
|
-
}
|
|
113
|
-
if (strategy.includes("jwt")) {
|
|
114
|
-
throw new Error("JWT auth not supported yet");
|
|
115
|
-
}
|
|
116
|
-
if (strategy.includes("key")) {
|
|
117
|
-
throw new Error("Key auth not supported yet");
|
|
118
|
-
}
|
|
119
|
-
throw new Error("Unknown auth type");
|
|
120
|
-
};
|
|
121
|
-
async function emailLogin(ctx) {
|
|
122
|
-
const { email, password } = ctx.body;
|
|
123
|
-
if (!email) throw ServerError_default.LOGIN_NO_EMAIL();
|
|
124
|
-
if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
|
|
125
|
-
if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
|
|
126
|
-
if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
|
|
127
|
-
const store = ctx.options.auth.store;
|
|
128
|
-
if (!await store.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
|
|
129
|
-
const user = await store.get(email);
|
|
130
|
-
const isValid = await verify(password, user.password);
|
|
131
|
-
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
132
|
-
return createSession(user, ctx);
|
|
133
|
-
}
|
|
134
|
-
async function emailRegister(ctx) {
|
|
135
|
-
const { email, password, ...data } = ctx.body;
|
|
136
|
-
if (!email) throw ServerError_default.REGISTER_NO_EMAIL();
|
|
137
|
-
if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
|
|
138
|
-
if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
|
|
139
|
-
if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
|
|
140
|
-
const store = ctx.options.auth.store;
|
|
141
|
-
if (await store.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
|
|
142
|
-
const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
|
|
143
|
-
const user = {
|
|
144
|
-
id: createId(email),
|
|
145
|
-
strategy: ctx.options.auth.strategy,
|
|
146
|
-
provider: "email",
|
|
147
|
-
email,
|
|
148
|
-
password: await hash(password),
|
|
149
|
-
time,
|
|
150
|
-
...data
|
|
151
|
-
};
|
|
152
|
-
await store.set(email, user);
|
|
153
|
-
return createSession(user, ctx);
|
|
92
|
+
// src/helpers/clientIp.ts
|
|
93
|
+
var first = (v) => (Array.isArray(v) ? v[0] : v) || "";
|
|
94
|
+
var normalize = (ip) => ip.replace(/^::ffff:/, "");
|
|
95
|
+
function clientIp(headers2, opts = {}) {
|
|
96
|
+
const { remoteAddress = "", trustProxy = false } = opts;
|
|
97
|
+
const cf = first(headers2["cf-connecting-ip"]);
|
|
98
|
+
if (cf) return normalize(cf);
|
|
99
|
+
const nf = first(headers2["x-nf-client-connection-ip"]);
|
|
100
|
+
if (nf) return normalize(nf);
|
|
101
|
+
if (trustProxy) {
|
|
102
|
+
const xff = first(headers2["x-forwarded-for"]);
|
|
103
|
+
if (xff) return normalize(xff.split(",")[0].trim());
|
|
104
|
+
const real = first(headers2["x-real-ip"]);
|
|
105
|
+
if (real) return normalize(real);
|
|
106
|
+
}
|
|
107
|
+
return normalize(remoteAddress);
|
|
154
108
|
}
|
|
155
|
-
async function emailResetPassword() {
|
|
156
|
-
}
|
|
157
|
-
async function emailUpdatePassword(ctx) {
|
|
158
|
-
const passwords = ctx.body;
|
|
159
|
-
const fullUser = await ctx.options.auth.store.get(ctx.user.email);
|
|
160
|
-
if (!fullUser) throw ServerError_default.AUTH_NO_USER();
|
|
161
|
-
const isValid = await verify(passwords.previous, fullUser.password);
|
|
162
|
-
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
163
|
-
fullUser.password = await hash(passwords.updated);
|
|
164
|
-
await updateUser(fullUser, ctx.user, ctx.options.auth.store);
|
|
165
|
-
return 200;
|
|
166
|
-
}
|
|
167
|
-
var email_default = {
|
|
168
|
-
login: emailLogin,
|
|
169
|
-
register: emailRegister,
|
|
170
|
-
reset: emailResetPassword,
|
|
171
|
-
password: emailUpdatePassword
|
|
172
|
-
};
|
|
173
109
|
|
|
174
110
|
// src/helpers/isReadableStream.ts
|
|
175
111
|
function isReadableStream(obj) {
|
|
@@ -254,10 +190,16 @@ var Reply = class {
|
|
|
254
190
|
const isHtml = body.trim().startsWith("<");
|
|
255
191
|
headers2.set("content-type", isHtml ? "text/html" : "text/plain");
|
|
256
192
|
}
|
|
193
|
+
if (!headers2.has("content-length")) {
|
|
194
|
+
headers2.set("content-length", String(Buffer.byteLength(body)));
|
|
195
|
+
}
|
|
257
196
|
return new Response(body, { status: status2, headers: headers2 });
|
|
258
197
|
}
|
|
259
198
|
const name = body?.constructor?.name;
|
|
260
199
|
if (name === "Buffer") {
|
|
200
|
+
if (!headers2.has("content-length")) {
|
|
201
|
+
headers2.set("content-length", String(body.length));
|
|
202
|
+
}
|
|
261
203
|
return new Response(body, { status: status2, headers: headers2 });
|
|
262
204
|
}
|
|
263
205
|
if (typeof body?.getReader === "function") {
|
|
@@ -272,7 +214,11 @@ var Reply = class {
|
|
|
272
214
|
if (!headers2.get("content-type")) {
|
|
273
215
|
headers2.set("content-type", "application/json");
|
|
274
216
|
}
|
|
275
|
-
|
|
217
|
+
const payload = JSON.stringify(body);
|
|
218
|
+
if (!headers2.has("content-length")) {
|
|
219
|
+
headers2.set("content-length", String(Buffer.byteLength(payload)));
|
|
220
|
+
}
|
|
221
|
+
return new Response(payload, { status: status2, headers: headers2 });
|
|
276
222
|
}
|
|
277
223
|
};
|
|
278
224
|
var r = () => new Reply();
|
|
@@ -286,6 +232,379 @@ var json = (...args) => r().json(...args);
|
|
|
286
232
|
var file = (...args) => r().file(...args);
|
|
287
233
|
var redirect = (...args) => r().redirect(...args);
|
|
288
234
|
|
|
235
|
+
// src/auth/finishLogin.ts
|
|
236
|
+
async function finishLogin(ctx, input) {
|
|
237
|
+
const settings = ctx.options.auth;
|
|
238
|
+
const { strategy, cleanUser } = settings;
|
|
239
|
+
const key = String(input.key);
|
|
240
|
+
const auth2 = {
|
|
241
|
+
id: createId(),
|
|
242
|
+
strategy,
|
|
243
|
+
provider: input.provider,
|
|
244
|
+
user: key,
|
|
245
|
+
email: input.email,
|
|
246
|
+
time: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
|
|
247
|
+
};
|
|
248
|
+
let user = input.user;
|
|
249
|
+
if (input.store !== false) {
|
|
250
|
+
const existing = await settings.store.get(key);
|
|
251
|
+
user = { ...existing ?? {}, ...input.user };
|
|
252
|
+
}
|
|
253
|
+
user = await cleanUser(user);
|
|
254
|
+
if (input.store !== false) await settings.store.set(key, user);
|
|
255
|
+
await settings.session.set(auth2.id, auth2, { expires: "1w" });
|
|
256
|
+
if (strategy.includes("token")) {
|
|
257
|
+
return status(201).json({ ...user, token: auth2.id });
|
|
258
|
+
}
|
|
259
|
+
if (strategy.includes("cookie")) {
|
|
260
|
+
return cookies("authentication", {
|
|
261
|
+
value: auth2.id,
|
|
262
|
+
path: "/",
|
|
263
|
+
httpOnly: true,
|
|
264
|
+
secure: ctx.platform.production,
|
|
265
|
+
sameSite: "Lax"
|
|
266
|
+
}).redirect(settings.redirect);
|
|
267
|
+
}
|
|
268
|
+
if (strategy.includes("jwt")) throw new Error("JWT auth not supported yet");
|
|
269
|
+
if (strategy.includes("key")) throw new Error("Key auth not supported yet");
|
|
270
|
+
throw new Error("Unknown auth type");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/helpers/createCookies.ts
|
|
274
|
+
var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
|
|
275
|
+
var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
|
|
276
|
+
parse.millisecond = parse.ms = 1e-3;
|
|
277
|
+
parse.second = parse.sec = parse.s = parse[""] = 1;
|
|
278
|
+
parse.minute = parse.min = parse.m = parse.s * 60;
|
|
279
|
+
parse.hour = parse.hr = parse.h = parse.m * 60;
|
|
280
|
+
parse.day = parse.d = parse.h * 24;
|
|
281
|
+
parse.week = parse.wk = parse.w = parse.d * 7;
|
|
282
|
+
parse.year = parse.yr = parse.y = parse.d * 365.25;
|
|
283
|
+
parse.month = parse.b = parse.y / 12;
|
|
284
|
+
function parse(str) {
|
|
285
|
+
if (str === null || str === void 0) return null;
|
|
286
|
+
if (typeof str === "number") return str;
|
|
287
|
+
if (typeof str !== "string") {
|
|
288
|
+
throw new Error(`Not a string: ${str} (${typeof str})`);
|
|
289
|
+
}
|
|
290
|
+
str = str.toLowerCase().replace(/[,_]/g, "");
|
|
291
|
+
const [_, value, units] = times.exec(str) || [];
|
|
292
|
+
if (!units) return null;
|
|
293
|
+
const unitValue = parse[units] || parse[units.replace(/s$/, "")];
|
|
294
|
+
if (!unitValue) return null;
|
|
295
|
+
const result = unitValue * parseFloat(value);
|
|
296
|
+
return Math.abs(Math.round(result * 1e3));
|
|
297
|
+
}
|
|
298
|
+
function normalizeExpires(expires) {
|
|
299
|
+
if (expires === null || expires === void 0) return void 0;
|
|
300
|
+
if (expires === 0) return EXPIRED2;
|
|
301
|
+
if (typeof expires === "string") {
|
|
302
|
+
if (/^[\d._]+\w+$/.test(expires)) {
|
|
303
|
+
return new Date(Date.now() + parse(expires)).toUTCString();
|
|
304
|
+
} else {
|
|
305
|
+
return expires;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (typeof expires === "number") {
|
|
309
|
+
return new Date(Date.now() + expires).toUTCString();
|
|
310
|
+
}
|
|
311
|
+
if (expires instanceof Date) {
|
|
312
|
+
return expires.toUTCString();
|
|
313
|
+
}
|
|
314
|
+
return void 0;
|
|
315
|
+
}
|
|
316
|
+
function createCookies(key, val) {
|
|
317
|
+
if (val.value === null) val.expires = EXPIRED2;
|
|
318
|
+
const { value, path: path2, expires, maxAge, httpOnly, secure, sameSite } = val;
|
|
319
|
+
let str = `${key}=${value || ""};Path=${path2 || "/"}`;
|
|
320
|
+
if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
|
|
321
|
+
if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
|
|
322
|
+
if (httpOnly) str += ";HttpOnly";
|
|
323
|
+
if (secure) str += ";Secure";
|
|
324
|
+
if (sameSite) str += `;SameSite=${sameSite}`;
|
|
325
|
+
return str;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/auth/state.ts
|
|
329
|
+
var NAME = "oauth_state";
|
|
330
|
+
function startState(ctx, crossSite = false) {
|
|
331
|
+
const state = createId();
|
|
332
|
+
return {
|
|
333
|
+
state,
|
|
334
|
+
cookie: {
|
|
335
|
+
value: state,
|
|
336
|
+
path: "/",
|
|
337
|
+
expires: "10m",
|
|
338
|
+
httpOnly: true,
|
|
339
|
+
secure: crossSite || ctx.platform.production,
|
|
340
|
+
sameSite: crossSite ? "None" : "Lax"
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function checkState(ctx, received) {
|
|
345
|
+
const expected = ctx.cookies[NAME];
|
|
346
|
+
if (!expected || !received || expected !== received) {
|
|
347
|
+
throw ServerError_default.AUTH_INVALID_STATE();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function clearState() {
|
|
351
|
+
return createCookies(NAME, { value: null });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/auth/providers/apple.ts
|
|
355
|
+
var AUTHORIZE = "https://appleid.apple.com/auth/authorize";
|
|
356
|
+
var TOKEN = "https://appleid.apple.com/auth/token";
|
|
357
|
+
var b64url = (data) => {
|
|
358
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
359
|
+
let bin = "";
|
|
360
|
+
for (const byte of bytes) bin += String.fromCharCode(byte);
|
|
361
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
362
|
+
};
|
|
363
|
+
var b64urlJson = (segment) => {
|
|
364
|
+
let b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
365
|
+
b64 += "=".repeat((4 - b64.length % 4) % 4);
|
|
366
|
+
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
367
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
368
|
+
};
|
|
369
|
+
var clientSecret = async () => {
|
|
370
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
371
|
+
const header = { alg: "ES256", kid: env.APPLE_KEY_ID, typ: "JWT" };
|
|
372
|
+
const payload = {
|
|
373
|
+
iss: env.APPLE_TEAM_ID,
|
|
374
|
+
iat: now,
|
|
375
|
+
exp: now + 3600,
|
|
376
|
+
aud: "https://appleid.apple.com",
|
|
377
|
+
sub: env.APPLE_ID
|
|
378
|
+
};
|
|
379
|
+
const data = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
|
|
380
|
+
const pem = String(env.APPLE_PRIVATE_KEY).replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
|
381
|
+
const der = Uint8Array.from(atob(pem), (c) => c.charCodeAt(0));
|
|
382
|
+
const key = await crypto.subtle.importKey(
|
|
383
|
+
"pkcs8",
|
|
384
|
+
der,
|
|
385
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
386
|
+
false,
|
|
387
|
+
["sign"]
|
|
388
|
+
);
|
|
389
|
+
const sig = await crypto.subtle.sign(
|
|
390
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
391
|
+
key,
|
|
392
|
+
new TextEncoder().encode(data)
|
|
393
|
+
);
|
|
394
|
+
return `${data}.${b64url(new Uint8Array(sig))}`;
|
|
395
|
+
};
|
|
396
|
+
var login = (ctx) => {
|
|
397
|
+
const { state, cookie } = startState(ctx, true);
|
|
398
|
+
const params = new URLSearchParams({
|
|
399
|
+
client_id: env.APPLE_ID,
|
|
400
|
+
redirect_uri: `${ctx.url.origin}/auth/callback/apple`,
|
|
401
|
+
response_type: "code",
|
|
402
|
+
scope: "name email",
|
|
403
|
+
// Requesting scopes forces Apple to POST the result back (form_post)
|
|
404
|
+
response_mode: "form_post",
|
|
405
|
+
state
|
|
406
|
+
});
|
|
407
|
+
return cookies("oauth_state", cookie).redirect(`${AUTHORIZE}?${params}`);
|
|
408
|
+
};
|
|
409
|
+
var callback = async (ctx) => {
|
|
410
|
+
const body = ctx.body || {};
|
|
411
|
+
checkState(ctx, body.state);
|
|
412
|
+
const tokenRes = await fetch(TOKEN, {
|
|
413
|
+
method: "POST",
|
|
414
|
+
headers: {
|
|
415
|
+
accept: "application/json",
|
|
416
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
417
|
+
},
|
|
418
|
+
body: new URLSearchParams({
|
|
419
|
+
client_id: env.APPLE_ID,
|
|
420
|
+
client_secret: await clientSecret(),
|
|
421
|
+
code: body.code,
|
|
422
|
+
grant_type: "authorization_code",
|
|
423
|
+
redirect_uri: `${ctx.url.origin}/auth/callback/apple`
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
if (!tokenRes.ok) throw new Error("apple: token exchange failed");
|
|
427
|
+
const token = await tokenRes.json();
|
|
428
|
+
const claims = b64urlJson(token.id_token.split(".")[1]);
|
|
429
|
+
let name;
|
|
430
|
+
if (body.user) {
|
|
431
|
+
const parsed = JSON.parse(body.user).name;
|
|
432
|
+
if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
|
|
433
|
+
}
|
|
434
|
+
const res = await finishLogin(ctx, {
|
|
435
|
+
provider: "apple",
|
|
436
|
+
key: claims.sub,
|
|
437
|
+
email: claims.email,
|
|
438
|
+
user: { id: claims.sub, name, email: claims.email }
|
|
439
|
+
});
|
|
440
|
+
res.headers.append("set-cookie", clearState());
|
|
441
|
+
return res;
|
|
442
|
+
};
|
|
443
|
+
var apple_default = { login, callback };
|
|
444
|
+
|
|
445
|
+
// src/auth/providers/oauth.ts
|
|
446
|
+
function oauthProvider(config2) {
|
|
447
|
+
const KEY = config2.name.toUpperCase();
|
|
448
|
+
const callbackUrl = (ctx) => `${ctx.url.origin}/auth/callback/${config2.name}`;
|
|
449
|
+
const login3 = (ctx) => {
|
|
450
|
+
const { state, cookie } = startState(ctx);
|
|
451
|
+
const params = new URLSearchParams({
|
|
452
|
+
client_id: env[`${KEY}_ID`],
|
|
453
|
+
redirect_uri: callbackUrl(ctx),
|
|
454
|
+
response_type: "code",
|
|
455
|
+
scope: config2.scope,
|
|
456
|
+
state
|
|
457
|
+
});
|
|
458
|
+
return cookies("oauth_state", cookie).redirect(
|
|
459
|
+
`${config2.authorizeUrl}?${params}`
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
const callback3 = async (ctx) => {
|
|
463
|
+
checkState(ctx, ctx.url.query.state);
|
|
464
|
+
const tokenRes = await fetch(config2.tokenUrl, {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: {
|
|
467
|
+
accept: "application/json",
|
|
468
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
469
|
+
},
|
|
470
|
+
body: new URLSearchParams({
|
|
471
|
+
client_id: env[`${KEY}_ID`],
|
|
472
|
+
client_secret: env[`${KEY}_SECRET`],
|
|
473
|
+
code: ctx.url.query.code,
|
|
474
|
+
grant_type: "authorization_code",
|
|
475
|
+
redirect_uri: callbackUrl(ctx)
|
|
476
|
+
})
|
|
477
|
+
});
|
|
478
|
+
if (!tokenRes.ok) throw new Error(`${config2.name}: token exchange failed`);
|
|
479
|
+
const token = await tokenRes.json();
|
|
480
|
+
const profileRes = await fetch(config2.profileUrl, {
|
|
481
|
+
headers: {
|
|
482
|
+
accept: "application/json",
|
|
483
|
+
authorization: `Bearer ${token.access_token}`
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
if (!profileRes.ok) throw new Error(`${config2.name}: profile fetch failed`);
|
|
487
|
+
const profile = config2.profile(await profileRes.json());
|
|
488
|
+
const res = await finishLogin(ctx, {
|
|
489
|
+
provider: config2.name,
|
|
490
|
+
key: profile.id,
|
|
491
|
+
email: profile.email,
|
|
492
|
+
user: {
|
|
493
|
+
id: profile.id,
|
|
494
|
+
name: profile.name,
|
|
495
|
+
email: profile.email,
|
|
496
|
+
picture: profile.picture
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
res.headers.append("set-cookie", clearState());
|
|
500
|
+
return res;
|
|
501
|
+
};
|
|
502
|
+
return { login: login3, callback: callback3 };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/auth/providers/discord.ts
|
|
506
|
+
var discord_default = oauthProvider({
|
|
507
|
+
name: "discord",
|
|
508
|
+
authorizeUrl: "https://discord.com/oauth2/authorize",
|
|
509
|
+
tokenUrl: "https://discord.com/api/oauth2/token",
|
|
510
|
+
profileUrl: "https://discord.com/api/users/@me",
|
|
511
|
+
scope: "identify email",
|
|
512
|
+
profile: (p) => ({
|
|
513
|
+
id: p.id,
|
|
514
|
+
email: p.email,
|
|
515
|
+
name: p.global_name || p.username,
|
|
516
|
+
picture: p.avatar ? `https://cdn.discordapp.com/avatars/${p.id}/${p.avatar}.png` : void 0
|
|
517
|
+
})
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// src/auth/updateUser.ts
|
|
521
|
+
async function updateUser(user, auth2, store) {
|
|
522
|
+
if (auth2.provider === "email") {
|
|
523
|
+
return await store.set(auth2.email, user);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/auth/providers/email.ts
|
|
528
|
+
async function emailLogin(ctx) {
|
|
529
|
+
const { email, password } = ctx.body;
|
|
530
|
+
if (!email) throw ServerError_default.LOGIN_NO_EMAIL();
|
|
531
|
+
if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
|
|
532
|
+
if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
|
|
533
|
+
if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
|
|
534
|
+
const store = ctx.options.auth.store;
|
|
535
|
+
if (!await store.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
|
|
536
|
+
const user = await store.get(email);
|
|
537
|
+
const isValid = await verify(password, user.password);
|
|
538
|
+
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
539
|
+
return finishLogin(ctx, {
|
|
540
|
+
provider: "email",
|
|
541
|
+
key: user.email,
|
|
542
|
+
email: user.email,
|
|
543
|
+
user,
|
|
544
|
+
store: false
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
async function emailRegister(ctx) {
|
|
548
|
+
const { email, password, ...data } = ctx.body;
|
|
549
|
+
if (!email) throw ServerError_default.REGISTER_NO_EMAIL();
|
|
550
|
+
if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
|
|
551
|
+
if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
|
|
552
|
+
if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
|
|
553
|
+
const store = ctx.options.auth.store;
|
|
554
|
+
if (await store.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
|
|
555
|
+
const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
|
|
556
|
+
const user = {
|
|
557
|
+
id: createId(email),
|
|
558
|
+
strategy: ctx.options.auth.strategy,
|
|
559
|
+
provider: "email",
|
|
560
|
+
email,
|
|
561
|
+
password: await hash(password),
|
|
562
|
+
time,
|
|
563
|
+
...data
|
|
564
|
+
};
|
|
565
|
+
await store.set(email, user);
|
|
566
|
+
return finishLogin(ctx, {
|
|
567
|
+
provider: "email",
|
|
568
|
+
key: email,
|
|
569
|
+
email,
|
|
570
|
+
user,
|
|
571
|
+
store: false
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
async function emailResetPassword() {
|
|
575
|
+
}
|
|
576
|
+
async function emailUpdatePassword(ctx) {
|
|
577
|
+
const passwords = ctx.body;
|
|
578
|
+
const fullUser = await ctx.options.auth.store.get(ctx.user.email);
|
|
579
|
+
if (!fullUser) throw ServerError_default.AUTH_NO_USER();
|
|
580
|
+
const isValid = await verify(passwords.previous, fullUser.password);
|
|
581
|
+
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
582
|
+
fullUser.password = await hash(passwords.updated);
|
|
583
|
+
await updateUser(fullUser, ctx.user, ctx.options.auth.store);
|
|
584
|
+
return 200;
|
|
585
|
+
}
|
|
586
|
+
var email_default = {
|
|
587
|
+
login: emailLogin,
|
|
588
|
+
register: emailRegister,
|
|
589
|
+
reset: emailResetPassword,
|
|
590
|
+
password: emailUpdatePassword
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// src/auth/providers/facebook.ts
|
|
594
|
+
var facebook_default = oauthProvider({
|
|
595
|
+
name: "facebook",
|
|
596
|
+
authorizeUrl: "https://www.facebook.com/v18.0/dialog/oauth",
|
|
597
|
+
tokenUrl: "https://graph.facebook.com/v18.0/oauth/access_token",
|
|
598
|
+
profileUrl: "https://graph.facebook.com/me?fields=id,name,email,picture",
|
|
599
|
+
scope: "email public_profile",
|
|
600
|
+
profile: (p) => ({
|
|
601
|
+
id: p.id,
|
|
602
|
+
email: p.email,
|
|
603
|
+
name: p.name,
|
|
604
|
+
picture: p.picture?.data?.url
|
|
605
|
+
})
|
|
606
|
+
});
|
|
607
|
+
|
|
289
608
|
// src/auth/providers/github.ts
|
|
290
609
|
var oauth = async (code) => {
|
|
291
610
|
const fch = async (url, { body, headers: headers2 = {}, ...rest } = {}) => {
|
|
@@ -309,9 +628,15 @@ var oauth = async (code) => {
|
|
|
309
628
|
});
|
|
310
629
|
};
|
|
311
630
|
};
|
|
312
|
-
var
|
|
313
|
-
|
|
314
|
-
|
|
631
|
+
var login2 = (ctx) => {
|
|
632
|
+
const { state, cookie } = startState(ctx);
|
|
633
|
+
const params = new URLSearchParams({
|
|
634
|
+
client_id: env.GITHUB_ID,
|
|
635
|
+
scope: "user:email",
|
|
636
|
+
state
|
|
637
|
+
});
|
|
638
|
+
return cookies("oauth_state", cookie).redirect(
|
|
639
|
+
`https://github.com/login/oauth/authorize?${params}`
|
|
315
640
|
);
|
|
316
641
|
};
|
|
317
642
|
var getUserProfile = async (code) => {
|
|
@@ -323,47 +648,67 @@ var getUserProfile = async (code) => {
|
|
|
323
648
|
const email = emails.sort((a) => a.primary ? -1 : 1)[0]?.email;
|
|
324
649
|
return { ...profile, email };
|
|
325
650
|
};
|
|
326
|
-
var
|
|
327
|
-
|
|
651
|
+
var callback2 = async (ctx) => {
|
|
652
|
+
checkState(ctx, ctx.url.query.state);
|
|
328
653
|
const profile = await getUserProfile(ctx.url.query.code);
|
|
329
|
-
const
|
|
330
|
-
id: createId(),
|
|
331
|
-
strategy,
|
|
654
|
+
const res = await finishLogin(ctx, {
|
|
332
655
|
provider: "github",
|
|
333
|
-
|
|
334
|
-
email: profile.email,
|
|
335
|
-
time: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
|
|
336
|
-
};
|
|
337
|
-
const existing = await store.get(String(profile.id));
|
|
338
|
-
const user = cleanUser({
|
|
339
|
-
...existing ?? {},
|
|
340
|
-
id: profile.id,
|
|
341
|
-
name: profile.name,
|
|
656
|
+
key: profile.id,
|
|
342
657
|
email: profile.email,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
658
|
+
user: {
|
|
659
|
+
id: profile.id,
|
|
660
|
+
name: profile.name,
|
|
661
|
+
email: profile.email,
|
|
662
|
+
picture: profile.avatar_url,
|
|
663
|
+
location: profile.location,
|
|
664
|
+
created: profile.created_at
|
|
665
|
+
}
|
|
346
666
|
});
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (auth2.strategy.includes("token")) {
|
|
350
|
-
return status(201).json({ ...user, token: auth2.id });
|
|
351
|
-
}
|
|
352
|
-
if (auth2.strategy.includes("cookie")) {
|
|
353
|
-
return status(302).cookies({ authentication: auth2.id }).redirect(redirect2);
|
|
354
|
-
}
|
|
355
|
-
if (auth2.strategy.includes("jwt")) {
|
|
356
|
-
throw new Error("JWT auth not supported yet");
|
|
357
|
-
}
|
|
358
|
-
if (auth2.strategy.includes("key")) {
|
|
359
|
-
throw new Error("Key auth not supported yet");
|
|
360
|
-
}
|
|
361
|
-
throw new Error("Unknown auth type");
|
|
667
|
+
res.headers.append("set-cookie", clearState());
|
|
668
|
+
return res;
|
|
362
669
|
};
|
|
363
|
-
var github_default = { login, callback };
|
|
670
|
+
var github_default = { login: login2, callback: callback2 };
|
|
671
|
+
|
|
672
|
+
// src/auth/providers/google.ts
|
|
673
|
+
var google_default = oauthProvider({
|
|
674
|
+
name: "google",
|
|
675
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
676
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
677
|
+
profileUrl: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
678
|
+
scope: "openid email profile",
|
|
679
|
+
profile: (p) => ({
|
|
680
|
+
id: p.sub,
|
|
681
|
+
email: p.email,
|
|
682
|
+
name: p.name,
|
|
683
|
+
picture: p.picture
|
|
684
|
+
})
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// src/auth/providers/microsoft.ts
|
|
688
|
+
var microsoft_default = oauthProvider({
|
|
689
|
+
name: "microsoft",
|
|
690
|
+
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
691
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
692
|
+
profileUrl: "https://graph.microsoft.com/v1.0/me",
|
|
693
|
+
scope: "openid email profile User.Read",
|
|
694
|
+
profile: (p) => ({
|
|
695
|
+
// Personal accounts expose `userPrincipalName` rather than `mail`
|
|
696
|
+
id: p.id,
|
|
697
|
+
email: p.mail || p.userPrincipalName,
|
|
698
|
+
name: p.displayName
|
|
699
|
+
})
|
|
700
|
+
});
|
|
364
701
|
|
|
365
702
|
// src/auth/providers/index.ts
|
|
366
|
-
var providers_default = {
|
|
703
|
+
var providers_default = {
|
|
704
|
+
apple: apple_default,
|
|
705
|
+
discord: discord_default,
|
|
706
|
+
email: email_default,
|
|
707
|
+
facebook: facebook_default,
|
|
708
|
+
github: github_default,
|
|
709
|
+
google: google_default,
|
|
710
|
+
microsoft: microsoft_default
|
|
711
|
+
};
|
|
367
712
|
|
|
368
713
|
// src/auth/parseAuthOptions.ts
|
|
369
714
|
var defaultRedirect = "/user";
|
|
@@ -398,7 +743,7 @@ function parseAuthOptions(auth2, all) {
|
|
|
398
743
|
throw new Error("Auth options needs a strategy");
|
|
399
744
|
}
|
|
400
745
|
const strategy = auth2.strategy;
|
|
401
|
-
if (!auth2.provider
|
|
746
|
+
if (!auth2.provider?.length) {
|
|
402
747
|
throw new Error("Auth options needs a provider");
|
|
403
748
|
}
|
|
404
749
|
const provider = getProviders(auth2.provider);
|
|
@@ -439,7 +784,7 @@ function thinLocalBucket(root) {
|
|
|
439
784
|
read: async (name) => {
|
|
440
785
|
const fullPath = absolute(name);
|
|
441
786
|
const stats = await fsp.stat(fullPath).catch(() => null);
|
|
442
|
-
if (!stats
|
|
787
|
+
if (!stats?.isFile()) return null;
|
|
443
788
|
const nodeStream = fs.createReadStream(fullPath);
|
|
444
789
|
return new ReadableStream({
|
|
445
790
|
start(controller) {
|
|
@@ -544,6 +889,115 @@ function createId(source, size = 16) {
|
|
|
544
889
|
return randomId(size);
|
|
545
890
|
}
|
|
546
891
|
|
|
892
|
+
// src/helpers/color.ts
|
|
893
|
+
var map = {
|
|
894
|
+
reset: 0,
|
|
895
|
+
bright: 1,
|
|
896
|
+
dim: 2,
|
|
897
|
+
under: 4,
|
|
898
|
+
blink: 5,
|
|
899
|
+
reverse: 7,
|
|
900
|
+
black: 30,
|
|
901
|
+
red: 31,
|
|
902
|
+
green: 32,
|
|
903
|
+
yellow: 33,
|
|
904
|
+
blue: 34,
|
|
905
|
+
magenta: 35,
|
|
906
|
+
cyan: 36,
|
|
907
|
+
white: 37,
|
|
908
|
+
bgblack: 40,
|
|
909
|
+
bgred: 41,
|
|
910
|
+
bggreen: 42,
|
|
911
|
+
bgyellow: 43,
|
|
912
|
+
bgblue: 44,
|
|
913
|
+
bgmagenta: 45,
|
|
914
|
+
bgcyan: 46,
|
|
915
|
+
bgwhite: 47
|
|
916
|
+
};
|
|
917
|
+
var replace = (k) => {
|
|
918
|
+
if (process.env.NO_COLOR) return "";
|
|
919
|
+
if (!(k in map)) throw new Error(`"{${k}}" is not a valid color`);
|
|
920
|
+
return `\x1B[${map[k]}m`;
|
|
921
|
+
};
|
|
922
|
+
function color(str, ...vals) {
|
|
923
|
+
if (typeof str === "string") {
|
|
924
|
+
return str.replace(/\{(\w+)\}/g, (_m, k) => replace(k)).replace(/\{\/\w*\}/g, () => replace("reset"));
|
|
925
|
+
}
|
|
926
|
+
return color(str[0] + vals.map((v, i) => v + str[i + 1]).join(""));
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/helpers/logger.ts
|
|
930
|
+
var STATUS_TEXT = {
|
|
931
|
+
200: "OK",
|
|
932
|
+
201: "Created",
|
|
933
|
+
202: "Accepted",
|
|
934
|
+
204: "No Content",
|
|
935
|
+
301: "Moved Permanently",
|
|
936
|
+
302: "Found",
|
|
937
|
+
303: "See Other",
|
|
938
|
+
304: "Not Modified",
|
|
939
|
+
307: "Temporary Redirect",
|
|
940
|
+
308: "Permanent Redirect",
|
|
941
|
+
400: "Bad Request",
|
|
942
|
+
401: "Unauthorized",
|
|
943
|
+
403: "Forbidden",
|
|
944
|
+
404: "Not Found",
|
|
945
|
+
405: "Method Not Allowed",
|
|
946
|
+
409: "Conflict",
|
|
947
|
+
413: "Payload Too Large",
|
|
948
|
+
422: "Unprocessable Entity",
|
|
949
|
+
429: "Too Many Requests",
|
|
950
|
+
500: "Internal Server Error",
|
|
951
|
+
502: "Bad Gateway",
|
|
952
|
+
503: "Service Unavailable"
|
|
953
|
+
};
|
|
954
|
+
var UNITS = ["b", "kb", "mb", "gb", "tb"];
|
|
955
|
+
function formatBytes(bytes) {
|
|
956
|
+
if (!bytes || bytes < 0) return "0b";
|
|
957
|
+
const i = Math.min(
|
|
958
|
+
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
959
|
+
UNITS.length - 1
|
|
960
|
+
);
|
|
961
|
+
const value = bytes / 1024 ** i;
|
|
962
|
+
const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
963
|
+
return `${rounded}${UNITS[i]}`;
|
|
964
|
+
}
|
|
965
|
+
var SCOPE_COLORS = {
|
|
966
|
+
start: "green",
|
|
967
|
+
api: "cyan"
|
|
968
|
+
};
|
|
969
|
+
var MODULE_COLOR = "magenta";
|
|
970
|
+
var paint = (name, text) => `${color(`{${name}}`)}${text}${color("{/}")}`;
|
|
971
|
+
function createLogger(level) {
|
|
972
|
+
const enabled = !!level;
|
|
973
|
+
const message = (scope, msg) => {
|
|
974
|
+
if (!enabled) return;
|
|
975
|
+
const c = SCOPE_COLORS[scope] || MODULE_COLOR;
|
|
976
|
+
console.log(paint(c, `[server:${scope}] ${msg}`));
|
|
977
|
+
};
|
|
978
|
+
const request = (ctx, res) => {
|
|
979
|
+
if (!enabled) return;
|
|
980
|
+
const method = ctx.method.toUpperCase();
|
|
981
|
+
const path2 = ctx.url.pathname;
|
|
982
|
+
const reqLen = Number(ctx.headers["content-length"]) || 0;
|
|
983
|
+
const resLen = Number(res.headers.get("content-length")) || 0;
|
|
984
|
+
const status2 = res.status;
|
|
985
|
+
const text = STATUS_TEXT[status2] || "";
|
|
986
|
+
const reqSize = reqLen ? ` ${formatBytes(reqLen)}` : "";
|
|
987
|
+
const resSize = resLen ? ` ${formatBytes(resLen)}` : "";
|
|
988
|
+
let line = `${method} ${path2}${reqSize} \u2192 ${status2}${text ? ` ${text}` : ""}${resSize}`;
|
|
989
|
+
const location = res.headers.get("location");
|
|
990
|
+
if (location) line += ` \u2192 ${location}`;
|
|
991
|
+
message("api", line);
|
|
992
|
+
};
|
|
993
|
+
return {
|
|
994
|
+
level,
|
|
995
|
+
message,
|
|
996
|
+
start: (url) => message("start", url),
|
|
997
|
+
request
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
547
1001
|
// src/helpers/upload.ts
|
|
548
1002
|
function parseBytes(value) {
|
|
549
1003
|
if (typeof value === "number") return value;
|
|
@@ -608,7 +1062,7 @@ var UploadPipeline = class {
|
|
|
608
1062
|
}
|
|
609
1063
|
if (!this._bucket) {
|
|
610
1064
|
throw new Error(
|
|
611
|
-
`No destination configured
|
|
1065
|
+
`No destination configured. Pass a bucket to upload() or call .store()`
|
|
612
1066
|
);
|
|
613
1067
|
}
|
|
614
1068
|
return saveFileToBucket(originalName, data, this._bucket, contentType);
|
|
@@ -621,9 +1075,18 @@ function upload(bucket) {
|
|
|
621
1075
|
// src/helpers/config.ts
|
|
622
1076
|
function config(options = {}) {
|
|
623
1077
|
const env2 = globalThis.env;
|
|
1078
|
+
const raw = options.log ?? env2.LOG_LEVEL;
|
|
1079
|
+
const level = raw === true ? "info" : raw === false ? void 0 : raw;
|
|
1080
|
+
const log = createLogger(level);
|
|
624
1081
|
const settings = {
|
|
625
1082
|
port: options.port || env2.PORT || 3e3,
|
|
626
|
-
secret: options.secret || env2.SECRET || `unsafe-${createId()}
|
|
1083
|
+
secret: options.secret || env2.SECRET || `unsafe-${createId()}`,
|
|
1084
|
+
log,
|
|
1085
|
+
// Trust X-Forwarded-* headers for ctx.ip (on by default; set it to false
|
|
1086
|
+
// when clients connect directly so a client can't spoof its IP).
|
|
1087
|
+
security: {
|
|
1088
|
+
trustProxy: options.security?.trustProxy ?? true
|
|
1089
|
+
}
|
|
627
1090
|
};
|
|
628
1091
|
options.cors = options.cors || env2.CORS || null;
|
|
629
1092
|
if (options.cors) {
|
|
@@ -652,6 +1115,9 @@ function config(options = {}) {
|
|
|
652
1115
|
if ("headers" in options.cors) {
|
|
653
1116
|
cors2.headers = Array.isArray(options.cors.headers) ? options.cors.headers.join(",") : options.cors.headers;
|
|
654
1117
|
}
|
|
1118
|
+
if (options.cors.credentials) {
|
|
1119
|
+
cors2.credentials = true;
|
|
1120
|
+
}
|
|
655
1121
|
}
|
|
656
1122
|
if (typeof cors2.origin === "string") {
|
|
657
1123
|
cors2.origin = cors2.origin.toLowerCase();
|
|
@@ -661,6 +1127,7 @@ function config(options = {}) {
|
|
|
661
1127
|
settings.views = options.views ? bucket_default(options.views) : null;
|
|
662
1128
|
settings.public = options.public ? bucket_default(options.public) : null;
|
|
663
1129
|
settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket_default(options.uploads) : null;
|
|
1130
|
+
if (options.favicon) settings.favicon = options.favicon;
|
|
664
1131
|
settings.store = options.store ?? null;
|
|
665
1132
|
settings.cookies = options.cookies ?? null;
|
|
666
1133
|
if (options.session) {
|
|
@@ -682,6 +1149,20 @@ function config(options = {}) {
|
|
|
682
1149
|
status: error.status || 500
|
|
683
1150
|
});
|
|
684
1151
|
});
|
|
1152
|
+
const loc = (v) => typeof v === "string" ? v : "enabled";
|
|
1153
|
+
if (settings.auth) {
|
|
1154
|
+
log.message("auth", `${settings.auth.provider.join(", ")} auth enabled`);
|
|
1155
|
+
}
|
|
1156
|
+
if (settings.public) log.message("public", loc(options.public));
|
|
1157
|
+
if (settings.views) log.message("views", loc(options.views));
|
|
1158
|
+
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
1159
|
+
if (settings.session) log.message("session", "enabled");
|
|
1160
|
+
if (settings.cors) {
|
|
1161
|
+
const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
|
|
1162
|
+
log.message("cors", origin);
|
|
1163
|
+
}
|
|
1164
|
+
if (settings.favicon) log.message("favicon", loc(settings.favicon));
|
|
1165
|
+
if (settings.openapi) log.message("openapi", settings.openapi.path || "/docs");
|
|
685
1166
|
return settings;
|
|
686
1167
|
}
|
|
687
1168
|
|
|
@@ -698,57 +1179,26 @@ function cors(config2, origin = "") {
|
|
|
698
1179
|
console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
|
|
699
1180
|
return null;
|
|
700
1181
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
parse.week = parse.wk = parse.w = parse.d * 7;
|
|
711
|
-
parse.year = parse.yr = parse.y = parse.d * 365.25;
|
|
712
|
-
parse.month = parse.b = parse.y / 12;
|
|
713
|
-
function parse(str) {
|
|
714
|
-
if (str === null || str === void 0) return null;
|
|
715
|
-
if (typeof str === "number") return str;
|
|
716
|
-
if (typeof str !== "string") {
|
|
717
|
-
throw new Error(`Not a string: ${str} (${typeof str})`);
|
|
1182
|
+
function applyCors(res, ctx) {
|
|
1183
|
+
const settings = ctx.options.cors;
|
|
1184
|
+
if (!settings) return;
|
|
1185
|
+
const requestOrigin = ctx.headers.origin || "";
|
|
1186
|
+
let origin = cors(settings.origin, requestOrigin);
|
|
1187
|
+
if (!origin) return;
|
|
1188
|
+
if (settings.credentials && origin === "*") {
|
|
1189
|
+
if (!requestOrigin) return;
|
|
1190
|
+
origin = requestOrigin.toLowerCase();
|
|
718
1191
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const result = unitValue * parseFloat(value);
|
|
725
|
-
return Math.abs(Math.round(result * 1e3));
|
|
726
|
-
}
|
|
727
|
-
function normalizeExpires(expires) {
|
|
728
|
-
if (expires === null || expires === void 0) return void 0;
|
|
729
|
-
if (expires === 0) return EXPIRED2;
|
|
730
|
-
if (typeof expires === "string") {
|
|
731
|
-
if (/^[\d._]+\w+$/.test(expires)) {
|
|
732
|
-
return new Date(Date.now() + parse(expires)).toUTCString();
|
|
733
|
-
} else {
|
|
734
|
-
return expires;
|
|
735
|
-
}
|
|
1192
|
+
res.headers.set("Access-Control-Allow-Origin", origin);
|
|
1193
|
+
res.headers.set("Access-Control-Allow-Methods", settings.methods);
|
|
1194
|
+
res.headers.set("Access-Control-Allow-Headers", settings.headers);
|
|
1195
|
+
if (settings.credentials) {
|
|
1196
|
+
res.headers.set("Access-Control-Allow-Credentials", "true");
|
|
736
1197
|
}
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (expires instanceof Date) {
|
|
741
|
-
return expires.toUTCString();
|
|
1198
|
+
if (origin !== "*") res.headers.append("Vary", "Origin");
|
|
1199
|
+
if (ctx.method === "options") {
|
|
1200
|
+
res.headers.set("Access-Control-Max-Age", "86400");
|
|
742
1201
|
}
|
|
743
|
-
return void 0;
|
|
744
|
-
}
|
|
745
|
-
function createCookies(key, val) {
|
|
746
|
-
if (val.value === null) val.expires = EXPIRED2;
|
|
747
|
-
const { value, path: path2, expires } = val;
|
|
748
|
-
const pathPart = `;Path=${path2 || "/"}`;
|
|
749
|
-
const expiresStr = normalizeExpires(expires);
|
|
750
|
-
const expiresPart = typeof expires !== "undefined" ? `;Expires=${expiresStr}` : "";
|
|
751
|
-
return `${key}=${value || ""}${pathPart}${expiresPart}`;
|
|
752
1202
|
}
|
|
753
1203
|
|
|
754
1204
|
// src/helpers/createWebsocket.ts
|
|
@@ -825,7 +1275,12 @@ async function parseResponse(out, ctx) {
|
|
|
825
1275
|
}
|
|
826
1276
|
if (typeof out === "string") {
|
|
827
1277
|
const type2 = /^\s*</.test(out) ? "text/html" : "text/plain";
|
|
828
|
-
out = new Response(out, {
|
|
1278
|
+
out = new Response(out, {
|
|
1279
|
+
headers: {
|
|
1280
|
+
"content-type": type2,
|
|
1281
|
+
"content-length": String(Buffer.byteLength(out))
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
829
1284
|
}
|
|
830
1285
|
if (out?.constructor === Object || Array.isArray(out)) {
|
|
831
1286
|
out = json(out);
|
|
@@ -848,17 +1303,7 @@ async function parseResponse(out, ctx) {
|
|
|
848
1303
|
if (!(out instanceof Response)) {
|
|
849
1304
|
throw new Error(`Invalid response type ${out}`);
|
|
850
1305
|
}
|
|
851
|
-
|
|
852
|
-
const origin = cors(ctx.options.cors.origin, ctx.headers.origin);
|
|
853
|
-
if (origin) {
|
|
854
|
-
out.headers.set("Access-Control-Allow-Origin", origin);
|
|
855
|
-
out.headers.set("Access-Control-Allow-Methods", ctx.options.cors.methods);
|
|
856
|
-
out.headers.set("Access-Control-Allow-Headers", ctx.options.cors.headers);
|
|
857
|
-
if (ctx.options.cors.credentials) {
|
|
858
|
-
out.headers.set("Access-Control-Allow-Credentials", "true");
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
1306
|
+
applyCors(out, ctx);
|
|
862
1307
|
if (ctx.time?.times?.length > 1) {
|
|
863
1308
|
out.headers.set("Server-Timing", ctx.time.headers());
|
|
864
1309
|
}
|
|
@@ -976,6 +1421,11 @@ function validate(ctx, schema) {
|
|
|
976
1421
|
|
|
977
1422
|
// src/helpers/handleRequest.ts
|
|
978
1423
|
async function handleRequest(handlers, ctx) {
|
|
1424
|
+
const res = await getResponse(handlers, ctx);
|
|
1425
|
+
if (res) ctx.options.log.request(ctx, res);
|
|
1426
|
+
return res;
|
|
1427
|
+
}
|
|
1428
|
+
async function getResponse(handlers, ctx) {
|
|
979
1429
|
try {
|
|
980
1430
|
for (const [method, matcher, ...cbs] of handlers[ctx.method]) {
|
|
981
1431
|
const match = pathPattern(matcher, ctx.url.pathname || "/");
|
|
@@ -995,7 +1445,9 @@ async function handleRequest(handlers, ctx) {
|
|
|
995
1445
|
if (ctx.platform.provider === "netlify") return;
|
|
996
1446
|
throw new ServerError_default("NOT_FOUND", 404, "Not Found");
|
|
997
1447
|
} catch (error) {
|
|
998
|
-
|
|
1448
|
+
const res = await ctx.options.onError(error, ctx);
|
|
1449
|
+
applyCors(res, ctx);
|
|
1450
|
+
return res;
|
|
999
1451
|
}
|
|
1000
1452
|
}
|
|
1001
1453
|
|
|
@@ -1342,10 +1794,10 @@ async function verify(password, hash3) {
|
|
|
1342
1794
|
// src/auth/findSessionId.ts
|
|
1343
1795
|
var validateToken = (authorization) => {
|
|
1344
1796
|
const [type2, id] = authorization.trim().split(" ");
|
|
1345
|
-
if (
|
|
1797
|
+
if (type2?.toLowerCase() !== "bearer") {
|
|
1346
1798
|
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
1347
1799
|
}
|
|
1348
|
-
if (
|
|
1800
|
+
if (id?.length !== 16) {
|
|
1349
1801
|
throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1350
1802
|
}
|
|
1351
1803
|
return id;
|
|
@@ -1407,7 +1859,7 @@ async function logout(ctx) {
|
|
|
1407
1859
|
return { token: null };
|
|
1408
1860
|
}
|
|
1409
1861
|
if (strategy.includes("cookie")) {
|
|
1410
|
-
return cookies({
|
|
1862
|
+
return cookies({ authentication: null }).redirect("/");
|
|
1411
1863
|
}
|
|
1412
1864
|
if (strategy.includes("jwt")) {
|
|
1413
1865
|
throw new Error("JWT auth not supported yet");
|
|
@@ -1419,18 +1871,37 @@ async function logout(ctx) {
|
|
|
1419
1871
|
}
|
|
1420
1872
|
|
|
1421
1873
|
// src/auth/index.ts
|
|
1874
|
+
var oauth2 = [
|
|
1875
|
+
"github",
|
|
1876
|
+
"google",
|
|
1877
|
+
"microsoft",
|
|
1878
|
+
"discord",
|
|
1879
|
+
"facebook"
|
|
1880
|
+
];
|
|
1422
1881
|
function auth(app) {
|
|
1423
1882
|
app.use(async function middle(ctx) {
|
|
1424
1883
|
ctx.user = await getUser(ctx);
|
|
1425
1884
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
if (!
|
|
1885
|
+
const enabled = app.settings.auth.provider;
|
|
1886
|
+
for (const name of oauth2) {
|
|
1887
|
+
if (!enabled.includes(name)) continue;
|
|
1888
|
+
const key = name.toUpperCase();
|
|
1889
|
+
if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
|
|
1890
|
+
if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
|
|
1429
1891
|
app.get("/auth/logout", logout);
|
|
1430
|
-
app.get(
|
|
1431
|
-
app.get(
|
|
1892
|
+
app.get(`/auth/login/${name}`, providers_default[name].login);
|
|
1893
|
+
app.get(`/auth/callback/${name}`, providers_default[name].callback);
|
|
1432
1894
|
}
|
|
1433
|
-
if (
|
|
1895
|
+
if (enabled.includes("apple")) {
|
|
1896
|
+
const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
|
|
1897
|
+
for (const key of keys) {
|
|
1898
|
+
if (!env[key]) throw new Error(`${key} not defined`);
|
|
1899
|
+
}
|
|
1900
|
+
app.get("/auth/logout", logout);
|
|
1901
|
+
app.get("/auth/login/apple", providers_default.apple.login);
|
|
1902
|
+
app.post("/auth/callback/apple", providers_default.apple.callback);
|
|
1903
|
+
}
|
|
1904
|
+
if (enabled.includes("email")) {
|
|
1434
1905
|
app.post("/auth/logout", logout);
|
|
1435
1906
|
app.post("/auth/register/email", providers_default.email.register);
|
|
1436
1907
|
app.post("/auth/login/email", providers_default.email.login);
|
|
@@ -1452,6 +1923,23 @@ async function assets(ctx) {
|
|
|
1452
1923
|
}
|
|
1453
1924
|
}
|
|
1454
1925
|
|
|
1926
|
+
// src/middle/favicon.ts
|
|
1927
|
+
async function favicon(ctx) {
|
|
1928
|
+
if (ctx.method !== "get") return;
|
|
1929
|
+
if (ctx.url.pathname !== "/favicon.ico") return;
|
|
1930
|
+
const fav = ctx.options.favicon;
|
|
1931
|
+
if (fav) {
|
|
1932
|
+
if (typeof fav === "string") return file(fav);
|
|
1933
|
+
const icon = await fav.read("favicon.ico");
|
|
1934
|
+
return icon ? type("ico").send(icon) : 204;
|
|
1935
|
+
}
|
|
1936
|
+
const handled = ctx.app.handlers.get.some(
|
|
1937
|
+
([method, matcher]) => method !== "*" && pathPattern(matcher, "/favicon.ico")
|
|
1938
|
+
);
|
|
1939
|
+
if (handled) return;
|
|
1940
|
+
return 204;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1455
1943
|
// src/middle/openapi.ts
|
|
1456
1944
|
import * as fsp2 from "fs/promises";
|
|
1457
1945
|
var entities = {
|
|
@@ -1614,6 +2102,17 @@ var openapi_default = async (ctx) => {
|
|
|
1614
2102
|
</html> `;
|
|
1615
2103
|
};
|
|
1616
2104
|
|
|
2105
|
+
// src/middle/preflight.ts
|
|
2106
|
+
function preflight(ctx) {
|
|
2107
|
+
if (ctx.method !== "options") return;
|
|
2108
|
+
if (!ctx.headers["access-control-request-method"]) return;
|
|
2109
|
+
const handled = ctx.app.handlers.options.some(
|
|
2110
|
+
([method, matcher]) => method !== "*" && pathPattern(matcher, ctx.url.pathname)
|
|
2111
|
+
);
|
|
2112
|
+
if (handled) return;
|
|
2113
|
+
return 204;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1617
2116
|
// src/middle/NoSession.ts
|
|
1618
2117
|
var NoSession = class {
|
|
1619
2118
|
};
|
|
@@ -1670,9 +2169,9 @@ import { TLSSocket } from "tls";
|
|
|
1670
2169
|
// src/context/createEvents.ts
|
|
1671
2170
|
function createEvents() {
|
|
1672
2171
|
const events = {};
|
|
1673
|
-
events.on = (name,
|
|
2172
|
+
events.on = (name, callback3) => {
|
|
1674
2173
|
events[name] = events[name] || [];
|
|
1675
|
-
events[name].push(
|
|
2174
|
+
events[name].push(callback3);
|
|
1676
2175
|
};
|
|
1677
2176
|
events.trigger = (name, data) => {
|
|
1678
2177
|
if (!events[name]) return;
|
|
@@ -1723,6 +2222,9 @@ async function createNode(req, app) {
|
|
|
1723
2222
|
const body2 = [];
|
|
1724
2223
|
req.on("data", (chunk) => body2.push(chunk)).on("end", () => resolve2(Buffer.concat(body2))).on("error", reject);
|
|
1725
2224
|
});
|
|
2225
|
+
if (rawBody.length && !headers2["content-length"]) {
|
|
2226
|
+
headers2["content-length"] = String(rawBody.length);
|
|
2227
|
+
}
|
|
1726
2228
|
const body = rawBody ? await parseBody(rawBody, headers2["content-type"], app.settings.uploads) : void 0;
|
|
1727
2229
|
const events = createEvents();
|
|
1728
2230
|
return {
|
|
@@ -1736,12 +2238,16 @@ async function createNode(req, app) {
|
|
|
1736
2238
|
session: {},
|
|
1737
2239
|
init,
|
|
1738
2240
|
events,
|
|
1739
|
-
app
|
|
2241
|
+
app,
|
|
2242
|
+
ip: clientIp(headers2, {
|
|
2243
|
+
remoteAddress: req.socket.remoteAddress || "",
|
|
2244
|
+
trustProxy: app.settings.security.trustProxy
|
|
2245
|
+
})
|
|
1740
2246
|
};
|
|
1741
2247
|
}
|
|
1742
2248
|
|
|
1743
2249
|
// src/context/winter.ts
|
|
1744
|
-
async function createWinter(req, app) {
|
|
2250
|
+
async function createWinter(req, app, server2) {
|
|
1745
2251
|
const init = performance.now();
|
|
1746
2252
|
const method = req.method.toLowerCase();
|
|
1747
2253
|
if (!isValidMethod(method)) {
|
|
@@ -1757,6 +2263,9 @@ async function createWinter(req, app) {
|
|
|
1757
2263
|
(url2) => Object.fromEntries(url2.searchParams.entries())
|
|
1758
2264
|
);
|
|
1759
2265
|
const rawBody = Buffer.from(await req.arrayBuffer());
|
|
2266
|
+
if (rawBody.length && !headers2["content-length"]) {
|
|
2267
|
+
headers2["content-length"] = String(rawBody.length);
|
|
2268
|
+
}
|
|
1760
2269
|
const body = req.body ? await parseBody(rawBody, headers2["content-type"], app.settings.uploads) : void 0;
|
|
1761
2270
|
const events = createEvents();
|
|
1762
2271
|
return {
|
|
@@ -1770,7 +2279,11 @@ async function createWinter(req, app) {
|
|
|
1770
2279
|
session: {},
|
|
1771
2280
|
init,
|
|
1772
2281
|
events,
|
|
1773
|
-
app
|
|
2282
|
+
app,
|
|
2283
|
+
ip: clientIp(headers2, {
|
|
2284
|
+
remoteAddress: server2?.requestIP?.(req)?.address || "",
|
|
2285
|
+
trustProxy: app.settings.security.trustProxy
|
|
2286
|
+
})
|
|
1774
2287
|
};
|
|
1775
2288
|
}
|
|
1776
2289
|
|
|
@@ -1778,7 +2291,7 @@ async function createWinter(req, app) {
|
|
|
1778
2291
|
var Winter = async (app, request, env2) => {
|
|
1779
2292
|
if (env2?.upgrade(request)) return;
|
|
1780
2293
|
Object.assign(globalThis.env, env2);
|
|
1781
|
-
const ctx = await createWinter(request, app);
|
|
2294
|
+
const ctx = await createWinter(request, app, env2);
|
|
1782
2295
|
const res = await handleRequest(app.handlers, ctx);
|
|
1783
2296
|
ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
|
|
1784
2297
|
return res;
|
|
@@ -1796,7 +2309,9 @@ var Node = async (app) => {
|
|
|
1796
2309
|
response.write(out.body || "");
|
|
1797
2310
|
}
|
|
1798
2311
|
response.end();
|
|
1799
|
-
}).listen(app.settings.port)
|
|
2312
|
+
}).listen(app.settings.port, () => {
|
|
2313
|
+
app.settings.log.start(`http://localhost:${app.settings.port}/`);
|
|
2314
|
+
});
|
|
1800
2315
|
};
|
|
1801
2316
|
var Netlify = async (app, request, context) => {
|
|
1802
2317
|
request.context = context;
|
|
@@ -1964,9 +2479,13 @@ var Server = class extends Router {
|
|
|
1964
2479
|
this.websocket = createWebsocket(this.sockets, this.handlers);
|
|
1965
2480
|
if (this.platform.runtime === "node") {
|
|
1966
2481
|
this.node();
|
|
2482
|
+
} else if (this.platform.runtime === "bun") {
|
|
2483
|
+
this.settings.log.start(`http://localhost:${this.settings.port}/`);
|
|
1967
2484
|
}
|
|
1968
2485
|
this.use(timer);
|
|
2486
|
+
if (this.settings.cors) this.use(preflight);
|
|
1969
2487
|
this.use(assets);
|
|
2488
|
+
this.use(favicon);
|
|
1970
2489
|
this.use(session);
|
|
1971
2490
|
if (this.settings.auth) {
|
|
1972
2491
|
auth(this);
|