@server/next 0.49.0 → 0.49.2

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 CHANGED
@@ -7,22 +7,28 @@ type FileInfo = {
7
7
  type: string | null;
8
8
  modified: Date;
9
9
  };
10
+ type ReadOptions = {
11
+ signal?: AbortSignal;
12
+ };
10
13
  type BucketFile = {
11
14
  readonly path: string;
12
15
  readonly name: string;
13
16
  readonly type?: string;
14
- exists(): Promise<boolean>;
15
- info?(): Promise<FileInfo | null>;
17
+ exists(opts?: ReadOptions): Promise<boolean>;
18
+ info?(opts?: ReadOptions): Promise<FileInfo | null>;
16
19
  write(content: string | Buffer | ReadableStream, options?: {
17
20
  type?: string;
18
- }): Promise<void>;
19
- stream(): ReadableStream;
21
+ } & ReadOptions): Promise<unknown>;
22
+ stream(opts?: ReadOptions): ReadableStream;
20
23
  slice?(start: number, end?: number): BucketFile;
21
- bytes(): Promise<Uint8Array>;
22
- remove(): Promise<void>;
24
+ bytes(opts?: ReadOptions): Promise<Uint8Array>;
25
+ remove(opts?: ReadOptions): Promise<unknown>;
23
26
  };
24
27
  type Bucket = {
25
28
  file(name: string): BucketFile;
29
+ create(content: string | Buffer | ReadableStream, options?: {
30
+ type?: string;
31
+ } & ReadOptions): Promise<BucketFile>;
26
32
  folder?(prefix: string): Bucket;
27
33
  };
28
34
 
@@ -203,6 +209,7 @@ type AuthOption = string | AuthFunction | AuthConfig<any> | AuthVerify<any> | Au
203
209
  type AuthContext = Pick<Context, "options" | "headers" | "cookies" | "platform" | "app">;
204
210
  type AuthEntry = {
205
211
  name: string;
212
+ providers?: string[];
206
213
  user: (ctx: AuthContext) => Promise<any>;
207
214
  routes?: () => Router;
208
215
  };
@@ -577,6 +584,10 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
577
584
  referrerPolicy?: ReferrerPolicy;
578
585
  window?: null;
579
586
  }) => Promise<Response>;
587
+ readonly cookies: {
588
+ [k: string]: string;
589
+ };
590
+ clear: () => void;
580
591
  };
581
592
  }
