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,61 @@
1
+ # Reviewing an app's changes for security problems
2
+
3
+ The Gateway in front of an app does the sign-in and passes identity in; the
4
+ app does the rest. Review the app's pending changes before a pull request
5
+ that touches identity, public routes, tools, secrets or queries: locally,
6
+ by the agent that made the change, with no CI job and no API key.
7
+
8
+ ## The app boundaries
9
+
10
+ What the review looks for (what the Gateway promises and what the app must
11
+ do itself is the threat model, `docs/threat-model.md` in the Every App
12
+ checkout, served at https://everyapp.com/docs/agent/repo/threat-model):
13
+
14
+ - A route, server function or tool that reads who the user is from anything
15
+ but the SDK (`everyApp()`, `getEveryAppUser()`, `requireEveryAppUser()`, a
16
+ tool call's identity): a client-supplied `x-everyapp-*` header, a cookie, a
17
+ query parameter or a body field naming a user or organization
18
+ - A route listed under `public` in `everyapp.config.ts` that returns member
19
+ data or changes state without a check of its own (a webhook signature, a
20
+ bearer token the app verifies): `public` means anyone on the internet
21
+ reaches it, and the Gateway forwards their `Authorization` header untouched
22
+ - A database query that doesn't scope by the user or organization from the
23
+ identity, so one person's rows reach another; SQL built from strings instead
24
+ of the query builder's parameters
25
+ - Agent tools (`defineTools()`): a tool that deletes or changes data without
26
+ declaring the matching risk, or that trusts an argument as a path, URL or
27
+ id without a check
28
+ - A secret read from anywhere but the Worker's bindings, or printed, logged
29
+ or sent to a client
30
+ - `fetch()` to a URL whose host or scheme comes from user input, and HTML
31
+ built from user input outside React (`dangerouslySetInnerHTML`, string
32
+ templates)
33
+
34
+ ## How to run it
35
+
36
+ - In Claude Code: `/security-review` (its built-in review of the branch's
37
+ pending changes), telling it to judge the diff against the boundaries
38
+ above (point it at this file). It reads the code; it does not need to run
39
+ anything.
40
+ - Any other agent: read `git diff <base>...HEAD` plus the working tree
41
+ against the list above, and report each finding as file, line, severity
42
+ (high: directly exploitable; medium: needs specific conditions; low:
43
+ defense in depth), what an attacker does, and the fix. Only report what
44
+ you are confident is exploitable.
45
+ - The model reads the diff as instructions too: the review is a second pair
46
+ of eyes on your own changes, never a gate on a stranger's. Read a
47
+ contributor's diff yourself first.
48
+ - Not findings: denial of service, rate limits, resource use, a missing
49
+ hardening measure, a dependency's version, test files, prose, anything
50
+ that needs a Cloudflare login or an environment variable the attacker
51
+ cannot have. The Gateway strips inbound `Cookie`, `Authorization`,
52
+ `x-everyapp-*` and `cf-access-*` headers before the app sees a private
53
+ request, and every private request carries a fresh identity token the
54
+ SDK verifies: a route with no authentication of its own is expected.
55
+
56
+ ## With a finding
57
+
58
+ Fix it on the branch (and add a test), or say in the pull request why it is
59
+ a false positive. A public route that must stay open (a webhook, a tracking
60
+ collector) is not a finding when it checks what it must itself; a public
61
+ route that returns member data without a check is.
@@ -0,0 +1,263 @@
1
+ # Setting up what the Gateway holds, end to end
2
+
3
+ For a coding agent setting up one integration for a person: the provider's
4
+ side (an account, a plan, an app registration), the key or OAuth client it
5
+ gives, the Gateway setting that holds it, and the check that it works. One
6
+ file per integration, below. Read this page first, then the one file, each
7
+ on its own with your file-reading tool (the two together are longer than a
8
+ shell tool shows: a `cat` of both was cut off in testing); follow the
9
+ file's steps in order. The person is a solopreneur or a small team's
10
+ founder: say what each step is for in one line, and say exactly what to
11
+ click when they have to do something themselves.
12
+
13
+ | Integration | File | Gateway settings | Used by |
14
+ | --------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
15
+ | Cloudflare account, the Gateway and its owner | [gateway.md](gateway.md) | set by `everyapp setup` (optional: `CF_DEPLOY_TOKEN`, `BROWSER_APP_INSTALLS`) | everything |
16
+ | Cloudflare Access in front of the Gateway | [access.md](access.md) | `CLOUDFLARE_ACCESS_TEAM_DOMAIN`, `CLOUDFLARE_ACCESS_AUD`, `CLOUDFLARE_ACCESS_APPS_AUD` (set by `everyapp access on`; optional: `EMAIL_PASSWORD_SIGN_IN`, `CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS`, `CLOUDFLARE_ACCESS_OUTER_RING_AUTO_JOIN`) | sign-in, every app |
17
+ | Email sending (Cloudflare Email Sending or Resend) | [email.md](email.md) | `EMAIL_FROM`, `EMAIL_FROM_NAME`; `RESEND_API_KEY`, `EMAIL_PROVIDER`, `EMAIL_REST_API_TOKEN` | invitations, password resets, agentic-inbox, Open Analytics (optional), any app that sends |
18
+ | DataForSEO | [dataforseo.md](dataforseo.md) | `DATAFORSEO_API_KEY` | OpenSEO; the DataForSEO MCP app (`pnpm everyapp mcp on dataforseo`) |
19
+ | Any remote MCP server, as an MCP app | [remote-mcp.md](remote-mcp.md) | `<ID>_MCP_TOKEN` (one per server; none for a public one) | the MCP app added with `pnpm everyapp mcp add <url>` |
20
+ | Context.dev | [context.md](context.md) | `CONTEXT_API_KEY` | Open CRM; OpenSEO (optional: rendered audits) |
21
+ | OpenRouter | [openrouter.md](openrouter.md) | `OPENROUTER_API_KEY` | OpenSEO, Open CRM, agentic-inbox |
22
+ | OpenAI | [openai.md](openai.md) | `OPENAI_API_KEY`, `AI_PROXY_EXTRA_ROUTES` | apps listing the `openai` provider |
23
+ | Workers AI | [workers-ai.md](workers-ai.md) | `WORKERS_AI_API_TOKEN` (`CLOUDFLARE_ACCOUNT_ID` is set by `everyapp setup`) | agentic-inbox (optional) |
24
+ | Google (Gmail, Calendar, Search Console and Analytics) | [google.md](google.md) | `GOOGLE_WORKSPACE_CLIENT_ID`, `GOOGLE_WORKSPACE_CLIENT_SECRET` | Open CRM |
25
+ | Microsoft 365 (Outlook mail and calendar) | [microsoft-365.md](microsoft-365.md) | `MICROSOFT_365_CLIENT_ID`, `MICROSOFT_365_CLIENT_SECRET` | Open CRM |
26
+ | Slack (OAuth client) | [slack.md](slack.md) | `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET` | Open CRM |
27
+ | Every catalog provider, one section each (generated) | [providers.md](providers.md) | each provider's own, named there | apps listing the provider |
28
+
29
+ A provider of the built-in catalog without a file of its own above is a
30
+ section of [providers.md](providers.md) under its id
31
+ (`setup/providers.md#<id>`: its settings, where the key comes from, how
32
+ the Gateway sends it, a smoke request, cost and rate), generated from the
33
+ catalog by `pnpm -C packages/cli docs:update`; one with a file has a
34
+ section there too, which points at the file. A provider registered on
35
+ one Gateway (`pnpm everyapp provider add`, the agent docs' `providers`
36
+ page) has no file at all: `pnpm everyapp provider show <id>` prints its
37
+ whole spec, hint and smoke request included, and its setting is set like
38
+ any Gateway setting (`pnpm everyapp secret set <NAME>`, which first names
39
+ the provider and the hosts its key goes to).
40
+
41
+ Which integrations a person needs: the apps they deploy decide. The four
42
+ Every App apps (OpenSEO, OpenCRM, OpenAnalytics, Agentic Inbox) are
43
+ installed from their GitHub releases by [gateway.md](gateway.md)'s last
44
+ step, each unless the person skips it; the keys they then ask for are the
45
+ files below. From any
46
+ directory, `pnpm everyapp secret list <app id> --json` (the id as `status`
47
+ lists it in `data.apps[].id`; in the app's directory it can be left out)
48
+ reports the app's setup state as `data.app.setup`: `ready` (the app
49
+ opens), `unmet` (the settings keeping it closed), `settings` (every
50
+ setting, set or not), `groups` (each any-of group, `met` once one member
51
+ is set) and `command`, the one `secret set` that sets everything missing
52
+ (`{ command, note? }`, the note an any-of group's alternatives; null once
53
+ ready). A setting has `names` (all must be set), `holder` (`gateway`, or
54
+ `app` for the app's own secret), `need.kind` (`required`, `optional`, or
55
+ `anyOf` with its `group`), `set` (true, false, or null when the app
56
+ Worker's names were never recorded, which never blocks), `blocking`, and
57
+ when known `why`, `hint` (what the key is and where it comes from, from
58
+ the provider's entry), `docs` (where the key comes from) and `setup`: the file
59
+ here that sets it up, relative to the everyapp skill (`setup/dataforseo.md`;
60
+ in an app `.claude/skills/everyapp/setup/dataforseo.md`, in this checkout
61
+ `packages/cli/skills/everyapp/setup/dataforseo.md`). An unset optional
62
+ setting is in `settings` with `set: false`, never in `unmet`. The older
63
+ fields still hold: `data.gateway.needed` (each Gateway name with `required`
64
+ and `set`) and `data.gateway.missing` (the required ones unset); from the
65
+ checkout root with no app named, `data.apps[]` has every app's `id` and
66
+ `setup`. `pnpm everyapp status --json` carries the same per app as
67
+ `data.apps[].setup`, and a deployed app whose `ready` is false gets the
68
+ `data.fixes` entry `APP_NEEDS_SETUP` (a warning, `<app> needs setup`,
69
+ `run` the command above). Under each unmet setting that has a guide,
70
+ `status` (under "<app> needs setup"; the same lines are in
71
+ `data.fixes[].message`) and `secret list <app id>` (under its unmet settings)
72
+ print `Guide: .claude/skills/everyapp/setup/<name>.md`, once per guide, and
73
+ `secret list --json` names it as the setting's `setup`: that is how an agent lands on
74
+ one of these files. A provider registered on the Gateway has no file, so
75
+ its line is `Guide: pnpm everyapp provider show <id>` instead. On the Gateway's home such an app shows
76
+ "Needs setup" and opens its setup dialog instead of the app (a browser
77
+ visit to the app's own host lands in that dialog too; any other request
78
+ gets a 503 `app_needs_setup`), and its tools stay out of the Gateway's
79
+ `/mcp`. Set up what is unmet, in the order the person wants; an optional
80
+ setting when they want its feature. A setting nobody needs is simply not
81
+ offered to apps.
82
+
83
+ ## The rules every setup follows
84
+
85
+ **Where you are.** Every command runs from the `every-app` checkout, as
86
+ `pnpm everyapp …` (never `npx`), with `--json`: one event per line, `done`
87
+ means success, exit 2 means an `action_required` for the person (show its
88
+ `message`, `data.url` and `data.code`; rerun `data.rerun` when they are
89
+ done), exit 1 an error (show it; its `data.fix` or `data.rerun` says what
90
+ to do). An event with `"waiting": true` is being waited for: keep reading.
91
+ A Gateway setting is set from any directory; an app's own secret (a name
92
+ its `everyapp.config.ts` lists under `secrets`) from the app's directory,
93
+ or from anywhere with `--app <id>` (against the manifest its last deploy
94
+ stored; `setup.command` carries the flag when it sets one), either way
95
+ after its first deploy. Every `cf …` in these files is the CLI's pinned
96
+ `cf` (1.0.0-beta.14, which `pnpm install` puts under `packages/cli`, not
97
+ on PATH): from the `every-app` folder, run it as `pnpm --filter everyapp
98
+ exec cf …` (the root's bare `pnpm exec cf` finds none). A bare `cf` only
99
+ when `cf --version` answers `1.0.0-beta.14` (a global install); never
100
+ install one.
101
+
102
+ **Deep references.** A command's flags and exit codes:
103
+ `pnpm everyapp <command> --help` (0 done, 1 error, 2 action required,
104
+ 251/252 a bad command or its arguments). The agent docs:
105
+ https://everyapp.com/docs/agent/, each page also as Markdown at
106
+ `https://everyapp.com/docs/agent/<page>.md` (`setup`, `cli`, `json`,
107
+ `errors`, `troubleshooting`, `manifest`, `identity`, `secrets`, `dev`,
108
+ `email`, `mcp`, `deploy`, `providers`, `access`; an error code's fix is at
109
+ `errors.md#<code>`, the code lower-cased). They go live with the launch on
110
+ 2026-10-27; until then each page is in the checkout at
111
+ `landing-page/content/agent/<page>.md`; `docs/runbooks/`,
112
+ `docs/threat-model.md` and `pnpm everyapp <command> --help` stay the
113
+ deeper reference. An error code these files don't
114
+ cover carries its own `data.fix` or `data.rerun`; it is raised in the
115
+ command's file under `packages/cli/src/commands/` or its library under
116
+ `packages/cli/src/lib/` (the error types: `packages/cli/src/lib/errors.ts`).
117
+ These files say what to do; the agent docs say why and what else is
118
+ possible.
119
+
120
+ **Not a Gateway setting.** A secret of the app's own repository (a GitHub
121
+ Actions secret for a workflow the person added; a new app ships none) is
122
+ set in that repository (`gh secret set NAME` there, the value from the
123
+ person's terminal), never with `everyapp secret set`, which refuses names
124
+ it doesn't know.
125
+
126
+ **Preconditions, checked, not assumed.** Before the provider's side, both,
127
+ in either order (the integration files lead with 2, which says whether
128
+ the file applies):
129
+
130
+ 1. `pnpm everyapp status --json`: `data.installed` and `data.claimed` are
131
+ true, `data.gatewayUrl` is the Gateway (its host is `<gateway host>`
132
+ below; `data.appBaseDomain` is where apps live, `<app>.<app base domain>`).
133
+ Not installed, or no owner: [gateway.md](gateway.md) first. `status`,
134
+ `secret list` and `email show` all need the Cloudflare login and an
135
+ installed Gateway. Each logs in by itself: with nothing logged in it
136
+ emits `action_required cloudflare_login` with `waiting: true` (show
137
+ `data.url` and `data.code`, keep reading; approved within 5 minutes,
138
+ the run goes on, otherwise it ends with the error
139
+ `CLOUDFLARE_LOGIN_FAILED`, exit 1, whose `data.rerun` is `pnpm everyapp
140
+ login`, which also logs in ahead of time). With no Gateway, `status`
141
+ stops with `install_gateway` (exit 2; its `data.rerun` is the setup
142
+ command), while `secret list` and `email show` end with the error
143
+ `GATEWAY_NOT_FOUND` (exit 1); the fix for both is
144
+ [gateway.md](gateway.md). In the report, `data.access` is `{ on,
145
+ teamDomain }` when Cloudflare Access can be checked, else null (a hosted
146
+ Gateway, no Worker yet, or cf couldn't list the Worker's settings: then
147
+ `data.fixes` has `ACCESS_UNKNOWN`). Each `data.fixes` entry carries
148
+ `code`, `severity` (`error`, `warning`, `info` or `settling`), `message`,
149
+ `run` (null without a command), and when it has them `note` (what `run`
150
+ does) and `fix` (a remedy that isn't a command). Only `error` and
151
+ `warning` are something to fix; `info` (`GATEWAY_OUTDATED`: `pnpm
152
+ everyapp update` updates the Gateway now; leave it unless the
153
+ person asks) and `settling` need nothing from you.
154
+ 2. `pnpm everyapp secret list <app id> --json`, from anywhere (the app's
155
+ directory is no longer required): this file applies when its setting
156
+ is in `data.app.setup.unmet` (each entry's `names`, `need.kind`,
157
+ `holder` and `setup` say what goes where and which file), or, for an
158
+ optional one, in `settings` with `set: false`; `data.app.setup.command`
159
+ is the one command that sets what is missing. For a setting no app
160
+ declares (the Gateway's own, Cloudflare Access's), from the checkout
161
+ root `pnpm everyapp secret list --json`: `data.gateway.names` is what
162
+ is set. Set already (`set: true`, or the name in `data.gateway.names`):
163
+ ask whether to replace it before doing anything on the provider's side;
164
+ a value can't be read back, so "replace" means a new key.
165
+
166
+ **The value never goes through the chat.** A key or client secret is never
167
+ asked for in the conversation, never shown, never put on a command line
168
+ (shell history, the process list), never written into a file you create in
169
+ the repository. `pnpm everyapp secret set NAME` reads the value from one of
170
+ three places, and your shell has no terminal, so it never prompts for you:
171
+
172
+ - **The person types it.** Tell them to open a second terminal in the
173
+ `every-app` folder and run `pnpm everyapp secret set NAME`; it asks for
174
+ the value with a hidden prompt (pasting works; nothing is echoed). Then
175
+ they tell you, and you verify. This is the default when the value is on a
176
+ dashboard the person is looking at.
177
+ - **A command produced it.** When a CLI or API call returns the key
178
+ (Microsoft's `az ad app credential reset`, Slack's `apps.manifest.create`,
179
+ a JSON file the console let the person download), pipe it straight in and
180
+ the value never appears anywhere: `… | jq -r '.client_secret' | pnpm everyapp secret set NAME --json`.
181
+ Never run the producing command on its own to look at the result first.
182
+ - **It is in an environment variable.** In their own terminal, in the
183
+ `every-app` folder, the person reads it in hidden (bash or zsh:
184
+ `read -rs NAME && export NAME`; fish: `read -s -x NAME`), pastes the
185
+ value and presses Enter (nothing is echoed; bash and zsh show no prompt
186
+ either), then runs `pnpm everyapp secret set NAME --from-env NAME --json`
187
+ there, and `unset NAME` (fish: `set -e NAME`) once the file's smoke
188
+ check is done. Never a typed `export NAME=…`: on a default macOS zsh and
189
+ on bash it lands in the shell history, leading space or not (only a
190
+ shell set up with `HIST_IGNORE_SPACE` or `HISTCONTROL=ignorespace` drops
191
+ it). A variable they exported before starting you: you run the same
192
+ `pnpm everyapp secret set NAME --from-env VAR --json`. Never `echo $VAR`,
193
+ and never ask them to paste the export line into the chat.
194
+
195
+ One value per call for a pipe or `--from-env`; several names at once only
196
+ at the hidden prompt. The change is live on the Gateway at once (one new
197
+ version of its Worker, no deploy). If `secret set` fails with
198
+ `SECRET_VALUE_REQUIRED`, nothing was piped: use one of the three ways above;
199
+ `SECRET_EMPTY` means the `--from-env` variable is empty or unset in the
200
+ shell that ran it: the hidden read again, in that same terminal.
201
+ `SETTING_VALUE_INVALID` means a plain setting (`EMAIL_FROM`,
202
+ `EMAIL_PROVIDER`, `EMAIL_PASSWORD_SIGN_IN`, a `CLOUDFLARE_ACCESS_*` or
203
+ `MCP_*` switch) got a value the Gateway wouldn't read: the message names the
204
+ setting and the value, `data.fix` says what it takes, and nothing was
205
+ written; pipe the right value. A plain setting's prompt at a terminal is
206
+ visible and says what it takes; a credential's stays hidden.
207
+
208
+ **Only a person can** sign in to a provider, accept its terms, add a card
209
+ or choose a plan, approve an OAuth consent, pick a Google Cloud project's
210
+ organization, or copy a secret off a screen. For each such step, say what
211
+ page to open (a direct URL where one exists), what to click, what to type,
212
+ and what they will see when it worked; then wait. Everything else (reading
213
+ status, creating what an API or CLI can create, setting the value from a
214
+ pipe, verifying) you do. With a browser tool you may do the clicking too,
215
+ but a secret shown on screen still goes in through the person's terminal
216
+ or a file they save, not through you.
217
+
218
+ **Verify, every time.** After `secret set`:
219
+
220
+ 1. `pnpm everyapp secret list <app id> --json`: in `data.app.setup` the
221
+ setting has `set: true` and is out of `unmet` (`ready: true` and
222
+ `command: null` once nothing else is missing); the older fields agree
223
+ (the name in `data.gateway.names`, its `data.gateway.needed` entry
224
+ `set: true`, out of `data.gateway.missing`). For the app as a whole,
225
+ `pnpm everyapp status --json`: `data.apps[].setup.ready` is true for it
226
+ and `data.fixes` has no `APP_NEEDS_SETUP` for it (a `SETTINGS_UNKNOWN`
227
+ warning means cf couldn't list a Worker's secrets, so those rows read
228
+ as unknown: run it again rather than trust them). For a setting no app
229
+ declares, `data.gateway.names` alone.
230
+ 2. The file's smoke check: a call that uses the value, made where the
231
+ value is (the person's terminal, with the variable they exported) or
232
+ through the app that uses it, and what its failure means.
233
+ 3. The app: open it and use the feature, or `pnpm everyapp status --json`
234
+ for `data.fixes` when the file says the setting changes what status
235
+ checks. A Gateway refusal names the setting: `integration_not_configured`
236
+ and `ai_provider_not_configured` mean the Gateway doesn't hold it (set
237
+ it, or the name is wrong); the provider's own error means the value is
238
+ wrong or the account isn't ready (the file says which).
239
+ 4. Logs, when a file's step sends you there: the Cloudflare dashboard,
240
+ **Workers & Pages** → `every-app-gateway` → **Observability** (**Logs**
241
+ on older accounts) → **Live** streams now; the stored Workers Logs are
242
+ on the same page (the Gateway deploys with them on). Which tab name an
243
+ account shows wasn't checked in a browser here (unverified). `cf`
244
+ 1.0.0-beta.14 has no Worker tail; `npx wrangler tail every-app-gateway`
245
+ streams the same after wrangler's own login. Each file says which log
246
+ lines to look for.
247
+
248
+ **Resume.** Every step can be run again: `secret set` replaces, the
249
+ provider's console shows what exists, `everyapp setup` and `everyapp access
250
+ on` change only what differs. When a run was interrupted, or the person
251
+ comes back later, start from the Verify section, keep what is done, and
252
+ continue at the first step that isn't.
253
+
254
+ **Remove.** `pnpm everyapp secret unset NAME --json` (it asks at a
255
+ terminal; with `--json` add `--yes` only when the person said so). Then
256
+ revoke the key on the provider's side: a key the Gateway no longer holds is
257
+ still a key.
258
+
259
+ **Never**, on your own: create a paid account or buy a plan, change a
260
+ setting on a Gateway that isn't this person's, run `secret unset` or
261
+ `access off` without being asked, or set a value you were given in the
262
+ chat (tell them it's in the chat now, and to rotate it on the provider's
263
+ side; then use one of the three ways).
@@ -0,0 +1,178 @@
1
+ # Cloudflare Access for the Gateway
2
+
3
+ Read [README.md](README.md) first: the rules every setup follows.
4
+
5
+ Cloudflare Access puts a second door in front of the Gateway and every app
6
+ host (`<app>.<app base domain>`: OpenSEO, Open CRM, OpenAnalytics,
7
+ agentic-inbox, any app made with `everyapp create`), at Cloudflare's edge:
8
+ nobody reaches them before Access has checked who they are, with their
9
+ Cloudflare account (members of the operator's Cloudflare account), a
10
+ one-time code emailed to them, or the team's identity provider. `pnpm
11
+ everyapp access on` does it all from the Cloudflare login: the Zero Trust
12
+ team (or the one Cloudflare's onboarding made, step 1), both sign-in
13
+ methods (Sign in with Cloudflare and the one-time PIN, each created when the
14
+ team lacks it), an allow policy with every member's and invitee's email, two
15
+ Access applications, and the three Gateway settings, in one run. At the end
16
+ the person has Access's login before the Gateway, MCP clients, the Gateway's
17
+ `/health` and each app's public routes still reachable without it, and
18
+ optionally no passwords. A person running `pnpm everyapp setup` at a
19
+ terminal is offered the same by setup itself ("Put Cloudflare Access in
20
+ front of your Gateway?", Yes by default); under `--json` nobody is, so the
21
+ offer is yours, after the owner exists. This
22
+ is the "outer ring" of `docs/runbooks/cloudflare-access.md`; the other way,
23
+ a "Continue with Company SSO" button, is
24
+ the runbook's "OIDC sign-in" and isn't covered here.
25
+
26
+ What it adds, and what it doesn't (`docs/threat-model.md`, "What
27
+ Cloudflare Access adds, and what it doesn't", and its credentials table):
28
+ every Gateway page and every app's private route is behind Access's login
29
+ before the Gateway sees the request; a stolen session cookie gets nothing
30
+ in a browser that hasn't passed Access as that person; taking someone out
31
+ of the policy ends their browser access when their Access session ends
32
+ (720h as `access on` sets it). It does not cover personal access tokens or
33
+ the OAuth tokens of connected AI apps (they use the bypassed paths and work
34
+ until revoked), apps' public routes (reachable by anyone), deploy authority
35
+ (the Cloudflare login, not a Gateway account). Taking
36
+ someone off is **Deactivate** in the Gateway's **Users** page (the threat
37
+ model's "Admin → Users"), which revokes their tokens and connections at once.
38
+
39
+ ## Settings
40
+
41
+ All three written by `everyapp access on`; nothing to type. Values can't be read back.
42
+
43
+ | NAME | required or optional | what it holds |
44
+ | ---------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
45
+ | `CLOUDFLARE_ACCESS_TEAM_DOMAIN` | required | the Zero Trust team's domain, `<team>.cloudflareaccess.com` |
46
+ | `CLOUDFLARE_ACCESS_AUD` | required | the AUD tag of the Access application on `<gateway host>` (several: comma separated); only its tokens sign people in |
47
+ | `CLOUDFLARE_ACCESS_APPS_AUD` | required | the AUD tag of the Access application on `*.<app base domain>`; its tokens only vouch for a session on an app host |
48
+ | `EMAIL_PASSWORD_SIGN_IN` | optional | `false` turns passwords off once an owner has linked Access; unset or `true` keeps them. Not a secret |
49
+ | `CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS` | optional | email domains whose people join on their first Access sign-in without an invitation, comma separated (`<your domain>`) |
50
+ | `CLOUDFLARE_ACCESS_OUTER_RING_AUTO_JOIN` | optional | `everyone`: anyone the Access policy lets in joins the owner's organization |
51
+
52
+ ## Costs and plans
53
+
54
+ As of October 2026: Cloudflare's Zero Trust Free plan costs nothing and
55
+ covers 50 users; once the seats are used, the next person to sign in is
56
+ blocked. Beyond that, pay-as-you-go is $7 per user per month. Every person
57
+ who signs in through Access uses a seat; the bypassed paths (MCP, OAuth,
58
+ public routes) and service tokens use none. Cloudflare's dashboard onboarding
59
+ asks for payment details even on the Free plan and says it won't charge. The
60
+ login page is `<team>.cloudflareaccess.com`: no domain of the person's own.
61
+ An Access session lasts up to one month (`720h`, the command's default); a
62
+ one-time PIN expires 10 minutes after it is sent.
63
+
64
+ ## Only a person can
65
+
66
+ - Choose the Zero Trust team name: `<name>.cloudflareaccess.com` is the sign-in page. Cloudflare lets it be changed later (Zero Trust → **Settings** → **Team name and domain**), but that frees the old name for anyone to claim and breaks the Gateway's `CLOUDFLARE_ACCESS_TEAM_DOMAIN` until `pnpm everyapp access on --write-settings --json` is rerun: better avoided.
67
+ - Approve the Cloudflare login in the browser (the Access permissions come with it).
68
+ - Sign in through Access: with their Cloudflare account, or the code that lands in their inbox; each existing member does it once, with their password.
69
+ - Decide who else gets in (`--email`, `--email-domain`), how long a sign-in lasts (`--session`), whether passwords go, and whether people join without an invitation (step 3).
70
+ - Ask for `access off` or `secret unset`; you never run them unasked.
71
+
72
+ ## Before you start
73
+
74
+ 1. `pnpm everyapp access --help` exits 0 (any `access` subcommand that answers, such as `access show --json`, proves the same); otherwise (`Unknown command access`, exit 251) the checkout predates 2026-10-24: `git pull` in the `every-app` checkout (the CLI rebuilds itself on its next run), then check again.
75
+ 2. `pnpm everyapp status --json`: `data.installed` and `data.claimed` true (claimed means an owner exists, so the policy has someone); `data.mode` is `self_hosted` (a hosted Gateway refuses `on`: `ACCESS_HOSTED`); `data.gatewayUrl` gives `<gateway host>`, `data.appBaseDomain` gives `<app base domain>`. `data.access` is `{ on, teamDomain }`: `on` true means the Worker holds all three settings, so Access is on already, go to Verify (`teamDomain` is what `access on` recorded; null when nothing was recorded yet); the human report shows the same as an `Access on|off` row, and no separate `secret list` is needed. `data.access` null means a hosted Gateway, no Worker yet, or that cf couldn't read the settings (then `data.fixes` has `ACCESS_UNKNOWN`, severity `warning`: run it again, or go on to `access show`). A `data.fixes` entry's `severity` says whether it is a problem (`error`, `warning`) or not (`info`, such as `GATEWAY_OUTDATED`; `settling`). Not installed or no owner: [gateway.md](gateway.md) first. `action_required cloudflare_login` with `waiting: true` (`status` logs in by itself): show `data.url` and `data.code`, keep reading; `pnpm everyapp login --json` is only for logging in ahead of time. Several accounts: add `--account-id <account id>` to every command.
76
+ 3. `pnpm everyapp access show --json` (changes nothing; on a hosted Gateway it answers `data.hosted` true instead of the error): `data.team` null means the account has no Zero Trust team (or never opened Zero Trust: both the same to `on`) and `on` will need `--team`. What `on` would change: `data.applications.gateway.inSync` and `data.applications.apps.inSync` (paths and public routes), both `signInInSync` (every provider of the team allowed), both `policyAttached`, `data.signIn` (`cloudflare` and `pin`: whether the team has each sign-in method; `openToAnyCloudflareAccount`: provider ids whose "Restrict to account members" is off, with the warning `ACCESS_IDP_ANY_CLOUDFLARE_ACCOUNT`, which the person turns on in Zero Trust → Settings → Authentication), `data.policy.notYet` (members and invitees the policy doesn't name yet), `data.settings` (`{ names, current }`: `names` the Access settings on the Worker; `current` `false` means they name an application that no longer exists, so nobody signs in; null when nothing is recorded yet or Access is off) and `data.desired.apps`. `data.linkPending` lists members who still have to sign in once; `data.fixes`, when present, is only the rerun, `pnpm everyapp access on` (there whenever Access is off, something is out of step, a sign-in method is missing, or `current` is `false`).
77
+ 4. Optional: `cf auth whoami`, whose `scopes` include Access ones such as `access:write`. cf 1.0.0-beta.14's login asks for them; an older login may lack them, which `on` reports as `ACCESS_PERMISSION_MISSING` on its first Access call, before it changes anything (If it fails says what to do), so skipping this check costs one failed run at most. Never repeat the output's email or ids.
78
+ 5. Apps with public routes (a tracking script, a webhook): `on` reads them from the deployed manifests. Deploy such apps first, or rerun `access on` after; deploys keep them in step by themselves. `pnpm everyapp status --json` lists each app's as `data.apps[].publicRoutes` (`{ path, methods }`; the PUBLIC column in the human report), so you can tell the person which routes Access will let through.
79
+ 6. Ask the person: whether everyone at `<your domain>` should pass Access, who else, and the session length. The team name is step 1's.
80
+
81
+ ## Steps
82
+
83
+ 1. **You, then the person:** the team name, when `data.team` was null in Before you start 3 (with a team already there is nothing to choose; `--team` is ignored, not even checked). The person chooses it: lowercase letters, digits and dashes (everyapp's check; Cloudflare only asks that it be unique), like the company's name. Say that `https://<name>.cloudflareaccess.com` is where everyone signs in, that names are shared across Cloudflare (a taken one fails), and that changing it later is possible but costly (Only a person can). Wait for the name; step 2 passes it with `--team`, and `on` creates the team, on an account that never opened Zero Trust too (no dashboard onboarding, plan pick or payment details first; checked live 2026-10-24).
84
+ 2. **You:** turn it on. Nothing to `secret set`: the three settings are README's "a command produced it" case, written by `on` itself in one new Worker version.
85
+ ```sh
86
+ pnpm everyapp access on --json
87
+ ```
88
+ Add `--team <name>` on step 1's first path (ignored once a team exists), `--email-domain <your domain>` so everyone at that domain passes Access (they still need an invitation to get an account; `data.next` then suggests `CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS`, step 3), `--email <address>` for one person, `--session 24h` for a daily code (default `720h`, 30 days; Cloudflare's maximum is one month; an existing application keeps its own unless given). Its order: the team, the two ways to sign in (Sign in with Cloudflare, restricted to the Cloudflare account's members, and the one-time PIN; each created when the team lacks it, any other provider kept), the policy "Every App gateway members", the application on `*.<app base domain>`, the one on `<gateway host>` (both allowing those two and never the team's other providers, listed in `data.signInMethods` with `allowed: false` for the person to allow in the dashboard if wanted; an application from before gains the two, `data.applications.<which>.signInAdded`), then the settings. It never removes anyone from the policy, never takes a sign-in method off an application, and leaves alone what it didn't make. In the `done` event read `data.team.domain` and `data.team.created`, `data.identityProviders.oneTimePinAdded` and `cloudflareAdded`, `data.signInMethods`, `data.policy.emails` and `data.policy.domains`, `data.applications.gateway.outcome` and `data.applications.apps.outcome` (`created`, `updated`, `unchanged`, `skipped`), `data.settings.outcome` (`set`, with `data.settings.reason` one of `missing`, `created`, `unrecorded`, `changed`, `asked`; or `kept` with `reason` null, said as "Gateway settings kept: they name these applications already (--write-settings writes them again)"), `data.signIn` (`url`, the Gateway; `pending`, who signs in once with their password; `first`, `both` for Access's chooser between Sign in with Cloudflare and the emailed code, else `pin` or `idp`), `data.team.globalSession` (the team's global session: null, or shorter than `--session`, has meant a daily code; say so and ask before `--team-session 720h`, next paragraph), and `data.next` (`command` and `note` pairs). Relay every `warning` (codes under If it fails). Don't print the AUD tags.
89
+ `data.team.globalSession` null is Access's default, 24h (`show` reports the same value as `data.team.sessionDuration`). Observed on one Gateway, not Cloudflare's documented rule: a `720h` application session still asked for the code daily until the team's global session was raised to match; Cloudflare only says the global session is usually set to equal or exceed the application's. When it is null or shorter than `--session`: tell the person they may be asked for the code daily, and that `pnpm everyapp access on --team-session 720h --json` raises it for every Access application in the account (it is how often people sign in at the identity provider); run it only when they say so.
90
+ 3. **You, only when the person asks:** the two membership settings. Neither is a secret, so a pipe is README's "a command produced it" way. `echo "<your domain>" | pnpm everyapp secret set CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS --json` (several: comma separated) lets people at that domain who pass Access join as members on their first sign-in, with no invitation; `echo everyone | pnpm everyapp secret set CLOUDFLARE_ACCESS_OUTER_RING_AUTO_JOIN --json` lets anyone the Access policy admits join (the policy becomes the member list). Live at once; `secret unset` undoes either, when asked.
91
+ 4. **Person:** each address in `data.signIn.pending` signs in once. Give them these four lines, the buttons' labels as written:
92
+ 1. Open `https://<gateway host>`; Access's page on `https://<team>.cloudflareaccess.com` opens, with **Sign in with Cloudflare** and the email field.
93
+ 2. A member of the Cloudflare account whose Cloudflare email is their Gateway address selects **Sign in with Cloudflare** (the warning `ACCESS_LOGIN_NOT_MEMBER` at step 2 means the person's isn't: they use the code). Everyone else types the email and selects **Send login code**, then pastes the code from the email (sender `noreply@notify.cloudflare.com`; it lasts 10 minutes) and selects **Sign in**.
94
+ 3. The Gateway's own sign-in page follows: sign in with the Gateway password as usual.
95
+ That links the two; from then on Access alone signs them in. The code page says "A code has been emailed to you" whether or not the address is in the policy (an address outside it gets no email at all: see If it fails). Someone with a pending invitation signs in through Access and joins without a password. Wait until everyone has done it.
96
+ 5. **You:** `pnpm everyapp access show --json`: `data.linkPending` is empty.
97
+ 6. **You:** only when the person asks, and only with `data.linkPending` empty, turn passwords off. Not a secret, so it may be piped:
98
+ ```sh
99
+ echo false | pnpm everyapp secret set EMAIL_PASSWORD_SIGN_IN --json
100
+ ```
101
+ The Gateway applies it only once an owner has linked Access, so it can't lock the owner out; a member who never linked can't sign in until it is `true` again: `echo true | pnpm everyapp secret set EMAIL_PASSWORD_SIGN_IN --json`.
102
+ 7. **You:** after every invitation later, `pnpm everyapp access on --json` again, so Access lets the new person in. Taking someone off is Deactivate in the Gateway (**Manage workspace → Users → ⋯ → Deactivate**; the threat model's "Admin → Users"), never the policy: the policy doesn't end their tokens or AI connections, and their Access session runs to its end.
103
+
104
+ ## Verify
105
+
106
+ 1. `pnpm everyapp status --json`: `data.access` is `{ "on": true, "teamDomain": "<team>.cloudflareaccess.com" }` (the human report's row: `Access on · everyone signs in through Cloudflare Access first`), and no `ACCESS_UNKNOWN` in `data.fixes` (an `APP_NEEDS_SETUP` there, like `data.apps[].setup`, says what an app still needs set: an integration's file, never this one, since none of these settings is an app's). This reads the Worker's secret names and the record `on` kept, so no separate `secret list` is needed; it asks nothing of Zero Trust, which is the next step's job. `on` true with `teamDomain` null: the settings predate the record (set by hand, or by a CLI from before the record existed); `pnpm everyapp access on --json` writes and records them (`data.settings.reason` `unrecorded`).
107
+ 2. `pnpm everyapp access show --json`: `data.on` true, `data.settings.names` lists all three and `data.settings.current` true, `data.signIn.cloudflare` and `data.signIn.pin` true with `openToAnyCloudflareAccount` empty, `data.applications.gateway.inSync` and `data.applications.apps.inSync` true, both `signInInSync` true, both `policyAttached` true, `data.policy.notYet` and `data.linkPending` empty, no `data.fixes`. Then `GET https://<gateway host>/health` answers `200 {"status":"ok"}` with no login: the uptime monitor's path.
108
+ 3. Smoke check, from your shell: a bypassed path answers the Gateway, the rest answers Access.
109
+ ```sh
110
+ curl -sS -o /dev/null -w '%{http_code} %{content_type} %{redirect_url}\n' https://<gateway host>/.well-known/oauth-authorization-server
111
+ curl -sS -o /dev/null -w '%{http_code} %{content_type} %{redirect_url}\n' https://<gateway host>/ | sed -E 's/kid=[^&]*/kid=…/'
112
+ ```
113
+ Expected: the first `200 application/json` (the Gateway's OAuth discovery document, let through by `/.well-known/*`; `/mcp` answers `401` with no HTML); the second `302` to `https://<team>.cloudflareaccess.com/cdn-cgi/access/login/...`, which is Access (its `kid=` is the gateway application's AUD tag; the `sed` keeps it off the screen). HTML or a redirect on the first means a bypass is missing: `pnpm everyapp access on --json`. `404 application/json` on the first means `MCP_OAUTH_ENABLED` is off, not a missing bypass: curl `https://<gateway host>/mcp` instead and expect `401 application/json`.
114
+ 4. **Person:** open `https://<gateway host>` in a private window (Access's email page first, then the Gateway signed in), then `https://<app>.<app base domain>`; ask them to tell you whether the app opened with no second sign-in page (it should; a sign-in page there is If it fails' "apps send people back to the sign-in page"). An AI app connected over MCP keeps working.
115
+ 5. Logs: Cloudflare dashboard, README's path: **Workers & Pages** → `every-app-gateway` → **Observability** (**Logs** on older accounts) → **Live** (cf 1.0.0-beta.14 has no `workers tail`; its `cf o11y telemetry query` needs a query body, so the dashboard is the practical path). `access.config_problem` names a malformed setting; `access.jwks_fetch_failed` means the team's keys couldn't be fetched: sign-ins keep working on the cached keys for up to 10 minutes after the first failure, then stop until a fetch succeeds. A line that keeps coming means `CLOUDFLARE_ACCESS_TEAM_DOMAIN` is wrong or Cloudflare is unreachable.
116
+
117
+ ## If it fails
118
+
119
+ | error or symptom | meaning | what to do |
120
+ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
+ | `ACCESS_PERMISSION_MISSING` | the Cloudflare login or token can't manage Access (raised only by the Access calls; a Workers or D1 refusal in the same run is cf's own auth error) | `cf auth logout`, then `pnpm everyapp login --json` (the person approves the link; cf asks for Access permissions), rerun. A token: add "Access: Apps and Policies (edit)" and "Access: Organizations, Identity Providers, and Groups (edit)" at https://dash.cloudflare.com/profile/api-tokens (its `data.url`) |
122
+ | `ACCESS_NOBODY_ALLOWED` | no member, no invitation, no `--email`: the policy would let nobody in | claim the Gateway ([gateway.md](gateway.md)), or `--email <address>` |
123
+ | `ACCESS_TEAM_REQUIRED` | no Zero Trust team and no terminal to ask at | ask the person; rerun with `--team <name>` (`data.suggested` is a suggestion) |
124
+ | `ACCESS_TEAM_INVALID` | not lowercase letters, digits and dashes (checked only when a team is being created; with one, `--team` is ignored) | another name |
125
+ | `ACCESS_TEAM_CREATE_FAILED` | Cloudflare refused the team; its message says why (a taken name is the usual one: team names are shared across Cloudflare) | another `--team`; a name is all the create needs (no Zero Trust onboarding, plan pick or payment details first) |
126
+ | a raw Cloudflare error `access.api.error.not_enabled`, "Access is not enabled" (API code 9999), from `show` or `on` | an account that never opened Zero Trust answers this to every Access call but `organization create`; `on` and `show` read it as "no team", and `on` creates one (checked live 2026-10-24), so seeing it raw means the create failed, or a later call refused after it | rerun `pnpm everyapp access on --json`; only if Cloudflare keeps answering it once the team exists, the person opens Zero Trust once in the dashboard, then rerun |
127
+ | `ACCESS_HOSTED` | a hosted Gateway ignores Access | not available there |
128
+ | `ACCESS_SESSION_INVALID`, `ACCESS_EMAIL_INVALID`, `ACCESS_DOMAIN_INVALID` | a flag's value | durations like `720h` or `2h30m`, a full address, the part after the `@` |
129
+ | warning `ACCESS_EMAIL_NOT_INVITED` | `--email` names someone without an account; the Gateway refuses them | invite them in **Manage workspace → Users** |
130
+ | warning `ACCESS_IDP_UNVERIFIED_EMAILS` | a generic OIDC or SAML provider lets people pick their own email; `on` leaves it off the Every App applications | nothing, unless the person wants it allowed anyway: then they allow it in the dashboard, knowing the risk (runbook: "Verified emails") |
131
+ | warning `ACCESS_ONE_APPLICATION` | one Access application covers both hosts | kept and updated; an app host's Access cookie is then a Gateway login too |
132
+ | warning `ACCESS_FOREIGN_DESTINATION` | the application also covers something that isn't Every App's | kept as found; `access off` won't delete that application |
133
+ | warning `ACCESS_INLINE_POLICIES` | the application has policies of its own, so the shared policy can't attach | the person makes them reusable in the dashboard, or manages it there |
134
+ | warning `ACCESS_ROUTE_NOT_EXPRESSIBLE`, `ACCESS_TOO_MANY_PUBLIC_ROUTES` | a public route stays behind Access: no Access pattern for it (one `*` per path segment), or past the 25 overrides per host the command allows (the runbook's limit; Cloudflare's account limits page lists none) | people signed in still reach it; merge routes under one prefix |
135
+ | warning `ACCESS_APP_OUTSIDE_BASE` | the app's host isn't one label under `<app base domain>`, so the apps' application on `*.<app base domain>` doesn't cover it: Access isn't in front of that app at all (the Gateway still signs people in there); merging routes changes nothing | the person moves the app under the base domain, or makes an Access application for that host by hand in **Zero Trust → Access controls → Applications** |
136
+ | warning `ACCESS_SYNC_FAILED` after a deploy or delete | the apps' application wasn't updated (the app's public routes need an Access login, or a deleted app's host stays listed) | its `data.rerun`: `pnpm everyapp access on --json` |
137
+ | warning `ACCESS_NOT_CHECKED` after a deploy or delete | Access couldn't be listed (a login without the Access permissions, such as a CI token, or another cf failure), so whether the Gateway is behind it is unknown and the app's public routes weren't synced; the message says "if the Gateway is behind Access" | its `data.rerun`, with a login that can manage Access: `pnpm everyapp access on --json` |
138
+ | `ACCESS_UNKNOWN` in `status --json`'s `data.fixes` (severity `warning`; `data.access` null) | cf couldn't read the Gateway Worker's secret names or the record | run `status` again; `pnpm everyapp access show --json` is the fuller check |
139
+ | Access's page: "That account does not have access." (a company identity provider login) | the email isn't in the policy | invite them, then `access on`; or `--email <address>` |
140
+ | the page said "A code has been emailed to you", no code arrives | with the one-time PIN Access sends nothing to an address outside the policy, or it was mistyped (check `data.policy.emails` in `access show --json`); else a mail gateway blocks or scans the mail | invite them and rerun `access on`, or `--email <address>`; otherwise allowlist `noreply@notify.cloudflare.com`, check spam, request a new code |
141
+ | Access's page: "This One-Time PIN has already been used" | a mail scanner followed the link first | request a new code |
142
+ | "This gateway signs people in through Cloudflare Access, and this request didn't come through it." | wrong AUD tag, or Access isn't in front of that host | `pnpm everyapp access on --write-settings --json` |
143
+ | "You don't have an account on this gateway yet." | Access knows them, the Gateway doesn't | invite them, or `CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS` |
144
+ | "You're signed in to Cloudflare Access as X, but this account is Y" | behind Access a browser is only the person Access says | **Not you? Sign out of Access** on the sign-in page, sign in as Y; or invite X and deactivate Y (the owner's account: runbook "Access says you're someone else") |
145
+ | apps send people back to the sign-in page | `CLOUDFLARE_ACCESS_APPS_AUD` doesn't match the apps' application | `pnpm everyapp access on --write-settings --json` |
146
+ | MCP clients get an HTML login page | a bypass is missing | `pnpm everyapp access on --json` |
147
+ | teammates can't log in at Access at all | the team's only provider is Sign in with Cloudflare, restricted to the account's members | `pnpm everyapp access on --json` adds the one-time PIN beside it, on the applications too |
148
+ | Access's forbidden page after **Sign in with Cloudflare** | the person's Cloudflare account email isn't a Gateway member (`ACCESS_LOGIN_NOT_MEMBER`) | use **Send login code** with the Gateway address, or invite the Cloudflare one |
149
+ | `ACCESS_IDP_ANY_CLOUDFLARE_ACCOUNT` from `on` or `show` | a Cloudflare provider's **Restrict to account members** is off; `on` leaves it off the Every App applications (the PIN alone signs people in) | the person turns it on: Zero Trust → Settings → Authentication → the provider; then `pnpm everyapp access on --json` allows it |
150
+ | the 51st person is blocked by Access | the Free plan's 50 seats | the person removes inactive users (Zero Trust → **Team & Resources → Users** → **Action → Remove users**; `cf zero-trust seats edit` does it from the shell, only when they ask), or pays |
151
+ | `EMAIL_PASSWORD_SIGN_IN=false` changes nothing | no owner has linked Access yet (no log line for it: `pnpm everyapp access show --json` still lists the owner in `data.linkPending`), or Access isn't configured (`access.config_problem` in the logs) | an owner signs in through Access once; or fix the settings |
152
+ | locked out: nobody passes Access | the policy or the one-time PIN is wrong at Cloudflare's edge; the Worker's settings can't let anyone in | when the person asks: `pnpm everyapp access off --json --yes` (deletes the two applications too); or the person fixes the policy, or deletes the applications, in **Zero Trust → Access controls → Applications** |
153
+ | locked out: Access passes, the Gateway refuses | the settings are wrong (an AUD tag, the team domain) | `pnpm everyapp access on --write-settings --json`; with no checkout at hand `cf workers secrets delete CLOUDFLARE_ACCESS_AUD --worker every-app-gateway` turns the outer ring off (Access still asks at the edge; passwords work again), then `pnpm everyapp setup` later |
154
+ | `CONFIRMATION_REQUIRED` | `access off` or `secret unset` under `--json` without `--yes` | add `--yes` only when the person said so |
155
+ | `SETTING_VALUE_INVALID` with `data.name` | `EMAIL_PASSWORD_SIGN_IN` (takes true/false, 1/0, on/off, yes/no) or `CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS` got a value the Gateway wouldn't read; nothing was written | pipe the value `data.fix` describes |
156
+ | `SECRET_VALUE_REQUIRED` | nothing was piped to `secret set` | one of README's three ways |
157
+
158
+ ## Rerun, rotate, remove
159
+
160
+ - Rerun: `pnpm everyapp access on --json` any time; it changes only what differs. Run it after inviting someone and after a change by hand. It records what it wrote in the Gateway's D1 (table `_everyapp_access`) and rewrites the three settings itself when one is missing, when it created an application, when nothing is recorded yet (settings written before the record existed: `data.settings.reason` says `unrecorded`), or when the applications it finds don't match the record (an application recreated in the dashboard has a new AUD tag: `changed`); `--write-settings` (`asked`) forces the rewrite when `show` still reports `data.settings.current` true.
161
+ - Rotate: nothing here is a secret; the team name can be changed in the dashboard but better not (Only a person can). To end every Access session: dashboard **Zero Trust → Access controls → Applications → Configure → Revoke existing tokens**, or, when the person asks, `cf zero-trust access applications revoke-tokens <id>` for both ids in `access show --json` (`data.applications.gateway.id`, `data.applications.apps.id`). One person: Deactivate in the Gateway (**Manage workspace → Users → ⋯ → Deactivate**), which also revokes their personal access tokens and AI connections, which Access never sees; their Access sessions alone: **Team & Resources → Users → Action → Revoke**, or `cf zero-trust access users revoke --email <address>` when asked.
162
+ - Remove: `pnpm everyapp access off --json` (asks; `--yes` only when the person said so; `--keep-applications` leaves Access asking while the Gateway stops requiring it). It removes the three settings first, so passwords sign people in again at once (`EMAIL_PASSWORD_SIGN_IN=false` is ignored without Access; set it to `true` anyway: `echo true | pnpm everyapp secret set EMAIL_PASSWORD_SIGN_IN --json`), then deletes the two applications. The team, the one-time PIN and the policy stay in the Cloudflare account; nothing to revoke elsewhere. Never delete the Zero Trust organization itself in the dashboard: Cloudflare reserves a deleted team's name forever, for every account.
163
+
164
+ ## Sources
165
+
166
+ - https://developers.cloudflare.com/cloudflare-one/identity/one-time-pin/ (the PIN page, "Send login code", the sender, 10 minutes)
167
+ - https://developers.cloudflare.com/cloudflare-one/setup/ (team name, plan, payment details on the Free plan, https://one.dash.cloudflare.com)
168
+ - https://developers.cloudflare.com/cloudflare-one/faq/getting-started-faq/ (the team name can be changed; the old name is freed; a deleted organization's name is reserved forever)
169
+ - https://developers.cloudflare.com/cloudflare-one/account-limits/ (Access limits: no row for path overrides)
170
+ - https://www.cloudflare.com/plans/sase-zero-trust/ (Free: 50 users; pay-as-you-go $7 per user per month)
171
+ - https://developers.cloudflare.com/cloudflare-one/identity/users/seat-management/ (seats, blocked beyond the limit, service tokens, removing users)
172
+ - https://developers.cloudflare.com/cloudflare-one/identity/users/session-management/ (sessions up to one month, 24h default, revoking tokens)
173
+ - https://developers.cloudflare.com/workers/observability/logs/real-time-logs/ (Workers & Pages → Logs → Live)
174
+ - https://dash.cloudflare.com/profile/api-tokens (where a token gets the Access permissions)
175
+ - https://developers.cloudflare.com/api/resources/zero_trust/subresources/organizations/methods/create/ (the call `on --team` makes; token permission "Access: Organizations, Identity Providers, and Groups Write")
176
+ - https://developers.cloudflare.com/learning-paths/zero-trust-web-access/initial-setup/create-zero-trust-org/ (Cloudflare's own Terraform path signs up for Zero Trust in the dashboard first)
177
+ - `docs/threat-model.md` ("What Cloudflare Access adds, and what it doesn't"; the credentials table's Cloudflare Access row) and `docs/threat-model-invariants.md` on main (PR #413)
178
+ - `docs/runbooks/cloudflare-access.md`, `packages/cli/src/commands/access/`, `packages/cli/src/lib/access/` (`record.ts`: the `_everyapp_access` record; `settings.ts`: what `status` reads), `packages/cli/src/lib/cf/access.ts` and `landing-page/content/docs/access.mdx` on main (PR #379, squash commit c0f916ad); `packages/cli/src/commands/status/` for `data.access`, the `Access` row and `fixes[].severity` (PR #400); checked 2026-10-24