acrylblends 1.0.1 → 1.0.3

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.
Files changed (38) hide show
  1. package/README.md +19 -15
  2. package/bin/acrylblends.js +9 -1
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +136 -5
  5. package/dist/index.js.map +1 -1
  6. package/docs/framework/README.md +1 -0
  7. package/docs/framework/agents/index.md +18 -0
  8. package/docs/framework/cordis/migration-to-1-0.md +4 -3
  9. package/docs/framework/getting-started/desktop.md +20 -0
  10. package/docs/framework/guides/create-acrylblends.md +21 -0
  11. package/docs/framework/guides/data.md +12 -0
  12. package/docs/framework/guides/production.md +11 -1
  13. package/docs/framework/workshop/README.md +381 -0
  14. package/docs/framework/workshop/facilitator.md +107 -0
  15. package/lib/cordis/db-sqlite.js +10 -2
  16. package/lib/cordis/kv-json.js +10 -2
  17. package/lib/cordis/node-server.js +1 -1
  18. package/lib/cordis/router-http.js +1 -1
  19. package/lib/node-server.js +1 -1
  20. package/lib/{test-websocket-ZXHNHQSU.js → test-websocket-D8WWqE5G.js} +1 -2
  21. package/package.json +1 -1
  22. package/plugins/acryl-db-sqlite/plugin.json +1 -1
  23. package/plugins/acryl-db-sqlite/src/index.ts +8 -1
  24. package/plugins/acryl-kv-json/plugin.json +1 -1
  25. package/plugins/acryl-kv-json/src/index.ts +8 -1
  26. package/plugins/acryl-node-server/src/adapter/request.ts +3 -2
  27. package/plugins/agent-panel/headless.ts +24 -2
  28. package/plugins/desktop/launch.ts +48 -0
  29. package/plugins/desktop/plugin.json +1 -1
  30. package/plugins/desktop-web/launch.ts +48 -0
  31. package/plugins/desktop-web/plugin.json +1 -1
  32. package/templates/chat/AGENTS.md +1 -1
  33. package/templates/chat/src/access.ts +110 -0
  34. package/templates/chat/src/chat-app.ts +7 -1
  35. package/templates/chat/src/routes.ts +5 -1
  36. package/templates/chat/test/chat.test.ts +65 -0
  37. package/templates/notes/test/notes.test.ts +1 -1
  38. package/templates/todo/src/main.ts +2 -0
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url'
7
7
  import type {} from '../plugins/acryl-db/src/cordis.ts'
8
8
  import type { Db } from '../plugins/acryl-db/src/index.ts'
9
9
  import { pinnedInstance, withPort } from '../plugins/instance/index.ts'
10
+ import { chatAccess } from '../src/access.ts'
10
11
  import { createApp as boot } from '../src/app.ts'
11
12
  import { createApp } from '../src/chat-app.ts'
12
13
  import type { Llm } from '../src/llm.ts'
@@ -196,3 +197,67 @@ test('the chat page loads its client component, served by the running app', asyn
196
197
  assert.match(module.headers.get('content-type') ?? '', /javascript/)
197
198
  assert.equal((await running.root.router.request('/client/@acrylblends/jsx-dom.js')).status, 200)
198
199
  })
