@server/next 0.38.0 → 0.39.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 +9 -8
- package/index.js +95 -167
- package/package.json +5 -3
- package/readme.md +28 -0
package/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
|
+
export { default as kv } from 'polystore';
|
|
3
|
+
export { default as bucket } from 'bucket';
|
|
2
4
|
|
|
3
5
|
type LimitOptions = {
|
|
4
6
|
maxSize?: number | string;
|
|
@@ -128,6 +130,7 @@ type BasicValue = string | number | boolean | null;
|
|
|
128
130
|
type SerializableValue = BasicValue | {
|
|
129
131
|
[key: string]: SerializableValue;
|
|
130
132
|
} | Array<SerializableValue>;
|
|
133
|
+
type StoreSource = KVStore | Map<string, any> | string | Record<string, any>;
|
|
131
134
|
type KVStore = {
|
|
132
135
|
name?: string;
|
|
133
136
|
prefix: (prefix?: string) => KVStore;
|
|
@@ -157,8 +160,8 @@ type AuthOption = `${Strategy}:${Provider}` | "key" | {
|
|
|
157
160
|
strategy: Strategy;
|
|
158
161
|
providers?: Provider | Provider[];
|
|
159
162
|
key?: string;
|
|
160
|
-
session?:
|
|
161
|
-
store?:
|
|
163
|
+
session?: StoreSource;
|
|
164
|
+
store?: StoreSource;
|
|
162
165
|
redirect?: string;
|
|
163
166
|
cleanUser?: <T = AuthUser>(user: T) => T | Promise<T>;
|
|
164
167
|
};
|
|
@@ -204,10 +207,9 @@ type Options = {
|
|
|
204
207
|
secret?: string;
|
|
205
208
|
public?: string | Bucket;
|
|
206
209
|
uploads?: string | Bucket | UploadOptions;
|
|
207
|
-
store?:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
store: KVStore;
|
|
210
|
+
store?: StoreSource;
|
|
211
|
+
session?: StoreSource | {
|
|
212
|
+
store: StoreSource;
|
|
211
213
|
};
|
|
212
214
|
cors?: CorsOptions;
|
|
213
215
|
auth?: AuthOption;
|
|
@@ -228,7 +230,6 @@ type Settings = {
|
|
|
228
230
|
bucket: Bucket;
|
|
229
231
|
} & LimitOptions) | null;
|
|
230
232
|
store?: KVStore;
|
|
231
|
-
cookies?: KVStore;
|
|
232
233
|
session?: {
|
|
233
234
|
store: KVStore;
|
|
234
235
|
};
|
|
@@ -500,4 +501,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
500
501
|
}
|
|
501
502
|
declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
|
|
502
503
|
|
|
503
|
-
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, 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, type UploadOptions, type UploadedFile, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
|
|
504
|
+
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, 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 StoreSource, type Strategy, type Time, type UploadOptions, type UploadedFile, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
|
package/index.js
CHANGED
|
@@ -175,7 +175,7 @@ async function saveFileToBucket(originalName, data, bucket2, contentType) {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
function validateFile(originalName, data, contentType, limits) {
|
|
178
|
-
const { maxSize, minSize, fileType } = limits;
|
|
178
|
+
const { maxSize, minSize, fileType: fileType2 } = limits;
|
|
179
179
|
if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
|
|
180
180
|
throw new Error(
|
|
181
181
|
`File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
|
|
@@ -186,15 +186,15 @@ function validateFile(originalName, data, contentType, limits) {
|
|
|
186
186
|
`File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
|
-
if (
|
|
189
|
+
if (fileType2 && fileType2.length > 0) {
|
|
190
190
|
const ext2 = getExt(originalName);
|
|
191
191
|
const mime = contentType.toLowerCase();
|
|
192
|
-
const allowed =
|
|
192
|
+
const allowed = fileType2.some(
|
|
193
193
|
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
|
|
194
194
|
);
|
|
195
195
|
if (!allowed) {
|
|
196
196
|
throw new Error(
|
|
197
|
-
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${
|
|
197
|
+
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType2.join(", ")})`
|
|
198
198
|
);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
@@ -531,9 +531,9 @@ async function parseBody(input, contentType, dest, max = INF) {
|
|
|
531
531
|
let limits;
|
|
532
532
|
if (dest && "bucket" in dest) {
|
|
533
533
|
bucket2 = dest.bucket;
|
|
534
|
-
const { maxSize, minSize, fileType } = dest;
|
|
535
|
-
if (maxSize != null || minSize != null ||
|
|
536
|
-
limits = { maxSize, minSize, fileType };
|
|
534
|
+
const { maxSize, minSize, fileType: fileType2 } = dest;
|
|
535
|
+
if (maxSize != null || minSize != null || fileType2 != null) {
|
|
536
|
+
limits = { maxSize, minSize, fileType: fileType2 };
|
|
537
537
|
}
|
|
538
538
|
} else {
|
|
539
539
|
bucket2 = dest;
|
|
@@ -661,8 +661,8 @@ function normalizeExpires(expires) {
|
|
|
661
661
|
}
|
|
662
662
|
function createCookies(key, val) {
|
|
663
663
|
if (val.value === null) val.expires = EXPIRED;
|
|
664
|
-
const { value, path
|
|
665
|
-
let str = `${key}=${encodeURIComponent(value ?? "")};Path=${
|
|
664
|
+
const { value, path, expires, maxAge, httpOnly, secure, sameSite } = val;
|
|
665
|
+
let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path || "/"}`;
|
|
666
666
|
if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
|
|
667
667
|
if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
|
|
668
668
|
if (httpOnly) str += ";HttpOnly";
|
|
@@ -725,6 +725,16 @@ function clientIp(headers2, opts = {}) {
|
|
|
725
725
|
return normalize(remoteAddress);
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
// src/helpers/store.ts
|
|
729
|
+
import kv from "polystore";
|
|
730
|
+
function toStore(source) {
|
|
731
|
+
const store = source;
|
|
732
|
+
if (store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function") {
|
|
733
|
+
return store;
|
|
734
|
+
}
|
|
735
|
+
return kv(source);
|
|
736
|
+
}
|
|
737
|
+
|
|
728
738
|
// src/helpers/disposition.ts
|
|
729
739
|
var encodeExt = (name) => encodeURIComponent(name).replace(
|
|
730
740
|
/['()*]/g,
|
|
@@ -740,6 +750,14 @@ function disposition(name) {
|
|
|
740
750
|
return `${value}; filename*=UTF-8''${encodeExt(clean)}`;
|
|
741
751
|
}
|
|
742
752
|
|
|
753
|
+
// src/helpers/fileType.ts
|
|
754
|
+
function fileType(file2) {
|
|
755
|
+
if (file2.type) return file2.type;
|
|
756
|
+
const name = file2.path || file2.name || "";
|
|
757
|
+
const ext2 = name.split(".").pop()?.toLowerCase();
|
|
758
|
+
return ext2 ? mimes_default[ext2] : void 0;
|
|
759
|
+
}
|
|
760
|
+
|
|
743
761
|
// src/helpers/isHtml.ts
|
|
744
762
|
var TAG = /^\s*<[a-zA-Z!/]/;
|
|
745
763
|
function isHtml(body) {
|
|
@@ -817,22 +835,22 @@ var Reply = class {
|
|
|
817
835
|
}
|
|
818
836
|
return this.send(JSON.stringify(body));
|
|
819
837
|
}
|
|
820
|
-
redirect(
|
|
821
|
-
this.headers("location",
|
|
838
|
+
redirect(path) {
|
|
839
|
+
this.headers("location", path);
|
|
822
840
|
if (this.res.status == null) this.res.status = 302;
|
|
823
841
|
return this.send();
|
|
824
842
|
}
|
|
825
|
-
async file(
|
|
826
|
-
if (typeof
|
|
827
|
-
if (!await
|
|
828
|
-
return this.type(
|
|
843
|
+
async file(path) {
|
|
844
|
+
if (typeof path !== "string") {
|
|
845
|
+
if (!await path.exists()) return this.status(404).send();
|
|
846
|
+
return this.type(fileType(path)).send(path.stream());
|
|
829
847
|
}
|
|
830
|
-
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(
|
|
848
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) return this.status(404).send();
|
|
831
849
|
try {
|
|
832
|
-
const
|
|
833
|
-
const ext2 =
|
|
834
|
-
await
|
|
835
|
-
const stream =
|
|
850
|
+
const fs = await import("fs");
|
|
851
|
+
const ext2 = path.split(".").pop();
|
|
852
|
+
await fs.promises.access(path);
|
|
853
|
+
const stream = fs.createReadStream(path);
|
|
836
854
|
return this.type(ext2).send(stream);
|
|
837
855
|
} catch (error) {
|
|
838
856
|
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
@@ -1301,8 +1319,8 @@ var oauth = async (code) => {
|
|
|
1301
1319
|
code
|
|
1302
1320
|
})
|
|
1303
1321
|
});
|
|
1304
|
-
return (
|
|
1305
|
-
return fch(`https://api.github.com${
|
|
1322
|
+
return (path) => {
|
|
1323
|
+
return fch(`https://api.github.com${path}`, {
|
|
1306
1324
|
headers: { Authorization: `Bearer ${res.access_token}` }
|
|
1307
1325
|
});
|
|
1308
1326
|
};
|
|
@@ -1437,112 +1455,24 @@ function parseAuthOptions(auth2, all) {
|
|
|
1437
1455
|
if (!auth2.session && !all.store) {
|
|
1438
1456
|
throw new Error("Need a sessionStore store for Auth");
|
|
1439
1457
|
}
|
|
1440
|
-
const store =
|
|
1441
|
-
const
|
|
1458
|
+
const store = all.store ? toStore(all.store) : null;
|
|
1459
|
+
const authStore = auth2.store ? toStore(auth2.store) : store.prefix("user:");
|
|
1460
|
+
const sessionStore = auth2.session ? toStore(auth2.session) : store.prefix("auth:");
|
|
1442
1461
|
return {
|
|
1443
1462
|
strategy,
|
|
1444
1463
|
providers: list,
|
|
1445
1464
|
redirect: redirect2,
|
|
1446
1465
|
cleanUser,
|
|
1447
|
-
store,
|
|
1448
|
-
session:
|
|
1466
|
+
store: authStore,
|
|
1467
|
+
session: sessionStore
|
|
1449
1468
|
};
|
|
1450
1469
|
}
|
|
1451
1470
|
|
|
1452
1471
|
// src/helpers/bucket.ts
|
|
1453
|
-
import
|
|
1454
|
-
import * as fsp from "fs/promises";
|
|
1455
|
-
import * as path from "path";
|
|
1456
|
-
function localBucket(root, prefix = "") {
|
|
1457
|
-
const base = path.resolve(root);
|
|
1458
|
-
const resolveKey = (name) => {
|
|
1459
|
-
if (!name) throw new Error("File name is required");
|
|
1460
|
-
const full = path.resolve(base, name.replace(/^\/+/, ""));
|
|
1461
|
-
if (full !== base && !full.startsWith(base + path.sep)) {
|
|
1462
|
-
throw new Error(`Path "${name}" escapes the bucket root`);
|
|
1463
|
-
}
|
|
1464
|
-
return full;
|
|
1465
|
-
};
|
|
1466
|
-
const file2 = (name, win) => {
|
|
1467
|
-
const full = resolveKey(name);
|
|
1468
|
-
const key = prefix + name.replace(/^\/+/, "");
|
|
1469
|
-
const type2 = mimes_default[path.extname(name).slice(1).toLowerCase()];
|
|
1470
|
-
const read = () => {
|
|
1471
|
-
let opts;
|
|
1472
|
-
if (win) {
|
|
1473
|
-
opts = { start: win.start };
|
|
1474
|
-
if (Number.isFinite(win.end)) opts.end = Math.max(win.start, win.end - 1);
|
|
1475
|
-
}
|
|
1476
|
-
const nodeStream = fs.createReadStream(full, opts);
|
|
1477
|
-
return new ReadableStream({
|
|
1478
|
-
start(controller) {
|
|
1479
|
-
nodeStream.on("data", (chunk) => controller.enqueue(chunk));
|
|
1480
|
-
nodeStream.on("end", () => controller.close());
|
|
1481
|
-
nodeStream.on("error", (err) => controller.error(err));
|
|
1482
|
-
},
|
|
1483
|
-
cancel() {
|
|
1484
|
-
nodeStream.destroy();
|
|
1485
|
-
}
|
|
1486
|
-
});
|
|
1487
|
-
};
|
|
1488
|
-
return {
|
|
1489
|
-
path: key,
|
|
1490
|
-
name: path.basename(name),
|
|
1491
|
-
type: type2,
|
|
1492
|
-
async exists() {
|
|
1493
|
-
const stats = await fsp.stat(full).catch(() => null);
|
|
1494
|
-
return !!stats?.isFile();
|
|
1495
|
-
},
|
|
1496
|
-
async info() {
|
|
1497
|
-
const stats = await fsp.stat(full).catch(() => null);
|
|
1498
|
-
if (!stats?.isFile()) return null;
|
|
1499
|
-
const size = win ? Math.max(0, Math.min(win.end, stats.size) - win.start) : stats.size;
|
|
1500
|
-
return { size, type: type2 ?? null, modified: stats.mtime };
|
|
1501
|
-
},
|
|
1502
|
-
// Read-only view of [start, end), composed relative to the current window.
|
|
1503
|
-
slice(start, end) {
|
|
1504
|
-
const base2 = win?.start ?? 0;
|
|
1505
|
-
const cap = win?.end ?? Number.POSITIVE_INFINITY;
|
|
1506
|
-
const s = Math.min(cap, base2 + Math.max(0, start));
|
|
1507
|
-
const e = end === void 0 ? cap : Math.min(cap, base2 + end);
|
|
1508
|
-
return file2(name, { start: s, end: e });
|
|
1509
|
-
},
|
|
1510
|
-
async write(content) {
|
|
1511
|
-
await fsp.mkdir(path.dirname(full), { recursive: true });
|
|
1512
|
-
if (content instanceof ReadableStream) {
|
|
1513
|
-
const writable = fs.createWriteStream(full);
|
|
1514
|
-
for await (const chunk of content) {
|
|
1515
|
-
writable.write(chunk);
|
|
1516
|
-
}
|
|
1517
|
-
await new Promise((resolve2, reject) => {
|
|
1518
|
-
writable.on("error", reject);
|
|
1519
|
-
writable.end(() => resolve2());
|
|
1520
|
-
});
|
|
1521
|
-
return;
|
|
1522
|
-
}
|
|
1523
|
-
await fsp.writeFile(full, content);
|
|
1524
|
-
},
|
|
1525
|
-
stream() {
|
|
1526
|
-
return read();
|
|
1527
|
-
},
|
|
1528
|
-
async bytes() {
|
|
1529
|
-
if (win) return new Uint8Array(await new Response(read()).arrayBuffer());
|
|
1530
|
-
return new Uint8Array(await fsp.readFile(full));
|
|
1531
|
-
},
|
|
1532
|
-
async remove() {
|
|
1533
|
-
await fsp.unlink(full).catch(() => {
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
};
|
|
1537
|
-
};
|
|
1538
|
-
return {
|
|
1539
|
-
file: file2,
|
|
1540
|
-
folder: (sub) => localBucket(path.join(base, sub), `${prefix}${sub.replace(/^\/+|\/+$/g, "")}/`)
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1472
|
+
import FileSystem from "bucket/fs";
|
|
1543
1473
|
function bucket(root) {
|
|
1544
1474
|
if (!root) return null;
|
|
1545
|
-
if (typeof root === "string") return
|
|
1475
|
+
if (typeof root === "string") return FileSystem(root);
|
|
1546
1476
|
if (typeof root.file === "function") return root;
|
|
1547
1477
|
throw new Error(
|
|
1548
1478
|
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
@@ -1638,14 +1568,14 @@ function createLogger(level) {
|
|
|
1638
1568
|
const request = (ctx, res) => {
|
|
1639
1569
|
if (!enabled) return;
|
|
1640
1570
|
const method = ctx.method.toUpperCase();
|
|
1641
|
-
const
|
|
1571
|
+
const path = ctx.url.pathname;
|
|
1642
1572
|
const reqLen = Number(ctx.headers["content-length"]) || 0;
|
|
1643
1573
|
const resLen = Number(res.headers.get("content-length")) || 0;
|
|
1644
1574
|
const status2 = res.status;
|
|
1645
1575
|
const text = STATUS_TEXT[status2] || "";
|
|
1646
1576
|
const reqSize = reqLen ? ` ${formatBytes(reqLen)}` : "";
|
|
1647
1577
|
const resSize = resLen ? ` ${formatBytes(resLen)}` : "";
|
|
1648
|
-
let line = `${method} ${
|
|
1578
|
+
let line = `${method} ${path}${reqSize} \u2192 ${status2}${text ? ` ${text}` : ""}${resSize}`;
|
|
1649
1579
|
const location = res.headers.get("location");
|
|
1650
1580
|
if (location) line += ` \u2192 ${location}`;
|
|
1651
1581
|
message("api", line);
|
|
@@ -1772,22 +1702,22 @@ function config(options = {}) {
|
|
|
1772
1702
|
if (!up) {
|
|
1773
1703
|
settings.uploads = null;
|
|
1774
1704
|
} else if (typeof up === "object" && "bucket" in up) {
|
|
1775
|
-
const { bucket: bucket2, maxSize, minSize, fileType } = up;
|
|
1705
|
+
const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
|
|
1776
1706
|
if (maxSize != null) parseBytes(maxSize);
|
|
1777
1707
|
if (minSize != null) parseBytes(minSize);
|
|
1778
|
-
settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType };
|
|
1708
|
+
settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
|
|
1779
1709
|
} else {
|
|
1780
1710
|
settings.uploads = { bucket: bucket(up) };
|
|
1781
1711
|
}
|
|
1782
1712
|
const favicon2 = options.favicon || env2.FAVICON;
|
|
1783
1713
|
if (favicon2) settings.favicon = favicon2;
|
|
1784
|
-
settings.store = options.store
|
|
1785
|
-
settings.cookies = options.cookies ?? null;
|
|
1714
|
+
settings.store = options.store ? toStore(options.store) : null;
|
|
1786
1715
|
if (options.session) {
|
|
1787
|
-
|
|
1716
|
+
const store = typeof options.session === "object" && "store" in options.session ? options.session.store : options.session;
|
|
1717
|
+
settings.session = { store: toStore(store) };
|
|
1788
1718
|
}
|
|
1789
|
-
if (
|
|
1790
|
-
settings.session = { store:
|
|
1719
|
+
if (settings.store && !options.session) {
|
|
1720
|
+
settings.session = { store: settings.store.prefix("session:") };
|
|
1791
1721
|
}
|
|
1792
1722
|
if (options.auth || env2.AUTH) {
|
|
1793
1723
|
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null, options);
|
|
@@ -1940,9 +1870,10 @@ async function parseResponse(out, ctx) {
|
|
|
1940
1870
|
if (!await out.exists()) {
|
|
1941
1871
|
out = new Response(null, { status: 404 });
|
|
1942
1872
|
} else {
|
|
1873
|
+
const type2 = fileType(out);
|
|
1943
1874
|
out = new Response(
|
|
1944
1875
|
out.stream(),
|
|
1945
|
-
|
|
1876
|
+
type2 ? { headers: { "content-type": type2 } } : void 0
|
|
1946
1877
|
);
|
|
1947
1878
|
}
|
|
1948
1879
|
}
|
|
@@ -2011,13 +1942,6 @@ async function parseResponse(out, ctx) {
|
|
|
2011
1942
|
}
|
|
2012
1943
|
ctx.options.session.store.set(id, ctx.session);
|
|
2013
1944
|
}
|
|
2014
|
-
if (ctx.options.cookies) {
|
|
2015
|
-
if (Object.keys(ctx.res?.cookies || {}).length) {
|
|
2016
|
-
for (const cookie of Object.values(ctx.res.cookies)) {
|
|
2017
|
-
ctx.res.headers.append("set-cookie", cookie);
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
1945
|
if (ctx?.res?.headers) {
|
|
2022
1946
|
for (const key in ctx.res.headers) {
|
|
2023
1947
|
out.headers[key] = ctx.res.headers[key];
|
|
@@ -2027,14 +1951,14 @@ async function parseResponse(out, ctx) {
|
|
|
2027
1951
|
}
|
|
2028
1952
|
|
|
2029
1953
|
// src/pathPattern.ts
|
|
2030
|
-
function pathPattern(pattern,
|
|
2031
|
-
if (pattern === "*" &&
|
|
1954
|
+
function pathPattern(pattern, path) {
|
|
1955
|
+
if (pattern === "*" && path === "/") return {};
|
|
2032
1956
|
pattern = `/${pattern.replace(/^\//, "")}`;
|
|
2033
1957
|
pattern = pattern.replace(/\/$/, "") || "/";
|
|
2034
|
-
|
|
2035
|
-
if (pattern ===
|
|
1958
|
+
path = path.replace(/\/$/, "") || "/";
|
|
1959
|
+
if (pattern === path) return {};
|
|
2036
1960
|
const params = {};
|
|
2037
|
-
const pathParts =
|
|
1961
|
+
const pathParts = path.split("/").slice(1).map((u) => decodeURIComponent(u));
|
|
2038
1962
|
const pattParts = pattern.split("/").slice(1);
|
|
2039
1963
|
let allSame = true;
|
|
2040
1964
|
for (let i = 0; i < Math.max(pathParts.length, pattParts.length); i++) {
|
|
@@ -2093,7 +2017,7 @@ function validate(ctx, schema) {
|
|
|
2093
2017
|
} catch (error) {
|
|
2094
2018
|
if (error.name === "ZodError" || error.constructor.name === "ZodError") {
|
|
2095
2019
|
const message = error.issues.map(
|
|
2096
|
-
({ path
|
|
2020
|
+
({ path, message: message2 }) => `[${base}.${path.join(".")}]: ${message2}`
|
|
2097
2021
|
).sort().join("\n");
|
|
2098
2022
|
throw new StatusError(message, 422);
|
|
2099
2023
|
}
|
|
@@ -2289,7 +2213,7 @@ async function verify(password, hash3) {
|
|
|
2289
2213
|
const [, variant, , memory, passes, parallelism, saltB64, hashB64] = match;
|
|
2290
2214
|
const nonce = Buffer.from(saltB64, "base64");
|
|
2291
2215
|
const expected = Buffer.from(hashB64, "base64");
|
|
2292
|
-
return new Promise((
|
|
2216
|
+
return new Promise((resolve, reject) => {
|
|
2293
2217
|
crypto3.argon2(
|
|
2294
2218
|
`argon2${variant}`,
|
|
2295
2219
|
{
|
|
@@ -2303,9 +2227,9 @@ async function verify(password, hash3) {
|
|
|
2303
2227
|
(err, derivedKey) => {
|
|
2304
2228
|
if (err) return reject(err);
|
|
2305
2229
|
if (derivedKey.length === expected.length && timingSafeEqual(derivedKey, expected)) {
|
|
2306
|
-
|
|
2230
|
+
resolve(true);
|
|
2307
2231
|
} else {
|
|
2308
|
-
|
|
2232
|
+
resolve(false);
|
|
2309
2233
|
}
|
|
2310
2234
|
}
|
|
2311
2235
|
);
|
|
@@ -2500,8 +2424,8 @@ async function assets(ctx) {
|
|
|
2500
2424
|
const info = file2.info?.bind(file2);
|
|
2501
2425
|
const meta = info ? await info() : null;
|
|
2502
2426
|
if (info ? !meta : !await file2.exists()) return;
|
|
2503
|
-
const ext2 = ctx.url.pathname.split(".").pop();
|
|
2504
|
-
const ctype = meta?.type || ext2;
|
|
2427
|
+
const ext2 = ctx.url.pathname.split(".").pop()?.toLowerCase();
|
|
2428
|
+
const ctype = ext2 && mimes_default[ext2] || meta?.type || ext2;
|
|
2505
2429
|
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2506
2430
|
let tag;
|
|
2507
2431
|
if (meta) {
|
|
@@ -2564,7 +2488,7 @@ async function favicon(ctx) {
|
|
|
2564
2488
|
}
|
|
2565
2489
|
|
|
2566
2490
|
// src/middle/openapi.ts
|
|
2567
|
-
import * as
|
|
2491
|
+
import * as fsp from "fs/promises";
|
|
2568
2492
|
var entities = {
|
|
2569
2493
|
"&": "&",
|
|
2570
2494
|
"<": "<",
|
|
@@ -2610,7 +2534,7 @@ function zodToSchema(schema) {
|
|
|
2610
2534
|
}
|
|
2611
2535
|
return { type: type2 };
|
|
2612
2536
|
}
|
|
2613
|
-
var pkgProm =
|
|
2537
|
+
var pkgProm = fsp.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
|
|
2614
2538
|
var getTag = (name, fn) => {
|
|
2615
2539
|
const found = fn.toString().split("\n").filter((l) => /\s+\/\/\s/.test(l)).map((l) => l.trim().replace("// ", "")).find((l) => l.startsWith(name));
|
|
2616
2540
|
if (!found) return "";
|
|
@@ -2622,14 +2546,14 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2622
2546
|
const paths = {};
|
|
2623
2547
|
for (const [method, routes] of Object.entries(handlers)) {
|
|
2624
2548
|
for (const route of routes) {
|
|
2625
|
-
const
|
|
2549
|
+
const path = route.path;
|
|
2626
2550
|
const fn = route.fns.find((p) => typeof p === "function");
|
|
2627
2551
|
const meta = route.fns.find((p) => typeof p === "object");
|
|
2628
2552
|
const config2 = getConfig(route.options);
|
|
2629
|
-
if (typeof
|
|
2553
|
+
if (typeof path !== "string" || path === "*" || path === "/docs" || !fn) {
|
|
2630
2554
|
continue;
|
|
2631
2555
|
}
|
|
2632
|
-
const normalizedPath =
|
|
2556
|
+
const normalizedPath = path.replace(/\(\w+\)/gi, "").replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
2633
2557
|
if (!paths[normalizedPath]) {
|
|
2634
2558
|
paths[normalizedPath] = {};
|
|
2635
2559
|
}
|
|
@@ -2656,7 +2580,7 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2656
2580
|
};
|
|
2657
2581
|
}
|
|
2658
2582
|
const parameters = [];
|
|
2659
|
-
const matched = Array.from(
|
|
2583
|
+
const matched = Array.from(path.matchAll(/:[\w()]+/gi));
|
|
2660
2584
|
matched.forEach((match) => {
|
|
2661
2585
|
const [name, type2 = "string"] = match[0].slice(1).replace(/\)/, "").split("(");
|
|
2662
2586
|
parameters.push({
|
|
@@ -2995,18 +2919,18 @@ async function createNode(req, app) {
|
|
|
2995
2919
|
const cookies2 = parseCookies(headers2.cookie);
|
|
2996
2920
|
const scheme = req.socket instanceof TLSSocket ? "https" : "http";
|
|
2997
2921
|
const host = headers2.host || `localhost:${app.settings.port}`;
|
|
2998
|
-
const
|
|
2922
|
+
const path = (req.url || "/").replace(/\/$/, "") || "/";
|
|
2999
2923
|
const baseUrl = `${scheme}://${host}`;
|
|
3000
|
-
const url = new URL(
|
|
2924
|
+
const url = new URL(path, baseUrl);
|
|
3001
2925
|
define(
|
|
3002
2926
|
url,
|
|
3003
2927
|
"query",
|
|
3004
2928
|
(url2) => Object.fromEntries(url2.searchParams.entries())
|
|
3005
2929
|
);
|
|
3006
2930
|
const source = {
|
|
3007
|
-
getBuffer: () => new Promise((
|
|
2931
|
+
getBuffer: () => new Promise((resolve, reject) => {
|
|
3008
2932
|
const chunks2 = [];
|
|
3009
|
-
req.on("data", (chunk) => chunks2.push(chunk)).on("end", () =>
|
|
2933
|
+
req.on("data", (chunk) => chunks2.push(chunk)).on("end", () => resolve(Buffer.concat(chunks2))).on("error", reject);
|
|
3010
2934
|
}),
|
|
3011
2935
|
getStream: () => toWeb(req)
|
|
3012
2936
|
};
|
|
@@ -3158,9 +3082,9 @@ var Router = class _Router {
|
|
|
3158
3082
|
// functions into a single flat `fns` list. A plain options object may sit
|
|
3159
3083
|
// between the path and the handlers, and it's pulled out here.
|
|
3160
3084
|
handle(method, pathOrFn, ...rest) {
|
|
3161
|
-
let
|
|
3085
|
+
let path = "*";
|
|
3162
3086
|
if (typeof pathOrFn === "string") {
|
|
3163
|
-
|
|
3087
|
+
path = pathOrFn;
|
|
3164
3088
|
} else if (pathOrFn != null) {
|
|
3165
3089
|
rest.unshift(pathOrFn);
|
|
3166
3090
|
}
|
|
@@ -3170,7 +3094,7 @@ var Router = class _Router {
|
|
|
3170
3094
|
}
|
|
3171
3095
|
const base = method === "socket" ? [] : this.middleware;
|
|
3172
3096
|
const fns = [...base, ...rest].filter((fn) => fn != null);
|
|
3173
|
-
this.handlers[method].push({ path
|
|
3097
|
+
this.handlers[method].push({ path, options, fns });
|
|
3174
3098
|
return this.self();
|
|
3175
3099
|
}
|
|
3176
3100
|
socket(pathOrMid, optionsOrMid, ...middleware) {
|
|
@@ -3235,31 +3159,33 @@ function isSerializable(body) {
|
|
|
3235
3159
|
}
|
|
3236
3160
|
function ServerTest(app) {
|
|
3237
3161
|
const port = app.settings.port;
|
|
3238
|
-
const fetch2 = async (method,
|
|
3162
|
+
const fetch2 = async (method, path, options = {}) => {
|
|
3239
3163
|
if (!options.headers) options.headers = {};
|
|
3240
3164
|
if (isSerializable(options.body)) {
|
|
3241
3165
|
options.headers["content-type"] = "application/json";
|
|
3242
3166
|
options.body = JSON.stringify(options.body);
|
|
3243
3167
|
}
|
|
3244
3168
|
return await app.fetch(
|
|
3245
|
-
new Request(`http://localhost:${port}${
|
|
3169
|
+
new Request(`http://localhost:${port}${path}`, {
|
|
3246
3170
|
method,
|
|
3247
3171
|
...options
|
|
3248
3172
|
})
|
|
3249
3173
|
);
|
|
3250
3174
|
};
|
|
3251
3175
|
return {
|
|
3252
|
-
get: (
|
|
3253
|
-
head: (
|
|
3254
|
-
post: (
|
|
3255
|
-
put: (
|
|
3256
|
-
patch: (
|
|
3257
|
-
delete: (
|
|
3258
|
-
options: (
|
|
3176
|
+
get: (path, options) => fetch2("get", path, options),
|
|
3177
|
+
head: (path, options) => fetch2("head", path, options),
|
|
3178
|
+
post: (path, body, options) => fetch2("post", path, { body, ...options }),
|
|
3179
|
+
put: (path, body, options) => fetch2("put", path, { body, ...options }),
|
|
3180
|
+
patch: (path, body, options) => fetch2("patch", path, { body, ...options }),
|
|
3181
|
+
delete: (path, options) => fetch2("delete", path, options),
|
|
3182
|
+
options: (path, options) => fetch2("options", path, options)
|
|
3259
3183
|
};
|
|
3260
3184
|
}
|
|
3261
3185
|
|
|
3262
3186
|
// src/index.ts
|
|
3187
|
+
import { default as default2 } from "polystore";
|
|
3188
|
+
import { default as default3 } from "bucket";
|
|
3263
3189
|
var Server = class extends Router {
|
|
3264
3190
|
settings;
|
|
3265
3191
|
platform;
|
|
@@ -3327,6 +3253,7 @@ function server(options) {
|
|
|
3327
3253
|
export {
|
|
3328
3254
|
Server,
|
|
3329
3255
|
ServerError_default as ServerError,
|
|
3256
|
+
default3 as bucket,
|
|
3330
3257
|
cache,
|
|
3331
3258
|
cookies,
|
|
3332
3259
|
server as default,
|
|
@@ -3334,6 +3261,7 @@ export {
|
|
|
3334
3261
|
file,
|
|
3335
3262
|
headers,
|
|
3336
3263
|
json,
|
|
3264
|
+
default2 as kv,
|
|
3337
3265
|
redirect,
|
|
3338
3266
|
router,
|
|
3339
3267
|
send,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
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",
|
|
@@ -64,15 +64,17 @@
|
|
|
64
64
|
},
|
|
65
65
|
"tutorials": "docs/tutorials"
|
|
66
66
|
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"bucket": "^0.6.0",
|
|
69
|
+
"polystore": "^0.23.2"
|
|
70
|
+
},
|
|
67
71
|
"devDependencies": {
|
|
68
72
|
"@types/bun": "^1.3.0",
|
|
69
73
|
"@types/jest": "^30.0.0",
|
|
70
74
|
"@types/node": "^24.10.0",
|
|
71
|
-
"bucket": "^0.5.0",
|
|
72
75
|
"bun": "^1.3.13",
|
|
73
76
|
"check-dts": "^0.8.2",
|
|
74
77
|
"jest": "^29.7.0",
|
|
75
|
-
"polystore": "^0.21.1",
|
|
76
78
|
"tsup": "^8.5.1",
|
|
77
79
|
"typescript": "^6.0.2"
|
|
78
80
|
},
|
package/readme.md
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
1
|
# Server JS [](https://www.npmjs.com/package/@server/next) [](https://github.com/franciscop/server-next/actions)
|
|
2
|
+
|
|
3
|
+
A modern web server for Bun and Node, with routing, authentication, uploads, WebSockets and testing built in.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @server/next
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import server from '@server/next';
|
|
11
|
+
|
|
12
|
+
export default server({ store: new Map(), uploads: './uploads' })
|
|
13
|
+
.get('/', () => 'Hello world')
|
|
14
|
+
.get('/users/:id', (ctx) => db.users.find(ctx.url.params.id))
|
|
15
|
+
.post('/avatar', (ctx) => ctx.body.avatar.path);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Key-value stores and file storage come included, so `store` takes a plain `Map` and `uploads` takes a folder path. For Redis, S3 and the rest, `kv` and `bucket` are exported too:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import server, { kv, bucket } from '@server/next';
|
|
22
|
+
|
|
23
|
+
const store = kv(createClient({ url }).connect());
|
|
24
|
+
const uploads = bucket.S3('my-bucket', { id, key });
|
|
25
|
+
|
|
26
|
+
export default server({ store, uploads });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
See the [full documentation](https://serverjs.io/documentation).
|