@apifuse/provider-sdk 2.2.0-beta.26 → 2.2.0-beta.28

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 (90) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-dev.ts +2 -2
  3. package/bin/apifuse-pack-smoke.ts +1 -1
  4. package/bin/apifuse-pack-types.ts +2 -1
  5. package/bin/apifuse-perf.ts +2 -4
  6. package/bin/apifuse-record.ts +1 -1
  7. package/dist/auth-turn/index.d.ts +1 -1
  8. package/dist/auth-turn/index.js +1 -1
  9. package/dist/ceremonies/index.js +52 -11
  10. package/dist/config/loader.d.ts +1 -1
  11. package/dist/config/loader.js +4 -2
  12. package/dist/contract-serialization.d.ts +1 -20
  13. package/dist/contract-serialization.js +8 -587
  14. package/dist/contract.d.ts +0 -2
  15. package/dist/contract.js +5 -9
  16. package/dist/index.d.ts +9 -8
  17. package/dist/index.js +6 -8
  18. package/dist/provider.d.ts +3 -2
  19. package/dist/provider.js +2 -2
  20. package/dist/runtime/auth-flow.js +1 -1
  21. package/dist/runtime/http.d.ts +1 -0
  22. package/dist/runtime/http.js +135 -12
  23. package/dist/runtime/instrumentation.js +1 -1
  24. package/dist/runtime/native-network-errors.d.ts +33 -0
  25. package/dist/runtime/native-network-errors.js +69 -0
  26. package/dist/runtime/native-network.d.ts +2 -33
  27. package/dist/runtime/native-network.js +2 -68
  28. package/dist/runtime/redis.d.ts +1 -1
  29. package/dist/runtime/redis.js +4 -2
  30. package/dist/runtime/resolver-config.d.ts +6 -0
  31. package/dist/runtime/resolver-config.js +6 -0
  32. package/dist/runtime/resolver-shared.d.ts +3 -0
  33. package/dist/runtime/resolver-shared.js +12 -0
  34. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
  35. package/dist/runtime/resolver-vendors/twocaptcha.js +80 -43
  36. package/dist/runtime/resolver-vendors/types.d.ts +1 -1
  37. package/dist/runtime/resolver.d.ts +6 -10
  38. package/dist/runtime/resolver.js +19 -18
  39. package/dist/runtime/state.js +5 -115
  40. package/dist/runtime/stealth-cookies.d.ts +20 -0
  41. package/dist/runtime/stealth-cookies.js +111 -0
  42. package/dist/runtime/stealth.js +7 -130
  43. package/dist/schema.d.ts +0 -63
  44. package/dist/schema.js +8 -808
  45. package/dist/serve.d.ts +1 -1
  46. package/dist/serve.js +1 -1
  47. package/dist/server/index.d.ts +1 -1
  48. package/dist/server/index.js +1 -1
  49. package/dist/server/self-test.d.ts +13 -0
  50. package/dist/server/self-test.js +124 -46
  51. package/dist/server/serve-implementation.d.ts +199 -0
  52. package/dist/server/serve-implementation.js +2054 -0
  53. package/dist/server/serve.d.ts +1 -187
  54. package/dist/server/serve.js +1 -1827
  55. package/dist/stateful/errors.d.ts +5 -0
  56. package/dist/stateful/errors.js +10 -0
  57. package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
  58. package/dist/stateful/stateful-provider-session-routing.js +2 -10
  59. package/dist/stream.js +7 -1
  60. package/package.json +27 -2
  61. package/src/auth-turn/index.ts +1 -1
  62. package/src/ceremonies/index.ts +68 -18
  63. package/src/config/loader.ts +5 -2
  64. package/src/contract-serialization.ts +8 -859
  65. package/src/contract.ts +5 -16
  66. package/src/index.ts +18 -34
  67. package/src/provider.ts +12 -24
  68. package/src/runtime/auth-flow.ts +1 -1
  69. package/src/runtime/http.ts +155 -11
  70. package/src/runtime/instrumentation.ts +1 -1
  71. package/src/runtime/native-network-errors.ts +99 -0
  72. package/src/runtime/native-network.ts +16 -97
  73. package/src/runtime/redis.ts +7 -2
  74. package/src/runtime/resolver-config.ts +6 -0
  75. package/src/runtime/resolver-shared.ts +17 -0
  76. package/src/runtime/resolver-vendors/twocaptcha.ts +100 -49
  77. package/src/runtime/resolver-vendors/types.ts +1 -0
  78. package/src/runtime/resolver.ts +47 -22
  79. package/src/runtime/state.ts +5 -144
  80. package/src/runtime/stealth-cookies.ts +132 -0
  81. package/src/runtime/stealth.ts +14 -157
  82. package/src/schema.ts +9 -1060
  83. package/src/serve.ts +6 -1
  84. package/src/server/index.ts +1 -0
  85. package/src/server/self-test.ts +184 -59
  86. package/src/server/serve-implementation.ts +3024 -0
  87. package/src/server/serve.ts +1 -2661
  88. package/src/stateful/errors.ts +12 -0
  89. package/src/stateful/stateful-provider-session-routing.ts +2 -11
  90. package/src/stream.ts +8 -1
