@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.
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +2 -2
- package/bin/apifuse-pack-smoke.ts +1 -1
- package/bin/apifuse-pack-types.ts +2 -1
- package/bin/apifuse-perf.ts +2 -4
- package/bin/apifuse-record.ts +1 -1
- package/dist/auth-turn/index.d.ts +1 -1
- package/dist/auth-turn/index.js +1 -1
- package/dist/ceremonies/index.js +52 -11
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +4 -2
- package/dist/contract-serialization.d.ts +1 -20
- package/dist/contract-serialization.js +8 -587
- package/dist/contract.d.ts +0 -2
- package/dist/contract.js +5 -9
- package/dist/index.d.ts +9 -8
- package/dist/index.js +6 -8
- package/dist/provider.d.ts +3 -2
- package/dist/provider.js +2 -2
- package/dist/runtime/auth-flow.js +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +135 -12
- package/dist/runtime/instrumentation.js +1 -1
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +2 -33
- package/dist/runtime/native-network.js +2 -68
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
- package/dist/runtime/resolver-vendors/twocaptcha.js +80 -43
- package/dist/runtime/resolver-vendors/types.d.ts +1 -1
- package/dist/runtime/resolver.d.ts +6 -10
- package/dist/runtime/resolver.js +19 -18
- package/dist/runtime/state.js +5 -115
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.js +7 -130
- package/dist/schema.d.ts +0 -63
- package/dist/schema.js +8 -808
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/self-test.d.ts +13 -0
- package/dist/server/self-test.js +124 -46
- package/dist/server/serve-implementation.d.ts +199 -0
- package/dist/server/serve-implementation.js +2054 -0
- package/dist/server/serve.d.ts +1 -187
- package/dist/server/serve.js +1 -1827
- package/dist/stateful/errors.d.ts +5 -0
- package/dist/stateful/errors.js +10 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
- package/dist/stateful/stateful-provider-session-routing.js +2 -10
- package/dist/stream.js +7 -1
- package/package.json +27 -2
- package/src/auth-turn/index.ts +1 -1
- package/src/ceremonies/index.ts +68 -18
- package/src/config/loader.ts +5 -2
- package/src/contract-serialization.ts +8 -859
- package/src/contract.ts +5 -16
- package/src/index.ts +18 -34
- package/src/provider.ts +12 -24
- package/src/runtime/auth-flow.ts +1 -1
- package/src/runtime/http.ts +155 -11
- package/src/runtime/instrumentation.ts +1 -1
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +16 -97
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +100 -49
- package/src/runtime/resolver-vendors/types.ts +1 -0
- package/src/runtime/resolver.ts +47 -22
- package/src/runtime/state.ts +5 -144
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +14 -157
- package/src/schema.ts +9 -1060
- package/src/serve.ts +6 -1
- package/src/server/index.ts +1 -0
- package/src/server/self-test.ts +184 -59
- package/src/server/serve-implementation.ts +3024 -0
- package/src/server/serve.ts +1 -2661
- package/src/stateful/errors.ts +12 -0
- package/src/stateful/stateful-provider-session-routing.ts +2 -11
- 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";
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
package/dist/server/self-test.js
CHANGED
|
@@ -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
|
|
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(() =>
|
|
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
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
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
|
|
699
|
-
//
|
|
700
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
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 {};
|