@bgord/bun 1.23.0 → 1.23.2

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 (154) hide show
  1. package/dist/error-classifier-http-exception-hono.strategy.d.ts +2 -4
  2. package/dist/error-classifier-http-exception-hono.strategy.d.ts.map +1 -1
  3. package/dist/error-classifier-http-exception-hono.strategy.js +5 -4
  4. package/dist/error-classifier-http-exception-hono.strategy.js.map +1 -1
  5. package/dist/error-classifier-invariant.strategy.d.ts +4 -5
  6. package/dist/error-classifier-invariant.strategy.d.ts.map +1 -1
  7. package/dist/error-classifier-invariant.strategy.js +11 -7
  8. package/dist/error-classifier-invariant.strategy.js.map +1 -1
  9. package/dist/error-classifier-validation.strategy.d.ts +2 -4
  10. package/dist/error-classifier-validation.strategy.d.ts.map +1 -1
  11. package/dist/error-classifier-validation.strategy.js +3 -3
  12. package/dist/error-classifier-validation.strategy.js.map +1 -1
  13. package/dist/error-hono.handler.d.ts +2 -2
  14. package/dist/error-hono.handler.d.ts.map +1 -1
  15. package/dist/error-hono.handler.js +2 -2
  16. package/dist/error-hono.handler.js.map +1 -1
  17. package/dist/error.handler.d.ts +6 -4
  18. package/dist/error.handler.d.ts.map +1 -1
  19. package/dist/error.handler.js +7 -6
  20. package/dist/error.handler.js.map +1 -1
  21. package/dist/file-type-detector-fixed.strategy.d.ts +8 -0
  22. package/dist/file-type-detector-fixed.strategy.d.ts.map +1 -0
  23. package/dist/file-type-detector-fixed.strategy.js +10 -0
  24. package/dist/file-type-detector-fixed.strategy.js.map +1 -0
  25. package/dist/file-type-detector-magic-bytes.strategy.d.ts +6 -0
  26. package/dist/file-type-detector-magic-bytes.strategy.d.ts.map +1 -0
  27. package/dist/file-type-detector-magic-bytes.strategy.js +25 -0
  28. package/dist/file-type-detector-magic-bytes.strategy.js.map +1 -0
  29. package/dist/file-type-detector-text.strategy.d.ts +8 -0
  30. package/dist/file-type-detector-text.strategy.d.ts.map +1 -0
  31. package/dist/file-type-detector-text.strategy.js +22 -0
  32. package/dist/file-type-detector-text.strategy.js.map +1 -0
  33. package/dist/file-type-detector.strategy.d.ts +5 -0
  34. package/dist/file-type-detector.strategy.d.ts.map +1 -0
  35. package/dist/file-type-detector.strategy.js +2 -0
  36. package/dist/file-type-detector.strategy.js.map +1 -0
  37. package/dist/file-uploader-hono.middleware.d.ts +6 -1
  38. package/dist/file-uploader-hono.middleware.d.ts.map +1 -1
  39. package/dist/file-uploader-hono.middleware.js +2 -2
  40. package/dist/file-uploader-hono.middleware.js.map +1 -1
  41. package/dist/file-uploader.middleware.d.ts +7 -1
  42. package/dist/file-uploader.middleware.d.ts.map +1 -1
  43. package/dist/file-uploader.middleware.js +6 -3
  44. package/dist/file-uploader.middleware.js.map +1 -1
  45. package/dist/hcaptcha.service.d.ts +1 -0
  46. package/dist/hcaptcha.service.d.ts.map +1 -1
  47. package/dist/healthcheck.handler.d.ts +1 -1
  48. package/dist/healthcheck.handler.d.ts.map +1 -1
  49. package/dist/healthcheck.handler.js +1 -1
  50. package/dist/healthcheck.handler.js.map +1 -1
  51. package/dist/http-exception-errors.d.ts +20 -0
  52. package/dist/http-exception-errors.d.ts.map +1 -0
  53. package/dist/http-exception-errors.js +31 -0
  54. package/dist/http-exception-errors.js.map +1 -0
  55. package/dist/index.d.ts +7 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +7 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/readiness.handler.d.ts +2 -0
  60. package/dist/readiness.handler.d.ts.map +1 -1
  61. package/dist/readiness.handler.js +1 -1
  62. package/dist/readiness.handler.js.map +1 -1
  63. package/dist/remote-file-storage-disk.adapter.d.ts.map +1 -1
  64. package/dist/remote-file-storage-disk.adapter.js +4 -1
  65. package/dist/remote-file-storage-disk.adapter.js.map +1 -1
  66. package/dist/sealer-aes-gcm.adapter.d.ts.map +1 -1
  67. package/dist/sealer-aes-gcm.adapter.js +8 -3
  68. package/dist/sealer-aes-gcm.adapter.js.map +1 -1
  69. package/dist/security-rule-user-agent.strategy.d.ts.map +1 -1
  70. package/dist/security-rule-user-agent.strategy.js +4 -3
  71. package/dist/security-rule-user-agent.strategy.js.map +1 -1
  72. package/dist/security-rule-violation-threshold.strategy.d.ts +3 -0
  73. package/dist/security-rule-violation-threshold.strategy.d.ts.map +1 -1
  74. package/dist/security-rule-violation-threshold.strategy.js +3 -3
  75. package/dist/security-rule-violation-threshold.strategy.js.map +1 -1
  76. package/dist/shield-body-limit.strategy.d.ts.map +1 -1
  77. package/dist/shield-body-limit.strategy.js +3 -1
  78. package/dist/shield-body-limit.strategy.js.map +1 -1
  79. package/dist/shield-hcaptcha-hono-local.strategy.d.ts +1 -0
  80. package/dist/shield-hcaptcha-hono-local.strategy.d.ts.map +1 -1
  81. package/dist/shield-hcaptcha-hono-local.strategy.js +5 -1
  82. package/dist/shield-hcaptcha-hono-local.strategy.js.map +1 -1
  83. package/dist/shield-hcaptcha-hono.strategy.d.ts +2 -2
  84. package/dist/shield-hcaptcha-hono.strategy.d.ts.map +1 -1
  85. package/dist/shield-hcaptcha-hono.strategy.js +3 -3
  86. package/dist/shield-hcaptcha-hono.strategy.js.map +1 -1
  87. package/dist/shield-hcaptcha.strategy.d.ts +6 -2
  88. package/dist/shield-hcaptcha.strategy.d.ts.map +1 -1
  89. package/dist/shield-hcaptcha.strategy.js +9 -5
  90. package/dist/shield-hcaptcha.strategy.js.map +1 -1
  91. package/dist/shield-recaptcha.strategy.d.ts +4 -0
  92. package/dist/shield-recaptcha.strategy.d.ts.map +1 -1
  93. package/dist/shield-recaptcha.strategy.js +4 -0
  94. package/dist/shield-recaptcha.strategy.js.map +1 -1
  95. package/dist/subject-segment-remote-ip.strategy.d.ts +7 -0
  96. package/dist/subject-segment-remote-ip.strategy.d.ts.map +1 -0
  97. package/dist/subject-segment-remote-ip.strategy.js +8 -0
  98. package/dist/subject-segment-remote-ip.strategy.js.map +1 -0
  99. package/dist/subject-segment-request.strategy.d.ts +2 -2
  100. package/dist/subject-segment-request.strategy.d.ts.map +1 -1
  101. package/dist/subject-segment-request.strategy.js.map +1 -1
  102. package/dist/webhook-body-builder-signed-headers.strategy.d.ts +8 -0
  103. package/dist/webhook-body-builder-signed-headers.strategy.d.ts.map +1 -0
  104. package/dist/webhook-body-builder-signed-headers.strategy.js +11 -0
  105. package/dist/webhook-body-builder-signed-headers.strategy.js.map +1 -0
  106. package/dist/webhook-signature.vo.d.ts +2 -1
  107. package/dist/webhook-signature.vo.d.ts.map +1 -1
  108. package/dist/webhook-signature.vo.js +4 -1
  109. package/dist/webhook-signature.vo.js.map +1 -1
  110. package/dist/webhook-verifier-sha256.strategy.d.ts.map +1 -1
  111. package/dist/webhook-verifier-sha256.strategy.js +4 -3
  112. package/dist/webhook-verifier-sha256.strategy.js.map +1 -1
  113. package/dist/woodchopper.d.ts +1 -1
  114. package/dist/woodchopper.d.ts.map +1 -1
  115. package/dist/woodchopper.js +1 -3
  116. package/dist/woodchopper.js.map +1 -1
  117. package/package.json +2 -2
  118. package/readme.md +7 -1
  119. package/src/error-classifier-http-exception-hono.strategy.ts +14 -3
  120. package/src/error-classifier-invariant.strategy.ts +19 -9
  121. package/src/error-classifier-validation.strategy.ts +9 -5
  122. package/src/error-hono.handler.ts +3 -3
  123. package/src/error.handler.ts +19 -11
  124. package/src/file-type-detector-fixed.strategy.ts +10 -0
  125. package/src/file-type-detector-magic-bytes.strategy.ts +32 -0
  126. package/src/file-type-detector-text.strategy.ts +25 -0
  127. package/src/file-type-detector.strategy.ts +5 -0
  128. package/src/file-uploader-hono.middleware.ts +5 -2
  129. package/src/file-uploader.middleware.ts +10 -3
  130. package/src/hcaptcha.service.ts +1 -1
  131. package/src/healthcheck.handler.ts +2 -2
  132. package/src/http-exception-errors.ts +31 -0
  133. package/src/index.ts +7 -1
  134. package/src/readiness.handler.ts +6 -2
  135. package/src/remote-file-storage-disk.adapter.ts +4 -1
  136. package/src/sealer-aes-gcm.adapter.ts +7 -3
  137. package/src/security-rule-user-agent.strategy.ts +4 -3
  138. package/src/security-rule-violation-threshold.strategy.ts +14 -8
  139. package/src/shield-body-limit.strategy.ts +3 -1
  140. package/src/shield-hcaptcha-hono-local.strategy.ts +6 -1
  141. package/src/shield-hcaptcha-hono.strategy.ts +7 -4
  142. package/src/shield-hcaptcha.strategy.ts +8 -3
  143. package/src/shield-recaptcha.strategy.ts +9 -2
  144. package/src/subject-segment-remote-ip.strategy.ts +14 -0
  145. package/src/subject-segment-request.strategy.ts +2 -0
  146. package/src/webhook-body-builder-signed-headers.strategy.ts +12 -0
  147. package/src/webhook-signature.vo.ts +5 -0
  148. package/src/webhook-verifier-sha256.strategy.ts +4 -3
  149. package/src/woodchopper.ts +2 -4
  150. package/dist/invariant-error-handler.service.d.ts +0 -13
  151. package/dist/invariant-error-handler.service.d.ts.map +0 -1
  152. package/dist/invariant-error-handler.service.js +0 -15
  153. package/dist/invariant-error-handler.service.js.map +0 -1
  154. package/src/invariant-error-handler.service.ts +0 -21
