@every-app/sdk 0.3.0-rc.2 → 0.3.0-rc.3
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/dist/cloudflare/index.d.ts +2 -2
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/sentinel.d.ts +39 -1
- package/dist/cloudflare/sentinel.d.ts.map +1 -1
- package/dist/cloudflare/sentinel.js +22 -1
- package/dist/cloudflare/worker.d.ts +119 -2
- package/dist/cloudflare/worker.d.ts.map +1 -1
- package/dist/cloudflare/worker.js +90 -6
- package/dist/internal/aiDefault.d.ts +141 -0
- package/dist/internal/aiDefault.d.ts.map +1 -0
- package/dist/internal/aiDefault.js +204 -0
- package/dist/internal/aiProviders.d.ts +4 -1
- package/dist/internal/aiProviders.d.ts.map +1 -1
- package/dist/internal/aiProviders.js +5 -1
- package/dist/internal/appSettings.d.ts +20 -2
- package/dist/internal/appSettings.d.ts.map +1 -1
- package/dist/internal/appSettings.js +53 -7
- 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 +275 -6
- package/dist/internal/capabilities.d.ts.map +1 -1
- package/dist/internal/capabilities.js +111 -0
- package/dist/internal/catalog/index.d.ts +38 -1
- package/dist/internal/catalog/index.d.ts.map +1 -1
- package/dist/internal/catalog/inference.d.ts +38 -1
- package/dist/internal/catalog/inference.d.ts.map +1 -1
- package/dist/internal/catalog/inference.js +33 -1
- package/dist/internal/devProviders.d.ts +2 -2
- package/dist/internal/devProviders.d.ts.map +1 -1
- package/dist/internal/devProviders.js +19 -1
- package/dist/internal/events.d.ts +13 -2
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +19 -3
- package/dist/internal/index.d.ts +4 -0
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +4 -0
- package/dist/internal/oauthPresets.d.ts +29 -0
- package/dist/internal/oauthPresets.d.ts.map +1 -0
- package/dist/internal/oauthPresets.js +534 -0
- package/dist/internal/oauthScopes.d.ts +56 -1
- package/dist/internal/oauthScopes.d.ts.map +1 -1
- package/dist/internal/oauthScopes.js +98 -4
- package/dist/internal/oauthSpec.d.ts +59 -0
- package/dist/internal/oauthSpec.d.ts.map +1 -0
- package/dist/internal/oauthSpec.js +676 -0
- package/dist/internal/protocol.d.ts +11 -0
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +9 -0
- package/dist/internal/providerRegistry.d.ts +17 -6
- package/dist/internal/providerRegistry.d.ts.map +1 -1
- package/dist/internal/providerRegistry.js +47 -2
- package/dist/internal/providerSpec.d.ts +11 -7
- package/dist/internal/providerSpec.d.ts.map +1 -1
- package/dist/internal/providerSpec.js +134 -37
- package/dist/internal/signInProviders.d.ts.map +1 -1
- package/dist/internal/signInProviders.js +11 -4
- package/dist/server/ai.d.ts +45 -0
- package/dist/server/ai.d.ts.map +1 -0
- package/dist/server/ai.js +40 -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/gatewayFetch.d.ts.map +1 -1
- package/dist/server/gatewayFetch.js +72 -8
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -1
- package/dist/server/integrationFetch.d.ts +39 -2
- package/dist/server/integrationFetch.d.ts.map +1 -1
- package/dist/server/integrationFetch.js +177 -13
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gatewayFetch.d.ts","sourceRoot":"","sources":["../../src/server/gatewayFetch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gatewayFetch.d.ts","sourceRoot":"","sources":["../../src/server/gatewayFetch.ts"],"names":[],"mappings":"AAaA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAE1B,eAAO,MAAM,cAAc,sCAAsC,CAAC;AAGlE,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxE;AAED,MAAM,WAAW,eAAe;IAC9B,iBAAiB,CAAC,EAAE,qBAAqB,CAAC;IAC1C,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,kEAAkE;IAClE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,eAAe,CAAC;IACrB;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,cAAc,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,GAAG,EACH,GAAG,EAAE,MAAM,GACZ,EAAE,yBAAyB,GAAG,OAAO,KAAK,CAoE1C;AAwCD,sFAAsF;AACtF,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,OAAO,GACf,OAAO,CAUT"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { aiProviderSettings, authorizationOf, ConfigurationError, DEV_AI_PROVIDERS, devAiProvider, joinProviderUrl, } from "../internal/index.js";
|
|
1
|
+
import { AI_DEFAULT_MODEL, AI_DEFAULT_PROVIDER, AI_DEFAULT_ROUTES, aiProviderSettings, authorizationOf, ConfigurationError, DEV_AI_PROVIDERS, devAiProvider, joinProviderUrl, resolveAiDefault, } from "../internal/index.js";
|
|
2
2
|
import { applyAttribution, checkAttribution, } from "./attribution.js";
|
|
3
3
|
export const BINDING_ORIGIN = "http://every-app-gateway.internal";
|
|
4
4
|
const AI_PROXY_PREFIX = "/api/ai/";
|
|
@@ -28,7 +28,8 @@ export function createGatewayFetch({ env, for: target, }) {
|
|
|
28
28
|
const bindingRequest = streamingRequest(`${BINDING_ORIGIN}${url.pathname}${url.search}`, outbound, headers);
|
|
29
29
|
return binding.fetch(bindingRequest);
|
|
30
30
|
}
|
|
31
|
-
const
|
|
31
|
+
const direct = directProviderPath(outbound.url, env);
|
|
32
|
+
const { provider, spec, path, search } = direct;
|
|
32
33
|
const dev = devProviders[provider];
|
|
33
34
|
if (!dev) {
|
|
34
35
|
throw new ConfigurationError(`Set ${settingsNames(spec)} in .dev.vars to call ${provider} in local development.`);
|
|
@@ -37,6 +38,11 @@ export function createGatewayFetch({ env, for: target, }) {
|
|
|
37
38
|
if (!url)
|
|
38
39
|
throw new ConfigurationError("Invalid provider proxy path.");
|
|
39
40
|
applyAttribution(headers, undefined);
|
|
41
|
+
// The default route in dev: the model filled in as the gateway would.
|
|
42
|
+
let body = outbound;
|
|
43
|
+
if (direct.fillModel !== null) {
|
|
44
|
+
body = await withModel(outbound, direct.fillModel, headers);
|
|
45
|
+
}
|
|
40
46
|
// The developer's key, put where the provider's entry says (a bearer
|
|
41
47
|
// token for the three today), as the gateway would put its own.
|
|
42
48
|
const auth = authorizationOf(spec.auth, { [spec.keyEnv]: dev.key });
|
|
@@ -46,11 +52,44 @@ export function createGatewayFetch({ env, for: target, }) {
|
|
|
46
52
|
for (const [name, value] of Object.entries(auth?.query ?? {})) {
|
|
47
53
|
url.searchParams.set(name, value);
|
|
48
54
|
}
|
|
49
|
-
const response = await fetch(streamingRequest(url.toString(),
|
|
55
|
+
const response = await fetch(streamingRequest(url.toString(), body, headers));
|
|
50
56
|
// What the gateway does to the provider's answer, so dev behaves alike.
|
|
51
57
|
return spec.normalizeResponse?.(path, response) ?? response;
|
|
52
58
|
};
|
|
53
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* `source` with `model` put into its JSON body where the app named none
|
|
62
|
+
* or named `default`, as the gateway's default route does. A body that
|
|
63
|
+
* isn't a JSON object goes through as it is.
|
|
64
|
+
*/
|
|
65
|
+
async function withModel(source, model, headers) {
|
|
66
|
+
if (!canIncludeBody(source.method) || !source.body)
|
|
67
|
+
return source;
|
|
68
|
+
const text = await source.clone().text();
|
|
69
|
+
let payload;
|
|
70
|
+
try {
|
|
71
|
+
payload = JSON.parse(text);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return source;
|
|
75
|
+
}
|
|
76
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
77
|
+
return source;
|
|
78
|
+
}
|
|
79
|
+
const record = payload;
|
|
80
|
+
if (record["model"] === undefined ||
|
|
81
|
+
record["model"] === null ||
|
|
82
|
+
record["model"] === AI_DEFAULT_MODEL) {
|
|
83
|
+
record["model"] = model;
|
|
84
|
+
}
|
|
85
|
+
headers.delete("content-length");
|
|
86
|
+
return new Request(source.url, {
|
|
87
|
+
method: source.method,
|
|
88
|
+
headers,
|
|
89
|
+
body: JSON.stringify(record),
|
|
90
|
+
signal: source.signal,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
54
93
|
/** A copy of `source` aimed at `url`, streaming its body rather than buffering it. */
|
|
55
94
|
export function streamingRequest(url, source, headers) {
|
|
56
95
|
const body = canIncludeBody(source.method) ? source.body : undefined;
|
|
@@ -82,19 +121,44 @@ function localProviders(env) {
|
|
|
82
121
|
}
|
|
83
122
|
return providers;
|
|
84
123
|
}
|
|
85
|
-
function directProviderPath(input) {
|
|
124
|
+
function directProviderPath(input, env) {
|
|
86
125
|
const incoming = new URL(input);
|
|
87
126
|
const match = incoming.pathname.match(/^\/api\/ai\/([a-z][a-z0-9-]*)(\/.*)?$/);
|
|
88
|
-
|
|
127
|
+
let provider = match?.[1];
|
|
128
|
+
let path = match?.[2] || "/";
|
|
129
|
+
let fillModel = null;
|
|
130
|
+
if (provider === AI_DEFAULT_PROVIDER) {
|
|
131
|
+
// The default in dev, by the gateway's rule on the developer's own
|
|
132
|
+
// `.dev.vars`: the first provider of the order with every setting set.
|
|
133
|
+
const values = env;
|
|
134
|
+
const resolution = resolveAiDefault({
|
|
135
|
+
choice: null,
|
|
136
|
+
isSet: (name) => typeof values[name] === "string" &&
|
|
137
|
+
values[name].trim().length > 0,
|
|
138
|
+
});
|
|
139
|
+
if (!resolution.configured) {
|
|
140
|
+
throw new ConfigurationError(`Set ${resolution.settings.join(" and ")} (or another model provider's key) in .dev.vars to call the default model in local development.`);
|
|
141
|
+
}
|
|
142
|
+
const route = path === AI_DEFAULT_ROUTES.chat
|
|
143
|
+
? resolution.chat
|
|
144
|
+
: path === AI_DEFAULT_ROUTES.embeddings
|
|
145
|
+
? resolution.embeddings
|
|
146
|
+
: undefined;
|
|
147
|
+
if (!route) {
|
|
148
|
+
throw new ConfigurationError(`The default model serves ${AI_DEFAULT_ROUTES.chat} and ${AI_DEFAULT_ROUTES.embeddings} only; call /api/ai/<provider>/… for anything else.`);
|
|
149
|
+
}
|
|
150
|
+
provider = resolution.provider;
|
|
151
|
+
path = route.path;
|
|
152
|
+
fillModel = path === resolution.chat.path ? resolution.model : route.model;
|
|
153
|
+
}
|
|
89
154
|
const spec = provider ? devAiProvider(provider) : undefined;
|
|
90
155
|
if (!provider || !spec) {
|
|
91
|
-
throw new ConfigurationError(`Direct local provider fallback only supports ${listOf(Object.keys(DEV_AI_PROVIDERS).map((name) => `${AI_PROXY_PREFIX}${name}`), "and")} paths.`);
|
|
156
|
+
throw new ConfigurationError(`Direct local provider fallback only supports ${listOf([AI_DEFAULT_PROVIDER, ...Object.keys(DEV_AI_PROVIDERS)].map((name) => `${AI_PROXY_PREFIX}${name}`), "and")} paths.`);
|
|
92
157
|
}
|
|
93
|
-
const path = match?.[2] || "/";
|
|
94
158
|
if (path.startsWith("//") || path.includes("\\")) {
|
|
95
159
|
throw new ConfigurationError("Invalid provider proxy path.");
|
|
96
160
|
}
|
|
97
|
-
return { provider, spec, path, search: incoming.search };
|
|
161
|
+
return { provider, spec, path, search: incoming.search, fillModel };
|
|
98
162
|
}
|
|
99
163
|
/** A provider's settings as a person reads them: `KEY`, or `KEY and SETTING`. */
|
|
100
164
|
function settingsNames(spec, conjunction = "and") {
|
package/dist/server/index.d.ts
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
export { ConfigurationError, IdentityError, everyApp, getEveryAppUser, hasScope, requireEveryAppUser, type EveryAppOptions, type EveryAppManifestLike, type EveryAppHandler, type EveryAppUser, type IdentityResult, } from "./everyApp.js";
|
|
7
7
|
export { AttributionError, type GatewayCallFor } from "./attribution.js";
|
|
8
8
|
export { authorizeUrl, type AuthorizeUrlOptions } from "./authorizeUrl.js";
|
|
9
|
+
export { connectPath, connectUrl, type ConnectUrlOptions, } from "./connectUrl.js";
|
|
9
10
|
export { createGatewayFetch, type CreateGatewayFetchOptions, type GatewayFetchEnv, } from "./gatewayFetch.js";
|
|
10
|
-
export {
|
|
11
|
+
export { ai, AI_DEFAULT_BASE_URL, type AiClient } from "./ai.js";
|
|
12
|
+
export { GatewayRefusalError, gatewayRefusal, devAccessTokenName, integrationFetch, integrationStatus, type EmailIntegrationStatus, type GatewayRefusal, type IntegrationFetchOptions, type IntegrationStatus, type IntegrationStatusOf, type OAuthClientIntegrationStatus, } from "./integrationFetch.js";
|
|
11
13
|
export { nodeServerHandler, type NodeServer, type NodeServerHandlerOptions, } from "./nodeServer.js";
|
|
12
14
|
export { sendEmail, type SendEmailOptions } from "./sendEmail.js";
|
|
13
15
|
export { listMembers, type ListMembersOptions } from "./listMembers.js";
|
|
14
16
|
export { emit, type EmitOptions } from "./emit.js";
|
|
15
|
-
export type { ApiKeyCapabilityId, CapabilityId, OAuthClientCapabilityId, EmailAddressInput, EmailAttachmentInput, SendEmailMessage, SendEmailResult, ListMembersResult, OrganizationMember, OrganizationMemberRole, EmitEventResult, } from "../internal/index.js";
|
|
17
|
+
export type { ApiKeyCapabilityId, CapabilityId, OAuthClientCapabilityId, ConnectionAccount, EmailAddressInput, EmailAttachmentInput, SendEmailMessage, SendEmailResult, ListMembersResult, OrganizationMember, OrganizationMemberRole, EmitEventResult, } from "../internal/index.js";
|
|
16
18
|
export { ToolError, defineTools, toolResult, type DefineToolsOptions, type PlainToolSchema, type StandardSchemaWithJSON, type ToolContext, type ToolDefinition, type ToolResult, type ToolSchema, type ToolSet, } from "./tools.js";
|
|
17
19
|
export type { ToolRisk } from "../internal/index.js";
|
|
18
20
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,4BAA4B,GAClC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,iBAAiB,EACjB,KAAK,UAAU,EACf,KAAK,wBAAwB,GAC9B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,YAAY,EACV,kBAAkB,EAClB,YAAY,EACZ,uBAAuB,EACvB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,SAAS,EACT,WAAW,EACX,UAAU,EACV,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,OAAO,GACb,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,WAAW,EACX,UAAU,EACV,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,EAAE,EAAE,mBAAmB,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,4BAA4B,GAClC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,iBAAiB,EACjB,KAAK,UAAU,EACf,KAAK,wBAAwB,GAC9B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,YAAY,EACV,kBAAkB,EAClB,YAAY,EACZ,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,SAAS,EACT,WAAW,EACX,UAAU,EACV,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,OAAO,GACb,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
export { ConfigurationError, IdentityError, everyApp, getEveryAppUser, hasScope, requireEveryAppUser, } from "./everyApp.js";
|
|
7
7
|
export { AttributionError } from "./attribution.js";
|
|
8
8
|
export { authorizeUrl } from "./authorizeUrl.js";
|
|
9
|
+
export { connectPath, connectUrl, } from "./connectUrl.js";
|
|
9
10
|
export { createGatewayFetch, } from "./gatewayFetch.js";
|
|
10
|
-
export {
|
|
11
|
+
export { ai, AI_DEFAULT_BASE_URL } from "./ai.js";
|
|
12
|
+
export { GatewayRefusalError, gatewayRefusal, devAccessTokenName, integrationFetch, integrationStatus, } from "./integrationFetch.js";
|
|
11
13
|
export { nodeServerHandler, } from "./nodeServer.js";
|
|
12
14
|
export { sendEmail } from "./sendEmail.js";
|
|
13
15
|
export { listMembers } from "./listMembers.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ApiKeyCapabilityId, type CapabilityId, type EmailCapabilityStatus, type OAuthClientCapabilityId } from "../internal/index.js";
|
|
1
|
+
import { type ApiKeyCapabilityId, type CapabilityId, type ConnectionAccount, type EmailCapabilityStatus, type OAuthClientCapabilityId } from "../internal/index.js";
|
|
2
2
|
import { type GatewayCallFor } from "./attribution.js";
|
|
3
3
|
export interface IntegrationFetchOptions {
|
|
4
4
|
/**
|
|
@@ -24,6 +24,13 @@ export interface IntegrationFetchOptions {
|
|
|
24
24
|
* pass one or the other ({@link AttributionError}).
|
|
25
25
|
*/
|
|
26
26
|
for?: GatewayCallFor;
|
|
27
|
+
/**
|
|
28
|
+
* For an OAuth provider the gateway holds connections for: whose
|
|
29
|
+
* connection the call uses, the person's (`user`, the default when a
|
|
30
|
+
* person is behind the call) or the organization's (`organization`,
|
|
31
|
+
* the default for background work). Sent as `x-everyapp-account`.
|
|
32
|
+
*/
|
|
33
|
+
account?: ConnectionAccount;
|
|
27
34
|
}
|
|
28
35
|
/**
|
|
29
36
|
* A `fetch` for one of the gateway's capabilities. Call the provider's real
|
|
@@ -47,7 +54,14 @@ export interface IntegrationFetchOptions {
|
|
|
47
54
|
* provider show <id> --save` writes it), put on the call as the gateway's
|
|
48
55
|
* spec says.
|
|
49
56
|
*/
|
|
50
|
-
export declare function integrationFetch({ env, provider, for: target, }: IntegrationFetchOptions): typeof fetch;
|
|
57
|
+
export declare function integrationFetch({ env, provider, for: target, account, }: IntegrationFetchOptions): typeof fetch;
|
|
58
|
+
/**
|
|
59
|
+
* The `.dev.vars` name of a held token for `provider` in local dev
|
|
60
|
+
* (`GOOGLE_WORKSPACE_ACCESS_TOKEN`, `GITHUB_ACCESS_TOKEN`): a token the
|
|
61
|
+
* developer obtained themselves, standing in for the connection the
|
|
62
|
+
* gateway would hold. Dev-only: the gateway never reads it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function devAccessTokenName(provider: string): string;
|
|
51
65
|
export interface IntegrationStatus {
|
|
52
66
|
/** The app's manifest lists the capability. */
|
|
53
67
|
requested: boolean;
|
|
@@ -92,6 +106,29 @@ export interface OAuthClientIntegrationStatus extends IntegrationStatus {
|
|
|
92
106
|
scopes?: readonly string[];
|
|
93
107
|
/** Slack's `user_scope` strings, when the sets name any. */
|
|
94
108
|
userScopes?: readonly string[];
|
|
109
|
+
/**
|
|
110
|
+
* Which flows the client supports: `app` (the app runs authorize and
|
|
111
|
+
* callback itself and keeps the tokens, the social networks), `gateway`
|
|
112
|
+
* (the Gateway holds the connection; API calls go through
|
|
113
|
+
* `integrationFetch` with the token injected) or `both` (Google,
|
|
114
|
+
* Microsoft, Slack, Discord and every registered OAuth provider).
|
|
115
|
+
*/
|
|
116
|
+
flow?: "app" | "gateway" | "both";
|
|
117
|
+
/** Where a person connects on the Gateway: `/connect/<id>`; see `connectUrl()`. */
|
|
118
|
+
connectPath?: string;
|
|
119
|
+
/**
|
|
120
|
+
* The connection the caller's account would use (the person's, or the
|
|
121
|
+
* organization's for background work), or null when there is none yet;
|
|
122
|
+
* `reconnect` when it exists but lacks a set the app declares or has
|
|
123
|
+
* expired, so the person has to connect again.
|
|
124
|
+
*/
|
|
125
|
+
connection?: {
|
|
126
|
+
account: ConnectionAccount;
|
|
127
|
+
label: string | null;
|
|
128
|
+
scopeSets: readonly string[];
|
|
129
|
+
ready: boolean;
|
|
130
|
+
reconnect?: boolean;
|
|
131
|
+
} | null;
|
|
95
132
|
}
|
|
96
133
|
/** The status of a capability: email's carries its senders, an OAuth client's its id. */
|
|
97
134
|
export type IntegrationStatusOf<P extends string> = P extends "email" ? EmailIntegrationStatus : P extends OAuthClientCapabilityId ? OAuthClientIntegrationStatus : IntegrationStatus;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrationFetch.d.ts","sourceRoot":"","sources":["../../src/server/integrationFetch.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"integrationFetch.d.ts","sourceRoot":"","sources":["../../src/server/integrationFetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CATALOG, ConfigurationError, GATEWAY_ERROR_HEADER, INTEGRATION_PROXY_PREFIX, OAUTH_TOKEN_BODY_LIMIT, allowedScopes, apiKeyHosts, authorizationFor, capabilitySettings, carriesToken, declaredScopeSets, describeTokenEndpoints, devProviderFile, grantedScopes, injectClientCredentials, integrationProxyPath, oauthClientCredentials, oauthClientOrigin, requestedScopes, requestedTokenScopes, resolveDevProvider, rewriteTokenRequestScope, scopeExcess, tokenEndpointFor, tokenGrantType, EMAIL_SENDERS_ENV, chooseEmailProvider, parseEmailList, parseEmailSender, } from "../internal/index.js";
|
|
1
|
+
import { ACCOUNT_HEADER, CATALOG, ConfigurationError, GATEWAY_ERROR_HEADER, INTEGRATION_PROXY_PREFIX, OAUTH_TOKEN_BODY_LIMIT, allowedScopes, apiKeyHosts, authorizationFor, capabilitySettings, carriesToken, declaredScopeSets, describeTokenEndpoints, devProviderFile, grantedScopes, heldTokenCallAllowed, injectClientCredentials, integrationProxyPath, oauthClientCredentials, oauthClientOrigin, oauthReadMethods, oauthWriteMethods, providerSettingPrefix, requestedScopes, requestedTokenScopes, resolveDevProvider, rewriteTokenRequestScope, scopeExcess, tokenEndpointFor, tokenGrantType, EMAIL_SENDERS_ENV, chooseEmailProvider, parseEmailList, parseEmailSender, } from "../internal/index.js";
|
|
2
2
|
import { applyAttribution, checkAttribution, } from "./attribution.js";
|
|
3
3
|
import { BINDING_ORIGIN, streamingRequest, } from "./gatewayFetch.js";
|
|
4
4
|
/**
|
|
@@ -23,7 +23,7 @@ import { BINDING_ORIGIN, streamingRequest, } from "./gatewayFetch.js";
|
|
|
23
23
|
* provider show <id> --save` writes it), put on the call as the gateway's
|
|
24
24
|
* spec says.
|
|
25
25
|
*/
|
|
26
|
-
export function integrationFetch({ env, provider, for: target, }) {
|
|
26
|
+
export function integrationFetch({ env, provider, for: target, account, }) {
|
|
27
27
|
const catalogEntry = fetchableCapability(provider);
|
|
28
28
|
checkAttribution(env, target, `integrationFetch("${provider}")`);
|
|
29
29
|
const values = env;
|
|
@@ -36,8 +36,23 @@ export function integrationFetch({ env, provider, for: target, }) {
|
|
|
36
36
|
const integration = catalogEntry ??
|
|
37
37
|
(binding ? undefined : devRegisteredProvider(provider, values));
|
|
38
38
|
const local = binding || !integration ? null : localCredentials(integration, values);
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// In dev an OAuth provider with a held token in `.dev.vars` serves API
|
|
40
|
+
// calls without the client (the token stands in for a connection).
|
|
41
|
+
const devToken = !binding && integration?.mode === "oauth_client" && integration.flow
|
|
42
|
+
? stringValue(values[devAccessTokenName(provider)])
|
|
43
|
+
: undefined;
|
|
44
|
+
// An OAuth provider with a Gateway-owned flow answers an API call
|
|
45
|
+
// without the client (`integration_not_connected`, below); a token
|
|
46
|
+
// endpoint call without the client throws when it is made.
|
|
47
|
+
const decidesLater = integration?.mode === "oauth_client" && integration.flow !== undefined;
|
|
48
|
+
if (!binding && !local && !devToken && integration && !decidesLater) {
|
|
49
|
+
const names = [
|
|
50
|
+
...capabilitySettings(integration),
|
|
51
|
+
...(integration.mode === "oauth_client" && integration.flow
|
|
52
|
+
? [devAccessTokenName(provider)]
|
|
53
|
+
: []),
|
|
54
|
+
];
|
|
55
|
+
throw new ConfigurationError(`${integration.label} is unavailable: deploy with the EVERY_APP_GATEWAY binding, or set ${names.join(", ")} in .dev.vars for local development.`);
|
|
41
56
|
}
|
|
42
57
|
return async (input, init) => {
|
|
43
58
|
const outbound = new Request(input, init);
|
|
@@ -55,6 +70,30 @@ export function integrationFetch({ env, provider, for: target, }) {
|
|
|
55
70
|
return binding.fetch(streamingRequest(`${BINDING_ORIGIN}${integrationProxyPath(provider, upstream)}`, outbound, headers));
|
|
56
71
|
}
|
|
57
72
|
if (integration.mode === "oauth_client") {
|
|
73
|
+
// A call to the provider's API with the token the gateway holds
|
|
74
|
+
// (docs/design/2026-10-oauth-providers.md section 7), for an entry
|
|
75
|
+
// that supports the Gateway-owned flow; a token endpoint stays the
|
|
76
|
+
// app-driven call it always was.
|
|
77
|
+
const toApi = integration.flow !== undefined &&
|
|
78
|
+
upstream.protocol === "https:" &&
|
|
79
|
+
integration.api.includes(upstream.host) &&
|
|
80
|
+
!tokenEndpointFor(integration, upstream, binding && integration.hostSetting ? undefined : values);
|
|
81
|
+
if (toApi) {
|
|
82
|
+
if (binding) {
|
|
83
|
+
if (account)
|
|
84
|
+
headers.set(ACCOUNT_HEADER, account);
|
|
85
|
+
applyAttribution(headers, target);
|
|
86
|
+
return binding.fetch(streamingRequest(`${BINDING_ORIGIN}${integrationProxyPath(provider, upstream)}`, outbound, headers));
|
|
87
|
+
}
|
|
88
|
+
return devHeldTokenCall({
|
|
89
|
+
provider,
|
|
90
|
+
capability: integration,
|
|
91
|
+
outbound,
|
|
92
|
+
upstream,
|
|
93
|
+
headers,
|
|
94
|
+
values,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
58
97
|
return oauthClientCall({
|
|
59
98
|
provider: provider,
|
|
60
99
|
capability: integration,
|
|
@@ -92,6 +131,93 @@ export function integrationFetch({ env, provider, for: target, }) {
|
|
|
92
131
|
return fetch(streamingRequest(upstream.toString(), outbound, headers));
|
|
93
132
|
};
|
|
94
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* The `.dev.vars` name of a held token for `provider` in local dev
|
|
136
|
+
* (`GOOGLE_WORKSPACE_ACCESS_TOKEN`, `GITHUB_ACCESS_TOKEN`): a token the
|
|
137
|
+
* developer obtained themselves, standing in for the connection the
|
|
138
|
+
* gateway would hold. Dev-only: the gateway never reads it.
|
|
139
|
+
*/
|
|
140
|
+
export function devAccessTokenName(provider) {
|
|
141
|
+
return `${providerSettingPrefix(provider)}ACCESS_TOKEN`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* An app's call to an OAuth provider's API in local dev, with no
|
|
145
|
+
* gateway: the gateway's held-token checks in its order (the app's
|
|
146
|
+
* declared sets, the method, the route), then the developer's own token
|
|
147
|
+
* from `.dev.vars` as the bearer, straight to the provider.
|
|
148
|
+
*/
|
|
149
|
+
async function devHeldTokenCall({ provider, capability, outbound, upstream, headers, values, }) {
|
|
150
|
+
const token = stringValue(values[devAccessTokenName(provider)]);
|
|
151
|
+
if (!token) {
|
|
152
|
+
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 at /connect/${provider}.`, { connectPath: `/connect/${provider}`, reconnect: false });
|
|
153
|
+
}
|
|
154
|
+
const policy = capability.scopePolicy;
|
|
155
|
+
const grant = policy
|
|
156
|
+
? devScopeSets(policy, provider, capability.label, values, "any")
|
|
157
|
+
: null;
|
|
158
|
+
if (grant && "problem" in grant) {
|
|
159
|
+
return devRefusal(403, grant.code, grant.problem);
|
|
160
|
+
}
|
|
161
|
+
const method = outbound.method.toUpperCase();
|
|
162
|
+
const reads = oauthReadMethods(capability).includes(method);
|
|
163
|
+
const writes = !reads &&
|
|
164
|
+
capability.access.includes("write") &&
|
|
165
|
+
oauthWriteMethods(capability).includes(method) &&
|
|
166
|
+
devDeclaredAccess(provider, values) === "write";
|
|
167
|
+
if (policy && grant) {
|
|
168
|
+
const allowed = heldTokenCallAllowed({
|
|
169
|
+
policy,
|
|
170
|
+
declaredSets: grant.sets,
|
|
171
|
+
// The developer's token stands for a connection granted every set.
|
|
172
|
+
connectionSets: grant.sets,
|
|
173
|
+
hosts: capability.api,
|
|
174
|
+
method,
|
|
175
|
+
upstream,
|
|
176
|
+
readMethods: oauthReadMethods(capability),
|
|
177
|
+
writes,
|
|
178
|
+
});
|
|
179
|
+
if (!allowed.ok) {
|
|
180
|
+
const status = {
|
|
181
|
+
integration_not_connected: 409,
|
|
182
|
+
integration_method_not_allowed: 405,
|
|
183
|
+
integration_scope_not_allowed: 403,
|
|
184
|
+
}[allowed.code];
|
|
185
|
+
return devRefusal(status, allowed.code, allowed.code === "integration_not_connected"
|
|
186
|
+
? `${capability.label} is connected without ${allowed.missingSets.join(", ")}.`
|
|
187
|
+
: allowed.message);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (!reads && !writes) {
|
|
191
|
+
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.`);
|
|
192
|
+
}
|
|
193
|
+
applyAttribution(headers, undefined);
|
|
194
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
195
|
+
for (const name of ["access_token", "token"]) {
|
|
196
|
+
upstream.searchParams.delete(name);
|
|
197
|
+
}
|
|
198
|
+
headers.delete("x-http-method-override");
|
|
199
|
+
return fetch(new Request(upstream.toString(), {
|
|
200
|
+
method,
|
|
201
|
+
headers,
|
|
202
|
+
body: method === "GET" || method === "HEAD" ? null : outbound.body,
|
|
203
|
+
signal: outbound.signal,
|
|
204
|
+
// As the gateway: the token never follows a redirect.
|
|
205
|
+
redirect: "manual",
|
|
206
|
+
...(method === "GET" || method === "HEAD" ? {} : { duplex: "half" }),
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
/** The `access` the manifest entry for `provider` declares in dev (EVERYAPP_DEV_CAPABILITIES), or null. */
|
|
210
|
+
function devDeclaredAccess(provider, values) {
|
|
211
|
+
const raw = values["EVERYAPP_DEV_CAPABILITIES"];
|
|
212
|
+
try {
|
|
213
|
+
const capabilities = typeof raw === "string" ? jsonRecord(JSON.parse(raw)) : undefined;
|
|
214
|
+
const entry = jsonRecord(capabilities?.[provider]);
|
|
215
|
+
return typeof entry?.["access"] === "string" ? entry["access"] : null;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
95
221
|
/**
|
|
96
222
|
* The developer's own key or client from `.dev.vars` (with EVERYAPP_DEV
|
|
97
223
|
* set), under the gateway's names; null until every setting it needs is.
|
|
@@ -125,6 +251,9 @@ async function oauthClientCall({ provider, capability, outbound, upstream, heade
|
|
|
125
251
|
return binding.fetch(streamingRequest(`${BINDING_ORIGIN}${integrationProxyPath(provider, upstream)}`, outbound, headers));
|
|
126
252
|
}
|
|
127
253
|
applyAttribution(headers, undefined);
|
|
254
|
+
if (!client) {
|
|
255
|
+
throw new ConfigurationError(`${capability.label}'s token endpoints need the client in .dev.vars for local development: set ${capabilitySettings(capability).join(", ")}.`);
|
|
256
|
+
}
|
|
128
257
|
// The gateway's scope checks, in its order, answering its refusals: an
|
|
129
258
|
// app sees in `everyapp dev` what the gateway will say.
|
|
130
259
|
const policy = capability.scopePolicy;
|
|
@@ -173,13 +302,15 @@ async function oauthClientCall({ provider, capability, outbound, upstream, heade
|
|
|
173
302
|
* `x-everyapp-error`) from the local-dev twin, so {@link gatewayRefusal}
|
|
174
303
|
* reads it the same way.
|
|
175
304
|
*/
|
|
176
|
-
function devRefusal(status, code, message) {
|
|
305
|
+
function devRefusal(status, code, message, extra = {}) {
|
|
177
306
|
const labels = {
|
|
178
307
|
400: "Bad Request",
|
|
179
308
|
403: "Forbidden",
|
|
309
|
+
405: "Method Not Allowed",
|
|
310
|
+
409: "Conflict",
|
|
180
311
|
502: "Bad Gateway",
|
|
181
312
|
};
|
|
182
|
-
return Response.json({ error: labels[status] ?? "Error", code, message }, { status, headers: { [GATEWAY_ERROR_HEADER]: code } });
|
|
313
|
+
return Response.json({ error: labels[status] ?? "Error", code, message, ...extra }, { status, headers: { [GATEWAY_ERROR_HEADER]: code } });
|
|
183
314
|
}
|
|
184
315
|
/**
|
|
185
316
|
* The scope sets the manifest declares for `id`, read from
|
|
@@ -191,7 +322,9 @@ function devRefusal(status, code, message) {
|
|
|
191
322
|
* doesn't offer). Fails closed like the gateway: a hand-rolled dev harness
|
|
192
323
|
* that passes no capabilities gets nothing.
|
|
193
324
|
*/
|
|
194
|
-
function devScopeSets(policy, id, label, values
|
|
325
|
+
function devScopeSets(policy, id, label, values,
|
|
326
|
+
/** The grant the entry needs: `write` for a token-endpoint call (the app-driven flow), `any` for a held-token call. */
|
|
327
|
+
grant = "write") {
|
|
195
328
|
const offered = Object.keys(policy.sets).join(", ");
|
|
196
329
|
const raw = values["EVERYAPP_DEV_CAPABILITIES"];
|
|
197
330
|
let capabilities;
|
|
@@ -211,11 +344,16 @@ function devScopeSets(policy, id, label, values) {
|
|
|
211
344
|
const entry = Object.hasOwn(capabilities, id)
|
|
212
345
|
? jsonRecord(capabilities[id])
|
|
213
346
|
: undefined;
|
|
214
|
-
// The gateway's check 2: listing the client for writing is the grant
|
|
215
|
-
|
|
347
|
+
// The gateway's check 2: listing the client for writing is the grant
|
|
348
|
+
// of the app-driven flow; a held-token call takes any listing.
|
|
349
|
+
const access = entry?.["access"];
|
|
350
|
+
if (!entry ||
|
|
351
|
+
(grant === "write"
|
|
352
|
+
? access !== "write"
|
|
353
|
+
: access !== "read" && access !== "write")) {
|
|
216
354
|
return {
|
|
217
355
|
code: "capability_not_requested",
|
|
218
|
-
problem: `This app doesn't list ${label} in its capabilities. Add capabilities: { ${JSON.stringify(id)}: { access: "write" } } to everyapp.config.ts and start everyapp dev again.`,
|
|
356
|
+
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.`,
|
|
219
357
|
};
|
|
220
358
|
}
|
|
221
359
|
const sets = declaredScopeSets(policy, entry);
|
|
@@ -315,6 +453,8 @@ async function checkTokenResponse(policy, capability, sets, response) {
|
|
|
315
453
|
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.",
|
|
316
454
|
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.",
|
|
317
455
|
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.",
|
|
456
|
+
space: "Ask for only the scopes integrationStatus() returns and connect again, or add the scope set to everyapp.config.ts and deploy.",
|
|
457
|
+
comma: "Ask for only the scopes integrationStatus() returns and connect again, or add the scope set to everyapp.config.ts and deploy.",
|
|
318
458
|
}[policy.dialect];
|
|
319
459
|
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}`);
|
|
320
460
|
}
|
|
@@ -431,9 +571,31 @@ function devOAuthClientStatus(id, capability, values) {
|
|
|
431
571
|
: null;
|
|
432
572
|
const origin = oauthClientOrigin(capability, values);
|
|
433
573
|
const policy = capability.scopePolicy;
|
|
574
|
+
// An entry with a Gateway-owned flow is listed for reading or writing;
|
|
575
|
+
// the app-driven flow alone needs the write grant.
|
|
434
576
|
const grant = policy
|
|
435
|
-
? devScopeSets(policy, id, capability.label, values)
|
|
577
|
+
? devScopeSets(policy, id, capability.label, values, capability.flow ? "any" : "write")
|
|
436
578
|
: null;
|
|
579
|
+
// The Gateway-owned flow's fields, in dev: a token in `.dev.vars`
|
|
580
|
+
// stands for a connection granted every declared set.
|
|
581
|
+
const flow = capability.flow ? "both" : "app";
|
|
582
|
+
const devToken = capability.flow
|
|
583
|
+
? stringValue(values[devAccessTokenName(id)])
|
|
584
|
+
: undefined;
|
|
585
|
+
const gateway = (sets) => capability.flow
|
|
586
|
+
? {
|
|
587
|
+
flow,
|
|
588
|
+
connectPath: `/connect/${id}`,
|
|
589
|
+
connection: devToken
|
|
590
|
+
? {
|
|
591
|
+
account: "user",
|
|
592
|
+
label: devAccessTokenName(id),
|
|
593
|
+
scopeSets: sets,
|
|
594
|
+
ready: true,
|
|
595
|
+
}
|
|
596
|
+
: null,
|
|
597
|
+
}
|
|
598
|
+
: { flow };
|
|
437
599
|
if (grant && "problem" in grant) {
|
|
438
600
|
if (grant.code === "capability_not_requested") {
|
|
439
601
|
return { requested: false, configured: false, ready: false };
|
|
@@ -443,16 +605,17 @@ function devOAuthClientStatus(id, capability, values) {
|
|
|
443
605
|
configured: Boolean(client),
|
|
444
606
|
ready: false,
|
|
445
607
|
scopeSets: [],
|
|
608
|
+
...gateway([]),
|
|
446
609
|
};
|
|
447
610
|
}
|
|
448
611
|
const asked = policy && grant ? requestedScopes(policy, grant.sets) : null;
|
|
449
612
|
return {
|
|
450
613
|
requested: true,
|
|
451
614
|
configured: Boolean(client),
|
|
452
|
-
ready: Boolean(client),
|
|
615
|
+
ready: Boolean(client) || Boolean(devToken),
|
|
453
616
|
...(client ? { clientId: client.clientId } : {}),
|
|
454
617
|
...(client && origin ? { origin } : {}),
|
|
455
|
-
...(client && grant && asked
|
|
618
|
+
...((client || devToken) && grant && asked
|
|
456
619
|
? {
|
|
457
620
|
scopeSets: grant.sets,
|
|
458
621
|
scopes: asked.scopes,
|
|
@@ -461,6 +624,7 @@ function devOAuthClientStatus(id, capability, values) {
|
|
|
461
624
|
: {}),
|
|
462
625
|
}
|
|
463
626
|
: {}),
|
|
627
|
+
...gateway(grant?.sets ?? []),
|
|
464
628
|
};
|
|
465
629
|
}
|
|
466
630
|
/**
|