@every-app/sdk 0.3.0-rc.10 → 0.3.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -64
- package/dist/cloudflare/catalogSettings.generated.d.ts +7 -4
- package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -1
- package/dist/cloudflare/catalogSettings.generated.js +7 -6
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/worker.d.ts +35 -1
- package/dist/cloudflare/worker.d.ts.map +1 -1
- package/dist/cloudflare/worker.js +31 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/aiDefault.d.ts +4 -3
- package/dist/internal/aiDefault.d.ts.map +1 -1
- package/dist/internal/aiDefault.js +6 -5
- package/dist/internal/aiProviders.d.ts.map +1 -1
- package/dist/internal/aiProviders.js +2 -2
- package/dist/internal/capabilities.d.ts +97 -481
- package/dist/internal/capabilities.d.ts.map +1 -1
- package/dist/internal/capabilities.js +47 -548
- package/dist/internal/catalog/index.d.ts +35 -0
- package/dist/internal/catalog/index.d.ts.map +1 -1
- package/dist/internal/catalog/infra.d.ts +35 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -1
- package/dist/internal/catalog/infra.js +65 -0
- package/dist/internal/events.d.ts +52 -1
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +65 -3
- package/dist/internal/oauthBuiltIns.d.ts +101 -0
- package/dist/internal/oauthBuiltIns.d.ts.map +1 -0
- package/dist/internal/oauthBuiltIns.js +122 -0
- package/dist/internal/oauthClient.d.ts +7 -47
- package/dist/internal/oauthClient.d.ts.map +1 -1
- package/dist/internal/oauthClient.js +1 -115
- package/dist/internal/oauthPresets.d.ts +14 -15
- package/dist/internal/oauthPresets.d.ts.map +1 -1
- package/dist/internal/oauthPresets.js +42 -853
- package/dist/internal/oauthScopes.d.ts +43 -155
- package/dist/internal/oauthScopes.d.ts.map +1 -1
- package/dist/internal/oauthScopes.js +82 -475
- package/dist/internal/oauthSpec.d.ts +72 -71
- package/dist/internal/oauthSpec.d.ts.map +1 -1
- package/dist/internal/oauthSpec.js +178 -447
- package/dist/internal/protocol.d.ts +42 -13
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +45 -7
- package/dist/internal/providerRegistry.d.ts +1 -61
- package/dist/internal/providerRegistry.d.ts.map +1 -1
- package/dist/internal/providerRegistry.js +8 -115
- package/dist/internal/providerSpec.d.ts.map +1 -1
- package/dist/internal/providerSpec.js +14 -36
- package/dist/internal/remoteMcp.d.ts.map +1 -1
- package/dist/internal/remoteMcp.js +1 -5
- package/dist/internal/tools.d.ts +5 -4
- package/dist/internal/tools.d.ts.map +1 -1
- package/dist/internal/tools.js +4 -3
- package/dist/manifest.d.ts +34 -5
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +1 -1
- package/dist/server/attribution.d.ts +9 -0
- package/dist/server/attribution.d.ts.map +1 -1
- package/dist/server/attribution.js +7 -1
- package/dist/server/connectUrl.d.ts +4 -9
- package/dist/server/connectUrl.d.ts.map +1 -1
- package/dist/server/connectUrl.js +17 -14
- package/dist/server/eventsEndpoint.d.ts +48 -0
- package/dist/server/eventsEndpoint.d.ts.map +1 -0
- package/dist/server/eventsEndpoint.js +145 -0
- package/dist/server/everyApp.d.ts +15 -6
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +12 -3
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/integrationFetch.d.ts +59 -71
- package/dist/server/integrationFetch.d.ts.map +1 -1
- package/dist/server/integrationFetch.js +117 -392
- package/dist/server/tools.d.ts +7 -2
- package/dist/server/tools.d.ts.map +1 -1
- package/dist/server/tools.js +2 -1
- package/dist/server/toolsEndpoint.d.ts +3 -1
- package/dist/server/toolsEndpoint.d.ts.map +1 -1
- package/dist/server/toolsEndpoint.js +9 -3
- package/dist/server/webhooks.d.ts +56 -0
- package/dist/server/webhooks.d.ts.map +1 -0
- package/dist/server/webhooks.js +205 -0
- package/package.json +1 -1
- package/dist/server/authorizeUrl.d.ts +0 -43
- package/dist/server/authorizeUrl.d.ts.map +0 -1
- package/dist/server/authorizeUrl.js +0 -106
|
@@ -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,30 @@ export const OAUTH_AUTH_FIELDS = [
|
|
|
49
41
|
"clientSecret",
|
|
50
42
|
"inject",
|
|
51
43
|
"pkce",
|
|
52
|
-
"
|
|
44
|
+
"scopeSeparator",
|
|
45
|
+
"scopePrefix",
|
|
53
46
|
"authorizeParams",
|
|
54
47
|
"tokenHeaders",
|
|
55
48
|
"identity",
|
|
49
|
+
"tolerated",
|
|
56
50
|
];
|
|
51
|
+
/**
|
|
52
|
+
* Fields an `oauth2-user` spec had before scopes moved to each app's
|
|
53
|
+
* `everyapp.config.ts`, each with what to write instead: refused with
|
|
54
|
+
* that message rather than "unknown field".
|
|
55
|
+
*/
|
|
56
|
+
export const REMOVED_OAUTH_FIELDS = {
|
|
57
|
+
scopeSets: "scope sets are gone: each app lists the provider's own scopes in its everyapp.config.ts. Remove scopeSets.",
|
|
58
|
+
alwaysRequested: "alwaysRequested is gone: put scopes every connect needs (a refresh token's, the identity URL's) in auth.identity.scopes.",
|
|
59
|
+
tolerated: "tolerated moved into auth: auth.tolerated lists the scopes a token may carry beyond what was asked.",
|
|
60
|
+
access: "access is gone for an OAuth provider: the scopes an app lists are the grant, for every method.",
|
|
61
|
+
readMethods: "readMethods is gone for an OAuth provider: the scopes an app lists are the grant, for every method.",
|
|
62
|
+
writeMethods: "writeMethods is gone for an OAuth provider: the scopes an app lists are the grant, for every method.",
|
|
63
|
+
};
|
|
64
|
+
/** `auth` fields renamed, each with its new name. */
|
|
65
|
+
const RENAMED_AUTH_FIELDS = {
|
|
66
|
+
scopeDialect: "renamed auth.scopeSeparator: space or comma (a provider's prefix goes in auth.scopePrefix)",
|
|
67
|
+
};
|
|
57
68
|
function isRecord(value) {
|
|
58
69
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59
70
|
}
|
|
@@ -109,8 +120,8 @@ function endpoint(value, path, problems, { query }) {
|
|
|
109
120
|
}
|
|
110
121
|
return { url, host: parsed.hostname.toLowerCase() };
|
|
111
122
|
}
|
|
112
|
-
function settingName(value, path, problems
|
|
113
|
-
if (typeof value !== "string" ||
|
|
123
|
+
function settingName(value, path, problems) {
|
|
124
|
+
if (typeof value !== "string" || !/^[A-Z][A-Z0-9_]{2,63}$/.test(value)) {
|
|
114
125
|
problems.push({
|
|
115
126
|
path,
|
|
116
127
|
message: "must name a Gateway setting: upper case letters, digits and underscores, a letter first, 3 to 64 characters (GITHUB_CLIENT_ID)",
|
|
@@ -141,7 +152,8 @@ function stringMap(value, path, problems, each) {
|
|
|
141
152
|
}
|
|
142
153
|
return out;
|
|
143
154
|
}
|
|
144
|
-
|
|
155
|
+
/** A list of provider scope strings: each one scope, with no space or comma, each once. */
|
|
156
|
+
export function scopeList(value, path, problems) {
|
|
145
157
|
if (value === undefined)
|
|
146
158
|
return undefined;
|
|
147
159
|
if (!Array.isArray(value) || value.length > MAX_SCOPES) {
|
|
@@ -170,290 +182,18 @@ function scopeList(value, path, problems) {
|
|
|
170
182
|
}
|
|
171
183
|
return out;
|
|
172
184
|
}
|
|
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
185
|
function identity(value, hosts, problems) {
|
|
446
186
|
if (value === undefined)
|
|
447
187
|
return undefined;
|
|
448
188
|
if (!isRecord(value)) {
|
|
449
189
|
problems.push({
|
|
450
190
|
path: "auth.identity",
|
|
451
|
-
message: "must be { url, label, id? }",
|
|
191
|
+
message: "must be { url, label, id?, scopes? }",
|
|
452
192
|
});
|
|
453
193
|
return undefined;
|
|
454
194
|
}
|
|
455
195
|
for (const field of Object.keys(value)) {
|
|
456
|
-
if (!["url", "label", "id"].includes(field)) {
|
|
196
|
+
if (!["url", "label", "id", "scopes"].includes(field)) {
|
|
457
197
|
problems.push({
|
|
458
198
|
path: `auth.identity.${field}`,
|
|
459
199
|
message: "unknown field",
|
|
@@ -489,30 +229,34 @@ function identity(value, hosts, problems) {
|
|
|
489
229
|
message: "must be a JSON field name, or a dotted path to a nested one",
|
|
490
230
|
});
|
|
491
231
|
}
|
|
232
|
+
const scopes = scopeList(value["scopes"], "auth.identity.scopes", problems);
|
|
492
233
|
if (!url || label === undefined)
|
|
493
234
|
return undefined;
|
|
494
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
identity: { url: url.url, label, ...(id === undefined ? {} : { id }) },
|
|
237
|
+
scopes: scopes ?? [],
|
|
238
|
+
};
|
|
495
239
|
}
|
|
496
240
|
/**
|
|
497
|
-
* Parse the OAuth half of a spec whose `auth.kind` is `oauth2-user
|
|
498
|
-
*
|
|
499
|
-
* the
|
|
500
|
-
* success the catalog-shaped capability with its `flow`.
|
|
241
|
+
* Parse the OAuth half of a spec whose `auth.kind` is `oauth2-user`, with
|
|
242
|
+
* the common fields already parsed. Every problem is reported; on success
|
|
243
|
+
* the capability with its `flow`.
|
|
501
244
|
*/
|
|
502
245
|
export function parseOAuthUserSpec(input, common, problems) {
|
|
503
|
-
const settingName_ = (value, path, into) => settingName(value, path, into, /^[A-Z][A-Z0-9_]{2,63}$/);
|
|
504
246
|
const auth = input["auth"];
|
|
505
247
|
for (const field of Object.keys(auth)) {
|
|
506
|
-
|
|
248
|
+
const renamed = RENAMED_AUTH_FIELDS[field];
|
|
249
|
+
if (renamed) {
|
|
250
|
+
problems.push({ path: `auth.${field}`, message: renamed });
|
|
251
|
+
}
|
|
252
|
+
else if (!OAUTH_AUTH_FIELDS.includes(field)) {
|
|
507
253
|
problems.push({
|
|
508
254
|
path: `auth.${field}`,
|
|
509
255
|
message: "not a field of auth.kind oauth2-user",
|
|
510
256
|
});
|
|
511
257
|
}
|
|
512
258
|
}
|
|
513
|
-
const authorize = endpoint(auth["authorizeUrl"], "auth.authorizeUrl", problems, {
|
|
514
|
-
query: true,
|
|
515
|
-
});
|
|
259
|
+
const authorize = endpoint(auth["authorizeUrl"], "auth.authorizeUrl", problems, { query: true });
|
|
516
260
|
if (auth["authorizeUrl"] === undefined) {
|
|
517
261
|
problems.push({ path: "auth.authorizeUrl", message: "required" });
|
|
518
262
|
}
|
|
@@ -535,15 +279,12 @@ export function parseOAuthUserSpec(input, common, problems) {
|
|
|
535
279
|
});
|
|
536
280
|
}
|
|
537
281
|
if (revokeMethod !== undefined && revoke === undefined) {
|
|
538
|
-
problems.push({
|
|
539
|
-
path: "auth.revokeMethod",
|
|
540
|
-
message: "needs a revokeUrl",
|
|
541
|
-
});
|
|
282
|
+
problems.push({ path: "auth.revokeMethod", message: "needs a revokeUrl" });
|
|
542
283
|
}
|
|
543
|
-
const clientId =
|
|
284
|
+
const clientId = settingName(auth["clientId"], "auth.clientId", problems);
|
|
544
285
|
const clientSecret = auth["clientSecret"] === undefined
|
|
545
286
|
? undefined
|
|
546
|
-
:
|
|
287
|
+
: settingName(auth["clientSecret"], "auth.clientSecret", problems);
|
|
547
288
|
if (clientSecret !== undefined && clientSecret === clientId) {
|
|
548
289
|
problems.push({
|
|
549
290
|
path: "auth.clientSecret",
|
|
@@ -561,29 +302,32 @@ export function parseOAuthUserSpec(input, common, problems) {
|
|
|
561
302
|
if (typeof pkce !== "boolean") {
|
|
562
303
|
problems.push({ path: "auth.pkce", message: "must be true or false" });
|
|
563
304
|
}
|
|
564
|
-
const
|
|
565
|
-
if (!
|
|
305
|
+
const separator = auth["scopeSeparator"] ?? "space";
|
|
306
|
+
if (!SEPARATORS.includes(separator)) {
|
|
566
307
|
problems.push({
|
|
567
|
-
path: "auth.
|
|
568
|
-
message: `must be one of ${
|
|
308
|
+
path: "auth.scopeSeparator",
|
|
309
|
+
message: `must be one of ${SEPARATORS.join(", ")}`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const prefix = endpoint(auth["scopePrefix"], "auth.scopePrefix", problems, {
|
|
313
|
+
query: false,
|
|
314
|
+
});
|
|
315
|
+
if (prefix && !prefix.url.endsWith("/")) {
|
|
316
|
+
problems.push({
|
|
317
|
+
path: "auth.scopePrefix",
|
|
318
|
+
message: "must end in / (https://www.googleapis.com/auth/): an app's scope is joined onto it",
|
|
569
319
|
});
|
|
570
320
|
}
|
|
571
321
|
const authorizeParams = stringMap(auth["authorizeParams"], "auth.authorizeParams", problems, (name, at) => {
|
|
572
322
|
if (!PARAM_NAME.test(name)) {
|
|
573
323
|
problems.push({ path: at, message: "must be a query parameter name" });
|
|
574
324
|
}
|
|
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)) {
|
|
325
|
+
if (RESERVED_AUTHORIZE_PARAMS.includes(name)) {
|
|
584
326
|
problems.push({
|
|
585
327
|
path: at,
|
|
586
|
-
message:
|
|
328
|
+
message: name === "include_granted_scopes"
|
|
329
|
+
? "include_granted_scopes would widen the token beyond the app's scopes; the Gateway never sends it"
|
|
330
|
+
: `${name} is a parameter the Gateway sets itself`,
|
|
587
331
|
});
|
|
588
332
|
}
|
|
589
333
|
});
|
|
@@ -600,78 +344,53 @@ export function parseOAuthUserSpec(input, common, problems) {
|
|
|
600
344
|
}
|
|
601
345
|
});
|
|
602
346
|
const parsedIdentity = identity(auth["identity"], common.hosts, problems);
|
|
603
|
-
const
|
|
604
|
-
const alwaysRequested = scopeList(input["alwaysRequested"], "alwaysRequested", problems);
|
|
605
|
-
const tolerated = scopeList(input["tolerated"], "tolerated", problems);
|
|
347
|
+
const tolerated = scopeList(auth["tolerated"], "auth.tolerated", problems);
|
|
606
348
|
if (!authorize ||
|
|
607
349
|
!token ||
|
|
608
350
|
clientId === undefined ||
|
|
609
|
-
!sets ||
|
|
610
351
|
typeof pkce !== "boolean" ||
|
|
611
|
-
!
|
|
352
|
+
!SEPARATORS.includes(separator) ||
|
|
612
353
|
!INJECTIONS.includes(inject)) {
|
|
613
354
|
return undefined;
|
|
614
355
|
}
|
|
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
356
|
const flow = {
|
|
357
|
+
authorizeUrl: authorize.url,
|
|
636
358
|
tokenUrl: token.url,
|
|
637
359
|
...(revoke === undefined ? {} : { revokeUrl: revoke.url }),
|
|
638
360
|
...(revokeMethod === undefined
|
|
639
361
|
? {}
|
|
640
362
|
: { revokeMethod: revokeMethod }),
|
|
363
|
+
inject: inject,
|
|
641
364
|
pkce,
|
|
642
|
-
|
|
365
|
+
scopeSeparator: separator,
|
|
366
|
+
...(prefix === undefined ? {} : { scopePrefix: prefix.url }),
|
|
643
367
|
...(authorizeParams === undefined ? {} : { authorizeParams }),
|
|
644
368
|
...(tokenHeaders === undefined ? {} : { tokenHeaders }),
|
|
645
|
-
...(parsedIdentity === undefined
|
|
369
|
+
...(parsedIdentity === undefined
|
|
370
|
+
? {}
|
|
371
|
+
: { identity: parsedIdentity.identity }),
|
|
372
|
+
identityScopes: parsedIdentity?.scopes ?? [],
|
|
373
|
+
tolerated: tolerated ?? [],
|
|
646
374
|
};
|
|
647
|
-
const
|
|
648
|
-
|
|
375
|
+
const capability = oauthCapabilityOf(common, {
|
|
376
|
+
clientId,
|
|
377
|
+
...(clientSecret === undefined ? {} : { clientSecret }),
|
|
378
|
+
}, flow);
|
|
379
|
+
const hosts = [...common.hosts];
|
|
380
|
+
for (const host of [authorize.host, token.host, revoke?.host]) {
|
|
381
|
+
if (host && !hosts.includes(host))
|
|
382
|
+
hosts.push(host);
|
|
383
|
+
}
|
|
384
|
+
return { capability, hosts };
|
|
385
|
+
}
|
|
386
|
+
function oauthCapabilityOf(common, secrets, flow) {
|
|
387
|
+
return {
|
|
649
388
|
mode: "oauth_client",
|
|
650
389
|
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"],
|
|
390
|
+
secrets,
|
|
664
391
|
console: common.docs,
|
|
665
|
-
authorize: { url: authorize.url, scopes: union },
|
|
666
|
-
scopePolicy: policy,
|
|
667
392
|
api: [...common.hosts],
|
|
668
393
|
flow,
|
|
669
|
-
...(common.readMethods === undefined
|
|
670
|
-
? {}
|
|
671
|
-
: { readMethods: common.readMethods }),
|
|
672
|
-
...(common.writeMethods === undefined
|
|
673
|
-
? {}
|
|
674
|
-
: { writeMethods: common.writeMethods }),
|
|
675
394
|
...(common.description === undefined
|
|
676
395
|
? {}
|
|
677
396
|
: { description: common.description }),
|
|
@@ -680,24 +399,78 @@ export function parseOAuthUserSpec(input, common, problems) {
|
|
|
680
399
|
...(common.example === undefined ? {} : { example: common.example }),
|
|
681
400
|
...(common.tags === undefined ? {} : { tags: common.tags }),
|
|
682
401
|
};
|
|
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
402
|
}
|
|
690
403
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
404
|
+
* A spec already known to be good (a built-in, checked by its test) as
|
|
405
|
+
* its capability, without the parse: what the catalog runs at load, where
|
|
406
|
+
* calling the full parser would be an import cycle.
|
|
407
|
+
*/
|
|
408
|
+
export function oauthCapabilityFromSpec(spec) {
|
|
409
|
+
const { auth } = spec;
|
|
410
|
+
const flow = {
|
|
411
|
+
authorizeUrl: auth.authorizeUrl,
|
|
412
|
+
tokenUrl: auth.tokenUrl,
|
|
413
|
+
...(auth.revokeUrl === undefined ? {} : { revokeUrl: auth.revokeUrl }),
|
|
414
|
+
...(auth.revokeMethod === undefined
|
|
415
|
+
? {}
|
|
416
|
+
: { revokeMethod: auth.revokeMethod }),
|
|
417
|
+
inject: auth.inject ?? "form",
|
|
418
|
+
pkce: auth.pkce ?? true,
|
|
419
|
+
scopeSeparator: auth.scopeSeparator ?? "space",
|
|
420
|
+
...(auth.scopePrefix === undefined
|
|
421
|
+
? {}
|
|
422
|
+
: { scopePrefix: auth.scopePrefix }),
|
|
423
|
+
...(auth.authorizeParams === undefined
|
|
424
|
+
? {}
|
|
425
|
+
: { authorizeParams: { ...auth.authorizeParams } }),
|
|
426
|
+
...(auth.tokenHeaders === undefined
|
|
427
|
+
? {}
|
|
428
|
+
: { tokenHeaders: { ...auth.tokenHeaders } }),
|
|
429
|
+
...(auth.identity === undefined
|
|
430
|
+
? {}
|
|
431
|
+
: {
|
|
432
|
+
identity: {
|
|
433
|
+
url: auth.identity.url,
|
|
434
|
+
label: auth.identity.label,
|
|
435
|
+
...(auth.identity.id === undefined ? {} : { id: auth.identity.id }),
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
identityScopes: [...(auth.identity?.scopes ?? [])],
|
|
439
|
+
tolerated: [...(auth.tolerated ?? [])],
|
|
440
|
+
};
|
|
441
|
+
return oauthCapabilityOf({
|
|
442
|
+
id: spec.id,
|
|
443
|
+
name: spec.name,
|
|
444
|
+
docs: spec.docs,
|
|
445
|
+
hosts: spec.hosts,
|
|
446
|
+
...(spec.description === undefined
|
|
447
|
+
? {}
|
|
448
|
+
: { description: spec.description }),
|
|
449
|
+
...(spec.api === undefined ? {} : { reference: spec.api }),
|
|
450
|
+
...(spec.hint === undefined ? {} : { hint: spec.hint }),
|
|
451
|
+
...(spec.example === undefined ? {} : { example: spec.example }),
|
|
452
|
+
...(spec.tags === undefined ? {} : { tags: spec.tags }),
|
|
453
|
+
}, {
|
|
454
|
+
clientId: auth.clientId,
|
|
455
|
+
...(auth.clientSecret === undefined
|
|
456
|
+
? {}
|
|
457
|
+
: { clientSecret: auth.clientSecret }),
|
|
458
|
+
}, flow);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The JSON spec of an OAuth capability (`provider show`, a preset's round
|
|
462
|
+
* trip): `parseProviderSpec` of it gives the capability back.
|
|
694
463
|
*/
|
|
695
464
|
export function oauthSpecOf(id, capability) {
|
|
696
|
-
const flow = capability
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
465
|
+
const { flow } = capability;
|
|
466
|
+
const identity = flow.identity === undefined
|
|
467
|
+
? undefined
|
|
468
|
+
: {
|
|
469
|
+
...flow.identity,
|
|
470
|
+
...(flow.identityScopes.length === 0
|
|
471
|
+
? {}
|
|
472
|
+
: { scopes: [...flow.identityScopes] }),
|
|
473
|
+
};
|
|
701
474
|
return {
|
|
702
475
|
id,
|
|
703
476
|
name: capability.label,
|
|
@@ -711,7 +484,7 @@ export function oauthSpecOf(id, capability) {
|
|
|
711
484
|
hosts: [...capability.api],
|
|
712
485
|
auth: {
|
|
713
486
|
kind: "oauth2-user",
|
|
714
|
-
authorizeUrl:
|
|
487
|
+
authorizeUrl: flow.authorizeUrl,
|
|
715
488
|
tokenUrl: flow.tokenUrl,
|
|
716
489
|
...(flow.revokeUrl === undefined ? {} : { revokeUrl: flow.revokeUrl }),
|
|
717
490
|
...(flow.revokeMethod === undefined
|
|
@@ -721,57 +494,23 @@ export function oauthSpecOf(id, capability) {
|
|
|
721
494
|
...(capability.secrets.clientSecret === undefined
|
|
722
495
|
? {}
|
|
723
496
|
: { clientSecret: capability.secrets.clientSecret }),
|
|
724
|
-
inject,
|
|
497
|
+
inject: flow.inject,
|
|
725
498
|
pkce: flow.pkce,
|
|
726
|
-
|
|
499
|
+
scopeSeparator: flow.scopeSeparator,
|
|
500
|
+
...(flow.scopePrefix === undefined
|
|
501
|
+
? {}
|
|
502
|
+
: { scopePrefix: flow.scopePrefix }),
|
|
727
503
|
...(flow.authorizeParams === undefined
|
|
728
504
|
? {}
|
|
729
505
|
: { authorizeParams: { ...flow.authorizeParams } }),
|
|
730
506
|
...(flow.tokenHeaders === undefined
|
|
731
507
|
? {}
|
|
732
508
|
: { tokenHeaders: { ...flow.tokenHeaders } }),
|
|
733
|
-
...(
|
|
509
|
+
...(identity === undefined ? {} : { identity }),
|
|
510
|
+
...(flow.tolerated.length === 0
|
|
734
511
|
? {}
|
|
735
|
-
: {
|
|
512
|
+
: { tolerated: [...flow.tolerated] }),
|
|
736
513
|
},
|
|
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
514
|
...(capability.hint === undefined ? {} : { hint: capability.hint }),
|
|
776
515
|
...(capability.example === undefined
|
|
777
516
|
? {}
|
|
@@ -779,11 +518,3 @@ export function oauthSpecOf(id, capability) {
|
|
|
779
518
|
...(capability.tags === undefined ? {} : { tags: [...capability.tags] }),
|
|
780
519
|
};
|
|
781
520
|
}
|
|
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
|
-
}
|