@apifuse/provider-sdk 2.2.0-beta.61 → 2.2.0-beta.63
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 +268 -9
- package/CHANGELOG.md +8 -0
- package/README.md +1 -0
- package/bin/apifuse-check.ts +84 -2
- package/bin/apifuse-dev.ts +11 -31
- package/bin/apifuse-migrate-deployment.ts +200 -0
- package/bin/apifuse-pack-check.ts +11 -4
- package/bin/apifuse-pack-types.ts +191 -4
- package/bin/apifuse-record.ts +44 -32
- package/bin/apifuse-test.ts +3 -1
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +11 -0
- package/dist/cli/deployment-intent.d.ts +178 -0
- package/dist/cli/deployment-intent.js +277 -0
- package/dist/cli/migrate-deployment-intent.d.ts +132 -0
- package/dist/cli/migrate-deployment-intent.js +1552 -0
- package/dist/cli/migrate-provider-shape.d.ts +5 -1
- package/dist/cli/migrate-provider-shape.js +4 -2
- package/dist/cli/resolver-runtime.d.ts +21 -0
- package/dist/cli/resolver-runtime.js +31 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.js +5 -0
- package/dist/dev.d.ts +3 -1
- package/dist/dev.js +6 -1
- package/dist/engine.d.ts +35 -1
- package/dist/engine.js +21 -2
- package/dist/error-resolution.d.ts +1 -0
- package/dist/error-resolution.js +54 -0
- package/dist/errors.d.ts +39 -1
- package/dist/errors.js +50 -0
- package/dist/handle-meta.d.ts +18 -2
- package/dist/handle-meta.js +30 -2
- package/dist/handle.d.ts +33 -12
- package/dist/handle.js +69 -24
- package/dist/i18n/error-messages.d.ts +86 -0
- package/dist/i18n/error-messages.js +192 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/keys.d.ts +12 -0
- package/dist/i18n/keys.js +76 -5
- package/dist/index.d.ts +10 -2
- package/dist/index.js +9 -1
- package/dist/lint.d.ts +10 -0
- package/dist/lint.js +440 -57
- package/dist/provider.d.ts +7 -5
- package/dist/provider.js +4 -2
- package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
- package/dist/runtime/browser-telemetry-binding.js +83 -0
- package/dist/runtime/browser-telemetry.d.ts +76 -0
- package/dist/runtime/browser-telemetry.js +165 -0
- package/dist/runtime/browser.d.ts +3 -0
- package/dist/runtime/browser.js +208 -47
- package/dist/runtime/cache-telemetry.d.ts +71 -0
- package/dist/runtime/cache-telemetry.js +177 -0
- package/dist/runtime/cache.js +67 -7
- package/dist/runtime/capability-response.d.ts +3 -0
- package/dist/runtime/capability-response.js +56 -0
- package/dist/runtime/capability-telemetry.d.ts +46 -0
- package/dist/runtime/capability-telemetry.js +68 -0
- package/dist/runtime/diagnostic-env.d.ts +3 -0
- package/dist/runtime/diagnostic-env.js +10 -1
- package/dist/runtime/engine-mode.d.ts +74 -0
- package/dist/runtime/engine-mode.js +152 -0
- package/dist/runtime/handle.js +15 -10
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +16 -0
- package/dist/runtime/http-telemetry-guard.js +202 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +356 -174
- package/dist/runtime/instrumentation.js +92 -13
- package/dist/runtime/ocr-telemetry.d.ts +82 -0
- package/dist/runtime/ocr-telemetry.js +192 -0
- package/dist/runtime/ocr.js +30 -8
- package/dist/runtime/request-telemetry.d.ts +2 -0
- package/dist/runtime/request-telemetry.js +2 -1
- package/dist/runtime/resolver-runtime-options.d.ts +14 -0
- package/dist/runtime/resolver-runtime-options.js +33 -0
- package/dist/runtime/resolver-shared.d.ts +3 -2
- package/dist/runtime/resolver-shared.js +4 -4
- package/dist/runtime/resolver-telemetry.d.ts +1 -0
- package/dist/runtime/resolver-telemetry.js +1 -1
- package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
- package/dist/runtime/resolver-vendors/browser.js +46 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
- package/dist/runtime/resolver-vendors/hosts.js +35 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
- package/dist/runtime/resolver-vendors/types.d.ts +5 -4
- package/dist/runtime/resolver.d.ts +4 -1
- package/dist/runtime/resolver.js +46 -13
- package/dist/runtime/state-telemetry.d.ts +79 -0
- package/dist/runtime/state-telemetry.js +194 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +26 -7
- package/dist/runtime/stealth-cookies.d.ts +5 -1
- package/dist/runtime/stealth-cookies.js +21 -1
- package/dist/runtime/stealth-owned-headers.d.ts +26 -0
- package/dist/runtime/stealth-owned-headers.js +37 -0
- package/dist/runtime/stealth-telemetry.d.ts +92 -0
- package/dist/runtime/stealth-telemetry.js +179 -0
- package/dist/runtime/stealth.d.ts +4 -1
- package/dist/runtime/stealth.js +228 -85
- package/dist/runtime/stt-telemetry.d.ts +77 -0
- package/dist/runtime/stt-telemetry.js +176 -0
- package/dist/runtime/stt.js +15 -6
- package/dist/server/error-stack-frames.d.ts +35 -0
- package/dist/server/error-stack-frames.js +219 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.js +5 -0
- package/dist/server/sdk-version.d.ts +3 -0
- package/dist/server/sdk-version.js +19 -0
- package/dist/server/self-test.js +1 -18
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +9 -0
- package/dist/server/serve-implementation.d.ts +53 -2
- package/dist/server/serve-implementation.js +369 -123
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +7 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
- package/dist/stateful/stateful-provider-runtime-context.js +1 -0
- package/dist/types.d.ts +94 -1
- package/package.json +6 -1
- package/src/cli/commands.ts +14 -0
- package/src/cli/deployment-intent.ts +364 -0
- package/src/cli/migrate-deployment-intent.ts +1908 -0
- package/src/cli/migrate-provider-shape.ts +10 -4
- package/src/cli/resolver-runtime.ts +72 -0
- package/src/define.ts +13 -0
- package/src/dev.ts +9 -2
- package/src/engine.ts +38 -2
- package/src/error-resolution.ts +58 -0
- package/src/errors.ts +77 -1
- package/src/handle-meta.ts +39 -4
- package/src/handle.ts +121 -38
- package/src/i18n/error-messages.ts +255 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/keys.ts +80 -6
- package/src/index.ts +91 -0
- package/src/lint.ts +552 -57
- package/src/provider.ts +20 -0
- package/src/runtime/browser-telemetry-binding.ts +92 -0
- package/src/runtime/browser-telemetry.ts +289 -0
- package/src/runtime/browser.ts +249 -65
- package/src/runtime/cache-telemetry.ts +247 -0
- package/src/runtime/cache.ts +68 -17
- package/src/runtime/capability-response.ts +53 -0
- package/src/runtime/capability-telemetry.ts +101 -0
- package/src/runtime/diagnostic-env.ts +11 -1
- package/src/runtime/engine-mode.ts +233 -0
- package/src/runtime/handle.ts +20 -10
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +208 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +457 -204
- package/src/runtime/instrumentation.ts +105 -16
- package/src/runtime/ocr-telemetry.ts +308 -0
- package/src/runtime/ocr.ts +109 -74
- package/src/runtime/request-telemetry.ts +2 -1
- package/src/runtime/resolver-runtime-options.ts +50 -0
- package/src/runtime/resolver-shared.ts +9 -4
- package/src/runtime/resolver-telemetry.ts +1 -1
- package/src/runtime/resolver-vendors/browser.ts +65 -7
- package/src/runtime/resolver-vendors/hosts.ts +43 -0
- package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
- package/src/runtime/resolver-vendors/types.ts +5 -4
- package/src/runtime/resolver.ts +64 -21
- package/src/runtime/state-telemetry.ts +298 -0
- package/src/runtime/state.ts +46 -45
- package/src/runtime/stealth-cookies.ts +24 -1
- package/src/runtime/stealth-owned-headers.ts +41 -0
- package/src/runtime/stealth-telemetry.ts +304 -0
- package/src/runtime/stealth.ts +355 -150
- package/src/runtime/stt-telemetry.ts +282 -0
- package/src/runtime/stt.ts +93 -71
- package/src/server/error-stack-frames.ts +214 -0
- package/src/server/index.ts +66 -0
- package/src/server/sdk-version.ts +22 -0
- package/src/server/self-test.ts +1 -21
- package/src/server/sensitive-values.ts +14 -0
- package/src/server/serve-implementation.ts +601 -157
- package/src/server/types.ts +7 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
- package/src/stateful/stateful-provider-runtime-context.ts +4 -0
- package/src/types.ts +99 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
package/src/lint.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import type { ZodType } from "zod";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
SDK_CANONICAL_ERROR_CODE_RETRYABILITY,
|
|
6
7
|
SDK_RUNTIME_OWNED_ERROR_CODES,
|
|
7
8
|
SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES,
|
|
8
9
|
} from "./error-resolution.js";
|
|
@@ -11,9 +12,21 @@ import {
|
|
|
11
12
|
type HandleFieldMeta,
|
|
12
13
|
type HandleKindDeclaration,
|
|
13
14
|
handleFieldDescription,
|
|
15
|
+
handleIssuerList,
|
|
16
|
+
isHandleIssuedBy,
|
|
14
17
|
isHandleFieldMeta,
|
|
15
18
|
} from "./handle-meta.js";
|
|
19
|
+
import { PROVIDER_ERROR_CATALOG_NAMESPACE } from "./i18n/error-messages.js";
|
|
20
|
+
import {
|
|
21
|
+
getProviderLocaleSegments,
|
|
22
|
+
isProviderLocaleKey,
|
|
23
|
+
type ProviderLocaleCatalog,
|
|
24
|
+
} from "./i18n/keys.js";
|
|
16
25
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
26
|
+
import {
|
|
27
|
+
isStealthOwnedHeaderName,
|
|
28
|
+
SDK_OWNED_CHROME_HEADER_PREFIX,
|
|
29
|
+
} from "./runtime/stealth-owned-headers.js";
|
|
17
30
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
18
31
|
import type { AuthMode, OperationApprovalPolicy, OperationRiskClass } from "./types.js";
|
|
19
32
|
|
|
@@ -1012,7 +1025,14 @@ function lintSelfHostedBrowserPatterns(
|
|
|
1012
1025
|
return diagnostics;
|
|
1013
1026
|
}
|
|
1014
1027
|
|
|
1015
|
-
|
|
1028
|
+
// Captures the constructor name so each rule can pick its own scope: the
|
|
1029
|
+
// undeclared-code rule keeps its historical ProviderError/ValidationError
|
|
1030
|
+
// surface, while the localization rules also cover AuthError.
|
|
1031
|
+
const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(ProviderError|AuthError|ValidationError)\s*\(/g;
|
|
1032
|
+
const UNDECLARED_CODE_ERROR_CLASSES: ReadonlySet<string> = new Set([
|
|
1033
|
+
"ProviderError",
|
|
1034
|
+
"ValidationError",
|
|
1035
|
+
]);
|
|
1016
1036
|
|
|
1017
1037
|
const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
1018
1038
|
const RECORDED_FIXTURE_SOURCE_FILE_PATTERN =
|
|
@@ -1023,8 +1043,13 @@ const VERSIONED_PROFILE_LITERAL_PATTERN =
|
|
|
1023
1043
|
const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
|
|
1024
1044
|
const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
|
|
1025
1045
|
const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
|
|
1046
|
+
// Calls that write a header entry as (name, value): Headers/Map `set` and
|
|
1047
|
+
// `append`, Node's `setHeader`. Predicates, replacements, and logging calls
|
|
1048
|
+
// take string arguments too and must not count as header writes.
|
|
1049
|
+
const HEADER_SETTER_CALLEE_PATTERN = /^(?:set|append|setHeader|addHeader)$/;
|
|
1050
|
+
const OWNED_CLIENT_HINT_HEADER_PREFIX = "sec-ch-ua";
|
|
1026
1051
|
|
|
1027
|
-
type BrowserVersionLiteralKind = "profile" | "user-agent" | "sec-ch-ua";
|
|
1052
|
+
type BrowserVersionLiteralKind = "profile" | "user-agent" | "sec-ch-ua" | "owned-header";
|
|
1028
1053
|
|
|
1029
1054
|
type BrowserVersionLiteralFinding = {
|
|
1030
1055
|
kind: BrowserVersionLiteralKind;
|
|
@@ -1052,6 +1077,77 @@ function isSecChUaHeaderName(value: string | undefined): boolean {
|
|
|
1052
1077
|
return value?.toLowerCase() === "sec-ch-ua";
|
|
1053
1078
|
}
|
|
1054
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* True for the code paths that reach the stealth transport: `ctx.stealth`,
|
|
1082
|
+
* `ctx["stealth"]`, and `const { stealth } = ctx`. Read from the AST so a
|
|
1083
|
+
* comment or string that mentions ctx.stealth does not scope a ctx.http file
|
|
1084
|
+
* into this rule.
|
|
1085
|
+
*/
|
|
1086
|
+
function isStealthContextAccess(node: import("typescript").Node): boolean {
|
|
1087
|
+
const ts = getTypeScript();
|
|
1088
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1089
|
+
return (
|
|
1090
|
+
ts.isIdentifier(node.expression) &&
|
|
1091
|
+
node.expression.text === "ctx" &&
|
|
1092
|
+
node.name.text === "stealth"
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
if (ts.isElementAccessExpression(node)) {
|
|
1096
|
+
return (
|
|
1097
|
+
ts.isIdentifier(node.expression) &&
|
|
1098
|
+
node.expression.text === "ctx" &&
|
|
1099
|
+
staticStringText(node.argumentExpression) === "stealth"
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
if (ts.isBindingElement(node)) {
|
|
1103
|
+
const key = node.propertyName ?? node.name;
|
|
1104
|
+
if (!ts.isIdentifier(key) || key.text !== "stealth") return false;
|
|
1105
|
+
const declaration = node.parent?.parent;
|
|
1106
|
+
return (
|
|
1107
|
+
declaration !== undefined &&
|
|
1108
|
+
ts.isVariableDeclaration(declaration) &&
|
|
1109
|
+
declaration.initializer !== undefined &&
|
|
1110
|
+
ts.isIdentifier(declaration.initializer) &&
|
|
1111
|
+
declaration.initializer.text === "ctx"
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function isStaticUndefined(node: import("typescript").Node | undefined): boolean {
|
|
1118
|
+
if (!node) return false;
|
|
1119
|
+
const ts = getTypeScript();
|
|
1120
|
+
return (ts.isIdentifier(node) && node.text === "undefined") || ts.isVoidExpression(node);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function calleeName(node: import("typescript").CallExpression): string | undefined {
|
|
1124
|
+
const ts = getTypeScript();
|
|
1125
|
+
if (ts.isPropertyAccessExpression(node.expression)) return node.expression.name.text;
|
|
1126
|
+
if (ts.isIdentifier(node.expression)) return node.expression.text;
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Header names this rule reports in ctx.stealth files: the Sec-Fetch-* and
|
|
1132
|
+
* client-hint (sec-ch-ua*) families, restricted to what the stealth runtime
|
|
1133
|
+
* really rejects (isStealthOwnedHeaderName). Deliberately narrower than the
|
|
1134
|
+
* runtime rejection: host, connection, and accept-encoding are left alone
|
|
1135
|
+
* because ctx.http callers set them legitimately in the same files, and
|
|
1136
|
+
* user-agent only reaches a finding through the "user-agent" kind, which
|
|
1137
|
+
* matches a versioned literal value rather than the name — a user-agent read
|
|
1138
|
+
* from a variable is rejected by the runtime and stays silent here. Widening
|
|
1139
|
+
* the set needs a per-file transport scope and a suppression path first.
|
|
1140
|
+
*/
|
|
1141
|
+
function isReportedOwnedHeaderName(value: string | undefined): boolean {
|
|
1142
|
+
if (value === undefined) return false;
|
|
1143
|
+
const name = value.toLowerCase();
|
|
1144
|
+
if (!isStealthOwnedHeaderName(name)) return false;
|
|
1145
|
+
if (name.startsWith(SDK_OWNED_CHROME_HEADER_PREFIX)) {
|
|
1146
|
+
return name.length > SDK_OWNED_CHROME_HEADER_PREFIX.length;
|
|
1147
|
+
}
|
|
1148
|
+
return name.startsWith(OWNED_CLIENT_HINT_HEADER_PREFIX);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1055
1151
|
function collectBrowserVersionLiteralFindings(source: string): BrowserVersionLiteralFinding[] {
|
|
1056
1152
|
const ts = getTypeScript();
|
|
1057
1153
|
const sourceFile = ts.createSourceFile(
|
|
@@ -1061,8 +1157,13 @@ function collectBrowserVersionLiteralFindings(source: string): BrowserVersionLit
|
|
|
1061
1157
|
true,
|
|
1062
1158
|
ts.ScriptKind.TSX,
|
|
1063
1159
|
);
|
|
1064
|
-
|
|
1160
|
+
let findings: BrowserVersionLiteralFinding[] = [];
|
|
1065
1161
|
const seen = new Set<string>();
|
|
1162
|
+
// Owned-header entries only count once the file is known to reach
|
|
1163
|
+
// ctx.stealth, which may appear after the entry; collect them and decide
|
|
1164
|
+
// after the walk.
|
|
1165
|
+
let stealthAccess = false;
|
|
1166
|
+
const ownedHeaderEntries: Array<{ name: string; position: number; valuePosition?: number }> = [];
|
|
1066
1167
|
|
|
1067
1168
|
const addFinding = (kind: BrowserVersionLiteralKind, literal: string, position: number) => {
|
|
1068
1169
|
const key = `${kind}:${position}:${literal}`;
|
|
@@ -1088,29 +1189,97 @@ function collectBrowserVersionLiteralFindings(source: string): BrowserVersionLit
|
|
|
1088
1189
|
}
|
|
1089
1190
|
};
|
|
1090
1191
|
|
|
1192
|
+
// A header entry `name: value` in any of the shapes below. In a ctx.stealth
|
|
1193
|
+
// file an owned name is the finding (the runtime rejects it whatever the
|
|
1194
|
+
// value); elsewhere only a versioned sec-ch-ua value is. A statically
|
|
1195
|
+
// undefined value is exempt only in the "record" shapes, the ones the
|
|
1196
|
+
// runtime drops before the ownership check (`normalizedHeaderEntries`):
|
|
1197
|
+
// `Headers.set(name, undefined)` stringifies to "undefined" and still
|
|
1198
|
+
// throws, so it stays a finding.
|
|
1199
|
+
const inspectHeaderEntry = (
|
|
1200
|
+
nameNode: import("typescript").Node,
|
|
1201
|
+
name: string | undefined,
|
|
1202
|
+
valueNode: import("typescript").Node | undefined,
|
|
1203
|
+
ownedHeaderShape: false | "record" | "entry",
|
|
1204
|
+
) => {
|
|
1205
|
+
if (name === undefined) return;
|
|
1206
|
+
if (
|
|
1207
|
+
ownedHeaderShape !== false &&
|
|
1208
|
+
isReportedOwnedHeaderName(name) &&
|
|
1209
|
+
!(ownedHeaderShape === "record" && isStaticUndefined(valueNode))
|
|
1210
|
+
) {
|
|
1211
|
+
ownedHeaderEntries.push({
|
|
1212
|
+
name,
|
|
1213
|
+
position: nameNode.getStart(sourceFile),
|
|
1214
|
+
valuePosition: valueNode?.getStart(sourceFile),
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
if (isSecChUaHeaderName(name)) inspectSecChUaValue(valueNode);
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1091
1220
|
const visit = (node: import("typescript").Node) => {
|
|
1092
1221
|
const text = staticStringText(node);
|
|
1093
1222
|
if (text !== undefined) inspectLiteral(text, node.getStart(sourceFile));
|
|
1223
|
+
if (isStealthContextAccess(node)) stealthAccess = true;
|
|
1224
|
+
|
|
1225
|
+
if (ts.isPropertyAssignment(node)) {
|
|
1226
|
+
inspectHeaderEntry(node.name, staticPropertyName(node.name), node.initializer, "record");
|
|
1227
|
+
}
|
|
1094
1228
|
|
|
1095
|
-
if (ts.
|
|
1096
|
-
|
|
1229
|
+
if (ts.isCallExpression(node) && node.arguments[0]) {
|
|
1230
|
+
// Only `headers.set(name, value)`-style calls write a header;
|
|
1231
|
+
// `name.startsWith("sec-fetch-")` or `log("sec-fetch-dest", x)` do not.
|
|
1232
|
+
inspectHeaderEntry(
|
|
1233
|
+
node.arguments[0],
|
|
1234
|
+
staticStringText(node.arguments[0]),
|
|
1235
|
+
node.arguments[1],
|
|
1236
|
+
node.arguments.length >= 2 && HEADER_SETTER_CALLEE_PATTERN.test(calleeName(node) ?? "")
|
|
1237
|
+
? "entry"
|
|
1238
|
+
: false,
|
|
1239
|
+
);
|
|
1097
1240
|
}
|
|
1098
1241
|
|
|
1099
|
-
if (ts.
|
|
1100
|
-
|
|
1242
|
+
if (ts.isArrayLiteralExpression(node) && node.elements[0]) {
|
|
1243
|
+
// Only a `[name, value]` tuple is a header. A name list such as
|
|
1244
|
+
// `new Set(["sec-ch-ua-mobile", "sec-ch-ua-platform"])` is not: it is
|
|
1245
|
+
// the argument of a constructor, or its second element is itself an
|
|
1246
|
+
// owned name.
|
|
1247
|
+
const second = node.elements[1];
|
|
1248
|
+
inspectHeaderEntry(
|
|
1249
|
+
node.elements[0],
|
|
1250
|
+
staticStringText(node.elements[0]),
|
|
1251
|
+
second,
|
|
1252
|
+
node.elements.length === 2 &&
|
|
1253
|
+
!ts.isNewExpression(node.parent) &&
|
|
1254
|
+
!isStealthOwnedHeaderName(staticStringText(second) ?? "")
|
|
1255
|
+
? "entry"
|
|
1256
|
+
: false,
|
|
1257
|
+
);
|
|
1101
1258
|
}
|
|
1102
1259
|
|
|
1103
1260
|
if (
|
|
1104
|
-
ts.
|
|
1105
|
-
|
|
1261
|
+
ts.isBinaryExpression(node) &&
|
|
1262
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
1263
|
+
ts.isElementAccessExpression(node.left)
|
|
1106
1264
|
) {
|
|
1107
|
-
|
|
1265
|
+
const nameNode = node.left.argumentExpression;
|
|
1266
|
+
inspectHeaderEntry(nameNode, staticStringText(nameNode), node.right, "record");
|
|
1108
1267
|
}
|
|
1109
1268
|
|
|
1110
1269
|
ts.forEachChild(node, visit);
|
|
1111
1270
|
};
|
|
1112
1271
|
|
|
1113
1272
|
visit(sourceFile);
|
|
1273
|
+
if (stealthAccess) {
|
|
1274
|
+
// The owned name is the finding; do not also report its sec-ch-ua value.
|
|
1275
|
+
const shadowedValues = new Set(ownedHeaderEntries.map((entry) => entry.valuePosition));
|
|
1276
|
+
findings = findings.filter(
|
|
1277
|
+
(finding) => !(finding.kind === "sec-ch-ua" && shadowedValues.has(finding.position)),
|
|
1278
|
+
);
|
|
1279
|
+
for (const entry of ownedHeaderEntries) {
|
|
1280
|
+
addFinding("owned-header", entry.name, entry.position);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1114
1283
|
return findings.sort((left, right) => left.position - right.position);
|
|
1115
1284
|
}
|
|
1116
1285
|
|
|
@@ -1122,6 +1291,8 @@ function browserVersionLiteralMessage(finding: BrowserVersionLiteralFinding): st
|
|
|
1122
1291
|
return `Hardcoded User-Agent browser version "${finding.literal}" can disagree with the stealth TLS fingerprint. Remove the literal. ctx.stealth owns User-Agent and rejects a caller value with STEALTH_HEADER_OVERRIDE_UNSUPPORTED, so omit the header there and select the profile with stealth: { browser: "chrome", os: "macos" }. For ctx.http only, derive it from getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.`;
|
|
1123
1292
|
case "sec-ch-ua":
|
|
1124
1293
|
return 'Hardcoded sec-ch-ua versions can disagree with the stealth TLS fingerprint. Remove the literal and let ctx.stealth generate client hints from stealth: { browser: "chrome", os: "macos" }. ctx.stealth also owns User-Agent, so omit that header there as well; for ctx.http only, derive it from getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.';
|
|
1294
|
+
case "owned-header":
|
|
1295
|
+
return `ctx.stealth owns the "${finding.literal}" header and rejects a caller value with STEALTH_HEADER_OVERRIDE_UNSUPPORTED (HTTP 500 at request time). Remove it; declare stealth: { requestClass: "navigation" | "xhr" | "post" } to drive Sec-Fetch-* and select the profile with stealth: { browser: "chrome", os: "macos" } for client hints.`;
|
|
1125
1296
|
}
|
|
1126
1297
|
}
|
|
1127
1298
|
|
|
@@ -1256,14 +1427,17 @@ function extractBalancedCallArguments(source: string, startIndex: number): strin
|
|
|
1256
1427
|
}
|
|
1257
1428
|
|
|
1258
1429
|
/**
|
|
1259
|
-
* Collects literal string values of top-level
|
|
1260
|
-
* ProviderError/ValidationError options object. Only plain `"..."` /
|
|
1261
|
-
* literals at options-object depth count; computed
|
|
1430
|
+
* Collects literal string values of the named top-level properties inside a
|
|
1431
|
+
* ProviderError/AuthError/ValidationError options object. Only plain `"..."` /
|
|
1432
|
+
* `'...'` literals at options-object depth count; computed values (identifiers,
|
|
1262
1433
|
* ternaries, template substitutions, concatenations, escapes) are skipped
|
|
1263
|
-
* silently so the
|
|
1434
|
+
* silently so the rules never guess. The first literal wins per property name.
|
|
1264
1435
|
*/
|
|
1265
|
-
function
|
|
1266
|
-
|
|
1436
|
+
function collectLiteralErrorOptionValues(
|
|
1437
|
+
args: string,
|
|
1438
|
+
optionNames: readonly string[],
|
|
1439
|
+
): Record<string, string> {
|
|
1440
|
+
const collected: Record<string, string> = {};
|
|
1267
1441
|
let braceDepth = 0;
|
|
1268
1442
|
let parenDepth = 0;
|
|
1269
1443
|
let bracketDepth = 0;
|
|
@@ -1273,7 +1447,7 @@ function collectLiteralErrorCodeValues(args: string): string[] {
|
|
|
1273
1447
|
if (char === '"' || char === "'" || char === "`") {
|
|
1274
1448
|
const end = skipStringLiteral(args, index);
|
|
1275
1449
|
if (end < 0) {
|
|
1276
|
-
return
|
|
1450
|
+
return collected;
|
|
1277
1451
|
}
|
|
1278
1452
|
index = end;
|
|
1279
1453
|
previousSignificantChar = char;
|
|
@@ -1282,7 +1456,7 @@ function collectLiteralErrorCodeValues(args: string): string[] {
|
|
|
1282
1456
|
if (char === "/" && args[index + 1] === "/") {
|
|
1283
1457
|
const newline = args.indexOf("\n", index);
|
|
1284
1458
|
if (newline === -1) {
|
|
1285
|
-
return
|
|
1459
|
+
return collected;
|
|
1286
1460
|
}
|
|
1287
1461
|
index = newline;
|
|
1288
1462
|
continue;
|
|
@@ -1290,7 +1464,7 @@ function collectLiteralErrorCodeValues(args: string): string[] {
|
|
|
1290
1464
|
if (char === "/" && args[index + 1] === "*") {
|
|
1291
1465
|
const end = args.indexOf("*/", index + 2);
|
|
1292
1466
|
if (end === -1) {
|
|
1293
|
-
return
|
|
1467
|
+
return collected;
|
|
1294
1468
|
}
|
|
1295
1469
|
index = end + 1;
|
|
1296
1470
|
continue;
|
|
@@ -1314,10 +1488,14 @@ function collectLiteralErrorCodeValues(args: string): string[] {
|
|
|
1314
1488
|
braceDepth === 1 &&
|
|
1315
1489
|
parenDepth === 0 &&
|
|
1316
1490
|
bracketDepth === 0 &&
|
|
1317
|
-
(previousSignificantChar === "{" || previousSignificantChar === ",")
|
|
1318
|
-
args.startsWith("code", index)
|
|
1491
|
+
(previousSignificantChar === "{" || previousSignificantChar === ",")
|
|
1319
1492
|
) {
|
|
1320
|
-
|
|
1493
|
+
const optionName = optionNames.find((name) => startsWithOptionName(args, index, name));
|
|
1494
|
+
if (optionName === undefined) {
|
|
1495
|
+
previousSignificantChar = char;
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
let cursor = index + optionName.length;
|
|
1321
1499
|
while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
|
|
1322
1500
|
cursor++;
|
|
1323
1501
|
}
|
|
@@ -1340,23 +1518,41 @@ function collectLiteralErrorCodeValues(args: string): string[] {
|
|
|
1340
1518
|
!value.includes("\\") &&
|
|
1341
1519
|
(nextChar === "," || nextChar === "}" || nextChar === "")
|
|
1342
1520
|
) {
|
|
1343
|
-
|
|
1521
|
+
collected[optionName] ??= value;
|
|
1344
1522
|
}
|
|
1345
1523
|
index = end;
|
|
1346
1524
|
previousSignificantChar = quote;
|
|
1347
1525
|
continue;
|
|
1348
1526
|
}
|
|
1349
|
-
return
|
|
1527
|
+
return collected;
|
|
1350
1528
|
}
|
|
1351
1529
|
}
|
|
1352
1530
|
}
|
|
1353
1531
|
previousSignificantChar = char;
|
|
1354
1532
|
}
|
|
1355
|
-
return
|
|
1533
|
+
return collected;
|
|
1356
1534
|
}
|
|
1357
1535
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1536
|
+
/** True when `name` starts at `index` and is not the prefix of a longer identifier. */
|
|
1537
|
+
function startsWithOptionName(args: string, index: number, name: string): boolean {
|
|
1538
|
+
if (!args.startsWith(name, index)) return false;
|
|
1539
|
+
const next = args[index + name.length] ?? "";
|
|
1540
|
+
return !/[A-Za-z0-9_$]/.test(next);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/** One `new ProviderError|AuthError|ValidationError(...)` construction found by source scan. */
|
|
1544
|
+
type ThrownErrorConstructionSite = {
|
|
1545
|
+
/** Constructor name, e.g. `ProviderError`. */
|
|
1546
|
+
readonly errorClass: string;
|
|
1547
|
+
readonly code?: string;
|
|
1548
|
+
readonly messageKey?: string;
|
|
1549
|
+
readonly fixKey?: string;
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
const THROWN_ERROR_LOCALE_OPTION_NAMES = ["code", "messageKey", "fixKey"] as const;
|
|
1553
|
+
|
|
1554
|
+
function collectThrownErrorConstructionSites(source: string): ThrownErrorConstructionSite[] {
|
|
1555
|
+
const sites: ThrownErrorConstructionSite[] = [];
|
|
1360
1556
|
THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = 0;
|
|
1361
1557
|
for (
|
|
1362
1558
|
let match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source);
|
|
@@ -1366,11 +1562,52 @@ function collectLiteralThrownErrorCodes(source: string): string[] {
|
|
|
1366
1562
|
const argsStart = match.index + match[0].length;
|
|
1367
1563
|
const args = extractBalancedCallArguments(source, argsStart);
|
|
1368
1564
|
if (args !== undefined) {
|
|
1369
|
-
|
|
1565
|
+
const values = collectLiteralErrorOptionValues(args, THROWN_ERROR_LOCALE_OPTION_NAMES);
|
|
1566
|
+
sites.push({
|
|
1567
|
+
errorClass: match[1] ?? "",
|
|
1568
|
+
...(values.code === undefined ? {} : { code: values.code }),
|
|
1569
|
+
...(values.messageKey === undefined ? {} : { messageKey: values.messageKey }),
|
|
1570
|
+
...(values.fixKey === undefined ? {} : { fixKey: values.fixKey }),
|
|
1571
|
+
});
|
|
1370
1572
|
}
|
|
1371
1573
|
THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = argsStart;
|
|
1372
1574
|
}
|
|
1373
|
-
return
|
|
1575
|
+
return sites;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
type ThrowSiteLintSource = { readonly field: string; readonly source: string };
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* The provider sources a throw-site rule scans, with the diagnostic field path
|
|
1582
|
+
* each one reports under. Prefers the CLI-collected source files (whole-repo
|
|
1583
|
+
* coverage) and falls back to the handler/auth-flow function text available
|
|
1584
|
+
* when `lintProvider` is called without them. Test sources are excluded.
|
|
1585
|
+
*/
|
|
1586
|
+
function collectThrowSiteLintSources(provider: {
|
|
1587
|
+
authFlowSource?: string;
|
|
1588
|
+
providerSourceFiles?: Record<string, string>;
|
|
1589
|
+
operations?: Record<string, { handler?: unknown; source?: string }>;
|
|
1590
|
+
}): ThrowSiteLintSource[] {
|
|
1591
|
+
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
|
|
1592
|
+
([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath),
|
|
1593
|
+
);
|
|
1594
|
+
if (sourceFiles.length > 0) {
|
|
1595
|
+
return sourceFiles.map(([filePath, source]) => ({
|
|
1596
|
+
field: `sourceFiles.${filePath}`,
|
|
1597
|
+
source,
|
|
1598
|
+
}));
|
|
1599
|
+
}
|
|
1600
|
+
const sources: ThrowSiteLintSource[] = [];
|
|
1601
|
+
if (provider.authFlowSource) {
|
|
1602
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1603
|
+
}
|
|
1604
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1605
|
+
const source = getOperationSource(operation);
|
|
1606
|
+
if (source) {
|
|
1607
|
+
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return sources;
|
|
1374
1611
|
}
|
|
1375
1612
|
|
|
1376
1613
|
/**
|
|
@@ -1415,30 +1652,15 @@ function lintUndeclaredThrownErrorCodes(provider: {
|
|
|
1415
1652
|
}
|
|
1416
1653
|
}
|
|
1417
1654
|
|
|
1418
|
-
const sources
|
|
1419
|
-
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
|
|
1420
|
-
([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath),
|
|
1421
|
-
);
|
|
1422
|
-
if (sourceFiles.length > 0) {
|
|
1423
|
-
for (const [filePath, source] of sourceFiles) {
|
|
1424
|
-
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
1425
|
-
}
|
|
1426
|
-
} else {
|
|
1427
|
-
if (provider.authFlowSource) {
|
|
1428
|
-
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1429
|
-
}
|
|
1430
|
-
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1431
|
-
const source = getOperationSource(operation);
|
|
1432
|
-
if (source) {
|
|
1433
|
-
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1655
|
+
const sources = collectThrowSiteLintSources(provider);
|
|
1437
1656
|
|
|
1438
1657
|
const diagnostics: LintDiagnostic[] = [];
|
|
1439
1658
|
for (const { field, source } of sources) {
|
|
1440
1659
|
const undeclaredCodes = new Set(
|
|
1441
|
-
|
|
1660
|
+
collectThrownErrorConstructionSites(source)
|
|
1661
|
+
.filter((site) => UNDECLARED_CODE_ERROR_CLASSES.has(site.errorClass))
|
|
1662
|
+
.flatMap((site) => (site.code === undefined ? [] : [site.code]))
|
|
1663
|
+
.filter((code) => !knownCodes.has(code)),
|
|
1442
1664
|
);
|
|
1443
1665
|
for (const code of undeclaredCodes) {
|
|
1444
1666
|
diagnostics.push({
|
|
@@ -1452,6 +1674,245 @@ function lintUndeclaredThrownErrorCodes(provider: {
|
|
|
1452
1674
|
return diagnostics;
|
|
1453
1675
|
}
|
|
1454
1676
|
|
|
1677
|
+
/**
|
|
1678
|
+
* Flags an operation `errorCodes` entry whose declared `status` or `retryable`
|
|
1679
|
+
* contradicts the SDK's canonical resolution for that code.
|
|
1680
|
+
*
|
|
1681
|
+
* For a code the SDK status-maps but does not runtime-own (UPSTREAM_ERROR,
|
|
1682
|
+
* BLOCKED, and the fleet-consensus codes registered alongside them), the
|
|
1683
|
+
* declaration still wins at runtime — `toStatusCode` reads the declaration
|
|
1684
|
+
* before the canonical map. So registering a code never silently rewrites an
|
|
1685
|
+
* existing declaration's status, and it must not: that would change a served
|
|
1686
|
+
* status behind the author's back. What it does create is a fleet that answers
|
|
1687
|
+
* one code two ways. This rule is where that divergence surfaces, by name, with
|
|
1688
|
+
* the canonical value to converge on.
|
|
1689
|
+
*
|
|
1690
|
+
* Codes the SDK runtime-owns are handled separately: `defineProvider` already
|
|
1691
|
+
* warns there, because for those the declaration really is ignored.
|
|
1692
|
+
*
|
|
1693
|
+
* Warning level, deliberately: the divergent providers are live and converge on
|
|
1694
|
+
* their own schedule, so this must not fail `apifuse check`.
|
|
1695
|
+
*/
|
|
1696
|
+
function lintErrorCodeDeclarationConflicts(provider: {
|
|
1697
|
+
id?: string;
|
|
1698
|
+
operations?: Record<
|
|
1699
|
+
string,
|
|
1700
|
+
{
|
|
1701
|
+
errorCodes?: ReadonlyArray<{ code: string; status?: number; retryable?: boolean }>;
|
|
1702
|
+
}
|
|
1703
|
+
>;
|
|
1704
|
+
}): LintDiagnostic[] {
|
|
1705
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
1706
|
+
const providerId = provider.id ?? "unknown";
|
|
1707
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1708
|
+
for (const [index, entry] of (operation.errorCodes ?? []).entries()) {
|
|
1709
|
+
if (typeof entry?.code !== "string") continue;
|
|
1710
|
+
// SDK-owned codes ignore the declaration outright; defineProvider owns
|
|
1711
|
+
// that message so authors are not told two different things.
|
|
1712
|
+
if (SDK_RUNTIME_OWNED_ERROR_CODES.has(entry.code)) continue;
|
|
1713
|
+
|
|
1714
|
+
const field = `operations.${operationKey}.errorCodes[${index}]`;
|
|
1715
|
+
const canonicalStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(entry.code);
|
|
1716
|
+
if (canonicalStatus !== undefined && entry.status !== undefined) {
|
|
1717
|
+
if (entry.status !== canonicalStatus) {
|
|
1718
|
+
diagnostics.push({
|
|
1719
|
+
rule: "error-code-status-conflicts-sdk",
|
|
1720
|
+
level: "warn",
|
|
1721
|
+
field: `${field}.status`,
|
|
1722
|
+
message: `Provider "${providerId}" operation "${operationKey}" declares status ${entry.status} for SDK-registered error code "${entry.code}", whose canonical status is ${canonicalStatus}. The declaration still wins at runtime, so this operation serves ${entry.status} while the rest of the fleet serves ${canonicalStatus} for the same code. Drop the status (the SDK supplies ${canonicalStatus}) or, if ${entry.status} is genuinely the right answer here, throw a distinct code that means that.`,
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
const canonicalRetryable = SDK_CANONICAL_ERROR_CODE_RETRYABILITY.get(entry.code);
|
|
1728
|
+
if (
|
|
1729
|
+
canonicalRetryable !== undefined &&
|
|
1730
|
+
entry.retryable !== undefined &&
|
|
1731
|
+
entry.retryable !== canonicalRetryable
|
|
1732
|
+
) {
|
|
1733
|
+
diagnostics.push({
|
|
1734
|
+
rule: "error-code-retryable-conflicts-sdk",
|
|
1735
|
+
level: "warn",
|
|
1736
|
+
field: `${field}.retryable`,
|
|
1737
|
+
message: `Provider "${providerId}" operation "${operationKey}" declares retryable ${entry.retryable} for SDK-registered error code "${entry.code}", whose canonical retryability is ${canonicalRetryable}. The declaration still wins at runtime, so callers and the Gateway retry policy get a different answer here than everywhere else the same code is thrown. Drop the retryable (the SDK supplies ${canonicalRetryable}) or throw a distinct code for the retryable condition.`,
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return diagnostics;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/** Declared `errorCodes[]` entry shape the localization rules read. */
|
|
1746
|
+
type OperationErrorCodeLike = {
|
|
1747
|
+
code: string;
|
|
1748
|
+
messageKey?: string;
|
|
1749
|
+
fixKey?: string;
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
type ErrorLocaleKeyStatus = "ok" | "missing" | "malformed";
|
|
1753
|
+
|
|
1754
|
+
function errorLocaleKeyStatus(catalog: ProviderLocaleCatalog, key: string): ErrorLocaleKeyStatus {
|
|
1755
|
+
if (!isProviderLocaleKey(key)) return "malformed";
|
|
1756
|
+
const value = getProviderLocaleSegments(catalog, key.split("."));
|
|
1757
|
+
return typeof value === "string" && value.trim().length > 0 ? "ok" : "missing";
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function hasDerivedErrorCatalogText(
|
|
1761
|
+
catalog: ProviderLocaleCatalog,
|
|
1762
|
+
code: string,
|
|
1763
|
+
field: "message" | "fix",
|
|
1764
|
+
): boolean {
|
|
1765
|
+
const value = getProviderLocaleSegments(catalog, [PROVIDER_ERROR_CATALOG_NAMESPACE, code, field]);
|
|
1766
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* True when a thrown code reaches the caller as a real provider error whose
|
|
1771
|
+
* text the declared/derived catalog path can actually reach.
|
|
1772
|
+
*
|
|
1773
|
+
* A code qualifies either because the provider declares it in an operation's
|
|
1774
|
+
* `errorCodes`, or because the SDK registers a canonical status for it. The
|
|
1775
|
+
* second arm matters because registering a code is precisely what lets a
|
|
1776
|
+
* provider *delete* the declaration — AUTHORING tells it to for
|
|
1777
|
+
* `UPSTREAM_AUTH_ERROR`, `UPSTREAM_SCHEMA_ERROR` and `INVALID_REQUEST` — and an
|
|
1778
|
+
* undeclared throw of a registered code is no longer the HTTP 500 that
|
|
1779
|
+
* `thrown-error-code-undeclared` reports. Without this arm the two rules leave
|
|
1780
|
+
* a silent hole exactly where the fleet is being told to converge: a 502
|
|
1781
|
+
* `UPSTREAM_SCHEMA_ERROR` served untranslated with nothing warning about it.
|
|
1782
|
+
*
|
|
1783
|
+
* Codes the SDK runtime-owns are excluded: `sdkOwnsErrorResolution` skips the
|
|
1784
|
+
* declaration and derived candidates for them at serve time, so the catalog
|
|
1785
|
+
* entry this rule asks for would never take effect.
|
|
1786
|
+
*/
|
|
1787
|
+
function isCatalogReachableThrownCode(code: string, declaredCodes: ReadonlySet<string>): boolean {
|
|
1788
|
+
if (declaredCodes.has(code)) return true;
|
|
1789
|
+
return (
|
|
1790
|
+
!SDK_RUNTIME_OWNED_ERROR_CODES.has(code) && SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.has(code)
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* Authoring rules for localized provider error text (see
|
|
1796
|
+
* `ProviderErrorOptions.messageKey`).
|
|
1797
|
+
*
|
|
1798
|
+
* - `error-locale-key-missing` / `error-locale-key-malformed` (**error**): a
|
|
1799
|
+
* literal `messageKey`/`fixKey`, whether on a throw site or on an
|
|
1800
|
+
* `errorCodes[]` declaration, that the provider's `en` catalog does not
|
|
1801
|
+
* resolve to non-empty text, or that is not a legal locale dot path. At
|
|
1802
|
+
* runtime such a key is silently skipped and the caller gets the English
|
|
1803
|
+
* literal, so nothing fails loudly without this rule.
|
|
1804
|
+
* - `thrown-error-message-not-localized` (**warn**): a throw site whose literal
|
|
1805
|
+
* `code` is declared in the provider's `errorCodes` — or SDK-registered, and
|
|
1806
|
+
* so servable without a declaration — but that carries no `messageKey`, no
|
|
1807
|
+
* declaration-level `messageKey`, and no derived `errors.<code>.message` in
|
|
1808
|
+
* `en`. Such an error is served untranslated to every caller. Warning while
|
|
1809
|
+
* the ~2,000 existing sites migrate; it is promoted to error per provider as
|
|
1810
|
+
* each migration wave lands.
|
|
1811
|
+
*
|
|
1812
|
+
* Both rules are skipped entirely when the caller did not supply
|
|
1813
|
+
* `localeCatalogEn`: without the catalog they could only guess.
|
|
1814
|
+
*/
|
|
1815
|
+
function lintErrorMessageLocalization(provider: {
|
|
1816
|
+
authFlowSource?: string;
|
|
1817
|
+
providerSourceFiles?: Record<string, string>;
|
|
1818
|
+
localeCatalogEn?: ProviderLocaleCatalog;
|
|
1819
|
+
operations?: Record<
|
|
1820
|
+
string,
|
|
1821
|
+
{
|
|
1822
|
+
handler?: unknown;
|
|
1823
|
+
source?: string;
|
|
1824
|
+
errorCodes?: ReadonlyArray<OperationErrorCodeLike>;
|
|
1825
|
+
}
|
|
1826
|
+
>;
|
|
1827
|
+
}): LintDiagnostic[] {
|
|
1828
|
+
const catalog = provider.localeCatalogEn;
|
|
1829
|
+
if (!catalog) return [];
|
|
1830
|
+
|
|
1831
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
1832
|
+
const declaredCodes = new Set<string>();
|
|
1833
|
+
const codesWithDeclaredMessageKey = new Set<string>();
|
|
1834
|
+
|
|
1835
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1836
|
+
for (const entry of operation.errorCodes ?? []) {
|
|
1837
|
+
if (typeof entry?.code !== "string") continue;
|
|
1838
|
+
declaredCodes.add(entry.code);
|
|
1839
|
+
for (const keyField of ["messageKey", "fixKey"] as const) {
|
|
1840
|
+
const key = entry[keyField];
|
|
1841
|
+
if (typeof key !== "string") continue;
|
|
1842
|
+
if (keyField === "messageKey") codesWithDeclaredMessageKey.add(entry.code);
|
|
1843
|
+
const status = errorLocaleKeyStatus(catalog, key);
|
|
1844
|
+
if (status === "ok") continue;
|
|
1845
|
+
diagnostics.push(
|
|
1846
|
+
errorLocaleKeyDiagnostic(
|
|
1847
|
+
status,
|
|
1848
|
+
`operations.${operationKey}.errorCodes`,
|
|
1849
|
+
key,
|
|
1850
|
+
`errorCodes entry "${entry.code}" of operation "${operationKey}"`,
|
|
1851
|
+
),
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
for (const { field, source } of collectThrowSiteLintSources(provider)) {
|
|
1858
|
+
const reportedKeys = new Set<string>();
|
|
1859
|
+
const reportedCodes = new Set<string>();
|
|
1860
|
+
for (const site of collectThrownErrorConstructionSites(source)) {
|
|
1861
|
+
for (const key of [site.messageKey, site.fixKey]) {
|
|
1862
|
+
if (key === undefined || reportedKeys.has(key)) continue;
|
|
1863
|
+
const status = errorLocaleKeyStatus(catalog, key);
|
|
1864
|
+
if (status === "ok") continue;
|
|
1865
|
+
reportedKeys.add(key);
|
|
1866
|
+
diagnostics.push(
|
|
1867
|
+
errorLocaleKeyDiagnostic(status, field, key, `a thrown error in ${field}`),
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
const code = site.code;
|
|
1871
|
+
if (code === undefined || reportedCodes.has(code)) continue;
|
|
1872
|
+
if (site.messageKey !== undefined) continue;
|
|
1873
|
+
if (!isCatalogReachableThrownCode(code, declaredCodes)) continue;
|
|
1874
|
+
if (codesWithDeclaredMessageKey.has(code)) continue;
|
|
1875
|
+
if (hasDerivedErrorCatalogText(catalog, code, "message")) continue;
|
|
1876
|
+
reportedCodes.add(code);
|
|
1877
|
+
const origin = declaredCodes.has(code)
|
|
1878
|
+
? `declared code "${code}"`
|
|
1879
|
+
: `SDK-registered code "${code}"`;
|
|
1880
|
+
const declarationClause = declaredCodes.has(code)
|
|
1881
|
+
? "its errorCodes declaration has none, and"
|
|
1882
|
+
: "no errorCodes declaration supplies one, and";
|
|
1883
|
+
diagnostics.push({
|
|
1884
|
+
rule: "thrown-error-message-not-localized",
|
|
1885
|
+
level: "warn",
|
|
1886
|
+
field,
|
|
1887
|
+
message: `${site.errorClass} with ${origin} (${field}) has no messageKey, ${declarationClause} locales/en.json has no "${PROVIDER_ERROR_CATALOG_NAMESPACE}.${code}.message"; this error is served untranslated to ko and ja callers. Add messageKey (or "${PROVIDER_ERROR_CATALOG_NAMESPACE}.${code}.message" to every locale catalog).`,
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
return diagnostics;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function errorLocaleKeyDiagnostic(
|
|
1896
|
+
status: Exclude<ErrorLocaleKeyStatus, "ok">,
|
|
1897
|
+
field: string,
|
|
1898
|
+
key: string,
|
|
1899
|
+
owner: string,
|
|
1900
|
+
): LintDiagnostic {
|
|
1901
|
+
return status === "malformed"
|
|
1902
|
+
? {
|
|
1903
|
+
rule: "error-locale-key-malformed",
|
|
1904
|
+
level: "error",
|
|
1905
|
+
field,
|
|
1906
|
+
message: `Locale key ${JSON.stringify(key)} on ${owner} is not a locale dot path such as "errors.upstreamSchema.message" or "errors.UPSTREAM_SCHEMA_ERROR.message"; segments are camelCase, an array index, or — directly under "errors" — the error code itself; the SDK skips a malformed key at serve time and the caller gets the English literal instead.`,
|
|
1907
|
+
}
|
|
1908
|
+
: {
|
|
1909
|
+
rule: "error-locale-key-missing",
|
|
1910
|
+
level: "error",
|
|
1911
|
+
field,
|
|
1912
|
+
message: `Locale key ${JSON.stringify(key)} on ${owner} is missing from locales/en.json (or resolves to a non-string/empty value); the SDK falls back to the English literal at serve time, so the key never takes effect.`,
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1455
1916
|
type HandleFieldOccurrence = {
|
|
1456
1917
|
/** Diagnostic path, e.g. `output.offers[].waiting_token`. */
|
|
1457
1918
|
path: string;
|
|
@@ -1534,7 +1995,7 @@ function readHandleKindDeclarations(value: unknown): HandleKindDeclaration[] {
|
|
|
1534
1995
|
typeof record.name === "string" &&
|
|
1535
1996
|
(record.type === "cursor" || record.type === "draft") &&
|
|
1536
1997
|
typeof record.fieldName === "string" &&
|
|
1537
|
-
(record.issuedBy === undefined ||
|
|
1998
|
+
(record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy))
|
|
1538
1999
|
);
|
|
1539
2000
|
});
|
|
1540
2001
|
}
|
|
@@ -1542,8 +2003,9 @@ function readHandleKindDeclarations(value: unknown): HandleKindDeclaration[] {
|
|
|
1542
2003
|
/**
|
|
1543
2004
|
* ADR-0012 handle rules: every `x-apifuse-handle` schema field must belong to
|
|
1544
2005
|
* a declared kind under its declared property key; every declared kind must be
|
|
1545
|
-
* both issued (some output) and accepted (some input); `issuedBy`
|
|
1546
|
-
* operation
|
|
2006
|
+
* both issued (some output) and accepted (some input); every `issuedBy` entry
|
|
2007
|
+
* must be an operation key and at least one of them must output the field; and
|
|
2008
|
+
* `ctx.handle` needs `state`.
|
|
1547
2009
|
*/
|
|
1548
2010
|
function lintHandleDeclarations(provider: {
|
|
1549
2011
|
id?: string;
|
|
@@ -1636,19 +2098,39 @@ function lintHandleDeclarations(provider: {
|
|
|
1636
2098
|
}
|
|
1637
2099
|
|
|
1638
2100
|
if (kind.issuedBy === undefined) continue;
|
|
1639
|
-
|
|
2101
|
+
const issuers = handleIssuerList(kind.issuedBy);
|
|
2102
|
+
const known = issuers.filter((operationKey) => Object.hasOwn(operations, operationKey));
|
|
2103
|
+
for (const operationKey of issuers) {
|
|
2104
|
+
if (known.includes(operationKey)) continue;
|
|
1640
2105
|
diagnostics.push({
|
|
1641
2106
|
rule: "handle-issued-by",
|
|
1642
2107
|
level: "error",
|
|
1643
2108
|
field,
|
|
1644
|
-
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${
|
|
2109
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${operationKey}", which is not an operation key.`,
|
|
1645
2110
|
});
|
|
1646
|
-
}
|
|
2111
|
+
}
|
|
2112
|
+
if (known.length === 0) continue;
|
|
2113
|
+
const issuing = known.filter((operationKey) =>
|
|
2114
|
+
outputKindsByOperation.get(operationKey)?.has(kind.name),
|
|
2115
|
+
);
|
|
2116
|
+
if (issuing.length > 0) continue;
|
|
2117
|
+
if (known.length === 1) {
|
|
2118
|
+
diagnostics.push({
|
|
2119
|
+
rule: "handle-issued-by",
|
|
2120
|
+
level: "error",
|
|
2121
|
+
field,
|
|
2122
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind.fieldName}" handle field.`,
|
|
2123
|
+
});
|
|
2124
|
+
} else {
|
|
1647
2125
|
diagnostics.push({
|
|
1648
2126
|
rule: "handle-issued-by",
|
|
1649
2127
|
level: "error",
|
|
1650
2128
|
field,
|
|
1651
|
-
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy
|
|
2129
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy ${known
|
|
2130
|
+
.map((operationKey) => `"${operationKey}"`)
|
|
2131
|
+
.join(
|
|
2132
|
+
", ",
|
|
2133
|
+
)}, but none of those operations' outputs has a "${kind.fieldName}" handle field. At least one listed issuer must output it.`,
|
|
1652
2134
|
});
|
|
1653
2135
|
}
|
|
1654
2136
|
}
|
|
@@ -1878,6 +2360,11 @@ export function lintProvider(
|
|
|
1878
2360
|
};
|
|
1879
2361
|
authFlowSource?: string;
|
|
1880
2362
|
providerSourceFiles?: Record<string, string>;
|
|
2363
|
+
/**
|
|
2364
|
+
* The provider's `locales/en.json`, injected by `apifuse check`. Error
|
|
2365
|
+
* message localization rules are skipped when it is absent.
|
|
2366
|
+
*/
|
|
2367
|
+
localeCatalogEn?: ProviderLocaleCatalog;
|
|
1881
2368
|
operations?: Record<
|
|
1882
2369
|
string,
|
|
1883
2370
|
{
|
|
@@ -1892,7 +2379,13 @@ export function lintProvider(
|
|
|
1892
2379
|
fixtures?: unknown;
|
|
1893
2380
|
handler?: unknown;
|
|
1894
2381
|
source?: string;
|
|
1895
|
-
errorCodes?: ReadonlyArray<{
|
|
2382
|
+
errorCodes?: ReadonlyArray<{
|
|
2383
|
+
code: string;
|
|
2384
|
+
status?: number;
|
|
2385
|
+
retryable?: boolean;
|
|
2386
|
+
messageKey?: string;
|
|
2387
|
+
fixKey?: string;
|
|
2388
|
+
}>;
|
|
1896
2389
|
}
|
|
1897
2390
|
>;
|
|
1898
2391
|
meta?: {
|
|
@@ -1923,6 +2416,8 @@ export function lintProviderWithInformation(
|
|
|
1923
2416
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1924
2417
|
...lintBrowserVersionLiterals(provider),
|
|
1925
2418
|
...lintUndeclaredThrownErrorCodes(provider),
|
|
2419
|
+
...lintErrorCodeDeclarationConflicts(provider),
|
|
2420
|
+
...lintErrorMessageLocalization(provider),
|
|
1926
2421
|
...lintLegacyChoiceUsage(provider),
|
|
1927
2422
|
...lintHandleDeclarations(provider),
|
|
1928
2423
|
];
|