@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/dist/serve.d.ts CHANGED
@@ -1 +1 @@
1
- export { createServerApp, type ServeOptions, serve } from "./server/serve.js";
1
+ export { createServerApp, createServerAppAsync, type ServeOptions, serve, } from "./server/serve.js";
package/dist/serve.js CHANGED
@@ -1 +1 @@
1
- export { createServerApp, serve } from "./server/serve.js";
1
+ export { createServerApp, createServerAppAsync, serve, } from "./server/serve.js";
@@ -1,4 +1,4 @@
1
- export { createServerApp, ERROR_OBSERVABILITY_HEADER, type ErrorObservabilityDetails, type ProviderServerCloseOptions, type ProviderServerHandle, type ProviderServerLogEvent, type ProviderServerLogger, type ProviderServerOperationExecutor, type ProviderServerOperationExecutorInput, type ProviderServerOptions, type ProviderServerStatefulForwardEnvelope, type ServeOptions, serve, } from "./serve.js";
1
+ export { createServerApp, createServerAppAsync, ERROR_OBSERVABILITY_HEADER, type ErrorObservabilityDetails, type ProviderServerCloseOptions, type ProviderServerHandle, type ProviderServerLogEvent, type ProviderServerLogger, type ProviderServerOperationExecutor, type ProviderServerOperationExecutorInput, type ProviderServerOptions, type ProviderServerStatefulForwardEnvelope, type ServeOptions, serve, } from "./serve.js";
2
2
  export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_AUTH_FLOW_MULTI_TURN_SKIP_REASON, SELF_TEST_AUTH_FLOW_REJECTED_SKIP_REASON, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, type SelfTestAppOptions, type SelfTestAuthFlowInvoke, type SelfTestAuthFlowRoute, type SelfTestCaseResult, type SelfTestCaseStatus, type SelfTestOperationInvoke, type SelfTestRequest, SelfTestRequestSchema, type SelfTestResponse, } from "./self-test.js";
3
3
  export { type InputDateTokenCalendar, resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js";
4
4
  export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction.js";
@@ -1,4 +1,4 @@
1
- export { createServerApp, ERROR_OBSERVABILITY_HEADER, serve, } from "./serve.js";
1
+ export { createServerApp, createServerAppAsync, ERROR_OBSERVABILITY_HEADER, serve, } from "./serve.js";
2
2
  export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_AUTH_FLOW_MULTI_TURN_SKIP_REASON, SELF_TEST_AUTH_FLOW_REJECTED_SKIP_REASON, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, SelfTestRequestSchema, } from "./self-test.js";
