@every-app/sdk 0.2.0 → 0.3.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +608 -2
  2. package/dist/cloudflare/index.d.ts +22 -0
  3. package/dist/cloudflare/index.d.ts.map +1 -0
  4. package/dist/cloudflare/index.js +21 -0
  5. package/dist/cloudflare/sentinel.d.ts +69 -0
  6. package/dist/cloudflare/sentinel.d.ts.map +1 -0
  7. package/dist/cloudflare/sentinel.js +50 -0
  8. package/dist/cloudflare/worker.d.ts +511 -0
  9. package/dist/cloudflare/worker.d.ts.map +1 -0
  10. package/dist/cloudflare/worker.js +738 -0
  11. package/dist/dev/index.d.ts +8 -0
  12. package/dist/dev/index.d.ts.map +1 -0
  13. package/dist/dev/index.js +7 -0
  14. package/dist/dev/keypair.d.ts +24 -0
  15. package/dist/dev/keypair.d.ts.map +1 -0
  16. package/dist/dev/keypair.js +60 -0
  17. package/dist/internal/aiProviders.d.ts +67 -0
  18. package/dist/internal/aiProviders.d.ts.map +1 -0
  19. package/dist/internal/aiProviders.js +220 -0
  20. package/dist/internal/appSettings.d.ts +237 -0
  21. package/dist/internal/appSettings.d.ts.map +1 -0
  22. package/dist/internal/appSettings.js +374 -0
  23. package/dist/internal/capabilities.d.ts +1344 -0
  24. package/dist/internal/capabilities.d.ts.map +1 -0
  25. package/dist/internal/capabilities.js +1521 -0
  26. package/dist/internal/devProviders.d.ts +49 -0
  27. package/dist/internal/devProviders.d.ts.map +1 -0
  28. package/dist/internal/devProviders.js +62 -0
  29. package/dist/internal/email.d.ts +280 -0
  30. package/dist/internal/email.d.ts.map +1 -0
  31. package/dist/internal/email.js +613 -0
  32. package/dist/internal/events.d.ts +182 -0
  33. package/dist/internal/events.d.ts.map +1 -0
  34. package/dist/internal/events.js +324 -0
  35. package/dist/internal/gatewaySettings.d.ts +82 -0
  36. package/dist/internal/gatewaySettings.d.ts.map +1 -0
  37. package/dist/internal/gatewaySettings.js +209 -0
  38. package/dist/internal/identity.d.ts +18 -3
  39. package/dist/internal/identity.d.ts.map +1 -1
  40. package/dist/internal/identity.js +53 -14
  41. package/dist/internal/index.d.ts +17 -1
  42. package/dist/internal/index.d.ts.map +1 -1
  43. package/dist/internal/index.js +17 -1
  44. package/dist/internal/members.d.ts +35 -0
  45. package/dist/internal/members.d.ts.map +1 -0
  46. package/dist/internal/members.js +7 -0
  47. package/dist/internal/oauthClient.d.ts +63 -0
  48. package/dist/internal/oauthClient.d.ts.map +1 -0
  49. package/dist/internal/oauthClient.js +194 -0
  50. package/dist/internal/oauthScopes.d.ts +117 -0
  51. package/dist/internal/oauthScopes.d.ts.map +1 -0
  52. package/dist/internal/oauthScopes.js +416 -0
  53. package/dist/internal/organizationName.d.ts +18 -0
  54. package/dist/internal/organizationName.d.ts.map +1 -0
  55. package/dist/internal/organizationName.js +24 -0
  56. package/dist/internal/protocol.d.ts +86 -2
  57. package/dist/internal/protocol.d.ts.map +1 -1
  58. package/dist/internal/protocol.js +112 -1
  59. package/dist/internal/providerApps.d.ts +75 -0
  60. package/dist/internal/providerApps.d.ts.map +1 -0
  61. package/dist/internal/providerApps.js +59 -0
  62. package/dist/internal/providerRegistry.d.ts +147 -0
  63. package/dist/internal/providerRegistry.d.ts.map +1 -0
  64. package/dist/internal/providerRegistry.js +262 -0
  65. package/dist/internal/providerSpec.d.ts +55 -0
  66. package/dist/internal/providerSpec.d.ts.map +1 -0
  67. package/dist/internal/providerSpec.js +746 -0
  68. package/dist/internal/remoteMcp.d.ts +188 -0
  69. package/dist/internal/remoteMcp.d.ts.map +1 -0
  70. package/dist/internal/remoteMcp.js +543 -0
  71. package/dist/internal/tools.d.ts +115 -0
  72. package/dist/internal/tools.d.ts.map +1 -0
  73. package/dist/internal/tools.js +80 -0
  74. package/dist/server/attribution.d.ts +39 -0
  75. package/dist/server/attribution.d.ts.map +1 -0
  76. package/dist/server/attribution.js +67 -0
  77. package/dist/server/authorizeUrl.d.ts +43 -0
  78. package/dist/server/authorizeUrl.d.ts.map +1 -0
  79. package/dist/server/authorizeUrl.js +110 -0
  80. package/dist/server/emit.d.ts +63 -0
  81. package/dist/server/emit.d.ts.map +1 -0
  82. package/dist/server/emit.js +101 -0
  83. package/dist/server/everyApp.d.ts +38 -12
  84. package/dist/server/everyApp.d.ts.map +1 -1
  85. package/dist/server/everyApp.js +92 -28
  86. package/dist/server/gatewayFetch.d.ts +24 -8
  87. package/dist/server/gatewayFetch.d.ts.map +1 -1
  88. package/dist/server/gatewayFetch.js +81 -36
  89. package/dist/server/index.d.ts +10 -1
  90. package/dist/server/index.d.ts.map +1 -1
  91. package/dist/server/index.js +8 -1
  92. package/dist/server/integrationFetch.d.ts +131 -0
  93. package/dist/server/integrationFetch.d.ts.map +1 -0
  94. package/dist/server/integrationFetch.js +526 -0
  95. package/dist/server/listMembers.d.ts +32 -0
  96. package/dist/server/listMembers.d.ts.map +1 -0
  97. package/dist/server/listMembers.js +74 -0
  98. package/dist/server/nodeServer.d.ts +48 -0
  99. package/dist/server/nodeServer.d.ts.map +1 -0
  100. package/dist/server/nodeServer.js +45 -0
  101. package/dist/server/sendEmail.d.ts +39 -0
  102. package/dist/server/sendEmail.d.ts.map +1 -0
  103. package/dist/server/sendEmail.js +114 -0
  104. package/dist/server/tools.d.ts +184 -0
  105. package/dist/server/tools.d.ts.map +1 -0
  106. package/dist/server/tools.js +347 -0
  107. package/dist/server/toolsEndpoint.d.ts +32 -0
  108. package/dist/server/toolsEndpoint.d.ts.map +1 -0
  109. package/dist/server/toolsEndpoint.js +231 -0
  110. package/package.json +30 -17
  111. package/dist/server/mcp.d.ts +0 -66
  112. package/dist/server/mcp.d.ts.map +0 -1
  113. package/dist/server/mcp.js +0 -147
