@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-versioning": "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-versioning": "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
  }
@@ -13,18 +13,18 @@
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-webhooks": "0.28.0",
21
- "@voltro/protocol": "0.28.0",
22
- "@voltro/runtime": "0.28.0",
23
- "@voltro/sql-postgres": "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-webhooks": "0.30.0",
21
+ "@voltro/protocol": "0.30.0",
22
+ "@voltro/runtime": "0.30.0",
23
+ "@voltro/sql-postgres": "0.30.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.28.0",
27
+ "@voltro/testing": "0.30.0",
28
28
  "typescript": "^6.0.3",
29
29
  "vitest": "^4.1.10"
30
30
  }
@@ -12,12 +12,12 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/changelog": "0.28.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/client": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/i18n": "0.28.0",
20
- "@voltro/web": "0.28.0",
15
+ "@voltro/changelog": "0.30.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/client": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/i18n": "0.30.0",
20
+ "@voltro/web": "0.30.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
@@ -12,8 +12,8 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
- "@voltro/cli": "0.28.0",
16
- "@voltro/serverless": "0.28.0",
15
+ "@voltro/cli": "0.30.0",
16
+ "@voltro/serverless": "0.30.0",
17
17
  "effect": "^3.22.0"
18
18
  },
19
19
  "devDependencies": {
@@ -22,7 +22,7 @@ export default {
22
22
  // <I18nProvider> from these codes and loads the matching
23
23
  // `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
24
24
  // `useLocale()` work app-wide with no manual setup. The <LocaleSwitcher>
25
- // in the layout writes the `voltro:lang` cookie + reloads. Grow the app to
25
+ // in the layout writes the `voltro:locale` cookie + reloads. Grow the app to
26
26
  // more languages by adding a code here + a `src/locales/<code>.ts`.
27
27
  locales: ['en', 'de'] as const,
28
28
  defaultLocale: 'en' as const,
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.28.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/client": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/i18n": "0.28.0",
20
- "@voltro/ui-shadcn": "0.28.0",
21
- "@voltro/web": "0.28.0",
15
+ "@{{projectName}}/api": "0.30.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/client": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/i18n": "0.30.0",
20
+ "@voltro/ui-shadcn": "0.30.0",
21
+ "@voltro/web": "0.30.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.28.0",
26
+ "@voltro/devtools": "0.30.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "typescript": "^6.0.3",
@@ -1,6 +1,6 @@
1
1
  // Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
2
2
  // <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
3
- // hands `meta` the active locale from the `voltro:lang` cookie. Inside React,
3
+ // hands `meta` the active locale from the `voltro:locale` cookie. Inside React,
4
4
  // use `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
5
5
 
6
6
  import en from './en'
@@ -27,7 +27,7 @@ export default {
27
27
  // <I18nProvider> from these codes and loads the matching
28
28
  // `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
29
29
  // `useLocale()` work app-wide with no manual setup. The <LocaleSwitcher>
30
- // in the layout writes the `voltro:lang` cookie + reloads. Grow the app to
30
+ // in the layout writes the `voltro:locale` cookie + reloads. Grow the app to
31
31
  // more languages by adding a code here + a `src/locales/<code>.ts`.
32
32
  locales: ['en', 'de'] as const,
33
33
  defaultLocale: 'en' as const,
@@ -12,18 +12,18 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.28.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/client": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/i18n": "0.28.0",
20
- "@voltro/ui-shadcn": "0.28.0",
21
- "@voltro/web": "0.28.0",
15
+ "@{{projectName}}/api": "0.30.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/client": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/i18n": "0.30.0",
20
+ "@voltro/ui-shadcn": "0.30.0",
21
+ "@voltro/web": "0.30.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.28.0",
26
+ "@voltro/devtools": "0.30.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "typescript": "^6.0.3",
@@ -1,6 +1,6 @@
1
1
  // Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
2
2
  // <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
3
- // hands `meta` the active locale from the `voltro:lang` cookie. Inside React,
3
+ // hands `meta` the active locale from the `voltro:locale` cookie. Inside React,
4
4
  // use `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
5
5
 
6
6
  import en from './en'
@@ -4,7 +4,7 @@
4
4
  // `theme: 'dark' | 'light' | 'system'` in app.config.ts for the dark class.
5
5
  //
6
6
  // The app is bilingual (see `locales` in app.config.ts): nav labels come from
7
- // the catalog via `<T>`, and <LocaleSwitcher> writes the `voltro:lang` cookie
7
+ // the catalog via `<T>`, and <LocaleSwitcher> writes the `voltro:locale` cookie
8
8
  // + reloads so the framework re-renders in the chosen language.
9
9
 
10
10
  import type { ReactNode } from 'react'
@@ -20,7 +20,7 @@ import { T, useTFn } from '@voltro/i18n'
20
20
  import { getCatalog } from '../locales'
21
21
 
22
22
  // Locale-aware tab title: @voltro/web resolves meta({ locale }) from the
23
- // active locale (the `voltro:lang` cookie in this cookie-i18n app).
23
+ // active locale (the `voltro:locale` cookie in this cookie-i18n app).
24
24
  export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
25
25
  title: getCatalog(locale)['meta.notes.title'],
26
26
  })
@@ -0,0 +1,78 @@
1
+ # {{capProjectName}} {{capAppName}} — auth surface
2
+
3
+ A drop-in set of auth pages other apps in the project link to, so the login UI
4
+ lives in ONE place. Every page is a real form POSTing to the sibling api's
5
+ `/auth/*` routes (from [`@voltro/plugin-auth`](https://voltro.cloud/docs/plugins/auth))
6
+ with CSRF. Bilingual (en/de).
7
+
8
+ ```bash
9
+ voltro init my-app --api=api-auth --web=frontend-auth
10
+ ```
11
+
12
+ ## Pages
13
+
14
+ | URL | Route it drives | Purpose |
15
+ |---|---|---|
16
+ | `/login` | `POST /auth/sign-in` | email + password |
17
+ | `/signup` | `POST /auth/sign-up` | register + auto sign-in (with a strength meter) |
18
+ | `/forgot` | `POST /auth/password-reset` | request a reset link (neutral 202) |
19
+ | `/reset?token=` | `POST /auth/password-reset/confirm` | set a new password |
20
+ | `/magic` | `POST /auth/magic-link` | request a passwordless sign-in link |
21
+ | `/verify?token=` | `POST /auth/magic-link/callback` | consume the link / confirm email |
22
+ | `/logout` | `POST /auth/sign-out` | revoke the session, back to /login |
23
+
24
+ The emailed reset link points at `/reset?token=…`; the magic / verification link
25
+ points at `/verify?token=…`. Delivery needs `sendEmail` wired on the api (via
26
+ `@voltro/plugin-mail`) — without it those routes still mint tokens and return
27
+ `202`, they just don't send.
28
+
29
+ ## Safety built in
30
+
31
+ - **Open-redirect guard** (`lib/redirect.ts`) — after sign-in we only honour a
32
+ same-origin, absolute-path `?next=`; a cross-origin or protocol-relative value
33
+ falls back to `AUTH_HOME`. Unit-tested.
34
+ - **CSRF** — `lib/auth.ts` fetches a token from `GET /auth/csrf` and echoes it on
35
+ every state-changing POST.
36
+ - **No account enumeration** — `/forgot` and `/magic` always show the same
37
+ neutral "check your email" message.
38
+
39
+ ## Files
40
+
41
+ ```
42
+ app.config.ts web app; apis.auth → @{{projectName}}/api
43
+ src/lib/
44
+ auth.ts csrfToken() + postAuth() over /auth/*
45
+ redirect.ts(.test) safeNext() open-redirect guard (unit-tested)
46
+ src/components/
47
+ AuthShell.tsx centered card + brand + locale switcher
48
+ PasswordStrength.tsx advisory 0–4 meter
49
+ src/pages/
50
+ page.tsx / → redirect to /login
51
+ login/ · signup/ · forgot/ · reset/ · magic/ · verify/ · logout/
52
+ src/locales/{en,de}.ts bilingual catalogs (parity-enforced)
53
+ ```
54
+
55
+ ## Opt-in: standalone auth subdomain (shared-cookie SSO)
56
+
57
+ The default is a **path-based** auth surface mounted inside your app (`/login`,
58
+ `/signup`, …) — cookies are same-origin and nothing extra is needed. If instead
59
+ you want a dedicated `auth.acme.dev` that signs a cookie for the whole
60
+ `.acme.dev` family (so `app.acme.dev` and `cms.acme.dev` share one session),
61
+ deploy this app on its own subdomain and set the api's cookie domain
62
+ (`authRoutesPlugin({ cookieDomain: '.acme.dev' })`). The pages are identical; only
63
+ the deploy topology + cookie domain change.
64
+
65
+ ## Opt-in: OAuth / SSO providers
66
+
67
+ The base `@voltro/plugin-auth` ships password + magic-link + reset. Third-party
68
+ sign-in (Google, GitHub, WorkOS, Auth0, Kinde, Supabase, generic OIDC) is a
69
+ SEPARATE provider plugin (`@voltro/plugin-auth-*`) with its own callback routes —
70
+ add the plugin on the api, then render its buttons here. We don't ship dead
71
+ OAuth buttons wired to routes that don't exist.
72
+
73
+ ## Out of scope (V2)
74
+
75
+ - 2FA / passkeys — `@voltro/plugin-auth` has the primitives (`/auth/mfa/*`,
76
+ `/auth/passkey/*`); the pages follow.
77
+ - Bot defence (hCaptcha / Turnstile) — behind an env flag, render the widget only
78
+ when configured.
@@ -0,0 +1,34 @@
1
+ // Web app for the {{projectName}} project — the AUTH surface. Every page is a
2
+ // static form that POSTs to the sibling api's `/auth/*` routes (contributed by
3
+ // @voltro/plugin-auth), which set the HttpOnly session cookie. Other apps in the
4
+ // project (dashboard, cms, portal) link here for sign-in so the auth UI lives in
5
+ // ONE place.
6
+ //
7
+ // The `apis` mapping names the api whose `/auth/*` routes the dev/serve pipeline
8
+ // proxies to and whose typed rpc surface codegen pulls (this template calls the
9
+ // HTTP routes directly, so it needs no rpc — the mapping is what wires the proxy
10
+ // + the shared origin the session cookie is set on).
11
+ import { defineEnv, envVar } from '@voltro/env'
12
+
13
+ export const env = defineEnv({
14
+ VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
15
+ // Where a successful sign-in lands when the request carries no safelisted
16
+ // `?next=`. Public (build-time) — never a secret in the browser bundle.
17
+ VOLTRO_PUBLIC_AUTH_HOME: envVar.string({ access: 'public', default: '/' }),
18
+ })
19
+
20
+ export default {
21
+ type: 'web' as const,
22
+ name: '{{capProjectName}}{{capAppName}}',
23
+ port: {{port}},
24
+ theme: 'system' as const,
25
+ env,
26
+ // Bilingual by default (cookie-driven <I18nProvider>, SSR too).
27
+ locales: ['en', 'de'] as const,
28
+ defaultLocale: 'en' as const,
29
+ // The api whose /auth/* routes back these pages. Pair with the api-auth
30
+ // template: `voltro init my-app --api=api-auth --web=frontend-auth`.
31
+ apis: {
32
+ auth: { package: '@{{projectName}}/api' },
33
+ },
34
+ }
@@ -0,0 +1,32 @@
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
+ "build": "voltro build .",
10
+ "start": "voltro start .",
11
+ "typecheck": "tsc --noEmit",
12
+ "test": "voltro test"
13
+ },
14
+ "dependencies": {
15
+ "@{{projectName}}/api": "0.30.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/client": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/i18n": "0.30.0",
20
+ "@voltro/ui-shadcn": "0.30.0",
21
+ "@voltro/web": "0.30.0",
22
+ "react": "^19.0.0",
23
+ "react-dom": "^19.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@voltro/devtools": "0.30.0",
27
+ "@types/react": "^19.0.0",
28
+ "@types/react-dom": "^19.0.0",
29
+ "typescript": "^6.0.3",
30
+ "vitest": "^4.1.10"
31
+ }
32
+ }
@@ -0,0 +1,35 @@
1
+ // The centered auth card every page renders inside — brand, title, the form
2
+ // (children), an optional message, a footer link row, and the locale switcher.
3
+ import type { ReactNode } from 'react'
4
+ import { T, useLocale, useT } from '@voltro/i18n'
5
+ import { LocaleSwitcher } from '@voltro/ui-shadcn'
6
+ import { APP_NAME } from '../config'
7
+
8
+ const LOCALES = [
9
+ { code: 'en', label: 'English' },
10
+ { code: 'de', label: 'Deutsch' },
11
+ ]
12
+
13
+ export interface AuthShellProps {
14
+ readonly titleId: string
15
+ readonly children: ReactNode
16
+ readonly footer?: ReactNode
17
+ }
18
+
19
+ export function AuthShell({ titleId, children, footer }: AuthShellProps): ReactNode {
20
+ const locale = useLocale()
21
+ const langLabel = useT('lang.label')
22
+ return (
23
+ <div className="auth-wrap">
24
+ <div className="auth-card">
25
+ <div className="auth-head">
26
+ <span className="brand">{APP_NAME}</span>
27
+ <LocaleSwitcher locales={LOCALES} current={locale} ariaLabel={langLabel} />
28
+ </div>
29
+ <h1><T id={titleId} /></h1>
30
+ {children}
31
+ {footer ? <div className="auth-footer">{footer}</div> : null}
32
+ </div>
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,33 @@
1
+ // A tiny, dependency-free password strength meter. Purely advisory client-side
2
+ // UX — the api is the authority on password policy. Scores 0–4 on length +
3
+ // character-class variety and renders a labelled bar.
4
+ import type { ReactNode } from 'react'
5
+ import { useT } from '@voltro/i18n'
6
+
7
+ const score = (pw: string): number => {
8
+ if (pw.length === 0) return 0
9
+ let s = 0
10
+ if (pw.length >= 8) s++
11
+ if (pw.length >= 12) s++
12
+ if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++
13
+ if (/\d/.test(pw) && /[^A-Za-z0-9]/.test(pw)) s++
14
+ return Math.min(s, 4)
15
+ }
16
+
17
+ const LEVEL_KEYS = ['pw.weak', 'pw.weak', 'pw.fair', 'pw.good', 'pw.strong'] as const
18
+
19
+ export function PasswordStrength({ value }: { readonly value: string }): ReactNode {
20
+ const s = score(value)
21
+ const label = useT(LEVEL_KEYS[s] ?? 'pw.weak')
22
+ if (value.length === 0) return null
23
+ return (
24
+ <div className="pw-meter" aria-hidden={false}>
25
+ <div className="pw-bars">
26
+ {[0, 1, 2, 3].map((i) => (
27
+ <span key={i} className={i < s ? `bar on lvl${s}` : 'bar'} />
28
+ ))}
29
+ </div>
30
+ <span className="pw-label">{label}</span>
31
+ </div>
32
+ )
33
+ }
@@ -0,0 +1,11 @@
1
+ // Brand + defaults shown across the auth pages.
2
+ //
3
+ // Plain constants — not `publicEnv` — so the template typechecks BEFORE
4
+ // `voltro dev` generates the env types. `VOLTRO_PUBLIC_APP_NAME` /
5
+ // `VOLTRO_PUBLIC_AUTH_HOME` are still declared in app.config.ts (validated + in
6
+ // the env manifest); to read them typed instead, after one `voltro dev` swap
7
+ // these for `publicEnv.VOLTRO_PUBLIC_*` from '@voltro/env/public'.
8
+ export const APP_NAME = '{{capProjectName}}'
9
+
10
+ // Where a successful sign-in lands when no safelisted `?next=` is present.
11
+ export const AUTH_HOME = '/'
@@ -0,0 +1,105 @@
1
+ /* Plain CSS — no Tailwind, so there's no @source scanner to configure. */
2
+ :root {
3
+ --bg: #ffffff;
4
+ --fg: #0a0a0a;
5
+ --muted: #6b7280;
6
+ --accent: #4f46e5;
7
+ --danger: #dc2626;
8
+ --border: #e5e7eb;
9
+ --panel: #f9fafb;
10
+ }
11
+ :root.dark {
12
+ --bg: #0a0a0a;
13
+ --fg: #fafafa;
14
+ --muted: #9ca3af;
15
+ --accent: #818cf8;
16
+ --danger: #f87171;
17
+ --border: #1f2937;
18
+ --panel: #111827;
19
+ }
20
+
21
+ * { box-sizing: border-box; }
22
+ body {
23
+ margin: 0;
24
+ background: var(--bg);
25
+ color: var(--fg);
26
+ font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
27
+ line-height: 1.55;
28
+ }
29
+ a { color: inherit; }
30
+ .muted { color: var(--muted); }
31
+ .spacer { flex: 1; }
32
+
33
+ /* Buttons + inputs */
34
+ .btn {
35
+ display: inline-block; cursor: pointer; border: 0; border-radius: 8px;
36
+ background: var(--accent); color: #fff; padding: 0.55rem 1rem;
37
+ font: inherit; font-weight: 600; text-decoration: none;
38
+ }
39
+ .btn:disabled { opacity: 0.6; cursor: default; }
40
+ .btn.ghost { background: transparent; color: var(--accent); border: 1px solid var(--border); }
41
+ .link { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; padding: 0; }
42
+ input {
43
+ font: inherit; padding: 0.5rem 0.65rem; border: 1px solid var(--border);
44
+ border-radius: 8px; background: var(--bg); color: var(--fg); min-width: 0;
45
+ }
46
+ .error-text { color: var(--danger); }
47
+
48
+ /* Marketing */
49
+ .marketing header {
50
+ display: flex; align-items: center; gap: 1rem;
51
+ padding: 1rem 1.5rem; border-bottom: 1px solid var(--border);
52
+ }
53
+ .marketing .brand { font-weight: 700; text-decoration: none; }
54
+ .hero { max-width: 40rem; margin: 0 auto; padding: 3rem 1.5rem; }
55
+ .hero h1 { margin: 0 0 0.75rem; }
56
+ .auth-form { display: flex; flex-direction: column; gap: 0.85rem; max-width: 22rem; margin-top: 1.25rem; }
57
+ .auth-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.9rem; }
58
+
59
+ /* Dashboard shell */
60
+ .dash { display: grid; grid-template-columns: 15rem 1fr; min-height: 100vh; }
61
+ .dash aside { border-right: 1px solid var(--border); padding: 1.25rem; background: var(--panel); }
62
+ .dash aside .brand { font-weight: 700; margin-bottom: 1rem; }
63
+ .dash nav { display: flex; flex-direction: column; gap: 0.5rem; }
64
+ .dash nav a { text-decoration: none; color: var(--muted); padding: 0.35rem 0; }
65
+ .dash nav a[aria-current="page"] { color: var(--fg); font-weight: 600; }
66
+ .dash main { padding: 1.5rem 2rem; }
67
+ .topbar { display: flex; align-items: center; margin-bottom: 1rem; }
68
+ @media (max-width: 40rem) { .dash { grid-template-columns: 1fr; } .dash aside { border-right: 0; border-bottom: 1px solid var(--border); } }
69
+
70
+ /* Bits */
71
+ .row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-top: 0.5rem; }
72
+ .empty { color: var(--muted); margin-top: 1.5rem; }
73
+ .cards { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 0.75rem; }
74
+ .card { border: 1px solid var(--border); border-radius: 10px; padding: 0.9rem 1rem; background: var(--panel); }
75
+ .card .value { font-weight: 600; }
76
+ .card .label { color: var(--muted); font-size: 0.85rem; margin-top: 0.2rem; }
77
+ .paywall { border: 1px solid var(--accent); border-radius: 12px; padding: 1rem 1.25rem; margin-top: 1.25rem; background: color-mix(in oklab, var(--accent) 8%, transparent); }
78
+ .paywall p { margin: 0.35rem 0 0.75rem; }
79
+ .facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1.5rem; margin-top: 1rem; }
80
+ .facts dt { color: var(--muted); }
81
+ .facts dd { margin: 0; font-weight: 600; }
82
+ .rows { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.4rem; }
83
+ .rows li { display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.9rem; background: var(--panel); }
84
+ .tag { font-size: 0.8rem; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 0.05rem 0.55rem; }
85
+ code { background: color-mix(in oklab, var(--fg) 8%, transparent); padding: 0.1em 0.35em; border-radius: 4px; }
86
+
87
+ /* Auth card */
88
+ .auth-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 1.5rem; }
89
+ .auth-card { width: 100%; max-width: 24rem; border: 1px solid var(--border); border-radius: 14px; background: var(--panel); padding: 1.5rem 1.5rem 1.25rem; }
90
+ .auth-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; }
91
+ .auth-head .brand { font-weight: 700; }
92
+ .auth-card h1 { margin: 0 0 1rem; font-size: 1.35rem; }
93
+ .auth-card .auth-form { max-width: none; margin-top: 0; }
94
+ .auth-footer { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; margin-top: 1.1rem; padding-top: 0.9rem; border-top: 1px solid var(--border); font-size: 0.9rem; }
95
+ .auth-footer a { color: var(--accent); text-decoration: none; }
96
+ .ok { color: var(--accent); }
97
+
98
+ /* Password strength meter */
99
+ .pw-meter { display: flex; align-items: center; gap: 0.5rem; margin-top: -0.25rem; }
100
+ .pw-bars { display: flex; gap: 3px; flex: 1; }
101
+ .pw-bars .bar { height: 5px; flex: 1; border-radius: 3px; background: var(--border); }
102
+ .pw-bars .bar.on { background: var(--danger); }
103
+ .pw-bars .bar.on.lvl3 { background: #d97706; }
104
+ .pw-bars .bar.on.lvl4 { background: var(--accent); }
105
+ .pw-label { font-size: 0.8rem; color: var(--muted); min-width: 3.2rem; text-align: right; }
@@ -0,0 +1,6 @@
1
+ // Ambient declarations.
2
+ //
3
+ // `declare module '*.css'`: TypeScript with `moduleResolution: Bundler` rejects
4
+ // a bare `import './globals.css'` unless the asset module is declared. Vite
5
+ // resolves it at build time; the declaration is type-only.
6
+ declare module '*.css'
@@ -0,0 +1,34 @@
1
+ // Thin wrappers over the api's `/auth/*` HTTP routes (@voltro/plugin-auth).
2
+ //
3
+ // State-changing auth routes are CSRF-protected (double-submit): `GET /auth/csrf`
4
+ // sets a cookie AND returns a token that must be echoed in the `x-csrf-token`
5
+ // header on the following POST. `postAuth` does that round-trip so each page is
6
+ // a one-liner. These call the SAME-ORIGIN api (the dev/serve pipeline proxies
7
+ // `/auth/*` to the api named in app.config), so the HttpOnly session cookie the
8
+ // api sets lands on this origin.
9
+
10
+ export interface AuthResponse {
11
+ readonly ok: boolean
12
+ readonly status: number
13
+ readonly data: unknown
14
+ }
15
+
16
+ /** Fetch a CSRF token (also drops the csrf cookie into the jar). */
17
+ export const csrfToken = async (): Promise<string> => {
18
+ const res = await fetch('/auth/csrf')
19
+ const body = (await res.json()) as { csrfToken?: string }
20
+ return body.csrfToken ?? ''
21
+ }
22
+
23
+ /** POST a JSON body to an `/auth/*` route with the CSRF token attached. */
24
+ export const postAuth = async (path: string, body: Record<string, unknown>): Promise<AuthResponse> => {
25
+ const token = await csrfToken()
26
+ const res = await fetch(path, {
27
+ method: 'POST',
28
+ headers: { 'content-type': 'application/json', 'x-csrf-token': token },
29
+ body: JSON.stringify(body),
30
+ })
31
+ let data: unknown = null
32
+ try { data = await res.json() } catch { /* some routes return 202 with no body */ }
33
+ return { ok: res.ok, status: res.status, data }
34
+ }
@@ -0,0 +1,24 @@
1
+ // Pure unit test of the open-redirect guard — no DOM needed. The safelist is
2
+ // the security-load-bearing part of the auth flow, so it gets a direct test.
3
+ import { describe, expect, test } from 'vitest'
4
+ import { safeNext } from './redirect'
5
+
6
+ describe('safeNext', () => {
7
+ test('passes a same-origin absolute path through', () => {
8
+ expect(safeNext('/dashboard', '/')).toBe('/dashboard')
9
+ expect(safeNext('/a/b?x=1', '/')).toBe('/a/b?x=1')
10
+ })
11
+
12
+ test('falls back to home for a missing value', () => {
13
+ expect(safeNext(null, '/home')).toBe('/home')
14
+ expect(safeNext(undefined, '/home')).toBe('/home')
15
+ expect(safeNext('', '/home')).toBe('/home')
16
+ })
17
+
18
+ test('rejects cross-origin + protocol-relative + absolute URLs', () => {
19
+ expect(safeNext('https://evil.example/login', '/')).toBe('/')
20
+ expect(safeNext('//evil.example', '/')).toBe('/')
21
+ expect(safeNext('/\\evil.example', '/')).toBe('/')
22
+ expect(safeNext('javascript:alert(1)', '/')).toBe('/')
23
+ })
24
+ })
@@ -0,0 +1,29 @@
1
+ // Open-redirect guard for the `?next=` parameter.
2
+ //
3
+ // After sign-in we send the browser to wherever it was headed — but a `next=`
4
+ // an attacker controls is a phishing vector (`?next=https://evil.example/login`
5
+ // that looks like us). So we accept ONLY same-origin, absolute-PATH targets:
6
+ // a value must start with a single `/` and NOT with `//` (a protocol-relative
7
+ // URL like `//evil.example` that browsers treat as cross-origin). Anything else
8
+ // falls back to `home`.
9
+
10
+ /** The safelisted redirect target, or `home` when `next` is missing/unsafe. */
11
+ export const safeNext = (next: string | null | undefined, home: string): string => {
12
+ if (!next) return home
13
+ // Must be an absolute path, and not protocol-relative (`//host`) or a
14
+ // backslash-smuggled variant (`/\evil`).
15
+ if (!next.startsWith('/') || next.startsWith('//') || next.startsWith('/\\')) return home
16
+ return next
17
+ }
18
+
19
+ /** Read + safelist `?next=` from the current URL (browser only). */
20
+ export const nextFromLocation = (home: string): string => {
21
+ if (typeof window === 'undefined') return home
22
+ return safeNext(new URLSearchParams(window.location.search).get('next'), home)
23
+ }
24
+
25
+ /** Read a `?token=` from the current URL (browser only). */
26
+ export const tokenFromLocation = (): string | null => {
27
+ if (typeof window === 'undefined') return null
28
+ return new URLSearchParams(window.location.search).get('token')
29
+ }