better-call 0.0.0-experimental.7f728d4b
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 +254 -0
- package/dist/error.cjs +67 -0
- package/dist/error.cjs.map +1 -0
- package/dist/error.d.cts +46 -0
- package/dist/error.d.mts +46 -0
- package/dist/error.mjs +64 -0
- package/dist/error.mjs.map +1 -0
- package/dist/fn.cjs +335 -0
- package/dist/fn.cjs.map +1 -0
- package/dist/fn.d.cts +286 -0
- package/dist/fn.d.mts +286 -0
- package/dist/fn.mjs +335 -0
- package/dist/fn.mjs.map +1 -0
- package/dist/index.cjs +37 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +46 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.mjs +24 -0
- package/dist/index.mjs.map +1 -0
- package/dist/module.cjs +111 -0
- package/dist/module.cjs.map +1 -0
- package/dist/module.d.cts +249 -0
- package/dist/module.d.mts +249 -0
- package/dist/module.mjs +102 -0
- package/dist/module.mjs.map +1 -0
- package/dist/plugins/http.cjs +185 -0
- package/dist/plugins/http.cjs.map +1 -0
- package/dist/plugins/http.d.cts +2155 -0
- package/dist/plugins/http.d.mts +2155 -0
- package/dist/plugins/http.mjs +175 -0
- package/dist/plugins/http.mjs.map +1 -0
- package/dist/schema.cjs +166 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +201 -0
- package/dist/schema.d.mts +201 -0
- package/dist/schema.mjs +159 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/scope.d.cts +18 -0
- package/dist/scope.d.mts +18 -0
- package/dist/storage.cjs +256 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.cts +195 -0
- package/dist/storage.d.mts +195 -0
- package/dist/storage.mjs +253 -0
- package/dist/storage.mjs.map +1 -0
- package/dist/types.d.cts +8 -0
- package/dist/types.d.mts +8 -0
- package/dist/var.cjs +162 -0
- package/dist/var.cjs.map +1 -0
- package/dist/var.d.cts +36 -0
- package/dist/var.d.mts +36 -0
- package/dist/var.mjs +154 -0
- package/dist/var.mjs.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Bereket Engida
|
|
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
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Better Call v3 (Expt)
|
|
2
|
+
|
|
3
|
+
An experimental rewrite of better-call around three primitives: **fns**, **vars**, and **modules**. Everything else — plugins, HTTP, capability security — is a usage of those three, not a new concept.
|
|
4
|
+
|
|
5
|
+
## Core
|
|
6
|
+
|
|
7
|
+
### fns
|
|
8
|
+
|
|
9
|
+
A fn is a typed, keyed, callable unit. The key is what interceptors target; the input is validated at the door.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { v } from "better-call";
|
|
13
|
+
|
|
14
|
+
const signIn = v.fn(
|
|
15
|
+
"sign_in.email",
|
|
16
|
+
{ input: { email: v.string(), password: v.string() } },
|
|
17
|
+
async (c) => {
|
|
18
|
+
// c.input is validated and typed
|
|
19
|
+
return { user: { id: "user:1" } };
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
await signIn({ email: "b@acme.com", password: "pw" });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Called without a key or options, `v.fn` becomes a builder: keys concatenate, options merge, and the first handler terminates the chain.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const auth = v.fn({ use: [{ session, user }] });
|
|
30
|
+
const createSession = auth.fn("create_session", { ... }, async (c) => { ... });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A **tuple input** declares positional args — one schema per position, each validated at its index, and `c.input` is the parsed tuple:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const add = v.fn({ input: [v.number(), v.number()] }, (c) => c.input[0] + c.input[1]);
|
|
37
|
+
add(2, 3); // 5
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A handler-less builder doubles as an **input schema for a fn**: the value crossing is a fn with that signature. A plain closure passed there gets the declared input validated at its door on every call; a real `v.fn` passes through untouched and validates itself. Both compose (see `test/tools.ts`):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const createTool = v.fn("create_tool", {
|
|
44
|
+
input: [
|
|
45
|
+
v.string(),
|
|
46
|
+
v.object({ description: v.string() }),
|
|
47
|
+
v.fn({ input: { location: v.string() } }), // "a fn taking { location }"
|
|
48
|
+
],
|
|
49
|
+
}, (c) => { const [name, , execute] = c.input; ... });
|
|
50
|
+
|
|
51
|
+
createTool("get_weather", { description: "..." }, async ({ location }) => ({ ... }));
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Inside a **data schema** (a `v.object` shape, a var's `schema`), declare fn-typed fields with `v.fn.type` — and bare `v.fn` (never called) is "any function":
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const db = v.var("db", {
|
|
58
|
+
schema: v.object({
|
|
59
|
+
user: v.object({
|
|
60
|
+
create: v.fn.type({ // a fn from { id } to { id }
|
|
61
|
+
input: { id: v.string() },
|
|
62
|
+
output: v.object({ id: v.string() }),
|
|
63
|
+
}),
|
|
64
|
+
drop: v.fn, // any function at all
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Validation checks what a signature *can* be checked for: the value is a function, and a plain closure gets the declared input validated at its door on every call — the rest of the signature lives at the type level. `v.fn.type` exists apart from the handler-less builder for **inline** spots like the one above: a `v.fn(...)` *call* written inline inside another call's arguments makes TypeScript defer it (the handler overloads return a callable, which trips higher-order inference), silently wiping the enclosing `v.object`/`v.var`'s type inference. A handler-less `v.fn({ ... })` still works as a schema when hoisted to its own `const`.
|
|
71
|
+
|
|
72
|
+
A fn schema whose input **is a var** (`create: v.fn.type({ input: user })`) resolves that input against the **scope it is read in**, not just the schema it was written with: whatever the scope mounts on the var — a `v.extend` extension, a `customize`d re-export — widens the fn's call args, the same way `ApplyOn` widens a used fn. Declare storage against the core `user`, let the app mount `userWithEmail`, and every `db.createUser(...)` call site inside that scope demands the email:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const db = v.var("db", {
|
|
76
|
+
schema: v.object({ createUser: v.fn.type({ input: user }) }),
|
|
77
|
+
});
|
|
78
|
+
const app = v.fn({ use: [{ user, userWithEmail, db }] });
|
|
79
|
+
app.fn("auth.x", async (c) => {
|
|
80
|
+
c.db?.createUser({ id: "1", email: "a@b.c" }); // email required HERE
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### errors
|
|
85
|
+
|
|
86
|
+
Errors are the third contract door: input validates on entry, output on exit, errors at throw. A fn declares its failures as `tag -> payload schema`; `c.error` only accepts declared tags and validates the payload at mint:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const signIn = v.fn("sign_in.email", {
|
|
90
|
+
input: { email: v.string(), password: v.string() },
|
|
91
|
+
errors: { invalid_credentials: { attempts: v.number() } },
|
|
92
|
+
}, async (c) => {
|
|
93
|
+
if (bad) throw c.error("invalid_credentials", { attempts: 3 });
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
What this buys, all Effect-inspired but with plain functions:
|
|
98
|
+
|
|
99
|
+
- **Failure vs defect.** A thrown `c.error(...)` is a `FnError` - a domain outcome, tagged, serializable (`{ tag, data, trail }` survives a wire). Once a fn declares `errors`, any *untagged* throw escaping its body is a bug and comes out as `UnexpectedError` with the original on `cause`. Callers never string-match to tell the two apart.
|
|
100
|
+
- **Typed recovery.** `fn.try(input)` returns `{ ok: true, value } | { ok: false, error }` where `error` is the union of declared errors - TS narrows on `error.tag`. Defects and contract violations still throw. `FnErrors<typeof fn>` gives the union for catch sites.
|
|
101
|
+
- **The trail.** As an error crosses fn frames it collects their keys - `["audit.log", "profile.update", "capability.exec"]` - origin first, so nothing about where a failure started is ever lost.
|
|
102
|
+
- **All issues, not the first.** Validation collects every bad field / tuple position in one `ValidationError.issues` list.
|
|
103
|
+
|
|
104
|
+
### vars
|
|
105
|
+
|
|
106
|
+
A var is named, scoped state that travels down the call tree — no threading through arguments. Fns declare their contract against vars: `provides` (checked on exit), `requires` (checked on entry), `readonly` (the whole subtree's scope locks).
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const session = v.var("session", {
|
|
110
|
+
default: null,
|
|
111
|
+
schema: v.object({ userId: v.string() }),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const createSession = v.fn(
|
|
115
|
+
"create_session",
|
|
116
|
+
{ input: { userId: v.string() }, provides: ["session"], use: [{ session }] },
|
|
117
|
+
async (c) => {
|
|
118
|
+
c.session = { userId: c.input.userId };
|
|
119
|
+
return { created: true };
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
There are also accumulating vars (`v.record`), computed vars (`v.derive`), reshaping (`customize`), and mountable widening (`v.extend`).
|
|
125
|
+
|
|
126
|
+
### modules
|
|
127
|
+
|
|
128
|
+
A module is the unit of composition: a plain record of members — fns, vars, `on` entries — usually just what a file exports. "Plugin" is not a concept, only a usage: mounting someone else's module with `use`.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const coreSession = { createUser, createSession, session, user };
|
|
132
|
+
|
|
133
|
+
const app = v.fn({ use: [coreSession] });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`v.on` mounts onto another fn by name (or reference): the handler replaces the target's body and receives `next` — call it to delegate, or don't. Targets take exact keys, `*` wildcards, RegExps, and `var.set.<name>` events for intercepting writes.
|
|
137
|
+
|
|
138
|
+
## Fns vs plain functions
|
|
139
|
+
|
|
140
|
+
Not everything is a fn. A fn is for an **operation** — a unit that participates in the app's composition model. A plain function is for a **primitive** — a pure computation. Everything `v.fn` buys — validation at the door, a declared error channel, vars traveling down the tree, interception by key, a name a router can serve — is aimed at the application boundary. Where none of that applies, the wrapper is dead weight.
|
|
141
|
+
|
|
142
|
+
Write a `v.fn` when at least one of these is true:
|
|
143
|
+
|
|
144
|
+
- **Its input is untrusted** — it arrives from a user, a wire, another process — so validation at the door means something.
|
|
145
|
+
- **It reads or provides vars** — it needs session, storage, request state without threading arguments.
|
|
146
|
+
- **Someone else should be able to change it** — a plugin hooking it with `on`, an override through `use`. Extensibility is the point.
|
|
147
|
+
- **It should be addressable** — a router exposes it, a capability names it, an error trail should record it.
|
|
148
|
+
|
|
149
|
+
Keep a plain function when the opposite holds:
|
|
150
|
+
|
|
151
|
+
- **The input is already trusted and precisely typed.** A `CryptoKey`, a `Uint8Array`, a `JsonWebKey` — TypeScript checks these better than any schema could express them. Wrapping them in `v.any()` is validation theater with runtime cost.
|
|
152
|
+
- **It's pure.** Same input, same output, no context — the var machinery would carry nothing, and pure helpers sit in hot paths.
|
|
153
|
+
- **Interception would be a liability, not a feature.** A signature check or a hash a plugin can wrap is an attack surface. Security primitives should be boringly non-extensible.
|
|
154
|
+
- **It should stay portable.** A leaf module that imports nothing from the runtime can be lifted anywhere — making it a fn inverts the dependency arrow.
|
|
155
|
+
|
|
156
|
+
The worked example is `expt-better-auth`: `sign_up.email` and `two_factor.enable` are fns — untrusted input, session vars, plugin surface, router paths. Its `src/crypto/` (base64url, JWK thumbprints, JWT sign/verify) is plain functions — pure, precisely typed, deliberately uninterceptable.
|
|
157
|
+
|
|
158
|
+
When you *do* want extensibility around a primitive — say, letting an app observe or veto token verification — hang the hook on the operation that calls it, not on the primitive itself. The fn layer is where `on` belongs; the primitive stays sealed.
|
|
159
|
+
|
|
160
|
+
## Capability-based security
|
|
161
|
+
|
|
162
|
+
`test/capability.ts` and `test/capability-demo.ts` explore what security looks like when it is built out of the primitives above. The model in one sentence:
|
|
163
|
+
|
|
164
|
+
> A fn may be called exactly by whoever **holds a reference** to it, and every fn validates that about its caller before doing anything.
|
|
165
|
+
|
|
166
|
+
### The reference is the capability
|
|
167
|
+
|
|
168
|
+
How the reference arrives is the whole story — there are only two ways:
|
|
169
|
+
|
|
170
|
+
**Direct (in-process).** The caller is another fn whose body holds this fn in memory — through `use`, an import, a closure. Possession IS authorization: some scope that held the reference chose to hand it over. There is nothing to verify, so fn-to-fn calls inside a process carry no token and check nothing.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const updateProfile = v.fn(
|
|
174
|
+
"profile.update",
|
|
175
|
+
{ input: { name: v.string() }, use: [{ capability, audit }] },
|
|
176
|
+
async (c) => {
|
|
177
|
+
// fn to fn: no token, no ceremony. `use` handed this body a
|
|
178
|
+
// REFERENCE to audit, and in-process possession IS authorization.
|
|
179
|
+
await c.audit({ event: `renamed to "${c.input.name}"` });
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Reified (across a boundary).** No memory reference can travel over a wire, so the reference becomes data: a signed **delegation** naming the fn (optionally pinned to input), exercised one call at a time by a signed **invocation**. Same model, two encodings.
|
|
185
|
+
|
|
186
|
+
A capability is never created or registered — it is inferred from the fn it names, and answers exactly one question: may the holder call THAT fn, with THAT input?
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
"profile.update" // any input
|
|
190
|
+
{ fn: "profile.update", input: { name: "X" } } // only this input
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### The rule
|
|
194
|
+
|
|
195
|
+
Every served fn runs one check before its body: *was my caller authorized to make this call with this input?* (`validateCaller` in `test/capability.ts`):
|
|
196
|
+
|
|
197
|
+
- No boundary above this call → the caller reached this fn through a memory reference. Possession is the capability; nothing to verify.
|
|
198
|
+
- This fn is the wire entry → the caller holds no memory reference, only the reified one. It must cover this fn AND this input, or the fn refuses.
|
|
199
|
+
- Called from inside by a fn that already passed the boundary check → its body holds this fn by reference. Implied.
|
|
200
|
+
|
|
201
|
+
Once the entry frame passes, the wire hop is spent: everything below runs on direct references again.
|
|
202
|
+
|
|
203
|
+
The boundary (`serve`/`exec`) only proves the token is *genuinely held* — every link signed, attenuating, unexpired, rooted at this server, and the spend signed by the chain's audience (a stolen delegation is inert). Whether the chain covers a given call is deliberately not its question: each fn asks that itself.
|
|
204
|
+
|
|
205
|
+
### Authority
|
|
206
|
+
|
|
207
|
+
Authority never checks a call — the rule above owns that. It answers the questions that come *before* any call:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const server = await serve(modules, {
|
|
211
|
+
// what references does a caller start with, given who was proven?
|
|
212
|
+
// `null` IS an answer: nobody's defaults are just enough to go
|
|
213
|
+
// earn attestation (here: sign in).
|
|
214
|
+
defaults: (subject) => (subject ? ["profile.read"] : ["sign_in.email"]),
|
|
215
|
+
|
|
216
|
+
// what proves WHO? reads a proven subject out of ANY fn's result;
|
|
217
|
+
// attested proof rides back alongside it.
|
|
218
|
+
identify: (result) => result?.user?.id ?? null,
|
|
219
|
+
|
|
220
|
+
// how are requests for more references settled?
|
|
221
|
+
decide: ({ caps }) => (dangerous(caps) ? "deny" : "challenge"),
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`attest`/`verify` default to a token the server signs itself, and can be swapped together to lean on an external IDP instead.
|
|
226
|
+
|
|
227
|
+
### Delegation attenuates
|
|
228
|
+
|
|
229
|
+
A held reference can be re-minted for another key — fewer fns, or the same fn pinned to narrower input — and only ever narrows. Escalation anywhere in the chain fails verification.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// hand a slice: readProfile only
|
|
233
|
+
second.hold(await agent.delegate(second.id, ["profile.read"]));
|
|
234
|
+
|
|
235
|
+
// or the same fn, pinned: "may set the name to exactly this"
|
|
236
|
+
renamer.hold(
|
|
237
|
+
await agent.delegate(renamer.id, [
|
|
238
|
+
{ fn: "profile.update", input: { name: "Bekacru II" } },
|
|
239
|
+
]),
|
|
240
|
+
);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Run the demo
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
npx tsx test/capability-demo.ts
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The arc it walks: in-process calls need no token → an agent is born asking and gets only the sign-in bootstrap → nothing is public → signing in attests WHO and trades for the defaults → a fn calls a fn the remote caller was never granted (implied reference) while the same fn refuses the wire → widening is challenged and user-approved → stolen delegations are inert → attenuation and input-pinning hold → attestation outlives the agent.
|
|
250
|
+
|
|
251
|
+
## Other explorations
|
|
252
|
+
|
|
253
|
+
- `test/http-demo.ts` — serving fns over HTTP via `src/plugins/http.ts`
|
|
254
|
+
- `test/better-auth.ts`, `test/email-password.ts`, `test/session.ts`, `test/birthday.ts` — module composition sketches
|
package/dist/error.cjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/error.ts
|
|
3
|
+
/** A contract violation - input, output, requires, provides. Carries
|
|
4
|
+
* EVERY issue found in the pass that threw it, not just the first;
|
|
5
|
+
* `message` lists them all. */
|
|
6
|
+
var ValidationError = class extends Error {
|
|
7
|
+
path;
|
|
8
|
+
issues;
|
|
9
|
+
constructor(path, message, issues) {
|
|
10
|
+
const all = issues?.length ? issues : [{
|
|
11
|
+
path,
|
|
12
|
+
message
|
|
13
|
+
}];
|
|
14
|
+
super(all.map((issue) => `${issue.path}: ${issue.message}`).join("; "));
|
|
15
|
+
this.name = "ValidationError";
|
|
16
|
+
this.path = all[0]?.path ?? path;
|
|
17
|
+
this.issues = all;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A DECLARED failure - a domain outcome, not a bug. Minted only by
|
|
22
|
+
* `c.error(tag, data)`, so the payload is already validated against the
|
|
23
|
+
* fn's `errors` schema. The `tag` is the discriminant callers narrow on;
|
|
24
|
+
* `trail` records the fn that threw, then every frame it crossed.
|
|
25
|
+
* Serializes as data, so it survives a remote boundary intact.
|
|
26
|
+
*/
|
|
27
|
+
var FnError = class extends Error {
|
|
28
|
+
tag;
|
|
29
|
+
data;
|
|
30
|
+
trail;
|
|
31
|
+
constructor(tag, data, fn) {
|
|
32
|
+
super(`${fn}: ${tag}`);
|
|
33
|
+
this.tag = tag;
|
|
34
|
+
this.data = data;
|
|
35
|
+
this.name = "FnError";
|
|
36
|
+
this.trail = [fn];
|
|
37
|
+
}
|
|
38
|
+
toJSON() {
|
|
39
|
+
return {
|
|
40
|
+
name: this.name,
|
|
41
|
+
tag: this.tag,
|
|
42
|
+
data: this.data,
|
|
43
|
+
trail: this.trail
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A failure the fn did NOT declare - a defect. Only minted once a fn
|
|
49
|
+
* opts into `errors`: from then on, anything untagged escaping its body
|
|
50
|
+
* comes out wrapped, so callers can tell a domain refusal (`FnError`)
|
|
51
|
+
* from a bug without string matching. The original throw rides on
|
|
52
|
+
* `cause`, untouched.
|
|
53
|
+
*/
|
|
54
|
+
var UnexpectedError = class extends Error {
|
|
55
|
+
trail;
|
|
56
|
+
constructor(cause, fn) {
|
|
57
|
+
super(`${fn}: unexpected - ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
58
|
+
this.name = "UnexpectedError";
|
|
59
|
+
this.trail = [fn];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
exports.FnError = FnError;
|
|
64
|
+
exports.UnexpectedError = UnexpectedError;
|
|
65
|
+
exports.ValidationError = ValidationError;
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=error.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.cjs","names":[],"sources":["../src/error.ts"],"sourcesContent":["export type Issue = { path: string; message: string };\n\n/** A contract violation - input, output, requires, provides. Carries\n * EVERY issue found in the pass that threw it, not just the first;\n * `message` lists them all. */\nexport class ValidationError extends Error {\n\tpublic path: string;\n\tpublic issues: Issue[];\n\tconstructor(path: string, message: string, issues?: Issue[]) {\n\t\tconst all = issues?.length ? issues : [{ path, message }];\n\t\tsuper(all.map((issue) => `${issue.path}: ${issue.message}`).join(\"; \"));\n\t\tthis.name = \"ValidationError\";\n\t\tthis.path = all[0]?.path ?? path;\n\t\tthis.issues = all;\n\t}\n}\n\n/**\n * A DECLARED failure - a domain outcome, not a bug. Minted only by\n * `c.error(tag, data)`, so the payload is already validated against the\n * fn's `errors` schema. The `tag` is the discriminant callers narrow on;\n * `trail` records the fn that threw, then every frame it crossed.\n * Serializes as data, so it survives a remote boundary intact.\n */\nexport class FnError<\n\tTag extends string = string,\n\tData = unknown,\n> extends Error {\n\tpublic trail: string[];\n\tconstructor(\n\t\tpublic tag: Tag,\n\t\tpublic data: Data,\n\t\tfn: string,\n\t) {\n\t\tsuper(`${fn}: ${tag}`);\n\t\tthis.name = \"FnError\";\n\t\tthis.trail = [fn];\n\t}\n\ttoJSON() {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\ttag: this.tag,\n\t\t\tdata: this.data,\n\t\t\ttrail: this.trail,\n\t\t};\n\t}\n}\n\n/**\n * A failure the fn did NOT declare - a defect. Only minted once a fn\n * opts into `errors`: from then on, anything untagged escaping its body\n * comes out wrapped, so callers can tell a domain refusal (`FnError`)\n * from a bug without string matching. The original throw rides on\n * `cause`, untouched.\n */\nexport class UnexpectedError extends Error {\n\tpublic trail: string[];\n\tconstructor(cause: unknown, fn: string) {\n\t\tsuper(\n\t\t\t`${fn}: unexpected - ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = \"UnexpectedError\";\n\t\tthis.trail = [fn];\n\t}\n}\n"],"mappings":";;;;;AAKA,IAAa,kBAAb,cAAqC,MAAM;CAC1C;CACA;CACA,YAAY,MAAc,SAAiB,QAAkB;EAC5D,MAAM,MAAM,QAAQ,SAAS,SAAS,CAAC;GAAE;GAAM;EAAQ,CAAC;EACxD,MAAM,IAAI,KAAK,UAAU,GAAG,MAAM,KAAK,IAAI,MAAM,SAAS,CAAC,CAAC,KAAK,IAAI,CAAC;EACtE,KAAK,OAAO;EACZ,KAAK,OAAO,IAAI,EAAE,EAAE,QAAQ;EAC5B,KAAK,SAAS;CACf;AACD;;;;;;;;AASA,IAAa,UAAb,cAGU,MAAM;CAGP;CACA;CAHR;CACA,YACC,KACA,MACA,IACC;EACD,MAAM,GAAG,GAAG,IAAI,KAAK;EAJd,KAAA,MAAA;EACA,KAAA,OAAA;EAIP,KAAK,OAAO;EACZ,KAAK,QAAQ,CAAC,EAAE;CACjB;CACA,SAAS;EACR,OAAO;GACN,MAAM,KAAK;GACX,KAAK,KAAK;GACV,MAAM,KAAK;GACX,OAAO,KAAK;EACb;CACD;AACD;;;;;;;;AASA,IAAa,kBAAb,cAAqC,MAAM;CAC1C;CACA,YAAY,OAAgB,IAAY;EACvC,MACC,GAAG,GAAG,iBAAiB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAC5E,EAAE,MAAM,CACT;EACA,KAAK,OAAO;EACZ,KAAK,QAAQ,CAAC,EAAE;CACjB;AACD"}
|
package/dist/error.d.cts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/error.d.ts
|
|
2
|
+
type Issue = {
|
|
3
|
+
path: string;
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
/** A contract violation - input, output, requires, provides. Carries
|
|
7
|
+
* EVERY issue found in the pass that threw it, not just the first;
|
|
8
|
+
* `message` lists them all. */
|
|
9
|
+
declare class ValidationError extends Error {
|
|
10
|
+
path: string;
|
|
11
|
+
issues: Issue[];
|
|
12
|
+
constructor(path: string, message: string, issues?: Issue[]);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A DECLARED failure - a domain outcome, not a bug. Minted only by
|
|
16
|
+
* `c.error(tag, data)`, so the payload is already validated against the
|
|
17
|
+
* fn's `errors` schema. The `tag` is the discriminant callers narrow on;
|
|
18
|
+
* `trail` records the fn that threw, then every frame it crossed.
|
|
19
|
+
* Serializes as data, so it survives a remote boundary intact.
|
|
20
|
+
*/
|
|
21
|
+
declare class FnError<Tag extends string = string, Data = unknown> extends Error {
|
|
22
|
+
tag: Tag;
|
|
23
|
+
data: Data;
|
|
24
|
+
trail: string[];
|
|
25
|
+
constructor(tag: Tag, data: Data, fn: string);
|
|
26
|
+
toJSON(): {
|
|
27
|
+
name: string;
|
|
28
|
+
tag: Tag;
|
|
29
|
+
data: Data;
|
|
30
|
+
trail: string[];
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A failure the fn did NOT declare - a defect. Only minted once a fn
|
|
35
|
+
* opts into `errors`: from then on, anything untagged escaping its body
|
|
36
|
+
* comes out wrapped, so callers can tell a domain refusal (`FnError`)
|
|
37
|
+
* from a bug without string matching. The original throw rides on
|
|
38
|
+
* `cause`, untouched.
|
|
39
|
+
*/
|
|
40
|
+
declare class UnexpectedError extends Error {
|
|
41
|
+
trail: string[];
|
|
42
|
+
constructor(cause: unknown, fn: string);
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { FnError, Issue, UnexpectedError, ValidationError };
|
|
46
|
+
//# sourceMappingURL=error.d.cts.map
|
package/dist/error.d.mts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/error.d.ts
|
|
2
|
+
type Issue = {
|
|
3
|
+
path: string;
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
/** A contract violation - input, output, requires, provides. Carries
|
|
7
|
+
* EVERY issue found in the pass that threw it, not just the first;
|
|
8
|
+
* `message` lists them all. */
|
|
9
|
+
declare class ValidationError extends Error {
|
|
10
|
+
path: string;
|
|
11
|
+
issues: Issue[];
|
|
12
|
+
constructor(path: string, message: string, issues?: Issue[]);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A DECLARED failure - a domain outcome, not a bug. Minted only by
|
|
16
|
+
* `c.error(tag, data)`, so the payload is already validated against the
|
|
17
|
+
* fn's `errors` schema. The `tag` is the discriminant callers narrow on;
|
|
18
|
+
* `trail` records the fn that threw, then every frame it crossed.
|
|
19
|
+
* Serializes as data, so it survives a remote boundary intact.
|
|
20
|
+
*/
|
|
21
|
+
declare class FnError<Tag extends string = string, Data = unknown> extends Error {
|
|
22
|
+
tag: Tag;
|
|
23
|
+
data: Data;
|
|
24
|
+
trail: string[];
|
|
25
|
+
constructor(tag: Tag, data: Data, fn: string);
|
|
26
|
+
toJSON(): {
|
|
27
|
+
name: string;
|
|
28
|
+
tag: Tag;
|
|
29
|
+
data: Data;
|
|
30
|
+
trail: string[];
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A failure the fn did NOT declare - a defect. Only minted once a fn
|
|
35
|
+
* opts into `errors`: from then on, anything untagged escaping its body
|
|
36
|
+
* comes out wrapped, so callers can tell a domain refusal (`FnError`)
|
|
37
|
+
* from a bug without string matching. The original throw rides on
|
|
38
|
+
* `cause`, untouched.
|
|
39
|
+
*/
|
|
40
|
+
declare class UnexpectedError extends Error {
|
|
41
|
+
trail: string[];
|
|
42
|
+
constructor(cause: unknown, fn: string);
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { FnError, Issue, UnexpectedError, ValidationError };
|
|
46
|
+
//# sourceMappingURL=error.d.mts.map
|
package/dist/error.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/error.ts
|
|
2
|
+
/** A contract violation - input, output, requires, provides. Carries
|
|
3
|
+
* EVERY issue found in the pass that threw it, not just the first;
|
|
4
|
+
* `message` lists them all. */
|
|
5
|
+
var ValidationError = class extends Error {
|
|
6
|
+
path;
|
|
7
|
+
issues;
|
|
8
|
+
constructor(path, message, issues) {
|
|
9
|
+
const all = issues?.length ? issues : [{
|
|
10
|
+
path,
|
|
11
|
+
message
|
|
12
|
+
}];
|
|
13
|
+
super(all.map((issue) => `${issue.path}: ${issue.message}`).join("; "));
|
|
14
|
+
this.name = "ValidationError";
|
|
15
|
+
this.path = all[0]?.path ?? path;
|
|
16
|
+
this.issues = all;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A DECLARED failure - a domain outcome, not a bug. Minted only by
|
|
21
|
+
* `c.error(tag, data)`, so the payload is already validated against the
|
|
22
|
+
* fn's `errors` schema. The `tag` is the discriminant callers narrow on;
|
|
23
|
+
* `trail` records the fn that threw, then every frame it crossed.
|
|
24
|
+
* Serializes as data, so it survives a remote boundary intact.
|
|
25
|
+
*/
|
|
26
|
+
var FnError = class extends Error {
|
|
27
|
+
tag;
|
|
28
|
+
data;
|
|
29
|
+
trail;
|
|
30
|
+
constructor(tag, data, fn) {
|
|
31
|
+
super(`${fn}: ${tag}`);
|
|
32
|
+
this.tag = tag;
|
|
33
|
+
this.data = data;
|
|
34
|
+
this.name = "FnError";
|
|
35
|
+
this.trail = [fn];
|
|
36
|
+
}
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
name: this.name,
|
|
40
|
+
tag: this.tag,
|
|
41
|
+
data: this.data,
|
|
42
|
+
trail: this.trail
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A failure the fn did NOT declare - a defect. Only minted once a fn
|
|
48
|
+
* opts into `errors`: from then on, anything untagged escaping its body
|
|
49
|
+
* comes out wrapped, so callers can tell a domain refusal (`FnError`)
|
|
50
|
+
* from a bug without string matching. The original throw rides on
|
|
51
|
+
* `cause`, untouched.
|
|
52
|
+
*/
|
|
53
|
+
var UnexpectedError = class extends Error {
|
|
54
|
+
trail;
|
|
55
|
+
constructor(cause, fn) {
|
|
56
|
+
super(`${fn}: unexpected - ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
57
|
+
this.name = "UnexpectedError";
|
|
58
|
+
this.trail = [fn];
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { FnError, UnexpectedError, ValidationError };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=error.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.mjs","names":[],"sources":["../src/error.ts"],"sourcesContent":["export type Issue = { path: string; message: string };\n\n/** A contract violation - input, output, requires, provides. Carries\n * EVERY issue found in the pass that threw it, not just the first;\n * `message` lists them all. */\nexport class ValidationError extends Error {\n\tpublic path: string;\n\tpublic issues: Issue[];\n\tconstructor(path: string, message: string, issues?: Issue[]) {\n\t\tconst all = issues?.length ? issues : [{ path, message }];\n\t\tsuper(all.map((issue) => `${issue.path}: ${issue.message}`).join(\"; \"));\n\t\tthis.name = \"ValidationError\";\n\t\tthis.path = all[0]?.path ?? path;\n\t\tthis.issues = all;\n\t}\n}\n\n/**\n * A DECLARED failure - a domain outcome, not a bug. Minted only by\n * `c.error(tag, data)`, so the payload is already validated against the\n * fn's `errors` schema. The `tag` is the discriminant callers narrow on;\n * `trail` records the fn that threw, then every frame it crossed.\n * Serializes as data, so it survives a remote boundary intact.\n */\nexport class FnError<\n\tTag extends string = string,\n\tData = unknown,\n> extends Error {\n\tpublic trail: string[];\n\tconstructor(\n\t\tpublic tag: Tag,\n\t\tpublic data: Data,\n\t\tfn: string,\n\t) {\n\t\tsuper(`${fn}: ${tag}`);\n\t\tthis.name = \"FnError\";\n\t\tthis.trail = [fn];\n\t}\n\ttoJSON() {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\ttag: this.tag,\n\t\t\tdata: this.data,\n\t\t\ttrail: this.trail,\n\t\t};\n\t}\n}\n\n/**\n * A failure the fn did NOT declare - a defect. Only minted once a fn\n * opts into `errors`: from then on, anything untagged escaping its body\n * comes out wrapped, so callers can tell a domain refusal (`FnError`)\n * from a bug without string matching. The original throw rides on\n * `cause`, untouched.\n */\nexport class UnexpectedError extends Error {\n\tpublic trail: string[];\n\tconstructor(cause: unknown, fn: string) {\n\t\tsuper(\n\t\t\t`${fn}: unexpected - ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = \"UnexpectedError\";\n\t\tthis.trail = [fn];\n\t}\n}\n"],"mappings":";;;;AAKA,IAAa,kBAAb,cAAqC,MAAM;CAC1C;CACA;CACA,YAAY,MAAc,SAAiB,QAAkB;EAC5D,MAAM,MAAM,QAAQ,SAAS,SAAS,CAAC;GAAE;GAAM;EAAQ,CAAC;EACxD,MAAM,IAAI,KAAK,UAAU,GAAG,MAAM,KAAK,IAAI,MAAM,SAAS,CAAC,CAAC,KAAK,IAAI,CAAC;EACtE,KAAK,OAAO;EACZ,KAAK,OAAO,IAAI,EAAE,EAAE,QAAQ;EAC5B,KAAK,SAAS;CACf;AACD;;;;;;;;AASA,IAAa,UAAb,cAGU,MAAM;CAGP;CACA;CAHR;CACA,YACC,KACA,MACA,IACC;EACD,MAAM,GAAG,GAAG,IAAI,KAAK;EAJd,KAAA,MAAA;EACA,KAAA,OAAA;EAIP,KAAK,OAAO;EACZ,KAAK,QAAQ,CAAC,EAAE;CACjB;CACA,SAAS;EACR,OAAO;GACN,MAAM,KAAK;GACX,KAAK,KAAK;GACV,MAAM,KAAK;GACX,OAAO,KAAK;EACb;CACD;AACD;;;;;;;;AASA,IAAa,kBAAb,cAAqC,MAAM;CAC1C;CACA,YAAY,OAAgB,IAAY;EACvC,MACC,GAAG,GAAG,iBAAiB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAC5E,EAAE,MAAM,CACT;EACA,KAAK,OAAO;EACZ,KAAK,QAAQ,CAAC,EAAE;CACjB;AACD"}
|