Phase 0 scaffold: SvelteKit 5 + Drizzle + auth + storage interface

Stack matches sibling buildfor_life_* apps: SvelteKit 5 with adapter-node,
Svelte 5 runes, TypeScript, Tailwind v4 with @theme inline tokens,
PostgreSQL via Drizzle ORM, Argon2id sessions via @node-rs/argon2 and
@oslojs/crypto, EasyMDE ready for wiki/decision markdown, Sharp for
thumbnails.

Included in this commit:
- Config: package.json, svelte.config.js, vite.config.ts, tsconfig.json,
  drizzle.config.ts, .gitignore, .env.example, .gitattributes, .npmrc
- Tenancy schema: companies, users, company_users, sessions
  (10 enums pre-declared for the full domain so downstream migrations
   don't re-diff them; decision_scope widened to include asset +
   work_package per product decision)
- Auth: password hashing + SHA-256-hashed session cookies,
  session lifetime 30d with sliding renewal at T-15d,
  login + logout + session refresh in hooks
- Storage: StorageAdapter interface + LocalDiskStorage with HMAC-signed
  URLs served by /api/files, S3 drop-in with zero schema change
- UI shell: dark-mode bootstrap in app.html identical to siblings,
  sidebar (w-64, h-14 header, amber attention band pattern from repair),
  topbar with breadcrumbs, theme toggle with cross-tab sync via
  storage event, blue-600 primary, responsive drawer
- Routes: (app) authed group with auto-redirect to /login,
  (auth) login group, dashboard placeholder, error page, signed-file API
- Scripts: create-user script for bootstrapping first admin user
- Drizzle: initial migration generated (0000_init.sql)
- Shared agents and skills committed under .claude/; per-user
  permissions gitignored

Typecheck: 0 errors / 0 warnings across 555 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 15:38:14 +07:00
commit 0a3aaa5798
120 changed files with 19771 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import { error } from '@sveltejs/kit';
import { getStorage } from '$lib/server/storage';
import { LocalDiskStorage } from '$lib/server/storage/local';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const storage = getStorage();
if (!(storage instanceof LocalDiskStorage)) {
throw error(501, 'Non-local storage adapter must use its own signed URL');
}
let verified: { key: string; disposition: string; filename: string };
try {
verified = storage.verifySignedUrl(url.searchParams);
} catch {
throw error(403, 'Invalid or expired link');
}
const obj = await storage.get(verified.key).catch(() => null);
if (!obj) throw error(404, 'Not found');
const headers = new Headers({
'content-type': obj.contentType,
'content-length': String(obj.sizeBytes)
});
if (verified.filename) {
const dispHeader =
verified.disposition === 'attachment'
? `attachment; filename="${verified.filename.replace(/"/g, '')}"`
: `inline; filename="${verified.filename.replace(/"/g, '')}"`;
headers.set('content-disposition', dispHeader);
}
return new Response(obj.stream as unknown as ReadableStream, { headers });
};