@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/dist/lint.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "./error-resolution.js";
|
|
3
|
-
import { APIFUSE_HANDLE_META_KEY, handleFieldDescription, isHandleFieldMeta, } from "./handle-meta.js";
|
|
2
|
+
import { SDK_CANONICAL_ERROR_CODE_RETRYABILITY, SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "./error-resolution.js";
|
|
3
|
+
import { APIFUSE_HANDLE_META_KEY, handleFieldDescription, handleIssuerList, isHandleIssuedBy, isHandleFieldMeta, } from "./handle-meta.js";
|
|
4
|
+
import { PROVIDER_ERROR_CATALOG_NAMESPACE } from "./i18n/error-messages.js";
|
|
5
|
+
import { getProviderLocaleSegments, isProviderLocaleKey, } from "./i18n/keys.js";
|
|
4
6
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
7
|
+
import { isStealthOwnedHeaderName, SDK_OWNED_CHROME_HEADER_PREFIX, } from "./runtime/stealth-owned-headers.js";
|
|
5
8
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
6
9
|
const requireModule = createRequire(import.meta.url);
|
|
7
10
|
// `typeof import(...)` keeps the type without emitting a static import: the
|
|
@@ -750,7 +753,14 @@ function lintSelfHostedBrowserPatterns(provider, options) {
|
|
|
750
753
|
}
|
|
751
754
|
return diagnostics;
|
|
752
755
|
}
|
|
753
|
-
|
|
756
|
+
// Captures the constructor name so each rule can pick its own scope: the
|
|
757
|
+
// undeclared-code rule keeps its historical ProviderError/ValidationError
|
|
758
|
+
// surface, while the localization rules also cover AuthError.
|
|
759
|
+
const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(ProviderError|AuthError|ValidationError)\s*\(/g;
|
|
760
|
+
const UNDECLARED_CODE_ERROR_CLASSES = new Set([
|
|
761
|
+
"ProviderError",
|
|
762
|
+
"ValidationError",
|
|
763
|
+
]);
|
|
754
764
|
const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
755
765
|
const RECORDED_FIXTURE_SOURCE_FILE_PATTERN = /(?:^|\/)__fixtures__(?:\/|$)|(?:^|\/)__tests__\/fixtures(?:\/|$)/;
|
|
756
766
|
const JAVASCRIPT_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
@@ -758,6 +768,11 @@ const VERSIONED_PROFILE_LITERAL_PATTERN = /\b(?:chrome|chromium|firefox|safari|e
|
|
|
758
768
|
const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
|
|
759
769
|
const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
|
|
760
770
|
const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
|
|
771
|
+
// Calls that write a header entry as (name, value): Headers/Map `set` and
|
|
772
|
+
// `append`, Node's `setHeader`. Predicates, replacements, and logging calls
|
|
773
|
+
// take string arguments too and must not count as header writes.
|
|
774
|
+
const HEADER_SETTER_CALLEE_PATTERN = /^(?:set|append|setHeader|addHeader)$/;
|
|
775
|
+
const OWNED_CLIENT_HINT_HEADER_PREFIX = "sec-ch-ua";
|
|
761
776
|
function staticStringText(node) {
|
|
762
777
|
if (!node)
|
|
763
778
|
return undefined;
|
|
@@ -778,11 +793,83 @@ function staticPropertyName(node) {
|
|
|
778
793
|
function isSecChUaHeaderName(value) {
|
|
779
794
|
return value?.toLowerCase() === "sec-ch-ua";
|
|
780
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* True for the code paths that reach the stealth transport: `ctx.stealth`,
|
|
798
|
+
* `ctx["stealth"]`, and `const { stealth } = ctx`. Read from the AST so a
|
|
799
|
+
* comment or string that mentions ctx.stealth does not scope a ctx.http file
|
|
800
|
+
* into this rule.
|
|
801
|
+
*/
|
|
802
|
+
function isStealthContextAccess(node) {
|
|
803
|
+
const ts = getTypeScript();
|
|
804
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
805
|
+
return (ts.isIdentifier(node.expression) &&
|
|
806
|
+
node.expression.text === "ctx" &&
|
|
807
|
+
node.name.text === "stealth");
|
|
808
|
+
}
|
|
809
|
+
if (ts.isElementAccessExpression(node)) {
|
|
810
|
+
return (ts.isIdentifier(node.expression) &&
|
|
811
|
+
node.expression.text === "ctx" &&
|
|
812
|
+
staticStringText(node.argumentExpression) === "stealth");
|
|
813
|
+
}
|
|
814
|
+
if (ts.isBindingElement(node)) {
|
|
815
|
+
const key = node.propertyName ?? node.name;
|
|
816
|
+
if (!ts.isIdentifier(key) || key.text !== "stealth")
|
|
817
|
+
return false;
|
|
818
|
+
const declaration = node.parent?.parent;
|
|
819
|
+
return (declaration !== undefined &&
|
|
820
|
+
ts.isVariableDeclaration(declaration) &&
|
|
821
|
+
declaration.initializer !== undefined &&
|
|
822
|
+
ts.isIdentifier(declaration.initializer) &&
|
|
823
|
+
declaration.initializer.text === "ctx");
|
|
824
|
+
}
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
function isStaticUndefined(node) {
|
|
828
|
+
if (!node)
|
|
829
|
+
return false;
|
|
830
|
+
const ts = getTypeScript();
|
|
831
|
+
return (ts.isIdentifier(node) && node.text === "undefined") || ts.isVoidExpression(node);
|
|
832
|
+
}
|
|
833
|
+
function calleeName(node) {
|
|
834
|
+
const ts = getTypeScript();
|
|
835
|
+
if (ts.isPropertyAccessExpression(node.expression))
|
|
836
|
+
return node.expression.name.text;
|
|
837
|
+
if (ts.isIdentifier(node.expression))
|
|
838
|
+
return node.expression.text;
|
|
839
|
+
return undefined;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Header names this rule reports in ctx.stealth files: the Sec-Fetch-* and
|
|
843
|
+
* client-hint (sec-ch-ua*) families, restricted to what the stealth runtime
|
|
844
|
+
* really rejects (isStealthOwnedHeaderName). Deliberately narrower than the
|
|
845
|
+
* runtime rejection: host, connection, and accept-encoding are left alone
|
|
846
|
+
* because ctx.http callers set them legitimately in the same files, and
|
|
847
|
+
* user-agent only reaches a finding through the "user-agent" kind, which
|
|
848
|
+
* matches a versioned literal value rather than the name — a user-agent read
|
|
849
|
+
* from a variable is rejected by the runtime and stays silent here. Widening
|
|
850
|
+
* the set needs a per-file transport scope and a suppression path first.
|
|
851
|
+
*/
|
|
852
|
+
function isReportedOwnedHeaderName(value) {
|
|
853
|
+
if (value === undefined)
|
|
854
|
+
return false;
|
|
855
|
+
const name = value.toLowerCase();
|
|
856
|
+
if (!isStealthOwnedHeaderName(name))
|
|
857
|
+
return false;
|
|
858
|
+
if (name.startsWith(SDK_OWNED_CHROME_HEADER_PREFIX)) {
|
|
859
|
+
return name.length > SDK_OWNED_CHROME_HEADER_PREFIX.length;
|
|
860
|
+
}
|
|
861
|
+
return name.startsWith(OWNED_CLIENT_HINT_HEADER_PREFIX);
|
|
862
|
+
}
|
|
781
863
|
function collectBrowserVersionLiteralFindings(source) {
|
|
782
864
|
const ts = getTypeScript();
|
|
783
865
|
const sourceFile = ts.createSourceFile("provider-source.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
784
|
-
|
|
866
|
+
let findings = [];
|
|
785
867
|
const seen = new Set();
|
|
868
|
+
// Owned-header entries only count once the file is known to reach
|
|
869
|
+
// ctx.stealth, which may appear after the entry; collect them and decide
|
|
870
|
+
// after the walk.
|
|
871
|
+
let stealthAccess = false;
|
|
872
|
+
const ownedHeaderEntries = [];
|
|
786
873
|
const addFinding = (kind, literal, position) => {
|
|
787
874
|
const key = `${kind}:${position}:${literal}`;
|
|
788
875
|
if (seen.has(key))
|
|
@@ -805,23 +892,73 @@ function collectBrowserVersionLiteralFindings(source) {
|
|
|
805
892
|
addFinding("sec-ch-ua", "sec-ch-ua", node.getStart(sourceFile));
|
|
806
893
|
}
|
|
807
894
|
};
|
|
895
|
+
// A header entry `name: value` in any of the shapes below. In a ctx.stealth
|
|
896
|
+
// file an owned name is the finding (the runtime rejects it whatever the
|
|
897
|
+
// value); elsewhere only a versioned sec-ch-ua value is. A statically
|
|
898
|
+
// undefined value is exempt only in the "record" shapes, the ones the
|
|
899
|
+
// runtime drops before the ownership check (`normalizedHeaderEntries`):
|
|
900
|
+
// `Headers.set(name, undefined)` stringifies to "undefined" and still
|
|
901
|
+
// throws, so it stays a finding.
|
|
902
|
+
const inspectHeaderEntry = (nameNode, name, valueNode, ownedHeaderShape) => {
|
|
903
|
+
if (name === undefined)
|
|
904
|
+
return;
|
|
905
|
+
if (ownedHeaderShape !== false &&
|
|
906
|
+
isReportedOwnedHeaderName(name) &&
|
|
907
|
+
!(ownedHeaderShape === "record" && isStaticUndefined(valueNode))) {
|
|
908
|
+
ownedHeaderEntries.push({
|
|
909
|
+
name,
|
|
910
|
+
position: nameNode.getStart(sourceFile),
|
|
911
|
+
valuePosition: valueNode?.getStart(sourceFile),
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
if (isSecChUaHeaderName(name))
|
|
915
|
+
inspectSecChUaValue(valueNode);
|
|
916
|
+
};
|
|
808
917
|
const visit = (node) => {
|
|
809
918
|
const text = staticStringText(node);
|
|
810
919
|
if (text !== undefined)
|
|
811
920
|
inspectLiteral(text, node.getStart(sourceFile));
|
|
812
|
-
if (
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
921
|
+
if (isStealthContextAccess(node))
|
|
922
|
+
stealthAccess = true;
|
|
923
|
+
if (ts.isPropertyAssignment(node)) {
|
|
924
|
+
inspectHeaderEntry(node.name, staticPropertyName(node.name), node.initializer, "record");
|
|
925
|
+
}
|
|
926
|
+
if (ts.isCallExpression(node) && node.arguments[0]) {
|
|
927
|
+
// Only `headers.set(name, value)`-style calls write a header;
|
|
928
|
+
// `name.startsWith("sec-fetch-")` or `log("sec-fetch-dest", x)` do not.
|
|
929
|
+
inspectHeaderEntry(node.arguments[0], staticStringText(node.arguments[0]), node.arguments[1], node.arguments.length >= 2 && HEADER_SETTER_CALLEE_PATTERN.test(calleeName(node) ?? "")
|
|
930
|
+
? "entry"
|
|
931
|
+
: false);
|
|
932
|
+
}
|
|
933
|
+
if (ts.isArrayLiteralExpression(node) && node.elements[0]) {
|
|
934
|
+
// Only a `[name, value]` tuple is a header. A name list such as
|
|
935
|
+
// `new Set(["sec-ch-ua-mobile", "sec-ch-ua-platform"])` is not: it is
|
|
936
|
+
// the argument of a constructor, or its second element is itself an
|
|
937
|
+
// owned name.
|
|
938
|
+
const second = node.elements[1];
|
|
939
|
+
inspectHeaderEntry(node.elements[0], staticStringText(node.elements[0]), second, node.elements.length === 2 &&
|
|
940
|
+
!ts.isNewExpression(node.parent) &&
|
|
941
|
+
!isStealthOwnedHeaderName(staticStringText(second) ?? "")
|
|
942
|
+
? "entry"
|
|
943
|
+
: false);
|
|
944
|
+
}
|
|
945
|
+
if (ts.isBinaryExpression(node) &&
|
|
946
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
947
|
+
ts.isElementAccessExpression(node.left)) {
|
|
948
|
+
const nameNode = node.left.argumentExpression;
|
|
949
|
+
inspectHeaderEntry(nameNode, staticStringText(nameNode), node.right, "record");
|
|
821
950
|
}
|
|
822
951
|
ts.forEachChild(node, visit);
|
|
823
952
|
};
|
|
824
953
|
visit(sourceFile);
|
|
954
|
+
if (stealthAccess) {
|
|
955
|
+
// The owned name is the finding; do not also report its sec-ch-ua value.
|
|
956
|
+
const shadowedValues = new Set(ownedHeaderEntries.map((entry) => entry.valuePosition));
|
|
957
|
+
findings = findings.filter((finding) => !(finding.kind === "sec-ch-ua" && shadowedValues.has(finding.position)));
|
|
958
|
+
for (const entry of ownedHeaderEntries) {
|
|
959
|
+
addFinding("owned-header", entry.name, entry.position);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
825
962
|
return findings.sort((left, right) => left.position - right.position);
|
|
826
963
|
}
|
|
827
964
|
function browserVersionLiteralMessage(finding) {
|
|
@@ -832,6 +969,8 @@ function browserVersionLiteralMessage(finding) {
|
|
|
832
969
|
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.`;
|
|
833
970
|
case "sec-ch-ua":
|
|
834
971
|
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.';
|
|
972
|
+
case "owned-header":
|
|
973
|
+
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.`;
|
|
835
974
|
}
|
|
836
975
|
}
|
|
837
976
|
function lintBrowserVersionLiterals(provider) {
|
|
@@ -959,14 +1098,14 @@ function extractBalancedCallArguments(source, startIndex) {
|
|
|
959
1098
|
return undefined;
|
|
960
1099
|
}
|
|
961
1100
|
/**
|
|
962
|
-
* Collects literal string values of top-level
|
|
963
|
-
* ProviderError/ValidationError options object. Only plain `"..."` /
|
|
964
|
-
* literals at options-object depth count; computed
|
|
1101
|
+
* Collects literal string values of the named top-level properties inside a
|
|
1102
|
+
* ProviderError/AuthError/ValidationError options object. Only plain `"..."` /
|
|
1103
|
+
* `'...'` literals at options-object depth count; computed values (identifiers,
|
|
965
1104
|
* ternaries, template substitutions, concatenations, escapes) are skipped
|
|
966
|
-
* silently so the
|
|
1105
|
+
* silently so the rules never guess. The first literal wins per property name.
|
|
967
1106
|
*/
|
|
968
|
-
function
|
|
969
|
-
const
|
|
1107
|
+
function collectLiteralErrorOptionValues(args, optionNames) {
|
|
1108
|
+
const collected = {};
|
|
970
1109
|
let braceDepth = 0;
|
|
971
1110
|
let parenDepth = 0;
|
|
972
1111
|
let bracketDepth = 0;
|
|
@@ -976,7 +1115,7 @@ function collectLiteralErrorCodeValues(args) {
|
|
|
976
1115
|
if (char === '"' || char === "'" || char === "`") {
|
|
977
1116
|
const end = skipStringLiteral(args, index);
|
|
978
1117
|
if (end < 0) {
|
|
979
|
-
return
|
|
1118
|
+
return collected;
|
|
980
1119
|
}
|
|
981
1120
|
index = end;
|
|
982
1121
|
previousSignificantChar = char;
|
|
@@ -985,7 +1124,7 @@ function collectLiteralErrorCodeValues(args) {
|
|
|
985
1124
|
if (char === "/" && args[index + 1] === "/") {
|
|
986
1125
|
const newline = args.indexOf("\n", index);
|
|
987
1126
|
if (newline === -1) {
|
|
988
|
-
return
|
|
1127
|
+
return collected;
|
|
989
1128
|
}
|
|
990
1129
|
index = newline;
|
|
991
1130
|
continue;
|
|
@@ -993,7 +1132,7 @@ function collectLiteralErrorCodeValues(args) {
|
|
|
993
1132
|
if (char === "/" && args[index + 1] === "*") {
|
|
994
1133
|
const end = args.indexOf("*/", index + 2);
|
|
995
1134
|
if (end === -1) {
|
|
996
|
-
return
|
|
1135
|
+
return collected;
|
|
997
1136
|
}
|
|
998
1137
|
index = end + 1;
|
|
999
1138
|
continue;
|
|
@@ -1022,9 +1161,13 @@ function collectLiteralErrorCodeValues(args) {
|
|
|
1022
1161
|
else if (braceDepth === 1 &&
|
|
1023
1162
|
parenDepth === 0 &&
|
|
1024
1163
|
bracketDepth === 0 &&
|
|
1025
|
-
(previousSignificantChar === "{" || previousSignificantChar === ",")
|
|
1026
|
-
|
|
1027
|
-
|
|
1164
|
+
(previousSignificantChar === "{" || previousSignificantChar === ",")) {
|
|
1165
|
+
const optionName = optionNames.find((name) => startsWithOptionName(args, index, name));
|
|
1166
|
+
if (optionName === undefined) {
|
|
1167
|
+
previousSignificantChar = char;
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
let cursor = index + optionName.length;
|
|
1028
1171
|
while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
|
|
1029
1172
|
cursor++;
|
|
1030
1173
|
}
|
|
@@ -1045,32 +1188,72 @@ function collectLiteralErrorCodeValues(args) {
|
|
|
1045
1188
|
const nextChar = after < args.length ? (args[after] ?? "") : "";
|
|
1046
1189
|
if (!value.includes("\\") &&
|
|
1047
1190
|
(nextChar === "," || nextChar === "}" || nextChar === "")) {
|
|
1048
|
-
|
|
1191
|
+
collected[optionName] ??= value;
|
|
1049
1192
|
}
|
|
1050
1193
|
index = end;
|
|
1051
1194
|
previousSignificantChar = quote;
|
|
1052
1195
|
continue;
|
|
1053
1196
|
}
|
|
1054
|
-
return
|
|
1197
|
+
return collected;
|
|
1055
1198
|
}
|
|
1056
1199
|
}
|
|
1057
1200
|
}
|
|
1058
1201
|
previousSignificantChar = char;
|
|
1059
1202
|
}
|
|
1060
|
-
return
|
|
1203
|
+
return collected;
|
|
1061
1204
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1205
|
+
/** True when `name` starts at `index` and is not the prefix of a longer identifier. */
|
|
1206
|
+
function startsWithOptionName(args, index, name) {
|
|
1207
|
+
if (!args.startsWith(name, index))
|
|
1208
|
+
return false;
|
|
1209
|
+
const next = args[index + name.length] ?? "";
|
|
1210
|
+
return !/[A-Za-z0-9_$]/.test(next);
|
|
1211
|
+
}
|
|
1212
|
+
const THROWN_ERROR_LOCALE_OPTION_NAMES = ["code", "messageKey", "fixKey"];
|
|
1213
|
+
function collectThrownErrorConstructionSites(source) {
|
|
1214
|
+
const sites = [];
|
|
1064
1215
|
THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = 0;
|
|
1065
1216
|
for (let match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source); match; match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source)) {
|
|
1066
1217
|
const argsStart = match.index + match[0].length;
|
|
1067
1218
|
const args = extractBalancedCallArguments(source, argsStart);
|
|
1068
1219
|
if (args !== undefined) {
|
|
1069
|
-
|
|
1220
|
+
const values = collectLiteralErrorOptionValues(args, THROWN_ERROR_LOCALE_OPTION_NAMES);
|
|
1221
|
+
sites.push({
|
|
1222
|
+
errorClass: match[1] ?? "",
|
|
1223
|
+
...(values.code === undefined ? {} : { code: values.code }),
|
|
1224
|
+
...(values.messageKey === undefined ? {} : { messageKey: values.messageKey }),
|
|
1225
|
+
...(values.fixKey === undefined ? {} : { fixKey: values.fixKey }),
|
|
1226
|
+
});
|
|
1070
1227
|
}
|
|
1071
1228
|
THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = argsStart;
|
|
1072
1229
|
}
|
|
1073
|
-
return
|
|
1230
|
+
return sites;
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* The provider sources a throw-site rule scans, with the diagnostic field path
|
|
1234
|
+
* each one reports under. Prefers the CLI-collected source files (whole-repo
|
|
1235
|
+
* coverage) and falls back to the handler/auth-flow function text available
|
|
1236
|
+
* when `lintProvider` is called without them. Test sources are excluded.
|
|
1237
|
+
*/
|
|
1238
|
+
function collectThrowSiteLintSources(provider) {
|
|
1239
|
+
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath));
|
|
1240
|
+
if (sourceFiles.length > 0) {
|
|
1241
|
+
return sourceFiles.map(([filePath, source]) => ({
|
|
1242
|
+
field: `sourceFiles.${filePath}`,
|
|
1243
|
+
source,
|
|
1244
|
+
}));
|
|
1245
|
+
}
|
|
1246
|
+
const sources = [];
|
|
1247
|
+
if (provider.authFlowSource) {
|
|
1248
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1249
|
+
}
|
|
1250
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1251
|
+
const source = getOperationSource(operation);
|
|
1252
|
+
if (source) {
|
|
1253
|
+
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return sources;
|
|
1074
1257
|
}
|
|
1075
1258
|
/**
|
|
1076
1259
|
* Static counterpart of the runtime `unregistered_provider_error_code`
|
|
@@ -1102,38 +1285,216 @@ function lintUndeclaredThrownErrorCodes(provider) {
|
|
|
1102
1285
|
}
|
|
1103
1286
|
}
|
|
1104
1287
|
}
|
|
1105
|
-
const sources =
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1288
|
+
const sources = collectThrowSiteLintSources(provider);
|
|
1289
|
+
const diagnostics = [];
|
|
1290
|
+
for (const { field, source } of sources) {
|
|
1291
|
+
const undeclaredCodes = new Set(collectThrownErrorConstructionSites(source)
|
|
1292
|
+
.filter((site) => UNDECLARED_CODE_ERROR_CLASSES.has(site.errorClass))
|
|
1293
|
+
.flatMap((site) => (site.code === undefined ? [] : [site.code]))
|
|
1294
|
+
.filter((code) => !knownCodes.has(code)));
|
|
1295
|
+
for (const code of undeclaredCodes) {
|
|
1296
|
+
diagnostics.push({
|
|
1297
|
+
rule: "thrown-error-code-undeclared",
|
|
1298
|
+
level: "warn",
|
|
1299
|
+
field,
|
|
1300
|
+
message: `Thrown error code "${code}" (${field}) is neither SDK-registered nor declared in any operation's errorCodes; at runtime it serves HTTP 500 and emits the unregistered_provider_error_code signal. Declare it in the owning operation's errorCodes with status and retryable.`,
|
|
1301
|
+
});
|
|
1110
1302
|
}
|
|
1111
1303
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1304
|
+
return diagnostics;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Flags an operation `errorCodes` entry whose declared `status` or `retryable`
|
|
1308
|
+
* contradicts the SDK's canonical resolution for that code.
|
|
1309
|
+
*
|
|
1310
|
+
* For a code the SDK status-maps but does not runtime-own (UPSTREAM_ERROR,
|
|
1311
|
+
* BLOCKED, and the fleet-consensus codes registered alongside them), the
|
|
1312
|
+
* declaration still wins at runtime — `toStatusCode` reads the declaration
|
|
1313
|
+
* before the canonical map. So registering a code never silently rewrites an
|
|
1314
|
+
* existing declaration's status, and it must not: that would change a served
|
|
1315
|
+
* status behind the author's back. What it does create is a fleet that answers
|
|
1316
|
+
* one code two ways. This rule is where that divergence surfaces, by name, with
|
|
1317
|
+
* the canonical value to converge on.
|
|
1318
|
+
*
|
|
1319
|
+
* Codes the SDK runtime-owns are handled separately: `defineProvider` already
|
|
1320
|
+
* warns there, because for those the declaration really is ignored.
|
|
1321
|
+
*
|
|
1322
|
+
* Warning level, deliberately: the divergent providers are live and converge on
|
|
1323
|
+
* their own schedule, so this must not fail `apifuse check`.
|
|
1324
|
+
*/
|
|
1325
|
+
function lintErrorCodeDeclarationConflicts(provider) {
|
|
1326
|
+
const diagnostics = [];
|
|
1327
|
+
const providerId = provider.id ?? "unknown";
|
|
1328
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1329
|
+
for (const [index, entry] of (operation.errorCodes ?? []).entries()) {
|
|
1330
|
+
if (typeof entry?.code !== "string")
|
|
1331
|
+
continue;
|
|
1332
|
+
// SDK-owned codes ignore the declaration outright; defineProvider owns
|
|
1333
|
+
// that message so authors are not told two different things.
|
|
1334
|
+
if (SDK_RUNTIME_OWNED_ERROR_CODES.has(entry.code))
|
|
1335
|
+
continue;
|
|
1336
|
+
const field = `operations.${operationKey}.errorCodes[${index}]`;
|
|
1337
|
+
const canonicalStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(entry.code);
|
|
1338
|
+
if (canonicalStatus !== undefined && entry.status !== undefined) {
|
|
1339
|
+
if (entry.status !== canonicalStatus) {
|
|
1340
|
+
diagnostics.push({
|
|
1341
|
+
rule: "error-code-status-conflicts-sdk",
|
|
1342
|
+
level: "warn",
|
|
1343
|
+
field: `${field}.status`,
|
|
1344
|
+
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.`,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const canonicalRetryable = SDK_CANONICAL_ERROR_CODE_RETRYABILITY.get(entry.code);
|
|
1349
|
+
if (canonicalRetryable !== undefined &&
|
|
1350
|
+
entry.retryable !== undefined &&
|
|
1351
|
+
entry.retryable !== canonicalRetryable) {
|
|
1352
|
+
diagnostics.push({
|
|
1353
|
+
rule: "error-code-retryable-conflicts-sdk",
|
|
1354
|
+
level: "warn",
|
|
1355
|
+
field: `${field}.retryable`,
|
|
1356
|
+
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.`,
|
|
1357
|
+
});
|
|
1120
1358
|
}
|
|
1121
1359
|
}
|
|
1122
1360
|
}
|
|
1361
|
+
return diagnostics;
|
|
1362
|
+
}
|
|
1363
|
+
function errorLocaleKeyStatus(catalog, key) {
|
|
1364
|
+
if (!isProviderLocaleKey(key))
|
|
1365
|
+
return "malformed";
|
|
1366
|
+
const value = getProviderLocaleSegments(catalog, key.split("."));
|
|
1367
|
+
return typeof value === "string" && value.trim().length > 0 ? "ok" : "missing";
|
|
1368
|
+
}
|
|
1369
|
+
function hasDerivedErrorCatalogText(catalog, code, field) {
|
|
1370
|
+
const value = getProviderLocaleSegments(catalog, [PROVIDER_ERROR_CATALOG_NAMESPACE, code, field]);
|
|
1371
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* True when a thrown code reaches the caller as a real provider error whose
|
|
1375
|
+
* text the declared/derived catalog path can actually reach.
|
|
1376
|
+
*
|
|
1377
|
+
* A code qualifies either because the provider declares it in an operation's
|
|
1378
|
+
* `errorCodes`, or because the SDK registers a canonical status for it. The
|
|
1379
|
+
* second arm matters because registering a code is precisely what lets a
|
|
1380
|
+
* provider *delete* the declaration — AUTHORING tells it to for
|
|
1381
|
+
* `UPSTREAM_AUTH_ERROR`, `UPSTREAM_SCHEMA_ERROR` and `INVALID_REQUEST` — and an
|
|
1382
|
+
* undeclared throw of a registered code is no longer the HTTP 500 that
|
|
1383
|
+
* `thrown-error-code-undeclared` reports. Without this arm the two rules leave
|
|
1384
|
+
* a silent hole exactly where the fleet is being told to converge: a 502
|
|
1385
|
+
* `UPSTREAM_SCHEMA_ERROR` served untranslated with nothing warning about it.
|
|
1386
|
+
*
|
|
1387
|
+
* Codes the SDK runtime-owns are excluded: `sdkOwnsErrorResolution` skips the
|
|
1388
|
+
* declaration and derived candidates for them at serve time, so the catalog
|
|
1389
|
+
* entry this rule asks for would never take effect.
|
|
1390
|
+
*/
|
|
1391
|
+
function isCatalogReachableThrownCode(code, declaredCodes) {
|
|
1392
|
+
if (declaredCodes.has(code))
|
|
1393
|
+
return true;
|
|
1394
|
+
return (!SDK_RUNTIME_OWNED_ERROR_CODES.has(code) && SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.has(code));
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Authoring rules for localized provider error text (see
|
|
1398
|
+
* `ProviderErrorOptions.messageKey`).
|
|
1399
|
+
*
|
|
1400
|
+
* - `error-locale-key-missing` / `error-locale-key-malformed` (**error**): a
|
|
1401
|
+
* literal `messageKey`/`fixKey`, whether on a throw site or on an
|
|
1402
|
+
* `errorCodes[]` declaration, that the provider's `en` catalog does not
|
|
1403
|
+
* resolve to non-empty text, or that is not a legal locale dot path. At
|
|
1404
|
+
* runtime such a key is silently skipped and the caller gets the English
|
|
1405
|
+
* literal, so nothing fails loudly without this rule.
|
|
1406
|
+
* - `thrown-error-message-not-localized` (**warn**): a throw site whose literal
|
|
1407
|
+
* `code` is declared in the provider's `errorCodes` — or SDK-registered, and
|
|
1408
|
+
* so servable without a declaration — but that carries no `messageKey`, no
|
|
1409
|
+
* declaration-level `messageKey`, and no derived `errors.<code>.message` in
|
|
1410
|
+
* `en`. Such an error is served untranslated to every caller. Warning while
|
|
1411
|
+
* the ~2,000 existing sites migrate; it is promoted to error per provider as
|
|
1412
|
+
* each migration wave lands.
|
|
1413
|
+
*
|
|
1414
|
+
* Both rules are skipped entirely when the caller did not supply
|
|
1415
|
+
* `localeCatalogEn`: without the catalog they could only guess.
|
|
1416
|
+
*/
|
|
1417
|
+
function lintErrorMessageLocalization(provider) {
|
|
1418
|
+
const catalog = provider.localeCatalogEn;
|
|
1419
|
+
if (!catalog)
|
|
1420
|
+
return [];
|
|
1123
1421
|
const diagnostics = [];
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1422
|
+
const declaredCodes = new Set();
|
|
1423
|
+
const codesWithDeclaredMessageKey = new Set();
|
|
1424
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1425
|
+
for (const entry of operation.errorCodes ?? []) {
|
|
1426
|
+
if (typeof entry?.code !== "string")
|
|
1427
|
+
continue;
|
|
1428
|
+
declaredCodes.add(entry.code);
|
|
1429
|
+
for (const keyField of ["messageKey", "fixKey"]) {
|
|
1430
|
+
const key = entry[keyField];
|
|
1431
|
+
if (typeof key !== "string")
|
|
1432
|
+
continue;
|
|
1433
|
+
if (keyField === "messageKey")
|
|
1434
|
+
codesWithDeclaredMessageKey.add(entry.code);
|
|
1435
|
+
const status = errorLocaleKeyStatus(catalog, key);
|
|
1436
|
+
if (status === "ok")
|
|
1437
|
+
continue;
|
|
1438
|
+
diagnostics.push(errorLocaleKeyDiagnostic(status, `operations.${operationKey}.errorCodes`, key, `errorCodes entry "${entry.code}" of operation "${operationKey}"`));
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
for (const { field, source } of collectThrowSiteLintSources(provider)) {
|
|
1443
|
+
const reportedKeys = new Set();
|
|
1444
|
+
const reportedCodes = new Set();
|
|
1445
|
+
for (const site of collectThrownErrorConstructionSites(source)) {
|
|
1446
|
+
for (const key of [site.messageKey, site.fixKey]) {
|
|
1447
|
+
if (key === undefined || reportedKeys.has(key))
|
|
1448
|
+
continue;
|
|
1449
|
+
const status = errorLocaleKeyStatus(catalog, key);
|
|
1450
|
+
if (status === "ok")
|
|
1451
|
+
continue;
|
|
1452
|
+
reportedKeys.add(key);
|
|
1453
|
+
diagnostics.push(errorLocaleKeyDiagnostic(status, field, key, `a thrown error in ${field}`));
|
|
1454
|
+
}
|
|
1455
|
+
const code = site.code;
|
|
1456
|
+
if (code === undefined || reportedCodes.has(code))
|
|
1457
|
+
continue;
|
|
1458
|
+
if (site.messageKey !== undefined)
|
|
1459
|
+
continue;
|
|
1460
|
+
if (!isCatalogReachableThrownCode(code, declaredCodes))
|
|
1461
|
+
continue;
|
|
1462
|
+
if (codesWithDeclaredMessageKey.has(code))
|
|
1463
|
+
continue;
|
|
1464
|
+
if (hasDerivedErrorCatalogText(catalog, code, "message"))
|
|
1465
|
+
continue;
|
|
1466
|
+
reportedCodes.add(code);
|
|
1467
|
+
const origin = declaredCodes.has(code)
|
|
1468
|
+
? `declared code "${code}"`
|
|
1469
|
+
: `SDK-registered code "${code}"`;
|
|
1470
|
+
const declarationClause = declaredCodes.has(code)
|
|
1471
|
+
? "its errorCodes declaration has none, and"
|
|
1472
|
+
: "no errorCodes declaration supplies one, and";
|
|
1127
1473
|
diagnostics.push({
|
|
1128
|
-
rule: "thrown-error-
|
|
1474
|
+
rule: "thrown-error-message-not-localized",
|
|
1129
1475
|
level: "warn",
|
|
1130
1476
|
field,
|
|
1131
|
-
message:
|
|
1477
|
+
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).`,
|
|
1132
1478
|
});
|
|
1133
1479
|
}
|
|
1134
1480
|
}
|
|
1135
1481
|
return diagnostics;
|
|
1136
1482
|
}
|
|
1483
|
+
function errorLocaleKeyDiagnostic(status, field, key, owner) {
|
|
1484
|
+
return status === "malformed"
|
|
1485
|
+
? {
|
|
1486
|
+
rule: "error-locale-key-malformed",
|
|
1487
|
+
level: "error",
|
|
1488
|
+
field,
|
|
1489
|
+
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.`,
|
|
1490
|
+
}
|
|
1491
|
+
: {
|
|
1492
|
+
rule: "error-locale-key-missing",
|
|
1493
|
+
level: "error",
|
|
1494
|
+
field,
|
|
1495
|
+
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.`,
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1137
1498
|
function collectHandleFields(schema, basePath, key = undefined, seen = new Set()) {
|
|
1138
1499
|
if (!isSchema(schema))
|
|
1139
1500
|
return [];
|
|
@@ -1204,14 +1565,15 @@ function readHandleKindDeclarations(value) {
|
|
|
1204
1565
|
return (typeof record.name === "string" &&
|
|
1205
1566
|
(record.type === "cursor" || record.type === "draft") &&
|
|
1206
1567
|
typeof record.fieldName === "string" &&
|
|
1207
|
-
(record.issuedBy === undefined ||
|
|
1568
|
+
(record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy)));
|
|
1208
1569
|
});
|
|
1209
1570
|
}
|
|
1210
1571
|
/**
|
|
1211
1572
|
* ADR-0012 handle rules: every `x-apifuse-handle` schema field must belong to
|
|
1212
1573
|
* a declared kind under its declared property key; every declared kind must be
|
|
1213
|
-
* both issued (some output) and accepted (some input); `issuedBy`
|
|
1214
|
-
* operation
|
|
1574
|
+
* both issued (some output) and accepted (some input); every `issuedBy` entry
|
|
1575
|
+
* must be an operation key and at least one of them must output the field; and
|
|
1576
|
+
* `ctx.handle` needs `state`.
|
|
1215
1577
|
*/
|
|
1216
1578
|
function lintHandleDeclarations(provider) {
|
|
1217
1579
|
const diagnostics = [];
|
|
@@ -1292,20 +1654,39 @@ function lintHandleDeclarations(provider) {
|
|
|
1292
1654
|
}
|
|
1293
1655
|
if (kind.issuedBy === undefined)
|
|
1294
1656
|
continue;
|
|
1295
|
-
|
|
1657
|
+
const issuers = handleIssuerList(kind.issuedBy);
|
|
1658
|
+
const known = issuers.filter((operationKey) => Object.hasOwn(operations, operationKey));
|
|
1659
|
+
for (const operationKey of issuers) {
|
|
1660
|
+
if (known.includes(operationKey))
|
|
1661
|
+
continue;
|
|
1296
1662
|
diagnostics.push({
|
|
1297
1663
|
rule: "handle-issued-by",
|
|
1298
1664
|
level: "error",
|
|
1299
1665
|
field,
|
|
1300
|
-
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${
|
|
1666
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${operationKey}", which is not an operation key.`,
|
|
1301
1667
|
});
|
|
1302
1668
|
}
|
|
1303
|
-
|
|
1669
|
+
if (known.length === 0)
|
|
1670
|
+
continue;
|
|
1671
|
+
const issuing = known.filter((operationKey) => outputKindsByOperation.get(operationKey)?.has(kind.name));
|
|
1672
|
+
if (issuing.length > 0)
|
|
1673
|
+
continue;
|
|
1674
|
+
if (known.length === 1) {
|
|
1675
|
+
diagnostics.push({
|
|
1676
|
+
rule: "handle-issued-by",
|
|
1677
|
+
level: "error",
|
|
1678
|
+
field,
|
|
1679
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind.fieldName}" handle field.`,
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
else {
|
|
1304
1683
|
diagnostics.push({
|
|
1305
1684
|
rule: "handle-issued-by",
|
|
1306
1685
|
level: "error",
|
|
1307
1686
|
field,
|
|
1308
|
-
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy
|
|
1687
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy ${known
|
|
1688
|
+
.map((operationKey) => `"${operationKey}"`)
|
|
1689
|
+
.join(", ")}, but none of those operations' outputs has a "${kind.fieldName}" handle field. At least one listed issuer must output it.`,
|
|
1309
1690
|
});
|
|
1310
1691
|
}
|
|
1311
1692
|
}
|
|
@@ -1486,6 +1867,8 @@ export function lintProviderWithInformation(provider, options = {}) {
|
|
|
1486
1867
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1487
1868
|
...lintBrowserVersionLiterals(provider),
|
|
1488
1869
|
...lintUndeclaredThrownErrorCodes(provider),
|
|
1870
|
+
...lintErrorCodeDeclarationConflicts(provider),
|
|
1871
|
+
...lintErrorMessageLocalization(provider),
|
|
1489
1872
|
...lintLegacyChoiceUsage(provider),
|
|
1490
1873
|
...lintHandleDeclarations(provider),
|
|
1491
1874
|
];
|