everyapp 0.2.2 → 0.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -77
  3. package/bin/everyapp.js +235 -0
  4. package/dist/access-LGEN3ABJ.js +1 -0
  5. package/dist/activity-RSD4D66F.js +12 -0
  6. package/dist/apps-4RCFXT74.js +13 -0
  7. package/dist/chunk-23HJ5I2A.js +2 -0
  8. package/dist/chunk-2Y5Q6A5K.js +6 -0
  9. package/dist/chunk-4JRIS6S6.js +1 -0
  10. package/dist/chunk-5PTNYZK6.js +4 -0
  11. package/dist/chunk-6OGVO7QK.js +6 -0
  12. package/dist/chunk-6PGUCTJ6.js +11 -0
  13. package/dist/chunk-AGTCKFRB.js +22 -0
  14. package/dist/chunk-BHMRY53V.js +2 -0
  15. package/dist/chunk-CB4U7H2G.js +4 -0
  16. package/dist/chunk-CCWBPAKH.js +4 -0
  17. package/dist/chunk-CZTEDWZ2.js +22 -0
  18. package/dist/chunk-CZXHKPRZ.js +2 -0
  19. package/dist/chunk-E3EPK27P.js +10 -0
  20. package/dist/chunk-EGEWRFVY.js +1 -0
  21. package/dist/chunk-ELK7QZ2M.js +1 -0
  22. package/dist/chunk-FJJSSVET.js +33 -0
  23. package/dist/chunk-GKBPYVP3.js +25 -0
  24. package/dist/chunk-H6QQMY7B.js +18 -0
  25. package/dist/chunk-H7PDL3X5.js +1 -0
  26. package/dist/chunk-HOUHG236.js +20 -0
  27. package/dist/chunk-IFCPALVC.js +46 -0
  28. package/dist/chunk-IUOBIB5M.js +30 -0
  29. package/dist/chunk-J35DDCNU.js +1 -0
  30. package/dist/chunk-J455FAEN.js +7 -0
  31. package/dist/chunk-JYKRUS5C.js +1 -0
  32. package/dist/chunk-LXYMDZER.js +1 -0
  33. package/dist/chunk-LYNBZ7RV.js +57 -0
  34. package/dist/chunk-M2QQ2UD7.js +132 -0
  35. package/dist/chunk-N4E5YE4X.js +5 -0
  36. package/dist/chunk-N5GQLGEC.js +2 -0
  37. package/dist/chunk-NFU4YFXB.js +1 -0
  38. package/dist/chunk-OM2HDWVP.js +1 -0
  39. package/dist/chunk-ONLNJXVQ.js +32 -0
  40. package/dist/chunk-OP4DYXYI.js +2 -0
  41. package/dist/chunk-PNLU5U3P.js +1 -0
  42. package/dist/chunk-PS7S74DG.js +6 -0
  43. package/dist/chunk-RMF4YGD6.js +98 -0
  44. package/dist/chunk-T2GD7RHS.js +10 -0
  45. package/dist/chunk-TLRM3P7V.js +15 -0
  46. package/dist/chunk-W3JCQHHU.js +1 -0
  47. package/dist/chunk-WOVS3GV5.js +10 -0
  48. package/dist/chunk-YHJZMJYZ.js +1 -0
  49. package/dist/chunk-ZECBN24L.js +15 -0
  50. package/dist/chunk-ZEXVHB22.js +2 -0
  51. package/dist/chunk-ZFCUH5PK.js +7 -0
  52. package/dist/configure-F7UZCJUG.js +8 -0
  53. package/dist/create-2E5HVWYK.js +22 -0
  54. package/dist/db-HEQVTDTM.js +9 -0
  55. package/dist/delete-XCSIQIRS.js +3 -0
  56. package/dist/deploy-3QOZEZHC.js +1 -0
  57. package/dist/dev-JL3ZP5BT.js +86 -0
  58. package/dist/email-RLZFUFUN.js +10 -0
  59. package/dist/events-2V5VRBWP.js +15 -0
  60. package/dist/groups-HDG5DVFY.js +8 -0
  61. package/dist/index.js +97 -1
  62. package/dist/install-Z5FYXUYM.js +1 -0
  63. package/dist/login-KHJBC64Z.js +1 -0
  64. package/dist/mcp-EFDUKJ5V.js +1 -0
  65. package/dist/provider-EOI5PTFH.js +13 -0
  66. package/dist/release-74J5ZIL3.js +1 -0
  67. package/dist/restore-FFGYV7NO.js +3 -0
  68. package/dist/secret-7FKKTLHB.js +1 -0
  69. package/dist/setup-PAKRCAFO.js +1 -0
  70. package/dist/signin-AP3D4XM4.js +12 -0
  71. package/dist/status-RMWNHOEK.js +4 -0
  72. package/dist/teardown-E4JNKAZJ.js +22 -0
  73. package/dist/update-DWVZFNPS.js +1 -0
  74. package/dist/workspace-CJC5I66L.js +21 -0
  75. package/package.json +19 -16
  76. package/skills/build-every-app/SKILL.md +199 -0
  77. package/skills/build-every-app/patterns.md +140 -0
  78. package/skills/everyapp/SKILL.md +292 -0
  79. package/skills/everyapp/mcp.md +350 -0
  80. package/skills/everyapp/security-review.md +61 -0
  81. package/skills/everyapp/setup/README.md +274 -0
  82. package/skills/everyapp/setup/access.md +178 -0
  83. package/skills/everyapp/setup/context.md +117 -0
  84. package/skills/everyapp/setup/dataforseo.md +287 -0
  85. package/skills/everyapp/setup/email.md +366 -0
  86. package/skills/everyapp/setup/gateway.md +243 -0
  87. package/skills/everyapp/setup/google.md +360 -0
  88. package/skills/everyapp/setup/microsoft-365.md +281 -0
  89. package/skills/everyapp/setup/openai.md +318 -0
  90. package/skills/everyapp/setup/openrouter.md +309 -0
  91. package/skills/everyapp/setup/providers.md +1253 -0
  92. package/skills/everyapp/setup/remote-mcp.md +199 -0
  93. package/skills/everyapp/setup/signin-apple.md +178 -0
  94. package/skills/everyapp/setup/signin-cloudflare.md +148 -0
  95. package/skills/everyapp/setup/signin-github.md +139 -0
  96. package/skills/everyapp/setup/signin-google.md +178 -0
  97. package/skills/everyapp/setup/signin-microsoft.md +183 -0
  98. package/skills/everyapp/setup/signin-oidc.md +163 -0
  99. package/skills/everyapp/setup/slack.md +344 -0
  100. package/skills/everyapp/setup/workers-ai.md +138 -0
  101. package/bin/__everyapp_bash_complete.js +0 -12
  102. package/dist/bash-complete.js +0 -3
  103. package/dist/chunk-7BOQJFI5.js +0 -8
  104. package/dist/chunk-7YVJFS4A.js +0 -1
  105. package/dist/chunk-BIXKPCDH.js +0 -2
  106. package/dist/chunk-DLGPLHBH.js +0 -47
  107. package/dist/chunk-FD6K2YA7.js +0 -9
  108. package/dist/chunk-GAJEQYJH.js +0 -39
  109. package/dist/chunk-O2KDOP5R.js +0 -13
  110. package/dist/chunk-OI2MBGBG.js +0 -4
  111. package/dist/chunk-POSDLD3G.js +0 -8
  112. package/dist/chunk-RB5D2R45.js +0 -1
  113. package/dist/chunk-U4LIQ5VR.js +0 -1
  114. package/dist/chunk-VEYSNPDH.js +0 -3
  115. package/dist/chunk-VP6SKW44.js +0 -7
  116. package/dist/chunk-WO3NE7ZU.js +0 -7
  117. package/dist/chunk-XTO2VTPY.js +0 -6
  118. package/dist/chunk-YJ2DKIZS.js +0 -1
  119. package/dist/create-DYQ3NBWJ.js +0 -22
  120. package/dist/deploy-PYTCMDGV.js +0 -7
  121. package/dist/deploy-RMAHOA4Z.js +0 -55
  122. package/dist/dev-SAE237FS.js +0 -0
  123. package/dist/generateConfig-46Z3ERF3.js +0 -1
  124. package/dist/login-B6EF3SMX.js +0 -1
  125. package/dist/logout-XMKZBLNV.js +0 -1
  126. package/dist/remoteD1Shell-A67JMO3H.js +0 -6
  127. package/dist/setupLocal-TZJUW5ES.js +0 -6
  128. package/gateway/every-app-gateway-build.tar.gz +0 -0
