@apifuse/provider-sdk 2.2.0-beta.62 → 2.2.0-beta.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +367 -9
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -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 +187 -4
- package/bin/apifuse-record.ts +44 -32
- package/bin/apifuse-submit-check.ts +9 -5
- 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 +36 -1
- package/dist/handle-meta.js +16 -0
- package/dist/handle.d.ts +23 -5
- package/dist/handle.js +54 -8
- package/dist/health-scenario.d.ts +38 -0
- package/dist/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 +11 -3
- package/dist/index.js +9 -1
- package/dist/lint.d.ts +15 -0
- package/dist/lint.js +469 -82
- package/dist/provider.d.ts +6 -6
- package/dist/provider.js +3 -3
- 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 +9 -1
- package/dist/runtime/http-telemetry-guard.d.ts +10 -0
- package/dist/runtime/http-telemetry-guard.js +62 -10
- package/dist/runtime/http.js +141 -35
- 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 +35 -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/runtime-boundary-lint.d.ts +132 -0
- package/dist/runtime-boundary-lint.js +586 -0
- 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 +7 -0
- package/dist/server/index.js +4 -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 +51 -2
- package/dist/server/serve-implementation.js +337 -90
- 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 +123 -1
- package/dist/typescript-module.d.ts +39 -0
- package/dist/typescript-module.js +72 -0
- 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 +51 -1
- package/src/handle.ts +92 -10
- package/src/health-scenario.ts +38 -0
- 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 +86 -0
- package/src/lint.ts +604 -91
- package/src/provider.ts +12 -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 +9 -1
- package/src/runtime/http-telemetry-guard.ts +66 -13
- package/src/runtime/http.ts +168 -37
- package/src/runtime/instrumentation.ts +105 -16
- package/src/runtime/ocr-telemetry.ts +308 -0
- package/src/runtime/ocr.ts +114 -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/runtime-boundary-lint.ts +703 -0
- package/src/server/error-stack-frames.ts +214 -0
- package/src/server/index.ts +53 -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 +569 -108
- 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 +128 -1
- package/src/typescript-module.ts +100 -0
- 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,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { SDK_CANONICAL_ERROR_CODE_RETRYABILITY, SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "./error-resolution.js";
|
|
2
|
+
import { APIFUSE_HANDLE_META_KEY, handleFieldDescription, handleFieldNameFor, handleHasDirectionalFieldNames, handleIssuerList, isHandleFieldMeta, isHandleIssuedBy, } from "./handle-meta.js";
|
|
3
|
+
import { PROVIDER_ERROR_CATALOG_NAMESPACE } from "./i18n/error-messages.js";
|
|
4
|
+
import { getProviderLocaleSegments, isProviderLocaleKey, } from "./i18n/keys.js";
|
|
4
5
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
6
|
+
import { isStealthOwnedHeaderName, SDK_OWNED_CHROME_HEADER_PREFIX, } from "./runtime/stealth-owned-headers.js";
|
|
7
|
+
import { lintRuntimeBoundary, scriptKindForSourceFile } from "./runtime-boundary-lint.js";
|
|
5
8
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
6
|
-
|
|
7
|
-
// `typeof import(...)` keeps the type without emitting a static import: the
|
|
8
|
-
// typescript package is a CLI-only dependency and src/lint.ts is production
|
|
9
|
-
// runtime, which the typescript-import-boundary test enforces.
|
|
10
|
-
let typeScriptModule;
|
|
11
|
-
function getTypeScript() {
|
|
12
|
-
typeScriptModule ??= requireModule("typescript");
|
|
13
|
-
return typeScriptModule;
|
|
14
|
-
}
|
|
9
|
+
import { getTypeScript } from "./typescript-module.js";
|
|
15
10
|
const CREDENTIAL_BEARING_AUTH_MODES = [
|
|
16
11
|
"credentials",
|
|
17
12
|
"oauth2",
|
|
@@ -750,7 +745,14 @@ function lintSelfHostedBrowserPatterns(provider, options) {
|
|
|
750
745
|
}
|
|
751
746
|
return diagnostics;
|
|
752
747
|
}
|
|
753
|
-
|
|
748
|
+
// Captures the constructor name so each rule can pick its own scope: the
|
|
749
|
+
// undeclared-code rule keeps its historical ProviderError/ValidationError
|
|
750
|
+
// surface, while the localization rules also cover AuthError.
|
|
751
|
+
const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(ProviderError|AuthError|ValidationError)\s*\(/g;
|
|
752
|
+
const UNDECLARED_CODE_ERROR_CLASSES = new Set([
|
|
753
|
+
"ProviderError",
|
|
754
|
+
"ValidationError",
|
|
755
|
+
]);
|
|
754
756
|
const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
755
757
|
const RECORDED_FIXTURE_SOURCE_FILE_PATTERN = /(?:^|\/)__fixtures__(?:\/|$)|(?:^|\/)__tests__\/fixtures(?:\/|$)/;
|
|
756
758
|
const JAVASCRIPT_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
@@ -758,6 +760,11 @@ const VERSIONED_PROFILE_LITERAL_PATTERN = /\b(?:chrome|chromium|firefox|safari|e
|
|
|
758
760
|
const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
|
|
759
761
|
const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
|
|
760
762
|
const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
|
|
763
|
+
// Calls that write a header entry as (name, value): Headers/Map `set` and
|
|
764
|
+
// `append`, Node's `setHeader`. Predicates, replacements, and logging calls
|
|
765
|
+
// take string arguments too and must not count as header writes.
|
|
766
|
+
const HEADER_SETTER_CALLEE_PATTERN = /^(?:set|append|setHeader|addHeader)$/;
|
|
767
|
+
const OWNED_CLIENT_HINT_HEADER_PREFIX = "sec-ch-ua";
|
|
761
768
|
function staticStringText(node) {
|
|
762
769
|
if (!node)
|
|
763
770
|
return undefined;
|
|
@@ -778,11 +785,83 @@ function staticPropertyName(node) {
|
|
|
778
785
|
function isSecChUaHeaderName(value) {
|
|
779
786
|
return value?.toLowerCase() === "sec-ch-ua";
|
|
780
787
|
}
|
|
781
|
-
|
|
788
|
+
/**
|
|
789
|
+
* True for the code paths that reach the stealth transport: `ctx.stealth`,
|
|
790
|
+
* `ctx["stealth"]`, and `const { stealth } = ctx`. Read from the AST so a
|
|
791
|
+
* comment or string that mentions ctx.stealth does not scope a ctx.http file
|
|
792
|
+
* into this rule.
|
|
793
|
+
*/
|
|
794
|
+
function isStealthContextAccess(node) {
|
|
795
|
+
const ts = getTypeScript();
|
|
796
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
797
|
+
return (ts.isIdentifier(node.expression) &&
|
|
798
|
+
node.expression.text === "ctx" &&
|
|
799
|
+
node.name.text === "stealth");
|
|
800
|
+
}
|
|
801
|
+
if (ts.isElementAccessExpression(node)) {
|
|
802
|
+
return (ts.isIdentifier(node.expression) &&
|
|
803
|
+
node.expression.text === "ctx" &&
|
|
804
|
+
staticStringText(node.argumentExpression) === "stealth");
|
|
805
|
+
}
|
|
806
|
+
if (ts.isBindingElement(node)) {
|
|
807
|
+
const key = node.propertyName ?? node.name;
|
|
808
|
+
if (!ts.isIdentifier(key) || key.text !== "stealth")
|
|
809
|
+
return false;
|
|
810
|
+
const declaration = node.parent?.parent;
|
|
811
|
+
return (declaration !== undefined &&
|
|
812
|
+
ts.isVariableDeclaration(declaration) &&
|
|
813
|
+
declaration.initializer !== undefined &&
|
|
814
|
+
ts.isIdentifier(declaration.initializer) &&
|
|
815
|
+
declaration.initializer.text === "ctx");
|
|
816
|
+
}
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
function isStaticUndefined(node) {
|
|
820
|
+
if (!node)
|
|
821
|
+
return false;
|
|
822
|
+
const ts = getTypeScript();
|
|
823
|
+
return (ts.isIdentifier(node) && node.text === "undefined") || ts.isVoidExpression(node);
|
|
824
|
+
}
|
|
825
|
+
function calleeName(node) {
|
|
782
826
|
const ts = getTypeScript();
|
|
783
|
-
|
|
784
|
-
|
|
827
|
+
if (ts.isPropertyAccessExpression(node.expression))
|
|
828
|
+
return node.expression.name.text;
|
|
829
|
+
if (ts.isIdentifier(node.expression))
|
|
830
|
+
return node.expression.text;
|
|
831
|
+
return undefined;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Header names this rule reports in ctx.stealth files: the Sec-Fetch-* and
|
|
835
|
+
* client-hint (sec-ch-ua*) families, restricted to what the stealth runtime
|
|
836
|
+
* really rejects (isStealthOwnedHeaderName). Deliberately narrower than the
|
|
837
|
+
* runtime rejection: host, connection, and accept-encoding are left alone
|
|
838
|
+
* because ctx.http callers set them legitimately in the same files, and
|
|
839
|
+
* user-agent only reaches a finding through the "user-agent" kind, which
|
|
840
|
+
* matches a versioned literal value rather than the name — a user-agent read
|
|
841
|
+
* from a variable is rejected by the runtime and stays silent here. Widening
|
|
842
|
+
* the set needs a per-file transport scope and a suppression path first.
|
|
843
|
+
*/
|
|
844
|
+
function isReportedOwnedHeaderName(value) {
|
|
845
|
+
if (value === undefined)
|
|
846
|
+
return false;
|
|
847
|
+
const name = value.toLowerCase();
|
|
848
|
+
if (!isStealthOwnedHeaderName(name))
|
|
849
|
+
return false;
|
|
850
|
+
if (name.startsWith(SDK_OWNED_CHROME_HEADER_PREFIX)) {
|
|
851
|
+
return name.length > SDK_OWNED_CHROME_HEADER_PREFIX.length;
|
|
852
|
+
}
|
|
853
|
+
return name.startsWith(OWNED_CLIENT_HINT_HEADER_PREFIX);
|
|
854
|
+
}
|
|
855
|
+
function collectBrowserVersionLiteralFindings(fileName, source) {
|
|
856
|
+
const ts = getTypeScript();
|
|
857
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKindForSourceFile(ts, fileName));
|
|
858
|
+
let findings = [];
|
|
785
859
|
const seen = new Set();
|
|
860
|
+
// Owned-header entries only count once the file is known to reach
|
|
861
|
+
// ctx.stealth, which may appear after the entry; collect them and decide
|
|
862
|
+
// after the walk.
|
|
863
|
+
let stealthAccess = false;
|
|
864
|
+
const ownedHeaderEntries = [];
|
|
786
865
|
const addFinding = (kind, literal, position) => {
|
|
787
866
|
const key = `${kind}:${position}:${literal}`;
|
|
788
867
|
if (seen.has(key))
|
|
@@ -805,23 +884,73 @@ function collectBrowserVersionLiteralFindings(source) {
|
|
|
805
884
|
addFinding("sec-ch-ua", "sec-ch-ua", node.getStart(sourceFile));
|
|
806
885
|
}
|
|
807
886
|
};
|
|
887
|
+
// A header entry `name: value` in any of the shapes below. In a ctx.stealth
|
|
888
|
+
// file an owned name is the finding (the runtime rejects it whatever the
|
|
889
|
+
// value); elsewhere only a versioned sec-ch-ua value is. A statically
|
|
890
|
+
// undefined value is exempt only in the "record" shapes, the ones the
|
|
891
|
+
// runtime drops before the ownership check (`normalizedHeaderEntries`):
|
|
892
|
+
// `Headers.set(name, undefined)` stringifies to "undefined" and still
|
|
893
|
+
// throws, so it stays a finding.
|
|
894
|
+
const inspectHeaderEntry = (nameNode, name, valueNode, ownedHeaderShape) => {
|
|
895
|
+
if (name === undefined)
|
|
896
|
+
return;
|
|
897
|
+
if (ownedHeaderShape !== false &&
|
|
898
|
+
isReportedOwnedHeaderName(name) &&
|
|
899
|
+
!(ownedHeaderShape === "record" && isStaticUndefined(valueNode))) {
|
|
900
|
+
ownedHeaderEntries.push({
|
|
901
|
+
name,
|
|
902
|
+
position: nameNode.getStart(sourceFile),
|
|
903
|
+
valuePosition: valueNode?.getStart(sourceFile),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
if (isSecChUaHeaderName(name))
|
|
907
|
+
inspectSecChUaValue(valueNode);
|
|
908
|
+
};
|
|
808
909
|
const visit = (node) => {
|
|
809
910
|
const text = staticStringText(node);
|
|
810
911
|
if (text !== undefined)
|
|
811
912
|
inspectLiteral(text, node.getStart(sourceFile));
|
|
812
|
-
if (
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
913
|
+
if (isStealthContextAccess(node))
|
|
914
|
+
stealthAccess = true;
|
|
915
|
+
if (ts.isPropertyAssignment(node)) {
|
|
916
|
+
inspectHeaderEntry(node.name, staticPropertyName(node.name), node.initializer, "record");
|
|
917
|
+
}
|
|
918
|
+
if (ts.isCallExpression(node) && node.arguments[0]) {
|
|
919
|
+
// Only `headers.set(name, value)`-style calls write a header;
|
|
920
|
+
// `name.startsWith("sec-fetch-")` or `log("sec-fetch-dest", x)` do not.
|
|
921
|
+
inspectHeaderEntry(node.arguments[0], staticStringText(node.arguments[0]), node.arguments[1], node.arguments.length >= 2 && HEADER_SETTER_CALLEE_PATTERN.test(calleeName(node) ?? "")
|
|
922
|
+
? "entry"
|
|
923
|
+
: false);
|
|
924
|
+
}
|
|
925
|
+
if (ts.isArrayLiteralExpression(node) && node.elements[0]) {
|
|
926
|
+
// Only a `[name, value]` tuple is a header. A name list such as
|
|
927
|
+
// `new Set(["sec-ch-ua-mobile", "sec-ch-ua-platform"])` is not: it is
|
|
928
|
+
// the argument of a constructor, or its second element is itself an
|
|
929
|
+
// owned name.
|
|
930
|
+
const second = node.elements[1];
|
|
931
|
+
inspectHeaderEntry(node.elements[0], staticStringText(node.elements[0]), second, node.elements.length === 2 &&
|
|
932
|
+
!ts.isNewExpression(node.parent) &&
|
|
933
|
+
!isStealthOwnedHeaderName(staticStringText(second) ?? "")
|
|
934
|
+
? "entry"
|
|
935
|
+
: false);
|
|
936
|
+
}
|
|
937
|
+
if (ts.isBinaryExpression(node) &&
|
|
938
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
939
|
+
ts.isElementAccessExpression(node.left)) {
|
|
940
|
+
const nameNode = node.left.argumentExpression;
|
|
941
|
+
inspectHeaderEntry(nameNode, staticStringText(nameNode), node.right, "record");
|
|
821
942
|
}
|
|
822
943
|
ts.forEachChild(node, visit);
|
|
823
944
|
};
|
|
824
945
|
visit(sourceFile);
|
|
946
|
+
if (stealthAccess) {
|
|
947
|
+
// The owned name is the finding; do not also report its sec-ch-ua value.
|
|
948
|
+
const shadowedValues = new Set(ownedHeaderEntries.map((entry) => entry.valuePosition));
|
|
949
|
+
findings = findings.filter((finding) => !(finding.kind === "sec-ch-ua" && shadowedValues.has(finding.position)));
|
|
950
|
+
for (const entry of ownedHeaderEntries) {
|
|
951
|
+
addFinding("owned-header", entry.name, entry.position);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
825
954
|
return findings.sort((left, right) => left.position - right.position);
|
|
826
955
|
}
|
|
827
956
|
function browserVersionLiteralMessage(finding) {
|
|
@@ -832,6 +961,8 @@ function browserVersionLiteralMessage(finding) {
|
|
|
832
961
|
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
962
|
case "sec-ch-ua":
|
|
834
963
|
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.';
|
|
964
|
+
case "owned-header":
|
|
965
|
+
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
966
|
}
|
|
836
967
|
}
|
|
837
968
|
function lintBrowserVersionLiterals(provider) {
|
|
@@ -841,19 +972,27 @@ function lintBrowserVersionLiterals(provider) {
|
|
|
841
972
|
!RECORDED_FIXTURE_SOURCE_FILE_PATTERN.test(filePath));
|
|
842
973
|
if (sourceFiles.length > 0) {
|
|
843
974
|
for (const [filePath, source] of sourceFiles) {
|
|
844
|
-
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
975
|
+
sources.push({ field: `sourceFiles.${filePath}`, fileName: filePath, source });
|
|
845
976
|
}
|
|
846
977
|
}
|
|
847
978
|
else {
|
|
848
979
|
if (provider.authFlowSource)
|
|
849
|
-
sources.push({
|
|
980
|
+
sources.push({
|
|
981
|
+
field: "auth.flow",
|
|
982
|
+
fileName: "auth-flow.ts",
|
|
983
|
+
source: provider.authFlowSource,
|
|
984
|
+
});
|
|
850
985
|
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
851
986
|
const source = getOperationSource(operation);
|
|
852
987
|
if (source)
|
|
853
|
-
sources.push({
|
|
988
|
+
sources.push({
|
|
989
|
+
field: `operations.${operationKey}.handler`,
|
|
990
|
+
fileName: `${operationKey}.ts`,
|
|
991
|
+
source,
|
|
992
|
+
});
|
|
854
993
|
}
|
|
855
994
|
}
|
|
856
|
-
return sources.flatMap(({ field, source }) => collectBrowserVersionLiteralFindings(source).map((finding) => ({
|
|
995
|
+
return sources.flatMap(({ field, fileName, source }) => collectBrowserVersionLiteralFindings(fileName, source).map((finding) => ({
|
|
857
996
|
rule: "browser-version-literal",
|
|
858
997
|
level: "error",
|
|
859
998
|
field,
|
|
@@ -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,6 +1565,7 @@ function readHandleKindDeclarations(value) {
|
|
|
1204
1565
|
return (typeof record.name === "string" &&
|
|
1205
1566
|
(record.type === "cursor" || record.type === "draft") &&
|
|
1206
1567
|
typeof record.fieldName === "string" &&
|
|
1568
|
+
(record.outputFieldName === undefined || typeof record.outputFieldName === "string") &&
|
|
1207
1569
|
(record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy)));
|
|
1208
1570
|
});
|
|
1209
1571
|
}
|
|
@@ -1239,7 +1601,13 @@ function lintHandleDeclarations(provider) {
|
|
|
1239
1601
|
for (const { side, fields } of positions) {
|
|
1240
1602
|
for (const occurrence of fields) {
|
|
1241
1603
|
const field = `operations.${operationKey}.${occurrence.path}`;
|
|
1242
|
-
const { kind
|
|
1604
|
+
const { kind } = occurrence.meta;
|
|
1605
|
+
// A kind may name one handle differently in each direction (the canon's
|
|
1606
|
+
// `cursor` in, `next_cursor` out). The rule is still exact: each side
|
|
1607
|
+
// must use the key declared for THAT side, so the handle's identity
|
|
1608
|
+
// stays readable from the property key and the round trip has exactly
|
|
1609
|
+
// one edge — read `output`, send back under `input`.
|
|
1610
|
+
const expected = handleFieldNameFor(occurrence.meta, side);
|
|
1243
1611
|
if (!kindByName.has(kind)) {
|
|
1244
1612
|
diagnostics.push({
|
|
1245
1613
|
rule: "handle-kind-undeclared",
|
|
@@ -1248,12 +1616,16 @@ function lintHandleDeclarations(provider) {
|
|
|
1248
1616
|
message: `${providerLabel} operation "${operationKey}" ${side} field "${occurrence.path}" is a handle of kind "${kind}", which is not declared in provider.handle. Add the kind to handle: [...] or remove the field.`,
|
|
1249
1617
|
});
|
|
1250
1618
|
}
|
|
1251
|
-
if (occurrence.key !==
|
|
1619
|
+
if (occurrence.key !== expected) {
|
|
1620
|
+
const directional = handleHasDirectionalFieldNames(occurrence.meta);
|
|
1621
|
+
const advice = directional
|
|
1622
|
+
? `Use the kind's ${side} fieldName on every ${side} schema; its ${side === "input" ? "output" : "input"} fieldName is "${handleFieldNameFor(occurrence.meta, side === "input" ? "output" : "input")}".`
|
|
1623
|
+
: "Use the same key in every schema so the LLM sees one name.";
|
|
1252
1624
|
diagnostics.push({
|
|
1253
1625
|
rule: "handle-field-name",
|
|
1254
1626
|
level: "error",
|
|
1255
1627
|
field,
|
|
1256
|
-
message: `${providerLabel} operation "${operationKey}" ${side} declares handle kind "${kind}" under property "${occurrence.key}" but the kind's fieldName is "${
|
|
1628
|
+
message: `${providerLabel} operation "${operationKey}" ${side} declares handle kind "${kind}" under property "${occurrence.key}" but the kind's ${directional ? `${side} ` : ""}fieldName is "${expected}". ${advice}`,
|
|
1257
1629
|
});
|
|
1258
1630
|
}
|
|
1259
1631
|
if (side === "input") {
|
|
@@ -1280,7 +1652,7 @@ function lintHandleDeclarations(provider) {
|
|
|
1280
1652
|
rule: "handle-field-parity",
|
|
1281
1653
|
level: "error",
|
|
1282
1654
|
field,
|
|
1283
|
-
message: `${providerLabel} handle kind "${kind.name}" is never issued: no operation output contains its field "${kind
|
|
1655
|
+
message: `${providerLabel} handle kind "${kind.name}" is never issued: no operation output contains its field "${handleFieldNameFor(kind, "output")}". Add ${kind.name}.field() to the issuing operation's output.`,
|
|
1284
1656
|
});
|
|
1285
1657
|
}
|
|
1286
1658
|
if (!accepted) {
|
|
@@ -1288,7 +1660,7 @@ function lintHandleDeclarations(provider) {
|
|
|
1288
1660
|
rule: "handle-field-parity",
|
|
1289
1661
|
level: "error",
|
|
1290
1662
|
field,
|
|
1291
|
-
message: `${providerLabel} handle kind "${kind.name}" is never accepted: no operation input contains its field "${kind
|
|
1663
|
+
message: `${providerLabel} handle kind "${kind.name}" is never accepted: no operation input contains its field "${handleFieldNameFor(kind, "input")}". Add ${kind.name}.field() to the consuming operation's input.`,
|
|
1292
1664
|
});
|
|
1293
1665
|
}
|
|
1294
1666
|
if (kind.issuedBy === undefined)
|
|
@@ -1315,7 +1687,7 @@ function lintHandleDeclarations(provider) {
|
|
|
1315
1687
|
rule: "handle-issued-by",
|
|
1316
1688
|
level: "error",
|
|
1317
1689
|
field,
|
|
1318
|
-
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind
|
|
1690
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${handleFieldNameFor(kind, "output")}" handle field.`,
|
|
1319
1691
|
});
|
|
1320
1692
|
}
|
|
1321
1693
|
else {
|
|
@@ -1325,7 +1697,7 @@ function lintHandleDeclarations(provider) {
|
|
|
1325
1697
|
field,
|
|
1326
1698
|
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy ${known
|
|
1327
1699
|
.map((operationKey) => `"${operationKey}"`)
|
|
1328
|
-
.join(", ")}, but none of those operations' outputs has a "${kind
|
|
1700
|
+
.join(", ")}, but none of those operations' outputs has a "${handleFieldNameFor(kind, "output")}" handle field. At least one listed issuer must output it.`,
|
|
1329
1701
|
});
|
|
1330
1702
|
}
|
|
1331
1703
|
}
|
|
@@ -1350,7 +1722,7 @@ const LEGACY_CHOICE_IDENTIFIERS = new Set([
|
|
|
1350
1722
|
* the provider) while code inside `${}` interpolations still does. Falls back
|
|
1351
1723
|
* to a raw-source regex when TypeScript is not installed.
|
|
1352
1724
|
*/
|
|
1353
|
-
function findLegacyChoiceUsage(source) {
|
|
1725
|
+
function findLegacyChoiceUsage(fileName, source) {
|
|
1354
1726
|
let ts;
|
|
1355
1727
|
try {
|
|
1356
1728
|
ts = getTypeScript();
|
|
@@ -1358,7 +1730,7 @@ function findLegacyChoiceUsage(source) {
|
|
|
1358
1730
|
catch {
|
|
1359
1731
|
return LEGACY_CHOICE_USAGE_PATTERN.exec(source)?.[0];
|
|
1360
1732
|
}
|
|
1361
|
-
const file = ts.createSourceFile(
|
|
1733
|
+
const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, false, scriptKindForSourceFile(ts, fileName));
|
|
1362
1734
|
let found;
|
|
1363
1735
|
const visit = (node) => {
|
|
1364
1736
|
if (found)
|
|
@@ -1382,19 +1754,23 @@ function findLegacyChoiceUsage(source) {
|
|
|
1382
1754
|
function lintLegacyChoiceUsage(provider) {
|
|
1383
1755
|
const sources = [];
|
|
1384
1756
|
if (provider.authFlowSource) {
|
|
1385
|
-
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1757
|
+
sources.push({ field: "auth.flow", fileName: "auth-flow.ts", source: provider.authFlowSource });
|
|
1386
1758
|
}
|
|
1387
1759
|
for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
|
|
1388
|
-
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
1760
|
+
sources.push({ field: `sourceFiles.${filePath}`, fileName: filePath, source });
|
|
1389
1761
|
}
|
|
1390
1762
|
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1391
1763
|
const source = getOperationSource(operation);
|
|
1392
1764
|
if (source) {
|
|
1393
|
-
sources.push({
|
|
1765
|
+
sources.push({
|
|
1766
|
+
field: `operations.${operationKey}.handler`,
|
|
1767
|
+
fileName: `${operationKey}.ts`,
|
|
1768
|
+
source,
|
|
1769
|
+
});
|
|
1394
1770
|
}
|
|
1395
1771
|
}
|
|
1396
|
-
return sources.flatMap(({ field, source }) => {
|
|
1397
|
-
const match = findLegacyChoiceUsage(source);
|
|
1772
|
+
return sources.flatMap(({ field, fileName, source }) => {
|
|
1773
|
+
const match = findLegacyChoiceUsage(fileName, source);
|
|
1398
1774
|
if (!match)
|
|
1399
1775
|
return [];
|
|
1400
1776
|
return [
|
|
@@ -1506,9 +1882,15 @@ export function lintProviderWithInformation(provider, options = {}) {
|
|
|
1506
1882
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1507
1883
|
...lintBrowserVersionLiterals(provider),
|
|
1508
1884
|
...lintUndeclaredThrownErrorCodes(provider),
|
|
1885
|
+
...lintErrorCodeDeclarationConflicts(provider),
|
|
1886
|
+
...lintErrorMessageLocalization(provider),
|
|
1509
1887
|
...lintLegacyChoiceUsage(provider),
|
|
1510
1888
|
...lintHandleDeclarations(provider),
|
|
1511
1889
|
];
|
|
1890
|
+
// Runtime boundary rules (process.env, node:fs/net/child_process, raw
|
|
1891
|
+
// fetch) also yield information entries for `@apifuse-allow` acknowledgements.
|
|
1892
|
+
const runtimeBoundary = lintRuntimeBoundary(getTypeScript(), provider, getOperationSource);
|
|
1893
|
+
diagnostics.push(...runtimeBoundary.diagnostics);
|
|
1512
1894
|
if (provider.operations) {
|
|
1513
1895
|
const authMode = provider.auth?.mode;
|
|
1514
1896
|
for (const [operationKey, operation] of Object.entries(provider.operations)) {
|
|
@@ -1548,7 +1930,7 @@ export function lintProviderWithInformation(provider, options = {}) {
|
|
|
1548
1930
|
}
|
|
1549
1931
|
}
|
|
1550
1932
|
if (!provider.operations) {
|
|
1551
|
-
return applyPinnedWireFieldPaths(provider, diagnostics);
|
|
1933
|
+
return withInformation(applyPinnedWireFieldPaths(provider, diagnostics), runtimeBoundary.information);
|
|
1552
1934
|
}
|
|
1553
1935
|
diagnostics.push(...Object.entries(provider.operations).flatMap(([operationKey, operation]) => [
|
|
1554
1936
|
...lintOperation({
|
|
@@ -1567,5 +1949,10 @@ export function lintProviderWithInformation(provider, options = {}) {
|
|
|
1567
1949
|
: `operations.${operationKey}`,
|
|
1568
1950
|
message: `[${operationKey}] ${diagnostic.message}`,
|
|
1569
1951
|
}))));
|
|
1570
|
-
return applyPinnedWireFieldPaths(provider, diagnostics);
|
|
1952
|
+
return withInformation(applyPinnedWireFieldPaths(provider, diagnostics), runtimeBoundary.information);
|
|
1953
|
+
}
|
|
1954
|
+
function withInformation(result, information) {
|
|
1955
|
+
if (information.length === 0)
|
|
1956
|
+
return result;
|
|
1957
|
+
return { diagnostics: result.diagnostics, information: [...information, ...result.information] };
|
|
1571
1958
|
}
|