@sansavision/create-pulse 0.4.4 → 0.4.5
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/package.json +2 -2
- package/templates/aurora-auth-node-demo/README.md +43 -0
- package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
- package/templates/aurora-auth-node-demo/bun.lock +679 -0
- package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
- package/templates/aurora-auth-node-demo/package.json +39 -0
- package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/aurora-auth-node-demo/server.mjs +46 -0
- package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
- package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
- package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
- package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
- package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
- package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
- package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
- package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
- package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
- package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
- package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
- package/templates/nextjs-auth-demo/package.json +8 -7
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
- package/templates/nextjs-auth-node-demo/.env.example +10 -0
- package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
- package/templates/nextjs-auth-node-demo/README.md +159 -0
- package/templates/nextjs-auth-node-demo/_gitignore +33 -0
- package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
- package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
- package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
- package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
- package/templates/nextjs-auth-node-demo/package.json +38 -0
- package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
- package/templates/nextjs-auth-node-demo/server.mjs +45 -0
- package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
- package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
- package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
- package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
- package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
- package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Better Auth
|
|
2
|
+
BETTER_AUTH_SECRET=replace-me-with-a-32-char-secret-key
|
|
3
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
4
|
+
|
|
5
|
+
# Pulse Relay (embedded — started automatically by server.mjs)
|
|
6
|
+
NEXT_PUBLIC_PULSE_URL=ws://localhost:4001
|
|
7
|
+
|
|
8
|
+
# Optional: override ports
|
|
9
|
+
# PORT=3000
|
|
10
|
+
# PULSE_PORT=4001
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FROM node:22-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install dependencies
|
|
6
|
+
COPY package*.json ./
|
|
7
|
+
RUN npm ci --production
|
|
8
|
+
|
|
9
|
+
# Copy source
|
|
10
|
+
COPY . .
|
|
11
|
+
|
|
12
|
+
# Build Next.js
|
|
13
|
+
RUN npm run build
|
|
14
|
+
|
|
15
|
+
# Expose app + embedded relay
|
|
16
|
+
EXPOSE 3000 4001
|
|
17
|
+
|
|
18
|
+
# Start the unified server
|
|
19
|
+
CMD ["npm", "start"]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Pulse + Next.js Auth Demo (Embedded Node.js)
|
|
2
|
+
|
|
3
|
+
A **full-featured** demo showcasing [Pulse](https://github.com/Sansa-Organisation/pulse) with the relay engine **embedded directly inside your Node.js process** using `@sansavision/pulse-node`. No separate relay process needed — everything runs as a single `node server.mjs`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⚡ **Embedded Relay** — Pulse engine runs in-process via native Rust bindings (napi-rs)
|
|
8
|
+
- 🔐 **Better Auth** — Full authentication with email/password, local SQLite + Drizzle ORM
|
|
9
|
+
- 💬 **Real-time Chat** — Multi-user rooms with presence and typing indicators
|
|
10
|
+
- 📹 **Watch Together** — Synchronized video playback across users
|
|
11
|
+
- 📬 **Durable Queues** — Offline simulation with message persistence
|
|
12
|
+
- 🎮 **Game State Sync** — Real-time tic-tac-toe with shared state
|
|
13
|
+
- 🔒 **E2E Encrypted Chat** — End-to-end encryption with ciphertext visualization
|
|
14
|
+
- 🏟️ **Arena Game** — Real-time multiplayer arena
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Install dependencies
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Configure environment
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cp .env.example .env
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Edit `.env` and set your secret (generate one with `openssl rand -base64 32`):
|
|
31
|
+
|
|
32
|
+
```env
|
|
33
|
+
BETTER_AUTH_SECRET=your-generated-secret-here
|
|
34
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
35
|
+
NEXT_PUBLIC_PULSE_URL=ws://localhost:4001
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 3. Create database tables
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm run db:push
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 4. Start the app
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it! **No separate relay process needed.** The embedded Pulse engine starts automatically alongside Next.js.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
⚡ Pulse Relay running on port 4001
|
|
54
|
+
Region: local | Capacity: 100000
|
|
55
|
+
🌐 Next.js ready on http://localhost:3000
|
|
56
|
+
Pulse relay (embedded) on ws://localhost:4001
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
|
60
|
+
|
|
61
|
+
## How It Works
|
|
62
|
+
|
|
63
|
+
Unlike the standard `nextjs-auth-demo` which requires running a separate Pulse relay, this template uses `@sansavision/pulse-node` — native Rust bindings via napi-rs — to embed the full relay engine directly inside the Node.js process.
|
|
64
|
+
|
|
65
|
+
The `server.mjs` file:
|
|
66
|
+
1. Starts the Pulse relay on a background Tokio thread pool (non-blocking)
|
|
67
|
+
2. Prepares and serves the Next.js app via a custom HTTP server
|
|
68
|
+
3. Handles graceful shutdown on SIGTERM/SIGINT
|
|
69
|
+
|
|
70
|
+
## Multi-User Testing
|
|
71
|
+
|
|
72
|
+
1. Open `http://localhost:3000` in a browser
|
|
73
|
+
2. Create an account and sign in
|
|
74
|
+
3. Open a second browser tab (or incognito window)
|
|
75
|
+
4. Create a different account
|
|
76
|
+
5. Both users can now interact in real-time across all demos
|
|
77
|
+
|
|
78
|
+
## Architecture
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Single Node.js Process (server.mjs)
|
|
82
|
+
├── Pulse Relay (embedded, port 4001)
|
|
83
|
+
│ ├── WebSocket gateway
|
|
84
|
+
│ ├── SFU (Selective Forwarding Unit)
|
|
85
|
+
│ ├── QUIC transport
|
|
86
|
+
│ └── WASM media pipeline
|
|
87
|
+
└── Next.js App (port 3000)
|
|
88
|
+
├── /api/auth/[...all]
|
|
89
|
+
├── /api/pulse/verify
|
|
90
|
+
├── / (Landing)
|
|
91
|
+
├── /auth/sign-in
|
|
92
|
+
├── /auth/sign-up
|
|
93
|
+
└── /dashboard (protected)
|
|
94
|
+
├── /demos/chat
|
|
95
|
+
├── /demos/watch-together
|
|
96
|
+
├── /demos/queues
|
|
97
|
+
├── /demos/game-sync
|
|
98
|
+
├── /demos/arena-game
|
|
99
|
+
└── /demos/encrypted-chat
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Technologies
|
|
103
|
+
|
|
104
|
+
- **Next.js 16** + **React 19** + **Tailwind CSS v4**
|
|
105
|
+
- **Better Auth** with **bearer plugin**
|
|
106
|
+
- **SQLite** + **Drizzle ORM**
|
|
107
|
+
- **Pulse SDK** (`@sansavision/pulse-sdk`) — client-side
|
|
108
|
+
- **Pulse Node** (`@sansavision/pulse-node`) — embedded relay
|
|
109
|
+
|
|
110
|
+
## Environment Variables
|
|
111
|
+
|
|
112
|
+
| Variable | Description | Default |
|
|
113
|
+
|----------|-------------|---------|
|
|
114
|
+
| `BETTER_AUTH_SECRET` | Auth encryption secret (32+ chars) | — |
|
|
115
|
+
| `BETTER_AUTH_URL` | Base URL of the app | `http://localhost:3000` |
|
|
116
|
+
| `NEXT_PUBLIC_PULSE_URL` | Pulse relay WebSocket URL | `ws://localhost:4001` |
|
|
117
|
+
| `PORT` | Next.js server port | `3000` |
|
|
118
|
+
| `PULSE_PORT` | Embedded relay port | `4001` |
|
|
119
|
+
|
|
120
|
+
## Deploying to Production
|
|
121
|
+
|
|
122
|
+
The embedded relay runs on background Tokio threads. Deploy just like any Node.js app:
|
|
123
|
+
|
|
124
|
+
```dockerfile
|
|
125
|
+
FROM node:22-slim
|
|
126
|
+
WORKDIR /app
|
|
127
|
+
COPY package*.json ./
|
|
128
|
+
RUN npm ci --production
|
|
129
|
+
COPY . .
|
|
130
|
+
RUN npm run build
|
|
131
|
+
EXPOSE 3000 4001
|
|
132
|
+
CMD ["npm", "start"]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> **Note:** `@sansavision/pulse-node` includes prebuilt native binaries for
|
|
136
|
+
> `darwin-arm64`, `darwin-x64`, `linux-x64-gnu`, `linux-arm64-gnu`, and `win32-x64-msvc`.
|
|
137
|
+
> No Rust toolchain is needed at deploy time.
|
|
138
|
+
|
|
139
|
+
## Comparison: Standard vs Embedded
|
|
140
|
+
|
|
141
|
+
| | Standard (`nextjs-auth-demo`) | Embedded (`nextjs-auth-node-demo`) |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| Relay | Separate process | In-process (napi-rs) |
|
|
144
|
+
| Setup | 2 terminals | 1 command |
|
|
145
|
+
| Deploy | Sidecar or Docker Compose | Single container |
|
|
146
|
+
| Use case | Multi-service, scaling relay independently | Monolith, simplicity, edge |
|
|
147
|
+
|
|
148
|
+
## Troubleshooting
|
|
149
|
+
|
|
150
|
+
### "Invalid origin" error on signup
|
|
151
|
+
|
|
152
|
+
Your `BETTER_AUTH_URL` doesn't match the port. Set it to match your `PORT` env.
|
|
153
|
+
|
|
154
|
+
### Database errors
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
rm -f sqlite.db
|
|
158
|
+
npm run db:push
|
|
159
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnp
|
|
4
|
+
.pnp.js
|
|
5
|
+
|
|
6
|
+
# builds
|
|
7
|
+
.next/
|
|
8
|
+
out/
|
|
9
|
+
|
|
10
|
+
# database
|
|
11
|
+
*.db
|
|
12
|
+
*.db-journal
|
|
13
|
+
|
|
14
|
+
# misc
|
|
15
|
+
.DS_Store
|
|
16
|
+
*.pem
|
|
17
|
+
|
|
18
|
+
# debug
|
|
19
|
+
npm-debug.log*
|
|
20
|
+
yarn-debug.log*
|
|
21
|
+
yarn-error.log*
|
|
22
|
+
.pnpm-debug.log*
|
|
23
|
+
|
|
24
|
+
# env files
|
|
25
|
+
.env
|
|
26
|
+
.env*.local
|
|
27
|
+
|
|
28
|
+
# vercel
|
|
29
|
+
.vercel
|
|
30
|
+
|
|
31
|
+
# typescript
|
|
32
|
+
*.tsbuildinfo
|
|
33
|
+
next-env.d.ts
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pulse-nextjs-auth-node-demo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "NODE_ENV=development node server.mjs",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "NODE_ENV=production node server.mjs",
|
|
9
|
+
"db:push": "npx drizzle-kit push",
|
|
10
|
+
"db:generate": "npx drizzle-kit generate",
|
|
11
|
+
"db:migrate": "npx drizzle-kit migrate",
|
|
12
|
+
"db:studio": "npx drizzle-kit studio"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@sansavision/pulse-node": "file:../../../pulse-node",
|
|
16
|
+
"@sansavision/pulse-sdk": "^0.4.2",
|
|
17
|
+
"better-auth": "^1.2.0",
|
|
18
|
+
"better-sqlite3": "^12.0.0",
|
|
19
|
+
"drizzle-orm": "^0.41.0",
|
|
20
|
+
"lucide-react": "^0.412.0",
|
|
21
|
+
"next": "16.1.6",
|
|
22
|
+
"react": "19.2.3",
|
|
23
|
+
"react-dom": "19.2.3",
|
|
24
|
+
"react-player": "^2.16.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@tailwindcss/postcss": "^4",
|
|
28
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
29
|
+
"@types/node": "^20",
|
|
30
|
+
"@types/react": "^19",
|
|
31
|
+
"@types/react-dom": "^19",
|
|
32
|
+
"drizzle-kit": "^0.31.0",
|
|
33
|
+
"eslint": "^9",
|
|
34
|
+
"eslint-config-next": "16.1.6",
|
|
35
|
+
"tailwindcss": "^4",
|
|
36
|
+
"typescript": "^5"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { parse } from "url";
|
|
3
|
+
import next from "next";
|
|
4
|
+
import { PulseRelay } from "@sansavision/pulse-node";
|
|
5
|
+
|
|
6
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
7
|
+
const hostname = process.env.HOST || "localhost";
|
|
8
|
+
const port = parseInt(process.env.PORT || "3000", 10);
|
|
9
|
+
const pulsePort = parseInt(process.env.PULSE_PORT || "4001", 10);
|
|
10
|
+
|
|
11
|
+
// ── Start the embedded Pulse relay ──────────────────────────────────────────
|
|
12
|
+
const relay = new PulseRelay({
|
|
13
|
+
bind: `0.0.0.0:${pulsePort}`,
|
|
14
|
+
enableWebsocket: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const info = await relay.start();
|
|
18
|
+
console.log(`⚡ Pulse Relay running on port ${info.wsPort || info.quicPort}`);
|
|
19
|
+
console.log(` Region: ${info.region} | Capacity: ${info.capacity}`);
|
|
20
|
+
|
|
21
|
+
// ── Start Next.js ───────────────────────────────────────────────────────────
|
|
22
|
+
const app = next({ dev, hostname, port });
|
|
23
|
+
const handle = app.getRequestHandler();
|
|
24
|
+
|
|
25
|
+
await app.prepare();
|
|
26
|
+
|
|
27
|
+
createServer((req, res) => {
|
|
28
|
+
const parsedUrl = parse(req.url, true);
|
|
29
|
+
handle(req, res, parsedUrl);
|
|
30
|
+
}).listen(port, () => {
|
|
31
|
+
console.log(`🌐 Next.js ready on http://${hostname}:${port}`);
|
|
32
|
+
console.log(` Pulse relay (embedded) on ws://${hostname}:${pulsePort}`);
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log(" Open in your browser to get started!");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ── Graceful shutdown ───────────────────────────────────────────────────────
|
|
38
|
+
const shutdown = async () => {
|
|
39
|
+
console.log("\n🛑 Shutting down...");
|
|
40
|
+
await relay.stop();
|
|
41
|
+
process.exit(0);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
process.on("SIGTERM", shutdown);
|
|
45
|
+
process.on("SIGINT", shutdown);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { auth } from "@/lib/auth";
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pulse Relay Auth Webhook
|
|
6
|
+
*
|
|
7
|
+
* The Pulse relay calls this endpoint during the CONNECT handshake
|
|
8
|
+
* to verify the client's auth token. We use Better Auth's session
|
|
9
|
+
* API to validate the bearer token and return user identity.
|
|
10
|
+
*
|
|
11
|
+
* Relay config: --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
|
|
12
|
+
*/
|
|
13
|
+
export async function POST(req: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
const body = await req.json();
|
|
16
|
+
const { token } = body;
|
|
17
|
+
|
|
18
|
+
if (!token) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ allow: false, reason: "No token provided" },
|
|
21
|
+
{ status: 200 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Verify the bearer token against Better Auth's session store
|
|
26
|
+
const session = await auth.api.getSession({
|
|
27
|
+
headers: new Headers({
|
|
28
|
+
Authorization: `Bearer ${token}`,
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!session?.user) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ allow: false, reason: "Invalid or expired session token" },
|
|
35
|
+
{ status: 200 }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({
|
|
40
|
+
allow: true,
|
|
41
|
+
user_id: session.user.id,
|
|
42
|
+
claims: {
|
|
43
|
+
email: session.user.email || "",
|
|
44
|
+
name: session.user.name || "",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("[Pulse Verify] Error:", error);
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ allow: false, reason: "Internal verification error" },
|
|
51
|
+
{ status: 200 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { signIn } from "@/lib/auth-client";
|
|
7
|
+
import { Zap, Mail, Lock, ArrowRight, Loader2 } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
export default function SignInPage() {
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const [email, setEmail] = useState("");
|
|
12
|
+
const [password, setPassword] = useState("");
|
|
13
|
+
const [error, setError] = useState("");
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
|
|
16
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
setError("");
|
|
19
|
+
setLoading(true);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const result = await signIn.email({
|
|
23
|
+
email,
|
|
24
|
+
password,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (result.error) {
|
|
28
|
+
setError(result.error.message || "Sign in failed");
|
|
29
|
+
} else {
|
|
30
|
+
router.push("/dashboard");
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
setError("An unexpected error occurred");
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="min-h-screen flex items-center justify-center px-4 relative overflow-hidden">
|
|
41
|
+
{/* Background effects */}
|
|
42
|
+
<div className="absolute top-1/4 left-1/3 w-80 h-80 bg-purple-500/15 rounded-full blur-[100px]" />
|
|
43
|
+
<div className="absolute bottom-1/4 right-1/3 w-80 h-80 bg-cyan-500/15 rounded-full blur-[100px]" />
|
|
44
|
+
|
|
45
|
+
<div className="w-full max-w-md relative z-10">
|
|
46
|
+
{/* Logo */}
|
|
47
|
+
<div className="text-center mb-8">
|
|
48
|
+
<Link href="/" className="inline-flex items-center gap-2 mb-6">
|
|
49
|
+
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center">
|
|
50
|
+
<Zap className="w-6 h-6 text-white" />
|
|
51
|
+
</div>
|
|
52
|
+
<span className="text-2xl font-bold">Pulse</span>
|
|
53
|
+
</Link>
|
|
54
|
+
<h1 className="text-2xl font-bold mb-2">Welcome back</h1>
|
|
55
|
+
<p className="text-slate-400">Sign in to access the demo dashboard</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Form */}
|
|
59
|
+
<form
|
|
60
|
+
onSubmit={handleSubmit}
|
|
61
|
+
className="glass rounded-2xl p-8 space-y-5"
|
|
62
|
+
>
|
|
63
|
+
{error && (
|
|
64
|
+
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
|
65
|
+
{error}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<div>
|
|
70
|
+
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
|
71
|
+
Email
|
|
72
|
+
</label>
|
|
73
|
+
<div className="relative">
|
|
74
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
75
|
+
<input
|
|
76
|
+
type="email"
|
|
77
|
+
value={email}
|
|
78
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
79
|
+
placeholder="you@example.com"
|
|
80
|
+
required
|
|
81
|
+
className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div>
|
|
87
|
+
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
|
88
|
+
Password
|
|
89
|
+
</label>
|
|
90
|
+
<div className="relative">
|
|
91
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
92
|
+
<input
|
|
93
|
+
type="password"
|
|
94
|
+
value={password}
|
|
95
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
96
|
+
placeholder="••••••••"
|
|
97
|
+
required
|
|
98
|
+
className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<button
|
|
104
|
+
type="submit"
|
|
105
|
+
disabled={loading}
|
|
106
|
+
className="w-full py-2.5 bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-500 hover:to-purple-400 rounded-lg font-semibold transition-all hover:shadow-lg hover:shadow-purple-500/25 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
107
|
+
>
|
|
108
|
+
{loading ? (
|
|
109
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
110
|
+
) : (
|
|
111
|
+
<>
|
|
112
|
+
Sign In
|
|
113
|
+
<ArrowRight className="w-4 h-4" />
|
|
114
|
+
</>
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
<p className="text-center text-sm text-slate-400">
|
|
119
|
+
Don't have an account?{" "}
|
|
120
|
+
<Link
|
|
121
|
+
href="/auth/sign-up"
|
|
122
|
+
className="text-purple-400 hover:text-purple-300 font-medium"
|
|
123
|
+
>
|
|
124
|
+
Create one
|
|
125
|
+
</Link>
|
|
126
|
+
</p>
|
|
127
|
+
</form>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|