@every-app/sdk 0.3.0-rc.1 → 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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cloudflare/index.d.ts +2 -2
  3. package/dist/cloudflare/index.d.ts.map +1 -1
  4. package/dist/cloudflare/index.js +1 -1
  5. package/dist/cloudflare/sentinel.d.ts +39 -1
  6. package/dist/cloudflare/sentinel.d.ts.map +1 -1
  7. package/dist/cloudflare/sentinel.js +22 -1
  8. package/dist/cloudflare/worker.d.ts +205 -7
  9. package/dist/cloudflare/worker.d.ts.map +1 -1
  10. package/dist/cloudflare/worker.js +174 -11
  11. package/dist/internal/aiDefault.d.ts +141 -0
  12. package/dist/internal/aiDefault.d.ts.map +1 -0
  13. package/dist/internal/aiDefault.js +204 -0
  14. package/dist/internal/aiProviders.d.ts +4 -1
  15. package/dist/internal/aiProviders.d.ts.map +1 -1
  16. package/dist/internal/aiProviders.js +5 -1
  17. package/dist/internal/appSettings.d.ts +20 -2
  18. package/dist/internal/appSettings.d.ts.map +1 -1
  19. package/dist/internal/appSettings.js +53 -7
  20. package/dist/internal/audit.d.ts +108 -0
  21. package/dist/internal/audit.d.ts.map +1 -0
  22. package/dist/internal/audit.js +235 -0
  23. package/dist/internal/capabilities.d.ts +2315 -141
  24. package/dist/internal/capabilities.d.ts.map +1 -1
  25. package/dist/internal/capabilities.js +122 -0
  26. package/dist/internal/catalog/commerce.d.ts +394 -0
  27. package/dist/internal/catalog/commerce.d.ts.map +1 -0
  28. package/dist/internal/catalog/commerce.js +477 -0
  29. package/dist/internal/catalog/comms.d.ts +338 -0
  30. package/dist/internal/catalog/comms.d.ts.map +1 -0
  31. package/dist/internal/catalog/comms.js +477 -0
  32. package/dist/internal/catalog/index.d.ts +1936 -0
  33. package/dist/internal/catalog/index.d.ts.map +1 -0
  34. package/dist/internal/catalog/index.js +19 -0
  35. package/dist/internal/catalog/inference.d.ts +393 -0
  36. package/dist/internal/catalog/inference.d.ts.map +1 -0
  37. package/dist/internal/catalog/inference.js +457 -0
  38. package/dist/internal/catalog/infra.d.ts +423 -0
  39. package/dist/internal/catalog/infra.d.ts.map +1 -0
  40. package/dist/internal/catalog/infra.js +543 -0
  41. package/dist/internal/catalog/search.d.ts +400 -0
  42. package/dist/internal/catalog/search.d.ts.map +1 -0
  43. package/dist/internal/catalog/search.js +463 -0
  44. package/dist/internal/devProviders.d.ts +2 -2
  45. package/dist/internal/devProviders.d.ts.map +1 -1
  46. package/dist/internal/devProviders.js +19 -1
  47. package/dist/internal/events.d.ts +13 -2
  48. package/dist/internal/events.d.ts.map +1 -1
  49. package/dist/internal/events.js +21 -3
  50. package/dist/internal/gatewaySettings.d.ts +2 -1
  51. package/dist/internal/gatewaySettings.d.ts.map +1 -1
  52. package/dist/internal/gatewaySettings.js +10 -2
  53. package/dist/internal/index.d.ts +5 -0
  54. package/dist/internal/index.d.ts.map +1 -1
  55. package/dist/internal/index.js +5 -0
  56. package/dist/internal/oauthPresets.d.ts +29 -0
  57. package/dist/internal/oauthPresets.d.ts.map +1 -0
  58. package/dist/internal/oauthPresets.js +534 -0
  59. package/dist/internal/oauthScopes.d.ts +56 -1
  60. package/dist/internal/oauthScopes.d.ts.map +1 -1
  61. package/dist/internal/oauthScopes.js +98 -4
  62. package/dist/internal/oauthSpec.d.ts +59 -0
  63. package/dist/internal/oauthSpec.d.ts.map +1 -0
  64. package/dist/internal/oauthSpec.js +676 -0
  65. package/dist/internal/protocol.d.ts +11 -0
  66. package/dist/internal/protocol.d.ts.map +1 -1
  67. package/dist/internal/protocol.js +9 -0
  68. package/dist/internal/providerRegistry.d.ts +17 -6
  69. package/dist/internal/providerRegistry.d.ts.map +1 -1
  70. package/dist/internal/providerRegistry.js +51 -2
  71. package/dist/internal/providerSpec.d.ts +11 -7
  72. package/dist/internal/providerSpec.d.ts.map +1 -1
  73. package/dist/internal/providerSpec.js +142 -33
  74. package/dist/internal/signInProviders.d.ts +104 -0
  75. package/dist/internal/signInProviders.d.ts.map +1 -0
  76. package/dist/internal/signInProviders.js +245 -0
  77. package/dist/server/ai.d.ts +45 -0
  78. package/dist/server/ai.d.ts.map +1 -0
  79. package/dist/server/ai.js +40 -0
  80. package/dist/server/connectUrl.d.ts +21 -0
  81. package/dist/server/connectUrl.d.ts.map +1 -0
  82. package/dist/server/connectUrl.js +37 -0
  83. package/dist/server/gatewayFetch.d.ts.map +1 -1
  84. package/dist/server/gatewayFetch.js +72 -8
  85. package/dist/server/index.d.ts +4 -2
  86. package/dist/server/index.d.ts.map +1 -1
  87. package/dist/server/index.js +3 -1
  88. package/dist/server/integrationFetch.d.ts +39 -2
  89. package/dist/server/integrationFetch.d.ts.map +1 -1
  90. package/dist/server/integrationFetch.js +177 -13
  91. package/package.json +12 -12