@@ -0,0 +1,199 @@
1
+ # A remote MCP server as an MCP app
2
+
3
+ Any remote MCP server that speaks Streamable HTTP (a docs search, a
4
+ company's API, a SaaS tool's MCP endpoint) can become an MCP app of the
5
+ Gateway's: `pnpm everyapp mcp add <url>` registers it, the Gateway serves its
6
+ tools on its own `/mcp` to every member it is granted to as `<id>_<tool>`,
7
+ holds the server's credential itself (a Gateway setting, never sent to an AI
8
+ client), sends it only to the server's origin, and attributes, audits and
9
+ counts every call like an app's. A read-only connection sees only the tools
10
+ whose risk is `read`. At the end the person has the app on, its credential
11
+ set and pinned to the server's origin, the people who need it granted, and
12
+ one tool call through the Gateway's `/mcp` that worked.
13
+
14
+ ## Settings
15
+
16
+ | NAME | required or optional | what it holds |
17
+ | ------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
+ | `<ID>_MCP_TOKEN` (the app id upper-cased, `-` as `_`; `--secret` picks another name ending in `_MCP_TOKEN`) | required, unless the server takes no credential (`--auth none`) | What the server's docs call its API key, token or password. `--auth bearer` sends it as `Authorization: Bearer <value>`; `--auth header --header X-Api-Key` as `X-Api-Key: <value>`; `--auth basic` takes `user:password` and sends HTTP Basic. |
19
+
20
+ The name is a Gateway setting like any provider key, set with
21
+ `pnpm everyapp secret set <NAME>` (any name ending in `_MCP_TOKEN` goes to the
22
+ Gateway; nothing else is accepted for it). When it is set, the CLI pins it to
23
+ the origin of the MCP app that declares it (`https://treg.to` for
24
+ `https://treg.to/mcp/`): the Gateway sends a credential only to the origin it
25
+ was set for. A server re-pointed or re-added at another address needs its
26
+ credential set again; only the operator (the Cloudflare login) can, so an
27
+ owner or admin who changes an app in the Gateway never moves a credential.
28
+ `mcp list` and `mcp show` say "needs setup" until it is set and pinned.
29
+
30
+ `--auth oauth` is accepted and recorded, but the Gateway doesn't sign in to
31
+ MCP servers with OAuth this week: such an app serves no tools ("OAuth: not
32
+ supported yet"). If an OAuth server also takes an API key, add it with
33
+ `--auth bearer` instead.
34
+
35
+ ## Costs and plans
36
+
37
+ Every App charges nothing for an MCP app. The server's provider charges what
38
+ it charges for its calls (its own plan and limits apply; read them on its
39
+ site). Every forwarded call is counted in the organization's usage as
40
+ `mcp:<id>`, and an owner or admin can cap an app's calls per month in Admin →
41
+ Usage (off by default): past the cap a tool answers a tool error naming it
42
+ until the next UTC month.
43
+
44
+ ## Only a person can
45
+
46
+ - Create an account with the server's provider and accept its terms, when it
47
+ has any.
48
+ - Create the API key or token on the provider's dashboard and type it at the
49
+ hidden prompt.
50
+ - Decide who gets the app beyond the owners and admins (Admin → Apps → Manage
51
+ access), and whether a tool the server says is safe should count as a write
52
+ (you suggest; they decide).
53
+ - Decide to replace a credential the Gateway already holds.
54
+
55
+ ## Before you start
56
+
57
+ 1. `pnpm everyapp status --json`: `data.installed` and `data.claimed` are
58
+ true; `data.gatewayUrl` is `https://<gateway host>`. Not installed, or no
59
+ owner: [gateway.md](gateway.md) first.
60
+ 2. The server's MCP address and how it authenticates, from its docs: an
61
+ `https://` URL (often ending in `/mcp`) that speaks Streamable HTTP. An old
62
+ HTTP+SSE-only endpoint (a separate `/sse` URL from 2024) isn't served. The
63
+ credential is usually an API key sent as a bearer token; some servers read
64
+ a header of their own (`X-Api-Key`); public servers take none. The
65
+ Gateway follows no redirects, so use the address the server answers at,
66
+ not one that redirects to it.
67
+ 3. `pnpm everyapp mcp list --json`: the server isn't added already (no entry
68
+ in `data.apps` with its `url`). The id is the domain's name, the label
69
+ before its suffix (`https://mcp.deepwiki.com/mcp` → `deepwiki`,
70
+ `https://treg.to/mcp/` → `treg`), or `--name <id>` (lowercase letters,
71
+ digits and hyphens, at most 32 characters); it is the tools' prefix, so
72
+ keep it short.
73
+
74
+ ## Steps
75
+
76
+ One step per message: give the step, wait for the words it ends with
77
+ ("done", "I have it"), then the next.
78
+
79
+ 1. **You:** add it. `pnpm everyapp mcp add <url> --auth bearer --json` (or
80
+ `--auth header --header <name>`, `--auth basic`; nothing for a public
81
+ server), with `--name <id>` when the host name makes a poor id and
82
+ `--risk read` for a server that only reads and doesn't say so in its tool
83
+ annotations (`--risk write` treats every tool as a write). The app is on
84
+ at once for the person running it (when they are a member), the owners
85
+ and the admins. The `done` event's `data.id`, `data.key` (null for
86
+ `--auth none`; else the credential's `name` and whether it is `set` and
87
+ `bound` to the URL's origin) and `data.probe` (what one look at the server
88
+ from this machine found, without the credential) say what happened.
89
+ `PROVIDER_KEY_UNSET` means the credential comes next.
90
+ `REMOTE_MCP_PROBE_FAILED` and `REMOTE_MCP_WANTS_OAUTH` are warnings, never
91
+ refusals: a 401 with `--auth none` means the server wants a credential
92
+ (`pnpm everyapp mcp remove <id>`, then the same add with `--auth bearer`), a
93
+ redirect means another address, and "Couldn't reach" may be this
94
+ machine's network only (the Gateway fetches from Cloudflare).
95
+ 2. **Person:** the credential. They create the key on the provider's
96
+ dashboard, then, in a second terminal in the `every-app` folder:
97
+ `pnpm everyapp secret set <NAME>` (the name from step 1, for example
98
+ `pnpm everyapp secret set TREG_MCP_TOKEN`) and paste it at the hidden
99
+ prompt (for `--auth basic`, `user:password`). It ends with
100
+ `Set <NAME> on the Gateway`. When the person runs `mcp add` themselves at
101
+ a terminal, it asks `Set <NAME> now?` right after adding and, on yes, shows
102
+ the same hidden prompt (`secret set`'s own); No leaves it for `secret set`.
103
+ The README's other two ways work as for any `secret set` (a pipe into
104
+ `pnpm everyapp secret set <NAME> --json`, or the hidden read then
105
+ `--from-env <VAR>`); `mcp add <url> --auth bearer --from-env <VAR> --json`
106
+ takes the variable at the add itself, read before anything is written.
107
+ Wait for "done".
108
+ 3. **Person:** who gets it. The owners, the admins and whoever added it have
109
+ it already. Anyone else: Admin → Apps → the app's row → **Manage access**.
110
+ A member sees its tools after granting, in a client connected to the
111
+ Gateway (the next step).
112
+ 4. **Person:** connect an AI client to the Gateway if theirs isn't yet:
113
+ `claude mcp add --transport http everyapp https://<gateway host>/mcp`,
114
+ then `/mcp` → **everyapp** → **Authenticate** in Claude Code (other clients:
115
+ the same URL; mcp.md beside this folder, "Connecting a person").
116
+
117
+ ## Verify
118
+
119
+ 1. `pnpm everyapp mcp show <id> --json`: `data.status` is `"on"` (not
120
+ `"needs setup"`), `data.key.set` and `data.key.bound` are true and
121
+ `data.key.boundOrigin` is the URL's origin (all three skipped for
122
+ `--auth none`: `data.key` is null). `data.snapshot` is null until the
123
+ Gateway lists the tools, on the first request to its `/mcp` after the add
124
+ (or within the hour). From the checkout root,
125
+ `pnpm everyapp secret list --json` agrees: the name is in
126
+ `data.gateway.names`, its `data.gateway.mcpCredentials` entry has
127
+ `set: true`, and the app has left `data.apps` (it is listed there only
128
+ while it needs setup, with `setup.command` the `secret set` that sets it
129
+ up).
130
+ 2. One tool call through the Gateway: in the person's connected client, ask
131
+ for something the server does ("use <id> to …", or "list the tools <id>
132
+ offers"); the client shows a call to an `<id>_<tool>` tool and its
133
+ answer. On a Gateway with many tools the client may list them compactly:
134
+ then it finds them with `everyapp_search_tools` (`{ "app": "<id>" }`) and
135
+ runs one through `everyapp_call_read`. Then
136
+ `pnpm everyapp mcp show <id> --json` again: `data.snapshot.tools` is the
137
+ count listed and `data.tools[]` has each with its `name`, `serverName`,
138
+ `risk` and `source` (`hint`: the server's annotation; `default`: no
139
+ annotation, counted as a write; `app`: the `--risk` default;
140
+ `override`: yours).
141
+ 3. Risks, with the person: a tool the server marks read-only is callable by
142
+ every connection; one without a hint is a write (hidden from read-only
143
+ connections); one marked destructive is hidden from every client until
144
+ approvals exist. Change one with
145
+ `pnpm everyapp mcp tool <id> <tool> --risk read|write|destructive --json`
146
+ (`--risk annotations` drops the override), or leave a tool out entirely
147
+ with `--off`; the Gateway lists the tools again on first use.
148
+ 4. Logs, when a call fails: README's path (**Workers & Pages** →
149
+ `every-app-gateway` → **Observability** → **Live**). The remote proxy logs
150
+ each failed upstream call as `mcp.remote.upstream_failed` with the status
151
+ (never the credential), and a first-use pin as
152
+ `mcp.remote.credential_bound` (names from the design,
153
+ docs/design/2026-10-mcp-by-url.md; not checked against a live Gateway
154
+ here, unverified).
155
+
156
+ ## If it fails
157
+
158
+ | error or symptom | meaning | what to do |
159
+ | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
160
+ | `SECRET_VALUE_REQUIRED` from `secret set --json` | Nothing was piped and there is no terminal to prompt in | One of the README's three ways: the person's own terminal, a pipe, or `--from-env` |
161
+ | `SECRET_EMPTY` | The `--from-env` variable is empty or not exported in that shell (from `mcp add --from-env`, nothing was added) | The hidden read (README's third way) in that same terminal first, then rerun |
162
+ | `REMOTE_MCP_URL_REFUSED` | Not `https://`, an IP address, the Gateway's own host or app domain, or a provider the Gateway holds a key for | The server's own MCP address, by host name, over https |
163
+ | `REMOTE_MCP_EXISTS` | That id is an MCP app already | `pnpm everyapp mcp show <id>`; another address is `mcp remove <id>` then `mcp add` again, or another `--name` |
164
+ | `REMOTE_MCP_SECRET_TAKEN` | Another MCP app at another origin uses that credential name | `--secret <OTHER>_MCP_TOKEN`: one credential name per origin |
165
+ | `REMOTE_MCP_INVALID` | A bad id, risk, header or credential name, or flags that say two things | As the message says |
166
+ | `PROVIDER_APP_UNKNOWN` | The id isn't an MCP app here | `pnpm everyapp mcp list` for the ids |
167
+ | `REMOTE_MCP_SECRET_BOUND_ELSEWHERE` from `mcp add`, or `needs setup` in `mcp list` or `mcp show` with the key set | The credential is pinned to another origin (`data.key.boundOrigin`): it was set for an earlier server | `pnpm everyapp secret set <NAME>` again (Steps 2) |
168
+ | `REMOTE_MCP_TOOL_NOT_LISTED` from `mcp tool` | The tool's name isn't in the last listing: the override was kept under that name, as the server's | `pnpm everyapp mcp show <id>` for both names, then `mcp tool` with the right one |
169
+ | `REMOTE_MCP_SECRET_UNDECLARED` from `secret set` | No MCP app declares that name (a typo, or set before `mcp add`) | Check the name with `mcp show <id>`; `mcp add` first |
170
+ | `REMOTE_MCP_WANTS_OAUTH` | The server wants OAuth, which the Gateway doesn't do yet | If it also takes an API key: `--auth bearer` and set the key; otherwise it can't be served this week |
171
+ | `REMOTE_MCP_GATEWAY_OUTDATED` | The Gateway predates MCP apps by URL | `pnpm everyapp update`, then Verify |
172
+ | `data.snapshot.status` `error`, `lastError` naming 401 | The server rejected the credential | A fresh key from the provider, `secret set` again, then `pnpm everyapp mcp refresh <id>` |
173
+ | `lastError` naming a redirect or a timeout | The address redirects, or the server is slow or down | The address it redirects to (remove, add again); a slow server: retry later |
174
+ | The tools changed on the server, not on the Gateway | The Gateway's snapshot is up to an hour old | `pnpm everyapp mcp refresh <id>` |
175
+ | A tool is missing for one person only | Their connection is read-only (write tools hidden), or they weren't granted the app | Admin → Apps → Manage access; a read & write connection |
176
+
177
+ ## Rerun, rotate, remove
178
+
179
+ - **Rerun.** `mcp add` refuses an id that exists (`REMOTE_MCP_EXISTS`), so a
180
+ rerun starts from Verify. `secret set` replaces; `mcp tool` and
181
+ `mcp refresh` can run again.
182
+ - **Rotate.** A new key on the provider's dashboard, then Steps 2 again: the
183
+ Gateway uses the new value on its next request. A value that went through
184
+ the chat is compromised: rotate it.
185
+ - **Off, and back on.** `pnpm everyapp mcp off <id> --json` stops serving it
186
+ and keeps everyone's access, tokens and the snapshot;
187
+ `pnpm everyapp mcp on <id> --json` serves it again at once.
188
+ - **Remove.** `pnpm everyapp mcp remove <id> --json`: a tombstone, with
189
+ everyone's access, its tokens and its place in AI connections gone (adding
190
+ it again starts fresh). The credential and its pin stay;
191
+ `pnpm everyapp secret unset <NAME> --json` removes both (it asks; with
192
+ `--json` add `--yes` only once the person said so). Then revoke the key on
193
+ the provider's side.
194
+
195
+ ## Sources
196
+
197
+ - docs/design/2026-10-mcp-by-url.md (the remote block, the credential family and its pin, the proxy, the defaults taken)
198
+ - In code: `packages/sdk/src/internal/remoteMcp.ts` (the URL, id, auth and risk rules: `remoteMcpUrlProblem`, `remoteMcpIdFromUrl`, `remoteToolRisk`), `packages/sdk/src/internal/gatewaySettings.ts` (`isRemoteMcpSecretName`: any name ending in `_MCP_TOKEN` is the Gateway's), `packages/sdk/src/internal/appSettings.ts` (the `remote-mcp` setting kind naming this file), `packages/perimeter/src/registry/remoteMcpApps.ts` (`planRemoteMcpAppAdd` and its refusals), `packages/cli/src/commands/provider/index.ts` (`mcp add`, `show`, `tool`, the probe's warnings), `packages/cli/src/lib/gateway/remoteMcpApps.ts` (the rows, the pin in `remote_mcp_credentials`), `packages/cli/src/commands/secret/index.ts` (the pin on `secret set`, dropped on `secret unset`), `apps/every-app-gateway/src/db/remote-mcp.schema.ts` (migration 0011)
199
+ - https://modelcontextprotocol.io/specification (Streamable HTTP transport, tool annotations `readOnlyHint` and `destructiveHint`, authorization with `resource_metadata`)
@@ -0,0 +1,178 @@
1
+ # Sign in with Apple on the Gateway
2
+
3
+ Read [README.md](README.md) first: the rules every setup follows.
4
+
5
+ Puts **Continue with Apple** on the Gateway's sign-in page, beside the
6
+ password. At the end the person has an App ID, a Services ID and a Sign in
7
+ with Apple key in their Apple Developer account, the Gateway holds the
8
+ Services ID and a client secret signed with that key, and they have signed
9
+ in with Apple once. `pnpm everyapp signin show apple --json` names this
10
+ file as `setup`, and the CLI prints
11
+ `Guide: .claude/skills/everyapp/setup/signin-apple.md`.
12
+
13
+ Who gets in doesn't change: Apple only says who someone is. A member signs
14
+ in; someone with a pending invitation for that exact address joins with
15
+ the invited role; everyone else is refused ("You don't have an account on
16
+ this gateway.") and nothing is created. Someone who picks **Hide My
17
+ Email** on Apple's sheet arrives as a `@privaterelay.appleid.com` address
18
+ that no invitation names: ask them to pick **Share My Email**, or invite
19
+ the relay address. The rules: `docs/runbooks/sign-in-providers.md`.
20
+
21
+ Apple differs from the others twice. The client secret is not copied from
22
+ a page: it is a JWT you sign with the key, valid six months at most, so
23
+ it must be generated again before then or sign-in stops. And Apple returns
24
+ to the Gateway with a POST; behind Cloudflare Access, `everyapp access on`
25
+ leaves `/api/auth/callback/apple` open for it (Verify 4).
26
+
27
+ ## Settings
28
+
29
+ | Name | Required | Flag | What it holds |
30
+ | ---------------------------- | -------- | -------------------------------- | ----------------------------------------------------------------------------- |
31
+ | `APPLE_SIGNIN_CLIENT_ID` | required | `--client-id <id>` | the Services ID's identifier (`com.example.gateway`); not a secret |
32
+ | `APPLE_SIGNIN_CLIENT_SECRET` | required | `--client-secret-from-env <VAR>` | the ES256 JWT from step 5; expires within six months |
33
+
34
+ Scopes asked for: `email name`. The email is the ID token's `email`,
35
+ counted only when `email_verified` is true.
36
+
37
+ ## Costs and plans
38
+
39
+ A paid Apple Developer Program membership: US$99 a year (Apple's
40
+ fee-waiver page names the 99 USD fee; local prices vary), as an individual
41
+ or an organization; enrolling an organization needs a D-U-N-S number and
42
+ can take days. Sign in with Apple itself costs nothing more. Only the
43
+ Account Holder or an Admin of the team can set up the identifiers.
44
+
45
+ ## Only a person can
46
+
47
+ - Enroll in the Apple Developer Program and pay.
48
+ - Create the App ID, the Services ID and the key in Certificates,
49
+ Identifiers & Profiles, and download the key's `.p8` file (once only).
50
+ - Keep the `.p8` file safe: it is the only way to make the next secret.
51
+ - Sign in with Apple to check it.
52
+
53
+ ## Before you start
54
+
55
+ 1. `pnpm everyapp status --json`: `data.installed` and `data.claimed` true;
56
+ `data.gatewayUrl` gives `<gateway host>`. Not installed or no owner:
57
+ [gateway.md](gateway.md) first. `data.access.on` true: see Verify 4.
58
+ 2. `pnpm everyapp signin show apple --json` (changes nothing; `Unknown
59
+ command signin`: `git pull` in the `every-app` folder). `data.status`
60
+ `off`: this file applies; `on`: it works, and only the secret may need
61
+ renewing (Rotate). `data.callbackUrl` is the Return URL,
62
+ `https://<gateway host>/api/auth/callback/apple`.
63
+ 3. Ask whether they have a paid Apple Developer membership, and their
64
+ Team ID (top right of https://developer.apple.com/account, or
65
+ Membership details: ten characters). The Team ID is not a secret.
66
+
67
+ ## Steps
68
+
69
+ 1. **You:** give the person `<gateway host>` and `data.callbackUrl`.
70
+ 2. **Person:** the App ID. https://developer.apple.com/account/resources/identifiers/list
71
+ → **+** → **App IDs** → **Continue** → **App** → **Continue** →
72
+ Description (`Every App`), Bundle ID **Explicit** (`com.<their domain
73
+ reversed>.everyapp`) → tick **Sign in with Apple** (as a primary App
74
+ ID) → **Continue** → **Register**.
75
+ 3. **Person:** the Services ID, which is the client id. Same page → **+** →
76
+ **Services IDs** → **Continue** → Description (people see it, for
77
+ example `Every App`), Identifier (`com.<their domain reversed>.gateway`;
78
+ not the App ID's) → **Continue** → **Register**. Open it from the list
79
+ (the filter top right switches to **Services IDs**) → tick **Sign in
80
+ with Apple** → **Configure** → Primary App ID: step 2's → the domain
81
+ `<gateway host>` and the return URL `data.callbackUrl` (Apple's help
82
+ calls the field **Website URLs**, a comma-separated list; some screens
83
+ show **Domains and Subdomains** and **Return URLs** apart) → **Done** →
84
+ **Continue** → **Save**. No file to upload to the domain.
85
+ 4. **Person:** the key. https://developer.apple.com/account/resources/authkeys/list
86
+ → **+** → Key Name (`Every App sign-in`) → tick **Sign in with Apple**
87
+ → **Configure** → step 2's App ID → **Save** → **Continue** →
88
+ **Register** → **Download** (one chance: the file is
89
+ `AuthKey_<Key ID>.p8`, in Downloads) → **Done**. The Key ID is in the
90
+ file name and on the key's page. They tell you the Key ID, the Services
91
+ ID, the Team ID and the file's full path (none of these is a secret;
92
+ the file's contents are, and nobody opens it). Labels in this step are
93
+ from Apple's general key steps plus the runbook (unverified on Apple's
94
+ Sign in with Apple key page, which was missing when checked).
95
+ 5. **You:** make the secret and set both at once, from the `every-app`
96
+ folder, in bash or zsh. The JWT goes straight into an environment
97
+ variable of the `signin add` process: it never appears on screen, on a
98
+ command line or in history. Never run the inner command on its own (it
99
+ prints the JWT) and never open the `.p8`. The key path must be absolute
100
+ (the command runs inside `packages/sdk`, which is where `jose` resolves).
101
+ ```sh
102
+ APPLE_SIGNIN_CLIENT_SECRET="$(APPLE_KEY_FILE=/Users/<them>/Downloads/AuthKey_<Key ID>.p8 APPLE_KEY_ID=<Key ID> APPLE_TEAM_ID=<Team ID> APPLE_SERVICES_ID=<Services ID> pnpm --filter @every-app/sdk exec node --input-type=module -e "import { importPKCS8, SignJWT } from 'jose'; import { readFileSync } from 'node:fs'; const e = process.env; const key = await importPKCS8(readFileSync(e.APPLE_KEY_FILE, 'utf8'), 'ES256'); process.stdout.write(await new SignJWT({}).setProtectedHeader({ alg: 'ES256', kid: e.APPLE_KEY_ID }).setIssuer(e.APPLE_TEAM_ID).setSubject(e.APPLE_SERVICES_ID).setAudience('https://appleid.apple.com').setIssuedAt().setExpirationTime('180d').sign(key))")" pnpm everyapp signin add apple --client-id <Services ID> --client-secret-from-env APPLE_SIGNIN_CLIENT_SECRET --json
103
+ ```
104
+ The JWT: header `alg` ES256, `kid` the Key ID; claims `iss` the Team ID,
105
+ `sub` the Services ID, `aud` `https://appleid.apple.com`, `iat` now,
106
+ `exp` 180 days out (Apple's limit is 15777000 seconds, about six
107
+ months). A wrong path or key fails inside `$(…)`, the variable is
108
+ empty and `signin add` stops with `SECRET_EMPTY`: nothing is written.
109
+ The done event's `data.status` is `"on"`.
110
+ 6. **You:** tell the person to set a reminder for five months from today
111
+ (Rotate), and to move the `.p8` out of Downloads into a password
112
+ manager or another safe place outside the repository: Apple never lets
113
+ it be downloaded again.
114
+
115
+ ## Verify
116
+
117
+ 1. `pnpm everyapp signin list --json`: the `apple` entry has `status:
118
+ "on"` and `missing` empty.
119
+ 2. The person's Apple ID address must be a member's or have a pending
120
+ invitation: `pnpm everyapp workspace invite <email>` for someone new.
121
+ 3. **Person:** private window, `https://<gateway host>/sign-in` →
122
+ **Continue with Apple** → sign in with the Apple ID → **Share My
123
+ Email** → **Continue** → the Gateway, signed in.
124
+ 4. Behind Cloudflare Access: the Apple address must be the one Access
125
+ knows the person by, and Access must let Apple's POST through. Run
126
+ `pnpm everyapp access show --json`: `data.applications.gateway.inSync`
127
+ false means the ring predates the Apple bypass; `pnpm everyapp access on --json`
128
+ adds it (also after `pnpm everyapp update`). A ring made by hand without
129
+ that bypass blocks Apple sign-in.
130
+ 5. Logs (README's Verify 4): `signin.config_problem` for a malformed or
131
+ missing setting.
132
+
133
+ ## If it fails
134
+
135
+ | error or symptom | meaning | what to do |
136
+ | --- | --- | --- |
137
+ | no **Continue with Apple** button | a setting missing or malformed, or the version isn't live yet | `pnpm everyapp signin list --json`; logs' `signin.config_problem` |
138
+ | Apple's page says the redirect URL or client is invalid (wording unverified) | the Return URL isn't exactly `data.callbackUrl`, or the client id isn't the Services ID | step 3; the client id is the Services ID's identifier, not the App ID |
139
+ | `invalid_client` after Apple's sheet | the JWT is expired, signed with another key, or names another Team ID, Key ID or Services ID | step 5 again with the right values (Rotate) |
140
+ | works for months, then stops | the JWT expired | Rotate |
141
+ | Access's login page, or a 403, right after Apple's sheet | the outer ring blocks Apple's POST to `/api/auth/callback/apple` | `pnpm everyapp access on --json` |
142
+ | "You don't have an account on this gateway." | the address (often a `@privaterelay.appleid.com` one) isn't a member's or invited | **Share My Email**, or invite the address Apple returns |
143
+ | `SECRET_EMPTY` | the JWT command failed (path not absolute, wrong file, a typo in an id) | run step 5 again with corrected values; its stderr says what failed |
144
+ | `SIGNIN_VALUE_REQUIRED` | unattended, and the client id is neither given nor set | `data.rerun`, with `--client-id <Services ID>` |
145
+
146
+ ## Rerun, rotate, remove
147
+
148
+ - Rerun: step 5 any time; values given replace, settings not given are
149
+ kept.
150
+ - Rotate, before six months pass: the same command with the same `.p8`,
151
+ leaving out `--client-id` (it is kept), or pipe the JWT into the one
152
+ setting (same environment variables and `node … -e "…"` as step 5):
153
+ `APPLE_KEY_FILE=… pnpm --filter @every-app/sdk exec node --input-type=module -e "…" | pnpm everyapp secret set APPLE_SIGNIN_CLIENT_SECRET --json`.
154
+ A lost `.p8`: the person makes a new key (step 4), revokes the old one,
155
+ and step 5 runs with the new Key ID.
156
+ - Remove, only when asked: `pnpm everyapp signin remove apple --json --yes`
157
+ (`--yes` only when they said so). Accounts made with it stay. Then the
158
+ person revokes the key and deletes the Services ID in Apple's console.
159
+
160
+ ## Sources
161
+
162
+ - https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
163
+ (the JWT's header and claims; at most 15777000 seconds; the page didn't
164
+ render when fetched, so the claim list is checked against the runbook
165
+ and the test below)
166
+ - https://developer.apple.com/help/account/capabilities/configure-sign-in-with-apple-for-the-web
167
+ (Services ID → Sign in with Apple → Configure → primary App ID → Website
168
+ URLs → Done → Continue → Save; no domain file)
169
+ - https://developer.apple.com/help/account/capabilities/about-sign-in-with-apple
170
+ (primary App ID; Account Holder or Admin)
171
+ - https://developer.apple.com/help/account/keys/create-a-private-key
172
+ (Keys → +; download once, `.p8` in Downloads)
173
+ - https://developer.apple.com/support/fee-waiver (the 99 USD yearly fee)
174
+ - Step 5's command was run on 2026-10-24 against a throwaway P-256 key:
175
+ the JWT verified with `jose` (ES256, `kid`, `iss`, `sub`, `aud`, `exp -
176
+ iat` = 15552000), and pnpm prints nothing else on stdout.
177
+ - `docs/runbooks/sign-in-providers.md` ("Apple") and
178
+ `packages/sdk/src/internal/signInProviders.ts`
@@ -0,0 +1,148 @@
1
+ # Sign in with Cloudflare on the Gateway
2
+
3
+ Read [README.md](README.md) first: the rules every setup follows.
4
+
5
+ Puts **Continue with Cloudflare** on the Gateway's sign-in page, beside
6
+ the password, for the members of the person's Cloudflare account. At the
7
+ end the person has a private OAuth client in that account, its two
8
+ settings on the Gateway, and has signed in with Cloudflare once.
9
+ `pnpm everyapp signin show cloudflare --json` names this file as `setup`,
10
+ and the CLI prints `Guide: .claude/skills/everyapp/setup/signin-cloudflare.md`.
11
+
12
+ Who gets in doesn't change: Cloudflare only says who someone is. A member
13
+ of the Gateway signs in; someone with a pending invitation for that exact
14
+ address joins with the invited role; everyone else is refused ("You don't
15
+ have an account on this gateway.") and nothing is created. An existing
16
+ password account connects Cloudflare once, by signing in with the password
17
+ when the page asks. The rules: `docs/runbooks/sign-in-providers.md`.
18
+
19
+ **Keep the client private** (the default). Cloudflare's `/user` answer has
20
+ no "verified" flag; the address is trusted only because a private client
21
+ can be authorized by nobody but the Cloudflare account's members, who got
22
+ in by accepting an invitation sent to that address. Making a client public
23
+ is permanent and would let any Cloudflare user present an address nobody
24
+ checked. Everyone outside the Cloudflare account uses another provider, a
25
+ password, or Access's one-time PIN.
26
+
27
+ Not the same as Access's "Sign in with Cloudflare" ([access.md](access.md)):
28
+ that one is a login *in front of* the Gateway, at Cloudflare's edge, for
29
+ every page and app. This one is a button *on* the Gateway's sign-in page,
30
+ like Google's. Both can be on; with Access on, this button is rarely needed.
31
+
32
+ ## Settings
33
+
34
+ | Name | Required | Flag | What it holds |
35
+ | --------------------------------- | -------- | -------------------------------- | ------------------------------------------------ |
36
+ | `CLOUDFLARE_SIGNIN_CLIENT_ID` | required | `--client-id <id>` | the OAuth client's Client ID; not a secret |
37
+ | `CLOUDFLARE_SIGNIN_CLIENT_SECRET` | required | `--client-secret-from-env <VAR>` | the OAuth client's Client Secret, shown once |
38
+
39
+ Scope asked for: `user-details.read` (User Details Read), nothing an app
40
+ could act with. The email is the Cloudflare API's `/user` `email`.
41
+
42
+ ## Costs and plans
43
+
44
+ No price on Cloudflare's OAuth client page, and no plan named (free as far
45
+ as checked; unverified). Creating a client needs the Super Administrator,
46
+ Administrator or OAuth Client Write role on the account.
47
+
48
+ ## Only a person can
49
+
50
+ - Sign in to the Cloudflare dashboard and create the OAuth client.
51
+ - Copy the Client Secret off the screen (shown once).
52
+ - Type it at the hidden prompt of `pnpm everyapp signin add cloudflare`.
53
+ - Sign in with Cloudflare to check it; decide never to make the client
54
+ public.
55
+
56
+ ## Before you start
57
+
58
+ 1. `pnpm everyapp status --json`: `data.installed` and `data.claimed` true;
59
+ `data.gatewayUrl` gives `<gateway host>`. Not installed or no owner:
60
+ [gateway.md](gateway.md) first. `data.access.on` true: see Verify 4.
61
+ 2. `pnpm everyapp signin show cloudflare --json` (changes nothing;
62
+ `Unknown command signin`: `git pull` in the `every-app` folder).
63
+ `data.status` `off`: this file applies; `needs setup`: `data.missing`
64
+ says what is left (step 3); `on`: ask before replacing.
65
+ `data.callbackUrl` is `https://<gateway host>/api/auth/callback/cloudflare`.
66
+ 3. Ask who should use it: only people who are members of their Cloudflare
67
+ account can. For anyone else, suggest Google, Microsoft or the password.
68
+
69
+ ## Steps
70
+
71
+ 1. **You:** give the person `data.callbackUrl`.
72
+ 2. **Person:** in the Cloudflare dashboard, the account the Gateway runs
73
+ in: https://dash.cloudflare.com/?to=/:account/oauth-clients (or
74
+ **Manage Account** → **OAuth clients**) → **Create client**. Client name
75
+ (`Every App`); response type code, grant type **Authorization Code**;
76
+ token authentication method `client_secret_basic`; redirect URL the
77
+ exact URL from step 1 → **Continue** → scopes: **User Details Read**,
78
+ left **Required** (an optional one can be declined, and then sign-in
79
+ can't finish) → **Create client**. The page shows the **Client ID** and
80
+ the **Client Secret**, the secret only now. Leave its visibility
81
+ private: never **Change Visibility**.
82
+ 3. **Person:** in a second terminal in the `every-app` folder:
83
+ ```sh
84
+ pnpm everyapp signin add cloudflare
85
+ ```
86
+ It asks for the Client ID (shown as typed) and the Client Secret
87
+ (hidden: pasting works, nothing is echoed), and writes both in one new
88
+ Gateway Worker version, live on the next request. They tell you when
89
+ it's done.
90
+ Another way: in that terminal, the hidden read (bash or zsh:
91
+ `read -rs CLOUDFLARE_SIGNIN_CLIENT_SECRET && export CLOUDFLARE_SIGNIN_CLIENT_SECRET`;
92
+ fish: `read -s -x CLOUDFLARE_SIGNIN_CLIENT_SECRET`), paste, Enter, then
93
+ `pnpm everyapp signin add cloudflare --client-id <Client ID> --client-secret-from-env CLOUDFLARE_SIGNIN_CLIENT_SECRET --json`
94
+ and `unset CLOUDFLARE_SIGNIN_CLIENT_SECRET` (fish: `set -e CLOUDFLARE_SIGNIN_CLIENT_SECRET`).
95
+
96
+ ## Verify
97
+
98
+ 1. `pnpm everyapp signin list --json`: the `cloudflare` entry has `status:
99
+ "on"` and `missing` empty.
100
+ 2. The person's Cloudflare login address must be a Gateway member's or
101
+ have a pending invitation: `pnpm everyapp workspace invite <email>` for
102
+ someone new (who must also be a member of the Cloudflare account).
103
+ 3. **Person:** private window, `https://<gateway host>/sign-in` →
104
+ **Continue with Cloudflare** → Cloudflare's login → the consent screen
105
+ → allow → the Gateway, signed in.
106
+ 4. Behind Cloudflare Access: nothing else changes, but the Cloudflare
107
+ address must be the one Access knows the person by.
108
+ 5. Logs (README's Verify 4): `signin.config_problem` for a malformed or
109
+ missing setting.
110
+
111
+ ## If it fails
112
+
113
+ | error or symptom | meaning | what to do |
114
+ | --- | --- | --- |
115
+ | no **Continue with Cloudflare** button | a setting missing or malformed, or the version isn't live yet | `pnpm everyapp signin list --json`; logs' `signin.config_problem` |
116
+ | Cloudflare refuses the redirect (wording unverified) | the client's redirect URL isn't exactly `data.callbackUrl` | fix it on the client |
117
+ | Cloudflare refuses someone at its consent screen | they aren't a member of the Cloudflare account (a private client) | invite them to the Cloudflare account, or they use another way in |
118
+ | the Gateway refuses after consent, or the profile can't be read | **User Details Read** missing or made optional and declined | edit the client: the scope, **Required** |
119
+ | "You don't have an account on this gateway." | their Cloudflare address isn't a member's or invited | `pnpm everyapp workspace invite <email>` with the Cloudflare address |
120
+ | `SIGNIN_VALUE_REQUIRED` / `SECRET_EMPTY` | unattended, a required setting neither given nor set / the env variable empty in that shell | `data.rerun` with the flag / the hidden read again in the same terminal |
121
+ | Create client is missing or refused | the person's role on the account lacks OAuth Client Write | an Administrator creates it, or grants the role |
122
+
123
+ ## Rerun, rotate, remove
124
+
125
+ - Rerun: `pnpm everyapp signin add cloudflare` any time; values given
126
+ replace, settings not given are kept (Enter at a prompt keeps it).
127
+ - Rotate: rotate the secret on the client's page in the dashboard (a lost
128
+ secret can only be rotated), then `pnpm everyapp signin add cloudflare`
129
+ with Enter at the Client ID prompt, or the hidden read and
130
+ `pnpm everyapp signin add cloudflare --client-secret-from-env CLOUDFLARE_SIGNIN_CLIENT_SECRET --json`.
131
+ - Remove, only when asked: `pnpm everyapp signin remove cloudflare --json --yes`
132
+ (`--yes` only when they said so). Accounts made with it stay. Then
133
+ delete the client in **Manage Account** → **OAuth clients**.
134
+
135
+ ## Sources
136
+
137
+ - https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/
138
+ (Manage Account → OAuth clients → Create client; the three roles;
139
+ Authorization Code only; `client_secret_basic`; Required scopes; secret
140
+ shown once; private by default, public permanent, Change Visibility; no
141
+ price on the page)
142
+ - https://developers.cloudflare.com/api/resources/user/methods/get/
143
+ (`GET /user`, User Details Read)
144
+ - https://better-auth.com/docs/authentication/cloudflare (`/user` for the
145
+ email; keep `user-details.read` required)
146
+ - `docs/runbooks/sign-in-providers.md` ("Cloudflare", "Sign in with
147
+ Cloudflare: Access's identity provider versus this") and
148
+ `packages/sdk/src/internal/signInProviders.ts`