mirror of
https://github.com/kennethnym/aris.git
synced 2026-02-02 13:11:17 +00:00
Compare commits
4 Commits
feat/sourc
...
feat/auth
| Author | SHA1 | Date | |
|---|---|---|---|
| c10c8a553a | |||
| fffcccc227 | |||
| b744af9c51 | |||
| 13300fb6a6 |
8
apps/aris-backend/.env.example
Normal file
8
apps/aris-backend/.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# PostgreSQL connection string
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/aris
|
||||||
|
|
||||||
|
# BetterAuth secret (min 32 chars, generate with: openssl rand -base64 32)
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
|
||||||
|
# Base URL of the backend
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
22
apps/aris-backend/package.json
Normal file
22
apps/aris-backend/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@aris/backend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/server.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --watch src/server.ts",
|
||||||
|
"start": "bun run src/server.ts",
|
||||||
|
"test": "bun test src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aris/core": "workspace:*",
|
||||||
|
"@aris/source-location": "workspace:*",
|
||||||
|
"@aris/source-weatherkit": "workspace:*",
|
||||||
|
"better-auth": "^1",
|
||||||
|
"hono": "^4",
|
||||||
|
"pg": "^8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "^8"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/aris-backend/src/auth/http.ts
Normal file
7
apps/aris-backend/src/auth/http.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Hono } from "hono"
|
||||||
|
|
||||||
|
import { auth } from "./index.ts"
|
||||||
|
|
||||||
|
export function registerAuthHandlers(app: Hono): void {
|
||||||
|
app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw))
|
||||||
|
}
|
||||||
10
apps/aris-backend/src/auth/index.ts
Normal file
10
apps/aris-backend/src/auth/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
|
import { pool } from "../db.ts"
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: pool,
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
54
apps/aris-backend/src/auth/session-middleware.ts
Normal file
54
apps/aris-backend/src/auth/session-middleware.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Context, Next } from "hono"
|
||||||
|
|
||||||
|
import { auth } from "./index.ts"
|
||||||
|
|
||||||
|
type SessionUser = typeof auth.$Infer.Session.user
|
||||||
|
type Session = typeof auth.$Infer.Session.session
|
||||||
|
|
||||||
|
export interface SessionVariables {
|
||||||
|
user: SessionUser | null
|
||||||
|
session: Session | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware that attaches session and user to the context.
|
||||||
|
* Does not reject unauthenticated requests - use requireSession for that.
|
||||||
|
*/
|
||||||
|
export async function sessionMiddleware(c: Context, next: Next): Promise<void> {
|
||||||
|
const session = await auth.api.getSession({ headers: c.req.raw.headers })
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
c.set("user", session.user)
|
||||||
|
c.set("session", session.session)
|
||||||
|
} else {
|
||||||
|
c.set("user", null)
|
||||||
|
c.set("session", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
await next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware that requires a valid session. Returns 401 if not authenticated.
|
||||||
|
*/
|
||||||
|
export async function requireSession(c: Context, next: Next): Promise<Response | void> {
|
||||||
|
const session = await auth.api.getSession({ headers: c.req.raw.headers })
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: "Unauthorized" }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.set("user", session.user)
|
||||||
|
c.set("session", session.session)
|
||||||
|
await next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session from headers. Useful for WebSocket upgrade validation.
|
||||||
|
*/
|
||||||
|
export async function getSessionFromHeaders(
|
||||||
|
headers: Headers,
|
||||||
|
): Promise<{ user: SessionUser; session: Session } | null> {
|
||||||
|
const session = await auth.api.getSession({ headers })
|
||||||
|
return session
|
||||||
|
}
|
||||||
5
apps/aris-backend/src/db.ts
Normal file
5
apps/aris-backend/src/db.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Pool } from "pg"
|
||||||
|
|
||||||
|
export const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
})
|
||||||
14
apps/aris-backend/src/server.ts
Normal file
14
apps/aris-backend/src/server.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Hono } from "hono"
|
||||||
|
|
||||||
|
import { registerAuthHandlers } from "./auth/http.ts"
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.get("/health", (c) => c.json({ status: "ok" }))
|
||||||
|
|
||||||
|
registerAuthHandlers(app)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
port: 3000,
|
||||||
|
fetch: app.fetch,
|
||||||
|
}
|
||||||
4
apps/aris-backend/tsconfig.json
Normal file
4
apps/aris-backend/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
81
bun.lock
81
bun.lock
@@ -13,6 +13,21 @@
|
|||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"apps/aris-backend": {
|
||||||
|
"name": "@aris/backend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aris/core": "workspace:*",
|
||||||
|
"@aris/source-location": "workspace:*",
|
||||||
|
"@aris/source-weatherkit": "workspace:*",
|
||||||
|
"better-auth": "^1",
|
||||||
|
"hono": "^4",
|
||||||
|
"pg": "^8",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "^8",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/aris-core": {
|
"packages/aris-core": {
|
||||||
"name": "@aris/core",
|
"name": "@aris/core",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -52,6 +67,8 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@aris/backend": ["@aris/backend@workspace:apps/aris-backend"],
|
||||||
|
|
||||||
"@aris/core": ["@aris/core@workspace:packages/aris-core"],
|
"@aris/core": ["@aris/core@workspace:packages/aris-core"],
|
||||||
|
|
||||||
"@aris/data-source-weatherkit": ["@aris/data-source-weatherkit@workspace:packages/aris-data-source-weatherkit"],
|
"@aris/data-source-weatherkit": ["@aris/data-source-weatherkit@workspace:packages/aris-data-source-weatherkit"],
|
||||||
@@ -66,6 +83,18 @@
|
|||||||
|
|
||||||
"@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
|
"@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
|
||||||
|
|
||||||
|
"@better-auth/core": ["@better-auth/core@1.4.17", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-WSaEQDdUO6B1CzAmissN6j0lx9fM9lcslEYzlApB5UzFaBeAOHNUONTdglSyUs6/idiZBoRvt0t/qMXCgIU8ug=="],
|
||||||
|
|
||||||
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.17", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.17" } }, "sha512-R1BC4e/bNjQbXu7lG6ubpgmsPj7IMqky5DvMlzAtnAJWJhh99pMh/n6w5gOHa0cqDZgEAuj75IPTxv+q3YiInA=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||||
|
|
||||||
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||||
|
|
||||||
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="],
|
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="],
|
||||||
|
|
||||||
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="],
|
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="],
|
||||||
@@ -98,24 +127,76 @@
|
|||||||
|
|
||||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="],
|
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||||
|
|
||||||
|
"@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="],
|
||||||
|
|
||||||
"arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="],
|
"arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="],
|
||||||
|
|
||||||
"arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="],
|
"arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="],
|
||||||
|
|
||||||
|
"better-auth": ["better-auth@1.4.17", "", { "dependencies": { "@better-auth/core": "1.4.17", "@better-auth/telemetry": "1.4.17", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew=="],
|
||||||
|
|
||||||
|
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.11.5", "", {}, "sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.28.10", "", {}, "sha512-ksNxfzIW77OcZ+QWSAPC7yDqUSaIVwkTWnTPNiIy//vifNbwsSgQ57OkkncHxxpcBHM3LRfLAZVEh7kjq5twVA=="],
|
||||||
|
|
||||||
|
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
|
||||||
|
|
||||||
"oxfmt": ["oxfmt@0.24.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.24.0", "@oxfmt/darwin-x64": "0.24.0", "@oxfmt/linux-arm64-gnu": "0.24.0", "@oxfmt/linux-arm64-musl": "0.24.0", "@oxfmt/linux-x64-gnu": "0.24.0", "@oxfmt/linux-x64-musl": "0.24.0", "@oxfmt/win32-arm64": "0.24.0", "@oxfmt/win32-x64": "0.24.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw=="],
|
"oxfmt": ["oxfmt@0.24.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.24.0", "@oxfmt/darwin-x64": "0.24.0", "@oxfmt/linux-arm64-gnu": "0.24.0", "@oxfmt/linux-arm64-musl": "0.24.0", "@oxfmt/linux-x64-gnu": "0.24.0", "@oxfmt/linux-x64-musl": "0.24.0", "@oxfmt/win32-arm64": "0.24.0", "@oxfmt/win32-x64": "0.24.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw=="],
|
||||||
|
|
||||||
"oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="],
|
"oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="],
|
||||||
|
|
||||||
|
"pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="],
|
||||||
|
|
||||||
|
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="],
|
||||||
|
|
||||||
|
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||||
|
|
||||||
|
"pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="],
|
||||||
|
|
||||||
|
"pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
|
||||||
|
|
||||||
|
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||||
|
|
||||||
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
|
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
|
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||||
|
|
||||||
|
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
|
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||||
|
|
||||||
|
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
"tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="],
|
"tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
178
docs/backend-spec.md
Normal file
178
docs/backend-spec.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# ARIS Backend Specification
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
ARIS needs a backend service that manages per-user FeedEngine instances and delivers real-time feed updates to clients. The backend must handle authentication, maintain WebSocket connections for live updates, and accept context updates (like location) that trigger feed recalculations.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- Email/password authentication using BetterAuth
|
||||||
|
- PostgreSQL for session and user storage
|
||||||
|
- Session tokens validated via `Authorization: Bearer <token>` header
|
||||||
|
- Auth endpoints exposed via BetterAuth's built-in routes
|
||||||
|
|
||||||
|
### FeedEngine Management
|
||||||
|
- Each authenticated user gets their own FeedEngine instance
|
||||||
|
- Instances are cached in memory with a 30-minute TTL
|
||||||
|
- TTL resets on any activity (WebSocket message, location update)
|
||||||
|
- Default sources registered for each user: `LocationSource`, `WeatherSource`, `TflSource`
|
||||||
|
- Source configuration is hardcoded initially (customization deferred)
|
||||||
|
|
||||||
|
### WebSocket Connection
|
||||||
|
- Single endpoint: `GET /ws` (upgrades to WebSocket)
|
||||||
|
- Authentication via `Authorization: Bearer <token>` header on upgrade request
|
||||||
|
- Rejected before upgrade if token is invalid
|
||||||
|
- Multiple connections per user allowed (e.g., multiple devices)
|
||||||
|
- All connections for a user receive the same feed updates
|
||||||
|
- On connect: immediately send current feed state
|
||||||
|
|
||||||
|
### JSON-RPC Protocol
|
||||||
|
All WebSocket communication uses JSON-RPC 2.0.
|
||||||
|
|
||||||
|
**Client → Server (Requests):**
|
||||||
|
```json
|
||||||
|
{ "jsonrpc": "2.0", "method": "location.update", "params": { "lat": 51.5, "lng": -0.1, "accuracy": 10, "timestamp": "2025-01-01T12:00:00Z" }, "id": 1 }
|
||||||
|
{ "jsonrpc": "2.0", "method": "feed.refresh", "params": {}, "id": 2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (Responses):**
|
||||||
|
```json
|
||||||
|
{ "jsonrpc": "2.0", "result": { "ok": true }, "id": 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (Notifications - no id):**
|
||||||
|
```json
|
||||||
|
{ "jsonrpc": "2.0", "method": "feed.update", "params": { "items": [...], "errors": [...] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON-RPC Methods
|
||||||
|
|
||||||
|
| Method | Params | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `location.update` | `{ lat, lng, accuracy, timestamp }` | Push location update, triggers feed refresh |
|
||||||
|
| `feed.refresh` | `{}` | Force manual feed refresh |
|
||||||
|
|
||||||
|
### Server Notifications
|
||||||
|
|
||||||
|
| Method | Params | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `feed.update` | `{ context, items, errors }` | Feed state changed |
|
||||||
|
| `error` | `{ code, message, data? }` | Source or system error |
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Source failures during refresh are reported via `error` notification
|
||||||
|
- Format: `{ "jsonrpc": "2.0", "method": "error", "params": { "code": -32000, "message": "...", "data": { "sourceId": "weather" } } }`
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. **Auth Flow**
|
||||||
|
- [ ] User can sign up with email/password via `POST /api/auth/sign-up`
|
||||||
|
- [ ] User can sign in via `POST /api/auth/sign-in` and receive session token
|
||||||
|
- [ ] Invalid credentials return 401
|
||||||
|
|
||||||
|
2. **WebSocket Connection**
|
||||||
|
- [ ] `GET /ws` with valid `Authorization` header upgrades to WebSocket
|
||||||
|
- [ ] `GET /ws` without valid token returns 401 (no upgrade)
|
||||||
|
- [ ] On successful connect, client receives `feed.update` notification with current state
|
||||||
|
- [ ] Multiple connections from same user all receive updates
|
||||||
|
|
||||||
|
3. **FeedEngine Lifecycle**
|
||||||
|
- [ ] First connection for a user creates FeedEngine with default sources
|
||||||
|
- [ ] Subsequent connections reuse the same FeedEngine
|
||||||
|
- [ ] FeedEngine is destroyed after 30 minutes of inactivity
|
||||||
|
- [ ] Activity (any WebSocket message) resets the TTL
|
||||||
|
|
||||||
|
4. **JSON-RPC Methods**
|
||||||
|
- [ ] `location.update` updates LocationSource and triggers feed refresh
|
||||||
|
- [ ] `feed.refresh` triggers manual refresh
|
||||||
|
- [ ] Both return `{ "ok": true }` on success
|
||||||
|
- [ ] Invalid method returns JSON-RPC error
|
||||||
|
|
||||||
|
5. **Feed Updates**
|
||||||
|
- [ ] FeedEngine subscription pushes updates to all user's WebSocket connections
|
||||||
|
- [ ] Updates include `context`, `items`, and `errors`
|
||||||
|
|
||||||
|
## Implementation Approach
|
||||||
|
|
||||||
|
### Phase 1: Project Setup
|
||||||
|
1. Create `apps/aris-backend` with Hono
|
||||||
|
2. Configure TypeScript, add dependencies (hono, better-auth, postgres driver)
|
||||||
|
3. Set up database connection and BetterAuth
|
||||||
|
|
||||||
|
### Phase 2: Authentication
|
||||||
|
4. Configure BetterAuth with email/password provider
|
||||||
|
5. Mount BetterAuth routes at `/api/auth/*`
|
||||||
|
6. Create session validation helper for extracting user from token
|
||||||
|
|
||||||
|
### Phase 3: FeedEngine Manager
|
||||||
|
7. Create `FeedEngineManager` class:
|
||||||
|
- `getOrCreate(userId): FeedEngine` - returns cached or creates new
|
||||||
|
- `touch(userId)` - resets TTL
|
||||||
|
- `destroy(userId)` - manual cleanup
|
||||||
|
- Internal TTL cleanup loop
|
||||||
|
8. Factory function to create FeedEngine with default sources
|
||||||
|
|
||||||
|
### Phase 4: WebSocket Handler
|
||||||
|
9. Create WebSocket upgrade endpoint at `/ws`
|
||||||
|
10. Validate `Authorization` header before upgrade
|
||||||
|
11. On connect: register connection, send initial feed state
|
||||||
|
12. On disconnect: unregister connection
|
||||||
|
|
||||||
|
### Phase 5: JSON-RPC Handler
|
||||||
|
13. Create JSON-RPC message parser and dispatcher
|
||||||
|
14. Implement `location.update` method
|
||||||
|
15. Implement `feed.refresh` method
|
||||||
|
16. Wire FeedEngine subscription to broadcast `feed.update` to all user connections
|
||||||
|
|
||||||
|
### Phase 6: Connection Manager
|
||||||
|
17. Create `ConnectionManager` to track WebSocket connections per user
|
||||||
|
18. Broadcast helper to send to all connections for a user
|
||||||
|
|
||||||
|
### Phase 7: Integration & Testing
|
||||||
|
19. Integration test: auth → connect → location update → receive feed
|
||||||
|
20. Test multiple connections receive same updates
|
||||||
|
21. Test TTL cleanup
|
||||||
|
|
||||||
|
## Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/aris-backend/
|
||||||
|
├── package.json
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # Entry point, Hono app
|
||||||
|
│ ├── auth.ts # BetterAuth configuration
|
||||||
|
│ ├── db.ts # Database connection
|
||||||
|
│ ├── ws/
|
||||||
|
│ │ ├── handler.ts # WebSocket upgrade & message handling
|
||||||
|
│ │ ├── jsonrpc.ts # JSON-RPC parser & types
|
||||||
|
│ │ └── methods.ts # Method implementations
|
||||||
|
│ ├── feed/
|
||||||
|
│ │ ├── manager.ts # FeedEngineManager (TTL cache)
|
||||||
|
│ │ ├── factory.ts # Creates FeedEngine with default sources
|
||||||
|
│ │ └── connections.ts # ConnectionManager (user → WebSocket[])
|
||||||
|
│ └── types.ts # Shared types
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4",
|
||||||
|
"better-auth": "^1",
|
||||||
|
"postgres": "^3",
|
||||||
|
"@aris/core": "workspace:*",
|
||||||
|
"@aris/source-location": "workspace:*",
|
||||||
|
"@aris/source-weatherkit": "workspace:*",
|
||||||
|
"@aris/data-source-tfl": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open Questions (Deferred)
|
||||||
|
|
||||||
|
- User source configuration storage (database schema)
|
||||||
|
- Rate limiting on WebSocket methods
|
||||||
|
- Reconnection handling (client-side concern)
|
||||||
|
- Horizontal scaling (would need Redis for shared state)
|
||||||
Reference in New Issue
Block a user