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

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 (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -64
  3. package/dist/cloudflare/catalogSettings.generated.d.ts +84 -0
  4. package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
  5. package/dist/cloudflare/catalogSettings.generated.js +175 -0
  6. package/dist/cloudflare/index.d.ts +3 -2
  7. package/dist/cloudflare/index.d.ts.map +1 -1
  8. package/dist/cloudflare/index.js +4 -1
  9. package/dist/cloudflare/sentinel.d.ts +60 -2
  10. package/dist/cloudflare/sentinel.d.ts.map +1 -1
  11. package/dist/cloudflare/sentinel.js +37 -2
  12. package/dist/cloudflare/worker.d.ts +391 -63
  13. package/dist/cloudflare/worker.d.ts.map +1 -1
  14. package/dist/cloudflare/worker.js +394 -108
  15. package/dist/dev/index.d.ts +1 -1
  16. package/dist/dev/index.d.ts.map +1 -1
  17. package/dist/dev/index.js +1 -1
  18. package/dist/dev/keypair.d.ts +2 -0
  19. package/dist/dev/keypair.d.ts.map +1 -1
  20. package/dist/dev/keypair.js +2 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +5 -0
  24. package/dist/internal/aiDefault.d.ts +205 -0
  25. package/dist/internal/aiDefault.d.ts.map +1 -0
  26. package/dist/internal/aiDefault.js +335 -0
  27. package/dist/internal/aiProviders.d.ts +7 -2
  28. package/dist/internal/aiProviders.d.ts.map +1 -1
  29. package/dist/internal/aiProviders.js +19 -6
  30. package/dist/internal/appSettings.d.ts +82 -4
  31. package/dist/internal/appSettings.d.ts.map +1 -1
  32. package/dist/internal/appSettings.js +204 -29
  33. package/dist/internal/audit.d.ts +108 -0
  34. package/dist/internal/audit.d.ts.map +1 -0
  35. package/dist/internal/audit.js +235 -0
  36. package/dist/internal/capabilities.d.ts +2261 -815
  37. package/dist/internal/capabilities.d.ts.map +1 -1
  38. package/dist/internal/capabilities.js +122 -1034
  39. package/dist/internal/catalog/commerce.d.ts +394 -0
  40. package/dist/internal/catalog/commerce.d.ts.map +1 -0
  41. package/dist/internal/catalog/commerce.js +477 -0
  42. package/dist/internal/catalog/comms.d.ts +375 -0
  43. package/dist/internal/catalog/comms.d.ts.map +1 -0
  44. package/dist/internal/catalog/comms.js +520 -0
  45. package/dist/internal/catalog/index.d.ts +2008 -0
  46. package/dist/internal/catalog/index.d.ts.map +1 -0
  47. package/dist/internal/catalog/index.js +19 -0
  48. package/dist/internal/catalog/inference.d.ts +393 -0
  49. package/dist/internal/catalog/inference.d.ts.map +1 -0
  50. package/dist/internal/catalog/inference.js +460 -0
  51. package/dist/internal/catalog/infra.d.ts +458 -0
  52. package/dist/internal/catalog/infra.d.ts.map +1 -0
  53. package/dist/internal/catalog/infra.js +608 -0
  54. package/dist/internal/catalog/search.d.ts +400 -0
  55. package/dist/internal/catalog/search.d.ts.map +1 -0
  56. package/dist/internal/catalog/search.js +463 -0
  57. package/dist/internal/devProviders.d.ts +2 -2
  58. package/dist/internal/devProviders.d.ts.map +1 -1
  59. package/dist/internal/devProviders.js +19 -1
  60. package/dist/internal/events.d.ts +65 -3
  61. package/dist/internal/events.d.ts.map +1 -1
  62. package/dist/internal/events.js +110 -5
  63. package/dist/internal/gatewaySettings.d.ts +3 -2
  64. package/dist/internal/gatewaySettings.d.ts.map +1 -1
  65. package/dist/internal/gatewaySettings.js +12 -6
  66. package/dist/internal/identity.d.ts +41 -4
  67. package/dist/internal/identity.d.ts.map +1 -1
  68. package/dist/internal/identity.js +48 -10
  69. package/dist/internal/index.d.ts +6 -1
  70. package/dist/internal/index.d.ts.map +1 -1
  71. package/dist/internal/index.js +6 -1
  72. package/dist/internal/members.d.ts +3 -2
  73. package/dist/internal/members.d.ts.map +1 -1
  74. package/dist/internal/oauthBuiltIns.d.ts +101 -0
  75. package/dist/internal/oauthBuiltIns.d.ts.map +1 -0
  76. package/dist/internal/oauthBuiltIns.js +122 -0
  77. package/dist/internal/oauthClient.d.ts +8 -50
  78. package/dist/internal/oauthClient.d.ts.map +1 -1
  79. package/dist/internal/oauthClient.js +2 -177
  80. package/dist/internal/oauthPresets.d.ts +32 -0
  81. package/dist/internal/oauthPresets.d.ts.map +1 -0
  82. package/dist/internal/oauthPresets.js +253 -0
  83. package/dist/internal/oauthScopes.d.ts +43 -100
  84. package/dist/internal/oauthScopes.d.ts.map +1 -1
  85. package/dist/internal/oauthScopes.js +83 -382
  86. package/dist/internal/oauthSpec.d.ts +103 -0
  87. package/dist/internal/oauthSpec.d.ts.map +1 -0
  88. package/dist/internal/oauthSpec.js +520 -0
  89. package/dist/internal/protocol.d.ts +97 -5
  90. package/dist/internal/protocol.d.ts.map +1 -1
  91. package/dist/internal/protocol.js +89 -1
  92. package/dist/internal/providerRegistry.d.ts +17 -6
  93. package/dist/internal/providerRegistry.d.ts.map +1 -1
  94. package/dist/internal/providerRegistry.js +71 -8
  95. package/dist/internal/providerSpec.d.ts +18 -7
  96. package/dist/internal/providerSpec.d.ts.map +1 -1
  97. package/dist/internal/providerSpec.js +420 -34
  98. package/dist/internal/remoteMcp.d.ts.map +1 -1
  99. package/dist/internal/remoteMcp.js +1 -5
  100. package/dist/internal/signInProviders.d.ts +104 -0
  101. package/dist/internal/signInProviders.d.ts.map +1 -0
  102. package/dist/internal/signInProviders.js +245 -0
  103. package/dist/internal/tools.d.ts +5 -4
  104. package/dist/internal/tools.d.ts.map +1 -1
  105. package/dist/internal/tools.js +4 -3
  106. package/dist/manifest.d.ts +267 -0
  107. package/dist/manifest.d.ts.map +1 -0
  108. package/dist/manifest.js +30 -0
  109. package/dist/server/ai.d.ts +111 -0
  110. package/dist/server/ai.d.ts.map +1 -0
  111. package/dist/server/ai.js +145 -0
  112. package/dist/server/aiBinding.d.ts +68 -0
  113. package/dist/server/aiBinding.d.ts.map +1 -0
  114. package/dist/server/aiBinding.js +294 -0
  115. package/dist/server/attribution.d.ts +18 -5
  116. package/dist/server/attribution.d.ts.map +1 -1
  117. package/dist/server/attribution.js +19 -5
  118. package/dist/server/connectUrl.d.ts +16 -0
  119. package/dist/server/connectUrl.d.ts.map +1 -0
  120. package/dist/server/connectUrl.js +40 -0
  121. package/dist/server/context.d.ts +68 -0
  122. package/dist/server/context.d.ts.map +1 -0
  123. package/dist/server/context.js +117 -0
  124. package/dist/server/emailBinding.d.ts +72 -0
  125. package/dist/server/emailBinding.d.ts.map +1 -0
  126. package/dist/server/emailBinding.js +124 -0
  127. package/dist/server/emit.d.ts +1 -1
  128. package/dist/server/emit.d.ts.map +1 -1
  129. package/dist/server/emit.js +7 -7
  130. package/dist/server/env.d.ts +44 -0
  131. package/dist/server/env.d.ts.map +1 -0
  132. package/dist/server/env.js +1 -0
  133. package/dist/server/eventsEndpoint.d.ts +48 -0
  134. package/dist/server/eventsEndpoint.d.ts.map +1 -0
  135. package/dist/server/eventsEndpoint.js +145 -0
  136. package/dist/server/everyApp.d.ts +40 -11
  137. package/dist/server/everyApp.d.ts.map +1 -1
  138. package/dist/server/everyApp.js +116 -6
  139. package/dist/server/fromMcpTools.d.ts +89 -0
  140. package/dist/server/fromMcpTools.d.ts.map +1 -0
  141. package/dist/server/fromMcpTools.js +192 -0
  142. package/dist/server/gatewayFetch.d.ts +42 -18
  143. package/dist/server/gatewayFetch.d.ts.map +1 -1
  144. package/dist/server/gatewayFetch.js +138 -19
  145. package/dist/server/gatewayUrl.d.ts +13 -0
  146. package/dist/server/gatewayUrl.d.ts.map +1 -0
  147. package/dist/server/gatewayUrl.js +34 -0
  148. package/dist/server/index.d.ts +16 -4
  149. package/dist/server/index.d.ts.map +1 -1
  150. package/dist/server/index.js +13 -3
  151. package/dist/server/integrationFetch.d.ts +92 -73
  152. package/dist/server/integrationFetch.d.ts.map +1 -1
  153. package/dist/server/integrationFetch.js +206 -319
  154. package/dist/server/listMembers.d.ts +1 -1
  155. package/dist/server/listMembers.d.ts.map +1 -1
  156. package/dist/server/listMembers.js +8 -11
  157. package/dist/server/refusal.d.ts +55 -0
  158. package/dist/server/refusal.d.ts.map +1 -0
  159. package/dist/server/refusal.js +193 -0
  160. package/dist/server/sendEmail.d.ts +1 -1
  161. package/dist/server/sendEmail.d.ts.map +1 -1
  162. package/dist/server/sendEmail.js +7 -7
  163. package/dist/server/tools.d.ts +7 -2
  164. package/dist/server/tools.d.ts.map +1 -1
  165. package/dist/server/tools.js +2 -1
  166. package/dist/server/toolsEndpoint.d.ts +3 -1
  167. package/dist/server/toolsEndpoint.d.ts.map +1 -1
  168. package/dist/server/toolsEndpoint.js +19 -7
  169. package/dist/server/webhooks.d.ts +56 -0
  170. package/dist/server/webhooks.d.ts.map +1 -0
  171. package/dist/server/webhooks.js +205 -0
  172. package/dist/tanstack-start/index.d.ts +9 -0
  173. package/dist/tanstack-start/index.d.ts.map +1 -0
  174. package/dist/tanstack-start/index.js +29 -0
  175. package/dist/testing/index.d.ts +93 -0
  176. package/dist/testing/index.d.ts.map +1 -0
  177. package/dist/testing/index.js +156 -0
  178. package/package.json +35 -14
  179. package/dist/server/authorizeUrl.d.ts +0 -43
  180. package/dist/server/authorizeUrl.d.ts.map +0 -1
  181. package/dist/server/authorizeUrl.js +0 -110
@@ -0,0 +1,294 @@
1
+ /**
2
+ * `gatewayAiBinding(env)`: the Gateway's AI providers as a Workers AI
3
+ * binding, for code written against `env.AI.run(model, inputs, options)`.
4
+ *
5
+ * An app on Every App needs no `ai` binding of its own to call models: the
6
+ * Gateway holds the keys, counts the calls and keeps the app to `ai.models`.
7
+ * Code that already calls `env.AI.run()` (upstream code a port keeps, the
8
+ * Agents SDK, `workers-ai-provider`) gets this object in its place:
9
+ *
10
+ * const AI = gatewayAiBinding(env, { for: request });
11
+ * await AI.run("@cf/meta/llama-3.1-8b-instruct-fast", { messages });
12
+ *
13
+ * Each `run()` becomes an OpenAI-compatible call on the provider's route
14
+ * through the Gateway (`createGatewayFetch`): text generation (`messages`
15
+ * or `prompt`) on its chat completions route, text embeddings (`text`) on
16
+ * its embeddings route, answered in Workers AI's shapes (`{ response,
17
+ * tool_calls, usage }`, `{ shape, data }`, or a stream of `data:
18
+ * {"response": …}` events with `stream: true`). In local development
19
+ * without a Gateway, the developer's own key in `.dev.vars` is used, as
20
+ * `createGatewayFetch` does (WORKERS_AI_API_TOKEN and CLOUDFLARE_ACCOUNT_ID
21
+ * for Workers AI).
22
+ */
23
+ import { AI_DEFAULT_MODEL, AI_DEFAULT_PROVIDER, AI_DEFAULT_ROUTES, ConfigurationError, devAiProvider, } from "../internal/index.js";
24
+ import { BINDING_ORIGIN, createGatewayFetch } from "./gatewayFetch.js";
25
+ import { GatewayRefusalError, gatewayRefusal } from "./refusal.js";
26
+ /**
27
+ * A Workers AI binding (`Ai`) over the Gateway: pass it where code takes
28
+ * `env.AI`. `run()` covers text generation (messages or prompt; tools;
29
+ * `stream: true`; `returnRawResponse`, which returns the provider's
30
+ * OpenAI-shaped `Response`) and text embeddings; the binding's other
31
+ * methods (`gateway()`, `autorag()`, `models()`, `toMarkdown()`, …) and
32
+ * other tasks (images, audio, batches, websockets) throw
33
+ * `ConfigurationError`. `options.gateway` is ignored: the Gateway's
34
+ * operator decides whether calls go through Cloudflare AI Gateway
35
+ * (`everyapp ai gateway on`).
36
+ *
37
+ * Errors are thrown as a binding throws them: `GatewayRefusalError` when
38
+ * the Gateway refused (`ai_provider_not_configured`, `model_not_allowed`,
39
+ * …), `ConfigurationError` where createGatewayFetch throws one (no binding
40
+ * and no dev key), and an `Error` naming the status when the provider
41
+ * answered with an error. Nothing is checked when the binding is made, so
42
+ * making one per request costs nothing.
43
+ */
44
+ export function gatewayAiBinding(env, options = {}) {
45
+ const provider = options.provider ?? "workers-ai";
46
+ async function run(model, inputs, runOptions = {}) {
47
+ if (runOptions.websocket) {
48
+ throw unsupported("run() with websocket: true");
49
+ }
50
+ if (!inputs || typeof inputs !== "object") {
51
+ throw unsupported("run() without an inputs object");
52
+ }
53
+ if (Array.isArray(inputs["requests"])) {
54
+ throw unsupported("run() with a batch of requests");
55
+ }
56
+ const embedding = !("messages" in inputs) && !("prompt" in inputs) && "text" in inputs;
57
+ if (!embedding && !("messages" in inputs) && !("prompt" in inputs)) {
58
+ throw unsupported("run() for anything but text generation (messages or prompt) and text embeddings (text)");
59
+ }
60
+ const url = `${BINDING_ORIGIN}/api/ai/${provider}${routeOf(provider, embedding ? "embeddings" : "chat")}`;
61
+ const sent = modelToSend(provider, model, options.models);
62
+ const body = embedding
63
+ ? { model: sent, input: inputs["text"] }
64
+ : chatBody(sent, inputs);
65
+ const gatewayFetch = createGatewayFetch({
66
+ env,
67
+ for: options.for,
68
+ });
69
+ const response = await gatewayFetch(url, {
70
+ method: "POST",
71
+ headers: { "content-type": "application/json" },
72
+ body: JSON.stringify(body),
73
+ ...(runOptions.signal ? { signal: runOptions.signal } : {}),
74
+ });
75
+ if (runOptions.returnRawResponse)
76
+ return response;
77
+ await throwIfFailed(response, provider);
78
+ if (embedding)
79
+ return embeddingOutput(await response.json());
80
+ if (inputs["stream"] === true) {
81
+ if (!response.body)
82
+ return new ReadableStream();
83
+ return response.body.pipeThrough(toWorkersAiStream());
84
+ }
85
+ return textOutput(await response.json());
86
+ }
87
+ const notHere = (method) => () => {
88
+ throw unsupported(`env.AI.${method}()`);
89
+ };
90
+ const binding = {
91
+ aiGatewayLogId: null,
92
+ run,
93
+ gateway: notHere("gateway"),
94
+ autorag: notHere("autorag"),
95
+ aiSearch: notHere("aiSearch"),
96
+ websearch: notHere("websearch"),
97
+ models: notHere("models"),
98
+ toMarkdown: notHere("toMarkdown"),
99
+ };
100
+ // `Ai` is the app's own Workers types' class (it changes with each
101
+ // version); the object above is its working subset, the rest throwing.
102
+ return binding;
103
+ }
104
+ function unsupported(what) {
105
+ return new ConfigurationError(`${what} isn't available through the Every App gateway; call the provider's routes with createGatewayFetch() instead.`);
106
+ }
107
+ /** The provider's path for a chat or an embeddings call, under `/api/ai/<provider>`. */
108
+ function routeOf(provider, kind) {
109
+ if (provider === AI_DEFAULT_PROVIDER)
110
+ return AI_DEFAULT_ROUTES[kind];
111
+ const spec = devAiProvider(provider);
112
+ if (!spec) {
113
+ throw new ConfigurationError(`gatewayAiBinding: ${JSON.stringify(provider)} is not an AI provider of the catalog.`);
114
+ }
115
+ const route = spec[kind];
116
+ if (!route) {
117
+ throw new ConfigurationError(`gatewayAiBinding: ${provider} has no OpenAI-compatible ${kind === "chat" ? "chat completions" : "embeddings"} route.`);
118
+ }
119
+ return route.path;
120
+ }
121
+ function modelToSend(provider, model, models) {
122
+ const mapped = models && Object.hasOwn(models, model) ? models[model] : null;
123
+ if (mapped)
124
+ return mapped;
125
+ if (provider === AI_DEFAULT_PROVIDER && model.startsWith("@cf/")) {
126
+ return AI_DEFAULT_MODEL;
127
+ }
128
+ return model;
129
+ }
130
+ /**
131
+ * Workers AI's text generation inputs as a chat completions body: `prompt`
132
+ * as one user message, legacy tools (`{ name, description, parameters }`)
133
+ * as OpenAI's, everything else (max_tokens, temperature, response_format,
134
+ * stream, …) as it is. Never adds `stream_options` (Workers AI refuses it
135
+ * on a call that doesn't stream).
136
+ */
137
+ function chatBody(model, inputs) {
138
+ const { prompt, messages, tools, raw: _raw, lora: _lora, ...rest } = inputs;
139
+ const body = {
140
+ ...rest,
141
+ model,
142
+ messages: messages ??
143
+ (typeof prompt === "string" ? [{ role: "user", content: prompt }] : []),
144
+ };
145
+ if (Array.isArray(tools))
146
+ body["tools"] = tools.map(openAiTool);
147
+ return body;
148
+ }
149
+ function openAiTool(tool) {
150
+ if (!isRecord(tool) || "function" in tool)
151
+ return tool;
152
+ const { name, description, parameters } = tool;
153
+ return {
154
+ type: "function",
155
+ function: {
156
+ name,
157
+ ...(description === undefined ? {} : { description }),
158
+ ...(parameters === undefined ? {} : { parameters }),
159
+ },
160
+ };
161
+ }
162
+ async function throwIfFailed(response, provider) {
163
+ if (response.ok)
164
+ return;
165
+ const refusal = await gatewayRefusal(response.clone());
166
+ if (refusal)
167
+ throw new GatewayRefusalError(refusal);
168
+ const text = await response.text().catch(() => "");
169
+ throw new Error(`AI (${provider}) answered ${response.status}${text ? `: ${text.slice(0, 500)}` : ""}`);
170
+ }
171
+ /** A chat completion as Workers AI answers text generation. */
172
+ function textOutput(body) {
173
+ const choice = firstChoice(body);
174
+ const message = isRecord(choice?.["message"]) ? choice["message"] : {};
175
+ const toolCalls = workersAiToolCalls(message["tool_calls"]);
176
+ const usage = isRecord(body) ? body["usage"] : undefined;
177
+ return {
178
+ // Workers AI may send an all-digit reply as a JSON number.
179
+ response: textOf(message["content"]),
180
+ ...(toolCalls ? { tool_calls: toolCalls } : {}),
181
+ ...(isRecord(usage) ? { usage } : {}),
182
+ };
183
+ }
184
+ /**
185
+ * OpenAI tool calls as both forms Workers AI answers with: the legacy
186
+ * `{ name, arguments }` (arguments parsed) and the OpenAI-shaped
187
+ * `{ id, type, function }`.
188
+ */
189
+ function workersAiToolCalls(calls) {
190
+ if (!Array.isArray(calls) || calls.length === 0)
191
+ return undefined;
192
+ return calls.filter(isRecord).map((call) => {
193
+ const fn = isRecord(call["function"]) ? call["function"] : {};
194
+ const args = fn["arguments"];
195
+ return {
196
+ ...(call["id"] === undefined ? {} : { id: call["id"] }),
197
+ type: "function",
198
+ function: { name: fn["name"], arguments: args },
199
+ name: fn["name"],
200
+ arguments: typeof args === "string" ? parsedOr(args) : args,
201
+ };
202
+ });
203
+ }
204
+ /** Embeddings as Workers AI answers them: `{ shape: [rows, dimensions], data }`. */
205
+ function embeddingOutput(body) {
206
+ const rows = isRecord(body) && Array.isArray(body["data"]) ? body["data"] : [];
207
+ const data = rows
208
+ .filter(isRecord)
209
+ .sort((a, b) => Number(a["index"] ?? 0) - Number(b["index"] ?? 0))
210
+ .map((row) => Array.isArray(row["embedding"]) ? row["embedding"] : []);
211
+ return { shape: [data.length, data[0]?.length ?? 0], data };
212
+ }
213
+ /**
214
+ * An OpenAI chat completions event stream as Workers AI streams text:
215
+ * one `data: {"response": "<token>"}` event per chunk with text (tool call
216
+ * deltas and usage alongside), then `data: [DONE]`.
217
+ */
218
+ function toWorkersAiStream() {
219
+ const decoder = new TextDecoder();
220
+ const encoder = new TextEncoder();
221
+ let pending = "";
222
+ const convert = (line) => {
223
+ const match = /^data:[ ]?(.*?)\r?$/s.exec(line);
224
+ if (!match)
225
+ return "";
226
+ const data = match[1].trim();
227
+ if (data === "[DONE]")
228
+ return "data: [DONE]\n\n";
229
+ let chunk;
230
+ try {
231
+ chunk = JSON.parse(data);
232
+ }
233
+ catch {
234
+ return "";
235
+ }
236
+ const choice = firstChoice(chunk);
237
+ const delta = isRecord(choice?.["delta"]) ? choice["delta"] : {};
238
+ const usage = isRecord(chunk) ? chunk["usage"] : undefined;
239
+ const toolCalls = Array.isArray(delta["tool_calls"])
240
+ ? delta["tool_calls"]
241
+ : undefined;
242
+ const event = {
243
+ response: textOf(delta["content"]),
244
+ ...(toolCalls ? { tool_calls: toolCalls } : {}),
245
+ ...(isRecord(usage) ? { usage } : {}),
246
+ };
247
+ if (!event.response && !toolCalls && !isRecord(usage))
248
+ return "";
249
+ return `data: ${JSON.stringify(event)}\n\n`;
250
+ };
251
+ return new TransformStream({
252
+ transform(bytes, controller) {
253
+ pending += decoder.decode(bytes, { stream: true });
254
+ const end = pending.lastIndexOf("\n");
255
+ if (end === -1)
256
+ return;
257
+ const lines = pending.slice(0, end).split("\n");
258
+ pending = pending.slice(end + 1);
259
+ const out = lines.map(convert).join("");
260
+ if (out)
261
+ controller.enqueue(encoder.encode(out));
262
+ },
263
+ flush(controller) {
264
+ pending += decoder.decode();
265
+ const out = convert(pending);
266
+ if (out)
267
+ controller.enqueue(encoder.encode(out));
268
+ },
269
+ });
270
+ }
271
+ function firstChoice(body) {
272
+ if (!isRecord(body) || !Array.isArray(body["choices"]))
273
+ return undefined;
274
+ const choice = body["choices"][0];
275
+ return isRecord(choice) ? choice : undefined;
276
+ }
277
+ function textOf(content) {
278
+ if (typeof content === "string")
279
+ return content;
280
+ if (typeof content === "number")
281
+ return String(content);
282
+ return "";
283
+ }
284
+ function parsedOr(text) {
285
+ try {
286
+ return JSON.parse(text);
287
+ }
288
+ catch {
289
+ return text;
290
+ }
291
+ }
292
+ function isRecord(value) {
293
+ return typeof value === "object" && value !== null && !Array.isArray(value);
294
+ }
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Whom a call through the gateway is for, which decides the organization
3
- * it counts against:
3
+ * it counts against. Left out, it comes from the request context: the
4
+ * request `everyApp()` is serving (its person, while the identity is good),
5
+ * or the organization `withOrganization()` named. Given explicitly:
4
6
  *
5
7
  * - the inbound `Request`: its person, by the identity the gateway put on
6
8
  * it (forwarded as is);
@@ -10,10 +12,19 @@
10
12
  * checks is active and has a live member. An organization's own app may
11
13
  * leave it out (the gateway knows its organization) and may only name
12
14
  * its own.
15
+ * - `{ background: true, organizationId, userId }`: a call to an OAuth
16
+ * provider's API (`integrationFetch`) on the connection member `userId`
17
+ * made in this app, at any time, even while serving another person's
18
+ * request: the call carries that member (`x-everyapp-user`) and never
19
+ * the identity of the person being served. Only an organization's own
20
+ * app may name a member, and only one who may still open the app. No
21
+ * identity for the member reaches the app. Other gateway calls ignore
22
+ * `userId`.
13
23
  */
14
24
  export type GatewayCallFor = Request | {
15
25
  background: true;
16
26
  organizationId?: string;
27
+ userId?: string;
17
28
  };
18
29
  /**
19
30
  * A gateway call that doesn't say whom it is for where it must: from a
@@ -24,11 +35,13 @@ export declare class AttributionError extends Error {
24
35
  constructor(message: string);
25
36
  }
26
37
  /**
27
- * Check `target` before any call is made: a shared app's Worker must pass
28
- * the request or name the organization, and anything else passed must be
29
- * one of the two shapes.
38
+ * Whom the call is for, checked before any call is made: `target` as the
39
+ * app passed it, else the request context's answer
40
+ * ({@link attributionFromContext}; a `RequestContextError` outside every
41
+ * context). A shared app's Worker must end up with the request or an
42
+ * organization, and anything passed must be one of the two shapes.
30
43
  */
31
- export declare function checkAttribution(env: object, target: GatewayCallFor | undefined, what: string): void;
44
+ export declare function attributionFor(env: object, target: GatewayCallFor | undefined, what: string): GatewayCallFor | undefined;
32
45
  /**
33
46
  * Put the call's attribution on its headers: the request's identity, or the
34
47
  * background work's organization, and never one the app set itself.
@@ -1 +1 @@
1
- {"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../../src/server/attribution.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GACtB,OAAO,GACP;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,0BAA0B;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,GACX,IAAI,CAsBN;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,cAAc,GAAG,SAAS,GACjC,IAAI,CAUN;AASD,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAO1D"}
1
+ {"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../../src/server/attribution.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,cAAc,GACtB,OAAO,GACP;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,0BAA0B;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,GACX,cAAc,GAAG,SAAS,CAI5B;AAmCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,cAAc,GAAG,SAAS,GACjC,IAAI,CAYN;AASD,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAO1D"}
@@ -1,4 +1,5 @@
1
- import { IDENTITY_HEADER, ORG_HEADER, SHARED_ENV } from "../internal/index.js";
1
+ import { IDENTITY_HEADER, ORG_HEADER, SHARED_ENV, USER_HEADER, } from "../internal/index.js";
2
+ import { attributionFromContext } from "./context.js";
2
3
  /**
3
4
  * A gateway call that doesn't say whom it is for where it must: from a
4
5
  * shared app's Worker, with neither the request nor an organization.
@@ -11,15 +12,25 @@ export class AttributionError extends Error {
11
12
  }
12
13
  }
13
14
  /**
14
- * Check `target` before any call is made: a shared app's Worker must pass
15
- * the request or name the organization, and anything else passed must be
16
- * one of the two shapes.
15
+ * Whom the call is for, checked before any call is made: `target` as the
16
+ * app passed it, else the request context's answer
17
+ * ({@link attributionFromContext}; a `RequestContextError` outside every
18
+ * context). A shared app's Worker must end up with the request or an
19
+ * organization, and anything passed must be one of the two shapes.
17
20
  */
18
- export function checkAttribution(env, target, what) {
21
+ export function attributionFor(env, target, what) {
22
+ const resolved = target === undefined ? attributionFromContext(what) : target;
23
+ checkAttribution(env, resolved, what);
24
+ return resolved;
25
+ }
26
+ function checkAttribution(env, target, what) {
19
27
  if (target !== undefined && !isRequest(target)) {
20
28
  if (target === null || typeof target !== "object" || !target.background) {
21
29
  throw new AttributionError(`${what}: \`for\` is the inbound Request or { background: true, organizationId }.`);
22
30
  }
31
+ if (target.userId !== undefined && !nonEmpty(target.userId)) {
32
+ throw new AttributionError(`${what}: { background: true, userId } needs a non-empty user id.`);
33
+ }
23
34
  if (target.organizationId !== undefined &&
24
35
  !nonEmpty(target.organizationId)) {
25
36
  throw new AttributionError(`${what}: { background: true, organizationId } needs a non-empty organization id.`);
@@ -38,6 +49,7 @@ export function checkAttribution(env, target, what) {
38
49
  export function applyAttribution(headers, target) {
39
50
  headers.delete(IDENTITY_HEADER);
40
51
  headers.delete(ORG_HEADER);
52
+ headers.delete(USER_HEADER);
41
53
  if (target === undefined)
42
54
  return;
43
55
  if (isRequest(target)) {
@@ -48,6 +60,8 @@ export function applyAttribution(headers, target) {
48
60
  }
49
61
  if (target.organizationId)
50
62
  headers.set(ORG_HEADER, target.organizationId);
63
+ if (target.userId)
64
+ headers.set(USER_HEADER, target.userId.trim());
51
65
  }
52
66
  function isSharedWorker(env) {
53
67
  const value = env[SHARED_ENV];
@@ -0,0 +1,16 @@
1
+ export interface ConnectUrlOptions {
2
+ /** The Worker env: `GATEWAY_URL` and the app's id, which every deployed Worker and `everyapp dev` set. */
3
+ env: object;
4
+ /** The OAuth provider's id (`google-workspace`, `github`). */
5
+ provider: string;
6
+ /**
7
+ * Where to send the person afterwards: the app's own URL (absolute) or
8
+ * a path on the Gateway. Absent: Settings → Connected accounts.
9
+ */
10
+ returnTo?: string;
11
+ }
12
+ /** The path of the connect page for `provider` on the Gateway, without the app. */
13
+ export declare function connectPath(provider: string): string;
14
+ /** The absolute URL that connects the person to `provider` in this app. */
15
+ export declare function connectUrl({ env, provider, returnTo, }: ConnectUrlOptions): string;
16
+ //# sourceMappingURL=connectUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectUrl.d.ts","sourceRoot":"","sources":["../../src/server/connectUrl.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,iBAAiB;IAChC,0GAA0G;IAC1G,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,QAAQ,EACR,QAAQ,GACT,EAAE,iBAAiB,GAAG,MAAM,CAwB5B"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Where an app sends a person to connect an OAuth provider the Gateway
3
+ * runs the flow for (docs/design/2026-11-oauth-simplify.md):
4
+ * `<gateway>/connect/<id>?app=<this app>&return=<back here>`. A
5
+ * connection belongs to one person and one app: the Gateway checks the
6
+ * session and that the person may open the app, asks the provider for
7
+ * exactly the scopes the app's `everyapp.config.ts` lists (plus the ones
8
+ * that name the account), finishes on its own callback and sends the
9
+ * browser back to `returnTo`, which must be a path on the Gateway or an
10
+ * https URL on one of its app hosts (in local dev also localhost).
11
+ */
12
+ import { APP_ID_ENV, ConfigurationError } from "../internal/index.js";
13
+ /** The path of the connect page for `provider` on the Gateway, without the app. */
14
+ export function connectPath(provider) {
15
+ return `/connect/${encodeURIComponent(provider)}`;
16
+ }
17
+ /** The absolute URL that connects the person to `provider` in this app. */
18
+ export function connectUrl({ env, provider, returnTo, }) {
19
+ const values = env;
20
+ const gateway = values["GATEWAY_URL"];
21
+ if (typeof gateway !== "string" || !gateway.trim()) {
22
+ throw new ConfigurationError("connectUrl() needs GATEWAY_URL in the Worker env: the deploy sets it, and everyapp dev sets it to the local gateway.");
23
+ }
24
+ let base;
25
+ try {
26
+ base = new URL(gateway.trim());
27
+ }
28
+ catch {
29
+ throw new ConfigurationError(`GATEWAY_URL isn't a URL: ${gateway}`);
30
+ }
31
+ const app = values[APP_ID_ENV];
32
+ if (typeof app !== "string" || !app.trim()) {
33
+ throw new ConfigurationError(`connectUrl() needs ${APP_ID_ENV} in the Worker env: a connection belongs to one app, and the deploy and everyapp dev set it.`);
34
+ }
35
+ const url = new URL(connectPath(provider), base.origin);
36
+ url.searchParams.set("app", app.trim());
37
+ if (returnTo)
38
+ url.searchParams.set("return", returnTo);
39
+ return url.toString();
40
+ }
@@ -0,0 +1,68 @@
1
+ import type { EveryAppUser } from "../internal/index.js";
2
+ /** What `everyApp()` keeps while it serves one request. */
3
+ export interface ServedRequest {
4
+ request: Request;
5
+ /** The verified user, or null on a public route. */
6
+ user: EveryAppUser | null;
7
+ /** When the forwarded identity expires (epoch ms); null without one. */
8
+ expiresAt: number | null;
9
+ }
10
+ /**
11
+ * A forwarded identity lives 120 seconds. One this close to its expiry could
12
+ * expire before the Gateway checks it (a 401 the call can't recover from),
13
+ * so work still running then goes on as the organization's background work.
14
+ */
15
+ export declare const IDENTITY_EXPIRY_MARGIN_MS = 15000;
16
+ /**
17
+ * Code `request_context_missing`: `currentUser()` or a Gateway call with
18
+ * no `for`, outside any request `everyApp()` serves and outside
19
+ * `withOrganization()`. Work no request started names its organization.
20
+ */
21
+ export declare class RequestContextError extends Error {
22
+ readonly code = "request_context_missing";
23
+ constructor(message: string);
24
+ }
25
+ /**
26
+ * Code `attribution_organization_mismatch`: `withOrganization()` named
27
+ * another organization than the request being served belongs to.
28
+ */
29
+ export declare class OrganizationMismatchError extends Error {
30
+ readonly code = "attribution_organization_mismatch";
31
+ constructor(message: string);
32
+ }
33
+ /** Serve `fn` for one request: `everyApp()` calls this; apps don't. */
34
+ export declare function serveRequest<T>(served: ServedRequest, fn: () => T): T;
35
+ /**
36
+ * Run `fn` as `organizationId`'s background work: every Gateway call made
37
+ * inside it (and in what it awaits or schedules) is that organization's,
38
+ * with no person behind it. For a Workflow step, a cron, a queue consumer or
39
+ * a Durable Object alarm, which no request started. An organization's own
40
+ * app names its own (`env.EVERY_APP_ORG_ID`); a shared app's Worker names
41
+ * the one the work is for. Inside a request another organization's person
42
+ * is driving, it throws {@link OrganizationMismatchError}; inside that
43
+ * organization's own request, the person stays attributed while their
44
+ * identity is good.
45
+ */
46
+ export declare function withOrganization<T>(organizationId: string, fn: () => T): T;
47
+ /**
48
+ * The verified user of the request being served: an `EveryAppUser`, or
49
+ * `null` on a public route and inside `withOrganization()`'s background
50
+ * work. Throws {@link RequestContextError} outside both: there is no one
51
+ * to be, and a handler that isn't under `everyApp()` is a bug.
52
+ */
53
+ export declare function currentUser(): EveryAppUser | null;
54
+ /**
55
+ * Whom a Gateway call made now is for, from the context:
56
+ *
57
+ * - the request being served, while its identity is good;
58
+ * - else that organization's background work (the identity is about to
59
+ * expire, or `withOrganization()` named it);
60
+ * - `undefined` on a public route of an organization's own app, which the
61
+ * Gateway attributes to the app's organization; and
62
+ * - a {@link RequestContextError} outside every context.
63
+ */
64
+ export declare function attributionFromContext(what: string, now?: number): Request | {
65
+ background: true;
66
+ organizationId?: string;
67
+ } | undefined;
68
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/server/context.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,2DAA2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,oDAAoD;IACpD,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IAC1B,wEAAwE;IACxE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAUD;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,6BAA6B;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,IAAI,uCAAuC;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAErE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAc1E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,IAAI,YAAY,GAAG,IAAI,CAQjD;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,GAAG,GAAE,MAAmB,GACvB,OAAO,GAAG;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAoBrE"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The request context: whom the app is serving, kept for the whole of a
3
+ * request's work (every `await`, `ctx.waitUntil()`, a streaming body) in an
4
+ * `AsyncLocalStorage`, which every Every App Worker has (`nodejs_compat`).
5
+ *
6
+ * `everyApp()` opens it around the handler and around each tool call, with
7
+ * the verified user and the request the Gateway forwarded. Deeper in the
8
+ * app, `currentUser()` reads the user without the request, and every call
9
+ * back to the Gateway (`integrationFetch`, `ai`, `sendEmail`, `emit`, ...)
10
+ * is attributed to that person without a `for`.
11
+ *
12
+ * Work no request started (a cron, a Workflow step, a queue consumer, a
13
+ * Durable Object alarm) has no context of its own: it runs under
14
+ * `withOrganization(organizationId, fn)`, which makes every Gateway call
15
+ * inside it that organization's background work. A Gateway call with
16
+ * neither is refused before it is made ({@link RequestContextError}): the
17
+ * SDK never acts as nobody.
18
+ */
19
+ import { AsyncLocalStorage } from "node:async_hooks";
20
+ const scopes = new AsyncLocalStorage();
21
+ /**
22
+ * A forwarded identity lives 120 seconds. One this close to its expiry could
23
+ * expire before the Gateway checks it (a 401 the call can't recover from),
24
+ * so work still running then goes on as the organization's background work.
25
+ */
26
+ export const IDENTITY_EXPIRY_MARGIN_MS = 15_000;
27
+ /**
28
+ * Code `request_context_missing`: `currentUser()` or a Gateway call with
29
+ * no `for`, outside any request `everyApp()` serves and outside
30
+ * `withOrganization()`. Work no request started names its organization.
31
+ */
32
+ export class RequestContextError extends Error {
33
+ code = "request_context_missing";
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "RequestContextError";
37
+ }
38
+ }
39
+ /**
40
+ * Code `attribution_organization_mismatch`: `withOrganization()` named
41
+ * another organization than the request being served belongs to.
42
+ */
43
+ export class OrganizationMismatchError extends Error {
44
+ code = "attribution_organization_mismatch";
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = "OrganizationMismatchError";
48
+ }
49
+ }
50
+ /** Serve `fn` for one request: `everyApp()` calls this; apps don't. */
51
+ export function serveRequest(served, fn) {
52
+ return scopes.run({ served }, fn);
53
+ }
54
+ /**
55
+ * Run `fn` as `organizationId`'s background work: every Gateway call made
56
+ * inside it (and in what it awaits or schedules) is that organization's,
57
+ * with no person behind it. For a Workflow step, a cron, a queue consumer or
58
+ * a Durable Object alarm, which no request started. An organization's own
59
+ * app names its own (`env.EVERY_APP_ORG_ID`); a shared app's Worker names
60
+ * the one the work is for. Inside a request another organization's person
61
+ * is driving, it throws {@link OrganizationMismatchError}; inside that
62
+ * organization's own request, the person stays attributed while their
63
+ * identity is good.
64
+ */
65
+ export function withOrganization(organizationId, fn) {
66
+ if (typeof organizationId !== "string" || organizationId.trim() === "") {
67
+ throw new TypeError("withOrganization(organizationId, fn) needs a non-empty organization id (an organization's own app passes env.EVERY_APP_ORG_ID).");
68
+ }
69
+ const parent = scopes.getStore();
70
+ const served = parent?.served;
71
+ if (served?.user && served.user.orgId !== organizationId) {
72
+ throw new OrganizationMismatchError(`withOrganization("${organizationId}") was called while serving a request of organization "${served.user.orgId}".`);
73
+ }
74
+ return scopes.run({ ...parent, organizationId }, fn);
75
+ }
76
+ /**
77
+ * The verified user of the request being served: an `EveryAppUser`, or
78
+ * `null` on a public route and inside `withOrganization()`'s background
79
+ * work. Throws {@link RequestContextError} outside both: there is no one
80
+ * to be, and a handler that isn't under `everyApp()` is a bug.
81
+ */
82
+ export function currentUser() {
83
+ const scope = scopes.getStore();
84
+ if (!scope) {
85
+ throw new RequestContextError("currentUser() was called outside a request everyApp() serves and outside withOrganization(): there is no user here.");
86
+ }
87
+ return scope.served?.user ?? null;
88
+ }
89
+ /**
90
+ * Whom a Gateway call made now is for, from the context:
91
+ *
92
+ * - the request being served, while its identity is good;
93
+ * - else that organization's background work (the identity is about to
94
+ * expire, or `withOrganization()` named it);
95
+ * - `undefined` on a public route of an organization's own app, which the
96
+ * Gateway attributes to the app's organization; and
97
+ * - a {@link RequestContextError} outside every context.
98
+ */
99
+ export function attributionFromContext(what, now = Date.now()) {
100
+ const scope = scopes.getStore();
101
+ if (!scope) {
102
+ throw new RequestContextError(`${what} was called with no \`for\`, outside a request everyApp() serves and outside withOrganization(): the call would be nobody's. Pass for: request, or run the work under withOrganization(organizationId, fn).`);
103
+ }
104
+ const served = scope.served;
105
+ if (served?.user) {
106
+ const fresh = served.expiresAt === null ||
107
+ now < served.expiresAt - IDENTITY_EXPIRY_MARGIN_MS;
108
+ // withOrganization() refused any other organization than the request's.
109
+ if (fresh)
110
+ return served.request;
111
+ return { background: true, organizationId: served.user.orgId };
112
+ }
113
+ if (scope.organizationId) {
114
+ return { background: true, organizationId: scope.organizationId };
115
+ }
116
+ return undefined;
117
+ }