@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/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(source: string): BrowserVersionLiteralFinding[] {
1147
+ function collectBrowserVersionLiteralFindings(
1148
+ fileName: string,
1149
+ source: string,
1150
+ ): BrowserVersionLiteralFinding[] {
1152
1151
  const ts = getTypeScript();
1153
1152
  const sourceFile = ts.createSourceFile(
1154
- "provider-source.ts",
1153
+ fileName,
1155
1154
  source,
1156
1155
  ts.ScriptTarget.Latest,
1157
1156
  true,
1158
- ts.ScriptKind.TSX,
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({ field: "auth.flow", source: provider.authFlowSource });
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) sources.push({ field: `operations.${operationKey}.handler`, 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, fieldName } = occurrence.meta;
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 !== fieldName) {
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 "${fieldName}". Use the same key in every schema so the LLM sees one name.`,
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.fieldName}". Add ${kind.name}.field() to the issuing operation's output.`,
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.fieldName}". Add ${kind.name}.field() to the consuming operation's input.`,
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.fieldName}" handle field.`,
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.fieldName}" handle field. At least one listed issuer must output it.`,
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
- "provider.ts",
2192
+ fileName,
2174
2193
  source,
2175
2194
  ts.ScriptTarget.Latest,
2176
2195
  false,
2177
- ts.ScriptKind.TSX,
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({ field: `operations.${operationKey}.handler`, source });
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 applyPinnedWireFieldPaths(provider, diagnostics);
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 applyPinnedWireFieldPaths(provider, diagnostics);
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
@@ -166,6 +166,8 @@ export {
166
166
  APIFUSE_HANDLE_META_KEY,
167
167
  HANDLE_KIND_NAME_PATTERN,
168
168
  handleFieldDescription,
169
+ handleFieldNameFor,
170
+ handleHasDirectionalFieldNames,
169
171
  isHandleFieldMeta,
170
172
  } from "./handle-meta.js";
171
173
  export {
@@ -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}`;
@@ -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" });