@awsmbiz/create-inbis-app 0.0.0-stage → 0.1.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 (222) hide show
  1. package/README.md +28 -2
  2. package/dist/cli.mjs +850 -0
  3. package/dist/cli.mjs.map +1 -0
  4. package/package.json +43 -3
  5. package/template/.dev.vars.example +4 -0
  6. package/template/README.md +115 -0
  7. package/template/e2e/accessibility.spec.ts +33 -0
  8. package/template/e2e/axe.ts +47 -0
  9. package/template/e2e/shell.spec.ts +33 -0
  10. package/template/e2e/tsconfig.json +8 -0
  11. package/template/eslint.config.js +7 -0
  12. package/template/migrations/0001_app_initial.sql +8 -0
  13. package/template/modules/README.md +6 -0
  14. package/template/modules/access-control/files/src/access-control-routes.ts +36 -0
  15. package/template/modules/access-control/files/src/access-control.ts +46 -0
  16. package/template/modules/access-control/files/test/access-control.test.ts +78 -0
  17. package/template/modules/access-control/module.json +54 -0
  18. package/template/modules/access-control/wiring/admin/admin.imports.tsx +1 -0
  19. package/template/modules/access-control/wiring/admin/admin.nav.tsx +2 -0
  20. package/template/modules/access-control/wiring/admin/admin.pages.tsx +4 -0
  21. package/template/modules/access-control/wiring/admin/admin.userSections.tsx +1 -0
  22. package/template/modules/access-control/wiring/admin/readme.modules.md +3 -0
  23. package/template/modules/access-control/wiring/admin/web.messages.ts +4 -0
  24. package/template/modules/access-control/wiring/audit/audit.catalogues.imports.ts +1 -0
  25. package/template/modules/access-control/wiring/audit/audit.catalogues.ts +1 -0
  26. package/template/modules/access-control/wiring/audit/readme.modules.md +4 -0
  27. package/template/modules/access-control/wiring/audit/web.messages.ts +6 -0
  28. package/template/modules/access-control/wiring/d1.jsonc +8 -0
  29. package/template/modules/access-control/wiring/env.bindings.ts +2 -0
  30. package/template/modules/access-control/wiring/readme.modules.md +20 -0
  31. package/template/modules/access-control/wiring/test.env.ts +1 -0
  32. package/template/modules/access-control/wiring/test.migrate.ts +1 -0
  33. package/template/modules/access-control/wiring/vitest.bindings.ts +1 -0
  34. package/template/modules/access-control/wiring/worker.imports.ts +1 -0
  35. package/template/modules/access-control/wiring/worker.routes.ts +2 -0
  36. package/template/modules/access-control/with-admin/e2e/access-roles.spec.ts +51 -0
  37. package/template/modules/access-control/with-admin/web/src/admin-roles.tsx +38 -0
  38. package/template/modules/access-control/with-audit/test/access-control-audit.test.ts +25 -0
  39. package/template/modules/admin/files/e2e/admin-accessibility.spec.ts +86 -0
  40. package/template/modules/admin/files/e2e/admin-area.spec.ts +109 -0
  41. package/template/modules/admin/files/e2e/admin-helpers.ts +39 -0
  42. package/template/modules/admin/files/src/admin.ts +46 -0
  43. package/template/modules/admin/files/test/admin.test.ts +47 -0
  44. package/template/modules/admin/files/web/src/admin-area.tsx +140 -0
  45. package/template/modules/admin/files/web/src/admin-client.ts +49 -0
  46. package/template/modules/admin/files/web/src/admin-example.tsx +62 -0
  47. package/template/modules/admin/files/web/src/admin-gate.tsx +112 -0
  48. package/template/modules/admin/module.json +50 -0
  49. package/template/modules/admin/wiring/account.sections.tsx +16 -0
  50. package/template/modules/admin/wiring/audit/admin.authAudit.ts +2 -0
  51. package/template/modules/admin/wiring/audit/admin.imports.tsx +1 -0
  52. package/template/modules/admin/wiring/audit/admin.nav.tsx +1 -0
  53. package/template/modules/admin/wiring/audit/admin.pages.tsx +1 -0
  54. package/template/modules/admin/wiring/audit/admin.server.imports.ts +3 -0
  55. package/template/modules/admin/wiring/audit/admin.stats.ts +3 -0
  56. package/template/modules/admin/wiring/audit/admin.userSections.tsx +1 -0
  57. package/template/modules/admin/wiring/audit/readme.modules.md +4 -0
  58. package/template/modules/admin/wiring/audit/web.messages.ts +1 -0
  59. package/template/modules/admin/wiring/readme.modules.md +26 -0
  60. package/template/modules/admin/wiring/web.messages.ts +22 -0
  61. package/template/modules/admin/wiring/web.routes.imports.tsx +1 -0
  62. package/template/modules/admin/wiring/web.routes.tsx +2 -0
  63. package/template/modules/admin/wiring/web.styles.css +1 -0
  64. package/template/modules/admin/wiring/worker.imports.ts +1 -0
  65. package/template/modules/admin/wiring/worker.routes.ts +2 -0
  66. package/template/modules/admin/with-audit/e2e/admin-activity.spec.ts +37 -0
  67. package/template/modules/admin/with-audit/test/admin-audit.test.ts +37 -0
  68. package/template/modules/admin/with-audit/web/src/admin-activity.tsx +21 -0
  69. package/template/modules/audit/files/src/audit-actions.ts +20 -0
  70. package/template/modules/audit/files/src/audit-log.ts +33 -0
  71. package/template/modules/audit/files/test/audit.test.ts +47 -0
  72. package/template/modules/audit/module.json +36 -0
  73. package/template/modules/audit/wiring/audit.imports.ts +1 -0
  74. package/template/modules/audit/wiring/audit.sink.ts +2 -0
  75. package/template/modules/audit/wiring/auth/audit.catalogues.imports.ts +1 -0
  76. package/template/modules/audit/wiring/auth/audit.catalogues.ts +1 -0
  77. package/template/modules/audit/wiring/auth/readme.modules.md +7 -0
  78. package/template/modules/audit/wiring/auth/web.messages.ts +26 -0
  79. package/template/modules/audit/wiring/auth/web.routes.imports.tsx +1 -0
  80. package/template/modules/audit/wiring/auth/web.routes.tsx +1 -0
  81. package/template/modules/audit/wiring/auth/worker.imports.ts +1 -0
  82. package/template/modules/audit/wiring/auth/worker.routes.ts +2 -0
  83. package/template/modules/audit/wiring/d1.jsonc +8 -0
  84. package/template/modules/audit/wiring/env.bindings.ts +4 -0
  85. package/template/modules/audit/wiring/index.imports.ts +1 -0
  86. package/template/modules/audit/wiring/index.setup.ts +2 -0
  87. package/template/modules/audit/wiring/readme.modules.md +16 -0
  88. package/template/modules/audit/wiring/test.env.ts +1 -0
  89. package/template/modules/audit/wiring/test.migrate.ts +1 -0
  90. package/template/modules/audit/wiring/vars.deployed.jsonc +2 -0
  91. package/template/modules/audit/wiring/vars.dev.jsonc +2 -0
  92. package/template/modules/audit/wiring/vitest.bindings.ts +1 -0
  93. package/template/modules/audit/with-auth/e2e/my-activity.spec.ts +89 -0
  94. package/template/modules/audit/with-auth/src/audit-routes.ts +21 -0
  95. package/template/modules/audit/with-auth/test/audit-routes.test.ts +17 -0
  96. package/template/modules/audit/with-auth/web/src/my-activity.tsx +168 -0
  97. package/template/modules/auth/files/e2e/auth-accessibility.spec.ts +68 -0
  98. package/template/modules/auth/files/e2e/auth-bootstrap-admin.spec.ts +58 -0
  99. package/template/modules/auth/files/e2e/auth-helpers.ts +74 -0
  100. package/template/modules/auth/files/e2e/auth-invitations.spec.ts +51 -0
  101. package/template/modules/auth/files/e2e/auth-phone.spec.ts +41 -0
  102. package/template/modules/auth/files/e2e/auth-seeded.ts +97 -0
  103. package/template/modules/auth/files/e2e/auth-sign-in.spec.ts +57 -0
  104. package/template/modules/auth/files/e2e/auth-two-factor.spec.ts +44 -0
  105. package/template/modules/auth/files/scripts/seed-user.mjs +64 -0
  106. package/template/modules/auth/files/src/auth-choices.ts +33 -0
  107. package/template/modules/auth/files/src/auth-config.ts +55 -0
  108. package/template/modules/auth/files/src/auth.ts +68 -0
  109. package/template/modules/auth/files/src/local-test-mode.ts +49 -0
  110. package/template/modules/auth/files/src/notifications.ts +94 -0
  111. package/template/modules/auth/files/test/auth-fixtures.ts +135 -0
  112. package/template/modules/auth/files/test/auth.test.ts +210 -0
  113. package/template/modules/auth/files/test/totp.ts +49 -0
  114. package/template/modules/auth/files/test/turnstile-fake.ts +42 -0
  115. package/template/modules/auth/files/web/src/account-menu.tsx +25 -0
  116. package/template/modules/auth/files/web/src/account.tsx +87 -0
  117. package/template/modules/auth/files/web/src/auth-client.ts +60 -0
  118. package/template/modules/auth/files/web/src/auth-pages.tsx +152 -0
  119. package/template/modules/auth/module.json +56 -0
  120. package/template/modules/auth/wiring/bindings.deployed.jsonc +2 -0
  121. package/template/modules/auth/wiring/d1.jsonc +7 -0
  122. package/template/modules/auth/wiring/devvars.txt +12 -0
  123. package/template/modules/auth/wiring/env.bindings.ts +26 -0
  124. package/template/modules/auth/wiring/index.deps.ts +1 -0
  125. package/template/modules/auth/wiring/index.imports.ts +4 -0
  126. package/template/modules/auth/wiring/index.setup.ts +7 -0
  127. package/template/modules/auth/wiring/playwright.imports.ts +1 -0
  128. package/template/modules/auth/wiring/playwright.servers.ts +12 -0
  129. package/template/modules/auth/wiring/playwright.setup.ts +2 -0
  130. package/template/modules/auth/wiring/playwright.vars.ts +2 -0
  131. package/template/modules/auth/wiring/readme.modules.md +30 -0
  132. package/template/modules/auth/wiring/readme.provisioning.md +11 -0
  133. package/template/modules/auth/wiring/secrets.jsonc +2 -0
  134. package/template/modules/auth/wiring/test.env.ts +1 -0
  135. package/template/modules/auth/wiring/test.migrate.ts +1 -0
  136. package/template/modules/auth/wiring/vars.dev.jsonc +19 -0
  137. package/template/modules/auth/wiring/vars.production.jsonc +13 -0
  138. package/template/modules/auth/wiring/vars.staging.jsonc +13 -0
  139. package/template/modules/auth/wiring/vitest.bindings.ts +6 -0
  140. package/template/modules/auth/wiring/web.header.tsx +1 -0
  141. package/template/modules/auth/wiring/web.layout.imports.tsx +1 -0
  142. package/template/modules/auth/wiring/web.messages.ts +17 -0
  143. package/template/modules/auth/wiring/web.routes.imports.tsx +12 -0
  144. package/template/modules/auth/wiring/web.routes.tsx +11 -0
  145. package/template/modules/auth/wiring/web.styles.css +1 -0
  146. package/template/modules/auth/wiring/worker.deps.ts +2 -0
  147. package/template/modules/auth/wiring/worker.imports.ts +1 -0
  148. package/template/modules/auth/wiring/worker.routes.ts +2 -0
  149. package/template/modules/example/files/e2e/example-notes.spec.ts +39 -0
  150. package/template/modules/example/files/migrations/0002_example_notes.sql +12 -0
  151. package/template/modules/example/files/src/example-audit-actions.ts +12 -0
  152. package/template/modules/example/files/src/example-notes.ts +136 -0
  153. package/template/modules/example/files/test/example-notes-fixtures.ts +30 -0
  154. package/template/modules/example/files/test/example-notes.test.ts +52 -0
  155. package/template/modules/example/files/web/src/example-notes.tsx +176 -0
  156. package/template/modules/example/module.json +61 -0
  157. package/template/modules/example/wiring/access-control/access.permissions.ts +3 -0
  158. package/template/modules/example/wiring/access-control/access.roles.ts +2 -0
  159. package/template/modules/example/wiring/access-control/example.e2e.imports.ts +2 -0
  160. package/template/modules/example/wiring/access-control/example.e2e.setup.ts +18 -0
  161. package/template/modules/example/wiring/access-control/example.imports.ts +3 -0
  162. package/template/modules/example/wiring/access-control/example.policy.ts +10 -0
  163. package/template/modules/example/wiring/access-control/example.test.caller.ts +17 -0
  164. package/template/modules/example/wiring/access-control/example.test.imports.ts +1 -0
  165. package/template/modules/example/wiring/access-control/readme.modules.md +5 -0
  166. package/template/modules/example/wiring/admin/admin.imports.tsx +1 -0
  167. package/template/modules/example/wiring/admin/admin.nav.tsx +2 -0
  168. package/template/modules/example/wiring/admin/admin.pages.tsx +2 -0
  169. package/template/modules/example/wiring/admin/readme.modules.md +4 -0
  170. package/template/modules/example/wiring/admin/web.messages.ts +11 -0
  171. package/template/modules/example/wiring/admin/worker.imports.ts +1 -0
  172. package/template/modules/example/wiring/admin/worker.routes.ts +2 -0
  173. package/template/modules/example/wiring/audit/audit.catalogues.imports.ts +1 -0
  174. package/template/modules/example/wiring/audit/audit.catalogues.ts +2 -0
  175. package/template/modules/example/wiring/audit/readme.modules.md +4 -0
  176. package/template/modules/example/wiring/readme.modules.md +16 -0
  177. package/template/modules/example/wiring/web.messages.ts +19 -0
  178. package/template/modules/example/wiring/web.routes.imports.tsx +1 -0
  179. package/template/modules/example/wiring/web.routes.tsx +2 -0
  180. package/template/modules/example/wiring/worker.imports.ts +1 -0
  181. package/template/modules/example/wiring/worker.routes.ts +2 -0
  182. package/template/modules/example/with-access-control/test/example-notes-access.test.ts +51 -0
  183. package/template/modules/example/with-admin/e2e/example-notes-admin.spec.ts +55 -0
  184. package/template/modules/example/with-admin/src/example-notes-admin.ts +62 -0
  185. package/template/modules/example/with-admin/test/example-notes-admin.test.ts +42 -0
  186. package/template/modules/example/with-admin/web/src/admin-notes.tsx +99 -0
  187. package/template/modules/example/with-audit/test/example-notes-audit.test.ts +22 -0
  188. package/template/package.json +57 -0
  189. package/template/playwright.config.ts +38 -0
  190. package/template/renovate.json +12 -0
  191. package/template/src/app.ts +46 -0
  192. package/template/src/audit-catalogues.ts +9 -0
  193. package/template/src/audit-sink.ts +21 -0
  194. package/template/src/env.d.ts +21 -0
  195. package/template/src/index.ts +18 -0
  196. package/template/src/v1.ts +24 -0
  197. package/template/standalone/.github/workflows/ci.yml +21 -0
  198. package/template/standalone/.github/workflows/deploy.yml +32 -0
  199. package/template/standalone/.nvmrc +1 -0
  200. package/template/standalone/_gitignore +9 -0
  201. package/template/standalone/pnpm-workspace.yaml +11 -0
  202. package/template/template.json +56 -0
  203. package/template/test/env.d.ts +11 -0
  204. package/template/test/migrate.ts +11 -0
  205. package/template/test/migrations.test.ts +22 -0
  206. package/template/test/worker.test.ts +65 -0
  207. package/template/tsconfig.json +7 -0
  208. package/template/vitest.config.ts +24 -0
  209. package/template/web/index.html +12 -0
  210. package/template/web/src/app.tsx +44 -0
  211. package/template/web/src/home.tsx +51 -0
  212. package/template/web/src/layout.tsx +48 -0
  213. package/template/web/src/main.tsx +16 -0
  214. package/template/web/src/messages.ts +38 -0
  215. package/template/web/src/navigation.tsx +34 -0
  216. package/template/web/src/route-focus.ts +49 -0
  217. package/template/web/src/routes.tsx +38 -0
  218. package/template/web/src/styles.css +11 -0
  219. package/template/web/src/system-theme.ts +16 -0
  220. package/template/web/tsconfig.json +7 -0
  221. package/template/web/vite.config.ts +15 -0
  222. package/template/wrangler.jsonc +96 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/ask.ts","../src/auth-answers.ts","../src/template-modules.ts","../src/answers.ts","../src/args.ts","../src/generate.ts","../src/paths.ts","../src/prompts.ts","../src/main.ts","../src/cli.ts"],"sourcesContent":["// The prompt interface and the error for a wrong answer, shared by every question.\n\n/** The prompts, one per kind of question. Each returns the answer (validated with `validate`). */\nexport interface Ask {\n text(question: { message: string; initialValue: string; validate: (value: string) => string | undefined }): Promise<string>;\n multiselect(question: { message: string; options: { value: string; label: string; hint?: string }[] }): Promise<string[]>;\n select(question: { message: string; options: { value: string; label: string; hint?: string }[]; initialValue: string }): Promise<string>;\n confirm(question: { message: string; initialValue: boolean }): Promise<boolean>;\n info(message: string): void;\n}\n\n/** A wrong answer given as a flag (or a missing one without a terminal). */\nexport class AnswerError extends Error {}\n","// The auth module's questions: sign-in methods (slice 1 only, plan Q59), the\n// registration mode and, with phone OTP, the phone country allow/block lists.\n// The lists have no default anywhere (plan Q35): without a terminal they must be\n// given as flags. The answers are written to the app's src/auth-choices.ts.\nimport { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { AnswerError, type Ask } from \"./ask.js\";\n\nexport const AUTH_MODULE = \"auth\";\n/** The admin area: only administrators use it, and they sign in with email-password and totp. */\nexport const ADMIN_MODULE = \"admin\";\n\nexport const AUTH_METHODS = [\"email-password\", \"phone-otp\", \"totp\"] as const;\nexport type AuthMethodChoice = (typeof AUTH_METHODS)[number];\n\n/** Same values as `@awsmbiz/inbis-auth`'s `REGISTRATION_MODES`. */\nexport const REGISTRATION_MODES = [\"open\", \"invite-only\", \"admin-only\", \"closed\"] as const;\nexport type RegistrationModeChoice = (typeof REGISTRATION_MODES)[number];\n\nexport interface AuthAnswers {\n methods: AuthMethodChoice[];\n registration: RegistrationModeChoice;\n /** With phone OTP only. An empty list means: no restriction of that kind. */\n phone?: { allowed: string[]; blocked: string[] };\n}\n\n/** The auth answers given as flags. A list of `[]` is `none`. */\nexport interface AuthGiven {\n methods?: string[];\n registration?: string;\n phoneAllow?: string[];\n phoneBlock?: string[];\n}\n\nconst DEFAULT_METHODS: AuthMethodChoice[] = [\"email-password\", \"totp\"];\nconst DEFAULT_REGISTRATION: RegistrationModeChoice = \"invite-only\";\n\nconst METHOD_LABELS: Record<AuthMethodChoice, string> = {\n \"email-password\": \"Email and password\",\n \"phone-otp\": \"Phone number and SMS code\",\n totp: \"Two-factor authentication (authenticator app)\",\n};\nconst MODE_HINTS: Record<RegistrationModeChoice, string> = {\n open: \"anyone can sign up\",\n \"invite-only\": \"administrators create accounts or send invitations\",\n \"admin-only\": \"only administrators create accounts; no invitations\",\n closed: \"no new accounts (except the first administrator)\",\n};\n\n/** A calling-code prefix, as `@awsmbiz/inbis-auth` checks it: `+`, then 1-6 digits not starting with 0. */\nconst CALLING_CODE = /^\\+[1-9]\\d{0,5}$/;\n\n/** A comma-separated list; `none` is the empty list. */\nexport function parseList(value: string): string[] {\n return value.trim() === \"none\" ? [] : value.split(\",\").map((item) => item.trim()).filter(Boolean);\n}\n\nfunction checkMethods(methods: string[]): AuthMethodChoice[] {\n for (const method of methods) {\n if (!(AUTH_METHODS as readonly string[]).includes(method)) {\n throw new AnswerError(`Unknown sign-in method \"${method}\". Methods: ${AUTH_METHODS.join(\", \")}.`);\n }\n }\n const chosen = methods as AuthMethodChoice[];\n if (!chosen.includes(\"email-password\") && !chosen.includes(\"phone-otp\")) {\n throw new AnswerError(\"Choose at least one sign-in method: email-password and/or phone-otp.\");\n }\n if (chosen.includes(\"totp\") && !chosen.includes(\"email-password\")) {\n throw new AnswerError(\"totp (2FA) needs email-password: it completes password sign-ins.\");\n }\n return AUTH_METHODS.filter((method) => chosen.includes(method));\n}\n\nfunction checkRegistration(mode: string, methods: AuthMethodChoice[]): RegistrationModeChoice {\n if (!(REGISTRATION_MODES as readonly string[]).includes(mode)) {\n throw new AnswerError(`Unknown registration mode \"${mode}\". Modes: ${REGISTRATION_MODES.join(\", \")}.`);\n }\n if (mode !== \"open\" && !(methods.includes(\"email-password\") && methods.includes(\"totp\"))) {\n throw new AnswerError(\n `Registration \"${mode}\" needs an administrator to create accounts, and administrators sign in with email-password and totp (2FA). Add both methods or choose \"open\".`,\n );\n }\n return mode as RegistrationModeChoice;\n}\n\n/** Why a phone country list is wrong, or `undefined`. */\nfunction listError(list: string[]): string | undefined {\n const bad = list.find((prefix) => !CALLING_CODE.test(prefix));\n return bad === undefined ? undefined : `\"${bad}\" is not a calling-code prefix such as +44 or +1876.`;\n}\n\nfunction checkPhone(allowed: string[], blocked: string[]) {\n const error = listError(allowed) ?? listError(blocked);\n if (error !== undefined) throw new AnswerError(error);\n const both = allowed.find((prefix) => blocked.includes(prefix));\n if (both !== undefined) throw new AnswerError(`\"${both}\" is both allowed and blocked.`);\n return { allowed, blocked };\n}\n\n/** Rejects auth flags given without the auth module (before anything is asked). */\nexport function checkAuthFlagsWithoutModule(given: AuthGiven): void {\n if (given.methods !== undefined) throw new AnswerError(\"--auth-methods needs the auth module (--modules auth).\");\n if (given.registration !== undefined) throw new AnswerError(\"--registration needs the auth module (--modules auth).\");\n if (given.phoneAllow !== undefined || given.phoneBlock !== undefined) {\n throw new AnswerError(\"--phone-allow and --phone-block need the auth module (--modules auth).\");\n }\n}\n\n/**\n * Turns the auth flags into answers: flags are validated and used; missing\n * answers are asked for when `ask` is given, otherwise defaulted, except the\n * phone country lists, which have no default.\n */\nexport async function resolveAuthAnswers(given: AuthGiven, ask: Ask | undefined, modules: string[] = [AUTH_MODULE]): Promise<AuthAnswers> {\n const methodsFor = (answer: string[]) => {\n const methods = checkMethods(answer);\n if (modules.includes(ADMIN_MODULE) && !(methods.includes(\"email-password\") && methods.includes(\"totp\"))) {\n throw new AnswerError(\"The admin module needs administrators, who sign in with email-password and totp (2FA). Add both methods.\");\n }\n return methods;\n };\n let methods: AuthMethodChoice[];\n if (given.methods !== undefined) methods = methodsFor(given.methods);\n else if (ask === undefined) methods = DEFAULT_METHODS;\n else {\n methods = await askUntilValid(ask, () =>\n ask.multiselect({\n message: \"Sign-in methods\",\n options: AUTH_METHODS.map((value) => ({ value, label: METHOD_LABELS[value], ...(DEFAULT_METHODS.includes(value) ? { hint: \"default\" } : {}) })),\n }),\n methodsFor,\n );\n }\n\n let registration: RegistrationModeChoice;\n if (given.registration !== undefined) registration = checkRegistration(given.registration, methods);\n else if (ask === undefined) registration = checkRegistration(DEFAULT_REGISTRATION, methods);\n else {\n registration = await askUntilValid(\n ask,\n () =>\n ask.select({\n message: \"Who may create an account? (registration mode)\",\n options: REGISTRATION_MODES.map((value) => ({ value, label: value, hint: MODE_HINTS[value] })),\n initialValue: methods.includes(\"totp\") ? DEFAULT_REGISTRATION : \"open\",\n }),\n (mode) => checkRegistration(mode, methods),\n );\n }\n\n if (!methods.includes(\"phone-otp\")) {\n if (given.phoneAllow !== undefined || given.phoneBlock !== undefined) {\n throw new AnswerError(\"--phone-allow and --phone-block need phone-otp in --auth-methods.\");\n }\n return { methods, registration };\n }\n\n if (ask === undefined && (given.phoneAllow === undefined || given.phoneBlock === undefined)) {\n throw new AnswerError(\n \"Phone OTP needs --phone-allow and --phone-block: the calling-code prefixes that may and may not receive codes (e.g. --phone-allow +44,+1 --phone-block none). There is no default.\",\n );\n }\n const askList = (message: string) =>\n ask === undefined\n ? Promise.resolve([])\n : ask\n .text({\n message,\n initialValue: \"\",\n validate: (value) => (value.trim() === \"\" ? 'Give a comma-separated list of prefixes, or \"none\".' : listError(parseList(value))),\n })\n .then(parseList);\n const allowed = given.phoneAllow ?? (await askList('Phone countries that may receive codes (calling-code prefixes, e.g. +44,+1; \"none\" = every country)'));\n const blocked = given.phoneBlock ?? (await askList('Phone countries that never receive codes (e.g. +1876; \"none\" = nothing blocked)'));\n return { methods, registration, phone: checkPhone(allowed, blocked) };\n}\n\n/** Asks again after an answer the rules reject, showing why. */\nasync function askUntilValid<A, T>(ask: Ask, question: () => Promise<A>, rule: (answer: A) => T): Promise<T> {\n for (;;) {\n const answer = await question();\n try {\n return rule(answer);\n } catch (error) {\n if (!(error instanceof AnswerError)) throw error;\n ask.info(error.message);\n }\n }\n}\n\nconst list = (items: string[]) => `[${items.map((item) => JSON.stringify(item)).join(\", \")}]`;\n\n/** The app's src/auth-choices.ts for these answers. */\nexport function renderAuthChoices(answers: AuthAnswers): string {\n const phone = answers.phone;\n const phoneOtp =\n phone === undefined\n ? \"false\"\n : `{ ${[\n ...(phone.allowed.length === 0 ? [] : [`allowedCountries: ${list(phone.allowed)}`]),\n `blockedCountries: ${list(phone.blocked)}`,\n ].join(\", \")} }`;\n return `${AUTH_CHOICES_HEADER}\nexport const AUTH_CHOICES: AuthChoices = {\n emailPassword: ${String(answers.methods.includes(\"email-password\"))},\n phoneOtp: ${phoneOtp},\n twoFactor: ${String(answers.methods.includes(\"totp\"))},\n registration: ${JSON.stringify(answers.registration)},\n};\n`;\n}\n\n/** Everything above the `AUTH_CHOICES` constant; the same text as the module's own file. */\nconst AUTH_CHOICES_HEADER = `import type { RegistrationMode } from \"@awsmbiz/inbis-auth\";\n\n/**\n * The sign-in choices made when this app was created (create-inbis-app). They are\n * yours to change: src/auth-config.ts turns them into the auth config, and the\n * tests read them to know which journeys apply.\n */\nexport interface AuthChoices {\n /** Email + password sign-in (and password reset, email verification). */\n emailPassword: boolean;\n /**\n * Sign-in with an SMS code, or \\`false\\`. Countries are calling-code prefixes\n * (\\`\"+44\"\\`, \\`\"+1876\"\\`): absent \\`allowedCountries\\` means every country;\n * \\`blockedCountries\\` wins over it. There is no platform default.\n */\n phoneOtp: false | { allowedCountries?: readonly string[]; blockedCountries: readonly string[] };\n /** Two-factor authentication with an authenticator app (password sign-ins). Administrators need it. */\n twoFactor: boolean;\n /**\n * Who may create an account: \\`open\\` (anyone signs up), \\`invite-only\\`\n * (administrators create accounts or invite), \\`admin-only\\` (administrators\n * create accounts; invitations are refused), \\`closed\\` (no new accounts but\n * the first administrator).\n */\n registration: RegistrationMode;\n}\n`;\n\n/** Writes the answers into the generated app. */\nexport function writeAuthChoices(outDir: string, answers: AuthAnswers): void {\n writeFileSync(join(outDir, \"src\", \"auth-choices.ts\"), renderAuthChoices(answers));\n}\n","// The template module mechanism (docs/template-modules.md): copies the app\n// template's base into a directory and applies the chosen modules to it.\n// Used by the generator and, through `scripts/template-modules.mjs`, by the\n// platform's own checks. Node built-ins only, and erasable TypeScript only, so\n// Node runs this file directly (type stripping) without a build.\nimport { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, relative, sep } from \"node:path\";\n\n/** A snippet file (relative to the module directory), or one per value of the marker's `env` argument. */\nexport type Contribution = string | Record<string, string>;\n\nexport interface Part {\n files?: string;\n wiring?: Record<string, Contribution>;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n scripts?: Record<string, string>;\n}\n\nexport interface ModuleManifest extends Part {\n name: string;\n description?: string;\n order: number;\n requires?: string[];\n /** Binding names the module adds that the generator lets the developer rename (name → description). */\n bindings?: Record<string, string>;\n when?: (Part & { modules: string[] })[];\n /**\n * Wiring points the module marks in files it adds (e.g. admin's page list), for the\n * modules applied alongside it. They exist only when this module is selected.\n */\n points?: Record<string, WiringPoint>;\n dir?: string;\n}\n\n/** A named place in a file where modules insert code (marked `inbis:wire <point>`). */\nexport interface WiringPoint {\n file: string;\n description: string;\n}\n\nexport interface TemplateManifest {\n exclude?: string[];\n standalone?: string;\n /** Binding names of the base that the generator lets the developer rename (name → description). */\n bindings?: Record<string, string>;\n wiring: Record<string, WiringPoint>;\n}\n\ntype PackageJson = Record<string, unknown> & Partial<Record<DependencyField, Record<string, string>>>;\ntype DependencyField = \"dependencies\" | \"devDependencies\" | \"scripts\";\n\n/** A wiring marker: `inbis:wire <point> [key=value ...]` inside any comment. */\nconst MARKER = /inbis:wire\\s+([\\w.-]+)((?:[ \\t]+[\\w-]+=[^\\s*>]+)*)/;\n\n/** Reads `templateDir/template.json`. */\nexport function loadTemplate(templateDir: string): TemplateManifest {\n return JSON.parse(readFileSync(join(templateDir, \"template.json\"), \"utf8\")) as TemplateManifest;\n}\n\n/**\n * Reads every `<name>/module.json` under `root` (the template's `modules/`),\n * checking that each module is ordered after the modules it requires (so\n * manifest order is a dependency order).\n */\nexport function loadModules(root: string): ModuleManifest[] {\n if (!existsSync(root)) return [];\n const manifests = readdirSync(root)\n .filter((name) => existsSync(join(root, name, \"module.json\")))\n .map((name) => {\n const manifest = JSON.parse(readFileSync(join(root, name, \"module.json\"), \"utf8\")) as ModuleManifest;\n if (manifest.name !== name) throw new Error(`modules/${name}/module.json is named \"${manifest.name}\"; it must match its directory.`);\n return { ...manifest, dir: join(root, name) };\n });\n const byName = new Map(manifests.map((m) => [m.name, m]));\n for (const m of manifests) {\n for (const required of m.requires ?? []) {\n const dep = byName.get(required);\n if (dep === undefined) throw new Error(`Module \"${m.name}\" requires unknown module \"${required}\".`);\n if (dep.order >= m.order) throw new Error(`Module \"${m.name}\" (order ${m.order}) requires \"${required}\" (order ${dep.order}); it must come later.`);\n }\n }\n return manifests;\n}\n\n/**\n * Adds the modules the selection requires (transitively) and orders the result\n * by manifest `order`. `added` lists each module that was not asked for, with\n * the module that first required it.\n */\nexport function resolveModules(manifests: Pick<ModuleManifest, \"name\" | \"order\" | \"requires\">[], selected: string[]) {\n const byName = new Map(manifests.map((m) => [m.name, m]));\n const chosen = new Set<string>();\n const added: { name: string; requiredBy: string }[] = [];\n const visit = (name: string, requiredBy: string | undefined) => {\n const manifest = byName.get(name);\n if (manifest === undefined) {\n const known = [...byName.keys()].sort().join(\", \");\n throw new Error(`Unknown module \"${name}\". Known modules: ${known === \"\" ? \"(none)\" : known}.`);\n }\n if (chosen.has(name)) return;\n chosen.add(name);\n if (requiredBy !== undefined && !selected.includes(name)) added.push({ name, requiredBy });\n for (const required of manifest.requires ?? []) visit(required, name);\n };\n for (const name of selected) visit(name, undefined);\n const order = (name: string) => byName.get(name)?.order ?? 0;\n const modules = [...chosen].sort((a, b) => order(a) - order(b) || a.localeCompare(b));\n return { modules, added };\n}\n\n/**\n * Every file under `dir`, as paths relative to it with `/` separators. Paths in\n * `skip` (files or whole directories) are left out without being read.\n */\nexport function listFiles(dir: string, skip = new Set<string>()): string[] {\n const files: string[] = [];\n const walk = (current: string) => {\n for (const entry of readdirSync(current)) {\n const path = join(current, entry);\n const rel = relative(dir, path).split(sep).join(\"/\");\n if (skip.has(rel)) continue;\n if (statSync(path).isDirectory()) walk(path);\n else files.push(rel);\n }\n };\n if (existsSync(dir)) walk(dir);\n return files.sort();\n}\n\n/** Copies `from/**` into `to`, refusing to overwrite (a module may only add files). */\nfunction addFiles(from: string, to: string, owner: string) {\n for (const file of listFiles(from)) {\n const target = join(to, file);\n if (existsSync(target)) throw new Error(`Module \"${owner}\" adds ${file}, which already exists.`);\n mkdirSync(dirname(target), { recursive: true });\n cpSync(join(from, file), target);\n }\n}\n\n/** Merges `extra` into `target`, refusing a key that is already set to a different value. */\nfunction mergeDeps(target: Record<string, string>, extra: Record<string, string>, field: string, owner: string) {\n for (const [name, value] of Object.entries(extra)) {\n const current = target[name];\n if (current !== undefined && current !== value) {\n throw new Error(`Module \"${owner}\" conflicts on ${field} \"${name}\": already ${current}, the module wants ${value}.`);\n }\n target[name] = value;\n }\n}\n\nconst sortKeys = (object: Record<string, string>) =>\n Object.fromEntries(Object.entries(object).sort(([a], [b]) => a.localeCompare(b)));\n\n/** Parses a marker's `key=value` arguments. */\nfunction markerArgs(text: string): Record<string, string> {\n return Object.fromEntries(\n text\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((pair) => pair.split(\"=\", 2) as [string, string]),\n );\n}\n\n/** Snippet lines with their common indentation removed (trailing blank lines dropped). */\nfunction dedent(text: string): string[] {\n const lines = text.replace(/\\s+$/, \"\").split(\"\\n\");\n const indents = lines.filter((line) => line.trim() !== \"\").map((line) => /^[ \\t]*/.exec(line)?.[0].length ?? 0);\n const common = Math.min(...indents, Infinity);\n return lines.map((line) => (line.trim() === \"\" ? \"\" : line.slice(common === Infinity ? 0 : common)));\n}\n\nexport interface ApplyOptions {\n templateDir: string;\n outDir: string;\n modules: string[];\n /** Where modules live; defaults to the template's `modules/` (tests point it at fixture modules). */\n modulesDir?: string;\n /**\n * Also copy the template's standalone files (the ones that would break the\n * monorepo workspace if they lived in the template itself, e.g. `pnpm-workspace.yaml`).\n */\n standalone?: boolean;\n /** Remove the marker lines. */\n stripMarkers?: boolean;\n}\n\n/**\n * Copies the template's base to `outDir` and applies `modules` (plus the ones\n * they require). Throws on an unknown module or wiring point, a missing\n * marker, a file clash or a conflicting dependency (the output directory is\n * then incomplete: use a fresh one).\n */\nexport function applyModules({\n templateDir,\n outDir,\n modules: selected,\n modulesDir = join(templateDir, \"modules\"),\n standalone = false,\n stripMarkers = false,\n}: ApplyOptions) {\n const template = loadTemplate(templateDir);\n const manifests = loadModules(modulesDir);\n const { modules, added } = resolveModules(manifests, selected);\n const byName = new Map(manifests.map((m) => [m.name, m]));\n\n // Every module's parts that apply to this selection, in module order.\n const parts: { owner: string; dir: string; part: Part }[] = [];\n const chosen = modules.flatMap((name) => byName.get(name) ?? []);\n for (const manifest of chosen) {\n const { name } = manifest;\n const dir = manifest.dir ?? join(modulesDir, name);\n parts.push({ owner: name, dir, part: { ...manifest, files: manifest.files ?? \"files\" } });\n for (const extra of manifest.when ?? []) {\n if (extra.modules.every((m) => modules.includes(m))) parts.push({ owner: name, dir, part: extra });\n }\n }\n // The template's wiring points, plus those the selected modules declare.\n const points: Record<string, WiringPoint> = { ...template.wiring };\n for (const manifest of chosen) {\n for (const [point, declaration] of Object.entries(manifest.points ?? {})) {\n if (points[point] !== undefined) throw new Error(`Module \"${manifest.name}\" declares wiring point \"${point}\", which is already declared.`);\n points[point] = declaration;\n }\n }\n for (const { owner, part } of parts) {\n for (const point of Object.keys(part.wiring ?? {})) {\n if (points[point] === undefined) throw new Error(`Module \"${owner}\" contributes to unknown wiring point \"${point}\".`);\n }\n }\n\n // Base files.\n const exclude = new Set([...(template.exclude ?? []), \"template.json\", \"modules\", \"node_modules\"]);\n if (template.standalone !== undefined) exclude.add(template.standalone);\n mkdirSync(outDir, { recursive: true });\n for (const file of listFiles(templateDir, exclude)) {\n mkdirSync(dirname(join(outDir, file)), { recursive: true });\n cpSync(join(templateDir, file), join(outDir, file));\n }\n if (standalone && template.standalone !== undefined) addFiles(join(templateDir, template.standalone), outDir, \"(standalone)\");\n\n // Module files and package.json.\n const pkgPath = join(outDir, \"package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as PackageJson;\n for (const { owner, dir, part } of parts) {\n if (part.files !== undefined) addFiles(join(dir, part.files), outDir, owner);\n for (const field of [\"dependencies\", \"devDependencies\", \"scripts\"] as const) {\n const extra = part[field];\n if (extra === undefined) continue;\n const merged = { ...pkg[field] };\n mergeDeps(merged, extra, field, owner);\n pkg[field] = sortKeys(merged);\n }\n }\n writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\\n`);\n\n // Wiring: each marker line receives the contributions to its point, in module order.\n const files = new Set(Object.values(points).map((point) => point.file));\n for (const file of files) {\n const path = join(outDir, file);\n if (!existsSync(path)) throw new Error(`Wiring file ${file} does not exist in the app.`);\n const seen = new Set<string>();\n const output: string[] = [];\n for (const line of readFileSync(path, \"utf8\").split(\"\\n\")) {\n const match = MARKER.exec(line);\n if (match === null) {\n output.push(line);\n continue;\n }\n const point = match[1] ?? \"\";\n if (points[point]?.file !== file) throw new Error(`${file} marks wiring point \"${point}\", which neither template.json nor a selected module declares for it.`);\n seen.add(point);\n const args = markerArgs(match[2] ?? \"\");\n const indent = /^[ \\t]*/.exec(line)?.[0] ?? \"\";\n for (const { owner, dir, part } of parts) {\n const contribution = part.wiring?.[point];\n if (contribution === undefined) continue;\n const snippet = typeof contribution === \"string\" ? contribution : contribution[args[\"env\"] ?? \"\"];\n if (snippet === undefined) continue;\n const text = readFileSync(join(dir, snippet), \"utf8\").replace(/\\{\\{(\\w+)\\}\\}/g, (placeholder, key: string) => {\n const value = args[key];\n if (value === undefined) throw new Error(`Module \"${owner}\" uses ${placeholder} in ${snippet}, which the \"${point}\" marker in ${file} does not supply.`);\n return value;\n });\n for (const snippetLine of dedent(text)) output.push(snippetLine === \"\" ? \"\" : indent + snippetLine);\n }\n if (!stripMarkers) output.push(line);\n }\n for (const [point, { file: declared }] of Object.entries(points)) {\n if (declared === file && !seen.has(point)) throw new Error(`Wiring point \"${point}\" has no marker in ${file}.`);\n }\n writeFileSync(path, output.join(\"\\n\"));\n }\n\n return { modules, added, manifests: chosen };\n}\n","// What the developer chose: every answer comes from a flag, a prompt or a\n// default. Prompting goes through `Ask` (the clack adapter in prompts.ts), so\n// the rules here are the same with and without a terminal.\nimport { existsSync, readdirSync } from \"node:fs\";\nimport { basename, resolve } from \"node:path\";\nimport { AUTH_MODULE, checkAuthFlagsWithoutModule, resolveAuthAnswers, type AuthAnswers, type AuthGiven } from \"./auth-answers.js\";\nimport { AnswerError, type Ask } from \"./ask.js\";\nimport type { ModuleManifest } from \"./template-modules.js\";\nimport { resolveModules } from \"./template-modules.js\";\n\nexport interface Answers {\n /** Absolute path of the new app's directory. */\n directory: string;\n /** package.json name. */\n appName: string;\n /** Cloudflare Worker name; deployed Workers are `<worker>-staging` and `<worker>-production`. */\n workerName: string;\n domains: { staging: string; production: string };\n /** Every renameable binding: template name → name in the app. */\n bindings: Record<string, string>;\n /** Modules as resolved (required ones added), in module order. */\n modules: string[];\n /** The auth module's answers; present exactly when the auth module is chosen. */\n auth?: AuthAnswers;\n git: boolean;\n}\n\n/** Answers given as flags (parsed by args.ts). Missing ones are prompted for or defaulted. */\nexport interface Given {\n directory?: string;\n appName?: string;\n workerName?: string;\n stagingDomain?: string;\n productionDomain?: string;\n bindings?: Record<string, string>;\n modules?: string[];\n /** The auth module's answers (auth-answers.ts). */\n auth?: AuthGiven;\n git?: boolean;\n}\n\n/** What the template offers: its renameable bindings and its modules. */\nexport interface Offer {\n bindings: Record<string, string>;\n modules: ModuleManifest[];\n}\n\nconst WORKER_NAME_MAX = 52; // `<worker>-production` stays within Cloudflare's 63 characters.\n\nexport const validate = {\n directory: (value: string, cwd = process.cwd()): string | undefined => {\n if (value.trim() === \"\") return \"Give the directory to create the app in.\";\n const path = resolve(cwd, value);\n if (existsSync(path) && readdirSync(path).length > 0) return `${path} is not empty.`;\n return undefined;\n },\n appName: (value: string): string | undefined => {\n if (value.length > 214 || !/^(?:@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(value)) {\n return `\"${value}\" is not a valid package name: use lowercase letters, digits, \"-\", \".\" or \"_\" (optionally @scope/name).`;\n }\n return undefined;\n },\n workerName: (value: string): string | undefined => {\n if (value.length > WORKER_NAME_MAX || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value)) {\n return `\"${value}\" is not a valid Worker name: lowercase letters, digits and inner \"-\", at most ${WORKER_NAME_MAX} characters.`;\n }\n return undefined;\n },\n domain: (value: string): string | undefined => {\n if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/.test(value)) {\n return `\"${value}\" is not a domain name (e.g. app.example.com).`;\n }\n return undefined;\n },\n binding: (value: string): string | undefined => {\n if (!/^[A-Z][A-Z0-9_]*$/.test(value)) return `\"${value}\" is not a binding name: uppercase letters, digits and \"_\", starting with a letter.`;\n return undefined;\n },\n};\n\n/** A Worker name derived from a package name (scope dropped, other characters replaced). */\nexport function workerNameFor(appName: string): string {\n const name = (appName.split(\"/\").pop() ?? appName)\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, WORKER_NAME_MAX)\n .replace(/-+$/, \"\");\n return name === \"\" ? \"app\" : name;\n}\n\nfunction check(error: string | undefined) {\n if (error !== undefined) throw new AnswerError(error);\n}\n\n/**\n * Turns flags into answers: a flag is validated and used; a missing answer is\n * asked for when `ask` is given (with the default filled in), otherwise\n * defaulted, and the directory, which has no default, is an error. Paths are\n * relative to `cwd`.\n */\nexport async function resolveAnswers(given: Given, offer: Offer, ask: Ask | undefined, cwd = process.cwd()): Promise<Answers> {\n const answer = async (value: string | undefined, fallback: string, message: string, rule: (value: string) => string | undefined) => {\n if (value !== undefined) {\n check(rule(value));\n return value;\n }\n if (ask === undefined) {\n check(rule(fallback));\n return fallback;\n }\n return ask.text({ message, initialValue: fallback, validate: rule });\n };\n\n if (given.directory === undefined && ask === undefined) {\n throw new AnswerError(\"Give the directory to create the app in: create-inbis-app <directory> (see --help).\");\n }\n const inCwd = (value: string) => validate.directory(value, cwd);\n const directory = resolve(cwd, await answer(given.directory, \"my-inbis-app\", \"Directory for the new app\", inCwd));\n const appName = await answer(given.appName, basename(directory), \"App name (package.json)\", validate.appName);\n const workerName = await answer(given.workerName, workerNameFor(appName), \"Cloudflare Worker name\", validate.workerName);\n const staging = await answer(given.stagingDomain, `staging.${workerName}.example.com`, \"Staging domain\", validate.domain);\n const production = await answer(given.productionDomain, `${workerName}.example.com`, \"Production domain\", validate.domain);\n\n const modules = await chooseModules(given.modules, offer.modules, ask);\n if (!modules.includes(AUTH_MODULE)) checkAuthFlagsWithoutModule(given.auth ?? {});\n const auth = modules.includes(AUTH_MODULE) ? await resolveAuthAnswers(given.auth ?? {}, ask, modules) : undefined;\n const bindings = await nameBindings(given.bindings ?? {}, offer, modules, ask);\n const git = given.git ?? (ask === undefined ? false : await ask.confirm({ message: \"Initialise a git repository?\", initialValue: true }));\n\n return { directory, appName, workerName, domains: { staging, production }, bindings, modules, ...(auth === undefined ? {} : { auth }), git };\n}\n\nasync function chooseModules(given: string[] | undefined, offered: ModuleManifest[], ask: Ask | undefined): Promise<string[]> {\n let selected = given ?? [];\n if (given === undefined && ask !== undefined && offered.length > 0) {\n selected = await ask.multiselect({\n message: \"Modules (none gives the minimal app)\",\n options: offered.map((m) => ({ value: m.name, label: m.name, ...(m.description === undefined ? {} : { hint: m.description }) })),\n });\n }\n let resolved: ReturnType<typeof resolveModules>;\n try {\n resolved = resolveModules(offered, selected);\n } catch (error) {\n throw new AnswerError((error as Error).message);\n }\n for (const { name, requiredBy } of resolved.added) {\n const message = `Added module \"${name}\": \"${requiredBy}\" requires it.`;\n if (ask === undefined) console.log(message);\n else ask.info(message);\n }\n return resolved.modules;\n}\n\nasync function nameBindings(given: Record<string, string>, offer: Offer, modules: string[], ask: Ask | undefined) {\n const declared: Record<string, string> = { ...offer.bindings };\n for (const m of offer.modules) if (modules.includes(m.name)) Object.assign(declared, m.bindings);\n for (const name of Object.keys(given)) {\n if (declared[name] === undefined) {\n const known = Object.keys(declared).sort().join(\", \");\n throw new AnswerError(`There is no binding \"${name}\" to rename. Renameable bindings: ${known === \"\" ? \"(none)\" : known}.`);\n }\n }\n const bindings: Record<string, string> = {};\n for (const [name, description] of Object.entries(declared)) {\n const value = given[name];\n if (value !== undefined) check(validate.binding(value));\n bindings[name] =\n value ?? (ask === undefined ? name : await ask.text({ message: `Binding name for ${description}`, initialValue: name, validate: validate.binding }));\n }\n const names = Object.values(bindings);\n const twice = names.find((name, index) => names.indexOf(name) !== index);\n if (twice !== undefined) throw new AnswerError(`Two bindings would be named \"${twice}\".`);\n return bindings;\n}\n","// Command-line flags: one per prompt, so generation can be scripted.\nimport { parseArgs } from \"node:util\";\nimport type { Given } from \"./answers.js\";\nimport { AnswerError } from \"./ask.js\";\nimport { parseList, type AuthGiven } from \"./auth-answers.js\";\n\nexport const USAGE = `Usage: pnpm create @awsmbiz/inbis-app [directory] [flags]\n\nCreates a new Inbis app. Every question has a flag; questions without one are\nasked in a terminal, or take their default with --yes (or without a terminal).\n\n [directory] Directory to create (must be empty or missing)\n --name <name> App (package.json) name. Default: the directory name\n --worker <name> Cloudflare Worker name. Default: from the app name\n --staging-domain <domain> Staging custom domain. Default: staging.<worker>.example.com\n --production-domain <dom> Production custom domain. Default: <worker>.example.com\n --binding <NAME=NEW> Rename a binding (repeatable), e.g. --binding DB=APP_DB\n --modules <a,b|none> Modules to add; ones they require are added too\n --auth-methods <a,b> auth: sign-in methods among email-password, phone-otp,\n totp. Default: email-password,totp\n --registration <mode> auth: who may create an account: open, invite-only,\n admin-only, closed. Default: invite-only\n --phone-allow <+44,..|none> auth with phone-otp: calling-code prefixes that may\n receive codes (none: every country). No default\n --phone-block <+1876,..|none>\n auth with phone-otp: prefixes that never receive\n codes (none: nothing blocked). No default\n --git / --no-git Initialise a git repository. Default: ask; no with --yes\n -y, --yes Do not prompt: use the defaults for missing answers\n --platform-version <ver> Version to pin @awsmbiz packages to. Default: this\n generator's version (the platform releases in lockstep)\n --modules-dir <dir> Take modules from <dir> instead of the template's own\n (platform tests)\n -h, --help Show this help\n -v, --version Show the version`;\n\nexport interface Flags {\n given: Given;\n yes: boolean;\n help: boolean;\n version: boolean;\n platformVersion?: string;\n modulesDir?: string;\n}\n\n/** Parses argv into flags; unknown flags and malformed values are an AnswerError. */\nexport function parseFlags(argv: string[]): Flags {\n let parsed: ReturnType<typeof parse>;\n try {\n parsed = parse(argv);\n } catch (error) {\n throw new AnswerError(`${(error as Error).message}\\n\\n${USAGE}`);\n }\n const { values, positionals } = parsed;\n if (positionals.length > 1) throw new AnswerError(`Give one directory, not ${positionals.length}: ${positionals.join(\" \")}.`);\n\n const given: Given = {};\n const set = <K extends keyof Given>(key: K, value: Given[K] | undefined) => {\n if (value !== undefined) given[key] = value;\n };\n set(\"directory\", positionals[0]);\n set(\"appName\", values.name);\n set(\"workerName\", values.worker);\n set(\"stagingDomain\", values[\"staging-domain\"]?.toLowerCase());\n set(\"productionDomain\", values[\"production-domain\"]?.toLowerCase());\n set(\"git\", values.git);\n if (values.modules !== undefined) {\n const list = values.modules.trim() === \"none\" ? [] : values.modules.split(\",\").map((m) => m.trim()).filter(Boolean);\n set(\"modules\", list);\n }\n const auth: AuthGiven = {};\n if (values[\"auth-methods\"] !== undefined) auth.methods = parseList(values[\"auth-methods\"]);\n if (values.registration !== undefined) auth.registration = values.registration;\n if (values[\"phone-allow\"] !== undefined) auth.phoneAllow = parseList(values[\"phone-allow\"]);\n if (values[\"phone-block\"] !== undefined) auth.phoneBlock = parseList(values[\"phone-block\"]);\n if (Object.keys(auth).length > 0) set(\"auth\", auth);\n if (values.binding !== undefined) {\n const bindings: Record<string, string> = {};\n for (const pair of values.binding) {\n const at = pair.indexOf(\"=\");\n if (at < 1 || at === pair.length - 1) throw new AnswerError(`--binding takes NAME=NEW_NAME, got \"${pair}\".`);\n bindings[pair.slice(0, at)] = pair.slice(at + 1);\n }\n set(\"bindings\", bindings);\n }\n\n return {\n given,\n yes: values.yes,\n help: values.help,\n version: values.version,\n ...(values[\"platform-version\"] === undefined ? {} : { platformVersion: values[\"platform-version\"] }),\n ...(values[\"modules-dir\"] === undefined ? {} : { modulesDir: values[\"modules-dir\"] }),\n };\n}\n\nfunction parse(argv: string[]) {\n return parseArgs({\n args: argv,\n allowPositionals: true,\n allowNegative: true,\n strict: true,\n options: {\n name: { type: \"string\" },\n worker: { type: \"string\" },\n \"staging-domain\": { type: \"string\" },\n \"production-domain\": { type: \"string\" },\n binding: { type: \"string\", multiple: true },\n modules: { type: \"string\" },\n \"auth-methods\": { type: \"string\" },\n registration: { type: \"string\" },\n \"phone-allow\": { type: \"string\" },\n \"phone-block\": { type: \"string\" },\n git: { type: \"boolean\" },\n yes: { type: \"boolean\", short: \"y\", default: false },\n \"platform-version\": { type: \"string\" },\n \"modules-dir\": { type: \"string\" },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n}\n","// Writes the app: the template's base plus the chosen modules, with the\n// template's placeholder names replaced by the answers and the platform\n// packages pinned to the published version. The result keeps no link to the\n// template (plan Q18): no markers, no manifests.\nimport { execFileSync } from \"node:child_process\";\nimport { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport type { Answers } from \"./answers.js\";\nimport { writeAuthChoices } from \"./auth-answers.js\";\nimport { applyModules, listFiles } from \"./template-modules.js\";\n\n/** The template's own name, used for the app name, the Worker name and in resource names. */\nexport const PLACEHOLDER = \"react-hono-app\";\n\nexport interface GenerateOptions {\n templateDir: string;\n modulesDir?: string;\n answers: Answers;\n platformVersion: string;\n}\n\ntype PackageJson = Record<string, unknown> & Partial<Record<\"dependencies\" | \"devDependencies\", Record<string, string>>>;\n\nexport function generate({ templateDir, modulesDir, answers, platformVersion }: GenerateOptions) {\n const outDir = answers.directory;\n const staged = stageTemplate(templateDir);\n try {\n applyModules({\n templateDir: staged,\n outDir,\n modules: answers.modules,\n ...(modulesDir === undefined ? {} : { modulesDir }),\n standalone: true,\n stripMarkers: true,\n });\n } finally {\n rmSync(staged, { recursive: true, force: true });\n }\n\n if (answers.auth !== undefined) writeAuthChoices(outDir, answers.auth);\n substitute(outDir, answers);\n\n const pkgPath = join(outDir, \"package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as PackageJson;\n pkg[\"name\"] = answers.appName;\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\n const deps = pkg[field] ?? {};\n for (const [name, range] of Object.entries(deps)) if (range.startsWith(\"workspace:\")) deps[name] = platformVersion;\n }\n writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\\n`);\n\n if (answers.git) execFileSync(\"git\", [\"init\", \"--quiet\", \"--initial-branch=main\"], { cwd: outDir, stdio: \"ignore\" });\n}\n\n/** Dotfiles that `pnpm pack` drops; the package stores them as `_gitignore`, `_npmrc`, `_npmignore`. */\nconst STORED_DOTFILES = new Set([\"_gitignore\", \"_npmrc\", \"_npmignore\"]);\n\n/** Copies the shipped template to a temporary directory with its dotfiles' real names restored. */\nfunction stageTemplate(templateDir: string): string {\n const staged = mkdtempSync(join(tmpdir(), \"inbis-template-\"));\n for (const file of listFiles(templateDir)) {\n const name = basename(file);\n const target = join(staged, dirname(file), STORED_DOTFILES.has(name) ? `.${name.slice(1)}` : name);\n mkdirSync(dirname(target), { recursive: true });\n cpSync(join(templateDir, file), target);\n }\n return staged;\n}\n\nconst escape = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/**\n * Replaces, in every text file and in one pass (so a replacement is never\n * replaced again): the template's staging and production domains, then its\n * name (the Worker name: wrangler `name`, D1 database names, README commands),\n * then each renamed binding as a whole word. The README's title becomes the app name.\n */\nfunction substitute(outDir: string, answers: Answers) {\n const replacements = new Map<string, string>([\n [`staging.${PLACEHOLDER}.example.com`, answers.domains.staging],\n [`${PLACEHOLDER}.example.com`, answers.domains.production],\n [PLACEHOLDER, answers.workerName],\n ]);\n const words = Object.entries(answers.bindings).filter(([from, to]) => from !== to);\n const pattern = new RegExp(\n [...[...replacements.keys()].map(escape), ...words.map(([from]) => `\\\\b${escape(from)}\\\\b`)].join(\"|\"),\n \"g\",\n );\n const bindings = new Map(words);\n for (const file of listFiles(outDir)) {\n const path = join(outDir, file);\n const buffer = readFileSync(path);\n if (buffer.includes(0)) continue; // binary\n const before = buffer.toString(\"utf8\");\n let after = before.replace(pattern, (match) => replacements.get(match) ?? bindings.get(match) ?? match);\n if (file === \"README.md\" && before.startsWith(`# ${PLACEHOLDER}\\n`)) after = `# ${answers.appName}${after.slice(after.indexOf(\"\\n\"))}`;\n if (after !== before) writeFileSync(path, after);\n }\n}\n","import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/** The package root: dist/cli.mjs lives one level below it, next to template/. */\nconst packageRoot = join(import.meta.dirname, \"..\");\n\n/** The app template shipped in the package (copied there by `pnpm build`). */\nexport const templateDir = join(packageRoot, \"template\");\n\n/** This generator's version, which is also the platform's: every package is released in lockstep. */\nexport const ownVersion = (JSON.parse(readFileSync(join(packageRoot, \"package.json\"), \"utf8\")) as { version: string }).version;\n","// The clack adapter for `Ask`: prompts in a terminal. Cancelling (Ctrl+C)\n// stops generation before anything is written.\nimport type { Readable, Writable } from \"node:stream\";\nimport * as p from \"@clack/prompts\";\nimport type { Ask } from \"./ask.js\";\n\nexport class Cancelled extends Error {}\n\nfunction answered<T>(value: T): Exclude<T, symbol> {\n if (p.isCancel(value) || typeof value === \"symbol\") throw new Cancelled(\"Cancelled: nothing was created.\");\n return value as Exclude<T, symbol>;\n}\n\nexport function clackAsk(io: { input?: Readable; output?: Writable }): Ask {\n return {\n async text({ message, initialValue, validate }) {\n return answered(await p.text({ ...io, message, initialValue, validate: (value) => validate(value ?? \"\") }));\n },\n async multiselect({ message, options }) {\n return answered(await p.multiselect({ ...io, message, options, required: false }));\n },\n async select({ message, options, initialValue }) {\n return answered(await p.select({ ...io, message, options, initialValue }));\n },\n async confirm({ message, initialValue }) {\n return answered(await p.confirm({ ...io, message, initialValue }));\n },\n info(message) {\n p.log.info(message, io);\n },\n };\n}\n\nexport const intro = (title: string, io: { output?: Writable }) => {\n p.intro(title, io);\n};\nexport const outro = (message: string, io: { output?: Writable }) => {\n p.outro(message, io);\n};\n","import { join, relative, resolve } from \"node:path\";\nimport type { Readable, Writable } from \"node:stream\";\nimport { resolveAnswers } from \"./answers.js\";\nimport { AnswerError } from \"./ask.js\";\nimport { parseFlags, USAGE } from \"./args.js\";\nimport { generate } from \"./generate.js\";\nimport { ownVersion, templateDir } from \"./paths.js\";\nimport { Cancelled, clackAsk, intro, outro } from \"./prompts.js\";\nimport { loadModules, loadTemplate } from \"./template-modules.js\";\n\nexport interface Io {\n stdin: Readable & { isTTY?: boolean };\n stdout: Writable;\n stderr: Writable;\n env: Record<string, string | undefined>;\n cwd: string;\n}\n\nconst defaultIo: Io = { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, env: process.env, cwd: process.cwd() };\n\n/**\n * The CLI: parses flags, asks for what is missing (only in a terminal and\n * without --yes), writes the app. Returns the exit code.\n */\nexport async function main(argv: string[], io: Io = defaultIo): Promise<number> {\n const say = (text: string) => io.stdout.write(`${text}\\n`);\n try {\n const flags = parseFlags(argv);\n if (flags.help) {\n say(USAGE);\n return 0;\n }\n if (flags.version) {\n say(ownVersion);\n return 0;\n }\n const ci = io.env[\"CI\"] !== undefined && io.env[\"CI\"] !== \"\" && io.env[\"CI\"] !== \"false\";\n const interactive = !flags.yes && io.stdin.isTTY === true && !ci;\n const prompts = { input: io.stdin, output: io.stdout };\n const modulesDir = flags.modulesDir === undefined ? undefined : resolve(io.cwd, flags.modulesDir);\n const offer = {\n bindings: loadTemplate(templateDir).bindings ?? {},\n modules: loadModules(modulesDir ?? join(templateDir, \"modules\")),\n };\n\n if (interactive) intro(\"create-inbis-app\", prompts);\n const answers = await resolveAnswers(flags.given, offer, interactive ? clackAsk(prompts) : undefined, io.cwd);\n generate({\n templateDir,\n ...(modulesDir === undefined ? {} : { modulesDir }),\n answers,\n platformVersion: flags.platformVersion ?? ownVersion,\n });\n\n const where = relative(io.cwd, answers.directory) || \".\";\n const what = answers.modules.length === 0 ? \"the minimal app\" : `the app with ${answers.modules.join(\", \")}`;\n const next = [`cd ${where}`, \"pnpm install\", \"pnpm dev\"].map((step) => ` ${step}`).join(\"\\n\");\n const summary = `Created ${what} in ${where} (platform ${flags.platformVersion ?? ownVersion}).\\nNext steps (see README.md):\\n${next}`;\n if (interactive) outro(summary, prompts);\n else say(summary);\n return 0;\n } catch (error) {\n if (error instanceof AnswerError || error instanceof Cancelled) {\n io.stderr.write(`${error.message}\\n`);\n return 1;\n }\n throw error;\n }\n}\n","#!/usr/bin/env node\n// `pnpm create @awsmbiz/inbis-app [directory] [flags]` runs this bin.\nimport { main } from \"./main.js\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@awsmbiz/create-inbis-app",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Creates a new Inbis app from the platform's app template: `pnpm create @awsmbiz/inbis-app`.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/awsmbiz/inbis-platform.git",
