@nifrajs/core 5.3.0 → 5.4.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/CHANGELOG.md +36 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/range.d.ts.map +1 -1
- package/dist/range.js +62 -2
- package/dist/range.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +8 -3
- package/dist/server/server.js.map +1 -1
- package/dist/static-file.d.ts +104 -0
- package/dist/static-file.d.ts.map +1 -0
- package/dist/static-file.js +238 -0
- package/dist/static-file.js.map +1 -0
- package/package.json +6 -1
- package/src/index.ts +1 -1
- package/src/range.ts +61 -2
- package/src/server/core-copies.ts +1 -1
- package/src/server/server.ts +8 -3
- package/src/static-file.ts +306 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a file served from disk is typed as, which files are never served, and when a client's copy of
|
|
3
|
+
* one is still current - the shared owner for every static-file surface.
|
|
4
|
+
*
|
|
5
|
+
* `@nifrajs/web`'s `servePublicDir` (dev, the Bun entry), the Deno entry's handler and
|
|
6
|
+
* `@nifrajs/node`'s `static` reader (the Node entry) answer the same deployed files. Copies that drift
|
|
7
|
+
* serve a `.pdf` as a download on one runtime and inline on the other, a `.well-known` file on one and
|
|
8
|
+
* a 404 on the other, or a resumed download spliced from two versions of a file on one, so all three
|
|
9
|
+
* read this module. The Node adapter runs a generated copy (scripts/gen-node-outcome.ts), so this
|
|
10
|
+
* module imports nothing.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Media types by extension. A table rather than the runtime's own file typing, which Node and Deno
|
|
14
|
+
* lack. Unknown extensions, and extensionless files such as ACME tokens, are
|
|
15
|
+
* `application/octet-stream`, which together with `nosniff` keeps a browser from guessing a more
|
|
16
|
+
* dangerous type.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CONTENT_TYPES: Readonly<Record<string, string>>;
|
|
19
|
+
/** The media type for a file path or name by its extension, case-insensitively. */
|
|
20
|
+
export declare function contentTypeOf(path: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a relative path crosses a dot-segment other than `.well-known` (RFC 8615, public by
|
|
23
|
+
* definition). Serving, the build's `public/` copy and the parity listing all apply it, so an editor
|
|
24
|
+
* or VCS artifact (`.env`, `.git/`, `.DS_Store`) is neither served nor shipped. Both separators split:
|
|
25
|
+
* a request path uses `/`, a relative file path the OS separator.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isHiddenPath(rel: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The stat fields that tell one version of a file from another on one host: node:fs `Stats` as it is,
|
|
30
|
+
* or Deno's `FileInfo` with its dates as epoch milliseconds and a field it lacks as `NaN`.
|
|
31
|
+
*/
|
|
32
|
+
export interface FileVersion {
|
|
33
|
+
readonly dev: number;
|
|
34
|
+
readonly ino: number;
|
|
35
|
+
readonly size: number;
|
|
36
|
+
readonly mtimeMs: number;
|
|
37
|
+
readonly ctimeMs: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The time a file can be dated by in Last-Modified, or `undefined`: its mtime, only while that is
|
|
41
|
+
* the time of its own last change. A copy or build that keeps or normalizes timestamps - `cp -p`,
|
|
42
|
+
* `rsync -a`, tar, a container image layer, SOURCE_DATE_EPOCH, the Nix store's 1 s - gives a
|
|
43
|
+
* same-size replacement the old file's date, so its files are left undated and their content tag
|
|
44
|
+
* validates them alone.
|
|
45
|
+
*/
|
|
46
|
+
export declare function lastModifiedOf(version: FileVersion): number | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the client's copy is current, so the answer is a bodyless 304. `If-None-Match` (the header,
|
|
49
|
+
* or `null`) decides whenever it is sent (RFC 9110 section 13.2.2), by weak comparison with `etag`, and
|
|
50
|
+
* `*` matches any file. Otherwise an `If-Modified-Since` at or after `lastModified` (epoch
|
|
51
|
+
* milliseconds), compared at the whole seconds an HTTP date carries; an unparseable date, or a file
|
|
52
|
+
* with no date, is never current.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isNotModified(ifNoneMatch: string | null, ifModifiedSince: string | null, etag: string | undefined, lastModified: number | undefined): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Whether `If-Range` (the header, or `null`) lets a range request be answered partially; otherwise the
|
|
57
|
+
* whole file is the conformant answer, not a fallback. An entity tag must equal the file's strong
|
|
58
|
+
* `etag` (RFC 9110 section 13.1.5). A date must equal `lastModified` to the second, and counts only
|
|
59
|
+
* for a file with no strong tag: a client holding the tag sends it, and a date cannot tell apart two
|
|
60
|
+
* versions dated in the same second - here or on another instance - so resuming by one could splice
|
|
61
|
+
* them.
|
|
62
|
+
*/
|
|
63
|
+
export declare function ifRangeAllowsPartial(ifRange: string | null, etag: string | undefined, lastModified: number | undefined): boolean;
|
|
64
|
+
/** What node:crypto's `createHash` returns; Bun, Node and Deno all provide it. */
|
|
65
|
+
export interface ContentHash {
|
|
66
|
+
update(bytes: Uint8Array): unknown;
|
|
67
|
+
digest(encoding: "base64url"): string;
|
|
68
|
+
}
|
|
69
|
+
/** Reads into `bytes` from byte `position` of a file, resolving to the count read (0 at its end). */
|
|
70
|
+
export type ReadAt = (bytes: Uint8Array, position: number) => Promise<number>;
|
|
71
|
+
/**
|
|
72
|
+
* Strong entity tags from what a file holds, the same for the same bytes on every instance and
|
|
73
|
+
* runtime, so a replacement that keeps a file's size and timestamps still changes its tag. A tag is
|
|
74
|
+
* remembered per real path for the version it was computed from, so serving a known file reads no
|
|
75
|
+
* more than before.
|
|
76
|
+
*/
|
|
77
|
+
export interface ContentTags {
|
|
78
|
+
/** The tag remembered for this version of the file at `path`, if any. */
|
|
79
|
+
cached(path: string, version: FileVersion): string | undefined;
|
|
80
|
+
/** The tag of `bytes`, read whole from this version of the file at `path`. */
|
|
81
|
+
ofBytes(path: string, version: FileVersion, bytes: Uint8Array): string;
|
|
82
|
+
/**
|
|
83
|
+
* The tag of this version of the file at `path`, read whole through `read`; requests for a version
|
|
84
|
+
* already being read wait for that read. A file over {@link CONTENT_TAG_MAX} is not read: its tag
|
|
85
|
+
* names its identity on this host instead, or is `undefined` where the runtime reports none.
|
|
86
|
+
*/
|
|
87
|
+
ofFile(path: string, version: FileVersion, read: ReadAt): Promise<string | undefined>;
|
|
88
|
+
}
|
|
89
|
+
export interface ContentTagOptions {
|
|
90
|
+
/** Most tags remembered at once (default 8192); past it the earliest is forgotten. */
|
|
91
|
+
readonly limit?: number;
|
|
92
|
+
/** The clock compared with a file's ctime (default `Date.now`). */
|
|
93
|
+
readonly now?: () => number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Largest file tagged by its content. Its first request waits for one read of the whole file, ~0.1 s
|
|
97
|
+
* per 256 MiB with hardware SHA-256 and several times that on a slow disk or CPU, so a larger file -
|
|
98
|
+
* a long video that is only ever fetched by ranges, say - is tagged by its device, inode, size and
|
|
99
|
+
* times. That tag is still strong and changes with any replacement, but differs between instances.
|
|
100
|
+
*/
|
|
101
|
+
export declare const CONTENT_TAG_MAX: number;
|
|
102
|
+
/** Tags from `createHash` - node:crypto's, which every runtime nifra serves files on provides. */
|
|
103
|
+
export declare function contentTags(createHash: (algorithm: "sha256") => ContentHash, options?: ContentTagOptions): ContentTags;
|
|
104
|
+
//# sourceMappingURL=static-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-file.d.ts","sourceRoot":"","sources":["../src/static-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuCzD,CAAA;AAEF,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAKjD;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAOD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAGvE;AAWD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAcT;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAQT;AAED,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAA;IAClC,MAAM,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAAA;CACtC;AAED,qGAAqG;AACrG,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;AAE7E;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAAA;IAC9D,8EAA8E;IAC9E,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAAA;IACtE;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACtF;AAED,MAAM,WAAW,iBAAiB;IAChC,sFAAsF;IACtF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,mEAAmE;IACnE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAoB,CAAA;AAqBhD,kGAAkG;AAClG,wBAAgB,WAAW,CACzB,UAAU,EAAE,CAAC,SAAS,EAAE,QAAQ,KAAK,WAAW,EAChD,OAAO,GAAE,iBAAsB,GAC9B,WAAW,CAsEb"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a file served from disk is typed as, which files are never served, and when a client's copy of
|
|
3
|
+
* one is still current - the shared owner for every static-file surface.
|
|
4
|
+
*
|
|
5
|
+
* `@nifrajs/web`'s `servePublicDir` (dev, the Bun entry), the Deno entry's handler and
|
|
6
|
+
* `@nifrajs/node`'s `static` reader (the Node entry) answer the same deployed files. Copies that drift
|
|
7
|
+
* serve a `.pdf` as a download on one runtime and inline on the other, a `.well-known` file on one and
|
|
8
|
+
* a 404 on the other, or a resumed download spliced from two versions of a file on one, so all three
|
|
9
|
+
* read this module. The Node adapter runs a generated copy (scripts/gen-node-outcome.ts), so this
|
|
10
|
+
* module imports nothing.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Media types by extension. A table rather than the runtime's own file typing, which Node and Deno
|
|
14
|
+
* lack. Unknown extensions, and extensionless files such as ACME tokens, are
|
|
15
|
+
* `application/octet-stream`, which together with `nosniff` keeps a browser from guessing a more
|
|
16
|
+
* dangerous type.
|
|
17
|
+
*/
|
|
18
|
+
export const CONTENT_TYPES = Object.freeze({
|
|
19
|
+
".html": "text/html; charset=utf-8",
|
|
20
|
+
".htm": "text/html; charset=utf-8",
|
|
21
|
+
".css": "text/css; charset=utf-8",
|
|
22
|
+
".js": "text/javascript; charset=utf-8",
|
|
23
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
24
|
+
".cjs": "text/javascript; charset=utf-8",
|
|
25
|
+
".json": "application/json; charset=utf-8",
|
|
26
|
+
".map": "application/json; charset=utf-8",
|
|
27
|
+
".webmanifest": "application/manifest+json",
|
|
28
|
+
".txt": "text/plain; charset=utf-8",
|
|
29
|
+
".md": "text/markdown; charset=utf-8",
|
|
30
|
+
".csv": "text/csv; charset=utf-8",
|
|
31
|
+
".xml": "application/xml",
|
|
32
|
+
".rss": "application/rss+xml",
|
|
33
|
+
".atom": "application/atom+xml",
|
|
34
|
+
".svg": "image/svg+xml",
|
|
35
|
+
".png": "image/png",
|
|
36
|
+
".jpg": "image/jpeg",
|
|
37
|
+
".jpeg": "image/jpeg",
|
|
38
|
+
".gif": "image/gif",
|
|
39
|
+
".webp": "image/webp",
|
|
40
|
+
".avif": "image/avif",
|
|
41
|
+
".ico": "image/x-icon",
|
|
42
|
+
".bmp": "image/bmp",
|
|
43
|
+
".woff": "font/woff",
|
|
44
|
+
".woff2": "font/woff2",
|
|
45
|
+
".ttf": "font/ttf",
|
|
46
|
+
".otf": "font/otf",
|
|
47
|
+
".wasm": "application/wasm",
|
|
48
|
+
".pdf": "application/pdf",
|
|
49
|
+
".zip": "application/zip",
|
|
50
|
+
".mp4": "video/mp4",
|
|
51
|
+
".webm": "video/webm",
|
|
52
|
+
".ogg": "audio/ogg",
|
|
53
|
+
".mp3": "audio/mpeg",
|
|
54
|
+
".wav": "audio/wav",
|
|
55
|
+
".m4a": "audio/mp4",
|
|
56
|
+
".vtt": "text/vtt; charset=utf-8",
|
|
57
|
+
});
|
|
58
|
+
/** The media type for a file path or name by its extension, case-insensitively. */
|
|
59
|
+
export function contentTypeOf(path) {
|
|
60
|
+
const dot = path.lastIndexOf(".");
|
|
61
|
+
// A dot before the last separator belongs to a directory; a leading dot names a dotfile, not a type.
|
|
62
|
+
const name = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1;
|
|
63
|
+
if (dot <= name)
|
|
64
|
+
return "application/octet-stream";
|
|
65
|
+
return CONTENT_TYPES[path.slice(dot).toLowerCase()] ?? "application/octet-stream";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether a relative path crosses a dot-segment other than `.well-known` (RFC 8615, public by
|
|
69
|
+
* definition). Serving, the build's `public/` copy and the parity listing all apply it, so an editor
|
|
70
|
+
* or VCS artifact (`.env`, `.git/`, `.DS_Store`) is neither served nor shipped. Both separators split:
|
|
71
|
+
* a request path uses `/`, a relative file path the OS separator.
|
|
72
|
+
*/
|
|
73
|
+
export function isHiddenPath(rel) {
|
|
74
|
+
for (const segment of rel.split(/[\\/]/)) {
|
|
75
|
+
if (segment.charCodeAt(0) === 46 /* '.' */ && segment !== ".well-known")
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
// A write sets a file's mtime and ctime together (a chmod or rename right after adds well under a
|
|
81
|
+
// millisecond), and Deno truncates the ctime it reports to the second. No tool can set a ctime, so a
|
|
82
|
+
// larger gap means the mtime came from somewhere else.
|
|
83
|
+
const MODIFIED_LAG_MS = 1000;
|
|
84
|
+
/**
|
|
85
|
+
* The time a file can be dated by in Last-Modified, or `undefined`: its mtime, only while that is
|
|
86
|
+
* the time of its own last change. A copy or build that keeps or normalizes timestamps - `cp -p`,
|
|
87
|
+
* `rsync -a`, tar, a container image layer, SOURCE_DATE_EPOCH, the Nix store's 1 s - gives a
|
|
88
|
+
* same-size replacement the old file's date, so its files are left undated and their content tag
|
|
89
|
+
* validates them alone.
|
|
90
|
+
*/
|
|
91
|
+
export function lastModifiedOf(version) {
|
|
92
|
+
const lag = Math.abs(version.ctimeMs - version.mtimeMs);
|
|
93
|
+
return version.mtimeMs > 0 && lag < MODIFIED_LAG_MS ? version.mtimeMs : undefined;
|
|
94
|
+
}
|
|
95
|
+
/** HTTP dates carry whole seconds; comparing raw milliseconds makes every file look stale. */
|
|
96
|
+
function seconds(time) {
|
|
97
|
+
return Math.floor(time / 1000);
|
|
98
|
+
}
|
|
99
|
+
function opaqueTag(tag) {
|
|
100
|
+
return tag.startsWith("W/") ? tag.slice(2) : tag;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether the client's copy is current, so the answer is a bodyless 304. `If-None-Match` (the header,
|
|
104
|
+
* or `null`) decides whenever it is sent (RFC 9110 section 13.2.2), by weak comparison with `etag`, and
|
|
105
|
+
* `*` matches any file. Otherwise an `If-Modified-Since` at or after `lastModified` (epoch
|
|
106
|
+
* milliseconds), compared at the whole seconds an HTTP date carries; an unparseable date, or a file
|
|
107
|
+
* with no date, is never current.
|
|
108
|
+
*/
|
|
109
|
+
export function isNotModified(ifNoneMatch, ifModifiedSince, etag, lastModified) {
|
|
110
|
+
if (ifNoneMatch !== null) {
|
|
111
|
+
const current = etag === undefined ? undefined : opaqueTag(etag);
|
|
112
|
+
for (const raw of ifNoneMatch.split(",")) {
|
|
113
|
+
const candidate = raw.trim();
|
|
114
|
+
if (candidate === "*" || (current !== undefined && opaqueTag(candidate) === current)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (ifModifiedSince === null || lastModified === undefined)
|
|
121
|
+
return false;
|
|
122
|
+
const since = Date.parse(ifModifiedSince);
|
|
123
|
+
return Number.isFinite(since) && seconds(since) >= seconds(lastModified);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Whether `If-Range` (the header, or `null`) lets a range request be answered partially; otherwise the
|
|
127
|
+
* whole file is the conformant answer, not a fallback. An entity tag must equal the file's strong
|
|
128
|
+
* `etag` (RFC 9110 section 13.1.5). A date must equal `lastModified` to the second, and counts only
|
|
129
|
+
* for a file with no strong tag: a client holding the tag sends it, and a date cannot tell apart two
|
|
130
|
+
* versions dated in the same second - here or on another instance - so resuming by one could splice
|
|
131
|
+
* them.
|
|
132
|
+
*/
|
|
133
|
+
export function ifRangeAllowsPartial(ifRange, etag, lastModified) {
|
|
134
|
+
if (ifRange === null)
|
|
135
|
+
return true;
|
|
136
|
+
const item = ifRange.trim();
|
|
137
|
+
const strong = etag !== undefined && !etag.startsWith("W/");
|
|
138
|
+
if (item.startsWith('"') || item.startsWith("W/"))
|
|
139
|
+
return strong && item === etag;
|
|
140
|
+
if (strong || lastModified === undefined)
|
|
141
|
+
return false;
|
|
142
|
+
const date = Date.parse(item);
|
|
143
|
+
return Number.isFinite(date) && seconds(date) === seconds(lastModified);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Largest file tagged by its content. Its first request waits for one read of the whole file, ~0.1 s
|
|
147
|
+
* per 256 MiB with hardware SHA-256 and several times that on a slow disk or CPU, so a larger file -
|
|
148
|
+
* a long video that is only ever fetched by ranges, say - is tagged by its device, inode, size and
|
|
149
|
+
* times. That tag is still strong and changes with any replacement, but differs between instances.
|
|
150
|
+
*/
|
|
151
|
+
export const CONTENT_TAG_MAX = 256 * 1024 * 1024;
|
|
152
|
+
const READ_CHUNK = 1024 * 1024;
|
|
153
|
+
// A file changed this recently can change again within its ctime's granularity (a jiffy on ext4, a
|
|
154
|
+
// second on HFS+ and as Deno reports it) without its stat showing it, so it is not tagged by version.
|
|
155
|
+
const RACY_MS = 2000;
|
|
156
|
+
function sameVersion(a, b) {
|
|
157
|
+
return (a.ctimeMs === b.ctimeMs &&
|
|
158
|
+
a.mtimeMs === b.mtimeMs &&
|
|
159
|
+
a.size === b.size &&
|
|
160
|
+
a.ino === b.ino &&
|
|
161
|
+
a.dev === b.dev);
|
|
162
|
+
}
|
|
163
|
+
function quoted(hash) {
|
|
164
|
+
return `"${hash.digest("base64url").slice(0, 22)}"`;
|
|
165
|
+
}
|
|
166
|
+
/** Tags from `createHash` - node:crypto's, which every runtime nifra serves files on provides. */
|
|
167
|
+
export function contentTags(createHash, options = {}) {
|
|
168
|
+
const limit = options.limit ?? 8192;
|
|
169
|
+
const now = options.now ?? Date.now;
|
|
170
|
+
const known = new Map();
|
|
171
|
+
const reading = new Map();
|
|
172
|
+
const remember = (path, version, tag) => {
|
|
173
|
+
if (!(now() - version.ctimeMs >= RACY_MS))
|
|
174
|
+
return;
|
|
175
|
+
const { dev, ino, size, mtimeMs, ctimeMs } = version;
|
|
176
|
+
known.delete(path);
|
|
177
|
+
known.set(path, { dev, ino, size, mtimeMs, ctimeMs, tag });
|
|
178
|
+
for (const earliest of known.keys()) {
|
|
179
|
+
if (known.size <= limit)
|
|
180
|
+
break;
|
|
181
|
+
known.delete(earliest);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const readWhole = async (path, version, read) => {
|
|
185
|
+
const hash = createHash("sha256");
|
|
186
|
+
const chunk = new Uint8Array(Math.min(version.size, READ_CHUNK));
|
|
187
|
+
let position = 0;
|
|
188
|
+
while (position < version.size) {
|
|
189
|
+
const count = await read(chunk.subarray(0, Math.min(chunk.length, version.size - position)), position);
|
|
190
|
+
if (count === 0)
|
|
191
|
+
break;
|
|
192
|
+
hash.update(chunk.subarray(0, count));
|
|
193
|
+
position += count;
|
|
194
|
+
}
|
|
195
|
+
const tag = quoted(hash);
|
|
196
|
+
if (position === version.size)
|
|
197
|
+
remember(path, version, tag);
|
|
198
|
+
return tag;
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
cached(path, version) {
|
|
202
|
+
const entry = known.get(path);
|
|
203
|
+
return entry !== undefined && sameVersion(entry, version) ? entry.tag : undefined;
|
|
204
|
+
},
|
|
205
|
+
ofBytes(path, version, bytes) {
|
|
206
|
+
const hash = createHash("sha256");
|
|
207
|
+
hash.update(bytes);
|
|
208
|
+
const tag = quoted(hash);
|
|
209
|
+
if (bytes.byteLength === version.size)
|
|
210
|
+
remember(path, version, tag);
|
|
211
|
+
return tag;
|
|
212
|
+
},
|
|
213
|
+
async ofFile(path, version, read) {
|
|
214
|
+
if (version.size > CONTENT_TAG_MAX) {
|
|
215
|
+
const { dev, ino, size, mtimeMs, ctimeMs } = version;
|
|
216
|
+
if (!Number.isFinite(ino) || !(now() - ctimeMs >= RACY_MS))
|
|
217
|
+
return undefined;
|
|
218
|
+
const hash = createHash("sha256");
|
|
219
|
+
hash.update(new TextEncoder().encode(`${dev}:${ino}:${size}:${mtimeMs}:${ctimeMs}`));
|
|
220
|
+
return quoted(hash);
|
|
221
|
+
}
|
|
222
|
+
const pending = reading.get(path);
|
|
223
|
+
if (pending !== undefined && sameVersion(pending.version, version))
|
|
224
|
+
return pending.tag;
|
|
225
|
+
const entry = { version, tag: readWhole(path, version, read) };
|
|
226
|
+
reading.set(path, entry);
|
|
227
|
+
try {
|
|
228
|
+
return await entry.tag;
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
// A read of a newer version may have replaced this entry; leave that one in place.
|
|
232
|
+
if (reading.get(path) === entry)
|
|
233
|
+
reading.delete(path);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=static-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-file.js","sourceRoot":"","sources":["../src/static-file.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Bun-native, contract-first HTTP framework - the router, server, and route descriptor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -200,6 +200,11 @@
|
|
|
200
200
|
"types": "./dist/range.d.ts",
|
|
201
201
|
"default": "./dist/range.js"
|
|
202
202
|
},
|
|
203
|
+
"./static-file": {
|
|
204
|
+
"bun": "./src/static-file.ts",
|
|
205
|
+
"types": "./dist/static-file.d.ts",
|
|
206
|
+
"default": "./dist/static-file.js"
|
|
207
|
+
},
|
|
203
208
|
"./contract": {
|
|
204
209
|
"bun": "./src/server/contract.ts",
|
|
205
210
|
"types": "./dist/server/contract.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
|
|
17
17
|
* `check:publish` asserts it equals `@nifrajs/core`'s package version.
|
|
18
18
|
*/
|
|
19
|
-
export const VERSION = "5.
|
|
19
|
+
export const VERSION = "5.4.0" as const
|
|
20
20
|
|
|
21
21
|
export type Version = typeof VERSION
|
|
22
22
|
|
package/src/range.ts
CHANGED
|
@@ -32,6 +32,63 @@ function validSize(size: number): void {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `bytes=<digits>-<digits>`, the one form clients send, answered with a single scan; `undefined`
|
|
37
|
+
* for anything else, which the general path below answers. The answers must stay the general path's:
|
|
38
|
+
* test/range.test.ts compares both against a frozen copy of it.
|
|
39
|
+
*/
|
|
40
|
+
function parseSingleRange(value: string, size: number): ByteRangeResult | undefined {
|
|
41
|
+
const length = value.length
|
|
42
|
+
// `| 0x20` folds only ASCII case, which is all the general path's /bytes/i matches.
|
|
43
|
+
if (
|
|
44
|
+
length < 7 ||
|
|
45
|
+
value.charCodeAt(5) !== 0x3d ||
|
|
46
|
+
(value.charCodeAt(0) | 0x20) !== 0x62 ||
|
|
47
|
+
(value.charCodeAt(1) | 0x20) !== 0x79 ||
|
|
48
|
+
(value.charCodeAt(2) | 0x20) !== 0x74 ||
|
|
49
|
+
(value.charCodeAt(3) | 0x20) !== 0x65 ||
|
|
50
|
+
(value.charCodeAt(4) | 0x20) !== 0x73
|
|
51
|
+
) {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
let dash = -1
|
|
55
|
+
for (let index = 6; index < length; index++) {
|
|
56
|
+
const code = value.charCodeAt(index)
|
|
57
|
+
if (code === 0x2d) {
|
|
58
|
+
if (dash !== -1) return undefined
|
|
59
|
+
dash = index
|
|
60
|
+
} else if (code < 0x30 || code > 0x39) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (dash === -1) return undefined
|
|
65
|
+
const leftDigits = dash - 6
|
|
66
|
+
const rightDigits = length - dash - 1
|
|
67
|
+
// INTEGER: no leading zero.
|
|
68
|
+
if (
|
|
69
|
+
(leftDigits > 1 && value.charCodeAt(6) === 0x30) ||
|
|
70
|
+
(rightDigits > 1 && value.charCodeAt(dash + 1) === 0x30)
|
|
71
|
+
) {
|
|
72
|
+
return { kind: "none" }
|
|
73
|
+
}
|
|
74
|
+
let start: number
|
|
75
|
+
let end: number
|
|
76
|
+
if (leftDigits === 0) {
|
|
77
|
+
// A suffix with no length, or a zero one, is ignored.
|
|
78
|
+
if (rightDigits === 0 || (rightDigits === 1 && value.charCodeAt(dash + 1) === 0x30)) {
|
|
79
|
+
return { kind: "none" }
|
|
80
|
+
}
|
|
81
|
+
const suffix = Number(value.slice(dash + 1))
|
|
82
|
+
start = suffix >= size ? 0 : size - suffix
|
|
83
|
+
end = size - 1
|
|
84
|
+
} else {
|
|
85
|
+
start = Number(value.slice(6, dash))
|
|
86
|
+
end = rightDigits === 0 ? size - 1 : Math.min(Number(value.slice(dash + 1)), size - 1)
|
|
87
|
+
}
|
|
88
|
+
if (start > end || start >= size || size === 0) return { kind: "unsatisfiable" }
|
|
89
|
+
return { kind: "satisfiable", ranges: [{ start, end }] }
|
|
90
|
+
}
|
|
91
|
+
|
|
35
92
|
/**
|
|
36
93
|
* Parse an HTTP `Range: bytes=...` header against a known representation size.
|
|
37
94
|
*
|
|
@@ -45,12 +102,14 @@ export function parseByteRange(value: string | null, size: number): ByteRangeRes
|
|
|
45
102
|
// Bound the input before the regex runs, not after: an oversized header must never reach the
|
|
46
103
|
// matcher in the first place.
|
|
47
104
|
if (value.length > MAX_RANGE_HEADER_LENGTH) return { kind: "none" }
|
|
105
|
+
const single = parseSingleRange(value, size)
|
|
106
|
+
if (single !== undefined) return single
|
|
48
107
|
const match = /^bytes\s*=\s*(.+)$/i.exec(value)
|
|
49
108
|
if (match === null) return { kind: "none" }
|
|
50
109
|
const source = match[1]!
|
|
51
110
|
let separators = 0
|
|
52
|
-
for (
|
|
53
|
-
if (
|
|
111
|
+
for (let comma = source.indexOf(","); comma !== -1; comma = source.indexOf(",", comma + 1)) {
|
|
112
|
+
if (++separators >= MAX_RANGE_SPECIFIERS) return { kind: "none" }
|
|
54
113
|
}
|
|
55
114
|
|
|
56
115
|
const ranges: ByteRange[] = []
|
|
@@ -23,7 +23,7 @@ if (!copies.includes(here) && copies.push(here) > 1) {
|
|
|
23
23
|
|
|
24
24
|
/** This copy's version: a literal like the package root's `VERSION` (no fs on the edge), which
|
|
25
25
|
* `scripts/version.ts` rewrites on every release and `check:publish` asserts. */
|
|
26
|
-
const COPY_VERSION = "5.
|
|
26
|
+
const COPY_VERSION = "5.4.0"
|
|
27
27
|
|
|
28
28
|
/** Record this copy's version beside its URL. Called by each `server()` behind the development guard,
|
|
29
29
|
* so a production bundle drops it with the version; a copy that built a server has recorded one. */
|
package/src/server/server.ts
CHANGED
|
@@ -861,6 +861,9 @@ interface HookAuditState {
|
|
|
861
861
|
checked: boolean
|
|
862
862
|
}
|
|
863
863
|
const hookAudit = /* @__PURE__ */ new WeakMap<object, HookAuditState>()
|
|
864
|
+
// Audits begun and not yet sealed. Zero outside development, so a request tests it before the
|
|
865
|
+
// NODE_ENV literal and never reads `process.env` (a native call of ~130 ns on Node).
|
|
866
|
+
let unsealedHookAudits = 0
|
|
864
867
|
|
|
865
868
|
/** Whether `from` reaches `target` through mounts that keep the path and take a request under `path`.
|
|
866
869
|
* Module-level, not a `Server` method, so a production bundle drops it with its dev-only caller. */
|
|
@@ -892,6 +895,7 @@ const beginHookAudit = (
|
|
|
892
895
|
policy: "warn" | "error" | "off",
|
|
893
896
|
hasCustomLogger: boolean,
|
|
894
897
|
): void => {
|
|
898
|
+
unsealedHookAudits++
|
|
895
899
|
hookAudit.set(key, {
|
|
896
900
|
policy,
|
|
897
901
|
hasCustomLogger,
|
|
@@ -1007,6 +1011,7 @@ const sealHookAudit = (
|
|
|
1007
1011
|
const state = hookAudit.get(key)
|
|
1008
1012
|
if (state === undefined || state.checked) return
|
|
1009
1013
|
state.checked = true
|
|
1014
|
+
unsealedHookAudits--
|
|
1010
1015
|
if (state.policy === "off" || total + sockets === 0) return
|
|
1011
1016
|
const own = total - state.merged
|
|
1012
1017
|
const dead = state.sites.filter(
|
|
@@ -3193,8 +3198,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3193
3198
|
platform?: Platform<EnvOf<Ctx>>,
|
|
3194
3199
|
): MaybePromise<Response> {
|
|
3195
3200
|
// Seal once on the first request so test servers (no `listen()`) still get the dead-hook check.
|
|
3196
|
-
// After that it is
|
|
3197
|
-
if (
|
|
3201
|
+
// After that it is one integer compare; the literal stays so a production define drops the branch.
|
|
3202
|
+
if (unsealedHookAudits !== 0 && process.env.NODE_ENV !== "production")
|
|
3198
3203
|
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger)
|
|
3199
3204
|
// Off path (default): straight through - one property check, no closure, no promise.
|
|
3200
3205
|
if (this.capacityGate === undefined) return this.fetchSourceInner(source, platform)
|
|
@@ -3378,7 +3383,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3378
3383
|
suppliedRuntime?: NodeOutcomeRuntime,
|
|
3379
3384
|
): MaybePromise<NodeServeOutcome> {
|
|
3380
3385
|
// Seal once on the Node-direct lane too - it can bypass `fetchSource` entirely (hookless outcome).
|
|
3381
|
-
if (
|
|
3386
|
+
if (unsealedHookAudits !== 0 && process.env.NODE_ENV !== "production")
|
|
3382
3387
|
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger)
|
|
3383
3388
|
// A paired header-only native hook can preserve the Node-direct JSON/body outcome for successful
|
|
3384
3389
|
// responses. Arbitrary onResponse transforms need a real Web Response, but a buffered outcome can
|