@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.50
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 +537 -33
- package/CHANGELOG.md +201 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1178 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +179 -16
- package/dist/config/loader.js +435 -127
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +43 -10
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +9 -4
- package/dist/stealth/profiles.js +160 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1654 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +690 -163
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +202 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/lint.js
CHANGED
|
@@ -1,6 +1,109 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "./error-resolution.js";
|
|
1
3
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
2
4
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
5
|
+
const requireModule = createRequire(import.meta.url);
|
|
6
|
+
// `typeof import(...)` keeps the type without emitting a static import: the
|
|
7
|
+
// typescript package is a CLI-only dependency and src/lint.ts is production
|
|
8
|
+
// runtime, which the typescript-import-boundary test enforces.
|
|
9
|
+
let typeScriptModule;
|
|
10
|
+
function getTypeScript() {
|
|
11
|
+
typeScriptModule ??= requireModule("typescript");
|
|
12
|
+
return typeScriptModule;
|
|
13
|
+
}
|
|
14
|
+
const CREDENTIAL_BEARING_AUTH_MODES = [
|
|
15
|
+
"credentials",
|
|
16
|
+
"oauth2",
|
|
17
|
+
"oauth2_proxied",
|
|
18
|
+
];
|
|
19
|
+
function isCredentialBearingAuthMode(mode) {
|
|
20
|
+
return CREDENTIAL_BEARING_AUTH_MODES.some((credentialMode) => credentialMode === mode);
|
|
21
|
+
}
|
|
22
|
+
function defaultApprovalPolicy(riskClass) {
|
|
23
|
+
if (riskClass === "read")
|
|
24
|
+
return "never";
|
|
25
|
+
if (riskClass === "write")
|
|
26
|
+
return "risk-based";
|
|
27
|
+
return "always";
|
|
28
|
+
}
|
|
29
|
+
// Operations that perform an auth-lifecycle action belong on the single
|
|
30
|
+
// `auth.flow` interface, never on a provider operation:
|
|
31
|
+
// - entry (login / signin / authenticate) => auth.flow.start/continue
|
|
32
|
+
// - exit (logout / signout / disconnect) => auth.flow.abort
|
|
33
|
+
//
|
|
34
|
+
// Matching works on `-`/`_` separated segments rather than a raw substring or a
|
|
35
|
+
// leading anchor, so `shop-logout`, `shop_logout` and `user-sign-out-everywhere`
|
|
36
|
+
// are all recognised: a domain prefix does not make the operation any less of an
|
|
37
|
+
// auth-lifecycle action, and operation ids may use either separator.
|
|
38
|
+
//
|
|
39
|
+
// Vocabulary is split into two tiers because auth words collide with ordinary
|
|
40
|
+
// domain verbs. Measured against the live fleet plus synthetic domain ids:
|
|
41
|
+
// - `authorize-payment`, `revoke-invitation`, `unlink-record`,
|
|
42
|
+
// `disconnect-device` are domain actions that never touch the connection
|
|
43
|
+
// credential, so these verbs are NOT matched as segments;
|
|
44
|
+
// - the same verbs as a complete operation id (`authorize`, `revoke`) do
|
|
45
|
+
// refer to the credential itself, so they are matched only in that form.
|
|
46
|
+
// `exchange`, `callback`, `connect`, `session`, `token`, `credential`,
|
|
47
|
+
// `password` and `otp` stay out entirely for the same reason.
|
|
48
|
+
const AUTH_LIFECYCLE_SEGMENT_WORDS = new Set([
|
|
49
|
+
"login",
|
|
50
|
+
"logout",
|
|
51
|
+
"signin",
|
|
52
|
+
"signout",
|
|
53
|
+
"signup",
|
|
54
|
+
"authenticate",
|
|
55
|
+
"reauth",
|
|
56
|
+
"auth",
|
|
57
|
+
]);
|
|
58
|
+
// Ambiguous as a prefix, unambiguous when they are the whole operation id.
|
|
59
|
+
const AUTH_LIFECYCLE_WHOLE_ID_WORDS = new Set([
|
|
60
|
+
"authorize",
|
|
61
|
+
"revoke",
|
|
62
|
+
"unlink",
|
|
63
|
+
"disconnect",
|
|
64
|
+
]);
|
|
65
|
+
// A verb stem followed by a direction word across two segments: `sign-out`,
|
|
66
|
+
// `user_sign_up_flow`, and the spelled-out `log-in` / `shop-log-out` forms
|
|
67
|
+
// (their fused equivalents `login`/`logout` live in the segment set above).
|
|
68
|
+
// `sign` pairs match anywhere; `log` pairs match only at the END of the id,
|
|
69
|
+
// because mid-id `log` is the noun in domain phrases measured against real
|
|
70
|
+
// fleets (`audit-log-in-range`, `change-log-out-of-band` are reads of a log,
|
|
71
|
+
// while `shop-log-out` is a logout).
|
|
72
|
+
const AUTH_DIRECTION_PAIRS = new Map([
|
|
73
|
+
["sign", { directions: new Set(["in", "out", "up"]), endOnly: false }],
|
|
74
|
+
["log", { directions: new Set(["in", "out"]), endOnly: true }],
|
|
75
|
+
]);
|
|
76
|
+
// Legacy anchored form kept for token-plumbing words whose bare use is only
|
|
77
|
+
// auth-related when it leads the operation id (`exchange-code`, `refresh`).
|
|
3
78
|
const AUTH_OPERATION_ID_PATTERN = /^(?:auth[-_])?(?:login|exchange|continue|refresh|callback)(?:[-_]|$)/i;
|
|
79
|
+
function isAuthLifecycleOperationId(operationId, authMode) {
|
|
80
|
+
const segments = operationId.toLowerCase().split(/[-_]+/).filter(Boolean);
|
|
81
|
+
if (segments.some((segment) => AUTH_LIFECYCLE_SEGMENT_WORDS.has(segment)))
|
|
82
|
+
return true;
|
|
83
|
+
// A verb stem + direction spread across two segments (`sign-out`,
|
|
84
|
+
// `sign_up`, `shop-log-out`); see AUTH_DIRECTION_PAIRS for positioning.
|
|
85
|
+
if (segments.some((segment, index) => {
|
|
86
|
+
const pair = AUTH_DIRECTION_PAIRS.get(segment);
|
|
87
|
+
if (pair === undefined || index + 1 >= segments.length)
|
|
88
|
+
return false;
|
|
89
|
+
if (!pair.directions.has(segments[index + 1]))
|
|
90
|
+
return false;
|
|
91
|
+
return pair.endOnly ? index + 2 === segments.length : true;
|
|
92
|
+
})) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (segments.length === 1 && AUTH_LIFECYCLE_WHOLE_ID_WORDS.has(segments[0])) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
// The legacy anchored pattern keeps its original scope. It matches ordinary
|
|
99
|
+
// domain ids such as `exchange-rates` and `refresh-catalog`, so extending it
|
|
100
|
+
// to `oauth2_proxied` would spread that behavior to providers it never
|
|
101
|
+
// applied to; proxied providers are covered by the segment tiers above.
|
|
102
|
+
if (authMode === "credentials" || authMode === "oauth2") {
|
|
103
|
+
return AUTH_OPERATION_ID_PATTERN.test(operationId);
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
4
107
|
function lintAllowedHosts(providerId, allowedHosts) {
|
|
5
108
|
const prefix = providerId ? `Provider "${providerId}"` : "Provider";
|
|
6
109
|
if (!allowedHosts) {
|
|
@@ -422,18 +525,6 @@ function collectSchemaDescriptionKeyDiagnostics(schema, basePath, seen = new Set
|
|
|
422
525
|
}
|
|
423
526
|
return diagnostics;
|
|
424
527
|
}
|
|
425
|
-
function isComplexSchema(schema, seen = new Set()) {
|
|
426
|
-
if (!isSchema(schema) || seen.has(schema)) {
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
seen.add(schema);
|
|
430
|
-
const children = getChildSchemas(schema);
|
|
431
|
-
const hasNestedComposite = children.some(({ schema: child }) => {
|
|
432
|
-
const childChildren = getChildSchemas(child);
|
|
433
|
-
return childChildren.length > 0;
|
|
434
|
-
});
|
|
435
|
-
return hasNestedComposite || children.some(({ schema: child }) => isComplexSchema(child, seen));
|
|
436
|
-
}
|
|
437
528
|
function hasBidirectionalFixtures(fixtures) {
|
|
438
529
|
if (!fixtures || typeof fixtures !== "object") {
|
|
439
530
|
return true;
|
|
@@ -587,50 +678,398 @@ function lintSelfHostedBrowserPatterns(provider, options) {
|
|
|
587
678
|
}
|
|
588
679
|
return diagnostics;
|
|
589
680
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
681
|
+
const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(?:ProviderError|ValidationError)\s*\(/g;
|
|
682
|
+
const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
683
|
+
const RECORDED_FIXTURE_SOURCE_FILE_PATTERN = /(?:^|\/)__fixtures__(?:\/|$)|(?:^|\/)__tests__\/fixtures(?:\/|$)/;
|
|
684
|
+
const JAVASCRIPT_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
685
|
+
const VERSIONED_PROFILE_LITERAL_PATTERN = /\b(?:chrome|chromium|firefox|safari|edge|opera|ios[-_]safari)[-_]\d+(?:[._-]\d+)*(?=$|[^A-Za-z0-9])/i;
|
|
686
|
+
const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
|
|
687
|
+
const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
|
|
688
|
+
const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
|
|
689
|
+
function staticStringText(node) {
|
|
690
|
+
if (!node)
|
|
691
|
+
return undefined;
|
|
692
|
+
const ts = getTypeScript();
|
|
693
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
694
|
+
return node.text;
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
function staticPropertyName(node) {
|
|
698
|
+
const ts = getTypeScript();
|
|
699
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
|
|
700
|
+
return node.text;
|
|
601
701
|
}
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
702
|
+
if (ts.isComputedPropertyName(node))
|
|
703
|
+
return staticStringText(node.expression);
|
|
704
|
+
return undefined;
|
|
705
|
+
}
|
|
706
|
+
function isSecChUaHeaderName(value) {
|
|
707
|
+
return value?.toLowerCase() === "sec-ch-ua";
|
|
708
|
+
}
|
|
709
|
+
function collectBrowserVersionLiteralFindings(source) {
|
|
710
|
+
const ts = getTypeScript();
|
|
711
|
+
const sourceFile = ts.createSourceFile("provider-source.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
712
|
+
const findings = [];
|
|
713
|
+
const seen = new Set();
|
|
714
|
+
const addFinding = (kind, literal, position) => {
|
|
715
|
+
const key = `${kind}:${position}:${literal}`;
|
|
716
|
+
if (seen.has(key))
|
|
717
|
+
return;
|
|
718
|
+
seen.add(key);
|
|
719
|
+
findings.push({ kind, literal, position });
|
|
720
|
+
};
|
|
721
|
+
const inspectLiteral = (text, position) => {
|
|
722
|
+
const profile = text.match(VERSIONED_PROFILE_LITERAL_PATTERN)?.[0];
|
|
723
|
+
if (profile)
|
|
724
|
+
addFinding("profile", profile, position);
|
|
725
|
+
const userAgent = text.match(VERSIONED_USER_AGENT_PATTERN)?.[0] ??
|
|
726
|
+
text.match(VERSIONED_SAFARI_USER_AGENT_PATTERN)?.[0];
|
|
727
|
+
if (userAgent)
|
|
728
|
+
addFinding("user-agent", userAgent, position);
|
|
729
|
+
};
|
|
730
|
+
const inspectSecChUaValue = (node) => {
|
|
731
|
+
const text = staticStringText(node);
|
|
732
|
+
if (text && VERSIONED_CLIENT_HINT_PATTERN.test(text)) {
|
|
733
|
+
addFinding("sec-ch-ua", "sec-ch-ua", node.getStart(sourceFile));
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
const visit = (node) => {
|
|
737
|
+
const text = staticStringText(node);
|
|
738
|
+
if (text !== undefined)
|
|
739
|
+
inspectLiteral(text, node.getStart(sourceFile));
|
|
740
|
+
if (ts.isPropertyAssignment(node) && isSecChUaHeaderName(staticPropertyName(node.name))) {
|
|
741
|
+
inspectSecChUaValue(node.initializer);
|
|
742
|
+
}
|
|
743
|
+
if (ts.isCallExpression(node) && isSecChUaHeaderName(staticStringText(node.arguments[0]))) {
|
|
744
|
+
inspectSecChUaValue(node.arguments[1]);
|
|
745
|
+
}
|
|
746
|
+
if (ts.isArrayLiteralExpression(node) &&
|
|
747
|
+
isSecChUaHeaderName(staticStringText(node.elements[0]))) {
|
|
748
|
+
inspectSecChUaValue(node.elements[1]);
|
|
749
|
+
}
|
|
750
|
+
ts.forEachChild(node, visit);
|
|
751
|
+
};
|
|
752
|
+
visit(sourceFile);
|
|
753
|
+
return findings.sort((left, right) => left.position - right.position);
|
|
754
|
+
}
|
|
755
|
+
function browserVersionLiteralMessage(finding) {
|
|
756
|
+
switch (finding.kind) {
|
|
757
|
+
case "profile":
|
|
758
|
+
return `Hardcoded stealth profile "${finding.literal}" pins a browser version and will rot. Select the browser and OS structurally, for example stealth: { browser: "chrome", os: "macos" }.`;
|
|
759
|
+
case "user-agent":
|
|
760
|
+
return `Hardcoded User-Agent browser version "${finding.literal}" can disagree with the stealth TLS fingerprint. Remove the literal and derive it from the structured profile, for example getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.`;
|
|
761
|
+
case "sec-ch-ua":
|
|
762
|
+
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" }; derive any explicit User-Agent with getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.';
|
|
609
763
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
764
|
+
}
|
|
765
|
+
function lintBrowserVersionLiterals(provider) {
|
|
766
|
+
const sources = [];
|
|
767
|
+
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(([filePath]) => JAVASCRIPT_SOURCE_FILE_PATTERN.test(filePath) &&
|
|
768
|
+
!TEST_SOURCE_FILE_PATTERN.test(filePath) &&
|
|
769
|
+
!RECORDED_FIXTURE_SOURCE_FILE_PATTERN.test(filePath));
|
|
770
|
+
if (sourceFiles.length > 0) {
|
|
771
|
+
for (const [filePath, source] of sourceFiles) {
|
|
772
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
773
|
+
}
|
|
617
774
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
775
|
+
else {
|
|
776
|
+
if (provider.authFlowSource)
|
|
777
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
778
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
779
|
+
const source = getOperationSource(operation);
|
|
780
|
+
if (source)
|
|
781
|
+
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return sources.flatMap(({ field, source }) => collectBrowserVersionLiteralFindings(source).map((finding) => ({
|
|
785
|
+
rule: "browser-version-literal",
|
|
786
|
+
level: "error",
|
|
787
|
+
field,
|
|
788
|
+
message: browserVersionLiteralMessage(finding),
|
|
789
|
+
})));
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Skips a string literal starting at `startIndex` (which must point at the
|
|
793
|
+
* opening quote). Returns the index of the closing quote, or -1 when the
|
|
794
|
+
* literal is unterminated. Template literals handle nested `${...}`
|
|
795
|
+
* expressions, including strings inside them.
|
|
796
|
+
*/
|
|
797
|
+
function skipStringLiteral(source, startIndex) {
|
|
798
|
+
const quote = source[startIndex];
|
|
799
|
+
for (let index = startIndex + 1; index < source.length; index++) {
|
|
800
|
+
const char = source[index];
|
|
801
|
+
if (char === "\\") {
|
|
802
|
+
index++;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (quote === "`" && char === "$" && source[index + 1] === "{") {
|
|
806
|
+
index = skipTemplateExpression(source, index + 2);
|
|
807
|
+
if (index < 0) {
|
|
808
|
+
return -1;
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (char === quote) {
|
|
813
|
+
return index;
|
|
814
|
+
}
|
|
815
|
+
if (quote !== "`" && char === "\n") {
|
|
816
|
+
return -1;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return -1;
|
|
820
|
+
}
|
|
821
|
+
function skipTemplateExpression(source, startIndex) {
|
|
822
|
+
let depth = 1;
|
|
823
|
+
for (let index = startIndex; index < source.length; index++) {
|
|
824
|
+
const char = source[index];
|
|
825
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
826
|
+
index = skipStringLiteral(source, index);
|
|
827
|
+
if (index < 0) {
|
|
828
|
+
return -1;
|
|
829
|
+
}
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (char === "{") {
|
|
833
|
+
depth++;
|
|
834
|
+
}
|
|
835
|
+
else if (char === "}") {
|
|
836
|
+
depth--;
|
|
837
|
+
if (depth === 0) {
|
|
838
|
+
return index;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return -1;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Extracts the argument text of a call whose opening paren has already been
|
|
846
|
+
* consumed (`startIndex` points just past it). Returns undefined when the
|
|
847
|
+
* call never closes in this source, which the caller treats as "skip
|
|
848
|
+
* silently" — this scanner is conservative by design.
|
|
849
|
+
*/
|
|
850
|
+
function extractBalancedCallArguments(source, startIndex) {
|
|
851
|
+
let depth = 1;
|
|
852
|
+
for (let index = startIndex; index < source.length; index++) {
|
|
853
|
+
const char = source[index];
|
|
854
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
855
|
+
index = skipStringLiteral(source, index);
|
|
856
|
+
if (index < 0) {
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (char === "/" && source[index + 1] === "/") {
|
|
862
|
+
const newline = source.indexOf("\n", index);
|
|
863
|
+
if (newline === -1) {
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
index = newline;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
870
|
+
const end = source.indexOf("*/", index + 2);
|
|
871
|
+
if (end === -1) {
|
|
872
|
+
return undefined;
|
|
873
|
+
}
|
|
874
|
+
index = end + 1;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (char === "(") {
|
|
878
|
+
depth++;
|
|
879
|
+
}
|
|
880
|
+
else if (char === ")") {
|
|
881
|
+
depth--;
|
|
882
|
+
if (depth === 0) {
|
|
883
|
+
return source.slice(startIndex, index);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return undefined;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Collects literal string values of top-level `code:` properties inside a
|
|
891
|
+
* ProviderError/ValidationError options object. Only plain `"..."` / `'...'`
|
|
892
|
+
* literals at options-object depth count; computed codes (identifiers,
|
|
893
|
+
* ternaries, template substitutions, concatenations, escapes) are skipped
|
|
894
|
+
* silently so the rule never guesses.
|
|
895
|
+
*/
|
|
896
|
+
function collectLiteralErrorCodeValues(args) {
|
|
897
|
+
const codes = [];
|
|
898
|
+
let braceDepth = 0;
|
|
899
|
+
let parenDepth = 0;
|
|
900
|
+
let bracketDepth = 0;
|
|
901
|
+
let previousSignificantChar = "";
|
|
902
|
+
for (let index = 0; index < args.length; index++) {
|
|
903
|
+
const char = args[index] ?? "";
|
|
904
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
905
|
+
const end = skipStringLiteral(args, index);
|
|
906
|
+
if (end < 0) {
|
|
907
|
+
return codes;
|
|
908
|
+
}
|
|
909
|
+
index = end;
|
|
910
|
+
previousSignificantChar = char;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (char === "/" && args[index + 1] === "/") {
|
|
914
|
+
const newline = args.indexOf("\n", index);
|
|
915
|
+
if (newline === -1) {
|
|
916
|
+
return codes;
|
|
917
|
+
}
|
|
918
|
+
index = newline;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (char === "/" && args[index + 1] === "*") {
|
|
922
|
+
const end = args.indexOf("*/", index + 2);
|
|
923
|
+
if (end === -1) {
|
|
924
|
+
return codes;
|
|
925
|
+
}
|
|
926
|
+
index = end + 1;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (/\s/.test(char)) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (char === "{") {
|
|
933
|
+
braceDepth++;
|
|
934
|
+
}
|
|
935
|
+
else if (char === "}") {
|
|
936
|
+
braceDepth--;
|
|
937
|
+
}
|
|
938
|
+
else if (char === "(") {
|
|
939
|
+
parenDepth++;
|
|
940
|
+
}
|
|
941
|
+
else if (char === ")") {
|
|
942
|
+
parenDepth--;
|
|
943
|
+
}
|
|
944
|
+
else if (char === "[") {
|
|
945
|
+
bracketDepth++;
|
|
946
|
+
}
|
|
947
|
+
else if (char === "]") {
|
|
948
|
+
bracketDepth--;
|
|
949
|
+
}
|
|
950
|
+
else if (braceDepth === 1 &&
|
|
951
|
+
parenDepth === 0 &&
|
|
952
|
+
bracketDepth === 0 &&
|
|
953
|
+
(previousSignificantChar === "{" || previousSignificantChar === ",") &&
|
|
954
|
+
args.startsWith("code", index)) {
|
|
955
|
+
let cursor = index + "code".length;
|
|
956
|
+
while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
|
|
957
|
+
cursor++;
|
|
958
|
+
}
|
|
959
|
+
if (args[cursor] === ":") {
|
|
960
|
+
cursor++;
|
|
961
|
+
while (cursor < args.length && /\s/.test(args[cursor] ?? "")) {
|
|
962
|
+
cursor++;
|
|
963
|
+
}
|
|
964
|
+
const quote = args[cursor];
|
|
965
|
+
if (quote === '"' || quote === "'") {
|
|
966
|
+
const end = skipStringLiteral(args, cursor);
|
|
967
|
+
if (end > cursor) {
|
|
968
|
+
const value = args.slice(cursor + 1, end);
|
|
969
|
+
let after = end + 1;
|
|
970
|
+
while (after < args.length && /\s/.test(args[after] ?? "")) {
|
|
971
|
+
after++;
|
|
972
|
+
}
|
|
973
|
+
const nextChar = after < args.length ? (args[after] ?? "") : "";
|
|
974
|
+
if (!value.includes("\\") && (nextChar === "," || nextChar === "}" || nextChar === "")) {
|
|
975
|
+
codes.push(value);
|
|
976
|
+
}
|
|
977
|
+
index = end;
|
|
978
|
+
previousSignificantChar = quote;
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
return codes;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
previousSignificantChar = char;
|
|
986
|
+
}
|
|
987
|
+
return codes;
|
|
988
|
+
}
|
|
989
|
+
function collectLiteralThrownErrorCodes(source) {
|
|
990
|
+
const codes = [];
|
|
991
|
+
THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = 0;
|
|
992
|
+
for (let match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source); match; match = THROWN_ERROR_CONSTRUCTION_PATTERN.exec(source)) {
|
|
993
|
+
const argsStart = match.index + match[0].length;
|
|
994
|
+
const args = extractBalancedCallArguments(source, argsStart);
|
|
995
|
+
if (args !== undefined) {
|
|
996
|
+
codes.push(...collectLiteralErrorCodeValues(args));
|
|
997
|
+
}
|
|
998
|
+
THROWN_ERROR_CONSTRUCTION_PATTERN.lastIndex = argsStart;
|
|
999
|
+
}
|
|
1000
|
+
return codes;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Static counterpart of the runtime `unregistered_provider_error_code`
|
|
1004
|
+
* signal (honest-provider-error-contract Phase 3.5.5): flags
|
|
1005
|
+
* `new ProviderError(...)` / `new ValidationError(...)` constructions whose
|
|
1006
|
+
* literal `code` is neither SDK-registered (SDK_RUNTIME_OWNED_ERROR_CODES
|
|
1007
|
+
* plus the canonical status-mapped codes shared with serve.ts toStatusCode)
|
|
1008
|
+
* nor declared in any operation's errorCodes. At runtime such a code
|
|
1009
|
+
* serves HTTP 500 and emits the signal; this rule surfaces it at check time.
|
|
1010
|
+
*
|
|
1011
|
+
* A throw site cannot be attributed to a specific operation statically —
|
|
1012
|
+
* providers routinely throw from helpers shared across operations — so this
|
|
1013
|
+
* rule matches against the provider-level union of declared codes. That is
|
|
1014
|
+
* the honest scope: it will not catch a code declared only on the "wrong"
|
|
1015
|
+
* operation, and it never claims per-operation attribution it cannot prove.
|
|
1016
|
+
* Only literal string codes are checked; computed/dynamic codes and test
|
|
1017
|
+
* sources are skipped silently. Warning level: the long tail of existing
|
|
1018
|
+
* providers converges gradually, so this must not fail `apifuse check`.
|
|
1019
|
+
*/
|
|
1020
|
+
function lintUndeclaredThrownErrorCodes(provider) {
|
|
1021
|
+
const knownCodes = new Set([
|
|
1022
|
+
...SDK_RUNTIME_OWNED_ERROR_CODES,
|
|
1023
|
+
...SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.keys(),
|
|
1024
|
+
]);
|
|
1025
|
+
for (const operation of Object.values(provider.operations ?? {})) {
|
|
1026
|
+
for (const entry of operation.errorCodes ?? []) {
|
|
1027
|
+
if (typeof entry?.code === "string") {
|
|
1028
|
+
knownCodes.add(entry.code);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const sources = [];
|
|
1033
|
+
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(([filePath]) => !TEST_SOURCE_FILE_PATTERN.test(filePath));
|
|
1034
|
+
if (sourceFiles.length > 0) {
|
|
1035
|
+
for (const [filePath, source] of sourceFiles) {
|
|
1036
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
if (provider.authFlowSource) {
|
|
1041
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1042
|
+
}
|
|
1043
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1044
|
+
const source = getOperationSource(operation);
|
|
1045
|
+
if (source) {
|
|
1046
|
+
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const diagnostics = [];
|
|
1051
|
+
for (const { field, source } of sources) {
|
|
1052
|
+
const undeclaredCodes = new Set(collectLiteralThrownErrorCodes(source).filter((code) => !knownCodes.has(code)));
|
|
1053
|
+
for (const code of undeclaredCodes) {
|
|
1054
|
+
diagnostics.push({
|
|
1055
|
+
rule: "thrown-error-code-undeclared",
|
|
1056
|
+
level: "warn",
|
|
1057
|
+
field,
|
|
1058
|
+
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.`,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
625
1061
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1062
|
+
return diagnostics;
|
|
1063
|
+
}
|
|
1064
|
+
export function lintOperation(op) {
|
|
1065
|
+
const diagnostics = [];
|
|
1066
|
+
const hasDescriptionKey = typeof op.descriptionKey === "string" && op.descriptionKey.length > 0;
|
|
1067
|
+
if (!hasDescriptionKey) {
|
|
629
1068
|
diagnostics.push({
|
|
630
|
-
rule: "description-
|
|
631
|
-
level: "
|
|
632
|
-
field: "
|
|
633
|
-
message:
|
|
1069
|
+
rule: "description-key-required",
|
|
1070
|
+
level: "error",
|
|
1071
|
+
field: "descriptionKey",
|
|
1072
|
+
message: "Operation must declare a locale-backed descriptionKey.",
|
|
634
1073
|
});
|
|
635
1074
|
}
|
|
636
1075
|
diagnostics.push(...collectSchemaDescriptionKeyDiagnostics(op.input, "input"), ...collectSchemaDescriptionKeyDiagnostics(op.output, "output"));
|
|
@@ -642,14 +1081,6 @@ export function lintOperation(op) {
|
|
|
642
1081
|
message: "Fixtures must include both request and response.",
|
|
643
1082
|
});
|
|
644
1083
|
}
|
|
645
|
-
if (isComplexSchema(op.input) && (op.inputExamples?.length ?? 0) < 2) {
|
|
646
|
-
diagnostics.push({
|
|
647
|
-
rule: "complex-input-has-examples",
|
|
648
|
-
level: "warn",
|
|
649
|
-
field: "inputExamples",
|
|
650
|
-
message: "Complex input schemas should provide at least 2 input examples.",
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
1084
|
for (const field of uniqueFields(collectUnmarkedSensitiveFields(op.input, "input"))) {
|
|
654
1085
|
diagnostics.push({
|
|
655
1086
|
rule: "sensitive-field-unmarked",
|
|
@@ -668,7 +1099,57 @@ export function lintOperation(op) {
|
|
|
668
1099
|
}
|
|
669
1100
|
return diagnostics;
|
|
670
1101
|
}
|
|
1102
|
+
function declaredPinnedWireFieldPaths(provider) {
|
|
1103
|
+
const pins = provider.meta?.contract?.pinnedWireFieldPaths;
|
|
1104
|
+
if (!Array.isArray(pins))
|
|
1105
|
+
return [];
|
|
1106
|
+
return pins.filter((pin) => pin !== null &&
|
|
1107
|
+
typeof pin === "object" &&
|
|
1108
|
+
typeof pin.path === "string" &&
|
|
1109
|
+
pin.path.trim().length > 0 &&
|
|
1110
|
+
typeof pin.reason === "string" &&
|
|
1111
|
+
pin.reason.trim().length > 0);
|
|
1112
|
+
}
|
|
1113
|
+
function applyPinnedWireFieldPaths(provider, diagnostics) {
|
|
1114
|
+
const pins = declaredPinnedWireFieldPaths(provider);
|
|
1115
|
+
if (pins.length === 0) {
|
|
1116
|
+
return { diagnostics: [...diagnostics], information: [] };
|
|
1117
|
+
}
|
|
1118
|
+
const pinsByPath = new Map(pins.map((pin) => [pin.path, pin]));
|
|
1119
|
+
const matchedPaths = new Set();
|
|
1120
|
+
const remainingDiagnostics = diagnostics.filter((diagnostic) => {
|
|
1121
|
+
if (diagnostic.rule !== "public-schema-upstream-field" ||
|
|
1122
|
+
diagnostic.field === undefined ||
|
|
1123
|
+
!pinsByPath.has(diagnostic.field)) {
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
matchedPaths.add(diagnostic.field);
|
|
1127
|
+
return false;
|
|
1128
|
+
});
|
|
1129
|
+
const information = [];
|
|
1130
|
+
for (const pin of pins) {
|
|
1131
|
+
if (matchedPaths.has(pin.path)) {
|
|
1132
|
+
information.push({
|
|
1133
|
+
rule: "public-schema-pinned-wire-field",
|
|
1134
|
+
field: pin.path,
|
|
1135
|
+
message: `Suppressed exact public-schema-upstream-field diagnostic. Reason: ${pin.reason}`,
|
|
1136
|
+
});
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
remainingDiagnostics.push({
|
|
1140
|
+
rule: "public-schema-pinned-wire-field-stale",
|
|
1141
|
+
level: "error",
|
|
1142
|
+
field: pin.path,
|
|
1143
|
+
message: `Pinned wire field path ${JSON.stringify(pin.path)} matches no current public-schema-upstream-field diagnostic; remove the stale declaration.`,
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
return { diagnostics: remainingDiagnostics, information };
|
|
1147
|
+
}
|
|
671
1148
|
export function lintProvider(provider, options = {}) {
|
|
1149
|
+
return lintProviderWithInformation(provider, options).diagnostics;
|
|
1150
|
+
}
|
|
1151
|
+
/** Internal detailed result used by the CLI to render suppression audits. */
|
|
1152
|
+
export function lintProviderWithInformation(provider, options = {}) {
|
|
672
1153
|
const diagnostics = [
|
|
673
1154
|
...lintAllowedHosts(provider.id, provider.allowedHosts),
|
|
674
1155
|
...lintReviewed(provider.id, provider.reviewed),
|
|
@@ -677,38 +1158,60 @@ export function lintProvider(provider, options = {}) {
|
|
|
677
1158
|
...lintCredentialWriteUsage(provider),
|
|
678
1159
|
...lintPlaywrightDirectImports(provider),
|
|
679
1160
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1161
|
+
...lintBrowserVersionLiterals(provider),
|
|
1162
|
+
...lintUndeclaredThrownErrorCodes(provider),
|
|
680
1163
|
];
|
|
681
1164
|
if (provider.operations) {
|
|
682
1165
|
const authMode = provider.auth?.mode;
|
|
683
|
-
|
|
1166
|
+
for (const [operationKey, operation] of Object.entries(provider.operations)) {
|
|
1167
|
+
if (isCredentialBearingAuthMode(authMode) && operation.connectionMode === undefined) {
|
|
1168
|
+
diagnostics.push({
|
|
1169
|
+
rule: "mixed-auth-connection-mode-required",
|
|
1170
|
+
level: "error",
|
|
1171
|
+
field: `operations.${operationKey}.connectionMode`,
|
|
1172
|
+
message: `Provider "${provider.id ?? "unknown"}" uses credential-bearing auth.mode "${authMode}"; operation "${operationKey}" must declare connectionMode explicitly.`,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
if (operation.riskClass !== undefined &&
|
|
1176
|
+
operation.approval !== undefined &&
|
|
1177
|
+
operation.approval === defaultApprovalPolicy(operation.riskClass)) {
|
|
1178
|
+
diagnostics.push({
|
|
1179
|
+
rule: "redundant-approval",
|
|
1180
|
+
level: "error",
|
|
1181
|
+
field: `operations.${operationKey}.approval`,
|
|
1182
|
+
message: `Operation "${operationKey}" approval "${operation.approval}" repeats the default for riskClass "${operation.riskClass}"; omit approval unless it is a deliberate override.`,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
// Every authenticated mode owns an auth.flow; `oauth2_proxied` was
|
|
1187
|
+
// previously exempt, which let auth-lifecycle operations ship on
|
|
1188
|
+
// proxied providers unchecked.
|
|
1189
|
+
if (authMode === "credentials" ||
|
|
1190
|
+
authMode === "oauth2" ||
|
|
1191
|
+
authMode === "oauth2_proxied") {
|
|
684
1192
|
for (const operationKey of Object.keys(provider.operations)) {
|
|
685
|
-
if (
|
|
1193
|
+
if (isAuthLifecycleOperationId(operationKey, authMode)) {
|
|
686
1194
|
diagnostics.push({
|
|
687
1195
|
rule: "auth-operation-unsupported",
|
|
688
1196
|
level: "error",
|
|
689
1197
|
field: `operations.${operationKey}`,
|
|
690
|
-
message: `Provider "${provider.id ?? "unknown"}" operation "${operationKey}"
|
|
1198
|
+
message: `Provider "${provider.id ?? "unknown"}" operation "${operationKey}" performs an auth-lifecycle action (login, logout, token exchange or similar). Authenticated providers must expose the whole credential lifecycle through the single auth.flow interface because Gateway persists only auth.flow complete turn data.credential as the connection credential, and an operation that mutates the session outside that interface leaves the stored connection stale. Move sign-in logic into auth.flow.start/continue and sign-out/disconnect logic into auth.flow.abort (served by POST /auth/disconnect) instead of a provider operation.`,
|
|
691
1199
|
});
|
|
692
1200
|
}
|
|
693
1201
|
}
|
|
694
1202
|
}
|
|
695
1203
|
}
|
|
696
1204
|
if (!provider.operations) {
|
|
697
|
-
return diagnostics;
|
|
1205
|
+
return applyPinnedWireFieldPaths(provider, diagnostics);
|
|
698
1206
|
}
|
|
699
1207
|
diagnostics.push(...Object.entries(provider.operations).flatMap(([operationKey, operation]) => [
|
|
700
1208
|
...lintOperation({
|
|
701
|
-
description: operation.description ?? "",
|
|
702
1209
|
descriptionKey: operation.descriptionKey,
|
|
703
|
-
whenToUse: operation.whenToUse,
|
|
704
1210
|
whenToUseKeys: operation.whenToUseKeys,
|
|
705
|
-
whenNotToUse: operation.whenNotToUse,
|
|
706
1211
|
whenNotToUseKeys: operation.whenNotToUseKeys,
|
|
707
1212
|
input: operation.input,
|
|
708
1213
|
output: operation.output,
|
|
709
1214
|
fixtures: operation.fixtures,
|
|
710
|
-
inputExamples: operation.inputExamples,
|
|
711
|
-
derivations: operation.derivations,
|
|
712
1215
|
}),
|
|
713
1216
|
...lintPublicSchemaFieldNames(provider.id, operationKey, operation.input, operation.output, provider.meta?.contract?.publicSchemaFieldNames === "normalized"),
|
|
714
1217
|
].map((diagnostic) => ({
|
|
@@ -718,5 +1221,5 @@ export function lintProvider(provider, options = {}) {
|
|
|
718
1221
|
: `operations.${operationKey}`,
|
|
719
1222
|
message: `[${operationKey}] ${diagnostic.message}`,
|
|
720
1223
|
}))));
|
|
721
|
-
return diagnostics;
|
|
1224
|
+
return applyPinnedWireFieldPaths(provider, diagnostics);
|
|
722
1225
|
}
|