@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.
Files changed (73) hide show
  1. package/dist/cloudflare/index.d.ts +2 -2
  2. package/dist/cloudflare/index.d.ts.map +1 -1
  3. package/dist/cloudflare/index.js +1 -1
  4. package/dist/cloudflare/sentinel.d.ts +39 -1
  5. package/dist/cloudflare/sentinel.d.ts.map +1 -1
  6. package/dist/cloudflare/sentinel.js +22 -1
  7. package/dist/cloudflare/worker.d.ts +119 -2
  8. package/dist/cloudflare/worker.d.ts.map +1 -1
  9. package/dist/cloudflare/worker.js +90 -6
  10. package/dist/internal/aiDefault.d.ts +141 -0
  11. package/dist/internal/aiDefault.d.ts.map +1 -0
  12. package/dist/internal/aiDefault.js +204 -0
  13. package/dist/internal/aiProviders.d.ts +4 -1
  14. package/dist/internal/aiProviders.d.ts.map +1 -1
  15. package/dist/internal/aiProviders.js +5 -1
  16. package/dist/internal/appSettings.d.ts +20 -2
  17. package/dist/internal/appSettings.d.ts.map +1 -1
  18. package/dist/internal/appSettings.js +53 -7
  19. package/dist/internal/audit.d.ts +108 -0
  20. package/dist/internal/audit.d.ts.map +1 -0
  21. package/dist/internal/audit.js +235 -0
  22. package/dist/internal/capabilities.d.ts +275 -6
  23. package/dist/internal/capabilities.d.ts.map +1 -1
  24. package/dist/internal/capabilities.js +111 -0
  25. package/dist/internal/catalog/index.d.ts +38 -1
  26. package/dist/internal/catalog/index.d.ts.map +1 -1
  27. package/dist/internal/catalog/inference.d.ts +38 -1
  28. package/dist/internal/catalog/inference.d.ts.map +1 -1
  29. package/dist/internal/catalog/inference.js +33 -1
  30. package/dist/internal/devProviders.d.ts +2 -2
  31. package/dist/internal/devProviders.d.ts.map +1 -1
  32. package/dist/internal/devProviders.js +19 -1
  33. package/dist/internal/events.d.ts +13 -2
  34. package/dist/internal/events.d.ts.map +1 -1
  35. package/dist/internal/events.js +19 -3
  36. package/dist/internal/index.d.ts +4 -0
  37. package/dist/internal/index.d.ts.map +1 -1
  38. package/dist/internal/index.js +4 -0
  39. package/dist/internal/oauthPresets.d.ts +29 -0
  40. package/dist/internal/oauthPresets.d.ts.map +1 -0
  41. package/dist/internal/oauthPresets.js +534 -0
  42. package/dist/internal/oauthScopes.d.ts +56 -1
  43. package/dist/internal/oauthScopes.d.ts.map +1 -1
  44. package/dist/internal/oauthScopes.js +98 -4
  45. package/dist/internal/oauthSpec.d.ts +59 -0
  46. package/dist/internal/oauthSpec.d.ts.map +1 -0
  47. package/dist/internal/oauthSpec.js +676 -0
  48. package/dist/internal/protocol.d.ts +11 -0
  49. package/dist/internal/protocol.d.ts.map +1 -1
  50. package/dist/internal/protocol.js +9 -0
  51. package/dist/internal/providerRegistry.d.ts +17 -6
  52. package/dist/internal/providerRegistry.d.ts.map +1 -1
  53. package/dist/internal/providerRegistry.js +47 -2
  54. package/dist/internal/providerSpec.d.ts +11 -7
  55. package/dist/internal/providerSpec.d.ts.map +1 -1
  56. package/dist/internal/providerSpec.js +134 -37
  57. package/dist/internal/signInProviders.d.ts.map +1 -1
  58. package/dist/internal/signInProviders.js +11 -4
  59. package/dist/server/ai.d.ts +45 -0
  60. package/dist/server/ai.d.ts.map +1 -0
  61. package/dist/server/ai.js +40 -0
  62. package/dist/server/connectUrl.d.ts +21 -0
  63. package/dist/server/connectUrl.d.ts.map +1 -0
  64. package/dist/server/connectUrl.js +37 -0
  65. package/dist/server/gatewayFetch.d.ts.map +1 -1
  66. package/dist/server/gatewayFetch.js +72 -8
  67. package/dist/server/index.d.ts +4 -2
  68. package/dist/server/index.d.ts.map +1 -1
  69. package/dist/server/index.js +3 -1
  70. package/dist/server/integrationFetch.d.ts +39 -2
  71. package/dist/server/integrationFetch.d.ts.map +1 -1
  72. package/dist/server/integrationFetch.js +177 -13
  73. 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":"AASA,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,CA8D1C;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,OAAO,GACf,OAAO,CAUT"}
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 { provider, spec, path, search } = directProviderPath(outbound.url);
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(), outbound, headers));
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
- const provider = match?.[1];
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") {
@@ -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 { GatewayRefusalError, gatewayRefusal, integrationFetch, integrationStatus, type EmailIntegrationStatus, type GatewayRefusal, type IntegrationFetchOptions, type IntegrationStatus, type IntegrationStatusOf, type OAuthClientIntegrationStatus, } from "./integrationFetch.js";
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"}
@@ -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 { GatewayRefusalError, gatewayRefusal, integrationFetch, integrationStatus, } from "./integrationFetch.js";
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,EA2BL,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAE1B,KAAK,uBAAuB,EAO7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAO1B,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,QAAQ,EAAE,kBAAkB,GAAG,uBAAuB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IACvE;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,cAAc,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,QAAQ,EACR,GAAG,EAAE,MAAM,GACZ,EAAE,uBAAuB,GAAG,OAAO,KAAK,CAkGxC;AA+XD,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,SAAS,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,UAAU,EAAE,OAAO,CAAC;IACpB,8BAA8B;IAC9B,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GACpD,IAAI,CAAC,qBAAqB,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC,CAAC;AAEjE;;;;GAIG;AACH,MAAM,WAAW,4BAA6B,SAAQ,iBAAiB;IACrE,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,4DAA4D;IAC5D,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,yFAAyF;AACzF,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,OAAO,GACjE,sBAAsB,GACtB,CAAC,SAAS,uBAAuB,GAC/B,4BAA4B,GAC5B,iBAAiB,CAAC;AAExB;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,CAAC,SAAS,YAAY,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,EACtC,EACA,GAAG,EACH,QAAQ,GACT,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,CAAC,CAAC;CACb,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAoElC;AA2FD,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,KAAM,YAAW,cAAc;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,cAAc;CAMtD;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAWhC"}
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
- if (!binding && !local && integration) {
40
- 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
+ // 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
- if (entry?.["access"] !== "write") {
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@every-app/sdk",
3
- "version": "0.3.0-rc.2",
3
+ "version": "0.3.0-rc.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {