@shivaedev/server 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +190 -0
- package/README.md +1 -1
- package/dist/RedactionPolicy.d.ts +5 -1
- package/dist/RedactionPolicy.d.ts.map +1 -1
- package/dist/RedactionPolicy.js +3 -0
- package/dist/RedactionPolicy.js.map +1 -1
- package/dist/hostChanges.d.ts +1 -1
- package/dist/hostChanges.d.ts.map +1 -1
- package/dist/hostChanges.js +6 -12
- package/dist/hostChanges.js.map +1 -1
- package/dist/hostRpcRoutes.d.ts.map +1 -1
- package/dist/hostRpcRoutes.js +4 -4
- package/dist/hostRpcRoutes.js.map +1 -1
- package/dist/redactingDiagnostics.d.ts +2 -1
- package/dist/redactingDiagnostics.d.ts.map +1 -1
- package/dist/redactingDiagnostics.js +15 -3
- package/dist/redactingDiagnostics.js.map +1 -1
- package/dist/redactingRpcSerialization.d.ts +1 -1
- package/dist/redactingRpcSerialization.d.ts.map +1 -1
- package/dist/redactingRpcSerialization.js +3 -3
- package/dist/redactingRpcSerialization.js.map +1 -1
- package/dist/redactingTracer.d.ts +2 -2
- package/dist/redactingTracer.d.ts.map +1 -1
- package/dist/redactingTracer.js +17 -3
- package/dist/redactingTracer.js.map +1 -1
- package/dist/rpcRequestTracing.d.ts +5 -0
- package/dist/rpcRequestTracing.d.ts.map +1 -0
- package/dist/rpcRequestTracing.js +8 -0
- package/dist/rpcRequestTracing.js.map +1 -0
- package/dist/safeOtlpSerialization.d.ts.map +1 -1
- package/dist/safeOtlpSerialization.js +20 -15
- package/dist/safeOtlpSerialization.js.map +1 -1
- package/dist/schemaSensitiveAst.d.ts +7 -0
- package/dist/schemaSensitiveAst.d.ts.map +1 -0
- package/dist/schemaSensitiveAst.js +102 -0
- package/dist/schemaSensitiveAst.js.map +1 -0
- package/dist/schemaSensitiveKeys.d.ts +1 -0
- package/dist/schemaSensitiveKeys.d.ts.map +1 -1
- package/dist/schemaSensitiveKeys.js +27 -123
- package/dist/schemaSensitiveKeys.js.map +1 -1
- package/dist/setupDatabases.d.ts +1 -1
- package/dist/setupDatabases.d.ts.map +1 -1
- package/dist/setupDatabases.js +1 -1
- package/dist/setupDatabases.js.map +1 -1
- package/package.json +13 -17
- package/src/RedactionPolicy.ts +7 -1
- package/src/hostChanges.ts +12 -16
- package/src/hostRpcRoutes.ts +4 -4
- package/src/redactingDiagnostics.ts +15 -3
- package/src/redactingRpcSerialization.ts +3 -2
- package/src/redactingTracer.ts +18 -4
- package/src/rpcRequestTracing.ts +12 -0
- package/src/safeOtlpSerialization.ts +20 -15
- package/src/schemaSensitiveAst.ts +106 -0
- package/src/schemaSensitiveKeys.ts +28 -127
- package/src/setupDatabases.ts +1 -1
package/src/hostRpcRoutes.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { type Rpc, type RpcGroup, RpcSerialization } from "effect/rpc";
|
|
|
5
5
|
import type { Authenticated, MaybeAuthenticated, RequestTracing } from "@shivaedev/identity/middleware.ts";
|
|
6
6
|
import { localRpcGuard } from "@shivaedev/identity/server/local.ts";
|
|
7
7
|
import { authenticatedLayer, maybeAuthenticatedLayer, type SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
8
|
-
import { requestTracingLayer } from "@shivaedev/identity/server/tracing.ts";
|
|
9
8
|
import type { AppChanges } from "#appHost.ts";
|
|
10
9
|
import { boundedRpcRequest } from "#boundedRpcRequest.ts";
|
|
11
10
|
import { hostChanges } from "#hostChanges.ts";
|
|
@@ -13,6 +12,7 @@ import type { HostPolicy } from "#hostOptions.ts";
|
|
|
13
12
|
import { hostRpc, rpcCacheControl } from "#hostRpc.ts";
|
|
14
13
|
import type { hostTransport } from "#hostTransport.ts";
|
|
15
14
|
import { redactingRpcSerialization } from "#redactingRpcSerialization.ts";
|
|
15
|
+
import { rpcRequestTracing } from "#rpcRequestTracing.ts";
|
|
16
16
|
import type { schemaSensitiveKeys } from "#schemaSensitiveKeys.ts";
|
|
17
17
|
import { socketRpc } from "#socketRpc.ts";
|
|
18
18
|
|
|
@@ -28,11 +28,11 @@ export function hostRpcRoutes<TRpcs extends Rpc.Any, THandlers, TError, TRequire
|
|
|
28
28
|
) {
|
|
29
29
|
const middleware: Layer.Layer<RequestTracing> | Layer.Layer<Authenticated | MaybeAuthenticated | RequestTracing> =
|
|
30
30
|
options.policy === "loopback"
|
|
31
|
-
?
|
|
32
|
-
: Layer.mergeAll(authenticatedLayer(options.sessions), maybeAuthenticatedLayer(options.sessions),
|
|
31
|
+
? rpcRequestTracing(sensitive)
|
|
32
|
+
: Layer.mergeAll(authenticatedLayer(options.sessions), maybeAuthenticatedLayer(options.sessions), rpcRequestTracing(sensitive));
|
|
33
33
|
return HttpRouter.use((router) =>
|
|
34
34
|
Effect.gen(function* () {
|
|
35
|
-
const wire = redactingRpcSerialization(yield* RpcSerialization.RpcSerialization, sensitive, sensitive.maskFailure);
|
|
35
|
+
const wire = redactingRpcSerialization(yield* RpcSerialization.RpcSerialization, sensitive, sensitive.maskFailure, sensitive.forRpc);
|
|
36
36
|
const httpGroup = options.group.omit(...[...options.group.requests.keys()].filter((tag) => tag.endsWith(".$changes")));
|
|
37
37
|
const handler = yield* hostRpc(httpGroup).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire));
|
|
38
38
|
const guarded = localRpcGuard(handler, trustedProxy, nativeOrigins);
|
|
@@ -2,11 +2,21 @@ import { Context, Effect, ErrorReporter, Layer, Logger, References } from "effec
|
|
|
2
2
|
import { redact } from "@shivaedev/errors/redact.ts";
|
|
3
3
|
import { redactDiagnosticCause, redactingErrorReporter } from "@shivaedev/errors/redactCause.ts";
|
|
4
4
|
import type { SensitiveKey } from "@shivaedev/errors/sensitive.ts";
|
|
5
|
+
import { type HostRedactionPolicy, redactionForRpc } from "#RedactionPolicy.ts";
|
|
5
6
|
|
|
6
7
|
export function redactRecord(values: Readonly<Record<string, unknown>>, sensitive: SensitiveKey): Record<string, unknown> {
|
|
7
8
|
return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, sensitive(key) ? "<redacted>" : redact(value, sensitive)]));
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
function requestErrorReporter(reporter: ErrorReporter.ErrorReporter, policy: HostRedactionPolicy): ErrorReporter.ErrorReporter {
|
|
11
|
+
return {
|
|
12
|
+
[ErrorReporter.TypeId]: ErrorReporter.TypeId,
|
|
13
|
+
report: (options) => {
|
|
14
|
+
const sensitive = redactionForRpc(policy, options.fiber.getRef(References.CurrentLogAnnotations)["rpc.method"]);
|
|
15
|
+
return redactingErrorReporter(reporter, sensitive).report(options);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function redactingDiagnostics(policy: HostRedactionPolicy) {
|
|
10
20
|
return Layer.effectContext(
|
|
11
21
|
Effect.gen(function* () {
|
|
12
22
|
const loggers = yield* Logger.CurrentLoggers;
|
|
@@ -15,7 +25,9 @@ export function redactingDiagnostics(sensitive: SensitiveKey) {
|
|
|
15
25
|
[...loggers].map((logger) =>
|
|
16
26
|
Logger.make((options) => {
|
|
17
27
|
const context = options.fiber.context;
|
|
18
|
-
const
|
|
28
|
+
const raw = options.fiber.getRef(References.CurrentLogAnnotations);
|
|
29
|
+
const sensitive = redactionForRpc(policy, raw["rpc.method"]);
|
|
30
|
+
const annotations = redactRecord(raw, sensitive);
|
|
19
31
|
options.fiber.setContext(Context.add(context, References.CurrentLogAnnotations, annotations));
|
|
20
32
|
try {
|
|
21
33
|
return logger.log({ ...options, cause: redactDiagnosticCause(options.cause, sensitive), message: redact(options.message, sensitive) });
|
|
@@ -26,7 +38,7 @@ export function redactingDiagnostics(sensitive: SensitiveKey) {
|
|
|
26
38
|
),
|
|
27
39
|
);
|
|
28
40
|
return Context.make(Logger.CurrentLoggers, safeLoggers).pipe(
|
|
29
|
-
Context.add(ErrorReporter.CurrentErrorReporters, new Set([...reporters].map((reporter) =>
|
|
41
|
+
Context.add(ErrorReporter.CurrentErrorReporters, new Set([...reporters].map((reporter) => requestErrorReporter(reporter, policy)))),
|
|
30
42
|
);
|
|
31
43
|
}),
|
|
32
44
|
);
|
|
@@ -10,9 +10,10 @@ export function redactingRpcSerialization(
|
|
|
10
10
|
wire: RpcSerialization.RpcSerialization["Service"],
|
|
11
11
|
sensitive: SensitiveKey,
|
|
12
12
|
maskFailure: (tag: string | undefined, reason: "Fail" | "Die") => boolean = () => false,
|
|
13
|
+
sensitiveForRpc: (tag: string | undefined) => SensitiveKey = () => sensitive,
|
|
13
14
|
): typeof wire {
|
|
14
15
|
function reason(value: unknown, tag: string | undefined): unknown {
|
|
15
|
-
const redacted = redactWire(value,
|
|
16
|
+
const redacted = redactWire(value, sensitiveForRpc(tag));
|
|
16
17
|
if (Predicate.isObjectOrArray(redacted)) {
|
|
17
18
|
if (Predicate.isTagged(value, "Fail") && maskFailure(tag, "Fail")) {
|
|
18
19
|
return { ...redacted, _tag: "Fail", error: REDACTED };
|
|
@@ -31,7 +32,7 @@ export function redactingRpcSerialization(
|
|
|
31
32
|
}
|
|
32
33
|
function exit(value: unknown, tag: string | undefined): unknown {
|
|
33
34
|
if (Predicate.isTagged(value, "Failure") && Predicate.hasProperty(value, "cause")) {
|
|
34
|
-
const cause = Array.isArray(value.cause) ? value.cause.map((item) => reason(item, tag)) : redactWire(value.cause,
|
|
35
|
+
const cause = Array.isArray(value.cause) ? value.cause.map((item) => reason(item, tag)) : redactWire(value.cause, sensitiveForRpc(tag));
|
|
35
36
|
return { ...value, cause };
|
|
36
37
|
}
|
|
37
38
|
return value;
|
package/src/redactingTracer.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { Effect, Exit, Layer, Tracer } from "effect";
|
|
1
|
+
import { Effect, Exit, Layer, Option, Tracer } from "effect";
|
|
2
2
|
import { redact } from "@shivaedev/errors/redact.ts";
|
|
3
3
|
import { redactDiagnosticCause } from "@shivaedev/errors/redactCause.ts";
|
|
4
4
|
import { REDACTED, type SensitiveKey } from "@shivaedev/errors/sensitive.ts";
|
|
5
|
+
import { type HostRedactionPolicy, redactionForRpc } from "#RedactionPolicy.ts";
|
|
5
6
|
import { redactRecord } from "#redactingDiagnostics.ts";
|
|
6
7
|
|
|
7
8
|
function redactLinks(values: readonly Tracer.SpanLink[], sensitive: SensitiveKey) {
|
|
8
9
|
return values.map((link) => ({ ...link, attributes: redactRecord(link.attributes, sensitive) }));
|
|
9
10
|
}
|
|
10
|
-
function redactSpan(span: Tracer.Span,
|
|
11
|
+
function redactSpan(span: Tracer.Span, policy: HostRedactionPolicy) {
|
|
11
12
|
const end = span.end.bind(span);
|
|
12
13
|
const attribute = span.attribute.bind(span);
|
|
13
14
|
const event = span.event.bind(span);
|
|
14
15
|
const addLinks = span.addLinks.bind(span);
|
|
16
|
+
function sensitive(key: string) {
|
|
17
|
+
return redactionForRpc(policy, span.attributes.get("rpc.method"))(key);
|
|
18
|
+
}
|
|
15
19
|
return Object.assign(span, {
|
|
16
20
|
addLinks: (values: readonly Tracer.SpanLink[]) => addLinks(redactLinks(values, sensitive)),
|
|
17
21
|
attribute: (key: string, value: unknown) => attribute(key, sensitive(key) ? REDACTED : redact(value, sensitive)),
|
|
@@ -24,13 +28,23 @@ function redactSpan(span: Tracer.Span, sensitive: SensitiveKey) {
|
|
|
24
28
|
event: (name: string, time: bigint, attributes?: Record<string, unknown>) => event(name, time, attributes && redactRecord(attributes, sensitive)),
|
|
25
29
|
});
|
|
26
30
|
}
|
|
27
|
-
export function redactingTracer(sensitive:
|
|
31
|
+
export function redactingTracer(sensitive: HostRedactionPolicy) {
|
|
28
32
|
return Layer.effect(
|
|
29
33
|
Tracer.Tracer,
|
|
30
34
|
Effect.map(Tracer.Tracer, (tracer) =>
|
|
31
35
|
Tracer.make({
|
|
32
36
|
...tracer,
|
|
33
|
-
span: (options) =>
|
|
37
|
+
span: (options) => {
|
|
38
|
+
const method = Option.match(options.parent, {
|
|
39
|
+
onNone: () => undefined,
|
|
40
|
+
onSome: (parent) => (parent._tag === "Span" ? parent.attributes.get("rpc.method") : undefined),
|
|
41
|
+
});
|
|
42
|
+
const span = tracer.span({ ...options, links: redactLinks(options.links, redactionForRpc(sensitive, method)) });
|
|
43
|
+
if (typeof method === "string") {
|
|
44
|
+
span.attribute("rpc.method", method);
|
|
45
|
+
}
|
|
46
|
+
return redactSpan(span, sensitive);
|
|
47
|
+
},
|
|
34
48
|
}),
|
|
35
49
|
),
|
|
36
50
|
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { RequestTracing } from "@shivaedev/identity/middleware.ts";
|
|
3
|
+
import { requestTracingLayer } from "@shivaedev/identity/server/tracing.ts";
|
|
4
|
+
import { type HostRedactionPolicy, redactionForRpc } from "#RedactionPolicy.ts";
|
|
5
|
+
|
|
6
|
+
export function rpcRequestTracing(sensitive: HostRedactionPolicy) {
|
|
7
|
+
return Layer.succeed(RequestTracing, (effect, request) =>
|
|
8
|
+
RequestTracing.use((tracing) => tracing(effect, request)).pipe(
|
|
9
|
+
Effect.provide(requestTracingLayer({ sensitive: redactionForRpc(sensitive, request.rpc._tag) })),
|
|
10
|
+
),
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -2,7 +2,7 @@ import { Effect, Layer } from "effect";
|
|
|
2
2
|
import type { OtlpResource, OtlpTracer } from "effect/observability";
|
|
3
3
|
import { OtlpSerialization } from "effect/observability";
|
|
4
4
|
import { isSensitiveKey, REDACTED, redactText, type SensitiveKey } from "@shivaedev/errors/sensitive.ts";
|
|
5
|
-
import { RedactionPolicy } from "#RedactionPolicy.ts";
|
|
5
|
+
import { type HostRedactionPolicy, RedactionPolicy, redactionForRpc } from "#RedactionPolicy.ts";
|
|
6
6
|
import { redactOtlpUrl } from "#redactOtlpUrl.ts";
|
|
7
7
|
|
|
8
8
|
function otlpSensitive(sensitive: SensitiveKey): SensitiveKey {
|
|
@@ -37,25 +37,30 @@ function spanName(span: { readonly name: string; readonly kind: number; readonly
|
|
|
37
37
|
return span.name;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function redactTraces(data: OtlpTracer.TraceData,
|
|
41
|
-
|
|
42
|
-
return redactOtlpText(value, "", sensitive);
|
|
43
|
-
}
|
|
40
|
+
function redactTraces(data: OtlpTracer.TraceData, policy: HostRedactionPolicy): OtlpTracer.TraceData {
|
|
41
|
+
const resourceSensitive = otlpSensitive(policy);
|
|
44
42
|
return {
|
|
45
43
|
...data,
|
|
46
44
|
resourceSpans: data.resourceSpans.map((resource) => ({
|
|
47
45
|
...resource,
|
|
48
|
-
resource: { ...resource.resource, attributes: attributes(resource.resource.attributes,
|
|
46
|
+
resource: { ...resource.resource, attributes: attributes(resource.resource.attributes, resourceSensitive) },
|
|
49
47
|
scopeSpans: resource.scopeSpans.map((scope) => ({
|
|
50
48
|
...scope,
|
|
51
|
-
spans: scope.spans.map((span) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
spans: scope.spans.map((span) => {
|
|
50
|
+
const method = span.attributes.find(({ key }) => key === "rpc.method")?.value.stringValue;
|
|
51
|
+
const sensitive = otlpSensitive(redactionForRpc(policy, method));
|
|
52
|
+
function text(value: string) {
|
|
53
|
+
return redactOtlpText(value, "", sensitive);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
...span,
|
|
57
|
+
attributes: attributes(span.attributes, sensitive),
|
|
58
|
+
events: span.events.map((event) => ({ ...event, attributes: attributes(event.attributes, sensitive), name: text(event.name) })),
|
|
59
|
+
links: span.links.map((link) => ({ ...link, attributes: attributes(link.attributes, sensitive) })),
|
|
60
|
+
name: text(spanName(span)),
|
|
61
|
+
status: { ...span.status, ...(span.status.message === undefined ? {} : { message: text(span.status.message) }) },
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
59
64
|
})),
|
|
60
65
|
})),
|
|
61
66
|
};
|
|
@@ -64,7 +69,7 @@ export const safeOtlpSerialization = Layer.effect(
|
|
|
64
69
|
OtlpSerialization.OtlpSerialization,
|
|
65
70
|
Effect.gen(function* () {
|
|
66
71
|
const serialization = yield* OtlpSerialization.OtlpSerialization;
|
|
67
|
-
const sensitive =
|
|
72
|
+
const sensitive = yield* RedactionPolicy;
|
|
68
73
|
return OtlpSerialization.OtlpSerialization.of({
|
|
69
74
|
...serialization,
|
|
70
75
|
traces: (data) => serialization.traces(redactTraces(data, sensitive)),
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Predicate, type SchemaAST } from "effect";
|
|
2
|
+
import { isSensitiveKey } from "@shivaedev/errors/sensitive.ts";
|
|
3
|
+
|
|
4
|
+
function children(ast: SchemaAST.AST): readonly SchemaAST.AST[] {
|
|
5
|
+
switch (ast._tag) {
|
|
6
|
+
case "Objects":
|
|
7
|
+
return [...ast.propertySignatures.map((property) => property.type), ...ast.indexSignatures.flatMap((index) => [index.parameter, index.type])];
|
|
8
|
+
case "Arrays":
|
|
9
|
+
return [...ast.elements, ...ast.rest];
|
|
10
|
+
case "Union":
|
|
11
|
+
return ast.types;
|
|
12
|
+
case "Declaration":
|
|
13
|
+
return ast.typeParameters;
|
|
14
|
+
case "Suspend":
|
|
15
|
+
return [ast.thunk()];
|
|
16
|
+
default:
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function sensitiveCheck(check: SchemaAST.Check<never>): boolean {
|
|
21
|
+
return check.annotations?.sensitive === true || (check._tag === "FilterGroup" && check.checks.some(sensitiveCheck));
|
|
22
|
+
}
|
|
23
|
+
export function marked(ast: SchemaAST.AST, seen = new Set<SchemaAST.AST>()): boolean {
|
|
24
|
+
if (seen.has(ast)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
seen.add(ast);
|
|
28
|
+
if (
|
|
29
|
+
ast.annotations?.sensitive === true
|
|
30
|
+
|| ast.context?.annotations?.sensitive === true
|
|
31
|
+
|| ast.checks?.some(sensitiveCheck)
|
|
32
|
+
|| (Predicate.hasProperty(ast.annotations?.representation, "id") && ast.annotations.representation.id === "effect/schema/Redacted")
|
|
33
|
+
) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const nested = ast._tag === "Objects" ? ast.indexSignatures.map((index) => index.type) : children(ast);
|
|
37
|
+
return (ast.encoding?.some((link) => marked(link.to, seen)) ?? false) || nested.some((type) => marked(type, seen));
|
|
38
|
+
}
|
|
39
|
+
function containsSensitive(ast: SchemaAST.AST, keys: ReadonlySet<string>): boolean {
|
|
40
|
+
return (
|
|
41
|
+
marked(ast)
|
|
42
|
+
|| propertiesOf(ast).some((property) => typeof property.name === "string" && (keys.has(property.name) || isSensitiveKey(property.name)))
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
export function collectEncodedKeys(ast: SchemaAST.AST, keys: Set<string>) {
|
|
46
|
+
let source = ast;
|
|
47
|
+
let sensitive = false;
|
|
48
|
+
for (const link of ast.encoding ?? []) {
|
|
49
|
+
sensitive ||= containsSensitive(source, keys);
|
|
50
|
+
if (sensitive) {
|
|
51
|
+
for (const property of propertiesOf(link.to)) {
|
|
52
|
+
if (typeof property.name === "string") {
|
|
53
|
+
keys.add(property.name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
source = link.to;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function fixedOutput(ast: SchemaAST.AST): boolean {
|
|
61
|
+
if (ast._tag === "Union") {
|
|
62
|
+
return ast.types.every(fixedOutput);
|
|
63
|
+
}
|
|
64
|
+
return ast._tag === "Objects" && ast.indexSignatures.length === 0;
|
|
65
|
+
}
|
|
66
|
+
export function maskedValue(ast: SchemaAST.AST, keys: ReadonlySet<string>, seen = new Set<SchemaAST.AST>()): boolean {
|
|
67
|
+
if (seen.has(ast)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
seen.add(ast);
|
|
71
|
+
if (marked(ast)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
let source = ast;
|
|
75
|
+
let sensitive = false;
|
|
76
|
+
for (const link of ast.encoding ?? []) {
|
|
77
|
+
sensitive ||= containsSensitive(source, keys);
|
|
78
|
+
if (sensitive && !fixedOutput(link.to)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
source = link.to;
|
|
82
|
+
}
|
|
83
|
+
const nested = ast._tag === "Objects" ? ast.indexSignatures.map((index) => index.type) : children(ast);
|
|
84
|
+
return nested.some((type) => maskedValue(type, keys, seen));
|
|
85
|
+
}
|
|
86
|
+
export function collect(ast: SchemaAST.AST, nodes: Set<SchemaAST.AST>, encodings = true) {
|
|
87
|
+
if (nodes.has(ast)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
nodes.add(ast);
|
|
91
|
+
for (const child of [...children(ast), ...(encodings ? (ast.encoding?.map((link) => link.to) ?? []) : [])]) {
|
|
92
|
+
collect(child, nodes, encodings);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function propertiesOf(ast: SchemaAST.AST) {
|
|
96
|
+
const nodes = new Set<SchemaAST.AST>();
|
|
97
|
+
collect(ast, nodes, false);
|
|
98
|
+
return [...nodes].flatMap((node) => (node._tag === "Objects" ? node.propertySignatures : []));
|
|
99
|
+
}
|
|
100
|
+
export function collectFieldKeys(properties: SchemaAST.Objects["propertySignatures"], keys: Set<string>, sensitive: (ast: SchemaAST.AST) => boolean) {
|
|
101
|
+
for (const property of properties) {
|
|
102
|
+
if (typeof property.name === "string" && sensitive(property.type)) {
|
|
103
|
+
keys.add(property.name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,130 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Schema, SchemaAST } from "effect";
|
|
2
2
|
import { Rpc, type RpcGroup } from "effect/rpc";
|
|
3
3
|
import { isSensitiveKey } from "@shivaedev/errors/sensitive.ts";
|
|
4
|
+
import { collect, collectEncodedKeys, collectFieldKeys, marked, maskedValue } from "#schemaSensitiveAst.ts";
|
|
4
5
|
|
|
5
|
-
function
|
|
6
|
-
switch (ast._tag) {
|
|
7
|
-
case "Objects":
|
|
8
|
-
return [...ast.propertySignatures.map((property) => property.type), ...ast.indexSignatures.flatMap((index) => [index.parameter, index.type])];
|
|
9
|
-
case "Arrays":
|
|
10
|
-
return [...ast.elements, ...ast.rest];
|
|
11
|
-
case "Union":
|
|
12
|
-
return ast.types;
|
|
13
|
-
case "Declaration":
|
|
14
|
-
return ast.typeParameters;
|
|
15
|
-
case "Suspend":
|
|
16
|
-
return [ast.thunk()];
|
|
17
|
-
default:
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function sensitiveCheck(check: SchemaAST.Check<never>): boolean {
|
|
22
|
-
return check.annotations?.sensitive === true || (check._tag === "FilterGroup" && check.checks.some(sensitiveCheck));
|
|
23
|
-
}
|
|
24
|
-
function marked(ast: SchemaAST.AST, seen = new Set<SchemaAST.AST>()): boolean {
|
|
25
|
-
if (seen.has(ast)) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
seen.add(ast);
|
|
29
|
-
if (
|
|
30
|
-
ast.annotations?.sensitive === true
|
|
31
|
-
|| ast.context?.annotations?.sensitive === true
|
|
32
|
-
|| ast.checks?.some(sensitiveCheck)
|
|
33
|
-
|| (Predicate.hasProperty(ast.annotations?.representation, "id") && ast.annotations.representation.id === "effect/schema/Redacted")
|
|
34
|
-
) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
const nested = ast._tag === "Objects" ? ast.indexSignatures.map((index) => index.type) : children(ast);
|
|
38
|
-
return (ast.encoding?.some((link) => marked(link.to, seen)) ?? false) || nested.some((type) => marked(type, seen));
|
|
39
|
-
}
|
|
40
|
-
function containsSensitive(ast: SchemaAST.AST, keys: ReadonlySet<string>): boolean {
|
|
41
|
-
return (
|
|
42
|
-
marked(ast)
|
|
43
|
-
|| propertiesOf(ast).some((property) => typeof property.name === "string" && (keys.has(property.name) || isSensitiveKey(property.name)))
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
function collectEncodedKeys(ast: SchemaAST.AST, keys: Set<string>) {
|
|
47
|
-
let source = ast;
|
|
48
|
-
let sensitive = false;
|
|
49
|
-
for (const link of ast.encoding ?? []) {
|
|
50
|
-
sensitive ||= containsSensitive(source, keys);
|
|
51
|
-
if (sensitive) {
|
|
52
|
-
for (const property of propertiesOf(link.to)) {
|
|
53
|
-
if (typeof property.name === "string") {
|
|
54
|
-
keys.add(property.name);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
source = link.to;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function fixedOutput(ast: SchemaAST.AST): boolean {
|
|
62
|
-
if (ast._tag === "Union") {
|
|
63
|
-
return ast.types.every(fixedOutput);
|
|
64
|
-
}
|
|
65
|
-
return ast._tag === "Objects" && ast.indexSignatures.length === 0;
|
|
66
|
-
}
|
|
67
|
-
function maskedValue(ast: SchemaAST.AST, keys: ReadonlySet<string>, seen = new Set<SchemaAST.AST>()): boolean {
|
|
68
|
-
if (seen.has(ast)) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
seen.add(ast);
|
|
72
|
-
if (marked(ast)) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
let source = ast;
|
|
76
|
-
let sensitive = false;
|
|
77
|
-
for (const link of ast.encoding ?? []) {
|
|
78
|
-
sensitive ||= containsSensitive(source, keys);
|
|
79
|
-
if (sensitive && !fixedOutput(link.to)) {
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
source = link.to;
|
|
83
|
-
}
|
|
84
|
-
const nested = ast._tag === "Objects" ? ast.indexSignatures.map((index) => index.type) : children(ast);
|
|
85
|
-
return nested.some((type) => maskedValue(type, keys, seen));
|
|
86
|
-
}
|
|
87
|
-
function collect(ast: SchemaAST.AST, nodes: Set<SchemaAST.AST>, encodings = true) {
|
|
88
|
-
if (nodes.has(ast)) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
nodes.add(ast);
|
|
92
|
-
for (const child of [...children(ast), ...(encodings ? (ast.encoding?.map((link) => link.to) ?? []) : [])]) {
|
|
93
|
-
collect(child, nodes, encodings);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
function propertiesOf(ast: SchemaAST.AST) {
|
|
97
|
-
const nodes = new Set<SchemaAST.AST>();
|
|
98
|
-
collect(ast, nodes, false);
|
|
99
|
-
return [...nodes].flatMap((node) => (node._tag === "Objects" ? node.propertySignatures : []));
|
|
100
|
-
}
|
|
101
|
-
function collectFieldKeys(properties: SchemaAST.Objects["propertySignatures"], keys: Set<string>, sensitive: (ast: SchemaAST.AST) => boolean) {
|
|
102
|
-
for (const property of properties) {
|
|
103
|
-
if (typeof property.name === "string" && sensitive(property.type)) {
|
|
104
|
-
keys.add(property.name);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export function schemaSensitiveKeys<R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, extra: readonly string[] = []) {
|
|
6
|
+
function rpcSensitiveKeys(rpc: Rpc.AnyWithProps, extra: readonly string[]) {
|
|
109
7
|
const keys = new Set(extra);
|
|
110
8
|
const nodes = new Set<SchemaAST.AST>();
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
rpc.successSchema,
|
|
121
|
-
rpc.errorSchema,
|
|
122
|
-
rpc.defectSchema,
|
|
123
|
-
...[...rpc.middlewares].map((m) => m.error),
|
|
124
|
-
];
|
|
125
|
-
for (const schema of schemas) {
|
|
126
|
-
collect(schema.ast, nodes);
|
|
127
|
-
}
|
|
9
|
+
const schemas: Schema.Top[] = [
|
|
10
|
+
rpc.payloadSchema,
|
|
11
|
+
rpc.successSchema,
|
|
12
|
+
rpc.errorSchema,
|
|
13
|
+
rpc.defectSchema,
|
|
14
|
+
...[...rpc.middlewares].map((m) => m.error),
|
|
15
|
+
];
|
|
16
|
+
for (const schema of schemas) {
|
|
17
|
+
collect(schema.ast, nodes);
|
|
128
18
|
}
|
|
129
19
|
const properties = [...nodes].flatMap((ast) => (ast._tag === "Objects" ? ast.propertySignatures : []));
|
|
130
20
|
collectFieldKeys(properties, keys, marked);
|
|
@@ -136,13 +26,24 @@ export function schemaSensitiveKeys<R extends Rpc.Any>(group: RpcGroup.RpcGroup<
|
|
|
136
26
|
collectEncodedKeys(ast, keys);
|
|
137
27
|
}
|
|
138
28
|
} while (keys.size !== previousSize);
|
|
139
|
-
|
|
29
|
+
return Object.assign((key: string) => keys.has(key) || isSensitiveKey(key), {
|
|
140
30
|
die: maskedValue(rpc.defectSchema.ast, keys),
|
|
141
31
|
fail: [rpc.errorSchema, ...[...rpc.middlewares].map((m) => m.error)].some((schema) => maskedValue(schema.ast, keys)),
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function schemaSensitiveKeys<R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, extra: readonly string[] = []) {
|
|
35
|
+
const policies = new Map<string, ReturnType<typeof rpcSensitiveKeys>>();
|
|
36
|
+
for (const rpc of group.requests.values()) {
|
|
37
|
+
if (Rpc.isRpc(rpc)) {
|
|
38
|
+
policies.set(rpc._tag, rpcSensitiveKeys(rpc, extra));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function sensitive(key: string) {
|
|
42
|
+
return extra.includes(key) || isSensitiveKey(key) || [...policies.values()].some((policy) => policy(key));
|
|
43
|
+
}
|
|
44
|
+
return Object.assign(sensitive, {
|
|
45
|
+
forRpc: (tag: string | undefined) => (tag === undefined ? sensitive : (policies.get(tag) ?? sensitive)),
|
|
145
46
|
maskFailure: (tag: string | undefined, reason: "Fail" | "Die") =>
|
|
146
|
-
|
|
47
|
+
[...policies].some(([method, policy]) => (tag === undefined || tag === method) && (reason === "Fail" ? policy.fail : policy.die)),
|
|
147
48
|
});
|
|
148
49
|
}
|
package/src/setupDatabases.ts
CHANGED
|
@@ -8,5 +8,5 @@ export const setupDatabases = Effect.fn("Platform.setupDatabases")(function* ()
|
|
|
8
8
|
"DOCKER_HOST": Config.String("DOCKER_HOST").pipe(Config.withDefault("")),
|
|
9
9
|
"TEST_DATABASE_URL": Config.String("TEST_DATABASE_URL"),
|
|
10
10
|
});
|
|
11
|
-
yield*
|
|
11
|
+
yield* localPostgres(config).prepareDatabases([config.DATABASE_URL, config.TEST_DATABASE_URL]);
|
|
12
12
|
});
|