@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.
Files changed (183) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +2 -2
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/auth/clerk-sdk/generated.js +2 -1
  8. package/dist/auth/cloudflare-access.d.ts +3 -1
  9. package/dist/auth/cloudflare-access.js +22 -11
  10. package/dist/auth/downstream-oauth.js +195 -270
  11. package/dist/auth/downstream-revocation.js +1 -1
  12. package/dist/auth/oauth-grant.js +11 -0
  13. package/dist/auth/oauth-token-response.js +1 -1
  14. package/dist/auth/vercel-agent.js +2 -1
  15. package/dist/auth-elicitation.js +2 -2
  16. package/dist/branding.d.ts +1 -15
  17. package/dist/branding.js +2 -2
  18. package/dist/bytes.js +42 -0
  19. package/dist/call-admission-context.js +2 -5
  20. package/dist/call-admission.js +5 -0
  21. package/dist/call-recovery.js +0 -3
  22. package/dist/catalog-cache.js +26 -7
  23. package/dist/catalog-fingerprint.js +2 -18
  24. package/dist/catalog-revision.js +9 -0
  25. package/dist/catalog-service.js +51 -20
  26. package/dist/config-defaults.d.ts +1 -0
  27. package/dist/config-defaults.js +1 -0
  28. package/dist/config-schema.d.ts +1 -1
  29. package/dist/config-schema.js +6 -2
  30. package/dist/config.d.ts +41 -14
  31. package/dist/config.js +21 -71
  32. package/dist/connector-caller.js +1 -1
  33. package/dist/connectors/api-connector.js +4 -3
  34. package/dist/connectors/guarded-fetch.d.ts +0 -6
  35. package/dist/connectors/guarded-fetch.js +1 -11
  36. package/dist/connectors/remote-mcp.js +93 -69
  37. package/dist/connectors/resource-uri.js +0 -4
  38. package/dist/credentials.js +3 -0
  39. package/dist/d1.d.ts +0 -89
  40. package/dist/d1.js +0 -34
  41. package/dist/downstream-input.js +2 -2
  42. package/dist/execute.js +161 -116
  43. package/dist/executor-result.js +2 -1
  44. package/dist/executors/quickjs-runtime.js +2 -25
  45. package/dist/executors/quickjs.js +1 -1
  46. package/dist/guest-runtime.js +5 -3
  47. package/dist/guest-types.d.ts +5 -5
  48. package/dist/http/client.js +1 -1
  49. package/dist/http/errors.js +2 -2
  50. package/dist/http/json-schema.js +1 -1
  51. package/dist/http/node-transport.js +0 -1
  52. package/dist/http/oauth.js +1 -1
  53. package/dist/http/paging.js +6 -9
  54. package/dist/http/provider.js +1 -1
  55. package/dist/http/safety.js +1 -1
  56. package/dist/http/services.js +1 -8
  57. package/dist/http/tools.js +1 -7
  58. package/dist/http/vendor.js +7 -7
  59. package/dist/identity.js +2 -6
  60. package/dist/index.js +1 -2
  61. package/dist/invocation-auth.js +2 -1
  62. package/dist/invocation.js +8 -2
  63. package/dist/meta-tools.js +27 -27
  64. package/dist/native-tools.js +2 -1
  65. package/dist/oauth-handoff.js +2 -2
  66. package/dist/operation-vocabulary.js +1 -1
  67. package/dist/operator-record.d.ts +8 -2
  68. package/dist/operator-record.js +18 -50
  69. package/dist/operator-ui/contract.d.ts +4 -2
  70. package/dist/operator-ui/generated.js +3 -3
  71. package/dist/operator-ui/model.d.ts +7 -2
  72. package/dist/program-writes.js +3 -8
  73. package/dist/provider-call.js +0 -7
  74. package/dist/provider.d.ts +1 -1
  75. package/dist/providers/_shared/google/kit.js +1 -6
  76. package/dist/providers/_shared/google/service-account.js +1 -3
  77. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  78. package/dist/providers/_shared/guide.js +3 -0
  79. package/dist/providers/_shared/json.js +12 -0
  80. package/dist/providers/_shared/rest/compact-index.js +1 -1
  81. package/dist/providers/_shared/rest/kit.js +1 -3
  82. package/dist/providers/_shared/rest/operation-index.js +1 -3
  83. package/dist/providers/_shared/rest/value-safety.js +26 -28
  84. package/dist/providers/breeze/api.js +2 -7
  85. package/dist/providers/breeze/index.js +4 -2
  86. package/dist/providers/breeze/tools.js +3 -4
  87. package/dist/providers/ccb/api.js +1 -6
  88. package/dist/providers/ccb/index.js +2 -1
  89. package/dist/providers/ccb/tools.js +1 -6
  90. package/dist/providers/cloudflare/api.js +2 -2
  91. package/dist/providers/cloudflare/index.js +2 -1
  92. package/dist/providers/cloudflare/safety.js +3 -3
  93. package/dist/providers/docs/tools.js +2 -10
  94. package/dist/providers/drive/tools.js +12 -25
  95. package/dist/providers/forms/tools.js +1 -9
  96. package/dist/providers/gmail/tools.js +5 -21
  97. package/dist/providers/overflow/api.js +1 -3
  98. package/dist/providers/overflow/index.js +2 -1
  99. package/dist/providers/overflow/tools.js +3 -7
  100. package/dist/providers/planning-center/api.js +2 -1
  101. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  102. package/dist/providers/planning-center/lib/json-api.js +1 -9
  103. package/dist/providers/planning-center/tools.js +2 -1
  104. package/dist/providers/sheets/tools.js +1 -9
  105. package/dist/providers/slides/tools.js +4 -13
  106. package/dist/providers/stripe/index.js +2 -1
  107. package/dist/providers/stripe/safety.js +2 -2
  108. package/dist/providers/tithely/api.js +2 -7
  109. package/dist/providers/tithely/index.js +3 -2
  110. package/dist/providers/tithely/tools.js +7 -5
  111. package/dist/providers/vercel/api.js +1 -6
  112. package/dist/providers/vercel/lib/pin.js +1 -1
  113. package/dist/providers/vercel/safety.js +1 -1
  114. package/dist/providers/vercel/tools.js +2 -1
  115. package/dist/redaction-marker.js +2 -2
  116. package/dist/registry.d.ts +4 -1
  117. package/dist/registry.js +35 -22
  118. package/dist/request-principal.js +15 -2
  119. package/dist/request-state.js +2 -2
  120. package/dist/routes/access-tokens.js +1 -1
  121. package/dist/routes/activity-disclosure.js +1 -1
  122. package/dist/routes/connect.js +2 -2
  123. package/dist/routes/credentials.js +1 -1
  124. package/dist/routes/dispatch.js +10 -0
  125. package/dist/routes/mcp.js +23 -21
  126. package/dist/routes/oauth-management.js +1 -1
  127. package/dist/routes/operator.js +53 -7
  128. package/dist/routes/shared.d.ts +8 -8
  129. package/dist/routes/shared.js +22 -15
  130. package/dist/routes/ui-config.js +6 -6
  131. package/dist/routes/ui-data.js +2 -2
  132. package/dist/routes/ui.js +9 -4
  133. package/dist/runtime/call-admission.js +1 -1
  134. package/dist/runtime/call.js +1 -1
  135. package/dist/runtime/settlement.js +1 -1
  136. package/dist/runtime/shared-read.js +2 -15
  137. package/dist/sent-secrets.d.ts +14 -6
  138. package/dist/sent-secrets.js +231 -85
  139. package/dist/server.js +4 -4
  140. package/dist/skills.js +8 -35
  141. package/dist/sqlite.d.ts +0 -19
  142. package/dist/sqlite.js +2 -83
  143. package/dist/storage/keys.js +29 -124
  144. package/dist/storage/sql-native.js +14 -111
  145. package/dist/storage/sql.d.ts +1 -24
  146. package/dist/storage/sql.js +1 -12
  147. package/dist/tool-error.js +4 -0
  148. package/dist/types.d.ts +4 -3
  149. package/dist/ui.js +3 -0
  150. package/dist/url-safety.js +0 -4
  151. package/dist/usage-guide.js +2 -2
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/dist/worker.js +1 -1
  155. package/documentation/README.md +6 -42
  156. package/documentation/architecture.md +190 -507
  157. package/documentation/auth.md +172 -58
  158. package/documentation/code-mode.md +98 -66
  159. package/documentation/deploying.md +19 -40
  160. package/documentation/integrating.md +8 -19
  161. package/documentation/meta-tools.md +23 -6
  162. package/documentation/migration-0.33.md +5 -5
  163. package/documentation/migration-0.34.md +2 -4
  164. package/documentation/migration-retired-layouts.md +74 -0
  165. package/documentation/operator-tests.md +19 -119
  166. package/documentation/operator-ui.md +46 -8
  167. package/documentation/provider-auth-migration.md +4 -10
  168. package/documentation/provider-migration-0.29.md +4 -15
  169. package/examples/worker/AGENTS.md +4 -6
  170. package/examples/worker/README.md +20 -115
  171. package/guides/connecting-agents.md +7 -30
  172. package/guides/how-it-works.md +2 -2
  173. package/guides/safety-and-trust.md +4 -1
  174. package/package.json +19 -19
  175. package/templates/node/README.md +9 -26
  176. package/templates/node/package.json +1 -1
  177. package/dist/auth/oauth-v2-migration.js +0 -135
  178. package/dist/provider-call-facts.d.ts +0 -115
  179. package/dist/provider-call.d.ts +0 -112
  180. package/dist/storage/d1-copy.js +0 -255
  181. package/dist/storage/sql-schema.d.ts +0 -6
  182. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  183. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -1,3 +1,4 @@
