@api-doctor/cli 0.0.4 → 0.0.5
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/README.md +3 -0
- package/dist/cli.cjs +2515 -139
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +2515 -139
- package/dist/cli.mjs.map +1 -1
- package/dist/plugin.d.ts +538 -0
- package/dist/plugin.js +2103 -1
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -905,6 +905,267 @@ var tiptapManifest = {
|
|
|
905
905
|
]
|
|
906
906
|
};
|
|
907
907
|
|
|
908
|
+
// src/providers/elevenlabs/manifest.ts
|
|
909
|
+
var elevenlabsManifest = {
|
|
910
|
+
name: "elevenlabs",
|
|
911
|
+
displayName: "ElevenLabs",
|
|
912
|
+
detect: {
|
|
913
|
+
packages: ["@11labs/client", "elevenlabs", "@elevenlabs/elevenlabs-js"],
|
|
914
|
+
imports: ["@11labs/client", "elevenlabs", "@elevenlabs/elevenlabs-js"],
|
|
915
|
+
urlPatterns: ["api.elevenlabs.io"]
|
|
916
|
+
},
|
|
917
|
+
oxlintRules: [
|
|
918
|
+
{
|
|
919
|
+
key: "elevenlabs-validate-signed-url-response",
|
|
920
|
+
resultRule: "elevenlabs/correctness/validate-signed-url-response",
|
|
921
|
+
message: "The signed URL response is used without validating that signed_url is present.",
|
|
922
|
+
fix: "Check that data.signed_url exists before using it, and throw/return an error response if it is missing.",
|
|
923
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
|
|
924
|
+
severity: "error"
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
key: "elevenlabs-no-error-object-logging",
|
|
928
|
+
resultRule: "elevenlabs/security/no-error-object-logging",
|
|
929
|
+
message: "A catch block around an ElevenLabs API call logs the raw error object.",
|
|
930
|
+
fix: 'Log a sanitized message (e.g. error instanceof Error ? error.message : "Unknown error") instead of the raw error object.',
|
|
931
|
+
docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
|
|
932
|
+
severity: "error"
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
key: "elevenlabs-fetch-timeout-required",
|
|
936
|
+
resultRule: "elevenlabs/reliability/fetch-timeout-required",
|
|
937
|
+
message: "A fetch call to the ElevenLabs API has no abort signal/timeout.",
|
|
938
|
+
fix: "Pass an AbortController signal (e.g. via setTimeout(() => controller.abort(), 5000)) so a slow or unresponsive API does not hang the request indefinitely.",
|
|
939
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
|
|
940
|
+
severity: "warning"
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
key: "elevenlabs-validate-agent-id-format",
|
|
944
|
+
resultRule: "elevenlabs/correctness/validate-agent-id-format",
|
|
945
|
+
message: "agentId is checked for existence but not validated against an expected format.",
|
|
946
|
+
fix: "Validate agentId against an allowed pattern (e.g. /^[a-zA-Z0-9\\-]{1,64}$/) before using it in an ElevenLabs API request.",
|
|
947
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/customization/authentication",
|
|
948
|
+
severity: "error"
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
key: "elevenlabs-secure-session-id-generation",
|
|
952
|
+
resultRule: "elevenlabs/security/secure-session-id-generation",
|
|
953
|
+
message: "A session id is generated with Math.random(), which is not cryptographically secure.",
|
|
954
|
+
fix: "Use crypto.getRandomValues() to generate session ids instead of Math.random().",
|
|
955
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-api/guides/how-to/best-practices/security",
|
|
956
|
+
severity: "error"
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
key: "elevenlabs-conversation-error-recovery",
|
|
960
|
+
resultRule: "elevenlabs/reliability/conversation-error-recovery",
|
|
961
|
+
message: "A loading flag set before starting a conversation is never reset on failure.",
|
|
962
|
+
fix: "Reset the loading flag in the catch block (or a finally block) so a failed attempt does not leave the UI stuck loading.",
|
|
963
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
|
|
964
|
+
severity: "warning"
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
key: "elevenlabs-api-version-pinning",
|
|
968
|
+
resultRule: "elevenlabs/reliability/api-version-pinning",
|
|
969
|
+
message: "A fetch call to the ElevenLabs API has no explicit API version header.",
|
|
970
|
+
fix: "Add an explicit version header (e.g. elevenlabs-version) to the request instead of relying on the URL path alone.",
|
|
971
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/breaking-changes-policy",
|
|
972
|
+
severity: "warning"
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
key: "elevenlabs-check-http-status-before-json",
|
|
976
|
+
resultRule: "elevenlabs/correctness/check-http-status-before-json",
|
|
977
|
+
message: "response.json() is called on an ElevenLabs API response without checking response.ok/status first.",
|
|
978
|
+
fix: "Check response.ok (or response.status) before calling response.json(), so an error body is not parsed as valid data.",
|
|
979
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/errors",
|
|
980
|
+
severity: "error"
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
key: "elevenlabs-conversation-cleanup-on-error",
|
|
984
|
+
resultRule: "elevenlabs/reliability/conversation-cleanup-on-error",
|
|
985
|
+
message: "A call to end the conversation has no surrounding try/catch.",
|
|
986
|
+
fix: "Wrap conversation.endSession()/endConversation() in try/catch so a rejection does not leave the conversation state in memory.",
|
|
987
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
|
|
988
|
+
severity: "warning"
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
key: "elevenlabs-env-var-validation",
|
|
992
|
+
resultRule: "elevenlabs/correctness/env-var-validation",
|
|
993
|
+
message: "An ElevenLabs API key is only validated inside a request handler, not at module load.",
|
|
994
|
+
fix: "Read and validate the env var at module scope (and throw if missing) so a misconfigured deployment fails at startup.",
|
|
995
|
+
docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
|
|
996
|
+
severity: "warning"
|
|
997
|
+
}
|
|
998
|
+
]
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// src/providers/twilio/manifest.ts
|
|
1002
|
+
var twilioManifest = {
|
|
1003
|
+
name: "twilio",
|
|
1004
|
+
displayName: "Twilio",
|
|
1005
|
+
detect: {
|
|
1006
|
+
packages: ["twilio"],
|
|
1007
|
+
imports: ["twilio"],
|
|
1008
|
+
urlPatterns: ["api.twilio.com"]
|
|
1009
|
+
},
|
|
1010
|
+
oxlintRules: [
|
|
1011
|
+
{
|
|
1012
|
+
key: "twilio-validate-webhook-signature",
|
|
1013
|
+
resultRule: "twilio/security/validate-webhook-signature",
|
|
1014
|
+
message: "A POST webhook route reads req.body but never validates the X-Twilio-Signature header.",
|
|
1015
|
+
fix: "Validate the X-Twilio-Signature header with twilio.validateRequest()/RequestValidator before trusting the body.",
|
|
1016
|
+
docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-security",
|
|
1017
|
+
severity: "error"
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
key: "twilio-taskrouter-attributes-match-consumer",
|
|
1021
|
+
resultRule: "twilio/correctness/taskrouter-attributes-match-consumer",
|
|
1022
|
+
message: "A .task() attributes object is missing a field that a reservation handler reads back out of TaskAttributes.",
|
|
1023
|
+
fix: "Add the missing field to the Task attributes object so the consumer's JSON.parse(TaskAttributes) destructure actually finds it.",
|
|
1024
|
+
docsUrl: "https://www.twilio.com/docs/taskrouter/twiml-queue-calls",
|
|
1025
|
+
severity: "error"
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
key: "twilio-enqueue-task-json-stringify",
|
|
1029
|
+
resultRule: "twilio/security/enqueue-task-json-stringify",
|
|
1030
|
+
message: "A .task() call builds JSON attributes via raw string interpolation instead of JSON.stringify().",
|
|
1031
|
+
fix: "Build the Task attributes as an object and serialize with JSON.stringify() instead of interpolating values into a JSON string/template literal.",
|
|
1032
|
+
docsUrl: "https://www.twilio.com/docs/api/errors/14221",
|
|
1033
|
+
severity: "error"
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
key: "twilio-media-streams-key-by-call-sid",
|
|
1037
|
+
resultRule: "twilio/reliability/media-streams-key-by-call-sid",
|
|
1038
|
+
message: "A Media Streams session map is keyed by a phone-number-like field instead of callSid.",
|
|
1039
|
+
fix: "Key the map by start.callSid instead of the caller phone number, and propagate callSid (not the number) downstream as the lookup key.",
|
|
1040
|
+
docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
|
|
1041
|
+
severity: "error"
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
key: "twilio-await-or-catch-rest-calls-in-event-handlers",
|
|
1045
|
+
resultRule: "twilio/reliability/await-or-catch-rest-calls-in-event-handlers",
|
|
1046
|
+
message: "A Twilio REST call inside an event-handler callback has no surrounding try/catch.",
|
|
1047
|
+
fix: "Wrap the REST call in try/catch inside the handler so a rejected promise does not become an unhandled rejection.",
|
|
1048
|
+
docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-faq",
|
|
1049
|
+
severity: "error"
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
key: "twilio-use-twiml-builder-not-string-templates",
|
|
1053
|
+
resultRule: "twilio/security/use-twiml-builder-not-string-templates",
|
|
1054
|
+
message: "TwiML is built as a raw template literal instead of the VoiceResponse builder.",
|
|
1055
|
+
fix: "Build TwiML with new VoiceResponse() and its builder methods (say, connect, stream, parameter) instead of interpolating values into an XML template literal.",
|
|
1056
|
+
docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
|
|
1057
|
+
severity: "warning"
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
key: "twilio-media-streams-mark-pacing",
|
|
1061
|
+
resultRule: "twilio/reliability/media-streams-mark-pacing",
|
|
1062
|
+
message: "Media is forwarded to a Stream with no mark-based pacing (isLast) anywhere in the file.",
|
|
1063
|
+
fix: "Pace sends with mark events (pass isLast: true periodically) and wait for the inbound mark acknowledgment before queuing more audio.",
|
|
1064
|
+
docsUrl: "https://www.twilio.com/docs/api/errors/31931",
|
|
1065
|
+
severity: "warning"
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
key: "twilio-validate-all-request-inputs",
|
|
1069
|
+
resultRule: "twilio/correctness/validate-all-request-inputs",
|
|
1070
|
+
message: "A webhook route reads a request field that is not covered by its schema (or has no schema at all).",
|
|
1071
|
+
fix: "Add a querystring/body schema covering every field the handler reads, and reject the request with a 400 if a required field is missing.",
|
|
1072
|
+
docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
|
|
1073
|
+
severity: "warning"
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
key: "twilio-media-streams-mark-name-string",
|
|
1077
|
+
resultRule: "twilio/correctness/media-streams-mark-name-string",
|
|
1078
|
+
message: "mark.name is set to a non-string value (Twilio requires a string).",
|
|
1079
|
+
fix: "Wrap the value in String(...) (e.g. String(Date.now())) so mark.name matches Twilio's documented string schema.",
|
|
1080
|
+
docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
|
|
1081
|
+
severity: "warning"
|
|
1082
|
+
}
|
|
1083
|
+
]
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
// src/providers/openai-realtime/manifest.ts
|
|
1087
|
+
var openaiRealtimeManifest = {
|
|
1088
|
+
name: "openai-realtime",
|
|
1089
|
+
displayName: "OpenAI Realtime API",
|
|
1090
|
+
detect: {
|
|
1091
|
+
urlPatterns: ["api.openai.com/v1/realtime"]
|
|
1092
|
+
},
|
|
1093
|
+
oxlintRules: [
|
|
1094
|
+
{
|
|
1095
|
+
key: "openai-realtime-migrate-beta-to-ga",
|
|
1096
|
+
resultRule: "openai-realtime/correctness/migrate-beta-to-ga",
|
|
1097
|
+
message: "This Realtime connection sends the deprecated OpenAI-Beta: realtime=v1 header.",
|
|
1098
|
+
fix: "Remove the OpenAI-Beta header and migrate the session/event shapes to the GA interface (session.type, audio.output nesting, response.output_audio.delta event names).",
|
|
1099
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
|
|
1100
|
+
severity: "error"
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
key: "openai-realtime-no-log-raw-message-payloads",
|
|
1104
|
+
resultRule: "openai-realtime/security/no-log-raw-message-payloads",
|
|
1105
|
+
message: "A raw OpenAI Realtime message is logged verbatim, which can include live call audio or transcript content.",
|
|
1106
|
+
fix: "Log only derived fields (e.g. { type: message.type }) at info level; log full payloads only at trace level behind an explicit opt-in.",
|
|
1107
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
|
|
1108
|
+
severity: "error"
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
key: "openai-realtime-handle-error-server-event",
|
|
1112
|
+
resultRule: "openai-realtime/reliability/handle-error-server-event",
|
|
1113
|
+
message: 'This Realtime message handler branches on event types but never checks for the API-level "error" event.',
|
|
1114
|
+
fix: "Add an explicit branch for message.type === 'error' that logs the error and surfaces/fails over, instead of letting it fall through silently.",
|
|
1115
|
+
docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime_server_events",
|
|
1116
|
+
severity: "error"
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
key: "openai-realtime-reconnect-on-drop",
|
|
1120
|
+
resultRule: "openai-realtime/reliability/reconnect-on-drop",
|
|
1121
|
+
message: "This Realtime socket's close handler only logs and never attempts to reconnect.",
|
|
1122
|
+
fix: "On close, attempt a bounded number of reconnects with a fresh session.update, and proactively end/flag the call if reconnection ultimately fails.",
|
|
1123
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
|
|
1124
|
+
severity: "error"
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
key: "openai-realtime-avoid-dated-preview-snapshots",
|
|
1128
|
+
resultRule: "openai-realtime/correctness/avoid-dated-preview-snapshots",
|
|
1129
|
+
message: "The Realtime connection is pinned to a dated preview model snapshot instead of the GA alias.",
|
|
1130
|
+
fix: "Use the GA model id (e.g. 'gpt-realtime') or a current dated snapshot tracked against OpenAI's deprecation notices.",
|
|
1131
|
+
docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
|
|
1132
|
+
severity: "warning"
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
key: "openai-realtime-verify-deprecated-session-fields",
|
|
1136
|
+
resultRule: "openai-realtime/correctness/verify-deprecated-session-fields",
|
|
1137
|
+
message: "The session config sets 'temperature', a field not documented in the current GA Realtime sessions schema.",
|
|
1138
|
+
fix: "Re-verify this field against the current sessions reference before relying on it, or drop it and control determinism via turn_detection/instructions instead.",
|
|
1139
|
+
docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
|
|
1140
|
+
severity: "warning"
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
key: "openai-realtime-buffer-audio-until-session-ready",
|
|
1144
|
+
resultRule: "openai-realtime/reliability/buffer-audio-until-session-ready",
|
|
1145
|
+
message: "Audio sent before the Realtime socket reaches the open state is dropped instead of buffered.",
|
|
1146
|
+
fix: "Queue outbound input_audio_buffer.append messages until the open event fires, then flush them in order.",
|
|
1147
|
+
docsUrl: "https://developers.openai.com/api/docs/voice/media-streams/websocket-messages",
|
|
1148
|
+
severity: "warning"
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
key: "openai-realtime-send-safety-identifier",
|
|
1152
|
+
resultRule: "openai-realtime/security/send-safety-identifier",
|
|
1153
|
+
message: "This Realtime WebSocket connection does not send an OpenAI-Safety-Identifier header.",
|
|
1154
|
+
fix: "Add an OpenAI-Safety-Identifier header with a stable, privacy-preserving value (e.g. a hashed account/call id) to support abuse/safety monitoring.",
|
|
1155
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
|
|
1156
|
+
severity: "info"
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
key: "openai-realtime-transcription-model-choice",
|
|
1160
|
+
resultRule: "openai-realtime/correctness/transcription-model-choice",
|
|
1161
|
+
message: "input_audio_transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions.",
|
|
1162
|
+
fix: "Switch to 'gpt-realtime-whisper' if transcription output is consumed, or drop input_audio_transcription entirely if it isn't.",
|
|
1163
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
|
|
1164
|
+
severity: "info"
|
|
1165
|
+
}
|
|
1166
|
+
]
|
|
1167
|
+
};
|
|
1168
|
+
|
|
908
1169
|
// src/providers/index.ts
|
|
909
1170
|
var providers = [
|
|
910
1171
|
resendManifest,
|
|
@@ -914,7 +1175,10 @@ var providers = [
|
|
|
914
1175
|
lovableManifest,
|
|
915
1176
|
browserbaseManifest,
|
|
916
1177
|
openaiCuaManifest,
|
|
917
|
-
tiptapManifest
|
|
1178
|
+
tiptapManifest,
|
|
1179
|
+
elevenlabsManifest,
|
|
1180
|
+
twilioManifest,
|
|
1181
|
+
openaiRealtimeManifest
|
|
918
1182
|
];
|
|
919
1183
|
|
|
920
1184
|
// src/reporter/animate.ts
|
|
@@ -7001,145 +7265,2247 @@ var rule82 = {
|
|
|
7001
7265
|
};
|
|
7002
7266
|
var tiptapTiptapMarkdownMissingNodeSpecRule = rule82;
|
|
7003
7267
|
|
|
7004
|
-
// src/
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
"
|
|
7009
|
-
"resend-api-key-hardcoded": resendApiKeyHardcodedRule,
|
|
7010
|
-
"resend-api-key-in-client-bundle": resendApiKeyInClientBundleRule,
|
|
7011
|
-
"resend-marketing-via-batch-send": resendMarketingViaBatchSendRule,
|
|
7012
|
-
"resend-marketing-missing-unsubscribe": resendMarketingMissingUnsubscribeRule,
|
|
7013
|
-
"resend-test-domain-in-production-path": resendTestDomainInProductionPathRule,
|
|
7014
|
-
"resend-from-address-not-friendly-format": resendFromAddressNotFriendlyFormatRule,
|
|
7015
|
-
"resend-batch-size-not-enforced": resendBatchSizeNotEnforcedRule,
|
|
7016
|
-
"resend-missing-idempotency-key": resendMissingIdempotencyKeyRule,
|
|
7017
|
-
"resend-no-error-code-mapping": resendNoErrorCodeMappingRule,
|
|
7018
|
-
"resend-webhook-no-idempotency": resendWebhookNoIdempotencyRule,
|
|
7019
|
-
"resend-missing-tags": resendMissingTagsRule,
|
|
7020
|
-
"resend-request-id-not-logged": resendRequestIdNotLoggedRule,
|
|
7021
|
-
"supabase-scope-queries-by-tenant-column": supabaseScopeQueriesByTenantColumnRule,
|
|
7022
|
-
"supabase-validate-uuid-columns": supabaseValidateUuidColumnsRule,
|
|
7023
|
-
"supabase-order-by-timestamp-not-identity": supabaseOrderByTimestampNotIdentityRule,
|
|
7024
|
-
"supabase-consistent-input-length-limits": supabaseConsistentInputLengthLimitsRule,
|
|
7025
|
-
"supabase-idempotent-mutations": supabaseIdempotentMutationsRule,
|
|
7026
|
-
"supabase-fail-fast-env-validation": supabaseFailFastEnvValidationRule,
|
|
7027
|
-
"supabase-no-user-metadata-authz": supabaseNoUserMetadataAuthzRule,
|
|
7028
|
-
"supabase-single-without-error-check": supabaseSingleWithoutErrorCheckRule,
|
|
7029
|
-
"supabase-non-atomic-replace-pattern": supabaseNonAtomicReplacePatternRule,
|
|
7030
|
-
"supabase-unchecked-mutation-error": supabaseUncheckedMutationErrorRule,
|
|
7031
|
-
"supabase-realtime-missing-filter": supabaseRealtimeMissingFilterRule,
|
|
7032
|
-
"supabase-storage-error-not-surfaced": supabaseStorageErrorNotSurfacedRule,
|
|
7033
|
-
"auth0-required-audience-validation": auth0RequiredAudienceValidationRule,
|
|
7034
|
-
"auth0-no-account-link-without-verified-email": auth0NoAccountLinkWithoutVerifiedEmailRule,
|
|
7035
|
-
"auth0-dead-claim-verification-check": auth0DeadClaimVerificationCheckRule,
|
|
7036
|
-
"auth0-jwks-refresh-on-unknown-kid": auth0JwksRefreshOnUnknownKidRule,
|
|
7037
|
-
"firebase-missing-app-check": firebaseMissingAppCheckRule,
|
|
7038
|
-
"firebase-unhandled-auth-popup-rejection": firebaseUnhandledAuthPopupRejectionRule,
|
|
7039
|
-
"firebase-rtdb-list-read-for-single-item": firebaseRtdbListReadForSingleItemRule,
|
|
7040
|
-
"firebase-unvalidated-external-data-to-rtdb": firebaseUnvalidatedExternalDataToRtdbRule,
|
|
7041
|
-
"firebase-rtdb-batch-write-not-atomic": firebaseRtdbBatchWriteNotAtomicRule,
|
|
7042
|
-
"firebase-rtdb-listener-error-not-handled": firebaseRtdbListenerErrorNotHandledRule,
|
|
7043
|
-
"firebase-effect-deps-whole-user-object": firebaseEffectDepsWholeUserObjectRule,
|
|
7044
|
-
"firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
|
|
7045
|
-
"firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
|
|
7046
|
-
"firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
|
|
7047
|
-
"firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
|
|
7048
|
-
"firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
|
|
7049
|
-
"firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
|
|
7050
|
-
"firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
|
|
7051
|
-
"firebase-use-array-union-remove": firebaseUseArrayUnionRemoveRule,
|
|
7052
|
-
"firebase-duplicate-initialize-app": firebaseDuplicateInitializeAppRule,
|
|
7053
|
-
"firebase-onSnapshot-async-throw": firebaseOnSnapshotAsyncThrowRule,
|
|
7054
|
-
"firebase-onSnapshot-missing-error-callback": firebaseOnSnapshotMissingErrorCallbackRule,
|
|
7055
|
-
"firebase-firestore-document-size-guard": firebaseFirestoreDocumentSizeGuardRule,
|
|
7056
|
-
"firebase-use-timestamp-now": firebaseUseTimestampNowRule,
|
|
7057
|
-
"lovable-no-client-side-secret-fetch": lovableNoClientSideSecretFetchRule,
|
|
7058
|
-
"lovable-paid-flag-without-edge-function": lovablePaidFlagWithoutEdgeFunctionRule,
|
|
7059
|
-
"lovable-expiry-column-never-checked": lovableExpiryColumnNeverCheckedRule,
|
|
7060
|
-
"lovable-silent-catch-on-provider-call": lovableSilentCatchOnProviderCallRule,
|
|
7061
|
-
"browserbase-no-conditional-authz-on-anonymous-user": browserbaseNoConditionalAuthzOnAnonymousUserRule,
|
|
7062
|
-
"browserbase-no-connect-url-in-api-response": browserbaseNoConnectUrlInApiResponseRule,
|
|
7063
|
-
"browserbase-session-id-requires-ownership-check": browserbaseSessionIdRequiresOwnershipCheckRule,
|
|
7064
|
-
"browserbase-no-concurrent-shared-context": browserbaseNoConcurrentSharedContextRule,
|
|
7065
|
-
"browserbase-mobile-device-requires-os-setting": browserbaseMobileDeviceRequiresOsSettingRule,
|
|
7066
|
-
"browserbase-use-typed-exception-status-not-substring": browserbaseUseTypedExceptionStatusNotSubstringRule,
|
|
7067
|
-
"browserbase-release-session-on-connect-failure": browserbaseReleaseSessionOnConnectFailureRule,
|
|
7068
|
-
"browserbase-dont-stack-custom-retry-on-sdk-retry": browserbaseDontStackCustomRetryOnSdkRetryRule,
|
|
7069
|
-
"browserbase-no-overbroad-error-substring-match": browserbaseNoOverbroadErrorSubstringMatchRule,
|
|
7070
|
-
"browserbase-use-sdk-not-raw-requests": browserbaseUseSdkNotRawRequestsRule,
|
|
7071
|
-
"browserbase-centralize-request-release": browserbaseCentralizeRequestReleaseRule,
|
|
7072
|
-
"openai-cua-no-domain-allowlist": openaiCuaNoDomainAllowlistRule,
|
|
7073
|
-
"openai-cua-scroll-delta-default-zero": openaiCuaScrollDeltaDefaultZeroRule,
|
|
7074
|
-
"openai-cua-structured-step-metadata-not-text-json": openaiCuaStructuredStepMetadataNotTextJsonRule,
|
|
7075
|
-
"openai-cua-no-blind-safety-check-ack": openaiCuaNoBlindSafetyCheckAckRule,
|
|
7076
|
-
"openai-cua-retry-transient-turn-errors": openaiCuaRetryTransientTurnErrorsRule,
|
|
7077
|
-
"openai-cua-check-response-status-incomplete": openaiCuaCheckResponseStatusIncompleteRule,
|
|
7078
|
-
"openai-cua-set-safety-identifier": openaiCuaSetSafetyIdentifierRule,
|
|
7079
|
-
"tiptap-upload-validate-fn-void": tiptapUploadValidateFnVoidRule,
|
|
7080
|
-
"tiptap-script-src-hardcoded-api-key": tiptapScriptSrcHardcodedApiKeyRule,
|
|
7081
|
-
"tiptap-dynamic-script-no-sri": tiptapDynamicScriptNoSriRule,
|
|
7082
|
-
"tiptap-addAttributes-missing-renderHTML": tiptapAddAttributesMissingRenderHTMLRule,
|
|
7083
|
-
"tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
|
|
7084
|
-
"tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
|
|
7085
|
-
"tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
|
|
7086
|
-
"tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
|
|
7087
|
-
"tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
|
|
7088
|
-
"tiptap-prefer-table-kit": tiptapPreferTableKitRule,
|
|
7089
|
-
"tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule
|
|
7268
|
+
// src/providers/elevenlabs/utils.ts
|
|
7269
|
+
function isElevenLabsUrlArg(node) {
|
|
7270
|
+
if (!node) return false;
|
|
7271
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
7272
|
+
return node.value.includes("elevenlabs.io");
|
|
7090
7273
|
}
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
const registry2 = /* @__PURE__ */ new Map();
|
|
7096
|
-
for (const [key, rule83] of Object.entries(plugin.rules)) {
|
|
7097
|
-
const docs = rule83?.meta?.docs ?? {};
|
|
7098
|
-
registry2.set(key, {
|
|
7099
|
-
category: docs.category,
|
|
7100
|
-
description: docs.description ?? "",
|
|
7101
|
-
rationale: docs.rationale ?? "",
|
|
7102
|
-
docsUrl: docs.docsUrl,
|
|
7103
|
-
cwe: docs.cwe,
|
|
7104
|
-
owasp: docs.owasp
|
|
7105
|
-
});
|
|
7274
|
+
if (node.type === "TemplateLiteral") {
|
|
7275
|
+
return (node.quasis ?? []).some(
|
|
7276
|
+
(q) => typeof q?.value?.raw === "string" && q.value.raw.includes("elevenlabs.io")
|
|
7277
|
+
);
|
|
7106
7278
|
}
|
|
7107
|
-
return
|
|
7279
|
+
return false;
|
|
7108
7280
|
}
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7281
|
+
function findElevenLabsFetchCall(node, depth = 0) {
|
|
7282
|
+
if (!node || typeof node !== "object" || depth > 20) return null;
|
|
7283
|
+
if (Array.isArray(node)) {
|
|
7284
|
+
for (const n of node) {
|
|
7285
|
+
const found = findElevenLabsFetchCall(n, depth + 1);
|
|
7286
|
+
if (found) return found;
|
|
7287
|
+
}
|
|
7288
|
+
return null;
|
|
7289
|
+
}
|
|
7290
|
+
if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "fetch") {
|
|
7291
|
+
if (isElevenLabsUrlArg(node.arguments?.[0])) return node;
|
|
7292
|
+
}
|
|
7293
|
+
for (const key of Object.keys(node)) {
|
|
7294
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7295
|
+
const val = node[key];
|
|
7296
|
+
if (val && typeof val === "object") {
|
|
7297
|
+
const found = findElevenLabsFetchCall(val, depth + 1);
|
|
7298
|
+
if (found) return found;
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
return null;
|
|
7112
7302
|
}
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7303
|
+
function unwrapAwait(node) {
|
|
7304
|
+
return node?.type === "AwaitExpression" ? node.argument : node;
|
|
7305
|
+
}
|
|
7306
|
+
function collectVarDeclarators(node, out, depth = 0) {
|
|
7307
|
+
if (!node || typeof node !== "object" || depth > 24) return;
|
|
7308
|
+
if (Array.isArray(node)) {
|
|
7309
|
+
for (const n of node) collectVarDeclarators(n, out, depth + 1);
|
|
7310
|
+
return;
|
|
7311
|
+
}
|
|
7312
|
+
if (node.type === "VariableDeclarator") out.push(node);
|
|
7313
|
+
for (const key of Object.keys(node)) {
|
|
7314
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7315
|
+
const val = node[key];
|
|
7316
|
+
if (val && typeof val === "object") collectVarDeclarators(val, out, depth + 1);
|
|
7123
7317
|
}
|
|
7124
|
-
return { lines, highlightedLine: highlighted };
|
|
7125
7318
|
}
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
info: 2
|
|
7132
|
-
};
|
|
7133
|
-
function scoreToSeverityLabel(score) {
|
|
7134
|
-
if (score >= 80) return "excellent";
|
|
7135
|
-
if (score >= 60) return "good";
|
|
7136
|
-
if (score >= 40) return "needs-work";
|
|
7137
|
-
return "critical";
|
|
7319
|
+
function posOf(n) {
|
|
7320
|
+
if (typeof n?.range?.[0] === "number") return n.range[0];
|
|
7321
|
+
const line = n?.loc?.start?.line ?? 0;
|
|
7322
|
+
const column = n?.loc?.start?.column ?? 0;
|
|
7323
|
+
return line * 1e6 + column;
|
|
7138
7324
|
}
|
|
7139
7325
|
|
|
7140
|
-
// src/
|
|
7141
|
-
|
|
7142
|
-
|
|
7326
|
+
// src/providers/elevenlabs/rules/validate-signed-url-response.ts
|
|
7327
|
+
var rule83 = {
|
|
7328
|
+
meta: {
|
|
7329
|
+
type: "problem",
|
|
7330
|
+
docs: {
|
|
7331
|
+
description: "ElevenLabs signed URL responses must be validated before the signed_url field is used",
|
|
7332
|
+
category: "correctness",
|
|
7333
|
+
cwe: "CWE-252",
|
|
7334
|
+
rationale: "The signed-url endpoint assumes the ElevenLabs API always returns a well-formed body. If the API ever returns an unexpected shape (error payload, empty body, schema change), `data.signed_url` is silently `undefined` and the failure only surfaces downstream when the client tries to connect with an invalid URL.",
|
|
7335
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
|
|
7336
|
+
recommended: true
|
|
7337
|
+
},
|
|
7338
|
+
messages: {
|
|
7339
|
+
missingValidation: "This code reads signed_url from the ElevenLabs API response without checking that the field exists. A malformed or unexpected response will silently produce an undefined signed URL."
|
|
7340
|
+
}
|
|
7341
|
+
},
|
|
7342
|
+
create(context) {
|
|
7343
|
+
function analyzeFunction(fnNode) {
|
|
7344
|
+
const declarators = [];
|
|
7345
|
+
collectVarDeclarators(fnNode.body, declarators);
|
|
7346
|
+
const responseVarNames = [];
|
|
7347
|
+
for (const d of declarators) {
|
|
7348
|
+
const init = unwrapAwait(d.init);
|
|
7349
|
+
if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "fetch" && isElevenLabsUrlArg(init.arguments?.[0]) && d.id?.type === "Identifier") {
|
|
7350
|
+
responseVarNames.push(d.id.name);
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
if (responseVarNames.length === 0) return;
|
|
7354
|
+
for (const responseVarName of responseVarNames) {
|
|
7355
|
+
for (const d of declarators) {
|
|
7356
|
+
const init = unwrapAwait(d.init);
|
|
7357
|
+
if (init?.type === "CallExpression" && init.callee?.type === "MemberExpression" && init.callee.property?.type === "Identifier" && init.callee.property.name === "json" && init.callee.object?.type === "Identifier" && init.callee.object.name === responseVarName && d.id?.type === "Identifier") {
|
|
7358
|
+
analyzeDataVar(fnNode, d.id.name, d);
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
}
|
|
7362
|
+
}
|
|
7363
|
+
function analyzeDataVar(fnNode, dataVarName, dataDeclaratorNode) {
|
|
7364
|
+
function isSignedUrlMember(n) {
|
|
7365
|
+
if (n?.type !== "MemberExpression") return false;
|
|
7366
|
+
if (n.object?.type !== "Identifier" || n.object.name !== dataVarName) return false;
|
|
7367
|
+
if (!n.computed) return n.property?.type === "Identifier" && n.property.name === "signed_url";
|
|
7368
|
+
return n.property?.type === "Literal" && n.property.value === "signed_url";
|
|
7369
|
+
}
|
|
7370
|
+
function isNullishLiteral(n) {
|
|
7371
|
+
return n?.type === "Identifier" && n.name === "undefined" || n?.type === "Literal" && n.value === null;
|
|
7372
|
+
}
|
|
7373
|
+
function isFalsyGuardOnSignedUrl(test) {
|
|
7374
|
+
if (!test) return false;
|
|
7375
|
+
if (test.type === "UnaryExpression" && test.operator === "!") {
|
|
7376
|
+
return isSignedUrlMember(test.argument);
|
|
7377
|
+
}
|
|
7378
|
+
if (test.type === "BinaryExpression" && (test.operator === "==" || test.operator === "===")) {
|
|
7379
|
+
return isSignedUrlMember(test.left) && isNullishLiteral(test.right) || isSignedUrlMember(test.right) && isNullishLiteral(test.left);
|
|
7380
|
+
}
|
|
7381
|
+
if (test.type === "LogicalExpression") {
|
|
7382
|
+
return isFalsyGuardOnSignedUrl(test.left) || isFalsyGuardOnSignedUrl(test.right);
|
|
7383
|
+
}
|
|
7384
|
+
return false;
|
|
7385
|
+
}
|
|
7386
|
+
let guardPos = null;
|
|
7387
|
+
let usagePos = null;
|
|
7388
|
+
function walk3(n, depth = 0) {
|
|
7389
|
+
if (!n || typeof n !== "object" || depth > 40) return;
|
|
7390
|
+
if (Array.isArray(n)) {
|
|
7391
|
+
for (const item of n) walk3(item, depth + 1);
|
|
7392
|
+
return;
|
|
7393
|
+
}
|
|
7394
|
+
if (n.type === "IfStatement" && isFalsyGuardOnSignedUrl(n.test)) {
|
|
7395
|
+
const p = posOf(n);
|
|
7396
|
+
if (guardPos === null || p < guardPos) guardPos = p;
|
|
7397
|
+
}
|
|
7398
|
+
if (isSignedUrlMember(n) && n !== dataDeclaratorNode) {
|
|
7399
|
+
const p = posOf(n);
|
|
7400
|
+
if (usagePos === null || p < usagePos) usagePos = p;
|
|
7401
|
+
}
|
|
7402
|
+
for (const key of Object.keys(n)) {
|
|
7403
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7404
|
+
const val = n[key];
|
|
7405
|
+
if (val && typeof val === "object") walk3(val, depth + 1);
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
walk3(fnNode.body);
|
|
7409
|
+
if (usagePos === null) return;
|
|
7410
|
+
if (guardPos !== null && guardPos < usagePos) return;
|
|
7411
|
+
context.report({ node: dataDeclaratorNode, messageId: "missingValidation" });
|
|
7412
|
+
}
|
|
7413
|
+
return {
|
|
7414
|
+
FunctionDeclaration(node) {
|
|
7415
|
+
analyzeFunction(node);
|
|
7416
|
+
},
|
|
7417
|
+
FunctionExpression(node) {
|
|
7418
|
+
analyzeFunction(node);
|
|
7419
|
+
},
|
|
7420
|
+
ArrowFunctionExpression(node) {
|
|
7421
|
+
analyzeFunction(node);
|
|
7422
|
+
}
|
|
7423
|
+
};
|
|
7424
|
+
}
|
|
7425
|
+
};
|
|
7426
|
+
var elevenlabsValidateSignedUrlResponseRule = rule83;
|
|
7427
|
+
|
|
7428
|
+
// src/providers/elevenlabs/rules/no-error-object-logging.ts
|
|
7429
|
+
var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
|
|
7430
|
+
var rule84 = {
|
|
7431
|
+
meta: {
|
|
7432
|
+
type: "problem",
|
|
7433
|
+
docs: {
|
|
7434
|
+
description: "Catch blocks must not log the raw caught error object",
|
|
7435
|
+
category: "security",
|
|
7436
|
+
cwe: "CWE-532",
|
|
7437
|
+
rationale: "Error objects thrown by fetch/SDK calls can carry response bodies, headers, or internal state. Logging them directly with console.error(error) writes that data verbatim into server logs, which may be shipped to third-party log aggregators or be readable by anyone with log access.",
|
|
7438
|
+
docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
|
|
7439
|
+
recommended: true
|
|
7440
|
+
},
|
|
7441
|
+
messages: {
|
|
7442
|
+
rawErrorLogged: "This catch block logs the raw error object instead of a sanitized message \u2014 error responses, headers, or internal state may leak into server logs."
|
|
7443
|
+
}
|
|
7444
|
+
},
|
|
7445
|
+
create(context) {
|
|
7446
|
+
function isConsoleLogCall(node) {
|
|
7447
|
+
if (node?.type !== "CallExpression") return false;
|
|
7448
|
+
const callee = node.callee;
|
|
7449
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
7450
|
+
if (callee.object?.type !== "Identifier" || callee.object.name !== "console") return false;
|
|
7451
|
+
return callee.property?.type === "Identifier" && LOGGING_METHODS.has(callee.property.name);
|
|
7452
|
+
}
|
|
7453
|
+
function referencesRawIdentifier(node, paramName, depth = 0) {
|
|
7454
|
+
if (!node || typeof node !== "object" || depth > 10) return false;
|
|
7455
|
+
if (Array.isArray(node)) return node.some((n) => referencesRawIdentifier(n, paramName, depth + 1));
|
|
7456
|
+
if (node.type === "Identifier" && node.name === paramName) return true;
|
|
7457
|
+
if (node.type === "ObjectExpression") {
|
|
7458
|
+
return (node.properties ?? []).some((p) => {
|
|
7459
|
+
if (p.type !== "Property") return false;
|
|
7460
|
+
return referencesRawIdentifier(p.value, paramName, depth + 1);
|
|
7461
|
+
});
|
|
7462
|
+
}
|
|
7463
|
+
if (node.type === "ArrayExpression") {
|
|
7464
|
+
return (node.elements ?? []).some((el) => referencesRawIdentifier(el, paramName, depth + 1));
|
|
7465
|
+
}
|
|
7466
|
+
return false;
|
|
7467
|
+
}
|
|
7468
|
+
return {
|
|
7469
|
+
TryStatement(node) {
|
|
7470
|
+
if (!findElevenLabsFetchCall(node.block)) return;
|
|
7471
|
+
const handler = node.handler;
|
|
7472
|
+
const paramName = handler?.param?.type === "Identifier" ? handler.param.name : null;
|
|
7473
|
+
if (!paramName) return;
|
|
7474
|
+
function walk3(n, depth = 0) {
|
|
7475
|
+
if (!n || typeof n !== "object" || depth > 30) return;
|
|
7476
|
+
if (Array.isArray(n)) {
|
|
7477
|
+
for (const item of n) walk3(item, depth + 1);
|
|
7478
|
+
return;
|
|
7479
|
+
}
|
|
7480
|
+
if (isConsoleLogCall(n)) {
|
|
7481
|
+
const args = n.arguments ?? [];
|
|
7482
|
+
const loggedRaw = args.some((a) => referencesRawIdentifier(a, paramName));
|
|
7483
|
+
if (loggedRaw) {
|
|
7484
|
+
context.report({ node: n, messageId: "rawErrorLogged" });
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
for (const key of Object.keys(n)) {
|
|
7488
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7489
|
+
const val = n[key];
|
|
7490
|
+
if (val && typeof val === "object") walk3(val, depth + 1);
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
walk3(handler.body);
|
|
7494
|
+
}
|
|
7495
|
+
};
|
|
7496
|
+
}
|
|
7497
|
+
};
|
|
7498
|
+
var elevenlabsNoErrorObjectLoggingRule = rule84;
|
|
7499
|
+
|
|
7500
|
+
// src/providers/elevenlabs/rules/fetch-timeout-required.ts
|
|
7501
|
+
var rule85 = {
|
|
7502
|
+
meta: {
|
|
7503
|
+
type: "suggestion",
|
|
7504
|
+
docs: {
|
|
7505
|
+
description: "Fetch calls to the ElevenLabs API must set an abort timeout",
|
|
7506
|
+
category: "reliability",
|
|
7507
|
+
rationale: "Native fetch has no default timeout. If the ElevenLabs API becomes slow or hangs, a request with no AbortController/signal will wait indefinitely, tying up the request handler and starving the server of available connections.",
|
|
7508
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
|
|
7509
|
+
recommended: true
|
|
7510
|
+
},
|
|
7511
|
+
messages: {
|
|
7512
|
+
missingTimeout: "This fetch call to the ElevenLabs API has no abort signal/timeout \u2014 a slow or unresponsive API will hang the request indefinitely."
|
|
7513
|
+
}
|
|
7514
|
+
},
|
|
7515
|
+
create(context) {
|
|
7516
|
+
function hasSignalOption(optionsArg) {
|
|
7517
|
+
if (optionsArg?.type !== "ObjectExpression") return false;
|
|
7518
|
+
return (optionsArg.properties ?? []).some((p) => {
|
|
7519
|
+
if (p.type === "SpreadElement") return true;
|
|
7520
|
+
return p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "signal";
|
|
7521
|
+
});
|
|
7522
|
+
}
|
|
7523
|
+
return {
|
|
7524
|
+
CallExpression(node) {
|
|
7525
|
+
if (node.callee?.type !== "Identifier" || node.callee.name !== "fetch") return;
|
|
7526
|
+
if (!isElevenLabsUrlArg(node.arguments?.[0])) return;
|
|
7527
|
+
const optionsArg = node.arguments?.[1];
|
|
7528
|
+
if (hasSignalOption(optionsArg)) return;
|
|
7529
|
+
context.report({ node, messageId: "missingTimeout" });
|
|
7530
|
+
}
|
|
7531
|
+
};
|
|
7532
|
+
}
|
|
7533
|
+
};
|
|
7534
|
+
var elevenlabsFetchTimeoutRequiredRule = rule85;
|
|
7535
|
+
|
|
7536
|
+
// src/providers/elevenlabs/rules/validate-agent-id-format.ts
|
|
7537
|
+
var rule86 = {
|
|
7538
|
+
meta: {
|
|
7539
|
+
type: "problem",
|
|
7540
|
+
docs: {
|
|
7541
|
+
description: "agentId must be format-validated, not just checked for existence, before use",
|
|
7542
|
+
category: "correctness",
|
|
7543
|
+
cwe: "CWE-20",
|
|
7544
|
+
rationale: "A query-param agentId that is only checked with `if (!agentId)` accepts any non-empty string. An attacker can pass arbitrary values \u2014 oversized strings, path-traversal-like sequences, or characters the ElevenLabs API was never designed to receive \u2014 directly into the request URL, producing undefined behavior instead of a clean 400.",
|
|
7545
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/customization/authentication",
|
|
7546
|
+
recommended: true
|
|
7547
|
+
},
|
|
7548
|
+
messages: {
|
|
7549
|
+
missingFormatValidation: "agentId is checked for existence but never validated against an expected format before being used in an ElevenLabs API request."
|
|
7550
|
+
}
|
|
7551
|
+
},
|
|
7552
|
+
create(context) {
|
|
7553
|
+
function isSearchParamsGetAgentId(node) {
|
|
7554
|
+
if (node?.type !== "CallExpression") return false;
|
|
7555
|
+
const callee = node.callee;
|
|
7556
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
7557
|
+
if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
|
|
7558
|
+
const arg = node.arguments?.[0];
|
|
7559
|
+
return arg?.type === "Literal" && arg.value === "agentId";
|
|
7560
|
+
}
|
|
7561
|
+
function analyzeFunction(fnNode) {
|
|
7562
|
+
const declarators = [];
|
|
7563
|
+
collectVarDeclarators(fnNode.body, declarators);
|
|
7564
|
+
let agentIdVarName = null;
|
|
7565
|
+
let agentIdDeclaratorNode = null;
|
|
7566
|
+
for (const d of declarators) {
|
|
7567
|
+
const init = unwrapAwait(d.init);
|
|
7568
|
+
if (isSearchParamsGetAgentId(init) && d.id?.type === "Identifier") {
|
|
7569
|
+
agentIdVarName = d.id.name;
|
|
7570
|
+
agentIdDeclaratorNode = d;
|
|
7571
|
+
}
|
|
7572
|
+
}
|
|
7573
|
+
if (!agentIdVarName) {
|
|
7574
|
+
for (const param of fnNode.params ?? []) {
|
|
7575
|
+
if (param?.type === "Identifier" && param.name === "agentId") {
|
|
7576
|
+
agentIdVarName = param.name;
|
|
7577
|
+
agentIdDeclaratorNode = param;
|
|
7578
|
+
}
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
if (!agentIdVarName) return;
|
|
7582
|
+
function isAgentIdMember(n) {
|
|
7583
|
+
return n?.type === "Identifier" && n.name === agentIdVarName;
|
|
7584
|
+
}
|
|
7585
|
+
function isFormatCheck(n, depth = 0) {
|
|
7586
|
+
if (!n || typeof n !== "object" || depth > 10) return false;
|
|
7587
|
+
if (Array.isArray(n)) return n.some((x) => isFormatCheck(x, depth + 1));
|
|
7588
|
+
if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
|
|
7589
|
+
const propName2 = n.callee.property?.type === "Identifier" ? n.callee.property.name : null;
|
|
7590
|
+
if (propName2 === "test" && isAgentIdMember(n.arguments?.[0])) return true;
|
|
7591
|
+
if ((propName2 === "match" || propName2 === "matchAll") && isAgentIdMember(n.callee.object)) return true;
|
|
7592
|
+
}
|
|
7593
|
+
if (n.type === "LogicalExpression") {
|
|
7594
|
+
return isFormatCheck(n.left, depth + 1) || isFormatCheck(n.right, depth + 1);
|
|
7595
|
+
}
|
|
7596
|
+
if (n.type === "UnaryExpression") return isFormatCheck(n.argument, depth + 1);
|
|
7597
|
+
return false;
|
|
7598
|
+
}
|
|
7599
|
+
let formatGuardPos = null;
|
|
7600
|
+
let usagePos = null;
|
|
7601
|
+
function walk3(n, depth = 0) {
|
|
7602
|
+
if (!n || typeof n !== "object" || depth > 40) return;
|
|
7603
|
+
if (Array.isArray(n)) {
|
|
7604
|
+
for (const item of n) walk3(item, depth + 1);
|
|
7605
|
+
return;
|
|
7606
|
+
}
|
|
7607
|
+
if (n.type === "IfStatement" && isFormatCheck(n.test)) {
|
|
7608
|
+
const p = posOf(n);
|
|
7609
|
+
if (formatGuardPos === null || p < formatGuardPos) formatGuardPos = p;
|
|
7610
|
+
}
|
|
7611
|
+
if (n.type === "CallExpression" && n.callee?.type === "Identifier" && n.callee.name === "fetch" && isElevenLabsUrlArg(n.arguments?.[0])) {
|
|
7612
|
+
const urlArg = n.arguments[0];
|
|
7613
|
+
const containsAgentId = urlArg.type === "TemplateLiteral" && (urlArg.expressions ?? []).some((e) => isAgentIdMember(e));
|
|
7614
|
+
if (containsAgentId) {
|
|
7615
|
+
const p = posOf(n);
|
|
7616
|
+
if (usagePos === null || p < usagePos) usagePos = p;
|
|
7617
|
+
}
|
|
7618
|
+
}
|
|
7619
|
+
for (const key of Object.keys(n)) {
|
|
7620
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7621
|
+
const val = n[key];
|
|
7622
|
+
if (val && typeof val === "object") walk3(val, depth + 1);
|
|
7623
|
+
}
|
|
7624
|
+
}
|
|
7625
|
+
walk3(fnNode.body);
|
|
7626
|
+
if (usagePos === null) return;
|
|
7627
|
+
if (formatGuardPos !== null && formatGuardPos < usagePos) return;
|
|
7628
|
+
context.report({ node: agentIdDeclaratorNode, messageId: "missingFormatValidation" });
|
|
7629
|
+
}
|
|
7630
|
+
return {
|
|
7631
|
+
FunctionDeclaration(node) {
|
|
7632
|
+
analyzeFunction(node);
|
|
7633
|
+
},
|
|
7634
|
+
FunctionExpression(node) {
|
|
7635
|
+
analyzeFunction(node);
|
|
7636
|
+
},
|
|
7637
|
+
ArrowFunctionExpression(node) {
|
|
7638
|
+
analyzeFunction(node);
|
|
7639
|
+
}
|
|
7640
|
+
};
|
|
7641
|
+
}
|
|
7642
|
+
};
|
|
7643
|
+
var elevenlabsValidateAgentIdFormatRule = rule86;
|
|
7644
|
+
|
|
7645
|
+
// src/providers/elevenlabs/rules/secure-session-id-generation.ts
|
|
7646
|
+
var rule87 = {
|
|
7647
|
+
meta: {
|
|
7648
|
+
type: "problem",
|
|
7649
|
+
docs: {
|
|
7650
|
+
description: "Session ids must be generated with a cryptographically secure RNG",
|
|
7651
|
+
category: "security",
|
|
7652
|
+
cwe: "CWE-338",
|
|
7653
|
+
rationale: 'Math.random() is a non-cryptographic PRNG \u2014 its output can be predicted by an attacker who observes enough samples or knows the engine implementation. If a "session_*" value derived from it is later trusted for routing, deduplication, or any access-control-adjacent decision, that predictability can be exploited.',
|
|
7654
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-api/guides/how-to/best-practices/security",
|
|
7655
|
+
recommended: true
|
|
7656
|
+
},
|
|
7657
|
+
messages: {
|
|
7658
|
+
insecureSessionId: "This session id is generated with Math.random(), which is not cryptographically secure and can be predicted. Use crypto.getRandomValues() instead."
|
|
7659
|
+
}
|
|
7660
|
+
},
|
|
7661
|
+
create(context) {
|
|
7662
|
+
function containsMathRandomCall(node, depth = 0) {
|
|
7663
|
+
if (!node || typeof node !== "object" || depth > 20) return false;
|
|
7664
|
+
if (Array.isArray(node)) return node.some((n) => containsMathRandomCall(n, depth + 1));
|
|
7665
|
+
if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "Math" && node.callee.property?.type === "Identifier" && node.callee.property.name === "random") {
|
|
7666
|
+
return true;
|
|
7667
|
+
}
|
|
7668
|
+
for (const key of Object.keys(node)) {
|
|
7669
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7670
|
+
const val = node[key];
|
|
7671
|
+
if (val && typeof val === "object" && containsMathRandomCall(val, depth + 1)) return true;
|
|
7672
|
+
}
|
|
7673
|
+
return false;
|
|
7674
|
+
}
|
|
7675
|
+
function looksLikeSessionId(name) {
|
|
7676
|
+
return typeof name === "string" && /session/i.test(name);
|
|
7677
|
+
}
|
|
7678
|
+
function isSessionIdValue(init) {
|
|
7679
|
+
if (init?.type === "TemplateLiteral") {
|
|
7680
|
+
const firstQuasi = init.quasis?.[0]?.value?.raw ?? "";
|
|
7681
|
+
if (/session[-_]?/i.test(firstQuasi) && containsMathRandomCall(init)) return true;
|
|
7682
|
+
}
|
|
7683
|
+
if (init?.type === "BinaryExpression" && init.operator === "+") {
|
|
7684
|
+
const leftLiteral = init.left?.type === "Literal" && typeof init.left.value === "string";
|
|
7685
|
+
if (leftLiteral && /session[-_]?/i.test(init.left.value) && containsMathRandomCall(init)) return true;
|
|
7686
|
+
}
|
|
7687
|
+
return false;
|
|
7688
|
+
}
|
|
7689
|
+
return {
|
|
7690
|
+
VariableDeclarator(node) {
|
|
7691
|
+
const init = node.init;
|
|
7692
|
+
if (!init) return;
|
|
7693
|
+
const declaredName = node.id?.type === "Identifier" ? node.id.name : node.id?.type === "ArrayPattern" && node.id.elements?.[0]?.type === "Identifier" ? node.id.elements[0].name : void 0;
|
|
7694
|
+
let valueNode = init;
|
|
7695
|
+
if (init.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "useState" && init.arguments?.[0]?.type === "ArrowFunctionExpression") {
|
|
7696
|
+
const body = init.arguments[0].body;
|
|
7697
|
+
valueNode = body?.type === "BlockStatement" ? null : body;
|
|
7698
|
+
if (body?.type === "BlockStatement") {
|
|
7699
|
+
const returnStmt = (body.body ?? []).find((s) => s.type === "ReturnStatement");
|
|
7700
|
+
valueNode = returnStmt?.argument ?? null;
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7703
|
+
if (!valueNode) return;
|
|
7704
|
+
if (!looksLikeSessionId(declaredName) && !isSessionIdValue(valueNode)) return;
|
|
7705
|
+
if (!containsMathRandomCall(valueNode)) return;
|
|
7706
|
+
context.report({ node, messageId: "insecureSessionId" });
|
|
7707
|
+
}
|
|
7708
|
+
};
|
|
7709
|
+
}
|
|
7710
|
+
};
|
|
7711
|
+
var elevenlabsSecureSessionIdGenerationRule = rule87;
|
|
7712
|
+
|
|
7713
|
+
// src/providers/elevenlabs/rules/conversation-error-recovery.ts
|
|
7714
|
+
var rule88 = {
|
|
7715
|
+
meta: {
|
|
7716
|
+
type: "suggestion",
|
|
7717
|
+
docs: {
|
|
7718
|
+
description: "A loading flag set before starting a conversation must be reset on failure",
|
|
7719
|
+
category: "reliability",
|
|
7720
|
+
rationale: 'If startConversation() sets isLoading true and getSignedUrl()/Conversation.startSession() throws, the loading flag must be reset in the catch or finally block. Otherwise the UI is stuck "loading" forever and the user can trigger overlapping conversation attempts by clicking again.',
|
|
7721
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
|
|
7722
|
+
recommended: true
|
|
7723
|
+
},
|
|
7724
|
+
messages: {
|
|
7725
|
+
missingLoadingReset: "This function sets a loading flag true before a try block, but neither the catch nor a finally block resets it to false on failure."
|
|
7726
|
+
}
|
|
7727
|
+
},
|
|
7728
|
+
create(context) {
|
|
7729
|
+
function isSetterCallWithBoolean(n, setterName, expected) {
|
|
7730
|
+
if (n?.type !== "CallExpression") return false;
|
|
7731
|
+
if (n.callee?.type !== "Identifier" || n.callee.name !== setterName) return false;
|
|
7732
|
+
const arg = n.arguments?.[0];
|
|
7733
|
+
return arg?.type === "Literal" && arg.value === expected;
|
|
7734
|
+
}
|
|
7735
|
+
function containsCall(node, predicate, depth = 0) {
|
|
7736
|
+
if (!node || typeof node !== "object" || depth > 20) return false;
|
|
7737
|
+
if (Array.isArray(node)) return node.some((n) => containsCall(n, predicate, depth + 1));
|
|
7738
|
+
if (predicate(node)) return true;
|
|
7739
|
+
for (const key of Object.keys(node)) {
|
|
7740
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7741
|
+
const val = node[key];
|
|
7742
|
+
if (val && typeof val === "object" && containsCall(val, predicate, depth + 1)) return true;
|
|
7743
|
+
}
|
|
7744
|
+
return false;
|
|
7745
|
+
}
|
|
7746
|
+
function findLoadingSetterFromUseState(fnNode) {
|
|
7747
|
+
let setterName = null;
|
|
7748
|
+
function collect(n, depth = 0) {
|
|
7749
|
+
if (setterName || !n || typeof n !== "object" || depth > 20) return;
|
|
7750
|
+
if (Array.isArray(n)) {
|
|
7751
|
+
for (const item of n) collect(item, depth + 1);
|
|
7752
|
+
return;
|
|
7753
|
+
}
|
|
7754
|
+
if (n.type === "VariableDeclarator" && n.id?.type === "ArrayPattern" && n.id.elements?.length === 2 && n.id.elements[0]?.type === "Identifier" && /loading/i.test(n.id.elements[0].name) && n.id.elements[1]?.type === "Identifier" && n.init?.type === "CallExpression" && n.init.callee?.type === "Identifier" && n.init.callee.name === "useState") {
|
|
7755
|
+
setterName = n.id.elements[1].name;
|
|
7756
|
+
return;
|
|
7757
|
+
}
|
|
7758
|
+
for (const key of Object.keys(n)) {
|
|
7759
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7760
|
+
const val = n[key];
|
|
7761
|
+
if (val && typeof val === "object") collect(val, depth + 1);
|
|
7762
|
+
}
|
|
7763
|
+
}
|
|
7764
|
+
collect(fnNode);
|
|
7765
|
+
return setterName;
|
|
7766
|
+
}
|
|
7767
|
+
function isLoadingTrueStatement(stmt, setterName) {
|
|
7768
|
+
return stmt?.type === "ExpressionStatement" && isSetterCallWithBoolean(stmt.expression, setterName, true);
|
|
7769
|
+
}
|
|
7770
|
+
function analyzeFunction(fnNode) {
|
|
7771
|
+
const setterName = findLoadingSetterFromUseState(fnNode);
|
|
7772
|
+
if (!setterName) return;
|
|
7773
|
+
function walk3(n, depth = 0) {
|
|
7774
|
+
if (!n || typeof n !== "object" || depth > 30) return;
|
|
7775
|
+
if (Array.isArray(n)) {
|
|
7776
|
+
for (const item of n) walk3(item, depth + 1);
|
|
7777
|
+
return;
|
|
7778
|
+
}
|
|
7779
|
+
if (n.type === "BlockStatement" && Array.isArray(n.body)) {
|
|
7780
|
+
for (let i = 0; i < n.body.length - 1; i++) {
|
|
7781
|
+
if (!isLoadingTrueStatement(n.body[i], setterName)) continue;
|
|
7782
|
+
const tryNode = n.body[i + 1];
|
|
7783
|
+
if (tryNode?.type !== "TryStatement") continue;
|
|
7784
|
+
const handler = tryNode.handler;
|
|
7785
|
+
const finalizer = tryNode.finalizer;
|
|
7786
|
+
const finallyResets = finalizer && containsCall(finalizer, (x) => isSetterCallWithBoolean(x, setterName, false));
|
|
7787
|
+
const catchResets = handler && containsCall(handler.body, (x) => isSetterCallWithBoolean(x, setterName, false));
|
|
7788
|
+
if (!finallyResets && !catchResets) {
|
|
7789
|
+
context.report({ node: tryNode, messageId: "missingLoadingReset" });
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7793
|
+
for (const key of Object.keys(n)) {
|
|
7794
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7795
|
+
const val = n[key];
|
|
7796
|
+
if (val && typeof val === "object") walk3(val, depth + 1);
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7799
|
+
walk3(fnNode.body);
|
|
7800
|
+
}
|
|
7801
|
+
return {
|
|
7802
|
+
FunctionDeclaration(node) {
|
|
7803
|
+
analyzeFunction(node);
|
|
7804
|
+
},
|
|
7805
|
+
FunctionExpression(node) {
|
|
7806
|
+
analyzeFunction(node);
|
|
7807
|
+
},
|
|
7808
|
+
ArrowFunctionExpression(node) {
|
|
7809
|
+
analyzeFunction(node);
|
|
7810
|
+
}
|
|
7811
|
+
};
|
|
7812
|
+
}
|
|
7813
|
+
};
|
|
7814
|
+
var elevenlabsConversationErrorRecoveryRule = rule88;
|
|
7815
|
+
|
|
7816
|
+
// src/providers/elevenlabs/rules/api-version-pinning.ts
|
|
7817
|
+
var rule89 = {
|
|
7818
|
+
meta: {
|
|
7819
|
+
type: "suggestion",
|
|
7820
|
+
docs: {
|
|
7821
|
+
description: "ElevenLabs API requests should pin an explicit API version header",
|
|
7822
|
+
category: "reliability",
|
|
7823
|
+
rationale: "The endpoint is hardcoded to /v1 in the URL path with no explicit version header. If ElevenLabs ships a v2 API and changes default response behavior for unversioned clients, requests with no version header silently pick up the new behavior instead of failing loudly or staying pinned.",
|
|
7824
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/breaking-changes-policy",
|
|
7825
|
+
recommended: true
|
|
7826
|
+
},
|
|
7827
|
+
messages: {
|
|
7828
|
+
missingVersionHeader: "This fetch call to the ElevenLabs API has no explicit API version header \u2014 a future API change could silently alter behavior."
|
|
7829
|
+
}
|
|
7830
|
+
},
|
|
7831
|
+
create(context) {
|
|
7832
|
+
function hasVersionHeader(optionsArg) {
|
|
7833
|
+
if (optionsArg?.type !== "ObjectExpression") return false;
|
|
7834
|
+
const headersProp = (optionsArg.properties ?? []).find(
|
|
7835
|
+
(p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "headers"
|
|
7836
|
+
);
|
|
7837
|
+
if (!headersProp) return false;
|
|
7838
|
+
if (headersProp.value?.type !== "ObjectExpression") return true;
|
|
7839
|
+
return (headersProp.value.properties ?? []).some((p) => {
|
|
7840
|
+
if (p.type === "SpreadElement") return true;
|
|
7841
|
+
if (p.type !== "Property") return false;
|
|
7842
|
+
const keyName = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
|
|
7843
|
+
return typeof keyName === "string" && /version/i.test(keyName);
|
|
7844
|
+
});
|
|
7845
|
+
}
|
|
7846
|
+
return {
|
|
7847
|
+
CallExpression(node) {
|
|
7848
|
+
if (node.callee?.type !== "Identifier" || node.callee.name !== "fetch") return;
|
|
7849
|
+
if (!isElevenLabsUrlArg(node.arguments?.[0])) return;
|
|
7850
|
+
const optionsArg = node.arguments?.[1];
|
|
7851
|
+
if (hasVersionHeader(optionsArg)) return;
|
|
7852
|
+
context.report({ node, messageId: "missingVersionHeader" });
|
|
7853
|
+
}
|
|
7854
|
+
};
|
|
7855
|
+
}
|
|
7856
|
+
};
|
|
7857
|
+
var elevenlabsApiVersionPinningRule = rule89;
|
|
7858
|
+
|
|
7859
|
+
// src/providers/elevenlabs/rules/check-http-status-before-json.ts
|
|
7860
|
+
var rule90 = {
|
|
7861
|
+
meta: {
|
|
7862
|
+
type: "problem",
|
|
7863
|
+
docs: {
|
|
7864
|
+
description: "response.json() must not be called before checking the HTTP status of an ElevenLabs response",
|
|
7865
|
+
category: "correctness",
|
|
7866
|
+
rationale: "Calling response.json() unconditionally parses whatever body the server returned, including error payloads, as if it were a successful response. Without checking response.ok (or response.status) first, a 4xx/5xx error from the ElevenLabs API is silently treated as valid data.",
|
|
7867
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/errors",
|
|
7868
|
+
recommended: true
|
|
7869
|
+
},
|
|
7870
|
+
messages: {
|
|
7871
|
+
missingStatusCheck: "response.json() is called on this ElevenLabs API response without first checking response.ok/status \u2014 an error response will be parsed as if it were valid data."
|
|
7872
|
+
}
|
|
7873
|
+
},
|
|
7874
|
+
create(context) {
|
|
7875
|
+
function analyzeFunction(fnNode) {
|
|
7876
|
+
const declarators = [];
|
|
7877
|
+
collectVarDeclarators(fnNode.body, declarators);
|
|
7878
|
+
const responseVarNames = [];
|
|
7879
|
+
for (const d of declarators) {
|
|
7880
|
+
const init = unwrapAwait(d.init);
|
|
7881
|
+
if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "fetch" && isElevenLabsUrlArg(init.arguments?.[0]) && d.id?.type === "Identifier") {
|
|
7882
|
+
responseVarNames.push(d.id.name);
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7885
|
+
for (const responseVarName of responseVarNames) {
|
|
7886
|
+
analyzeResponseVar(fnNode, responseVarName);
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
function analyzeResponseVar(fnNode, responseVarName) {
|
|
7890
|
+
function isResponseStatusCheck(test) {
|
|
7891
|
+
if (!test) return false;
|
|
7892
|
+
if (test.type === "UnaryExpression" && test.operator === "!") return isResponseStatusCheck(test.argument);
|
|
7893
|
+
if (test.type === "MemberExpression" && test.object?.type === "Identifier" && test.object.name === responseVarName && test.property?.type === "Identifier" && test.property.name === "ok") {
|
|
7894
|
+
return true;
|
|
7895
|
+
}
|
|
7896
|
+
if (test.type === "BinaryExpression") {
|
|
7897
|
+
const sideIsStatus = (n) => n?.type === "MemberExpression" && n.object?.type === "Identifier" && n.object.name === responseVarName && n.property?.type === "Identifier" && n.property.name === "status";
|
|
7898
|
+
if (sideIsStatus(test.left) || sideIsStatus(test.right)) return true;
|
|
7899
|
+
}
|
|
7900
|
+
if (test.type === "LogicalExpression") {
|
|
7901
|
+
return isResponseStatusCheck(test.left) || isResponseStatusCheck(test.right);
|
|
7902
|
+
}
|
|
7903
|
+
return false;
|
|
7904
|
+
}
|
|
7905
|
+
function isResponseJsonCall2(n) {
|
|
7906
|
+
if (n?.type !== "CallExpression") return false;
|
|
7907
|
+
const callee = n.callee;
|
|
7908
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
7909
|
+
if (callee.property?.type !== "Identifier" || callee.property.name !== "json") return false;
|
|
7910
|
+
return callee.object?.type === "Identifier" && callee.object.name === responseVarName;
|
|
7911
|
+
}
|
|
7912
|
+
let guardPos = null;
|
|
7913
|
+
let jsonCallNode = null;
|
|
7914
|
+
let jsonCallPos = null;
|
|
7915
|
+
function walk3(n, depth = 0) {
|
|
7916
|
+
if (!n || typeof n !== "object" || depth > 40) return;
|
|
7917
|
+
if (Array.isArray(n)) {
|
|
7918
|
+
for (const item of n) walk3(item, depth + 1);
|
|
7919
|
+
return;
|
|
7920
|
+
}
|
|
7921
|
+
if (n.type === "IfStatement" && isResponseStatusCheck(n.test)) {
|
|
7922
|
+
const p = posOf(n);
|
|
7923
|
+
if (guardPos === null || p < guardPos) guardPos = p;
|
|
7924
|
+
}
|
|
7925
|
+
if (isResponseJsonCall2(n)) {
|
|
7926
|
+
const p = posOf(n);
|
|
7927
|
+
if (jsonCallPos === null || p < jsonCallPos) {
|
|
7928
|
+
jsonCallPos = p;
|
|
7929
|
+
jsonCallNode = n;
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
for (const key of Object.keys(n)) {
|
|
7933
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
7934
|
+
const val = n[key];
|
|
7935
|
+
if (val && typeof val === "object") walk3(val, depth + 1);
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7938
|
+
walk3(fnNode.body);
|
|
7939
|
+
if (jsonCallPos === null) return;
|
|
7940
|
+
if (guardPos !== null && guardPos < jsonCallPos) return;
|
|
7941
|
+
context.report({ node: jsonCallNode, messageId: "missingStatusCheck" });
|
|
7942
|
+
}
|
|
7943
|
+
return {
|
|
7944
|
+
FunctionDeclaration(node) {
|
|
7945
|
+
analyzeFunction(node);
|
|
7946
|
+
},
|
|
7947
|
+
FunctionExpression(node) {
|
|
7948
|
+
analyzeFunction(node);
|
|
7949
|
+
},
|
|
7950
|
+
ArrowFunctionExpression(node) {
|
|
7951
|
+
analyzeFunction(node);
|
|
7952
|
+
}
|
|
7953
|
+
};
|
|
7954
|
+
}
|
|
7955
|
+
};
|
|
7956
|
+
var elevenlabsCheckHttpStatusBeforeJsonRule = rule90;
|
|
7957
|
+
|
|
7958
|
+
// src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
|
|
7959
|
+
var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
|
|
7960
|
+
var rule91 = {
|
|
7961
|
+
meta: {
|
|
7962
|
+
type: "suggestion",
|
|
7963
|
+
docs: {
|
|
7964
|
+
description: "Ending a conversation must be wrapped in try/catch so a rejected call is handled",
|
|
7965
|
+
category: "reliability",
|
|
7966
|
+
rationale: "If conversation.endSession()/endConversation() rejects (e.g. during a GL-mode transition) and the call site has no try/catch, the rejection becomes an unhandled promise rejection and the conversation state is never cleaned up, leaving stale connections in memory.",
|
|
7967
|
+
docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
|
|
7968
|
+
recommended: true
|
|
7969
|
+
},
|
|
7970
|
+
messages: {
|
|
7971
|
+
missingTryCatch: "This call to end the conversation has no surrounding try/catch \u2014 a rejection will go unhandled and the conversation state may never be cleaned up."
|
|
7972
|
+
}
|
|
7973
|
+
},
|
|
7974
|
+
create(context) {
|
|
7975
|
+
function posEnd(n) {
|
|
7976
|
+
if (typeof n?.range?.[1] === "number") return n.range[1];
|
|
7977
|
+
const line = n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0;
|
|
7978
|
+
const column = n?.loc?.end?.column ?? n?.loc?.start?.column ?? 0;
|
|
7979
|
+
return line * 1e6 + column;
|
|
7980
|
+
}
|
|
7981
|
+
function isConversationEndCall(node) {
|
|
7982
|
+
if (node?.type !== "CallExpression") return false;
|
|
7983
|
+
const callee = node.callee;
|
|
7984
|
+
if (callee?.type === "MemberExpression") {
|
|
7985
|
+
return callee.property?.type === "Identifier" && END_METHODS.has(callee.property.name);
|
|
7986
|
+
}
|
|
7987
|
+
return callee?.type === "Identifier" && END_METHODS.has(callee.name);
|
|
7988
|
+
}
|
|
7989
|
+
return {
|
|
7990
|
+
"Program:exit"(program2) {
|
|
7991
|
+
const tryRanges = [];
|
|
7992
|
+
const endCalls = [];
|
|
7993
|
+
function walk3(n, depth = 0) {
|
|
7994
|
+
if (!n || typeof n !== "object" || depth > 60) return;
|
|
7995
|
+
if (Array.isArray(n)) {
|
|
7996
|
+
for (const item of n) walk3(item, depth + 1);
|
|
7997
|
+
return;
|
|
7998
|
+
}
|
|
7999
|
+
if (n.type === "TryStatement" && n.block) {
|
|
8000
|
+
tryRanges.push([posOf(n.block), posEnd(n.block)]);
|
|
8001
|
+
}
|
|
8002
|
+
if (isConversationEndCall(n)) endCalls.push(n);
|
|
8003
|
+
for (const key of Object.keys(n)) {
|
|
8004
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8005
|
+
const val = n[key];
|
|
8006
|
+
if (val && typeof val === "object") walk3(val, depth + 1);
|
|
8007
|
+
}
|
|
8008
|
+
}
|
|
8009
|
+
walk3(program2);
|
|
8010
|
+
for (const call of endCalls) {
|
|
8011
|
+
const p = posOf(call);
|
|
8012
|
+
const covered = tryRanges.some(([start, end]) => p >= start && p <= end);
|
|
8013
|
+
if (!covered) {
|
|
8014
|
+
context.report({ node: call, messageId: "missingTryCatch" });
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
}
|
|
8018
|
+
};
|
|
8019
|
+
}
|
|
8020
|
+
};
|
|
8021
|
+
var elevenlabsConversationCleanupOnErrorRule = rule91;
|
|
8022
|
+
|
|
8023
|
+
// src/providers/elevenlabs/rules/env-var-validation.ts
|
|
8024
|
+
var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
|
|
8025
|
+
var rule92 = {
|
|
8026
|
+
meta: {
|
|
8027
|
+
type: "suggestion",
|
|
8028
|
+
docs: {
|
|
8029
|
+
description: "ElevenLabs API key environment variables should be validated at module load, not only at request time",
|
|
8030
|
+
category: "correctness",
|
|
8031
|
+
rationale: "Checking `if (!apiKey)` inside a request handler means a misconfigured deployment only fails the first time a user exercises the feature, instead of failing fast at startup where it would show up in deploy logs or a health check.",
|
|
8032
|
+
docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
|
|
8033
|
+
recommended: true
|
|
8034
|
+
},
|
|
8035
|
+
messages: {
|
|
8036
|
+
missingStartupValidation: "This module reads an ElevenLabs API key from process.env but only validates it inside a request handler \u2014 validate it at module scope so a missing key fails at startup, not at first use."
|
|
8037
|
+
}
|
|
8038
|
+
},
|
|
8039
|
+
create(context) {
|
|
8040
|
+
function isElevenLabsEnvAccess(node) {
|
|
8041
|
+
if (node?.type !== "MemberExpression") return false;
|
|
8042
|
+
if (node.object?.type !== "MemberExpression") return false;
|
|
8043
|
+
const inner = node.object;
|
|
8044
|
+
if (inner.object?.type !== "Identifier" || inner.object.name !== "process") return false;
|
|
8045
|
+
if (inner.property?.type !== "Identifier" || inner.property.name !== "env") return false;
|
|
8046
|
+
const propName2 = node.property?.type === "Identifier" ? node.property.name : node.property?.value;
|
|
8047
|
+
return typeof propName2 === "string" && ELEVENLABS_ENV_VAR_PATTERN.test(propName2);
|
|
8048
|
+
}
|
|
8049
|
+
function isInsideFunction(node, ancestors) {
|
|
8050
|
+
return ancestors.some(
|
|
8051
|
+
(a) => a?.type === "FunctionDeclaration" || a?.type === "FunctionExpression" || a?.type === "ArrowFunctionExpression"
|
|
8052
|
+
);
|
|
8053
|
+
}
|
|
8054
|
+
return {
|
|
8055
|
+
"Program:exit"(program2) {
|
|
8056
|
+
let moduleScopeAccess = false;
|
|
8057
|
+
let handlerScopeAccess = false;
|
|
8058
|
+
function walk3(n, ancestors, depth = 0) {
|
|
8059
|
+
if (!n || typeof n !== "object" || depth > 60) return;
|
|
8060
|
+
if (Array.isArray(n)) {
|
|
8061
|
+
for (const item of n) walk3(item, ancestors, depth + 1);
|
|
8062
|
+
return;
|
|
8063
|
+
}
|
|
8064
|
+
if (isElevenLabsEnvAccess(n)) {
|
|
8065
|
+
if (isInsideFunction(n, ancestors)) {
|
|
8066
|
+
handlerScopeAccess = true;
|
|
8067
|
+
} else {
|
|
8068
|
+
moduleScopeAccess = true;
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
8071
|
+
const nextAncestors = n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" ? [...ancestors, n] : ancestors;
|
|
8072
|
+
for (const key of Object.keys(n)) {
|
|
8073
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8074
|
+
const val = n[key];
|
|
8075
|
+
if (val && typeof val === "object") walk3(val, nextAncestors, depth + 1);
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
walk3(program2, []);
|
|
8079
|
+
if (handlerScopeAccess && !moduleScopeAccess) {
|
|
8080
|
+
context.report({ node: program2, messageId: "missingStartupValidation" });
|
|
8081
|
+
}
|
|
8082
|
+
}
|
|
8083
|
+
};
|
|
8084
|
+
}
|
|
8085
|
+
};
|
|
8086
|
+
var elevenlabsEnvVarValidationRule = rule92;
|
|
8087
|
+
|
|
8088
|
+
// src/providers/twilio/utils.ts
|
|
8089
|
+
var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
|
|
8090
|
+
var ROUTER_OBJECT_NAMES = /* @__PURE__ */ new Set(["server", "app", "router", "fastify"]);
|
|
8091
|
+
function isPostRouteRegistration(node) {
|
|
8092
|
+
if (node?.type !== "CallExpression") return false;
|
|
8093
|
+
const callee = node.callee;
|
|
8094
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8095
|
+
if (callee.property?.type !== "Identifier" || !POST_METHOD_NAMES.has(callee.property.name)) return false;
|
|
8096
|
+
return callee.object?.type === "Identifier" && ROUTER_OBJECT_NAMES.has(callee.object.name);
|
|
8097
|
+
}
|
|
8098
|
+
function findInSubtree(node, predicate, depth = 0) {
|
|
8099
|
+
if (!node || typeof node !== "object" || depth > 40) return null;
|
|
8100
|
+
if (Array.isArray(node)) {
|
|
8101
|
+
for (const n of node) {
|
|
8102
|
+
const found = findInSubtree(n, predicate, depth + 1);
|
|
8103
|
+
if (found) return found;
|
|
8104
|
+
}
|
|
8105
|
+
return null;
|
|
8106
|
+
}
|
|
8107
|
+
if (predicate(node)) return node;
|
|
8108
|
+
for (const key of Object.keys(node)) {
|
|
8109
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8110
|
+
const val = node[key];
|
|
8111
|
+
if (val && typeof val === "object") {
|
|
8112
|
+
const found = findInSubtree(val, predicate, depth + 1);
|
|
8113
|
+
if (found) return found;
|
|
8114
|
+
}
|
|
8115
|
+
}
|
|
8116
|
+
return null;
|
|
8117
|
+
}
|
|
8118
|
+
function referencesRequestBody(node) {
|
|
8119
|
+
return !!findInSubtree(node, (n) => {
|
|
8120
|
+
if (n?.type !== "MemberExpression") return false;
|
|
8121
|
+
if (n.property?.type !== "Identifier" || n.property.name !== "body") return false;
|
|
8122
|
+
return n.object?.type === "Identifier" && (n.object.name === "req" || n.object.name === "request");
|
|
8123
|
+
});
|
|
8124
|
+
}
|
|
8125
|
+
function collectVarDeclarators2(node, out, depth = 0) {
|
|
8126
|
+
if (!node || typeof node !== "object" || depth > 24) return;
|
|
8127
|
+
if (Array.isArray(node)) {
|
|
8128
|
+
for (const n of node) collectVarDeclarators2(n, out, depth + 1);
|
|
8129
|
+
return;
|
|
8130
|
+
}
|
|
8131
|
+
if (node.type === "VariableDeclarator") out.push(node);
|
|
8132
|
+
for (const key of Object.keys(node)) {
|
|
8133
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8134
|
+
const val = node[key];
|
|
8135
|
+
if (val && typeof val === "object") collectVarDeclarators2(val, out, depth + 1);
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
8138
|
+
|
|
8139
|
+
// src/providers/twilio/rules/validate-webhook-signature.ts
|
|
8140
|
+
var rule93 = {
|
|
8141
|
+
meta: {
|
|
8142
|
+
type: "problem",
|
|
8143
|
+
docs: {
|
|
8144
|
+
description: "Twilio webhook routes must validate the X-Twilio-Signature header before trusting the body",
|
|
8145
|
+
category: "security",
|
|
8146
|
+
cwe: "CWE-345",
|
|
8147
|
+
owasp: "A07:2021 Identification and Authentication Failures",
|
|
8148
|
+
rationale: "Webhook URLs are public. Anyone who learns or guesses the path can POST a forged body \u2014 fake CallSid/From/TaskAttributes values \u2014 and the handler has no way to tell it apart from a real Twilio request. Without validating the signature first, an attacker can trigger outbound calls (toll/billing impact) or falsely invoke reservation/task callbacks to manipulate call state for an arbitrary caller.",
|
|
8149
|
+
docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-security",
|
|
8150
|
+
recommended: true
|
|
8151
|
+
},
|
|
8152
|
+
messages: {
|
|
8153
|
+
missingSignatureValidation: "This POST webhook route reads req.body but the file never validates the X-Twilio-Signature header via twilio.validateRequest()/RequestValidator \u2014 a forged request body is indistinguishable from a real Twilio callback."
|
|
8154
|
+
}
|
|
8155
|
+
},
|
|
8156
|
+
create(context) {
|
|
8157
|
+
function isValidateRequestCall(n) {
|
|
8158
|
+
if (n?.type !== "CallExpression") return false;
|
|
8159
|
+
const callee = n.callee;
|
|
8160
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8161
|
+
return callee.property?.type === "Identifier" && callee.property.name === "validateRequest";
|
|
8162
|
+
}
|
|
8163
|
+
function isRequestValidatorConstruction(n) {
|
|
8164
|
+
return n?.type === "NewExpression" && n.callee?.type === "Identifier" && n.callee.name === "RequestValidator";
|
|
8165
|
+
}
|
|
8166
|
+
return {
|
|
8167
|
+
"Program:exit"(program2) {
|
|
8168
|
+
const hasValidation = !!findInSubtree(program2, isValidateRequestCall) || !!findInSubtree(program2, isRequestValidatorConstruction);
|
|
8169
|
+
if (hasValidation) return;
|
|
8170
|
+
const postRoutes = [];
|
|
8171
|
+
findInSubtree(program2, (n) => {
|
|
8172
|
+
if (isPostRouteRegistration(n) && referencesRequestBody(n)) postRoutes.push(n);
|
|
8173
|
+
return false;
|
|
8174
|
+
});
|
|
8175
|
+
for (const route of postRoutes) {
|
|
8176
|
+
context.report({ node: route, messageId: "missingSignatureValidation" });
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
};
|
|
8180
|
+
}
|
|
8181
|
+
};
|
|
8182
|
+
var twilioValidateWebhookSignatureRule = rule93;
|
|
8183
|
+
|
|
8184
|
+
// src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
|
|
8185
|
+
var rule94 = {
|
|
8186
|
+
meta: {
|
|
8187
|
+
type: "problem",
|
|
8188
|
+
docs: {
|
|
8189
|
+
description: "TaskRouter Task attributes must include every field the reservation handler reads back out",
|
|
8190
|
+
category: "correctness",
|
|
8191
|
+
rationale: "A Task is created with attributes like `{ name, type }`, and later a reservation-accepted handler does `const { from } = JSON.parse(req.body.TaskAttributes)` to look up state by `from`. If the producer never set `from` on the Task attributes, that destructure is always `undefined`, the lookup always misses, and the handler returns 404 \u2014 even though every other part of the flow worked correctly.",
|
|
8192
|
+
docsUrl: "https://www.twilio.com/docs/taskrouter/twiml-queue-calls",
|
|
8193
|
+
recommended: true
|
|
8194
|
+
},
|
|
8195
|
+
messages: {
|
|
8196
|
+
attributeMismatch: 'This .task() attributes object does not set "{{field}}", but a reservation handler in this codebase destructures "{{field}}" from JSON.parse(TaskAttributes) \u2014 that lookup will always be undefined.'
|
|
8197
|
+
}
|
|
8198
|
+
},
|
|
8199
|
+
create(context) {
|
|
8200
|
+
function isTaskCall(n) {
|
|
8201
|
+
if (n?.type !== "CallExpression") return false;
|
|
8202
|
+
const callee = n.callee;
|
|
8203
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8204
|
+
return callee.property?.type === "Identifier" && callee.property.name === "task";
|
|
8205
|
+
}
|
|
8206
|
+
function extractAttributeKeys(taskCallNode) {
|
|
8207
|
+
const arg = taskCallNode.arguments?.[0];
|
|
8208
|
+
if (!arg) return null;
|
|
8209
|
+
if (arg.type === "CallExpression" && arg.callee?.type === "MemberExpression" && arg.callee.object?.type === "Identifier" && arg.callee.object.name === "JSON" && arg.callee.property?.type === "Identifier" && arg.callee.property.name === "stringify" && arg.arguments?.[0]?.type === "ObjectExpression") {
|
|
8210
|
+
return objectKeys(arg.arguments[0]);
|
|
8211
|
+
}
|
|
8212
|
+
if (arg.type === "TemplateLiteral") {
|
|
8213
|
+
const raw = arg.quasis.map((q) => q.value?.raw ?? "").join("");
|
|
8214
|
+
const keys = /* @__PURE__ */ new Set();
|
|
8215
|
+
for (const m of raw.matchAll(/"([a-zA-Z0-9_]+)"\s*:/g)) keys.add(m[1]);
|
|
8216
|
+
return keys;
|
|
8217
|
+
}
|
|
8218
|
+
if (arg.type === "Literal" && typeof arg.value === "string") {
|
|
8219
|
+
const keys = /* @__PURE__ */ new Set();
|
|
8220
|
+
for (const m of arg.value.matchAll(/"([a-zA-Z0-9_]+)"\s*:/g)) keys.add(m[1]);
|
|
8221
|
+
return keys;
|
|
8222
|
+
}
|
|
8223
|
+
return null;
|
|
8224
|
+
}
|
|
8225
|
+
function objectKeys(objExpr) {
|
|
8226
|
+
const keys = /* @__PURE__ */ new Set();
|
|
8227
|
+
for (const p of objExpr.properties ?? []) {
|
|
8228
|
+
if (p.type !== "Property") continue;
|
|
8229
|
+
const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
|
|
8230
|
+
if (typeof name === "string") keys.add(name);
|
|
8231
|
+
}
|
|
8232
|
+
return keys;
|
|
8233
|
+
}
|
|
8234
|
+
function isTaskAttributesJsonParse(n) {
|
|
8235
|
+
if (n?.type !== "CallExpression") return false;
|
|
8236
|
+
const callee = n.callee;
|
|
8237
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8238
|
+
if (callee.object?.type !== "Identifier" || callee.object.name !== "JSON") return false;
|
|
8239
|
+
if (callee.property?.type !== "Identifier" || callee.property.name !== "parse") return false;
|
|
8240
|
+
const arg = n.arguments?.[0];
|
|
8241
|
+
if (arg?.type !== "MemberExpression") return false;
|
|
8242
|
+
return arg.property?.type === "Identifier" && arg.property.name === "TaskAttributes";
|
|
8243
|
+
}
|
|
8244
|
+
function findConsumedFields(program2) {
|
|
8245
|
+
const fields = /* @__PURE__ */ new Set();
|
|
8246
|
+
const declarators = [];
|
|
8247
|
+
collectVarDeclarators2(program2, declarators);
|
|
8248
|
+
for (const d of declarators) {
|
|
8249
|
+
if (!isTaskAttributesJsonParse(d.init)) continue;
|
|
8250
|
+
if (d.id?.type === "ObjectPattern") {
|
|
8251
|
+
for (const p of d.id.properties ?? []) {
|
|
8252
|
+
if (p.type !== "Property") continue;
|
|
8253
|
+
const name = p.key?.type === "Identifier" ? p.key.name : null;
|
|
8254
|
+
if (name) fields.add(name);
|
|
8255
|
+
}
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
return fields;
|
|
8259
|
+
}
|
|
8260
|
+
return {
|
|
8261
|
+
"Program:exit"(program2) {
|
|
8262
|
+
const consumedFields = findConsumedFields(program2);
|
|
8263
|
+
if (consumedFields.size === 0) return;
|
|
8264
|
+
const taskCalls = [];
|
|
8265
|
+
findInSubtree(program2, (n) => {
|
|
8266
|
+
if (isTaskCall(n)) taskCalls.push(n);
|
|
8267
|
+
return false;
|
|
8268
|
+
});
|
|
8269
|
+
for (const taskCall of taskCalls) {
|
|
8270
|
+
const producedKeys = extractAttributeKeys(taskCall);
|
|
8271
|
+
if (!producedKeys) continue;
|
|
8272
|
+
for (const field of consumedFields) {
|
|
8273
|
+
if (!producedKeys.has(field)) {
|
|
8274
|
+
context.report({ node: taskCall, messageId: "attributeMismatch", data: { field } });
|
|
8275
|
+
}
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
};
|
|
8280
|
+
}
|
|
8281
|
+
};
|
|
8282
|
+
var twilioTaskrouterAttributesMatchConsumerRule = rule94;
|
|
8283
|
+
|
|
8284
|
+
// src/providers/twilio/rules/enqueue-task-json-stringify.ts
|
|
8285
|
+
var rule95 = {
|
|
8286
|
+
meta: {
|
|
8287
|
+
type: "problem",
|
|
8288
|
+
docs: {
|
|
8289
|
+
description: "TaskRouter .task() attributes must be built with JSON.stringify(), not raw string interpolation",
|
|
8290
|
+
category: "security",
|
|
8291
|
+
cwe: "CWE-116",
|
|
8292
|
+
owasp: "CWE-91 XML/JSON Injection",
|
|
8293
|
+
rationale: 'Hand-building the Task attributes JSON by interpolating an unvalidated request field directly into a string literal breaks the moment that field contains a `"` or `\\`. The resulting body is invalid JSON, and Twilio rejects the Enqueue with error 14221 ("Provided Attributes JSON was not valid") \u2014 turning a single unexpected character in a caller-controlled field (like a name with a quote in it) into a hard failure of the call flow.',
|
|
8294
|
+
docsUrl: "https://www.twilio.com/docs/api/errors/14221",
|
|
8295
|
+
recommended: true
|
|
8296
|
+
},
|
|
8297
|
+
messages: {
|
|
8298
|
+
rawJsonInterpolation: 'This .task() call builds JSON attributes via raw string interpolation instead of JSON.stringify() \u2014 a "/\\\\ character in the interpolated value produces invalid JSON and Twilio error 14221.'
|
|
8299
|
+
}
|
|
8300
|
+
},
|
|
8301
|
+
create(context) {
|
|
8302
|
+
function isTaskCall(n) {
|
|
8303
|
+
if (n?.type !== "CallExpression") return false;
|
|
8304
|
+
const callee = n.callee;
|
|
8305
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8306
|
+
return callee.property?.type === "Identifier" && callee.property.name === "task";
|
|
8307
|
+
}
|
|
8308
|
+
function hasInterpolatedExpression(templateLiteral) {
|
|
8309
|
+
return (templateLiteral.expressions ?? []).length > 0;
|
|
8310
|
+
}
|
|
8311
|
+
return {
|
|
8312
|
+
CallExpression(node) {
|
|
8313
|
+
if (!isTaskCall(node)) return;
|
|
8314
|
+
const arg = node.arguments?.[0];
|
|
8315
|
+
if (!arg) return;
|
|
8316
|
+
if (arg.type === "TemplateLiteral" && hasInterpolatedExpression(arg)) {
|
|
8317
|
+
context.report({ node, messageId: "rawJsonInterpolation" });
|
|
8318
|
+
return;
|
|
8319
|
+
}
|
|
8320
|
+
if (arg.type === "BinaryExpression" && arg.operator === "+") {
|
|
8321
|
+
context.report({ node, messageId: "rawJsonInterpolation" });
|
|
8322
|
+
}
|
|
8323
|
+
}
|
|
8324
|
+
};
|
|
8325
|
+
}
|
|
8326
|
+
};
|
|
8327
|
+
var twilioEnqueueTaskJsonStringifyRule = rule95;
|
|
8328
|
+
|
|
8329
|
+
// src/providers/twilio/rules/media-streams-key-by-call-sid.ts
|
|
8330
|
+
var rule96 = {
|
|
8331
|
+
meta: {
|
|
8332
|
+
type: "problem",
|
|
8333
|
+
docs: {
|
|
8334
|
+
description: "Media Streams session maps must be keyed by callSid, not phone number",
|
|
8335
|
+
category: "reliability",
|
|
8336
|
+
rationale: "A Map keyed on the caller's phone number breaks the moment the same number places two concurrent calls \u2014 a retry after a dropped call, two family members sharing a line, a misdial-and-redial. The second map.set() silently overwrites the first call's entry, and that first call's downstream events (reservation-accepted, outbound-leg wiring) end up pointed at the wrong session, crossing audio streams between two unrelated calls. Twilio provides a unique callSid on every start message specifically to avoid this collision.",
|
|
8337
|
+
docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
|
|
8338
|
+
recommended: true
|
|
8339
|
+
},
|
|
8340
|
+
messages: {
|
|
8341
|
+
keyedByPhoneNumber: `This Map is keyed by "{{key}}" (a phone-number-like field) instead of callSid \u2014 two concurrent calls from the same number will silently overwrite each other's entry.`
|
|
8342
|
+
}
|
|
8343
|
+
},
|
|
8344
|
+
create(context) {
|
|
8345
|
+
function isFromLikeIdentifier(n) {
|
|
8346
|
+
if (n?.type === "Identifier" && n.name === "from") return "from";
|
|
8347
|
+
if (n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "from" && !n.computed) {
|
|
8348
|
+
return "from";
|
|
8349
|
+
}
|
|
8350
|
+
return null;
|
|
8351
|
+
}
|
|
8352
|
+
function isMapMutationCall(n, methodNames) {
|
|
8353
|
+
if (n?.type !== "CallExpression") return false;
|
|
8354
|
+
const callee = n.callee;
|
|
8355
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8356
|
+
return callee.property?.type === "Identifier" && methodNames.has(callee.property.name);
|
|
8357
|
+
}
|
|
8358
|
+
const SET_OR_GET = /* @__PURE__ */ new Set(["set", "get"]);
|
|
8359
|
+
return {
|
|
8360
|
+
"Program:exit"(program2) {
|
|
8361
|
+
let callSidAvailable = false;
|
|
8362
|
+
function scanForCallSid(n, depth = 0) {
|
|
8363
|
+
if (callSidAvailable || !n || typeof n !== "object" || depth > 60) return;
|
|
8364
|
+
if (Array.isArray(n)) {
|
|
8365
|
+
for (const item of n) scanForCallSid(item, depth + 1);
|
|
8366
|
+
return;
|
|
8367
|
+
}
|
|
8368
|
+
if (n.type === "Identifier" && n.name === "callSid") {
|
|
8369
|
+
callSidAvailable = true;
|
|
8370
|
+
return;
|
|
8371
|
+
}
|
|
8372
|
+
if (n.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "callSid") {
|
|
8373
|
+
callSidAvailable = true;
|
|
8374
|
+
return;
|
|
8375
|
+
}
|
|
8376
|
+
for (const key of Object.keys(n)) {
|
|
8377
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8378
|
+
const val = n[key];
|
|
8379
|
+
if (val && typeof val === "object") scanForCallSid(val, depth + 1);
|
|
8380
|
+
}
|
|
8381
|
+
}
|
|
8382
|
+
scanForCallSid(program2);
|
|
8383
|
+
if (!callSidAvailable) return;
|
|
8384
|
+
function walk3(n, depth = 0) {
|
|
8385
|
+
if (!n || typeof n !== "object" || depth > 60) return;
|
|
8386
|
+
if (Array.isArray(n)) {
|
|
8387
|
+
for (const item of n) walk3(item, depth + 1);
|
|
8388
|
+
return;
|
|
8389
|
+
}
|
|
8390
|
+
if (isMapMutationCall(n, SET_OR_GET)) {
|
|
8391
|
+
const firstArg = n.arguments?.[0];
|
|
8392
|
+
const key = isFromLikeIdentifier(firstArg);
|
|
8393
|
+
if (key) context.report({ node: n, messageId: "keyedByPhoneNumber", data: { key } });
|
|
8394
|
+
}
|
|
8395
|
+
for (const k of Object.keys(n)) {
|
|
8396
|
+
if (k === "parent" || k === "loc" || k === "range") continue;
|
|
8397
|
+
const val = n[k];
|
|
8398
|
+
if (val && typeof val === "object") walk3(val, depth + 1);
|
|
8399
|
+
}
|
|
8400
|
+
}
|
|
8401
|
+
walk3(program2);
|
|
8402
|
+
}
|
|
8403
|
+
};
|
|
8404
|
+
}
|
|
8405
|
+
};
|
|
8406
|
+
var twilioMediaStreamsKeyByCallSidRule = rule96;
|
|
8407
|
+
|
|
8408
|
+
// src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
|
|
8409
|
+
var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
|
|
8410
|
+
var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
|
|
8411
|
+
var rule97 = {
|
|
8412
|
+
meta: {
|
|
8413
|
+
type: "problem",
|
|
8414
|
+
docs: {
|
|
8415
|
+
description: "Twilio REST calls inside event-handler callbacks must be wrapped in try/catch",
|
|
8416
|
+
category: "reliability",
|
|
8417
|
+
rationale: 'Event-emitter style callbacks (onStart, onMessage, on("message", ...)) are typically invoked via something like `callbacks.map((cb) => cb(event))`, which discards the returned promise. If the callback is `async` and calls `await twilio.calls.create(...)` with no try/catch, any Twilio REST error (invalid number, suspended account, rate limit) becomes an unhandled promise rejection. If the process has a global `unhandledRejection` handler that calls `process.exit()` (a common pattern), one failed outbound call takes down the entire server and every other in-progress call, not just the failing one.',
|
|
8418
|
+
docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-faq",
|
|
8419
|
+
recommended: true
|
|
8420
|
+
},
|
|
8421
|
+
messages: {
|
|
8422
|
+
missingTryCatch: "This await twilio.{{resource}}.{{method}}() call is inside an event-handler callback with no surrounding try/catch \u2014 a REST API error here becomes an unhandled promise rejection that can crash the whole process."
|
|
8423
|
+
}
|
|
8424
|
+
},
|
|
8425
|
+
create(context) {
|
|
8426
|
+
function isAwaitedTwilioRestCall(n) {
|
|
8427
|
+
if (n?.type !== "AwaitExpression") return null;
|
|
8428
|
+
const call = n.argument;
|
|
8429
|
+
if (call?.type !== "CallExpression") return null;
|
|
8430
|
+
const callee = call.callee;
|
|
8431
|
+
if (callee?.type !== "MemberExpression") return null;
|
|
8432
|
+
const method = callee.property?.type === "Identifier" ? callee.property.name : null;
|
|
8433
|
+
if (!method || !REST_RESOURCE_METHODS.has(method)) return null;
|
|
8434
|
+
const obj = callee.object;
|
|
8435
|
+
if (obj?.type !== "MemberExpression") return null;
|
|
8436
|
+
const resource = obj.property?.type === "Identifier" ? obj.property.name : null;
|
|
8437
|
+
if (!resource) return null;
|
|
8438
|
+
const base = obj.object;
|
|
8439
|
+
const baseName = base?.type === "Identifier" ? base.name : null;
|
|
8440
|
+
if (!baseName || !/^(twilio\w*|client)$/i.test(baseName)) return null;
|
|
8441
|
+
return { resource, method };
|
|
8442
|
+
}
|
|
8443
|
+
function isEventHandlerRegistration(n) {
|
|
8444
|
+
if (n?.type !== "CallExpression") return false;
|
|
8445
|
+
const callee = n.callee;
|
|
8446
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8447
|
+
return callee.property?.type === "Identifier" && EVENT_REGISTRATION_METHODS.has(callee.property.name);
|
|
8448
|
+
}
|
|
8449
|
+
function isAsyncCallback(n) {
|
|
8450
|
+
return (n?.type === "ArrowFunctionExpression" || n?.type === "FunctionExpression") && n.async === true;
|
|
8451
|
+
}
|
|
8452
|
+
function posOf2(n) {
|
|
8453
|
+
if (typeof n?.range?.[0] === "number") return n.range[0];
|
|
8454
|
+
const line = n?.loc?.start?.line ?? 0;
|
|
8455
|
+
const column = n?.loc?.start?.column ?? 0;
|
|
8456
|
+
return line * 1e6 + column;
|
|
8457
|
+
}
|
|
8458
|
+
function posEnd(n) {
|
|
8459
|
+
if (typeof n?.range?.[1] === "number") return n.range[1];
|
|
8460
|
+
const line = n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0;
|
|
8461
|
+
const column = n?.loc?.end?.column ?? n?.loc?.start?.column ?? 0;
|
|
8462
|
+
return line * 1e6 + column;
|
|
8463
|
+
}
|
|
8464
|
+
function isWithinTryBlock(node, root) {
|
|
8465
|
+
const tryRanges = [];
|
|
8466
|
+
function collectTryRanges(n, depth = 0) {
|
|
8467
|
+
if (!n || typeof n !== "object" || depth > 60) return;
|
|
8468
|
+
if (Array.isArray(n)) {
|
|
8469
|
+
for (const item of n) collectTryRanges(item, depth + 1);
|
|
8470
|
+
return;
|
|
8471
|
+
}
|
|
8472
|
+
if (n.type === "TryStatement" && n.block) {
|
|
8473
|
+
tryRanges.push([posOf2(n.block), posEnd(n.block)]);
|
|
8474
|
+
}
|
|
8475
|
+
for (const key of Object.keys(n)) {
|
|
8476
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8477
|
+
const val = n[key];
|
|
8478
|
+
if (val && typeof val === "object") collectTryRanges(val, depth + 1);
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
collectTryRanges(root);
|
|
8482
|
+
const p = posOf2(node);
|
|
8483
|
+
return tryRanges.some(([start, end]) => p >= start && p <= end);
|
|
8484
|
+
}
|
|
8485
|
+
return {
|
|
8486
|
+
CallExpression(node) {
|
|
8487
|
+
if (!isEventHandlerRegistration(node)) return;
|
|
8488
|
+
const callback = node.arguments?.find((a) => isAsyncCallback(a));
|
|
8489
|
+
if (!callback) return;
|
|
8490
|
+
const restCalls = [];
|
|
8491
|
+
function collect(n, depth = 0) {
|
|
8492
|
+
if (!n || typeof n !== "object" || depth > 40) return;
|
|
8493
|
+
if (Array.isArray(n)) {
|
|
8494
|
+
for (const item of n) collect(item, depth + 1);
|
|
8495
|
+
return;
|
|
8496
|
+
}
|
|
8497
|
+
const info = isAwaitedTwilioRestCall(n);
|
|
8498
|
+
if (info) restCalls.push({ node: n, ...info });
|
|
8499
|
+
for (const key of Object.keys(n)) {
|
|
8500
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8501
|
+
const val = n[key];
|
|
8502
|
+
if (val && typeof val === "object") collect(val, depth + 1);
|
|
8503
|
+
}
|
|
8504
|
+
}
|
|
8505
|
+
collect(callback.body);
|
|
8506
|
+
for (const call of restCalls) {
|
|
8507
|
+
if (!isWithinTryBlock(call.node, callback.body)) {
|
|
8508
|
+
context.report({
|
|
8509
|
+
node: call.node,
|
|
8510
|
+
messageId: "missingTryCatch",
|
|
8511
|
+
data: { resource: call.resource, method: call.method }
|
|
8512
|
+
});
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
};
|
|
8517
|
+
}
|
|
8518
|
+
};
|
|
8519
|
+
var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule97;
|
|
8520
|
+
|
|
8521
|
+
// src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
|
|
8522
|
+
var rule98 = {
|
|
8523
|
+
meta: {
|
|
8524
|
+
type: "suggestion",
|
|
8525
|
+
docs: {
|
|
8526
|
+
description: "TwiML responses must be built with the VoiceResponse builder, not raw XML template strings",
|
|
8527
|
+
category: "security",
|
|
8528
|
+
cwe: "CWE-91",
|
|
8529
|
+
rationale: "The VoiceResponse builder XML-escapes every interpolated value automatically. A raw template literal that interpolates values directly into XML attribute or text positions has no such protection \u2014 if either value's source ever changes to something attacker-influenced (e.g. a SIP header instead of a Twilio-controlled CallSid), unescaped `<`/`\"`/`&` characters can break out of the intended XML structure.",
|
|
8530
|
+
docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
|
|
8531
|
+
recommended: true
|
|
8532
|
+
},
|
|
8533
|
+
messages: {
|
|
8534
|
+
rawTwimlTemplate: "TwiML is built here as a raw template literal with interpolated values instead of the VoiceResponse builder \u2014 interpolated values are not XML-escaped, unlike new VoiceResponse()."
|
|
8535
|
+
}
|
|
8536
|
+
},
|
|
8537
|
+
create(context) {
|
|
8538
|
+
function looksLikeTwimlXml(raw) {
|
|
8539
|
+
return /<Response>|<Connect>|<Say>|<Stream\b|<Enqueue\b|<Dial\b/.test(raw);
|
|
8540
|
+
}
|
|
8541
|
+
return {
|
|
8542
|
+
TemplateLiteral(node) {
|
|
8543
|
+
if ((node.expressions ?? []).length === 0) return;
|
|
8544
|
+
const raw = (node.quasis ?? []).map((q) => q.value?.raw ?? "").join("");
|
|
8545
|
+
if (!looksLikeTwimlXml(raw)) return;
|
|
8546
|
+
context.report({ node, messageId: "rawTwimlTemplate" });
|
|
8547
|
+
}
|
|
8548
|
+
};
|
|
8549
|
+
}
|
|
8550
|
+
};
|
|
8551
|
+
var twilioUseTwimlBuilderNotStringTemplatesRule = rule98;
|
|
8552
|
+
|
|
8553
|
+
// src/providers/twilio/rules/media-streams-mark-pacing.ts
|
|
8554
|
+
var rule99 = {
|
|
8555
|
+
meta: {
|
|
8556
|
+
type: "suggestion",
|
|
8557
|
+
docs: {
|
|
8558
|
+
description: "Media Streams audio forwarding should use mark-based pacing to avoid buffer overflow",
|
|
8559
|
+
category: "reliability",
|
|
8560
|
+
rationale: 'Twilio buffers at most 10 minutes of audio per bidirectional Stream. If audio chunks are forwarded with no mark events and no pacing, and the sender produces media faster than real-time (or playback stalls), Twilio stops accepting further audio for that Stream and emits warning 31931 ("Media Discarded") \u2014 silently dropping audio rather than erroring loudly.',
|
|
8561
|
+
docsUrl: "https://www.twilio.com/docs/api/errors/31931",
|
|
8562
|
+
recommended: true
|
|
8563
|
+
},
|
|
8564
|
+
messages: {
|
|
8565
|
+
noMarkPacing: "This file forwards media payloads via send() but never passes isLast/a mark argument anywhere \u2014 sustained high-throughput forwarding risks Twilio silently discarding buffered audio (warning 31931)."
|
|
8566
|
+
}
|
|
8567
|
+
},
|
|
8568
|
+
create(context) {
|
|
8569
|
+
function isMediaSendCall(n) {
|
|
8570
|
+
if (n?.type !== "CallExpression") return false;
|
|
8571
|
+
const callee = n.callee;
|
|
8572
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8573
|
+
if (callee.property?.type !== "Identifier" || callee.property.name !== "send") return false;
|
|
8574
|
+
const arg = n.arguments?.[0];
|
|
8575
|
+
const argText = arg?.type === "ArrayExpression" && (arg.elements ?? []).some(
|
|
8576
|
+
(el) => el?.type === "MemberExpression" && el.property?.type === "Identifier" && el.property.name === "delta"
|
|
8577
|
+
) || arg?.type === "Identifier" && /delta|payload|media/i.test(arg.name);
|
|
8578
|
+
return !!argText;
|
|
8579
|
+
}
|
|
8580
|
+
function hasIsLastOrMarkSignal(program2) {
|
|
8581
|
+
let found = false;
|
|
8582
|
+
function walk3(n, depth = 0) {
|
|
8583
|
+
if (found || !n || typeof n !== "object" || depth > 60) return;
|
|
8584
|
+
if (Array.isArray(n)) {
|
|
8585
|
+
for (const item of n) walk3(item, depth + 1);
|
|
8586
|
+
return;
|
|
8587
|
+
}
|
|
8588
|
+
if (typeof n.type === "string" && n.type.startsWith("TS")) return;
|
|
8589
|
+
if (n.type === "CallExpression") {
|
|
8590
|
+
const callee = n.callee;
|
|
8591
|
+
if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && callee.property.name === "send") {
|
|
8592
|
+
const second = n.arguments?.[1];
|
|
8593
|
+
if (second?.type === "Literal" && second.value === true) {
|
|
8594
|
+
found = true;
|
|
8595
|
+
return;
|
|
8596
|
+
}
|
|
8597
|
+
if (second?.type === "Identifier" && /isLast/i.test(second.name)) {
|
|
8598
|
+
found = true;
|
|
8599
|
+
return;
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8602
|
+
}
|
|
8603
|
+
if (n.type === "Identifier" && n.name === "isLast") {
|
|
8604
|
+
found = true;
|
|
8605
|
+
return;
|
|
8606
|
+
}
|
|
8607
|
+
for (const key of Object.keys(n)) {
|
|
8608
|
+
if (key === "parent" || key === "loc" || key === "range" || key === "typeAnnotation" || key === "returnType") continue;
|
|
8609
|
+
const val = n[key];
|
|
8610
|
+
if (val && typeof val === "object") walk3(val, depth + 1);
|
|
8611
|
+
}
|
|
8612
|
+
}
|
|
8613
|
+
walk3(program2);
|
|
8614
|
+
return found;
|
|
8615
|
+
}
|
|
8616
|
+
return {
|
|
8617
|
+
"Program:exit"(program2) {
|
|
8618
|
+
const sendCalls = [];
|
|
8619
|
+
findInSubtree(program2, (n) => {
|
|
8620
|
+
if (isMediaSendCall(n)) sendCalls.push(n);
|
|
8621
|
+
return false;
|
|
8622
|
+
});
|
|
8623
|
+
if (sendCalls.length === 0) return;
|
|
8624
|
+
if (hasIsLastOrMarkSignal(program2)) return;
|
|
8625
|
+
context.report({ node: sendCalls[0], messageId: "noMarkPacing" });
|
|
8626
|
+
}
|
|
8627
|
+
};
|
|
8628
|
+
}
|
|
8629
|
+
};
|
|
8630
|
+
var twilioMediaStreamsMarkPacingRule = rule99;
|
|
8631
|
+
|
|
8632
|
+
// src/providers/twilio/rules/validate-all-request-inputs.ts
|
|
8633
|
+
var rule100 = {
|
|
8634
|
+
meta: {
|
|
8635
|
+
type: "suggestion",
|
|
8636
|
+
docs: {
|
|
8637
|
+
description: "Fastify Twilio webhook routes must declare a schema for every request input they read",
|
|
8638
|
+
category: "correctness",
|
|
8639
|
+
rationale: "A route that validates `body` but not `querystring` (or declares no schema at all) lets unvalidated, possibly-undefined fields flow downstream. If a later call does `value.toString()` or similar on a field that never arrived, that throws instead of failing the request cleanly with a 400 \u2014 the failure surfaces far from the actual missing-validation root cause.",
|
|
8640
|
+
docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
|
|
8641
|
+
recommended: true
|
|
8642
|
+
},
|
|
8643
|
+
messages: {
|
|
8644
|
+
missingQuerystringSchema: "This route reads req.query.{{field}} but the route schema validates body without a matching querystring schema \u2014 a missing query param flows downstream as undefined instead of failing with a 400.",
|
|
8645
|
+
missingSchemaEntirely: "This route reads req.body.{{field}} but declares no request schema at all \u2014 req.body is untyped and unvalidated before use."
|
|
8646
|
+
}
|
|
8647
|
+
},
|
|
8648
|
+
create(context) {
|
|
8649
|
+
function getOptionsArg(routeCall) {
|
|
8650
|
+
return routeCall.arguments?.length === 3 ? routeCall.arguments[1] : null;
|
|
8651
|
+
}
|
|
8652
|
+
function getHandlerArg(routeCall) {
|
|
8653
|
+
const args = routeCall.arguments ?? [];
|
|
8654
|
+
return args[args.length - 1];
|
|
8655
|
+
}
|
|
8656
|
+
function hasSchemaProperty(optionsArg, propName2) {
|
|
8657
|
+
if (optionsArg?.type !== "ObjectExpression") return false;
|
|
8658
|
+
const schemaProp = (optionsArg.properties ?? []).find(
|
|
8659
|
+
(p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "schema"
|
|
8660
|
+
);
|
|
8661
|
+
if (!schemaProp || schemaProp.value?.type !== "ObjectExpression") return false;
|
|
8662
|
+
return (schemaProp.value.properties ?? []).some(
|
|
8663
|
+
(p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === propName2
|
|
8664
|
+
);
|
|
8665
|
+
}
|
|
8666
|
+
function hasAnySchema(optionsArg) {
|
|
8667
|
+
if (optionsArg?.type !== "ObjectExpression") return false;
|
|
8668
|
+
return (optionsArg.properties ?? []).some(
|
|
8669
|
+
(p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "schema"
|
|
8670
|
+
);
|
|
8671
|
+
}
|
|
8672
|
+
function findReadFields(handlerBody, objectNames) {
|
|
8673
|
+
const fields = /* @__PURE__ */ new Set();
|
|
8674
|
+
findInSubtree(handlerBody, (n) => {
|
|
8675
|
+
if (n?.type !== "MemberExpression") return false;
|
|
8676
|
+
const obj = n.object;
|
|
8677
|
+
if (obj?.type !== "MemberExpression") return false;
|
|
8678
|
+
if (obj.object?.type !== "Identifier" || obj.object.name !== "req" && obj.object.name !== "request") return false;
|
|
8679
|
+
if (obj.property?.type !== "Identifier" || !objectNames.has(obj.property.name)) return false;
|
|
8680
|
+
const field = n.property?.type === "Identifier" ? n.property.name : null;
|
|
8681
|
+
if (field) fields.add(field);
|
|
8682
|
+
return false;
|
|
8683
|
+
});
|
|
8684
|
+
return fields;
|
|
8685
|
+
}
|
|
8686
|
+
return {
|
|
8687
|
+
CallExpression(node) {
|
|
8688
|
+
if (!isPostRouteRegistration(node)) return;
|
|
8689
|
+
const optionsArg = getOptionsArg(node);
|
|
8690
|
+
const handler = getHandlerArg(node);
|
|
8691
|
+
if (!handler || handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression") return;
|
|
8692
|
+
const queryFields = findReadFields(handler.body, /* @__PURE__ */ new Set(["query"]));
|
|
8693
|
+
const bodyFields = findReadFields(handler.body, /* @__PURE__ */ new Set(["body"]));
|
|
8694
|
+
const hasBodySchema = hasSchemaProperty(optionsArg, "body");
|
|
8695
|
+
const hasQuerystringSchema = hasSchemaProperty(optionsArg, "querystring");
|
|
8696
|
+
if (queryFields.size > 0 && hasBodySchema && !hasQuerystringSchema) {
|
|
8697
|
+
const field = [...queryFields][0];
|
|
8698
|
+
context.report({ node, messageId: "missingQuerystringSchema", data: { field } });
|
|
8699
|
+
}
|
|
8700
|
+
if (bodyFields.size > 0 && !hasAnySchema(optionsArg)) {
|
|
8701
|
+
const field = [...bodyFields][0];
|
|
8702
|
+
context.report({ node, messageId: "missingSchemaEntirely", data: { field } });
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
};
|
|
8706
|
+
}
|
|
8707
|
+
};
|
|
8708
|
+
var twilioValidateAllRequestInputsRule = rule100;
|
|
8709
|
+
|
|
8710
|
+
// src/providers/twilio/rules/media-streams-mark-name-string.ts
|
|
8711
|
+
var rule101 = {
|
|
8712
|
+
meta: {
|
|
8713
|
+
type: "suggestion",
|
|
8714
|
+
docs: {
|
|
8715
|
+
description: "Media Streams mark.name must be a string, not a number",
|
|
8716
|
+
category: "correctness",
|
|
8717
|
+
rationale: 'Twilio\'s documented Media Streams `mark` message schema and every example show `mark.name` as a string (e.g. "my label"). Setting it to a bare number \u2014 `Date.now()` is a common culprit \u2014 means JSON.stringify() serializes it as a numeric literal, not the documented string type, a latent type mismatch that surfaces once mark-based pacing actually depends on matching mark names.',
|
|
8718
|
+
docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
|
|
8719
|
+
recommended: true
|
|
8720
|
+
},
|
|
8721
|
+
messages: {
|
|
8722
|
+
markNameNotString: "mark.name is set to a non-string value here \u2014 Twilio's documented schema requires mark.name to be a string (e.g. String(Date.now()))."
|
|
8723
|
+
}
|
|
8724
|
+
},
|
|
8725
|
+
create(context) {
|
|
8726
|
+
const NUMERIC_CALLS = /* @__PURE__ */ new Set(["Date.now", "performance.now", "Math.random", "Math.floor", "Math.round"]);
|
|
8727
|
+
function isNumericLooking(n) {
|
|
8728
|
+
if (!n) return false;
|
|
8729
|
+
if (n.type === "Literal" && typeof n.value === "number") return true;
|
|
8730
|
+
if (n.type === "CallExpression") {
|
|
8731
|
+
const callee = n.callee;
|
|
8732
|
+
if (callee?.type === "Identifier") return false;
|
|
8733
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.property?.type === "Identifier") {
|
|
8734
|
+
return NUMERIC_CALLS.has(`${callee.object.name}.${callee.property.name}`);
|
|
8735
|
+
}
|
|
8736
|
+
return false;
|
|
8737
|
+
}
|
|
8738
|
+
return false;
|
|
8739
|
+
}
|
|
8740
|
+
function findMarkNameProperty(markObjExpr) {
|
|
8741
|
+
for (const p of markObjExpr.properties ?? []) {
|
|
8742
|
+
if (p.type !== "Property") continue;
|
|
8743
|
+
const keyName = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
|
|
8744
|
+
if (keyName === "name") return p;
|
|
8745
|
+
}
|
|
8746
|
+
return null;
|
|
8747
|
+
}
|
|
8748
|
+
return {
|
|
8749
|
+
Property(node) {
|
|
8750
|
+
const keyName = node.key?.type === "Identifier" ? node.key.name : node.key?.type === "Literal" ? node.key.value : null;
|
|
8751
|
+
if (keyName !== "mark") return;
|
|
8752
|
+
if (node.value?.type !== "ObjectExpression") return;
|
|
8753
|
+
const nameProp = findMarkNameProperty(node.value);
|
|
8754
|
+
if (!nameProp) return;
|
|
8755
|
+
if (!isNumericLooking(nameProp.value)) return;
|
|
8756
|
+
context.report({ node: nameProp, messageId: "markNameNotString" });
|
|
8757
|
+
}
|
|
8758
|
+
};
|
|
8759
|
+
}
|
|
8760
|
+
};
|
|
8761
|
+
var twilioMediaStreamsMarkNameStringRule = rule101;
|
|
8762
|
+
|
|
8763
|
+
// src/providers/openai-realtime/utils.ts
|
|
8764
|
+
function isOpenAIRealtimeUrlArg(node) {
|
|
8765
|
+
if (!node) return false;
|
|
8766
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
8767
|
+
return node.value.includes("api.openai.com/v1/realtime");
|
|
8768
|
+
}
|
|
8769
|
+
if (node.type === "TemplateLiteral") {
|
|
8770
|
+
return (node.quasis ?? []).some(
|
|
8771
|
+
(q) => typeof q?.value?.raw === "string" && q.value.raw.includes("api.openai.com/v1/realtime")
|
|
8772
|
+
);
|
|
8773
|
+
}
|
|
8774
|
+
return false;
|
|
8775
|
+
}
|
|
8776
|
+
function collectOpenAIRealtimeUrlVarNames(program2) {
|
|
8777
|
+
const names = /* @__PURE__ */ new Set();
|
|
8778
|
+
visit(program2);
|
|
8779
|
+
return names;
|
|
8780
|
+
function visit(node, depth = 0) {
|
|
8781
|
+
if (!node || typeof node !== "object" || depth > 40) return;
|
|
8782
|
+
if (Array.isArray(node)) {
|
|
8783
|
+
for (const n of node) visit(n, depth + 1);
|
|
8784
|
+
return;
|
|
8785
|
+
}
|
|
8786
|
+
if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && isOpenAIRealtimeUrlArg(node.init)) {
|
|
8787
|
+
names.add(node.id.name);
|
|
8788
|
+
}
|
|
8789
|
+
for (const key of Object.keys(node)) {
|
|
8790
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8791
|
+
const val = node[key];
|
|
8792
|
+
if (val && typeof val === "object") visit(val, depth + 1);
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
function isOpenAIRealtimeUrlNode(urlArgNode, urlVarNames) {
|
|
8797
|
+
if (isOpenAIRealtimeUrlArg(urlArgNode)) return true;
|
|
8798
|
+
return urlArgNode?.type === "Identifier" && urlVarNames.has(urlArgNode.name);
|
|
8799
|
+
}
|
|
8800
|
+
function isOpenAIRealtimeNewWebSocket(node, urlVarNames = /* @__PURE__ */ new Set()) {
|
|
8801
|
+
if (node?.type !== "NewExpression") return false;
|
|
8802
|
+
if (node.callee?.type !== "Identifier" || node.callee.name !== "WebSocket") return false;
|
|
8803
|
+
return isOpenAIRealtimeUrlNode(node.arguments?.[0], urlVarNames);
|
|
8804
|
+
}
|
|
8805
|
+
function findProperty4(objectExpression, propertyName) {
|
|
8806
|
+
if (objectExpression?.type !== "ObjectExpression") return null;
|
|
8807
|
+
for (const prop of objectExpression.properties ?? []) {
|
|
8808
|
+
if (prop?.type !== "Property") continue;
|
|
8809
|
+
const key = prop.key;
|
|
8810
|
+
const name = key?.type === "Identifier" ? key.name : key?.type === "Literal" ? key.value : null;
|
|
8811
|
+
if (name === propertyName) return prop;
|
|
8812
|
+
}
|
|
8813
|
+
return null;
|
|
8814
|
+
}
|
|
8815
|
+
function collectOpenAIRealtimeSocketVarNames(program2) {
|
|
8816
|
+
const urlVarNames = collectOpenAIRealtimeUrlVarNames(program2);
|
|
8817
|
+
const names = /* @__PURE__ */ new Set();
|
|
8818
|
+
visit(program2);
|
|
8819
|
+
return names;
|
|
8820
|
+
function visit(node, depth = 0) {
|
|
8821
|
+
if (!node || typeof node !== "object" || depth > 40) return;
|
|
8822
|
+
if (Array.isArray(node)) {
|
|
8823
|
+
for (const n of node) visit(n, depth + 1);
|
|
8824
|
+
return;
|
|
8825
|
+
}
|
|
8826
|
+
if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && isOpenAIRealtimeNewWebSocket(node.init, urlVarNames)) {
|
|
8827
|
+
names.add(node.id.name);
|
|
8828
|
+
}
|
|
8829
|
+
if (node.type === "AssignmentExpression" && node.operator === "=" && isOpenAIRealtimeNewWebSocket(node.right, urlVarNames)) {
|
|
8830
|
+
const left = node.left;
|
|
8831
|
+
if (left?.type === "Identifier") {
|
|
8832
|
+
names.add(left.name);
|
|
8833
|
+
} else if (left?.type === "MemberExpression" && left.property) {
|
|
8834
|
+
const propName2 = left.property.name;
|
|
8835
|
+
if (typeof propName2 === "string") names.add(propName2);
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
for (const key of Object.keys(node)) {
|
|
8839
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8840
|
+
const val = node[key];
|
|
8841
|
+
if (val && typeof val === "object") visit(val, depth + 1);
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
function isTrackedSocketRef(objNode, socketVarNames) {
|
|
8846
|
+
if (objNode?.type === "Identifier") return socketVarNames.has(objNode.name);
|
|
8847
|
+
if (objNode?.type === "MemberExpression" && objNode.property) {
|
|
8848
|
+
const propName2 = objNode.property.name;
|
|
8849
|
+
return typeof propName2 === "string" && socketVarNames.has(propName2);
|
|
8850
|
+
}
|
|
8851
|
+
return false;
|
|
8852
|
+
}
|
|
8853
|
+
function isSocketOnCall(node, socketVarNames, eventName) {
|
|
8854
|
+
if (node?.type !== "CallExpression") return false;
|
|
8855
|
+
const callee = node.callee;
|
|
8856
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
8857
|
+
const prop = callee.property;
|
|
8858
|
+
if (prop?.type !== "Identifier" || prop.name !== "on") return false;
|
|
8859
|
+
if (!isTrackedSocketRef(callee.object, socketVarNames)) return false;
|
|
8860
|
+
const firstArg = node.arguments?.[0];
|
|
8861
|
+
return firstArg?.type === "Literal" && firstArg.value === eventName;
|
|
8862
|
+
}
|
|
8863
|
+
function collectStringVarValues(program2) {
|
|
8864
|
+
const values = /* @__PURE__ */ new Map();
|
|
8865
|
+
visit(program2);
|
|
8866
|
+
return values;
|
|
8867
|
+
function visit(node, depth = 0) {
|
|
8868
|
+
if (!node || typeof node !== "object" || depth > 40) return;
|
|
8869
|
+
if (Array.isArray(node)) {
|
|
8870
|
+
for (const n of node) visit(n, depth + 1);
|
|
8871
|
+
return;
|
|
8872
|
+
}
|
|
8873
|
+
if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
|
|
8874
|
+
const init = node.init;
|
|
8875
|
+
if (init?.type === "Literal" && typeof init.value === "string") {
|
|
8876
|
+
values.set(node.id.name, init.value);
|
|
8877
|
+
} else if (init?.type === "TemplateLiteral" && (init.expressions ?? []).length === 0) {
|
|
8878
|
+
values.set(node.id.name, (init.quasis ?? []).map((q) => q.value?.raw ?? "").join(""));
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8881
|
+
for (const key of Object.keys(node)) {
|
|
8882
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8883
|
+
const val = node[key];
|
|
8884
|
+
if (val && typeof val === "object") visit(val, depth + 1);
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
function resolveStringValue(node, stringVarValues) {
|
|
8889
|
+
if (!node) return null;
|
|
8890
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
8891
|
+
if (node.type === "TemplateLiteral" && (node.expressions ?? []).length === 0) {
|
|
8892
|
+
return (node.quasis ?? []).map((q) => q.value?.raw ?? "").join("");
|
|
8893
|
+
}
|
|
8894
|
+
if (node.type === "Identifier" && stringVarValues.has(node.name)) {
|
|
8895
|
+
return stringVarValues.get(node.name) ?? null;
|
|
8896
|
+
}
|
|
8897
|
+
return null;
|
|
8898
|
+
}
|
|
8899
|
+
|
|
8900
|
+
// src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
|
|
8901
|
+
var rule102 = {
|
|
8902
|
+
meta: {
|
|
8903
|
+
type: "problem",
|
|
8904
|
+
docs: {
|
|
8905
|
+
description: "OpenAI Realtime connections must not pin to the deprecated beta interface",
|
|
8906
|
+
category: "correctness",
|
|
8907
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
|
|
8908
|
+
rationale: "OpenAI's current Realtime guide states that beta integrations must migrate to the GA interface before new work proceeds, and explicitly calls out removing the OpenAI-Beta: realtime=v1 header as a required step. Staying on the beta header keeps a connection locked to the legacy session shape and event names with no confirmed sunset date \u2014 a ticking liability rather than an active failure.",
|
|
8909
|
+
recommended: true
|
|
8910
|
+
},
|
|
8911
|
+
messages: {
|
|
8912
|
+
betaHeaderPresent: "This OpenAI Realtime connection sends the deprecated 'OpenAI-Beta: realtime=v1' header instead of using the GA interface."
|
|
8913
|
+
},
|
|
8914
|
+
schema: []
|
|
8915
|
+
},
|
|
8916
|
+
create(context) {
|
|
8917
|
+
let urlVarNames = /* @__PURE__ */ new Set();
|
|
8918
|
+
return {
|
|
8919
|
+
Program(node) {
|
|
8920
|
+
urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
|
|
8921
|
+
},
|
|
8922
|
+
NewExpression(node) {
|
|
8923
|
+
if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
|
|
8924
|
+
const optionsArg = node.arguments?.[1];
|
|
8925
|
+
if (optionsArg?.type !== "ObjectExpression") return;
|
|
8926
|
+
const headersProp = findProperty4(optionsArg, "headers");
|
|
8927
|
+
if (headersProp?.value?.type !== "ObjectExpression") return;
|
|
8928
|
+
const betaProp = findProperty4(headersProp.value, "OpenAI-Beta");
|
|
8929
|
+
if (!betaProp) return;
|
|
8930
|
+
const value = betaProp.value;
|
|
8931
|
+
const isRealtimeV1 = value?.type === "Literal" && typeof value.value === "string" && value.value.includes("realtime=v1");
|
|
8932
|
+
if (!isRealtimeV1) return;
|
|
8933
|
+
context.report({ node: betaProp, messageId: "betaHeaderPresent" });
|
|
8934
|
+
}
|
|
8935
|
+
};
|
|
8936
|
+
}
|
|
8937
|
+
};
|
|
8938
|
+
var openaiRealtimeMigrateBetaToGaRule = rule102;
|
|
8939
|
+
|
|
8940
|
+
// src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
|
|
8941
|
+
var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
|
|
8942
|
+
function isLogCall(node) {
|
|
8943
|
+
if (node?.type !== "CallExpression") return { matches: false };
|
|
8944
|
+
const callee = node.callee;
|
|
8945
|
+
if (callee?.type !== "MemberExpression") return { matches: false };
|
|
8946
|
+
const prop = callee.property;
|
|
8947
|
+
if (prop?.type !== "Identifier" || !LOG_METHOD_NAMES.has(prop.name)) return { matches: false };
|
|
8948
|
+
return { matches: true, calleeProp: prop };
|
|
8949
|
+
}
|
|
8950
|
+
function referencesRawParam(argNode, paramName) {
|
|
8951
|
+
if (!argNode) return false;
|
|
8952
|
+
if (argNode.type === "Identifier" && argNode.name === paramName) return true;
|
|
8953
|
+
if (argNode.type === "CallExpression" && argNode.callee?.type === "MemberExpression" && argNode.callee.object?.type === "Identifier" && argNode.callee.object.name === paramName && argNode.callee.property?.type === "Identifier" && argNode.callee.property.name === "toString") {
|
|
8954
|
+
return true;
|
|
8955
|
+
}
|
|
8956
|
+
if (argNode.type === "TemplateLiteral") {
|
|
8957
|
+
return (argNode.expressions ?? []).some((expr) => referencesRawParam(expr, paramName));
|
|
8958
|
+
}
|
|
8959
|
+
return false;
|
|
8960
|
+
}
|
|
8961
|
+
function findCallExpressions(node, out, depth = 0) {
|
|
8962
|
+
if (!node || typeof node !== "object" || depth > 40) return;
|
|
8963
|
+
if (Array.isArray(node)) {
|
|
8964
|
+
for (const n of node) findCallExpressions(n, out, depth + 1);
|
|
8965
|
+
return;
|
|
8966
|
+
}
|
|
8967
|
+
if (node.type === "CallExpression") out.push(node);
|
|
8968
|
+
for (const key of Object.keys(node)) {
|
|
8969
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
8970
|
+
const val = node[key];
|
|
8971
|
+
if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
|
|
8972
|
+
}
|
|
8973
|
+
}
|
|
8974
|
+
var rule103 = {
|
|
8975
|
+
meta: {
|
|
8976
|
+
type: "problem",
|
|
8977
|
+
docs: {
|
|
8978
|
+
description: "Raw OpenAI Realtime message payloads must not be logged verbatim",
|
|
8979
|
+
category: "security",
|
|
8980
|
+
cwe: "CWE-532",
|
|
8981
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
|
|
8982
|
+
rationale: "Every inbound Realtime WebSocket message can include response.audio.delta payloads (base64-encoded live call audio) and, once transcription is wired up, transcript text. Logging the raw message object or string verbatim writes a durable, unredacted record of live conversation content into whatever log sink the application ships to.",
|
|
8983
|
+
recommended: true
|
|
8984
|
+
},
|
|
8985
|
+
messages: {
|
|
8986
|
+
rawPayloadLogged: "A raw OpenAI Realtime message is logged verbatim here, which can include live call audio or transcript content."
|
|
8987
|
+
},
|
|
8988
|
+
schema: []
|
|
8989
|
+
},
|
|
8990
|
+
create(context) {
|
|
8991
|
+
let socketVarNames = /* @__PURE__ */ new Set();
|
|
8992
|
+
return {
|
|
8993
|
+
Program(node) {
|
|
8994
|
+
socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
|
|
8995
|
+
},
|
|
8996
|
+
CallExpression(node) {
|
|
8997
|
+
if (socketVarNames.size === 0) return;
|
|
8998
|
+
if (!isSocketOnCall(node, socketVarNames, "message")) return;
|
|
8999
|
+
const handler = node.arguments?.[1];
|
|
9000
|
+
if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
|
|
9001
|
+
const param = handler.params?.[0];
|
|
9002
|
+
if (param?.type !== "Identifier") return;
|
|
9003
|
+
const paramName = param.name;
|
|
9004
|
+
const calls = [];
|
|
9005
|
+
findCallExpressions(handler.body, calls);
|
|
9006
|
+
for (const call of calls) {
|
|
9007
|
+
const { matches } = isLogCall(call);
|
|
9008
|
+
if (!matches) continue;
|
|
9009
|
+
const hasRawArg = (call.arguments ?? []).some((arg) => referencesRawParam(arg, paramName));
|
|
9010
|
+
if (hasRawArg) {
|
|
9011
|
+
context.report({ node: call, messageId: "rawPayloadLogged" });
|
|
9012
|
+
}
|
|
9013
|
+
}
|
|
9014
|
+
}
|
|
9015
|
+
};
|
|
9016
|
+
}
|
|
9017
|
+
};
|
|
9018
|
+
var openaiRealtimeNoLogRawMessagePayloadsRule = rule103;
|
|
9019
|
+
|
|
9020
|
+
// src/providers/openai-realtime/rules/handle-error-server-event.ts
|
|
9021
|
+
function isTypeMemberExpression(node) {
|
|
9022
|
+
return node?.type === "MemberExpression" && node.property?.type === "Identifier" && node.property.name === "type";
|
|
9023
|
+
}
|
|
9024
|
+
function collectTypeComparisonLiterals(node, out, depth = 0) {
|
|
9025
|
+
if (!node || typeof node !== "object" || depth > 60) return;
|
|
9026
|
+
if (Array.isArray(node)) {
|
|
9027
|
+
for (const n of node) collectTypeComparisonLiterals(n, out, depth + 1);
|
|
9028
|
+
return;
|
|
9029
|
+
}
|
|
9030
|
+
if (node.type === "BinaryExpression" && (node.operator === "===" || node.operator === "==")) {
|
|
9031
|
+
if (isTypeMemberExpression(node.left) && node.right?.type === "Literal" && typeof node.right.value === "string") {
|
|
9032
|
+
out.add(node.right.value);
|
|
9033
|
+
}
|
|
9034
|
+
if (isTypeMemberExpression(node.right) && node.left?.type === "Literal" && typeof node.left.value === "string") {
|
|
9035
|
+
out.add(node.left.value);
|
|
9036
|
+
}
|
|
9037
|
+
}
|
|
9038
|
+
if (node.type === "SwitchStatement" && isTypeMemberExpression(node.discriminant)) {
|
|
9039
|
+
for (const c of node.cases ?? []) {
|
|
9040
|
+
if (c?.test?.type === "Literal" && typeof c.test.value === "string") out.add(c.test.value);
|
|
9041
|
+
}
|
|
9042
|
+
}
|
|
9043
|
+
for (const key of Object.keys(node)) {
|
|
9044
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
9045
|
+
const val = node[key];
|
|
9046
|
+
if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
|
|
9047
|
+
}
|
|
9048
|
+
}
|
|
9049
|
+
var rule104 = {
|
|
9050
|
+
meta: {
|
|
9051
|
+
type: "problem",
|
|
9052
|
+
docs: {
|
|
9053
|
+
description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
|
|
9054
|
+
category: "reliability",
|
|
9055
|
+
docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime_server_events",
|
|
9056
|
+
rationale: "The Realtime API's own error server event \u2014 distinct from the WebSocket transport-level error event \u2014 covers rate limits, invalid session.update payloads, content-moderation blocks, and retired/invalid model ids. A message handler that branches on event types but never checks for 'error' lets the call continue silently with translation/processing permanently dead for that leg, with no operator-visible signal of why.",
|
|
9057
|
+
recommended: true
|
|
9058
|
+
},
|
|
9059
|
+
messages: {
|
|
9060
|
+
missingErrorBranch: "This Realtime message handler branches on event types but never checks for message.type === 'error'."
|
|
9061
|
+
},
|
|
9062
|
+
schema: []
|
|
9063
|
+
},
|
|
9064
|
+
create(context) {
|
|
9065
|
+
let socketVarNames = /* @__PURE__ */ new Set();
|
|
9066
|
+
return {
|
|
9067
|
+
Program(node) {
|
|
9068
|
+
socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
|
|
9069
|
+
},
|
|
9070
|
+
CallExpression(node) {
|
|
9071
|
+
if (socketVarNames.size === 0) return;
|
|
9072
|
+
if (!isSocketOnCall(node, socketVarNames, "message")) return;
|
|
9073
|
+
const handler = node.arguments?.[1];
|
|
9074
|
+
if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
|
|
9075
|
+
const literals = /* @__PURE__ */ new Set();
|
|
9076
|
+
collectTypeComparisonLiterals(handler.body, literals);
|
|
9077
|
+
if (literals.size === 0) return;
|
|
9078
|
+
if (!literals.has("error")) {
|
|
9079
|
+
context.report({ node, messageId: "missingErrorBranch" });
|
|
9080
|
+
}
|
|
9081
|
+
}
|
|
9082
|
+
};
|
|
9083
|
+
}
|
|
9084
|
+
};
|
|
9085
|
+
var openaiRealtimeHandleErrorServerEventRule = rule104;
|
|
9086
|
+
|
|
9087
|
+
// src/providers/openai-realtime/rules/reconnect-on-drop.ts
|
|
9088
|
+
var RECONNECT_NAME_PATTERN = /reconnect/i;
|
|
9089
|
+
function hasReconnectAttempt(node, depth = 0) {
|
|
9090
|
+
if (!node || typeof node !== "object" || depth > 60) return false;
|
|
9091
|
+
if (Array.isArray(node)) {
|
|
9092
|
+
return node.some((n) => hasReconnectAttempt(n, depth + 1));
|
|
9093
|
+
}
|
|
9094
|
+
if (node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "WebSocket") {
|
|
9095
|
+
return true;
|
|
9096
|
+
}
|
|
9097
|
+
if (node.type === "CallExpression") {
|
|
9098
|
+
const callee = node.callee;
|
|
9099
|
+
const calleeName = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : null;
|
|
9100
|
+
if (typeof calleeName === "string" && RECONNECT_NAME_PATTERN.test(calleeName)) return true;
|
|
9101
|
+
}
|
|
9102
|
+
for (const key of Object.keys(node)) {
|
|
9103
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
9104
|
+
const val = node[key];
|
|
9105
|
+
if (val && typeof val === "object" && hasReconnectAttempt(val, depth + 1)) return true;
|
|
9106
|
+
}
|
|
9107
|
+
return false;
|
|
9108
|
+
}
|
|
9109
|
+
var rule105 = {
|
|
9110
|
+
meta: {
|
|
9111
|
+
type: "problem",
|
|
9112
|
+
docs: {
|
|
9113
|
+
description: "A dropped OpenAI Realtime connection must be retried, not just logged",
|
|
9114
|
+
category: "reliability",
|
|
9115
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
|
|
9116
|
+
rationale: "For a multi-minute phone call, a single transient OpenAI-side disconnect that is only logged on 'close' permanently kills translation/processing for the remainder of the call in that direction \u2014 the call itself stays connected and silently degraded, with neither party getting a signal that something stopped working.",
|
|
9117
|
+
recommended: true
|
|
9118
|
+
},
|
|
9119
|
+
messages: {
|
|
9120
|
+
noReconnectAttempt: "This Realtime socket's close handler only logs and never attempts to reconnect."
|
|
9121
|
+
},
|
|
9122
|
+
schema: []
|
|
9123
|
+
},
|
|
9124
|
+
create(context) {
|
|
9125
|
+
let socketVarNames = /* @__PURE__ */ new Set();
|
|
9126
|
+
return {
|
|
9127
|
+
Program(node) {
|
|
9128
|
+
socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
|
|
9129
|
+
},
|
|
9130
|
+
CallExpression(node) {
|
|
9131
|
+
if (socketVarNames.size === 0) return;
|
|
9132
|
+
if (!isSocketOnCall(node, socketVarNames, "close")) return;
|
|
9133
|
+
const handler = node.arguments?.[1];
|
|
9134
|
+
if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
|
|
9135
|
+
if (!hasReconnectAttempt(handler.body)) {
|
|
9136
|
+
context.report({ node, messageId: "noReconnectAttempt" });
|
|
9137
|
+
}
|
|
9138
|
+
}
|
|
9139
|
+
};
|
|
9140
|
+
}
|
|
9141
|
+
};
|
|
9142
|
+
var openaiRealtimeReconnectOnDropRule = rule105;
|
|
9143
|
+
|
|
9144
|
+
// src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
|
|
9145
|
+
var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
|
|
9146
|
+
var rule106 = {
|
|
9147
|
+
meta: {
|
|
9148
|
+
type: "suggestion",
|
|
9149
|
+
docs: {
|
|
9150
|
+
description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
|
|
9151
|
+
category: "correctness",
|
|
9152
|
+
docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
|
|
9153
|
+
rationale: "Dated preview snapshots are the category of model id OpenAI has historically retired with advance-notice sunset windows. Pinning to a dated preview snapshot rather than the floating GA alias (or a current dated GA snapshot) maximizes exposure to an eventual retirement, with no code path to detect or gracefully react to a model_not_found-style error if/when that happens.",
|
|
9154
|
+
recommended: true
|
|
9155
|
+
},
|
|
9156
|
+
messages: {
|
|
9157
|
+
datedPreviewSnapshot: "This Realtime connection is pinned to a dated preview model snapshot instead of the GA alias."
|
|
9158
|
+
},
|
|
9159
|
+
schema: []
|
|
9160
|
+
},
|
|
9161
|
+
create(context) {
|
|
9162
|
+
let stringVarValues = /* @__PURE__ */ new Map();
|
|
9163
|
+
let urlVarNames = /* @__PURE__ */ new Set();
|
|
9164
|
+
return {
|
|
9165
|
+
Program(node) {
|
|
9166
|
+
stringVarValues = collectStringVarValues(node);
|
|
9167
|
+
urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
|
|
9168
|
+
},
|
|
9169
|
+
NewExpression(node) {
|
|
9170
|
+
if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
|
|
9171
|
+
const urlArg = node.arguments?.[0];
|
|
9172
|
+
const urlString = resolveStringValue(urlArg, stringVarValues);
|
|
9173
|
+
if (!urlString) return;
|
|
9174
|
+
if (DATED_PREVIEW_MODEL_PATTERN.test(urlString)) {
|
|
9175
|
+
context.report({ node: urlArg, messageId: "datedPreviewSnapshot" });
|
|
9176
|
+
}
|
|
9177
|
+
}
|
|
9178
|
+
};
|
|
9179
|
+
}
|
|
9180
|
+
};
|
|
9181
|
+
var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule106;
|
|
9182
|
+
|
|
9183
|
+
// src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
|
|
9184
|
+
var rule107 = {
|
|
9185
|
+
meta: {
|
|
9186
|
+
type: "suggestion",
|
|
9187
|
+
docs: {
|
|
9188
|
+
description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
|
|
9189
|
+
category: "correctness",
|
|
9190
|
+
docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
|
|
9191
|
+
rationale: "Two independent fetches of the current Realtime sessions API reference did not surface 'temperature' as a documented session field. Sending an undocumented field is typically harmless (silently ignored), but code that depends on an assumed constraint (e.g. 'a hard floor of 0.6') for behavior like deterministic output is relying on a claim that is no longer traceable to any current, citable source.",
|
|
9192
|
+
recommended: true
|
|
9193
|
+
},
|
|
9194
|
+
messages: {
|
|
9195
|
+
unverifiedTemperatureField: "This session.update payload sets 'temperature', a field not documented in the current GA Realtime sessions schema."
|
|
9196
|
+
},
|
|
9197
|
+
schema: []
|
|
9198
|
+
},
|
|
9199
|
+
create(context) {
|
|
9200
|
+
return {
|
|
9201
|
+
ObjectExpression(node) {
|
|
9202
|
+
const typeProp = findProperty4(node, "type");
|
|
9203
|
+
const typeValue = typeProp?.value;
|
|
9204
|
+
const isSessionUpdate = typeValue?.type === "Literal" && typeof typeValue.value === "string" && typeValue.value === "session.update";
|
|
9205
|
+
if (!isSessionUpdate) return;
|
|
9206
|
+
const sessionProp = findProperty4(node, "session");
|
|
9207
|
+
if (sessionProp?.value?.type !== "ObjectExpression") return;
|
|
9208
|
+
const temperatureProp = findProperty4(sessionProp.value, "temperature");
|
|
9209
|
+
if (!temperatureProp) return;
|
|
9210
|
+
context.report({ node: temperatureProp, messageId: "unverifiedTemperatureField" });
|
|
9211
|
+
}
|
|
9212
|
+
};
|
|
9213
|
+
}
|
|
9214
|
+
};
|
|
9215
|
+
var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule107;
|
|
9216
|
+
|
|
9217
|
+
// src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
|
|
9218
|
+
var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
|
|
9219
|
+
function isReadyStateOpenCheck(test) {
|
|
9220
|
+
if (test?.type !== "BinaryExpression" || test.operator !== "===" && test.operator !== "==") return false;
|
|
9221
|
+
const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
|
|
9222
|
+
const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
|
|
9223
|
+
return isReadyStateMember(test.left) && isOpenValue(test.right) || isReadyStateMember(test.right) && isOpenValue(test.left);
|
|
9224
|
+
}
|
|
9225
|
+
function hasQueueCall(node, depth = 0) {
|
|
9226
|
+
if (!node || typeof node !== "object" || depth > 40) return false;
|
|
9227
|
+
if (Array.isArray(node)) return node.some((n) => hasQueueCall(n, depth + 1));
|
|
9228
|
+
if (node.type === "CallExpression") {
|
|
9229
|
+
const callee = node.callee;
|
|
9230
|
+
const calleeName = callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : callee?.type === "Identifier" ? callee.name : null;
|
|
9231
|
+
if (typeof calleeName === "string" && QUEUE_CALL_NAME_PATTERN.test(calleeName)) return true;
|
|
9232
|
+
}
|
|
9233
|
+
for (const key of Object.keys(node)) {
|
|
9234
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
9235
|
+
const val = node[key];
|
|
9236
|
+
if (val && typeof val === "object" && hasQueueCall(val, depth + 1)) return true;
|
|
9237
|
+
}
|
|
9238
|
+
return false;
|
|
9239
|
+
}
|
|
9240
|
+
var rule108 = {
|
|
9241
|
+
meta: {
|
|
9242
|
+
type: "suggestion",
|
|
9243
|
+
docs: {
|
|
9244
|
+
description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
|
|
9245
|
+
category: "reliability",
|
|
9246
|
+
docsUrl: "https://developers.openai.com/api/docs/voice/media-streams/websocket-messages",
|
|
9247
|
+
rationale: "Caller audio can arrive before the Realtime WebSocket finishes its connect + session.update round-trip. A readyState !== OPEN branch that only logs and returns silently drops every audio chunk that arrives in that window, meaning the first fragment of speech is lost to translation/processing on every call.",
|
|
9248
|
+
recommended: true
|
|
9249
|
+
},
|
|
9250
|
+
messages: {
|
|
9251
|
+
audioDroppedNotBuffered: "Audio sent while this Realtime socket is not yet open is dropped here instead of being buffered and flushed once open."
|
|
9252
|
+
},
|
|
9253
|
+
schema: []
|
|
9254
|
+
},
|
|
9255
|
+
create(context) {
|
|
9256
|
+
return {
|
|
9257
|
+
IfStatement(node) {
|
|
9258
|
+
if (!isReadyStateOpenCheck(node.test)) return;
|
|
9259
|
+
if (!node.alternate) return;
|
|
9260
|
+
if (hasQueueCall(node.alternate)) return;
|
|
9261
|
+
context.report({ node: node.alternate, messageId: "audioDroppedNotBuffered" });
|
|
9262
|
+
}
|
|
9263
|
+
};
|
|
9264
|
+
}
|
|
9265
|
+
};
|
|
9266
|
+
var openaiRealtimeBufferAudioUntilSessionReadyRule = rule108;
|
|
9267
|
+
|
|
9268
|
+
// src/providers/openai-realtime/rules/send-safety-identifier.ts
|
|
9269
|
+
var rule109 = {
|
|
9270
|
+
meta: {
|
|
9271
|
+
type: "suggestion",
|
|
9272
|
+
docs: {
|
|
9273
|
+
description: "OpenAI Realtime connections should send an OpenAI-Safety-Identifier header",
|
|
9274
|
+
category: "security",
|
|
9275
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
|
|
9276
|
+
rationale: "OpenAI's Realtime guidance recommends including an OpenAI-Safety-Identifier header with a stable, privacy-preserving value (e.g. a hashed user/account id) on Realtime connections, to support OpenAI's abuse/safety monitoring for voice content. A connection that omits it gives OpenAI no way to correlate abusive sessions back to an account without per-connection metadata.",
|
|
9277
|
+
recommended: true
|
|
9278
|
+
},
|
|
9279
|
+
messages: {
|
|
9280
|
+
missingSafetyIdentifier: "This OpenAI Realtime WebSocket connection does not send an OpenAI-Safety-Identifier header."
|
|
9281
|
+
},
|
|
9282
|
+
schema: []
|
|
9283
|
+
},
|
|
9284
|
+
create(context) {
|
|
9285
|
+
let urlVarNames = /* @__PURE__ */ new Set();
|
|
9286
|
+
return {
|
|
9287
|
+
Program(node) {
|
|
9288
|
+
urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
|
|
9289
|
+
},
|
|
9290
|
+
NewExpression(node) {
|
|
9291
|
+
if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
|
|
9292
|
+
const optionsArg = node.arguments?.[1];
|
|
9293
|
+
const headersProp = optionsArg?.type === "ObjectExpression" ? findProperty4(optionsArg, "headers") : null;
|
|
9294
|
+
const headersObj = headersProp?.value?.type === "ObjectExpression" ? headersProp.value : null;
|
|
9295
|
+
const safetyIdProp = headersObj ? findProperty4(headersObj, "OpenAI-Safety-Identifier") : null;
|
|
9296
|
+
if (safetyIdProp) return;
|
|
9297
|
+
context.report({ node: headersObj ?? optionsArg ?? node, messageId: "missingSafetyIdentifier" });
|
|
9298
|
+
}
|
|
9299
|
+
};
|
|
9300
|
+
}
|
|
9301
|
+
};
|
|
9302
|
+
var openaiRealtimeSendSafetyIdentifierRule = rule109;
|
|
9303
|
+
|
|
9304
|
+
// src/providers/openai-realtime/rules/transcription-model-choice.ts
|
|
9305
|
+
var rule110 = {
|
|
9306
|
+
meta: {
|
|
9307
|
+
type: "suggestion",
|
|
9308
|
+
docs: {
|
|
9309
|
+
description: "OpenAI Realtime sessions should not configure 'whisper-1' for input transcription",
|
|
9310
|
+
category: "correctness",
|
|
9311
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
|
|
9312
|
+
rationale: "OpenAI's transcription guidance describes 'whisper-1' as for existing Whisper integrations that are not natively streaming, while 'gpt-realtime-whisper' is the natively-streaming option designed for realtime sessions. Configuring input_audio_transcription with 'whisper-1' is the wrong tool for a realtime session, adding latency/cost to a streaming pipeline.",
|
|
9313
|
+
recommended: true
|
|
9314
|
+
},
|
|
9315
|
+
messages: {
|
|
9316
|
+
nonStreamingTranscriptionModel: "input_audio_transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
|
|
9317
|
+
},
|
|
9318
|
+
schema: []
|
|
9319
|
+
},
|
|
9320
|
+
create(context) {
|
|
9321
|
+
return {
|
|
9322
|
+
ObjectExpression(node) {
|
|
9323
|
+
const typeProp = findProperty4(node, "type");
|
|
9324
|
+
const typeValue = typeProp?.value;
|
|
9325
|
+
const isSessionUpdate = typeValue?.type === "Literal" && typeof typeValue.value === "string" && typeValue.value === "session.update";
|
|
9326
|
+
if (!isSessionUpdate) return;
|
|
9327
|
+
const sessionProp = findProperty4(node, "session");
|
|
9328
|
+
if (sessionProp?.value?.type !== "ObjectExpression") return;
|
|
9329
|
+
const transcriptionProp = findProperty4(sessionProp.value, "input_audio_transcription");
|
|
9330
|
+
if (transcriptionProp?.value?.type !== "ObjectExpression") return;
|
|
9331
|
+
const modelProp = findProperty4(transcriptionProp.value, "model");
|
|
9332
|
+
const modelValue = modelProp?.value;
|
|
9333
|
+
const isWhisper1 = modelValue?.type === "Literal" && typeof modelValue.value === "string" && modelValue.value === "whisper-1";
|
|
9334
|
+
if (!isWhisper1) return;
|
|
9335
|
+
context.report({ node: modelProp, messageId: "nonStreamingTranscriptionModel" });
|
|
9336
|
+
}
|
|
9337
|
+
};
|
|
9338
|
+
}
|
|
9339
|
+
};
|
|
9340
|
+
var openaiRealtimeTranscriptionModelChoiceRule = rule110;
|
|
9341
|
+
|
|
9342
|
+
// src/plugin/index.ts
|
|
9343
|
+
var plugin = {
|
|
9344
|
+
meta: { name: PLUGIN_NAME, version: "0.0.1" },
|
|
9345
|
+
rules: {
|
|
9346
|
+
"resend-webhook-signature": resendWebhookSignatureRule,
|
|
9347
|
+
"resend-api-key-hardcoded": resendApiKeyHardcodedRule,
|
|
9348
|
+
"resend-api-key-in-client-bundle": resendApiKeyInClientBundleRule,
|
|
9349
|
+
"resend-marketing-via-batch-send": resendMarketingViaBatchSendRule,
|
|
9350
|
+
"resend-marketing-missing-unsubscribe": resendMarketingMissingUnsubscribeRule,
|
|
9351
|
+
"resend-test-domain-in-production-path": resendTestDomainInProductionPathRule,
|
|
9352
|
+
"resend-from-address-not-friendly-format": resendFromAddressNotFriendlyFormatRule,
|
|
9353
|
+
"resend-batch-size-not-enforced": resendBatchSizeNotEnforcedRule,
|
|
9354
|
+
"resend-missing-idempotency-key": resendMissingIdempotencyKeyRule,
|
|
9355
|
+
"resend-no-error-code-mapping": resendNoErrorCodeMappingRule,
|
|
9356
|
+
"resend-webhook-no-idempotency": resendWebhookNoIdempotencyRule,
|
|
9357
|
+
"resend-missing-tags": resendMissingTagsRule,
|
|
9358
|
+
"resend-request-id-not-logged": resendRequestIdNotLoggedRule,
|
|
9359
|
+
"supabase-scope-queries-by-tenant-column": supabaseScopeQueriesByTenantColumnRule,
|
|
9360
|
+
"supabase-validate-uuid-columns": supabaseValidateUuidColumnsRule,
|
|
9361
|
+
"supabase-order-by-timestamp-not-identity": supabaseOrderByTimestampNotIdentityRule,
|
|
9362
|
+
"supabase-consistent-input-length-limits": supabaseConsistentInputLengthLimitsRule,
|
|
9363
|
+
"supabase-idempotent-mutations": supabaseIdempotentMutationsRule,
|
|
9364
|
+
"supabase-fail-fast-env-validation": supabaseFailFastEnvValidationRule,
|
|
9365
|
+
"supabase-no-user-metadata-authz": supabaseNoUserMetadataAuthzRule,
|
|
9366
|
+
"supabase-single-without-error-check": supabaseSingleWithoutErrorCheckRule,
|
|
9367
|
+
"supabase-non-atomic-replace-pattern": supabaseNonAtomicReplacePatternRule,
|
|
9368
|
+
"supabase-unchecked-mutation-error": supabaseUncheckedMutationErrorRule,
|
|
9369
|
+
"supabase-realtime-missing-filter": supabaseRealtimeMissingFilterRule,
|
|
9370
|
+
"supabase-storage-error-not-surfaced": supabaseStorageErrorNotSurfacedRule,
|
|
9371
|
+
"auth0-required-audience-validation": auth0RequiredAudienceValidationRule,
|
|
9372
|
+
"auth0-no-account-link-without-verified-email": auth0NoAccountLinkWithoutVerifiedEmailRule,
|
|
9373
|
+
"auth0-dead-claim-verification-check": auth0DeadClaimVerificationCheckRule,
|
|
9374
|
+
"auth0-jwks-refresh-on-unknown-kid": auth0JwksRefreshOnUnknownKidRule,
|
|
9375
|
+
"firebase-missing-app-check": firebaseMissingAppCheckRule,
|
|
9376
|
+
"firebase-unhandled-auth-popup-rejection": firebaseUnhandledAuthPopupRejectionRule,
|
|
9377
|
+
"firebase-rtdb-list-read-for-single-item": firebaseRtdbListReadForSingleItemRule,
|
|
9378
|
+
"firebase-unvalidated-external-data-to-rtdb": firebaseUnvalidatedExternalDataToRtdbRule,
|
|
9379
|
+
"firebase-rtdb-batch-write-not-atomic": firebaseRtdbBatchWriteNotAtomicRule,
|
|
9380
|
+
"firebase-rtdb-listener-error-not-handled": firebaseRtdbListenerErrorNotHandledRule,
|
|
9381
|
+
"firebase-effect-deps-whole-user-object": firebaseEffectDepsWholeUserObjectRule,
|
|
9382
|
+
"firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
|
|
9383
|
+
"firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
|
|
9384
|
+
"firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
|
|
9385
|
+
"firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
|
|
9386
|
+
"firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
|
|
9387
|
+
"firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
|
|
9388
|
+
"firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
|
|
9389
|
+
"firebase-use-array-union-remove": firebaseUseArrayUnionRemoveRule,
|
|
9390
|
+
"firebase-duplicate-initialize-app": firebaseDuplicateInitializeAppRule,
|
|
9391
|
+
"firebase-onSnapshot-async-throw": firebaseOnSnapshotAsyncThrowRule,
|
|
9392
|
+
"firebase-onSnapshot-missing-error-callback": firebaseOnSnapshotMissingErrorCallbackRule,
|
|
9393
|
+
"firebase-firestore-document-size-guard": firebaseFirestoreDocumentSizeGuardRule,
|
|
9394
|
+
"firebase-use-timestamp-now": firebaseUseTimestampNowRule,
|
|
9395
|
+
"lovable-no-client-side-secret-fetch": lovableNoClientSideSecretFetchRule,
|
|
9396
|
+
"lovable-paid-flag-without-edge-function": lovablePaidFlagWithoutEdgeFunctionRule,
|
|
9397
|
+
"lovable-expiry-column-never-checked": lovableExpiryColumnNeverCheckedRule,
|
|
9398
|
+
"lovable-silent-catch-on-provider-call": lovableSilentCatchOnProviderCallRule,
|
|
9399
|
+
"browserbase-no-conditional-authz-on-anonymous-user": browserbaseNoConditionalAuthzOnAnonymousUserRule,
|
|
9400
|
+
"browserbase-no-connect-url-in-api-response": browserbaseNoConnectUrlInApiResponseRule,
|
|
9401
|
+
"browserbase-session-id-requires-ownership-check": browserbaseSessionIdRequiresOwnershipCheckRule,
|
|
9402
|
+
"browserbase-no-concurrent-shared-context": browserbaseNoConcurrentSharedContextRule,
|
|
9403
|
+
"browserbase-mobile-device-requires-os-setting": browserbaseMobileDeviceRequiresOsSettingRule,
|
|
9404
|
+
"browserbase-use-typed-exception-status-not-substring": browserbaseUseTypedExceptionStatusNotSubstringRule,
|
|
9405
|
+
"browserbase-release-session-on-connect-failure": browserbaseReleaseSessionOnConnectFailureRule,
|
|
9406
|
+
"browserbase-dont-stack-custom-retry-on-sdk-retry": browserbaseDontStackCustomRetryOnSdkRetryRule,
|
|
9407
|
+
"browserbase-no-overbroad-error-substring-match": browserbaseNoOverbroadErrorSubstringMatchRule,
|
|
9408
|
+
"browserbase-use-sdk-not-raw-requests": browserbaseUseSdkNotRawRequestsRule,
|
|
9409
|
+
"browserbase-centralize-request-release": browserbaseCentralizeRequestReleaseRule,
|
|
9410
|
+
"openai-cua-no-domain-allowlist": openaiCuaNoDomainAllowlistRule,
|
|
9411
|
+
"openai-cua-scroll-delta-default-zero": openaiCuaScrollDeltaDefaultZeroRule,
|
|
9412
|
+
"openai-cua-structured-step-metadata-not-text-json": openaiCuaStructuredStepMetadataNotTextJsonRule,
|
|
9413
|
+
"openai-cua-no-blind-safety-check-ack": openaiCuaNoBlindSafetyCheckAckRule,
|
|
9414
|
+
"openai-cua-retry-transient-turn-errors": openaiCuaRetryTransientTurnErrorsRule,
|
|
9415
|
+
"openai-cua-check-response-status-incomplete": openaiCuaCheckResponseStatusIncompleteRule,
|
|
9416
|
+
"openai-cua-set-safety-identifier": openaiCuaSetSafetyIdentifierRule,
|
|
9417
|
+
"tiptap-upload-validate-fn-void": tiptapUploadValidateFnVoidRule,
|
|
9418
|
+
"tiptap-script-src-hardcoded-api-key": tiptapScriptSrcHardcodedApiKeyRule,
|
|
9419
|
+
"tiptap-dynamic-script-no-sri": tiptapDynamicScriptNoSriRule,
|
|
9420
|
+
"tiptap-addAttributes-missing-renderHTML": tiptapAddAttributesMissingRenderHTMLRule,
|
|
9421
|
+
"tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
|
|
9422
|
+
"tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
|
|
9423
|
+
"tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
|
|
9424
|
+
"tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
|
|
9425
|
+
"tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
|
|
9426
|
+
"tiptap-prefer-table-kit": tiptapPreferTableKitRule,
|
|
9427
|
+
"tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,
|
|
9428
|
+
"elevenlabs-validate-signed-url-response": elevenlabsValidateSignedUrlResponseRule,
|
|
9429
|
+
"elevenlabs-no-error-object-logging": elevenlabsNoErrorObjectLoggingRule,
|
|
9430
|
+
"elevenlabs-fetch-timeout-required": elevenlabsFetchTimeoutRequiredRule,
|
|
9431
|
+
"elevenlabs-validate-agent-id-format": elevenlabsValidateAgentIdFormatRule,
|
|
9432
|
+
"elevenlabs-secure-session-id-generation": elevenlabsSecureSessionIdGenerationRule,
|
|
9433
|
+
"elevenlabs-conversation-error-recovery": elevenlabsConversationErrorRecoveryRule,
|
|
9434
|
+
"elevenlabs-api-version-pinning": elevenlabsApiVersionPinningRule,
|
|
9435
|
+
"elevenlabs-check-http-status-before-json": elevenlabsCheckHttpStatusBeforeJsonRule,
|
|
9436
|
+
"elevenlabs-conversation-cleanup-on-error": elevenlabsConversationCleanupOnErrorRule,
|
|
9437
|
+
"elevenlabs-env-var-validation": elevenlabsEnvVarValidationRule,
|
|
9438
|
+
"twilio-validate-webhook-signature": twilioValidateWebhookSignatureRule,
|
|
9439
|
+
"twilio-taskrouter-attributes-match-consumer": twilioTaskrouterAttributesMatchConsumerRule,
|
|
9440
|
+
"twilio-enqueue-task-json-stringify": twilioEnqueueTaskJsonStringifyRule,
|
|
9441
|
+
"twilio-media-streams-key-by-call-sid": twilioMediaStreamsKeyByCallSidRule,
|
|
9442
|
+
"twilio-await-or-catch-rest-calls-in-event-handlers": twilioAwaitOrCatchRestCallsInEventHandlersRule,
|
|
9443
|
+
"twilio-use-twiml-builder-not-string-templates": twilioUseTwimlBuilderNotStringTemplatesRule,
|
|
9444
|
+
"twilio-media-streams-mark-pacing": twilioMediaStreamsMarkPacingRule,
|
|
9445
|
+
"twilio-validate-all-request-inputs": twilioValidateAllRequestInputsRule,
|
|
9446
|
+
"twilio-media-streams-mark-name-string": twilioMediaStreamsMarkNameStringRule,
|
|
9447
|
+
"openai-realtime-migrate-beta-to-ga": openaiRealtimeMigrateBetaToGaRule,
|
|
9448
|
+
"openai-realtime-no-log-raw-message-payloads": openaiRealtimeNoLogRawMessagePayloadsRule,
|
|
9449
|
+
"openai-realtime-handle-error-server-event": openaiRealtimeHandleErrorServerEventRule,
|
|
9450
|
+
"openai-realtime-reconnect-on-drop": openaiRealtimeReconnectOnDropRule,
|
|
9451
|
+
"openai-realtime-avoid-dated-preview-snapshots": openaiRealtimeAvoidDatedPreviewSnapshotsRule,
|
|
9452
|
+
"openai-realtime-verify-deprecated-session-fields": openaiRealtimeVerifyDeprecatedSessionFieldsRule,
|
|
9453
|
+
"openai-realtime-buffer-audio-until-session-ready": openaiRealtimeBufferAudioUntilSessionReadyRule,
|
|
9454
|
+
"openai-realtime-send-safety-identifier": openaiRealtimeSendSafetyIdentifierRule,
|
|
9455
|
+
"openai-realtime-transcription-model-choice": openaiRealtimeTranscriptionModelChoiceRule
|
|
9456
|
+
}
|
|
9457
|
+
};
|
|
9458
|
+
|
|
9459
|
+
// src/plugin/rule-registry.ts
|
|
9460
|
+
function buildRegistry() {
|
|
9461
|
+
const registry2 = /* @__PURE__ */ new Map();
|
|
9462
|
+
for (const [key, rule111] of Object.entries(plugin.rules)) {
|
|
9463
|
+
const docs = rule111?.meta?.docs ?? {};
|
|
9464
|
+
registry2.set(key, {
|
|
9465
|
+
category: docs.category,
|
|
9466
|
+
description: docs.description ?? "",
|
|
9467
|
+
rationale: docs.rationale ?? "",
|
|
9468
|
+
docsUrl: docs.docsUrl,
|
|
9469
|
+
cwe: docs.cwe,
|
|
9470
|
+
owasp: docs.owasp
|
|
9471
|
+
});
|
|
9472
|
+
}
|
|
9473
|
+
return registry2;
|
|
9474
|
+
}
|
|
9475
|
+
var registry = buildRegistry();
|
|
9476
|
+
function getRuleDocsMeta(ruleKey) {
|
|
9477
|
+
return registry.get(ruleKey);
|
|
9478
|
+
}
|
|
9479
|
+
|
|
9480
|
+
// src/reporter/snippet.ts
|
|
9481
|
+
function extractCodeSnippet(content, line, contextLines = 2) {
|
|
9482
|
+
const allLines = content.split(/\r?\n/);
|
|
9483
|
+
const highlighted = Math.min(Math.max(line, 1), allLines.length || 1);
|
|
9484
|
+
const start = Math.max(1, highlighted - contextLines);
|
|
9485
|
+
const end = Math.min(allLines.length, highlighted + contextLines);
|
|
9486
|
+
const lines = [];
|
|
9487
|
+
for (let n = start; n <= end; n++) {
|
|
9488
|
+
lines.push({ number: n, text: allLines[n - 1] ?? "" });
|
|
9489
|
+
}
|
|
9490
|
+
return { lines, highlightedLine: highlighted };
|
|
9491
|
+
}
|
|
9492
|
+
|
|
9493
|
+
// src/types.ts
|
|
9494
|
+
var SEVERITY_ORDER = {
|
|
9495
|
+
error: 0,
|
|
9496
|
+
warning: 1,
|
|
9497
|
+
info: 2
|
|
9498
|
+
};
|
|
9499
|
+
function scoreToSeverityLabel(score) {
|
|
9500
|
+
if (score >= 80) return "excellent";
|
|
9501
|
+
if (score >= 60) return "good";
|
|
9502
|
+
if (score >= 40) return "needs-work";
|
|
9503
|
+
return "critical";
|
|
9504
|
+
}
|
|
9505
|
+
|
|
9506
|
+
// src/reporter/report-builder.ts
|
|
9507
|
+
function computeScore(errors, warnings) {
|
|
9508
|
+
return Math.max(0, 100 - errors * 15 - warnings * 5);
|
|
7143
9509
|
}
|
|
7144
9510
|
function buildSummary(results) {
|
|
7145
9511
|
const errors = results.filter((r) => r.severity === "error").length;
|
|
@@ -7264,7 +9630,17 @@ async function detectProviders(directory, filesContent) {
|
|
|
7264
9630
|
continue;
|
|
7265
9631
|
}
|
|
7266
9632
|
const urls = provider.detect.urlPatterns ?? [];
|
|
7267
|
-
|
|
9633
|
+
const matchedUrl = urls.find((u) => {
|
|
9634
|
+
if (!allSources.includes(u)) return false;
|
|
9635
|
+
const isShadowedByMoreSpecificProvider = providers.some((other) => {
|
|
9636
|
+
if (other === provider) return false;
|
|
9637
|
+
return (other.detect.urlPatterns ?? []).some(
|
|
9638
|
+
(otherUrl) => otherUrl !== u && otherUrl.includes(u) && allSources.includes(otherUrl)
|
|
9639
|
+
);
|
|
9640
|
+
});
|
|
9641
|
+
return !isShadowedByMoreSpecificProvider;
|
|
9642
|
+
});
|
|
9643
|
+
if (matchedUrl) {
|
|
7268
9644
|
detected.set(provider.name, {
|
|
7269
9645
|
name: provider.name,
|
|
7270
9646
|
source: "url-patterns",
|
|
@@ -7319,9 +9695,9 @@ function buildOxlintConfig(detectedNames) {
|
|
|
7319
9695
|
const ruleMetaByKey = /* @__PURE__ */ new Map();
|
|
7320
9696
|
for (const provider of providers) {
|
|
7321
9697
|
if (!detectedNames.has(provider.name)) continue;
|
|
7322
|
-
for (const
|
|
7323
|
-
oxlintRules[`${PLUGIN_NAME}/${
|
|
7324
|
-
ruleMetaByKey.set(
|
|
9698
|
+
for (const rule111 of provider.oxlintRules) {
|
|
9699
|
+
oxlintRules[`${PLUGIN_NAME}/${rule111.key}`] = rule111.severity === "error" || rule111.severity === void 0 ? "error" : "warn";
|
|
9700
|
+
ruleMetaByKey.set(rule111.key, rule111);
|
|
7325
9701
|
}
|
|
7326
9702
|
}
|
|
7327
9703
|
return { oxlintRules, ruleMetaByKey };
|
|
@@ -7439,9 +9815,9 @@ var import_node_path5 = require("path");
|
|
|
7439
9815
|
function rationaleByRule() {
|
|
7440
9816
|
const map = /* @__PURE__ */ new Map();
|
|
7441
9817
|
for (const provider of providers) {
|
|
7442
|
-
for (const
|
|
7443
|
-
const docs = getRuleDocsMeta(
|
|
7444
|
-
if (docs?.rationale) map.set(
|
|
9818
|
+
for (const rule111 of provider.oxlintRules) {
|
|
9819
|
+
const docs = getRuleDocsMeta(rule111.key);
|
|
9820
|
+
if (docs?.rationale) map.set(rule111.resultRule, docs.rationale);
|
|
7445
9821
|
}
|
|
7446
9822
|
}
|
|
7447
9823
|
return map;
|