@guuey/threads 0.7.2 → 0.8.1

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/README.md CHANGED
@@ -13,8 +13,9 @@ harnesses — share it:
13
13
  latest-replace snapshot), and the prompt-lane history projection.
14
14
  - **`ThreadPersistencePort`** — the narrow surface a binding implements.
15
15
  `InMemoryThreadPersistence` ships in the box (dev, tests, CI); guuey's
16
- hosted runtime binds DynamoDB; implement the port against your own store
17
- for ejected deployments.
16
+ hosted runtime binds DynamoDB; `HttpThreadPersistence` points a
17
+ self-hosted agent at guuey's hosted thread API ("eject the code, keep your
18
+ memory"); or implement the port against your own store.
18
19
  - **fold ↔ row mapping** — `agMessageToRow`, `reassembleFold`,
19
20
  `uiCardArtifactsFromMessages` and friends: byte-identity persistence of an
20
21
  `@silverprotocol/core` `AgReduceResult`, including the projection that
@@ -48,3 +49,32 @@ const history = await store.loadHistory(threadId);
48
49
  import { runThreadPersistenceContractSuite } from "@guuey/threads/testing";
49
50
  runThreadPersistenceContractSuite("MyBinding", async () => ({ port: makeMyPort() }));
50
51
  ```
52
+
53
+ ## Hosted binding — keep your memory after ejecting
54
+
55
+ `HttpThreadPersistence` implements the same port over guuey's thread API, so
56
+ an agent you run yourself reads and writes the SAME conversation rows the
57
+ hosted runtime does. It is scoped to one app and one end-user: pass the
58
+ end-user's bearer token — one your app's configured identity issuer minted
59
+ (your own IdP, or guuey's per-app widget issuer via `@guuey/widget-auth`).
60
+ The server verifies it against that issuer, derives the same end-user id the
61
+ hosted runtime uses, and confines every op to `(app, user)`.
62
+
63
+ ```ts
64
+ import { HttpThreadPersistence, ThreadStore } from "@guuey/threads";
65
+
66
+ const appId = process.env.GUUEY_APP_ID!;
67
+ const port = new HttpThreadPersistence({
68
+ baseUrl: "https://api.us-east-1.guuey.com",
69
+ appId,
70
+ token: endUserToken, // per request — the end-user's own token
71
+ });
72
+ const { userId, region } = await port.scope(); // what the token resolves to
73
+ const store = new ThreadStore(port);
74
+ const threadId = await store.ensureThread({ threadId: clientThreadId, userId, appId, region });
75
+ ```
76
+
77
+ Requires the app to run identified end-users (`userAuthMode: "byo"`); guests
78
+ and Cognito sessions are served on-platform only. Errors surface as
79
+ `HttpThreadStoreError` (`status`, `code`). Reads retry once on a network
80
+ failure or 5xx; writes never do.
package/dist/http.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Hosted HTTP binding — {@link ThreadPersistencePort} over guuey's thread
3
+ * API, `POST <base>/v1/thread-store/<op>` (guuey#208). The "eject the code,
4
+ * keep your memory" leg of the ejected ladder: point a `ThreadStore` at
5
+ * this binding with an end-user token and your self-hosted agent reads and
6
+ * writes the SAME conversation rows guuey's hosted runtime does.
7
+ *
8
+ * Wire protocol (the `@guuey/state` `HttpKv` shape):
9
+ * - Request: JSON body `{ context: { appId }, args: {...} }`, header
10
+ * `Authorization: Bearer <token>`.
11
+ * - Success: `200 { result }`.
12
+ * - Failure: `4xx/5xx { code, message }` → {@link HttpThreadStoreError}.
13
+ *
14
+ * Auth + tenancy: the token is an END-USER token for the app — one the
15
+ * app's configured identity issuer minted (your own IdP, or guuey's per-app
16
+ * widget issuer via `@guuey/widget-auth`). The server verifies it against
17
+ * that issuer, derives the same `byo_…` userId the hosted runtime would, and
18
+ * scopes every op to `(appId, userId)`: rows you write must carry that
19
+ * userId, threads you touch must belong to it. `context` on the wire is
20
+ * advisory — the token is authoritative. Call {@link scope} to learn the
21
+ * derived userId (and the API's region) before `ensureThread`.
22
+ *
23
+ * Retry policy: reads (`getThread`/`listRecentMessages`/
24
+ * `findByClientMessageId`/`getSnapshot`/`scope`) are idempotent and retried
25
+ * once on a network failure or a 5xx. Writes (`createThread`/`incrementSeq`/
26
+ * `putMessage`/`putSnapshot`) are NEVER retried automatically —
27
+ * `incrementSeq` is not idempotent, and a blind retry after a server-side
28
+ * apply would double-allocate a seq.
29
+ */
30
+ import type { ThreadMessageRow, ThreadPersistencePort, ThreadRow, ThreadSnapshotRow } from "./rows.js";
31
+ /** What the token resolves to server-side — the scope every op is bound to. */
32
+ export interface ThreadScope {
33
+ appId: string;
34
+ /** The derived end-user id (`byo_…`) — the `userId` to build rows with. */
35
+ userId: string;
36
+ /** The API's serving region — the `region` to mint fresh threads with. */
37
+ region: string;
38
+ }
39
+ export interface HttpThreadPersistenceOptions {
40
+ /** API origin, e.g. `https://api.us-east-1.guuey.com` (no `/v1`). */
41
+ baseUrl: string;
42
+ /** The guuey app whose conversations these are. */
43
+ appId: string;
44
+ /** The end-user's bearer token (see the module doc). */
45
+ token: string;
46
+ /** Injection seam for tests / custom transports. Defaults to global `fetch`. */
47
+ fetchImpl?: typeof fetch;
48
+ }
49
+ /**
50
+ * Thrown for every non-2xx response and every exhausted network failure.
51
+ * `code` is the server's flat error code (`UNAUTHORIZED`, `FORBIDDEN`,
52
+ * `INVALID_ARGUMENT`, `INVALID_CONTEXT`, `THREAD_NOT_FOUND`, `CONFLICT`,
53
+ * `NOT_FOUND`, `TRANSPORT`); `status` is the HTTP status (`0` for a network
54
+ * failure that never produced a response).
55
+ */
56
+ export declare class HttpThreadStoreError extends Error {
57
+ readonly status: number;
58
+ readonly code: string;
59
+ constructor(status: number, code: string, message: string, options?: {
60
+ cause?: unknown;
61
+ });
62
+ }
63
+ export declare class HttpThreadPersistence implements ThreadPersistencePort {
64
+ private readonly baseUrl;
65
+ private readonly appId;
66
+ private readonly token;
67
+ private readonly fetchImpl;
68
+ constructor(opts: HttpThreadPersistenceOptions);
69
+ /** The `(appId, userId, region)` the server bound this token to. */
70
+ scope(): Promise<ThreadScope>;
71
+ getThread(threadId: string): Promise<ThreadRow | undefined>;
72
+ createThread(row: ThreadRow): Promise<void>;
73
+ incrementSeq(threadId: string, preview: string | null, atIso: string): Promise<number>;
74
+ putMessage(row: ThreadMessageRow): Promise<void>;
75
+ listRecentMessages(threadId: string, limit: number): Promise<ThreadMessageRow[]>;
76
+ findByClientMessageId(threadId: string, clientMessageId: string): Promise<ThreadMessageRow | undefined>;
77
+ getSnapshot(threadId: string): Promise<ThreadSnapshotRow | undefined>;
78
+ putSnapshot(row: ThreadSnapshotRow): Promise<void>;
79
+ /**
80
+ * One shared retry budget (`retried`) covers BOTH retry triggers — a
81
+ * network failure and a 5xx response — so a retryable op makes at most
82
+ * 2 total requests, never 3.
83
+ */
84
+ private call;
85
+ }
86
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EACV,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAEnB,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,4BAA4B;IAC3C,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAaD;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKhC;AAgBD,qBAAa,qBAAsB,YAAW,qBAAqB;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,IAAI,EAAE,4BAA4B;IAO9C,oEAAoE;IAC9D,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;IAI7B,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAI3D,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAItF,UAAU,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhD,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAIhF,qBAAqB,CACzB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAUlC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIrE,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxD;;;;OAIG;YACW,IAAI;CAqCnB"}
package/dist/http.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Thrown for every non-2xx response and every exhausted network failure.
3
+ * `code` is the server's flat error code (`UNAUTHORIZED`, `FORBIDDEN`,
4
+ * `INVALID_ARGUMENT`, `INVALID_CONTEXT`, `THREAD_NOT_FOUND`, `CONFLICT`,
5
+ * `NOT_FOUND`, `TRANSPORT`); `status` is the HTTP status (`0` for a network
6
+ * failure that never produced a response).
7
+ */
8
+ export class HttpThreadStoreError extends Error {
9
+ status;
10
+ code;
11
+ constructor(status, code, message, options) {
12
+ super(message, options);
13
+ this.status = status;
14
+ this.code = code;
15
+ this.name = "HttpThreadStoreError";
16
+ }
17
+ }
18
+ async function toError(res) {
19
+ let body;
20
+ try {
21
+ body = (await res.json());
22
+ }
23
+ catch {
24
+ body = {};
25
+ }
26
+ return new HttpThreadStoreError(res.status, body.code ?? "TRANSPORT", body.message ?? `guuey thread API error (HTTP ${res.status})`);
27
+ }
28
+ export class HttpThreadPersistence {
29
+ baseUrl;
30
+ appId;
31
+ token;
32
+ fetchImpl;
33
+ constructor(opts) {
34
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
35
+ this.appId = opts.appId;
36
+ this.token = opts.token;
37
+ this.fetchImpl = opts.fetchImpl ?? fetch;
38
+ }
39
+ /** The `(appId, userId, region)` the server bound this token to. */
40
+ async scope() {
41
+ return this.call("scope", {}, true);
42
+ }
43
+ async getThread(threadId) {
44
+ return (await this.call("getThread", { threadId }, true)) ?? undefined;
45
+ }
46
+ async createThread(row) {
47
+ await this.call("createThread", { row }, false);
48
+ }
49
+ async incrementSeq(threadId, preview, atIso) {
50
+ return this.call("incrementSeq", { threadId, preview, atIso }, false);
51
+ }
52
+ async putMessage(row) {
53
+ await this.call("putMessage", { row }, false);
54
+ }
55
+ async listRecentMessages(threadId, limit) {
56
+ return this.call("listRecentMessages", { threadId, limit }, true);
57
+ }
58
+ async findByClientMessageId(threadId, clientMessageId) {
59
+ return ((await this.call("findByClientMessageId", { threadId, clientMessageId }, true)) ?? undefined);
60
+ }
61
+ async getSnapshot(threadId) {
62
+ return (await this.call("getSnapshot", { threadId }, true)) ?? undefined;
63
+ }
64
+ async putSnapshot(row) {
65
+ await this.call("putSnapshot", { row }, false);
66
+ }
67
+ // ── internals ──────────────────────────────────────────────────────
68
+ /**
69
+ * One shared retry budget (`retried`) covers BOTH retry triggers — a
70
+ * network failure and a 5xx response — so a retryable op makes at most
71
+ * 2 total requests, never 3.
72
+ */
73
+ async call(op, args, retryable) {
74
+ const doOnce = async () => this.fetchImpl(`${this.baseUrl}/v1/thread-store/${op}`, {
75
+ method: "POST",
76
+ headers: {
77
+ "content-type": "application/json",
78
+ authorization: `Bearer ${this.token}`,
79
+ },
80
+ body: JSON.stringify({ context: { appId: this.appId }, args }),
81
+ });
82
+ let res;
83
+ let retried = false;
84
+ for (;;) {
85
+ try {
86
+ res = await doOnce();
87
+ }
88
+ catch (err) {
89
+ if (!retryable || retried) {
90
+ throw new HttpThreadStoreError(0, "TRANSPORT", `network failure calling guuey thread API${retried ? " (after retry)" : ""}`, { cause: err });
91
+ }
92
+ retried = true;
93
+ continue;
94
+ }
95
+ if (res.status >= 500 && retryable && !retried) {
96
+ retried = true;
97
+ continue;
98
+ }
99
+ break;
100
+ }
101
+ if (!res.ok)
102
+ throw await toError(res);
103
+ const body = (await res.json());
104
+ return body.result;
105
+ }
106
+ }
package/dist/index.d.ts CHANGED
@@ -8,7 +8,9 @@
8
8
  * (messages + card rows + snapshot), prompt-lane history.
