@wabot-dev/framework 0.9.80 → 2.0.0-beta.0

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 (77) hide show
  1. package/bin/skills.mjs +151 -0
  2. package/bin/wabot-skills.mjs +120 -0
  3. package/dist/build/build.js +1031 -8
  4. package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
  5. package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
  6. package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
  7. package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
  8. package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
  9. package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
  10. package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
  11. package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
  12. package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
  13. package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
  14. package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
  15. package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
  16. package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
  17. package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
  18. package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
  19. package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
  20. package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
  21. package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
  22. package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
  23. package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
  24. package/dist/src/addon/ui/preact/outlet.js +22 -0
  25. package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
  26. package/dist/src/core/repository/CrudRepository.js +7 -7
  27. package/dist/src/feature/async/computeDedupKey.js +1 -1
  28. package/dist/src/feature/pg/@pgExtension.js +2 -4
  29. package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
  30. package/dist/src/feature/project-runner/scanner.js +1 -1
  31. package/dist/src/feature/repository/@memExtension.js +1 -2
  32. package/dist/src/feature/ui-controller/actions.js +35 -0
  33. package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
  34. package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
  35. package/dist/src/feature/ui-controller/bundler/index.js +4 -0
  36. package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
  37. package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
  38. package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
  39. package/dist/src/feature/ui-controller/document/escape.js +17 -0
  40. package/dist/src/feature/ui-controller/document/helpers.js +13 -0
  41. package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
  42. package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
  43. package/dist/src/feature/ui-controller/island/island.js +40 -0
  44. package/dist/src/feature/ui-controller/island/serialize.js +35 -0
  45. package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
  46. package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
  47. package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
  48. package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
  49. package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
  50. package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
  51. package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
  52. package/dist/src/index.d.ts +632 -3
  53. package/dist/src/index.js +30 -1
  54. package/dist/src/testing/index.d.ts +43 -1
  55. package/dist/src/testing/index.js +1 -0
  56. package/dist/src/testing/uiHarness.js +102 -0
  57. package/dist/src/ui/client.js +6 -0
  58. package/dist/src/ui/index.d.ts +427 -0
  59. package/dist/src/ui/index.js +29 -0
  60. package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
  61. package/dist/src/ui/jsx-dev-runtime.js +1 -0
  62. package/dist/src/ui/jsx-runtime.d.ts +1 -0
  63. package/dist/src/ui/jsx-runtime.js +1 -0
  64. package/package.json +33 -13
  65. package/skills/wabot-async/SKILL.md +143 -0
  66. package/skills/wabot-auth/SKILL.md +153 -0
  67. package/skills/wabot-chat/SKILL.md +140 -0
  68. package/skills/wabot-di-config/SKILL.md +117 -0
  69. package/skills/wabot-framework/SKILL.md +81 -0
  70. package/skills/wabot-framework/references/quickstart.md +85 -0
  71. package/skills/wabot-mindset/SKILL.md +159 -0
  72. package/skills/wabot-ops/SKILL.md +151 -0
  73. package/skills/wabot-persistence/SKILL.md +159 -0
  74. package/skills/wabot-rest-socket/SKILL.md +167 -0
  75. package/skills/wabot-testing/SKILL.md +214 -0
  76. package/skills/wabot-ui/SKILL.md +201 -0
  77. package/skills/wabot-validation/SKILL.md +108 -0
