@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.
Files changed (180) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +3 -2
  3. package/bin/connecta.mjs +4 -8
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +6 -4
  6. package/dist/agent-routes.js +231 -0
  7. package/dist/auth/downstream-oauth/consent.js +519 -0
  8. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  9. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  10. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  11. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  12. package/dist/auth/downstream-oauth.js +66 -2382
  13. package/dist/auth/native.js +37 -0
  14. package/dist/auth/oauth-grant.js +13 -0
  15. package/dist/auth/static-oauth.js +153 -131
  16. package/dist/auth-elicitation.js +7 -1
  17. package/dist/authorization-handoff.js +11 -5
  18. package/dist/catalog-cache.js +219 -59
  19. package/dist/catalog-policy.js +18 -0
  20. package/dist/catalog-service.js +284 -47
  21. package/dist/catalog.js +18 -2
  22. package/dist/config.d.ts +16 -11
  23. package/dist/config.js +9 -6
  24. package/dist/connector-caller.d.ts +1 -1
  25. package/dist/connector-health.js +59 -0
  26. package/dist/connector-status.js +117 -0
  27. package/dist/connectors/api-connector.d.ts +7 -2
  28. package/dist/connectors/api-connector.js +7 -4
  29. package/dist/connectors/negotiation-cache.js +22 -4
  30. package/dist/connectors/remote-mcp/call.js +164 -0
  31. package/dist/connectors/remote-mcp/catalog.js +278 -0
  32. package/dist/connectors/remote-mcp/config.js +263 -0
  33. package/dist/connectors/remote-mcp/connection.js +682 -0
  34. package/dist/connectors/remote-mcp/errors.js +488 -0
  35. package/dist/connectors/remote-mcp/oauth.js +473 -0
  36. package/dist/connectors/remote-mcp/resources.js +473 -0
  37. package/dist/connectors/remote-mcp/runtime.js +1 -0
  38. package/dist/connectors/remote-mcp/transport.js +555 -0
  39. package/dist/connectors/remote-mcp.js +62 -2863
  40. package/dist/credential-contract.d.ts +23 -0
  41. package/dist/credential-reads.js +17 -0
  42. package/dist/credential-verification.js +53 -0
  43. package/dist/credentials.d.ts +7 -0
  44. package/dist/credentials.js +60 -25
  45. package/dist/describe-config.d.ts +12 -2
  46. package/dist/describe-config.js +2 -0
  47. package/dist/discovery-health.js +199 -0
  48. package/dist/downstream-input.js +2 -0
  49. package/dist/errors.d.ts +55 -17
  50. package/dist/errors.js +95 -41
  51. package/dist/execute.js +152 -114
  52. package/dist/executor-result.js +3 -1
  53. package/dist/guest-functions.js +83 -0
  54. package/dist/guest-runtime.js +60 -19
  55. package/dist/guest-types.d.ts +19 -5
  56. package/dist/http/argument-problems.js +35 -0
  57. package/dist/http/errors.js +5 -4
  58. package/dist/http/fields.js +192 -13
  59. package/dist/http/hatch-response.js +73 -0
  60. package/dist/http/hatch.js +88 -23
  61. package/dist/http/oauth.js +3 -2
  62. package/dist/http/provider.js +32 -4
  63. package/dist/http/safety.js +8 -0
  64. package/dist/http/tools.js +44 -23
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.js +9 -2
  67. package/dist/invocation.js +26 -8
  68. package/dist/membership.d.ts +13 -5
  69. package/dist/membership.js +34 -2
  70. package/dist/meta-tools.js +54 -31
  71. package/dist/native-recovery.js +4 -16
  72. package/dist/native-tools.js +5 -2
  73. package/dist/oauth-sealing.js +10 -0
  74. package/dist/operation-vocabulary.js +135 -13
  75. package/dist/operator-record.d.ts +2 -2
  76. package/dist/operator-record.js +1 -0
  77. package/dist/operator-ui/contract.d.ts +10 -0
  78. package/dist/operator-ui/generated.js +2 -2
  79. package/dist/operator-ui/model.d.ts +22 -1
  80. package/dist/provider-call-facts.js +6 -2
  81. package/dist/providers/_shared/rest/operation-index.js +48 -50
  82. package/dist/providers/_shared/rest/value-safety.js +80 -0
  83. package/dist/providers/breeze/api.js +7 -1
  84. package/dist/providers/breeze/skill.generated.js +1 -1
  85. package/dist/providers/breeze/tools.js +4 -3
  86. package/dist/providers/ccb/api.js +2 -1
  87. package/dist/providers/ccb/index.js +5 -0
  88. package/dist/providers/ccb/skill.generated.js +1 -1
  89. package/dist/providers/cloudflare/api.js +8 -1
  90. package/dist/providers/cloudflare/skill.generated.js +1 -1
  91. package/dist/providers/docs/lib/schemas.js +24 -22
  92. package/dist/providers/docs/safety.js +2 -0
  93. package/dist/providers/docs/skill.generated.js +1 -1
  94. package/dist/providers/docs/tools.js +2 -4
  95. package/dist/providers/drive/api.js +1 -0
  96. package/dist/providers/drive/lib/contracts.js +8 -5
  97. package/dist/providers/drive/skill.generated.js +1 -1
  98. package/dist/providers/drive/tools.js +11 -16
  99. package/dist/providers/forms/api.js +1 -0
  100. package/dist/providers/forms/skill.generated.js +1 -1
  101. package/dist/providers/forms/tools.js +10 -7
  102. package/dist/providers/github/api.js +1 -0
  103. package/dist/providers/github/lib/discovery.js +1 -1
  104. package/dist/providers/github/skill.generated.js +1 -1
  105. package/dist/providers/github/tools.js +61 -17
  106. package/dist/providers/gmail/lib/schemas.js +60 -50
  107. package/dist/providers/gmail/skill.generated.js +1 -1
  108. package/dist/providers/gmail/tools.js +7 -5
  109. package/dist/providers/granola/index.js +2 -0
  110. package/dist/providers/granola/skill.generated.js +1 -1
  111. package/dist/providers/overflow/api.js +7 -1
  112. package/dist/providers/overflow/skill.generated.js +1 -1
  113. package/dist/providers/planning-center/api.js +3 -1
  114. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  115. package/dist/providers/planning-center/skill.generated.js +1 -1
  116. package/dist/providers/quickbooks/safety.js +80 -39
  117. package/dist/providers/quickbooks/skill.generated.js +1 -1
  118. package/dist/providers/sheets/api.js +1 -0
  119. package/dist/providers/sheets/skill.generated.js +1 -1
  120. package/dist/providers/sheets/tools.js +13 -14
  121. package/dist/providers/slides/skill.generated.js +1 -1
  122. package/dist/providers/slides/tools.js +15 -13
  123. package/dist/providers/stripe/skill.generated.js +1 -1
  124. package/dist/providers/tithely/api.js +3 -1
  125. package/dist/providers/tithely/skill.generated.js +1 -1
  126. package/dist/providers/vercel/api.js +1 -1
  127. package/dist/providers/vercel/skill.generated.js +1 -1
  128. package/dist/providers/vercel/tools.js +4 -3
  129. package/dist/registry.d.ts +10 -4
  130. package/dist/registry.js +41 -13
  131. package/dist/routes/credentials.js +129 -60
  132. package/dist/routes/dispatch.js +5 -1
  133. package/dist/routes/mcp.js +30 -14
  134. package/dist/routes/operator.js +2 -2
  135. package/dist/routes/shared.d.ts +6 -1
  136. package/dist/routes/shared.js +7 -1
  137. package/dist/routes/ui-config.js +2 -2
  138. package/dist/routes/ui-data.js +126 -20
  139. package/dist/routes/ui.js +16 -0
  140. package/dist/runtime/call.js +7 -2
  141. package/dist/runtime/native-storage.js +12 -0
  142. package/dist/runtime/request-reads.js +207 -0
  143. package/dist/runtime/request-scope.js +16 -0
  144. package/dist/runtime/storage.js +12 -0
  145. package/dist/skills.js +37 -20
  146. package/dist/storage/keys.js +53 -3
  147. package/dist/storage/memory.js +8 -2
  148. package/dist/storage/sql-native.js +53 -1
  149. package/dist/storage/sql.js +12 -2
  150. package/dist/storage-digest.js +32 -0
  151. package/dist/timeout.js +8 -0
  152. package/dist/types.d.ts +33 -0
  153. package/dist/usage-guide.js +16 -35
  154. package/dist/validate.d.ts +3 -0
  155. package/dist/validate.js +220 -158
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/dist/write-outcome.js +0 -3
  159. package/documentation/README.md +13 -13
  160. package/documentation/architecture.md +385 -20
  161. package/documentation/auth.md +233 -64
  162. package/documentation/code-mode.md +122 -39
  163. package/documentation/deploying.md +6 -3
  164. package/documentation/integrating.md +16 -2
  165. package/documentation/meta-tools.md +240 -49
  166. package/documentation/migration-0.33.md +5 -1
  167. package/documentation/migration-0.34.md +3 -1
  168. package/documentation/operator-tests.md +1 -1
  169. package/documentation/operator-ui.md +105 -12
  170. package/documentation/pool-membership-upgrade.md +44 -0
  171. package/documentation/providers.md +105 -0
  172. package/examples/worker/AGENTS.md +10 -10
  173. package/examples/worker/README.md +4 -3
  174. package/examples/worker/src/connecta.config.ts +1 -0
  175. package/guides/connecting-agents.md +2 -1
  176. package/guides/how-it-works.md +4 -2
  177. package/package.json +1 -1
  178. package/templates/node/AGENTS.md +13 -12
  179. package/templates/node/package.json +1 -1
  180. package/templates/node/src/connecta.config.ts +1 -0
