alchemy 0.37.2 → 0.38.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 (255) hide show
  1. package/bin/alchemy.mjs +44306 -48055
  2. package/bin/alchemy.ts +80 -85
  3. package/bin/commands/create.ts +263 -0
  4. package/bin/constants.ts +13 -0
  5. package/bin/errors.ts +11 -0
  6. package/bin/services/dependencies.ts +47 -0
  7. package/bin/services/get-package-version.ts +11 -0
  8. package/bin/services/package-manager.ts +113 -0
  9. package/bin/services/template-manager.ts +137 -0
  10. package/bin/types.ts +83 -0
  11. package/lib/alchemy.d.ts.map +1 -1
  12. package/lib/alchemy.js +12 -3
  13. package/lib/alchemy.js.map +1 -1
  14. package/lib/apply.d.ts +2 -0
  15. package/lib/apply.d.ts.map +1 -1
  16. package/lib/apply.js +47 -12
  17. package/lib/apply.js.map +1 -1
  18. package/lib/build-date.d.ts +6 -0
  19. package/lib/build-date.d.ts.map +1 -0
  20. package/lib/build-date.js +8 -0
  21. package/lib/build-date.js.map +1 -0
  22. package/lib/cloudflare/api.d.ts.map +1 -1
  23. package/lib/cloudflare/api.js +2 -1
  24. package/lib/cloudflare/api.js.map +1 -1
  25. package/lib/cloudflare/pipeline.d.ts +1 -1
  26. package/lib/cloudflare/pipeline.d.ts.map +1 -1
  27. package/lib/cloudflare/pipeline.js +6 -1
  28. package/lib/cloudflare/pipeline.js.map +1 -1
  29. package/lib/cloudflare/worker-stub.d.ts +23 -2
  30. package/lib/cloudflare/worker-stub.d.ts.map +1 -1
  31. package/lib/cloudflare/worker-stub.js +16 -2
  32. package/lib/cloudflare/worker-stub.js.map +1 -1
  33. package/lib/cloudflare/worker.d.ts +4 -3
  34. package/lib/cloudflare/worker.d.ts.map +1 -1
  35. package/lib/cloudflare/worker.js +2 -1
  36. package/lib/cloudflare/worker.js.map +1 -1
  37. package/lib/context.d.ts +2 -2
  38. package/lib/context.d.ts.map +1 -1
  39. package/lib/destroy.d.ts +8 -2
  40. package/lib/destroy.d.ts.map +1 -1
  41. package/lib/destroy.js +19 -5
  42. package/lib/destroy.js.map +1 -1
  43. package/lib/scope.d.ts +26 -4
  44. package/lib/scope.d.ts.map +1 -1
  45. package/lib/scope.js +104 -9
  46. package/lib/scope.js.map +1 -1
  47. package/lib/sentry/project.js +1 -1
  48. package/lib/sentry/project.js.map +1 -1
  49. package/lib/util/logger.js +1 -1
  50. package/lib/util/logger.js.map +1 -1
  51. package/lib/util/mutex.d.ts +9 -0
  52. package/lib/util/mutex.d.ts.map +1 -0
  53. package/lib/util/mutex.js +35 -0
  54. package/lib/util/mutex.js.map +1 -0
  55. package/package.json +16 -6
  56. package/src/alchemy.ts +17 -5
  57. package/src/apply.ts +67 -17
  58. package/src/build-date.ts +8 -0
  59. package/src/cloudflare/api.ts +2 -1
  60. package/src/cloudflare/pipeline.ts +6 -0
  61. package/src/cloudflare/worker-stub.ts +31 -2
  62. package/src/cloudflare/worker.ts +4 -3
  63. package/src/context.ts +2 -2
  64. package/src/destroy.ts +30 -7
  65. package/src/scope.ts +145 -10
  66. package/src/sentry/project.ts +1 -1
  67. package/src/util/logger.ts +1 -1
  68. package/src/util/mutex.ts +33 -0
  69. package/templates/astro/.vscode/extensions.json +4 -0
  70. package/templates/astro/.vscode/launch.json +11 -0
  71. package/templates/astro/README.md +48 -0
  72. package/templates/astro/_env.example +1 -0
  73. package/templates/astro/_gitignore +27 -0
  74. package/templates/astro/alchemy.run.ts +16 -0
  75. package/templates/astro/astro.config.mjs +8 -0
  76. package/templates/astro/package.json +23 -0
  77. package/templates/astro/public/favicon.svg +9 -0
  78. package/templates/astro/src/assets/astro.svg +1 -0
  79. package/templates/astro/src/assets/background.svg +1 -0
  80. package/templates/astro/src/components/Welcome.astro +210 -0
  81. package/templates/astro/src/layouts/Layout.astro +22 -0
  82. package/templates/astro/src/pages/api/hello.ts +19 -0
  83. package/templates/astro/src/pages/index.astro +11 -0
  84. package/templates/astro/tsconfig.json +18 -0
  85. package/templates/astro/types/env.d.ts +16 -0
  86. package/templates/astro/wrangler.jsonc +6 -0
  87. package/templates/nuxt/README.md +75 -0
  88. package/templates/nuxt/_env +1 -0
  89. package/templates/nuxt/_env.example +1 -0
  90. package/templates/nuxt/_gitignore +27 -0
  91. package/templates/nuxt/alchemy.run.ts +16 -0
  92. package/templates/nuxt/app.vue +6 -0
  93. package/templates/nuxt/nuxt.config.ts +13 -0
  94. package/templates/nuxt/package.json +26 -0
  95. package/templates/nuxt/public/favicon.ico +0 -0
  96. package/templates/nuxt/public/robots.txt +2 -0
  97. package/templates/nuxt/server/api/hello.ts +7 -0
  98. package/templates/nuxt/server/middleware/auth.ts +5 -0
  99. package/templates/nuxt/server/middleware/hello.ts +5 -0
  100. package/templates/nuxt/server/tsconfig.json +3 -0
  101. package/templates/nuxt/tsconfig.json +15 -0
  102. package/templates/nuxt/types/env.d.ts +16 -0
  103. package/templates/react-router/.vscode/settings.json +5 -0
  104. package/templates/react-router/README.md +79 -0
  105. package/templates/react-router/_env +1 -0
  106. package/templates/react-router/_env.example +1 -0
  107. package/templates/react-router/_gitignore +15 -0
  108. package/templates/react-router/alchemy.run.ts +17 -0
  109. package/templates/react-router/app/app.css +15 -0
  110. package/templates/react-router/app/entry.server.tsx +43 -0
  111. package/templates/react-router/app/root.tsx +75 -0
  112. package/templates/react-router/app/routes/home.tsx +17 -0
  113. package/templates/react-router/app/routes.ts +3 -0
  114. package/templates/react-router/app/welcome/logo-dark.svg +23 -0
  115. package/templates/react-router/app/welcome/logo-light.svg +23 -0
  116. package/templates/react-router/app/welcome/welcome.tsx +90 -0
  117. package/templates/react-router/package.json +34 -0
  118. package/templates/react-router/public/favicon.ico +0 -0
  119. package/templates/react-router/react-router.config.ts +8 -0
  120. package/templates/react-router/tsconfig.cloudflare.json +27 -0
  121. package/templates/react-router/tsconfig.json +17 -0
  122. package/templates/react-router/tsconfig.node.json +20 -0
  123. package/templates/react-router/types/env.d.ts +16 -0
  124. package/templates/react-router/vite.config.ts +14 -0
  125. package/templates/react-router/workers/app.ts +23 -0
  126. package/templates/react-router/wrangler.jsonc +6 -0
  127. package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-interruptors.mdc +247 -0
  128. package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-middleware.mdc +75 -0
  129. package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-react.mdc +110 -0
  130. package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-request-response.mdc +213 -0
  131. package/templates/rwsdk/.devcontainer/Dockerfile +8 -0
  132. package/templates/rwsdk/.devcontainer/devcontainer.json +21 -0
  133. package/templates/rwsdk/README.md +62 -0
  134. package/templates/rwsdk/_env +5 -0
  135. package/templates/rwsdk/_env.example +5 -0
  136. package/templates/rwsdk/_gitignore +71 -0
  137. package/templates/rwsdk/alchemy.run.ts +29 -0
  138. package/templates/rwsdk/package.json +61 -0
  139. package/templates/rwsdk/prisma/schema.prisma +43 -0
  140. package/templates/rwsdk/src/app/Document.tsx +16 -0
  141. package/templates/rwsdk/src/app/headers.ts +32 -0
  142. package/templates/rwsdk/src/app/pages/Home.tsx +13 -0
  143. package/templates/rwsdk/src/app/pages/user/Login.tsx +79 -0
  144. package/templates/rwsdk/src/app/pages/user/functions.ts +171 -0
  145. package/templates/rwsdk/src/app/pages/user/routes.ts +17 -0
  146. package/templates/rwsdk/src/app/shared/links.ts +3 -0
  147. package/templates/rwsdk/src/client.tsx +3 -0
  148. package/templates/rwsdk/src/db.ts +26 -0
  149. package/templates/rwsdk/src/scripts/seed.ts +21 -0
  150. package/templates/rwsdk/src/session/durableObject.ts +63 -0
  151. package/templates/rwsdk/src/session/store.ts +13 -0
  152. package/templates/rwsdk/src/worker.tsx +63 -0
  153. package/templates/rwsdk/tsconfig.json +47 -0
  154. package/templates/rwsdk/types/env.d.ts +16 -0
  155. package/templates/rwsdk/types/rw.d.ts +5 -0
  156. package/templates/rwsdk/types/vite.d.ts +4 -0
  157. package/templates/rwsdk/vite.config.mts +6 -0
  158. package/templates/rwsdk/wrangler.jsonc +28 -0
  159. package/templates/sveltekit/README.md +38 -0
  160. package/templates/sveltekit/_env +1 -0
  161. package/templates/sveltekit/_env.example +1 -0
  162. package/templates/sveltekit/_gitignore +26 -0
  163. package/templates/sveltekit/_npmrc +1 -0
  164. package/templates/sveltekit/_prettierrc +3 -0
  165. package/templates/sveltekit/alchemy.run.ts +16 -0
  166. package/templates/sveltekit/bun.lock +1099 -0
  167. package/templates/sveltekit/package.json +33 -0
  168. package/templates/sveltekit/src/app.css +1 -0
  169. package/templates/sveltekit/src/app.d.ts +13 -0
  170. package/templates/sveltekit/src/app.html +12 -0
  171. package/templates/sveltekit/src/routes/+layout.svelte +7 -0
  172. package/templates/sveltekit/src/routes/+page.svelte +2 -0
  173. package/templates/sveltekit/static/favicon.png +0 -0
  174. package/templates/sveltekit/svelte.config.js +12 -0
  175. package/templates/sveltekit/tsconfig.json +27 -0
  176. package/templates/sveltekit/types/env.d.ts +16 -0
  177. package/templates/sveltekit/vite.config.ts +6 -0
  178. package/templates/sveltekit/wrangler.jsonc +7 -0
  179. package/templates/tanstack-start/.vscode/settings.json +11 -0
  180. package/templates/tanstack-start/README.md +72 -0
  181. package/templates/tanstack-start/_env +1 -0
  182. package/templates/tanstack-start/_env.example +1 -0
  183. package/templates/tanstack-start/_gitignore +22 -0
  184. package/templates/tanstack-start/_prettierignore +4 -0
  185. package/templates/tanstack-start/alchemy.run.ts +16 -0
  186. package/templates/tanstack-start/package.json +37 -0
  187. package/templates/tanstack-start/public/android-chrome-192x192.png +0 -0
  188. package/templates/tanstack-start/public/android-chrome-512x512.png +0 -0
  189. package/templates/tanstack-start/public/apple-touch-icon.png +0 -0
  190. package/templates/tanstack-start/public/favicon-16x16.png +0 -0
  191. package/templates/tanstack-start/public/favicon-32x32.png +0 -0
  192. package/templates/tanstack-start/public/favicon.ico +0 -0
  193. package/templates/tanstack-start/public/favicon.png +0 -0
  194. package/templates/tanstack-start/public/site.webmanifest +19 -0
  195. package/templates/tanstack-start/src/components/DefaultCatchBoundary.tsx +53 -0
  196. package/templates/tanstack-start/src/components/NotFound.tsx +25 -0
  197. package/templates/tanstack-start/src/components/PostError.tsx +5 -0
  198. package/templates/tanstack-start/src/components/UserError.tsx +5 -0
  199. package/templates/tanstack-start/src/router.tsx +22 -0
  200. package/templates/tanstack-start/src/routes/__root.tsx +146 -0
  201. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-a.tsx +10 -0
  202. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-b.tsx +10 -0
  203. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout.tsx +34 -0
  204. package/templates/tanstack-start/src/routes/_pathlessLayout.tsx +16 -0
  205. package/templates/tanstack-start/src/routes/api/users.$userId.ts +28 -0
  206. package/templates/tanstack-start/src/routes/api/users.ts +59 -0
  207. package/templates/tanstack-start/src/routes/customScript[.]js.ts +10 -0
  208. package/templates/tanstack-start/src/routes/deferred.tsx +62 -0
  209. package/templates/tanstack-start/src/routes/index.tsx +12 -0
  210. package/templates/tanstack-start/src/routes/posts.$postId.tsx +34 -0
  211. package/templates/tanstack-start/src/routes/posts.index.tsx +8 -0
  212. package/templates/tanstack-start/src/routes/posts.tsx +38 -0
  213. package/templates/tanstack-start/src/routes/posts_.$postId.deep.tsx +29 -0
  214. package/templates/tanstack-start/src/routes/redirect.tsx +9 -0
  215. package/templates/tanstack-start/src/routes/users.$userId.tsx +44 -0
  216. package/templates/tanstack-start/src/routes/users.index.tsx +18 -0
  217. package/templates/tanstack-start/src/routes/users.tsx +49 -0
  218. package/templates/tanstack-start/src/styles/app.css +12 -0
  219. package/templates/tanstack-start/src/utils/loggingMiddleware.tsx +41 -0
  220. package/templates/tanstack-start/src/utils/posts.tsx +40 -0
  221. package/templates/tanstack-start/src/utils/seo.ts +33 -0
  222. package/templates/tanstack-start/src/utils/users.tsx +5 -0
  223. package/templates/tanstack-start/tsconfig.json +31 -0
  224. package/templates/tanstack-start/types/env.d.ts +16 -0
  225. package/templates/tanstack-start/vite.config.ts +33 -0
  226. package/templates/tanstack-start/wrangler.jsonc +7 -0
  227. package/templates/typescript/README.md +15 -0
  228. package/templates/typescript/_gitignore +37 -0
  229. package/templates/typescript/alchemy.run.ts +15 -0
  230. package/templates/typescript/index.ts +1 -0
  231. package/templates/typescript/package.json +18 -0
  232. package/templates/typescript/src/worker.js +5 -0
  233. package/templates/typescript/src/worker.ts +7 -0
  234. package/templates/typescript/tsconfig.json +21 -0
  235. package/templates/typescript/types/env.d.ts +16 -0
  236. package/templates/vite/README.md +54 -0
  237. package/templates/vite/_env.example +1 -0
  238. package/templates/vite/_gitignore +27 -0
  239. package/templates/vite/alchemy.run.ts +17 -0
  240. package/templates/vite/eslint.config.js +28 -0
  241. package/templates/vite/index.html +13 -0
  242. package/templates/vite/package.json +35 -0
  243. package/templates/vite/public/vite.svg +1 -0
  244. package/templates/vite/src/App.css +42 -0
  245. package/templates/vite/src/App.tsx +35 -0
  246. package/templates/vite/src/assets/react.svg +1 -0
  247. package/templates/vite/src/index.css +68 -0
  248. package/templates/vite/src/main.tsx +10 -0
  249. package/templates/vite/src/vite-env.d.ts +1 -0
  250. package/templates/vite/tsconfig.json +34 -0
  251. package/templates/vite/types/env.d.ts +16 -0
  252. package/templates/vite/vite.config.ts +9 -0
  253. package/templates/vite/worker/index.ts +12 -0
  254. package/templates/vite/wrangler.jsonc +6 -0
  255. package/bin/create-alchemy.ts +0 -1220
