@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
@@ -1,74 +1,18 @@
1
- import { bytesToHex, sha256Hex } from "../bytes.js";
1
+ import { META_ROUTES } from "../agent-routes.js";
2
2
  import { UnauthorizedError } from "@modelcontextprotocol/client";
3
- import { Effect, Exit } from "effect";
4
- import { byteReadResponse } from "../byte-read-response.js";
5
3
  import { deriveContext } from "../derived-context.js";
6
4
  import { ConnectorCallError, resendAdvice } from "../errors.js";
7
- import { oauthPartitionFor, retainOAuthPartition } from "../oauth-partition.js";
8
- import { keepErrorName } from "../redaction-identity.js";
9
- import { fromSignal, runEdge } from "../runtime/run.js";
10
- import { Settlement } from "../runtime/settlement.js";
11
5
  import { SentSecrets } from "../sent-secrets.js";
12
- import { OAUTH_FLOW_TTL_SECONDS, OAUTH_REFRESH_LEASE_SECONDS, oauthFlowKeys, oauthGrantKeys, retiredOAuthKeys, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../storage/keys.js";
13
6
  import { authenticateRemoteClient, downstreamClientMetadata, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
14
- import { revokeDownstreamGrant } from "./downstream-revocation.js";
15
- import { authAwait, authCall, authCatch, authDetached, authFlow, authInvoke, authMethod, authOwnMethod, authProgram, authStorage, authThen, authValue, registerAuth, registerAuthMethod, } from "./native.js";
16
- import { isOAuthCredentialRequest, isRefreshTokenRequest, refreshResponseOutcome, sdkSafeTokenResponse, tokenGrantType, } from "./oauth-token-response.js";
17
- import { discoveryIssuer } from "./oauth-grant.js";
18
- /** @internal Keep the SDK's cloned body reads inside the code exchange's request. */
19
- export function codeExchangeResponse(response, signal) {
20
- if (!response.body)
21
- return response;
22
- const reader = response.body.getReader();
23
- void reader.closed.catch(() => { });
24
- let closed = false;
25
- let abort;
26
- const release = () => {
27
- closed = true;
28
- signal.removeEventListener("abort", abort);
29
- reader.releaseLock();
30
- };
31
- const body = new ReadableStream({
32
- start(controller) {
33
- abort = () => {
34
- if (closed)
35
- return;
36
- void reader.cancel().catch(() => { });
37
- release();
38
- controller.error(signal.reason);
39
- };
40
- signal.addEventListener("abort", abort, { once: true });
41
- if (signal.aborted)
42
- abort();
43
- },
44
- async pull(controller) {
45
- try {
46
- const next = await reader.read();
47
- if (closed)
48
- return;
49
- if (next.done) {
50
- release();
51
- controller.close();
52
- }
53
- else
54
- controller.enqueue(next.value);
55
- }
56
- catch (error) {
57
- if (!closed) {
58
- release();
59
- controller.error(error);
60
- }
61
- }
62
- },
63
- cancel() {
64
- if (!closed) {
65
- void reader.cancel().catch(() => { });
66
- release();
67
- }
68
- },
69
- });
70
- return byteReadResponse(new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }));
71
- }
7
+ import { authMethods } from "./native.js";
8
+ import { OAUTH_REFRESH_SKEW_MS } from "./oauth-grant.js";
9
+ import { consentMethods } from "./downstream-oauth/consent.js";
10
+ import { ACTIVE_EPOCH_PREFIX, GRANT, grantStoreMethods, isDisconnectedEpoch, } from "./downstream-oauth/grant-store.js";
11
+ import { aborted, tokenResponse, } from "./downstream-oauth/refresh-coordinator.js";
12
+ import { refreshRecordMethods } from "./downstream-oauth/refresh-records.js";
13
+ import { sdkHookMethods } from "./downstream-oauth/sdk-hooks.js";
14
+ export { earlyRenewalFetch, EarlyRenewals, OAuthRefreshCoordinator, refreshCoordinatorsByPartition, } from "./downstream-oauth/refresh-coordinator.js";
15
+ export { codeExchangeResponse, OAuthCallbackClaimedError, oauthStateDigest } from "./downstream-oauth/consent.js";
72
16
  /** RFC 6749 section 3.3: one or more scope tokens, separated by single spaces. */
73
17
  const OAUTH_SCOPE = /^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/;
74
18
  /** Refuse, at construction, a configured scope no authorization server could parse. */