200
+
201
+ const production = (env: Record<string, string> = {}) =>
202
+ chatAccess({ NODE_ENV: 'production', ...env })
203
+
204
+ test('in development the chat is open and prints nothing', () => {
205
+ const { access, notices } = chatAccess({})
206
+ assert.equal(access.kind, 'open')
207
+ assert.deepEqual(notices, [])
208
+ })
209
+
210
+ test('in production the chat is closed until it has a token or is made public on purpose', async () => {
211
+ const closed = production()
212
+ assert.equal(closed.access.kind, 'closed')
213
+ assert.match(closed.notices.join(), /closed/)
214
+ const { llm, asked } = mock(['x'])
215
+ const app = createApp(await memoryDb(), llm, closed.access)
216
+ for (const path of ['/', '/api/conversations'])
217
+ assert.equal((await app.request(path)).status, 503, path)
218
+ assert.equal((await app.request('/api/chat', post({ message: 'hi' }))).status, 503)
219
+ assert.equal((await app.request('/health')).status, 200)
220
+ assert.equal(asked.length, 0)
221
+ })
222
+
223
+ test('with CHAT_ACCESS_TOKEN the browser must send it, as the password of a basic login', async () => {
224
+ const gated = production({ CHAT_ACCESS_TOKEN: 'a-long-shared-token' })
225
+ assert.equal(gated.access.kind, 'token')
226
+ assert.deepEqual(gated.notices, [])
227
+ const { llm, asked } = mock(['ok'])
228
+ const app = createApp(await memoryDb(), llm, gated.access)
229
+ const login = (password: string) => ({
230
+ ...post({ message: 'hi' }),
231
+ headers: {
232
+ 'content-type': 'application/json',
233
+ authorization: `Basic ${Buffer.from(`me:${password}`).toString('base64')}`,
234
+ },
235
+ })
236
+ assert.equal((await app.request('/api/chat', post({ message: 'hi' }))).status, 401)
237
+ assert.equal((await app.request('/api/chat', login('wrong'))).status, 401)
238
+ assert.equal((await app.request('/health')).status, 200)
239
+ assert.equal(asked.length, 0)
240
+ const ok = await app.request('/api/chat', login('a-long-shared-token'))
241
+ assert.equal(ok.status, 200)
242
+ assert.deepEqual(
243
+ events(await ok.text()).map(([e]) => e),
244
+ ['meta', 'token', 'done'],
245
+ )
246
+ })
247
+
248
+ test('ACRYL_PRODUCTION_KEEP=chat makes the chat public with a loud WARNING and a per-address rate limit', async () => {
249
+ const open = production({ ACRYL_PRODUCTION_KEEP: 'agent, chat', CHAT_RATE_PER_MINUTE: '2' })
250
+ assert.equal(open.access.kind, 'public')
251
+ assert.match(open.notices.join(), /^WARNING: the chat is public in production/)
252
+ const { llm, asked } = mock(['ok'])
253
+ const app = createApp(await memoryDb(), llm, open.access)
254
+ const send = () => app.request('/api/chat', post({ message: 'hi' }))
255
+ assert.equal((await (await send()).text()).includes('event: done'), true)
256
+ assert.equal((await send()).status, 200)
257
+ const limited = await send()
258
+ assert.equal(limited.status, 429)
259
+ assert.ok(Number(limited.headers.get('retry-after')) > 0)
260
+ assert.equal(asked.length, 2)
261
+ // Other routes are not counted.
262
+ assert.equal((await app.request('/api/conversations')).status, 200)
263
+ })
@@ -62,7 +62,7 @@ test('the home page sends a visitor to the notes, which send them to log in', as
62
62
  test('sign up validates, refuses a duplicate, and starts a session', async () => {
63
63
  const app = createApp(await memoryDb())
64
64
  assert.match(await (await app.request('/signup')).text(), /Create account/)
65
- const bad = await app.request('/signup', form({ email: 'nope', password: 'short' }))
65
+ const bad = await app.request('/signup', form({ email: 'nope', password: 'no way' }))
66
66
  assert.equal(bad.status, 400)
67
67
  const text = await bad.text()
68
68
  assert.match(text, /valid email/)
@@ -9,6 +9,8 @@ const chosen = selectInstance({
9
9
  })
10
10
  const app = await createApp(chosen)
11
11
  console.log(`${APP_NAME} listening on ${app.url} (data in ${chosen.home})`)
12
+ // The list is served under /todo, not at the address above: say where to open it.
13
+ console.log(`open ${app.url}/todo`)
12
14
 
13
15
  const stop = async () => {
14
16
  await app.stop()