@zackbart/connecta 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +961 -5
- package/README.md +62 -39
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1632 -1509
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +526 -0
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +220 -0
- package/dist/connectors/api-connector.js +214 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +13 -99
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +108 -25
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1780 -351
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +25 -257
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-partition.js +17 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/basecamp/index.d.ts +43 -0
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/breeze/index.d.ts +34 -0
- package/dist/providers/breeze/index.js +1446 -0
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/ccb/index.d.ts +103 -0
- package/dist/providers/ccb/index.js +1725 -0
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/overflow/index.d.ts +60 -0
- package/dist/providers/overflow/index.js +1535 -0
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/planning-center/index.d.ts +59 -0
- package/dist/providers/planning-center/index.js +2199 -0
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/tithely/index.d.ts +49 -0
- package/dist/providers/tithely/index.js +1003 -0
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +136 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +665 -230
- package/documentation/auth.md +962 -458
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +125 -35
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { ConnectorCallError } from "../errors.js";
|
|
3
|
+
import { fromSignal, runEdge } from "../runtime/run.js";
|
|
4
|
+
import { classifyHost } from "../url-safety.js";
|
|
5
|
+
import { authenticateRemoteClient, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
|
|
6
|
+
import { discoveryIssuer } from "./oauth-v2-migration.js";
|
|
7
|
+
/** Local removal succeeded; provider-side removal needs the operator's attention. */
|
|
8
|
+
export class OAuthRevocationError extends ConnectorCallError {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("oauth_revocation_failed", "Local OAuth authorization was removed, but provider revocation could not be confirmed. Revoke the grant in the provider's console.");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const REVOCATION_DEADLINE_MS = 20_000;
|
|
14
|
+
/** Revoke only the issuer-bound snapshot removed by a successful reset CAS, once. */
|
|
15
|
+
export async function revokeDownstreamGrant(body, send, configuredClient) {
|
|
16
|
+
const metadata = body.discovery?.authorizationServerMetadata;
|
|
17
|
+
const revocation = metadata;
|
|
18
|
+
const endpoint = revocation?.revocation_endpoint;
|
|
19
|
+
if (endpoint === undefined || !body.tokens)
|
|
20
|
+
return;
|
|
21
|
+
const deadline = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => deadline.abort(), REVOCATION_DEADLINE_MS);
|
|
23
|
+
try {
|
|
24
|
+
if (typeof endpoint !== "string" ||
|
|
25
|
+
!body.issuer ||
|
|
26
|
+
discoveryIssuer(body.discovery) !== body.issuer ||
|
|
27
|
+
!body.client ||
|
|
28
|
+
(body.client.value.issuer !== undefined && body.client.value.issuer !== body.issuer))
|
|
29
|
+
throw new OAuthRevocationError();
|
|
30
|
+
const target = new URL(endpoint);
|
|
31
|
+
// An operator-trusted HTTP MCP origin does not authorize plaintext
|
|
32
|
+
// credential endpoints. Only the existing loopback development exception
|
|
33
|
+
// is eligible; the connector's destination guard still checks that scope.
|
|
34
|
+
if ((target.protocol !== "https:" &&
|
|
35
|
+
!(target.protocol === "http:" && classifyHost(target.hostname) === "loopback")) ||
|
|
36
|
+
target.username ||
|
|
37
|
+
target.password ||
|
|
38
|
+
target.hash)
|
|
39
|
+
throw new OAuthRevocationError();
|
|
40
|
+
const information = body.client.value;
|
|
41
|
+
const secret = configuredClient ? configuredClient.clientSecret : information.client_secret;
|
|
42
|
+
if (configuredClient &&
|
|
43
|
+
(body.issuer !== configuredClient.issuer || information.client_id !== configuredClient.clientId))
|
|
44
|
+
throw new OAuthRevocationError();
|
|
45
|
+
const advertisedMethods = revocation?.revocation_endpoint_auth_methods_supported;
|
|
46
|
+
if (advertisedMethods !== undefined &&
|
|
47
|
+
(!Array.isArray(advertisedMethods) || !advertisedMethods.every((method) => typeof method === "string")))
|
|
48
|
+
throw new OAuthRevocationError();
|
|
49
|
+
const methods = advertisedMethods;
|
|
50
|
+
const declared = configuredClient
|
|
51
|
+
? remoteClientAuthMethod(configuredClient)
|
|
52
|
+
: "token_endpoint_auth_method" in information
|
|
53
|
+
? information.token_endpoint_auth_method
|
|
54
|
+
: undefined;
|
|
55
|
+
const method = declared ??
|
|
56
|
+
(secret === undefined
|
|
57
|
+
? "none"
|
|
58
|
+
: methods?.includes("client_secret_basic") || !methods
|
|
59
|
+
? "client_secret_basic"
|
|
60
|
+
: "client_secret_post");
|
|
61
|
+
if ((method !== "none" && method !== "client_secret_basic" && method !== "client_secret_post") ||
|
|
62
|
+
(method === "none") !== (secret === undefined) ||
|
|
63
|
+
(methods && !methods.includes(method)))
|
|
64
|
+
throw new OAuthRevocationError();
|
|
65
|
+
const token = body.tokens.refresh_token ?? body.tokens.access_token;
|
|
66
|
+
if (typeof token !== "string" || !token)
|
|
67
|
+
throw new OAuthRevocationError();
|
|
68
|
+
const params = new URLSearchParams({
|
|
69
|
+
token,
|
|
70
|
+
token_type_hint: body.tokens.refresh_token ? "refresh_token" : "access_token",
|
|
71
|
+
});
|
|
72
|
+
const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
|
|
73
|
+
authenticateRemoteClient({
|
|
74
|
+
issuer: body.issuer,
|
|
75
|
+
clientId: information.client_id,
|
|
76
|
+
...(secret !== undefined ? { clientSecret: secret } : {}),
|
|
77
|
+
tokenEndpointAuthMethod: method,
|
|
78
|
+
}, headers, params);
|
|
79
|
+
await runEdge(Effect.raceAllFirst([
|
|
80
|
+
fromSignal(deadline.signal),
|
|
81
|
+
Effect.tryPromise({
|
|
82
|
+
try: async () => {
|
|
83
|
+
const response = await send(target, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers,
|
|
86
|
+
body: params,
|
|
87
|
+
redirect: "manual",
|
|
88
|
+
signal: deadline.signal,
|
|
89
|
+
});
|
|
90
|
+
// RFC 7009's success is HTTP 200. Bodies and headers never explain a failure.
|
|
91
|
+
void response.body?.cancel().catch(() => { });
|
|
92
|
+
if (response.status !== 200)
|
|
93
|
+
throw new OAuthRevocationError();
|
|
94
|
+
},
|
|
95
|
+
catch: () => new OAuthRevocationError(),
|
|
96
|
+
}),
|
|
97
|
+
]));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// No provider text, credential-bearing URL, original exception, or cause survives.
|
|
101
|
+
throw new OAuthRevocationError();
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// The token endpoint's answers as downstream OAuth classifies them and the SDK
|
|
2
|
+
// gets to parse them: a refresh's re-consent verdict, and every failure
|
|
3
|
+
// rebuilt from its OAuth `error` code alone, so nothing else a token endpoint
|
|
4
|
+
// wrote reaches the SDK's console output (#695).
|
|
5
|
+
/** The `grant_type` of a token request, or undefined for any other request. */
|
|
6
|
+
export function tokenGrantType(init) {
|
|
7
|
+
if ((init?.method ?? "GET").toUpperCase() !== "POST")
|
|
8
|
+
return undefined;
|
|
9
|
+
const body = init?.body;
|
|
10
|
+
if (body instanceof URLSearchParams)
|
|
11
|
+
return body.get("grant_type") ?? undefined;
|
|
12
|
+
if (typeof body !== "string")
|
|
13
|
+
return undefined;
|
|
14
|
+
return new URLSearchParams(body).get("grant_type") ?? undefined;
|
|
15
|
+
}
|
|
16
|
+
export function isRefreshTokenRequest(init) {
|
|
17
|
+
return tokenGrantType(init) === "refresh_token";
|
|
18
|
+
}
|
|
19
|
+
/** Grant exchanges and revocation forms carry credentials that must not be replayed. */
|
|
20
|
+
export function isOAuthCredentialRequest(init) {
|
|
21
|
+
if (tokenGrantType(init) !== undefined)
|
|
22
|
+
return true;
|
|
23
|
+
if ((init?.method ?? "GET").toUpperCase() !== "POST")
|
|
24
|
+
return false;
|
|
25
|
+
const body = init?.body;
|
|
26
|
+
const form = body instanceof URLSearchParams ? body : typeof body === "string" ? new URLSearchParams(body) : undefined;
|
|
27
|
+
return form?.has("token") === true;
|
|
28
|
+
}
|
|
29
|
+
function sdkAcceptsOAuthTokens(value) {
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
31
|
+
return false;
|
|
32
|
+
const candidate = value;
|
|
33
|
+
if (typeof candidate.access_token !== "string" || typeof candidate.token_type !== "string") {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
for (const key of ["id_token", "scope", "refresh_token"]) {
|
|
37
|
+
if (candidate[key] !== undefined && typeof candidate[key] !== "string") {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (candidate.expires_in !== undefined) {
|
|
42
|
+
try {
|
|
43
|
+
if (!Number.isFinite(Number(candidate.expires_in)))
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const MAX_REFRESH_RESPONSE_BYTES = 65_536;
|
|
53
|
+
class OversizedRefreshResponse extends Error {
|
|
54
|
+
constructor() {
|
|
55
|
+
super(`OAuth refresh response exceeded ${MAX_REFRESH_RESPONSE_BYTES} bytes.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function readRefreshResponse(response, signal, clone = true) {
|
|
59
|
+
if (Number(response.headers.get("content-length")) > MAX_REFRESH_RESPONSE_BYTES) {
|
|
60
|
+
void response.body?.cancel().catch(() => { });
|
|
61
|
+
throw new OversizedRefreshResponse();
|
|
62
|
+
}
|
|
63
|
+
const reader = (clone ? response.clone() : response).body?.getReader();
|
|
64
|
+
if (!reader)
|
|
65
|
+
return undefined;
|
|
66
|
+
void reader.closed.catch(() => { });
|
|
67
|
+
const cancel = () => {
|
|
68
|
+
void reader.cancel().catch(() => { });
|
|
69
|
+
void response.body?.cancel().catch(() => { });
|
|
70
|
+
};
|
|
71
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
72
|
+
if (signal?.aborted)
|
|
73
|
+
cancel();
|
|
74
|
+
const decoder = new TextDecoder();
|
|
75
|
+
let size = 0;
|
|
76
|
+
let text = "";
|
|
77
|
+
try {
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done)
|
|
81
|
+
break;
|
|
82
|
+
size += value.byteLength;
|
|
83
|
+
if (size > MAX_REFRESH_RESPONSE_BYTES) {
|
|
84
|
+
// A cloned body's cancellation can await its sibling. Cancel both,
|
|
85
|
+
// without making the refusal wait for the provider to finish sending.
|
|
86
|
+
void reader.cancel().catch(() => { });
|
|
87
|
+
void response.body?.cancel().catch(() => { });
|
|
88
|
+
throw new OversizedRefreshResponse();
|
|
89
|
+
}
|
|
90
|
+
text += decoder.decode(value, { stream: true });
|
|
91
|
+
}
|
|
92
|
+
return JSON.parse(text + decoder.decode());
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
signal?.removeEventListener("abort", cancel);
|
|
96
|
+
reader.releaseLock();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function oauthErrorCode(body) {
|
|
100
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
101
|
+
return undefined;
|
|
102
|
+
const code = body.error;
|
|
103
|
+
return typeof code === "string" ? code : undefined;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The OAuth `error` codes a token-endpoint failure may carry to the SDK, each
|
|
107
|
+
* with the only description it will ever see beside it. RFC 6749's error
|
|
108
|
+
* codes, RFC 8707's, and RFC 9449's: the SDK decides its control flow on the
|
|
109
|
+
* code, so the code is all that has to survive.
|
|
110
|
+
*/
|
|
111
|
+
const SDK_TOKEN_ERROR_DESCRIPTIONS = {
|
|
112
|
+
invalid_request: "The authorization server refused the token request.",
|
|
113
|
+
invalid_client: "The authorization server refused the client.",
|
|
114
|
+
invalid_grant: "The authorization server refused the grant.",
|
|
115
|
+
unauthorized_client: "The authorization server refused the client for this grant.",
|
|
116
|
+
unsupported_grant_type: "The authorization server refused the grant type.",
|
|
117
|
+
invalid_scope: "The authorization server refused the requested scope.",
|
|
118
|
+
invalid_target: "The authorization server refused the requested resource.",
|
|
119
|
+
server_error: "The authorization server is temporarily unavailable.",
|
|
120
|
+
temporarily_unavailable: "The authorization server is temporarily unavailable.",
|
|
121
|
+
invalid_dpop_proof: "The authorization server refused the DPoP proof.",
|
|
122
|
+
use_dpop_nonce: "The authorization server requires a DPoP nonce.",
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* A token-endpoint failure as the SDK gets to parse it: the OAuth `error`
|
|
126
|
+
* code, a fixed description, the status, and `Retry-After`, and nothing else
|
|
127
|
+
* the server wrote. Since client 2.1.0 the SDK writes a failure's description
|
|
128
|
+
* to `console.warn` when it restarts or falls through to consent, and a body
|
|
129
|
+
* that is no OAuth error at all goes there raw. A token endpoint that echoes
|
|
130
|
+
* the form it refused would put the refresh token and client secret in the
|
|
131
|
+
* host's console, below any logger the deployment configured.
|
|
132
|
+
*/
|
|
133
|
+
function sdkTokenFailure(original, code, status = original.status) {
|
|
134
|
+
void original.body?.cancel().catch(() => { });
|
|
135
|
+
const retryAfter = original.headers.get("retry-after");
|
|
136
|
+
const known = code in SDK_TOKEN_ERROR_DESCRIPTIONS ? code : "invalid_request";
|
|
137
|
+
return Response.json({ error: known, error_description: SDK_TOKEN_ERROR_DESCRIPTIONS[known] }, {
|
|
138
|
+
status,
|
|
139
|
+
...(retryAfter !== null ? { headers: { "retry-after": retryAfter } } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Rebuild a code exchange's answer (or any token request's but a refresh,
|
|
144
|
+
* which `refreshResponseOutcome` classifies) before the SDK parses it, unless
|
|
145
|
+
* it is a token response the SDK accepts. Whatever fails that check is a
|
|
146
|
+
* failure, and the SDK puts a failure's body in its error message whole — a
|
|
147
|
+
* 200 whose `error` is `null`, a number, or an object included. A code outside
|
|
148
|
+
* the registered set becomes `invalid_request`, and a body without a string
|
|
149
|
+
* code becomes `server_error`, the classes the SDK already gave them.
|
|
150
|
+
*/
|
|
151
|
+
export async function sdkSafeTokenResponse(response, accept) {
|
|
152
|
+
if (response.status >= 300 && response.status < 400) {
|
|
153
|
+
return sdkTokenFailure(response, "invalid_request", 400);
|
|
154
|
+
}
|
|
155
|
+
let parsed;
|
|
156
|
+
try {
|
|
157
|
+
parsed = await readRefreshResponse(response);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Not JSON, or too large to be a token response: only the status is left.
|
|
161
|
+
return sdkTokenFailure(response, "server_error");
|
|
162
|
+
}
|
|
163
|
+
if (response.ok && sdkAcceptsOAuthTokens(parsed)) {
|
|
164
|
+
await accept?.(parsed);
|
|
165
|
+
return response;
|
|
166
|
+
}
|
|
167
|
+
return sdkTokenFailure(response, oauthErrorCode(parsed) ?? "server_error");
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Classify the token endpoint's answer once with a bounded read, and keep the parsed
|
|
171
|
+
* tokens: a valid response has already consumed the rotating refresh token, so
|
|
172
|
+
* the coordinator stores it before releasing anyone, rather than trusting the
|
|
173
|
+
* SDK's later `saveTokens` to arrive on a request that may already be cancelled.
|
|
174
|
+
*
|
|
175
|
+
* A failure also decides what the SDK gets to parse. Pinned against
|
|
176
|
+
* `@modelcontextprotocol/client` 2.3.1, `src/client/auth.ts`: `authInternal()`
|
|
177
|
+
* swallows a refresh failure that is not an `OAuthError`, or is `server_error`,
|
|
178
|
+
* and falls through to `startAuthorization`. All dispatched failures reach the
|
|
179
|
+
* SDK as `invalid_grant`, except client refusals that also clear registration.
|
|
180
|
+
* Provider hooks preserve the re-consent verdict for passive calls. Rebuilding
|
|
181
|
+
* the response keeps provider text out of SDK output.
|
|
182
|
+
*/
|
|
183
|
+
export async function refreshResponseOutcome(response, signal) {
|
|
184
|
+
if (response.status >= 300 && response.status < 400) {
|
|
185
|
+
return {
|
|
186
|
+
failure: new Error("OAuth token endpoint redirected; authorization required."),
|
|
187
|
+
verdict: { kind: "dead" },
|
|
188
|
+
forSdk: sdkTokenFailure(response, "invalid_grant", 400),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
let code;
|
|
193
|
+
try {
|
|
194
|
+
const body = await readRefreshResponse(response, signal, false);
|
|
195
|
+
if (body && typeof body === "object" && ("access_token" in body || "refresh_token" in body)) {
|
|
196
|
+
// A provider issuing tokens while reporting failure may have rotated.
|
|
197
|
+
throw new Error("OAuth refresh failure response may contain a rotation.");
|
|
198
|
+
}
|
|
199
|
+
code = oauthErrorCode(body);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// Complete non-JSON failures still require re-consent. A lost body or
|
|
203
|
+
// evidence of issued tokens cannot prove a committed outcome.
|
|
204
|
+
if (!(error instanceof SyntaxError))
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
const failure = new Error(`OAuth refresh failed with HTTP ${response.status}.`);
|
|
208
|
+
return {
|
|
209
|
+
failure,
|
|
210
|
+
verdict: { kind: "dead" },
|
|
211
|
+
forSdk: sdkTokenFailure(response, code === "invalid_client" || code === "unauthorized_client" ? code : "invalid_grant", 400),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// A successful HTTP status may have consumed the token even when its
|
|
215
|
+
// body cannot be read or parsed. The coordinator refuses the fingerprint.
|
|
216
|
+
const parsed = await readRefreshResponse(response, signal, false);
|
|
217
|
+
if (sdkAcceptsOAuthTokens(parsed)) {
|
|
218
|
+
// An `issuer` names the server a grant is bound to, and only the client
|
|
219
|
+
// stamps it — never the server answering, as the SDK's own parse agrees.
|
|
220
|
+
const { issuer: _ignored, ...tokens } = parsed;
|
|
221
|
+
return { tokens };
|
|
222
|
+
}
|
|
223
|
+
if (oauthErrorCode(parsed) !== undefined) {
|
|
224
|
+
return {
|
|
225
|
+
failure: new Error("OAuth refresh was refused by the authorization server."),
|
|
226
|
+
verdict: { kind: "dead" },
|
|
227
|
+
forSdk: sdkTokenFailure(response, "invalid_grant", 400),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
throw new Error("OAuth refresh response did not match the token schema.");
|
|
231
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { oauthV2Keys } from "../storage/keys.js";
|
|
2
|
+
function plainObject(value) {
|
|
3
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function parse(raw) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** The issuer a discovery record names, as the SDK derives it. */
|
|
14
|
+
export function discoveryIssuer(state) {
|
|
15
|
+
if (!plainObject(state) || typeof state.authorizationServerUrl !== "string")
|
|
16
|
+
return undefined;
|
|
17
|
+
const metadata = state.authorizationServerMetadata;
|
|
18
|
+
const issuer = plainObject(metadata) && typeof metadata.issuer === "string" ? metadata.issuer : state.authorizationServerUrl;
|
|
19
|
+
return issuer === "" ? undefined : issuer;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Layout 2's grant in this namespace, or nothing when it holds none.
|
|
23
|
+
*
|
|
24
|
+
* Only issuer-stamped envelopes (`connectaOAuthVersion: 2`, written since
|
|
25
|
+
* v0.9.0) of the active generation are read; anything older, and any value
|
|
26
|
+
* the vault cannot open, is left behind, as layout 2's own flow entry would
|
|
27
|
+
* have retired it. A grant is kept only when every credential carries a
|
|
28
|
+
* stamp, the stamps agree, and they name the server the generation's
|
|
29
|
+
* discovery names, if any; otherwise it migrates empty and needs consent.
|
|
30
|
+
* One-shot consent state (pending URL, verifier, state) is not carried.
|
|
31
|
+
*/
|
|
32
|
+
export async function readV2Grant(storage, sealer) {
|
|
33
|
+
const keys = await listV2Keys(storage);
|
|
34
|
+
if (keys.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
const generation = (await storage.get(oauthV2Keys.generation)) ?? "legacy";
|
|
37
|
+
if (generation.startsWith("disconnected:"))
|
|
38
|
+
return { disconnected: true, body: {} };
|
|
39
|
+
if (!generation.startsWith("v2:") && generation !== "legacy") {
|
|
40
|
+
// An unfinished reset, or a numeric generation from before epochs.
|
|
41
|
+
return { disconnected: false, body: {} };
|
|
42
|
+
}
|
|
43
|
+
const epoch = generation === "legacy" ? null : generation;
|
|
44
|
+
const read = async (field) => {
|
|
45
|
+
const physicalKey = oauthV2Keys.value(field, epoch);
|
|
46
|
+
const raw = await storage.get(physicalKey);
|
|
47
|
+
if (raw === null)
|
|
48
|
+
return undefined;
|
|
49
|
+
let parsed = parse(raw);
|
|
50
|
+
if (plainObject(parsed) && parsed.connectaOAuthSealed === 1 && typeof parsed.sealed === "string") {
|
|
51
|
+
if (!sealer)
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
parsed = parse(await sealer.open(physicalKey, parsed.sealed));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (plainObject(parsed) &&
|
|
61
|
+
parsed.connectaOAuthVersion === 2 &&
|
|
62
|
+
parsed.generation === generation &&
|
|
63
|
+
(parsed.issuer === undefined || typeof parsed.issuer === "string") &&
|
|
64
|
+
(parsed.binding === undefined || typeof parsed.binding === "string")) {
|
|
65
|
+
return {
|
|
66
|
+
value: parsed.value,
|
|
67
|
+
...(parsed.issuer !== undefined ? { issuer: parsed.issuer } : {}),
|
|
68
|
+
...(parsed.binding !== undefined ? { binding: parsed.binding } : {}),
|
|
69
|
+
...(parsed.carried === true ? { carried: true } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Layout 2 wrote discovery bare under the legacy generation.
|
|
73
|
+
return field === oauthV2Keys.field.discovery && epoch === null && discoveryIssuer(parsed)
|
|
74
|
+
? { value: parsed }
|
|
75
|
+
: undefined;
|
|
76
|
+
};
|
|
77
|
+
const [client, tokens, discovery] = await Promise.all([
|
|
78
|
+
read(oauthV2Keys.field.client),
|
|
79
|
+
read(oauthV2Keys.field.tokens),
|
|
80
|
+
read(oauthV2Keys.field.discovery),
|
|
81
|
+
]);
|
|
82
|
+
const named = discovery ? discoveryIssuer(discovery.value) : undefined;
|
|
83
|
+
const credentials = [client, tokens].filter((value) => value !== undefined);
|
|
84
|
+
const issuer = credentials[0]?.issuer ?? named;
|
|
85
|
+
const consistent = credentials.every((value) => value.issuer !== undefined && value.issuer === issuer && plainObject(value.value)) &&
|
|
86
|
+
(named === undefined || named === issuer);
|
|
87
|
+
if (!consistent || issuer === undefined)
|
|
88
|
+
return { disconnected: false, body: {} };
|
|
89
|
+
return {
|
|
90
|
+
disconnected: false,
|
|
91
|
+
body: {
|
|
92
|
+
issuer,
|
|
93
|
+
...(client
|
|
94
|
+
? {
|
|
95
|
+
client: {
|
|
96
|
+
value: client.value,
|
|
97
|
+
...(client.binding !== undefined ? { binding: client.binding } : {}),
|
|
98
|
+
...(client.carried ? { carried: true } : {}),
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
...(tokens ? { tokens: tokens.value } : {}),
|
|
103
|
+
...(discovery && named !== undefined ? { discovery: discovery.value } : {}),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Only historical families are removed, never grants, flows, or leases. */
|
|
108
|
+
async function listV2Keys(storage) {
|
|
109
|
+
return (await storage.list(oauthV2Keys.scan)).filter((key) => oauthV2Keys.family.prefixes.some((prefix) => key.startsWith(prefix)));
|
|
110
|
+
}
|
|
111
|
+
/** Return true only when every historical key was deleted. */
|
|
112
|
+
export async function deleteV2Keys(storage) {
|
|
113
|
+
const keys = await listV2Keys(storage);
|
|
114
|
+
const results = await Promise.allSettled(keys.map((key) => storage.delete(key)));
|
|
115
|
+
return results.every((result) => result.status === "fulfilled");
|
|
116
|
+
}
|