@@ -0,0 +1,62 @@
1
+ # Standard RedwoodSDK Starter
2
+
3
+ This "standard starter" is the recommended implementation for RedwoodSDK. You get a Typescript project with:
4
+
5
+ - Vite
6
+ - database (Prisma via D1)
7
+ - Session Management (via DurableObjects)
8
+ - Passkey authentication (Webauthn)
9
+ - Storage (via R2)
10
+
11
+ ## Creating your project
12
+
13
+ ```shell
14
+ npx create-rwsdk my-project-name
15
+ cd my-project-name
16
+ pnpm install
17
+ ```
18
+
19
+ ## Running the dev server
20
+
21
+ ```shell
22
+ pnpm dev
23
+ ```
24
+
25
+ Point your browser to the URL displayed in the terminal (e.g. `http://localhost:5173/`). You should see a "Hello World" message in your browser.
26
+
27
+ ## Deploying your app
28
+
29
+ ### Wrangler Setup
30
+
31
+ Within your project's `wrangler.jsonc`:
32
+
33
+ - Replace the `__change_me__` placeholders with a name for your application
34
+
35
+ - Create a new D1 database:
36
+
37
+ ```shell
38
+ npx wrangler d1 create my-project-db
39
+ ```
40
+
41
+ Copy the database ID provided and paste it into your project's `wrangler.jsonc` file:
42
+
43
+ ```jsonc
44
+ {
45
+ "d1_databases": [
46
+ {
47
+ "binding": "DB",
48
+ "database_name": "my-project-db",
49
+ "database_id": "your-database-id",
50
+ },
51
+ ],
52
+ }
53
+ ```
54
+
55
+ ### Authentication Setup
56
+
57
+ For authentication setup and configuration, including optional bot protection, see the [Authentication Documentation](https://docs.rwsdk.com/core/authentication).
58
+
59
+ ## Further Reading
60
+
61
+ - [RedwoodSDK Documentation](https://docs.rwsdk.com/)
62
+ - [Cloudflare Workers Secrets](https://developers.cloudflare.com/workers/runtime-apis/secrets/)
@@ -0,0 +1,5 @@
1
+ WEBAUTHN_RP_ID=localhost
2
+ AUTH_SECRET_KEY=your-development-secret-key
3
+ # Optional: Enable Turnstile bot protection
4
+ # TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
5
+ ALCHEMY_PASSWORD=change-me
@@ -0,0 +1,5 @@
1
+ WEBAUTHN_RP_ID=localhost
2
+ AUTH_SECRET_KEY=your-development-secret-key
3
+ # Optional: Enable Turnstile bot protection
4
+ # TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
5
+ ALCHEMY_PASSWORD=change-me
@@ -0,0 +1,71 @@
1
+ # Node modules
2
+ node_modules
3
+
4
+ # Logs
5
+ logs
6
+ *.log
7
+ npm-debug.log*
8
+ pnpm-debug.log*
9
+
10
+ # Environment variables
11
+ .env
12
+ .dev.vars
13
+
14
+ # Vite build output
15
+ dist
16
+
17
+ # TypeScript
18
+ *.tsbuildinfo
19
+
20
+ # IDEs and editors
21
+ .vscode/
22
+ .idea/
23
+ *.suo
24
+ *.ntvs*
25
+ *.njsproj
26
+ *.sln
27
+ *.sw?
28
+
29
+ # MacOS
30
+ .DS_Store
31
+
32
+ # Optional npm cache directory
33
+ .npm
34
+
35
+ # Optional eslint cache
36
+ .eslintcache
37
+
38
+ # Optional stylelint cache
39
+ .stylelintcache
40
+
41
+ # Optional REPL history
42
+ .node_repl_history
43
+
44
+ # Output of 'npm pack'
45
+ *.tgz
46
+
47
+ # pnpm store directory
48
+ .pnpm-store
49
+
50
+ # dotenv environment variables file
51
+ .env.local
52
+ .env.development.local
53
+ .env.test.local
54
+ .env.production.local
55
+
56
+ # Vite cache
57
+ .vite
58
+
59
+ # Coverage directory used by tools like istanbul
60
+ coverage
61
+
62
+ # Temporary files
63
+ *.tmp
64
+ *.temp
65
+
66
+ .wrangler
67
+
68
+ generated/
69
+
70
+ .alchemy/
71
+ .env
@@ -0,0 +1,29 @@
1
+ /// <reference types="@types/node" />
2
+ import alchemy from "alchemy";
3
+ import { D1Database, DurableObjectNamespace, Redwood } from "alchemy/cloudflare";
4
+
5
+ const app = await alchemy("my-alchemy-app");
6
+
7
+ const database = await D1Database("database", {
8
+ name: "my-alchemy-app-db",
9
+ migrationsDir: "migrations",
10
+ });
11
+
12
+ export const worker = await Redwood("website", {
13
+ name: "my-alchemy-app-website",
14
+ command: "bun run build",
15
+ bindings: {
16
+ AUTH_SECRET_KEY: alchemy.secret(process.env.AUTH_SECRET_KEY),
17
+ DB: database,
18
+ SESSION_DURABLE_OBJECT: new DurableObjectNamespace("session", {
19
+ className: "SessionDurableObject",
20
+ }),
21
+ },
22
+ });
23
+
24
+ console.log({
25
+ url: worker.url,
26
+ });
27
+
28
+ await app.finalize();
29
+
@@ -0,0 +1,61 @@
1
+ {
2
+ "type": "module",
3
+ "name": "rwsdk",
4
+ "version": "1.0.0",
5
+ "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime. ",
6
+ "main": "index.js",
7
+ "keywords": [],
8
+ "author": "",
9
+ "license": "MIT",
10
+ "private": true,
11
+ "scripts": {
12
+ "build": "vite build",
13
+ "check": "npm run generate && npm run types",
14
+ "clean": "npm run clean:vite",
15
+ "clean:vite": "rm -rf ./node_modules/.vite",
16
+ "deploy": "tsx --env-file .env ./alchemy.run.ts",
17
+ "destroy": "tsx --env-file .env ./alchemy.run.ts --destroy",
18
+ "dev": "NODE_ENV=${NODE_ENV:-development} vite dev",
19
+ "dev:init": "rw-scripts dev-init",
20
+ "generate": "rw-scripts ensure-env && prisma generate && wrangler types",
21
+ "migrate:dev": "prisma generate && wrangler d1 migrations apply DB --local",
22
+ "migrate:new": "rw-scripts migrate-new",
23
+ "migrate:prd": "wrangler d1 migrations apply DB --remote",
24
+ "prepare": "vibe-rules install cursor",
25
+ "preview": "vite preview",
26
+ "release": "rw-scripts ensure-deploy-env && npm run clean && prisma generate && RWSDK_DEPLOY=1 npm run build && wrangler deploy",
27
+ "seed": "npm run worker:run ./src/scripts/seed.ts",
28
+ "types": "tsc",
29
+ "worker:run": "rw-scripts worker-run"
30
+ },
31
+ "dependencies": {
32
+ "@prisma/adapter-d1": "~6.8.2",
33
+ "@prisma/client": "~6.8.2",
34
+ "@simplewebauthn/browser": "^13.1.0",
35
+ "@simplewebauthn/server": "^13.1.1",
36
+ "rwsdk": "0.0.89"
37
+ },
38
+ "devDependencies": {
39
+ "@cloudflare/workers-types": "^4.20250620.0",
40
+ "miniflare": "^4.20250617.3",
41
+ "@types/node": "^22.14.0",
42
+ "@types/react": "^19.1.2",
43
+ "@types/react-dom": "^19.1.2",
44
+ "prisma": "~6.8.2",
45
+ "typescript": "^5.8.3",
46
+ "vibe-rules": "^0.2.31",
47
+ "alchemy": "workspace:*",
48
+ "vite": "^6.2.6",
49
+ "wrangler": "^4.16.0"
50
+ },
51
+ "pnpm": {
52
+ "onlyBuiltDependencies": [
53
+ "@prisma/client",
54
+ "@prisma/engines",
55
+ "esbuild",
56
+ "prisma",
57
+ "sharp",
58
+ "workerd"
59
+ ]
60
+ }
61
+ }
@@ -0,0 +1,43 @@
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5
+ // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6
+
7
+ generator client {
8
+ provider = "prisma-client"
9
+
10
+ runtime = "workerd"
11
+ moduleFormat = "esm"
12
+ generatedFileExtension = "ts"
13
+ importFileExtension = "ts"
14
+
15
+ output = "../generated/prisma"
16
+ previewFeatures = ["queryCompiler", "driverAdapters"]
17
+ }
18
+
19
+ datasource db {
20
+ provider = "sqlite"
21
+ url = env("DATABASE_URL")
22
+ }
23
+
24
+ model User {
25
+ id String @id @default(uuid()) // User ID (UUID-based)
26
+ username String @unique
27
+ createdAt DateTime @default(now())
28
+
29
+ credentials Credential[] // Relationship: One user can have many credentials
30
+ }
31
+
32
+ model Credential {
33
+ id String @id @default(uuid()) // Internal DB ID
34
+ userId String @unique // Every credential is linked to a specific user
35
+ user User @relation(fields: [userId], references: [id])
36
+ createdAt DateTime @default(now())
37
+ credentialId String @unique // WebAuthn credential identifier
38
+ publicKey Bytes
39
+ counter Int @default(0)
40
+
41
+ @@index([credentialId])
42
+ @@index([userId])
43
+ }
@@ -0,0 +1,16 @@
1
+ export const Document: React.FC<{ children: React.ReactNode }> = ({
2
+ children,
3
+ }) => (
4
+ <html lang="en">
5
+ <head>
6
+ <meta charSet="utf-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <title>@redwoodjs/starter-standard</title>
9
+ <link rel="modulepreload" href="/src/client.tsx" />
10
+ </head>
11
+ <body>
12
+ <div id="root">{children}</div>
13
+ <script>import("/src/client.tsx")</script>
14
+ </body>
15
+ </html>
16
+ );
@@ -0,0 +1,32 @@
1
+ import { RouteMiddleware } from "rwsdk/router";
2
+ import { IS_DEV } from "rwsdk/constants";
3
+
4
+ export const setCommonHeaders =
5
+ (): RouteMiddleware =>
6
+ ({ headers, rw: { nonce } }) => {
7
+ if (!IS_DEV) {
8
+ // Forces browsers to always use HTTPS for a specified time period (2 years)
9
+ headers.set(
10
+ "Strict-Transport-Security",
11
+ "max-age=63072000; includeSubDomains; preload",
12
+ );
13
+ }
14
+
15
+ // Forces browser to use the declared content-type instead of trying to guess/sniff it
16
+ headers.set("X-Content-Type-Options", "nosniff");
17
+
18
+ // Stops browsers from sending the referring webpage URL in HTTP headers
19
+ headers.set("Referrer-Policy", "no-referrer");
20
+
21
+ // Explicitly disables access to specific browser features/APIs
22
+ headers.set(
23
+ "Permissions-Policy",
24
+ "geolocation=(), microphone=(), camera=()",
25
+ );
26
+
27
+ // Defines trusted sources for content loading and script execution:
28
+ headers.set(
29
+ "Content-Security-Policy",
30
+ `default-src 'self'; script-src 'self' 'nonce-${nonce}' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; object-src 'none';`,
31
+ );
32
+ };
@@ -0,0 +1,13 @@
1
+ import { RequestInfo } from "rwsdk/worker";
2
+
3
+ export function Home({ ctx }: RequestInfo) {
4
+ return (
5
+ <div>
6
+ <p>
7
+ {ctx.user?.username
8
+ ? `You are logged in as user ${ctx.user.username}`
9
+ : "You are not logged in"}
10
+ </p>
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+ import {
5
+ startAuthentication,
6
+ startRegistration,
7
+ } from "@simplewebauthn/browser";
8
+ import {
9
+ finishPasskeyLogin,
10
+ finishPasskeyRegistration,
11
+ startPasskeyLogin,
12
+ startPasskeyRegistration,
13
+ } from "./functions";
14
+
15
+ export function Login() {
16
+ const [username, setUsername] = useState("");
17
+ const [result, setResult] = useState("");
18
+ const [isPending, startTransition] = useTransition();
19
+
20
+ const passkeyLogin = async () => {
21
+ // 1. Get a challenge from the worker
22
+ const options = await startPasskeyLogin();
23
+
24
+ // 2. Ask the browser to sign the challenge
25
+ const login = await startAuthentication({ optionsJSON: options });
26
+
27
+ // 3. Give the signed challenge to the worker to finish the login process
28
+ const success = await finishPasskeyLogin(login);
29
+
30
+ if (!success) {
31
+ setResult("Login failed");
32
+ } else {
33
+ setResult("Login successful!");
34
+ }
35
+ };
36
+
37
+ const passkeyRegister = async () => {
38
+ // 1. Get a challenge from the worker
39
+ const options = await startPasskeyRegistration(username);
40
+
41
+ // 2. Ask the browser to sign the challenge
42
+ const registration = await startRegistration({ optionsJSON: options });
43
+
44
+ // 3. Give the signed challenge to the worker to finish the registration process
45
+ const success = await finishPasskeyRegistration(username, registration);
46
+
47
+ if (!success) {
48
+ setResult("Registration failed");
49
+ } else {
50
+ setResult("Registration successful!");
51
+ }
52
+ };
53
+
54
+ const handlePerformPasskeyLogin = () => {
55
+ startTransition(() => void passkeyLogin());
56
+ };
57
+
58
+ const handlePerformPasskeyRegister = () => {
59
+ startTransition(() => void passkeyRegister());
60
+ };
61
+
62
+ return (
63
+ <>
64
+ <input
65
+ type="text"
66
+ value={username}
67
+ onChange={(e) => setUsername(e.target.value)}
68
+ placeholder="Username"
69
+ />
70
+ <button onClick={handlePerformPasskeyLogin} disabled={isPending}>
71
+ {isPending ? <>...</> : "Login with passkey"}
72
+ </button>
73
+ <button onClick={handlePerformPasskeyRegister} disabled={isPending}>
74
+ {isPending ? <>...</> : "Register with passkey"}
75
+ </button>
76
+ {result && <div>{result}</div>}
77
+ </>
78
+ );
79
+ }
@@ -0,0 +1,171 @@
1
+ "use server";
2
+ import {
3
+ generateRegistrationOptions,
4
+ generateAuthenticationOptions,
5
+ verifyRegistrationResponse,
6
+ verifyAuthenticationResponse,
7
+ RegistrationResponseJSON,
8
+ AuthenticationResponseJSON,
9
+ } from "@simplewebauthn/server";
10
+
11
+ import { sessions } from "@/session/store";
12
+ import { requestInfo } from "rwsdk/worker";
13
+ import { db } from "@/db";
14
+ import { env } from "cloudflare:workers";
15
+
16
+ const IS_DEV = process.env.NODE_ENV === "development";
17
+
18
+ function getWebAuthnConfig(request: Request) {
19
+ const rpID = env.WEBAUTHN_RP_ID ?? new URL(request.url).hostname;
20
+ const rpName = IS_DEV ? "Development App" : env.WEBAUTHN_APP_NAME;
21
+ return {
22
+ rpName,
23
+ rpID,
24
+ };
25
+ }
26
+
27
+ export async function startPasskeyRegistration(username: string) {
28
+ const { rpName, rpID } = getWebAuthnConfig(requestInfo.request);
29
+ const { headers } = requestInfo;
30
+
31
+ const options = await generateRegistrationOptions({
32
+ rpName,
33
+ rpID,
34
+ userName: username,
35
+ authenticatorSelection: {
36
+ // Require the authenticator to store the credential, enabling a username-less login experience
37
+ residentKey: "required",
38
+ // Prefer user verification (biometric, PIN, etc.), but allow authentication even if it's not available
39
+ userVerification: "preferred",
40
+ },
41
+ });
42
+
43
+ await sessions.save(headers, { challenge: options.challenge });
44
+
45
+ return options;
46
+ }
47
+
48
+ export async function startPasskeyLogin() {
49
+ const { rpID } = getWebAuthnConfig(requestInfo.request);
50
+ const { headers } = requestInfo;
51
+
52
+ const options = await generateAuthenticationOptions({
53
+ rpID,
54
+ userVerification: "preferred",
55
+ allowCredentials: [],
56
+ });
57
+
58
+ await sessions.save(headers, { challenge: options.challenge });
59
+
60
+ return options;
61
+ }
62
+
63
+ export async function finishPasskeyRegistration(
64
+ username: string,
65
+ registration: RegistrationResponseJSON,
66
+ ) {
67
+ const { request, headers } = requestInfo;
68
+ const { origin } = new URL(request.url);
69
+
70
+ const session = await sessions.load(request);
71
+ const challenge = session?.challenge;
72
+
73
+ if (!challenge) {
74
+ return false;
75
+ }
76
+
77
+ const verification = await verifyRegistrationResponse({
78
+ response: registration,
79
+ expectedChallenge: challenge,
80
+ expectedOrigin: origin,
81
+ expectedRPID: env.WEBAUTHN_RP_ID || new URL(request.url).hostname,
82
+ });
83
+
84
+ if (!verification.verified || !verification.registrationInfo) {
85
+ return false;
86
+ }
87
+
88
+ await sessions.save(headers, { challenge: null });
89
+
90
+ const user = await db.user.create({
91
+ data: {
92
+ username,
93
+ },
94
+ });
95
+
96
+ await db.credential.create({
97
+ data: {
98
+ userId: user.id,
99
+ credentialId: verification.registrationInfo.credential.id,
100
+ publicKey: verification.registrationInfo.credential.publicKey,
101
+ counter: verification.registrationInfo.credential.counter,
102
+ },
103
+ });
104
+
105
+ return true;
106
+ }
107
+
108
+ export async function finishPasskeyLogin(login: AuthenticationResponseJSON) {
109
+ const { request, headers } = requestInfo;
110
+ const { origin } = new URL(request.url);
111
+
112
+ const session = await sessions.load(request);
113
+ const challenge = session?.challenge;
114
+
115
+ if (!challenge) {
116
+ return false;
117
+ }
118
+
119
+ const credential = await db.credential.findUnique({
120
+ where: {
121
+ credentialId: login.id,
122
+ },
123
+ });
124
+
125
+ if (!credential) {
126
+ return false;
127
+ }
128
+
129
+ const verification = await verifyAuthenticationResponse({
130
+ response: login,
131
+ expectedChallenge: challenge,
132
+ expectedOrigin: origin,
133
+ expectedRPID: env.WEBAUTHN_RP_ID || new URL(request.url).hostname,
134
+ requireUserVerification: false,
135
+ credential: {
136
+ id: credential.credentialId,
137
+ publicKey: credential.publicKey,
138
+ counter: credential.counter,
139
+ },
140
+ });
141
+
142
+ if (!verification.verified) {
143
+ return false;
144
+ }
145
+
146
+ await db.credential.update({
147
+ where: {
148
+ credentialId: login.id,
149
+ },
150
+ data: {
151
+ counter: verification.authenticationInfo.newCounter,
152
+ },
153
+ });
154
+
155
+ const user = await db.user.findUnique({
156
+ where: {
157
+ id: credential.userId,
158
+ },
159
+ });
160
+
161
+ if (!user) {
162
+ return false;
163
+ }
164
+
165
+ await sessions.save(headers, {
166
+ userId: user.id,
167
+ challenge: null,
168
+ });
169
+
170
+ return true;
171
+ }
@@ -0,0 +1,17 @@
1
+ import { route } from "rwsdk/router";
2
+ import { Login } from "./Login";
3
+ import { sessions } from "@/session/store";
4
+
5
+ export const userRoutes = [
6
+ route("/login", [Login]),
7
+ route("/logout", async function ({ request }) {
8
+ const headers = new Headers();
9
+ await sessions.remove(request, headers);
10
+ headers.set("Location", "/");
11
+
12
+ return new Response(null, {
13
+ status: 302,
14
+ headers,
15
+ });
16
+ }),
17
+ ];
@@ -0,0 +1,3 @@
1
+ import { defineLinks } from "rwsdk/router";
2
+
3
+ export const link = defineLinks(["/", "/user/login", "/user/logout"]);
@@ -0,0 +1,3 @@
1
+ import { initClient } from "rwsdk/client";
2
+
3
+ initClient();
@@ -0,0 +1,26 @@
1
+ import { PrismaClient } from "@generated/prisma";
2
+ import { PrismaD1 } from "@prisma/adapter-d1";
3
+
4
+ export type * from "@generated/prisma";
5
+
6
+ export let db: PrismaClient;
7
+
8
+ // context(justinvdm, 21-05-2025): We need to instantiate the client via a
9
+ // function rather that at the module level for several reasons:
10
+ // * For prisma-client-js generator or cases where there are dynamic import to
11
+ // the prisma wasm modules, we need to make sure we are instantiating the
12
+ // prisma client later in the flow when the wasm would have been initialized
13
+ // * So that we can encapsulate workarounds, e.g. see `SELECT 1` workaround
14
+ // below
15
+ export const setupDb = async (env: Env) => {
16
+ db = new PrismaClient({
17
+ // context(justinvdm, 21-05-2025): prisma-client generated type appears to
18
+ // consider D1 adapter incompatible, though in runtime (dev and production)
19
+ // it works
20
+ // @ts-ignore
21
+ adapter: new PrismaD1(env.DB),
22
+ });
23
+
24
+ // context(justinvdm, 21-05-2025): https://github.com/cloudflare/workers-sdk/pull/8283
25
+ await db.$queryRaw`SELECT 1`;
26
+ };
@@ -0,0 +1,21 @@
1
+ import { defineScript } from "rwsdk/worker";
2
+ import { db, setupDb } from "@/db";
3
+ import { env } from "cloudflare:workers";
4
+
5
+ export default defineScript(async () => {
6
+ await setupDb(env);
7
+
8
+ await db.$executeRawUnsafe(`\
9
+ DELETE FROM User;
10
+ DELETE FROM sqlite_sequence;
11
+ `);
12
+
13
+ await db.user.create({
14
+ data: {
15
+ id: "1",
16
+ username: "testuser",
17
+ },
18
+ });
19
+
20
+ console.log("🌱 Finished seeding");
21
+ });