1
+ import { bytesToHex, sha256Hex } from "../bytes.js";
1
2
  import { UnauthorizedError } from "@modelcontextprotocol/client";
2
3
  import { Effect, Exit } from "effect";
3
4
  import { byteReadResponse } from "../byte-read-response.js";
@@ -8,12 +9,12 @@ import { keepErrorName } from "../redaction-identity.js";
8
9
  import { fromSignal, runEdge } from "../runtime/run.js";
9
10
  import { Settlement } from "../runtime/settlement.js";
10
11
  import { SentSecrets } from "../sent-secrets.js";
11
- import { OAUTH_FLOW_TTL_SECONDS, OAUTH_REFRESH_LEASE_SECONDS, oauthFlowKeys, oauthGrantKeys, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../storage/keys.js";
12
+ import { OAUTH_FLOW_TTL_SECONDS, OAUTH_REFRESH_LEASE_SECONDS, oauthFlowKeys, oauthGrantKeys, retiredOAuthKeys, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../storage/keys.js";
12
13
  import { authenticateRemoteClient, downstreamClientMetadata, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
13
14
  import { revokeDownstreamGrant } from "./downstream-revocation.js";
14
15
  import { authAwait, authCall, authCatch, authDetached, authFlow, authInvoke, authMethod, authOwnMethod, authProgram, authStorage, authThen, authValue, registerAuth, registerAuthMethod, } from "./native.js";
15
16
  import { isOAuthCredentialRequest, isRefreshTokenRequest, refreshResponseOutcome, sdkSafeTokenResponse, tokenGrantType, } from "./oauth-token-response.js";
16
- import { deleteV2Keys, discoveryIssuer, readV2Grant, } from "./oauth-v2-migration.js";
17
+ import { discoveryIssuer } from "./oauth-grant.js";
17
18
  /** @internal Keep the SDK's cloned body reads inside the code exchange's request. */
18
19
  export function codeExchangeResponse(response, signal) {
19
20
  if (!response.body)
@@ -105,12 +106,11 @@ export class OAuthRequestNotSentError extends ConnectorCallError {
105
106
  function randomState() {
106
107
  const bytes = new Uint8Array(32);
107
108
  crypto.getRandomValues(bytes);
108
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
109
+ return bytesToHex(bytes);
109
110
  }
110
111
  /** Lowercase hex SHA-256 of a consent's `state`: the name of its flow record. */
111
112
  export async function oauthStateDigest(state) {
112
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(state));
113
- return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
113
+ return sha256Hex(state);
114
114
  }
115
115
  function storedRefreshSpent(raw) {
116
116
  const value = raw === null ? undefined : parsed(raw);
@@ -126,10 +126,11 @@ function storedRefreshSpent(raw) {
126
126
  : undefined;
127
127
  }
128
128
  const REFRESH_REQUEST_DEADLINE_MS = 20_000;
129
+ const REFRESH_COMMIT_DEADLINE_MS = 20_000;
129
130
  const REFRESH_WAIT_DEADLINE_MS = 35_000;
130
- function refreshWait(signal) {
131
+ function refreshWait(signal, timeoutMs = REFRESH_WAIT_DEADLINE_MS) {
131
132
  const deadline = new AbortController();
132
- let timer = setTimeout(() => deadline.abort(), REFRESH_WAIT_DEADLINE_MS);
133
+ let timer = setTimeout(() => deadline.abort(), timeoutMs);
133
134
  let closed = false;
134
135
  let transferred = false;
135
136
  let requestStarted = false;
@@ -429,9 +430,13 @@ export class OAuthRefreshCoordinator {
429
430
  return authFlow(function* () {
430
431
  const flight = { outcome: new Settlement() };
431
432
  this.flights.set(epoch, flight);
432
- // Pin the owner partition until the flight settles, whoever is waiting.
433
+ // Pin the owner partition until this coordinator exits, whoever is waiting.
433
434
  const release = this.retainPartition();
435
+ let settled = false;
434
436
  const settle = (outcome) => {
437
+ if (settled)
438
+ return;
439
+ settled = true;
435
440
  if (this.flights.get(epoch) === flight)
436
441
  this.flights.delete(epoch);
437
442
  release();
@@ -443,131 +448,162 @@ export class OAuthRefreshCoordinator {
443
448
  provider.recordRefreshFailure(verdict);
444
449
  settle({ status: "failed", error, ...(verdict ? { verdict } : {}) });
445
450
  };
446
- // The authorization server this redemption answers to, fixed now.
447
- const issuer = provider.refreshIssuer(epoch);
448
- let lease;
449
- try {
450
- const claim = (yield authInvoke(provider, "claimRefresh", [epoch, requested, undefined, waiting]));
451
- if (claim instanceof Response) {
452
- const tokens = (yield authInvoke(waiting, "run", [
453
- authMethod(() => {
454
- return authInvoke(provider, "storedTokens", [waiting.signal]);
455
- }),
456
- ]));
457
- if (tokens)
458
- settle({ status: "refreshed", tokens });
459
- else
460
- settle({ status: "retired" });
461
- return claim;
462
- }
463
- lease = claim;
464
- }
465
- catch (error) {
466
- fail(error, provider.refreshVerdict());
467
- throw error;
468
- }
469
- const releaseLease = authMethod((verdict, resolution = "ambiguous") => {
470
- return authFlow(function* () {
471
- (yield authInvoke(provider, "releaseRefresh", [lease, verdict, undefined, resolution]));
472
- }.bind(this));
473
- });
474
- let outcome;
475
- const deadline = waiting.signal;
476
451
  let dispatched = false;
452
+ const unknownOutcome = () => new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
453
+ // One budget for the entire commit/cleanup phase, including retries
454
+ // and lease release. Interrupt native continuations, never detach a
455
+ // Promise whose late read could still start a credential write.
456
+ const complete = authMethod((operation) => {
457
+ const completion = refreshWait(undefined, REFRESH_COMMIT_DEADLINE_MS);
458
+ return authInvoke(completion, "run", [operation]).pipe(Effect.catch((error) => Effect.fail(completion.signal.aborted ? unknownOutcome() : error)), Effect.ensuring(Effect.sync(() => completion.close())));
459
+ });
477
460
  try {
478
- const read = authFlow(function* () {
479
- const response = (yield authInvoke(provider, "dispatchRefresh", [
480
- lease,
481
- authMethod(() => authFlow(function* () {
482
- waiting.beginRequest();
483
- dispatched = true;
484
- return (yield authCall(baseFetch, [
485
- input,
486
- { ...init, redirect: "manual", signal: deadline },
487
- ]));
488
- }.bind(this))),
489
- waiting,
490
- ]));
491
- if (deadline.aborted) {
492
- void response.body?.cancel().catch(() => { });
493
- throw aborted(deadline);
494
- }
495
- return (yield authCall(refreshResponseOutcome, [response, deadline]));
496
- }.bind(this));
497
- outcome = (yield Effect.raceFirst(read, fromSignal(deadline)));
498
- if (deadline.aborted)
499
- throw aborted(deadline);
500
- }
501
- catch (error) {
502
- if (!dispatched) {
503
- // Only the coordinator's own preparation deadline can interrupt a
504
- // submitted claim. Caller cancellation no longer reaches this work.
505
- if (!waiting.signal.aborted) {
506
- (yield authCatch(authInvoke(waiting, "run", [
461
+ // The authorization server this redemption answers to, fixed now.
462
+ const issuer = provider.refreshIssuer(epoch);
463
+ let lease;
464
+ try {
465
+ const claim = (yield authInvoke(provider, "claimRefresh", [epoch, requested, undefined, waiting]));
466
+ if (claim instanceof Response) {
467
+ const tokens = (yield authInvoke(waiting, "run", [
507
468
  authMethod(() => {
508
- return authCall(releaseLease, []);
469
+ return authInvoke(provider, "storedTokens", [waiting.signal]);
509
470
  }),
510
- ]), () => { }));
511
- if (provider.refreshVerdict()?.kind === "dead") {
471
+ ]));
472
+ if (tokens)
473
+ settle({ status: "refreshed", tokens });
474
+ else
475
+ settle({ status: "retired" });
476
+ return claim;
477
+ }
478
+ lease = claim;
479
+ }
480
+ catch (error) {
481
+ fail(error, provider.refreshVerdict());
482
+ throw error;
483
+ }
484
+ const releaseLease = authMethod((verdict, resolution = "ambiguous") => {
485
+ return authFlow(function* () {
486
+ (yield authInvoke(provider, "releaseRefresh", [lease, verdict, undefined, resolution]));
487
+ }.bind(this));
488
+ });
489
+ let outcome;
490
+ const deadline = waiting.signal;
491
+ try {
492
+ const read = authFlow(function* () {
493
+ const response = (yield authInvoke(provider, "dispatchRefresh", [
494
+ lease,
495
+ authMethod(() => authFlow(function* () {
496
+ waiting.beginRequest();
497
+ dispatched = true;
498
+ return (yield authCall(baseFetch, [
499
+ input,
500
+ { ...init, redirect: "manual", signal: deadline },
501
+ ]));
502
+ }.bind(this))),
503
+ waiting,
504
+ ]));
505
+ if (deadline.aborted) {
506
+ void response.body?.cancel().catch(() => { });
507
+ throw aborted(deadline);
508
+ }
509
+ return (yield authCall(refreshResponseOutcome, [response, deadline]));
510
+ }.bind(this));
511
+ outcome = (yield Effect.raceFirst(read, fromSignal(deadline)));
512
+ if (deadline.aborted)
513
+ throw aborted(deadline);
514
+ }
515
+ catch (error) {
516
+ if (!dispatched) {
517
+ // Only the coordinator's own preparation deadline can interrupt a
518
+ // submitted claim. Caller cancellation no longer reaches this work.
519
+ if (!waiting.signal.aborted) {
512
520
  (yield authCatch(authInvoke(waiting, "run", [
513
521
  authMethod(() => {
514
- return authInvoke(provider, "discardRefusedGrant", [requested, epoch]);
522
+ return authCall(releaseLease, []);
515
523
  }),
516
524
  ]), () => { }));
525
+ if (provider.refreshVerdict()?.kind === "dead") {
526
+ (yield authCatch(authInvoke(waiting, "run", [
527
+ authMethod(() => {
528
+ return authInvoke(provider, "discardRefusedGrant", [requested, epoch]);
529
+ }),
530
+ ]), () => { }));
531
+ }
517
532
  }
533
+ const failure = waiting.signal.aborted ? provider.refreshContended() : error;
534
+ fail(failure, provider.refreshVerdict());
535
+ throw failure;
518
536
  }
519
- const failure = waiting.signal.aborted ? provider.refreshContended() : error;
520
- fail(failure, provider.refreshVerdict());
537
+ // A sent request without a definitive answer may have rotated the token.
538
+ // Persist the refusal before dropping tokens, so interrupted cleanup or a
539
+ // later write of the same fingerprint cannot make it redeemable again.
540
+ const failure = new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
541
+ (yield authCall(complete, [
542
+ authMethod(() => authFlow(function* () {
543
+ (yield authCall(releaseLease, [{ kind: "dead" }]));
544
+ (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
545
+ })),
546
+ ]));
547
+ fail(failure, { kind: "dead" });
521
548
  throw failure;
522
549
  }
523
- // A sent request without a definitive answer may have rotated the token.
524
- // Persist the refusal before dropping tokens, so interrupted cleanup or a
525
- // later write of the same fingerprint cannot make it redeemable again.
526
- const failure = new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
527
- (yield authCall(releaseLease, [{ kind: "dead" }]));
528
- (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
529
- fail(failure, { kind: "dead" });
530
- throw failure;
550
+ finally {
551
+ waiting.close();
552
+ }
553
+ return (yield authCall(complete, [
554
+ authMethod(() => authFlow(function* () {
555
+ // An answer exists from here on, and is committed whatever the owner does.
556
+ if (outcome.failure) {
557
+ // Drop a refused grant while the flight still stands: a caller arriving
558
+ // meanwhile joins it instead of redeeming the dead token again.
559
+ (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
560
+ (yield authCall(releaseLease, [outcome.verdict, "resolved"]));
561
+ fail(outcome.failure, outcome.verdict);
562
+ return outcome.forSdk;
563
+ }
564
+ // The server consumed the old refresh token; the SDK merges it back the
565
+ // same way when a rotation omits one.
566
+ const accepted = {
567
+ ...outcome.tokens,
568
+ ...(outcome.tokens.refresh_token === undefined && requested !== null
569
+ ? { refresh_token: requested }
570
+ : {}),
571
+ };
572
+ let committed;
573
+ try {
574
+ committed = (yield authInvoke(provider, "storeRefresh", [epoch, requested, accepted, issuer]));
575
+ }
576
+ catch {
577
+ const failure = new UnauthorizedError("OAuth refresh could not be committed; authorization required.");
578
+ (yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
579
+ (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
580
+ fail(failure, { kind: "dead" });
581
+ throw failure;
582
+ }
583
+ if (!committed) {
584
+ // Restart, Disconnect, or another consent decides the newer grant.
585
+ (yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
586
+ const failure = new UnauthorizedError("OAuth refresh grant is no longer active.");
587
+ fail(failure, { kind: "dead" });
588
+ throw failure;
589
+ }
590
+ (yield authInvoke(provider, "releaseRefresh", [lease, undefined, committed, "resolved"]));
591
+ settle({ status: "refreshed", tokens: committed });
592
+ return provider.adoptRefresh(committed);
593
+ })),
594
+ ]));
595
+ }
596
+ catch (error) {
597
+ fail(error, dispatched ? { kind: "dead" } : provider.refreshVerdict());
598
+ throw error;
531
599
  }
532
600
  finally {
533
601
  waiting.close();
602
+ // No storage in this finalizer: even interruption or a failed
603
+ // cleanup must drop the local flight and its owner retention.
604
+ if (!settled)
605
+ fail(unknownOutcome(), dispatched ? { kind: "dead" } : provider.refreshVerdict());
534
606
  }
535
- // An answer exists from here on, and is committed whatever the owner does.
536
- if (outcome.failure) {
537
- // Drop a refused grant while the flight still stands: a caller arriving
538
- // meanwhile joins it instead of redeeming the dead token again.
539
- (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
540
- (yield authCall(releaseLease, [outcome.verdict, "resolved"]));
541
- fail(outcome.failure, outcome.verdict);
542
- return outcome.forSdk;
543
- }
544
- // The server consumed the old refresh token; the SDK merges it back the
545
- // same way when a rotation omits one.
546
- const accepted = {
547
- ...outcome.tokens,
548
- ...(outcome.tokens.refresh_token === undefined && requested !== null ? { refresh_token: requested } : {}),
549
- };
550
- let committed;
551
- try {
552
- committed = (yield authInvoke(provider, "storeRefresh", [epoch, requested, accepted, issuer]));
553
- }
554
- catch {
555
- const failure = new UnauthorizedError("OAuth refresh could not be committed; authorization required.");
556
- (yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
557
- (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
558
- fail(failure, { kind: "dead" });
559
- throw failure;
560
- }
561
- if (!committed) {
562
- // Restart, Disconnect, or another consent decides the newer grant.
563
- (yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
564
- const failure = new UnauthorizedError("OAuth refresh grant is no longer active.");
565
- fail(failure, { kind: "dead" });
566
- throw failure;
567
- }
568
- (yield authInvoke(provider, "releaseRefresh", [lease, undefined, committed, "resolved"]));
569
- settle({ status: "refreshed", tokens: committed });
570
- return provider.adoptRefresh(committed);
571
607
  }.bind(this));
572
608
  }, OAuthRefreshCoordinator.prototype.redeem);
573
609
  }
@@ -782,7 +818,7 @@ export class KvOAuthProvider {
782
818
  if (client)
783
819
  this.addClientAuthentication = (headers, params) => authenticateRemoteClient(client, headers, params);
784
820
  }
785
- /** Stored facts only: no migration, sealing writes, refresh or discovery. */
821
+ /** Stored facts only: no sealing writes, refresh or discovery. */
786
822
  storedAuthorization(issuer) {
787
823
  return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storedAuthorization", [issuer]));
788
824
  }
@@ -798,39 +834,6 @@ export class KvOAuthProvider {
798
834
  wait(work, signal = this.signal) {
799
835
  return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "wait", [work, signal]));
800
836
  }
801
- /** Legacy helpers must fence their inner reads/deletes too, not just our join. */
802
- preparationStorage(signal) {
803
- if (!signal)
804
- return this.storage;
805
- const storage = this.storage;
806
- return {
807
- ...storage,
808
- get: authMethod((key) => {
809
- return authInvoke(this, "wait", [
810
- authMethod(() => {
811
- return authStorage(storage, "get", [key]);
812
- }),
813
- signal,
814
- ]);
815
- }),
816
- list: authMethod((prefix) => {
817
- return authInvoke(this, "wait", [
818
- authMethod(() => {
819
- return authStorage(storage, "list", [prefix]);
820
- }),
821
- signal,
822
- ]);
823
- }),
824
- delete: authMethod((key) => {
825
- return authInvoke(this, "wait", [
826
- authMethod(() => {
827
- return authStorage(storage, "delete", [key]);
828
- }),
829
- signal,
830
- ]);
831
- }),
832
- };
833
- }
834
837
  // --- the grant record ---------------------------------------------------
835
838
  /** The grant's additional authenticated data: record and epoch. */
836
839
  grantLabel(epoch) {
@@ -848,21 +851,13 @@ export class KvOAuthProvider {
848
851
  return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openGrant", [raw, signal]));
849
852
  }
850
853
  /**
851
- * The grant as stored now, migrating layout 2 on first sight. A plaintext
854
+ * The current grant record. A plaintext
852
855
  * body, written before the vault could seal, is sealed where it lies by
853
856
  * compare-and-set; a failure leaves it readable for the next write to seal.
854
857
  */
855
858
  readGrant(signal = this.signal) {
856
859
  return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "readGrant", [signal]));
857
860
  }
858
- /** Layout 2 migration and its cleanup retain the reading operation's owner. */
859
- migrate(signal) {
860
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "migrate", [signal]));
861
- }
862
- /** Deletion is retried on every read, including reads after a reset. */
863
- cleanupV2(raw, signal = this.signal) {
864
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "cleanupV2", [raw, signal]));
865
- }
866
861
  /** The grant a bound flow reads: a replaced epoch fails it. */
867
862
  boundGrant(signal = this.signal) {
868
863
  return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "boundGrant", [signal]));
@@ -892,7 +887,6 @@ export class KvOAuthProvider {
892
887
  throw this.flowSuperseded();
893
888
  return {
894
889
  epoch: `${ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`,
895
- ...(grant.cleanupPending ? { cleanupPending: true } : {}),
896
890
  body,
897
891
  };
898
892
  }
@@ -948,9 +942,10 @@ export class KvOAuthProvider {
948
942
  * Begin a flow — a connect, whose 401 may refresh or start consent, or an
949
943
  * `api()` call or start — in the live epoch, and return it. Nothing about
950
944
  * the grant is decided here: a grant belongs to one server by construction.
945
+ * An expected epoch continues an existing attempt; a replacement fails it.
951
946
  */
952
- beginFlow() {
953
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "beginFlow", []));
947
+ beginFlow(expectedEpoch) {
948
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "beginFlow", [expectedEpoch]));
954
949
  }
955
950
  /** The epoch this flow is bound to, or the live one. */
956
951
  flowEpoch() {
@@ -1373,7 +1368,6 @@ export class KvOAuthProvider {
1373
1368
  const stored = {
1374
1369
  connectaOAuth: GRANT_VERSION,
1375
1370
  epoch: grant.epoch,
1376
- ...(grant.cleanupPending ? { cleanupPending: true } : {}),
1377
1371
  ...(grant.flow !== undefined ? { flow: grant.flow } : {}),
1378
1372
  };
1379
1373
  if (Object.keys(grant.body).length > 0) {
@@ -1429,7 +1423,6 @@ export class KvOAuthProvider {
1429
1423
  }
1430
1424
  return {
1431
1425
  epoch: stored.epoch,
1432
- ...(stored.cleanupPending ? { cleanupPending: true } : {}),
1433
1426
  ...(stored.flow !== undefined ? { flow: stored.flow } : {}),
1434
1427
  body: grantBody(body),
1435
1428
  };
@@ -1460,10 +1453,17 @@ export class KvOAuthProvider {
1460
1453
  }),
1461
1454
  signal,
1462
1455
  ]));
1463
- if (raw === null)
1464
- raw = (yield authInvoke(this, "migrate", [signal]));
1465
- if (raw !== null && storedGrant(raw)?.cleanupPending)
1466
- raw = (yield authInvoke(this, "cleanupV2", [raw, signal]));
1456
+ if (raw === null) {
1457
+ // Inspect names only: retired credentials are never opened or copied.
1458
+ const keys = (yield authInvoke(this, "wait", [
1459
+ authMethod(() => authStorage(this.storage, "list", [retiredOAuthKeys.scan])),
1460
+ signal,
1461
+ ]));
1462
+ if (keys.some((key) => retiredOAuthKeys.prefixes.some((prefix) => key.startsWith(prefix))))
1463
+ (this.sealer ?? console).warn(`[connecta] connector "${this.connectorId}" has an unsupported pre-layout-3 OAuth grant; ` +
1464
+ "treating it as absent. Connect again to authorize, or restore a backup and run the " +
1465
+ "OAuth migration on Connecta 0.37.1 before upgrading.");
1466
+ }
1467
1467
  const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
1468
1468
  if (raw !== null && this.sealer && storedGrant(raw)?.body !== undefined) {
1469
1469
  const sealed = (yield authInvoke(this, "encodeGrant", [grant, signal]));
@@ -1478,99 +1478,6 @@ export class KvOAuthProvider {
1478
1478
  return { raw, grant };
1479
1479
  }.bind(this));
1480
1480
  }, KvOAuthProvider.prototype.readGrant);
1481
- registerAuthMethod(KvOAuthProvider.prototype, "migrate", function (signal) {
1482
- return authFlow(function* () {
1483
- const sealer = this.sealer;
1484
- const found = (yield authInvoke(this, "wait", [
1485
- authMethod(() => {
1486
- return authCall(readV2Grant, [
1487
- this.preparationStorage(signal),
1488
- sealer && {
1489
- ...sealer,
1490
- open: authMethod((label, value) => {
1491
- return authInvoke(this, "wait", [
1492
- authMethod(() => {
1493
- return authInvoke(sealer, "open", [label, value]);
1494
- }),
1495
- signal,
1496
- ]);
1497
- }),
1498
- },
1499
- ]);
1500
- }),
1501
- signal,
1502
- ]));
1503
- if (!found)
1504
- return null;
1505
- const prefix = found.disconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX;
1506
- const encoded = (yield authInvoke(this, "encodeGrant", [
1507
- {
1508
- epoch: `${prefix}${crypto.randomUUID()}`,
1509
- body: found.body,
1510
- cleanupPending: true,
1511
- },
1512
- signal,
1513
- ]));
1514
- let written;
1515
- try {
1516
- written = (yield authInvoke(this, "wait", [
1517
- authMethod(() => {
1518
- return authStorage(this.storage, "compareAndSet", [GRANT, null, encoded]);
1519
- }),
1520
- signal,
1521
- ]));
1522
- }
1523
- catch {
1524
- if (signal?.aborted)
1525
- throw aborted(signal);
1526
- throw this.credentialWriteError();
1527
- }
1528
- return (yield written
1529
- ? authValue(() => encoded)
1530
- : authInvoke(this, "wait", [
1531
- authMethod(() => {
1532
- return authStorage(this.storage, "get", [GRANT]);
1533
- }),
1534
- signal,
1535
- ]));
1536
- }.bind(this));
1537
- }, KvOAuthProvider.prototype.migrate);
1538
- registerAuthMethod(KvOAuthProvider.prototype, "cleanupV2", function (raw, signal) {
1539
- signal = signal === undefined ? this.signal : signal;
1540
- return authFlow(function* () {
1541
- try {
1542
- if (!(yield authInvoke(this, "wait", [
1543
- authMethod(() => {
1544
- return authCall(deleteV2Keys, [this.preparationStorage(signal)]);
1545
- }),
1546
- signal,
1547
- ])))
1548
- return raw;
1549
- const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
1550
- delete grant.cleanupPending;
1551
- const cleared = (yield authInvoke(this, "encodeGrant", [grant, signal]));
1552
- if ((yield authInvoke(this, "wait", [
1553
- authMethod(() => {
1554
- return authStorage(this.storage, "compareAndSet", [GRANT, raw, cleared]);
1555
- }),
1556
- signal,
1557
- ])))
1558
- return cleared;
1559
- return (yield authInvoke(this, "wait", [
1560
- authMethod(() => {
1561
- return authStorage(this.storage, "get", [GRANT]);
1562
- }),
1563
- signal,
1564
- ]));
1565
- }
1566
- catch {
1567
- if (signal?.aborted)
1568
- throw aborted(signal);
1569
- // The durable marker survives both deletion and marker-clear failures.
1570
- return raw;
1571
- }
1572
- }.bind(this));
1573
- }, KvOAuthProvider.prototype.cleanupV2);
1574
1481
  registerAuthMethod(KvOAuthProvider.prototype, "boundGrant", function (signal) {
1575
1482
  signal = signal === undefined ? this.signal : signal;
1576
1483
  return authFlow(function* () {
@@ -1770,9 +1677,11 @@ export class KvOAuthProvider {
1770
1677
  return isDisconnectedEpoch((yield authInvoke(this, "liveEpoch", [])));
1771
1678
  }.bind(this));
1772
1679
  }, KvOAuthProvider.prototype.operatorDisconnected);
1773
- registerAuthMethod(KvOAuthProvider.prototype, "beginFlow", function () {
1680
+ registerAuthMethod(KvOAuthProvider.prototype, "beginFlow", function (expectedEpoch) {
1774
1681
  return authFlow(function* () {
1775
1682
  const epoch = (yield authInvoke(this, "liveEpoch", []));
1683
+ if (expectedEpoch !== undefined && epoch !== expectedEpoch)
1684
+ throw this.flowSuperseded();
1776
1685
  this.epoch = epoch;
1777
1686
  this.bound = true;
1778
1687
  return epoch;
@@ -1893,9 +1802,29 @@ export class KvOAuthProvider {
1893
1802
  const { body } = (yield authInvoke(this, "activeGrant", []));
1894
1803
  if (this.signal?.aborted)
1895
1804
  throw aborted(this.signal);
1896
- const state = body.discovery;
1805
+ let state = body.discovery;
1806
+ if (state?.resourceMetadataUrl && this.clientOptions?.resourceOrigin) {
1807
+ let sameOrigin = false;
1808
+ try {
1809
+ sameOrigin = new URL(state.resourceMetadataUrl).origin === this.clientOptions.resourceOrigin;
1810
+ }
1811
+ catch {
1812
+ // A malformed legacy URL is no more usable than a foreign one.
1813
+ }
1814
+ if (!sameOrigin) {
1815
+ // Never hand the SDK a legacy foreign URL or its resource
1816
+ // document. Keep the issuer binding for callbacks, but force
1817
+ // same-origin resource discovery on every SDK path. This read
1818
+ // does not mutate the stored grant.
1819
+ state = { ...state };
1820
+ delete state.resourceMetadataUrl;
1821
+ delete state.resourceMetadata;
1822
+ }
1823
+ }
1897
1824
  this.assertCredentialFree(state, body);
1898
- this.consentDiscovery = state;
1825
+ // Bind consent to durable discovery until the SDK saves a new
1826
+ // state. A same-origin metadata 404 can leave the cache unchanged.
1827
+ this.consentDiscovery = body.discovery;
1899
1828
  return state;
1900
1829
  }.bind(this));
1901
1830
  }, KvOAuthProvider.prototype.discoveryState);
@@ -2704,10 +2633,7 @@ export class KvOAuthProvider {
2704
2633
  for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
2705
2634
  const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
2706
2635
  const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
2707
- const encoded = (yield authInvoke(this, "encodeGrant", [
2708
- { epoch, body: carried ?? {}, cleanupPending: true },
2709
- null,
2710
- ]));
2636
+ const encoded = (yield authInvoke(this, "encodeGrant", [{ epoch, body: carried ?? {} }, null]));
2711
2637
  let written;
2712
2638
  try {
2713
2639
  written = (yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded]));
@@ -2718,7 +2644,6 @@ export class KvOAuthProvider {
2718
2644
  if (written) {
2719
2645
  this.refreshCoordinator?.retire(grant.epoch);
2720
2646
  try {
2721
- (yield authInvoke(this, "cleanupV2", [encoded, null]));
2722
2647
  (yield authInvoke(this, "sweepConsents", []));
2723
2648
  (yield authInvoke(this, "sweepRefreshLeases", []));
2724
2649
  }
@@ -4,7 +4,7 @@ import { fromSignal, runEdge } from "../runtime/run.js";
4
4
  import { classifyHost } from "../url-safety.js";
5
5
  import { authenticateRemoteClient, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
6
6
  import { authCall, authFlow, authProgram, registerAuth } from "./native.js";
7
- import { discoveryIssuer } from "./oauth-v2-migration.js";
7
+ import { discoveryIssuer } from "./oauth-grant.js";
8
8
  /** Local removal succeeded; provider-side removal needs the operator's attention. */
9
9
  export class OAuthRevocationError extends ConnectorCallError {
10
10
  constructor() {
@@ -0,0 +1,11 @@
1
+ function plainObject(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ /** The issuer a discovery record names, as the SDK derives it. */
5
+ export function discoveryIssuer(state) {
6
+ if (!plainObject(state) || typeof state.authorizationServerUrl !== "string")
7
+ return undefined;
8
+ const metadata = state.authorizationServerMetadata;
9
+ const issuer = plainObject(metadata) && typeof metadata.issuer === "string" ? metadata.issuer : state.authorizationServerUrl;
10
+ return issuer === "" ? undefined : issuer;
11
+ }
@@ -3,7 +3,7 @@ import { authCall, authFlow, authInvoke, authProgram, registerAuth } from "./nat
3
3
  // The token endpoint's answers as downstream OAuth classifies them and the SDK
4
4
  // gets to parse them: a refresh's re-consent verdict, and every failure
5
5
  // rebuilt from its OAuth `error` code alone, so nothing else a token endpoint
6
- // wrote reaches the SDK's console output (#695).
6
+ // wrote reaches the SDK's console output.
7
7
  /** The `grant_type` of a token request, or undefined for any other request. */
8
8
  export function tokenGrantType(init) {
9
9
  if ((init?.method ?? "GET").toUpperCase() !== "POST")