@zackbart/connecta 0.7.5 → 0.7.7

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 (72) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +1 -0
  3. package/dist/activity.d.ts +17 -0
  4. package/dist/activity.d.ts.map +1 -1
  5. package/dist/activity.js.map +1 -1
  6. package/dist/auth/clerk.d.ts.map +1 -1
  7. package/dist/auth/clerk.js +101 -0
  8. package/dist/auth/clerk.js.map +1 -1
  9. package/dist/auth/downstream-oauth.d.ts +57 -20
  10. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  11. package/dist/auth/downstream-oauth.js +275 -67
  12. package/dist/auth/downstream-oauth.js.map +1 -1
  13. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  14. package/dist/connectors/remote-mcp.js +165 -103
  15. package/dist/connectors/remote-mcp.js.map +1 -1
  16. package/dist/execute.d.ts.map +1 -1
  17. package/dist/execute.js +11 -0
  18. package/dist/execute.js.map +1 -1
  19. package/dist/executor-admission.d.ts +18 -1
  20. package/dist/executor-admission.d.ts.map +1 -1
  21. package/dist/executor-admission.js +82 -3
  22. package/dist/executor-admission.js.map +1 -1
  23. package/dist/executors/quickjs-protocol.d.ts +1 -0
  24. package/dist/executors/quickjs-protocol.d.ts.map +1 -1
  25. package/dist/executors/quickjs-protocol.js +7 -4
  26. package/dist/executors/quickjs-protocol.js.map +1 -1
  27. package/dist/executors/quickjs-runtime.d.ts.map +1 -1
  28. package/dist/executors/quickjs-runtime.js +22 -9
  29. package/dist/executors/quickjs-runtime.js.map +1 -1
  30. package/dist/executors/quickjs.d.ts.map +1 -1
  31. package/dist/executors/quickjs.js +44 -10
  32. package/dist/executors/quickjs.js.map +1 -1
  33. package/dist/index.d.ts +31 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +38 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/registry.d.ts +12 -0
  38. package/dist/registry.d.ts.map +1 -1
  39. package/dist/registry.js +70 -15
  40. package/dist/registry.js.map +1 -1
  41. package/dist/server.d.ts +3 -0
  42. package/dist/server.d.ts.map +1 -1
  43. package/dist/server.js +413 -35
  44. package/dist/server.js.map +1 -1
  45. package/dist/storage/file.d.ts.map +1 -1
  46. package/dist/storage/file.js +8 -0
  47. package/dist/storage/file.js.map +1 -1
  48. package/dist/types.d.ts +47 -0
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/ui.d.ts +7 -1
  51. package/dist/ui.d.ts.map +1 -1
  52. package/dist/ui.js +118 -4
  53. package/dist/ui.js.map +1 -1
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +2 -1
  57. package/src/activity.ts +20 -0
  58. package/src/auth/clerk.ts +124 -0
  59. package/src/auth/downstream-oauth.ts +359 -68
  60. package/src/connectors/remote-mcp.ts +172 -104
  61. package/src/execute.ts +11 -0
  62. package/src/executor-admission.ts +90 -3
  63. package/src/executors/quickjs-protocol.ts +7 -4
  64. package/src/executors/quickjs-runtime.ts +31 -9
  65. package/src/executors/quickjs.ts +61 -12
  66. package/src/index.ts +90 -1
  67. package/src/registry.ts +79 -19
  68. package/src/server.ts +523 -45
  69. package/src/storage/file.ts +7 -0
  70. package/src/types.ts +50 -0
  71. package/src/ui.ts +124 -3
  72. package/src/version.ts +1 -1
@@ -21,6 +21,65 @@ function timingSafeEqual(a: string, b: string): boolean {
21
21
  return diff === 0;
22
22
  }
23
23
 
