@sansavision/create-pulse 0.4.4 → 0.4.6

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 (97) hide show
  1. package/dist/index.js +2 -0
  2. package/package.json +2 -2
  3. package/templates/aurora-auth-node-demo/README.md +43 -0
  4. package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
  5. package/templates/aurora-auth-node-demo/bun.lock +679 -0
  6. package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
  7. package/templates/aurora-auth-node-demo/package.json +39 -0
  8. package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
  9. package/templates/aurora-auth-node-demo/server.mjs +46 -0
  10. package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
  11. package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
  12. package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
  13. package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
  14. package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
  15. package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
  16. package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
  17. package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
  18. package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
  19. package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
  20. package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
  21. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
  22. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
  23. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
  24. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
  25. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
  26. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
  27. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
  28. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
  29. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
  30. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
  31. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
  32. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
  33. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
  34. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
  35. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
  36. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
  37. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
  38. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
  39. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
  40. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
  41. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
  42. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
  43. package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
  44. package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
  45. package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
  46. package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
  47. package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
  48. package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
  49. package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
  50. package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
  51. package/templates/nextjs-auth-demo/package.json +8 -7
  52. package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
  53. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
  54. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
  55. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
  56. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
  57. package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
  58. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
  59. package/templates/nextjs-auth-node-demo/.env.example +10 -0
  60. package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
  61. package/templates/nextjs-auth-node-demo/README.md +159 -0
  62. package/templates/nextjs-auth-node-demo/_gitignore +33 -0
  63. package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
  64. package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
  65. package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
  66. package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
  67. package/templates/nextjs-auth-node-demo/package.json +38 -0
  68. package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
  69. package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
  70. package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
  71. package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
  72. package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
  73. package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
  74. package/templates/nextjs-auth-node-demo/server.mjs +45 -0
  75. package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
  76. package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
  77. package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
  78. package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
  79. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
  80. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
  81. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
  82. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
  83. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
  84. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
  85. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
  86. package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
  87. package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
  88. package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
  89. package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
  90. package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
  91. package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
  92. package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
  93. package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
  94. package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
  95. package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
  96. package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
  97. package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ dialect: "sqlite",