9
+ "directory": "packages/create-inbis-app"
10
+ },
11
+ "publishConfig": {
12
+ "registry": "https://registry.npmjs.org/",
13
+ "access": "public"
14
+ },
15
+ "type": "module",
16
+ "files": [
17
+ "dist",
18
+ "template"
19
+ ],
20
+ "bin": {
21
+ "create-inbis-app": "./dist/cli.mjs"
22
+ },
23
+ "engines": {
24
+ "node": ">=24.0.0"
25
+ },
26
+ "dependencies": {
27
+ "@clack/prompts": "1.8.1"
28
+ },
29
+ "devDependencies": {
30
+ "@awsmbiz/inbis-eslint-config": "0.1.0",
31
+ "@awsmbiz/inbis-tsconfig": "0.1.0",
32
+ "@types/node": "24.19.2",
33
+ "eslint": "10.14.0",
34
+ "publint": "0.3.25",
35
+ "tsdown": "0.23.4",
36
+ "typescript": "6.0.3",
37
+ "vitest": "5.0.5"
38
+ },
39
+ "scripts": {
40
+ "build": "tsdown && node scripts/copy-template.mjs",
41
+ "typecheck": "tsc",
42
+ "lint": "eslint . --max-warnings 0",
43
+ "test": "vitest run",
44
+ "check:exports": "publint --strict"
45
+ }
6
46
  }
