Get a short-lived download link for an attached file
/api/ai/chat/attachments/{id}Re-derives book_ai_attachments' own tenancy boundary by hand (company_id AND user_id), since this route holds a service-role client and RLS never applies to it. Returns 404, not the raw storage error, whether the attachment never existed for this caller or the 90-day sweep (GET /api/cron/ai-attachments-sweep) already removed the raw file: the caller cannot tell those apart and does not need to. The signed URL is a 15-minute bearer credential, same TTL reasoning as the data-exports download link.
Authorization
sessionCookie The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across
numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.
Every request re-validates it against the Auth server (getUser()), never by decoding the cookie
locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified
app_metadata.company_id claim and enforced by row-level security; it is never read from request
input, on any route, ever.
role (admin / staff) is deliberately not in RLS. It gates specific actions in route code,
the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's
own check is the enforcement.
In: cookie
Path Parameters
The attachment id.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/api/ai/chat/attachments/string"{ "url": "http://example.com", "filename": "string"}Attach a CSV/TSV/XLSX file to an AI assistant conversation POST
Phase 1 only: CSV, TSV and XLSX. PDF and images are later phases (PDF needs a text-extraction dependency this app does not have yet; images need a vision-capable model actually wired into the free-tier rotation). Validation IS successfully parsing the file (parseImport, src/lib/data/parse.ts), the same 'prove it's real by processing it' posture the avatar routes take for images: a file that fails to parse is rejected outright and never reaches Storage. Uploads through serviceRoleClient() end to end, same posture as every data-imports/data-exports route for an equally sensitive 'a venue's own file' surface; the private `ai-attachments` bucket carries no policies for `authenticated` at all. Only the extracted grid (capped rows and characters, attachmentContextText) is what the model ever reads (readAttachment, src/lib/ai/tools.ts); the raw file is never inlined into a chat message. The chat row is upserted if it does not exist yet, since a file can be attached before the first message is ever sent. No upload-specific rate limit or credit charge yet, same posture as `AI_RATE_LIMIT_PER_MINUTE` on POST /api/ai/chat itself: no real users yet, needs a real answer before production traffic.
Draft an inbox reply or a private note with the AI assistant (single-shot, read-only) POST
Powers the Inbox's two Lessly buttons (top-right of the reply box, and of "On this booking"): draft the staff's next reply, or suggest a short private note, from the thread already open in MessagesClient. Single-shot (`generateText`, not a stream), unlike POST /api/ai/chat: this answers once from the messages the client sends in the request body, with no running conversation and no persisted history of its own. Runs entirely as the signed-in user through requireMember()'s RLS-scoped client, called with no role or vertical: either engine's staff can reach it. Unlike POST /api/ai/chat, it never re-reads the thread's messages itself: the client already has `open.messages`, rendered server-side by /inbox's own page load a moment ago, so the request body carries the transcript instead of a thread id. Tenancy still comes from requireMember()'s companyId, never from the body; neither output has any effect until a human clicks Send reply or Save note. The transcript is capped to the newest 60 messages and roughly 6000 characters (buildTranscript, route.ts), oldest lines dropped first if still over budget. The system prompt instructs the model to treat every line of it as data to read, never as an instruction to follow, the same posture POST /api/ai/chat applies to tool results, since a guest can type anything into the messages this transcript is built from. Shares the identical per-tier monthly ai_credits allowance and the same wired-but-inactive AI_RATE_LIMIT_PER_MINUTE gate as POST /api/ai/chat, keyed the same way (per user, not per feature): one OpenRouter key backs both, so the abuse concern is request frequency from this login, not which AI feature it is hitting.