@trylight/app 0.1.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.
@@ -0,0 +1,58 @@
1
+ // The app's server half, bound once in `src/server/light.ts`: its database,
2
+ // its door (sign-in through Plaza), and the routes the SDK serves (entities,
3
+ // contracts, chat). Server-only.
4
+ import { drizzle } from "drizzle-orm/postgres-js";
5
+ import postgres from "postgres";
6
+ import { createMiiAuthCore } from "@shubh90/app-runtime/auth/core";
7
+ import { createMiiChatHandler } from "@shubh90/app-runtime/chat/server";
8
+ import { createMiiInvoke } from "@shubh90/app-runtime/invoke/server";
9
+ import { createEntitiesHandler } from "./entities.js";
10
+ import { env } from "./env.js";
11
+ import { readSession, signIn } from "./session.js";
12
+ export { cookieAttributes, safeNext, SESSION_COOKIE } from "@shubh90/app-runtime/auth/core";
13
+ export { defineContracts, InvocationError } from "@shubh90/app-runtime/invoke/server";
14
+ export { createHealthHandler, createSentryTunnelHandler } from "@shubh90/app-runtime/server";
15
+ /** Connections one server process holds open to the app's database. */
16
+ const POOL_MAX = 5;
17
+ export function createLightServer(options) {
18
+ const auth = createMiiAuthCore();
19
+ let database = null;
20
+ const db = () => {
21
+ if (database === null) {
22
+ const { databaseUrl } = env();
23
+ if (databaseUrl === undefined)
24
+ throw new Error("DATABASE_URL is not set: point it at a Postgres database (.env.local).");
25
+ database = drizzle(postgres(databaseUrl, { max: POOL_MAX }), { schema: options.schema });
26
+ }
27
+ return database;
28
+ };
29
+ const session = (request) => readSession(auth, request);
30
+ const invoke = createMiiInvoke(auth, options.contracts);
31
+ const chat = createMiiChatHandler(auth);
32
+ return {
33
+ auth,
34
+ session,
35
+ /** Whether sign-in is configured (a bare local dev server has none). */
36
+ configured: () => env().configured,
37
+ /** Email and password, checked by Plaza; the session to set as a cookie, or why not. */
38
+ signIn: (email, password) => signIn(auth, email, password),
39
+ /** The app's database, typed by its schema: `light.db.select().from(companies)`. */
40
+ get db() {
41
+ return db();
42
+ },
43
+ /** `/api/light/entities/*`: what `app.table(...)` calls. */
44
+ entities: createEntitiesHandler({ schema: options.schema, db, session }),
45
+ /** Server-side starts and reads of the app's contracts. */
46
+ invoke,
47
+ /** `/api/mii/invoke/*`: the contract buttons, forwarded to Plaza as the person. */
48
+ invokeRoute: needsSignIn(invoke.handler),
49
+ /** `/api/mii/chat/*`: the chat bubble's calls to the org's agents. */
50
+ chatRoute: needsSignIn(chat)
51
+ };
52
+ }
53
+ /** A route that forwards to Plaza as the person: nothing to forward without sign-in. */
54
+ function needsSignIn(handler) {
55
+ return (request) => env().configured
56
+ ? handler(request)
57
+ : Promise.resolve(Response.json({ error: "Sign-in isn't configured for this app.", code: "auth_not_configured" }, { status: 503 }));
58
+ }
@@ -0,0 +1,18 @@
1
+ import { type MiiAuthCore, type MiiUser } from "@shubh90/app-runtime/auth/core";
2
+ export type SessionUser = Pick<MiiUser, "id" | "name" | "email" | "avatarUrl" | "role">;
3
+ export type SessionState = {
4
+ user: SessionUser | null;
5
+ mode: "public" | "members";
6
+ };
7
+ /** The session a request's cookie names, and whether the app is members-only. */
8
+ export declare function readSession(auth: MiiAuthCore, request: Request): Promise<SessionState>;
9
+ export type SignIn = {
10
+ ok: true;
11
+ token: string;
12
+ expiresAt: Date;
13
+ } | {
14
+ ok: false;
15
+ message: string;
16
+ };
17
+ /** The login form's whole job: Plaza checks the password, the app starts a session. */
18
+ export declare function signIn(auth: MiiAuthCore, email: string, password: string): Promise<SignIn>;
@@ -0,0 +1,36 @@
1
+ // Who is asking, as the app's door sees it, and signing in through Plaza.
2
+ import { signInWithPlaza } from "@shubh90/app-runtime/auth/core";
3
+ import { readCookie } from "@shubh90/app-runtime/chat/server";
4
+ import { env } from "./env.js";
5
+ /** The session a request's cookie names, and whether the app is members-only. */
6
+ export async function readSession(auth, request) {
7
+ // No Plaza to sign in with: a bare local dev server has no door; any other
8
+ // server without its sign-in config keeps the door shut.
9
+ const { configured, local } = env();
10
+ if (!configured)
11
+ return { user: null, mode: local ? "public" : "members" };
12
+ const token = readCookie(request.headers.get("cookie"), auth.SESSION_COOKIE);
13
+ let found = null;
14
+ if (token !== null)
15
+ found = await auth.resolveSession(token);
16
+ // Plaza unreachable on a cold start throws: an error page, not a door that guesses.
17
+ const mode = await auth.getAuthMode();
18
+ if (found === null)
19
+ return { user: null, mode };
20
+ const { id, name, email, avatarUrl, role } = found;
21
+ return { user: { id, name, email, avatarUrl, role }, mode };
22
+ }
23
+ /** The login form's whole job: Plaza checks the password, the app starts a session. */
24
+ export async function signIn(auth, email, password) {
25
+ if (!env().configured)
26
+ return { ok: false, message: "Sign-in is not configured." };
27
+ const result = await signInWithPlaza(email, password);
28
+ if (!result.ok)
29
+ return { ok: false, message: result.failure.message };
30
+ const member = await auth.upsertMemberFromIdentity(result.identity);
31
+ if (member.status !== "active") {
32
+ return { ok: false, message: "Your account is not active in this workspace." };
33
+ }
34
+ const session = await auth.issueSession(member.id, result.session);
35
+ return { ok: true, ...session };
36
+ }
package/dist/vite.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from "vite";
2
+ export declare function lightDb(): Plugin;
package/dist/vite.js ADDED
@@ -0,0 +1,62 @@
1
+ // The dev server half of "writing the schema file is the migration": each
2
+ // time a file under db/schema changes (and when the server starts), it runs
3
+ // `light-db sync`, which applies the change within a second or two. A sync
4
+ // that fails (a migration the rows refuse) shows in the dev server's log and
5
+ // the browser's error overlay; the agent hears it by running `pnpm db:sync`
6
+ // itself after editing db/schema, as AGENTS.md says.
7
+ import { spawn } from "node:child_process";
8
+ import { join, relative, sep } from "node:path";
9
+ /** Edits land as bursts of saves; one sync after the last. */
10
+ const SETTLE_MS = 300;
11
+ export function lightDb() {
12
+ return {
13
+ name: "light-db",
14
+ // The dev server only: never a build, never vitest's server.
15
+ apply: (_config, env) => env.command === "serve" && env.mode !== "test",
16
+ configureServer(server) {
17
+ const schemaDir = join(server.config.root, "db", "schema");
18
+ let timer;
19
+ server.watcher.add(schemaDir);
20
+ server.watcher.on("all", (_event, file) => {
21
+ const path = relative(schemaDir, file);
22
+ if (path === ".." || path.startsWith(`..${sep}`))
23
+ return;
24
+ clearTimeout(timer);
25
+ // Overlapping syncs queue on the database's lock; this only skips the burst.
26
+ timer = setTimeout(() => void runSync(server), SETTLE_MS);
27
+ });
28
+ void runSync(server);
29
+ }
30
+ };
31
+ }
32
+ function runSync(server) {
33
+ const root = server.config.root;
34
+ const logger = server.config.logger;
35
+ return new Promise((resolve) => {
36
+ const child = spawn(join(root, "node_modules", ".bin", "light-db"), ["sync"], {
37
+ cwd: root,
38
+ env: process.env
39
+ });
40
+ let output = "";
41
+ child.stdout.on("data", (chunk) => (output += chunk));
42
+ child.stderr.on("data", (chunk) => (output += chunk));
43
+ child.on("error", (error) => {
44
+ output += error.message;
45
+ });
46
+ child.on("close", (code) => {
47
+ const text = output.trim();
48
+ if (code === 0) {
49
+ if (text !== "")
50
+ logger.info(`[light-db] ${text}`, { timestamp: true });
51
+ }
52
+ else {
53
+ logger.error(`[light-db] ${text}`, { timestamp: true });
54
+ server.ws.send({
55
+ type: "error",
56
+ err: { message: text, stack: "", plugin: "light-db", id: "db/schema" }
57
+ });
58
+ }
59
+ resolve();
60
+ });
61
+ });
62
+ }
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@trylight/app",
3
+ "version": "0.1.0",
4
+ "description": "The Light SDK every v2 org app depends on: entities (Drizzle tables with a typed client), sign-in, contracts and in-app chat.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/shubh9/mii.git",
13
+ "directory": "packages/light-app"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "bin"
18
+ ],
19
+ "bin": {
20
+ "light-db": "./bin/light-db.mjs"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/client.d.ts",
25
+ "default": "./dist/client.js"
26
+ },
27
+ "./schema": {
28
+ "types": "./dist/schema.d.ts",
29
+ "default": "./dist/schema.js"
30
+ },
31
+ "./server": {
32
+ "types": "./dist/server/index.d.ts",
33
+ "default": "./dist/server/index.js"
34
+ },
35
+ "./platform": {
36
+ "types": "./dist/platform.d.ts",
37
+ "default": "./dist/platform.js"
38
+ },
39
+ "./vite": {
40
+ "types": "./dist/vite.d.ts",
41
+ "default": "./dist/vite.js"
42
+ }
43
+ },
44
+ "peerDependencies": {
45
+ "@tanstack/react-query": ">=5.69",
46
+ "drizzle-orm": ">=0.45",
47
+ "postgres": ">=3",
48
+ "react": ">=19",
49
+ "react-dom": ">=19",
50
+ "vite": ">=7",
51
+ "zod": ">=4"
52
+ },
53
+ "dependencies": {
54
+ "@shubh90/app-runtime": "^0.10.3",
55
+ "drizzle-kit": "0.31.11",
56
+ "drizzle-zod": "^0.8.3",
57
+ "tsx": "^4.23.15"
58
+ },
59
+ "devDependencies": {
60
+ "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1",
61
+ "@tanstack/react-query": "^5.104.0",
62
+ "@types/node": "^22.20.4",
63
+ "@types/react": "^19.3.0",
64
+ "drizzle-orm": "^0.45.3",
65
+ "eslint": "^10.11.0",
66
+ "postgres": "^3.4.9",
67
+ "react": "^19.3.0",
68
+ "react-dom": "^19.3.0",
69
+ "typescript": "~5.9.3",
70
+ "typescript-eslint": "^8.70.1",
71
+ "vite": "^8.2.0",
72
+ "zod": "^4.6.5"
73
+ },
74
+ "scripts": {
75
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
76
+ "typecheck": "tsc --noEmit",
77
+ "lint": "eslint src test --max-warnings 0",
78
+ "test": "node --test --import tsx \"test/**/*.test.ts\"",
79
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build"
80
+ }
81
+ }