@bgord/bun 1.23.8 → 1.23.10
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/dist/cache-control-immutable.strategy.d.ts +8 -0
- package/dist/cache-control-immutable.strategy.d.ts.map +1 -0
- package/dist/cache-control-immutable.strategy.js +15 -0
- package/dist/cache-control-immutable.strategy.js.map +1 -0
- package/dist/cache-control-must-revalidate.strategy.d.ts +9 -0
- package/dist/cache-control-must-revalidate.strategy.d.ts.map +1 -0
- package/dist/cache-control-must-revalidate.strategy.js +10 -0
- package/dist/cache-control-must-revalidate.strategy.js.map +1 -0
- package/dist/cache-control-noop.strategy.d.ts +6 -0
- package/dist/cache-control-noop.strategy.d.ts.map +1 -0
- package/dist/cache-control-noop.strategy.js +6 -0
- package/dist/cache-control-noop.strategy.js.map +1 -0
- package/dist/cache-control.strategy.d.ts +5 -0
- package/dist/cache-control.strategy.d.ts.map +1 -0
- package/dist/cache-control.strategy.js +2 -0
- package/dist/cache-control.strategy.js.map +1 -0
- package/dist/cron-scheduler.adapter.d.ts.map +1 -1
- package/dist/cron-scheduler.adapter.js +2 -1
- package/dist/cron-scheduler.adapter.js.map +1 -1
- package/dist/cron-task-handler-bare.strategy.d.ts.map +1 -1
- package/dist/cron-task-handler-bare.strategy.js +4 -2
- package/dist/cron-task-handler-bare.strategy.js.map +1 -1
- package/dist/environment-loader-process-safe.adapter.d.ts +2 -12
- package/dist/environment-loader-process-safe.adapter.d.ts.map +1 -1
- package/dist/environment-loader-process-safe.adapter.js +2 -10
- package/dist/environment-loader-process-safe.adapter.js.map +1 -1
- package/dist/environment-loader-with-memo.adapter.d.ts +12 -0
- package/dist/environment-loader-with-memo.adapter.d.ts.map +1 -0
- package/dist/environment-loader-with-memo.adapter.js +17 -0
- package/dist/environment-loader-with-memo.adapter.js.map +1 -0
- package/dist/event-store-dispatching.adapter.d.ts.map +1 -1
- package/dist/event-store-dispatching.adapter.js +2 -1
- package/dist/event-store-dispatching.adapter.js.map +1 -1
- package/dist/hash-bytes-noop.strategy.d.ts +6 -0
- package/dist/hash-bytes-noop.strategy.d.ts.map +1 -0
- package/dist/hash-bytes-noop.strategy.js +7 -0
- package/dist/hash-bytes-noop.strategy.js.map +1 -0
- package/dist/hash-bytes-sha256.strategy.d.ts +6 -0
- package/dist/hash-bytes-sha256.strategy.d.ts.map +1 -0
- package/dist/hash-bytes-sha256.strategy.js +8 -0
- package/dist/hash-bytes-sha256.strategy.js.map +1 -0
- package/dist/hash-bytes.strategy.d.ts +5 -0
- package/dist/hash-bytes.strategy.d.ts.map +1 -0
- package/dist/hash-bytes.strategy.js +2 -0
- package/dist/hash-bytes.strategy.js.map +1 -0
- package/dist/hash-file-sha256.adapter.d.ts +4 -4
- package/dist/hash-file-sha256.adapter.d.ts.map +1 -1
- package/dist/hash-file-sha256.adapter.js +2 -2
- package/dist/hash-file-sha256.adapter.js.map +1 -1
- package/dist/idempotency-store-cache.adapter.d.ts.map +1 -1
- package/dist/idempotency-store-cache.adapter.js +1 -0
- package/dist/idempotency-store-cache.adapter.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/job-queue.adapter.d.ts.map +1 -1
- package/dist/job-queue.adapter.js +10 -3
- package/dist/job-queue.adapter.js.map +1 -1
- package/dist/markdown-generator.adapter.d.ts +1 -0
- package/dist/markdown-generator.adapter.d.ts.map +1 -1
- package/dist/markdown-generator.adapter.js +8 -1
- package/dist/markdown-generator.adapter.js.map +1 -1
- package/dist/redactor-key.vo.d.ts +8 -0
- package/dist/redactor-key.vo.d.ts.map +1 -0
- package/dist/redactor-key.vo.js +16 -0
- package/dist/redactor-key.vo.js.map +1 -0
- package/dist/redactor-mask.strategy.d.ts.map +1 -1
- package/dist/redactor-mask.strategy.js +7 -2
- package/dist/redactor-mask.strategy.js.map +1 -1
- package/dist/redactor-url-query.strategy.d.ts.map +1 -1
- package/dist/redactor-url-query.strategy.js +3 -2
- package/dist/redactor-url-query.strategy.js.map +1 -1
- package/dist/shield-body-limit.strategy.d.ts.map +1 -1
- package/dist/shield-body-limit.strategy.js +1 -0
- package/dist/shield-body-limit.strategy.js.map +1 -1
- package/dist/static-files-hono.service.d.ts +2 -6
- package/dist/static-files-hono.service.d.ts.map +1 -1
- package/dist/static-files-hono.service.js +12 -5
- package/dist/static-files-hono.service.js.map +1 -1
- package/dist/trailing-slash.middleware.d.ts +2 -2
- package/dist/trailing-slash.middleware.d.ts.map +1 -1
- package/dist/trailing-slash.middleware.js +5 -5
- package/dist/trailing-slash.middleware.js.map +1 -1
- package/package.json +4 -4
- package/readme.md +9 -0
- package/src/cache-control-immutable.strategy.ts +17 -0
- package/src/cache-control-must-revalidate.strategy.ts +11 -0
- package/src/cache-control-noop.strategy.ts +8 -0
- package/src/cache-control.strategy.ts +5 -0
- package/src/cron-scheduler.adapter.ts +2 -1
- package/src/cron-task-handler-bare.strategy.ts +5 -3
- package/src/environment-loader-process-safe.adapter.ts +2 -22
- package/src/environment-loader-with-memo.adapter.ts +21 -0
- package/src/event-store-dispatching.adapter.ts +2 -1
- package/src/hash-bytes-noop.strategy.ts +8 -0
- package/src/hash-bytes-sha256.strategy.ts +10 -0
- package/src/hash-bytes.strategy.ts +5 -0
- package/src/hash-file-sha256.adapter.ts +6 -6
- package/src/idempotency-store-cache.adapter.ts +1 -0
- package/src/index.ts +8 -0
- package/src/job-queue.adapter.ts +11 -3
- package/src/markdown-generator.adapter.ts +10 -1
- package/src/redactor-key.vo.ts +15 -0
- package/src/redactor-mask.strategy.ts +9 -2
- package/src/redactor-url-query.strategy.ts +5 -2
- package/src/shield-body-limit.strategy.ts +1 -0
- package/src/static-files-hono.service.ts +13 -13
- package/src/trailing-slash.middleware.ts +8 -10
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EnvironmentLoaderPort, EnvironmentResultType } from "./environment-loader.port";
|
|
2
|
+
|
|
3
|
+
type Config<T extends object> = { inner: EnvironmentLoaderPort<T> };
|
|
4
|
+
|
|
5
|
+
export class EnvironmentLoaderWithMemoAdapter<T extends object> implements EnvironmentLoaderPort<T> {
|
|
6
|
+
private memoized: Promise<Readonly<EnvironmentResultType<T>>> | null = null;
|
|
7
|
+
|
|
8
|
+
constructor(private readonly config: Config<T>) {}
|
|
9
|
+
|
|
10
|
+
async load(): Promise<Readonly<EnvironmentResultType<T>>> {
|
|
11
|
+
if (this.memoized === null) {
|
|
12
|
+
this.memoized = this.config.inner.load().catch((error) => {
|
|
13
|
+
this.memoized = null;
|
|
14
|
+
|
|
15
|
+
throw error;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return this.memoized;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -32,7 +32,8 @@ export class EventStoreDispatchingAdapter<Event extends GenericEvent> implements
|
|
|
32
32
|
): Promise<ReadonlyArray<SavedEvent>> {
|
|
33
33
|
const saved = await this.deps.inner.save(events);
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// Fire-and-forget: events are already persisted, handler errors are logged by the handler strategy.
|
|
36
|
+
for (const event of saved) this.deps.EventBus.emit(event).catch(() => {});
|
|
36
37
|
|
|
37
38
|
return saved;
|
|
38
39
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Hash } from "./hash.vo";
|
|
2
|
+
import type { HashBytesStrategy } from "./hash-bytes.strategy";
|
|
3
|
+
|
|
4
|
+
export class HashBytesNoopStrategy implements HashBytesStrategy {
|
|
5
|
+
async hash(_content: ArrayBuffer): Promise<Hash> {
|
|
6
|
+
return Hash.fromString("0".repeat(64));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Hash } from "./hash.vo";
|
|
2
|
+
import type { HashBytesStrategy } from "./hash-bytes.strategy";
|
|
3
|
+
|
|
4
|
+
export class HashBytesSha256Strategy implements HashBytesStrategy {
|
|
5
|
+
async hash(content: ArrayBuffer): Promise<Hash> {
|
|
6
|
+
const digest = await crypto.subtle.digest("SHA-256", content);
|
|
7
|
+
|
|
8
|
+
return Hash.fromBuffer(new Uint8Array(digest));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as tools from "@bgord/tools";
|
|
2
2
|
import type { FileInspectionPort } from "./file-inspection.port";
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
3
|
+
import type { FileReaderRawPort } from "./file-reader-raw.port";
|
|
4
|
+
import type { HashBytesStrategy } from "./hash-bytes.strategy";
|
|
5
5
|
import type { HashFilePort, HashFileResult } from "./hash-file.port";
|
|
6
6
|
|
|
7
7
|
type Dependencies = {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
HashBytes: HashBytesStrategy;
|
|
9
|
+
FileReaderRaw: FileReaderRawPort;
|
|
10
10
|
FileInspection: FileInspectionPort;
|
|
11
11
|
MimeRegistry: tools.MimeRegistry;
|
|
12
12
|
};
|
|
@@ -22,8 +22,8 @@ export class HashFileSha256Adapter implements HashFilePort {
|
|
|
22
22
|
|
|
23
23
|
const size = await this.deps.FileInspection.size(path);
|
|
24
24
|
const lastModified = await this.deps.FileInspection.lastModified(path);
|
|
25
|
-
const
|
|
25
|
+
const bytes = await this.deps.FileReaderRaw.read(path);
|
|
26
26
|
|
|
27
|
-
return { etag: await this.deps.
|
|
27
|
+
return { etag: await this.deps.HashBytes.hash(bytes), size, lastModified, mime };
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -7,6 +7,7 @@ type Dependencies = { CacheRepository: CacheRepositoryPort };
|
|
|
7
7
|
export class IdempotencyStoreCacheAdapter implements IdempotencyStorePort {
|
|
8
8
|
constructor(private readonly deps: Dependencies) {}
|
|
9
9
|
|
|
10
|
+
// Non-atomic get-then-set: concurrent claims of the same subject can both succeed
|
|
10
11
|
async claim(subject: Hash): Promise<boolean> {
|
|
11
12
|
const processed = await this.deps.CacheRepository.get(subject);
|
|
12
13
|
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,10 @@ export * from "./build-info.vo";
|
|
|
49
49
|
export * from "./cache-codec.strategy";
|
|
50
50
|
export * from "./cache-codec-identity.strategy";
|
|
51
51
|
export * from "./cache-codec-sitemap-entries.strategy";
|
|
52
|
+
export * from "./cache-control.strategy";
|
|
53
|
+
export * from "./cache-control-immutable.strategy";
|
|
54
|
+
export * from "./cache-control-must-revalidate.strategy";
|
|
55
|
+
export * from "./cache-control-noop.strategy";
|
|
52
56
|
export * from "./cache-file.service";
|
|
53
57
|
export * from "./cache-repository.port";
|
|
54
58
|
export * from "./cache-repository-node-cache.adapter";
|
|
@@ -115,6 +119,7 @@ export * from "./environment-loader-encrypted.adapter";
|
|
|
115
119
|
export * from "./environment-loader-noop.adapter";
|
|
116
120
|
export * from "./environment-loader-process.adapter";
|
|
117
121
|
export * from "./environment-loader-process-safe.adapter";
|
|
122
|
+
export * from "./environment-loader-with-memo.adapter";
|
|
118
123
|
export * from "./error.handler";
|
|
119
124
|
export * from "./error-classifier.strategy";
|
|
120
125
|
export * from "./error-classifier-http-exception-hono.strategy";
|
|
@@ -208,6 +213,9 @@ export * from "./handler-with-logger.strategy";
|
|
|
208
213
|
export * from "./handler-with-logger-safe.strategy";
|
|
209
214
|
export * from "./hash.vo";
|
|
210
215
|
export * from "./hash-bucket.vo";
|
|
216
|
+
export * from "./hash-bytes.strategy";
|
|
217
|
+
export * from "./hash-bytes-noop.strategy";
|
|
218
|
+
export * from "./hash-bytes-sha256.strategy";
|
|
211
219
|
export * from "./hash-content.strategy";
|
|
212
220
|
export * from "./hash-content-noop.strategy";
|
|
213
221
|
export * from "./hash-content-sha256.strategy";
|
package/src/job-queue.adapter.ts
CHANGED
|
@@ -34,10 +34,18 @@ export class JobQueueAdapter<Job extends GenericJob> implements JobQueuePort<Job
|
|
|
34
34
|
|
|
35
35
|
async claim(limit: tools.IntegerPositiveType): Promise<ReadonlyArray<Job>> {
|
|
36
36
|
const jobs = await this.config.claimer.claim(this.config.registry.names, limit);
|
|
37
|
+
const valid: Array<Job> = [];
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
for (const job of jobs) {
|
|
40
|
+
try {
|
|
41
|
+
valid.push(
|
|
42
|
+
this.config.registry.validate({ ...job, payload: this.config.serializer.deserialize(job.payload) }),
|
|
43
|
+
);
|
|
44
|
+
} catch {
|
|
45
|
+
await this.config.failer.fail(job.id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return valid;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
async complete(id: GenericJob["id"]): Promise<void> {
|
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
import type { MarkdownGeneratorPort } from "./markdown-generator.port";
|
|
3
3
|
|
|
4
4
|
export class MarkdownGeneratorAdapter implements MarkdownGeneratorPort {
|
|
5
|
+
private static readonly ALLOWED_SCHEMES = ["http", "https", "mailto"];
|
|
6
|
+
|
|
5
7
|
generate(template: string): string {
|
|
6
|
-
|
|
8
|
+
const html = Bun.markdown.html(template, { tagFilter: true, noHtmlBlocks: true, noHtmlSpans: true });
|
|
9
|
+
|
|
10
|
+
return html.replace(/ href="([^"]*)"/g, (attribute, url: string) => {
|
|
11
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(url)?.[1]?.toLowerCase();
|
|
12
|
+
|
|
13
|
+
if (scheme === undefined || MarkdownGeneratorAdapter.ALLOWED_SCHEMES.includes(scheme)) return attribute;
|
|
14
|
+
return ' href="#"';
|
|
15
|
+
});
|
|
7
16
|
}
|
|
8
17
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class RedactorKey {
|
|
2
|
+
private constructor(private readonly value: string) {}
|
|
3
|
+
|
|
4
|
+
static fromString(candidate: string): RedactorKey {
|
|
5
|
+
return new RedactorKey(candidate.toLowerCase().replace(/[_-]/g, ""));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get(): string {
|
|
9
|
+
return this.value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
matches(another: RedactorKey): boolean {
|
|
13
|
+
return this.value === another.value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
/* cspell:disable */
|
|
2
2
|
import * as tools from "@bgord/tools";
|
|
3
3
|
import type { RedactorStrategy } from "./redactor.strategy";
|
|
4
|
+
import { RedactorKey } from "./redactor-key.vo";
|
|
4
5
|
|
|
5
6
|
export class RedactorMask implements RedactorStrategy {
|
|
6
7
|
static readonly DEFAULT_KEYS: ReadonlyArray<string> = [
|
|
7
8
|
"authorization",
|
|
9
|
+
"proxy-authorization",
|
|
8
10
|
"cookie",
|
|
9
11
|
"set-cookie",
|
|
10
12
|
"x-api-key",
|
|
13
|
+
"x-auth-token",
|
|
11
14
|
"apikey",
|
|
12
15
|
"token",
|
|
13
16
|
"accesstoken",
|
|
14
17
|
"refreshtoken",
|
|
18
|
+
"idtoken",
|
|
15
19
|
"password",
|
|
16
20
|
"currentpassword",
|
|
17
21
|
"newpassword",
|
|
18
22
|
"passwordconfirmation",
|
|
19
23
|
"clientsecret",
|
|
24
|
+
"privatekey",
|
|
20
25
|
"secret",
|
|
21
26
|
"otp",
|
|
22
27
|
"code",
|
|
@@ -25,12 +30,14 @@ export class RedactorMask implements RedactorStrategy {
|
|
|
25
30
|
private readonly keys: Set<string>;
|
|
26
31
|
|
|
27
32
|
constructor(keys?: ReadonlyArray<string>) {
|
|
28
|
-
this.keys = new Set(
|
|
33
|
+
this.keys = new Set(
|
|
34
|
+
(keys?.length ? keys : RedactorMask.DEFAULT_KEYS).map((key) => RedactorKey.fromString(key).get()),
|
|
35
|
+
);
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
redact<T>(input: T): T {
|
|
32
39
|
return tools.deepCloneWith(input, (_value, key) =>
|
|
33
|
-
typeof key === "string" && this.keys.has(key.
|
|
40
|
+
typeof key === "string" && this.keys.has(RedactorKey.fromString(key).get()) ? "***" : undefined,
|
|
34
41
|
);
|
|
35
42
|
}
|
|
36
43
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import * as tools from "@bgord/tools";
|
|
2
2
|
import type { RedactorStrategy } from "./redactor.strategy";
|
|
3
|
+
import { RedactorKey } from "./redactor-key.vo";
|
|
3
4
|
import { RedactorMask } from "./redactor-mask.strategy";
|
|
4
5
|
|
|
5
6
|
export class RedactorUrlQuery implements RedactorStrategy {
|
|
6
7
|
private readonly keys: Set<string>;
|
|
7
8
|
|
|
8
9
|
constructor(keys?: ReadonlyArray<string>) {
|
|
9
|
-
this.keys = new Set(
|
|
10
|
+
this.keys = new Set(
|
|
11
|
+
(keys?.length ? keys : RedactorMask.DEFAULT_KEYS).map((key) => RedactorKey.fromString(key).get()),
|
|
12
|
+
);
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
redact<T>(input: T): T {
|
|
@@ -24,7 +27,7 @@ export class RedactorUrlQuery implements RedactorStrategy {
|
|
|
24
27
|
const masked = new URLSearchParams(
|
|
25
28
|
Array.from(new URLSearchParams(query), ([key, value]) => [
|
|
26
29
|
key,
|
|
27
|
-
this.keys.has(key.
|
|
30
|
+
this.keys.has(RedactorKey.fromString(key).get()) ? "***" : value,
|
|
28
31
|
]),
|
|
29
32
|
);
|
|
30
33
|
|
|
@@ -12,6 +12,7 @@ export class ShieldBodyLimitStrategy {
|
|
|
12
12
|
evaluate(context: HasRequestHeader): boolean {
|
|
13
13
|
const header = context.request.header("content-length");
|
|
14
14
|
|
|
15
|
+
// Header-only check: bodies without Content-Length (chunked/streamed) are bounded by Bun.serve's maxRequestBodySize instead.
|
|
15
16
|
if (header === undefined) return true;
|
|
16
17
|
|
|
17
18
|
const contentLength = v.safeParse(tools.SizeBytes, Number(header));
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
// [BUN DEPENDENCY]
|
|
2
2
|
// cspell:ignore nosniff
|
|
3
|
-
import
|
|
4
|
-
import { type Context, Hono } from "hono";
|
|
3
|
+
import { Hono } from "hono";
|
|
5
4
|
import { serveStatic } from "hono/bun";
|
|
6
5
|
import { etag } from "hono/etag";
|
|
7
6
|
import { secureHeaders } from "hono/secure-headers";
|
|
7
|
+
import type { CacheControlStrategy } from "./cache-control.strategy";
|
|
8
|
+
import { RequestContextHonoAdapter } from "./request-context-hono.adapter";
|
|
8
9
|
|
|
9
10
|
const noop = async () => {};
|
|
10
11
|
|
|
11
|
-
type StaticFilesStrategy = (path: string, context: Context) => Promise<void> | void;
|
|
12
|
-
|
|
13
|
-
export const StaticFileStrategyNoop: StaticFilesStrategy = () => {};
|
|
14
|
-
|
|
15
|
-
export const StaticFileStrategyMustRevalidate: (duration: tools.Duration) => StaticFilesStrategy =
|
|
16
|
-
(duration) => (_, c) => {
|
|
17
|
-
c.header("Cache-Control", `public, max-age=${duration.seconds}, must-revalidate`);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
12
|
const staticAssetHeaders = secureHeaders({
|
|
21
13
|
crossOriginResourcePolicy: "same-origin",
|
|
22
14
|
crossOriginOpenerPolicy: "same-origin",
|
|
@@ -61,7 +53,7 @@ const staticDocumentHeaders = secureHeaders({
|
|
|
61
53
|
type StaticFilesOptions = { root?: string };
|
|
62
54
|
|
|
63
55
|
export class StaticFilesHono {
|
|
64
|
-
static handle(path: string, strategy:
|
|
56
|
+
static handle(path: string, strategy: CacheControlStrategy, options?: StaticFilesOptions) {
|
|
65
57
|
// Stryker disable all
|
|
66
58
|
const root = options?.root ?? "./";
|
|
67
59
|
// Stryker restore all
|
|
@@ -78,7 +70,15 @@ export class StaticFilesHono {
|
|
|
78
70
|
else await staticAssetHeaders(context, noop);
|
|
79
71
|
},
|
|
80
72
|
etag(),
|
|
81
|
-
serveStatic({
|
|
73
|
+
serveStatic({
|
|
74
|
+
root,
|
|
75
|
+
precompressed: true,
|
|
76
|
+
onFound: (_, context) => {
|
|
77
|
+
const value = strategy.resolve(new RequestContextHonoAdapter(context));
|
|
78
|
+
|
|
79
|
+
if (value) context.header("Cache-Control", value);
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
82
|
).fetch,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HasRequestUrl } from "./request-context.port";
|
|
2
2
|
|
|
3
3
|
export type TrailingSlashResult = { redirect: false } | { redirect: true; pathname: string; status: 308 };
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const LEADING_SLASHES = /^\/+/;
|
|
5
|
+
const EDGE_SLASHES = /^\/+|\/+$/g;
|
|
7
6
|
|
|
8
7
|
export class TrailingSlashMiddleware {
|
|
9
8
|
// RFC 7231 6.4.7 - 308 preserves the request method and body, unlike 301.
|
|
10
9
|
static readonly STATUS = 308;
|
|
11
10
|
|
|
12
|
-
evaluate(context:
|
|
13
|
-
|
|
14
|
-
if (context.request.path === "/") return { redirect: false };
|
|
11
|
+
evaluate(context: HasRequestUrl): TrailingSlashResult {
|
|
12
|
+
const path = new URL(context.request.url()).pathname;
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
14
|
+
if (!path.endsWith("/")) return { redirect: false };
|
|
15
|
+
if (path === "/") return { redirect: false };
|
|
16
|
+
|
|
17
|
+
const pathname = `/${path.replace(EDGE_SLASHES, "")}`;
|
|
20
18
|
|
|
21
19
|
return { redirect: true, pathname, status: TrailingSlashMiddleware.STATUS };
|
|
22
20
|
}
|