@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.
- package/dist/error-classifier-http-exception-hono.strategy.d.ts +2 -4
- package/dist/error-classifier-http-exception-hono.strategy.d.ts.map +1 -1
- package/dist/error-classifier-http-exception-hono.strategy.js +5 -4
- package/dist/error-classifier-http-exception-hono.strategy.js.map +1 -1
- package/dist/error-classifier-invariant.strategy.d.ts +4 -5
- package/dist/error-classifier-invariant.strategy.d.ts.map +1 -1
- package/dist/error-classifier-invariant.strategy.js +11 -7
- package/dist/error-classifier-invariant.strategy.js.map +1 -1
- package/dist/error-classifier-validation.strategy.d.ts +2 -4
- package/dist/error-classifier-validation.strategy.d.ts.map +1 -1
- package/dist/error-classifier-validation.strategy.js +3 -3
- package/dist/error-classifier-validation.strategy.js.map +1 -1
- package/dist/error-hono.handler.d.ts +2 -2
- package/dist/error-hono.handler.d.ts.map +1 -1
- package/dist/error-hono.handler.js +2 -2
- package/dist/error-hono.handler.js.map +1 -1
- package/dist/error.handler.d.ts +6 -4
- package/dist/error.handler.d.ts.map +1 -1
- package/dist/error.handler.js +7 -6
- package/dist/error.handler.js.map +1 -1
- package/dist/file-type-detector-fixed.strategy.d.ts +8 -0
- package/dist/file-type-detector-fixed.strategy.d.ts.map +1 -0
- package/dist/file-type-detector-fixed.strategy.js +10 -0
- package/dist/file-type-detector-fixed.strategy.js.map +1 -0
- package/dist/file-type-detector-magic-bytes.strategy.d.ts +6 -0
- package/dist/file-type-detector-magic-bytes.strategy.d.ts.map +1 -0
- package/dist/file-type-detector-magic-bytes.strategy.js +25 -0
- package/dist/file-type-detector-magic-bytes.strategy.js.map +1 -0
- package/dist/file-type-detector-text.strategy.d.ts +8 -0
- package/dist/file-type-detector-text.strategy.d.ts.map +1 -0
- package/dist/file-type-detector-text.strategy.js +22 -0
- package/dist/file-type-detector-text.strategy.js.map +1 -0
- package/dist/file-type-detector.strategy.d.ts +5 -0
- package/dist/file-type-detector.strategy.d.ts.map +1 -0
- package/dist/file-type-detector.strategy.js +2 -0
- package/dist/file-type-detector.strategy.js.map +1 -0
- package/dist/file-uploader-hono.middleware.d.ts +6 -1
- package/dist/file-uploader-hono.middleware.d.ts.map +1 -1
- package/dist/file-uploader-hono.middleware.js +2 -2
- package/dist/file-uploader-hono.middleware.js.map +1 -1
- package/dist/file-uploader.middleware.d.ts +7 -1
- package/dist/file-uploader.middleware.d.ts.map +1 -1
- package/dist/file-uploader.middleware.js +6 -3
- package/dist/file-uploader.middleware.js.map +1 -1
- package/dist/hcaptcha.service.d.ts +1 -0
- package/dist/hcaptcha.service.d.ts.map +1 -1
- package/dist/healthcheck.handler.d.ts +1 -1
- package/dist/healthcheck.handler.d.ts.map +1 -1
- package/dist/healthcheck.handler.js +1 -1
- package/dist/healthcheck.handler.js.map +1 -1
- package/dist/http-exception-errors.d.ts +20 -0
- package/dist/http-exception-errors.d.ts.map +1 -0
- package/dist/http-exception-errors.js +31 -0
- package/dist/http-exception-errors.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/readiness.handler.d.ts +2 -0
- package/dist/readiness.handler.d.ts.map +1 -1
- package/dist/readiness.handler.js +1 -1
- package/dist/readiness.handler.js.map +1 -1
- package/dist/remote-file-storage-disk.adapter.d.ts.map +1 -1
- package/dist/remote-file-storage-disk.adapter.js +4 -1
- package/dist/remote-file-storage-disk.adapter.js.map +1 -1
- package/dist/sealer-aes-gcm.adapter.d.ts.map +1 -1
- package/dist/sealer-aes-gcm.adapter.js +8 -3
- package/dist/sealer-aes-gcm.adapter.js.map +1 -1
- package/dist/security-rule-user-agent.strategy.d.ts.map +1 -1
- package/dist/security-rule-user-agent.strategy.js +4 -3
- package/dist/security-rule-user-agent.strategy.js.map +1 -1
- package/dist/security-rule-violation-threshold.strategy.d.ts +3 -0
- package/dist/security-rule-violation-threshold.strategy.d.ts.map +1 -1
- package/dist/security-rule-violation-threshold.strategy.js +3 -3
- package/dist/security-rule-violation-threshold.strategy.js.map +1 -1
- package/dist/shield-body-limit.strategy.d.ts.map +1 -1
- package/dist/shield-body-limit.strategy.js +3 -1
- package/dist/shield-body-limit.strategy.js.map +1 -1
- package/dist/shield-hcaptcha-hono-local.strategy.d.ts +1 -0
- package/dist/shield-hcaptcha-hono-local.strategy.d.ts.map +1 -1
- package/dist/shield-hcaptcha-hono-local.strategy.js +5 -1
- package/dist/shield-hcaptcha-hono-local.strategy.js.map +1 -1
- package/dist/shield-hcaptcha-hono.strategy.d.ts +2 -2
- package/dist/shield-hcaptcha-hono.strategy.d.ts.map +1 -1
- package/dist/shield-hcaptcha-hono.strategy.js +3 -3
- package/dist/shield-hcaptcha-hono.strategy.js.map +1 -1
- package/dist/shield-hcaptcha.strategy.d.ts +6 -2
- package/dist/shield-hcaptcha.strategy.d.ts.map +1 -1
- package/dist/shield-hcaptcha.strategy.js +9 -5
- package/dist/shield-hcaptcha.strategy.js.map +1 -1
- package/dist/shield-recaptcha.strategy.d.ts +4 -0
- package/dist/shield-recaptcha.strategy.d.ts.map +1 -1
- package/dist/shield-recaptcha.strategy.js +4 -0
- package/dist/shield-recaptcha.strategy.js.map +1 -1
- package/dist/subject-segment-remote-ip.strategy.d.ts +7 -0
- package/dist/subject-segment-remote-ip.strategy.d.ts.map +1 -0
- package/dist/subject-segment-remote-ip.strategy.js +8 -0
- package/dist/subject-segment-remote-ip.strategy.js.map +1 -0
- package/dist/subject-segment-request.strategy.d.ts +2 -2
- package/dist/subject-segment-request.strategy.d.ts.map +1 -1
- package/dist/subject-segment-request.strategy.js.map +1 -1
- package/dist/webhook-body-builder-signed-headers.strategy.d.ts +8 -0
- package/dist/webhook-body-builder-signed-headers.strategy.d.ts.map +1 -0
- package/dist/webhook-body-builder-signed-headers.strategy.js +11 -0
- package/dist/webhook-body-builder-signed-headers.strategy.js.map +1 -0
- package/dist/webhook-signature.vo.d.ts +2 -1
- package/dist/webhook-signature.vo.d.ts.map +1 -1
- package/dist/webhook-signature.vo.js +4 -1
- package/dist/webhook-signature.vo.js.map +1 -1
- package/dist/webhook-verifier-sha256.strategy.d.ts.map +1 -1
- package/dist/webhook-verifier-sha256.strategy.js +4 -3
- package/dist/webhook-verifier-sha256.strategy.js.map +1 -1
- package/dist/woodchopper.d.ts +1 -1
- package/dist/woodchopper.d.ts.map +1 -1
- package/dist/woodchopper.js +1 -3
- package/dist/woodchopper.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +7 -1
- package/src/error-classifier-http-exception-hono.strategy.ts +14 -3
- package/src/error-classifier-invariant.strategy.ts +19 -9
- package/src/error-classifier-validation.strategy.ts +9 -5
- package/src/error-hono.handler.ts +3 -3
- package/src/error.handler.ts +19 -11
- package/src/file-type-detector-fixed.strategy.ts +10 -0
- package/src/file-type-detector-magic-bytes.strategy.ts +32 -0
- package/src/file-type-detector-text.strategy.ts +25 -0
- package/src/file-type-detector.strategy.ts +5 -0
- package/src/file-uploader-hono.middleware.ts +5 -2
- package/src/file-uploader.middleware.ts +10 -3
- package/src/hcaptcha.service.ts +1 -1
- package/src/healthcheck.handler.ts +2 -2
- package/src/http-exception-errors.ts +31 -0
- package/src/index.ts +7 -1
- package/src/readiness.handler.ts +6 -2
- package/src/remote-file-storage-disk.adapter.ts +4 -1
- package/src/sealer-aes-gcm.adapter.ts +7 -3
- package/src/security-rule-user-agent.strategy.ts +4 -3
- package/src/security-rule-violation-threshold.strategy.ts +14 -8
- package/src/shield-body-limit.strategy.ts +3 -1
- package/src/shield-hcaptcha-hono-local.strategy.ts +6 -1
- package/src/shield-hcaptcha-hono.strategy.ts +7 -4
- package/src/shield-hcaptcha.strategy.ts +8 -3
- package/src/shield-recaptcha.strategy.ts +9 -2
- package/src/subject-segment-remote-ip.strategy.ts +14 -0
- package/src/subject-segment-request.strategy.ts +2 -0
- package/src/webhook-body-builder-signed-headers.strategy.ts +12 -0
- package/src/webhook-signature.vo.ts +5 -0
- package/src/webhook-verifier-sha256.strategy.ts +4 -3
- package/src/woodchopper.ts +2 -4
- package/dist/invariant-error-handler.service.d.ts +0 -13
- package/dist/invariant-error-handler.service.d.ts.map +0 -1
- package/dist/invariant-error-handler.service.js +0 -15
- package/dist/invariant-error-handler.service.js.map +0 -1
- 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
|
+
}
|
|
@@ -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(
|
|
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
|
-
|
|
32
|
-
|
|
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 };
|
package/src/hcaptcha.service.ts
CHANGED
|
@@ -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
|
|
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
|
|
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";
|
package/src/readiness.handler.ts
CHANGED
|
@@ -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 = {
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
37
|
+
try {
|
|
38
|
+
const encrypted = Uint8Array.fromBase64(value.slice(SealerAesGcmAdapter.PREFIX.length));
|
|
39
|
+
const plaintext = await CryptoAesGcm.decrypt(key, encrypted);
|
|
39
40
|
|
|
40
|
-
|
|
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
|
|
11
|
+
const ua = context.identity.ua()?.toLowerCase();
|
|
13
12
|
|
|
14
|
-
|
|
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 {
|
|
10
|
+
import type { SubjectSegmentRequestStrategy } from "./subject-segment-request.strategy";
|
|
11
11
|
|
|
12
12
|
type Dependencies = { CacheRepository: CacheRepositoryPort; HashContent: HashContentStrategy };
|
|
13
|
-
type Config = {
|
|
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
|
|
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
|
|
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(
|
|
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 {
|
|
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(
|
|
12
|
-
this.strategy = new ShieldHcaptchaStrategy(
|
|
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
|
|
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
|
|
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 = {
|
|
8
|
-
|
|
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 !==
|
|
15
|
-
return timingSafeEqual(
|
|
15
|
+
if (digest.length !== candidate.length) return false;
|
|
16
|
+
return timingSafeEqual(digest, candidate);
|
|
16
17
|
}
|
|
17
18
|
}
|
package/src/woodchopper.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type WoodchopperConfig = {
|
|
|
24
24
|
level: LogLevelEnum;
|
|
25
25
|
environment: NodeEnvironmentEnum;
|
|
26
26
|
dispatcher: WoodchopperDispatcher;
|
|
27
|
-
redactor
|
|
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
|
-
}
|