@voltro/cli 0.28.0 → 0.30.0

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 (354) hide show
  1. package/CHANGELOG.md +655 -0
  2. package/THIRD-PARTY-NOTICES.md +90 -4
  3. package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
  4. package/dist/apiBuild-CxvR1Dwt.js +2 -0
  5. package/dist/bin.js +3 -3
  6. package/dist/checkCommand-BMr-X48w.js +2427 -0
  7. package/dist/checkCommand-BOk9y264.js +2 -0
  8. package/dist/cliRuntime-Oh517vCV.js +96 -0
  9. package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
  10. package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
  11. package/dist/dbCommand-D8nhyppR.js +2 -0
  12. package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
  13. package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
  14. package/dist/fileConventions-Cof68_BL.js +33 -0
  15. package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
  16. package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +1 -1
  19. package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
  20. package/dist/inspect-mmBuRXmy.js +2 -0
  21. package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
  22. package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
  23. package/dist/manifestBuild-Dj8Jjoto.js +2 -0
  24. package/dist/sdkgen-CqpOq_fy.js +582 -0
  25. package/dist/serveCommand-Ciq_wIY8.js +1577 -0
  26. package/dist/serveEntry.js +2 -2
  27. package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
  28. package/dist/startEntry.js +2 -2
  29. package/package.json +17 -17
  30. package/templates/AGENTS.core.md +50 -0
  31. package/templates/AGENTS.md +54 -2
  32. package/templates/agent-docs/_index.md +4 -2
  33. package/templates/agent-docs/_manifest.json +26 -5
  34. package/templates/agent-docs/ai.md +191 -0
  35. package/templates/agent-docs/cli.md +143 -6
  36. package/templates/agent-docs/configuration.md +32 -0
  37. package/templates/agent-docs/data.md +282 -0
  38. package/templates/agent-docs/database/migrations.md +47 -0
  39. package/templates/agent-docs/database/misc.md +57 -0
  40. package/templates/agent-docs/database/schema.md +3 -1
  41. package/templates/agent-docs/internationalization.md +169 -8
  42. package/templates/agent-docs/local-first-mobile.md +426 -0
  43. package/templates/agent-docs/routing.md +94 -3
  44. package/templates/agent-docs/schema-driven-ui.md +12 -0
  45. package/templates/agent-docs/security.md +17 -0
  46. package/templates/agent-docs/templates/apibackends.md +210 -1
  47. package/templates/agent-docs/templates/appshells.md +144 -1
  48. package/templates/agent-docs/templates/mobile.md +60 -0
  49. package/templates/agent-docs/templates/overview.md +17 -7
  50. package/templates/agent-docs/whats-new.md +238 -158
  51. package/templates/agent-docs/workflows.md +376 -0
  52. package/templates/apps/api-ai/package.json +7 -7
  53. package/templates/apps/api-auth/package.json +8 -8
  54. package/templates/apps/api-backend/package.json +7 -7
  55. package/templates/apps/api-backend-deactivation/package.json +7 -7
  56. package/templates/apps/api-backend-mail/package.json +8 -8
  57. package/templates/apps/api-backend-mariadb/package.json +9 -9
  58. package/templates/apps/api-backend-sqlite/.env.example +19 -0
  59. package/templates/apps/api-backend-sqlite/README.md +38 -0
  60. package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
  61. package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
  62. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
  63. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
  64. package/templates/apps/api-backend-sqlite/package.json +30 -0
  65. package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
  66. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
  67. package/templates/apps/api-backend-sqlite/template.json +6 -0
  68. package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
  69. package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
  70. package/templates/apps/api-backend-storage/package.json +8 -8
  71. package/templates/apps/api-cms/README.md +104 -0
  72. package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
  73. package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
  74. package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
  75. package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
  76. package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
  77. package/templates/apps/api-cms/actions/me.action.ts +16 -0
  78. package/templates/apps/api-cms/app.config.ts +61 -0
  79. package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
  80. package/templates/apps/api-cms/content/index.ts +18 -0
  81. package/templates/apps/api-cms/content/page.contentType.ts +24 -0
  82. package/templates/apps/api-cms/database/schema.ts +64 -0
  83. package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
  84. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
  85. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
  86. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
  87. package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
  88. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
  89. package/templates/apps/api-cms/package.json +32 -0
  90. package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
  91. package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
  92. package/templates/apps/api-cms/template.json +6 -0
  93. package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
  94. package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
  95. package/templates/apps/api-cms/tsconfig.json +5 -0
  96. package/templates/apps/api-collab/README.md +70 -0
  97. package/templates/apps/api-collab/app.config.ts +28 -0
  98. package/templates/apps/api-collab/database/schema.ts +63 -0
  99. package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
  100. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
  101. package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
  102. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
  103. package/templates/apps/api-collab/package.json +30 -0
  104. package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
  105. package/templates/apps/api-collab/queries/documents.query.ts +23 -0
  106. package/templates/apps/api-collab/template.json +6 -0
  107. package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
  108. package/templates/apps/api-collab/tsconfig.json +5 -0
  109. package/templates/apps/api-data-advanced/package.json +8 -8
  110. package/templates/apps/api-durable/package.json +8 -8
  111. package/templates/apps/api-feature-flags/package.json +9 -9
  112. package/templates/apps/api-governance/package.json +8 -8
  113. package/templates/apps/api-kv/package.json +8 -8
  114. package/templates/apps/api-moderation/package.json +8 -8
  115. package/templates/apps/api-observability/package.json +8 -8
  116. package/templates/apps/api-ratelimit/package.json +8 -8
  117. package/templates/apps/api-rbac/package.json +8 -8
  118. package/templates/apps/api-rest/package.json +7 -7
  119. package/templates/apps/api-saas/package.json +11 -11
  120. package/templates/apps/api-saas-starter/README.md +103 -0
  121. package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
  122. package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
  123. package/templates/apps/api-saas-starter/app.config.ts +87 -0
  124. package/templates/apps/api-saas-starter/database/schema.ts +57 -0
  125. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
  126. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
  127. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
  128. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
  129. package/templates/apps/api-saas-starter/package.json +32 -0
  130. package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
  131. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
  132. package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
  133. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
  134. package/templates/apps/api-saas-starter/template.json +6 -0
  135. package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
  136. package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
  137. package/templates/apps/api-saas-starter/tsconfig.json +5 -0
  138. package/templates/apps/api-search/package.json +8 -8
  139. package/templates/apps/api-status/README.md +88 -0
  140. package/templates/apps/api-status/app.config.ts +36 -0
  141. package/templates/apps/api-status/authz.ts +33 -0
  142. package/templates/apps/api-status/database/schema.ts +70 -0
  143. package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
  144. package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
  145. package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
  146. package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
  147. package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
  148. package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
  149. package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
  150. package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
  151. package/templates/apps/api-status/package.json +30 -0
  152. package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
  153. package/templates/apps/api-status/queries/components.list.query.ts +13 -0
  154. package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
  155. package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
  156. package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
  157. package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
  158. package/templates/apps/api-status/template.json +6 -0
  159. package/templates/apps/api-status/tests/status.test.ts +70 -0
  160. package/templates/apps/api-status/tsconfig.json +5 -0
  161. package/templates/apps/api-versioning/package.json +8 -8
  162. package/templates/apps/api-webhooks/package.json +9 -9
  163. package/templates/apps/changelog/package.json +6 -6
  164. package/templates/apps/edge-functions/package.json +2 -2
  165. package/templates/apps/frontend-admin/app.config.ts +1 -1
  166. package/templates/apps/frontend-admin/package.json +8 -8
  167. package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
  168. package/templates/apps/frontend-app/app.config.ts +1 -1
  169. package/templates/apps/frontend-app/package.json +8 -8
  170. package/templates/apps/frontend-app/src/locales/index.ts +1 -1
  171. package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
  172. package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
  173. package/templates/apps/frontend-auth/README.md +78 -0
  174. package/templates/apps/frontend-auth/app.config.ts +34 -0
  175. package/templates/apps/frontend-auth/package.json +32 -0
  176. package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
  177. package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
  178. package/templates/apps/frontend-auth/src/config.ts +11 -0
  179. package/templates/apps/frontend-auth/src/globals.css +105 -0
  180. package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
  181. package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
  182. package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
  183. package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
  184. package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
  185. package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
  186. package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
  187. package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
  188. package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
  189. package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
  190. package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
  191. package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
  192. package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
  193. package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
  194. package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
  195. package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
  196. package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
  197. package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
  198. package/templates/apps/frontend-auth/template.json +6 -0
  199. package/templates/apps/frontend-auth/tsconfig.json +5 -0
  200. package/templates/apps/frontend-blank/app.config.ts +1 -1
  201. package/templates/apps/frontend-blank/package.json +7 -7
  202. package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
  203. package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
  204. package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
  205. package/templates/apps/frontend-cms/README.md +47 -0
  206. package/templates/apps/frontend-cms/app.config.ts +37 -0
  207. package/templates/apps/frontend-cms/package.json +33 -0
  208. package/templates/apps/frontend-cms/src/config.ts +8 -0
  209. package/templates/apps/frontend-cms/src/globals.css +105 -0
  210. package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
  211. package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
  212. package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
  213. package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
  214. package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
  215. package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
  216. package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
  217. package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
  218. package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
  219. package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
  220. package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
  221. package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
  222. package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
  223. package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
  224. package/templates/apps/frontend-cms/template.json +6 -0
  225. package/templates/apps/frontend-cms/tsconfig.json +5 -0
  226. package/templates/apps/frontend-collab/README.md +80 -0
  227. package/templates/apps/frontend-collab/app.config.ts +38 -0
  228. package/templates/apps/frontend-collab/package.json +33 -0
  229. package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
  230. package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
  231. package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
  232. package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
  233. package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
  234. package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
  235. package/templates/apps/frontend-collab/template.json +6 -0
  236. package/templates/apps/frontend-collab/tsconfig.json +11 -0
  237. package/templates/apps/frontend-contact/package.json +7 -7
  238. package/templates/apps/frontend-dashboard/app.config.ts +1 -1
  239. package/templates/apps/frontend-dashboard/package.json +7 -7
  240. package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
  241. package/templates/apps/frontend-docs/package.json +7 -7
  242. package/templates/apps/frontend-i18n/package.json +6 -6
  243. package/templates/apps/frontend-landing/package.json +7 -7
  244. package/templates/apps/frontend-portal/README.md +71 -0
  245. package/templates/apps/frontend-portal/app.config.ts +37 -0
  246. package/templates/apps/frontend-portal/package.json +32 -0
  247. package/templates/apps/frontend-portal/src/config.ts +8 -0
  248. package/templates/apps/frontend-portal/src/globals.css +93 -0
  249. package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
  250. package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
  251. package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
  252. package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
  253. package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
  254. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
  255. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
  256. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
  257. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
  258. package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
  259. package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
  260. package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
  261. package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
  262. package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
  263. package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
  264. package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
  265. package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
  266. package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
  267. package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
  268. package/templates/apps/frontend-portal/template.json +6 -0
  269. package/templates/apps/frontend-portal/tsconfig.json +5 -0
  270. package/templates/apps/frontend-saas/README.md +73 -0
  271. package/templates/apps/frontend-saas/app.config.ts +46 -0
  272. package/templates/apps/frontend-saas/package.json +32 -0
  273. package/templates/apps/frontend-saas/src/config.ts +8 -0
  274. package/templates/apps/frontend-saas/src/globals.css +85 -0
  275. package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
  276. package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
  277. package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
  278. package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
  279. package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
  280. package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
  281. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
  282. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
  283. package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
  284. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
  285. package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
  286. package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
  287. package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
  288. package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
  289. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
  290. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
  291. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
  292. package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
  293. package/templates/apps/frontend-saas/template.json +6 -0
  294. package/templates/apps/frontend-saas/tsconfig.json +5 -0
  295. package/templates/apps/frontend-spa/app.config.ts +1 -1
  296. package/templates/apps/frontend-spa/package.json +7 -7
  297. package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
  298. package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
  299. package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
  300. package/templates/apps/frontend-ssr/app.config.ts +1 -1
  301. package/templates/apps/frontend-ssr/package.json +7 -7
  302. package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
  303. package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
  304. package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
  305. package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
  306. package/templates/apps/frontend-ssr-api/package.json +8 -8
  307. package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
  308. package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
  309. package/templates/apps/frontend-static-blog/package.json +6 -6
  310. package/templates/apps/frontend-status/README.md +51 -0
  311. package/templates/apps/frontend-status/app.config.ts +39 -0
  312. package/templates/apps/frontend-status/package.json +32 -0
  313. package/templates/apps/frontend-status/src/config.ts +7 -0
  314. package/templates/apps/frontend-status/src/globals.css +70 -0
  315. package/templates/apps/frontend-status/src/globals.d.ts +6 -0
  316. package/templates/apps/frontend-status/src/lib/status.ts +81 -0
  317. package/templates/apps/frontend-status/src/locales/de.ts +41 -0
  318. package/templates/apps/frontend-status/src/locales/en.ts +45 -0
  319. package/templates/apps/frontend-status/src/locales/index.ts +13 -0
  320. package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
  321. package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
  322. package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
  323. package/templates/apps/frontend-status/template.json +6 -0
  324. package/templates/apps/frontend-status/tsconfig.json +11 -0
  325. package/templates/apps/mobile-app/README.md +58 -0
  326. package/templates/apps/mobile-app/app.config.ts +33 -0
  327. package/templates/apps/mobile-app/babel.config.js +5 -0
  328. package/templates/apps/mobile-app/metro.config.js +20 -0
  329. package/templates/apps/mobile-app/package.json +33 -0
  330. package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
  331. package/templates/apps/mobile-app/src/app/index.tsx +62 -0
  332. package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
  333. package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
  334. package/templates/apps/mobile-app/src/client.ts +74 -0
  335. package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
  336. package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
  337. package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
  338. package/templates/apps/mobile-app/src/persistence.ts +38 -0
  339. package/templates/apps/mobile-app/template.json +6 -0
  340. package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
  341. package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
  342. package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
  343. package/templates/apps/mobile-app/tsconfig.json +10 -0
  344. package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
  345. package/templates/baselines/compose/docker-compose.prod.yml +15 -0
  346. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
  347. package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
  348. package/dist/apiBuild-DgBS9ayv.js +0 -2
  349. package/dist/dbCommand-DvguqlzF.js +0 -2
  350. package/dist/fileConventions-3bffWssN.js +0 -30
  351. package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
  352. package/dist/inspect-gt8bq-Tz.js +0 -2
  353. package/dist/manifestBuild-ifczArzr.js +0 -2
  354. package/dist/serveCommand-ZTn-dPFa.js +0 -1425
