@every-app/sdk 0.2.0 → 0.3.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +609 -3
  3. package/dist/cloudflare/catalogSettings.generated.d.ts +81 -0
  4. package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
  5. package/dist/cloudflare/catalogSettings.generated.js +174 -0
  6. package/dist/cloudflare/index.d.ts +23 -0
  7. package/dist/cloudflare/index.d.ts.map +1 -0
  8. package/dist/cloudflare/index.js +24 -0
  9. package/dist/cloudflare/sentinel.d.ts +127 -0
  10. package/dist/cloudflare/sentinel.d.ts.map +1 -0
  11. package/dist/cloudflare/sentinel.js +85 -0
  12. package/dist/cloudflare/worker.d.ts +805 -0
  13. package/dist/cloudflare/worker.d.ts.map +1 -0
  14. package/dist/cloudflare/worker.js +997 -0
  15. package/dist/dev/index.d.ts +8 -0
  16. package/dist/dev/index.d.ts.map +1 -0
  17. package/dist/dev/index.js +7 -0
  18. package/dist/dev/keypair.d.ts +26 -0
  19. package/dist/dev/keypair.d.ts.map +1 -0
  20. package/dist/dev/keypair.js +62 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +5 -0
  24. package/dist/internal/aiDefault.d.ts +204 -0
  25. package/dist/internal/aiDefault.d.ts.map +1 -0
  26. package/dist/internal/aiDefault.js +334 -0
  27. package/dist/internal/aiProviders.d.ts +72 -0
  28. package/dist/internal/aiProviders.d.ts.map +1 -0
  29. package/dist/internal/aiProviders.js +233 -0
  30. package/dist/internal/appSettings.d.ts +315 -0
  31. package/dist/internal/appSettings.d.ts.map +1 -0
  32. package/dist/internal/appSettings.js +549 -0
  33. package/dist/internal/audit.d.ts +108 -0
  34. package/dist/internal/audit.d.ts.map +1 -0
  35. package/dist/internal/audit.js +235 -0
  36. package/dist/internal/capabilities.d.ts +3174 -0
  37. package/dist/internal/capabilities.d.ts.map +1 -0
  38. package/dist/internal/capabilities.js +1110 -0
  39. package/dist/internal/catalog/commerce.d.ts +394 -0
  40. package/dist/internal/catalog/commerce.d.ts.map +1 -0
  41. package/dist/internal/catalog/commerce.js +477 -0
  42. package/dist/internal/catalog/comms.d.ts +375 -0
  43. package/dist/internal/catalog/comms.d.ts.map +1 -0
  44. package/dist/internal/catalog/comms.js +520 -0
  45. package/dist/internal/catalog/index.d.ts +1973 -0
  46. package/dist/internal/catalog/index.d.ts.map +1 -0
  47. package/dist/internal/catalog/index.js +19 -0
  48. package/dist/internal/catalog/inference.d.ts +393 -0
  49. package/dist/internal/catalog/inference.d.ts.map +1 -0
  50. package/dist/internal/catalog/inference.js +460 -0
  51. package/dist/internal/catalog/infra.d.ts +423 -0
  52. package/dist/internal/catalog/infra.d.ts.map +1 -0
  53. package/dist/internal/catalog/infra.js +543 -0
  54. package/dist/internal/catalog/search.d.ts +400 -0
  55. package/dist/internal/catalog/search.d.ts.map +1 -0
  56. package/dist/internal/catalog/search.js +463 -0
  57. package/dist/internal/devProviders.d.ts +49 -0
  58. package/dist/internal/devProviders.d.ts.map +1 -0
  59. package/dist/internal/devProviders.js +80 -0
  60. package/dist/internal/email.d.ts +280 -0
  61. package/dist/internal/email.d.ts.map +1 -0
  62. package/dist/internal/email.js +613 -0
  63. package/dist/internal/events.d.ts +193 -0
  64. package/dist/internal/events.d.ts.map +1 -0
  65. package/dist/internal/events.js +367 -0
  66. package/dist/internal/gatewaySettings.d.ts +83 -0
  67. package/dist/internal/gatewaySettings.d.ts.map +1 -0
  68. package/dist/internal/gatewaySettings.js +215 -0
  69. package/dist/internal/identity.d.ts +57 -5
  70. package/dist/internal/identity.d.ts.map +1 -1
  71. package/dist/internal/identity.js +99 -22
  72. package/dist/internal/index.d.ts +22 -1
  73. package/dist/internal/index.d.ts.map +1 -1
  74. package/dist/internal/index.js +22 -1
  75. package/dist/internal/members.d.ts +36 -0
  76. package/dist/internal/members.d.ts.map +1 -0
  77. package/dist/internal/members.js +7 -0
  78. package/dist/internal/oauthClient.d.ts +61 -0
  79. package/dist/internal/oauthClient.d.ts.map +1 -0
  80. package/dist/internal/oauthClient.js +133 -0
  81. package/dist/internal/oauthPresets.d.ts +33 -0
  82. package/dist/internal/oauthPresets.d.ts.map +1 -0
  83. package/dist/internal/oauthPresets.js +1064 -0
  84. package/dist/internal/oauthScopes.d.ts +172 -0
  85. package/dist/internal/oauthScopes.d.ts.map +1 -0
  86. package/dist/internal/oauthScopes.js +510 -0
  87. package/dist/internal/oauthSpec.d.ts +102 -0
  88. package/dist/internal/oauthSpec.d.ts.map +1 -0
  89. package/dist/internal/oauthSpec.js +789 -0
  90. package/dist/internal/organizationName.d.ts +18 -0
  91. package/dist/internal/organizationName.d.ts.map +1 -0
  92. package/dist/internal/organizationName.js +24 -0
  93. package/dist/internal/protocol.d.ts +150 -3
  94. package/dist/internal/protocol.d.ts.map +1 -1
  95. package/dist/internal/protocol.js +163 -2
  96. package/dist/internal/providerApps.d.ts +75 -0
  97. package/dist/internal/providerApps.d.ts.map +1 -0
  98. package/dist/internal/providerApps.js +59 -0
  99. package/dist/internal/providerRegistry.d.ts +218 -0
  100. package/dist/internal/providerRegistry.d.ts.map +1 -0
  101. package/dist/internal/providerRegistry.js +432 -0
  102. package/dist/internal/providerSpec.d.ts +66 -0
  103. package/dist/internal/providerSpec.d.ts.map +1 -0
  104. package/dist/internal/providerSpec.js +1154 -0
  105. package/dist/internal/remoteMcp.d.ts +188 -0
  106. package/dist/internal/remoteMcp.d.ts.map +1 -0
  107. package/dist/internal/remoteMcp.js +543 -0
  108. package/dist/internal/signInProviders.d.ts +104 -0
  109. package/dist/internal/signInProviders.d.ts.map +1 -0
  110. package/dist/internal/signInProviders.js +245 -0
  111. package/dist/internal/tools.d.ts +115 -0
  112. package/dist/internal/tools.d.ts.map +1 -0
  113. package/dist/internal/tools.js +80 -0
  114. package/dist/manifest.d.ts +238 -0
  115. package/dist/manifest.d.ts.map +1 -0
  116. package/dist/manifest.js +30 -0
  117. package/dist/server/ai.d.ts +111 -0
  118. package/dist/server/ai.d.ts.map +1 -0
  119. package/dist/server/ai.js +145 -0
  120. package/dist/server/aiBinding.d.ts +68 -0
  121. package/dist/server/aiBinding.d.ts.map +1 -0
  122. package/dist/server/aiBinding.js +294 -0
  123. package/dist/server/attribution.d.ts +43 -0
  124. package/dist/server/attribution.d.ts.map +1 -0
  125. package/dist/server/attribution.js +75 -0
  126. package/dist/server/authorizeUrl.d.ts +43 -0
  127. package/dist/server/authorizeUrl.d.ts.map +1 -0
  128. package/dist/server/authorizeUrl.js +106 -0
  129. package/dist/server/connectUrl.d.ts +21 -0
  130. package/dist/server/connectUrl.d.ts.map +1 -0
  131. package/dist/server/connectUrl.js +37 -0
  132. package/dist/server/context.d.ts +68 -0
  133. package/dist/server/context.d.ts.map +1 -0
  134. package/dist/server/context.js +117 -0
  135. package/dist/server/emailBinding.d.ts +72 -0
  136. package/dist/server/emailBinding.d.ts.map +1 -0
  137. package/dist/server/emailBinding.js +124 -0
  138. package/dist/server/emit.d.ts +63 -0
  139. package/dist/server/emit.d.ts.map +1 -0
  140. package/dist/server/emit.js +101 -0
  141. package/dist/server/env.d.ts +44 -0
  142. package/dist/server/env.d.ts.map +1 -0
  143. package/dist/server/env.js +1 -0
  144. package/dist/server/everyApp.d.ts +65 -19
  145. package/dist/server/everyApp.d.ts.map +1 -1
  146. package/dist/server/everyApp.js +195 -30
  147. package/dist/server/fromMcpTools.d.ts +89 -0
  148. package/dist/server/fromMcpTools.d.ts.map +1 -0
  149. package/dist/server/fromMcpTools.js +192 -0
  150. package/dist/server/gatewayFetch.d.ts +54 -14
  151. package/dist/server/gatewayFetch.d.ts.map +1 -1
  152. package/dist/server/gatewayFetch.js +201 -37
  153. package/dist/server/gatewayUrl.d.ts +13 -0
  154. package/dist/server/gatewayUrl.d.ts.map +1 -0
  155. package/dist/server/gatewayUrl.js +34 -0
  156. package/dist/server/index.d.ts +23 -3
  157. package/dist/server/index.d.ts.map +1 -1
  158. package/dist/server/index.js +19 -2
  159. package/dist/server/integrationFetch.d.ts +162 -0
  160. package/dist/server/integrationFetch.d.ts.map +1 -0
  161. package/dist/server/integrationFetch.js +688 -0
  162. package/dist/server/listMembers.d.ts +32 -0
  163. package/dist/server/listMembers.d.ts.map +1 -0
  164. package/dist/server/listMembers.js +71 -0
  165. package/dist/server/nodeServer.d.ts +48 -0
  166. package/dist/server/nodeServer.d.ts.map +1 -0
  167. package/dist/server/nodeServer.js +45 -0
  168. package/dist/server/refusal.d.ts +55 -0
  169. package/dist/server/refusal.d.ts.map +1 -0
  170. package/dist/server/refusal.js +193 -0
  171. package/dist/server/sendEmail.d.ts +39 -0
  172. package/dist/server/sendEmail.d.ts.map +1 -0
  173. package/dist/server/sendEmail.js +114 -0
  174. package/dist/server/tools.d.ts +184 -0
  175. package/dist/server/tools.d.ts.map +1 -0
  176. package/dist/server/tools.js +347 -0
  177. package/dist/server/toolsEndpoint.d.ts +32 -0
  178. package/dist/server/toolsEndpoint.d.ts.map +1 -0
  179. package/dist/server/toolsEndpoint.js +237 -0
  180. package/dist/tanstack-start/index.d.ts +9 -0
  181. package/dist/tanstack-start/index.d.ts.map +1 -0
  182. package/dist/tanstack-start/index.js +29 -0
  183. package/dist/testing/index.d.ts +93 -0
  184. package/dist/testing/index.d.ts.map +1 -0
  185. package/dist/testing/index.js +156 -0
  186. package/package.json +42 -8
  187. package/dist/server/mcp.d.ts +0 -66
  188. package/dist/server/mcp.d.ts.map +0 -1
  189. package/dist/server/mcp.js +0 -147
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Scope sets for an `oauth_client` capability with a `scopePolicy`
3
+ * (docs/capabilities.md "OAuth clients"): what an app's manifest entry
4
+ * declares, what that lets the app ask the provider for, how each provider
5
+ * spells what it granted, and which API routes a set unlocks. Pure
6
+ * functions over the registry, shared by the gateway's token-endpoint
7
+ * check, the SDK's local-dev twin of it and, later, the proxy and the
8
+ * outbound Worker. Nothing here reads a secret or a token value.
9
+ */
10
+ import type { OAuthScopeDialect, OAuthScopePolicy } from "./capabilities.js";
11
+ import type { TokenRequest } from "./oauthClient.js";
12
+ /** Scopes by the token they belong to: the bot or only token, and Slack's user token. */
13
+ export interface ScopeList {
14
+ scopes: string[];
15
+ userScopes: string[];
16
+ }
17
+ /** What a token response says it granted; a field is present when that kind of token is. */
18
+ export interface GrantedScopes {
19
+ scopes?: string[];
20
+ userScopes?: string[];
21
+ }
22
+ /**
23
+ * The scope set names a manifest entry declares, or null when the entry
24
+ * voids its grant: `scopes` missing, not an array, empty, a non-string
25
+ * element, a name listed twice, or any name the policy doesn't offer. An
26
+ * unknown name voids the whole grant rather than narrowing it silently, so
27
+ * a newer CLI's set name reaching an older gateway is refused outright and
28
+ * a message can say so.
29
+ */
30
+ export declare function declaredScopeSets(policy: OAuthScopePolicy, entry: {
31
+ scopes?: unknown;
32
+ } | undefined): string[] | null;
33
+ /**
34
+ * What an app with `sets` asks the provider for: the sets' scopes in
35
+ * registry order, deduplicated, then `alwaysRequested`. The status hands
36
+ * these out and the authorize URL (Microsoft: the token leg too) carries
37
+ * them verbatim.
38
+ */
39
+ export declare function requestedScopes(policy: OAuthScopePolicy, sets: readonly string[]): ScopeList;
40
+ /**
41
+ * What a token an app with `sets` is handed may carry: the requested
42
+ * scopes and the tolerated ones, in the dialect's canonical spelling so a
43
+ * granted list canonicalised the same way compares exactly.
44
+ */
45
+ export declare function allowedScopes(policy: OAuthScopePolicy, sets: readonly string[]): ScopeList;
46
+ /**
47
+ * Whether a token response body carries a token: an access, refresh or ID
48
+ * token, or Slack's user token under `authed_user`. A 2xx without one
49
+ * (Slack's `ok: false`) is an error the app must see, not a grant.
50
+ */
51
+ export declare function carriesToken(json: unknown): boolean;
52
+ /**
53
+ * The scopes a token response says it granted, canonicalised per dialect,
54
+ * or null when the answer names none for a token it carries, which fails
55
+ * closed: every documented success response of the three providers names
56
+ * them (map-providers: Google and Microsoft `scope`, Slack `scope` and
57
+ * `authed_user.scope`), so a missing field is an answer the gateway can't
58
+ * hold to anything.
59
+ */
60
+ export declare function grantedScopes(policy: OAuthScopePolicy, json: unknown): GrantedScopes | null;
61
+ /**
62
+ * What a token response granted, for the Gateway-owned flow: as
63
+ * {@link grantedScopes}, except that a `space` or `comma` provider that
64
+ * names no `scope` at all is taken at its word (RFC 6749 §5.1: the field
65
+ * is optional when the grant matches the request), so `requested` stands.
66
+ * The three parsed dialects keep failing closed.
67
+ */
68
+ export declare function grantedScopesOrRequested(policy: OAuthScopePolicy, json: unknown, requested: ScopeList): GrantedScopes | null;
69
+ /** Why the proxy refuses a held-token call, with what would let it through. */
70
+ export type HeldTokenRefusal = {
71
+ ok: false;
72
+ code: "integration_not_connected";
73
+ /** The declared sets the connection was not granted. */
74
+ missingSets: string[];
75
+ } | {
76
+ ok: false;
77
+ code: "integration_scope_not_allowed" | "integration_method_not_allowed";
78
+ message: string;
79
+ };
80
+ /**
81
+ * Whether an app's call with a held token goes through, and under which
82
+ * set (docs/design/2026-10-oauth-providers.md section 7). In order: every
83
+ * set the app declares must be among the connection's (else the person
84
+ * connects again: `integration_not_connected` with the missing sets); the
85
+ * host must be one of the provider's `api` hosts; the path passes the
86
+ * segment rules (`scopeRouteAllows`'s: no batch tunnels, no dot segments,
87
+ * no denied segments); then a declared set with routes must cover the
88
+ * call, or a declared set without routes covers its hosts by access: a
89
+ * read set unlocks `readMethods` (GET and HEAD by default), a write set
90
+ * every method the provider offers. `writes` says whether the method is
91
+ * one the grant allows beyond reads, decided by the caller as for an
92
+ * API-key provider.
93
+ */
94
+ export declare function heldTokenCallAllowed({ policy, declaredSets, connectionSets, hosts, method, upstream, readMethods, writes, }: {
95
+ policy: OAuthScopePolicy;
96
+ declaredSets: readonly string[];
97
+ connectionSets: readonly string[];
98
+ /** The provider's `api` hosts. */
99
+ hosts: readonly string[];
100
+ method: string;
101
+ upstream: URL;
102
+ readMethods?: readonly string[];
103
+ /** Whether `method` is a write the app's grant allows. */
104
+ writes: boolean;
105
+ }): {
106
+ ok: true;
107
+ set: string;
108
+ } | HeldTokenRefusal;
109
+ /**
110
+ * What `granted` carries beyond `allowed`, per token kind, for a refusal's
111
+ * message and log line: the user token's extras as `user:<scope>`. Both
112
+ * lists are in the dialect's canonical spelling already.
113
+ */
114
+ export declare function scopeExcess(granted: GrantedScopes, allowed: ScopeList): string[];
115
+ /**
116
+ * The scopes a token request itself asks for (`scope`, and Slack's
117
+ * `user_scope`, in any case) in its form or JSON body, canonicalised per
118
+ * dialect for comparison with `allowedScopes()`, or null when the body
119
+ * names none. The query string is never read: the gateway refuses a
120
+ * request that puts these there, so only one place decides.
121
+ */
122
+ export declare function requestedTokenScopes(policy: OAuthScopePolicy, request: TokenRequest): GrantedScopes | null;
123
+ /**
124
+ * `request` with its `scope`, in any case, as the policy's `requestScope`
125
+ * says: replaced by the requested list of the app's `sets`, dropped, or
126
+ * kept as sent. A separate step after `injectClientCredentials()`, whose
127
+ * re-serialised form or JSON body this edits the same way; any other body
128
+ * is left alone.
129
+ */
130
+ export declare function rewriteTokenRequestScope(policy: OAuthScopePolicy, sets: readonly string[], request: TokenRequest): TokenRequest;
131
+ /**
132
+ * The declared set that unlocks `method upstream`, or null. Not wired to
133
+ * any live path yet: it becomes the proxy's boundary once the gateway
134
+ * holds tokens, and the outbound Worker's for dispatched apps before that.
135
+ *
136
+ * In order: https only; the host must be on one of the declared sets'
137
+ * routes; the raw pathname is refused when it holds `\`, `//` or `%00`;
138
+ * each raw segment, and each percent-decoding of it while a `%` remains
139
+ * (a provider's layers may each decode once), is refused when it won't
140
+ * decode, holds a control or format character or a backslash, is one of
141
+ * the policy's denied segments in any case, or splits on `/` into a dot
142
+ * segment: `.` or `..`, with or without a `;` parameter (`..;x`), which
143
+ * Google's front end resolves like the bare one. The denied comparison is
144
+ * of the whole segment, because an encoded slash is part of one id to the
145
+ * provider, never a separator: a Search Console site URL whose own path
146
+ * holds `batch` stays matchable, and `2%2FmeasurementProtocolSecrets` is
147
+ * a data stream id that doesn't exist, not the secrets. Then the raw
148
+ * pathname, case preserved, is matched: GET and HEAD need a `paths` entry
149
+ * (exact, or a prefix when it ends in `/`), POST needs a `posts` pattern,
150
+ * and no other method goes through. Matching the raw path is what lets
151
+ * Search Console's `sites/https%3A%2F%2Fexample.com%2F/searchAnalytics/query`
152
+ * match `[^/]+` while `..`, batch tunnels and secrets stay refused however
153
+ * encoded. `X-HTTP-Method-Override`, `access_token` and `token` parameters
154
+ * are the proxy's to strip before it asks; nothing here reads them. The
155
+ * proxy fetches with `redirect: "manual"`: Google answers a dot segment it
156
+ * resolves with a 302 to the resolved path, and following one would carry
157
+ * the token to a path this never matched.
158
+ */
159
+ export declare function scopeRouteAllows(policy: OAuthScopePolicy, sets: readonly string[], method: string, upstream: URL): string | null;
160
+ /** `scopes` in the dialect's one spelling, deduplicated, order kept. */
161
+ /**
162
+ * `scopes` in the dialect's canonical spelling (Google's long names,
163
+ * Microsoft's bare lower-case names), for comparing a set's scopes with
164
+ * what a token response says it granted.
165
+ */
166
+ export declare function canonicalScopes(dialect: OAuthScopeDialect, scopes: readonly string[]): string[];
167
+ /**
168
+ * `text` as a JSON object, or null for anything else (an array, a scalar,
169
+ * not JSON). Shared with the gateway's read of a provider's token answer.
170
+ */
171
+ export declare function jsonObject(text: string): Record<string, unknown> | null;
172
+ //# sourceMappingURL=oauthScopes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthScopes.d.ts","sourceRoot":"","sources":["../../src/internal/oauthScopes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,510 @@
1
+ /**
2
+ * The scope set names a manifest entry declares, or null when the entry
3
+ * voids its grant: `scopes` missing, not an array, empty, a non-string
4
+ * element, a name listed twice, or any name the policy doesn't offer. An
5
+ * unknown name voids the whole grant rather than narrowing it silently, so
6
+ * a newer CLI's set name reaching an older gateway is refused outright and
7
+ * a message can say so.
8
+ */
9
+ export function declaredScopeSets(policy, entry) {
10
+ const declared = entry?.scopes;
11
+ if (!Array.isArray(declared) || declared.length === 0)
12
+ return null;
13
+ const names = [];
14
+ for (const name of declared) {
15
+ if (typeof name !== "string" ||
16
+ !Object.hasOwn(policy.sets, name) ||
17
+ names.includes(name)) {
18
+ return null;
19
+ }
20
+ names.push(name);
21
+ }
22
+ return names;
23
+ }
24
+ /**
25
+ * What an app with `sets` asks the provider for: the sets' scopes in
26
+ * registry order, deduplicated, then `alwaysRequested`. The status hands
27
+ * these out and the authorize URL (Microsoft: the token leg too) carries
28
+ * them verbatim.
29
+ */
30
+ export function requestedScopes(policy, sets) {
31
+ const scopes = [];
32
+ const userScopes = [];
33
+ for (const [, set] of setsOf(policy, sets)) {
34
+ pushNew(scopes, set.scopes);
35
+ pushNew(userScopes, set.userScopes ?? []);
36
+ }
37
+ pushNew(scopes, policy.alwaysRequested);
38
+ return { scopes, userScopes };
39
+ }
40
+ /**
41
+ * What a token an app with `sets` is handed may carry: the requested
42
+ * scopes and the tolerated ones, in the dialect's canonical spelling so a
43
+ * granted list canonicalised the same way compares exactly.
44
+ */
45
+ export function allowedScopes(policy, sets) {
46
+ const requested = requestedScopes(policy, sets);
47
+ return {
48
+ scopes: canonical(policy.dialect, [
49
+ ...requested.scopes,
50
+ ...policy.tolerated,
51
+ ]),
52
+ userScopes: canonical(policy.dialect, requested.userScopes),
53
+ };
54
+ }
55
+ /**
56
+ * Whether a token response body carries a token: an access, refresh or ID
57
+ * token, or Slack's user token under `authed_user`. A 2xx without one
58
+ * (Slack's `ok: false`) is an error the app must see, not a grant.
59
+ */
60
+ export function carriesToken(json) {
61
+ const body = record(json);
62
+ if (!body)
63
+ return false;
64
+ return (typeof body["access_token"] === "string" ||
65
+ typeof body["refresh_token"] === "string" ||
66
+ typeof body["id_token"] === "string" ||
67
+ typeof record(body["authed_user"])?.["access_token"] === "string");
68
+ }
69
+ /**
70
+ * The scopes a token response says it granted, canonicalised per dialect,
71
+ * or null when the answer names none for a token it carries, which fails
72
+ * closed: every documented success response of the three providers names
73
+ * them (map-providers: Google and Microsoft `scope`, Slack `scope` and
74
+ * `authed_user.scope`), so a missing field is an answer the gateway can't
75
+ * hold to anything.
76
+ */
77
+ export function grantedScopes(policy, json) {
78
+ const body = record(json);
79
+ if (!body)
80
+ return null;
81
+ switch (policy.dialect) {
82
+ case "google": {
83
+ // Space-delimited, case-sensitive strings (Google's own words).
84
+ const scopes = splitScopes("google", body["scope"]);
85
+ return scopes ? { scopes } : null;
86
+ }
87
+ case "microsoft": {
88
+ // Documented URL-encoded with the Graph prefix
89
+ // (`https%3A%2F%2Fgraph.microsoft.com%2Fmail.read`), seen as short
90
+ // names in either case; `offline_access` is never echoed, and dropped
91
+ // if it ever is.
92
+ if (typeof body["scope"] !== "string")
93
+ return null;
94
+ let decoded;
95
+ try {
96
+ decoded = decodeURIComponent(body["scope"]);
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ const scopes = splitScopes("microsoft", decoded)?.filter((scope) => scope !== "offline_access");
102
+ return scopes && scopes.length > 0 ? { scopes } : null;
103
+ }
104
+ case "slack":
105
+ return slackGrantedScopes(body);
106
+ case "space":
107
+ case "comma": {
108
+ const scopes = splitScopes(policy.dialect, body["scope"]);
109
+ return scopes ? { scopes } : null;
110
+ }
111
+ }
112
+ }
113
+ /**
114
+ * What a token response granted, for the Gateway-owned flow: as
115
+ * {@link grantedScopes}, except that a `space` or `comma` provider that
116
+ * names no `scope` at all is taken at its word (RFC 6749 §5.1: the field
117
+ * is optional when the grant matches the request), so `requested` stands.
118
+ * The three parsed dialects keep failing closed.
119
+ */
120
+ export function grantedScopesOrRequested(policy, json, requested) {
121
+ const granted = grantedScopes(policy, json);
122
+ if (granted)
123
+ return granted;
124
+ const body = record(json);
125
+ if (body &&
126
+ body["scope"] === undefined &&
127
+ (policy.dialect === "space" || policy.dialect === "comma")) {
128
+ return { scopes: canonical(policy.dialect, requested.scopes) };
129
+ }
130
+ return null;
131
+ }
132
+ /**
133
+ * Whether an app's call with a held token goes through, and under which
134
+ * set (docs/design/2026-10-oauth-providers.md section 7). In order: every
135
+ * set the app declares must be among the connection's (else the person
136
+ * connects again: `integration_not_connected` with the missing sets); the
137
+ * host must be one of the provider's `api` hosts; the path passes the
138
+ * segment rules (`scopeRouteAllows`'s: no batch tunnels, no dot segments,
139
+ * no denied segments); then a declared set with routes must cover the
140
+ * call, or a declared set without routes covers its hosts by access: a
141
+ * read set unlocks `readMethods` (GET and HEAD by default), a write set
142
+ * every method the provider offers. `writes` says whether the method is
143
+ * one the grant allows beyond reads, decided by the caller as for an
144
+ * API-key provider.
145
+ */
146
+ export function heldTokenCallAllowed({ policy, declaredSets, connectionSets, hosts, method, upstream, readMethods = ["GET", "HEAD"], writes, }) {
147
+ const missingSets = declaredSets.filter((name) => !connectionSets.includes(name));
148
+ if (missingSets.length > 0) {
149
+ return { ok: false, code: "integration_not_connected", missingSets };
150
+ }
151
+ if (upstream.protocol !== "https:" || !hosts.includes(upstream.host)) {
152
+ return {
153
+ ok: false,
154
+ code: "integration_scope_not_allowed",
155
+ message: `${upstream.host} is not a host this provider's token reaches (${hosts.join(", ")}).`,
156
+ };
157
+ }
158
+ if (!safeSegments(policy, upstream.pathname)) {
159
+ return {
160
+ ok: false,
161
+ code: "integration_scope_not_allowed",
162
+ message: `${upstream.pathname} is a path no scope set unlocks.`,
163
+ };
164
+ }
165
+ const verb = method.toUpperCase();
166
+ const reads = readMethods.includes(verb);
167
+ // A set's routes decide first: a read-shaped POST a read set lists
168
+ // under `posts` (a search, a query) is a read whatever the grant.
169
+ const routed = scopeRouteAllows(policy, declaredSets, verb, upstream);
170
+ if (routed)
171
+ return { ok: true, set: routed };
172
+ if (!reads && !writes) {
173
+ return {
174
+ ok: false,
175
+ code: "integration_method_not_allowed",
176
+ message: `${verb} ${upstream.pathname}: calls may use ${readMethods.join(" or ")} with access: "read"; ${verb} needs access: "write" in the app's capabilities entry, on a provider that offers it, or a scope set whose routes list it.`,
177
+ };
178
+ }
179
+ for (const [name, set] of setsOf(policy, declaredSets)) {
180
+ if (set.routes.length > 0)
181
+ continue;
182
+ if (reads || (writes && set.access === "write")) {
183
+ return { ok: true, set: name };
184
+ }
185
+ }
186
+ return {
187
+ ok: false,
188
+ code: "integration_scope_not_allowed",
189
+ message: `${verb} ${upstream.pathname} is outside the scope sets this app declares (${declaredSets.join(", ")}).`,
190
+ };
191
+ }
192
+ /**
193
+ * Slack answers with up to two tokens, each with its own scopes: the bot
194
+ * token at the top level and the installing person's under `authed_user`.
195
+ * Which kind the top-level one is comes from `token_type`, because a
196
+ * token-rotation refresh of a user token comes back at the top level with
197
+ * `token_type: "user"`; `authed_user` is a user token unless it says so.
198
+ */
199
+ function slackGrantedScopes(body) {
200
+ const granted = {};
201
+ const take = (token, kind) => {
202
+ if (kind !== "bot" && kind !== "user")
203
+ return false;
204
+ const scopes = splitScopes("slack", token["scope"]);
205
+ if (!scopes)
206
+ return false;
207
+ const key = kind === "bot" ? "scopes" : "userScopes";
208
+ granted[key] = pushNew(granted[key] ?? [], scopes);
209
+ return true;
210
+ };
211
+ if (typeof body["access_token"] === "string") {
212
+ if (!take(body, body["token_type"]))
213
+ return null;
214
+ }
215
+ const authedUser = record(body["authed_user"]);
216
+ if (authedUser && typeof authedUser["access_token"] === "string") {
217
+ if (!take(authedUser, authedUser["token_type"] ?? "user"))
218
+ return null;
219
+ }
220
+ return granted.scopes || granted.userScopes ? granted : null;
221
+ }
222
+ /**
223
+ * What `granted` carries beyond `allowed`, per token kind, for a refusal's
224
+ * message and log line: the user token's extras as `user:<scope>`. Both
225
+ * lists are in the dialect's canonical spelling already.
226
+ */
227
+ export function scopeExcess(granted, allowed) {
228
+ const extra = (scopes, within) => (scopes ?? []).filter((scope) => !within.includes(scope));
229
+ return [
230
+ ...extra(granted.scopes, allowed.scopes),
231
+ ...extra(granted.userScopes, allowed.userScopes).map((scope) => `user:${scope}`),
232
+ ];
233
+ }
234
+ /**
235
+ * The scopes a token request itself asks for (`scope`, and Slack's
236
+ * `user_scope`, in any case) in its form or JSON body, canonicalised per
237
+ * dialect for comparison with `allowedScopes()`, or null when the body
238
+ * names none. The query string is never read: the gateway refuses a
239
+ * request that puts these there, so only one place decides.
240
+ */
241
+ export function requestedTokenScopes(policy, request) {
242
+ const fields = bodyFields(request);
243
+ if (!fields)
244
+ return null;
245
+ const requested = {};
246
+ const scopes = splitScopes(policy.dialect, fields["scope"]);
247
+ if (scopes)
248
+ requested.scopes = scopes;
249
+ const userScopes = splitScopes(policy.dialect, fields["user_scope"]);
250
+ if (userScopes)
251
+ requested.userScopes = userScopes;
252
+ return requested.scopes || requested.userScopes ? requested : null;
253
+ }
254
+ /**
255
+ * `request` with its `scope`, in any case, as the policy's `requestScope`
256
+ * says: replaced by the requested list of the app's `sets`, dropped, or
257
+ * kept as sent. A separate step after `injectClientCredentials()`, whose
258
+ * re-serialised form or JSON body this edits the same way; any other body
259
+ * is left alone.
260
+ */
261
+ export function rewriteTokenRequestScope(policy, sets, request) {
262
+ if (policy.requestScope === "keep" || !request.body)
263
+ return request;
264
+ const replacement = policy.requestScope === "replace"
265
+ ? requestedScopes(policy, sets).scopes.join(" ")
266
+ : null;
267
+ const type = mediaType(request.headers.get("content-type"));
268
+ // Every spelling goes, as `injectClientCredentials()` strips the client:
269
+ // Entra reads a form's names in any case, so a `Scope` left beside the
270
+ // gateway's `scope` is refused upstream as a duplicate, or read instead.
271
+ const isScope = (name) => name.toLowerCase() === "scope";
272
+ let body = request.body;
273
+ if (type === FORM) {
274
+ const params = new URLSearchParams(body);
275
+ for (const name of new Set([...params.keys()].filter(isScope))) {
276
+ params.delete(name);
277
+ }
278
+ if (replacement !== null)
279
+ params.set("scope", replacement);
280
+ body = params.toString();
281
+ }
282
+ else if (type === JSON_TYPE) {
283
+ const parsed = jsonObject(body);
284
+ if (!parsed)
285
+ return request;
286
+ for (const name of Object.keys(parsed)) {
287
+ if (isScope(name))
288
+ delete parsed[name];
289
+ }
290
+ if (replacement !== null)
291
+ parsed["scope"] = replacement;
292
+ body = JSON.stringify(parsed);
293
+ }
294
+ else {
295
+ return request;
296
+ }
297
+ return { url: request.url, headers: request.headers, body };
298
+ }
299
+ /**
300
+ * The declared set that unlocks `method upstream`, or null. Not wired to
301
+ * any live path yet: it becomes the proxy's boundary once the gateway
302
+ * holds tokens, and the outbound Worker's for dispatched apps before that.
303
+ *
304
+ * In order: https only; the host must be on one of the declared sets'
305
+ * routes; the raw pathname is refused when it holds `\`, `//` or `%00`;
306
+ * each raw segment, and each percent-decoding of it while a `%` remains
307
+ * (a provider's layers may each decode once), is refused when it won't
308
+ * decode, holds a control or format character or a backslash, is one of
309
+ * the policy's denied segments in any case, or splits on `/` into a dot
310
+ * segment: `.` or `..`, with or without a `;` parameter (`..;x`), which
311
+ * Google's front end resolves like the bare one. The denied comparison is
312
+ * of the whole segment, because an encoded slash is part of one id to the
313
+ * provider, never a separator: a Search Console site URL whose own path
314
+ * holds `batch` stays matchable, and `2%2FmeasurementProtocolSecrets` is
315
+ * a data stream id that doesn't exist, not the secrets. Then the raw
316
+ * pathname, case preserved, is matched: GET and HEAD need a `paths` entry
317
+ * (exact, or a prefix when it ends in `/`), POST needs a `posts` pattern,
318
+ * and no other method goes through. Matching the raw path is what lets
319
+ * Search Console's `sites/https%3A%2F%2Fexample.com%2F/searchAnalytics/query`
320
+ * match `[^/]+` while `..`, batch tunnels and secrets stay refused however
321
+ * encoded. `X-HTTP-Method-Override`, `access_token` and `token` parameters
322
+ * are the proxy's to strip before it asks; nothing here reads them. The
323
+ * proxy fetches with `redirect: "manual"`: Google answers a dot segment it
324
+ * resolves with a 302 to the resolved path, and following one would carry
325
+ * the token to a path this never matched.
326
+ */
327
+ export function scopeRouteAllows(policy, sets, method, upstream) {
328
+ if (upstream.protocol !== "https:")
329
+ return null;
330
+ const onHost = setsOf(policy, sets).flatMap(([name, set]) => set.routes
331
+ .filter((route) => route.host === upstream.host)
332
+ .map((route) => [name, route]));
333
+ if (onHost.length === 0)
334
+ return null;
335
+ const pathname = upstream.pathname;
336
+ if (!safeSegments(policy, pathname))
337
+ return null;
338
+ const verb = method.toUpperCase();
339
+ for (const [name, route] of onHost) {
340
+ if (verb === "GET" || verb === "HEAD") {
341
+ const covered = route.paths.some((entry) => entry.endsWith("/") ? pathname.startsWith(entry) : pathname === entry);
342
+ if (covered)
343
+ return name;
344
+ }
345
+ else if (verb === "POST") {
346
+ if (route.posts?.some((pattern) => pattern.test(pathname)))
347
+ return name;
348
+ }
349
+ }
350
+ return null;
351
+ }
352
+ /** Control and format characters: in no provider's path once decoded. */
353
+ const UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
354
+ /** A dot segment, bare or with a path parameter (`..;x`): Google's front end resolves both. */
355
+ const DOT_SEGMENT = /^\.{1,2}(;|$)/;
356
+ /**
357
+ * Whether every segment of the raw `pathname` passes the checks above in
358
+ * each form a provider's layers could see it: raw, then decoded again
359
+ * while a `%` remains.
360
+ */
361
+ function safeSegments(policy, pathname) {
362
+ if (pathname.includes("\\") ||
363
+ pathname.includes("//") ||
364
+ pathname.includes("%00")) {
365
+ return false;
366
+ }
367
+ const denied = policy.deniedSegments.map((segment) => segment.toLowerCase());
368
+ for (const raw of pathname.split("/")) {
369
+ let text = raw;
370
+ for (;;) {
371
+ if (!safeSegment(text, denied))
372
+ return false;
373
+ if (!text.includes("%"))
374
+ break;
375
+ // A `%` left after a decode is a byte the provider may decode again
376
+ // (double encoding), so that form is checked too; one that isn't an
377
+ // escape (`%2525` ends in a bare `%`) won't decode and refuses, as a
378
+ // malformed escape in the raw path does.
379
+ try {
380
+ text = decodeURIComponent(text);
381
+ }
382
+ catch {
383
+ return false;
384
+ }
385
+ }
386
+ }
387
+ return true;
388
+ }
389
+ /**
390
+ * Whether one form of a segment is safe: no control or format character,
391
+ * no backslash (in no provider's grammar), not a denied segment in any
392
+ * case, and no dot segment among its `/`-split parts (a traversal the
393
+ * provider might resolve). The denied comparison is of the whole: an
394
+ * encoded slash is part of one id to the provider, never a separator, so
395
+ * a Search Console site URL whose own path holds `batch` stays matchable.
396
+ */
397
+ function safeSegment(text, denied) {
398
+ if (UNPRINTABLE.test(text) || text.includes("\\"))
399
+ return false;
400
+ if (denied.includes(text.toLowerCase()))
401
+ return false;
402
+ return !text.split("/").some((part) => DOT_SEGMENT.test(part));
403
+ }
404
+ /** The policy's sets in registry order, keeping those `names` declares. */
405
+ function setsOf(policy, names) {
406
+ return Object.entries(policy.sets).filter(([name]) => names.includes(name));
407
+ }
408
+ const GOOGLE_ALIASES = new Map([
409
+ ["email", "https://www.googleapis.com/auth/userinfo.email"],
410
+ ["profile", "https://www.googleapis.com/auth/userinfo.profile"],
411
+ ]);
412
+ const GRAPH_PREFIX = /^https:\/\/graph\.microsoft\.com\//i;
413
+ /** `scopes` in the dialect's one spelling, deduplicated, order kept. */
414
+ /**
415
+ * `scopes` in the dialect's canonical spelling (Google's long names,
416
+ * Microsoft's bare lower-case names), for comparing a set's scopes with
417
+ * what a token response says it granted.
418
+ */
419
+ export function canonicalScopes(dialect, scopes) {
420
+ return canonical(dialect, [...scopes]);
421
+ }
422
+ function canonical(dialect, scopes) {
423
+ return pushNew([], scopes.map((scope) => {
424
+ switch (dialect) {
425
+ case "google":
426
+ return GOOGLE_ALIASES.get(scope) ?? scope;
427
+ case "microsoft":
428
+ return scope.replace(GRAPH_PREFIX, "").toLowerCase();
429
+ case "slack":
430
+ case "space":
431
+ case "comma":
432
+ return scope;
433
+ }
434
+ }));
435
+ }
436
+ /**
437
+ * A provider's scope field split into scopes, canonicalised, or null when
438
+ * it isn't a string or names nothing. Google and Microsoft separate with
439
+ * spaces; Slack with commas (spaces too, in its legacy form).
440
+ */
441
+ function splitScopes(dialect, value) {
442
+ if (typeof value !== "string")
443
+ return null;
444
+ const separator = dialect === "slack" || dialect === "comma" ? /[,\s]+/ : /\s+/;
445
+ const scopes = canonical(dialect, value.split(separator).filter(Boolean));
446
+ return scopes.length > 0 ? scopes : null;
447
+ }
448
+ /** `into` with each of `more` it doesn't have yet, in order. */
449
+ function pushNew(into, more) {
450
+ for (const item of more)
451
+ if (!into.includes(item))
452
+ into.push(item);
453
+ return into;
454
+ }
455
+ const FORM = "application/x-www-form-urlencoded";
456
+ const JSON_TYPE = "application/json";
457
+ /** A token request's body fields, form or JSON, by lowercased name; null for any other body. */
458
+ function bodyFields(request) {
459
+ if (!request.body)
460
+ return null;
461
+ const type = mediaType(request.headers.get("content-type"));
462
+ if (type === FORM)
463
+ return foldFields(new URLSearchParams(request.body));
464
+ if (type === JSON_TYPE) {
465
+ const parsed = jsonObject(request.body);
466
+ return parsed && foldFields(Object.entries(parsed));
467
+ }
468
+ return null;
469
+ }
470
+ /**
471
+ * `pairs` by lowercased name, as Entra reads a form's names in any case. A
472
+ * name's values are one space-joined list, so a field sent twice, or as
473
+ * `scope` and `Scope`, hides nothing in the second copy; a value that
474
+ * isn't a string leaves its field not a string, which reads as naming
475
+ * none.
476
+ */
477
+ function foldFields(pairs) {
478
+ const fields = new Map();
479
+ for (const [name, value] of pairs) {
480
+ const key = name.toLowerCase();
481
+ const before = fields.get(key);
482
+ fields.set(key, !fields.has(key)
483
+ ? value
484
+ : typeof before === "string" && typeof value === "string"
485
+ ? `${before} ${value}`
486
+ : [before, value]);
487
+ }
488
+ return Object.fromEntries(fields);
489
+ }
490
+ /** A Content-Type's media type, lowercased and without parameters. */
491
+ function mediaType(contentType) {
492
+ return (contentType ?? "").split(";")[0].trim().toLowerCase();
493
+ }
494
+ /**
495
+ * `text` as a JSON object, or null for anything else (an array, a scalar,
496
+ * not JSON). Shared with the gateway's read of a provider's token answer.
497
+ */
498
+ export function jsonObject(text) {
499
+ try {
500
+ return record(JSON.parse(text));
501
+ }
502
+ catch {
503
+ return null;
504
+ }
505
+ }
506
+ function record(value) {
507
+ return value && typeof value === "object" && !Array.isArray(value)
508
+ ? value
509
+ : null;
510
+ }