582
593
  declare function server<U = AuthProfile>(options: Omit<Options, "auth"> & {
package/index.js CHANGED
@@ -245,7 +245,8 @@ function createCookies(key, val) {
245
245
  if (val.value === null) val.expires = EXPIRED;
246
246
  const { value, path, expires, maxAge, httpOnly, secure, sameSite } = val;
247
247
  let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path || "/"}`;
248
- if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
248
+ if (typeof expires !== "undefined")
249
+ str += `;Expires=${normalizeExpires(expires)}`;
249
250
  if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
250
251
  if (httpOnly) str += ";HttpOnly";
251
252
  if (secure) str += ";Secure";
@@ -513,7 +514,11 @@ function serialize(body, headers2) {
513
514
  return body;
514
515
  }
515
516
  if (typeof body === "string") {
516
- fill(headers2, isHtml(body) ? mimes_default.html : mimes_default.text, Buffer.byteLength(body));
517
+ fill(
518
+ headers2,
519
+ isHtml(body) ? mimes_default.html : mimes_default.text,
520
+ Buffer.byteLength(body)
521
+ );
517
522
  return body;
518
523
  }
519
524
  if (body instanceof Uint8Array) {
@@ -676,13 +681,22 @@ var redirect = (...args) => r().redirect(...args);
676
681
  // src/body/sniff.ts
677
682
  var ascii = (text) => [...text].map((char) => char.charCodeAt(0));
678
683
  var SIGNATURES = [
679
- { type: "image/png", magic: [137, 80, 78, 71, 13, 10, 26, 10] },
684
+ {
685
+ type: "image/png",
686
+ magic: [137, 80, 78, 71, 13, 10, 26, 10]
687
+ },
680
688
  { type: "image/jpeg", magic: [255, 216, 255] },
681
689
  { type: "image/gif", magic: ascii("GIF87a") },
682
690
  { type: "image/gif", magic: ascii("GIF89a") },
683
691
  // RIFF containers: the format is at byte 8, so the whole thing is one match
684
- { type: "image/webp", magic: [...ascii("RIFF"), null, null, null, null, ...ascii("WEBP")] },
685
- { type: "audio/wav", magic: [...ascii("RIFF"), null, null, null, null, ...ascii("WAVE")] },
692
+ {
693
+ type: "image/webp",
694
+ magic: [...ascii("RIFF"), null, null, null, null, ...ascii("WEBP")]
695
+ },
696
+ {
697
+ type: "audio/wav",
698
+ magic: [...ascii("RIFF"), null, null, null, null, ...ascii("WAVE")]
699
+ },
686
700
  { type: "image/bmp", magic: ascii("BM") },
687
701
  { type: "image/tiff", magic: [73, 73, 42, 0] },
688
702
  { type: "image/tiff", magic: [77, 77, 0, 42] },
@@ -748,26 +762,25 @@ function resolveUploads(up) {
748
762
  maxFiles: DEFAULT_FILES
749
763
  };
750
764
  }
751
- function getExt(filename) {
752
- const i = filename.lastIndexOf(".");
753
- if (i <= 0) return ".bin";
754
- return filename.slice(i).toLowerCase();
755
- }
756
765
  function validateFile(originalName, contentType, limits, sniffed) {
757
766
  const { fileType: fileType2 } = limits;
758
- if (!fileType2 || fileType2.length === 0) return;
759
767
  if (sniffed === null && isSniffable(contentType)) {
760
768
  throw errors_default.UPLOAD_TYPE_NOT_ALLOWED({
761
769
  name: originalName,
762
770
  type: contentType,
763
- allowed: fileType2
771
+ allowed: fileType2 ?? [contentType]
764
772
  });
765
773
  }
766
- const ext = getExt(originalName);
767
- const mime = contentType.toLowerCase();
768
- const allowed = fileType2.some(
769
- (t) => t.toLowerCase() === mime || t.toLowerCase() === ext
770
- );
774
+ if (!fileType2 || fileType2.length === 0) return;
775
+ const base = (value) => value.split(";")[0].trim().toLowerCase();
776
+ const type2 = base(contentType);
777
+ const allowed = fileType2.some((one) => {
778
+ const entry = one.trim().toLowerCase();
779
+ if (entry.endsWith("/*")) return type2.startsWith(entry.slice(0, -1));
780
+ if (entry.includes("/")) return base(entry) === type2;
781
+ const mapped = mimes_default[entry.replace(/^\./, "")];
782
+ return Boolean(mapped) && base(mapped) === type2;
783
+ });
771
784
  if (!allowed) {
772
785
  throw errors_default.UPLOAD_TYPE_NOT_ALLOWED({
773
786
  name: originalName,
@@ -828,7 +841,11 @@ var Router = class _Router {
828
841
  }
829
842
  const base = method === "socket" ? [] : this.middleware;
830
843
  const fns = [...base, ...rest].filter((fn) => fn != null);
831
- this.handlers[method].push({ path, options, fns });
844
+ this.handlers[method].push({
845
+ path,
846
+ options,
847
+ fns
848
+ });
832
849
  return this.self();
833
850
  }
834
851
  socket(pathOrMid, optionsOrMid, ...middleware) {
@@ -1018,7 +1035,9 @@ function validate(strategy, expires, config2) {
1018
1035
  seconds(expires);
1019
1036
  const { onLogin, getUser, toPublicUser } = config2;
1020
1037
  if (onLogin && !getUser) {
1021
- throw new Error("`onLogin` needs a `getUser`: something has to resolve the id it returns.");
1038
+ throw new Error(
1039
+ "`onLogin` needs a `getUser`: something has to resolve the id it returns."
1040
+ );
1022
1041
  }
1023
1042
  if (isSigned(strategy)) {
1024
1043
  if (getUser && !toPublicUser) {
@@ -1054,7 +1073,9 @@ async function credentialPayload(config2, strategy, ctx, profile) {
1054
1073
  if (!isSigned(strategy)) return { sub: String(id) };
1055
1074
  const user = await getUser(String(id), ctx);
1056
1075
  if (user === void 0 || user === null) {
1057
- throw new Error(`getUser returned nothing for the id "${id}" that onLogin just returned`);
1076
+ throw new Error(
1077
+ `getUser returned nothing for the id "${id}" that onLogin just returned`
1078
+ );
1058
1079
  }
1059
1080
  return { user: await toPublicUser(user) };
1060
1081
  }
@@ -1450,7 +1471,12 @@ var callbackRoute = ({ name, options, provider }, redirects, finish) => async (c
1450
1471
  const pending = await readState(ctx, query.state);
1451
1472
  if (!query.code) throw errors_default.AUTH_NO_CODE();
1452
1473
  try {
1453
- const profile = await provider.exchange(ctx, options, query.code, pending);
1474
+ const profile = await provider.exchange(
1475
+ ctx,
1476
+ options,
1477
+ query.code,
1478
+ pending
1479
+ );
1454
1480
  return spendState(await finish(ctx, profile));
1455
1481
  } catch (error) {
1456
1482
  const message = failureMessage(error, name);
@@ -1477,6 +1503,7 @@ function flowEntry(config2) {
1477
1503
  };
1478
1504
  return {
1479
1505
  name: "flow",
1506
+ providers: list.map((one) => one.name),
1480
1507
  async user(ctx) {
1481
1508
  const payload = await read(ctx, strategy);
1482
1509
  if (!payload) return;
@@ -1489,11 +1516,16 @@ function flowEntry(config2) {
1489
1516
  const r2 = router();
1490
1517
  for (const one of list) {
1491
1518
  r2.get(`/auth/login/${one.name}`, SPEC, loginRoute(one));
1492
- r2.get(callbackPath(one.name), SPEC, callbackRoute(one, redirects, finish));
1519
+ r2.get(
1520
+ callbackPath(one.name),
1521
+ SPEC,
1522
+ callbackRoute(one, redirects, finish)
1523
+ );
1493
1524
  }
1494
1525
  r2.post("/auth/logout", SPEC, async (ctx) => {
1495
1526
  const payload = await read(ctx, strategy).catch(() => void 0);
1496
- if (onLogout && payload?.sub) await onLogout(payload.sub, ctx);
1527
+ const id = payload?.sub ?? payload?.user?.id;
1528
+ if (onLogout && id != null) await onLogout(String(id), ctx);
1497
1529
  const to = await target(redirects.logout, "/", null, ctx);
1498
1530
  if (!inCookie(strategy)) return status(204);
1499
1531
  return cookies(NAME, { value: null }).redirect(to);
@@ -1548,7 +1580,9 @@ function keysOf(issuer, refresh = false) {
1548
1580
  if (!algorithm) continue;
1549
1581
  out.set(
1550
1582
  jwk.kid,
1551
- await crypto.subtle.importKey("jwk", jwk, algorithm, false, ["verify"])
1583
+ await crypto.subtle.importKey("jwk", jwk, algorithm, false, [
1584
+ "verify"
1585
+ ])
1552
1586
  );
1553
1587
  }
1554
1588
  return out;
@@ -1717,7 +1751,7 @@ function toEntry(auth2) {
1717
1751
  if ("handler" in auth2) return instanceEntry(auth2);
1718
1752
  }
1719
1753
  throw new Error(
1720
- "Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, a library instance, or an array of those."
1754
+ "Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, or a library instance."
1721
1755
  );
1722
1756
  }
1723
1757
 
@@ -2005,7 +2039,10 @@ function config(options = {}) {
2005
2039
  settings.onError = options.onError || defaultOnError;
2006
2040
  settings.onResponse = options.onResponse;
2007
2041
  const loc = (v) => typeof v === "string" ? v : "enabled";
2008
- if (settings.auth) log.message("auth", `${settings.auth.name} enabled`);
2042
+ if (settings.auth) {
2043
+ const { name, providers: providers2 } = settings.auth;
2044
+ log.message("auth", `${providers2?.join(",") ?? name} enabled`);
2045
+ }
2009
2046
  if (settings.public) log.message("public", loc(options.public));
2010
2047
  if (settings.uploads) log.message("uploads", loc(options.uploads));
2011
2048
  if (settings.cors) {
@@ -2109,11 +2146,11 @@ async function assets(ctx) {
2109
2146
  try {
2110
2147
  const key = ctx.url.pathname.replace(/^\/+/, "");
2111
2148
  const file2 = ctx.options.public.file(key);
2149
+ const read2 = { signal: ctx.signal };
2112
2150
  const info = file2.info?.bind(file2);
2113
- const meta2 = info ? await info() : null;
2114
- if (info ? !meta2 : !await file2.exists()) return;
2115
- const ext = ctx.url.pathname.split(".").pop()?.toLowerCase();
2116
- const ctype = mimeOf(ctx.url.pathname) || meta2?.type || ext;
2151
+ const meta2 = info ? await info(read2) : null;
2152
+ if (info ? !meta2 : !await file2.exists(read2)) return;
2153
+ const ctype = mimeOf(ctx.url.pathname) || meta2?.type || void 0;
2117
2154
  const headers2 = {
2118
2155
  "cache-control": resolveCache(ctx.options.cache) ?? DEFAULT_CACHE
2119
2156
  };
@@ -2142,10 +2179,10 @@ async function assets(ctx) {
2142
2179
  ...headers2,
2143
2180
  "content-range": `bytes ${start}-${end}/${meta2.size}`,
2144
2181
  "content-length": String(end - start + 1)
2145
- }).send(file2.slice(start, end + 1).stream());
2182
+ }).send(file2.slice(start, end + 1).stream(read2));
2146
2183
  }
2147
2184
  }
2148
- return type(ctype).headers(headers2).send(file2.stream());
2185
+ return type(ctype).headers(headers2).send(file2.stream(read2));
2149
2186
  } catch {
2150
2187
  }
2151
2188
  }
@@ -2213,7 +2250,10 @@ var generateOpenApiPaths = async (handlers, specPath) => {
2213
2250
  const schema = await toJsonSchema(meta2.response);
2214
2251
  if (schema) {
2215
2252
  responses = {
2216
- 200: { description: "OK", content: { "application/json": { schema } } }
2253
+ 200: {
2254
+ description: "OK",
2255
+ content: { "application/json": { schema } }
2256
+ }
2217
2257
  };
2218
2258
  }
2219
2259
  }
@@ -2354,70 +2394,6 @@ async function socketUser(app, headers2, cookies2) {
2354
2394
  return app.settings.auth.user(ctx);
2355
2395
  }
2356
2396
 
2357
- // src/http/cors.ts
2358
- var localhost = /^https?:\/\/localhost(:\d+)?$/;
2359
- function cors(config2, origin = "") {
2360
- origin = origin?.toLowerCase();
2361
- if (config2 === true) return origin || null;
2362
- if (config2 === "*") return "*";
2363
- if (!origin) return null;
2364
- if (localhost.test(origin)) return origin;
2365
- const arr = typeof config2 === "string" ? config2.split(/\s*,\s*/g) : [];
2366
- if (arr.includes(origin)) return origin;
2367
- console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
2368
- return null;
2369
- }
2370
- function applyCors(res, ctx) {
2371
- const settings = ctx.options.cors;
2372
- if (!settings) return;
2373
- const requestOrigin = ctx.headers.origin || "";
2374
- let origin = cors(settings.origin, requestOrigin);
2375
- if (!origin) return;
2376
- if (settings.credentials && origin === "*") {
2377
- if (!requestOrigin) return;
2378
- origin = requestOrigin.toLowerCase();
2379
- }
2380
- res.headers.set("Access-Control-Allow-Origin", origin);
2381
- res.headers.set("Access-Control-Allow-Methods", settings.methods);
2382
- res.headers.set("Access-Control-Allow-Headers", settings.headers);
2383
- if (settings.credentials) {
2384
- res.headers.set("Access-Control-Allow-Credentials", "true");
2385
- }
2386
- if (origin !== "*") res.headers.append("Vary", "Origin");
2387
- if (ctx.method === "options") {
2388
- res.headers.set("Access-Control-Max-Age", "86400");
2389
- }
2390
- }
2391
-
2392
- // src/pipeline/parseResponse.ts
2393
- async function parseResponse(out, ctx) {
2394
- if (!out && typeof out !== "string") return null;
2395
- if (typeof out === "function") {
2396
- out = await out(ctx);
2397
- if (!out && typeof out !== "string") return null;
2398
- }
2399
- if (typeof out === "number") {
2400
- return new Response(null, { status: out });
2401
- }
2402
- if (!(out instanceof Response) || out.url) {
2403
- out = await send(out);
2404
- }
2405
- return out;
2406
- }
2407
- async function finalize(out, ctx) {
2408
- applyCors(out, ctx);
2409
- applySecurity(out, ctx);
2410
- out = await applyCache(out, ctx);
2411
- const stale = toClear(ctx);
2412
- if (stale) {
2413
- out.headers.append("set-cookie", clearCookie(stale));
2414
- }
2415
- if (ctx.time?.times?.length > 1) {
2416
- out.headers.set("Server-Timing", ctx.time.headers());
2417
- }
2418
- return out;
2419
- }
2420
-
2421
2397
  // src/body/bodyParts.ts
2422
2398
  var asIterable = (s) => s;
2423
2399
  function getMatching(string, regex) {
@@ -2432,8 +2408,6 @@ function isProbablyText(buffer) {
2432
2408
  }
2433
2409
  return true;
2434
2410
  }
2435
- var extByMime = {};
2436
- for (const ext in mimes_default) extByMime[mimes_default[ext]] = ext;
2437
2411
  function addField(body, name, value) {
2438
2412
  if (body[name] === void 0) {
2439
2413
  body[name] = value;
@@ -2442,7 +2416,7 @@ function addField(body, name, value) {
2442
2416
  if (!Array.isArray(body[name])) body[name] = [body[name]];
2443
2417
  body[name].push(value);
2444
2418
  }
2445
- function makeFilePart(name, filename, declared, bucket2, limits, budget) {
2419
+ function makeFilePart(name, filename, declared, bucket2, limits, budget, signal) {
2446
2420
  return {
2447
2421
  kind: "file",
2448
2422
  name,
@@ -2451,13 +2425,14 @@ function makeFilePart(name, filename, declared, bucket2, limits, budget) {
2451
2425
  bucket: bucket2,
2452
2426
  limits,
2453
2427
  budget,
2428
+ signal,
2454
2429
  head: [],
2455
2430
  headSize: 0,
2456
2431
  opened: null,
2457
2432
  size: 0
2458
2433
  };
2459
2434
  }
2460
- function startPart(headerStr, bucket2, limits, budget) {
2435
+ function startPart(headerStr, bucket2, limits, budget, signal) {
2461
2436
  const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
2462
2437
  if (!name) return { kind: "skip" };
2463
2438
  const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
@@ -2470,7 +2445,7 @@ function startPart(headerStr, bucket2, limits, budget) {
2470
2445
  if (maxFiles != null && budget.files > maxFiles) {
2471
2446
  throw errors_default.UPLOAD_TOO_MANY_FILES({ limit: String(maxFiles) });
2472
2447
  }
2473
- return makeFilePart(name, filename, type2, bucket2, limits, budget);
2448
+ return makeFilePart(name, filename, type2, bucket2, limits, budget, signal);
2474
2449
  }
2475
2450
  async function abortFile(part, error) {
2476
2451
  if (part.opened) {
@@ -2480,8 +2455,6 @@ async function abortFile(part, error) {
2480
2455
  });
2481
2456
  } catch {
2482
2457
  }
2483
- await part.opened.file.remove().catch(() => {
2484
- });
2485
2458
  }
2486
2459
  throw error;
2487
2460
  }
@@ -2514,16 +2487,17 @@ function openFile(part) {
2514
2487
  const sniffed = sniff(head);
2515
2488
  const type2 = resolveType(sniffed, part.declared);
2516
2489
  validateFile(part.filename, type2, part.limits, sniffed);
2517
- const ext = sniffed ? extByMime[type2] : void 0;
2518
- const id = `${createId()}${ext ? `.${ext}` : ""}`;
2519
2490
  let controller;
2520
2491
  const readable = new ReadableStream({
2521
2492
  start(c) {
2522
2493
  controller = c;
2523
2494
  }
2524
2495
  });
2525
- const file2 = part.bucket.file(id);
2526
- part.opened = { type: type2, file: file2, controller, write: file2.write(readable, { type: type2 }) };
2496
+ part.opened = {
2497
+ type: type2,
2498
+ controller,
2499
+ write: part.bucket.create(readable, { type: type2, signal: part.signal })
2500
+ };
2527
2501
  }
2528
2502
  async function feedPart(part, data) {
2529
2503
  if (data.length === 0) return;
@@ -2566,10 +2540,10 @@ async function endPart(part, body) {
2566
2540
  }
2567
2541
  const opened = part.opened;
2568
2542
  opened.controller.close();
2569
- await opened.write;
2543
+ const file2 = await opened.write;
2570
2544
  const { minSize } = part.limits;
2571
2545
  if (minSize != null && part.size < parseBytes(minSize)) {
2572
- await opened.file.remove().catch(() => {
2546
+ await file2.remove().catch(() => {
2573
2547
  });
2574
2548
  throw errors_default.UPLOAD_TOO_SMALL({
2575
2549
  name: part.filename,
@@ -2579,7 +2553,7 @@ async function endPart(part, body) {
2579
2553
  }
2580
2554
  addField(body, part.name, {
2581
2555
  name: part.filename,
2582
- path: opened.file.path,
2556
+ path: file2.path,
2583
2557
  type: opened.type,
2584
2558
  size: part.size
2585
2559
  });
@@ -2599,7 +2573,7 @@ function getBoundary(header) {
2599
2573
  return null;
2600
2574
  }
2601
2575
  var BREAK = Buffer.from("\r\n\r\n");
2602
- async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
2576
+ async function parseMultipart(stream, boundary, bucket2, limits, max = INF, signal) {
2603
2577
  const budget = { used: 0, max: INF, files: 0 };
2604
2578
  const delim = Buffer.from(`\r
2605
2579
  --${boundary}`);
@@ -2637,7 +2611,13 @@ async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
2637
2611
  } else if (state === "headers") {
2638
2612
  const i = buf.indexOf(BREAK);
2639
2613
  if (i === -1) break;
2640
- part = startPart(buf.subarray(0, i).toString("utf-8"), bucket2, limits, budget);
2614
+ part = startPart(
2615
+ buf.subarray(0, i).toString("utf-8"),
2616
+ bucket2,
2617
+ limits,
2618
+ budget,
2619
+ signal
2620
+ );
2641
2621
  buf = buf.subarray(i + BREAK.length);
2642
2622
  state = "body";
2643
2623
  advanced = true;
@@ -2700,12 +2680,16 @@ function parseUrlEncoded(text) {
2700
2680
  }
2701
2681
  return out;
2702
2682
  }
2703
- async function streamRawToBucket(stream, type2, bucket2, limits) {
2704
- const part = makeFilePart("body", "upload", type2, bucket2, limits, {
2705
- used: 0,
2706
- max: INF,
2707
- files: 0
2708
- });
2683
+ async function streamRawToBucket(stream, type2, bucket2, limits, signal) {
2684
+ const part = makeFilePart(
2685
+ "body",
2686
+ "upload",
2687
+ type2,
2688
+ bucket2,
2689
+ limits,
2690
+ { used: 0, max: INF, files: 0 },
2691
+ signal
2692
+ );
2709
2693
  for await (const chunk of asIterable(stream)) {
2710
2694
  await feedPart(part, Buffer.from(chunk));
2711
2695
  }
@@ -2713,7 +2697,7 @@ async function streamRawToBucket(stream, type2, bucket2, limits) {
2713
2697
  await endPart(part, body);
2714
2698
  return part.size ? body.body : void 0;
2715
2699
  }
2716
- async function parseBody(input, contentType, dest, max = INF, length) {
2700
+ async function parseBody(input, contentType, dest, max = INF, length, signal) {
2717
2701
  const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
2718
2702
  let bucket2;
2719
2703
  let limits = {};
@@ -2727,7 +2711,14 @@ async function parseBody(input, contentType, dest, max = INF, length) {
2727
2711
  if (type2 && /multipart\/form-data/i.test(type2)) {
2728
2712
  const boundary = getBoundary(type2);
2729
2713
  if (!boundary) throw errors_default.BODY_INVALID_MULTIPART();
2730
- return parseMultipart(toStream(input), boundary, bucket2, limits, max);
2714
+ return parseMultipart(
2715
+ toStream(input),
2716
+ boundary,
2717
+ bucket2,
2718
+ limits,
2719
+ max,
2720
+ signal
2721
+ );
2731
2722
  }
2732
2723
  if (!type2 || /^text\//i.test(type2)) {
2733
2724
  const buf = await toBuffer(input, max);
@@ -2745,7 +2736,8 @@ async function parseBody(input, contentType, dest, max = INF, length) {
2745
2736
  const buf = await toBuffer(input, max);
2746
2737
  return buf.length ? buf : void 0;
2747
2738
  }
2748
- if (!bucket2) throw errors_default.UPLOAD_NOT_CONFIGURED({ name: "the request body" });
2739
+ if (!bucket2)
2740
+ throw errors_default.UPLOAD_NOT_CONFIGURED({ name: "the request body" });
2749
2741
  const { maxFileSize } = limits;
2750
2742
  if (length != null && maxFileSize != null && length > parseBytes(maxFileSize)) {
2751
2743
  throw errors_default.UPLOAD_TOO_LARGE({
@@ -2754,7 +2746,7 @@ async function parseBody(input, contentType, dest, max = INF, length) {
2754
2746
  limit: String(maxFileSize)
2755
2747
  });
2756
2748
  }
2757
- return streamRawToBucket(toStream(input), type2, bucket2, limits);
2749
+ return streamRawToBucket(toStream(input), type2, bucket2, limits, signal);
2758
2750
  }
2759
2751
 
2760
2752
  // src/body/body.ts
@@ -2796,7 +2788,8 @@ async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
2796
2788
  ctx.headers["content-type"],
2797
2789
  ctx.options.uploads,
2798
2790
  max,
2799
- Number.isFinite(declared) ? declared : void 0
2791
+ Number.isFinite(declared) ? declared : void 0,
2792
+ ctx.signal
2800
2793
  );
2801
2794
  if (size && !ctx.headers["content-length"]) {
2802
2795
  ctx.headers["content-length"] = String(size);
@@ -2804,6 +2797,21 @@ async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
2804
2797
  return parsed;
2805
2798
  }
2806
2799
 
2800
+ // src/context/isValidMethod.ts
2801
+ var methods = [
2802
+ "get",
2803
+ "post",
2804
+ "put",
2805
+ "patch",
2806
+ "delete",
2807
+ "head",
2808
+ "options",
2809
+ "socket"
2810
+ ];
2811
+ function isValidMethod(method) {
2812
+ return methods.includes(method);
2813
+ }
2814
+
2807
2815
  // src/util/define.ts
2808
2816
  function define(obj, key, cb) {
2809
2817
  Object.defineProperty(obj, key, {
@@ -2820,6 +2828,70 @@ function define(obj, key, cb) {
2820
2828
  });
2821
2829
  }
2822
2830
 
2831
+ // src/http/cors.ts
2832
+ var localhost = /^https?:\/\/localhost(:\d+)?$/;
2833
+ function cors(config2, origin = "") {
2834
+ origin = origin?.toLowerCase();
2835
+ if (config2 === true) return origin || null;
2836
+ if (config2 === "*") return "*";
2837
+ if (!origin) return null;
2838
+ if (localhost.test(origin)) return origin;
2839
+ const arr = typeof config2 === "string" ? config2.split(/\s*,\s*/g) : [];
2840
+ if (arr.includes(origin)) return origin;
2841
+ console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
2842
+ return null;
2843
+ }
2844
+ function applyCors(res, ctx) {
2845
+ const settings = ctx.options.cors;
2846
+ if (!settings) return;
2847
+ const requestOrigin = ctx.headers.origin || "";
2848
+ let origin = cors(settings.origin, requestOrigin);
2849
+ if (!origin) return;
2850
+ if (settings.credentials && origin === "*") {
2851
+ if (!requestOrigin) return;
2852
+ origin = requestOrigin.toLowerCase();
2853
+ }
2854
+ res.headers.set("Access-Control-Allow-Origin", origin);
2855
+ res.headers.set("Access-Control-Allow-Methods", settings.methods);
2856
+ res.headers.set("Access-Control-Allow-Headers", settings.headers);
2857
+ if (settings.credentials) {
2858
+ res.headers.set("Access-Control-Allow-Credentials", "true");
2859
+ }
2860
+ if (origin !== "*") res.headers.append("Vary", "Origin");
2861
+ if (ctx.method === "options") {
2862
+ res.headers.set("Access-Control-Max-Age", "86400");
2863
+ }
2864
+ }
2865
+
2866
+ // src/pipeline/parseResponse.ts
2867
+ async function parseResponse(out, ctx) {
2868
+ if (!out && typeof out !== "string") return null;
2869
+ if (typeof out === "function") {
2870
+ out = await out(ctx);
2871
+ if (!out && typeof out !== "string") return null;
2872
+ }
2873
+ if (typeof out === "number") {
2874
+ return new Response(null, { status: out });
2875
+ }
2876
+ if (!(out instanceof Response) || out.url) {
2877
+ out = await send(out);
2878
+ }
2879
+ return out;
2880
+ }
2881
+ async function finalize(out, ctx) {
2882
+ applyCors(out, ctx);
2883
+ applySecurity(out, ctx);
2884
+ out = await applyCache(out, ctx);
2885
+ const stale = toClear(ctx);
2886
+ if (stale) {
2887
+ out.headers.append("set-cookie", clearCookie(stale));
2888
+ }
2889
+ if (ctx.time?.times?.length > 1) {
2890
+ out.headers.set("Server-Timing", ctx.time.headers());
2891
+ }
2892
+ return out;
2893
+ }
2894
+
2823
2895
  // src/errors/ValidationError.ts
2824
2896
  var ValidationError = class extends errors_default {
2825
2897
  source;
@@ -2862,21 +2934,6 @@ function replace2(target2, values) {
2862
2934
  Object.assign(target2, values);
2863
2935
  }
2864
2936
 
2865
- // src/context/isValidMethod.ts
2866
- var methods = [
2867
- "get",
2868
- "post",
2869
- "put",
2870
- "patch",
2871
- "delete",
2872
- "head",
2873
- "options",
2874
- "socket"
2875
- ];
2876
- function isValidMethod(method) {
2877
- return methods.includes(method);
2878
- }
2879
-
2880
2937
  // src/pipeline/handleRequest.ts
2881
2938
  async function handleRequest(app, ctx) {
2882
2939
  let res = await getResponse(app, ctx);
@@ -2943,6 +3000,7 @@ async function getResponse(app, ctx) {
2943
3000
  if (ctx.platform.provider === "netlify") return;
2944
3001
  throw errors_default.NOT_FOUND();
2945
3002
  } catch (error) {
3003
+ if (ctx.signal.aborted) return;
2946
3004
  return ctx.options.onError(error, ctx);
2947
3005
  }
2948
3006
  }
@@ -2984,6 +3042,11 @@ var parseHeaders_default = (raw) => {
2984
3042
 
2985
3043
  // src/context/writeResponse.ts
2986
3044
  async function writeResponse(out, response) {
3045
+ if (response.destroyed || response.writableEnded) {
3046
+ out.body?.cancel().catch(() => {
3047
+ });
3048
+ return;
3049
+ }
2987
3050
  response.writeHead(out.status || 200, parseHeaders_default(out.headers));
2988
3051
  try {
2989
3052
  if (out.body instanceof ReadableStream) {
@@ -3240,7 +3303,14 @@ function forwarded(url, headers2, trustProxy) {
3240
3303
  }
3241
3304
 
3242
3305
  // src/context/create.ts
3243
- function createContext(app, { method: rawMethod, headers: rawHeaders, url: rawUrl, signal, remoteAddress, source }) {
3306
+ function createContext(app, {
3307
+ method: rawMethod,
3308
+ headers: rawHeaders,
3309
+ url: rawUrl,
3310
+ signal,
3311
+ remoteAddress,
3312
+ source
3313
+ }) {
3244
3314
  const init = performance.now();
3245
3315
  const method = rawMethod?.toLowerCase() || "get";
3246
3316
  const headers2 = parseHeaders_default(rawHeaders);
@@ -3354,7 +3424,8 @@ var Node = async (app) => {
3354
3424
  response.end("Server Error");
3355
3425
  return;
3356
3426
  }
3357
- await writeResponse(out, response);
3427
+ if (out) await writeResponse(out, response);
3428
+ else if (!response.destroyed) response.destroy();
3358
3429
  }
3359
3430
  );
3360
3431
  await attachWebsocket(server2, app);
@@ -3384,8 +3455,27 @@ function isSerializable(body) {
3384
3455
  if (body instanceof URLSearchParams) return false;
3385
3456
  return true;
3386
3457
  }
3458
+ var deletes = (attrs) => attrs.some((attr) => {
3459
+ const [rawKey, value = ""] = attr.split("=");
3460
+ const key = rawKey.trim().toLowerCase();
3461
+ if (key === "max-age") return Number(value) <= 0;
3462
+ if (key === "expires")
3463
+ return new Date(value.trim()).getTime() <= Date.now();
3464
+ return false;
3465
+ });
3387
3466
  function ServerTest(app) {
3388
3467
  const port = app.settings.port;
3468
+ const jar = /* @__PURE__ */ new Map();
3469
+ const keep = (res) => {
3470
+ for (const line of res.headers.getSetCookie?.() ?? []) {
3471
+ const [pair, ...attrs] = line.split(";");
3472
+ const eq = pair.indexOf("=");
3473
+ if (eq === -1) continue;
3474
+ const name = pair.slice(0, eq).trim();
3475
+ if (deletes(attrs)) jar.delete(name);
3476
+ else jar.set(name, pair.slice(eq + 1).trim());
3477
+ }
3478
+ };
3389
3479
  const fetch2 = async (method, path, options = {}) => {
3390
3480
  const headers2 = new Headers(options.headers);
3391
3481
  let body = options.body;
@@ -3393,13 +3483,17 @@ function ServerTest(app) {
3393
3483
  headers2.set("content-type", "application/json");
3394
3484
  body = JSON.stringify(body);
3395
3485
  }
3486
+ if (jar.size && !headers2.has("cookie")) {
3487
+ const sent = [...jar].map(([name, value]) => `${name}=${value}`);
3488
+ headers2.set("cookie", sent.join("; "));
3489
+ }
3396
3490
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path) && !/^https?:\/\//i.test(path)) {
3397
3491
  throw new Error(
3398
3492
  `Only http(s) URLs can be tested, received "${path}". Pass a path, or the full URL of the host the request should hit.`
3399
3493
  );
3400
3494
  }
3401
3495
  const url = /^https?:\/\//i.test(path) ? path : `http://localhost:${port}${path}`;
3402
- return await app.fetch(
3496
+ const res = await app.fetch(
3403
3497
  new Request(url, {
3404
3498
  ...options,
3405
3499
  method,
@@ -3407,6 +3501,8 @@ function ServerTest(app) {
3407
3501
  body
3408
3502
  })
3409
3503
  );
3504
+ if (res) keep(res);
3505
+ return res;
3410
3506
  };
3411
3507
  return {
3412
3508
  get: (path, options) => fetch2("get", path, options),
@@ -3415,7 +3511,12 @@ function ServerTest(app) {
3415
3511
  put: (path, body, options) => fetch2("put", path, { body, ...options }),
3416
3512
  patch: (path, body, options) => fetch2("patch", path, { body, ...options }),
3417
3513
  delete: (path, options) => fetch2("delete", path, options),
3418
- options: (path, options) => fetch2("options", path, options)
3514
+ options: (path, options) => fetch2("options", path, options),
3515
+ // The cookies the app has set so far, and a fresh session on demand
3516
+ get cookies() {
3517
+ return Object.fromEntries(jar);
3518
+ },
3519
+ clear: () => jar.clear()
3419
3520
  };
3420
3521
  }
3421
3522
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.49.0",
3
+ "version": "0.49.2",
4
4
  "description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
5
5
  "homepage": "https://server-js.com/",
6
6
  "repository": "github:franciscop/server-next",
@@ -14,11 +14,11 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24 --external zod --external @valibot/to-json-schema",
17
- "prepare": "mkdir -p node_modules/@server && ln -sfn ../.. node_modules/@server/next",
17
+ "postinstall": "mkdir -p node_modules/@server && ln -sfn ../.. node_modules/@server/next",
18
18
  "start": "bun test --watch",
19
- "lint": "npx tsc --noEmit && npx @biomejs/biome lint ./src --error-on-warnings",
20
- "test": "npm run test:bun && npm run lint",
21
- "test:bun": "bun test src demo"
19
+ "lint": "npm run prettier && npx tsc --noEmit && npx @biomejs/biome lint ./src --error-on-warnings",
20
+ "prettier": "prettier --write ./src",
21
+ "test": "bun test src demo"
22
22
  },
23
23
  "main": "index.js",
24
24
  "type": "module",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "antarctic": "^0.6.0",
69
- "bucket": "^0.7.1"
69
+ "bucket": "^0.10.1"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/bun": "^1.3.0",
@@ -75,6 +75,7 @@
75
75
  "arktype": "^2.2.3",
76
76
  "bun": "^1.3.13",
77
77
  "check-dts": "^0.8.2",
78
+ "prettier": "^3.9.6",
78
79
  "tsup": "^8.5.1",
79
80
  "typescript": "^6.0.2",
80
81
  "valibot": "^1.4.2",
package/readme.md CHANGED
@@ -7,7 +7,7 @@ npm install @server/next
7
7
  ```
8
8
 
9
9
  ```js
10
- import server from '@server/next';
10
+ import server from "@server/next";
11
11
 
12
12
  export default server({ uploads: './uploads' })
13
13
  .get('/', () => 'Hello world')
@@ -15,24 +15,26 @@ export default server({ uploads: './uploads' })
15
15
  .post('/avatar', (ctx) => ctx.body.avatar.path);
16
16
  ```
17
17
 
18
- Key-value stores and file storage come included, so logins work out of the box and `uploads` takes a folder path. For Redis, S3 and the rest, pass the client straight in:
18
+ File storage comes included, so `uploads` takes a folder path or any bucket. Auth stores nothing of its own: two callbacks put the user wherever you already keep data.
19
19
 
20
20
  ```js
21
- import server, { bucket, kv } from '@server/next';
22
- import { createClient } from 'redis';
21
+ import server, { bucket } from "@server/next";
22
+ import { createClient } from "redis";
23
23
 
24
- const redis = kv(createClient({ url }));
25
- const uploads = bucket.S3('my-bucket', { id, key });
24
+ const redis = await createClient({ url }).connect();
25
+ const uploads = bucket.S3('my-bucket', { id, secret });
26
26
 
27
27
  export default server({
28
28
  uploads,
29
29
  auth: {
30
- strategy: 'cookie',
31
30
  providers: ['github'],
32
- users: redis.prefix('user:'),
33
- sessions: redis.prefix('session:'),
31
+ onLogin: async (profile) => {
32
+ await redis.set(`user:${profile.id}`, JSON.stringify(profile));
33
+ return profile.id;
34
+ },
35
+ getUser: async (id) => JSON.parse(await redis.get(`user:${id}`)),
34
36
  },
35
37
  });
36
38
  ```
37
39
 
38
- See the [full documentation](https://serverjs.io/documentation).
40
+ See the [full documentation](https://server-js.com/documentation).
@@ -47,18 +47,20 @@ const minifyCss = (str) => {
47
47
  // squeezed, then put back. Comments go first and unconditionally, so one
48
48
  // written inside a string is stripped along with the rest.
49
49
  const quoted = [];
50
- return str
51
- .replace(/\/\*[\s\S]*?\*\//g, "")
52
- .replace(
53
- /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,
54
- (match) => `\0${quoted.push(match) - 1}\0`,
55
- )
56
- .replace(/\s+/g, " ")
57
- // `+` is left out on purpose for `calc(100% + 10px)`
58
- .replace(/\s*([{}:;,>~])\s*/g, "$1")
59
- .replace(/;}/g, "}")
60
- .replace(/\0(\d+)\0/g, (_, i) => quoted[i])
61
- .trim();
50
+ return (
51
+ str
52
+ .replace(/\/\*[\s\S]*?\*\//g, "")
53
+ .replace(
54
+ /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,
55
+ (match) => `\0${quoted.push(match) - 1}\0`,
56
+ )
57
+ .replace(/\s+/g, " ")
58
+ // `+` is left out on purpose for `calc(100% + 10px)`
59
+ .replace(/\s*([{}:;,>~])\s*/g, "$1")
60
+ .replace(/;}/g, "}")
61
+ .replace(/\0(\d+)\0/g, (_, i) => quoted[i])
62
+ .trim()
63
+ );
62
64
  };
63
65
 
64
66
  // React element detection (safe for custom objects too)