24
+ const LEGACY_GENERATION = "legacy";
25
+ const ACTIVE_GENERATION_PREFIX = "v2:";
26
+ const RESETTING_GENERATION_PREFIX = "reset:";
27
+ const DISCONNECTED_GENERATION_PREFIX = "disconnected:";
28
+ const STORED_VALUE_VERSION = 1;
29
+ const OAUTH_VALUE_KEYS = [
30
+ "oauth:client",
31
+ "oauth:tokens",
32
+ "oauth:pending",
33
+ "oauth:verifier",
34
+ "oauth:state",
35
+ ] as const;
36
+ const MAX_CLEANUP_BACKLOG = 1_000;
37
+
38
+ interface StoredOAuthValue<T> {
39
+ connectaOAuthVersion: typeof STORED_VALUE_VERSION;
40
+ generation: string;
41
+ value: T;
42
+ }
43
+
44
+ function storedOAuthValue<T>(
45
+ value: unknown,
46
+ ): value is StoredOAuthValue<T> {
47
+ if (!value || typeof value !== "object") return false;
48
+ const candidate = value as Partial<StoredOAuthValue<T>>;
49
+ return (
50
+ candidate.connectaOAuthVersion === STORED_VALUE_VERSION &&
51
+ typeof candidate.generation === "string" &&
52
+ "value" in candidate
53
+ );
54
+ }
55
+
56
+ function isModernGeneration(generation: string): boolean {
57
+ return (
58
+ generation.startsWith(ACTIVE_GENERATION_PREFIX) ||
59
+ generation.startsWith(RESETTING_GENERATION_PREFIX) ||
60
+ generation.startsWith(DISCONNECTED_GENERATION_PREFIX)
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Physical key for an OAuth value in one authorization epoch. Legacy values
66
+ * keep their historical names so upgrades can read an existing grant. Modern
67
+ * values get an epoch-specific namespace: a stale write or delete can then
68
+ * affect only its own flow, even if it lands after a replacement flow.
69
+ */
70
+ export function oauthValueStorageKey(
71
+ key: string,
72
+ generation: string | null,
73
+ ): string {
74
+ return generation !== null && isModernGeneration(generation)
75
+ ? `${key}:epoch:${generation}`
76
+ : key;
77
+ }
78
+
79
+ function cleanupBacklogKey(generation: string): string {
80
+ return `oauth:cleanup:${encodeURIComponent(generation)}`;
81
+ }
82
+
24
83
  /**
25
84
  * OAuthClientProvider implemented over KVStorage for a single downstream
26
85
  * connector. Keys live in the connector's namespace as `oauth:<field>`.
@@ -32,11 +91,11 @@ function timingSafeEqual(a: string, b: string): boolean {
32
91
  */
33
92
  export class KvOAuthProvider implements OAuthClientProvider {
34
93
  /**
35
- * The force-reauth generation this provider's flow started under, captured by
36
- * the connector before it drives a connect. `null` until stamped a provider
37
- * used outside a connect flow (or in isolation) writes unconditionally.
94
+ * The reset generation this provider's flow started under. Every OAuth value
95
+ * it writes carries this epoch, so a late write can land after a reset without
96
+ * becoming readable under the new generation.
38
97
  */
39
- private capturedGeneration: number | null = null;
98
+ private capturedGeneration: string | null = null;
40
99
 
41
100
  constructor(
42
101
  private readonly connectorId: string,
@@ -46,24 +105,161 @@ export class KvOAuthProvider implements OAuthClientProvider {
46
105
 
47
106
  /**
48
107
  * Stamp the force-reauth generation the current connect flow started under.
49
- * Subsequent saveTokens/saveClientInformation writes are dropped if KV's
50
- * generation has since advanced (a concurrent force wiped credentials) — see
51
- * isStale(). Called by the connector once per connect, before c.connect().
108
+ * Called by the connector once per connect, before c.connect(). The callback
109
+ * path captures the generation stored beside its verified state instead.
52
110
  */
53
- captureGeneration(gen: number): void {
111
+ captureGeneration(gen: string): void {
54
112
  this.capturedGeneration = gen;
55
113
  }
56
114
 
57
115
  /**
58
- * Whether a concurrent force re-auth advanced the KV generation past the one
59
- * this flow captured. When true, this isolate's SDK is mid-flight against
60
- * credentials that were just wiped — persisting them would resurrect the
61
- * wiped-and-reauthorized connector for later isolates. Fails open (writes)
62
- * when no generation was captured.
116
+ * The epoch this provider writes under. Direct unit/custom use lazily captures
117
+ * the current generation; connector-driven connect and callback paths stamp it
118
+ * explicitly before the SDK can write.
119
+ */
120
+ private async writeGeneration(): Promise<string> {
121
+ this.capturedGeneration ??= await this.generation();
122
+ return this.capturedGeneration;
123
+ }
124
+
125
+ /**
126
+ * Store a value in the flow's physical epoch namespace. The pre-write check
127
+ * avoids needless stale residue. The namespaced key closes the remaining
128
+ * check-then-write race: a late old write cannot overwrite a replacement
129
+ * flow's value because the two writes have different physical keys.
63
130
  */
64
- private async isStale(): Promise<boolean> {
65
- if (this.capturedGeneration === null) return false;
66
- return (await this.generation()) > this.capturedGeneration;
131
+ private async writeValue<T>(
132
+ key: string,
133
+ value: T,
134
+ serializeLegacy: (value: T) => string,
135
+ ): Promise<void> {
136
+ const generation = await this.writeGeneration();
137
+ if (
138
+ generation.startsWith(RESETTING_GENERATION_PREFIX) ||
139
+ generation.startsWith(DISCONNECTED_GENERATION_PREFIX) ||
140
+ (await this.generation()) !== generation
141
+ ) {
142
+ return;
143
+ }
144
+ const stored: StoredOAuthValue<T> = {
145
+ connectaOAuthVersion: STORED_VALUE_VERSION,
146
+ generation,
147
+ value,
148
+ };
149
+ const physicalKey = oauthValueStorageKey(key, generation);
150
+ await this.storage.set(
151
+ physicalKey,
152
+ isModernGeneration(generation)
153
+ ? JSON.stringify(stored)
154
+ : serializeLegacy(value),
155
+ );
156
+ // If reset landed after the pre-write check and completed its cleanup
157
+ // before this set, remove the now-unreachable residue ourselves. The epoch
158
+ // key already provides correctness; this second check is physical hygiene.
159
+ const current = await this.generation();
160
+ if (current !== generation) {
161
+ try {
162
+ await this.storage.delete(physicalKey);
163
+ } catch {
164
+ // Make a transient cleanup failure retryable by the next force reset.
165
+ // This is still best-effort if storage cannot accept the backlog write.
166
+ try {
167
+ await this.rememberRetiredGeneration(current, generation);
168
+ } catch {
169
+ // The old namespace is already unreadable; storage availability is
170
+ // the remaining physical-hygiene boundary.
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Read a value only when it belongs to the active generation. Plain legacy
178
+ * values remain readable until the first v2 reset, so upgrades do not discard
179
+ * an existing grant; once a modern epoch exists, untagged residue fails
180
+ * closed.
181
+ */
182
+ private async readValue<T>(
183
+ key: string,
184
+ parseLegacy: (raw: string) => T,
185
+ ): Promise<{ value: T; generation: string } | undefined> {
186
+ const generation = await this.generation();
187
+ const raw = await this.storage.get(
188
+ oauthValueStorageKey(key, generation),
189
+ );
190
+ if (raw === null) return undefined;
191
+ if (
192
+ generation.startsWith(RESETTING_GENERATION_PREFIX) ||
193
+ generation.startsWith(DISCONNECTED_GENERATION_PREFIX)
194
+ ) {
195
+ return undefined;
196
+ }
197
+
198
+ let parsed: unknown;
199
+ try {
200
+ parsed = JSON.parse(raw);
201
+ } catch {
202
+ // Raw string state from a pre-envelope deployment is handled below.
203
+ }
204
+ if (storedOAuthValue<T>(parsed)) {
205
+ return parsed.generation === generation
206
+ ? { value: parsed.value, generation }
207
+ : undefined;
208
+ }
209
+ if (isModernGeneration(generation)) return undefined;
210
+ return { value: parseLegacy(raw), generation };
211
+ }
212
+
213
+ /** Attempt every key deletion, then report the first backend failure. */
214
+ private async deleteAll(keys: readonly string[]): Promise<void> {
215
+ let firstError: unknown;
216
+ for (const key of keys) {
217
+ try {
218
+ await this.storage.delete(key);
219
+ } catch (error) {
220
+ firstError ??= error;
221
+ }
222
+ }
223
+ if (firstError) throw firstError;
224
+ }
225
+
226
+ private async cleanupBacklog(generation: string): Promise<string[]> {
227
+ const raw = await this.storage.get(cleanupBacklogKey(generation));
228
+ if (raw === null) return [];
229
+ const parsed: unknown = JSON.parse(raw);
230
+ if (
231
+ !Array.isArray(parsed) ||
232
+ parsed.length > MAX_CLEANUP_BACKLOG ||
233
+ !parsed.every((value) => typeof value === "string")
234
+ ) {
235
+ throw new Error(
236
+ `Invalid OAuth cleanup backlog for "${this.connectorId}"`,
237
+ );
238
+ }
239
+ return [...new Set(parsed)];
240
+ }
241
+
242
+ private async rememberRetiredGeneration(
243
+ active: string,
244
+ retired: string,
245
+ ): Promise<void> {
246
+ const backlog = await this.cleanupBacklog(active);
247
+ if (backlog.includes(retired)) return;
248
+ if (backlog.length >= MAX_CLEANUP_BACKLOG) {
249
+ throw new Error(
250
+ `OAuth cleanup backlog for "${this.connectorId}" is full`,
251
+ );
252
+ }
253
+ await this.storage.set(
254
+ cleanupBacklogKey(active),
255
+ JSON.stringify([...backlog, retired]),
256
+ );
257
+ }
258
+
259
+ private valueKeysForGeneration(generation: string): string[] {
260
+ return OAUTH_VALUE_KEYS.map((key) =>
261
+ oauthValueStorageKey(key, generation),
262
+ );
67
263
  }
68
264
 
69
265
  get redirectUrl(): string {
@@ -81,39 +277,35 @@ export class KvOAuthProvider implements OAuthClientProvider {
81
277
  }
82
278
 
83
279
  async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
84
- const raw = await this.storage.get("oauth:client");
85
- return raw ? (JSON.parse(raw) as OAuthClientInformationMixed) : undefined;
280
+ return (
281
+ await this.readValue(
282
+ "oauth:client",
283
+ (raw) => JSON.parse(raw) as OAuthClientInformationMixed,
284
+ )
285
+ )?.value;
86
286
  }
87
287
 
88
288
  async saveClientInformation(
89
289
  info: OAuthClientInformationMixed,
90
290
  ): Promise<void> {
91
- // A concurrent force wiped credentials and bumped the generation while this
92
- // flow ran — skip rather than re-register a client under revoked state. The
93
- // in-memory SDK keeps this info for the current (doomed) connect, which the
94
- // connector's post-connect fence then discards.
95
- if (await this.isStale()) return;
96
- await this.storage.set("oauth:client", JSON.stringify(info));
291
+ await this.writeValue("oauth:client", info, (value) =>
292
+ JSON.stringify(value),
293
+ );
97
294
  }
98
295
 
99
296
  async tokens(): Promise<OAuthTokens | undefined> {
100
- const raw = await this.storage.get("oauth:tokens");
101
- return raw ? (JSON.parse(raw) as OAuthTokens) : undefined;
297
+ return (
298
+ await this.readValue(
299
+ "oauth:tokens",
300
+ (raw) => JSON.parse(raw) as OAuthTokens,
301
+ )
302
+ )?.value;
102
303
  }
103
304
 
104
305
  async saveTokens(tokens: OAuthTokens): Promise<void> {
105
- // Generation-guarded write. The dangerous case: an isolate mid-connect (or
106
- // mid-refresh) whose SDK just minted fresh tokens against a still-valid
107
- // grant, racing a force re-auth that wiped KV and bumped the generation.
108
- // Persisting here would resurrect those tokens for later isolates to read.
109
- // A silent skip (not a throw) is deliberate: throwing propagates out of the
110
- // SDK's auth() and fails the in-flight request/connect noisily, whereas
111
- // skipping lets the in-memory client finish its current operation while
112
- // leaving KV wiped — the connector's generation check drops that client on
113
- // its next call. Fails open when no generation was captured, so ordinary
114
- // token refresh (no force) still persists.
115
- if (await this.isStale()) return;
116
- await this.storage.set("oauth:tokens", JSON.stringify(tokens));
306
+ await this.writeValue("oauth:tokens", tokens, (value) =>
307
+ JSON.stringify(value),
308
+ );
117
309
  }
118
310
 
119
311
  /**
@@ -127,7 +319,7 @@ export class KvOAuthProvider implements OAuthClientProvider {
127
319
  */
128
320
  async state(): Promise<string> {
129
321
  const value = randomState();
130
- await this.storage.set("oauth:state", value);
322
+ await this.writeValue("oauth:state", value, (raw) => raw);
131
323
  return value;
132
324
  }
133
325
 
@@ -136,67 +328,166 @@ export class KvOAuthProvider implements OAuthClientProvider {
136
328
  * value. Absent stored state or absent candidate → false (fail closed).
137
329
  */
138
330
  async verifyState(candidate: string | null): Promise<boolean> {
139
- const expected = await this.storage.get("oauth:state");
331
+ const expected = await this.readValue("oauth:state", (raw) => raw);
140
332
  if (!expected || candidate === null) return false;
141
- return timingSafeEqual(candidate, expected);
333
+ const matches = timingSafeEqual(candidate, expected.value);
334
+ if (matches) this.captureGeneration(expected.generation);
335
+ return matches;
142
336
  }
143
337
 
144
338
  async saveCodeVerifier(verifier: string): Promise<void> {
145
- await this.storage.set("oauth:verifier", verifier);
339
+ await this.writeValue("oauth:verifier", verifier, (raw) => raw);
146
340
  }
147
341
 
148
342
  async codeVerifier(): Promise<string> {
149
- const raw = await this.storage.get("oauth:verifier");
150
- if (!raw) throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
151
- return raw;
343
+ const stored = await this.readValue("oauth:verifier", (raw) => raw);
344
+ if (!stored) {
345
+ throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
346
+ }
347
+ return stored.value;
152
348
  }
153
349
 
154
350
  async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
155
- await this.storage.set("oauth:pending", authorizationUrl.toString());
351
+ await this.writeValue(
352
+ "oauth:pending",
353
+ authorizationUrl.toString(),
354
+ (raw) => raw,
355
+ );
156
356
  }
157
357
 
158
358
  /** The stored authorization URL, if a flow is pending. */
159
359
  async pendingAuthorizationUrl(): Promise<string | undefined> {
160
- return (await this.storage.get("oauth:pending")) ?? undefined;
360
+ return (
361
+ await this.readValue("oauth:pending", (raw) => raw)
362
+ )?.value;
161
363
  }
162
364
 
163
365
  /** Clear one-shot flow state after the callback completes. */
164
366
  async clearPending(): Promise<void> {
165
- await this.storage.delete("oauth:pending");
166
- await this.storage.delete("oauth:verifier");
167
- await this.storage.delete("oauth:state");
367
+ const generation = await this.writeGeneration();
368
+ await this.deleteAll([
369
+ oauthValueStorageKey("oauth:pending", generation),
370
+ oauthValueStorageKey("oauth:verifier", generation),
371
+ oauthValueStorageKey("oauth:state", generation),
372
+ ]);
168
373
  }
169
374
 
170
375
  /**
171
- * Force-reauth generation. Monotonic counter shared across isolates via KV;
172
- * NOT cleared by clearPending/invalidateCredentials its whole job is to
173
- * keep advancing so an isolate holding a client from a prior generation can
174
- * notice it went stale. Defaults to 0 when never bumped.
376
+ * Force-reauth epoch shared across isolates through storage. Old numeric
377
+ * generations remain valid strings for migration; new resets use unique
378
+ * nonces, avoiding the lost-update race of read/increment/write.
175
379
  */
176
- async generation(): Promise<number> {
177
- const raw = await this.storage.get("oauth:generation");
178
- const n = raw ? Number(raw) : 0;
179
- return Number.isFinite(n) ? n : 0;
380
+ async generation(): Promise<string> {
381
+ return (await this.storage.get("oauth:generation")) ?? LEGACY_GENERATION;
180
382
  }
181
383
 
182
- /** Advance the generation (on force re-auth) and return the new value. */
183
- async bumpGeneration(): Promise<number> {
184
- const next = (await this.generation()) + 1;
185
- await this.storage.set("oauth:generation", String(next));
384
+ /** True only after an operator disconnect, until an explicit authorization starts. */
385
+ async operatorDisconnected(): Promise<boolean> {
386
+ return this.isOperatorDisconnectedGeneration(await this.generation());
387
+ }
388
+
389
+ /** Interpret a generation already read by a connector without another KV lookup. */
390
+ isOperatorDisconnectedGeneration(generation: string): boolean {
391
+ return generation.startsWith(DISCONNECTED_GENERATION_PREFIX);
392
+ }
393
+
394
+ /** Publish a unique active epoch without a read/modify/write race. */
395
+ async bumpGeneration(): Promise<string> {
396
+ const next = `${ACTIVE_GENERATION_PREFIX}${crypto.randomUUID()}`;
397
+ await this.storage.set("oauth:generation", next);
186
398
  return next;
187
399
  }
188
400
 
401
+ /**
402
+ * Fence every flow that could still write, then remove all durable and
403
+ * one-shot authorization state. The generation is intentionally retained:
404
+ * it is the epoch fence that tells another isolate not to resurrect
405
+ * credentials it read before this reset.
406
+ *
407
+ * Once the fence is durable, attempt every deletion even if one fails. A
408
+ * partial backend outage should not leave unrelated secrets behind merely
409
+ * because an earlier key happened to be the first failed delete.
410
+ */
411
+ async resetAuthorization(operatorDisconnected = false): Promise<void> {
412
+ const nonce = crypto.randomUUID();
413
+ const previous = await this.generation();
414
+ const inherited = await this.cleanupBacklog(previous);
415
+ const active = `${
416
+ operatorDisconnected
417
+ ? DISCONNECTED_GENERATION_PREFIX
418
+ : ACTIVE_GENERATION_PREFIX
419
+ }${nonce}`;
420
+ const retired = [...new Set([...inherited, previous])];
421
+ if (retired.length > MAX_CLEANUP_BACKLOG) {
422
+ throw new Error(
423
+ `OAuth cleanup backlog for "${this.connectorId}" is full`,
424
+ );
425
+ }
426
+ // Publish the complete inherited cleanup work under the prospective epoch
427
+ // before making that epoch active. A crash or later retry can therefore
428
+ // always recover the older namespaces without a storage prefix scan.
429
+ await this.storage.set(
430
+ cleanupBacklogKey(active),
431
+ JSON.stringify(retired),
432
+ );
433
+ // This is the one authoritative transition. From this point onward every
434
+ // old physical namespace is unreadable. There is deliberately no second
435
+ // "finalize" write: concurrent resets therefore cannot overwrite a newer
436
+ // reset's epoch after their cleanup finishes out of order.
437
+ try {
438
+ await this.storage.set("oauth:generation", active);
439
+ } catch (error) {
440
+ try {
441
+ await this.storage.delete(cleanupBacklogKey(active));
442
+ } catch {
443
+ // Best-effort removal of a manifest for an epoch never activated.
444
+ }
445
+ throw error;
446
+ }
447
+
448
+ let firstError: unknown;
449
+ for (const generation of retired) {
450
+ try {
451
+ await this.deleteAll(this.valueKeysForGeneration(generation));
452
+ await this.storage.delete(cleanupBacklogKey(generation));
453
+ } catch (error) {
454
+ firstError ??= error;
455
+ }
456
+ }
457
+ // Keep the active manifest immutable for the epoch's whole lifetime, even
458
+ // after successful cleanup. A late old-epoch write can land after cleanup;
459
+ // if its self-delete fails, the next reset must still inherit the complete
460
+ // lineage without racing a manifest shrink/delete. The successor copies
461
+ // this manifest before activation and then removes this retired copy.
462
+ if (firstError) throw firstError;
463
+ }
464
+
189
465
  async invalidateCredentials(
190
466
  scope: "all" | "client" | "tokens" | "verifier" | "discovery",
191
467
  ): Promise<void> {
192
- if (scope === "all" || scope === "client") {
193
- await this.storage.delete("oauth:client");
468
+ const generation = await this.writeGeneration();
469
+ if (scope === "all") {
470
+ await this.deleteAll([
471
+ oauthValueStorageKey("oauth:client", generation),
472
+ oauthValueStorageKey("oauth:tokens", generation),
473
+ oauthValueStorageKey("oauth:verifier", generation),
474
+ ]);
475
+ return;
476
+ }
477
+ if (scope === "client") {
478
+ await this.storage.delete(
479
+ oauthValueStorageKey("oauth:client", generation),
480
+ );
194
481
  }
195
- if (scope === "all" || scope === "tokens") {
196
- await this.storage.delete("oauth:tokens");
482
+ if (scope === "tokens") {
483
+ await this.storage.delete(
484
+ oauthValueStorageKey("oauth:tokens", generation),
485
+ );
197
486
  }
198
- if (scope === "all" || scope === "verifier") {
199
- await this.storage.delete("oauth:verifier");
487
+ if (scope === "verifier") {
488
+ await this.storage.delete(
489
+ oauthValueStorageKey("oauth:verifier", generation),
490
+ );
200
491
  }
201
492
  }
202
493
  }