@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,96 @@
1
+ ---
2
+ name: google-login
3
+ description: Set up Google member sign-in in Tovu or Google via an app's existing Supabase Auth.
4
+ ---
5
+
6
+ # Google login
7
+
8
+ This integration is marked **soon** and has not been tested with a real Google account. Tell the person this before configuring it; do not claim it works until the real sign-in has been checked.
9
+
10
+ ## Choose the destination first
11
+
12
+ Ask whether the site uses **Tovu members** or **Supabase Auth**, or check whether the supabase plugin is active at runtime. An active plugin is a clue; confirm which authentication system the target app actually uses. Never import the supabase plugin or add a Supabase runtime dependency. Its branch below is instructions only.
13
+
14
+ Tovu member login and Google Workspace have separate credentials and activation. Use this plugin for visitor sign-in; Google Workspace owns operator file access. Never replace a working client unless asked. Never ask for the Client Secret in chat, tool arguments, pasted configuration, screenshots, or logs. The person enters it in the secure card or Supabase dashboard themselves.
15
+
16
+ ## Prepare the Google client
17
+
18
+ 1. Open [Google Cloud Console](https://console.cloud.google.com/auth/clients) and select or create the app's project.
19
+ 2. In Google Auth Platform, configure **Branding** (app identity and support contact), **Audience**, and **Data Access**. Choose External unless access is intentionally restricted to one Workspace organization. Request only `openid email profile`; do not add Drive, Sheets, Gmail, offline access, or refresh-token permissions.
20
+ 3. Under **Clients → Create client**, choose **Web application**. The older navigation is **APIs & Services → Credentials → Create Credentials → OAuth client ID**.
21
+ 4. Add the exact authorized redirect URI for the chosen branch below. HTTPS is required for production; Google permits localhost development exceptions. Scheme, host, port, path and any configured base path must match exactly. A JavaScript origin is not a replacement for a redirect URI.
22
+ 5. Check Audience's Testing/Production status before rollout. Identity-only `openid email profile` requests have an exception to the usual testing-user restrictions. Additional permissions can change those rules. Publishing and verification are separate; consult [Google's production readiness guidance](https://developers.google.com/identity/protocols/oauth2/production-readiness/overview) for the app's current requirements.
23
+
24
+ ## Tovu members
25
+
26
+ 1. Enable Google Login in this site's Agent Plugins. It is bundled but starts disabled.
27
+ 2. Read the site's **configured canonical address** and the actual bound workspace ID. Do not infer the address from request Host or forwarded headers. Compute and show the exact callback using the URL API, preserving any configured base path:
28
+
29
+ ```js
30
+ const callback = new URL(configuredSiteAddress);
31
+ callback.pathname = callback.pathname.replace(/\/$/, '')
32
+ + '/api/members/v1/workspaces/' + encodeURIComponent(workspaceId) + '/oidc/google/callback';
33
+ callback.search = '';
34
+ callback.hash = '';
35
+ callback.href;
36
+ ```
37
+
38
+ For example, `http://localhost:3000/api/members/v1/workspaces/<workspaceId>/oidc/google/callback`. Replace the placeholder before showing it to the person. Copy the resulting URI into Google's **Authorized redirect URIs**. Use separate local and deployed clients when appropriate.
39
+ 3. Open the existing secure card with `credential_save { kind: "plugin-provider", target: "google-login-oauth-client" }`. The person pastes the Client ID into **Google client ID** (`username`) and the Client Secret into the masked secret field (`token`). The existing vault seals it; the assistant never receives it. No extra callback field is needed on the card.
40
+ 4. Visit `/signin` and try **Sign in with Google**. Success returns to `/account`. This creates or links a site member, with no administrator privileges. Tovu core owns state, browser binding, PKCE, token validation and member sessions; do not implement a second flow in this plugin.
41
+ 5. Explain initial linking: Google-verified `@gmail.com`/`@googlemail.com` addresses and verified Google Workspace accounts with an `hd` claim auto-link directly. Other Google addresses require **one emailed code** to the member's address before the first auto-link, because Google's email verification can outlive mailbox ownership. Later sign-ins use issuer + subject, even if email changes. Disabled or trashed members cannot sign in.
42
+ 6. Check sign-out and `/account` **Unlink Google**. Unlink requires a recent sign-in and another usable sign-in method; set a password if directed. Unlink must remain effective on the next Google sign-in. Reconnect deliberately with **Connect Google** from the signed-in `/account` page. It does not revoke Google Workspace file access.
43
+
44
+ ## Google via Supabase Auth
45
+
46
+ 1. Open the target project's **Supabase dashboard → Authentication → Providers → Google** and enable Google.
47
+ 2. The person enters the **same Google Client ID and Client Secret** directly in that Supabase panel themselves, never in chat. Tovu's credential card is unnecessary unless this site also uses Tovu members.
48
+ 3. Copy the exact **Callback URL (for OAuth)** shown on that panel. Add it to the Google OAuth client's **Authorized redirect URIs**. A hosted callback commonly looks like `https://<project-ref>.supabase.co/auth/v1/callback`; use the displayed value, including a custom domain if configured. For local Supabase, use the callback shown by that instance.
49
+ 4. One Google Web application client can list both the Supabase callback and Tovu's callback when both systems are used. Keep both exact URIs in Google; do not substitute one for the other.
50
+ 5. In Supabase **Authentication → URL Configuration**, configure the app's own allowed return URL. The Google-to-Supabase callback and the Supabase-to-app return URL serve different steps.
51
+ 6. Test through the app's existing Supabase sign-in flow. These instructions do not automatically associate Supabase users with Tovu members or replace the app's session system. See [Supabase's Google setup](https://supabase.com/docs/guides/auth/social-sign-in/auth-google).
52
+
53
+ ## Troubleshooting without exposing secrets
54
+
55
+ - `redirect_uri_mismatch`: compare the exact configured callback with Google's authorized list, including scheme, port and base path.
56
+ - Missing Tovu button: check plugin activation, the saved Client ID/Secret card and configured canonical address. Do not switch to Workspace credentials.
57
+ - Browser/state failure: restart from `/signin` in the same browser; do not forward an authorization URL, bypass binding, weaken validation or add issuer aliases.
58
+ - Google access denied: check Audience restrictions and the selected account; request only identity permissions. Never copy callback query strings, codes, state, verifier, tokens, claims or upstream error bodies into chat or logs. Report fixed outcome codes only.
59
+
60
+ ## Embed the sign-in button on a public page
61
+
62
+ Use the existing `content_read.plugin` export contract for `tovu/google-login` → `sign-in-button`.
63
+ The plugin must be enabled and its Google client configured. `host-unavailable` means finish
64
+ setup first; `props-required` means the host is configured and the author must choose a variant.
65
+ The required `variant` is `light`, `dark`, or `neutral`; no template defaults or logic are added.
66
+ Use the marker owner to serialize config; do not construct a second JSON attribute escaper.
67
+
68
+ ```html
69
+ <div data-embed-config='{"type":"plugin","plugin":"tovu/google-login","component":"sign-in-button","version":1,"props":{"variant":"light"}}'>
70
+ <p>Google sign-in is currently unavailable.</p>
71
+ </div>
72
+ ```
73
+
74
+ Keep a useful authored fallback. Disabled/unconfigured plugins render that fallback without a
75
+ button. Static exports always render the fallback, while bundling the stylesheet and mark.
76
+ The action is a native link to the host's same-origin start route; Tab focuses it and Enter
77
+ starts sign-in, including with JavaScript off. Do not replace the host link or add OAuth JavaScript.
78
+ The Interactive editor uses a protected card; Preview and the published page render the real UI.
79
+ The card's availability is a page-load snapshot; reload after changing plugin/client setup.
80
+
81
+ Theme hooks are `.plugin-google-login-sign-in-button`, its `__mark` and `__label` parts,
82
+ and its `--light`, `--dark`, `--neutral` variant classes. Defaults live in `@layer plugin-exports`,
83
+ so ordinary theme CSS wins. Set inherited custom properties on the authored wrapper or a theme:
84
+ `--plugin-signin-bg`, `--plugin-signin-fg`, `--plugin-signin-border`, `--plugin-signin-radius`,
85
+ `--plugin-signin-height`, `--plugin-signin-padding`, `--plugin-signin-gap`, `--plugin-signin-font`,
86
+ `--plugin-signin-font-size`, `--plugin-signin-font-weight`, `--plugin-signin-line-height`,
87
+ `--plugin-signin-letter-spacing`, `--plugin-signin-hover-shadow`, `--plugin-signin-active-shadow`,
88
+ `--plugin-signin-focus-color`, `--plugin-signin-focus-width`, `--plugin-signin-focus-offset`.
89
+
90
+ Keep Google's fixed sign-in wording, approved palette, spacing, visible focus, and full-color G.
91
+ Do not stretch, recolor, filter or replace the mark. The mark stays on white in all variants.
92
+ Prefer Google Sans Medium when the host theme supplies it; the offline fallback is Arial.
93
+ Check the current [Google branding rules](https://developers.google.com/identity/branding-guidelines)
94
+ before visual acceptance. The unchanged gradient SVG is bundled from
95
+ [SVGL](https://github.com/pheralb/svgl/blob/main/static/library/google.svg)
96
+ (Git blob `8ab1351b939a780aa05645d31e5bed3fcf5c5493`); Google's trademarks remain Google's.
@@ -0,0 +1,21 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "providers": [
4
+ {
5
+ "id": "google",
6
+ "label": "Google",
7
+ "issuer": "https://accounts.google.com",
8
+ "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth",
9
+ "tokenEndpoint": "https://oauth2.googleapis.com/token",
10
+ "jwksUri": "https://www.googleapis.com/oauth2/v3/certs",
11
+ "scopes": ["openid", "email", "profile"],
12
+ "signingAlgorithm": "RS256",
13
+ "clientAuthentication": "client_secret_post",
14
+ "pkce": "S256",
15
+ "credentialLabel": "google-login/google OAuth client",
16
+ "credentialCardTarget": "google-login-oauth-client",
17
+ "authoritativeEmailDomains": ["gmail.com", "googlemail.com"],
18
+ "hostedDomainClaim": "hd"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,132 @@
1
+ # Google Workspace plugin (`google-workspace`)
2
+
3
+ ## Purpose
4
+
5
+ One bundled Agent Plugin for everything Google: the Google sign-in and every Google product as a
6
+ module. v1 ships Sheets. All Google-specific code (endpoints, scopes, request formats, response
7
+ parsing, revocation, later the Picker) lives here; host core and Jini core name no provider. It ships
8
+ as `@jini-ai/agent-plugins/google-workspace` (this folder, `packages/agent-plugins/google-workspace`).
9
+ A host that selects it stages the payload into its bundled-source root and seeds it **disabled**;
10
+ the bundled-digest ledger records its digest at seed time, which the executable trust gate checks
11
+ before any module loads. Host-specific chat guidance (exact tool names, admin paths, the OAuth
12
+ callback route) lives in the host's overlay for this plugin, not here.
13
+
14
+ ## Layout
15
+
16
+ | Path | What it is |
17
+ |---|---|
18
+ | `plugin.json`, `mcp.json` | Manifest (no MCP servers). |
19
+ | `tovu-connections.json` | The sign-in (the `tovu-*.json` files are the Tovu host's contribution contracts; their names and keys are frozen): Google endpoints, per-client-kind parameters, Picker start params and the `picked_file_ids` callback field, and the NAMES of the host config holding the client ids (`TOVU_GOOGLE_WORKSPACE_OAUTH_{WEB,DESKTOP}_CLIENT_{ID,SECRET}`, placeholders until the Cloud project exists). `byoClient: true` lets a workspace admin use their own OAuth client instead (saved as the custom credential `google-workspace/account OAuth client`; see `skills/google-workspace/references/connection.md`). |
20
+ | `products.json` | Product catalog: id, label, tool prefix (every tool named with it belongs to the product and is gated by it; a security pin enumerates them), required scopes. Sheets = `drive.file` only. |
21
+ | `tovu-agent-tools.json` + `tools/` | Executable tools (the host adds its own prefix to each name): `account_info`, and the Sheets tools in `tools/sheets/` (see its README). |
22
+ | `tovu-admin-surfaces.json` + `ui/` | Plugin-owned admin surfaces: the connection panel, Choose spreadsheet (Picker) and Form destinations, each with a server-side `handlerModule`. See `ui/README.md`. |
23
+ | `tovu-form-destinations.json` + `form-destinations/` | Push-only `google-sheets` destination (no `receive`): frozen column mapping, bounded submission-id dedupe, contract-shaped results. See `form-destinations/README.md`. Not yet called by the host's forms fan-out (wired in a later lot). |
24
+ | `lib/` | Shared code (below). |
25
+ | `skills/google-workspace/` | What the in-app assistant reads. |
26
+ | `__tests__/` | Plugin-local `node:test` suites (not shipped): `fake-google-http.mjs` (127.0.0.1 fake Google), the OAuth contract pins and the skill-content pins. |
27
+
28
+ ## `lib/` public API
29
+
30
+ | Module | Exports |
31
+ |---|---|
32
+ | `auth.mjs` | Default `{ createHooks({ http }) }`: Jini's `ApiConnectionHooks` (`resolveScopes`, `readAccount`, `revoke`). `GOOGLE_REVOCATION_ENDPOINT`, `GOOGLE_ACCOUNT_ACCESS_PAGE`. |
33
+ | `http.mjs` | `createGoogleClient({ http, connections }, { sleep, random }).request({ connectionRef, url, method, query, body, kind })`; `accessTokenFor`; `buildUrl`; `pathSegment` (one encoded path segment, dot segments refused). |
34
+ | `drive-files.mjs` | `readAccount({ client, connectionRef })` via Drive `about.get` (works under `drive.file`); `copyFile({ client, connectionRef, fileId, name }, { parentId })` via Drive `files.copy`. |
35
+ | `sheets-client.mjs` | The one Sheets client: `sheetsClientFor({ capabilities })` (Sheets product gate per request), `googleClientFor` (ungated: delivery), `sendSheetsRequest`, `assertSpreadsheetId` / `SPREADSHEET_ID_PATTERN` (the one id rule, also every schema's pattern), `sheetsUrl` (the one URL builder; checks the id), `getSpreadsheet`, `runBatchUpdate` (inventory check first), `valueInputOption` (RAW default), `SHEETS_LIMITS` and the bounds (`boundValueRanges`, `assertPayloadWithin`, `outputFits` / `assertOutputWithin`, `spreadsheetFields` (grid-data masks need the opt-in)), `resourceLines` (card lines), `spreadsheetLine`, `tabsOf`. |
36
+ | `sheets-a1.mjs` | A1 helpers, one name and contract each: `columnName`, `columnIndex`, `parseA1Range` (Google's normalized range → parts or undefined), `parseBoundsA1` (a plain `A1:B2` → bounds, else `invalid-input`), `tabRange` (tab-title quoting), `gridRangeLabel` (card label). |
37
+ | `product-state.mjs` | `PRODUCTS` (validated catalog), `scopesFor({ productIds })`, and the product gate: `PRODUCT_STATE_KEY` (`'products'`), `isProductEnabled({ state, productId })`, `assertProductEnabled(...)` (`product-disabled`); form delivery uses `isProductEnabled` over the read-only plugin-state reader the host passes as `optional.state`. A product is off unless the plugin's settings hold `products: { <id>: true }`. |
38
+ | `picker-selection.mjs` | `verifySelectedSpreadsheet({ client, connectionRef, fileId, expectedAccountId })` (plain Drive id, same account, spreadsheet MIME via Drive `files.get`, real access via Sheets `spreadsheets.get`; metadata only), `verifySpreadsheetAccess({ client, connectionRef, fileId })` (live spreadsheet per Drive, then opened by the Sheets API; shared with `sheets_open_existing`), `parsePickedFileIds({ value })` (desktop callback field → one id or null on cancel), `DESKTOP_PICKER_START_PARAMS`, `SPREADSHEET_MIME_TYPE`. |
39
+ | `surface-actions.mjs` | For the admin-surface handlers: `CONNECTION_ID` (`'account'`), `surfaceSheetsClient({ capabilities })` (the one Sheets client over the surface port, which names connections by id), `surfaceAccount`, `sheetsUnavailableReason`, `withSurfaceFailures` (plugin/host refusals → surface-safe failures), `surfaceRefusal`. |
40
+ | `picker-flow.mjs` | `createPickerFlow({}, { now, randomId })` → `bootstrap` (hosted session + short-lived token, or `system-browser` / `unavailable`), `verifyHosted`, `startSystemBrowser`, `finishSystemBrowser`; `readPickedSpreadsheets`, `rememberPickedSpreadsheet`, `PICKER_CONFIG_NAMES`, `PICKER_SESSION_TTL_MS`. |
41
+ | `form-destination-settings.mjs` | `readFormDestinations({ deliveryBindings })`, `saveFormDestination({ deliveryBindings, formSlug, destination }, { bindingId })`, `SHEETS_ADAPTER_ID`, `FORM_SLUG_PATTERN`, `MAX_FORM_DESTINATIONS`. |
42
+ | `request-policy.mjs` | `GoogleWorkspaceError`, `classifyFailure`, `parseRetryAfter`, `backoffMs`, `shouldRetryInline`. |
43
+
44
+ ## Rules the plugin keeps
45
+
46
+ - Tokens come only from the host capability, right before each request; only access tokens ever
47
+ reach the plugin, and none is returned, logged or put in an error.
48
+ - Errors carry a fixed message per code, never Google's text (it echoes ranges and values).
49
+ - `kind: 'write'` (append, create, copy) with an unknown outcome is `uncertain-outcome` and never
50
+ retried; reads and idempotent writes retry with jittered backoff that honours Retry-After.
51
+ - Revocation is confirmed only by Google's 200 (or `invalid_token`: already revoked); a failed
52
+ revoke throws `provider-unreachable` or `provider-refused` for the host to record.
53
+ - An access token can expire in flight: a 401 gets one more token read and is retried only if the
54
+ token differs. An unchanged token or second 401 is `reconnect-required`. Tools and form delivery share this.
55
+ - The budgets declared in `tovu-connections.json` (45/min per account, 240/min per instance) are
56
+ charged by the host on every token read; its refusal (`rate-limited`) becomes `quota-delay` with
57
+ the host's `retryAfterMs` and `outcome: 'not-applied'`, and follows the same inline backoff rules
58
+ as Google's 429 (nothing was sent, so even an append may wait and retry).
59
+ - Modules import nothing outside this directory except what the host injects.
60
+
61
+ ## How it fits
62
+
63
+ The host reads each contribution file and runs it on Jini's services: `tovu-connections.json` on
64
+ `@jini-ai/integrations/api-connections`; `tovu-agent-tools.json` on
65
+ `@jini-ai/agent-plugins/lifecycle/tools`; `tovu-admin-surfaces.json` on
66
+ `@jini-ai/agent-plugins/admin-surfaces`; `tovu-form-destinations.json` through the host's form
67
+ destination registry. In Tovu those readers are `features/api-connections/contribution.ts`,
68
+ `features/agent-plugins/executable-tools`, `features/agent-plugins/admin-surfaces` and
69
+ `features/form-destinations/registry.ts`.
70
+
71
+ ## Extension points
72
+
73
+ A new product adds an entry to `products.json` (its scopes), its tools to `tovu-agent-tools.json`
74
+ under its own prefix, and its modules under `products/<id>/`, all on the same sign-in.
75
+
76
+ ## Known gaps
77
+
78
+ - Duplicate submission ids found in a tab are delivered as `ok` with a "review" note in
79
+ `reference`, which the forms ledger drops; the contract has no reviewable success outcome, so
80
+ no person sees it yet (Jini contract change needed for a distinct outcome).
81
+ - Form-delivery retry window: product-off (`adapter-unavailable`) and quota (`rate-limited`)
82
+ refusals are retried within the site's forms retry policy (about 8 attempts over about a day),
83
+ then the job fails; authorization failures (`authorization-required`) fail at once and stay as
84
+ failed jobs, with no automatic re-send after a reconnect. Owner decision pending on longer holds
85
+ (for example a `paused` outcome resumed by turning the product on or reconnecting).
86
+
87
+ - Products are read from the plugin's settings and default to OFF; the connection panel writes them,
88
+ and the host hands form delivery the same settings (read-only) as `optional.state`.
89
+ - The Form destinations panel saves each form's choice as the form's delivery binding through the
90
+ host's `deliveryBindings` capability (both the destination and connection surfaces declare
91
+ `deliveryAdapters: ["google-sheets"]`): one binding per form slug, on this plugin's `account`
92
+ connection, config = spreadsheet, tab (numeric sheet id), the frozen `accountId`, plus the names the
93
+ panels show. The host writes it to `core.forms/deliveryBindings` as the signed-in admin.
94
+ - Panels (G13b) are built and unit-tested only; they need a Chrome check (hosted Picker inside the
95
+ sandboxed frame, desktop system-browser Picker, the account-page button). The host says whether it
96
+ is hosted or desktop (`context.hostKind`); a hosted site also needs both
97
+ `TOVU_GOOGLE_PICKER_BROWSER_KEY` and `TOVU_GOOGLE_PICKER_APP_ID` (placeholders, never real values)
98
+ or the Picker is `picker-not-configured`. The panels cannot open links themselves (no
99
+ `allow-popups`): the connection panel declares `linkOrigins: ["https://myaccount.google.com"]` and
100
+ asks the admin page to open the Google account-access page (`client.openLink`); the address also
101
+ stays visible as text.
102
+ - Quotas: the per-account (45/min) and per-instance (240/min) budgets declared in
103
+ `tovu-connections.json` are implemented by the host and charged on every token read; there is no
104
+ plugin-side limiter. Form delivery reads a token per Google request (`credential.getAccessToken`),
105
+ so it is charged per request like the tools.
106
+ - Hosted Picker sessions are kept one per surface (`picker-session:<surfaceId>`), not per tab or
107
+ admin: opening the same panel in a second tab (or by a second admin) starts a new session and
108
+ the first tab's pick is refused as `picker-expired`. Documented in
109
+ `skills/google-workspace/references/troubleshooting.md`; not changed (a pick only needs one
110
+ open panel).
111
+
112
+ ## Sheets admin tools (`tools/sheets-admin/`, lot G12)
113
+
114
+ Eleven tools for tabs, sorting, filters, filter views, formatting, named and protected ranges,
115
+ developer metadata, raw batch updates, CSV export and opening a picked file; see
116
+ `tools/sheets-admin/README.md`. Their helpers:
117
+
118
+ | Module | Exports |
119
+ |---|---|
120
+ | `lib/sheets-admin-request-kinds.mjs` | `REQUEST_KINDS` (the reviewed batchUpdate inventory: effect, replay class, required fields), `checkRequests`, `describeRequests`, batch bounds. |
121
+ | `lib/sheets-admin-csv.mjs` | `planChunks`, `encodeCsv`, export bounds. |
122
+
123
+ These tools use the plugin-side codes `invalid-input` and `limit-exceeded` (shared with the values
124
+ tools) and add `unsupported-request`; `sheets_open_existing` reuses the Picker's `not-a-spreadsheet`, plus an optional plugin-composed `detail`
125
+ on `GoogleWorkspaceError` (a request index or kind name, never Google's text or a cell value).
126
+
127
+
128
+ Hosted sign-in asks for consent when Jini has no renewable credential stored (`forceConsentParams`),
129
+ so connecting another workspace can obtain offline access. Renewable same-account reconnects keep
130
+ incremental grants without forcing consent. Client settings now use the full plugin namespace,
131
+ `TOVU_GOOGLE_WORKSPACE_OAUTH_{WEB,DESKTOP}_CLIENT_{ID,SECRET}`. Provision these settings
132
+ before enabling this build; the earlier abbreviated names were never configured, so no fallback is needed.
@@ -0,0 +1,87 @@
1
+ # `form-destinations/` — the Google Sheets form destination
2
+
3
+ **Purpose.** Push each accepted form submission into a Google Sheets tab as one row, at least once,
4
+ with best-effort dedupe. Declared in `../tovu-form-destinations.json` as `google-sheets` (push only,
5
+ no `receive`), part of the Sheets product (`../products.json`).
6
+
7
+ ## Public API
8
+
9
+ `sheets.mjs` default export: `deliver(required, optional)`, the forms delivery contract's push
10
+ method (see the forms-multidest delivery contract, §2–§3).
11
+
12
+ - `required`: `{ kit, credential, form, submission, binding, idempotencyKey }` exactly as the host
13
+ hands a frozen job. `credential.token` is the access token the host resolved for this attempt
14
+ from `binding.connectionRef`; it is used only in the `authorization` header.
15
+ - `optional`: `{ state? }` — the host's read-only reader of this plugin's settings (the store the
16
+ connection panel writes). Unless it holds `products.sheets === true` (no reader counts as off),
17
+ delivery refuses as `adapter-unavailable`, retryable: while Sheets is off a job is retried within
18
+ the forms retry policy (about 8 attempts over about a day), then marked failed; no reader never enables delivery.
19
+ - `credential.getAccessToken()` (OAuth connections): one fresh token per Google request, so the
20
+ host's declared budgets count every request and the frozen account is re-checked each time.
21
+ - Returns a `FormDeliveryResult`; it never throws.
22
+
23
+ Binding config (non-secret, frozen with the job):
24
+
25
+ ```json
26
+ { "spreadsheetId": "…", "accountId": "…", "tabs": { "<formSlug>": { "sheetId": 7 } } }
27
+ ```
28
+
29
+ `accountId` is read by the host, not this module: the OAuth connection port refuses a job whose
30
+ connection now holds another account (`authorization-required`), and a binding without one
31
+ (`configuration-invalid`).
32
+
33
+ Which column holds which field is kept in the sheet, not the binding: the first time a field is
34
+ written, its column gets a developer-metadata marker (`tovu_form_field` = the field id, visibility
35
+ `DOCUMENT`, attached to that one column). The marker decides the column from then on, so a label
36
+ edited in the form, a header renamed in the sheet, or columns inserted or moved before it keep the
37
+ field's data in one column (Google moves column metadata with the column). An untagged field is
38
+ matched by its label in row 1 and tagged, or gets a new column at the end (header = label, else
39
+ field id). Chosen over the binding because the adapter cannot write the host's binding, and over a
40
+ header note because people neither see nor edit metadata. Growing the grid, writing new headers
41
+ (as text cells) and tagging columns happen in ONE atomic `batchUpdate` before the append.
42
+
43
+ More than one row already holding the submission id (another writer, or a late append racing the
44
+ scan) is reported as delivered with the reference `sheet <id> rows <a>, <b> (duplicate submission
45
+ id: review)`; nothing is appended again. Nobody sees that note today: the forms delivery contract has no reviewable success outcome and the ledger keeps only `ok` (the
46
+ reference is dropped), and any `ok: false` code would fail or re-try a row that is already
47
+ there. A distinct outcome needs a contract change in Jini (`@jini-ai/cms/forms`).
48
+
49
+ ## Files
50
+
51
+ | file | job |
52
+ |---|---|
53
+ | `sheets.mjs` | One attempt: product check → tab by sheet id → header row + column markers → dedupe scan → one atomic column batch (grid, headers, markers) → RAW append → re-scan after a lost answer. |
54
+ | `mapping.mjs` | Field id ↔ marker / header ↔ column (`buildMapping`, `readMarkers`, `planColumns`); mapping errors; the RAW row (checkbox groups joined `", "`). |
55
+ | `dedupe.mjs` | Scans the `_tovu_submission_id` column in 5,000-row chunks; > 50,000 data rows → `destination-limit`. |
56
+ | `delivery-result.mjs` | The contract's result shape and the plugin-error → contract-code table. |
57
+
58
+ ## How it fits
59
+
60
+ All HTTP goes through `../lib/sheets-client.mjs` → `../lib/http.mjs` (the one Google client:
61
+ retries, Retry-After, safe errors; quotas are the host's budgets). The host's `kit.fetch(url, init, { timeoutMs })` is
62
+ the transport. The forms ledger (outbox) owns attempts, backoff and terminal state; this module
63
+ only reports one attempt's outcome.
64
+
65
+ Not exactly-once: there are no row reservations, and a late append or another writer can race the
66
+ scan. Two bindings writing to the same tab share the id column, so the second sees the first's row
67
+ as already delivered.
68
+
69
+ ## Extension points
70
+
71
+ A second destination (another product or another provider plugin) copies this shape: a
72
+ push-only descriptor, a `deliver` that maps its own errors to the nine contract codes, and its own
73
+ dedupe. Nothing here is imported by a host's core or Jini core.
74
+
75
+ ## Wiring (G14b) and what is left
76
+
77
+ Wired: the host composes the OAuth connection port ahead of saved custom credentials, so a binding
78
+ whose `connectionRef` is this plugin's connection resolves through it (`additionalHosts` = the
79
+ declared `apiOrigins` after the first, as exact origins); delivery gets `optional.state` and a
80
+ per-request `getAccessToken()`.
81
+
82
+ Bindings (G14c): the destination panel saves each form's choice as its binding through the host's
83
+ admin-surface `deliveryBindings` capability (`../lib/form-destination-settings.mjs`). The binding names
84
+ the form by slug (the host stores the form's row id) and the `account` connection by id (the host
85
+ stores its own ref); an admin clicking Save on the panel is the human action. A binding whose plugin
86
+ is disabled is not delivered (`adapter-unavailable`, retryable: the host reads only active plugins'
87
+ adapters on every attempt), the same code as Sheets turned off here.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @file Best-effort dedupe for the Google Sheets form destination: finds the rows whose
3
+ * `_tovu_submission_id` cell holds a submission id, by reading that one column in bounded chunks.
4
+ *
5
+ * Exists because a retry (a lost answer, a replayed job) must not add the same submission twice,
6
+ * and Google's append has no idempotency key. The delivery scans before every append and again
7
+ * after an append whose outcome is unknown. The scan covers every row of the tab's grid, so
8
+ * "not found" is a complete answer; a tab with more than {@link DEDUPE_LIMITS.maxScannedRows} data
9
+ * rows is refused as `destination-limit` rather than appended to on an incomplete search.
10
+ *
11
+ * This is NOT exactly-once: a late append or another writer can still race the scan, and there
12
+ * are no row reservations. Delivery is at-least-once with best-effort dedupe (documented in
13
+ * `references/form-delivery.md`).
14
+ */
15
+ import { DeliveryRefusal } from './delivery-result.mjs';
16
+
17
+ export const DEDUPE_LIMITS = Object.freeze({ maxScannedRows: 50_000, chunkRows: 5_000 });
18
+
19
+ /** Row numbers (1-based) in `[from, to]` of a chunk whose cell equals the id. @complexity O(rows). */
20
+ function matchesIn(values, from, submissionId) {
21
+ return values.flatMap((row, offset) => (String(row?.[0] ?? '').trim() === submissionId ? [from + offset] : []));
22
+ }
23
+
24
+ /**
25
+ * Every data row (row 2 onwards) whose id cell equals `submissionId`, in sheet order.
26
+ * @param {{ readColumn: (input: { from: number, to: number }) => Promise<readonly (readonly unknown[])[]>, rowCount: number, submissionId: string }} required
27
+ * `readColumn` returns the id column's rows `from..to` (rows-major; Google omits trailing blanks).
28
+ * `rowCount` is the tab's grid row count, header included.
29
+ * @returns {Promise<number[]>}
30
+ * @throws {DeliveryRefusal} `destination-limit` (not retryable) when the tab has more data rows
31
+ * than the scan ceiling; nothing is read then.
32
+ * @complexity O(rowCount / chunkRows) reads, O(rowCount) cells, each read ≤ chunkRows cells.
33
+ */
34
+ export async function findSubmissionRows(required, _optional = {}) {
35
+ const { readColumn, rowCount, submissionId } = required;
36
+ if (rowCount - 1 > DEDUPE_LIMITS.maxScannedRows) throw new DeliveryRefusal({ code: 'destination-limit', retryable: false });
37
+ const found = [];
38
+ for (let from = 2; from <= rowCount; from += DEDUPE_LIMITS.chunkRows) {
39
+ const to = Math.min(rowCount, from + DEDUPE_LIMITS.chunkRows - 1);
40
+ found.push(...matchesIn(await readColumn({ from, to }), from, submissionId));
41
+ }
42
+ return found;
43
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @file What the Google Sheets form destination reports back to the forms delivery ledger: the
3
+ * contract's `FormDeliveryResult` and the translation from the plugin's safe Google errors to the
4
+ * contract's nine codes.
5
+ *
6
+ * Exists because only the bare code, `retryable`, `retryAfterMs` and `outcome` ever reach the
7
+ * ledger (the host drops everything else, and a thrown error would become a generic retryable
8
+ * failure). So every failure of a delivery is turned into one of those values here, in one table,
9
+ * and no message, Google text, token or answer ever leaves the adapter. What the person does next
10
+ * for each code is documented in `skills/google-workspace/references/form-delivery.md`.
11
+ */
12
+ import { GoogleWorkspaceError } from '../lib/request-policy.mjs';
13
+
14
+ const DELIVERY_FAILED = 'delivery-failed';
15
+
16
+ /** A refusal decided by the adapter itself (config, mapping, dedupe limit, product off). */
17
+ export class DeliveryRefusal extends Error {
18
+ /**
19
+ * @param {{ code: string, retryable: boolean }} required A contract code; `message` is the code.
20
+ * @param {{ outcome?: 'not-applied' | 'unknown' }} [optional] Defaults to `not-applied`: every
21
+ * adapter refusal happens before the append is sent, except the uncertain-append verdict.
22
+ */
23
+ constructor(required, optional = {}) {
24
+ super(required.code);
25
+ this.name = 'DeliveryRefusal';
26
+ this.code = required.code;
27
+ this.retryable = required.retryable;
28
+ this.outcome = optional.outcome ?? 'not-applied';
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Plugin safe code → [contract code, retryable]. Authorization problems are not retried: the job
34
+ * fails at once and stays in the ledger as failed (it is not re-sent after a reconnect). Quota,
35
+ * product-off and transient failures are retried within the forms retry policy (about 8 attempts
36
+ * over about a day), then the job fails.
37
+ */
38
+ const CONTRACT_CODES = new Map([
39
+ ['not-connected', ['authorization-required', false]], ['reconnect-required', ['authorization-required', false]],
40
+ ['account-changed', ['authorization-required', false]], ['missing-permission', ['authorization-required', false]],
41
+ ['file-not-granted', ['resource-not-granted', false]], ['access-denied', ['resource-not-granted', false]],
42
+ ['admin-policy', ['resource-not-granted', false]],
43
+ ['quota-delay', ['rate-limited', true]],
44
+ ['invalid-request', ['mapping-invalid', false]], ['invalid-input', ['configuration-invalid', false]],
45
+ ['limit-exceeded', ['destination-limit', false]],
46
+ ['uncertain-outcome', ['outcome-unknown', true]],
47
+ ['product-disabled', ['adapter-unavailable', true]],
48
+ ['conflict', [DELIVERY_FAILED, true]], ['service-unavailable', [DELIVERY_FAILED, true]], ['network-error', [DELIVERY_FAILED, true]],
49
+ ]);
50
+
51
+ /** `{ ok: false, error }` with only the fields the contract keeps. @complexity O(1). */
52
+ export function failureResult(required, _optional = {}) {
53
+ const { code, retryable, outcome, retryAfterMs } = required;
54
+ return { ok: false, error: { code, retryable, ...(outcome ? { outcome } : {}), ...(retryAfterMs === undefined ? {} : { retryAfterMs }) } };
55
+ }
56
+
57
+ /**
58
+ * The result a failed delivery reports. A plugin error keeps its outcome and Retry-After; anything
59
+ * unrecognised is a retryable `delivery-failed` with no outcome claimed, and its text is dropped.
60
+ * @param {{ error: unknown }} required
61
+ * @complexity O(1).
62
+ */
63
+ export function resultForError(required, _optional = {}) {
64
+ const { error } = required;
65
+ if (error instanceof DeliveryRefusal) return failureResult(error);
66
+ if (!(error instanceof GoogleWorkspaceError)) return failureResult({ code: DELIVERY_FAILED, retryable: true });
67
+ const [code, retryable] = CONTRACT_CODES.get(error.code) ?? [DELIVERY_FAILED, false];
68
+ return failureResult({ code, retryable, outcome: error.outcome, retryAfterMs: error.retryAfterMs });
69
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @file Column mapping for the Google Sheets form destination: which column each form field is
3
+ * written to, which new columns a delivery adds, and the row of RAW values one submission becomes.
4
+ *
5
+ * Exists so a spreadsheet's columns never silently change meaning. Fields are identified by their
6
+ * stable field id (`name`); the label is only the header text. The first time the plugin writes a field
7
+ * to a column it tags that column with a developer-metadata marker (`tovu_form_field` = field id,
8
+ * attached to the column itself). From then on the marker decides the column: a label edited in
9
+ * the form, a header renamed in the sheet, or columns inserted or moved before it do not move the
10
+ * field's data (Google moves column metadata with its column). Why a marker in the sheet and not
11
+ * the binding: the delivery adapter cannot write the host's binding, and a marker needs no state
12
+ * outside the spreadsheet it describes; unlike a header note, people do not see or edit it.
13
+ * An untagged field is matched by its label in row 1 (and tagged), else added as a new column at
14
+ * the end. A tab that should have the plugin's columns but lacks them, a header found twice, a field
15
+ * tagged on two columns, or two entries landing on one column is a mapping error the person
16
+ * repairs; nothing is appended into a sheet whose columns cannot be matched. The visitor's IP is
17
+ * never a field, and answers that are not fields of the frozen form are never written.
18
+ */
19
+ import { DeliveryRefusal } from './delivery-result.mjs';
20
+
21
+ // These names keep their `tovu` prefix: deliveries have already written them into people's
22
+ // spreadsheets, and a renamed header or marker key would no longer match those columns.
23
+ export const SUBMISSION_ID_HEADER = '_tovu_submission_id';
24
+ export const SUBMITTED_AT_HEADER = '_tovu_submitted_at';
25
+ /** Developer-metadata key tagging a column with the form field id it holds. */
26
+ export const FIELD_MARKER_KEY = 'tovu_form_field';
27
+
28
+ const mappingInvalid = () => new DeliveryRefusal({ code: 'mapping-invalid', retryable: false });
29
+
30
+ /** One answer as a RAW cell: a checkbox group is joined with ", "; scalars keep their type. @complexity O(items). */
31
+ export function cellValue(required, _optional = {}) {
32
+ const { value } = required;
33
+ if (value === undefined || value === null) return '';
34
+ if (Array.isArray(value)) return value.map((item) => String(item)).join(', ');
35
+ return typeof value === 'number' || typeof value === 'boolean' ? value : String(value);
36
+ }
37
+
38
+ /**
39
+ * The mapping for one job: the two plugin columns, then one entry per field of the frozen form.
40
+ * @param {{ form: { fields: readonly { name: string, label?: string }[] } }} required
41
+ * @returns {{ header: string, field?: string, source?: 'id' | 'submittedAt' }[]}
42
+ * @throws {DeliveryRefusal} `mapping-invalid` when two entries would share a header.
43
+ * @complexity O(fields).
44
+ */
45
+ export function buildMapping(required, _optional = {}) {
46
+ const entries = [
47
+ { header: SUBMISSION_ID_HEADER, source: 'id' },
48
+ { header: SUBMITTED_AT_HEADER, source: 'submittedAt' },
49
+ // Trimmed like the sheet's cells are, so stray spaces never decide a match; a blank label falls back to the id.
50
+ ...required.form.fields.map((field) => ({ field: field.name, header: String(field.label ?? '').trim() || field.name })),
51
+ ];
52
+ if (new Set(entries.map((entry) => entry.header)).size !== entries.length) throw mappingInvalid();
53
+ return entries;
54
+ }
55
+
56
+ /** Whether a dimension range is exactly one column of this tab. @complexity O(1). */
57
+ const isOneColumnOf = (range, sheetId) => range?.sheetId === sheetId && range.dimension === 'COLUMNS' && range.endIndex - range.startIndex === 1;
58
+
59
+ /** One column marker as `{ field, index }`; anything not exactly one column of this tab is ignored. @complexity O(1). */
60
+ function columnMarker(metadata, sheetId) {
61
+ const range = metadata?.location?.dimensionRange;
62
+ const isMarker = metadata?.metadataKey === FIELD_MARKER_KEY && typeof metadata.metadataValue === 'string';
63
+ return isMarker && isOneColumnOf(range, sheetId) ? { field: metadata.metadataValue, index: range.startIndex } : undefined;
64
+ }
65
+
66
+ /**
67
+ * Field id → the columns tagged with it, from a `developerMetadata:search` answer.
68
+ * @param {{ matches: readonly unknown[], sheetId: number }} required `matchedDeveloperMetadata`.
69
+ * @returns {Map<string, number[]>}
70
+ * @complexity O(matches).
71
+ */
72
+ export function readMarkers(required, _optional = {}) {
73
+ const markers = new Map();
74
+ for (const match of required.matches) {
75
+ const marker = columnMarker(match?.developerMetadata, required.sheetId);
76
+ if (marker) markers.set(marker.field, [...(markers.get(marker.field) ?? []), marker.index]);
77
+ }
78
+ return markers;
79
+ }
80
+
81
+ const headerText = (cell) => String(cell ?? '').trim();
82
+
83
+ /** Untagged columns of row 1 whose header is `header`. @complexity O(width). */
84
+ function headerPositions(plan, header) {
85
+ return plan.headerRow.flatMap((cell, index) => (headerText(cell) === header && !plan.tagged.has(index) ? [index] : []));
86
+ }
87
+
88
+ /** A new column at the end, headed `header`. @complexity O(1). */
89
+ function addColumn(plan, header) {
90
+ const index = plan.width;
91
+ plan.width += 1;
92
+ plan.newHeaders.push({ index, header });
93
+ return index;
94
+ }
95
+
96
+ /** A plugin column (`_tovu_*` header): found once by its fixed header, or new on a fresh tab. @complexity O(width). */
97
+ function tovuColumn(plan, entry) {
98
+ const positions = headerPositions(plan, entry.header);
99
+ if (positions.length === 1) return positions[0];
100
+ if (positions.length > 1 || !plan.fresh) throw mappingInvalid();
101
+ return addColumn(plan, entry.header);
102
+ }
103
+
104
+ /** A field's column: its marker, else its header found once, else a new column; untagged ones get tagged. @complexity O(width). */
105
+ function fieldColumn(plan, entry, taggedAt) {
106
+ if (taggedAt.length > 1) throw mappingInvalid();
107
+ if (taggedAt.length === 1) return taggedAt[0];
108
+ const positions = headerPositions(plan, entry.header);
109
+ if (positions.length > 1) throw mappingInvalid();
110
+ const index = positions.length === 1 ? positions[0] : addColumn(plan, entry.header);
111
+ plan.untagged.push({ field: entry.field, index });
112
+ return index;
113
+ }
114
+
115
+ /**
116
+ * Where each mapping entry is written. An empty first row is a fresh tab. The plugin's two columns are
117
+ * found by their fixed headers (refused when missing from a tab that is not fresh); each field by
118
+ * its marker, else by its label (see the file comment).
119
+ * @param {{ mapping: readonly { header: string, field?: string, source?: string }[], headerRow: readonly unknown[], markers: ReadonlyMap<string, readonly number[]> }} required
120
+ * @returns {{ columns: number[], newHeaders: { index: number, header: string }[], untagged: { field: string, index: number }[], width: number }}
121
+ * `newHeaders` are contiguous at the end; `untagged` are the field columns to tag.
122
+ * @throws {DeliveryRefusal} `mapping-invalid` for a duplicated, missing or conflicting column.
123
+ * @complexity O(entries * width).
124
+ */
125
+ export function planColumns(required, _optional = {}) {
126
+ const { mapping, markers } = required;
127
+ const fresh = required.headerRow.every((cell) => headerText(cell) === '');
128
+ const tagged = new Set([...markers.values()].flat());
129
+ const headerRow = fresh ? [] : required.headerRow;
130
+ const plan = { fresh, tagged, headerRow, width: Math.max(headerRow.length, ...[...tagged].map((index) => index + 1)), newHeaders: [], untagged: [] };
131
+ const columns = mapping.map((entry) => (entry.source ? tovuColumn(plan, entry) : fieldColumn(plan, entry, markers.get(entry.field) ?? [])));
132
+ if (new Set(columns).size !== columns.length) throw mappingInvalid();
133
+ return { columns, newHeaders: plan.newHeaders, untagged: plan.untagged, width: plan.width };
134
+ }
135
+
136
+ /**
137
+ * The appended row: each mapped value at its column, blanks elsewhere (columns of removed fields
138
+ * or someone else's columns stay empty for this row).
139
+ * @param {{ mapping: readonly { field?: string, source?: 'id' | 'submittedAt' }[], columns: readonly number[], width: number, submission: { id: string, submittedAt: string, answers: Readonly<Record<string, unknown>> } }} required
140
+ * @complexity O(width + entries).
141
+ */
142
+ export function buildRow(required, _optional = {}) {
143
+ const { mapping, columns, submission } = required;
144
+ const row = Array.from({ length: required.width }, () => '');
145
+ const valueFor = (entry) => {
146
+ if (entry.source) return submission[entry.source];
147
+ // Own keys only: a field id such as `constructor` must not read the prototype.
148
+ return cellValue({ value: Object.hasOwn(submission.answers, entry.field) ? submission.answers[entry.field] : undefined });
149
+ };
150
+ mapping.forEach((entry, index) => { row[columns[index]] = valueFor(entry); });
151
+ return row;
152
+ }