Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
21da04a
chore(deps): add better-auth 1.7.5 and the drizzle adapter
claude Sep 22, 2026
cb5f334
chore: merge claude/chore-app-skeleton-3
claude Sep 22, 2026
1cf8f2f
feat(identity): company roles and authorize() (test-first), organizat…
claude Sep 22, 2026
e4d28d2
feat(db): auth schema (Better Auth) and app.requests with tenant policy
claude Sep 23, 2026
c67654e
feat(db): migration for auth tables and app.requests
claude Sep 23, 2026
0364a58
wip(identity,tenancy): withTenant, auth wiring and integration tests
claude Sep 23, 2026
0958d25
feat(identity,tenancy): invite-only Better Auth, withTenant, forced R…
claude Sep 23, 2026
4992b90
feat(app): auth route, login/sign-up/invite pages, demo seed
claude Sep 23, 2026
799551e
chore: merge claude/chore-app-skeleton-3
claude Sep 23, 2026
6a3490a
docs: data model with classification, architecture map and changelog …
claude Sep 23, 2026
5dfea7d
chore: merge claude/chore-app-skeleton-3
claude Sep 23, 2026
fce4d6e
chore(deps): add pg-boss 12.33.6
claude Sep 23, 2026
10623ec
feat(db,jobs): documents and audit_events tables, intake columns, pg-…
claude Sep 23, 2026
e0afd29
feat(db): migration 0003 intake
claude Sep 23, 2026
40d3c8f
feat(intake): upload validation, fingerprint, atomic submit, upload a…
claude Sep 23, 2026
81ee674
test(intake): atomic submit with injected failure, duplicates, append…
claude Sep 23, 2026
a8a2808
fix(identity): address the fresh security review of PR #31
claude Sep 23, 2026
20aeb0f
chore: merge claude/feat-identity-tenancy-4
claude Sep 23, 2026
c75a955
test(intake): upload/download routes – tenant 404, anonymous 401, typ…
claude Sep 23, 2026
bff8c68
feat(app): requests list, upload form, request detail with downloads;…
claude Sep 23, 2026
02715e0
fix(config): explicit APP_ENV; only local may use the committed auth …
claude Sep 23, 2026
c0da064
chore: merge claude/feat-identity-tenancy-4
claude Sep 23, 2026
fef74ea
chore(ai-service): scaffold uv project with pinned deps and PDF fixtu…
claude Sep 22, 2026
15c1c9d
test(ai-service): add failing grounding verifier tests (red)
claude Sep 22, 2026
77f8371
feat(ai-service): implement deterministic grounding verifier
claude Sep 22, 2026
e18d8b7
test(ai-service): add failing parser tests for EML, PDF and type dete…
claude Sep 22, 2026
50fe234
feat(ai-service): parse EML and PDF into segments with stable locators
claude Sep 22, 2026
75e139a
test(ai-service): add failing tests for prompt rendering and the Vert…
claude Sep 22, 2026
12d7ac6
feat(ai-service): add versioned prompt and fail-closed Gemini model c…
claude Sep 22, 2026
7b98b18
test(ai-service): add failing pipeline, API and contract tests (red)
claude Sep 22, 2026
5f23ee6
feat(ai-service): expose POST /v1/extract with bearer auth, JSON logs…
claude Sep 22, 2026
b4519ad
fix(ai-service): never echo rejected settings values in startup errors
claude Sep 22, 2026
450af28
build(ai-service): add Dockerfile (python:3.13-slim, uv, non-root, uv…
claude Sep 22, 2026
3e22e5a
docs(ai-service): document run, env vars, contract, verified and unve…
claude Sep 22, 2026
7de6d3d
test(ai-service): pin the verbatim-injection-quote limitation; clarif…
claude Sep 22, 2026
78b0e52
feat(extraction): TS types generated from the AI-service contract, dr…
claude Sep 23, 2026
66ab13c
wip(intake): review fixes – pg-boss into src/db, body bounds, OOXML c…
claude Sep 23, 2026
2a0b891
fix(intake): address the fresh security review of PR #32
claude Sep 23, 2026
a162aa9
test: rate-limit assertion matches Better Auth's expanded IPv6 key
claude Sep 23, 2026
20942a3
chore: merge claude/feat-intake-upload-5
claude Sep 23, 2026
d805a9a
refactor(db): pg-boss pool factory in its own file, covered by the no…
claude Sep 23, 2026
8c19a21
chore: merge claude/feat-intake-upload-5
claude Sep 23, 2026
afb34e6
test(ai): ISO date normalisation, word-boundary text and ambiguous da…
claude Sep 23, 2026
633a779
fix(ai): return ISO dates and trimmed text, match text on word bounda…
claude Sep 23, 2026
ef330f1
test(ai): dev flag together with VERTEX_PROJECT must fail closed (red)
claude Sep 23, 2026
4a63df4
fix(ai): refuse to start when the Gemini dev flag and VERTEX_PROJECT …
claude Sep 23, 2026
a1437bc
test(ai): auth and length checks before the body is read, 500 with re…
claude Sep 23, 2026
e2bea12
fix(ai): check token and Content-Length before reading the body; 500 …
claude Sep 23, 2026
16971c0
test(ai): PDF page cap and fail-closed layout pipeline at startup (red)
claude Sep 23, 2026
5d2a686
fix(ai): cap PDF pages and build the layout pipeline at startup
claude Sep 23, 2026
8cbddf8
docs(ai): document early request checks, value normalisation, page ca…
claude Sep 23, 2026
3b6e72b
chore(extraction): regenerate AI service contract types
claude Sep 23, 2026
0ca5ea9
test(ai): request guard must also apply behind a proxy root path (red)
claude Sep 23, 2026
a749ef4
fix(ai): apply the request guard to the route path, not the raw path
claude Sep 23, 2026
22f6ddb
docs(ai): mark the qpdf stderr level as not established; guard behind…
claude Sep 23, 2026
e10e5ee
style(ai): ruff format
claude Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ module.exports = {
severity: "error",
comment: "Feature modules never open connections or run migrations: createDatabase()/runMigrations() belong to the composition roots (src/app/_server, src/*.ts entrypoints). Types and table definitions (src/db/schema) are fine.",
from: { path: "^src/features/" },
to: { path: "^src/db/(index|client|migrate)\\.ts$", dependencyTypesNot: ["type-only"] },
to: { path: "^src/db/(index|client|migrate|job-queue-client)\\.ts$", dependencyTypesNot: ["type-only"] },
},
{
name: "pg-boss-client-only-in-db",
severity: "error",
comment: "pg-boss opens its own pool: construct it only in src/db (job-queue.ts); features use injected JobSender types.",
from: { path: "^src/", pathNot: "^src/db/" },
to: { path: "(^|/)node_modules/pg-boss(/|$)", dependencyTypesNot: ["type-only"] },
},
{
name: "not-to-unresolvable",
Expand Down
25 changes: 25 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@ S3_FORCE_PATH_STYLE=true
# Host port of the local S3 gateway.
S3_PORT=8333

# --- Environment -------------------------------------------------------------------------------
# local | showcase | production. Only `local` accepts the committed local-default auth secret.
APP_ENV=local

# --- Authentication (Better Auth) ---------------------------------------------------------------
# Signs sessions and cookies; at least 32 characters. Generate a real one: `openssl rand -base64 32`.
BETTER_AUTH_SECRET=local-dev-only-secret-change-me-0123456789
# Public base URL of the web app (cookies, redirects, trusted origin).
BETTER_AUTH_URL=http://localhost:3000
# Client IP for the login rate limit. Set to what YOUR reverse proxy writes and list the proxy
# addresses; without trusted proxies only a single-value header is trusted. If the web container is
# reachable without a proxy, clients can forge this header – put a proxy in front (see operations.md).
AUTH_IP_HEADERS=x-forwarded-for
AUTH_TRUSTED_PROXIES=

# Demo seed only (`pnpm seed:demo`): password of the synthetic demo accounts, local use only.
SEED_PASSWORD=demo-password-local-only

# --- Upload limits (intake) ---------------------------------------------------------------------
# Maximum size per file in bytes (default 20 MiB) and files per request (default 10).
UPLOAD_MAX_FILE_BYTES=20971520
UPLOAD_MAX_FILES=10
# Cap of one whole upload request (all files + form overhead); requests without Content-Length are refused.
UPLOAD_MAX_REQUEST_BYTES=41943040

# --- Web ---------------------------------------------------------------------------------------
# Host port of the web container.
WEB_PORT=3000
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ next-env.d.ts
# Python
__pycache__/
*.py[cod]
.venv/
.venv
.pytest_cache/
.ruff_cache/
.mypy_cache/
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ This file records what changes **in the product** – process and session state
## [Unreleased]

### Added
- AI service (`services/ai`): `POST /v1/extract` turns an e-mail or PDF into segments with stable
locators and extracts company, contact person and requested delivery date with evidence; a
deterministic verifier marks every value whose quote is not in the cited segment as `unverified`.
- Upload of a quote request (`/requests`): .eml, .msg, .pdf, .xlsx, .docx up to a configured size;
originals stored privately, download only for the own company. Request, documents, audit entry and
the processing job are created in one step; exact duplicates are flagged and linked.
- Invite-only login (e-mail + password): admins invite staff into their own company and hand over
an invitation link; sign-up without a valid invitation link creates no account. Roles `admin` and `clerk` per company.
- Tenant isolation: every company-owned table has forced row-level security; data access runs
inside `withTenant()`.
- Login rate limit (stored in the database) and `pnpm seed:demo` with two synthetic companies.
- Runnable local stack: `docker compose up` starts PostgreSQL 17, SeaweedFS (S3), a one-shot `setup`
step (migrations + private bucket), the web app and a no-op worker.
- `GET /api/health` reports database and storage status (200 / 503, no connection details).
Expand Down
3 changes: 3 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ services:
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-local-access-key}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-local-secret-key}
S3_FORCE_PATH_STYLE: "true"
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-local-dev-only-secret-change-me-0123456789}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
APP_ENV: ${APP_ENV:-local}
depends_on:
postgres:
condition: service_healthy
Expand Down
Loading
Loading