@every-app/sdk 0.3.0-rc.1 → 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/LICENSE +21 -0
- package/README.md +62 -64
- package/dist/cloudflare/catalogSettings.generated.d.ts +84 -0
- package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
- package/dist/cloudflare/catalogSettings.generated.js +175 -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 +391 -63
- package/dist/cloudflare/worker.d.ts.map +1 -1
- package/dist/cloudflare/worker.js +394 -108
- 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 +205 -0
- package/dist/internal/aiDefault.d.ts.map +1 -0
- package/dist/internal/aiDefault.js +335 -0
- package/dist/internal/aiProviders.d.ts +7 -2
- package/dist/internal/aiProviders.d.ts.map +1 -1
- package/dist/internal/aiProviders.js +19 -6
- 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 +2261 -815
- package/dist/internal/capabilities.d.ts.map +1 -1
- package/dist/internal/capabilities.js +122 -1034
- 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 +2008 -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 +458 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -0
- package/dist/internal/catalog/infra.js +608 -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 +65 -3
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +110 -5
- 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/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 +8 -50
- package/dist/internal/oauthClient.d.ts.map +1 -1
- package/dist/internal/oauthClient.js +2 -177
- package/dist/internal/oauthPresets.d.ts +32 -0
- package/dist/internal/oauthPresets.d.ts.map +1 -0
- package/dist/internal/oauthPresets.js +253 -0
- package/dist/internal/oauthScopes.d.ts +43 -100
- package/dist/internal/oauthScopes.d.ts.map +1 -1
- package/dist/internal/oauthScopes.js +83 -382
- package/dist/internal/oauthSpec.d.ts +103 -0
- package/dist/internal/oauthSpec.d.ts.map +1 -0
- package/dist/internal/oauthSpec.js +520 -0
- package/dist/internal/protocol.d.ts +97 -5
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +89 -1
- package/dist/internal/providerRegistry.d.ts +17 -6
- package/dist/internal/providerRegistry.d.ts.map +1 -1
- package/dist/internal/providerRegistry.js +71 -8
- package/dist/internal/providerSpec.d.ts +18 -7
- package/dist/internal/providerSpec.d.ts.map +1 -1
- package/dist/internal/providerSpec.js +420 -34
- package/dist/internal/remoteMcp.d.ts.map +1 -1
- package/dist/internal/remoteMcp.js +1 -5
- 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/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 +267 -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 +18 -5
- package/dist/server/attribution.d.ts.map +1 -1
- package/dist/server/attribution.js +19 -5
- package/dist/server/connectUrl.d.ts +16 -0
- package/dist/server/connectUrl.d.ts.map +1 -0
- package/dist/server/connectUrl.js +40 -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/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 +40 -11
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +116 -6
- 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 +16 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +13 -3
- package/dist/server/integrationFetch.d.ts +92 -73
- package/dist/server/integrationFetch.d.ts.map +1 -1
- package/dist/server/integrationFetch.js +206 -319
- 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/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 +19 -7
- 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/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
- package/dist/server/authorizeUrl.d.ts +0 -43
- package/dist/server/authorizeUrl.d.ts.map +0 -1
- package/dist/server/authorizeUrl.js +0 -110
|
@@ -1,416 +1,117 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* a newer CLI's set name reaching an older gateway is refused outright and
|
|
7
|
-
* a message can say so.
|
|
2
|
+
* A scope as the provider is asked for it: an app's short scope joined
|
|
3
|
+
* onto the spec's prefix; a full one (with `://`), one of the spec's own
|
|
4
|
+
* identity or tolerated scopes, or any scope of a provider without a
|
|
5
|
+
* prefix, as written.
|
|
8
6
|
*/
|
|
9
|
-
export function
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
12
|
-
return
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
export function expandScope(rules, scope) {
|
|
8
|
+
const prefix = rules.scopePrefix;
|
|
9
|
+
if (!prefix || scope.includes("://"))
|
|
10
|
+
return scope;
|
|
11
|
+
const own = [...rules.identityScopes, ...rules.tolerated];
|
|
12
|
+
return own.includes(scope) ? scope : `${prefix}${scope}`;
|
|
13
|
+
}
|
|
14
|
+
/** A scope in the one spelling comparisons use: decoded, lower case, without the prefix. */
|
|
15
|
+
export function canonicalScope(rules, scope) {
|
|
16
|
+
let text = scope.trim();
|
|
17
|
+
if (text.includes("%")) {
|
|
18
|
+
try {
|
|
19
|
+
text = decodeURIComponent(text);
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return names;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* What an app with `sets` asks the provider for: the sets' scopes in
|
|
26
|
-
* registry order, deduplicated, then `alwaysRequested`. The status hands
|
|
27
|
-
* these out and the authorize URL (Microsoft: the token leg too) carries
|
|
28
|
-
* them verbatim.
|
|
29
|
-
*/
|
|
30
|
-
export function requestedScopes(policy, sets) {
|
|
31
|
-
const scopes = [];
|
|
32
|
-
const userScopes = [];
|
|
33
|
-
for (const [, set] of declaredSets(policy, sets)) {
|
|
34
|
-
pushNew(scopes, set.scopes);
|
|
35
|
-
pushNew(userScopes, set.userScopes ?? []);
|
|
36
|
-
}
|
|
37
|
-
pushNew(scopes, policy.alwaysRequested);
|
|
38
|
-
return { scopes, userScopes };
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* What a token an app with `sets` is handed may carry: the requested
|
|
42
|
-
* scopes and the tolerated ones, in the dialect's canonical spelling so a
|
|
43
|
-
* granted list canonicalised the same way compares exactly.
|
|
44
|
-
*/
|
|
45
|
-
export function allowedScopes(policy, sets) {
|
|
46
|
-
const requested = requestedScopes(policy, sets);
|
|
47
|
-
return {
|
|
48
|
-
scopes: canonical(policy.dialect, [
|
|
49
|
-
...requested.scopes,
|
|
50
|
-
...policy.tolerated,
|
|
51
|
-
]),
|
|
52
|
-
userScopes: canonical(policy.dialect, requested.userScopes),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Whether a token response body carries a token: an access, refresh or ID
|
|
57
|
-
* token, or Slack's user token under `authed_user`. A 2xx without one
|
|
58
|
-
* (Slack's `ok: false`) is an error the app must see, not a grant.
|
|
59
|
-
*/
|
|
60
|
-
export function carriesToken(json) {
|
|
61
|
-
const body = record(json);
|
|
62
|
-
if (!body)
|
|
63
|
-
return false;
|
|
64
|
-
return (typeof body["access_token"] === "string" ||
|
|
65
|
-
typeof body["refresh_token"] === "string" ||
|
|
66
|
-
typeof body["id_token"] === "string" ||
|
|
67
|
-
typeof record(body["authed_user"])?.["access_token"] === "string");
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* The scopes a token response says it granted, canonicalised per dialect,
|
|
71
|
-
* or null when the answer names none for a token it carries, which fails
|
|
72
|
-
* closed: every documented success response of the three providers names
|
|
73
|
-
* them (map-providers: Google and Microsoft `scope`, Slack `scope` and
|
|
74
|
-
* `authed_user.scope`), so a missing field is an answer the gateway can't
|
|
75
|
-
* hold to anything.
|
|
76
|
-
*/
|
|
77
|
-
export function grantedScopes(policy, json) {
|
|
78
|
-
const body = record(json);
|
|
79
|
-
if (!body)
|
|
80
|
-
return null;
|
|
81
|
-
switch (policy.dialect) {
|
|
82
|
-
case "google": {
|
|
83
|
-
// Space-delimited, case-sensitive strings (Google's own words).
|
|
84
|
-
const scopes = splitScopes("google", body["scope"]);
|
|
85
|
-
return scopes ? { scopes } : null;
|
|
86
|
-
}
|
|
87
|
-
case "microsoft": {
|
|
88
|
-
// Documented URL-encoded with the Graph prefix
|
|
89
|
-
// (`https%3A%2F%2Fgraph.microsoft.com%2Fmail.read`), seen as short
|
|
90
|
-
// names in either case; `offline_access` is never echoed, and dropped
|
|
91
|
-
// if it ever is.
|
|
92
|
-
if (typeof body["scope"] !== "string")
|
|
93
|
-
return null;
|
|
94
|
-
let decoded;
|
|
95
|
-
try {
|
|
96
|
-
decoded = decodeURIComponent(body["scope"]);
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
const scopes = splitScopes("microsoft", decoded)?.filter((scope) => scope !== "offline_access");
|
|
102
|
-
return scopes && scopes.length > 0 ? { scopes } : null;
|
|
21
|
+
catch {
|
|
22
|
+
// A malformed escape stays as written, and matches nothing asked.
|
|
103
23
|
}
|
|
104
|
-
case "slack":
|
|
105
|
-
return slackGrantedScopes(body);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Slack answers with up to two tokens, each with its own scopes: the bot
|
|
110
|
-
* token at the top level and the installing person's under `authed_user`.
|
|
111
|
-
* Which kind the top-level one is comes from `token_type`, because a
|
|
112
|
-
* token-rotation refresh of a user token comes back at the top level with
|
|
113
|
-
* `token_type: "user"`; `authed_user` is a user token unless it says so.
|
|
114
|
-
*/
|
|
115
|
-
function slackGrantedScopes(body) {
|
|
116
|
-
const granted = {};
|
|
117
|
-
const take = (token, kind) => {
|
|
118
|
-
if (kind !== "bot" && kind !== "user")
|
|
119
|
-
return false;
|
|
120
|
-
const scopes = splitScopes("slack", token["scope"]);
|
|
121
|
-
if (!scopes)
|
|
122
|
-
return false;
|
|
123
|
-
const key = kind === "bot" ? "scopes" : "userScopes";
|
|
124
|
-
granted[key] = pushNew(granted[key] ?? [], scopes);
|
|
125
|
-
return true;
|
|
126
|
-
};
|
|
127
|
-
if (typeof body["access_token"] === "string") {
|
|
128
|
-
if (!take(body, body["token_type"]))
|
|
129
|
-
return null;
|
|
130
24
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
25
|
+
text = text.toLowerCase();
|
|
26
|
+
const prefix = rules.scopePrefix?.toLowerCase();
|
|
27
|
+
return prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text;
|
|
28
|
+
}
|
|
29
|
+
/** `scopes` canonical, deduplicated, order kept. */
|
|
30
|
+
export function canonicalScopes(rules, scopes) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const scope of scopes) {
|
|
33
|
+
const canonical = canonicalScope(rules, scope);
|
|
34
|
+
if (canonical && !out.includes(canonical))
|
|
35
|
+
out.push(canonical);
|
|
135
36
|
}
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* What `granted` carries beyond `allowed`, per token kind, for a refusal's
|
|
140
|
-
* message and log line: the user token's extras as `user:<scope>`. Both
|
|
141
|
-
* lists are in the dialect's canonical spelling already.
|
|
142
|
-
*/
|
|
143
|
-
export function scopeExcess(granted, allowed) {
|
|
144
|
-
const extra = (scopes, within) => (scopes ?? []).filter((scope) => !within.includes(scope));
|
|
145
|
-
return [
|
|
146
|
-
...extra(granted.scopes, allowed.scopes),
|
|
147
|
-
...extra(granted.userScopes, allowed.userScopes).map((scope) => `user:${scope}`),
|
|
148
|
-
];
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* The scopes a token request itself asks for (`scope`, and Slack's
|
|
152
|
-
* `user_scope`, in any case) in its form or JSON body, canonicalised per
|
|
153
|
-
* dialect for comparison with `allowedScopes()`, or null when the body
|
|
154
|
-
* names none. The query string is never read: the gateway refuses a
|
|
155
|
-
* request that puts these there, so only one place decides.
|
|
156
|
-
*/
|
|
157
|
-
export function requestedTokenScopes(policy, request) {
|
|
158
|
-
const fields = bodyFields(request);
|
|
159
|
-
if (!fields)
|
|
160
|
-
return null;
|
|
161
|
-
const requested = {};
|
|
162
|
-
const scopes = splitScopes(policy.dialect, fields["scope"]);
|
|
163
|
-
if (scopes)
|
|
164
|
-
requested.scopes = scopes;
|
|
165
|
-
const userScopes = splitScopes(policy.dialect, fields["user_scope"]);
|
|
166
|
-
if (userScopes)
|
|
167
|
-
requested.userScopes = userScopes;
|
|
168
|
-
return requested.scopes || requested.userScopes ? requested : null;
|
|
37
|
+
return out;
|
|
169
38
|
}
|
|
170
39
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* re-serialised form or JSON body this edits the same way; any other body
|
|
175
|
-
* is left alone.
|
|
40
|
+
* What a connect for an app that declares `declared` asks the provider
|
|
41
|
+
* for, in the provider's spelling: the app's scopes, then the spec's
|
|
42
|
+
* identity scopes, each once.
|
|
176
43
|
*/
|
|
177
|
-
export function
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Entra reads a form's names in any case, so a `Scope` left beside the
|
|
186
|
-
// gateway's `scope` is refused upstream as a duplicate, or read instead.
|
|
187
|
-
const isScope = (name) => name.toLowerCase() === "scope";
|
|
188
|
-
let body = request.body;
|
|
189
|
-
if (type === FORM) {
|
|
190
|
-
const params = new URLSearchParams(body);
|
|
191
|
-
for (const name of new Set([...params.keys()].filter(isScope))) {
|
|
192
|
-
params.delete(name);
|
|
193
|
-
}
|
|
194
|
-
if (replacement !== null)
|
|
195
|
-
params.set("scope", replacement);
|
|
196
|
-
body = params.toString();
|
|
197
|
-
}
|
|
198
|
-
else if (type === JSON_TYPE) {
|
|
199
|
-
const parsed = jsonObject(body);
|
|
200
|
-
if (!parsed)
|
|
201
|
-
return request;
|
|
202
|
-
for (const name of Object.keys(parsed)) {
|
|
203
|
-
if (isScope(name))
|
|
204
|
-
delete parsed[name];
|
|
205
|
-
}
|
|
206
|
-
if (replacement !== null)
|
|
207
|
-
parsed["scope"] = replacement;
|
|
208
|
-
body = JSON.stringify(parsed);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
return request;
|
|
44
|
+
export function requestedScopes(rules, declared) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const scope of [
|
|
47
|
+
...declared.map((each) => expandScope(rules, each)),
|
|
48
|
+
...rules.identityScopes,
|
|
49
|
+
]) {
|
|
50
|
+
if (!out.includes(scope))
|
|
51
|
+
out.push(scope);
|
|
212
52
|
}
|
|
213
|
-
return
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** Scopes joined as the provider's `scope` parameter takes them. */
|
|
56
|
+
export function joinScopes(rules, scopes) {
|
|
57
|
+
return scopes.join(rules.scopeSeparator === "comma" ? "," : " ");
|
|
214
58
|
}
|
|
215
59
|
/**
|
|
216
|
-
* The
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* In order: https only; the host must be on one of the declared sets'
|
|
221
|
-
* routes; the raw pathname is refused when it holds `\`, `//` or `%00`;
|
|
222
|
-
* each raw segment, and each percent-decoding of it while a `%` remains
|
|
223
|
-
* (a provider's layers may each decode once), is refused when it won't
|
|
224
|
-
* decode, holds a control or format character or a backslash, is one of
|
|
225
|
-
* the policy's denied segments in any case, or splits on `/` into a dot
|
|
226
|
-
* segment: `.` or `..`, with or without a `;` parameter (`..;x`), which
|
|
227
|
-
* Google's front end resolves like the bare one. The denied comparison is
|
|
228
|
-
* of the whole segment, because an encoded slash is part of one id to the
|
|
229
|
-
* provider, never a separator: a Search Console site URL whose own path
|
|
230
|
-
* holds `batch` stays matchable, and `2%2FmeasurementProtocolSecrets` is
|
|
231
|
-
* a data stream id that doesn't exist, not the secrets. Then the raw
|
|
232
|
-
* pathname, case preserved, is matched: GET and HEAD need a `paths` entry
|
|
233
|
-
* (exact, or a prefix when it ends in `/`), POST needs a `posts` pattern,
|
|
234
|
-
* and no other method goes through. Matching the raw path is what lets
|
|
235
|
-
* Search Console's `sites/https%3A%2F%2Fexample.com%2F/searchAnalytics/query`
|
|
236
|
-
* match `[^/]+` while `..`, batch tunnels and secrets stay refused however
|
|
237
|
-
* encoded. `X-HTTP-Method-Override`, `access_token` and `token` parameters
|
|
238
|
-
* are the proxy's to strip before it asks; nothing here reads them. The
|
|
239
|
-
* proxy fetches with `redirect: "manual"`: Google answers a dot segment it
|
|
240
|
-
* resolves with a 302 to the resolved path, and following one would carry
|
|
241
|
-
* the token to a path this never matched.
|
|
60
|
+
* The scopes a token response says it granted, canonical. A response
|
|
61
|
+
* with no `scope` field is taken as granted exactly as `requested` (RFC
|
|
62
|
+
* 6749 §5.1); one whose `scope` isn't a string is null (refused).
|
|
242
63
|
*/
|
|
243
|
-
export function
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
.map((route) => [name, route]));
|
|
249
|
-
if (onHost.length === 0)
|
|
64
|
+
export function grantedScopes(rules, body, requested) {
|
|
65
|
+
const raw = body["scope"];
|
|
66
|
+
if (raw === undefined)
|
|
67
|
+
return canonicalScopes(rules, requested);
|
|
68
|
+
if (typeof raw !== "string")
|
|
250
69
|
return null;
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
for (const [name, route] of onHost) {
|
|
256
|
-
if (verb === "GET" || verb === "HEAD") {
|
|
257
|
-
const covered = route.paths.some((entry) => entry.endsWith("/") ? pathname.startsWith(entry) : pathname === entry);
|
|
258
|
-
if (covered)
|
|
259
|
-
return name;
|
|
260
|
-
}
|
|
261
|
-
else if (verb === "POST") {
|
|
262
|
-
if (route.posts?.some((pattern) => pattern.test(pathname)))
|
|
263
|
-
return name;
|
|
70
|
+
let text = raw;
|
|
71
|
+
if (text.includes("%")) {
|
|
72
|
+
try {
|
|
73
|
+
text = decodeURIComponent(text);
|
|
264
74
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
/** Control and format characters: in no provider's path once decoded. */
|
|
269
|
-
const UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
|
|
270
|
-
/** A dot segment, bare or with a path parameter (`..;x`): Google's front end resolves both. */
|
|
271
|
-
const DOT_SEGMENT = /^\.{1,2}(;|$)/;
|
|
272
|
-
/**
|
|
273
|
-
* Whether every segment of the raw `pathname` passes the checks above in
|
|
274
|
-
* each form a provider's layers could see it: raw, then decoded again
|
|
275
|
-
* while a `%` remains.
|
|
276
|
-
*/
|
|
277
|
-
function safeSegments(policy, pathname) {
|
|
278
|
-
if (pathname.includes("\\") ||
|
|
279
|
-
pathname.includes("//") ||
|
|
280
|
-
pathname.includes("%00")) {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
const denied = policy.deniedSegments.map((segment) => segment.toLowerCase());
|
|
284
|
-
for (const raw of pathname.split("/")) {
|
|
285
|
-
let text = raw;
|
|
286
|
-
for (;;) {
|
|
287
|
-
if (!safeSegment(text, denied))
|
|
288
|
-
return false;
|
|
289
|
-
if (!text.includes("%"))
|
|
290
|
-
break;
|
|
291
|
-
// A `%` left after a decode is a byte the provider may decode again
|
|
292
|
-
// (double encoding), so that form is checked too; one that isn't an
|
|
293
|
-
// escape (`%2525` ends in a bare `%`) won't decode and refuses, as a
|
|
294
|
-
// malformed escape in the raw path does.
|
|
295
|
-
try {
|
|
296
|
-
text = decodeURIComponent(text);
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
301
77
|
}
|
|
302
78
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Whether one form of a segment is safe: no control or format character,
|
|
307
|
-
* no backslash (in no provider's grammar), not a denied segment in any
|
|
308
|
-
* case, and no dot segment among its `/`-split parts (a traversal the
|
|
309
|
-
* provider might resolve). The denied comparison is of the whole: an
|
|
310
|
-
* encoded slash is part of one id to the provider, never a separator, so
|
|
311
|
-
* a Search Console site URL whose own path holds `batch` stays matchable.
|
|
312
|
-
*/
|
|
313
|
-
function safeSegment(text, denied) {
|
|
314
|
-
if (UNPRINTABLE.test(text) || text.includes("\\"))
|
|
315
|
-
return false;
|
|
316
|
-
if (denied.includes(text.toLowerCase()))
|
|
317
|
-
return false;
|
|
318
|
-
return !text.split("/").some((part) => DOT_SEGMENT.test(part));
|
|
319
|
-
}
|
|
320
|
-
/** The policy's sets in registry order, keeping those `names` declares. */
|
|
321
|
-
function declaredSets(policy, names) {
|
|
322
|
-
return Object.entries(policy.sets).filter(([name]) => names.includes(name));
|
|
323
|
-
}
|
|
324
|
-
const GOOGLE_ALIASES = new Map([
|
|
325
|
-
["email", "https://www.googleapis.com/auth/userinfo.email"],
|
|
326
|
-
["profile", "https://www.googleapis.com/auth/userinfo.profile"],
|
|
327
|
-
]);
|
|
328
|
-
const GRAPH_PREFIX = /^https:\/\/graph\.microsoft\.com\//i;
|
|
329
|
-
/** `scopes` in the dialect's one spelling, deduplicated, order kept. */
|
|
330
|
-
function canonical(dialect, scopes) {
|
|
331
|
-
return pushNew([], scopes.map((scope) => {
|
|
332
|
-
switch (dialect) {
|
|
333
|
-
case "google":
|
|
334
|
-
return GOOGLE_ALIASES.get(scope) ?? scope;
|
|
335
|
-
case "microsoft":
|
|
336
|
-
return scope.replace(GRAPH_PREFIX, "").toLowerCase();
|
|
337
|
-
case "slack":
|
|
338
|
-
return scope;
|
|
339
|
-
}
|
|
340
|
-
}));
|
|
79
|
+
const split = rules.scopeSeparator === "comma" ? text.split(/[,\s]+/) : text.split(/\s+/);
|
|
80
|
+
return canonicalScopes(rules, split.filter(Boolean));
|
|
341
81
|
}
|
|
342
82
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
83
|
+
* The mint check: what `granted` (canonical) carries beyond `requested`,
|
|
84
|
+
* the identity scopes and the tolerated ones. Empty means the token may
|
|
85
|
+
* be kept; anything else and the Gateway drops it.
|
|
346
86
|
*/
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
/** `into` with each of `more` it doesn't have yet, in order. */
|
|
355
|
-
function pushNew(into, more) {
|
|
356
|
-
for (const item of more)
|
|
357
|
-
if (!into.includes(item))
|
|
358
|
-
into.push(item);
|
|
359
|
-
return into;
|
|
360
|
-
}
|
|
361
|
-
const FORM = "application/x-www-form-urlencoded";
|
|
362
|
-
const JSON_TYPE = "application/json";
|
|
363
|
-
/** A token request's body fields, form or JSON, by lowercased name; null for any other body. */
|
|
364
|
-
function bodyFields(request) {
|
|
365
|
-
if (!request.body)
|
|
366
|
-
return null;
|
|
367
|
-
const type = mediaType(request.headers.get("content-type"));
|
|
368
|
-
if (type === FORM)
|
|
369
|
-
return foldFields(new URLSearchParams(request.body));
|
|
370
|
-
if (type === JSON_TYPE) {
|
|
371
|
-
const parsed = jsonObject(request.body);
|
|
372
|
-
return parsed && foldFields(Object.entries(parsed));
|
|
373
|
-
}
|
|
374
|
-
return null;
|
|
87
|
+
export function scopeExcess(rules, granted, requested) {
|
|
88
|
+
const allowed = canonicalScopes(rules, [
|
|
89
|
+
...requested,
|
|
90
|
+
...rules.identityScopes,
|
|
91
|
+
...rules.tolerated,
|
|
92
|
+
]);
|
|
93
|
+
return granted.filter((scope) => !allowed.includes(scope));
|
|
375
94
|
}
|
|
376
95
|
/**
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
* `scope` and `Scope`, hides nothing in the second copy; a value that
|
|
380
|
-
* isn't a string leaves its field not a string, which reads as naming
|
|
381
|
-
* none.
|
|
96
|
+
* The scopes an app declares that a connection doesn't hold: `held` is
|
|
97
|
+
* the connection's canonical list, `declared` as the app wrote them.
|
|
382
98
|
*/
|
|
383
|
-
function
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
const key = name.toLowerCase();
|
|
387
|
-
const before = fields.get(key);
|
|
388
|
-
fields.set(key, !fields.has(key)
|
|
389
|
-
? value
|
|
390
|
-
: typeof before === "string" && typeof value === "string"
|
|
391
|
-
? `${before} ${value}`
|
|
392
|
-
: [before, value]);
|
|
393
|
-
}
|
|
394
|
-
return Object.fromEntries(fields);
|
|
395
|
-
}
|
|
396
|
-
/** A Content-Type's media type, lowercased and without parameters. */
|
|
397
|
-
function mediaType(contentType) {
|
|
398
|
-
return (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
99
|
+
export function missingScopes(rules, held, declared) {
|
|
100
|
+
const have = canonicalScopes(rules, held);
|
|
101
|
+
return declared.filter((scope) => !have.includes(canonicalScope(rules, scope)));
|
|
399
102
|
}
|
|
400
103
|
/**
|
|
401
104
|
* `text` as a JSON object, or null for anything else (an array, a scalar,
|
|
402
|
-
* not JSON).
|
|
105
|
+
* not JSON). The gateway's read of a provider's token answer.
|
|
403
106
|
*/
|
|
404
107
|
export function jsonObject(text) {
|
|
405
108
|
try {
|
|
406
|
-
|
|
109
|
+
const value = JSON.parse(text);
|
|
110
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
111
|
+
? value
|
|
112
|
+
: null;
|
|
407
113
|
}
|
|
408
114
|
catch {
|
|
409
115
|
return null;
|
|
410
116
|
}
|
|
411
117
|
}
|
|
412
|
-
function record(value) {
|
|
413
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
414
|
-
? value
|
|
415
|
-
: null;
|
|
416
|
-
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `oauth2-user` half of a provider spec: a registered OAuth provider,
|
|
3
|
+
* a preset (./oauthPresets.ts) or a built-in one (./oauthBuiltIns.ts), as
|
|
4
|
+
* JSON, turned into an {@link OAuthClientCapability} whose `flow` the
|
|
5
|
+
* Gateway runs. `parseProviderSpec` (./providerSpec.ts) hands the common
|
|
6
|
+
* fields here once it sees `auth.kind: "oauth2-user"`. Nothing here reads
|
|
7
|
+
* a value: a spec names Gateway settings, never holds one.
|
|
8
|
+
*
|
|
9
|
+
* Every difference between providers is a field here, never code: the
|
|
10
|
+
* endpoints, how the client goes on the token request, PKCE, the scope
|
|
11
|
+
* separator and prefix, extra authorize parameters and token headers, the
|
|
12
|
+
* identity URL with the scopes it needs, and the scopes a token may carry
|
|
13
|
+
* beyond what was asked (`tolerated`). Which scopes an app gets is its own
|
|
14
|
+
* `everyapp.config.ts`, never the spec.
|
|
15
|
+
*
|
|
16
|
+
* Only type imports from ./capabilities.ts: the catalog builds its
|
|
17
|
+
* built-in entries with {@link oauthCapabilityFromSpec} at load.
|
|
18
|
+
*/
|
|
19
|
+
import type { OAuthClientCapability, OAuthClientInjection, OAuthScopeSeparator } from "./capabilities.js";
|
|
20
|
+
export interface SpecProblem {
|
|
21
|
+
path: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
/** The fields of `auth` when its kind is `oauth2-user`. */
|
|
25
|
+
export declare const OAUTH_AUTH_FIELDS: readonly ["kind", "authorizeUrl", "tokenUrl", "revokeUrl", "revokeMethod", "clientId", "clientSecret", "inject", "pkce", "scopeSeparator", "scopePrefix", "authorizeParams", "tokenHeaders", "identity", "tolerated"];
|
|
26
|
+
/**
|
|
27
|
+
* Fields an `oauth2-user` spec had before scopes moved to each app's
|
|
28
|
+
* `everyapp.config.ts`, each with what to write instead: refused with
|
|
29
|
+
* that message rather than "unknown field".
|
|
30
|
+
*/
|
|
31
|
+
export declare const REMOVED_OAUTH_FIELDS: Readonly<Record<string, string>>;
|
|
32
|
+
/** The common fields `parseProviderSpec` parsed before handing over. */
|
|
33
|
+
export interface OAuthSpecCommon {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
docs: string;
|
|
37
|
+
hosts: readonly string[];
|
|
38
|
+
description?: string;
|
|
39
|
+
reference?: string;
|
|
40
|
+
hint?: string;
|
|
41
|
+
example?: string;
|
|
42
|
+
tags?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
/** A list of provider scope strings: each one scope, with no space or comma, each once. */
|
|
45
|
+
export declare function scopeList(value: unknown, path: string, problems: SpecProblem[]): string[] | undefined;
|
|
46
|
+
/** What `parseOAuthUserSpec` returns on success, before the registry's rules. */
|
|
47
|
+
export interface ParsedOAuthSpec {
|
|
48
|
+
capability: OAuthClientCapability;
|
|
49
|
+
/** Every host a row's rules must accept: the API hosts and the endpoints'. */
|
|
50
|
+
hosts: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse the OAuth half of a spec whose `auth.kind` is `oauth2-user`, with
|
|
54
|
+
* the common fields already parsed. Every problem is reported; on success
|
|
55
|
+
* the capability with its `flow`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseOAuthUserSpec(input: Record<string, unknown>, common: OAuthSpecCommon, problems: SpecProblem[]): ParsedOAuthSpec | undefined;
|
|
58
|
+
/** The JSON shape of an `oauth2-user` spec, as the built-ins and presets write it. */
|
|
59
|
+
export interface OAuthSpecJson {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly description?: string;
|
|
63
|
+
readonly docs: string;
|
|
64
|
+
readonly api?: string;
|
|
65
|
+
readonly hosts: readonly string[];
|
|
66
|
+
readonly auth: {
|
|
67
|
+
readonly kind: "oauth2-user";
|
|
68
|
+
readonly authorizeUrl: string;
|
|
69
|
+
readonly tokenUrl: string;
|
|
70
|
+
readonly revokeUrl?: string;
|
|
71
|
+
readonly revokeMethod?: "POST" | "DELETE";
|
|
72
|
+
readonly clientId: string;
|
|
73
|
+
readonly clientSecret?: string;
|
|
74
|
+
readonly inject?: OAuthClientInjection;
|
|
75
|
+
readonly pkce?: boolean;
|
|
76
|
+
readonly scopeSeparator?: OAuthScopeSeparator;
|
|
77
|
+
readonly scopePrefix?: string;
|
|
78
|
+
readonly authorizeParams?: Readonly<Record<string, string>>;
|
|
79
|
+
readonly tokenHeaders?: Readonly<Record<string, string>>;
|
|
80
|
+
readonly identity?: {
|
|
81
|
+
readonly url: string;
|
|
82
|
+
readonly label: string;
|
|
83
|
+
readonly id?: string;
|
|
84
|
+
readonly scopes?: readonly string[];
|
|
85
|
+
};
|
|
86
|
+
readonly tolerated?: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
readonly hint?: string;
|
|
89
|
+
readonly example?: string;
|
|
90
|
+
readonly tags?: readonly string[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A spec already known to be good (a built-in, checked by its test) as
|
|
94
|
+
* its capability, without the parse: what the catalog runs at load, where
|
|
95
|
+
* calling the full parser would be an import cycle.
|
|
96
|
+
*/
|
|
97
|
+
export declare function oauthCapabilityFromSpec(spec: OAuthSpecJson): OAuthClientCapability;
|
|
98
|
+
/**
|
|
99
|
+
* The JSON spec of an OAuth capability (`provider show`, a preset's round
|
|
100
|
+
* trip): `parseProviderSpec` of it gives the capability back.
|
|
101
|
+
*/
|
|
102
|
+
export declare function oauthSpecOf(id: string, capability: OAuthClientCapability): Record<string, unknown>;
|
|
103
|
+
//# sourceMappingURL=oauthSpec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthSpec.d.ts","sourceRoot":"","sources":["../../src/internal/oauthSpec.ts"],"names":[],"mappings":"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"}
|