@apifuse/provider-sdk 2.2.0-beta.21 → 2.2.0-beta.23
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 +12 -0
- package/bin/apifuse-dev.ts +2 -0
- package/bin/apifuse-record.ts +2 -0
- package/dist/contract-types.d.ts +1 -0
- package/dist/contract.js +2 -0
- package/dist/define.d.ts +2 -1
- package/dist/define.js +15 -0
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +32 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/lint.d.ts +5 -0
- package/dist/lint.js +277 -0
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/browser.js +28 -9
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/server/serve.d.ts +3 -1
- package/dist/server/serve.js +12 -29
- package/dist/testing/run.js +3 -0
- package/dist/types.d.ts +51 -0
- package/package.json +1 -1
- package/src/contract-types.ts +1 -0
- package/src/contract.ts +2 -0
- package/src/define.ts +17 -0
- package/src/error-resolution.ts +36 -0
- package/src/index.ts +25 -0
- package/src/lint.ts +303 -0
- package/src/runtime/auth-flow.ts +4 -0
- package/src/runtime/browser.ts +70 -19
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/server/serve.ts +15 -28
- package/src/testing/run.ts +7 -0
- package/src/types.ts +58 -0
package/dist/types.d.ts
CHANGED
|
@@ -196,6 +196,54 @@ export interface SmsOtpMatcherDefinition {
|
|
|
196
196
|
/** Runtime/fixture helper. Not serialized into generated registry artifacts. */
|
|
197
197
|
extractOtp(body: string): string | null;
|
|
198
198
|
}
|
|
199
|
+
export interface ProviderOcrConfig {
|
|
200
|
+
readonly mode: "required" | "optional";
|
|
201
|
+
}
|
|
202
|
+
export type OcrImageInput = {
|
|
203
|
+
readonly kind: "base64";
|
|
204
|
+
readonly data: string;
|
|
205
|
+
readonly mediaType?: string;
|
|
206
|
+
} | {
|
|
207
|
+
readonly kind: "url";
|
|
208
|
+
readonly url: string;
|
|
209
|
+
};
|
|
210
|
+
export interface OcrRecognizeRequest {
|
|
211
|
+
readonly image: OcrImageInput;
|
|
212
|
+
readonly hint?: "captcha" | "document" | "generic";
|
|
213
|
+
readonly prompt?: string;
|
|
214
|
+
readonly maxTokens?: number;
|
|
215
|
+
readonly timeoutMs?: number;
|
|
216
|
+
}
|
|
217
|
+
export interface OcrWarning {
|
|
218
|
+
readonly code: string;
|
|
219
|
+
readonly message: string;
|
|
220
|
+
}
|
|
221
|
+
export interface OcrResult {
|
|
222
|
+
readonly text: string;
|
|
223
|
+
readonly model: string;
|
|
224
|
+
readonly warnings?: readonly OcrWarning[];
|
|
225
|
+
}
|
|
226
|
+
export interface OcrCaptchaOptions {
|
|
227
|
+
readonly length?: number;
|
|
228
|
+
/** Allowed characters. A RegExp is applied to each character, not to the whole text. */
|
|
229
|
+
readonly charset?: string | RegExp;
|
|
230
|
+
readonly caseSensitive?: boolean;
|
|
231
|
+
readonly maxCandidates?: number;
|
|
232
|
+
}
|
|
233
|
+
export interface OcrCaptchaCandidate {
|
|
234
|
+
readonly text: string;
|
|
235
|
+
readonly satisfiesConstraints: boolean;
|
|
236
|
+
}
|
|
237
|
+
export interface OcrCaptchaResult {
|
|
238
|
+
readonly text: string;
|
|
239
|
+
readonly candidates: readonly OcrCaptchaCandidate[];
|
|
240
|
+
readonly satisfiesConstraints: boolean;
|
|
241
|
+
readonly model: string;
|
|
242
|
+
}
|
|
243
|
+
export interface OcrContext {
|
|
244
|
+
recognize(request: OcrRecognizeRequest): Promise<OcrResult>;
|
|
245
|
+
extractCaptchaText(image: OcrImageInput, options?: OcrCaptchaOptions): Promise<OcrCaptchaResult>;
|
|
246
|
+
}
|
|
199
247
|
export type SttTranscribeMode = "general" | "otp";
|
|
200
248
|
export type SttPromptPolicy = "none" | "default-hint" | "custom-hint";
|
|
201
249
|
export type SttUnsupportedOptionPolicy = "warn" | "error";
|
|
@@ -1543,6 +1591,7 @@ export interface FlowContext {
|
|
|
1543
1591
|
env: EnvContext;
|
|
1544
1592
|
credential?: CredentialContext;
|
|
1545
1593
|
context: ContextScratchpad;
|
|
1594
|
+
ocr: OcrContext;
|
|
1546
1595
|
stt: SttContext;
|
|
1547
1596
|
auth: AuthFlowTerminalContext;
|
|
1548
1597
|
}
|
|
@@ -1634,6 +1683,7 @@ export interface ProviderContext {
|
|
|
1634
1683
|
browser: BrowserClient;
|
|
1635
1684
|
trace: TraceContext;
|
|
1636
1685
|
auth: AuthContext;
|
|
1686
|
+
ocr: OcrContext;
|
|
1637
1687
|
stt: SttContext;
|
|
1638
1688
|
choice: ProviderChoiceContext;
|
|
1639
1689
|
}
|
|
@@ -1785,6 +1835,7 @@ export interface ProviderDefinition {
|
|
|
1785
1835
|
platform: StealthPlatform;
|
|
1786
1836
|
};
|
|
1787
1837
|
proxy?: ProviderProxyConfig;
|
|
1838
|
+
ocr?: ProviderOcrConfig;
|
|
1788
1839
|
stt?: ProviderSttConfig;
|
|
1789
1840
|
browser?: {
|
|
1790
1841
|
engine: BrowserEngine;
|
package/package.json
CHANGED
package/src/contract-types.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface ProviderContractSnapshot {
|
|
|
13
13
|
readonly allowedHosts?: readonly string[];
|
|
14
14
|
readonly stealth?: JsonValue;
|
|
15
15
|
readonly proxy?: JsonValue;
|
|
16
|
+
readonly ocr?: JsonValue;
|
|
16
17
|
readonly stt?: JsonValue;
|
|
17
18
|
readonly browser?: JsonValue;
|
|
18
19
|
readonly auth?: JsonValue;
|
package/src/contract.ts
CHANGED
|
@@ -35,6 +35,7 @@ export function extractProviderContract(provider: ProviderDefinition): ProviderC
|
|
|
35
35
|
const auth = extractAuth(provider.auth);
|
|
36
36
|
const stealth = toJsonValue(provider.stealth);
|
|
37
37
|
const proxy = toJsonValue(provider.proxy);
|
|
38
|
+
const ocr = toJsonValue(provider.ocr);
|
|
38
39
|
const stt = toJsonValue(provider.stt);
|
|
39
40
|
const browser = toJsonValue(provider.browser);
|
|
40
41
|
const reviewed = toJsonValue(provider.reviewed);
|
|
@@ -59,6 +60,7 @@ export function extractProviderContract(provider: ProviderDefinition): ProviderC
|
|
|
59
60
|
...(provider.allowedHosts ? { allowedHosts: [...provider.allowedHosts].sort() } : {}),
|
|
60
61
|
...(stealth === undefined ? {} : { stealth }),
|
|
61
62
|
...(proxy === undefined ? {} : { proxy }),
|
|
63
|
+
...(ocr === undefined ? {} : { ocr }),
|
|
62
64
|
...(stt === undefined ? {} : { stt }),
|
|
63
65
|
...(browser === undefined ? {} : { browser }),
|
|
64
66
|
...(auth === undefined ? {} : { auth }),
|
package/src/define.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
OperationTransport,
|
|
28
28
|
OperationWebSocketTransport,
|
|
29
29
|
NativeProviderConfig,
|
|
30
|
+
ProviderOcrConfig,
|
|
30
31
|
ProviderAccessConfig,
|
|
31
32
|
ProviderDefinition,
|
|
32
33
|
ProviderDeploymentOverrides,
|
|
@@ -123,6 +124,7 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
123
124
|
"auth-flow",
|
|
124
125
|
"connection",
|
|
125
126
|
] as const;
|
|
127
|
+
const VALID_PROVIDER_OCR_MODES = ["optional", "required"] as const;
|
|
126
128
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
|
|
127
129
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
128
130
|
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
@@ -248,6 +250,7 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
|
|
|
248
250
|
platform: StealthPlatform;
|
|
249
251
|
};
|
|
250
252
|
proxy?: ProviderProxyConfig;
|
|
253
|
+
ocr?: ProviderOcrConfig;
|
|
251
254
|
stt?: ProviderSttConfig;
|
|
252
255
|
browser?: { engine: BrowserEngine };
|
|
253
256
|
auth?: AuthConfig;
|
|
@@ -697,6 +700,18 @@ function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): v
|
|
|
697
700
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
698
701
|
}
|
|
699
702
|
|
|
703
|
+
function validateProviderOcr(config: { id: string; ocr?: ProviderOcrConfig }): void {
|
|
704
|
+
const ocr = config.ocr;
|
|
705
|
+
if (ocr === undefined) return;
|
|
706
|
+
if (!ocr || typeof ocr !== "object" || Array.isArray(ocr)) {
|
|
707
|
+
throw new ValidationError(`Provider "${config.id}" has invalid ocr: must be an object.`, {
|
|
708
|
+
fix: `Use ocr: { mode: "required" } or ocr: { mode: "optional" }.`,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
rejectUnknownFields(ocr, new Set(["mode"]), "ocr");
|
|
712
|
+
assertLiteralField(ocr.mode, "ocr.mode", VALID_PROVIDER_OCR_MODES, config.id);
|
|
713
|
+
}
|
|
714
|
+
|
|
700
715
|
function validateOperationIds(
|
|
701
716
|
providerId: string,
|
|
702
717
|
operations: Record<string, ProviderOperation>,
|
|
@@ -2386,6 +2401,7 @@ export function defineProvider<
|
|
|
2386
2401
|
throw error;
|
|
2387
2402
|
}
|
|
2388
2403
|
validateProviderProxy(config);
|
|
2404
|
+
validateProviderOcr(config);
|
|
2389
2405
|
validateProviderStt(config);
|
|
2390
2406
|
if (config.runtime === "browser" && !config.browser)
|
|
2391
2407
|
throw new ProviderError(
|
|
@@ -2410,6 +2426,7 @@ export function defineProvider<
|
|
|
2410
2426
|
native: config.native,
|
|
2411
2427
|
stealth: config.stealth,
|
|
2412
2428
|
proxy: config.proxy,
|
|
2429
|
+
ocr: config.ocr,
|
|
2413
2430
|
stt: config.stt,
|
|
2414
2431
|
browser: config.browser,
|
|
2415
2432
|
auth: config.auth,
|
package/src/error-resolution.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ProviderErrorStatus } from "./types.js";
|
|
2
|
+
|
|
1
3
|
// This set suppresses the unregistered-provider-error-code signal for codes
|
|
2
4
|
// intentionally emitted by SDK paths. It is not the complete authority for
|
|
3
5
|
// runtime error resolution: branded errors and additional canonical SDK codes
|
|
@@ -28,6 +30,7 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
|
28
30
|
"flow_expired",
|
|
29
31
|
"turn_validation_error",
|
|
30
32
|
"context_access_error",
|
|
33
|
+
"OCR_UPSTREAM_FAILED",
|
|
31
34
|
"UNSUPPORTED_STT_OPTION",
|
|
32
35
|
"INVALID_STT_AUDIO",
|
|
33
36
|
"STT_AUDIO_TOO_LARGE",
|
|
@@ -83,9 +86,42 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
|
83
86
|
export const SDK_RUNTIME_OWNED_ERROR_CODES = new Set([
|
|
84
87
|
...SDK_OWNED_PROVIDER_ERROR_CODES,
|
|
85
88
|
"reauth_required",
|
|
89
|
+
"OCR_UNAVAILABLE",
|
|
90
|
+
"UNSUPPORTED_OCR_BACKEND",
|
|
86
91
|
"STT_UNAVAILABLE",
|
|
87
92
|
"UNSUPPORTED_STT_BACKEND",
|
|
88
93
|
"OUTPUT_VALIDATION_FAILED",
|
|
89
94
|
"NOT_FOUND",
|
|
90
95
|
"not_found",
|
|
91
96
|
]);
|
|
97
|
+
|
|
98
|
+
// Canonical SDK status mapping for recognized provider-thrown error codes.
|
|
99
|
+
// serve.ts toStatusCode consults this map (after operation-declared overrides
|
|
100
|
+
// for non-SDK-owned codes), and the authoring lint treats these codes as
|
|
101
|
+
// SDK-registered. Add new codes here instead of duplicating literals in
|
|
102
|
+
// either consumer.
|
|
103
|
+
export const SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES: ReadonlyMap<string, ProviderErrorStatus> =
|
|
104
|
+
new Map<string, ProviderErrorStatus>([
|
|
105
|
+
["AUTH_REQUIRED", 401],
|
|
106
|
+
["reauth_required", 401],
|
|
107
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
108
|
+
// upstream failure — explicit 400.
|
|
109
|
+
["MISSING_SECRET", 400],
|
|
110
|
+
["NOT_FOUND", 404],
|
|
111
|
+
["not_found", 404],
|
|
112
|
+
["NO_DATA", 404],
|
|
113
|
+
["RATE_LIMITED", 429],
|
|
114
|
+
["UPSTREAM_RATE_LIMIT", 429],
|
|
115
|
+
["LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR", 429],
|
|
116
|
+
// Deterministic upstream business refusal (honest-provider-error-
|
|
117
|
+
// contract): the upstream evaluated the request and said no under its
|
|
118
|
+
// own rules — a conflict with upstream state, never a 5xx.
|
|
119
|
+
["UPSTREAM_REJECTED", 409],
|
|
120
|
+
["UPSTREAM_ERROR", 502],
|
|
121
|
+
["BLOCKED", 502],
|
|
122
|
+
["OCR_UNAVAILABLE", 503],
|
|
123
|
+
["UNSUPPORTED_OCR_BACKEND", 503],
|
|
124
|
+
["STT_UNAVAILABLE", 503],
|
|
125
|
+
["UNSUPPORTED_STT_BACKEND", 503],
|
|
126
|
+
["STATEFUL_FORWARDING_REPLAY_CACHE_FULL", 503],
|
|
127
|
+
]);
|
package/src/index.ts
CHANGED
|
@@ -111,6 +111,22 @@ export {
|
|
|
111
111
|
UnsupportedProviderStateError,
|
|
112
112
|
} from "./runtime/state.js";
|
|
113
113
|
export { createStealthClient } from "./runtime/stealth.js";
|
|
114
|
+
export {
|
|
115
|
+
APIFUSE__OCR__API_KEY_ENV,
|
|
116
|
+
APIFUSE__OCR__BACKEND_ENV,
|
|
117
|
+
APIFUSE__OCR__BASE_URL_ENV,
|
|
118
|
+
APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV,
|
|
119
|
+
APIFUSE__OCR__MODEL_ENV,
|
|
120
|
+
CLOUDFLARE_ACCOUNT_ID_ENV,
|
|
121
|
+
CLOUDFLARE_WORKERS_AI_OCR_BACKEND,
|
|
122
|
+
createCloudflareWorkersAiOcrClient,
|
|
123
|
+
createOcrClientFromEnv,
|
|
124
|
+
createOpenAiCompatibleOcrClient,
|
|
125
|
+
createUnsupportedOcrClient,
|
|
126
|
+
DEFAULT_CLOUDFLARE_WORKERS_AI_OCR_MODEL,
|
|
127
|
+
extractCaptchaCandidates,
|
|
128
|
+
OPENAI_COMPATIBLE_OCR_BACKEND,
|
|
129
|
+
} from "./runtime/ocr.js";
|
|
114
130
|
export {
|
|
115
131
|
APIFUSE__STT__BACKEND_ENV,
|
|
116
132
|
APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV,
|
|
@@ -229,6 +245,14 @@ export type {
|
|
|
229
245
|
NativeTcpPortRange,
|
|
230
246
|
NativeTcpTlsMode,
|
|
231
247
|
NativeTlsConnectOptions,
|
|
248
|
+
OcrCaptchaCandidate,
|
|
249
|
+
OcrCaptchaOptions,
|
|
250
|
+
OcrCaptchaResult,
|
|
251
|
+
OcrContext,
|
|
252
|
+
OcrImageInput,
|
|
253
|
+
OcrRecognizeRequest,
|
|
254
|
+
OcrResult,
|
|
255
|
+
OcrWarning,
|
|
232
256
|
OperationAnnotations,
|
|
233
257
|
OperationApprovalPolicy,
|
|
234
258
|
OperationContractMetadata,
|
|
@@ -274,6 +298,7 @@ export type {
|
|
|
274
298
|
ProviderLogoProfile,
|
|
275
299
|
ProviderLogoSource,
|
|
276
300
|
ProviderMeta,
|
|
301
|
+
ProviderOcrConfig,
|
|
277
302
|
ProviderProxyConfig,
|
|
278
303
|
ProviderProxyMode,
|
|
279
304
|
ProviderProxyPolicy,
|
package/src/lint.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ZodType } from "zod";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
SDK_RUNTIME_OWNED_ERROR_CODES,
|
|
5
|
+
SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES,
|
|
6
|
+
} from "./error-resolution.js";
|
|
3
7
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
4
8
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
5
9
|
|
|
@@ -799,6 +803,303 @@ function lintSelfHostedBrowserPatterns(
|
|
|
799
803
|
return diagnostics;
|
|
800
804
|
}
|
|
801
805
|
|
|
806
|
+
const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(?:ProviderError|ValidationError)\s*\(/g;
|
|
807
|
+
|
|
808
|
+
const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Skips a string literal starting at `startIndex` (which must point at the
|
|
812
|
+
* opening quote). Returns the index of the closing quote, or -1 when the
|
|
813
|
+
* literal is unterminated. Template literals handle nested `${...}`
|
|
814
|
+
* expressions, including strings inside them.
|
|
815
|
+
*/
|
|
816
|
+
function skipStringLiteral(source: string, startIndex: number): number {
|
|
817
|
+
const quote = source[startIndex];
|
|
818
|
+
for (let index = startIndex + 1; index < source.length; index++) {
|
|
819
|
+
const char = source[index];
|
|
820
|
+
if (char === "\\") {
|
|
821
|
+
index++;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (quote === "`" && char === "$" && source[index + 1] === "{") {
|
|
825
|
+
index = skipTemplateExpression(source, index + 2);
|
|
826
|
+
if (index < 0) {
|
|
827
|
+
return -1;
|
|
828
|
+
}
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (char === quote) {
|
|
832
|
+
return index;
|
|
833
|
+
}
|
|
834
|
+
if (quote !== "`" && char === "\n") {
|
|
835
|
+
return -1;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return -1;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function skipTemplateExpression(source: string, startIndex: number): number {
|
|
842
|
+
let depth = 1;
|
|
843
|
+
for (let index = startIndex; index < source.length; index++) {
|
|
844
|
+
const char = source[index];
|
|
845
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
846
|
+
index = skipStringLiteral(source, index);
|
|
847
|
+
if (index < 0) {
|
|
848
|
+
return -1;
|
|
849
|
+
}
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (char === "{") {
|
|
853
|
+
depth++;
|
|
854
|
+
} else if (char === "}") {
|
|
855
|
+
depth--;
|
|
856
|
+
if (depth === 0) {
|
|
857
|
+
return index;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return -1;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Extracts the argument text of a call whose opening paren has already been
|
|
866
|
+
* consumed (`startIndex` points just past it). Returns undefined when the
|
|
867
|
+
* call never closes in this source, which the caller treats as "skip
|
|
868
|
+
* silently" — this scanner is conservative by design.
|
|
869
|
+
*/
|
|
870
|
+
function extractBalancedCallArguments(source: string, startIndex: number): string | undefined {
|
|
871
|
+
let depth = 1;
|
|
872
|
+
for (let index = startIndex; index < source.length; index++) {
|
|
873
|
+
const char = source[index];
|
|
874
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
875
|
+
index = skipStringLiteral(source, index);
|
|
876
|
+
if (index < 0) {
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (char === "/" && source[index + 1] === "/") {
|
|
882
|
+
const newline = source.indexOf("\n", index);
|
|
883
|
+
if (newline === -1) {
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
index = newline;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
890
|
+
const end = source.indexOf("*/", index + 2);
|
|
891
|
+
if (end === -1) {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
index = end + 1;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (char === "(") {
|
|
898
|
+
depth++;
|
|
899
|
+
} else if (char === ")") {
|
|
900
|
+
depth--;
|
|
901
|
+
if (depth === 0) {
|
|
902
|
+
return source.slice(startIndex, index);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Collects literal string values of top-level `code:` properties inside a
|
|
911
|
+
* ProviderError/ValidationError options object. Only plain `"..."` / `'...'`
|
|
912
|
+
* literals at options-object depth count; computed codes (identifiers,
|
|
913
|
+
* ternaries, template substitutions, concatenations, escapes) are skipped
|
|
914
|
+
* silently so the rule never guesses.
|
|
915
|
+
*/
|
|
916
|
+
function collectLiteralErrorCodeValues(args: string): string[] {
|
|
917
|
+
const codes: string[] = [];
|
|
918
|
+
let braceDepth = 0;
|
|
919
|
+
let parenDepth = 0;
|
|
920
|
+
let bracketDepth = 0;
|
|
921
|
+
let previousSignificantChar = "";
|
|
922
|
+
for (let index = 0; index < args.length; index++) {
|
|
923
|
+
const char = args[index] ?? "";
|
|
924
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
925
|
+
const end = skipStringLiteral(args, index);
|
|
926
|
+
if (end < 0) {
|
|
927
|
+
return codes;
|
|
928
|
+
}
|
|
929
|
+
index = end;
|
|
930
|
+
previousSignificantChar = char;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (char === "/" && args[index + 1] === "/") {
|
|
934
|
+
const newline = args.indexOf("\n", index);
|
|
935
|
+
if (newline === -1) {
|
|
936
|
+
return codes;
|
|
937
|
+
}
|
|
938
|
+
index = newline;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (char === "/" && args[index + 1] === "*") {
|
|
942
|
+
const end = args.indexOf("*/", index + 2);
|
|
943
|
+
if (end === -1) {
|
|
944
|
+
return codes;
|
|
945
|
+
}
|
|
946
|
+
index = end + 1;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (/\s/.test(char)) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (char === "{") {
|
|
953
|
+
braceDepth++;
|
|
954
|
+
} else if (char === "}") {
|
|
955
|
+
braceDepth--;
|
|
956
|
+
} else if (char === "(") {
|
|
957
|
+
parenDepth++;
|
|
958
|
+
} else if (char === ")") {
|
|
959
|
+
parenDepth--;
|
|
960
|
+
} else if (char === "[") {
|
|
961
|
+
bracketDepth++;
|
|
962
|
+
} else if (char === "]") {
|
|
963
|
+
bracketDepth--;
|
|
964
|
+
} else if (
|
|
965
|
+
braceDepth === 1 &&
|
|
966
|
+
parenDepth === 0 &&
|
|
967
|
+
bracketDepth === 0 &&
|
|
968
|
+
(previousSignificantChar === "{" || previousSignificantChar === ",") &&
|
|
969
|
+
args.startsWith("code", index)
|
|
970
|
+
) {
|
|
971
|
+
let cursor = index + "code".length;
|
|
972
|
+
while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
|
|
973
|
+
cursor++;
|
|
974
|
+
}
|
|
975
|
+
if (args[cursor] === ":") {
|
|
976
|
+
cursor++;
|
|
977
|
+
while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
|
|
978
|
+
cursor++;
|
|
979
|
+
}
|
|
980
|
+
const quote = args[cursor];
|
|
981
|
+
if (quote === '"' || quote === "'") {
|
|
982
|
+
const end = skipStringLiteral(args, cursor);
|
|
983
|
+
if (end > cursor) {
|
|
984
|
+
const value = args.slice(cursor + 1, end);
|
|
985
|
+
let after = end + 1;
|
|
986
|
+
while (after < args.length && /\s/.test(args[after] ?? "")) {
|
|
987
|
+
after++;
|
|
988
|
+
}
|
|
989
|
+
const nextChar = after < args.length ? (args[after] ?? "") : "";
|
|
990
|
+
if (!value.includes("\\") && (nextChar === "," || nextChar === "}" || nextChar === "")) {
|
|
991
|
+
codes.push(value);
|
|
992
|
+
}
|
|
993
|
+
index = end;
|
|
994
|
+
previousSignificantChar = quote;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
return codes;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
previousSignificantChar = char;
|
|
1002
|
+
}
|
|
1003
|
+
return codes;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function collectLiteralThrownErrorCodes(source: string): string[] {
|
|
1007
|
+
const codes: string[] = [];
|
|
1008
|
+
THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = 0;
|
|
1009
|
+
for (
|
|
1010
|
+
let match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source);
|
|
1011
|
+
match;
|
|
1012
|
+
match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source)
|
|
1013
|
+
) {
|
|
1014
|
+
const argsStart = match.index + match[0].length;
|
|
1015
|
+
const args = extractBalancedCallArguments(source, argsStart);
|
|
1016
|
+
if (args !== undefined) {
|
|
1017
|
+
codes.push(...collectLiteralErrorCodeValues(args));
|
|
1018
|
+
}
|
|
1019
|
+
THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = argsStart;
|
|
1020
|
+
}
|
|
1021
|
+
return codes;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Static counterpart of the runtime `unregistered_provider_error_code`
|
|
1026
|
+
* signal (honest-provider-error-contract Phase 3.5.5): flags
|
|
1027
|
+
* `new ProviderError(...)` / `new ValidationError(...)` constructions whose
|
|
1028
|
+
* literal `code` is neither SDK-registered (SDK_RUNTIME_OWNED_ERROR_CODES
|
|
1029
|
+
* plus the canonical status-mapped codes shared with serve.ts toStatusCode)
|
|
1030
|
+
* nor declared in any operation's docs.errorCodes. At runtime such a code
|
|
1031
|
+
* serves HTTP 500 and emits the signal; this rule surfaces it at check time.
|
|
1032
|
+
*
|
|
1033
|
+
* A throw site cannot be attributed to a specific operation statically —
|
|
1034
|
+
* providers routinely throw from helpers shared across operations — so this
|
|
1035
|
+
* rule matches against the provider-level union of declared codes. That is
|
|
1036
|
+
* the honest scope: it will not catch a code declared only on the "wrong"
|
|
1037
|
+
* operation, and it never claims per-operation attribution it cannot prove.
|
|
1038
|
+
* Only literal string codes are checked; computed/dynamic codes and test
|
|
1039
|
+
* sources are skipped silently. Warning level: the long tail of existing
|
|
1040
|
+
* providers converges gradually, so this must not fail `apifuse check`.
|
|
1041
|
+
*/
|
|
1042
|
+
function lintUndeclaredThrownErrorCodes(provider: {
|
|
1043
|
+
authFlowSource?: string;
|
|
1044
|
+
providerSourceFiles?: Record<string, string>;
|
|
1045
|
+
operations?: Record<
|
|
1046
|
+
string,
|
|
1047
|
+
{
|
|
1048
|
+
handler?: unknown;
|
|
1049
|
+
source?: string;
|
|
1050
|
+
docs?: { errorCodes?: ReadonlyArray<{ code: string }> };
|
|
1051
|
+
}
|
|
1052
|
+
>;
|
|
1053
|
+
}): LintDiagnostic[] {
|
|
1054
|
+
const knownCodes = new Set<string>([
|
|
1055
|
+
...SDK_RUNTIME_OWNED_ERROR_CODES,
|
|
1056
|
+
...SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.keys(),
|
|
1057
|
+
]);
|
|
1058
|
+
for (const operation of Object.values(provider.operations ?? {})) {
|
|
1059
|
+
for (const entry of operation.docs?.errorCodes ?? []) {
|
|
1060
|
+
if (typeof entry?.code === "string") {
|
|
1061
|
+
knownCodes.add(entry.code);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const sources: Array<{ field: string; source: string }> = [];
|
|
1067
|
+
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
|
|
1068
|
+
([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath),
|
|
1069
|
+
);
|
|
1070
|
+
if (sourceFiles.length > 0) {
|
|
1071
|
+
for (const [filePath, source] of sourceFiles) {
|
|
1072
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
1073
|
+
}
|
|
1074
|
+
} else {
|
|
1075
|
+
if (provider.authFlowSource) {
|
|
1076
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1077
|
+
}
|
|
1078
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1079
|
+
const source = getOperationSource(operation);
|
|
1080
|
+
if (source) {
|
|
1081
|
+
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
1087
|
+
for (const { field, source } of sources) {
|
|
1088
|
+
const undeclaredCodes = new Set(
|
|
1089
|
+
collectLiteralThrownErrorCodes(source).filter((code) => !knownCodes.has(code)),
|
|
1090
|
+
);
|
|
1091
|
+
for (const code of undeclaredCodes) {
|
|
1092
|
+
diagnostics.push({
|
|
1093
|
+
rule: "thrown-error-code-undeclared",
|
|
1094
|
+
level: "warn",
|
|
1095
|
+
field,
|
|
1096
|
+
message: `Thrown error code "${code}" (${field}) is neither SDK-registered nor declared in any operation's docs.errorCodes; at runtime it serves HTTP 500 and emits the unregistered_provider_error_code signal. Declare it in the owning operation's docs.errorCodes with status and retryable.`,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return diagnostics;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
802
1103
|
export function lintOperation(op: {
|
|
803
1104
|
description?: string;
|
|
804
1105
|
descriptionKey?: string;
|
|
@@ -941,6 +1242,7 @@ export function lintProvider(
|
|
|
941
1242
|
derivations?: Record<string, string>;
|
|
942
1243
|
handler?: unknown;
|
|
943
1244
|
source?: string;
|
|
1245
|
+
docs?: { errorCodes?: ReadonlyArray<{ code: string }> };
|
|
944
1246
|
}
|
|
945
1247
|
>;
|
|
946
1248
|
meta?: {
|
|
@@ -958,6 +1260,7 @@ export function lintProvider(
|
|
|
958
1260
|
...lintCredentialWriteUsage(provider),
|
|
959
1261
|
...lintPlaywrightDirectImports(provider),
|
|
960
1262
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1263
|
+
...lintUndeclaredThrownErrorCodes(provider),
|
|
961
1264
|
];
|
|
962
1265
|
|
|
963
1266
|
if (provider.operations) {
|
package/src/runtime/auth-flow.ts
CHANGED
|
@@ -5,9 +5,11 @@ import type {
|
|
|
5
5
|
EnvContext,
|
|
6
6
|
FlowContext,
|
|
7
7
|
HttpClient,
|
|
8
|
+
OcrContext,
|
|
8
9
|
StealthClient,
|
|
9
10
|
SttContext,
|
|
10
11
|
} from "../types.js";
|
|
12
|
+
import { createUnsupportedOcrClient } from "./ocr.js";
|
|
11
13
|
import { createUnsupportedSttClient } from "./stt.js";
|
|
12
14
|
|
|
13
15
|
function normalizeAllowedKeys(allowedKeys: string[]): Set<string> {
|
|
@@ -58,6 +60,7 @@ export function createFlowContext(options: {
|
|
|
58
60
|
externalRef?: string;
|
|
59
61
|
allowedKeys: string[];
|
|
60
62
|
initialContext?: Record<string, unknown>;
|
|
63
|
+
ocr?: OcrContext;
|
|
61
64
|
stt?: SttContext;
|
|
62
65
|
}): FlowContext {
|
|
63
66
|
return {
|
|
@@ -70,6 +73,7 @@ export function createFlowContext(options: {
|
|
|
70
73
|
stealth: options.stealth,
|
|
71
74
|
env: options.env,
|
|
72
75
|
context: createScratchpad(options.allowedKeys, options.initialContext),
|
|
76
|
+
ocr: options.ocr ?? createUnsupportedOcrClient(),
|
|
73
77
|
stt: options.stt ?? createUnsupportedSttClient(),
|
|
74
78
|
auth: createAuthFlowHelpers(),
|
|
75
79
|
};
|