@shivaedev/errors 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +71 -1
- package/dist/boundary.d.ts +9 -0
- package/dist/boundary.d.ts.map +1 -0
- package/dist/boundary.js +18 -0
- package/dist/boundary.js.map +1 -0
- package/dist/redact.d.ts +4 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +64 -0
- package/dist/redact.js.map +1 -0
- package/dist/redactCause.d.ts +7 -0
- package/dist/redactCause.d.ts.map +1 -0
- package/dist/redactCause.js +92 -0
- package/dist/redactCause.js.map +1 -0
- package/dist/rejectedField.d.ts +7 -0
- package/dist/rejectedField.d.ts.map +1 -0
- package/dist/rejectedField.js +7 -0
- package/dist/rejectedField.js.map +1 -0
- package/dist/sensitive.d.ts +6 -0
- package/dist/sensitive.d.ts.map +1 -0
- package/dist/sensitive.js +78 -0
- package/dist/sensitive.js.map +1 -0
- package/dist/taxonomy.d.ts +55 -0
- package/dist/taxonomy.d.ts.map +1 -0
- package/dist/taxonomy.js +24 -0
- package/dist/taxonomy.js.map +1 -0
- package/package.json +64 -4
- package/src/boundary.ts +26 -0
- package/src/redact.ts +64 -0
- package/src/redactCause.ts +104 -0
- package/src/rejectedField.ts +10 -0
- package/src/sensitive.ts +79 -0
- package/src/taxonomy.ts +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShivaeDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
1
|
# @shivaedev/errors
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared expected failures and secret-safe failure reporting for Effect applications. Every layer of an application declares, returns and reads the same tagged error classes, and every boundary that reports a failure masks secrets the same way.
|
|
4
|
+
|
|
5
|
+
## Why you want this
|
|
6
|
+
|
|
7
|
+
An application that lets each feature invent its own "not found" or "name is taken" ends up with a dozen shapes for the same answer, and every client screen has to learn each one. A shared vocabulary means an RPC declares the failures it can return, the handler fails with one of them, and the client reads the same class back with its field and message intact.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { rejectedField } from "@shivaedev/errors/rejectedField.ts";
|
|
11
|
+
import { Conflict } from "@shivaedev/errors/taxonomy.ts";
|
|
12
|
+
|
|
13
|
+
const field = rejectedField(new Conflict({ field: "name", message: "Name is taken" }));
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`field` is `Option.some({ field: "name", message: "Name is taken" })`, ready to show beside the form input it names. The same modules run in the browser and on the server.
|
|
17
|
+
|
|
18
|
+
## Using it
|
|
19
|
+
|
|
20
|
+
### How to think about it
|
|
21
|
+
|
|
22
|
+
There are two kinds of failure. Expected failures are part of an operation's contract: the caller can act on them, so they carry a public message and travel to the client. Unexpected failures are defects and causes the caller cannot act on; they go to logs, traces and error reporters, and anything secret in them must be masked first. This package holds the vocabulary for the first kind and the masking policy for the second.
|
|
23
|
+
|
|
24
|
+
### Expected failures
|
|
25
|
+
|
|
26
|
+
`taxonomy.ts` exports `NotFound`, `Unauthorized`, `Forbidden`, `BadRequest`, `Conflict`, `PreconditionFailed`, `TooManyRequests`, `AuthUnavailable`, `InternalServerError` and `ServiceUnavailable`. Each class is a Schema tagged error with a string `message`; `BadRequest` and `Conflict` also accept an optional string `field`. Declare only the failures an operation can return:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Schema } from "effect";
|
|
30
|
+
import { Rpc } from "effect/rpc";
|
|
31
|
+
import { BadRequest, Conflict } from "@shivaedev/errors/taxonomy.ts";
|
|
32
|
+
|
|
33
|
+
export const Rename = Rpc.make("Rename", {
|
|
34
|
+
error: Schema.Union([BadRequest, Conflict]),
|
|
35
|
+
payload: { name: Schema.String },
|
|
36
|
+
success: Schema.String,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A handler fails with `new Conflict({ field: "name", message: "Name is taken" })`, and native RPC JSON decodes the rejection back to the error instance with its field. `rejectedField(error)` reads either that instance or its tagged JSON. Any value with a string `_tag`, `field` and `message` qualifies, including an application's own tagged error; an untagged object, a missing field or a non-string field yields `Option.none()`.
|
|
41
|
+
|
|
42
|
+
### Mapping unexpected failures at the handler edge
|
|
43
|
+
|
|
44
|
+
At the handler edge, `rpcBoundary(schema, options)` keeps declared rejections and maps other failures and defects into one declared fallback. It leaves successful values and interruption intact:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { Schema } from "effect";
|
|
48
|
+
import { Conflict, InternalServerError } from "@shivaedev/errors/taxonomy.ts";
|
|
49
|
+
import { rpcBoundary } from "@shivaedev/errors/boundary.ts";
|
|
50
|
+
|
|
51
|
+
const Rejection = Schema.Union([Conflict, InternalServerError]);
|
|
52
|
+
const expose = rpcBoundary(Rejection, {
|
|
53
|
+
unexpected: (message) => new InternalServerError({ message }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const save = storageSave.pipe(expose);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`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. 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.
|
|
60
|
+
|
|
61
|
+
### Masking secrets in failures
|
|
62
|
+
|
|
63
|
+
`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`. A `SensitiveKey` is any `(key: string) => boolean`, so an application can widen the policy with its own names. `redactText(text, sensitive, maxLength?)` masks credential-shaped text and `key=value` assignments inside a string, and cuts it at 2,048 characters by default; passing `Infinity` keeps complete public text. Masked values become `REDACTED`, the string `<redacted>`.
|
|
64
|
+
|
|
65
|
+
`redact.ts` applies that policy to structured values. `redact(value, sensitive?)` masks values under sensitive keys and credential text inside strings, summarises large collections and binary data, and stops cyclic payloads at a depth limit; use it for diagnostics. `redactWire(value, sensitive?)` applies the same masking without summaries or truncation; use it for failure data sent to a client.
|
|
66
|
+
|
|
67
|
+
`redactCause.ts` applies it to Effect causes. `redactCause(cause, sensitive?)` and `redactDiagnosticCause(cause, sensitive?)` rebuild a cause with masked failures and defects, `redactDefect(defect, sensitive?)` masks one defect, and `redactingErrorReporter(reporter, sensitive?)` wraps an Effect error reporter so it only ever receives masked causes.
|
|
68
|
+
|
|
69
|
+
The policy matches names, not values: it does not scan prose for personal data, so keep secrets out of free text and span names.
|
|
70
|
+
|
|
71
|
+
### Install and limits
|
|
72
|
+
|
|
73
|
+
Install `@shivaedev/errors` with `effect` as its peer. The package has no root entry; import the module that defines the name. Every module is browser-safe and depends only on `effect`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
export interface RpcBoundaryOptions<E> {
|
|
3
|
+
readonly message?: string;
|
|
4
|
+
readonly unexpected: (message: string) => E;
|
|
5
|
+
}
|
|
6
|
+
export declare function rpcBoundary<TSchema extends Schema.ConstraintDecoder<{
|
|
7
|
+
readonly message: string;
|
|
8
|
+
}>>(schema: TSchema, options: RpcBoundaryOptions<TSchema["Type"]>): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, TSchema["Type"], R>;
|
|
9
|
+
//# sourceMappingURL=boundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary.d.ts","sourceRoot":"","sources":["../src/boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE/C,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,CAAC,CAAC;CAC5C;AACD,wBAAgB,WAAW,CAC1B,OAAO,SAAS,MAAM,CAAC,iBAAiB,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB,CAAC,EACD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAEtD,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAapF"}
|
package/dist/boundary.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Cause, Effect, Schema } from "effect";
|
|
2
|
+
import { isSensitiveKey, redactText } from "#sensitive.ts";
|
|
3
|
+
export function rpcBoundary(schema, options) {
|
|
4
|
+
const declared = Schema.is(schema);
|
|
5
|
+
return (self) => Effect.catchCause(self, (cause) => {
|
|
6
|
+
if (Cause.hasInterruptsOnly(cause)) {
|
|
7
|
+
return Effect.failCause(Cause.fromReasons(cause.reasons.filter(Cause.isInterruptReason)));
|
|
8
|
+
}
|
|
9
|
+
const reason = cause.reasons[0];
|
|
10
|
+
if (cause.reasons.length === 1 && reason && Cause.isFailReason(reason) && declared(reason.error)) {
|
|
11
|
+
return Effect.fail(reason.error);
|
|
12
|
+
}
|
|
13
|
+
const rejection = options.unexpected(redactText(options.message ?? "Internal server error", isSensitiveKey));
|
|
14
|
+
Object.defineProperty(rejection, "cause", { configurable: true, enumerable: false, value: cause });
|
|
15
|
+
return Effect.failCause(Cause.fromReasons([...Cause.fail(rejection).reasons, ...cause.reasons.filter(Cause.isInterruptReason)]));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary.js","sourceRoot":"","sources":["../src/boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAK3D,MAAM,UAAU,WAAW,CAIzB,MAAe,EAAE,OAA4C;IAC9D,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;IACnC,OAAO,CAAU,IAA4B,EAAwC,EAAE,CACtF,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE;QACjC,IAAI,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;QAC3F,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAClG,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC;QACD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,IAAI,uBAAuB,EAAE,cAAc,CAAC,CAAC,CAAC;QAC7G,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACnG,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;IAClI,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AACA,OAAO,EAA0D,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAyD1G,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,GAAE,YAA6B,GAAG,OAAO,CAExF;AACD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,GAAE,YAA6B,GAAG,OAAO,CAE5F"}
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Predicate, Redacted } from "effect";
|
|
2
|
+
import { isSensitiveKey, REDACTED, redactErrorStack, redactText } from "#sensitive.ts";
|
|
3
|
+
const MAX_DEPTH = 8;
|
|
4
|
+
const MAX_ENTRIES = 50;
|
|
5
|
+
function binary(value) {
|
|
6
|
+
if (ArrayBuffer.isView(value)) {
|
|
7
|
+
return `<${value.constructor.name} ${value.byteLength} bytes>`;
|
|
8
|
+
}
|
|
9
|
+
if (value instanceof ArrayBuffer) {
|
|
10
|
+
return `<ArrayBuffer ${value.byteLength} bytes>`;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
function entriesOf(value, sensitive, bounded) {
|
|
15
|
+
const entries = Object.entries(value);
|
|
16
|
+
if (!Predicate.isError(value)) {
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
19
|
+
const shape = [
|
|
20
|
+
["name", value.name],
|
|
21
|
+
["message", value.message],
|
|
22
|
+
["stack", bounded ? redactErrorStack(value, sensitive) : value.stack],
|
|
23
|
+
];
|
|
24
|
+
const extra = entries.filter(([key]) => !["name", "message", "stack", "cause"].includes(key));
|
|
25
|
+
return value.cause === undefined ? [...shape, ...extra] : [...shape, ...extra, ["cause", value.cause]];
|
|
26
|
+
}
|
|
27
|
+
function collection(value, sensitive, depth, bounded) {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
const items = value.slice(0, bounded ? MAX_ENTRIES : value.length).map((item) => walk(item, sensitive, depth + 1, bounded));
|
|
30
|
+
return bounded && value.length > MAX_ENTRIES ? [...items, `<${value.length - MAX_ENTRIES} more items>`] : items;
|
|
31
|
+
}
|
|
32
|
+
const entries = entriesOf(value, sensitive, bounded);
|
|
33
|
+
const kept = entries.slice(0, bounded ? MAX_ENTRIES : entries.length).map(([key, nested]) => {
|
|
34
|
+
const stackHeader = key === "stack" && typeof nested === "string" && (sensitive("name") || sensitive("message"));
|
|
35
|
+
return [key, sensitive(key) || stackHeader ? REDACTED : walk(nested, sensitive, depth + 1, bounded)];
|
|
36
|
+
});
|
|
37
|
+
return Object.fromEntries(bounded && entries.length > MAX_ENTRIES ? [...kept, ["<truncated>", `${entries.length - MAX_ENTRIES} more keys`]] : kept);
|
|
38
|
+
}
|
|
39
|
+
function walk(value, sensitive, depth, bounded) {
|
|
40
|
+
if (Redacted.isRedacted(value)) {
|
|
41
|
+
return REDACTED;
|
|
42
|
+
}
|
|
43
|
+
if (Predicate.isString(value)) {
|
|
44
|
+
return bounded ? redactText(value, sensitive) : redactText(value, sensitive, Number.POSITIVE_INFINITY);
|
|
45
|
+
}
|
|
46
|
+
if (!Predicate.isObjectOrArray(value)) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
const summary = binary(value);
|
|
50
|
+
if (summary !== undefined) {
|
|
51
|
+
return bounded ? summary : value;
|
|
52
|
+
}
|
|
53
|
+
if (bounded && depth >= MAX_DEPTH) {
|
|
54
|
+
return "<truncated>";
|
|
55
|
+
}
|
|
56
|
+
return collection(value, sensitive, depth, bounded);
|
|
57
|
+
}
|
|
58
|
+
export function redact(value, sensitive = isSensitiveKey) {
|
|
59
|
+
return walk(value, sensitive, 0, true);
|
|
60
|
+
}
|
|
61
|
+
export function redactWire(value, sensitive = isSensitiveKey) {
|
|
62
|
+
return walk(value, sensitive, 0, false);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Cause, ErrorReporter } from "effect";
|
|
2
|
+
import { type SensitiveKey } from "#sensitive.ts";
|
|
3
|
+
export declare function redactDefect(defect: unknown, sensitive?: SensitiveKey): unknown;
|
|
4
|
+
export declare function redactCause<E>(cause: Cause.Cause<E>, sensitive?: SensitiveKey): Cause.Cause<E>;
|
|
5
|
+
export declare function redactDiagnosticCause(cause: Cause.Cause<unknown>, sensitive?: SensitiveKey): Cause.Cause<unknown>;
|
|
6
|
+
export declare function redactingErrorReporter(reporter: ErrorReporter.ErrorReporter, sensitive?: SensitiveKey): ErrorReporter.ErrorReporter;
|
|
7
|
+
//# sourceMappingURL=redactCause.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redactCause.d.ts","sourceRoot":"","sources":["../src/redactCause.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAW,aAAa,EAA8B,MAAM,QAAQ,CAAC;AAEnF,OAAO,EAA0D,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AA4B1G,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,GAAE,YAA6B,GAAG,OAAO,CAE/F;AACD,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,GAAE,YAA6B,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAQ9G;AAgDD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,SAAS,GAAE,YAA6B,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAQjI;AACD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,EAAE,SAAS,GAAE,YAA6B,GAAG,aAAa,CAAC,aAAa,CAKnJ"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Cause, Context, ErrorReporter, Predicate } from "effect";
|
|
2
|
+
import { redact } from "#redact.ts";
|
|
3
|
+
import { isSensitiveKey, REDACTED, redactErrorStack, redactText } from "#sensitive.ts";
|
|
4
|
+
function reporterHints(error, sensitive) {
|
|
5
|
+
return {
|
|
6
|
+
...(ErrorReporter.isIgnored(error) ? { [ErrorReporter.ignore]: true } : {}),
|
|
7
|
+
...(ErrorReporter.severity in error ? { [ErrorReporter.severity]: ErrorReporter.getSeverity(error) } : {}),
|
|
8
|
+
...(ErrorReporter.attributes in error ? { [ErrorReporter.attributes]: redact(ErrorReporter.getAttributes(error), sensitive) } : {}),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function errorText(key, value, sensitive) {
|
|
12
|
+
return sensitive(key) ? REDACTED : redactText(value, sensitive);
|
|
13
|
+
}
|
|
14
|
+
function redactError(error, sensitive) {
|
|
15
|
+
const cause = sensitive("cause") ? REDACTED : redact(error.cause, sensitive);
|
|
16
|
+
const options = error.cause === undefined ? undefined : { cause };
|
|
17
|
+
const copy = Object.assign(new Error(errorText("message", error.message, sensitive), options), redact(Object.fromEntries(Object.entries(error)), sensitive), reporterHints(error, sensitive));
|
|
18
|
+
Object.defineProperty(copy, "name", { configurable: true, value: errorText("name", error.name, sensitive), writable: true });
|
|
19
|
+
Object.defineProperty(copy, "stack", {
|
|
20
|
+
configurable: true,
|
|
21
|
+
value: redactErrorStack(error, sensitive),
|
|
22
|
+
writable: true,
|
|
23
|
+
});
|
|
24
|
+
return copy;
|
|
25
|
+
}
|
|
26
|
+
export function redactDefect(defect, sensitive = isSensitiveKey) {
|
|
27
|
+
return Predicate.isError(defect) ? redactError(defect, sensitive) : redact(defect, sensitive);
|
|
28
|
+
}
|
|
29
|
+
export function redactCause(cause, sensitive = isSensitiveKey) {
|
|
30
|
+
return Cause.fromReasons(cause.reasons.flatMap((reason) => Cause.isDieReason(reason)
|
|
31
|
+
? Cause.annotate(Cause.die(redactDefect(reason.defect, sensitive)), Context.makeUnsafe(reason.annotations)).reasons
|
|
32
|
+
: [reason]));
|
|
33
|
+
}
|
|
34
|
+
function diagnosticReason(reason, sensitive) {
|
|
35
|
+
switch (reason._tag) {
|
|
36
|
+
case "Fail":
|
|
37
|
+
return Cause.fail(redactDefect(reason.error, sensitive));
|
|
38
|
+
case "Die":
|
|
39
|
+
return Cause.die(redactDefect(reason.defect, sensitive));
|
|
40
|
+
case "Interrupt":
|
|
41
|
+
return Cause.interrupt(reason.fiberId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function stackFrame(frame, sensitive, remaining = 8) {
|
|
45
|
+
if (frame === undefined || remaining === 0) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
name: sensitive("name") ? REDACTED : redactText(frame.name, sensitive),
|
|
50
|
+
parent: stackFrame(frame.parent, sensitive, remaining - 1),
|
|
51
|
+
stack: () => {
|
|
52
|
+
const value = frame.stack();
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return sensitive("name") || sensitive("message") ? REDACTED : redactText(value, sensitive);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function diagnosticAnnotations(reason, sensitive) {
|
|
61
|
+
const nativeStacks = [
|
|
62
|
+
Cause.StackTrace,
|
|
63
|
+
Cause.InterruptorStackTrace,
|
|
64
|
+
];
|
|
65
|
+
const annotations = new Map([...reason.annotations]
|
|
66
|
+
.filter(([key]) => !nativeStacks.some((service) => service.key === key))
|
|
67
|
+
.map(([key, value]) => [key, sensitive(key) ? REDACTED : redact(value, sensitive)]));
|
|
68
|
+
if (!sensitive("stack")) {
|
|
69
|
+
const context = Cause.reasonAnnotations(reason);
|
|
70
|
+
for (const service of nativeStacks) {
|
|
71
|
+
const frame = Context.getOrUndefined(context, service);
|
|
72
|
+
if (frame !== undefined && !sensitive(service.key)) {
|
|
73
|
+
annotations.set(service.key, stackFrame(frame, sensitive));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return annotations;
|
|
78
|
+
}
|
|
79
|
+
export function redactDiagnosticCause(cause, sensitive = isSensitiveKey) {
|
|
80
|
+
return Cause.fromReasons(cause.reasons.flatMap((reason) => {
|
|
81
|
+
const redacted = diagnosticReason(reason, sensitive);
|
|
82
|
+
const annotations = diagnosticAnnotations(reason, sensitive);
|
|
83
|
+
return Cause.annotate(redacted, Context.makeUnsafe(annotations), { overwrite: true }).reasons;
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
export function redactingErrorReporter(reporter, sensitive = isSensitiveKey) {
|
|
87
|
+
return {
|
|
88
|
+
[ErrorReporter.TypeId]: ErrorReporter.TypeId,
|
|
89
|
+
report: (options) => reporter.report({ ...options, cause: redactDiagnosticCause(options.cause, sensitive) }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=redactCause.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redactCause.js","sourceRoot":"","sources":["../src/redactCause.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rejectedField.d.ts","sourceRoot":"","sources":["../src/rejectedField.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAE1E"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
const Rejected = Schema.Struct({ _tag: Schema.String, field: Schema.String, message: Schema.String });
|
|
3
|
+
const decode = Schema.decodeUnknownOption(Rejected);
|
|
4
|
+
export function rejectedField(error) {
|
|
5
|
+
return Option.map(decode(error), ({ field, message }) => ({ field, message }));
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=rejectedField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rejectedField.js","sourceRoot":"","sources":["../src/rejectedField.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKxC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AACtG,MAAM,MAAM,GAAG,MAAM,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;AACpD,MAAM,UAAU,aAAa,CAAC,KAAc;IAC3C,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;AAChF,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const REDACTED = "<redacted>";
|
|
2
|
+
export type SensitiveKey = (key: string) => boolean;
|
|
3
|
+
export declare function isSensitiveKey(key: string): boolean;
|
|
4
|
+
export declare function redactText(text: string, sensitive: SensitiveKey, maxLength?: number): string;
|
|
5
|
+
export declare function redactErrorStack(error: Error, sensitive: SensitiveKey): string | undefined;
|
|
6
|
+
//# sourceMappingURL=sensitive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitive.d.ts","sourceRoot":"","sources":["../src/sensitive.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ,eAAe,CAAC;AAwBrC,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;AAQpD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AA8BD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,SAAa,GAAG,MAAM,CAGhG;AACD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAK1F"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const REDACTED = "<redacted>";
|
|
2
|
+
const MAX_STRING = 2048;
|
|
3
|
+
const SENSITIVE_PARTS = [
|
|
4
|
+
"password",
|
|
5
|
+
"passphrase",
|
|
6
|
+
"secret",
|
|
7
|
+
"token",
|
|
8
|
+
"authorization",
|
|
9
|
+
"cookie",
|
|
10
|
+
"apikey",
|
|
11
|
+
"accesskey",
|
|
12
|
+
"credential",
|
|
13
|
+
"privatekey",
|
|
14
|
+
"jwt",
|
|
15
|
+
"bearer",
|
|
16
|
+
"signature",
|
|
17
|
+
"sessionid",
|
|
18
|
+
"cardnumber",
|
|
19
|
+
"accountnumber",
|
|
20
|
+
"csrf",
|
|
21
|
+
"xsrf",
|
|
22
|
+
];
|
|
23
|
+
const SENSITIVE_WORDS = ["otp", "pin", "cvv", "cvc", "ssn", "iban"];
|
|
24
|
+
const SENSITIVE_NAMES = ["session", "pan", "sig"];
|
|
25
|
+
function wordsOf(key) {
|
|
26
|
+
return key
|
|
27
|
+
.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower> $<upper>")
|
|
28
|
+
.replaceAll(/(?<initial>[A-Z])(?<word>[A-Z][a-z])/gu, "$<initial> $<word>")
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.split(/[\s_-]+/u);
|
|
31
|
+
}
|
|
32
|
+
export function isSensitiveKey(key) {
|
|
33
|
+
const normalized = key.toLowerCase().replaceAll(/[-_]/gu, "");
|
|
34
|
+
return (SENSITIVE_NAMES.includes(normalized)
|
|
35
|
+
|| SENSITIVE_PARTS.some((part) => normalized.includes(part))
|
|
36
|
+
|| wordsOf(key).some((word) => SENSITIVE_WORDS.includes(word)));
|
|
37
|
+
}
|
|
38
|
+
const AUTH_SCHEME = /\b(?<scheme>Bearer|Basic)\s+[A-Za-z0-9%._~+/-]+=*/giu;
|
|
39
|
+
const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/gu;
|
|
40
|
+
const PRIVATE_KEY = /-----BEGIN (?<kind>(?:[A-Z0-9]+ )*PRIVATE KEY)-----[\s\S]*?-----END \k<kind>-----/gu;
|
|
41
|
+
const URL_PASSWORD = /(?<authority>\/\/[^:/\s@]+:)[^@/\s]+@/gu;
|
|
42
|
+
function redactAssignments(text, sensitive) {
|
|
43
|
+
const matcher = /(?<key>[A-Za-z][\w.-]*)(?<separator>["']?\s*[=:]\s*["']?)(?<value>[^\s"'&,;]+)/gu;
|
|
44
|
+
let result = "";
|
|
45
|
+
let cursor = 0;
|
|
46
|
+
for (let match = matcher.exec(text); match !== null; match = matcher.exec(text)) {
|
|
47
|
+
const key = match[1] ?? "";
|
|
48
|
+
const separator = match[2] ?? "";
|
|
49
|
+
if (sensitive(key)) {
|
|
50
|
+
result += `${text.slice(cursor, match.index)}${key}${separator}${REDACTED}`;
|
|
51
|
+
cursor = matcher.lastIndex;
|
|
52
|
+
}
|
|
53
|
+
else if (match[3]?.includes("=")) {
|
|
54
|
+
// Revisit embedded parameters without parsing URL and source-location colons as assignments.
|
|
55
|
+
matcher.lastIndex = match.index + key.length + separator.length;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result + text.slice(cursor);
|
|
59
|
+
}
|
|
60
|
+
function scrub(text, sensitive) {
|
|
61
|
+
const credentials = text
|
|
62
|
+
.replaceAll(PRIVATE_KEY, REDACTED)
|
|
63
|
+
.replaceAll(AUTH_SCHEME, `$<scheme> ${REDACTED}`)
|
|
64
|
+
.replaceAll(JWT, REDACTED)
|
|
65
|
+
.replaceAll(URL_PASSWORD, `$<authority>${REDACTED}@`);
|
|
66
|
+
return redactAssignments(credentials, sensitive);
|
|
67
|
+
}
|
|
68
|
+
export function redactText(text, sensitive, maxLength = MAX_STRING) {
|
|
69
|
+
const scrubbed = scrub(text, sensitive);
|
|
70
|
+
return scrubbed.length > maxLength ? `${scrubbed.slice(0, maxLength)}…<${scrubbed.length - maxLength} more chars>` : scrubbed;
|
|
71
|
+
}
|
|
72
|
+
export function redactErrorStack(error, sensitive) {
|
|
73
|
+
if (error.stack === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return ["message", "name", "stack"].some((key) => sensitive(key)) ? REDACTED : redactText(error.stack, sensitive);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=sensitive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitive.js","sourceRoot":"","sources":["../src/sensitive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
declare const NotFound_base: Schema.Class<NotFound, Schema.TaggedStruct<"NotFound", {
|
|
3
|
+
message: Schema.String;
|
|
4
|
+
}>, import("effect/Cause").YieldableError>;
|
|
5
|
+
export declare class NotFound extends NotFound_base {
|
|
6
|
+
}
|
|
7
|
+
declare const Unauthorized_base: Schema.Class<Unauthorized, Schema.TaggedStruct<"Unauthorized", {
|
|
8
|
+
message: Schema.String;
|
|
9
|
+
}>, import("effect/Cause").YieldableError>;
|
|
10
|
+
export declare class Unauthorized extends Unauthorized_base {
|
|
11
|
+
}
|
|
12
|
+
declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
|
|
13
|
+
message: Schema.String;
|
|
14
|
+
}>, import("effect/Cause").YieldableError>;
|
|
15
|
+
export declare class Forbidden extends Forbidden_base {
|
|
16
|
+
}
|
|
17
|
+
declare const BadRequest_base: Schema.Class<BadRequest, Schema.TaggedStruct<"BadRequest", {
|
|
18
|
+
field: Schema.optionalKey<Schema.String>;
|
|
19
|
+
message: Schema.String;
|
|
20
|
+
}>, import("effect/Cause").YieldableError>;
|
|
21
|
+
export declare class BadRequest extends BadRequest_base {
|
|
22
|
+
}
|
|
23
|
+
declare const Conflict_base: Schema.Class<Conflict, Schema.TaggedStruct<"Conflict", {
|
|
24
|
+
field: Schema.optionalKey<Schema.String>;
|
|
25
|
+
message: Schema.String;
|
|
26
|
+
}>, import("effect/Cause").YieldableError>;
|
|
27
|
+
export declare class Conflict extends Conflict_base {
|
|
28
|
+
}
|
|
29
|
+
declare const PreconditionFailed_base: Schema.Class<PreconditionFailed, Schema.TaggedStruct<"PreconditionFailed", {
|
|
30
|
+
message: Schema.String;
|
|
31
|
+
}>, import("effect/Cause").YieldableError>;
|
|
32
|
+
export declare class PreconditionFailed extends PreconditionFailed_base {
|
|
33
|
+
}
|
|
34
|
+
declare const TooManyRequests_base: Schema.Class<TooManyRequests, Schema.TaggedStruct<"TooManyRequests", {
|
|
35
|
+
message: Schema.String;
|
|
36
|
+
}>, import("effect/Cause").YieldableError>;
|
|
37
|
+
export declare class TooManyRequests extends TooManyRequests_base {
|
|
38
|
+
}
|
|
39
|
+
declare const AuthUnavailable_base: Schema.Class<AuthUnavailable, Schema.TaggedStruct<"AuthUnavailable", {
|
|
40
|
+
message: Schema.String;
|
|
41
|
+
}>, import("effect/Cause").YieldableError>;
|
|
42
|
+
export declare class AuthUnavailable extends AuthUnavailable_base {
|
|
43
|
+
}
|
|
44
|
+
declare const InternalServerError_base: Schema.Class<InternalServerError, Schema.TaggedStruct<"InternalServerError", {
|
|
45
|
+
message: Schema.String;
|
|
46
|
+
}>, import("effect/Cause").YieldableError>;
|
|
47
|
+
export declare class InternalServerError extends InternalServerError_base {
|
|
48
|
+
}
|
|
49
|
+
declare const ServiceUnavailable_base: Schema.Class<ServiceUnavailable, Schema.TaggedStruct<"ServiceUnavailable", {
|
|
50
|
+
message: Schema.String;
|
|
51
|
+
}>, import("effect/Cause").YieldableError>;
|
|
52
|
+
export declare class ServiceUnavailable extends ServiceUnavailable_base {
|
|
53
|
+
}
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=taxonomy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taxonomy.d.ts","sourceRoot":"","sources":["../src/taxonomy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;AAIhC,qBAAa,QAAS,SAAQ,aAAqD;CAAG;;;;AACtF,qBAAa,YAAa,SAAQ,iBAA6D;CAAG;;;;AAClG,qBAAa,SAAU,SAAQ,cAAuD;CAAG;;;;;AACzF,qBAAa,UAAW,SAAQ,eAAwD;CAAG;;;;;AAC3F,qBAAa,QAAS,SAAQ,aAAoD;CAAG;;;;AACrF,qBAAa,kBAAmB,SAAQ,uBAAyE;CAAG;;;;AACpH,qBAAa,eAAgB,SAAQ,oBAAmE;CAAG;;;;AAC3G,qBAAa,eAAgB,SAAQ,oBAAmE;CAAG;;;;AAC3G,qBAAa,mBAAoB,SAAQ,wBAA2E;CAAG;;;;AACvH,qBAAa,kBAAmB,SAAQ,uBAAyE;CAAG"}
|
package/dist/taxonomy.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
const described = { message: Schema.String };
|
|
3
|
+
const perField = { field: Schema.optionalKey(Schema.String), message: Schema.String };
|
|
4
|
+
export class NotFound extends Schema.TaggedError()("NotFound", described) {
|
|
5
|
+
}
|
|
6
|
+
export class Unauthorized extends Schema.TaggedError()("Unauthorized", described) {
|
|
7
|
+
}
|
|
8
|
+
export class Forbidden extends Schema.TaggedError()("Forbidden", described) {
|
|
9
|
+
}
|
|
10
|
+
export class BadRequest extends Schema.TaggedError()("BadRequest", perField) {
|
|
11
|
+
}
|
|
12
|
+
export class Conflict extends Schema.TaggedError()("Conflict", perField) {
|
|
13
|
+
}
|
|
14
|
+
export class PreconditionFailed extends Schema.TaggedError()("PreconditionFailed", described) {
|
|
15
|
+
}
|
|
16
|
+
export class TooManyRequests extends Schema.TaggedError()("TooManyRequests", described) {
|
|
17
|
+
}
|
|
18
|
+
export class AuthUnavailable extends Schema.TaggedError()("AuthUnavailable", described) {
|
|
19
|
+
}
|
|
20
|
+
export class InternalServerError extends Schema.TaggedError()("InternalServerError", described) {
|
|
21
|
+
}
|
|
22
|
+
export class ServiceUnavailable extends Schema.TaggedError()("ServiceUnavailable", described) {
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=taxonomy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taxonomy.js","sourceRoot":"","sources":["../src/taxonomy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,SAAS,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;AAC7C,MAAM,QAAQ,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;AACtF,MAAM,OAAO,QAAS,SAAQ,MAAM,CAAC,WAAW,EAAY,CAAC,UAAU,EAAE,SAAS,CAAC;CAAG;AACtF,MAAM,OAAO,YAAa,SAAQ,MAAM,CAAC,WAAW,EAAgB,CAAC,cAAc,EAAE,SAAS,CAAC;CAAG;AAClG,MAAM,OAAO,SAAU,SAAQ,MAAM,CAAC,WAAW,EAAa,CAAC,WAAW,EAAE,SAAS,CAAC;CAAG;AACzF,MAAM,OAAO,UAAW,SAAQ,MAAM,CAAC,WAAW,EAAc,CAAC,YAAY,EAAE,QAAQ,CAAC;CAAG;AAC3F,MAAM,OAAO,QAAS,SAAQ,MAAM,CAAC,WAAW,EAAY,CAAC,UAAU,EAAE,QAAQ,CAAC;CAAG;AACrF,MAAM,OAAO,kBAAmB,SAAQ,MAAM,CAAC,WAAW,EAAsB,CAAC,oBAAoB,EAAE,SAAS,CAAC;CAAG;AACpH,MAAM,OAAO,eAAgB,SAAQ,MAAM,CAAC,WAAW,EAAmB,CAAC,iBAAiB,EAAE,SAAS,CAAC;CAAG;AAC3G,MAAM,OAAO,eAAgB,SAAQ,MAAM,CAAC,WAAW,EAAmB,CAAC,iBAAiB,EAAE,SAAS,CAAC;CAAG;AAC3G,MAAM,OAAO,mBAAoB,SAAQ,MAAM,CAAC,WAAW,EAAuB,CAAC,qBAAqB,EAAE,SAAS,CAAC;CAAG;AACvH,MAAM,OAAO,kBAAmB,SAAQ,MAAM,CAAC,WAAW,EAAsB,CAAC,oBAAoB,EAAE,SAAS,CAAC;CAAG"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/errors",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed expected failures and secret-safe failure reporting for Effect applications",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"effect"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/errors#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/ShivaeDev/platform/issues"
|
|
11
|
+
},
|
|
6
12
|
"repository": {
|
|
7
13
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/ShivaeDev/platform.git"
|
|
14
|
+
"url": "git+https://github.com/ShivaeDev/platform.git",
|
|
15
|
+
"directory": "packages/errors"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"type": "module",
|
|
20
|
+
"imports": {
|
|
21
|
+
"#test/*": "./src/test-support/*",
|
|
22
|
+
"#*.ts": {
|
|
23
|
+
"source": "./src/*.ts",
|
|
24
|
+
"types": "./dist/*.d.ts",
|
|
25
|
+
"default": "./dist/*.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
"./*.ts": {
|
|
30
|
+
"source": "./src/*.ts",
|
|
31
|
+
"types": "./dist/*.d.ts",
|
|
32
|
+
"default": "./dist/*.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"!src/**/*.test.ts",
|
|
40
|
+
"!src/**/*.test.tsx",
|
|
41
|
+
"!src/**/*.spec.ts",
|
|
42
|
+
"!src/**/*.spec.tsx",
|
|
43
|
+
"!src/**/test-support",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@shivaedev/quality": "0.42.0",
|
|
48
|
+
"@shivaedev/test": "0.10.0",
|
|
49
|
+
"@types/node": "24.19.2",
|
|
50
|
+
"effect": "4.0.5",
|
|
51
|
+
"typescript": "7.0.2",
|
|
52
|
+
"vitest": "5.0.5"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"effect": "4.0.5"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=24"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public",
|
|
62
|
+
"provenance": true
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
|
|
66
|
+
"ready": "pnpm typecheck && pnpm test && pnpm build",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"typecheck": "tsc --noEmit"
|
|
9
69
|
}
|
|
10
70
|
}
|
package/src/boundary.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Cause, Effect, Schema } from "effect";
|
|
2
|
+
import { isSensitiveKey, redactText } from "#sensitive.ts";
|
|
3
|
+
export interface RpcBoundaryOptions<E> {
|
|
4
|
+
readonly message?: string;
|
|
5
|
+
readonly unexpected: (message: string) => E;
|
|
6
|
+
}
|
|
7
|
+
export function rpcBoundary<
|
|
8
|
+
TSchema extends Schema.ConstraintDecoder<{
|
|
9
|
+
readonly message: string;
|
|
10
|
+
}>,
|
|
11
|
+
>(schema: TSchema, options: RpcBoundaryOptions<TSchema["Type"]>) {
|
|
12
|
+
const declared = Schema.is(schema);
|
|
13
|
+
return <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, TSchema["Type"], R> =>
|
|
14
|
+
Effect.catchCause(self, (cause) => {
|
|
15
|
+
if (Cause.hasInterruptsOnly(cause)) {
|
|
16
|
+
return Effect.failCause(Cause.fromReasons(cause.reasons.filter(Cause.isInterruptReason)));
|
|
17
|
+
}
|
|
18
|
+
const reason = cause.reasons[0];
|
|
19
|
+
if (cause.reasons.length === 1 && reason && Cause.isFailReason(reason) && declared(reason.error)) {
|
|
20
|
+
return Effect.fail(reason.error);
|
|
21
|
+
}
|
|
22
|
+
const rejection = options.unexpected(redactText(options.message ?? "Internal server error", isSensitiveKey));
|
|
23
|
+
Object.defineProperty(rejection, "cause", { configurable: true, enumerable: false, value: cause });
|
|
24
|
+
return Effect.failCause(Cause.fromReasons([...Cause.fail(rejection).reasons, ...cause.reasons.filter(Cause.isInterruptReason)]));
|
|
25
|
+
});
|
|
26
|
+
}
|
package/src/redact.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Predicate, Redacted } from "effect";
|
|
2
|
+
import { isSensitiveKey, REDACTED, redactErrorStack, redactText, type SensitiveKey } from "#sensitive.ts";
|
|
3
|
+
|
|
4
|
+
const MAX_DEPTH = 8;
|
|
5
|
+
const MAX_ENTRIES = 50;
|
|
6
|
+
function binary(value: object): string | undefined {
|
|
7
|
+
if (ArrayBuffer.isView(value)) {
|
|
8
|
+
return `<${value.constructor.name} ${value.byteLength} bytes>`;
|
|
9
|
+
}
|
|
10
|
+
if (value instanceof ArrayBuffer) {
|
|
11
|
+
return `<ArrayBuffer ${value.byteLength} bytes>`;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
function entriesOf(value: object, sensitive: SensitiveKey, bounded: boolean): [string, unknown][] {
|
|
16
|
+
const entries = Object.entries(value);
|
|
17
|
+
if (!Predicate.isError(value)) {
|
|
18
|
+
return entries;
|
|
19
|
+
}
|
|
20
|
+
const shape: [string, unknown][] = [
|
|
21
|
+
["name", value.name],
|
|
22
|
+
["message", value.message],
|
|
23
|
+
["stack", bounded ? redactErrorStack(value, sensitive) : value.stack],
|
|
24
|
+
];
|
|
25
|
+
const extra = entries.filter(([key]) => !["name", "message", "stack", "cause"].includes(key));
|
|
26
|
+
return value.cause === undefined ? [...shape, ...extra] : [...shape, ...extra, ["cause", value.cause]];
|
|
27
|
+
}
|
|
28
|
+
function collection(value: object, sensitive: SensitiveKey, depth: number, bounded: boolean): unknown {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
const items = value.slice(0, bounded ? MAX_ENTRIES : value.length).map((item) => walk(item, sensitive, depth + 1, bounded));
|
|
31
|
+
return bounded && value.length > MAX_ENTRIES ? [...items, `<${value.length - MAX_ENTRIES} more items>`] : items;
|
|
32
|
+
}
|
|
33
|
+
const entries = entriesOf(value, sensitive, bounded);
|
|
34
|
+
const kept = entries.slice(0, bounded ? MAX_ENTRIES : entries.length).map(([key, nested]): [string, unknown] => {
|
|
35
|
+
const stackHeader = key === "stack" && typeof nested === "string" && (sensitive("name") || sensitive("message"));
|
|
36
|
+
return [key, sensitive(key) || stackHeader ? REDACTED : walk(nested, sensitive, depth + 1, bounded)];
|
|
37
|
+
});
|
|
38
|
+
return Object.fromEntries(bounded && entries.length > MAX_ENTRIES ? [...kept, ["<truncated>", `${entries.length - MAX_ENTRIES} more keys`]] : kept);
|
|
39
|
+
}
|
|
40
|
+
function walk(value: unknown, sensitive: SensitiveKey, depth: number, bounded: boolean): unknown {
|
|
41
|
+
if (Redacted.isRedacted(value)) {
|
|
42
|
+
return REDACTED;
|
|
43
|
+
}
|
|
44
|
+
if (Predicate.isString(value)) {
|
|
45
|
+
return bounded ? redactText(value, sensitive) : redactText(value, sensitive, Number.POSITIVE_INFINITY);
|
|
46
|
+
}
|
|
47
|
+
if (!Predicate.isObjectOrArray(value)) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
const summary = binary(value);
|
|
51
|
+
if (summary !== undefined) {
|
|
52
|
+
return bounded ? summary : value;
|
|
53
|
+
}
|
|
54
|
+
if (bounded && depth >= MAX_DEPTH) {
|
|
55
|
+
return "<truncated>";
|
|
56
|
+
}
|
|
57
|
+
return collection(value, sensitive, depth, bounded);
|
|
58
|
+
}
|
|
59
|
+
export function redact(value: unknown, sensitive: SensitiveKey = isSensitiveKey): unknown {
|
|
60
|
+
return walk(value, sensitive, 0, true);
|
|
61
|
+
}
|
|
62
|
+
export function redactWire(value: unknown, sensitive: SensitiveKey = isSensitiveKey): unknown {
|
|
63
|
+
return walk(value, sensitive, 0, false);
|
|
64
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Cause, Context, ErrorReporter, Predicate, type References } from "effect";
|
|
2
|
+
import { redact } from "#redact.ts";
|
|
3
|
+
import { isSensitiveKey, REDACTED, redactErrorStack, redactText, type SensitiveKey } from "#sensitive.ts";
|
|
4
|
+
|
|
5
|
+
function reporterHints(error: Error, sensitive: SensitiveKey) {
|
|
6
|
+
return {
|
|
7
|
+
...(ErrorReporter.isIgnored(error) ? { [ErrorReporter.ignore]: true } : {}),
|
|
8
|
+
...(ErrorReporter.severity in error ? { [ErrorReporter.severity]: ErrorReporter.getSeverity(error) } : {}),
|
|
9
|
+
...(ErrorReporter.attributes in error ? { [ErrorReporter.attributes]: redact(ErrorReporter.getAttributes(error), sensitive) } : {}),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function errorText(key: string, value: string, sensitive: SensitiveKey): string {
|
|
13
|
+
return sensitive(key) ? REDACTED : redactText(value, sensitive);
|
|
14
|
+
}
|
|
15
|
+
function redactError(error: Error, sensitive: SensitiveKey): Error {
|
|
16
|
+
const cause = sensitive("cause") ? REDACTED : redact(error.cause, sensitive);
|
|
17
|
+
const options = error.cause === undefined ? undefined : { cause };
|
|
18
|
+
const copy = Object.assign(
|
|
19
|
+
new Error(errorText("message", error.message, sensitive), options),
|
|
20
|
+
redact(Object.fromEntries(Object.entries(error)), sensitive),
|
|
21
|
+
reporterHints(error, sensitive),
|
|
22
|
+
);
|
|
23
|
+
Object.defineProperty(copy, "name", { configurable: true, value: errorText("name", error.name, sensitive), writable: true });
|
|
24
|
+
Object.defineProperty(copy, "stack", {
|
|
25
|
+
configurable: true,
|
|
26
|
+
value: redactErrorStack(error, sensitive),
|
|
27
|
+
writable: true,
|
|
28
|
+
});
|
|
29
|
+
return copy;
|
|
30
|
+
}
|
|
31
|
+
export function redactDefect(defect: unknown, sensitive: SensitiveKey = isSensitiveKey): unknown {
|
|
32
|
+
return Predicate.isError(defect) ? redactError(defect, sensitive) : redact(defect, sensitive);
|
|
33
|
+
}
|
|
34
|
+
export function redactCause<E>(cause: Cause.Cause<E>, sensitive: SensitiveKey = isSensitiveKey): Cause.Cause<E> {
|
|
35
|
+
return Cause.fromReasons(
|
|
36
|
+
cause.reasons.flatMap<Cause.Reason<E>>((reason) =>
|
|
37
|
+
Cause.isDieReason(reason)
|
|
38
|
+
? Cause.annotate(Cause.die(redactDefect(reason.defect, sensitive)), Context.makeUnsafe(reason.annotations)).reasons
|
|
39
|
+
: [reason],
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
function diagnosticReason(reason: Cause.Reason<unknown>, sensitive: SensitiveKey): Cause.Cause<unknown> {
|
|
44
|
+
switch (reason._tag) {
|
|
45
|
+
case "Fail":
|
|
46
|
+
return Cause.fail(redactDefect(reason.error, sensitive));
|
|
47
|
+
case "Die":
|
|
48
|
+
return Cause.die(redactDefect(reason.defect, sensitive));
|
|
49
|
+
case "Interrupt":
|
|
50
|
+
return Cause.interrupt(reason.fiberId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function stackFrame(frame: References.StackFrame | undefined, sensitive: SensitiveKey, remaining = 8): References.StackFrame | undefined {
|
|
54
|
+
if (frame === undefined || remaining === 0) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
name: sensitive("name") ? REDACTED : redactText(frame.name, sensitive),
|
|
59
|
+
parent: stackFrame(frame.parent, sensitive, remaining - 1),
|
|
60
|
+
stack: () => {
|
|
61
|
+
const value = frame.stack();
|
|
62
|
+
if (value === undefined) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return sensitive("name") || sensitive("message") ? REDACTED : redactText(value, sensitive);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function diagnosticAnnotations(reason: Cause.Reason<unknown>, sensitive: SensitiveKey) {
|
|
70
|
+
const nativeStacks: Context.Key<Cause.StackTrace | Cause.InterruptorStackTrace, References.StackFrame>[] = [
|
|
71
|
+
Cause.StackTrace,
|
|
72
|
+
Cause.InterruptorStackTrace,
|
|
73
|
+
];
|
|
74
|
+
const annotations = new Map(
|
|
75
|
+
[...reason.annotations]
|
|
76
|
+
.filter(([key]) => !nativeStacks.some((service) => service.key === key))
|
|
77
|
+
.map(([key, value]) => [key, sensitive(key) ? REDACTED : redact(value, sensitive)]),
|
|
78
|
+
);
|
|
79
|
+
if (!sensitive("stack")) {
|
|
80
|
+
const context = Cause.reasonAnnotations(reason);
|
|
81
|
+
for (const service of nativeStacks) {
|
|
82
|
+
const frame = Context.getOrUndefined(context, service);
|
|
83
|
+
if (frame !== undefined && !sensitive(service.key)) {
|
|
84
|
+
annotations.set(service.key, stackFrame(frame, sensitive));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return annotations;
|
|
89
|
+
}
|
|
90
|
+
export function redactDiagnosticCause(cause: Cause.Cause<unknown>, sensitive: SensitiveKey = isSensitiveKey): Cause.Cause<unknown> {
|
|
91
|
+
return Cause.fromReasons(
|
|
92
|
+
cause.reasons.flatMap((reason) => {
|
|
93
|
+
const redacted = diagnosticReason(reason, sensitive);
|
|
94
|
+
const annotations = diagnosticAnnotations(reason, sensitive);
|
|
95
|
+
return Cause.annotate(redacted, Context.makeUnsafe(annotations), { overwrite: true }).reasons;
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
export function redactingErrorReporter(reporter: ErrorReporter.ErrorReporter, sensitive: SensitiveKey = isSensitiveKey): ErrorReporter.ErrorReporter {
|
|
100
|
+
return {
|
|
101
|
+
[ErrorReporter.TypeId]: ErrorReporter.TypeId,
|
|
102
|
+
report: (options) => reporter.report({ ...options, cause: redactDiagnosticCause(options.cause, sensitive) }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
export interface RejectedField {
|
|
3
|
+
readonly field: string;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
}
|
|
6
|
+
const Rejected = Schema.Struct({ _tag: Schema.String, field: Schema.String, message: Schema.String });
|
|
7
|
+
const decode = Schema.decodeUnknownOption(Rejected);
|
|
8
|
+
export function rejectedField(error: unknown): Option.Option<RejectedField> {
|
|
9
|
+
return Option.map(decode(error), ({ field, message }) => ({ field, message }));
|
|
10
|
+
}
|
package/src/sensitive.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const REDACTED = "<redacted>";
|
|
2
|
+
const MAX_STRING = 2048;
|
|
3
|
+
const SENSITIVE_PARTS = [
|
|
4
|
+
"password",
|
|
5
|
+
"passphrase",
|
|
6
|
+
"secret",
|
|
7
|
+
"token",
|
|
8
|
+
"authorization",
|
|
9
|
+
"cookie",
|
|
10
|
+
"apikey",
|
|
11
|
+
"accesskey",
|
|
12
|
+
"credential",
|
|
13
|
+
"privatekey",
|
|
14
|
+
"jwt",
|
|
15
|
+
"bearer",
|
|
16
|
+
"signature",
|
|
17
|
+
"sessionid",
|
|
18
|
+
"cardnumber",
|
|
19
|
+
"accountnumber",
|
|
20
|
+
"csrf",
|
|
21
|
+
"xsrf",
|
|
22
|
+
];
|
|
23
|
+
const SENSITIVE_WORDS = ["otp", "pin", "cvv", "cvc", "ssn", "iban"];
|
|
24
|
+
const SENSITIVE_NAMES = ["session", "pan", "sig"];
|
|
25
|
+
export type SensitiveKey = (key: string) => boolean;
|
|
26
|
+
function wordsOf(key: string): string[] {
|
|
27
|
+
return key
|
|
28
|
+
.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower> $<upper>")
|
|
29
|
+
.replaceAll(/(?<initial>[A-Z])(?<word>[A-Z][a-z])/gu, "$<initial> $<word>")
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.split(/[\s_-]+/u);
|
|
32
|
+
}
|
|
33
|
+
export function isSensitiveKey(key: string): boolean {
|
|
34
|
+
const normalized = key.toLowerCase().replaceAll(/[-_]/gu, "");
|
|
35
|
+
return (
|
|
36
|
+
SENSITIVE_NAMES.includes(normalized)
|
|
37
|
+
|| SENSITIVE_PARTS.some((part) => normalized.includes(part))
|
|
38
|
+
|| wordsOf(key).some((word) => SENSITIVE_WORDS.includes(word))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const AUTH_SCHEME = /\b(?<scheme>Bearer|Basic)\s+[A-Za-z0-9%._~+/-]+=*/giu;
|
|
42
|
+
const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/gu;
|
|
43
|
+
const PRIVATE_KEY = /-----BEGIN (?<kind>(?:[A-Z0-9]+ )*PRIVATE KEY)-----[\s\S]*?-----END \k<kind>-----/gu;
|
|
44
|
+
const URL_PASSWORD = /(?<authority>\/\/[^:/\s@]+:)[^@/\s]+@/gu;
|
|
45
|
+
function redactAssignments(text: string, sensitive: SensitiveKey): string {
|
|
46
|
+
const matcher = /(?<key>[A-Za-z][\w.-]*)(?<separator>["']?\s*[=:]\s*["']?)(?<value>[^\s"'&,;]+)/gu;
|
|
47
|
+
let result = "";
|
|
48
|
+
let cursor = 0;
|
|
49
|
+
for (let match = matcher.exec(text); match !== null; match = matcher.exec(text)) {
|
|
50
|
+
const key = match[1] ?? "";
|
|
51
|
+
const separator = match[2] ?? "";
|
|
52
|
+
if (sensitive(key)) {
|
|
53
|
+
result += `${text.slice(cursor, match.index)}${key}${separator}${REDACTED}`;
|
|
54
|
+
cursor = matcher.lastIndex;
|
|
55
|
+
} else if (match[3]?.includes("=")) {
|
|
56
|
+
// Revisit embedded parameters without parsing URL and source-location colons as assignments.
|
|
57
|
+
matcher.lastIndex = match.index + key.length + separator.length;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result + text.slice(cursor);
|
|
61
|
+
}
|
|
62
|
+
function scrub(text: string, sensitive: SensitiveKey): string {
|
|
63
|
+
const credentials = text
|
|
64
|
+
.replaceAll(PRIVATE_KEY, REDACTED)
|
|
65
|
+
.replaceAll(AUTH_SCHEME, `$<scheme> ${REDACTED}`)
|
|
66
|
+
.replaceAll(JWT, REDACTED)
|
|
67
|
+
.replaceAll(URL_PASSWORD, `$<authority>${REDACTED}@`);
|
|
68
|
+
return redactAssignments(credentials, sensitive);
|
|
69
|
+
}
|
|
70
|
+
export function redactText(text: string, sensitive: SensitiveKey, maxLength = MAX_STRING): string {
|
|
71
|
+
const scrubbed = scrub(text, sensitive);
|
|
72
|
+
return scrubbed.length > maxLength ? `${scrubbed.slice(0, maxLength)}…<${scrubbed.length - maxLength} more chars>` : scrubbed;
|
|
73
|
+
}
|
|
74
|
+
export function redactErrorStack(error: Error, sensitive: SensitiveKey): string | undefined {
|
|
75
|
+
if (error.stack === undefined) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return ["message", "name", "stack"].some((key) => sensitive(key)) ? REDACTED : redactText(error.stack, sensitive);
|
|
79
|
+
}
|
package/src/taxonomy.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
const described = { message: Schema.String };
|
|
4
|
+
const perField = { field: Schema.optionalKey(Schema.String), message: Schema.String };
|
|
5
|
+
export class NotFound extends Schema.TaggedError<NotFound>()("NotFound", described) {}
|
|
6
|
+
export class Unauthorized extends Schema.TaggedError<Unauthorized>()("Unauthorized", described) {}
|
|
7
|
+
export class Forbidden extends Schema.TaggedError<Forbidden>()("Forbidden", described) {}
|
|
8
|
+
export class BadRequest extends Schema.TaggedError<BadRequest>()("BadRequest", perField) {}
|
|
9
|
+
export class Conflict extends Schema.TaggedError<Conflict>()("Conflict", perField) {}
|
|
10
|
+
export class PreconditionFailed extends Schema.TaggedError<PreconditionFailed>()("PreconditionFailed", described) {}
|
|
11
|
+
export class TooManyRequests extends Schema.TaggedError<TooManyRequests>()("TooManyRequests", described) {}
|
|
12
|
+
export class AuthUnavailable extends Schema.TaggedError<AuthUnavailable>()("AuthUnavailable", described) {}
|
|
13
|
+
export class InternalServerError extends Schema.TaggedError<InternalServerError>()("InternalServerError", described) {}
|
|
14
|
+
export class ServiceUnavailable extends Schema.TaggedError<ServiceUnavailable>()("ServiceUnavailable", described) {}
|