@jini-ai/agent-plugins 0.6.17 → 0.6.19

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 (335) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/NOTICE +14 -0
  3. package/README.md +35 -14
  4. package/charts-echarts/plugin.json +39 -0
  5. package/charts-echarts/skills/charts-echarts/SKILL.md +110 -0
  6. package/charts-recharts/plugin.json +32 -0
  7. package/charts-recharts/skills/charts-recharts/SKILL.md +108 -0
  8. package/cloudflare/NOTES.md +16 -0
  9. package/cloudflare/form-destinations/cloudflare-d1.mjs +9 -0
  10. package/cloudflare/lib/api.mjs +96 -0
  11. package/cloudflare/lib/assets.mjs +65 -0
  12. package/cloudflare/lib/dns.mjs +32 -0
  13. package/cloudflare/lib/domain-inventory.mjs +61 -0
  14. package/cloudflare/lib/domain-status.mjs +48 -0
  15. package/cloudflare/lib/domains.mjs +95 -0
  16. package/cloudflare/lib/forms.mjs +254 -0
  17. package/cloudflare/lib/pages-upload.mjs +51 -0
  18. package/cloudflare/lib/target.mjs +165 -0
  19. package/cloudflare/lib/workers-upload.mjs +48 -0
  20. package/cloudflare/lib/zones.mjs +25 -0
  21. package/cloudflare/mcp.json +4 -0
  22. package/cloudflare/plugin.json +96 -0
  23. package/cloudflare/skills/cloudflare/SKILL.md +57 -0
  24. package/cloudflare/skills/cloudflare/references/domains.md +31 -0
  25. package/cloudflare/skills/cloudflare/references/pages.md +18 -0
  26. package/cloudflare/skills/cloudflare/references/permissions.md +29 -0
  27. package/cloudflare/skills/cloudflare/references/troubleshooting.md +20 -0
  28. package/cloudflare/skills/cloudflare/references/workers.md +42 -0
  29. package/cloudflare/targets/cloudflare-pages.mjs +12 -0
  30. package/cloudflare/targets/cloudflare-workers.mjs +17 -0
  31. package/cloudflare/tovu-consent.json +6 -0
  32. package/cloudflare/tovu-deploy-targets.json +630 -0
  33. package/cloudflare/tovu-form-destinations.json +12 -0
  34. package/cloudflare/worker/forms-receiver.mjs +249 -0
  35. package/composio/mcp.json +10 -0
  36. package/composio/plugin.json +68 -0
  37. package/composio/skills/composio/SKILL.md +85 -0
  38. package/dist/core/identity-namespace.d.ts +12 -0
  39. package/dist/core/identity-namespace.js +20 -0
  40. package/dist/folder-zip-node.js +29 -7
  41. package/dist/lifecycle/bundled-digests.js +4 -3
  42. package/dist/lifecycle/install.d.ts +7 -0
  43. package/dist/lifecycle/install.js +45 -3
  44. package/dist/lifecycle/manifest.d.ts +2 -2
  45. package/dist/lifecycle/manifest.js +5 -3
  46. package/dist/lifecycle/node.js +2 -0
  47. package/dist/lifecycle/persistent-state.js +1 -1
  48. package/dist/lifecycle/ports.d.ts +8 -0
  49. package/dist/lifecycle/seed-bundled.js +4 -1
  50. package/dist/manifest.d.ts +1 -1
  51. package/dist/manifest.js +3 -2
  52. package/fly/deploy-configs/fly.mjs +115 -0
  53. package/fly/deploy-ops/fly.mjs +102 -0
  54. package/fly/mcp.json +4 -0
  55. package/fly/plugin.json +81 -0
  56. package/fly/scripts/render-deploy-workflow.mjs +150 -0
  57. package/fly/skills/fly/SKILL.md +43 -0
  58. package/fly/skills/fly/references/fly-deploy.template.yml +117 -0
  59. package/fly/skills/fly/references/fly.template.toml +81 -0
  60. package/fly/tovu-credential-schemes.json +11 -0
  61. package/fly/tovu-deploy-configs.json +21 -0
  62. package/fly/tovu-deploy-ops.json +14 -0
  63. package/github/deploy-ops/github-actions.mjs +164 -0
  64. package/github/deployments/github.mjs +415 -0
  65. package/github/mcp.json +4 -0
  66. package/github/plugin.json +105 -0
  67. package/github/skills/github/SKILL.md +249 -0
  68. package/github/skills/github/references/actions.md +224 -0
  69. package/github/skills/github/references/auth-and-tokens.md +140 -0
  70. package/github/skills/github/references/repo-files.md +135 -0
  71. package/github/skills/github/references/site-backup.md +159 -0
  72. package/github/skills/github/references/source-control.md +123 -0
  73. package/github/source-control/backup-push.mjs +242 -0
  74. package/github/source-control/commit-site.mjs +459 -0
  75. package/github/source-control/github.mjs +93 -0
  76. package/github/source-control/write-files.mjs +468 -0
  77. package/github/tovu-deploy-ops.json +13 -0
  78. package/github/tovu-source-control.json +117 -0
  79. package/github-pages/NOTES.md +19 -0
  80. package/github-pages/mcp.json +4 -0
  81. package/github-pages/plugin.json +48 -0
  82. package/github-pages/skills/github-pages/SKILL.md +44 -0
  83. package/github-pages/targets/github-pages.mjs +543 -0
  84. package/github-pages/tovu-deploy-targets.json +327 -0
  85. package/google-login/exports/google-g.svg +1 -0
  86. package/google-login/exports/sign-in-button.css +68 -0
  87. package/google-login/exports/sign-in-button.html +4 -0
  88. package/google-login/mcp.json +4 -0
  89. package/google-login/plugin.json +59 -0
  90. package/google-login/skills/google-login/SKILL.md +96 -0
  91. package/google-login/tovu-login-providers.json +21 -0
  92. package/google-workspace/README.md +132 -0
  93. package/google-workspace/form-destinations/README.md +87 -0
  94. package/google-workspace/form-destinations/dedupe.mjs +43 -0
  95. package/google-workspace/form-destinations/delivery-result.mjs +69 -0
  96. package/google-workspace/form-destinations/mapping.mjs +152 -0
  97. package/google-workspace/form-destinations/sheets.mjs +202 -0
  98. package/google-workspace/lib/auth.mjs +81 -0
  99. package/google-workspace/lib/drive-files.mjs +54 -0
  100. package/google-workspace/lib/form-destination-settings.mjs +72 -0
  101. package/google-workspace/lib/http.mjs +168 -0
  102. package/google-workspace/lib/picker-flow.mjs +171 -0
  103. package/google-workspace/lib/picker-selection.mjs +107 -0
  104. package/google-workspace/lib/product-state.mjs +93 -0
  105. package/google-workspace/lib/request-policy.mjs +163 -0
  106. package/google-workspace/lib/sheets-a1.mjs +79 -0
  107. package/google-workspace/lib/sheets-admin-csv.mjs +82 -0
  108. package/google-workspace/lib/sheets-admin-request-kinds.mjs +183 -0
  109. package/google-workspace/lib/sheets-client.mjs +286 -0
  110. package/google-workspace/lib/surface-actions.mjs +107 -0
  111. package/google-workspace/mcp.json +4 -0
  112. package/google-workspace/plugin.json +77 -0
  113. package/google-workspace/products.json +12 -0
  114. package/google-workspace/skills/google-workspace/SKILL.md +82 -0
  115. package/google-workspace/skills/google-workspace/references/connection.md +102 -0
  116. package/google-workspace/skills/google-workspace/references/form-delivery.md +50 -0
  117. package/google-workspace/skills/google-workspace/references/sheets.md +149 -0
  118. package/google-workspace/skills/google-workspace/references/troubleshooting.md +31 -0
  119. package/google-workspace/tools/account-info.mjs +18 -0
  120. package/google-workspace/tools/sheets/README.md +54 -0
  121. package/google-workspace/tools/sheets/copy-tab.mjs +23 -0
  122. package/google-workspace/tools/sheets/copy.mjs +23 -0
  123. package/google-workspace/tools/sheets/create.mjs +35 -0
  124. package/google-workspace/tools/sheets/find-replace.mjs +38 -0
  125. package/google-workspace/tools/sheets/find.mjs +52 -0
  126. package/google-workspace/tools/sheets/get-by-data-filter.mjs +23 -0
  127. package/google-workspace/tools/sheets/get.mjs +22 -0
  128. package/google-workspace/tools/sheets/values-append.mjs +27 -0
  129. package/google-workspace/tools/sheets/values-batch-clear.mjs +20 -0
  130. package/google-workspace/tools/sheets/values-batch-get.mjs +20 -0
  131. package/google-workspace/tools/sheets/values-batch-update.mjs +29 -0
  132. package/google-workspace/tools/sheets/values-by-data-filter.mjs +62 -0
  133. package/google-workspace/tools/sheets/values-clear.mjs +20 -0
  134. package/google-workspace/tools/sheets/values-get.mjs +21 -0
  135. package/google-workspace/tools/sheets/values-update.mjs +26 -0
  136. package/google-workspace/tools/sheets-admin/README.md +45 -0
  137. package/google-workspace/tools/sheets-admin/basic-filter.mjs +32 -0
  138. package/google-workspace/tools/sheets-admin/batch-update.mjs +25 -0
  139. package/google-workspace/tools/sheets-admin/developer-metadata.mjs +28 -0
  140. package/google-workspace/tools/sheets-admin/export-csv.mjs +67 -0
  141. package/google-workspace/tools/sheets-admin/filter-views.mjs +38 -0
  142. package/google-workspace/tools/sheets-admin/format.mjs +70 -0
  143. package/google-workspace/tools/sheets-admin/named-ranges.mjs +42 -0
  144. package/google-workspace/tools/sheets-admin/open-existing.mjs +34 -0
  145. package/google-workspace/tools/sheets-admin/protected-ranges-get.mjs +18 -0
  146. package/google-workspace/tools/sheets-admin/sort.mjs +18 -0
  147. package/google-workspace/tools/sheets-admin/tab-modify.mjs +36 -0
  148. package/google-workspace/tovu-admin-surfaces.json +63 -0
  149. package/google-workspace/tovu-agent-tools.json +1178 -0
  150. package/google-workspace/tovu-connections.json +35 -0
  151. package/google-workspace/tovu-form-destinations.json +10 -0
  152. package/google-workspace/ui/README.md +73 -0
  153. package/google-workspace/ui/connection-panel.handler.mjs +167 -0
  154. package/google-workspace/ui/connection-panel.html +17 -0
  155. package/google-workspace/ui/connection-panel.mjs +189 -0
  156. package/google-workspace/ui/connection-view.mjs +120 -0
  157. package/google-workspace/ui/destination-panel.handler.mjs +106 -0
  158. package/google-workspace/ui/destination-panel.html +17 -0
  159. package/google-workspace/ui/destination-panel.mjs +150 -0
  160. package/google-workspace/ui/dom.mjs +140 -0
  161. package/google-workspace/ui/panel.css +170 -0
  162. package/google-workspace/ui/picker-flow.mjs +87 -0
  163. package/google-workspace/ui/picker-view.mjs +44 -0
  164. package/google-workspace/ui/picker.handler.mjs +56 -0
  165. package/google-workspace/ui/picker.html +17 -0
  166. package/google-workspace/ui/picker.mjs +88 -0
  167. package/google-workspace/ui/surface-client.mjs +227 -0
  168. package/higgsfield-media/mcp.json +9 -0
  169. package/higgsfield-media/plugin.json +109 -0
  170. package/higgsfield-media/skills/higgsfield-media/SKILL.md +325 -0
  171. package/higgsfield-media/skills/higgsfield-media/references/failure-modes.md +116 -0
  172. package/higgsfield-media/skills/higgsfield-media/references/models-and-plan-gates.md +142 -0
  173. package/jev/mcp.json +4 -0
  174. package/jev/plugin.json +44 -0
  175. package/jev/skills/jev/SKILL.md +308 -0
  176. package/jev/skills/jev/references/cookbooks.md +197 -0
  177. package/jev/skills/jev/references/jaggedness.md +135 -0
  178. package/jev/skills/jev/references/javascript-sdk.md +146 -0
  179. package/jev/skills/jev/references/patterns.md +123 -0
  180. package/jev/skills/jev/references/primitives-reference.md +187 -0
  181. package/langfuse/plugin.json +22 -0
  182. package/meilisearch/mcp.json +4 -0
  183. package/meilisearch/plugin.json +54 -0
  184. package/meilisearch/providers/meilisearch.mjs +651 -0
  185. package/meilisearch/skills/meilisearch/SKILL.md +43 -0
  186. package/meilisearch/tovu-search-providers.json +10 -0
  187. package/netlify/NOTES.md +13 -0
  188. package/netlify/mcp.json +4 -0
  189. package/netlify/plugin.json +47 -0
  190. package/netlify/skills/netlify/SKILL.md +39 -0
  191. package/netlify/targets/netlify.mjs +384 -0
  192. package/netlify/tovu-consent.json +6 -0
  193. package/netlify/tovu-deploy-targets.json +163 -0
  194. package/opentelemetry/mcp.json +4 -0
  195. package/opentelemetry/plugin.json +52 -0
  196. package/opentelemetry/skills/opentelemetry/SKILL.md +39 -0
  197. package/package.json +161 -6
  198. package/posthog/mcp.json +4 -0
  199. package/posthog/plugin.json +46 -0
  200. package/posthog/skills/posthog/SKILL.md +36 -0
  201. package/railway/deploy-configs/railway.mjs +135 -0
  202. package/railway/deploy-ops/railway.mjs +167 -0
  203. package/railway/mcp.json +4 -0
  204. package/railway/plugin.json +45 -0
  205. package/railway/skills/railway/SKILL.md +66 -0
  206. package/railway/tovu-deploy-configs.json +16 -0
  207. package/railway/tovu-deploy-ops.json +11 -0
  208. package/render/deploy-configs/render.mjs +141 -0
  209. package/render/deploy-ops/render.mjs +152 -0
  210. package/render/mcp.json +4 -0
  211. package/render/plugin.json +47 -0
  212. package/render/skills/render/SKILL.md +63 -0
  213. package/render/tovu-deploy-configs.json +16 -0
  214. package/render/tovu-deploy-ops.json +11 -0
  215. package/resend/NOTES.md +52 -0
  216. package/resend/adapters/resend.mjs +161 -0
  217. package/resend/lib/domains.mjs +137 -0
  218. package/resend/lib/errors.mjs +25 -0
  219. package/resend/lib/feedback.mjs +118 -0
  220. package/resend/mcp.json +13 -0
  221. package/resend/plugin.json +192 -0
  222. package/resend/skills/resend/SKILL.md +107 -0
  223. package/resend/tovu-mail-adapters.json +22 -0
  224. package/s3-compatible/NOTES.md +11 -0
  225. package/s3-compatible/mcp.json +4 -0
  226. package/s3-compatible/object-stores/s3-compatible.mjs +287 -0
  227. package/s3-compatible/plugin.json +56 -0
  228. package/s3-compatible/skills/s3-compatible/SKILL.md +39 -0
  229. package/s3-compatible/targets/s3-compatible.mjs +1054 -0
  230. package/s3-compatible/tovu-deploy-targets.json +432 -0
  231. package/s3-compatible/tovu-object-stores.json +29 -0
  232. package/sentry/mcp.json +4 -0
  233. package/sentry/plugin.json +49 -0
  234. package/sentry/skills/sentry/SKILL.md +34 -0
  235. package/shopify/mcp.json +4 -0
  236. package/shopify/plugin.json +22 -0
  237. package/shopify/skills/shopify/SKILL.md +103 -0
  238. package/shopify/skills/shopify/references/catalog-sync.md +74 -0
  239. package/shopify/skills/shopify/references/domains.md +26 -0
  240. package/shopify/skills/shopify/references/live-storefront.md +34 -0
  241. package/shopify/skills/shopify/references/storefront-snippets.md +21 -0
  242. package/site-compliance/mcp.json +4 -0
  243. package/site-compliance/plugin.json +34 -0
  244. package/site-compliance/skills/site-compliance/SKILL.md +220 -0
  245. package/site-compliance/skills/site-compliance/references/ccpa-cpra.md +115 -0
  246. package/site-compliance/skills/site-compliance/references/gdpr-eu-uk.md +143 -0
  247. package/site-compliance/skills/site-compliance/references/wcag-2-2.md +129 -0
  248. package/site-import/mcp.json +4 -0
  249. package/site-import/plugin.json +41 -0
  250. package/site-import/skills/site-import/SKILL.md +232 -0
  251. package/site-import/skills/site-import/references/content-mapping.md +166 -0
  252. package/site-import/skills/site-import/references/discovery-and-classification.md +117 -0
  253. package/site-import/skills/site-import/references/theme-extraction.md +34 -0
  254. package/slide-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  255. package/slide-design/mcp.json +4 -0
  256. package/slide-design/plugin.json +26 -0
  257. package/slide-design/skills/slide-design/SKILL.md +103 -0
  258. package/slide-design/skills/slide-design/references/charts-and-exhibits.md +100 -0
  259. package/slide-design/skills/slide-design/references/html-deck.md +175 -0
  260. package/slide-design/skills/slide-design/references/layout-and-style.md +131 -0
  261. package/slide-design/skills/slide-design/references/review.md +93 -0
  262. package/slide-design/skills/slide-design/references/storyline.md +118 -0
  263. package/stripe/plugin.json +22 -0
  264. package/supabase/mcp.json +26 -0
  265. package/supabase/plugin.json +78 -0
  266. package/supabase/skills/supabase/SKILL.md +169 -0
  267. package/supabase/skills/supabase/references/failure-modes.md +101 -0
  268. package/ui-ux-design/CHANGELOG.md +28 -0
  269. package/ui-ux-design/LICENSE +201 -0
  270. package/ui-ux-design/NOTICE +14 -0
  271. package/ui-ux-design/README.md +62 -0
  272. package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  273. package/ui-ux-design/mcp.json +4 -0
  274. package/ui-ux-design/plugin.json +26 -0
  275. package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
  276. package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
  277. package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
  278. package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
  279. package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
  280. package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
  281. package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
  282. package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
  283. package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
  284. package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
  285. package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
  286. package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
  287. package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
  288. package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
  289. package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
  290. package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
  291. package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
  292. package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
  293. package/ui-ux-design/skills/shadcn-ui/SKILL.md +333 -0
  294. package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
  295. package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
  296. package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
  297. package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
  298. package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
  299. package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
  300. package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
  301. package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
  302. package/ui-ux-design/skills/ui-ux-design/SKILL.md +171 -0
  303. package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +78 -0
  304. package/ui-ux-design/skills/ui-ux-design/references/chrome-mobile-test-matrix.md +137 -0
  305. package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +112 -0
  306. package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +54 -0
  307. package/ui-ux-design/skills/ui-ux-design/references/foundations.md +145 -0
  308. package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
  309. package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +104 -0
  310. package/ui-ux-design/skills/ui-ux-design/references/kuinetic-reference.md +184 -0
  311. package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
  312. package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +55 -0
  313. package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +88 -0
  314. package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +103 -0
  315. package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +119 -0
  316. package/ui-ux-design/skills/ui-ux-design/references/ui-animation.md +173 -0
  317. package/ui-ux-design/skills/ui-ux-design/references/ui-loop.md +234 -0
  318. package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +54 -0
  319. package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
  320. package/ui-ux-design/skills/web-compliance/SKILL.md +134 -0
  321. package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
  322. package/vercel/NOTES.md +23 -0
  323. package/vercel/mcp.json +4 -0
  324. package/vercel/object-stores/vercel-blob.mjs +147 -0
  325. package/vercel/plugin.json +47 -0
  326. package/vercel/skills/vercel/SKILL.md +48 -0
  327. package/vercel/targets/vercel.mjs +586 -0
  328. package/vercel/tovu-consent.json +6 -0
  329. package/vercel/tovu-deploy-targets.json +219 -0
  330. package/vercel/tovu-object-stores.json +10 -0
  331. package/wordpress/NOTES.md +33 -0
  332. package/wordpress/plugin.json +30 -0
  333. package/wordpress/skills/wordpress/SKILL.md +43 -0
  334. package/wordpress/skills/wordpress/references/free-site-redirect.md +46 -0
  335. package/wordpress/skills/wordpress/references/subdomain.md +66 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @file CSV export planning and encoding for the `export_csv` tool: split one tab (or an A1 range of
3
+ * it) into bounded values reads, and encode the rows as RFC 4180 CSV.
4
+ *
5
+ * Exists because Google's own CSV export covers only a spreadsheet's first tab and an arbitrary tab
6
+ * would need an undocumented `gid` URL; bounded `values.batchGet` reads plus local encoding work for
7
+ * any tab. The size is bounded BEFORE the first read (from the tab's grid size or the range), and an
8
+ * export over a bound is an error, never a silently partial file. Text cells that start like a
9
+ * formula are prefixed with `'` so opening the file never runs a visitor's formula.
10
+ */
11
+ import { GoogleWorkspaceError } from './request-policy.mjs';
12
+ import { columnName, tabRange } from './sheets-a1.mjs';
13
+
14
+ export const MAX_EXPORT_ROWS = 20_000;
15
+ export const MAX_EXPORT_CELLS = 100_000;
16
+ export const MAX_CSV_BYTES = 4 * 1024 * 1024;
17
+ /** Cells per read: keeps each answer far below the host's 1 MiB response bound. */
18
+ const CHUNK_CELLS = 10_000;
19
+
20
+ const tooLarge = (detail) => new GoogleWorkspaceError({ code: 'limit-exceeded' }, { detail });
21
+
22
+ /**
23
+ * The reads covering `area` (zero-based, inclusive), in row order.
24
+ * @param {{ title: string, area: { firstRow: number, lastRow: number, firstColumn: number, lastColumn: number } }} required
25
+ * @returns {{ range: string, rowCount: number }[]}
26
+ * @throws {GoogleWorkspaceError} `limit-exceeded` when the area is over the row or cell bound.
27
+ * @complexity O(chunks), at most MAX_EXPORT_CELLS / CHUNK_CELLS.
28
+ */
29
+ export function planChunks(required, _optional = {}) {
30
+ const { firstRow, lastRow, firstColumn, lastColumn } = required.area;
31
+ const rows = lastRow - firstRow + 1;
32
+ const columns = lastColumn - firstColumn + 1;
33
+ if (rows > MAX_EXPORT_ROWS) throw tooLarge(`at most ${MAX_EXPORT_ROWS} rows; give a range`);
34
+ if (rows * columns > MAX_EXPORT_CELLS) throw tooLarge(`at most ${MAX_EXPORT_CELLS} cells; give a smaller range`);
35
+ const rowsPerChunk = Math.max(1, Math.floor(CHUNK_CELLS / columns));
36
+ const [from, to] = [columnName({ index: firstColumn }), columnName({ index: lastColumn })];
37
+ const chunks = [];
38
+ for (let start = firstRow; start <= lastRow; start += rowsPerChunk) {
39
+ const end = Math.min(lastRow, start + rowsPerChunk - 1);
40
+ chunks.push({ range: tabRange({ title: required.title, cells: `${from}${start + 1}:${to}${end + 1}` }), rowCount: end - start + 1 });
41
+ }
42
+ return chunks;
43
+ }
44
+
45
+ /**
46
+ * Text a spreadsheet app would run as a formula when it opens the CSV (OWASP "CSV injection"):
47
+ * a leading `=`, `+`, `-`, `@`, tab or carriage return. Form answers are stored RAW, so a visitor's
48
+ * `=HYPERLINK(...)` comes back from Google as plain text and would otherwise become live.
49
+ */
50
+ const FORMULA_TRIGGER = /^[=+\-@\t\r]/;
51
+
52
+ /**
53
+ * One CSV field: text that a spreadsheet app would evaluate gets a leading `'` (shown as text, not
54
+ * run), then the field is quoted when it holds a comma, quote or line break. Only strings are
55
+ * prefixed: a number Google returns as a number (`UNFORMATTED_VALUE`) stays exact, while a
56
+ * formatted value such as `-5` or `+1 555` is text and keeps the `'` (the fidelity trade-off,
57
+ * documented in `references/sheets.md`).
58
+ * @complexity O(length).
59
+ */
60
+ function csvField(value) {
61
+ const raw = value === undefined || value === null ? '' : String(value);
62
+ const text = typeof value === 'string' && FORMULA_TRIGGER.test(value) ? `'${raw}` : raw;
63
+ return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
64
+ }
65
+
66
+ /**
67
+ * RFC 4180 CSV (CRLF line ends) of `rows`, padded to the widest row, trailing empty rows dropped.
68
+ * @param {{ rows: readonly (readonly unknown[])[] }} required
69
+ * @returns {{ csv: string, rows: number, columns: number, bytes: number }}
70
+ * @throws {GoogleWorkspaceError} `limit-exceeded` over {@link MAX_CSV_BYTES}.
71
+ * @complexity O(cells).
72
+ */
73
+ export function encodeCsv(required, _optional = {}) {
74
+ let used = required.rows.length;
75
+ while (used > 0 && required.rows[used - 1].length === 0) used -= 1;
76
+ const kept = required.rows.slice(0, used);
77
+ const columns = Math.max(0, ...kept.map((row) => row.length));
78
+ const csv = kept.map((row) => Array.from({ length: columns }, (_, index) => csvField(row[index])).join(',')).join('\r\n');
79
+ const bytes = Buffer.byteLength(csv);
80
+ if (bytes > MAX_CSV_BYTES) throw tooLarge('the CSV would be over 4 MiB; give a smaller range');
81
+ return { csv, rows: used, columns, bytes };
82
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @file The reviewed inventory of Sheets `spreadsheets.batchUpdate` request kinds the Google
3
+ * Workspace tools may send, and the one check every batch passes before it reaches Google (the raw
4
+ * `batch_update` tool and every named wrapper: tab, sort, filter, format, named ranges).
5
+ *
6
+ * Exists because a raw batch tool is the broad-coverage escape hatch, so it must fail closed: only
7
+ * kinds listed here are sent; an unknown, misspelt or preview kind is refused before any request.
8
+ * Each listed kind carries:
9
+ * - `effect`: `destructive` when it deletes something, overwrites or moves cell values, or changes
10
+ * protection; otherwise `write`. The confirmation card names every kind with its effect.
11
+ * - `replay`: `unsafe` when sending the same request twice can change the sheet twice (adds,
12
+ * inserts, appends, deletes by index, moves, find/replace, randomize). A batch holding one is a
13
+ * `write` for the HTTP client, so an unknown outcome is `uncertain-outcome` and never replayed.
14
+ * The class judges a request ALONE. Google applies a batch's requests in order, so two safe kinds
15
+ * can compose into an unsafe sequence (copy A1 to B1, then set A1: a replay copies the new A1 into
16
+ * B1). No sequence is reviewed as safe, so a batch of more than one request is always a `write`.
17
+ * - `requires`: the fields Google needs; an inner array means "at least one of". `fields` (the field
18
+ * mask) must be explicit: `*` would reset every property the request does not set.
19
+ *
20
+ * Reviewed against the Request union of Sheets API v4
21
+ * (https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/request).
22
+ * Deliberately NOT listed (refused as unsupported): the data-source kinds (`addDataSource`,
23
+ * `updateDataSource`, `deleteDataSource`, `refreshDataSource`, `cancelDataSourceRefresh`). They need
24
+ * Connected Sheets and a BigQuery permission that this sign-in never requests (drive.file only);
25
+ * offering them would only produce permission failures. Adding a kind means reviewing it against the
26
+ * reference and adding one row here.
27
+ */
28
+ import { GoogleWorkspaceError } from './request-policy.mjs';
29
+
30
+ export const MAX_BATCH_REQUESTS = 100;
31
+ export const MAX_BATCH_BYTES = 1024 * 1024;
32
+ /** The confirmation card shows at most 50 lines: one for the spreadsheet, one per kind. */
33
+ export const MAX_DISTINCT_KINDS = 48;
34
+
35
+ const W = 'write';
36
+ const D = 'destructive';
37
+ const SAFE = 'idempotent';
38
+ const UNSAFE = 'unsafe';
39
+
40
+ /** [kind, effect, replay, requires] */
41
+ const ROWS = [
42
+ ['updateSpreadsheetProperties', W, SAFE, ['properties', 'fields']],
43
+ // Shrinking gridProperties.rowCount/columnCount deletes cells.
44
+ ['updateSheetProperties', D, SAFE, ['properties', 'fields']],
45
+ ['updateDimensionProperties', W, SAFE, ['properties', 'fields', ['range', 'dataSourceSheetRange']]],
46
+ ['updateNamedRange', W, SAFE, ['namedRange', 'fields']],
47
+ ['repeatCell', D, SAFE, ['range', 'cell', 'fields']],
48
+ ['addNamedRange', W, UNSAFE, ['namedRange']],
49
+ ['deleteNamedRange', D, SAFE, ['namedRangeId']],
50
+ ['addSheet', W, UNSAFE, []],
51
+ ['deleteSheet', D, SAFE, ['sheetId']],
52
+ ['autoFill', D, SAFE, [['range', 'sourceAndDestination']]],
53
+ ['cutPaste', D, UNSAFE, ['source', 'destination']],
54
+ ['copyPaste', D, SAFE, ['source', 'destination']],
55
+ ['mergeCells', D, SAFE, ['range', 'mergeType']],
56
+ ['unmergeCells', W, SAFE, ['range']],
57
+ ['updateBorders', W, SAFE, ['range']],
58
+ ['updateCells', D, SAFE, ['rows', 'fields', ['start', 'range']]],
59
+ ['addFilterView', W, UNSAFE, ['filter']],
60
+ ['appendCells', W, UNSAFE, ['sheetId', 'rows', 'fields']],
61
+ ['clearBasicFilter', W, SAFE, ['sheetId']],
62
+ ['deleteDimension', D, UNSAFE, ['range']],
63
+ ['deleteEmbeddedObject', D, SAFE, ['objectId']],
64
+ ['deleteFilterView', D, SAFE, ['filterId']],
65
+ ['duplicateFilterView', W, UNSAFE, ['filterId']],
66
+ ['duplicateSheet', W, UNSAFE, ['sourceSheetId']],
67
+ ['findReplace', D, UNSAFE, ['find', ['range', 'sheetId', 'allSheets']]],
68
+ ['insertDimension', W, UNSAFE, ['range']],
69
+ ['insertRange', W, UNSAFE, ['range', 'shiftDimension']],
70
+ ['moveDimension', W, UNSAFE, ['source', 'destinationIndex']],
71
+ ['updateEmbeddedObjectPosition', W, SAFE, ['objectId', 'newPosition', 'fields']],
72
+ ['pasteData', D, SAFE, ['coordinate', 'data']],
73
+ ['textToColumns', D, UNSAFE, ['source']],
74
+ ['updateFilterView', W, SAFE, ['filter', 'fields']],
75
+ ['deleteRange', D, UNSAFE, ['range', 'shiftDimension']],
76
+ ['appendDimension', W, UNSAFE, ['sheetId', 'dimension', 'length']],
77
+ ['addConditionalFormatRule', W, UNSAFE, ['rule', 'index']],
78
+ ['updateConditionalFormatRule', W, UNSAFE, ['index', 'sheetId', ['rule', 'newIndex']]],
79
+ ['deleteConditionalFormatRule', D, UNSAFE, ['index', 'sheetId']],
80
+ ['sortRange', D, SAFE, ['range', 'sortSpecs']],
81
+ ['setDataValidation', W, SAFE, ['range']],
82
+ // A basic filter's sortSpecs physically reorder the rows, like sortRange.
83
+ ['setBasicFilter', D, SAFE, ['filter']],
84
+ ['addProtectedRange', D, UNSAFE, ['protectedRange']],
85
+ ['updateProtectedRange', D, SAFE, ['protectedRange', 'fields']],
86
+ ['deleteProtectedRange', D, SAFE, ['protectedRangeId']],
87
+ ['autoResizeDimensions', W, SAFE, [['dimensions', 'dataSourceSheetDimensions']]],
88
+ ['addChart', W, UNSAFE, ['chart']],
89
+ ['updateChartSpec', W, SAFE, ['chartId', 'spec']],
90
+ ['updateBanding', W, SAFE, ['bandedRange', 'fields']],
91
+ ['addBanding', W, UNSAFE, ['bandedRange']],
92
+ ['deleteBanding', D, SAFE, ['bandedRangeId']],
93
+ ['createDeveloperMetadata', W, UNSAFE, ['developerMetadata']],
94
+ ['updateDeveloperMetadata', W, SAFE, ['dataFilters', 'developerMetadata', 'fields']],
95
+ ['deleteDeveloperMetadata', D, SAFE, ['dataFilter']],
96
+ ['randomizeRange', D, UNSAFE, ['range']],
97
+ ['addDimensionGroup', W, UNSAFE, ['range']],
98
+ ['deleteDimensionGroup', D, UNSAFE, ['range']],
99
+ ['updateDimensionGroup', W, SAFE, ['dimensionGroup', 'fields']],
100
+ ['trimWhitespace', D, SAFE, ['range']],
101
+ ['deleteDuplicates', D, SAFE, ['range']],
102
+ ['updateEmbeddedObjectBorder', W, SAFE, ['objectId', 'border', 'fields']],
103
+ ['addSlicer', W, UNSAFE, ['slicer']],
104
+ ['updateSlicerSpec', W, SAFE, ['slicerId', 'spec', 'fields']],
105
+ ['addTable', W, UNSAFE, ['table']],
106
+ ['updateTable', W, SAFE, ['table', 'fields']],
107
+ ['deleteTable', D, SAFE, ['tableId']],
108
+ ];
109
+
110
+ /** kind → `{ effect, replay, requires }`, frozen. */
111
+ export const REQUEST_KINDS = Object.freeze(Object.fromEntries(ROWS.map(([kind, effect, replay, requires]) => [kind, Object.freeze({ effect, replay, requires })])));
112
+
113
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
114
+ const refuse = (code, detail) => new GoogleWorkspaceError({ code }, { detail });
115
+
116
+ /** Whether one requirement (a field, or "one of" these fields) is present. @complexity O(1). */
117
+ function satisfied(body, requirement) {
118
+ return [requirement].flat().some((field) => body[field] !== undefined && body[field] !== null);
119
+ }
120
+
121
+ /** What is wrong with a listed kind's body, or undefined. @complexity O(f). */
122
+ function bodyProblem(entry, body) {
123
+ const missing = entry.requires.find((requirement) => !satisfied(body, requirement));
124
+ if (missing) return `needs ${[missing].flat().join(' or ')}`;
125
+ const explicitMask = typeof body.fields === 'string' && body.fields.trim() !== '' && !body.fields.includes('*');
126
+ return body.fields === undefined || explicitMask ? undefined : 'needs an explicit field mask, not *';
127
+ }
128
+
129
+ /**
130
+ * The listed kind of one request, or the refusal that stops the whole batch.
131
+ * @complexity O(f) fields of the kind's requirements.
132
+ */
133
+ function checkOne(request, index) {
134
+ const keys = isPlainObject(request) ? Object.keys(request) : [];
135
+ if (keys.length !== 1) return { error: refuse('invalid-input', `request ${index + 1} must name exactly one request kind`) };
136
+ const kind = keys[0];
137
+ // Own-property lookup only, so `constructor` or `__proto__` can never pass as a kind.
138
+ const entry = Object.hasOwn(REQUEST_KINDS, kind) ? REQUEST_KINDS[kind] : undefined;
139
+ if (!entry) return { error: refuse('unsupported-request', `request ${index + 1}`) };
140
+ const body = request[kind];
141
+ if (!isPlainObject(body)) return { error: refuse('invalid-input', `request ${index + 1} (${kind}) must be an object`) };
142
+ const problem = bodyProblem(entry, body);
143
+ return problem ? { error: refuse('invalid-input', `request ${index + 1} (${kind}) ${problem}`) } : { kind, entry };
144
+ }
145
+
146
+ /** Refuses an empty, oversized or too varied batch. @complexity O(serialized size). */
147
+ function checkBounds(requests) {
148
+ if (!Array.isArray(requests) || requests.length === 0) throw refuse('invalid-input', 'the batch has no requests');
149
+ if (requests.length > MAX_BATCH_REQUESTS) throw refuse('limit-exceeded', `at most ${MAX_BATCH_REQUESTS} requests per batch`);
150
+ if (Buffer.byteLength(JSON.stringify(requests)) > MAX_BATCH_BYTES) throw refuse('limit-exceeded', 'at most 1 MiB of requests per batch');
151
+ }
152
+
153
+ /**
154
+ * Checks a whole batch against the inventory. Nothing is sent unless every request passes.
155
+ * @param {{ requests: unknown }} required
156
+ * @returns {{ kind: 'write' | 'idempotent-write', kinds: Map<string, { effect: string, count: number }> }}
157
+ * `kind` is what the HTTP client needs to know about replays; `kinds` is in first-seen order.
158
+ * @throws {GoogleWorkspaceError} `invalid-input`, `unsupported-request` or `limit-exceeded`.
159
+ * @complexity O(n) requests plus their serialized size.
160
+ */
161
+ export function checkRequests(required, _optional = {}) {
162
+ checkBounds(required.requests);
163
+ const kinds = new Map();
164
+ let unsafe = false;
165
+ required.requests.forEach((request, index) => {
166
+ const checked = checkOne(request, index);
167
+ if (checked.error) throw checked.error;
168
+ unsafe ||= checked.entry.replay === UNSAFE;
169
+ kinds.set(checked.kind, { effect: checked.entry.effect, count: (kinds.get(checked.kind)?.count ?? 0) + 1 });
170
+ });
171
+ if (kinds.size > MAX_DISTINCT_KINDS) throw refuse('limit-exceeded', `at most ${MAX_DISTINCT_KINDS} different request kinds per batch`);
172
+ return { kind: unsafe || required.requests.length > 1 ? 'write' : 'idempotent-write', kinds };
173
+ }
174
+
175
+ /**
176
+ * One confirmation line per request kind in the batch: `deleteDimension x2 (destructive)`.
177
+ * @param {{ requests: unknown }} required
178
+ * @complexity O(n).
179
+ */
180
+ export function describeRequests(required, _optional = {}) {
181
+ const { kinds } = checkRequests(required);
182
+ return [...kinds].map(([kind, { effect, count }]) => `${kind} x${count} (${effect})`);
183
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * @file The one Sheets client of the Google Workspace plugin: every Sheets tool (values, search,
3
+ * tabs, filters, formatting, batch updates, export) and the form destination build their endpoint,
4
+ * check and encode ids and ranges, apply the RAW input default and enforce the §D bounds here, then
5
+ * send through the one Google client (`http.mjs`), which owns tokens, retries and safe errors
6
+ * (quotas are the host's, charged on each token read). A1 helpers live in `sheets-a1.mjs`.
7
+ *
8
+ * Exists so the rules that keep a call inside what the person asked for live in one place:
9
+ * - a spreadsheet id must look like a Google file id ({@link assertSpreadsheetId}) and, like a
10
+ * range, is ONE encoded path segment (`.`/`..` refused), so caller text can never change which
11
+ * endpoint is called;
12
+ * - every batch, raw or built by a named tool, passes the reviewed request-kind inventory first
13
+ * ({@link runBatchUpdate});
14
+ * - values are written `RAW` unless the caller explicitly asks for `USER_ENTERED`, so text a
15
+ * visitor typed never becomes a formula;
16
+ * - reads return at most 500 rows, 5,000 cells and 64 KiB, with an explicit continuation range
17
+ * instead of a silent partial; mutation payloads stay under 1 MiB;
18
+ * - every request a Sheets tool sends first checks that the Sheets product is on
19
+ * ({@link sheetsClientFor}), so turning Sheets off blocks every Sheets tool at once.
20
+ */
21
+ import { createGoogleClient, pathSegment } from './http.mjs';
22
+ import { assertProductEnabled } from './product-state.mjs';
23
+ import { GoogleWorkspaceError } from './request-policy.mjs';
24
+ import { parseA1Range } from './sheets-a1.mjs';
25
+ import { MAX_BATCH_BYTES, MAX_BATCH_REQUESTS, checkRequests } from './sheets-admin-request-kinds.mjs';
26
+
27
+ export const SHEETS_API = 'https://sheets.googleapis.com/v4/spreadsheets';
28
+ /** The `products.json` id every Sheets tool belongs to (its tools are named `sheets_*`). */
29
+ export const SHEETS_PRODUCT_ID = 'sheets';
30
+ export const DEFAULT_VALUE_INPUT_OPTION = 'RAW';
31
+
32
+ /** §D starting bounds. Batch reads share ONE output budget across all their ranges. */
33
+ export const SHEETS_LIMITS = Object.freeze({
34
+ maxRows: 500, maxCells: 5_000, maxOutputBytes: 64 * 1024, maxRanges: 20,
35
+ // One mutation bound for values writes and batch updates alike.
36
+ maxMutationEntries: MAX_BATCH_REQUESTS, maxPayloadBytes: MAX_BATCH_BYTES,
37
+ // find reads more than it returns: it scans up to this many cells and returns at most maxMatches.
38
+ maxScannedCells: 50_000, maxMatches: 100, maxExcerptChars: 120,
39
+ });
40
+
41
+ /** Default spreadsheet metadata mask: no grid data, only what chat needs to name tabs. */
42
+ const SHEET_PROPERTIES = 'properties(sheetId,title,index,sheetType,gridProperties(rowCount,columnCount,frozenRowCount,frozenColumnCount))';
43
+ const SPREADSHEET_PREFIX = 'spreadsheetId,spreadsheetUrl,properties(title,locale,timeZone)';
44
+ export const SPREADSHEET_FIELDS = `${SPREADSHEET_PREFIX},sheets(${SHEET_PROPERTIES})`;
45
+ const GRID_DATA_FIELDS = `${SPREADSHEET_PREFIX},sheets(${SHEET_PROPERTIES},data(startRow,startColumn,rowData(values(formattedValue))))`;
46
+
47
+ const byteLength = (value) => Buffer.byteLength(JSON.stringify(value) ?? '', 'utf8');
48
+
49
+ /** Google file ids are URL-safe base64 characters; the input schemas use the same pattern. */
50
+ export const SPREADSHEET_ID_PATTERN = /^[A-Za-z0-9_-]{10,200}$/;
51
+
52
+ /**
53
+ * The id, when it is a plausible Google file id; refused before it reaches any URL otherwise (the
54
+ * value is never echoed).
55
+ * @param {{ id: unknown }} required
56
+ * @param {{ code?: 'invalid-input' | 'invalid-selection' }} [optional] The Picker refuses with
57
+ * `invalid-selection` (re-pick the file); tools with `invalid-input` (the default).
58
+ * @throws {GoogleWorkspaceError}
59
+ * @complexity O(n) characters.
60
+ */
61
+ export function assertSpreadsheetId(required, optional = {}) {
62
+ if (typeof required.id === 'string' && SPREADSHEET_ID_PATTERN.test(required.id)) return required.id;
63
+ const code = optional.code ?? 'invalid-input';
64
+ throw new GoogleWorkspaceError({ code }, code === 'invalid-input' ? { detail: 'spreadsheetId' } : {});
65
+ }
66
+
67
+ /**
68
+ * `<SHEETS_API>/<spreadsheetId>[/<segment>…][<method>]`, the id checked first. `method`
69
+ * (`:append`, `:batchUpdate`, …) is always a constant from plugin code, never caller text.
70
+ * @param {{ spreadsheetId: string }} required
71
+ * @param {{ segments?: readonly string[], method?: string }} [optional]
72
+ * @throws {GoogleWorkspaceError} `invalid-input` for an id or segment that cannot be a path segment.
73
+ * @complexity O(n) characters.
74
+ */
75
+ export function sheetsUrl(required, optional = {}) {
76
+ const segments = [assertSpreadsheetId({ id: required.spreadsheetId }), ...(optional.segments ?? [])].map((value) => pathSegment({ value }));
77
+ return `${SHEETS_API}/${segments.join('/')}${optional.method ?? ''}`;
78
+ }
79
+
80
+ /**
81
+ * Sends one Sheets request for a tool call through the shared Google client.
82
+ * @param {{ call: { input: { connectionRef: string }, capabilities: { http: object, connections: object, state?: object }, signal?: AbortSignal }, request: { url: string, method?: string, query?: object, body?: unknown, kind: 'read' | 'idempotent-write' | 'write' } }} required
83
+ * `call` is the tool's `invoke` argument as received.
84
+ * @returns {Promise<any>} Google's parsed JSON body.
85
+ * @throws {GoogleWorkspaceError} Every failure, already mapped to a safe code.
86
+ * @complexity O(attempts) (see `createGoogleClient`).
87
+ */
88
+ export function sendSheetsRequest(required, _optional = {}) {
89
+ const { input, capabilities, signal } = required.call;
90
+ return sheetsClientFor({ capabilities }).request({ connectionRef: input.connectionRef, ...required.request }, signal ? { signal } : {});
91
+ }
92
+
93
+ /** The shared Google client over one call's host capabilities. @complexity O(1). */
94
+ export function googleClientFor(required, _optional = {}) {
95
+ const { capabilities } = required;
96
+ return createGoogleClient({ http: capabilities.http, connections: capabilities.connections });
97
+ }
98
+
99
+ /**
100
+ * The Google client a Sheets tool uses: each request first checks that the Sheets product is on in
101
+ * the plugin's settings (`capabilities.state`), then goes through the shared client. Checked per
102
+ * request, so turning Sheets off also stops a multi-request call (an export) part way, before the
103
+ * next request is sent.
104
+ * @param {{ capabilities: { http: object, connections: object, state?: { get(input: { key: string }): Promise<unknown> } } }} required
105
+ * @throws {GoogleWorkspaceError} `product-disabled` from `request` while Sheets is off.
106
+ * @complexity O(1) to build; each request adds one state read.
107
+ */
108
+ export function sheetsClientFor(required, _optional = {}) {
109
+ const { capabilities } = required;
110
+ const client = googleClientFor({ capabilities });
111
+ return {
112
+ async request(request, options = {}) {
113
+ await assertProductEnabled({ state: capabilities.state, productId: SHEETS_PRODUCT_ID });
114
+ return client.request(request, options);
115
+ },
116
+ };
117
+ }
118
+
119
+ /** A mask that reaches cell data: `*`, or a `data` field (`sheets.data`, `sheets(data)`), not `dataSources`. */
120
+ const GRID_DATA_MASK = /\*|\bdata\b/;
121
+
122
+ /**
123
+ * The spreadsheet field mask: the caller's, else the default (plus cell values when grid data is
124
+ * asked for). Google ignores `includeGridData` once a mask is set, so a caller mask that reaches
125
+ * cell data counts as asking for grid data: it needs `includeGridData` (which `sheets_get` only
126
+ * allows with explicit ranges; `get_by_data_filter` always names its data filters).
127
+ * @throws {GoogleWorkspaceError} `invalid-input` for a grid-data mask without `includeGridData`.
128
+ * @complexity O(n) mask length.
129
+ */
130
+ export function spreadsheetFields(required, _optional = {}) {
131
+ if (required.fields !== undefined && GRID_DATA_MASK.test(required.fields) && !required.includeGridData) {
132
+ throw new GoogleWorkspaceError({ code: 'invalid-input' }, { detail: 'a fields mask with * or data needs includeGridData and ranges' });
133
+ }
134
+ if (required.fields !== undefined) return required.fields;
135
+ return required.includeGridData ? GRID_DATA_FIELDS : SPREADSHEET_FIELDS;
136
+ }
137
+
138
+ const MAX_RESOURCE_LINES = 50;
139
+ const MAX_RESOURCE_CHARS = 300;
140
+
141
+ /**
142
+ * Lines for the confirmation card, inside the projection's bounds (50 lines of 300 characters):
143
+ * a long line is shortened and extra lines are summarized as a count, so nothing is hidden silently.
144
+ * @complexity O(lines).
145
+ */
146
+ export function resourceLines(required, _optional = {}) {
147
+ const fit = (line) => (line.length > MAX_RESOURCE_CHARS ? `${line.slice(0, MAX_RESOURCE_CHARS - 1)}…` : line);
148
+ const { lines } = required;
149
+ if (lines.length <= MAX_RESOURCE_LINES) return lines.map(fit);
150
+ return [...lines.slice(0, MAX_RESOURCE_LINES - 1).map(fit), `… and ${lines.length - MAX_RESOURCE_LINES + 1} more`];
151
+ }
152
+
153
+ /** The value input option to send: `RAW` unless the caller explicitly chose `USER_ENTERED`. @complexity O(1). */
154
+ export function valueInputOption(required, _optional = {}) {
155
+ return required.option === 'USER_ENTERED' ? 'USER_ENTERED' : DEFAULT_VALUE_INPUT_OPTION;
156
+ }
157
+
158
+ /** Read options every values read sends: rows-major, plus the caller's render choices. @complexity O(1). */
159
+ export function readOptions(required, _optional = {}) {
160
+ const { valueRenderOption, dateTimeRenderOption } = required.input;
161
+ return { majorDimension: 'ROWS', ...(valueRenderOption ? { valueRenderOption } : {}), ...(dateTimeRenderOption ? { dateTimeRenderOption } : {}) };
162
+ }
163
+
164
+ /** Total cells in rows of values. @complexity O(rows). */
165
+ export function cellCount(required, _optional = {}) {
166
+ return required.values.reduce((total, row) => total + row.length, 0);
167
+ }
168
+
169
+ /**
170
+ * Refuses a mutation whose serialized body exceeds the payload bound, before anything is sent.
171
+ * @throws {GoogleWorkspaceError} `limit-exceeded`.
172
+ * @complexity O(n) serialized size.
173
+ */
174
+ export function assertPayloadWithin(required, _optional = {}) {
175
+ if (byteLength(required.value) > SHEETS_LIMITS.maxPayloadBytes) throw new GoogleWorkspaceError({ code: 'limit-exceeded' });
176
+ return required.value;
177
+ }
178
+
179
+ /**
180
+ * Whether a result fits the tool-output bound, less `headroomBytes` (room a caller keeps for the
181
+ * rest of its answer).
182
+ * @complexity O(n) serialized size.
183
+ */
184
+ export function outputFits(required, optional = {}) {
185
+ return byteLength(required.value) <= SHEETS_LIMITS.maxOutputBytes - (optional.headroomBytes ?? 0);
186
+ }
187
+
188
+ /**
189
+ * Returns a result only when it fits the tool-output bound; a larger result is an error, never a
190
+ * silently cut one (used where no range continuation exists: metadata, data-filter and admin reads).
191
+ * @throws {GoogleWorkspaceError} `limit-exceeded`.
192
+ * @complexity O(n) serialized size.
193
+ */
194
+ export function assertOutputWithin(required, _optional = {}) {
195
+ if (!outputFits(required)) throw new GoogleWorkspaceError({ code: 'limit-exceeded' }, { detail: 'answer over 64 KiB; narrow it to one tab or fewer items' });
196
+ return required.value;
197
+ }
198
+
199
+ const signalOption = (signal) => (signal ? { signal } : {});
200
+
201
+ /**
202
+ * `spreadsheets.get` with a field mask (grid data is never requested).
203
+ * @param {{ client: object, connectionRef: string, spreadsheetId: string, fields: string }} required
204
+ * @param {{ signal?: AbortSignal }} [optional]
205
+ * @complexity O(1) requests.
206
+ */
207
+ export function getSpreadsheet(required, optional = {}) {
208
+ return required.client.request({
209
+ connectionRef: required.connectionRef, url: sheetsUrl({ spreadsheetId: required.spreadsheetId }), kind: 'read', query: { fields: required.fields },
210
+ }, signalOption(optional.signal));
211
+ }
212
+
213
+ /**
214
+ * `spreadsheets.batchUpdate` after the inventory check; a batch holding any non-replayable kind is a
215
+ * `write`, so an unknown outcome is reported as uncertain and never replayed.
216
+ * @param {{ client: object, connectionRef: string, spreadsheetId: string, requests: readonly object[] }} required
217
+ * @param {{ signal?: AbortSignal }} [optional]
218
+ * @returns {Promise<{ replies: unknown[] }>}
219
+ * @complexity O(n) requests to check, one HTTP request.
220
+ */
221
+ export async function runBatchUpdate(required, optional = {}) {
222
+ const { kind } = checkRequests({ requests: required.requests });
223
+ const body = await required.client.request({
224
+ connectionRef: required.connectionRef, url: sheetsUrl({ spreadsheetId: required.spreadsheetId }, { method: ':batchUpdate' }),
225
+ method: 'POST', kind, body: { requests: required.requests },
226
+ }, signalOption(optional.signal));
227
+ return { replies: Array.isArray(body?.replies) ? body.replies : [] };
228
+ }
229
+
230
+ /** The first line of every admin tool's disclosure. @complexity O(1). */
231
+ export function spreadsheetLine(required, _optional = {}) {
232
+ return `spreadsheet ${assertSpreadsheetId({ id: required.spreadsheetId })}`;
233
+ }
234
+
235
+ /**
236
+ * The tabs of a `sheets(properties(...),<part>)` answer, optionally only one tab.
237
+ * @complexity O(t) tabs.
238
+ */
239
+ export function tabsOf(required, _optional = {}) {
240
+ const sheets = Array.isArray(required.body?.sheets) ? required.body.sheets : [];
241
+ return required.sheetId === undefined ? sheets : sheets.filter((sheet) => sheet?.properties?.sheetId === required.sheetId);
242
+ }
243
+
244
+ /**
245
+ * The range that continues `range` after its first `rows` rows, same columns.
246
+ * @throws {GoogleWorkspaceError} `limit-exceeded` when Google's range cannot be continued, so the
247
+ * caller errors instead of returning an undisclosed partial read.
248
+ * @complexity O(n) characters.
249
+ */
250
+ export function continuationRange(required, _optional = {}) {
251
+ const parts = parseA1Range({ range: required.range });
252
+ if (!parts) throw new GoogleWorkspaceError({ code: 'limit-exceeded' });
253
+ const start = `${parts.sheet}!${parts.startColumn}${parts.startRow + required.rows}`;
254
+ return parts.endRow === undefined ? start : `${start}:${parts.endColumn}${parts.endRow}`;
255
+ }
256
+
257
+ /** Whether one more row fits the shared output budget. @complexity O(1). */
258
+ function rowFits(totals, row, rowBytes) {
259
+ return totals.rows < SHEETS_LIMITS.maxRows && totals.cells + row.length <= SHEETS_LIMITS.maxCells && totals.bytes + rowBytes <= SHEETS_LIMITS.maxOutputBytes;
260
+ }
261
+
262
+ /**
263
+ * Cuts value ranges to ONE shared output budget (rows, cells, bytes) in order. A cut range gets
264
+ * `continuation`: the exact range to read next.
265
+ * @param {{ valueRanges: readonly { range: string, values?: unknown[][] }[] }} required
266
+ * @returns {{ range: string, values: unknown[][], continuation?: string }[]}
267
+ * @throws {GoogleWorkspaceError} `limit-exceeded` when not even the first row fits (no progress possible).
268
+ * @complexity O(cells).
269
+ */
270
+ export function boundValueRanges(required, _optional = {}) {
271
+ const totals = { rows: 0, cells: 0, bytes: 0 };
272
+ const bounded = required.valueRanges.map((valueRange) => {
273
+ const values = valueRange.values ?? [];
274
+ let kept = 0;
275
+ for (const row of values) {
276
+ const rowBytes = byteLength(row);
277
+ if (!rowFits(totals, row, rowBytes)) break;
278
+ Object.assign(totals, { rows: totals.rows + 1, cells: totals.cells + row.length, bytes: totals.bytes + rowBytes });
279
+ kept += 1;
280
+ }
281
+ if (kept === values.length) return { range: valueRange.range, values };
282
+ return { range: valueRange.range, values: values.slice(0, kept), continuation: continuationRange({ range: valueRange.range, rows: kept }) };
283
+ });
284
+ if (totals.rows === 0 && bounded.some((entry) => entry.continuation)) throw new GoogleWorkspaceError({ code: 'limit-exceeded' });
285
+ return bounded;
286
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @file Server-side pieces every Google Workspace admin-surface handler shares: the plugin's one
3
+ * connection id, a Google client over the surface capability port, the "is Sheets usable right now"
4
+ * check, and the translation of plugin and host refusals into surface-safe failures.
5
+ *
6
+ * Exists because the three panels (connection, Picker, form destination) run in the host's
7
+ * admin-surface handler seam, whose port differs from the tool port in one way that matters here:
8
+ * connections are named by the surface's declared connection id, not by a connection ref. Keeping
9
+ * that adaptation and the failure mapping in one place means every panel sends the same safe codes
10
+ * and fixed messages (never Google's text, a token, or a file's contents) to the frame.
11
+ */
12
+ import { readAccount } from './drive-files.mjs';
13
+ import { isProductEnabled } from './product-state.mjs';
14
+ import { GoogleWorkspaceError } from './request-policy.mjs';
15
+ import { SHEETS_PRODUCT_ID, sheetsClientFor } from './sheets-client.mjs';
16
+
17
+ /** The plugin's one sign-in: `tovu-connections.json`'s id, declared by every surface that uses it. */
18
+ export const CONNECTION_ID = 'account';
19
+
20
+ const FAILURE_CODE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
21
+
22
+ /** Panel-only refusals (nothing was sent to Google), with the fixed sentence the frame shows. */
23
+ const SURFACE_FAILURES = Object.freeze({
24
+ 'picker-expired': 'The file picker session ended. Close this panel, open it again, and choose the spreadsheet again.',
25
+ 'invalid-destination': 'The form name or tab is not valid. Use the form\'s short name (letters, digits, - or _) and choose a tab from the list.',
26
+ 'tab-not-found': 'That tab is not in the spreadsheet any more. Reload the tabs and choose one again.',
27
+ 'confirm-external-rows': 'Confirm that removing this destination later leaves the rows already written in the spreadsheet, then save again.',
28
+ 'too-many-destinations': 'This plugin keeps at most 50 form destinations. Remove one you no longer use, then save again.',
29
+ });
30
+
31
+ /**
32
+ * A surface-safe failure for a panel-only refusal.
33
+ * @param {{ fail: (input: { code: string, message: string }) => Error, code: keyof typeof SURFACE_FAILURES }} required
34
+ * @returns {Error}
35
+ * @complexity O(1).
36
+ */
37
+ export function surfaceRefusal(required, _optional = {}) {
38
+ return required.fail({ code: required.code, message: SURFACE_FAILURES[required.code] });
39
+ }
40
+
41
+ /**
42
+ * The plugin's one Sheets client (product gate on every request) over the surface port: the client
43
+ * asks for a `connectionRef`, and inside a surface handler that reference is the declared connection
44
+ * id, which the host resolves for this plugin and workspace only.
45
+ * @param {{ capabilities: { http: object, state: object, connections: { getAccessToken(input: { connectionId: string }): Promise<string> } } }} required
46
+ * @returns {{ request(input: object, options?: object): Promise<unknown> }}
47
+ * @complexity O(1).
48
+ */
49
+ export function surfaceSheetsClient(required, _optional = {}) {
50
+ const { http, state, connections } = required.capabilities;
51
+ return sheetsClientFor({
52
+ capabilities: { http, state, connections: { getAccessToken: ({ connectionRef }) => connections.getAccessToken({ connectionId: connectionRef }) } },
53
+ });
54
+ }
55
+
56
+ /** The signed-in Google account (`{ accountId, displayName }`), read through the surface client. @complexity O(1): one request. */
57
+ export function surfaceAccount(required, _optional = {}) {
58
+ return readAccount({ client: surfaceSheetsClient(required), connectionRef: CONNECTION_ID });
59
+ }
60
+
61
+ /** Status → why Sheets cannot be used yet, as a safe code. @complexity O(s) granted scope sets. */
62
+ function connectionRefusal(status) {
63
+ if (status.status === 'reconnect-required') return 'reconnect-required';
64
+ if (status.status !== 'connected') return 'not-connected';
65
+ return status.grantedScopeSetIds.includes(SHEETS_PRODUCT_ID) ? null : 'missing-permission';
66
+ }
67
+
68
+ /**
69
+ * Why the Picker (or anything else Sheets-backed on a panel) cannot run now, or null: the
70
+ * connection must be connected with the Sheets permission granted, and Sheets must be turned on.
71
+ * @param {{ capabilities: { connections: { status(input: { connectionId: string }): Promise<{ status: string, grantedScopeSetIds: readonly string[] }> }, state: { get(input: { key: string }): Promise<unknown> } } }} required
72
+ * @returns {Promise<'not-connected' | 'reconnect-required' | 'missing-permission' | 'product-disabled' | null>}
73
+ * @complexity O(1): one status read and one state read.
74
+ */
75
+ export async function sheetsUnavailableReason(required, _optional = {}) {
76
+ const { connections, state } = required.capabilities;
77
+ const refusal = connectionRefusal(await connections.status({ connectionId: CONNECTION_ID }));
78
+ if (refusal !== null) return refusal;
79
+ return (await isProductEnabled({ state, productId: SHEETS_PRODUCT_ID })) ? null : 'product-disabled';
80
+ }
81
+
82
+ /**
83
+ * Turns a refusal into what the frame may see: a plugin `GoogleWorkspaceError` keeps its code, fixed
84
+ * message and retryable flag; a host refusal with a plain lowercase code keeps its code and message
85
+ * (host messages are written for people and never carry secrets); anything else is rethrown so the
86
+ * host's dispatcher answers with its generic failure and logs the cause.
87
+ * @complexity O(1).
88
+ */
89
+ function surfaceFailure(error, fail) {
90
+ if (error instanceof GoogleWorkspaceError) return fail({ code: error.code, message: error.message }, { retryable: error.retryable });
91
+ const hostRefusal = typeof error?.code === 'string' && FAILURE_CODE.test(error.code) && typeof error.message === 'string';
92
+ return hostRefusal ? fail({ code: error.code, message: error.message }, { retryable: error.retryable === true }) : error;
93
+ }
94
+
95
+ /**
96
+ * Runs one handler body with {@link surfaceFailure} applied to whatever it throws.
97
+ * @param {{ fail: Function, run: () => Promise<unknown> }} required `fail` is the handler call's own.
98
+ * @returns {Promise<unknown>} The body's result.
99
+ * @complexity O(1) plus the body.
100
+ */
101
+ export async function withSurfaceFailures(required, _optional = {}) {
102
+ try {
103
+ return await required.run();
104
+ } catch (error) {
105
+ throw surfaceFailure(error, required.fail);
106
+ }
107
+ }