@jskit-ai/connectors-catalog 0.1.1

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.
Files changed (399) hide show
  1. package/README.md +192 -0
  2. package/docs/ai-upstream-notices.md +51 -0
  3. package/docs/ai.md +191 -0
  4. package/docs/airtable.md +130 -0
  5. package/docs/algolia.md +190 -0
  6. package/docs/amazon-redshift.md +229 -0
  7. package/docs/amplitude.md +210 -0
  8. package/docs/apify.md +162 -0
  9. package/docs/apollo-io.md +200 -0
  10. package/docs/asana.md +126 -0
  11. package/docs/ashby.md +188 -0
  12. package/docs/atlassian.md +224 -0
  13. package/docs/attention.md +162 -0
  14. package/docs/aws-athena.md +150 -0
  15. package/docs/aws-credentials.md +75 -0
  16. package/docs/aws-s3.md +193 -0
  17. package/docs/bigquery.md +146 -0
  18. package/docs/brevo.md +122 -0
  19. package/docs/calendly.md +158 -0
  20. package/docs/canva.md +191 -0
  21. package/docs/chargebee.md +192 -0
  22. package/docs/clay.md +194 -0
  23. package/docs/clickhouse.md +242 -0
  24. package/docs/confidence-exp.md +74 -0
  25. package/docs/confidence-flags.md +75 -0
  26. package/docs/confidence-oauth.md +137 -0
  27. package/docs/contentful.md +123 -0
  28. package/docs/databricks.md +180 -0
  29. package/docs/dbt-semantic-layer.md +175 -0
  30. package/docs/elevenlabs.md +102 -0
  31. package/docs/figma.md +132 -0
  32. package/docs/firebase-cloud-messaging.md +262 -0
  33. package/docs/firecrawl.md +137 -0
  34. package/docs/fireflies.md +95 -0
  35. package/docs/fireworks-ai.md +53 -0
  36. package/docs/gatewayapi.md +113 -0
  37. package/docs/gemini-enterprise.md +224 -0
  38. package/docs/github-api.md +133 -0
  39. package/docs/gitlab-api.md +138 -0
  40. package/docs/gmail.md +133 -0
  41. package/docs/gong.md +205 -0
  42. package/docs/google-ads.md +156 -0
  43. package/docs/google-analytics.md +202 -0
  44. package/docs/google-docs.md +105 -0
  45. package/docs/google-drive.md +108 -0
  46. package/docs/google-maps-platform.md +208 -0
  47. package/docs/google-oauth.md +123 -0
  48. package/docs/google-search-console.md +124 -0
  49. package/docs/google-sheets.md +94 -0
  50. package/docs/google-slides.md +95 -0
  51. package/docs/granola.md +178 -0
  52. package/docs/hex.md +161 -0
  53. package/docs/heygen.md +124 -0
  54. package/docs/hubspot.md +142 -0
  55. package/docs/incident-io.md +118 -0
  56. package/docs/inngest.md +178 -0
  57. package/docs/klipy.md +149 -0
  58. package/docs/lexware.md +144 -0
  59. package/docs/lightspeed.md +193 -0
  60. package/docs/linear.md +137 -0
  61. package/docs/linkedin.md +196 -0
  62. package/docs/logo-dev.md +152 -0
  63. package/docs/mailgun.md +153 -0
  64. package/docs/mapbox.md +174 -0
  65. package/docs/microsoft-excel.md +97 -0
  66. package/docs/microsoft-fabric.md +249 -0
  67. package/docs/microsoft-oauth.md +143 -0
  68. package/docs/microsoft-onedrive.md +76 -0
  69. package/docs/microsoft-onenote.md +75 -0
  70. package/docs/microsoft-outlook.md +90 -0
  71. package/docs/microsoft-powerpoint.md +119 -0
  72. package/docs/microsoft-sharepoint.md +75 -0
  73. package/docs/microsoft-teams.md +72 -0
  74. package/docs/microsoft-word.md +119 -0
  75. package/docs/miro.md +117 -0
  76. package/docs/n8n.md +169 -0
  77. package/docs/notion.md +137 -0
  78. package/docs/oura.md +161 -0
  79. package/docs/paddle.md +168 -0
  80. package/docs/perplexity.md +85 -0
  81. package/docs/pipedrive.md +120 -0
  82. package/docs/polar.md +63 -0
  83. package/docs/posthog.md +227 -0
  84. package/docs/prestashop.md +151 -0
  85. package/docs/replicate.md +60 -0
  86. package/docs/resend.md +153 -0
  87. package/docs/salesforce.md +234 -0
  88. package/docs/sanity.md +122 -0
  89. package/docs/semrush.md +276 -0
  90. package/docs/sentry.md +112 -0
  91. package/docs/sevdesk.md +107 -0
  92. package/docs/shopify.md +206 -0
  93. package/docs/slack.md +257 -0
  94. package/docs/snowflake.md +271 -0
  95. package/docs/storyblok.md +130 -0
  96. package/docs/stripe.md +77 -0
  97. package/docs/supabase.md +51 -0
  98. package/docs/tally.md +105 -0
  99. package/docs/telegram.md +165 -0
  100. package/docs/tiktok.md +201 -0
  101. package/docs/twilio.md +284 -0
  102. package/docs/twitch.md +428 -0
  103. package/docs/wave.md +440 -0
  104. package/docs/wix.md +1249 -0
  105. package/docs/wiz.md +177 -0
  106. package/docs/woocommerce.md +400 -0
  107. package/docs/wordpress-com.md +358 -0
  108. package/docs/wordpress-self-hosted.md +252 -0
  109. package/docs/workday.md +292 -0
  110. package/docs/x-twitter.md +188 -0
  111. package/docs/xero.md +429 -0
  112. package/docs/zoho-books.md +214 -0
  113. package/docs/zoho-crm.md +192 -0
  114. package/package.json +153 -0
  115. package/patterns/ai-connections/PATTERN.md +84 -0
  116. package/patterns/ai-connections/example/ai-model.js +11 -0
  117. package/patterns/ai-connections/example/perplexity-answer.js +29 -0
  118. package/patterns/api-key-connection/PATTERN.md +368 -0
  119. package/patterns/api-key-connection/example/connections.js +18 -0
  120. package/patterns/api-key-connection/example/integrations.json +19 -0
  121. package/patterns/assistant-mcp/PATTERN.md +110 -0
  122. package/patterns/assistant-mcp/example/integrations.json +13 -0
  123. package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
  124. package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
  125. package/patterns/aws-storage-queries/PATTERN.md +169 -0
  126. package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
  127. package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
  128. package/patterns/aws-storage-queries/example/formats/package.json +12 -0
  129. package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
  130. package/patterns/aws-storage-queries/example/integrations.json +38 -0
  131. package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
  132. package/patterns/event-delivery/PATTERN.md +151 -0
  133. package/patterns/event-delivery/example/integrations.json +17 -0
  134. package/patterns/firebase-messaging/PATTERN.md +156 -0
  135. package/patterns/firebase-messaging/example/server/notifications.js +23 -0
  136. package/patterns/google-ads-search/PATTERN.md +92 -0
  137. package/patterns/google-ads-search/example/ads-setup.js +25 -0
  138. package/patterns/oauth-connection/PATTERN.md +724 -0
  139. package/patterns/oauth-connection/example/integrations.json +20 -0
  140. package/patterns/paddle-catalogue/PATTERN.md +78 -0
  141. package/patterns/paddle-catalogue/example/create-products.js +38 -0
  142. package/patterns/public-image/PATTERN.md +84 -0
  143. package/patterns/public-image/example/integrations.json +13 -0
  144. package/patterns/public-image/example/logo-url.js +16 -0
  145. package/patterns/redshift-queries/PATTERN.md +150 -0
  146. package/patterns/redshift-queries/example/integrations.json +33 -0
  147. package/patterns/source-scanning/PATTERN.md +83 -0
  148. package/patterns/source-scanning/example/source-scanner.js +13 -0
  149. package/src/client/firebase-cloud-messaging.js +10 -0
  150. package/src/client/google-analytics.js +72 -0
  151. package/src/client/google-maps-platform.js +39 -0
  152. package/src/client/logo-dev.js +36 -0
  153. package/src/server/ai.js +46 -0
  154. package/src/server/airtable.js +67 -0
  155. package/src/server/algolia.js +63 -0
  156. package/src/server/amazon-redshift.js +96 -0
  157. package/src/server/amplitude.js +31 -0
  158. package/src/server/apify.js +68 -0
  159. package/src/server/apollo-io.js +85 -0
  160. package/src/server/asana.js +64 -0
  161. package/src/server/ashby.js +61 -0
  162. package/src/server/atlassian.js +32 -0
  163. package/src/server/attention.js +93 -0
  164. package/src/server/aws-athena.js +82 -0
  165. package/src/server/aws-s3.js +49 -0
  166. package/src/server/awsTransport.js +75 -0
  167. package/src/server/bigquery.js +97 -0
  168. package/src/server/brevo.js +97 -0
  169. package/src/server/calendly.js +77 -0
  170. package/src/server/canva.js +13 -0
  171. package/src/server/chargebee.js +88 -0
  172. package/src/server/clay.js +78 -0
  173. package/src/server/clickhouse.js +66 -0
  174. package/src/server/confidence.js +29 -0
  175. package/src/server/contentful.js +40 -0
  176. package/src/server/databricks.js +55 -0
  177. package/src/server/dbt-semantic-layer.js +95 -0
  178. package/src/server/elevenlabs.js +81 -0
  179. package/src/server/figma.js +24 -0
  180. package/src/server/firebase-cloud-messaging.js +111 -0
  181. package/src/server/firecrawl.js +70 -0
  182. package/src/server/fireflies.js +31 -0
  183. package/src/server/fireworks-ai.js +16 -0
  184. package/src/server/gatewayapi.js +48 -0
  185. package/src/server/gemini-enterprise.js +47 -0
  186. package/src/server/github-api.js +90 -0
  187. package/src/server/gitlab-api.js +67 -0
  188. package/src/server/gmail.js +60 -0
  189. package/src/server/gong.js +52 -0
  190. package/src/server/google-ads-search.js +134 -0
  191. package/src/server/google-ads.js +80 -0
  192. package/src/server/google-docs.js +36 -0
  193. package/src/server/google-drive.js +71 -0
  194. package/src/server/google-maps-platform.js +90 -0
  195. package/src/server/google-search-console.js +31 -0
  196. package/src/server/google-sheets.js +60 -0
  197. package/src/server/google-slides.js +36 -0
  198. package/src/server/google.js +61 -0
  199. package/src/server/granola.js +115 -0
  200. package/src/server/graphqlOperation.js +15 -0
  201. package/src/server/hex.js +29 -0
  202. package/src/server/heygen.js +54 -0
  203. package/src/server/hubspot.js +85 -0
  204. package/src/server/incident-io.js +68 -0
  205. package/src/server/inngest.js +86 -0
  206. package/src/server/jsonOperation.js +34 -0
  207. package/src/server/klipy.js +34 -0
  208. package/src/server/lexware.js +59 -0
  209. package/src/server/lightspeed.js +82 -0
  210. package/src/server/linear.js +52 -0
  211. package/src/server/linkedin.js +74 -0
  212. package/src/server/mailgun.js +76 -0
  213. package/src/server/mapbox.js +46 -0
  214. package/src/server/mcpRegistration.js +41 -0
  215. package/src/server/mcpTransport.js +60 -0
  216. package/src/server/microsoft-excel.js +67 -0
  217. package/src/server/microsoft-fabric.js +81 -0
  218. package/src/server/microsoft-onedrive.js +58 -0
  219. package/src/server/microsoft-onenote.js +52 -0
  220. package/src/server/microsoft-outlook.js +58 -0
  221. package/src/server/microsoft-powerpoint.js +5 -0
  222. package/src/server/microsoft-sharepoint.js +46 -0
  223. package/src/server/microsoft-teams.js +29 -0
  224. package/src/server/microsoft-word.js +5 -0
  225. package/src/server/microsoft.js +88 -0
  226. package/src/server/miro.js +29 -0
  227. package/src/server/n8n.js +78 -0
  228. package/src/server/notion.js +76 -0
  229. package/src/server/oura.js +48 -0
  230. package/src/server/paddle.js +83 -0
  231. package/src/server/perplexity.js +15 -0
  232. package/src/server/pipedrive.js +95 -0
  233. package/src/server/polar.js +20 -0
  234. package/src/server/posthog.js +44 -0
  235. package/src/server/prestashop.js +42 -0
  236. package/src/server/replicate.js +42 -0
  237. package/src/server/resend.js +93 -0
  238. package/src/server/salesforce.js +84 -0
  239. package/src/server/sanity.js +31 -0
  240. package/src/server/semrush.js +359 -0
  241. package/src/server/sentry.js +30 -0
  242. package/src/server/sevdesk.js +20 -0
  243. package/src/server/shopify.js +99 -0
  244. package/src/server/slack.js +143 -0
  245. package/src/server/snowflake.js +139 -0
  246. package/src/server/storyblok.js +49 -0
  247. package/src/server/stripe.js +13 -0
  248. package/src/server/supabase.js +13 -0
  249. package/src/server/tally.js +78 -0
  250. package/src/server/telegram.js +55 -0
  251. package/src/server/tiktok.js +86 -0
  252. package/src/server/twilio.js +90 -0
  253. package/src/server/twitch.js +409 -0
  254. package/src/server/wave.js +520 -0
  255. package/src/server/wix.js +1048 -0
  256. package/src/server/wiz.js +117 -0
  257. package/src/server/woocommerce.js +412 -0
  258. package/src/server/wordpress-com.js +236 -0
  259. package/src/server/wordpress-self-hosted.js +134 -0
  260. package/src/server/workday.js +136 -0
  261. package/src/server/x-twitter.js +110 -0
  262. package/src/server/xero.js +397 -0
  263. package/src/server/zoho-books.js +90 -0
  264. package/src/server/zoho-crm.js +89 -0
  265. package/src/server/zohoOAuth.js +21 -0
  266. package/src/shared/ai-models.json +8049 -0
  267. package/src/shared/ai.js +63 -0
  268. package/src/shared/algolia.js +29 -0
  269. package/src/shared/amazon-redshift.js +51 -0
  270. package/src/shared/amplitude.js +28 -0
  271. package/src/shared/atlassian.js +43 -0
  272. package/src/shared/aws.js +72 -0
  273. package/src/shared/canva.js +66 -0
  274. package/src/shared/chargebee.js +26 -0
  275. package/src/shared/clickhouse.js +38 -0
  276. package/src/shared/confidence.js +28 -0
  277. package/src/shared/databricks.js +33 -0
  278. package/src/shared/dbt-semantic-layer.js +32 -0
  279. package/src/shared/definitions.js +140 -0
  280. package/src/shared/figma.js +21 -0
  281. package/src/shared/firebase-cloud-messaging.js +45 -0
  282. package/src/shared/gemini-enterprise.js +33 -0
  283. package/src/shared/gong.js +33 -0
  284. package/src/shared/google-ads-search.js +31 -0
  285. package/src/shared/google-ads.js +28 -0
  286. package/src/shared/google-maps-platform.js +28 -0
  287. package/src/shared/google.js +133 -0
  288. package/src/shared/granola.js +25 -0
  289. package/src/shared/hex.js +28 -0
  290. package/src/shared/inngest.js +35 -0
  291. package/src/shared/lightspeed.js +75 -0
  292. package/src/shared/linkedin.js +27 -0
  293. package/src/shared/logo-dev.js +21 -0
  294. package/src/shared/mapbox.js +38 -0
  295. package/src/shared/mcp.js +92 -0
  296. package/src/shared/microsoft-fabric.js +47 -0
  297. package/src/shared/microsoft.js +231 -0
  298. package/src/shared/miro.js +27 -0
  299. package/src/shared/oura.js +23 -0
  300. package/src/shared/posthog.js +33 -0
  301. package/src/shared/prestashop.js +23 -0
  302. package/src/shared/salesforce.js +36 -0
  303. package/src/shared/semrush.js +24 -0
  304. package/src/shared/shopify.js +51 -0
  305. package/src/shared/siteUrl.js +12 -0
  306. package/src/shared/slack.js +88 -0
  307. package/src/shared/snowflake.js +55 -0
  308. package/src/shared/tiktok.js +22 -0
  309. package/src/shared/tokens.js +747 -0
  310. package/src/shared/twilio.js +41 -0
  311. package/src/shared/twitch.js +49 -0
  312. package/src/shared/wave.js +52 -0
  313. package/src/shared/wix.js +40 -0
  314. package/src/shared/wiz.js +41 -0
  315. package/src/shared/wordpress-com.js +30 -0
  316. package/src/shared/wordpress.js +69 -0
  317. package/src/shared/workday.js +50 -0
  318. package/src/shared/x-twitter.js +19 -0
  319. package/src/shared/xero.js +46 -0
  320. package/src/shared/zoho-books.js +39 -0
  321. package/src/shared/zoho-crm.js +43 -0
  322. package/src/shared/zoho-regions.js +12 -0
  323. package/test/ai.test.js +136 -0
  324. package/test/airtable.test.js +125 -0
  325. package/test/algolia.test.js +207 -0
  326. package/test/amazon-redshift.test.js +272 -0
  327. package/test/amplitude.test.js +239 -0
  328. package/test/apify.test.js +93 -0
  329. package/test/apollo-io.test.js +125 -0
  330. package/test/asana.test.js +99 -0
  331. package/test/ashby.test.js +97 -0
  332. package/test/atlassian.test.js +249 -0
  333. package/test/attention.test.js +79 -0
  334. package/test/aws.test.js +405 -0
  335. package/test/brevo.test.js +71 -0
  336. package/test/calendly.test.js +136 -0
  337. package/test/canva.test.js +258 -0
  338. package/test/chargebee.test.js +195 -0
  339. package/test/clickhouse.test.js +278 -0
  340. package/test/confidence.test.js +205 -0
  341. package/test/databricks.test.js +253 -0
  342. package/test/dbt-semantic-layer.test.js +221 -0
  343. package/test/designMcp.test.js +336 -0
  344. package/test/elevenlabs.test.js +52 -0
  345. package/test/firebase-cloud-messaging.test.js +242 -0
  346. package/test/firecrawl.test.js +66 -0
  347. package/test/gemini-enterprise.test.js +218 -0
  348. package/test/github-api.test.js +167 -0
  349. package/test/gitlab-api.test.js +160 -0
  350. package/test/gong.test.js +192 -0
  351. package/test/google-ads.test.js +350 -0
  352. package/test/google-maps-platform.test.js +191 -0
  353. package/test/google.test.js +710 -0
  354. package/test/granola.test.js +240 -0
  355. package/test/hex.test.js +253 -0
  356. package/test/heygen.test.js +121 -0
  357. package/test/hubspot.test.js +103 -0
  358. package/test/inngest.test.js +219 -0
  359. package/test/lightspeed.test.js +250 -0
  360. package/test/linear.test.js +146 -0
  361. package/test/linkedin.test.js +243 -0
  362. package/test/logo-dev.test.js +79 -0
  363. package/test/mailgun.test.js +107 -0
  364. package/test/mapbox.test.js +181 -0
  365. package/test/mcp.test.js +227 -0
  366. package/test/microsoft-fabric.test.js +258 -0
  367. package/test/microsoft.test.js +554 -0
  368. package/test/n8nOAuth.test.js +188 -0
  369. package/test/notionOAuth.test.js +183 -0
  370. package/test/oura.test.js +211 -0
  371. package/test/paddleCatalogue.test.js +51 -0
  372. package/test/paddlePermissions.test.js +101 -0
  373. package/test/perplexityComposition.test.js +39 -0
  374. package/test/pipedriveOAuth.test.js +140 -0
  375. package/test/posthog.test.js +200 -0
  376. package/test/prestashop.test.js +207 -0
  377. package/test/resend.test.js +163 -0
  378. package/test/salesforce.test.js +276 -0
  379. package/test/semrush.test.js +479 -0
  380. package/test/sentry.test.js +35 -0
  381. package/test/shopify.test.js +247 -0
  382. package/test/slack.test.js +320 -0
  383. package/test/snowflake.test.js +391 -0
  384. package/test/tally.test.js +75 -0
  385. package/test/telegram.test.js +58 -0
  386. package/test/tiktok.test.js +243 -0
  387. package/test/tokens.test.js +1079 -0
  388. package/test/twilio.test.js +221 -0
  389. package/test/twitch.test.js +573 -0
  390. package/test/wave.test.js +678 -0
  391. package/test/wix.test.js +1099 -0
  392. package/test/wiz.test.js +185 -0
  393. package/test/wordpress-com.test.js +469 -0
  394. package/test/wordpress.test.js +660 -0
  395. package/test/workday.test.js +314 -0
  396. package/test/x-twitter.test.js +256 -0
  397. package/test/xero.test.js +700 -0
  398. package/test/zoho-books.test.js +253 -0
  399. package/test/zoho-crm.test.js +223 -0
