everyapp 0.2.2 → 0.3.0-rc.2

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -77
  3. package/bin/everyapp.js +235 -0
  4. package/dist/access-LGEN3ABJ.js +1 -0
  5. package/dist/activity-RSD4D66F.js +12 -0
  6. package/dist/apps-4RCFXT74.js +13 -0
  7. package/dist/chunk-23HJ5I2A.js +2 -0
  8. package/dist/chunk-2Y5Q6A5K.js +6 -0
  9. package/dist/chunk-4JRIS6S6.js +1 -0
  10. package/dist/chunk-5PTNYZK6.js +4 -0
  11. package/dist/chunk-6OGVO7QK.js +6 -0
  12. package/dist/chunk-6PGUCTJ6.js +11 -0
  13. package/dist/chunk-AGTCKFRB.js +22 -0
  14. package/dist/chunk-BHMRY53V.js +2 -0
  15. package/dist/chunk-CB4U7H2G.js +4 -0
  16. package/dist/chunk-CCWBPAKH.js +4 -0
  17. package/dist/chunk-CZTEDWZ2.js +22 -0
  18. package/dist/chunk-CZXHKPRZ.js +2 -0
  19. package/dist/chunk-E3EPK27P.js +10 -0
  20. package/dist/chunk-EGEWRFVY.js +1 -0
  21. package/dist/chunk-ELK7QZ2M.js +1 -0
  22. package/dist/chunk-FJJSSVET.js +33 -0
  23. package/dist/chunk-GKBPYVP3.js +25 -0
  24. package/dist/chunk-H6QQMY7B.js +18 -0
  25. package/dist/chunk-H7PDL3X5.js +1 -0
  26. package/dist/chunk-HOUHG236.js +20 -0
  27. package/dist/chunk-IFCPALVC.js +46 -0
  28. package/dist/chunk-IUOBIB5M.js +30 -0
  29. package/dist/chunk-J35DDCNU.js +1 -0
  30. package/dist/chunk-J455FAEN.js +7 -0
  31. package/dist/chunk-JYKRUS5C.js +1 -0
  32. package/dist/chunk-LXYMDZER.js +1 -0
  33. package/dist/chunk-LYNBZ7RV.js +57 -0
  34. package/dist/chunk-M2QQ2UD7.js +132 -0
  35. package/dist/chunk-N4E5YE4X.js +5 -0
  36. package/dist/chunk-N5GQLGEC.js +2 -0
  37. package/dist/chunk-NFU4YFXB.js +1 -0
  38. package/dist/chunk-OM2HDWVP.js +1 -0
  39. package/dist/chunk-ONLNJXVQ.js +32 -0
  40. package/dist/chunk-OP4DYXYI.js +2 -0
  41. package/dist/chunk-PNLU5U3P.js +1 -0
  42. package/dist/chunk-PS7S74DG.js +6 -0
  43. package/dist/chunk-RMF4YGD6.js +98 -0
  44. package/dist/chunk-T2GD7RHS.js +10 -0
  45. package/dist/chunk-TLRM3P7V.js +15 -0
  46. package/dist/chunk-W3JCQHHU.js +1 -0
  47. package/dist/chunk-WOVS3GV5.js +10 -0
  48. package/dist/chunk-YHJZMJYZ.js +1 -0
  49. package/dist/chunk-ZECBN24L.js +15 -0
  50. package/dist/chunk-ZEXVHB22.js +2 -0
  51. package/dist/chunk-ZFCUH5PK.js +7 -0
  52. package/dist/configure-F7UZCJUG.js +8 -0
  53. package/dist/create-2E5HVWYK.js +22 -0
  54. package/dist/db-HEQVTDTM.js +9 -0
  55. package/dist/delete-XCSIQIRS.js +3 -0
  56. package/dist/deploy-3QOZEZHC.js +1 -0
  57. package/dist/dev-JL3ZP5BT.js +86 -0
  58. package/dist/email-RLZFUFUN.js +10 -0
  59. package/dist/events-2V5VRBWP.js +15 -0
  60. package/dist/groups-HDG5DVFY.js +8 -0
  61. package/dist/index.js +97 -1
  62. package/dist/install-Z5FYXUYM.js +1 -0
  63. package/dist/login-KHJBC64Z.js +1 -0
  64. package/dist/mcp-EFDUKJ5V.js +1 -0
  65. package/dist/provider-EOI5PTFH.js +13 -0
  66. package/dist/release-74J5ZIL3.js +1 -0
  67. package/dist/restore-FFGYV7NO.js +3 -0
  68. package/dist/secret-7FKKTLHB.js +1 -0
  69. package/dist/setup-PAKRCAFO.js +1 -0
  70. package/dist/signin-AP3D4XM4.js +12 -0
  71. package/dist/status-RMWNHOEK.js +4 -0
  72. package/dist/teardown-E4JNKAZJ.js +22 -0
  73. package/dist/update-DWVZFNPS.js +1 -0
  74. package/dist/workspace-CJC5I66L.js +21 -0
  75. package/package.json +19 -16
  76. package/skills/build-every-app/SKILL.md +199 -0
  77. package/skills/build-every-app/patterns.md +140 -0
  78. package/skills/everyapp/SKILL.md +292 -0
  79. package/skills/everyapp/mcp.md +350 -0
  80. package/skills/everyapp/security-review.md +61 -0
  81. package/skills/everyapp/setup/README.md +274 -0
  82. package/skills/everyapp/setup/access.md +178 -0
  83. package/skills/everyapp/setup/context.md +117 -0
  84. package/skills/everyapp/setup/dataforseo.md +287 -0
  85. package/skills/everyapp/setup/email.md +366 -0
  86. package/skills/everyapp/setup/gateway.md +243 -0
  87. package/skills/everyapp/setup/google.md +360 -0
  88. package/skills/everyapp/setup/microsoft-365.md +281 -0
  89. package/skills/everyapp/setup/openai.md +318 -0
  90. package/skills/everyapp/setup/openrouter.md +309 -0
  91. package/skills/everyapp/setup/providers.md +1253 -0
  92. package/skills/everyapp/setup/remote-mcp.md +199 -0
  93. package/skills/everyapp/setup/signin-apple.md +178 -0
  94. package/skills/everyapp/setup/signin-cloudflare.md +148 -0
  95. package/skills/everyapp/setup/signin-github.md +139 -0
  96. package/skills/everyapp/setup/signin-google.md +178 -0
  97. package/skills/everyapp/setup/signin-microsoft.md +183 -0
  98. package/skills/everyapp/setup/signin-oidc.md +163 -0
  99. package/skills/everyapp/setup/slack.md +344 -0
  100. package/skills/everyapp/setup/workers-ai.md +138 -0
  101. package/bin/__everyapp_bash_complete.js +0 -12
  102. package/dist/bash-complete.js +0 -3
  103. package/dist/chunk-7BOQJFI5.js +0 -8
  104. package/dist/chunk-7YVJFS4A.js +0 -1
  105. package/dist/chunk-BIXKPCDH.js +0 -2
  106. package/dist/chunk-DLGPLHBH.js +0 -47
  107. package/dist/chunk-FD6K2YA7.js +0 -9
  108. package/dist/chunk-GAJEQYJH.js +0 -39
  109. package/dist/chunk-O2KDOP5R.js +0 -13
  110. package/dist/chunk-OI2MBGBG.js +0 -4
  111. package/dist/chunk-POSDLD3G.js +0 -8
  112. package/dist/chunk-RB5D2R45.js +0 -1
  113. package/dist/chunk-U4LIQ5VR.js +0 -1
  114. package/dist/chunk-VEYSNPDH.js +0 -3
  115. package/dist/chunk-VP6SKW44.js +0 -7
  116. package/dist/chunk-WO3NE7ZU.js +0 -7
  117. package/dist/chunk-XTO2VTPY.js +0 -6
  118. package/dist/chunk-YJ2DKIZS.js +0 -1
  119. package/dist/create-DYQ3NBWJ.js +0 -22
  120. package/dist/deploy-PYTCMDGV.js +0 -7
  121. package/dist/deploy-RMAHOA4Z.js +0 -55
  122. package/dist/dev-SAE237FS.js +0 -0
  123. package/dist/generateConfig-46Z3ERF3.js +0 -1
  124. package/dist/login-B6EF3SMX.js +0 -1
  125. package/dist/logout-XMKZBLNV.js +0 -1
  126. package/dist/remoteD1Shell-A67JMO3H.js +0 -6
  127. package/dist/setupLocal-TZJUW5ES.js +0 -6
  128. package/gateway/every-app-gateway-build.tar.gz +0 -0
