@every-app/sdk 0.2.0 → 0.3.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +609 -3
- package/dist/cloudflare/catalogSettings.generated.d.ts +81 -0
- package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
- package/dist/cloudflare/catalogSettings.generated.js +174 -0
- package/dist/cloudflare/index.d.ts +23 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +24 -0
- package/dist/cloudflare/sentinel.d.ts +127 -0
- package/dist/cloudflare/sentinel.d.ts.map +1 -0
- package/dist/cloudflare/sentinel.js +85 -0
- package/dist/cloudflare/worker.d.ts +805 -0
- package/dist/cloudflare/worker.d.ts.map +1 -0
- package/dist/cloudflare/worker.js +997 -0
- package/dist/dev/index.d.ts +8 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +7 -0
- package/dist/dev/keypair.d.ts +26 -0
- package/dist/dev/keypair.d.ts.map +1 -0
- package/dist/dev/keypair.js +62 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/internal/aiDefault.d.ts +204 -0
- package/dist/internal/aiDefault.d.ts.map +1 -0
- package/dist/internal/aiDefault.js +334 -0
- package/dist/internal/aiProviders.d.ts +72 -0
- package/dist/internal/aiProviders.d.ts.map +1 -0
- package/dist/internal/aiProviders.js +233 -0
- package/dist/internal/appSettings.d.ts +315 -0
- package/dist/internal/appSettings.d.ts.map +1 -0
- package/dist/internal/appSettings.js +549 -0
- package/dist/internal/audit.d.ts +108 -0
- package/dist/internal/audit.d.ts.map +1 -0
- package/dist/internal/audit.js +235 -0
- package/dist/internal/capabilities.d.ts +3174 -0
- package/dist/internal/capabilities.d.ts.map +1 -0
- package/dist/internal/capabilities.js +1110 -0
- package/dist/internal/catalog/commerce.d.ts +394 -0
- package/dist/internal/catalog/commerce.d.ts.map +1 -0
- package/dist/internal/catalog/commerce.js +477 -0
- package/dist/internal/catalog/comms.d.ts +375 -0
- package/dist/internal/catalog/comms.d.ts.map +1 -0
- package/dist/internal/catalog/comms.js +520 -0
- package/dist/internal/catalog/index.d.ts +1973 -0
- package/dist/internal/catalog/index.d.ts.map +1 -0
- package/dist/internal/catalog/index.js +19 -0
- package/dist/internal/catalog/inference.d.ts +393 -0
- package/dist/internal/catalog/inference.d.ts.map +1 -0
- package/dist/internal/catalog/inference.js +460 -0
- package/dist/internal/catalog/infra.d.ts +423 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -0
- package/dist/internal/catalog/infra.js +543 -0
- package/dist/internal/catalog/search.d.ts +400 -0
- package/dist/internal/catalog/search.d.ts.map +1 -0
- package/dist/internal/catalog/search.js +463 -0
- package/dist/internal/devProviders.d.ts +49 -0
- package/dist/internal/devProviders.d.ts.map +1 -0
- package/dist/internal/devProviders.js +80 -0
- package/dist/internal/email.d.ts +280 -0
- package/dist/internal/email.d.ts.map +1 -0
- package/dist/internal/email.js +613 -0
- package/dist/internal/events.d.ts +193 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +367 -0
- package/dist/internal/gatewaySettings.d.ts +83 -0
- package/dist/internal/gatewaySettings.d.ts.map +1 -0
- package/dist/internal/gatewaySettings.js +215 -0
- package/dist/internal/identity.d.ts +57 -5
- package/dist/internal/identity.d.ts.map +1 -1
- package/dist/internal/identity.js +99 -22
- package/dist/internal/index.d.ts +22 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +22 -1
- package/dist/internal/members.d.ts +36 -0
- package/dist/internal/members.d.ts.map +1 -0
- package/dist/internal/members.js +7 -0
- package/dist/internal/oauthClient.d.ts +61 -0
- package/dist/internal/oauthClient.d.ts.map +1 -0
- package/dist/internal/oauthClient.js +133 -0
- package/dist/internal/oauthPresets.d.ts +33 -0
- package/dist/internal/oauthPresets.d.ts.map +1 -0
- package/dist/internal/oauthPresets.js +1064 -0
- package/dist/internal/oauthScopes.d.ts +172 -0
- package/dist/internal/oauthScopes.d.ts.map +1 -0
- package/dist/internal/oauthScopes.js +510 -0
- package/dist/internal/oauthSpec.d.ts +102 -0
- package/dist/internal/oauthSpec.d.ts.map +1 -0
- package/dist/internal/oauthSpec.js +789 -0
- package/dist/internal/organizationName.d.ts +18 -0
- package/dist/internal/organizationName.d.ts.map +1 -0
- package/dist/internal/organizationName.js +24 -0
- package/dist/internal/protocol.d.ts +150 -3
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +163 -2
- package/dist/internal/providerApps.d.ts +75 -0
- package/dist/internal/providerApps.d.ts.map +1 -0
- package/dist/internal/providerApps.js +59 -0
- package/dist/internal/providerRegistry.d.ts +218 -0
- package/dist/internal/providerRegistry.d.ts.map +1 -0
- package/dist/internal/providerRegistry.js +432 -0
- package/dist/internal/providerSpec.d.ts +66 -0
- package/dist/internal/providerSpec.d.ts.map +1 -0
- package/dist/internal/providerSpec.js +1154 -0
- package/dist/internal/remoteMcp.d.ts +188 -0
- package/dist/internal/remoteMcp.d.ts.map +1 -0
- package/dist/internal/remoteMcp.js +543 -0
- package/dist/internal/signInProviders.d.ts +104 -0
- package/dist/internal/signInProviders.d.ts.map +1 -0
- package/dist/internal/signInProviders.js +245 -0
- package/dist/internal/tools.d.ts +115 -0
- package/dist/internal/tools.d.ts.map +1 -0
- package/dist/internal/tools.js +80 -0
- package/dist/manifest.d.ts +238 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +30 -0
- package/dist/server/ai.d.ts +111 -0
- package/dist/server/ai.d.ts.map +1 -0
- package/dist/server/ai.js +145 -0
- package/dist/server/aiBinding.d.ts +68 -0
- package/dist/server/aiBinding.d.ts.map +1 -0
- package/dist/server/aiBinding.js +294 -0
- package/dist/server/attribution.d.ts +43 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +75 -0
- package/dist/server/authorizeUrl.d.ts +43 -0
- package/dist/server/authorizeUrl.d.ts.map +1 -0
- package/dist/server/authorizeUrl.js +106 -0
- package/dist/server/connectUrl.d.ts +21 -0
- package/dist/server/connectUrl.d.ts.map +1 -0
- package/dist/server/connectUrl.js +37 -0
- package/dist/server/context.d.ts +68 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +117 -0
- package/dist/server/emailBinding.d.ts +72 -0
- package/dist/server/emailBinding.d.ts.map +1 -0
- package/dist/server/emailBinding.js +124 -0
- package/dist/server/emit.d.ts +63 -0
- package/dist/server/emit.d.ts.map +1 -0
- package/dist/server/emit.js +101 -0
- package/dist/server/env.d.ts +44 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +1 -0
- package/dist/server/everyApp.d.ts +65 -19
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +195 -30
- package/dist/server/fromMcpTools.d.ts +89 -0
- package/dist/server/fromMcpTools.d.ts.map +1 -0
- package/dist/server/fromMcpTools.js +192 -0
- package/dist/server/gatewayFetch.d.ts +54 -14
- package/dist/server/gatewayFetch.d.ts.map +1 -1
- package/dist/server/gatewayFetch.js +201 -37
- package/dist/server/gatewayUrl.d.ts +13 -0
- package/dist/server/gatewayUrl.d.ts.map +1 -0
- package/dist/server/gatewayUrl.js +34 -0
- package/dist/server/index.d.ts +23 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +19 -2
- package/dist/server/integrationFetch.d.ts +162 -0
- package/dist/server/integrationFetch.d.ts.map +1 -0
- package/dist/server/integrationFetch.js +688 -0
- package/dist/server/listMembers.d.ts +32 -0
- package/dist/server/listMembers.d.ts.map +1 -0
- package/dist/server/listMembers.js +71 -0
- package/dist/server/nodeServer.d.ts +48 -0
- package/dist/server/nodeServer.d.ts.map +1 -0
- package/dist/server/nodeServer.js +45 -0
- package/dist/server/refusal.d.ts +55 -0
- package/dist/server/refusal.d.ts.map +1 -0
- package/dist/server/refusal.js +193 -0
- package/dist/server/sendEmail.d.ts +39 -0
- package/dist/server/sendEmail.d.ts.map +1 -0
- package/dist/server/sendEmail.js +114 -0
- package/dist/server/tools.d.ts +184 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +347 -0
- package/dist/server/toolsEndpoint.d.ts +32 -0
- package/dist/server/toolsEndpoint.d.ts.map +1 -0
- package/dist/server/toolsEndpoint.js +237 -0
- package/dist/tanstack-start/index.d.ts +9 -0
- package/dist/tanstack-start/index.d.ts.map +1 -0
- package/dist/tanstack-start/index.js +29 -0
- package/dist/testing/index.d.ts +93 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +156 -0
- package/package.json +42 -8
- package/dist/server/mcp.d.ts +0 -66
- package/dist/server/mcp.d.ts.map +0 -1
- package/dist/server/mcp.js +0 -147
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whom a call through the gateway is for, which decides the organization
|
|
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:
|
|
6
|
+
*
|
|
7
|
+
* - the inbound `Request`: its person, by the identity the gateway put on
|
|
8
|
+
* it (forwarded as is);
|
|
9
|
+
* - `{ background: true, organizationId }`: work no request started (a
|
|
10
|
+
* cron, a Workflow step, a queue). A shared app's Worker serves every
|
|
11
|
+
* organization, so it names the one the work is for, which the gateway
|
|
12
|
+
* checks is active and has a live member. An organization's own app may
|
|
13
|
+
* leave it out (the gateway knows its organization) and may only name
|
|
14
|
+
* its own.
|
|
15
|
+
*/
|
|
16
|
+
export type GatewayCallFor = Request | {
|
|
17
|
+
background: true;
|
|
18
|
+
organizationId?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A gateway call that doesn't say whom it is for where it must: from a
|
|
22
|
+
* shared app's Worker, with neither the request nor an organization.
|
|
23
|
+
*/
|
|
24
|
+
export declare class AttributionError extends Error {
|
|
25
|
+
readonly code = "attribution_required";
|
|
26
|
+
constructor(message: string);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whom the call is for, checked before any call is made: `target` as the
|
|
30
|
+
* app passed it, else the request context's answer
|
|
31
|
+
* ({@link attributionFromContext}; a `RequestContextError` outside every
|
|
32
|
+
* context). A shared app's Worker must end up with the request or an
|
|
33
|
+
* organization, and anything passed must be one of the two shapes.
|
|
34
|
+
*/
|
|
35
|
+
export declare function attributionFor(env: object, target: GatewayCallFor | undefined, what: string): GatewayCallFor | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Put the call's attribution on its headers: the request's identity, or the
|
|
38
|
+
* background work's organization, and never one the app set itself.
|
|
39
|
+
*/
|
|
40
|
+
export declare function applyAttribution(headers: Headers, target: GatewayCallFor | undefined): void;
|
|
41
|
+
/** Whether `for` is the inbound request (rather than background work). */
|
|
42
|
+
export declare function isRequest(value: unknown): value is Request;
|
|
43
|
+
//# sourceMappingURL=attribution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../../src/server/attribution.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;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;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,GACX,cAAc,GAAG,SAAS,CAI5B;AA8BD;;;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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { IDENTITY_HEADER, ORG_HEADER, SHARED_ENV } from "../internal/index.js";
|
|
2
|
+
import { attributionFromContext } from "./context.js";
|
|
3
|
+
/**
|
|
4
|
+
* A gateway call that doesn't say whom it is for where it must: from a
|
|
5
|
+
* shared app's Worker, with neither the request nor an organization.
|
|
6
|
+
*/
|
|
7
|
+
export class AttributionError extends Error {
|
|
8
|
+
code = "attribution_required";
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "AttributionError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
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.
|
|
20
|
+
*/
|
|
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) {
|
|
27
|
+
if (target !== undefined && !isRequest(target)) {
|
|
28
|
+
if (target === null || typeof target !== "object" || !target.background) {
|
|
29
|
+
throw new AttributionError(`${what}: \`for\` is the inbound Request or { background: true, organizationId }.`);
|
|
30
|
+
}
|
|
31
|
+
if (target.organizationId !== undefined &&
|
|
32
|
+
!nonEmpty(target.organizationId)) {
|
|
33
|
+
throw new AttributionError(`${what}: { background: true, organizationId } needs a non-empty organization id.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!isSharedWorker(env))
|
|
37
|
+
return;
|
|
38
|
+
if (target === undefined || (!isRequest(target) && !target.organizationId)) {
|
|
39
|
+
throw new AttributionError(`${what} in a shared app's Worker serves every organization, so it must say whom the call is for: for: request, or for: { background: true, organizationId } for work no request started.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Put the call's attribution on its headers: the request's identity, or the
|
|
44
|
+
* background work's organization, and never one the app set itself.
|
|
45
|
+
*/
|
|
46
|
+
export function applyAttribution(headers, target) {
|
|
47
|
+
headers.delete(IDENTITY_HEADER);
|
|
48
|
+
headers.delete(ORG_HEADER);
|
|
49
|
+
if (target === undefined)
|
|
50
|
+
return;
|
|
51
|
+
if (isRequest(target)) {
|
|
52
|
+
const identity = target.headers.get(IDENTITY_HEADER);
|
|
53
|
+
if (identity)
|
|
54
|
+
headers.set(IDENTITY_HEADER, identity);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (target.organizationId)
|
|
58
|
+
headers.set(ORG_HEADER, target.organizationId);
|
|
59
|
+
}
|
|
60
|
+
function isSharedWorker(env) {
|
|
61
|
+
const value = env[SHARED_ENV];
|
|
62
|
+
return value === "1" || value === "true" || value === true;
|
|
63
|
+
}
|
|
64
|
+
// Duck-typed: a Request from another realm (a test runner, a polyfill) is
|
|
65
|
+
// still the request.
|
|
66
|
+
/** Whether `for` is the inbound request (rather than background work). */
|
|
67
|
+
export function isRequest(value) {
|
|
68
|
+
return (typeof value === "object" &&
|
|
69
|
+
value !== null &&
|
|
70
|
+
"headers" in value &&
|
|
71
|
+
typeof value.headers?.get === "function");
|
|
72
|
+
}
|
|
73
|
+
function nonEmpty(value) {
|
|
74
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type OAuthClientCapabilityId } from "../internal/index.js";
|
|
2
|
+
import type { OAuthClientIntegrationStatus } from "./integrationFetch.js";
|
|
3
|
+
export interface AuthorizeUrlOptions {
|
|
4
|
+
/** The OAuth client, as listed in the manifest's `capabilities`. */
|
|
5
|
+
provider: OAuthClientCapabilityId;
|
|
6
|
+
/** What `integrationStatus()` answered for it, ready and with its `clientId`. */
|
|
7
|
+
status: OAuthClientIntegrationStatus;
|
|
8
|
+
/** One of the redirect URIs the operator registered on the client. */
|
|
9
|
+
redirectUri: string;
|
|
10
|
+
/** The app's own anti-forgery value, checked when the person comes back. */
|
|
11
|
+
state: string;
|
|
12
|
+
/** PKCE: the S256 challenge of the verifier the app keeps for the exchange. */
|
|
13
|
+
codeChallenge?: string;
|
|
14
|
+
/**
|
|
15
|
+
* The provider's extras (`prompt`, `login_hint`, Google's `hd`). Never
|
|
16
|
+
* one of the parameters this helper sets (PKCE's and the provider's own
|
|
17
|
+
* included), nor Google's `include_granted_scopes`.
|
|
18
|
+
*/
|
|
19
|
+
params?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The provider's authorize URL to send a person to, for a client with
|
|
23
|
+
* scope sets (`google-workspace`, `microsoft-365`, `slack`), built from the
|
|
24
|
+
* capability's status: the registry's authorize endpoint (or the instance
|
|
25
|
+
* the status names), `client_id`, `response_type=code`, `redirect_uri`,
|
|
26
|
+
* `state`, and the scopes the status allows this app, which are exactly
|
|
27
|
+
* what the gateway will let the token carry. Each provider's own needs
|
|
28
|
+
* are added: Google's `access_type=offline` (a refresh token), Microsoft's
|
|
29
|
+
* `response_mode=query`, Slack's comma-joined `scope` and its `user_scope`
|
|
30
|
+
* (the status's `userScopes`, when the app's sets include a user set). With
|
|
31
|
+
* `codeChallenge`, PKCE as S256. The app keeps its `state` and verifier
|
|
32
|
+
* for the callback.
|
|
33
|
+
*
|
|
34
|
+
* A client without scope sets is refused: the
|
|
35
|
+
* registry's checklist for one isn't what the gateway holds a token to,
|
|
36
|
+
* so an app builds that URL itself from the status's `clientId`.
|
|
37
|
+
*
|
|
38
|
+
* Throws a `TypeError` naming the fix when the client has no scope sets,
|
|
39
|
+
* when the status isn't ready or has no `clientId`, when the status lacks
|
|
40
|
+
* `scopes`, or when `params` names a parameter the helper sets.
|
|
41
|
+
*/
|
|
42
|
+
export declare function authorizeUrl({ provider, status, redirectUri, state, codeChallenge, params, }: AuthorizeUrlOptions): URL;
|
|
43
|
+
//# sourceMappingURL=authorizeUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorizeUrl.d.ts","sourceRoot":"","sources":["../../src/server/authorizeUrl.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,EAAE,uBAAuB,CAAC;IAClC,iFAAiF;IACjF,MAAM,EAAE,4BAA4B,CAAC;IACrC,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAgCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,MAAM,EACN,WAAW,EACX,KAAK,EACL,aAAa,EACb,MAAM,GACP,EAAE,mBAAmB,GAAG,GAAG,CAqD3B"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CAPABILITIES, ConfigurationError, } from "../internal/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parameters the helper owns: the status decides the scopes, the caller's
|
|
4
|
+
* other arguments decide the rest (PKCE from `codeChallenge`, always as
|
|
5
|
+
* S256), each provider's own are fixed (Google's `access_type=offline` is
|
|
6
|
+
* what gets a refresh token, Microsoft's `response_mode=query` is what
|
|
7
|
+
* the callback reads), and Google's `include_granted_scopes` would widen
|
|
8
|
+
* the token to every scope the person ever granted this client, which the
|
|
9
|
+
* gateway refuses (`integration_scope_exceeded`).
|
|
10
|
+
*/
|
|
11
|
+
const RESERVED_PARAMS = [
|
|
12
|
+
"scope",
|
|
13
|
+
"user_scope",
|
|
14
|
+
"client_id",
|
|
15
|
+
"redirect_uri",
|
|
16
|
+
"state",
|
|
17
|
+
"response_type",
|
|
18
|
+
"code_challenge",
|
|
19
|
+
"code_challenge_method",
|
|
20
|
+
"access_type",
|
|
21
|
+
"response_mode",
|
|
22
|
+
"include_granted_scopes",
|
|
23
|
+
];
|
|
24
|
+
/** The OAuth clients with scope sets: the ones this helper builds for. */
|
|
25
|
+
function scopedClientIds() {
|
|
26
|
+
return Object.entries(CAPABILITIES)
|
|
27
|
+
.filter(([, found]) => found.mode === "oauth_client" && found.scopePolicy)
|
|
28
|
+
.map(([id]) => id);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The provider's authorize URL to send a person to, for a client with
|
|
32
|
+
* scope sets (`google-workspace`, `microsoft-365`, `slack`), built from the
|
|
33
|
+
* capability's status: the registry's authorize endpoint (or the instance
|
|
34
|
+
* the status names), `client_id`, `response_type=code`, `redirect_uri`,
|
|
35
|
+
* `state`, and the scopes the status allows this app, which are exactly
|
|
36
|
+
* what the gateway will let the token carry. Each provider's own needs
|
|
37
|
+
* are added: Google's `access_type=offline` (a refresh token), Microsoft's
|
|
38
|
+
* `response_mode=query`, Slack's comma-joined `scope` and its `user_scope`
|
|
39
|
+
* (the status's `userScopes`, when the app's sets include a user set). With
|
|
40
|
+
* `codeChallenge`, PKCE as S256. The app keeps its `state` and verifier
|
|
41
|
+
* for the callback.
|
|
42
|
+
*
|
|
43
|
+
* A client without scope sets is refused: the
|
|
44
|
+
* registry's checklist for one isn't what the gateway holds a token to,
|
|
45
|
+
* so an app builds that URL itself from the status's `clientId`.
|
|
46
|
+
*
|
|
47
|
+
* Throws a `TypeError` naming the fix when the client has no scope sets,
|
|
48
|
+
* when the status isn't ready or has no `clientId`, when the status lacks
|
|
49
|
+
* `scopes`, or when `params` names a parameter the helper sets.
|
|
50
|
+
*/
|
|
51
|
+
export function authorizeUrl({ provider, status, redirectUri, state, codeChallenge, params, }) {
|
|
52
|
+
const capability = oauthClientCapability(provider);
|
|
53
|
+
if (!capability.scopePolicy) {
|
|
54
|
+
throw new TypeError(`authorizeUrl() builds URLs for clients with scope sets (${scopedClientIds().join(", ")}); build ${capability.label}'s authorize URL yourself from the status's clientId.`);
|
|
55
|
+
}
|
|
56
|
+
if (!status.ready || !status.clientId) {
|
|
57
|
+
throw new TypeError(`authorizeUrl(${JSON.stringify(provider)}) needs a ready status with a clientId, and integrationStatus() answered ready: ${String(status.ready)}. Show ${capability.label} as not available instead: its operator sets the client up on the gateway, and an app lists the scope sets it uses in everyapp.config.ts.`);
|
|
58
|
+
}
|
|
59
|
+
if (!status.scopes) {
|
|
60
|
+
throw new TypeError(`authorizeUrl(${JSON.stringify(provider)}) takes ${capability.label}'s scopes from integrationStatus().scopes, which this status lacks: pass the status as the gateway (or everyapp dev) answered it.`);
|
|
61
|
+
}
|
|
62
|
+
for (const name of Object.keys(params ?? {})) {
|
|
63
|
+
if (!RESERVED_PARAMS.includes(name))
|
|
64
|
+
continue;
|
|
65
|
+
throw new TypeError(name === "include_granted_scopes"
|
|
66
|
+
? `authorizeUrl(${JSON.stringify(provider)}): include_granted_scopes would widen the token to every scope the person granted this client, which the gateway refuses (integration_scope_exceeded); leave it out of params.`
|
|
67
|
+
: `authorizeUrl(${JSON.stringify(provider)}) sets ${name} itself; leave it out of params.`);
|
|
68
|
+
}
|
|
69
|
+
const url = new URL(capability.authorize.url);
|
|
70
|
+
// The status speaks for the gateway: its scopes are the token's ceiling.
|
|
71
|
+
const scopes = status.scopes;
|
|
72
|
+
const slack = url.host === "slack.com";
|
|
73
|
+
url.searchParams.set("client_id", status.clientId);
|
|
74
|
+
url.searchParams.set("response_type", "code");
|
|
75
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
76
|
+
url.searchParams.set("state", state);
|
|
77
|
+
url.searchParams.set("scope", scopes.join(slack ? "," : " "));
|
|
78
|
+
if (status.userScopes && status.userScopes.length > 0) {
|
|
79
|
+
url.searchParams.set("user_scope", status.userScopes.join(","));
|
|
80
|
+
}
|
|
81
|
+
if (codeChallenge) {
|
|
82
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
83
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
84
|
+
}
|
|
85
|
+
if (url.host === "accounts.google.com") {
|
|
86
|
+
// Without it Google issues no refresh token.
|
|
87
|
+
url.searchParams.set("access_type", "offline");
|
|
88
|
+
}
|
|
89
|
+
else if (url.host === "login.microsoftonline.com") {
|
|
90
|
+
url.searchParams.set("response_mode", "query");
|
|
91
|
+
}
|
|
92
|
+
for (const [name, value] of Object.entries(params ?? {})) {
|
|
93
|
+
url.searchParams.set(name, value);
|
|
94
|
+
}
|
|
95
|
+
return url;
|
|
96
|
+
}
|
|
97
|
+
/** The OAuth client `provider` names, from plain JavaScript or a stale type too. */
|
|
98
|
+
function oauthClientCapability(provider) {
|
|
99
|
+
const found = Object.hasOwn(CAPABILITIES, provider)
|
|
100
|
+
? CAPABILITIES[provider]
|
|
101
|
+
: undefined;
|
|
102
|
+
if (found?.mode !== "oauth_client") {
|
|
103
|
+
throw new ConfigurationError(`${JSON.stringify(String(provider))} isn't an OAuth client the gateway holds.`);
|
|
104
|
+
}
|
|
105
|
+
return found;
|
|
106
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ConnectionAccount } from "../internal/index.js";
|
|
2
|
+
export interface ConnectUrlOptions {
|
|
3
|
+
/** The Worker env: `GATEWAY_URL`, which every deployed Worker and `everyapp dev` set. */
|
|
4
|
+
env: object;
|
|
5
|
+
/** The OAuth provider's id (`google-workspace`, `github`). */
|
|
6
|
+
provider: string;
|
|
7
|
+
/**
|
|
8
|
+
* Where to send the person afterwards: the app's own URL (absolute) or
|
|
9
|
+
* a path on the Gateway. Absent: Settings → Connected accounts.
|
|
10
|
+
*/
|
|
11
|
+
returnTo?: string;
|
|
12
|
+
/** Whose connection to make: the person's (default) or the organization's (owner or admin). */
|
|
13
|
+
account?: ConnectionAccount;
|
|
14
|
+
/** Scope sets to ask for; absent: the union every active app that lists the provider declares. */
|
|
15
|
+
sets?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/** The path of the connect page for `provider` on the Gateway. */
|
|
18
|
+
export declare function connectPath(provider: string): string;
|
|
19
|
+
/** The absolute URL that starts the Gateway-owned flow for `provider`. */
|
|
20
|
+
export declare function connectUrl({ env, provider, returnTo, account, sets, }: ConnectUrlOptions): string;
|
|
21
|
+
//# sourceMappingURL=connectUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectUrl.d.ts","sourceRoot":"","sources":["../../src/server/connectUrl.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,kGAAkG;IAClG,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,IAAI,GACL,EAAE,iBAAiB,GAAG,MAAM,CAmB5B"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an app sends a person to connect an OAuth provider the Gateway
|
|
3
|
+
* runs the flow for (docs/design/2026-10-oauth-providers.md section 5):
|
|
4
|
+
* `<gateway>/connect/<id>?return=<back here>&account=<user|organization>`.
|
|
5
|
+
* The Gateway checks the session, asks the provider for the union of the
|
|
6
|
+
* sets every active app declares, finishes on its own callback and sends
|
|
7
|
+
* the browser back to `returnTo`, which must be a path on the Gateway or
|
|
8
|
+
* an https URL on one of its app hosts (in local dev also localhost).
|
|
9
|
+
*/
|
|
10
|
+
import { ConfigurationError } from "../internal/index.js";
|
|
11
|
+
/** The path of the connect page for `provider` on the Gateway. */
|
|
12
|
+
export function connectPath(provider) {
|
|
13
|
+
return `/connect/${encodeURIComponent(provider)}`;
|
|
14
|
+
}
|
|
15
|
+
/** The absolute URL that starts the Gateway-owned flow for `provider`. */
|
|
16
|
+
export function connectUrl({ env, provider, returnTo, account, sets, }) {
|
|
17
|
+
const values = env;
|
|
18
|
+
const gateway = values["GATEWAY_URL"];
|
|
19
|
+
if (typeof gateway !== "string" || !gateway.trim()) {
|
|
20
|
+
throw new ConfigurationError("connectUrl() needs GATEWAY_URL in the Worker env: the deploy sets it, and everyapp dev sets it to the local gateway.");
|
|
21
|
+
}
|
|
22
|
+
let base;
|
|
23
|
+
try {
|
|
24
|
+
base = new URL(gateway.trim());
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new ConfigurationError(`GATEWAY_URL isn't a URL: ${gateway}`);
|
|
28
|
+
}
|
|
29
|
+
const url = new URL(connectPath(provider), base.origin);
|
|
30
|
+
if (returnTo)
|
|
31
|
+
url.searchParams.set("return", returnTo);
|
|
32
|
+
if (account)
|
|
33
|
+
url.searchParams.set("account", account);
|
|
34
|
+
if (sets && sets.length > 0)
|
|
35
|
+
url.searchParams.set("sets", sets.join(","));
|
|
36
|
+
return url.toString();
|
|
37
|
+
}
|