@@ -87,650 +31,9 @@ export function assertOAuthScope(connectorId, scope) {
87
31
  export function authorizingContext(ctx) {
88
32
  return deriveContext(ctx, { allowAuthorization: true });
89
33
  }
90
- /**
91
- * A callback whose consent another callback already claimed: fixed text, and
92
- * nothing was sent. Its own class so the callback route can answer it as the
93
- * already-used link it is, not as an exchange the provider refused.
94
- */
95
- export class OAuthCallbackClaimedError extends ConnectorCallError {
96
- constructor(connectorId) {
97
- super("connector_call_failed", `Connector "${connectorId}" authorization callback was already used by another request; nothing was exchanged.`);
98
- this.name = "OAuthCallbackClaimedError";
99
- }
100
- }
101
- keepErrorName(OAuthCallbackClaimedError, "OAuthCallbackClaimedError");
102
34
  /** A local destination guard proved that no OAuth HTTP request left. */
103
35
  export class OAuthRequestNotSentError extends ConnectorCallError {
104
36
  }
105
- /** A 256-bit random opaque value, hex-encoded — used for the OAuth `state`. */
106
- function randomState() {
107
- const bytes = new Uint8Array(32);
108
- crypto.getRandomValues(bytes);
109
- return bytesToHex(bytes);
110
- }
111
- /** Lowercase hex SHA-256 of a consent's `state`: the name of its flow record. */
112
- export async function oauthStateDigest(state) {
113
- return sha256Hex(state);
114
- }
115
- function storedRefreshSpent(raw) {
116
- const value = raw === null ? undefined : parsed(raw);
117
- return plainObject(value) &&
118
- value.connectaOAuthRefreshSpent === 1 &&
119
- typeof value.epoch === "string" &&
120
- typeof value.holder === "string" &&
121
- typeof value.activeKey === "string" &&
122
- (value.state === "outstanding" ||
123
- value.state === "ambiguous" ||
124
- (value.state === "resolved" && typeof value.resolvedAt === "number" && Number.isFinite(value.resolvedAt)))
125
- ? value
126
- : undefined;
127
- }
128
- const REFRESH_REQUEST_DEADLINE_MS = 20_000;
129
- const REFRESH_COMMIT_DEADLINE_MS = 20_000;
130
- const REFRESH_WAIT_DEADLINE_MS = 35_000;
131
- function refreshWait(signal, timeoutMs = REFRESH_WAIT_DEADLINE_MS) {
132
- const deadline = new AbortController();
133
- let timer = setTimeout(() => deadline.abort(), timeoutMs);
134
- let closed = false;
135
- let transferred = false;
136
- let requestStarted = false;
137
- const abort = () => deadline.abort(aborted(signal));
138
- signal?.addEventListener("abort", abort, { once: true });
139
- if (signal?.aborted)
140
- abort();
141
- return {
142
- signal: deadline.signal,
143
- get requestStarted() {
144
- return requestStarted;
145
- },
146
- run: authMethod((operation) => {
147
- return authFlow(function* () {
148
- // A race's losing operation may still start. Check before constructing it,
149
- // then again before handing a late result back to preparation.
150
- if (deadline.signal.aborted)
151
- return (yield Effect.fail(aborted(deadline.signal)));
152
- return (yield authThen(Effect.raceAllFirst([fromSignal(deadline.signal), authCall(operation, [])]), (value) => {
153
- if (deadline.signal.aborted)
154
- throw aborted(deadline.signal);
155
- return value;
156
- }));
157
- });
158
- }),
159
- transfer: () => {
160
- if (deadline.signal.aborted)
161
- throw aborted(deadline.signal);
162
- if (transferred)
163
- return;
164
- // Synchronous with CAS submission: cancellation can prevent the write
165
- // only before this point. Keep the independent preparation timer alive.
166
- signal?.removeEventListener("abort", abort);
167
- transferred = true;
168
- },
169
- beginRequest: () => {
170
- if (deadline.signal.aborted)
171
- throw aborted(deadline.signal);
172
- requestStarted = true;
173
- clearTimeout(timer);
174
- timer = setTimeout(() => {
175
- deadline.abort(new DOMException("OAuth refresh timed out", "TimeoutError"));
176
- }, REFRESH_REQUEST_DEADLINE_MS);
177
- },
178
- close: () => {
179
- if (closed)
180
- return;
181
- closed = true;
182
- clearTimeout(timer);
183
- signal?.removeEventListener("abort", abort);
184
- },
185
- };
186
- }
187
- function aborted(signal) {
188
- return signal?.reason ?? new DOMException("This operation was aborted", "AbortError");
189
- }
190
- /**
191
- * The outcome of a flight another request owns, failures included. Rejects
192
- * only with this caller's own abort.
193
- *
194
- * The owner settles the flight from its own request. Settlement wakes this
195
- * waiter through a Promise this request created, so the wait — and the
196
- * caller's storage reads after it — resume here, never inside the owner's
197
- * call (src/runtime/settlement.ts).
198
- */
199
- function waitForRefreshFlight(flight, signal, deadline) {
200
- const settled = flight.outcome.wait();
201
- // The abort goes first, so a caller that has already left never joins.
202
- return runEdge(Effect.raceAllFirst([
203
- ...(signal ? [fromSignal(signal)] : []),
204
- ...(deadline ? [fromSignal(deadline)] : []),
205
- settled,
206
- ]));
207
- }
208
- /** Tokens as a token response the SDK parses: only the client stamps an issuer. */
209
- function tokenResponse(tokens) {
210
- const { issuer: _issuer, ...answer } = tokens;
211
- return Response.json(answer);
212
- }
213
- function refreshTokensDigest(tokens) {
214
- return runEdge(authProgram(refreshTokensDigestNative, [tokens]));
215
- }
216
- const refreshTokensDigestNative = /* @__PURE__ */ registerAuth(refreshTokensDigest, function (tokens) {
217
- return authFlow(function* () {
218
- const { issuer: _issuer, ...answer } = tokens;
219
- return (yield authCall(oauthStateDigest, [
220
- JSON.stringify(Object.fromEntries(Object.entries(answer).sort(([a], [b]) => a.localeCompare(b)))),
221
- ]));
222
- });
223
- });
224
- function requestedRefreshToken(init) {
225
- return init?.body instanceof URLSearchParams
226
- ? init.body.get("refresh_token")
227
- : new URLSearchParams(String(init?.body ?? "")).get("refresh_token");
228
- }
229
- /**
230
- * The token endpoint as one connector runtime's OAuth flows reach it.
231
- *
232
- * A refresh generation is redeemed once per owner partition: the first request
233
- * owns the fetch, classifies the answer, and stores an accepted rotation, by
234
- * compare-and-set, before anyone is released, so a cancelled owner or an SDK
235
- * that redirects instead of saving cannot strand a spent refresh token.
236
- * Requests that arrive meanwhile join its outcome; a request whose token read
237
- * predates a stored refresh is handed that refresh instead of redeeming again.
238
- * A shared-storage lease prevents concurrent redemption across isolates;
239
- * the grant CAS also fences stale rotations.
240
- *
241
- * A code exchange claims its consent's flow record and re-checks the epoch in
242
- * the turn that dispatches it (`KvOAuthProvider.claimCodeExchange`,
243
- * `dispatchCodeExchange`). Every token answer the SDK parses is rebuilt from
244
- * the OAuth `error` code alone unless it is a token response.
245
- */
246
- export class OAuthRefreshCoordinator {
247
- retainPartition;
248
- flights = new Map();
249
- constructor(retainPartition = () => () => { }) {
250
- this.retainPartition = retainPartition;
251
- }
252
- coordinatedFetch(provider, baseFetch, requestSignal, defer, resourceFetch = baseFetch, prepareCredentialRequest) {
253
- return authMethod((input, init) => {
254
- return authFlow(function* () {
255
- // Freeze the form before any await: classification and the spent digest
256
- // must describe the exact token sent, even if its caller mutates a form.
257
- init = init
258
- ? { ...init, ...(init.body instanceof URLSearchParams ? { body: new URLSearchParams(init.body) } : {}) }
259
- : undefined;
260
- // Production adapters resolve the required host policy before a code or
261
- // refresh claim can consume its single-use credential. Custom SDK leaves
262
- // may supply their own policy; this hook performs no I/O and owns no data.
263
- if (isOAuthCredentialRequest(init))
264
- prepareCredentialRequest?.();
265
- if (isRefreshTokenRequest(init)) {
266
- const signal = requestSignal && init?.signal
267
- ? AbortSignal.any([requestSignal, init.signal])
268
- : (requestSignal ?? init?.signal ?? undefined);
269
- return (yield authInvoke(this, "refresh", [provider, input, init, baseFetch, signal, defer]));
270
- }
271
- const grantType = tokenGrantType(init);
272
- const tokenRequest = isOAuthCredentialRequest(init);
273
- const exchange = grantType === "authorization_code";
274
- if (exchange) {
275
- const answered = (yield authInvoke(provider, "claimCodeExchange", []));
276
- if (answered)
277
- return answered;
278
- }
279
- const dispatch = authMethod(() => {
280
- return authFlow(function* () {
281
- if (requestSignal?.aborted)
282
- throw aborted(requestSignal);
283
- const signal = requestSignal
284
- ? init?.signal
285
- ? AbortSignal.any([requestSignal, init.signal])
286
- : requestSignal
287
- : init?.signal;
288
- // Credential-bearing OAuth requests use the raw send path. A redirect
289
- // is a dispatched failure, never authority to send the form again.
290
- return (yield tokenRequest
291
- ? authCall(baseFetch, [input, { ...init, redirect: "manual", ...(signal ? { signal } : {}) }])
292
- : authCall(resourceFetch, [input, { ...init, ...(signal ? { signal } : {}) }]));
293
- }.bind(this));
294
- });
295
- // Await the composed fetch here so workerd associates a rejection with
296
- // the fetch the SDK is already awaiting.
297
- const response = (yield exchange
298
- ? authInvoke(provider, "dispatchCodeExchange", [dispatch, input])
299
- : authCall(dispatch, []));
300
- if (!tokenRequest)
301
- return response;
302
- // A code exchange's failure is the SDK's to log as well.
303
- const forSdk = (yield authCall(sdkSafeTokenResponse, [
304
- response,
305
- exchange
306
- ? authMethod((tokens) => {
307
- return authInvoke(provider, "recordCodeExchangeTokens", [tokens]);
308
- })
309
- : undefined,
310
- ]));
311
- if (exchange && forSdk !== response)
312
- provider.recordCodeExchangeRefusal(forSdk);
313
- return forSdk;
314
- }.bind(this));
315
- });
316
- }
317
- refresh(provider, input, init, baseFetch, signal, defer) {
318
- return runEdge(authOwnMethod(OAuthRefreshCoordinator.prototype, this, "refresh", [
319
- provider,
320
- input,
321
- init,
322
- baseFetch,
323
- signal,
324
- defer,
325
- ]));
326
- }
327
- redeem(provider, epoch, requested, input, init, baseFetch, waiting) {
328
- return runEdge(authOwnMethod(OAuthRefreshCoordinator.prototype, this, "redeem", [
329
- provider,
330
- epoch,
331
- requested,
332
- input,
333
- init,
334
- baseFetch,
335
- waiting,
336
- ]));
337
- }
338
- /** A reset fenced `epoch`: wake its joiners now rather than at the answer. */
339
- retire(epoch) {
340
- const flight = this.flights.get(epoch);
341
- if (!flight)
342
- return;
343
- this.flights.delete(epoch);
344
- flight.outcome.settle(Exit.succeed({ status: "retired" }));
345
- }
346
- static {
347
- registerAuthMethod(OAuthRefreshCoordinator.prototype, "refresh", function (provider, input, init, baseFetch, signal, defer) {
348
- return authFlow(function* () {
349
- if (signal?.aborted)
350
- throw aborted(signal);
351
- const waiting = refreshWait(signal);
352
- let redeeming = false;
353
- try {
354
- // A verdict belongs to one refresh; never let an older one decide this.
355
- provider.recordRefreshFailure(undefined);
356
- const epoch = (yield authInvoke(waiting, "run", [
357
- authMethod(() => {
358
- return authInvoke(provider, "flowEpoch", []);
359
- }),
360
- ]));
361
- const requested = requestedRefreshToken(init);
362
- for (let attempt = 0; attempt < 8; attempt++) {
363
- if (signal?.aborted)
364
- throw aborted(signal);
365
- const joined = this.flights.get(epoch);
366
- if (joined) {
367
- const outcome = (yield authCall(waitForRefreshFlight, [
368
- joined,
369
- signal,
370
- waiting.signal,
371
- ]));
372
- if (signal?.aborted)
373
- throw aborted(signal);
374
- if (outcome.status === "failed") {
375
- provider.recordRefreshFailure(outcome.verdict);
376
- throw outcome.error;
377
- }
378
- if (outcome.status === "refreshed")
379
- return provider.adoptRefresh(outcome.tokens);
380
- continue;
381
- }
382
- // Preparation reads share the same bound as later claim waits.
383
- const current = (yield authInvoke(waiting, "run", [
384
- authMethod(() => {
385
- return authInvoke(provider, "storedTokens", [waiting.signal]);
386
- }),
387
- ]));
388
- if (this.flights.has(epoch))
389
- continue;
390
- if (current?.refresh_token &&
391
- (current.refresh_token !== requested || provider.refreshedSinceRead(current, epoch))) {
392
- return provider.adoptRefresh(current);
393
- }
394
- if (current?.refresh_token !== requested) {
395
- provider.recordRefreshFailure({ kind: "dead" });
396
- return Response.json({ error: "invalid_grant", error_description: "Refresh token is no longer active." }, { status: 400 });
397
- }
398
- redeeming = true;
399
- // Claim submission transfers renewal, including preparation, token
400
- // request and commit, to a bounded native coordinator. Only
401
- // this caller's wait races its signal; the coordinator receives
402
- // no request/call Context authority or caller-owned deadline.
403
- const redemption = authDetached(authInvoke(this, "redeem", [
404
- provider,
405
- epoch,
406
- requested,
407
- input,
408
- init,
409
- baseFetch,
410
- waiting,
411
- ]).pipe(Effect.ensuring(Effect.sync(() => waiting.close()))), defer);
412
- return (yield signal ? Effect.raceFirst(redemption, fromSignal(signal)) : redemption);
413
- }
414
- throw provider.refreshContended();
415
- }
416
- catch (error) {
417
- if (signal?.aborted)
418
- throw aborted(signal);
419
- if (waiting.signal.aborted && !waiting.requestStarted)
420
- throw provider.refreshContended();
421
- throw error;
422
- }
423
- finally {
424
- if (!redeeming)
425
- waiting.close();
426
- }
427
- }.bind(this));
428
- }, OAuthRefreshCoordinator.prototype.refresh);
429
- registerAuthMethod(OAuthRefreshCoordinator.prototype, "redeem", function (provider, epoch, requested, input, init, baseFetch, waiting) {
430
- return authFlow(function* () {
431
- const flight = { outcome: new Settlement() };
432
- this.flights.set(epoch, flight);
433
- // Pin the owner partition until this coordinator exits, whoever is waiting.
434
- const release = this.retainPartition();
435
- let settled = false;
436
- const settle = (outcome) => {
437
- if (settled)
438
- return;
439
- settled = true;
440
- if (this.flights.get(epoch) === flight)
441
- this.flights.delete(epoch);
442
- release();
443
- // Last: joined waiters wake in their own requests, after this.
444
- flight.outcome.settle(Exit.succeed(outcome));
445
- };
446
- const fail = (error, verdict) => {
447
- if (verdict)
448
- provider.recordRefreshFailure(verdict);
449
- settle({ status: "failed", error, ...(verdict ? { verdict } : {}) });
450
- };
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
- });
460
- try {
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", [
468
- authMethod(() => {
469
- return authInvoke(provider, "storedTokens", [waiting.signal]);
470
- }),
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) {
520
- (yield authCatch(authInvoke(waiting, "run", [
521
- authMethod(() => {
522
- return authCall(releaseLease, []);
523
- }),
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
- }
532
- }
533
- const failure = waiting.signal.aborted ? provider.refreshContended() : error;
534
- fail(failure, provider.refreshVerdict());
535
- throw failure;
536
- }
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" });
548
- throw failure;
549
- }
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;
599
- }
600
- finally {
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());
606
- }
607
- }.bind(this));
608
- }, OAuthRefreshCoordinator.prototype.redeem);
609
- }
610
- }
611
- /**
612
- * One refresh coordinator per owner partition, for one connector runtime.
613
- * Long-lived enough for distinct request scopes to join one token redemption;
614
- * it owns no client, transport, or request state. Production contexts carry a
615
- * stable registry partition; hand-written contexts coordinate by their shared
616
- * storage object instead.
617
- */
618
- export function refreshCoordinatorsByPartition() {
619
- const coordinators = new WeakMap();
620
- return (ctx) => {
621
- const partition = oauthPartitionFor(ctx) ?? ctx.storage;
622
- let coordinator = coordinators.get(partition);
623
- if (!coordinator) {
624
- coordinator = new OAuthRefreshCoordinator(() => retainOAuthPartition(partition));
625
- coordinators.set(partition, coordinator);
626
- }
627
- return coordinator;
628
- };
629
- }
630
- const GRANT = oauthGrantKeys.grant;
631
- /** The epoch of an owner with no grant record yet. */
632
- const INITIAL_EPOCH = "initial";
633
- /** The epoch of a record that does not parse: readers see no grant in it. */
634
- const UNREADABLE_EPOCH = "unreadable";
635
- const ACTIVE_EPOCH_PREFIX = "v3:";
636
- const DISCONNECTED_EPOCH_PREFIX = "disconnected:";
637
- const GRANT_VERSION = 3;
638
- const FLOW_VERSION = 1;
639
- /** Attempts at one grant write while other writers keep winning. */
640
- const MAX_GRANT_WRITES = 32;
641
- /**
642
- * How long a pending authorization URL may be handed out again instead of
643
- * starting a fresh flow. The authorization server's half of it expires: a
644
- * pushed request URI lives seconds to minutes (RFC 9126), and login
645
- * transactions are commonly held for minutes. Past this age a reissued URL
646
- * more likely lands on an expired session than on consent, and a fresh start
647
- * costs one authorization request, not a registration. It sits inside the
648
- * flow record's fifteen minutes.
649
- */
650
- const PENDING_AUTHORIZATION_MAX_AGE_MS = 10 * 60 * 1000;
651
- function discoveryBinding(state) {
652
- return runEdge(authProgram(discoveryBindingNative, [state]));
653
- }
654
- const discoveryBindingNative = /* @__PURE__ */ registerAuth(discoveryBinding, function (state) {
655
- return authFlow(function* () {
656
- return state
657
- ? {
658
- issuer: discoveryIssuer(state),
659
- tokenEndpoint: state.authorizationServerMetadata?.token_endpoint,
660
- ...(state.authorizationServerMetadata?.authorization_response_iss_parameter_supported === true
661
- ? { issRequired: true }
662
- : {}),
663
- discovery: (yield authCall(oauthStateDigest, [JSON.stringify(state)])),
664
- }
665
- : {};
666
- });
667
- });
668
- function plainObject(value) {
669
- return value !== null && typeof value === "object" && !Array.isArray(value);
670
- }
671
- function parsed(raw) {
672
- try {
673
- return JSON.parse(raw);
674
- }
675
- catch {
676
- return undefined;
677
- }
678
- }
679
- function storedGrant(raw) {
680
- const value = parsed(raw);
681
- return plainObject(value) &&
682
- value.connectaOAuth === GRANT_VERSION &&
683
- typeof value.epoch === "string" &&
684
- (value.flow === undefined || typeof value.flow === "string") &&
685
- (value.sealed === undefined || typeof value.sealed === "string")
686
- ? value
687
- : undefined;
688
- }
689
- /** The epoch a raw grant record names, without opening its body. */
690
- function epochOf(raw) {
691
- if (raw === null)
692
- return INITIAL_EPOCH;
693
- return storedGrant(raw)?.epoch ?? UNREADABLE_EPOCH;
694
- }
695
- /** Nothing here trusts stored bytes: keep only what has the expected shape. */
696
- function grantBody(value) {
697
- if (!plainObject(value))
698
- return {};
699
- const { issuer, client, tokens, discovery, refreshConsent } = value;
700
- return {
701
- ...(typeof issuer === "string" ? { issuer } : {}),
702
- ...(plainObject(client) && plainObject(client.value) && typeof client.value.client_id === "string"
703
- ? {
704
- client: {
705
- value: client.value,
706
- ...(typeof client.binding === "string" ? { binding: client.binding } : {}),
707
- ...(client.carried === true ? { carried: true } : {}),
708
- ...(client.registrationPath === "cimd" ||
709
- client.registrationPath === "dcr" ||
710
- client.registrationPath === "static"
711
- ? { registrationPath: client.registrationPath }
712
- : {}),
713
- },
714
- }
715
- : {}),
716
- ...(plainObject(tokens) && typeof tokens.access_token === "string"
717
- ? { tokens: tokens }
718
- : {}),
719
- ...(discoveryIssuer(discovery) !== undefined ? { discovery: discovery } : {}),
720
- ...(plainObject(refreshConsent) &&
721
- typeof refreshConsent.digest === "string" &&
722
- typeof refreshConsent.resolution === "string"
723
- ? { refreshConsent: { digest: refreshConsent.digest, resolution: refreshConsent.resolution } }
724
- : {}),
725
- };
726
- }
727
- /** What an invalidation compares: the value exactly as this flow saw it. */
728
- function fingerprint(value) {
729
- return value === undefined ? undefined : JSON.stringify(value);
730
- }
731
- function isDisconnectedEpoch(epoch) {
732
- return epoch.startsWith(DISCONNECTED_EPOCH_PREFIX);
733
- }
734
37
  /**
735
38
  * OAuthClientProvider over one owner's grant record and its consents' flow
736
39
  * records, in one connector's namespace.
@@ -743,6 +46,11 @@ function isDisconnectedEpoch(epoch) {
743
46
  * to one epoch. Its reads fail once another epoch is live, and its writes are
744
47
  * compare-and-sets against the record it read, so a restart or disconnect,
745
48
  * which replaces the epoch, fences every flow that began before it.
49
+ *
50
+ * Each native method is defined once, in `./downstream-oauth/` by seam, and
51
+ * `authMethods` installs it with its Promise edge; the class declares only the
52
+ * edge's type. State those bodies read is not `private` for that reason; it
53
+ * remains internal to src/auth.
746
54
  */
