everyapp 0.2.2 → 0.3.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.
Files changed (120) hide show
  1. package/README.md +6 -77
  2. package/bin/everyapp.js +235 -0
  3. package/dist/access-F3GKBKAW.js +1 -0
  4. package/dist/activity-4MFMH5JP.js +12 -0
  5. package/dist/apps-63K2DGVT.js +9 -0
  6. package/dist/chunk-2UFJRKEE.js +4 -0
  7. package/dist/chunk-3AAHHUJC.js +11 -0
  8. package/dist/chunk-3Z53CH4F.js +6 -0
  9. package/dist/chunk-5ZTFC6DK.js +1 -0
  10. package/dist/chunk-6O3GJS5H.js +15 -0
  11. package/dist/chunk-74EST4VW.js +2 -0
  12. package/dist/chunk-7LG3F4FF.js +10 -0
  13. package/dist/chunk-AUDGOIHE.js +1 -0
  14. package/dist/chunk-BHMRY53V.js +2 -0
  15. package/dist/chunk-DQWOXSL3.js +32 -0
  16. package/dist/chunk-E3EPK27P.js +10 -0
  17. package/dist/chunk-E7EUXUVD.js +1 -0
  18. package/dist/chunk-ECLJ5IAL.js +126 -0
  19. package/dist/chunk-EQX4GKJ2.js +4 -0
  20. package/dist/chunk-EUNGJMJ6.js +20 -0
  21. package/dist/chunk-F6IH47GT.js +4 -0
  22. package/dist/chunk-G64S77GC.js +33 -0
  23. package/dist/chunk-GD2JZMD3.js +1 -0
  24. package/dist/chunk-GRFPP4UW.js +7 -0
  25. package/dist/chunk-IA534DGW.js +10 -0
  26. package/dist/chunk-IG26HYRR.js +45 -0
  27. package/dist/chunk-IZEOAOKL.js +6 -0
  28. package/dist/chunk-JYKRUS5C.js +1 -0
  29. package/dist/chunk-KMAOZRV4.js +18 -0
  30. package/dist/chunk-MC56CUQI.js +7 -0
  31. package/dist/chunk-MDORVBPD.js +1 -0
  32. package/dist/chunk-N4E5YE4X.js +5 -0
  33. package/dist/chunk-NJFWEYX7.js +1 -0
  34. package/dist/chunk-OEOVNJAN.js +1 -0
  35. package/dist/chunk-OVKVV6MK.js +2 -0
  36. package/dist/chunk-PL6EORDJ.js +15 -0
  37. package/dist/chunk-PMPIMHKG.js +6 -0
  38. package/dist/chunk-QZWSSWMP.js +22 -0
  39. package/dist/chunk-R4CGF622.js +2 -0
  40. package/dist/chunk-RFVMR32Y.js +1 -0
  41. package/dist/chunk-RQFHVU3Y.js +25 -0
  42. package/dist/chunk-SAT3PGZU.js +57 -0
  43. package/dist/chunk-SPPR6WCI.js +1 -0
  44. package/dist/chunk-TPWSGRNT.js +30 -0
  45. package/dist/chunk-VVQRET35.js +2 -0
  46. package/dist/chunk-VXJQGJ7Y.js +2 -0
  47. package/dist/chunk-WEM3WLOC.js +1 -0
  48. package/dist/chunk-Y3VIFPN5.js +1 -0
  49. package/dist/chunk-YJBYZWSP.js +22 -0
  50. package/dist/chunk-ZYL2ZHCF.js +98 -0
  51. package/dist/configure-2KMHM22T.js +8 -0
  52. package/dist/create-ELEJMZBS.js +22 -0
  53. package/dist/db-MZIAKGBN.js +9 -0
  54. package/dist/delete-H47KPJSB.js +3 -0
  55. package/dist/deploy-HGFJLLUO.js +1 -0
  56. package/dist/dev-62VA7UEM.js +86 -0
  57. package/dist/email-NNFD3VVB.js +10 -0
  58. package/dist/events-JDY76EG6.js +15 -0
  59. package/dist/groups-NQFXDZZP.js +8 -0
  60. package/dist/index.js +92 -1
  61. package/dist/install-JTZWRZIL.js +1 -0
  62. package/dist/login-P6LYQQWP.js +1 -0
  63. package/dist/mcp-NCFWEBMD.js +1 -0
  64. package/dist/provider-WVBI2VED.js +13 -0
  65. package/dist/release-6FYSPXXA.js +1 -0
  66. package/dist/restore-XBU7DOJV.js +3 -0
  67. package/dist/secret-ML5EXS3O.js +1 -0
  68. package/dist/setup-IEXCISLP.js +1 -0
  69. package/dist/status-OPLPKJ4U.js +4 -0
  70. package/dist/teardown-3JMMLK6O.js +22 -0
  71. package/dist/update-37ZDATLR.js +1 -0
  72. package/dist/workspace-DAFZVYQ2.js +21 -0
  73. package/package.json +30 -27
  74. package/skills/build-every-app/SKILL.md +199 -0
  75. package/skills/build-every-app/patterns.md +134 -0
  76. package/skills/everyapp/SKILL.md +292 -0
  77. package/skills/everyapp/mcp.md +336 -0
  78. package/skills/everyapp/security-review.md +61 -0
  79. package/skills/everyapp/setup/README.md +263 -0
  80. package/skills/everyapp/setup/access.md +178 -0
  81. package/skills/everyapp/setup/context.md +117 -0
  82. package/skills/everyapp/setup/dataforseo.md +287 -0
  83. package/skills/everyapp/setup/email.md +366 -0
  84. package/skills/everyapp/setup/gateway.md +243 -0
  85. package/skills/everyapp/setup/google.md +360 -0
  86. package/skills/everyapp/setup/microsoft-365.md +281 -0
  87. package/skills/everyapp/setup/openai.md +318 -0
  88. package/skills/everyapp/setup/openrouter.md +309 -0
  89. package/skills/everyapp/setup/providers.md +353 -0
  90. package/skills/everyapp/setup/remote-mcp.md +199 -0
  91. package/skills/everyapp/setup/slack.md +344 -0
  92. package/skills/everyapp/setup/workers-ai.md +138 -0
  93. package/bin/__everyapp_bash_complete.js +0 -12
  94. package/dist/bash-complete.js +0 -3
  95. package/dist/chunk-7BOQJFI5.js +0 -8
  96. package/dist/chunk-7YVJFS4A.js +0 -1
  97. package/dist/chunk-BIXKPCDH.js +0 -2
  98. package/dist/chunk-DLGPLHBH.js +0 -47
  99. package/dist/chunk-FD6K2YA7.js +0 -9
  100. package/dist/chunk-GAJEQYJH.js +0 -39
  101. package/dist/chunk-O2KDOP5R.js +0 -13
  102. package/dist/chunk-OI2MBGBG.js +0 -4
  103. package/dist/chunk-POSDLD3G.js +0 -8
  104. package/dist/chunk-RB5D2R45.js +0 -1
  105. package/dist/chunk-U4LIQ5VR.js +0 -1
  106. package/dist/chunk-VEYSNPDH.js +0 -3
  107. package/dist/chunk-VP6SKW44.js +0 -7
  108. package/dist/chunk-WO3NE7ZU.js +0 -7
  109. package/dist/chunk-XTO2VTPY.js +0 -6
  110. package/dist/chunk-YJ2DKIZS.js +0 -1
  111. package/dist/create-DYQ3NBWJ.js +0 -22
  112. package/dist/deploy-PYTCMDGV.js +0 -7
  113. package/dist/deploy-RMAHOA4Z.js +0 -55
  114. package/dist/dev-SAE237FS.js +0 -0
  115. package/dist/generateConfig-46Z3ERF3.js +0 -1
  116. package/dist/login-B6EF3SMX.js +0 -1
  117. package/dist/logout-XMKZBLNV.js +0 -1
  118. package/dist/remoteD1Shell-A67JMO3H.js +0 -6
  119. package/dist/setupLocal-TZJUW5ES.js +0 -6
  120. package/gateway/every-app-gateway-build.tar.gz +0 -0