5
+ schema: "./src/lib/schema.ts",
6
+ dbCredentials: {
7
+ url: "./sqlite.db",
8
+ },
9
+ });
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "pulse-aurora-auth-node-demo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "NODE_ENV=development bun server.mjs",
8
+ "build": "aurora build --target node",
9
+ "start": "NODE_ENV=production bun server.mjs",
10
+ "doctor": "aurora doctor",
11
+ "db:push": "bunx drizzle-kit push",
12
+ "db:generate": "bunx drizzle-kit generate",
13
+ "db:migrate": "bunx drizzle-kit migrate",
14
+ "db:studio": "bunx drizzle-kit studio"
15
+ },
16
+ "dependencies": {
17
+ "@sansavision/aurora": "0.1.0-alpha.20260307.5",
18
+ "@sansavision/pulse-node": "file:../../../pulse-node",
19
+ "@sansavision/pulse-sdk": "^0.4.2",
20
+ "better-auth": "^1.2.0",
21
+ "better-sqlite3": "^12.0.0",
22
+ "drizzle-orm": "^0.41.0",
23
+ "lucide-react": "^0.412.0",
24
+ "react": "19.2.3",
25
+ "react-dom": "19.2.3",
26
+ "react-player": "^2.16.0"
27
+ },
28
+ "devDependencies": {
29
+ "@tailwindcss/cli": "^4.2.1",
30
+ "@tailwindcss/postcss": "^4",
31
+ "@types/better-sqlite3": "^7.6.0",
32
+ "@types/node": "^20",
33
+ "@types/react": "^19",
34
+ "@types/react-dom": "^19",
35
+ "drizzle-kit": "^0.31.0",
36
+ "tailwindcss": "^4.2.1",
37
+ "typescript": "^5"
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,46 @@
1
+ import { spawn } from "node:child_process";
2
+ import { PulseRelay } from "@sansavision/pulse-node";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const dev = process.env.NODE_ENV !== "production";
6
+ const hostname = process.env.HOST || "localhost";
7
+ const port = parseInt(process.env.PORT || "3000", 10);
8
+ const pulsePort = parseInt(process.env.PULSE_PORT || "4001", 10);
9
+
10
+ // ── Start the embedded Pulse relay ──────────────────────────────────────────
11
+ const relay = new PulseRelay({
12
+ bind: `0.0.0.0:${pulsePort}`,
13
+ enableWebsocket: true,
14
+ });
15
+
16
+ const info = await relay.start();
17
+ console.log(`⚡ Pulse Relay running on port ${info.wsPort || info.quicPort}`);
18
+ console.log(` Region: ${info.region} | Capacity: ${info.capacity}`);
19
+
20
+ // ── Start Web Server ────────────────────────────────────────────────────────
21
+ const args = dev ? ["run", "dev"] : ["run", "start"];
22
+ args.push("--port", port.toString());
23
+
24
+ const cp = spawn("bunx", ["aurora", ...args.slice(1)], {
25
+ stdio: "inherit",
26
+ env: process.env,
27
+ });
28
+
29
+ cp.on("close", (code) => {
30
+ console.log(`Aurora exited with code ${code}`);
31
+ shutdown();
32
+ });
33
+
34
+ // ── Graceful shutdown ───────────────────────────────────────────────────────
35
+ const shutdown = async () => {
36
+ console.log("\n🛑 Shutting down...");
37
+ await relay.stop();
38
+ if (cp.exitCode === null) {
39
+ cp.kill();
40
+ }
41
+ process.exit(0);
42
+ };
43
+
44
+ process.on("SIGTERM", shutdown);
45
+ process.on("SIGINT", shutdown);
46
+
@@ -0,0 +1,31 @@
1
+ // aurora:justify-auth-none: template demo action; replace with real authentication (e.g. .auth("required")) before shipping.
2
+ import { err, ok, s, server } from "@sansavision/aurora";
3
+
4
+ import { requireUser } from "../lib/auth.server";
5
+ import { appendMessage } from "../queries/listMessages.server";
6
+
7
+ const createMessageInput = s.object({
8
+ text: s.string().trim().min(1).max(500),
9
+ });
10
+
11
+ export const createMessage = server
12
+ .action("messages.create")
13
+ .input(createMessageInput)
14
+ .handler(async ({ input }: { input: { text: string } }) => {
15
+ if (input.text.length > 280) {
16
+ return err("MESSAGE_TOO_LONG", {
17
+ max: 280,
18
+ });
19
+ }
20
+
21
+ const user = await requireUser();
22
+ const record = appendMessage(input.text);
23
+
24
+ return ok({
25
+ ...record,
26
+ authorId: user.id,
27
+ });
28
+ })
29
+ .invalidates(["messages"])
30
+ .auth("none")
31
+ .errors(["MESSAGE_TOO_LONG"] as const);
@@ -0,0 +1,65 @@
1
+ import { defineAuthPipeline } from "@sansavision/aurora/server";
2
+ import { auth } from "@/lib/auth";
3
+
4
+ function normalizeAuroraRoles(role: string | undefined): string[] {
5
+ if (role === "game_master") {
6
+ return ["user", "admin"];
7
+ }
8
+ return ["user"];
9
+ }
10
+
11
+ export default defineAuthPipeline({
12
+ async resolve(request: Request) {
13
+ const cookieHeader = request.headers.get("cookie") ?? "";
14
+ if (!cookieHeader) {
15
+ return null;
16
+ }
17
+
18
+ const headers = new Headers();
19
+ headers.set("cookie", cookieHeader);
20
+
21
+ const session = await auth.api.getSession({ headers });
22
+
23
+ if (!session?.user?.id) {
24
+ return null;
25
+ }
26
+
27
+ // Better Auth's getSession returns the full user row — our custom
28
+ // `role` column is present at runtime but not in the default TS types.
29
+ const userRecord = session.user as typeof session.user & { role?: string };
30
+ const roles = normalizeAuroraRoles(userRecord.role);
31
+
32
+ return {
33
+ authenticated: true,
34
+ subjectId: session.user.id,
35
+ roles,
36
+ permissions: [],
37
+ provider: "better-auth",
38
+ claims: {
39
+ email: session.user.email,
40
+ name: session.user.name,
41
+ role: userRecord.role ?? "player",
42
+ },
43
+ };
44
+ },
45
+ deny: {
46
+ redirect: "/auth/login",
47
+ },
48
+ routing: [
49
+ {
50
+ path: "/auth/login",
51
+ condition: "authenticated",
52
+ redirect: "/",
53
+ },
54
+ {
55
+ path: "/lobbies",
56
+ condition: "unauthenticated",
57
+ redirect: "/auth/login",
58
+ },
59
+ {
60
+ path: "/matches",
61
+ condition: "unauthenticated",
62
+ redirect: "/auth/login",
63
+ },
64
+ ],
65
+ });
@@ -0,0 +1,30 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+
3
+ // Aurora only dispatches handler.ts files for non-GET HTTP methods.
4
+ // better-auth uses GET for /get-session. We wrap fetch to convert
5
+ // those GETs into POSTs so they hit the handler instead of the page renderer.
6
+ const auroraFetch: typeof fetch = (input, init) => {
7
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
8
+ if (url.includes("/api/auth/") && (!init?.method || init.method === "GET")) {
9
+ return fetch(input, { ...init, method: "POST" });
10
+ }
11
+ return fetch(input, init);
12
+ };
13
+
14
+ export const authClient = createAuthClient({
15
+ baseURL: typeof window !== "undefined" ? window.location.origin : "http://localhost:3000",
16
+ fetchOptions: {
17
+ customFetchImpl: auroraFetch,
18
+ onSuccess: (ctx) => {
19
+ const authToken = ctx.response.headers.get("set-auth-token");
20
+ if (authToken && typeof window !== "undefined") {
21
+ localStorage.setItem("pulse_bearer_token", authToken);
22
+ }
23
+ },
24
+ },
25
+ });
26
+
27
+ export const signIn = authClient.signIn;
28
+ export const signUp = authClient.signUp;
29
+ export const signOut = authClient.signOut;
30
+ export const useSession = authClient.useSession;
@@ -0,0 +1,11 @@
1
+ export interface AuthUser {
2
+ id: string;
3
+ role: "admin" | "user";
4
+ }
5
+
6
+ export async function requireUser(): Promise<AuthUser> {
7
+ return {
8
+ id: "demo-user",
9
+ role: "admin",
10
+ };
11
+ }
@@ -0,0 +1,30 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { bearer } from "better-auth/plugins";
4
+ import { db } from "./db";
5
+ import * as schema from "./schema";
6
+
7
+ const baseURL =
8
+ process.env.BETTER_AUTH_URL ||
9
+ `http://localhost:${process.env.PORT || "3000"}`;
10
+
11
+ export const auth = betterAuth({
12
+ baseURL,
13
+ database: drizzleAdapter(db, { provider: "sqlite", schema }),
14
+ emailAndPassword: {
15
+ enabled: true,
16
+ },
17
+ session: {
18
+ // Aurora only dispatches handler.ts for non-GET methods, so the
19
+ // client converts GET /get-session → POST. better-auth requires
20
+ // deferSessionRefresh to accept POST on the get-session endpoint.
21
+ deferSessionRefresh: true,
22
+ },
23
+ plugins: [bearer()],
24
+ // In production, set BETTER_AUTH_URL to your domain.
25
+ // In dev, we trust any localhost origin regardless of port.
26
+ trustedOrigins: (origin) => {
27
+ if (origin.startsWith("http://localhost:")) return true;
28
+ return origin === baseURL;
29
+ },
30
+ });
@@ -0,0 +1,6 @@
1
+ import Database from "better-sqlite3";
2
+ import { drizzle } from "drizzle-orm/better-sqlite3";
3
+ import * as schema from "./schema";
4
+
5
+ const sqlite = new Database("./sqlite.db");
6
+ export const db = drizzle(sqlite, { schema });
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { Pulse } from "@sansavision/pulse-sdk";
4
+
5
+ let pulseInstance: Pulse | null = null;
6
+
7
+ export function getPulse(): Pulse {
8
+ if (!pulseInstance) {
9
+ pulseInstance = new Pulse({ apiKey: "demo" });
10
+ }
11
+ return pulseInstance;
12
+ }
13
+
14
+ /**
15
+ * Get the bearer token stored by Better Auth for Pulse auth.
16
+ */
17
+ export function getPulseToken(): string | null {
18
+ if (typeof window === "undefined") return null;
19
+ return localStorage.getItem("pulse_bearer_token");
20
+ }
21
+
22
+ /**
23
+ * Connect to the Pulse relay with authentication.
24
+ * Uses the Better Auth bearer token for the PLP CONNECT handshake.
25
+ */
26
+ export async function connectWithAuth() {
27
+ const pulse = getPulse();
28
+ const token = getPulseToken();
29
+ const url = process.env.NEXT_PUBLIC_PULSE_URL || "ws://localhost:4001";
30
+
31
+ const conn = await pulse.connect(url, {
32
+ encoding: "json",
33
+ autoReconnect: true,
34
+ ...(token
35
+ ? {
36
+ auth: {
37
+ token,
38
+ provider: "better-auth",
39
+ },
40
+ }
41
+ : {}),
42
+ });
43
+
44
+ return conn;
45
+ }
@@ -0,0 +1,107 @@
1
+ import { relations, sql } from "drizzle-orm";
2
+ import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
3
+
4
+ export const user = sqliteTable("user", {
5
+ id: text("id").primaryKey(),
6
+ name: text("name").notNull(),
7
+ email: text("email").notNull().unique(),
8
+ emailVerified: integer("email_verified", { mode: "boolean" })
9
+ .default(false)
10
+ .notNull(),
11
+ image: text("image"),
12
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
13
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
14
+ .notNull(),
15
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
16
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
17
+ .$onUpdate(() => /* @__PURE__ */ new Date())
18
+ .notNull(),
19
+ });
20
+
21
+ export const session = sqliteTable(
22
+ "session",
23
+ {
24
+ id: text("id").primaryKey(),
25
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
26
+ token: text("token").notNull().unique(),
27
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
28
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
29
+ .notNull(),
30
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
31
+ .$onUpdate(() => /* @__PURE__ */ new Date())
32
+ .notNull(),
33
+ ipAddress: text("ip_address"),
34
+ userAgent: text("user_agent"),
35
+ userId: text("user_id")
36
+ .notNull()
37
+ .references(() => user.id, { onDelete: "cascade" }),
38
+ },
39
+ (table) => [index("session_userId_idx").on(table.userId)],
40
+ );
41
+
42
+ export const account = sqliteTable(
43
+ "account",
44
+ {
45
+ id: text("id").primaryKey(),
46
+ accountId: text("account_id").notNull(),
47
+ providerId: text("provider_id").notNull(),
48
+ userId: text("user_id")
49
+ .notNull()
50
+ .references(() => user.id, { onDelete: "cascade" }),
51
+ accessToken: text("access_token"),
52
+ refreshToken: text("refresh_token"),
53
+ idToken: text("id_token"),
54
+ accessTokenExpiresAt: integer("access_token_expires_at", {
55
+ mode: "timestamp_ms",
56
+ }),
57
+ refreshTokenExpiresAt: integer("refresh_token_expires_at", {
58
+ mode: "timestamp_ms",
59
+ }),
60
+ scope: text("scope"),
61
+ password: text("password"),
62
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
63
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
64
+ .notNull(),
65
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
66
+ .$onUpdate(() => /* @__PURE__ */ new Date())
67
+ .notNull(),
68
+ },
69
+ (table) => [index("account_userId_idx").on(table.userId)],
70
+ );
71
+
72
+ export const verification = sqliteTable(
73
+ "verification",
74
+ {
75
+ id: text("id").primaryKey(),
76
+ identifier: text("identifier").notNull(),
77
+ value: text("value").notNull(),
78
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
79
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
80
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
81
+ .notNull(),
82
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
83
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
84
+ .$onUpdate(() => /* @__PURE__ */ new Date())
85
+ .notNull(),
86
+ },
87
+ (table) => [index("verification_identifier_idx").on(table.identifier)],
88
+ );
89
+
90
+ export const userRelations = relations(user, ({ many }) => ({
91
+ sessions: many(session),
92
+ accounts: many(account),
93
+ }));
94
+
95
+ export const sessionRelations = relations(session, ({ one }) => ({
96
+ user: one(user, {
97
+ fields: [session.userId],
98
+ references: [user.id],
99
+ }),
100
+ }));
101
+
102
+ export const accountRelations = relations(account, ({ one }) => ({
103
+ user: one(user, {
104
+ fields: [account.userId],
105
+ references: [user.id],
106
+ }),
107
+ }));
@@ -0,0 +1,25 @@
1
+ import { server } from "@sansavision/aurora";
2
+
3
+ export interface MessageListItem {
4
+ id: string;
5
+ text: string;
6
+ createdAt: number;
7
+ }
8
+
9
+ const messages: MessageListItem[] = [];
10
+
11
+ export function appendMessage(text: string): MessageListItem {
12
+ const record: MessageListItem = {
13
+ id: `msg-${Date.now()}`,
14
+ text,
15
+ createdAt: Date.now(),
16
+ };
17
+
18
+ messages.unshift(record);
19
+ return record;
20
+ }
21
+
22
+ export const listMessages = server
23
+ .query("messages.list", async () => messages)
24
+ .key(() => ["messages", "list"])
25
+ .tags(() => ["messages"]);
@@ -0,0 +1,14 @@
1
+ import { auth } from "@/lib/auth";
2
+
3
+ // Aurora handler resolution requires a default export.
4
+ // Aurora may pass either a raw Request or a context object { request, params }.
5
+ // We handle both cases.
6
+ export default async function handler(input: Request | { request: Request; params?: Record<string, string> }) {
7
+ const request = input instanceof Request ? input : input.request;
8
+ try {
9
+ return await auth.handler(request);
10
+ } catch (error) {
11
+ console.error("[auth handler] Error:", error);
12
+ return Response.json({ error: "Internal server error" }, { status: 500 });
13
+ }
14
+ }
@@ -0,0 +1,55 @@
1
+ import { auth } from "@/lib/auth";
2
+
3
+ // Aurora handler resolution requires a default export.
4
+ // Aurora may pass either a raw Request or a context object { request, params }.
5
+ export default async function handler(input: Request | { request: Request; params?: Record<string, string> }) {
6
+ const request = input instanceof Request ? input : input.request;
7
+
8
+ if (request.method !== "POST") {
9
+ return Response.json(
10
+ { error: "Method not allowed" },
11
+ { status: 405 }
12
+ );
13
+ }
14
+
15
+ try {
16
+ const body = await request.json();
17
+ const { token } = body;
18
+
19
+ if (!token) {
20
+ return Response.json(
21
+ { allow: false, reason: "No token provided" },
22
+ { status: 200 }
23
+ );
24
+ }
25
+
26
+ // Verify the bearer token against Better Auth's session store
27
+ const session = await auth.api.getSession({
28
+ headers: new Headers({
29
+ Authorization: `Bearer ${token}`,
30
+ }),
31
+ });
32
+
33
+ if (!session?.user) {
34
+ return Response.json(
35
+ { allow: false, reason: "Invalid or expired session token" },
36
+ { status: 200 }
37
+ );
38
+ }
39
+
40
+ return Response.json({
41
+ allow: true,
42
+ user_id: session.user.id,
43
+ claims: {
44
+ email: session.user.email || "",
45
+ name: session.user.name || "",
46
+ },
47
+ });
48
+ } catch (error) {
49
+ console.error("[Pulse Verify] Error:", error);
50
+ return Response.json(
51
+ { allow: false, reason: "Internal verification error" },
52
+ { status: 200 }
53
+ );
54
+ }
55
+ }
@@ -0,0 +1,132 @@
1
+
2
+
3
+ import { useState } from "react";
4
+ import { createClientNavigation } from "@sansavision/aurora/router";
5
+
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 = createClientNavigation();
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.navigate("/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
+ <a 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
+ </a>
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
+ method="post"
61
+ onSubmit={handleSubmit}
62
+ className="glass rounded-2xl p-8 space-y-5"
63
+ >
64
+ {error && (
65
+ <div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
66
+ {error}
67
+ </div>
68
+ )}
69
+
70
+ <div>
71
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
72
+ Email
73
+ </label>
74
+ <div className="relative">
75
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
76
+ <input
77
+ type="email"
78
+ value={email}
79
+ onChange={(e) => setEmail(e.target.value)}
80
+ placeholder="you@example.com"
81
+ required
82
+ 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"
83
+ />
84
+ </div>
85
+ </div>
86
+
87
+ <div>
88
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
89
+ Password
90
+ </label>
91
+ <div className="relative">
92
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
93
+ <input
94
+ type="password"
95
+ value={password}
96
+ onChange={(e) => setPassword(e.target.value)}
97
+ placeholder="••••••••"
98
+ required
99
+ 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"
100
+ />
101
+ </div>
102
+ </div>
103
+
104
+ <button
105
+ type="submit"
106
+ disabled={loading}
107
+ 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"
108
+ >
109
+ {loading ? (
110
+ <Loader2 className="w-4 h-4 animate-spin" />
111
+ ) : (
112
+ <>
113
+ Sign In
114
+ <ArrowRight className="w-4 h-4" />
115
+ </>
116
+ )}
117
+ </button>
118
+
119
+ <p className="text-center text-sm text-slate-400">
120
+ Don&apos;t have an account?{" "}
121
+ <a
122
+ href="/auth/sign-up"
123
+ className="text-purple-400 hover:text-purple-300 font-medium"
124
+ >
125
+ Create one
126
+ </a>
127
+ </p>
128
+ </form>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,5 @@
1
+ import SignInPage from "./page.client";
2
+
3
+ export default function Page() {
4
+ return <SignInPage />;
5
+ }