747
55
  export class KvOAuthProvider {
748
56
  connectorId;
@@ -787,6 +95,17 @@ export class KvOAuthProvider {
787
95
  refreshBasis;
788
96
  /** The access token of a refresh the coordinator already stored. */
789
97
  storedRefresh;
98
+ /**
99
+ * @internal When this flow's last token request other than a refresh was
100
+ * sent. The tokens the SDK saves from its answer expire counted from here,
101
+ * never from the later save, which a slow answer would push past expiry.
102
+ */
103
+ tokenRequestSent;
104
+ /**
105
+ * The tokens of the grant this provider last opened or wrote, as facts
106
+ * only: no credential beyond the access token it already handed out.
107
+ */
108
+ observed;
790
109
  constructor(connectorId, storage, redirectUri, refreshCoordinator, allowAuthorization = true,
791
110
  /** Present when the deployment's vault can seal; the grant is then ciphertext. */
792
111
  sealer,
@@ -818,9 +137,31 @@ export class KvOAuthProvider {
818
137
  if (client)
819
138
  this.addClientAuthentication = (headers, params) => authenticateRemoteClient(client, headers, params);
820
139
  }
821
- /** Stored facts only: no sealing writes, refresh or discovery. */
822
- storedAuthorization(issuer) {
823
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storedAuthorization", [issuer]));
140
+ /**
141
+ * @internal The access token a call should renew before sending it: the
142
+ * one this provider last read, refreshable and within
143
+ * OAUTH_REFRESH_SKEW_MS of its stored expiry, in the epoch it is bound to.
144
+ * Memory only: no storage read, refresh or discovery. A grant without an
145
+ * expiry is never due; a 401 renews it.
146
+ */
147
+ renewalDue(now = Date.now()) {
148
+ const seen = this.observed;
149
+ if (!seen?.refreshable || seen.expiresAt === undefined)
150
+ return undefined;
151
+ if (this.bound && seen.epoch !== this.epoch)
152
+ return undefined;
153
+ return seen.expiresAt - OAUTH_REFRESH_SKEW_MS <= now ? seen.accessToken : undefined;
154
+ }
155
+ observe(grant) {
156
+ const { tokens, expiresAt } = grant.body;
157
+ this.observed = tokens
158
+ ? {
159
+ epoch: grant.epoch,
160
+ accessToken: tokens.access_token,
161
+ refreshable: typeof tokens.refresh_token === "string" && tokens.refresh_token !== "",
162
+ ...(expiresAt !== undefined ? { expiresAt } : {}),
163
+ }
164
+ : undefined;
824
165
  }
825
166
  get redirectUrl() {
826
167
  return this.redirectUri;
@@ -829,58 +170,11 @@ export class KvOAuthProvider {
829
170
  const client = this.clientOptions?.client;
830
171
  return downstreamClientMetadata(this.redirectUri, this.scope, this.clientOptions?.name, client ? remoteClientAuthMethod(client) : "none");
831
172
  }
832
- /** A preparation await owns no continuation after its call/request closes.
833
- * null explicitly selects coordinator/reset ownership, never the caller. */
834
- wait(work, signal = this.signal) {
835
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "wait", [work, signal]));
836
- }
837
173
  // --- the grant record ---------------------------------------------------
838
174
  /** The grant's additional authenticated data: record and epoch. */
839
175
  grantLabel(epoch) {
840
176
  return `${GRANT}:${epoch}`;
841
177
  }
842
- encodeGrant(grant, signal = this.signal) {
843
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "encodeGrant", [grant, signal]));
844
- }
845
- /**
846
- * Open a stored grant. A body that does not open — tampered, under a
847
- * rotated key, moved from another connector, owner, or epoch, or sealed
848
- * with no sealer configured — reads as empty: authorization again.
849
- */
850
- openGrant(raw, signal = this.signal) {
851
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openGrant", [raw, signal]));
852
- }
853
- /**
854
- * The current grant record. A plaintext
855
- * body, written before the vault could seal, is sealed where it lies by
856
- * compare-and-set; a failure leaves it readable for the next write to seal.
857
- */
858
- readGrant(signal = this.signal) {
859
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "readGrant", [signal]));
860
- }
861
- /** The grant a bound flow reads: a replaced epoch fails it. */
862
- boundGrant(signal = this.signal) {
863
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "boundGrant", [signal]));
864
- }
865
- /** SDK preparation is call/request-owned; coordinator reads explicitly opt out. */
866
- activeGrant() {
867
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "activeGrant", []));
868
- }
869
- /**
870
- * The one way a grant changes: read, change, compare-and-set, again while
871
- * other writers win. The write belongs to `epoch` (default: the flow's,
872
- * captured now if unset) and lands nowhere once another is live: `quiet`
873
- * drops it silently, otherwise a bound flow fails as superseded. `commit`
874
- * writes after the request's abort too. `change` returning undefined
875
- * leaves the grant as it is.
876
- */
877
- updateGrant(change, options = {}) {
878
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "updateGrant", [change, options]));
879
- }
880
- /** Store a client or tokens for `issuer`'s grant, replacing another server's. */
881
- writeCredential(field, value, issuer, refreshConsent) {
882
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "writeCredential", [field, value, issuer, refreshConsent]));
883
- }
884
178
  withIssuer(grant, body) {
885
179
  if (grant.body.issuer !== undefined && grant.body.issuer !== body.issuer) {
886
180
  if (this.codeExchange)
@@ -896,12 +190,6 @@ export class KvOAuthProvider {
896
190
  configuredAuthorizationBinding() {
897
191
  return undefined;
898
192
  }
899
- bindingForGrant(grant) {
900
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "bindingForGrant", [grant]));
901
- }
902
- checkFlowBinding(grant) {
903
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "checkFlowBinding", [grant]));
904
- }
905
193
  /** Validate RFC 9207 against the server this consent selected. */
906
194
  validateCallbackIssuer(issuer) {
907
195
  if (!this.callback ||
@@ -915,55 +203,9 @@ export class KvOAuthProvider {
915
203
  flowLabel(key, epoch) {
916
204
  return `${key}:${epoch}`;
917
205
  }
918
- encodeFlow(key, flow) {
919
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "encodeFlow", [key, flow]));
920
- }
921
- /** A flow record, its verifier opened only when `withVerifier`. */
922
- openFlow(key, raw, withVerifier) {
923
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openFlow", [key, raw, withVerifier]));
924
- }
925
- /** The live epoch's latest consent, until a callback claims it. */
926
- latestConsent() {
927
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "latestConsent", []));
928
- }
929
- // --- flows and epochs -----------------------------------------------------
930
- /** The live epoch; an owner with no grant yet has the initial one. */
931
- liveEpoch() {
932
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "liveEpoch", []));
933
- }
934
- /** True only after an operator disconnect, until an explicit start replaces it. */
935
- operatorDisconnected() {
936
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "operatorDisconnected", []));
937
- }
938
206
  isOperatorDisconnectedEpoch(epoch) {
939
207
  return isDisconnectedEpoch(epoch);
940
208
  }
941
- /**
942
- * Begin a flow — a connect, whose 401 may refresh or start consent, or an
943
- * `api()` call or start — in the live epoch, and return it. Nothing about
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.
946
- */
947
- beginFlow(expectedEpoch) {
948
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "beginFlow", [expectedEpoch]));
949
- }
950
- /** The epoch this flow is bound to, or the live one. */
951
- flowEpoch() {
952
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "flowEpoch", []));
953
- }
954
- /**
955
- * Bind a code exchange: to the epoch its verified consent was written in, or
956
- * the live one for an exchange no callback verified. An epoch already
957
- * replaced fails here, before the SDK can send anything. What the grant
958
- * holds now is all a refused code may invalidate.
959
- */
960
- bindFlow() {
961
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "bindFlow", []));
962
- }
963
- /** Retain an already verified snapshot only for the exact same state. */
964
- verifyCallbackState(state) {
965
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "verifyCallbackState", [state]));
966
- }
967
209
  /** Whether `verifyState` matched a consent on this provider. */
968
210
  verified() {
969
211
  return this.callback !== undefined;
@@ -996,19 +238,9 @@ export class KvOAuthProvider {
996
238
  const failure = this.refreshFailure;
997
239
  if (failure?.kind === "contended")
998
240
  return this.refreshContended();
999
- return new UnauthorizedError("Authorization required. Use authorize_connector or Connect to start consent.");
1000
- }
1001
- // --- the SDK's hooks --------------------------------------------------------
1002
- clientInformation(ctx) {
1003
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "clientInformation", [
1004
- ctx,
1005
- ]));
1006
- }
1007
- saveClientInformation(info, ctx) {
1008
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveClientInformation", [info, ctx]));
1009
- }
1010
- registrationPath() {
1011
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "registrationPath", []));
241
+ // Only the class reaches callers: remoteMcp replaces this error with its own
242
+ // typed auth error, so this text keeps the meta name.
243
+ return new UnauthorizedError(`Authorization required. Use ${META_ROUTES.authorize} or Connect to start consent.`);
1012
244
  }
1013
245
  /** Intake and saved URLs share a guard. Include the current grant's secrets
1014
246
  * so a fresh request can reject an older cache before sending credentials. */
@@ -1060,130 +292,18 @@ export class KvOAuthProvider {
1060
292
  throw new ConnectorCallError("connector_call_failed", "OAuth metadata or consent URL contains a credential; refusing authorization.", { retryable: false });
1061
293
  }
1062
294
  }
1063
- /** Only metadata bound to the discovered issuer can supply operator origins. */
1064
- validatedIssuerOrigin() {
1065
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "validatedIssuerOrigin", []));
1066
- }
1067
- discoveryState() {
1068
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "discoveryState", []));
1069
- }
1070
- /**
1071
- * Discovery names the grant's server: the same one keeps the client and
1072
- * tokens beside it, another replaces them. The SDK discovers afresh only
1073
- * without a cached record — a restart's carried client, say — and what a
1074
- * different server leaves behind would never be sent to it anyway.
1075
- */
1076
- saveDiscoveryState(state) {
1077
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveDiscoveryState", [state]));
1078
- }
1079
- /**
1080
- * The grant's tokens, for the server that issued them only. An
1081
- * issuer-aware read records what it handed the SDK, so the coordinator can
1082
- * tell a refresh another request completed since.
1083
- */
1084
- tokens(ctx) {
1085
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "tokens", [ctx]));
1086
- }
1087
- saveTokens(tokens, ctx) {
1088
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveTokens", [tokens, ctx]));
1089
- }
1090
- /**
1091
- * OAuth `state`: 256 random bits the consent URL carries and the callback
1092
- * must present, so nobody holding a consent URL can complete it with their
1093
- * own account (login CSRF). Stored, by digest, with the consent.
1094
- */
1095
- state() {
1096
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "state", []));
1097
- }
1098
- saveCodeVerifier(verifier) {
1099
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveCodeVerifier", [verifier]));
1100
- }
1101
- codeVerifier() {
1102
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "codeVerifier", []));
295
+ /** @internal The access token of the last refresh this flow adopted. */
296
+ adoptedRefresh() {
297
+ return this.storedRefresh;
1103
298
  }
1104
- /**
1105
- * Store the consent: its flow record first, by state digest with the
1106
- * link's lifetime, then the grant's pointer to it, in the flow's epoch. A
1107
- * reset that replaced the epoch fails the start, and the record is removed,
1108
- * so no start hands out a consent its callback could not complete.
1109
- */
1110
- redirectToAuthorization(authorizationUrl) {
1111
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "redirectToAuthorization", [authorizationUrl]));
1112
- }
1113
- /** The consent this provider stored, while its epoch is still live. */
1114
- consentUrl(signal = this.signal) {
1115
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "consentUrl", [signal]));
1116
- }
1117
- /** The live epoch's pending consent URL, if one is unclaimed. */
1118
- pendingAuthorizationUrl() {
1119
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "pendingAuthorizationUrl", []));
1120
- }
1121
- /**
1122
- * The pending consent URL, only while it is still worth handing out again:
1123
- * unclaimed, written within PENDING_AUTHORIZATION_MAX_AGE_MS of now in
1124
- * either direction (clock skew between isolates), and naming the grant's
1125
- * client. A callback whose exchange the server refused with
1126
- * `invalid_client` drops the client, and its URL would send the operator to
1127
- * a consent the server has already refused.
1128
- */
1129
- reusablePendingAuthorizationUrl() {
1130
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "reusablePendingAuthorizationUrl", []));
1131
- }
1132
- /**
1133
- * Find the consent a callback's `state` names. One read whatever the
1134
- * answer; the callback route's refusals pay the same. A claimed consent
1135
- * no longer matches.
1136
- */
1137
- verifyState(candidate) {
1138
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "verifyState", [candidate]));
1139
- }
1140
- /**
1141
- * @internal The first half of the fence a code exchange crosses after every
1142
- * read it depends on, just before the code leaves: claim the consent by
1143
- * compare-and-set from the exact record `verifyState` found to a claimed
1144
- * one. Of several callbacks carrying one state, exactly one wins; the
1145
- * others send nothing. A claim that fails because a restart swept the
1146
- * consent reports the supersession. The claim is spent whether or not the
1147
- * exchange succeeds; the SDK's one retry after a refused code is answered
1148
- * with that refusal, never sent again.
1149
- */
1150
- claimCodeExchange() {
1151
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimCodeExchange", []));
1152
- }
1153
- /** Terminate the verified consent on an error, sharing the code exchange CAS. */
1154
- consumeAuthError() {
1155
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "consumeAuthError", []));
1156
- }
1157
- claimConsent() {
1158
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimConsent", []));
1159
- }
1160
- /**
1161
- * @internal The second half: read the live epoch and, in the reaction to
1162
- * that read, dispatch. No await stands between the check and `send`. A
1163
- * reset published at or before the read fails the exchange with nothing
1164
- * sent; one published after it, while the request is leaving, cannot be
1165
- * ordered before the send without a lock across requests, and its tokens
1166
- * are refused by the grant's compare-and-set: one more consent.
1167
- */
1168
- dispatchCodeExchange(send, endpoint) {
1169
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "dispatchCodeExchange", [send, endpoint]));
299
+ /** @internal The stored expiry of `accessToken`, when this provider last read or wrote it. */
300
+ observedExpiry(accessToken) {
301
+ return this.observed?.accessToken === accessToken ? this.observed.expiresAt : undefined;
1170
302
  }
