@every-app/sdk 0.3.0-rc.9 → 0.3.0

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 (131) hide show
  1. package/README.md +75 -74
  2. package/dist/cloudflare/catalogSettings.generated.d.ts +19 -6
  3. package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -1
  4. package/dist/cloudflare/catalogSettings.generated.js +43 -14
  5. package/dist/cloudflare/index.d.ts +1 -1
  6. package/dist/cloudflare/index.d.ts.map +1 -1
  7. package/dist/cloudflare/worker.d.ts +63 -9
  8. package/dist/cloudflare/worker.d.ts.map +1 -1
  9. package/dist/cloudflare/worker.js +45 -9
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/internal/aiDefault.d.ts +36 -79
  13. package/dist/internal/aiDefault.d.ts.map +1 -1
  14. package/dist/internal/aiDefault.js +47 -149
  15. package/dist/internal/aiProviders.d.ts +1 -1
  16. package/dist/internal/aiProviders.d.ts.map +1 -1
  17. package/dist/internal/aiProviders.js +2 -2
  18. package/dist/internal/appSettings.d.ts +140 -89
  19. package/dist/internal/appSettings.d.ts.map +1 -1
  20. package/dist/internal/appSettings.js +264 -156
  21. package/dist/internal/capabilities.d.ts +108 -503
  22. package/dist/internal/capabilities.d.ts.map +1 -1
  23. package/dist/internal/capabilities.js +29 -550
  24. package/dist/internal/catalog/commerce.js +1 -1
  25. package/dist/internal/catalog/index.d.ts +35 -0
  26. package/dist/internal/catalog/index.d.ts.map +1 -1
  27. package/dist/internal/catalog/infra.d.ts +35 -0
  28. package/dist/internal/catalog/infra.d.ts.map +1 -1
  29. package/dist/internal/catalog/infra.js +65 -0
  30. package/dist/internal/devProviders.d.ts +1 -19
  31. package/dist/internal/devProviders.d.ts.map +1 -1
  32. package/dist/internal/devProviders.js +7 -1
  33. package/dist/internal/events.d.ts +52 -1
  34. package/dist/internal/events.d.ts.map +1 -1
  35. package/dist/internal/events.js +65 -3
  36. package/dist/internal/gatewaySettings.d.ts +6 -16
  37. package/dist/internal/gatewaySettings.d.ts.map +1 -1
  38. package/dist/internal/gatewaySettings.js +5 -23
  39. package/dist/internal/index.d.ts +1 -0
  40. package/dist/internal/index.d.ts.map +1 -1
  41. package/dist/internal/index.js +1 -0
  42. package/dist/internal/mcpOAuth.d.ts +210 -0
  43. package/dist/internal/mcpOAuth.d.ts.map +1 -0
  44. package/dist/internal/mcpOAuth.js +735 -0
  45. package/dist/internal/oauthClient.d.ts +7 -47
  46. package/dist/internal/oauthClient.d.ts.map +1 -1
  47. package/dist/internal/oauthClient.js +1 -115
  48. package/dist/internal/oauthPresets.d.ts +356 -20
  49. package/dist/internal/oauthPresets.d.ts.map +1 -1
  50. package/dist/internal/oauthPresets.js +183 -861
  51. package/dist/internal/oauthScopes.d.ts +43 -155
  52. package/dist/internal/oauthScopes.d.ts.map +1 -1
  53. package/dist/internal/oauthScopes.js +82 -475
  54. package/dist/internal/oauthSpec.d.ts +28 -71
  55. package/dist/internal/oauthSpec.d.ts.map +1 -1
  56. package/dist/internal/oauthSpec.js +172 -448
  57. package/dist/internal/protocol.d.ts +24 -14
  58. package/dist/internal/protocol.d.ts.map +1 -1
  59. package/dist/internal/protocol.js +26 -7
  60. package/dist/internal/providerApps.d.ts +2 -2
  61. package/dist/internal/providerApps.js +2 -2
  62. package/dist/internal/providerRegistry.d.ts +5 -64
  63. package/dist/internal/providerRegistry.d.ts.map +1 -1
  64. package/dist/internal/providerRegistry.js +19 -118
  65. package/dist/internal/providerSpec.d.ts.map +1 -1
  66. package/dist/internal/providerSpec.js +14 -36
  67. package/dist/internal/remoteMcp.d.ts +191 -47
  68. package/dist/internal/remoteMcp.d.ts.map +1 -1
  69. package/dist/internal/remoteMcp.js +338 -133
  70. package/dist/internal/signInProviders.d.ts +1 -1
  71. package/dist/internal/signInProviders.d.ts.map +1 -1
  72. package/dist/internal/tools.d.ts +15 -8
  73. package/dist/internal/tools.d.ts.map +1 -1
  74. package/dist/internal/tools.js +9 -8
  75. package/dist/manifest.d.ts +96 -40
  76. package/dist/manifest.d.ts.map +1 -1
  77. package/dist/manifest.js +1 -1
  78. package/dist/server/ai.d.ts +1 -93
  79. package/dist/server/ai.d.ts.map +1 -1
  80. package/dist/server/ai.js +17 -111
  81. package/dist/server/attribution.d.ts +10 -1
  82. package/dist/server/attribution.d.ts.map +1 -1
  83. package/dist/server/attribution.js +7 -1
  84. package/dist/server/connectUrl.d.ts +4 -9
  85. package/dist/server/connectUrl.d.ts.map +1 -1
  86. package/dist/server/connectUrl.js +17 -14
  87. package/dist/server/context.d.ts +12 -14
  88. package/dist/server/context.d.ts.map +1 -1
  89. package/dist/server/context.js +23 -25
  90. package/dist/server/eventsEndpoint.d.ts +48 -0
  91. package/dist/server/eventsEndpoint.d.ts.map +1 -0
  92. package/dist/server/eventsEndpoint.js +145 -0
  93. package/dist/server/everyApp.d.ts +16 -26
  94. package/dist/server/everyApp.d.ts.map +1 -1
  95. package/dist/server/everyApp.js +22 -46
  96. package/dist/server/gatewayFetch.d.ts +1 -1
  97. package/dist/server/gatewayFetch.js +1 -1
  98. package/dist/server/index.d.ts +6 -9
  99. package/dist/server/index.d.ts.map +1 -1
  100. package/dist/server/index.js +4 -8
  101. package/dist/server/integrationFetch.d.ts +60 -72
  102. package/dist/server/integrationFetch.d.ts.map +1 -1
  103. package/dist/server/integrationFetch.js +120 -393
  104. package/dist/server/refusal.d.ts +0 -9
  105. package/dist/server/refusal.d.ts.map +1 -1
  106. package/dist/server/refusal.js +0 -22
  107. package/dist/server/tools.d.ts +17 -6
  108. package/dist/server/tools.d.ts.map +1 -1
  109. package/dist/server/tools.js +14 -4
  110. package/dist/server/toolsEndpoint.d.ts +2 -1
  111. package/dist/server/toolsEndpoint.d.ts.map +1 -1
  112. package/dist/server/toolsEndpoint.js +4 -3
  113. package/dist/server/webhooks.d.ts +56 -0
  114. package/dist/server/webhooks.d.ts.map +1 -0
  115. package/dist/server/webhooks.js +205 -0
  116. package/dist/testing/index.d.ts +2 -3
  117. package/dist/testing/index.d.ts.map +1 -1
  118. package/dist/testing/index.js +2 -6
  119. package/package.json +6 -1
  120. package/dist/server/aiBinding.d.ts +0 -68
  121. package/dist/server/aiBinding.d.ts.map +0 -1
  122. package/dist/server/aiBinding.js +0 -294
  123. package/dist/server/authorizeUrl.d.ts +0 -43
  124. package/dist/server/authorizeUrl.d.ts.map +0 -1
  125. package/dist/server/authorizeUrl.js +0 -106
  126. package/dist/server/emailBinding.d.ts +0 -72
  127. package/dist/server/emailBinding.d.ts.map +0 -1
  128. package/dist/server/emailBinding.js +0 -124
  129. package/dist/server/fromMcpTools.d.ts +0 -89
  130. package/dist/server/fromMcpTools.d.ts.map +0 -1
  131. package/dist/server/fromMcpTools.js +0 -192
