@apifuse/provider-sdk 2.2.0-beta.63 → 2.2.0-beta.64
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/AUTHORING.md +99 -0
- package/CHANGELOG.md +4 -0
- package/LICENSE +21 -0
- package/bin/apifuse-submit-check.ts +9 -5
- package/dist/handle-meta.d.ts +36 -1
- package/dist/handle-meta.js +16 -0
- package/dist/handle.d.ts +23 -5
- package/dist/handle.js +54 -8
- package/dist/health-scenario.d.ts +38 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/lint.d.ts +5 -0
- package/dist/lint.js +57 -33
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/handle.js +9 -1
- package/dist/runtime/ocr.js +5 -0
- package/dist/runtime-boundary-lint.d.ts +132 -0
- package/dist/runtime-boundary-lint.js +586 -0
- package/dist/types.d.ts +29 -0
- package/dist/typescript-module.d.ts +39 -0
- package/dist/typescript-module.js +72 -0
- package/package.json +1 -1
- package/src/handle-meta.ts +51 -1
- package/src/handle.ts +92 -10
- package/src/health-scenario.ts +38 -0
- package/src/index.ts +14 -0
- package/src/lint.ts +82 -41
- package/src/provider.ts +2 -0
- package/src/runtime/handle.ts +9 -1
- package/src/runtime/ocr.ts +5 -0
- package/src/runtime-boundary-lint.ts +703 -0
- package/src/types.ts +29 -0
- package/src/typescript-module.ts +100 -0
package/src/lint.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
|
|
3
1
|
import type { ZodType } from "zod";
|
|
4
2
|
|
|
5
3
|
import {
|
|
@@ -12,9 +10,11 @@ import {
|
|
|
12
10
|
type HandleFieldMeta,
|
|
13
11
|
type HandleKindDeclaration,
|
|
14
12
|
handleFieldDescription,
|
|
13
|
+
handleFieldNameFor,
|
|
14
|
+
handleHasDirectionalFieldNames,
|
|
15
15
|
handleIssuerList,
|
|
16
|
-
isHandleIssuedBy,
|
|
17
16
|
isHandleFieldMeta,
|
|
17
|
+
isHandleIssuedBy,
|
|
18
18
|
} from "./handle-meta.js";
|
|
19
19
|
import { PROVIDER_ERROR_CATALOG_NAMESPACE } from "./i18n/error-messages.js";
|
|
20
20
|
import {
|
|
@@ -27,19 +27,10 @@ import {
|
|
|
27
27
|
isStealthOwnedHeaderName,
|
|
28
28
|
SDK_OWNED_CHROME_HEADER_PREFIX,
|
|
29
29
|
} from "./runtime/stealth-owned-headers.js";
|
|
30
|
+
import { lintRuntimeBoundary, scriptKindForSourceFile } from "./runtime-boundary-lint.js";
|
|
30
31
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
31
32
|
import type { AuthMode, OperationApprovalPolicy, OperationRiskClass } from "./types.js";
|
|
32
|
-
|
|
33
|
-
const requireModule = createRequire(import.meta.url);
|
|
34
|
-
// `typeof import(...)` keeps the type without emitting a static import: the
|
|
35
|
-
// typescript package is a CLI-only dependency and src/lint.ts is production
|
|
36
|
-
// runtime, which the typescript-import-boundary test enforces.
|
|
37
|
-
let typeScriptModule: typeof import("typescript") | undefined;
|
|
38
|
-
|
|
39
|
-
function getTypeScript(): typeof import("typescript") {
|
|
40
|
-
typeScriptModule ??= requireModule("typescript") as typeof import("typescript");
|
|
41
|
-
return typeScriptModule;
|
|
42
|
-
}
|
|
33
|
+
import { getTypeScript } from "./typescript-module.js";
|
|
43
34
|
|
|
44
35
|
type AuthModeLike =
|
|
45
36
|
| "none"
|
|
@@ -202,12 +193,17 @@ type ProviderLintOptions = {
|
|
|
202
193
|
mode?: ProviderLintMode;
|
|
203
194
|
};
|
|
204
195
|
|
|
196
|
+
/**
|
|
197
|
+
* A non-blocking audit line: a finding the author acknowledged with
|
|
198
|
+
* `// @apifuse-allow <rule>: <reason>`, kept visible in `apifuse check` output.
|
|
199
|
+
*/
|
|
205
200
|
export interface ProviderLintInformation {
|
|
206
201
|
rule: string;
|
|
207
202
|
message: string;
|
|
208
203
|
field?: string;
|
|
209
204
|
}
|
|
210
205
|
|
|
206
|
+
/** Diagnostics plus the acknowledged findings that accompany them. */
|
|
211
207
|
export interface ProviderLintResult {
|
|
212
208
|
diagnostics: LintDiagnostic[];
|
|
213
209
|
information: ProviderLintInformation[];
|
|
@@ -1148,14 +1144,17 @@ function isReportedOwnedHeaderName(value: string | undefined): boolean {
|
|
|
1148
1144
|
return name.startsWith(OWNED_CLIENT_HINT_HEADER_PREFIX);
|
|
1149
1145
|
}
|
|
1150
1146
|
|
|
1151
|
-
function collectBrowserVersionLiteralFindings(
|
|
1147
|
+
function collectBrowserVersionLiteralFindings(
|
|
1148
|
+
fileName: string,
|
|
1149
|
+
source: string,
|
|
1150
|
+
): BrowserVersionLiteralFinding[] {
|
|
1152
1151
|
const ts = getTypeScript();
|
|
1153
1152
|
const sourceFile = ts.createSourceFile(
|
|
1154
|
-
|
|
1153
|
+
fileName,
|
|
1155
1154
|
source,
|
|
1156
1155
|
ts.ScriptTarget.Latest,
|
|
1157
1156
|
true,
|
|
1158
|
-
ts
|
|
1157
|
+
scriptKindForSourceFile(ts, fileName),
|
|
1159
1158
|
);
|
|
1160
1159
|
let findings: BrowserVersionLiteralFinding[] = [];
|
|
1161
1160
|
const seen = new Set<string>();
|
|
@@ -1297,7 +1296,7 @@ function browserVersionLiteralMessage(finding: BrowserVersionLiteralFinding): st
|
|
|
1297
1296
|
}
|
|
1298
1297
|
|
|
1299
1298
|
function lintBrowserVersionLiterals(provider: ProviderSourceLike): LintDiagnostic[] {
|
|
1300
|
-
const sources: Array<{ field: string; source: string }> = [];
|
|
1299
|
+
const sources: Array<{ field: string; fileName: string; source: string }> = [];
|
|
1301
1300
|
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
|
|
1302
1301
|
([filePath]) =>
|
|
1303
1302
|
JAVASCRIPT_SOURCE_FILE_PATTERN.test(filePath) &&
|
|
@@ -1306,19 +1305,28 @@ function lintBrowserVersionLiterals(provider: ProviderSourceLike): LintDiagnosti
|
|
|
1306
1305
|
);
|
|
1307
1306
|
if (sourceFiles.length > 0) {
|
|
1308
1307
|
for (const [filePath, source] of sourceFiles) {
|
|
1309
|
-
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
1308
|
+
sources.push({ field: `sourceFiles.${filePath}`, fileName: filePath, source });
|
|
1310
1309
|
}
|
|
1311
1310
|
} else {
|
|
1312
1311
|
if (provider.authFlowSource)
|
|
1313
|
-
sources.push({
|
|
1312
|
+
sources.push({
|
|
1313
|
+
field: "auth.flow",
|
|
1314
|
+
fileName: "auth-flow.ts",
|
|
1315
|
+
source: provider.authFlowSource,
|
|
1316
|
+
});
|
|
1314
1317
|
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1315
1318
|
const source = getOperationSource(operation);
|
|
1316
|
-
if (source)
|
|
1319
|
+
if (source)
|
|
1320
|
+
sources.push({
|
|
1321
|
+
field: `operations.${operationKey}.handler`,
|
|
1322
|
+
fileName: `${operationKey}.ts`,
|
|
1323
|
+
source,
|
|
1324
|
+
});
|
|
1317
1325
|
}
|
|
1318
1326
|
}
|
|
1319
1327
|
|
|
1320
|
-
return sources.flatMap(({ field, source }) =>
|
|
1321
|
-
collectBrowserVersionLiteralFindings(source).map((finding) => ({
|
|
1328
|
+
return sources.flatMap(({ field, fileName, source }) =>
|
|
1329
|
+
collectBrowserVersionLiteralFindings(fileName, source).map((finding) => ({
|
|
1322
1330
|
rule: "browser-version-literal",
|
|
1323
1331
|
level: "error" as const,
|
|
1324
1332
|
field,
|
|
@@ -1995,6 +2003,7 @@ function readHandleKindDeclarations(value: unknown): HandleKindDeclaration[] {
|
|
|
1995
2003
|
typeof record.name === "string" &&
|
|
1996
2004
|
(record.type === "cursor" || record.type === "draft") &&
|
|
1997
2005
|
typeof record.fieldName === "string" &&
|
|
2006
|
+
(record.outputFieldName === undefined || typeof record.outputFieldName === "string") &&
|
|
1998
2007
|
(record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy))
|
|
1999
2008
|
);
|
|
2000
2009
|
});
|
|
@@ -2040,7 +2049,13 @@ function lintHandleDeclarations(provider: {
|
|
|
2040
2049
|
for (const { side, fields } of positions) {
|
|
2041
2050
|
for (const occurrence of fields) {
|
|
2042
2051
|
const field = `operations.${operationKey}.${occurrence.path}`;
|
|
2043
|
-
const { kind
|
|
2052
|
+
const { kind } = occurrence.meta;
|
|
2053
|
+
// A kind may name one handle differently in each direction (the canon's
|
|
2054
|
+
// `cursor` in, `next_cursor` out). The rule is still exact: each side
|
|
2055
|
+
// must use the key declared for THAT side, so the handle's identity
|
|
2056
|
+
// stays readable from the property key and the round trip has exactly
|
|
2057
|
+
// one edge — read `output`, send back under `input`.
|
|
2058
|
+
const expected = handleFieldNameFor(occurrence.meta, side);
|
|
2044
2059
|
|
|
2045
2060
|
if (!kindByName.has(kind)) {
|
|
2046
2061
|
diagnostics.push({
|
|
@@ -2051,12 +2066,16 @@ function lintHandleDeclarations(provider: {
|
|
|
2051
2066
|
});
|
|
2052
2067
|
}
|
|
2053
2068
|
|
|
2054
|
-
if (occurrence.key !==
|
|
2069
|
+
if (occurrence.key !== expected) {
|
|
2070
|
+
const directional = handleHasDirectionalFieldNames(occurrence.meta);
|
|
2071
|
+
const advice = directional
|
|
2072
|
+
? `Use the kind's ${side} fieldName on every ${side} schema; its ${side === "input" ? "output" : "input"} fieldName is "${handleFieldNameFor(occurrence.meta, side === "input" ? "output" : "input")}".`
|
|
2073
|
+
: "Use the same key in every schema so the LLM sees one name.";
|
|
2055
2074
|
diagnostics.push({
|
|
2056
2075
|
rule: "handle-field-name",
|
|
2057
2076
|
level: "error",
|
|
2058
2077
|
field,
|
|
2059
|
-
message: `${providerLabel} operation "${operationKey}" ${side} declares handle kind "${kind}" under property "${occurrence.key}" but the kind's fieldName is "${
|
|
2078
|
+
message: `${providerLabel} operation "${operationKey}" ${side} declares handle kind "${kind}" under property "${occurrence.key}" but the kind's ${directional ? `${side} ` : ""}fieldName is "${expected}". ${advice}`,
|
|
2060
2079
|
});
|
|
2061
2080
|
}
|
|
2062
2081
|
|
|
@@ -2085,7 +2104,7 @@ function lintHandleDeclarations(provider: {
|
|
|
2085
2104
|
rule: "handle-field-parity",
|
|
2086
2105
|
level: "error",
|
|
2087
2106
|
field,
|
|
2088
|
-
message: `${providerLabel} handle kind "${kind.name}" is never issued: no operation output contains its field "${kind
|
|
2107
|
+
message: `${providerLabel} handle kind "${kind.name}" is never issued: no operation output contains its field "${handleFieldNameFor(kind, "output")}". Add ${kind.name}.field() to the issuing operation's output.`,
|
|
2089
2108
|
});
|
|
2090
2109
|
}
|
|
2091
2110
|
if (!accepted) {
|
|
@@ -2093,7 +2112,7 @@ function lintHandleDeclarations(provider: {
|
|
|
2093
2112
|
rule: "handle-field-parity",
|
|
2094
2113
|
level: "error",
|
|
2095
2114
|
field,
|
|
2096
|
-
message: `${providerLabel} handle kind "${kind.name}" is never accepted: no operation input contains its field "${kind
|
|
2115
|
+
message: `${providerLabel} handle kind "${kind.name}" is never accepted: no operation input contains its field "${handleFieldNameFor(kind, "input")}". Add ${kind.name}.field() to the consuming operation's input.`,
|
|
2097
2116
|
});
|
|
2098
2117
|
}
|
|
2099
2118
|
|
|
@@ -2119,7 +2138,7 @@ function lintHandleDeclarations(provider: {
|
|
|
2119
2138
|
rule: "handle-issued-by",
|
|
2120
2139
|
level: "error",
|
|
2121
2140
|
field,
|
|
2122
|
-
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind
|
|
2141
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${handleFieldNameFor(kind, "output")}" handle field.`,
|
|
2123
2142
|
});
|
|
2124
2143
|
} else {
|
|
2125
2144
|
diagnostics.push({
|
|
@@ -2130,7 +2149,7 @@ function lintHandleDeclarations(provider: {
|
|
|
2130
2149
|
.map((operationKey) => `"${operationKey}"`)
|
|
2131
2150
|
.join(
|
|
2132
2151
|
", ",
|
|
2133
|
-
)}, but none of those operations' outputs has a "${kind
|
|
2152
|
+
)}, but none of those operations' outputs has a "${handleFieldNameFor(kind, "output")}" handle field. At least one listed issuer must output it.`,
|
|
2134
2153
|
});
|
|
2135
2154
|
}
|
|
2136
2155
|
}
|
|
@@ -2162,7 +2181,7 @@ const LEGACY_CHOICE_IDENTIFIERS = new Set([
|
|
|
2162
2181
|
* the provider) while code inside `${}` interpolations still does. Falls back
|
|
2163
2182
|
* to a raw-source regex when TypeScript is not installed.
|
|
2164
2183
|
*/
|
|
2165
|
-
function findLegacyChoiceUsage(source: string): string | undefined {
|
|
2184
|
+
function findLegacyChoiceUsage(fileName: string, source: string): string | undefined {
|
|
2166
2185
|
let ts: typeof import("typescript");
|
|
2167
2186
|
try {
|
|
2168
2187
|
ts = getTypeScript();
|
|
@@ -2170,11 +2189,11 @@ function findLegacyChoiceUsage(source: string): string | undefined {
|
|
|
2170
2189
|
return LEGACY_CHOICE_USAGE_PATTERN.exec(source)?.[0];
|
|
2171
2190
|
}
|
|
2172
2191
|
const file = ts.createSourceFile(
|
|
2173
|
-
|
|
2192
|
+
fileName,
|
|
2174
2193
|
source,
|
|
2175
2194
|
ts.ScriptTarget.Latest,
|
|
2176
2195
|
false,
|
|
2177
|
-
ts
|
|
2196
|
+
scriptKindForSourceFile(ts, fileName),
|
|
2178
2197
|
);
|
|
2179
2198
|
let found: string | undefined;
|
|
2180
2199
|
const visit = (node: import("typescript").Node): void => {
|
|
@@ -2199,23 +2218,27 @@ function findLegacyChoiceUsage(source: string): string | undefined {
|
|
|
2199
2218
|
}
|
|
2200
2219
|
|
|
2201
2220
|
function lintLegacyChoiceUsage(provider: ProviderSourceLike): LintDiagnostic[] {
|
|
2202
|
-
const sources: Array<{ field: string; source: string }> = [];
|
|
2221
|
+
const sources: Array<{ field: string; fileName: string; source: string }> = [];
|
|
2203
2222
|
|
|
2204
2223
|
if (provider.authFlowSource) {
|
|
2205
|
-
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
2224
|
+
sources.push({ field: "auth.flow", fileName: "auth-flow.ts", source: provider.authFlowSource });
|
|
2206
2225
|
}
|
|
2207
2226
|
for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
|
|
2208
|
-
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
2227
|
+
sources.push({ field: `sourceFiles.${filePath}`, fileName: filePath, source });
|
|
2209
2228
|
}
|
|
2210
2229
|
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
2211
2230
|
const source = getOperationSource(operation);
|
|
2212
2231
|
if (source) {
|
|
2213
|
-
sources.push({
|
|
2232
|
+
sources.push({
|
|
2233
|
+
field: `operations.${operationKey}.handler`,
|
|
2234
|
+
fileName: `${operationKey}.ts`,
|
|
2235
|
+
source,
|
|
2236
|
+
});
|
|
2214
2237
|
}
|
|
2215
2238
|
}
|
|
2216
2239
|
|
|
2217
|
-
return sources.flatMap(({ field, source }) => {
|
|
2218
|
-
const match = findLegacyChoiceUsage(source);
|
|
2240
|
+
return sources.flatMap(({ field, fileName, source }) => {
|
|
2241
|
+
const match = findLegacyChoiceUsage(fileName, source);
|
|
2219
2242
|
if (!match) return [];
|
|
2220
2243
|
return [
|
|
2221
2244
|
{
|
|
@@ -2421,6 +2444,10 @@ export function lintProviderWithInformation(
|
|
|
2421
2444
|
...lintLegacyChoiceUsage(provider),
|
|
2422
2445
|
...lintHandleDeclarations(provider),
|
|
2423
2446
|
];
|
|
2447
|
+
// Runtime boundary rules (process.env, node:fs/net/child_process, raw
|
|
2448
|
+
// fetch) also yield information entries for `@apifuse-allow` acknowledgements.
|
|
2449
|
+
const runtimeBoundary = lintRuntimeBoundary(getTypeScript(), provider, getOperationSource);
|
|
2450
|
+
diagnostics.push(...runtimeBoundary.diagnostics);
|
|
2424
2451
|
|
|
2425
2452
|
if (provider.operations) {
|
|
2426
2453
|
const authMode = provider.auth?.mode;
|
|
@@ -2464,7 +2491,10 @@ export function lintProviderWithInformation(
|
|
|
2464
2491
|
}
|
|
2465
2492
|
|
|
2466
2493
|
if (!provider.operations) {
|
|
2467
|
-
return
|
|
2494
|
+
return withInformation(
|
|
2495
|
+
applyPinnedWireFieldPaths(provider, diagnostics),
|
|
2496
|
+
runtimeBoundary.information,
|
|
2497
|
+
);
|
|
2468
2498
|
}
|
|
2469
2499
|
|
|
2470
2500
|
diagnostics.push(
|
|
@@ -2495,5 +2525,16 @@ export function lintProviderWithInformation(
|
|
|
2495
2525
|
),
|
|
2496
2526
|
);
|
|
2497
2527
|
|
|
2498
|
-
return
|
|
2528
|
+
return withInformation(
|
|
2529
|
+
applyPinnedWireFieldPaths(provider, diagnostics),
|
|
2530
|
+
runtimeBoundary.information,
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
function withInformation(
|
|
2535
|
+
result: ProviderLintResult,
|
|
2536
|
+
information: readonly ProviderLintInformation[],
|
|
2537
|
+
): ProviderLintResult {
|
|
2538
|
+
if (information.length === 0) return result;
|
|
2539
|
+
return { diagnostics: result.diagnostics, information: [...information, ...result.information] };
|
|
2499
2540
|
}
|
package/src/provider.ts
CHANGED
package/src/runtime/handle.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
isDraftKind,
|
|
28
28
|
type ResultOf,
|
|
29
29
|
} from "../handle.js";
|
|
30
|
-
import { formatHandleIssuers } from "../handle-meta.js";
|
|
30
|
+
import { formatHandleIssuers, handleHasDirectionalFieldNames } from "../handle-meta.js";
|
|
31
31
|
import type {
|
|
32
32
|
ProviderRequestContext,
|
|
33
33
|
ProviderRuntimeState,
|
|
@@ -248,6 +248,14 @@ export function formatNormalizationClasses(classes: readonly string[]): string {
|
|
|
248
248
|
// ---------------------------------------------------------------------------
|
|
249
249
|
|
|
250
250
|
function publicCollapsedMessage(kind: HandleKind): string {
|
|
251
|
+
// "a new one" is unambiguous only while the kind has one name. When it is
|
|
252
|
+
// issued under `next_cursor` and accepted under `cursor`, the caller has to
|
|
253
|
+
// be told which field to read, so use the full recovery sentence — it names
|
|
254
|
+
// both keys and is identical for invalid / expired / not-found, which is
|
|
255
|
+
// what keeps the public collapse indistinguishable.
|
|
256
|
+
if (handleHasDirectionalFieldNames(kind)) {
|
|
257
|
+
return `\`${kind.fieldName}\` is not valid or has expired. ${handleRecoverySentence(kind)}`;
|
|
258
|
+
}
|
|
251
259
|
const issuers = formatHandleIssuers(kind.issuedBy);
|
|
252
260
|
const again = issuers ? `Call ${issuers} again to get a new one.` : "Request a new one.";
|
|
253
261
|
return `\`${kind.fieldName}\` is not valid or has expired. ${again}`;
|
package/src/runtime/ocr.ts
CHANGED
|
@@ -86,6 +86,10 @@ function createErrorOcrClient(options: ErrorOcrClientOptions): OcrContext {
|
|
|
86
86
|
};
|
|
87
87
|
return tagCapability(
|
|
88
88
|
{
|
|
89
|
+
// Own data property: `bindOcrTelemetry` re-spreads the context, so a
|
|
90
|
+
// getter or prototype member would silently vanish before reaching
|
|
91
|
+
// provider code.
|
|
92
|
+
available: false,
|
|
89
93
|
async recognize() {
|
|
90
94
|
return unavailable();
|
|
91
95
|
},
|
|
@@ -355,6 +359,7 @@ function createOcrClient(
|
|
|
355
359
|
recognize: (request: OcrRecognizeRequest) => Promise<OcrResult>,
|
|
356
360
|
): OcrContext {
|
|
357
361
|
return {
|
|
362
|
+
available: true,
|
|
358
363
|
recognize,
|
|
359
364
|
async extractCaptchaText(image, options = {}): Promise<OcrCaptchaResult> {
|
|
360
365
|
const result = await recognize({ image, hint: "captcha" });
|