@boldare/askdare 0.0.0-stage → 0.1.0-rc.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.
@@ -0,0 +1,43 @@
1
+ # Migrating from @boldare/openai-assistant
2
+
3
+ `@boldare/openai-assistant` stopped working on 2026-08-26, when OpenAI shut down the Assistants API it was built on. askdare is its successor from the same team, but not a new version of it: the old library was a NestJS module with an Angular widget, and askdare is a Next.js library on the Responses API. There is no upgrade in place. You move your assistant's setup into a Next.js app instead.
4
+
5
+ Most deployments take an afternoon. The assistant's instructions, its tools and its knowledge files carry over. Old conversations don't: they lived in Assistants API threads, which can no longer be read.
6
+
7
+ ## What maps to what
8
+
9
+ | openai-assistant 1.x | askdare |
10
+ |---|---|
11
+ | `AssistantModule.forRoot(config)` in a NestJS app, plus `ASSISTANT_ID` | `defineBot({...})` in `askdare.config.ts`, served by `createChatHandler({ bots })` from one Next.js route. There is no assistant object at OpenAI to create or keep in sync. |
12
+ | Agents extending `AgentBase`, with arguments as a raw JSON string | `defineTool` with a Zod schema. Arguments are validated before your code runs, and each call has a timeout. See [Tools](tools.md). |
13
+ | Threads, `ThreadsController` | Conversations in the OpenAI Conversations API. The visitor's browser keeps only a signed token. No database. |
14
+ | socket.io `ChatGateway`, raw Assistants stream events | Server-sent events over `POST`, in askdare's own versioned [protocol](protocol.md). |
15
+ | Run polling | Streaming Responses API calls, with limits on tool steps and a deadline per turn. |
16
+ | Knowledge files uploaded at startup, `.env` rewritten at runtime | `npx @boldare/askdare kb sync <folder>`, run when the files change. It prints the vector store ID once; the app only reads `KB_VECTOR_STORE_ID`. See [Knowledge base](knowledge-base.md). |
17
+ | `FilesController` | Image and PDF attachments, checked by file type and size, and referenced by signed handles. See [Attachments](attachments.md). |
18
+ | `whisper-1` speech to text | Voice input with a transcription model you choose. See [Voice input](voice-input.md). |
19
+ | `tts-1` text to speech | Not available. OpenAI is retiring its text-to-speech models. |
20
+ | Annotations resolved with a file lookup per citation | Citations with a title and a link to your page, set by `kb sync`. |
21
+ | The `ai-embedded` iframe and its `postMessage` | One `<script>` tag. The chat runs in an iframe that only the sites in `allowedOrigins` may embed. See [Embedding](embedding.md). |
22
+ | Angular widget configured at build time | Widget settings in `defineBot({ widget })`: four themes, English and Polish texts, your own CSS. See [Styling](styling.md) and [Themes](themes.md). |
23
+ | No sign-in, CORS open to every origin | Short-lived session tokens, rate limits, daily token budgets and an origin allowlist by default. See [Going public](going-public.md). |
24
+
25
+ ## Steps
26
+
27
+ 1. **Pick the Next.js app** that will serve the chat: an existing Next.js 16 app with the App Router, or a new one from the [starter](https://github.com/boldare/askdare/tree/main/examples/starter). Your other sites embed the chat from there, so it needn't be the site visitors see.
28
+ 2. **Add askdare** as [Getting started](getting-started.md) describes: `npm install @boldare/askdare zod`, then `npx @boldare/askdare init`.
29
+ 3. **Move the assistant's setup** into `askdare.config.ts`:
30
+ - your instructions go into `instructions`;
31
+ - the model goes into `ASKDARE_MODEL`. Pick a current one: the models your old assistant used may be retired;
32
+ - the greeting, starter questions and colors go into `widget`.
33
+ 4. **Rewrite each agent as a tool.** The handler body usually stays as it is. Replace the JSON string parsing with a Zod schema, and return a plain object instead of a string. Read the [Tools](tools.md) guide first: a tool must never trust IDs that the model chose.
34
+ 5. **Sync the knowledge files:** `npx @boldare/askdare kb sync <folder>`, then set `KB_VECTOR_STORE_ID`. Add a `url` per file if citations should link to your pages.
35
+ 6. **Replace the embed code** on your sites with the askdare script tag, and list those sites in `security.allowedOrigins`.
36
+ 7. **Check and deploy.** `npx @boldare/askdare doctor` checks the key, the model and the headers. [Deployment](deployment.md) covers Vercel and Heroku.
37
+ 8. **Remove the old service** and its NestJS module. Delete the old assistant's files and vector stores in the OpenAI dashboard if you no longer need them.
38
+
39
+ ## What you lose
40
+
41
+ - **Old conversations.** Threads from the Assistants API can't be read any more, so there is nothing to import.
42
+ - **Text to speech.** Answers are text. Dictation (speech to text) is still there.
43
+ - **Assistants edited in the OpenAI dashboard.** Instructions now live in your code, so they are versioned and reviewed like the rest of it.
@@ -0,0 +1,175 @@
1
+ # Privacy
2
+
3
+ This page lists what data the chat handles, where it goes and how long it stays, so you can write your privacy notice. It describes what askdare does; it isn't legal advice. Your instructions, your tools and your hosting can add data of their own.
4
+
5
+ ## At a glance
6
+
7
+ | Data | Where | How long |
8
+ | --- | --- | --- |
9
+ | A handle to the current conversation (resume token) | The visitor's browser: `localStorage` of your app's origin, inside the chat's iframe | Until the visitor starts a new conversation; it works for at most 30 days |
10
+ | A session token | The iframe's memory, never stored | 15 minutes |
11
+ | Messages, answers, page context, tool calls and their results | OpenAI, as a conversation in your project | Until deleted |
12
+ | Model responses | OpenAI | OpenAI's retention for stored responses |
13
+ | Uploaded images and PDFs | OpenAI, as files in your project | 30 days, or until the conversation is deleted |
14
+ | Dictations | Sent to OpenAI for transcription | askdare keeps no audio |
15
+ | IP addresses, visitor IDs, conversation IDs | Rate-limit counters, in your server's memory or in Upstash | IP addresses up to 2 days, conversation IDs up to 30 days |
16
+ | An HMAC of the visitor or user ID (`safety_identifier`) | Sent to OpenAI with every model request | OpenAI's retention |
17
+ | Error reports | Your server log, or your `onError` function | Your hosting's log retention |
18
+
19
+ ## In the visitor's browser
20
+
21
+ The script on your site (`widget.js`) adds the chat button and an iframe. It stores nothing on your site and sets no cookies. The chat itself runs inside the iframe, on your app's origin:
22
+
23
+ - Its Content Security Policy lets it load scripts, styles, fonts and images only from your app, and talk only to your app. Images in answers are never loaded; the chat shows their alt text.
24
+ - The iframe is loaded with `referrerpolicy="strict-origin"`, so your app learns which site embeds the chat, but not which page.
25
+
26
+ ### What it stores
27
+
28
+ After the visitor's first message, the chat writes one entry to `localStorage`, in the iframe's storage for your app's origin. Nothing is written before that.
29
+
30
+ | | |
31
+ | --- | --- |
32
+ | Key | `askdare:<bot id>:resume` for an anonymous visitor; `askdare:<bot id>:resume:<key>` for a [signed-in visitor](identity.md), where `<key>` is 32 hexadecimal characters derived from the bot and user ID with `IDENTITY_SECRET`. It reveals nothing about the user |
33
+ | Value | An encrypted token with the visitor ID and a reference to the conversation. The browser can't read it |
34
+ | Purpose | To show and continue the conversation after a reload |
35
+ | Lifetime | It works for 30 days after the last answer in the conversation: each answer renews it. It is removed when the visitor starts a new conversation, and when the server says the conversation is gone or the token expired |
36
+
37
+ Everything else stays in memory and disappears with the page: the session token, which lasts 15 minutes, and a signed-in visitor's identity.
38
+
39
+ The chat sets no cookies. Browsers may partition or block storage in iframes from other sites; then the conversation doesn't survive a reload, and the chat still works. There is no setting to turn the entry off.
40
+
41
+ ### Consent banners
42
+
43
+ Whether this entry needs the visitor's consent under the rules that apply to you, such as the ePrivacy Directive in the EU, is for you and your legal advisers to decide. These facts are often relevant:
44
+
45
+ - It is written only after the visitor sends a message, and only to continue that conversation.
46
+ - It isn't used for tracking, analytics or advertising, and it isn't shared with other sites.
47
+ - It contains no readable personal data, and it stops working after 30 days at most.
48
+
49
+ ## On your server
50
+
51
+ askdare needs no database. Your app keeps only rate-limit counters.
52
+
53
+ ### IP addresses
54
+
55
+ - The client IP is used for rate limits and budgets only, never to identify anyone.
56
+ - It isn't hashed. IPv4 addresses are kept whole; IPv6 addresses are cut to their /64 network. They are part of counter names, such as `ask:ti:<bot id>:<ip>:<date>`.
57
+ - The counters live in the server's memory by default, in your Upstash database with `@boldare/askdare-upstash`, or in your own [store](custom-store.md).
58
+ - Each counter lasts as long as its window: from a minute up to 2 days for the daily budgets. Upstash deletes a counter when its window ends. The memory store drops expired counters within a minute, and everything when the process restarts.
59
+
60
+ ### Visitor IDs
61
+
62
+ The server gives every new visitor a random ID: `v_` followed by 32 hexadecimal characters. It never comes from the browser, and it identifies a chat in one browser, not a person. It travels inside the encrypted tokens, names some rate-limit counters, and is stored with the conversation at OpenAI.
63
+
64
+ ### Logs
65
+
66
+ - askdare never logs message contents, and cuts API keys down to their prefix.
67
+ - It logs one line per server error: the error code, an explanation of setup problems, and the underlying error, such as OpenAI's status, error code and request ID. It also logs the ID of an uploaded file it couldn't delete.
68
+ - Errors thrown by your own tools are logged as they are. Keep personal data out of their messages.
69
+ - With `onError` in `createChatHandler`, the same reports go to your function, for example to Sentry, instead of the log.
70
+ - Your hosting provider may log every request, IP addresses included, according to its own settings.
71
+
72
+ ### `safety_identifier`
73
+
74
+ Every model request carries a `safety_identifier`, so OpenAI can trace abuse to one end user instead of to your whole project.
75
+
76
+ - For an anonymous visitor, it is an HMAC of the random visitor ID.
77
+ - For a signed-in visitor, it is an HMAC of `user:` followed by your user ID, so it is the same on every device.
78
+
79
+ The HMAC key is derived from `CHAT_SECRET`, so knowing a user ID isn't enough to compute it. It is still a pseudonym, not anonymous data: with the secret, the link to a user can be restored. It changes when `CHAT_SECRET` rotates.
80
+
81
+ ## At OpenAI
82
+
83
+ Everything below lives in the OpenAI project whose key the app uses. Name OpenAI in your privacy notice.
84
+
85
+ ### What each message sends
86
+
87
+ - Your instructions for that request, with whatever you put in them, such as a signed-in user's name.
88
+ - The visitor's message, and the page context from `askdare('configure')` if the bot has `pageContext: true`.
89
+ - Attached files, by reference.
90
+ - Your tools' definitions and, when the model calls them, their results.
91
+ - The `safety_identifier`, and `prompt_cache_key: "askdare:<bot id>"`, which holds no personal data.
92
+
93
+ ### Conversations
94
+
95
+ - Conversations are stored with OpenAI's Conversations API, in your project, **until they are deleted** ([ADR-0003](https://github.com/boldare/askdare/blob/main/docs/adr/0003-conversation-state.md)). Each one carries the bot ID and the visitor ID as metadata (`askdare_bot`, `askdare_visitor`).
96
+ - A conversation holds the visitor's messages, the page context, the answers, and the tool calls with their results. The instructions are sent with each request rather than stored in the conversation.
97
+ - **The visitor can delete it.** "Start a new conversation" asks "Start a new conversation? The current one will be deleted." and then deletes it at OpenAI. For bots with attachments, the files attached in it are deleted too; a file that fails to delete expires on its own.
98
+ - **Nothing else deletes it.** When the browser's handle expires after 30 days, or the visitor clears the browser, the conversation stays in your project. askdare keeps no list of conversations, and OpenAI's API has no endpoint to list them, so there is no tool to find, export or delete one visitor's conversation. Take this into account in your retention decision ([Going public](going-public.md#6-privacy-and-transparency)).
99
+ - **Who can read it:** whoever uses the same browser can continue an anonymous conversation and see its history while the handle works. On a shared computer, that includes the next person, unless the visitor started a new conversation. A conversation started while signed in is shown only to that user ([Signed-in visitors](identity.md)).
100
+
101
+ ### Responses
102
+
103
+ askdare stores each model response (`store: true`), because a conversation needs it. A stored response includes the instructions it was given. OpenAI's [data controls](https://developers.openai.com/api/docs/guides/your-data) list 30 days by default for this stored state of the Responses API, and up to 30 days for abuse monitoring logs. Check that page for the current terms.
104
+
105
+ ### Uploaded files
106
+
107
+ - Files are uploaded with purpose `user_data` and `expires_after` set to 30 days from the upload. OpenAI deletes them then, or askdare deletes them earlier with their conversation.
108
+ - Files that are uploaded but never sent stay until they expire.
109
+ - The widget re-encodes images in the browser, which drops EXIF data such as the GPS position. PDFs are uploaded as they are.
110
+ - The file name goes to OpenAI too, cleaned of unusual characters.
111
+ - Files are never served back to browsers. See [Attachments](attachments.md).
112
+
113
+ ### Dictations
114
+
115
+ The recording goes to OpenAI's transcription API and nowhere else, and askdare stores no audio. The transcript lands in the message field; it reaches the conversation only if the visitor sends it. See [Voice input](voice-input.md).
116
+
117
+ ### Zero Data Retention
118
+
119
+ askdare doesn't use Zero Data Retention. OpenAI grants it only after approval, and its data controls page lists the Conversations API as not eligible. Clients who need zero retention or data residency need the planned own-storage add-on.
120
+
121
+ ## AI disclosure
122
+
123
+ By default, the chat shows under the message field: "You’re chatting with an AI assistant. It can make mistakes." (`disclosure.text`).
124
+
125
+ ```ts
126
+ defineBot({
127
+ // …
128
+ widget: {
129
+ disclosure: { privacyUrl: 'https://www.example.com/privacy' }, // or a path such as '/privacy'
130
+ },
131
+ });
132
+ ```
133
+
134
+ - `privacyUrl` adds a "Privacy policy" link (`disclosure.privacy`). It must be an https URL or a path on your app.
135
+ - You can change both texts with `widget.texts` ([Styling](styling.md#1-configuration)).
136
+ - `disclosure: false` hides the note. Do that only when your page already tells visitors they are talking to an AI.
137
+
138
+ The note exists for the transparency duty in Article 50 of the EU AI Act: people should know when they interact with an AI system, unless that is obvious from the context. Who carries the duty, and whether the default note is enough for your site, depends on your role and setup. Check with your legal advisers.
139
+
140
+ ## Subprocessors
141
+
142
+ | Who | When | What they receive |
143
+ | --- | --- | --- |
144
+ | OpenAI | Always | Everything under [At OpenAI](#at-openai) |
145
+ | Upstash | With `@boldare/askdare-upstash` | Rate-limit counters, whose names hold IP addresses, visitor IDs and conversation IDs |
146
+ | Your hosting provider, such as Vercel or Heroku | Always | Every request to the app, with IP addresses, and the server log |
147
+ | Your error monitoring | With `onError` | Error reports, without message contents or API keys |
148
+
149
+ Your tools may send data to further services of your own.
150
+
151
+ ## Rotating secrets
152
+
153
+ ### `CHAT_SECRET`
154
+
155
+ `CHAT_SECRET` encrypts the session tokens, the resume tokens in browsers and the handles of uploaded files.
156
+
157
+ - **With `CHAT_SECRET_PREVIOUS`:** put the old secret there and set a new `CHAT_SECRET`. Existing tokens keep working, and new ones use the new secret. Remove the old one after 30 days, when the last resume tokens expire ([Going public](going-public.md#2-secrets-and-environment)).
158
+ - **Without it:** every visitor loses access to their current conversation. The chat forgets it and starts a new one at the next message, and files uploaded but not yet sent must be attached again. The old conversations stay in your OpenAI project, and nobody can reach them through the chat any more.
159
+
160
+ ### `IDENTITY_SECRET`
161
+
162
+ There is no previous-secret setting for `IDENTITY_SECRET`. When you change it:
163
+
164
+ - Identity tokens signed with the old secret are refused, and those visitors continue anonymously. Change the secret in your site's backend and in the askdare app at the same time.
165
+ - Signed-in visitors with an open chat become anonymous at the next session renewal, within about 15 minutes.
166
+ - Conversations started while signed in are bound to the old secret. Their users can't resume, read or delete them in the chat any more, and they stay in your OpenAI project.
167
+
168
+ ## For your privacy notice
169
+
170
+ - [ ] Name OpenAI, and Upstash if you use it, as processors, and your hosting provider.
171
+ - [ ] Say that conversations, including what visitors type and attach, are stored by OpenAI until deleted, and how visitors delete them.
172
+ - [ ] Describe the `localStorage` entry and its purpose.
173
+ - [ ] Mention IP addresses in rate limits and their retention.
174
+ - [ ] Decide on a retention policy for conversations.
175
+ - [ ] Keep the AI disclosure, and link this notice from it with `privacyUrl`.
@@ -0,0 +1,174 @@
1
+ # Wire protocol (version 1)
2
+
3
+ This is how the askdare server streams a chat turn to its clients: the embed iframe app now, `@boldare/askdare/react` later. The protocol belongs to askdare; raw OpenAI stream events are never forwarded ([ADR-0002](https://github.com/boldare/askdare/blob/main/docs/adr/0002-openai-only-no-leaked-types.md)).
4
+
5
+ Source of truth: `packages/askdare/src/protocol/`.
6
+
7
+ ## Endpoints
8
+
9
+ Every path sits under the route's base path: `/api/chat` by default, or `basePath` in `createChatHandler`. Any other path or method answers `404`.
10
+
11
+ | Method and path | Token | Purpose |
12
+ | --- | --- | --- |
13
+ | `GET /widget.js` | None | The loader script for your pages. `?bot=<id>` adds that bot's launcher look |
14
+ | `GET /embed/<bot>` | None | The chat document in the iframe. `?lang=<tag>` carries the embedding page's language for `locale: 'auto'`, and `?mode=inline` marks a chat inside the page |
15
+ | `GET /theme/<bot>.css` | None | The bot's theme and `widget.css`, with a content hash in `?v=` |
16
+ | `GET /assets/<file>` | None | The chat's script and stylesheet, and the preset fonts |
17
+ | `POST /session` | None | Starts or renews a visitor's session and answers an access token ([Identity](#identity)) |
18
+ | `POST /messages` | Access token | Sends a message and answers with the event stream ([Events](#events)) |
19
+ | `POST /history` | Access token | The visible messages of the visitor's conversation |
20
+ | `DELETE /conversation` | Access token | Deletes the visitor's conversation, and its files when the bot accepts attachments |
21
+ | `POST /attachments` | Access token | Uploads one file ([Attachments](#attachments)) |
22
+ | `POST /transcriptions` | Access token | Transcribes one dictation ([Dictation](#dictation)) |
23
+ | `POST /doctor` | Doctor token | Checks the setup for `askdare doctor` ([Diagnostics](#diagnostics)) |
24
+
25
+ - **`POST /messages`** takes `{ "botId", "text", "resumeToken"?, "attachments"?, "context"?, "locale"? }`. Without `resumeToken`, the server starts a new conversation and sends `conversation.created`. `locale` is the visitor's language tag, passed to the bot's instructions.
26
+ - **`POST /history`** takes `{ "botId", "resumeToken" }` and answers `{ "messages": [{ "id", "role", "text", "citations", "attachments" }] }`, where `attachments` is a count.
27
+ - **`DELETE /conversation`** takes the same body and answers `204` with no body.
28
+ - **Same origin only.** Every `POST` and `DELETE` except `/doctor` refuses cross-site requests with `401`. The API serves the chat's own iframe, which runs on the same origin.
29
+
30
+ ## Transport
31
+
32
+ - **Request:** `POST` with a JSON body and a `Bearer` access token. No cookies.
33
+ - **Response:** Server-Sent Events. Clients read them with `fetch` and a `ReadableStream`, because `EventSource` can't send a POST body.
34
+ - **Response headers:**
35
+ - `Content-Type: text/event-stream; charset=utf-8`
36
+ - `Cache-Control: no-cache, no-transform`
37
+ - `X-Accel-Buffering: no`
38
+ - `x-askdare-protocol: 1`
39
+ - **Framing:** one event per frame, `event: <type>` followed by `data: <json>` and a blank line. The JSON always repeats `type`.
40
+ - **First byte:** the server sends a comment (`: ok`) before contacting OpenAI. Heroku requires a first byte within 30 s.
41
+ - **Heartbeat:** a `: ping` comment every 15 s, on a timer that is independent of model output. Heroku closes connections after 55 s without bytes.
42
+
43
+ ## Events
44
+
45
+ | Type | Payload | Meaning |
46
+ |---|---|---|
47
+ | `conversation.created` | `resumeToken` | A new conversation started; store the token to resume it later |
48
+ | `user.ack` | `messageId` | The server accepted the user's message and assigned it this ID |
49
+ | `message.start` | `messageId` | The assistant started a reply |
50
+ | `reasoning` | `messageId`, `state: started \| done` | The model is thinking; show a status, never content |
51
+ | `text.delta` | `messageId`, `delta` | Append text to the reply (markdown) |
52
+ | `citation` | `messageId`, `index`, `title`, `url?` | Source for marker `[index]`; `url` is http(s) only |
53
+ | `tool.status` | `messageId`, `callId`, `name`, `state: running \| done \| error` | A tool ran; arguments and results stay on the server. `name: file_search` is the knowledge base search |
54
+ | `message.end` | `messageId`, `reason`, `resumeToken?` | The reply finished. A resumed conversation gets a fresh `resumeToken`, so it stays reachable 30 days after its last message. Reasons: `completed`, `max_steps`, `max_output_tokens`, `timeout`, `content_filter`, `aborted` |
55
+ | `error` | `code`, `message`, `retryAfterMs?`, `key?`, `params?` | The turn failed; `message` is safe to show ([keys and parameters](#error-keys-and-parameters)) |
56
+
57
+ ## Attachments
58
+
59
+ When a bot has `attachments` enabled, files travel separately from the message:
60
+
61
+ 1. `POST /attachments` with the raw file as the body (`Content-Type: application/octet-stream`), the `Bearer` token, and two headers: `x-askdare-bot` (bot ID) and `x-askdare-filename` (URL-encoded name). The server detects the type from the bytes, uploads the file to OpenAI and answers `{ "attachment": { "id", "kind": "image" | "pdf", "name", "size" } }`.
62
+ 2. `POST /messages` with `"attachments": ["<id>", …]` next to `text`. `text` may be empty when files are attached.
63
+
64
+ `id` is an encrypted handle bound to the bot and the visitor, valid for 1 hour. OpenAI file IDs never reach clients, and the server refuses raw file IDs. History (`POST /history`) reports only how many files a message had.
65
+
66
+ ## Dictation
67
+
68
+ When a bot has `voice` enabled, `POST /transcriptions` takes a recording as the raw body (`Content-Type: application/octet-stream`) with the `Bearer` token and two headers: `x-askdare-bot` (bot ID) and `x-askdare-languages` (comma-separated language tags, used as hints). It answers `{ "text": "…" }`. Clients put the text in the message field for the visitor to check; nothing is sent to the model.
69
+
70
+ ## Identity
71
+
72
+ `POST /session` takes an optional `identityToken`: the JWT from `askdare('identify')`, or `null` to sign out. Without it, a renewal keeps the identity the previous access token had. Widgets send the page's latest token with every session request. The answer adds `identified` (boolean), `identityKey` (an HMAC that tells users apart without revealing them; widgets keep one conversation handle per key), `resumeToken` when signing in adopted an anonymous conversation, and `identityProblem` when a token was refused. A signed-in access token expires with the identity token. Identities travel inside the encrypted access token; clients never see the claims ([ADR-0009](https://github.com/boldare/askdare/blob/main/docs/adr/0009-visitor-identity.md)).
73
+
74
+ ## Diagnostics
75
+
76
+ `POST /doctor` serves `npx @boldare/askdare doctor`. It takes a 2-minute token that the CLI encrypts with `CHAT_SECRET` and answers with a report on each bot: whether the server's key, models, vector store and permissions work. The report format is internal and may change in any release.
77
+
78
+ ## Error codes
79
+
80
+ These codes are stable and safe to branch on in the UI:
81
+
82
+ | Code | Meaning |
83
+ |---|---|
84
+ | `invalid_input` | The request was rejected as malformed or too long |
85
+ | `unauthorized` | The access token is missing or invalid |
86
+ | `session_expired` | The access token expired; refresh it and retry |
87
+ | `not_found` | Unknown bot or conversation (also returned for conversations you don't own) |
88
+ | `rate_limited` | Too many requests; wait `retryAfterMs` |
89
+ | `budget_exceeded` | The bot's daily budget is spent |
90
+ | `bot_unavailable` | The bot is disabled (kill switch) or misconfigured |
91
+ | `content_blocked` | Reserved for moderation; this version never sends it |
92
+ | `timeout` | The turn exceeded the server deadline |
93
+ | `upstream_error` | OpenAI failed |
94
+ | `internal_error` | Unexpected server error |
95
+
96
+ ## Error keys and parameters
97
+
98
+ Next to `code` and `message`, an `error` event can carry:
99
+
100
+ | Field | Meaning |
101
+ | --- | --- |
102
+ | `retryAfterMs` | How long to wait before retrying, in milliseconds |
103
+ | `key` | The [text key](texts.md) for this error, such as `error.messageLength`, so a client can show it in the visitor's language |
104
+ | `params` | Values for the placeholders in that text, such as `{ "max": 4000 }` |
105
+
106
+ `message` is the English text. The widget looks `key` up in the bot's texts, overrides included, fills in `params`, and shows `message` when it has no text for the key.
107
+
108
+ `key` is absent in two cases:
109
+
110
+ - **Explained in development.** When `NODE_ENV` is `development`, as under `next dev`, a `bot_unavailable` error caused by the setup carries a description for you in `message` instead of a key: a missing or weak secret, a missing `OPENAI_API_KEY`, a key OpenAI rejects, a model the project can't use. The widget shows that description as it is. In production, the server never sends one.
111
+ - **Messages for developers.** Some refusals carry only a plain English `message`, such as `Expected an application/json body.`, `Malformed JSON.`, `Request body is too large.` and `Cross-origin requests are not allowed.`
112
+
113
+ Error responses outside the stream have the same fields, inside `error`:
114
+
115
+ ```json
116
+ {
117
+ "error": {
118
+ "code": "invalid_input",
119
+ "message": "Messages must be between 1 and 4000 characters.",
120
+ "key": "error.messageLength",
121
+ "params": { "max": 4000 }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## HTTP status codes
127
+
128
+ | Status | Code | When |
129
+ | --- | --- | --- |
130
+ | `200` | | Success. For `POST /messages`, the stream has started, and later failures arrive as an `error` event |
131
+ | `204` | | `DELETE /conversation` succeeded, also when the conversation was already gone |
132
+ | `400` | `invalid_input` | The body is not what the endpoint expects (wrong `Content-Type`, malformed JSON, a JSON body over 256 KB), or the message, file or recording is refused, including when it is too large |
133
+ | `401` | `unauthorized` | The `Bearer` token is missing or invalid, or the request is cross-site |
134
+ | `401` | `session_expired` | The access token expired; renew it with `POST /session` and retry |
135
+ | `404` | `not_found` | An unknown path, bot or asset; a resume token that is invalid or belongs to someone else; a conversation that no longer exists |
136
+ | `429` | `rate_limited` | A rate limit was reached, the conversation reached `maxTurns`, or another dictation is still being transcribed |
137
+ | `429` | `budget_exceeded` | A daily token or speech budget is spent |
138
+ | `500` | `internal_error` | Unexpected server error |
139
+ | `502` | `upstream_error` | OpenAI failed or could not be reached |
140
+ | `503` | `bot_unavailable` | The kill switch (`ASKDARE_DISABLED=true`) is on, a secret or `OPENAI_API_KEY` is missing or invalid, or OpenAI refused the key or the model |
141
+ | `504` | `timeout` | OpenAI did not answer in time |
142
+
143
+ An error response with `retryAfterMs` also has a `Retry-After` header, in whole seconds rounded up. For `budget_exceeded`, it lasts until the next midnight UTC.
144
+
145
+ | Endpoint | Statuses |
146
+ | --- | --- |
147
+ | `GET /widget.js` | `200` |
148
+ | `GET /embed/<bot>`, `GET /theme/<bot>.css`, `GET /assets/<file>` | `200`, `404` |
149
+ | `POST /session` | `200`, `400`, `401`, `404`, `429`, `503` |
150
+ | `POST /messages` | `200`, `400`, `401`, `404`, `429`, `503` |
151
+ | `POST /history` | `200`, `400`, `401`, `404`, `429`, `502`, `503`, `504` |
152
+ | `DELETE /conversation` | `204`, `400`, `401`, `404`, `429`, `502`, `503`, `504` |
153
+ | `POST /attachments`, `POST /transcriptions` | `200`, `400`, `401`, `404`, `429`, `502`, `503`, `504` |
154
+ | `POST /doctor` | `200`, `401`, `429`, `503` |
155
+
156
+ Any endpoint can also answer `500`.
157
+
158
+ ## Protocol version
159
+
160
+ `PROTOCOL_VERSION`, exported from `@boldare/askdare`, is the version of this protocol: `1`. Every event stream (`POST /messages` answered with `200`) carries it in the `x-askdare-protocol` response header. Other responses don't have this header.
161
+
162
+ The widget compares the header with the version it was built with. On a mismatch, it doesn't read the stream and shows `error.updated` ("The assistant was updated. Please reload the page.").
163
+
164
+ ## Client rules
165
+
166
+ - **Tolerate the unknown.** Ignore unknown event types and unknown fields. Newer servers may add them within the same protocol version.
167
+ - **Check the version.** Compare `x-askdare-protocol` with the version the client supports. On a mismatch, show a "please reload" state.
168
+ - **Run a watchdog.** If no bytes (events or comments) arrive for 45 s, abort and re-fetch the conversation history.
169
+ - **Render text safely.** Treat `text.delta` content as untrusted markdown and sanitize it before rendering.
170
+
171
+ ## Versioning
172
+
173
+ - **Adding** an event type or an optional field is not breaking.
174
+ - **Removing or renaming** an event or field, or changing a field's meaning, is breaking. It bumps the protocol version and requires a minor release of `@boldare/askdare` (ADR-0006).
@@ -0,0 +1,166 @@
1
+ # Styling and customization
2
+
3
+ Three levels, each with more freedom than the last ([ADR-0008](https://github.com/boldare/askdare/blob/main/docs/adr/0008-ui-customization-and-themes.md)). Streaming, security and limits stay in the library whatever you change.
4
+
5
+ | Level | How | For |
6
+ | --- | --- | --- |
7
+ | 1. Configuration | `widget: { … }` in `defineBot` | Texts, language, theme, suggestions, avatar, disclosure |
8
+ | 2. Your own CSS | `widget.css`, plus `::part()` for the launcher | Any look, on top of the parts and tokens below |
9
+ | 3. Page JavaScript | `askdare(…)` and attributes on the script tag | Your own button, the chat inside your page, per-page texts and context |
10
+
11
+ ## 1. Configuration
12
+
13
+ ```ts
14
+ defineBot({
15
+ // …
16
+ widget: {
17
+ title: 'Brightloop assistant',
18
+ subtitle: 'Answers from our help center',
19
+ greeting: 'Hi! How can I help?',
20
+ starters: ['What does the Business plan include?'],
21
+ avatar: '/assistant.png',
22
+ locale: 'auto', // the page's language, else the visitor's browser
23
+ texts: { 'composer.placeholder': 'Ask about plans, support or security…' },
24
+ disclosure: { privacyUrl: 'https://www.example.com/privacy' },
25
+ theme: 'aura', // see docs/themes.md
26
+ colorScheme: 'auto',
27
+ },
28
+ });
29
+ ```
30
+
31
+ - **Language.** Texts ship in English and Polish. `locale: 'auto'` follows the `lang` of the page that embeds the chat, else the visitor's browser; any other language shows English unless you set `texts`. The locale is also the default answer language.
32
+ - **Bold words.** In a greeting, `**words**` are shown in bold. Aura uses this for its two-weight headline.
33
+ - **Texts.** Every text has a key; `texts` overrides any of them. `title`, `greeting`, `placeholder` and `launcherLabel` are shortcuts for `header.title`, `welcome.greeting`, `composer.placeholder` and `launcher.label`, and a configured text is used in every language. The full list, with plurals and how to add a language, is in [Texts](texts.md); the keys are public API.
34
+ - **Disclosure.** By default the chat says, under the message field, that the visitor is talking to an AI (EU AI Act, art. 50). Add `privacyUrl` for a link to your privacy policy. `disclosure: false` hides it; only do that when your page already says so.
35
+
36
+ ## 2. Your own CSS
37
+
38
+ ```ts
39
+ widget: {
40
+ css: `
41
+ [data-ask-part="bubble"] { border-radius: 6px; }
42
+ [data-ask-part="message"][data-role="user"] [data-ask-part="bubble"] { font-weight: 500; }
43
+ :root { --ask-color-accent: #0f766e; }
44
+ `,
45
+ }
46
+ ```
47
+
48
+ Your CSS loads after the theme. The library's styles are in `@layer askdare`, so yours wins without `!important`. The chat's Content Security Policy still applies: images load only from your app or as `data:` and `blob:` URLs, and fonts only from your app.
49
+
50
+ A full example is the demo site's BLDR theme, in Boldare's style: `defineTheme` over Halo for colors, type and square corners, a few lines of `widget.css` for the hard shadows and highlights, and `::part(launcher)` on the page for the button. The code is in [Themes: a theme in your brand](themes.md#example-a-theme-in-your-brand).
51
+
52
+ **Tokens:** the `--ask-*` variables listed in [Themes](themes.md#tokens). Only those are public API. The chat's stylesheet also defines the variables below; they are internal and can change in any release, so don't build on them.
53
+
54
+ | Internal variable | What it holds |
55
+ | --- | --- |
56
+ | `--ask-space` | The spacing unit: 8px, or 6px with the `compact` density |
57
+ | `--ask-radius-small` | A smaller radius, 0.6 × `--ask-radius` |
58
+ | `--ask-shadow` | The shadow of raised elements, mixed from `--ask-color-text` |
59
+ | `--ask-ring` | The ring around the focused message field, mixed from `--ask-color-accent` |
60
+ | `--ask-glass` | The translucent surface of the `glass` variant, mixed from `--ask-color-surface` |
61
+ | `--ask-ease` | The easing curve of transitions |
62
+ | `--ask-level` | The microphone level while recording, set by the chat |
63
+ | `--ask-aura-gradient-end` | Where Aura's gradient on the visitor's bubbles and the send button ends |
64
+
65
+ The launcher's short variables on your page (`--a`, `--o` and the like) are internal too.
66
+
67
+ **Parts** (`[data-ask-part="…"]`, public API): `panel`, `header`, `identity`, `identity-text`, `title`, `subtitle`, `actions`, `new-conversation`, `close`, `avatar`, `orb`, `messages`, `welcome`, `greeting`, `starters`, `starter`, `persona`, `persona-name`, `presence`, `intro`, `intro-card`, `message`, `message-body`, `bubble`, `image-alt`, `typing`, `citations`, `citation`, `attachments-note`, `status`, `error`, `composer`, `composer-box`, `input`, `attach`, `voice`, `send`, `attachments`, `message-attachments`, `attachment`, `attachment-preview`, `attachment-badge`, `attachment-name`, `attachment-status`, `attachment-remove`, `recorder`, `recorder-level`, `recorder-label`, `recorder-time`, `recorder-cancel`, `recorder-done`, `send-label` (shown by Aura), `disclosure`, and the question before a new conversation: `confirm-backdrop`, `confirm`, `confirm-icon`, `confirm-text`, `confirm-actions`, `confirm-cancel`, `confirm-accept`.
68
+
69
+ `identity-text` holds the title and subtitle in the header. `image-alt` stands in for an image in a reply: the chat never loads images that the model writes, so it shows their alt text in brackets.
70
+
71
+ **States** are attributes on the parts:
72
+
73
+ | Part | Attribute | Values |
74
+ | --- | --- | --- |
75
+ | `panel` | `data-busy` | Present while the assistant answers or a dictation runs |
76
+ | `panel` | `data-dragging` | Present while files are dragged over the chat (only when the bot accepts attachments) |
77
+ | `message` | `data-role` | `user`, `assistant` |
78
+ | `message` | `data-status` | `streaming`, `done`, `error` |
79
+ | `attachment` | `data-status` | `uploading`, `ready`, `error`, on files not sent yet; absent in sent messages |
80
+ | `attachment` | `data-kind` | `image`, `pdf`; absent until the chat has recognized the file |
81
+ | `recorder` | `data-state` | `recording`, `transcribing` |
82
+ | `avatar` | `data-kind` | `orb`, `icon`, `initials`, `image` |
83
+ | `avatar` | `data-size` | `small` in the header, next to the assistant's messages and in the `intro` welcome; `large` in the `hero` and `persona` welcome |
84
+ | `welcome` | `data-layout` | `hero`, `persona`, `intro` |
85
+
86
+ Select a present-or-absent attribute by its name alone, such as `[data-ask-part="panel"][data-dragging]`.
87
+
88
+ **Variants** of the theme are attributes on `<html>`: `data-ask-preset`, `data-ask-scheme`, `data-ask-avatar`, `data-ask-welcome`, `data-ask-launcher`, `data-ask-bubbles`, `data-ask-surface`, `data-ask-density`, `data-ask-composer`.
89
+
90
+ **The launcher** follows the theme's `launcher` variant: `circle` is a chat icon, `pill` the bot's picture or initials with its label, and `orb` the theme's orb with its label (Aura and Halo). The label is `launcherLabel`. It lives in a shadow root on your page. Style it from your own CSS with `::part`:
91
+
92
+ ```css
93
+ [data-askdare]::part(launcher) { right: 32px; bottom: 32px; }
94
+ [data-askdare]::part(panel) { border-radius: 8px; }
95
+ ```
96
+
97
+ The shadow root has three parts: `launcher`, `panel` (the dialog that holds the chat) and `unavailable` (the notice in the panel when the chat doesn't load). The element that holds the shadow root has `data-askdare` set to the bot ID, so `[data-askdare="support"]::part(launcher)` styles one bot's launcher only.
98
+
99
+ ### Brand fonts
100
+
101
+ The chat's Content Security Policy allows fonts and stylesheets only from your app's own origin (`font-src 'self'`, `style-src 'self'`). So:
102
+
103
+ - Serve the font files from your app, for example from `public/fonts/` in Next.js.
104
+ - Declare them with `@font-face` in `widget.css`. Use a path that starts with `/`: `widget.css` is served from under the chat's base path, so a relative URL would point there.
105
+ - Fonts and stylesheets from other origins, such as a font CDN, are blocked, and so are fonts in `data:` URLs.
106
+
107
+ ```ts
108
+ widget: {
109
+ css: `
110
+ @font-face {
111
+ font-family: "Brand Sans";
112
+ src: url("/fonts/brand-sans.woff2") format("woff2");
113
+ font-display: swap;
114
+ }
115
+ :root {
116
+ --ask-font-family: "Brand Sans", system-ui, sans-serif;
117
+ --ask-font-family-heading: "Brand Sans", system-ui, sans-serif;
118
+ }
119
+ `,
120
+ }
121
+ ```
122
+
123
+ Every preset sets both font tokens, so set both. You can also set them as the `fontFamily` and `headingFamily` tokens of a theme ([Themes](themes.md#adjust-a-preset)) and keep only the `@font-face` rule in `widget.css`. The launcher is on your page, not in the chat, so it can use your page's fonts through `::part(launcher)`.
124
+
125
+ ## 3. Page JavaScript
126
+
127
+ ```html
128
+ <!-- Your own button instead of the launcher -->
129
+ <button onclick="askdare('open')">Chat with us</button>
130
+ <script src="https://app.example.com/api/chat/widget.js?bot=support" data-launcher="none" async></script>
131
+
132
+ <!-- The chat inside your page -->
133
+ <div id="chat" style="height: 600px"></div>
134
+ <script src="https://app.example.com/api/chat/widget.js?bot=support" data-mode="inline" data-target="#chat" async></script>
135
+ ```
136
+
137
+ | Call | Effect |
138
+ | --- | --- |
139
+ | `askdare('open')`, `askdare('close')`, `askdare('toggle')` | Shows or hides the chat. Closing returns focus to whatever opened it. |
140
+ | `askdare('identify', token)` | Signs the visitor in with a token from your backend, or out with `null`; see [Signed-in visitors](identity.md). |
141
+ | `askdare('configure', { greeting, starters, context })` | Per-page greeting and suggestions, and `context`: a short description of the page (up to 1000 characters) for the assistant. |
142
+
143
+ `configure` accepts only those three fields: never the model, instructions or tools. The context is off unless the bot opts in with `pageContext: true` in `defineBot`. It then travels with the visitor's message, as a marked part of their own turn: anyone can set it from their browser, so it gets no more trust than what the visitor types, and it never becomes part of the bot's instructions. It is sent again only when it changes, and the visitor's history doesn't show it. Define the small queue first, then call it any time:
144
+
145
+ ```html
146
+ <script>
147
+ // Safe to call before the script has loaded: calls queue up and run once it is ready.
148
+ window.askdare = window.askdare || function () {
149
+ (window.askdare.q = window.askdare.q || []).push(arguments);
150
+ };
151
+ askdare('configure', {
152
+ greeting: 'Questions about our prices?',
153
+ starters: ['How much is the Business plan?'],
154
+ context: 'Pricing page listing the Free, Team and Business plans.',
155
+ });
156
+ </script>
157
+ <script src="https://app.example.com/api/chat/widget.js?bot=support" async></script>
158
+ ```
159
+
160
+ An inline chat loads with the page without taking focus or scrolling to itself.
161
+
162
+ In a React app, Next.js included, add the script after hydration, for example from a `useEffect` in a small client component. The loader adds the chat inside your element, and React must not find it there while it hydrates. The demo site's [`chat-script.tsx`](https://github.com/boldare/askdare/blob/main/site/app/chat-script.tsx) does this. The chat loads once per page load and lives as long as its element: put the element in a layout that stays, or link to the page with full page loads, because a client-side navigation away and back leaves the element empty.
163
+
164
+ ## Accessibility
165
+
166
+ Accessibility is not configurable away. The focus ring is always visible, motion follows `prefers-reduced-motion`, the chat is a labelled dialog, and Escape closes it (or cancels a dictation). Every preset passes axe in every state, and its colors pass WCAG AA (AAA for text in Care); a customized theme with weak contrast triggers a warning in development.
@@ -0,0 +1,48 @@
1
+ # Support
2
+
3
+ What askdare is tested with and promises to keep working. The reasoning is in [ADR-0006](https://github.com/boldare/askdare/blob/main/docs/adr/0006-support-matrix-and-versioning.md).
4
+
5
+ ## Your app
6
+
7
+ | | Supported | Not supported |
8
+ | --- | --- | --- |
9
+ | Node.js | 22 and 24 | 20 and older |
10
+ | Next.js | 16.x with the App Router, on the Node.js runtime; Turbopack and `next build --webpack` | Pages Router, Edge runtime, static export |
11
+ | React | 19 | 18 |
12
+ | zod | 4.1 or later | 3 |
13
+ | TypeScript | 5.9, 6.0 and 7.0 | |
14
+ | Hosting | Vercel and Heroku | Other hosts that run `next start` should work, but aren't tested |
15
+
16
+ ## Visitors' browsers
17
+
18
+ The current and previous major versions of Chrome, Edge, Firefox and Safari, on desktop and mobile, at least:
19
+
20
+ | Browser | Version |
21
+ | --- | --- |
22
+ | Chrome, Edge | 111 |
23
+ | Firefox | 121 |
24
+ | Safari (macOS and iOS) | 16.4 |
25
+
26
+ Older browsers may show the chat with a plainer look or not at all. Dictation also needs a microphone and `MediaRecorder`, which all of these have.
27
+
28
+ Every release is tested in Chromium, Firefox and WebKit: embedding, the page API and accessibility (axe). Safari on iOS, dictation in Safari and screen readers (VoiceOver, NVDA) are checked by hand before each release candidate.
29
+
30
+ ## What a visitor downloads
31
+
32
+ | | Gzipped | Budget |
33
+ | --- | --- | --- |
34
+ | The loader (`widget.js`), on every page | about 3 KB | 5 KB |
35
+ | The chat itself, when the visitor opens it | about 34 KB, plus 5 KB of styles | 50 KB and 8 KB |
36
+ | One theme's fonts, from your app | up to 82 KB | 82 KB |
37
+
38
+ The build fails when one of these grows past its budget.
39
+
40
+ ## How we keep it working
41
+
42
+ - **Every change:** lint, types, unit and browser tests, the package checks (publint, are-the-types-wrong), the size budgets, and a snapshot of the published types. A change to the public API has to update the snapshot on purpose and come with a changeset.
43
+ - **Every night:** the same against `next@canary` and the newest `openai` SDK, so a breaking release shows up before it reaches you.
44
+ - **Every day:** a live test against the real OpenAI API with a cheap model: text, a tool call, file search and an image. It catches retired models and API changes.
45
+
46
+ ## Versions
47
+
48
+ askdare is in 0.x. Minor releases may break things; patch releases never do. A removal is announced by a deprecation warning one minor release earlier, and every release lists its changes.