package/README.md CHANGED
@@ -2,8 +2,614 @@
2
2
 
3
3
  The embedded SDK for building apps on the Every App platform.
4
4
 
5
- This SDK handles authentication and session management for apps running within the Every App Gateway. It provides integrations for TanStack Start and Cloudflare Workers.
5
+ It verifies the identity the Gateway injects, defines the app's Worker and its agent tools, and reaches the Gateway's AI, email and integrations. There is no client-side export.
6
6
 
7
7
  ## Documentation
8
8
 
9
- For full API reference and usage guides, see the [Embedded SDK documentation](https://everyapp.dev/docs/embedded-sdk/overview/).
9
+ The complete references are the agent docs at https://everyapp.com/llms.txt (`identity.md`, `manifest.md`, `mcp.md`, `email.md`, `secrets.md`).
10
+
11
+ ## Entry points
12
+
13
+ - `@every-app/sdk/server`: runs in the Worker. `everyApp()` verifies the identity the gateway injects. Tokens carry the app's Worker name as their audience, which `everyAppWorker()` declares as `env.EVERYAPP_AUDIENCE` and the deploy fills in (in development, where `everyapp dev` mints for the app id, `env.EVERYAPP_APP_ID` stands in); the app id comes from `env.EVERYAPP_APP_ID`. The handler gets the runtime's `ExecutionContext` as the Worker received it, so it goes on to Hono's `app.fetch(request, env, ctx)` or React Router's request handler without a cast.
14
+ - `@every-app/sdk/cloudflare`: runs in Node while `cf` loads `cloudflare.config.ts`. `everyAppWorker()` builds the Worker definition from `everyapp.config.ts`:
15
+
16
+ ```ts
17
+ import { defineConfig } from "cf/config";
18
+ import * as entrypoint from "./src/entry.worker.ts" with { type: "cf-worker" };
19
+ import manifest from "./everyapp.config.ts";
20
+ import { everyAppWorker } from "@every-app/sdk/cloudflare";
21
+
22
+ export default defineConfig((ctx) => ({
23
+ worker: everyAppWorker({ manifest, entrypoint, ctx }),
24
+ }));
25
+ ```
26
+
27
+ Production builds are tenant-neutral: the same Build Output whoever builds the app, with placeholders (`sentinelDeployTarget()`, `BUILD_SENTINEL` in `@every-app/sdk/cloudflare/sentinel`) wherever an install's Worker name, organization, gateway URL, identity keys and resource names or ids go. `everyapp deploy` builds with `EVERYAPP_BUILD=1` and then installs the output (rewrites every placeholder, refusing one that survives); without that variable a production build throws, so deploy with `everyapp deploy` rather than `cf deploy`. A deploy that deletes Durable Object classes adds their tombstones.
28
+
29
+ `localD1Path(root, id)` is the sqlite file `cf dev` keeps the local D1 database `id` in (`env.DB.id` of a development `everyAppWorker()`, or `localD1Id(workerName, binding)`), for drizzle-kit studio.
30
+
31
+ - `@every-app/sdk/dev`: the well-known keypair and issuer `everyapp dev` signs local identities with. Public by design: the SDK accepts it only in development (`EVERYAPP_DEV`, which `everyAppWorker()` sets there and a deploy never does) and only from the dev issuer.
32
+
33
+ ## Scheduled work and secrets
34
+
35
+ `everyAppWorker()` also takes cron schedules, and the manifest names the
36
+ app's own secrets:
37
+
38
+ ```ts
39
+ // everyapp.config.ts
40
+ export default {
41
+ id: "repolens",
42
+ // The app's own secrets, set with `everyapp secret set GITHUB_TOKEN`
43
+ // in this directory; in dev, declared when .dev.vars sets them.
44
+ secrets: ["GITHUB_TOKEN"],
45
+ } as const;
46
+
47
+ // cloudflare.config.ts
48
+ export default defineConfig((ctx) => ({
49
+ worker: everyAppWorker({
50
+ manifest,
51
+ entrypoint,
52
+ ctx,
53
+ // Runs the Worker's scheduled handler every six hours (UTC).
54
+ crons: ["17 */6 * * *"],
55
+ }),
56
+ }));
57
+ ```
58
+
59
+ - **Crons.** Export a `scheduled` handler next to `everyApp()`'s `fetch`:
60
+ `export default { ...everyApp(handler, { tools }), scheduled }`. A
61
+ scheduled run has no user (identity is per request). Deploys keep the
62
+ triggers; an app can't declare routes or domains.
63
+ - **Secrets.** The manifest's `secrets` lists the app's own by name; a
64
+ name that is one of the Gateway's settings (`OPENAI_API_KEY`, say) is
65
+ refused there, since the app reaches those through `providers` and
66
+ `capabilities`. `everyapp secret set GITHUB_TOKEN` in the app's directory
67
+ puts it on the live Worker (and each helper) in a new version, and later
68
+ deploys keep it, so production builds don't declare it. In dev, put it
69
+ in `.dev.vars`: `everyAppWorker()` declares it only when it is set there,
70
+ since the Vite plugin passes a `.dev.vars` value through only for a
71
+ declared secret. Type it as optional in `src/env.d.ts`
72
+ (`GITHUB_TOKEN?: string` in `Cloudflare.Env`). An entry can carry its
73
+ need: `{ name: "GITHUB_TOKEN", why, docs, optional: true }`, or
74
+ `anyOf: "<group>"` when one of a group must be set, and
75
+ `setup: "setup/<name>.md"` names its guide in the everyapp skill;
76
+ `providers` and `capabilities` entries take `why`, `docs`, `optional` and
77
+ `anyOf` too. Until every required setting is set the app shows "Needs
78
+ setup" and its tools stay out of `/mcp`; `pnpm everyapp secret list` says
79
+ what is missing (`docs/manifest-settings.md`).
80
+
81
+ ## Compatibility flags
82
+
83
+ Every Worker `everyAppWorker()` and `everyAppAuxiliaryWorker()` build has
84
+ `nodejs_compat` and the compatibility date `2026-06-01` (or the manifest's
85
+ `compatibilityDate`). `compatibilityFlags` adds flags after `nodejs_compat`,
86
+ each once:
87
+
88
+ ```ts
89
+ everyAppWorker({
90
+ manifest,
91
+ entrypoint,
92
+ ctx,
93
+ // fetch() reaches only the public internet: no private or local addresses.
94
+ compatibilityFlags: ["global_fetch_strictly_public"],
95
+ });
96
+ ```
97
+
98
+ ## Durable Objects
99
+
100
+ Declare a Durable Object by binding and class,
101
+ `resources: { durableObjects: [{ name: "MAILBOX", className: "MailboxDO" }] }`,
102
+ and export the class from the Worker entry. When `entrypoint` is the entry
103
+ imported as a `cf-worker` module (as above), `cf workers types` types the
104
+ binding with the class, `env.MAILBOX: DurableObjectNamespace<MailboxDO>`, so
105
+ RPC calls on a stub type-check: the binding names the Worker by its
106
+ definition (`worker: { name, compatibilityDate, entrypoint }`), which cf
107
+ resolves to the Worker's name. With a path entrypoint, and for a class a
108
+ helper Worker defines, the binding names the Worker by name and the
109
+ namespace is untyped. Retype it in `src/env.d.ts` (a plain cast is refused):
110
+
111
+ ```ts
112
+ import type { Scratchpad } from "./audit.worker";
113
+
114
+ export type AppEnv = Omit<Env, "SCRATCHPAD"> & {
115
+ SCRATCHPAD: DurableObjectNamespace<Scratchpad>;
116
+ };
117
+ ```
118
+
119
+ ## Workflows
120
+
121
+ Declare a [Workflow](https://developers.cloudflare.com/workflows/) in
122
+ `everyapp.config.ts` by binding and class,
123
+ `resources: { workflows: [{ name: "RANK_CHECK", className: "RankCheckWorkflow" }] }`,
124
+ and export the `WorkflowEntrypoint` class from the Worker entry.
125
+ `everyAppWorker()` defines and binds it under a name after the Worker
126
+ (`every-acme-seo--rank-check`, from `workflowNameFor()`), because Workflow
127
+ names are shared by the whole Cloudflare account. Start one with
128
+ `env.RANK_CHECK.create({ params })`; a run has no user.
129
+
130
+ ## Helper Workers
131
+
132
+ An app can run part of its work on Workers of its own, e.g. a memory-hungry
133
+ job that would crowd the app's isolate. Declare each one, and the Durable
134
+ Objects and Workflows it defines:
135
+
136
+ ```ts
137
+ resources: {
138
+ durableObjects: [{ name: "SCRATCHPAD", className: "Scratchpad", worker: "audit" }],
139
+ workflows: [{ name: "SITE_AUDIT", className: "SiteAuditWorkflow", worker: "audit" }],
140
+ },
141
+ workers: [{ name: "audit", binding: "AUDIT" }],
142
+ ```
143
+
144
+ Build it as an auxiliary Worker of the Vite plugin:
145
+
146
+ ```ts
147
+ // vite.config.ts: defineConfig(({ mode }) => ({ plugins: [ … ] }))
148
+ cloudflare({
149
+ viteEnvironment: { name: "ssr" },
150
+ auxiliaryWorkers: [
151
+ {
152
+ config: everyAppAuxiliaryWorker({
153
+ manifest,
154
+ worker: "audit",
155
+ entrypoint: "./src/audit.worker.ts",
156
+ ctx: { mode },
157
+ }),
158
+ },
159
+ ],
160
+ }),
161
+ ```
162
+
163
+ It deploys as `every-acme-seo--audit`, private like the app: only the app's
164
+ Worker reaches it, through `env.AUDIT` and the bindings of the classes it
165
+ defines. It shares the app's D1 databases, KV namespaces and R2 buckets, and
166
+ calls the gateway as the app, with the app's `GATEWAY_URL` and
167
+ `EVERY_APP_ORG_ID`, so its background calls (a Workflow's, say) are the
168
+ organization's like the app's. `everyapp deploy` checks and uploads helpers before the
169
+ app's Worker, and `everyapp delete --purge` deletes them, and the Workflows
170
+ they run, with it. A class stays in the Worker that first defined it.
171
+
172
+ ## Postgres through Hyperdrive
173
+
174
+ An app whose data lives in Postgres declares the database and a
175
+ [Hyperdrive](https://developers.cloudflare.com/hyperdrive/) config, and
176
+ migrates it with drizzle:
177
+
178
+ ```ts
179
+ resources: { hyperdrive: [{ name: "HYPERDRIVE" }] },
180
+ database: { provider: "postgres", region: "aws:us-east-1" },
181
+ migrations: { engine: "drizzle", dialect: "postgresql", dir: "drizzle/pg", config: "drizzle/pg.config.ts" },
182
+ workers: [{ name: "audit", limits: { cpuMs: 300_000 } }],
183
+ limits: { cpuMs: 30_000 },
184
+ ```
185
+
186
+ Every one of the app's Workers binds `env.HYPERDRIVE` (a config with
187
+ `worker: "<helper>"` binds only that helper) and reads `DATABASE_PROVIDER`
188
+ (`"postgres"`); deployed, they run near `database.region`. `limits.cpuMs`
189
+ raises a Worker's CPU time per invocation, up to 5 minutes. `everyapp
190
+ deploy` reads the database's URL from `.everyapp/secrets.env`, which it
191
+ never uploads or prints:
192
+
193
+ ```sh
194
+ POSTGRES_DATABASE_URL=postgres://<user>:<password>@<host>:<port>/<database>
195
+ # optional: the origin a binding's config connects to, when it isn't that one
196
+ HYPERDRIVE_ORIGIN_URL=postgres://<app user>:<password>@<host>:<port>/<database>
197
+ ```
198
+
199
+ It creates the config `every-acme-seo--hyperdrive` from it, or adopts one of
200
+ that name that connects to the same database, runs `drizzle-kit migrate`
201
+ (your config reads `process.env.POSTGRES_DATABASE_URL`) before it uploads, and
202
+ `everyapp delete --purge` deletes the config, never the database. In
203
+ development, set `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE`
204
+ in `.dev.vars` to a local Postgres; without it the binding is absent.
205
+
206
+ The configs `everyapp deploy` creates cache no queries: Hyperdrive never
207
+ invalidates a cached read when the app writes, and app code reads its own
208
+ writes. A deploy turns caching back off on a config that caches. For a
209
+ read-heavy config that tolerates reads up to a minute old,
210
+ `hyperdrive: [{ name: "HYPERDRIVE", caching: "default" }]` keeps Cloudflare's
211
+ own caching (60 s, served stale for 15 s more).
212
+
213
+ An app on Prisma migrates with `prisma migrate deploy` instead:
214
+
215
+ ```ts
216
+ migrations: { engine: "prisma", dialect: "postgresql", config: "prisma.config.ts" },
217
+ ```
218
+
219
+ The deploy runs the app's own `prisma` (a devDependency) with
220
+ `POSTGRES_DATABASE_URL` in its environment, so `prisma.config.ts` reads it:
221
+ `datasource: { url: process.env.POSTGRES_DATABASE_URL }`. Prisma finds its
222
+ migrations through its config; a `dir` in the manifest only makes the deploy
223
+ skip the migration when that directory is missing. The deploy can't check the
224
+ schema against the migrations (Prisma needs a shadow database for that) and
225
+ says so; run `prisma migrate diff --from-migrations <dir> --to-schema
226
+ <schema> --exit-code` yourself. Prisma on D1 is no engine of its own: generate
227
+ the SQL (`prisma migrate diff --from-empty --to-schema schema.prisma
228
+ --script`), put `--> statement-breakpoint` between its statements, and ship it
229
+ with `migrations: { engine: "d1-sql", dir: "migrations", binding: "DB" }`.
230
+
231
+ ## R2 buckets
232
+
233
+ Declare the app's buckets by binding, and every one of its Workers (helpers
234
+ included) binds them:
235
+
236
+ ```ts
237
+ resources: { r2: ["R2", "MEDIA"] },
238
+ ```
239
+
240
+ `R2` is the bucket named after the app's Worker (`every-acme-postiz`), any
241
+ other binding `<worker>--<binding>` (`every-acme-postiz--media`); the
242
+ gateway assigns the names when it registers the app, and the deploy's policy
243
+ refuses any other bucket. The deploy doesn't create, record or purge buckets
244
+ yet: `cf deploy` should create a bound bucket that doesn't exist (not yet
245
+ confirmed on a live account), and the dashboard can create one before the
246
+ first deploy. A bucket bound by hand in
247
+ `cloudflare.config.ts`, `bindings.r2({ name: worker.name })`, keeps working
248
+ while the manifest declares none.
249
+
250
+ ## Node frameworks (Express, NestJS)
251
+
252
+ A Node HTTP framework runs in the Worker on workerd's `node:http` bridge
253
+ (`cloudflare:node`), which `nodejs_compat` and the compatibility date turn
254
+ on. `nodeServerHandler()` starts the server on the isolate's first request
255
+ and hands it every request `everyApp()` lets through:
256
+
257
+ ```ts
258
+ // src/entry.worker.ts
259
+ import "reflect-metadata";
260
+ import { NestFactory } from "@nestjs/core";
261
+ import { everyApp, nodeServerHandler } from "@every-app/sdk/server";
262
+ import { AppModule } from "./app.module";
263
+
264
+ const handler = nodeServerHandler({
265
+ boot: async () => {
266
+ const app = await NestFactory.create(AppModule, { rawBody: true });
267
+ await app.listen(0);
268
+ return { port: app.getHttpServer().address().port };
269
+ },
270
+ });
271
+
272
+ export default { ...everyApp(handler), scheduled };
273
+ ```
274
+
275
+ - **`listen(0)`.** workerd picks the port. A fixed port is still held by the
276
+ server of the module graph a Vite reload replaced, so the next boot fails
277
+ with `EADDRINUSE`.
278
+ - **Identity.** `everyApp()` has verified the identity before the Node app
279
+ sees the request, which still carries it as `x-everyapp-identity`. To know
280
+ the user in Node code, verify it again from the request's headers with the
281
+ Worker's text bindings, which `nodejs_compat` puts on `process.env`:
282
+ `await getEveryAppUser(new Request(url, { headers }), process.env)`.
283
+ - **What comes back.** `Set-Cookie` is dropped: the gateway owns the
284
+ session and strips cookies from what reaches the app. Drop `compression()`
285
+ too (Cloudflare compresses at the edge); a body Node encodes anyway is
286
+ passed through as is, since the bridge alone would encode it a second
287
+ time.
288
+ - **No identity outside `fetch`.** `scheduled` (and `email`) handlers sit
289
+ beside `fetch` and run with no user; they reach the Node app only if they
290
+ call it themselves.
291
+
292
+ Three rules decide whether such a Worker runs at all:
293
+
294
+ 1. **Minify the Worker.** A Node framework's dependency graph is large, and
295
+ the isolate's heap grows with the source it loads (about 2.5 MB per MiB
296
+ locally), against a 128 MB limit. `unplugin-swc` (for Nest's decorator
297
+ metadata) sets `esbuild: false`, which also silently skips Vite's esbuild
298
+ minifier: `build.minify: true` changes nothing. Use
299
+ `build.minify: "terser"` (with `terser` installed), or minify the Worker's
300
+ chunks yourself:
301
+
302
+ ```ts
303
+ // vite.config.ts
304
+ import { transform } from "esbuild";
305
+
306
+ const minifyWorker = {
307
+ name: "minify-worker",
308
+ apply: "build" as const,
309
+ enforce: "post" as const,
310
+ applyToEnvironment: (environment: { name: string }) =>
311
+ environment.name !== "client",
312
+ async renderChunk(code: string) {
313
+ const result = await transform(code, {
314
+ minify: true,
315
+ format: "esm",
316
+ target: "es2022",
317
+ legalComments: "none",
318
+ });
319
+ return { code: result.code, map: null };
320
+ },
321
+ };
322
+ ```
323
+
324
+ Check that the bundle's size actually changed. On Vite 8 (oxc instead of
325
+ SWC) measure it again before and after.
326
+
327
+ 2. **No work at module scope.** workerd refuses random values
328
+ (`crypto.randomUUID`, `getRandomValues`), timers and I/O (`fetch`,
329
+ sockets) outside a request ("Disallowed operation called within global
330
+ scope"), and `import.meta.url` is undefined, so
331
+ `createRequire(import.meta.url)` and `fileURLToPath(import.meta.url)`
332
+ throw. Nothing reachable from a module's top level may do these: make
333
+ every client (`new Stripe()`, `new OpenAI()`, SDK singletons) a lazy
334
+ factory, and create the app in `boot`.
335
+
336
+ 3. **Stay inside the heap budget.** After `boot` and a few requests, the
337
+ isolate's used heap should be at most about 64 MB locally, half the
338
+ limit, leaving room for request bodies and caches. Read it from
339
+ miniflare's inspector (`inspectorPort`, then `Runtime.getHeapUsage` over
340
+ the websocket); `v8.getHeapStatistics()` reads zero in workerd.
341
+ `wrangler check startup` is only a smoke test for module-scope work.
342
+
343
+ ## Agent tools
344
+
345
+ `defineTools()` describes the actions an app offers to AI clients (Claude,
346
+ Cursor, any MCP client). The app never speaks MCP itself: the gateway
347
+ snapshots the catalog when you deploy, serves MCP clients from it, and calls a
348
+ tool over a private, internal contract with a verified identity.
349
+
350
+ ```ts
351
+ import { defineTools, everyApp, ToolError } from "@every-app/sdk/server";
352
+ import { z } from "zod"; // or any Standard Schema + Standard JSON Schema validator
353
+
354
+ export const tools = defineTools(
355
+ {
356
+ list_todos: {
357
+ description: "List the user's todos.",
358
+ risk: "read",
359
+ input: z.object({}).strict(),
360
+ handler: async (_input, { user }) => listTodos(user.id),
361
+ },
362
+ delete_todo: {
363
+ description: "Permanently delete one todo.",
364
+ risk: "destructive", // hidden from AI clients until approvals exist
365
+ input: z.object({ id: z.string() }).strict(),
366
+ handler: async ({ id }, { user }) => {
367
+ if (!(await deleteTodo(user.id, id)))
368
+ throw new ToolError("Todo not found");
369
+ },
370
+ },
371
+ },
372
+ { instructions: "Never guess todo ids; call list_todos first." },
373
+ );
374
+
375
+ export default everyApp(handler, { tools });
376
+ ```
377
+
378
+ - `risk` is `read`, `write` or `destructive`. AI clients never see
379
+ destructive tools yet.
380
+ - `input` (and the optional `output`) take zod >= 4.2 or any validator that
381
+ implements Standard Schema and Standard JSON Schema; a plain
382
+ `{ jsonSchema, validate }` pair works too. Both describe objects: a
383
+ `z.object`, or a union of objects. A tool whose schema can't be described
384
+ that way is left out of the catalog (the other tools keep working), and
385
+ `everyapp deploy` prints why.
386
+ - Throw `ToolError` for messages the model may read. Other errors are logged
387
+ in the app and reported as "Internal error in <app> (ref <call id>)".
388
+ - Return `toolResult({ text, structured })` to give the model a written
389
+ summary while clients get the data: `structured` is checked against the
390
+ tool's `output` schema. A plain value is sent as JSON text.
391
+ - A handler's second argument carries `user`, `env` (typed as
392
+ `Cloudflare.Env`, which `cf` generates from `cloudflare.config.ts`), `ctx`,
393
+ `request` and `callId`.
394
+
395
+ ## Capabilities
396
+
397
+ Third-party APIs whose key the gateway holds (`docs/capabilities.md`). List
398
+ one in `everyapp.config.ts` (`capabilities: { dataforseo: { access: "read",
399
+ why: "Keyword research" } }`) and call the provider's real URLs:
400
+
401
+ ```ts
402
+ import { gatewayRefusal, integrationFetch } from "@every-app/sdk/server";
403
+
404
+ const dataforseo = integrationFetch({
405
+ env,
406
+ provider: "dataforseo",
407
+ for: request,
408
+ });
409
+ const response = await dataforseo(
410
+ "https://api.dataforseo.com/v3/dataforseo_labs/google/keyword_ideas/live",
411
+ { method: "POST", body: JSON.stringify([{ keywords: ["seo"] }]) },
412
+ );
413
+ const refused = await gatewayRefusal(response); // null: DataForSEO answered
414
+ ```
415
+
416
+ The call goes over the gateway binding, which puts the key on it.
417
+ `gatewayRefusal(response)`
418
+ tells the gateway's own refusals (`x-everyapp-error`: the manifest doesn't
419
+ list the capability, the gateway holds no key) from the provider's answers.
420
+ `integrationStatus({ env, provider })` says whether calls would go through,
421
+ without making one. In local dev, with no binding, the key comes from
422
+ `.dev.vars` under the gateway secret's name (`DATAFORSEO_API_KEY`), and so do
423
+ the AI providers' (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, or
424
+ `WORKERS_AI_API_TOKEN` with `CLOUDFLARE_ACCOUNT_ID` for Workers AI, which is
425
+ billed to that account) for `createGatewayFetch`: `everyAppWorker()` declares
426
+ the settings of each capability and provider the manifest lists when they
427
+ are set there, so the dev server passes them through.
428
+
429
+ ### Email
430
+
431
+ The gateway sends mail for apps (`docs/capabilities.md`, "Gateway services:
432
+ email"). List `capabilities: { email: { access: "write" } }`; to send as
433
+ your own addresses, set them on the installation with
434
+ `pnpm everyapp email set --sender "*@mail.example.com"` in the app's
435
+ directory after its first deploy (each domain onboarded to Email Sending in
436
+ the gateway's account), and send:
437
+
438
+ ```ts
439
+ import { GatewayRefusalError, sendEmail } from "@every-app/sdk/server";
440
+
441
+ const { messageId, status } = await sendEmail({
442
+ env,
443
+ for: request,
444
+ message: {
445
+ to: ["customer@example.net"],
446
+ subject: "Re: your question",
447
+ text: "Thanks for writing.",
448
+ from: { address: "support@mail.example.com", name: "Acme Support" },
449
+ headers: { "In-Reply-To": "<original@example.net>" },
450
+ },
451
+ });
452
+ ```
453
+
454
+ Leave `from` out to send as the gateway's sender. A refusal throws
455
+ `GatewayRefusalError` with its `code` (`email_sender_not_allowed`,
456
+ `email_budget_exhausted`, `integration_not_configured`, ...) and a message to
457
+ show. `integrationStatus({ env, provider: "email" })` adds the gateway's
458
+ `sender` and the `senders` the installation allows. In local dev, with no
459
+ binding, the message is checked as the gateway would, then printed and
460
+ answered `{ status: "logged" }`; set `EVERYAPP_EMAIL_SENDERS` in `.dev.vars`
461
+ to allow `from` addresses locally, and `EMAIL_REST_API_TOKEN`,
462
+ `CLOUDFLARE_ACCOUNT_ID` and `EMAIL_FROM` to send for real from your own
463
+ account. An `email()` handler (inbound mail, declared with
464
+ `email: { inbound: true }`; `everyapp email set --inbound` routes addresses
465
+ to it) sits beside `everyApp()`'s `fetch` and passes
466
+ `for: { background: true, organizationId }`.
467
+
468
+ ### Members
469
+
470
+ The gateway lists the organization's members who can open the app now
471
+ (`docs/capabilities.md`, "Gateway services: members"): read it when acting
472
+ for people outside a request, such as choosing whom a scheduled report goes
473
+ to, rather than a copy the app kept. List
474
+ `capabilities: { members: { access: "read" } }`, and:
475
+
476
+ ```ts
477
+ import { listMembers } from "@every-app/sdk/server";
478
+
479
+ const { members } = await listMembers({ env, for: { background: true } });
480
+ // [{ id, email, name, role: "owner" | "admin" | "member" }, ...]
481
+ ```
482
+
483
+ People who were deactivated, left the organization or (for an
484
+ organization's own app) weren't granted the app are left out. An agent's
485
+ tool call is refused (`integration_agent_not_allowed`). In local dev, with
486
+ no binding, it answers the request's signed-in user alone, and no one for
487
+ background work.
488
+
489
+ ### Events
490
+
491
+ An app puts its own events on the gateway's event bus, for the
492
+ organization's webhook and email subscribers
493
+ (`docs/design/2026-10-events-out.md`). No manifest entry is needed: the
494
+ gateway names the event `app.<app id>.<event>`, and owners and admins
495
+ subscribe to `app.<app id>.*` (or one name) in Manage workspace, Events.
496
+
497
+ ```ts
498
+ import { emit } from "@every-app/sdk/server";
499
+ import { waitUntil } from "cloudflare:workers";
500
+
501
+ waitUntil(
502
+ emit({ env, for: request, event: "invoice.paid", data: { invoiceId } }),
503
+ );
504
+ ```
505
+
506
+ `event` is lowercase dotted segments (`invoice.paid`), at most 64
507
+ characters. `data` is a JSON object, at most 8 KiB and 8 levels deep, and
508
+ goes as written to every URL a subscriber chose: never a credential, and a
509
+ key named like one at any depth (`token`, `secret`, `password`, `apiKey`,
510
+ `session`, `cookie`, ...) is refused. It answers `{ id, type }`, the
511
+ event's id and its delivered type. A refusal throws `GatewayRefusalError`
512
+ with its `code` (`event_name_invalid`, `event_data_invalid`,
513
+ `event_data_refused`, `event_data_too_large`, `event_rate_limited` past 600
514
+ an hour, ...) and a message to show; an emit that must never fail the work
515
+ around it goes under `waitUntil` or a `.catch()`. In local dev, with no
516
+ binding, the event is checked as the gateway would, printed as
517
+ `[everyapp dev] event app.<id>.<event>` (never its data) and answered
518
+ `{ id: "dev-<uuid>", type }`.
519
+
520
+ ### Social networks' OAuth clients
521
+
522
+ The gateway holds a client for each social network its operator set up
523
+ (`docs/capabilities.md`, "OAuth clients": `linkedin`, `facebook`,
524
+ `instagram`, `threads`, `youtube`, `tiktok`, `tiktok-business`, `reddit`,
525
+ `pinterest`, `discord`, `slack`, `mastodon`, `kick`, `twitch`, `tumblr`,
526
+ `dribbble`; and for mail and calendar, `google-workspace` and
527
+ `microsoft-365`). List one for writing, send people to the provider's
528
+ authorize URL (for a client with scope sets, the one `authorizeUrl()`
529
+ builds from its status; for the others, one you build from the status's
530
+ `clientId` and the network's documented scopes), and exchange or refresh
531
+ tokens through the gateway without any client:
532
+
533
+ ```ts
534
+ // everyapp.config.ts
535
+ capabilities: {
536
+ linkedin: { access: "write" },
537
+ // A client several apps share lists the scope sets it uses.
538
+ "google-workspace": {
539
+ access: "write",
540
+ scopes: ["identity", "search-console.read"],
541
+ why: "Reads Search Console for the sites you connect",
542
+ },
543
+ }
544
+ ```
545
+
546
+ ```ts
547
+ import {
548
+ authorizeUrl,
549
+ integrationFetch,
550
+ integrationStatus,
551
+ } from "@every-app/sdk/server";
552
+
553
+ const status = await integrationStatus({ env, provider: "google-workspace" });
554
+ if (!status.ready) return notAvailable(); // the operator hasn't set it up
555
+ // ...redirect to
556
+ const url = authorizeUrl({
557
+ provider: "google-workspace",
558
+ status,
559
+ redirectUri: `${origin}/integrations/google/callback`,
560
+ state,
561
+ codeChallenge, // optional PKCE (S256)
562
+ });
563
+ // ...and on the callback, exchange the code:
564
+ const tokens = await integrationFetch({
565
+ env,
566
+ provider: "google-workspace",
567
+ for: request,
568
+ })("https://oauth2.googleapis.com/token", {
569
+ method: "POST",
570
+ headers: { "content-type": "application/x-www-form-urlencoded" },
571
+ body: new URLSearchParams({
572
+ grant_type: "authorization_code",
573
+ code,
574
+ redirect_uri,
575
+ code_verifier,
576
+ }),
577
+ });
578
+ ```
579
+
580
+ The gateway completes the network's token endpoints only, putting its client
581
+ where each one takes it (form body, HTTP Basic, query string or JSON body);
582
+ anything else throws a `TypeError` before it is sent. Call the network's API
583
+ with the user's token directly. Mastodon's status also gives the instance as
584
+ `origin`.
585
+
586
+ For a client with scope sets (`google-workspace`, `microsoft-365`, `slack`),
587
+ the status carries the sets the manifest declares as `scopeSets` and the
588
+ provider's scope strings they ask for as `scopes`, which `authorizeUrl()`
589
+ puts on the URL (with Google's `access_type=offline`, Microsoft's
590
+ `response_mode=query`, Slack's comma-joined `scope` and `user_scope`, and
591
+ never `include_granted_scopes`); the gateway
592
+ refuses an exchange or refresh whose granted scopes exceed the sets
593
+ (`integration_scope_exceeded`), so a person's wider consent on another app
594
+ never reaches this one. A manifest entry without usable `scopes` is not
595
+ `ready`. The manifest's `scopes` field needs a gateway and CLI from the
596
+ same release as this SDK: an older `everyapp dev` refuses a manifest that
597
+ has it at startup. In local dev, with no binding, `.dev.vars` holds your
598
+ own client under the gateway's names (`LINKEDIN_CLIENT_ID`,
599
+ `LINKEDIN_CLIENT_SECRET`, `MASTODON_URL`, ...), the same rule puts it on
600
+ the call, and the same scope checks apply, from the manifest `everyapp dev`
601
+ passes the Worker.
602
+
603
+ ### Whom a call is for
604
+
605
+ `createGatewayFetch`, `integrationFetch`, `sendEmail`, `listMembers` and
606
+ `emit` take `for`, which decides the organization the call is attributed to:
607
+
608
+ - `for: request`, the inbound request: the identity the gateway put on it
609
+ is forwarded, so the call is that person's, in their organization.
610
+ - `for: { background: true, organizationId }`, for work no request started
611
+ (a cron, a Workflow step, a queue): sent as `x-everyapp-org` with no
612
+ identity.
613
+ - Omitted: allowed only in an organization's own app, whose background
614
+ calls are the app's, in its own organization (the same as
615
+ `{ background: true }`). Naming another organization is refused.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @every-app/sdk/cloudflare — the Worker definition for an app's
3
+ * `cloudflare.config.ts`:
4
+ *
5
+ * import { defineConfig } from "cf/config";
6
+ * import * as entrypoint from "./src/entry.worker.ts" with { type: "cf-worker" };
7
+ * import manifest from "./everyapp.config.ts";
8
+ * import { everyAppWorker } from "@every-app/sdk/cloudflare";
9
+ *
10
+ * export default defineConfig((ctx) => ({
11
+ * worker: everyAppWorker({ manifest, entrypoint, ctx }),
12
+ * }));
13
+ *
14
+ * This runs in Node while cf and the Vite plugin load the config, never in the
15
+ * Worker. Keep it self-contained: the plugin hides every file the config
16
+ * imports from the Worker in dev, so sharing a module with `/server` or
17
+ * `/internal` would break `cf dev` wherever the SDK is linked from source.
18
+ * (The dev keypair it hands the dev Worker is a leaf the Worker never loads.)
19
+ */
20
+ export { EVERYAPP_OBSERVABILITY, everyAppAuxiliaryWorker, everyAppWorker, helperWorkerName, localD1Id, localD1Path, workflowNameFor, type D1Binding, type DurableObjectBinding, type DurableObjectExport, type EveryAppAuxiliaryWorkerConfig, type EveryAppAuxiliaryWorkerOptions, type EveryAppBinding, type EveryAppConfigContext, type EveryAppDeployEnv, type EveryAppDevEnv, type EveryAppExports, type EveryAppObservability, type EveryAppPlacement, type EveryAppWorkerConfig, type EveryAppWorkerManifest, type EveryAppWorkerOptions, type EveryAppWorkerReference, type GatewayBinding, type HyperdriveBinding, type KvBinding, type SecretBinding, type ServiceBinding, type TextBinding, type VersionMetadataBinding, type WorkflowBinding, type WorkflowExport, } from "./worker.js";
21
+ export { BUILD_SENTINEL, resourceSentinel, sentinelDeployTarget, type EveryAppDeployTarget, } from "./sentinel.js";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cloudflare/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,WAAW,EACX,eAAe,EACf,KAAK,SAAS,EACd,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,6BAA6B,EAClC,KAAK,8BAA8B,EACnC,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @every-app/sdk/cloudflare — the Worker definition for an app's
3
+ * `cloudflare.config.ts`:
4
+ *
5
+ * import { defineConfig } from "cf/config";
6
+ * import * as entrypoint from "./src/entry.worker.ts" with { type: "cf-worker" };
7
+ * import manifest from "./everyapp.config.ts";
8
+ * import { everyAppWorker } from "@every-app/sdk/cloudflare";
9
+ *
10
+ * export default defineConfig((ctx) => ({
11
+ * worker: everyAppWorker({ manifest, entrypoint, ctx }),
12
+ * }));
13
+ *
14
+ * This runs in Node while cf and the Vite plugin load the config, never in the
15
+ * Worker. Keep it self-contained: the plugin hides every file the config
16
+ * imports from the Worker in dev, so sharing a module with `/server` or
17
+ * `/internal` would break `cf dev` wherever the SDK is linked from source.
18
+ * (The dev keypair it hands the dev Worker is a leaf the Worker never loads.)
19
+ */
20
+ export { EVERYAPP_OBSERVABILITY, everyAppAuxiliaryWorker, everyAppWorker, helperWorkerName, localD1Id, localD1Path, workflowNameFor, } from "./worker.js";
21
+ export { BUILD_SENTINEL, resourceSentinel, sentinelDeployTarget, } from "./sentinel.js";