@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 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"}
@@ -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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"}
@@ -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"}