@@ -0,0 +1,90 @@
1
+ import { twilioDefinition } from "../shared/twilio.js";
2
+ import { jsonOperation } from "./jsonOperation.js";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { createSchema } from "json-rest-schema";
5
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
6
+ import { createHmac, timingSafeEqual } from "node:crypto";
7
+
8
+ const origins = {
9
+ us1: "https://api.twilio.com", ie1: "https://api.dublin.ie1.twilio.com", au1: "https://api.sydney.au1.twilio.com"
10
+ };
11
+ const phone = { type: "string", required: true, pattern: "^\\+[1-9][0-9]{1,14}$" };
12
+ const sid = prefix => ({ type: "string", required: true, pattern: `^${prefix}[0-9a-fA-F]{32}$` });
13
+ const callbackUrl = { type: "string", maxLength: 2048, validator: value => {
14
+ try { const url = new URL(value); return url.protocol === "https:" && !url.username && !url.password && !url.hash || "Use an HTTPS URL without credentials or a fragment."; }
15
+ catch { return "Use an absolute HTTPS URL."; }
16
+ } };
17
+ function resourceOperation(resource, method, fields, checkInput) {
18
+ const schema = createSchema(fields);
19
+ return {
20
+ scopes: [],
21
+ request(input, settings) {
22
+ const { resourceSid, ...values } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
23
+ checkInput?.(values, settings);
24
+ const url = `${origins[settings.region]}/2010-04-01/Accounts/${settings.accountSid}/${resource}${resourceSid ? `/${resourceSid}` : ""}.json`;
25
+ if (method === "GET") return { method, url };
26
+ return { method, url, headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(values).toString() };
27
+ },
28
+ validateResult: result => new RegExp(`^${resource === "Calls" ? "CA" : "SM"}[0-9a-fA-F]{32}$`, "u").test(result?.sid) &&
29
+ /^AC[0-9a-fA-F]{32}$/u.test(result?.account_sid) && typeof result.status === "string"
30
+ };
31
+ }
32
+ const twilioProvider = Object.freeze({
33
+ ...twilioDefinition,
34
+ apiOrigins: (settings) => [origins[settings.region]],
35
+ apiKey: { headers: (secret, settings) => ({ Authorization: `Basic ${Buffer.from(`${settings.apiKeySid}:${secret}`).toString("base64")}` }) },
36
+ checkOperation: "calls.list",
37
+ operations: {
38
+ "messages.send": resourceOperation("Messages", "POST", {
39
+ To: phone, From: { ...phone, required: false }, MessagingServiceSid: { ...sid("MG"), required: false },
40
+ Body: { type: "string", required: true, noTrim: true, minLength: 1, maxLength: 1600 }, StatusCallback: callbackUrl
41
+ }, values => {
42
+ if (Boolean(values.From) === Boolean(values.MessagingServiceSid)) throw new ConnectorError("connector_input_invalid", "Choose one sender number or Messaging Service SID.", { statusCode: 422 });
43
+ }),
44
+ "messages.get": resourceOperation("Messages", "GET", { resourceSid: sid("SM") }),
45
+ "calls.create": resourceOperation("Calls", "POST", {
46
+ To: phone, From: phone, Url: callbackUrl,
47
+ Twiml: { type: "string", noTrim: true, minLength: 1, maxLength: 4000 },
48
+ StatusCallback: callbackUrl, Timeout: { type: "integer", min: 5, max: 600, defaultTo: 60 }
49
+ }, values => {
50
+ if (Boolean(values.Url) === Boolean(values.Twiml)) throw new ConnectorError("connector_input_invalid", "Supply either a call-control URL or inline TwiML.", { statusCode: 422 });
51
+ }),
52
+ "calls.get": resourceOperation("Calls", "GET", { resourceSid: sid("CA") }),
53
+ "calls.list": jsonOperation((settings) => `${origins[settings.region]}/2010-04-01/Accounts/${settings.accountSid}/Calls.json`, {
54
+ PageSize: { type: "integer", min: 1, max: 1000, defaultTo: 50 },
55
+ Page: { type: "integer", min: 0, max: 2147483647, defaultTo: 0 },
56
+ PageToken: { type: "string", minLength: 1, maxLength: 4096 },
57
+ To: { type: "string", minLength: 1, maxLength: 256 },
58
+ From: { type: "string", minLength: 1, maxLength: 256 },
59
+ Status: { type: "string", enum: ["queued", "ringing", "in-progress", "canceled", "completed", "failed", "busy", "no-answer"] }
60
+ }, (result) => Array.isArray(result?.calls) && Number.isInteger(result.page) && result.page >= 0 &&
61
+ Number.isInteger(result.page_size) && result.page_size > 0 &&
62
+ (result.next_page_uri === null || typeof result.next_page_uri === "string"))
63
+ }
64
+ });
65
+ // POST form callbacks only. The caller supplies the exact public URL configured
66
+ // at Twilio, never a URL reconstructed from untrusted forwarded headers.
67
+ function verifyTwilioRequest({ rawBody, signature, authToken, url, accountSid, contentType }) {
68
+ const reject = () => { throw new ConnectorError("connector_webhook_invalid", "Invalid Twilio callback.", { statusCode: 401 }); };
69
+ if (!(rawBody instanceof Uint8Array) || rawBody.byteLength > 1048576 ||
70
+ typeof signature !== "string" || !/^[A-Za-z0-9+/]{27}=$/u.test(signature) ||
71
+ typeof authToken !== "string" || !authToken ||
72
+ typeof accountSid !== "string" || !/^AC[0-9a-fA-F]{32}$/u.test(accountSid) ||
73
+ typeof url !== "string" || url.length > 4096 ||
74
+ typeof contentType !== "string" || contentType.split(";", 1)[0].trim().toLowerCase() !== "application/x-www-form-urlencoded") reject();
75
+ let form;
76
+ try {
77
+ const parsedUrl = new URL(url);
78
+ if (parsedUrl.protocol !== "https:" || parsedUrl.username || parsedUrl.password || parsedUrl.hash) reject();
79
+ form = new URLSearchParams(new TextDecoder("utf-8", { fatal: true }).decode(rawBody));
80
+ } catch { reject(); }
81
+ const names = [...form.keys()];
82
+ // Repeated fields are ambiguous to framework parsers; never silently choose one.
83
+ if (names.length > 256 || new Set(names).size !== names.length) reject();
84
+ const signed = url + names.sort().map(name => name + form.get(name)).join("");
85
+ const expected = createHmac("sha1", authToken).update(signed).digest();
86
+ if (!timingSafeEqual(expected, Buffer.from(signature, "base64"))) reject();
87
+ if (form.get("AccountSid") !== accountSid) reject();
88
+ return Object.fromEntries(form);
89
+ }
90
+ export { twilioProvider, verifyTwilioRequest };
@@ -0,0 +1,409 @@
1
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
2
+ import { twitchDefinition } from "../shared/twitch.js";
3
+ import { jsonOperation } from "./jsonOperation.js";
4
+ import { createSchema } from "json-rest-schema";
5
+
6
+ const validationUrl = "https://id.twitch.tv/oauth2/validate";
7
+ const userId = (value) => typeof value === "string" && /^[1-9]\d*$/u.test(value);
8
+ const scopeList = (value) => Array.isArray(value) && value.every((scope) => typeof scope === "string" && /^[^\s]+$/u.test(scope));
9
+ const validToken = (value) => typeof value?.client_id === "string" && value.client_id.length > 0 &&
10
+ userId(value.user_id) && typeof value.login === "string" && value.login.length > 0 &&
11
+ scopeList(value.scopes) && Number.isSafeInteger(value.expires_in) && value.expires_in > 0;
12
+ const idField = { type: "string", validator: value => userId(value) || "Enter a numeric Twitch ID." };
13
+ const pageFields = { first: { type: "integer", min: 1, max: 100, defaultTo: 20 }, after: { type: "string", minLength: 1, maxLength: 4096 } };
14
+ const opaqueId = { type: "string", required: true, minLength: 1, maxLength: 100 };
15
+ const eventTypes = {
16
+ "stream.online": ["1", []], "stream.offline": ["1", []], "channel.update": ["2", []],
17
+ "channel.subscribe": ["1", ["channel:read:subscriptions"]],
18
+ "channel.cheer": ["1", ["bits:read"]],
19
+ ...Object.fromEntries(["begin", "progress", "end"].map(phase => [`channel.poll.${phase}`, ["1", ["channel:read:polls", "channel:manage:polls"]]])),
20
+ ...Object.fromEntries(["begin", "progress", "lock", "end"].map(phase => [`channel.prediction.${phase}`, ["1", ["channel:read:predictions", "channel:manage:predictions"]]])),
21
+ ...Object.fromEntries(["add", "update"].map(phase => [`channel.channel_points_custom_reward_redemption.${phase}`, ["1", ["channel:read:redemptions", "channel:manage:redemptions"]]]))
22
+ };
23
+ const eventSubscriptions = value => Array.isArray(value?.data) && value.data.every(item => typeof item?.id === "string" &&
24
+ typeof item.type === "string" && typeof item.status === "string" && item.transport?.method === "websocket");
25
+ const choiceList = max => ({ type: "array", required: true,
26
+ items: { type: "object", schema: createSchema({ title: { type: "string", required: true, minLength: 1, maxLength: 25 } }) },
27
+ validator: values => values.length >= 2 && values.length <= max || `Use between two and ${max} choices.` });
28
+ const interactionResult = value => Array.isArray(value?.data) && value.data.every(item =>
29
+ typeof item?.id === "string" && userId(item.broadcaster_id) && typeof item.status === "string");
30
+ function helixOperation(path, fields, scopes, validateResult, method = "GET", checkInput) {
31
+ const operation = jsonOperation(`https://api.twitch.tv/helix/${path}`, fields, validateResult, method === "GET" ? "GET" : "POST");
32
+ return { ...operation, scopes, request(input) {
33
+ const request = operation.request(input);
34
+ checkInput?.(request.body);
35
+ return { ...request, method };
36
+ } };
37
+ }
38
+ const page = (value, record) => Array.isArray(value?.data) && value.data.every(record) &&
39
+ value.pagination && typeof value.pagination === "object" && !Array.isArray(value.pagination) &&
40
+ (value.pagination.cursor === undefined || typeof value.pagination.cursor === "string");
41
+ function analyticsOperation(kind, identity) {
42
+ const date = { type: "string", validator: value => /^\d{4}-\d{2}-\d{2}T00:00:00Z$/u.test(value) &&
43
+ Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value.replace("Z", ".000Z") || "Enter a real UTC date at midnight, YYYY-MM-DDT00:00:00Z." };
44
+ const operation = helixOperation(`analytics/${kind}`, {
45
+ ...pageFields, [identity]: { ...opaqueId, required: false },
46
+ type: { type: "string", enum: ["overview_v2"], defaultTo: "overview_v2" },
47
+ started_at: date, ended_at: date
48
+ }, [`analytics:read:${kind}`], value => page(value, item => {
49
+ if (typeof item?.[identity] !== "string" || !item[identity] || item.type !== "overview_v2" ||
50
+ typeof item.date_range?.started_at !== "string" || typeof item.date_range?.ended_at !== "string") return false;
51
+ try { const url = new URL(item.URL); return url.protocol === "https:" && !url.username && !url.password; } catch { return false; }
52
+ }));
53
+ return { ...operation, request(input) {
54
+ const request = operation.request(input);
55
+ const query = new URL(request.url).searchParams;
56
+ const start = query.get("started_at"), end = query.get("ended_at");
57
+ if (Boolean(start) !== Boolean(end) || (start && start > end)) {
58
+ throw new ConnectorError("connector_input_invalid", "Supply both report dates, with the end on or after the start.", { statusCode: 422 });
59
+ }
60
+ return request;
61
+ } };
62
+ }
63
+ const broadcastUpdate = jsonOperation("https://api.twitch.tv/helix/channels", {
64
+ title: { type: "string", noTrim: true, minLength: 1, maxLength: 140 },
65
+ game_id: { type: "string", pattern: "^[0-9]*$", maxLength: 30 },
66
+ broadcaster_language: { type: "string", pattern: "^([a-z]{2}|other)$" },
67
+ tags: { type: "array", items: { type: "string", minLength: 1, maxLength: 25, validator: value => /^[\p{L}\p{N}]+$/u.test(value) || "Use letters and numbers in tags." }, validator: values => values.length <= 10 || "Use at most ten tags." },
68
+ is_branded_content: { type: "boolean" }
69
+ }, value => value === null, "POST");
70
+ const clipCreation = jsonOperation("https://api.twitch.tv/helix/clips", {
71
+ broadcaster_id: { ...idField, required: true }
72
+ }, value => Array.isArray(value?.data) && value.data.length === 1 && typeof value.data[0]?.id === "string" &&
73
+ value.data[0].id.length > 0 && typeof value.data[0].edit_url === "string" &&
74
+ /^https:\/\/(?:www\.twitch\.tv|clips\.twitch\.tv)\//u.test(value.data[0].edit_url));
75
+ const rewardFields = {
76
+ title: { type: "string", minLength: 1, maxLength: 45 }, cost: { type: "integer", min: 1, max: Number.MAX_SAFE_INTEGER },
77
+ prompt: { type: "string", maxLength: 200 }, is_enabled: { type: "boolean" },
78
+ is_user_input_required: { type: "boolean" }, background_color: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
79
+ is_max_per_stream_enabled: { type: "boolean" }, max_per_stream: { type: "integer", min: 1, max: Number.MAX_SAFE_INTEGER },
80
+ is_max_per_user_per_stream_enabled: { type: "boolean" }, max_per_user_per_stream: { type: "integer", min: 1, max: Number.MAX_SAFE_INTEGER },
81
+ is_global_cooldown_enabled: { type: "boolean" }, global_cooldown_seconds: { type: "integer", min: 1, max: 604800 },
82
+ should_redemptions_skip_request_queue: { type: "boolean" }
83
+ };
84
+ const rewardResult = value => Array.isArray(value?.data) && value.data.every(item =>
85
+ typeof item?.id === "string" && userId(item.broadcaster_id) && typeof item.title === "string" && Number.isSafeInteger(item.cost) && item.cost > 0);
86
+ const scheduleTime = { type: "string", maxLength: 40, validator: value =>
87
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u.test(value) && Number.isFinite(Date.parse(value)) &&
88
+ new Date(value).toISOString().slice(0, 19) === value.slice(0, 19) || "Enter a real UTC RFC3339 timestamp." };
89
+ const scheduleFields = {
90
+ start_time: scheduleTime, duration: { type: "integer", min: 30, max: 1380 },
91
+ timezone: { type: "string", maxLength: 100, validator: value => {
92
+ try { new Intl.DateTimeFormat("en", { timeZone: value }); return true; } catch { return "Enter an IANA timezone such as Australia/Perth."; }
93
+ } },
94
+ title: { type: "string", maxLength: 140 }, category_id: idField
95
+ };
96
+ const scheduleResult = value => userId(value?.data?.broadcaster_id) && Array.isArray(value.data.segments) &&
97
+ value.data.segments.every(item => typeof item?.id === "string" && typeof item.start_time === "string" && typeof item.end_time === "string");
98
+ const vacationSettings = jsonOperation("https://api.twitch.tv/helix/schedule/settings", {
99
+ is_vacation_enabled: { type: "boolean", required: true }, vacation_start_time: scheduleTime,
100
+ vacation_end_time: scheduleTime, timezone: scheduleFields.timezone
101
+ }, value => value === null);
102
+ function scheduleWrite(fields, method) {
103
+ const operation = helixOperation("schedule/segment", fields, ["channel:manage:schedule"], method === "DELETE" ? value => value === null : scheduleResult, method);
104
+ return { ...operation, request(input) {
105
+ const request = operation.request(input);
106
+ const url = new URL(request.url);
107
+ if (request.body.id) { url.searchParams.set("id", request.body.id); delete request.body.id; }
108
+ if (method === "PATCH" && !Object.keys(request.body).length) throw new ConnectorError("connector_input_invalid", "Supply at least one schedule change.", { statusCode: 422 });
109
+ if (request.body.duration !== undefined) request.body.duration = String(request.body.duration);
110
+ return { ...request, url: url.href, body: method === "DELETE" ? undefined : request.body };
111
+ } };
112
+ }
113
+ function rewardWrite(path, fields, method, queryNames, validateResult) {
114
+ const operation = helixOperation(`channel_points/custom_rewards${path}`, fields, ["channel:manage:redemptions"], validateResult, method);
115
+ return { ...operation, request(input) {
116
+ const request = operation.request(input);
117
+ const url = new URL(request.url);
118
+ for (const [enabled, amount] of [["is_max_per_stream_enabled", "max_per_stream"],
119
+ ["is_max_per_user_per_stream_enabled", "max_per_user_per_stream"], ["is_global_cooldown_enabled", "global_cooldown_seconds"]]) {
120
+ if (request.body[enabled] === true && request.body[amount] === undefined) {
121
+ throw new ConnectorError("connector_input_invalid", `Supply ${amount} when enabling this reward limit.`, { statusCode: 422 });
122
+ }
123
+ }
124
+ for (const name of queryNames) { url.searchParams.set(name, request.body[name]); delete request.body[name]; }
125
+ if (method === "PATCH" && Object.keys(request.body).length === 0) throw new ConnectorError("connector_input_invalid", "Supply at least one reward change.", { statusCode: 422 });
126
+ return { ...request, url: url.href, body: method === "DELETE" ? undefined : request.body };
127
+ } };
128
+ }
129
+
130
+ async function normalizeTokenResponse(response) {
131
+ const failure = {
132
+ 401: ["connector_reconnect_required", "Connect this Twitch account again."],
133
+ 403: ["connector_permission_denied", "Twitch denied the token request."],
134
+ 429: ["connector_rate_limited", "Twitch's request limit was reached."]
135
+ }[response.status];
136
+ if (failure) throw new ConnectorError(...failure, { statusCode: response.status });
137
+ let value;
138
+ try { value = await response.clone().json(); } catch {
139
+ if (!response.ok) return response;
140
+ throw new ConnectorError("connector_response_invalid", "Twitch returned an invalid token response.", { statusCode: 502 });
141
+ }
142
+ if (!response.ok) {
143
+ if (response.status === 400 && value?.message === "Invalid refresh token") {
144
+ throw new ConnectorError("connector_reconnect_required", "Connect this Twitch account again.", { statusCode: 401 });
145
+ }
146
+ return response;
147
+ }
148
+ if (typeof value?.scope === "string" && (value.scope === "" || /^[^\s]+(?: [^\s]+)*$/u.test(value.scope))) return response;
149
+ if (!scopeList(value?.scope)) {
150
+ throw new ConnectorError("connector_response_invalid", "Twitch returned an invalid permission grant.", { statusCode: 502 });
151
+ }
152
+ return Response.json({ ...value, scope: value.scope.join(" ") }, { status: response.status });
153
+ }
154
+
155
+ const twitchProvider = Object.freeze({
156
+ ...twitchDefinition,
157
+ oauth: {
158
+ issuer: "https://id.twitch.tv/oauth2",
159
+ authorization_endpoint: "https://id.twitch.tv/oauth2/authorize",
160
+ token_endpoint: "https://id.twitch.tv/oauth2/token"
161
+ },
162
+ normalizeTokenResponse,
163
+ oauthHeaders: ({ clientId }) => ({ "Client-Id": clientId }),
164
+ apiOrigins: ["https://id.twitch.tv", "https://api.twitch.tv"],
165
+ checkOperation: "token.validate",
166
+ grantedScopesFromVerification: (value) => value.scopes,
167
+ async exchange(address, options, { request }) {
168
+ const url = new URL(address);
169
+ const permissions = {
170
+ "GET /helix/users": "user:read:email", "GET /helix/channels/followed": "user:read:follows",
171
+ "GET /helix/streams": null, "GET /helix/channels": null, "GET /helix/search/channels": null,
172
+ "PATCH /helix/channels": "channel:manage:broadcast",
173
+ "GET /helix/polls": ["channel:read:polls", "channel:manage:polls"],
174
+ "POST /helix/polls": "channel:manage:polls", "PATCH /helix/polls": "channel:manage:polls",
175
+ "GET /helix/predictions": ["channel:read:predictions", "channel:manage:predictions"],
176
+ "POST /helix/predictions": "channel:manage:predictions", "PATCH /helix/predictions": "channel:manage:predictions",
177
+ "POST /helix/chat/messages": "user:write:chat",
178
+ "GET /helix/subscriptions": "channel:read:subscriptions",
179
+ "GET /helix/subscriptions/user": "user:read:subscriptions",
180
+ "GET /helix/channels/vips": "channel:read:vips", "GET /helix/channels/editors": "channel:read:editors",
181
+ "GET /helix/moderation/banned": "moderation:read", "GET /helix/moderation/moderators": "moderation:read",
182
+ "GET /helix/channels/followers": "moderator:read:followers", "GET /helix/chat/chatters": "moderator:read:chatters",
183
+ "GET /helix/hypetrain/status": "channel:read:hype_train",
184
+ "GET /helix/bits/leaderboard": "bits:read",
185
+ "GET /helix/analytics/extensions": "analytics:read:extensions",
186
+ "GET /helix/analytics/games": "analytics:read:games",
187
+ "POST /helix/clips": "clips:edit", "GET /helix/clips": null,
188
+ "GET /helix/channel_points/custom_rewards": ["channel:read:redemptions", "channel:manage:redemptions"],
189
+ "POST /helix/channel_points/custom_rewards": "channel:manage:redemptions",
190
+ "PATCH /helix/channel_points/custom_rewards": "channel:manage:redemptions",
191
+ "DELETE /helix/channel_points/custom_rewards": "channel:manage:redemptions",
192
+ "GET /helix/channel_points/custom_rewards/redemptions": ["channel:read:redemptions", "channel:manage:redemptions"],
193
+ "PATCH /helix/channel_points/custom_rewards/redemptions": "channel:manage:redemptions",
194
+ "GET /helix/schedule": null, "POST /helix/schedule/segment": "channel:manage:schedule",
195
+ "PATCH /helix/schedule/segment": "channel:manage:schedule", "DELETE /helix/schedule/segment": "channel:manage:schedule",
196
+ "PATCH /helix/schedule/settings": "channel:manage:schedule",
197
+ "POST /helix/eventsub/subscriptions": null, "GET /helix/eventsub/subscriptions": null,
198
+ "DELETE /helix/eventsub/subscriptions": null
199
+ };
200
+ const operation = `${options.method} ${url.pathname}`;
201
+ if (!(url.href === validationUrl && options.method === "GET") &&
202
+ (url.origin !== "https://api.twitch.tv" || !Object.hasOwn(permissions, operation))) {
203
+ throw new ConnectorError("connector_destination_invalid", "This Twitch operation has an invalid destination.");
204
+ }
205
+ const validation = await request(validationUrl, { ...options, method: "GET", body: undefined });
206
+ if (!validToken(validation)) {
207
+ throw new ConnectorError("connector_response_invalid", "Twitch returned an invalid user-token validation.", { statusCode: 502 });
208
+ }
209
+ if (validation.client_id !== new Headers(options.headers).get("Client-Id")) {
210
+ throw new ConnectorError("connector_reconnect_required", "The Twitch token belongs to a different application.", { statusCode: 401 });
211
+ }
212
+ if (url.href === validationUrl) return validation;
213
+ const requiredScope = permissions[operation];
214
+ if (requiredScope && ![requiredScope].flat().some(scope => validation.scopes.includes(scope))) {
215
+ throw new ConnectorError("connector_reconnect_required", "Connect Twitch again with the permission needed for this operation.", { statusCode: 401 });
216
+ }
217
+ if (operation === "POST /helix/eventsub/subscriptions") {
218
+ const [version, scopes] = eventTypes[options.body.type];
219
+ if (scopes.length && !scopes.some(scope => validation.scopes.includes(scope))) {
220
+ throw new ConnectorError("connector_reconnect_required", "Connect Twitch again with permission for this event.", { statusCode: 401 });
221
+ }
222
+ options = { ...options, body: { type: options.body.type, version,
223
+ condition: { broadcaster_user_id: validation.user_id },
224
+ transport: { method: "websocket", session_id: options.body.session_id } } };
225
+ }
226
+ if (url.pathname === "/helix/channels/followed") url.searchParams.set("user_id", validation.user_id);
227
+ if (operation === "PATCH /helix/channels") url.searchParams.set("broadcaster_id", validation.user_id);
228
+ if (["/helix/polls", "/helix/predictions"].includes(url.pathname)) {
229
+ if (options.method === "GET") url.searchParams.set("broadcaster_id", validation.user_id);
230
+ else options = { ...options, body: { ...options.body, broadcaster_id: validation.user_id } };
231
+ }
232
+ if (operation === "POST /helix/chat/messages") options = { ...options, body: { ...options.body, sender_id: validation.user_id } };
233
+ if (url.pathname.startsWith("/helix/channel_points/custom_rewards")) url.searchParams.set("broadcaster_id", validation.user_id);
234
+ if (["/helix/schedule/segment", "/helix/schedule/settings"].includes(url.pathname)) url.searchParams.set("broadcaster_id", validation.user_id);
235
+ if (["/helix/subscriptions", "/helix/channels/vips", "/helix/channels/editors", "/helix/moderation/banned", "/helix/moderation/moderators", "/helix/hypetrain/status"].includes(url.pathname)) url.searchParams.set("broadcaster_id", validation.user_id);
236
+ if (url.pathname === "/helix/subscriptions/user") url.searchParams.set("user_id", validation.user_id);
237
+ if (url.pathname === "/helix/chat/chatters") url.searchParams.set("moderator_id", validation.user_id);
238
+ if (url.pathname === "/helix/channels/followers" && !url.searchParams.has("broadcaster_id")) url.searchParams.set("broadcaster_id", validation.user_id);
239
+ const result = await request(url.href, options);
240
+ if (url.pathname === "/helix/users" && result?.data?.[0]?.id !== validation.user_id) {
241
+ throw new ConnectorError("connector_response_invalid", "Twitch returned a profile for a different user.", { statusCode: 502 });
242
+ }
243
+ return result;
244
+ },
245
+ operations: {
246
+ "events.subscribe": helixOperation("eventsub/subscriptions", {
247
+ type: { type: "string", required: true, enum: Object.keys(eventTypes) }, session_id: opaqueId
248
+ }, [], eventSubscriptions, "POST"),
249
+ "events.list": helixOperation("eventsub/subscriptions", { after: pageFields.after,
250
+ type: { type: "string", enum: Object.keys(eventTypes) }
251
+ }, [], value => eventSubscriptions(value) && page(value, () => true)),
252
+ "events.delete": { ...helixOperation("eventsub/subscriptions", { id: opaqueId }, [], value => value === null), request(input) {
253
+ const operation = jsonOperation("https://api.twitch.tv/helix/eventsub/subscriptions", { id: opaqueId }, value => value === null);
254
+ return { ...operation.request(input), method: "DELETE" };
255
+ } },
256
+ "schedule.vacation": { ...vacationSettings, scopes: ["channel:manage:schedule"], request(input) {
257
+ const request = vacationSettings.request(input);
258
+ const query = new URL(request.url).searchParams;
259
+ if (query.get("is_vacation_enabled") === "true" && (!query.has("timezone") || !query.has("vacation_start_time") || !query.has("vacation_end_time") ||
260
+ Date.parse(query.get("vacation_end_time")) <= Date.parse(query.get("vacation_start_time")))) {
261
+ throw new ConnectorError("connector_input_invalid", "Enabling vacation requires a timezone and an end after the start.", { statusCode: 422 });
262
+ }
263
+ return { ...request, method: "PATCH" };
264
+ } },
265
+ "schedule.get": helixOperation("schedule", { broadcaster_id: { ...idField, required: true }, id: { ...opaqueId, required: false }, start_time: scheduleTime,
266
+ ...pageFields, first: { type: "integer", min: 1, max: 25, defaultTo: 20 }
267
+ }, [], scheduleResult),
268
+ "schedule.create": scheduleWrite({ ...scheduleFields, start_time: { ...scheduleFields.start_time, required: true },
269
+ timezone: { ...scheduleFields.timezone, required: true }, duration: { ...scheduleFields.duration, required: true },
270
+ is_recurring: { type: "boolean", required: true }
271
+ }, "POST"),
272
+ "schedule.update": scheduleWrite({ id: opaqueId, ...scheduleFields, is_canceled: { type: "boolean" } }, "PATCH"),
273
+ "schedule.delete": scheduleWrite({ id: opaqueId }, "DELETE"),
274
+ "rewards.list": helixOperation("channel_points/custom_rewards", { id: { ...opaqueId, required: false }, only_manageable_rewards: { type: "boolean", defaultTo: false } }, ["channel:read:redemptions", "channel:manage:redemptions"], rewardResult),
275
+ "rewards.create": rewardWrite("", { ...rewardFields, title: { ...rewardFields.title, required: true }, cost: { ...rewardFields.cost, required: true } }, "POST", [], rewardResult),
276
+ "rewards.update": rewardWrite("", { id: opaqueId, ...rewardFields, is_paused: { type: "boolean" } }, "PATCH", ["id"], rewardResult),
277
+ "rewards.delete": rewardWrite("", { id: opaqueId }, "DELETE", ["id"], value => value === null),
278
+ "redemptions.list": helixOperation("channel_points/custom_rewards/redemptions", {
279
+ reward_id: opaqueId, id: { ...opaqueId, required: false },
280
+ status: { type: "string", enum: ["UNFULFILLED", "FULFILLED", "CANCELED"], defaultTo: "UNFULFILLED" },
281
+ sort: { type: "string", enum: ["OLDEST", "NEWEST"], defaultTo: "OLDEST" },
282
+ ...pageFields, first: { type: "integer", min: 1, max: 50, defaultTo: 20 }
283
+ }, ["channel:read:redemptions", "channel:manage:redemptions"], value => page(value, item =>
284
+ typeof item?.id === "string" && userId(item.broadcaster_id) && userId(item.user_id) && ["UNFULFILLED", "FULFILLED", "CANCELED"].includes(item.status))),
285
+ "redemptions.update": rewardWrite("/redemptions", { reward_id: opaqueId, id: opaqueId,
286
+ status: { type: "string", required: true, enum: ["FULFILLED", "CANCELED"] }
287
+ }, "PATCH", ["reward_id", "id"], interactionResult),
288
+ "clips.create": { ...clipCreation, scopes: ["clips:edit"], request(input) {
289
+ return { ...clipCreation.request(input), method: "POST" };
290
+ } },
291
+ "clips.get": helixOperation("clips", { id: opaqueId }, [], value => Array.isArray(value?.data) && value.data.every(item =>
292
+ typeof item?.id === "string" && userId(item.broadcaster_id) && typeof item.url === "string" && typeof item.title === "string")),
293
+ "analytics.extensions": analyticsOperation("extensions", "extension_id"),
294
+ "analytics.games": analyticsOperation("games", "game_id"),
295
+ "bits.leaderboard": helixOperation("bits/leaderboard", {
296
+ count: { type: "integer", min: 1, max: 100, defaultTo: 10 },
297
+ period: { type: "string", enum: ["day", "week", "month", "year", "all"], defaultTo: "all" },
298
+ started_at: { type: "string", maxLength: 40, validator: value =>
299
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(value) && Number.isFinite(Date.parse(value)) || "Enter an RFC3339 timestamp." },
300
+ user_id: idField
301
+ }, ["bits:read"], value => Array.isArray(value?.data) && value.data.every(item =>
302
+ userId(item?.user_id) && Number.isSafeInteger(item.rank) && item.rank > 0 && Number.isSafeInteger(item.score) && item.score >= 0) &&
303
+ Number.isSafeInteger(value.total) && value.total >= 0 && typeof value.date_range?.started_at === "string" && typeof value.date_range?.ended_at === "string"),
304
+ "subscriptions.list": helixOperation("subscriptions", { ...pageFields, user_id: idField }, ["channel:read:subscriptions"], value => page(value, item => userId(item?.user_id) && typeof item.tier === "string")),
305
+ "subscriptions.check": helixOperation("subscriptions/user", { broadcaster_id: { ...idField, required: true } }, ["user:read:subscriptions"], value => Array.isArray(value?.data) && value.data.every(item => userId(item?.broadcaster_id) && typeof item.tier === "string")),
306
+ "vips.list": helixOperation("channels/vips", { ...pageFields, user_id: idField }, ["channel:read:vips"], value => page(value, item => userId(item?.user_id))),
307
+ "editors.list": helixOperation("channels/editors", {}, ["channel:read:editors"], value => Array.isArray(value?.data) && value.data.every(item => userId(item?.user_id))),
308
+ "bannedUsers.list": helixOperation("moderation/banned", { ...pageFields, user_id: idField }, ["moderation:read"], value => page(value, item => userId(item?.user_id))),
309
+ "moderators.list": helixOperation("moderation/moderators", { ...pageFields, user_id: idField }, ["moderation:read"], value => page(value, item => userId(item?.user_id))),
310
+ "followers.list": helixOperation("channels/followers", { ...pageFields, broadcaster_id: idField, user_id: idField }, ["moderator:read:followers"], value => page(value, item => userId(item?.user_id)) && Number.isSafeInteger(value.total) && value.total >= 0),
311
+ "chatters.list": helixOperation("chat/chatters", { ...pageFields, broadcaster_id: { ...idField, required: true } }, ["moderator:read:chatters"], value => page(value, item => userId(item?.user_id)) && Number.isSafeInteger(value.total) && value.total >= 0),
312
+ "hypeTrain.status": helixOperation("hypetrain/status", {}, ["channel:read:hype_train"], value => Array.isArray(value?.data) && value.data.length === 1 &&
313
+ (value.data[0]?.current === null || typeof value.data[0]?.current?.id === "string")),
314
+ "polls.list": helixOperation("polls", { ...pageFields, first: { type: "integer", min: 1, max: 20, defaultTo: 20 }, id: { ...opaqueId, required: false } }, ["channel:read:polls", "channel:manage:polls"], interactionResult),
315
+ "polls.create": helixOperation("polls", {
316
+ title: { type: "string", required: true, minLength: 1, maxLength: 60 }, choices: choiceList(5),
317
+ duration: { type: "integer", required: true, min: 15, max: 1800 }
318
+ }, ["channel:manage:polls"], interactionResult, "POST"),
319
+ "polls.end": helixOperation("polls", { id: opaqueId, status: { type: "string", required: true, enum: ["TERMINATED", "ARCHIVED"] } }, ["channel:manage:polls"], interactionResult, "PATCH"),
320
+ "predictions.list": helixOperation("predictions", { ...pageFields, first: { type: "integer", min: 1, max: 20, defaultTo: 20 }, id: { ...opaqueId, required: false } }, ["channel:read:predictions", "channel:manage:predictions"], interactionResult),
321
+ "predictions.create": helixOperation("predictions", {
322
+ title: { type: "string", required: true, minLength: 1, maxLength: 45 }, outcomes: choiceList(10),
323
+ prediction_window: { type: "integer", required: true, min: 30, max: 1800 }
324
+ }, ["channel:manage:predictions"], interactionResult, "POST"),
325
+ "predictions.end": helixOperation("predictions", { id: opaqueId,
326
+ status: { type: "string", required: true, enum: ["LOCKED", "CANCELED", "RESOLVED"] }, winning_outcome_id: { ...opaqueId, required: false }
327
+ }, ["channel:manage:predictions"], interactionResult, "PATCH", values => {
328
+ if ((values.status === "RESOLVED") !== Boolean(values.winning_outcome_id)) throw new ConnectorError("connector_input_invalid", "Supply a winning outcome only when resolving the prediction.", { statusCode: 422 });
329
+ }),
330
+ "chat.send": helixOperation("chat/messages", { broadcaster_id: { ...idField, required: true },
331
+ message: { type: "string", required: true, noTrim: true, minLength: 1, maxLength: 500 }, reply_parent_message_id: { ...opaqueId, required: false }
332
+ }, ["user:write:chat"], value => Array.isArray(value?.data) && value.data.length === 1 &&
333
+ typeof value.data[0]?.message_id === "string" && typeof value.data[0].is_sent === "boolean", "POST"),
334
+ "streams.list": jsonOperation("https://api.twitch.tv/helix/streams", {
335
+ ...pageFields, user_id: idField, game_id: idField,
336
+ user_login: { type: "string", minLength: 1, maxLength: 25, pattern: "^[A-Za-z0-9_]+$" },
337
+ language: { type: "string", pattern: "^([a-z]{2}|other)$" }, type: { type: "string", enum: ["all", "live"], defaultTo: "all" }
338
+ }, value => page(value, stream => userId(stream?.id) && userId(stream.user_id) &&
339
+ typeof stream.title === "string" && Number.isSafeInteger(stream.viewer_count) && stream.viewer_count >= 0)),
340
+ "channels.read": jsonOperation("https://api.twitch.tv/helix/channels", { broadcaster_id: { ...idField, required: true } },
341
+ value => Array.isArray(value?.data) && value.data.every(channel => userId(channel?.broadcaster_id) && typeof channel.title === "string")),
342
+ "channels.search": jsonOperation("https://api.twitch.tv/helix/search/channels", {
343
+ ...pageFields, query: { type: "string", required: true, minLength: 1, maxLength: 100 }, live_only: { type: "boolean", defaultTo: false }
344
+ }, value => page(value, channel => userId(channel?.id) && typeof channel.display_name === "string" && typeof channel.is_live === "boolean")),
345
+ "broadcast.update": {
346
+ scopes: ["channel:manage:broadcast"], validateResult: broadcastUpdate.validateResult,
347
+ request(input) {
348
+ const request = broadcastUpdate.request(input);
349
+ if (!Object.keys(request.body).length) throw new ConnectorError("connector_input_invalid", "Supply at least one broadcast change.", { statusCode: 422 });
350
+ return { ...request, method: "PATCH" };
351
+ }
352
+ },
353
+ "token.validate": jsonOperation(validationUrl, {}, validToken),
354
+ "profile.read": {
355
+ ...jsonOperation("https://api.twitch.tv/helix/users", {}, (value) => Array.isArray(value?.data) &&
356
+ value.data.length === 1 && userId(value.data[0]?.id) && typeof value.data[0].login === "string"),
357
+ scopes: ["user:read:email"]
358
+ },
359
+ "channels.followed": {
360
+ ...jsonOperation("https://api.twitch.tv/helix/channels/followed", {
361
+ first: { type: "integer", min: 1, max: 100, defaultTo: 20 },
362
+ after: { type: "string", minLength: 1, maxLength: 4096 },
363
+ broadcaster_id: { type: "string", validator: (value) => userId(value) || "Enter a Twitch broadcaster ID." }
364
+ }, (value) => Array.isArray(value?.data) && value.data.every((channel) => userId(channel?.broadcaster_id)) &&
365
+ Number.isSafeInteger(value.total) && value.total >= 0 && value.pagination !== null &&
366
+ typeof value.pagination === "object" && !Array.isArray(value.pagination) &&
367
+ (value.pagination.cursor === undefined || (typeof value.pagination.cursor === "string" && value.pagination.cursor.length > 0))),
368
+ scopes: ["user:read:follows"]
369
+ }
370
+ }
371
+ });
372
+
373
+ function parseTwitchEventSubMessage(raw, { broadcasterId, sessionId, subscriptionIds = [] } = {}) {
374
+ const invalid = () => new ConnectorError("connector_event_invalid", "Twitch returned an invalid or unexpected EventSub message.", { statusCode: 422 });
375
+ if (typeof raw !== "string" || Buffer.byteLength(raw, "utf8") > 1048576 || !userId(broadcasterId)) throw invalid();
376
+ let message;
377
+ try { message = JSON.parse(raw); } catch { throw invalid(); }
378
+ const metadata = message?.metadata, payload = message?.payload;
379
+ if (!metadata || typeof metadata.message_id !== "string" || !metadata.message_id || metadata.message_id.length > 256 ||
380
+ typeof metadata.message_timestamp !== "string" || !Number.isFinite(Date.parse(metadata.message_timestamp)) ||
381
+ !payload || typeof payload !== "object" || Array.isArray(payload)) throw invalid();
382
+ const type = metadata.message_type;
383
+ if (type === "session_welcome" || type === "session_reconnect") {
384
+ const session = payload.session;
385
+ if (!session || typeof session.id !== "string" || !session.id || session.id.length > 256) throw invalid();
386
+ if (type === "session_welcome") {
387
+ if (session.status !== "connected" || !Number.isSafeInteger(session.keepalive_timeout_seconds) || session.keepalive_timeout_seconds < 1) throw invalid();
388
+ } else {
389
+ if (session.id !== sessionId || session.status !== "reconnecting") throw invalid();
390
+ let url;
391
+ try { url = new URL(session.reconnect_url); } catch { throw invalid(); }
392
+ if (url.protocol !== "wss:" || url.hostname !== "eventsub.wss.twitch.tv" || url.port || url.username || url.password || url.hash) throw invalid();
393
+ }
394
+ } else if (type === "notification" || type === "revocation") {
395
+ const subscription = payload.subscription;
396
+ if (!subscription || !Array.isArray(subscriptionIds) || !subscriptionIds.includes(subscription.id) ||
397
+ !sessionId || subscription.transport?.method !== "websocket" || subscription.transport.session_id !== sessionId ||
398
+ subscription.condition?.broadcaster_user_id !== broadcasterId || !Object.hasOwn(eventTypes, subscription.type) ||
399
+ subscription.version !== eventTypes[subscription.type][0] || metadata.subscription_type !== subscription.type ||
400
+ metadata.subscription_version !== subscription.version) throw invalid();
401
+ if (type === "notification") {
402
+ if (subscription.status !== "enabled" || !payload.event || typeof payload.event !== "object" || Array.isArray(payload.event) ||
403
+ payload.event.broadcaster_user_id !== broadcasterId) throw invalid();
404
+ } else if (!["authorization_revoked", "user_removed", "version_removed"].includes(subscription.status)) throw invalid();
405
+ } else if (type !== "session_keepalive") throw invalid();
406
+ return message;
407
+ }
408
+
409
+ export { twitchProvider, parseTwitchEventSubMessage };