1171
303
  /** @internal Keep a refused code's answer for the SDK's retry to read. */
1172
304
  recordCodeExchangeRefusal(forSdk) {
1173
305
  this.exchangeRefusal = forSdk.clone();
1174
306
  }
1175
- /** @internal Observe resolution while accepting the code response, never on a later SDK save. */
1176
- recordCodeExchangeTokens(tokens) {
1177
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "recordCodeExchangeTokens", [tokens]));
1178
- }
1179
- /**
1180
- * Remove what the SDK asks, but only as this flow saw it: a refused code or
1181
- * refresh says nothing about a client or tokens another flow wrote since.
1182
- * The verifier lives in its consent, which the claim already spent.
1183
- */
1184
- invalidateCredentials(scope) {
1185
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "invalidateCredentials", [scope]));
1186
- }
1187
307
  // --- refresh, for the coordinator ---------------------------------------------
1188
308
  /**
1189
309
  * @internal How the refresh this provider owned or joined failed; undefined
@@ -1193,34 +313,10 @@ export class KvOAuthProvider {
1193
313
  recordRefreshFailure(failure) {
1194
314
  this.refreshFailure = failure;
1195
315
  }
1196
- /** @internal A shared-storage claim precedes every refresh dispatch. */
1197
- claimRefresh(epoch, requested, signal, sharedWait) {
1198
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimRefresh", [
1199
- epoch,
1200
- requested,
1201
- signal,
1202
- sharedWait,
1203
- ]));
1204
- }
1205
- /** @internal Only the successful claimed-to-dispatched CAS may send. */
1206
- dispatchRefresh(lease, send, waiting) {
1207
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "dispatchRefresh", [lease, send, waiting]));
1208
- }
1209
- /** @internal Release by CAS after a commit or a classified failure. */
1210
- releaseRefresh(lease, verdict, tokens, resolution = "ambiguous") {
1211
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "releaseRefresh", [lease, verdict, tokens, resolution]));
1212
- }
1213
- resolveRefreshSpent(lease, state) {
1214
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "resolveRefreshSpent", [lease, state]));
1215
- }
1216
316
  /** @internal The same typed verdict for local and cross-isolate joiners. */
1217
317
  refreshVerdict() {
1218
318
  return this.refreshFailure;
1219
319
  }
1220
- /** @internal The grant's tokens as stored, in the flow's epoch. */
1221
- storedTokens(signal = null) {
1222
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storedTokens", [signal]));
1223
- }
1224
320
  /** @internal True when a refresh landed after this flow's issuer-aware read. */
1225
321
  refreshedSinceRead(current, epoch) {
1226
322
  const basis = this.refreshBasis;
@@ -1236,71 +332,10 @@ export class KvOAuthProvider {
1236
332
  this.storedRefresh = tokens.access_token;
1237
333
  return tokenResponse(tokens);
1238
334
  }
1239
- /**
1240
- * @internal Store an accepted rotation in `epoch`, even after the request
1241
- * that redeemed it has left, unless a newer rotation, another server's
1242
- * grant, or another epoch is there now. A token response names no issuer:
1243
- * the one the refreshed grant was bound to is stamped here.
1244
- */
1245
- storeRefresh(epoch, requested, tokens, issuer) {
1246
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storeRefresh", [epoch, requested, tokens, issuer]));
1247
- }
1248
- /**
1249
- * @internal Drop the tokens the authorization server just refused, only
1250
- * while the grant still holds that refresh token in `epoch`: a consent
1251
- * that completed meanwhile survives. Best effort: if storage fails, the
1252
- * next refresh is refused again and ends the same way.
1253
- */
1254
- discardRefusedGrant(refreshToken, epoch, beforeWrite, signal) {
1255
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "discardRefusedGrant", [
1256
- refreshToken,
1257
- epoch,
1258
- beforeWrite,
1259
- signal,
1260
- ]));
1261
- }
1262
- // --- restart and disconnect ---------------------------------------------------
1263
- /**
1264
- * Replace the epoch, by one compare-and-set: the grant goes with it, and
1265
- * every flow bound to the old epoch fails at its next read or write. A
1266
- * forced restart may carry the registration forward (`carriableClient`);
1267
- * a disconnect leaves only its epoch, which no passive request leaves.
1268
- * Then the old epochs' consents are removed; that is hygiene, since their
1269
- * callbacks fail the epoch check anyway and the records expire.
1270
- */
1271
- resetAuthorization(operatorDisconnected = false, preserveClient = false) {
1272
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "resetAuthorization", [
1273
- operatorDisconnected,
1274
- preserveClient,
1275
- ]));
1276
- }
1277
- /** Remove the local grant before a bounded, best-effort RFC 7009 request. */
1278
- disconnectAuthorization(send) {
1279
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "disconnectAuthorization", [send]));
1280
- }
1281
335
  /** Static adapters supply trusted configuration without persisting client secrets. */
1282
336
  revocationSnapshot(body) {
1283
337
  return { body, ...(this.clientOptions?.client ? { client: this.clientOptions.client } : {}) };
1284
338
  }
