@server/next 0.34.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +45 -17
- package/index.js +663 -272
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -130,6 +130,187 @@ function createId(source, size = 16) {
|
|
|
130
130
|
return randomId(size);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// src/helpers/mimes.ts
|
|
134
|
+
var mimes_default = {
|
|
135
|
+
aac: "audio/aac",
|
|
136
|
+
abw: "application/x-abiword",
|
|
137
|
+
arc: "application/x-freearc",
|
|
138
|
+
avif: "image/avif",
|
|
139
|
+
avi: "video/x-msvideo",
|
|
140
|
+
azw: "application/vnd.amazon.ebook",
|
|
141
|
+
bin: "application/octet-stream",
|
|
142
|
+
bmp: "image/bmp",
|
|
143
|
+
bz: "application/x-bzip",
|
|
144
|
+
bz2: "application/x-bzip2",
|
|
145
|
+
cda: "application/x-cdf",
|
|
146
|
+
csh: "application/x-csh",
|
|
147
|
+
css: "text/css",
|
|
148
|
+
csv: "text/csv",
|
|
149
|
+
doc: "application/msword",
|
|
150
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
151
|
+
eot: "application/vnd.ms-fontobject",
|
|
152
|
+
epub: "application/epub+zip",
|
|
153
|
+
gz: "application/gzip",
|
|
154
|
+
gif: "image/gif",
|
|
155
|
+
htm: "text/html",
|
|
156
|
+
html: "text/html",
|
|
157
|
+
ico: "image/vnd.microsoft.icon",
|
|
158
|
+
ics: "text/calendar",
|
|
159
|
+
jar: "application/java-archive",
|
|
160
|
+
jpeg: "image/jpeg",
|
|
161
|
+
jpg: "image/jpeg",
|
|
162
|
+
js: "text/javascript",
|
|
163
|
+
json: "application/json",
|
|
164
|
+
jsonld: "application/ld+json",
|
|
165
|
+
md: "text/markdown",
|
|
166
|
+
mid: "audio/midi",
|
|
167
|
+
midi: "audio/midi",
|
|
168
|
+
mjs: "text/javascript",
|
|
169
|
+
mp3: "audio/mpeg",
|
|
170
|
+
mp4: "video/mp4",
|
|
171
|
+
mpeg: "video/mpeg",
|
|
172
|
+
mpkg: "application/vnd.apple.installer+xml",
|
|
173
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
174
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
175
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
176
|
+
oga: "audio/ogg",
|
|
177
|
+
ogv: "video/ogg",
|
|
178
|
+
ogx: "application/ogg",
|
|
179
|
+
opus: "audio/opus",
|
|
180
|
+
otf: "font/otf",
|
|
181
|
+
png: "image/png",
|
|
182
|
+
pdf: "application/pdf",
|
|
183
|
+
php: "application/x-httpd-php",
|
|
184
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
185
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
186
|
+
rar: "application/vnd.rar",
|
|
187
|
+
rtf: "application/rtf",
|
|
188
|
+
sh: "application/x-sh",
|
|
189
|
+
svg: "image/svg+xml",
|
|
190
|
+
tar: "application/x-tar",
|
|
191
|
+
text: "text/plain",
|
|
192
|
+
tif: "image/tiff",
|
|
193
|
+
tiff: "image/tiff",
|
|
194
|
+
ts: "video/mp2t",
|
|
195
|
+
ttf: "font/ttf",
|
|
196
|
+
txt: "text/plain",
|
|
197
|
+
vsd: "application/vnd.visio",
|
|
198
|
+
wav: "audio/wav",
|
|
199
|
+
weba: "audio/webm",
|
|
200
|
+
webm: "video/webm",
|
|
201
|
+
webp: "image/webp",
|
|
202
|
+
woff: "font/woff",
|
|
203
|
+
woff2: "font/woff2",
|
|
204
|
+
xhtml: "application/xhtml+xml",
|
|
205
|
+
xls: "application/vnd.ms-excel",
|
|
206
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
207
|
+
xml: "application/xml",
|
|
208
|
+
xul: "application/vnd.mozilla.xul+xml",
|
|
209
|
+
zip: "application/zip",
|
|
210
|
+
"3gp": "video/3gpp",
|
|
211
|
+
"3g2": "video/3gpp2",
|
|
212
|
+
"7z": "application/x-7z-compressed"
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/helpers/bucket.ts
|
|
216
|
+
import * as fs from "fs";
|
|
217
|
+
import * as fsp from "fs/promises";
|
|
218
|
+
import * as path from "path";
|
|
219
|
+
function localBucket(root) {
|
|
220
|
+
const base = path.resolve(root);
|
|
221
|
+
const resolveKey = (name) => {
|
|
222
|
+
if (!name) throw new Error("File name is required");
|
|
223
|
+
const full = path.resolve(base, name.replace(/^\/+/, ""));
|
|
224
|
+
if (full !== base && !full.startsWith(base + path.sep)) {
|
|
225
|
+
throw new Error(`Path "${name}" escapes the bucket root`);
|
|
226
|
+
}
|
|
227
|
+
return full;
|
|
228
|
+
};
|
|
229
|
+
const file2 = (name, win) => {
|
|
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
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
path: full,
|
|
251
|
+
id: name.replace(/^\/+/, ""),
|
|
252
|
+
name: path.basename(name),
|
|
253
|
+
async exists() {
|
|
254
|
+
const stats = await fsp.stat(full).catch(() => null);
|
|
255
|
+
return !!stats?.isFile();
|
|
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
|
+
},
|
|
272
|
+
async write(content) {
|
|
273
|
+
await fsp.mkdir(path.dirname(full), { recursive: true });
|
|
274
|
+
if (content instanceof ReadableStream) {
|
|
275
|
+
const writable = fs.createWriteStream(full);
|
|
276
|
+
for await (const chunk of content) {
|
|
277
|
+
writable.write(chunk);
|
|
278
|
+
}
|
|
279
|
+
await new Promise((resolve2, reject) => {
|
|
280
|
+
writable.on("error", reject);
|
|
281
|
+
writable.end(() => resolve2());
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await fsp.writeFile(full, content);
|
|
286
|
+
},
|
|
287
|
+
stream() {
|
|
288
|
+
return read();
|
|
289
|
+
},
|
|
290
|
+
async bytes() {
|
|
291
|
+
if (win) return new Uint8Array(await new Response(read()).arrayBuffer());
|
|
292
|
+
return new Uint8Array(await fsp.readFile(full));
|
|
293
|
+
},
|
|
294
|
+
async remove() {
|
|
295
|
+
await fsp.unlink(full).catch(() => {
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
return {
|
|
301
|
+
file: file2,
|
|
302
|
+
folder: (prefix) => localBucket(path.join(base, prefix))
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function bucket(root) {
|
|
306
|
+
if (!root) return null;
|
|
307
|
+
if (typeof root === "string") return localBucket(root);
|
|
308
|
+
if (typeof root.file === "function") return root;
|
|
309
|
+
throw new Error(
|
|
310
|
+
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
133
314
|
// src/helpers/upload.ts
|
|
134
315
|
function parseBytes(value) {
|
|
135
316
|
if (typeof value === "number") return value;
|
|
@@ -148,24 +329,31 @@ function getExt(filename) {
|
|
|
148
329
|
if (i <= 0) return ".bin";
|
|
149
330
|
return filename.slice(i).toLowerCase();
|
|
150
331
|
}
|
|
151
|
-
async function saveFileToBucket(originalName, data,
|
|
152
|
-
const
|
|
153
|
-
const id = `${createId()}${
|
|
154
|
-
const
|
|
155
|
-
|
|
332
|
+
async function saveFileToBucket(originalName, data, bucket2, contentType) {
|
|
333
|
+
const ext2 = getExt(originalName);
|
|
334
|
+
const id = `${createId()}${ext2}`;
|
|
335
|
+
const file2 = bucket2.file(id);
|
|
336
|
+
await file2.write(data, { type: contentType });
|
|
337
|
+
return {
|
|
338
|
+
name: originalName,
|
|
339
|
+
id,
|
|
340
|
+
path: file2.path,
|
|
341
|
+
type: contentType,
|
|
342
|
+
size: data.length
|
|
343
|
+
};
|
|
156
344
|
}
|
|
157
345
|
var UploadPipeline = class {
|
|
158
346
|
_bucket;
|
|
159
347
|
_limits = {};
|
|
160
|
-
constructor(
|
|
161
|
-
this._bucket = bucket ??
|
|
348
|
+
constructor(bucket2) {
|
|
349
|
+
this._bucket = bucket(bucket2 ?? void 0);
|
|
162
350
|
}
|
|
163
351
|
limit(options) {
|
|
164
352
|
this._limits = { ...this._limits, ...options };
|
|
165
353
|
return this;
|
|
166
354
|
}
|
|
167
|
-
store(
|
|
168
|
-
this._bucket = bucket;
|
|
355
|
+
store(bucket2) {
|
|
356
|
+
this._bucket = bucket(bucket2);
|
|
169
357
|
return this;
|
|
170
358
|
}
|
|
171
359
|
async processFile(originalName, data, contentType) {
|
|
@@ -181,10 +369,10 @@ var UploadPipeline = class {
|
|
|
181
369
|
);
|
|
182
370
|
}
|
|
183
371
|
if (fileType && fileType.length > 0) {
|
|
184
|
-
const
|
|
372
|
+
const ext2 = getExt(originalName);
|
|
185
373
|
const mime = contentType.toLowerCase();
|
|
186
374
|
const allowed = fileType.some(
|
|
187
|
-
(t) => t.toLowerCase() === mime || t.toLowerCase() ===
|
|
375
|
+
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
|
|
188
376
|
);
|
|
189
377
|
if (!allowed) {
|
|
190
378
|
throw new Error(
|
|
@@ -200,8 +388,8 @@ var UploadPipeline = class {
|
|
|
200
388
|
return saveFileToBucket(originalName, data, this._bucket, contentType);
|
|
201
389
|
}
|
|
202
390
|
};
|
|
203
|
-
function upload(
|
|
204
|
-
return new UploadPipeline(
|
|
391
|
+
function upload(bucket2) {
|
|
392
|
+
return new UploadPipeline(bucket2);
|
|
205
393
|
}
|
|
206
394
|
|
|
207
395
|
// src/helpers/parseBody.ts
|
|
@@ -231,24 +419,12 @@ function isProbablyText(buffer) {
|
|
|
231
419
|
}
|
|
232
420
|
return true;
|
|
233
421
|
}
|
|
234
|
-
var
|
|
235
|
-
|
|
236
|
-
"application/pdf": ".pdf",
|
|
237
|
-
"application/zip": ".zip",
|
|
238
|
-
"text/plain": ".txt",
|
|
239
|
-
"text/html": ".html",
|
|
240
|
-
"text/csv": ".csv",
|
|
241
|
-
"image/jpeg": ".jpg",
|
|
242
|
-
"image/png": ".png",
|
|
243
|
-
"image/gif": ".gif",
|
|
244
|
-
"image/webp": ".webp",
|
|
245
|
-
"image/svg+xml": ".svg",
|
|
246
|
-
"video/mp4": ".mp4",
|
|
247
|
-
"audio/mpeg": ".mp3"
|
|
248
|
-
};
|
|
422
|
+
var extByMime = {};
|
|
423
|
+
for (const ext2 in mimes_default) extByMime[mimes_default[ext2]] = ext2;
|
|
249
424
|
function extFromType(type2) {
|
|
250
425
|
const base = (type2 || "").split(";")[0].trim().toLowerCase();
|
|
251
|
-
|
|
426
|
+
const ext2 = extByMime[base];
|
|
427
|
+
if (ext2) return `.${ext2}`;
|
|
252
428
|
const sub = base.split("/")[1];
|
|
253
429
|
return sub && /^[a-z0-9]+$/.test(sub) ? `.${sub}` : ".bin";
|
|
254
430
|
}
|
|
@@ -303,6 +479,7 @@ function startPart(headerStr, dest) {
|
|
|
303
479
|
controller = c;
|
|
304
480
|
}
|
|
305
481
|
});
|
|
482
|
+
const file2 = dest.file(id);
|
|
306
483
|
return {
|
|
307
484
|
kind: "file",
|
|
308
485
|
name,
|
|
@@ -310,7 +487,8 @@ function startPart(headerStr, dest) {
|
|
|
310
487
|
type: type2,
|
|
311
488
|
id,
|
|
312
489
|
controller,
|
|
313
|
-
|
|
490
|
+
file: file2,
|
|
491
|
+
write: file2.write(readable, { type: type2 }),
|
|
314
492
|
size: 0
|
|
315
493
|
};
|
|
316
494
|
}
|
|
@@ -333,11 +511,11 @@ async function endPart(part, body) {
|
|
|
333
511
|
addField(body, part.name, ref);
|
|
334
512
|
} else if (part.kind === "file") {
|
|
335
513
|
part.controller.close();
|
|
336
|
-
|
|
514
|
+
await part.write;
|
|
337
515
|
addField(body, part.name, {
|
|
338
516
|
name: part.filename,
|
|
339
517
|
id: part.id,
|
|
340
|
-
path:
|
|
518
|
+
path: part.file.path,
|
|
341
519
|
type: part.type,
|
|
342
520
|
size: part.size
|
|
343
521
|
});
|
|
@@ -401,8 +579,9 @@ async function parseMultipart(stream, boundary, dest) {
|
|
|
401
579
|
if (part) await endPart(part, body);
|
|
402
580
|
return body;
|
|
403
581
|
}
|
|
404
|
-
async function streamToBucket(stream, type2,
|
|
582
|
+
async function streamToBucket(stream, type2, bucket2) {
|
|
405
583
|
const id = `${createId()}${extFromType(type2)}`;
|
|
584
|
+
const file2 = bucket2.file(id);
|
|
406
585
|
let size = 0;
|
|
407
586
|
let controller;
|
|
408
587
|
const readable = new ReadableStream({
|
|
@@ -410,15 +589,15 @@ async function streamToBucket(stream, type2, bucket) {
|
|
|
410
589
|
controller = c;
|
|
411
590
|
}
|
|
412
591
|
});
|
|
413
|
-
const write =
|
|
592
|
+
const write = file2.write(readable, { type: type2 });
|
|
414
593
|
for await (const chunk of asIterable(stream)) {
|
|
415
594
|
controller.enqueue(chunk);
|
|
416
595
|
size += chunk.byteLength;
|
|
417
596
|
}
|
|
418
597
|
controller.close();
|
|
419
|
-
|
|
598
|
+
await write;
|
|
420
599
|
if (!size) return void 0;
|
|
421
|
-
return { name: id, id, path:
|
|
600
|
+
return { name: id, id, path: file2.path, type: type2, size };
|
|
422
601
|
}
|
|
423
602
|
async function parseBody(input, contentType, dest) {
|
|
424
603
|
const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
@@ -447,17 +626,34 @@ async function parseBody(input, contentType, dest) {
|
|
|
447
626
|
return streamToBucket(toStream(input), type2, dest);
|
|
448
627
|
}
|
|
449
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
|
+
|
|
450
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);
|
|
451
642
|
var sources = /* @__PURE__ */ new WeakMap();
|
|
452
643
|
function setBodySource(ctx, source) {
|
|
453
644
|
sources.set(ctx, source);
|
|
454
645
|
}
|
|
455
|
-
async function resolveBody(ctx,
|
|
646
|
+
async function resolveBody(ctx, body) {
|
|
456
647
|
const source = sources.get(ctx);
|
|
457
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);
|
|
458
653
|
if (mode === "stream") return source.getStream();
|
|
459
654
|
if (mode === "raw") {
|
|
460
655
|
const raw = await source.getBuffer();
|
|
656
|
+
if (raw.length > max) throw tooLarge(max);
|
|
461
657
|
if (!raw.length) return void 0;
|
|
462
658
|
if (!ctx.headers["content-length"]) {
|
|
463
659
|
ctx.headers["content-length"] = String(raw.length);
|
|
@@ -471,11 +667,12 @@ async function resolveBody(ctx, mode) {
|
|
|
471
667
|
new TransformStream({
|
|
472
668
|
transform(chunk, controller) {
|
|
473
669
|
size += chunk.byteLength;
|
|
670
|
+
if (size > max) return controller.error(tooLarge(max));
|
|
474
671
|
controller.enqueue(chunk);
|
|
475
672
|
}
|
|
476
673
|
})
|
|
477
674
|
);
|
|
478
|
-
const
|
|
675
|
+
const parsed = await parseBody(
|
|
479
676
|
counted,
|
|
480
677
|
ctx.headers["content-type"],
|
|
481
678
|
ctx.options.uploads
|
|
@@ -483,7 +680,7 @@ async function resolveBody(ctx, mode) {
|
|
|
483
680
|
if (size && !ctx.headers["content-length"]) {
|
|
484
681
|
ctx.headers["content-length"] = String(size);
|
|
485
682
|
}
|
|
486
|
-
return
|
|
683
|
+
return parsed;
|
|
487
684
|
}
|
|
488
685
|
|
|
489
686
|
// src/helpers/clientIp.ts
|
|
@@ -524,13 +721,13 @@ var Reply = class {
|
|
|
524
721
|
}
|
|
525
722
|
type(type2) {
|
|
526
723
|
if (!type2) return this;
|
|
527
|
-
type2 =
|
|
724
|
+
type2 = mimes_default[type2.replace(/^\./, "")] || type2;
|
|
528
725
|
this.res.headers.set("content-type", type2);
|
|
529
726
|
return this;
|
|
530
727
|
}
|
|
531
728
|
download(name) {
|
|
532
|
-
const
|
|
533
|
-
if (
|
|
729
|
+
const ext2 = name?.split(".").pop();
|
|
730
|
+
if (ext2 && !this.res.headers.get("content-type")) this.type(ext2);
|
|
534
731
|
const filename = name ? `; filename="${encodeURIComponent(name)}"` : "";
|
|
535
732
|
return this.headers("content-disposition", `attachment${filename}`);
|
|
536
733
|
}
|
|
@@ -570,9 +767,9 @@ var Reply = class {
|
|
|
570
767
|
async file(path2) {
|
|
571
768
|
try {
|
|
572
769
|
const fs2 = await import("fs");
|
|
573
|
-
const
|
|
770
|
+
const ext2 = path2.split(".").pop();
|
|
574
771
|
const stream = fs2.createReadStream(path2);
|
|
575
|
-
return this.type(
|
|
772
|
+
return this.type(ext2).send(stream);
|
|
576
773
|
} catch (error) {
|
|
577
774
|
if (error.code === "ENOENT") {
|
|
578
775
|
return this.status(404).send();
|
|
@@ -582,6 +779,9 @@ var Reply = class {
|
|
|
582
779
|
}
|
|
583
780
|
send(body = "") {
|
|
584
781
|
const { status: status2 = 200, headers: headers2 } = this.res;
|
|
782
|
+
if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
|
|
783
|
+
return new Response(null, { status: status2, headers: headers2 });
|
|
784
|
+
}
|
|
585
785
|
if (typeof body === "string") {
|
|
586
786
|
if (!headers2.get("content-type")) {
|
|
587
787
|
const isHtml = body.trim().startsWith("<");
|
|
@@ -629,6 +829,73 @@ var json = (...args) => r().json(...args);
|
|
|
629
829
|
var file = (...args) => r().file(...args);
|
|
630
830
|
var redirect = (...args) => r().redirect(...args);
|
|
631
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
|
+
|
|
632
899
|
// src/auth/finishLogin.ts
|
|
633
900
|
async function finishLogin(ctx, input) {
|
|
634
901
|
const settings = ctx.options.auth;
|
|
@@ -649,7 +916,13 @@ async function finishLogin(ctx, input) {
|
|
|
649
916
|
}
|
|
650
917
|
user = await cleanUser(user);
|
|
651
918
|
if (input.store !== false) await settings.store.set(key, user);
|
|
652
|
-
|
|
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
|
+
}
|
|
653
926
|
if (strategy.includes("token")) {
|
|
654
927
|
return status(201).json({ ...user, token: auth2.id });
|
|
655
928
|
}
|
|
@@ -662,7 +935,6 @@ async function finishLogin(ctx, input) {
|
|
|
662
935
|
sameSite: "Lax"
|
|
663
936
|
}).redirect(settings.redirect);
|
|
664
937
|
}
|
|
665
|
-
if (strategy.includes("jwt")) throw new Error("JWT auth not supported yet");
|
|
666
938
|
if (strategy.includes("key")) throw new Error("Key auth not supported yet");
|
|
667
939
|
throw new Error("Unknown auth type");
|
|
668
940
|
}
|
|
@@ -751,7 +1023,7 @@ function clearState() {
|
|
|
751
1023
|
// src/auth/providers/apple.ts
|
|
752
1024
|
var AUTHORIZE = "https://appleid.apple.com/auth/authorize";
|
|
753
1025
|
var TOKEN = "https://appleid.apple.com/auth/token";
|
|
754
|
-
var
|
|
1026
|
+
var b64url2 = (data) => {
|
|
755
1027
|
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
756
1028
|
let bin = "";
|
|
757
1029
|
for (const byte of bytes) bin += String.fromCharCode(byte);
|
|
@@ -773,7 +1045,7 @@ var clientSecret = async () => {
|
|
|
773
1045
|
aud: "https://appleid.apple.com",
|
|
774
1046
|
sub: env.APPLE_ID
|
|
775
1047
|
};
|
|
776
|
-
const data = `${
|
|
1048
|
+
const data = `${b64url2(JSON.stringify(header))}.${b64url2(JSON.stringify(payload))}`;
|
|
777
1049
|
const pem = String(env.APPLE_PRIVATE_KEY).replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
|
778
1050
|
const der = Uint8Array.from(atob(pem), (c) => c.charCodeAt(0));
|
|
779
1051
|
const key = await crypto.subtle.importKey(
|
|
@@ -788,7 +1060,7 @@ var clientSecret = async () => {
|
|
|
788
1060
|
key,
|
|
789
1061
|
new TextEncoder().encode(data)
|
|
790
1062
|
);
|
|
791
|
-
return `${data}.${
|
|
1063
|
+
return `${data}.${b64url2(new Uint8Array(sig))}`;
|
|
792
1064
|
};
|
|
793
1065
|
var login = (ctx) => {
|
|
794
1066
|
const { state, cookie } = startState(ctx, true);
|
|
@@ -1124,6 +1396,19 @@ function parseAuthOptions(auth2, all) {
|
|
|
1124
1396
|
throw new Error("Auth options needs a strategy");
|
|
1125
1397
|
}
|
|
1126
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
|
+
}
|
|
1127
1412
|
const list = Array.isArray(auth2.providers) ? auth2.providers : auth2.providers ? [auth2.providers] : [];
|
|
1128
1413
|
if (!list.length) {
|
|
1129
1414
|
throw new Error("Auth options needs a provider");
|
|
@@ -1154,101 +1439,6 @@ function parseAuthOptions(auth2, all) {
|
|
|
1154
1439
|
};
|
|
1155
1440
|
}
|
|
1156
1441
|
|
|
1157
|
-
// src/helpers/bucket.ts
|
|
1158
|
-
import * as fs from "fs";
|
|
1159
|
-
import * as fsp from "fs/promises";
|
|
1160
|
-
import * as path from "path";
|
|
1161
|
-
function thinLocalBucket(root) {
|
|
1162
|
-
const absolute = (name) => {
|
|
1163
|
-
if (!name) throw new Error("File name is required");
|
|
1164
|
-
return path.resolve(path.join(root, name));
|
|
1165
|
-
};
|
|
1166
|
-
return {
|
|
1167
|
-
location: path.resolve(root),
|
|
1168
|
-
read: async (name) => {
|
|
1169
|
-
const fullPath = absolute(name);
|
|
1170
|
-
const stats = await fsp.stat(fullPath).catch(() => null);
|
|
1171
|
-
if (!stats?.isFile()) return null;
|
|
1172
|
-
const nodeStream = fs.createReadStream(fullPath);
|
|
1173
|
-
return new ReadableStream({
|
|
1174
|
-
start(controller) {
|
|
1175
|
-
nodeStream.on("data", (chunk) => controller.enqueue(chunk));
|
|
1176
|
-
nodeStream.on("end", () => controller.close());
|
|
1177
|
-
nodeStream.on("error", (err) => controller.error(err));
|
|
1178
|
-
},
|
|
1179
|
-
cancel() {
|
|
1180
|
-
nodeStream.destroy();
|
|
1181
|
-
}
|
|
1182
|
-
});
|
|
1183
|
-
},
|
|
1184
|
-
write: async (name, value, type2) => {
|
|
1185
|
-
const fullPath = absolute(name);
|
|
1186
|
-
if (!value) return fs.createWriteStream(fullPath);
|
|
1187
|
-
await fsp.mkdir(path.dirname(fullPath), { recursive: true });
|
|
1188
|
-
if (value instanceof ReadableStream) {
|
|
1189
|
-
const writable = fs.createWriteStream(fullPath);
|
|
1190
|
-
for await (const chunk of value) {
|
|
1191
|
-
writable.write(chunk);
|
|
1192
|
-
}
|
|
1193
|
-
await new Promise((resolve2, reject) => {
|
|
1194
|
-
writable.on("error", reject);
|
|
1195
|
-
writable.end(resolve2);
|
|
1196
|
-
});
|
|
1197
|
-
return fullPath;
|
|
1198
|
-
}
|
|
1199
|
-
await fsp.writeFile(fullPath, value, type2);
|
|
1200
|
-
return fullPath;
|
|
1201
|
-
},
|
|
1202
|
-
delete: async (name) => {
|
|
1203
|
-
const fullPath = absolute(name);
|
|
1204
|
-
try {
|
|
1205
|
-
await fsp.unlink(fullPath);
|
|
1206
|
-
return true;
|
|
1207
|
-
} catch {
|
|
1208
|
-
return false;
|
|
1209
|
-
}
|
|
1210
|
-
},
|
|
1211
|
-
folder: (prefix) => thinLocalBucket(path.join(root, prefix))
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
function thinBunBucket(s3, prefix = "") {
|
|
1215
|
-
const key = (name) => prefix ? `${prefix}/${name}` : name;
|
|
1216
|
-
return {
|
|
1217
|
-
read: async (name) => {
|
|
1218
|
-
const file2 = s3.file(key(name));
|
|
1219
|
-
if (!await file2.exists()) return null;
|
|
1220
|
-
return await file2.stream();
|
|
1221
|
-
},
|
|
1222
|
-
write: async (name, value) => {
|
|
1223
|
-
const file2 = s3.file(key(name));
|
|
1224
|
-
if (value) {
|
|
1225
|
-
await file2.write(value);
|
|
1226
|
-
return key(name);
|
|
1227
|
-
}
|
|
1228
|
-
return s3.presign(key(name), {
|
|
1229
|
-
expiresIn: 3600,
|
|
1230
|
-
acl: "public-read-write"
|
|
1231
|
-
});
|
|
1232
|
-
},
|
|
1233
|
-
delete: async (name) => {
|
|
1234
|
-
const file2 = s3.file(key(name));
|
|
1235
|
-
if (!await file2.exists()) return null;
|
|
1236
|
-
return await file2.delete();
|
|
1237
|
-
},
|
|
1238
|
-
folder: (sub) => thinBunBucket(s3, key(sub))
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
function bucket_default(root) {
|
|
1242
|
-
if (!root) return null;
|
|
1243
|
-
if (typeof root === "string") {
|
|
1244
|
-
return thinLocalBucket(root);
|
|
1245
|
-
}
|
|
1246
|
-
if (root.file && root.write) {
|
|
1247
|
-
return thinBunBucket(root);
|
|
1248
|
-
}
|
|
1249
|
-
return root;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
1442
|
// src/helpers/color.ts
|
|
1253
1443
|
var map = {
|
|
1254
1444
|
reset: 0,
|
|
@@ -1452,8 +1642,8 @@ function config(options = {}) {
|
|
|
1452
1642
|
}
|
|
1453
1643
|
settings.cors = cors2;
|
|
1454
1644
|
}
|
|
1455
|
-
settings.public = options.public ?
|
|
1456
|
-
settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ?
|
|
1645
|
+
settings.public = options.public ? bucket(options.public) : null;
|
|
1646
|
+
settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket(options.uploads) : null;
|
|
1457
1647
|
if (options.favicon) settings.favicon = options.favicon;
|
|
1458
1648
|
settings.store = options.store ?? null;
|
|
1459
1649
|
settings.cookies = options.cookies ?? null;
|
|
@@ -1466,6 +1656,11 @@ function config(options = {}) {
|
|
|
1466
1656
|
if (options.auth || env2.AUTH) {
|
|
1467
1657
|
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null, options);
|
|
1468
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
|
+
}
|
|
1469
1664
|
if (options.openapi) {
|
|
1470
1665
|
if (options.openapi === true) {
|
|
1471
1666
|
settings.openapi = {};
|
|
@@ -1527,6 +1722,16 @@ function applyCors(res, ctx) {
|
|
|
1527
1722
|
}
|
|
1528
1723
|
}
|
|
1529
1724
|
|
|
1725
|
+
// src/helpers/etag.ts
|
|
1726
|
+
function etag(bytes) {
|
|
1727
|
+
let h = 2166136261;
|
|
1728
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1729
|
+
h ^= bytes[i];
|
|
1730
|
+
h = Math.imul(h, 16777619);
|
|
1731
|
+
}
|
|
1732
|
+
return `"${bytes.length.toString(16)}-${(h >>> 0).toString(16)}"`;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1530
1735
|
// src/helpers/createWebsocket.ts
|
|
1531
1736
|
function createWebsocket(sockets, handlers) {
|
|
1532
1737
|
const run = (event, socket, body) => {
|
|
@@ -1647,13 +1852,20 @@ async function parseResponse(out, ctx) {
|
|
|
1647
1852
|
if (!ctx.options.session?.store) {
|
|
1648
1853
|
throw ServerError_default.NO_STORE();
|
|
1649
1854
|
}
|
|
1650
|
-
|
|
1855
|
+
let id = ctx.cookies.session;
|
|
1856
|
+
if (!id) {
|
|
1857
|
+
id = createId();
|
|
1651
1858
|
out.headers.append(
|
|
1652
1859
|
"set-cookie",
|
|
1653
|
-
createCookies("session", {
|
|
1860
|
+
createCookies("session", {
|
|
1861
|
+
value: id,
|
|
1862
|
+
path: "/",
|
|
1863
|
+
httpOnly: true,
|
|
1864
|
+
secure: ctx.platform.production,
|
|
1865
|
+
sameSite: "Lax"
|
|
1866
|
+
})
|
|
1654
1867
|
);
|
|
1655
1868
|
}
|
|
1656
|
-
const id = ctx.cookies.session;
|
|
1657
1869
|
ctx.options.session.store.set(id, ctx.session);
|
|
1658
1870
|
}
|
|
1659
1871
|
if (ctx.options.cookies) {
|
|
@@ -1714,15 +1926,6 @@ function pathPattern(pattern, path2) {
|
|
|
1714
1926
|
return null;
|
|
1715
1927
|
}
|
|
1716
1928
|
|
|
1717
|
-
// src/helpers/StatusError.ts
|
|
1718
|
-
var StatusError = class extends Error {
|
|
1719
|
-
status;
|
|
1720
|
-
constructor(msg, status2 = 500) {
|
|
1721
|
-
super(msg);
|
|
1722
|
-
this.status = status2;
|
|
1723
|
-
}
|
|
1724
|
-
};
|
|
1725
|
-
|
|
1726
1929
|
// src/helpers/validate.ts
|
|
1727
1930
|
function validate(ctx, schema) {
|
|
1728
1931
|
if (!schema || typeof schema !== "object") return;
|
|
@@ -1916,89 +2119,6 @@ function toWeb(nodeStream) {
|
|
|
1916
2119
|
});
|
|
1917
2120
|
}
|
|
1918
2121
|
|
|
1919
|
-
// src/helpers/types.ts
|
|
1920
|
-
var types = {
|
|
1921
|
-
aac: "audio/aac",
|
|
1922
|
-
abw: "application/x-abiword",
|
|
1923
|
-
arc: "application/x-freearc",
|
|
1924
|
-
avif: "image/avif",
|
|
1925
|
-
avi: "video/x-msvideo",
|
|
1926
|
-
azw: "application/vnd.amazon.ebook",
|
|
1927
|
-
bin: "application/octet-stream",
|
|
1928
|
-
bmp: "image/bmp",
|
|
1929
|
-
bz: "application/x-bzip",
|
|
1930
|
-
bz2: "application/x-bzip2",
|
|
1931
|
-
cda: "application/x-cdf",
|
|
1932
|
-
csh: "application/x-csh",
|
|
1933
|
-
css: "text/css",
|
|
1934
|
-
csv: "text/csv",
|
|
1935
|
-
doc: "application/msword",
|
|
1936
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1937
|
-
eot: "application/vnd.ms-fontobject",
|
|
1938
|
-
epub: "application/epub+zip",
|
|
1939
|
-
gz: "application/gzip",
|
|
1940
|
-
gif: "image/gif",
|
|
1941
|
-
htm: "text/html",
|
|
1942
|
-
html: "text/html",
|
|
1943
|
-
ico: "image/vnd.microsoft.icon",
|
|
1944
|
-
ics: "text/calendar",
|
|
1945
|
-
jar: "application/java-archive",
|
|
1946
|
-
jpeg: "image/jpeg",
|
|
1947
|
-
jpg: "image/jpeg",
|
|
1948
|
-
js: "text/javascript",
|
|
1949
|
-
json: "application/json",
|
|
1950
|
-
jsonld: "application/ld+json",
|
|
1951
|
-
md: "text/markdown",
|
|
1952
|
-
mid: "audio/midi",
|
|
1953
|
-
midi: "audio/midi",
|
|
1954
|
-
mjs: "text/javascript",
|
|
1955
|
-
mp3: "audio/mpeg",
|
|
1956
|
-
mp4: "video/mp4",
|
|
1957
|
-
mpeg: "video/mpeg",
|
|
1958
|
-
mpkg: "application/vnd.apple.installer+xml",
|
|
1959
|
-
odp: "application/vnd.oasis.opendocument.presentation",
|
|
1960
|
-
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
1961
|
-
odt: "application/vnd.oasis.opendocument.text",
|
|
1962
|
-
oga: "audio/ogg",
|
|
1963
|
-
ogv: "video/ogg",
|
|
1964
|
-
ogx: "application/ogg",
|
|
1965
|
-
opus: "audio/opus",
|
|
1966
|
-
otf: "font/otf",
|
|
1967
|
-
png: "image/png",
|
|
1968
|
-
pdf: "application/pdf",
|
|
1969
|
-
php: "application/x-httpd-php",
|
|
1970
|
-
ppt: "application/vnd.ms-powerpoint",
|
|
1971
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1972
|
-
rar: "application/vnd.rar",
|
|
1973
|
-
rtf: "application/rtf",
|
|
1974
|
-
sh: "application/x-sh",
|
|
1975
|
-
svg: "image/svg+xml",
|
|
1976
|
-
tar: "application/x-tar",
|
|
1977
|
-
text: "text/plain",
|
|
1978
|
-
tif: "image/tiff",
|
|
1979
|
-
tiff: "image/tiff",
|
|
1980
|
-
ts: "video/mp2t",
|
|
1981
|
-
ttf: "font/ttf",
|
|
1982
|
-
txt: "text/plain",
|
|
1983
|
-
vsd: "application/vnd.visio",
|
|
1984
|
-
wav: "audio/wav",
|
|
1985
|
-
weba: "audio/webm",
|
|
1986
|
-
webm: "video/webm",
|
|
1987
|
-
webp: "image/webp",
|
|
1988
|
-
woff: "font/woff",
|
|
1989
|
-
woff2: "font/woff2",
|
|
1990
|
-
xhtml: "application/xhtml+xml",
|
|
1991
|
-
xls: "application/vnd.ms-excel",
|
|
1992
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1993
|
-
xml: "application/xml",
|
|
1994
|
-
xul: "application/vnd.mozilla.xul+xml",
|
|
1995
|
-
zip: "application/zip",
|
|
1996
|
-
"3gp": "video/3gpp",
|
|
1997
|
-
"3g2": "video/3gpp2",
|
|
1998
|
-
"7z": "application/x-7z-compressed"
|
|
1999
|
-
};
|
|
2000
|
-
var types_default = types;
|
|
2001
|
-
|
|
2002
2122
|
// src/helpers/verify.ts
|
|
2003
2123
|
import * as crypto3 from "crypto";
|
|
2004
2124
|
function timingSafeEqual(a, b) {
|
|
@@ -2045,6 +2165,14 @@ async function verify(password, hash3) {
|
|
|
2045
2165
|
});
|
|
2046
2166
|
}
|
|
2047
2167
|
|
|
2168
|
+
// src/helpers/safeEqual.ts
|
|
2169
|
+
function safeEqual(a, b) {
|
|
2170
|
+
if (a.length !== b.length) return false;
|
|
2171
|
+
let diff = 0;
|
|
2172
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2173
|
+
return diff === 0;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2048
2176
|
// src/auth/findSessionId.ts
|
|
2049
2177
|
var validateToken = (authorization) => {
|
|
2050
2178
|
const [type2, id] = authorization.trim().split(" ");
|
|
@@ -2077,12 +2205,41 @@ function findSessionId(ctx) {
|
|
|
2077
2205
|
}
|
|
2078
2206
|
|
|
2079
2207
|
// src/auth/getUser.ts
|
|
2208
|
+
function getKeyUser(ctx) {
|
|
2209
|
+
const expected = ctx.options.auth.key;
|
|
2210
|
+
const header = ctx.headers.authorization;
|
|
2211
|
+
if (!header) return;
|
|
2212
|
+
const [type2, provided] = header.trim().split(" ");
|
|
2213
|
+
if (type2?.toLowerCase() !== "bearer" || !provided) {
|
|
2214
|
+
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2215
|
+
}
|
|
2216
|
+
if (!expected || !safeEqual(provided, expected)) {
|
|
2217
|
+
throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2218
|
+
}
|
|
2219
|
+
return { id: "key", strategy: "key", provider: "key" };
|
|
2220
|
+
}
|
|
2221
|
+
async function getAuthSession(ctx) {
|
|
2222
|
+
const strategy = ctx.options.auth.strategy;
|
|
2223
|
+
if (strategy.includes("jwt")) {
|
|
2224
|
+
const header = ctx.headers.authorization;
|
|
2225
|
+
if (!header) return;
|
|
2226
|
+
const [type2, token] = header.trim().split(" ");
|
|
2227
|
+
if (type2?.toLowerCase() !== "bearer" || !token) {
|
|
2228
|
+
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2229
|
+
}
|
|
2230
|
+
const payload = await verifyJwt(token, ctx.options.secret);
|
|
2231
|
+
if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2232
|
+
return payload;
|
|
2233
|
+
}
|
|
2234
|
+
const id = findSessionId(ctx);
|
|
2235
|
+
if (!id) return;
|
|
2236
|
+
return ctx.options.auth.session.get(id);
|
|
2237
|
+
}
|
|
2080
2238
|
async function getUser(ctx) {
|
|
2081
2239
|
if (!ctx.options.auth) return;
|
|
2082
2240
|
const options = ctx.options.auth;
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const auth2 = await options.session.get(sessionId);
|
|
2241
|
+
if (options.strategy === "key") return getKeyUser(ctx);
|
|
2242
|
+
const auth2 = await getAuthSession(ctx);
|
|
2086
2243
|
if (!auth2) return;
|
|
2087
2244
|
if (options.strategy !== auth2.strategy) {
|
|
2088
2245
|
throw ServerError_default.AUTH_INVALID_STRATEGY({
|
|
@@ -2105,19 +2262,17 @@ async function getUser(ctx) {
|
|
|
2105
2262
|
|
|
2106
2263
|
// src/auth/logout.ts
|
|
2107
2264
|
async function logout(ctx) {
|
|
2108
|
-
const session2 = findSessionId(ctx);
|
|
2109
2265
|
const { strategy } = ctx.user;
|
|
2110
|
-
await ctx.options.auth.session.del(session2);
|
|
2111
2266
|
if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
|
|
2112
|
-
if (strategy.includes("
|
|
2267
|
+
if (!strategy.includes("jwt")) {
|
|
2268
|
+
await ctx.options.auth.session.del(findSessionId(ctx));
|
|
2269
|
+
}
|
|
2270
|
+
if (strategy.includes("token") || strategy.includes("jwt")) {
|
|
2113
2271
|
return { token: null };
|
|
2114
2272
|
}
|
|
2115
2273
|
if (strategy.includes("cookie")) {
|
|
2116
2274
|
return cookies({ authentication: null }).redirect("/");
|
|
2117
2275
|
}
|
|
2118
|
-
if (strategy.includes("jwt")) {
|
|
2119
|
-
throw new Error("JWT auth not supported yet");
|
|
2120
|
-
}
|
|
2121
2276
|
if (strategy.includes("key")) {
|
|
2122
2277
|
throw new Error("Key auth not supported yet");
|
|
2123
2278
|
}
|
|
@@ -2136,6 +2291,7 @@ function auth(app) {
|
|
|
2136
2291
|
app.use(async function middle(ctx) {
|
|
2137
2292
|
ctx.user = await getUser(ctx);
|
|
2138
2293
|
});
|
|
2294
|
+
if (app.settings.auth.strategy === "key") return;
|
|
2139
2295
|
app.post("/auth/logout", logout);
|
|
2140
2296
|
const enabled = app.settings.auth.providers;
|
|
2141
2297
|
for (const name of oauth2) {
|
|
@@ -2162,34 +2318,101 @@ function auth(app) {
|
|
|
2162
2318
|
}
|
|
2163
2319
|
}
|
|
2164
2320
|
|
|
2321
|
+
// src/helpers/parseRange.ts
|
|
2322
|
+
function parseRange(header, size) {
|
|
2323
|
+
if (!header) return null;
|
|
2324
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
2325
|
+
if (!match) return null;
|
|
2326
|
+
const [, rawStart, rawEnd] = match;
|
|
2327
|
+
if (rawStart === "" && rawEnd === "") return null;
|
|
2328
|
+
let start;
|
|
2329
|
+
let end;
|
|
2330
|
+
if (rawStart === "") {
|
|
2331
|
+
const n = Number(rawEnd);
|
|
2332
|
+
if (n <= 0) return "unsatisfiable";
|
|
2333
|
+
start = Math.max(0, size - n);
|
|
2334
|
+
end = size - 1;
|
|
2335
|
+
} else {
|
|
2336
|
+
start = Number(rawStart);
|
|
2337
|
+
end = rawEnd === "" ? size - 1 : Number(rawEnd);
|
|
2338
|
+
}
|
|
2339
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
2340
|
+
if (size === 0 || start > end || start >= size) return "unsatisfiable";
|
|
2341
|
+
return { start, end: Math.min(end, size - 1) };
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2165
2344
|
// src/middle/assets.ts
|
|
2345
|
+
var CACHE_CONTROL = "public, max-age=3600";
|
|
2166
2346
|
async function assets(ctx) {
|
|
2167
2347
|
if (!ctx.options.public) return;
|
|
2168
2348
|
if (ctx.method !== "get") return;
|
|
2169
2349
|
if (ctx.url.pathname === "/") return;
|
|
2170
2350
|
try {
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2351
|
+
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
2352
|
+
const file2 = ctx.options.public.file(key);
|
|
2353
|
+
const meta = file2.info ? await file2.info() : null;
|
|
2354
|
+
if (meta ? !meta.exists : !await file2.exists()) return;
|
|
2355
|
+
const ext2 = ctx.url.pathname.split(".").pop();
|
|
2356
|
+
const ctype = meta?.type || ext2;
|
|
2357
|
+
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2358
|
+
let tag;
|
|
2359
|
+
if (meta) {
|
|
2360
|
+
const stamp = meta.date ? meta.date.getTime() : 0;
|
|
2361
|
+
tag = `W/"${meta.size.toString(16)}-${stamp.toString(16)}"`;
|
|
2362
|
+
headers2.etag = tag;
|
|
2363
|
+
if (meta.date) headers2["last-modified"] = meta.date.toUTCString();
|
|
2364
|
+
}
|
|
2365
|
+
const canRange = !!(meta && file2.slice);
|
|
2366
|
+
if (canRange) headers2["accept-ranges"] = "bytes";
|
|
2367
|
+
if (tag && ctx.headers["if-none-match"] === tag) {
|
|
2368
|
+
return status(304).headers(headers2).send();
|
|
2369
|
+
}
|
|
2370
|
+
const rangeHeader = ctx.headers.range;
|
|
2371
|
+
const ifRange = ctx.headers["if-range"];
|
|
2372
|
+
if (meta && file2.slice && rangeHeader && (!ifRange || ifRange === tag)) {
|
|
2373
|
+
const range = parseRange(rangeHeader, meta.size);
|
|
2374
|
+
if (range === "unsatisfiable") {
|
|
2375
|
+
return status(416).headers({ ...headers2, "content-range": `bytes */${meta.size}` }).send();
|
|
2376
|
+
}
|
|
2377
|
+
if (range) {
|
|
2378
|
+
const { start, end } = range;
|
|
2379
|
+
return type(ctype).status(206).headers({
|
|
2380
|
+
...headers2,
|
|
2381
|
+
"content-range": `bytes ${start}-${end}/${meta.size}`,
|
|
2382
|
+
"content-length": String(end - start + 1)
|
|
2383
|
+
}).send(file2.slice(start, end + 1).stream());
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
return type(ctype).headers(headers2).send(file2.stream());
|
|
2174
2387
|
} catch {
|
|
2175
2388
|
}
|
|
2176
2389
|
}
|
|
2177
2390
|
|
|
2178
2391
|
// src/middle/favicon.ts
|
|
2392
|
+
var CACHE_CONTROL2 = "public, max-age=86400";
|
|
2393
|
+
var ext = (name) => name.split(".").pop() || "ico";
|
|
2394
|
+
async function loadFavicon(fav) {
|
|
2395
|
+
try {
|
|
2396
|
+
const type2 = ext(typeof fav === "string" ? fav : fav?.name);
|
|
2397
|
+
const bytes = typeof fav === "string" ? await (await import("fs/promises")).readFile(fav) : Buffer.from(await fav.bytes());
|
|
2398
|
+
return { bytes, type: type2, etag: etag(bytes) };
|
|
2399
|
+
} catch {
|
|
2400
|
+
return null;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2179
2403
|
async function favicon(ctx) {
|
|
2180
|
-
if (ctx.method !== "get") return;
|
|
2181
|
-
if (ctx.url.pathname !== "/favicon.ico") return;
|
|
2182
2404
|
const fav = ctx.options.favicon;
|
|
2183
|
-
if (fav)
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
return icon ? type("ico").send(icon) : 204;
|
|
2405
|
+
if (!fav) return;
|
|
2406
|
+
if (ctx.app.faviconCache === void 0) {
|
|
2407
|
+
ctx.app.faviconCache = await loadFavicon(fav);
|
|
2187
2408
|
}
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
if (
|
|
2192
|
-
|
|
2409
|
+
const entry = ctx.app.faviconCache;
|
|
2410
|
+
if (!entry) return 204;
|
|
2411
|
+
const headers2 = { "cache-control": CACHE_CONTROL2, etag: entry.etag };
|
|
2412
|
+
if (ctx.headers["if-none-match"] === entry.etag) {
|
|
2413
|
+
return status(304).headers(headers2).send();
|
|
2414
|
+
}
|
|
2415
|
+
return type(entry.type).headers(headers2).send(entry.bytes);
|
|
2193
2416
|
}
|
|
2194
2417
|
|
|
2195
2418
|
// src/middle/openapi.ts
|
|
@@ -2409,6 +2632,168 @@ function timer(ctx) {
|
|
|
2409
2632
|
ctx.time = createTime();
|
|
2410
2633
|
}
|
|
2411
2634
|
|
|
2635
|
+
// src/helpers/wsNode.ts
|
|
2636
|
+
var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
2637
|
+
var CONTINUATION = 0;
|
|
2638
|
+
var TEXT = 1;
|
|
2639
|
+
var BINARY = 2;
|
|
2640
|
+
var CLOSE = 8;
|
|
2641
|
+
var PING = 9;
|
|
2642
|
+
var PONG = 10;
|
|
2643
|
+
function encodeFrame(payload, opcode) {
|
|
2644
|
+
const len = payload.length;
|
|
2645
|
+
let header;
|
|
2646
|
+
if (len < 126) {
|
|
2647
|
+
header = Buffer.from([128 | opcode, len]);
|
|
2648
|
+
} else if (len < 65536) {
|
|
2649
|
+
header = Buffer.allocUnsafe(4);
|
|
2650
|
+
header[0] = 128 | opcode;
|
|
2651
|
+
header[1] = 126;
|
|
2652
|
+
header.writeUInt16BE(len, 2);
|
|
2653
|
+
} else {
|
|
2654
|
+
header = Buffer.allocUnsafe(10);
|
|
2655
|
+
header[0] = 128 | opcode;
|
|
2656
|
+
header[1] = 127;
|
|
2657
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
2658
|
+
}
|
|
2659
|
+
return Buffer.concat([header, payload]);
|
|
2660
|
+
}
|
|
2661
|
+
var NodeWebSocket = class {
|
|
2662
|
+
socket;
|
|
2663
|
+
handlers;
|
|
2664
|
+
buffer;
|
|
2665
|
+
fragments;
|
|
2666
|
+
fragmentOpcode;
|
|
2667
|
+
closed;
|
|
2668
|
+
readyState;
|
|
2669
|
+
constructor(socket, handlers) {
|
|
2670
|
+
this.socket = socket;
|
|
2671
|
+
this.handlers = handlers;
|
|
2672
|
+
this.buffer = Buffer.alloc(0);
|
|
2673
|
+
this.fragments = [];
|
|
2674
|
+
this.fragmentOpcode = TEXT;
|
|
2675
|
+
this.closed = false;
|
|
2676
|
+
this.readyState = 1;
|
|
2677
|
+
}
|
|
2678
|
+
send(data) {
|
|
2679
|
+
if (this.closed) return;
|
|
2680
|
+
const isString = typeof data === "string";
|
|
2681
|
+
const payload = isString ? Buffer.from(data) : Buffer.from(data);
|
|
2682
|
+
this.socket.write(encodeFrame(payload, isString ? TEXT : BINARY));
|
|
2683
|
+
}
|
|
2684
|
+
close(code = 1e3, reason = "") {
|
|
2685
|
+
if (this.closed) return;
|
|
2686
|
+
const payload = Buffer.alloc(2 + Buffer.byteLength(reason));
|
|
2687
|
+
payload.writeUInt16BE(code, 0);
|
|
2688
|
+
payload.write(reason, 2);
|
|
2689
|
+
try {
|
|
2690
|
+
this.socket.write(encodeFrame(payload, CLOSE));
|
|
2691
|
+
} catch {
|
|
2692
|
+
}
|
|
2693
|
+
this.shutdown();
|
|
2694
|
+
}
|
|
2695
|
+
// Called once, whether the peer closed, the socket died, or we closed.
|
|
2696
|
+
shutdown() {
|
|
2697
|
+
if (this.closed) return;
|
|
2698
|
+
this.closed = true;
|
|
2699
|
+
this.readyState = 3;
|
|
2700
|
+
try {
|
|
2701
|
+
this.socket.end();
|
|
2702
|
+
} catch {
|
|
2703
|
+
}
|
|
2704
|
+
this.handlers.onClose();
|
|
2705
|
+
}
|
|
2706
|
+
// Feed raw bytes from the TCP socket; parses as many complete frames as it can
|
|
2707
|
+
// and buffers the remainder for the next chunk.
|
|
2708
|
+
receive(chunk) {
|
|
2709
|
+
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
|
|
2710
|
+
while (true) {
|
|
2711
|
+
const buf = this.buffer;
|
|
2712
|
+
if (buf.length < 2) return;
|
|
2713
|
+
const fin = (buf[0] & 128) !== 0;
|
|
2714
|
+
const opcode = buf[0] & 15;
|
|
2715
|
+
const masked = (buf[1] & 128) !== 0;
|
|
2716
|
+
let len = buf[1] & 127;
|
|
2717
|
+
let offset = 2;
|
|
2718
|
+
if (len === 126) {
|
|
2719
|
+
if (buf.length < 4) return;
|
|
2720
|
+
len = buf.readUInt16BE(2);
|
|
2721
|
+
offset = 4;
|
|
2722
|
+
} else if (len === 127) {
|
|
2723
|
+
if (buf.length < 10) return;
|
|
2724
|
+
len = Number(buf.readBigUInt64BE(2));
|
|
2725
|
+
offset = 10;
|
|
2726
|
+
}
|
|
2727
|
+
let mask = null;
|
|
2728
|
+
if (masked) {
|
|
2729
|
+
if (buf.length < offset + 4) return;
|
|
2730
|
+
mask = buf.subarray(offset, offset + 4);
|
|
2731
|
+
offset += 4;
|
|
2732
|
+
}
|
|
2733
|
+
if (buf.length < offset + len) return;
|
|
2734
|
+
const payload = Buffer.from(buf.subarray(offset, offset + len));
|
|
2735
|
+
if (mask) {
|
|
2736
|
+
for (let i = 0; i < len; i++) payload[i] ^= mask[i & 3];
|
|
2737
|
+
}
|
|
2738
|
+
this.buffer = buf.subarray(offset + len);
|
|
2739
|
+
this.frame(fin, opcode, payload);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
frame(fin, opcode, payload) {
|
|
2743
|
+
if (opcode === CLOSE) {
|
|
2744
|
+
this.shutdown();
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
if (opcode === PING) {
|
|
2748
|
+
if (!this.closed) this.socket.write(encodeFrame(payload, PONG));
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
if (opcode === PONG) return;
|
|
2752
|
+
if (opcode === CONTINUATION) {
|
|
2753
|
+
this.fragments.push(payload);
|
|
2754
|
+
} else {
|
|
2755
|
+
this.fragments = [payload];
|
|
2756
|
+
this.fragmentOpcode = opcode;
|
|
2757
|
+
}
|
|
2758
|
+
if (!fin) return;
|
|
2759
|
+
const full = this.fragments.length === 1 ? this.fragments[0] : Buffer.concat(this.fragments);
|
|
2760
|
+
this.fragments = [];
|
|
2761
|
+
const body = this.fragmentOpcode === TEXT ? full.toString("utf8") : full;
|
|
2762
|
+
this.handlers.onMessage(body);
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
async function attachWebsocket(server2, app) {
|
|
2766
|
+
const { createHash } = await import("crypto");
|
|
2767
|
+
server2.on("upgrade", (req, socket, head) => {
|
|
2768
|
+
const key = req.headers["sec-websocket-key"];
|
|
2769
|
+
const upgrade = String(req.headers.upgrade || "").toLowerCase();
|
|
2770
|
+
if (upgrade !== "websocket" || !key || !app.handlers.socket.length) {
|
|
2771
|
+
socket.destroy();
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
const accept = createHash("sha1").update(key + GUID).digest("base64");
|
|
2775
|
+
socket.write(
|
|
2776
|
+
`HTTP/1.1 101 Switching Protocols\r
|
|
2777
|
+
Upgrade: websocket\r
|
|
2778
|
+
Connection: Upgrade\r
|
|
2779
|
+
Sec-WebSocket-Accept: ${accept}\r
|
|
2780
|
+
\r
|
|
2781
|
+
`
|
|
2782
|
+
);
|
|
2783
|
+
socket.setTimeout(0);
|
|
2784
|
+
socket.setNoDelay(true);
|
|
2785
|
+
const ws = new NodeWebSocket(socket, {
|
|
2786
|
+
onMessage: (body) => app.websocket.message(ws, body),
|
|
2787
|
+
onClose: () => app.websocket.close(ws)
|
|
2788
|
+
});
|
|
2789
|
+
app.websocket.open(ws);
|
|
2790
|
+
if (head?.length) ws.receive(head);
|
|
2791
|
+
socket.on("data", (chunk) => ws.receive(chunk));
|
|
2792
|
+
socket.on("close", () => ws.shutdown());
|
|
2793
|
+
socket.on("error", () => ws.shutdown());
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2412
2797
|
// src/context/node.ts
|
|
2413
2798
|
import { TLSSocket } from "tls";
|
|
2414
2799
|
|
|
@@ -2525,22 +2910,25 @@ var Winter = async (app, request, env2) => {
|
|
|
2525
2910
|
};
|
|
2526
2911
|
var Node = async (app) => {
|
|
2527
2912
|
const http = await import("http");
|
|
2528
|
-
const { attachWebsocket } = await import("./wsNode-GEJUCJQ7.js");
|
|
2529
2913
|
const server2 = http.createServer(
|
|
2530
2914
|
async (request, response) => {
|
|
2531
2915
|
const ctx = await createNode(request, app);
|
|
2532
2916
|
if ("error" in ctx) throw ctx.error;
|
|
2533
2917
|
const out = await handleRequest(app, ctx);
|
|
2534
2918
|
response.writeHead(out.status || 200, parseHeaders_default(out.headers));
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2919
|
+
try {
|
|
2920
|
+
if (out.body instanceof ReadableStream) {
|
|
2921
|
+
await iterate(out.body, (chunk) => response.write(chunk));
|
|
2922
|
+
} else {
|
|
2923
|
+
response.write(out.body || "");
|
|
2924
|
+
}
|
|
2925
|
+
response.end();
|
|
2926
|
+
} catch {
|
|
2927
|
+
if (!response.destroyed) response.destroy();
|
|
2539
2928
|
}
|
|
2540
|
-
response.end();
|
|
2541
2929
|
}
|
|
2542
2930
|
);
|
|
2543
|
-
attachWebsocket(server2, app);
|
|
2931
|
+
await attachWebsocket(server2, app);
|
|
2544
2932
|
server2.listen(app.settings.port, () => {
|
|
2545
2933
|
app.settings.log.start(`http://localhost:${app.settings.port}/`);
|
|
2546
2934
|
});
|
|
@@ -2687,6 +3075,9 @@ var Server = class extends Router {
|
|
|
2687
3075
|
platform;
|
|
2688
3076
|
sockets;
|
|
2689
3077
|
websocket;
|
|
3078
|
+
// Lazily-loaded favicon bytes, cached per server until restart (see favicon
|
|
3079
|
+
// middleware). `undefined` = not loaded yet; `null` = configured but missing.
|
|
3080
|
+
faviconCache;
|
|
2690
3081
|
port;
|
|
2691
3082
|
constructor(options = {}) {
|
|
2692
3083
|
super();
|
|
@@ -2705,7 +3096,7 @@ var Server = class extends Router {
|
|
|
2705
3096
|
this.use(timer);
|
|
2706
3097
|
if (this.settings.cors) this.use(preflight);
|
|
2707
3098
|
this.use(assets);
|
|
2708
|
-
this.
|
|
3099
|
+
if (this.settings.favicon) this.get("/favicon.ico", favicon);
|
|
2709
3100
|
this.use(session);
|
|
2710
3101
|
if (this.settings.auth) {
|
|
2711
3102
|
auth(this);
|