@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.
- package/CHANGELOG.md +5 -0
- package/dist/ceremonies/index.d.ts +8 -0
- package/dist/ceremonies/index.js +57 -0
- package/dist/define.js +103 -1
- package/dist/index.d.ts +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -0
- package/dist/runtime/auth-flow.js +1 -0
- package/dist/runtime/state.js +223 -22
- package/dist/server/serve.d.ts +9 -8
- package/dist/server/serve.js +7 -1
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.js +7 -1
- package/dist/testing/run.js +8 -2
- package/dist/types.d.ts +28 -12
- package/package.json +1 -1
- package/src/ceremonies/index.ts +79 -0
- package/src/define.ts +138 -1
- package/src/index.ts +1 -0
- package/src/lint.ts +7 -1
- package/src/provider.ts +1 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/state.ts +317 -28
- package/src/server/serve.ts +20 -14
- package/src/server/types.ts +7 -1
- package/src/testing/run.ts +8 -2
- package/src/types.ts +48 -26
package/src/runtime/state.ts
CHANGED
|
@@ -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
|
|
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
|
|
284
|
+
private async backfillLegacyIndex(): Promise<void> {
|
|
179
285
|
await requireRedisReady(this.backend.redis);
|
|
180
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
683
|
+
key: string,
|
|
684
|
+
expectedVersion: number,
|
|
685
|
+
value: T,
|
|
686
|
+
options?: StateWriteOptions,
|
|
414
687
|
): Promise<StateCasResult<T>> {
|
|
415
|
-
|
|
416
|
-
|
|
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> {
|
package/src/server/serve.ts
CHANGED
|
@@ -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(
|
|
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
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
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
|
*/
|
package/src/server/types.ts
CHANGED
|
@@ -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([
|
|
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(),
|
package/src/testing/run.ts
CHANGED
|
@@ -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 = [
|
|
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(`
|
|
763
|
+
lines.push(` ${currentLine}`);
|
|
758
764
|
}
|
|
759
765
|
continue;
|
|
760
766
|
}
|