@@ -13,17 +13,17 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/database": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/plugin-multitenancy": "0.28.0",
20
- "@voltro/plugin-search": "0.28.0",
21
- "@voltro/protocol": "0.28.0",
22
- "@voltro/runtime": "0.28.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/plugin-multitenancy": "0.30.0",
20
+ "@voltro/plugin-search": "0.30.0",
21
+ "@voltro/protocol": "0.30.0",
22
+ "@voltro/runtime": "0.30.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.28.0",
26
+ "@voltro/testing": "0.30.0",
27
27
  "typescript": "^6.0.3",
28
28
  "vitest": "^4.1.10"
29
29
  }
@@ -0,0 +1,88 @@
1
+ # {{capProjectName}} {{capAppName}} — status-page backend
2
+
3
+ The backend for a public **status page**: live queries anyone can subscribe to,
4
+ plus operator-only mutations to declare and resolve incidents. Boots
5
+ **zero-infra** (`store: 'memory'`). Pair it with the **frontend-status** web
6
+ template:
7
+
8
+ ```bash
9
+ voltro init my-status --api=api-status --web=frontend-status
10
+ voltro dev
11
+ ```
12
+
13
+ ## The asymmetry it encodes
14
+
15
+ A status page is read by everyone and written by a few — so:
16
+
17
+ - **Reads are PUBLIC.** `incidents.live`, `updates.list`, `components.list`
18
+ carry **no guard**, so an anonymous browser can `useSubscription('app',
19
+ 'incidents.live')` and the timeline updates live.
20
+ - **Writes are OPERATOR-ONLY.** Every mutation declares
21
+ `guards: [{ scope: 'status:write' }]`, enforced by `@voltro/plugin-rbac` in the
22
+ dispatch spine before the executor. The demo resolver (`authz.ts`) grants the
23
+ `operator` role to the `ops` tenant; everyone else — anonymous included — is
24
+ denied.
25
+
26
+ ## Surface
27
+
28
+ | Tag | Kind | Guard |
29
+ |---|---|---|
30
+ | `incidents.live` | query (stream) | — public |
31
+ | `updates.list` | query (stream) | — public |
32
+ | `components.list` | query (stream) | — public |
33
+ | `incidents.create` | mutation | `status:write` |
34
+ | `incidents.update` | mutation | `status:write` |
35
+ | `incidents.resolve` | mutation | `status:write` |
36
+ | `components.create` | mutation | `status:write` |
37
+
38
+ ## Try it (curl)
39
+
40
+ ```bash
41
+ voltro dev .
42
+
43
+ # A public read — no auth (anyone can watch)
44
+ curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
45
+ -H 'content-type: application/json' \
46
+ -d '{"tag":"incidents.live","input":{}}'
47
+
48
+ # An operator write — needs the `ops` tenant (demo resolver → operator role)
49
+ curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
50
+ -H 'content-type: application/json' -H 'x-tenant: ops' \
51
+ -d '{"tag":"incidents.create","input":{"title":"API degraded","impact":"major"}}'
52
+
53
+ # The SAME write without the operator tenant → typed ScopeError
54
+ curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
55
+ -H 'content-type: application/json' \
56
+ -d '{"tag":"incidents.create","input":{"title":"nope","impact":"minor"}}'
57
+ # → { "ok": false, "error": { "_tag": "ScopeError", "required": "status:write" } }
58
+ ```
59
+
60
+ ## Files
61
+
62
+ ```
63
+ app.config.ts rbacPlugin (roles + demo resolver)
64
+ authz.ts the role→scope map (status:write) — one source of truth
65
+ database/schema.ts components / incidents / incident_updates (PUBLIC, not tenant-scoped)
66
+ queries/ incidents.live / updates.list / components.list — ungated streams
67
+ mutations/ incidents.create|update|resolve, components.create — status:write
68
+ tests/ the public-read / operator-write asymmetry + descriptor pins
69
+ ```
70
+
71
+ ## Going to production
72
+
73
+ - **Real operator auth.** Replace the demo `resolveRoles` with subject metadata
74
+ or a DB lookup, and pair this api with `api-auth` / `api-saas-starter` so
75
+ operators actually sign in. The rbac gate is already in place — you only swap
76
+ where the roles come from.
77
+ - **Durable store.** `store: 'postgres'` (or `sqlite`) so incident history
78
+ survives a restart.
79
+ - **Notify subscribers** on open/resolve with `@voltro/plugin-mail` (opt-in).
80
+
81
+ ## Anti-patterns
82
+
83
+ - **Tenant-scoping the public tables.** Adding `tenant()` to `incidents` would
84
+ make an anonymous visitor (no tenant) see nothing. Public reads stay
85
+ unscoped; the rbac guard on the writes is the boundary.
86
+ - **A write-only authz model.** These reads are intentionally ungated because
87
+ they're public — but if you ever gate a read, gate it with `guards:` on the
88
+ query (enforced on open AND every delivery), not a check inside the handler.
@@ -0,0 +1,36 @@
1
+ // Backend for the {{projectName}} public status page. Read by `voltro dev`.
2
+ //
3
+ // The shape of a status page is asymmetric, and this config encodes it:
4
+ // • READS are PUBLIC — `incidents.live` / `updates.list` / `components.list`
5
+ // carry NO guard, so an anonymous visitor's browser can subscribe and see
6
+ // the live timeline. (A status page that requires a login is useless during
7
+ // an outage.)
8
+ // • WRITES are OPERATOR-ONLY — every mutation declares a `status:write` guard.
9
+ // `rbacPlugin` resolves each caller's ROLES to SCOPES and the framework
10
+ // enforces the guard in the dispatch spine, before the executor. `voltro
11
+ // check` can see the guard statically.
12
+ //
13
+ // The role map lives in `authz.ts`. Config-only — no external infra.
14
+ import { defineEnv, envVar } from '@voltro/env'
15
+ import { rbacPlugin } from '@voltro/plugin-rbac'
16
+ import { demoRolesForTenant, roles } from './authz'
17
+
18
+ export const env = defineEnv({
19
+ LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
20
+ })
21
+
22
+ export default {
23
+ type: 'api' as const,
24
+ name: '{{capProjectName}}{{capAppName}}',
25
+ store: 'memory' as const,
26
+ plugins: [
27
+ rbacPlugin({
28
+ roles,
29
+ // Demo: `ops` tenant → operator. Swap for a real resolver (subject
30
+ // metadata / a DB lookup) when you wire operator auth — pair this api
31
+ // with the api-auth / api-saas-starter auth backend.
32
+ resolveRoles: (subject) => demoRolesForTenant(subject.tenantId),
33
+ }),
34
+ ],
35
+ env,
36
+ }
@@ -0,0 +1,33 @@
1
+ // The status page's authorization vocabulary, in ONE place — split out of
2
+ // app.config.ts so the tests build the real plugin from the real role map (a
3
+ // test that redeclares the roles only proves its own copy is self-consistent).
4
+ //
5
+ // A status page has exactly two kinds of caller: the PUBLIC (anonymous
6
+ // visitors, who only READ — the live queries carry no guard) and OPERATORS
7
+ // (who post incidents). So there is one write scope, `status:write`, held by
8
+ // the `operator` role; `admin` gets the wildcard bypass.
9
+ //
10
+ // This map is ALSO the app's declared scope vocabulary: `rbacPlugin` publishes
11
+ // its union, and `voltro check` errors on any descriptor guard requiring a
12
+ // scope that appears nowhere here — so a guard and its grant can't silently
13
+ // drift out of sync.
14
+ export const roles = {
15
+ operator: ['status:write'],
16
+ admin: ['*'],
17
+ }
18
+
19
+ /**
20
+ * DEMO role resolver — grants `operator` to callers in the `ops` tenant so
21
+ * every operator flow is reachable with an `x-tenant: ops` header and zero auth
22
+ * setup. Everyone else (including anonymous visitors) gets NO role, so the
23
+ * ungated read queries still work but every `status:write` mutation is denied.
24
+ *
25
+ * PRODUCTION reads the caller's REAL roles: the default resolver reads
26
+ * `subject.metadata.roles` (set by your auth strategy — pair this with the
27
+ * api-auth / api-saas-starter auth wiring), or do a DB lookup here. If this
28
+ * throws, rbac degrades to the subject's own scopes — it never grants on
29
+ * failure.
30
+ */
31
+ export const demoRolesForTenant = (
32
+ tenantId: string | null | undefined,
33
+ ): ReadonlyArray<string> => (tenantId === 'ops' ? ['operator'] : [])
@@ -0,0 +1,70 @@
1
+ // Schema for the {{projectName}} status page.
2
+ //
3
+ // These tables are PUBLIC and NOT tenant-scoped on purpose: a status page is
4
+ // read by anyone, so the live queries must return rows for an anonymous caller.
5
+ // (Adding `tenant()` here would AND-merge `tenantId = subject.tenantId` and an
6
+ // anonymous visitor — tenantId null — would see nothing.) Write access is
7
+ // controlled by the rbac `status:write` guard on the mutations instead.
8
+ //
9
+ // `actors` + `tenants` are the framework core tables.
10
+ import {
11
+ databaseHandle,
12
+ id,
13
+ table,
14
+ text,
15
+ timestamp,
16
+ type InferRow,
17
+ } from '@voltro/database'
18
+
19
+ export const actors = table('actors', {
20
+ id: id(),
21
+ kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
22
+ displayName: text().nullable(),
23
+ createdAt: timestamp().default('now'),
24
+ })
25
+
26
+ export const tenants = table('tenants', {
27
+ id: id(),
28
+ name: text(),
29
+ createdAt: timestamp().default('now'),
30
+ })
31
+
32
+ // A monitored service component (API, Dashboard, Webhooks, …).
33
+ export const components = table('components', {
34
+ id: id({ prefix: 'cmp' }),
35
+ name: text(),
36
+ status: text().oneOf(['operational', 'degraded', 'partial_outage', 'major_outage']),
37
+ })
38
+
39
+ // An incident. `impact` drives the public banner colour; `status` walks the
40
+ // lifecycle investigating → identified → monitoring → resolved.
41
+ export const incidents = table('incidents', {
42
+ id: id({ prefix: 'inc' }),
43
+ title: text(),
44
+ impact: text().oneOf(['minor', 'major', 'critical']),
45
+ status: text().oneOf(['investigating', 'identified', 'monitoring', 'resolved']),
46
+ startedAt: timestamp().default('now'),
47
+ resolvedAt: timestamp().nullable(),
48
+ })
49
+
50
+ // A timeline entry posted against an incident. The public page renders these in
51
+ // order under each incident.
52
+ export const incidentUpdates = table('incident_updates', {
53
+ id: id({ prefix: 'iu' }),
54
+ incidentId: text(),
55
+ body: text(),
56
+ status: text().oneOf(['investigating', 'identified', 'monitoring', 'resolved']),
57
+ createdAt: timestamp().default('now'),
58
+ })
59
+
60
+ export type Component = InferRow<typeof components>
61
+ export type Incident = InferRow<typeof incidents>
62
+ export type IncidentUpdate = InferRow<typeof incidentUpdates>
63
+
64
+ export const database = databaseHandle({
65
+ actors,
66
+ tenants,
67
+ components,
68
+ incidents,
69
+ incidentUpdates,
70
+ })
@@ -0,0 +1,19 @@
1
+ import { Effect } from 'effect'
2
+ import type { AppContext } from '@voltro/runtime'
3
+ import { EffectStore } from '@voltro/runtime'
4
+
5
+ const execute = (input: { name: string }, _ctx: AppContext) =>
6
+ Effect.gen(function* () {
7
+ const store = yield* EffectStore
8
+ const row = yield* store.insert('components', {
9
+ name: input.name,
10
+ status: 'operational',
11
+ })
12
+ return {
13
+ id: row['id'] as string,
14
+ name: row['name'] as string,
15
+ status: row['status'] as string,
16
+ }
17
+ })
18
+
19
+ export default execute
@@ -0,0 +1,16 @@
1
+ // Add a monitored component — OPERATOR ONLY. Seed your components once
2
+ // (API, Dashboard, Webhooks, …); the public page lists them with their status.
3
+ import { defineMutation } from '@voltro/protocol'
4
+ import { Schema } from 'effect'
5
+
6
+ export const createComponent = defineMutation({
7
+ name: 'components.create',
8
+ target: { table: 'components', op: 'insert' },
9
+ guards: [{ scope: 'status:write' }],
10
+ input: Schema.Struct({ name: Schema.NonEmptyString }),
11
+ output: Schema.Struct({
12
+ id: Schema.String,
13
+ name: Schema.String,
14
+ status: Schema.String,
15
+ }),
16
+ })
@@ -0,0 +1,25 @@
1
+ // Authz-free here: by the time this runs the framework has already enforced the
2
+ // `status:write` guard from the descriptor. A new incident opens in the
3
+ // `investigating` state; `startedAt` is stamped by the column default.
4
+ import { Effect } from 'effect'
5
+ import type { AppContext } from '@voltro/runtime'
6
+ import { EffectStore } from '@voltro/runtime'
7
+
8
+ const execute = (input: { title: string; impact: string }, _ctx: AppContext) =>
9
+ Effect.gen(function* () {
10
+ const store = yield* EffectStore
11
+ const row = yield* store.insert('incidents', {
12
+ title: input.title,
13
+ impact: input.impact,
14
+ status: 'investigating',
15
+ resolvedAt: null,
16
+ })
17
+ return {
18
+ id: row['id'] as string,
19
+ title: row['title'] as string,
20
+ impact: row['impact'] as string,
21
+ status: row['status'] as string,
22
+ }
23
+ })
24
+
25
+ export default execute
@@ -0,0 +1,22 @@
1
+ // Declare an incident — OPERATOR ONLY. The `guards: [{ scope: 'status:write' }]`
2
+ // is enforced by the framework before the executor runs; `voltro check` sees it
3
+ // statically. Declaring `guards:` merges `ScopeError` into the wire-error union
4
+ // automatically (no `error:` needed).
5
+ import { defineMutation } from '@voltro/protocol'
6
+ import { Schema } from 'effect'
7
+
8
+ export const createIncident = defineMutation({
9
+ name: 'incidents.create',
10
+ target: { table: 'incidents', op: 'insert' },
11
+ guards: [{ scope: 'status:write' }],
12
+ input: Schema.Struct({
13
+ title: Schema.NonEmptyString,
14
+ impact: Schema.Literal('minor', 'major', 'critical'),
15
+ }),
16
+ output: Schema.Struct({
17
+ id: Schema.String,
18
+ title: Schema.String,
19
+ impact: Schema.String,
20
+ status: Schema.String,
21
+ }),
22
+ })
@@ -0,0 +1,24 @@
1
+ import { Effect } from 'effect'
2
+ import type { AppContext } from '@voltro/runtime'
3
+ import { EffectStore } from '@voltro/runtime'
4
+
5
+ const execute = (input: { incidentId: string; body: string }, _ctx: AppContext) =>
6
+ Effect.gen(function* () {
7
+ const store = yield* EffectStore
8
+ const row = yield* store.update('incidents', input.incidentId, {
9
+ status: 'resolved',
10
+ resolvedAt: new Date(),
11
+ })
12
+ // A closing timeline entry (default body when the operator left it blank).
13
+ yield* store.insert('incident_updates', {
14
+ incidentId: input.incidentId,
15
+ body: input.body === '' ? 'Resolved.' : input.body,
16
+ status: 'resolved',
17
+ })
18
+ return {
19
+ id: input.incidentId,
20
+ status: (row?.['status'] as string | undefined) ?? 'resolved',
21
+ }
22
+ })
23
+
24
+ export default execute
@@ -0,0 +1,19 @@
1
+ // Resolve an incident — OPERATOR ONLY. Sets `status: 'resolved'` + stamps
2
+ // `resolvedAt`, and appends a final `resolved` timeline entry. The public page's
3
+ // banner goes green and the incident archives into the history.
4
+ import { defineMutation } from '@voltro/protocol'
5
+ import { Schema } from 'effect'
6
+
7
+ export const resolveIncident = defineMutation({
8
+ name: 'incidents.resolve',
9
+ target: { table: 'incidents', op: 'update' },
10
+ guards: [{ scope: 'status:write' }],
11
+ input: Schema.Struct({
12
+ incidentId: Schema.NonEmptyString,
13
+ body: Schema.String,
14
+ }),
15
+ output: Schema.Struct({
16
+ id: Schema.String,
17
+ status: Schema.String,
18
+ }),
19
+ })
@@ -0,0 +1,26 @@
1
+ import { Effect } from 'effect'
2
+ import type { AppContext } from '@voltro/runtime'
3
+ import { EffectStore } from '@voltro/runtime'
4
+
5
+ const execute = (
6
+ input: { incidentId: string; body: string; status: string },
7
+ _ctx: AppContext,
8
+ ) =>
9
+ Effect.gen(function* () {
10
+ const store = yield* EffectStore
11
+ // Advance the incident's own status so the banner reflects the latest note.
12
+ yield* store.update('incidents', input.incidentId, { status: input.status })
13
+ // Append the timeline entry.
14
+ const row = yield* store.insert('incident_updates', {
15
+ incidentId: input.incidentId,
16
+ body: input.body,
17
+ status: input.status,
18
+ })
19
+ return {
20
+ id: row['id'] as string,
21
+ incidentId: row['incidentId'] as string,
22
+ status: row['status'] as string,
23
+ }
24
+ })
25
+
26
+ export default execute
@@ -0,0 +1,21 @@
1
+ // Post a timeline update to an open incident — OPERATOR ONLY. Writes an
2
+ // `incident_updates` row AND advances the incident's own `status` so the public
3
+ // banner + timeline move together.
4
+ import { defineMutation } from '@voltro/protocol'
5
+ import { Schema } from 'effect'
6
+
7
+ export const updateIncident = defineMutation({
8
+ name: 'incidents.update',
9
+ target: { table: 'incident_updates', op: 'insert' },
10
+ guards: [{ scope: 'status:write' }],
11
+ input: Schema.Struct({
12
+ incidentId: Schema.NonEmptyString,
13
+ body: Schema.NonEmptyString,
14
+ status: Schema.Literal('investigating', 'identified', 'monitoring'),
15
+ }),
16
+ output: Schema.Struct({
17
+ id: Schema.String,
18
+ incidentId: Schema.String,
19
+ status: Schema.String,
20
+ }),
21
+ })
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@{{projectName}}/{{appName}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
9
+ "migrate": "voltro migrate",
10
+ "test": "voltro test",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@effect/platform": "^0.97.0",
15
+ "@effect/rpc": "^0.76.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/plugin-rbac": "0.30.0",
20
+ "@voltro/protocol": "0.30.0",
21
+ "@voltro/runtime": "0.30.0",
22
+ "@voltro/sql-postgres": "0.30.0",
23
+ "effect": "^3.22.0"
24
+ },
25
+ "devDependencies": {
26
+ "@voltro/testing": "0.30.0",
27
+ "typescript": "^6.0.3",
28
+ "vitest": "^4.1.10"
29
+ }
30
+ }
@@ -0,0 +1,14 @@
1
+ import type { AppContext } from '@voltro/runtime'
2
+
3
+ const execute = (_input: Record<string, never>, _ctx: AppContext) => ({
4
+ descriptor: {
5
+ table: 'components' as const,
6
+ predicate: undefined,
7
+ order: [{ column: 'name' as const, direction: 'asc' as const }],
8
+ take: 100,
9
+ skip: undefined,
10
+ projection: undefined,
11
+ },
12
+ })
13
+
14
+ export default execute
@@ -0,0 +1,13 @@
1
+ // Live list of service components + their current status. Public (no guard).
2
+ import { defineQuery } from '@voltro/protocol'
3
+ import { Schema } from 'effect'
4
+
5
+ export const componentsList = defineQuery({
6
+ name: 'components.list',
7
+ input: Schema.Struct({}),
8
+ output: Schema.Struct({
9
+ id: Schema.String,
10
+ name: Schema.String,
11
+ status: Schema.String,
12
+ }),
13
+ })
@@ -0,0 +1,14 @@
1
+ import type { AppContext } from '@voltro/runtime'
2
+
3
+ const execute = (_input: Record<string, never>, _ctx: AppContext) => ({
4
+ descriptor: {
5
+ table: 'incidents' as const,
6
+ predicate: undefined,
7
+ order: [{ column: 'startedAt' as const, direction: 'desc' as const }],
8
+ take: 100,
9
+ skip: undefined,
10
+ projection: undefined,
11
+ },
12
+ })
13
+
14
+ export default execute
@@ -0,0 +1,20 @@
1
+ // The live incident feed — the heart of the status page. NO `guards:`, so an
2
+ // anonymous visitor's browser can subscribe: `useSubscription('app',
3
+ // 'incidents.live')`. Every insert/update to `incidents` lands as a delta, so a
4
+ // newly-declared incident (or a resolve) shows up on every open status page
5
+ // within seconds — no polling.
6
+ import { defineQuery } from '@voltro/protocol'
7
+ import { Schema } from 'effect'
8
+
9
+ export const incidentsLive = defineQuery({
10
+ name: 'incidents.live',
11
+ input: Schema.Struct({}),
12
+ output: Schema.Struct({
13
+ id: Schema.String,
14
+ title: Schema.String,
15
+ impact: Schema.String,
16
+ status: Schema.String,
17
+ startedAt: Schema.Date,
18
+ resolvedAt: Schema.NullOr(Schema.Date),
19
+ }),
20
+ })
@@ -0,0 +1,14 @@
1
+ import type { AppContext } from '@voltro/runtime'
2
+
3
+ const execute = (_input: Record<string, never>, _ctx: AppContext) => ({
4
+ descriptor: {
5
+ table: 'incident_updates' as const,
6
+ predicate: undefined,
7
+ order: [{ column: 'createdAt' as const, direction: 'desc' as const }],
8
+ take: 200,
9
+ skip: undefined,
10
+ projection: undefined,
11
+ },
12
+ })
13
+
14
+ export default execute
@@ -0,0 +1,17 @@
1
+ // Live feed of incident timeline entries. Public (no guard). The page joins
2
+ // these to incidents by `incidentId` client-side to render each incident's
3
+ // update history.
4
+ import { defineQuery } from '@voltro/protocol'
5
+ import { Schema } from 'effect'
6
+
7
+ export const updatesList = defineQuery({
8
+ name: 'updates.list',
9
+ input: Schema.Struct({}),
10
+ output: Schema.Struct({
11
+ id: Schema.String,
12
+ incidentId: Schema.String,
13
+ body: Schema.String,
14
+ status: Schema.String,
15
+ createdAt: Schema.Date,
16
+ }),
17
+ })
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "api-status",
3
+ "kind": "api",
4
+ "summary": "The backend for a public status page: ungated live queries (incidents.live, updates.list, components.list) any visitor can subscribe to, plus operator-only mutations (create/update/resolve incidents, manage components) gated by a @voltro/plugin-rbac `status:write` scope. Reads are public so an anonymous status page works; writes need an operator. Pairs with the frontend-status web template. Boots zero-infra.",
5
+ "tags": ["api", "status-page", "incidents", "rbac", "live", "useSubscription", "operator"]
6
+ }
@@ -0,0 +1,70 @@
1
+ // Authorization + descriptor tests, driven through the REAL rbac plugin.
2
+ //
3
+ // `makeTestContext({ plugins: [...] })` + `invoke` composes this app's actual
4
+ // interceptor chain and enforces each descriptor's `guards:` with the same
5
+ // `checkGuardsEffect` the serve pipeline calls — so we exercise the whole path
6
+ // (tenant → role → compiled scope → guard), not a hand-stamped `subject.scopes`.
7
+ // The asymmetry that defines a status page is the thing under test: reads are
8
+ // public, writes are operator-only.
9
+ //
10
+ // Run with `voltro test` (vitest).
11
+
12
+ import { describe, it, expect } from 'vitest'
13
+ import { Schema } from 'effect'
14
+ import { makeTestContext, mockStore, invoke } from '@voltro/testing'
15
+ import { rbacPlugin } from '@voltro/plugin-rbac'
16
+ import type { Subject } from '@voltro/protocol'
17
+
18
+ import { roles, demoRolesForTenant } from '../authz'
19
+ import { database } from '../database/schema' // registers incidents / components / updates / core
20
+ import { createIncident } from '../mutations/incidents.create.mutation'
21
+ import createIncidentHandler from '../mutations/incidents.create.mutation.server'
22
+ import { incidentsLive } from '../queries/incidents.live.query'
23
+ import incidentsLiveHandler from '../queries/incidents.live.query.server'
24
+
25
+ /** The app's real plugin, built from the app's real role map. */
26
+ const plugin = () => rbacPlugin({ roles, resolveRoles: (subject) => demoRolesForTenant(subject.tenantId) })
27
+
28
+ const subjectFor = (tenantId: string | null, id: string | null = 'u_1', type = 'user'): Subject =>
29
+ ({ type, id, tenantId, scopes: [] }) as Subject
30
+
31
+ const ctxFor = (subject: Subject) =>
32
+ makeTestContext({ subject, store: mockStore({ incidents: [], components: [], incident_updates: [] }), plugins: [plugin()] })
33
+
34
+ describe('incidents.create — operator-gated', () => {
35
+ it('an operator (tenant `ops`) may declare an incident', async () => {
36
+ const ctx = ctxFor(subjectFor('ops'))
37
+ const inc = await invoke(createIncident, createIncidentHandler, { title: 'API degraded', impact: 'major' }, ctx)
38
+ expect(inc.title).toBe('API degraded')
39
+ expect(inc.status).toBe('investigating')
40
+ })
41
+
42
+ it('an anonymous visitor is refused with a typed ScopeError, and nothing is written', async () => {
43
+ const ctx = ctxFor(subjectFor(null, null, 'anonymous'))
44
+ await expect(invoke(createIncident, createIncidentHandler, { title: 'nope', impact: 'minor' }, ctx))
45
+ .rejects.toMatchObject({ _tag: 'ScopeError', required: 'status:write' })
46
+ expect(await ctx.store.query(database.incidents.descriptor)).toHaveLength(0)
47
+ })
48
+
49
+ it('a signed-in NON-operator is also refused', async () => {
50
+ const ctx = ctxFor(subjectFor('marketing'))
51
+ await expect(invoke(createIncident, createIncidentHandler, { title: 'nope', impact: 'minor' }, ctx))
52
+ .rejects.toMatchObject({ _tag: 'ScopeError', required: 'status:write' })
53
+ })
54
+ })
55
+
56
+ describe('incidents.live — reads are PUBLIC', () => {
57
+ it('an anonymous visitor may subscribe (no guard on the query)', async () => {
58
+ const ctx = ctxFor(subjectFor(null, null, 'anonymous'))
59
+ await expect(invoke(incidentsLive, incidentsLiveHandler, {}, ctx)).resolves.toBeDefined()
60
+ })
61
+ })
62
+
63
+ describe('incidents.create — descriptor', () => {
64
+ it('accepts a valid impact and rejects an unknown one', () => {
65
+ const decode = Schema.decodeUnknownSync(createIncident.input)
66
+ expect(decode({ title: 'x', impact: 'critical' })).toEqual({ title: 'x', impact: 'critical' })
67
+ expect(() => decode({ title: 'x', impact: 'catastrophic' })).toThrow()
68
+ expect(() => decode({ title: '', impact: 'minor' })).toThrow()
69
+ })
70
+ })
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["**/*.ts", "**/*.tsx"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }