@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,258 @@
1
+ # Embedding the chat
2
+
3
+ The chat runs in your Next.js app and shows up on any page that loads one script from it: pages of the app itself, or any other website. The script draws the chat button in a shadow root and opens the chat in an iframe that your app serves. The design is in [ADR-0005](https://github.com/boldare/askdare/blob/main/docs/adr/0005-embedding-model.md).
4
+
5
+ ## The snippet
6
+
7
+ Paste one tag into the HTML of every page that should show the chat:
8
+
9
+ ```html
10
+ <script src="https://app.example.com/api/chat/widget.js?bot=support" async></script>
11
+ ```
12
+
13
+ - `https://app.example.com` is where your Next.js app runs.
14
+ - `/api/chat` is the route that `init` created.
15
+ - `support` is the `id` of the bot in `askdare.config.ts`.
16
+
17
+ The site must be in the bot's `security.allowedOrigins`. Pages of the app itself are always allowed.
18
+
19
+ Load it as a plain script. Don't add `type="module"`, `crossorigin` or `integrity`: the loader finds its settings through its own `<script>` tag, and `widget.js` is served without CORS headers.
20
+
21
+ The loader passes the `lang` of the page's `<html>` to the chat. A bot with `locale: 'auto'` follows it; see [Styling](styling.md#1-configuration).
22
+
23
+ ## Script tag attributes
24
+
25
+ The loader reads these four attributes from its own `<script>` tag, and no others:
26
+
27
+ | Attribute | Value | Effect |
28
+ | --- | --- | --- |
29
+ | `data-bot` | A bot ID | The bot to load. Wins over `?bot=` in `src`. |
30
+ | `data-mode` | `inline` | The chat fills the element that `data-target` names instead of floating over the page. Any other value is ignored. |
31
+ | `data-target` | A CSS selector, such as `#chat` | The element for an inline chat. Read only with `data-mode="inline"`. |
32
+ | `data-launcher` | `none` | No chat button. Open the chat from your own button with `askdare('open')`. Ignored for an inline chat. |
33
+
34
+ An inline chat waits until the page has parsed its target element, so the script can sit in `<head>`. When no element matches the selector, the console says `[askdare] No element matches #chat.` and the chat floats instead. Without `data-target`, it floats too.
35
+
36
+ Examples of both modes are in [Styling](styling.md#3-page-javascript).
37
+
38
+ ### `?bot=` or `data-bot`
39
+
40
+ Both name the bot, and `data-bot` wins when both are set. They differ in one way:
41
+
42
+ - **`?bot=`** also brings the bot's button. Your app builds `widget.js` for the bot in `?bot=`, with its colors, button style and texts.
43
+ - **`data-bot`** alone gets a default button: a blue chat icon with English texts.
44
+
45
+ Use `?bot=`. If you also set `data-bot`, give it the same ID; with a different one, the button falls back to the default look.
46
+
47
+ Without either, nothing loads, and the console says `[askdare] Load the widget as widget.js?bot=<bot id>.`
48
+
49
+ ## Several bots in one app
50
+
51
+ One app can serve several bots, for example one for support and one for sales:
52
+
53
+ ```ts
54
+ // askdare.config.ts
55
+ import { defineBot } from '@boldare/askdare';
56
+
57
+ export const bots = [
58
+ defineBot({
59
+ id: 'support',
60
+ model: process.env.ASKDARE_MODEL ?? '',
61
+ instructions: 'You answer support questions about our product.',
62
+ security: { allowedOrigins: ['https://www.example.com'] },
63
+ }),
64
+ defineBot({
65
+ id: 'sales',
66
+ model: process.env.ASKDARE_MODEL ?? '',
67
+ instructions: 'You answer questions about plans and prices.',
68
+ security: { allowedOrigins: ['https://shop.example.com'] },
69
+ }),
70
+ ];
71
+ ```
72
+
73
+ - The one route serves every bot in `bots`, through `createChatHandler({ bots })`. You don't need a second route.
74
+ - A bot ID uses 1 to 48 lowercase letters, digits or dashes, and starts with a letter or digit. Two bots with the same ID fail with `[askdare] Duplicate bot id "sales".`
75
+ - Each bot has its own `allowedOrigins`, look and limits.
76
+ - Each page picks its bot with `?bot=`: `widget.js?bot=support` on one site, `widget.js?bot=sales` on another.
77
+
78
+ A page shows one chat. The loader starts once per page load, and a second snippet on the same page does nothing, for the same bot or another one. In an app that navigates without full page loads, the first chat stays until the next full page load.
79
+
80
+ ## Your site's Content Security Policy
81
+
82
+ If the site that embeds the chat sends a Content Security Policy, allow your app's origin where the loader needs it:
83
+
84
+ | Directive | Add | Why |
85
+ | --- | --- | --- |
86
+ | `script-src` | Your app's origin | The page loads `widget.js` from your app. |
87
+ | `frame-src` | Your app's origin | The chat is an iframe of `/api/chat/embed/<bot>` on your app. Without `frame-src`, browsers use `child-src`, then `default-src`. |
88
+ | `img-src` | Your app's origin, or `data:` | Only for the `pill` button with a bot `avatar`: a path loads from your app, a data URL needs `data:`. When the image is blocked, the button shows the bot's initials. |
89
+
90
+ For example, with the chat at `https://app.example.com`:
91
+
92
+ ```text
93
+ Content-Security-Policy: script-src 'self' https://app.example.com; frame-src https://app.example.com; img-src 'self' https://app.example.com
94
+ ```
95
+
96
+ You don't need to change anything else:
97
+
98
+ - **`style-src`:** the button's styles are a constructed stylesheet in its shadow root, and its colors are set through the CSSOM. A Content Security Policy doesn't block either, so you don't need `'unsafe-inline'`. Only Safari before 16.4, older than the [supported browsers](support.md#visitors-browsers), falls back to a `<style>` element, which a strict `style-src` blocks.
99
+ - **`font-src`:** the button uses the system font. The chat's own fonts load inside the iframe.
100
+ - **`connect-src`:** the loader sends no requests of its own. The iframe talks to your app under the chat's own policy.
101
+
102
+ Your own inline scripts follow your policy as usual. Under a policy without `'unsafe-inline'`, give the `askdare(…)` queue script a nonce or hash, or move it into a file. Attach your button's click handler with `addEventListener` instead of an `onclick` attribute.
103
+
104
+ Dictation needs the microphone. If your site sends `Permissions-Policy: microphone=()`, see [Voice input](voice-input.md#embedding).
105
+
106
+ ## What your app sends back
107
+
108
+ The chat page, `/api/chat/embed/<bot>`, answers with these headers:
109
+
110
+ | Header | Value |
111
+ | --- | --- |
112
+ | `Content-Security-Policy` | `default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data: blob:; font-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self' <allowedOrigins>` |
113
+ | `Permissions-Policy` | `camera=(), geolocation=(), microphone=()`, or `microphone=(self)` for a bot with `voice` |
114
+ | `Referrer-Policy` | `strict-origin` |
115
+ | `Cross-Origin-Opener-Policy` | `same-origin` |
116
+ | `X-Content-Type-Options` | `nosniff` |
117
+ | `Cache-Control` | `no-store` |
118
+
119
+ `frame-ancestors` decides who may embed the chat: `'self'`, which is your app, plus the bot's `security.allowedOrigins`. With `allowedOrigins: ['https://www.example.com']`, the header ends in `frame-ancestors 'self' https://www.example.com`. Browsers enforce it, so the list holds even if someone copies the snippet.
120
+
121
+ `widget.js` is served with `Cache-Control: public, max-age=300, stale-while-revalidate=86400`, or `no-cache` under `next dev`. A new button look can take a few minutes to show on other sites.
122
+
123
+ Don't let anything in front of the route add `X-Frame-Options` or a second Content Security Policy to the chat page. A second policy applies on top of askdare's and can block the sites on your list. `doctor` reports both; see also [Deployment](deployment.md#behind-another-proxy-or-cdn).
124
+
125
+ ### Allowed origins
126
+
127
+ Each entry in `security.allowedOrigins` is one exact origin:
128
+
129
+ - `https://` plus the host, and a port if the site uses one, such as `https://www.example.com`.
130
+ - `http://` only for `localhost`, `127.0.0.1`, `[::1]` and `*.localhost`.
131
+ - No path, no trailing slash and no wildcard.
132
+ - Up to 50 per bot.
133
+
134
+ `https://example.com` and `https://www.example.com` are different origins; list both if both serve pages. `defineBot` rejects anything else with `must be an https origin like https://www.example.com (no path or wildcard; http only for localhost)`.
135
+
136
+ ### On a site that isn't allowed
137
+
138
+ The browser refuses to show the chat page inside the site. The loader waits for the chat to say it's ready. After about 8 seconds, the panel shows "The chat couldn’t load. Please try again later." (the `launcher.unavailable` text, in the visitor's language), and the console says:
139
+
140
+ ```text
141
+ [askdare] The chat "support" didn't load on https://www.example.com. Is this origin in the bot's security.allowedOrigins? Check with: npx @boldare/askdare doctor --origin https://www.example.com
142
+ ```
143
+
144
+ The 8 seconds start when the visitor first opens the chat, or when the page loads for an inline chat. The same happens when the bot ID doesn't exist in the app, or when something in front of the route blocks the chat page.
145
+
146
+ Check a site against the running app:
147
+
148
+ ```bash
149
+ npx @boldare/askdare doctor --origin https://www.example.com
150
+ ```
151
+
152
+ For a deployed app, add `--url https://app.example.com --env-file .env.production.local`. See [Command line](cli.md#doctor).
153
+
154
+ ## Test on a second origin locally
155
+
156
+ Pages of the app itself are always allowed, so `http://localhost:3000` never tests `allowedOrigins`. Use a second origin, for example `http://127.0.0.1:4000`, against the app on `http://localhost:3000`:
157
+
158
+ 1. Add `http://127.0.0.1:4000` to the bot's `security.allowedOrigins`, or to `ASKDARE_ALLOWED_ORIGINS` in `.env.local` (see [Staging and production](#staging-and-production)). Restart `next dev`.
159
+ 2. Create a test page, for example `embed-test/index.html`:
160
+
161
+ ```html
162
+ <!doctype html>
163
+ <html lang="en">
164
+ <head>
165
+ <meta charset="utf-8">
166
+ <title>Embed test</title>
167
+ </head>
168
+ <body>
169
+ <h1>Embed test</h1>
170
+ <script src="http://localhost:3000/api/chat/widget.js?bot=support" async></script>
171
+ </body>
172
+ </html>
173
+ ```
174
+
175
+ 3. Serve the folder on port 4000 with any static file server, for example:
176
+
177
+ ```bash
178
+ python3 -m http.server 4000 --bind 127.0.0.1 --directory embed-test
179
+ ```
180
+
181
+ 4. Open `http://127.0.0.1:4000` and check it:
182
+
183
+ ```bash
184
+ npx @boldare/askdare doctor --origin http://127.0.0.1:4000
185
+ ```
186
+
187
+ Keep the test origin out of production. Putting it only in `.env.local` does that.
188
+
189
+ ## Next.js apps
190
+
191
+ In your own Next.js app, load the chat with `next/script` in the root layout. This is what `init` adds:
192
+
193
+ ```tsx
194
+ // app/layout.tsx
195
+ import Script from 'next/script';
196
+ import type { ReactNode } from 'react';
197
+
198
+ export default function RootLayout({ children }: { readonly children: ReactNode }) {
199
+ return (
200
+ <html lang="en">
201
+ <body>
202
+ {children}
203
+ <Script src="/api/chat/widget.js?bot=support" strategy="afterInteractive" />
204
+ </body>
205
+ </html>
206
+ );
207
+ }
208
+ ```
209
+
210
+ - `strategy="afterInteractive"` loads the script after React has hydrated the page, so React never finds the loader's elements in `<body>`.
211
+ - The path is relative: the app serves the script itself.
212
+ - `next/script` passes `data-*` attributes on to the tag, for example `data-launcher="none"`.
213
+
214
+ An inline chat inside a React app has a hydration caveat. [Styling](styling.md#3-page-javascript) explains it.
215
+
216
+ ## Staging and production
217
+
218
+ Each environment usually embeds the chat on different sites. Read the list from an environment variable, for example `ASKDARE_ALLOWED_ORIGINS`, split on commas:
219
+
220
+ ```ts
221
+ // askdare.config.ts
222
+ import { defineBot } from '@boldare/askdare';
223
+
224
+ // Sites that may embed the chat, comma-separated. The app's own origin is always allowed.
225
+ const allowedOrigins = (process.env.ASKDARE_ALLOWED_ORIGINS ?? '')
226
+ .split(',')
227
+ .map((origin) => origin.trim())
228
+ .filter(Boolean);
229
+
230
+ export const bots = [
231
+ defineBot({
232
+ id: 'support',
233
+ model: process.env.ASKDARE_MODEL ?? '',
234
+ instructions: 'You are the assistant on our website.',
235
+ security: { allowedOrigins },
236
+ }),
237
+ ];
238
+ ```
239
+
240
+ Then set it per environment:
241
+
242
+ ```bash
243
+ # Production
244
+ ASKDARE_ALLOWED_ORIGINS=https://www.example.com,https://shop.example.com
245
+ # Staging
246
+ ASKDARE_ALLOWED_ORIGINS=https://staging.example.com
247
+ # .env.local
248
+ ASKDARE_ALLOWED_ORIGINS=http://127.0.0.1:4000
249
+ ```
250
+
251
+ - askdare doesn't read this variable itself; your config does. The name is only a convention.
252
+ - Without the variable, only the app's own pages may embed the chat.
253
+ - `defineBot` checks every entry, so an invalid one stops the build or the app's start.
254
+ - Set it in your host's settings for every environment, then deploy. Check each site with `doctor --origin`.
255
+
256
+ ## Page JavaScript
257
+
258
+ The page can open and close the chat, sign the visitor in, and set a greeting, suggestions and page context per page: `askdare('open')`, `askdare('close')`, `askdare('toggle')`, `askdare('identify', token)` and `askdare('configure', { … })`. See [Styling](styling.md#3-page-javascript) and [Signed-in visitors](identity.md).
@@ -0,0 +1,116 @@
1
+ # Getting started
2
+
3
+ From an empty Next.js app to a working chat in about 15 minutes. You need Node 22 or later, a Next.js 16 app that uses the App Router, and an OpenAI account.
4
+
5
+ No Next.js app yet? Create one with the [Next.js installation guide](https://nextjs.org/docs/app/getting-started/installation), then come back.
6
+
7
+ ## 1. Get an OpenAI key
8
+
9
+ 1. In the [OpenAI dashboard](https://platform.openai.com/), create a **new project** just for this chat.
10
+ 2. Set a **monthly budget with a hard limit** for the project, so a busy day can never cost more than you planned.
11
+ 3. Create an **API key** in that project. Keep it out of chats, tickets and your shell history.
12
+
13
+ The [going-public checklist](going-public.md) covers restricted keys and model allowlists before you go live.
14
+
15
+ ## 2. Add askdare
16
+
17
+ In the folder of your app:
18
+
19
+ ```bash
20
+ npm install @boldare/askdare zod
21
+ npx @boldare/askdare init
22
+ ```
23
+
24
+ Use `pnpm add`, `yarn add` or `bun add` if that's what your app uses. `init` adds:
25
+
26
+ | File | What it is |
27
+ |---|---|
28
+ | `askdare.config.ts` | Your bot: its model, instructions, the sites that may embed it, and how the chat looks. |
29
+ | `app/api/chat/[...path]/route.ts` | The single route that serves the chat, its API and its assets. |
30
+ | `.env.local` | `OPENAI_API_KEY`, a generated `CHAT_SECRET` and `ASKDARE_MODEL`. It also makes sure `.gitignore` covers the file. |
31
+ | `app/layout.tsx` | One `<Script>` tag (`next/script`) that shows the chat button on every page. |
32
+ | `AGENTS.md` | A short note that points AI coding agents to the askdare docs in `node_modules`. |
33
+
34
+ It never overwrites a file, so it's safe to run again.
35
+
36
+ Already know your model? `npx @boldare/askdare init --model <model>` fills in `ASKDARE_MODEL` too. Every option is in [Command line](cli.md#init).
37
+
38
+ ## 3. Fill in `.env.local`
39
+
40
+ ```bash
41
+ OPENAI_API_KEY=sk-... # the key from step 1
42
+ ASKDARE_MODEL=... # a current model, see https://platform.openai.com/docs/models
43
+ ```
44
+
45
+ askdare never picks a model for you: models change every few months, and the choice decides your costs.
46
+
47
+ `ASKDARE_MODEL` must be set in every environment that builds the app, Vercel Preview included. Without it, `next build` fails with "model is required".
48
+
49
+ `init` already generated `CHAT_SECRET`. Each environment needs its own. To make a new one without `openssl`:
50
+
51
+ ```bash
52
+ node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"
53
+ ```
54
+
55
+ Is the project already on Vercel? Keep the variables in its settings, and copy them to your machine with `vercel env pull .env.local`. See [Deployment](deployment.md#vercel).
56
+
57
+ ## 4. Run it and check it
58
+
59
+ Start the app:
60
+
61
+ ```bash
62
+ npm run dev
63
+ ```
64
+
65
+ While it runs, check it from a second terminal:
66
+
67
+ ```bash
68
+ npx @boldare/askdare doctor
69
+ ```
70
+
71
+ Open http://localhost:3000. The chat button is in the bottom-right corner.
72
+
73
+ `doctor` checks your project and your environment. It then asks the running app to test its own setup: the OpenAI key, the model, the permissions the key needs, the knowledge base, and the headers that let other sites embed the chat. Every problem comes with what to do about it:
74
+
75
+ ```text
76
+ Bot "support"
77
+ ✓ Model <model> is available.
78
+ ✓ The key can create responses with <model>.
79
+ ✓ The key can create and delete conversations.
80
+ ✓ widget.js loads.
81
+ ✓ Sites that may embed the chat: 'self'
82
+ ```
83
+
84
+ Every check and option is in [Command line](cli.md#doctor).
85
+
86
+ ## 5. Put the chat on another website
87
+
88
+ The chat can live on any site, not only on your Next.js app:
89
+
90
+ 1. Add the site to `security.allowedOrigins` in `askdare.config.ts`, for example `['https://www.example.com']`.
91
+ 2. Paste one tag into that site's HTML:
92
+
93
+ ```html
94
+ <script src="https://your-app.example.com/api/chat/widget.js?bot=support" async></script>
95
+ ```
96
+
97
+ Browsers refuse to show the chat on sites that aren't on the list. Check a site with `npx @boldare/askdare doctor --origin https://www.example.com`.
98
+
99
+ Staging and production usually embed the chat on different sites. Read the list from an environment variable, such as `ASKDARE_ALLOWED_ORIGINS`. [Embedding](embedding.md#staging-and-production) shows how, along with the script's options and your site's Content Security Policy.
100
+
101
+ ## Next steps
102
+
103
+ | To… | Read |
104
+ |---|---|
105
+ | Look up every option of a bot | [Configuration](configuration.md) |
106
+ | Embed the chat on other sites, inline or behind your own button | [Embedding](embedding.md) |
107
+ | Look up a command or its options | [Command line](cli.md) |
108
+ | Fix a chat that doesn't work | [Troubleshooting](troubleshooting.md) |
109
+ | Answer from your documents, with citations | [Knowledge base](knowledge-base.md) |
110
+ | Let the bot call your own functions | [Tools](tools.md) |
111
+ | Know who is signed in | [Signed-in visitors](identity.md) |
112
+ | Accept images and PDFs | [Attachments](attachments.md) |
113
+ | Let visitors dictate | [Voice input](voice-input.md) |
114
+ | Change texts, language, colors or the button | [Styling](styling.md) and [Themes](themes.md) |
115
+ | Deploy to Vercel or Heroku | [Deployment](deployment.md) |
116
+ | Open the chat to the public | [Going public](going-public.md) |
@@ -0,0 +1,81 @@
1
+ # Going public: deployment checklist
2
+
3
+ Complete every step before the chat is reachable from the internet, demos included. The reasoning is in [THREAT_MODEL.md](https://github.com/boldare/askdare/blob/main/THREAT_MODEL.md).
4
+
5
+ ## 1. OpenAI project
6
+
7
+ 1. Create a **dedicated project** for this deployment in the OpenAI dashboard. Don't reuse a project that other apps use.
8
+ 2. Create a **restricted API key** with only the permissions askdare needs: Responses API (write), conversations (write) and List models (read); add Vector Stores (read) for a knowledge base, Files (write) for attachments and Model capabilities (request) for voice input. `npx @boldare/askdare doctor` tells you which one is missing.
9
+ 3. Set an **allowlist of models** to the one your bot uses.
10
+ 4. Set a **monthly budget with "Enforce a hard limit"** and email alerts at 50% and 80%. Send a test alert.
11
+
12
+ ## 2. Secrets and environment
13
+
14
+ ```bash
15
+ openssl rand -base64 32 # use the output as CHAT_SECRET
16
+ ```
17
+
18
+ | Variable | Required | Notes |
19
+ |---|---|---|
20
+ | `OPENAI_API_KEY` | yes | The restricted key from step 1 |
21
+ | `CHAT_SECRET` | yes | At least 32 random characters. Never commit it. |
22
+ | `CHAT_SECRET_PREVIOUS` | during rotation | Comma-separated former secrets |
23
+ | `IDENTITY_SECRET` | for signed-in visitors | Shared with your site's backend only; never in a page or in git |
24
+ | `ASKDARE_DISABLED` | no | Set `true` to switch every bot off. It takes effect when the app restarts with the new value: on Vercel, redeploy; on Heroku, changing a config var restarts the dynos. |
25
+
26
+ **To rotate the secret:**
27
+ 1. Move the current secret to `CHAT_SECRET_PREVIOUS` and set a new `CHAT_SECRET`.
28
+ 2. After 30 days, when the last resume tokens expire, remove the old secret.
29
+
30
+ ## 3. Bot configuration
31
+
32
+ - **`security.allowedOrigins`:** the exact sites allowed to embed the widget, such as `https://www.example.com`. No paths and no wildcards. The app's own origin is always allowed.
33
+ - **`limits`:** start with the defaults and tune `dailyTokens` (per bot) and `dailyTokensPerIp` to your budget. Add `limits: { globalDailyTokens }` to `createChatHandler` to cap all bots together. These budgets are the main cost control; the OpenAI hard limit is the backstop.
34
+ - **Instructions:** they must contain **no secrets**. Assume visitors can make the model reveal them.
35
+
36
+ ## 4. Rate limits across instances
37
+
38
+ The default counters live in memory and work per instance. If more than one instance serves traffic (Vercel, several Heroku dynos, autoscaling), use a shared store:
39
+
40
+ ```ts
41
+ import { upstashStore } from '@boldare/askdare-upstash';
42
+
43
+ export const { GET, POST, DELETE } = createChatHandler({
44
+ bots,
45
+ limits: { store: upstashStore() }, // UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN
46
+ });
47
+ ```
48
+
49
+ **Client IP:**
50
+ - The client IP comes from proxy headers. The default `trustProxy: 'auto'` detects Vercel and assumes one proxy otherwise, which matches Heroku.
51
+ - Behind additional proxies (a CDN in front of Heroku, for example), set `trustProxy` to the number of proxies.
52
+ - **Never expose `next start` directly to the internet.** Without a proxy that sets `X-Forwarded-For`, clients choose their own IP and per-IP limits stop working.
53
+
54
+ ## 5. Hosting
55
+
56
+ - **Vercel:** keep `export const maxDuration = 300` in the route file, and keep each bot's `limits.turnTimeoutMs` below it.
57
+ - **Heroku:**
58
+ - Use `web: next start --port $PORT --keepAliveTimeout 95000` in the `Procfile`.
59
+ - Streaming already sends a first byte immediately and heartbeats every 15 s, which satisfies Heroku's 30 s and 55 s router rules.
60
+ - **Behind nginx or another proxy:** don't buffer `text/event-stream` responses. askdare sends `X-Accel-Buffering: no`.
61
+
62
+ ## 6. Privacy and transparency
63
+
64
+ - **Retention:**
65
+ - Conversations are stored by OpenAI until they are deleted (ADR-0003).
66
+ - Decide on and document a retention policy.
67
+ - Clients with data-residency or zero-retention requirements need the planned own-storage add-on.
68
+ - **Disclosures:**
69
+ - Publish a privacy notice that names OpenAI (and Upstash, if used) as processors.
70
+ - Tell visitors they are talking to an AI. The chat says so under the message field by default; add `widget.disclosure.privacyUrl` to link your privacy notice ([Styling](styling.md#1-configuration)).
71
+
72
+ ## 7. Final check
73
+
74
+ - [ ] Restricted key, model allowlist and a hard spend limit, with a tested alert.
75
+ - [ ] `CHAT_SECRET` is random, at least 32 characters, and not in git.
76
+ - [ ] `allowedOrigins` lists only your sites.
77
+ - [ ] Shared store configured if you run more than one instance.
78
+ - [ ] Instructions contain no secrets.
79
+ - [ ] Privacy notice, retention decision and AI disclosure are in place.
80
+ - [ ] You know how to flip `ASKDARE_DISABLED=true` and redeploy or restart, and how long that takes on your host.
81
+ - [ ] `npx @boldare/askdare doctor --url <your app> --env-file <production env>` reports no problems ([Deployment](deployment.md)).
@@ -0,0 +1,109 @@
1
+ # Signed-in visitors
2
+
3
+ When your site knows who is signed in, the assistant can too: a tool can look up *this* user's orders, and the instructions can greet them by name. The model never decides who the visitor is. Your backend signs a short token, and askdare checks it. The design is in [ADR-0009](https://github.com/boldare/askdare/blob/main/docs/adr/0009-visitor-identity.md).
4
+
5
+ ## 1. Share a secret
6
+
7
+ Generate a secret and give it to both your site's backend and the askdare app:
8
+
9
+ ```bash
10
+ openssl rand -base64 32 # IDENTITY_SECRET; it must differ from CHAT_SECRET
11
+ ```
12
+
13
+ Without `IDENTITY_SECRET` in the askdare app, identity is off and every visitor is anonymous.
14
+
15
+ ## 2. Sign the user in on your backend
16
+
17
+ Sign an HS256 JWT for the signed-in user, every time you render a page for them. With [`jose`](https://github.com/panva/jose):
18
+
19
+ ```ts
20
+ import { SignJWT } from 'jose';
21
+
22
+ const identityToken = await new SignJWT({ name: user.name, email: user.email, plan: user.plan })
23
+ .setProtectedHeader({ alg: 'HS256' })
24
+ .setSubject(String(user.id))
25
+ .setAudience('support') // the bot ID
26
+ .setIssuedAt()
27
+ .setExpirationTime('1h')
28
+ .sign(new TextEncoder().encode(process.env.IDENTITY_SECRET));
29
+ ```
30
+
31
+ Or with nothing but Node.js:
32
+
33
+ ```js
34
+ import { createHmac } from 'node:crypto';
35
+
36
+ function identityToken(user, botId) {
37
+ const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
38
+ const now = Math.floor(Date.now() / 1000);
39
+ const claims = { sub: String(user.id), name: user.name, aud: botId, iat: now, exp: now + 3600 };
40
+ const unsigned = `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(claims)}`;
41
+ const signature = createHmac('sha256', process.env.IDENTITY_SECRET).update(unsigned).digest('base64url');
42
+ return `${unsigned}.${signature}`;
43
+ }
44
+ ```
45
+
46
+ Any language with a JWT library works the same way.
47
+
48
+ | Claim | Required | Notes |
49
+ | --- | --- | --- |
50
+ | `sub` | yes | Your user ID, up to 256 characters |
51
+ | `aud` | yes | The bot ID, so a token for one bot can't be used with another |
52
+ | `iat`, `exp` | yes | At most 24 hours apart; an hour or less is best |
53
+ | `name`, `email` | no | Shown to your tools and instructions |
54
+ | anything else | no | Strings, numbers and booleans reach tools as `claims`, such as a plan or a role. Up to 20, and 2 KB for the whole identity |
55
+
56
+ ## 3. Hand it to the chat
57
+
58
+ ```html
59
+ <script>
60
+ window.askdare = window.askdare || function () {
61
+ (window.askdare.q = window.askdare.q || []).push(arguments);
62
+ };
63
+ askdare('identify', '<the token your backend rendered into the page>');
64
+ </script>
65
+ <script src="https://app.example.com/api/chat/widget.js?bot=support" async></script>
66
+ ```
67
+
68
+ - On sign-out, call `askdare('identify', null)`. Anything that isn't a token, `undefined` included, signs out too. Single-page apps call `identify` whenever the user changes; repeating the same token costs nothing.
69
+ - The chat sends the token with every session request, about every 15 minutes, and the server checks it each time. The identity ends with the token's `exp`; after that the chat continues anonymously until the page calls `identify` with a fresh token. Long-lived pages should do that before `exp`.
70
+ - A token that fails the check leaves the chat anonymous. The browser console says why: `signature`, `expired`, `audience`, `lifetime`, `claims`, `too_large`, `malformed`, or `disabled` when the app has no `IDENTITY_SECRET`.
71
+
72
+ ## 4. Use it
73
+
74
+ ```ts
75
+ import { defineBot, defineTool } from '@boldare/askdare';
76
+ import { z } from 'zod';
77
+ import { orders } from './lib/orders'; // your own data access
78
+
79
+ // Declare tools before the bot that lists them.
80
+ export const myOrders = defineTool({
81
+ name: 'get_my_orders',
82
+ description: "The signed-in visitor's recent orders.",
83
+ input: z.object({}),
84
+ execute: async (_input, { identity }) => {
85
+ if (!identity) return { error: 'Ask the visitor to sign in first.' };
86
+ return { orders: await orders.forUser(identity.id) };
87
+ },
88
+ });
89
+
90
+ defineBot({
91
+ // …
92
+ instructions: ({ locale, identity }) =>
93
+ [
94
+ 'You are the assistant of our store.',
95
+ // The name is the user's own text: quoted, and one line (askdare removes line breaks).
96
+ identity ? `The visitor is signed in as "${identity.name ?? identity.id}".` : 'The visitor is not signed in.',
97
+ `Reply in ${locale}.`,
98
+ ].join(' '),
99
+ tools: [myOrders],
100
+ });
101
+ ```
102
+
103
+ `identity` is `{ id, name?, email?, claims }`, and only ever holds what your backend signed. `null` in `name` or `email` counts as missing.
104
+
105
+ ## What it protects
106
+
107
+ - **A conversation belongs to who started it.** A conversation started while signed in can't be resumed, read or deleted by anyone else on the same browser: another user, or the same browser after sign-out. The browser keeps each user's conversation apart, so signing back in brings it back. An anonymous conversation becomes the user's when they sign in during it.
108
+ - **Abuse is traced to the person.** OpenAI receives an HMAC of the user's ID as `safety_identifier`, so its abuse checks follow a person across devices.
109
+ - **What it doesn't do:** the token is valid until `exp` for whoever holds it, like any bearer token. Keep `exp` short, and keep `IDENTITY_SECRET` in your backend's secrets, never in a page or a repository.
@@ -0,0 +1,65 @@
1
+ # Knowledge base
2
+
3
+ A bot can answer from your own documents. askdare uploads them to an OpenAI vector store, the model searches it with OpenAI file search, and each answer cites its sources as links to your pages.
4
+
5
+ ## 1. Prepare a folder
6
+
7
+ Put the documents in one folder, for example `knowledge/`. Supported: `.md`, `.mdx`, `.txt`, `.pdf`, `.docx`, `.pptx`, `.html`, `.htm`, `.json`. Other files and hidden files are skipped.
8
+
9
+ Give each document a title and the public URL that citations should link to:
10
+
11
+ - Markdown: front matter.
12
+
13
+ ```md
14
+ ---
15
+ title: Pricing and plans
16
+ url: https://www.example.com/pricing
17
+ ---
18
+ ```
19
+
20
+ - Any file, for example a PDF: `askdare.kb.json` in the folder root, keyed by path inside the folder.
21
+
22
+ ```json
23
+ {
24
+ "guides/handbook.pdf": { "title": "Customer handbook", "url": "https://www.example.com/handbook" }
25
+ }
26
+ ```
27
+
28
+ Without a title the file name is used. Without a URL the citation is shown as plain text. URLs must be `http(s)`, up to 512 characters.
29
+
30
+ ## 2. Sync
31
+
32
+ ```bash
33
+ npx @boldare/askdare kb sync knowledge
34
+ ```
35
+
36
+ - Reads `OPENAI_API_KEY` from the environment, `.env.local` or `.env` (or `--env-file <path>`).
37
+ - The first run creates a vector store and prints its ID. Save it as `KB_VECTOR_STORE_ID`.
38
+ - Later runs pass the store through `KB_VECTOR_STORE_ID` or `--store vs_…` and change only what changed: new and edited files are uploaded, removed files are deleted, a new title or URL only updates the file's attributes. A second run without changes does nothing.
39
+ - `--dry-run` shows the plan without changing anything.
40
+
41
+ The store is the source of truth: every file carries its path, content hash, title and URL as attributes, so no state is kept locally and the command can run from CI. Files in the store that askdare did not upload are left alone.
42
+
43
+ Vector stores belong to an OpenAI project. Use one store per environment (development, staging, production), each with its own `KB_VECTOR_STORE_ID`.
44
+
45
+ ## 3. Connect the bot
46
+
47
+ ```ts
48
+ defineBot({
49
+ // …
50
+ knowledge: process.env.KB_VECTOR_STORE_ID
51
+ ? { vectorStoreId: process.env.KB_VECTOR_STORE_ID, maxResults: 5 }
52
+ : undefined,
53
+ });
54
+ ```
55
+
56
+ `maxResults` (1–50, default 5) is how many chunks the model reads per search. More grounds answers better but costs more input tokens; the turn's budget reservation grows with it.
57
+
58
+ Tell the model in `instructions` when to use the documents, for example "Answer questions about our products only from the documents you can search, and cite them."
59
+
60
+ ## Costs and limits
61
+
62
+ - File search is billed per call, and the retrieved chunks count as input tokens. Vector store storage is billed per GB per day above the free tier. See OpenAI pricing.
63
+ - The tool name `file_search` is reserved; your own tools cannot use it.
64
+
65
+ Try it in the demo site: `pnpm --dir site kb:sync` uploads the sample documents in `site/knowledge/`.