@@ -0,0 +1,4 @@
1
+ # Local development secrets for `wrangler dev`. Copy to .dev.vars (gitignored):
2
+ # cp .dev.vars.example .dev.vars
3
+ # Deployed environments set them with `wrangler secret put <NAME> --env <env>`.
4
+ # inbis:wire devvars
@@ -0,0 +1,115 @@
1
+ # react-hono-app
2
+
3
+ A Hono API and a React SPA served by one Cloudflare Worker, with D1, built on the Inbis
4
+ platform packages (`@awsmbiz/inbis-*`). This code is yours: change anything. Platform
5
+ behaviour arrives through package upgrades, never by syncing this template.
6
+
7
+ ## Set up
8
+
9
+ Platform packages are public on npm, so there is no registry or token to set up.
10
+
11
+ ```sh
12
+ pnpm install
13
+ cp .dev.vars.example .dev.vars # local secrets, if any
14
+ pnpm db:migrate # apply every migration source to the local D1
15
+ pnpm dev # build the SPA, then wrangler dev on http://localhost:8787
16
+ pnpm dev:web # SPA with hot reload on :5173, API proxied to :8787
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ | Command | |
22
+ |---|---|
23
+ | `pnpm typecheck` | TypeScript for the Worker and tests, the SPA (`web/`) and the browser tests (`e2e/`) |
24
+ | `pnpm lint` | ESLint |
25
+ | `pnpm test` | Workers integration tests (Vitest in workerd, same `wrangler.jsonc` as `wrangler dev`) |
26
+ | `pnpm build` | Build the SPA and bundle the Worker (`wrangler deploy --dry-run`) |
27
+ | `pnpm test:e2e` | Playwright + axe on a fresh local D1 (port 8790). Run `pnpm exec playwright install chromium` once |
28
+ | `pnpm db:migrate` / `pnpm db:status` | Apply / show migrations (local; add `--remote --env <env>` via `pnpm exec inbis-db`) |
29
+ | `pnpm typegen` | Write wrangler's view of the Worker's types to `.wrangler/types/env.d.ts`, to compare with `src/env.d.ts` |
30
+
31
+ ## What it serves
32
+
33
+ | Path | |
34
+ |---|---|
35
+ | `GET /health`, `GET /health/ready` | Liveness; readiness (checks D1) |
36
+ | `GET /openapi.json` | The OpenAPI 3.1 description of every route |
37
+ | `GET /v1/hello?name=` | An example route: replace it with your own in `src/v1.ts` |
38
+ | everything else | The SPA (`web/`, Workers Static Assets, unknown paths fall back to `index.html`) |
39
+
40
+ ## Structure
41
+
42
+ - `src/app.ts`: `createApp(deps)` builds the Worker app with `@awsmbiz/inbis-api`
43
+ (request IDs, error envelope, CORS and origin checks from `TRUSTED_ORIGINS`, health,
44
+ OpenAPI). `src/index.ts` is the Worker entry. `src/v1.ts` holds the `/v1` routes.
45
+ - `src/env.d.ts`: the Worker's bindings and vars. Keep it in step with `wrangler.jsonc`.
46
+ - `src/audit-sink.ts`, `src/audit-catalogues.ts`: where platform packages record audit
47
+ events (`auditSinkFor(env)`), and the platform action names the audit trail accepts. Both
48
+ stay empty until the app has an audit trail (`@awsmbiz/inbis-audit`).
49
+ - `migrations/`: the app's own D1 migrations (binding `DB`, table `d1_migrations`).
50
+ Platform packages bring theirs through their own bindings, applied first.
51
+ - `web/`: the React SPA (Vite, Tailwind v4, `@awsmbiz/inbis-ui`). Pages are listed in
52
+ `web/src/routes.tsx`; every user-facing string is a message key in `web/src/messages.ts`.
53
+ The app follows the system light/dark setting.
54
+ - `test/`: Vitest in workerd. `e2e/`: Playwright, with axe checks of every page in both
55
+ themes at desktop and phone sizes (WCAG 2.2 AA).
56
+
57
+ <!-- inbis:wire readme.modules -->
58
+
59
+ ## Provision Cloudflare resources
60
+
61
+ Resources are named `<app>-<env>`. D1 is created in the `apac` location by default; pick
62
+ another location hint (`weur`, `enam`, …) or a jurisdiction (`eu`, `us`, `fedramp`) if your
63
+ users or rules need it.
64
+
65
+ 1. Create a database per deployed environment and put each printed `database_id` into
66
+ `wrangler.jsonc` (every D1 binding of an environment uses the same ID):
67
+
68
+ ```sh
69
+ pnpm exec wrangler d1 create react-hono-app-staging-db --location apac
70
+ pnpm exec wrangler d1 create react-hono-app-production-db --location apac
71
+ ```
72
+
73
+ 2. Set the custom domains in `env.staging.routes` / `env.production.routes` and the matching
74
+ `TRUSTED_ORIGINS`.
75
+ 3. Set every secret listed under `secrets.required` for each environment
76
+ (`pnpm exec wrangler secret put <NAME> --env staging`). `wrangler deploy` refuses to
77
+ deploy while one is missing.
78
+ <!-- inbis:wire readme.provisioning -->
79
+ 4. Apply migrations, then deploy:
80
+
81
+ ```sh
82
+ pnpm exec inbis-db migrate --remote --env staging
83
+ pnpm exec wrangler deploy --env staging
84
+ ```
85
+
86
+ The top level of `wrangler.jsonc` is local development; `env.staging` and `env.production`
87
+ are the deployed Workers (`react-hono-app-staging`, `react-hono-app-production`). Vars,
88
+ secrets and bindings are not inherited by environments, so each one lists its own.
89
+
90
+ ## CI and deploy
91
+
92
+ `.github/workflows/ci.yml` and `deploy.yml` call the platform's reusable workflows
93
+ (`awsmbiz/inbis-platform`, pinned to `ci-v1`; see its `docs/deployment.md`):
94
+
95
+ - **CI** on every pull request and push to `main`: install, typecheck, lint, tests, build,
96
+ Playwright + axe.
97
+ - **Deploy**: a push to `main` deploys staging. A `v*` tag on a commit that is on `main`,
98
+ pushed by someone listed in `production-actors`, deploys staging and then production.
99
+ Each environment runs a secrets check, then migrations (platform first, then the app),
100
+ then the deploy, then a smoke test. Production also takes a D1 Time Travel bookmark first.
101
+
102
+ To set it up:
103
+
104
+ 1. Ask a platform maintainer to give this repository read access to the `@awsmbiz`
105
+ packages.
106
+ 2. Add repository secrets `CLOUDFLARE_API_TOKEN_STAGING` and
107
+ `CLOUDFLARE_API_TOKEN_PRODUCTION`, one token per environment. Add
108
+ `CLOUDFLARE_ACCOUNT_ID` too if a token can see more than one account.
109
+ 3. In `deploy.yml`, fill in `production-actors` (the GitHub logins allowed to release) and
110
+ check `staging-url` / `production-url`.
111
+ 4. Release: `git tag v0.1.0 && git push origin v0.1.0`.
112
+
113
+ On GitHub Free, tags and `main` cannot be protected in a private repository. Anyone with
114
+ write access can push a tag or edit the allow-list, so keep write access to people you
115
+ trust with production.
@@ -0,0 +1,33 @@
1
+ import { expect, test, type Page } from "@playwright/test";
2
+ import { expectNoPageScroll, expectNoViolations, THEMES, VIEWPORTS } from "./axe.js";
3
+
4
+ // Zero axe violations on every page of the base app, in light and dark, at desktop and
5
+ // phone sizes. Modules add their own pages to their own specs.
6
+ const PAGES: { name: string; path: string; ready: (page: Page) => Promise<void> }[] = [
7
+ {
8
+ name: "home",
9
+ path: "/",
10
+ ready: (p) => expect(p.getByRole("status")).toHaveText("The API and database are ready."),
11
+ },
12
+ {
13
+ name: "not found",
14
+ path: "/no-such-page",
15
+ ready: (p) => expect(p.getByRole("heading", { name: "Page not found" })).toBeVisible(),
16
+ },
17
+ ];
18
+
19
+ for (const theme of THEMES) {
20
+ for (const [viewport, size] of Object.entries(VIEWPORTS)) {
21
+ for (const screen of PAGES) {
22
+ test(`${screen.name} (${theme}, ${viewport}) has no accessibility violations`, async ({ page }) => {
23
+ await page.emulateMedia({ colorScheme: theme });
24
+ await page.setViewportSize(size);
25
+ await page.goto(screen.path);
26
+ await screen.ready(page);
27
+ await expect(page.locator("html")).toHaveClass(theme === "dark" ? /\bdark\b/ : /^(?!.*\bdark\b)/);
28
+ await expectNoPageScroll(page);
29
+ await expectNoViolations(page);
30
+ });
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,47 @@
1
+ import { AxeBuilder } from "@axe-core/playwright";
2
+ import { expect, type Page } from "@playwright/test";
3
+
4
+ // axe-core checks (WCAG 2.x A and AA, 2.2 AA, best practice). A violation fails the test
5
+ // with the rule, the impact and the offending elements.
6
+ const TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"];
7
+
8
+ /** Desktop and phone viewports; every page is checked in both, in both themes. */
9
+ export const VIEWPORTS = {
10
+ desktop: { width: 1280, height: 800 },
11
+ phone: { width: 390, height: 844 },
12
+ } as const;
13
+ export const THEMES = ["light", "dark"] as const;
14
+
15
+ export async function expectNoViolations(page: Page) {
16
+ // Finish enter animations and colour transitions, so nothing is measured mid-fade.
17
+ // Endless ones (spinners, skeleton pulses) are left running.
18
+ await page.evaluate(async () => {
19
+ const nextFrame = () =>
20
+ new Promise((resolve) => {
21
+ requestAnimationFrame(resolve);
22
+ setTimeout(resolve, 100);
23
+ });
24
+ for (let quiet = 0, round = 0; quiet < 2 && round < 50; round++) {
25
+ const finite = document
26
+ .getAnimations()
27
+ .filter((a) => (a.playState === "running" || a.pending) && a.effect?.getComputedTiming().iterations !== Infinity);
28
+ for (const animation of finite) animation.finish();
29
+ quiet = finite.length === 0 ? quiet + 1 : 0;
30
+ await nextFrame();
31
+ }
32
+ });
33
+ const results = await new AxeBuilder({ page }).withTags(TAGS).analyze();
34
+ const summary = results.violations.map((v) => ({
35
+ rule: v.id,
36
+ impact: v.impact,
37
+ help: v.help,
38
+ targets: v.nodes.map((node) => `${node.target.join(" ")}: ${node.failureSummary ?? ""}`),
39
+ }));
40
+ expect(summary).toEqual([]);
41
+ }
42
+
43
+ /** No page-level horizontal scrolling. */
44
+ export async function expectNoPageScroll(page: Page) {
45
+ const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
46
+ expect(overflow, "page-level horizontal scroll (px)").toBeLessThanOrEqual(0);
47
+ }
@@ -0,0 +1,33 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("the home page shows the app and reports the API ready", async ({ page }) => {
4
+ await page.goto("/");
5
+ await expect(page.getByRole("heading", { level: 1, name: "Welcome to React Hono App" })).toBeVisible();
6
+ await expect(page.getByRole("status")).toHaveText("The API and database are ready.");
7
+ await expect(page).toHaveTitle("Home · React Hono App");
8
+ await expect(page.getByRole("navigation", { name: "Main" }).getByRole("link", { name: "Home" })).toHaveAttribute("aria-current", "page");
9
+ });
10
+
11
+ test("an unknown path shows the not-found page, with a way home that moves focus to the heading", async ({ page }) => {
12
+ await page.goto("/no-such-page");
13
+ await expect(page.getByRole("heading", { level: 1, name: "Page not found" })).toBeVisible();
14
+ await page.getByRole("link", { name: "Go to the home page" }).click();
15
+ await expect(page).toHaveURL("/");
16
+ await expect(page.getByRole("heading", { level: 1, name: "Welcome to React Hono App" })).toBeFocused();
17
+ });
18
+
19
+ test("the skip link is the first stop and moves to the main content", async ({ page }) => {
20
+ await page.goto("/");
21
+ await page.keyboard.press("Tab");
22
+ const skip = page.getByRole("link", { name: "Skip to content" });
23
+ await expect(skip).toBeFocused();
24
+ await expect(skip).toBeVisible();
25
+ await page.keyboard.press("Enter");
26
+ await expect(page).toHaveURL(/#main$/);
27
+ });
28
+
29
+ test("the API is served from the same Worker", async ({ request }) => {
30
+ expect((await request.get("/health")).status()).toBe(200);
31
+ const doc = (await (await request.get("/openapi.json")).json()) as { info: { title: string } };
32
+ expect(doc.info.title).toBe("React Hono App");
33
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ // Playwright specs run in Node; their page.evaluate callbacks run in the browser (DOM).
3
+ "extends": "@awsmbiz/inbis-tsconfig/react.json",
4
+ "compilerOptions": {
5
+ "types": ["node"]
6
+ },
7
+ "include": ["."]
8
+ }
@@ -0,0 +1,7 @@
1
+ import { inbis } from "@awsmbiz/inbis-eslint-config";
2
+
3
+ export default [
4
+ ...inbis({ tsconfigRootDir: import.meta.dirname }),
5
+ // modules/ and standalone/ are template sources, not part of this app (see template.json).
6
+ { ignores: ["web/dist/**", "playwright-report/**", "test-results/**", "modules/**", "standalone/**"] },
7
+ ];
@@ -0,0 +1,8 @@
1
+ -- The app's first migration (binding DB, history table d1_migrations).
2
+ -- Add your own tables in new numbered files; never edit a released one.
3
+ -- `pnpm db:migrate` applies platform sources first, then these.
4
+ CREATE TABLE `app_setting` (
5
+ `key` text PRIMARY KEY NOT NULL,
6
+ `value` text NOT NULL,
7
+ `updated_at` integer NOT NULL
8
+ );
@@ -0,0 +1,6 @@
1
+ # Template modules
2
+
3
+ One directory per optional module (`auth`, `admin`, `audit`, `access-control`, `example`),
4
+ each with a `module.json`. This directory is not part of the base app or of generated apps.
5
+ See `docs/template-modules.md` at the repository root for the manifest format, the wiring
6
+ points and how to build a combination.
@@ -0,0 +1,36 @@
1
+ import { createRoute, z, type OpenAPIHono } from "@hono/zod-openapi";
2
+ import { mountRoleAdmin } from "@awsmbiz/inbis-access-control/hono";
3
+ import type { ApiEnv } from "@awsmbiz/inbis-api";
4
+ import { requireAdmin, requireAuth, requireRecentAdminSignIn, sessionMiddleware } from "@awsmbiz/inbis-auth/hono";
5
+ import { accessControlOf, requirePermission } from "./access-control.js";
6
+ import { authResolver, type AuthDeps } from "./auth.js";
7
+
8
+ const ExampleSchema = z.object({ message: z.string() }).openapi("ProtectedExample");
9
+
10
+ const protectedExample = createRoute({
11
+ method: "get",
12
+ path: "/v1/example/protected",
13
+ tags: ["app"],
14
+ summary: "An example route that needs the example.read permission (replace it with your own)",
15
+ responses: {
16
+ 200: { description: "The caller holds example.read", content: { "application/json": { schema: ExampleSchema } } },
17
+ },
18
+ });
19
+
20
+ /**
21
+ * Access control on the app:
22
+ *
23
+ * - The role admin API under `/api/admin` (platform administrators; changes also need a
24
+ * recent sign-in): `/permissions`, `/roles`, `/users/:userId/roles` (see the
25
+ * `@awsmbiz/inbis-access-control` README).
26
+ * - `GET /v1/example/protected`, an example business route: signed in (`requireAuth()`)
27
+ * and holding `example.read` (`requirePermission`). `/v1` has no session of its own, so
28
+ * the route adds auth's session middleware first; `/api/*` routes already have it.
29
+ */
30
+ export function mountAccessControl(app: OpenAPIHono<ApiEnv<Env>>, auth: AuthDeps | undefined): void {
31
+ if (auth === undefined) throw new Error("createApp needs deps.auth (see src/index.ts).");
32
+ mountRoleAdmin(app, { accessControl: accessControlOf, guard: requireAdmin(), changeGuard: requireRecentAdminSignIn() });
33
+
34
+ app.use(protectedExample.path, sessionMiddleware(authResolver(auth)), requireAuth(), requirePermission("example.read"));
35
+ app.openapi(protectedExample, (c) => c.json({ message: "You hold the example.read permission." }, 200));
36
+ }
@@ -0,0 +1,46 @@
1
+ import { createAccessControl, defineAccessControl } from "@awsmbiz/inbis-access-control";
2
+ import { honoAccessControl } from "@awsmbiz/inbis-access-control/hono";
3
+ import type { ApiEnv } from "@awsmbiz/inbis-api";
4
+ import { requireBinding } from "@awsmbiz/inbis-cloudflare";
5
+ import { auditSinkFor } from "./audit-sink.js";
6
+
7
+ /**
8
+ * The app's business permissions and system roles (`@awsmbiz/inbis-access-control`).
9
+ * Permission names are dotted lowercase (`invoices.approve`) and typed: checking or
10
+ * granting an undeclared one is a compile error. The catalogue is seeded into D1 on an
11
+ * isolate's first permission check; system roles are read-only in the admin API, and
12
+ * administrators can add custom roles from these permissions.
13
+ *
14
+ * A stub: replace the example permission and role with your own.
15
+ */
16
+ export const APP_ACCESS = defineAccessControl({
17
+ permissions: {
18
+ "example.read": "Read the example resource (GET /v1/example/protected)",
19
+ // inbis:wire access.permissions
20
+ },
21
+ roles: {
22
+ viewer: { name: "Viewer", description: "Reads the example resource.", permissions: ["example.read"] },
23
+ // inbis:wire access.roles
24
+ },
25
+ });
26
+
27
+ /** A permission the app declares. */
28
+ export type AppPermission = (typeof APP_ACCESS.permissions)[number]["name"];
29
+
30
+ /**
31
+ * `requirePermission(permission)`: Hono middleware, `401 UNAUTHENTICATED` without a
32
+ * signed-in user and `403 FORBIDDEN` without the permission. Put it after auth's
33
+ * `requireAuth()` on a route that has the session (`/api/*` always does).
34
+ * `accessControlOf(c)`: the request's access control, for checks in code
35
+ * (`authorize`, `assignRole`, `rolesOf`, ...).
36
+ *
37
+ * Role changes are recorded in the app's audit trail when it has one (src/audit-sink.ts).
38
+ */
39
+ export const { requirePermission, accessControlOf } = honoAccessControl<AppPermission, ApiEnv<Env>>((c) => {
40
+ const audit = auditSinkFor(c.env, { logger: c.var.logger });
41
+ return createAccessControl(requireBinding(c.env, "ACCESS_CONTROL_DB"), APP_ACCESS, {
42
+ requestId: c.var.requestId,
43
+ logger: c.var.logger,
44
+ ...(audit === undefined ? {} : { audit }),
45
+ });
46
+ });