@@ -0,0 +1,25 @@
1
+ import type * as tools from "@bgord/tools";
2
+ import type { FileTypeDetectorStrategy } from "./file-type-detector.strategy";
3
+
4
+ const ALLOWED_CONTROL_BYTES = [0x09, 0x0a, 0x0c, 0x0d];
5
+
6
+ const isControlByte = (byte: number) =>
7
+ (byte < 0x20 && !ALLOWED_CONTROL_BYTES.includes(byte)) || byte === 0x7f;
8
+
9
+ export class FileTypeDetectorTextStrategy implements FileTypeDetectorStrategy {
10
+ constructor(private readonly mime: tools.Mime) {}
11
+
12
+ async detect(file: File): Promise<tools.Mime | null> {
13
+ const bytes = new Uint8Array(await file.slice(0, 1445).arrayBuffer());
14
+
15
+ if (bytes.length === 0) return null;
16
+ if (bytes.some(isControlByte)) return null;
17
+
18
+ const text = new TextDecoder().decode(bytes);
19
+
20
+ if (text.includes("\ufffd")) return null;
21
+ if (text.trimStart().startsWith("<")) return null;
22
+
23
+ return this.mime;
24
+ }
25
+ }
@@ -0,0 +1,5 @@
1
+ import type * as tools from "@bgord/tools";
2
+
3
+ export interface FileTypeDetectorStrategy {
4
+ detect(file: File): Promise<tools.Mime | null>;
5
+ }
@@ -1,14 +1,17 @@
1
1
  import { createMiddleware } from "hono/factory";
