@shivaedev/errors 0.2.0 → 0.4.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 +23 -0
- package/README.md +17 -1
- package/dist/rpcBoundaryHandlers.d.ts +14 -0
- package/dist/rpcBoundaryHandlers.d.ts.map +1 -0
- package/dist/rpcBoundaryHandlers.js +9 -0
- package/dist/rpcBoundaryHandlers.js.map +1 -0
- package/dist/schemaFieldScope.d.ts +16 -0
- package/dist/schemaFieldScope.d.ts.map +1 -0
- package/dist/schemaFieldScope.js +90 -0
- package/dist/schemaFieldScope.js.map +1 -0
- package/dist/schemaSensitiveAst.d.ts +1 -0
- package/dist/schemaSensitiveAst.d.ts.map +1 -1
- package/dist/schemaSensitiveAst.js +16 -1
- package/dist/schemaSensitiveAst.js.map +1 -1
- package/dist/schemaSensitiveKeys.d.ts +3 -2
- package/dist/schemaSensitiveKeys.d.ts.map +1 -1
- package/dist/schemaSensitiveKeys.js +15 -26
- package/dist/schemaSensitiveKeys.js.map +1 -1
- package/package.json +2 -2
- package/src/rpcBoundaryHandlers.ts +47 -0
- package/src/schemaFieldScope.ts +104 -0
- package/src/schemaSensitiveAst.ts +18 -1
- package/src/schemaSensitiveKeys.ts +17 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Derive encoded sensitive names separately for each RPC schema, keeping private fields in one method from changing public state and native failure decoding in another method.
|
|
8
|
+
- Keep the child key names of a fully masked field public elsewhere, including through native `OptionFromNullOr` encoding.
|
|
9
|
+
|
|
10
|
+
### Breaking
|
|
11
|
+
|
|
12
|
+
- Refuse RPC registration when a schema-sensitive field name also names a public field within that method's schemas, including nested fields, encoded names and middleware errors. Diagnostics cannot distinguish those plain values. Rename one field or mark both sensitive; fields in separate RPC methods may keep different sensitivity.
|
|
13
|
+
- `schemaSensitiveKeys` now returns an Effect. Native consumers must use `yield* schemaSensitiveKeys(group, extraKeys)` to obtain the predicate and handle `SensitiveFieldConflict` from `@shivaedev/errors/schemaFieldScope.ts`. `appHost` and Work Board's `boardLayer` include that failure in their Layer error channels.
|
|
14
|
+
|
|
15
|
+
## 0.3.0 - 2026-10-31
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `rpcBoundaryHandlers(group, boundary, handlers)` applies one boundary and unexpected mapping to a native RPC handler map, including Effect and Stream handlers and native RPC wrappers.
|
|
20
|
+
- Contract changes streams declare `WebSocketRequired` so HTTP clients can decode the protocol error.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Hosted `$changes` subscriptions over HTTP fail with a message saying they need the WebSocket protocol.
|
|
25
|
+
|
|
3
26
|
## 0.2.0 - 2026-10-31
|
|
4
27
|
|
|
5
28
|
### Added
|
package/README.md
CHANGED
|
@@ -70,6 +70,20 @@ export const Handlers = Api.toLayer({
|
|
|
70
70
|
|
|
71
71
|
`storageSave` is the application's Effect. `unexpected` constructs a member of the declared schema from the public message. The default message is `Internal server error`; `message` supplies application text, with credential-shaped strings redacted. Before masking an unexpected failure, the boundary reports its diagnostic-redacted cause through Effect's registered `ErrorReporter`s at `Error` severity. Request tracing logs that masked failure at ERROR with its redacted stack, while declared rejections stay at INFO. The original Effect cause is kept on the server rejection's non-enumerable `cause`, so a server span can trace it. Keep `cause` out of the wire schema, as the taxonomy classes do. `ServiceUnavailable` describes a declared availability failure without implying a session-provider outage.
|
|
72
72
|
|
|
73
|
+
Apply the same boundary to an entire handler registration with `rpcBoundaryHandlers(group, boundary, handlers)`:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { rpcBoundaryHandlers } from "@shivaedev/errors/rpcBoundaryHandlers.ts";
|
|
77
|
+
|
|
78
|
+
export const Handlers = Api.toLayer(
|
|
79
|
+
rpcBoundaryHandlers(Api, expose, {
|
|
80
|
+
Save: () => storageSave,
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The group supplies payload and result types. Each RPC must declare the boundary's rejection schema. Effect and Stream handlers share the mapping, including failures after a stream has delivered values; native `Rpc.Wrapper` options remain available. Handler service requirements pass through to the native handler Layer. Use this map inside an Effect passed to `toLayer` when handler construction needs services. Queue-backed streams and Deferred results use native handlers with an explicit boundary at their producer instead.
|
|
86
|
+
|
|
73
87
|
### Masking secrets in failures
|
|
74
88
|
|
|
75
89
|
`sensitive.ts` decides which names are secret. `isSensitiveKey(key)` matches credential-shaped names whatever their casing or separator, such as `sessionId`, `bearerToken`, `access_key_id` or `cardNumber`, and database diagnostic keys `db.user`, `db.user.name` and `db.connection_string`. A `SensitiveKey` is any `(key: string) => boolean`, so an application can widen the policy with its own names. `redactText(text, sensitive, maxLength?, key?)` masks credential-shaped text and `key=value` assignments inside a string, URL user information, query values and fragments, and cuts it at 2,048 characters by default; passing `Infinity` keeps complete public text. The optional `key` supplies context for standalone URL components such as `url.query`. `redactUrl(value, key?)` from `sensitive.ts` applies just the URL rules, including nested JSON strings. Masked values become `REDACTED`, the string `<redacted>`.
|
|
@@ -78,7 +92,9 @@ export const Handlers = Api.toLayer({
|
|
|
78
92
|
|
|
79
93
|
`redactCause.ts` applies it to Effect causes. `redactCause(cause, sensitive?)` rebuilds a cause with masked defects and keeps its typed failures, interruptions and annotations as they are, so the failure channel keeps its type. `redactDiagnosticCause(cause, sensitive?)` masks failures, defects and annotations for diagnostics, `redactDefect(defect, sensitive?)` masks one defect, and `redactingErrorReporter(reporter, sensitive?)` wraps an Effect error reporter so it only ever receives diagnostic-masked causes.
|
|
80
94
|
|
|
81
|
-
`schemaSensitiveKeys(group, extraKeys?)` from `schemaSensitiveKeys.ts` derives the key predicate from native RPC payload, success, error, defect and middleware error schemas. It includes nested sensitive annotations, Redacted schemas, encoded field names and additional application keys.
|
|
95
|
+
`schemaSensitiveKeys(group, extraKeys?)` from `schemaSensitiveKeys.ts` derives the key predicate from native RPC payload, success, error, defect and middleware error schemas. It includes nested sensitive annotations, Redacted schemas, encoded field names and additional application keys. The predicate’s `forRpc(tag)` method selects the schema-derived policy for one RPC; additional application keys apply to every RPC. The predicate’s `maskFailure(tag, reason)` method identifies whole error or defect roots that need masking. The host applies this predicate to logs, error reports, RPC failure data and exported traces.
|
|
96
|
+
|
|
97
|
+
Use `yield* schemaSensitiveKeys(group, extraKeys)` to obtain the predicate. Each schema derives its encoded sensitive names independently. Masking a containing field does not independently mark its child names sensitive; native `OptionFromNullOr` encoding preserves those field scopes. Different RPC methods can use the same field name with different sensitivity. Within one RPC, diagnostics carry plain values without schema identity, so a sensitive name cannot also name a public field in its payload, success, failure or middleware schemas, including nested fields. The Effect fails with `SensitiveFieldConflict` from `schemaFieldScope.ts`, naming both schema paths. Rename one field or mark both sensitive. Explicit application keys and built-in credential rules apply everywhere and do not create mixed visibility.
|
|
82
98
|
|
|
83
99
|
The policy matches names and credential or URL shapes, not arbitrary values: it does not scan prose for personal data, so keep secrets out of free text and span names.
|
|
84
100
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
|
+
import { Rpc, type RpcGroup, type RpcSchema } from "effect/rpc";
|
|
3
|
+
type Handlers<R extends Rpc.Any> = {
|
|
4
|
+
readonly [TRpc in R as TRpc["_tag"]]: (...args: Parameters<Rpc.ToHandlerFn<TRpc>>) => Rpc.WrapperOr<TRpc extends {
|
|
5
|
+
readonly successSchema: RpcSchema.Stream<infer TSuccess, infer _E>;
|
|
6
|
+
} ? Stream.Stream<TSuccess["Type"], unknown, unknown> : Effect.Effect<Rpc.Success<TRpc>, unknown, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
type Bounded<T, E> = T extends Effect.Effect<infer A, infer _E, infer R> ? Effect.Effect<A, E, R> : T extends Stream.Stream<infer A, infer _E, infer R> ? Stream.Stream<A, E, R> : T extends Rpc.Wrapper<infer TValue> ? Rpc.Wrapper<Bounded<TValue, E>> : never;
|
|
9
|
+
type BoundedHandlers<THandlers, E> = {
|
|
10
|
+
readonly [TKey in keyof THandlers]: THandlers[TKey] extends (...args: infer TArgs) => infer TValue ? (...args: TArgs) => Bounded<TValue, E> : never;
|
|
11
|
+
};
|
|
12
|
+
export declare function rpcBoundaryHandlers<TRpcs extends Rpc.Any, E, THandlers extends Handlers<TRpcs>>(group: RpcGroup.RpcGroup<TRpcs>, boundary: <A, TError, R>(effect: Effect.Effect<A, TError, R>) => Effect.Effect<A, E, R>, handlers: THandlers): BoundedHandlers<THandlers, E>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=rpcBoundaryHandlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpcBoundaryHandlers.d.ts","sourceRoot":"","sources":["../src/rpcBoundaryHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,GAAG,EAAE,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAGhE,KAAK,QAAQ,CAAC,CAAC,SAAS,GAAG,CAAC,GAAG,IAAI;IAClC,QAAQ,EAAE,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CACrC,GAAG,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,KACtC,GAAG,CAAC,SAAS,CACjB,IAAI,SAAS;QAAE,QAAQ,CAAC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAA;KAAE,GAChF,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,GACjD,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CACrD;CACD,CAAC;AAEF,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,IAChB,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,GAChD,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACtB,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,GAClD,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACtB,CAAC,SAAS,GAAG,CAAC,OAAO,CAAC,MAAM,MAAM,CAAC,GAClC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,GAC/B,KAAK,CAAC;AAEZ,KAAK,eAAe,CAAC,SAAS,EAAE,CAAC,IAAI;IACpC,QAAQ,EAAE,IAAI,IAAI,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,KAAK,KAAK,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,KAAK;CACnJ,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,SAAS,SAAS,QAAQ,CAAC,KAAK,CAAC,EAC9F,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAC/B,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACvF,QAAQ,EAAE,SAAS,GACjB,eAAe,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
|
+
import { Rpc } from "effect/rpc";
|
|
3
|
+
export function rpcBoundaryHandlers(_group, boundary, handlers) {
|
|
4
|
+
return Object.fromEntries(Object.entries(handlers).map(([tag, handler]) => [
|
|
5
|
+
tag,
|
|
6
|
+
(...args) => Rpc.wrapMap(handler(...args), (result) => Effect.isEffect(result) ? boundary(result) : Stream.catchCause(result, (cause) => Stream.fromEffect(boundary(Effect.failCause(cause))))),
|
|
7
|
+
]));
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=rpcBoundaryHandlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpcBoundaryHandlers.js","sourceRoot":"","sources":["../src/rpcBoundaryHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,GAAG,EAAiC,MAAM,YAAY,CAAC;AA+BhE,MAAM,UAAU,mBAAmB,CAClC,MAAkC,EAClC,QAA6F,EAC7F,QAA+E;IAE/E,OAAO,MAAM,CAAC,WAAW,CACxB,MAAM,CAAC,OAAO,CAA8C,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC;QAC7F,GAAG;QACH,CAAC,GAAG,IAAa,EAAE,EAAE,CACpB,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CACxC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CACvI;KACF,CAAC,CACF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Effect, Schema, type SchemaAST } from "effect";
|
|
2
|
+
export declare function schemaFieldKeys(ast: SchemaAST.AST, extra: readonly string[]): Set<string>;
|
|
3
|
+
declare const SensitiveFieldConflict_base: Schema.Class<SensitiveFieldConflict, Schema.TaggedStruct<"SensitiveFieldConflict", {
|
|
4
|
+
readonly field: Schema.String;
|
|
5
|
+
readonly message: Schema.String;
|
|
6
|
+
readonly publicSchema: Schema.String;
|
|
7
|
+
readonly sensitiveSchema: Schema.String;
|
|
8
|
+
}>, import("effect/Cause").YieldableError>;
|
|
9
|
+
export declare class SensitiveFieldConflict extends SensitiveFieldConflict_base {
|
|
10
|
+
}
|
|
11
|
+
export declare const validateFieldScope: (schemas: readonly {
|
|
12
|
+
readonly ast: SchemaAST.AST;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
}[], extra: readonly string[]) => Effect.Effect<undefined, SensitiveFieldConflict, never>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=schemaFieldScope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemaFieldScope.d.ts","sourceRoot":"","sources":["../src/schemaFieldScope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAIxD,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,eAe3E;;;;;;;AAyDD,qBAAa,sBAAuB,SAAQ,2BAK1C;CAAG;AAEL,eAAO,MAAM,kBAAkB;kBACI,SAAS,CAAC,GAAG;mBAAiB,MAAM;yFAmBrE,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { children, collect, collectEncodedKeys, collectFieldKeys, marked, maskedValue } from "#schemaSensitiveAst.ts";
|
|
3
|
+
import { isSensitiveKey } from "#sensitive.ts";
|
|
4
|
+
export function schemaFieldKeys(ast, extra) {
|
|
5
|
+
const keys = new Set(extra);
|
|
6
|
+
const nodes = new Set();
|
|
7
|
+
collect(ast, nodes);
|
|
8
|
+
const properties = [...nodes].flatMap((node) => (node._tag === "Objects" ? node.propertySignatures : []));
|
|
9
|
+
collectFieldKeys(properties, keys, marked);
|
|
10
|
+
let previousSize;
|
|
11
|
+
do {
|
|
12
|
+
previousSize = keys.size;
|
|
13
|
+
collectFieldKeys(properties, keys, (node) => maskedValue(node, keys));
|
|
14
|
+
for (const node of nodes) {
|
|
15
|
+
collectEncodedKeys(node, keys);
|
|
16
|
+
}
|
|
17
|
+
} while (keys.size !== previousSize);
|
|
18
|
+
return keys;
|
|
19
|
+
}
|
|
20
|
+
function fieldsOf(ast, schema, extra, candidates) {
|
|
21
|
+
const fields = [];
|
|
22
|
+
function visitIndex(index, path, hidden, seen) {
|
|
23
|
+
const accepts = Schema.is(Schema.make(index.parameter));
|
|
24
|
+
for (const key of candidates) {
|
|
25
|
+
if (accepts(key)) {
|
|
26
|
+
fields.push({ key, path: `${path}[${key}]`, sensitive: hidden || extra.includes(key) || isSensitiveKey(key) });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
visit(index.type, `${path}[value]`, hidden, seen);
|
|
30
|
+
}
|
|
31
|
+
function visitObject(node, path, hidden, seen) {
|
|
32
|
+
for (const property of node.propertySignatures) {
|
|
33
|
+
if (typeof property.name !== "string") {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const sensitive = hidden || extra.includes(property.name) || isSensitiveKey(property.name) || maskedValue(property.type, schemaFieldKeys(property.type, []));
|
|
37
|
+
const fieldPath = `${path}.${property.name}`;
|
|
38
|
+
fields.push({ key: property.name, path: fieldPath, sensitive });
|
|
39
|
+
visit(property.type, fieldPath, sensitive, seen);
|
|
40
|
+
}
|
|
41
|
+
for (const index of node.indexSignatures) {
|
|
42
|
+
visitIndex(index, path, hidden, seen);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function visit(node, path, hidden, ancestors) {
|
|
46
|
+
if (ancestors.has(node)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const seen = new Set(ancestors).add(node);
|
|
50
|
+
const masked = hidden || marked(node);
|
|
51
|
+
if (node._tag === "Objects") {
|
|
52
|
+
visitObject(node, path, masked, seen);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const [index, child] of children(node).entries()) {
|
|
56
|
+
visit(child, `${path}[${index}]`, masked, seen);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
visit(ast, schema, false, new Set());
|
|
60
|
+
for (const key of schemaFieldKeys(ast, [])) {
|
|
61
|
+
if (!fields.some((field) => field.key === key && field.sensitive)) {
|
|
62
|
+
fields.push({ key, path: `${schema}.encoded.${key}`, sensitive: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return fields;
|
|
66
|
+
}
|
|
67
|
+
export class SensitiveFieldConflict extends Schema.TaggedError()("SensitiveFieldConflict", {
|
|
68
|
+
field: Schema.String,
|
|
69
|
+
message: Schema.String,
|
|
70
|
+
publicSchema: Schema.String,
|
|
71
|
+
sensitiveSchema: Schema.String,
|
|
72
|
+
}) {
|
|
73
|
+
}
|
|
74
|
+
export const validateFieldScope = Effect.fn("Errors.validateFieldScope")(function* (schemas, extra) {
|
|
75
|
+
const candidates = new Set(schemas.flatMap(({ ast }) => [...schemaFieldKeys(ast, [])]));
|
|
76
|
+
const fields = schemas.flatMap(({ ast, name }) => fieldsOf(ast, name, extra, candidates));
|
|
77
|
+
// A masked container protects its children without making each child name sensitive.
|
|
78
|
+
for (const hidden of fields.filter((field) => candidates.has(field.key) && field.sensitive && !extra.includes(field.key) && !isSensitiveKey(field.key))) {
|
|
79
|
+
const publicField = fields.find((field) => field.key === hidden.key && !field.sensitive);
|
|
80
|
+
if (publicField !== undefined) {
|
|
81
|
+
return yield* new SensitiveFieldConflict({
|
|
82
|
+
field: hidden.key,
|
|
83
|
+
message: `Sensitive field "${hidden.key}" in schema ${hidden.path} conflicts with public field in schema ${publicField.path}; RPC diagnostics cannot distinguish these fields. Use distinct field names or mark both fields sensitive.`,
|
|
84
|
+
publicSchema: publicField.path,
|
|
85
|
+
sensitiveSchema: hidden.path,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=schemaFieldScope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemaFieldScope.js","sourceRoot":"","sources":["../src/schemaFieldScope.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SchemaAST } from "effect";
|
|
2
|
+
export declare function children(ast: SchemaAST.AST): readonly SchemaAST.AST[];
|
|
2
3
|
export declare function marked(ast: SchemaAST.AST, seen?: Set<SchemaAST.AST>): boolean;
|
|
3
4
|
export declare function collectEncodedKeys(ast: SchemaAST.AST, keys: Set<string>): void;
|
|
4
5
|
export declare function maskedValue(ast: SchemaAST.AST, keys: ReadonlySet<string>, seen?: Set<SchemaAST.AST>): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemaSensitiveAst.d.ts","sourceRoot":"","sources":["../src/schemaSensitiveAst.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"schemaSensitiveAst.d.ts","sourceRoot":"","sources":["../src/schemaSensitiveAst.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAGnD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,GAAG,SAAS,SAAS,CAAC,GAAG,EAAE,CAerE;AAID,wBAAgB,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,IAAI,qBAA2B,GAAG,OAAO,CAenF;AAgBD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,QAkBvE;AAOD,wBAAgB,WAAW,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,qBAA2B,GAAG,OAAO,CAuBnH;AACD,wBAAgB,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,SAAS,UAAO,QAQtF;AAMD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,KAAK,OAAO,QAMlJ"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Predicate } from "effect";
|
|
2
2
|
import { isSensitiveKey } from "#sensitive.ts";
|
|
3
|
-
function children(ast) {
|
|
3
|
+
export function children(ast) {
|
|
4
4
|
switch (ast._tag) {
|
|
5
5
|
case "Objects":
|
|
6
6
|
return [...ast.propertySignatures.map((property) => property.type), ...ast.indexSignatures.flatMap((index) => [index.parameter, index.type])];
|
|
@@ -37,10 +37,21 @@ function containsSensitive(ast, keys) {
|
|
|
37
37
|
return (marked(ast)
|
|
38
38
|
|| propertiesOf(ast).some((property) => typeof property.name === "string" && (keys.has(property.name) || isSensitiveKey(property.name))));
|
|
39
39
|
}
|
|
40
|
+
function preservesFields(source, target) {
|
|
41
|
+
return (source._tag === "Declaration"
|
|
42
|
+
&& Predicate.hasProperty(source.annotations?.representation, "id")
|
|
43
|
+
&& source.annotations.representation.id === "effect/schema/Option"
|
|
44
|
+
&& target._tag === "Union"
|
|
45
|
+
&& target.types.some((type) => type._tag === "Null"));
|
|
46
|
+
}
|
|
40
47
|
export function collectEncodedKeys(ast, keys) {
|
|
41
48
|
let source = ast;
|
|
42
49
|
let sensitive = false;
|
|
43
50
|
for (const link of ast.encoding ?? []) {
|
|
51
|
+
if (preservesFields(source, link.to)) {
|
|
52
|
+
source = link.to;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
44
55
|
sensitive ||= containsSensitive(source, keys);
|
|
45
56
|
if (sensitive) {
|
|
46
57
|
for (const property of propertiesOf(link.to)) {
|
|
@@ -69,6 +80,10 @@ export function maskedValue(ast, keys, seen = new Set()) {
|
|
|
69
80
|
let source = ast;
|
|
70
81
|
let sensitive = false;
|
|
71
82
|
for (const link of ast.encoding ?? []) {
|
|
83
|
+
if (preservesFields(source, link.to)) {
|
|
84
|
+
source = link.to;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
72
87
|
sensitive ||= containsSensitive(source, keys);
|
|
73
88
|
if (sensitive && !fixedOutput(link.to)) {
|
|
74
89
|
return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemaSensitiveAst.js","sourceRoot":"","sources":["../src/schemaSensitiveAst.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,
|
|
1
|
+
{"version":3,"file":"schemaSensitiveAst.js","sourceRoot":"","sources":["../src/schemaSensitiveAst.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,UAAU,QAAQ,CAAC,GAAkB;IAC1C,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,SAAS;YACb,OAAO,CAAC,GAAG,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC/I,KAAK,QAAQ;YACZ,OAAO,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,KAAK,OAAO;YACX,OAAO,GAAG,CAAC,KAAK,CAAC;QAClB,KAAK,aAAa;YACjB,OAAO,GAAG,CAAC,cAAc,CAAC;QAC3B,KAAK,SAAS;YACb,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;QACtB;YACC,OAAO,EAAE,CAAC;IACZ,CAAC;AACF,CAAC;AACD,SAAS,cAAc,CAAC,KAA6B;IACpD,OAAO,KAAK,CAAC,WAAW,EAAE,SAAS,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;AACrH,CAAC;AACD,MAAM,UAAU,MAAM,CAAC,GAAkB,EAAE,IAAI,GAAG,IAAI,GAAG,EAAiB;IACzE,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;IACd,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACd,IACC,GAAG,CAAC,WAAW,EAAE,SAAS,KAAK,IAAI;WAChC,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,KAAK,IAAI;WAC5C,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,CAAC;WAChC,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,EAAE,cAAc,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,KAAK,wBAAwB,CAAC,EAClI,CAAC;QACF,OAAO,IAAI,CAAC;IACb,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AACpH,CAAC;AACD,SAAS,iBAAiB,CAAC,GAAkB,EAAE,IAAyB;IACvE,OAAO,CACN,MAAM,CAAC,GAAG,CAAC;WACR,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CACxI,CAAC;AACH,CAAC;AACD,SAAS,eAAe,CAAC,MAAqB,EAAE,MAAqB;IACpE,OAAO,CACN,MAAM,CAAC,IAAI,KAAK,aAAa;WAC1B,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,WAAW,EAAE,cAAc,EAAE,IAAI,CAAC;WAC/D,MAAM,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,KAAK,sBAAsB;WAC/D,MAAM,CAAC,IAAI,KAAK,OAAO;WACvB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CACpD,CAAC;AACH,CAAC;AACD,MAAM,UAAU,kBAAkB,CAAC,GAAkB,EAAE,IAAiB;IACvE,IAAI,MAAM,GAAG,GAAG,CAAC;IACjB,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QACvC,IAAI,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACtC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;YACjB,SAAS;QACV,CAAC;QACD,SAAS,KAAK,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC9C,IAAI,SAAS,EAAE,CAAC;YACf,KAAK,MAAM,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC9C,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACvC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBACzB,CAAC;YACF,CAAC;QACF,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAClB,CAAC;AACF,CAAC;AACD,SAAS,WAAW,CAAC,GAAkB;IACtC,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC1B,OAAO,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,CAAC;AACnE,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,GAAkB,EAAE,IAAyB,EAAE,IAAI,GAAG,IAAI,GAAG,EAAiB;IACzG,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;IACd,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACd,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACb,CAAC;IACD,IAAI,MAAM,GAAG,GAAG,CAAC;IACjB,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QACvC,IAAI,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACtC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;YACjB,SAAS;QACV,CAAC;QACD,SAAS,KAAK,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC9C,IAAI,SAAS,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC;QACb,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvG,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAC7D,CAAC;AACD,MAAM,UAAU,OAAO,CAAC,GAAkB,EAAE,KAAyB,EAAE,SAAS,GAAG,IAAI;IACtF,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACpB,OAAO;IACR,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QAC5G,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IAClC,CAAC;AACF,CAAC;AACD,SAAS,YAAY,CAAC,GAAkB;IACvC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiB,CAAC;IACvC,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAC3B,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/F,CAAC;AACD,MAAM,UAAU,gBAAgB,CAAC,UAAmD,EAAE,IAAiB,EAAE,SAA0C;IAClJ,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACnE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;IACF,CAAC;AACF,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { Rpc, type RpcGroup } from "effect/rpc";
|
|
2
|
-
export declare
|
|
3
|
+
export declare const schemaSensitiveKeys: <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, extra?: readonly string[] | undefined) => Effect.Effect<((key: string) => boolean) & {
|
|
3
4
|
forRpc: (tag: string | undefined) => (key: string) => boolean;
|
|
4
5
|
maskFailure: (tag: string | undefined, reason: "Fail" | "Die") => boolean;
|
|
5
|
-
}
|
|
6
|
+
}, import("#schemaFieldScope.ts").SensitiveFieldConflict, never>;
|
|
6
7
|
//# sourceMappingURL=schemaSensitiveKeys.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemaSensitiveKeys.d.ts","sourceRoot":"","sources":["../src/schemaSensitiveKeys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schemaSensitiveKeys.d.ts","sourceRoot":"","sources":["../src/schemaSensitiveKeys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,GAAG,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAoBhD,eAAO,MAAM,mBAAmB,GAAgC,CAAC,SAAS,GAAG,CAAC,GAAG,8FAOxD,MAAM;kBAIf,MAAM,GAAG,SAAS,WAJT,MAAM;uBAKV,MAAM,GAAG,SAAS,UAAU,MAAM,GAAG,KAAK;gEAG7D,CAAC"}
|
|
@@ -1,39 +1,28 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { Rpc } from "effect/rpc";
|
|
2
|
-
import {
|
|
3
|
+
import { schemaFieldKeys, validateFieldScope } from "#schemaFieldScope.ts";
|
|
4
|
+
import { maskedValue } from "#schemaSensitiveAst.ts";
|
|
3
5
|
import { isSensitiveKey } from "#sensitive.ts";
|
|
4
|
-
|
|
5
|
-
const keys = new Set(extra);
|
|
6
|
-
const nodes = new Set();
|
|
6
|
+
const rpcSensitiveKeys = Effect.fnUntraced(function* (rpc, extra) {
|
|
7
7
|
const schemas = [
|
|
8
|
-
rpc.payloadSchema,
|
|
9
|
-
rpc.successSchema,
|
|
10
|
-
rpc.errorSchema,
|
|
11
|
-
rpc.defectSchema,
|
|
12
|
-
...[...rpc.middlewares].map((
|
|
8
|
+
{ ast: rpc.payloadSchema.ast, name: `${rpc._tag}.payload` },
|
|
9
|
+
{ ast: rpc.successSchema.ast, name: `${rpc._tag}.success` },
|
|
10
|
+
{ ast: rpc.errorSchema.ast, name: `${rpc._tag}.error` },
|
|
11
|
+
{ ast: rpc.defectSchema.ast, name: `${rpc._tag}.defect` },
|
|
12
|
+
...[...rpc.middlewares].map((middleware, index) => ({ ast: middleware.error.ast, name: `${rpc._tag}.middleware[${index}].error` })),
|
|
13
13
|
];
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
const properties = [...nodes].flatMap((ast) => (ast._tag === "Objects" ? ast.propertySignatures : []));
|
|
18
|
-
collectFieldKeys(properties, keys, marked);
|
|
19
|
-
let previousSize;
|
|
20
|
-
do {
|
|
21
|
-
previousSize = keys.size;
|
|
22
|
-
collectFieldKeys(properties, keys, (ast) => maskedValue(ast, keys));
|
|
23
|
-
for (const ast of nodes) {
|
|
24
|
-
collectEncodedKeys(ast, keys);
|
|
25
|
-
}
|
|
26
|
-
} while (keys.size !== previousSize);
|
|
14
|
+
yield* validateFieldScope(schemas, extra);
|
|
15
|
+
const keys = new Set(schemas.flatMap(({ ast }) => [...schemaFieldKeys(ast, extra)]));
|
|
27
16
|
return Object.assign((key) => keys.has(key) || isSensitiveKey(key), {
|
|
28
17
|
die: maskedValue(rpc.defectSchema.ast, keys),
|
|
29
18
|
fail: [rpc.errorSchema, ...[...rpc.middlewares].map((m) => m.error)].some((schema) => maskedValue(schema.ast, keys)),
|
|
30
19
|
});
|
|
31
|
-
}
|
|
32
|
-
export
|
|
20
|
+
});
|
|
21
|
+
export const schemaSensitiveKeys = Effect.fnUntraced(function* (group, extra = []) {
|
|
33
22
|
const policies = new Map();
|
|
34
23
|
for (const rpc of group.requests.values()) {
|
|
35
24
|
if (Rpc.isRpc(rpc)) {
|
|
36
|
-
policies.set(rpc._tag, rpcSensitiveKeys(rpc, extra));
|
|
25
|
+
policies.set(rpc._tag, yield* rpcSensitiveKeys(rpc, extra));
|
|
37
26
|
}
|
|
38
27
|
}
|
|
39
28
|
function sensitive(key) {
|
|
@@ -43,5 +32,5 @@ export function schemaSensitiveKeys(group, extra = []) {
|
|
|
43
32
|
forRpc: (tag) => (tag === undefined ? sensitive : (policies.get(tag) ?? sensitive)),
|
|
44
33
|
maskFailure: (tag, reason) => [...policies].some(([method, policy]) => (tag === undefined || tag === method) && (reason === "Fail" ? policy.fail : policy.die)),
|
|
45
34
|
});
|
|
46
|
-
}
|
|
35
|
+
});
|
|
47
36
|
//# sourceMappingURL=schemaSensitiveKeys.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemaSensitiveKeys.js","sourceRoot":"","sources":["../src/schemaSensitiveKeys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schemaSensitiveKeys.js","sourceRoot":"","sources":["../src/schemaSensitiveKeys.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.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Typed expected failures and secret-safe failure reporting for Effect applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"README.md"
|
|
46
46
|
],
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@shivaedev/quality": "0.
|
|
48
|
+
"@shivaedev/quality": "0.52.0",
|
|
49
49
|
"@shivaedev/test": "0.12.0",
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
51
|
"effect": "4.0.6",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
|
+
import { Rpc, type RpcGroup, type RpcSchema } from "effect/rpc";
|
|
3
|
+
|
|
4
|
+
type Result = Effect.Effect<unknown, unknown, unknown> | Stream.Stream<unknown, unknown, unknown>;
|
|
5
|
+
type Handlers<R extends Rpc.Any> = {
|
|
6
|
+
readonly [TRpc in R as TRpc["_tag"]]: (
|
|
7
|
+
...args: Parameters<Rpc.ToHandlerFn<TRpc>>
|
|
8
|
+
) => Rpc.WrapperOr<
|
|
9
|
+
TRpc extends { readonly successSchema: RpcSchema.Stream<infer TSuccess, infer _E> }
|
|
10
|
+
? Stream.Stream<TSuccess["Type"], unknown, unknown>
|
|
11
|
+
: Effect.Effect<Rpc.Success<TRpc>, unknown, unknown>
|
|
12
|
+
>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Bounded<T, E> =
|
|
16
|
+
T extends Effect.Effect<infer A, infer _E, infer R>
|
|
17
|
+
? Effect.Effect<A, E, R>
|
|
18
|
+
: T extends Stream.Stream<infer A, infer _E, infer R>
|
|
19
|
+
? Stream.Stream<A, E, R>
|
|
20
|
+
: T extends Rpc.Wrapper<infer TValue>
|
|
21
|
+
? Rpc.Wrapper<Bounded<TValue, E>>
|
|
22
|
+
: never;
|
|
23
|
+
|
|
24
|
+
type BoundedHandlers<THandlers, E> = {
|
|
25
|
+
readonly [TKey in keyof THandlers]: THandlers[TKey] extends (...args: infer TArgs) => infer TValue ? (...args: TArgs) => Bounded<TValue, E> : never;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function rpcBoundaryHandlers<TRpcs extends Rpc.Any, E, THandlers extends Handlers<TRpcs>>(
|
|
29
|
+
group: RpcGroup.RpcGroup<TRpcs>,
|
|
30
|
+
boundary: <A, TError, R>(effect: Effect.Effect<A, TError, R>) => Effect.Effect<A, E, R>,
|
|
31
|
+
handlers: THandlers,
|
|
32
|
+
): BoundedHandlers<THandlers, E>;
|
|
33
|
+
export function rpcBoundaryHandlers(
|
|
34
|
+
_group: RpcGroup.RpcGroup<Rpc.Any>,
|
|
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>>>,
|
|
37
|
+
) {
|
|
38
|
+
return Object.fromEntries(
|
|
39
|
+
Object.entries<(...args: never[]) => Rpc.WrapperOr<Result>>(handlers).map(([tag, handler]) => [
|
|
40
|
+
tag,
|
|
41
|
+
(...args: never[]) =>
|
|
42
|
+
Rpc.wrapMap(handler(...args), (result) =>
|
|
43
|
+
Effect.isEffect(result) ? boundary(result) : Stream.catchCause(result, (cause) => Stream.fromEffect(boundary(Effect.failCause(cause)))),
|
|
44
|
+
),
|
|
45
|
+
]),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Effect, Schema, type SchemaAST } from "effect";
|
|
2
|
+
import { children, collect, collectEncodedKeys, collectFieldKeys, marked, maskedValue } from "#schemaSensitiveAst.ts";
|
|
3
|
+
import { isSensitiveKey } from "#sensitive.ts";
|
|
4
|
+
|
|
5
|
+
export function schemaFieldKeys(ast: SchemaAST.AST, extra: readonly string[]) {
|
|
6
|
+
const keys = new Set(extra);
|
|
7
|
+
const nodes = new Set<SchemaAST.AST>();
|
|
8
|
+
collect(ast, nodes);
|
|
9
|
+
const properties = [...nodes].flatMap((node) => (node._tag === "Objects" ? node.propertySignatures : []));
|
|
10
|
+
collectFieldKeys(properties, keys, marked);
|
|
11
|
+
let previousSize: number;
|
|
12
|
+
do {
|
|
13
|
+
previousSize = keys.size;
|
|
14
|
+
collectFieldKeys(properties, keys, (node) => maskedValue(node, keys));
|
|
15
|
+
for (const node of nodes) {
|
|
16
|
+
collectEncodedKeys(node, keys);
|
|
17
|
+
}
|
|
18
|
+
} while (keys.size !== previousSize);
|
|
19
|
+
return keys;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Field {
|
|
23
|
+
readonly key: string;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly sensitive: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fieldsOf(ast: SchemaAST.AST, schema: string, extra: readonly string[], candidates: ReadonlySet<string>): Field[] {
|
|
29
|
+
const fields: Field[] = [];
|
|
30
|
+
function visitIndex(index: SchemaAST.IndexSignature, path: string, hidden: boolean, seen: ReadonlySet<SchemaAST.AST>) {
|
|
31
|
+
const accepts = Schema.is(Schema.make(index.parameter));
|
|
32
|
+
for (const key of candidates) {
|
|
33
|
+
if (accepts(key)) {
|
|
34
|
+
fields.push({ key, path: `${path}[${key}]`, sensitive: hidden || extra.includes(key) || isSensitiveKey(key) });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
visit(index.type, `${path}[value]`, hidden, seen);
|
|
38
|
+
}
|
|
39
|
+
function visitObject(node: SchemaAST.Objects, path: string, hidden: boolean, seen: ReadonlySet<SchemaAST.AST>) {
|
|
40
|
+
for (const property of node.propertySignatures) {
|
|
41
|
+
if (typeof property.name !== "string") {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const sensitive =
|
|
45
|
+
hidden || extra.includes(property.name) || isSensitiveKey(property.name) || maskedValue(property.type, schemaFieldKeys(property.type, []));
|
|
46
|
+
const fieldPath = `${path}.${property.name}`;
|
|
47
|
+
fields.push({ key: property.name, path: fieldPath, sensitive });
|
|
48
|
+
visit(property.type, fieldPath, sensitive, seen);
|
|
49
|
+
}
|
|
50
|
+
for (const index of node.indexSignatures) {
|
|
51
|
+
visitIndex(index, path, hidden, seen);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function visit(node: SchemaAST.AST, path: string, hidden: boolean, ancestors: ReadonlySet<SchemaAST.AST>) {
|
|
55
|
+
if (ancestors.has(node)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const seen = new Set(ancestors).add(node);
|
|
59
|
+
const masked = hidden || marked(node);
|
|
60
|
+
if (node._tag === "Objects") {
|
|
61
|
+
visitObject(node, path, masked, seen);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const [index, child] of children(node).entries()) {
|
|
65
|
+
visit(child, `${path}[${index}]`, masked, seen);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
visit(ast, schema, false, new Set());
|
|
69
|
+
for (const key of schemaFieldKeys(ast, [])) {
|
|
70
|
+
if (!fields.some((field) => field.key === key && field.sensitive)) {
|
|
71
|
+
fields.push({ key, path: `${schema}.encoded.${key}`, sensitive: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return fields;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class SensitiveFieldConflict extends Schema.TaggedError<SensitiveFieldConflict>()("SensitiveFieldConflict", {
|
|
78
|
+
field: Schema.String,
|
|
79
|
+
message: Schema.String,
|
|
80
|
+
publicSchema: Schema.String,
|
|
81
|
+
sensitiveSchema: Schema.String,
|
|
82
|
+
}) {}
|
|
83
|
+
|
|
84
|
+
export const validateFieldScope = Effect.fn("Errors.validateFieldScope")(function* (
|
|
85
|
+
schemas: readonly { readonly ast: SchemaAST.AST; readonly name: string }[],
|
|
86
|
+
extra: readonly string[],
|
|
87
|
+
) {
|
|
88
|
+
const candidates = new Set(schemas.flatMap(({ ast }) => [...schemaFieldKeys(ast, [])]));
|
|
89
|
+
const fields = schemas.flatMap(({ ast, name }) => fieldsOf(ast, name, extra, candidates));
|
|
90
|
+
// A masked container protects its children without making each child name sensitive.
|
|
91
|
+
for (const hidden of fields.filter(
|
|
92
|
+
(field) => candidates.has(field.key) && field.sensitive && !extra.includes(field.key) && !isSensitiveKey(field.key),
|
|
93
|
+
)) {
|
|
94
|
+
const publicField = fields.find((field) => field.key === hidden.key && !field.sensitive);
|
|
95
|
+
if (publicField !== undefined) {
|
|
96
|
+
return yield* new SensitiveFieldConflict({
|
|
97
|
+
field: hidden.key,
|
|
98
|
+
message: `Sensitive field "${hidden.key}" in schema ${hidden.path} conflicts with public field in schema ${publicField.path}; RPC diagnostics cannot distinguish these fields. Use distinct field names or mark both fields sensitive.`,
|
|
99
|
+
publicSchema: publicField.path,
|
|
100
|
+
sensitiveSchema: hidden.path,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Predicate, type SchemaAST } from "effect";
|
|
2
2
|
import { isSensitiveKey } from "#sensitive.ts";
|
|
3
3
|
|
|
4
|
-
function children(ast: SchemaAST.AST): readonly SchemaAST.AST[] {
|
|
4
|
+
export function children(ast: SchemaAST.AST): readonly SchemaAST.AST[] {
|
|
5
5
|
switch (ast._tag) {
|
|
6
6
|
case "Objects":
|
|
7
7
|
return [...ast.propertySignatures.map((property) => property.type), ...ast.indexSignatures.flatMap((index) => [index.parameter, index.type])];
|
|
@@ -42,10 +42,23 @@ function containsSensitive(ast: SchemaAST.AST, keys: ReadonlySet<string>): boole
|
|
|
42
42
|
|| propertiesOf(ast).some((property) => typeof property.name === "string" && (keys.has(property.name) || isSensitiveKey(property.name)))
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
|
+
function preservesFields(source: SchemaAST.AST, target: SchemaAST.AST): boolean {
|
|
46
|
+
return (
|
|
47
|
+
source._tag === "Declaration"
|
|
48
|
+
&& Predicate.hasProperty(source.annotations?.representation, "id")
|
|
49
|
+
&& source.annotations.representation.id === "effect/schema/Option"
|
|
50
|
+
&& target._tag === "Union"
|
|
51
|
+
&& target.types.some((type) => type._tag === "Null")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
45
54
|
export function collectEncodedKeys(ast: SchemaAST.AST, keys: Set<string>) {
|
|
46
55
|
let source = ast;
|
|
47
56
|
let sensitive = false;
|
|
48
57
|
for (const link of ast.encoding ?? []) {
|
|
58
|
+
if (preservesFields(source, link.to)) {
|
|
59
|
+
source = link.to;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
49
62
|
sensitive ||= containsSensitive(source, keys);
|
|
50
63
|
if (sensitive) {
|
|
51
64
|
for (const property of propertiesOf(link.to)) {
|
|
@@ -74,6 +87,10 @@ export function maskedValue(ast: SchemaAST.AST, keys: ReadonlySet<string>, seen
|
|
|
74
87
|
let source = ast;
|
|
75
88
|
let sensitive = false;
|
|
76
89
|
for (const link of ast.encoding ?? []) {
|
|
90
|
+
if (preservesFields(source, link.to)) {
|
|
91
|
+
source = link.to;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
77
94
|
sensitive ||= containsSensitive(source, keys);
|
|
78
95
|
if (sensitive && !fixedOutput(link.to)) {
|
|
79
96
|
return true;
|
|
@@ -1,41 +1,29 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Effect } from "effect";
|
|
2
2
|
import { Rpc, type RpcGroup } from "effect/rpc";
|
|
3
|
-
import {
|
|
3
|
+
import { schemaFieldKeys, validateFieldScope } from "#schemaFieldScope.ts";
|
|
4
|
+
import { maskedValue } from "#schemaSensitiveAst.ts";
|
|
4
5
|
import { isSensitiveKey } from "#sensitive.ts";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
rpc.
|
|
11
|
-
rpc.
|
|
12
|
-
rpc.
|
|
13
|
-
rpc.defectSchema,
|
|
14
|
-
...[...rpc.middlewares].map((m) => m.error),
|
|
7
|
+
const rpcSensitiveKeys = Effect.fnUntraced(function* (rpc: Rpc.AnyWithProps, extra: readonly string[]) {
|
|
8
|
+
const schemas = [
|
|
9
|
+
{ ast: rpc.payloadSchema.ast, name: `${rpc._tag}.payload` },
|
|
10
|
+
{ ast: rpc.successSchema.ast, name: `${rpc._tag}.success` },
|
|
11
|
+
{ ast: rpc.errorSchema.ast, name: `${rpc._tag}.error` },
|
|
12
|
+
{ ast: rpc.defectSchema.ast, name: `${rpc._tag}.defect` },
|
|
13
|
+
...[...rpc.middlewares].map((middleware, index) => ({ ast: middleware.error.ast, name: `${rpc._tag}.middleware[${index}].error` })),
|
|
15
14
|
];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
const properties = [...nodes].flatMap((ast) => (ast._tag === "Objects" ? ast.propertySignatures : []));
|
|
20
|
-
collectFieldKeys(properties, keys, marked);
|
|
21
|
-
let previousSize: number;
|
|
22
|
-
do {
|
|
23
|
-
previousSize = keys.size;
|
|
24
|
-
collectFieldKeys(properties, keys, (ast) => maskedValue(ast, keys));
|
|
25
|
-
for (const ast of nodes) {
|
|
26
|
-
collectEncodedKeys(ast, keys);
|
|
27
|
-
}
|
|
28
|
-
} while (keys.size !== previousSize);
|
|
15
|
+
yield* validateFieldScope(schemas, extra);
|
|
16
|
+
const keys = new Set(schemas.flatMap(({ ast }) => [...schemaFieldKeys(ast, extra)]));
|
|
29
17
|
return Object.assign((key: string) => keys.has(key) || isSensitiveKey(key), {
|
|
30
18
|
die: maskedValue(rpc.defectSchema.ast, keys),
|
|
31
19
|
fail: [rpc.errorSchema, ...[...rpc.middlewares].map((m) => m.error)].some((schema) => maskedValue(schema.ast, keys)),
|
|
32
20
|
});
|
|
33
|
-
}
|
|
34
|
-
export function
|
|
35
|
-
const policies = new Map<string, ReturnType<typeof rpcSensitiveKeys
|
|
21
|
+
});
|
|
22
|
+
export const schemaSensitiveKeys = Effect.fnUntraced(function* <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, extra: readonly string[] = []) {
|
|
23
|
+
const policies = new Map<string, Effect.Success<ReturnType<typeof rpcSensitiveKeys>>>();
|
|
36
24
|
for (const rpc of group.requests.values()) {
|
|
37
25
|
if (Rpc.isRpc(rpc)) {
|
|
38
|
-
policies.set(rpc._tag, rpcSensitiveKeys(rpc, extra));
|
|
26
|
+
policies.set(rpc._tag, yield* rpcSensitiveKeys(rpc, extra));
|
|
39
27
|
}
|
|
40
28
|
}
|
|
41
29
|
function sensitive(key: string) {
|
|
@@ -46,4 +34,4 @@ export function schemaSensitiveKeys<R extends Rpc.Any>(group: RpcGroup.RpcGroup<
|
|
|
46
34
|
maskFailure: (tag: string | undefined, reason: "Fail" | "Die") =>
|
|
47
35
|
[...policies].some(([method, policy]) => (tag === undefined || tag === method) && (reason === "Fail" ? policy.fail : policy.die)),
|
|
48
36
|
});
|
|
49
|
-
}
|
|
37
|
+
});
|