Skip to content

feat: initial server implementation#1

Open
cuibonobo wants to merge 15 commits into
mainfrom
claude/nice-ptolemy-Iai17
Open

feat: initial server implementation#1
cuibonobo wants to merge 15 commits into
mainfrom
claude/nice-ptolemy-Iai17

Conversation

@cuibonobo
Copy link
Copy Markdown
Member

Reference server implementation for the Haverstack stack spec (docs/spec.md in haverstack/core).

Stack

  • Hono v4 — TypeScript-first HTTP framework, @hono/node-server for Node.js
  • @haverstack/adapter-sqlite — SQLiteAdapter for storage
  • Pino — structured logging with pino-pretty in dev
  • Vitest — integration tests against a real in-process stack

Endpoints

All 24 endpoints from the spec plus GET /health:

Group Endpoints
Discovery GET /.well-known/stack
Health GET /health
Records GET /records, POST /records/query, POST /records, GET /records/:id, PATCH /records/:id, DELETE /records/:id
Permissions GET /records/:id/permissions, PUT /records/:id/permissions
Associations GET /records/:id/associations, POST /records/:id/associations, DELETE /records/:id/associations
Versions GET /records/:id/versions, GET /records/:id/versions/:version, POST /records/:id/restore/:version
Types GET /types, GET /types/:id, POST /types
Attachments POST /attachments, GET /attachments/:fileId, DELETE /attachments/:fileId
Entity GET /entity, PATCH /entity

Notable implementation details

AuthAUTH_TOKENS=token:entityId,... env var; bearer token middleware maps token → entityId. Unauthenticated requests get null auth and are subject to permission checks.

Permission enforcementsrc/lib/access.ts implements the full permission model: private (owner only), public, entity (direct entityId match), and group (walks _group record's relationship associations for member/admin).

Version snapshotting — server calls adapter.saveVersion() before every PATCH, capturing the previous state. APIAdapter.saveVersion() is a no-op per spec; snapshotting is the server's responsibility.

Attachments — content-addressed storage: file ID = SHA-256 of bytes. Uploading identical content twice returns the same fileId without writing a second copy. Content-Disposition header accepted on upload to store an original filename; returned on download if stored. Upload size limit via MAX_ATTACHMENT_BYTES (default 50 MB). adapter.deleteAttachment() handles file removal so the route layer is clean.

MIME types — stored verbatim at upload time and retrieved via adapter.getAttachmentMeta(), avoiding any extension-mapping round-trip.

Configuration

Variable Default Description
DB_PATH Path to SQLite file (required)
AUTH_TOKENS token:entityId pairs, comma-separated (required)
ENTITY_ID Owner entity ID (required on first run)
PORT 3000 Listen port
TIMEZONE UTC IANA timezone (used on first run)
CORS_ORIGINS * Allowed origins
BASE_URL Canonical server URL
MAX_ATTACHMENT_BYTES 52428800 Upload size limit (50 MB)

Test plan

  • pnpm test passes — integration tests cover all route groups using an in-process Hono app backed by a real SQLiteAdapter in a per-test temp directory
  • pnpm typecheck passes
  • DB_PATH=./stack.db ENTITY_ID=test AUTH_TOKENS=tok:test pnpm dev starts and GET /health returns { status: "ok" }
  • GET /.well-known/stack returns correct capabilities
  • Upload attachment with Content-Disposition, verify filename returned on download

Generated by Claude Code

cuibonobo added 15 commits May 25, 2026 12:35
…d early 404

Replaces the brittle extension-scanning approach (detectMimeType + extToMime)
with a direct DB query via the new optional StackAdapter.getAttachmentMeta method.
When available this gives the exact MIME type stored at upload time and lets the
GET handler return 404 before reading the binary payload.
… deletion

- MAX_ATTACHMENT_BYTES env var (default 50 MB); 413 on oversized uploads
- POST reads filename from Content-Disposition header and passes to adapter
- GET sets Content-Disposition response header when filename is stored
- GET uses meta.size for Content-Length instead of reading buffer length
- DELETE delegates file removal to adapter.deleteAttachment (no more manual scan)
- attachmentRoutes no longer needs dbPath; takes maxAttachmentBytes instead
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant