@zackbart/connecta 0.37.1 → 0.39.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 (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -0,0 +1,447 @@
1
+ import { UnauthorizedError } from "@modelcontextprotocol/client";
2
+ import { Effect } from "effect";
3
+ import { fromSignal } from "../../runtime/run.js";
4
+ import { OAUTH_REFRESH_LEASE_SECONDS, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../../storage/keys.js";
5
+ import { authCall, authCatch, authFlow, authInvoke, authMethod, authStorage, authStorageMany } from "../native.js";
6
+ import { tokenExpiry } from "../oauth-grant.js";
7
+ import { GRANT, MAX_GRANT_WRITES, epochOf, parsed, plainObject } from "./grant-store.js";
8
+ import { aborted, refreshTokensDigest, refreshWait } from "./refresh-coordinator.js";
9
+ import { oauthStateDigest } from "./consent.js";
10
+ export function storedRefreshSpent(raw) {
11
+ const value = raw === null ? undefined : parsed(raw);
12
+ return plainObject(value) &&
13
+ value.connectaOAuthRefreshSpent === 1 &&
14
+ typeof value.epoch === "string" &&
15
+ typeof value.holder === "string" &&
16
+ typeof value.activeKey === "string" &&
17
+ (value.state === "outstanding" ||
18
+ value.state === "ambiguous" ||
19
+ (value.state === "resolved" && typeof value.resolvedAt === "number" && Number.isFinite(value.resolvedAt)))
20
+ ? value
21
+ : undefined;
22
+ }
23
+ /**
24
+ * Polling for another isolate's refresh. Each poll is one batched storage
25
+ * read of its lease, the grant, the spent fingerprint and the owner's
26
+ * liveness key. Doubling from 50 ms to 2 s, a wait that runs to the 35 s
27
+ * deadline costs at most 22 polls; a typical sub-second refresh, four or five.
28
+ */
29
+ const REFRESH_POLL_INITIAL_MS = 50;
30
+ const REFRESH_POLL_MAX_MS = 2_000;
31
+ /** KvOAuthProvider's refresh lease and spent record methods; `authMethods` installs them. */
32
+ export const refreshRecordMethods = {
33
+ claimRefresh: function (epoch, requested, signal, sharedWait) {
34
+ return authFlow(function* () {
35
+ const waiting = sharedWait ?? refreshWait(signal);
36
+ const wait = waiting.run;
37
+ const read = (key) => authCall(wait, [authMethod(() => authStorage(this.storage, "get", [key]))]);
38
+ const discard = () => authCall(wait, [
39
+ authMethod(() => authInvoke(this, "discardRefusedGrant", [requested, epoch, waiting.transfer, waiting.signal])),
40
+ ]);
41
+ const tokens = () => authCall(wait, [
42
+ authMethod(() => authInvoke(this, "storedTokens", [waiting.signal])),
43
+ ]);
44
+ let delay = REFRESH_POLL_INITIAL_MS;
45
+ try {
46
+ const digest = (yield authCall(wait, [
47
+ authMethod(() => {
48
+ return authCall(oauthStateDigest, [requested ?? ""]);
49
+ }),
50
+ ]));
51
+ const key = oauthRefreshKeys.lease(epoch, digest);
52
+ const spentKey = oauthRefreshSpentKeys.spent(digest);
53
+ // One batched read per poll. The lease comes first, so a store that
54
+ // reads the keys one at a time never pairs a completed lease with the
55
+ // grant as it was before that lease's commit.
56
+ const poll = (activeKey) => authCall(wait, [
57
+ authMethod(() => authFlow(function* () {
58
+ const [lease, raw, spent, active] = (yield authStorageMany(this.storage, [
59
+ key,
60
+ GRANT,
61
+ spentKey,
62
+ ...(activeKey !== undefined ? [activeKey] : []),
63
+ ]));
64
+ const grant = (yield authInvoke(this, "openGrant", [raw ?? null, waiting.signal]));
65
+ if (this.bound && grant.epoch !== this.epoch)
66
+ throw this.flowSuperseded();
67
+ return { lease: lease ?? null, grant, spent: spent ?? null, active: active ?? null };
68
+ }.bind(this))),
69
+ ]);
70
+ /** The liveness key the previous poll's lease named, read with the next poll. */
71
+ let activeKey;
72
+ while (!waiting.signal.aborted) {
73
+ const polled = (yield poll(activeKey));
74
+ const current = polled.grant.body.tokens;
75
+ if (current?.refresh_token !== requested || (current && this.refreshedSinceRead(current, epoch))) {
76
+ if (!current)
77
+ this.recordRefreshFailure({ kind: "dead" });
78
+ return current ? this.adoptRefresh(current) : Response.json({ error: "invalid_grant" }, { status: 400 });
79
+ }
80
+ const raw = polled.lease;
81
+ const stored = raw === null ? undefined : parsed(raw);
82
+ const committed = plainObject(stored) &&
83
+ (stored.completed === true || (stored.state === "claimed" && typeof stored.predecessor === "string")) &&
84
+ current !== undefined &&
85
+ stored.tokenDigest ===
86
+ (yield authCall(wait, [
87
+ authMethod(() => {
88
+ return authCall(refreshTokensDigest, [current]);
89
+ }),
90
+ ]));
91
+ const completed = committed && stored.completed === true;
92
+ if (plainObject(stored) && (plainObject(stored.verdict) || (stored.completed === true && !committed))) {
93
+ this.recordRefreshFailure({ kind: "dead" });
94
+ (yield discard());
95
+ throw new UnauthorizedError("OAuth refresh grant is no longer active.");
96
+ }
97
+ if (plainObject(stored) && stored.state !== "claimed" && !completed) {
98
+ // Never compare another isolate's clock for a sent request. The
99
+ // durable record survives expiry of this storage-owned liveness key.
100
+ const named = typeof stored.activeKey === "string" ? stored.activeKey : undefined;
101
+ const active = named === undefined
102
+ ? null
103
+ : named === activeKey
104
+ ? polled.active
105
+ : (yield read(named));
106
+ activeKey = named;
107
+ if (active === null) {
108
+ const refused = JSON.stringify({ ...stored, verdict: { kind: "dead" } });
109
+ if ((yield authCall(wait, [
110
+ authMethod(() => {
111
+ return authStorage(this.storage, "compareAndSet", [key, raw, refused], () => waiting.transfer());
112
+ }),
113
+ ]))) {
114
+ (yield authCall(wait, [
115
+ authMethod(() => {
116
+ return authInvoke(this, "resolveRefreshSpent", [
117
+ { key, spentKey, spentExpected: null, raw: raw },
118
+ "ambiguous",
119
+ ]);
120
+ }),
121
+ ]));
122
+ this.recordRefreshFailure({ kind: "dead" });
123
+ (yield discard());
124
+ throw new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
125
+ }
126
+ continue;
127
+ }
128
+ }
129
+ else {
130
+ const spentExpected = polled.spent;
131
+ if (spentExpected !== null) {
132
+ const spent = storedRefreshSpent(spentExpected);
133
+ const consent = polled.grant.body.refreshConsent;
134
+ if (spent?.state !== "resolved" ||
135
+ !(
136
+ // A completed lease proves these exact tokens committed.
137
+ // Reusable refresh tokens may start another generation,
138
+ // but only after its own dispatch is durably resolved.
139
+ (committed &&
140
+ spent.epoch === epoch &&
141
+ spent.holder === (completed ? stored.holder : stored.predecessor)) ||
142
+ (spent.epoch !== epoch && consent?.digest === digest && consent.resolution === spentExpected))) {
143
+ // Lease and fingerprint are separate records. A competing
144
+ // generation can advance both between these reads; retry
145
+ // its new lease instead of discarding a live grant.
146
+ const latestLease = (yield read(key));
147
+ if (latestLease !== raw)
148
+ continue;
149
+ // Reset can sweep the old lease while its request remains live.
150
+ // Only storage-owned expiry can make an outstanding send ambiguous.
151
+ if (spent?.state === "outstanding" && (yield read(spent.activeKey)) === null) {
152
+ (yield authCall(wait, [
153
+ authMethod(() => {
154
+ return authStorage(this.storage, "compareAndSet", [spentKey, spentExpected, JSON.stringify({ ...spent, state: "ambiguous" })], () => waiting.transfer());
155
+ }),
156
+ ]));
157
+ }
158
+ this.recordRefreshFailure({ kind: "dead" });
159
+ (yield discard());
160
+ throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
161
+ }
162
+ }
163
+ if (raw === null ||
164
+ completed ||
165
+ (plainObject(stored) && typeof stored.expiresAt === "number" && stored.expiresAt <= this.refreshNow())) {
166
+ // Clock skew can replace an unsent claim; its dispatch CAS then loses.
167
+ const predecessor = completed ? stored.holder : plainObject(stored) ? stored.predecessor : undefined;
168
+ const claimed = JSON.stringify({
169
+ connectaOAuthRefresh: 1,
170
+ holder: crypto.randomUUID(),
171
+ state: "claimed",
172
+ expiresAt: this.refreshNow() + OAUTH_REFRESH_LEASE_SECONDS * 1000,
173
+ ...(committed && typeof predecessor === "string" && typeof stored.tokenDigest === "string"
174
+ ? {
175
+ predecessor,
176
+ tokenDigest: stored.tokenDigest,
177
+ }
178
+ : {}),
179
+ });
180
+ let won;
181
+ try {
182
+ won = (yield authCall(wait, [
183
+ authMethod(() => {
184
+ return authStorage(this.storage, "compareAndSet", [key, raw, claimed], () => waiting.transfer());
185
+ }),
186
+ ]));
187
+ }
188
+ catch {
189
+ throw this.credentialWriteError();
190
+ }
191
+ if (won) {
192
+ try {
193
+ const latest = (yield tokens());
194
+ if (latest?.refresh_token !== requested || (latest && this.refreshedSinceRead(latest, epoch))) {
195
+ (yield authCall(wait, [
196
+ authMethod(() => {
197
+ return authInvoke(this, "releaseRefresh", [
198
+ { key, spentKey, spentExpected, raw: claimed },
199
+ ]);
200
+ }),
201
+ ]));
202
+ return latest
203
+ ? this.adoptRefresh(latest)
204
+ : Response.json({ error: "invalid_grant" }, { status: 400 });
205
+ }
206
+ return { key, spentKey, spentExpected, raw: claimed };
207
+ }
208
+ catch (error) {
209
+ if (!waiting.signal.aborted) {
210
+ (yield authCall(wait, [
211
+ authMethod(() => {
212
+ return authInvoke(this, "releaseRefresh", [
213
+ { key, spentKey, spentExpected, raw: claimed },
214
+ ]);
215
+ }),
216
+ ]));
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+ }
222
+ }
223
+ (yield Effect.raceAllFirst([Effect.sleep(delay), fromSignal(waiting.signal)]));
224
+ delay = Math.min(REFRESH_POLL_MAX_MS, delay * 2);
225
+ }
226
+ }
227
+ catch (error) {
228
+ if (!waiting.signal.aborted)
229
+ throw error;
230
+ }
231
+ finally {
232
+ if (!sharedWait)
233
+ waiting.close();
234
+ }
235
+ throw this.refreshContended();
236
+ }.bind(this));
237
+ },
238
+ dispatchRefresh: function (lease, send, waiting) {
239
+ return authFlow(function* () {
240
+ const stored = parsed(lease.raw);
241
+ const epoch = (yield authInvoke(waiting, "run", [
242
+ authMethod(() => {
243
+ return authInvoke(this, "flowEpoch", []);
244
+ }),
245
+ ]));
246
+ const activeKey = oauthRefreshActiveKeys.holder(epoch, stored.holder);
247
+ (yield authInvoke(waiting, "run", [
248
+ authMethod(() => {
249
+ return authStorage(this.storage, "set", [
250
+ activeKey,
251
+ JSON.stringify({ connectaOAuthRefresh: 1 }),
252
+ {
253
+ ttlSeconds: OAUTH_REFRESH_LEASE_SECONDS,
254
+ },
255
+ ]);
256
+ }),
257
+ ]));
258
+ const dispatched = JSON.stringify({
259
+ ...stored,
260
+ state: "dispatched",
261
+ activeKey,
262
+ });
263
+ const won = (yield authInvoke(waiting, "run", [
264
+ authMethod(() => authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, dispatched])),
265
+ ]));
266
+ if (!won)
267
+ throw this.refreshContended();
268
+ lease.raw = dispatched;
269
+ if (waiting.signal.aborted)
270
+ throw aborted(waiting.signal);
271
+ // One CAS owns the fingerprint across epochs and generations. Only a
272
+ // proven successful commit or later consent may replace a resolved send.
273
+ lease.spentRaw = JSON.stringify({
274
+ connectaOAuthRefreshSpent: 1,
275
+ epoch,
276
+ holder: stored.holder,
277
+ activeKey,
278
+ state: "outstanding",
279
+ });
280
+ let unspent;
281
+ try {
282
+ unspent = (yield authInvoke(waiting, "run", [
283
+ authMethod(() => authStorage(this.storage, "compareAndSet", [
284
+ lease.spentKey,
285
+ lease.spentExpected,
286
+ lease.spentRaw,
287
+ ])),
288
+ ]));
289
+ }
290
+ catch {
291
+ this.recordRefreshFailure({ kind: "dead" });
292
+ throw new UnauthorizedError("OAuth refresh dispatch could not be recorded; authorization required.");
293
+ }
294
+ if (!unspent) {
295
+ this.recordRefreshFailure({ kind: "dead" });
296
+ throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
297
+ }
298
+ // Final owner check and physical submission share one reaction.
299
+ // Submission already transferred ownership to the coordinator. Only
300
+ // its own deadline can stop this send; the departed caller cannot.
301
+ if (waiting.signal.aborted)
302
+ throw aborted(waiting.signal);
303
+ return (yield authCall(send, []));
304
+ }.bind(this));
305
+ },
306
+ releaseRefresh: function (lease, verdict, tokens, resolution = "ambiguous") {
307
+ return authFlow(function* () {
308
+ const stored = parsed(lease.raw);
309
+ // Unsent successors restore the prior completed proof; other unsent
310
+ // claims may be removed. Dispatched outcomes never reopen a send.
311
+ const next = stored.state === "claimed"
312
+ ? stored.predecessor && stored.tokenDigest
313
+ ? JSON.stringify({
314
+ ...stored,
315
+ holder: stored.predecessor,
316
+ state: "dispatched",
317
+ completed: true,
318
+ })
319
+ : null
320
+ : JSON.stringify({
321
+ ...stored,
322
+ ...(tokens
323
+ ? {
324
+ completed: true,
325
+ tokenDigest: (yield authCall(refreshTokensDigest, [tokens])),
326
+ }
327
+ : { verdict: { kind: "dead" } }),
328
+ });
329
+ // Verdicts contain fixed typed facts; no downstream causes or text.
330
+ if (verdict)
331
+ this.recordRefreshFailure(verdict);
332
+ (yield authInvoke(this, "resolveRefreshSpent", [lease, resolution]));
333
+ const released = (yield authCatch(authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, next]), () => false));
334
+ if (released && stored.activeKey)
335
+ (yield authCatch(authStorage(this.storage, "delete", [stored.activeKey]), () => { }));
336
+ }.bind(this));
337
+ },
338
+ resolveRefreshSpent: function (lease, state) {
339
+ return authFlow(function* () {
340
+ try {
341
+ const raw = lease.spentRaw ?? (yield authStorage(this.storage, "get", [lease.spentKey]));
342
+ const spent = storedRefreshSpent(raw);
343
+ const holder = parsed(lease.raw).holder;
344
+ if (spent?.state !== "outstanding" || spent.holder !== holder)
345
+ return;
346
+ // The timestamp belongs to this resolution write, never to dispatch.
347
+ // Consent copies the exact stored receipt, so wall clocks grant no sends.
348
+ (yield authStorage(this.storage, "compareAndSet", [
349
+ lease.spentKey,
350
+ raw,
351
+ JSON.stringify({
352
+ ...spent,
353
+ state,
354
+ ...(state === "resolved" ? { resolvedAt: Date.now() } : {}),
355
+ }),
356
+ ]));
357
+ }
358
+ catch {
359
+ // A lost resolution answer leaves outstanding or the durable outcome;
360
+ // either blocks replay until a later consent observes a resolved write.
361
+ }
362
+ }.bind(this));
363
+ },
364
+ storedTokens: function (signal = null) {
365
+ return authFlow(function* () {
366
+ return (yield authInvoke(this, "boundGrant", [signal])).body.tokens;
367
+ }.bind(this));
368
+ },
369
+ storeRefresh: function (epoch, requested, tokens, issuer, issuedAt = Date.now()) {
370
+ return authFlow(function* () {
371
+ for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
372
+ try {
373
+ const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
374
+ if (grant.epoch !== epoch || (issuer !== undefined && grant.body.issuer !== issuer))
375
+ return undefined;
376
+ if (grant.body.tokens?.refresh_token !== requested)
377
+ return grant.body.tokens;
378
+ const committed = issuer !== undefined ? { ...tokens, issuer } : tokens;
379
+ const { expiresAt: _previous, ...body } = grant.body;
380
+ const expiresAt = tokenExpiry(committed, issuedAt);
381
+ const next = {
382
+ ...grant,
383
+ body: { ...body, tokens: committed, ...(expiresAt !== undefined ? { expiresAt } : {}) },
384
+ };
385
+ const encoded = (yield authInvoke(this, "encodeGrant", [next, null]));
386
+ if ((yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded]))) {
387
+ this.observe(next);
388
+ return committed;
389
+ }
390
+ }
391
+ catch {
392
+ // A failed or lost CAS answer may still have committed. Re-read the
393
+ // epoch and retry these in-memory tokens without another HTTP request.
394
+ }
395
+ }
396
+ throw this.credentialWriteError();
397
+ }.bind(this));
398
+ },
399
+ discardRefusedGrant: function (refreshToken, epoch, beforeWrite, signal) {
400
+ return authFlow(function* () {
401
+ try {
402
+ (yield authInvoke(this, "updateGrant", [
403
+ (grant) => {
404
+ if (grant.body.tokens?.refresh_token !== refreshToken)
405
+ return undefined;
406
+ const { tokens: _refused, expiresAt: _expiry, ...body } = grant.body;
407
+ return { ...grant, body };
408
+ },
409
+ {
410
+ epoch,
411
+ quiet: true,
412
+ commit: true,
413
+ ...(beforeWrite ? { beforeWrite } : {}),
414
+ ...(signal ? { signal } : {}),
415
+ },
416
+ ]));
417
+ }
418
+ catch {
419
+ // See above: a refusal that could not be recorded recurs, it does not hide.
420
+ }
421
+ }.bind(this));
422
+ },
423
+ sweepRefreshLeases: function () {
424
+ return authFlow(function* () {
425
+ try {
426
+ const records = [];
427
+ // A reset may sweep an old lease while its HTTP request is still live.
428
+ // Keep holder liveness until completion or its storage-owned TTL: spent
429
+ // records in later epochs still use it to distinguish live from crashed.
430
+ for (const key of (yield authStorage(this.storage, "list", [oauthRefreshKeys.prefix]))) {
431
+ const raw = (yield authStorage(this.storage, "get", [key]));
432
+ if (raw !== null)
433
+ records.push([key, raw]);
434
+ }
435
+ const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
436
+ for (const [key, raw] of records) {
437
+ if (!key.startsWith(`${oauthRefreshKeys.prefix}${live}:`)) {
438
+ (yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
439
+ }
440
+ }
441
+ }
442
+ catch {
443
+ // Stale dispatch records cannot grant credentials: epochs never recur.
444
+ }
445
+ }.bind(this));
446
+ },
447
+ };