@zackbart/connecta 0.37.0 → 0.38.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 +80 -0
- package/README.md +2 -2
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/cloudflare-access.d.ts +3 -1
- package/dist/auth/cloudflare-access.js +22 -11
- package/dist/auth/downstream-oauth.js +195 -270
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/oauth-grant.js +11 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +2 -2
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-admission.js +5 -0
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +26 -7
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-revision.js +9 -0
- package/dist/catalog-service.js +51 -20
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +41 -14
- package/dist/config.js +21 -71
- package/dist/connector-caller.js +1 -1
- package/dist/connectors/api-connector.js +4 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/remote-mcp.js +93 -69
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credentials.js +3 -0
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/downstream-input.js +2 -2
- package/dist/execute.js +161 -116
- package/dist/executor-result.js +2 -1
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-runtime.js +5 -3
- package/dist/guest-types.d.ts +5 -5
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +2 -2
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +1 -1
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +1 -1
- package/dist/http/safety.js +1 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +1 -7
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.js +1 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +8 -2
- package/dist/meta-tools.js +27 -27
- package/dist/native-tools.js +2 -1
- package/dist/oauth-handoff.js +2 -2
- package/dist/operation-vocabulary.js +1 -1
- package/dist/operator-record.d.ts +8 -2
- package/dist/operator-record.js +18 -50
- package/dist/operator-ui/contract.d.ts +4 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +7 -2
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +1 -3
- package/dist/providers/_shared/rest/value-safety.js +26 -28
- package/dist/providers/breeze/api.js +2 -7
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/tools.js +3 -4
- package/dist/providers/ccb/api.js +1 -6
- package/dist/providers/ccb/index.js +2 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/docs/tools.js +2 -10
- package/dist/providers/drive/tools.js +12 -25
- package/dist/providers/forms/tools.js +1 -9
- package/dist/providers/gmail/tools.js +5 -21
- package/dist/providers/overflow/api.js +1 -3
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +2 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/sheets/tools.js +1 -9
- package/dist/providers/slides/tools.js +4 -13
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/tithely/api.js +2 -7
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +1 -6
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/tools.js +2 -1
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +35 -22
- package/dist/request-principal.js +15 -2
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +1 -1
- package/dist/routes/dispatch.js +10 -0
- package/dist/routes/mcp.js +23 -21
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +53 -7
- package/dist/routes/shared.d.ts +8 -8
- package/dist/routes/shared.js +22 -15
- package/dist/routes/ui-config.js +6 -6
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -4
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +1 -1
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/sent-secrets.d.ts +14 -6
- package/dist/sent-secrets.js +231 -85
- package/dist/server.js +4 -4
- package/dist/skills.js +8 -35
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +29 -124
- package/dist/storage/sql-native.js +14 -111
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +1 -12
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +4 -3
- package/dist/ui.js +3 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/documentation/README.md +6 -42
- package/documentation/architecture.md +190 -507
- package/documentation/auth.md +172 -58
- package/documentation/code-mode.md +98 -66
- package/documentation/deploying.md +19 -40
- package/documentation/integrating.md +8 -19
- package/documentation/meta-tools.md +23 -6
- package/documentation/migration-0.33.md +5 -5
- package/documentation/migration-0.34.md +2 -4
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +19 -119
- package/documentation/operator-ui.md +46 -8
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/examples/worker/AGENTS.md +4 -6
- package/examples/worker/README.md +20 -115
- package/guides/connecting-agents.md +7 -30
- package/guides/how-it-works.md +2 -2
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
package/dist/config.js
CHANGED
|
@@ -79,11 +79,12 @@ const discovery = {
|
|
|
79
79
|
const calls = {
|
|
80
80
|
/**
|
|
81
81
|
* Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
|
|
82
|
-
* `timeoutMs`. An explicit per-call value wins.
|
|
83
|
-
*
|
|
82
|
+
* `timeoutMs`. An explicit per-call value wins. Unset by default, when those
|
|
83
|
+
* calls fall back to `execute.hostCallTimeoutMs`.
|
|
84
84
|
*
|
|
85
|
-
* Each call makes one attempt. `
|
|
86
|
-
*
|
|
85
|
+
* Each call makes one attempt. `connecta.call` inside `execute_code` uses
|
|
86
|
+
* it too, ahead of `execute.hostCallTimeoutMs`; `connecta.result`,
|
|
87
|
+
* `connecta.skill` and `connecta.read` do not.
|
|
87
88
|
*/
|
|
88
89
|
defaultTimeoutMs: whole({ min: 1 }),
|
|
89
90
|
/**
|
|
@@ -111,9 +112,12 @@ const execute = {
|
|
|
111
112
|
/** Host calls one program may make. Default 20. */
|
|
112
113
|
maxHostCalls: whole({ min: 1, default: D.execute.maxHostCalls }),
|
|
113
114
|
/**
|
|
114
|
-
* Deadline for
|
|
115
|
-
*
|
|
116
|
-
*
|
|
115
|
+
* Deadline for a program's `connecta.result`, `connecta.skill` and
|
|
116
|
+
* `connecta.read`, in milliseconds. Default 15_000. Discovery uses
|
|
117
|
+
* `discovery.probeTimeoutMs`. `connecta.call` and the direct call tools use it only when
|
|
118
|
+
* `calls.defaultTimeoutMs` is unset, and an explicit per-call `timeoutMs`
|
|
119
|
+
* always wins. Raise it for providers whose legitimate calls run longer,
|
|
120
|
+
* such as analytics queries.
|
|
117
121
|
*/
|
|
118
122
|
hostCallTimeoutMs: whole({ min: 1, default: D.execute.hostCallTimeoutMs }),
|
|
119
123
|
/**
|
|
@@ -129,6 +133,14 @@ const execute = {
|
|
|
129
133
|
* budget every call already spends. Default 10.
|
|
130
134
|
*/
|
|
131
135
|
maxWrites: whole({ min: 1, default: D.execute.maxWrites }),
|
|
136
|
+
/**
|
|
137
|
+
* Serialized characters a program may return, shared with emitted text,
|
|
138
|
+
* before Connecta stashes the return and answers with a paging notice.
|
|
139
|
+
* Default 24_000 (~6k tokens). Raise it when agents need large results whole
|
|
140
|
+
* and the model's context can afford them: each paging round trip costs a
|
|
141
|
+
* model step. From 4_000 to 1_000_000.
|
|
142
|
+
*/
|
|
143
|
+
maxResultChars: whole({ min: 4_000, max: 1_000_000, default: D.execute.maxResultChars }),
|
|
132
144
|
};
|
|
133
145
|
function admissionPool(defaults) {
|
|
134
146
|
return {
|
|
@@ -492,48 +504,6 @@ const connectaConfig = {
|
|
|
492
504
|
readOnly: required(opaque({ check: assertFlag }), (path) => READ_ONLY_REQUIRED(path)),
|
|
493
505
|
};
|
|
494
506
|
const CONFIG = object(connectaConfig);
|
|
495
|
-
/** Pause-only options #672 removed with `resume_execution`. */
|
|
496
|
-
const RETIRED_PAUSE_OPTIONS = ["ConnectaConfig.execute.resumableWrites", "ConnectaConfig.execute.pausedRunTtlSeconds"];
|
|
497
|
-
/** Access options pools replaced (#837, decisions/0006). */
|
|
498
|
-
const isRetiredPoolGrant = (path) => /^ConnectaConfig\.pools\.[^.]+\.grant$/.test(path);
|
|
499
|
-
/** Management resolvers maintainers and pool membership replaced (#837, decisions/0006). */
|
|
500
|
-
const RETIRED_IDENTITY = [
|
|
501
|
-
["credentialAdministration", "maintainers administer shared credentials and OAuth grants. List them in maintainers."],
|
|
502
|
-
[
|
|
503
|
-
"personalConnection",
|
|
504
|
-
"pool members connect and disconnect their own personal connectors in their pools, and maintainers their own. Add people to a pool's members.",
|
|
505
|
-
],
|
|
506
|
-
[
|
|
507
|
-
"accessTokenManagement",
|
|
508
|
-
"maintainers issue, rotate, and revoke agent tokens from an interactive session. List them in maintainers.",
|
|
509
|
-
],
|
|
510
|
-
["activityAccess", "maintainers read activity; activity.readGate can still narrow it. List them in maintainers."],
|
|
511
|
-
];
|
|
512
|
-
/** `trust`, on the deployment or a pool, was replaced by `readOnly` (#820). */
|
|
513
|
-
const isRetiredTrust = (path) => path === "ConnectaConfig.trust" || (path.startsWith("ConnectaConfig.pools.") && path.endsWith(".trust"));
|
|
514
|
-
/**
|
|
515
|
-
* `identity` was removed whole (#837), so the walk reports only the section.
|
|
516
|
-
* Name each key it still holds, read by descriptor without running a getter
|
|
517
|
-
* or echoing a value, so a retired resolver's message names its replacement.
|
|
518
|
-
*/
|
|
519
|
-
function expandRetiredIdentity(config, paths) {
|
|
520
|
-
if (!paths.includes("ConnectaConfig.identity"))
|
|
521
|
-
return paths;
|
|
522
|
-
let keys = [];
|
|
523
|
-
try {
|
|
524
|
-
const descriptor = Reflect.getOwnPropertyDescriptor(config, "identity");
|
|
525
|
-
const value = descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
526
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
527
|
-
keys = Reflect.ownKeys(value).filter((key) => typeof key === "string");
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
keys = [];
|
|
532
|
-
}
|
|
533
|
-
if (keys.length === 0)
|
|
534
|
-
return paths;
|
|
535
|
-
return paths.flatMap((path) => path === "ConnectaConfig.identity" ? keys.map((key) => `ConnectaConfig.identity.${key}`) : [path]);
|
|
536
|
-
}
|
|
537
507
|
function rejectUnknownOptions(paths) {
|
|
538
508
|
if (paths.length === 0)
|
|
539
509
|
return;
|
|
@@ -547,27 +517,6 @@ function rejectUnknownOptions(paths) {
|
|
|
547
517
|
: "") +
|
|
548
518
|
(paths.includes("ConnectaConfig.execute.approval")
|
|
549
519
|
? "\nexecute.approval was removed. The MCP client owns approval; set readOnly: true on the deployment or a pool to refuse writes."
|
|
550
|
-
: "") +
|
|
551
|
-
(paths.some((path) => RETIRED_PAUSE_OPTIONS.includes(path))
|
|
552
|
-
? "\nPrograms no longer pause at writes, so there is nothing to configure: writable endpoints let programs and call_destructive_tool write; readOnly endpoints refuse every write (issue #672). Delete the option."
|
|
553
|
-
: "") +
|
|
554
|
-
(paths.includes("ConnectaConfig.identity.connectorAccess")
|
|
555
|
-
? "\nidentity.connectorAccess was removed (#837): pools are the access model. List who may use plain /mcp in maintainers, and give each pool its tools and members. A guarded { tool, requireReadOnly: true } grant becomes a pool with readOnly: true."
|
|
556
|
-
: "") +
|
|
557
|
-
RETIRED_IDENTITY.filter(([key]) => paths.includes(`ConnectaConfig.identity.${key}`))
|
|
558
|
-
.map(([key, replacement]) => `\nidentity.${key} was removed (#837): ${replacement}`)
|
|
559
|
-
.join("") +
|
|
560
|
-
(paths.some((path) => path === "ConnectaConfig.identity" || path.startsWith("ConnectaConfig.identity."))
|
|
561
|
-
? "\nThe identity section was removed (#837): maintainers and pool members decide access and management. Delete it."
|
|
562
|
-
: "") +
|
|
563
|
-
(paths.includes("ConnectaConfig.grant")
|
|
564
|
-
? "\nThe top-level grant was removed (#837); list the people who may use plain /mcp in maintainers."
|
|
565
|
-
: "") +
|
|
566
|
-
(paths.some(isRetiredPoolGrant)
|
|
567
|
-
? "\nPool grant was removed (#837); list who may open the pool in its members."
|
|
568
|
-
: "") +
|
|
569
|
-
(paths.some(isRetiredTrust)
|
|
570
|
-
? '\ntrust was removed (#820); set readOnly, which every deployment now chooses explicitly. trust: "read-only" becomes readOnly: true, which now also refuses call_destructive_tool. trust: "trusted" becomes readOnly: false. A pool inherits the deployment readOnly unless it sets its own.'
|
|
571
520
|
: ""));
|
|
572
521
|
}
|
|
573
522
|
/** Checks that span fields; each value is already individually valid. */
|
|
@@ -598,7 +547,7 @@ export function resolveConfig(config) {
|
|
|
598
547
|
*/
|
|
599
548
|
export function readConfig(config) {
|
|
600
549
|
const read = readPlain(config, "ConnectaConfig", CONFIG);
|
|
601
|
-
rejectUnknownOptions(
|
|
550
|
+
rejectUnknownOptions(read.unknown);
|
|
602
551
|
const input = read.value;
|
|
603
552
|
const parsed = CONFIG.parse(input, "ConnectaConfig");
|
|
604
553
|
assertCoherent(parsed);
|
|
@@ -648,5 +597,6 @@ export function executeLimits(config) {
|
|
|
648
597
|
hostCallTimeoutMs: config.execute.hostCallTimeoutMs,
|
|
649
598
|
watchdogMs: config.execute.watchdogMs,
|
|
650
599
|
maxWrites: config.execute.maxWrites,
|
|
600
|
+
maxResultChars: config.execute.maxResultChars,
|
|
651
601
|
};
|
|
652
602
|
}
|
package/dist/connector-caller.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// registry view that request receives, and `api()` hands handlers that same
|
|
18
18
|
// context object; no argument or program can name it, and a header only
|
|
19
19
|
// through an inbound-auth provider configured to read one (a bearer's asserted
|
|
20
|
-
// principal). A context no request admitted
|
|
20
|
+
// principal). A context no request admitted has no caller, and a delegated provider fails closed on it,
|
|
21
21
|
// as it does on the anonymous caller an open deployment admits.
|
|
22
22
|
import { Caller, serviceFor, setViewService } from "./runtime/call.js";
|
|
23
23
|
const callerIdentities = new WeakSet();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapViewCredentials } from "../runtime/call.js";
|
|
1
2
|
import { storedAuthObservation, withStoredAuth } from "../auth/stored-observation.js";
|
|
2
3
|
import { Effect, Exit } from "effect";
|
|
3
4
|
import { registerConnectorVocabulary } from "../operation-vocabulary.js";
|
|
@@ -13,7 +14,7 @@ import { describedEndpoint, describedOrigin, describedTools } from "../described
|
|
|
13
14
|
import { READ_ONLY, CALL_ADMISSION, CREDENTIAL, USAGE_GUIDE } from "./option-shapes.js";
|
|
14
15
|
import { assertStaticToolNames } from "../tool-name.js";
|
|
15
16
|
import { registerInvocationAuth } from "../invocation-auth.js";
|
|
16
|
-
import { redactCatalog, redactSentSecrets, sentSecretsFetch
|
|
17
|
+
import { redactCatalog, redactSentSecrets, sentSecretsFetch } from "../sent-secrets.js";
|
|
17
18
|
import { callClassification } from "../call-classification.js";
|
|
18
19
|
import { classifyTool } from "../tool-safety.js";
|
|
19
20
|
import { closeProviderCall, enterProviderBoundary, inLogicalCall, knownProviderFailure, nestedProviderRequests, providerBoundary, providerCallContext, providerFetch, providerRequests, tracksRequests, } from "../provider-call.js";
|
|
@@ -24,7 +25,7 @@ export function defined(value) {
|
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Three things a hand-written surface is refused for at construction rather
|
|
27
|
-
* than in production
|
|
28
|
+
* than in production: no description (discovery has nothing to route on
|
|
28
29
|
* and a guess costs a call), no explicit `readOnlyHint` (connecta never infers
|
|
29
30
|
* the safety class, so an unclassified tool is a deployment bug), and an
|
|
30
31
|
* `inputSchema` the validator cannot compile (declaring one is optional;
|
|
@@ -173,7 +174,7 @@ export function apiConnector(id, opts, oauth) {
|
|
|
173
174
|
callTool: (name, args, ctx, options) => runEdge(core(name, args, ctx, options)),
|
|
174
175
|
};
|
|
175
176
|
const core = (name, args, ctx) => Effect.gen(function* () {
|
|
176
|
-
|
|
177
|
+
wrapViewCredentials(ctx);
|
|
177
178
|
enterProviderBoundary(ctx, connector);
|
|
178
179
|
const tool = byName.get(name);
|
|
179
180
|
// Refusals before the handler runs are known failures: nothing was sent.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { retryAfterMs } from "../http-policy.js";
|
|
2
|
-
import { type ProviderRequest } from "../provider-call.js";
|
|
3
2
|
import type { ConnectorContext } from "../types.js";
|
|
4
3
|
/** The methods a hand-written provider surface actually uses. */
|
|
5
4
|
type GuardedMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -182,11 +181,6 @@ type GuardedFetcher = (url: string, init: RequestInit, ctx: ConnectorContext) =>
|
|
|
182
181
|
export declare function oauthBearer(provider: string): Pick<GuardedFetchOptions, "authenticate" | "fetch">;
|
|
183
182
|
/** Send one guarded request and map its response with provider knowledge. */
|
|
184
183
|
export type GuardedTransport = <T>(request: GuardedRequest, ctx: ConnectorContext, map: GuardedResponseMapper<T>) => Promise<T>;
|
|
185
|
-
/**
|
|
186
|
-
* Test-only legacy response lookup for renewal-provenance fixtures outside
|
|
187
|
-
* the kit. Production renewal uses the kit ledger. Undefined for other objects.
|
|
188
|
-
*/
|
|
189
|
-
export declare function answeredRequest(response: object): ProviderRequest | undefined;
|
|
190
184
|
/**
|
|
191
185
|
* Build the transport a connector sends every request through. `maxResponseBytes`
|
|
192
186
|
* is required rather than defaulted: what counts as an absurd response is a fact
|
|
@@ -3,7 +3,7 @@ export { retryAfterMs } from "../http-policy.js";
|
|
|
3
3
|
import { callerSignal, duringViewCall, settleOnClose, viewSignal } from "../runtime/call.js";
|
|
4
4
|
/**
|
|
5
5
|
* The safety machinery every hand-written HTTP surface needs, extracted once
|
|
6
|
-
|
|
6
|
+
*: path confinement, encoded query and framed bodies, per-request
|
|
7
7
|
* credential headers applied last, a refused redirect, a bounded read, and
|
|
8
8
|
* network-failure normalization. It owns no vendor-specific meaning and never
|
|
9
9
|
* invents an auth scheme, because the same 403 means an ungranted
|
|
@@ -280,15 +280,6 @@ async function jsonResult(read) {
|
|
|
280
280
|
return { parseError: cause };
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
-
// Each response a mapper reads, and the ledger entry of the request it answered.
|
|
284
|
-
const answeredRequests = new WeakMap();
|
|
285
|
-
/**
|
|
286
|
-
* Test-only legacy response lookup for renewal-provenance fixtures outside
|
|
287
|
-
* the kit. Production renewal uses the kit ledger. Undefined for other objects.
|
|
288
|
-
*/
|
|
289
|
-
export function answeredRequest(response) {
|
|
290
|
-
return answeredRequests.get(response);
|
|
291
|
-
}
|
|
292
283
|
/**
|
|
293
284
|
* Build the transport a connector sends every request through. `maxResponseBytes`
|
|
294
285
|
* is required rather than defaulted: what counts as an absurd response is a fact
|
|
@@ -463,7 +454,6 @@ export function guardedFetch(options) {
|
|
|
463
454
|
throw oversized(provider, limit, `a declared ${declared} bytes`);
|
|
464
455
|
}
|
|
465
456
|
const answer = boundedResponse(provider, response, limit, signal);
|
|
466
|
-
answeredRequests.set(answer, dispatch);
|
|
467
457
|
const abort = () => {
|
|
468
458
|
void response?.body?.cancel().catch(() => { });
|
|
469
459
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { wrapViewCredentials } from "../runtime/call.js";
|
|
2
|
+
import { sha256Hex, utf8Length } from "../bytes.js";
|
|
1
3
|
import { withStoredAuth } from "../auth/stored-observation.js";
|
|
2
4
|
import { authorizationParamsFor } from "../auth/oauth-authorization-params.js";
|
|
3
5
|
import { authView } from "../auth/native-call.js";
|
|
@@ -6,12 +8,12 @@ import { isOAuthCredentialRequest } from "../auth/oauth-token-response.js";
|
|
|
6
8
|
import { deriveContext } from "../derived-context.js";
|
|
7
9
|
import { operationContext, openRequest, viewServices, viewSignal } from "../runtime/call.js";
|
|
8
10
|
import { assertRemoteOAuthClient, declareSelfHostedClient, downstreamClientMetadata, downstreamRedirectUri, remoteClientAuthMethod, selfHostedClientUrl, trackRemoteClientRequest, } from "../auth/downstream-client-metadata.js";
|
|
9
|
-
import { AuthorizationServerMismatchError, Client, InsecureTokenEndpointError, InsufficientScopeError, IssuerMismatchError, MissingRequiredClientCapabilityError, OAuthClientFlowError, OAuthError, ProtocolError, RegistrationRejectedError, ResourceNotFoundError, SdkError, SdkErrorCode, SdkHttpError, SseError, UnsupportedProtocolVersionError, UrlElicitationRequiredError, isInputRequiredResult, fromJsonSchema, isJSONRPCErrorResponse, isJSONRPCNotification, specTypeSchemas, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
11
|
+
import { AuthorizationServerMismatchError, Client, InsecureTokenEndpointError, InsufficientScopeError, IssuerMismatchError, MissingRequiredClientCapabilityError, OAuthClientFlowError, OAuthError, ProtocolError, RegistrationRejectedError, ResourceNotFoundError, SdkError, SdkErrorCode, SdkHttpError, SseError, UnsupportedProtocolVersionError, UrlElicitationRequiredError, isInputRequiredResult, fromJsonSchema, isJSONRPCErrorResponse, isJSONRPCNotification, extractWWWAuthenticateParams, specTypeSchemas, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
10
12
|
import { Deferred, Duration, Effect, Exit, Scope } from "effect";
|
|
11
13
|
import { assertOAuthScope, authorizingContext, codeExchangeResponse, KvOAuthProvider, OAuthRequestNotSentError, refreshCoordinatorsByPartition, } from "../auth/downstream-oauth.js";
|
|
12
14
|
import { trackOAuthStartReset } from "../auth/oauth-start-reset.js";
|
|
13
15
|
import { byteReadResponse } from "../byte-read-response.js";
|
|
14
|
-
import { redactCatalog, redactSentSecrets, requestSentSecrets, sentSecretsFor, shortSecretWarning,
|
|
16
|
+
import { redactCatalog, redactSentSecrets, requestSentSecrets, sentSecretsFor, shortSecretWarning, } from "../sent-secrets.js";
|
|
15
17
|
import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "../catalog-limits.js";
|
|
16
18
|
import { catalogClientOptions, catalogItems, observeCompletedCatalogRefresh, closeCatalogCacheScope, observeCatalogChange, observeCatalogFetch, observeCatalogExpiry, } from "../catalog-cache.js";
|
|
17
19
|
import { connectorScopeCleanupClaimed } from "../connector-scope.js";
|
|
@@ -28,7 +30,7 @@ import { registerInvocationAuth } from "../invocation-auth.js";
|
|
|
28
30
|
import { downstreamInputCapabilities, assertDownstreamOutputSafe, downstreamWriteContinuation, } from "../downstream-input-context.js";
|
|
29
31
|
import { detach, runEdge } from "../runtime/run.js";
|
|
30
32
|
import { assertKnownOptions, keys, optionsOf, strings } from "../config-schema.js";
|
|
31
|
-
import { requestPrincipalFor } from "../request-principal.js";
|
|
33
|
+
import { requestPrincipalFor, tagPrincipalForwarding } from "../request-principal.js";
|
|
32
34
|
import { describedEndpoint, describedUrl } from "../described.js";
|
|
33
35
|
import { retryAfterMs } from "../http-policy.js";
|
|
34
36
|
import { resourceUriMatchesTemplates } from "./resource-uri.js";
|
|
@@ -115,32 +117,11 @@ const MAX_SKILL_BYTES = 16 * 1024 * 1024;
|
|
|
115
117
|
// Leave room for the envelope while accepting every file up to the spec limit.
|
|
116
118
|
const MAX_SKILL_READ_RPC_BYTES = 6 * MAX_SKILL_BYTES + 1024 * 1024;
|
|
117
119
|
/** Count UTF-8 without allocating another full copy of a bounded response. */
|
|
118
|
-
function skillTextBytes(value) {
|
|
119
|
-
let bytes = 0;
|
|
120
|
-
for (let i = 0; i < value.length; i++) {
|
|
121
|
-
const code = value.charCodeAt(i);
|
|
122
|
-
if (code < 0x80)
|
|
123
|
-
bytes++;
|
|
124
|
-
else if (code < 0x800)
|
|
125
|
-
bytes += 2;
|
|
126
|
-
else if (code >= 0xd800 &&
|
|
127
|
-
code <= 0xdbff &&
|
|
128
|
-
i + 1 < value.length &&
|
|
129
|
-
value.charCodeAt(i + 1) >= 0xdc00 &&
|
|
130
|
-
value.charCodeAt(i + 1) <= 0xdfff) {
|
|
131
|
-
bytes += 4;
|
|
132
|
-
i++;
|
|
133
|
-
}
|
|
134
|
-
else
|
|
135
|
-
bytes += 3;
|
|
136
|
-
}
|
|
137
|
-
return bytes;
|
|
138
|
-
}
|
|
139
120
|
function skillObject(value) {
|
|
140
121
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
141
122
|
}
|
|
142
123
|
function skillJsonBytes(value) {
|
|
143
|
-
return
|
|
124
|
+
return utf8Length(JSON.stringify(value));
|
|
144
125
|
}
|
|
145
126
|
function validSkill(value) {
|
|
146
127
|
if (!skillObject(value) ||
|
|
@@ -229,9 +210,7 @@ function skillReadSchema(uri) {
|
|
|
229
210
|
return { issues: [{ message: "Unexpected skill resource contents." }] };
|
|
230
211
|
}
|
|
231
212
|
const mimeType = typeof content.mimeType === "string" ? { mimeType: content.mimeType } : {};
|
|
232
|
-
if (typeof content.text === "string" &&
|
|
233
|
-
!("blob" in content) &&
|
|
234
|
-
skillTextBytes(content.text) <= MAX_SKILL_BYTES) {
|
|
213
|
+
if (typeof content.text === "string" && !("blob" in content) && utf8Length(content.text) <= MAX_SKILL_BYTES) {
|
|
235
214
|
return { value: [{ uri, ...mimeType, text: content.text }] };
|
|
236
215
|
}
|
|
237
216
|
if (typeof content.blob === "string" &&
|
|
@@ -668,18 +647,6 @@ function base64Utf8(value) {
|
|
|
668
647
|
}
|
|
669
648
|
return btoa(binary);
|
|
670
649
|
}
|
|
671
|
-
/**
|
|
672
|
-
* Hex SHA-256 of a credential, used only to notice that a cached client
|
|
673
|
-
* connected with a value the vault no longer holds. WebCrypto rather than a
|
|
674
|
-
* `node:` hash: the whole core has to keep running unchanged on Workers.
|
|
675
|
-
*/
|
|
676
|
-
async function digestOf(value) {
|
|
677
|
-
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
678
|
-
let hex = "";
|
|
679
|
-
for (const byte of bytes)
|
|
680
|
-
hex += byte.toString(16).padStart(2, "0");
|
|
681
|
-
return hex;
|
|
682
|
-
}
|
|
683
650
|
/**
|
|
684
651
|
* Assemble the one header a credential-auth connector sends.
|
|
685
652
|
*
|
|
@@ -1132,13 +1099,16 @@ export function remoteMcp(id, opts) {
|
|
|
1132
1099
|
// A closed scope keeps its (emptied) entry, so a late or future lookup finds
|
|
1133
1100
|
// it closed rather than recreating an ownerless connection under it.
|
|
1134
1101
|
const states = new WeakMap();
|
|
1102
|
+
// Post-connect auth checks keep the exact provider that bound this client,
|
|
1103
|
+
// even if another attempt has since replaced the request scope's state.
|
|
1104
|
+
const connectedProviders = new WeakMap();
|
|
1135
1105
|
const cacheOwner = {};
|
|
1136
1106
|
// A cached transport may dispatch concurrent calls and listings. Register
|
|
1137
1107
|
// its actual sent auth with every operation using that client, including
|
|
1138
1108
|
// handshake/discovery requests and OAuth rotations. Remove settled users.
|
|
1139
1109
|
const activeContexts = new WeakMap();
|
|
1140
1110
|
// Resource headers are non-secret context by contract. They never join the
|
|
1141
|
-
// sent set, so arguments and results equal to them pass unchanged
|
|
1111
|
+
// sent set, so arguments and results equal to them pass unchanged.
|
|
1142
1112
|
// An argument mirrored as `Mcp-Param-*` carrying a resource value is the
|
|
1143
1113
|
// same context under a name the tracker may read as credential-shaped.
|
|
1144
1114
|
const resourceValues = new Set(Object.values(extraHeaders ?? {}));
|
|
@@ -1451,11 +1421,22 @@ export function remoteMcp(id, opts) {
|
|
|
1451
1421
|
}
|
|
1452
1422
|
}));
|
|
1453
1423
|
adapted.onUnauthorized = authMethod((ctx) => authCall(flow, [
|
|
1454
|
-
authMethod((trace) =>
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1424
|
+
authMethod((trace) => authFlow(function* () {
|
|
1425
|
+
// This seam receives every 401, during status, starts, and live
|
|
1426
|
+
// calls. Use the SDK's parser so the check covers exactly the
|
|
1427
|
+
// challenged URL it would honor. The provider separately drops
|
|
1428
|
+
// foreign cached resource metadata on every SDK discovery read,
|
|
1429
|
+
// including callbacks and flows without a challenge.
|
|
1430
|
+
const { resourceMetadataUrl } = extractWWWAuthenticateParams(ctx.response);
|
|
1431
|
+
if (resourceMetadataUrl && resourceMetadataUrl.origin !== destination.origin) {
|
|
1432
|
+
throw new RemoteMcpDestinationError(id, "resource metadata outside the configured resource origin is not allowed");
|
|
1433
|
+
}
|
|
1434
|
+
yield authInvoke(Reflect, "apply", [
|
|
1435
|
+
onUnauthorized,
|
|
1436
|
+
adapted,
|
|
1437
|
+
[{ ...ctx, fetchFn: trace(ctx.fetchFn ?? fetch) }],
|
|
1438
|
+
]);
|
|
1439
|
+
})),
|
|
1459
1440
|
renewalSignal(ctx.response),
|
|
1460
1441
|
]));
|
|
1461
1442
|
internals._stepUpAuthorize = authMethod((challenge, retries) => authCall(flow, [
|
|
@@ -1487,6 +1468,16 @@ export function remoteMcp(id, opts) {
|
|
|
1487
1468
|
super("auth_required", message);
|
|
1488
1469
|
}
|
|
1489
1470
|
}
|
|
1471
|
+
/**
|
|
1472
|
+
* A request resolver threw something untyped. Calls still fail with
|
|
1473
|
+
* `auth_required`, but status reports an error: the resolver's own outage
|
|
1474
|
+
* is not an authorization the operator can grant.
|
|
1475
|
+
*/
|
|
1476
|
+
class UnresolvedTokenError extends ConnectorCallError {
|
|
1477
|
+
constructor() {
|
|
1478
|
+
super("auth_required", "The request token could not be resolved.");
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1490
1481
|
/**
|
|
1491
1482
|
* Read this request's credential. Called before every connect and before
|
|
1492
1483
|
* trusting any cached client, so an operator's replacement is picked up
|
|
@@ -1502,7 +1493,7 @@ export function remoteMcp(id, opts) {
|
|
|
1502
1493
|
catch (error) {
|
|
1503
1494
|
if (error instanceof ConnectorCallError)
|
|
1504
1495
|
throw error;
|
|
1505
|
-
throw new
|
|
1496
|
+
throw new UnresolvedTokenError();
|
|
1506
1497
|
}
|
|
1507
1498
|
ctx.signal?.throwIfAborted();
|
|
1508
1499
|
if (typeof value !== "string" ||
|
|
@@ -1781,7 +1772,13 @@ export function remoteMcp(id, opts) {
|
|
|
1781
1772
|
},
|
|
1782
1773
|
}
|
|
1783
1774
|
: {}),
|
|
1784
|
-
}), (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), metadataUrl, oauthScope, undefined, {
|
|
1775
|
+
}), (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), metadataUrl, oauthScope, undefined, {
|
|
1776
|
+
name: ctx.oauthClientName,
|
|
1777
|
+
client: staticClient,
|
|
1778
|
+
secrets: sentSecretsFor(ctx),
|
|
1779
|
+
authorizationParams,
|
|
1780
|
+
resourceOrigin: destination.origin,
|
|
1781
|
+
});
|
|
1785
1782
|
if (state)
|
|
1786
1783
|
state.provider = provider;
|
|
1787
1784
|
return provider;
|
|
@@ -1811,6 +1808,7 @@ export function remoteMcp(id, opts) {
|
|
|
1811
1808
|
headers.set(name, value);
|
|
1812
1809
|
return { ...init, headers };
|
|
1813
1810
|
};
|
|
1811
|
+
const responseOwner = Context.get(viewServices(ctx), RequestScope);
|
|
1814
1812
|
const initiatingSignal = ctx.signal;
|
|
1815
1813
|
ctx = deriveContext(operationContext(ctx), signal ? { signal } : {});
|
|
1816
1814
|
if (opts._transportFactory)
|
|
@@ -1897,6 +1895,12 @@ export function remoteMcp(id, opts) {
|
|
|
1897
1895
|
return writeContinuation ? send(input, { ...init, redirect: "manual" }) : send(input, init);
|
|
1898
1896
|
})();
|
|
1899
1897
|
return pending.then((response) => {
|
|
1898
|
+
// RPC fetch adapters can return a disposable Response. Own it at
|
|
1899
|
+
// the physical Promise leaf, including a late answer after close,
|
|
1900
|
+
// and keep its body live until the existing request boundary.
|
|
1901
|
+
const dispose = response[Symbol.dispose];
|
|
1902
|
+
if (dispose)
|
|
1903
|
+
responseOwner.own(() => dispose.call(response));
|
|
1900
1904
|
if (entry)
|
|
1901
1905
|
recordProviderResponse(entry, response.status);
|
|
1902
1906
|
if (flight?.write) {
|
|
@@ -2140,7 +2144,7 @@ export function remoteMcp(id, opts) {
|
|
|
2140
2144
|
credentialFramed = credentialHeaderValue(credentialScheme, credentialValue);
|
|
2141
2145
|
// The principal joins the client's identity: another principal never
|
|
2142
2146
|
// reuses this client, even if its resolver returns the same token.
|
|
2143
|
-
credentialDigest = (yield authCall(
|
|
2147
|
+
credentialDigest = (yield authCall(sha256Hex, [
|
|
2144
2148
|
principal ? JSON.stringify([principal, credentialValue]) : credentialValue,
|
|
2145
2149
|
]));
|
|
2146
2150
|
if ((state.client || state.connecting) &&
|
|
@@ -2265,7 +2269,7 @@ export function remoteMcp(id, opts) {
|
|
|
2265
2269
|
clientName: ctx.oauthClientName,
|
|
2266
2270
|
}
|
|
2267
2271
|
: undefined;
|
|
2268
|
-
const negotiationDigest = yield* promised(authMethod(() => authCall(
|
|
2272
|
+
const negotiationDigest = yield* promised(authMethod(() => authCall(sha256Hex, [
|
|
2269
2273
|
JSON.stringify([
|
|
2270
2274
|
opts.url,
|
|
2271
2275
|
opts.versionNegotiation ?? "auto",
|
|
@@ -2312,7 +2316,7 @@ export function remoteMcp(id, opts) {
|
|
|
2312
2316
|
provider
|
|
2313
2317
|
? authMethod(() => authFlow(function* () {
|
|
2314
2318
|
const tokens = yield authInvoke(provider, "tokens", []);
|
|
2315
|
-
return yield authCall(
|
|
2319
|
+
return yield authCall(sha256Hex, [JSON.stringify(tokens)]);
|
|
2316
2320
|
}))
|
|
2317
2321
|
: undefined,
|
|
2318
2322
|
])));
|
|
@@ -2396,6 +2400,8 @@ export function remoteMcp(id, opts) {
|
|
|
2396
2400
|
}
|
|
2397
2401
|
}
|
|
2398
2402
|
state.client = c;
|
|
2403
|
+
if (provider)
|
|
2404
|
+
connectedProviders.set(c, provider);
|
|
2399
2405
|
state.connectedGeneration = genAtStart;
|
|
2400
2406
|
state.authRequired = false;
|
|
2401
2407
|
const discover = c.getDiscoverResult();
|
|
@@ -2464,8 +2470,8 @@ export function remoteMcp(id, opts) {
|
|
|
2464
2470
|
* Read after connecting: connect has bound the epoch, migrated an older
|
|
2465
2471
|
* layout, and refused a disconnected one within the scope's lifetime.
|
|
2466
2472
|
*/
|
|
2467
|
-
const granted = authMethod((
|
|
2468
|
-
return Boolean(yield authInvoke(
|
|
2473
|
+
const granted = authMethod((provider) => authFlow(function* () {
|
|
2474
|
+
return Boolean(yield authInvoke(provider, "tokens", []));
|
|
2469
2475
|
}));
|
|
2470
2476
|
/**
|
|
2471
2477
|
* Start consent for a downstream that answered without challenging. The
|
|
@@ -2474,7 +2480,7 @@ export function remoteMcp(id, opts) {
|
|
|
2474
2480
|
* well-known URLs, the same issuer and resource checks, consent state, and
|
|
2475
2481
|
* CIMD/DCR/static client order. Nothing is sent to the MCP endpoint.
|
|
2476
2482
|
*/
|
|
2477
|
-
const startUnchallenged = authMethod((ctx) => authFlow(function* () {
|
|
2483
|
+
const startUnchallenged = authMethod((ctx, connectedProvider) => authFlow(function* () {
|
|
2478
2484
|
const required = {
|
|
2479
2485
|
state: "auth_required",
|
|
2480
2486
|
message: "Authorization required \u2014 open the URL to connect.",
|
|
@@ -2482,10 +2488,12 @@ export function remoteMcp(id, opts) {
|
|
|
2482
2488
|
// A test transport carries no OAuth flow to start.
|
|
2483
2489
|
if (opts._transportFactory)
|
|
2484
2490
|
return required;
|
|
2485
|
-
//
|
|
2486
|
-
//
|
|
2491
|
+
// A cached connection may belong to a passive status or call. Give this
|
|
2492
|
+
// explicit start its own authorization-capable provider without widening
|
|
2493
|
+
// the transport's authority or adopting a Restart's replacement epoch.
|
|
2494
|
+
const epoch = (yield authInvoke(connectedProvider, "flowEpoch", []));
|
|
2487
2495
|
const provider = newProvider(ctx);
|
|
2488
|
-
(yield authInvoke(provider, "beginFlow", []));
|
|
2496
|
+
(yield authInvoke(provider, "beginFlow", [epoch]));
|
|
2489
2497
|
// Never started, this transport holds no session or stream to close.
|
|
2490
2498
|
const transport = buildTransport(ctx, provider);
|
|
2491
2499
|
const internals = transport;
|
|
@@ -2967,6 +2975,8 @@ export function remoteMcp(id, opts) {
|
|
|
2967
2975
|
return yield authView(ctx, authMethod(() => authValue(() => closing)), true);
|
|
2968
2976
|
})),
|
|
2969
2977
|
status: authMethod((ctx) => authFlow(function* () {
|
|
2978
|
+
if (principalPolicy && !callerOf(ctx)?.authenticated)
|
|
2979
|
+
return ownStatus({ state: "per_caller", message: "Authenticates as each caller; verified on use." });
|
|
2970
2980
|
const state = stateFor(ctx);
|
|
2971
2981
|
const report = authMethod((status) => authFlow(function* () {
|
|
2972
2982
|
const path = isOauth
|
|
@@ -2979,10 +2989,10 @@ export function remoteMcp(id, opts) {
|
|
|
2979
2989
|
});
|
|
2980
2990
|
}));
|
|
2981
2991
|
try {
|
|
2982
|
-
(yield authCall(ensureConnected, [ctx, state]));
|
|
2992
|
+
const client = (yield authCall(ensureConnected, [ctx, state]));
|
|
2983
2993
|
// The grant decides, as it does for a call: a downstream that
|
|
2984
2994
|
// answers without credentials is not connected for this owner.
|
|
2985
|
-
if (isOauth && !(yield authCall(granted, [
|
|
2995
|
+
if (isOauth && !(yield authCall(granted, [connectedProviders.get(client)])))
|
|
2986
2996
|
return (yield authCall(report, [
|
|
2987
2997
|
{ state: "auth_required", message: "Authorization required \u2014 open the URL to connect." },
|
|
2988
2998
|
]));
|
|
@@ -2995,7 +3005,10 @@ export function remoteMcp(id, opts) {
|
|
|
2995
3005
|
if (err instanceof OperatorDisconnectedError) {
|
|
2996
3006
|
return (yield authCall(report, [{ state: "auth_required", message: err.message }]));
|
|
2997
3007
|
}
|
|
2998
|
-
if (state.authRequired
|
|
3008
|
+
if (state.authRequired ||
|
|
3009
|
+
(err instanceof ConnectorCallError &&
|
|
3010
|
+
err.code === "auth_required" &&
|
|
3011
|
+
!(err instanceof UnresolvedTokenError))) {
|
|
2999
3012
|
return (yield authCall(report, [
|
|
3000
3013
|
{
|
|
3001
3014
|
state: "auth_required",
|
|
@@ -3008,7 +3021,9 @@ export function remoteMcp(id, opts) {
|
|
|
3008
3021
|
return failureStatus(id, err);
|
|
3009
3022
|
}
|
|
3010
3023
|
})),
|
|
3011
|
-
|
|
3024
|
+
};
|
|
3025
|
+
if (isOauth) {
|
|
3026
|
+
connector.finishAuth = authMethod((code, ctx, callbackParams) => authView(ctx, authMethod((signal) => authFlow(function* () {
|
|
3012
3027
|
const state = stateFor(ctx);
|
|
3013
3028
|
const provider = newProvider(ctx, state);
|
|
3014
3029
|
const consent = callbackParams?.get("state") ?? null;
|
|
@@ -3041,9 +3056,7 @@ export function remoteMcp(id, opts) {
|
|
|
3041
3056
|
finally {
|
|
3042
3057
|
detach(closeConnection(null, t, ctx.logger));
|
|
3043
3058
|
}
|
|
3044
|
-
})), true))
|
|
3045
|
-
};
|
|
3046
|
-
if (opts.auth?.type === "oauth") {
|
|
3059
|
+
})), true));
|
|
3047
3060
|
connector.verifyState = authMethod((oauthState, ctx) => authFlow(function* () {
|
|
3048
3061
|
const state = stateFor(ctx);
|
|
3049
3062
|
return (yield authInvoke(newProvider(ctx, state), "verifyState", [oauthState]));
|
|
@@ -3090,18 +3103,27 @@ export function remoteMcp(id, opts) {
|
|
|
3090
3103
|
if (ctx.signal?.aborted)
|
|
3091
3104
|
throw ctx.signal.reason;
|
|
3092
3105
|
try {
|
|
3106
|
+
// A passive call's refusal stays latched for status and calls. A
|
|
3107
|
+
// successful connection with no tokens still permits explicit
|
|
3108
|
+
// consent, bound to that connection rather than the live grant.
|
|
3109
|
+
if (state.authRequired && state.client) {
|
|
3110
|
+
const provider = connectedProviders.get(state.client);
|
|
3111
|
+
if (!(yield authCall(granted, [provider])))
|
|
3112
|
+
return (yield authCall(startUnchallenged, [ctx, provider]));
|
|
3113
|
+
}
|
|
3093
3114
|
// The handshake runs first, so a 401 challenge's
|
|
3094
3115
|
// `resource_metadata` starts consent as it always has.
|
|
3095
|
-
(yield authCall(ensureConnected, [ctx, state]));
|
|
3096
|
-
|
|
3097
|
-
|
|
3116
|
+
const client = (yield authCall(ensureConnected, [ctx, state]));
|
|
3117
|
+
const provider = connectedProviders.get(client);
|
|
3118
|
+
if (!(yield authCall(granted, [provider])))
|
|
3119
|
+
return (yield authCall(startUnchallenged, [ctx, provider]));
|
|
3098
3120
|
return {
|
|
3099
3121
|
state: "ok",
|
|
3100
3122
|
message: "Already authorized \u2014 connection is healthy.",
|
|
3101
3123
|
};
|
|
3102
3124
|
}
|
|
3103
3125
|
catch (err) {
|
|
3104
|
-
if (state.authRequired) {
|
|
3126
|
+
if (state.authRequired && requiresAuthorization(err)) {
|
|
3105
3127
|
let authorizationUrl;
|
|
3106
3128
|
try {
|
|
3107
3129
|
authorizationUrl = (yield state.attemptProvider
|
|
@@ -3131,7 +3153,7 @@ export function remoteMcp(id, opts) {
|
|
|
3131
3153
|
}
|
|
3132
3154
|
}));
|
|
3133
3155
|
const withActiveSecrets = authMethod((ctx, run, redactResult = true, preserveInput = false) => authFlow(function* () {
|
|
3134
|
-
|
|
3156
|
+
wrapViewCredentials(ctx);
|
|
3135
3157
|
const secrets = sentSecretsFor(ctx);
|
|
3136
3158
|
if (opts.auth?.type === "headers") {
|
|
3137
3159
|
for (const value of Object.values(opts.auth.headers))
|
|
@@ -3272,5 +3294,7 @@ export function remoteMcp(id, opts) {
|
|
|
3272
3294
|
})), 0), 0));
|
|
3273
3295
|
if (isOauth)
|
|
3274
3296
|
withStoredAuth(connector, (ctx) => newProvider(ctx).storedAuthorization());
|
|
3297
|
+
if (principalPolicy)
|
|
3298
|
+
tagPrincipalForwarding(connector);
|
|
3275
3299
|
return connector;
|
|
3276
3300
|
}
|
|
@@ -26,10 +26,6 @@ export function resourceUriMatchesTemplates(uri, templates, note = () => { }) {
|
|
|
26
26
|
}
|
|
27
27
|
return matched ? { matched } : { matched, ...(refusal ? { refusal } : {}) };
|
|
28
28
|
}
|
|
29
|
-
export function resourceUriMatchesTemplate(uri, template) {
|
|
30
|
-
const parsed = parse(template);
|
|
31
|
-
return typeof parsed === "object" && matches(uri, parsed);
|
|
32
|
-
}
|
|
33
29
|
function parse(template) {
|
|
34
30
|
if (template.length > MAX_URI_LENGTH)
|
|
35
31
|
return;
|
package/dist/credentials.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { advanceCatalogRevision } from "./catalog-revision.js";
|
|
1
2
|
import { deriveOAuthHandoffKey, deriveRequestStateKey } from "./oauth-sealing.js";
|
|
2
3
|
import { credentialKeys } from "./storage/keys.js";
|
|
3
4
|
const KEY_BYTES = 32;
|
|
@@ -201,10 +202,12 @@ export class CredentialVault {
|
|
|
201
202
|
ciphertext: bytesToBase64(new Uint8Array(ciphertext)),
|
|
202
203
|
};
|
|
203
204
|
await this.storage.set(storageKey(connectorId, owner), JSON.stringify(envelope));
|
|
205
|
+
advanceCatalogRevision(this);
|
|
204
206
|
return (await this.metadata(connectorId, owner));
|
|
205
207
|
}
|
|
206
208
|
async delete(connectorId, owner) {
|
|
207
209
|
await this.storage.delete(storageKey(connectorId, owner));
|
|
210
|
+
advanceCatalogRevision(this);
|
|
208
211
|
}
|
|
209
212
|
/** Encrypt downstream OAuth state under the vault key; nothing is stored. */
|
|
210
213
|
async seal(connectorId, purpose, plaintext, owner) {
|