@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,678 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
7
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
8
+ import { parseIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
9
+ import { waveProvider } from "../src/server/wave.js";
10
+
11
+ const context = { applicationId: "app-one", subjectId: "person-one" };
12
+ const input = { context, integrationId: "wave" };
13
+ const scopes = ["user:read", "business:read", "customer:read", "invoice:read"];
14
+ const callback = "https://app.example.test/connections/wave/callback";
15
+ const user = { data: { user: { id: "VXNlcjpmaXh0dXJl", firstName: "Fixture", lastName: "User", defaultEmail: "fixture@example.test" } } };
16
+ const businessId = "QnVzaW5lc3M6Zml4dHVyZQ==";
17
+ const page = (nodes = [], currentPage = 1, totalPages = 1, totalCount = nodes.length) => ({
18
+ pageInfo: { currentPage, totalPages, totalCount }, edges: nodes.map((node) => ({ node }))
19
+ });
20
+ const business = { id: businessId, name: "Fixture business", isPersonal: false };
21
+ const customer = { id: "Q3VzdG9tZXI6b25l", name: "Customer", email: null };
22
+ const invoice = { id: "SW52b2ljZTpvbmU=", invoiceNumber: "001", invoiceDate: "2026-09-09", status: "DRAFT", currency: { code: "USD" } };
23
+
24
+ async function fixture(t, requestedScopes = scopes) {
25
+ const directory = await mkdtemp(path.join(tmpdir(), "wave-connector-"));
26
+ t.after(() => rm(directory, { recursive: true, force: true }));
27
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(7) }, activeKeyId: "current" });
28
+ const requests = [];
29
+ const state = { time: Date.now(), tokenCount: 0, tokenStatus: 200, tokenResponse: null, tokenScopes: requestedScopes.join(" "), status: 200, response: null, hang: false };
30
+ const bindings = { "env:WAVE_SECRET": "fixture-wave-secret", "env:WAVE_CALLBACK": callback };
31
+ const options = {
32
+ configuration: { schemaVersion: 1, integrations: { wave: { provider: "wave", accountMode: "per-user", scopes: requestedScopes,
33
+ authentication: { method: "oauth2", registrationRef: "wave" } } }, registrations: { wave: { source: "own", clientId: "fixture-wave-client",
34
+ clientSecretRef: "env:WAVE_SECRET", callbackUrlRef: "env:WAVE_CALLBACK", tokenEndpointAuthMethod: "client_secret_post" } } },
35
+ providers: [{ ...waveProvider, requestTimeoutMs: 50 }], authorize: async (owner) => owner,
36
+ resolveReference: async (ref) => bindings[ref], now: () => state.time,
37
+ store: createFileConnectionStore({ directory, protection }),
38
+ fetchImpl: async (address, init) => {
39
+ const url = new URL(String(address));
40
+ requests.push({ url, init, headers: new Headers(init.headers) });
41
+ if (url.origin === "https://api.waveapps.com") {
42
+ assert.equal(url.pathname, "/oauth2/token/");
43
+ assert.equal(init.method, "POST");
44
+ const body = new URLSearchParams(init.body);
45
+ assert.equal(body.get("client_id"), "fixture-wave-client");
46
+ assert.equal(body.get("client_secret"), "fixture-wave-secret");
47
+ assert.equal(body.get("redirect_uri"), callback);
48
+ if (body.get("grant_type") === "refresh_token") assert.equal(body.get("refresh_token"), `fixture-refresh-${state.tokenCount}`);
49
+ if (state.tokenStatus === 200) state.tokenCount += 1;
50
+ return Response.json(state.tokenResponse || { access_token: `fixture-access-${state.tokenCount}`, refresh_token: `fixture-refresh-${state.tokenCount}`,
51
+ token_type: "Bearer", expires_in: 60, scope: state.tokenScopes, userId: user.data.user.id, businessId }, { status: state.tokenStatus });
52
+ }
53
+ assert.equal(url.href, "https://gql.waveapps.com/graphql/public");
54
+ assert.equal(init.method, "POST");
55
+ assert.equal(init.redirect, "error");
56
+ assert.equal(new Headers(init.headers).get("authorization"), `Bearer fixture-access-${state.tokenCount}`);
57
+ if (state.hang) return new Promise((_, reject) => {
58
+ const timer = setTimeout(() => reject(new Error("Abort was not delivered")), 1000);
59
+ const abort = () => { clearTimeout(timer); reject(init.signal.reason); };
60
+ if (init.signal.aborted) abort(); else init.signal.addEventListener("abort", abort, { once: true });
61
+ });
62
+ const body = JSON.parse(init.body);
63
+ let value = user;
64
+ if (body.query.includes("ConnectorWaveBusinesses")) value = { data: { businesses: page([business], body.variables.page) } };
65
+ if (body.query.includes("ConnectorWaveCustomers")) value = { data: { business: { id: businessId, customers: page([customer], body.variables.page) } } };
66
+ if (body.query.includes("ConnectorWaveInvoices")) value = { data: { business: { id: businessId, invoices: page([invoice], body.variables.page) } } };
67
+ return Response.json(state.responses?.shift() || state.response || value, { status: state.status });
68
+ }
69
+ };
70
+ const service = createConnectionService(options);
71
+ async function start() {
72
+ const result = await service.beginAuthorization(input);
73
+ const url = new URL(result.authorizationUrl);
74
+ const redirect = new URL(callback);
75
+ redirect.searchParams.set("code", "fixture-wave-code");
76
+ redirect.searchParams.set("state", url.searchParams.get("state"));
77
+ return { url, callbackUrl: redirect.href };
78
+ }
79
+ const connect = async () => service.completeAuthorization({ ...input, callbackUrl: (await start()).callbackUrl });
80
+ return { service, options, state, requests, directory, bindings, start, connect };
81
+ }
82
+
83
+ test("Wave code consent verifies the user and persists encrypted state for CLI restart", async (t) => {
84
+ const { service, options, requests, directory, start } = await fixture(t);
85
+ const started = await start();
86
+ assert.equal(started.url.origin + started.url.pathname, "https://api.waveapps.com/oauth2/authorize/");
87
+ assert.equal(started.url.searchParams.get("scope"), scopes.join(" "));
88
+ assert.equal(started.url.searchParams.get("redirect_uri"), callback);
89
+ assert.equal(started.url.searchParams.get("code_challenge_method"), "S256");
90
+ assert.equal(started.url.searchParams.has("client_secret"), false);
91
+ const connected = await service.completeAuthorization({ ...input, callbackUrl: started.callbackUrl });
92
+ assert.equal(connected.status, "connected");
93
+ assert.deepEqual(connected.grantedScopes, scopes);
94
+ const exchange = new URLSearchParams(requests[0].init.body);
95
+ assert.equal(exchange.get("grant_type"), "authorization_code");
96
+ assert.equal(exchange.get("code"), "fixture-wave-code");
97
+ assert.ok(exchange.get("code_verifier"));
98
+ assert.match(JSON.parse(requests[1].init.body).query, /query ConnectorWaveUser/);
99
+ for (const file of await readdir(directory)) {
100
+ const text = await readFile(path.join(directory, file), "utf8");
101
+ for (const secret of ["fixture-wave-secret", "fixture-wave-code", "fixture-access-1", "fixture-refresh-1"]) assert.equal(text.includes(secret), false);
102
+ }
103
+ const restarted = createConnectionService(options);
104
+ assert.deepEqual(await restarted.status(input), { ...connected, callbackUrl: callback });
105
+ assert.deepEqual(await restarted.invoke({ ...input, operation: "user.read" }), user);
106
+ });
107
+
108
+ test("Wave reads bounded business, customer and invoice pages without following returned URLs", async (t) => {
109
+ const { service, state, requests, connect } = await fixture(t);
110
+ await connect();
111
+ const businesses = await service.invoke({ ...input, operation: "businesses.list" });
112
+ assert.deepEqual(businesses.data.businesses.edges, [{ node: business }]);
113
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { page: 1, pageSize: 20 });
114
+ for (const [operation, resource, node] of [["customers.list", "customers", customer], ["invoices.list", "invoices", invoice]]) {
115
+ const result = await service.invoke({ ...input, operation, input: { businessId, page: 2, pageSize: 5 } });
116
+ assert.deepEqual(result.data.business[resource].edges, [{ node }]);
117
+ const body = JSON.parse(requests.at(-1).init.body);
118
+ assert.deepEqual(body.variables, { businessId, page: 2, pageSize: 5 });
119
+ assert.equal(body.query.includes(businessId), false);
120
+ state.response = { data: { business: { id: businessId, [resource]: page([], 3, 3, 6) } } };
121
+ assert.deepEqual(await service.invoke({ ...input, operation, input: { businessId, page: 3 } }), state.response);
122
+ state.response = null;
123
+ }
124
+ state.response = { data: { businesses: page([null], 1, 2, 2) } };
125
+ assert.deepEqual(await service.invoke({ ...input, operation: "businesses.list" }), state.response);
126
+ state.response = { data: { businesses: page([], 1, 0, 0) } };
127
+ assert.deepEqual(await service.invoke({ ...input, operation: "businesses.list" }), state.response);
128
+ });
129
+
130
+ test("Wave validates all operation inputs before HTTP and rejects arbitrary queries or destinations", async (t) => {
131
+ const { service, requests, connect } = await fixture(t);
132
+ await connect();
133
+ const count = requests.length;
134
+ for (const values of [{}, { businessId: "" }, { businessId: " " }, { businessId: "bad\nvalue" }, { businessId: "x".repeat(2049) },
135
+ { businessId, page: 0 }, { businessId, page: 2.5 }, { businessId, page: 2_147_483_648 }, { businessId, pageSize: 101 },
136
+ { businessId, pageSize: 0 }, { businessId, url: "https://invalid.test" }, { businessId, query: "mutation { anything }" }]) {
137
+ await assert.rejects(service.invoke({ ...input, operation: "customers.list", input: values }), { code: "connector_input_invalid" });
138
+ }
139
+ await assert.rejects(service.invoke({ ...input, operation: "user.read", input: { id: "other" } }), { code: "connector_input_invalid" });
140
+ await assert.rejects(service.invoke({ ...input, operation: "arbitrary.query" }), { code: "connector_operation_unknown" });
141
+ assert.equal(requests.length, count);
142
+ });
143
+
144
+ test("Wave enforces granted read scopes, preserves wildcard grants and never treats write as read", async (t) => {
145
+ const reduced = await fixture(t);
146
+ reduced.state.tokenScopes = "user:read business:read";
147
+ await reduced.connect();
148
+ const count = reduced.requests.length;
149
+ await assert.rejects(reduced.service.invoke({ ...input, operation: "customers.list", input: { businessId } }), { code: "connector_scope_missing" });
150
+ assert.equal(reduced.requests.length, count);
151
+ const writes = await fixture(t, ["user:read", "customer:write"]);
152
+ await writes.connect();
153
+ await assert.rejects(writes.service.invoke({ ...input, operation: "customers.list", input: { businessId } }), { code: "connector_scope_missing" });
154
+ assert.equal(writes.requests.length, 2);
155
+ const wildcard = await fixture(t, ["user:*", "business:*", "customer:*", "invoice:*"]);
156
+ await wildcard.connect();
157
+ for (const operation of ["businesses.list", "customers.list", "invoices.list"]) {
158
+ await wildcard.service.invoke({ ...input, operation, input: operation === "businesses.list" ? {} : { businessId } });
159
+ }
160
+ assert.deepEqual((await wildcard.service.status(input)).grantedScopes, ["user:*", "business:*", "customer:*", "invoice:*"]);
161
+ });
162
+
163
+ test("Wave serializes refresh with the original callback and preserves rotated grants after API failure", async (t) => {
164
+ const { service, options, state, requests, connect } = await fixture(t);
165
+ await connect(); state.time += 40_000;
166
+ await Promise.all([service.invoke({ ...input, operation: "user.read" }), service.invoke({ ...input, operation: "user.read" })]);
167
+ assert.equal(state.tokenCount, 2);
168
+ const refresh = new URLSearchParams(requests.find((r) => String(r.init.body).includes("grant_type=refresh_token")).init.body);
169
+ assert.equal(refresh.get("redirect_uri"), callback);
170
+ state.time += 40_000; state.status = 429;
171
+ state.tokenScopes = "user:read business:read";
172
+ await assert.rejects(service.invoke({ ...input, operation: "user.read" }), { code: "connector_rate_limited" });
173
+ assert.equal(state.tokenCount, 3);
174
+ state.status = 200;
175
+ const restarted = createConnectionService(options);
176
+ await restarted.invoke({ ...input, operation: "user.read" });
177
+ assert.equal(state.tokenCount, 3);
178
+ assert.deepEqual((await restarted.status(input)).grantedScopes, ["user:read", "business:read"]);
179
+ await assert.rejects(restarted.invoke({ ...input, operation: "invoices.list", input: { businessId } }), { code: "connector_scope_missing" });
180
+ });
181
+
182
+ test("Wave rejects cross-owner access, changed callbacks and denied host policy without provider calls", async (t) => {
183
+ const { service, options, bindings, requests, connect } = await fixture(t);
184
+ await connect(); const count = requests.length;
185
+ for (const owner of [{ ...context, applicationId: "other" }, { ...context, subjectId: "other" }]) {
186
+ await assert.rejects(service.invoke({ ...input, context: owner, operation: "user.read" }), { code: "connector_reconnect_required" });
187
+ }
188
+ bindings["env:WAVE_CALLBACK"] = "https://new.example.test/callback";
189
+ assert.equal((await service.status(input)).status, "reconnect-required");
190
+ await assert.rejects(createConnectionService(options).invoke({ ...input, operation: "user.read" }), { code: "connector_reconnect_required" });
191
+ delete bindings["env:WAVE_CALLBACK"];
192
+ assert.equal((await service.status(input)).status, "reconnect-required");
193
+ bindings["env:WAVE_CALLBACK"] = callback;
194
+ assert.equal((await service.status(input)).status, "connected");
195
+ const denied = createConnectionService({ ...options, authorize: async () => null });
196
+ await assert.rejects(denied.invoke({ ...input, operation: "customers.list", input: { businessId } }), { code: "connector_access_denied" });
197
+ assert.equal(requests.length, count);
198
+ });
199
+
200
+ test("Wave consent cancellation, denial and replay preserve an existing connection", async (t) => {
201
+ const { service, requests, start, connect } = await fixture(t);
202
+ const original = await connect(); const count = requests.length;
203
+ const pending = await start();
204
+ await service.cancelAuthorization({ ...input, state: pending.url.searchParams.get("state") });
205
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: pending.callbackUrl }), { code: "connector_attempt_invalid" });
206
+ const denied = await start(); const redirect = new URL(denied.callbackUrl);
207
+ redirect.searchParams.delete("code"); redirect.searchParams.set("error", "access_denied");
208
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: redirect.href }), { code: "connector_consent_denied" });
209
+ assert.equal(requests.length, count);
210
+ assert.deepEqual(await service.status(input), { ...original, callbackUrl: callback });
211
+ const complete = await start(); await service.completeAuthorization({ ...input, callbackUrl: complete.callbackUrl });
212
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: complete.callbackUrl }), { code: "connector_attempt_invalid" });
213
+ await service.disconnect(input);
214
+ assert.equal((await service.status(input)).status, "disconnected");
215
+ assert.equal(requests.length, count + 2);
216
+ });
217
+
218
+ test("Wave maps HTTP and GraphQL errors without returning private partial data or provider messages", async (t) => {
219
+ const { service, state, connect } = await fixture(t);
220
+ for (const [status, code] of [[401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
221
+ state.status = status; state.response = { message: "fixture-wave-secret" };
222
+ await assert.rejects(connect(), (error) => { assert.equal(error.code, code); assert.equal(error.message.includes("fixture-wave-secret"), false); return true; });
223
+ assert.equal((await service.status(input)).status, "disconnected");
224
+ }
225
+ state.status = 200; state.response = null; await connect();
226
+ for (const [providerCode, code] of [["NOT_FOUND", "connector_resource_not_found"], ["INTERNAL_SERVER_ERROR", "connector_provider_failed"], ["GRAPHQL_VALIDATION_FAILED", "connector_provider_failed"], ["UNKNOWN", "connector_provider_failed"], ["UNAUTHENTICATED", "connector_reconnect_required"]]) {
227
+ state.response = { ...user, errors: [{ message: "fixture-wave-secret", extensions: { code: providerCode } }] };
228
+ await assert.rejects(service.invoke({ ...input, operation: "user.read" }), (error) => {
229
+ assert.equal(error.code, code); assert.equal(error.message.includes("fixture-wave-secret"), false); assert.equal(JSON.stringify(error).includes("fixture-wave-secret"), false); return true;
230
+ });
231
+ }
232
+ assert.equal((await service.status(input)).status, "reconnect-required");
233
+ });
234
+
235
+ test("Wave rejects malformed success responses, wrong businesses and inconsistent paging", async (t) => {
236
+ const { service, state, connect } = await fixture(t);
237
+ for (const response of [{}, { data: null }, { data: { user: null } }, { data: { user: { id: 1, defaultEmail: "a" } } }, { ...user, errors: {} }]) {
238
+ state.response = response;
239
+ await assert.rejects(connect(), { code: "connector_response_invalid" });
240
+ }
241
+ state.response = null; await connect();
242
+ for (const value of [
243
+ { id: "different", customers: page([customer]) },
244
+ { id: businessId, customers: page([customer], 2) },
245
+ { id: businessId, customers: page([customer, customer]) },
246
+ { id: businessId, customers: page([{ id: customer.id }]) },
247
+ { id: businessId, customers: { edges: [], pageInfo: { currentPage: 1, totalPages: -1, totalCount: 0 } } }
248
+ ]) {
249
+ state.response = { data: { business: value } };
250
+ await assert.rejects(service.invoke({ ...input, operation: "customers.list", input: { businessId, pageSize: 1 } }), { code: "connector_response_invalid" });
251
+ }
252
+ });
253
+
254
+ test("Wave requires explicit token scopes and surfaces expired grants and subscription failures", async (t) => {
255
+ const { service, state, connect } = await fixture(t);
256
+ for (const scope of [undefined, null, [], "", "user:read\ninvoice:read"]) {
257
+ state.tokenResponse = { access_token: "fixture", token_type: "Bearer", expires_in: 60, scope };
258
+ await assert.rejects(connect(), { code: "connector_response_invalid" });
259
+ }
260
+ state.tokenResponse = null; await connect(); state.time += 40_000;
261
+ state.tokenStatus = 403; state.tokenResponse = { message: "fixture-wave-secret" };
262
+ await assert.rejects(service.invoke({ ...input, operation: "user.read" }), { code: "connector_permission_denied" });
263
+ state.tokenStatus = 400; state.tokenResponse = { error: "invalid_grant", error_description: "fixture-wave-secret" };
264
+ await assert.rejects(service.invoke({ ...input, operation: "user.read" }), { code: "connector_reconnect_required" });
265
+ assert.equal((await service.status(input)).status, "reconnect-required");
266
+ });
267
+
268
+ test("Wave request cancellation and timeout stop the transport without retries", async (t) => {
269
+ const { service, state, requests, connect } = await fixture(t);
270
+ await connect(); state.hang = true;
271
+ const count = requests.length;
272
+ const controller = new AbortController();
273
+ const pending = service.invoke({ ...input, operation: "user.read", signal: controller.signal });
274
+ setTimeout(() => controller.abort(), 5);
275
+ await assert.rejects(pending, { code: "connector_cancelled" });
276
+ await assert.rejects(service.invoke({ ...input, operation: "user.read" }), { code: "connector_provider_timeout" });
277
+ assert.equal(requests.length, count + 2);
278
+ });
279
+
280
+ test("Wave guide is valid portable configuration with all captured and granular permission choices", async () => {
281
+ const guide = await readFile(new URL("../docs/wave.md", import.meta.url), "utf8");
282
+ const json = guide.match(/```json\n([\s\S]*?)\n```/u)[1];
283
+ const config = parseIntegrationConfiguration(json, { providers: [waveProvider] });
284
+ assert.deepEqual(config.integrations.wave.scopes, scopes);
285
+ assert.equal(config.registrations.wave.tokenEndpointAuthMethod, "client_secret_post");
286
+ assert.equal(waveProvider.scopes.length, 32);
287
+ for (const scope of ["user:read", "business:read", "account:*", "customer:*", "estimate:*", "invoice:*", "product:*", "sales_tax:*", "transaction:*", "vendor:*"]) {
288
+ assert.ok(waveProvider.scopes.some((value) => value.value === scope));
289
+ }
290
+ });
291
+
292
+
293
+ test("Wave creates a customer in the explicitly selected business without replaying writes", async t => {
294
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "customer:write"]);
295
+ await connect();
296
+ const created = { ...customer, business: { id: businessId } };
297
+ state.response = { data: { customerCreate: { didSucceed: true, inputErrors: [], customer: created } } };
298
+ const details = { businessId, name: "Customer", email: "customer@example.test", internalNotes: "Appointment customer" };
299
+ const result = await service.invoke({ ...input, operation: "customers.create", input: { input: details } });
300
+ assert.deepEqual(result.data.customerCreate.customer, created);
301
+ const wire = JSON.parse(requests.at(-1).init.body);
302
+ assert.match(wire.query, /^mutation ConnectorWaveCustomerCreate/);
303
+ assert.deepEqual(wire.variables, { input: details });
304
+ for (const bad of [{ name: "Missing business" }, { businessId, name: "" }, { ...details, unexpected: true }]) {
305
+ const count = requests.length;
306
+ await assert.rejects(service.invoke({ ...input, operation: "customers.create", input: { input: bad } }), { code: "connector_input_invalid" });
307
+ assert.equal(requests.length, count);
308
+ }
309
+ state.response = { data: { customerCreate: { didSucceed: false, inputErrors: [{ code: "INVALID", path: ["input", "email"] }], customer: null } } };
310
+ let count = requests.length;
311
+ await assert.rejects(service.invoke({ ...input, operation: "customers.create", input: { input: details } }), { code: "connector_input_invalid" });
312
+ assert.equal(requests.length, count + 1);
313
+ state.response = { data: { customerCreate: { didSucceed: true, inputErrors: [], customer: { ...created, business: { id: "another-business" } } } } };
314
+ await assert.rejects(service.invoke({ ...input, operation: "customers.create", input: { input: details } }), { code: "connector_response_invalid" });
315
+ state.status = 503;
316
+ count = requests.length;
317
+ await assert.rejects(service.invoke({ ...input, operation: "customers.create", input: { input: details } }));
318
+ assert.equal(requests.length, count + 1);
319
+ });
320
+
321
+ test("Wave customer writes require their own granted scope", async t => {
322
+ const { service, requests, connect } = await fixture(t);
323
+ await connect();
324
+ const count = requests.length;
325
+ await assert.rejects(service.invoke({ ...input, operation: "customers.create", input: { input: { businessId, name: "Denied" } } }),
326
+ { code: "connector_scope_missing" });
327
+ assert.equal(requests.length, count);
328
+ });
329
+
330
+
331
+ test("Wave accounting selectors preserve decimal values and enforce business, page and scope bounds", async t => {
332
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "account:read", "product:read", "sales_tax:read", "vendor:read"]);
333
+ await connect();
334
+ const records = {
335
+ accounts: { id: "account-one", name: "Services", currency: { code: "USD" }, type: { value: "INCOME" }, subtype: { value: "INCOME" }, isArchived: false, balance: "9007199254740993.25" },
336
+ products: { id: "product-one", name: "Grooming", description: null, unitPrice: "75.123456789", isSold: true, isBought: false, incomeAccount: { id: "account-one" }, expenseAccount: null },
337
+ salesTaxes: { id: "tax-one", name: "Sales tax", abbreviation: "GST", rate: "0.1", isCompound: false, isRecoverable: true, isArchived: false },
338
+ vendors: { id: "vendor-one", name: "Supplies", email: null, phone: "+10000000000" }
339
+ };
340
+ for (const [resource, record] of Object.entries(records)) {
341
+ state.response = { data: { business: { id: businessId, [resource]: page([record], 2, 3, 3) } } };
342
+ const result = await service.invoke({ ...input, operation: `${resource}.list`, input: { businessId, page: 2, pageSize: 2 } });
343
+ assert.deepEqual(result.data.business[resource].edges[0].node, record);
344
+ const wire = JSON.parse(requests.at(-1).init.body);
345
+ assert.deepEqual(wire.variables, { businessId, page: 2, pageSize: 2 });
346
+ assert.ok(wire.query.includes(`${resource}(page: $page`));
347
+ if (resource === "products") assert.match(wire.query, /sort: \[NAME_ASC\]/);
348
+ state.response.data.business.id = "another-business";
349
+ await assert.rejects(service.invoke({ ...input, operation: `${resource}.list`, input: { businessId } }), { code: "connector_response_invalid" });
350
+ state.response = { data: { business: { id: businessId, [resource]: page([]) } } };
351
+ assert.deepEqual((await service.invoke({ ...input, operation: `${resource}.list`, input: { businessId } })).data.business[resource].edges, []);
352
+ }
353
+ state.response = { data: { business: { id: businessId, products: page([{ ...records.products, unitPrice: 75.1 }]) } } };
354
+ await assert.rejects(service.invoke({ ...input, operation: "products.list", input: { businessId } }), { code: "connector_response_invalid" });
355
+ state.response = null;
356
+ await service.invoke({ ...input, operation: "invoices.list", input: { businessId } });
357
+ assert.match(JSON.parse(requests.at(-1).init.body).query, /sort: \[CREATED_AT_DESC\]/);
358
+ });
359
+
360
+
361
+ test("Wave updates customers only after verifying the selected business, with one deliberate mutation", async t => {
362
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "customer:write"]);
363
+ await connect();
364
+ const owner = { data: { business: { id: businessId, customer: { id: customer.id, business: { id: businessId } } } } };
365
+ const updated = { data: { customerPatch: { didSucceed: true, inputErrors: [], customer: { ...customer, name: "Updated", business: { id: businessId } } } } };
366
+ const values = { businessId, input: { id: customer.id, name: "Updated" } };
367
+ state.responses = [owner, updated];
368
+ assert.deepEqual(await service.invoke({ ...input, operation: "customers.update", input: values }), updated);
369
+ const preflight = JSON.parse(requests.at(-2).init.body);
370
+ assert.match(preflight.query, /^query ConnectorWaveCustomerOwner/);
371
+ assert.deepEqual(preflight.variables, { businessId, id: customer.id });
372
+ const write = JSON.parse(requests.at(-1).init.body);
373
+ assert.match(write.query, /^mutation ConnectorWaveCustomerPatch/);
374
+ assert.deepEqual(write.variables, { input: values.input });
375
+ state.responses = [{ data: { business: { id: businessId, customer: null } } }];
376
+ let count = requests.length;
377
+ await assert.rejects(service.invoke({ ...input, operation: "customers.update", input: values }), { code: "connector_resource_not_found" });
378
+ assert.equal(requests.length, count + 1);
379
+ count = requests.length;
380
+ await assert.rejects(service.invoke({ ...input, operation: "customers.update", input: { businessId, input: { id: customer.id } } }), { code: "connector_input_invalid" });
381
+ assert.equal(requests.length, count);
382
+ state.responses = [owner, { data: { customerPatch: { didSucceed: false, inputErrors: [{ code: "INVALID", path: ["name"] }], customer: null } } }];
383
+ count = requests.length;
384
+ await assert.rejects(service.invoke({ ...input, operation: "customers.update", input: values }), { code: "connector_input_invalid" });
385
+ assert.equal(requests.length, count + 2);
386
+ });
387
+
388
+ test("Wave invoice draft, edit, approve, send and read preserve money and enforce deliberate transitions", async t => {
389
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "invoice:write", "invoice:send"]);
390
+ await connect();
391
+ const owner = { data: { business: { id: businessId, customer: { id: customer.id, business: { id: businessId } } } } };
392
+ const record = { ...invoice, business: { id: businessId }, customer: { id: customer.id },
393
+ total: { value: "112.50000001", currency: { code: "USD" } }, amountDue: { value: "112.50000001", currency: { code: "USD" } },
394
+ pdfUrl: "https://example.test/private-invoice.pdf", viewUrl: "https://example.test/invoice" };
395
+ const outcome = (action, status = "DRAFT") => ({ data: { [`invoice${action}`]: { didSucceed: true, inputErrors: [], invoice: { ...record, status } } } });
396
+ const invoiceOwner = (status = "DRAFT", enabled = true) => ({ data: { business: { id: businessId, emailSendEnabled: enabled,
397
+ invoice: { id: invoice.id, status, business: { id: businessId } } } } });
398
+ const details = { customerId: customer.id, invoiceDate: "2026-09-13", items: [{ productId: "product-one", quantity: "1.5", unitPrice: "75.00000001", taxes: [] }] };
399
+ state.responses = [owner, outcome("Create")];
400
+ const created = await service.invoke({ ...input, operation: "invoices.create", input: { businessId, input: details } });
401
+ assert.equal(created.data.invoiceCreate.invoice.total.value, "112.50000001");
402
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: { ...details, status: "DRAFT", businessId } });
403
+ assert.match(JSON.parse(requests.at(-1).init.body).query, /^mutation ConnectorWaveInvoiceCreate/);
404
+ state.responses = [invoiceOwner(), outcome("Patch")];
405
+ await service.invoke({ ...input, operation: "invoices.update", input: { businessId, input: { id: invoice.id, memo: "Appointment completed" } } });
406
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: { id: invoice.id, memo: "Appointment completed" } });
407
+ state.responses = [invoiceOwner(), outcome("Approve", "SAVED")];
408
+ await service.invoke({ ...input, operation: "invoices.approve", input: { businessId, input: { invoiceId: invoice.id } } });
409
+ assert.match(JSON.parse(requests.at(-1).init.body).query, /^mutation ConnectorWaveInvoiceApprove/);
410
+ const send = { invoiceId: invoice.id, to: ["customer@example.test"], attachPDF: true, message: "Your invoice" };
411
+ state.responses = [invoiceOwner("SAVED"), { data: { invoiceSend: { didSucceed: true, inputErrors: [] } } }];
412
+ const sent = await service.invoke({ ...input, operation: "invoices.send", input: { businessId, input: send } });
413
+ assert.equal(sent.data.invoiceSend.didSucceed, true);
414
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: send });
415
+ state.responses = [{ data: { business: { id: businessId, invoice: record } } }];
416
+ assert.equal((await service.invoke({ ...input, operation: "invoices.get", input: { businessId, invoiceId: invoice.id } })).data.business.invoice.pdfUrl, record.pdfUrl);
417
+ state.responses = [{ data: { business: { id: businessId, invoice: { ...record, business: { id: "different-business" } } } } }];
418
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.get", input: { businessId, invoiceId: invoice.id } }),
419
+ { code: "connector_response_invalid" });
420
+ for (const preflight of [invoiceOwner("DRAFT"), invoiceOwner("SAVED", false), invoiceOwner(null), invoiceOwner(42), invoiceOwner("")]) {
421
+ state.responses = [preflight]; const count = requests.length;
422
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.send", input: { businessId, input: send } }), { code: "connector_input_invalid" });
423
+ assert.equal(requests.length, count + 1);
424
+ }
425
+ state.responses = [invoiceOwner("SAVED"), { data: { invoiceSend: { didSucceed: false, inputErrors: [{ code: "INVALID", path: ["to"] }] } } }];
426
+ const count = requests.length;
427
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.send", input: { businessId, input: send } }), { code: "connector_input_invalid" });
428
+ assert.equal(requests.length, count + 2);
429
+ });
430
+
431
+ test("Wave invoice validation and ownership failures stop writes before mutation", async t => {
432
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "invoice:write", "invoice:send"]);
433
+ await connect();
434
+ const details = { customerId: customer.id, items: [{ productId: "product-one", unitPrice: "10.25" }] };
435
+ for (const values of [{ ...details, invoiceDate: "2026-02-30" }, { ...details, items: [] },
436
+ { ...details, items: [{ productId: "product-one", unitPrice: 10.25 }] },
437
+ { ...details, items: [{ productId: "product-one", unitPrice: "1.123456789" }] }]) {
438
+ const count = requests.length;
439
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.create", input: { businessId, input: values } }), { code: "connector_input_invalid" });
440
+ assert.equal(requests.length, count);
441
+ }
442
+ state.responses = [{ data: { business: { id: businessId, customer: null } } }];
443
+ let count = requests.length;
444
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.create", input: { businessId, input: details } }), { code: "connector_resource_not_found" });
445
+ assert.equal(requests.length, count + 1);
446
+ state.responses = [{ data: { business: { id: businessId, invoice: { id: invoice.id, business: { id: "different-business" } }, emailSendEnabled: true } } }];
447
+ count = requests.length;
448
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.approve", input: { businessId, input: { invoiceId: invoice.id } } }), { code: "connector_resource_not_found" });
449
+ assert.equal(requests.length, count + 1);
450
+ count = requests.length;
451
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.update", input: { businessId, input: { id: invoice.id } } }), { code: "connector_input_invalid" });
452
+ assert.equal(requests.length, count);
453
+ });
454
+
455
+ test("Wave estimates create, update, approve, send and read use their distinct schema", async t => {
456
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "estimate:*"]);
457
+ await connect();
458
+ const record = { id: "estimate-one", estimateNumber: "E-1", estimateDate: "2026-09-13", dueDate: "2026-10-13", title: "Estimate",
459
+ status: "DRAFT", exchangeRate: "1", currency: { code: "USD" }, customer: { id: customer.id, business: { id: businessId } },
460
+ total: { value: "10.25000001", currency: { code: "USD" } }, amountDue: { value: "10.25000001", currency: { code: "USD" } }, pdfUrl: null, viewUrl: null };
461
+ const owner = { data: { business: { id: businessId, customer: record.customer } } };
462
+ const existing = status => ({ data: { business: { id: businessId, estimate: { ...record, status } } } });
463
+ const outcome = (action, status = "DRAFT") => ({ data: { [`estimate${action}`]: { didSucceed: true, inputErrors: [], estimate: { ...record, status } } } });
464
+ const details = { customerId: customer.id, items: [{ productId: "product-one", unitPrice: "10.25000001", taxes: [] }] };
465
+ state.responses = [owner, outcome("Create")];
466
+ assert.equal((await service.invoke({ ...input, operation: "estimates.create", input: { businessId, input: details } })).data.estimateCreate.estimate.total.value, "10.25000001");
467
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: { ...details, status: "DRAFT", businessId } });
468
+ assert.match(JSON.parse(requests.at(-1).init.body).query, /\$input: EstimateCreateInput!/);
469
+ const patch = { id: record.id, customerId: customer.id, status: "DRAFT", title: "Revised estimate", currency: "USD",
470
+ estimateDate: record.estimateDate, dueDate: record.dueDate, exchangeRate: "1", memo: "Updated" };
471
+ state.responses = [existing("DRAFT"), owner, outcome("Patch")];
472
+ await service.invoke({ ...input, operation: "estimates.update", input: { businessId, input: patch } });
473
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: patch });
474
+ state.responses = [existing("DRAFT"), outcome("Approve", "APPROVED")];
475
+ await service.invoke({ ...input, operation: "estimates.approve", input: { businessId, input: { estimateId: record.id } } });
476
+ assert.match(JSON.parse(requests.at(-1).init.body).query, /estimateApprove/);
477
+ const send = { estimateId: record.id, to: ["customer@example.test"], attachPDF: true };
478
+ state.responses = [existing("APPROVED"), { data: { estimateSend: { didSucceed: true, inputErrors: [] } } }];
479
+ assert.equal((await service.invoke({ ...input, operation: "estimates.send", input: { businessId, input: send } })).data.estimateSend.didSucceed, true);
480
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: send });
481
+ state.responses = [existing("APPROVED")];
482
+ assert.equal((await service.invoke({ ...input, operation: "estimates.get", input: { businessId, estimateId: record.id } })).data.business.estimate.id, record.id);
483
+ state.responses = [{ data: { business: { id: businessId, estimates: page([record]) } } }];
484
+ await service.invoke({ ...input, operation: "estimates.list", input: { businessId } });
485
+ assert.match(JSON.parse(requests.at(-1).init.body).query, /sort: CREATED_AT_DESC/);
486
+ for (const status of ["DRAFT", "DELETED", "CONVERTED", null]) {
487
+ state.responses = [existing(status)]; const count = requests.length;
488
+ await assert.rejects(service.invoke({ ...input, operation: "estimates.send", input: { businessId, input: send } }), { code: "connector_input_invalid" });
489
+ assert.equal(requests.length, count + 1);
490
+ }
491
+ state.responses = [existing("APPROVED"), { data: { estimateSend: { didSucceed: false, inputErrors: [{ code: "INVALID", path: ["to"] }] } } }];
492
+ const count = requests.length;
493
+ await assert.rejects(service.invoke({ ...input, operation: "estimates.send", input: { businessId, input: send } }), { code: "connector_input_invalid" });
494
+ assert.equal(requests.length, count + 2);
495
+ });
496
+
497
+ test("Wave estimates reject incomplete accounting values and cross-business results", async t => {
498
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "estimate:*"]);
499
+ await connect();
500
+ for (const values of [
501
+ { customerId: customer.id, items: [{ productId: "product-one" }] },
502
+ { customerId: customer.id, items: [{ productId: "product-one", unitPrice: 10.25 }] },
503
+ { customerId: customer.id, status: "SAVED", items: [{ productId: "product-one", unitPrice: "10" }] }
504
+ ]) {
505
+ const count = requests.length;
506
+ await assert.rejects(service.invoke({ ...input, operation: "estimates.create", input: { businessId, input: values } }), { code: "connector_input_invalid" });
507
+ assert.equal(requests.length, count);
508
+ }
509
+ let count = requests.length;
510
+ await assert.rejects(service.invoke({ ...input, operation: "estimates.update", input: { businessId, input: { id: "estimate-one", memo: "Insufficient" } } }), { code: "connector_input_invalid" });
511
+ assert.equal(requests.length, count);
512
+ const wrong = { id: "estimate-one", customer: { id: customer.id, business: { id: "other-business" } }, status: "APPROVED" };
513
+ state.responses = [{ data: { business: { id: businessId, estimate: wrong } } }];
514
+ count = requests.length;
515
+ await assert.rejects(service.invoke({ ...input, operation: "estimates.approve", input: { businessId, input: { estimateId: wrong.id } } }), { code: "connector_resource_not_found" });
516
+ assert.equal(requests.length, count + 1);
517
+ state.responses = [{ data: { business: { id: businessId, estimate: wrong } } }];
518
+ await assert.rejects(service.invoke({ ...input, operation: "estimates.get", input: { businessId, estimateId: wrong.id } }), { code: "connector_response_invalid" });
519
+ state.responses = [{ data: { business: { id: businessId, estimates: page([wrong]) } } }];
520
+ await assert.rejects(service.invoke({ ...input, operation: "estimates.list", input: { businessId } }), { code: "connector_response_invalid" });
521
+ });
522
+
523
+ test("Wave product writes verify related records and preserve explicit decimal prices", async t => {
524
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "product:*", "account:read", "sales_tax:read"]);
525
+ await connect();
526
+ const owner = id => ({ data: { business: { id: businessId, record: { id, business: { id: businessId } } } } });
527
+ const record = { id: "product-one", name: "Grooming", unitPrice: "125.12345", business: { id: businessId } };
528
+ const outcome = action => ({ data: { [`product${action}`]: { didSucceed: true, inputErrors: [], product: record } } });
529
+ const details = { name: record.name, unitPrice: record.unitPrice, incomeAccountId: "income-one", defaultSalesTaxIds: ["tax-one"] };
530
+ state.responses = [owner("income-one"), owner("tax-one"), outcome("Create")];
531
+ const created = await service.invoke({ ...input, operation: "products.create", input: { businessId, input: details } });
532
+ assert.equal(created.data.productCreate.product.unitPrice, record.unitPrice);
533
+ assert.match(JSON.parse(requests.at(-3).init.body).query, /record: account/);
534
+ assert.match(JSON.parse(requests.at(-2).init.body).query, /record: salesTax/);
535
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: { ...details, businessId } });
536
+ state.responses = [owner(record.id), outcome("Patch")];
537
+ await service.invoke({ ...input, operation: "products.update", input: { businessId, input: { id: record.id, defaultSalesTaxIds: [] } } });
538
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: { id: record.id, defaultSalesTaxIds: [] } });
539
+ for (const bad of [{ ...details, unitPrice: 125 }, { ...details, unitPrice: "1.123456" }, { ...details, defaultSalesTaxIds: ["tax-one", "tax-one"] }]) {
540
+ const count = requests.length;
541
+ await assert.rejects(service.invoke({ ...input, operation: "products.create", input: { businessId, input: bad } }), { code: "connector_input_invalid" });
542
+ assert.equal(requests.length, count);
543
+ }
544
+ state.responses = [{ data: { business: { id: businessId, record: { id: "income-one", business: { id: "other" } } } } }];
545
+ let count = requests.length;
546
+ await assert.rejects(service.invoke({ ...input, operation: "products.create", input: { businessId, input: details } }), { code: "connector_resource_not_found" });
547
+ assert.equal(requests.length, count + 1);
548
+ state.responses = [owner(record.id), { data: { productPatch: { didSucceed: false, inputErrors: [{ code: "INVALID", path: ["name"] }], product: null } } }];
549
+ count = requests.length;
550
+ await assert.rejects(service.invoke({ ...input, operation: "products.update", input: { businessId, input: { id: record.id, name: "New" } } }), { code: "connector_input_invalid" });
551
+ assert.equal(requests.length, count + 2);
552
+ state.responses = [owner(record.id), { data: { productPatch: { didSucceed: true, inputErrors: [], product: { ...record, id: "different-product" } } } }];
553
+ await assert.rejects(service.invoke({ ...input, operation: "products.update", input: { businessId, input: { id: record.id, name: "New" } } }), { code: "connector_response_invalid" });
554
+ });
555
+
556
+ test("Wave accounting writes preserve account revisions and dated decimal tax rates", async t => {
557
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "account:*", "sales_tax:*"]);
558
+ await connect();
559
+ const account = { id: "account-one", name: "Grooming revenue", sequence: 2, currency: { code: "USD" }, subtype: { value: "INCOME" }, isArchived: false, business: { id: businessId } };
560
+ const tax = { id: "tax-one", name: "Sales tax", abbreviation: "GST", rate: "0.1", rates: [{ effective: "2026-01-01", rate: "0.1" }], isArchived: false, business: { id: businessId } };
561
+ for (const [resource, plural, record, create, patch] of [
562
+ ["account", "accounts", account, { name: account.name, subtype: "INCOME" }, { id: account.id, sequence: 2, name: "Services" }],
563
+ ["salesTax", "salesTaxes", tax, { name: tax.name, abbreviation: "GST", rate: "0.1" }, { id: tax.id, rates: [{ effective: "2026-10-01", rate: "0.15" }] }]
564
+ ]) {
565
+ const outcome = action => ({ data: { [`${resource}${action}`]: { didSucceed: true, inputErrors: [], [resource]: record } } });
566
+ const owner = { data: { business: { id: businessId, record } } };
567
+ state.responses = [outcome("Create")];
568
+ await service.invoke({ ...input, operation: `${plural}.create`, input: { businessId, input: create } });
569
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: { ...create, businessId } });
570
+ state.responses = [{ data: { business: { id: businessId, [resource]: record } } }];
571
+ assert.deepEqual((await service.invoke({ ...input, operation: `${plural}.get`, input: { businessId, [`${resource}Id`]: record.id } })).data.business[resource], record);
572
+ state.responses = [owner, outcome("Patch")];
573
+ await service.invoke({ ...input, operation: `${plural}.update`, input: { businessId, input: patch } });
574
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: patch });
575
+ state.responses = [{ data: { business: { id: businessId, record: { ...record, business: { id: "other" } } } } }];
576
+ let count = requests.length;
577
+ await assert.rejects(service.invoke({ ...input, operation: `${plural}.update`, input: { businessId, input: patch } }), { code: "connector_resource_not_found" });
578
+ assert.equal(requests.length, count + 1);
579
+ state.responses = [owner, { data: { [`${resource}Patch`]: { didSucceed: false, inputErrors: [{ code: "INVALID", path: ["name"] }] } } }];
580
+ count = requests.length;
581
+ await assert.rejects(service.invoke({ ...input, operation: `${plural}.update`, input: { businessId, input: patch } }), { code: "connector_input_invalid" });
582
+ assert.equal(requests.length, count + 2);
583
+ }
584
+ state.responses = [{ data: { business: { id: businessId, record: account } } }];
585
+ let count = requests.length;
586
+ await assert.rejects(service.invoke({ ...input, operation: "accounts.update", input: { businessId, input: { id: account.id, sequence: 1, name: "Stale" } } }), { code: "connector_conflict" });
587
+ assert.equal(requests.length, count + 1);
588
+ for (const rate of [0.1, "0.1234567", "-0.1"]) {
589
+ count = requests.length;
590
+ await assert.rejects(service.invoke({ ...input, operation: "salesTaxes.create", input: { businessId, input: { name: "GST", abbreviation: "GST", rate } } }), { code: "connector_input_invalid" });
591
+ assert.equal(requests.length, count);
592
+ }
593
+ count = requests.length;
594
+ await assert.rejects(service.invoke({ ...input, operation: "accounts.create", input: { businessId, input: { name: "System", subtype: "UNKNOWN_ACCOUNT" } } }), { code: "connector_input_invalid" });
595
+ await assert.rejects(service.invoke({ ...input, operation: "salesTaxes.update", input: { businessId, input: { id: tax.id, rates: [{ effective: "2026-02-30", rate: "0.1" }] } } }), { code: "connector_input_invalid" });
596
+ assert.equal(requests.length, count);
597
+ });
598
+
599
+ test("Wave records deliberate money transactions with related ownership checks and no write replay", async t => {
600
+ const { service, state, requests, connect } = await fixture(t, [...scopes, "transaction:write", "account:read", "sales_tax:read"]);
601
+ await connect();
602
+ const owner = id => ({ data: { business: { id: businessId, record: { id, business: { id: businessId } } } } });
603
+ const details = { externalId: "receipt-2026-001", date: "2026-09-13", description: "Grooming sale",
604
+ anchor: { accountId: "bank", amount: "110.00", direction: "DEPOSIT" },
605
+ lineItems: [{ accountId: "sales", amount: "100.00", balance: "INCREASE", customerId: customer.id,
606
+ taxes: [{ salesTaxId: "gst", amount: "10.00" }] }] };
607
+ const related = () => [owner("bank"), owner("sales"), owner(customer.id), owner("gst")];
608
+ const accepted = { data: { moneyTransactionCreate: { didSucceed: true, inputErrors: [], transaction: { id: "transaction-one" } } } };
609
+ state.responses = [...related(), accepted];
610
+ const created = await service.invoke({ ...input, operation: "transactions.create", input: { businessId, input: details } });
611
+ assert.equal(created.data.moneyTransactionCreate.transaction.id, "transaction-one");
612
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { input: { ...details, businessId } });
613
+ assert.match(JSON.parse(requests.at(-1).init.body).query, /MoneyTransactionCreateInput!/);
614
+ for (const bad of [{ ...details, externalId: "" }, { ...details, lineItems: [] },
615
+ { ...details, anchor: { ...details.anchor, amount: 110 } },
616
+ { ...details, anchor: { ...details.anchor, amount: "-110" } },
617
+ { ...details, anchor: { ...details.anchor, amount: "1.001" } }]) {
618
+ const count = requests.length;
619
+ await assert.rejects(service.invoke({ ...input, operation: "transactions.create", input: { businessId, input: bad } }), { code: "connector_input_invalid" });
620
+ assert.equal(requests.length, count);
621
+ }
622
+ state.responses = [{ data: { business: { id: businessId, record: { id: "bank", business: { id: "other" } } } } }];
623
+ let count = requests.length;
624
+ await assert.rejects(service.invoke({ ...input, operation: "transactions.create", input: { businessId, input: details } }), { code: "connector_resource_not_found" });
625
+ assert.equal(requests.length, count + 1);
626
+ state.responses = [...related(), { data: { moneyTransactionCreate: { didSucceed: false, inputErrors: [{ code: "UNBALANCED", path: ["lineItems"] }], transaction: null } } }];
627
+ count = requests.length;
628
+ await assert.rejects(service.invoke({ ...input, operation: "transactions.create", input: { businessId, input: details } }), { code: "connector_input_invalid" });
629
+ assert.equal(requests.length, count + 5);
630
+ assert.equal(requests.slice(count).filter(r => JSON.parse(r.init.body).query.startsWith("mutation ")).length, 1);
631
+ });
632
+
633
+
634
+ test("Wave ownership preflights preserve reconnect errors and never submit the write", async (t) => {
635
+ for (const [operation, args] of [
636
+ ["customers.update", { businessId, input: { id: customer.id, name: "Changed" } }],
637
+ ["products.update", { businessId, input: { id: "product-one", name: "Changed" } }],
638
+ ["accounts.update", { businessId, input: { id: "account-one", sequence: 1, name: "Changed" } }],
639
+ ["invoices.approve", { businessId, input: { invoiceId: invoice.id } }],
640
+ ["estimates.approve", { businessId, input: { estimateId: "estimate-one" } }]
641
+ ]) {
642
+ const { service, state, requests, connect } = await fixture(t, ["user:read", "business:read", "customer:*", "product:*", "account:*", "invoice:*", "estimate:*"]);
643
+ await connect();
644
+ state.response = { errors: [{ message: "fixture-wave-secret", extensions: { code: "UNAUTHENTICATED" } }] };
645
+ const before = requests.length;
646
+ await assert.rejects(service.invoke({ ...input, operation, input: args }), error => {
647
+ assert.equal(error.code, "connector_reconnect_required");
648
+ assert.equal(error.message.includes("fixture-wave-secret"), false);
649
+ return true;
650
+ });
651
+ assert.equal(requests.length, before + 1);
652
+ assert.equal((await service.status(input)).status, "reconnect-required");
653
+ }
654
+ });
655
+
656
+
657
+ test("Wave archives an account only in its verified business, with no write retry", async (t) => {
658
+ const { service, state, requests, connect } = await fixture(t, ["user:read", "business:read", "account:*"]);
659
+ await connect();
660
+ const owner = { data: { business: { id: businessId, record: { id: "account-one", business: { id: businessId }, sequence: 4 } } } };
661
+ const invoke = () => service.invoke({ ...input, operation: "accounts.archive", input: { businessId, input: { id: "account-one" } } });
662
+ state.responses = [owner, { data: { accountArchive: { didSucceed: true, inputErrors: [] } } }];
663
+ const before = requests.length;
664
+ await invoke();
665
+ assert.equal(requests.length, before + 2);
666
+ const body = JSON.parse(requests.at(-1).init.body);
667
+ assert.deepEqual(body.variables, { input: { id: "account-one" } });
668
+ assert.match(body.query, /accountArchive/);
669
+ assert.doesNotMatch(body.query, /account \{/);
670
+ state.response = { data: { business: { id: "another-business" } } };
671
+ const denied = requests.length;
672
+ await assert.rejects(invoke(), { code: "connector_resource_not_found" });
673
+ assert.equal(requests.length, denied + 1);
674
+ state.responses = [owner, { data: { accountArchive: { didSucceed: false, inputErrors: [{ code: "FORBIDDEN", path: ["id"] }] } } }];
675
+ const rejected = requests.length;
676
+ await assert.rejects(invoke(), { code: "connector_input_invalid" });
677
+ assert.equal(requests.length, rejected + 2);
678
+ });