@@ -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;
@@ -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, authOwnMethod, authProgram, authValue, registerAuth, registerAuthMethod, } from "./native.js";
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
- registerAuthMethod(StaticOAuthProvider.prototype, "clientInformation", function () {
310
- return authValue(() => this.configuredClientInformation());
311
- }, StaticOAuthProvider.prototype.clientInformation);
312
- registerAuthMethod(StaticOAuthProvider.prototype, "saveClientInformation", function () {
313
- // Nothing to save: the SDK only saves a client it registered or stamped.
314
- return Effect.void;
315
- }, StaticOAuthProvider.prototype.saveClientInformation);
316
- registerAuthMethod(StaticOAuthProvider.prototype, "discoveryState", function () {
317
- return authValue(() => {
318
- const { settings } = this;
319
- const metadata = {
320
- authorization_endpoint: settings.authorizationEndpoint.href,
321
- token_endpoint: settings.tokenEndpoint.href,
322
- response_types_supported: ["code"],
323
- grant_types_supported: ["authorization_code", "refresh_token"],
324
- code_challenge_methods_supported: ["S256"],
325
- token_endpoint_auth_methods_supported: [settings.clientAuthentication],
326
- };
327
- return {
328
- authorizationServerUrl: settings.identity,
329
- authorizationServerMetadata: metadata,
330
- resourceMetadata: { resource: settings.serverUrl.origin },
331
- };
332
- });
333
- }, StaticOAuthProvider.prototype.discoveryState);
334
- registerAuthMethod(StaticOAuthProvider.prototype, "saveDiscoveryState", function () {
335
- // Configuration is the discovery; there is nothing to remember.
336
- return Effect.void;
337
- }, StaticOAuthProvider.prototype.saveDiscoveryState);
338
- registerAuthMethod(StaticOAuthProvider.prototype, "validateResourceURL", function () {
339
- return authValue(() => {
340
- return undefined;
341
- });
342
- }, StaticOAuthProvider.prototype.validateResourceURL);
343
- registerAuthMethod(StaticOAuthProvider.prototype, "redirectToAuthorization", function (authorizationUrl) {
344
- return authFlow(function* () {
345
- if (!this.settings.pkce) {
346
- authorizationUrl.searchParams.delete("code_challenge");
347
- authorizationUrl.searchParams.delete("code_challenge_method");
348
- }
349
- (yield authCall(KvOAuthProvider.prototype.redirectToAuthorization, [authorizationUrl], this));
350
- }.bind(this));
351
- }, StaticOAuthProvider.prototype.redirectToAuthorization);
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: coordinatorFor(ctx).coordinatedFetch(provider, authMethod((input, init) => {
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 — call authorize_connector({ connector: "${id}" }) and open the returned URL.`);
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: one another request has
525
- * already rotated in, or else one coordinated refresh.
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, [provider, ctx, undefined, requestOwned]);
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
- /** One call's grant use; submitted refresh work keeps the coordinator's owner. */
566
- const grantFor = (ctx, requestOwned = false) => {
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 status = authMethod((ctx) => {
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),
@@ -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({ principal: self.options.principal, endpoint: self.options.endpoint }),
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
- return `Then ${unknownWrite ? RECONCILE : "retry the original call"}; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`;
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 call " +
47
- "authorize_connector again.",
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
  });