@@ -0,0 +1,344 @@
1
+ # Slack for the Gateway
2
+
3
+ One Slack app, registered once at api.slack.com, whose OAuth client the
4
+ Gateway holds. Open CRM uses it: an owner or admin connects their workspace
5
+ from **Settings → Connections**, and the CRM's team agents post to approved
6
+ channels, manage those channels and match members to Slack users by email.
7
+ At the end the person has a Slack app in their workspace, the Gateway holds
8
+ its Client ID and Client Secret, and **Connect Slack** in Open CRM completes.
9
+
10
+ ## Settings
11
+
12
+ | Name | Required | What it holds |
13
+ | --------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
14
+ | `SLACK_CLIENT_ID` | required | The app's **Client ID** (Basic Information → App Credentials), as shown. Public; apps get it for the authorize URL. |
15
+ | `SLACK_CLIENT_SECRET` | required | The app's **Client Secret** from the same section, as shown. The Gateway puts it on `oauth.v2.access` only. |
16
+
17
+ Set both: the Gateway offers Slack only when it holds the two.
18
+
19
+ ## Costs and plans
20
+
21
+ As of October 2026: creating a Slack app is free, no card. It needs a Slack
22
+ workspace the person may add apps to; Slack's free plan allows 10 app
23
+ installs per workspace, custom apps included. `apps.manifest.create` is Tier
24
+ 1 (about one call a minute); `chat.postMessage` allows about one message a
25
+ second per channel. Slack's 2025 limit on `conversations.history` (1 request
26
+ a minute, 15 objects) applies to apps sold outside the Marketplace, not to
27
+ an internal app installed in its own workspace.
28
+
29
+ ## Only a person can
30
+
31
+ - Sign in to Slack and pick the workspace. Anyone can create the app; a
32
+ workspace that requires approved apps turns the install into a request a
33
+ Workspace Owner (or an app manager they named) approves first. That
34
+ approval is a wait: resume at Verify 3 once they say it is approved.
35
+ - Create the app in the browser, or generate the app configuration token
36
+ that lets you create it.
37
+ - Copy the Client ID and Client Secret off Basic Information (path A).
38
+ - Click **Allow** on Slack's consent page when connecting Open CRM.
39
+
40
+ ## Before you start
41
+
42
+ 1. `pnpm everyapp secret list crm --json`, from any directory (the id reads
43
+ the manifest Open CRM's last deploy stored; in its directory the id can
44
+ be left off), before the Slack side, not at Verify. The rule: read
45
+ `data.app.setup.settings`, `unmet` and `command`. The client is one
46
+ setting there, `id: "slack"`, `kind: "capability"`, `holder:
47
+ "gateway"`, `names: ["SLACK_CLIENT_ID", "SLACK_CLIENT_SECRET"]` (an
48
+ OAuth client's id and secret are the two names of one setting, set
49
+ only when both are), `need.kind` as the CRM's manifest declares it
50
+ (Open CRM: `optional`: it sits in `settings` with `set: false`, never
51
+ in `unmet`, the CRM opens without Slack and `command`,
52
+ `pnpm everyapp configure crm`, offers it after the required ones;
53
+ `required` would put it in `unmet`, close the CRM and have configure
54
+ ask for both names first), `setup: "setup/slack.md"` (this file). While
55
+ its `set` is false this file applies.
56
+ The same entry in `data.app.setup.settings` with `set: true`: ask
57
+ before replacing (a new app, or a regenerated secret). Not deployed
58
+ yet: `secret list crm` stops with `APP_NOT_FOUND`; a bare
59
+ `pnpm everyapp secret list --json` from the checkout still says whether
60
+ the two names are in `data.gateway.names` (ask before replacing), and
61
+ the rest of this item waits for the first deploy.
62
+ 2. `pnpm everyapp status --json`: `data.installed` and `data.claimed` are
63
+ true; `data.appBaseDomain` is `<app base domain>` (the redirect URL to
64
+ register is `https://<app>.<app base domain>/integrations/slack/callback`,
65
+ deployed as `crm`: `https://crm.<app base domain>/integrations/slack/callback`);
66
+ Open CRM's entry in `data.apps[]` (its `id`, `crm` below) has
67
+ `"status": "active"`, `"bound": true`, the `workerName` Verify 4 needs
68
+ and, as `setup`, 1's `data.app.setup` again: what else the CRM needs.
69
+ Not deployed yet: the URL is the same, so the Slack side can go first;
70
+ Verify 3 needs the CRM deployed.
71
+ 3. In 1's answer, `data.app.workers[].names` must hold
72
+ `TOKEN_ENCRYPTION_KEY`, the CRM's own secret its Slack connect needs (it
73
+ stores the tokens encrypted under it; optional in the manifest, so its
74
+ entry sits in `settings` with `holder: "app"`, never in `unmet`).
75
+ Unset: set it first, after the CRM's first deploy, at the hidden prompt
76
+ of `pnpm everyapp configure crm` (which offers it with the optional
77
+ ones) or `pnpm everyapp secret set TOKEN_ENCRYPTION_KEY --app crm`, from
78
+ anywhere, 32 or more random characters, never through the chat; without
79
+ it **Connect Slack** stops
80
+ before Slack opens (the table). Not deployed yet: set it right after its
81
+ first deploy, before Verify 3.
82
+ 4. Ask: which workspace; whether a Slack app for this exists already at
83
+ https://api.slack.com/apps (reuse it: step 2a's second paragraph); and
84
+ whether they have `curl` and `jq` (path B).
85
+ 5. Pick a path. **A**: they create the app from a manifest in the browser
86
+ and type the client at a hidden prompt. **B**: they generate an app
87
+ configuration token; `apps.manifest.create` returns the client, piped in.
88
+
89
+ ## Steps
90
+
91
+ 1. **You:** write the manifest to a file outside the repository, say
92
+ `/tmp/slack-manifest.json`, with `<app>` (Open CRM's `id`) and
93
+ `<app base domain>` filled in. Nothing in it is secret.
94
+
95
+ ```json
96
+ {
97
+ "_metadata": { "major_version": 2, "minor_version": 1 },
98
+ "display_information": {
99
+ "name": "Open CRM",
100
+ "description": "Team agents post to your Slack channels from Open CRM."
101
+ },
102
+ "features": { "bot_user": { "display_name": "open-crm", "always_online": false } },
103
+ "oauth_config": {
104
+ "redirect_urls": ["https://<app>.<app base domain>/integrations/slack/callback"],
105
+ "scopes": {
106
+ "bot": ["users:read", "users:read.email", "channels:read", "groups:read", "chat:write", "chat:write.public", "im:write", "channels:join", "channels:manage", "groups:write", "channels:write.invites", "groups:write.invites"],
107
+ "user": ["channels:read", "channels:write", "groups:read", "groups:write"]
108
+ }
109
+ },
110
+ "settings": { "org_deploy_enabled": false, "socket_mode_enabled": false, "token_rotation_enabled": false }
111
+ }
112
+ ```
113
+
114
+ Use these lists as written: they are Open CRM's own (its
115
+ `src/shared/slack.ts`, which matters only when a newer CRM changes them;
116
+ a CRM directory with no `src/` has nothing to compare, and needs none):
117
+ bot scopes to post, manage channels and match members by email; user
118
+ scopes so the installer can invite the bot to private channels. Its
119
+ authorize URL carries them as `scope` (bot) and `user_scope` (user), and
120
+ that URL decides what Slack asks the installer for: Slack's docs name
121
+ only `invalid_scope` (a scope that doesn't exist, or conflicting ones)
122
+ and `unapproved_scope` (a published Marketplace app requesting scopes
123
+ its review didn't cover) on the authorize URL. The manifest's lists are
124
+ what the console's own Install button and a reinstall request. The
125
+ registry (`packages/sdk/src/internal/capabilities.ts`, the `slack`
126
+ entry's `scopePolicy`, #425) groups them into sets an app lists in its
127
+ manifest entry (`everyapp deploy` refuses `slack` without `scopes`):
128
+ bot sets `channels.read`, `users.read`, `users.email`,
129
+ `messages.write`, `channels.join`, `channels.manage`, `files.write`,
130
+ user set `user-channels.manage` (the table in docs/capabilities.md,
131
+ "OAuth clients"). Open CRM lists all but `files.write`; the lists
132
+ above are the provider scopes behind its sets, less
133
+ `chat:write.customize` (in `messages.write`; the CRM doesn't ask for
134
+ it). The Gateway holds the CRM to its sets at `oauth.v2.access` (#411):
135
+ an entry without `scopes` is refused (`integration_scopes_undeclared`),
136
+ an installation carrying a scope outside them too
137
+ (`integration_scope_exceeded`: reinstall from the CRM); a CRM deployed
138
+ before its manifest carried `scopes` must redeploy before the Gateway
139
+ is updated from main (TODO.md "OAuth scopes (2026-10-24)", follow-up 0).
140
+ Slack's limits: `name` 35 characters, `description` 140; `display_name`
141
+ takes a-z, 0-9, `-`, `_` and `.` only. Step 3b validates the file first.
142
+
143
+ 2a. **Person (path A):** open https://api.slack.com/apps, signed in to
144
+ Slack: **Create New App** (https://api.slack.com/apps?new_app=1 opens
145
+ that dialog) → **From an app manifest** (older dialogs: **From a
146
+ manifest**) → pick the workspace → **Next** → the **JSON** tab, paste
147
+ the file's content → **Next** (the review lists the name, the redirect
148
+ URL and the scopes) → **Create**. It lands on **Basic Information**,
149
+ whose **App Credentials** section shows **Client ID** and a hidden
150
+ **Client Secret**; 3a uses both.
151
+ Reusing an app: **OAuth & Permissions**, add the redirect URL under
152
+ **Redirect URLs** and save; under **Scopes**, add the bot scopes to **Bot
153
+ Token Scopes** (**Add an OAuth Scope**) and the user scopes to **User
154
+ Token Scopes**. Or, with path B's token, `apps.manifest.update` with
155
+ `app_id=` and the same manifest; it returns no credentials.
156
+
157
+ 3a. **Person (path A):** in a second terminal, in the `every-app` folder:
158
+ `pnpm everyapp configure crm` (it asks for the client's two names, id
159
+ then secret, with hidden prompts, after the CRM's required settings;
160
+ the client is optional for Open CRM, so Yes to `Set the N optional
161
+ settings too?`, Enter skipping the others; with one of the two
162
+ names set it asks only for the other; or pick
163
+ `SLACK_CLIENT_ID + SLACK_CLIENT_SECRET` from the menu once the CRM is
164
+ ready) or `pnpm everyapp secret set SLACK_CLIENT_ID SLACK_CLIENT_SECRET`
165
+ for just these two (README's first way; no `--json`, so it prompts).
166
+ Paste the Client ID at the first prompt; click **Show** next to Client
167
+ Secret and paste it at the second. Nothing is echoed. Give 2a and 3a in
168
+ one message and wait once, until they say both values are in (a create
169
+ that failed, they report instead). Go to Verify.
170
+
171
+ 2b. **Person (path B):** open https://api.slack.com/apps; below the list of
172
+ apps, under **Your App Configuration Tokens**, click **Generate Token**,
173
+ pick the workspace, **Generate**; copy the **Access Token** (it starts
174
+ `xoxe.xoxp-`, expires 12 hours later, and adds "Slack Tooling Tokens
175
+ Vendor" to the workspace). In their terminal, in the `every-app` folder,
176
+ read it in hidden (README's third way), never on a command line or in a
177
+ typed `export …=`, which the shell history keeps: bash or zsh
178
+ `read -rs SLACK_CONFIG_TOKEN && export SLACK_CONFIG_TOKEN`, fish
179
+ `read -s -x SLACK_CONFIG_TOKEN`; they paste the token and press Enter;
180
+ nothing is echoed. Wait until they say it is exported.
181
+
182
+ 3b. **Person (path B)**, or **You** when they exported it before starting
183
+ you: one line at a time. `printf` keeps the token out of the process
184
+ list; `-d @-` reads it from stdin.
185
+
186
+ ```sh
187
+ printf 'token=%s' "$SLACK_CONFIG_TOKEN" | curl -s -d @- --data-urlencode manifest@/tmp/slack-manifest.json https://slack.com/api/apps.manifest.validate
188
+ ```
189
+
190
+ Expect `{"ok":true,"errors":[]}`; `invalid_manifest` names each problem
191
+ in `errors[].pointer`. Then, once only (each run makes another app). The
192
+ answer carries three secrets (`client_secret`, `signing_secret`,
193
+ `verification_token`), so it goes to a file only you and `jq` read,
194
+ created private first, and removed on the same line as the last pipe:
195
+
196
+ ```sh
197
+ install -m 600 /dev/null /tmp/slack-app.json
198
+ printf 'token=%s' "$SLACK_CONFIG_TOKEN" | curl -s -d @- --data-urlencode manifest@/tmp/slack-manifest.json https://slack.com/api/apps.manifest.create > /tmp/slack-app.json
199
+ jq -e '.ok' /tmp/slack-app.json && jq '.app_id' /tmp/slack-app.json
200
+ jq -r '.credentials.client_secret // empty' /tmp/slack-app.json | pnpm everyapp secret set SLACK_CLIENT_SECRET --json
201
+ jq -r '.credentials.client_id // empty' /tmp/slack-app.json | pnpm everyapp secret set SLACK_CLIENT_ID --json; rm -f /tmp/slack-app.json
202
+ ```
203
+
204
+ README's second way. `jq -e '.ok'` printing `false`: `jq '.error'`
205
+ names why (step 1, or the name is taken); stop and
206
+ `rm -f /tmp/slack-app.json`. `// empty` keeps a missing field from
207
+ going in as the string `null`: the pipe is empty and `secret set` stops
208
+ with `SECRET_VALUE_REQUIRED`. Never print the file, and remove it even
209
+ when a step failed. Each `secret set` ends with a `done` event. Keep the
210
+ `app_id` for Verify.
211
+
212
+ ## Verify
213
+
214
+ 1. `pnpm everyapp secret list crm --json`, from anywhere: the `slack`
215
+ setting has `set: true` (optional for Open CRM, so it was never in
216
+ `data.app.setup.unmet`; a manifest that requires it has it leave
217
+ `unmet`, with `ready: true` and `command` null once nothing else is
218
+ missing); both names in `data.gateway.names`, `"set": true` in
219
+ `data.gateway.needed`, not in `data.gateway.missing`. Then
220
+ `pnpm everyapp status --json`: the `crm` entry's `setup.ready` is true
221
+ and `data.fixes` has no `APP_NEEDS_SETUP` for it (the Gateway opens the
222
+ CRM again).
223
+ 2. Smoke check, in the person's terminal while `SLACK_CONFIG_TOKEN` is
224
+ exported (path B), after `export SLACK_APP_ID=<app_id>` (fish:
225
+ `set -x SLACK_APP_ID <app_id>`; the id isn't secret):
226
+
227
+ ```sh
228
+ printf 'token=%s&app_id=%s' "$SLACK_CONFIG_TOKEN" "$SLACK_APP_ID" | curl -s -d @- https://slack.com/api/apps.manifest.export | jq '.manifest.oauth_config'
229
+ ```
230
+
231
+ Expected: `redirect_urls` holds exactly
232
+ `https://<app>.<app base domain>/integrations/slack/callback`, and
233
+ `scopes.bot` and `scopes.user` the lists in step 1. Then
234
+ `unset SLACK_CONFIG_TOKEN SLACK_APP_ID` (fish: `set -e`). Path A: the
235
+ same is on the app's **OAuth & Permissions** page.
236
+ 3. The app, deployed and open: 1's `data.app.setup.ready` true. While
237
+ another required setting of the CRM is unmet, the Gateway sends a
238
+ browser to its setup dialog instead of the CRM (anything else gets 503
239
+ `app_needs_setup`): finish those first (each `unmet` entry's `setup`
240
+ names its file); until then 1 is the check of the Slack client. Then
241
+ at `https://<app>.<app base domain>`, **Settings →
242
+ Connections → Slack** (an owner or admin). With the client on the
243
+ Gateway the card has **Connect Slack**; without it the section reads
244
+ **Slack is not configured** and, for an owner or admin, shows the
245
+ redirect URL and `pnpm everyapp secret set SLACK_CLIENT_ID SLACK_CLIENT_SECRET`
246
+ (Verify 1 again; the same when the deployed CRM doesn't list `slack`,
247
+ or lists it without `scopes`).
248
+ **Connect Slack**: Slack's page names the app and the workspace and
249
+ lists the permissions; **Allow**; the CRM comes back connected and shows
250
+ the workspace. Otherwise its page says why, `?error=` naming one of
251
+ seven: `access_denied` (Cancel was clicked), `invalid_state` (the
252
+ approval is older than ten minutes, was used already, or was started by
253
+ someone else: start again), `forbidden` (not an owner or admin),
254
+ `rejected` (Slack sent another error, or no code: try again),
255
+ `exchange_failed` (the token call returned no token: Verify 4, then the
256
+ table), `failed` (the CRM couldn't store the tokens: its log has
257
+ `[slack] connect failed`), `not_configured` (local dev only: the CRM
258
+ has no Gateway binding).
259
+ 4. Logs. Slack's error code is in Open CRM's Worker, not the Gateway's: the
260
+ Cloudflare dashboard, README's path with the CRM's Worker in place of
261
+ the Gateway's: **Workers & Pages** → the `crm` entry's `workerName` →
262
+ **Observability** (**Logs** on older accounts) → **Live** (Workers Logs keeps them; `cf workers` has
263
+ no tail, and `cf o11y telemetry query` needs a query body). On
264
+ `exchange_failed` the line `[slack] connect refused` carries `status`
265
+ and `reason`: Slack's code (`invalid_code`, `bad_redirect_uri`,
266
+ `invalid_client_id`, `bad_client_secret`: the table), or a Gateway
267
+ refusal that reached the callback (`status` 503, reason
268
+ `Service Unavailable`: `integration_not_configured`; 403, `Forbidden`:
269
+ `capability_not_requested`, `integration_scopes_undeclared` (the
270
+ deployed CRM lists `slack` without `scopes`) or
271
+ `integration_scope_exceeded` (the installation carries a scope outside
272
+ the CRM's sets: reinstall from the CRM)). The Gateway's own log (`every-app-gateway`,
273
+ same path) has one `integration.proxy.call` per token call:
274
+ `capability`, host, path, method, `grantType` (the request's
275
+ `grant_type`) and `status`; never the body, and Slack answers `200` with
276
+ `"ok": false` on a bad client, so it says only whether the call went
277
+ through.
278
+
279
+ ## If it fails
280
+
281
+ | Error or symptom | Meaning | What to do |
282
+ | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
283
+ | `SECRET_VALUE_REQUIRED` | Nothing was piped (path B: the field was missing, so `// empty` piped nothing) and there is no terminal to prompt at | `jq '.error' /tmp/slack-app.json`; or one of README's three ways (its `data.rerun` shows the `--from-env` form) |
284
+ | `SECRET_VALUE_REQUIRED` from `pnpm everyapp configure crm --json` | Nothing was piped (with `--from-env`, stdin isn't read), no variable of a missing name was found, and there is no terminal; the message names what the CRM still needs (this client is optional for Open CRM, so never among them) | The pipes in 3b, or the person's terminal (3a) |
285
+ | `SECRET_SEVERAL_NAMES` | A pipe or `--from-env` with two names | One name per call (the fix in `data.fix`) |
286
+ | `SECRET_NOT_DECLARED` with `data.didYouMean` | The name differs from a known one in case or underscores only | rerun with the name in `data.didYouMean` and the same value source (`--from-env VAR --json`, or the same pipe); `data.rerun` carries only the names, so run as is it stops with `SECRET_VALUE_REQUIRED` |
287
+ | `SECRET_NOT_DECLARED` without it | A misspelt name; in Open CRM's directory its `data.fix` offers to add it to the CRM's own `secrets` | Retype the name from Settings above; never add a `SLACK_*` name to Open CRM's `secrets` |
288
+ | CRM: **Slack is not configured**, the `secret set` command, no **Connect Slack** | The Gateway holds no client, or one of the two names (a call would get `integration_not_configured`); or the deployed CRM doesn't list `slack` with `access: "write"` and `scopes` (`capability_not_requested`; without `scopes`, `integration_scopes_undeclared`) | `secret set` both, then Verify 1; or its `everyapp.config.ts` (`slack: { access: "write", scopes: [...], why }`), then `pnpm everyapp deploy --json` in its directory |
289
+ | CRM: `exchange_failed`; its log `[slack] connect refused` with `status` 503 or 403 | The Gateway refused between **Connect Slack** and the callback: the client was unset, or the manifest changed, meanwhile | The row above, then connect again |
290
+ | CRM: **Connect Slack** fails before Slack opens: "Connections aren't set up on this CRM yet" | The CRM's own `TOKEN_ENCRYPTION_KEY` is unset, or under 32 characters | Set it after the CRM's first deploy: `pnpm everyapp configure crm` (which asks for it) or `pnpm everyapp secret set TOKEN_ENCRYPTION_KEY --app crm` from anywhere (in the CRM's directory the flag can be left off), the value in one of README's three ways (Before you start 3); connect again |
291
+ | Slack's page: "redirect_uri did not match any configured URIs", then `Passed URI: …` | The URI the CRM sent isn't registered on the app exactly, or under a registered URL | Register the Passed URI, `https://<app>.<app base domain>/integrations/slack/callback` (https only, no `#`), under OAuth & Permissions → Redirect URLs; Verify 2 |
292
+ | `bad_redirect_uri` (token call) | `redirect_uri` differed between the authorize and token calls | The CRM sends its own host; check the app runs on `https://<app>.<app base domain>` |
293
+ | `invalid_scope` | A scope name doesn't exist, or bot and user scopes are mixed up | Compare with step 1; bot scopes go in `scope`, user scopes in `user_scope` |
294
+ | `invalid_client_id` | `SLACK_CLIENT_ID` is wrong, or another app's | Set it again from Basic Information → App Credentials |
295
+ | `bad_client_secret` | `SLACK_CLIENT_SECRET` is wrong or was regenerated | Set it again (Rotate, below) |
296
+ | `invalid_code` | The code was used already or is older than ten minutes; also when `SLACK_CLIENT_ID` is another app's (Slack checks the code before the client) | Connect again and **Allow**; a second failure: compare the Client ID on Basic Information |
297
+ | CRM: `access_denied` | The person clicked Cancel on Slack's page | Connect again and **Allow** |
298
+ | `invalid_team_for_non_distributed_app` | Installing into a workspace other than the one the app was created in | Create the app in that workspace, or **Manage Distribution → Activate Public Distribution** |
299
+ | `token_expired`, `invalid_auth`, `not_authed` from `apps.manifest.*` | The configuration token is past 12 hours, wrong, or missing from the body | Generate a new one (step 2b) |
300
+ | `invalid_manifest` with `errors[]` | The manifest breaks Slack's schema | Fix what each `pointer` names; validate again |
301
+ | CRM: "Slack refused: a permission is missing. Reconnect Slack." | The workspace's install predates a scope | Disconnect and connect again, so Slack asks for the new scopes |
302
+
303
+ ## Rerun, rotate, remove
304
+
305
+ - **Rerun.** `configure` and `secret set` replace. Never run `apps.manifest.create` twice
306
+ for one app: update the existing one (step 2a's second paragraph, or
307
+ `apps.manifest.update`). A duplicate it made: with `SLACK_CONFIG_TOKEN`
308
+ and the duplicate's id exported, and only when the person said to delete
309
+ it (it is permanent),
310
+ `printf 'token=%s&app_id=%s' "$SLACK_CONFIG_TOKEN" "$SLACK_APP_ID" | curl -s -d @- https://slack.com/api/apps.manifest.delete`
311
+ (expect `{"ok":true}`). Path A, or a token past 12 hours: its **Basic
312
+ Information** page.
313
+ - **Rotate.** Basic Information → App Credentials → **Regenerate** next to
314
+ **Client Secret** (Slack's FAQ still says **Change secret**). The old
315
+ secret works for 24 hours unless revoked manually. The person sets
316
+ `SLACK_CLIENT_SECRET` again at the hidden prompt (step 3a); no API
317
+ returns the new secret.
318
+ - **Remove.** In Open CRM, **Settings → Connections → Disconnect Slack**.
319
+ Then `pnpm everyapp secret unset SLACK_CLIENT_ID SLACK_CLIENT_SECRET --json --yes`
320
+ (`--yes` only when the person said so). Then delete the app: the
321
+ `apps.manifest.delete` line above (path B, token still valid, person
322
+ said so) or https://api.slack.com/apps → the app → Basic Information; or
323
+ at least regenerate its secret: a client the Gateway no longer holds is
324
+ still a client.
325
+
326
+ ## Sources
327
+
328
+ - https://docs.slack.dev/app-manifests/configuring-apps-with-app-manifests (manifest flow; configuration tokens)
329
+ - https://docs.slack.dev/reference/app-manifest (fields and limits)
330
+ - https://docs.slack.dev/reference/methods/apps.manifest.create, …/apps.manifest.validate, …/apps.manifest.export, …/apps.manifest.delete (permanent; `token` and `app_id`)
331
+ - https://docs.slack.dev/tools/bolt-python/tutorial/custom-steps-workflow-builder-new/ (the create-from-manifest clicks: Create New App, From an app manifest, Next, JSON, Create)
332
+ - https://docs.slack.dev/authentication/installing-with-oauth/ (authorize parameters, redirect rules: https, no `#`, a subdirectory matches; `invalid_scope`, `unapproved_scope`, `bad_redirect_uri`; the code expires after ten minutes)
333
+ - https://docs.slack.dev/legacy/legacy-authentication/ (`error=access_denied` on Cancel; codes exchanged once, expire 10 minutes)
334
+ - https://docs.slack.dev/reference/methods/oauth.v2.access/ (token-call errors)
335
+ - https://docs.slack.dev/reference/scopes/channels.write (User only) and …/channels.manage (Bot only)
336
+ - https://docs.slack.dev/distribution/ (own workspace only until **Activate Public Distribution**)
337
+ - https://slack.com/help/articles/1500009181142 (no setting stops members creating internal apps) and https://slack.com/help/articles/222386767 (app approval: Workspace Owners and app managers approve requests)
338
+ - https://docs.slack.dev/authentication/verifying-requests-from-slack/ (**Regenerate**; the old secret valid 24 hours unless revoked) and https://docs.slack.dev/faq/ (**Change secret**)
339
+ - https://docs.slack.dev/apis/web-api/rate-limits and https://docs.slack.dev/changelog/2025/05/29/rate-limit-changes-for-non-marketplace-apps
340
+ - https://slack.com/help/articles/27204752526611 (free plan: 10 app installs)
341
+ - https://community.n8n.io/t/slack-oauth-redirect-uri-did-not-match-any-configured-uris-self-hosted-on-windows/271936 (the redirect error's wording, with its `Passed URI:` line)
342
+ - https://developers.cloudflare.com/workers/observability/logs/real-time-logs/ (Workers & Pages → Overview → the Worker → Logs → Live)
343
+ - https://api.slack.com/apps (the console; `?new_app=1` opens the create dialog)
344
+ - Local, 2026-10-24: `packages/sdk/src/internal/capabilities.ts` (the `slack` entry: its `scopePolicy`, seven bot sets and one user set, #425), docs/capabilities.md ("OAuth clients": the scope sets table), TODO.md ("OAuth scopes (2026-10-24)", the Slack bullet)
@@ -0,0 +1,138 @@
1
+ # Workers AI for the Gateway
2
+
3
+ Workers AI runs Meta's Llama and other open models inside the Gateway's own
4
+ Cloudflare account, behind the Gateway's AI proxy. agentic-inbox lists it as
5
+ `optional: true` beside a required `openrouter`, so `secret list` shows the
6
+ token in `settings` with `set: false`, never in `unmet`: the inbox opens
7
+ without it. Its Worker calls Workers AI (the Email Agent, the draft verifier
8
+ and the prompt-injection scanner, on `/api/ai/workers-ai/v1/chat/completions`)
9
+ only when built with `AI_PROVIDER=workers-ai`, which is still the build
10
+ default until the inbox's PR #6 flips it to OpenRouter; a Worker built with
11
+ it and no token says so in the chat. Only the token is missing: the account is
12
+ `CLOUDFLARE_ACCOUNT_ID`, which `everyapp setup` set. At the end the person
13
+ has one Cloudflare API token limited to Workers AI on that account, held by
14
+ the Gateway, and a Workers AI model answering through it.
15
+
16
+ ## Settings
17
+
18
+ | Name | Required | What it holds |
19
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `WORKERS_AI_API_TOKEN` | optional for agentic-inbox (`{ id: "workers-ai", optional: true }`); needed only by a Worker built with `AI_PROVIDER=workers-ai` | A Cloudflare API token with Workers AI Read on the Gateway's account (and Edit when made by hand: Cloudflare's REST guide asks for both on a custom token): the token string alone, one line, no `Bearer`. New user tokens start `cfut_`, account tokens `cfat_`. |
21
+ | `CLOUDFLARE_ACCOUNT_ID` | set once by `everyapp setup`; `secret set` refuses it (`SECRET_NOT_SETTABLE` from the every-app checkout, `SECRET_NOT_DECLARED` from an app directory) | The account inference runs and bills in: `data.accountId` in `pnpm everyapp status --json`, written `<account id>` below. The token must include this account. |
22
+
23
+ `OPENROUTER_API_KEY` stays: the inbox's manifest requires it, and OpenSEO and Open CRM use it. This file adds a provider; it removes none.
24
+
25
+ ## Costs and plans
26
+
27
+ As of October 2026 (Cloudflare's pricing page, updated 2026-10-20):
28
+
29
+ - The Gateway's account is on Workers Paid already ($5 a month; everyapp needs it). Workers AI adds no fixed fee, and an API token is free.
30
+ - 10,000 neurons a day are included. Beyond that, Workers Paid bills $0.011 per 1,000 neurons on the monthly invoice; Workers Free stops for the day instead (error 3036).
31
+ - The Workers AI path's default agent model (and its draft verifier), `@cf/meta/llama-4-scout-17b-16e-instruct`: $0.27 per million input tokens, $0.85 per million output; 300 requests a minute.
32
+ - `@cf/moonshotai/kimi-k2.6` (the inbox's optional `AGENT_MODEL`): paid-only; $0.95 in and $4.00 out per million tokens; 20 requests a minute per account and model on Workers Paid (Cloudflare's 50-a-minute tier is for calls through an AI Gateway on prepaid credits, which the Gateway doesn't use), shared by every organization on the Gateway.
33
+
34
+ ## Only a person can
35
+
36
+ - Sign in to the Cloudflare dashboard and create the token. `cf`'s OAuth login cannot mint one: `cf auth whoami` lists no API-token scope, and the token endpoints answer it 403 code 9109. So neither `cf accounts tokens create` nor the API works from the login; it needs a token to make a token.
37
+ - Copy the token off the screen (shown once) and type or export it in their own terminal.
38
+ - Choose a user token (tied to their login) or an account token (a team's choice; needs API Token Provisioning or Super Administrator on the account), and whether it expires.
39
+
40
+ ## Before you start
41
+
42
+ 1. `pnpm everyapp secret list agentic-inbox --json`, from any directory: the `WORKERS_AI_API_TOKEN` entry in `data.app.setup.settings` (`names` `["WORKERS_AI_API_TOKEN"]`, `need.kind` `optional`, `holder` `gateway`, `setup` `setup/workers-ai.md`) with `"set": false` means this file applies. It is never in `unmet` (`command`, `pnpm everyapp configure agentic-inbox`, offers it after the inbox's required settings) and the inbox opens without it; what keeps the inbox closed is `OPENROUTER_API_KEY` ([openrouter.md](openrouter.md)) and its email sender ([email.md](email.md)); `data.app.setup.groups` is `[]`. `"set": true`: ask before replacing (a value can't be read back; replacing means a new token). In the older fields `data.gateway.needed` has it with `"required": false`, never in `data.gateway.missing`. `status` prints no `Guide:` line for it (only unmet settings get one): `secret list --json`, carrying the path as the entry's `setup`, is how an agent lands here (in the checkout: `packages/cli/skills/everyapp/setup/workers-ai.md`). `APP_NOT_FOUND` ("agentic-inbox isn't deployed to this Gateway yet"): deploy the app first, from its directory (the token can be set before that all the same, from anywhere).
43
+ 2. `pnpm everyapp status --json`: `data.installed` and `data.claimed` true; `data.accountId` is the account the token must include (tell the person; it is not a secret); `data.apps[]` has `"id": "agentic-inbox"` with `"status": "active"`, `"bound": true` and the same `setup`; while a required setting is missing `data.fixes` has `APP_NEEDS_SETUP` for it (severity `warning`, headline "agentic-inbox needs setup", `run` the same command). `action_required cloudflare_login` with `waiting: true` (`status` logs in by itself): show `data.url` and `data.code` to the person and keep reading; `pnpm everyapp login --json` is only for logging in ahead of time.
44
+ 3. In 1's output (or `pnpm everyapp secret list --json` from any directory), `data.gateway.names` has `CLOUDFLARE_ACCOUNT_ID`. Missing: stop and tell the person. It is written once, at install, from the account the Gateway lives in (`data.accountId` is what it should be); `secret set` refuses it and `setup` adds nothing to a Gateway that already has a version, so there is no everyapp repair today. The settings model leaves it out (an install-owned setting), so the setup gate doesn't notice it missing: the proxy's 503 (If it fails) is where it shows.
45
+ 4. Ask, together with 5 in one message, and wait for both answers before Steps (they change the steps): was the inbox's Worker deployed with `AI_PROVIDER=workers-ai` (a variable of its own deploy; `status` doesn't show it)? It decides whether the inbox needs this token at all: the manifest marks it optional, so the inbox opens either way. The build default is still `workers-ai` (the inbox's PR #6 flips it to OpenRouter; open): a Worker deployed without the variable uses this token; one deployed with `AI_PROVIDER=openrouter` never does, until its own Worker is deployed again without it, from its repository (its `cloudflare.config.ts` reads it), not from here: say so, and never redeploy the inbox from this file.
46
+ 5. Ask whether the person already holds a Cloudflare API token with Workers AI on this account (a token from another project is fine if it includes this account). Yes: skip to step 3.
47
+
48
+ ## Steps
49
+
50
+ One step per message, and wait between them: step 1 once the two answers from Before you start are in; step 2, then "copied"; step 3, then "done". The curl line comes in Verify, after that.
51
+
52
+ 1. **You:** Say which token to make. A user token (My Profile) is the quick path for a solo operator; an account token keeps working after that person leaves and is the better fit for a team. Both reach Workers AI the same way: the Gateway sends either as a Bearer token to `api.cloudflare.com/client/v4/accounts/<account id>/ai/v1/…`, and Cloudflare's compatibility matrix lists Workers AI as supporting account tokens (not tried live here). Give them `data.accountId` to check against the token's account resources.
53
+
54
+ 2. **Person:** Create the token. Done when the screen shows the token once, with a copy button and a `curl … /user/tokens/verify` test under it. Wait for "copied".
55
+ - Prefilled user token: open https://dash.cloudflare.com/?to=/:account/ai/workers-ai (the Workers AI page; pick the Gateway's account if asked), select **Use REST API**, then **Create a Workers AI API Token**. A token form opens prefilled (Cloudflare's guide says it grants Workers AI Read on this account, which is what the `/ai/*` endpoints need; one permission row is expected): check that **Account Resources** names the Gateway's account (`data.accountId`), leave **TTL (time to live)** empty unless they want an expiry (the Gateway stops the day it expires), select **Create API Token**, then **Copy API Token**.
56
+ - Custom user token: https://dash.cloudflare.com/profile/api-tokens, **Create Token**, the custom token at the bottom (label unverified), name it `every-app-gateway workers-ai`, permissions **Account** | **Workers AI** | **Read** and a second row **Account** | **Workers AI** | **Edit** (Cloudflare's REST guide asks for both on a token made by hand), account resources: Include, the Gateway's account (`data.accountId`), no client IP filter (the Gateway's egress addresses vary), **Continue to summary**, **Create Token**.
57
+ - Account token: in the dashboard pick the Gateway's account, **Manage account** > **Account API tokens**, **Create Token**, the same name and permissions, optional expiration, **Continue to summary**, **Create Token**.
58
+
59
+ 3. **Person:** Hand it to the Gateway (README's third way, the hidden read, in their own terminal, in the `every-app` folder; bash or zsh):
60
+ ```sh
61
+ read -rs WORKERS_AI_API_TOKEN; export WORKERS_AI_API_TOKEN
62
+ pnpm everyapp secret set WORKERS_AI_API_TOKEN --from-env WORKERS_AI_API_TOKEN --json
63
+ ```
64
+ After the first line the terminal shows nothing, not even a prompt: paste the token, press Enter, then run the second line. `SECRET_EMPTY` means the paste didn't land: redo the first line in the same terminal. fish: `read -s -x WORKERS_AI_API_TOKEN` (it does show `read> `), then the same `pnpm everyapp secret set … --from-env …` line; `set -e WORKERS_AI_API_TOKEN` clears it afterwards.
65
+ The last line is `"type": "done"`; the change is live at once (one new Gateway version, no deploy). `pnpm everyapp configure agentic-inbox --from-env --json` sets it from the same variable (and every other unset setting of the inbox whose variable is exported; its `done` event's `data.set` names them). Keep the variable until the smoke check below is done, then `unset WORKERS_AI_API_TOKEN`. Without the smoke check, README's first way is simpler: in a second terminal, `pnpm everyapp configure agentic-inbox` (it asks for the token after the inbox's required settings, as an optional one: Yes to `Set the N optional settings too?` (`Set the optional setting too?` for one), Enter skips the others; once the inbox is ready its menu lists `WORKERS_AI_API_TOKEN`) or `pnpm everyapp secret set WORKERS_AI_API_TOKEN` for just this one; paste at the hidden prompt. Wait for "done".
66
+
67
+ 4. **You:** Run the Verify section. The person runs its curl line in the terminal that still has the variable.
68
+
69
+ ## Verify
70
+
71
+ 1. `pnpm everyapp secret list agentic-inbox --json`, from anywhere: the `WORKERS_AI_API_TOKEN` entry in `data.app.setup.settings` has `"set": true` (it was never in `data.app.setup.unmet`: optional); `data.app.setup.ready` doesn't depend on it (`OPENROUTER_API_KEY`, required by the same manifest, and its email sender do: until then `command` and `data.next` name what is left). In the older fields, its `data.gateway.needed` entry has `"set": true`, `data.gateway.missing` never listed it, and `data.gateway.names` does (for an inbox not deployed yet, the root `pnpm everyapp secret list --json` shows `data.gateway.names` and that is the check). Then `pnpm everyapp status --json`: the `agentic-inbox` entry's `setup.ready` is true and `data.fixes` has no `APP_NEEDS_SETUP` for it, once everything required is set.
72
+ 2. Smoke check, in the person's terminal with `WORKERS_AI_API_TOKEN` exported. The header goes in on stdin (`-H @-`) so the token never sits in curl's argument list, which the process list shows. The token reaches Workers AI on the Gateway's account (a few neurons):
73
+ ```sh
74
+ printf 'Authorization: Bearer %s\n' "$WORKERS_AI_API_TOKEN" | curl -s -H @- -H "Content-Type: application/json" https://api.cloudflare.com/client/v4/accounts/<account id>/ai/v1/chat/completions -d '{"model":"@cf/meta/llama-4-scout-17b-16e-instruct","messages":[{"role":"user","content":"Say ok."}],"max_tokens":5}'
75
+ ```
76
+ expects a JSON body with `choices[0].message.content`. Any `"success":false` body is read in If it fails; a 401 starts with the verify curl there. Then `unset WORKERS_AI_API_TOKEN`.
77
+ 3. The app, only when the inbox's Worker was built with `AI_PROVIDER=workers-ai` (the build default until the inbox's PR #6; Before you start, 4); one built with `AI_PROVIDER=openrouter` never sends this token and the curl line above is the whole proof. Either way the inbox opens only once every required setting is set (Verify 1): a browser sent to `https://<gateway host>/?setup=agentic-inbox` means something still isn't. Open `https://agentic-inbox.<app base domain>/`, open a mailbox, **Show agent panel** (the header button), type in "Ask your email agent..." and send. A reply streams in, and the Gateway's `ai.proxy.usage` line (step 4) says `"provider":"workers-ai"`. A missing token shows up in the panel as "AI isn't set up on this gateway yet, so I can't help with this inbox. An admin sets WORKERS_AI_API_TOKEN with `pnpm everyapp secret set WORKERS_AI_API_TOKEN`."; any other refusal as the Gateway's message.
78
+ 4. The Gateway's logs, when a refusal isn't clear: `cf` has no terminal tail (`cf workers` lists none; `cf o11y telemetry live-tail` only prepares a websocket, not tried here; `cf o11y telemetry query` is the raw query API). Cloudflare's CLI for it is `npx wrangler tail every-app-gateway --format json` (after its own `wrangler login`; wrangler isn't in this checkout). Dashboard, README's path: https://dash.cloudflare.com/?to=/:account/workers-and-pages, then **Workers & Pages** → `every-app-gateway` → **Observability** (**Logs** on older accounts) → **Live**. The proxy writes one JSON line per call: `"evt":"ai.proxy.usage"` with `provider`, `model`, `status` and `usage`; `"evt":"ai.proxy.upstream_failed"` with the message when the request to Cloudflare failed. The Gateway's config turns these logs on; Workers Paid keeps 7 days.
79
+
80
+ ## If it fails
81
+
82
+ A 401 from the chat call: first, in the same terminal, Cloudflare's verify route says whether the string itself is good. A user token (`cfut_`):
83
+
84
+ ```sh
85
+ printf 'Authorization: Bearer %s\n' "$WORKERS_AI_API_TOKEN" | curl -s -H @- https://api.cloudflare.com/client/v4/user/tokens/verify
86
+ ```
87
+
88
+ An account token (`cfat_`): the same header on `https://api.cloudflare.com/client/v4/accounts/<account id>/tokens/verify`; on the user route it answers 401 code 1000 "Invalid API Token", like any token that route doesn't know. `"success":true` with `"status":"active"`: the string is good, and the rows for code 10000 and 9109 say what is (the permission or the account). 401 code 1000 on its own route: the string (its row).
89
+
90
+ | Error or symptom | Meaning | What to do |
91
+ | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
92
+ | a browser opening the inbox is sent to `https://<gateway host>/?setup=agentic-inbox`; any other request gets 503 with the body `{"error":"app_needs_setup","app":"agentic-inbox"}` | the Gateway's setup gate: a required setting of the inbox is still unset (`OPENROUTER_API_KEY` or its email sender; this token is optional and never closes the inbox) | `data.app.setup.command`, `pnpm everyapp configure agentic-inbox`, then Verify 1 |
93
+ | 503 `ai_provider_not_configured`, "needs WORKERS_AI_API_TOKEN and CLOUDFLARE_ACCOUNT_ID" | the Gateway holds no token (the inbox opens anyway: its manifest marks the provider optional; a Worker built with `AI_PROVIDER=workers-ai` shows it in the chat as "AI isn't set up on this gateway yet, so I can't help with this inbox. An admin sets WORKERS_AI_API_TOKEN with `pnpm everyapp secret set WORKERS_AI_API_TOKEN`."), or no account id (which the gate doesn't check) | `secret list --json`; set the token; `CLOUDFLARE_ACCOUNT_ID` missing: no everyapp repair today, tell the person (Before you start, 3) |
94
+ | `provider_not_allowed` | the app's manifest doesn't list `workers-ai` under `providers` | agentic-inbox lists it; another app adds it to `everyapp.config.ts` and deploys |
95
+ | `model_not_allowed` | a model id outside the app's `ai.models` | use one from the manifest, or add it and deploy |
96
+ | `ai_route_not_allowed` | a path other than `/v1/chat/completions`, `/v1/embeddings`, `/v1/responses` (the native run route is closed) | call the OpenAI-compatible routes |
97
+ | 502 `ai_upstream_failed` | the request to `api.cloudflare.com` failed before an answer | retry; the Gateway log line `ai.proxy.upstream_failed` has the message |
98
+ | 401, code 1000 "Invalid API Token" (the verify routes) | the string is wrong, expired, rolled or deleted; or a `cfat_` token on the user route | verify a `cfat_` token on `/accounts/<account id>/tokens/verify`; otherwise make a new token and set it again |
99
+ | 401, code 10000 "Authentication error" (the `/ai/v1` route: curl's chat completion, or passed through by the Gateway) | token invalid, expired, rolled, deleted, IP-filtered, or without Workers AI on this account; or `$WORKERS_AI_API_TOKEN` empty in that terminal | the verify curl above; make a new token with Workers AI Read including `data.accountId`; set it again |
100
+ | 403, code 9109 "Unauthorized to access requested resource" or "Invalid access token" | the token's resources don't include this account | a token whose account resources include the Gateway's account |
101
+ | 403, code 5035 "This model requires a Workers Paid plan." | a paid-only model (`kimi-k2.6`) on an account without Workers Paid | the Gateway's account must be on Workers Paid (everyapp needs it anyway) |
102
+ | 429, code 3036 "You have used up your daily free allocation of 10,000 neurons." | Workers Free's daily cap | Workers Paid; it doesn't occur there |
103
+ | 429, code 3040 "Capacity temporarily exceeded", or a bare 429 | Cloudflare capacity, or a rate limit (kimi: 20 a minute per account; the limit's own code unverified) | retry later; on kimi expect it, or use `@cf/meta/llama-4-scout-17b-16e-instruct` |
104
+ | 400 code 5007 "No such model", or 404 code 3042 "The model name is invalid" | the model id isn't in the catalog | `cf ai models list --search llama` (read-only) for a current id; change the app's manifest and code |
105
+ | `SECRET_EMPTY` | `$WORKERS_AI_API_TOKEN` is empty in the terminal that ran `secret set` | redo the `read -rs …; export …` line in that same terminal, then rerun |
106
+ | `SECRET_VALUE_REQUIRED` | nothing was piped and there is no terminal | one of README's three ways |
107
+ | `SECRET_VALUE_REQUIRED` from `pnpm everyapp configure agentic-inbox --json` | nothing was piped (with `--from-env`, stdin isn't read), no variable of a missing name was found, and there is no terminal; the message names what the inbox still needs (this token is optional, so never among them) | the person's terminal (step 3), `NAME=value` lines on stdin, or the hidden read then its `data.rerun`, `pnpm everyapp configure agentic-inbox --from-env` |
108
+ | `SECRET_SEVERAL_NAMES` | `--from-env` or a pipe with several names | one name per call |
109
+ | `SECRET_NOT_SETTABLE` (from the every-app checkout) or `SECRET_NOT_DECLARED` (from an app directory) | the name is one setup writes (`CLOUDFLARE_ACCOUNT_ID`) | set only `WORKERS_AI_API_TOKEN`; never add it to the manifest's `secrets`; a wrong account id has no everyapp repair today (tell the person) |
110
+ | 400, code 6003 "Invalid request headers" / 6111 "Invalid format for Authorization header" (the verify routes) | `$WORKERS_AI_API_TOKEN` is empty in that terminal (not exported, or a new terminal); the chat route shows the same as 401 code 10000 | redo step 3 in one terminal, in order; an HTML page instead means a mistyped URL |
111
+
112
+ ## Rerun, rotate, remove
113
+
114
+ - Rerun: every step is safe to repeat. `configure` and `secret set` replace the value in one new Gateway version; a second token on Cloudflare's side is harmless until you delete it. Interrupted or back later: start at Verify and continue at the first step that isn't done.
115
+ - Rotate: on the token's row in the dashboard (user tokens at https://dash.cloudflare.com/profile/api-tokens, account tokens under **Manage account** > **Account API tokens**) the three-dot menu, **Roll**, **Confirm**. The new secret keeps the permissions and the old one stops at once, so run step 3 again straight away. The API's roll: `cf user tokens roll <token-id>` for a user token, `cf accounts tokens roll <token-id>` for an account token; both need an API token with the matching API Tokens Edit permission, not the login.
116
+ - Remove: `pnpm everyapp secret unset WORKERS_AI_API_TOKEN --json` (add `--yes` only when the person said so). The inbox keeps opening (its manifest marks the provider optional); a Worker built with `AI_PROVIDER=workers-ai` then loses its agent, verifier and scanner to `ai_provider_not_configured`, one built with `AI_PROVIDER=openrouter` is unaffected. Then delete the token in the same row menu (label unverified): a token the Gateway no longer holds is still a token.
117
+
118
+ ## Sources
119
+
120
+ Verified 2026-10-23:
121
+
122
+ - https://developers.cloudflare.com/workers-ai/get-started/rest-api/ (Use REST API, Create a Workers AI API Token; the prefilled token grants Workers AI Read, a token made by hand needs Read and Edit)
123
+ - https://developers.cloudflare.com/fundamentals/api/get-started/create-token/ (dashboard flow, TTL, verify command, `cfut_`)
124
+ - https://developers.cloudflare.com/fundamentals/api/get-started/account-owned-tokens/ (Manage account > Account API tokens, roles, Workers AI supported, `cfat_`)
125
+ - https://developers.cloudflare.com/fundamentals/api/how-to/create-via-api/ (token creation needs a token with API Tokens Edit)
126
+ - https://developers.cloudflare.com/fundamentals/api/how-to/roll-token/ (Roll, Confirm)
127
+ - https://developers.cloudflare.com/fundamentals/api/reference/permissions/ (Workers AI Read, Workers AI Edit)
128
+ - https://developers.cloudflare.com/api/resources/user/subresources/tokens/methods/verify/ and https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/verify/; probed with a bogus token and an empty header: both verify routes answer 401 code 1000 and 400 code 6003/6111, `/ai/v1/chat/completions` 401 code 10000 for either
129
+ - https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/ (`/accounts/<account id>/ai/v1/chat/completions`, embeddings, responses for gpt-oss only)
130
+ - https://developers.cloudflare.com/workers-ai/platform/pricing/ (neurons, per-model prices, paid-only list)
131
+ - https://developers.cloudflare.com/workers-ai/platform/limits/ (300 a minute; 20 for paid-only models, 50 only through an AI Gateway on prepaid credits with Unified billing)
132
+ - https://developers.cloudflare.com/workers-ai/platform/errors/ (3036, 3040, 3042, 5007, 5035)
133
+ - https://developers.cloudflare.com/ai-gateway/usage/rest-api/ (`/accounts/<account id>/ai/*` needs Workers AI Read; code 10000 without it)
134
+ - https://developers.cloudflare.com/workers-ai/models/llama-4-scout-17b-16e-instruct/ and https://developers.cloudflare.com/workers-ai/models/kimi-k2.6/
135
+ - the live catalog via `cf ai models list` (`--search`, from its `--help`)
136
+ - https://developers.cloudflare.com/workers/observability/logs/real-time-logs/ (`npx wrangler tail`, no storage) and https://developers.cloudflare.com/workers/observability/logs/workers-logs/ (Observability, retention); https://developers.cloudflare.com/workers/wrangler/commands/workers/ (`wrangler tail [WORKER]`, `--format json`)
137
+ - `cf` 1.0.0-beta.14: `cf workers --help`, `cf o11y telemetry live-tail --help`, `cf user tokens --help`, `cf accounts tokens --help`
138
+ - In this repository: `apps/every-app-gateway/src/server/ai-proxy.ts`, `apps/every-app-gateway/src/server/app-needs-setup.ts` (the setup gate: the redirect to `/?setup=<app id>` and 503 `app_needs_setup`), `packages/sdk/src/internal/aiProviders.ts` (`setup/workers-ai.md`; `CLOUDFLARE_ACCOUNT_ID` under `alsoNeeds`), `packages/sdk/src/internal/appSettings.ts` (`AppSetup`: `unmet`, `settings`, `groups`, `command`; `needOf`: a plain provider id is required, `anyOf` makes a group; `providerNames` leaves install-owned settings out), `packages/sdk/src/internal/gatewaySettings.ts`, `packages/cli/src/lib/gateway/apply.ts` (install-owned secrets go with the first deploy only), `packages/cli/src/commands/secret/index.ts` (`secret list <app>` from anywhere, `data.app.setup`, the kept `data.gateway.needed` and `missing`, `APP_NOT_FOUND`), `packages/cli/src/commands/configure/index.ts` (`configure <app>`: the optional settings' prompts and the menu, `--from-env`), `packages/cli/src/lib/app/settings.ts` (the `Guide:` line), `packages/cli/src/commands/status/problems.ts` (`APP_NEEDS_SETUP`), `packages/cli/src/lib/secretValue.ts`, `docs/capabilities.md`; in agentic-inbox: `everyapp.config.ts` (`openrouter` required, `workers-ai` `optional: true`; no any-of group), `cloudflare.config.ts` (`AI_PROVIDER` defaults to `workers-ai` until its PR #6) and `workers/everyapp/gateway.ts` (each provider's models)
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- // Committed launcher so package managers can link the bin at install time,
3
- // before dist/ exists (fresh CI checkouts install with --ignore-scripts).
4
- import("../dist/bash-complete.js").catch((error) => {
5
- if (error?.code === "ERR_MODULE_NOT_FOUND") {
6
- console.error(
7
- "everyapp CLI is not built. In the monorepo, run: pnpm -C packages/cli build",
8
- );
9
- process.exit(1);
10
- }
11
- throw error;
12
- });
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import{a as o,b as s}from"./chunk-POSDLD3G.js";import"./chunk-RB5D2R45.js";import"./chunk-YJ2DKIZS.js";import{proposeCompletions as r}from"@stricli/core";var t=process.argv.slice(3);process.env.COMP_LINE?.endsWith(" ")&&t.push("");await r(s,t,o(process));try{for(let{completion:p}of await r(s,t,o(process)))process.stdout.write(`${p}
3
- `)}catch{}
@@ -1,8 +0,0 @@
1
- import{c as l,d as m,e as f,m as g,n as w,o as y}from"./chunk-DLGPLHBH.js";import{c as d}from"./chunk-U4LIQ5VR.js";import{c as h,d as k}from"./chunk-WO3NE7ZU.js";import I from"enquirer";import p from"chalk";async function x(){if(process.env.CLOUDFLARE_API_TOKEN){let r=process.env.CLOUDFLARE_ACCOUNT_ID;return r?f(r):(console.log(p.yellow(`
2
- CLOUDFLARE_ACCOUNT_ID is required when using CLOUDFLARE_API_TOKEN
3
- `)),d(1))}let e=await g(),{account:t,otherAccounts:o}=await y(e);return w(t,o),{id:t.account.id,name:t.account.name}}async function C(e="this",t=!1){let o=await x();if(process.env.CLOUDFLARE_API_TOKEN&&(console.log(p.dim(` ${o.name} (${o.id})`)),console.log()),t)return console.log(p.dim(`Skipping deployment confirmation due to -y flag.
4
- `)),!0;let{confirm:r}=await I.prompt({type:"confirm",name:"confirm",message:`Do you want to deploy ${e} to ${o.name}?`,initial:!1});return r}function v({domain:e,accountId:t,mode:o,hostname:r}){let n=t?`https://dash.cloudflare.com/${t}/${e}/dns/records`:`https://dash.cloudflare.com -> ${e} -> DNS`,i=r??`todo.${e}`,s=o==="app-blocking"?[`Each app you deploy will live at [app].${e} - this app at`,`${i} - but that address does not resolve yet. To support`,"this, you must add the DNS record below:"]:[`Each app you deploy will live at [app].${e} - for example`,`todo.${e}. To support this, you must add the DNS record`,"below (the login this CLI uses isn't allowed to add it for you):"],a=o==="app-blocking"?["After DNS propagates, re-run everyapp app deploy.","If you know DNS is ready and this check is blocked locally, re-run with --skip-dns-check."]:["Continuing the deploy - once you add that record, apps at",`*.${e} will work properly. You only do this once.`];return["",...s,"",` Add record at: ${n}`," Type: CNAME"," Name: *",` Target: ${e}`," Proxy: ON (orange cloud)","","Here are the docs explaining how this works:"," https://developers.cloudflare.com/workers/configuration/routing/routes/"," https://github.com/cloudflare/workers-sdk/issues/5568","",...a,""].join(`
5
- `)}import c from"chalk";async function u(e,t){let o=await m();if(t)return{accountId:o,workerName:t};let r=await k(e),n=h(r.id);return{accountId:o,workerName:n}}async function N(e){return await l(`/accounts/${e.accountId}/workers/scripts/${e.workerName}/secrets`)}async function W({cwd:e,workerName:t}){let o=await u(e,t);return(await N(o)).map(n=>n.name)}async function F({secretName:e,cwd:t,workerName:o,verbose:r=!1}){r&&console.log(c.dim(` Checking secret: ${e}`));let n=await u(t,o),s=(await N(n)).some(a=>a.name===e);return s&&r&&console.log(c.dim(` Secret already exists
6
- `)),s}async function R({secretName:e,secretValue:t,cwd:o,workerName:r,verbose:n=!1,description:i}){n&&i&&console.log(c.dim(` ${i}
7
- `));let s=await u(o,r);await l(`/accounts/${s.accountId}/workers/scripts/${s.workerName}/secrets`,{method:"PUT",body:JSON.stringify({name:e,text:t,type:"secret_text"})}),n&&console.log(c.green(`Created secret: ${e}
8
- `))}export{C as a,W as b,F as c,R as d,v as e};
@@ -1 +0,0 @@
1
- import{b as i,d,g as D}from"./chunk-DLGPLHBH.js";import{f}from"./chunk-WO3NE7ZU.js";import{execa as A}from"execa";async function E(s){let{config:t}=await f(s);if(!t.d1_databases||t.d1_databases.length===0)throw new Error("No D1 databases found in everyapp.config.ts. Please add a D1 resource declaration.");let a=t.d1_databases[0];if(!a)throw new Error("No D1 database configuration found.");let e=a.database_name;if(!e)throw new Error("No database_name found in generated D1 database configuration.");let[n,r]=await Promise.all([d(),i()]),o=(await D(n)).find(u=>u.name===e);if(!o)throw new Error(`Database "${e}" not found in your Cloudflare account. Have you run deployment to create the database?`);return{CLOUDFLARE_ACCOUNT_ID:n,CLOUDFLARE_DATABASE_ID:o.uuid,CLOUDFLARE_API_TOKEN:r}}async function O(s,t,a){let{cwd:e,env:n={},verbose:r=!1}=a,c=a.d1DatabaseId?{CLOUDFLARE_ACCOUNT_ID:await d(),CLOUDFLARE_DATABASE_ID:a.d1DatabaseId,CLOUDFLARE_API_TOKEN:await i()}:await E(e),o={cwd:e,stdio:r?"inherit":"pipe",env:{...process.env,...n,...c}};await A(s,t,o)}export{E as a,O as b};
@@ -1,2 +0,0 @@
1
- import{b as w,c as y}from"./chunk-VEYSNPDH.js";var h=15e3,c="INTERNAL_APIS_DISABLED",a=class extends Error{status;code;constructor({message:e,status:r,code:o}){super(e),this.name="GatewayApiError",this.status=r,this.code=o}},i=class extends a{constructor(e="OUTDATED_GATEWAY"){super({message:e,status:404}),this.name="OutdatedGatewayError"}},u=class extends a{constructor(e="GATEWAY_INTERNAL_APIS_DISABLED"){super({message:e,status:404,code:c}),this.name="GatewayInternalApisDisabledError"}},p=class extends a{constructor(e,r="GATEWAY_INTERNAL_AUTH"){super({message:r,status:e}),this.name="GatewayAuthError"}};function A(s){return s instanceof i}function f(s){return s instanceof u}function G(s){return s instanceof p}var l=class{gatewayUrl;getAuthToken;constructor({gatewayUrl:e,getAuthToken:r}){this.gatewayUrl=e.replace(/\/+$/,""),this.getAuthToken=r??(()=>y(this.gatewayUrl))}getIdentityKeys(){return this.request("/api/deploy/identity-keys",{method:"GET"})}registerApp(e){return this.request("/api/deploy/register",{method:"POST",body:e})}async hasOwner(){let e=await this.request("/api/admin/has-owner",{method:"GET",auth:!1});if(typeof e.hasOwner!="boolean")throw new a({message:"Gateway returned an invalid owner response",status:200});return e.hasOwner}whoami(){return this.request("/api/deploy/whoami",{method:"GET"})}async request(e,r){let o={"content-type":"application/json"};if(r.auth!==!1){let g=await this.getAuthToken();o.authorization=`Bearer ${g}`}let t=await fetch(`${this.gatewayUrl}${e}`,{method:r.method,signal:AbortSignal.timeout(h),headers:o,body:r.body?JSON.stringify(r.body):void 0}),d=await t.text(),n=null;if(d.trim())try{n=JSON.parse(d)}catch{throw t.ok?new a({message:"Gateway returned an invalid JSON response",status:t.status}):new a({message:`Gateway request failed (${t.status})`,status:t.status})}if(t.status===404)throw n?.code===c?new u:new i;if(!t.ok)throw t.status===401||t.status===403?new p(t.status,[n?.error||"Gateway deploy-token authorization failed.",w(this.gatewayUrl)].join(`
2
- `)):new a({message:n?.error||`Gateway request failed (${t.status})`,status:t.status,code:n?.code});return n??{}}};export{A as a,f as b,G as c,l as d};