3
3
  export { resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js";
4
4
  export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction.js";
@@ -51,11 +51,21 @@ export interface SelfTestResponse {
51
51
  result?: SelfTestCaseResult;
52
52
  results: SelfTestCaseResult[];
53
53
  }
54
+ export interface SelfTestCancellationLogEvent {
55
+ level: "info";
56
+ event: "self_test_run_cancelled";
57
+ providerId: string;
58
+ requestId: string;
59
+ operationId: string;
60
+ caseName: string;
61
+ reason: string;
62
+ }
54
63
  export type SelfTestOperationInvoke = (args: {
55
64
  operationId: string;
56
65
  input: unknown;
57
66
  connection?: OperationConnection;
58
67
  requestId: string;
68
+ signal?: AbortSignal;
59
69
  }) => Promise<{
60
70
  status: number;
61
71
  data: unknown;
@@ -78,6 +88,7 @@ export type SelfTestAuthFlowInvoke = (args: {
78
88
  externalRef?: string;
79
89
  input?: Record<string, unknown>;
80
90
  context?: Record<string, unknown>;
91
+ signal?: AbortSignal;
81
92
  }) => Promise<{
82
93
  status: number;
83
94
  body: unknown;
@@ -112,6 +123,8 @@ export interface SelfTestAppOptions {
112
123
  requestBudgetMs?: number;
113
124
  /** Env override for secret collection + budget resolution (tests). */
114
125
  env?: Readonly<Record<string, string | undefined>>;
126
+ /** Structured server logger; defaults to the same JSON console shape as the provider server. */
127
+ logger?: (event: SelfTestCancellationLogEvent) => void;
115
128
  }
116
129
  /**
117
130
  * Stable sha256 over the provider's declared health plan (operations, case
@@ -132,8 +132,8 @@ export function isSelfTestReadOnlyOperation(operation) {
132
132
  }
133
133
  /** Binds the self-test executor to a tenant app's /v1 pipeline in-process. */
134
134
  export function createSelfTestInvoke(app) {
135
- return async ({ operationId, input, connection, requestId }) => {
136
- const response = await app.request(`/v1/${encodeURIComponent(operationId)}`, {
135
+ return async ({ operationId, input, connection, requestId, signal }) => {
136
+ const responsePromise = Promise.resolve(app.request(`/v1/${encodeURIComponent(operationId)}`, {
137
137
  method: "POST",
138
138
  headers: { "content-type": "application/json" },
139
139
  body: JSON.stringify({
@@ -141,7 +141,14 @@ export function createSelfTestInvoke(app) {
141
141
  input: input ?? {},
142
142
  ...(connection ? { connection } : {}),
143
143
  }),
144
- });
144
+ ...(signal ? { signal } : {}),
145
+ }));
146
+ const response = await responsePromise;
147
+ if (signal?.aborted) {
148
+ await cancelSelfTestResponse(response);
149
+ signal.throwIfAborted();
150
+ }
151
+ // An abort after body consumption starts may let response.text() finish.
145
152
  const text = await response.text();
146
153
  let body = text;
147
154
  try {
@@ -161,9 +168,36 @@ export function createSelfTestInvoke(app) {
161
168
  return { status: response.status, data: body };
162
169
  };
163
170
  }
171
+ async function cancelSelfTestResponse(response) {
172
+ await response.body?.cancel().catch(() => undefined);
173
+ }
174
+ function selfTestAbortReason(reason, sensitiveValues) {
175
+ const text = reason instanceof Error
176
+ ? reason.message || reason.name
177
+ : reason === undefined
178
+ ? "aborted"
179
+ : String(reason);
180
+ return redactSelfTestText(text, sensitiveValues);
181
+ }
182
+ function logSelfTestCancellation(provider, options, requestId, selected, signal, sensitiveValues) {
183
+ const event = {
184
+ level: "info",
185
+ event: "self_test_run_cancelled",
186
+ providerId: provider.id,
187
+ requestId,
188
+ operationId: selected.operationId,
189
+ caseName: selected.healthCase.name,
190
+ reason: selfTestAbortReason(signal.reason, sensitiveValues),
191
+ };
192
+ if (options.logger) {
193
+ options.logger(event);
194
+ return;
195
+ }
196
+ console.log(JSON.stringify(event));
197
+ }
164
198
  /** Binds the self-test auth-flow driver to a tenant app's /auth pipeline in-process. */
165
199
  export function createSelfTestAuthFlowInvoke(app) {
166
- return async ({ route, requestId, flowId, connectionId, externalRef, input, context }) => {
200
+ return async ({ route, requestId, flowId, connectionId, externalRef, input, context, signal, }) => {
167
201
  const response = await app.request(`/auth/${route}`, {
168
202
  method: "POST",
169
203
  headers: { "content-type": "application/json" },
@@ -175,6 +209,7 @@ export function createSelfTestAuthFlowInvoke(app) {
175
209
  ...(input ? { input } : {}),
176
210
  ...(context ? { context } : {}),
177
211
  }),
212
+ ...(signal ? { signal } : {}),
178
213
  });
179
214
  const text = await response.text();
180
215
  let body = text;
@@ -193,19 +228,32 @@ class SelfTestCaseTimeoutError extends Error {
193
228
  this.name = "SelfTestCaseTimeoutError";
194
229
  }
195
230
  }
196
- async function withCaseTimeout(run, timeoutMs) {
231
+ async function withCaseTimeout(run, timeoutMs, controller) {
197
232
  let timer;
233
+ let onAbort;
198
234
  try {
199
235
  return await Promise.race([
200
236
  run(),
201
237
  new Promise((_, reject) => {
202
- timer = setTimeout(() => reject(new SelfTestCaseTimeoutError(timeoutMs)), timeoutMs);
238
+ timer = setTimeout(() => {
239
+ const error = new SelfTestCaseTimeoutError(timeoutMs);
240
+ controller.abort(error);
241
+ reject(error);
242
+ }, timeoutMs);
243
+ }),
244
+ new Promise((_, reject) => {
245
+ onAbort = () => reject(controller.signal.reason);
246
+ controller.signal.addEventListener("abort", onAbort, { once: true });
247
+ if (controller.signal.aborted)
248
+ onAbort();
203
249
  }),
204
250
  ]);
205
251
  }
206
252
  finally {
207
253
  if (timer !== undefined)
208
254
  clearTimeout(timer);
255
+ if (onAbort)
256
+ controller.signal.removeEventListener("abort", onAbort);
209
257
  }
210
258
  }
211
259
  function objectProperty(value, key) {
@@ -389,6 +437,7 @@ async function materializeFlowCredential(execution, inputs, options = {}) {
389
437
  flowId,
390
438
  ...(options.connectionId ? { connectionId: options.connectionId } : {}),
391
439
  ...(options.externalRef ? { externalRef: options.externalRef } : {}),
440
+ ...(options.signal ? { signal: options.signal } : {}),
392
441
  }));
393
442
  if (!started.ok) {
394
443
  return { kind: "flow_error", code: started.code, message: started.message };
@@ -460,6 +509,7 @@ async function materializeFlowCredential(execution, inputs, options = {}) {
460
509
  ...(options.externalRef ? { externalRef: options.externalRef } : {}),
461
510
  input: submitInputs,
462
511
  ...(Object.keys(flowContext).length > 0 ? { context: flowContext } : {}),
512
+ ...(options.signal ? { signal: options.signal } : {}),
463
513
  }));
464
514
  if (!continued.ok) {
465
515
  // Providers built with defineCredentialsAuth cannot return a retry
@@ -613,6 +663,7 @@ async function resolveSelfTestConnection(execution, operationId, suite, options
613
663
  ...(options.isAbandoned !== undefined ? { isAbandoned: options.isAbandoned } : {}),
614
664
  connectionId,
615
665
  externalRef: `${execution.provider.id}-${operationId}-self-test`,
666
+ ...(options.signal ? { signal: options.signal } : {}),
616
667
  });
617
668
  if (!("credential" in materialized)) {
618
669
  // Only the multi-turn SKIP is negative-cached. Flow ERRORS
@@ -632,10 +683,9 @@ async function resolveSelfTestConnection(execution, operationId, suite, options
632
683
  }
633
684
  return materialized;
634
685
  }
635
- // A flow that outlived the case deadline still completes here (the timeout
636
- // only races the promise, it cannot cancel it). The case already reported
637
- // self_test_timeout caching this credential would let the next probe
638
- // reuse a login whose latency just failed the case, hiding the failure.
686
+ // Abort is cooperative: a flow may still complete after the case deadline
687
+ // if provider code ignores the signal. Never cache that late credential or
688
+ // let the next probe hide the timed-out login.
639
689
  if (options.isAbandoned?.() === true) {
640
690
  return {
641
691
  kind: "flow_error",
@@ -659,7 +709,7 @@ async function resolveSelfTestConnection(execution, operationId, suite, options
659
709
  function isAuthFailureCaseResult(result) {
660
710
  return result.status === "failed" && (result.httpStatus === 401 || result.httpStatus === 403);
661
711
  }
662
- async function executeSelfTestCase(execution, operationId, suite, healthCase) {
712
+ async function executeSelfTestCase(execution, operationId, suite, healthCase, caseController) {
663
713
  const { provider, invoke } = execution;
664
714
  // execution.sensitiveValues may grow while the case runs (flow-issued
665
715
  // secrets); redact always reads the live array.
@@ -671,6 +721,7 @@ async function executeSelfTestCase(execution, operationId, suite, healthCase) {
671
721
  // a 30s case must never take ~4×30s across its stages.
672
722
  const caseDeadlineAtMs = performance.now() + timeoutMs;
673
723
  const remainingCaseTimeoutMs = () => Math.max(1, Math.ceil(caseDeadlineAtMs - performance.now()));
724
+ const runWithCaseTimeout = (run, remainingMs) => withCaseTimeout(run, remainingMs, caseController);
674
725
  const beginCase = () => {
675
726
  const startedAt = new Date().toISOString();
676
727
  const startedAtMs = performance.now();
@@ -695,14 +746,15 @@ async function executeSelfTestCase(execution, operationId, suite, healthCase) {
695
746
  });
696
747
  }
697
748
  const resolveConnection = async (forceLogin) => {
698
- // The timeout only races the flow promise it cannot cancel it. Once
699
- // the deadline fires, the still-running resolution is marked abandoned
700
- // so its late completion cannot write the session cache.
749
+ // The deadline aborts the in-process flow request. Keep the abandonment
750
+ // guard as a backstop for provider code that ignores cancellation so a
751
+ // late completion still cannot write the session cache.
701
752
  let abandoned = false;
702
753
  try {
703
- return await withCaseTimeout(() => resolveSelfTestConnection(execution, operationId, suite, {
754
+ return await runWithCaseTimeout(() => resolveSelfTestConnection(execution, operationId, suite, {
704
755
  forceLogin,
705
756
  isAbandoned: () => abandoned,
757
+ signal: caseController.signal,
706
758
  }), remainingCaseTimeoutMs());
707
759
  }
708
760
  catch (error) {
@@ -739,7 +791,7 @@ async function executeSelfTestCase(execution, operationId, suite, healthCase) {
739
791
  // operation attempt, or a slow login reads as a fast healthy case.
740
792
  const { startedAtMs, finish } = caseScope;
741
793
  try {
742
- return await withCaseTimeout(async () => {
794
+ return await runWithCaseTimeout(async () => {
743
795
  const resolvedInput = resolveHealthCheckInputDateTokens(healthCase.input);
744
796
  const preparedInput = healthCase.prepareInput
745
797
  ? await healthCase.prepareInput({
@@ -758,6 +810,7 @@ async function executeSelfTestCase(execution, operationId, suite, healthCase) {
758
810
  input: gatewayInput,
759
811
  connection,
760
812
  requestId: `${execution.requestId}-prepare-${randomUUID()}`,
813
+ signal: caseController.signal,
761
814
  });
762
815
  if (executed.status === 401 || executed.status === 403) {
763
816
  prepareAuthStatus = executed.status;
@@ -777,6 +830,7 @@ async function executeSelfTestCase(execution, operationId, suite, healthCase) {
777
830
  input: preparedInput,
778
831
  connection,
779
832
  requestId: `${execution.requestId}-${randomUUID()}`,
833
+ signal: caseController.signal,
780
834
  });
781
835
  const durationMs = performance.now() - startedAtMs;
782
836
  if (executed.status < 200 || executed.status >= 300) {
@@ -1054,41 +1108,65 @@ export function createSelfTestApp(provider, options) {
1054
1108
  };
1055
1109
  const deadline = performance.now() + requestBudgetMs;
1056
1110
  const results = [];
1111
+ const runSignal = c.req.raw.signal;
1112
+ let interruptedCase;
1057
1113
  // Sequential execution (parallelism 1): self-tests run on serving pods
1058
1114
  // and must never compete with themselves for upstream quota.
1059
1115
  for (const selected of selection.cases) {
1060
- if (performance.now() >= deadline) {
1061
- const now = new Date().toISOString();
1062
- results.push({
1063
- operationId: selected.operationId,
1064
- caseName: selected.healthCase.name,
1065
- status: "skipped",
1066
- label: selected.healthCase.name,
1067
- responseTimeMs: 0,
1068
- skipReason: "budget_exhausted",
1069
- startedAt: now,
1070
- finishedAt: now,
1071
- });
1072
- continue;
1116
+ const caseController = new AbortController();
1117
+ const abortFromRun = () => caseController.abort(runSignal.reason);
1118
+ runSignal.addEventListener("abort", abortFromRun, { once: true });
1119
+ if (runSignal.aborted)
1120
+ abortFromRun();
1121
+ try {
1122
+ if (runSignal.aborted) {
1123
+ interruptedCase = selected;
1124
+ break;
1125
+ }
1126
+ if (performance.now() >= deadline) {
1127
+ const now = new Date().toISOString();
1128
+ results.push({
1129
+ operationId: selected.operationId,
1130
+ caseName: selected.healthCase.name,
1131
+ status: "skipped",
1132
+ label: selected.healthCase.name,
1133
+ responseTimeMs: 0,
1134
+ skipReason: "budget_exhausted",
1135
+ startedAt: now,
1136
+ finishedAt: now,
1137
+ });
1138
+ continue;
1139
+ }
1140
+ if (!isSelfTestReadOnlyOperation(selected.operation)) {
1141
+ const now = new Date().toISOString();
1142
+ results.push({
1143
+ operationId: selected.operationId,
1144
+ caseName: selected.healthCase.name,
1145
+ status: "error",
1146
+ label: selected.healthCase.name,
1147
+ responseTimeMs: 0,
1148
+ error: {
1149
+ code: "operation_not_read_only",
1150
+ message: `Operation "${selected.operationId}" is not classified read-only; self-test refuses to execute it.`,
1151
+ },
1152
+ startedAt: now,
1153
+ finishedAt: now,
1154
+ });
1155
+ continue;
1156
+ }
1157
+ results.push(await executeSelfTestCase(execution, selected.operationId, selected.suite, selected.healthCase, caseController));
1158
+ if (runSignal.aborted) {
1159
+ interruptedCase = selected;
1160
+ break;
1161
+ }
1073
1162
  }
1074
- if (!isSelfTestReadOnlyOperation(selected.operation)) {
1075
- const now = new Date().toISOString();
1076
- results.push({
1077
- operationId: selected.operationId,
1078
- caseName: selected.healthCase.name,
1079
- status: "error",
1080
- label: selected.healthCase.name,
1081
- responseTimeMs: 0,
1082
- error: {
1083
- code: "operation_not_read_only",
1084
- message: `Operation "${selected.operationId}" is not classified read-only; self-test refuses to execute it.`,
1085
- },
1086
- startedAt: now,
1087
- finishedAt: now,
1088
- });
1089
- continue;
1163
+ finally {
1164
+ runSignal.removeEventListener("abort", abortFromRun);
1165
+ caseController.abort();
1090
1166
  }
1091
- results.push(await executeSelfTestCase(execution, selected.operationId, selected.suite, selected.healthCase));
1167
+ }
1168
+ if (interruptedCase) {
1169
+ logSelfTestCancellation(provider, options, request.requestId, interruptedCase, runSignal, execution.sensitiveValues);
1092
1170
  }
1093
1171
  const singleCase = request.operationId !== undefined && request.caseName !== undefined;
1094
1172
  const response = {
@@ -0,0 +1,199 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import { type ProviderErrorCategory } from "../observability.js";
4
+ import type { OcrContext, ProviderContext, ProviderDefinition, ProviderRuntimeState, ResolverContext, SttContext } from "../types.js";
5
+ import type { SelfTestCancellationLogEvent } from "./self-test.js";
6
+ import { type OperationRequest } from "./types.js";
7
+ /** Compact SDK-owned error classification emitted separately from the public response body. */
8
+ export declare const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
9
+ export type ErrorObservabilityDetails = {
10
+ category: ProviderErrorCategory;
11
+ taxonomyVersion: string;
12
+ retryable: boolean;
13
+ upstreamStatus?: number;
14
+ };
15
+ export declare const ProviderServerStatefulForwardEnvelopeSchema: z.ZodObject<{
16
+ requestId: z.ZodString;
17
+ providerId: z.ZodString;
18
+ operationId: z.ZodString;
19
+ sessionKey: z.ZodString;
20
+ connectionId: z.ZodString;
21
+ serviceAccountId: z.ZodString;
22
+ ownerPodId: z.ZodString;
23
+ generation: z.ZodNumber;
24
+ sourcePodId: z.ZodString;
25
+ forwardedAt: z.ZodString;
26
+ deadlineAt: z.ZodOptional<z.ZodString>;
27
+ idempotencyKey: z.ZodOptional<z.ZodString>;
28
+ operationRequest: z.ZodObject<{
29
+ requestId: z.ZodString;
30
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
31
+ connectionId: z.ZodOptional<z.ZodString>;
32
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
33
+ trace: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
34
+ connection: z.ZodOptional<z.ZodObject<{
35
+ id: z.ZodString;
36
+ mode: z.ZodEnum<{
37
+ none: "none";
38
+ credentials: "credentials";
39
+ oauth2: "oauth2";
40
+ "platform-managed": "platform-managed";
41
+ oauth2_proxied: "oauth2_proxied";
42
+ }>;
43
+ secrets: z.ZodRecord<z.ZodString, z.ZodString>;
44
+ scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
45
+ metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
46
+ externalRef: z.ZodString;
47
+ }, z.core.$strict>>;
48
+ }, z.core.$strict>;
49
+ }, z.core.$strict>;
50
+ export type ProviderServerStatefulForwardEnvelope = Readonly<z.infer<typeof ProviderServerStatefulForwardEnvelopeSchema>>;
51
+ export type ProviderServerStatefulOwnerFence = Readonly<Pick<ProviderServerStatefulForwardEnvelope, "providerId" | "sessionKey" | "ownerPodId" | "generation" | "sourcePodId" | "forwardedAt" | "requestId" | "idempotencyKey">>;
52
+ export type ProviderServerStatefulOwnerFenceValidator = (fence: ProviderServerStatefulOwnerFence, signal: AbortSignal) => boolean | Promise<boolean>;
53
+ export type ProviderServerOperationExecutorInput = {
54
+ readonly provider: ProviderDefinition;
55
+ readonly operationId: string;
56
+ readonly ctx: ProviderContext;
57
+ readonly request: OperationRequest & {
58
+ readonly deadlineAt?: string;
59
+ };
60
+ readonly signal?: AbortSignal;
61
+ readonly internalStatefulForward?: ProviderServerStatefulForwardEnvelope;
62
+ };
63
+ export type ProviderServerOperationExecutor = (input: ProviderServerOperationExecutorInput) => Promise<unknown>;
64
+ export declare function resolveProviderProxyAffinityKey(provider: ProviderDefinition, request: OperationRequest, operationId: string): string;
65
+ export declare function resolveProviderResolverIdentityScope(provider: ProviderDefinition, affinityKey: string, contextId: string): string;
66
+ type ProviderRequestCost = {
67
+ durationMs: number;
68
+ cpuUserMicros: number;
69
+ cpuSystemMicros: number;
70
+ cpuTotalMicros: number;
71
+ };
72
+ type ProviderServerLogEventBase = ProviderRequestCost & {
73
+ providerId: string;
74
+ kind: "operation" | "auth";
75
+ route: string;
76
+ requestId?: string;
77
+ status: number;
78
+ };
79
+ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
80
+ level: "info";
81
+ event: "provider_request_completed";
82
+ }) | (ProviderServerLogEventBase & {
83
+ level: "warn" | "error";
84
+ event: "provider_request_failed";
85
+ code: string;
86
+ errorClass: string;
87
+ message: string;
88
+ upstreamStatus?: number;
89
+ errorCategory?: ProviderErrorCategory;
90
+ taxonomyVersion?: string;
91
+ retryable?: boolean;
92
+ signal?: "unregistered_provider_error_code";
93
+ signalFix?: string;
94
+ issues?: Array<{
95
+ path: string;
96
+ code: string;
97
+ message: string;
98
+ }>;
99
+ }) | {
100
+ level: "warn";
101
+ event: "provider_secrets_missing";
102
+ providerId: string;
103
+ missingSecrets: string[];
104
+ } | {
105
+ level: "warn";
106
+ event: "provider_cleanup_failed";
107
+ providerId: string;
108
+ kind: "operation" | "auth";
109
+ route: string;
110
+ requestId?: string;
111
+ resource: "browser" | "stealth";
112
+ errorClass: string;
113
+ message: string;
114
+ } | {
115
+ level: "error";
116
+ event: "provider_shutdown_hook_failed";
117
+ providerId: string;
118
+ hookIndex: number;
119
+ errorClass: string;
120
+ message: string;
121
+ } | SelfTestCancellationLogEvent;
122
+ export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
123
+ export type ProviderServerOptions = {
124
+ logger?: ProviderServerLogger;
125
+ /** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
126
+ operationExecutor?: ProviderServerOperationExecutor;
127
+ /** Optional signed internal executor for stateful owner forwarding. */
128
+ internalOperationExecutor?: ProviderServerOperationExecutor;
129
+ statefulForwarding?: {
130
+ readonly secret: string;
131
+ readonly maxSkewMs?: number;
132
+ readonly replayCacheMaxEntries?: number;
133
+ /** Required fail-closed check against the SDK/runtime owner registry. */
134
+ readonly validateOwnerFence: ProviderServerStatefulOwnerFenceValidator;
135
+ };
136
+ /** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
137
+ stt?: SttContext;
138
+ /** Optional OCR override for tests or custom hosts; local/prod normally resolves from env. */
139
+ ocr?: OcrContext;
140
+ /** Optional resolver override for tests or custom hosts; local/prod normally resolves from env. */
141
+ resolver?: ResolverContext;
142
+ /** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
143
+ state?: ProviderRuntimeState;
144
+ /** Allow process-local runtime state only for local development and tests. */
145
+ allowMemoryStateFallback?: boolean;
146
+ /**
147
+ * Graceful process shutdown. Hooks run in declaration order after listeners stop accepting work.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * await serve(provider, {
152
+ * shutdown: {
153
+ * hooks: [
154
+ * async () => { await emitter.flush(); },
155
+ * async () => { await sessionManager.closeAll("server-shutdown"); },
156
+ * async () => { await lease.release(); },
157
+ * async () => { await router.close(); },
158
+ * ],
159
+ * },
160
+ * });
161
+ * ```
162
+ */
163
+ shutdown?: {
164
+ readonly hooks?: Array<() => Promise<void>>;
165
+ readonly signals?: boolean | NodeJS.Signals[];
166
+ readonly timeoutMs?: number;
167
+ };
168
+ };
169
+ /**
170
+ * Primary, cross-runtime app factory. Declared capability ESM is preloaded
171
+ * asynchronously, so this path works on Bun and every supported Node release.
172
+ */
173
+ export declare function createServerAppAsync(provider: ProviderDefinition, options?: ProviderServerOptions): Promise<Hono>;
174
+ /**
175
+ * Synchronous compatibility factory. Standard providers remain synchronous on
176
+ * every runtime because they load no capability modules. Providers declaring a
177
+ * capability require Bun or Node >=22.12; older Node releases receive an
178
+ * actionable error directing them to createServerAppAsync().
179
+ */
180
+ export declare function createServerApp(provider: ProviderDefinition, options?: ProviderServerOptions): Hono;
181
+ export type ProviderServerCloseOptions = {
182
+ readonly timeoutMs?: number;
183
+ };
184
+ export type ProviderServerHandle = {
185
+ readonly port: number;
186
+ close(options?: ProviderServerCloseOptions): Promise<void>;
187
+ };
188
+ export interface ServeOptions extends ProviderServerOptions {
189
+ host?: string;
190
+ port?: number;
191
+ /**
192
+ * Port for the internal self-test listener (default 3001 or
193
+ * APIFUSE__PROVIDER_RUNTIME__SELF_TEST_PORT). The listener only starts
194
+ * when APIFUSE__PROVIDER_RUNTIME__SELF_TEST_MASTER_SECRET is present.
195
+ */
196
+ selfTestPort?: number;
197
+ }
198
+ export declare function serve(provider: ProviderDefinition, options?: ServeOptions): Promise<ProviderServerHandle>;
199
+ export {};