@shivaedev/errors 0.4.1 → 0.6.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 (50) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +17 -2
  3. package/dist/boundary.d.ts.map +1 -1
  4. package/dist/boundary.js +18 -3
  5. package/dist/boundary.js.map +1 -1
  6. package/dist/redactBrowserText.d.ts +3 -0
  7. package/dist/redactBrowserText.d.ts.map +1 -0
  8. package/dist/redactBrowserText.js +6 -0
  9. package/dist/redactBrowserText.js.map +1 -0
  10. package/dist/rpcBoundaryHandlers.d.ts +1 -1
  11. package/dist/rpcBoundaryHandlers.d.ts.map +1 -1
  12. package/dist/rpcBoundaryHandlers.js +12 -4
  13. package/dist/rpcBoundaryHandlers.js.map +1 -1
  14. package/dist/schemaFieldScope.d.ts.map +1 -1
  15. package/dist/schemaFieldScope.js +24 -13
  16. package/dist/schemaFieldScope.js.map +1 -1
  17. package/dist/schemaSensitiveAst.d.ts +1 -0
  18. package/dist/schemaSensitiveAst.d.ts.map +1 -1
  19. package/dist/schemaSensitiveAst.js +13 -0
  20. package/dist/schemaSensitiveAst.js.map +1 -1
  21. package/dist/schemaSensitiveKeys.d.ts +2 -1
  22. package/dist/schemaSensitiveKeys.d.ts.map +1 -1
  23. package/dist/schemaSensitiveKeys.js +16 -3
  24. package/dist/schemaSensitiveKeys.js.map +1 -1
  25. package/dist/sensitive.d.ts.map +1 -1
  26. package/dist/sensitive.js +4 -2
  27. package/dist/sensitive.js.map +1 -1
  28. package/dist/taxonomy.d.ts +3 -1
  29. package/dist/taxonomy.d.ts.map +1 -1
  30. package/dist/taxonomy.js +5 -1
  31. package/dist/taxonomy.js.map +1 -1
  32. package/dist/wireMask.d.ts +11 -0
  33. package/dist/wireMask.d.ts.map +1 -0
  34. package/dist/wireMask.js +88 -0
  35. package/dist/wireMask.js.map +1 -0
  36. package/dist/wireRedactor.d.ts +6 -0
  37. package/dist/wireRedactor.d.ts.map +1 -0
  38. package/dist/wireRedactor.js +105 -0
  39. package/dist/wireRedactor.js.map +1 -0
  40. package/package.json +3 -4
  41. package/src/boundary.ts +16 -3
  42. package/src/redactBrowserText.ts +6 -0
  43. package/src/rpcBoundaryHandlers.ts +16 -9
  44. package/src/schemaFieldScope.ts +29 -13
  45. package/src/schemaSensitiveAst.ts +13 -0
  46. package/src/schemaSensitiveKeys.ts +16 -3
  47. package/src/sensitive.ts +5 -2
  48. package/src/taxonomy.ts +5 -1
  49. package/src/wireMask.ts +94 -0
  50. package/src/wireRedactor.ts +131 -0
