@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.
Files changed (189) hide show
  1. package/dist/atomic-file-writer-collecting.adapter.d.ts +8 -0
  2. package/dist/atomic-file-writer-collecting.adapter.d.ts.map +1 -0
  3. package/dist/atomic-file-writer-collecting.adapter.js +7 -0
  4. package/dist/atomic-file-writer-collecting.adapter.js.map +1 -0
  5. package/dist/atomic-file-writer-noop.adapter.d.ts +7 -0
  6. package/dist/atomic-file-writer-noop.adapter.d.ts.map +1 -0
  7. package/dist/atomic-file-writer-noop.adapter.js +4 -0
  8. package/dist/atomic-file-writer-noop.adapter.js.map +1 -0
  9. package/dist/atomic-file-writer.adapter.d.ts +19 -0
  10. package/dist/atomic-file-writer.adapter.d.ts.map +1 -0
  11. package/dist/atomic-file-writer.adapter.js +18 -0
  12. package/dist/atomic-file-writer.adapter.js.map +1 -0
  13. package/dist/atomic-file-writer.port.d.ts +6 -0
  14. package/dist/atomic-file-writer.port.d.ts.map +1 -0
  15. package/dist/atomic-file-writer.port.js +2 -0
  16. package/dist/atomic-file-writer.port.js.map +1 -0
  17. package/dist/encryption-aes-gcm.adapter.d.ts +2 -2
  18. package/dist/encryption-aes-gcm.adapter.d.ts.map +1 -1
  19. package/dist/encryption-aes-gcm.adapter.js +2 -2
  20. package/dist/encryption-aes-gcm.adapter.js.map +1 -1
  21. package/dist/event-stream-accept.service.d.ts +4 -0
  22. package/dist/event-stream-accept.service.d.ts.map +1 -0
  23. package/dist/event-stream-accept.service.js +11 -0
  24. package/dist/event-stream-accept.service.js.map +1 -0
  25. package/dist/gzip.adapter.d.ts +2 -2
  26. package/dist/gzip.adapter.d.ts.map +1 -1
  27. package/dist/gzip.adapter.js +1 -1
  28. package/dist/gzip.adapter.js.map +1 -1
  29. package/dist/healthcheck.handler.d.ts +4 -2
  30. package/dist/healthcheck.handler.d.ts.map +1 -1
  31. package/dist/healthcheck.handler.js +4 -11
  32. package/dist/healthcheck.handler.js.map +1 -1
  33. package/dist/http-exception-errors.js +2 -2
  34. package/dist/http-exception-errors.js.map +1 -1
  35. package/dist/http-logger-hono.middleware.d.ts +1 -0
  36. package/dist/http-logger-hono.middleware.d.ts.map +1 -1
  37. package/dist/http-logger-hono.middleware.js +10 -6
  38. package/dist/http-logger-hono.middleware.js.map +1 -1
  39. package/dist/http-logger.middleware.d.ts.map +1 -1
  40. package/dist/http-logger.middleware.js +4 -6
  41. package/dist/http-logger.middleware.js.map +1 -1
  42. package/dist/image-blur.adapter.d.ts +2 -6
  43. package/dist/image-blur.adapter.d.ts.map +1 -1
  44. package/dist/image-blur.adapter.js +1 -4
  45. package/dist/image-blur.adapter.js.map +1 -1
  46. package/dist/image-compressor.adapter.d.ts +2 -6
  47. package/dist/image-compressor.adapter.d.ts.map +1 -1
  48. package/dist/image-compressor.adapter.js +1 -4
  49. package/dist/image-compressor.adapter.js.map +1 -1
  50. package/dist/image-exif-clear.adapter.d.ts +2 -6
  51. package/dist/image-exif-clear.adapter.d.ts.map +1 -1
  52. package/dist/image-exif-clear.adapter.js +1 -3
  53. package/dist/image-exif-clear.adapter.js.map +1 -1
  54. package/dist/image-formatter.adapter.d.ts +2 -6
  55. package/dist/image-formatter.adapter.d.ts.map +1 -1
  56. package/dist/image-formatter.adapter.js +1 -3
  57. package/dist/image-formatter.adapter.js.map +1 -1
  58. package/dist/image-grayscale.adapter.d.ts +2 -6
  59. package/dist/image-grayscale.adapter.d.ts.map +1 -1
  60. package/dist/image-grayscale.adapter.js +1 -4
  61. package/dist/image-grayscale.adapter.js.map +1 -1
  62. package/dist/image-processor.adapter.d.ts +2 -6
  63. package/dist/image-processor.adapter.d.ts.map +1 -1
  64. package/dist/image-processor.adapter.js +1 -3
  65. package/dist/image-processor.adapter.js.map +1 -1
  66. package/dist/image-resizer.adapter.d.ts +2 -6
  67. package/dist/image-resizer.adapter.d.ts.map +1 -1
  68. package/dist/image-resizer.adapter.js +1 -4
  69. package/dist/image-resizer.adapter.js.map +1 -1
  70. package/dist/index.d.ts +11 -0
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +11 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/job-pruner-sqlite.adapter.d.ts.map +1 -1
  75. package/dist/job-pruner-sqlite.adapter.js +2 -1
  76. package/dist/job-pruner-sqlite.adapter.js.map +1 -1
  77. package/dist/job-queue-stats-provider-sqlite.adapter.js +4 -4
  78. package/dist/job-queue-stats-provider-sqlite.adapter.js.map +1 -1
  79. package/dist/job-recoverer-collecting.adapter.d.ts +7 -0
  80. package/dist/job-recoverer-collecting.adapter.d.ts.map +1 -0
  81. package/dist/job-recoverer-collecting.adapter.js +10 -0
  82. package/dist/job-recoverer-collecting.adapter.js.map +1 -0
  83. package/dist/job-recoverer-noop.adapter.d.ts +6 -0
  84. package/dist/job-recoverer-noop.adapter.d.ts.map +1 -0
  85. package/dist/job-recoverer-noop.adapter.js +7 -0
  86. package/dist/job-recoverer-noop.adapter.js.map +1 -0
  87. package/dist/job-recoverer-sqlite.adapter.d.ts +13 -0
  88. package/dist/job-recoverer-sqlite.adapter.d.ts.map +1 -0
  89. package/dist/job-recoverer-sqlite.adapter.js +14 -0
  90. package/dist/job-recoverer-sqlite.adapter.js.map +1 -0
  91. package/dist/job-recoverer.port.d.ts +5 -0
  92. package/dist/job-recoverer.port.d.ts.map +1 -0
  93. package/dist/job-recoverer.port.js +2 -0
  94. package/dist/job-recoverer.port.js.map +1 -0
  95. package/dist/memory-consumption.service.d.ts +1 -2
  96. package/dist/memory-consumption.service.d.ts.map +1 -1
  97. package/dist/memory-consumption.service.js.map +1 -1
  98. package/dist/path-matcher.service.d.ts +6 -0
  99. package/dist/path-matcher.service.d.ts.map +1 -0
  100. package/dist/path-matcher.service.js +13 -0
  101. package/dist/path-matcher.service.js.map +1 -0
  102. package/dist/remote-file-storage-disk.adapter.d.ts +2 -6
  103. package/dist/remote-file-storage-disk.adapter.d.ts.map +1 -1
  104. package/dist/remote-file-storage-disk.adapter.js +1 -9
  105. package/dist/remote-file-storage-disk.adapter.js.map +1 -1
  106. package/dist/request-context-hono.adapter.d.ts +1 -1
  107. package/dist/request-context-hono.adapter.d.ts.map +1 -1
  108. package/dist/request-context-hono.adapter.js +3 -3
  109. package/dist/request-context-hono.adapter.js.map +1 -1
  110. package/dist/request-context.port.d.ts +4 -0
  111. package/dist/request-context.port.d.ts.map +1 -1
  112. package/dist/request-context.port.js +4 -1
  113. package/dist/request-context.port.js.map +1 -1
  114. package/dist/runtime-stats-provider-noop.adapter.d.ts +5 -0
  115. package/dist/runtime-stats-provider-noop.adapter.d.ts.map +1 -0
  116. package/dist/runtime-stats-provider-noop.adapter.js +14 -0
  117. package/dist/runtime-stats-provider-noop.adapter.js.map +1 -0
  118. package/dist/runtime-stats-provider-system.adapter.d.ts +12 -0
  119. package/dist/runtime-stats-provider-system.adapter.d.ts.map +1 -0
  120. package/dist/runtime-stats-provider-system.adapter.js +21 -0
  121. package/dist/runtime-stats-provider-system.adapter.js.map +1 -0
  122. package/dist/runtime-stats-provider.port.d.ts +18 -0
  123. package/dist/runtime-stats-provider.port.d.ts.map +1 -0
  124. package/dist/runtime-stats-provider.port.js +2 -0
  125. package/dist/runtime-stats-provider.port.js.map +1 -0
  126. package/dist/security-rule-violation-threshold.strategy.js +1 -1
  127. package/dist/security-rule-violation-threshold.strategy.js.map +1 -1
  128. package/dist/setup-hono.service.js +1 -1
  129. package/dist/setup-hono.service.js.map +1 -1
  130. package/dist/shield-auth-hono.strategy.d.ts.map +1 -1
  131. package/dist/shield-auth-hono.strategy.js +4 -3
  132. package/dist/shield-auth-hono.strategy.js.map +1 -1
  133. package/dist/shield-auth.strategy.d.ts +1 -5
  134. package/dist/shield-auth.strategy.d.ts.map +1 -1
  135. package/dist/shield-auth.strategy.js +3 -6
  136. package/dist/shield-auth.strategy.js.map +1 -1
  137. package/dist/shield-maintenance.strategy.d.ts.map +1 -1
  138. package/dist/shield-maintenance.strategy.js +2 -5
  139. package/dist/shield-maintenance.strategy.js.map +1 -1
  140. package/dist/temporary-file-absolute.adapter.d.ts +2 -6
  141. package/dist/temporary-file-absolute.adapter.d.ts.map +1 -1
  142. package/dist/temporary-file-absolute.adapter.js +1 -9
  143. package/dist/temporary-file-absolute.adapter.js.map +1 -1
  144. package/dist/timing.middleware.d.ts +0 -1
  145. package/dist/timing.middleware.d.ts.map +1 -1
  146. package/dist/timing.middleware.js +2 -9
  147. package/dist/timing.middleware.js.map +1 -1
  148. package/package.json +1 -1
  149. package/readme.md +13 -0
  150. package/src/atomic-file-writer-collecting.adapter.ts +14 -0
  151. package/src/atomic-file-writer-noop.adapter.ts +10 -0
  152. package/src/atomic-file-writer.adapter.ts +34 -0
  153. package/src/atomic-file-writer.port.ts +6 -0
  154. package/src/encryption-aes-gcm.adapter.ts +4 -4
  155. package/src/event-stream-accept.service.ts +10 -0
  156. package/src/gzip.adapter.ts +3 -3
  157. package/src/healthcheck.handler.ts +8 -11
  158. package/src/http-exception-errors.ts +2 -2
  159. package/src/http-logger-hono.middleware.ts +12 -6
  160. package/src/http-logger.middleware.ts +4 -7
  161. package/src/image-blur.adapter.ts +3 -13
  162. package/src/image-compressor.adapter.ts +3 -12
  163. package/src/image-exif-clear.adapter.ts +3 -11
  164. package/src/image-formatter.adapter.ts +3 -12
  165. package/src/image-grayscale.adapter.ts +3 -13
  166. package/src/image-processor.adapter.ts +3 -11
  167. package/src/image-resizer.adapter.ts +3 -12
  168. package/src/index.ts +11 -0
  169. package/src/job-pruner-sqlite.adapter.ts +2 -1
  170. package/src/job-queue-stats-provider-sqlite.adapter.ts +4 -4
  171. package/src/job-recoverer-collecting.adapter.ts +14 -0
  172. package/src/job-recoverer-noop.adapter.ts +8 -0
  173. package/src/job-recoverer-sqlite.adapter.ts +20 -0
  174. package/src/job-recoverer.port.ts +5 -0
  175. package/src/memory-consumption.service.ts +4 -1
  176. package/src/path-matcher.service.ts +14 -0
  177. package/src/remote-file-storage-disk.adapter.ts +3 -17
  178. package/src/request-context-hono.adapter.ts +3 -4
  179. package/src/request-context.port.ts +5 -0
  180. package/src/runtime-stats-provider-noop.adapter.ts +16 -0
  181. package/src/runtime-stats-provider-system.adapter.ts +24 -0
  182. package/src/runtime-stats-provider.port.ts +16 -0
  183. package/src/security-rule-violation-threshold.strategy.ts +2 -2
  184. package/src/setup-hono.service.ts +1 -1
  185. package/src/shield-auth-hono.strategy.ts +4 -3
  186. package/src/shield-auth.strategy.ts +3 -8
  187. package/src/shield-maintenance.strategy.ts +2 -6
  188. package/src/temporary-file-absolute.adapter.ts +3 -18
  189. 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 { FileRenamerPort } from "./file-renamer.port";
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
- FileWriter: FileWriterPort;
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.FileWriter.write(temporary.get(), resized);
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["pending"] ?? 0),
29
- claimed: tools.Int.nonNegative(counts["claimed"] ?? 0),
30
- completed: tools.Int.nonNegative(counts["completed"] ?? 0),
31
- failed: tools.Int.nonNegative(counts["failed"] ?? 0),
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
+ }
@@ -0,0 +1,5 @@
1
+ import type * as tools from "@bgord/tools";
2
+
3
+ export interface JobRecovererPort {
4
+ recover(): Promise<tools.IntegerNonNegativeType>;
5
+ }
@@ -1,6 +1,9 @@
1
1
  import * as tools from "@bgord/tools";
2
2
 
3
- type MemoryConsumptionSnapshotType = { total: tools.Size; heap: { used: tools.Size; total: tools.Size } };
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.FileCopier.copy(input.path, temporary);
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 { RequestContext } from "./request-context.port";
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(ShieldAuthStrategyError.NotAttached);
62
- if (user === null) throw new HTTPException(401, { message: ShieldAuthStrategyError.Rejected });
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, ShieldAuthStrategyError } from "./shield-auth.strategy";
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: ShieldAuthStrategyError.Rejected });
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: ShieldAuthStrategyError.Rejected });
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 { HasRequestHeaders } from "./request-context.port";
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(ShieldAuthStrategyError.NotAttached);
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(ShieldAuthStrategyError.NotAttached);
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.FileWriter.write(temporary.get(), content);
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
  }
@@ -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 (TimingMiddleware.isEventStream(context.request.header("accept"))) return null;
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
  }