9
9
  * - {@link ThreadPersistencePort} — the narrow surface a binding
10
10
  * implements. `InMemoryThreadPersistence` ships here; guuey's hosted
11
- * runtime binds DynamoDB; bring your own store for ejected agents.
11
+ * runtime binds DynamoDB; `HttpThreadPersistence` points an ejected
12
+ * agent at guuey's hosted thread API with an end-user token ("eject
13
+ * the code, keep your memory"); or bring your own store.
12
14
  * - fold↔row mapping — `agMessageToRow`/`reassembleFold`/friends, the
13
15
  * byte-identity persistence of an `AgReduceResult`, including the
14
16
  * UI-card projection (`uiCardArtifactsFromMessages`, guuey#86).
@@ -18,5 +20,6 @@
18
20
  export type { StoredHistoryMessage, ThreadMessageKind, ThreadMessageRole, ThreadMessageRow, ThreadPersistencePort, ThreadRow, ThreadSnapshotRow, } from "./rows.js";
19
21
  export { ThreadStore, type AppendFoldInput, type AppendFoldResult, type AppendMessageInput, type AppendMessageResult, type EnsureThreadInput, } from "./store.js";
20
22
  export { InMemoryThreadPersistence } from "./in-memory.js";
23
+ export { HttpThreadPersistence, HttpThreadStoreError, type HttpThreadPersistenceOptions, type ThreadScope, } from "./http.js";
21
24
  export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageText, reassembleFold, rowToAgMessage, seedEventsForReducer, uiCardArtifactsFromMessages, type RowCtx, } from "./fold-rows.js";