@@ -0,0 +1,88 @@
1
+ import { Effect, Exit, Predicate, Schema } from "effect";
2
+ import { REDACTED } from "#sensitive.ts";
3
+ export class InvalidRedactionMask extends Schema.TaggedError()("InvalidRedactionMask", {
4
+ field: Schema.String,
5
+ message: Schema.String,
6
+ }) {
7
+ }
8
+ const objectMask = Effect.fnUntraced(function* (ast, field, seen) {
9
+ const entries = [];
10
+ for (const property of ast.propertySignatures) {
11
+ if (!property.type.context?.isOptional) {
12
+ entries.push([property.name, yield* wireMask(property.type, `${field}.${String(property.name)}`, seen)]);
13
+ }
14
+ }
15
+ return Object.fromEntries(entries);
16
+ });
17
+ const arrayMask = Effect.fnUntraced(function* (ast, field, seen) {
18
+ const items = [];
19
+ for (const [index, element] of ast.elements.entries()) {
20
+ if (!element.context?.isOptional) {
21
+ items.push(yield* wireMask(element, `${field}[${index}]`, seen));
22
+ }
23
+ }
24
+ return items;
25
+ });
26
+ const unionMask = Effect.fnUntraced(function* (ast, field, seen) {
27
+ for (const member of ast.types) {
28
+ const candidate = yield* Effect.result(wireMask(member, field, seen));
29
+ if (candidate._tag === "Success") {
30
+ return candidate.success;
31
+ }
32
+ }
33
+ return undefined;
34
+ });
35
+ function genericMask(ast, field, seen) {
36
+ switch (ast._tag) {
37
+ case "String":
38
+ case "Unknown":
39
+ case "Any":
40
+ return Effect.succeed(REDACTED);
41
+ case "Number":
42
+ return Effect.succeed(0);
43
+ case "Boolean":
44
+ return Effect.succeed(false);
45
+ case "Null":
46
+ return Effect.succeed(null);
47
+ case "Undefined":
48
+ case "Void":
49
+ return Effect.succeed(undefined);
50
+ case "Literal":
51
+ return Effect.succeed(ast.literal);
52
+ case "Objects":
53
+ return objectMask(ast, field, seen);
54
+ case "Arrays":
55
+ return arrayMask(ast, field, seen);
56
+ case "Union":
57
+ return unionMask(ast, field, seen);
58
+ case "Suspend":
59
+ return wireMask(ast.thunk(), field, seen);
60
+ default:
61
+ return Effect.succeed(REDACTED);
62
+ }
63
+ }
64
+ export const wireMask = Effect.fnUntraced(function* (ast, field, ancestors = new Set()) {
65
+ function invalid() {
66
+ return new InvalidRedactionMask({
67
+ field,
68
+ message: `Sensitive field "${field}" needs a schema-compatible mask; supply a redactionMask annotation on its wire schema.`,
69
+ });
70
+ }
71
+ if (ancestors.has(ast)) {
72
+ return yield* invalid();
73
+ }
74
+ const seen = new Set(ancestors).add(ast);
75
+ const Annotations = Schema.resolveAnnotations(Schema.make(ast));
76
+ let value;
77
+ if (Predicate.hasProperty(Annotations, "redactionMask")) {
78
+ value = Annotations.redactionMask;
79
+ }
80
+ else {
81
+ value = ast.encoding === undefined ? yield* genericMask(ast, field, seen) : yield* wireMask(ast.encoding.at(-1)?.to ?? ast, field, seen);
82
+ }
83
+ if (Exit.isFailure(Schema.decodeUnknownExit(Schema.toCodecJson(Schema.make(ast)))(value))) {
84
+ return yield* invalid();
85
+ }
86
+ return value;
87
+ });
88
+ //# sourceMappingURL=wireMask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireMask.js","sourceRoot":"","sources":["../src/wireMask.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { Effect, type SchemaAST } from "effect";
2
+ import { type SensitiveKey } from "#sensitive.ts";
3
+ type Redactor = (value: unknown) => unknown;
4
+ export declare const wireRedactor: (ast: SchemaAST.AST, field: string, sensitive: SensitiveKey, keys: ReadonlySet<string>) => Effect.Effect<Redactor, import("#wireMask.ts").InvalidRedactionMask, never>;
5
+ export {};
6
+ //# sourceMappingURL=wireRedactor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireRedactor.d.ts","sourceRoot":"","sources":["../src/wireRedactor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAGnE,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAG5D,KAAK,QAAQ,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;AAyB5C,eAAO,MAAM,YAAY,wKAmGvB,CAAC"}
@@ -0,0 +1,105 @@
1
+ import { Effect, Predicate, Schema } from "effect";
2
+ import { redactWire } from "#redact.ts";
3
+ import { collect, maskedValue, sensitiveEncoding } from "#schemaSensitiveAst.ts";
4
+ import { REDACTED } from "#sensitive.ts";
5
+ import { wireMask } from "#wireMask.ts";
6
+ function objectRedactor(fields, indexes, sensitive) {
7
+ function field(key) {
8
+ const property = fields.get(key);
9
+ if (property !== undefined) {
10
+ return property;
11
+ }
12
+ const index = indexes.find((entry) => entry.accepts(key));
13
+ if (index !== undefined) {
14
+ return sensitive(key) ? index.mask : index.apply;
15
+ }
16
+ return (value) => (sensitive(key) ? REDACTED : redactWire(value, sensitive));
17
+ }
18
+ return (value) => Predicate.isObjectOrArray(value)
19
+ ? Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, field(key)(nested)]))
20
+ : redactWire(value, sensitive);
21
+ }
22
+ export const wireRedactor = Effect.fnUntraced(function* (ast, field, sensitive, keys) {
23
+ const cache = new Map();
24
+ function constrainedContainer(node) {
25
+ if (node.checks === undefined) {
26
+ return false;
27
+ }
28
+ const nodes = new Set();
29
+ collect(node, nodes);
30
+ return [...nodes].some((child) => child._tag === "Objects" && child.propertySignatures.some((property) => typeof property.name === "string" && sensitive(property.name)));
31
+ }
32
+ function encoded(node) {
33
+ if (node.encoding !== undefined) {
34
+ return node.encoding.at(-1)?.to ?? node;
35
+ }
36
+ return node._tag === "Declaration" ? Schema.toEncoded(Schema.toCodecJson(Schema.make(node))).ast : node;
37
+ }
38
+ const compileObject = Effect.fnUntraced(function* (node, path) {
39
+ const fields = new Map();
40
+ for (const property of node.propertySignatures) {
41
+ const stackHeader = property.name === "stack" && (sensitive("message") || sensitive("name"));
42
+ fields.set(property.name, yield* compile(property.type, `${path}.${String(property.name)}`, stackHeader || (typeof property.name === "string" && sensitive(property.name))));
43
+ }
44
+ const indexes = yield* Effect.forEach(node.indexSignatures, (index) => Effect.gen(function* () {
45
+ const apply = yield* compile(index.type, `${path}[value]`);
46
+ const mask = yield* compile(index.type, `${path}[value]`, true);
47
+ return { accepts: Schema.is(Schema.make(index.parameter)), apply, mask };
48
+ }));
49
+ return objectRedactor(fields, indexes, sensitive);
50
+ });
51
+ const compileArray = Effect.fnUntraced(function* (node, path) {
52
+ const elements = yield* Effect.forEach(node.elements, (element, index) => compile(element, `${path}[${index}]`));
53
+ const rest = yield* Effect.forEach(node.rest, (element) => compile(element, `${path}[]`));
54
+ return (value) => Array.isArray(value)
55
+ ? value.map((item, index) => (elements[index] ?? rest[0] ?? ((entry) => redactWire(entry, sensitive)))(item))
56
+ : redactWire(value, sensitive);
57
+ });
58
+ const compileUnion = Effect.fnUntraced(function* (node, path) {
59
+ const members = yield* Effect.forEach(node.types, (member, index) => Effect.map(compile(member, `${path}[${index}]`), (mask) => ({
60
+ accepts: Schema.is(Schema.toEncoded(Schema.toCodecJson(Schema.make(member)))),
61
+ mask,
62
+ })));
63
+ return (value) => {
64
+ const member = members.find(({ accepts }) => accepts(value));
65
+ return member === undefined ? REDACTED : member.mask(value);
66
+ };
67
+ });
68
+ function contents(node, path) {
69
+ switch (node._tag) {
70
+ case "Objects":
71
+ return compileObject(node, path);
72
+ case "Arrays":
73
+ return compileArray(node, path);
74
+ case "Union":
75
+ return compileUnion(node, path);
76
+ case "Suspend":
77
+ return compile(node.thunk(), path);
78
+ default:
79
+ return Effect.succeed((value) => redactWire(value, sensitive));
80
+ }
81
+ }
82
+ function compile(node, path, hidden = false) {
83
+ return Effect.gen(function* () {
84
+ if (hidden
85
+ || constrainedContainer(node)
86
+ || sensitiveEncoding(node, keys)
87
+ || (node._tag !== "Union" && maskedValue(node, keys))
88
+ || Schema.resolveAnnotations(Schema.make(node))?.sensitive === true) {
89
+ const mask = yield* wireMask(node, path);
90
+ return () => mask;
91
+ }
92
+ const cached = cache.get(node);
93
+ if (cached !== undefined) {
94
+ return cached;
95
+ }
96
+ let apply = () => REDACTED;
97
+ const redactor = (value) => apply(value);
98
+ cache.set(node, redactor);
99
+ apply = yield* contents(encoded(node), path);
100
+ return redactor;
101
+ });
102
+ }
103
+ return yield* compile(ast, field);
104
+ });
105
+ //# sourceMappingURL=wireRedactor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireRedactor.js","sourceRoot":"","sources":["../src/wireRedactor.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/errors",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Typed expected failures and secret-safe failure reporting for Effect applications",
5
5
  "keywords": [
6
6
  "effect"
@@ -45,9 +45,8 @@
45
45
  "README.md"
46
46
  ],
