Captures the owner's requirements and decisions from the planning conversation, current branch/worktree state, open tasks 17-19, pending owner decisions and the controller's rulings so a new agent can resume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 KiB
13 KiB
Task 16 report: apps/server
Status: DONE_WITH_CONCERNS. The code is complete and verified. The permission and Commands docs step is blocked (see Concerns 1).
Branch: task/16 (worktree /home/user/STTracker/.worktrees/t16). Merged feat/phase1 (7f3d0d5, Task 9's jobs.ts fix) first, as the coordinator asked. It was a fast-forward with no conflicts.
Commits
7b0a673feat(core): API support helpers: token list, suggestions, attachment links, connection runs, device auth, file-mode sync6963e29feat(server): REST API, auth, sources, changes, export02b7eedfeat(server): boot: migrate, load plugins, worker, scheduler, daily backup, web placeholder348b04edocs: server architecture, API decisions, follow-ups0a54020test(server): silence the broken fixture plugin warning
What was built
config.ts:loadConfig(env)returns DATABASE_URL (defaultsqlite://${DATA_DIR}/sttracker.db), DATA_DIR (./data), STTRACKER_SECRET (required, with the exact message), PUBLIC_URL, PORT (3000) and PLUGIN_DIRS (plugins,${DATA_DIR}/plugins).http.ts:route()(zod body and query, with types inferred),apiHandler, auth modesnone|session|token:*|any:*, thestt_sessioncookie (HttpOnly, SameSite=Lax, Secure on an https PUBLIC_URL), the CSRF headerX-Requested-With: sttrackeron cookie writes, body caps (JSON 1 MB, multipart 50 MB, counted while streaming, 413validation), a limit of 10 login/register attempts per minute per IP (429rate_limited), and the error mapping from ruling 5. Unique violations map to 409conflict. Unknown errors return 500internalwith a fixed message and are logged.app.ts:createApp({db, registry, secret, dataDir, publicUrl?})returns{fetch, routes}./ingest/*returns a 404 JSON error.routes/: auth, entries, engagements, works, lists, copies, custom-fields, attachments, sources, changes, export and meta (openapi.json, about, admin/plugins/reload). Shared pieces live inschemas.tsandform.ts. They cover every route in the brief.openapi.ts: OpenAPI 3.1 built from the route table withz.toJSONSchema. It gives each route its method, path, summary, security, path and query parameters, and request body.boot.ts:nextBackupAtandensureBackupScheduled(lockKeybackup, runAt the next 03:00 local).main.ts: mkdir DATA_DIR/plugins, then createDb, migrate, loadPlugins, worker (sync + backup), scheduler, daily backup, thenBun.servewith routes/api/*and/ingest/*going to the app and/*going to the HTML import. SIGTERM/SIGINT run a clean shutdown.apps/web/index.html: the placeholder required by ruling 1.- Core additions, needed because the API had nothing to call:
auth.listAppTokens(metadata only).entries.listSuggestions.attachments.requireLinkTarget(ownership check per entity, run beforestoreAttachment) andlistAttachmentLinks.readAttachmentnow also returnsfilename.- In
connections:getConnection(hides deleted and other users' connections),isDeletedConnection,listRuns,deviceStartanddevicePoll. syncHandlerfile mode:{connectionId, file:{name, base64}}→runSync{file}mode, recorded as run modefile.jobs.tswas not touched.
Decisions worth review
- Actor: all API writes use actor
"user", including token writes. The change engine only protects fields whose source is"user", so atoken:actor would let the next sync overwrite a script's edit. - Cookie-only routes: tokens,
POST/PATCH/DELETE sources, device auth,import/bundleand admin need a session cookie. App tokens can't mint tokens or handle credentials. - Daily backup re-enqueue: the finishing job still holds lockKey
backup, so an immediate re-enqueue would return null. The next job is queued 5 s after each run, with an hourly idempotent safety net and once at boot. - Request field names:
PATCH list-items/:idtakes{position}, which maps tomoveListItem(core has no note edit).DELETE me/tokens/:id.POST sources/:id/device/polltakes{deviceCode}.- Register requires a password of at least 8 characters.
TDD evidence
- RED for the server, before src existed:
bun test apps/servergaveerror: Cannot find module '../src/app.ts', 0 pass, 1 fail. - RED for the core helpers:
bun test packages/core/test/api-support.test.tsgave 0 pass, 1 fail, 1 error (missing exports). - RED for boot:
bun test apps/server/test/boot.test.tsgave 1 fail, 1 error (missingboot.ts). - GREEN:
bun test apps/server: 23 pass, 0 fail.- The same with Postgres: 40 pass, 0 fail.
api-support: 6 pass on SQLite, 12 pass on both.
- A float4 snag on Postgres:
match_suggestions.scoreisreal, so 0.9 came back as 0.8999999761581421. The test now uses 0.75. The helper itself is unaffected. - Full suite after the merge:
bun test: 307 pass, 0 fail (34 files).TEST_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:55432/postgres bun test: 492 pass, 0 fail.
- Typecheck:
bun run typecheck(tsc --noEmit -p tsconfig.json) is clean. - Test output has no stray lines.
Boot verification (real)
$ STTRACKER_SECRET=test DATA_DIR=$(mktemp -d) PORT=3999 bun apps/server/src/main.ts &
$ curl -s localhost:3999/api/v1/openapi.json | head -c 200
{"openapi":"3.1.0","info":{"title":"STTracker API","version":"1"},"servers":[{"url":"/"}],"components":{"securitySchemes":{"session":{"type":"apiKey","in":"cookie","name":"stt_session","description":"
server log: STTracker listening on http://localhost:3999/ (sqlite, 6 plugin(s))
kill -TERM -> "SIGTERM: shutting down", process exited; DATA_DIR contains plugins/ and sttracker.db
GET / -> placeholder HTML (bundled by Bun)
GET /api/v1/entries (no auth) -> {"error":{"code":"unauthenticated","message":"Sign in first."}}
POST /ingest/x/y -> {"error":{"code":"not_found","message":"No ingest endpoint for this plugin."}}
no STTRACKER_SECRET -> error: Set STTRACKER_SECRET to a long random string. It encrypts source credentials.
bun --hot apps/server/src/main.ts (PORT=3998): served openapi.json, exited cleanly on SIGTERM
Concerns
- AGENTS.md Commands, PERMISSIONS.md and the tool permission files were not updated.
- The auto-mode classifier denied my edit to
.claude/settings.jsonand.gemini/settings.jsonas agent self-modification. I didn't work around it. - AGENTS.md requires PERMISSIONS.md to change in the same commit as the tool files, and allows only
workedrows in Commands. So I left all of them unchanged. - The exact change is recorded in
docs/TODO.md(Now). It needs a human:- Allow
bun run typecheck,bun apps/server/src/main.tsandbun --hot apps/server/src/main.ts. All three were verified this session. - Add matching rows to PERMISSIONS.md, and drop the stale "typecheck failed" row.
- Set the AGENTS.md Commands: Single test
bun test <path>, Typecheckbun run typecheck, Devbun --hot apps/server/src/main.ts(needs STTRACKER_SECRET), Startbun apps/server/src/main.ts.
- Allow
- The auto-mode classifier denied my edit to
- Hidden paused connections. A connection the user paused that never had credentials looks deleted, by Task 9's definition, so it disappears from
GET sources. This is noted in TODO; the fix is an explicitdeleted_at. - Limits deferred to TODO:
- The rate limit is per process and uses the socket IP, so all clients share one key behind a proxy.
- File imports are stored as base64 in
jobs.payload. - OpenAPI has no response schemas.
- Bun FormData filename quirk. Bun's FormData serializer does not escape
"in filenames. ThesafeFilenamesanitizer is unit-tested directly for quotes and CR/LF instead of end to end.
Fix round 1
Status: DONE. The agent permission files were not touched.
Commits
e3a759dfix(core): soft-delete source connections with deleted_ataa8e4dbfix(core): token writes keep their actor in history but are user-owned in field_sources5f7d54bfix(server): token actor, login CSRF, 415, cookie decoding, admin invites, production default0b34ededocs: token actor, connection deleted_at and CSRF decisions; follow-ups
Item by item
deleted_at.- Migration
0003_source_connections_deleted_at(nullable text, withdown) is registered and added toschema.ts. deleteConnectionsets it (still pauses and wipes credentials) and is a 404 the second time.ownedRow,listConnections,updateConnection, the scheduler due-query (jobs.ts, as the ruling allows) andsyncHandler(DropJobError) all ignore deleted rows.isDeletedConnectionand the route-side filter are gone.- Task 9's connections test asserted that a deleted connection is still listed. It now asserts that it is hidden and that
deleted_atis set. - Tests:
- core: paused and resumable; deleted gives 404 on get, update and delete; never scheduled; queued sync dropped;
- API: paused source listed and resumed.
- The
schema.testdown-migration still passes.
- Migration
- Token actor.
fieldOwner(actor)inhistory.tsstores every non-plugin:actor as"user". It is applied inentries.writeEntry,engagements(add and update) andapply.listFieldSources(list ownership read from history). Copies have nofield_sources.authenticateAppTokenreturns{ user, tokenName };verifyAppTokendelegates to it, so its signature is unchanged.http.tssetsctx.actor:token:<name>for Bearer,"user"for sessions. Every write route passes it, now including lists and copies.- Tests:
- core: history has actor
token:scriptandfield_sourceshas"user"; a later sync (pluginflip) leaves the status alone, files a pendingchanged, and still applies the source-owned rating; - API: a write-scope token's PATCH gives history actor
token:scriptandfieldSources.rating"user".
- core: history has actor
- DECISIONS: a new entry supersedes the earlier bullets, because AGENTS.md makes DECISIONS append-only.
- CSRF and 415.
X-Requested-With: sttrackeris required on every non-Bearer, non-GET request, login and register included.- JSON bodies need
application/json; anything else gets 415unsupported_media_type. Empty bodies are still allowed for optional-body routes such assources/:id/run. - Tests cover login, register, Bearer exemption and 415.
- Malformed cookie.
readCookiecatches the decode error.stt_session=%now gives 401 (tested). - Production by default.
- Dev mode is on only when
NODE_ENV === "development"orprocess.execArgv.includes("--hot"). - Deviation:
Bun.argvdoes not contain--hot. I checked: underbun --hot,Bun.argvheld only the bun path and the script, andprocess.execArgvheld["--hot"]. - TODO has a hand-off line for Task 19.
- Dev mode is on only when
DELETE me/tokens/:id.revokeAppTokenreturns a boolean, and the route returns 404 for unknown, foreign or already-revoked ids (tested).listSuggestions. It queries by the resolved canonical id. A test asks by an aliased id; it fails without the fix and passes with it.admin/invites. Nowadmin: true. Tests: a member gets 403 on invites and on plugin reload; an admin can reload.- OpenAPI multipart.
RouteDef.formFieldsis set on attachments. The multipart schema listsfile(binary) plusentity,entityIdandcaption, with the required fields (tested). - Tests.
- Cookie plus a bogus Bearer on a write gives 401.
- A member calling reload gets 403.
- A multipart import over 50 MB gives 413
validation. - A positive test with a write-scope token.
- The attachment test now uses bob's real entry (404), and checks that bob cannot read ana's attachment.
TDD notes
- The core items were RED first:
deleted_attests: 2 fail.- Token actor: missing
authenticateAppTokenexport.
- For items 3, 4 and 7 I wrote the fix before the test. I then confirmed each test catches its bug by reverting the fix temporarily: the readCookie try/catch, the
none-route CSRF exemption, the 415 check and the canonical suggestions id each made their test fail.
Verification
bun run typecheck: clean.bun test: 321 pass, 0 fail (34 files).TEST_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:55432/postgres bun test: 520 pass, 0 fail.- Real boot on a fresh SQLite DATA_DIR:
- migrations up to 0003 applied;
openapi.jsonwas served;- a login POST without the header returned the 403 CSRF error;
- SIGTERM shut it down cleanly.
git status: clean.
Still open
- The permission files and AGENTS.md Commands are still not updated; this needs a human (see Concerns 1 above).
restoreVersion,acceptSuggestionandresolveChangetake no actor, so token calls to them record"user"in history. Logged indocs/TODO.md.