@molecule/api-resource-ai-conversation
v1.0.1
Published
AI conversation history for projects
Readme
@molecule/api-resource-ai-conversation
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Per-project AI chat conversation resource for molecule.dev.
Persists ONE conversation per project (messages + AI context as JSONB) and
exposes a streaming chat endpoint plus history/clear, nested under
/projects/:projectId/chat. The chat handler resolves the bonded AI
provider, streams the assistant reply to the client as Server-Sent Events,
and appends both sides to the stored message log.
Quick Start
import { routes, requestHandlerMap } from '@molecule/api-resource-ai-conversation'
// Wired by mlcl inject:
// POST /projects/:projectId/chat — send a message, reply streams via SSE
// GET /projects/:projectId/chat — conversation history
// DELETE /projects/:projectId/chat — clear the conversationType
resource
Installation
npm install @molecule/api-resource-ai-conversation @molecule/api-ai @molecule/api-bond @molecule/api-database @molecule/api-i18n @molecule/api-locales-ai-conversation @molecule/api-logger @molecule/api-resourceAPI
Interfaces
AIContext
AI context carried across a conversation: system prompt, tool state, and model selection.
interface AIContext {
system?: string
toolState?: Record<string, unknown>
model?: string
}ChatAttachment
A file attachment sent with a chat message.
interface ChatAttachment {
/** MIME type (e.g., 'image/jpeg', 'application/pdf'). */
mediaType: string
/** Base64-encoded file data (no data-URL prefix). */
data: string
/** Original filename for display. */
filename?: string
}ChatMessage
A single message in a conversation (user, assistant, or system) with optional tool call data.
interface ChatMessage {
role: 'user' | 'assistant' | 'system'
content: string
toolCalls?: Array<{
id: string
name: string
input: unknown
output?: unknown
}>
/** File attachment metadata (no base64 data — for display in history). */
attachments?: Array<{ filename: string; mediaType: string; size: number }>
timestamp: string
}Conversation
A conversation record containing messages, AI context, and project association.
interface Conversation {
id: string
projectId: string
messages: ChatMessage[]
aiContext: AIContext
createdAt: string
updatedAt: string
}SendMessageInput
Input payload for sending a message to a conversation (message text and optional model override).
interface SendMessageInput {
message: string
model?: string
/** File attachments (images, PDFs, audio, video) to include with the message. */
attachments?: ChatAttachment[]
}Functions
authUser(req, res, next)
Object-level authorization middleware for a project's conversation routes
(/projects/:projectId/chat).
Delegates to {@link ensureProjectAccess}: calls next() only when the caller
is authenticated and owns the project, otherwise the request is rejected with
401/403. This is the shipped default referenced by routes.ts for the
chat/history/clear routes, mirroring @molecule/api-resource-project's
authUser, so generated apps do NOT expose other tenants' conversations (or
allow unauthenticated AI cost abuse) by default.
function authUser(
req: MoleculeRequest,
res: MoleculeResponse,
next: MoleculeNextFunction,
): Promise<void>req— The request object (usesparams.projectId).res— The response object (readslocals.session, writeslocals.project).next— Passes control to the next handler on success.
chat(req, res)
Sends a message to a conversation and streams the AI response via SSE.
function chat(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request object.res— The response object.
clear(req, res)
Deletes a conversation and all its messages for a given project.
function clear(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request object.res— The response object.
ensureProjectAccess(req, res)
Verifies the caller is authenticated AND owns the project named by
req.params.projectId, sending the appropriate failure response if not.
Fails closed: looks up the project scoped to BOTH the route's projectId and
the authenticated session.userId, so a row is returned only when the caller
owns it. On success the owned row is stashed in res.locals.project (avoiding a
second query downstream) and true is returned. Otherwise it writes 401 (no
session) or 403 (project missing or owned by someone else — deliberately
indistinguishable so existence is not leaked) and returns false.
This is the single source of truth shared by the {@link authUser} route middleware and the in-handler defense-in-depth checks, so every entry point fails closed identically even if a route middleware is dropped by codegen.
function ensureProjectAccess(req: MoleculeRequest, res: MoleculeResponse): Promise<boolean>req— The request object (usesparams.projectId).res— The response object (readslocals.session, writeslocals.project).
Returns: true when authorized (response untouched); false when a 401/403 was sent.
history(req, res)
Returns the full message history for a project's conversation.
function history(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request object.res— The response object.
read(req, res)
Reads a single conversation by project ID, returning 404 if not found.
function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request object.res— The response object.
update(req, res)
Updates a conversation's AI context (system prompt, model, tool state).
function update(req: MoleculeRequest, res: MoleculeResponse): Promise<void>req— The request object.res— The response object.
Constants
i18nRegistered
The i18n registered.
const i18nRegistered: truerequestHandlerMap
Map of request handler names to implementations. authUser is the
object-level authorization middleware referenced by routes.ts for the
chat/history/clear routes — registering it here keeps the codegen
scanner from stripping it (it only keeps middlewares that are keys of this
map), so generated apps actually gate the routes.
const requestHandlerMap: {
readonly chat: typeof chat
readonly history: typeof history
readonly read: typeof read
readonly update: typeof update
readonly clear: typeof clear
readonly authUser: typeof authUser
}routes
Route array for conversation endpoints: POST chat (SSE streaming), GET history, DELETE clear.
All three are gated by authUser, the object-level authorization middleware
(see authorizers/authUser.ts) that fails closed — it requires an authenticated
session AND verifies the caller owns :projectId, 401/403ing otherwise. It is a
key of requestHandlerMap, so the codegen scanner keeps it (a bare
'authenticate' token that isn't a handler key was being stripped, shipping
these routes UNGATED: unauthenticated AI cost abuse + cross-tenant IDOR). The
handlers also re-check ownership inline (ensureProjectAccess) so they stay
secure even if a middleware is dropped.
const routes: readonly [
{
readonly method: 'post'
readonly path: '/projects/:projectId/chat'
readonly handler: 'chat'
readonly middlewares: readonly ['authUser']
},
{
readonly method: 'get'
readonly path: '/projects/:projectId/chat'
readonly handler: 'history'
readonly middlewares: readonly ['authUser']
},
{
readonly method: 'delete'
readonly path: '/projects/:projectId/chat'
readonly handler: 'clear'
readonly middlewares: readonly ['authUser']
},
]Injection Notes
Requirements
Peer dependencies:
@molecule/api-ai^1.0.1@molecule/api-bond^1.0.1@molecule/api-database^1.0.1@molecule/api-i18n^1.0.1@molecule/api-logger^1.0.1@molecule/api-resource^1.0.1@molecule/api-locales-ai-conversation^1.0.1
Runtime Dependencies
@molecule/api-ai@molecule/api-bond@molecule/api-database@molecule/api-i18n@molecule/api-locales-ai-conversation@molecule/api-logger@molecule/api-resourceTwo prerequisites this package does not create. (1) An AI provider must be bonded before the first chat request (wire an
@molecule/api-ai-*bond, e.g.bond('ai', 'anthropic', provider), inbonds.ts) — the handler uses the@molecule/api-aiaccessor and fails without one. (2) Aprojectstable with auserIdcolumn must exist:src/__setup__/conversations.sqldeclaresprojectId REFERENCES "projects"("id"), so apply@molecule/api-resource-project's migration FIRST, then this package's.Keep
authUserinrequestHandlerMap. All three routes are gated by theauthUserobject-level authorizer, which authenticates AND verifies the caller owns:projectId(403 otherwise, existence not leaked). Route codegen keeps only middlewares that are keys ofrequestHandlerMap— removing the entry ships these routes UNGATED: anonymous AI-cost abuse plus cross-tenant reads. The handlers also re-check ownership inline; keep that too.The chat response is
text/event-stream. The client must consume SSE (the@molecule/app-ai-chat-httpbond does); a plain JSON fetch appears to hang. Don't put a buffering proxy in front without SSE passthrough.One conversation row per project — history accumulates in a JSONB
messagesarray;DELETE …/chatresets it. There is no per-message CRUD.
E2E Tests
Integration checklist — drive the real UI (the project's chat panel via live
preview, no mocks), adapt each item to this app's actual screens, and check
every box off one by one. A box you can't check is an integration bug to fix,
not a skip. This resource is the STORAGE of chat history (one conversations
row per project, a messages JSONB array) — the reply text itself comes from
the bonded @molecule/api-ai provider, so verify the transcript + privacy
here, not generation quality:
- [ ] Sending a message from the chat UI persists BOTH sides in order: the
POST /projects/:projectId/chatappends the user turn (role: 'user', your exactcontent), then after the SSE stream ends appends the assistant turn (role: 'assistant', the streamed text). The first message auto-creates the conversation row for that project. - [ ] Reloading the project (
GET /projects/:projectId/chat) shows the FULL transcript in send order across several back-and-forth exchanges — every user/assistant turn present, none lost, dropped, or reordered. - [ ] Clearing the chat (
DELETE /projects/:projectId/chat) deletes the conversation: history immediately returns{ messages: [] }and the row is not re-fetchable (there is no archive/undo — clear removes it). Sending a new message afterward starts a fresh conversation from empty. - [ ] If token usage is surfaced, each assistant response records its
inputTokens/outputTokens(theconversation.ai_responseanalytics event) — usage is tracked per response, not accumulated on the row. - [ ] AUTHORIZATION / PRIVACY — chat history is strictly per project owner. A
second user hitting another user's
:projectId(send, history, OR clear) gets403, indistinguishable from "no such project" so existence isn't leaked, and never sees or clears that chat. The owner is the authenticated session (the project is looked up scoped tosession.userId), NEVER a request-bodyuserId— forging one changes nothing. Chat content (which may be sensitive) is never returned cross-user or logged in the clear.
