@beignet/core 0.0.1
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/CHANGELOG.md +5 -0
- package/README.md +288 -0
- package/dist/application/index.d.ts +260 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +324 -0
- package/dist/application/index.js.map +1 -0
- package/dist/client/client.d.ts +241 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +531 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +10 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +139 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/config/index.d.ts +122 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +216 -0
- package/dist/config/index.js.map +1 -0
- package/dist/contracts/contract-builder.d.ts +121 -0
- package/dist/contracts/contract-builder.d.ts.map +1 -0
- package/dist/contracts/contract-builder.js +346 -0
- package/dist/contracts/contract-builder.js.map +1 -0
- package/dist/contracts/contract-group.d.ts +106 -0
- package/dist/contracts/contract-group.d.ts.map +1 -0
- package/dist/contracts/contract-group.js +240 -0
- package/dist/contracts/contract-group.js.map +1 -0
- package/dist/contracts/contract-like.d.ts +21 -0
- package/dist/contracts/contract-like.d.ts.map +1 -0
- package/dist/contracts/contract-like.js +9 -0
- package/dist/contracts/contract-like.js.map +1 -0
- package/dist/contracts/index.d.ts +15 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +11 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/openapi-meta.d.ts +23 -0
- package/dist/contracts/openapi-meta.d.ts.map +1 -0
- package/dist/contracts/openapi-meta.js +2 -0
- package/dist/contracts/openapi-meta.js.map +1 -0
- package/dist/contracts/path-template.d.ts +17 -0
- package/dist/contracts/path-template.d.ts.map +1 -0
- package/dist/contracts/path-template.js +50 -0
- package/dist/contracts/path-template.js.map +1 -0
- package/dist/contracts/rate-limit.d.ts +50 -0
- package/dist/contracts/rate-limit.d.ts.map +1 -0
- package/dist/contracts/rate-limit.js +2 -0
- package/dist/contracts/rate-limit.js.map +1 -0
- package/dist/contracts/types.d.ts +97 -0
- package/dist/contracts/types.d.ts.map +1 -0
- package/dist/contracts/types.js +54 -0
- package/dist/contracts/types.js.map +1 -0
- package/dist/contracts/utils.d.ts +3 -0
- package/dist/contracts/utils.d.ts.map +1 -0
- package/dist/contracts/utils.js +44 -0
- package/dist/contracts/utils.js.map +1 -0
- package/dist/domain/entity.d.ts +87 -0
- package/dist/domain/entity.d.ts.map +1 -0
- package/dist/domain/entity.js +155 -0
- package/dist/domain/entity.js.map +1 -0
- package/dist/domain/events.d.ts +41 -0
- package/dist/domain/events.d.ts.map +1 -0
- package/dist/domain/events.js +21 -0
- package/dist/domain/events.js.map +1 -0
- package/dist/domain/index.d.ts +14 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +14 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/value-object.d.ts +60 -0
- package/dist/domain/value-object.d.ts.map +1 -0
- package/dist/domain/value-object.js +87 -0
- package/dist/domain/value-object.js.map +1 -0
- package/dist/errors/catalog.d.ts +71 -0
- package/dist/errors/catalog.d.ts.map +1 -0
- package/dist/errors/catalog.js +71 -0
- package/dist/errors/catalog.js.map +1 -0
- package/dist/errors/http.d.ts +77 -0
- package/dist/errors/http.d.ts.map +1 -0
- package/dist/errors/http.js +74 -0
- package/dist/errors/http.js.map +1 -0
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +14 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/response.d.ts +26 -0
- package/dist/errors/response.d.ts.map +1 -0
- package/dist/errors/response.js +34 -0
- package/dist/errors/response.js.map +1 -0
- package/dist/errors/validation.d.ts +18 -0
- package/dist/errors/validation.d.ts.map +1 -0
- package/dist/errors/validation.js +21 -0
- package/dist/errors/validation.js.map +1 -0
- package/dist/events/index.d.ts +58 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +102 -0
- package/dist/events/index.js.map +1 -0
- package/dist/jobs/index.d.ts +56 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +89 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/mail/index.d.ts +75 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +84 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/openapi/index.d.ts +207 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +449 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/schema-introspector.d.ts +38 -0
- package/dist/openapi/schema-introspector.d.ts.map +1 -0
- package/dist/openapi/schema-introspector.js +67 -0
- package/dist/openapi/schema-introspector.js.map +1 -0
- package/dist/ports/audit.d.ts +58 -0
- package/dist/ports/audit.d.ts.map +1 -0
- package/dist/ports/audit.js +74 -0
- package/dist/ports/audit.js.map +1 -0
- package/dist/ports/auth.d.ts +23 -0
- package/dist/ports/auth.d.ts.map +1 -0
- package/dist/ports/auth.js +31 -0
- package/dist/ports/auth.js.map +1 -0
- package/dist/ports/builder.d.ts +61 -0
- package/dist/ports/builder.d.ts.map +1 -0
- package/dist/ports/builder.js +48 -0
- package/dist/ports/builder.js.map +1 -0
- package/dist/ports/cache.d.ts +15 -0
- package/dist/ports/cache.d.ts.map +1 -0
- package/dist/ports/cache.js +57 -0
- package/dist/ports/cache.js.map +1 -0
- package/dist/ports/clock.d.ts +10 -0
- package/dist/ports/clock.d.ts.map +1 -0
- package/dist/ports/clock.js +21 -0
- package/dist/ports/clock.js.map +1 -0
- package/dist/ports/events.d.ts +71 -0
- package/dist/ports/events.d.ts.map +1 -0
- package/dist/ports/events.js +2 -0
- package/dist/ports/events.js.map +1 -0
- package/dist/ports/id-generator.d.ts +12 -0
- package/dist/ports/id-generator.d.ts.map +1 -0
- package/dist/ports/id-generator.js +22 -0
- package/dist/ports/id-generator.js.map +1 -0
- package/dist/ports/index.d.ts +98 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +67 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/ports/logger.d.ts +22 -0
- package/dist/ports/logger.d.ts.map +1 -0
- package/dist/ports/logger.js +34 -0
- package/dist/ports/logger.js.map +1 -0
- package/dist/ports/mailer.d.ts +6 -0
- package/dist/ports/mailer.d.ts.map +1 -0
- package/dist/ports/mailer.js +2 -0
- package/dist/ports/mailer.js.map +1 -0
- package/dist/ports/policy.d.ts +53 -0
- package/dist/ports/policy.d.ts.map +1 -0
- package/dist/ports/policy.js +81 -0
- package/dist/ports/policy.js.map +1 -0
- package/dist/ports/rate-limit.d.ts +41 -0
- package/dist/ports/rate-limit.d.ts.map +1 -0
- package/dist/ports/rate-limit.js +37 -0
- package/dist/ports/rate-limit.js.map +1 -0
- package/dist/ports/redaction.d.ts +26 -0
- package/dist/ports/redaction.d.ts.map +1 -0
- package/dist/ports/redaction.js +126 -0
- package/dist/ports/redaction.js.map +1 -0
- package/dist/ports/schedules.d.ts +9 -0
- package/dist/ports/schedules.d.ts.map +1 -0
- package/dist/ports/schedules.js +2 -0
- package/dist/ports/schedules.js.map +1 -0
- package/dist/ports/storage.d.ts +47 -0
- package/dist/ports/storage.d.ts.map +1 -0
- package/dist/ports/storage.js +185 -0
- package/dist/ports/storage.js.map +1 -0
- package/dist/ports/testing.d.ts +73 -0
- package/dist/ports/testing.d.ts.map +1 -0
- package/dist/ports/testing.js +105 -0
- package/dist/ports/testing.js.map +1 -0
- package/dist/ports/unit-of-work.d.ts +56 -0
- package/dist/ports/unit-of-work.d.ts.map +1 -0
- package/dist/ports/unit-of-work.js +64 -0
- package/dist/ports/unit-of-work.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/instrumentation.d.ts +91 -0
- package/dist/providers/instrumentation.d.ts.map +1 -0
- package/dist/providers/instrumentation.js +93 -0
- package/dist/providers/instrumentation.js.map +1 -0
- package/dist/providers/provider.d.ts +146 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +31 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/schedules/index.d.ts +105 -0
- package/dist/schedules/index.d.ts.map +1 -0
- package/dist/schedules/index.js +178 -0
- package/dist/schedules/index.js.map +1 -0
- package/dist/server/contract-like.d.ts +5 -0
- package/dist/server/contract-like.d.ts.map +1 -0
- package/dist/server/contract-like.js +5 -0
- package/dist/server/contract-like.js.map +1 -0
- package/dist/server/health.d.ts +41 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +46 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/hooks/auth.d.ts +42 -0
- package/dist/server/hooks/auth.d.ts.map +1 -0
- package/dist/server/hooks/auth.js +61 -0
- package/dist/server/hooks/auth.js.map +1 -0
- package/dist/server/hooks/cors.d.ts +13 -0
- package/dist/server/hooks/cors.d.ts.map +1 -0
- package/dist/server/hooks/cors.js +70 -0
- package/dist/server/hooks/cors.js.map +1 -0
- package/dist/server/hooks/errors.d.ts +66 -0
- package/dist/server/hooks/errors.d.ts.map +1 -0
- package/dist/server/hooks/errors.js +83 -0
- package/dist/server/hooks/errors.js.map +1 -0
- package/dist/server/hooks/index.d.ts +12 -0
- package/dist/server/hooks/index.d.ts.map +1 -0
- package/dist/server/hooks/index.js +12 -0
- package/dist/server/hooks/index.js.map +1 -0
- package/dist/server/hooks/logging.d.ts +33 -0
- package/dist/server/hooks/logging.d.ts.map +1 -0
- package/dist/server/hooks/logging.js +90 -0
- package/dist/server/hooks/logging.js.map +1 -0
- package/dist/server/hooks/rate-limit.d.ts +29 -0
- package/dist/server/hooks/rate-limit.d.ts.map +1 -0
- package/dist/server/hooks/rate-limit.js +93 -0
- package/dist/server/hooks/rate-limit.js.map +1 -0
- package/dist/server/hooks/utils.d.ts +9 -0
- package/dist/server/hooks/utils.d.ts.map +1 -0
- package/dist/server/hooks/utils.js +16 -0
- package/dist/server/hooks/utils.js.map +1 -0
- package/dist/server/hooks.d.ts +2 -0
- package/dist/server/hooks.d.ts.map +1 -0
- package/dist/server/hooks.js +2 -0
- package/dist/server/hooks.js.map +1 -0
- package/dist/server/http.d.ts +124 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +2 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +19 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +15 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/openapi.d.ts +32 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/openapi.js +43 -0
- package/dist/server/openapi.js.map +1 -0
- package/dist/server/providers/index.d.ts +4 -0
- package/dist/server/providers/index.d.ts.map +1 -0
- package/dist/server/providers/index.js +4 -0
- package/dist/server/providers/index.js.map +1 -0
- package/dist/server/providers/loadProviderConfig.d.ts +7 -0
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -0
- package/dist/server/providers/loadProviderConfig.js +42 -0
- package/dist/server/providers/loadProviderConfig.js.map +1 -0
- package/dist/server/server.d.ts +86 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +1031 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/types.d.ts +3 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/package.json +129 -0
- package/src/application/index.ts +747 -0
- package/src/client/client.ts +1105 -0
- package/src/client/index.ts +45 -0
- package/src/client/types.ts +305 -0
- package/src/config/index.ts +497 -0
- package/src/contracts/contract-builder.ts +583 -0
- package/src/contracts/contract-group.ts +502 -0
- package/src/contracts/contract-like.ts +29 -0
- package/src/contracts/index.ts +53 -0
- package/src/contracts/openapi-meta.ts +22 -0
- package/src/contracts/path-template.ts +91 -0
- package/src/contracts/rate-limit.ts +50 -0
- package/src/contracts/types.ts +207 -0
- package/src/contracts/utils.ts +56 -0
- package/src/domain/entity.ts +256 -0
- package/src/domain/events.ts +52 -0
- package/src/domain/index.ts +18 -0
- package/src/domain/value-object.ts +135 -0
- package/src/errors/catalog.ts +149 -0
- package/src/errors/http.ts +80 -0
- package/src/errors/index.ts +28 -0
- package/src/errors/response.ts +54 -0
- package/src/errors/validation.ts +35 -0
- package/src/events/index.ts +246 -0
- package/src/jobs/index.ts +211 -0
- package/src/mail/index.ts +177 -0
- package/src/openapi/index.ts +865 -0
- package/src/openapi/schema-introspector.ts +107 -0
- package/src/ports/audit.ts +176 -0
- package/src/ports/auth.ts +76 -0
- package/src/ports/builder.ts +97 -0
- package/src/ports/cache.ts +94 -0
- package/src/ports/clock.ts +34 -0
- package/src/ports/events.ts +100 -0
- package/src/ports/id-generator.ts +36 -0
- package/src/ports/index.ts +221 -0
- package/src/ports/logger.ts +67 -0
- package/src/ports/policy.ts +242 -0
- package/src/ports/rate-limit.ts +91 -0
- package/src/ports/redaction.ts +199 -0
- package/src/ports/storage.ts +282 -0
- package/src/ports/testing.ts +234 -0
- package/src/ports/unit-of-work.ts +134 -0
- package/src/providers/index.ts +40 -0
- package/src/providers/instrumentation.ts +248 -0
- package/src/providers/provider.ts +191 -0
- package/src/schedules/index.ts +442 -0
- package/src/server/contract-like.ts +8 -0
- package/src/server/health.ts +82 -0
- package/src/server/hooks/auth.ts +147 -0
- package/src/server/hooks/cors.ts +87 -0
- package/src/server/hooks/errors.ts +126 -0
- package/src/server/hooks/index.ts +43 -0
- package/src/server/hooks/logging.ts +121 -0
- package/src/server/hooks/rate-limit.ts +171 -0
- package/src/server/hooks/utils.ts +16 -0
- package/src/server/hooks.ts +1 -0
- package/src/server/http.ts +189 -0
- package/src/server/index.ts +35 -0
- package/src/server/openapi.ts +72 -0
- package/src/server/providers/index.ts +3 -0
- package/src/server/providers/loadProviderConfig.ts +72 -0
- package/src/server/server.ts +1521 -0
- package/src/server/types.ts +2 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export const DEFAULT_REDACTED_VALUE = "[redacted]";
|
|
2
|
+
export const DEFAULT_TRUNCATED_VALUE = "[truncated]";
|
|
3
|
+
export const DEFAULT_CIRCULAR_VALUE = "[circular]";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_SENSITIVE_KEYS = [
|
|
6
|
+
"authorization",
|
|
7
|
+
"cookie",
|
|
8
|
+
"set-cookie",
|
|
9
|
+
"x-api-key",
|
|
10
|
+
"api-key",
|
|
11
|
+
"apikey",
|
|
12
|
+
"access-token",
|
|
13
|
+
"refresh-token",
|
|
14
|
+
"credentials",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_SENSITIVE_KEY_TERMS = [
|
|
18
|
+
"token",
|
|
19
|
+
"password",
|
|
20
|
+
"secret",
|
|
21
|
+
"credential",
|
|
22
|
+
"private-key",
|
|
23
|
+
"privatekey",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export interface RedactionDecisionContext {
|
|
27
|
+
key: string;
|
|
28
|
+
path: readonly string[];
|
|
29
|
+
value: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RedactionOptions {
|
|
33
|
+
replacement?: string;
|
|
34
|
+
truncatedValue?: string;
|
|
35
|
+
circularValue?: string;
|
|
36
|
+
maxDepth?: number;
|
|
37
|
+
sensitiveKeys?: readonly string[];
|
|
38
|
+
sensitiveKeyTerms?: readonly string[];
|
|
39
|
+
shouldRedactKey?: (context: RedactionDecisionContext) => boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type Redactor<T = unknown> = (value: T) => T;
|
|
43
|
+
|
|
44
|
+
export type RedactableHeaders =
|
|
45
|
+
| Headers
|
|
46
|
+
| Iterable<readonly [string, unknown]>
|
|
47
|
+
| Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
function normalizeKey(key: string): string {
|
|
50
|
+
return key.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isSensitiveKey(
|
|
54
|
+
key: string,
|
|
55
|
+
options: RedactionOptions = {},
|
|
56
|
+
context?: Omit<RedactionDecisionContext, "key">,
|
|
57
|
+
): boolean {
|
|
58
|
+
const normalized = normalizeKey(key);
|
|
59
|
+
const exactKeys = new Set(
|
|
60
|
+
[...DEFAULT_SENSITIVE_KEYS, ...(options.sensitiveKeys ?? [])].map(
|
|
61
|
+
normalizeKey,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
if (exactKeys.has(normalized)) return true;
|
|
65
|
+
|
|
66
|
+
const terms = [
|
|
67
|
+
...DEFAULT_SENSITIVE_KEY_TERMS,
|
|
68
|
+
...(options.sensitiveKeyTerms ?? []),
|
|
69
|
+
].map(normalizeKey);
|
|
70
|
+
if (terms.some((term) => normalized.includes(term))) return true;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
options.shouldRedactKey?.({
|
|
74
|
+
key,
|
|
75
|
+
path: context?.path ?? [],
|
|
76
|
+
value: context?.value,
|
|
77
|
+
}) ?? false
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function redactUnknown(
|
|
82
|
+
value: unknown,
|
|
83
|
+
options: RedactionOptions,
|
|
84
|
+
path: readonly string[],
|
|
85
|
+
seen: WeakSet<object>,
|
|
86
|
+
): unknown {
|
|
87
|
+
const maxDepth = options.maxDepth ?? 6;
|
|
88
|
+
if (path.length > maxDepth) {
|
|
89
|
+
return options.truncatedValue ?? DEFAULT_TRUNCATED_VALUE;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (value === null || value === undefined) return value;
|
|
93
|
+
|
|
94
|
+
const valueType = typeof value;
|
|
95
|
+
if (
|
|
96
|
+
valueType === "string" ||
|
|
97
|
+
valueType === "number" ||
|
|
98
|
+
valueType === "boolean"
|
|
99
|
+
) {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (valueType === "bigint") {
|
|
104
|
+
return value.toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (value instanceof Date) {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (value instanceof Error) {
|
|
112
|
+
return {
|
|
113
|
+
name: value.name,
|
|
114
|
+
message: value.message,
|
|
115
|
+
stack: value.stack,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (valueType !== "object") {
|
|
120
|
+
return String(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const objectValue = value as object;
|
|
124
|
+
if (seen.has(objectValue)) {
|
|
125
|
+
return options.circularValue ?? DEFAULT_CIRCULAR_VALUE;
|
|
126
|
+
}
|
|
127
|
+
seen.add(objectValue);
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
const output = value.map((item, index) =>
|
|
131
|
+
redactUnknown(item, options, [...path, String(index)], seen),
|
|
132
|
+
);
|
|
133
|
+
seen.delete(objectValue);
|
|
134
|
+
return output;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const output: Record<string, unknown> = {};
|
|
138
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
139
|
+
output[key] = isSensitiveKey(key, options, {
|
|
140
|
+
path: [...path, key],
|
|
141
|
+
value: nestedValue,
|
|
142
|
+
})
|
|
143
|
+
? (options.replacement ?? DEFAULT_REDACTED_VALUE)
|
|
144
|
+
: redactUnknown(nestedValue, options, [...path, key], seen);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
seen.delete(objectValue);
|
|
148
|
+
return output;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function redactValue<T = unknown>(
|
|
152
|
+
value: T,
|
|
153
|
+
options: RedactionOptions = {},
|
|
154
|
+
): T {
|
|
155
|
+
return redactUnknown(value, options, [], new WeakSet()) as T;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function headerEntries(
|
|
159
|
+
headers: RedactableHeaders,
|
|
160
|
+
): Iterable<readonly [string, unknown]> {
|
|
161
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
162
|
+
const entries: Array<readonly [string, unknown]> = [];
|
|
163
|
+
headers.forEach((value, key) => {
|
|
164
|
+
entries.push([key, value]);
|
|
165
|
+
});
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
typeof (headers as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===
|
|
171
|
+
"function"
|
|
172
|
+
) {
|
|
173
|
+
return headers as Iterable<readonly [string, unknown]>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Object.entries(headers);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function redactHeaders(
|
|
180
|
+
headers: RedactableHeaders,
|
|
181
|
+
options: RedactionOptions = {},
|
|
182
|
+
): Record<string, unknown> {
|
|
183
|
+
const output: Record<string, unknown> = {};
|
|
184
|
+
for (const [key, value] of headerEntries(headers)) {
|
|
185
|
+
output[key] = isSensitiveKey(key, options, {
|
|
186
|
+
path: [key],
|
|
187
|
+
value,
|
|
188
|
+
})
|
|
189
|
+
? (options.replacement ?? DEFAULT_REDACTED_VALUE)
|
|
190
|
+
: redactValue(value, options);
|
|
191
|
+
}
|
|
192
|
+
return output;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createRedactor<T = unknown>(
|
|
196
|
+
options: RedactionOptions = {},
|
|
197
|
+
): Redactor<T> {
|
|
198
|
+
return (value) => redactValue(value, options);
|
|
199
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
export type StorageVisibility = "private" | "public";
|
|
2
|
+
|
|
3
|
+
export type StorageMetadata = Record<string, string>;
|
|
4
|
+
|
|
5
|
+
export type StorageBody =
|
|
6
|
+
| string
|
|
7
|
+
| ArrayBuffer
|
|
8
|
+
| Uint8Array
|
|
9
|
+
| Blob
|
|
10
|
+
| ReadableStream<Uint8Array>;
|
|
11
|
+
|
|
12
|
+
export interface StoragePutOptions {
|
|
13
|
+
contentType?: string;
|
|
14
|
+
cacheControl?: string;
|
|
15
|
+
metadata?: StorageMetadata;
|
|
16
|
+
visibility?: StorageVisibility;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StorageObject {
|
|
20
|
+
key: string;
|
|
21
|
+
size: number;
|
|
22
|
+
contentType?: string;
|
|
23
|
+
cacheControl?: string;
|
|
24
|
+
metadata: StorageMetadata;
|
|
25
|
+
visibility: StorageVisibility;
|
|
26
|
+
lastModified: Date;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StorageObjectBody extends StorageObject {
|
|
30
|
+
/**
|
|
31
|
+
* Whether this object body has already been consumed. Like Fetch response
|
|
32
|
+
* bodies, storage bodies are one-shot so providers can stream objects without
|
|
33
|
+
* buffering them.
|
|
34
|
+
*/
|
|
35
|
+
readonly bodyUsed: boolean;
|
|
36
|
+
stream(): ReadableStream<Uint8Array>;
|
|
37
|
+
bytes(): Promise<Uint8Array>;
|
|
38
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
39
|
+
text(): Promise<string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StoragePort {
|
|
43
|
+
put(
|
|
44
|
+
key: string,
|
|
45
|
+
body: StorageBody,
|
|
46
|
+
options?: StoragePutOptions,
|
|
47
|
+
): Promise<StorageObject>;
|
|
48
|
+
get(key: string): Promise<StorageObjectBody | null>;
|
|
49
|
+
stat(key: string): Promise<StorageObject | null>;
|
|
50
|
+
delete(key: string): Promise<boolean>;
|
|
51
|
+
exists(key: string): Promise<boolean>;
|
|
52
|
+
publicUrl(key: string): Promise<string | null>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MemoryStorageOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Base URL used by `publicUrl(...)` for objects written with
|
|
58
|
+
* `visibility: "public"`.
|
|
59
|
+
*/
|
|
60
|
+
publicBaseUrl?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type MemoryStorageEntry = StorageObject & {
|
|
64
|
+
bytes: Uint8Array;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function copyBytes(bytes: Uint8Array): Uint8Array {
|
|
68
|
+
return new Uint8Array(bytes);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function bytesToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
72
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
73
|
+
new Uint8Array(buffer).set(bytes);
|
|
74
|
+
return buffer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
78
|
+
const copy = copyBytes(bytes);
|
|
79
|
+
return new ReadableStream<Uint8Array>({
|
|
80
|
+
start(controller) {
|
|
81
|
+
controller.enqueue(copy);
|
|
82
|
+
controller.close();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function streamToBytes(
|
|
88
|
+
stream: ReadableStream<Uint8Array>,
|
|
89
|
+
): Promise<Uint8Array> {
|
|
90
|
+
const reader = stream.getReader();
|
|
91
|
+
const chunks: Uint8Array[] = [];
|
|
92
|
+
let size = 0;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
while (true) {
|
|
96
|
+
const result = await reader.read();
|
|
97
|
+
if (result.done) break;
|
|
98
|
+
chunks.push(result.value);
|
|
99
|
+
size += result.value.byteLength;
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
reader.releaseLock();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const bytes = new Uint8Array(size);
|
|
106
|
+
let offset = 0;
|
|
107
|
+
for (const chunk of chunks) {
|
|
108
|
+
bytes.set(chunk, offset);
|
|
109
|
+
offset += chunk.byteLength;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return bytes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function storageBodyToBytes(body: StorageBody): Promise<Uint8Array> {
|
|
116
|
+
if (typeof body === "string") {
|
|
117
|
+
return new TextEncoder().encode(body);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (body instanceof Uint8Array) {
|
|
121
|
+
return copyBytes(body);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (body instanceof ArrayBuffer) {
|
|
125
|
+
return new Uint8Array(body.slice(0));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (body instanceof Blob) {
|
|
129
|
+
return new Uint8Array(await body.arrayBuffer());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return streamToBytes(body);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function cloneObject(entry: MemoryStorageEntry): StorageObject {
|
|
136
|
+
return {
|
|
137
|
+
key: entry.key,
|
|
138
|
+
size: entry.size,
|
|
139
|
+
...(entry.contentType !== undefined
|
|
140
|
+
? { contentType: entry.contentType }
|
|
141
|
+
: {}),
|
|
142
|
+
...(entry.cacheControl !== undefined
|
|
143
|
+
? { cacheControl: entry.cacheControl }
|
|
144
|
+
: {}),
|
|
145
|
+
metadata: { ...entry.metadata },
|
|
146
|
+
visibility: entry.visibility,
|
|
147
|
+
lastModified: new Date(entry.lastModified),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createObjectBody(entry: MemoryStorageEntry): StorageObjectBody {
|
|
152
|
+
const object = cloneObject(entry);
|
|
153
|
+
let bodyUsed = false;
|
|
154
|
+
|
|
155
|
+
function consumeBytes(): Uint8Array {
|
|
156
|
+
if (bodyUsed) {
|
|
157
|
+
throw new Error("Storage object body has already been consumed.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
bodyUsed = true;
|
|
161
|
+
return copyBytes(entry.bytes);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...object,
|
|
166
|
+
get bodyUsed() {
|
|
167
|
+
return bodyUsed;
|
|
168
|
+
},
|
|
169
|
+
stream() {
|
|
170
|
+
return bytesToStream(consumeBytes());
|
|
171
|
+
},
|
|
172
|
+
async bytes() {
|
|
173
|
+
return consumeBytes();
|
|
174
|
+
},
|
|
175
|
+
async arrayBuffer() {
|
|
176
|
+
return bytesToArrayBuffer(consumeBytes());
|
|
177
|
+
},
|
|
178
|
+
async text() {
|
|
179
|
+
return new TextDecoder().decode(consumeBytes());
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateStorageKey(key: string): void {
|
|
185
|
+
if (key.length === 0) {
|
|
186
|
+
throw new Error("Storage key must not be empty.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (key.startsWith("/")) {
|
|
190
|
+
throw new Error("Storage key must not start with '/'.");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (key.endsWith("/")) {
|
|
194
|
+
throw new Error("Storage key must not end with '/'.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (key.includes("\\")) {
|
|
198
|
+
throw new Error("Storage key must use '/' separators, not '\\'.");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const segments = key.split("/");
|
|
202
|
+
|
|
203
|
+
if (segments.some((segment) => segment === "")) {
|
|
204
|
+
throw new Error("Storage key must not include empty path segments.");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
208
|
+
throw new Error("Storage key must not include '.' or '..' segments.");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function joinPublicUrl(baseUrl: string, key: string): string {
|
|
213
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
214
|
+
const encodedKey = key
|
|
215
|
+
.split("/")
|
|
216
|
+
.map((part) => encodeURIComponent(part))
|
|
217
|
+
.join("/");
|
|
218
|
+
|
|
219
|
+
return `${base}/${encodedKey}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createMemoryStorage(
|
|
223
|
+
options: MemoryStorageOptions = {},
|
|
224
|
+
): StoragePort {
|
|
225
|
+
const objects = new Map<string, MemoryStorageEntry>();
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
async put(key, body, putOptions) {
|
|
229
|
+
validateStorageKey(key);
|
|
230
|
+
const bytes = await storageBodyToBytes(body);
|
|
231
|
+
const entry: MemoryStorageEntry = {
|
|
232
|
+
key,
|
|
233
|
+
size: bytes.byteLength,
|
|
234
|
+
...(putOptions?.contentType !== undefined
|
|
235
|
+
? { contentType: putOptions.contentType }
|
|
236
|
+
: {}),
|
|
237
|
+
...(putOptions?.cacheControl !== undefined
|
|
238
|
+
? { cacheControl: putOptions.cacheControl }
|
|
239
|
+
: {}),
|
|
240
|
+
metadata: { ...(putOptions?.metadata ?? {}) },
|
|
241
|
+
visibility: putOptions?.visibility ?? "private",
|
|
242
|
+
lastModified: new Date(),
|
|
243
|
+
bytes,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
objects.set(key, entry);
|
|
247
|
+
|
|
248
|
+
return cloneObject(entry);
|
|
249
|
+
},
|
|
250
|
+
async get(key) {
|
|
251
|
+
validateStorageKey(key);
|
|
252
|
+
const entry = objects.get(key);
|
|
253
|
+
if (!entry) return null;
|
|
254
|
+
|
|
255
|
+
return createObjectBody(entry);
|
|
256
|
+
},
|
|
257
|
+
async stat(key) {
|
|
258
|
+
validateStorageKey(key);
|
|
259
|
+
const entry = objects.get(key);
|
|
260
|
+
if (!entry) return null;
|
|
261
|
+
|
|
262
|
+
return cloneObject(entry);
|
|
263
|
+
},
|
|
264
|
+
async delete(key) {
|
|
265
|
+
validateStorageKey(key);
|
|
266
|
+
return objects.delete(key);
|
|
267
|
+
},
|
|
268
|
+
async exists(key) {
|
|
269
|
+
validateStorageKey(key);
|
|
270
|
+
return objects.has(key);
|
|
271
|
+
},
|
|
272
|
+
async publicUrl(key) {
|
|
273
|
+
validateStorageKey(key);
|
|
274
|
+
const entry = objects.get(key);
|
|
275
|
+
if (!entry || entry.visibility !== "public" || !options.publicBaseUrl) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return joinPublicUrl(options.publicBaseUrl, key);
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { EventBusPort } from "./events";
|
|
2
|
+
import {
|
|
3
|
+
type CreateGateOptions,
|
|
4
|
+
createGate,
|
|
5
|
+
type GateDecision,
|
|
6
|
+
type GatePort,
|
|
7
|
+
type PolicyContextFromDefinitions,
|
|
8
|
+
type PolicyDefinition,
|
|
9
|
+
type PolicyMapFromDefinitions,
|
|
10
|
+
type PolicySubjectArgs,
|
|
11
|
+
} from "./policy";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A recorded event entry from the recording event bus.
|
|
15
|
+
*/
|
|
16
|
+
export interface RecordedEvent {
|
|
17
|
+
name: string;
|
|
18
|
+
payload: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a recording event bus for testing.
|
|
23
|
+
*
|
|
24
|
+
* This bus records all published events for later assertion,
|
|
25
|
+
* but does not support subscription (throws if called).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const { bus, events } = createRecordingEventBus();
|
|
30
|
+
*
|
|
31
|
+
* // Inject bus into your use case
|
|
32
|
+
* await createUser({ ports: { eventBus: bus } });
|
|
33
|
+
*
|
|
34
|
+
* // Assert on recorded events
|
|
35
|
+
* expect(events).toHaveLength(1);
|
|
36
|
+
* expect(events[0].name).toBe("user.registered");
|
|
37
|
+
* expect(events[0].payload).toEqual({ userId: "123", email: "test@example.com" });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function createRecordingEventBus(): {
|
|
41
|
+
bus: EventBusPort;
|
|
42
|
+
events: RecordedEvent[];
|
|
43
|
+
} {
|
|
44
|
+
const events: RecordedEvent[] = [];
|
|
45
|
+
|
|
46
|
+
const bus: EventBusPort = {
|
|
47
|
+
publish(event, payload) {
|
|
48
|
+
events.push({ name: event.name, payload });
|
|
49
|
+
},
|
|
50
|
+
subscribe() {
|
|
51
|
+
throw new Error("Not implemented for recording bus");
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return { bus, events };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type PolicyMatrixExpectation = "allow" | "deny";
|
|
59
|
+
|
|
60
|
+
type PolicyMatrixSubject<TResolver> =
|
|
61
|
+
PolicySubjectArgs<TResolver> extends [subject: infer Subject]
|
|
62
|
+
? { subject: Subject }
|
|
63
|
+
: { subject?: never };
|
|
64
|
+
|
|
65
|
+
export type PolicyMatrixCase<
|
|
66
|
+
TContext,
|
|
67
|
+
TPolicies extends readonly PolicyDefinition[] = readonly PolicyDefinition[],
|
|
68
|
+
> = {
|
|
69
|
+
[TAbility in keyof PolicyMapFromDefinitions<TPolicies> & string]: {
|
|
70
|
+
name: string;
|
|
71
|
+
ctx: TContext;
|
|
72
|
+
ability: TAbility;
|
|
73
|
+
expected: PolicyMatrixExpectation;
|
|
74
|
+
reason?: string;
|
|
75
|
+
code?: string;
|
|
76
|
+
} & PolicyMatrixSubject<PolicyMapFromDefinitions<TPolicies>[TAbility]>;
|
|
77
|
+
}[keyof PolicyMapFromDefinitions<TPolicies> & string];
|
|
78
|
+
|
|
79
|
+
export type UntypedPolicyMatrixCase<TContext> = {
|
|
80
|
+
name: string;
|
|
81
|
+
ctx: TContext;
|
|
82
|
+
ability: string;
|
|
83
|
+
subject?: unknown;
|
|
84
|
+
expected: PolicyMatrixExpectation;
|
|
85
|
+
reason?: string;
|
|
86
|
+
code?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type PolicyMatrixResult<
|
|
90
|
+
TContext,
|
|
91
|
+
TPolicies extends readonly PolicyDefinition[] = readonly PolicyDefinition[],
|
|
92
|
+
> = {
|
|
93
|
+
case: PolicyMatrixCase<TContext, TPolicies>;
|
|
94
|
+
decision: GateDecision;
|
|
95
|
+
passed: boolean;
|
|
96
|
+
message?: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type PolicyTester<
|
|
100
|
+
TContext,
|
|
101
|
+
TPolicies extends readonly PolicyDefinition[],
|
|
102
|
+
> = {
|
|
103
|
+
gate: GatePort<TContext, TPolicies>;
|
|
104
|
+
evaluateMatrix(
|
|
105
|
+
cases: readonly PolicyMatrixCase<TContext, TPolicies>[],
|
|
106
|
+
): Promise<PolicyMatrixResult<TContext, TPolicies>[]>;
|
|
107
|
+
assertMatrix(
|
|
108
|
+
cases: readonly PolicyMatrixCase<TContext, TPolicies>[],
|
|
109
|
+
): Promise<void>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export function createPolicyTester<
|
|
113
|
+
const TPolicies extends readonly PolicyDefinition[],
|
|
114
|
+
>(
|
|
115
|
+
options: CreateGateOptions<
|
|
116
|
+
PolicyContextFromDefinitions<TPolicies>,
|
|
117
|
+
TPolicies
|
|
118
|
+
>,
|
|
119
|
+
): PolicyTester<PolicyContextFromDefinitions<TPolicies>, TPolicies> {
|
|
120
|
+
const gate = createGate(options);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
gate,
|
|
124
|
+
evaluateMatrix: (cases) => evaluatePolicyMatrix(gate, cases),
|
|
125
|
+
assertMatrix: (cases) => assertPolicyMatrix(gate, cases),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function evaluatePolicyMatrix<
|
|
130
|
+
TContext,
|
|
131
|
+
TPolicies extends readonly PolicyDefinition[],
|
|
132
|
+
>(
|
|
133
|
+
gate: GatePort<TContext, TPolicies>,
|
|
134
|
+
cases: readonly PolicyMatrixCase<TContext, TPolicies>[],
|
|
135
|
+
): Promise<PolicyMatrixResult<TContext, TPolicies>[]> {
|
|
136
|
+
const results: PolicyMatrixResult<TContext, TPolicies>[] = [];
|
|
137
|
+
|
|
138
|
+
for (const matrixCase of cases) {
|
|
139
|
+
const decision = await inspectPolicyMatrixCase(gate, matrixCase);
|
|
140
|
+
const passed = policyMatrixCasePassed(matrixCase, decision);
|
|
141
|
+
|
|
142
|
+
results.push({
|
|
143
|
+
case: matrixCase,
|
|
144
|
+
decision,
|
|
145
|
+
passed,
|
|
146
|
+
message: passed
|
|
147
|
+
? undefined
|
|
148
|
+
: policyMatrixFailureMessage(matrixCase, decision),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function assertPolicyMatrix<
|
|
156
|
+
TContext,
|
|
157
|
+
TPolicies extends readonly PolicyDefinition[],
|
|
158
|
+
>(
|
|
159
|
+
gate: GatePort<TContext, TPolicies>,
|
|
160
|
+
cases: readonly PolicyMatrixCase<TContext, TPolicies>[],
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
const results = await evaluatePolicyMatrix(gate, cases);
|
|
163
|
+
const failures = results.filter((result) => !result.passed);
|
|
164
|
+
|
|
165
|
+
if (failures.length === 0) return;
|
|
166
|
+
|
|
167
|
+
throw new Error(
|
|
168
|
+
[
|
|
169
|
+
`Policy matrix failed for ${failures.length} case${
|
|
170
|
+
failures.length === 1 ? "" : "s"
|
|
171
|
+
}.`,
|
|
172
|
+
...failures.map((failure) => `- ${failure.message}`),
|
|
173
|
+
].join("\n"),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function inspectPolicyMatrixCase<
|
|
178
|
+
TContext,
|
|
179
|
+
TPolicies extends readonly PolicyDefinition[],
|
|
180
|
+
>(
|
|
181
|
+
gate: GatePort<TContext, TPolicies>,
|
|
182
|
+
matrixCase: PolicyMatrixCase<TContext, TPolicies>,
|
|
183
|
+
): Promise<GateDecision> {
|
|
184
|
+
const inspect = gate.inspect as DynamicGateInspect<TContext>;
|
|
185
|
+
|
|
186
|
+
if ("subject" in matrixCase) {
|
|
187
|
+
return inspect(matrixCase.ctx, matrixCase.ability, matrixCase.subject);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return inspect(matrixCase.ctx, matrixCase.ability);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function policyMatrixCasePassed<TContext>(
|
|
194
|
+
matrixCase: UntypedPolicyMatrixCase<TContext>,
|
|
195
|
+
decision: GateDecision,
|
|
196
|
+
): boolean {
|
|
197
|
+
if (matrixCase.expected === "allow" && !decision.allowed) return false;
|
|
198
|
+
if (matrixCase.expected === "deny" && decision.allowed) return false;
|
|
199
|
+
if (matrixCase.reason && decision.allowed) return false;
|
|
200
|
+
if (matrixCase.code && decision.allowed) return false;
|
|
201
|
+
if (decision.allowed) return true;
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
(!matrixCase.reason || decision.reason === matrixCase.reason) &&
|
|
205
|
+
(!matrixCase.code || decision.code === matrixCase.code)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function policyMatrixFailureMessage<TContext>(
|
|
210
|
+
matrixCase: UntypedPolicyMatrixCase<TContext>,
|
|
211
|
+
decision: GateDecision,
|
|
212
|
+
): string {
|
|
213
|
+
const expected: string[] = [matrixCase.expected];
|
|
214
|
+
if (matrixCase.reason) expected.push(`reason "${matrixCase.reason}"`);
|
|
215
|
+
if (matrixCase.code) expected.push(`code "${matrixCase.code}"`);
|
|
216
|
+
|
|
217
|
+
const actual = decision.allowed
|
|
218
|
+
? "allow"
|
|
219
|
+
: [
|
|
220
|
+
"deny",
|
|
221
|
+
decision.reason ? `reason "${decision.reason}"` : undefined,
|
|
222
|
+
decision.code ? `code "${decision.code}"` : undefined,
|
|
223
|
+
]
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.join(", ");
|
|
226
|
+
|
|
227
|
+
return `${matrixCase.name}: expected ${expected.join(", ")}, received ${actual}.`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type DynamicGateInspect<TContext> = (
|
|
231
|
+
ctx: TContext,
|
|
232
|
+
ability: string,
|
|
233
|
+
subject?: unknown,
|
|
234
|
+
) => Promise<GateDecision>;
|