@every-app/sdk 0.3.0-rc.9 → 0.3.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.
- package/README.md +79 -74
- package/dist/cloudflare/catalogSettings.generated.d.ts +19 -6
- package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -1
- package/dist/cloudflare/catalogSettings.generated.js +43 -14
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/worker.d.ts +63 -9
- package/dist/cloudflare/worker.d.ts.map +1 -1
- package/dist/cloudflare/worker.js +45 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/aiDefault.d.ts +36 -79
- package/dist/internal/aiDefault.d.ts.map +1 -1
- package/dist/internal/aiDefault.js +47 -149
- package/dist/internal/aiProviders.d.ts +1 -1
- package/dist/internal/aiProviders.d.ts.map +1 -1
- package/dist/internal/aiProviders.js +2 -2
- package/dist/internal/appSettings.d.ts +140 -89
- package/dist/internal/appSettings.d.ts.map +1 -1
- package/dist/internal/appSettings.js +264 -156
- package/dist/internal/capabilities.d.ts +109 -503
- package/dist/internal/capabilities.d.ts.map +1 -1
- package/dist/internal/capabilities.js +30 -550
- package/dist/internal/catalog/commerce.js +1 -1
- package/dist/internal/catalog/index.d.ts +35 -0
- package/dist/internal/catalog/index.d.ts.map +1 -1
- package/dist/internal/catalog/infra.d.ts +35 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -1
- package/dist/internal/catalog/infra.js +65 -0
- package/dist/internal/devProviders.d.ts +1 -19
- package/dist/internal/devProviders.d.ts.map +1 -1
- package/dist/internal/devProviders.js +7 -1
- package/dist/internal/events.d.ts +52 -1
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +66 -4
- package/dist/internal/gatewaySettings.d.ts +6 -16
- package/dist/internal/gatewaySettings.d.ts.map +1 -1
- package/dist/internal/gatewaySettings.js +5 -23
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -0
- package/dist/internal/mcpOAuth.d.ts +210 -0
- package/dist/internal/mcpOAuth.d.ts.map +1 -0
- package/dist/internal/mcpOAuth.js +735 -0
- package/dist/internal/oauthClient.d.ts +36 -44
- package/dist/internal/oauthClient.d.ts.map +1 -1
- package/dist/internal/oauthClient.js +30 -113
- package/dist/internal/oauthPresets.d.ts +356 -20
- package/dist/internal/oauthPresets.d.ts.map +1 -1
- package/dist/internal/oauthPresets.js +183 -861
- package/dist/internal/oauthScopes.d.ts +43 -155
- package/dist/internal/oauthScopes.d.ts.map +1 -1
- package/dist/internal/oauthScopes.js +82 -475
- package/dist/internal/oauthSpec.d.ts +28 -71
- package/dist/internal/oauthSpec.d.ts.map +1 -1
- package/dist/internal/oauthSpec.js +172 -448
- package/dist/internal/protocol.d.ts +24 -14
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +26 -7
- package/dist/internal/providerApps.d.ts +2 -2
- package/dist/internal/providerApps.js +2 -2
- package/dist/internal/providerRegistry.d.ts +5 -64
- package/dist/internal/providerRegistry.d.ts.map +1 -1
- package/dist/internal/providerRegistry.js +19 -118
- package/dist/internal/providerSpec.d.ts.map +1 -1
- package/dist/internal/providerSpec.js +14 -36
- package/dist/internal/remoteMcp.d.ts +191 -47
- package/dist/internal/remoteMcp.d.ts.map +1 -1
- package/dist/internal/remoteMcp.js +338 -133
- package/dist/internal/signInProviders.d.ts +1 -1
- package/dist/internal/signInProviders.d.ts.map +1 -1
- package/dist/internal/tools.d.ts +15 -8
- package/dist/internal/tools.d.ts.map +1 -1
- package/dist/internal/tools.js +9 -8
- package/dist/manifest.d.ts +96 -40
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +1 -1
- package/dist/server/ai.d.ts +1 -93
- package/dist/server/ai.d.ts.map +1 -1
- package/dist/server/ai.js +17 -111
- package/dist/server/attribution.d.ts +11 -1
- package/dist/server/attribution.d.ts.map +1 -1
- package/dist/server/attribution.js +7 -1
- package/dist/server/connectUrl.d.ts +5 -10
- package/dist/server/connectUrl.d.ts.map +1 -1
- package/dist/server/connectUrl.js +22 -14
- package/dist/server/context.d.ts +12 -14
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +23 -25
- package/dist/server/eventsEndpoint.d.ts +48 -0
- package/dist/server/eventsEndpoint.d.ts.map +1 -0
- package/dist/server/eventsEndpoint.js +145 -0
- package/dist/server/everyApp.d.ts +16 -26
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +22 -46
- package/dist/server/gatewayFetch.d.ts +1 -1
- package/dist/server/gatewayFetch.js +1 -1
- package/dist/server/index.d.ts +6 -9
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -8
- package/dist/server/integrationFetch.d.ts +66 -72
- package/dist/server/integrationFetch.d.ts.map +1 -1
- package/dist/server/integrationFetch.js +120 -392
- package/dist/server/refusal.d.ts +0 -9
- package/dist/server/refusal.d.ts.map +1 -1
- package/dist/server/refusal.js +0 -22
- package/dist/server/tools.d.ts +18 -7
- package/dist/server/tools.d.ts.map +1 -1
- package/dist/server/tools.js +15 -5
- package/dist/server/toolsEndpoint.d.ts +2 -1
- package/dist/server/toolsEndpoint.d.ts.map +1 -1
- package/dist/server/toolsEndpoint.js +4 -3
- package/dist/server/webhooks.d.ts +56 -0
- package/dist/server/webhooks.d.ts.map +1 -0
- package/dist/server/webhooks.js +205 -0
- package/dist/testing/index.d.ts +2 -3
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -6
- package/package.json +6 -1
- package/dist/server/aiBinding.d.ts +0 -68
- package/dist/server/aiBinding.d.ts.map +0 -1
- package/dist/server/aiBinding.js +0 -294
- package/dist/server/authorizeUrl.d.ts +0 -43
- package/dist/server/authorizeUrl.d.ts.map +0 -1
- package/dist/server/authorizeUrl.js +0 -106
- package/dist/server/emailBinding.d.ts +0 -72
- package/dist/server/emailBinding.d.ts.map +0 -1
- package/dist/server/emailBinding.js +0 -124
- package/dist/server/fromMcpTools.d.ts +0 -89
- package/dist/server/fromMcpTools.d.ts.map +0 -1
- package/dist/server/fromMcpTools.js +0 -192
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { APP_ID_ENV, CATALOG, ConfigurationError, GATEWAY_ERROR_HEADER, INTEGRATION_PROXY_PREFIX, oauthPreset, apiKeyHosts, authorizationFor, capabilitySettings, devProviderFile, integrationProxyPath, providerSettingPrefix, resolveDevProvider, EMAIL_SENDERS_ENV, chooseEmailProvider, parseEmailList, parseEmailSender, } from "../internal/index.js";
|
|
2
2
|
import { applyAttribution, attributionFor, } from "./attribution.js";
|
|
3
|
+
import { connectPath } from "./connectUrl.js";
|
|
3
4
|
import { BINDING_ORIGIN, gatewayBinding, streamingRequest, } from "./gatewayFetch.js";
|
|
4
|
-
export function integrationFetch({ env, provider, for: explicitTarget,
|
|
5
|
+
export function integrationFetch({ env, provider, for: explicitTarget, optional = false, }) {
|
|
5
6
|
const catalogEntry = fetchableCapability(provider);
|
|
6
7
|
const what = `integrationFetch("${provider}")`;
|
|
7
8
|
// Checked now, so a call nobody's fails where the fetch is made; and
|
|
@@ -16,26 +17,15 @@ export function integrationFetch({ env, provider, for: explicitTarget, account,
|
|
|
16
17
|
// or in local dev, the registered row's spec `everyapp dev` hands over.
|
|
17
18
|
const integration = catalogEntry ??
|
|
18
19
|
(binding ? undefined : devRegisteredProvider(provider, values));
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// An OAuth provider with a Gateway-owned flow answers an API call
|
|
26
|
-
// without the client (`integration_not_connected`, below); a token
|
|
27
|
-
// endpoint call without the client throws when it is made.
|
|
28
|
-
const decidesLater = integration?.mode === "oauth_client" && integration.flow !== undefined;
|
|
29
|
-
if (!binding && !local && !devToken && integration && !decidesLater) {
|
|
20
|
+
// An API-key provider in local dev: the developer's own key. An OAuth
|
|
21
|
+
// provider decides per call (a dev token, or `integration_not_connected`).
|
|
22
|
+
const local = binding || integration?.mode !== "api_key"
|
|
23
|
+
? null
|
|
24
|
+
: localAuthorization(integration, values);
|
|
25
|
+
if (!binding && integration?.mode === "api_key" && !local) {
|
|
30
26
|
if (optional)
|
|
31
27
|
return null;
|
|
32
|
-
|
|
33
|
-
...capabilitySettings(integration),
|
|
34
|
-
...(integration.mode === "oauth_client" && integration.flow
|
|
35
|
-
? [devAccessTokenName(provider)]
|
|
36
|
-
: []),
|
|
37
|
-
];
|
|
38
|
-
throw new ConfigurationError(`${integration.label} is unavailable: deploy with the EVERY_APP_GATEWAY binding, or set ${names.join(", ")} in .dev.vars for local development.`);
|
|
28
|
+
throw new ConfigurationError(`${integration.label} is unavailable: deploy with the EVERY_APP_GATEWAY binding, or set ${capabilitySettings(integration).join(", ")} in .dev.vars for local development.`);
|
|
39
29
|
}
|
|
40
30
|
return async (input, init) => {
|
|
41
31
|
const target = attributionFor(env, explicitTarget, what);
|
|
@@ -53,48 +43,13 @@ export function integrationFetch({ env, provider, for: explicitTarget, account,
|
|
|
53
43
|
applyAttribution(headers, target);
|
|
54
44
|
return binding.fetch(streamingRequest(`${BINDING_ORIGIN}${integrationProxyPath(provider, upstream)}`, outbound, headers));
|
|
55
45
|
}
|
|
56
|
-
if (integration.mode === "oauth_client") {
|
|
57
|
-
// A call to the provider's API with the token the gateway holds
|
|
58
|
-
// (docs/design/2026-10-oauth-providers.md section 7), for an entry
|
|
59
|
-
// that supports the Gateway-owned flow; a token endpoint stays the
|
|
60
|
-
// app-driven call it always was.
|
|
61
|
-
const toApi = integration.flow !== undefined &&
|
|
62
|
-
upstream.protocol === "https:" &&
|
|
63
|
-
integration.api.includes(upstream.host) &&
|
|
64
|
-
!tokenEndpointFor(integration, upstream);
|
|
65
|
-
if (toApi) {
|
|
66
|
-
if (binding) {
|
|
67
|
-
if (account)
|
|
68
|
-
headers.set(ACCOUNT_HEADER, account);
|
|
69
|
-
applyAttribution(headers, target);
|
|
70
|
-
return binding.fetch(streamingRequest(`${BINDING_ORIGIN}${integrationProxyPath(provider, upstream)}`, outbound, headers));
|
|
71
|
-
}
|
|
72
|
-
return devHeldTokenCall({
|
|
73
|
-
provider,
|
|
74
|
-
capability: integration,
|
|
75
|
-
outbound,
|
|
76
|
-
upstream,
|
|
77
|
-
headers,
|
|
78
|
-
values,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
return oauthClientCall({
|
|
82
|
-
provider: provider,
|
|
83
|
-
capability: integration,
|
|
84
|
-
outbound,
|
|
85
|
-
upstream,
|
|
86
|
-
headers,
|
|
87
|
-
values,
|
|
88
|
-
...(binding
|
|
89
|
-
? { binding, target }
|
|
90
|
-
: { client: local }),
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
46
|
// In local dev a provider's host setting (`.dev.vars`) adds its host,
|
|
94
47
|
// as the gateway's setting does there.
|
|
95
|
-
const hosts =
|
|
96
|
-
? [...integration.
|
|
97
|
-
:
|
|
48
|
+
const hosts = integration.mode === "oauth_client"
|
|
49
|
+
? [...integration.api]
|
|
50
|
+
: binding
|
|
51
|
+
? [...integration.hosts]
|
|
52
|
+
: apiKeyHosts(integration, values);
|
|
98
53
|
if (upstream.protocol !== "https:" || !hosts.includes(upstream.host)) {
|
|
99
54
|
throw new TypeError(`integrationFetch("${provider}") only reaches ${hosts.join(", ")}, not ${upstream.host}.`);
|
|
100
55
|
}
|
|
@@ -103,6 +58,16 @@ export function integrationFetch({ env, provider, for: explicitTarget, account,
|
|
|
103
58
|
return binding.fetch(streamingRequest(`${BINDING_ORIGIN}${integrationProxyPath(provider, upstream)}`, outbound, headers));
|
|
104
59
|
}
|
|
105
60
|
applyAttribution(headers, undefined);
|
|
61
|
+
if (integration.mode === "oauth_client") {
|
|
62
|
+
return devHeldTokenCall({
|
|
63
|
+
provider,
|
|
64
|
+
capability: integration,
|
|
65
|
+
outbound,
|
|
66
|
+
upstream,
|
|
67
|
+
headers,
|
|
68
|
+
values,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
106
71
|
// The developer's own key, put where the provider's entry says, as the
|
|
107
72
|
// gateway would put its own.
|
|
108
73
|
const auth = local;
|
|
@@ -124,57 +89,31 @@ export function integrationFetch({ env, provider, for: explicitTarget, account,
|
|
|
124
89
|
export function devAccessTokenName(provider) {
|
|
125
90
|
return `${providerSettingPrefix(provider)}ACCESS_TOKEN`;
|
|
126
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Where a person connects `provider`, or allows this app to use their
|
|
94
|
+
* connection to it: `/connect/<id>?app=<EVERYAPP_APP_ID>`, as the
|
|
95
|
+
* gateway's refusal and status name it (without the app id when the env
|
|
96
|
+
* has none).
|
|
97
|
+
*/
|
|
98
|
+
function appConnectPath(provider, values) {
|
|
99
|
+
const app = stringValue(values[APP_ID_ENV]);
|
|
100
|
+
return app
|
|
101
|
+
? `${connectPath(provider)}?app=${encodeURIComponent(app)}`
|
|
102
|
+
: connectPath(provider);
|
|
103
|
+
}
|
|
127
104
|
/**
|
|
128
105
|
* An app's call to an OAuth provider's API in local dev, with no
|
|
129
|
-
* gateway: the
|
|
130
|
-
*
|
|
131
|
-
*
|
|
106
|
+
* gateway: the developer's own token from `.dev.vars` as the bearer,
|
|
107
|
+
* straight to the provider (the host was checked already), or the
|
|
108
|
+
* gateway's `integration_not_connected` refusal without one.
|
|
132
109
|
*/
|
|
133
110
|
async function devHeldTokenCall({ provider, capability, outbound, upstream, headers, values, }) {
|
|
134
111
|
const token = stringValue(values[devAccessTokenName(provider)]);
|
|
135
112
|
if (!token) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const policy = capability.scopePolicy;
|
|
139
|
-
const grant = policy
|
|
140
|
-
? devScopeSets(policy, provider, capability.label, values, "any")
|
|
141
|
-
: null;
|
|
142
|
-
if (grant && "problem" in grant) {
|
|
143
|
-
return devRefusal(403, grant.code, grant.problem);
|
|
113
|
+
const path = appConnectPath(provider, values);
|
|
114
|
+
return devRefusal(409, "integration_not_connected", `${capability.label} isn't connected here. In local dev, set ${devAccessTokenName(provider)} in .dev.vars to a token you obtained yourself; deployed, the person connects in the app at ${path}.`, { connectPath: path, reconnect: false });
|
|
144
115
|
}
|
|
145
116
|
const method = outbound.method.toUpperCase();
|
|
146
|
-
const reads = oauthReadMethods(capability).includes(method);
|
|
147
|
-
const writes = !reads &&
|
|
148
|
-
capability.access.includes("write") &&
|
|
149
|
-
oauthWriteMethods(capability).includes(method) &&
|
|
150
|
-
devDeclaredAccess(provider, values) === "write";
|
|
151
|
-
if (policy && grant) {
|
|
152
|
-
const allowed = heldTokenCallAllowed({
|
|
153
|
-
policy,
|
|
154
|
-
declaredSets: grant.sets,
|
|
155
|
-
// The developer's token stands for a connection granted every set.
|
|
156
|
-
connectionSets: grant.sets,
|
|
157
|
-
hosts: capability.api,
|
|
158
|
-
method,
|
|
159
|
-
upstream,
|
|
160
|
-
readMethods: oauthReadMethods(capability),
|
|
161
|
-
writes,
|
|
162
|
-
});
|
|
163
|
-
if (!allowed.ok) {
|
|
164
|
-
const status = {
|
|
165
|
-
integration_not_connected: 409,
|
|
166
|
-
integration_method_not_allowed: 405,
|
|
167
|
-
integration_scope_not_allowed: 403,
|
|
168
|
-
}[allowed.code];
|
|
169
|
-
return devRefusal(status, allowed.code, allowed.code === "integration_not_connected"
|
|
170
|
-
? `${capability.label} is connected without ${allowed.missingSets.join(", ")}.`
|
|
171
|
-
: allowed.message);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
else if (!reads && !writes) {
|
|
175
|
-
return devRefusal(405, "integration_method_not_allowed", `${capability.label} calls may use ${oauthReadMethods(capability).join(" or ")} with access: "read"; ${method} needs access: "write" in the app's capabilities entry, on a provider that offers it.`);
|
|
176
|
-
}
|
|
177
|
-
applyAttribution(headers, undefined);
|
|
178
117
|
headers.set("authorization", `Bearer ${token}`);
|
|
179
118
|
for (const name of ["access_token", "token"]) {
|
|
180
119
|
upstream.searchParams.delete(name);
|
|
@@ -190,255 +129,53 @@ async function devHeldTokenCall({ provider, capability, outbound, upstream, head
|
|
|
190
129
|
...(method === "GET" || method === "HEAD" ? {} : { duplex: "half" }),
|
|
191
130
|
}));
|
|
192
131
|
}
|
|
193
|
-
/** The `access` the manifest entry for `provider` declares in dev (EVERYAPP_DEV_CAPABILITIES), or null. */
|
|
194
|
-
function devDeclaredAccess(provider, values) {
|
|
195
|
-
const raw = values["EVERYAPP_DEV_CAPABILITIES"];
|
|
196
|
-
try {
|
|
197
|
-
const capabilities = typeof raw === "string" ? jsonRecord(JSON.parse(raw)) : undefined;
|
|
198
|
-
const entry = jsonRecord(capabilities?.[provider]);
|
|
199
|
-
return typeof entry?.["access"] === "string" ? entry["access"] : null;
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
132
|
/**
|
|
206
|
-
* The developer's own key
|
|
207
|
-
*
|
|
133
|
+
* The developer's own key from `.dev.vars` (with EVERYAPP_DEV set), under
|
|
134
|
+
* the gateway's names; null until every setting it needs is.
|
|
208
135
|
*/
|
|
209
|
-
function
|
|
136
|
+
function localAuthorization(integration, values) {
|
|
210
137
|
if (!values["EVERYAPP_DEV"])
|
|
211
138
|
return null;
|
|
212
|
-
if (integration.mode === "oauth_client") {
|
|
213
|
-
return oauthClientCredentials(integration, values);
|
|
214
|
-
}
|
|
215
139
|
return authorizationFor(integration, values);
|
|
216
140
|
}
|
|
217
|
-
/**
|
|
218
|
-
* A call to an OAuth client's token endpoint: over the binding, where the
|
|
219
|
-
* gateway puts its client on it, or in local dev straight to the provider
|
|
220
|
-
* with the developer's client put on it by the gateway's own rule.
|
|
221
|
-
*/
|
|
222
|
-
async function oauthClientCall({ provider, capability, outbound, upstream, headers, values, binding, target, client, }) {
|
|
223
|
-
const endpoint = tokenEndpointFor(capability, upstream);
|
|
224
|
-
const method = outbound.method.toUpperCase();
|
|
225
|
-
if (!endpoint || !endpoint.methods.includes(method)) {
|
|
226
|
-
throw new TypeError(`integrationFetch("${provider}") only reaches ${capability.label}'s token endpoints (${describeTokenEndpoints(capability)}), not ${method} ${upstream.origin}${upstream.pathname}. Call its API with the user's token directly.`);
|
|
227
|
-
}
|
|
228
|
-
if (binding) {
|
|
229
|
-
applyAttribution(headers, target);
|
|
230
|
-
return binding.fetch(streamingRequest(`${BINDING_ORIGIN}${integrationProxyPath(provider, upstream)}`, outbound, headers));
|
|
231
|
-
}
|
|
232
|
-
applyAttribution(headers, undefined);
|
|
233
|
-
if (!client) {
|
|
234
|
-
throw new ConfigurationError(`${capability.label}'s token endpoints need the client in .dev.vars for local development: set ${capabilitySettings(capability).join(", ")}.`);
|
|
235
|
-
}
|
|
236
|
-
// The gateway's scope checks, in its order, answering its refusals: an
|
|
237
|
-
// app sees in `everyapp dev` what the gateway will say.
|
|
238
|
-
const policy = capability.scopePolicy;
|
|
239
|
-
const grant = policy
|
|
240
|
-
? devScopeSets(policy, provider, capability.label, values)
|
|
241
|
-
: null;
|
|
242
|
-
if (grant && "problem" in grant) {
|
|
243
|
-
return devRefusal(403, grant.code, grant.problem);
|
|
244
|
-
}
|
|
245
|
-
const sets = grant?.sets;
|
|
246
|
-
const body = method === "GET" ? null : await outbound.text();
|
|
247
|
-
if (body !== null &&
|
|
248
|
-
new TextEncoder().encode(body).byteLength > OAUTH_TOKEN_BODY_LIMIT) {
|
|
249
|
-
throw new TypeError(`A token request's body is at most ${OAUTH_TOKEN_BODY_LIMIT / 1024} KiB.`);
|
|
250
|
-
}
|
|
251
|
-
if (policy &&
|
|
252
|
-
["grant_type", "scope", "user_scope"].some((name) => upstream.searchParams.has(name))) {
|
|
253
|
-
return devRefusal(400, "integration_body_invalid", "Put grant_type, scope and user_scope in the token request's body, not its query string; the gateway decides on the body.");
|
|
254
|
-
}
|
|
255
|
-
let injected = injectClientCredentials({ url: upstream, headers, body }, endpoint, client);
|
|
256
|
-
if (!injected.ok)
|
|
257
|
-
throw new TypeError(injected.problem);
|
|
258
|
-
if (policy && sets) {
|
|
259
|
-
const refused = refuseTokenRequest(policy, capability, sets, injected);
|
|
260
|
-
if (refused)
|
|
261
|
-
return refused;
|
|
262
|
-
injected = {
|
|
263
|
-
ok: true,
|
|
264
|
-
...rewriteTokenRequestScope(policy, sets, injected),
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
const response = await fetch(new Request(injected.url, {
|
|
268
|
-
method,
|
|
269
|
-
headers: injected.headers,
|
|
270
|
-
body: injected.body,
|
|
271
|
-
signal: outbound.signal,
|
|
272
|
-
// As the gateway: the client never follows a redirect.
|
|
273
|
-
redirect: "manual",
|
|
274
|
-
}));
|
|
275
|
-
if (!policy || !sets || !response.ok)
|
|
276
|
-
return response;
|
|
277
|
-
return checkTokenResponse(policy, capability, sets, response);
|
|
278
|
-
}
|
|
279
141
|
/**
|
|
280
142
|
* The gateway's refusal shape (`{ error, code, message }`, the code in
|
|
281
143
|
* `x-everyapp-error`) from the local-dev twin, so {@link gatewayRefusal}
|
|
282
144
|
* reads it the same way.
|
|
283
145
|
*/
|
|
284
146
|
function devRefusal(status, code, message, extra = {}) {
|
|
285
|
-
const labels = {
|
|
286
|
-
400: "Bad Request",
|
|
287
|
-
403: "Forbidden",
|
|
288
|
-
405: "Method Not Allowed",
|
|
289
|
-
409: "Conflict",
|
|
290
|
-
502: "Bad Gateway",
|
|
291
|
-
};
|
|
147
|
+
const labels = { 409: "Conflict" };
|
|
292
148
|
return Response.json({ error: labels[status] ?? "Error", code, message, ...extra }, { status, headers: { [GATEWAY_ERROR_HEADER]: code } });
|
|
293
149
|
}
|
|
294
150
|
/**
|
|
295
|
-
* The
|
|
151
|
+
* The manifest's entry for `id` in local dev, from
|
|
296
152
|
* EVERYAPP_DEV_CAPABILITIES (the manifest's `capabilities` as JSON, which
|
|
297
|
-
* `everyAppWorker()` writes in dev)
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* missing, the entry without usable `scopes`, or a set this registry
|
|
301
|
-
* doesn't offer). Fails closed like the gateway: a hand-rolled dev harness
|
|
302
|
-
* that passes no capabilities gets nothing.
|
|
153
|
+
* `everyAppWorker()` writes in dev): the entry, `null` when the manifest
|
|
154
|
+
* doesn't list it, `undefined` when the variable is missing or isn't JSON
|
|
155
|
+
* (a hand-rolled harness), so nothing is known.
|
|
303
156
|
*/
|
|
304
|
-
function
|
|
305
|
-
/** The grant the entry needs: `write` for a token-endpoint call (the app-driven flow), `any` for a held-token call. */
|
|
306
|
-
grant = "write") {
|
|
307
|
-
const offered = Object.keys(policy.sets).join(", ");
|
|
157
|
+
function devManifestEntry(id, values) {
|
|
308
158
|
const raw = values["EVERYAPP_DEV_CAPABILITIES"];
|
|
159
|
+
if (typeof raw !== "string")
|
|
160
|
+
return undefined;
|
|
309
161
|
let capabilities;
|
|
310
162
|
try {
|
|
311
|
-
capabilities =
|
|
312
|
-
typeof raw === "string" ? jsonRecord(JSON.parse(raw)) : undefined;
|
|
163
|
+
capabilities = jsonRecord(JSON.parse(raw));
|
|
313
164
|
}
|
|
314
165
|
catch {
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
if (!capabilities) {
|
|
318
|
-
return {
|
|
319
|
-
code: "integration_scopes_undeclared",
|
|
320
|
-
problem: `${label}'s scope sets aren't known here: everyapp dev passes the manifest's capabilities to the Worker as EVERYAPP_DEV_CAPABILITIES when everyapp.config.ts builds it with everyAppWorker() from @every-app/sdk/cloudflare.`,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
const entry = Object.hasOwn(capabilities, id)
|
|
324
|
-
? jsonRecord(capabilities[id])
|
|
325
|
-
: undefined;
|
|
326
|
-
// The gateway's check 2: listing the client for writing is the grant
|
|
327
|
-
// of the app-driven flow; a held-token call takes any listing.
|
|
328
|
-
const access = entry?.["access"];
|
|
329
|
-
if (!entry ||
|
|
330
|
-
(grant === "write"
|
|
331
|
-
? access !== "write"
|
|
332
|
-
: access !== "read" && access !== "write")) {
|
|
333
|
-
return {
|
|
334
|
-
code: "capability_not_requested",
|
|
335
|
-
problem: `This app doesn't list ${label} in its capabilities. Add capabilities: { ${JSON.stringify(id)}: { access: "${grant === "write" ? "write" : "read"}" } } to everyapp.config.ts and start everyapp dev again.`,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
const sets = declaredScopeSets(policy, entry);
|
|
339
|
-
if (sets)
|
|
340
|
-
return { sets };
|
|
341
|
-
const listed = entry["scopes"];
|
|
342
|
-
const unknown = Array.isArray(listed)
|
|
343
|
-
? listed.filter((name) => typeof name === "string" && !Object.hasOwn(policy.sets, name))
|
|
344
|
-
: [];
|
|
345
|
-
if (unknown.length > 0) {
|
|
346
|
-
return {
|
|
347
|
-
code: "integration_scopes_undeclared",
|
|
348
|
-
problem: `This app's ${label} entry names scope sets this SDK doesn't offer: ${unknown.join(", ")}. It offers: ${offered}. Fix everyapp.config.ts and start everyapp dev again, or update @every-app/sdk if it is older than the set.`,
|
|
349
|
-
};
|
|
166
|
+
return undefined;
|
|
350
167
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
168
|
+
if (!capabilities)
|
|
169
|
+
return undefined;
|
|
170
|
+
return Object.hasOwn(capabilities, id)
|
|
171
|
+
? (jsonRecord(capabilities[id]) ?? null)
|
|
172
|
+
: null;
|
|
355
173
|
}
|
|
356
174
|
function jsonRecord(value) {
|
|
357
175
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
358
176
|
? value
|
|
359
177
|
: undefined;
|
|
360
178
|
}
|
|
361
|
-
/**
|
|
362
|
-
* The gateway's checks on a token request before it goes out: the grant
|
|
363
|
-
* type must be one the policy completes, and a `scope` the body names must
|
|
364
|
-
* stay within the sets. Null when the request may go.
|
|
365
|
-
*/
|
|
366
|
-
function refuseTokenRequest(policy, capability, sets, request) {
|
|
367
|
-
const grantType = tokenGrantType(request);
|
|
368
|
-
if (!policy.grantTypes.includes(grantType)) {
|
|
369
|
-
const completes = policy.grantTypes.map((type) => type === null ? "a code exchange without grant_type" : type);
|
|
370
|
-
const asked = grantType === null
|
|
371
|
-
? "a request without grant_type"
|
|
372
|
-
: grantType === "other"
|
|
373
|
-
? "this request's grant_type"
|
|
374
|
-
: grantType;
|
|
375
|
-
return devRefusal(403, "integration_grant_not_allowed", `Through the gateway, ${capability.label} completes ${completes.join(" and ")} grants only; ${asked} isn't one of them. An app never gets app-level tokens with the operator's client.`);
|
|
376
|
-
}
|
|
377
|
-
const asked = requestedTokenScopes(policy, request);
|
|
378
|
-
const excess = asked ? scopeExcess(asked, allowedScopes(policy, sets)) : [];
|
|
379
|
-
if (excess.length > 0) {
|
|
380
|
-
return devRefusal(403, "integration_scope_not_allowed", `This app may ask ${capability.label} for ${sets.join(", ")} (${requestedScopes(policy, sets).scopes.join(", ")}); the request also asks ${excess.join(", ")}. Build the authorize URL and the token request from integrationStatus().scopes.`);
|
|
381
|
-
}
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* The gateway's check on a provider's 2xx answer: a token it carries must
|
|
386
|
-
* name scopes, and those must stay within the sets; on a refusal the
|
|
387
|
-
* answer is dropped, never forwarded, so no app holds a token outside its
|
|
388
|
-
* grant. An answer without a token (Slack's `ok: false`) is the app's to
|
|
389
|
-
* read. A non-2xx answer never reaches here.
|
|
390
|
-
*/
|
|
391
|
-
async function checkTokenResponse(policy, capability, sets, response) {
|
|
392
|
-
const invalid = () => devRefusal(502, "integration_response_invalid", `${capability.label} answered with something that isn't a token response (over ${OAUTH_TOKEN_BODY_LIMIT / 1024} KiB or not a JSON object); nothing was passed on.`);
|
|
393
|
-
const bytes = await response.arrayBuffer();
|
|
394
|
-
if (bytes.byteLength > OAUTH_TOKEN_BODY_LIMIT)
|
|
395
|
-
return invalid();
|
|
396
|
-
const text = new TextDecoder().decode(bytes);
|
|
397
|
-
let json;
|
|
398
|
-
try {
|
|
399
|
-
json = JSON.parse(text);
|
|
400
|
-
}
|
|
401
|
-
catch {
|
|
402
|
-
return invalid();
|
|
403
|
-
}
|
|
404
|
-
if (!json || typeof json !== "object" || Array.isArray(json)) {
|
|
405
|
-
return invalid();
|
|
406
|
-
}
|
|
407
|
-
// The body was decoded on read: its length and encoding headers no
|
|
408
|
-
// longer describe it.
|
|
409
|
-
const headers = new Headers(response.headers);
|
|
410
|
-
for (const name of [
|
|
411
|
-
"set-cookie",
|
|
412
|
-
"content-length",
|
|
413
|
-
"content-encoding",
|
|
414
|
-
"transfer-encoding",
|
|
415
|
-
]) {
|
|
416
|
-
headers.delete(name);
|
|
417
|
-
}
|
|
418
|
-
const forward = () => new Response(text, {
|
|
419
|
-
status: response.status,
|
|
420
|
-
statusText: response.statusText,
|
|
421
|
-
headers,
|
|
422
|
-
});
|
|
423
|
-
if (!carriesToken(json))
|
|
424
|
-
return forward();
|
|
425
|
-
const granted = grantedScopes(policy, json);
|
|
426
|
-
if (!granted) {
|
|
427
|
-
return devRefusal(502, "integration_scope_unverified", `${capability.label} issued a token without saying which scopes it carries, so the gateway can't hold it to this app's scope sets; nothing was passed on. Ask the person to connect again.`);
|
|
428
|
-
}
|
|
429
|
-
const excess = scopeExcess(granted, allowedScopes(policy, sets));
|
|
430
|
-
if (excess.length > 0) {
|
|
431
|
-
const remedy = {
|
|
432
|
-
google: "Ask for only the scopes integrationStatus() returns, without include_granted_scopes, and connect again; if the app needs more, add the scope set to everyapp.config.ts and deploy.",
|
|
433
|
-
microsoft: "On a shared Microsoft client a token carries everything the person consented to this client: have them remove this app's consent at https://myapplications.microsoft.com and connect again, or add the scope set to everyapp.config.ts and deploy.",
|
|
434
|
-
slack: "Slack scopes add up per workspace installation: reinstall the app in the workspace with only the status's scopes, or add the scope set to everyapp.config.ts and deploy.",
|
|
435
|
-
space: "Ask for only the scopes integrationStatus() returns and connect again, or add the scope set to everyapp.config.ts and deploy.",
|
|
436
|
-
comma: "Ask for only the scopes integrationStatus() returns and connect again, or add the scope set to everyapp.config.ts and deploy.",
|
|
437
|
-
}[policy.dialect];
|
|
438
|
-
return devRefusal(403, "integration_scope_exceeded", `${capability.label} granted scopes this app doesn't declare: ${excess.join(", ")}. It declares ${sets.join(", ")} (${requestedScopes(policy, sets).scopes.join(", ")}). Nothing was passed on. ${remedy}`);
|
|
439
|
-
}
|
|
440
|
-
return forward();
|
|
441
|
-
}
|
|
442
179
|
/**
|
|
443
180
|
* A provider outside the catalog in local dev (no binding): the gateway
|
|
444
181
|
* row's spec from EVERYAPP_DEV_PROVIDERS (`.everyapp/providers/<id>.json`,
|
|
@@ -480,8 +217,9 @@ function stringValue(value) {
|
|
|
480
217
|
/**
|
|
481
218
|
* Whether the app can use an integration right now, without calling the
|
|
482
219
|
* provider: show "an admin needs to set up DataForSEO" (or "sending isn't
|
|
483
|
-
* set up") up front instead of failing mid-task. An
|
|
484
|
-
*
|
|
220
|
+
* set up", or "connect Google") up front instead of failing mid-task. An
|
|
221
|
+
* OAuth provider's status carries the person's connection, once they
|
|
222
|
+
* allowed this app to use it, and where to connect or allow.
|
|
485
223
|
*/
|
|
486
224
|
export async function integrationStatus({ env, provider, for: explicitTarget, }) {
|
|
487
225
|
const status = await readIntegrationStatus({
|
|
@@ -507,6 +245,18 @@ function withSetup(status, provider, values) {
|
|
|
507
245
|
};
|
|
508
246
|
}
|
|
509
247
|
const integration = CATALOG.get(provider) ?? devRegisteredProvider(provider, values);
|
|
248
|
+
const label = integration?.label ?? provider;
|
|
249
|
+
if (integration?.mode === "oauth_client") {
|
|
250
|
+
// In dev a token stands in for the connection; the client is the
|
|
251
|
+
// deployed gateway's alone.
|
|
252
|
+
const name = devAccessTokenName(provider);
|
|
253
|
+
return {
|
|
254
|
+
...status,
|
|
255
|
+
settings: [name],
|
|
256
|
+
setup: `${label} isn't connected for local development: set ${name} in .dev.vars to a token you obtained yourself.`,
|
|
257
|
+
command: null,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
510
260
|
const settings = provider === "email"
|
|
511
261
|
? ["EMAIL_FROM"]
|
|
512
262
|
: integration
|
|
@@ -516,8 +266,8 @@ function withSetup(status, provider, values) {
|
|
|
516
266
|
...status,
|
|
517
267
|
settings,
|
|
518
268
|
setup: settings.length
|
|
519
|
-
? `${
|
|
520
|
-
: `${
|
|
269
|
+
? `${label} isn't set up for local development: set ${settings.join(" and ")} in .dev.vars.`
|
|
270
|
+
: `${label} isn't set up for local development.`,
|
|
521
271
|
command: null,
|
|
522
272
|
};
|
|
523
273
|
}
|
|
@@ -529,7 +279,9 @@ async function readIntegrationStatus({ env, provider, for: explicitTarget, }) {
|
|
|
529
279
|
// without one, the catalog, or in local dev the row's spec from
|
|
530
280
|
// `.everyapp/providers/<id>.json`.
|
|
531
281
|
const integration = binding && typeof binding.fetch === "function"
|
|
532
|
-
? CATALOG.get(String(provider))
|
|
282
|
+
? (CATALOG.get(String(provider)) ??
|
|
283
|
+
oauthPreset(String(provider)) ??
|
|
284
|
+
undefined)
|
|
533
285
|
: (CATALOG.get(String(provider)) ??
|
|
534
286
|
devRegisteredProvider(String(provider), values));
|
|
535
287
|
if (integration?.mode === "ai") {
|
|
@@ -544,11 +296,20 @@ async function readIntegrationStatus({ env, provider, for: explicitTarget, }) {
|
|
|
544
296
|
senders: [],
|
|
545
297
|
provider: null,
|
|
546
298
|
}
|
|
547
|
-
:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
299
|
+
: integration?.mode === "oauth_client"
|
|
300
|
+
? {
|
|
301
|
+
requested: false,
|
|
302
|
+
configured: false,
|
|
303
|
+
ready: false,
|
|
304
|
+
scopes: [],
|
|
305
|
+
connectPath: null,
|
|
306
|
+
connection: null,
|
|
307
|
+
}
|
|
308
|
+
: {
|
|
309
|
+
requested: false,
|
|
310
|
+
configured: false,
|
|
311
|
+
ready: false,
|
|
312
|
+
});
|
|
552
313
|
if (binding && typeof binding.fetch === "function") {
|
|
553
314
|
const statusUrl = `${BINDING_ORIGIN}${INTEGRATION_PROXY_PREFIX}/${provider}`;
|
|
554
315
|
let response;
|
|
@@ -556,7 +317,8 @@ async function readIntegrationStatus({ env, provider, for: explicitTarget, }) {
|
|
|
556
317
|
response = await binding.fetch(statusUrl);
|
|
557
318
|
}
|
|
558
319
|
else {
|
|
559
|
-
// Whose status: the person's identity (their
|
|
320
|
+
// Whose status: the person's identity (their connection, which
|
|
321
|
+
// they allowed this app), the member background work names, or the
|
|
560
322
|
// organization's, as a call would carry it.
|
|
561
323
|
const headers = new Headers();
|
|
562
324
|
applyAttribution(headers, target);
|
|
@@ -581,7 +343,7 @@ async function readIntegrationStatus({ env, provider, for: explicitTarget, }) {
|
|
|
581
343
|
};
|
|
582
344
|
}
|
|
583
345
|
if (integration.mode === "oauth_client") {
|
|
584
|
-
return
|
|
346
|
+
return devOAuthStatus(String(provider), values);
|
|
585
347
|
}
|
|
586
348
|
const configured = Boolean(values["EVERYAPP_DEV"] && authorizationFor(integration, values));
|
|
587
349
|
return {
|
|
@@ -591,70 +353,36 @@ async function readIntegrationStatus({ env, provider, for: explicitTarget, }) {
|
|
|
591
353
|
};
|
|
592
354
|
}
|
|
593
355
|
/**
|
|
594
|
-
* An OAuth
|
|
595
|
-
*
|
|
596
|
-
* EVERYAPP_DEV_CAPABILITIES,
|
|
597
|
-
*
|
|
598
|
-
*
|
|
356
|
+
* An OAuth provider in local dev, with no gateway, shaped as the
|
|
357
|
+
* gateway's status is: the scopes from the manifest's entry
|
|
358
|
+
* (EVERYAPP_DEV_CAPABILITIES), and a token in `.dev.vars` standing in for
|
|
359
|
+
* a connection that holds them all. An app whose manifest doesn't list
|
|
360
|
+
* the provider is told nothing.
|
|
599
361
|
*/
|
|
600
|
-
function
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
: null;
|
|
604
|
-
const policy = capability.scopePolicy;
|
|
605
|
-
// An entry with a Gateway-owned flow is listed for reading or writing;
|
|
606
|
-
// the app-driven flow alone needs the write grant.
|
|
607
|
-
const grant = policy
|
|
608
|
-
? devScopeSets(policy, id, capability.label, values, capability.flow ? "any" : "write")
|
|
609
|
-
: null;
|
|
610
|
-
// The Gateway-owned flow's fields, in dev: a token in `.dev.vars`
|
|
611
|
-
// stands for a connection granted every declared set.
|
|
612
|
-
const flow = capability.flow ? "both" : "app";
|
|
613
|
-
const devToken = capability.flow
|
|
614
|
-
? stringValue(values[devAccessTokenName(id)])
|
|
615
|
-
: undefined;
|
|
616
|
-
const gateway = (sets) => capability.flow
|
|
617
|
-
? {
|
|
618
|
-
flow,
|
|
619
|
-
connectPath: `/connect/${id}`,
|
|
620
|
-
connection: devToken
|
|
621
|
-
? {
|
|
622
|
-
account: "user",
|
|
623
|
-
label: devAccessTokenName(id),
|
|
624
|
-
scopeSets: sets,
|
|
625
|
-
ready: true,
|
|
626
|
-
}
|
|
627
|
-
: null,
|
|
628
|
-
}
|
|
629
|
-
: { flow };
|
|
630
|
-
if (grant && "problem" in grant) {
|
|
631
|
-
if (grant.code === "capability_not_requested") {
|
|
632
|
-
return { requested: false, configured: false, ready: false };
|
|
633
|
-
}
|
|
362
|
+
function devOAuthStatus(id, values) {
|
|
363
|
+
const entry = devManifestEntry(id, values);
|
|
364
|
+
if (entry === null) {
|
|
634
365
|
return {
|
|
635
|
-
requested:
|
|
636
|
-
configured:
|
|
366
|
+
requested: false,
|
|
367
|
+
configured: false,
|
|
637
368
|
ready: false,
|
|
638
|
-
|
|
639
|
-
|
|
369
|
+
scopes: [],
|
|
370
|
+
connectPath: null,
|
|
371
|
+
connection: null,
|
|
640
372
|
};
|
|
641
373
|
}
|
|
642
|
-
const
|
|
374
|
+
const listed = entry?.["scopes"];
|
|
375
|
+
const scopes = Array.isArray(listed)
|
|
376
|
+
? listed.filter((scope) => typeof scope === "string")
|
|
377
|
+
: [];
|
|
378
|
+
const token = Boolean(stringValue(values[devAccessTokenName(id)]));
|
|
643
379
|
return {
|
|
644
380
|
requested: true,
|
|
645
|
-
configured:
|
|
646
|
-
ready:
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
scopeSets: grant.sets,
|
|
651
|
-
scopes: asked.scopes,
|
|
652
|
-
...(asked.userScopes.length > 0
|
|
653
|
-
? { userScopes: asked.userScopes }
|
|
654
|
-
: {}),
|
|
655
|
-
}
|
|
656
|
-
: {}),
|
|
657
|
-
...gateway(grant?.sets ?? []),
|
|
381
|
+
configured: token,
|
|
382
|
+
ready: token,
|
|
383
|
+
scopes,
|
|
384
|
+
connectPath: appConnectPath(id, values),
|
|
385
|
+
connection: token ? { label: null, scopes, ready: true } : null,
|
|
658
386
|
};
|
|
659
387
|
}
|
|
660
388
|
/**
|