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.
- package/bin/alchemy.mjs +44306 -48055
- package/bin/alchemy.ts +80 -85
- package/bin/commands/create.ts +263 -0
- package/bin/constants.ts +13 -0
- package/bin/errors.ts +11 -0
- package/bin/services/dependencies.ts +47 -0
- package/bin/services/get-package-version.ts +11 -0
- package/bin/services/package-manager.ts +113 -0
- package/bin/services/template-manager.ts +137 -0
- package/bin/types.ts +83 -0
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +12 -3
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.d.ts +2 -0
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +47 -12
- package/lib/apply.js.map +1 -1
- package/lib/build-date.d.ts +6 -0
- package/lib/build-date.d.ts.map +1 -0
- package/lib/build-date.js +8 -0
- package/lib/build-date.js.map +1 -0
- package/lib/cloudflare/api.d.ts.map +1 -1
- package/lib/cloudflare/api.js +2 -1
- package/lib/cloudflare/api.js.map +1 -1
- package/lib/cloudflare/pipeline.d.ts +1 -1
- package/lib/cloudflare/pipeline.d.ts.map +1 -1
- package/lib/cloudflare/pipeline.js +6 -1
- package/lib/cloudflare/pipeline.js.map +1 -1
- package/lib/cloudflare/worker-stub.d.ts +23 -2
- package/lib/cloudflare/worker-stub.d.ts.map +1 -1
- package/lib/cloudflare/worker-stub.js +16 -2
- package/lib/cloudflare/worker-stub.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +4 -3
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +2 -1
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/context.d.ts +2 -2
- package/lib/context.d.ts.map +1 -1
- package/lib/destroy.d.ts +8 -2
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +19 -5
- package/lib/destroy.js.map +1 -1
- package/lib/scope.d.ts +26 -4
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +104 -9
- package/lib/scope.js.map +1 -1
- package/lib/sentry/project.js +1 -1
- package/lib/sentry/project.js.map +1 -1
- package/lib/util/logger.js +1 -1
- package/lib/util/logger.js.map +1 -1
- package/lib/util/mutex.d.ts +9 -0
- package/lib/util/mutex.d.ts.map +1 -0
- package/lib/util/mutex.js +35 -0
- package/lib/util/mutex.js.map +1 -0
- package/package.json +16 -6
- package/src/alchemy.ts +17 -5
- package/src/apply.ts +67 -17
- package/src/build-date.ts +8 -0
- package/src/cloudflare/api.ts +2 -1
- package/src/cloudflare/pipeline.ts +6 -0
- package/src/cloudflare/worker-stub.ts +31 -2
- package/src/cloudflare/worker.ts +4 -3
- package/src/context.ts +2 -2
- package/src/destroy.ts +30 -7
- package/src/scope.ts +145 -10
- package/src/sentry/project.ts +1 -1
- package/src/util/logger.ts +1 -1
- package/src/util/mutex.ts +33 -0
- package/templates/astro/.vscode/extensions.json +4 -0
- package/templates/astro/.vscode/launch.json +11 -0
- package/templates/astro/README.md +48 -0
- package/templates/astro/_env.example +1 -0
- package/templates/astro/_gitignore +27 -0
- package/templates/astro/alchemy.run.ts +16 -0
- package/templates/astro/astro.config.mjs +8 -0
- package/templates/astro/package.json +23 -0
- package/templates/astro/public/favicon.svg +9 -0
- package/templates/astro/src/assets/astro.svg +1 -0
- package/templates/astro/src/assets/background.svg +1 -0
- package/templates/astro/src/components/Welcome.astro +210 -0
- package/templates/astro/src/layouts/Layout.astro +22 -0
- package/templates/astro/src/pages/api/hello.ts +19 -0
- package/templates/astro/src/pages/index.astro +11 -0
- package/templates/astro/tsconfig.json +18 -0
- package/templates/astro/types/env.d.ts +16 -0
- package/templates/astro/wrangler.jsonc +6 -0
- package/templates/nuxt/README.md +75 -0
- package/templates/nuxt/_env +1 -0
- package/templates/nuxt/_env.example +1 -0
- package/templates/nuxt/_gitignore +27 -0
- package/templates/nuxt/alchemy.run.ts +16 -0
- package/templates/nuxt/app.vue +6 -0
- package/templates/nuxt/nuxt.config.ts +13 -0
- package/templates/nuxt/package.json +26 -0
- package/templates/nuxt/public/favicon.ico +0 -0
- package/templates/nuxt/public/robots.txt +2 -0
- package/templates/nuxt/server/api/hello.ts +7 -0
- package/templates/nuxt/server/middleware/auth.ts +5 -0
- package/templates/nuxt/server/middleware/hello.ts +5 -0
- package/templates/nuxt/server/tsconfig.json +3 -0
- package/templates/nuxt/tsconfig.json +15 -0
- package/templates/nuxt/types/env.d.ts +16 -0
- package/templates/react-router/.vscode/settings.json +5 -0
- package/templates/react-router/README.md +79 -0
- package/templates/react-router/_env +1 -0
- package/templates/react-router/_env.example +1 -0
- package/templates/react-router/_gitignore +15 -0
- package/templates/react-router/alchemy.run.ts +17 -0
- package/templates/react-router/app/app.css +15 -0
- package/templates/react-router/app/entry.server.tsx +43 -0
- package/templates/react-router/app/root.tsx +75 -0
- package/templates/react-router/app/routes/home.tsx +17 -0
- package/templates/react-router/app/routes.ts +3 -0
- package/templates/react-router/app/welcome/logo-dark.svg +23 -0
- package/templates/react-router/app/welcome/logo-light.svg +23 -0
- package/templates/react-router/app/welcome/welcome.tsx +90 -0
- package/templates/react-router/package.json +34 -0
- package/templates/react-router/public/favicon.ico +0 -0
- package/templates/react-router/react-router.config.ts +8 -0
- package/templates/react-router/tsconfig.cloudflare.json +27 -0
- package/templates/react-router/tsconfig.json +17 -0
- package/templates/react-router/tsconfig.node.json +20 -0
- package/templates/react-router/types/env.d.ts +16 -0
- package/templates/react-router/vite.config.ts +14 -0
- package/templates/react-router/workers/app.ts +23 -0
- package/templates/react-router/wrangler.jsonc +6 -0
- package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-interruptors.mdc +247 -0
- package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-middleware.mdc +75 -0
- package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-react.mdc +110 -0
- package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-request-response.mdc +213 -0
- package/templates/rwsdk/.devcontainer/Dockerfile +8 -0
- package/templates/rwsdk/.devcontainer/devcontainer.json +21 -0
- package/templates/rwsdk/README.md +62 -0
- package/templates/rwsdk/_env +5 -0
- package/templates/rwsdk/_env.example +5 -0
- package/templates/rwsdk/_gitignore +71 -0
- package/templates/rwsdk/alchemy.run.ts +29 -0
- package/templates/rwsdk/package.json +61 -0
- package/templates/rwsdk/prisma/schema.prisma +43 -0
- package/templates/rwsdk/src/app/Document.tsx +16 -0
- package/templates/rwsdk/src/app/headers.ts +32 -0
- package/templates/rwsdk/src/app/pages/Home.tsx +13 -0
- package/templates/rwsdk/src/app/pages/user/Login.tsx +79 -0
- package/templates/rwsdk/src/app/pages/user/functions.ts +171 -0
- package/templates/rwsdk/src/app/pages/user/routes.ts +17 -0
- package/templates/rwsdk/src/app/shared/links.ts +3 -0
- package/templates/rwsdk/src/client.tsx +3 -0
- package/templates/rwsdk/src/db.ts +26 -0
- package/templates/rwsdk/src/scripts/seed.ts +21 -0
- package/templates/rwsdk/src/session/durableObject.ts +63 -0
- package/templates/rwsdk/src/session/store.ts +13 -0
- package/templates/rwsdk/src/worker.tsx +63 -0
- package/templates/rwsdk/tsconfig.json +47 -0
- package/templates/rwsdk/types/env.d.ts +16 -0
- package/templates/rwsdk/types/rw.d.ts +5 -0
- package/templates/rwsdk/types/vite.d.ts +4 -0
- package/templates/rwsdk/vite.config.mts +6 -0
- package/templates/rwsdk/wrangler.jsonc +28 -0
- package/templates/sveltekit/README.md +38 -0
- package/templates/sveltekit/_env +1 -0
- package/templates/sveltekit/_env.example +1 -0
- package/templates/sveltekit/_gitignore +26 -0
- package/templates/sveltekit/_npmrc +1 -0
- package/templates/sveltekit/_prettierrc +3 -0
- package/templates/sveltekit/alchemy.run.ts +16 -0
- package/templates/sveltekit/bun.lock +1099 -0
- package/templates/sveltekit/package.json +33 -0
- package/templates/sveltekit/src/app.css +1 -0
- package/templates/sveltekit/src/app.d.ts +13 -0
- package/templates/sveltekit/src/app.html +12 -0
- package/templates/sveltekit/src/routes/+layout.svelte +7 -0
- package/templates/sveltekit/src/routes/+page.svelte +2 -0
- package/templates/sveltekit/static/favicon.png +0 -0
- package/templates/sveltekit/svelte.config.js +12 -0
- package/templates/sveltekit/tsconfig.json +27 -0
- package/templates/sveltekit/types/env.d.ts +16 -0
- package/templates/sveltekit/vite.config.ts +6 -0
- package/templates/sveltekit/wrangler.jsonc +7 -0
- package/templates/tanstack-start/.vscode/settings.json +11 -0
- package/templates/tanstack-start/README.md +72 -0
- package/templates/tanstack-start/_env +1 -0
- package/templates/tanstack-start/_env.example +1 -0
- package/templates/tanstack-start/_gitignore +22 -0
- package/templates/tanstack-start/_prettierignore +4 -0
- package/templates/tanstack-start/alchemy.run.ts +16 -0
- package/templates/tanstack-start/package.json +37 -0
- package/templates/tanstack-start/public/android-chrome-192x192.png +0 -0
- package/templates/tanstack-start/public/android-chrome-512x512.png +0 -0
- package/templates/tanstack-start/public/apple-touch-icon.png +0 -0
- package/templates/tanstack-start/public/favicon-16x16.png +0 -0
- package/templates/tanstack-start/public/favicon-32x32.png +0 -0
- package/templates/tanstack-start/public/favicon.ico +0 -0
- package/templates/tanstack-start/public/favicon.png +0 -0
- package/templates/tanstack-start/public/site.webmanifest +19 -0
- package/templates/tanstack-start/src/components/DefaultCatchBoundary.tsx +53 -0
- package/templates/tanstack-start/src/components/NotFound.tsx +25 -0
- package/templates/tanstack-start/src/components/PostError.tsx +5 -0
- package/templates/tanstack-start/src/components/UserError.tsx +5 -0
- package/templates/tanstack-start/src/router.tsx +22 -0
- package/templates/tanstack-start/src/routes/__root.tsx +146 -0
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-a.tsx +10 -0
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-b.tsx +10 -0
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout.tsx +34 -0
- package/templates/tanstack-start/src/routes/_pathlessLayout.tsx +16 -0
- package/templates/tanstack-start/src/routes/api/users.$userId.ts +28 -0
- package/templates/tanstack-start/src/routes/api/users.ts +59 -0
- package/templates/tanstack-start/src/routes/customScript[.]js.ts +10 -0
- package/templates/tanstack-start/src/routes/deferred.tsx +62 -0
- package/templates/tanstack-start/src/routes/index.tsx +12 -0
- package/templates/tanstack-start/src/routes/posts.$postId.tsx +34 -0
- package/templates/tanstack-start/src/routes/posts.index.tsx +8 -0
- package/templates/tanstack-start/src/routes/posts.tsx +38 -0
- package/templates/tanstack-start/src/routes/posts_.$postId.deep.tsx +29 -0
- package/templates/tanstack-start/src/routes/redirect.tsx +9 -0
- package/templates/tanstack-start/src/routes/users.$userId.tsx +44 -0
- package/templates/tanstack-start/src/routes/users.index.tsx +18 -0
- package/templates/tanstack-start/src/routes/users.tsx +49 -0
- package/templates/tanstack-start/src/styles/app.css +12 -0
- package/templates/tanstack-start/src/utils/loggingMiddleware.tsx +41 -0
- package/templates/tanstack-start/src/utils/posts.tsx +40 -0
- package/templates/tanstack-start/src/utils/seo.ts +33 -0
- package/templates/tanstack-start/src/utils/users.tsx +5 -0
- package/templates/tanstack-start/tsconfig.json +31 -0
- package/templates/tanstack-start/types/env.d.ts +16 -0
- package/templates/tanstack-start/vite.config.ts +33 -0
- package/templates/tanstack-start/wrangler.jsonc +7 -0
- package/templates/typescript/README.md +15 -0
- package/templates/typescript/_gitignore +37 -0
- package/templates/typescript/alchemy.run.ts +15 -0
- package/templates/typescript/index.ts +1 -0
- package/templates/typescript/package.json +18 -0
- package/templates/typescript/src/worker.js +5 -0
- package/templates/typescript/src/worker.ts +7 -0
- package/templates/typescript/tsconfig.json +21 -0
- package/templates/typescript/types/env.d.ts +16 -0
- package/templates/vite/README.md +54 -0
- package/templates/vite/_env.example +1 -0
- package/templates/vite/_gitignore +27 -0
- package/templates/vite/alchemy.run.ts +17 -0
- package/templates/vite/eslint.config.js +28 -0
- package/templates/vite/index.html +13 -0
- package/templates/vite/package.json +35 -0
- package/templates/vite/public/vite.svg +1 -0
- package/templates/vite/src/App.css +42 -0
- package/templates/vite/src/App.tsx +35 -0
- package/templates/vite/src/assets/react.svg +1 -0
- package/templates/vite/src/index.css +68 -0
- package/templates/vite/src/main.tsx +10 -0
- package/templates/vite/src/vite-env.d.ts +1 -0
- package/templates/vite/tsconfig.json +34 -0
- package/templates/vite/types/env.d.ts +16 -0
- package/templates/vite/vite.config.ts +9 -0
- package/templates/vite/worker/index.ts +12 -0
- package/templates/vite/wrangler.jsonc +6 -0
- 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,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,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,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
|
+
});
|