@zackbart/connecta 0.38.0 → 0.40.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 +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +7 -4
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +2 -4
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
package/dist/auth/native.js
CHANGED
|
@@ -40,6 +40,23 @@ export function registerAuthMethod(prototype, key, program, own) {
|
|
|
40
40
|
entries.set(key, program);
|
|
41
41
|
registerAuth(own, program);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Define each program once as `prototype[key]`: the native body owned
|
|
45
|
+
* programs compose by identity, and its Promise edge for SDK/custom callers,
|
|
46
|
+
* installed with a class method's name and property attributes. A class
|
|
47
|
+
* declares the edge's type with a `declare` member.
|
|
48
|
+
*/
|
|
49
|
+
export function authMethods(prototype, entries) {
|
|
50
|
+
for (const [key, program] of Object.entries(entries)) {
|
|
51
|
+
const edge = {
|
|
52
|
+
[key](...args) {
|
|
53
|
+
return runEdge(authOwnMethod(prototype, this, key, args));
|
|
54
|
+
},
|
|
55
|
+
}[key];
|
|
56
|
+
Object.defineProperty(prototype, key, { value: edge, writable: true, enumerable: false, configurable: true });
|
|
57
|
+
registerAuthMethod(prototype, key, program, edge);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
43
60
|
export function authOwnMethod(prototype, receiver, key, args) {
|
|
44
61
|
return Effect.suspend(() => methods.get(prototype).get(key).apply(receiver, args)).pipe(Effect.catchDefect(Effect.fail));
|
|
45
62
|
}
|
|
@@ -100,6 +117,26 @@ export function authStorage(storage, key, args, beforeSubmit) {
|
|
|
100
117
|
return Effect.flatMap(Effect.serviceOption(AuthenticationWork), (work) => work._tag === "Some" ? authDetached(submitted) : submitted);
|
|
101
118
|
});
|
|
102
119
|
}
|
|
120
|
+
/** Optional fresh token snapshot, with the same request ownership as an auth storage read. */
|
|
121
|
+
export function authAccessToken(storage, hash) {
|
|
122
|
+
return Effect.suspend(() => {
|
|
123
|
+
const read = getNativeStorage(storage)?.accessToken;
|
|
124
|
+
if (!read)
|
|
125
|
+
return Effect.succeed(undefined);
|
|
126
|
+
const operation = authValue(() => read(hash));
|
|
127
|
+
return Effect.flatMap(Effect.serviceOption(AuthenticationWork), (work) => work._tag === "Some" ? authDetached(operation) : operation);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Several keys in one round trip when the store can, else one `get` at a time. */
|
|
131
|
+
export function authStorageMany(storage, keys) {
|
|
132
|
+
return Effect.suspend(() => {
|
|
133
|
+
const getMany = getNativeStorage(storage)?.getMany;
|
|
134
|
+
if (!getMany)
|
|
135
|
+
return Effect.forEach(keys, (key) => authStorage(storage, "get", [key]));
|
|
136
|
+
const operation = authValue(() => getMany(keys));
|
|
137
|
+
return Effect.flatMap(Effect.serviceOption(AuthenticationWork), (work) => work._tag === "Some" ? authDetached(operation) : operation);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
103
140
|
function authSubmission(operation, beforeSubmit) {
|
|
104
141
|
return Effect.flatMap(Scheduler.Scheduler, (parent) => {
|
|
105
142
|
let submitting = false;
|
package/dist/auth/oauth-grant.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/** How long before `expiresAt` a call refreshes instead of sending the token. */
|
|
2
|
+
export const OAUTH_REFRESH_SKEW_MS = 60_000;
|
|
3
|
+
/**
|
|
4
|
+
* The expiry a token response's `expires_in` gives, counted from `issuedAt`.
|
|
5
|
+
* Zero is a token already expired, never a missing expiry: the next call
|
|
6
|
+
* refreshes before sending it.
|
|
7
|
+
*/
|
|
8
|
+
export function tokenExpiry(tokens, issuedAt) {
|
|
9
|
+
const lifetime = tokens.expires_in;
|
|
10
|
+
return typeof lifetime === "number" && Number.isFinite(lifetime) && lifetime >= 0
|
|
11
|
+
? issuedAt + lifetime * 1000
|
|
12
|
+
: undefined;
|
|
13
|
+
}
|
|
1
14
|
function plainObject(value) {
|
|
2
15
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
16
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { routePhrases } from "../agent-routes.js";
|
|
1
2
|
import { withStoredAuth } from "./stored-observation.js";
|
|
2
3
|
import { auth, OAuthError, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
3
4
|
import { Effect } from "effect";
|
|
@@ -15,9 +16,9 @@ import { SharedRead } from "../runtime/shared-read.js";
|
|
|
15
16
|
import { sentSecretsFetch, sentSecretsFor, shortSecretWarning } from "../sent-secrets.js";
|
|
16
17
|
import { unsentAfterUnauthorized } from "../write-outcome.js";
|
|
17
18
|
import { trackRemoteClientRequest } from "./downstream-client-metadata.js";
|
|
18
|
-
import { assertOAuthScope, authorizingContext, codeExchangeResponse, KvOAuthProvider, refreshCoordinatorsByPartition, } from "./downstream-oauth.js";
|
|
19
|
+
import { assertOAuthScope, authorizingContext, codeExchangeResponse, EarlyRenewals, earlyRenewalFetch, KvOAuthProvider, refreshCoordinatorsByPartition, } from "./downstream-oauth.js";
|
|
19
20
|
import { authView } from "./native-call.js";
|
|
20
|
-
import { authCall, authFlow, authInvoke, authMethod,
|
|
21
|
+
import { authCall, authFlow, authInvoke, authMethod, authProgram, authValue, registerAuth, authMethods, } from "./native.js";
|
|
21
22
|
import { trackOAuthStartReset } from "./oauth-start-reset.js";
|
|
22
23
|
import { bindRenewableGrant, OAUTH_CHALLENGE_MAX_BYTES } from "./renewable-grant.js";
|
|
23
24
|
/**
|
|
@@ -226,14 +227,6 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
226
227
|
undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope, undefined, { secrets: sentSecretsFor(ctx), authorizationParams: settings.authorizationParams });
|
|
227
228
|
this.settings = settings;
|
|
228
229
|
}
|
|
229
|
-
/**
|
|
230
|
-
* The pre-registered client, answered from configuration. Its secret is
|
|
231
|
-
* never written to storage, sealed or otherwise: it is deployment config,
|
|
232
|
-
* not grant state, and an operator disconnect cannot delete it.
|
|
233
|
-
*/
|
|
234
|
-
clientInformation() {
|
|
235
|
-
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "clientInformation", []));
|
|
236
|
-
}
|
|
237
230
|
configuredAuthorizationBinding() {
|
|
238
231
|
return {
|
|
239
232
|
issuer: this.settings.identity,
|
|
@@ -264,26 +257,6 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
264
257
|
},
|
|
265
258
|
};
|
|
266
259
|
}
|
|
267
|
-
saveClientInformation() {
|
|
268
|
-
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "saveClientInformation", []));
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Discovery as a constant. Supplying both halves keeps the SDK from
|
|
272
|
-
* fetching either: no protected-resource probe of the API origin, and no
|
|
273
|
-
* metadata request to an authorization server that may not publish any.
|
|
274
|
-
* The metadata names no `issuer`, so the SDK binds grants to the configured
|
|
275
|
-
* token endpoint and does not demand an RFC 9207 `iss` it cannot know.
|
|
276
|
-
*/
|
|
277
|
-
discoveryState() {
|
|
278
|
-
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "discoveryState", []));
|
|
279
|
-
}
|
|
280
|
-
saveDiscoveryState() {
|
|
281
|
-
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "saveDiscoveryState", []));
|
|
282
|
-
}
|
|
283
|
-
/** No RFC 8707 `resource` parameter: a plain REST API names no resource. */
|
|
284
|
-
validateResourceURL() {
|
|
285
|
-
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "validateResourceURL", []));
|
|
286
|
-
}
|
|
287
260
|
/**
|
|
288
261
|
* The code exchange without `code_verifier`, for a flow whose authorization
|
|
289
262
|
* request carried no challenge. Undefined everywhere else, which hands the
|
|
@@ -302,53 +275,52 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
302
275
|
exchanging(code) {
|
|
303
276
|
this.exchangeCode = code;
|
|
304
277
|
}
|
|
305
|
-
redirectToAuthorization(authorizationUrl) {
|
|
306
|
-
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "redirectToAuthorization", [authorizationUrl]));
|
|
307
|
-
}
|
|
308
278
|
static {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
279
|
+
authMethods(StaticOAuthProvider.prototype, {
|
|
280
|
+
clientInformation: function () {
|
|
281
|
+
return authValue(() => this.configuredClientInformation());
|
|
282
|
+
},
|
|
283
|
+
saveClientInformation: function () {
|
|
284
|
+
// Nothing to save: the SDK only saves a client it registered or stamped.
|
|
285
|
+
return Effect.void;
|
|
286
|
+
},
|
|
287
|
+
discoveryState: function () {
|
|
288
|
+
return authValue(() => {
|
|
289
|
+
const { settings } = this;
|
|
290
|
+
const metadata = {
|
|
291
|
+
authorization_endpoint: settings.authorizationEndpoint.href,
|
|
292
|
+
token_endpoint: settings.tokenEndpoint.href,
|
|
293
|
+
response_types_supported: ["code"],
|
|
294
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
295
|
+
code_challenge_methods_supported: ["S256"],
|
|
296
|
+
token_endpoint_auth_methods_supported: [settings.clientAuthentication],
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
authorizationServerUrl: settings.identity,
|
|
300
|
+
authorizationServerMetadata: metadata,
|
|
301
|
+
resourceMetadata: { resource: settings.serverUrl.origin },
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
saveDiscoveryState: function () {
|
|
306
|
+
// Configuration is the discovery; there is nothing to remember.
|
|
307
|
+
return Effect.void;
|
|
308
|
+
},
|
|
309
|
+
validateResourceURL: function () {
|
|
310
|
+
return authValue(() => {
|
|
311
|
+
return undefined;
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
redirectToAuthorization: function (authorizationUrl) {
|
|
315
|
+
return authFlow(function* () {
|
|
316
|
+
if (!this.settings.pkce) {
|
|
317
|
+
authorizationUrl.searchParams.delete("code_challenge");
|
|
318
|
+
authorizationUrl.searchParams.delete("code_challenge_method");
|
|
319
|
+
}
|
|
320
|
+
(yield authCall(KvOAuthProvider.prototype.redirectToAuthorization, [authorizationUrl], this));
|
|
321
|
+
}.bind(this));
|
|
322
|
+
},
|
|
323
|
+
});
|
|
352
324
|
}
|
|
353
325
|
}
|
|
354
326
|
const AUTH_REQUIRED_MESSAGE = "Authorization required — open the URL to connect.";
|
|
@@ -469,62 +441,66 @@ export function staticOAuth(id, config) {
|
|
|
469
441
|
* rotation commits, and re-consent verdicts. Credential-bearing requests
|
|
470
442
|
* never follow redirects in either OAuth adapter.
|
|
471
443
|
*/
|
|
472
|
-
const runAuth = authMethod((provider, ctx, exchange, requestOwned = false
|
|
444
|
+
const runAuth = authMethod((provider, ctx, exchange, requestOwned = false,
|
|
445
|
+
/** The access token an early renewal replaces. */
|
|
446
|
+
early) => {
|
|
473
447
|
return authFlow(function* () {
|
|
474
448
|
const secrets = sentSecretsFor(ctx);
|
|
449
|
+
const coordinated = coordinatorFor(ctx).coordinatedFetch(provider, authMethod((input, init) => {
|
|
450
|
+
return authFlow(function* () {
|
|
451
|
+
if (exchange) {
|
|
452
|
+
// A code exchange has no submitted refresh claim transferring
|
|
453
|
+
// ownership. Only explicit scope revocation stops a callback:
|
|
454
|
+
// the production callback route remains uninterruptible and
|
|
455
|
+
// does not revoke its own scope while exchanging a code.
|
|
456
|
+
return (yield authView(ctx, authMethod((signal) => {
|
|
457
|
+
return authFlow(function* () {
|
|
458
|
+
assertProviderCallOpen(ctx, id, true);
|
|
459
|
+
const response = (yield authCall(tokenEndpointFetch, [
|
|
460
|
+
secrets,
|
|
461
|
+
input,
|
|
462
|
+
{
|
|
463
|
+
...init,
|
|
464
|
+
signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal,
|
|
465
|
+
},
|
|
466
|
+
]));
|
|
467
|
+
// An override can ignore abort. Never hand its late tokens
|
|
468
|
+
// to the SDK, which would otherwise persist both credentials.
|
|
469
|
+
if (signal.aborted) {
|
|
470
|
+
void response.body?.cancel().catch(() => { });
|
|
471
|
+
throw signal.reason;
|
|
472
|
+
}
|
|
473
|
+
// The SDK clones and reads this body after fetch resolves.
|
|
474
|
+
// Revocation must cancel that read even for a custom fetch.
|
|
475
|
+
return codeExchangeResponse(response, signal);
|
|
476
|
+
});
|
|
477
|
+
}), true));
|
|
478
|
+
}
|
|
479
|
+
return (yield authCall(tokenEndpointFetch, [secrets, input, init]));
|
|
480
|
+
});
|
|
481
|
+
}), viewSignal(ctx, requestOwned || exchange !== undefined), ctx.defer, undefined, () => {
|
|
482
|
+
guardedTransport(true);
|
|
483
|
+
});
|
|
475
484
|
return (yield authCall(auth, [
|
|
476
485
|
provider,
|
|
477
486
|
{
|
|
478
487
|
serverUrl: settings.serverUrl,
|
|
479
488
|
...exchange,
|
|
480
|
-
fetchFn:
|
|
481
|
-
return authFlow(function* () {
|
|
482
|
-
if (exchange) {
|
|
483
|
-
// A code exchange has no submitted refresh claim transferring
|
|
484
|
-
// ownership. Only explicit scope revocation stops a callback:
|
|
485
|
-
// the production callback route remains uninterruptible and
|
|
486
|
-
// does not revoke its own scope while exchanging a code.
|
|
487
|
-
return (yield authView(ctx, authMethod((signal) => {
|
|
488
|
-
return authFlow(function* () {
|
|
489
|
-
assertProviderCallOpen(ctx, id, true);
|
|
490
|
-
const response = (yield authCall(tokenEndpointFetch, [
|
|
491
|
-
secrets,
|
|
492
|
-
input,
|
|
493
|
-
{
|
|
494
|
-
...init,
|
|
495
|
-
signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal,
|
|
496
|
-
},
|
|
497
|
-
]));
|
|
498
|
-
// An override can ignore abort. Never hand its late tokens
|
|
499
|
-
// to the SDK, which would otherwise persist both credentials.
|
|
500
|
-
if (signal.aborted) {
|
|
501
|
-
void response.body?.cancel().catch(() => { });
|
|
502
|
-
throw signal.reason;
|
|
503
|
-
}
|
|
504
|
-
// The SDK clones and reads this body after fetch resolves.
|
|
505
|
-
// Revocation must cancel that read even for a custom fetch.
|
|
506
|
-
return codeExchangeResponse(response, signal);
|
|
507
|
-
});
|
|
508
|
-
}), true));
|
|
509
|
-
}
|
|
510
|
-
return (yield authCall(tokenEndpointFetch, [secrets, input, init]));
|
|
511
|
-
});
|
|
512
|
-
}), viewSignal(ctx, requestOwned || exchange !== undefined), ctx.defer, undefined, () => {
|
|
513
|
-
guardedTransport(true);
|
|
514
|
-
}),
|
|
489
|
+
fetchFn: early === undefined ? coordinated : earlyRenewalFetch(coordinated, early),
|
|
515
490
|
},
|
|
516
491
|
]));
|
|
517
492
|
});
|
|
518
493
|
});
|
|
519
494
|
// No cause, as in remoteMcp(): the `UnauthorizedError` class is the whole
|
|
520
495
|
// verdict, and a logger rendering the chain renders whatever it says.
|
|
521
|
-
const authRequiredError = () => new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization —
|
|
496
|
+
const authRequiredError = (ctx) => new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization — ${routePhrases(ctx).authorizeAndOpen(id)}.`);
|
|
522
497
|
const disconnectedMessage = `Connector "${id}" was disconnected by an operator — explicitly start authorization to reconnect it.`;
|
|
523
498
|
/**
|
|
524
|
-
* The token to send after `spent` drew a 401
|
|
525
|
-
* already rotated in, or else
|
|
499
|
+
* The token to send after `spent` drew a 401, or instead of an `early`
|
|
500
|
+
* one about to expire: one another request has already rotated in, or else
|
|
501
|
+
* one coordinated refresh.
|
|
526
502
|
*/
|
|
527
|
-
const renewedToken = authMethod((provider, ctx, spent, requestOwned = false) => {
|
|
503
|
+
const renewedToken = authMethod((provider, ctx, spent, requestOwned = false, early = false) => {
|
|
528
504
|
return authFlow(function* () {
|
|
529
505
|
// Only renewal whose claim CAS was submitted may survive this call.
|
|
530
506
|
// Race each preparation read too, and never resume SDK work or consume a
|
|
@@ -548,29 +524,42 @@ export function staticOAuth(id, config) {
|
|
|
548
524
|
return current.access_token;
|
|
549
525
|
if ((yield authCall(live, [
|
|
550
526
|
authMethod(() => {
|
|
551
|
-
return authCall(runAuth, [
|
|
527
|
+
return authCall(runAuth, [
|
|
528
|
+
provider,
|
|
529
|
+
ctx,
|
|
530
|
+
undefined,
|
|
531
|
+
requestOwned,
|
|
532
|
+
early ? spent : undefined,
|
|
533
|
+
]);
|
|
552
534
|
}),
|
|
553
535
|
])) !== "AUTHORIZED")
|
|
554
|
-
throw authRequiredError();
|
|
536
|
+
throw authRequiredError(ctx);
|
|
555
537
|
const refreshed = (yield authCall(live, [
|
|
556
538
|
authMethod(() => {
|
|
557
539
|
return authInvoke(provider, "tokens", [issuerContext]);
|
|
558
540
|
}),
|
|
559
541
|
]));
|
|
560
542
|
if (!refreshed)
|
|
561
|
-
throw authRequiredError();
|
|
543
|
+
throw authRequiredError(ctx);
|
|
562
544
|
return refreshed.access_token;
|
|
563
545
|
});
|
|
564
546
|
});
|
|
565
|
-
|
|
566
|
-
|
|
547
|
+
// A token that lives under a minute is due again at once: the invocation's
|
|
548
|
+
// check renews it, and the handler sends it while it is unexpired.
|
|
549
|
+
const earlyRenewed = new EarlyRenewals();
|
|
550
|
+
/**
|
|
551
|
+
* One call's grant use; submitted refresh work keeps the coordinator's owner.
|
|
552
|
+
* `renewAgain` is the invocation's check, which renews any due token,
|
|
553
|
+
* even one this request's earlier renewal produced.
|
|
554
|
+
*/
|
|
555
|
+
const grantFor = (ctx, requestOwned = false, renewAgain = false) => {
|
|
567
556
|
let provider;
|
|
568
557
|
const use = authMethod((work) => {
|
|
569
558
|
return authView(ctx, authMethod((signal) => {
|
|
570
559
|
return authFlow(function* () {
|
|
571
560
|
assertProviderCallOpen(ctx, id, requestOwned);
|
|
572
561
|
if (rejectedScopes.has(scopeOf(ctx)))
|
|
573
|
-
throw authRequiredError();
|
|
562
|
+
throw authRequiredError(ctx);
|
|
574
563
|
const live = deriveContext(ctx, { signal });
|
|
575
564
|
const release = retainOAuthPartition(oauthPartitionFor(live));
|
|
576
565
|
try {
|
|
@@ -591,7 +580,7 @@ export function staticOAuth(id, config) {
|
|
|
591
580
|
throw signal.reason;
|
|
592
581
|
if (error instanceof UnauthorizedError) {
|
|
593
582
|
rejectedScopes.add(scopeOf(ctx));
|
|
594
|
-
throw authRequiredError();
|
|
583
|
+
throw authRequiredError(ctx);
|
|
595
584
|
}
|
|
596
585
|
throw error;
|
|
597
586
|
}
|
|
@@ -604,11 +593,26 @@ export function staticOAuth(id, config) {
|
|
|
604
593
|
return {
|
|
605
594
|
token: authMethod(() => {
|
|
606
595
|
return authCall(use, [
|
|
607
|
-
authMethod((provider) => {
|
|
596
|
+
authMethod((provider, live) => {
|
|
608
597
|
return authFlow(function* () {
|
|
609
598
|
const tokens = (yield authInvoke(provider, "tokens", [issuerContext]));
|
|
610
599
|
if (!tokens)
|
|
611
|
-
throw authRequiredError();
|
|
600
|
+
throw authRequiredError(ctx);
|
|
601
|
+
// A token about to expire is renewed before anything is sent with it,
|
|
602
|
+
// so a write never meets the 401 it could not be repeated after.
|
|
603
|
+
const scope = scopeOf(ctx);
|
|
604
|
+
if (provider.renewalDue() === tokens.access_token &&
|
|
605
|
+
(renewAgain || !earlyRenewed.settled(scope, tokens.access_token))) {
|
|
606
|
+
const fresh = (yield authCall(renewedToken, [
|
|
607
|
+
provider,
|
|
608
|
+
live,
|
|
609
|
+
tokens.access_token,
|
|
610
|
+
requestOwned,
|
|
611
|
+
true,
|
|
612
|
+
]));
|
|
613
|
+
earlyRenewed.record(scope, tokens.access_token, fresh, provider.observedExpiry(fresh));
|
|
614
|
+
return fresh;
|
|
615
|
+
}
|
|
612
616
|
return tokens.access_token;
|
|
613
617
|
});
|
|
614
618
|
}),
|
|
@@ -625,7 +629,7 @@ export function staticOAuth(id, config) {
|
|
|
625
629
|
return authView(ctx, authMethod(() => {
|
|
626
630
|
return authValue(() => {
|
|
627
631
|
rejectedScopes.add(scopeOf(ctx));
|
|
628
|
-
throw authRequiredError();
|
|
632
|
+
throw authRequiredError(ctx);
|
|
629
633
|
});
|
|
630
634
|
}), requestOwned);
|
|
631
635
|
}),
|
|
@@ -755,12 +759,12 @@ export function staticOAuth(id, config) {
|
|
|
755
759
|
// renewed nothing.
|
|
756
760
|
renewedAfterUnauthorized(dispatch, stale, fresh);
|
|
757
761
|
if (fresh === stale)
|
|
758
|
-
renewal = authRequiredError();
|
|
762
|
+
renewal = authRequiredError(ctx);
|
|
759
763
|
}
|
|
760
764
|
catch (error) {
|
|
761
765
|
if (error instanceof UnauthorizedError)
|
|
762
766
|
rejectedScopes.add(scopeOf(ctx));
|
|
763
|
-
renewal = error instanceof UnauthorizedError ? authRequiredError() : error;
|
|
767
|
+
renewal = error instanceof UnauthorizedError ? authRequiredError(ctx) : error;
|
|
764
768
|
}
|
|
765
769
|
throw vendorRefusal(unsentAfterUnauthorized(`Connector "${id}"'s API`, renewal, "downstream_oauth_required"), dispatch);
|
|
766
770
|
}
|
|
@@ -783,7 +787,7 @@ export function staticOAuth(id, config) {
|
|
|
783
787
|
throw signal.reason;
|
|
784
788
|
if (error instanceof UnauthorizedError) {
|
|
785
789
|
rejectedScopes.add(scopeOf(ctx));
|
|
786
|
-
throw authRequiredError();
|
|
790
|
+
throw authRequiredError(ctx);
|
|
787
791
|
}
|
|
788
792
|
throw error;
|
|
789
793
|
}
|
|
@@ -796,7 +800,7 @@ export function staticOAuth(id, config) {
|
|
|
796
800
|
}), auxiliary));
|
|
797
801
|
});
|
|
798
802
|
});
|
|
799
|
-
const
|
|
803
|
+
const statusOf = authMethod((ctx, provider) => {
|
|
800
804
|
return authFlow(function* () {
|
|
801
805
|
if (rejectedScopes.has(scopeOf(ctx))) {
|
|
802
806
|
return {
|
|
@@ -808,7 +812,6 @@ export function staticOAuth(id, config) {
|
|
|
808
812
|
// nothing here calls the SDK. A stored grant for the configured token
|
|
809
813
|
// endpoint is healthy until a call says otherwise — the downstream is
|
|
810
814
|
// asked nothing.
|
|
811
|
-
const provider = providerFor(ctx);
|
|
812
815
|
if ((yield authInvoke(provider, "operatorDisconnected", []))) {
|
|
813
816
|
return { state: "auth_required", message: disconnectedMessage };
|
|
814
817
|
}
|
|
@@ -817,6 +820,24 @@ export function staticOAuth(id, config) {
|
|
|
817
820
|
: { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
|
|
818
821
|
});
|
|
819
822
|
});
|
|
823
|
+
const status = authMethod((ctx) => {
|
|
824
|
+
return authCall(statusOf, [ctx, providerFor(ctx)]);
|
|
825
|
+
});
|
|
826
|
+
/**
|
|
827
|
+
* The invocation's check before a call enters its handler: the status read,
|
|
828
|
+
* then, when the token it read is within a minute of its stored expiry, one
|
|
829
|
+
* coordinated refresh. A failure ends the call before the handler runs, so
|
|
830
|
+
* no write can draw the 401 it could not be repeated after (INV-9).
|
|
831
|
+
*/
|
|
832
|
+
const prepareCall = authMethod((ctx) => {
|
|
833
|
+
return authFlow(function* () {
|
|
834
|
+
const provider = providerFor(ctx);
|
|
835
|
+
const state = (yield authCall(statusOf, [ctx, provider]));
|
|
836
|
+
if (state.state === "ok" && provider.renewalDue() !== undefined)
|
|
837
|
+
(yield authInvoke(grantFor(ctx, false, true), "token", []));
|
|
838
|
+
return state;
|
|
839
|
+
});
|
|
840
|
+
});
|
|
820
841
|
const startAuth = authMethod((original, opts) => {
|
|
821
842
|
return authFlow(function* () {
|
|
822
843
|
const ctx = authorizingContext(original);
|
|
@@ -949,6 +970,7 @@ export function staticOAuth(id, config) {
|
|
|
949
970
|
});
|
|
950
971
|
return withStoredAuth({
|
|
951
972
|
status: retainAuthPartition(status, 0),
|
|
973
|
+
prepareCall: retainAuthPartition(prepareCall, 0),
|
|
952
974
|
startAuth: retainAuthPartition(startAuth, 0),
|
|
953
975
|
disconnectAuth: retainAuthPartition(disconnectAuth, 0),
|
|
954
976
|
verifyState: retainAuthPartition(verifyState, 1),
|
package/dist/auth-elicitation.js
CHANGED
|
@@ -74,7 +74,11 @@ export class AuthElicitation {
|
|
|
74
74
|
self.codec ??= yield* Effect.cached(authCall(key, [], self.options.vault).pipe(Effect.map((bytes) => createRequestStateCodec({
|
|
75
75
|
key: bytes,
|
|
76
76
|
ttlSeconds: TTL_MS / 1000,
|
|
77
|
-
bind: () => canonical({
|
|
77
|
+
bind: () => canonical({
|
|
78
|
+
principal: self.options.principal,
|
|
79
|
+
endpoint: self.options.endpoint,
|
|
80
|
+
pool: self.options.registry.resultIdentity().pool,
|
|
81
|
+
}),
|
|
78
82
|
}))));
|
|
79
83
|
return yield* self.codec;
|
|
80
84
|
}), this.options.requestSignal).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
@@ -258,6 +262,7 @@ export class AuthElicitation {
|
|
|
258
262
|
version: 1,
|
|
259
263
|
principal,
|
|
260
264
|
endpoint: self.options.endpoint,
|
|
265
|
+
pool: self.options.registry.resultIdentity().pool,
|
|
261
266
|
connector: id,
|
|
262
267
|
tool,
|
|
263
268
|
...(typeof args.address === "string" ? { address: args.address } : {}),
|
|
@@ -365,6 +370,7 @@ export class AuthElicitation {
|
|
|
365
370
|
!principal ||
|
|
366
371
|
state.principal !== principal ||
|
|
367
372
|
state.endpoint !== endpoint ||
|
|
373
|
+
state.pool !== registry.resultIdentity().pool ||
|
|
368
374
|
typeof state.connector !== "string" ||
|
|
369
375
|
!registry.getConnector(state.connector) ||
|
|
370
376
|
!canManage(state.connector) ||
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { routePhrases } from "./agent-routes.js";
|
|
1
2
|
import { upwardStep } from "./upward.js";
|
|
2
3
|
import { authCall, authInvoke, authFlow, registerAuth } from "./auth/native.js";
|
|
3
4
|
import { Context, Deferred, Effect, Scheduler } from "effect";
|
|
@@ -14,8 +15,13 @@ function outcomeAware(build) {
|
|
|
14
15
|
afterUnknownWrite.set(handoff, build(true));
|
|
15
16
|
return handoff;
|
|
16
17
|
}
|
|
17
|
-
function oauthFollowUp(connectorId, unknownWrite) {
|
|
18
|
-
|
|
18
|
+
function oauthFollowUp(connectorId, unknownWrite, opts) {
|
|
19
|
+
const then = `Then ${unknownWrite ? RECONCILE : "retry the original call"}`;
|
|
20
|
+
// Without programs, no connecta.search can confirm the catalog.
|
|
21
|
+
const { program } = routePhrases(opts.routes);
|
|
22
|
+
return program
|
|
23
|
+
? `${then}; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside ${program} confirms the catalog now loads.`
|
|
24
|
+
: `${then}. Retry the original native call after recovery.`;
|
|
19
25
|
}
|
|
20
26
|
/** Shared with explicit authorization; errors never request a forced restart. */
|
|
21
27
|
export function authorizationHandoff(registry, baseUrl, requestScope, opts, args) {
|
|
@@ -43,8 +49,8 @@ const authorizationHandoffNative = /* @__PURE__ */ registerAuth(authorizationHan
|
|
|
43
49
|
connector: connector.id,
|
|
44
50
|
recovery: "unavailable",
|
|
45
51
|
message: "Credential recovery needs both a vault and the optional UI. Configure " +
|
|
46
|
-
"vault and ui in deployment code, then
|
|
47
|
-
|
|
52
|
+
"vault and ui in deployment code, then " +
|
|
53
|
+
`${routePhrases(opts.routes).authorizeAgain}.`,
|
|
48
54
|
};
|
|
49
55
|
}
|
|
50
56
|
const fields = connector.credential.fields?.map((field) => ({
|
|
@@ -90,7 +96,7 @@ const authorizationHandoffNative = /* @__PURE__ */ registerAuth(authorizationHan
|
|
|
90
96
|
status: "auth_required",
|
|
91
97
|
authorizationUrl,
|
|
92
98
|
instructions: "Copy authorizationUrl exactly as returned; do not decode or re-encode it. Open it in a browser and sign in as the user who requested this connection. Connecta verifies your identity and permission before starting consent. " +
|
|
93
|
-
oauthFollowUp(connector.id, unknownWrite),
|
|
99
|
+
oauthFollowUp(connector.id, unknownWrite, opts),
|
|
94
100
|
}));
|
|
95
101
|
});
|
|
96
102
|
});
|