@@ -0,0 +1,29 @@
1
+ import { container } from '../core/injection/index.js';
2
+ export { uiController } from '../feature/ui-controller/metadata/@uiController.js';
3
+ export { view } from '../feature/ui-controller/metadata/@view.js';
4
+ export { action } from '../feature/ui-controller/metadata/@action.js';
5
+ export { uiMiddleware } from '../feature/ui-controller/metadata/@uiMiddleware.js';
6
+ export { UiControllerMetadataStore } from '../feature/ui-controller/metadata/UiControllerMetadataStore.js';
7
+ import { UiRendererRegistry } from '../feature/ui-controller/renderer/UiRendererRegistry.js';
8
+ export { ISLAND_MARKER, getIslandMeta, isIsland, island, setIslandId } from '../feature/ui-controller/island/island.js';
9
+ export { deserializeProps, serializeProps } from '../feature/ui-controller/island/serialize.js';
10
+ export { ISLAND_FILE_PATTERN, IslandRegistry, isIslandFile, toIslandId } from '../feature/ui-controller/island/IslandRegistry.js';
11
+ export { renderDocument } from '../feature/ui-controller/document/renderDocument.js';
12
+ export { REDIRECT_MARKER, isRedirect, redirect } from '../feature/ui-controller/document/helpers.js';
13
+ export { escapeAttr, escapeHtml } from '../feature/ui-controller/document/escape.js';
14
+ export { actionUrl, callAction } from '../feature/ui-controller/actions.js';
15
+ export { registerUiControllers, runUiControllers } from '../feature/ui-controller/runUiControllers.js';
16
+ import { PreactRenderer } from '../addon/ui/preact/PreactRenderer.js';
17
+ export { Outlet } from '../addon/ui/preact/outlet.js';
18
+ export { Fragment, cloneElement, createContext, h as createElement, createRef, h, isValidElement, toChildArray } from 'preact';
19
+ export * from 'preact/hooks';
20
+ export { Signal, batch, computed, createModel, effect, signal, action as signalAction, untracked, useComputed, useModel, useSignal, useSignalEffect } from '@preact/signals';
21
+
22
+ // UI entrypoint: import from '@wabot-dev/framework/ui'.
23
+ //
24
+ // Importing this module registers Preact + @preact/signals as the default UI
25
+ // renderer. Configure your tsconfig with:
26
+ // "jsx": "react-jsx", "jsxImportSource": "@wabot-dev/framework/ui"
27
+ container.resolve(UiRendererRegistry).setDefault(new PreactRenderer());
28
+
29
+ export { UiRendererRegistry };
@@ -0,0 +1 @@
1
+ export * from 'preact/jsx-dev-runtime';
@@ -0,0 +1 @@
1
+ export * from 'preact/jsx-dev-runtime';
@@ -0,0 +1 @@
1
+ export * from 'preact/jsx-runtime';
@@ -0,0 +1 @@
1
+ export * from 'preact/jsx-runtime';
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@wabot-dev/framework",
3
- "version": "0.9.80",
3
+ "version": "2.0.0-beta.0",
4
4
  "description": "Framework for IA Chat Bots",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
7
7
  "module": "dist/src/index.js",
8
8
  "types": "dist/src/index.d.ts",
9
+ "bin": {
10
+ "wabot-skills": "./bin/wabot-skills.mjs"
11
+ },
9
12
  "exports": {
10
13
  ".": {
11
14
  "types": "./dist/src/index.d.ts",
@@ -14,6 +17,19 @@
14
17
  "./testing": {
15
18
  "types": "./dist/src/testing/index.d.ts",
16
19
  "import": "./dist/src/testing/index.js"
20
+ },
21
+ "./ui": {
22
+ "types": "./dist/src/ui/index.d.ts",
23
+ "browser": "./dist/src/ui/client.js",
24
+ "import": "./dist/src/ui/index.js"
25
+ },
26
+ "./ui/jsx-runtime": {
27
+ "types": "./dist/src/ui/jsx-runtime.d.ts",
28
+ "import": "./dist/src/ui/jsx-runtime.js"
29
+ },
30
+ "./ui/jsx-dev-runtime": {
31
+ "types": "./dist/src/ui/jsx-dev-runtime.d.ts",
32
+ "import": "./dist/src/ui/jsx-dev-runtime.js"
17
33
  }
18
34
  },
19
35
  "license": "MIT",
@@ -44,27 +60,25 @@
44
60
  "async-jobs",
45
61
  "cron",
46
62
  "messaging-platforms",
63
+ "slack",
47
64
  "bot-framework",
48
65
  "conversational-ai"
49
66
  ],
50
67
  "files": [
51
68
  "dist/src",
52
69
  "dist/build",
70
+ "bin",
71
+ "skills",
53
72
  "LICENSE.md"
54
73
  ],
55
74
  "scripts": {
56
- "build": "rollup --config rollup.config.ts --configPlugin typescript && tsup --dts-only --format esm --out-dir dist/src src/index.ts && tsup --dts-only --format esm --out-dir dist/src/testing src/testing/index.ts",
75
+ "build": "rollup --config rollup.config.ts --configPlugin typescript && tsup --dts-only --format esm --out-dir dist/src src/index.ts && tsup --dts-only --format esm --out-dir dist/src/testing src/testing/index.ts && tsup --dts-only --format esm --out-dir dist/src/ui src/ui/index.ts && tsup --dts-only --format esm --out-dir dist/src/ui src/ui/jsx-runtime.ts && tsup --dts-only --format esm --out-dir dist/src/ui src/ui/jsx-dev-runtime.ts",
57
76
  "test:units": "node --import @yucacodes/ts --test './src/**/*.unit.test.ts'",
58
77
  "test:integration": "node --import @yucacodes/ts --import ./env.mjs --test './src/**/*.integration.test.ts'",
59
78
  "test:multiprocess": "node --import @yucacodes/ts --import ./env.mjs --test './src/**/*.multiprocess.test.ts'",
60
- "test:elia": "node --import @yucacodes/ts --test './test/elia/**/*.unit.test.ts'",
61
- "test:elia:eval": "node --import @yucacodes/ts --import ./env.mjs --test './test/elia/**/*.eval.test.ts'",
62
79
  "fmt": "prettier --write .",
63
80
  "fmt:check": "prettier --check .",
64
- "types:check": "tsc --noEmit",
65
- "elia:dev": "node --import @yucacodes/ts --import ./env.mjs ./test/elia/_run_.ts",
66
- "elia:watch": "node --watch --import @yucacodes/ts --import ./env.mjs ./test/elia/_run_.ts",
67
- "elia:cmd": "node --import @yucacodes/ts ./test/elia/_cmd_.ts"
81
+ "types:check": "tsc --noEmit"
68
82
  },
