@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/dist/types.d.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for `@guuey/state`.
|
|
3
|
+
*
|
|
4
|
+
* The library is intentionally narrow — it's a KV scoped per
|
|
5
|
+
* `(user, mcp)`, not a database. See `docs/principles/mcp-hosting-policy.md`
|
|
6
|
+
* for the rationale: scoped primitives let MCPs hold light state
|
|
7
|
+
* (idempotency, counters, small per-user prefs) without making guuey
|
|
8
|
+
* a backend-as-a-service. Hard caps are the product.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Bundle of identifiers that scopes every operation to one
|
|
12
|
+
* `(user, mcp)` namespace. The library never reads these from
|
|
13
|
+
* env or globals at call time; consumers either pass them
|
|
14
|
+
* explicitly to `createGuueyState` or set them via
|
|
15
|
+
* `withGuueyContext` (see `./context`).
|
|
16
|
+
*
|
|
17
|
+
* `userId` is the end-user's guuey identity (Cognito sub, or the
|
|
18
|
+
* platform-derived id for BYO-auth users). `mcpId` is the MCP
|
|
19
|
+
* server's stable identifier (the deploy's app id).
|
|
20
|
+
*
|
|
21
|
+
* **Trust model (spec §3):** the hosted CLIENT binding (`HttpKv`)
|
|
22
|
+
* and the server (`stateApi`, the actual verifier) both ship. The
|
|
23
|
+
* server authenticates the federation-minted Bearer JWT and
|
|
24
|
+
* derives `userId` (the `sub` claim) + `mcpId` (hash of the
|
|
25
|
+
* canonicalized `aud` URL, see `mcpIdFromResourceUrl`)
|
|
26
|
+
* AUTHORITATIVELY server-side — the `context` this package puts on
|
|
27
|
+
* the wire is advisory-must-match, never trusted on its own. An MCP
|
|
28
|
+
* can only present tokens guuey itself issued it, so a compromised
|
|
29
|
+
* MCP's blast radius is its own app's scopes, across its own calling
|
|
30
|
+
* users — never another app's. Tokens minted for guest sessions
|
|
31
|
+
* (`issuerAuthMode: 'anonymous'`) are rejected: guests get no
|
|
32
|
+
* durable scope (mirrors the GuueyFS rule). `ScopeContext` itself
|
|
33
|
+
* stays caller-asserted at the type level — `scopeFromAuthorization`
|
|
34
|
+
* decodes without verifying, same as the in-memory binding always
|
|
35
|
+
* has; verification is the server's job, not this package's.
|
|
36
|
+
*/
|
|
37
|
+
export interface ScopeContext {
|
|
38
|
+
readonly userId: string;
|
|
39
|
+
readonly mcpId: string;
|
|
40
|
+
/**
|
|
41
|
+
* The inbound Bearer JWT guuey sent this MCP server (hosted binding
|
|
42
|
+
* only; in-memory ignores it). Obtain via `scopeFromAuthorization`.
|
|
43
|
+
*/
|
|
44
|
+
readonly token?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-operation options for `set`. TTL is REQUIRED at the type
|
|
48
|
+
* level — there is no "permanent" key in guuey-scoped KV. If
|
|
49
|
+
* the dev wants persistence, they pick a long TTL explicitly.
|
|
50
|
+
* Keeps the platform's storage costs bounded and forces the
|
|
51
|
+
* dev to think about expiry.
|
|
52
|
+
*
|
|
53
|
+
* `ttl` is in seconds. Hard cap: 90 days (60 * 60 * 24 * 90).
|
|
54
|
+
* Longer-lived data should live in the user's own SaaS via
|
|
55
|
+
* mcp-proxy credential brokering — that pattern is preferred
|
|
56
|
+
* for any "meaningful user data" anyway (best privacy posture).
|
|
57
|
+
*/
|
|
58
|
+
export interface SetOptions {
|
|
59
|
+
/** TTL in seconds. Max 90 days. */
|
|
60
|
+
readonly ttl: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Per-key counter options. Atomic increment is the one
|
|
64
|
+
* "transaction-like" operation the API offers — no MULTI/EXEC,
|
|
65
|
+
* no CAS on arbitrary values. Counters cover the common case
|
|
66
|
+
* (rate limits, generated-id sequences) without opening the door
|
|
67
|
+
* to "build any app on guuey."
|
|
68
|
+
*/
|
|
69
|
+
export interface IncrementOptions {
|
|
70
|
+
/** Amount to add (default 1). May be negative. */
|
|
71
|
+
readonly by?: number;
|
|
72
|
+
/** TTL in seconds for the (possibly fresh) counter key. */
|
|
73
|
+
readonly ttl: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One page of a `keys()` listing. `cursor` is present when more
|
|
77
|
+
* keys remain — pass it back to `keys()` to continue. Treat it as
|
|
78
|
+
* an opaque token (its format may differ between bindings).
|
|
79
|
+
*/
|
|
80
|
+
export interface KeysPage {
|
|
81
|
+
readonly keys: string[];
|
|
82
|
+
readonly cursor?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Snapshot of the current scope's storage usage. Returned by
|
|
86
|
+
* `kv.scope()`. Cheap to call; pulled from the platform on each
|
|
87
|
+
* invocation (no client-side caching) so quota checks before
|
|
88
|
+
* a write reflect the live state.
|
|
89
|
+
*/
|
|
90
|
+
export interface ScopeInfo {
|
|
91
|
+
readonly userId: string;
|
|
92
|
+
readonly mcpId: string;
|
|
93
|
+
/**
|
|
94
|
+
* Bytes used in this `(user, mcp)` scope — UTF-8 bytes of every
|
|
95
|
+
* live key PLUS its JSON-encoded value. Keys count because they
|
|
96
|
+
* are storage too: a scope of 1 KiB keys with 1-byte values is
|
|
97
|
+
* not "empty".
|
|
98
|
+
*/
|
|
99
|
+
readonly usedBytes: number;
|
|
100
|
+
/** Hard cap for the scope. Today: 1 MiB. */
|
|
101
|
+
readonly limitBytes: number;
|
|
102
|
+
/** Distinct keys in this scope. */
|
|
103
|
+
readonly keyCount: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The KV interface every binding implements (in-memory for local
|
|
107
|
+
* dev, HTTP for hosted pods). Consumer code only ever sees this
|
|
108
|
+
* shape — they never touch the underlying transport.
|
|
109
|
+
*
|
|
110
|
+
* All methods are async even when they could be sync — keeps the
|
|
111
|
+
* shape stable across local-vs-hosted bindings and discourages
|
|
112
|
+
* "sync KV in the hot path" patterns that hide latency.
|
|
113
|
+
*
|
|
114
|
+
* Type parameter `T` on `get`/`set`: a structural hint, NOT a
|
|
115
|
+
* runtime check. The library JSON-serializes on write and parses
|
|
116
|
+
* on read. If the read shape doesn't match the type parameter,
|
|
117
|
+
* you get a wrongly-typed value with no warning. Pair with a
|
|
118
|
+
* schema (zod, valibot, …) at the call site if runtime safety
|
|
119
|
+
* matters.
|
|
120
|
+
*/
|
|
121
|
+
export interface Kv {
|
|
122
|
+
/** Read a key. Returns `undefined` if absent or expired. */
|
|
123
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
124
|
+
/**
|
|
125
|
+
* Write a key. TTL is required (no permanent keys).
|
|
126
|
+
*
|
|
127
|
+
* Values must be JSON-serializable: top-level `undefined`,
|
|
128
|
+
* functions, symbols, `BigInt`, and circular structures throw
|
|
129
|
+
* `InvalidArgumentError`. Standard JSON semantics otherwise
|
|
130
|
+
* apply — `NaN`/`Infinity` serialize as `null`, and nested
|
|
131
|
+
* `undefined`/function properties are dropped, exactly as
|
|
132
|
+
* `JSON.stringify` does.
|
|
133
|
+
*/
|
|
134
|
+
set<T = unknown>(key: string, value: T, opts: SetOptions): Promise<void>;
|
|
135
|
+
/** Delete a key. No-op if absent. */
|
|
136
|
+
delete(key: string): Promise<void>;
|
|
137
|
+
/** Cheap existence check without deserializing the value. */
|
|
138
|
+
has(key: string): Promise<boolean>;
|
|
139
|
+
/**
|
|
140
|
+
* List keys in this scope, optionally filtered by prefix, in
|
|
141
|
+
* lexicographic order. Pages of up to 1000 keys; pass the
|
|
142
|
+
* returned `cursor` back to continue. Use this for diagnostics
|
|
143
|
+
* and small housekeeping; do NOT use it as a query engine. A
|
|
144
|
+
* scope that needs to "find all keys matching pattern X across
|
|
145
|
+
* millions of entries" wants a real database (Case B in the
|
|
146
|
+
* hosting policy).
|
|
147
|
+
*/
|
|
148
|
+
keys(opts?: {
|
|
149
|
+
prefix?: string;
|
|
150
|
+
limit?: number;
|
|
151
|
+
/** Opaque continuation token from a previous page. */
|
|
152
|
+
cursor?: string;
|
|
153
|
+
}): Promise<KeysPage>;
|
|
154
|
+
/**
|
|
155
|
+
* Atomic increment. Creates the key (initialized to 0) if
|
|
156
|
+
* missing, then adds `by` (default 1) and returns the new
|
|
157
|
+
* value. Useful for rate-limit counters, generated-id
|
|
158
|
+
* sequences, simple analytics.
|
|
159
|
+
*
|
|
160
|
+
* Counters are integer-only: `by` must be a safe integer
|
|
161
|
+
* (`InvalidArgumentError` otherwise), and a key holding a
|
|
162
|
+
* non-number value throws `TypeMismatchError`.
|
|
163
|
+
*
|
|
164
|
+
* The TTL is reset on every increment to the value passed —
|
|
165
|
+
* idle counters expire naturally.
|
|
166
|
+
*/
|
|
167
|
+
increment(key: string, opts: IncrementOptions): Promise<number>;
|
|
168
|
+
/** Inverse of `increment`. Same semantics. */
|
|
169
|
+
decrement(key: string, opts: IncrementOptions): Promise<number>;
|
|
170
|
+
/**
|
|
171
|
+
* Bulk read. Missing keys map to `undefined`. Cap: 100 keys per
|
|
172
|
+
* call (`InvalidArgumentError` beyond) — batch if you need more.
|
|
173
|
+
*/
|
|
174
|
+
mget<T = unknown>(keys: string[]): Promise<Record<string, T | undefined>>;
|
|
175
|
+
/**
|
|
176
|
+
* Snapshot of current scope usage. Use this BEFORE a large
|
|
177
|
+
* write to fail fast if you're near the quota cap rather than
|
|
178
|
+
* eating a `QuotaExceededError` mid-batch.
|
|
179
|
+
*/
|
|
180
|
+
scope(): Promise<ScopeInfo>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Options for `createGuueyState`. Binding selection:
|
|
184
|
+
*
|
|
185
|
+
* 1. If `bindingUrl` (or the `GUUEY_KV_URL` env) is set → the hosted
|
|
186
|
+
* HTTP binding (`HttpKv`). Requires a token — see `authToken`
|
|
187
|
+
* below — or `createGuueyState` throws `InvalidContextError`
|
|
188
|
+
* rather than silently handing back a non-durable in-memory store
|
|
189
|
+
* to a caller who asked for the hosted one.
|
|
190
|
+
* 2. Otherwise → in-memory binding (one-time warning).
|
|
191
|
+
*
|
|
192
|
+
* The in-memory binding is right for unit tests + local development.
|
|
193
|
+
* guuey-hosted and colocated MCP servers get `GUUEY_KV_URL` injected
|
|
194
|
+
* at boot and pick up the hosted binding unchanged; a dev-hosted
|
|
195
|
+
* `external` server sets it itself. No token env var is injected —
|
|
196
|
+
* the auth token is the inbound federation Bearer JWT, obtained per
|
|
197
|
+
* request via `scopeFromAuthorization` (`context.token`).
|
|
198
|
+
*/
|
|
199
|
+
export interface CreateGuueyStateOptions {
|
|
200
|
+
/** Scope identity. Required. */
|
|
201
|
+
readonly context: ScopeContext;
|
|
202
|
+
/** Override the binding URL (defaults to `GUUEY_KV_URL` env). */
|
|
203
|
+
readonly bindingUrl?: string;
|
|
204
|
+
/**
|
|
205
|
+
* Override the auth token (defaults to `GUUEY_KV_TOKEN` env, then
|
|
206
|
+
* `context.token`). Normally you never set this: the token is the
|
|
207
|
+
* inbound federation Bearer JWT and arrives via
|
|
208
|
+
* `scopeFromAuthorization` as `context.token`. `GUUEY_KV_TOKEN` /
|
|
209
|
+
* `authToken` are manual escape hatches (scripts, tests) — nothing
|
|
210
|
+
* on the platform injects them. See the `ScopeContext` trust model:
|
|
211
|
+
* the hosted KV API derives the mcp scope from this token
|
|
212
|
+
* server-side rather than trusting the caller's `mcpId`. Overrides
|
|
213
|
+
* `context.token` when both are set.
|
|
214
|
+
*/
|
|
215
|
+
readonly authToken?: string;
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for `@guuey/state`.
|
|
3
|
+
*
|
|
4
|
+
* The library is intentionally narrow — it's a KV scoped per
|
|
5
|
+
* `(user, mcp)`, not a database. See `docs/principles/mcp-hosting-policy.md`
|
|
6
|
+
* for the rationale: scoped primitives let MCPs hold light state
|
|
7
|
+
* (idempotency, counters, small per-user prefs) without making guuey
|
|
8
|
+
* a backend-as-a-service. Hard caps are the product.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const SCOPE_LIMIT_BYTES: number;
|
|
2
|
+
export declare const VALUE_LIMIT_BYTES: number;
|
|
3
|
+
export declare const KEY_LIMIT_BYTES = 1024;
|
|
4
|
+
export declare const TTL_MAX_SECONDS: number;
|
|
5
|
+
export declare const MGET_LIMIT = 100;
|
|
6
|
+
export declare const VALID_KEY: RegExp;
|
|
7
|
+
export declare function validateKey(key: string): void;
|
|
8
|
+
export declare function validateTtl(ttl: number): void;
|
|
9
|
+
/**
|
|
10
|
+
* JSON-encode a value for storage, converting every serialization
|
|
11
|
+
* failure into the library's typed error. `JSON.stringify` returns
|
|
12
|
+
* the VALUE `undefined` (not a string) for top-level `undefined`,
|
|
13
|
+
* functions, and symbols, and throws natively for circular
|
|
14
|
+
* structures and `BigInt` — none of which may escape as a bare
|
|
15
|
+
* `TypeError` (the error contract promises `GuueyStateError`
|
|
16
|
+
* subclasses from every failable operation).
|
|
17
|
+
*/
|
|
18
|
+
export declare function encodeValue(key: string, value: unknown): string;
|
|
19
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,iBAAiB,QAAkB,CAAC;AACjD,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAC3C,eAAO,MAAM,eAAe,OAAO,CAAC;AACpC,eAAO,MAAM,eAAe,QAAoB,CAAC;AACjD,eAAO,MAAM,UAAU,MAAM,CAAC;AAC9B,eAAO,MAAM,SAAS,QAAyB,CAAC;AAEhD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAa7C;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAW7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAiB/D"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared key/TTL/value validation + cap constants.
|
|
3
|
+
*
|
|
4
|
+
* Both bindings (in-memory, hosted HTTP) enforce the exact same
|
|
5
|
+
* contract on keys/TTLs/values before touching storage — living here
|
|
6
|
+
* once means the two implementations can never drift on what counts
|
|
7
|
+
* as a valid key, TTL, or JSON-serializable value.
|
|
8
|
+
*/
|
|
9
|
+
import { InvalidArgumentError, InvalidKeyError, InvalidTtlError } from "./errors.js";
|
|
10
|
+
export const SCOPE_LIMIT_BYTES = 1 * 1024 * 1024; // 1 MiB
|
|
11
|
+
export const VALUE_LIMIT_BYTES = 64 * 1024; // 64 KiB
|
|
12
|
+
export const KEY_LIMIT_BYTES = 1024;
|
|
13
|
+
export const TTL_MAX_SECONDS = 60 * 60 * 24 * 90; // 90 days
|
|
14
|
+
export const MGET_LIMIT = 100;
|
|
15
|
+
export const VALID_KEY = /^[A-Za-z0-9_.:\-/]+$/;
|
|
16
|
+
export function validateKey(key) {
|
|
17
|
+
if (key.length === 0) {
|
|
18
|
+
throw new InvalidKeyError(key, "must be non-empty");
|
|
19
|
+
}
|
|
20
|
+
if (Buffer.byteLength(key, "utf8") > KEY_LIMIT_BYTES) {
|
|
21
|
+
throw new InvalidKeyError(key, `must be <= ${KEY_LIMIT_BYTES} bytes`);
|
|
22
|
+
}
|
|
23
|
+
if (!VALID_KEY.test(key)) {
|
|
24
|
+
throw new InvalidKeyError(key, "may only contain ASCII letters, digits, and `_.:-/`");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function validateTtl(ttl) {
|
|
28
|
+
if (!Number.isFinite(ttl) || ttl <= 0) {
|
|
29
|
+
throw new InvalidTtlError(ttl, "must be a positive finite number");
|
|
30
|
+
}
|
|
31
|
+
if (ttl > TTL_MAX_SECONDS) {
|
|
32
|
+
throw new InvalidTtlError(ttl, `must be <= ${TTL_MAX_SECONDS} seconds (90 days). ` +
|
|
33
|
+
`Long-lived data belongs in user-owned storage via mcp-proxy.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* JSON-encode a value for storage, converting every serialization
|
|
38
|
+
* failure into the library's typed error. `JSON.stringify` returns
|
|
39
|
+
* the VALUE `undefined` (not a string) for top-level `undefined`,
|
|
40
|
+
* functions, and symbols, and throws natively for circular
|
|
41
|
+
* structures and `BigInt` — none of which may escape as a bare
|
|
42
|
+
* `TypeError` (the error contract promises `GuueyStateError`
|
|
43
|
+
* subclasses from every failable operation).
|
|
44
|
+
*/
|
|
45
|
+
export function encodeValue(key, value) {
|
|
46
|
+
let json;
|
|
47
|
+
try {
|
|
48
|
+
json = JSON.stringify(value);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
throw new InvalidArgumentError(`value for key ${JSON.stringify(key)} is not JSON-serializable ` +
|
|
52
|
+
`(${err instanceof Error ? err.message : String(err)})`);
|
|
53
|
+
}
|
|
54
|
+
if (json === undefined) {
|
|
55
|
+
throw new InvalidArgumentError(`value for key ${JSON.stringify(key)} is not JSON-serializable ` +
|
|
56
|
+
`(top-level undefined, function, or symbol)`);
|
|
57
|
+
}
|
|
58
|
+
return json;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAErF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;AAC1D,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,SAAS;AACrD,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AACpC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,UAAU;AAC5D,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAC;AAC9B,MAAM,CAAC,MAAM,SAAS,GAAG,sBAAsB,CAAC;AAEhD,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,eAAe,EAAE,CAAC;QACrD,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,cAAc,eAAe,QAAQ,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,eAAe,CACvB,GAAG,EACH,qDAAqD,CACtD,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,kCAAkC,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,GAAG,GAAG,eAAe,EAAE,CAAC;QAC1B,MAAM,IAAI,eAAe,CACvB,GAAG,EACH,cAAc,eAAe,sBAAsB;YACjD,8DAA8D,CACjE,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,KAAc;IACrD,IAAI,IAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,oBAAoB,CAC5B,iBAAiB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,4BAA4B;YAC9D,IAAI,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAC1D,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,oBAAoB,CAC5B,iBAAiB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,4BAA4B;YAC9D,4CAA4C,CAC/C,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guuey/state",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open-source KV client for MCP servers hosted on guuey.com. Scoped per (user, mcp). The MCP stays stateless from its own POV while the user retains data ownership. Hard caps are the product.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./context": {
|
|
20
|
+
"types": "./dist/context.d.ts",
|
|
21
|
+
"import": "./dist/context.js"
|
|
22
|
+
},
|
|
23
|
+
"./errors": {
|
|
24
|
+
"types": "./dist/errors.d.ts",
|
|
25
|
+
"import": "./dist/errors.js"
|
|
26
|
+
},
|
|
27
|
+
"./testing": {
|
|
28
|
+
"types": "./dist/testing/contract-suite.d.ts",
|
|
29
|
+
"import": "./dist/testing/contract-suite.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"vitest": "^3.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"vitest": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"typescript": "^5.7.0",
|
|
44
|
+
"vitest": "^3.2.4"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"mcp",
|
|
51
|
+
"model-context-protocol",
|
|
52
|
+
"guuey",
|
|
53
|
+
"kv",
|
|
54
|
+
"key-value",
|
|
55
|
+
"storage"
|
|
56
|
+
],
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
60
|
+
"directory": "packages/state"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://guuey.com",
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/loqu-co/guuey/issues"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsc",
|
|
68
|
+
"dev": "tsc --watch",
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"test:watch": "vitest"
|
|
72
|
+
}
|
|
73
|
+
}
|