mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-19 01:51:11 +08:00
3402a1e0991738d870fb6cb5e21135fa68cf2058
287
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
c2cdf998f4 |
refactor(server): group admin-console resources under http/console + usecases/console (#436)
Moves the wholly-admin resources into a console/ subdirectory in both http/ and usecases/: storages, users, email-config, audit (handler + its dedicated usecase), plus the teams-admin and licensing-admin handlers. The latter two keep their usecases in usecases/ because team.ts and licensing.ts are shared with user-facing routes. Mixed resources that expose both admin and public/user endpoints (quotas, branding, auth-providers, announcements, invite-codes, site-invitations, downloaders) are intentionally left in place — splitting them would re-fragment the one-usecase-per-resource consolidation. Pure file moves + import-path updates; no behavior change. lint:http still "fully locked", lint:arch clean, biome clean, full suite green (4327 passed). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
fbec74747e |
refactor(usecases): consolidate licensing into one file, merge tiny usecases, drop dead code (#434)
Group all license application logic into a single usecases/licensing.ts (certificate/token verification, binding-state, cloud refresh, license-gated policy) and collapse small single-purpose usecases that belonged together. - delete license-entitlement.ts: a write-only cache nobody read (loadEntitlement had zero live consumers); remove its invalidate* call-sites - merge licensing-refresh-runner -> license-refresh, then fold license-certificate + license-refresh + license-policy + binding-state into one licensing.ts (internal cert<-state<-refresh<-policy edges become in-file) - merge team-count + signup-mode -> license-policy (then into licensing.ts) - merge trash-retention -> purge (manual purge + scheduled retention sweep) Tests follow the source: the runner tests are rewritten against a fake LicensingCloud port (the old module-spy on performRefresh can't survive a same-module call), and the team-limit test seeds a real pro license instead of mocking the licensing module. Net 486+/861-. typecheck clean; unit+integration green (158 files / 3797 tests). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
191ee0a07d |
refactor(server): clean architecture migration (hono-cf-clean-arch) (#433)
* refactor(server): rename routes/ to http/ (clean-arch step 1) The HTTP delivery layer was already split per-resource; align the directory name with the hono-cf-clean-arch standard. Pure mechanical move via git mv; updates the three server-side importers (app.ts, image-hosting-domain middleware, openapi/downloader). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): add clean-arch backbone + migrate activity to a repo Introduce the composition root and dependency-injection seam: - usecases/ports.ts (barrel) + usecases/ports/<resource>.ts: framework-free port interfaces and DTOs - usecases/deps.ts: the Deps aggregate consumed via c.get('deps') - composition.ts: createDeps(platform) — the only place adapters are built - app.ts sets deps in request context after platform middleware First adapter: adapters/repos/activity.ts (ActivityRepo) replaces services/activity.ts. All 14 call sites rewired (routes use c.get('deps').activity.*; auth.ts and transitional services construct the repo from db). DTOs are now plain shapes, not drizzle $inferSelect. Behavior-preserving: typecheck + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract StorageRepo + migration tracker services/storage.ts -> adapters/repos/storage.ts (StorageRepo). All 14 callers rewired (http/middleware via c.get('deps').storages.*; transitional services via createStorageRepo(db)). Port DTO reuses the shared Storage contract with Date timestamps; the S3-credential 'Storage' type alias across 9 files now points at StorageRecord. Data-layer test moved next to the repo. Adds docs/clean-arch-migration.md as the living progress tracker. typecheck + lint + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract Profile/Announcement/Notification repos - profile -> ProfileRepo; the pure buildBreadcrumb moves to domain/breadcrumb.ts - announcement -> AnnouncementRepo; notification -> NotificationRepo - All callers rewired (routes via c.get('deps').*; auth.ts + services via create<X>Repo(db)); data-layer tests moved next to their repos - Test infra: createApp accepts an optional deps; createTestApp returns deps so tests fake a port by spying on testApp.deps.* (events SSE failure test no longer spies the service module) typecheck + lint + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract OrgRepo (authz) + InviteRepo - org -> OrgRepo (findPersonalOrg/getMemberRole/canReadOrg/canWriteToOrg/ isPersonalOrg); rewired across 4 routes + 2 auth middlewares + auth.ts - invite -> InviteRepo; rewired invite-codes route + auth.ts - data/unit tests for org & invite moved next to their repos typecheck + lint + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract BackgroundJobRepo (+ BackgroundJobError to ports) background-jobs -> adapters/repos/background-job.ts. The BackgroundJobError (caught by http for status mapping) moves to usecases/ports per the standard. Rewired: background-jobs route + events SSE (deps) + archive-processing (transitional repo). Unit + data tests relocated. typecheck + lint + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract QuotaRepo from effective-quota The foundational quota leaf. effective-quota.ts -> adapters/repos/quota.ts (QuotaRepo); the pure currentTrafficPeriod moves to domain/quota.ts; DTOs (EffectiveQuota, CurrentStoragePlan) move to ports. Rewired 14 callers (http -> deps.quota; services/auth/entry-node/workers.scheduled -> createQuotaRepo). scheduled-worker test now mocks the adapter (createQuotaRepo) instead of the service module. typecheck + lint + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract TeamRepo + TeamInviteRepo team -> adapters/repos/team.ts (TeamRepo; composes QuotaRepo for quota totals); team-invite -> adapters/repos/team-invite.ts. teams-admin + teams routes use c.get('deps').{teams,teamInvites}. Data tests relocated. typecheck + lint + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build(arch): enforce clean architecture via dependency-cruiser (ratchet) in CI Adds .dependency-cruiser.cjs with the full hono-cf-clean-arch rule set and wires pnpm lint:arch into CI. The drizzle-only-in-repos rule uses a shrinking MIGRATION_PENDING allowlist so it passes today while still enforcing every already-migrated layer; each future migration commit removes an entry. platform/ (Database driver type) and auth.ts are permanent named exceptions. Currently green: 222 modules / 926 deps, 0 violations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): combine user + org-entitlements into UserAdminRepo Resolves the pre-existing user <-> org-entitlements import cycle by merging both into adapters/repos/user-admin.ts (UserAdminRepo); shared types (UserWithOrg, QuotaEntitlementItem, UserOperationFailure, entitlement inputs) move to ports. users + teams-admin routes use c.get('deps').userAdmin. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract SiteInvitationRepo site-invitations -> adapters/repos/site-invitations.ts. Route uses c.get('deps').siteInvitations; the email helper now receives siteName from the handler (http stays out of adapters); auth.ts uses the repo. Result-type unions moved to ports. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cf): fix storages.cf-test seed after StorageRepo extraction cf-tests are excluded from typecheck; biome had pruned the transiently-unused createStorageRepo import during the storage migration. Restore the import and convert the platform.db seed calls. test:cf green (57 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): introduce BDD-lite spec/ + spec<->test traceability lint Adds the standard's product-spec layer: - spec/*.feature (Gherkin, no Cucumber runner) — one per capability, scenarios tagged @<capability>/<slug> + layer; spec/README.md documents the convention - [spec: <id>] breadcrumbs on home tests - scripts/lint-spec.mjs + pnpm lint:spec (wired into CI): every scenario id must have a referencing test and every breadcrumb must match a scenario Specced: storages, announcements, notifications, invite-codes, site-invitations (41 scenarios, all traced). Specs grow per capability as the migration proceeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract changelog + cf-custom-hostnames providers Establishes adapters/providers/. changelog (GitHub releases/CHANGELOG) and cf-custom-hostnames (CF for SaaS) move to adapters/providers/ behind ChangelogProvider / CfHostnamesProvider ports (CfConflictError -> ports). system + ihost-config routes use c.get('deps').{changelog,cfHostnames}. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): move db-transaction -> db/, path-template -> lib/ Two framework-free utilities leave services/ for their proper homes: db/transaction.ts (the drizzle batch/transaction helper) and lib/path-template.ts (object-key builder). Importers updated. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): migrate licensing subsystem drizzle to repos license-state -> adapters/repos/license-binding.ts (LicenseBindingRepo); instance-id + instance-info DB reads -> adapters/repos/instance.ts (InstanceRepo). licensing/ (has-feature, refresh, entitlement, instance-info) now uses the repos and imports no drizzle, so ^server/licensing leaves the dependency-cruiser ratchet. licensing-admin route uses c.get('deps').{licenseBinding,instance}; service callers construct the repos; instance-telemetry test mocks the adapter. typecheck + lint + lint:arch + 3807 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): move S3Service to adapters/gateways behind S3Gateway port Establishes adapters/gateways/ + deps.s3. S3Service -> adapters/gateways/s3.ts (implements S3Gateway; S3StorageCredentials -> ports). A thin services/s3.ts re-export shim keeps the http routes (objects/webdav/ihost/share-utils) and the 21 prototype-spy tests working unchanged until those routes migrate to deps.s3; s3-dependent services can now move to usecases using deps.s3. typecheck + lint + lint:arch + 3807 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): drain inline drizzle from me route (avatar -> ProfileRepo) ProfileRepo gains setAvatar; the /api/me avatar handlers use c.get('deps').profiles instead of inline user-table updates. 'me' leaves the dependency-cruiser ratchet. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): drain inline drizzle from quotas route (-> QuotaRepo.listOrgQuotaOverview) The admin quota-overview join moves into QuotaRepo; the route uses c.get('deps').quota. 'quotas' leaves the ratchet. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): SystemOptionsRepo drains auth-providers/system/email-config routes New adapters/repos/system-options.ts (key-value access to systemOptions) + deps.systemOptions. auth-providers, system, email-config routes drop inline drizzle and use c.get('deps').systemOptions; all three leave the ratchet. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): drain inline drizzle from teams route (logo -> TeamRepo.setLogo) TeamRepo gains setLogo; teams route uses c.get('deps').teams for logo set/clear and drops its dead db locals. 'teams' leaves the ratchet. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): drain inline drizzle from ihost-config (-> ImageHostingConfigRepo) New adapters/repos/image-hosting-config.ts + deps.imageHostingConfigs. The ihost-config route's custom-domain CRUD uses c.get('deps').imageHostingConfigs (cf-hostnames already via deps). 'ihost-config' leaves the ratchet. typecheck + lint + lint:arch + 3807 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): loadBindingState -> usecase, hasFeature/effectiveFeatures -> domain Finishes the feature-gate path: domain/licensing.ts (pure hasFeature/effectiveFeatures), usecases/licensing.ts (loadBindingState(deps) using LicenseBindingRepo + cert verify). licensing/has-feature.ts deleted. Rewired 10 callers (routes/middleware via c.get('deps'); services via createLicenseBindingRepo(db)). Tests retargeted to the new modules (domain + usecases licensing). typecheck + lint + lint:arch + 3807 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): extract StorageUsageRepo + storage-usage reservation usecase The quota-reservation crown dependency. adapters/repos/storage-usage.ts (StorageUsageRepo: rollbackReservations + reconcile); usecases/storage-usage.ts (reserveStorageUsage/withStorageUsageReservation/StorageUsageMutationContext taking {quota,storageUsage} deps); StorageQuotaExceededError -> ports. Rewired 9 callers (objects/webdav/ihost routes via c.get('deps'); matter/image-hosting/archive/purge/ save-to-drive via constructed repos). Unblocks the matter/image-hosting clusters. typecheck + lint + lint:arch + 3807 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): migrate 5 leaf service clusters to clean-arch (parallel wave) Extracted 7 services via parallel agents on file-disjoint components: - instance-telemetry -> usecases/instance-telemetry (reuses instance + systemOptions ports) - image-upload -> adapters/gateways/image-upload (ImageUpload port, deps.imageUpload) - archive-jobs -> adapters/gateways/archive-jobs (ArchiveJobsGateway, deps.archiveJobs) - zip-compress + zip-extract -> adapters/gateways/zip + adapters/repos/zip (ZipGateway + ZipPlanRepo) - object-upload-sessions -> adapters/repos/object-upload-session (ObjectUploadSessionRepo) - purge -> usecases/purge (pure usecase over existing s3/storages/storageUsage) Routes (objects/teams/me/internal/background-jobs) now reach these via c.get('deps'); entry files + workers build deps via createDeps(platform). Barrels wired by hand. typecheck + lint:arch (240 modules) + 3810 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): add quotas/profile/licensing feature specs + traceability 29 new scenarios traced to existing integration tests via [spec: id] breadcrumbs. lint:spec: 70 scenarios, all covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): migrate auth/webdav/cloud/branding/image-hosting clusters (parallel wave 2) 17 services extracted via 5 parallel agents on file-disjoint components: - auth-account: email->EmailGateway, share-notification->ShareNotificationRepo, member-count->MemberCountRepo, captcha->domain+usecase, signup-mode/team-count->usecases - webdav-middleware: api-keys/download-tokens gateways, webdav-state/webdav-path repos, webdav-xml->domain (pure) - cloud: licensing-cloud->LicensingCloudGateway, cloud-store/cloud-traffic-report/ remote-download-usage repos (cloud-traffic-metering + licensing-refresh-runner folded in) - branding: pure usecase over existing deps (no new port) - image-hosting: ImageHostingRepo 12 new deps fields wired by hand. WebDavMatterRow DTO moved into the webdav-path port (was importing services/matter, which cycled through the ports barrel); domain WebDavMatter dirtype widened to number|null to match the nullable column. Ratchet shrunk: ihost.ts + middleware/image-hosting-domain.ts no longer touch drizzle. services/ now 26->9 (matter crown). typecheck + lint:arch (261 modules, no cycles) + 3810 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): add users/audit/teams/avatar/background-jobs/events/health specs 64 new scenarios traced to existing integration tests. lint:spec: 133 scenarios, all covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): migrate share/save-to-drive/archive-processing/trash-retention (parallel wave 3) - share -> ShareRepo (+ domain/share, transitional ShareMatterRow DTO); shares.ts now holds ZERO drizzle (dropped from the ratchet) - save-to-drive -> pure usecase over deps (s3/storages/storageUsage/quota/activity/share) - archive-processing -> usecase + ArchiveTargetFolderRepo (archive-jobs gateway self-assembles its deps subset from platform to avoid a composition cycle) - trash-retention -> pure usecase purge gains deps.share for share cascade-delete. 2 new deps fields wired. services/ now 9->5 (matter, matter-name-conflict, downloads, s3 shim, site-public-origin remain). typecheck + lint:arch (265 modules, no cycles) + 3810 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): add branding/email-config/auth-providers/system/image-hosting/webdav/quota-store specs 128 new scenarios traced to existing integration tests. lint:spec: 261 scenarios, all covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): migrate the matter keystone + site-public-origin (wave 4) The crown. matter (644 lines, 17 exports) -> adapters/repos/matter.ts (MatterRepo: full drizzle CRUD + conflict resolution) + usecases/matter.ts (confirmUpload quota-guarded) + usecases/ports/matter.ts (Matter DTO + NameConflictError); matter-name-conflict -> domain. Fan-in of 10 rewired: objects/shares/trash routes now hold ZERO matter drizzle (via deps.matter); webdav + archive-processing/purge/save-to-drive/trash-retention usecases + zip/webdav-path repos repointed. site-public-origin -> domain (pure helpers) + usecase over deps.systemOptions. services/ now 5->2 (only downloads + the s3 shim remain). 1 new deps field (matter). typecheck + lint:arch (268 modules, no cycles) + 3810 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): add redirect + download-tasks specs 44 new scenarios traced to existing integration tests. lint:spec: 305 scenarios, all covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): migrate downloads (remote-download) cluster (wave 5) downloads/{core,mappers,types} (915 lines) -> adapters/repos/{downloader,download-task} (DownloaderRepo + DownloadTaskRepo) + usecases/downloads.ts (assignment + task state machine + remote-download credit billing) + usecases/ports/downloads.ts (DownloadError + DTOs). Rewired download-tasks/downloaders/events routes + objects.ts upload handlers to c.get('deps'). 2 new deps fields. services/ now down to ONLY the s3 shim. typecheck + lint:arch (268 modules) + 3810 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): add shares spec (32 scenarios) lint:spec: 337 scenarios, all covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): delete the s3 shim — services/ is empty, clean-arch complete Routed all 20 S3 call-sites in http (objects/webdav routes + share-utils consumers shares/redirect/ihost/image-hosting-domain) onto c.get('deps').s3; webdav's no-c helpers take an S3Gateway param. Repointed 17 test files off the shim onto adapters/gateways/s3. Deleted server/services/s3.ts — server/services/ is now empty and gone. Ratchet: dropped ^server/services (fully migrated); no-circular now fully enforced with no path exemptions. MIGRATION_PENDING is down to 2 deliberately-deferred files (http/webdav.ts listDescendants, middleware/auth.ts session lookup). Also adds the objects spec (39 scenarios) -> 376 scenarios across 26 capabilities. Final gates: typecheck + lint:arch (267 modules, no cycles) + lint:spec (376) + lint + 3810 tests + 57 cf-tests all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): migrate the last 2 ratchet files — architecture fully locked webdav.ts + middleware/auth.ts were the last files touching drizzle outside repos. - WebDAV: listDescendants/PROPPATCH-touch/PUT-overwrite/COPY-rollback + Basic-Auth username check moved to MatterRepo.{listActiveDescendants,trashByIds,restoreActiveByIds,touch,applyUpload} + UserAdminRepo.{isBanned,matchesUsername}. webdav.ts now imports no drizzle. - Auth middleware: disabled-user (banned) check -> deps.userAdmin.isBanned. Ratchet (MIGRATION_PENDING) is now empty and removed. no-circular + drizzle-only-in-repos are fully enforced with zero exemptions; only platform/, test/, auth.ts remain as permanent named exceptions. New methods covered by existing real-D1 webdav/auth integration tests. typecheck + lint:arch (267 modules) + lint:spec (376) + lint + 3810 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): spec the 4 remaining admin/auth capabilities Closes the spec gaps for capabilities that had routes+tests but no .feature: image-hosting-config (domain/CF custom-hostname admin), licensing-admin (cloud pairing/binding/refresh), teams-admin (team admin + entitlements), auth-username (username sign-up). 42 new scenarios traced to existing integration tests. lint:spec: 418 scenarios across 30 capabilities, all covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(matter): listActiveDescendants uses exact-prefix (SUBSTR) not LIKE Folder names can contain '_'/'%', which LIKE treats as wildcards and would over-match descendants in WebDAV recursive COPY/MOVE. Reuse the repo's existing descendantParentCondition (SUBSTR), consistent with getDescendants/cascadeParentPath. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): address review follow-ups (DTO dedupe, composition, dead locals) - Dedupe transitional DTOs: ShareMatterRow + WebDavMatterRow -> the canonical Matter port DTO (removes hand-copied duplicates + schema-drift risk; no cycle reintroduced). - composition.ts: hoist shared stateless instances (one s3/storages/systemOptions instead of constructing duplicates inline). - Remove the 21 dead 'const db = c.get(platform).db' locals -> biome warning-free. typecheck + lint:arch (267 modules) + lint:spec (418) + 3810 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): dissolve server/licensing into domain + usecases layers server/licensing/ was a feature-grouped dir outside the layer taxonomy — its 3 orchestration files imported adapters directly, escaping usecases-no-infrastructure. Now classified + enforced: - public-keys -> domain/license-keys (pure) - verify + cloud-event-token -> usecases/license-certificate (paseto/zod crypto helpers) - entitlement/instance-info/refresh -> deps-first usecases (license-entitlement, instance-info, license-refresh), using existing deps.{licenseBinding,instance,licensingCloud} 11 consumers rewired to deps; dead db param dropped from runLicensingRefresh. No barrel changes. server/licensing/ deleted — every server file now sits in an enforced layer (or a named exception: platform/test/auth.ts/lib/middleware). typecheck + lint:arch (266 modules) + lint:spec (418) + 3810 tests + 57 cf-tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6521d1b722 |
feat(events): unify SSE stream, replace frontend polling (#432)
* feat(events): unify SSE stream, replace frontend polling Add a single /api/events SSE endpoint that multiplexes domains via named events. Jobs and notifications are always-on; download tasks are an opt-in per-connection subscription carried in the EventSource URL, so the server only polls what an open page needs and a browser tab holds one connection. - Replace refetchInterval polling: sidebar active-job badge, tasks list, notification unread count - Fold the download-tasks SSE into the unified endpoint; remove the now-dead /api/download-tasks/events route, downloadTaskEventsUrl, downloadTasksUrlApi - Client subscription registry: useServerEvents (single connection, reconnects when the merged subscription query changes) + useServerEventSubscription - Decouple the keep-alive heartbeat (25s idle) from the 2s data poll Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloader): regenerate OpenAPI spec/client after dropping /events Removing /api/download-tasks/events from the unified-SSE refactor drifted the generated downloader OpenAPI doc and Go client (caught by openapi:downloader:check in CI). Regenerate both. With the events operation gone, the single-value assignedTo enum constant collapses from GetApiDownloadTasksParamsAssignedToMe to Me, so update the hand-written client reference to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(events): cover unified SSE store and endpoint Codecov flagged the new SSE code as uncovered. - Unit-test the pure server-events-store (merge/sort query key, listener notifications, subscription lifecycle) - Integration-test GET /api/events: 401 unauthenticated, and that an authed user with a queued job receives jobs + notifications events - Exclude useServerEvents.ts (EventSource/React-effect glue, not unit-testable without a DOM) from coverage, matching the existing src/routes & src/components exclusions; its logic lives in the now-covered store Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): stop waiting on networkidle now that SSE is always connected The unified /api/events stream is mounted on every authenticated page, so the network never goes idle and waitForLoadState('networkidle') hangs until the job times out. Drop the four networkidle waits (sign-in helper + image-host reloads); subsequent navigations and element auto-waits already gate readiness. Also broaden the /api/events test to cover the abort and error branches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
df07734da1 |
fix(upload): overwrite-on-replace instead of trashing the incumbent (#431)
Replacing a same-named file trashed the incumbent at create time. Trashed files still count toward quota, so the new file then needed headroom for both copies — replacing a same-size file at high quota 422'd, contrary to normal overwrite semantics (an overwrite frees the old file, it does not go to trash). The early trash also meant a failed/abandoned upload destroyed the existing file. Now createMatter defers the overwrite for a draft 'replace': the incumbent stays active until confirmUpload, which purges it (delete row, S3 object, shares), reserves only the net size increase, and reconciles. A failed upload therefore leaves the incumbent intact, and a same-size replace is net-neutral. The client reuses the create-resolved strategy at confirm so the user is not prompted twice. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7048ecd417 |
fix+refactor: audit bugs (license cache, timing-safe token) + architecture cleanup (#430)
* fix(security): constant-time compare for internal API token The internal telemetry endpoint compared the Bearer token with `!==`, a timing side-channel. Reuse a shared constantTimeEqual helper (extracted from download-tokens.ts so both call sites share one implementation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(licensing): drop cached entitlement once the certificate expires loadEntitlement cached the verified summary for 60s keyed only on wall-clock age, so a certificate expiring mid-window kept granting Pro/Business features until the cache lapsed. On a cache hit, also verify nowSeconds is still before certificateExpiresAt and licenseValidUntil; otherwise re-verify (which yields null for the expired cert). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(ui): dedupe currency and size formatters into @/lib/format Add formatCurrency (locale-aware, cents→currency) and replace four identical local formatMoney copies in the store components. Replace the duplicated formatFileSize/formatTrackSize and the verbatim users-list formatDate with the shared formatSize/formatDate. The null-handling formatDate variants are left alone — they intentionally render '—' for null, which the shared helper does not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(server): dedupe MIME→ext into lib/mime-utils Three services each hand-rolled a MIME_TO_EXT map + lookup. Consolidate into lib/mime-utils.mimeToExt (the superset of all keys, 'bin' fallback); each endpoint keeps its own allow-list and uses the shared lookup for naming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(server): central domain-error → HTTP mapper Routes hand-rolled the same StorageQuotaExceededError→422 and NameConflictError→409 (with NAME_CONFLICT body) mappings — 6 sites in objects.ts, 4 in webdav.ts, 1 in ihost.ts, plus the WebDavPathError mapping. Add lib/http-errors.mapDomainError returning { status, message, json } and use it in each catch (JSON routes render json, WebDAV renders text). Removes the per-route instanceof ladders and the duplicated conflictBody helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(quota): route getEffectiveQuota through the batch aggregation getEffectiveQuota (single org, ~8 queries) and getEffectiveQuotasByOrg (batch, 2 queries + in-memory aggregation) reimplemented the same EffectiveQuota assembly. Have the single path call the batch path with one id and return its entry; delete the now-unused per-resource SQL helpers (activeExtraEntitlement Bytes/Names/Where). One aggregation path, fewer queries per call, -75 lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b6e8c812d7 |
refactor: architecture cleanup (storage type, dedup, dead deps) (#429)
* fix(downloads): block SSRF targets in remote-download source URL The remote-download source URI was only length-validated, so an authenticated editor could point a task at the cloud metadata endpoint, loopback, or RFC 1918 hosts and have the response exfiltrated to their own drive. Add a shared isSafeHttpUrl/isBlockedUrlHost guard (scheme allowlist + private/loopback/link-local/metadata/IPv6 blocking) and cross-check source type vs uri in createDownloadTaskSchema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(api): cover 9 untested src/lib/api.ts wrappers Adds api.test.ts coverage (RPC path, method, payload, success + ApiError paths) for listObjectsByPath, isNameConflictError, listAdminAuthProviders, upsertAuthProvider, deleteAuthProvider, listInviteCodes, generateInviteCodes, deleteInviteCode, and listTeamActivities — satisfying the CLAUDE.md coverage gate that otherwise blocks PRs touching api.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(spaces): release source quota on cross-space move A cross-space "move" copied bytes into the target (reserving quota there) but only trashed the source. Trashed files still count toward usage, so the moved bytes were billed in both spaces and the source never freed — contrary to the design doc ("copy + delete source, quota effectively transfers"). Purge the source subtree (independent S3 copy already exists in the target) instead of trashing it, which deletes the objects, cascades share cleanup, and reconciles usage. Rename the response field sourceTrashed -> sourceDeleted and update the move hint copy accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(upload): wire S3 multipart for large files The upload UI only ever did a single presigned PUT, which caps at S3's 5 GiB limit and fails the whole transfer on any network blip — despite a complete multipart backend (object-upload-sessions) sitting unused. Add uploadPartToS3 (PUTs a part, returns its ETag) and a multipart-upload orchestrator: open session -> presign parts in batches of 100 -> PUT parts with bounded concurrency and per-part retry -> complete. Files over 100 MiB take this path; smaller files keep the single-PUT flow. Cancellation aborts the multipart and the draft. Also fixes the presignObjectUploadParts wrapper type to match the server's actual `url` field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(auth): add password-reset flow There was no self-service password recovery — a forgotten password needed admin intervention. SMTP/email sending was already built; this wires the last mile: better-auth sendResetPassword (reset email), a "Forgot password?" link on sign-in, and /forgot-password + /reset-password pages. The forgot-password page never reveals whether an account exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trash): auto-purge trashed items past a retention window Trashed files counted toward quota forever — trash never auto-emptied, so storage was never reclaimed without a manual "empty trash". Add a daily cron (CF Workers 0 4 * * * + Node setInterval) that purges trashed items older than ZPAN_TRASH_RETENTION_DAYS (default 30, 0 disables) across all orgs, reusing the existing purge path so S3 objects, share references, and quota are all cleaned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(notifications): typed NotificationType, i18n rendering, team-join Notifications were a bare-string type with only 3 producers, and server copy was stored as hardcoded English (zh users saw English). - Add a NotificationType union in shared/ and type the notification service. - Render notification title/body client-side from type + metadata via i18n, falling back to stored strings for older rows (fixes the hardcoded-English gap). - Notify users when they join a team (team_join). (Login auditing was intentionally dropped: reusing the activity-events feed for sign_in events would spam every user's per-org activity timeline. Proper auth auditing belongs in a dedicated log and can be added separately.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover SSRF guard and multipart upload branches Raise patch coverage on the new code: uploadPartToS3 pre-aborted-signal and network-error paths, the url-safety octet-overflow and public-IPv6 branches, and the invalid-magnet rejection in the download-task schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(storage): type S3Service against a narrow credentials shape The hand-written shared Storage type had a phantom `uid` field and lacked `filePath`, diverging from the DB row, so 44 call sites bridged the gap with `as unknown as S3Storage`. Introduce S3StorageCredentials (the 6 fields the S3 client actually reads); DB storage rows satisfy it structurally, so all casts are gone. Fix the shared Storage type to match the real API response (drop uid, add filePath, nullable customHost). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(storage): dedupe fileExt into path-template fileExt() was defined byte-identically in objects.ts, webdav.ts, and save-to-drive.ts, all feeding buildObjectKey. Move it next to buildObjectKey in path-template.ts and import it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(deps): remove 10 unused packages and the trash format-utils shim After migrating to the unified radix-ui package the individual @radix-ui/react-* packages (avatar, dialog, dropdown-menu, label, separator, slot, tooltip) were orphaned, along with @dnd-kit/sortable, @dnd-kit/utilities (only @dnd-kit/core is used), and @opentelemetry/api (transitive via better-auth, not imported directly). Also delete src/components/trash/format-utils.ts — a pure re-export of @/lib/format whose only consumer was its own test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(ui): dedupe getInitials into @/lib/format getInitials was reimplemented in 6 components/routes (user menu, org switcher, share layout, profile, users list, team settings) — behaviorally identical to the canonical @/lib/format.getInitials already used by the admin pages. Replace all locals with the shared import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(traffic): extract consumeAndReportDownloadTraffic The consume-quota (422) -> report-egress (402, refund) preamble was hand-rolled in the object, landing-share, direct-share, and WebDAV download paths. Extract consumeAndReportDownloadTraffic, parameterizing the 422 renderer (JSON vs text) and the compensating action (share download-counter decrement). The image-host redirect path reports after presigning, so it keeps its own sequence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7bad8d2aea |
fix: audit must-fixes + product gaps (SSRF, move-quota, multipart, password-reset, trash retention, notifications) (#428)
* fix(downloads): block SSRF targets in remote-download source URL The remote-download source URI was only length-validated, so an authenticated editor could point a task at the cloud metadata endpoint, loopback, or RFC 1918 hosts and have the response exfiltrated to their own drive. Add a shared isSafeHttpUrl/isBlockedUrlHost guard (scheme allowlist + private/loopback/link-local/metadata/IPv6 blocking) and cross-check source type vs uri in createDownloadTaskSchema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(api): cover 9 untested src/lib/api.ts wrappers Adds api.test.ts coverage (RPC path, method, payload, success + ApiError paths) for listObjectsByPath, isNameConflictError, listAdminAuthProviders, upsertAuthProvider, deleteAuthProvider, listInviteCodes, generateInviteCodes, deleteInviteCode, and listTeamActivities — satisfying the CLAUDE.md coverage gate that otherwise blocks PRs touching api.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(spaces): release source quota on cross-space move A cross-space "move" copied bytes into the target (reserving quota there) but only trashed the source. Trashed files still count toward usage, so the moved bytes were billed in both spaces and the source never freed — contrary to the design doc ("copy + delete source, quota effectively transfers"). Purge the source subtree (independent S3 copy already exists in the target) instead of trashing it, which deletes the objects, cascades share cleanup, and reconciles usage. Rename the response field sourceTrashed -> sourceDeleted and update the move hint copy accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(upload): wire S3 multipart for large files The upload UI only ever did a single presigned PUT, which caps at S3's 5 GiB limit and fails the whole transfer on any network blip — despite a complete multipart backend (object-upload-sessions) sitting unused. Add uploadPartToS3 (PUTs a part, returns its ETag) and a multipart-upload orchestrator: open session -> presign parts in batches of 100 -> PUT parts with bounded concurrency and per-part retry -> complete. Files over 100 MiB take this path; smaller files keep the single-PUT flow. Cancellation aborts the multipart and the draft. Also fixes the presignObjectUploadParts wrapper type to match the server's actual `url` field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(auth): add password-reset flow There was no self-service password recovery — a forgotten password needed admin intervention. SMTP/email sending was already built; this wires the last mile: better-auth sendResetPassword (reset email), a "Forgot password?" link on sign-in, and /forgot-password + /reset-password pages. The forgot-password page never reveals whether an account exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trash): auto-purge trashed items past a retention window Trashed files counted toward quota forever — trash never auto-emptied, so storage was never reclaimed without a manual "empty trash". Add a daily cron (CF Workers 0 4 * * * + Node setInterval) that purges trashed items older than ZPAN_TRASH_RETENTION_DAYS (default 30, 0 disables) across all orgs, reusing the existing purge path so S3 objects, share references, and quota are all cleaned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(notifications): typed NotificationType, i18n rendering, team-join Notifications were a bare-string type with only 3 producers, and server copy was stored as hardcoded English (zh users saw English). - Add a NotificationType union in shared/ and type the notification service. - Render notification title/body client-side from type + metadata via i18n, falling back to stored strings for older rows (fixes the hardcoded-English gap). - Notify users when they join a team (team_join). (Login auditing was intentionally dropped: reusing the activity-events feed for sign_in events would spam every user's per-org activity timeline. Proper auth auditing belongs in a dedicated log and can be added separately.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover SSRF guard and multipart upload branches Raise patch coverage on the new code: uploadPartToS3 pre-aborted-signal and network-error paths, the url-safety octet-overflow and public-IPv6 branches, and the invalid-magnet rejection in the download-task schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c55e806b44 |
refactor(admin): dedupe format helpers, merge entitlement dialog, group team routes (#427)
Post-merge quality cleanup from the #426 review: - Extract getInitials / formatDate / formatStorageUsage into src/lib/format.ts and migrate the admin users + teams pages off their local copies (also fixes a latent crash: users pages used part[0].toUpperCase() on empty segments). Unlimited quota now renders as ∞ consistently. - Merge GrantUserEntitlementDialog + GrantOrgEntitlementDialog into one GrantEntitlementDialog keyed by a { kind: 'user' | 'team' } target; the dialog's labels move to a shared admin.entitlement.* namespace and the per-namespace dialog-only keys are removed. - Move the org entitlement CRUD endpoints from /api/admin/quotas/:orgId to /api/admin/teams/:orgId so all team-admin functionality lives under one route tree; /api/admin/quotas keeps only the overview list. Frontend wrappers and tests follow. No behavior change. lint/typecheck/test/test:cf all green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
583678967d |
feat: spaces, quota ownership, and sharing — design doc §5 implementation (#426)
* docs: add spaces/quota/sharing design decisions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(store): require team owner role for billing and purchase endpoints Team orgs now gate checkout, billing portal, credits (balance/ledger/ redemptions), and order management behind the owner role. Personal orgs are unaffected. Implements docs/design/spaces-quota-sharing.md §2.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(store): clarify purchase target and gate billing UI to owners The store page now states which space purchases fund, hides purchase and billing surfaces from non-owner team members with guidance to contact the owner, and labels team orders with the team name on the cloud side. Implements docs/design/spaces-quota-sharing.md §2.1.2-3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(objects): cross-space copy/move with file manager entry Adds POST /api/objects/:id/transfers (copy or move a file/folder into another space) reusing the save-to-drive copy engine; move = copy + trash source, and the source survives any partial copy. The file manager gains a 'Copy/Move to space' action with a space/folder picker. Also fixes a privilege hole: save-to-drive (and the new transfer endpoint) previously accepted any personal org as a write target, allowing writes into other users' personal spaces; targets are now restricted to orgs with editor access or the caller's own personal org. The transfer folder picker also fixes the save-to-drive dialog listing the active org's folders instead of the selected target's. Implements docs/design/spaces-quota-sharing.md §3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(admin): per-team quota management and default team quota Admins can now grant, edit, and revoke storage entitlements for any space (team or personal) via /api/admin/quotas/:orgId/entitlements and a new admin Quotas page. New teams take their initial quota from the default_team_quota system option (falling back to default_org_quota), configurable in admin settings. Completes the v2.2 roadmap item 'Per-team storage quota set by admin'; implements design doc §2.3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(quotas): allocate purchased storage packs between owned spaces Space owners can move whole one-time purchased packs (cloud_order grants) between spaces they own via POST /api/quotas/me/entitlements/:id/transfers. Plans and admin grants are not transferable, and a transfer is blocked when the source space's usage would exceed its remaining quota. The storage page lists the current space's packs with a move dialog. Implements design doc §2.2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(shares): received shares inbox on the shares page GET /api/shares?box=received lists active shares addressed to the current user (matched by user id or the email the share targeted), with the sharer's name. The shares page gains a sent/received toggle; received items open the share landing page. This is an inbox of share links, not a mounted filesystem (design doc §4.4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(hooks): cover default team quota in site options hook Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(store,admin): drop purchase-target banner; scope admin quotas page to teams The storage page no longer shows the 'purchases fund X space' line — owners see the store normally and non-owner members keep the guidance notice. The admin Quotas page now lists team spaces only; personal quotas stay on the user detail page, removing the overlapping entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * revert(quotas): remove storage pack allocation Allocation (§5.5) operated on an empty set: the store only sells per-workspace subscriptions, and the original design restricted moves to one-time cloud packs, which don't exist in the catalog. Subscriptions can't be safely allocated either — the webhook cancellation/downgrade path matches on the original targetOrgId, so a moved entitlement's claw-back silently fails and leaves ghost capacity. The feature is also redundant: family owners subscribe the team space directly (§2.1) and self-hosted admins grant capacity straight to it (§2.3). Removes the quota-allocation service, /api/quotas/me/entitlements endpoints, the storage-page packs panel, isOrgOwner helper, and related tests/i18n. Design doc §2.2 updated to record why it was deferred and the prerequisite (a one-time pack SKU) for revisiting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(admin): replace quotas page with Teams management (list + detail) Promotes the admin quota surface to a proper Teams section, sibling to Users: a team list page where each row opens a team detail page, and the detail page manages quota entitlements (grant/edit/revoke) — mirroring the user detail page. All backed by org data. - New /api/admin/teams (list + detail) with member count, owner, and effective storage usage; teams identified by non-personal slug so legacy teams with null metadata are included. - New /admin/teams list + /admin/teams/$orgId detail routes; the old /admin/quotas page is removed and the nav item becomes 'Teams'. - Entitlement CRUD continues to reuse the org-generic /api/admin/quotas/:orgId/entitlements endpoints (invisible to users). - Personal-space quotas remain on the user detail page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(admin): use a distinct icon for the Teams nav item Users and Teams both used people icons (Users / UsersRound) and were hard to tell apart in the sidebar. Teams now uses Building2 (org). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(admin): chunk team member/owner IN-lists under D1's param cap listTeams bound all team orgIds into single member-count and owner-name queries; on D1 (100 bound-param cap) that breaks past ~100 teams. getEffectiveQuotasByOrg already chunks at 90 — match it for the two new queries. Members of a given org all land in one chunk, so per-org owner ordering is preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ce314538b7 |
test(licensing): assert PATCH confirm in pairing integration test
The pairing poll integration test still expected the old POST
/licenses/:id/confirm callback URL. Update it for the PATCH /licenses/:id
{ status: 'confirmed' } contract (method + body).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
5ce3845757 |
feat(store): add coupon entry to checkout
Stripe's native promo-code field is disabled on Cloud checkout, so collect the coupon on our side. Clicking a plan/credit purchase now opens a confirm dialog with a coupon input; applying a code calls the Cloud discount-quote endpoint and shows the server-computed subtotal/discount/total. Confirming threads the code through to the Stripe payment session. Uses zpan-cloud-sdk 2.2.0 (discount-quotes resource). Adds api wrappers with tests and the confirm dialog; updates the storage page checkout flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4f58c08b87 |
refactor(licensing): confirm cloud license via PATCH
Cloud replaced POST /licenses/:id/confirm with PATCH /licenses/:id
{ status: 'confirmed' }; follow the new SDK 2.2.0 contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
60c51cf3b1 |
fix(auth): eliminate get-session hangs from cross-request pending init
better-auth starts its $context init synchronously inside betterAuth(), within whichever request constructs the instance. Init eagerly resolves all social providers, and ours were 35 async functions doing one D1 query each. When the isolate's first request didn't touch auth (share SSR, /r/*, public APIs) or disconnected mid-init, its I/O context died with the queries in flight and $context never settled — on Workers a pending promise awaited from a later request never resolves, so the cached auth instance hung every subsequent auth call in the isolate (the recurring "get-session pending forever / 10s timeout" reports). - load all OAuth provider configs with one snapshot query; register builtin providers as static objects (init does zero per-provider I/O) - await auth.$context before returning from createAuth so a cached instance can never carry a pending promise tied to its creating request - only load captcha config for captcha-protected endpoints instead of every /api/auth/* request - cache the resolved site public origin at module scope (the WeakMap was keyed by the per-request db instance and never hit on Workers); cache settled values only, never promises - client: share one in-flight get-session across callers regardless of TTL, cache resolved values for 5s, never cache failures Regression tests pin the invariants: createAuth performs exactly one DB query during init and returns with $context already settled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a3866e2f67 | perf: fix get-session worker slowness and implement client-side cache | ||
|
|
fa136a5112 |
fix(upload): make Content-Disposition Latin-1 safe for non-ASCII filenames
Uploading a file with a non-ASCII name (Chinese, emoji, …) failed with "Failed to execute 'setRequestHeader' on 'XMLHttpRequest': String contains non ISO-8859-1 code point." The presigned PUT's signed Content-Disposition put the raw filename in the plain `filename="..."` parameter, which the browser then rejects when setting it as an XHR request header. Add a shared attachmentContentDisposition() helper that keeps `filename=` ASCII-only and carries the real name in `filename*=UTF-8''`, and route all three construction sites through it so the signed and client-returned values stay identical (the value is part of the SigV4 signature). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e6e67c5fe4 |
feat(licensing): harden cloud pairing — env keys, clear errors, confirm handshake (#424)
Three related robustness fixes for the cloud pairing flow: 1. Trusted license public keys are env-configurable (ZPAN_LICENSE_PUBLIC_KEYS) instead of hardcoding dev keys in source — a leaked dev key is rotated via config and never baked into production builds. Registered in all platform factories. 2. Certificate verification surfaces a specific rejection reason (signature/issuer/instance/expired/host), and the pairing modal distinguishes a cert-verification failure from a genuine timeout instead of showing both as "expired". On failure the poll handler rolls back the orphaned cloud binding. 3. After verifying + storing the certificate, the instance confirms the binding to the cloud (zpan-cloud-sdk 2.1.0's POST /licenses/:id/confirm) so the cloud pairing page resolves to success only once the instance actually accepted it. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e56d3dc61a |
feat(server): auto-trust loopback and LAN origins without TRUSTED_ORIGINS
Sign-in via 127.0.0.1 or a LAN IP failed with "Invalid origin" unless the user manually configured TRUSTED_ORIGINS. better-auth's trustedOrigins now uses the function form: origins on localhost, 127.0.0.0/8, ::1, or RFC 1918 private ranges are trusted per request. Browsers set the Origin header themselves, so a private address in it proves the page was served from the user's own machine or LAN — safe to trust for CSRF purposes. Also set advanced.disableOriginCheck: false explicitly: better-auth silently disables the origin check under NODE_ENV=test, so no test ever exercised real CSRF behavior. Test helpers now send an Origin header on cookie-bearing requests, like real browsers do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d2f3a34f05 |
fix(server): log underlying cause chain for failed D1 queries
Drizzle wraps the real D1 error in DrizzleQueryError.cause; both the access log and the origin-detect catch only logged .message, surfacing just "Failed query: <sql>" with no reason. Add formatError() to flatten the cause chain and use it at both sites so D1 failures are diagnosable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
53ca110218 |
feat(about): split runtime into runtime engine + deployment platform
Flatten the instance `runtime` object into two fields: `runtime` (the JS engine,
node | workerd) and `platform` (the deployment host). The About page shows each
as its own row with friendly labels (e.g. "workerd" + "Cloudflare Workers",
"Node.js" + "Docker").
- Detect the platform from the entry file (entry === target): each serverless
entry declares it; entry-node sniffs Cloud Run (K_SERVICE) / Docker
(ZPAN_RUNTIME, set in the Dockerfile) / bare node. Cloudflare is detected from
the D1 binding.
- Decouple the cloud payload: zpan-cloud-sdk fixes runtime { provider, target },
so CloudInstanceInfo keeps that shape and buildCloudInstanceInfo maps to it;
buildInstanceInfo serves the richer flat shape to the About API.
- Migrate PostHog instance telemetry to the runtime/platform shape and merge the
duplicate runtimeInfo in licensing-admin into the shared one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
dc936e5eff |
feat(about): add changelog refresh button that bypasses the cache
The changelog is cached server-side (1h) and by React Query, so stale content lingered. Add a refresh icon in the drawer header that calls the endpoint with ?refresh=true to skip the server cache and replaces the cached query data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6aa5257f01 |
feat(about): take latest version from GitHub Releases, keep CHANGELOG drawer
The latest-version indicator now reads the newest published GitHub Release's tag_name (the source of truth for "what shipped") instead of parsing the changelog file. The drawer still renders the hand-maintained, product-facing CHANGELOG.md — the auto-generated release notes are too technical for end users. - Release lookup is best-effort: a rate-limited/unreachable GitHub API hides the version badge but never breaks the drawer (CHANGELOG.md drives the markdown). - Drop parseLatestVersion; fetch the two sources concurrently behind the cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e700fd4977 |
feat(about): add changelog drawer, latest-version check, and commit hash
Maintain a CHANGELOG.md (Keep a Changelog format) at the repo root and surface it on the admin About page: - The page now shows the running build's short commit hash next to the version, linked to the GitHub commit. Commit is injected at build time via a new resolveAppCommit() (ZPAN_APP_COMMIT -> WORKERS_CI_COMMIT_SHA -> git rev-parse), wired through vite/tsup defines, the node entry, Docker, and CI. - A new admin-only GET /api/system/changelog endpoint fetches CHANGELOG.md from master on GitHub, caches it, parses the latest released version, and reports whether an update is available (semver compare against the running version). - The About page renders a "latest version" row with an update-available badge and a side drawer that displays the changelog markdown. Tests cover the semver compare, changelog parse/fetch caching, the API wrapper, and the route (admin-gated, parsed payload). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5a5583b4ed |
feat(licensing): regroup comparison table by capability and add social-login/downloader gates (#423)
* feat(licensing): regroup comparison table by capability and add social-login/downloader gates Decouple the feature comparison table's grouping axis from the pricing tier: features are now grouped by capability (core/advanced) while edition availability is expressed purely by the per-edition cells. Coming-soon features show a badge next to the name with a muted check in the target edition column, so it's clear which edition they will land in. Tier reclassification (per product decision): - Social login & OIDC: free = 1 provider, Pro/Business = unlimited - Downloaders: free = 1, Pro/Business = unlimited - Site announcements, Multi-IdP SSO, LDAP/SCIM, Analytics: Business-only New runtime gates (enforced, mirroring the storages count-gate): - social_login_unlimited in POST /api/admin/auth-providers (402 on 2nd) - downloaders_unlimited in POST /api/admin/downloaders (402 on 2nd) - site_announcements added to BUSINESS_ONLY_FEATURES Copy cleanup: - Rename rows that embedded a limit word: "Unlimited Team Workspaces" -> "Team Workspaces", "Storage Backends" -> "Storages" - Clarify "Storage Plans" -> "Sell Storage & Traffic" (the quota_store feature) - Make the Licensing page intro edition-neutral (Pro + Business) instead of the leftover Pro-only copy Tests: add gate tests for both new limits; seed licenses in the existing multi-provider/multi-downloader tests; switch announcement tests to a Business license. Note: site_announcements moving to Business takes full effect for real licenses only once zpan-cloud stops listing it in Pro certificates; the local edition-derived path is already updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(licensing): drop unused per-cert feature override, derive from edition The optional `features[]` override on the license certificate was added with the Pro/Business split but was never exercised: real certs carry only `edition`, and entitlements are derived from it via the feature registry (PRO_GATE_KEYS minus BUSINESS_ONLY_FEATURES). The override was dead in the normal flow and duplicated the edition→feature mapping in two places. Remove it so edition is the single source of truth: - Drop `LicenseAssertion.features` and `normalizeFeatures` (verify.ts) - `effectiveFeatures(edition)` no longer takes/honors an override list - Simplify the test seed helpers (no `features` arg, no test-side business-only set) and update licensing tests to assert edition-derived entitlements `BindingState.features` (the resolved list exposed to the client) is kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(downloader): regenerate OpenAPI spec and Go client for new 402 response The downloaders create route gained a 402 (feature_not_available) response for the free-plan limit; regenerate the committed OpenAPI document and Go client so openapi:downloader:check passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
891e331ca8 |
feat(admin): add About page with instance info and edition-aware cloud links (#422)
Add an admin About page (/admin/about) showing version, instance metadata (id/name/url/runtime/server OS/Node version) and edition status, plus two CTAs: GitHub (star) and ZPan Cloud. When the instance holds a valid license the cloud CTA deep-links to the certificate detail page. - server: expose GET /api/system/instance (admin-only); extract shared runtimeInfo() helper; move InstanceInfo to shared types - ribbon: link to the About page, relabel Community -> Free, recolor Pro (gold) / Business (indigo) to match the cloud certificate palette - licensing: BoundStatusCard "Manage" deep-links to the certificate detail page when active with a license_id - useEntitlement: expose licenseId - tests: api wrapper + system endpoint + ribbon coverage Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b10a1f3d84 |
fix(quota): chunk batch quota IN queries under D1's 100-param cap
getEffectiveQuotasByOrg passed every org id into a single IN clause. With more than ~100 orgs the bound-parameter count exceeded Cloudflare D1's per-query limit (100), so GET /api/admin/quotas threw "Failed query" in production. Local SQLite has no such cap, so tests and CI never hit it. Split the org ids into chunks of 90 (leaving headroom for the extra status/timestamp params on the entitlements query) and run the chunked queries concurrently, aggregating in memory. Query count stays constant (2 per chunk) instead of the old 8 per org. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9c16adbdc0 |
perf(quota): batch admin quota listing and move monthly reset to cron
The admin quota listing called getEffectiveQuota per org, firing ~8 sequential queries each (N+1). On D1 every query is a network round-trip, so the endpoint scaled linearly with org count and risked the Workers subrequest cap. - Add getEffectiveQuotasByOrg: resolves every org in 2 queries (quota rows + active entitlements) and aggregates in memory. Route uses it. - Remove the inline traffic-period reset writes from the read paths (getEffectiveQuota and the listing route). getEffectiveQuota already normalizes a stale period in memory, so reads stay correct. - Add resetExpiredTrafficQuotas and run it on a new monthly cron (0 0 1 * *) for CF Workers and a daily idempotent interval for Node. The consume write path keeps its atomic reset-and-consume as a self-healing fallback if a scheduled run is missed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f922fd4d6f |
feat(quota): allow admins to edit and revoke granted entitlements (#419)
Admin-granted storage entitlements were insert-only. Add edit + revoke for admin_grant entitlements (PATCH/DELETE /api/admin/users/:id/entitlements/:eid), guarded so system-managed sources (free_plan, cloud_order) are untouchable. Includes RPC wrappers, edit/revoke UI on the user detail page, i18n, and full unit + integration coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
af43701daf |
fix(version): set app version global in node entry (#418)
* fix(version): resolve app version at runtime in node entry E2E runs the Node server via tsx, which bypasses the tsup build-time define, leaving __ZPAN_APP_VERSION__ unset so getAppVersion throws and /api/licensing/pair returns 500. Resolve the version at runtime via resolveAppVersion (git describe) when the global is unset; in the built output the define inlines the constant, so the branch is never reached and git is not invoked in production. Keeps the git-describe version as the single source of truth across all paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(version): inject app version into docker build The Docker build excludes .git from its context (.dockerignore), so the build-time git describe in build:node would throw and break the image build. Let resolveAppVersion read ZPAN_APP_VERSION, set it from an APP_VERSION build arg in the builder stage, and pass the release tag from the release workflow. git describe stays the default everywhere else, so the version is unified across CF Workers, tsx, and Docker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
43e92caaa6 | chore(config): consolidate project tooling | ||
|
|
ef122ec317 | fix(version): inject app version at build time | ||
|
|
3b5f6c7fb5 | fix(telemetry): persist detected site origin | ||
|
|
48b82cc804 | fix(telemetry): avoid generic disable flag | ||
|
|
1a6a77435b | fix(telemetry): disable reports during e2e | ||
|
|
f52213b202 | fix(telemetry): standardize instance reports | ||
|
|
178e7311e4 | fix(telemetry): enable GeoIP enrichment | ||
|
|
400940204f | fix(telemetry): align instance report fields | ||
|
|
7b5cd90eb6 | fix(telemetry): use correct PostHog project token | ||
|
|
5c3df3c8ec | fix(telemetry): use PostHog SDK for instance reports | ||
|
|
bd9f047ebb | fix(telemetry): report instance after deployment (#417) | ||
|
|
1d7ac9b071 |
fix(telemetry): use built-in product reporting endpoint (#416)
* fix(telemetry): use built-in product reporting endpoint * fix(telemetry): send reports through posthog capture |
||
|
|
de0213ad5d |
feat: add PostHog instance telemetry (#414)
* feat: add PostHog instance telemetry Agent-Profile: https://agent-kanban.dev/agents/7bf89fb1be06098c * fix: include node os release in telemetry Agent-Profile: https://agent-kanban.dev/agents/7bf89fb1be06098c |
||
|
|
9d70acfdea |
feat(licensing): support independent business authorization
Support independent Pro and Business licensing, migrate Cloud store integration through the SDK, gate Business-only credit billing features, and validate the Cloud store E2E flow. |
||
|
|
ecb38df84d | fix(downloads): make remote usage billing resilient | ||
|
|
2e12929290 | fix(downloads): improve downloader assignment handling | ||
|
|
71af982044 | fix(downloader): store runtime updates as snapshots | ||
|
|
4579efda67 | fix(downloader): clear eta after transfer completion | ||
|
|
00f42e762f | fix(downloader): ignore stale seed progress after restart |