@@ -1,22 +1,4 @@
1
- /**
2
- * The `oauth2-user` half of a provider spec
3
- * (docs/design/2026-10-oauth-providers.md section 3): a registered OAuth
4
- * provider, or a preset (./oauthPresets.ts), as JSON, turned into the
5
- * same {@link OAuthClientCapability} a catalog client is, with a `flow`
6
- * the Gateway runs itself. `parseProviderSpec` (./providerSpec.ts) hands
7
- * the common fields here once it sees `auth.kind: "oauth2-user"`; nothing
8
- * here reads a value: a spec names Gateway settings, never holds one.
9
- */
10
- import { DEFAULT_WRITE_METHODS, } from "./capabilities.js";
11
- /** A scope set's name: `identity`, or `<area>.<access>` (`repo.read`), lowercase. */
12
- export const SCOPE_SET_NAME = /^[a-z][a-z0-9-]{0,31}(?:\.[a-z][a-z0-9-]{0,31})?$/;
13
- const DIALECTS = [
14
- "space",
15
- "comma",
16
- "google",
17
- "microsoft",
18
- "slack",
19
- ];
1
+ const SEPARATORS = ["space", "comma"];
20
2
  const INJECTIONS = ["form", "basic"];
21
3
  const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
22
4
  /** Headers the Gateway sets on the token request itself, or never sends. */
@@ -27,17 +9,27 @@ const RESERVED_TOKEN_HEADERS = new Set([
27
9
  "host",
28
10
  "cookie",
29
11
  ]);
12
+ /**
13
+ * Authorize parameters the Gateway sets itself, and
14
+ * `include_granted_scopes`, which would widen a token to every scope the
15
+ * person ever granted the client (the mint check would refuse it anyway).
16
+ */
17
+ const RESERVED_AUTHORIZE_PARAMS = [
18
+ "client_id",
19
+ "redirect_uri",
20
+ "response_type",
21
+ "scope",
22
+ "state",
23
+ "code_challenge",
24
+ "code_challenge_method",
25
+ "include_granted_scopes",
26
+ ];
30
27
  const PARAM_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
31
28
  /** A dot path into a JSON answer, at most four segments. */
