Files
zpan/server
Jasper Van 28c16eb2b0 feat: Images API /api/ihost/images — two-stage + stream-proxy upload + CRUD (#317)
* feat: add /api/ihost/images CRUD API with two-stage and stream-proxy upload

Implements the full image-hosting CRUD at /api/ihost/images:
- POST (JSON): two-stage upload — creates draft row + returns presigned URL
- POST (multipart): stream-proxy to S3 via PicGo-compatible tool response
- GET /: cursor-based list with optional pathPrefix filter
- GET /🆔 detail with org isolation enforced
- PATCH /:id action=confirm: transitions draft → active, increments quota
- DELETE /🆔 hard-deletes S3 object + DB row, decrements quota

Auth: session (all verbs) or apiKey with image-hosting:upload (POST only).
Path validation: no .., no leading/trailing /, max depth 5, max 256 chars.
Collision: auto-appends 4-hex suffix on (orgId, path) conflict.
MIME gate: allows png/jpeg/gif/webp; rejects svg+xml with 415.
Size gate: max 20 MB enforced at both JSON and multipart paths.

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

* fix(ihost): resolve PR #317 blockers — API key auth, status codes, test coverage

- Blocker 1: replace raw SQL key lookup with auth.api.verifyApiKey() so the
  SHA-256-hashed better-auth API keys are verified correctly
- Blocker 2: add explicit pre-checks in JSON branch returning 413 for size
  > 20 MB, 415 for SVG/unsupported MIME before falling through to zod (which
  was returning 400 for all of these); also guard non-JSON content type → 415
- Blocker 3: replace raw insertApiKey() SQL helper with createTestApiKey()
  that calls auth.api.createApiKey() server-side so tests use properly hashed
  keys; fix expected status codes (401 for missing permission, 415/413); add
  quota-refund assertion in S3 failure test; add quota exceeded confirm test
- Additional: handle selectStorage failure → 503, use Number.isFinite guard
  for Content-Length, add null guards to getOrgId/getUserId test helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ihost): correct rebase conflicts — merge T5 schema + remove duplicate tables

- Merge T5's image_hosting_configs/image_hostings FK constraints with CRUD
  service (resolveActiveImageByToken + incrementAccessCount) and CRUD schemas
- Remove duplicate table definitions in test/setup.ts left by rebase conflict
  resolution (keep T5's FK-constrained versions, add apikey table once)
- Fix org-isolation test: insert a real organization row to satisfy the
  image_hostings.org_id FK constraint added by T5

Agent-Profile: https://agent-kanban.dev/agents/$AK_AGENT_ID

* chore: trigger CI on rebased PR #317

Agent-Profile: https://agent-kanban.dev/agents/$AK_AGENT_ID

* chore(ihost): add export comment to trigger CI sync event

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ihost): add targeted tests to meet 95% patch coverage gate

Cover previously uncovered patch lines:
- s3.ts: add putObject unit test (was 0% — 3 missing lines)
- ihost.ts: add tests for 503 no-storage, 413 Content-Length header,
  415 unsupported content-type, 400 zod parse failure, 400 missing
  file field, 415 non-image MIME in multipart, 422 quota exceeded in
  multipart, nanoid fallback after collision retries, validatePath
  edge cases (starts-with-/, ends-with-/, invalid chars, path too long)
- image-hosting.ts: add direct service tests for deleteImageHosting
  null guard and confirmImageHosting with size=0; add validatePath
  tests via multipart path (bypasses zod max-256 guard)
- ihost.ts: remove dead code (unreachable 'Unknown action' branch —
  patchIhostImageSchema discriminated union only allows 'confirm')

Agent-Profile: https://agent-kanban.dev/agents/$AK_AGENT_ID

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 09:18:43 -04:00
..