@shivaedev/platform 0.18.2 → 0.20.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +30 -9
  3. package/dist/server/RedactionPolicy.d.ts +4 -0
  4. package/dist/server/RedactionPolicy.d.ts.map +1 -0
  5. package/dist/server/RedactionPolicy.js +4 -0
  6. package/dist/server/RedactionPolicy.js.map +1 -0
  7. package/dist/server/appHost.js +3 -3
  8. package/dist/server/appHost.js.map +1 -1
  9. package/dist/server/appHostHttp.d.ts +4 -4
  10. package/dist/server/appHostHttp.d.ts.map +1 -1
  11. package/dist/server/appHostHttp.js +17 -11
  12. package/dist/server/appHostHttp.js.map +1 -1
  13. package/dist/server/commandChanges.d.ts +4 -0
  14. package/dist/server/commandChanges.d.ts.map +1 -0
  15. package/dist/server/commandChanges.js +39 -0
  16. package/dist/server/commandChanges.js.map +1 -0
  17. package/dist/server/databaseLayer.d.ts +2 -2
  18. package/dist/server/databaseLayer.d.ts.map +1 -1
  19. package/dist/server/databaseLayer.js +6 -5
  20. package/dist/server/databaseLayer.js.map +1 -1
  21. package/dist/server/hostChanges.d.ts +5 -0
  22. package/dist/server/hostChanges.d.ts.map +1 -0
  23. package/dist/server/hostChanges.js +23 -0
  24. package/dist/server/hostChanges.js.map +1 -0
  25. package/dist/server/hostOptions.d.ts +1 -0
  26. package/dist/server/hostOptions.d.ts.map +1 -1
  27. package/dist/server/otlpHttpClient.d.ts.map +1 -1
  28. package/dist/server/otlpHttpClient.js +3 -1
  29. package/dist/server/otlpHttpClient.js.map +1 -1
  30. package/dist/server/otlpTracing.d.ts.map +1 -1
  31. package/dist/server/otlpTracing.js +4 -1
  32. package/dist/server/otlpTracing.js.map +1 -1
  33. package/dist/server/redactingDiagnostics.d.ts +5 -0
  34. package/dist/server/redactingDiagnostics.d.ts.map +1 -0
  35. package/dist/server/redactingDiagnostics.js +25 -0
  36. package/dist/server/redactingDiagnostics.js.map +1 -0
  37. package/dist/server/redactingRpcSerialization.d.ts +4 -0
  38. package/dist/server/redactingRpcSerialization.d.ts.map +1 -0
  39. package/dist/server/redactingRpcSerialization.js +74 -0
  40. package/dist/server/redactingRpcSerialization.js.map +1 -0
  41. package/dist/server/redactingTracer.d.ts +4 -0
  42. package/dist/server/redactingTracer.d.ts.map +1 -0
  43. package/dist/server/redactingTracer.js +32 -0
  44. package/dist/server/redactingTracer.js.map +1 -0
  45. package/dist/server/rpc-server/redact.d.ts +2 -1
  46. package/dist/server/rpc-server/redact.d.ts.map +1 -1
  47. package/dist/server/rpc-server/redact.js +26 -18
  48. package/dist/server/rpc-server/redact.js.map +1 -1
  49. package/dist/server/rpc-server/redactCause.d.ts +2 -1
  50. package/dist/server/rpc-server/redactCause.d.ts.map +1 -1
  51. package/dist/server/rpc-server/redactCause.js +64 -8
  52. package/dist/server/rpc-server/redactCause.js.map +1 -1
  53. package/dist/server/rpc-server/sensitive.d.ts +2 -1
  54. package/dist/server/rpc-server/sensitive.d.ts.map +1 -1
  55. package/dist/server/rpc-server/sensitive.js +8 -2
  56. package/dist/server/rpc-server/sensitive.js.map +1 -1
  57. package/dist/server/rpc-server/tracing.d.ts +1 -1
  58. package/dist/server/rpc-server/tracing.d.ts.map +1 -1
  59. package/dist/server/rpc-server/tracing.js +11 -4
  60. package/dist/server/rpc-server/tracing.js.map +1 -1
  61. package/dist/server/safeOtlpSerialization.d.ts +2 -1
  62. package/dist/server/safeOtlpSerialization.d.ts.map +1 -1
  63. package/dist/server/safeOtlpSerialization.js +32 -21
  64. package/dist/server/safeOtlpSerialization.js.map +1 -1
  65. package/dist/server/schemaSensitiveKeys.d.ts +5 -0
  66. package/dist/server/schemaSensitiveKeys.d.ts.map +1 -0
  67. package/dist/server/schemaSensitiveKeys.js +143 -0
  68. package/dist/server/schemaSensitiveKeys.js.map +1 -0
  69. package/package.json +6 -6
  70. package/src/server/RedactionPolicy.ts +4 -0
  71. package/src/server/appHost.ts +3 -3
  72. package/src/server/appHostHttp.ts +23 -9
  73. package/src/server/commandChanges.ts +46 -0
  74. package/src/server/databaseLayer.ts +7 -6
  75. package/src/server/hostChanges.ts +27 -0
  76. package/src/server/hostOptions.ts +1 -0
  77. package/src/server/otlpHttpClient.ts +3 -1
  78. package/src/server/otlpTracing.ts +4 -1
  79. package/src/server/redactingDiagnostics.ts +33 -0
  80. package/src/server/redactingRpcSerialization.ts +85 -0
  81. package/src/server/redactingTracer.ts +37 -0
  82. package/src/server/rpc-server/redact.ts +26 -18
  83. package/src/server/rpc-server/redactCause.ts +73 -9
  84. package/src/server/rpc-server/sensitive.ts +8 -2
  85. package/src/server/rpc-server/tracing.ts +11 -5
  86. package/src/server/safeOtlpSerialization.ts +44 -34
  87. package/src/server/schemaSensitiveKeys.ts +148 -0