32
29
  const DOT_PATH = /^[A-Za-z_][A-Za-z0-9_]{0,63}(?:\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/;
33
30
  const UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
34
- const MAX_SETS = 20;
35
31
  const MAX_SCOPES = 40;
36
- const MAX_ROUTES = 20;
37
- const MAX_PATHS = 50;
38
32
  const MAX_PARAMS = 10;
39
- /** Segments no set unlocks on any host: batch tunnels carry whole requests inside. */
40
- export const DEFAULT_DENIED_SEGMENTS = ["batch", "$batch"];
41
33
  /** The fields of `auth` when its kind is `oauth2-user`. */
42
34
  export const OAUTH_AUTH_FIELDS = [
43
35
  "kind",
@@ -49,11 +41,32 @@ export const OAUTH_AUTH_FIELDS = [
49
41
  "clientSecret",
50
42
  "inject",
51
43
  "pkce",
52
- "scopeDialect",
44
+ "scopeSeparator",
45
+ "scopePrefix",
53
46
  "authorizeParams",
54
47
  "tokenHeaders",
55
48
  "identity",
49
+ "tolerated",
50
+ "resource",
51
+ "issuer",
56
52
  ];
53
+ /**
54
+ * Fields an `oauth2-user` spec had before scopes moved to each app's
55
+ * `everyapp.config.ts`, each with what to write instead: refused with
56
+ * that message rather than "unknown field".
57
+ */
58
+ export const REMOVED_OAUTH_FIELDS = {
59
+ scopeSets: "scope sets are gone: each app lists the provider's own scopes in its everyapp.config.ts. Remove scopeSets.",
60
+ alwaysRequested: "alwaysRequested is gone: put scopes every connect needs (a refresh token's, the identity URL's) in auth.identity.scopes.",
61
+ tolerated: "tolerated moved into auth: auth.tolerated lists the scopes a token may carry beyond what was asked.",
62
+ access: "access is gone for an OAuth provider: the scopes an app lists are the grant, for every method.",
63
+ readMethods: "readMethods is gone for an OAuth provider: the scopes an app lists are the grant, for every method.",
64
+ writeMethods: "writeMethods is gone for an OAuth provider: the scopes an app lists are the grant, for every method.",
65
+ };
66
+ /** `auth` fields renamed, each with its new name. */
67
+ const RENAMED_AUTH_FIELDS = {
68
+ scopeDialect: "renamed auth.scopeSeparator: space or comma (a provider's prefix goes in auth.scopePrefix)",
69
+ };
57
70
  function isRecord(value) {
58
71
  return typeof value === "object" && value !== null && !Array.isArray(value);
59
72
  }
@@ -99,7 +112,9 @@ function endpoint(value, path, problems, { query }) {
99
112
  parsed.hash) {
100
113
  problems.push({
101
114
  path,
102
- message: "must be an https URL with no port, credentials or fragment",
115
+ message: parsed.port
116
+ ? `must be an https URL on the default port (${parsed.port} is not: the Gateway's OAuth requests go to port 443 only), with no credentials or fragment`
117
+ : "must be an https URL with no port, credentials or fragment",
103
118
  });
104
119
  return undefined;
105
120
  }
@@ -109,8 +124,44 @@ function endpoint(value, path, problems, { query }) {
109
124
  }
110
125
  return { url, host: parsed.hostname.toLowerCase() };
111
126
  }
112
- function settingName(value, path, problems, pattern) {
113
- if (typeof value !== "string" || !pattern.test(value)) {
127
+ /**
128
+ * An https URL that names something rather than being fetched (an RFC
129
+ * 8707 resource, an RFC 8414 issuer): a port is allowed, credentials and a
130
+ * fragment are not, and an issuer takes no query either.
131
+ */
132
+ function identifierUrl(value, path, problems, { query }) {
133
+ if (value === undefined)
134
+ return undefined;
135
+ const url = printable(value, path, problems, 2048);
136
+ if (url === undefined)
137
+ return undefined;
138
+ let parsed;
139
+ try {
140
+ parsed = new URL(url);
141
+ }
142
+ catch {
143
+ problems.push({ path, message: "must be an https URL" });
144
+ return undefined;
145
+ }
146
+ if (parsed.protocol !== "https:" ||
147
+ parsed.username ||
148
+ parsed.password ||
149
+ parsed.hash ||
150
+ url.includes("#")) {
151
+ problems.push({
152
+ path,
153
+ message: "must be an https URL with no credentials or fragment",
154
+ });
155
+ return undefined;
156
+ }
157
+ if (!query && parsed.search) {
158
+ problems.push({ path, message: "may not carry a query string" });
159
+ return undefined;
160
+ }
161
+ return url;
162
+ }
163
+ function settingName(value, path, problems) {
164
+ if (typeof value !== "string" || !/^[A-Z][A-Z0-9_]{2,63}$/.test(value)) {
114
165
  problems.push({
115
166
  path,
116
167
  message: "must name a Gateway setting: upper case letters, digits and underscores, a letter first, 3 to 64 characters (GITHUB_CLIENT_ID)",
@@ -141,7 +192,8 @@ function stringMap(value, path, problems, each) {
141
192
  }
142
193
  return out;
143
194
  }
144
- function scopeList(value, path, problems) {
195
+ /** A list of provider scope strings: each one scope, with no space or comma, each once. */
196
+ export function scopeList(value, path, problems) {
145
197
  if (value === undefined)
146
198
  return undefined;
147
199
  if (!Array.isArray(value) || value.length > MAX_SCOPES) {
@@ -170,290 +222,18 @@ function scopeList(value, path, problems) {
170
222
  }
171
223
  return out;
172
224
  }
173
- function routes(value, path, hosts, problems) {
174
- if (value === undefined)
175
- return undefined;
176
- if (!Array.isArray(value) || value.length > MAX_ROUTES) {
177
- problems.push({
178
- path,
179
- message: `must be a list of at most ${MAX_ROUTES} routes`,
180
- });
181
- return undefined;
182
- }
183
- const out = [];
184
- value.forEach((item, index) => {
185
- const at = `${path}.${index}`;
186
- if (!isRecord(item)) {
187
- problems.push({ path: at, message: "must be { host, paths, posts? }" });
188
- return;
189
- }
190
- for (const field of Object.keys(item)) {
191
- if (!["host", "paths", "posts"].includes(field)) {
192
- problems.push({ path: `${at}.${field}`, message: "unknown field" });
193
- }
194
- }
195
- const host = printable(item["host"], `${at}.host`, problems, 253);
196
- if (host !== undefined && !hosts.includes(host)) {
197
- problems.push({ path: `${at}.host`, message: "must be one of hosts" });
198
- }
199
- const paths = [];
200
- if (!Array.isArray(item["paths"]) || item["paths"].length > MAX_PATHS) {
201
- problems.push({
202
- path: `${at}.paths`,
203
- message: `must be a list of at most ${MAX_PATHS} paths`,
204
- });
205
- }
206
- else {
207
- item["paths"].forEach((entry, offset) => {
208
- const text = printable(entry, `${at}.paths.${offset}`, problems, 500);
209
- if (text === undefined)
210
- return;
211
- if (!text.startsWith("/") ||
212
- text.includes("//") ||
213
- text.includes("\\") ||
214
- text.includes("%") ||
215
- /(^|\/)\.\.?(\/|$)/.test(text)) {
216
- problems.push({
217
- path: `${at}.paths.${offset}`,
218
- message: "must start with / and hold no //, backslash, percent-escape or dot segment",
219
- });
220
- return;
221
- }
222
- paths.push(text);
223
- });
224
- }
225
- const posts = [];
226
- if (item["posts"] !== undefined) {
227
- if (!Array.isArray(item["posts"]) || item["posts"].length > MAX_PATHS) {
228
- problems.push({
229
- path: `${at}.posts`,
230
- message: `must be a list of at most ${MAX_PATHS} anchored patterns`,
231
- });
232
- }
233
- else {
234
- item["posts"].forEach((entry, offset) => {
235
- const text = printable(entry, `${at}.posts.${offset}`, problems, 500);
236
- if (text === undefined)
237
- return;
238
- if (!text.startsWith("^") || !text.endsWith("$")) {
239
- problems.push({
240
- path: `${at}.posts.${offset}`,
241
- message: "must be anchored: start with ^ and end with $",
242
- });
243
- return;
244
- }
245
- try {
246
- posts.push(new RegExp(text));
247
- }
248
- catch {
249
- problems.push({
250
- path: `${at}.posts.${offset}`,
251
- message: "is not a valid pattern",
252
- });
253
- }
254
- });
255
- }
256
- }
257
- if (host === undefined)
258
- return;
259
- out.push({
260
- host,
261
- paths,
262
- ...(item["posts"] === undefined ? {} : { posts }),
263
- });
264
- });
265
- return out;
266
- }
267
- /** The fields one scope set may carry, in a spec's `scopeSets` or an overlay. */
268
- const SCOPE_SET_FIELDS = ["label", "access", "scopes", "userScopes", "routes"];
269
- /**
270
- * One scope set (`{ label, access, scopes, userScopes?, routes? }`) at
271
- * `at`, its routes on `hosts` alone; undefined when a field is wrong,
272
- * every problem reported. `overlay`: a set added to a built-in client
273
- * must name a scope and say what it unlocks (a set with no routes unlocks
274
- * nothing on the held-token path); a spec's own set may leave both empty
275
- * (Notion's sets carry no scope string).
276
- */
277
- export function parseScopeSet(raw, at, hosts, problems, { overlay = false } = {}) {
278
- if (!isRecord(raw)) {
279
- problems.push({ path: at, message: "must be { label, access, scopes }" });
280
- return undefined;
281
- }
282
- for (const field of Object.keys(raw)) {
283
- if (!SCOPE_SET_FIELDS.includes(field)) {
284
- problems.push({ path: `${at}.${field}`, message: "unknown field" });
285
- }
286
- }
287
- const label = printable(raw["label"], `${at}.label`, problems, 80);
288
- if (raw["label"] === undefined) {
289
- problems.push({ path: `${at}.label`, message: "required" });
290
- }
291
- const access = raw["access"];
292
- if (access !== "read" && access !== "write") {
293
- problems.push({ path: `${at}.access`, message: "must be read or write" });
294
- }
295
- const scopes = scopeList(raw["scopes"], `${at}.scopes`, problems);
296
- if (raw["scopes"] === undefined) {
297
- problems.push({ path: `${at}.scopes`, message: "required" });
298
- }
299
- else if (overlay && scopes && scopes.length === 0) {
300
- problems.push({ path: `${at}.scopes`, message: "must list a scope" });
301
- }
302
- const userScopes = scopeList(raw["userScopes"], `${at}.userScopes`, problems);
303
- const parsedRoutes = routes(raw["routes"], `${at}.routes`, hosts, problems);
304
- if (overlay && (!parsedRoutes || parsedRoutes.length === 0)) {
305
- problems.push({
306
- path: `${at}.routes`,
307
- message: "required: at least one { host, paths } the set unlocks, on one of the client's API hosts",
308
- });
309
- }
310
- if (label === undefined ||
311
- (access !== "read" && access !== "write") ||
312
- scopes === undefined) {
313
- return undefined;
314
- }
315
- return {
316
- label,
317
- access,
318
- scopes,
319
- ...(userScopes === undefined ? {} : { userScopes }),
320
- routes: parsedRoutes ?? [],
321
- };
322
- }
323
- function scopeSets(value, hosts, problems) {
324
- if (!isRecord(value)) {
325
- problems.push({
326
- path: "scopeSets",
327
- message: 'required: at least one named set, { "repo.read": { "label", "access", "scopes" } }',
328
- });
329
- return undefined;
330
- }
331
- const entries = Object.entries(value);
332
- if (entries.length === 0 || entries.length > MAX_SETS) {
333
- problems.push({
334
- path: "scopeSets",
335
- message: `must name 1 to ${MAX_SETS} sets`,
336
- });
337
- return undefined;
338
- }
339
- const out = {};
340
- for (const [name, raw] of entries) {
341
- const at = `scopeSets.${name}`;
342
- if (!SCOPE_SET_NAME.test(name)) {
343
- problems.push({
344
- path: at,
345
- message: "a set is named identity, or <area>.<access> in lowercase (repo.read)",
346
- });
347
- continue;
348
- }
349
- const set = parseScopeSet(raw, at, hosts, problems);
350
- if (set)
351
- out[name] = set;
352
- }
353
- return out;
354
- }
355
- /** The fields a scope-set overlay carries: a set's, with its `name` and the client it belongs to. */
356
- const OVERLAY_FIELDS = ["provider", "name", ...SCOPE_SET_FIELDS];
357
- /**
358
- * A scope set an operator adds to a built-in OAuth client
359
- * (`everyapp provider scope-set add google-workspace gmail-send.json`):
360
- * `{ "name": "gmail.send", "label", "access", "scopes", "routes" }`, held
361
- * to the rules a built-in set is (the set's shape, routes on the client's
362
- * `api` hosts alone, anchored `posts`) plus two of its own: `access` is
363
- * said outright (a write set is never implied by its scopes) and `routes`
364
- * is required. A `provider` field, when present, must be `id`. The name
365
- * may not be one of the client's own sets: an overlay adds, never
366
- * replaces. Nothing here reads a value.
367
- */
368
- export function parseScopeSetOverlay(input, client) {
369
- const problems = [];
370
- if (!isRecord(input)) {
371
- return {
372
- ok: false,
373
- problems: [{ path: "(root)", message: "must be a JSON object" }],
374
- };
375
- }
376
- for (const field of Object.keys(input)) {
377
- if (!OVERLAY_FIELDS.includes(field)) {
378
- problems.push({ path: field, message: "unknown field" });
379
- }
380
- }
381
- const policy = client.capability.scopePolicy;
382
- if (!policy) {
383
- problems.push({
384
- path: "(root)",
385
- message: `${client.id} has no scope sets: the whole client is the grant`,
386
- });
387
- }
388
- if (input["provider"] !== undefined && input["provider"] !== client.id) {
389
- problems.push({
390
- path: "provider",
391
- message: `is ${String(input["provider"])}, not ${client.id}`,
392
- });
393
- }
394
- const name = input["name"];
395
- if (typeof name !== "string" || !SCOPE_SET_NAME.test(name)) {
396
- problems.push({
397
- path: "name",
398
- message: "required: identity, or <area>.<access> in lowercase (gmail.send)",
399
- });
400
- }
401
- else if (policy && Object.hasOwn(policy.sets, name)) {
402
- problems.push({
403
- path: "name",
404
- message: `${name} is one of ${client.id}'s own sets; an overlay adds a set, never replaces one`,
405
- });
406
- }
407
- // The set's own fields alone: an unknown one was reported above.
408
- const fields = Object.fromEntries(Object.entries(input).filter(([field]) => SCOPE_SET_FIELDS.includes(field)));
409
- const set = parseScopeSet(fields, "(set)", client.capability.api, problems, {
410
- overlay: true,
411
- });
412
- if (problems.length > 0 || !set || typeof name !== "string") {
413
- return { ok: false, problems };
414
- }
415
- return { ok: true, name, set };
416
- }
417
- /**
418
- * `capability` with `sets` added to its scope policy: the sets after the
419
- * client's own, in the order given, and each set's scopes added to the
420
- * authorize URL's list (what the operator's client must be allowed), as
421
- * `parseOAuthUserSpec` builds it. A name the client already has is left
422
- * as the client's (the registry refuses such an overlay before this).
423
- */
424
- export function withScopeSets(capability, sets) {
425
- const policy = capability.scopePolicy;
426
- if (!policy)
427
- return capability;
428
- const merged = { ...policy.sets };
429
- const scopes = [...capability.authorize.scopes];
430
- for (const [name, set] of Object.entries(sets)) {
431
- if (Object.hasOwn(merged, name))
432
- continue;
433
- merged[name] = set;
434
- for (const scope of set.scopes) {
435
- if (!scopes.includes(scope))
436
- scopes.push(scope);
437
- }
438
- }
439
- return {
440
- ...capability,
441
- authorize: { ...capability.authorize, scopes },
442
- scopePolicy: { ...policy, sets: merged },
443
- };
444
- }
445
225
  function identity(value, hosts, problems) {
446
226
  if (value === undefined)
447
227
  return undefined;
448
228
  if (!isRecord(value)) {
449
229
  problems.push({
450
230
  path: "auth.identity",
451
- message: "must be { url, label, id? }",
231
+ message: "must be { url, label, id?, scopes? }",
452
232
  });
453
233
  return undefined;
454
234
  }
455
235
  for (const field of Object.keys(value)) {
456
- if (!["url", "label", "id"].includes(field)) {
236
+ if (!["url", "label", "id", "scopes"].includes(field)) {
457
237
  problems.push({
458
238
  path: `auth.identity.${field}`,
459
239
  message: "unknown field",
@@ -489,30 +269,34 @@ function identity(value, hosts, problems) {
489
269
  message: "must be a JSON field name, or a dotted path to a nested one",
490
270
  });
491
271
  }
272
+ const scopes = scopeList(value["scopes"], "auth.identity.scopes", problems);
492
273
  if (!url || label === undefined)
493
274
  return undefined;
494
- return { url: url.url, label, ...(id === undefined ? {} : { id }) };
275
+ return {
276
+ identity: { url: url.url, label, ...(id === undefined ? {} : { id }) },
277
+ scopes: scopes ?? [],
278
+ };
495
279
  }
496
280
  /**
497
- * Parse the OAuth half of a spec whose `auth.kind` is `oauth2-user`:
498
- * `auth` itself, `scopeSets`, `alwaysRequested` and `tolerated`, with
499
- * the common fields already parsed. Every problem is reported; on
500
- * success the catalog-shaped capability with its `flow`.
281
+ * Parse the OAuth half of a spec whose `auth.kind` is `oauth2-user`, with
282
+ * the common fields already parsed. Every problem is reported; on success
283
+ * the capability with its `flow`.
501
284
  */
502
285
  export function parseOAuthUserSpec(input, common, problems) {
503
- const settingName_ = (value, path, into) => settingName(value, path, into, /^[A-Z][A-Z0-9_]{2,63}$/);
504
286
  const auth = input["auth"];
505
287
  for (const field of Object.keys(auth)) {
506
- if (!OAUTH_AUTH_FIELDS.includes(field)) {
288
+ const renamed = RENAMED_AUTH_FIELDS[field];
289
+ if (renamed) {
290
+ problems.push({ path: `auth.${field}`, message: renamed });
291
+ }
292
+ else if (!OAUTH_AUTH_FIELDS.includes(field)) {
507
293
  problems.push({
508
294
  path: `auth.${field}`,
509
295
  message: "not a field of auth.kind oauth2-user",
510
296
  });
511
297
  }
512
298
  }
513
- const authorize = endpoint(auth["authorizeUrl"], "auth.authorizeUrl", problems, {
514
- query: true,
515
- });
299
+ const authorize = endpoint(auth["authorizeUrl"], "auth.authorizeUrl", problems, { query: true });
516
300
  if (auth["authorizeUrl"] === undefined) {
517
301
  problems.push({ path: "auth.authorizeUrl", message: "required" });
518
302
  }
@@ -535,15 +319,12 @@ export function parseOAuthUserSpec(input, common, problems) {
535
319
  });
536
320
  }
537
321
  if (revokeMethod !== undefined && revoke === undefined) {
538
- problems.push({
539
- path: "auth.revokeMethod",
540
- message: "needs a revokeUrl",
541
- });
322
+ problems.push({ path: "auth.revokeMethod", message: "needs a revokeUrl" });
542
323
  }
543
- const clientId = settingName_(auth["clientId"], "auth.clientId", problems);
324
+ const clientId = settingName(auth["clientId"], "auth.clientId", problems);
544
325
  const clientSecret = auth["clientSecret"] === undefined
545
326
  ? undefined
546
- : settingName_(auth["clientSecret"], "auth.clientSecret", problems);
327
+ : settingName(auth["clientSecret"], "auth.clientSecret", problems);
547
328
  if (clientSecret !== undefined && clientSecret === clientId) {
548
329
  problems.push({
549
330
  path: "auth.clientSecret",
@@ -561,29 +342,32 @@ export function parseOAuthUserSpec(input, common, problems) {
561
342
  if (typeof pkce !== "boolean") {
562
343
  problems.push({ path: "auth.pkce", message: "must be true or false" });
563
344
  }
564
- const dialect = auth["scopeDialect"] ?? "space";
565
- if (!DIALECTS.includes(dialect)) {
345
+ const separator = auth["scopeSeparator"] ?? "space";
346
+ if (!SEPARATORS.includes(separator)) {
347
+ problems.push({
348
+ path: "auth.scopeSeparator",
349
+ message: `must be one of ${SEPARATORS.join(", ")}`,
350
+ });
351
+ }
352
+ const prefix = endpoint(auth["scopePrefix"], "auth.scopePrefix", problems, {
353
+ query: false,
354
+ });
355
+ if (prefix && !prefix.url.endsWith("/")) {
566
356
  problems.push({
567
- path: "auth.scopeDialect",
568
- message: `must be one of ${DIALECTS.join(", ")}`,
357
+ path: "auth.scopePrefix",
358
+ message: "must end in / (https://www.googleapis.com/auth/): an app's scope is joined onto it",
569
359
  });
570
360
  }
571
361
  const authorizeParams = stringMap(auth["authorizeParams"], "auth.authorizeParams", problems, (name, at) => {
572
362
  if (!PARAM_NAME.test(name)) {
573
363
  problems.push({ path: at, message: "must be a query parameter name" });
574
364
  }
575
- if ([
576
- "client_id",
577
- "redirect_uri",
578
- "response_type",
579
- "scope",
580
- "state",
581
- "code_challenge",
582
- "code_challenge_method",
583
- ].includes(name)) {
365
+ if (RESERVED_AUTHORIZE_PARAMS.includes(name)) {
584
366
  problems.push({
585
367
  path: at,
586
- message: `${name} is a parameter the Gateway sets itself`,
368
+ message: name === "include_granted_scopes"
369
+ ? "include_granted_scopes would widen the token beyond the app's scopes; the Gateway never sends it"
370
+ : `${name} is a parameter the Gateway sets itself`,
587
371
  });
588
372
  }
589
373
  });
@@ -600,78 +384,61 @@ export function parseOAuthUserSpec(input, common, problems) {
600
384
  }
601
385
  });
602
386
  const parsedIdentity = identity(auth["identity"], common.hosts, problems);
603
- const sets = scopeSets(input["scopeSets"], common.hosts, problems);
604
- const alwaysRequested = scopeList(input["alwaysRequested"], "alwaysRequested", problems);
605
- const tolerated = scopeList(input["tolerated"], "tolerated", problems);
387
+ const tolerated = scopeList(auth["tolerated"], "auth.tolerated", problems);
388
+ const resource = identifierUrl(auth["resource"], "auth.resource", problems, {
389
+ query: true,
390
+ });
391
+ const issuer = identifierUrl(auth["issuer"], "auth.issuer", problems, {
392
+ query: false,
393
+ });
606
394
  if (!authorize ||
607
395
  !token ||
608
396
  clientId === undefined ||
609
- !sets ||
610
397
  typeof pkce !== "boolean" ||
611
- !DIALECTS.includes(dialect) ||
398
+ !SEPARATORS.includes(separator) ||
612
399
  !INJECTIONS.includes(inject)) {
613
400
  return undefined;
614
401
  }
615
- const union = [];
616
- const add = (scopes) => {
617
- for (const scope of scopes)
618
- if (!union.includes(scope))
619
- union.push(scope);
620
- };
621
- for (const set of Object.values(sets)) {
622
- add(set.scopes);
623
- add(set.userScopes ?? []);
624
- }
625
- add(alwaysRequested ?? []);
626
- const policy = {
627
- sets,
628
- tolerated: tolerated ?? [],
629
- alwaysRequested: alwaysRequested ?? [],
630
- dialect: dialect,
631
- grantTypes: ["authorization_code", "refresh_token"],
632
- requestScope: "keep",
633
- deniedSegments: [...DEFAULT_DENIED_SEGMENTS],
634
- };
635
402
  const flow = {
403
+ authorizeUrl: authorize.url,
636
404
  tokenUrl: token.url,
637
405
  ...(revoke === undefined ? {} : { revokeUrl: revoke.url }),
638
406
  ...(revokeMethod === undefined
639
407
  ? {}
640
408
  : { revokeMethod: revokeMethod }),
409
+ inject: inject,
641
410
  pkce,
642
- scopeDialect: dialect,
411
+ scopeSeparator: separator,
412
+ ...(prefix === undefined ? {} : { scopePrefix: prefix.url }),
643
413
  ...(authorizeParams === undefined ? {} : { authorizeParams }),
644
414
  ...(tokenHeaders === undefined ? {} : { tokenHeaders }),
645
- ...(parsedIdentity === undefined ? {} : { identity: parsedIdentity }),
415
+ ...(parsedIdentity === undefined
416
+ ? {}
417
+ : { identity: parsedIdentity.identity }),
418
+ identityScopes: parsedIdentity?.scopes ?? [],
419
+ tolerated: tolerated ?? [],
420
+ ...(resource === undefined ? {} : { resource }),
421
+ ...(issuer === undefined ? {} : { issuer }),
646
422
  };
647
- const tokenPath = new URL(token.url).pathname;
648
- const capability = {
423
+ const capability = oauthCapabilityOf(common, {
424
+ clientId,
425
+ ...(clientSecret === undefined ? {} : { clientSecret }),
426
+ }, flow);
427
+ const hosts = [...common.hosts];
428
+ for (const host of [authorize.host, token.host, revoke?.host]) {
429
+ if (host && !hosts.includes(host))
430
+ hosts.push(host);
431
+ }
432
+ return { capability, hosts };
433
+ }
434
+ function oauthCapabilityOf(common, secrets, flow) {
435
+ return {
649
436
  mode: "oauth_client",
650
437
  label: common.name,
651
- secrets: {
652
- clientId,
653
- ...(clientSecret === undefined ? {} : { clientSecret }),
654
- },
655
- token: [
656
- {
657
- host: token.host,
658
- path: tokenPath,
659
- methods: ["POST"],
660
- inject: inject,
661
- },
662
- ],
663
- access: common.access ?? ["read"],
438
+ secrets,
664
439
  console: common.docs,
665
- authorize: { url: authorize.url, scopes: union },
666
- scopePolicy: policy,
667
440
  api: [...common.hosts],
668
441
  flow,
669
- ...(common.readMethods === undefined
670
- ? {}
671
- : { readMethods: common.readMethods }),
672
- ...(common.writeMethods === undefined
673
- ? {}
674
- : { writeMethods: common.writeMethods }),
675
442
  ...(common.description === undefined
676
443
  ? {}
677
444
  : { description: common.description }),
@@ -680,24 +447,21 @@ export function parseOAuthUserSpec(input, common, problems) {
680
447
  ...(common.example === undefined ? {} : { example: common.example }),
681
448
  ...(common.tags === undefined ? {} : { tags: common.tags }),
682
449
  };
683
- const hosts = [...common.hosts];
684
- for (const host of [authorize.host, token.host, revoke?.host]) {
685
- if (host && !hosts.includes(host))
686
- hosts.push(host);
687
- }
688
- return { capability, hosts };
689
450
  }
690
451
  /**
691
- * The JSON spec of an OAuth capability that has a `flow` (`provider
692
- * show`, a preset's round trip): `parseProviderSpec` of it gives the
693
- * capability back. A catalog client without a `flow` has no spec.
452
+ * The JSON spec of an OAuth capability (`provider show`, a preset's round
453
+ * trip): `parseProviderSpec` of it gives the capability back.
694
454
  */
695
455
  export function oauthSpecOf(id, capability) {
696
- const flow = capability.flow;
697
- const policy = capability.scopePolicy;
698
- if (!flow || !policy)
699
- return null;
700
- const inject = capability.token[0]?.inject ?? "form";
456
+ const { flow } = capability;
457
+ const identity = flow.identity === undefined
458
+ ? undefined
459
+ : {
460
+ ...flow.identity,
461
+ ...(flow.identityScopes.length === 0
462
+ ? {}
463
+ : { scopes: [...flow.identityScopes] }),
464
+ };
701
465
  return {
702
466
  id,
703
467
  name: capability.label,
@@ -711,7 +475,7 @@ export function oauthSpecOf(id, capability) {
711
475
  hosts: [...capability.api],
712
476
  auth: {
713
477
  kind: "oauth2-user",
714
- authorizeUrl: capability.authorize.url,
478
+ authorizeUrl: flow.authorizeUrl,
715
479
  tokenUrl: flow.tokenUrl,
716
480
  ...(flow.revokeUrl === undefined ? {} : { revokeUrl: flow.revokeUrl }),
717
481
  ...(flow.revokeMethod === undefined
@@ -721,57 +485,25 @@ export function oauthSpecOf(id, capability) {
721
485
  ...(capability.secrets.clientSecret === undefined
722
486
  ? {}
723
487
  : { clientSecret: capability.secrets.clientSecret }),
724
- inject,
488
+ inject: flow.inject,
725
489
  pkce: flow.pkce,
726
- scopeDialect: flow.scopeDialect,
490
+ scopeSeparator: flow.scopeSeparator,
491
+ ...(flow.scopePrefix === undefined
492
+ ? {}
493
+ : { scopePrefix: flow.scopePrefix }),
727
494
  ...(flow.authorizeParams === undefined
728
495
  ? {}
729
496
  : { authorizeParams: { ...flow.authorizeParams } }),
730
497
  ...(flow.tokenHeaders === undefined
731
498
  ? {}
732
499
  : { tokenHeaders: { ...flow.tokenHeaders } }),
733
- ...(flow.identity === undefined
500
+ ...(identity === undefined ? {} : { identity }),
501
+ ...(flow.tolerated.length === 0
734
502
  ? {}
735
- : { identity: { ...flow.identity } }),
503
+ : { tolerated: [...flow.tolerated] }),
504
+ ...(flow.resource === undefined ? {} : { resource: flow.resource }),
505
+ ...(flow.issuer === undefined ? {} : { issuer: flow.issuer }),
736
506
  },
737
- scopeSets: Object.fromEntries(Object.entries(policy.sets).map(([name, set]) => [
738
- name,
739
- {
740
- label: set.label,
741
- access: set.access,
742
- scopes: [...set.scopes],
743
- ...(set.userScopes === undefined
744
- ? {}
745
- : { userScopes: [...set.userScopes] }),
746
- ...(set.routes.length === 0
747
- ? {}
748
- : {
749
- routes: set.routes.map((route) => ({
750
- host: route.host,
751
- paths: [...route.paths],
752
- // `source` escapes `/` as `\/`; the spec wrote it bare.
753
- ...(route.posts === undefined
754
- ? {}
755
- : {
756
- posts: route.posts.map((pattern) => pattern.source.replace(/\\\//g, "/")),
757
- }),
758
- })),
759
- }),
760
- },
761
- ])),
762
- ...(policy.alwaysRequested.length === 0
763
- ? {}
764
- : { alwaysRequested: [...policy.alwaysRequested] }),
765
- ...(policy.tolerated.length === 0
766
- ? {}
767
- : { tolerated: [...policy.tolerated] }),
768
- access: [...capability.access],
769
- ...(capability.readMethods === undefined
770
- ? {}
771
- : { readMethods: [...capability.readMethods] }),
772
- ...(capability.writeMethods === undefined
773
- ? {}
774
- : { writeMethods: [...capability.writeMethods] }),
775
507
  ...(capability.hint === undefined ? {} : { hint: capability.hint }),
776
508
  ...(capability.example === undefined
777
509
  ? {}
@@ -779,11 +511,3 @@ export function oauthSpecOf(id, capability) {
779
511
  ...(capability.tags === undefined ? {} : { tags: [...capability.tags] }),
780
512
  };
781
513
  }
782
- /** The methods a held-token write may use on `capability` beyond its reads. */
783
- export function oauthWriteMethods(capability) {
784
- return capability.writeMethods ?? DEFAULT_WRITE_METHODS;
785
- }
786
- /** The methods a read grant may use on `capability` (GET and HEAD unless the spec says). */
787
- export function oauthReadMethods(capability) {
788
- return capability.readMethods ?? ["GET", "HEAD"];
789
- }