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.
- package/LICENSE +21 -0
- package/README.md +6 -77
- package/bin/everyapp.js +235 -0
- package/dist/access-LGEN3ABJ.js +1 -0
- package/dist/activity-RSD4D66F.js +12 -0
- package/dist/apps-4RCFXT74.js +13 -0
- package/dist/chunk-23HJ5I2A.js +2 -0
- package/dist/chunk-2Y5Q6A5K.js +6 -0
- package/dist/chunk-4JRIS6S6.js +1 -0
- package/dist/chunk-5PTNYZK6.js +4 -0
- package/dist/chunk-6OGVO7QK.js +6 -0
- package/dist/chunk-6PGUCTJ6.js +11 -0
- package/dist/chunk-AGTCKFRB.js +22 -0
- package/dist/chunk-BHMRY53V.js +2 -0
- package/dist/chunk-CB4U7H2G.js +4 -0
- package/dist/chunk-CCWBPAKH.js +4 -0
- package/dist/chunk-CZTEDWZ2.js +22 -0
- package/dist/chunk-CZXHKPRZ.js +2 -0
- package/dist/chunk-E3EPK27P.js +10 -0
- package/dist/chunk-EGEWRFVY.js +1 -0
- package/dist/chunk-ELK7QZ2M.js +1 -0
- package/dist/chunk-FJJSSVET.js +33 -0
- package/dist/chunk-GKBPYVP3.js +25 -0
- package/dist/chunk-H6QQMY7B.js +18 -0
- package/dist/chunk-H7PDL3X5.js +1 -0
- package/dist/chunk-HOUHG236.js +20 -0
- package/dist/chunk-IFCPALVC.js +46 -0
- package/dist/chunk-IUOBIB5M.js +30 -0
- package/dist/chunk-J35DDCNU.js +1 -0
- package/dist/chunk-J455FAEN.js +7 -0
- package/dist/chunk-JYKRUS5C.js +1 -0
- package/dist/chunk-LXYMDZER.js +1 -0
- package/dist/chunk-LYNBZ7RV.js +57 -0
- package/dist/chunk-M2QQ2UD7.js +132 -0
- package/dist/chunk-N4E5YE4X.js +5 -0
- package/dist/chunk-N5GQLGEC.js +2 -0
- package/dist/chunk-NFU4YFXB.js +1 -0
- package/dist/chunk-OM2HDWVP.js +1 -0
- package/dist/chunk-ONLNJXVQ.js +32 -0
- package/dist/chunk-OP4DYXYI.js +2 -0
- package/dist/chunk-PNLU5U3P.js +1 -0
- package/dist/chunk-PS7S74DG.js +6 -0
- package/dist/chunk-RMF4YGD6.js +98 -0
- package/dist/chunk-T2GD7RHS.js +10 -0
- package/dist/chunk-TLRM3P7V.js +15 -0
- package/dist/chunk-W3JCQHHU.js +1 -0
- package/dist/chunk-WOVS3GV5.js +10 -0
- package/dist/chunk-YHJZMJYZ.js +1 -0
- package/dist/chunk-ZECBN24L.js +15 -0
- package/dist/chunk-ZEXVHB22.js +2 -0
- package/dist/chunk-ZFCUH5PK.js +7 -0
- package/dist/configure-F7UZCJUG.js +8 -0
- package/dist/create-2E5HVWYK.js +22 -0
- package/dist/db-HEQVTDTM.js +9 -0
- package/dist/delete-XCSIQIRS.js +3 -0
- package/dist/deploy-3QOZEZHC.js +1 -0
- package/dist/dev-JL3ZP5BT.js +86 -0
- package/dist/email-RLZFUFUN.js +10 -0
- package/dist/events-2V5VRBWP.js +15 -0
- package/dist/groups-HDG5DVFY.js +8 -0
- package/dist/index.js +97 -1
- package/dist/install-Z5FYXUYM.js +1 -0
- package/dist/login-KHJBC64Z.js +1 -0
- package/dist/mcp-EFDUKJ5V.js +1 -0
- package/dist/provider-EOI5PTFH.js +13 -0
- package/dist/release-74J5ZIL3.js +1 -0
- package/dist/restore-FFGYV7NO.js +3 -0
- package/dist/secret-7FKKTLHB.js +1 -0
- package/dist/setup-PAKRCAFO.js +1 -0
- package/dist/signin-AP3D4XM4.js +12 -0
- package/dist/status-RMWNHOEK.js +4 -0
- package/dist/teardown-E4JNKAZJ.js +22 -0
- package/dist/update-DWVZFNPS.js +1 -0
- package/dist/workspace-CJC5I66L.js +21 -0
- package/package.json +19 -16
- package/skills/build-every-app/SKILL.md +199 -0
- package/skills/build-every-app/patterns.md +140 -0
- package/skills/everyapp/SKILL.md +292 -0
- package/skills/everyapp/mcp.md +350 -0
- package/skills/everyapp/security-review.md +61 -0
- package/skills/everyapp/setup/README.md +274 -0
- package/skills/everyapp/setup/access.md +178 -0
- package/skills/everyapp/setup/context.md +117 -0
- package/skills/everyapp/setup/dataforseo.md +287 -0
- package/skills/everyapp/setup/email.md +366 -0
- package/skills/everyapp/setup/gateway.md +243 -0
- package/skills/everyapp/setup/google.md +360 -0
- package/skills/everyapp/setup/microsoft-365.md +281 -0
- package/skills/everyapp/setup/openai.md +318 -0
- package/skills/everyapp/setup/openrouter.md +309 -0
- package/skills/everyapp/setup/providers.md +1253 -0
- package/skills/everyapp/setup/remote-mcp.md +199 -0
- package/skills/everyapp/setup/signin-apple.md +178 -0
- package/skills/everyapp/setup/signin-cloudflare.md +148 -0
- package/skills/everyapp/setup/signin-github.md +139 -0
- package/skills/everyapp/setup/signin-google.md +178 -0
- package/skills/everyapp/setup/signin-microsoft.md +183 -0
- package/skills/everyapp/setup/signin-oidc.md +163 -0
- package/skills/everyapp/setup/slack.md +344 -0
- package/skills/everyapp/setup/workers-ai.md +138 -0
- package/bin/__everyapp_bash_complete.js +0 -12
- package/dist/bash-complete.js +0 -3
- package/dist/chunk-7BOQJFI5.js +0 -8
- package/dist/chunk-7YVJFS4A.js +0 -1
- package/dist/chunk-BIXKPCDH.js +0 -2
- package/dist/chunk-DLGPLHBH.js +0 -47
- package/dist/chunk-FD6K2YA7.js +0 -9
- package/dist/chunk-GAJEQYJH.js +0 -39
- package/dist/chunk-O2KDOP5R.js +0 -13
- package/dist/chunk-OI2MBGBG.js +0 -4
- package/dist/chunk-POSDLD3G.js +0 -8
- package/dist/chunk-RB5D2R45.js +0 -1
- package/dist/chunk-U4LIQ5VR.js +0 -1
- package/dist/chunk-VEYSNPDH.js +0 -3
- package/dist/chunk-VP6SKW44.js +0 -7
- package/dist/chunk-WO3NE7ZU.js +0 -7
- package/dist/chunk-XTO2VTPY.js +0 -6
- package/dist/chunk-YJ2DKIZS.js +0 -1
- package/dist/create-DYQ3NBWJ.js +0 -22
- package/dist/deploy-PYTCMDGV.js +0 -7
- package/dist/deploy-RMAHOA4Z.js +0 -55
- package/dist/dev-SAE237FS.js +0 -0
- package/dist/generateConfig-46Z3ERF3.js +0 -1
- package/dist/login-B6EF3SMX.js +0 -1
- package/dist/logout-XMKZBLNV.js +0 -1
- package/dist/remoteD1Shell-A67JMO3H.js +0 -6
- package/dist/setupLocal-TZJUW5ES.js +0 -6
- package/gateway/every-app-gateway-build.tar.gz +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
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
|
+
| Sign in with Google | [signin-google.md](signin-google.md) | `GOOGLE_SIGNIN_CLIENT_ID`, `GOOGLE_SIGNIN_CLIENT_SECRET` | the Gateway's sign-in page (Continue with Google) |
|
|
29
|
+
| Sign in with Microsoft | [signin-microsoft.md](signin-microsoft.md) | `MICROSOFT_SIGNIN_CLIENT_ID`, `MICROSOFT_SIGNIN_CLIENT_SECRET` (optional: `MICROSOFT_SIGNIN_TENANT`) | the Gateway's sign-in page (Continue with Microsoft) |
|
|
30
|
+
| Sign in with GitHub | [signin-github.md](signin-github.md) | `GITHUB_SIGNIN_CLIENT_ID`, `GITHUB_SIGNIN_CLIENT_SECRET` | the Gateway's sign-in page (Continue with GitHub) |
|
|
31
|
+
| Sign in with Apple | [signin-apple.md](signin-apple.md) | `APPLE_SIGNIN_CLIENT_ID`, `APPLE_SIGNIN_CLIENT_SECRET` | the Gateway's sign-in page (Continue with Apple) |
|
|
32
|
+
| Sign in with Cloudflare | [signin-cloudflare.md](signin-cloudflare.md) | `CLOUDFLARE_SIGNIN_CLIENT_ID`, `CLOUDFLARE_SIGNIN_CLIENT_SECRET` | the Gateway's sign-in page (Continue with Cloudflare) |
|
|
33
|
+
| Sign in with any OpenID Connect issuer (SSO) | [signin-oidc.md](signin-oidc.md) | `OIDC_SIGNIN_ISSUER`, `OIDC_SIGNIN_CLIENT_ID`, `OIDC_SIGNIN_CLIENT_SECRET` (optional: `OIDC_SIGNIN_LABEL`) | the Gateway's sign-in page (Continue with SSO, or its label) |
|
|
34
|
+
|
|
35
|
+
The six sign-in providers are set with `pnpm everyapp signin add <id>`
|
|
36
|
+
rather than `secret set`: in the person's terminal it asks for each
|
|
37
|
+
setting (the client secret at a hidden prompt), and unattended the secret
|
|
38
|
+
comes from `--client-secret-from-env VAR`, never the command line.
|
|
39
|
+
|
|
40
|
+
A provider of the built-in catalog without a file of its own above is a
|
|
41
|
+
section of [providers.md](providers.md) under its id
|
|
42
|
+
(`setup/providers.md#<id>`: its settings, where the key comes from, how
|
|
43
|
+
the Gateway sends it, a smoke request, cost and rate), generated from the
|
|
44
|
+
catalog by `pnpm -C packages/cli docs:update`; one with a file has a
|
|
45
|
+
section there too, which points at the file. A provider registered on
|
|
46
|
+
one Gateway (`pnpm everyapp provider add`, the agent docs' `providers`
|
|
47
|
+
page) has no file at all: `pnpm everyapp provider show <id>` prints its
|
|
48
|
+
whole spec, hint and smoke request included, and its setting is set like
|
|
49
|
+
any Gateway setting (`pnpm everyapp secret set <NAME>`, which first names
|
|
50
|
+
the provider and the hosts its key goes to).
|
|
51
|
+
|
|
52
|
+
Which integrations a person needs: the apps they deploy decide. The four
|
|
53
|
+
Every App apps (OpenSEO, OpenCRM, OpenAnalytics, Agentic Inbox) are
|
|
54
|
+
installed from their GitHub releases by [gateway.md](gateway.md)'s last
|
|
55
|
+
step, each unless the person skips it; the keys they then ask for are the
|
|
56
|
+
files below. From any
|
|
57
|
+
directory, `pnpm everyapp secret list <app id> --json` (the id as `status`
|
|
58
|
+
lists it in `data.apps[].id`; in the app's directory it can be left out)
|
|
59
|
+
reports the app's setup state as `data.app.setup`: `ready` (the app
|
|
60
|
+
opens), `unmet` (the settings keeping it closed), `settings` (every
|
|
61
|
+
setting, set or not), `groups` (each any-of group, `met` once one member
|
|
62
|
+
is set) and `command`, the one `secret set` that sets everything missing
|
|
63
|
+
(`{ command, note? }`, the note an any-of group's alternatives; null once
|
|
64
|
+
ready). A setting has `names` (all must be set), `holder` (`gateway`, or
|
|
65
|
+
`app` for the app's own secret), `need.kind` (`required`, `optional`, or
|
|
66
|
+
`anyOf` with its `group`), `set` (true, false, or null when the app
|
|
67
|
+
Worker's names were never recorded, which never blocks), `blocking`, and
|
|
68
|
+
when known `why`, `hint` (what the key is and where it comes from, from
|
|
69
|
+
the provider's entry), `docs` (where the key comes from) and `setup`: the file
|
|
70
|
+
here that sets it up, relative to the everyapp skill (`setup/dataforseo.md`;
|
|
71
|
+
in an app `.claude/skills/everyapp/setup/dataforseo.md`, in this checkout
|
|
72
|
+
`packages/cli/skills/everyapp/setup/dataforseo.md`). An unset optional
|
|
73
|
+
setting is in `settings` with `set: false`, never in `unmet`. The older
|
|
74
|
+
fields still hold: `data.gateway.needed` (each Gateway name with `required`
|
|
75
|
+
and `set`) and `data.gateway.missing` (the required ones unset); from the
|
|
76
|
+
checkout root with no app named, `data.apps[]` has every app's `id` and
|
|
77
|
+
`setup`. `pnpm everyapp status --json` carries the same per app as
|
|
78
|
+
`data.apps[].setup`, and a deployed app whose `ready` is false gets the
|
|
79
|
+
`data.fixes` entry `APP_NEEDS_SETUP` (a warning, `<app> needs setup`,
|
|
80
|
+
`run` the command above). Under each unmet setting that has a guide,
|
|
81
|
+
`status` (under "<app> needs setup"; the same lines are in
|
|
82
|
+
`data.fixes[].message`) and `secret list <app id>` (under its unmet settings)
|
|
83
|
+
print `Guide: .claude/skills/everyapp/setup/<name>.md`, once per guide, and
|
|
84
|
+
`secret list --json` names it as the setting's `setup`: that is how an agent lands on
|
|
85
|
+
one of these files. A provider registered on the Gateway has no file, so
|
|
86
|
+
its line is `Guide: pnpm everyapp provider show <id>` instead. On the Gateway's home such an app shows
|
|
87
|
+
"Needs setup" and opens its setup dialog instead of the app (a browser
|
|
88
|
+
visit to the app's own host lands in that dialog too; any other request
|
|
89
|
+
gets a 503 `app_needs_setup`), and its tools stay out of the Gateway's
|
|
90
|
+
`/mcp`. Set up what is unmet, in the order the person wants; an optional
|
|
91
|
+
setting when they want its feature. A setting nobody needs is simply not
|
|
92
|
+
offered to apps.
|
|
93
|
+
|
|
94
|
+
## The rules every setup follows
|
|
95
|
+
|
|
96
|
+
**Where you are.** Every command runs from the `every-app` checkout, as
|
|
97
|
+
`pnpm everyapp …` (never `npx`), with `--json`: one event per line, `done`
|
|
98
|
+
means success, exit 2 means an `action_required` for the person (show its
|
|
99
|
+
`message`, `data.url` and `data.code`; rerun `data.rerun` when they are
|
|
100
|
+
done), exit 1 an error (show it; its `data.fix` or `data.rerun` says what
|
|
101
|
+
to do). An event with `"waiting": true` is being waited for: keep reading.
|
|
102
|
+
A Gateway setting is set from any directory; an app's own secret (a name
|
|
103
|
+
its `everyapp.config.ts` lists under `secrets`) from the app's directory,
|
|
104
|
+
or from anywhere with `--app <id>` (against the manifest its last deploy
|
|
105
|
+
stored; `setup.command` carries the flag when it sets one), either way
|
|
106
|
+
after its first deploy. Every `cf …` in these files is the CLI's pinned
|
|
107
|
+
`cf` (1.0.0-beta.14, which `pnpm install` puts under `packages/cli`, not
|
|
108
|
+
on PATH): from the `every-app` folder, run it as `pnpm --filter everyapp
|
|
109
|
+
exec cf …` (the root's bare `pnpm exec cf` finds none). A bare `cf` only
|
|
110
|
+
when `cf --version` answers `1.0.0-beta.14` (a global install); never
|
|
111
|
+
install one.
|
|
112
|
+
|
|
113
|
+
**Deep references.** A command's flags and exit codes:
|
|
114
|
+
`pnpm everyapp <command> --help` (0 done, 1 error, 2 action required,
|
|
115
|
+
251/252 a bad command or its arguments). The agent docs:
|
|
116
|
+
https://everyapp.com/docs/agent/, each page also as Markdown at
|
|
117
|
+
`https://everyapp.com/docs/agent/<page>.md` (`setup`, `cli`, `json`,
|
|
118
|
+
`errors`, `troubleshooting`, `manifest`, `identity`, `secrets`, `dev`,
|
|
119
|
+
`email`, `mcp`, `deploy`, `providers`, `access`; an error code's fix is at
|
|
120
|
+
`errors.md#<code>`, the code lower-cased). They go live with the launch on
|
|
121
|
+
2026-10-26; until then each page is in the checkout at
|
|
122
|
+
`landing-page/content/agent/<page>.md`; `docs/runbooks/`,
|
|
123
|
+
`docs/threat-model.md` and `pnpm everyapp <command> --help` stay the
|
|
124
|
+
deeper reference. An error code these files don't
|
|
125
|
+
cover carries its own `data.fix` or `data.rerun`; it is raised in the
|
|
126
|
+
command's file under `packages/cli/src/commands/` or its library under
|
|
127
|
+
`packages/cli/src/lib/` (the error types: `packages/cli/src/lib/errors.ts`).
|
|
128
|
+
These files say what to do; the agent docs say why and what else is
|
|
129
|
+
possible.
|
|
130
|
+
|
|
131
|
+
**Not a Gateway setting.** A secret of the app's own repository (a GitHub
|
|
132
|
+
Actions secret for a workflow the person added; a new app ships none) is
|
|
133
|
+
set in that repository (`gh secret set NAME` there, the value from the
|
|
134
|
+
person's terminal), never with `everyapp secret set`, which refuses names
|
|
135
|
+
it doesn't know.
|
|
136
|
+
|
|
137
|
+
**Preconditions, checked, not assumed.** Before the provider's side, both,
|
|
138
|
+
in either order (the integration files lead with 2, which says whether
|
|
139
|
+
the file applies):
|
|
140
|
+
|
|
141
|
+
1. `pnpm everyapp status --json`: `data.installed` and `data.claimed` are
|
|
142
|
+
true, `data.gatewayUrl` is the Gateway (its host is `<gateway host>`
|
|
143
|
+
below; `data.appBaseDomain` is where apps live, `<app>.<app base domain>`).
|
|
144
|
+
Not installed, or no owner: [gateway.md](gateway.md) first. `status`,
|
|
145
|
+
`secret list` and `email show` all need the Cloudflare login and an
|
|
146
|
+
installed Gateway. Each logs in by itself: with nothing logged in it
|
|
147
|
+
emits `action_required cloudflare_login` with `waiting: true` (show
|
|
148
|
+
`data.url` and `data.code`, keep reading; approved within 5 minutes,
|
|
149
|
+
the run goes on, otherwise it ends with the error
|
|
150
|
+
`CLOUDFLARE_LOGIN_FAILED`, exit 1, whose `data.rerun` is `pnpm everyapp
|
|
151
|
+
login`, which also logs in ahead of time). With no Gateway, `status`
|
|
152
|
+
stops with `install_gateway` (exit 2; its `data.rerun` is the setup
|
|
153
|
+
command), while `secret list` and `email show` end with the error
|
|
154
|
+
`GATEWAY_NOT_FOUND` (exit 1); the fix for both is
|
|
155
|
+
[gateway.md](gateway.md). In the report, `data.access` is `{ on,
|
|
156
|
+
teamDomain }` when Cloudflare Access can be checked, else null (a hosted
|
|
157
|
+
Gateway, no Worker yet, or cf couldn't list the Worker's settings: then
|
|
158
|
+
`data.fixes` has `ACCESS_UNKNOWN`). Each `data.fixes` entry carries
|
|
159
|
+
`code`, `severity` (`error`, `warning`, `info` or `settling`), `message`,
|
|
160
|
+
`run` (null without a command), and when it has them `note` (what `run`
|
|
161
|
+
does) and `fix` (a remedy that isn't a command). Only `error` and
|
|
162
|
+
`warning` are something to fix; `info` (`GATEWAY_OUTDATED`: `pnpm
|
|
163
|
+
everyapp update` updates the Gateway now; leave it unless the
|
|
164
|
+
person asks) and `settling` need nothing from you.
|
|
165
|
+
2. `pnpm everyapp secret list <app id> --json`, from anywhere (the app's
|
|
166
|
+
directory is no longer required): this file applies when its setting
|
|
167
|
+
is in `data.app.setup.unmet` (each entry's `names`, `need.kind`,
|
|
168
|
+
`holder` and `setup` say what goes where and which file), or, for an
|
|
169
|
+
optional one, in `settings` with `set: false`; `data.app.setup.command`
|
|
170
|
+
is the one command that sets what is missing. For a setting no app
|
|
171
|
+
declares (the Gateway's own, Cloudflare Access's), from the checkout
|
|
172
|
+
root `pnpm everyapp secret list --json`: `data.gateway.names` is what
|
|
173
|
+
is set. Set already (`set: true`, or the name in `data.gateway.names`):
|
|
174
|
+
ask whether to replace it before doing anything on the provider's side;
|
|
175
|
+
a value can't be read back, so "replace" means a new key.
|
|
176
|
+
|
|
177
|
+
**The value never goes through the chat.** A key or client secret is never
|
|
178
|
+
asked for in the conversation, never shown, never put on a command line
|
|
179
|
+
(shell history, the process list), never written into a file you create in
|
|
180
|
+
the repository. `pnpm everyapp secret set NAME` reads the value from one of
|
|
181
|
+
three places, and your shell has no terminal, so it never prompts for you:
|
|
182
|
+
|
|
183
|
+
- **The person types it.** Tell them to open a second terminal in the
|
|
184
|
+
`every-app` folder and run `pnpm everyapp secret set NAME`; it asks for
|
|
185
|
+
the value with a hidden prompt (pasting works; nothing is echoed). Then
|
|
186
|
+
they tell you, and you verify. This is the default when the value is on a
|
|
187
|
+
dashboard the person is looking at.
|
|
188
|
+
- **A command produced it.** When a CLI or API call returns the key
|
|
189
|
+
(Microsoft's `az ad app credential reset`, Slack's `apps.manifest.create`,
|
|
190
|
+
a JSON file the console let the person download), pipe it straight in and
|
|
191
|
+
the value never appears anywhere: `… | jq -r '.client_secret' | pnpm everyapp secret set NAME --json`.
|
|
192
|
+
Never run the producing command on its own to look at the result first.
|
|
193
|
+
- **It is in an environment variable.** In their own terminal, in the
|
|
194
|
+
`every-app` folder, the person reads it in hidden (bash or zsh:
|
|
195
|
+
`read -rs NAME && export NAME`; fish: `read -s -x NAME`), pastes the
|
|
196
|
+
value and presses Enter (nothing is echoed; bash and zsh show no prompt
|
|
197
|
+
either), then runs `pnpm everyapp secret set NAME --from-env NAME --json`
|
|
198
|
+
there, and `unset NAME` (fish: `set -e NAME`) once the file's smoke
|
|
199
|
+
check is done. Never a typed `export NAME=…`: on a default macOS zsh and
|
|
200
|
+
on bash it lands in the shell history, leading space or not (only a
|
|
201
|
+
shell set up with `HIST_IGNORE_SPACE` or `HISTCONTROL=ignorespace` drops
|
|
202
|
+
it). A variable they exported before starting you: you run the same
|
|
203
|
+
`pnpm everyapp secret set NAME --from-env VAR --json`. Never `echo $VAR`,
|
|
204
|
+
and never ask them to paste the export line into the chat.
|
|
205
|
+
|
|
206
|
+
One value per call for a pipe or `--from-env`; several names at once only
|
|
207
|
+
at the hidden prompt. The change is live on the Gateway at once (one new
|
|
208
|
+
version of its Worker, no deploy). If `secret set` fails with
|
|
209
|
+
`SECRET_VALUE_REQUIRED`, nothing was piped: use one of the three ways above;
|
|
210
|
+
`SECRET_EMPTY` means the `--from-env` variable is empty or unset in the
|
|
211
|
+
shell that ran it: the hidden read again, in that same terminal.
|
|
212
|
+
`SETTING_VALUE_INVALID` means a plain setting (`EMAIL_FROM`,
|
|
213
|
+
`EMAIL_PROVIDER`, `EMAIL_PASSWORD_SIGN_IN`, a `CLOUDFLARE_ACCESS_*` or
|
|
214
|
+
`MCP_*` switch) got a value the Gateway wouldn't read: the message names the
|
|
215
|
+
setting and the value, `data.fix` says what it takes, and nothing was
|
|
216
|
+
written; pipe the right value. A plain setting's prompt at a terminal is
|
|
217
|
+
visible and says what it takes; a credential's stays hidden.
|
|
218
|
+
|
|
219
|
+
**Only a person can** sign in to a provider, accept its terms, add a card
|
|
220
|
+
or choose a plan, approve an OAuth consent, pick a Google Cloud project's
|
|
221
|
+
organization, or copy a secret off a screen. For each such step, say what
|
|
222
|
+
page to open (a direct URL where one exists), what to click, what to type,
|
|
223
|
+
and what they will see when it worked; then wait. Everything else (reading
|
|
224
|
+
status, creating what an API or CLI can create, setting the value from a
|
|
225
|
+
pipe, verifying) you do. With a browser tool you may do the clicking too,
|
|
226
|
+
but a secret shown on screen still goes in through the person's terminal
|
|
227
|
+
or a file they save, not through you.
|
|
228
|
+
|
|
229
|
+
**Verify, every time.** After `secret set`:
|
|
230
|
+
|
|
231
|
+
1. `pnpm everyapp secret list <app id> --json`: in `data.app.setup` the
|
|
232
|
+
setting has `set: true` and is out of `unmet` (`ready: true` and
|
|
233
|
+
`command: null` once nothing else is missing); the older fields agree
|
|
234
|
+
(the name in `data.gateway.names`, its `data.gateway.needed` entry
|
|
235
|
+
`set: true`, out of `data.gateway.missing`). For the app as a whole,
|
|
236
|
+
`pnpm everyapp status --json`: `data.apps[].setup.ready` is true for it
|
|
237
|
+
and `data.fixes` has no `APP_NEEDS_SETUP` for it (a `SETTINGS_UNKNOWN`
|
|
238
|
+
warning means cf couldn't list a Worker's secrets, so those rows read
|
|
239
|
+
as unknown: run it again rather than trust them). For a setting no app
|
|
240
|
+
declares, `data.gateway.names` alone.
|
|
241
|
+
2. The file's smoke check: a call that uses the value, made where the
|
|
242
|
+
value is (the person's terminal, with the variable they exported) or
|
|
243
|
+
through the app that uses it, and what its failure means.
|
|
244
|
+
3. The app: open it and use the feature, or `pnpm everyapp status --json`
|
|
245
|
+
for `data.fixes` when the file says the setting changes what status
|
|
246
|
+
checks. A Gateway refusal names the setting: `integration_not_configured`
|
|
247
|
+
and `ai_provider_not_configured` mean the Gateway doesn't hold it (set
|
|
248
|
+
it, or the name is wrong); the provider's own error means the value is
|
|
249
|
+
wrong or the account isn't ready (the file says which).
|
|
250
|
+
4. Logs, when a file's step sends you there: the Cloudflare dashboard,
|
|
251
|
+
**Workers & Pages** → `every-app-gateway` → **Observability** (**Logs**
|
|
252
|
+
on older accounts) → **Live** streams now; the stored Workers Logs are
|
|
253
|
+
on the same page (the Gateway deploys with them on). Which tab name an
|
|
254
|
+
account shows wasn't checked in a browser here (unverified). `cf`
|
|
255
|
+
1.0.0-beta.14 has no Worker tail; `npx wrangler tail every-app-gateway`
|
|
256
|
+
streams the same after wrangler's own login. Each file says which log
|
|
257
|
+
lines to look for.
|
|
258
|
+
|
|
259
|
+
**Resume.** Every step can be run again: `secret set` replaces, the
|
|
260
|
+
provider's console shows what exists, `everyapp setup` and `everyapp access
|
|
261
|
+
on` change only what differs. When a run was interrupted, or the person
|
|
262
|
+
comes back later, start from the Verify section, keep what is done, and
|
|
263
|
+
continue at the first step that isn't.
|
|
264
|
+
|
|
265
|
+
**Remove.** `pnpm everyapp secret unset NAME --json` (it asks at a
|
|
266
|
+
terminal; with `--json` add `--yes` only when the person said so). Then
|
|
267
|
+
revoke the key on the provider's side: a key the Gateway no longer holds is
|
|
268
|
+
still a key.
|
|
269
|
+
|
|
270
|
+
**Never**, on your own: create a paid account or buy a plan, change a
|
|
271
|
+
setting on a Gateway that isn't this person's, run `secret unset` or
|
|
272
|
+
`access off` without being asked, or set a value you were given in the
|
|
273
|
+
chat (tell them it's in the chat now, and to rotate it on the provider's
|
|
274
|
+
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
|