@@ -2,9 +2,10 @@ import { Cause, Effect, Layer, Option, Predicate, Random } from "effect";
2
2
  import { Headers } from "effect/http";
3
3
  import { RequestId } from "#rpc/identity.ts";
4
4
  import { RequestTracing } from "#rpc/middleware.ts";
5
+ import { commandChanges } from "#server/commandChanges.ts";
5
6
  import { redact } from "./redact.ts";
6
7
  import { redactCause } from "./redactCause.ts";
7
- import type { SensitiveKey } from "./sensitive.ts";
8
+ import { isSensitiveKey, REDACTED, redactText, type SensitiveKey } from "./sensitive.ts";
8
9
  export interface RequestTracingOptions {
9
10
  readonly header?: string;
10
11
  readonly sensitive?: SensitiveKey;
@@ -23,10 +24,15 @@ function requestIdOf(headers: Headers.Headers, header: string): Effect.Effect<st
23
24
  },
24
25
  );
25
26
  }
26
- function failureTag(cause: Cause.Cause<unknown>): string {
27
+ function failureTag(cause: Cause.Cause<unknown>, sensitive: SensitiveKey = isSensitiveKey): string {
27
28
  return Option.match(Cause.findErrorOption(cause), {
28
29
  onNone: () => "unknown",
29
- onSome: (error) => (Predicate.hasProperty(error, "_tag") && Predicate.isString(error._tag) ? error._tag : "unknown"),
30
+ onSome: (error) => {
31
+ if (!(Predicate.hasProperty(error, "_tag") && Predicate.isString(error._tag))) {
32
+ return "unknown";
33
+ }
34
+ return sensitive("_tag") ? REDACTED : redactText(error._tag, sensitive);
35
+ },
30
36
  });
31
37
  }
32
38
  function logFailure(cause: Cause.Cause<unknown>, payload: unknown, sensitive: SensitiveKey | undefined): Effect.Effect<void> {
@@ -35,7 +41,7 @@ function logFailure(cause: Cause.Cause<unknown>, payload: unknown, sensitive: Se
35
41
  }
36
42
  const annotations = { "rpc.payload": redact(payload, sensitive) };
37
43
  if (!Cause.hasDies(cause)) {
38
- return Effect.annotateLogs(Effect.logInfo("RPC failure"), { ...annotations, "rpc.failure": failureTag(cause) });
44
+ return Effect.annotateLogs(Effect.logInfo("RPC failure"), { ...annotations, "rpc.failure": failureTag(cause, sensitive) });
39
45
  }
40
46
  const defects = cause.reasons.filter(Cause.isDieReason).map((reason) => redact(reason.defect, sensitive));
41
47
  return Effect.annotateLogs(Effect.logError("RPC defect"), { ...annotations, "rpc.defect": defects });
@@ -45,7 +51,7 @@ export function requestTracingLayer(options: RequestTracingOptions = {}): Layer.
45
51
  Effect.gen(function* () {
46
52
  const requestId = yield* requestIdOf(headers, options.header ?? "x-request-id");
47
53
  yield* Effect.annotateCurrentSpan({ "request.id": requestId, "rpc.method": rpc._tag });
48
- return yield* effect.pipe(
54
+ return yield* commandChanges(effect, rpc, payload).pipe(
49
55
  Effect.catchCauseIf(Cause.hasDies, (cause) => Effect.failCause(redactCause(cause, options.sensitive))),
50
56
  Effect.tapCause((cause) => logFailure(cause, payload, options.sensitive)),
51
57
  Effect.provideService(RequestId, requestId),
@@ -1,27 +1,28 @@
1
1
  import { Effect, Layer } from "effect";
2
- import type { OtlpResource } from "effect/observability";
2
+ import type { OtlpResource, OtlpTracer } from "effect/observability";
3
3
  import { OtlpSerialization } from "effect/observability";
4
+ import { RedactionPolicy } from "#server/RedactionPolicy.ts";
4
5
  import { redactOtlpUrl } from "#server/redactOtlpUrl.ts";
5
- import { isSensitiveKey, REDACTED, redactText } from "#server/rpc-server/sensitive.ts";
6
+ import { isSensitiveKey, REDACTED, redactText, type SensitiveKey } from "#server/rpc-server/sensitive.ts";
6
7
 
7
- function sensitive(key: string) {
8
- return isSensitiveKey(key) || ["db.user", "db.user.name", "db.connection_string"].includes(key.toLowerCase());
8
+ function otlpSensitive(sensitive: SensitiveKey): SensitiveKey {
9
+ return (key) => sensitive(key) || ["db.user", "db.user.name", "db.connection_string"].includes(key.toLowerCase());
9
10
  }
10
- export function redactOtlpText(value: string, key = "") {
11
- return redactText(redactOtlpUrl(value, key), sensitive);
11
+ export function redactOtlpText(value: string, key = "", sensitive: SensitiveKey = isSensitiveKey) {
12
+ return redactText(redactOtlpUrl(value, key), otlpSensitive(sensitive));
12
13
  }
13
- function attributeValue(value: OtlpResource.AnyValue, key: string): OtlpResource.AnyValue {
14
+ function attributeValue(value: OtlpResource.AnyValue, key: string, sensitive: SensitiveKey): OtlpResource.AnyValue {
14
15
  return {
15
16
  ...value,
16
- ...(typeof value.stringValue === "string" ? { stringValue: redactOtlpText(value.stringValue, key) } : {}),
17
- ...(value.arrayValue ? { arrayValue: { values: value.arrayValue.values.map((item) => attributeValue(item, key)) } } : {}),
18
- ...(value.kvlistValue ? { kvlistValue: { values: attributes(value.kvlistValue.values) } } : {}),
17
+ ...(typeof value.stringValue === "string" ? { stringValue: redactOtlpText(value.stringValue, key, sensitive) } : {}),
18
+ ...(value.arrayValue ? { arrayValue: { values: value.arrayValue.values.map((item) => attributeValue(item, key, sensitive)) } } : {}),
19
+ ...(value.kvlistValue ? { kvlistValue: { values: attributes(value.kvlistValue.values, sensitive) } } : {}),
19
20
  };
20
21
  }
21
- function attributes(values: OtlpResource.KeyValue[]): OtlpResource.KeyValue[] {
22
+ function attributes(values: OtlpResource.KeyValue[], sensitive: SensitiveKey): OtlpResource.KeyValue[] {
22
23
  return values.map(({ key, value }) => ({
23
24
  key,
24
- value: sensitive(key) ? { stringValue: REDACTED } : attributeValue(value, key),
25
+ value: sensitive(key) ? { stringValue: REDACTED } : attributeValue(value, key, sensitive),
25
26
  }));
26
27
  }
27
28
 
@@ -36,28 +37,37 @@ function spanName(span: { readonly name: string; readonly kind: number; readonly
36
37
  return span.name;
37
38
  }
38
39
 
40
+ function redactTraces(data: OtlpTracer.TraceData, sensitive: SensitiveKey): OtlpTracer.TraceData {
41
+ function text(value: string) {
42
+ return redactOtlpText(value, "", sensitive);
43
+ }
44
+ return {
45
+ ...data,
46
+ resourceSpans: data.resourceSpans.map((resource) => ({
47
+ ...resource,
48
+ resource: { ...resource.resource, attributes: attributes(resource.resource.attributes, sensitive) },
49
+ scopeSpans: resource.scopeSpans.map((scope) => ({
50
+ ...scope,
51
+ spans: scope.spans.map((span) => ({
52
+ ...span,
53
+ attributes: attributes(span.attributes, sensitive),
54
+ events: span.events.map((event) => ({ ...event, attributes: attributes(event.attributes, sensitive), name: text(event.name) })),
55
+ links: span.links.map((link) => ({ ...link, attributes: attributes(link.attributes, sensitive) })),
56
+ name: text(spanName(span)),
57
+ status: { ...span.status, ...(span.status.message === undefined ? {} : { message: text(span.status.message) }) },
58
+ })),
59
+ })),
60
+ })),
61
+ };
62
+ }
39
63
  export const safeOtlpSerialization = Layer.effect(
40
64
  OtlpSerialization.OtlpSerialization,
41
- Effect.map(OtlpSerialization.OtlpSerialization, (serialization) => ({
42
- ...serialization,
43
- traces: (data) =>
44
- serialization.traces({
45
- ...data,
46
- resourceSpans: data.resourceSpans.map((resource) => ({
47
- ...resource,
48
- resource: { ...resource.resource, attributes: attributes(resource.resource.attributes) },
49
- scopeSpans: resource.scopeSpans.map((scope) => ({
50
- ...scope,
51
- spans: scope.spans.map((span) => ({
52
- ...span,
53
- attributes: attributes(span.attributes),
54
- events: span.events.map((event) => ({ ...event, attributes: attributes(event.attributes), name: redactOtlpText(event.name) })),
55
- links: span.links.map((link) => ({ ...link, attributes: attributes(link.attributes) })),
56
- name: redactOtlpText(spanName(span)),
57
- status: { ...span.status, ...(span.status.message === undefined ? {} : { message: redactOtlpText(span.status.message) }) },
58
- })),
59
- })),
60
- })),
61
- }),
62
- })),
65
+ Effect.gen(function* () {
66
+ const serialization = yield* OtlpSerialization.OtlpSerialization;
67
+ const sensitive = otlpSensitive(yield* RedactionPolicy);
68
+ return OtlpSerialization.OtlpSerialization.of({
69
+ ...serialization,
70
+ traces: (data) => serialization.traces(redactTraces(data, sensitive)),
71
+ });
72
+ }),
63
73
  );
@@ -0,0 +1,148 @@
1
+ import { Predicate, type Schema, type SchemaAST } from "effect";
2
+ import { Rpc, type RpcGroup } from "effect/rpc";
3
+ import { isSensitiveKey } from "#server/rpc-server/sensitive.ts";
4
+
5
+ function children(ast: SchemaAST.AST): readonly SchemaAST.AST[] {
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[] = []) {
109
+ const keys = new Set(extra);
110
+ const nodes = new Set<SchemaAST.AST>();
111
+ const requests: Rpc.AnyWithProps[] = [];
112
+ for (const rpc of group.requests.values()) {
113
+ if (Rpc.isRpc(rpc)) {
114
+ requests.push(rpc);
115
+ }
116
+ }
117
+ for (const rpc of requests) {
118
+ const schemas: Schema.Top[] = [
119
+ rpc.payloadSchema,
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
+ }
128
+ }
129
+ const properties = [...nodes].flatMap((ast) => (ast._tag === "Objects" ? ast.propertySignatures : []));
130
+ collectFieldKeys(properties, keys, marked);
131
+ let previousSize: number;
132
+ do {
133
+ previousSize = keys.size;
134
+ collectFieldKeys(properties, keys, (ast) => maskedValue(ast, keys));
135
+ for (const ast of nodes) {
136
+ collectEncodedKeys(ast, keys);
137
+ }
138
+ } while (keys.size !== previousSize);
139
+ const roots = requests.map((rpc) => ({
140
+ die: maskedValue(rpc.defectSchema.ast, keys),
141
+ fail: [rpc.errorSchema, ...[...rpc.middlewares].map((m) => m.error)].some((schema) => maskedValue(schema.ast, keys)),
142
+ tag: rpc._tag,
143
+ }));
144
+ return Object.assign((key: string) => keys.has(key) || isSensitiveKey(key), {
145
+ maskFailure: (tag: string | undefined, reason: "Fail" | "Die") =>
146
+ roots.some((root) => (tag === undefined || tag === root.tag) && (reason === "Fail" ? root.fail : root.die)),
147
+ });
148
+ }