@every-app/sdk 0.3.0-rc.1 → 0.3.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/cloudflare/catalogSettings.generated.d.ts +81 -0
- package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
- package/dist/cloudflare/catalogSettings.generated.js +174 -0
- package/dist/cloudflare/index.d.ts +3 -2
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/index.js +4 -1
- package/dist/cloudflare/sentinel.d.ts +60 -2
- package/dist/cloudflare/sentinel.d.ts.map +1 -1
- package/dist/cloudflare/sentinel.js +37 -2
- package/dist/cloudflare/worker.d.ts +357 -63
- package/dist/cloudflare/worker.d.ts.map +1 -1
- package/dist/cloudflare/worker.js +363 -104
- package/dist/dev/index.d.ts +1 -1
- package/dist/dev/index.d.ts.map +1 -1
- package/dist/dev/index.js +1 -1
- package/dist/dev/keypair.d.ts +2 -0
- package/dist/dev/keypair.d.ts.map +1 -1
- package/dist/dev/keypair.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/internal/aiDefault.d.ts +204 -0
- package/dist/internal/aiDefault.d.ts.map +1 -0
- package/dist/internal/aiDefault.js +334 -0
- package/dist/internal/aiProviders.d.ts +7 -2
- package/dist/internal/aiProviders.d.ts.map +1 -1
- package/dist/internal/aiProviders.js +17 -4
- package/dist/internal/appSettings.d.ts +82 -4
- package/dist/internal/appSettings.d.ts.map +1 -1
- package/dist/internal/appSettings.js +204 -29
- package/dist/internal/audit.d.ts +108 -0
- package/dist/internal/audit.d.ts.map +1 -0
- package/dist/internal/audit.js +235 -0
- package/dist/internal/capabilities.d.ts +2268 -438
- package/dist/internal/capabilities.d.ts.map +1 -1
- package/dist/internal/capabilities.js +128 -539
- package/dist/internal/catalog/commerce.d.ts +394 -0
- package/dist/internal/catalog/commerce.d.ts.map +1 -0
- package/dist/internal/catalog/commerce.js +477 -0
- package/dist/internal/catalog/comms.d.ts +375 -0
- package/dist/internal/catalog/comms.d.ts.map +1 -0
- package/dist/internal/catalog/comms.js +520 -0
- package/dist/internal/catalog/index.d.ts +1973 -0
- package/dist/internal/catalog/index.d.ts.map +1 -0
- package/dist/internal/catalog/index.js +19 -0
- package/dist/internal/catalog/inference.d.ts +393 -0
- package/dist/internal/catalog/inference.d.ts.map +1 -0
- package/dist/internal/catalog/inference.js +460 -0
- package/dist/internal/catalog/infra.d.ts +423 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -0
- package/dist/internal/catalog/infra.js +543 -0
- package/dist/internal/catalog/search.d.ts +400 -0
- package/dist/internal/catalog/search.d.ts.map +1 -0
- package/dist/internal/catalog/search.js +463 -0
- package/dist/internal/devProviders.d.ts +2 -2
- package/dist/internal/devProviders.d.ts.map +1 -1
- package/dist/internal/devProviders.js +19 -1
- package/dist/internal/events.d.ts +13 -2
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +47 -4
- package/dist/internal/gatewaySettings.d.ts +3 -2
- package/dist/internal/gatewaySettings.d.ts.map +1 -1
- package/dist/internal/gatewaySettings.js +12 -6
- package/dist/internal/identity.d.ts +41 -4
- package/dist/internal/identity.d.ts.map +1 -1
- package/dist/internal/identity.js +48 -10
- package/dist/internal/index.d.ts +6 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +6 -1
- package/dist/internal/members.d.ts +3 -2
- package/dist/internal/members.d.ts.map +1 -1
- package/dist/internal/oauthClient.d.ts +10 -12
- package/dist/internal/oauthClient.d.ts.map +1 -1
- package/dist/internal/oauthClient.js +20 -81
- package/dist/internal/oauthPresets.d.ts +33 -0
- package/dist/internal/oauthPresets.d.ts.map +1 -0
- package/dist/internal/oauthPresets.js +1064 -0
- package/dist/internal/oauthScopes.d.ts +56 -1
- package/dist/internal/oauthScopes.d.ts.map +1 -1
- package/dist/internal/oauthScopes.js +98 -4
- package/dist/internal/oauthSpec.d.ts +102 -0
- package/dist/internal/oauthSpec.d.ts.map +1 -0
- package/dist/internal/oauthSpec.js +789 -0
- package/dist/internal/protocol.d.ts +64 -1
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +51 -1
- package/dist/internal/providerRegistry.d.ts +77 -6
- package/dist/internal/providerRegistry.d.ts.map +1 -1
- package/dist/internal/providerRegistry.js +178 -8
- package/dist/internal/providerSpec.d.ts +18 -7
- package/dist/internal/providerSpec.d.ts.map +1 -1
- package/dist/internal/providerSpec.js +442 -34
- package/dist/internal/signInProviders.d.ts +104 -0
- package/dist/internal/signInProviders.d.ts.map +1 -0
- package/dist/internal/signInProviders.js +245 -0
- package/dist/manifest.d.ts +238 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +30 -0
- package/dist/server/ai.d.ts +111 -0
- package/dist/server/ai.d.ts.map +1 -0
- package/dist/server/ai.js +145 -0
- package/dist/server/aiBinding.d.ts +68 -0
- package/dist/server/aiBinding.d.ts.map +1 -0
- package/dist/server/aiBinding.js +294 -0
- package/dist/server/attribution.d.ts +9 -5
- package/dist/server/attribution.d.ts.map +1 -1
- package/dist/server/attribution.js +12 -4
- package/dist/server/authorizeUrl.d.ts +1 -1
- package/dist/server/authorizeUrl.d.ts.map +1 -1
- package/dist/server/authorizeUrl.js +2 -6
- package/dist/server/connectUrl.d.ts +21 -0
- package/dist/server/connectUrl.d.ts.map +1 -0
- package/dist/server/connectUrl.js +37 -0
- package/dist/server/context.d.ts +68 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +117 -0
- package/dist/server/emailBinding.d.ts +72 -0
- package/dist/server/emailBinding.d.ts.map +1 -0
- package/dist/server/emailBinding.js +124 -0
- package/dist/server/emit.d.ts +1 -1
- package/dist/server/emit.d.ts.map +1 -1
- package/dist/server/emit.js +7 -7
- package/dist/server/env.d.ts +44 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +1 -0
- package/dist/server/everyApp.d.ts +27 -7
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +106 -5
- package/dist/server/fromMcpTools.d.ts +89 -0
- package/dist/server/fromMcpTools.d.ts.map +1 -0
- package/dist/server/fromMcpTools.js +192 -0
- package/dist/server/gatewayFetch.d.ts +42 -18
- package/dist/server/gatewayFetch.d.ts.map +1 -1
- package/dist/server/gatewayFetch.js +138 -19
- package/dist/server/gatewayUrl.d.ts +13 -0
- package/dist/server/gatewayUrl.d.ts.map +1 -0
- package/dist/server/gatewayUrl.js +34 -0
- package/dist/server/index.d.ts +15 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +12 -2
- package/dist/server/integrationFetch.d.ts +70 -39
- package/dist/server/integrationFetch.d.ts.map +1 -1
- package/dist/server/integrationFetch.js +244 -82
- package/dist/server/listMembers.d.ts +1 -1
- package/dist/server/listMembers.d.ts.map +1 -1
- package/dist/server/listMembers.js +8 -11
- package/dist/server/refusal.d.ts +55 -0
- package/dist/server/refusal.d.ts.map +1 -0
- package/dist/server/refusal.js +193 -0
- package/dist/server/sendEmail.d.ts +1 -1
- package/dist/server/sendEmail.d.ts.map +1 -1
- package/dist/server/sendEmail.js +7 -7
- package/dist/server/toolsEndpoint.d.ts.map +1 -1
- package/dist/server/toolsEndpoint.js +11 -5
- package/dist/tanstack-start/index.d.ts +9 -0
- package/dist/tanstack-start/index.d.ts.map +1 -0
- package/dist/tanstack-start/index.js +29 -0
- package/dist/testing/index.d.ts +93 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +156 -0
- package/package.json +35 -14
|
@@ -0,0 +1,789 @@
|
|
|
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
|
+
/** 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
|
+
function identity(value, hosts, problems) {
|
|
446
|
+
if (value === undefined)
|
|
447
|
+
return undefined;
|
|
448
|
+
if (!isRecord(value)) {
|
|
449
|
+
problems.push({
|
|
450
|
+
path: "auth.identity",
|
|
451
|
+
message: "must be { url, label, id? }",
|
|
452
|
+
});
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
for (const field of Object.keys(value)) {
|
|
456
|
+
if (!["url", "label", "id"].includes(field)) {
|
|
457
|
+
problems.push({
|
|
458
|
+
path: `auth.identity.${field}`,
|
|
459
|
+
message: "unknown field",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const url = endpoint(value["url"], "auth.identity.url", problems, {
|
|
464
|
+
query: true,
|
|
465
|
+
});
|
|
466
|
+
if (value["url"] === undefined) {
|
|
467
|
+
problems.push({ path: "auth.identity.url", message: "required" });
|
|
468
|
+
}
|
|
469
|
+
else if (url && !hosts.includes(url.host)) {
|
|
470
|
+
problems.push({
|
|
471
|
+
path: "auth.identity.url",
|
|
472
|
+
message: "must be on one of hosts (the token is sent there)",
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const label = printable(value["label"], "auth.identity.label", problems, 128);
|
|
476
|
+
if (label !== undefined && !DOT_PATH.test(label)) {
|
|
477
|
+
problems.push({
|
|
478
|
+
path: "auth.identity.label",
|
|
479
|
+
message: "must be a JSON field name, or a dotted path to a nested one",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (value["label"] === undefined) {
|
|
483
|
+
problems.push({ path: "auth.identity.label", message: "required" });
|
|
484
|
+
}
|
|
485
|
+
const id = printable(value["id"], "auth.identity.id", problems, 128);
|
|
486
|
+
if (id !== undefined && !DOT_PATH.test(id)) {
|
|
487
|
+
problems.push({
|
|
488
|
+
path: "auth.identity.id",
|
|
489
|
+
message: "must be a JSON field name, or a dotted path to a nested one",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (!url || label === undefined)
|
|
493
|
+
return undefined;
|
|
494
|
+
return { url: url.url, label, ...(id === undefined ? {} : { id }) };
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
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`.
|
|
501
|
+
*/
|
|
502
|
+
export function parseOAuthUserSpec(input, common, problems) {
|
|
503
|
+
const settingName_ = (value, path, into) => settingName(value, path, into, /^[A-Z][A-Z0-9_]{2,63}$/);
|
|
504
|
+
const auth = input["auth"];
|
|
505
|
+
for (const field of Object.keys(auth)) {
|
|
506
|
+
if (!OAUTH_AUTH_FIELDS.includes(field)) {
|
|
507
|
+
problems.push({
|
|
508
|
+
path: `auth.${field}`,
|
|
509
|
+
message: "not a field of auth.kind oauth2-user",
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const authorize = endpoint(auth["authorizeUrl"], "auth.authorizeUrl", problems, {
|
|
514
|
+
query: true,
|
|
515
|
+
});
|
|
516
|
+
if (auth["authorizeUrl"] === undefined) {
|
|
517
|
+
problems.push({ path: "auth.authorizeUrl", message: "required" });
|
|
518
|
+
}
|
|
519
|
+
const token = endpoint(auth["tokenUrl"], "auth.tokenUrl", problems, {
|
|
520
|
+
query: false,
|
|
521
|
+
});
|
|
522
|
+
if (auth["tokenUrl"] === undefined) {
|
|
523
|
+
problems.push({ path: "auth.tokenUrl", message: "required" });
|
|
524
|
+
}
|
|
525
|
+
const revoke = endpoint(auth["revokeUrl"], "auth.revokeUrl", problems, {
|
|
526
|
+
query: false,
|
|
527
|
+
});
|
|
528
|
+
const revokeMethod = auth["revokeMethod"];
|
|
529
|
+
if (revokeMethod !== undefined &&
|
|
530
|
+
revokeMethod !== "POST" &&
|
|
531
|
+
revokeMethod !== "DELETE") {
|
|
532
|
+
problems.push({
|
|
533
|
+
path: "auth.revokeMethod",
|
|
534
|
+
message: "must be POST or DELETE",
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
if (revokeMethod !== undefined && revoke === undefined) {
|
|
538
|
+
problems.push({
|
|
539
|
+
path: "auth.revokeMethod",
|
|
540
|
+
message: "needs a revokeUrl",
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
const clientId = settingName_(auth["clientId"], "auth.clientId", problems);
|
|
544
|
+
const clientSecret = auth["clientSecret"] === undefined
|
|
545
|
+
? undefined
|
|
546
|
+
: settingName_(auth["clientSecret"], "auth.clientSecret", problems);
|
|
547
|
+
if (clientSecret !== undefined && clientSecret === clientId) {
|
|
548
|
+
problems.push({
|
|
549
|
+
path: "auth.clientSecret",
|
|
550
|
+
message: "must be another setting than auth.clientId",
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const inject = auth["inject"] ?? "form";
|
|
554
|
+
if (!INJECTIONS.includes(inject)) {
|
|
555
|
+
problems.push({
|
|
556
|
+
path: "auth.inject",
|
|
557
|
+
message: "must be form (the client in the body) or basic (HTTP Basic)",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
const pkce = auth["pkce"] ?? true;
|
|
561
|
+
if (typeof pkce !== "boolean") {
|
|
562
|
+
problems.push({ path: "auth.pkce", message: "must be true or false" });
|
|
563
|
+
}
|
|
564
|
+
const dialect = auth["scopeDialect"] ?? "space";
|
|
565
|
+
if (!DIALECTS.includes(dialect)) {
|
|
566
|
+
problems.push({
|
|
567
|
+
path: "auth.scopeDialect",
|
|
568
|
+
message: `must be one of ${DIALECTS.join(", ")}`,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
const authorizeParams = stringMap(auth["authorizeParams"], "auth.authorizeParams", problems, (name, at) => {
|
|
572
|
+
if (!PARAM_NAME.test(name)) {
|
|
573
|
+
problems.push({ path: at, message: "must be a query parameter name" });
|
|
574
|
+
}
|
|
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)) {
|
|
584
|
+
problems.push({
|
|
585
|
+
path: at,
|
|
586
|
+
message: `${name} is a parameter the Gateway sets itself`,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
const tokenHeaders = stringMap(auth["tokenHeaders"], "auth.tokenHeaders", problems, (name, at) => {
|
|
591
|
+
if (!HEADER_NAME.test(name)) {
|
|
592
|
+
problems.push({ path: at, message: "must be a header name" });
|
|
593
|
+
}
|
|
594
|
+
else if (RESERVED_TOKEN_HEADERS.has(name.toLowerCase()) ||
|
|
595
|
+
name.toLowerCase().startsWith("x-everyapp-")) {
|
|
596
|
+
problems.push({
|
|
597
|
+
path: at,
|
|
598
|
+
message: `${name} is a header the Gateway sets itself`,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
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);
|
|
606
|
+
if (!authorize ||
|
|
607
|
+
!token ||
|
|
608
|
+
clientId === undefined ||
|
|
609
|
+
!sets ||
|
|
610
|
+
typeof pkce !== "boolean" ||
|
|
611
|
+
!DIALECTS.includes(dialect) ||
|
|
612
|
+
!INJECTIONS.includes(inject)) {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
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
|
+
const flow = {
|
|
636
|
+
tokenUrl: token.url,
|
|
637
|
+
...(revoke === undefined ? {} : { revokeUrl: revoke.url }),
|
|
638
|
+
...(revokeMethod === undefined
|
|
639
|
+
? {}
|
|
640
|
+
: { revokeMethod: revokeMethod }),
|
|
641
|
+
pkce,
|
|
642
|
+
scopeDialect: dialect,
|
|
643
|
+
...(authorizeParams === undefined ? {} : { authorizeParams }),
|
|
644
|
+
...(tokenHeaders === undefined ? {} : { tokenHeaders }),
|
|
645
|
+
...(parsedIdentity === undefined ? {} : { identity: parsedIdentity }),
|
|
646
|
+
};
|
|
647
|
+
const tokenPath = new URL(token.url).pathname;
|
|
648
|
+
const capability = {
|
|
649
|
+
mode: "oauth_client",
|
|
650
|
+
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"],
|
|
664
|
+
console: common.docs,
|
|
665
|
+
authorize: { url: authorize.url, scopes: union },
|
|
666
|
+
scopePolicy: policy,
|
|
667
|
+
api: [...common.hosts],
|
|
668
|
+
flow,
|
|
669
|
+
...(common.readMethods === undefined
|
|
670
|
+
? {}
|
|
671
|
+
: { readMethods: common.readMethods }),
|
|
672
|
+
...(common.writeMethods === undefined
|
|
673
|
+
? {}
|
|
674
|
+
: { writeMethods: common.writeMethods }),
|
|
675
|
+
...(common.description === undefined
|
|
676
|
+
? {}
|
|
677
|
+
: { description: common.description }),
|
|
678
|
+
...(common.reference === undefined ? {} : { reference: common.reference }),
|
|
679
|
+
...(common.hint === undefined ? {} : { hint: common.hint }),
|
|
680
|
+
...(common.example === undefined ? {} : { example: common.example }),
|
|
681
|
+
...(common.tags === undefined ? {} : { tags: common.tags }),
|
|
682
|
+
};
|
|
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
|
+
}
|
|
690
|
+
/**
|
|
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.
|
|
694
|
+
*/
|
|
695
|
+
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";
|
|
701
|
+
return {
|
|
702
|
+
id,
|
|
703
|
+
name: capability.label,
|
|
704
|
+
...(capability.description === undefined
|
|
705
|
+
? {}
|
|
706
|
+
: { description: capability.description }),
|
|
707
|
+
docs: capability.console,
|
|
708
|
+
...(capability.reference === undefined
|
|
709
|
+
? {}
|
|
710
|
+
: { api: capability.reference }),
|
|
711
|
+
hosts: [...capability.api],
|
|
712
|
+
auth: {
|
|
713
|
+
kind: "oauth2-user",
|
|
714
|
+
authorizeUrl: capability.authorize.url,
|
|
715
|
+
tokenUrl: flow.tokenUrl,
|
|
716
|
+
...(flow.revokeUrl === undefined ? {} : { revokeUrl: flow.revokeUrl }),
|
|
717
|
+
...(flow.revokeMethod === undefined
|
|
718
|
+
? {}
|
|
719
|
+
: { revokeMethod: flow.revokeMethod }),
|
|
720
|
+
clientId: capability.secrets.clientId,
|
|
721
|
+
...(capability.secrets.clientSecret === undefined
|
|
722
|
+
? {}
|
|
723
|
+
: { clientSecret: capability.secrets.clientSecret }),
|
|
724
|
+
inject,
|
|
725
|
+
pkce: flow.pkce,
|
|
726
|
+
scopeDialect: flow.scopeDialect,
|
|
727
|
+
...(flow.authorizeParams === undefined
|
|
728
|
+
? {}
|
|
729
|
+
: { authorizeParams: { ...flow.authorizeParams } }),
|
|
730
|
+
...(flow.tokenHeaders === undefined
|
|
731
|
+
? {}
|
|
732
|
+
: { tokenHeaders: { ...flow.tokenHeaders } }),
|
|
733
|
+
...(flow.identity === undefined
|
|
734
|
+
? {}
|
|
735
|
+
: { identity: { ...flow.identity } }),
|
|
736
|
+
},
|
|
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
|
+
...(capability.hint === undefined ? {} : { hint: capability.hint }),
|
|
776
|
+
...(capability.example === undefined
|
|
777
|
+
? {}
|
|
778
|
+
: { example: capability.example }),
|
|
779
|
+
...(capability.tags === undefined ? {} : { tags: [...capability.tags] }),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
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
|
+
}
|