@shivaedev/errors 0.2.0 → 0.3.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 +11 -0
- package/README.md +14 -0
- 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/package.json +2 -2
- package/src/rpcBoundaryHandlers.ts +47 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
- Contract changes streams declare `WebSocketRequired` so HTTP clients can decode the protocol error.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Hosted `$changes` subscriptions over HTTP fail with a message saying they need the WebSocket protocol.
|
|
13
|
+
|
|
3
14
|
## 0.2.0 - 2026-10-31
|
|
4
15
|
|
|
5
16
|
### 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>`.
|
|
@@ -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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/errors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.50.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
|
+
}
|