{"version":3,"sources":["../../../src/server/stores/drizzle/index.ts","../../../src/server/stores/drizzle/store.ts","../../../src/server/chat-store.ts","../../../src/server/stores/drizzle/client.ts","../../../src/server/stores/drizzle/schema.ts"],"sourcesContent":["/**\n * Default Drizzle/Postgres ChatStore — public entry.\n *\n * Imported via `@mordn/chat-widget/server/drizzle` so a BYO consumer who\n * passes their own `store` never pulls `postgres`/`drizzle-orm` into their\n * bundle. Consumers on the default path:\n *\n *   import { createDrizzleChatStore } from '@mordn/chat-widget/server/drizzle';\n *   createChatHandler({ store: createDrizzleChatStore(), ... });\n *\n * The schema is exported so `drizzle-kit` can generate/push migrations.\n */\nimport 'server-only';\n\nexport { createDrizzleChatStore } from './store';\nexport { getDrizzleDb, type DrizzleClientOptions, type DrizzleDb } from './client';\nexport * as schema from './schema';\nexport type {\n  ConversationRow,\n  NewConversationRow,\n  MessageRow,\n  NewMessageRow,\n} from './schema';\n","/**\n * Default ChatStore implementation, on Postgres via Drizzle.\n *\n * This is the \"hosted/default\" persistence the widget ships with. It is just\n * one implementation of the `ChatStore` interface — the interface, not this\n * file, is the contract. Every method here upholds the interface's security\n * invariants:\n *\n *   • The store is bound to one `userId` (constructor arg from the verified\n *     server session). No method takes a userId.\n *   • Reads are implicitly scoped to that user. `getConversation` /\n *     `listMessages` return null/[] for rows the user doesn't own — never\n *     another user's data, and not distinguishable from \"not found\".\n *   • Mutations verify ownership and throw `ConversationOwnershipError` on a\n *     foreign row.\n *   • `saveTurn` is idempotent on message id and bumps `updatedAt`.\n */\n\nimport 'server-only';\nimport { and, desc, eq, inArray, lt, or, sql } from 'drizzle-orm';\nimport { generateId, type UIMessage } from 'ai';\n\nimport {\n  ConversationOwnershipError,\n  type ChatStore,\n} from '../../chat-store';\nimport type {\n  ListMessagesOptions,\n  SaveTurnInput,\n  StoredConversation,\n  StoredMessage,\n} from '../../types';\nimport { getDrizzleDb, type DrizzleClientOptions, type DrizzleDb } from './client';\nimport { conversations, messages, type MessageRow, type ConversationRow } from './schema';\n\nconst MAX_PAGE = 100;\n\n/** Project the plain-text of a UIMessage's parts for the `text` column. */\nfunction textFromParts(parts: UIMessage['parts']): string {\n  if (!Array.isArray(parts)) return '';\n  return parts\n    .filter((p): p is { type: 'text'; text: string } =>\n      (p as { type?: string }).type === 'text' &&\n      typeof (p as { text?: unknown }).text === 'string',\n    )\n    .map((p) => p.text)\n    .join('')\n    .trim();\n}\n\nfunction toStoredConversation(row: ConversationRow, messageCount?: number): StoredConversation {\n  return {\n    id: row.id,\n    title: row.title,\n    metadata: row.metadata ?? null,\n    createdAt: row.createdAt,\n    updatedAt: row.updatedAt,\n    messageCount,\n  };\n}\n\nfunction toStoredMessage(row: MessageRow): StoredMessage {\n  return {\n    id: row.id,\n    role: row.role,\n    parts: row.parts,\n    text: row.text,\n    model: row.model ?? undefined,\n    createdAt: row.createdAt,\n  };\n}\n\nclass DrizzleChatStore implements ChatStore {\n  constructor(\n    public readonly userId: string,\n    private readonly db: DrizzleDb,\n  ) {}\n\n  async listConversations(): Promise<StoredConversation[]> {\n    const rows = await this.db\n      .select({\n        id: conversations.id,\n        userId: conversations.userId,\n        title: conversations.title,\n        metadata: conversations.metadata,\n        createdAt: conversations.createdAt,\n        updatedAt: conversations.updatedAt,\n        messageCount: sql<number>`(\n          SELECT COUNT(*)::int FROM ${messages}\n          WHERE ${messages.conversationId} = ${conversations.id}\n        )`,\n      })\n      .from(conversations)\n      .where(eq(conversations.userId, this.userId))\n      .orderBy(desc(conversations.updatedAt));\n\n    return rows.map((r) => toStoredConversation(r as ConversationRow, r.messageCount));\n  }\n\n  async getConversation(id: string): Promise<StoredConversation | null> {\n    const rows = await this.db\n      .select()\n      .from(conversations)\n      .where(and(eq(conversations.id, id), eq(conversations.userId, this.userId)))\n      .limit(1);\n    return rows.length ? toStoredConversation(rows[0]) : null;\n  }\n\n  async ensureConversation(id: string, init?: { title?: string }): Promise<StoredConversation> {\n    // Look up WITHOUT the user filter so we can distinguish \"doesn't exist\"\n    // (safe to create) from \"exists but owned by someone else\" (must reject).\n    // Filtering by user here would make a forged foreign id look identical to\n    // a brand-new id and we'd silently create a duplicate under this user.\n    const existing = await this.db\n      .select({ id: conversations.id, userId: conversations.userId })\n      .from(conversations)\n      .where(eq(conversations.id, id))\n      .limit(1);\n\n    if (existing.length) {\n      if (existing[0].userId !== this.userId) throw new ConversationOwnershipError(id);\n      const full = await this.getConversation(id);\n      // getConversation can't return null here (we just confirmed ownership),\n      // but satisfy the type and guard against a race-delete.\n      if (full) return full;\n    }\n\n    // Insert; tolerate a concurrent create of the same id (idempotent).\n    await this.db\n      .insert(conversations)\n      .values({ id, userId: this.userId, title: init?.title ?? 'New Chat', metadata: {} })\n      .onConflictDoNothing({ target: conversations.id });\n\n    const created = await this.getConversation(id);\n    if (created) return created;\n    // If we still can't read it back, a concurrent transaction created it under\n    // a different user between our check and insert — treat as ownership error.\n    throw new ConversationOwnershipError(id);\n  }\n\n  async renameConversation(id: string, title: string): Promise<void> {\n    await this.db\n      .update(conversations)\n      .set({ title, updatedAt: new Date() })\n      .where(and(eq(conversations.id, id), eq(conversations.userId, this.userId)));\n  }\n\n  async deleteConversation(id: string): Promise<boolean> {\n    const deleted = await this.db\n      .delete(conversations)\n      .where(and(eq(conversations.id, id), eq(conversations.userId, this.userId)))\n      .returning({ id: conversations.id });\n    return deleted.length > 0;\n  }\n\n  async listMessages(conversationId: string, opts?: ListMessagesOptions): Promise<StoredMessage[]> {\n    // Scope to the user FIRST: confirm ownership before reading messages, so a\n    // foreign conversation id yields [] rather than someone else's messages.\n    const owned = await this.getConversation(conversationId);\n    if (!owned) return [];\n\n    // Ceiling is MAX_PAGE + 1, not MAX_PAGE.\n    //\n    // The router asks for `limit + 1` to detect whether an older page exists\n    // without paying for a second COUNT query. Clamping at exactly MAX_PAGE\n    // silently ate that probe row at the maximum page size: a caller asking for\n    // 100 got 101 clamped back to 100, `page.length > limit` was never true,\n    // `hasMore` was always false, and the conversation appeared to end at\n    // message 100 with no way to scroll further (#55).\n    //\n    // One row of headroom keeps the probe working while the ceiling still does\n    // its real job — bounding what a hostile or careless caller can pull in a\n    // single query.\n    const limit = Math.min(Math.max(opts?.limit ?? MAX_PAGE, 1), MAX_PAGE + 1);\n    const cursor =\n      opts?.before && opts.beforeId\n        ? or(\n            lt(messages.createdAt, opts.before),\n            and(eq(messages.createdAt, opts.before), lt(messages.id, opts.beforeId)),\n          )\n        : opts?.before\n          ? lt(messages.createdAt, opts.before)\n          : undefined;\n    const where = cursor\n      ? and(eq(messages.conversationId, conversationId), cursor)\n      : eq(messages.conversationId, conversationId);\n\n    // Fetch newest-first with a stable id tiebreaker, then reverse to\n    // chronological order. The same tuple defines the cursor, so rows sharing a\n    // timestamp cannot move or disappear at a page boundary.\n    const rows = await this.db\n      .select()\n      .from(messages)\n      .where(where)\n      .orderBy(desc(messages.createdAt), desc(messages.id))\n      .limit(limit);\n\n    return rows.reverse().map(toStoredMessage);\n  }\n\n  async saveTurn(input: SaveTurnInput): Promise<void> {\n    const { conversationId, messages: turnMessages, model } = input;\n\n    // Defence in depth: verify ownership even though the router already called\n    // ensureConversation. saveTurn must never trust its caller did so.\n    const owned = await this.getConversation(conversationId);\n    if (!owned) throw new ConversationOwnershipError(conversationId);\n\n    if (turnMessages.length === 0) return;\n\n    // Idempotent insert keyed on message id. The handler configures the AI SDK\n    // to assign a stable id to every message (generateMessageId), and replays/\n    // retries re-deliver the same id — so onConflictDoNothing dedupes safely.\n    //\n    // Defence in depth: if a message arrives without an id (empty string),\n    // mint one rather than inserting it. Multiple id-less messages would\n    // otherwise all collide on the '' primary key and silently vanish — the\n    // exact bug an empty assistant id caused before generateMessageId was set.\n    const values = turnMessages.map((m) => ({\n      id: m.id && m.id.length > 0 ? m.id : generateId(),\n      conversationId,\n      role: m.role as 'user' | 'assistant' | 'system',\n      parts: m.parts,\n      text: textFromParts(m.parts),\n      model: m.role === 'assistant' ? model ?? null : null,\n    }));\n\n    // The PK is `id` alone, so onConflictDoNothing's dedup is GLOBAL, not\n    // per-conversation. A same-conversation id collision (a retry/replay\n    // re-delivering the same generateMessageId) is the intended idempotent\n    // path — onConflictDoNothing is exactly right for that. But a CLIENT\n    // -supplied user-message id can collide with a message id that already\n    // exists in a DIFFERENT conversation; if that happens, onConflictDoNothing\n    // would silently swallow the new message (data loss) and doubles as an\n    // existence oracle (the client can probe whether an id exists elsewhere).\n    // That's a client bug or a probe, not a replay — detect it and re-mint the\n    // id so the message is still persisted, rather than trusting a\n    // client-controlled PK across conversation boundaries.\n    const incomingIds = values.map((v) => v.id);\n    if (incomingIds.length > 0) {\n      const existing = await this.db\n        .select({ id: messages.id, conversationId: messages.conversationId })\n        .from(messages)\n        .where(inArray(messages.id, incomingIds));\n      const foreignIds = new Set(\n        existing.filter((row) => row.conversationId !== conversationId).map((row) => row.id),\n      );\n      for (const v of values) {\n        if (foreignIds.has(v.id)) v.id = generateId();\n      }\n    }\n\n    await this.db.insert(messages).values(values).onConflictDoNothing({ target: messages.id });\n\n    // No prefix auto-title here (removed): the title stays 'New Chat' until the\n    // handler's generated thread title lands via renameConversation. Keeping the\n    // default is load-bearing — it is the handler's \"still needs a title\" signal,\n    // so a failed generation retries naturally on the thread's next turn.\n\n    // Bump updatedAt so the conversation surfaces at the top of the history list.\n    await this.db\n      .update(conversations)\n      .set({ updatedAt: new Date() })\n      .where(and(eq(conversations.id, conversationId), eq(conversations.userId, this.userId)));\n  }\n}\n\n/**\n * Create a `ChatStoreFactory` backed by the default Drizzle/Postgres store.\n *\n * Pass to `createChatHandler({ store: createDrizzleChatStore() })`. The\n * factory binds each store instance to the verified `userId` the handler\n * provides per request. The underlying connection pool is shared.\n */\nexport function createDrizzleChatStore(options?: DrizzleClientOptions) {\n  const db = getDrizzleDb(options);\n  return (userId: string): ChatStore => new DrizzleChatStore(userId, db);\n}\n","/**\n * ChatStore — the persistence contract for chat conversations and messages.\n *\n * This is one of the two pluggable backends of the widget (the other is\n * `StorageAdapter` for attachments). The package ships a Drizzle/Postgres\n * implementation as the default; a hosted backend or a BYO store (Prisma,\n * raw SQL, DynamoDB, a test double) is simply another implementation of this\n * same interface.\n *\n * ──────────────────────────────────────────────────────────────────────────\n * The security model is in the shape of this API, not in its callers.\n * ──────────────────────────────────────────────────────────────────────────\n *\n * A `ChatStore` is *bound to one verified user* at construction time (see\n * `ChatStoreFactory`). None of its methods accept a `userId`. This is\n * deliberate and it is the core defence against the IDOR class of bug:\n *\n *   - You cannot ask the store for \"conversation X belonging to user Y\",\n *     because there is no parameter through which a foreign `userId` could\n *     enter. The only user the store will ever read or write is the one it\n *     was constructed with.\n *\n *   - Every method is therefore *implicitly scoped*. `listConversations()`\n *     returns only the bound user's conversations. `getConversation(id)`\n *     returns `null` — not someone else's row — when `id` exists but belongs\n *     to a different user. `saveTurn(...)` refuses (throws\n *     `ConversationOwnershipError`) if `conversationId` exists under another\n *     user.\n *\n * The route layer's job shrinks to: authenticate the request, derive the\n * real `userId` from the *server* session, construct a store bound to it,\n * and call methods. There is no per-route ownership check to forget, because\n * the store cannot be made to cross users.\n *\n * Implementations MUST uphold the contract documented on each method. The\n * Drizzle default does; if you write your own, these invariants are the\n * security boundary — treat them as load-bearing, not advisory.\n */\n\nimport type {\n  ListMessagesOptions,\n  SaveTurnInput,\n  StoredConversation,\n  StoredMessage,\n} from './types';\n\n/**\n * Thrown by mutating methods when the target conversation exists but is owned\n * by a different user than the one this store is bound to. Callers should map\n * this to an HTTP 403. (Read methods don't throw — they return `null`/`[]` —\n * so that probing for existence can't distinguish \"not found\" from\n * \"forbidden\", which would itself leak information.)\n */\nexport class ConversationOwnershipError extends Error {\n  constructor(public readonly conversationId: string) {\n    super(`Conversation ${conversationId} is not owned by the current user`);\n    this.name = 'ConversationOwnershipError';\n  }\n}\n\nexport interface ChatStore {\n  /**\n   * The user this store instance is bound to. Read-only; set at construction.\n   * Exposed so the router can stamp it onto storage paths, logs, etc. — never\n   * as something a caller can change.\n   */\n  readonly userId: string;\n\n  // ── Conversations ──────────────────────────────────────────────────────\n\n  /**\n   * List the bound user's conversations, most-recently-updated first.\n   * Returns `messageCount` on each row for the history list. Returns `[]`\n   * (never throws) when the user has none.\n   */\n  listConversations(): Promise<StoredConversation[]>;\n\n  /**\n   * Fetch a single conversation by id, scoped to the bound user.\n   *\n   * Returns `null` when the conversation does not exist OR exists but belongs\n   * to another user — the two cases are intentionally indistinguishable to\n   * the caller (and thus to an attacker). Never returns another user's row.\n   */\n  getConversation(id: string): Promise<StoredConversation | null>;\n\n  /**\n   * Ensure a conversation row exists for `id`, owned by the bound user.\n   *\n   * - If no row exists for `id`: creates it, owned by the bound user, and\n   *   returns it.\n   * - If a row exists and is owned by the bound user: returns it unchanged\n   *   (idempotent — safe to call at the top of every request).\n   * - If a row exists but is owned by a *different* user: throws\n   *   `ConversationOwnershipError` and writes nothing.\n   *\n   * This is the single chokepoint that makes \"write into someone else's\n   * conversation\" impossible: the router calls it before persisting any\n   * message, so a forged conversation id is rejected before any data lands.\n   */\n  ensureConversation(id: string, init?: { title?: string }): Promise<StoredConversation>;\n\n  /**\n   * Rename a conversation owned by the bound user. No-op (does not throw) if\n   * the conversation doesn't exist or isn't owned by the user — renaming is\n   * not security-sensitive and silent failure is friendlier here.\n   */\n  renameConversation(id: string, title: string): Promise<void>;\n\n  /**\n   * Delete a conversation (and cascade its messages + attachment rows) owned\n   * by the bound user. No-op if it doesn't exist or isn't owned by the user.\n   * Returns `true` if a row was actually deleted, `false` otherwise — lets\n   * the route return 404 vs 200 honestly without a separate existence check.\n   *\n   * Note: this deletes message *rows*. Purging the underlying attachment\n   * blobs from storage is the router's job (it has the `StorageAdapter`),\n   * driven off the attachments this method returns having referenced.\n   */\n  deleteConversation(id: string): Promise<boolean>;\n\n  // ── Messages ───────────────────────────────────────────────────────────\n\n  /**\n   * Load messages for a conversation, scoped to the bound user, newest-first\n   * internally but returned in chronological order (oldest → newest) ready to\n   * render. Returns `[]` if the conversation doesn't exist or isn't owned by\n   * the user — same non-distinguishing contract as `getConversation`.\n   *\n   * Honours `ListMessagesOptions` for pagination. Implementations MUST:\n   *\n   *  - Apply `before` as a strict composite cursor when `beforeId` is present:\n   *    `(createdAt < before) OR (createdAt = before AND id < beforeId)`.\n   *    Timestamp-only callers retain strict `createdAt < before` behavior.\n   *  - Return CHRONOLOGICAL order (oldest → newest), with `id` as the stable\n   *    tiebreaker for equal timestamps.\n   *  - Clamp `limit` so a hostile client cannot request an unbounded page —\n   *    but clamp to **101, not 100**. The router asks for `limit + 1` to detect\n   *    whether an older page exists without a second COUNT query; a ceiling of\n   *    exactly 100 eats that probe row at the maximum page size, so `hasMore`\n   *    silently reports `false` and the conversation appears to end at message\n   *    100 with no way to scroll further (#55). One row of headroom keeps the\n   *    probe working while the ceiling still bounds the query.\n   *\n   * A store that ignores these options is not merely slower — it breaks the\n   * router's `hasMore` contract and ships the whole transcript on every load.\n   */\n  listMessages(conversationId: string, opts?: ListMessagesOptions): Promise<StoredMessage[]>;\n\n  /**\n   * Persist the final messages of a completed turn.\n   *\n   * Contract:\n   *  - MUST verify the conversation is owned by the bound user first; throws\n   *    `ConversationOwnershipError` otherwise (defence in depth — the router\n   *    already called `ensureConversation`, but `saveTurn` must not trust\n   *    that).\n   *  - MUST be idempotent on message id: a message whose id already exists is\n   *    skipped, not duplicated. (The AI SDK delivers stable ids; replays and\n   *    retries re-deliver them.)\n   *  - MUST persist each message's full `parts` array as the source of truth,\n   *    plus a denormalised text projection for previews.\n   *  - MUST bump the conversation's `updatedAt`.\n   *\n   * Errors other than ownership (e.g. a transient DB failure) propagate so\n   * the router can log them loudly — a silently-dropped assistant turn is\n   * exactly the bug we're trying to design out.\n   */\n  saveTurn(input: SaveTurnInput): Promise<void>;\n}\n\n/**\n * Constructs a `ChatStore` bound to a specific, already-verified user.\n *\n * The router calls this *after* it has authenticated the request and derived\n * `userId` from the server session — never from anything client-supplied.\n * Passing a client-controlled value here would reintroduce the very IDOR the\n * bound-store design exists to prevent, so implementations should treat\n * `userId` as a trusted server secret, not as request input.\n *\n * Construction is intended to be cheap (the underlying DB pool/connection is\n * shared across instances) so a fresh store per request is the norm.\n */\nexport type ChatStoreFactory = (userId: string) => ChatStore;\n","/**\n * Postgres connection for the default Drizzle store.\n *\n * Carries forward the connection hygiene the original package got right:\n *   • cache the client on globalThis so Next.js dev HMR doesn't leak a fresh\n *     pool on every reload (which eventually exhausts Supabase's pooler),\n *   • `prepare: false` for the Supabase transaction-mode pooler,\n *   • a small, bounded pool.\n *\n * The connection string is read from `DATABASE_URL` by default but can be\n * passed explicitly so a host app with multiple databases stays in control.\n */\n\nimport 'server-only';\nimport { drizzle } from 'drizzle-orm/postgres-js';\nimport postgres from 'postgres';\nimport * as schema from './schema';\n\nexport type DrizzleDb = ReturnType<typeof drizzle<typeof schema>>;\n\nexport interface DrizzleClientOptions {\n  /** Postgres connection string. Defaults to `process.env.DATABASE_URL`. */\n  connectionString?: string;\n  /** Max pool connections. Defaults to 5 (dev-friendly; raise in prod). */\n  max?: number;\n}\n\nconst globalForDb = globalThis as unknown as {\n  __mordnChatWidgetDb?: Map<string, DrizzleDb>;\n};\n\n/**\n * Get (or lazily create + cache) a Drizzle db for a connection string. Cached\n * by connection string so multiple stores over the same DB share one pool.\n */\nexport function getDrizzleDb(options: DrizzleClientOptions = {}): DrizzleDb {\n  const connectionString = options.connectionString ?? process.env.DATABASE_URL;\n  if (!connectionString) {\n    throw new Error(\n      '[chat-widget] DATABASE_URL is not set and no connectionString was ' +\n        'provided to the default Drizzle store.',\n    );\n  }\n\n  const cache = (globalForDb.__mordnChatWidgetDb ??= new Map());\n  const cached = cache.get(connectionString);\n  if (cached) return cached;\n\n  const client = postgres(connectionString, {\n    prepare: false, // required for Supabase transaction-mode pooler\n    max: options.max ?? 5,\n    idle_timeout: 20,\n    connect_timeout: 10,\n  });\n  const db = drizzle(client, { schema });\n\n  // Only cache in non-prod: in prod each server instance loads the module once\n  // so caching buys nothing and would pin a pool across the process lifetime.\n  if (process.env.NODE_ENV !== 'production') cache.set(connectionString, db);\n  return db;\n}\n","/**\n * Drizzle schema for the default ChatStore (v2, parts-first).\n *\n * This is the schema the *default* store uses. A BYO store may use any schema\n * it likes — this one is not part of the public contract, the `ChatStore`\n * interface is. It's exported so consumers on the default path can run\n * `drizzle-kit` against it and so the migration can reference it.\n *\n * What changed from v0.7.1\n * ------------------------\n * The old schema stored a flattened `content: text` as the apparent source of\n * truth and tucked the real AI SDK `parts` into a `metadata` jsonb blob. That\n * inverted the actual authority — `parts` (text + reasoning + tool calls +\n * sources + files) is what the AI SDK round-trips and what rendering needs;\n * `content` was a lossy shadow.\n *\n * v2 makes that authority explicit:\n *   • `parts`  — jsonb NOT NULL — the canonical AI SDK message parts. Source\n *                of truth for rendering and model replay.\n *   • `text`   — text — a denormalised projection of the text parts, for\n *                cheap previews / titles / search. Never authoritative.\n *\n * A backfill migration populates these from the old columns so existing\n * installs upgrade without data loss (see migrations/).\n */\n\nimport { pgTable, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core';\nimport type { UIMessage } from 'ai';\n\nexport const conversations = pgTable(\n  'chat_conversations',\n  {\n    id: text('id').primaryKey(),\n    userId: text('user_id').notNull(),\n    title: text('title').notNull().default('New Chat'),\n    /** Free-form host-app metadata. Never read by the core. */\n    metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n    createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),\n    updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),\n  },\n  (table) => [\n    // Drives the history list (WHERE user_id = ? ORDER BY updated_at DESC).\n    index('chat_conversations_user_updated_idx').on(table.userId, table.updatedAt),\n  ],\n);\n\nexport const messages = pgTable(\n  'chat_messages',\n  {\n    id: text('id').primaryKey(),\n    conversationId: text('conversation_id')\n      .notNull()\n      .references(() => conversations.id, { onDelete: 'cascade' }),\n    role: text('role').notNull().$type<'user' | 'assistant' | 'system'>(),\n    /** Canonical AI SDK parts — source of truth. */\n    parts: jsonb('parts').$type<UIMessage['parts']>().notNull(),\n    /** Denormalised plain-text projection for previews/search. */\n    text: text('text').notNull().default(''),\n    /** Model that produced this message (assistant turns). */\n    model: text('model'),\n    createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),\n  },\n  (table) => [\n    // Drives history load (WHERE conversation_id = ? ORDER BY created_at).\n    index('chat_messages_conversation_created_idx').on(table.conversationId, table.createdAt),\n  ],\n);\n\nexport type ConversationRow = typeof conversations.$inferSelect;\nexport type NewConversationRow = typeof conversations.$inferInsert;\nexport type MessageRow = typeof messages.$inferSelect;\nexport type NewMessageRow = typeof messages.$inferInsert;\n"],"mappings":"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","names":[]}