@guuey/state 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 +285 -0
- package/dist/context.d.ts +37 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +114 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +99 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +144 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +30 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +161 -0
- package/dist/http.js.map +1 -0
- package/dist/in-memory.d.ts +28 -0
- package/dist/in-memory.d.ts.map +1 -0
- package/dist/in-memory.js +239 -0
- package/dist/in-memory.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +106 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/contract-suite.d.ts +11 -0
- package/dist/testing/contract-suite.d.ts.map +1 -0
- package/dist/testing/contract-suite.js +315 -0
- package/dist/testing/contract-suite.js.map +1 -0
- package/dist/types.d.ts +217 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +19 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +60 -0
- package/dist/validate.js.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Loqu, Inc.
|
|
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,285 @@
|
|
|
1
|
+
# `@guuey/state`
|
|
2
|
+
|
|
3
|
+
> Open-source KV client for MCP servers hosted on
|
|
4
|
+
> [guuey.com](https://guuey.com). Scoped per `(user, mcp)`.
|
|
5
|
+
> The MCP stays stateless from its own POV; the user retains
|
|
6
|
+
> data ownership.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @guuey/state
|
|
12
|
+
# or
|
|
13
|
+
pnpm add @guuey/state
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## When to reach for this
|
|
17
|
+
|
|
18
|
+
You're building an MCP server hosted on guuey, and you need a small
|
|
19
|
+
amount of per-user state — idempotency tokens, rate-limit counters,
|
|
20
|
+
OAuth `state` nonces, user preferences a few KB in size.
|
|
21
|
+
|
|
22
|
+
You should **not** reach for `@guuey/state` if you need:
|
|
23
|
+
|
|
24
|
+
- More than ~1 MB of data per user (per MCP).
|
|
25
|
+
- Queries / joins / indexes — KV only.
|
|
26
|
+
- Cross-user shared data — scopes are strict per `(user, mcp)`.
|
|
27
|
+
- Long-lived data (max TTL is 90 days; longer-lived data should
|
|
28
|
+
live in the user's own SaaS via mcp-proxy credential brokering).
|
|
29
|
+
|
|
30
|
+
Those cases are explicitly out of scope. The hard caps are the
|
|
31
|
+
product — per the guuey MCP Hosting Policy: most stateful MCPs only
|
|
32
|
+
need a tiny bit of state (idempotency tokens, rate-limit counters,
|
|
33
|
+
OAuth nonces, small preferences), and a scoped 1 MiB KV covers that
|
|
34
|
+
long tail without turning guuey into a backend-as-a-service.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
### Explicit context (tests, scripts)
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createGuueyState } from "@guuey/state";
|
|
42
|
+
|
|
43
|
+
const kv = createGuueyState({
|
|
44
|
+
context: { userId: "u_abc", mcpId: "mcp_xyz" },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await kv.set("user-prefs", { theme: "dark" }, { ttl: 60 * 60 * 24 * 7 });
|
|
48
|
+
const prefs = await kv.get<{ theme: string }>("user-prefs");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Implicit context (production MCP servers)
|
|
52
|
+
|
|
53
|
+
You install `withGuueyContext` yourself — in production typically
|
|
54
|
+
one line per request via `scopeFromAuthorization` on the Bearer JWT
|
|
55
|
+
guuey sends federated servers (see "Hosted binding" below). Inside
|
|
56
|
+
tool handlers you just import the barrel-exported `kv`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { kv } from "@guuey/state";
|
|
60
|
+
|
|
61
|
+
// Inside an MCP tool handler — middleware has bound the context.
|
|
62
|
+
async function handleAddTask(input: { title: string }) {
|
|
63
|
+
const seenKey = `idempotency:${hash(input.title)}`;
|
|
64
|
+
if (await kv.has(seenKey)) {
|
|
65
|
+
return { duplicate: true };
|
|
66
|
+
}
|
|
67
|
+
await kv.set(seenKey, true, { ttl: 60 });
|
|
68
|
+
// ... create the task
|
|
69
|
+
return { duplicate: false };
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If you call `kv.*` outside a context, you get a clear
|
|
74
|
+
`MissingContextError` instead of silently writing to a fallback
|
|
75
|
+
scope.
|
|
76
|
+
|
|
77
|
+
## Hosted binding
|
|
78
|
+
|
|
79
|
+
The hosted binding is a fetch-based transport that talks to guuey's KV
|
|
80
|
+
API. It activates automatically when `GUUEY_KV_URL` (or
|
|
81
|
+
`options.bindingUrl`) is set — nothing else about your code changes,
|
|
82
|
+
because it implements the same `Kv` interface as the in-memory binding.
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
# .env / deploy env — bare origin, NO trailing /v1 or /v1/state.
|
|
86
|
+
# The client appends `/v1/state/<op>` itself.
|
|
87
|
+
GUUEY_KV_URL=https://api.dev.sandbox.guuey.com # dev sandbox
|
|
88
|
+
# GUUEY_KV_URL=https://api.staging.sandbox.guuey.com # staging sandbox
|
|
89
|
+
# GUUEY_KV_URL=https://api.us-east-1.guuey.com # prod (region-qualified)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**`GUUEY_KV_URL` is injected automatically for guuey-hosted
|
|
93
|
+
(`kind: 'hosted'`) and colocated (`kind: 'colocated'`, agent-pod) MCP
|
|
94
|
+
servers** — no setup required, state just works. A **dev-hosted external**
|
|
95
|
+
server (your own `kind: 'external'` deployment reached over a real URL,
|
|
96
|
+
including federated ones) sets `GUUEY_KV_URL` itself (per-env values
|
|
97
|
+
above); omitting it silently falls back to the non-durable in-memory
|
|
98
|
+
binding rather than failing loud. The per-request auth token, in
|
|
99
|
+
contrast, you don't provision by hand: **only federated MCP servers get
|
|
100
|
+
one**, minted by the platform on every call. Set `federate: true` on an
|
|
101
|
+
`external` entry in `guuey.json` (or use `kind: 'hosted'`/`kind:
|
|
102
|
+
'colocated'`, or a `ggui` URL — those are federated automatically):
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{ "kind": "external", "url": "https://your-server.example.com", "federate": true }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Federation makes guuey mint a per-invoke Bearer JWT and send it as this
|
|
109
|
+
server's `Authorization` header on every call. That inbound token IS
|
|
110
|
+
the credential — a plain `external` entry with static `headers` gets no
|
|
111
|
+
token and can't use hosted state. Derive the `ScopeContext` from it with
|
|
112
|
+
`scopeFromAuthorization` and bind it per request:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { withGuueyContext, scopeFromAuthorization, kv } from "@guuey/state";
|
|
116
|
+
|
|
117
|
+
// Middleware / per-request handler — `req.headers.authorization` is the
|
|
118
|
+
// Bearer JWT guuey sent this (federated) server.
|
|
119
|
+
async function handler(req: { headers: { authorization?: string } }) {
|
|
120
|
+
const authHeader = req.headers.authorization;
|
|
121
|
+
if (typeof authHeader !== "string") throw new Error("missing Authorization header");
|
|
122
|
+
|
|
123
|
+
return withGuueyContext(scopeFromAuthorization(authHeader), async () => {
|
|
124
|
+
// kv.* inside here is scoped to this request's (userId, mcpId).
|
|
125
|
+
await kv.set("last-seen", Date.now(), { ttl: 60 * 60 * 24 });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`scopeFromAuthorization` decodes the JWT client-side (unverified — the
|
|
131
|
+
KV API is the verifier) into `{ userId, mcpId, token }`; the returned
|
|
132
|
+
`token` is what `createGuueyState`/the ALS-bound `kv` uses to
|
|
133
|
+
authenticate the hosted binding's requests, so you never touch
|
|
134
|
+
`GUUEY_KV_TOKEN` yourself in this path. `mcpId` is derived from the
|
|
135
|
+
JWT's `aud` claim (the server's federated resource URL) — deterministic
|
|
136
|
+
and identical to the server-side derivation, so two agents pointing at
|
|
137
|
+
the same external MCP share that MCP's per-user state.
|
|
138
|
+
|
|
139
|
+
## API
|
|
140
|
+
|
|
141
|
+
| Method | Purpose |
|
|
142
|
+
| ------------------------------------ | ------------------------------------------------------------ |
|
|
143
|
+
| `get<T>(key)` | Read. Returns `undefined` if absent or expired. |
|
|
144
|
+
| `set(key, value, { ttl })` | Write. TTL required (no permanent keys). |
|
|
145
|
+
| `delete(key)` | Delete (no-op if absent). |
|
|
146
|
+
| `has(key)` | Existence check. |
|
|
147
|
+
| `keys({ prefix?, limit?, cursor? })` | List keys, paginated (`{ keys, cursor? }`, ≤1000/page). |
|
|
148
|
+
| `increment(key, { ttl, by? })` | Atomic counter (creates key if absent). |
|
|
149
|
+
| `decrement(key, { ttl, by? })` | Inverse of `increment`. |
|
|
150
|
+
| `mget(keys[])` | Bulk read. |
|
|
151
|
+
| `scope()` | Live usage snapshot (`usedBytes`, `limitBytes`, `keyCount`). |
|
|
152
|
+
|
|
153
|
+
## Limits (== the product)
|
|
154
|
+
|
|
155
|
+
| Cap | Value | Why |
|
|
156
|
+
| ------------------ | -------------------- | -------------------------------------------------- |
|
|
157
|
+
| Scope size | 1 MiB | Forces opting into a real DB beyond this. |
|
|
158
|
+
| Single value | 64 KiB | Large blobs → `@guuey/files` (planned). |
|
|
159
|
+
| Key length | 1 KiB | Keeps keys cheap to log + index. |
|
|
160
|
+
| Key character set | `[A-Za-z0-9_.:\-/]+` | Same set CloudWatch/Datadog use. |
|
|
161
|
+
| TTL max | 90 days | Longer-lived data → user-owned SaaS via mcp-proxy. |
|
|
162
|
+
| `keys()` page size | 1000 | Diagnostics tool, not a query engine. |
|
|
163
|
+
| `mget()` batch | 100 keys | Bulk convenience, not a table scan. |
|
|
164
|
+
| Counters | Safe integers only | Rate limits + sequences; not float math. |
|
|
165
|
+
|
|
166
|
+
Sizes are **UTF-8 bytes of the key plus its JSON-encoded value** —
|
|
167
|
+
keys are storage too, so a scope of long keys with tiny values
|
|
168
|
+
cannot dodge the cap. The same accounting applies in every binding,
|
|
169
|
+
so quota behavior is identical in tests and production.
|
|
170
|
+
|
|
171
|
+
Values must be JSON-serializable: top-level `undefined`, functions,
|
|
172
|
+
symbols, `BigInt`, and circular structures throw
|
|
173
|
+
`InvalidArgumentError`. Standard JSON semantics otherwise apply
|
|
174
|
+
(`NaN`/`Infinity` become `null`; nested `undefined` properties are
|
|
175
|
+
dropped) — if that matters for your data, validate before writing.
|
|
176
|
+
|
|
177
|
+
## Identity & trust model
|
|
178
|
+
|
|
179
|
+
Every operation is scoped by a `ScopeContext` (`userId`, `mcpId`).
|
|
180
|
+
Where those ids come from — and who is trusted to assert them —
|
|
181
|
+
is the access-control story:
|
|
182
|
+
|
|
183
|
+
- **In-memory binding:** the context is caller-asserted.
|
|
184
|
+
That's fine because the store is process-local — your process owns
|
|
185
|
+
all of its own data, and both ids are validated (non-empty, no
|
|
186
|
+
whitespace/control characters, ≤256 chars) to fail wiring bugs
|
|
187
|
+
loudly.
|
|
188
|
+
- **Hosted binding:** the client sends the federation-minted
|
|
189
|
+
Bearer JWT (the `token` from `scopeFromAuthorization`) with every
|
|
190
|
+
request, and guuey's KV API is the verifier: it authenticates the
|
|
191
|
+
JWT and derives `userId` (the `sub` claim) and `mcpId` (hash of
|
|
192
|
+
the canonicalized `aud` resource URL) **server-side** — the
|
|
193
|
+
context the client sends is advisory-must-match, never trusted on
|
|
194
|
+
its own. An MCP server can only present tokens guuey itself minted
|
|
195
|
+
for it, so it can never reach another MCP's scopes, by
|
|
196
|
+
construction. Anonymous/guest callers get no durable scope at all —
|
|
197
|
+
their tokens are rejected outright, the same platform-layer
|
|
198
|
+
exclusion guuey applies to durable file storage. Reaching the
|
|
199
|
+
hosted binding is per-rung: guuey-hosted and colocated MCP servers
|
|
200
|
+
get `GUUEY_KV_URL` injected automatically; a dev-hosted `external`
|
|
201
|
+
server sets it itself (see "Hosted binding" above).
|
|
202
|
+
- **Blast radius:** within its own app an MCP server necessarily
|
|
203
|
+
handles every one of its users' requests, so a compromised server
|
|
204
|
+
can touch its own app's scopes — never another app's. Cross-MCP
|
|
205
|
+
isolation is enforced by the storage layer, not by cooperation.
|
|
206
|
+
- **Data ownership:** per-user export + delete lands in the guuey
|
|
207
|
+
console together with the hosted binding — end users can see and
|
|
208
|
+
remove what an MCP stored about them.
|
|
209
|
+
|
|
210
|
+
If you outgrow any of these, you're having a **Case-B moment** (per
|
|
211
|
+
the hosting policy) — build a real backend on a real cloud and treat
|
|
212
|
+
that backend as your MCP's "well-defined API."
|
|
213
|
+
|
|
214
|
+
## Local development
|
|
215
|
+
|
|
216
|
+
Without `GUUEY_KV_URL` (or `options.bindingUrl`) the library uses the
|
|
217
|
+
**in-memory binding**. It emits a one-time `console.warn` on first
|
|
218
|
+
use; data is per-process and lost on restart. That's exactly right
|
|
219
|
+
for tests and `guuey dev` runs.
|
|
220
|
+
|
|
221
|
+
Setting `GUUEY_KV_URL` (or `options.bindingUrl`) activates the hosted
|
|
222
|
+
HTTP binding — see "Hosted binding" above. It requires a token
|
|
223
|
+
(`authToken`/`GUUEY_KV_TOKEN`/`context.token`) or throws
|
|
224
|
+
`InvalidContextError` rather than silently handing back a non-durable
|
|
225
|
+
in-memory store to a caller who explicitly asked for the durable one.
|
|
226
|
+
|
|
227
|
+
## Errors
|
|
228
|
+
|
|
229
|
+
Every operation that can fail throws a subclass of
|
|
230
|
+
`GuueyStateError`. Use `instanceof` to discriminate:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { kv, QuotaExceededError, InvalidKeyError } from "@guuey/state";
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await kv.set("big-blob", huge, { ttl: 3600 });
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (err instanceof QuotaExceededError) {
|
|
239
|
+
// Scope hit its 1 MiB ceiling. Tell the user.
|
|
240
|
+
} else if (err instanceof InvalidKeyError) {
|
|
241
|
+
// Bug in the calling code.
|
|
242
|
+
} else {
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Codes: `QUOTA_EXCEEDED`, `VALUE_TOO_LARGE`, `INVALID_KEY`,
|
|
249
|
+
`INVALID_TTL`, `INVALID_ARGUMENT`, `INVALID_CONTEXT`,
|
|
250
|
+
`TYPE_MISMATCH`, `MISSING_CONTEXT`, `TRANSPORT`.
|
|
251
|
+
|
|
252
|
+
## What this library is NOT
|
|
253
|
+
|
|
254
|
+
- **Not a database.** No queries, joins, transactions across keys,
|
|
255
|
+
or secondary indexes.
|
|
256
|
+
- **Not durable beyond 90 days.** Longer-lived storage is the user's
|
|
257
|
+
own SaaS via mcp-proxy.
|
|
258
|
+
- **Not cross-MCP.** Two MCPs cannot share data via `@guuey/state`
|
|
259
|
+
even for the same user — by design.
|
|
260
|
+
- **Not for storing the agent's chat history** — the platform's
|
|
261
|
+
managed conversation history already handles that.
|
|
262
|
+
|
|
263
|
+
## Status
|
|
264
|
+
|
|
265
|
+
🧪 **Developer preview (`0.x`).** The API surface, error shapes, and
|
|
266
|
+
hard caps are **locked and enforced** — code written against this
|
|
267
|
+
package today keeps working unchanged when the hosted binding ships.
|
|
268
|
+
What exists vs. what's coming:
|
|
269
|
+
|
|
270
|
+
| Piece | Status |
|
|
271
|
+
| ------------------------------------------- | -------------------------------------- |
|
|
272
|
+
| `Kv` API, typed errors, context middleware | ✅ locked + tested |
|
|
273
|
+
| Cap enforcement (scope/value/TTL/key rules) | ✅ enforced in every binding |
|
|
274
|
+
| In-memory binding (dev, tests) | ✅ shipped |
|
|
275
|
+
| Hosted binding (durable, cross-pod) | ✅ shipped (dev gate green 2026-07-27) |
|
|
276
|
+
| Console export + delete (data ownership) | ✅ shipped with the hosted binding |
|
|
277
|
+
|
|
278
|
+
The hosted-binding client (`HttpKv`) and its server counterpart already
|
|
279
|
+
ship — the 🔜 rows track **per-environment availability** (whether
|
|
280
|
+
`GUUEY_KV_URL` points at a live gateway in your environment), not
|
|
281
|
+
whether the code exists.
|
|
282
|
+
|
|
283
|
+
Until the hosted binding is live in your environment, state is
|
|
284
|
+
per-pod and non-durable — design your MCP so that losing this state
|
|
285
|
+
is an inconvenience (re-auth, cache miss), never data loss.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ScopeContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reject unusable scope ids at bind time. Scope ids are
|
|
4
|
+
* platform-issued opaque identifiers (Cognito subs, app ids) —
|
|
5
|
+
* whitespace or control characters in one is always a wiring bug,
|
|
6
|
+
* and catching it here beats a scope-ambiguity bug in storage.
|
|
7
|
+
* Shared by `withGuueyContext` and `createGuueyState` so both
|
|
8
|
+
* entry points enforce the same contract.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateContext(context: ScopeContext): void;
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` with the given scope context bound to AsyncLocalStorage.
|
|
13
|
+
* Nested calls override the outer context for their lifetime.
|
|
14
|
+
*
|
|
15
|
+
* The MCP server frameworks (express/fastify/hono middleware,
|
|
16
|
+
* `@modelcontextprotocol/sdk` transports) call this once per
|
|
17
|
+
* incoming request after extracting the user/mcp ids from headers
|
|
18
|
+
* or the SDK's session context.
|
|
19
|
+
*/
|
|
20
|
+
export declare function withGuueyContext<T>(context: ScopeContext, fn: () => T | Promise<T>): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* Current scope context, or `undefined` if none is bound. The
|
|
23
|
+
* library's barrel-exported `kv` calls this on every operation
|
|
24
|
+
* and throws `MissingContextError` when it returns `undefined`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getCurrentContext(): ScopeContext | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Derive a ScopeContext from the Authorization header guuey sent this
|
|
29
|
+
* MCP server. Decodes WITHOUT verifying — the KV API is the verifier;
|
|
30
|
+
* this is DX so `withGuueyContext(scopeFromAuthorization(h), fn)` is
|
|
31
|
+
* one line. mcpId = "mcp_" + first 32 hex of sha256(canonical aud URL),
|
|
32
|
+
* identical to the server derivation (auth.ts — keep in sync).
|
|
33
|
+
*/
|
|
34
|
+
export declare function scopeFromAuthorization(header: string): ScopeContext;
|
|
35
|
+
/** Canonicalize + hash a resource URL into a scope-safe mcpId. */
|
|
36
|
+
export declare function mcpIdFromResourceUrl(url: string): string;
|
|
37
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/C;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAgB3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,OAAO,EAAE,YAAY,EACrB,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,CAAC,CAAC,CAWZ;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,YAAY,GAAG,SAAS,CAE5D;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAuBnE;AAED,kEAAkE;AAClE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIxD"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AsyncLocalStorage-based scope context.
|
|
3
|
+
*
|
|
4
|
+
* Two ways to feed a `ScopeContext` to the library:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Explicit per-call** — pass `{ context }` to
|
|
7
|
+
* `createGuueyState`. Best for tests, scripts, and code that
|
|
8
|
+
* runs outside an HTTP request lifecycle.
|
|
9
|
+
*
|
|
10
|
+
* 2. **Implicit via AsyncLocalStorage** — wrap the request
|
|
11
|
+
* handler in `withGuueyContext` and use the barrel-exported
|
|
12
|
+
* `kv` (or call `getCurrentContext()` directly). This is
|
|
13
|
+
* what guuey-hosted MCP servers do: a middleware reads
|
|
14
|
+
* `X-Guuey-User-Id` + `X-Guuey-Mcp-Id` headers per request
|
|
15
|
+
* and calls `withGuueyContext(...)` for the handler body.
|
|
16
|
+
*
|
|
17
|
+
* Why AsyncLocalStorage and not a plain module-level variable:
|
|
18
|
+
* concurrent requests on the same Node.js process can be in
|
|
19
|
+
* different scopes simultaneously. ALS gives us per-async-chain
|
|
20
|
+
* isolation.
|
|
21
|
+
*/
|
|
22
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { InvalidContextError } from "./errors.js";
|
|
25
|
+
const storage = new AsyncLocalStorage();
|
|
26
|
+
/**
|
|
27
|
+
* Reject unusable scope ids at bind time. Scope ids are
|
|
28
|
+
* platform-issued opaque identifiers (Cognito subs, app ids) —
|
|
29
|
+
* whitespace or control characters in one is always a wiring bug,
|
|
30
|
+
* and catching it here beats a scope-ambiguity bug in storage.
|
|
31
|
+
* Shared by `withGuueyContext` and `createGuueyState` so both
|
|
32
|
+
* entry points enforce the same contract.
|
|
33
|
+
*/
|
|
34
|
+
export function validateContext(context) {
|
|
35
|
+
for (const field of ["userId", "mcpId"]) {
|
|
36
|
+
const value = context[field];
|
|
37
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
38
|
+
throw new InvalidContextError(field, "must be a non-empty string");
|
|
39
|
+
}
|
|
40
|
+
if (/[\s\p{Cc}]/u.test(value)) {
|
|
41
|
+
throw new InvalidContextError(field, "must not contain whitespace or control characters");
|
|
42
|
+
}
|
|
43
|
+
if (value.length > 256) {
|
|
44
|
+
throw new InvalidContextError(field, "must be <= 256 characters");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Run `fn` with the given scope context bound to AsyncLocalStorage.
|
|
50
|
+
* Nested calls override the outer context for their lifetime.
|
|
51
|
+
*
|
|
52
|
+
* The MCP server frameworks (express/fastify/hono middleware,
|
|
53
|
+
* `@modelcontextprotocol/sdk` transports) call this once per
|
|
54
|
+
* incoming request after extracting the user/mcp ids from headers
|
|
55
|
+
* or the SDK's session context.
|
|
56
|
+
*/
|
|
57
|
+
export function withGuueyContext(context, fn) {
|
|
58
|
+
// Validation failures surface as a rejected promise (matching the
|
|
59
|
+
// return type), never a synchronous throw. `storage.run` itself must
|
|
60
|
+
// stay synchronous — AsyncLocalStorage binds the context only for
|
|
61
|
+
// the synchronous extent of `run`, and `fn` starts inside it.
|
|
62
|
+
try {
|
|
63
|
+
validateContext(context);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return Promise.reject(err);
|
|
67
|
+
}
|
|
68
|
+
return Promise.resolve(storage.run(context, fn));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Current scope context, or `undefined` if none is bound. The
|
|
72
|
+
* library's barrel-exported `kv` calls this on every operation
|
|
73
|
+
* and throws `MissingContextError` when it returns `undefined`.
|
|
74
|
+
*/
|
|
75
|
+
export function getCurrentContext() {
|
|
76
|
+
return storage.getStore();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Derive a ScopeContext from the Authorization header guuey sent this
|
|
80
|
+
* MCP server. Decodes WITHOUT verifying — the KV API is the verifier;
|
|
81
|
+
* this is DX so `withGuueyContext(scopeFromAuthorization(h), fn)` is
|
|
82
|
+
* one line. mcpId = "mcp_" + first 32 hex of sha256(canonical aud URL),
|
|
83
|
+
* identical to the server derivation (auth.ts — keep in sync).
|
|
84
|
+
*/
|
|
85
|
+
export function scopeFromAuthorization(header) {
|
|
86
|
+
const m = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
87
|
+
if (!m)
|
|
88
|
+
throw new InvalidContextError("token", "not a Bearer authorization header");
|
|
89
|
+
const token = m[1] ?? "";
|
|
90
|
+
const parts = token.split(".");
|
|
91
|
+
if (parts.length !== 3 || parts[1] === undefined) {
|
|
92
|
+
throw new InvalidContextError("token", "malformed JWT");
|
|
93
|
+
}
|
|
94
|
+
let payload;
|
|
95
|
+
try {
|
|
96
|
+
payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new InvalidContextError("token", "JWT payload is not valid JSON");
|
|
100
|
+
}
|
|
101
|
+
const aud = Array.isArray(payload.aud) ? payload.aud[0] : payload.aud;
|
|
102
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0)
|
|
103
|
+
throw new InvalidContextError("userId", "JWT has no sub claim");
|
|
104
|
+
if (typeof aud !== "string" || aud.length === 0)
|
|
105
|
+
throw new InvalidContextError("mcpId", "JWT has no aud claim");
|
|
106
|
+
return { userId: payload.sub, mcpId: mcpIdFromResourceUrl(aud), token };
|
|
107
|
+
}
|
|
108
|
+
/** Canonicalize + hash a resource URL into a scope-safe mcpId. */
|
|
109
|
+
export function mcpIdFromResourceUrl(url) {
|
|
110
|
+
const u = new URL(url);
|
|
111
|
+
const canonical = `${u.protocol.toLowerCase()}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
|
|
112
|
+
return `mcp_${createHash("sha256").update(canonical).digest("hex").slice(0, 32)}`;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error hierarchy for `@guuey/state`. Every operation that
|
|
3
|
+
* can fail throws a subclass of `GuueyStateError`, so call sites
|
|
4
|
+
* can `instanceof`-discriminate without parsing message strings.
|
|
5
|
+
*/
|
|
6
|
+
export declare class GuueyStateError extends Error {
|
|
7
|
+
/** Stable, machine-readable error code. */
|
|
8
|
+
readonly code: string;
|
|
9
|
+
constructor(code: string, message: string, options?: {
|
|
10
|
+
cause?: unknown;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The scope is at or above its byte limit. Thrown by `set` and
|
|
15
|
+
* `increment` when the write would push usage past the cap.
|
|
16
|
+
* Includes the live `usedBytes` + `limitBytes` so the caller can
|
|
17
|
+
* surface a meaningful error to the user.
|
|
18
|
+
*/
|
|
19
|
+
export declare class QuotaExceededError extends GuueyStateError {
|
|
20
|
+
readonly usedBytes: number;
|
|
21
|
+
readonly limitBytes: number;
|
|
22
|
+
constructor(usedBytes: number, limitBytes: number);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A single value exceeds the per-value cap. Today: 64 KiB.
|
|
26
|
+
* Large blobs belong in `@guuey/files` (planned), not in KV.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ValueTooLargeError extends GuueyStateError {
|
|
29
|
+
readonly valueBytes: number;
|
|
30
|
+
readonly limitBytes: number;
|
|
31
|
+
constructor(valueBytes: number, limitBytes: number);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Key string is too long or contains a forbidden character. Today:
|
|
35
|
+
* 1 KiB max length; ASCII letters, digits, and `_.:-/` only
|
|
36
|
+
* (the standard "URL-safe-ish" set). Keeps keys cheap to log + index.
|
|
37
|
+
*/
|
|
38
|
+
export declare class InvalidKeyError extends GuueyStateError {
|
|
39
|
+
readonly key: string;
|
|
40
|
+
constructor(key: string, reason: string);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* TTL is missing, zero, negative, or above the 90-day cap.
|
|
44
|
+
*/
|
|
45
|
+
export declare class InvalidTtlError extends GuueyStateError {
|
|
46
|
+
readonly ttl: unknown;
|
|
47
|
+
constructor(ttl: unknown, reason: string);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The library was called without a `ScopeContext` — either no
|
|
51
|
+
* `createGuueyState({ context })` AND no surrounding
|
|
52
|
+
* `withGuueyContext(...)` block. Common in unit tests that import
|
|
53
|
+
* `kv` from the barrel without setting up context.
|
|
54
|
+
*/
|
|
55
|
+
export declare class MissingContextError extends GuueyStateError {
|
|
56
|
+
constructor();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `increment`/`decrement` was called on a key whose stored value is
|
|
60
|
+
* not a number. Counter ops require number-typed keys; mixing a
|
|
61
|
+
* counter and a JSON value under one key is a calling-code bug.
|
|
62
|
+
*/
|
|
63
|
+
export declare class TypeMismatchError extends GuueyStateError {
|
|
64
|
+
readonly key: string;
|
|
65
|
+
readonly actualType: string;
|
|
66
|
+
constructor(key: string, actualType: string);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A per-call argument is unusable — a `keys()` limit outside
|
|
70
|
+
* 1..1000, an `mget` batch over 100 keys, a non-integer counter
|
|
71
|
+
* step, or a value that isn't JSON-serializable (top-level
|
|
72
|
+
* `undefined`/function/symbol, `BigInt`, circular structure).
|
|
73
|
+
* Distinct from `InvalidKeyError`/`InvalidTtlError`, which cover the
|
|
74
|
+
* key and TTL contracts specifically.
|
|
75
|
+
*/
|
|
76
|
+
export declare class InvalidArgumentError extends GuueyStateError {
|
|
77
|
+
constructor(reason: string);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A `ScopeContext` field is unusable — an id that is empty or contains
|
|
81
|
+
* whitespace/control characters, or a missing `token` when the hosted
|
|
82
|
+
* binding was explicitly requested. Scope ids are platform-issued
|
|
83
|
+
* opaque identifiers (Cognito subs, app ids); anything with
|
|
84
|
+
* whitespace in it is a wiring bug at the call site, and the
|
|
85
|
+
* storage layer refuses it rather than risking scope ambiguity.
|
|
86
|
+
*/
|
|
87
|
+
export declare class InvalidContextError extends GuueyStateError {
|
|
88
|
+
readonly field: "userId" | "mcpId" | "token";
|
|
89
|
+
constructor(field: "userId" | "mcpId" | "token", reason: string);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The KV binding's transport call failed (HTTP 5xx, network blip,
|
|
93
|
+
* timeout). Includes the underlying cause for diagnostics. The
|
|
94
|
+
* caller may safely retry idempotent reads.
|
|
95
|
+
*/
|
|
96
|
+
export declare class TransportError extends GuueyStateError {
|
|
97
|
+
constructor(message: string, cause?: unknown);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKzE;AAED;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,eAAe;IACrD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAWlD;AAED;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,eAAe;IACrD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAUnD;AAED;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,eAAe;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBAET,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAOxC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,eAAe;IAClD,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;gBAEV,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM;CAKzC;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,eAAe;;CAUvD;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,eAAe;IACpD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAU5C;AAED;;;;;;;GAOG;AACH,qBAAa,oBAAqB,SAAQ,eAAe;gBAC3C,MAAM,EAAE,MAAM;CAI3B;AAED;;;;;;;GAOG;AACH,qBAAa,mBAAoB,SAAQ,eAAe;IACtD,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;gBAEjC,KAAK,EAAE,QAAQ,GAAG,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM;CAKhE;AAED;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,eAAe;gBACrC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C"}
|