@apifuse/provider-sdk 2.2.0-beta.20 → 2.2.0-beta.21

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.
@@ -17,6 +17,93 @@ import {
17
17
 
18
18
  const DEFAULT_REDIS_TIMEOUT_MS = 250;
19
19
  const REDIS_STATE_PREFIX = "apifuse:provider-state:v1";
20
+ const LEGACY_INDEX_SCAN_COUNT = 256;
21
+ const LEGACY_INDEX_SCAN_MAX_PAGES = 8;
22
+ const SET_WITH_QUOTA_SCRIPT = `
23
+ local now = tonumber(ARGV[1])
24
+ local max_entries = tonumber(ARGV[2])
25
+ local expires_at = tonumber(ARGV[3])
26
+ local index_ttl = tonumber(ARGV[4])
27
+ local envelope = ARGV[5]
28
+
29
+ redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
30
+ local exists = redis.call("EXISTS", KEYS[1])
31
+ local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
32
+ if exists == 0 and not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
33
+ return {0, false}
34
+ end
35
+
36
+ redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
37
+ redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
38
+ redis.call("PEXPIRE", KEYS[2], index_ttl)
39
+ return {1, envelope}
40
+ `;
41
+
42
+ const COMPARE_AND_SET_WITH_QUOTA_SCRIPT = `
43
+ local current = redis.call("GET", KEYS[1])
44
+ local current_version = 0
45
+ if current then
46
+ local ok, decoded = pcall(cjson.decode, current)
47
+ if not ok or type(decoded) ~= "table" or type(decoded.version) ~= "number" then
48
+ return {-2, current}
49
+ end
50
+ current_version = decoded.version
51
+ end
52
+ if current_version ~= tonumber(ARGV[1]) then
53
+ return {-1, current or false}
54
+ end
55
+
56
+ local now = tonumber(ARGV[2])
57
+ local max_entries = tonumber(ARGV[3])
58
+ local expires_at = tonumber(ARGV[4])
59
+ local index_ttl = tonumber(ARGV[5])
60
+ local envelope = ARGV[6]
61
+ redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
62
+ local exists = current and 1 or 0
63
+ local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
64
+ if exists == 0 and not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
65
+ return {0, false}
66
+ end
67
+
68
+ redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
69
+ redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
70
+ redis.call("PEXPIRE", KEYS[2], index_ttl)
71
+ return {1, envelope}
72
+ `;
73
+
74
+ const DELETE_WITH_INDEX_SCRIPT = `
75
+ redis.call("DEL", KEYS[1])
76
+ redis.call("ZREM", KEYS[2], KEYS[1])
77
+ return 1
78
+ `;
79
+
80
+ // Older SDKs wrote only the value key. Every operation that depends on the
81
+ // namespace index advances a bounded SCAN cursor and lazily imports active
82
+ // legacy envelopes into the new ZSET. The cursor is
83
+ // deliberately cyclic rather than permanently "complete": an old pod may
84
+ // still write an unindexed key during a rolling deploy. Each list/write call
85
+ // does a fixed amount of migration work; Redis KEYS and unbounded scans remain
86
+ // forbidden.
87
+ const BACKFILL_LEGACY_INDEX_SCRIPT = `
88
+ local now = tonumber(ARGV[1])
89
+ local index_ttl = tonumber(ARGV[2])
90
+ local next_cursor = ARGV[3]
91
+
92
+ redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", now)
93
+ for i = 4, #ARGV, 3 do
94
+ local key = ARGV[i]
95
+ local expected = ARGV[i + 1]
96
+ local expires_at = tonumber(ARGV[i + 2])
97
+ if redis.call("GET", key) == expected then
98
+ redis.call("ZADD", KEYS[1], "NX", expires_at, key)
99
+ end
100
+ end
101
+ if redis.call("EXISTS", KEYS[1]) == 1 then
102
+ redis.call("PEXPIRE", KEYS[1], index_ttl)
103
+ end
104
+ redis.call("SET", KEYS[2], next_cursor, "PX", index_ttl)
105
+ return redis.call("ZCARD", KEYS[1])
106
+ `;
20
107
 
21
108
  type RedisProviderRuntimeStateOptions = {
22
109
  readonly redisUrl: string;
@@ -87,6 +174,10 @@ function publicStateKey(
87
174
  return redisKey.startsWith(prefix) ? redisKey.slice(prefix.length) : redisKey;
88
175
  }
89
176
 
177
+ function redisGlobLiteral(value: string): string {
178
+ return value.replace(/[\\*?\[\]]/g, "\\$&");
179
+ }
180
+
90
181
  function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
91
182
  const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
92
183
  if (!match) return 3_600_000;
@@ -171,13 +262,91 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
171
262
  return providerStateKey(this.providerId, this.namespaceName, key);
172
263
  }
173
264
 
174
- private prefix(): string {
265
+ private indexKey(): string {
266
+ // Keep bookkeeping outside the caller-owned keyspace. A provider may use
267
+ // any state key (including "__index"), so a suffix inside the namespace
268
+ // could turn the ZSET into a string and break every subsequent write.
269
+ const namespaceIdentity = Buffer.from(
270
+ providerStatePrefix(this.providerId, this.namespaceName),
271
+ "utf8",
272
+ ).toString("base64url");
273
+ return `${REDIS_STATE_PREFIX}:index:${namespaceIdentity}`;
274
+ }
275
+
276
+ private legacyScanCursorKey(): string {
277
+ return `${this.indexKey()}:legacy-scan-cursor`;
278
+ }
279
+
280
+ private legacyPrefix(): string {
175
281
  return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
176
282
  }
177
283
 
178
- private async activeKeys(): Promise<string[]> {
284
+ private async backfillLegacyIndex(): Promise<void> {
179
285
  await requireRedisReady(this.backend.redis);
180
- return await withRequiredRedis(() => this.backend.redis.keys(`${this.prefix()}*`));
286
+ const cursorKey = this.legacyScanCursorKey();
287
+ let cursor =
288
+ (await withRequiredRedis(() => this.backend.redis.get(cursorKey))) ?? "0";
289
+ const pattern = `${redisGlobLiteral(this.legacyPrefix())}*`;
290
+ const indexTtlMs = parseStateDurationMs(this.options.maxTtl);
291
+
292
+ for (let page = 0; page < LEGACY_INDEX_SCAN_MAX_PAGES; page += 1) {
293
+ const [nextCursor, keys] = await withRequiredRedis(() =>
294
+ this.backend.redis.scan(
295
+ cursor,
296
+ "MATCH",
297
+ pattern,
298
+ "COUNT",
299
+ LEGACY_INDEX_SCAN_COUNT,
300
+ ),
301
+ );
302
+ const rawValues =
303
+ keys.length > 0
304
+ ? await withRequiredRedis(() => this.backend.redis.mget(keys))
305
+ : [];
306
+ const now = Date.now();
307
+ const activeLegacyArgs: string[] = [];
308
+ for (const [index, raw] of rawValues.entries()) {
309
+ const key = keys[index];
310
+ if (!key || !raw) continue;
311
+ const envelope = envelopeFromJson(
312
+ publicStateKey(this.providerId, this.namespaceName, key),
313
+ raw,
314
+ );
315
+ const expiresAtMs = envelope ? Date.parse(envelope.expiresAt) : Number.NaN;
316
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) continue;
317
+ activeLegacyArgs.push(key, raw, String(expiresAtMs));
318
+ }
319
+ await withRequiredRedis(() =>
320
+ this.backend.redis.eval(
321
+ BACKFILL_LEGACY_INDEX_SCRIPT,
322
+ 2,
323
+ this.indexKey(),
324
+ cursorKey,
325
+ String(now),
326
+ String(indexTtlMs),
327
+ nextCursor,
328
+ ...activeLegacyArgs,
329
+ ),
330
+ );
331
+ cursor = nextCursor;
332
+ if (cursor === "0") break;
333
+ }
334
+ }
335
+
336
+ private async indexedKeys(limit: number): Promise<string[]> {
337
+ await requireRedisReady(this.backend.redis);
338
+ const now = Date.now();
339
+ return await withRequiredRedis(async () => {
340
+ await this.backend.redis.zremrangebyscore(this.indexKey(), "-inf", now);
341
+ return await this.backend.redis.zrangebyscore(
342
+ this.indexKey(),
343
+ now + 1,
344
+ "+inf",
345
+ "LIMIT",
346
+ 0,
347
+ limit,
348
+ );
349
+ });
181
350
  }
182
351
 
183
352
  private enforceValueSize(value: unknown): void {
@@ -189,23 +358,41 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
189
358
  }
190
359
  }
191
360
 
192
- private async enforceMaxEntries(key: string): Promise<void> {
193
- const keys = await this.activeKeys();
194
- const redisKey = this.redisKey(key);
195
- const otherKeys = keys.filter((candidate) => candidate !== redisKey);
196
- if (otherKeys.length >= this.options.maxEntries) {
361
+ private quotaExceeded(): UnsupportedProviderStateError {
362
+ return new UnsupportedProviderStateError(
363
+ `Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
364
+ );
365
+ }
366
+
367
+ private writeTiming(ttl: StateWriteOptions["ttl"]): {
368
+ expiresAt: string;
369
+ expiresAtMs: number;
370
+ indexTtlMs: number;
371
+ } {
372
+ const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
373
+ const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
374
+ if (ttlMs > maxTtlMs) {
197
375
  throw new UnsupportedProviderStateError(
198
- `Provider runtime state namespace quota exceeded (${otherKeys.length + 1} > ${this.options.maxEntries})`,
376
+ `Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
199
377
  );
200
378
  }
379
+ const expiresAtMs = Date.now() + ttlMs;
380
+ return {
381
+ expiresAt: new Date(expiresAtMs).toISOString(),
382
+ expiresAtMs,
383
+ indexTtlMs: maxTtlMs,
384
+ };
201
385
  }
202
386
 
203
387
  async list<T>(options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
204
- const keys = (await this.activeKeys()).filter((key) => {
388
+ const requestedLimit = Math.max(0, options?.limit ?? this.options.maxEntries);
389
+ if (requestedLimit === 0) return [];
390
+ await this.backfillLegacyIndex();
391
+ const keys = (await this.indexedKeys(this.options.maxEntries)).filter((key) => {
205
392
  const publicKey = publicStateKey(this.providerId, this.namespaceName, key);
206
393
  return options?.prefix ? publicKey.startsWith(options.prefix) : true;
207
394
  });
208
- const limited = keys.slice(0, Math.max(0, options?.limit ?? keys.length));
395
+ const limited = keys.slice(0, requestedLimit);
209
396
  if (limited.length === 0) return [];
210
397
  const values = await withRequiredRedis(() => this.backend.redis.mget(limited));
211
398
  return values.flatMap((raw, index) => {
@@ -224,22 +411,34 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
224
411
 
225
412
  async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
226
413
  this.enforceValueSize(value);
227
- await this.enforceMaxEntries(key);
414
+ await this.backfillLegacyIndex();
228
415
  const current = await this.get<T>(key);
229
416
  const createdAt = current?.createdAt ?? new Date().toISOString();
230
417
  const version = (current?.version ?? 0) + 1;
231
- const ttl = options?.ttl ?? this.options.defaultTtl;
232
- const ttlMs = parseStateDurationMs(ttl);
233
- const expiresAt = resolveExpiresAt(ttl);
234
- const envelope = redisEnvelope(value, version, createdAt, expiresAt);
235
- await withRequiredRedis(() =>
236
- this.backend.redis.set(this.redisKey(key), JSON.stringify(envelope), "PX", ttlMs),
418
+ const timing = this.writeTiming(options?.ttl);
419
+ const envelope = redisEnvelope(value, version, createdAt, timing.expiresAt);
420
+ await requireRedisReady(this.backend.redis);
421
+ const result = await withRequiredRedis(() =>
422
+ this.backend.redis.eval(
423
+ SET_WITH_QUOTA_SCRIPT,
424
+ 2,
425
+ this.redisKey(key),
426
+ this.indexKey(),
427
+ String(Date.now()),
428
+ String(this.options.maxEntries),
429
+ String(timing.expiresAtMs),
430
+ String(timing.indexTtlMs),
431
+ JSON.stringify(envelope),
432
+ ),
237
433
  );
434
+ if (!Array.isArray(result) || Number(result[0]) !== 1) {
435
+ throw this.quotaExceeded();
436
+ }
238
437
  return {
239
438
  key,
240
439
  value,
241
440
  version,
242
- expiresAt,
441
+ expiresAt: timing.expiresAt,
243
442
  createdAt,
244
443
  updatedAt: envelope.updatedAt,
245
444
  };
@@ -263,16 +462,64 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
263
462
  options?: StateWriteOptions,
264
463
  ): Promise<StateCasResult<T>> {
265
464
  this.enforceValueSize(value);
465
+ await this.backfillLegacyIndex();
266
466
  const current = await this.get<T>(key);
267
467
  if ((current?.version ?? 0) !== expectedVersion) {
268
468
  return { ok: false, current };
269
469
  }
270
- return { ok: true, value: await this.set(key, value, options) };
470
+ const createdAt = current?.createdAt ?? new Date().toISOString();
471
+ const timing = this.writeTiming(options?.ttl);
472
+ const envelope = redisEnvelope(
473
+ value,
474
+ expectedVersion + 1,
475
+ createdAt,
476
+ timing.expiresAt,
477
+ );
478
+ await requireRedisReady(this.backend.redis);
479
+ const result = await withRequiredRedis(() =>
480
+ this.backend.redis.eval(
481
+ COMPARE_AND_SET_WITH_QUOTA_SCRIPT,
482
+ 2,
483
+ this.redisKey(key),
484
+ this.indexKey(),
485
+ String(expectedVersion),
486
+ String(Date.now()),
487
+ String(this.options.maxEntries),
488
+ String(timing.expiresAtMs),
489
+ String(timing.indexTtlMs),
490
+ JSON.stringify(envelope),
491
+ ),
492
+ );
493
+ if (Array.isArray(result) && Number(result[0]) === 0) {
494
+ throw this.quotaExceeded();
495
+ }
496
+ if (!Array.isArray(result) || Number(result[0]) !== 1) {
497
+ const rawCurrent = Array.isArray(result) && typeof result[1] === "string" ? result[1] : null;
498
+ return { ok: false, current: envelopeFromJson(key, rawCurrent) };
499
+ }
500
+ return {
501
+ ok: true,
502
+ value: {
503
+ key,
504
+ value,
505
+ version: envelope.version,
506
+ expiresAt: timing.expiresAt,
507
+ createdAt,
508
+ updatedAt: envelope.updatedAt,
509
+ },
510
+ };
271
511
  }
272
512
 
273
513
  async delete(key: string): Promise<void> {
274
514
  await requireRedisReady(this.backend.redis);
275
- await withRequiredRedis(() => this.backend.redis.del(this.redisKey(key)));
515
+ await withRequiredRedis(() =>
516
+ this.backend.redis.eval(
517
+ DELETE_WITH_INDEX_SCRIPT,
518
+ 2,
519
+ this.redisKey(key),
520
+ this.indexKey(),
521
+ ),
522
+ );
276
523
  }
277
524
 
278
525
  async increment(
@@ -358,6 +605,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
358
605
 
359
606
  constructor(private readonly options: StateNamespaceOptions) {}
360
607
 
608
+ private enforceValueSize(value: unknown): void {
609
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
610
+ if (bytes > this.options.maxValueBytes) {
611
+ throw new UnsupportedProviderStateError(
612
+ `Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`,
613
+ );
614
+ }
615
+ }
616
+
617
+ private enforceWritePolicy(key: string, value: unknown, ttl: StateWriteOptions["ttl"]): void {
618
+ this.enforceValueSize(value);
619
+ const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
620
+ const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
621
+ if (ttlMs > maxTtlMs) {
622
+ throw new UnsupportedProviderStateError(
623
+ `Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
624
+ );
625
+ }
626
+ if (!this.values.has(key) && this.values.size >= this.options.maxEntries) {
627
+ throw new UnsupportedProviderStateError(
628
+ `Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
629
+ );
630
+ }
631
+ }
632
+
361
633
  private pruneExpired(nowMs = Date.now()): void {
362
634
  for (const [key, row] of this.values.entries()) {
363
635
  if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
@@ -381,6 +653,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
381
653
 
382
654
  async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
383
655
  this.pruneExpired();
656
+ this.enforceWritePolicy(key, value, options?.ttl);
384
657
  const now = new Date().toISOString();
385
658
  const current = this.values.get(key);
386
659
  const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
@@ -407,14 +680,30 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
407
680
  }
408
681
 
409
682
  async compareAndSet<T>(
410
- _key: string,
411
- _expectedVersion: number,
412
- _value: T,
413
- _options?: StateWriteOptions,
683
+ key: string,
684
+ expectedVersion: number,
685
+ value: T,
686
+ options?: StateWriteOptions,
414
687
  ): Promise<StateCasResult<T>> {
415
- throw new UnsupportedProviderStateError(
416
- "In-memory provider runtime state does not support compareAndSet",
417
- );
688
+ this.pruneExpired();
689
+ const current = this.values.get(key) as StateValue<T> | undefined;
690
+ if ((current?.version ?? 0) !== expectedVersion) {
691
+ return { ok: false, current: current ?? null };
692
+ }
693
+ this.enforceWritePolicy(key, value, options?.ttl);
694
+ const now = new Date().toISOString();
695
+ const stored = {
696
+ key,
697
+ value,
698
+ version: expectedVersion + 1,
699
+ expiresAt: resolveMemoryStateExpiresAt(
700
+ options?.ttl ?? this.options.defaultTtl,
701
+ ),
702
+ createdAt: current?.createdAt ?? now,
703
+ updatedAt: now,
704
+ } satisfies StateValue<T>;
705
+ this.values.set(key, stored);
706
+ return { ok: true, value: stored };
418
707
  }
419
708
 
420
709
  async delete(key: string): Promise<void> {
@@ -478,6 +478,7 @@ function createAuthFlowContext(
478
478
 
479
479
  return {
480
480
  context: {
481
+ flowId: request.flowId,
481
482
  connectionId: request.connectionId,
482
483
  externalRef: request.externalRef,
483
484
  tenantId: request.tenantId ?? "",
@@ -502,7 +503,12 @@ function createAuthFlowContext(
502
503
  },
503
504
  }
504
505
  : {}),
505
- env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
506
+ env: createEnvContext([
507
+ ...(provider.secrets?.map((secret) => secret.name) ?? []),
508
+ ...(provider.auth?.mode === "oauth2_proxied"
509
+ ? ["APIFUSE__AUTH_PROXY__URL"]
510
+ : []),
511
+ ]),
506
512
  credential,
507
513
  context: flowContextStore.context,
508
514
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
@@ -531,7 +537,7 @@ export type ProviderServerLogEvent =
531
537
  | (ProviderServerLogEventBase & {
532
538
  level: "info";
533
539
  event: "provider_request_completed";
534
- })
540
+ })
535
541
  | (ProviderServerLogEventBase & {
536
542
  level: "warn" | "error";
537
543
  event: "provider_request_failed";
@@ -545,13 +551,13 @@ export type ProviderServerLogEvent =
545
551
  signal?: "unregistered_provider_error_code";
546
552
  signalFix?: string;
547
553
  issues?: Array<{ path: string; code: string; message: string }>;
548
- })
554
+ })
549
555
  | {
550
556
  level: "warn";
551
557
  event: "provider_secrets_missing";
552
558
  providerId: string;
553
559
  missingSecrets: string[];
554
- }
560
+ }
555
561
  | {
556
562
  level: "warn";
557
563
  event: "provider_cleanup_failed";
@@ -562,7 +568,7 @@ export type ProviderServerLogEvent =
562
568
  resource: "browser" | "stealth";
563
569
  errorClass: string;
564
570
  message: string;
565
- }
571
+ }
566
572
  | {
567
573
  level: "error";
568
574
  event: "provider_shutdown_hook_failed";
@@ -570,7 +576,7 @@ export type ProviderServerLogEvent =
570
576
  hookIndex: number;
571
577
  errorClass: string;
572
578
  message: string;
573
- };
579
+ };
574
580
 
575
581
  export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
576
582
 
@@ -599,14 +605,14 @@ export type ProviderServerOptions = {
599
605
  * @example
600
606
  * ```ts
601
607
  * await serve(provider, {
602
- * shutdown: {
603
- * hooks: [
604
- * async () => { await emitter.flush(); },
605
- * async () => { await sessionManager.closeAll("server-shutdown"); },
606
- * async () => { await lease.release(); },
607
- * async () => { await router.close(); },
608
- * ],
609
- * },
608
+ * shutdown: {
609
+ * hooks: [
610
+ * async () => { await emitter.flush(); },
611
+ * async () => { await sessionManager.closeAll("server-shutdown"); },
612
+ * async () => { await lease.release(); },
613
+ * async () => { await router.close(); },
614
+ * ],
615
+ * },
610
616
  * });
611
617
  * ```
612
618
  */
@@ -3,7 +3,13 @@ import { PROVIDER_ERROR_SOURCES } from "../observability.js";
3
3
 
4
4
  import { HttpRetryPreset } from "../types.js";
5
5
 
6
- export const ConnectionModeSchema = z.enum(["oauth2", "credentials", "platform-managed", "none"]);
6
+ export const ConnectionModeSchema = z.enum([
7
+ "oauth2",
8
+ "oauth2_proxied",
9
+ "credentials",
10
+ "platform-managed",
11
+ "none",
12
+ ]);
7
13
 
8
14
  export const OperationConnectionSchema = z.object({
9
15
  id: z.string(),
@@ -29,7 +29,13 @@ import type {
29
29
  // A single lowercase segment (no hyphen) is a valid id, so the trailing group
30
30
  // is optional (`*`), matching providers like `kakaomap`, `kstartup`, `triple`.
31
31
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
32
- const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
32
+ const VALID_AUTH_MODES = [
33
+ "none",
34
+ "platform-managed",
35
+ "credentials",
36
+ "oauth2",
37
+ "oauth2_proxied",
38
+ ] as const;
33
39
  const UPDATE_SNAPSHOT_ARGS = new Set(["-u", "--update-snapshots"]);
34
40
  const snapshotCaptureStates = new WeakMap<
35
41
  ProviderContext,
@@ -754,7 +760,7 @@ function formatJsonDiff(current: unknown, expected: unknown): string {
754
760
 
755
761
  if (currentLine === expectedLine) {
756
762
  if (currentLine !== undefined) {
757
- lines.push(` ${currentLine}`);
763
+ lines.push(` ${currentLine}`);
758
764
  }
759
765
  continue;
760
766
  }