@bgord/bun 1.23.12 → 1.24.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/dist/atomic-file-writer-collecting.adapter.d.ts +8 -0
- package/dist/atomic-file-writer-collecting.adapter.d.ts.map +1 -0
- package/dist/atomic-file-writer-collecting.adapter.js +7 -0
- package/dist/atomic-file-writer-collecting.adapter.js.map +1 -0
- package/dist/atomic-file-writer-noop.adapter.d.ts +7 -0
- package/dist/atomic-file-writer-noop.adapter.d.ts.map +1 -0
- package/dist/atomic-file-writer-noop.adapter.js +4 -0
- package/dist/atomic-file-writer-noop.adapter.js.map +1 -0
- package/dist/atomic-file-writer.adapter.d.ts +19 -0
- package/dist/atomic-file-writer.adapter.d.ts.map +1 -0
- package/dist/atomic-file-writer.adapter.js +18 -0
- package/dist/atomic-file-writer.adapter.js.map +1 -0
- package/dist/atomic-file-writer.port.d.ts +6 -0
- package/dist/atomic-file-writer.port.d.ts.map +1 -0
- package/dist/atomic-file-writer.port.js +2 -0
- package/dist/atomic-file-writer.port.js.map +1 -0
- package/dist/encryption-aes-gcm.adapter.d.ts +2 -2
- package/dist/encryption-aes-gcm.adapter.d.ts.map +1 -1
- package/dist/encryption-aes-gcm.adapter.js +2 -2
- package/dist/encryption-aes-gcm.adapter.js.map +1 -1
- package/dist/event-stream-accept.service.d.ts +4 -0
- package/dist/event-stream-accept.service.d.ts.map +1 -0
- package/dist/event-stream-accept.service.js +11 -0
- package/dist/event-stream-accept.service.js.map +1 -0
- package/dist/gzip.adapter.d.ts +2 -2
- package/dist/gzip.adapter.d.ts.map +1 -1
- package/dist/gzip.adapter.js +1 -1
- package/dist/gzip.adapter.js.map +1 -1
- package/dist/healthcheck.handler.d.ts +4 -2
- package/dist/healthcheck.handler.d.ts.map +1 -1
- package/dist/healthcheck.handler.js +4 -11
- package/dist/healthcheck.handler.js.map +1 -1
- package/dist/http-exception-errors.js +2 -2
- package/dist/http-exception-errors.js.map +1 -1
- package/dist/http-logger-hono.middleware.d.ts +1 -0
- package/dist/http-logger-hono.middleware.d.ts.map +1 -1
- package/dist/http-logger-hono.middleware.js +10 -6
- package/dist/http-logger-hono.middleware.js.map +1 -1
- package/dist/http-logger.middleware.d.ts.map +1 -1
- package/dist/http-logger.middleware.js +4 -6
- package/dist/http-logger.middleware.js.map +1 -1
- package/dist/image-blur.adapter.d.ts +2 -6
- package/dist/image-blur.adapter.d.ts.map +1 -1
- package/dist/image-blur.adapter.js +1 -4
- package/dist/image-blur.adapter.js.map +1 -1
- package/dist/image-compressor.adapter.d.ts +2 -6
- package/dist/image-compressor.adapter.d.ts.map +1 -1
- package/dist/image-compressor.adapter.js +1 -4
- package/dist/image-compressor.adapter.js.map +1 -1
- package/dist/image-exif-clear.adapter.d.ts +2 -6
- package/dist/image-exif-clear.adapter.d.ts.map +1 -1
- package/dist/image-exif-clear.adapter.js +1 -3
- package/dist/image-exif-clear.adapter.js.map +1 -1
- package/dist/image-formatter.adapter.d.ts +2 -6
- package/dist/image-formatter.adapter.d.ts.map +1 -1
- package/dist/image-formatter.adapter.js +1 -3
- package/dist/image-formatter.adapter.js.map +1 -1
- package/dist/image-grayscale.adapter.d.ts +2 -6
- package/dist/image-grayscale.adapter.d.ts.map +1 -1
- package/dist/image-grayscale.adapter.js +1 -4
- package/dist/image-grayscale.adapter.js.map +1 -1
- package/dist/image-processor.adapter.d.ts +2 -6
- package/dist/image-processor.adapter.d.ts.map +1 -1
- package/dist/image-processor.adapter.js +1 -3
- package/dist/image-processor.adapter.js.map +1 -1
- package/dist/image-resizer.adapter.d.ts +2 -6
- package/dist/image-resizer.adapter.d.ts.map +1 -1
- package/dist/image-resizer.adapter.js +1 -4
- package/dist/image-resizer.adapter.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/job-pruner-sqlite.adapter.d.ts.map +1 -1
- package/dist/job-pruner-sqlite.adapter.js +2 -1
- package/dist/job-pruner-sqlite.adapter.js.map +1 -1
- package/dist/job-queue-stats-provider-sqlite.adapter.js +4 -4
- package/dist/job-queue-stats-provider-sqlite.adapter.js.map +1 -1
- package/dist/job-recoverer-collecting.adapter.d.ts +7 -0
- package/dist/job-recoverer-collecting.adapter.d.ts.map +1 -0
- package/dist/job-recoverer-collecting.adapter.js +10 -0
- package/dist/job-recoverer-collecting.adapter.js.map +1 -0
- package/dist/job-recoverer-noop.adapter.d.ts +6 -0
- package/dist/job-recoverer-noop.adapter.d.ts.map +1 -0
- package/dist/job-recoverer-noop.adapter.js +7 -0
- package/dist/job-recoverer-noop.adapter.js.map +1 -0
- package/dist/job-recoverer-sqlite.adapter.d.ts +13 -0
- package/dist/job-recoverer-sqlite.adapter.d.ts.map +1 -0
- package/dist/job-recoverer-sqlite.adapter.js +14 -0
- package/dist/job-recoverer-sqlite.adapter.js.map +1 -0
- package/dist/job-recoverer.port.d.ts +5 -0
- package/dist/job-recoverer.port.d.ts.map +1 -0
- package/dist/job-recoverer.port.js +2 -0
- package/dist/job-recoverer.port.js.map +1 -0
- package/dist/memory-consumption.service.d.ts +1 -2
- package/dist/memory-consumption.service.d.ts.map +1 -1
- package/dist/memory-consumption.service.js.map +1 -1
- package/dist/path-matcher.service.d.ts +6 -0
- package/dist/path-matcher.service.d.ts.map +1 -0
- package/dist/path-matcher.service.js +13 -0
- package/dist/path-matcher.service.js.map +1 -0
- package/dist/remote-file-storage-disk.adapter.d.ts +2 -6
- package/dist/remote-file-storage-disk.adapter.d.ts.map +1 -1
- package/dist/remote-file-storage-disk.adapter.js +1 -9
- package/dist/remote-file-storage-disk.adapter.js.map +1 -1
- package/dist/request-context-hono.adapter.d.ts +1 -1
- package/dist/request-context-hono.adapter.d.ts.map +1 -1
- package/dist/request-context-hono.adapter.js +3 -3
- package/dist/request-context-hono.adapter.js.map +1 -1
- package/dist/request-context.port.d.ts +4 -0
- package/dist/request-context.port.d.ts.map +1 -1
- package/dist/request-context.port.js +4 -1
- package/dist/request-context.port.js.map +1 -1
- package/dist/runtime-stats-provider-noop.adapter.d.ts +5 -0
- package/dist/runtime-stats-provider-noop.adapter.d.ts.map +1 -0
- package/dist/runtime-stats-provider-noop.adapter.js +14 -0
- package/dist/runtime-stats-provider-noop.adapter.js.map +1 -0
- package/dist/runtime-stats-provider-system.adapter.d.ts +12 -0
- package/dist/runtime-stats-provider-system.adapter.d.ts.map +1 -0
- package/dist/runtime-stats-provider-system.adapter.js +21 -0
- package/dist/runtime-stats-provider-system.adapter.js.map +1 -0
- package/dist/runtime-stats-provider.port.d.ts +18 -0
- package/dist/runtime-stats-provider.port.d.ts.map +1 -0
- package/dist/runtime-stats-provider.port.js +2 -0
- package/dist/runtime-stats-provider.port.js.map +1 -0
- package/dist/security-rule-violation-threshold.strategy.js +1 -1
- package/dist/security-rule-violation-threshold.strategy.js.map +1 -1
- package/dist/setup-hono.service.js +1 -1
- package/dist/setup-hono.service.js.map +1 -1
- package/dist/shield-auth-hono.strategy.d.ts.map +1 -1
- package/dist/shield-auth-hono.strategy.js +4 -3
- package/dist/shield-auth-hono.strategy.js.map +1 -1
- package/dist/shield-auth.strategy.d.ts +1 -5
- package/dist/shield-auth.strategy.d.ts.map +1 -1
- package/dist/shield-auth.strategy.js +3 -6
- package/dist/shield-auth.strategy.js.map +1 -1
- package/dist/shield-maintenance.strategy.d.ts.map +1 -1
- package/dist/shield-maintenance.strategy.js +2 -5
- package/dist/shield-maintenance.strategy.js.map +1 -1
- package/dist/temporary-file-absolute.adapter.d.ts +2 -6
- package/dist/temporary-file-absolute.adapter.d.ts.map +1 -1
- package/dist/temporary-file-absolute.adapter.js +1 -9
- package/dist/temporary-file-absolute.adapter.js.map +1 -1
- package/dist/timing.middleware.d.ts +0 -1
- package/dist/timing.middleware.d.ts.map +1 -1
- package/dist/timing.middleware.js +2 -9
- package/dist/timing.middleware.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +13 -0
- package/src/atomic-file-writer-collecting.adapter.ts +14 -0
- package/src/atomic-file-writer-noop.adapter.ts +10 -0
- package/src/atomic-file-writer.adapter.ts +34 -0
- package/src/atomic-file-writer.port.ts +6 -0
- package/src/encryption-aes-gcm.adapter.ts +4 -4
- package/src/event-stream-accept.service.ts +10 -0
- package/src/gzip.adapter.ts +3 -3
- package/src/healthcheck.handler.ts +8 -11
- package/src/http-exception-errors.ts +2 -2
- package/src/http-logger-hono.middleware.ts +12 -6
- package/src/http-logger.middleware.ts +4 -7
- package/src/image-blur.adapter.ts +3 -13
- package/src/image-compressor.adapter.ts +3 -12
- package/src/image-exif-clear.adapter.ts +3 -11
- package/src/image-formatter.adapter.ts +3 -12
- package/src/image-grayscale.adapter.ts +3 -13
- package/src/image-processor.adapter.ts +3 -11
- package/src/image-resizer.adapter.ts +3 -12
- package/src/index.ts +11 -0
- package/src/job-pruner-sqlite.adapter.ts +2 -1
- package/src/job-queue-stats-provider-sqlite.adapter.ts +4 -4
- package/src/job-recoverer-collecting.adapter.ts +14 -0
- package/src/job-recoverer-noop.adapter.ts +8 -0
- package/src/job-recoverer-sqlite.adapter.ts +20 -0
- package/src/job-recoverer.port.ts +5 -0
- package/src/memory-consumption.service.ts +4 -1
- package/src/path-matcher.service.ts +14 -0
- package/src/remote-file-storage-disk.adapter.ts +3 -17
- package/src/request-context-hono.adapter.ts +3 -4
- package/src/request-context.port.ts +5 -0
- package/src/runtime-stats-provider-noop.adapter.ts +16 -0
- package/src/runtime-stats-provider-system.adapter.ts +24 -0
- package/src/runtime-stats-provider.port.ts +16 -0
- package/src/security-rule-violation-threshold.strategy.ts +2 -2
- package/src/setup-hono.service.ts +1 -1
- package/src/shield-auth-hono.strategy.ts +4 -3
- package/src/shield-auth.strategy.ts +3 -8
- package/src/shield-maintenance.strategy.ts +2 -6
- package/src/temporary-file-absolute.adapter.ts +3 -18
- package/src/timing.middleware.ts +2 -10
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
// [BUN DEPENDENCY]
|
|
2
2
|
// cspell:ignore Resizer
|
|
3
3
|
import type * as tools from "@bgord/tools";
|
|
4
|
-
import type {
|
|
5
|
-
import type { FileWriterPort } from "./file-writer.port";
|
|
4
|
+
import type { AtomicFileWriterPort } from "./atomic-file-writer.port";
|
|
6
5
|
import type { ImageSupportedType } from "./image.types";
|
|
7
6
|
import type { ImageResizerPort, ImageResizerStrategy } from "./image-resizer.port";
|
|
8
|
-
import type { NonceProviderPort } from "./nonce-provider.port";
|
|
9
7
|
|
|
10
8
|
type Dependencies = {
|
|
11
|
-
|
|
12
|
-
FileRenamer: FileRenamerPort;
|
|
13
|
-
NonceProvider: NonceProviderPort;
|
|
9
|
+
AtomicFileWriter: AtomicFileWriterPort;
|
|
14
10
|
};
|
|
15
11
|
|
|
16
12
|
export class ImageResizerAdapter implements ImageResizerPort {
|
|
@@ -18,10 +14,6 @@ export class ImageResizerAdapter implements ImageResizerPort {
|
|
|
18
14
|
|
|
19
15
|
async resize(recipe: ImageResizerStrategy): Promise<tools.FilePathRelative | tools.FilePathAbsolute> {
|
|
20
16
|
const final = recipe.strategy === "output_path" ? recipe.output : recipe.input;
|
|
21
|
-
const filename = final.getFilename();
|
|
22
|
-
const temporary = final.withFilename(
|
|
23
|
-
filename.withSuffix(`-resized-${this.deps.NonceProvider.generate()}`),
|
|
24
|
-
);
|
|
25
17
|
|
|
26
18
|
const extension = final.getFilename().getExtension();
|
|
27
19
|
const format = (extension === "jpg" ? "jpeg" : extension) as ImageSupportedType;
|
|
@@ -32,8 +24,7 @@ export class ImageResizerAdapter implements ImageResizerPort {
|
|
|
32
24
|
[format]()
|
|
33
25
|
.bytes();
|
|
34
26
|
|
|
35
|
-
await this.deps.
|
|
36
|
-
await this.deps.FileRenamer.rename(temporary, final);
|
|
27
|
+
await this.deps.AtomicFileWriter.write(final, resized);
|
|
37
28
|
|
|
38
29
|
return final;
|
|
39
30
|
}
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,10 @@ export * from "./antivirus-with-semaphore.adapter";
|
|
|
36
36
|
export * from "./antivirus-with-timeout.adapter";
|
|
37
37
|
export * from "./api-version.middleware";
|
|
38
38
|
export * from "./api-version-hono.middleware";
|
|
39
|
+
export * from "./atomic-file-writer.adapter";
|
|
40
|
+
export * from "./atomic-file-writer.port";
|
|
41
|
+
export * from "./atomic-file-writer-collecting.adapter";
|
|
42
|
+
export * from "./atomic-file-writer-noop.adapter";
|
|
39
43
|
export * from "./auth-session-reader.port";
|
|
40
44
|
export * from "./auth-session-reader-better-auth.adapter";
|
|
41
45
|
export * from "./auth-session-reader-noop.adapter";
|
|
@@ -311,6 +315,10 @@ export * from "./job-queue-stats-provider-noop.adapter";
|
|
|
311
315
|
export * from "./job-queue-stats-provider-sqlite.adapter";
|
|
312
316
|
export * from "./job-queue-with-logger.adapter";
|
|
313
317
|
export * from "./job-queue-worker.service";
|
|
318
|
+
export * from "./job-recoverer.port";
|
|
319
|
+
export * from "./job-recoverer-collecting.adapter";
|
|
320
|
+
export * from "./job-recoverer-noop.adapter";
|
|
321
|
+
export * from "./job-recoverer-sqlite.adapter";
|
|
314
322
|
export * from "./job-registry.adapter";
|
|
315
323
|
export * from "./job-registry.port";
|
|
316
324
|
export * from "./job-requeuer.port";
|
|
@@ -441,6 +449,9 @@ export * from "./retry-backoff-exponential.strategy";
|
|
|
441
449
|
export * from "./retry-backoff-jitter.strategy";
|
|
442
450
|
export * from "./retry-backoff-linear.strategy";
|
|
443
451
|
export * from "./retry-backoff-noop.strategy";
|
|
452
|
+
export * from "./runtime-stats-provider.port";
|
|
453
|
+
export * from "./runtime-stats-provider-noop.adapter";
|
|
454
|
+
export * from "./runtime-stats-provider-system.adapter";
|
|
444
455
|
export * from "./sealer.port";
|
|
445
456
|
export * from "./sealer-aes-gcm.adapter";
|
|
446
457
|
export * from "./sealer-noop.adapter";
|
|
@@ -3,6 +3,7 @@ import type { Database } from "bun:sqlite";
|
|
|
3
3
|
import * as tools from "@bgord/tools";
|
|
4
4
|
import type { ClockPort } from "./clock.port";
|
|
5
5
|
import type { JobPrunerPort } from "./job-pruner.port";
|
|
6
|
+
import { JobStatusEnum } from "./job-status.vo";
|
|
6
7
|
|
|
7
8
|
type Dependencies = { db: Database; Clock: ClockPort };
|
|
8
9
|
|
|
@@ -14,7 +15,7 @@ export class JobPrunerSqliteAdapter implements JobPrunerPort {
|
|
|
14
15
|
|
|
15
16
|
const result = this.deps.db.run<[tools.TimestampValueType]>(
|
|
16
17
|
`DELETE FROM jobs
|
|
17
|
-
WHERE status IN ('completed', 'failed')
|
|
18
|
+
WHERE status IN ('${JobStatusEnum.completed}', '${JobStatusEnum.failed}')
|
|
18
19
|
AND createdAt <= ?`,
|
|
19
20
|
[threshold],
|
|
20
21
|
);
|
|
@@ -25,10 +25,10 @@ export class JobQueueStatsProviderSqliteAdapter implements JobQueueStatsProvider
|
|
|
25
25
|
const counts = Object.fromEntries(snapshot.map((row) => [row.status, row.count]));
|
|
26
26
|
|
|
27
27
|
return {
|
|
28
|
-
pending: tools.Int.nonNegative(counts[
|
|
29
|
-
claimed: tools.Int.nonNegative(counts[
|
|
30
|
-
completed: tools.Int.nonNegative(counts[
|
|
31
|
-
failed: tools.Int.nonNegative(counts[
|
|
28
|
+
pending: tools.Int.nonNegative(counts[JobStatusEnum.pending] ?? 0),
|
|
29
|
+
claimed: tools.Int.nonNegative(counts[JobStatusEnum.claimed] ?? 0),
|
|
30
|
+
completed: tools.Int.nonNegative(counts[JobStatusEnum.completed] ?? 0),
|
|
31
|
+
failed: tools.Int.nonNegative(counts[JobStatusEnum.failed] ?? 0),
|
|
32
32
|
oldestPending: oldestPending?.createdAt ? tools.Timestamp.fromNumber(oldestPending.createdAt).ms : null,
|
|
33
33
|
};
|
|
34
34
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as tools from "@bgord/tools";
|
|
2
|
+
import type { JobRecovererPort } from "./job-recoverer.port";
|
|
3
|
+
|
|
4
|
+
export class JobRecovererCollectingAdapter implements JobRecovererPort {
|
|
5
|
+
readonly recovered: Array<tools.IntegerNonNegativeType> = [];
|
|
6
|
+
|
|
7
|
+
async recover(): Promise<tools.IntegerNonNegativeType> {
|
|
8
|
+
const count = tools.Int.nonNegative(0);
|
|
9
|
+
|
|
10
|
+
this.recovered.push(count);
|
|
11
|
+
|
|
12
|
+
return count;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as tools from "@bgord/tools";
|
|
2
|
+
import type { JobRecovererPort } from "./job-recoverer.port";
|
|
3
|
+
|
|
4
|
+
export class JobRecovererNoopAdapter implements JobRecovererPort {
|
|
5
|
+
async recover(): Promise<tools.IntegerNonNegativeType> {
|
|
6
|
+
return tools.Int.nonNegative(0);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// [BUN DEPENDENCY]
|
|
2
|
+
import type { Database } from "bun:sqlite";
|
|
3
|
+
import * as tools from "@bgord/tools";
|
|
4
|
+
import type { JobRecovererPort } from "./job-recoverer.port";
|
|
5
|
+
import { JobStatusEnum } from "./job-status.vo";
|
|
6
|
+
|
|
7
|
+
type Dependencies = { db: Database };
|
|
8
|
+
|
|
9
|
+
// Single-process only: at boot, every claimed job was left behind by a dead process.
|
|
10
|
+
export class JobRecovererSqliteAdapter implements JobRecovererPort {
|
|
11
|
+
constructor(private readonly deps: Dependencies) {}
|
|
12
|
+
|
|
13
|
+
async recover(): Promise<tools.IntegerNonNegativeType> {
|
|
14
|
+
const result = this.deps.db.run(
|
|
15
|
+
`UPDATE jobs SET status = '${JobStatusEnum.pending}' WHERE status = '${JobStatusEnum.claimed}'`,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return tools.Int.nonNegative(result.changes);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as tools from "@bgord/tools";
|
|
2
2
|
|
|
3
|
-
type MemoryConsumptionSnapshotType = {
|
|
3
|
+
export type MemoryConsumptionSnapshotType = {
|
|
4
|
+
total: tools.Size;
|
|
5
|
+
heap: { used: tools.Size; total: tools.Size };
|
|
6
|
+
};
|
|
4
7
|
|
|
5
8
|
// RSS - resident set size - how much memory this process uses
|
|
6
9
|
// Heap total - how much memory is reserved for the JS heap
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type PathMatcherRule = string | URLPattern;
|
|
2
|
+
|
|
3
|
+
export class PathMatcher {
|
|
4
|
+
static matches(rules: ReadonlyArray<PathMatcherRule> | undefined, path: string): boolean {
|
|
5
|
+
return rules?.some((rule) => PathMatcher.matchesRule(rule, path)) ?? false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
private static matchesRule(rule: PathMatcherRule, path: string): boolean {
|
|
9
|
+
if (rule instanceof URLPattern) return rule.test({ pathname: path });
|
|
10
|
+
if (path === rule) return true;
|
|
11
|
+
|
|
12
|
+
return path.startsWith(rule.endsWith("/") ? rule : `${rule}/`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// [BUN DEPENDENCY]
|
|
2
2
|
import * as tools from "@bgord/tools";
|
|
3
3
|
import * as v from "valibot";
|
|
4
|
+
import type { AtomicFileWriterPort } from "./atomic-file-writer.port";
|
|
4
5
|
import type { DirectoryEnsurerPort } from "./directory-ensurer.port";
|
|
5
6
|
import type { FileCleanerPort } from "./file-cleaner.port";
|
|
6
|
-
import type { FileCopierPort } from "./file-copier.port";
|
|
7
7
|
import type { FileInspectionPort } from "./file-inspection.port";
|
|
8
|
-
import type { FileRenamerPort } from "./file-renamer.port";
|
|
9
8
|
import type { HashFilePort } from "./hash-file.port";
|
|
10
|
-
import type { NonceProviderPort } from "./nonce-provider.port";
|
|
11
9
|
import type {
|
|
12
10
|
RemoteFileStoragePort,
|
|
13
11
|
RemoteHeadResult,
|
|
@@ -16,13 +14,11 @@ import type {
|
|
|
16
14
|
} from "./remote-file-storage.port";
|
|
17
15
|
|
|
18
16
|
type Dependencies = {
|
|
17
|
+
AtomicFileWriter: AtomicFileWriterPort;
|
|
19
18
|
HashFile: HashFilePort;
|
|
20
19
|
FileInspection: FileInspectionPort;
|
|
21
20
|
FileCleaner: FileCleanerPort;
|
|
22
|
-
FileRenamer: FileRenamerPort;
|
|
23
|
-
FileCopier: FileCopierPort;
|
|
24
21
|
DirectoryEnsurer: DirectoryEnsurerPort;
|
|
25
|
-
NonceProvider: NonceProviderPort;
|
|
26
22
|
};
|
|
27
23
|
|
|
28
24
|
type Config = { root: tools.DirectoryPathAbsoluteType };
|
|
@@ -47,19 +43,9 @@ export class RemoteFileStorageDiskAdapter implements RemoteFileStoragePort {
|
|
|
47
43
|
|
|
48
44
|
async putFromPath(input: RemotePutFromPathInput): Promise<RemotePutFromPathResult> {
|
|
49
45
|
const final = this.resolveKeyToAbsoluteFilePath(input.key);
|
|
50
|
-
const temporary = final.withFilename(
|
|
51
|
-
final.getFilename().withSuffix(`-part-${this.deps.NonceProvider.generate()}`),
|
|
52
|
-
);
|
|
53
46
|
|
|
54
47
|
await this.deps.DirectoryEnsurer.ensure(final.getDirectory());
|
|
55
|
-
await this.deps.
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
await this.deps.FileRenamer.rename(temporary, final);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
await this.deps.FileCleaner.delete(temporary);
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
48
|
+
await this.deps.AtomicFileWriter.write(final, Bun.file(input.path.get()));
|
|
63
49
|
|
|
64
50
|
return this.deps.HashFile.hash(final);
|
|
65
51
|
}
|
|
@@ -4,8 +4,7 @@ import type { Context } from "hono";
|
|
|
4
4
|
import { getConnInfo } from "hono/bun";
|
|
5
5
|
import { getCookie } from "hono/cookie";
|
|
6
6
|
import { HTTPException } from "hono/http-exception";
|
|
7
|
-
import type
|
|
8
|
-
import { ShieldAuthStrategyError } from "./shield-auth.strategy";
|
|
7
|
+
import { type RequestContext, RequestIdentityError } from "./request-context.port";
|
|
9
8
|
|
|
10
9
|
export class RequestContextHonoAdapter implements RequestContext {
|
|
11
10
|
readonly request: RequestContext["request"];
|
|
@@ -58,8 +57,8 @@ export class RequestContextHonoAdapter implements RequestContext {
|
|
|
58
57
|
authenticatedUserId: () => {
|
|
59
58
|
const user = context.get("user");
|
|
60
59
|
|
|
61
|
-
if (user === undefined) throw new Error(
|
|
62
|
-
if (user === null) throw new HTTPException(401, { message:
|
|
60
|
+
if (user === undefined) throw new Error(RequestIdentityError.NotAttached);
|
|
61
|
+
if (user === null) throw new HTTPException(401, { message: RequestIdentityError.Rejected });
|
|
63
62
|
return user.id;
|
|
64
63
|
},
|
|
65
64
|
ip: () =>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type * as tools from "@bgord/tools";
|
|
2
2
|
import type { UUIDType } from "./uuid.vo";
|
|
3
3
|
|
|
4
|
+
export const RequestIdentityError = {
|
|
5
|
+
Rejected: "shield.auth.rejected",
|
|
6
|
+
NotAttached: "shield.auth.not.attached",
|
|
7
|
+
};
|
|
8
|
+
|
|
4
9
|
export interface RequestContext {
|
|
5
10
|
readonly request: {
|
|
6
11
|
readonly path: string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as tools from "@bgord/tools";
|
|
2
|
+
import type { RuntimeStatsProviderPort, RuntimeStatsSnapshot } from "./runtime-stats-provider.port";
|
|
3
|
+
|
|
4
|
+
export class RuntimeStatsProviderNoopAdapter implements RuntimeStatsProviderPort {
|
|
5
|
+
getStats(): RuntimeStatsSnapshot {
|
|
6
|
+
const zero = tools.Duration.Ms(0);
|
|
7
|
+
const empty = tools.Size.fromBytes(0);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
uptime: { duration: zero, formatted: "0 seconds ago" },
|
|
11
|
+
memory: { total: empty, heap: { used: empty, total: empty } },
|
|
12
|
+
eventLoop: { lag: { p50: zero, p95: zero, p99: zero }, utilization: 0 },
|
|
13
|
+
inFlight: tools.Int.of(0),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ClockPort } from "./clock.port";
|
|
2
|
+
import { EventLoopLag } from "./event-loop-lag.service";
|
|
3
|
+
import { EventLoopUtilization } from "./event-loop-utilization.service";
|
|
4
|
+
import { InFlightRequestsTracker } from "./in-flight-requests-tracker.service";
|
|
5
|
+
import { MemoryConsumption } from "./memory-consumption.service";
|
|
6
|
+
import type { RuntimeStatsProviderPort, RuntimeStatsSnapshot } from "./runtime-stats-provider.port";
|
|
7
|
+
import { Uptime } from "./uptime.service";
|
|
8
|
+
|
|
9
|
+
type Dependencies = { Clock: ClockPort };
|
|
10
|
+
|
|
11
|
+
export class RuntimeStatsProviderSystemAdapter implements RuntimeStatsProviderPort {
|
|
12
|
+
constructor(private readonly deps: Dependencies) {
|
|
13
|
+
EventLoopLag.start();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getStats(): RuntimeStatsSnapshot {
|
|
17
|
+
return {
|
|
18
|
+
uptime: Uptime.get(this.deps.Clock),
|
|
19
|
+
memory: MemoryConsumption.snapshot(),
|
|
20
|
+
eventLoop: { lag: EventLoopLag.snapshot(), utilization: EventLoopUtilization.snapshot() },
|
|
21
|
+
inFlight: InFlightRequestsTracker.get(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type * as tools from "@bgord/tools";
|
|
2
|
+
import type { EventLoopLagSnapshotType } from "./event-loop-lag.service";
|
|
3
|
+
import type { EventLoopUtilizationSnapshot } from "./event-loop-utilization.service";
|
|
4
|
+
import type { MemoryConsumptionSnapshotType } from "./memory-consumption.service";
|
|
5
|
+
import type { UptimeResultType } from "./uptime.service";
|
|
6
|
+
|
|
7
|
+
export type RuntimeStatsSnapshot = {
|
|
8
|
+
uptime: UptimeResultType;
|
|
9
|
+
memory: MemoryConsumptionSnapshotType;
|
|
10
|
+
eventLoop: { lag: EventLoopLagSnapshotType; utilization: EventLoopUtilizationSnapshot };
|
|
11
|
+
inFlight: tools.IntegerType;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface RuntimeStatsProviderPort {
|
|
15
|
+
getStats(): RuntimeStatsSnapshot;
|
|
16
|
+
}
|
|
@@ -31,12 +31,12 @@ export class SecurityRuleViolationThresholdStrategy implements SecurityRuleStrat
|
|
|
31
31
|
|
|
32
32
|
// Best-effort increment, occasional lost increments are acceptable for concurrent requests.
|
|
33
33
|
async isViolated(context: RequestContext): Promise<boolean> {
|
|
34
|
-
const subject = await this.resolver.resolve(context);
|
|
35
|
-
|
|
36
34
|
const violated = await this.rule.isViolated(context);
|
|
37
35
|
|
|
38
36
|
if (!violated) return false;
|
|
39
37
|
|
|
38
|
+
const subject = await this.resolver.resolve(context);
|
|
39
|
+
|
|
40
40
|
try {
|
|
41
41
|
const cached = await this.deps.CacheRepository.get(subject.hex);
|
|
42
42
|
const count = typeof cached === "number" ? cached : 0;
|
|
@@ -50,9 +50,9 @@ export class SetupHono {
|
|
|
50
50
|
deps: Dependencies,
|
|
51
51
|
): Array<MiddlewareHandler> {
|
|
52
52
|
return [
|
|
53
|
+
new CorrelationHonoMiddleware(deps).handle(),
|
|
53
54
|
new ShieldMaintenanceHonoStrategy(config.maintenanceMode).handle(),
|
|
54
55
|
new TrailingSlashHonoMiddleware().handle(),
|
|
55
|
-
new CorrelationHonoMiddleware(deps).handle(),
|
|
56
56
|
new TimingHonoMiddleware(deps).handle(),
|
|
57
57
|
new ApiVersionHonoMiddleware(deps).handle(),
|
|
58
58
|
new ShieldCsrfHonoStrategy(config.csrf).handle(),
|
|
@@ -2,8 +2,9 @@ import type hono from "hono";
|
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
3
|
import { HTTPException } from "hono/http-exception";
|
|
4
4
|
import type { AuthSessionReaderPort } from "./auth-session-reader.port";
|
|
5
|
+
import { RequestIdentityError } from "./request-context.port";
|
|
5
6
|
import { RequestContextHonoAdapter } from "./request-context-hono.adapter";
|
|
6
|
-
import { ShieldAuthStrategy
|
|
7
|
+
import { ShieldAuthStrategy } from "./shield-auth.strategy";
|
|
7
8
|
|
|
8
9
|
type Dependencies<User, Session> = { AuthSessionReader: AuthSessionReaderPort<User, Session> };
|
|
9
10
|
|
|
@@ -27,13 +28,13 @@ export class ShieldAuthHonoStrategy<User, Session> {
|
|
|
27
28
|
const user = c.get("user");
|
|
28
29
|
|
|
29
30
|
if (this.strategy.verify(user)) return next();
|
|
30
|
-
throw new HTTPException(401, { message:
|
|
31
|
+
throw new HTTPException(401, { message: RequestIdentityError.Rejected });
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
reverse = createMiddleware(async (c: hono.Context, next: hono.Next) => {
|
|
34
35
|
const user = c.get("user");
|
|
35
36
|
|
|
36
37
|
if (this.strategy.reverse(user)) return next();
|
|
37
|
-
throw new HTTPException(403, { message:
|
|
38
|
+
throw new HTTPException(403, { message: RequestIdentityError.Rejected });
|
|
38
39
|
});
|
|
39
40
|
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import type { AuthSessionReaderPort, AuthSessionType } from "./auth-session-reader.port";
|
|
2
|
-
import type
|
|
2
|
+
import { type HasRequestHeaders, RequestIdentityError } from "./request-context.port";
|
|
3
3
|
|
|
4
4
|
type Dependencies<User, Session> = { AuthSessionReader: AuthSessionReaderPort<User, Session> };
|
|
5
5
|
|
|
6
|
-
export const ShieldAuthStrategyError = {
|
|
7
|
-
Rejected: "shield.auth.rejected",
|
|
8
|
-
NotAttached: "shield.auth.not.attached",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
6
|
export class ShieldAuthStrategy<User, Session> {
|
|
12
7
|
constructor(private readonly deps: Dependencies<User, Session>) {}
|
|
13
8
|
|
|
@@ -16,12 +11,12 @@ export class ShieldAuthStrategy<User, Session> {
|
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
verify(user: User | null | undefined): boolean {
|
|
19
|
-
if (user === undefined) throw new Error(
|
|
14
|
+
if (user === undefined) throw new Error(RequestIdentityError.NotAttached);
|
|
20
15
|
return user !== null;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
reverse(user: User | null | undefined): boolean {
|
|
24
|
-
if (user === undefined) throw new Error(
|
|
19
|
+
if (user === undefined) throw new Error(RequestIdentityError.NotAttached);
|
|
25
20
|
return user === null;
|
|
26
21
|
}
|
|
27
22
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as tools from "@bgord/tools";
|
|
2
2
|
import * as v from "valibot";
|
|
3
|
+
import { PathMatcher } from "./path-matcher.service";
|
|
3
4
|
import type { ReactiveConfigPort } from "./reactive-config.port";
|
|
4
5
|
import type { HasRequestPath } from "./request-context.port";
|
|
5
6
|
|
|
@@ -31,12 +32,7 @@ export class ShieldMaintenanceStrategy {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
shouldSkip(context: HasRequestPath): boolean {
|
|
34
|
-
return (
|
|
35
|
-
this.config?.skip?.some((rule) => {
|
|
36
|
-
if (rule instanceof URLPattern) return rule.test({ pathname: context.request.path });
|
|
37
|
-
return context.request.path.startsWith(rule);
|
|
38
|
-
}) ?? false
|
|
39
|
-
);
|
|
35
|
+
return PathMatcher.matches(this.config?.skip, context.request.path);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
async evaluate(): Promise<ShieldMaintenanceResult> {
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import * as tools from "@bgord/tools";
|
|
2
|
+
import type { AtomicFileWriterPort } from "./atomic-file-writer.port";
|
|
2
3
|
import type { FileCleanerPort } from "./file-cleaner.port";
|
|
3
|
-
import type { FileRenamerPort } from "./file-renamer.port";
|
|
4
|
-
import type { FileWriterPort } from "./file-writer.port";
|
|
5
|
-
import type { NonceProviderPort } from "./nonce-provider.port";
|
|
6
4
|
import type { TemporaryFilePort } from "./temporary-file.port";
|
|
7
5
|
|
|
8
6
|
type Dependencies = {
|
|
7
|
+
AtomicFileWriter: AtomicFileWriterPort;
|
|
9
8
|
FileCleaner: FileCleanerPort;
|
|
10
|
-
FileRenamer: FileRenamerPort;
|
|
11
|
-
FileWriter: FileWriterPort;
|
|
12
|
-
NonceProvider: NonceProviderPort;
|
|
13
9
|
};
|
|
14
10
|
|
|
15
11
|
export class TemporaryFileAbsoluteAdapter implements TemporaryFilePort {
|
|
@@ -19,20 +15,9 @@ export class TemporaryFileAbsoluteAdapter implements TemporaryFilePort {
|
|
|
19
15
|
) {}
|
|
20
16
|
|
|
21
17
|
async write(filename: tools.Filename, content: File): Promise<tools.FilePathAbsolute> {
|
|
22
|
-
const temporary = tools.FilePathAbsolute.fromPartsSafe(
|
|
23
|
-
this.directory,
|
|
24
|
-
filename.withSuffix(`-part-${this.deps.NonceProvider.generate()}`),
|
|
25
|
-
);
|
|
26
18
|
const final = tools.FilePathAbsolute.fromPartsSafe(this.directory, filename);
|
|
27
19
|
|
|
28
|
-
await this.deps.
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
await this.deps.FileRenamer.rename(temporary, final);
|
|
32
|
-
} catch (error) {
|
|
33
|
-
await this.deps.FileCleaner.delete(temporary);
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
20
|
+
await this.deps.AtomicFileWriter.write(final, content);
|
|
36
21
|
|
|
37
22
|
return final;
|
|
38
23
|
}
|
package/src/timing.middleware.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClockPort } from "./clock.port";
|
|
2
|
+
import { EventStreamAccept } from "./event-stream-accept.service";
|
|
2
3
|
import type { HasRequestHeader } from "./request-context.port";
|
|
3
4
|
import { Stopwatch } from "./stopwatch.service";
|
|
4
5
|
|
|
@@ -10,7 +11,7 @@ export class TimingMiddleware {
|
|
|
10
11
|
constructor(private readonly deps: TimingMiddlewareDependencies) {}
|
|
11
12
|
|
|
12
13
|
async measure(context: HasRequestHeader, action: () => void | Promise<void>): Promise<string | null> {
|
|
13
|
-
if (
|
|
14
|
+
if (EventStreamAccept.matches(context.request.header("accept"))) return null;
|
|
14
15
|
|
|
15
16
|
const stopwatch = new Stopwatch(this.deps);
|
|
16
17
|
|
|
@@ -18,13 +19,4 @@ export class TimingMiddleware {
|
|
|
18
19
|
|
|
19
20
|
return `total;dur=${Math.max(0, stopwatch.stop().ms)}`;
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
private static isEventStream(accept: string | undefined): boolean {
|
|
23
|
-
if (!accept) return false;
|
|
24
|
-
|
|
25
|
-
return accept
|
|
26
|
-
.toLowerCase()
|
|
27
|
-
.split(",")
|
|
28
|
-
.some((type) => type.split(";")[0]?.trim() === "text/event-stream");
|
|
29
|
-
}
|
|
30
22
|
}
|