@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.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: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)",
49
- AUTH_INVALID_STRATEGY: "Invalid Authorization type '{strategy}', valid one is '{valid}'",
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: "Invalid provider '{provider}', valid ones are: '{valid}'",
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/auth/updateUser.ts
83
- async function updateUser(user, auth2, store) {
84
- if (auth2.provider === "email") {
85
- return await store.set(auth2.email, user);
86
- }
87
- }
88
-
89
- // src/auth/providers/email.ts
90
- var createSession = async (user, ctx) => {
91
- const { strategy, session: session2, cleanUser, redirect: redirect2 = "/user" } = ctx.options.auth;
92
- user = await cleanUser(user);
93
- const id = createId();
94
- const provider = "email";
95
- ctx.user = {
96
- id,
97
- strategy,
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
- return new Response(JSON.stringify(body), { status: status2, headers: headers2 });
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 login = function githubLogin() {
313
- return redirect(
314
- `https://github.com/login/oauth/authorize?client_id=${env.GITHUB_ID}&scope=user:email`
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 callback = async (ctx) => {
327
- const { strategy, cleanUser, store, session: session2, redirect: redirect2 } = ctx.options.auth;
651
+ var callback2 = async (ctx) => {
652
+ checkState(ctx, ctx.url.query.state);
328
653
  const profile = await getUserProfile(ctx.url.query.code);
329
- const auth2 = {
330
- id: createId(),
331
- strategy,
654
+ const res = await finishLogin(ctx, {
332
655
  provider: "github",
333
- user: String(profile.id),
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
- picture: profile.avatar_url,
344
- location: profile.location,
345
- created: profile.created_at
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
- await store.set(auth2.user, user);
348
- await session2.set(auth2.id, auth2, { expires: "1w" });
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 = { email: email_default, github: github_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 || !auth2.provider.length) {
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 || !stats.isFile()) return null;
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 \u2014 pass a bucket to upload() or call .store()`
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
- // src/helpers/createCookies.ts
703
- var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
704
- var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
705
- parse.millisecond = parse.ms = 1e-3;
706
- parse.second = parse.sec = parse.s = parse[""] = 1;
707
- parse.minute = parse.min = parse.m = parse.s * 60;
708
- parse.hour = parse.hr = parse.h = parse.m * 60;
709
- parse.day = parse.d = parse.h * 24;
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
- str = str.toLowerCase().replace(/[,_]/g, "");
720
- const [_, value, units] = times.exec(str) || [];
721
- if (!units) return null;
722
- const unitValue = parse[units] || parse[units.replace(/s$/, "")];
723
- if (!unitValue) return null;
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 (typeof expires === "number") {
738
- return new Date(Date.now() + expires).toUTCString();
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, { headers: { "content-type": type2 } });
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
- if (ctx.options.cors) {
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
- return ctx.options.onError(error, ctx);
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 (!type2 || type2.toLowerCase() !== "bearer") {
1797
+ if (type2?.toLowerCase() !== "bearer") {
1346
1798
  throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1347
1799
  }
1348
- if (!id || id.length !== 16) {
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({ authorization: null }).redirect("/");
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
- if (app.settings.auth.provider.includes("github")) {
1427
- if (!env.GITHUB_ID) throw new Error("GITHUB_ID not defined");
1428
- if (!env.GITHUB_SECRET) throw new Error("GITHUB_SECRET not defined");
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("/auth/login/github", providers_default.github.login);
1431
- app.get("/auth/callback/github", providers_default.github.callback);
1892
+ app.get(`/auth/login/${name}`, providers_default[name].login);
1893
+ app.get(`/auth/callback/${name}`, providers_default[name].callback);
1432
1894
  }
1433
- if (app.settings.auth.provider.includes("email")) {
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, callback2) => {
2172
+ events.on = (name, callback3) => {
1674
2173
  events[name] = events[name] || [];
1675
- events[name].push(callback2);
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);