package/src/serve.ts CHANGED
@@ -1 +1,6 @@
1
- export { createServerApp, type ServeOptions, serve } from "./server/serve.js";
1
+ export {
2
+ createServerApp,
3
+ createServerAppAsync,
4
+ type ServeOptions,
5
+ serve,
6
+ } from "./server/serve.js";
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  createServerApp,
3
+ createServerAppAsync,
3
4
  ERROR_OBSERVABILITY_HEADER,
4
5
  type ErrorObservabilityDetails,
5
6
  type ProviderServerCloseOptions,
@@ -77,11 +77,22 @@ export interface SelfTestResponse {
77
77
  results: SelfTestCaseResult[];
78
78
  }
79
79
 
80
+ export interface SelfTestCancellationLogEvent {
81
+ level: "info";
82
+ event: "self_test_run_cancelled";
83
+ providerId: string;
84
+ requestId: string;
85
+ operationId: string;
86
+ caseName: string;
87
+ reason: string;
88
+ }
89
+
80
90
  export type SelfTestOperationInvoke = (args: {
81
91
  operationId: string;
82
92
  input: unknown;
83
93
  connection?: OperationConnection;
84
94
  requestId: string;
95
+ signal?: AbortSignal;
85
96
  }) => Promise<{
86
97
  status: number;
87
98
  data: unknown;
@@ -106,6 +117,7 @@ export type SelfTestAuthFlowInvoke = (args: {
106
117
  externalRef?: string;
107
118
  input?: Record<string, unknown>;
108
119
  context?: Record<string, unknown>;
120
+ signal?: AbortSignal;
109
121
  }) => Promise<{
110
122
  status: number;
111
123
  body: unknown;
@@ -165,6 +177,8 @@ export interface SelfTestAppOptions {
165
177
  requestBudgetMs?: number;
166
178
  /** Env override for secret collection + budget resolution (tests). */
167
179
  env?: Readonly<Record<string, string | undefined>>;
180
+ /** Structured server logger; defaults to the same JSON console shape as the provider server. */
181
+ logger?: (event: SelfTestCancellationLogEvent) => void;
168
182
  }
169
183
 
170
184
  function resolveSdkVersion(): string {
@@ -248,16 +262,25 @@ export function isSelfTestReadOnlyOperation(operation: OperationDefinition): boo
248
262
  export function createSelfTestInvoke(app: {
249
263
  request: (input: string, requestInit?: RequestInit) => Response | Promise<Response>;
250
264
  }): SelfTestOperationInvoke {
251
- return async ({ operationId, input, connection, requestId }) => {
252
- const response = await app.request(`/v1/${encodeURIComponent(operationId)}`, {
253
- method: "POST",
254
- headers: { "content-type": "application/json" },
255
- body: JSON.stringify({
256
- requestId,
257
- input: input ?? {},
258
- ...(connection ? { connection } : {}),
265
+ return async ({ operationId, input, connection, requestId, signal }) => {
266
+ const responsePromise = Promise.resolve(
267
+ app.request(`/v1/${encodeURIComponent(operationId)}`, {
268
+ method: "POST",
269
+ headers: { "content-type": "application/json" },
270
+ body: JSON.stringify({
271
+ requestId,
272
+ input: input ?? {},
273
+ ...(connection ? { connection } : {}),
274
+ }),
275
+ ...(signal ? { signal } : {}),
259
276
  }),
260
- });
277
+ );
278
+ const response = await responsePromise;
279
+ if (signal?.aborted) {
280
+ await cancelSelfTestResponse(response);
281
+ signal.throwIfAborted();
282
+ }
283
+ // An abort after body consumption starts may let response.text() finish.
261
284
  const text = await response.text();
262
285
  let body: unknown = text;
263
286
  try {
@@ -277,11 +300,58 @@ export function createSelfTestInvoke(app: {
277
300
  };
278
301
  }
279
302
 
303
+ async function cancelSelfTestResponse(response: Response): Promise<void> {
304
+ await response.body?.cancel().catch(() => undefined);
305
+ }
306
+
307
+ function selfTestAbortReason(reason: unknown, sensitiveValues: readonly string[]): string {
308
+ const text =
309
+ reason instanceof Error
310
+ ? reason.message || reason.name
311
+ : reason === undefined
312
+ ? "aborted"
313
+ : String(reason);
314
+ return redactSelfTestText(text, sensitiveValues);
315
+ }
316
+
317
+ function logSelfTestCancellation(
318
+ provider: ProviderDefinition,
319
+ options: SelfTestAppOptions,
320
+ requestId: string,
321
+ selected: SelectedCase,
322
+ signal: AbortSignal,
323
+ sensitiveValues: readonly string[],
324
+ ): void {
325
+ const event: SelfTestCancellationLogEvent = {
326
+ level: "info",
327
+ event: "self_test_run_cancelled",
328
+ providerId: provider.id,
329
+ requestId,
330
+ operationId: selected.operationId,
331
+ caseName: selected.healthCase.name,
332
+ reason: selfTestAbortReason(signal.reason, sensitiveValues),
333
+ };
334
+ if (options.logger) {
335
+ options.logger(event);
336
+ return;
337
+ }
338
+ console.log(JSON.stringify(event));
339
+ }
340
+
280
341
  /** Binds the self-test auth-flow driver to a tenant app's /auth pipeline in-process. */
281
342
  export function createSelfTestAuthFlowInvoke(app: {
282
343
  request: (input: string, requestInit?: RequestInit) => Response | Promise<Response>;
283
344
  }): SelfTestAuthFlowInvoke {
284
- return async ({ route, requestId, flowId, connectionId, externalRef, input, context }) => {
345
+ return async ({
346
+ route,
347
+ requestId,
348
+ flowId,
349
+ connectionId,
350
+ externalRef,
351
+ input,
352
+ context,
353
+ signal,
354
+ }) => {
285
355
  const response = await app.request(`/auth/${route}`, {
286
356
  method: "POST",
287
357
  headers: { "content-type": "application/json" },
@@ -293,6 +363,7 @@ export function createSelfTestAuthFlowInvoke(app: {
293
363
  ...(input ? { input } : {}),
294
364
  ...(context ? { context } : {}),
295
365
  }),
366
+ ...(signal ? { signal } : {}),
296
367
  });
297
368
  const text = await response.text();
298
369
  let body: unknown = text;
@@ -312,17 +383,32 @@ class SelfTestCaseTimeoutError extends Error {
312
383
  }
313
384
  }
314
385
 
315
- async function withCaseTimeout<T>(run: () => Promise<T>, timeoutMs: number): Promise<T> {
386
+ async function withCaseTimeout<T>(
387
+ run: () => Promise<T>,
388
+ timeoutMs: number,
389
+ controller: AbortController,
390
+ ): Promise<T> {
316
391
  let timer: ReturnType<typeof setTimeout> | undefined;
392
+ let onAbort: (() => void) | undefined;
317
393
  try {
318
394
  return await Promise.race([
319
395
  run(),
320
396
  new Promise<never>((_, reject) => {
321
- timer = setTimeout(() => reject(new SelfTestCaseTimeoutError(timeoutMs)), timeoutMs);
397
+ timer = setTimeout(() => {
398
+ const error = new SelfTestCaseTimeoutError(timeoutMs);
399
+ controller.abort(error);
400
+ reject(error);
401
+ }, timeoutMs);
402
+ }),
403
+ new Promise<never>((_, reject) => {
404
+ onAbort = () => reject(controller.signal.reason);
405
+ controller.signal.addEventListener("abort", onAbort, { once: true });
406
+ if (controller.signal.aborted) onAbort();
322
407
  }),
323
408
  ]);
324
409
  } finally {
325
410
  if (timer !== undefined) clearTimeout(timer);
411
+ if (onAbort) controller.signal.removeEventListener("abort", onAbort);
326
412
  }
327
413
  }
328
414
 
@@ -595,6 +681,7 @@ async function materializeFlowCredential(
595
681
  isAbandoned?: () => boolean;
596
682
  connectionId?: string;
597
683
  externalRef?: string;
684
+ signal?: AbortSignal;
598
685
  } = {},
599
686
  ): Promise<
600
687
  | { credential: Record<string, string> }
@@ -621,6 +708,7 @@ async function materializeFlowCredential(
621
708
  flowId,
622
709
  ...(options.connectionId ? { connectionId: options.connectionId } : {}),
623
710
  ...(options.externalRef ? { externalRef: options.externalRef } : {}),
711
+ ...(options.signal ? { signal: options.signal } : {}),
624
712
  }),
625
713
  );
626
714
  if (!started.ok) {
@@ -696,6 +784,7 @@ async function materializeFlowCredential(
696
784
  ...(options.externalRef ? { externalRef: options.externalRef } : {}),
697
785
  input: submitInputs,
698
786
  ...(Object.keys(flowContext).length > 0 ? { context: flowContext } : {}),
787
+ ...(options.signal ? { signal: options.signal } : {}),
699
788
  }),
700
789
  );
701
790
  if (!continued.ok) {
@@ -756,7 +845,7 @@ async function resolveSelfTestConnection(
756
845
  execution: SelfTestExecutionContext,
757
846
  operationId: string,
758
847
  suite: AnyHealthCheckSuite,
759
- options: { forceLogin?: boolean; isAbandoned?: () => boolean } = {},
848
+ options: { forceLogin?: boolean; isAbandoned?: () => boolean; signal?: AbortSignal } = {},
760
849
  ): Promise<SelfTestConnectionResolution> {
761
850
  if (!suite.requiresConnection) return { kind: "connection" };
762
851
  const inputs = execution.credentials ?? {};
@@ -861,6 +950,7 @@ async function resolveSelfTestConnection(
861
950
  ...(options.isAbandoned !== undefined ? { isAbandoned: options.isAbandoned } : {}),
862
951
  connectionId,
863
952
  externalRef: `${execution.provider.id}-${operationId}-self-test`,
953
+ ...(options.signal ? { signal: options.signal } : {}),
864
954
  });
865
955
  if (!("credential" in materialized)) {
866
956
  // Only the multi-turn SKIP is negative-cached. Flow ERRORS
@@ -879,10 +969,9 @@ async function resolveSelfTestConnection(
879
969
  }
880
970
  return materialized;
881
971
  }
882
- // A flow that outlived the case deadline still completes here (the timeout
883
- // only races the promise, it cannot cancel it). The case already reported
884
- // self_test_timeout caching this credential would let the next probe
885
- // reuse a login whose latency just failed the case, hiding the failure.
972
+ // Abort is cooperative: a flow may still complete after the case deadline
973
+ // if provider code ignores the signal. Never cache that late credential or
974
+ // let the next probe hide the timed-out login.
886
975
  if (options.isAbandoned?.() === true) {
887
976
  return {
888
977
  kind: "flow_error",
@@ -913,6 +1002,7 @@ async function executeSelfTestCase(
913
1002
  operationId: string,
914
1003
  suite: AnyHealthCheckSuite,
915
1004
  healthCase: AnyHealthCheckCase,
1005
+ caseController: AbortController,
916
1006
  ): Promise<SelfTestCaseResult> {
917
1007
  const { provider, invoke } = execution;
918
1008
  // execution.sensitiveValues may grow while the case runs (flow-issued
@@ -926,6 +1016,8 @@ async function executeSelfTestCase(
926
1016
  const caseDeadlineAtMs = performance.now() + timeoutMs;
927
1017
  const remainingCaseTimeoutMs = () =>
928
1018
  Math.max(1, Math.ceil(caseDeadlineAtMs - performance.now()));
1019
+ const runWithCaseTimeout = <T>(run: () => Promise<T>, remainingMs: number) =>
1020
+ withCaseTimeout(run, remainingMs, caseController);
929
1021
 
930
1022
  const beginCase = () => {
931
1023
  const startedAt = new Date().toISOString();
@@ -959,16 +1051,17 @@ async function executeSelfTestCase(
959
1051
  }
960
1052
 
961
1053
  const resolveConnection = async (forceLogin: boolean): Promise<SelfTestConnectionResolution> => {
962
- // The timeout only races the flow promise it cannot cancel it. Once
963
- // the deadline fires, the still-running resolution is marked abandoned
964
- // so its late completion cannot write the session cache.
1054
+ // The deadline aborts the in-process flow request. Keep the abandonment
1055
+ // guard as a backstop for provider code that ignores cancellation so a
1056
+ // late completion still cannot write the session cache.
965
1057
  let abandoned = false;
966
1058
  try {
967
- return await withCaseTimeout(
1059
+ return await runWithCaseTimeout(
968
1060
  () =>
969
1061
  resolveSelfTestConnection(execution, operationId, suite, {
970
1062
  forceLogin,
971
1063
  isAbandoned: () => abandoned,
1064
+ signal: caseController.signal,
972
1065
  }),
973
1066
  remainingCaseTimeoutMs(),
974
1067
  );
@@ -1012,7 +1105,7 @@ async function executeSelfTestCase(
1012
1105
  // operation attempt, or a slow login reads as a fast healthy case.
1013
1106
  const { startedAtMs, finish } = caseScope;
1014
1107
  try {
1015
- return await withCaseTimeout(async () => {
1108
+ return await runWithCaseTimeout(async () => {
1016
1109
  const resolvedInput = resolveHealthCheckInputDateTokens(healthCase.input);
1017
1110
  const preparedInput = healthCase.prepareInput
1018
1111
  ? await healthCase.prepareInput({
@@ -1033,6 +1126,7 @@ async function executeSelfTestCase(
1033
1126
  input: gatewayInput,
1034
1127
  connection,
1035
1128
  requestId: `${execution.requestId}-prepare-${randomUUID()}`,
1129
+ signal: caseController.signal,
1036
1130
  });
1037
1131
  if (executed.status === 401 || executed.status === 403) {
1038
1132
  prepareAuthStatus = executed.status;
@@ -1053,6 +1147,7 @@ async function executeSelfTestCase(
1053
1147
  input: preparedInput,
1054
1148
  connection,
1055
1149
  requestId: `${execution.requestId}-${randomUUID()}`,
1150
+ signal: caseController.signal,
1056
1151
  });
1057
1152
  const durationMs = performance.now() - startedAtMs;
1058
1153
 
@@ -1383,47 +1478,77 @@ export function createSelfTestApp(provider: ProviderDefinition, options: SelfTes
1383
1478
  };
1384
1479
  const deadline = performance.now() + requestBudgetMs;
1385
1480
  const results: SelfTestCaseResult[] = [];
1481
+ const runSignal = c.req.raw.signal;
1482
+ let interruptedCase: SelectedCase | undefined;
1386
1483
  // Sequential execution (parallelism 1): self-tests run on serving pods
1387
1484
  // and must never compete with themselves for upstream quota.
1388
1485
  for (const selected of selection.cases) {
1389
- if (performance.now() >= deadline) {
1390
- const now = new Date().toISOString();
1391
- results.push({
1392
- operationId: selected.operationId,
1393
- caseName: selected.healthCase.name,
1394
- status: "skipped",
1395
- label: selected.healthCase.name,
1396
- responseTimeMs: 0,
1397
- skipReason: "budget_exhausted",
1398
- startedAt: now,
1399
- finishedAt: now,
1400
- });
1401
- continue;
1402
- }
1403
- if (!isSelfTestReadOnlyOperation(selected.operation)) {
1404
- const now = new Date().toISOString();
1405
- results.push({
1406
- operationId: selected.operationId,
1407
- caseName: selected.healthCase.name,
1408
- status: "error",
1409
- label: selected.healthCase.name,
1410
- responseTimeMs: 0,
1411
- error: {
1412
- code: "operation_not_read_only",
1413
- message: `Operation "${selected.operationId}" is not classified read-only; self-test refuses to execute it.`,
1414
- },
1415
- startedAt: now,
1416
- finishedAt: now,
1417
- });
1418
- continue;
1486
+ const caseController = new AbortController();
1487
+ const abortFromRun = () => caseController.abort(runSignal.reason);
1488
+ runSignal.addEventListener("abort", abortFromRun, { once: true });
1489
+ if (runSignal.aborted) abortFromRun();
1490
+ try {
1491
+ if (runSignal.aborted) {
1492
+ interruptedCase = selected;
1493
+ break;
1494
+ }
1495
+ if (performance.now() >= deadline) {
1496
+ const now = new Date().toISOString();
1497
+ results.push({
1498
+ operationId: selected.operationId,
1499
+ caseName: selected.healthCase.name,
1500
+ status: "skipped",
1501
+ label: selected.healthCase.name,
1502
+ responseTimeMs: 0,
1503
+ skipReason: "budget_exhausted",
1504
+ startedAt: now,
1505
+ finishedAt: now,
1506
+ });
1507
+ continue;
1508
+ }
1509
+ if (!isSelfTestReadOnlyOperation(selected.operation)) {
1510
+ const now = new Date().toISOString();
1511
+ results.push({
1512
+ operationId: selected.operationId,
1513
+ caseName: selected.healthCase.name,
1514
+ status: "error",
1515
+ label: selected.healthCase.name,
1516
+ responseTimeMs: 0,
1517
+ error: {
1518
+ code: "operation_not_read_only",
1519
+ message: `Operation "${selected.operationId}" is not classified read-only; self-test refuses to execute it.`,
1520
+ },
1521
+ startedAt: now,
1522
+ finishedAt: now,
1523
+ });
1524
+ continue;
1525
+ }
1526
+ results.push(
1527
+ await executeSelfTestCase(
1528
+ execution,
1529
+ selected.operationId,
1530
+ selected.suite,
1531
+ selected.healthCase,
1532
+ caseController,
1533
+ ),
1534
+ );
1535
+ if (runSignal.aborted) {
1536
+ interruptedCase = selected;
1537
+ break;
1538
+ }
1539
+ } finally {
1540
+ runSignal.removeEventListener("abort", abortFromRun);
1541
+ caseController.abort();
1419
1542
  }
1420
- results.push(
1421
- await executeSelfTestCase(
1422
- execution,
1423
- selected.operationId,
1424
- selected.suite,
1425
- selected.healthCase,
1426
- ),
1543
+ }
1544
+ if (interruptedCase) {
1545
+ logSelfTestCancellation(
1546
+ provider,
1547
+ options,
1548
+ request.requestId,
1549
+ interruptedCase,
1550
+ runSignal,
1551
+ execution.sensitiveValues,
1427
1552
  );
1428
1553
  }
1429
1554
  const singleCase = request.operationId !== undefined && request.caseName !== undefined;