22
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,2BAA2B,EAC3B,KAAK,MAAM,GACZ,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,4BAA4B,EACjC,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,2BAA2B,EAC3B,KAAK,MAAM,GACZ,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { ThreadStore, } from "./store.js";
2
2
  export { InMemoryThreadPersistence } from "./in-memory.js";
3
+ export { HttpThreadPersistence, HttpThreadStoreError, } from "./http.js";
3
4
  export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageText, reassembleFold, rowToAgMessage, seedEventsForReducer, uiCardArtifactsFromMessages, } from "./fold-rows.js";
package/dist/store.d.ts CHANGED
@@ -69,6 +69,15 @@ export declare class ThreadStore {
69
69
  * client-chosen id, so squatting is impossible.
70
70
  */
71
71
  ensureThread(input: EnsureThreadInput): Promise<string>;
72
+ /**
73
+ * Whether `threadId` exists AND is owned by `userId` — the SAME ownership
74
+ * predicate `ensureThread` applies before honouring a client-replayed id,
75
+ * exposed for doors that must BIND a client-echoed thread to the verified
76
+ * caller without minting anything (the pod's consent-answer door binds a
77
+ * `once` grant to a thread this way — guuey#207). No existence oracle: a
78
+ * missing thread and another user's thread are the same `false`.
79
+ */
80
+ ownsThread(threadId: string, userId: string): Promise<boolean>;
72
81
  /** Prior messages for the thread (seq-ASC, capped to the most recent N). */
73
82
  loadHistory(threadId: string, limit?: number): Promise<StoredHistoryMessage[]>;
74
83
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EAEjB,qBAAqB,EAErB,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAOnB,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,iBAAiB,CAAC;IACxB,yEAAyE;IACzE,OAAO,EAAE,OAAO,CAAC;IACjB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kEAAkE;IAClE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,qBAAqB;IAEtD;;;;;;;;;;;;;OAaG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B7D,4EAA4E;IACtE,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAA8B,GACpC,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAqBlC;;;;OAIG;IACG,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA4B5E,+EAA+E;IACzE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAI3E;;;;;;;;;;;;;;;;;OAiBG;IACG,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CA8EpE"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EAEjB,qBAAqB,EAErB,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAOnB,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,iBAAiB,CAAC;IACxB,yEAAyE;IACzE,OAAO,EAAE,OAAO,CAAC;IACjB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kEAAkE;IAClE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,qBAAqB;IAEtD;;;;;;;;;;;;;OAaG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B7D;;;;;;;OAOG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKpE,4EAA4E;IACtE,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAA8B,GACpC,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAqBlC;;;;OAIG;IACG,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA4B5E,+EAA+E;IACzE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAI3E;;;;;;;;;;;;;;;;;OAiBG;IACG,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CA8EpE"}
package/dist/store.js CHANGED
@@ -61,6 +61,18 @@ export class ThreadStore {
61
61
  await this.db.createThread(row);
62
62
  return row.id;
63
63
  }