1285
- performReset(operatorDisconnected, preserveClient, revoke) {
1286
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "performReset", [
1287
- operatorDisconnected,
1288
- preserveClient,
1289
- revoke,
1290
- ]));
1291
- }
1292
- /**
1293
- * Remove every consent of an epoch no longer live. The records are read
1294
- * before the epoch, and epochs never recur, so a consent whose epoch is not
1295
- * the live one then can never complete; each goes by compare-and-set
1296
- * against what was read. Best effort.
1297
- */
1298
- sweepConsents() {
1299
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "sweepConsents", []));
1300
- }
1301
- sweepRefreshLeases() {
1302
- return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "sweepRefreshLeases", []));
1303
- }
1304
339
  /**
1305
340
  * The registration a forced restart may carry into its new epoch, or
1306
341
  * nothing. It must be bound to a server (`issuer`) and registered under this
@@ -1348,1362 +383,11 @@ export class KvOAuthProvider {
1348
383
  };
1349
384
  }
1350
385
  static {
1351
- registerAuthMethod(KvOAuthProvider.prototype, "wait", function (work, signal) {
1352
- signal = signal === undefined ? this.signal : signal;
1353
- return authFlow(function* () {
1354
- if (signal?.aborted)
1355
- throw aborted(signal);
1356
- const value = signal
1357
- ? (yield authAwait(authCall(work, []), { signal }.signal))
1358
- : (yield authCall(work, []));
1359
- if (signal?.aborted)
1360
- throw aborted(signal);
1361
- return value;
1362
- });
1363
- }, KvOAuthProvider.prototype.wait);
1364
- registerAuthMethod(KvOAuthProvider.prototype, "encodeGrant", function (grant, signal) {
1365
- signal = signal === undefined ? this.signal : signal;
1366
- return authFlow(function* () {
1367
- this.assertCredentialFree({ issuer: grant.body.issuer, discovery: grant.body.discovery }, grant.body);
1368
- const stored = {
1369
- connectaOAuth: GRANT_VERSION,
1370
- epoch: grant.epoch,
1371
- ...(grant.flow !== undefined ? { flow: grant.flow } : {}),
1372
- };
1373
- if (Object.keys(grant.body).length > 0) {
1374
- if (this.sealer) {
1375
- stored.sealed = (yield authInvoke(this, "wait", [
1376
- authMethod(() => {
1377
- return authInvoke(this.sealer, "seal", [
1378
- this.grantLabel(grant.epoch),
1379
- JSON.stringify(grant.body),
1380
- ]);
1381
- }),
1382
- signal,
1383
- ]));
1384
- }
1385
- else {
1386
- stored.body = grant.body;
1387
- }
1388
- }
1389
- return JSON.stringify(stored);
1390
- }.bind(this));
1391
- }, KvOAuthProvider.prototype.encodeGrant);
1392
- registerAuthMethod(KvOAuthProvider.prototype, "openGrant", function (raw, signal) {
1393
- signal = signal === undefined ? this.signal : signal;
1394
- return authFlow(function* () {
1395
- if (raw === null)
1396
- return { epoch: INITIAL_EPOCH, body: {} };
1397
- const stored = storedGrant(raw);
1398
- if (!stored)
1399
- return { epoch: UNREADABLE_EPOCH, body: {} };
1400
- let body = stored.body;
1401
- if (stored.sealed !== undefined) {
1402
- body = undefined;
1403
- try {
1404
- if (this.sealer)
1405
- body = parsed((yield authInvoke(this, "wait", [
1406
- authMethod(() => {
1407
- return authInvoke(this.sealer, "open", [
1408
- this.grantLabel(stored.epoch),
1409
- stored.sealed,
1410
- ]);
1411
- }),
1412
- signal,
1413
- ])));
1414
- }
1415
- catch {
1416
- if (signal?.aborted)
1417
- throw aborted(signal);
1418
- this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth grant ` +
1419
- "the configured vault cannot open; treating it as absent, so the " +
1420
- "connector needs authorization again. A changed vault key or " +
1421
- "tampered storage causes this.");
1422
- }
1423
- }
1424
- return {
1425
- epoch: stored.epoch,
1426
- ...(stored.flow !== undefined ? { flow: stored.flow } : {}),
1427
- body: grantBody(body),
1428
- };
1429
- }.bind(this));
1430
- }, KvOAuthProvider.prototype.openGrant);
1431
- registerAuthMethod(KvOAuthProvider.prototype, "storedAuthorization", function (issuer) {
1432
- return authFlow(function* () {
1433
- const raw = (yield authStorage(this.storage, "get", [GRANT]));
1434
- const grant = (yield authInvoke(this, "openGrant", [raw]));
1435
- if (isDisconnectedEpoch(grant.epoch))
1436
- return false;
1437
- const body = grant.body;
1438
- const configured = issuer ?? this.clientOptions?.client?.issuer;
1439
- const discovered = body.discovery;
1440
- if (!configured &&
1441
- (!discovered || discovered.authorizationServerMetadata?.issuer !== discovered.authorizationServerUrl))
1442
- return false;
1443
- const expected = configured ?? discoveryIssuer(discovered);
1444
- return Boolean(expected && body.issuer === expected && body.tokens?.access_token);
1445
- }.bind(this)).pipe(Effect.catch(() => Effect.succeed(false)));
1446
- }, KvOAuthProvider.prototype.storedAuthorization);
1447
- registerAuthMethod(KvOAuthProvider.prototype, "readGrant", function (signal) {
1448
- signal = signal === undefined ? this.signal : signal;
1449
- return authFlow(function* () {
1450
- let raw = (yield authInvoke(this, "wait", [
1451
- authMethod(() => {
1452
- return authStorage(this.storage, "get", [GRANT]);
1453
- }),
1454
- signal,
1455
- ]));
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
- const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
1468
- if (raw !== null && this.sealer && storedGrant(raw)?.body !== undefined) {
1469
- const sealed = (yield authInvoke(this, "encodeGrant", [grant, signal]));
1470
- if ((yield authInvoke(this, "wait", [
1471
- authMethod(() => {
1472
- return authCatch(authStorage(this.storage, "compareAndSet", [GRANT, raw, sealed]), () => false);
1473
- }),
1474
- signal,
1475
- ])))
1476
- raw = sealed;
1477
- }
1478
- return { raw, grant };
1479
- }.bind(this));
1480
- }, KvOAuthProvider.prototype.readGrant);
1481
- registerAuthMethod(KvOAuthProvider.prototype, "boundGrant", function (signal) {
1482
- signal = signal === undefined ? this.signal : signal;
1483
- return authFlow(function* () {
1484
- const { grant } = (yield authInvoke(this, "readGrant", [signal]));
1485
- if (this.bound && grant.epoch !== this.epoch)
1486
- throw this.flowSuperseded();
1487
- return grant;
1488
- }.bind(this));
1489
- }, KvOAuthProvider.prototype.boundGrant);
1490
- registerAuthMethod(KvOAuthProvider.prototype, "activeGrant", function () {
1491
- return authInvoke(this, "boundGrant", []);
1492
- }, KvOAuthProvider.prototype.activeGrant);
1493
- registerAuthMethod(KvOAuthProvider.prototype, "updateGrant", function (change, options = {}) {
1494
- return authFlow(function* () {
1495
- const signal = options.signal ?? (options.commit ? null : this.signal);
1496
- for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
1497
- if (!options.commit && this.signal?.aborted)
1498
- return false;
1499
- const { raw, grant } = (yield authInvoke(this, "readGrant", [signal]));
1500
- const epoch = options.epoch ?? (this.epoch ??= grant.epoch);
1501
- if (grant.epoch !== epoch) {
1502
- if (options.quiet || !this.bound)
1503
- return false;
1504
- throw this.flowSuperseded();
1505
- }
1506
- // A disconnect's epoch holds nothing until an explicit start replaces it.
1507
- if (isDisconnectedEpoch(epoch))
1508
- return false;
1509
- const next = change(grant);
1510
- if (next === undefined)
1511
- return true;
1512
- const encoded = (yield authInvoke(this, "encodeGrant", [next, signal]));
1513
- if (!options.commit && this.signal?.aborted)
1514
- return false;
1515
- let written;
1516
- try {
1517
- written = (yield authInvoke(this, "wait", [
1518
- authMethod(() => {
1519
- return authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded], options.beforeWrite && (() => options.beforeWrite?.()));
1520
- }),
1521
- signal,
1522
- ]));
1523
- }
1524
- catch {
1525
- if (signal?.aborted)
1526
- throw aborted(signal);
1527
- // A store's own error can quote the value it refused.
1528
- throw this.credentialWriteError();
1529
- }
1530
- if (written) {
1531
- if (next.epoch !== grant.epoch)
1532
- this.epoch = next.epoch;
1533
- return true;
1534
- }
1535
- }
1536
- throw this.credentialWriteError();
1537
- }.bind(this));
1538
- }, KvOAuthProvider.prototype.updateGrant);
1539
- registerAuthMethod(KvOAuthProvider.prototype, "writeCredential", function (field, value, issuer, refreshConsent) {
1540
- return authFlow(function* () {
1541
- (yield authInvoke(this, "updateGrant", [
1542
- (grant) => {
1543
- const body = issuer !== undefined && grant.body.issuer !== issuer ? { issuer } : { ...grant.body };
1544
- if (field === "client")
1545
- body.client = value;
1546
- else {
1547
- body.tokens = value;
1548
- delete body.refreshConsent;
1549
- if (refreshConsent)
1550
- body.refreshConsent = refreshConsent;
1551
- }
1552
- return this.withIssuer(grant, body);
1553
- },
1554
- ]));
1555
- // A flow may invalidate what it wrote itself.
1556
- this.seen[field] = fingerprint(field === "client" ? value.value : value);
1557
- }.bind(this));
1558
- }, KvOAuthProvider.prototype.writeCredential);
1559
- registerAuthMethod(KvOAuthProvider.prototype, "bindingForGrant", function (grant) {
1560
- return authFlow(function* () {
1561
- const configured = this.configuredAuthorizationBinding();
1562
- return configured
1563
- ? { ...configured, issuer: grant.body.issuer }
1564
- : {
1565
- ...(yield authInvoke(this, "wait", [
1566
- authMethod(() => {
1567
- return authCall(discoveryBinding, [grant.body.discovery]);
1568
- }),
1569
- ])),
1570
- issuer: grant.body.issuer,
1571
- clientId: grant.body.client?.value.client_id,
1572
- };
1573
- }.bind(this));
1574
- }, KvOAuthProvider.prototype.bindingForGrant);
1575
- registerAuthMethod(KvOAuthProvider.prototype, "checkFlowBinding", function (grant) {
1576
- return authFlow(function* () {
1577
- const binding = this.callback?.flow.binding;
1578
- if (!binding ||
1579
- fingerprint(binding) !== fingerprint((yield authInvoke(this, "bindingForGrant", [grant])))) {
1580
- throw this.flowSuperseded();
1581
- }
1582
- }.bind(this));
1583
- }, KvOAuthProvider.prototype.checkFlowBinding);
1584
- registerAuthMethod(KvOAuthProvider.prototype, "encodeFlow", function (key, flow) {
1585
- return authFlow(function* () {
1586
- const { verifier, ...rest } = flow;
1587
- const stored = { connectaOAuthFlow: FLOW_VERSION, ...rest };
1588
- if (verifier !== undefined) {
1589
- if (this.sealer) {
1590
- stored.verifier = (yield authInvoke(this, "wait", [
1591
- authMethod(() => {
1592
- return authInvoke(this.sealer, "seal", [
1593
- this.flowLabel(key, flow.epoch),
1594
- verifier,
1595
- ]);
1596
- }),
1597
- ]));
1598
- stored.sealed = true;
1599
- }
1600
- else {
1601
- stored.verifier = verifier;
1602
- }
1603
- }
1604
- return JSON.stringify(stored);
1605
- }.bind(this));
1606
- }, KvOAuthProvider.prototype.encodeFlow);
1607
- registerAuthMethod(KvOAuthProvider.prototype, "openFlow", function (key, raw, withVerifier) {
1608
- return authFlow(function* () {
1609
- const value = parsed(raw);
1610
- if (!plainObject(value) ||
1611
- value.connectaOAuthFlow !== FLOW_VERSION ||
1612
- typeof value.epoch !== "string" ||
1613
- typeof value.at !== "number" ||
1614
- (value.url !== undefined && typeof value.url !== "string") ||
1615
- (value.verifier !== undefined && typeof value.verifier !== "string")) {
1616
- return undefined;
1617
- }
1618
- const flow = {
1619
- epoch: value.epoch,
1620
- at: value.at,
1621
- ...(plainObject(value.binding) ? { binding: value.binding } : {}),
1622
- ...(typeof value.url === "string" ? { url: value.url } : {}),
1623
- ...(value.consumed === true ? { consumed: true } : {}),
1624
- };
1625
- if (!withVerifier || typeof value.verifier !== "string")
1626
- return flow;
1627
- if (value.sealed !== true)
1628
- return { ...flow, verifier: value.verifier };
1629
- try {
1630
- if (!this.sealer)
1631
- return undefined;
1632
- return {
1633
- ...flow,
1634
- verifier: (yield authInvoke(this, "wait", [
1635
- authMethod(() => {
1636
- return authInvoke(this.sealer, "open", [
1637
- this.flowLabel(key, flow.epoch),
1638
- value.verifier,
1639
- ]);
1640
- }),
1641
- ])),
1642
- };
1643
- }
1644
- catch {
1645
- if (this.signal?.aborted)
1646
- throw aborted(this.signal);
1647
- this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth consent ` +
1648
- "the configured vault cannot open; its callback is refused.");
1649
- return undefined;
1650
- }
1651
- }.bind(this));
1652
- }, KvOAuthProvider.prototype.openFlow);
1653
- registerAuthMethod(KvOAuthProvider.prototype, "latestConsent", function () {
1654
- return authFlow(function* () {
1655
- const grant = (yield authInvoke(this, "boundGrant", []));
1656
- if (grant.flow === undefined)
1657
- return undefined;
1658
- const key = oauthFlowKeys.flow(grant.flow);
1659
- const raw = (yield authInvoke(this, "wait", [
1660
- authMethod(() => {
1661
- return authStorage(this.storage, "get", [key]);
1662
- }),
1663
- ]));
1664
- const flow = raw === null ? undefined : (yield authInvoke(this, "openFlow", [key, raw, false]));
1665
- return flow && !flow.consumed && flow.epoch === grant.epoch && flow.url !== undefined
1666
- ? { ...flow, url: flow.url }
1667
- : undefined;
1668
- }.bind(this));
1669
- }, KvOAuthProvider.prototype.latestConsent);
1670
- registerAuthMethod(KvOAuthProvider.prototype, "liveEpoch", function () {
1671
- return authFlow(function* () {
1672
- return (yield authInvoke(this, "readGrant", [])).grant.epoch;
1673
- }.bind(this));
1674
- }, KvOAuthProvider.prototype.liveEpoch);
1675
- registerAuthMethod(KvOAuthProvider.prototype, "operatorDisconnected", function () {
1676
- return authFlow(function* () {
1677
- return isDisconnectedEpoch((yield authInvoke(this, "liveEpoch", [])));
1678
- }.bind(this));
1679
- }, KvOAuthProvider.prototype.operatorDisconnected);
1680
- registerAuthMethod(KvOAuthProvider.prototype, "beginFlow", function (expectedEpoch) {
1681
- return authFlow(function* () {
1682
- const epoch = (yield authInvoke(this, "liveEpoch", []));
1683
- if (expectedEpoch !== undefined && epoch !== expectedEpoch)
1684
- throw this.flowSuperseded();
1685
- this.epoch = epoch;
1686
- this.bound = true;
1687
- return epoch;
1688
- }.bind(this));
1689
- }, KvOAuthProvider.prototype.beginFlow);
1690
- registerAuthMethod(KvOAuthProvider.prototype, "flowEpoch", function () {
1691
- return authFlow(function* () {
1692
- return this.epoch ?? (yield authInvoke(this, "liveEpoch", []));
1693
- }.bind(this));
1694
- }, KvOAuthProvider.prototype.flowEpoch);
1695
- registerAuthMethod(KvOAuthProvider.prototype, "bindFlow", function () {
1696
- return authFlow(function* () {
1697
- const { grant } = (yield authInvoke(this, "readGrant", []));
1698
- const epoch = this.callback?.flow.epoch ?? this.epoch ?? grant.epoch;
1699
- if (grant.epoch !== epoch)
1700
- throw this.flowSuperseded();
1701
- this.epoch = epoch;
1702
- this.bound = true;
1703
- this.codeExchange = true;
1704
- (yield authInvoke(this, "checkFlowBinding", [grant]));
1705
- this.seen = {
1706
- client: fingerprint(grant.body.client?.value),
1707
- tokens: fingerprint(grant.body.tokens),
1708
- };
1709
- }.bind(this));
1710
- }, KvOAuthProvider.prototype.bindFlow);
1711
- registerAuthMethod(KvOAuthProvider.prototype, "verifyCallbackState", function (state) {
1712
- return authFlow(function* () {
1713
- if (this.callback?.key ===
1714
- oauthFlowKeys.flow((yield authInvoke(this, "wait", [
1715
- authMethod(() => {
1716
- return authCall(oauthStateDigest, [state]);
1717
- }),
1718
- ]))))
1719
- return true;
1720
- return (yield authInvoke(this, "verifyState", [state]));
1721
- }.bind(this));
1722
- }, KvOAuthProvider.prototype.verifyCallbackState);
1723
- registerAuthMethod(KvOAuthProvider.prototype, "clientInformation", function (ctx) {
1724
- return authFlow(function* () {
1725
- if (this.signal?.aborted)
1726
- throw aborted(this.signal);
1727
- const { body } = (yield authInvoke(this, "activeGrant", []));
1728
- if (this.signal?.aborted)
1729
- throw aborted(this.signal);
1730
- const client = this.clientOptions?.client;
1731
- if (!body.client && !this.allowAuthorization)
1732
- throw this.authorizationRefused();
1733
- if (client) {
1734
- if ((ctx && ctx.issuer !== client.issuer) || body.issuer !== client.issuer)
1735
- throw this.flowSuperseded();
1736
- if (body.client &&
1737
- (body.client.value.client_id !== client.clientId || body.client.binding !== this.clientBinding))
1738
- throw this.flowSuperseded();
1739
- return {
1740
- client_id: client.clientId,
1741
- issuer: client.issuer,
1742
- token_endpoint_auth_method: this.clientMetadata.token_endpoint_auth_method,
1743
- ...(client.clientSecret !== undefined ? { client_secret: client.clientSecret } : {}),
1744
- };
1745
- }
1746
- if (!body.client || (ctx && body.issuer !== ctx.issuer))
1747
- return undefined;
1748
- this.seen.client = fingerprint(body.client.value);
1749
- return ctx ? { ...body.client.value, issuer: ctx.issuer } : body.client.value;
1750
- }.bind(this));
1751
- }, KvOAuthProvider.prototype.clientInformation);
1752
- registerAuthMethod(KvOAuthProvider.prototype, "saveClientInformation", function (info, ctx) {
1753
- return authFlow(function* () {
1754
- try {
1755
- const { body } = (yield authInvoke(this, "boundGrant", []));
1756
- const registrationPath = this.clientOptions?.client
1757
- ? "static"
1758
- : body.client?.value.client_id === info.client_id && body.client.registrationPath
1759
- ? body.client.registrationPath
1760
- : this.clientMetadataUrl &&
1761
- body.discovery?.authorizationServerMetadata?.client_id_metadata_document_supported === true
1762
- ? "cimd"
1763
- : "dcr";
1764
- (yield authInvoke(this, "writeCredential", [
1765
- "client",
1766
- {
1767
- value: this.clientOptions?.client ? { client_id: this.clientOptions.client.clientId } : info,
1768
- registrationPath,
1769
- ...(this.clientBinding !== undefined ? { binding: this.clientBinding } : {}),
1770
- },
1771
- ctx?.issuer,
1772
- ]));
1773
- }
1774
- catch (error) {
1775
- if (!this.signal?.aborted)
1776
- throw error;
1777
- }
1778
- }.bind(this));
1779
- }, KvOAuthProvider.prototype.saveClientInformation);
1780
- registerAuthMethod(KvOAuthProvider.prototype, "registrationPath", function () {
1781
- return authFlow(function* () {
1782
- const { body } = (yield authInvoke(this, "boundGrant", []));
1783
- return body.client?.registrationPath;
1784
- }.bind(this));
1785
- }, KvOAuthProvider.prototype.registrationPath);
1786
- registerAuthMethod(KvOAuthProvider.prototype, "validatedIssuerOrigin", function () {
1787
- return authFlow(function* () {
1788
- const { body } = (yield authInvoke(this, "boundGrant", []));
1789
- const state = body.discovery;
1790
- if (!state ||
1791
- state.authorizationServerMetadata?.issuer !== state.authorizationServerUrl ||
1792
- body.issuer !== state.authorizationServerUrl)
1793
- return undefined;
1794
- this.assertCredentialFree(state, body);
1795
- return new URL(state.authorizationServerUrl).origin;
1796
- }.bind(this));
1797
- }, KvOAuthProvider.prototype.validatedIssuerOrigin);
1798
- registerAuthMethod(KvOAuthProvider.prototype, "discoveryState", function () {
1799
- return authFlow(function* () {
1800
- if (this.signal?.aborted)
1801
- throw aborted(this.signal);
1802
- const { body } = (yield authInvoke(this, "activeGrant", []));
1803
- if (this.signal?.aborted)
1804
- throw aborted(this.signal);
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
- }
1824
- this.assertCredentialFree(state, body);
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;
1828
- return state;
1829
- }.bind(this));
1830
- }, KvOAuthProvider.prototype.discoveryState);
1831
- registerAuthMethod(KvOAuthProvider.prototype, "saveDiscoveryState", function (state) {
1832
- return authFlow(function* () {
1833
- this.assertCredentialFree(state, (yield authInvoke(this, "boundGrant", [])).body);
1834
- const issuer = discoveryIssuer(state);
1835
- if (this.clientOptions?.client && issuer !== this.clientOptions.client.issuer)
1836
- throw this.flowSuperseded();
1837
- if (!this.allowAuthorization && !(yield authInvoke(this, "boundGrant", [])).body.client)
1838
- throw this.authorizationRefused();
1839
- this.consentDiscovery = state;
1840
- (yield authInvoke(this, "updateGrant", [
1841
- (grant) => {
1842
- const body = grant.body.issuer !== issuer ? (issuer !== undefined ? { issuer } : {}) : { ...grant.body };
1843
- this.assertCredentialFree(state, grant.body);
1844
- body.discovery = state;
1845
- return this.withIssuer(grant, body);
1846
- },
1847
- ]));
1848
- }.bind(this));
1849
- }, KvOAuthProvider.prototype.saveDiscoveryState);
1850
- registerAuthMethod(KvOAuthProvider.prototype, "tokens", function (ctx) {
1851
- return authFlow(function* () {
1852
- if (this.signal?.aborted)
1853
- throw aborted(this.signal);
1854
- const grant = (yield authInvoke(this, "activeGrant", []));
1855
- if (this.signal?.aborted)
1856
- throw aborted(this.signal);
1857
- const { tokens } = grant.body;
1858
- if (!tokens || (ctx && grant.body.issuer !== ctx.issuer))
1859
- return undefined;
1860
- this.seen.tokens = fingerprint(tokens);
1861
- if (!ctx)
1862
- return tokens;
1863
- this.refreshBasis = { accessToken: tokens.access_token, epoch: grant.epoch, issuer: ctx.issuer };
1864
- return { ...tokens, issuer: ctx.issuer };
1865
- }.bind(this));
1866
- }, KvOAuthProvider.prototype.tokens);
1867
- registerAuthMethod(KvOAuthProvider.prototype, "saveTokens", function (tokens, ctx) {
1868
- return authFlow(function* () {
1869
- if (this.signal?.aborted)
1870
- throw aborted(this.signal);
1871
- if (this.refreshFailure?.kind === "dead")
1872
- throw this.authorizationRefused();
1873
- // The coordinator stored this refresh already, or handed on another's.
1874
- if (this.storedRefresh !== undefined && tokens.access_token === this.storedRefresh)
1875
- return;
1876
- const refreshConsent = this.exchangeConsent &&
1877
- tokens.refresh_token !== undefined &&
1878
- this.exchangeConsent.digest ===
1879
- (yield authInvoke(this, "wait", [
1880
- authMethod(() => {
1881
- return authCall(oauthStateDigest, [tokens.refresh_token]);
1882
- }),
1883
- ]))
1884
- ? this.exchangeConsent
1885
- : undefined;
1886
- (yield authInvoke(this, "writeCredential", ["tokens", tokens, ctx?.issuer, refreshConsent]));
1887
- this.refreshFailure = undefined;
1888
- }.bind(this));
1889
- }, KvOAuthProvider.prototype.saveTokens);
1890
- registerAuthMethod(KvOAuthProvider.prototype, "state", function () {
1891
- return authValue(() => {
1892
- if (!this.allowAuthorization)
1893
- throw this.authorizationRefused();
1894
- const state = randomState();
1895
- this.consent = { state };
1896
- return state;
1897
- });
1898
- }, KvOAuthProvider.prototype.state);
1899
- registerAuthMethod(KvOAuthProvider.prototype, "saveCodeVerifier", function (verifier) {
1900
- return authValue(() => {
1901
- if (this.signal?.aborted)
1902
- throw aborted(this.signal);
1903
- if (!this.allowAuthorization)
1904
- throw this.authorizationRefused();
1905
- this.consent = { ...this.consent, verifier };
1906
- });
1907
- }, KvOAuthProvider.prototype.saveCodeVerifier);
1908
- registerAuthMethod(KvOAuthProvider.prototype, "codeVerifier", function () {
1909
- return authValue(() => {
1910
- const verifier = this.callback?.flow.verifier ?? this.consent.verifier;
1911
- if (verifier === undefined) {
1912
- throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
1913
- }
1914
- return verifier;
1915
- });
1916
- }, KvOAuthProvider.prototype.codeVerifier);
1917
- registerAuthMethod(KvOAuthProvider.prototype, "redirectToAuthorization", function (authorizationUrl) {
1918
- return authFlow(function* () {
1919
- if (this.signal?.aborted)
1920
- return;
1921
- if (!this.allowAuthorization)
1922
- throw this.authorizationRefused();
1923
- for (const [name, value] of this.clientOptions?.authorizationParams ?? []) {
1924
- authorizationUrl.searchParams.set(name, value);
1925
- }
1926
- this.assertCredentialFree(authorizationUrl.toString(), (yield authInvoke(this, "boundGrant", [])).body, true);
1927
- const state = this.consent.state ?? authorizationUrl.searchParams.get("state");
1928
- if (!state)
1929
- throw new Error(`OAuth consent for "${this.connectorId}" carries no state`);
1930
- const client = this.clientOptions?.client;
1931
- if (client && !(yield authInvoke(this, "boundGrant", [])).body.client) {
1932
- (yield authInvoke(this, "saveClientInformation", [
1933
- { client_id: client.clientId },
1934
- { issuer: client.issuer },
1935
- ]));
1936
- }
1937
- const configured = this.configuredAuthorizationBinding();
1938
- if (configured?.issuer !== undefined) {
1939
- const issuer = configured.issuer;
1940
- // Static adapters do not save discovery or client configuration. Publish
1941
- // their server identity before consent so a changed endpoint fences the
1942
- // old grant before any code exchange can leave.
1943
- (yield authInvoke(this, "updateGrant", [
1944
- (grant) => (grant.body.issuer === issuer ? undefined : this.withIssuer(grant, { issuer })),
1945
- ]));
1946
- }
1947
- const epoch = (yield authInvoke(this, "flowEpoch", []));
1948
- this.epoch = epoch;
1949
- const digest = (yield authInvoke(this, "wait", [
1950
- authMethod(() => {
1951
- return authCall(oauthStateDigest, [state]);
1952
- }),
1953
- ]));
1954
- const key = oauthFlowKeys.flow(digest);
1955
- const url = authorizationUrl.toString();
1956
- const { verifier } = this.consent;
1957
- const grant = (yield authInvoke(this, "boundGrant", []));
1958
- this.assertCredentialFree(url, grant.body, true);
1959
- const binding = this.configuredAuthorizationBinding() ?? {
1960
- ...(yield authInvoke(this, "wait", [
1961
- authMethod(() => {
1962
- return authCall(discoveryBinding, [this.consentDiscovery ?? grant.body.discovery]);
1963
- }),
1964
- ])),
1965
- issuer: discoveryIssuer(this.consentDiscovery) ?? grant.body.issuer,
1966
- clientId: authorizationUrl.searchParams.get("client_id") ?? grant.body.client?.value.client_id,
1967
- };
1968
- const record = (yield authInvoke(this, "encodeFlow", [
1969
- key,
1970
- {
1971
- epoch,
1972
- at: Date.now(),
1973
- binding,
1974
- url,
1975
- ...(verifier !== undefined ? { verifier } : {}),
1976
- },
1977
- ]));
1978
- if (this.signal?.aborted)
1979
- return;
1980
- if (!(yield authInvoke(this, "wait", [
1981
- authMethod(() => {
1982
- return authStorage(this.storage, "compareAndSet", [
1983
- key,
1984
- null,
1985
- record,
1986
- { ttlSeconds: OAUTH_FLOW_TTL_SECONDS },
1987
- ]);
1988
- }),
1989
- ]))) {
1990
- throw new Error(`OAuth consent for "${this.connectorId}" reused a state`);
1991
- }
1992
- let published = false;
1993
- try {
1994
- published = (yield authInvoke(this, "updateGrant", [
1995
- (grant) => ({ ...grant, flow: digest }),
1996
- { epoch },
1997
- ]));
1998
- }
1999
- finally {
2000
- if (!published)
2001
- (yield authCatch(authInvoke(this, "wait", [
2002
- authMethod(() => {
2003
- return authStorage(this.storage, "compareAndSet", [key, record, null]);
2004
- }),
2005
- ]), () => { }));
2006
- }
2007
- if (published)
2008
- this.published = { epoch, url };
2009
- }.bind(this));
2010
- }, KvOAuthProvider.prototype.redirectToAuthorization);
2011
- registerAuthMethod(KvOAuthProvider.prototype, "consentUrl", function (signal) {
2012
- signal = signal === undefined ? this.signal : signal;
2013
- return authFlow(function* () {
2014
- const published = this.published;
2015
- if (!published)
2016
- return undefined;
2017
- if (epochOf((yield authInvoke(this, "wait", [
2018
- authMethod(() => {
2019
- return authStorage(this.storage, "get", [GRANT]);
2020
- }),
2021
- signal,
2022
- ]))) !== published.epoch)
2023
- throw this.flowSuperseded();
2024
- this.assertCredentialFree(published.url, (yield authInvoke(this, "boundGrant", [signal])).body, true);
2025
- return published.url;
2026
- }.bind(this));
2027
- }, KvOAuthProvider.prototype.consentUrl);
2028
- registerAuthMethod(KvOAuthProvider.prototype, "pendingAuthorizationUrl", function () {
2029
- return authFlow(function* () {
2030
- const url = (yield authInvoke(this, "latestConsent", []))?.url;
2031
- this.assertCredentialFree(url, (yield authInvoke(this, "boundGrant", [])).body, true);
2032
- return url;
2033
- }.bind(this));
2034
- }, KvOAuthProvider.prototype.pendingAuthorizationUrl);
2035
- registerAuthMethod(KvOAuthProvider.prototype, "reusablePendingAuthorizationUrl", function () {
2036
- return authFlow(function* () {
2037
- const consent = (yield authInvoke(this, "latestConsent", []));
2038
- if (!consent || Math.abs(Date.now() - consent.at) >= PENDING_AUTHORIZATION_MAX_AGE_MS) {
2039
- return undefined;
2040
- }
2041
- this.assertCredentialFree(consent.url, (yield authInvoke(this, "boundGrant", [])).body, true);
2042
- let clientId;
2043
- try {
2044
- clientId = new URL(consent.url).searchParams.get("client_id");
2045
- }
2046
- catch {
2047
- return undefined;
2048
- }
2049
- const client = (yield authInvoke(this, "clientInformation", []));
2050
- return clientId !== null && client?.client_id === clientId ? consent.url : undefined;
2051
- }.bind(this));
2052
- }, KvOAuthProvider.prototype.reusablePendingAuthorizationUrl);
2053
- registerAuthMethod(KvOAuthProvider.prototype, "verifyState", function (candidate) {
2054
- return authFlow(function* () {
2055
- this.callback = undefined;
2056
- if (candidate === null)
2057
- return false;
2058
- const key = oauthFlowKeys.flow((yield authInvoke(this, "wait", [
2059
- authMethod(() => {
2060
- return authCall(oauthStateDigest, [candidate]);
2061
- }),
2062
- ])));
2063
- const raw = (yield authInvoke(this, "wait", [
2064
- authMethod(() => {
2065
- return authStorage(this.storage, "get", [key]);
2066
- }),
2067
- ]));
2068
- const flow = raw === null ? undefined : (yield authInvoke(this, "openFlow", [key, raw, true]));
2069
- if (raw === null || !flow || flow.consumed)
2070
- return false;
2071
- this.callback = { key, raw, flow, claimed: false };
2072
- this.epoch = flow.epoch;
2073
- return true;
2074
- }.bind(this));
2075
- }, KvOAuthProvider.prototype.verifyState);
2076
- registerAuthMethod(KvOAuthProvider.prototype, "claimCodeExchange", function () {
2077
- return authFlow(function* () {
2078
- if (this.exchangeRefusal)
2079
- return this.exchangeRefusal.clone();
2080
- (yield authInvoke(this, "claimConsent", []));
2081
- return undefined;
2082
- }.bind(this));
2083
- }, KvOAuthProvider.prototype.claimCodeExchange);
2084
- registerAuthMethod(KvOAuthProvider.prototype, "consumeAuthError", function () {
2085
- return authFlow(function* () {
2086
- const { grant } = (yield authInvoke(this, "readGrant", []));
2087
- if (!this.callback || grant.epoch !== this.callback.flow.epoch)
2088
- throw this.flowSuperseded();
2089
- (yield authInvoke(this, "checkFlowBinding", [grant]));
2090
- (yield authInvoke(this, "claimConsent", []));
2091
- this.callback = undefined;
2092
- }.bind(this));
2093
- }, KvOAuthProvider.prototype.consumeAuthError);
2094
- registerAuthMethod(KvOAuthProvider.prototype, "claimConsent", function () {
2095
- return authFlow(function* () {
2096
- const callback = this.callback;
2097
- if (!callback)
2098
- throw this.flowSuperseded();
2099
- if (callback.claimed)
2100
- throw new OAuthCallbackClaimedError(this.connectorId);
2101
- callback.claimed = true;
2102
- const { epoch, at } = callback.flow;
2103
- const claimed = JSON.stringify({
2104
- connectaOAuthFlow: FLOW_VERSION,
2105
- epoch,
2106
- at,
2107
- consumed: true,
2108
- });
2109
- const remaining = Math.ceil((at + OAUTH_FLOW_TTL_SECONDS * 1000 - Date.now()) / 1000);
2110
- const ttlSeconds = Math.min(OAUTH_FLOW_TTL_SECONDS, Math.max(1, remaining));
2111
- if ((yield authInvoke(this, "wait", [
2112
- authMethod(() => {
2113
- return authStorage(this.storage, "compareAndSet", [
2114
- callback.key,
2115
- callback.raw,
2116
- claimed,
2117
- { ttlSeconds },
2118
- ]);
2119
- }),
2120
- ]))) {
2121
- return;
2122
- }
2123
- if (epochOf((yield authInvoke(this, "wait", [
2124
- authMethod(() => {
2125
- return authStorage(this.storage, "get", [GRANT]);
2126
- }),
2127
- ]))) !== epoch)
2128
- throw this.flowSuperseded();
2129
- throw new OAuthCallbackClaimedError(this.connectorId);
2130
- }.bind(this));
2131
- }, KvOAuthProvider.prototype.claimConsent);
2132
- registerAuthMethod(KvOAuthProvider.prototype, "dispatchCodeExchange", function (send, endpoint) {
2133
- return authFlow(function* () {
2134
- const epoch = this.epoch;
2135
- if (!this.codeExchange || epoch === undefined)
2136
- throw this.flowSuperseded();
2137
- const raw = (yield authInvoke(this, "wait", [
2138
- authMethod(() => {
2139
- return authStorage(this.storage, "get", [GRANT]);
2140
- }),
2141
- ]));
2142
- const grant = (yield authInvoke(this, "openGrant", [raw]));
2143
- if (grant.epoch !== epoch)
2144
- throw this.flowSuperseded();
2145
- (yield authInvoke(this, "checkFlowBinding", [grant]));
2146
- if (endpoint !== undefined && this.callback?.flow.binding?.tokenEndpoint !== String(endpoint)) {
2147
- throw this.flowSuperseded();
2148
- }
2149
- // Opening ciphertext may yield. Re-read the exact record after opening,
2150
- // then check and send in the same reaction, without another await.
2151
- return (yield authInvoke(this, "wait", [
2152
- authMethod(() => {
2153
- return authThen(authStorage(this.storage, "get", [GRANT]), authMethod((current) => {
2154
- return authFlow(function* () {
2155
- if (this.signal?.aborted)
2156
- throw aborted(this.signal);
2157
- if (current !== raw)
2158
- throw this.flowSuperseded();
2159
- const sent = send();
2160
- sent.catch(() => { });
2161
- return (yield authValue(() => sent));
2162
- }.bind(this));
2163
- }));
2164
- }),
2165
- ]));
2166
- }.bind(this));
2167
- }, KvOAuthProvider.prototype.dispatchCodeExchange);
2168
- registerAuthMethod(KvOAuthProvider.prototype, "recordCodeExchangeTokens", function (tokens) {
2169
- return authFlow(function* () {
2170
- this.exchangeConsent = undefined;
2171
- if (tokens.refresh_token === undefined)
2172
- return;
2173
- try {
2174
- const digest = (yield authInvoke(this, "wait", [
2175
- authMethod(() => {
2176
- return authCall(oauthStateDigest, [tokens.refresh_token]);
2177
- }),
2178
- ]));
2179
- const resolution = (yield authInvoke(this, "wait", [
2180
- authMethod(() => {
2181
- return authStorage(this.storage, "get", [oauthRefreshSpentKeys.spent(digest)]);
2182
- }),
2183
- ]));
2184
- const spent = storedRefreshSpent(resolution);
2185
- // Observing the exact resolved storage write before consent completion
2186
- // proves their order without comparing isolate clocks. A consent that
2187
- // saw outstanding/ambiguous cannot become eligible on a delayed save.
2188
- if (spent?.state === "resolved")
2189
- this.exchangeConsent = { digest, resolution: resolution };
2190
- }
2191
- catch {
2192
- if (this.signal?.aborted)
2193
- throw aborted(this.signal);
2194
- throw this.credentialWriteError();
2195
- }
2196
- }.bind(this));
2197
- }, KvOAuthProvider.prototype.recordCodeExchangeTokens);
2198
- registerAuthMethod(KvOAuthProvider.prototype, "invalidateCredentials", function (scope) {
2199
- return authFlow(function* () {
2200
- if (this.signal?.aborted)
2201
- throw aborted(this.signal);
2202
- const fields = {
2203
- all: ["client", "tokens", "discovery"],
2204
- client: ["client"],
2205
- tokens: ["tokens"],
2206
- discovery: ["discovery"],
2207
- verifier: [],
2208
- }[scope];
2209
- if (fields.length > 0) {
2210
- (yield authInvoke(this, "updateGrant", [
2211
- (grant) => {
2212
- const body = { ...grant.body };
2213
- let changed = false;
2214
- for (const field of fields) {
2215
- const current = field === "client" ? body.client?.value : body[field];
2216
- if (current === undefined)
2217
- continue;
2218
- if (field !== "discovery" && fingerprint(current) !== this.seen[field])
2219
- continue;
2220
- delete body[field];
2221
- changed = true;
2222
- }
2223
- return changed ? { ...grant, body } : undefined;
2224
- },
2225
- { quiet: true },
2226
- ]));
2227
- }
2228
- // The SDK invalidates after a refused refresh and then starts over, which
2229
- // on `invalid_client` means registering a new client. A passive request
2230
- // must not begin authorization, so it ends here, as auth_required.
2231
- if (!this.allowAuthorization && this.refreshFailure?.kind === "dead") {
2232
- throw this.authorizationRefused();
2233
- }
2234
- }.bind(this));
2235
- }, KvOAuthProvider.prototype.invalidateCredentials);
2236
- registerAuthMethod(KvOAuthProvider.prototype, "claimRefresh", function (epoch, requested, signal, sharedWait) {
2237
- return authFlow(function* () {
2238
- const waiting = sharedWait ?? refreshWait(signal);
2239
- const wait = waiting.run;
2240
- const read = (key) => authCall(wait, [authMethod(() => authStorage(this.storage, "get", [key]))]);
2241
- const discard = () => authCall(wait, [
2242
- authMethod(() => authInvoke(this, "discardRefusedGrant", [requested, epoch, waiting.transfer, waiting.signal])),
2243
- ]);
2244
- const tokens = () => authCall(wait, [
2245
- authMethod(() => authInvoke(this, "storedTokens", [waiting.signal])),
2246
- ]);
2247
- let delay = 10;
2248
- try {
2249
- const digest = (yield authCall(wait, [
2250
- authMethod(() => {
2251
- return authCall(oauthStateDigest, [requested ?? ""]);
2252
- }),
2253
- ]));
2254
- const key = oauthRefreshKeys.lease(epoch, digest);
2255
- const spentKey = oauthRefreshSpentKeys.spent(digest);
2256
- while (!waiting.signal.aborted) {
2257
- const current = (yield tokens());
2258
- if (current?.refresh_token !== requested || (current && this.refreshedSinceRead(current, epoch))) {
2259
- if (!current)
2260
- this.recordRefreshFailure({ kind: "dead" });
2261
- return current
2262
- ? this.adoptRefresh(current)
2263
- : Response.json({ error: "invalid_grant" }, { status: 400 });
2264
- }
2265
- const raw = (yield read(key));
2266
- const stored = raw === null ? undefined : parsed(raw);
2267
- const committed = plainObject(stored) &&
2268
- (stored.completed === true ||
2269
- (stored.state === "claimed" && typeof stored.predecessor === "string")) &&
2270
- current !== undefined &&
2271
- stored.tokenDigest ===
2272
- (yield authCall(wait, [
2273
- authMethod(() => {
2274
- return authCall(refreshTokensDigest, [current]);
2275
- }),
2276
- ]));
2277
- const completed = committed && stored.completed === true;
2278
- if (plainObject(stored) && (plainObject(stored.verdict) || (stored.completed === true && !committed))) {
2279
- this.recordRefreshFailure({ kind: "dead" });
2280
- (yield discard());
2281
- throw new UnauthorizedError("OAuth refresh grant is no longer active.");
2282
- }
2283
- if (plainObject(stored) && stored.state !== "claimed" && !completed) {
2284
- // Never compare another isolate's clock for a sent request. The
2285
- // durable record survives expiry of this storage-owned liveness key.
2286
- const active = typeof stored.activeKey === "string"
2287
- ? (yield read(stored.activeKey))
2288
- : null;
2289
- if (active === null) {
2290
- const refused = JSON.stringify({ ...stored, verdict: { kind: "dead" } });
2291
- if ((yield authCall(wait, [
2292
- authMethod(() => {
2293
- return authStorage(this.storage, "compareAndSet", [key, raw, refused], () => waiting.transfer());
2294
- }),
2295
- ]))) {
2296
- (yield authCall(wait, [
2297
- authMethod(() => {
2298
- return authInvoke(this, "resolveRefreshSpent", [
2299
- { key, spentKey, spentExpected: null, raw: raw },
2300
- "ambiguous",
2301
- ]);
2302
- }),
2303
- ]));
2304
- this.recordRefreshFailure({ kind: "dead" });
2305
- (yield discard());
2306
- throw new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
2307
- }
2308
- continue;
2309
- }
2310
- }
2311
- else {
2312
- const spentExpected = (yield read(spentKey));
2313
- if (spentExpected !== null) {
2314
- const spent = storedRefreshSpent(spentExpected);
2315
- const consent = (yield authCall(wait, [
2316
- authMethod(() => {
2317
- return authInvoke(this, "boundGrant", [waiting.signal]);
2318
- }),
2319
- ])).body.refreshConsent;
2320
- if (spent?.state !== "resolved" ||
2321
- !(
2322
- // A completed lease proves these exact tokens committed.
2323
- // Reusable refresh tokens may start another generation,
2324
- // but only after its own dispatch is durably resolved.
2325
- (committed &&
2326
- spent.epoch === epoch &&
2327
- spent.holder === (completed ? stored.holder : stored.predecessor)) ||
2328
- (spent.epoch !== epoch && consent?.digest === digest && consent.resolution === spentExpected))) {
2329
- // Lease and fingerprint are separate records. A competing
2330
- // generation can advance both between these reads; retry
2331
- // its new lease instead of discarding a live grant.
2332
- const latestLease = (yield read(key));
2333
- if (latestLease !== raw)
2334
- continue;
2335
- // Reset can sweep the old lease while its request remains live.
2336
- // Only storage-owned expiry can make an outstanding send ambiguous.
2337
- if (spent?.state === "outstanding" && (yield read(spent.activeKey)) === null) {
2338
- (yield authCall(wait, [
2339
- authMethod(() => {
2340
- return authStorage(this.storage, "compareAndSet", [spentKey, spentExpected, JSON.stringify({ ...spent, state: "ambiguous" })], () => waiting.transfer());
2341
- }),
2342
- ]));
2343
- }
2344
- this.recordRefreshFailure({ kind: "dead" });
2345
- (yield discard());
2346
- throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
2347
- }
2348
- }
2349
- if (raw === null ||
2350
- completed ||
2351
- (plainObject(stored) &&
2352
- typeof stored.expiresAt === "number" &&
2353
- stored.expiresAt <= this.refreshNow())) {
2354
- // Clock skew can replace an unsent claim; its dispatch CAS then loses.
2355
- const predecessor = completed
2356
- ? stored.holder
2357
- : plainObject(stored)
2358
- ? stored.predecessor
2359
- : undefined;
2360
- const claimed = JSON.stringify({
2361
- connectaOAuthRefresh: 1,
2362
- holder: crypto.randomUUID(),
2363
- state: "claimed",
2364
- expiresAt: this.refreshNow() + OAUTH_REFRESH_LEASE_SECONDS * 1000,
2365
- ...(committed && typeof predecessor === "string" && typeof stored.tokenDigest === "string"
2366
- ? {
2367
- predecessor,
2368
- tokenDigest: stored.tokenDigest,
2369
- }
2370
- : {}),
2371
- });
2372
- let won;
2373
- try {
2374
- won = (yield authCall(wait, [
2375
- authMethod(() => {
2376
- return authStorage(this.storage, "compareAndSet", [key, raw, claimed], () => waiting.transfer());
2377
- }),
2378
- ]));
2379
- }
2380
- catch {
2381
- throw this.credentialWriteError();
2382
- }
2383
- if (won) {
2384
- try {
2385
- const latest = (yield tokens());
2386
- if (latest?.refresh_token !== requested || (latest && this.refreshedSinceRead(latest, epoch))) {
2387
- (yield authCall(wait, [
2388
- authMethod(() => {
2389
- return authInvoke(this, "releaseRefresh", [
2390
- { key, spentKey, spentExpected, raw: claimed },
2391
- ]);
2392
- }),
2393
- ]));
2394
- return latest
2395
- ? this.adoptRefresh(latest)
2396
- : Response.json({ error: "invalid_grant" }, { status: 400 });
2397
- }
2398
- return { key, spentKey, spentExpected, raw: claimed };
2399
- }
2400
- catch (error) {
2401
- if (!waiting.signal.aborted) {
2402
- (yield authCall(wait, [
2403
- authMethod(() => {
2404
- return authInvoke(this, "releaseRefresh", [
2405
- { key, spentKey, spentExpected, raw: claimed },
2406
- ]);
2407
- }),
2408
- ]));
2409
- }
2410
- throw error;
2411
- }
2412
- }
2413
- }
2414
- }
2415
- (yield Effect.raceAllFirst([Effect.sleep(delay), fromSignal(waiting.signal)]));
2416
- delay = Math.min(250, delay * 2);
2417
- }
2418
- }
2419
- catch (error) {
2420
- if (!waiting.signal.aborted)
2421
- throw error;
2422
- }
2423
- finally {
2424
- if (!sharedWait)
2425
- waiting.close();
2426
- }
2427
- throw this.refreshContended();
2428
- }.bind(this));
2429
- }, KvOAuthProvider.prototype.claimRefresh);
2430
- registerAuthMethod(KvOAuthProvider.prototype, "dispatchRefresh", function (lease, send, waiting) {
2431
- return authFlow(function* () {
2432
- const stored = parsed(lease.raw);
2433
- const epoch = (yield authInvoke(waiting, "run", [
2434
- authMethod(() => {
2435
- return authInvoke(this, "flowEpoch", []);
2436
- }),
2437
- ]));
2438
- const activeKey = oauthRefreshActiveKeys.holder(epoch, stored.holder);
2439
- (yield authInvoke(waiting, "run", [
2440
- authMethod(() => {
2441
- return authStorage(this.storage, "set", [
2442
- activeKey,
2443
- JSON.stringify({ connectaOAuthRefresh: 1 }),
2444
- {
2445
- ttlSeconds: OAUTH_REFRESH_LEASE_SECONDS,
2446
- },
2447
- ]);
2448
- }),
2449
- ]));
2450
- const dispatched = JSON.stringify({
2451
- ...stored,
2452
- state: "dispatched",
2453
- activeKey,
2454
- });
2455
- const won = (yield authInvoke(waiting, "run", [
2456
- authMethod(() => authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, dispatched])),
2457
- ]));
2458
- if (!won)
2459
- throw this.refreshContended();
2460
- lease.raw = dispatched;
2461
- if (waiting.signal.aborted)
2462
- throw aborted(waiting.signal);
2463
- // One CAS owns the fingerprint across epochs and generations. Only a
2464
- // proven successful commit or later consent may replace a resolved send.
2465
- lease.spentRaw = JSON.stringify({
2466
- connectaOAuthRefreshSpent: 1,
2467
- epoch,
2468
- holder: stored.holder,
2469
- activeKey,
2470
- state: "outstanding",
2471
- });
2472
- let unspent;
2473
- try {
2474
- unspent = (yield authInvoke(waiting, "run", [
2475
- authMethod(() => authStorage(this.storage, "compareAndSet", [
2476
- lease.spentKey,
2477
- lease.spentExpected,
2478
- lease.spentRaw,
2479
- ])),
2480
- ]));
2481
- }
2482
- catch {
2483
- this.recordRefreshFailure({ kind: "dead" });
2484
- throw new UnauthorizedError("OAuth refresh dispatch could not be recorded; authorization required.");
2485
- }
2486
- if (!unspent) {
2487
- this.recordRefreshFailure({ kind: "dead" });
2488
- throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
2489
- }
2490
- // Final owner check and physical submission share one reaction.
2491
- // Submission already transferred ownership to the coordinator. Only
2492
- // its own deadline can stop this send; the departed caller cannot.
2493
- if (waiting.signal.aborted)
2494
- throw aborted(waiting.signal);
2495
- return (yield authCall(send, []));
2496
- }.bind(this));
2497
- }, KvOAuthProvider.prototype.dispatchRefresh);
2498
- registerAuthMethod(KvOAuthProvider.prototype, "releaseRefresh", function (lease, verdict, tokens, resolution = "ambiguous") {
2499
- return authFlow(function* () {
2500
- const stored = parsed(lease.raw);
2501
- // Unsent successors restore the prior completed proof; other unsent
2502
- // claims may be removed. Dispatched outcomes never reopen a send.
2503
- const next = stored.state === "claimed"
2504
- ? stored.predecessor && stored.tokenDigest
2505
- ? JSON.stringify({
2506
- ...stored,
2507
- holder: stored.predecessor,
2508
- state: "dispatched",
2509
- completed: true,
2510
- })
2511
- : null
2512
- : JSON.stringify({
2513
- ...stored,
2514
- ...(tokens
2515
- ? {
2516
- completed: true,
2517
- tokenDigest: (yield authCall(refreshTokensDigest, [tokens])),
2518
- }
2519
- : { verdict: { kind: "dead" } }),
2520
- });
2521
- // Verdicts contain fixed typed facts; no downstream causes or text.
2522
- if (verdict)
2523
- this.recordRefreshFailure(verdict);
2524
- (yield authInvoke(this, "resolveRefreshSpent", [lease, resolution]));
2525
- const released = (yield authCatch(authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, next]), () => false));
2526
- if (released && stored.activeKey)
2527
- (yield authCatch(authStorage(this.storage, "delete", [stored.activeKey]), () => { }));
2528
- }.bind(this));
2529
- }, KvOAuthProvider.prototype.releaseRefresh);
2530
- registerAuthMethod(KvOAuthProvider.prototype, "resolveRefreshSpent", function (lease, state) {
2531
- return authFlow(function* () {
2532
- try {
2533
- const raw = lease.spentRaw ?? (yield authStorage(this.storage, "get", [lease.spentKey]));
2534
- const spent = storedRefreshSpent(raw);
2535
- const holder = parsed(lease.raw).holder;
2536
- if (spent?.state !== "outstanding" || spent.holder !== holder)
2537
- return;
2538
- // The timestamp belongs to this resolution write, never to dispatch.
2539
- // Consent copies the exact stored receipt, so wall clocks grant no sends.
2540
- (yield authStorage(this.storage, "compareAndSet", [
2541
- lease.spentKey,
2542
- raw,
2543
- JSON.stringify({
2544
- ...spent,
2545
- state,
2546
- ...(state === "resolved" ? { resolvedAt: Date.now() } : {}),
2547
- }),
2548
- ]));
2549
- }
2550
- catch {
2551
- // A lost resolution answer leaves outstanding or the durable outcome;
2552
- // either blocks replay until a later consent observes a resolved write.
2553
- }
2554
- }.bind(this));
2555
- }, KvOAuthProvider.prototype.resolveRefreshSpent);
2556
- registerAuthMethod(KvOAuthProvider.prototype, "storedTokens", function (signal = null) {
2557
- return authFlow(function* () {
2558
- return (yield authInvoke(this, "boundGrant", [signal])).body.tokens;
2559
- }.bind(this));
2560
- }, KvOAuthProvider.prototype.storedTokens);
2561
- registerAuthMethod(KvOAuthProvider.prototype, "storeRefresh", function (epoch, requested, tokens, issuer) {
2562
- return authFlow(function* () {
2563
- for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
2564
- try {
2565
- const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
2566
- if (grant.epoch !== epoch || (issuer !== undefined && grant.body.issuer !== issuer))
2567
- return undefined;
2568
- if (grant.body.tokens?.refresh_token !== requested)
2569
- return grant.body.tokens;
2570
- const committed = issuer !== undefined ? { ...tokens, issuer } : tokens;
2571
- const encoded = (yield authInvoke(this, "encodeGrant", [
2572
- { ...grant, body: { ...grant.body, tokens: committed } },
2573
- null,
2574
- ]));
2575
- if ((yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded])))
2576
- return committed;
2577
- }
2578
- catch {
2579
- // A failed or lost CAS answer may still have committed. Re-read the
2580
- // epoch and retry these in-memory tokens without another HTTP request.
2581
- }
2582
- }
2583
- throw this.credentialWriteError();
2584
- }.bind(this));
2585
- }, KvOAuthProvider.prototype.storeRefresh);
2586
- registerAuthMethod(KvOAuthProvider.prototype, "discardRefusedGrant", function (refreshToken, epoch, beforeWrite, signal) {
2587
- return authFlow(function* () {
2588
- try {
2589
- (yield authInvoke(this, "updateGrant", [
2590
- (grant) => {
2591
- if (grant.body.tokens?.refresh_token !== refreshToken)
2592
- return undefined;
2593
- const { tokens: _refused, ...body } = grant.body;
2594
- return { ...grant, body };
2595
- },
2596
- {
2597
- epoch,
2598
- quiet: true,
2599
- commit: true,
2600
- ...(beforeWrite ? { beforeWrite } : {}),
2601
- ...(signal ? { signal } : {}),
2602
- },
2603
- ]));
2604
- }
2605
- catch {
2606
- // See above: a refusal that could not be recorded recurs, it does not hide.
2607
- }
2608
- }.bind(this));
2609
- }, KvOAuthProvider.prototype.discardRefusedGrant);
2610
- registerAuthMethod(KvOAuthProvider.prototype, "resetAuthorization", function (operatorDisconnected = false, preserveClient = false) {
2611
- return authFlow(function* () {
2612
- const operation = authInvoke(this, "performReset", [operatorDisconnected, preserveClient]);
2613
- if (!this.onReset)
2614
- return (yield operation);
2615
- const reset = runEdge(operation);
2616
- this.onReset?.(reset);
2617
- return (yield authValue(() => reset));
2618
- }.bind(this));
2619
- }, KvOAuthProvider.prototype.resetAuthorization);
2620
- registerAuthMethod(KvOAuthProvider.prototype, "disconnectAuthorization", function (send) {
2621
- return authFlow(function* () {
2622
- const operation = authInvoke(this, "performReset", [true, false, send]);
2623
- if (!this.onReset)
2624
- return (yield operation);
2625
- const reset = runEdge(operation);
2626
- this.onReset?.(reset);
2627
- return (yield authValue(() => reset));
2628
- }.bind(this));
2629
- }, KvOAuthProvider.prototype.disconnectAuthorization);
2630
- registerAuthMethod(KvOAuthProvider.prototype, "performReset", function (operatorDisconnected, preserveClient, revoke) {
2631
- return authFlow(function* () {
2632
- const epoch = `${operatorDisconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`;
2633
- for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
2634
- const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
2635
- const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
2636
- const encoded = (yield authInvoke(this, "encodeGrant", [{ epoch, body: carried ?? {} }, null]));
2637
- let written;
2638
- try {
2639
- written = (yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded]));
2640
- }
2641
- catch {
2642
- throw this.credentialWriteError();
2643
- }
2644
- if (written) {
2645
- this.refreshCoordinator?.retire(grant.epoch);
2646
- try {
2647
- (yield authInvoke(this, "sweepConsents", []));
2648
- (yield authInvoke(this, "sweepRefreshLeases", []));
2649
- }
2650
- finally {
2651
- if (revoke) {
2652
- const snapshot = this.revocationSnapshot(grant.body);
2653
- (yield authCall(revokeDownstreamGrant, [snapshot.body, revoke, snapshot.client]));
2654
- }
2655
- }
2656
- return;
2657
- }
2658
- }
2659
- throw this.credentialWriteError();
2660
- }.bind(this));
2661
- }, KvOAuthProvider.prototype.performReset);
2662
- registerAuthMethod(KvOAuthProvider.prototype, "sweepConsents", function () {
2663
- return authFlow(function* () {
2664
- try {
2665
- const consents = [];
2666
- for (const key of (yield authStorage(this.storage, "list", [oauthFlowKeys.prefix]))) {
2667
- const raw = (yield authStorage(this.storage, "get", [key]));
2668
- if (raw !== null)
2669
- consents.push([key, raw]);
2670
- }
2671
- const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
2672
- for (const [key, raw] of consents) {
2673
- const flow = parsed(raw);
2674
- if (plainObject(flow) && flow.epoch === live)
2675
- continue;
2676
- (yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
2677
- }
2678
- }
2679
- catch {
2680
- // Hygiene only: the epoch, not this, fences a stale consent.
2681
- }
2682
- }.bind(this));
2683
- }, KvOAuthProvider.prototype.sweepConsents);
2684
- registerAuthMethod(KvOAuthProvider.prototype, "sweepRefreshLeases", function () {
2685
- return authFlow(function* () {
2686
- try {
2687
- const records = [];
2688
- // A reset may sweep an old lease while its HTTP request is still live.
2689
- // Keep holder liveness until completion or its storage-owned TTL: spent
2690
- // records in later epochs still use it to distinguish live from crashed.
2691
- for (const key of (yield authStorage(this.storage, "list", [oauthRefreshKeys.prefix]))) {
2692
- const raw = (yield authStorage(this.storage, "get", [key]));
2693
- if (raw !== null)
2694
- records.push([key, raw]);
2695
- }
2696
- const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
2697
- for (const [key, raw] of records) {
2698
- if (!key.startsWith(`${oauthRefreshKeys.prefix}${live}:`)) {
2699
- (yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
2700
- }
2701
- }
2702
- }
2703
- catch {
2704
- // Stale dispatch records cannot grant credentials: epochs never recur.
2705
- }
2706
- }.bind(this));
2707
- }, KvOAuthProvider.prototype.sweepRefreshLeases);
386
+ authMethods(KvOAuthProvider.prototype, {
387
+ ...grantStoreMethods,
388
+ ...consentMethods,
389
+ ...sdkHookMethods,
390
+ ...refreshRecordMethods,
391
+ });
2708
392
  }
2709
393
  }