Commit Graph

90 Commits

Author SHA1 Message Date
jarvis bbf4a8e6c0 feat(agent-skills): publish Realmroot ZPan skill 2026-08-15 12:46:37 -04:00
Jasper Van b92df828ab ci: parallelize and isolate test suites (#556)
* ci: parallelize and isolate test suites

* ci: avoid unavailable Playwright video runtime

* ci: shard coverage and cache docker smoke

* ci: balance Playwright shards by test

* ci: smoke test the CLI container

* ci: enforce merged coverage thresholds

* ci: ratchet canonical coverage baseline

* ci: make coverage ratchet lossless

* ci: organize parallel gates by responsibility

* perf(ci): reduce total runner time

* perf(ci): balance runner cost and latency

* perf(ci): suppress passing test logs

* fix(test): make coverage sorting proof deterministic

* perf(docker): exclude test-only build inputs

* perf(ci): scope Docker smokes to packaging changes

* refactor(test): enforce fast test boundaries

* test: isolate coverage ownership

* perf(test): run backend projects concurrently

* perf(ci): separate test layers by runtime

* perf(test): separate integration boundaries

* perf(ci): prioritize test runners

* docs(ci): clarify package scheduling

* test: restore shared Cloudflare mocks

* fix(preview): isolate Cloudflare E2E build config

* fix(auth): bind preview sessions to request origin

* revert: remove ineffective preview auth workaround

* fix(auth): stop signing JWTs on session reads
2026-08-05 15:01:12 -04:00
saltbo 3064403c92 fix(ids): make normalization optional
Keep historical identifiers readable and addressable while generating all new entity IDs with Base62. Reject unsafe external ID references and preserve API keys and device codes during optional normalization.
2026-08-05 13:06:41 -04:00
Jasper Van 17db813d1c feat(ids)!: normalize persistent identifiers to Base62 (#554)
* feat(ids)!: normalize persistent identifiers to Base62

BREAKING CHANGE: historical ZPan-owned IDs and public tokens require the one-time normalization migration; old public links and stored credentials are invalidated without runtime fallback.

* fix(ids): namespace public redirect tokens

Generate share tokens as s plus 11 Base62 characters and image tokens as i plus 11 Base62 characters. Dispatch /r by namespace prefix, reuse the single resolved record, and update migration validation, contracts, documentation, and tests.

* fix(ids): guard future object storage keys

* fix(ids): normalize historical references safely
2026-08-05 00:36:18 -04:00
Jasper Van 4ab0b0922a fix(openapi): whitelist Better Auth downloader Device Flow (#552)
Publish only explicitly registered Better Auth operations in the ZPan product OpenAPI contract while preserving runtime auth routes and discovery.
2026-08-04 16:39:54 -04:00
saltbo 461becc3d3 fix(oauth): synchronize Realmroot client scopes 2026-08-02 17:15:17 -04:00
saltbo bc4a71f691 fix(api-keys): restore legacy Better Auth keys 2026-08-02 15:08:51 -04:00
Jasper Van 1caa4e580c refactor(oauth): replace agent access naming (#549) 2026-08-01 00:32:31 -04:00
saltbo 21305cc660 fix(ci): isolate quick tunnel callback traffic 2026-07-31 11:27:19 -04:00
saltbo 4a9588e551 fix(ci): initialize cloud e2e command errors 2026-07-31 11:03:29 -04:00
saltbo 920d5ee143 fix(ci): retry transient quick tunnel failures 2026-07-31 10:46:43 -04:00
saltbo bb25161011 fix(auth): backfill Better Auth account issuers 2026-07-31 10:16:58 -04:00
saltbo 4027f0c7a6 fix(auth): trust official worker preview origins 2026-07-31 09:42:25 -04:00
saltbo 8402844b5e feat(store): add agent x402 capacity purchases 2026-07-31 09:27:24 -04:00
saltbo d7ba55b9da feat(auth): support external agent resources
Replace bundled agent profiles, API keys, plugin, and skill surfaces with dynamic OAuth client registration, delegated DPoP tokens, discoverable scopes, and Arazzo-backed direct upload workflows.

Refs realmroot/realmroot#115
2026-07-30 10:41:39 -04:00
agent-kanban[bot] e50c19051a docs: publish ZPan Agent Skill (#543)
* docs: publish zpan agent skill

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

* test: cover openapi auth route metadata

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

* fix: align zpan skill restish commands

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

* docs: clarify zpan skill release boundaries

* docs: align zpan upload profile examples

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

* fix: select upload plugin profile via environment

* fix: return zpan upload plugin help

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

---------

Co-authored-by: Noah Reed <noah-reed@mails.agent-kanban.dev>
Co-authored-by: saltbo <saltbo@foxmail.com>
2026-07-29 20:19:16 -04:00
agent-kanban[bot] 4817afecdb feat: migrate content APIs to unified authorization (#534)
Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133

Co-authored-by: Noah Reed <noah-reed@mails.agent-kanban.dev>
2026-07-29 02:11:08 -04:00
agent-kanban[bot] 038e23172b feat: establish unified authorization kernel (#533)
* feat: establish unified authorization kernel

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

* docs(v2.9): sync confirmed agent access plan

---------

Co-authored-by: Ravi Shah <ravi-shah@mails.agent-kanban.dev>
Co-authored-by: saltbo <saltbo@foxmail.com>
2026-07-28 21:34:11 -04:00
Jasper Van b2859ec944 refactor(storage): remove legacy custom host downloads (#528)
* refactor(storage): remove custom host downloads

* test(download): cover private signed URLs end to end
2026-07-27 20:00:43 -04:00
Jasper Van ece5f413be fix(download): force browser-downloadable files to save (#527)
* fix(download): force browser-downloadable files to save

* fix(storage): require signed upload CORS headers

* fix(storage): keep upload metadata out of signatures

* fix(storage): sign bare upload commands

* revert: keep upload metadata in signed commands
2026-07-27 18:47:52 -04:00
saltbo 720f7c93aa feat(storage): redesign backend management 2026-07-23 18:16:54 -04:00
saltbo 91720c76fd feat(storage): add usage projection and management page 2026-07-23 13:36:18 -04:00
saltbo ea26ff0c83 fix(stats): honor traffic ledger history 2026-07-22 12:24:45 -04:00
saltbo 1b82630786 fix(stats): show exact boundary-day data 2026-07-22 11:08:36 -04:00
saltbo 5acbd7e1f8 fix(stats): backfill registration source rollups 2026-07-22 10:40:18 -04:00
saltbo e4750f84ce fix(stats): recover activity from authentication sessions 2026-07-22 10:14:52 -04:00
saltbo e79e03d8fa fix(stats): preserve missing data in dashboard charts 2026-07-22 00:45:17 -04:00
saltbo 498ce73261 fix(stats): split D1 counter queries within limits 2026-07-21 23:56:45 -04:00
saltbo 9e028f860e fix(stats): make D1 backfill exact and bounded 2026-07-21 23:37:24 -04:00
saltbo e55ee53496 refactor(stats): unify audit and fact pipelines
Centralize request audit recording, preserve immutable download-task history, and derive hourly statistics and backfills from the same authoritative sources. Add durable user registration facts so admin deletion no longer destroys signup history.
2026-07-21 23:02:52 -04:00
saltbo 40c5f10d48 feat(admin): show storage change trend 2026-07-21 12:04:42 -04:00
saltbo b6cbc614b8 fix(admin): enforce exact analytics data sources 2026-07-21 02:38:54 -04:00
saltbo 3abf4f231e fix(storage): preserve purge history and track usage deltas
Retain purged matter and image metadata while hiding tombstones from live reads. Record exact storage deltas and build hourly trends only after the opening balance.
2026-07-21 00:29:27 -04:00
saltbo e761b6aec7 feat(webdav): verify derived domains before publishing 2026-07-20 18:54:12 -04:00
saltbo 0bf1f9e973 feat(webdav): derive fixed domain from public URL 2026-07-20 13:45:22 -04:00
saltbo 75d7b50538 feat(webdav): support optional custom domains
Serve canonical root-relative DAV URLs on a configured hostname while preserving the existing /dav mount. Reconcile Cloudflare rewrite rules and Worker custom domains during fork deployments, and expose the effective URL through site options.
2026-07-20 12:59:58 -04:00
Jasper Van 34a801c49b fix(stats): validate required rollup dimensions (#514)
* fix(stats): validate required rollup dimensions

* test(stats): cover dimension validation failure
2026-07-20 12:03:17 -04:00
Jasper Van 53ef1664cb fix(stats): validate snapshot rollup markers by scope (#511) 2026-07-20 10:58:17 -04:00
Jasper Van c85e60f200 fix(analytics): enforce trustworthy offline statistics (#510)
* fix(analytics): enforce trustworthy offline statistics

Separate immutable counters from point-in-time snapshots, expose incomplete coverage instead of synthetic zeroes, and keep browser analytics result-only.

Restore finite Free quota baselines, fail closed for invalid storage quota, reconcile traffic reports fairly, and add production-safe backfill and data-quality diagnostics.

* fix(analytics): preserve global backfill totals

Group generated hourly backfill rows by their projected values so SQLite cannot resolve output aliases to source organization columns and overwrite cross-organization totals.
2026-07-20 10:23:22 -04:00
saltbo 3dc4b170d6 fix(analytics): snapshot mutable report status 2026-07-19 15:17:41 -04:00
saltbo ca654b8d03 fix(analytics): rebuild complete historical counters 2026-07-19 14:57:52 -04:00
saltbo 60f8f64481 refactor(analytics): enforce offline result boundaries
Build versioned UTC hourly rollups, validate analytics facts, and keep dashboard requests result-only with explicit coverage semantics.

Move derived metrics to the server and simplify the admin dashboard around one reconcilable UTC range without export.
2026-07-18 01:58:34 -04:00
Jasper Van 8bd1e55b5c refactor(analytics): replace daily stats with hourly rollups (#505)
* refactor(analytics): replace daily stats with hourly rollups

* fix(analytics): merge hourly inventory sources

* test(analytics): cover rollup failure boundaries

* test(analytics): verify growth dashboard metrics

* test(analytics): cover historical rollup reads

* test(analytics): verify hourly dashboard dimensions

* test(analytics): cover remaining metric boundaries

* test(analytics): cover backfill SQL parsing
2026-07-10 13:26:27 -04:00
Jasper Van 8a8fa3e9e1 fix: repair admin statistics integrity (#502)
* fix: repair admin statistics integrity

* test: add PR 502 preview evidence

* chore: remove preview evidence artifacts
2026-07-10 01:28:23 -04:00
agent-kanban-local[bot] 26c660854a fix: restore preview admin credential repair (#476)
Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0

Co-authored-by: Jordan Park <jordan-park@mails.agent-kanban.dev>
2026-06-24 01:17:35 -04:00
Jasper Van f701f4139a fix(security): resolve CodeQL code-scanning alerts (#472)
Clears all 23 open CodeQL alerts:

- actions/missing-workflow-permissions (19, medium): add a top-level
  least-privilege `permissions: contents: read` to ci.yml and the 7
  deploy workflows. The one CI job that needs `packages: write` already
  declares its own block; all deploys authenticate via static secrets
  (no OIDC / id-token, no repo writes), so read is sufficient.

- js/insecure-randomness (1, high): `genPassword()` built share
  passwords with Math.random(); switch to crypto.getRandomValues() over
  the same unambiguous alphabet (length/charset/uniqueness preserved).

- js/incomplete-url-substring-sanitization (2, high): two test fetch
  stubs routed on `String(url).includes('api.github.com')`; tighten to
  `new URL(url).hostname === 'api.github.com'` — precise and no longer
  flagged.

- js/stack-trace-exposure (1, medium): the E2E S3 mock echoed
  error.message in 500 responses; log server-side and return a generic
  body instead.

Verified: typecheck green; share-dialog/changelog/system.integration
tests pass.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:39:28 -04:00
Jasper Van c1542e6a9c refactor(openapi): drop the committed spec artifact; gen Go client from the full doc (#441)
No more docs/openapi/downloader.json and no curated subset. The Go client is
generated straight from the complete, live /api/openapi.json: a single
scripts/openapi-client.ts boots the in-memory app, reads the whole merged
document, and feeds it to oapi-codegen via a throwaway temp file — only the
generated client.gen.go is committed.

- delete docs/openapi/downloader.json, cmd/oapi-codegen.yaml, and the three
  build/generate/check scripts; replace with one scripts/openapi-client.ts
  (`pnpm openapi:client` / `--check`).
- generate from the full document — every endpoint, no allowlist/prune. The
  only transforms are whole-document mechanics for oapi-codegen (3.1→3.0
  nullable, strip security, declare better-auth's missing path params).
- rename the CI step + package scripts openapi:downloader:* → openapi:client*.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:17:33 -04:00
Jasper Van 91d10f9b97 feat(openapi): global OpenAPI document + Scalar UI, drop hand-written stubs (#440)
* feat(openapi): global OpenAPI document + Scalar UI, drop hand-written stubs

Replace the curated, partly hand-written "downloader" OpenAPI doc with a
single global document generated from the real routes.

- main app → OpenAPIHono; serve the aggregated spec at /api/openapi.json and
  the Scalar reference UI at /api/docs. A resource appears in the doc as soon
  as it is converted to `.openapi()` — no curation, no drift.
- enable better-auth's openAPI plugin; the auth/device flow now documents
  itself at /api/auth/reference instead of hand-written route stubs.
- convert objects.ts and events.ts to self-documenting OpenAPIHono routes;
  RPC types preserved (responses go through unwrap<T>, {id}→:id accessors hold).
- tag operations (Objects/Events/Download Tasks/Downloaders) + top-level tags
  so Scalar groups them.
- delete server/openapi/downloader.ts and its device/object/events stubs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(openapi): merge better-auth schema into one doc; regen Go client from it

Make /api/openapi.json a single fully-generated document and drive the Go
downloader client from it — no hand-written/maintained spec.

- merge better-auth's auto-generated schema (auth.api.generateOpenAPISchema)
  into /api/openapi.json, prefixed under /api/auth. The device-authorization
  flow and the rest of the auth API now appear in one doc + Scalar.
- correct one upstream bug in the merge: better-auth advertises
  POST /device/token as { session, user } but its handler returns the OAuth
  token { access_token, token_type, expires_in } — override that one response
  so the doc and the generated client match reality.
- rewire the Go-client codegen to generate from the merged document: a new
  build-client-spec.ts boots the in-memory app, reads the real merged
  /api/openapi.json, scopes it to the downloader's paths (device + downloads +
  objects), prunes unreferenced components, strips security metadata, and
  downconverts 3.1 nullable unions to 3.0 for oapi-codegen.
- regenerate docs/openapi/downloader.json + cmd/internal/openapi/client.gen.go
  and adapt cmd/internal/client to the regenerated device types (inline request
  bodies, optional pointer/number fields) and the 201-only object create.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:29:16 -04:00
Jasper Van 3402a1e099 refactor(api): RESTful resource-oriented API — drop /admin, status sub-resources, merge audience-split routers (#437)
* refactor(api): RESTful resource-oriented API — drop /admin, status sub-resources, merge audience-split routers

Reorganize the entire HTTP surface around resource abstraction instead of
business/audience abstraction.

- Auth: authMiddleware is now soft + global for /api/*; gating is per-route
  (requireAuth/requireAdmin/requireTeamRole), so one resource path serves
  public, user, and admin callers (no security change — guards moved, not dropped).
- Drop /admin from URLs; merge audience-split routers into one resource each
  (announcements, auth-providers, users, teams, quotas, invite-codes,
  site-invitations, downloaders, branding, audit).
- State transitions -> PUT /:id/status: objects (confirm/trash/restore),
  download-tasks (pause/resume/cancel), background-jobs, image-hosting confirm.
- Verbs -> noun sub-resources: objects/:id/copies, download-tasks/:id/attempts,
  background-jobs/:id/retries, site-invitations/:id/deliveries,
  licensing/pairings + /pairings/:code + refresh-runs, teams/:id/invite-links.
- Config -> /api/site/* (branding, email, options, instance, changelog);
  ihost -> image-hosting; me + profiles + admin/users -> one /api/users
  (the :username slot also resolves the internal id, so the admin UI is unchanged).
- External downloader OpenAPI contract cut over in lockstep.

Frontend (rpc.ts + api.ts) and all integration/CF/unit tests updated to match.
Typecheck (server + src), lint:http, biome, and all 4394 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(downloader): regenerate Go client + sync spec for the new RESTful contract

The Go downloader agent (cmd/) and the BDD spec live in this repo, so they must
move with the API:

- Regenerate docs/openapi/downloader.json and cmd/internal/openapi/client.gen.go
  from the updated server OpenAPI.
- Update the hand-written Go client: heartbeat -> /downloaders/me/heartbeats,
  register -> /downloaders, object confirm -> PUT /objects/:id/status, upload
  complete -> PUT .../status, abort -> DELETE .../uploads/:sid. Drop the now-dead
  union helpers (jsonBody/decodeJSON) and the bytes import.
- spec: drop the obsolete teams invite-token-missing scenario (the route is now
  a path param) and add the auth-providers anon-public-list scenario (the merged
  GET serves the public list to anonymous callers).

gofmt clean, go test (121) pass, lint:spec passes (418 scenarios covered).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(api): cover users admin detail/entitlements + getUser wrapper

Close the patch-coverage gaps from the users-resource merge: add integration
tests for GET /api/users/:id (admin detail, success + 404) and
GET /api/users/:id/entitlements (success + 404), and a unit test for the
getUser() api.ts wrapper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): update Playwright specs + global setup to the new RESTful paths

The e2e specs make direct API calls / response matchers that bypass the SPA, so
they need the new paths too: global-setup storage+options seeding
(/api/storages, /api/site/options), image-host (/api/image-hosting, confirm via
PUT /images/:id/status), object confirm in archive (PUT /objects/:id/status),
announcements and site-invitations (/api/announcements, /api/site-invitations,
/api/site/email). The cloud pairing action:'approve' is the external cloud API,
left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): fix cloud-store instance pairing path to /api/licensing/pairings

The cloud-store spec calls the INSTANCE pairing endpoint directly:
POST /api/licensing/pair -> /api/licensing/pairings and the poll
GET /api/licensing/pair/:code/poll -> GET /api/licensing/pairings/:code.
/api/licensing/status and /binding are unchanged; /api/pairings is the
external cloud API, left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(api): rename /api/site-invitations to /api/invitations

Avoids visual proximity with the /api/site/* config namespace. Top-level
/api/invitations is unambiguous — team invitations are nested under
/api/teams/:id/invitations and invite codes under /api/invite-codes. URL-only
change; the internal site-invitations naming stays (still the accurate concept).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(api): group resources by functional domain (URLs)

Move non-core resources under functional-domain prefixes (not permission):
- /api/site/* absorbs storages, auth-providers, audit-events, licensing,
  invitations, invite-codes (joining branding, email, options, instance, changelog)
- /api/downloads/* = tasks + downloaders (regenerated OpenAPI + Go client)
Core resources stay top-level. Updates app.ts, rpc.ts, OpenAPI doc + Go agent
client, and all integration/CF/e2e tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): mirror functional-domain grouping in http/ and usecases/ dirs

Reorganize source files to match the functional URL domains established for
the routes, so the directory tree reflects the same grouping as the API:

- http/{site,downloads,image-hosting}/ and usecases/{site,downloads,image-hosting}/
- dissolve the permission-based console/ dir — admin resources are grouped by
  domain (site), not by audience
- console/user -> top-level (users is a core resource, not an admin-only one)

Co-located tests move with their sources; relative imports and vi.mock paths
updated for the new depths. Pure file/directory restructure, no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): finish structural cleanup — merge split admin routers, drop rename leftovers

Three follow-ups from the directory-structure review, completing the
one-file-per-resource and domain-named-file conventions:

- Merge the last two audience-split router files into their resource file as a
  second export (matching branding/quotas/invite-codes/site-invitations):
  teams-admin.ts -> teams.ts (adminTeams), licensing-admin.ts -> licensing.ts
  (licensing + licensingAdmin).
- Drop pre-rename filename leftovers now that the dirs carry the domain:
  http/image-hosting/{ihost,ihost-config} -> {images,config};
  http/site/site-invitations -> invitations;
  usecases/site/{site-invitation,site-public-origin} -> {invitation,public-origin};
  usecases/image-hosting/{image-hosting,image-hosting-config} -> {images,config}.
- Group the loose store helpers under the store domain:
  http/{cloud-store-helpers,traffic-metering-utils} -> http/cloud-store/{helpers,traffic-metering}.

Routes and exports unchanged; pure file/structure move. tests + co-located
specs move with their sources. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(api): move announcements under /api/site, co-locate stray tests

Announcements is instance-level, admin-authored content (like branding) — a
site resource, not a top-level one. Move it under the site domain:
- /api/announcements -> /api/site/announcements (mount, RPC base path, api.test, e2e spec)
- http/announcements -> http/site/announcements; usecases/announcement -> usecases/site/announcement

Co-locate the tests that drifted from their sources during the dir reorg
(the 1:1-paired cf-test/unit tests belong next to what they exercise):
- http/storages.cf-test.ts -> http/site/ (next to storages.ts)
- usecases/{license-certificate,license-policy,license-refresh,licensing-admin}.test
  -> usecases/site/ (next to the licensing usecase; imports simplified to ./licensing)

No behavior change beyond the announcements path. Routes/exports otherwise stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(usecases): de-fragment the users and objects domains at the usecase layer

The HTTP layer already serves these as single resources; consolidate their
usecases to match, removing leftover files that mirrored the old split:

- Fold me.ts (avatar) + profile.ts (public lookup) into user.ts — one user
  usecase with self/public/admin sections; drop the stale /api/me/avatar and
  /api/profiles/:username doc comments. Their unit tests move into user.test.ts.
- Fold matter.ts (confirmUpload, draft→active) into object.ts — the objects
  domain is now under one "object" name (the Matter *type* stays in ports/).

Importers updated; no behavior change. server tsc + lint:http + lint:spec clean;
Node 4337 / CF 57 / libsql 6 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(usecases): fold sub-concern usecases into their resource (one file per resource)

Consolidate the usecase layer so each resource is a single source file:

- object.ts absorbs object-upload-session, purge, and save-to-drive (its
  upload-session / recursive-purge / save-to-drive sub-concerns)
- share.ts absorbs share-notification and share-ref

External importers re-pointed (trash, redirect, entry-node, workers/scheduled,
http/share-utils, and the surviving integration/cf tests). share.ts now pulls
copyMatterToOrg/saveShareToDrive from object. share.test.ts asserts the real
notification+email fan-out now that dispatchShareCreated is intra-module.

Shared domain services (storage-usage, cloud-traffic-metering, captcha) stay
separate — they're used by many resources. 5 files removed; no behavior change.
Node 4337 / CF 57 / libsql 6 green; tsc + lint:http + lint:spec clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(http): collapse concern-split integration tests into one per resource

Each resource now has a single Node integration test file; the scenario-split
files fold into their resource's main:

- objects-quota + object-multipart-live -> objects.integration.test.ts
- me + profile -> users.integration.test.ts
- quotas-listing -> quotas.integration.test.ts
- teams-admin -> teams.integration.test.ts
- share-public -> shares.integration.test.ts (share-public.cf-test stays — CF runtime)

Helpers de-duplicated or scoped per describe; all [spec:] breadcrumbs preserved
(lint:spec still 418). 7 files removed, all 4337 tests retained. The multipart-live
block now restoreAllMocks so it exercises the real S3 gateway (latent bug fixed).
Node 4337 / CF 57 / libsql 6 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: finish test-file reorg + convert cloud licensing to a real Playwright e2e

Directory grouping (finishing the reorg): auth tests -> http/auth/, cloud-store
test -> cloud-store/, captcha + signup-mode -> usecases/site/ (with import-depth
fixes the moves needed).

One file per resource at the test layer:
- save-to-drive.integration + purge.integration -> object.integration.test.ts
- save-to-drive.cf-test -> object.cf-test.ts
- share-notification.integration -> share.integration.test.ts
- webdav.e2e (a vitest integration test, not Playwright) -> merged into
  webdav.integration.test.ts

Cloud licensing e2e: e2e-cloud-integration.test.ts was a vitest file mostly
duplicating existing integration coverage (licensing-admin.integration +
licensing-cloud.test) and the pairing e2e already in cloud-store.spec.ts.
Replaced with a real Playwright e2e (e2e/licensing.spec.ts): pair+approve ->
assert a Pro gate opens -> unbind -> assert it closes. Shared pairing helpers
extracted to e2e/helpers.ts (cloud-store.spec now imports them). run-cloud-e2e
runs both cloud specs in one tunnel; CI grep-invert excludes the new title from
the no-cloud run.

tsc + lint:http + lint:spec clean; Node 4337 / CF 57 / libsql 6 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): move the cloud-store domain under store/ (matches /api/store)

Following the dir move: http/cloud-store/* -> http/store/*, the cloud-store +
cloud-traffic-metering usecases -> usecases/store/, and the top-level
cloud-traffic-metering http integration test -> http/store/. The http/cloud-store.ts
barrel now re-exports from ./store/*. All importers + moved-file imports rewired.

tsc + lint:http + lint:spec clean; Node 4337 / CF 57 / libsql 6 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): drop the cloud- prefix under store/ now that the dir carries it

- usecases/store/cloud-store -> store.ts; cloud-traffic-metering -> traffic-metering.ts
- http/store/cloud-store.integration -> store.integration; cloud-traffic-metering
  .integration -> traffic-metering.integration
- the http barrel http/cloud-store.ts -> http/store/index.ts (re-exports from
  ./storefront + ./webhooks); app.ts imports './http/store'

store/ is now uniformly named (storefront/webhooks/helpers/shared/traffic-metering
+ store + index). tsc + lint:http + lint:spec clean; Node 4337 / CF 57 / libsql 6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(e2e): licensing spec asserts the bind/unbind lifecycle, not a pro-only gate

The cloud E2E account is business-tier; its pairing certificate does not grant
open_registration (that's why the old vitest test seeded a local pro cert for
that assertion). Assert the edition-agnostic licensing lifecycle instead:
pairAndApprove (binds + waits active) -> unbind -> /status reports bound:false.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:51:01 -04:00
Jasper Van 705aa67a2d refactor(server): usecase-per-resource — move all handler logic into usecases, lock the http boundary (#435)
* refactor(server): enforce http→usecase boundary + extract storages usecase

Adds an AST-based lint (scripts/lint-http-boundary.ts, `pnpm lint:http`, wired
into CI) that forbids http handlers from reaching into deps ports directly
(`c.get('deps').<port>.<method>()`) — the runtime signal of business logic
leaking into the delivery layer, which dependency-cruiser's import-graph rules
cannot see. It ships with a migration ratchet of the 30 handlers that still
violate: CI fails on any new violation and on any ratcheted file that has become
clean, so the list only shrinks. When empty, the boundary is locked.

Converts storages as the first usecase-per-resource example:
- usecases/storage.ts owns all storage business rules (Community storage limit,
  egress-credit-billing feature gate, activity logging)
- http/storages.ts is now thin: validate → call usecase → serialize
- usecases/storage.test.ts exhausts the branches with fake ports (14 cases)
- storages removed from the ratchet (29 remain)

Behavior preserved: storages.integration.test.ts (24) unchanged and green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract profile + notification resource usecases

Converts two owner/public single-port resources to the usecase-per-resource
convention (handlers now only validate → call usecase → serialize):
- usecases/profile.ts (getPublicProfile) + usecases/notification.ts
  (list/unreadCount/markRead/markAllRead), each with fake-port unit tests
- http/profile.ts, http/notifications.ts no longer touch deps ports
- ratchet: 29 → 27

Behavior preserved: profile + notifications integration suites (23) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract audit + quota + announcement resource usecases

- usecases/audit.ts (listAuditEvents)
- usecases/quota.ts (listQuotaOverview with org-type parsing, getUserQuota with
  personal-org fallback)
- usecases/announcement.ts (user/admin list + CRUD)
Handlers keep only pure input parsing (pagination clamp) + serialization; no
deps-port access. Each usecase has fake-port unit tests (12 cases).
ratchet: 27 → 24

Behavior preserved: audit/quotas/announcements integration suites (65) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract auth-provider/background-job/email-config/invite-code/site-invitation/user usecases

Wave of six independent resources converted to usecase-per-resource (parallel
subagents, centrally verified). Each: new usecases/<resource>.ts holding all
port access + business rules, a thinned handler (validate → call usecase →
serialize, no deps-port access), and fake-port unit tests.

- auth-provider.ts: provider config list/upsert/delete; OIDC validation +
  social-login free-limit gate as outcome unions
- background-job.ts: list/get/cancel/create/retry; keeps port-thrown
  BackgroundJobError mapping
- email-config.ts: masked get / save rows / send-test (send_failed outcome)
- invite-code.ts: list/validate/generate(expiry policy)/delete outcome union
- site-invitation.ts: create/resend/revoke/getByToken; email-before-write
  ordering preserved
- user.ts: admin user status/delete + entitlement CRUD; repo-chosen failure
  statuses threaded through unchanged
ratchet: 24 → 18

Verified: typecheck, lint:http, lint:arch, biome all clean; 90 new unit tests +
124 existing integration tests green (behavior preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract ihost/team/branding/me/trash/system resource usecases

Second parallel wave (centrally verified). Handlers thinned to validate → call
usecase → serialize; all port access + business rules moved into usecases.

- image-hosting.ts (extended) + image-hosting-config.ts: ihost upload/list/delete
  + config CRUD with CF custom-hostname lifecycle; quota→422 preserved
- team.ts: /api/teams + /api/admin/teams (invite links, join, activity feed,
  org logo, admin quota entitlements); role checks + repo-failure threading
- branding.ts (extended): admin write orchestration (logo/favicon upload,
  theme, single audit event) + reset; white_label gating stays in middleware
- me.ts: avatar upload/delete (gateway status passthrough, DB-first delete)
- trash.ts: empty-trash (reuses purge.ts; trash_empty audit only when >0)
- system.ts: instance info, changelog, system-options CRUD (signup/captcha/quota
  validation ordering preserved)

Also removed dead code: the speculative team.ts createTeamGate (the team-create
limit is enforced in auth.ts via licensing.checkTeamLimit; nothing called it).
ratchet: 18 → 10

Verified: typecheck, lint:http, lint:arch, biome clean; 148 new unit tests +
228 integration tests green (behavior preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract cloud-store + licensing-admin + events usecases

Third parallel wave (centrally verified + test fixups).

- cloud-store.ts: storefront reads, checkout/orders, and webhook delivery
  (cloud event token verification + idempotency); binding gate as outcome union
- licensing.ts (extended, admin section): initiatePairing / pollPairing (cert
  verify + rollback) / triggerRefresh / unbindLicense
- events.ts: the multiplexed SSE stream as a (deps, params, signal, emit)
  usecase; the handler owns the ReadableStream/Response and feeds ONE
  AbortController from both teardown paths (request abort + body cancel)

Streaming fix: guard the stream controller so a consumer cancel() — which
already closes it before firing the abort listener — no longer double-closes
(ERR_INVALID_STATE), eliminating the unhandled errors in the events suite.

Test fixups (behavior was correct, verified by integration): cloud-store fake
rebuilt the bound client per request and reset its response queue (singleton
now); licensing-admin unit test forced onto the node env (paseto-ts needs a
real TextEncoder).
ratchet: 10 → 5

Verified: typecheck, lint:http, lint:arch, biome clean; 61 unit + 77 integration
tests green (behavior preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract download-traffic metering into a usecase

traffic-metering-utils.ts was an http helper that read deps off the request and
ran the quota+egress download meter inline. Moves the decision into
cloud-traffic-metering.ts as meterDownloadTraffic / reportDownloadEgress
(deps-first, returning a plain {ok|quota_exceeded|insufficient_credits}
outcome). The http helper stays as a thin Context adapter that resolves the
cloud base URL, calls the usecase (deps passed whole), and renders the 422/402
responses — so its four consumers (shares, objects, redirect, webdav) are
unchanged and the file is now boundary-clean.
ratchet: 5 → 4

Verified: typecheck, lint:http, lint:arch, biome clean; cloud-traffic-metering
unit (13, incl. 3 new download tests) + 104 consumer integration tests
(redirect/objects-quota/share-public/cloud-traffic-metering) green — download
metering behavior preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract redirect + share + object resource usecases

The download-flow consumers. Each download orchestration (resolve → access/
expiry/limit gates → atomic increment → meter → presign → audit) moved into its
resource usecase, which calls meterDownloadTraffic/reportDownloadEgress(deps, …)
directly; the handler computes cloudBaseUrl, manages cookies, and renders the
route-specific 302/JSON/410/422/402 responses from the returned outcome.

- usecases/redirect.ts: /r/:token (ds_ direct share + ih_ image hosting), with
  refer-allowlist + presign-rollback
- usecases/share.ts: public + authed share routes; cookies become usecase
  *decisions* the handler applies (view-dedup, password session); imports
  save-to-drive + share-notification unchanged
- usecases/object.ts: upload sessions, confirm, list/move/trash/restore/delete,
  copy/transfer, download; keeps ObjectUploadSessionError; consolidated two
  identical write-access middlewares
- usecases/share-ref.ts: pure share-token helpers (HMAC ref codec, breadcrumb,
  access gate, presign TTL) moved out of http/share-utils so usecases can import
  them without reaching into http; share-utils re-exports them for handlers
ratchet: 4 → 1 (only webdav remains)

Verified: typecheck, lint:http, lint:arch, biome clean; 152 new unit + 206
integration tests (redirect/shares/share-public/objects/objects-quota) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): extract webdav resource usecase — http boundary fully locked

The last and largest handler (1273 lines, 63 violations). All WebDAV port
orchestration — auth resolution, path/lock/dead-property access, PROPPATCH, PUT
(streamed reservation + rollback), MKCOL, DELETE, MOVE, recursive COPY, and the
GET download metering — moves into usecases/webdav.ts. The handler keeps the
protocol machinery: XML multistatus rendering, status codes (207/201/204/423/
412/409/416), header parsing (Depth/Destination/Range/If/Lock-Token/Overwrite),
basic-auth/API-key parsing, and all streaming Response framing (FixedLengthStream,
single-range 206, multipart/byteranges). The GET path calls meterDownloadTraffic
directly; getWebDavObjectBody returns the S3 body for the handler to stream,
preserving the exact (storage, object[, range]) call shape and refund-on-failure.

ratchet: 1 → 0. `pnpm lint:http` now reports "http boundary fully locked".

Verified: typecheck, lint:http (LOCKED), lint:arch, biome clean; 43 unit + 41
integration tests (the 2027-line webdav spec) green — behavior preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(webdav): preserve api-key rate-limit message; deterministic object test dates

- resolveWebDavAuth now threads the original ApiKeyRateLimitError message
  through its rate_limited outcome so the 429 body stays "Rate limit exceeded."
  (the webdav auth refactor had hardcoded "Rate limited") — restores
  api-keys-rate-limit.integration.test.ts.
- object.test.ts file() used argless new Date() in both the mock and the
  expected value; a shared FIXED_DATE makes the deep-equal deterministic (it
  flaked under full-suite load).

Full suite green: 4327 passed (184 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): isolate the e2e database — own throwaway DB, wiped each run

entry-node and e2e/global-setup both honor DATABASE_URL, but it defaulted to the
shared dev ./zpan.db and nothing wiped it — so a local `pnpm e2e` ran against
(and mutated) the dev database and wasn't clean between runs. playwright.config
now defaults DATABASE_URL to a throwaway .e2e/e2e.db (node runtime; CF uses D1)
and wipes it on every run, and sets reuseExistingServer:false so e2e never
silently reuses a running dev server. CI is unaffected (fresh box; reuse already
off). Opt out by setting DATABASE_URL yourself.

Verified: `pnpm e2e auth.spec.ts` (7 passed) ran on .e2e/e2e.db while ./zpan.db
stayed byte-for-byte unchanged (mtime+size identical).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:47:55 -04:00