From 35070e7a9a631e92557014b8f5461177ced3ca24 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:27:38 +1000 Subject: [PATCH] feat(ai): add shared Scope identity type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a single identity/isolation vocabulary (Scope) in @tanstack/ai for the subsystems that persist or recall per-conversation data — ai-persistence and ai-memory. threadId is the one conversation key (matching ChatMiddlewareContext.threadId); userId/tenantId/namespace are optional. Every field is an isolation boundary derived server-side, never from client input. Additive precursor: nothing consumes Scope yet. Landed ahead of the persistence (#785) and memory (#541) PRs so both build on one settled identity contract instead of diverging (sessionId vs threadId, bare threadId vs flat scope:string). --- .changeset/shared-scope.md | 29 +++++++++++++++++++++++ packages/ai/src/index.ts | 3 +++ packages/ai/src/scope.ts | 47 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 .changeset/shared-scope.md create mode 100644 packages/ai/src/scope.ts diff --git a/.changeset/shared-scope.md b/.changeset/shared-scope.md new file mode 100644 index 000000000..fc71894c4 --- /dev/null +++ b/.changeset/shared-scope.md @@ -0,0 +1,29 @@ +--- +'@tanstack/ai': minor +--- + +**Add a shared `Scope` identity type to `@tanstack/ai`.** + +`Scope` is the single identity/isolation vocabulary for the subsystems that +persist or recall per-conversation data — `@tanstack/ai-persistence` and +`@tanstack/ai-memory`. Rather than each subsystem inventing its own notion of +"whose data is this?", both import one type: + +```ts +interface Scope { + threadId: string // required — the single conversation key (same as ctx.threadId) + userId?: string // durable end-user identity; required in practice for multi-user apps + tenantId?: string // multi-tenant boundary + namespace?: string // reserved logical partition; no subsystem keys on it yet +} +``` + +`threadId` is the one conversation key across the codebase (matching +`ChatMiddlewareContext.threadId`, with `conversationId` already deprecated in +favor of it) — subsystems must not introduce a second name (`sessionId`, …) for +the same concept. Every field is an isolation boundary and must be derived +server-side from trusted session state, never from client input. + +This is additive: nothing consumes `Scope` yet. It lands ahead of the +persistence and memory PRs so both build on one settled, unambiguous identity +contract instead of diverging. diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 16a07fede..737b0b053 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -166,6 +166,9 @@ export type { // All types export * from './types' +// Shared identity/isolation scope for the persistence + memory subsystems +export type { Scope } from './scope' + export { firstSentence, renderLazyCatalogEntry, diff --git a/packages/ai/src/scope.ts b/packages/ai/src/scope.ts new file mode 100644 index 000000000..599bc4b04 --- /dev/null +++ b/packages/ai/src/scope.ts @@ -0,0 +1,47 @@ +/** + * Shared identity/isolation scope for the TanStack AI subsystems that persist + * or recall per-conversation data — `@tanstack/ai-persistence` (keyed CRUD over + * threads, runs, interrupts) and `@tanstack/ai-memory` (ranked recall/save). + * + * Both subsystems answer the same underlying question — "whose data is this?" — + * so they share ONE identity vocabulary defined here rather than each inventing + * its own. `threadId` is the single conversation key across the codebase (it is + * `ChatMiddlewareContext.threadId`, and `conversationId` is deprecated in favor + * of it); a subsystem must never introduce a second name (`sessionId`, + * `conversationId`, …) for the same concept. + * + * ## Security + * + * A `Scope` is an isolation boundary. Derive every field server-side from + * trusted, validated session state — **never** from client input. `threadId` + * in particular resolves from a client-supplied value on the request, so a + * subsystem that reads/writes user-owned data (memory, per-user metadata) must + * pair it with a server-trusted `userId`/`tenantId` and must not treat a bare + * client `threadId` as sufficient isolation: thread ids are guessable, so on + * their own they let one caller read another's data. + */ +export interface Scope { + /** + * The conversation this data belongs to. Required — the minimal isolation + * key both subsystems already center on. Same concept as + * `ChatMiddlewareContext.threadId`. + */ + threadId: string + /** + * Durable end-user identity, for cross-thread recall and per-user isolation. + * Optional, but required in practice for any multi-user deployment — a + * `threadId` alone is not an authorization boundary (see Security above). + */ + userId?: string + /** + * Tenant/organization boundary for multi-tenant deployments. When present, + * every read and write must be confined to it. + */ + tenantId?: string + /** + * Logical partition within a tenant/user (e.g. separating distinct memory + * banks or persistence namespaces). Reserved — no subsystem keys on it yet; + * adapters that don't understand it must ignore it rather than error. + */ + namespace?: string +}