2
2
  import { HTTPException } from "hono/http-exception";
3
+ import type { FileTypeDetectorStrategy } from "./file-type-detector.strategy";
3
4
  import { type FileUploaderConfig, FileUploaderMiddleware } from "./file-uploader.middleware";
4
5
  import type { MiddlewareHonoPort } from "./middleware-hono.port";
5
6
  import { RequestContextHonoAdapter } from "./request-context-hono.adapter";
6
7
 
8
+ type Dependencies = { FileTypeDetector: FileTypeDetectorStrategy };
9
+
7
10
  export class FileUploaderHonoMiddleware implements MiddlewareHonoPort {
8
11
  private readonly middleware: FileUploaderMiddleware;
9
12
 
10
- constructor(config: FileUploaderConfig) {
11
- this.middleware = new FileUploaderMiddleware(config);
13
+ constructor(config: FileUploaderConfig, deps: Dependencies) {
14
+ this.middleware = new FileUploaderMiddleware(config, deps);
12
15
  }
13
16
 
14
17
  handle() {
@@ -1,8 +1,11 @@
1
1
  import * as tools from "@bgord/tools";
2
+ import type { FileTypeDetectorStrategy } from "./file-type-detector.strategy";
2
3
  import type { HasRequestForm } from "./request-context.port";
3
4
 
4
5
  export type FileUploaderConfig = { MimeRegistry: tools.MimeRegistry; maxSize: tools.Size; field: string };
5
6
 
7
+ type Dependencies = { FileTypeDetector: FileTypeDetectorStrategy };
8
+
6
9
  export const FileUploaderError = {
7
10
  MissingFile: "file.uploader.missing.file",
8
11
  EmptyFile: "file.uploader.empty.file",
@@ -15,7 +18,10 @@ export type FileValidationError = (typeof FileUploaderError)[keyof typeof FileUp
15
18
  export type FileValidationResult = { valid: true } | { valid: false; error: FileValidationError };
16
19
 
17
20
  export class FileUploaderMiddleware {
18
- constructor(private readonly config: FileUploaderConfig) {}
21
+ constructor(
22
+ private readonly config: FileUploaderConfig,
23
+ private readonly deps: Dependencies,
24
+ ) {}
19
25
 
20
26
  async validate(context: HasRequestForm): Promise<FileValidationResult> {
21
27
  const form = await context.request.form();
@@ -28,8 +34,9 @@ export class FileUploaderMiddleware {
28
34
 
29
35
  if (size.isGreaterThan(this.config.maxSize)) return { valid: false, error: FileUploaderError.SizeLimit };
30
36
 
31
- // TODO: MIME spoofing possible by reading client-supplied file.type
32
- const mime = tools.Mime.fromString(file.type);
37
+ const mime = await this.deps.FileTypeDetector.detect(file);
38
+
39
+ if (!mime) return { valid: false, error: FileUploaderError.InvalidMime };
33
40
 
34
41
  if (!this.config.MimeRegistry.hasMime(mime)) {
35
42
  return { valid: false, error: FileUploaderError.InvalidMime };
@@ -4,7 +4,7 @@ export const HCaptchaServiceError = { Error: "hcaptcha.service.error" };
4
4
 
5
5
  type HcaptchaTokenType = string;
6
6
 
7
- export type HCaptchaVerificationResult = { success: boolean };
7
+ export type HCaptchaVerificationResult = { success: boolean; hostname?: string };
8
8
 
9
9
  export class HCaptchaService {
10
10
  async verify(
@@ -37,7 +37,7 @@ export type HealthcheckDependencies = {
37
37
  export type HealthcheckConfig = {
38
38
  Env: NodeEnvironmentEnum;
39
39
  prerequisites: ReadonlyArray<Prerequisite>;
40
- redactor?: RedactorStrategy;
40
+ redactor: RedactorStrategy;
41
41
  };
42
42
 
43
43
  const self = new Prerequisite("self", new PrerequisiteVerifierSelfAdapter());
@@ -111,7 +111,7 @@ export class HealthcheckHandler {
111
111
 
112
112
  return {
113
113
  label: prerequisite.label,
114
- outcome: this.config.redactor ? this.config.redactor.redact(outcome) : outcome,
114
+ outcome: this.config.redactor.redact(outcome),
115
115
  ms: stopwatch.stop().ms,
116
116
  };
117
117
  }),
@@ -0,0 +1,31 @@
1
+ import { FileUploaderError } from "./file-uploader.middleware";
2
+ import { ShieldApiKeyStrategyError } from "./shield-api-key.strategy";
3
+ import { ShieldAuthStrategyError } from "./shield-auth.strategy";
4
+ import { ShieldBasicAuthStrategyError } from "./shield-basic-auth.strategy";
5
+ import { ShieldBodyLimitError } from "./shield-body-limit.strategy";
6
+ import { ShieldCsrfStrategyError } from "./shield-csrf.strategy";
7
+ import { ShieldHcaptchaStrategyError } from "./shield-hcaptcha.strategy";
8
+ import { ShieldHcaptchaLocalStrategyError } from "./shield-hcaptcha-hono-local.strategy";
9
+ import { ShieldIpBlacklistStrategyError } from "./shield-ip-blacklist.strategy";
10
+ import { ShieldIpWhitelistStrategyError } from "./shield-ip-whitelist.strategy";
11
+ import { ShieldRateLimitStrategyError } from "./shield-rate-limit.strategy";
12
+ import { ShieldRecaptchaStrategyError } from "./shield-recaptcha.strategy";
13
+ import { ShieldTimeoutStrategyError } from "./shield-timeout.strategy";
14
+ import { ShieldWebhookStrategyError } from "./shield-webhook.strategy";
15
+
16
+ export const HttpExceptionErrors = {
17
+ ShieldApiKeyRejected: ShieldApiKeyStrategyError.Rejected,
18
+ ShieldAuthRejected: ShieldAuthStrategyError.Rejected,
19
+ ShieldBasicAuthRejected: ShieldBasicAuthStrategyError.Rejected,
20
+ ShieldBodyLimitTooBig: ShieldBodyLimitError.TooBig,
21
+ ShieldCsrfRejected: ShieldCsrfStrategyError.Rejected,
22
+ ShieldHcaptchaRejected: ShieldHcaptchaStrategyError.Rejected,
23
+ ShieldHcaptchaLocalRejected: ShieldHcaptchaLocalStrategyError.Rejected,
24
+ ShieldIpBlacklistRejected: ShieldIpBlacklistStrategyError.Rejected,
25
+ ShieldIpWhitelistRejected: ShieldIpWhitelistStrategyError.Rejected,
26
+ ShieldRateLimitRejected: ShieldRateLimitStrategyError.Rejected,
27
+ ShieldRecaptchaRejected: ShieldRecaptchaStrategyError.Rejected,
28
+ ShieldTimeoutRejected: ShieldTimeoutStrategyError.Rejected,
29
+ ShieldWebhookRejected: ShieldWebhookStrategyError.Rejected,
30
+ ...FileUploaderError,
31
+ } as const;
package/src/index.ts CHANGED
@@ -180,6 +180,10 @@ export * from "./file-renamer.port";
180
180
  export * from "./file-renamer-node.adapter";
181
181
  export * from "./file-renamer-node-forgiving.adapter";
182
182
  export * from "./file-renamer-noop.adapter";
183
+ export * from "./file-type-detector.strategy";
184
+ export * from "./file-type-detector-fixed.strategy";
185
+ export * from "./file-type-detector-magic-bytes.strategy";
186
+ export * from "./file-type-detector-text.strategy";
183
187
  export * from "./file-uploader.middleware";
184
188
  export * from "./file-uploader-hono.middleware";
185
189
  export * from "./file-writer.adapter";
@@ -212,6 +216,7 @@ export * from "./hcaptcha-site-key.vo";
212
216
  export * from "./healthcheck.handler";
213
217
  export * from "./healthcheck-hono.handler";
214
218
  export * from "./hostname.vo";
219
+ export * from "./http-exception-errors";
215
220
  export * from "./http-logger.middleware";
216
221
  export * from "./http-logger-hono.middleware";
217
222
  export * from "./id-provider.port";
@@ -261,7 +266,6 @@ export * from "./in-flight-requests-hono.middleware";
261
266
  export * from "./in-flight-requests-tracker.service";
262
267
  export * from "./instrumentation.service";
263
268
  export * from "./invariant.service";
264
- export * from "./invariant-error-handler.service";
265
269
  export * from "./job.types";
266
270
  export * from "./job-claimer.port";
267
271
  export * from "./job-claimer-noop.adapter";
@@ -537,6 +541,7 @@ export * from "./subject-segment-method.strategy";
537
541
  export * from "./subject-segment-param.strategy";
538
542
  export * from "./subject-segment-path.strategy";
539
543
  export * from "./subject-segment-query.strategy";
544
+ export * from "./subject-segment-remote-ip.strategy";
540
545
  export * from "./subject-segment-request.strategy";
541
546
  export * from "./subject-segment-user.strategy";
542
547
  export * from "./temporary-file.port";
@@ -576,6 +581,7 @@ export * from "./weak-etag-extractor-header.strategy";
576
581
  export * from "./weak-etag-extractor-hono.middleware";
577
582
  export * from "./weak-etag-extractor-param.strategy";
578
583
  export * from "./webhook-body-builder.strategy";
584
+ export * from "./webhook-body-builder-signed-headers.strategy";
579
585
  export * from "./webhook-body-builder-text.strategy";
580
586
  export * from "./webhook-id.vo";
581
587
  export * from "./webhook-id-extractor.strategy";
@@ -3,8 +3,12 @@ import {
3
3
  PrerequisiteVerificationOutcome,
4
4
  type PrerequisiteVerificationResult,
5
5
  } from "./prerequisite-verifier.port";
6
+ import type { RedactorStrategy } from "./redactor.strategy";
6
7
 
7
- export type ReadinessConfig = { prerequisites: ReadonlyArray<Prerequisite> };
8
+ export type ReadinessConfig = {
9
+ prerequisites: ReadonlyArray<Prerequisite>;
10
+ redactor: RedactorStrategy;
11
+ };
8
12
 
9
13
  export enum ReadinessStatusCode {
10
14
  Ready = 200,
@@ -28,7 +32,7 @@ export class ReadinessHandler {
28
32
  const details = await Promise.all(
29
33
  prerequisites.map(async (prerequisite) => ({
30
34
  label: prerequisite.label,
31
- outcome: await prerequisite.build().verify(),
35
+ outcome: this.config.redactor.redact(await prerequisite.build().verify()),
32
36
  })),
33
37
  );
34
38
 
@@ -37,7 +37,10 @@ export class RemoteFileStorageDiskAdapter implements RemoteFileStoragePort {
37
37
  const parts = key.split("/");
38
38
  const filename = tools.Filename.fromString(parts.pop() as string);
39
39
 
40
- const directory = v.parse(tools.DirectoryPathAbsoluteSchema, `${this.config.root}/${parts.join("/")}`);
40
+ if (parts.length === 0) return tools.FilePathAbsolute.fromPartsSafe(this.config.root, filename);
41
+
42
+ const base = this.config.root === "/" ? "" : this.config.root;
43
+ const directory = v.parse(tools.DirectoryPathAbsoluteSchema, `${base}/${parts.join("/")}`);
41
44
 
42
45
  return tools.FilePathAbsolute.fromPartsSafe(directory, filename);
43
46
  }
@@ -34,9 +34,13 @@ export class SealerAesGcmAdapter implements SealerPort {
34
34
 
35
35
  const key = await this.deps.CryptoKeyProvider.get();
36
36
 
37
- const encrypted = Uint8Array.fromBase64(value.slice(SealerAesGcmAdapter.PREFIX.length));
38
- const plaintext = await CryptoAesGcm.decrypt(key, encrypted);
37
+ try {
38
+ const encrypted = Uint8Array.fromBase64(value.slice(SealerAesGcmAdapter.PREFIX.length));
39
+ const plaintext = await CryptoAesGcm.decrypt(key, encrypted);
39
40
 
40
- return JSON.parse(new TextDecoder().decode(plaintext));
41
+ return JSON.parse(new TextDecoder().decode(plaintext));
42
+ } catch {
43
+ throw new Error(SealerAesGcmAdapterError.InvalidPayload);
44
+ }
41
45
  }
42
46
  }
@@ -1,6 +1,5 @@
1
1
  import * as v from "valibot";
2
2
  import { ALL_BOTS } from "./bots.vo";
3
- import { Client } from "./client.vo";
4
3
  import type { RequestContext } from "./request-context.port";
5
4
  import type { SecurityRuleStrategy } from "./security-rule.strategy";
6
5
  import { SecurityRuleName, type SecurityRuleNameType } from "./security-rule-name.vo";
@@ -9,9 +8,11 @@ export class SecurityRuleUserAgentStrategy implements SecurityRuleStrategy {
9
8
  constructor(private readonly blacklist: ReadonlyArray<string> = ALL_BOTS) {}
10
9
 
11
10
  async isViolated(context: RequestContext): Promise<boolean> {
12
- const client = Client.fromParts(context.identity.ip(), context.identity.ua());
11
+ const ua = context.identity.ua()?.toLowerCase();
13
12
 
14
- return this.blacklist.some((bot) => client.hasSameUa(Client.fromParts(undefined, bot)));
13
+ if (!ua) return false;
14
+
15
+ return this.blacklist.some((bot) => ua.includes(bot.toLowerCase()));
15
16
  }
16
17
 
17
18
  get name(): SecurityRuleNameType {
@@ -7,25 +7,31 @@ import type { SecurityRuleStrategy } from "./security-rule.strategy";
7
7
  import { SecurityRuleName, type SecurityRuleNameType } from "./security-rule-name.vo";
8
8
  import { SubjectRequestResolver } from "./subject-request-resolver.vo";
9
9
  import { SubjectSegmentFixedStrategy } from "./subject-segment-fixed.strategy";
10
- import { SubjectSegmentIpStrategy } from "./subject-segment-ip.strategy";
10
+ import type { SubjectSegmentRequestStrategy } from "./subject-segment-request.strategy";
11
11
 
12
12
  type Dependencies = { CacheRepository: CacheRepositoryPort; HashContent: HashContentStrategy };
13
- type Config = { threshold: tools.IntegerPositiveType };
13
+ type Config = {
14
+ threshold: tools.IntegerPositiveType;
15
+ segments: ReadonlyArray<SubjectSegmentRequestStrategy>;
16
+ };
14
17
 
15
18
  export class SecurityRuleViolationThresholdStrategy implements SecurityRuleStrategy {
19
+ private readonly resolver: SubjectRequestResolver;
20
+
16
21
  constructor(
17
22
  private readonly rule: SecurityRuleStrategy,
18
23
  private readonly config: Config,
19
24
  private readonly deps: Dependencies,
20
- ) {}
25
+ ) {
26
+ this.resolver = new SubjectRequestResolver(
27
+ [new SubjectSegmentFixedStrategy(this.name), ...this.config.segments],
28
+ this.deps,
29
+ );
30
+ }
21
31
 
22
32
  // Best-effort increment, occasional lost increments are acceptable for concurrent requests.
23
33
  async isViolated(context: RequestContext): Promise<boolean> {
24
- const resolver = new SubjectRequestResolver(
25
- [new SubjectSegmentFixedStrategy(this.name), new SubjectSegmentIpStrategy()],
26
- this.deps,
27
- );
28
- const subject = await resolver.resolve(context);
34
+ const subject = await this.resolver.resolve(context);
29
35
 
30
36
  const violated = await this.rule.isViolated(context);
31
37
 
@@ -12,9 +12,11 @@ export class ShieldBodyLimitStrategy {
12
12
  evaluate(context: HasRequestHeader): boolean {
13
13
  const header = context.request.header("content-length");
14
14
 
15
+ if (header === undefined) return true;
16
+
15
17
  const contentLength = v.safeParse(tools.SizeBytes, Number(header));
16
18
 
17
- if (!contentLength.success) return true;
19
+ if (!contentLength.success) return false;
18
20
 
19
21
  const size = tools.Size.fromBytes(contentLength.output);
20
22
 
@@ -18,8 +18,13 @@ export class ShieldHcaptchaLocalHonoStrategy implements MiddlewareHonoPort {
18
18
 
19
19
  private static readonly TOKEN_LOCAL = "10000000-aaaa-bbbb-cccc-000000000001";
20
20
 
21
+ private static readonly HOSTNAME_LOCAL = "local-pass";
22
+
21
23
  constructor() {
22
- this.strategy = new ShieldHcaptchaStrategy(ShieldHcaptchaLocalHonoStrategy.SECRET_KEY_LOCAL);
24
+ this.strategy = new ShieldHcaptchaStrategy({
25
+ secretKey: ShieldHcaptchaLocalHonoStrategy.SECRET_KEY_LOCAL,
26
+ hostname: ShieldHcaptchaLocalHonoStrategy.HOSTNAME_LOCAL,
27
+ });
23
28
  }
24
29
 
25
30
  handle(): MiddlewareHandler {
@@ -1,15 +1,18 @@
1
1
  import type { MiddlewareHandler } from "hono";
2
2
  import { HTTPException } from "hono/http-exception";
3
- import type { HCaptchaSecretKeyType } from "./hcaptcha-secret-key.vo";
4
3
  import type { MiddlewareHonoPort } from "./middleware-hono.port";
5
4
  import { RequestContextHonoAdapter } from "./request-context-hono.adapter";
6
- import { ShieldHcaptchaStrategy, ShieldHcaptchaStrategyError } from "./shield-hcaptcha.strategy";
5
+ import {
6
+ type ShieldHcaptchaConfig,
7
+ ShieldHcaptchaStrategy,
8
+ ShieldHcaptchaStrategyError,
9
+ } from "./shield-hcaptcha.strategy";
7
10
 
8
11
  export class ShieldHcaptchaHonoStrategy implements MiddlewareHonoPort {
9
12
  private readonly strategy: ShieldHcaptchaStrategy;
10
13
 
11
- constructor(secretKey: HCaptchaSecretKeyType) {
12
- this.strategy = new ShieldHcaptchaStrategy(secretKey);
14
+ constructor(config: ShieldHcaptchaConfig) {
15
+ this.strategy = new ShieldHcaptchaStrategy(config);
13
16
  }
14
17
 
15
18
  handle(): MiddlewareHandler {
@@ -6,10 +6,12 @@ export const ShieldHcaptchaStrategyError = { Rejected: "shield.hcaptcha.rejected
6
6
 
7
7
  export const ShieldHcaptchaStrategyField = "h-captcha-response";
8
8
 
9
+ export type ShieldHcaptchaConfig = { secretKey: HCaptchaSecretKeyType; hostname: string };
10
+
9
11
  export class ShieldHcaptchaStrategy {
10
12
  private readonly hcaptcha = new HCaptchaService();
11
13
 
12
- constructor(private readonly secretKey: HCaptchaSecretKeyType) {}
14
+ constructor(private readonly config: ShieldHcaptchaConfig) {}
13
15
 
14
16
  async evaluate(context: HasRequestForm & HasRequestJson): Promise<boolean> {
15
17
  try {
@@ -23,9 +25,12 @@ export class ShieldHcaptchaStrategy {
23
25
 
24
26
  if (!token) return false;
25
27
 
26
- const result = await this.hcaptcha.verify(this.secretKey, token);
28
+ const result = await this.hcaptcha.verify(this.config.secretKey, token);
29
+
30
+ if (!result.success) return false;
31
+ if (result.hostname !== this.config.hostname) return false;
27
32
 
28
- return result.success;
33
+ return true;
29
34
  } catch {
30
35
  return false;
31
36
  }
@@ -4,8 +4,13 @@ import * as v from "valibot";
4
4
  import type { RecaptchaSecretKeyType } from "./recaptcha-secret-key.vo";
5
5
  import type { HasIdentityIp, HasRequestForm, HasRequestJson } from "./request-context.port";
6
6
 
7
- export type ShieldRecaptchaConfig = { secretKey: RecaptchaSecretKeyType; threshold?: number };
8
- export type RecaptchaResult = { success: boolean; score: number };
7
+ export type ShieldRecaptchaConfig = {
8
+ secretKey: RecaptchaSecretKeyType;
9
+ hostname: string;
10
+ action?: string;
11
+ threshold?: number;
12
+ };
13
+ export type RecaptchaResult = { success: boolean; score: number; hostname?: string; action?: string };
9
14
 
10
15
  export const ShieldRecaptchaStrategyError = { Rejected: "shield.recaptcha.rejected" };
11
16
 
@@ -51,6 +56,8 @@ export class ShieldRecaptchaStrategy {
51
56
 
52
57
  if (!result.success) return false;
53
58
  if (typeof result.score !== "number" || result.score < threshold) return false;
59
+ if (result.hostname !== this.config.hostname) return false;
60
+ if (this.config.action !== undefined && result.action !== this.config.action) return false;
54
61
 
55
62
  return true;
56
63
  } catch {
@@ -0,0 +1,14 @@
1
+ import type { HasIdentityRemoteIp } from "./request-context.port";
2
+ import {
3
+ SubjectSegmentRequestEmpty,
4
+ type SubjectSegmentRequestStrategy,
5
+ type SubjectSegmentType,
6
+ } from "./subject-segment-request.strategy";
7
+
8
+ export class SubjectSegmentRemoteIpStrategy implements SubjectSegmentRequestStrategy {
9
+ readonly label = "remote-ip";
10
+
11
+ create(context: HasIdentityRemoteIp): SubjectSegmentType {
12
+ return context.identity.remoteIp() ?? SubjectSegmentRequestEmpty;
13
+ }
14
+ }
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  HasIdentityIp,
3
+ HasIdentityRemoteIp,
3
4
  HasIdentityUserId,
4
5
  HasRequestCookie,
5
6
  HasRequestHeader,
@@ -21,6 +22,7 @@ type RequestContextCapabilities = HasRequestCookie &
21
22
  HasRequestQueries &
22
23
  HasRequestParam &
23
24
  HasIdentityIp &
25
+ HasIdentityRemoteIp &
24
26
  HasIdentityUserId;
25
27
 
26
28
  export interface SubjectSegmentRequestStrategy {
@@ -0,0 +1,12 @@
1
+ import type { HasRequestHeader, HasRequestText } from "./request-context.port";
2
+ import type { WebhookBodyBuilderStrategy } from "./webhook-body-builder.strategy";
3
+
4
+ export class WebhookBodyBuilderSignedHeadersStrategy implements WebhookBodyBuilderStrategy {
5
+ constructor(private readonly headers: ReadonlyArray<string>) {}
6
+
7
+ async build(context: HasRequestHeader & HasRequestText): Promise<string> {
8
+ const headers = this.headers.map((name) => [name, context.request.header(name) ?? null]);
9
+
10
+ return JSON.stringify([headers, await context.request.text()]);
11
+ }
12
+ }
@@ -3,11 +3,16 @@ import * as v from "valibot";
3
3
  export const WebhookSignatureError = {
4
4
  Type: "webhook.signature.type",
5
5
  Empty: "webhook.signature.empty",
6
+ InvalidHex: "webhook.signature.invalid.hex",
6
7
  };
7
8
 
9
+ // Hex chars allowed
10
+ const CHARS_WHITELIST = /^[a-fA-F0-9]+$/;
11
+
8
12
  export const WebhookSignature = v.pipe(
9
13
  v.string(WebhookSignatureError.Type),
10
14
  v.nonEmpty(WebhookSignatureError.Empty),
15
+ v.regex(CHARS_WHITELIST, WebhookSignatureError.InvalidHex),
11
16
  // Stryker disable next-line StringLiteral
12
17
  v.brand("WebhookSignature"),
13
18
  );
@@ -9,9 +9,10 @@ export class WebhookVerifierSha256Strategy implements WebhookVerifierStrategy {
9
9
  constructor(private readonly config: Config) {}
10
10
 
11
11
  verify(body: string, signature: WebhookSignatureType): boolean {
12
- const digest = this.config.WebhookSignatureCreator.create(body);
12
+ const digest = Buffer.from(this.config.WebhookSignatureCreator.create(body));
13
+ const candidate = Buffer.from(signature);
13
14
 
14
- if (digest.length !== signature.length) return false;
15
- return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
15
+ if (digest.length !== candidate.length) return false;
16
+ return timingSafeEqual(digest, candidate);
16
17
  }
17
18
  }
@@ -24,7 +24,7 @@ export type WoodchopperConfig = {
24
24
  level: LogLevelEnum;
25
25
  environment: NodeEnvironmentEnum;
26
26
  dispatcher: WoodchopperDispatcher;
27
- redactor?: RedactorStrategy;
27
+ redactor: RedactorStrategy;
28
28
  diagnostics?: WoodchopperDiagnosticsStrategy;
29
29
  };
30
30
 
@@ -88,9 +88,7 @@ export class Woodchopper implements LoggerPort, LoggerStatsProviderPort {
88
88
 
89
89
  let withRedaction: LoggerEntry;
90
90
  try {
91
- withRedaction = this.config.redactor
92
- ? this.config.redactor.redact(withInjectedFields)
93
- : withInjectedFields;
91
+ withRedaction = this.config.redactor.redact(withInjectedFields);
94
92
  } catch (error) {
95
93
  this.stats.recordDropped();
96
94
  this.config.diagnostics?.handle({ kind: "redaction", error });
@@ -1,13 +0,0 @@
1
- import { type Invariant } from "./invariant.service";
2
- type BaseInvariantType = Invariant<any>;
3
- type InvariantMessageType = BaseInvariantType["message"];
4
- type ErrorResponseTupleType = [{
5
- message: InvariantMessageType;
6
- _known: true;
7
- }, number];
8
- export declare class InvariantErrorHandler {
9
- static detect(invariants: ReadonlyArray<BaseInvariantType>, error: unknown): BaseInvariantType | null;
10
- static respond(error: BaseInvariantType): ErrorResponseTupleType;
11
- }
12
- export {};
13
- //# sourceMappingURL=invariant-error-handler.service.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"invariant-error-handler.service.d.ts","sourceRoot":"","sources":["../src/invariant-error-handler.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAwB,MAAM,qBAAqB,CAAC;AAE3E,KAAK,iBAAiB,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;AACxC,KAAK,oBAAoB,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;AACzD,KAAK,sBAAsB,GAAG,CAAC;IAAE,OAAO,EAAE,oBAAoB,CAAC;IAAC,MAAM,EAAE,IAAI,CAAA;CAAE,EAAE,MAAM,CAAC,CAAC;AAExF,qBAAa,qBAAqB;IAChC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,iBAAiB,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAEpG;IAED,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,iBAAiB,GAAG,sBAAsB,CAQ/D;CACF"}
@@ -1,15 +0,0 @@
1
- import { InvariantFailureKind } from "./invariant.service";
2
- export class InvariantErrorHandler {
3
- static detect(invariants, error) {
4
- return invariants.find((invariant) => error instanceof invariant.error) ?? null;
5
- }
6
- static respond(error) {
7
- const code = {
8
- [InvariantFailureKind.forbidden]: 403,
9
- [InvariantFailureKind.not_found]: 404,
10
- [InvariantFailureKind.precondition]: 400,
11
- };
12
- return [{ message: error.message, _known: true }, code[error.kind]];
13
- }
14
- }
15
- //# sourceMappingURL=invariant-error-handler.service.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"invariant-error-handler.service.js","sourceRoot":"","sources":["../src/invariant-error-handler.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAM3E,MAAM,OAAO,qBAAqB;IAChC,MAAM,CAAC,MAAM,CAAC,UAA4C,EAAE,KAAc;QACxE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,KAAK,YAAY,SAAS,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;IAClF,CAAC;IAED,MAAM,CAAC,OAAO,CAAC,KAAwB;QACrC,MAAM,IAAI,GAAyC;YACjD,CAAC,oBAAoB,CAAC,SAAS,CAAC,EAAE,GAAG;YACrC,CAAC,oBAAoB,CAAC,SAAS,CAAC,EAAE,GAAG;YACrC,CAAC,oBAAoB,CAAC,YAAY,CAAC,EAAE,GAAG;SACzC,CAAC;QAEF,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACtE,CAAC;CACF"}
@@ -1,21 +0,0 @@
1
- import { type Invariant, InvariantFailureKind } from "./invariant.service";
2
-
3
- type BaseInvariantType = Invariant<any>;
4
- type InvariantMessageType = BaseInvariantType["message"];
5
- type ErrorResponseTupleType = [{ message: InvariantMessageType; _known: true }, number];
6
-
7
- export class InvariantErrorHandler {
8
- static detect(invariants: ReadonlyArray<BaseInvariantType>, error: unknown): BaseInvariantType | null {
9
- return invariants.find((invariant) => error instanceof invariant.error) ?? null;
10
- }
11
-
12
- static respond(error: BaseInvariantType): ErrorResponseTupleType {
13
- const code: Record<InvariantFailureKind, number> = {
14
- [InvariantFailureKind.forbidden]: 403,
15
- [InvariantFailureKind.not_found]: 404,
16
- [InvariantFailureKind.precondition]: 400,
17
- };
18
-
19
- return [{ message: error.message, _known: true }, code[error.kind]];
20
- }
21
- }