@server/next 0.49.0 → 0.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +5 -0
- package/index.js +197 -101
- package/package.json +6 -5
- package/readme.md +12 -10
- package/src/jsx/jsx-runtime.js +14 -12
package/index.d.ts
CHANGED
|
@@ -203,6 +203,7 @@ type AuthOption = string | AuthFunction | AuthConfig<any> | AuthVerify<any> | Au
|
|
|
203
203
|
type AuthContext = Pick<Context, "options" | "headers" | "cookies" | "platform" | "app">;
|
|
204
204
|
type AuthEntry = {
|
|
205
205
|
name: string;
|
|
206
|
+
providers?: string[];
|
|
206
207
|
user: (ctx: AuthContext) => Promise<any>;
|
|
207
208
|
routes?: () => Router;
|
|
208
209
|
};
|
|
@@ -577,6 +578,10 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
577
578
|
referrerPolicy?: ReferrerPolicy;
|
|
578
579
|
window?: null;
|
|
579
580
|
}) => Promise<Response>;
|
|
581
|
+
readonly cookies: {
|
|
582
|
+
[k: string]: string;
|
|
583
|
+
};
|
|
584
|
+
clear: () => void;
|
|
580
585
|
};
|
|
581
586
|
}
|
|
582
587
|
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")
|
|
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(
|
|
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
|
-
{
|
|
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
|
-
{
|
|
685
|
-
|
|
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] },
|
|
@@ -828,7 +842,11 @@ var Router = class _Router {
|
|
|
828
842
|
}
|
|
829
843
|
const base = method === "socket" ? [] : this.middleware;
|
|
830
844
|
const fns = [...base, ...rest].filter((fn) => fn != null);
|
|
831
|
-
this.handlers[method].push({
|
|
845
|
+
this.handlers[method].push({
|
|
846
|
+
path,
|
|
847
|
+
options,
|
|
848
|
+
fns
|
|
849
|
+
});
|
|
832
850
|
return this.self();
|
|
833
851
|
}
|
|
834
852
|
socket(pathOrMid, optionsOrMid, ...middleware) {
|
|
@@ -1018,7 +1036,9 @@ function validate(strategy, expires, config2) {
|
|
|
1018
1036
|
seconds(expires);
|
|
1019
1037
|
const { onLogin, getUser, toPublicUser } = config2;
|
|
1020
1038
|
if (onLogin && !getUser) {
|
|
1021
|
-
throw new Error(
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
"`onLogin` needs a `getUser`: something has to resolve the id it returns."
|
|
1041
|
+
);
|
|
1022
1042
|
}
|
|
1023
1043
|
if (isSigned(strategy)) {
|
|
1024
1044
|
if (getUser && !toPublicUser) {
|
|
@@ -1054,7 +1074,9 @@ async function credentialPayload(config2, strategy, ctx, profile) {
|
|
|
1054
1074
|
if (!isSigned(strategy)) return { sub: String(id) };
|
|
1055
1075
|
const user = await getUser(String(id), ctx);
|
|
1056
1076
|
if (user === void 0 || user === null) {
|
|
1057
|
-
throw new Error(
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`getUser returned nothing for the id "${id}" that onLogin just returned`
|
|
1079
|
+
);
|
|
1058
1080
|
}
|
|
1059
1081
|
return { user: await toPublicUser(user) };
|
|
1060
1082
|
}
|
|
@@ -1450,7 +1472,12 @@ var callbackRoute = ({ name, options, provider }, redirects, finish) => async (c
|
|
|
1450
1472
|
const pending = await readState(ctx, query.state);
|
|
1451
1473
|
if (!query.code) throw errors_default.AUTH_NO_CODE();
|
|
1452
1474
|
try {
|
|
1453
|
-
const profile = await provider.exchange(
|
|
1475
|
+
const profile = await provider.exchange(
|
|
1476
|
+
ctx,
|
|
1477
|
+
options,
|
|
1478
|
+
query.code,
|
|
1479
|
+
pending
|
|
1480
|
+
);
|
|
1454
1481
|
return spendState(await finish(ctx, profile));
|
|
1455
1482
|
} catch (error) {
|
|
1456
1483
|
const message = failureMessage(error, name);
|
|
@@ -1477,6 +1504,7 @@ function flowEntry(config2) {
|
|
|
1477
1504
|
};
|
|
1478
1505
|
return {
|
|
1479
1506
|
name: "flow",
|
|
1507
|
+
providers: list.map((one) => one.name),
|
|
1480
1508
|
async user(ctx) {
|
|
1481
1509
|
const payload = await read(ctx, strategy);
|
|
1482
1510
|
if (!payload) return;
|
|
@@ -1489,11 +1517,16 @@ function flowEntry(config2) {
|
|
|
1489
1517
|
const r2 = router();
|
|
1490
1518
|
for (const one of list) {
|
|
1491
1519
|
r2.get(`/auth/login/${one.name}`, SPEC, loginRoute(one));
|
|
1492
|
-
r2.get(
|
|
1520
|
+
r2.get(
|
|
1521
|
+
callbackPath(one.name),
|
|
1522
|
+
SPEC,
|
|
1523
|
+
callbackRoute(one, redirects, finish)
|
|
1524
|
+
);
|
|
1493
1525
|
}
|
|
1494
1526
|
r2.post("/auth/logout", SPEC, async (ctx) => {
|
|
1495
1527
|
const payload = await read(ctx, strategy).catch(() => void 0);
|
|
1496
|
-
|
|
1528
|
+
const id = payload?.sub ?? payload?.user?.id;
|
|
1529
|
+
if (onLogout && id != null) await onLogout(String(id), ctx);
|
|
1497
1530
|
const to = await target(redirects.logout, "/", null, ctx);
|
|
1498
1531
|
if (!inCookie(strategy)) return status(204);
|
|
1499
1532
|
return cookies(NAME, { value: null }).redirect(to);
|
|
@@ -1548,7 +1581,9 @@ function keysOf(issuer, refresh = false) {
|
|
|
1548
1581
|
if (!algorithm) continue;
|
|
1549
1582
|
out.set(
|
|
1550
1583
|
jwk.kid,
|
|
1551
|
-
await crypto.subtle.importKey("jwk", jwk, algorithm, false, [
|
|
1584
|
+
await crypto.subtle.importKey("jwk", jwk, algorithm, false, [
|
|
1585
|
+
"verify"
|
|
1586
|
+
])
|
|
1552
1587
|
);
|
|
1553
1588
|
}
|
|
1554
1589
|
return out;
|
|
@@ -1717,7 +1752,7 @@ function toEntry(auth2) {
|
|
|
1717
1752
|
if ("handler" in auth2) return instanceEntry(auth2);
|
|
1718
1753
|
}
|
|
1719
1754
|
throw new Error(
|
|
1720
|
-
"Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, a library instance
|
|
1755
|
+
"Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, or a library instance."
|
|
1721
1756
|
);
|
|
1722
1757
|
}
|
|
1723
1758
|
|
|
@@ -2005,7 +2040,10 @@ function config(options = {}) {
|
|
|
2005
2040
|
settings.onError = options.onError || defaultOnError;
|
|
2006
2041
|
settings.onResponse = options.onResponse;
|
|
2007
2042
|
const loc = (v) => typeof v === "string" ? v : "enabled";
|
|
2008
|
-
if (settings.auth)
|
|
2043
|
+
if (settings.auth) {
|
|
2044
|
+
const { name, providers: providers2 } = settings.auth;
|
|
2045
|
+
log.message("auth", `${providers2?.join(",") ?? name} enabled`);
|
|
2046
|
+
}
|
|
2009
2047
|
if (settings.public) log.message("public", loc(options.public));
|
|
2010
2048
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
2011
2049
|
if (settings.cors) {
|
|
@@ -2213,7 +2251,10 @@ var generateOpenApiPaths = async (handlers, specPath) => {
|
|
|
2213
2251
|
const schema = await toJsonSchema(meta2.response);
|
|
2214
2252
|
if (schema) {
|
|
2215
2253
|
responses = {
|
|
2216
|
-
200: {
|
|
2254
|
+
200: {
|
|
2255
|
+
description: "OK",
|
|
2256
|
+
content: { "application/json": { schema } }
|
|
2257
|
+
}
|
|
2217
2258
|
};
|
|
2218
2259
|
}
|
|
2219
2260
|
}
|
|
@@ -2354,70 +2395,6 @@ async function socketUser(app, headers2, cookies2) {
|
|
|
2354
2395
|
return app.settings.auth.user(ctx);
|
|
2355
2396
|
}
|
|
2356
2397
|
|
|
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
2398
|
// src/body/bodyParts.ts
|
|
2422
2399
|
var asIterable = (s) => s;
|
|
2423
2400
|
function getMatching(string, regex) {
|
|
@@ -2523,7 +2500,12 @@ function openFile(part) {
|
|
|
2523
2500
|
}
|
|
2524
2501
|
});
|
|
2525
2502
|
const file2 = part.bucket.file(id);
|
|
2526
|
-
part.opened = {
|
|
2503
|
+
part.opened = {
|
|
2504
|
+
type: type2,
|
|
2505
|
+
file: file2,
|
|
2506
|
+
controller,
|
|
2507
|
+
write: file2.write(readable, { type: type2 })
|
|
2508
|
+
};
|
|
2527
2509
|
}
|
|
2528
2510
|
async function feedPart(part, data) {
|
|
2529
2511
|
if (data.length === 0) return;
|
|
@@ -2637,7 +2619,12 @@ async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
|
|
|
2637
2619
|
} else if (state === "headers") {
|
|
2638
2620
|
const i = buf.indexOf(BREAK);
|
|
2639
2621
|
if (i === -1) break;
|
|
2640
|
-
part = startPart(
|
|
2622
|
+
part = startPart(
|
|
2623
|
+
buf.subarray(0, i).toString("utf-8"),
|
|
2624
|
+
bucket2,
|
|
2625
|
+
limits,
|
|
2626
|
+
budget
|
|
2627
|
+
);
|
|
2641
2628
|
buf = buf.subarray(i + BREAK.length);
|
|
2642
2629
|
state = "body";
|
|
2643
2630
|
advanced = true;
|
|
@@ -2745,7 +2732,8 @@ async function parseBody(input, contentType, dest, max = INF, length) {
|
|
|
2745
2732
|
const buf = await toBuffer(input, max);
|
|
2746
2733
|
return buf.length ? buf : void 0;
|
|
2747
2734
|
}
|
|
2748
|
-
if (!bucket2)
|
|
2735
|
+
if (!bucket2)
|
|
2736
|
+
throw errors_default.UPLOAD_NOT_CONFIGURED({ name: "the request body" });
|
|
2749
2737
|
const { maxFileSize } = limits;
|
|
2750
2738
|
if (length != null && maxFileSize != null && length > parseBytes(maxFileSize)) {
|
|
2751
2739
|
throw errors_default.UPLOAD_TOO_LARGE({
|
|
@@ -2804,6 +2792,21 @@ async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
|
|
|
2804
2792
|
return parsed;
|
|
2805
2793
|
}
|
|
2806
2794
|
|
|
2795
|
+
// src/context/isValidMethod.ts
|
|
2796
|
+
var methods = [
|
|
2797
|
+
"get",
|
|
2798
|
+
"post",
|
|
2799
|
+
"put",
|
|
2800
|
+
"patch",
|
|
2801
|
+
"delete",
|
|
2802
|
+
"head",
|
|
2803
|
+
"options",
|
|
2804
|
+
"socket"
|
|
2805
|
+
];
|
|
2806
|
+
function isValidMethod(method) {
|
|
2807
|
+
return methods.includes(method);
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2807
2810
|
// src/util/define.ts
|
|
2808
2811
|
function define(obj, key, cb) {
|
|
2809
2812
|
Object.defineProperty(obj, key, {
|
|
@@ -2820,6 +2823,70 @@ function define(obj, key, cb) {
|
|
|
2820
2823
|
});
|
|
2821
2824
|
}
|
|
2822
2825
|
|
|
2826
|
+
// src/http/cors.ts
|
|
2827
|
+
var localhost = /^https?:\/\/localhost(:\d+)?$/;
|
|
2828
|
+
function cors(config2, origin = "") {
|
|
2829
|
+
origin = origin?.toLowerCase();
|
|
2830
|
+
if (config2 === true) return origin || null;
|
|
2831
|
+
if (config2 === "*") return "*";
|
|
2832
|
+
if (!origin) return null;
|
|
2833
|
+
if (localhost.test(origin)) return origin;
|
|
2834
|
+
const arr = typeof config2 === "string" ? config2.split(/\s*,\s*/g) : [];
|
|
2835
|
+
if (arr.includes(origin)) return origin;
|
|
2836
|
+
console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
|
|
2837
|
+
return null;
|
|
2838
|
+
}
|
|
2839
|
+
function applyCors(res, ctx) {
|
|
2840
|
+
const settings = ctx.options.cors;
|
|
2841
|
+
if (!settings) return;
|
|
2842
|
+
const requestOrigin = ctx.headers.origin || "";
|
|
2843
|
+
let origin = cors(settings.origin, requestOrigin);
|
|
2844
|
+
if (!origin) return;
|
|
2845
|
+
if (settings.credentials && origin === "*") {
|
|
2846
|
+
if (!requestOrigin) return;
|
|
2847
|
+
origin = requestOrigin.toLowerCase();
|
|
2848
|
+
}
|
|
2849
|
+
res.headers.set("Access-Control-Allow-Origin", origin);
|
|
2850
|
+
res.headers.set("Access-Control-Allow-Methods", settings.methods);
|
|
2851
|
+
res.headers.set("Access-Control-Allow-Headers", settings.headers);
|
|
2852
|
+
if (settings.credentials) {
|
|
2853
|
+
res.headers.set("Access-Control-Allow-Credentials", "true");
|
|
2854
|
+
}
|
|
2855
|
+
if (origin !== "*") res.headers.append("Vary", "Origin");
|
|
2856
|
+
if (ctx.method === "options") {
|
|
2857
|
+
res.headers.set("Access-Control-Max-Age", "86400");
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
// src/pipeline/parseResponse.ts
|
|
2862
|
+
async function parseResponse(out, ctx) {
|
|
2863
|
+
if (!out && typeof out !== "string") return null;
|
|
2864
|
+
if (typeof out === "function") {
|
|
2865
|
+
out = await out(ctx);
|
|
2866
|
+
if (!out && typeof out !== "string") return null;
|
|
2867
|
+
}
|
|
2868
|
+
if (typeof out === "number") {
|
|
2869
|
+
return new Response(null, { status: out });
|
|
2870
|
+
}
|
|
2871
|
+
if (!(out instanceof Response) || out.url) {
|
|
2872
|
+
out = await send(out);
|
|
2873
|
+
}
|
|
2874
|
+
return out;
|
|
2875
|
+
}
|
|
2876
|
+
async function finalize(out, ctx) {
|
|
2877
|
+
applyCors(out, ctx);
|
|
2878
|
+
applySecurity(out, ctx);
|
|
2879
|
+
out = await applyCache(out, ctx);
|
|
2880
|
+
const stale = toClear(ctx);
|
|
2881
|
+
if (stale) {
|
|
2882
|
+
out.headers.append("set-cookie", clearCookie(stale));
|
|
2883
|
+
}
|
|
2884
|
+
if (ctx.time?.times?.length > 1) {
|
|
2885
|
+
out.headers.set("Server-Timing", ctx.time.headers());
|
|
2886
|
+
}
|
|
2887
|
+
return out;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2823
2890
|
// src/errors/ValidationError.ts
|
|
2824
2891
|
var ValidationError = class extends errors_default {
|
|
2825
2892
|
source;
|
|
@@ -2862,21 +2929,6 @@ function replace2(target2, values) {
|
|
|
2862
2929
|
Object.assign(target2, values);
|
|
2863
2930
|
}
|
|
2864
2931
|
|
|
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
2932
|
// src/pipeline/handleRequest.ts
|
|
2881
2933
|
async function handleRequest(app, ctx) {
|
|
2882
2934
|
let res = await getResponse(app, ctx);
|
|
@@ -2943,6 +2995,7 @@ async function getResponse(app, ctx) {
|
|
|
2943
2995
|
if (ctx.platform.provider === "netlify") return;
|
|
2944
2996
|
throw errors_default.NOT_FOUND();
|
|
2945
2997
|
} catch (error) {
|
|
2998
|
+
if (ctx.signal.aborted) return;
|
|
2946
2999
|
return ctx.options.onError(error, ctx);
|
|
2947
3000
|
}
|
|
2948
3001
|
}
|
|
@@ -2984,6 +3037,11 @@ var parseHeaders_default = (raw) => {
|
|
|
2984
3037
|
|
|
2985
3038
|
// src/context/writeResponse.ts
|
|
2986
3039
|
async function writeResponse(out, response) {
|
|
3040
|
+
if (response.destroyed || response.writableEnded) {
|
|
3041
|
+
out.body?.cancel().catch(() => {
|
|
3042
|
+
});
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
2987
3045
|
response.writeHead(out.status || 200, parseHeaders_default(out.headers));
|
|
2988
3046
|
try {
|
|
2989
3047
|
if (out.body instanceof ReadableStream) {
|
|
@@ -3240,7 +3298,14 @@ function forwarded(url, headers2, trustProxy) {
|
|
|
3240
3298
|
}
|
|
3241
3299
|
|
|
3242
3300
|
// src/context/create.ts
|
|
3243
|
-
function createContext(app, {
|
|
3301
|
+
function createContext(app, {
|
|
3302
|
+
method: rawMethod,
|
|
3303
|
+
headers: rawHeaders,
|
|
3304
|
+
url: rawUrl,
|
|
3305
|
+
signal,
|
|
3306
|
+
remoteAddress,
|
|
3307
|
+
source
|
|
3308
|
+
}) {
|
|
3244
3309
|
const init = performance.now();
|
|
3245
3310
|
const method = rawMethod?.toLowerCase() || "get";
|
|
3246
3311
|
const headers2 = parseHeaders_default(rawHeaders);
|
|
@@ -3354,7 +3419,8 @@ var Node = async (app) => {
|
|
|
3354
3419
|
response.end("Server Error");
|
|
3355
3420
|
return;
|
|
3356
3421
|
}
|
|
3357
|
-
await writeResponse(out, response);
|
|
3422
|
+
if (out) await writeResponse(out, response);
|
|
3423
|
+
else if (!response.destroyed) response.destroy();
|
|
3358
3424
|
}
|
|
3359
3425
|
);
|
|
3360
3426
|
await attachWebsocket(server2, app);
|
|
@@ -3384,8 +3450,27 @@ function isSerializable(body) {
|
|
|
3384
3450
|
if (body instanceof URLSearchParams) return false;
|
|
3385
3451
|
return true;
|
|
3386
3452
|
}
|
|
3453
|
+
var deletes = (attrs) => attrs.some((attr) => {
|
|
3454
|
+
const [rawKey, value = ""] = attr.split("=");
|
|
3455
|
+
const key = rawKey.trim().toLowerCase();
|
|
3456
|
+
if (key === "max-age") return Number(value) <= 0;
|
|
3457
|
+
if (key === "expires")
|
|
3458
|
+
return new Date(value.trim()).getTime() <= Date.now();
|
|
3459
|
+
return false;
|
|
3460
|
+
});
|
|
3387
3461
|
function ServerTest(app) {
|
|
3388
3462
|
const port = app.settings.port;
|
|
3463
|
+
const jar = /* @__PURE__ */ new Map();
|
|
3464
|
+
const keep = (res) => {
|
|
3465
|
+
for (const line of res.headers.getSetCookie?.() ?? []) {
|
|
3466
|
+
const [pair, ...attrs] = line.split(";");
|
|
3467
|
+
const eq = pair.indexOf("=");
|
|
3468
|
+
if (eq === -1) continue;
|
|
3469
|
+
const name = pair.slice(0, eq).trim();
|
|
3470
|
+
if (deletes(attrs)) jar.delete(name);
|
|
3471
|
+
else jar.set(name, pair.slice(eq + 1).trim());
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3389
3474
|
const fetch2 = async (method, path, options = {}) => {
|
|
3390
3475
|
const headers2 = new Headers(options.headers);
|
|
3391
3476
|
let body = options.body;
|
|
@@ -3393,13 +3478,17 @@ function ServerTest(app) {
|
|
|
3393
3478
|
headers2.set("content-type", "application/json");
|
|
3394
3479
|
body = JSON.stringify(body);
|
|
3395
3480
|
}
|
|
3481
|
+
if (jar.size && !headers2.has("cookie")) {
|
|
3482
|
+
const sent = [...jar].map(([name, value]) => `${name}=${value}`);
|
|
3483
|
+
headers2.set("cookie", sent.join("; "));
|
|
3484
|
+
}
|
|
3396
3485
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path) && !/^https?:\/\//i.test(path)) {
|
|
3397
3486
|
throw new Error(
|
|
3398
3487
|
`Only http(s) URLs can be tested, received "${path}". Pass a path, or the full URL of the host the request should hit.`
|
|
3399
3488
|
);
|
|
3400
3489
|
}
|
|
3401
3490
|
const url = /^https?:\/\//i.test(path) ? path : `http://localhost:${port}${path}`;
|
|
3402
|
-
|
|
3491
|
+
const res = await app.fetch(
|
|
3403
3492
|
new Request(url, {
|
|
3404
3493
|
...options,
|
|
3405
3494
|
method,
|
|
@@ -3407,6 +3496,8 @@ function ServerTest(app) {
|
|
|
3407
3496
|
body
|
|
3408
3497
|
})
|
|
3409
3498
|
);
|
|
3499
|
+
if (res) keep(res);
|
|
3500
|
+
return res;
|
|
3410
3501
|
};
|
|
3411
3502
|
return {
|
|
3412
3503
|
get: (path, options) => fetch2("get", path, options),
|
|
@@ -3415,7 +3506,12 @@ function ServerTest(app) {
|
|
|
3415
3506
|
put: (path, body, options) => fetch2("put", path, { body, ...options }),
|
|
3416
3507
|
patch: (path, body, options) => fetch2("patch", path, { body, ...options }),
|
|
3417
3508
|
delete: (path, options) => fetch2("delete", path, options),
|
|
3418
|
-
options: (path, options) => fetch2("options", path, options)
|
|
3509
|
+
options: (path, options) => fetch2("options", path, options),
|
|
3510
|
+
// The cookies the app has set so far, and a fresh session on demand
|
|
3511
|
+
get cookies() {
|
|
3512
|
+
return Object.fromEntries(jar);
|
|
3513
|
+
},
|
|
3514
|
+
clear: () => jar.clear()
|
|
3419
3515
|
};
|
|
3420
3516
|
}
|
|
3421
3517
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.1",
|
|
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
|
-
"
|
|
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
|
-
"
|
|
21
|
-
"test
|
|
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",
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
22
|
-
import { createClient } from
|
|
21
|
+
import server, { bucket } from "@server/next";
|
|
22
|
+
import { createClient } from "redis";
|
|
23
23
|
|
|
24
|
-
const redis =
|
|
25
|
-
const uploads = bucket.S3('my-bucket', { id,
|
|
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
|
-
|
|
33
|
-
|
|
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://
|
|
40
|
+
See the [full documentation](https://server-js.com/documentation).
|
package/src/jsx/jsx-runtime.js
CHANGED
|
@@ -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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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)
|