64
+ /**
65
+ * Whether `threadId` exists AND is owned by `userId` — the SAME ownership
66
+ * predicate `ensureThread` applies before honouring a client-replayed id,
67
+ * exposed for doors that must BIND a client-echoed thread to the verified
68
+ * caller without minting anything (the pod's consent-answer door binds a
69
+ * `once` grant to a thread this way — guuey#207). No existence oracle: a
70
+ * missing thread and another user's thread are the same `false`.
71
+ */
72
+ async ownsThread(threadId, userId) {
73
+ const existing = await this.db.getThread(threadId);
74
+ return existing !== undefined && existing.userId === userId;
75
+ }
64
76
  /** Prior messages for the thread (seq-ASC, capped to the most recent N). */
65
77
  async loadHistory(threadId, limit = DEFAULT_HISTORY_LIMIT) {
66
78
  const rows = await this.db.listRecentMessages(threadId, limit);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/threads",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "Universal session/thread persistence for AgJSON agents — the ThreadStore contract (append-fold, history, snapshots), the fold↔row mapping, and an in-memory binding. Guuey's hosted runtime is one binding; implement the port against your own store and run the exported contract suite for the same guarantees.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@silverprotocol/core": "0.5.0",
26
- "@guuey/mcp-apps-host": "0.7.2"
26
+ "@guuey/mcp-apps-host": "0.8.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.0.0",