@shivaedev/server 0.9.1 → 0.9.3
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 +178 -0
- 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.js +1 -1
- 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/package.json +11 -9
- package/src/RedactionPolicy.ts +7 -1
- package/src/hostChanges.ts +4 -2
- 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
|
@@ -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
|
}
|