69
83
  "devDependencies": {
70
84
  "@rollup/plugin-alias": "5.1.1",
@@ -81,8 +95,10 @@
81
95
  },
82
96
  "peerDependencies": {
83
97
  "@anthropic-ai/sdk": "^0.60.0",
98
+ "@slack/bolt": "^3.17.0",
84
99
  "@google/genai": "^1.16.0",
85
100
  "@openrouter/sdk": "^0.12.2",
101
+ "@preact/signals": "^2.9.2",
86
102
  "@types/big.js": "^6.2.2",
87
103
  "@types/debug": "^4.1.12",
88
104
  "@types/express": "^5.0.1",
@@ -90,28 +106,32 @@
90
106
  "@types/jsonwebtoken": "^9.0.10",
91
107
  "@types/node": "22.14.1",
92
108
  "@types/pg": "^8.11.14",
93
- "@yucacodes/ts": "0.0.8",
109
+ "@yucacodes/ts": "^0.1.0",
94
110
  "big.js": "^7.0.1",
95
111
  "body-parser": "^2.2.0",
96
112
  "cron-parser": "^5.4.0",
97
113
  "debug": "^4.4.0",
98
114
  "dotenv": "^16.5.0",
115
+ "esbuild": "^0.25.12",
99
116
  "express": "^5.1.0",
100
117
  "grammy": "^1.36.0",
101
118
  "html-to-text": "^9.0.5",
102
119
  "jsonwebtoken": "^9.0.2",
103
120
  "openai": "^6.10.0",
104
121
  "pg": "^8.15.6",
122
+ "preact": "^10.29.3",
123
+ "preact-render-to-string": "^6.7.0",
124
+ "prettier": "^3.5.3",
105
125
  "reflect-metadata": "^0.2.2",
106
126
  "short-uuid": "^6.0.3",
107
127
  "socket.io": "^4.8.1",
108
128
  "socket.io-client": "^4.8.1",
109
129
  "tslib": "^2.8.1",
110
- "tsyringe": "^4.9.1",
111
- "uuid": "^14.0.0",
112
- "wasenderapi": "^0.1.5",
130
+ "@hubspot/api-client": "^13.5.0",
113
131
  "tsup": "^8.4.0",
132
+ "tsyringe": "^4.9.1",
114
133
  "typescript": "^6.0.3",
115
- "prettier": "^3.5.3"
134
+ "uuid": "^14.0.0",
135
+ "wasenderapi": "^0.1.5"
116
136
  }