@@ -0,0 +1,676 @@
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
+ ];
20
+ const INJECTIONS = ["form", "basic"];
21
+ const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
22
+ /** Headers the Gateway sets on the token request itself, or never sends. */
23
+ const RESERVED_TOKEN_HEADERS = new Set([
24
+ "authorization",
25
+ "content-type",
26
+ "content-length",
27
+ "host",
28
+ "cookie",
29
+ ]);
30
+ const PARAM_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
31
+ /** A dot path into a JSON answer, at most four segments. */
32
+ const DOT_PATH = /^[A-Za-z_][A-Za-z0-9_]{0,63}(?:\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/;
33
+ const UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
34
+ const MAX_SETS = 20;
35
+ const MAX_SCOPES = 40;
36
+ const MAX_ROUTES = 20;
37
+ const MAX_PATHS = 50;
38
+ 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
+ /** The fields of `auth` when its kind is `oauth2-user`. */
42
+ export const OAUTH_AUTH_FIELDS = [
43
+ "kind",
44
+ "authorizeUrl",
45
+ "tokenUrl",
46
+ "revokeUrl",
47
+ "revokeMethod",
48
+ "clientId",
49
+ "clientSecret",
50
+ "inject",
51
+ "pkce",
52
+ "scopeDialect",
53
+ "authorizeParams",
54
+ "tokenHeaders",
55
+ "identity",
56
+ ];
57
+ function isRecord(value) {
58
+ return typeof value === "object" && value !== null && !Array.isArray(value);
59
+ }
60
+ function printable(value, path, problems, max) {
61
+ if (value === undefined)
62
+ return undefined;
63
+ if (typeof value !== "string" || value.length === 0 || value.length > max) {
64
+ problems.push({
65
+ path,
66
+ message: `must be a string of 1 to ${max} characters`,
67
+ });
68
+ return undefined;
69
+ }
70
+ if (UNPRINTABLE.test(value)) {
71
+ problems.push({ path, message: "holds a control character" });
72
+ return undefined;
73
+ }
74
+ return value;
75
+ }
76
+ /**
77
+ * An https URL with a host a row may name (the caller runs the gateway's
78
+ * host rules on `host` afterwards). `{client_id}` may stand in a path
79
+ * (GitHub's revoke URL names the client there).
80
+ */
81
+ function endpoint(value, path, problems, { query }) {
82
+ if (value === undefined)
83
+ return undefined;
84
+ const url = printable(value, path, problems, 500);
85
+ if (url === undefined)
86
+ return undefined;
87
+ let parsed;
88
+ try {
89
+ parsed = new URL(url);
90
+ }
91
+ catch {
92
+ problems.push({ path, message: "must be an https URL" });
93
+ return undefined;
94
+ }
95
+ if (parsed.protocol !== "https:" ||
96
+ parsed.username ||
97
+ parsed.password ||
98
+ parsed.port ||
99
+ parsed.hash) {
100
+ problems.push({
101
+ path,
102
+ message: "must be an https URL with no port, credentials or fragment",
103
+ });
104
+ return undefined;
105
+ }
106
+ if (!query && parsed.search) {
107
+ problems.push({ path, message: "may not carry a query string" });
108
+ return undefined;
109
+ }
110
+ return { url, host: parsed.hostname.toLowerCase() };
111
+ }
112
+ function settingName(value, path, problems, pattern) {
113
+ if (typeof value !== "string" || !pattern.test(value)) {
114
+ problems.push({
115
+ path,
116
+ message: "must name a Gateway setting: upper case letters, digits and underscores, a letter first, 3 to 64 characters (GITHUB_CLIENT_ID)",
117
+ });
118
+ return undefined;
119
+ }
120
+ return value;
121
+ }
122
+ function stringMap(value, path, problems, each) {
123
+ if (value === undefined)
124
+ return undefined;
125
+ if (!isRecord(value)) {
126
+ problems.push({ path, message: "must be an object of strings" });
127
+ return undefined;
128
+ }
129
+ const entries = Object.entries(value);
130
+ if (entries.length > MAX_PARAMS) {
131
+ problems.push({ path, message: `at most ${MAX_PARAMS} entries` });
132
+ return undefined;
133
+ }
134
+ const out = {};
135
+ for (const [name, raw] of entries) {
136
+ const at = `${path}.${name}`;
137
+ each(name, at);
138
+ const text = printable(raw, at, problems, 200);
139
+ if (text !== undefined)
140
+ out[name] = text;
141
+ }
142
+ return out;
143
+ }
144
+ function scopeList(value, path, problems) {
145
+ if (value === undefined)
146
+ return undefined;
147
+ if (!Array.isArray(value) || value.length > MAX_SCOPES) {
148
+ problems.push({
149
+ path,
150
+ message: `must be a list of at most ${MAX_SCOPES} scopes`,
151
+ });
152
+ return undefined;
153
+ }
154
+ const out = [];
155
+ value.forEach((item, index) => {
156
+ const scope = printable(item, `${path}.${index}`, problems, 200);
157
+ if (scope === undefined)
158
+ return;
159
+ if (/\s/.test(scope) || scope.includes(",")) {
160
+ problems.push({
161
+ path: `${path}.${index}`,
162
+ message: "one scope, with no space or comma",
163
+ });
164
+ return;
165
+ }
166
+ out.push(scope);
167
+ });
168
+ if (new Set(out).size !== out.length) {
169
+ problems.push({ path, message: "lists a scope twice" });
170
+ }
171
+ return out;
172
+ }
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
+ function scopeSets(value, hosts, problems) {
268
+ if (!isRecord(value)) {
269
+ problems.push({
270
+ path: "scopeSets",
271
+ message: 'required: at least one named set, { "repo.read": { "label", "access", "scopes" } }',
272
+ });
273
+ return undefined;
274
+ }
275
+ const entries = Object.entries(value);
276
+ if (entries.length === 0 || entries.length > MAX_SETS) {
277
+ problems.push({
278
+ path: "scopeSets",
279
+ message: `must name 1 to ${MAX_SETS} sets`,
280
+ });
281
+ return undefined;
282
+ }
283
+ const out = {};
284
+ for (const [name, raw] of entries) {
285
+ const at = `scopeSets.${name}`;
286
+ if (!SCOPE_SET_NAME.test(name)) {
287
+ problems.push({
288
+ path: at,
289
+ message: "a set is named identity, or <area>.<access> in lowercase (repo.read)",
290
+ });
291
+ continue;
292
+ }
293
+ if (!isRecord(raw)) {
294
+ problems.push({ path: at, message: "must be { label, access, scopes }" });
295
+ continue;
296
+ }
297
+ for (const field of Object.keys(raw)) {
298
+ if (!["label", "access", "scopes", "userScopes", "routes"].includes(field)) {
299
+ problems.push({ path: `${at}.${field}`, message: "unknown field" });
300
+ }
301
+ }
302
+ const label = printable(raw["label"], `${at}.label`, problems, 80);
303
+ if (raw["label"] === undefined) {
304
+ problems.push({ path: `${at}.label`, message: "required" });
305
+ }
306
+ const access = raw["access"];
307
+ if (access !== "read" && access !== "write") {
308
+ problems.push({ path: `${at}.access`, message: "must be read or write" });
309
+ }
310
+ const scopes = scopeList(raw["scopes"], `${at}.scopes`, problems);
311
+ if (raw["scopes"] === undefined) {
312
+ problems.push({ path: `${at}.scopes`, message: "required" });
313
+ }
314
+ const userScopes = scopeList(raw["userScopes"], `${at}.userScopes`, problems);
315
+ const parsedRoutes = routes(raw["routes"], `${at}.routes`, hosts, problems);
316
+ if (label === undefined ||
317
+ (access !== "read" && access !== "write") ||
318
+ scopes === undefined) {
319
+ continue;
320
+ }
321
+ out[name] = {
322
+ label,
323
+ access,
324
+ scopes,
325
+ ...(userScopes === undefined ? {} : { userScopes }),
326
+ routes: parsedRoutes ?? [],
327
+ };
328
+ }
329
+ return out;
330
+ }
331
+ function identity(value, hosts, problems) {
332
+ if (value === undefined)
333
+ return undefined;
334
+ if (!isRecord(value)) {
335
+ problems.push({
336
+ path: "auth.identity",
337
+ message: "must be { url, label, id? }",
338
+ });
339
+ return undefined;
340
+ }
341
+ for (const field of Object.keys(value)) {
342
+ if (!["url", "label", "id"].includes(field)) {
343
+ problems.push({
344
+ path: `auth.identity.${field}`,
345
+ message: "unknown field",
346
+ });
347
+ }
348
+ }
349
+ const url = endpoint(value["url"], "auth.identity.url", problems, {
350
+ query: true,
351
+ });
352
+ if (value["url"] === undefined) {
353
+ problems.push({ path: "auth.identity.url", message: "required" });
354
+ }
355
+ else if (url && !hosts.includes(url.host)) {
356
+ problems.push({
357
+ path: "auth.identity.url",
358
+ message: "must be on one of hosts (the token is sent there)",
359
+ });
360
+ }
361
+ const label = printable(value["label"], "auth.identity.label", problems, 128);
362
+ if (label !== undefined && !DOT_PATH.test(label)) {
363
+ problems.push({
364
+ path: "auth.identity.label",
365
+ message: "must be a JSON field name, or a dotted path to a nested one",
366
+ });
367
+ }
368
+ if (value["label"] === undefined) {
369
+ problems.push({ path: "auth.identity.label", message: "required" });
370
+ }
371
+ const id = printable(value["id"], "auth.identity.id", problems, 128);
372
+ if (id !== undefined && !DOT_PATH.test(id)) {
373
+ problems.push({
374
+ path: "auth.identity.id",
375
+ message: "must be a JSON field name, or a dotted path to a nested one",
376
+ });
377
+ }
378
+ if (!url || label === undefined)
379
+ return undefined;
380
+ return { url: url.url, label, ...(id === undefined ? {} : { id }) };
381
+ }
382
+ /**
383
+ * Parse the OAuth half of a spec whose `auth.kind` is `oauth2-user`:
384
+ * `auth` itself, `scopeSets`, `alwaysRequested` and `tolerated`, with
385
+ * the common fields already parsed. Every problem is reported; on
386
+ * success the catalog-shaped capability with its `flow`.
387
+ */
388
+ export function parseOAuthUserSpec(input, common, problems) {
389
+ const settingName_ = (value, path, into) => settingName(value, path, into, /^[A-Z][A-Z0-9_]{2,63}$/);
390
+ const auth = input["auth"];
391
+ for (const field of Object.keys(auth)) {
392
+ if (!OAUTH_AUTH_FIELDS.includes(field)) {
393
+ problems.push({
394
+ path: `auth.${field}`,
395
+ message: "not a field of auth.kind oauth2-user",
396
+ });
397
+ }
398
+ }
399
+ const authorize = endpoint(auth["authorizeUrl"], "auth.authorizeUrl", problems, {
400
+ query: true,
401
+ });
402
+ if (auth["authorizeUrl"] === undefined) {
403
+ problems.push({ path: "auth.authorizeUrl", message: "required" });
404
+ }
405
+ const token = endpoint(auth["tokenUrl"], "auth.tokenUrl", problems, {
406
+ query: false,
407
+ });
408
+ if (auth["tokenUrl"] === undefined) {
409
+ problems.push({ path: "auth.tokenUrl", message: "required" });
410
+ }
411
+ const revoke = endpoint(auth["revokeUrl"], "auth.revokeUrl", problems, {
412
+ query: false,
413
+ });
414
+ const revokeMethod = auth["revokeMethod"];
415
+ if (revokeMethod !== undefined &&
416
+ revokeMethod !== "POST" &&
417
+ revokeMethod !== "DELETE") {
418
+ problems.push({
419
+ path: "auth.revokeMethod",
420
+ message: "must be POST or DELETE",
421
+ });
422
+ }
423
+ if (revokeMethod !== undefined && revoke === undefined) {
424
+ problems.push({
425
+ path: "auth.revokeMethod",
426
+ message: "needs a revokeUrl",
427
+ });
428
+ }
429
+ const clientId = settingName_(auth["clientId"], "auth.clientId", problems);
430
+ const clientSecret = auth["clientSecret"] === undefined
431
+ ? undefined
432
+ : settingName_(auth["clientSecret"], "auth.clientSecret", problems);
433
+ if (clientSecret !== undefined && clientSecret === clientId) {
434
+ problems.push({
435
+ path: "auth.clientSecret",
436
+ message: "must be another setting than auth.clientId",
437
+ });
438
+ }
439
+ const inject = auth["inject"] ?? "form";
440
+ if (!INJECTIONS.includes(inject)) {
441
+ problems.push({
442
+ path: "auth.inject",
443
+ message: "must be form (the client in the body) or basic (HTTP Basic)",
444
+ });
445
+ }
446
+ const pkce = auth["pkce"] ?? true;
447
+ if (typeof pkce !== "boolean") {
448
+ problems.push({ path: "auth.pkce", message: "must be true or false" });
449
+ }
450
+ const dialect = auth["scopeDialect"] ?? "space";
451
+ if (!DIALECTS.includes(dialect)) {
452
+ problems.push({
453
+ path: "auth.scopeDialect",
454
+ message: `must be one of ${DIALECTS.join(", ")}`,
455
+ });
456
+ }
457
+ const authorizeParams = stringMap(auth["authorizeParams"], "auth.authorizeParams", problems, (name, at) => {
458
+ if (!PARAM_NAME.test(name)) {
459
+ problems.push({ path: at, message: "must be a query parameter name" });
460
+ }
461
+ if ([
462
+ "client_id",
463
+ "redirect_uri",
464
+ "response_type",
465
+ "scope",
466
+ "state",
467
+ "code_challenge",
468
+ "code_challenge_method",
469
+ ].includes(name)) {
470
+ problems.push({
471
+ path: at,
472
+ message: `${name} is a parameter the Gateway sets itself`,
473
+ });
474
+ }
475
+ });
476
+ const tokenHeaders = stringMap(auth["tokenHeaders"], "auth.tokenHeaders", problems, (name, at) => {
477
+ if (!HEADER_NAME.test(name)) {
478
+ problems.push({ path: at, message: "must be a header name" });
479
+ }
480
+ else if (RESERVED_TOKEN_HEADERS.has(name.toLowerCase()) ||
481
+ name.toLowerCase().startsWith("x-everyapp-")) {
482
+ problems.push({
483
+ path: at,
484
+ message: `${name} is a header the Gateway sets itself`,
485
+ });
486
+ }
487
+ });
488
+ const parsedIdentity = identity(auth["identity"], common.hosts, problems);
489
+ const sets = scopeSets(input["scopeSets"], common.hosts, problems);
490
+ const alwaysRequested = scopeList(input["alwaysRequested"], "alwaysRequested", problems);
491
+ const tolerated = scopeList(input["tolerated"], "tolerated", problems);
492
+ if (!authorize ||
493
+ !token ||
494
+ clientId === undefined ||
495
+ !sets ||
496
+ typeof pkce !== "boolean" ||
497
+ !DIALECTS.includes(dialect) ||
498
+ !INJECTIONS.includes(inject)) {
499
+ return undefined;
500
+ }
501
+ const union = [];
502
+ const add = (scopes) => {
503
+ for (const scope of scopes)
504
+ if (!union.includes(scope))
505
+ union.push(scope);
506
+ };
507
+ for (const set of Object.values(sets)) {
508
+ add(set.scopes);
509
+ add(set.userScopes ?? []);
510
+ }
511
+ add(alwaysRequested ?? []);
512
+ const policy = {
513
+ sets,
514
+ tolerated: tolerated ?? [],
515
+ alwaysRequested: alwaysRequested ?? [],
516
+ dialect: dialect,
517
+ grantTypes: ["authorization_code", "refresh_token"],
518
+ requestScope: "keep",
519
+ deniedSegments: [...DEFAULT_DENIED_SEGMENTS],
520
+ };
521
+ const flow = {
522
+ tokenUrl: token.url,
523
+ ...(revoke === undefined ? {} : { revokeUrl: revoke.url }),
524
+ ...(revokeMethod === undefined
525
+ ? {}
526
+ : { revokeMethod: revokeMethod }),
527
+ pkce,
528
+ scopeDialect: dialect,
529
+ ...(authorizeParams === undefined ? {} : { authorizeParams }),
530
+ ...(tokenHeaders === undefined ? {} : { tokenHeaders }),
531
+ ...(parsedIdentity === undefined ? {} : { identity: parsedIdentity }),
532
+ };
533
+ const tokenPath = new URL(token.url).pathname;
534
+ const capability = {
535
+ mode: "oauth_client",
536
+ label: common.name,
537
+ secrets: {
538
+ clientId,
539
+ ...(clientSecret === undefined ? {} : { clientSecret }),
540
+ },
541
+ token: [
542
+ {
543
+ host: token.host,
544
+ path: tokenPath,
545
+ methods: ["POST"],
546
+ inject: inject,
547
+ },
548
+ ],
549
+ access: common.access ?? ["read"],
550
+ console: common.docs,
551
+ authorize: { url: authorize.url, scopes: union },
552
+ scopePolicy: policy,
553
+ api: [...common.hosts],
554
+ redirectPaths: [],
555
+ flow,
556
+ ...(common.readMethods === undefined
557
+ ? {}
558
+ : { readMethods: common.readMethods }),
559
+ ...(common.writeMethods === undefined
560
+ ? {}
561
+ : { writeMethods: common.writeMethods }),
562
+ ...(common.description === undefined
563
+ ? {}
564
+ : { description: common.description }),
565
+ ...(common.reference === undefined ? {} : { reference: common.reference }),
566
+ ...(common.hint === undefined ? {} : { hint: common.hint }),
567
+ ...(common.example === undefined ? {} : { example: common.example }),
568
+ ...(common.tags === undefined ? {} : { tags: common.tags }),
569
+ };
570
+ const hosts = [...common.hosts];
571
+ for (const host of [authorize.host, token.host, revoke?.host]) {
572
+ if (host && !hosts.includes(host))
573
+ hosts.push(host);
574
+ }
575
+ return { capability, hosts };
576
+ }
577
+ /**
578
+ * The JSON spec of an OAuth capability that has a `flow` (`provider
579
+ * show`, a preset's round trip): `parseProviderSpec` of it gives the
580
+ * capability back. A catalog client without a `flow` has no spec.
581
+ */
582
+ export function oauthSpecOf(id, capability) {
583
+ const flow = capability.flow;
584
+ const policy = capability.scopePolicy;
585
+ if (!flow || !policy)
586
+ return null;
587
+ const inject = capability.token[0]?.inject ?? "form";
588
+ return {
589
+ id,
590
+ name: capability.label,
591
+ ...(capability.description === undefined
592
+ ? {}
593
+ : { description: capability.description }),
594
+ docs: capability.console,
595
+ ...(capability.reference === undefined
596
+ ? {}
597
+ : { api: capability.reference }),
598
+ hosts: [...capability.api],
599
+ auth: {
600
+ kind: "oauth2-user",
601
+ authorizeUrl: capability.authorize.url,
602
+ tokenUrl: flow.tokenUrl,
603
+ ...(flow.revokeUrl === undefined ? {} : { revokeUrl: flow.revokeUrl }),
604
+ ...(flow.revokeMethod === undefined
605
+ ? {}
606
+ : { revokeMethod: flow.revokeMethod }),
607
+ clientId: capability.secrets.clientId,
608
+ ...(capability.secrets.clientSecret === undefined
609
+ ? {}
610
+ : { clientSecret: capability.secrets.clientSecret }),
611
+ inject,
612
+ pkce: flow.pkce,
613
+ scopeDialect: flow.scopeDialect,
614
+ ...(flow.authorizeParams === undefined
615
+ ? {}
616
+ : { authorizeParams: { ...flow.authorizeParams } }),
617
+ ...(flow.tokenHeaders === undefined
618
+ ? {}
619
+ : { tokenHeaders: { ...flow.tokenHeaders } }),
620
+ ...(flow.identity === undefined
621
+ ? {}
622
+ : { identity: { ...flow.identity } }),
623
+ },
624
+ scopeSets: Object.fromEntries(Object.entries(policy.sets).map(([name, set]) => [
625
+ name,
626
+ {
627
+ label: set.label,
628
+ access: set.access,
629
+ scopes: [...set.scopes],
630
+ ...(set.userScopes === undefined
631
+ ? {}
632
+ : { userScopes: [...set.userScopes] }),
633
+ ...(set.routes.length === 0
634
+ ? {}
635
+ : {
636
+ routes: set.routes.map((route) => ({
637
+ host: route.host,
638
+ paths: [...route.paths],
639
+ // `source` escapes `/` as `\/`; the spec wrote it bare.
640
+ ...(route.posts === undefined
641
+ ? {}
642
+ : {
643
+ posts: route.posts.map((pattern) => pattern.source.replace(/\\\//g, "/")),
644
+ }),
645
+ })),
646
+ }),
647
+ },
648
+ ])),
649
+ ...(policy.alwaysRequested.length === 0
650
+ ? {}
651
+ : { alwaysRequested: [...policy.alwaysRequested] }),
652
+ ...(policy.tolerated.length === 0
653
+ ? {}
654
+ : { tolerated: [...policy.tolerated] }),
655
+ access: [...capability.access],
656
+ ...(capability.readMethods === undefined
657
+ ? {}
658
+ : { readMethods: [...capability.readMethods] }),
659
+ ...(capability.writeMethods === undefined
660
+ ? {}
661
+ : { writeMethods: [...capability.writeMethods] }),
662
+ ...(capability.hint === undefined ? {} : { hint: capability.hint }),
663
+ ...(capability.example === undefined
664
+ ? {}
665
+ : { example: capability.example }),
666
+ ...(capability.tags === undefined ? {} : { tags: [...capability.tags] }),
667
+ };
668
+ }
669
+ /** The methods a held-token write may use on `capability` beyond its reads. */
670
+ export function oauthWriteMethods(capability) {
671
+ return capability.writeMethods ?? DEFAULT_WRITE_METHODS;
672
+ }
673
+ /** The methods a read grant may use on `capability` (GET and HEAD unless the spec says). */
674
+ export function oauthReadMethods(capability) {
675
+ return capability.readMethods ?? ["GET", "HEAD"];
676
+ }
@@ -39,6 +39,17 @@ export declare const GATEWAY_ERROR_HEADER = "x-everyapp-error";
39
39
  * from what browsers send.
40
40
  */
41
41
  export declare const ORG_HEADER = "x-everyapp-org";
42
+ /**
43
+ * Which connection an app's call to an OAuth provider uses: the person's
44
+ * (`user`, the default when a person is behind the call) or the
45
+ * organization's (`organization`, the default for background work),
46
+ * docs/design/2026-10-oauth-providers.md section 7. Set by
47
+ * `integrationFetch({ account })`; the gateway reads it on the held-token
48
+ * branch and ignores it elsewhere.
49
+ */
50
+ export declare const ACCOUNT_HEADER = "x-everyapp-account";
51
+ /** The values {@link ACCOUNT_HEADER} may carry. */
52
+ export type ConnectionAccount = "user" | "organization";
42
53
  /** 120-second lifetime — caps revocation staleness while surviving clock skew. */
43
54
  export declare const IDENTITY_TTL_SECONDS = 120;
44
55
  /** System tokens are minted per call and live at most a minute. */