47
47
  "devDependencies": {
48
- "@shivaedev/quality": "0.53.1",
49
- "@shivaedev/test": "0.12.0",
50
- "@shivaedev/test-story": "0.31.4",
48
+ "@shivaedev/quality": "0.57.0",
49
+ "@shivaedev/test-story": "0.35.0",
51
50
  "@types/node": "24.19.2",
52
51
  "effect": "4.0.6",
53
52
  "typescript": "7.0.2",
package/src/boundary.ts CHANGED
@@ -19,6 +19,19 @@ function reportUnexpected(cause: Cause.Cause<unknown>): Effect.Effect<void> {
19
19
  ),
20
20
  );
21
21
  }
22
+ function declaredCause<E>(cause: Cause.Cause<unknown>, declared: (value: unknown) => value is E): Cause.Cause<E> | undefined {
23
+ const reasons: Cause.Reason<E>[] = [];
24
+ for (const reason of cause.reasons) {
25
+ if (Cause.isInterruptReason(reason)) {
26
+ reasons.push(reason);
27
+ } else if (Cause.isFailReason(reason) && declared(reason.error)) {
28
+ reasons.push(Cause.makeFailReason(reason.error).annotate(Context.makeUnsafe(reason.annotations)));
29
+ } else {
30
+ return undefined;
31
+ }
32
+ }
33
+ return Cause.fromReasons(reasons);
34
+ }
22
35
  export interface RpcBoundaryOptions<E> {
23
36
  readonly message?: string;
24
37
  readonly unexpected: (message: string) => E;
@@ -34,9 +47,9 @@ export function rpcBoundary<
34
47
  if (Cause.hasInterruptsOnly(cause)) {
35
48
  return Effect.failCause(Cause.fromReasons(cause.reasons.filter(Cause.isInterruptReason)));
36
49
  }
37
- const reason = cause.reasons[0];
38
- if (cause.reasons.length === 1 && reason && Cause.isFailReason(reason) && declared(reason.error)) {
39
- return Effect.fail(reason.error);
50
+ const expected = declaredCause(cause, declared);
51
+ if (expected !== undefined) {
52
+ return Effect.failCause(expected);
40
53
  }
41
54
  const rejection = options.unexpected(redactText(options.message ?? "Internal server error", isSensitiveKey));
42
55
  Object.defineProperties(rejection, {
@@ -0,0 +1,6 @@
1
+ import { isSensitiveKey, REDACTED, redactText, type SensitiveKey } from "#sensitive.ts";
2
+
3
+ export function redactBrowserText(text: string, sensitive: SensitiveKey = isSensitiveKey): string {
4
+ const masked = text.replaceAll(/(?<quote>["`]|(?<![\p{L}\p{N}_])')(?:\\[\s\S]|(?!\k<quote>)[^\\])*\k<quote>/gu, `$<quote>${REDACTED}$<quote>`);
5
+ return redactText(masked, sensitive);
6
+ }
@@ -1,7 +1,7 @@
1
- import { Effect, Stream } from "effect";
2
- import { Rpc, type RpcGroup, type RpcSchema } from "effect/rpc";
1
+ import { Effect, Result, Stream } from "effect";
2
+ import { Rpc, type RpcGroup, RpcSchema } from "effect/rpc";
3
3
 
4
- type Result = Effect.Effect<unknown, unknown, unknown> | Stream.Stream<unknown, unknown, unknown>;
4
+ type HandlerResult = Effect.Effect<unknown, unknown, unknown> | Stream.Stream<unknown, unknown, unknown>;
5
5
  type Handlers<R extends Rpc.Any> = {
6
6
  readonly [TRpc in R as TRpc["_tag"]]: (
7
7
  ...args: Parameters<Rpc.ToHandlerFn<TRpc>>
@@ -31,17 +31,24 @@ export function rpcBoundaryHandlers<TRpcs extends Rpc.Any, E, THandlers extends
31
31
  handlers: THandlers,
32
32
  ): BoundedHandlers<THandlers, E>;
33
33
  export function rpcBoundaryHandlers(
34
- _group: RpcGroup.RpcGroup<Rpc.Any>,
34
+ group: RpcGroup.RpcGroup<Rpc.Any>,
35
35
  boundary: <A, TError, R>(effect: Effect.Effect<A, TError, R>) => Effect.Effect<A, unknown, R>,
36
- handlers: Readonly<Record<string, (...args: never[]) => Rpc.WrapperOr<Result>>>,
36
+ handlers: Readonly<Record<string, (...args: never[]) => Rpc.WrapperOr<HandlerResult>>>,
37
37
  ) {
38
38
  return Object.fromEntries(
39
- Object.entries<(...args: never[]) => Rpc.WrapperOr<Result>>(handlers).map(([tag, handler]) => [
39
+ Object.entries<(...args: never[]) => Rpc.WrapperOr<HandlerResult>>(handlers).map(([tag, handler]) => [
40
40
  tag,
41
- (...args: never[]) =>
42
- Rpc.wrapMap(handler(...args), (result) =>
41
+ (...args: never[]) => {
42
+ const constructed = Result.try(() => handler(...args));
43
+ if (Result.isFailure(constructed)) {
44
+ const failure = boundary(Effect.die(constructed.failure));
45
+ const rpc = group.requests.get(tag);
46
+ return Rpc.isRpc(rpc) && RpcSchema.isStreamSchema(rpc.successSchema) ? Stream.fromEffect(failure) : failure;
47
+ }
48
+ return Rpc.wrapMap(constructed.success, (result) =>
43
49
  Effect.isEffect(result) ? boundary(result) : Stream.catchCause(result, (cause) => Stream.fromEffect(boundary(Effect.failCause(cause)))),
44
- ),
50
+ );
51
+ },
45
52
  ]),
46
53
  );
47
54
  }
@@ -27,45 +27,61 @@ interface Field {
27
27
 
28
28
  function fieldsOf(ast: SchemaAST.AST, schema: string, extra: readonly string[], candidates: ReadonlySet<string>): Field[] {
29
29
  const fields: Field[] = [];
30
- function visitIndex(index: SchemaAST.IndexSignature, path: string, hidden: boolean, seen: ReadonlySet<SchemaAST.AST>) {
30
+ function visitIndex(
31
+ index: SchemaAST.IndexSignature,
32
+ path: string,
33
+ hidden: boolean,
34
+ seen: ReadonlySet<SchemaAST.AST>,
35
+ encoded: ReadonlySet<string>,
36
+ ) {
31
37
  const accepts = Schema.is(Schema.make(index.parameter));
32
38
  for (const key of candidates) {
33
39
  if (accepts(key)) {
34
- fields.push({ key, path: `${path}[${key}]`, sensitive: hidden || extra.includes(key) || isSensitiveKey(key) });
40
+ fields.push({ key, path: `${path}[${key}]`, sensitive: hidden || encoded.has(key) || extra.includes(key) || isSensitiveKey(key) });
35
41
  }
36
42
  }
37
- visit(index.type, `${path}[value]`, hidden, seen);
43
+ visit(index.type, `${path}[value]`, hidden, seen, encoded);
38
44
  }
39
- function visitObject(node: SchemaAST.Objects, path: string, hidden: boolean, seen: ReadonlySet<SchemaAST.AST>) {
45
+ function visitObject(node: SchemaAST.Objects, path: string, hidden: boolean, seen: ReadonlySet<SchemaAST.AST>, encoded: ReadonlySet<string>) {
40
46
  for (const property of node.propertySignatures) {
41
47
  if (typeof property.name !== "string") {
42
48
  continue;
43
49
  }
44
50
  const sensitive =
45
- hidden || extra.includes(property.name) || isSensitiveKey(property.name) || maskedValue(property.type, schemaFieldKeys(property.type, []));
51
+ hidden
52
+ || encoded.has(property.name)
53
+ || extra.includes(property.name)
54
+ || isSensitiveKey(property.name)
55
+ || maskedValue(property.type, schemaFieldKeys(property.type, []));
46
56
  const fieldPath = `${path}.${property.name}`;
47
57
  fields.push({ key: property.name, path: fieldPath, sensitive });
48
- visit(property.type, fieldPath, sensitive, seen);
58
+ visit(property.type, fieldPath, sensitive, seen, encoded);
49
59
  }
50
60
  for (const index of node.indexSignatures) {
51
- visitIndex(index, path, hidden, seen);
61
+ visitIndex(index, path, hidden, seen, encoded);
52
62
  }
53
63
  }
54
- function visit(node: SchemaAST.AST, path: string, hidden: boolean, ancestors: ReadonlySet<SchemaAST.AST>) {
64
+ function visit(node: SchemaAST.AST, path: string, hidden: boolean, ancestors: ReadonlySet<SchemaAST.AST>, encoded: ReadonlySet<string>) {
55
65
  if (ancestors.has(node)) {
56
66
  return;
57
67
  }
58
68
  const seen = new Set(ancestors).add(node);
59
69
  const masked = hidden || marked(node);
60
70
  if (node._tag === "Objects") {
61
- visitObject(node, path, masked, seen);
62
- return;
71
+ visitObject(node, path, masked, seen, encoded);
72
+ } else {
73
+ for (const [index, child] of children(node).entries()) {
74
+ visit(child, `${path}[${index}]`, masked, seen, encoded);
75
+ }
63
76
  }
64
- for (const [index, child] of children(node).entries()) {
65
- visit(child, `${path}[${index}]`, masked, seen);
77
+ if (node.encoding !== undefined) {
78
+ const keys = new Set([...encoded, ...schemaFieldKeys(node, [])]);
79
+ for (const link of node.encoding) {
80
+ visit(link.to, `${path}.encoded`, masked, seen, keys);
81
+ }
66
82
  }
67
83
  }
68
- visit(ast, schema, false, new Set());
84
+ visit(ast, schema, false, new Set(), new Set());
69
85
  for (const key of schemaFieldKeys(ast, [])) {
70
86
  if (!fields.some((field) => field.key === key && field.sensitive)) {
71
87
  fields.push({ key, path: `${schema}.encoded.${key}`, sensitive: true });
@@ -43,6 +43,9 @@ function containsSensitive(ast: SchemaAST.AST, keys: ReadonlySet<string>): boole
43
43
  );
44
44
  }
45
45
  function preservesFields(source: SchemaAST.AST, target: SchemaAST.AST): boolean {
46
+ if (source._tag === "Declaration" && source.annotations?.["~constructor"] !== undefined && source.typeParameters[0] === target) {
47
+ return true;
48
+ }
46
49
  return (
47
50
  source._tag === "Declaration"
48
51
  && Predicate.hasProperty(source.annotations?.representation, "id")
@@ -70,6 +73,16 @@ export function collectEncodedKeys(ast: SchemaAST.AST, keys: Set<string>) {
70
73
  source = link.to;
71
74
  }
72
75
  }
76
+ export function sensitiveEncoding(ast: SchemaAST.AST, keys: ReadonlySet<string>): boolean {
77
+ let source = ast;
78
+ for (const link of ast.encoding ?? []) {
79
+ if (!preservesFields(source, link.to) && (containsSensitive(source, keys) || containsSensitive(link.to, keys))) {
80
+ return true;
81
+ }
82
+ source = link.to;
83
+ }
84
+ return false;
85
+ }
73
86
  function fixedOutput(ast: SchemaAST.AST): boolean {
74
87
  if (ast._tag === "Union") {
75
88
  return ast.types.every(fixedOutput);
@@ -2,21 +2,30 @@ import { Effect } from "effect";
2
2
  import { Rpc, type RpcGroup } from "effect/rpc";
3
3
  import { schemaFieldKeys, validateFieldScope } from "#schemaFieldScope.ts";
4
4
  import { maskedValue } from "#schemaSensitiveAst.ts";
5
- import { isSensitiveKey } from "#sensitive.ts";
5
+ import { isSensitiveKey, REDACTED } from "#sensitive.ts";
6
+ import { wireRedactor } from "#wireRedactor.ts";
6
7
 
7
8
  const rpcSensitiveKeys = Effect.fnUntraced(function* (rpc: Rpc.AnyWithProps, extra: readonly string[]) {
9
+ const Errors = Rpc.exitSchema(rpc).error;
8
10
  const schemas = [
9
11
  { ast: rpc.payloadSchema.ast, name: `${rpc._tag}.payload` },
10
12
  { ast: rpc.successSchema.ast, name: `${rpc._tag}.success` },
11
13
  { ast: rpc.errorSchema.ast, name: `${rpc._tag}.error` },
12
14
  { ast: rpc.defectSchema.ast, name: `${rpc._tag}.defect` },
13
15
  ...[...rpc.middlewares].map((middleware, index) => ({ ast: middleware.error.ast, name: `${rpc._tag}.middleware[${index}].error` })),
16
+ { ast: Errors.ast, name: `${rpc._tag}.exit.error` },
14
17
  ];
15
18
  yield* validateFieldScope(schemas, extra);
16
19
  const keys = new Set(schemas.flatMap(({ ast }) => [...schemaFieldKeys(ast, extra)]));
17
- return Object.assign((key: string) => keys.has(key) || isSensitiveKey(key), {
20
+ function sensitive(key: string) {
21
+ return keys.has(key) || isSensitiveKey(key);
22
+ }
23
+ const fail = yield* wireRedactor(Errors.ast, `${rpc._tag}.error`, sensitive, keys);
24
+ const die = yield* wireRedactor(rpc.defectSchema.ast, `${rpc._tag}.defect`, sensitive, keys);
25
+ return Object.assign(sensitive, {
18
26
  die: maskedValue(rpc.defectSchema.ast, keys),
19
- fail: [rpc.errorSchema, ...[...rpc.middlewares].map((m) => m.error)].some((schema) => maskedValue(schema.ast, keys)),
27
+ fail: maskedValue(Errors.ast, keys),
28
+ redactFailure: (reason: "Fail" | "Die", value: unknown) => (reason === "Fail" ? fail(value) : die(value)),
20
29
  });
21
30
  });
22
31
  export const schemaSensitiveKeys = Effect.fnUntraced(function* <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, extra: readonly string[] = []) {
@@ -33,5 +42,9 @@ export const schemaSensitiveKeys = Effect.fnUntraced(function* <R extends Rpc.An
33
42
  forRpc: (tag: string | undefined) => (tag === undefined ? sensitive : (policies.get(tag) ?? sensitive)),
34
43
  maskFailure: (tag: string | undefined, reason: "Fail" | "Die") =>
35
44
  [...policies].some(([method, policy]) => (tag === undefined || tag === method) && (reason === "Fail" ? policy.fail : policy.die)),
45
+ redactFailure: (tag: string | undefined, reason: "Fail" | "Die", value: unknown) => {
46
+ const policy = policies.get(tag ?? "");
47
+ return policy === undefined ? REDACTED : policy.redactFailure(reason, value);
48
+ },
36
49
  });
37
50
  });
package/src/sensitive.ts CHANGED
@@ -44,14 +44,17 @@ const AUTH_SCHEME = /\b(?<scheme>Bearer|Basic)\s+[A-Za-z0-9%._~+/-]+=*/giu;
44
44
  const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/gu;
45
45
  const PRIVATE_KEY = /-----BEGIN (?<kind>(?:[A-Z0-9]+ )*PRIVATE KEY)-----[\s\S]*?-----END \k<kind>-----/gu;
46
46
  function redactAssignments(text: string, sensitive: SensitiveKey): string {
47
- const matcher = /(?<key>[A-Za-z][\w.-]*)(?<separator>["']?\s*[=:]\s*["']?)(?<value>[^\s"'&,;]+)/gu;
47
+ const matcher =
48
+ /(?<key>[A-Za-z][\w.-]*)(?<separator>["']?\s*[=:]\s*)(?<value>(?<quote>["'])(?:\\[\s\S]?|(?!\k<quote>)[^\\])*(?<closingQuote>\k<quote>|$)|[^\s"'&,;]+)/gu;
48
49
  let result = "";
49
50
  let cursor = 0;
50
51
  for (let match = matcher.exec(text); match !== null; match = matcher.exec(text)) {
51
52
  const key = match[1] ?? "";
52
53
  const separator = match[2] ?? "";
53
54
  if (sensitive(key)) {
54
- result += `${text.slice(cursor, match.index)}${key}${separator}${REDACTED}`;
55
+ const quote = match.groups?.quote ?? "";
56
+ const closingQuote = match.groups?.closingQuote ?? "";
57
+ result += `${text.slice(cursor, match.index)}${key}${separator}${quote}${REDACTED}${closingQuote}`;
55
58
  cursor = matcher.lastIndex;
56
59
  } else if (match[3]?.includes("=")) {
57
60
  // Revisit embedded parameters without parsing URL and source-location colons as assignments.
package/src/taxonomy.ts CHANGED
@@ -8,7 +8,11 @@ export class Forbidden extends Schema.TaggedError<Forbidden>()("Forbidden", desc
8
8
  export class BadRequest extends Schema.TaggedError<BadRequest>()("BadRequest", perField) {}
9
9
  export class Conflict extends Schema.TaggedError<Conflict>()("Conflict", perField) {}
10
10
  export class PreconditionFailed extends Schema.TaggedError<PreconditionFailed>()("PreconditionFailed", described) {}
11
- export class TooManyRequests extends Schema.TaggedError<TooManyRequests>()("TooManyRequests", described) {}
11
+ export class TooManyRequests extends Schema.TaggedError<TooManyRequests>()("TooManyRequests", {
12
+ ...described,
13
+ key: Schema.optionalKey(Schema.String),
14
+ retryAt: Schema.optionalKey(Schema.Number),
15
+ }) {}
12
16
  export class AuthUnavailable extends Schema.TaggedError<AuthUnavailable>()("AuthUnavailable", described) {}
13
17
  export class InternalServerError extends Schema.TaggedError<InternalServerError>()("InternalServerError", described) {}
14
18
  export class ServiceUnavailable extends Schema.TaggedError<ServiceUnavailable>()("ServiceUnavailable", described) {}
@@ -0,0 +1,94 @@
1
+ import { Effect, Exit, Predicate, Schema, type SchemaAST } from "effect";
2
+ import { REDACTED } from "#sensitive.ts";
3
+
4
+ export class InvalidRedactionMask extends Schema.TaggedError<InvalidRedactionMask>()("InvalidRedactionMask", {
5
+ field: Schema.String,
6
+ message: Schema.String,
7
+ }) {}
8
+
9
+ type Ancestors = ReadonlySet<SchemaAST.AST>;
10
+
11
+ const objectMask = Effect.fnUntraced(function* (ast: SchemaAST.Objects, field: string, seen: Ancestors) {
12
+ const entries: [PropertyKey, unknown][] = [];
13
+ for (const property of ast.propertySignatures) {
14
+ if (!property.type.context?.isOptional) {
15
+ entries.push([property.name, yield* wireMask(property.type, `${field}.${String(property.name)}`, seen)]);
16
+ }
17
+ }
18
+ return Object.fromEntries(entries);
19
+ });
20
+ const arrayMask = Effect.fnUntraced(function* (ast: SchemaAST.Arrays, field: string, seen: Ancestors) {
21
+ const items: unknown[] = [];
22
+ for (const [index, element] of ast.elements.entries()) {
23
+ if (!element.context?.isOptional) {
24
+ items.push(yield* wireMask(element, `${field}[${index}]`, seen));
25
+ }
26
+ }
27
+ return items;
28
+ });
29
+ const unionMask = Effect.fnUntraced(function* (ast: SchemaAST.Union, field: string, seen: Ancestors) {
30
+ for (const member of ast.types) {
31
+ const candidate = yield* Effect.result(wireMask(member, field, seen));
32
+ if (candidate._tag === "Success") {
33
+ return candidate.success;
34
+ }
35
+ }
36
+ return undefined;
37
+ });
38
+ function genericMask(ast: SchemaAST.AST, field: string, seen: Ancestors): Effect.Effect<unknown, InvalidRedactionMask> {
39
+ switch (ast._tag) {
40
+ case "String":
41
+ case "Unknown":
42
+ case "Any":
43
+ return Effect.succeed(REDACTED);
44
+ case "Number":
45
+ return Effect.succeed(0);
46
+ case "Boolean":
47
+ return Effect.succeed(false);
48
+ case "Null":
49
+ return Effect.succeed(null);
50
+ case "Undefined":
51
+ case "Void":
52
+ return Effect.succeed(undefined);
53
+ case "Literal":
54
+ return Effect.succeed(ast.literal);
55
+ case "Objects":
56
+ return objectMask(ast, field, seen);
57
+ case "Arrays":
58
+ return arrayMask(ast, field, seen);
59
+ case "Union":
60
+ return unionMask(ast, field, seen);
61
+ case "Suspend":
62
+ return wireMask(ast.thunk(), field, seen);
63
+ default:
64
+ return Effect.succeed(REDACTED);
65
+ }
66
+ }
67
+
68
+ export const wireMask = Effect.fnUntraced(function* (
69
+ ast: SchemaAST.AST,
70
+ field: string,
71
+ ancestors: Ancestors = new Set(),
72
+ ): Effect.fn.Return<unknown, InvalidRedactionMask> {
73
+ function invalid() {
74
+ return new InvalidRedactionMask({
75
+ field,
76
+ message: `Sensitive field "${field}" needs a schema-compatible mask; supply a redactionMask annotation on its wire schema.`,
77
+ });
78
+ }
79
+ if (ancestors.has(ast)) {
80
+ return yield* invalid();
81
+ }
82
+ const seen = new Set(ancestors).add(ast);
83
+ const Annotations = Schema.resolveAnnotations(Schema.make(ast));
84
+ let value: unknown;
85
+ if (Predicate.hasProperty(Annotations, "redactionMask")) {
86
+ value = Annotations.redactionMask;
87
+ } else {
88
+ value = ast.encoding === undefined ? yield* genericMask(ast, field, seen) : yield* wireMask(ast.encoding.at(-1)?.to ?? ast, field, seen);
89
+ }
90
+ if (Exit.isFailure(Schema.decodeUnknownExit(Schema.toCodecJson(Schema.make<Schema.Codec<unknown>>(ast)))(value))) {
91
+ return yield* invalid();
92
+ }
93
+ return value;
94
+ });