117
137
  }
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: wabot-async
3
+ description: Use when running background work in Wabot — fire-and-forget commands, scheduled deferred commands, cron handlers, or wrapping side-effectful methods in a DB transaction. Covers @command, @commandHandler, @cronHandler, @transaction, the Async service (runCommand, scheduleCommand, IScheduleAt), ICommandHandler / ICronHandler, and how the in-memory vs PG job/cron stores are wired by the project runner.
4
+ ---
5
+
6
+ # Async commands, cron, and transactions
7
+
8
+ Wabot's async layer handles three kinds of work:
9
+
10
+ - **Commands** — typed messages with a registered handler. Run immediately (`runCommand`) or scheduled (`scheduleCommand`).
11
+ - **Crons** — handlers that fire on a cron expression.
12
+ - **Transactions** — wrap a method so all repository writes happen inside one DB transaction per registered adapter.
13
+
14
+ The job store, cron job store, and transaction adapter are auto-selected by the project runner based on `DATABASE_URL` (in-memory ↔ PostgreSQL). No manual registration.
15
+
16
+ ## Commands
17
+
18
+ A command is a plain class decorated with `@command(name)`. Fields use the standard validators from `wabot-validation`.
19
+
20
+ ```typescript
21
+ import { command, isNumber, isString, min } from '@wabot-dev/framework'
22
+
23
+ @command('orders.charge')
24
+ export class ChargeOrder {
25
+ @isString() orderId!: string
26
+ @isNumber() @min(1) amountCents!: number
27
+ }
28
+ ```
29
+
30
+ The handler implements `ICommandHandler<C>` and is bound with `@commandHandler(Command)`:
31
+
32
+ ```typescript
33
+ import { commandHandler, ICommandHandler, injectable } from '@wabot-dev/framework'
34
+ import { ChargeOrder } from './ChargeOrder'
35
+
36
+ @commandHandler(ChargeOrder)
37
+ export class ChargeOrderHandler implements ICommandHandler<ChargeOrder> {
38
+ constructor(private payments: PaymentService) {}
39
+
40
+ async handle(cmd: ChargeOrder) {
41
+ await this.payments.charge(cmd.orderId, cmd.amountCents)
42
+ }
43
+ }
44
+ ```
45
+
46
+ Trigger it with the `Async` singleton:
47
+
48
+ ```typescript
49
+ import { Async, container } from '@wabot-dev/framework'
50
+
51
+ const async = container.resolve(Async)
52
+
53
+ // Run as soon as a worker is free
54
+ await async.runCommand(ChargeOrder, { orderId: 'o_1', amountCents: 2500 })
55
+
56
+ // Schedule for later: absolute Date or relative delay
57
+ await async.scheduleCommand(ChargeOrder, { orderId: 'o_1', amountCents: 2500 }, { minutes: 30 })
58
+ await async.scheduleCommand(ChargeOrder, { orderId: 'o_1', amountCents: 2500 }, new Date('2026-12-31T08:00:00Z'))
59
+ ```
60
+
61
+ `IScheduleAt = Date | { seconds: number } | { minutes: number } | { hours: number } | { days: number }`.
62
+
63
+ `Async.runCommand` returns a `Job` immediately (it has an `id` you can persist). The payload is validated against the command class before the job is stored — bad shapes throw `CustomError`.
64
+
65
+ `@commandHandler` applies `@injectable()`. There is at most one handler per command name; registering a second throws.
66
+
67
+ ## Cron handlers
68
+
69
+ ```typescript
70
+ import { cronHandler, ICronHandler } from '@wabot-dev/framework'
71
+
72
+ @cronHandler({ name: 'daily-cleanup', cron: '0 3 * * *' })
73
+ export class DailyCleanup implements ICronHandler {
74
+ constructor(private games: GameRepository) {}
75
+
76
+ async handle() {
77
+ // ...
78
+ }
79
+
80
+ // Optional. Called when handle() throws.
81
+ async handleError(err: any) {
82
+ // ...
83
+ }
84
+ }
85
+ ```
86
+
87
+ - `cron` accepts standard 5-field or 6-field (with seconds) cron expressions.
88
+ - `disabled: true` turns a handler off without removing it (useful for guarded rollouts).
89
+ - `@cronHandler` applies `@singleton()` automatically — do not stack lifecycle decorators.
90
+
91
+ The runner registers these into `CronScheduler`, which persists per-cron state in `CronJobRepository` so missed runs aren't replayed on restart.
92
+
93
+ ## Transactions
94
+
95
+ `@transaction(dbNames?)` wraps an async method so every adapter listed runs the call inside a transaction. Without arguments, all registered adapters participate (today there is one: the runner registers `PgTransactionAdapter` under the name `'default'` when `DATABASE_URL` is Postgres).
96
+
97
+ ```typescript
98
+ import { transaction } from '@wabot-dev/framework'
99
+
100
+ @injectable()
101
+ export class Checkout {
102
+ @transaction()
103
+ async checkout(orderId: string) {
104
+ await this.orderRepository.update(...)
105
+ await this.paymentRepository.create(...)
106
+ }
107
+ }
108
+ ```
109
+
110
+ Behavior:
111
+ - Repository writes performed inside the method go to the same client/connection.
112
+ - Throwing rolls back; returning commits.
113
+ - Nested `@transaction`-decorated calls reuse the outer transaction (no nested begin).
114
+ - Without Postgres no adapter is registered, so `@transaction()` just executes the method (no wrap). Naming a specific adapter (e.g. `@transaction(['default'])`) throws at call time if it isn't registered — never write `@transaction(['pg'])`.
115
+
116
+ ## Manual run / stop
117
+
118
+ In production the runner discovers and starts everything. For testing or custom hosts:
119
+
120
+ ```typescript
121
+ import { runCommandHandlers, runCronHandlers, stopCommandHandlers, stopCronHandlers } from '@wabot-dev/framework'
122
+
123
+ runCommandHandlers([ChargeOrderHandler])
124
+ runCronHandlers([DailyCleanup])
125
+
126
+ // later
127
+ stopCommandHandlers([ChargeOrderHandler])
128
+ stopCronHandlers([DailyCleanup])
129
+ ```
130
+
131
+ `testAsync` is exported for integration tests against the real job/cron schedulers — see `wabot-ts/src/feature/async/testAsync.ts` for an example.
132
+
133
+ ## Rules
134
+
135
+ - Command classes are *data* classes. Don't put behavior on them — that belongs in the handler.
136
+ - Don't reuse a command name for two handlers. The framework throws on boot.
137
+ - Don't `await async.runCommand(...)` inside a request handler expecting it to finish synchronously — `runCommand` schedules the job and returns immediately. Use `async.scheduleCommand(..., { seconds: 0 })` if you want the same fire-and-forget behavior explicitly.
138
+ - Cron expressions in seconds (6 fields) are supported but be careful: the in-memory adapter polls fast, the PG adapter polls at a lower rate.
139
+ - Wrap multi-repository writes in `@transaction()` rather than chaining manual `withPgTransaction` calls.
140
+
141
+ ## Testing
142
+
143
+ `createAsyncHarness({ register?, authInfo? })` from `@wabot-dev/framework/testing` runs `@command` handlers (`harness.execute(Command, data)` — real validation, returns the transformed command) and `@cronHandler` jobs (`harness.runCron(Handler)`) inline, with no PostgreSQL or polling workers. `isValidCronSequence` and `waitUntil` help with schedule assertions. See the `wabot-testing` skill.
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: wabot-auth
3
+ description: Use when adding authentication to a Wabot REST endpoint or Socket.IO namespace, issuing JWT access/refresh tokens, or protecting endpoints with API keys. Covers the chat/request-scoped Auth<D> service, @jwtGuard / @jwtHandshakeGuard, JwtConfig and the JWT_* env vars, Jwt.createToken / findRefreshTokenAuthInfo, JwtRefreshToken, and @apiKeyGuard / @apiKeyHandshakeGuard with PgApiKeyRepository / RemoteApiKeyRepository.
4
+ ---
5
+
6
+ # Authentication
7
+
8
+ Wabot's auth is request-scoped. A guard middleware verifies the credential, then writes the resolved auth info into the per-request/socket `Auth<D>` instance. Downstream code reads it with `auth.require()`.
9
+
10
+ ## `Auth<D>`
11
+
12
+ ```typescript
13
+ import { Auth, injectable } from '@wabot-dev/framework'
14
+
15
+ interface SessionInfo {
16
+ userId: string
17
+ role: 'admin' | 'user'
18
+ }
19
+
20
+ @injectable()
21
+ export class OrdersService {
22
+ constructor(private auth: Auth<SessionInfo>) {}
23
+
24
+ list() {
25
+ const session = this.auth.require() // throws CustomError({ httpCode: 401 }) if not assigned
26
+ return this.repo.findByUserId(session.userId)
27
+ }
28
+ }
29
+ ```
30
+
31
+ `Auth` is `@scoped(Lifecycle.ContainerScoped)` and exists once per request, chat, or socket connection. Methods:
32
+
33
+ - `assign(info)` — sets the auth info (used by guards). Throws if already assigned.
34
+ - `override(info)` — replaces even when assigned (use cautiously).
35
+ - `require()` — returns the info or throws 401.
36
+ - `isAssigned()` / `wasOverrided()` / `clear()`.
37
+
38
+ ## JWT — REST
39
+
40
+ ```typescript
41
+ import { jwtGuard, onGet, onPost, restController } from '@wabot-dev/framework'
42
+
43
+ @restController('/account')
44
+ export class AccountController {
45
+ @onGet('/me')
46
+ @jwtGuard()
47
+ async me(_, auth: Auth<SessionInfo>) {
48
+ return auth.require()
49
+ }
50
+ }
51
+ ```
52
+
53
+ `@jwtGuard()` reads `Authorization: Bearer <token>`, verifies it with `JwtConfig` (env-driven, see below), and calls `auth.assign(payload)`.
54
+
55
+ ## JWT — Socket
56
+
57
+ ```typescript
58
+ import { jwtHandshakeGuard, socketController } from '@wabot-dev/framework'
59
+
60
+ @socketController('private')
61
+ @jwtHandshakeGuard()
62
+ export class PrivateSocketController { /* ... */ }
63
+ ```
64
+
65
+ The token is read from `socket.handshake.auth.token` first, falling back to the `Authorization` header.
66
+
67
+ ## Issuing tokens
68
+
69
+ ```typescript
70
+ import { Auth, Jwt, JwtAccessAndRefreshTokenDto, injectable, onPost, restController } from '@wabot-dev/framework'
71
+
72
+ @restController('/auth')
73
+ export class AuthController {
74
+ constructor(private jwt: Jwt, private auth: Auth<SessionInfo>) {}
75
+
76
+ @onPost('/login')
77
+ async login(body: LoginRequest): Promise<JwtAccessAndRefreshTokenDto> {
78
+ const session = await this.userService.verify(body)
79
+ this.auth.assign(session)
80
+ return this.jwt.createToken({ device: body.device ?? 'unknown' })
81
+ }
82
+
83
+ @onPost('/refresh')
84
+ async refresh(body: RefreshRequest): Promise<JwtAccessAndRefreshTokenDto> {
85
+ const session = await this.jwt.findRefreshTokenAuthInfo(body.refreshToken)
86
+ this.auth.override(session)
87
+ return this.jwt.createToken()
88
+ }
89
+ }
90
+ ```
91
+
92
+ `Jwt.createToken(metadata?)`:
93
+ - Reads the current `auth.require()` info.
94
+ - Persists a `JwtRefreshToken` via `JwtRefreshTokenRepository`. The base class throws `Method not implemented` — the project must register an implementation: `container.registerType(JwtRefreshTokenRepository, PgJwtRefreshTokenRepository)` (table `wabot.jwt_refresh_token`), or its own.
95
+ - Returns `{ access: { token, expiration }, refresh: { token, expiration } }`.
96
+
97
+ `Jwt.findRefreshTokenAuthInfo(secret)` validates the refresh secret (hash-compare + expiration + revocation) and returns the original auth payload. Throws 401 if any check fails.
98
+
99
+ ### Env vars
100
+
101
+ | Variable | Default | Description |
102
+ | --- | --- | --- |
103
+ | `JWT_SECRET` | (required) | Symmetric secret or asymmetric key used for sign/verify |
104
+ | `JWT_ALGORITHM` | `HS256` | Any `jsonwebtoken.Algorithm` |
105
+ | `JWT_ACCESS_EXPIRATION_SECONDS` | `600` (10 min) | Access token lifetime |
106
+ | `JWT_REFRESH_EXPIRATION_SECONDS` | `31536000` (1 yr) | Refresh token lifetime |
107
+
108
+ `JwtConfig` is `@singleton()` and reads these at boot.
109
+
110
+ ### Refresh-token metadata
111
+
112
+ `JwtRefreshToken<D>` exposes `metadata` (the arbitrary `Record<string, string>` you passed to `createToken`), `expirationTime`, `revoke()`, and `isValidToken(secret)`. The repository exposes `findByMetadata(metadata)` so you can revoke all tokens issued to a device.
113
+
114
+ ## API key
115
+
116
+ ```typescript
117
+ import { apiKeyGuard, apiKeyHandshakeGuard, onGet, restController, socketController } from '@wabot-dev/framework'
118
+
119
+ @restController('/internal')
120
+ export class InternalController {
121
+ @onGet('/health')
122
+ @apiKeyGuard()
123
+ async health() { return { ok: true } }
124
+ }
125
+
126
+ @socketController('admin')
127
+ @apiKeyHandshakeGuard()
128
+ export class AdminSocketController { /* ... */ }
129
+ ```
130
+
131
+ The guard reads `Authorization: Api-Key <secret>` (REST) or `socket.handshake.auth.token` / the `Authorization` handshake header (socket), validates via `ApiKeyRepository`, and assigns the lookup result to `Auth`.
132
+
133
+ `ApiKeyRepository` is a base class with no storage — register an implementation yourself: `container.registerType(ApiKeyRepository, PgApiKeyRepository)` (table `wabot.api_key`), or `RemoteApiKeyRepository` to delegate validation to an external service:
134
+
135
+ ```typescript
136
+ import { container, ApiKeyRepository, RemoteApiKeyRepository } from '@wabot-dev/framework'
137
+
138
+ container.registerType(ApiKeyRepository, RemoteApiKeyRepository)
139
+ ```
140
+
141
+ Set `WABOT_API_KEY` and `WABOT_LLM_URL` to point `RemoteApiKeyRepository` at your auth service (it expects the same shape as the Wabot platform).
142
+
143
+ ## Rules
144
+
145
+ - Always type `Auth<D>` with your session shape. `auth.require()` returns the typed value.
146
+ - A guard runs once per request/connection. Don't reassign manually — use `override(...)` only on token refresh.
147
+ - `@jwtGuard()` and `@apiKeyGuard()` work on REST endpoints; their `*HandshakeGuard()` variants work on socket controllers. Don't mix them up.
148
+ - Never log `auth.require()` results — they may contain PII or token claims. Use a redacted view.
149
+ - If you need a custom guard, implement `IMiddleware` (REST) or `IHandshakeMiddleware` (socket) and wire it with `@middleware(...)` / `@handshakeMiddlewares([...])`.
150
+
151
+ ## Testing
152
+
153
+ From `@wabot-dev/framework/testing`: `RestHarness` with `jwt: true` registers a test `JwtConfig` so the real `@jwtGuard` works without env secrets (`harness.as(authInfo)` signs per-request tokens; `harness.jwt.signInvalid()` for 401 tests). `TestApiKeyRepository` is an in-RAM `ApiKeyRepository` for `@apiKeyGuard` (`register: [[ApiKeyRepository, repo]]`, `repo.addKey(authInfo)`). Harnesses also accept `authInfo` to populate the container-scoped `Auth` directly. See the `wabot-testing` skill.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: wabot-chat
3
+ description: Use when wiring chat input/output for a Wabot bot — receiving messages from one or more channels (cmd, socket, telegram, whatsapp, wasender), injecting a ChatBot bound to a mindset, and replying. Covers @chatController, @chatBot, @cmd / @socket / @telegram / @whatsApp / @wasender, IReceivedMessage, the ChatBot.sendMessage callback, and the @chatAdapter / runChatAdapters provider model.
4
+ ---
5
+
6
+ # Chat controllers, channels, and adapters
7
+
8
+ A chat controller is the entry point for messages from a transport (terminal, socket, Telegram, WhatsApp). It owns a `ChatBot` bound to one mindset and forwards the message into the bot's processing loop.
9
+
10
+ ## Controller shape
11
+
12
+ ```typescript
13
+ import {
14
+ chatBot,
15
+ ChatBot,
16
+ chatController,
17
+ cmd,
18
+ socket,
19
+ telegram,
20
+ whatsApp,
21
+ type IReceivedMessage,
22
+ } from '@wabot-dev/framework'
23
+ import { PixelMindset } from './mindset/PixelMindset'
24
+
25
+ @chatController()
26
+ export class PixelChatController {
27
+ constructor(@chatBot(PixelMindset) private pixel: ChatBot) {}
28
+
29
+ @cmd()
30
+ @socket({ namespace: 'pixel' })
31
+ @telegram({ botToken: process.env.TELEGRAM_BOT_TOKEN! })
32
+ @whatsApp({ number: '+1555…', accessToken: '…', businessNumberId: '…' })
33
+ async onMessage(ctx: IReceivedMessage) {
34
+ await this.pixel.sendMessage(ctx.message, async (reply) => {
35
+ await ctx.reply(reply)
36
+ })
37
+ }
38
+ }
39
+ ```
40
+
41
+ Channel decorators stack: each one registers the same method on a different transport. The runner instantiates a channel per registration.
42
+
43
+ `@chatController()` takes no arguments today — `IchatControllerConfig` is currently empty. It applies `@injectable()`.
44
+
45
+ ## `ChatBot` injection
46
+
47
+ `@chatBot(MindsetCtor)` injects a `ChatBot` instance scoped to that mindset. You can have several in one controller (e.g. a "support" bot and a "sales" bot) — each gets a unique injection token under the hood.
48
+
49
+ `ChatBot.sendMessage(message, callback)`:
50
+ - Persists the incoming message as a `humanMessage` chat item.
51
+ - Calls the mindset → adapter loop, executing any tool calls.
52
+ - Invokes `callback(replyMessage)` once per `botMessage` item produced.
53
+
54
+ Inside the loop, the bot picks `models.visionLlm` if any image is attached, otherwise `models.llm`. It will only use providers registered through `@chatAdapter` (see below).
55
+
56
+ ## `IReceivedMessage` and `IChatMessage`
57
+
58
+ ```typescript
59
+ interface IReceivedMessage {
60
+ message: IChatMessage
61
+ reply: (message: IChatMessage) => Promise<Record<string, string> | void>
62
+ }
63
+
64
+ interface IChatMessage {
65
+ senderId?: string
66
+ senderName?: string
67
+ text?: string
68
+ images?: IChatMessageImage[]
69
+ documents?: IChatMessageDocument[]
70
+ object?: object
71
+ metadata?: Record<string, string>
72
+ }
73
+ ```
74
+
75
+ The `reply` callback comes from the channel — for `@cmd` it writes to stdout of the connected `cmd:channel` client; for `@socket` it emits a Socket.IO event; for `@telegram`/`@whatsApp`/`@wasender` it calls the provider's send API. You always pass an `IChatMessage` (with `text`, `images`, or `documents`); never raw strings.
76
+
77
+ ## Channels — what each decorator needs
78
+
79
+ | Decorator | Config | Notes |
80
+ | --- | --- | --- |
81
+ | `@cmd()` | none | Local Unix socket terminal client. Run `npm run cmd` to chat. State stored under `.wabot/cmd-channel/<route>/`. |
82
+ | `@socket({ namespace })` | `namespace: string \| ConfigReference<string>` | Mounts a Socket.IO namespace. Tag-fn refs (`str\`socket.namespace\``) supported. |
83
+ | `@telegram({ botToken })` | `botToken: string \| ConfigReference<string>` | Long-poll Telegram bot. Raw token or `str\`telegram.bot_token\``. |
84
+ | `@whatsApp(numberOrConfig)` | string OR `{ number, accessToken?, businessNumberId? }` (each may be a `ConfigReference`) | WhatsApp Cloud API. |
85
+ | `@wasender(config?)` | `{ apiKey?, webhookSecret?, phoneNumber?, webhookPath? }` (each may be a `ConfigReference`) | WaSender integration. |
86
+
87
+ Channel configs that accept `ConfigReference` resolve env vars automatically — use `str\`wasender.apiKey\`` from `@wabot-dev/framework` to point to `WASENDER_APIKEY`.
88
+
89
+ ## Chat resolution
90
+
91
+ When a message arrives, `ChatResolver` looks up the `Chat` by `IChatConnection` (`{ chatType: 'PRIVATE' | 'GROUP', channelName, id }`). If none exists, a new `Chat` is created under a lock. You never write this resolution code — it's already wired.
92
+
93
+ Inside a chat handler, these tokens are available in the child container:
94
+ - `Chat` — the resolved chat entity
95
+ - `ChatMemory` — append/read chat items for this chat
96
+ - `ChatOperator` — convenience wrapper (see `wabot-mindset`)
97
+ - `Auth` — populated if the channel attached auth (e.g. via `injectInstances`)
98
+
99
+ ## Chat adapters (providers)
100
+
101
+ Each LLM provider has a `@chatAdapter({ provider: 'name' })` class implementing `IChatAdapter.nextItems`. The framework ships adapters for `openai`, `anthropic`, `google`, `openrouter`, `deepseek`, and `wabot`.
102
+
103
+ The project runner registers adapters automatically based on env keys (`OPENAI_API_KEY` → OpenAI, etc.). To override, pass `chatAdapters: [...]` to `run({ chatAdapters: [MyAdapter, OpenaiChatAdapter] })`.
104
+
105
+ To call them manually (outside the runner):
106
+
107
+ ```typescript
108
+ import { runChatAdapters, OpenaiChatAdapter } from '@wabot-dev/framework'
109
+ runChatAdapters([OpenaiChatAdapter])
110
+ ```
111
+
112
+ `runChatAdapters` binds a `UnionChatAdapter` to the `ChatAdapter` token — it dispatches by `IMindsetModelRef.provider`.
113
+
114
+ To build your own adapter, implement `IChatAdapter`:
115
+
116
+ ```typescript
117
+ import { chatAdapter, IChatAdapter, IChatAdapterNextItemsReq, IChatAdapterNextItemsRes, singleton } from '@wabot-dev/framework'
118
+
119
+ @chatAdapter({ provider: 'my-llm' })
120
+ @singleton()
121
+ export class MyLlmAdapter implements IChatAdapter {
122
+ async nextItems(req: IChatAdapterNextItemsReq): Promise<IChatAdapterNextItemsRes> {
123
+ // map req.systemPrompt + req.prevItems + req.tools to your API, return { nextItems, usage }
124
+ return { nextItems: [], usage: { inputTokens: 0, outputTokens: 0, provider: 'my-llm', model: req.models[0].model } }
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## Rules
130
+
131
+ - Keep controllers thin — fetch from `ctx.message`, call `bot.sendMessage`, return. Business logic belongs in modules/services.
132
+ - Don't pass raw strings to `ctx.reply` — wrap them in `{ text: '...' }`.
133
+ - The reply callback is async; await it so the channel actually flushes.
134
+ - `@chatBot(Mindset)` is a parameter decorator — it can only appear in a constructor.
135
+ - If you add a new chat adapter, its file must be imported during the runner's scan (so the `@chatAdapter` side-effect runs); place it inside `directories`.
136
+ - Avoid building a mindset/controller pair when one controller can route between several mindsets with multiple `@chatBot(...)` constructor params.
137
+
138
+ ## Testing
139
+
140
+ `createChatControllerHarness({ controller })` from `@wabot-dev/framework/testing` drives a `@chatController` end-to-end without a real channel (same per-message container path as production, `@chatBot` included), with a scripted `MockChatAdapter` playing the LLM. Custom `IChatAdapter` implementations should pass `chatAdapterConformanceCases`. See the `wabot-testing` skill.