@server/next 0.34.2 → 0.35.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 +30 -14
- package/index.js +317 -54
- package/package.json +2 -2
package/index.d.ts
CHANGED
|
@@ -30,11 +30,15 @@ declare namespace JSX {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
type BodyMode = "parse" | "raw" | "stream";
|
|
33
|
+
type BodyOption = BodyMode | {
|
|
34
|
+
mode?: BodyMode;
|
|
35
|
+
max?: number | string | false;
|
|
36
|
+
};
|
|
33
37
|
type RouteOptions = {
|
|
34
38
|
tags?: string | string[];
|
|
35
39
|
title?: string;
|
|
36
40
|
description?: string;
|
|
37
|
-
body?:
|
|
41
|
+
body?: BodyOption;
|
|
38
42
|
};
|
|
39
43
|
type Route = {
|
|
40
44
|
path: string;
|
|
@@ -51,15 +55,23 @@ type Cookie = {
|
|
|
51
55
|
sameSite?: "Strict" | "Lax" | "None";
|
|
52
56
|
};
|
|
53
57
|
type RouterMethod = "*" | Method;
|
|
58
|
+
type FileInfo = {
|
|
59
|
+
exists: boolean;
|
|
60
|
+
size: number;
|
|
61
|
+
date: Date | null;
|
|
62
|
+
type?: string | null;
|
|
63
|
+
};
|
|
54
64
|
type BucketFile = {
|
|
55
65
|
readonly path: string;
|
|
56
66
|
readonly id: string;
|
|
57
67
|
readonly name: string;
|
|
58
68
|
exists(): Promise<boolean>;
|
|
69
|
+
info?(): Promise<FileInfo>;
|
|
59
70
|
write(content: string | Buffer | ReadableStream, options?: {
|
|
60
71
|
type?: string;
|
|
61
72
|
}): Promise<void>;
|
|
62
73
|
stream(): ReadableStream;
|
|
74
|
+
slice?(start: number, end?: number): BucketFile;
|
|
63
75
|
bytes(): Promise<Uint8Array>;
|
|
64
76
|
remove(): Promise<void>;
|
|
65
77
|
};
|
|
@@ -102,7 +114,7 @@ type KVStore = {
|
|
|
102
114
|
keys: () => Promise<string[]>;
|
|
103
115
|
};
|
|
104
116
|
type Provider = "email" | "github" | "google" | "microsoft" | "discord" | "facebook" | "apple";
|
|
105
|
-
type Strategy = "cookie" | "jwt" | "token";
|
|
117
|
+
type Strategy = "cookie" | "jwt" | "token" | "key";
|
|
106
118
|
type AuthSession = {
|
|
107
119
|
id: string;
|
|
108
120
|
provider: Provider;
|
|
@@ -115,9 +127,10 @@ type AuthUser<T = Record<string, any>> = T & {
|
|
|
115
127
|
strategy: Strategy;
|
|
116
128
|
email: string;
|
|
117
129
|
};
|
|
118
|
-
type AuthOption = `${Strategy}:${Provider}` | {
|
|
130
|
+
type AuthOption = `${Strategy}:${Provider}` | "key" | {
|
|
119
131
|
strategy: Strategy;
|
|
120
|
-
providers
|
|
132
|
+
providers?: Provider | Provider[];
|
|
133
|
+
key?: string;
|
|
121
134
|
session?: KVStore;
|
|
122
135
|
store?: KVStore;
|
|
123
136
|
redirect?: string;
|
|
@@ -128,6 +141,7 @@ type AuthSettings = {
|
|
|
128
141
|
strategy: Strategy;
|
|
129
142
|
store: KVStore;
|
|
130
143
|
session: KVStore;
|
|
144
|
+
key?: string;
|
|
131
145
|
cleanUser: <T = AuthUser>(user: T) => T | Promise<T>;
|
|
132
146
|
redirect: string;
|
|
133
147
|
};
|
|
@@ -173,7 +187,7 @@ type Options = {
|
|
|
173
187
|
log?: LogLevel | boolean;
|
|
174
188
|
favicon?: string | BucketFile;
|
|
175
189
|
security?: boolean | SecurityOptions;
|
|
176
|
-
body?:
|
|
190
|
+
body?: BodyOption;
|
|
177
191
|
};
|
|
178
192
|
type Settings = {
|
|
179
193
|
port: number;
|
|
@@ -192,7 +206,7 @@ type Settings = {
|
|
|
192
206
|
log: Logger;
|
|
193
207
|
favicon?: string | BucketFile;
|
|
194
208
|
security: SecuritySettings;
|
|
195
|
-
body:
|
|
209
|
+
body: BodyOption;
|
|
196
210
|
};
|
|
197
211
|
type Time = {
|
|
198
212
|
(name: string): void;
|
|
@@ -216,7 +230,9 @@ type ParamsToObject<Params extends string> = {
|
|
|
216
230
|
};
|
|
217
231
|
type PathToParams<Path extends string> = ParamsToObject<ExtractPathParams<Path>>;
|
|
218
232
|
type BunEnv = Record<string, string> & {
|
|
219
|
-
upgrade?: (req: Request
|
|
233
|
+
upgrade?: (req: Request, options?: {
|
|
234
|
+
data?: any;
|
|
235
|
+
}) => boolean;
|
|
220
236
|
};
|
|
221
237
|
type Context<Params extends Record<string, string | undefined> = Record<string, string>, O extends ServerConfig = object> = {
|
|
222
238
|
method: Method;
|
|
@@ -393,9 +409,9 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
393
409
|
signal?: AbortSignal | null;
|
|
394
410
|
window?: null;
|
|
395
411
|
}) => Promise<Response>;
|
|
396
|
-
post: (path: string, body?: string | number | boolean | ArrayBuffer | {
|
|
412
|
+
post: (path: string, body?: string | number | boolean | ArrayBuffer | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams | {
|
|
397
413
|
[key: string]: SerializableValue;
|
|
398
|
-
} | SerializableValue[]
|
|
414
|
+
} | SerializableValue[], options?: {
|
|
399
415
|
cache?: RequestCache;
|
|
400
416
|
credentials?: RequestCredentials;
|
|
401
417
|
headers?: HeadersInit;
|
|
@@ -410,9 +426,9 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
410
426
|
signal?: AbortSignal | null;
|
|
411
427
|
window?: null;
|
|
412
428
|
}) => Promise<Response>;
|
|
413
|
-
put: (path: string, body?: string | number | boolean | ArrayBuffer | {
|
|
429
|
+
put: (path: string, body?: string | number | boolean | ArrayBuffer | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams | {
|
|
414
430
|
[key: string]: SerializableValue;
|
|
415
|
-
} | SerializableValue[]
|
|
431
|
+
} | SerializableValue[], options?: {
|
|
416
432
|
cache?: RequestCache;
|
|
417
433
|
credentials?: RequestCredentials;
|
|
418
434
|
headers?: HeadersInit;
|
|
@@ -427,9 +443,9 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
427
443
|
signal?: AbortSignal | null;
|
|
428
444
|
window?: null;
|
|
429
445
|
}) => Promise<Response>;
|
|
430
|
-
patch: (path: string, body?: string | number | boolean | ArrayBuffer | {
|
|
446
|
+
patch: (path: string, body?: string | number | boolean | ArrayBuffer | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams | {
|
|
431
447
|
[key: string]: SerializableValue;
|
|
432
|
-
} | SerializableValue[]
|
|
448
|
+
} | SerializableValue[], options?: {
|
|
433
449
|
cache?: RequestCache;
|
|
434
450
|
credentials?: RequestCredentials;
|
|
435
451
|
headers?: HeadersInit;
|
|
@@ -478,4 +494,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
478
494
|
}
|
|
479
495
|
declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
|
|
480
496
|
|
|
481
|
-
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type Bucket, type BucketFile, type BunEnv, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type LimitOptions, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
|
|
497
|
+
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, type LimitOptions, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
|
package/index.js
CHANGED
|
@@ -226,8 +226,26 @@ function localBucket(root) {
|
|
|
226
226
|
}
|
|
227
227
|
return full;
|
|
228
228
|
};
|
|
229
|
-
const file2 = (name) => {
|
|
229
|
+
const file2 = (name, win) => {
|
|
230
230
|
const full = resolveKey(name);
|
|
231
|
+
const read = () => {
|
|
232
|
+
let opts;
|
|
233
|
+
if (win) {
|
|
234
|
+
opts = { start: win.start };
|
|
235
|
+
if (Number.isFinite(win.end)) opts.end = Math.max(win.start, win.end - 1);
|
|
236
|
+
}
|
|
237
|
+
const nodeStream = fs.createReadStream(full, opts);
|
|
238
|
+
return new ReadableStream({
|
|
239
|
+
start(controller) {
|
|
240
|
+
nodeStream.on("data", (chunk) => controller.enqueue(chunk));
|
|
241
|
+
nodeStream.on("end", () => controller.close());
|
|
242
|
+
nodeStream.on("error", (err) => controller.error(err));
|
|
243
|
+
},
|
|
244
|
+
cancel() {
|
|
245
|
+
nodeStream.destroy();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
};
|
|
231
249
|
return {
|
|
232
250
|
path: full,
|
|
233
251
|
id: name.replace(/^\/+/, ""),
|
|
@@ -236,6 +254,21 @@ function localBucket(root) {
|
|
|
236
254
|
const stats = await fsp.stat(full).catch(() => null);
|
|
237
255
|
return !!stats?.isFile();
|
|
238
256
|
},
|
|
257
|
+
async info() {
|
|
258
|
+
const stats = await fsp.stat(full).catch(() => null);
|
|
259
|
+
const exists = !!stats?.isFile();
|
|
260
|
+
const total = stats?.size ?? 0;
|
|
261
|
+
const size = win ? Math.max(0, Math.min(win.end, total) - win.start) : total;
|
|
262
|
+
return { exists, size, date: stats?.mtime ?? null };
|
|
263
|
+
},
|
|
264
|
+
// Read-only view of [start, end), composed relative to the current window.
|
|
265
|
+
slice(start, end) {
|
|
266
|
+
const base2 = win?.start ?? 0;
|
|
267
|
+
const cap = win?.end ?? Number.POSITIVE_INFINITY;
|
|
268
|
+
const s = Math.min(cap, base2 + Math.max(0, start));
|
|
269
|
+
const e = end === void 0 ? cap : Math.min(cap, base2 + end);
|
|
270
|
+
return file2(name, { start: s, end: e });
|
|
271
|
+
},
|
|
239
272
|
async write(content) {
|
|
240
273
|
await fsp.mkdir(path.dirname(full), { recursive: true });
|
|
241
274
|
if (content instanceof ReadableStream) {
|
|
@@ -252,19 +285,10 @@ function localBucket(root) {
|
|
|
252
285
|
await fsp.writeFile(full, content);
|
|
253
286
|
},
|
|
254
287
|
stream() {
|
|
255
|
-
|
|
256
|
-
return new ReadableStream({
|
|
257
|
-
start(controller) {
|
|
258
|
-
nodeStream.on("data", (chunk) => controller.enqueue(chunk));
|
|
259
|
-
nodeStream.on("end", () => controller.close());
|
|
260
|
-
nodeStream.on("error", (err) => controller.error(err));
|
|
261
|
-
},
|
|
262
|
-
cancel() {
|
|
263
|
-
nodeStream.destroy();
|
|
264
|
-
}
|
|
265
|
-
});
|
|
288
|
+
return read();
|
|
266
289
|
},
|
|
267
290
|
async bytes() {
|
|
291
|
+
if (win) return new Uint8Array(await new Response(read()).arrayBuffer());
|
|
268
292
|
return new Uint8Array(await fsp.readFile(full));
|
|
269
293
|
},
|
|
270
294
|
async remove() {
|
|
@@ -602,17 +626,34 @@ async function parseBody(input, contentType, dest) {
|
|
|
602
626
|
return streamToBucket(toStream(input), type2, dest);
|
|
603
627
|
}
|
|
604
628
|
|
|
629
|
+
// src/helpers/StatusError.ts
|
|
630
|
+
var StatusError = class extends Error {
|
|
631
|
+
status;
|
|
632
|
+
constructor(msg, status2 = 500) {
|
|
633
|
+
super(msg);
|
|
634
|
+
this.status = status2;
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
605
638
|
// src/helpers/body.ts
|
|
639
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
640
|
+
var resolveMax = (max) => max === false || max == null ? INF : parseBytes(max);
|
|
641
|
+
var tooLarge = (max) => new StatusError(`Request body exceeds the ${max}-byte limit`, 413);
|
|
606
642
|
var sources = /* @__PURE__ */ new WeakMap();
|
|
607
643
|
function setBodySource(ctx, source) {
|
|
608
644
|
sources.set(ctx, source);
|
|
609
645
|
}
|
|
610
|
-
async function resolveBody(ctx,
|
|
646
|
+
async function resolveBody(ctx, body) {
|
|
611
647
|
const source = sources.get(ctx);
|
|
612
648
|
if (!source) return void 0;
|
|
649
|
+
const mode = typeof body === "string" ? body : body?.mode ?? "parse";
|
|
650
|
+
const max = resolveMax(typeof body === "object" ? body?.max : void 0);
|
|
651
|
+
const declared = Number(ctx.headers["content-length"]);
|
|
652
|
+
if (max !== INF && declared > max) throw tooLarge(max);
|
|
613
653
|
if (mode === "stream") return source.getStream();
|
|
614
654
|
if (mode === "raw") {
|
|
615
655
|
const raw = await source.getBuffer();
|
|
656
|
+
if (raw.length > max) throw tooLarge(max);
|
|
616
657
|
if (!raw.length) return void 0;
|
|
617
658
|
if (!ctx.headers["content-length"]) {
|
|
618
659
|
ctx.headers["content-length"] = String(raw.length);
|
|
@@ -626,11 +667,12 @@ async function resolveBody(ctx, mode) {
|
|
|
626
667
|
new TransformStream({
|
|
627
668
|
transform(chunk, controller) {
|
|
628
669
|
size += chunk.byteLength;
|
|
670
|
+
if (size > max) return controller.error(tooLarge(max));
|
|
629
671
|
controller.enqueue(chunk);
|
|
630
672
|
}
|
|
631
673
|
})
|
|
632
674
|
);
|
|
633
|
-
const
|
|
675
|
+
const parsed = await parseBody(
|
|
634
676
|
counted,
|
|
635
677
|
ctx.headers["content-type"],
|
|
636
678
|
ctx.options.uploads
|
|
@@ -638,7 +680,7 @@ async function resolveBody(ctx, mode) {
|
|
|
638
680
|
if (size && !ctx.headers["content-length"]) {
|
|
639
681
|
ctx.headers["content-length"] = String(size);
|
|
640
682
|
}
|
|
641
|
-
return
|
|
683
|
+
return parsed;
|
|
642
684
|
}
|
|
643
685
|
|
|
644
686
|
// src/helpers/clientIp.ts
|
|
@@ -787,6 +829,73 @@ var json = (...args) => r().json(...args);
|
|
|
787
829
|
var file = (...args) => r().file(...args);
|
|
788
830
|
var redirect = (...args) => r().redirect(...args);
|
|
789
831
|
|
|
832
|
+
// src/helpers/jwt.ts
|
|
833
|
+
var enc = new TextEncoder();
|
|
834
|
+
var dec = new TextDecoder();
|
|
835
|
+
var b64url = (data) => {
|
|
836
|
+
const bytes = typeof data === "string" ? enc.encode(data) : data;
|
|
837
|
+
let bin = "";
|
|
838
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
839
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
840
|
+
};
|
|
841
|
+
var unb64url = (seg) => {
|
|
842
|
+
let b64 = seg.replace(/-/g, "+").replace(/_/g, "/");
|
|
843
|
+
b64 += "=".repeat((4 - b64.length % 4) % 4);
|
|
844
|
+
const bin = atob(b64);
|
|
845
|
+
const bytes = new Uint8Array(bin.length);
|
|
846
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
847
|
+
return bytes;
|
|
848
|
+
};
|
|
849
|
+
var hmacKey = (secret) => crypto.subtle.importKey(
|
|
850
|
+
"raw",
|
|
851
|
+
enc.encode(secret),
|
|
852
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
853
|
+
false,
|
|
854
|
+
["sign", "verify"]
|
|
855
|
+
);
|
|
856
|
+
async function signJwt(payload, secret, expires) {
|
|
857
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
858
|
+
const claims = {
|
|
859
|
+
iat: now,
|
|
860
|
+
...expires ? { exp: now + expires } : {},
|
|
861
|
+
...payload
|
|
862
|
+
};
|
|
863
|
+
const head = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
|
864
|
+
const body = b64url(JSON.stringify(claims));
|
|
865
|
+
const data = `${head}.${body}`;
|
|
866
|
+
const key = await hmacKey(secret);
|
|
867
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
|
|
868
|
+
return `${data}.${b64url(new Uint8Array(sig))}`;
|
|
869
|
+
}
|
|
870
|
+
async function verifyJwt(token, secret) {
|
|
871
|
+
const parts = token.split(".");
|
|
872
|
+
if (parts.length !== 3) return null;
|
|
873
|
+
const [head, body, sig] = parts;
|
|
874
|
+
let header;
|
|
875
|
+
try {
|
|
876
|
+
header = JSON.parse(dec.decode(unb64url(head)));
|
|
877
|
+
} catch {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
if (header?.alg !== "HS256") return null;
|
|
881
|
+
const key = await hmacKey(secret);
|
|
882
|
+
const ok = await crypto.subtle.verify(
|
|
883
|
+
"HMAC",
|
|
884
|
+
key,
|
|
885
|
+
unb64url(sig),
|
|
886
|
+
enc.encode(`${head}.${body}`)
|
|
887
|
+
);
|
|
888
|
+
if (!ok) return null;
|
|
889
|
+
let payload;
|
|
890
|
+
try {
|
|
891
|
+
payload = JSON.parse(dec.decode(unb64url(body)));
|
|
892
|
+
} catch {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
if (payload?.exp && Math.floor(Date.now() / 1e3) >= payload.exp) return null;
|
|
896
|
+
return payload;
|
|
897
|
+
}
|
|
898
|
+
|
|
790
899
|
// src/auth/finishLogin.ts
|
|
791
900
|
async function finishLogin(ctx, input) {
|
|
792
901
|
const settings = ctx.options.auth;
|
|
@@ -807,7 +916,13 @@ async function finishLogin(ctx, input) {
|
|
|
807
916
|
}
|
|
808
917
|
user = await cleanUser(user);
|
|
809
918
|
if (input.store !== false) await settings.store.set(key, user);
|
|
810
|
-
|
|
919
|
+
if (!strategy.includes("jwt")) {
|
|
920
|
+
await settings.session.set(auth2.id, auth2, { expires: "1w" });
|
|
921
|
+
}
|
|
922
|
+
if (strategy.includes("jwt")) {
|
|
923
|
+
const token = await signJwt(auth2, ctx.options.secret, 7 * 24 * 60 * 60);
|
|
924
|
+
return status(201).json({ ...user, token });
|
|
925
|
+
}
|
|
811
926
|
if (strategy.includes("token")) {
|
|
812
927
|
return status(201).json({ ...user, token: auth2.id });
|
|
813
928
|
}
|
|
@@ -820,7 +935,6 @@ async function finishLogin(ctx, input) {
|
|
|
820
935
|
sameSite: "Lax"
|
|
821
936
|
}).redirect(settings.redirect);
|
|
822
937
|
}
|
|
823
|
-
if (strategy.includes("jwt")) throw new Error("JWT auth not supported yet");
|
|
824
938
|
if (strategy.includes("key")) throw new Error("Key auth not supported yet");
|
|
825
939
|
throw new Error("Unknown auth type");
|
|
826
940
|
}
|
|
@@ -909,7 +1023,7 @@ function clearState() {
|
|
|
909
1023
|
// src/auth/providers/apple.ts
|
|
910
1024
|
var AUTHORIZE = "https://appleid.apple.com/auth/authorize";
|
|
911
1025
|
var TOKEN = "https://appleid.apple.com/auth/token";
|
|
912
|
-
var
|
|
1026
|
+
var b64url2 = (data) => {
|
|
913
1027
|
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
914
1028
|
let bin = "";
|
|
915
1029
|
for (const byte of bytes) bin += String.fromCharCode(byte);
|
|
@@ -931,7 +1045,7 @@ var clientSecret = async () => {
|
|
|
931
1045
|
aud: "https://appleid.apple.com",
|
|
932
1046
|
sub: env.APPLE_ID
|
|
933
1047
|
};
|
|
934
|
-
const data = `${
|
|
1048
|
+
const data = `${b64url2(JSON.stringify(header))}.${b64url2(JSON.stringify(payload))}`;
|
|
935
1049
|
const pem = String(env.APPLE_PRIVATE_KEY).replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
|
936
1050
|
const der = Uint8Array.from(atob(pem), (c) => c.charCodeAt(0));
|
|
937
1051
|
const key = await crypto.subtle.importKey(
|
|
@@ -946,7 +1060,7 @@ var clientSecret = async () => {
|
|
|
946
1060
|
key,
|
|
947
1061
|
new TextEncoder().encode(data)
|
|
948
1062
|
);
|
|
949
|
-
return `${data}.${
|
|
1063
|
+
return `${data}.${b64url2(new Uint8Array(sig))}`;
|
|
950
1064
|
};
|
|
951
1065
|
var login = (ctx) => {
|
|
952
1066
|
const { state, cookie } = startState(ctx, true);
|
|
@@ -1282,6 +1396,19 @@ function parseAuthOptions(auth2, all) {
|
|
|
1282
1396
|
throw new Error("Auth options needs a strategy");
|
|
1283
1397
|
}
|
|
1284
1398
|
const strategy = auth2.strategy;
|
|
1399
|
+
if (strategy === "key") {
|
|
1400
|
+
const key = auth2.key || env.AUTH_KEY;
|
|
1401
|
+
if (!key) {
|
|
1402
|
+
throw new Error("`key` auth needs the AUTH_KEY env var (or auth.key)");
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
strategy,
|
|
1406
|
+
providers: [],
|
|
1407
|
+
key,
|
|
1408
|
+
redirect: auth2.redirect || defaultRedirect,
|
|
1409
|
+
cleanUser: auth2.cleanUser || defaultCleanUser
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1285
1412
|
const list = Array.isArray(auth2.providers) ? auth2.providers : auth2.providers ? [auth2.providers] : [];
|
|
1286
1413
|
if (!list.length) {
|
|
1287
1414
|
throw new Error("Auth options needs a provider");
|
|
@@ -1529,6 +1656,11 @@ function config(options = {}) {
|
|
|
1529
1656
|
if (options.auth || env2.AUTH) {
|
|
1530
1657
|
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null, options);
|
|
1531
1658
|
}
|
|
1659
|
+
if (settings.auth?.strategy.includes("jwt") && settings.secret.startsWith("unsafe-")) {
|
|
1660
|
+
console.warn(
|
|
1661
|
+
"[server:auth] jwt strategy with no SECRET set: tokens are signed with a random per-process secret, so they break on restart and across instances. Set the SECRET environment variable (or the `secret` option)."
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1532
1664
|
if (options.openapi) {
|
|
1533
1665
|
if (options.openapi === true) {
|
|
1534
1666
|
settings.openapi = {};
|
|
@@ -1604,9 +1736,10 @@ function etag(bytes) {
|
|
|
1604
1736
|
function createWebsocket(sockets, handlers) {
|
|
1605
1737
|
const run = (event, socket, body) => {
|
|
1606
1738
|
const routes = handlers.socket?.filter((r2) => r2.path === event) ?? [];
|
|
1739
|
+
const user = socket.user ?? socket.data?.user;
|
|
1607
1740
|
for (const route of routes) {
|
|
1608
1741
|
for (const fn of route.fns) {
|
|
1609
|
-
fn({ socket, sockets, body });
|
|
1742
|
+
fn({ socket, sockets, body, user });
|
|
1610
1743
|
}
|
|
1611
1744
|
}
|
|
1612
1745
|
};
|
|
@@ -1720,13 +1853,20 @@ async function parseResponse(out, ctx) {
|
|
|
1720
1853
|
if (!ctx.options.session?.store) {
|
|
1721
1854
|
throw ServerError_default.NO_STORE();
|
|
1722
1855
|
}
|
|
1723
|
-
|
|
1856
|
+
let id = ctx.cookies.session;
|
|
1857
|
+
if (!id) {
|
|
1858
|
+
id = createId();
|
|
1724
1859
|
out.headers.append(
|
|
1725
1860
|
"set-cookie",
|
|
1726
|
-
createCookies("session", {
|
|
1861
|
+
createCookies("session", {
|
|
1862
|
+
value: id,
|
|
1863
|
+
path: "/",
|
|
1864
|
+
httpOnly: true,
|
|
1865
|
+
secure: ctx.platform.production,
|
|
1866
|
+
sameSite: "Lax"
|
|
1867
|
+
})
|
|
1727
1868
|
);
|
|
1728
1869
|
}
|
|
1729
|
-
const id = ctx.cookies.session;
|
|
1730
1870
|
ctx.options.session.store.set(id, ctx.session);
|
|
1731
1871
|
}
|
|
1732
1872
|
if (ctx.options.cookies) {
|
|
@@ -1787,15 +1927,6 @@ function pathPattern(pattern, path2) {
|
|
|
1787
1927
|
return null;
|
|
1788
1928
|
}
|
|
1789
1929
|
|
|
1790
|
-
// src/helpers/StatusError.ts
|
|
1791
|
-
var StatusError = class extends Error {
|
|
1792
|
-
status;
|
|
1793
|
-
constructor(msg, status2 = 500) {
|
|
1794
|
-
super(msg);
|
|
1795
|
-
this.status = status2;
|
|
1796
|
-
}
|
|
1797
|
-
};
|
|
1798
|
-
|
|
1799
1930
|
// src/helpers/validate.ts
|
|
1800
1931
|
function validate(ctx, schema) {
|
|
1801
1932
|
if (!schema || typeof schema !== "object") return;
|
|
@@ -2035,6 +2166,14 @@ async function verify(password, hash3) {
|
|
|
2035
2166
|
});
|
|
2036
2167
|
}
|
|
2037
2168
|
|
|
2169
|
+
// src/helpers/safeEqual.ts
|
|
2170
|
+
function safeEqual(a, b) {
|
|
2171
|
+
if (a.length !== b.length) return false;
|
|
2172
|
+
let diff = 0;
|
|
2173
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2174
|
+
return diff === 0;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2038
2177
|
// src/auth/findSessionId.ts
|
|
2039
2178
|
var validateToken = (authorization) => {
|
|
2040
2179
|
const [type2, id] = authorization.trim().split(" ");
|
|
@@ -2067,12 +2206,41 @@ function findSessionId(ctx) {
|
|
|
2067
2206
|
}
|
|
2068
2207
|
|
|
2069
2208
|
// src/auth/getUser.ts
|
|
2209
|
+
function getKeyUser(ctx) {
|
|
2210
|
+
const expected = ctx.options.auth.key;
|
|
2211
|
+
const header = ctx.headers.authorization;
|
|
2212
|
+
if (!header) return;
|
|
2213
|
+
const [type2, provided] = header.trim().split(" ");
|
|
2214
|
+
if (type2?.toLowerCase() !== "bearer" || !provided) {
|
|
2215
|
+
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2216
|
+
}
|
|
2217
|
+
if (!expected || !safeEqual(provided, expected)) {
|
|
2218
|
+
throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2219
|
+
}
|
|
2220
|
+
return { id: "key", strategy: "key", provider: "key" };
|
|
2221
|
+
}
|
|
2222
|
+
async function getAuthSession(ctx) {
|
|
2223
|
+
const strategy = ctx.options.auth.strategy;
|
|
2224
|
+
if (strategy.includes("jwt")) {
|
|
2225
|
+
const header = ctx.headers.authorization;
|
|
2226
|
+
if (!header) return;
|
|
2227
|
+
const [type2, token] = header.trim().split(" ");
|
|
2228
|
+
if (type2?.toLowerCase() !== "bearer" || !token) {
|
|
2229
|
+
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2230
|
+
}
|
|
2231
|
+
const payload = await verifyJwt(token, ctx.options.secret);
|
|
2232
|
+
if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2233
|
+
return payload;
|
|
2234
|
+
}
|
|
2235
|
+
const id = findSessionId(ctx);
|
|
2236
|
+
if (!id) return;
|
|
2237
|
+
return ctx.options.auth.session.get(id);
|
|
2238
|
+
}
|
|
2070
2239
|
async function getUser(ctx) {
|
|
2071
2240
|
if (!ctx.options.auth) return;
|
|
2072
2241
|
const options = ctx.options.auth;
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
const auth2 = await options.session.get(sessionId);
|
|
2242
|
+
if (options.strategy === "key") return getKeyUser(ctx);
|
|
2243
|
+
const auth2 = await getAuthSession(ctx);
|
|
2076
2244
|
if (!auth2) return;
|
|
2077
2245
|
if (options.strategy !== auth2.strategy) {
|
|
2078
2246
|
throw ServerError_default.AUTH_INVALID_STRATEGY({
|
|
@@ -2095,19 +2263,17 @@ async function getUser(ctx) {
|
|
|
2095
2263
|
|
|
2096
2264
|
// src/auth/logout.ts
|
|
2097
2265
|
async function logout(ctx) {
|
|
2098
|
-
const session2 = findSessionId(ctx);
|
|
2099
2266
|
const { strategy } = ctx.user;
|
|
2100
|
-
await ctx.options.auth.session.del(session2);
|
|
2101
2267
|
if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
|
|
2102
|
-
if (strategy.includes("
|
|
2268
|
+
if (!strategy.includes("jwt")) {
|
|
2269
|
+
await ctx.options.auth.session.del(findSessionId(ctx));
|
|
2270
|
+
}
|
|
2271
|
+
if (strategy.includes("token") || strategy.includes("jwt")) {
|
|
2103
2272
|
return { token: null };
|
|
2104
2273
|
}
|
|
2105
2274
|
if (strategy.includes("cookie")) {
|
|
2106
2275
|
return cookies({ authentication: null }).redirect("/");
|
|
2107
2276
|
}
|
|
2108
|
-
if (strategy.includes("jwt")) {
|
|
2109
|
-
throw new Error("JWT auth not supported yet");
|
|
2110
|
-
}
|
|
2111
2277
|
if (strategy.includes("key")) {
|
|
2112
2278
|
throw new Error("Key auth not supported yet");
|
|
2113
2279
|
}
|
|
@@ -2126,6 +2292,7 @@ function auth(app) {
|
|
|
2126
2292
|
app.use(async function middle(ctx) {
|
|
2127
2293
|
ctx.user = await getUser(ctx);
|
|
2128
2294
|
});
|
|
2295
|
+
if (app.settings.auth.strategy === "key") return;
|
|
2129
2296
|
app.post("/auth/logout", logout);
|
|
2130
2297
|
const enabled = app.settings.auth.providers;
|
|
2131
2298
|
for (const name of oauth2) {
|
|
@@ -2152,21 +2319,78 @@ function auth(app) {
|
|
|
2152
2319
|
}
|
|
2153
2320
|
}
|
|
2154
2321
|
|
|
2322
|
+
// src/helpers/parseRange.ts
|
|
2323
|
+
function parseRange(header, size) {
|
|
2324
|
+
if (!header) return null;
|
|
2325
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
2326
|
+
if (!match) return null;
|
|
2327
|
+
const [, rawStart, rawEnd] = match;
|
|
2328
|
+
if (rawStart === "" && rawEnd === "") return null;
|
|
2329
|
+
let start;
|
|
2330
|
+
let end;
|
|
2331
|
+
if (rawStart === "") {
|
|
2332
|
+
const n = Number(rawEnd);
|
|
2333
|
+
if (n <= 0) return "unsatisfiable";
|
|
2334
|
+
start = Math.max(0, size - n);
|
|
2335
|
+
end = size - 1;
|
|
2336
|
+
} else {
|
|
2337
|
+
start = Number(rawStart);
|
|
2338
|
+
end = rawEnd === "" ? size - 1 : Number(rawEnd);
|
|
2339
|
+
}
|
|
2340
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
2341
|
+
if (size === 0 || start > end || start >= size) return "unsatisfiable";
|
|
2342
|
+
return { start, end: Math.min(end, size - 1) };
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2155
2345
|
// src/middle/assets.ts
|
|
2346
|
+
var CACHE_CONTROL = "public, max-age=3600";
|
|
2156
2347
|
async function assets(ctx) {
|
|
2157
2348
|
if (!ctx.options.public) return;
|
|
2158
2349
|
if (ctx.method !== "get") return;
|
|
2159
2350
|
if (ctx.url.pathname === "/") return;
|
|
2160
2351
|
try {
|
|
2161
|
-
const
|
|
2162
|
-
|
|
2163
|
-
|
|
2352
|
+
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
2353
|
+
const file2 = ctx.options.public.file(key);
|
|
2354
|
+
const meta = file2.info ? await file2.info() : null;
|
|
2355
|
+
if (meta ? !meta.exists : !await file2.exists()) return;
|
|
2356
|
+
const ext2 = ctx.url.pathname.split(".").pop();
|
|
2357
|
+
const ctype = meta?.type || ext2;
|
|
2358
|
+
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2359
|
+
let tag;
|
|
2360
|
+
if (meta) {
|
|
2361
|
+
const stamp = meta.date ? meta.date.getTime() : 0;
|
|
2362
|
+
tag = `W/"${meta.size.toString(16)}-${stamp.toString(16)}"`;
|
|
2363
|
+
headers2.etag = tag;
|
|
2364
|
+
if (meta.date) headers2["last-modified"] = meta.date.toUTCString();
|
|
2365
|
+
}
|
|
2366
|
+
const canRange = !!(meta && file2.slice);
|
|
2367
|
+
if (canRange) headers2["accept-ranges"] = "bytes";
|
|
2368
|
+
if (tag && ctx.headers["if-none-match"] === tag) {
|
|
2369
|
+
return status(304).headers(headers2).send();
|
|
2370
|
+
}
|
|
2371
|
+
const rangeHeader = ctx.headers.range;
|
|
2372
|
+
const ifRange = ctx.headers["if-range"];
|
|
2373
|
+
if (meta && file2.slice && rangeHeader && (!ifRange || ifRange === tag)) {
|
|
2374
|
+
const range = parseRange(rangeHeader, meta.size);
|
|
2375
|
+
if (range === "unsatisfiable") {
|
|
2376
|
+
return status(416).headers({ ...headers2, "content-range": `bytes */${meta.size}` }).send();
|
|
2377
|
+
}
|
|
2378
|
+
if (range) {
|
|
2379
|
+
const { start, end } = range;
|
|
2380
|
+
return type(ctype).status(206).headers({
|
|
2381
|
+
...headers2,
|
|
2382
|
+
"content-range": `bytes ${start}-${end}/${meta.size}`,
|
|
2383
|
+
"content-length": String(end - start + 1)
|
|
2384
|
+
}).send(file2.slice(start, end + 1).stream());
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return type(ctype).headers(headers2).send(file2.stream());
|
|
2164
2388
|
} catch {
|
|
2165
2389
|
}
|
|
2166
2390
|
}
|
|
2167
2391
|
|
|
2168
2392
|
// src/middle/favicon.ts
|
|
2169
|
-
var
|
|
2393
|
+
var CACHE_CONTROL2 = "public, max-age=86400";
|
|
2170
2394
|
var ext = (name) => name.split(".").pop() || "ico";
|
|
2171
2395
|
async function loadFavicon(fav) {
|
|
2172
2396
|
try {
|
|
@@ -2185,7 +2409,7 @@ async function favicon(ctx) {
|
|
|
2185
2409
|
}
|
|
2186
2410
|
const entry = ctx.app.faviconCache;
|
|
2187
2411
|
if (!entry) return 204;
|
|
2188
|
-
const headers2 = { "cache-control":
|
|
2412
|
+
const headers2 = { "cache-control": CACHE_CONTROL2, etag: entry.etag };
|
|
2189
2413
|
if (ctx.headers["if-none-match"] === entry.etag) {
|
|
2190
2414
|
return status(304).headers(headers2).send();
|
|
2191
2415
|
}
|
|
@@ -2409,6 +2633,13 @@ function timer(ctx) {
|
|
|
2409
2633
|
ctx.time = createTime();
|
|
2410
2634
|
}
|
|
2411
2635
|
|
|
2636
|
+
// src/auth/socketUser.ts
|
|
2637
|
+
async function socketUser(app, headers2, cookies2) {
|
|
2638
|
+
if (!app.settings.auth) return void 0;
|
|
2639
|
+
const ctx = { options: app.settings, headers: headers2, cookies: cookies2 };
|
|
2640
|
+
return getUser(ctx);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2412
2643
|
// src/helpers/wsNode.ts
|
|
2413
2644
|
var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
2414
2645
|
var CONTINUATION = 0;
|
|
@@ -2443,6 +2674,9 @@ var NodeWebSocket = class {
|
|
|
2443
2674
|
fragmentOpcode;
|
|
2444
2675
|
closed;
|
|
2445
2676
|
readyState;
|
|
2677
|
+
// The auth user resolved from the upgrade request (see attachWebsocket), or
|
|
2678
|
+
// undefined for an anonymous connection. Read by socket handlers as `ctx.user`.
|
|
2679
|
+
user;
|
|
2446
2680
|
constructor(socket, handlers) {
|
|
2447
2681
|
this.socket = socket;
|
|
2448
2682
|
this.handlers = handlers;
|
|
@@ -2541,13 +2775,24 @@ var NodeWebSocket = class {
|
|
|
2541
2775
|
};
|
|
2542
2776
|
async function attachWebsocket(server2, app) {
|
|
2543
2777
|
const { createHash } = await import("crypto");
|
|
2544
|
-
server2.on("upgrade", (req, socket, head) => {
|
|
2778
|
+
server2.on("upgrade", async (req, socket, head) => {
|
|
2545
2779
|
const key = req.headers["sec-websocket-key"];
|
|
2546
2780
|
const upgrade = String(req.headers.upgrade || "").toLowerCase();
|
|
2547
2781
|
if (upgrade !== "websocket" || !key || !app.handlers.socket.length) {
|
|
2548
2782
|
socket.destroy();
|
|
2549
2783
|
return;
|
|
2550
2784
|
}
|
|
2785
|
+
const cookies2 = parseCookies(req.headers.cookie);
|
|
2786
|
+
let user;
|
|
2787
|
+
try {
|
|
2788
|
+
user = await socketUser(app, req.headers, cookies2);
|
|
2789
|
+
} catch {
|
|
2790
|
+
socket.write(
|
|
2791
|
+
"HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
|
2792
|
+
);
|
|
2793
|
+
socket.destroy();
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2551
2796
|
const accept = createHash("sha1").update(key + GUID).digest("base64");
|
|
2552
2797
|
socket.write(
|
|
2553
2798
|
`HTTP/1.1 101 Switching Protocols\r
|
|
@@ -2563,6 +2808,7 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
2563
2808
|
onMessage: (body) => app.websocket.message(ws, body),
|
|
2564
2809
|
onClose: () => app.websocket.close(ws)
|
|
2565
2810
|
});
|
|
2811
|
+
ws.user = user;
|
|
2566
2812
|
app.websocket.open(ws);
|
|
2567
2813
|
if (head?.length) ws.receive(head);
|
|
2568
2814
|
socket.on("data", (chunk) => ws.receive(chunk));
|
|
@@ -2679,7 +2925,20 @@ async function createWinter(req, app, server2) {
|
|
|
2679
2925
|
|
|
2680
2926
|
// src/context/handlers.ts
|
|
2681
2927
|
var Winter = async (app, request, env2) => {
|
|
2682
|
-
if (env2?.upgrade
|
|
2928
|
+
if (env2?.upgrade) {
|
|
2929
|
+
const wantsWs = String(request.headers.get("upgrade") || "").toLowerCase() === "websocket";
|
|
2930
|
+
if (wantsWs) {
|
|
2931
|
+
const headers2 = parseHeaders_default(request.headers);
|
|
2932
|
+
const cookies2 = parseCookies(headers2.cookie);
|
|
2933
|
+
let user;
|
|
2934
|
+
try {
|
|
2935
|
+
user = await socketUser(app, headers2, cookies2);
|
|
2936
|
+
} catch {
|
|
2937
|
+
return new Response("Unauthorized", { status: 401 });
|
|
2938
|
+
}
|
|
2939
|
+
if (env2.upgrade(request, { data: { user } })) return;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2683
2942
|
Object.assign(globalThis.env, env2);
|
|
2684
2943
|
const ctx = await createWinter(request, app, env2);
|
|
2685
2944
|
const res = await handleRequest(app, ctx);
|
|
@@ -2693,12 +2952,16 @@ var Node = async (app) => {
|
|
|
2693
2952
|
if ("error" in ctx) throw ctx.error;
|
|
2694
2953
|
const out = await handleRequest(app, ctx);
|
|
2695
2954
|
response.writeHead(out.status || 200, parseHeaders_default(out.headers));
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2955
|
+
try {
|
|
2956
|
+
if (out.body instanceof ReadableStream) {
|
|
2957
|
+
await iterate(out.body, (chunk) => response.write(chunk));
|
|
2958
|
+
} else {
|
|
2959
|
+
response.write(out.body || "");
|
|
2960
|
+
}
|
|
2961
|
+
response.end();
|
|
2962
|
+
} catch {
|
|
2963
|
+
if (!response.destroyed) response.destroy();
|
|
2700
2964
|
}
|
|
2701
|
-
response.end();
|
|
2702
2965
|
}
|
|
2703
2966
|
);
|
|
2704
2967
|
await attachWebsocket(server2, app);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@types/bun": "^1.3.0",
|
|
52
52
|
"@types/jest": "^30.0.0",
|
|
53
53
|
"@types/node": "^24.10.0",
|
|
54
|
-
"bucket": "^0.
|
|
54
|
+
"bucket": "^0.4.0",
|
|
55
55
|
"bun": "^1.3.13",
|
|
56
56
|
"check-dts": "^0.8.2",
|
|
57
57
|
"jest": "^29.7.0",
|