@@ -0,0 +1,163 @@
1
+ # Sign in with any OpenID Connect issuer (your SSO) on the Gateway
2
+
3
+ Read [README.md](README.md) first: the rules every setup follows.
4
+
5
+ Puts **Continue with SSO** (or a label the person picks, such as
6
+ `Continue with Okta`) on the Gateway's sign-in page, beside the password,
7
+ through the company's identity provider: Okta, Keycloak, Auth0, Authentik,
8
+ Google Workspace as an issuer, and the like. At the end the person has a
9
+ web-application client at their identity provider, its settings on the
10
+ Gateway, and has signed in through it once.
11
+ `pnpm everyapp signin show oidc --json` names this file as `setup`, and the
12
+ CLI prints `Guide: .claude/skills/everyapp/setup/signin-oidc.md`.
13
+
14
+ Who gets in doesn't change: the issuer only says who someone is, and only
15
+ an ID token with `email_verified: true` counts. A member signs in;
16
+ someone with a pending invitation for that exact address joins with the
17
+ invited role; everyone else is refused ("You don't have an account on this
18
+ gateway.") and nothing is created. The rules:
19
+ `docs/runbooks/sign-in-providers.md`, "Who gets in".
20
+
21
+ Not this file: Cloudflare Access for SaaS (also an OIDC issuer, but with
22
+ its own `CLOUDFLARE_ACCESS_OIDC_*` settings; [access.md](access.md) and
23
+ `docs/runbooks/cloudflare-access.md`). Microsoft Entra ID: prefer
24
+ [signin-microsoft.md](signin-microsoft.md), which handles Entra's
25
+ verified-email rules; Entra's ID tokens carry no `email_verified` claim
26
+ (not among its optional claims), so here every sign-in would be refused.
27
+
28
+ ## Settings
29
+
30
+ | Name | Required | Flag | What it holds |
31
+ | --------------------------- | -------- | -------------------------------- | ----------------------------------------------------------------------------- |
32
+ | `OIDC_SIGNIN_ISSUER` | required | `--issuer <url>` | the issuer URL, `https://…`, which serves `/.well-known/openid-configuration` |
33
+ | `OIDC_SIGNIN_CLIENT_ID` | required | `--client-id <id>` | the client's ID; not a secret |
34
+ | `OIDC_SIGNIN_CLIENT_SECRET` | required | `--client-secret-from-env <VAR>` | the client's secret |
35
+ | `OIDC_SIGNIN_LABEL` | optional | `--label <text>` | the button text after "Continue with" (default `SSO`; 40 characters at most) |
36
+
37
+ Scopes asked for: `openid email profile`. The email is the ID token's,
38
+ else the userinfo endpoint's (for the same `sub` only), and counts only
39
+ with `email_verified: true`. ID tokens must be signed with RS256 (every
40
+ major issuer's default) and carry the sign-in's nonce.
41
+
42
+ ## Costs and plans
43
+
44
+ Whatever the identity provider charges; the Gateway adds nothing. Most
45
+ include OIDC web clients on every plan (Okta, Auth0 and Authentik plans
46
+ not checked here: unverified).
47
+
48
+ ## Only a person can
49
+
50
+ - Sign in to the identity provider's admin console and create the client.
51
+ - Assign the people (or groups) who may use it, where the provider asks.
52
+ - Copy the client secret off the screen and type it at the hidden prompt
53
+ of `pnpm everyapp signin add oidc`.
54
+ - Choose the button's label; sign in through it to check.
55
+
56
+ ## Before you start
57
+
58
+ 1. `pnpm everyapp status --json`: `data.installed` and `data.claimed` true;
59
+ `data.gatewayUrl` gives `<gateway host>`. Not installed or no owner:
60
+ [gateway.md](gateway.md) first. `data.access.on` true: see Verify 4.
61
+ 2. `pnpm everyapp signin show oidc --json` (changes nothing; `Unknown
62
+ command signin`: `git pull` in the `every-app` folder). `data.status`
63
+ `off`: this file applies; `needs setup`: `data.missing` says what is
64
+ left (step 4); `on`: ask before replacing. `data.callbackUrl` is
65
+ `https://<gateway host>/api/auth/callback/oidc`.
66
+ 3. Ask which identity provider, and the label they want on the button.
67
+
68
+ ## Steps
69
+
70
+ 1. **You:** give the person `data.callbackUrl`.
71
+ 2. **Person:** in the identity provider's admin console, create an OpenID
72
+ Connect client: type web application (confidential, with a secret),
73
+ grant **authorization code**, sign-in redirect URI the exact URL from
74
+ step 1, scopes `openid email profile`. Assign it to the people or groups
75
+ who should sign in (Okta and Authentik refuse unassigned users). Note
76
+ the issuer URL the console shows. Usual forms (check each with step 3):
77
+ Okta `https://<org>.okta.com` or `https://<org>.okta.com/oauth2/default`;
78
+ Keycloak `https://<host>/realms/<realm>`; Auth0
79
+ `https://<tenant>.<region>.auth0.com/` (with the slash); Authentik
80
+ `https://<host>/application/o/<slug>/`; Google Workspace
81
+ `https://accounts.google.com` (a Web application client as in
82
+ [signin-google.md](signin-google.md), Internal audience). The page
83
+ shows the client ID and secret; keep it open.
84
+ 3. **You:** check the issuer, nothing secret in it:
85
+ ```sh
86
+ curl -s https://<issuer host and path>/.well-known/openid-configuration | jq -r .issuer
87
+ ```
88
+ (an issuer ending in `/`: drop that slash before `/.well-known`). The
89
+ answer must equal the issuer URL exactly, trailing slash included; that
90
+ exact string is `--issuer`. HTML, nothing, or another string: the
91
+ wrong URL (Okta's org server and its `default` server are two issuers).
92
+ 4. **Person:** in a second terminal in the `every-app` folder:
93
+ ```sh
94
+ pnpm everyapp signin add oidc
95
+ ```
96
+ It asks for the issuer and the client ID (shown as typed), the client
97
+ secret (hidden: pasting works, nothing is echoed) and the label (shown
98
+ as typed; Enter leaves the default, `SSO`), and writes them in one new Gateway Worker
99
+ version, live on the next request. They tell you when it's done.
100
+ Another way: in that terminal, the hidden read (bash or zsh:
101
+ `read -rs OIDC_SIGNIN_CLIENT_SECRET && export OIDC_SIGNIN_CLIENT_SECRET`;
102
+ fish: `read -s -x OIDC_SIGNIN_CLIENT_SECRET`), paste, Enter, then
103
+ `pnpm everyapp signin add oidc --issuer <issuer> --client-id <client ID> --label Okta --client-secret-from-env OIDC_SIGNIN_CLIENT_SECRET --json`
104
+ and `unset OIDC_SIGNIN_CLIENT_SECRET` (fish: `set -e OIDC_SIGNIN_CLIENT_SECRET`).
105
+
106
+ ## Verify
107
+
108
+ 1. `pnpm everyapp signin list --json`: the `oidc` entry has `status: "on"`
109
+ and `missing` empty.
110
+ 2. The person's address at the identity provider must be a member's or
111
+ have a pending invitation: `pnpm everyapp workspace invite <email>`.
112
+ 3. **Person:** private window, `https://<gateway host>/sign-in` →
113
+ **Continue with <label>** → the identity provider's login → the
114
+ Gateway, signed in. The issuer's discovery document is fetched at this
115
+ moment (and cached for an hour), not when the Gateway starts.
116
+ 4. Behind Cloudflare Access: nothing else changes, but the identity
117
+ provider's address must be the one Access knows the person by.
118
+ 5. Logs (README's Verify 4): `signin.config_problem` names a malformed
119
+ setting (an issuer that isn't an https URL, a label too long).
120
+
121
+ ## If it fails
122
+
123
+ | error or symptom | meaning | what to do |
124
+ | --- | --- | --- |
125
+ | no button | a setting missing or malformed, or the version isn't live yet | `pnpm everyapp signin list --json`; logs' `signin.config_problem` |
126
+ | `SIGNIN_ISSUER_INVALID` | not an https URL, or it has a query, a fragment or credentials | the bare issuer URL from step 3 |
127
+ | `SETTING_VALUE_INVALID` | the label is over 40 characters | a shorter `--label` |
128
+ | "The sign-in provider couldn't be reached." (`SIGNIN_PROVIDER_UNAVAILABLE`) | the discovery document couldn't be fetched, or names another issuer than `OIDC_SIGNIN_ISSUER` | step 3; `signin add oidc --issuer <exact issuer> --json` (the rest is kept) |
129
+ | the provider's redirect URI error | the client's redirect URI isn't exactly `data.callbackUrl` | fix it at the provider |
130
+ | "The sign-in provider hasn't verified this email address." | the ID token lacks `email_verified: true` | the provider marks the user's email verified (Keycloak: the user's **Email verified**; Auth0: verify the email), or another provider |
131
+ | sign-in fails after the provider's login, logs mention the token's algorithm | the ID token isn't RS256 | switch the client's signing algorithm to RS256 |
132
+ | "You don't have an account on this gateway." | the address isn't a member's or invited | invite it exactly as the provider spells it |
133
+ | `SIGNIN_VALUE_REQUIRED` / `SECRET_EMPTY` | unattended, a required setting neither given nor set / the env variable empty in that shell | `data.rerun` with the flags / the hidden read again in the same terminal |
134
+
135
+ ## Rerun, rotate, remove
136
+
137
+ - Rerun: `pnpm everyapp signin add oidc` any time; values given replace,
138
+ settings not given are kept (Enter at a prompt keeps it).
139
+ - Rotate: a new secret at the provider, then `pnpm everyapp signin add oidc`
140
+ with Enter at the other prompts, or the hidden read and
141
+ `pnpm everyapp signin add oidc --client-secret-from-env OIDC_SIGNIN_CLIENT_SECRET --json`.
142
+ Then revoke the old secret there.
143
+ - Back to the default label: `pnpm everyapp secret unset OIDC_SIGNIN_LABEL --json`
144
+ (`--yes` only when the person said so).
145
+ - Remove, only when asked: `pnpm everyapp signin remove oidc --json --yes`
146
+ (`--yes` only when they said so). Accounts made with it stay. Then
147
+ delete the client at the provider.
148
+
149
+ ## Sources
150
+
151
+ - https://openid.net/specs/openid-connect-discovery-1_0.html (the
152
+ `/.well-known/openid-configuration` path, a trailing `/` removed first;
153
+ the document's `issuer` must be identical to the issuer URL)
154
+ - https://openid.net/specs/openid-connect-core-1_0.html (`email_verified`,
155
+ `nonce`, RS256 as the default ID token algorithm)
156
+ - https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims-reference
157
+ (Entra's claims: `email`, `xms_edov`, `verified_primary_email`; no
158
+ `email_verified`)
159
+ - The issuer forms in step 2 are the providers' usual ones, not checked
160
+ against each provider's docs today (unverified); step 3 checks the real
161
+ one.
162
+ - `docs/runbooks/sign-in-providers.md` ("Any OpenID Connect issuer",
163
+ "Troubleshooting") and `packages/sdk/src/internal/signInProviders.ts`
@@ -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)