Commit Graph
92 Commits
Author SHA1 Message Date
Jasper VanandClaude Opus 4.8 c1542e6a9c refactor(openapi): drop the committed spec artifact; gen Go client from the full doc (#441)
No more docs/openapi/downloader.json and no curated subset. The Go client is
generated straight from the complete, live /api/openapi.json: a single
scripts/openapi-client.ts boots the in-memory app, reads the whole merged
document, and feeds it to oapi-codegen via a throwaway temp file — only the
generated client.gen.go is committed.

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:51:01 -04:00
Jasper VanandClaude Opus 4.8 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>
2026-06-13 14:56:24 -04:00
Jasper VanandClaude Fable 5 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>
2026-06-13 00:14:24 -04:00
saltbo d693837150 chore: update default branch references from master to main 2026-06-12 12:45:39 -04:00
Jasper VanandClaude Fable 5 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>
2026-06-12 01:12:45 -04:00
saltboandClaude Opus 4.8 e4b497ac92 docs: add v2 launch offers page and README link
Introduce the three ways to earn ZPan Pro for free, celebrating the v2
launch: old-stargazer reward (claim by 2026-09-01), contributor gift, and
stargazer loyalty. Link it from a header callout and the Documentation
section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:58:53 -04:00
saltboandClaude Opus 4.8 bb801d42a4 docs(changelog): drop top-level title, start directly at version entries
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:56:35 -04:00
saltboandClaude Opus 4.8 7985e022a8 docs(changelog): drop language-switcher nav, keep each changelog standalone
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:36:38 -04:00
saltboandClaude Opus 4.8 94a6bf5a18 docs(changelog): add product-oriented v2.7.3 entry across all languages
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:24:14 -04:00
saltboandClaude Opus 4.8 66ae9a38ea docs(i18n): translate README and CHANGELOG into 6 languages
Add zh-CN, ja, ko, ru, es, pt-BR translations under docs/i18n/, keeping
the canonical English README/CHANGELOG at the repo root. Each file gets a
language switcher bar with the current language bolded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:25:17 -04:00
Jasper VanandClaude Opus 4.8 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>
2026-06-09 13:32:59 -04:00
saltbo e17e2018d3 fix(docker): use host hostname for downloader registration 2026-06-07 01:00:35 -04:00
saltbo 5f83099c7c fix(docker): configure remote downloader runtime 2026-06-06 20:44:08 -04:00
saltbo 475cb46557 fix(docker): use host hostname for downloader 2026-06-06 20:34:16 -04:00
saltbo 7e7eeb4ca7 fix(downloads): expose torrent listen port in docker 2026-06-06 20:26:03 -04:00
saltbo 9486360389 feat(cli): promote downloader to zpan command 2026-06-06 01:38:16 -04:00
saltbo c19cbbfcac feat(downloader): auto bootstrap device login 2026-06-06 01:26:28 -04:00
saltbo 65eb5d9738 fix(downloader): reset runtime on task restart 2026-06-06 00:54:26 -04:00
saltbo 6aa711bfb6 refactor(downloads): reshape download task status model 2026-06-06 00:07:21 -04:00
saltbo 8f272b8a41 refactor(downloader): clarify task states and restart action 2026-06-05 21:51:32 -04:00
saltbo 9dd3ca0105 fix(downloader): separate interrupted task recovery 2026-06-05 21:34:57 -04:00
saltbo 3f7a859e73 refactor(downloader): consolidate api contract generation 2026-06-05 15:24:56 -04:00
saltbo d6a368c6c0 fix(downloader): multipart upload large results 2026-06-04 23:40:25 -04:00
saltbo dbd4755a5e fix(downloads): prevent table sorting lockup 2026-06-04 15:33:56 -04:00
saltbo 4dcbab55b1 feat(downloads): improve task controls and metadata 2026-06-04 14:57:44 -04:00
saltbo 1b4f149d05 feat(downloads): stream normalized task details 2026-06-03 13:08:14 -04:00
saltbo 754d1aaa3d fix(downloads): separate storage and peer upload metrics 2026-06-03 12:23:31 -04:00
saltbo cf3324b4a2 feat(downloads): preserve directory uploads 2026-06-03 12:05:55 -04:00
saltbo f698e5a8a9 feat(downloads): show normalized task details 2026-06-03 11:12:56 -04:00
saltbo 53076d7873 feat(downloads): add remote download workers 2026-06-03 02:21:50 -04:00
Jasper Van 650f209fe8 docs: clarify cloud credit billing boundaries
Clarify ZPAN's Cloud Credits boundaries and keep gift-card creation on the Credits-only contract.
2026-06-02 03:41:07 -04:00
Jasper Van f45e8732fc feat: integrate cloud credits store flow
Replace ZPAN Store Wallet semantics with Credits-only APIs and UI.

Proxy Cloud credits through credit-account routes, send unitless gift-card credits, and block metered downloads on insufficient Cloud credits before presign while rolling back local counters.

Verification waiver: E2E fails only because the hosted Cloud target rejects the merged Credits gift-card payload; AK follow-up kxel47x6x6l3 tracks that target gap.
2026-06-01 22:31:46 -04:00
saltbo 30bc6e1a12 build: migrate project to pnpm 2026-06-01 10:44:33 -04:00
saltbo 9d13ee7055 docs(roadmap): revise v2.7 download planning 2026-05-11 01:08:20 -04:00
saltbo 06c7be910b docs(roadmap): update v2.7 file workflow plan 2026-05-11 00:37:27 -04:00
saltbo 88f6e22f72 fix(traffic): sync cloud usage in background 2026-05-09 11:46:26 -04:00
Jasper Van 3b2a2b03bf test: document metered storage plan flow (#386)
* test: document metered storage plan flow

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* test: align subscription entitlement lifecycle

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-08 21:42:40 -04:00
Jasper Van 11ab430493 refactor(quota-store): unify orders and gift cards
* refactor(quota-store): unify orders and gift cards

* test(quota-store): cover helper paths

* test(quota-store): cover webhook idempotency

* test(quota-store): cover cloud response normalization
2026-05-07 09:03:55 -04:00
Jasper Van 5632cbbc83 fix: use pro binding auth for quota store (#367)
* fix: use pro binding auth for quota store

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix: align quota store event token audience

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix: match cloud delivery event audience contract

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-06 00:29:48 -04:00
Jasper Van 8f6e4c2ff3 Fix quota store Cloud binding contract (#366)
* fix(quota-store): use Pro binding for Cloud store auth

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix(quota-store): align Cloud store binding contract

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-05 22:32:40 -04:00
Jasper Van 05b14a4991 docs: verify pro quota store release notes (#364)
Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-05 17:13:54 -04:00
saltbo d7f1ceb6bc feat(licensing): adjust free plan limits 2026-04-27 20:41:46 -04:00
Jasper VanandBob 04f9d93ddf feat: v2.6 Z11 — production public key, Docker cron docs, release notes (#350)
* feat: v2.6 Z11 — prod public key, Docker cron docs, release notes

- Replace DEV placeholder in public-keys.ts with cloud.zpan.space
  production Ed25519 key (k4.public.sphdaogcyIh2_6_yZnO4_xQsi2m52HH9j2CPHcKlGGw)
  from cloud C5 cross-repo PR
- Add external cron section to docs/deploy/docker.md for the
  POST /api/licensing/refresh-cron endpoint (Z6)
- Create docs/v2.6-release-notes.md with what's new, retroactive gate
  notice (open_registration, teams_unlimited, team_quotas), upgrade guide

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

* test(licensing): decouple verify/entitlement tests from DEV secret key

Tests were hardcoded to the old DEV placeholder key. Now they generate
a fresh throwaway keypair per suite (beforeAll/afterAll), inject the
public key into PUBLIC_KEYS, and restore the original on teardown.

This keeps the tests independent of whichever production key is in
PUBLIC_KEYS, so rotating the key never breaks the test suite.

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

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 09:20:58 -04:00
Jasper VanandBob 29102e623d feat: v2.6 Z6 — 6h background entitlement refresh (#345)
- Add server/services/licensing-refresh-runner.ts: shared runner with
  5-min dedup guard, structured INFO logs, and no-op for unbound state
- Add workers/scheduled.ts + export scheduled() in workers/bootstrap.ts
  for CF Workers cron (every 6 hours)
- Add [triggers] crons = ["0 */6 * * *"] to wrangler.toml
- Add setInterval refresh on boot in server/entry-node.ts with
  "licensing.refresh.scheduler.started interval=6h" log
- Add POST /api/licensing/refresh-cron?secret=... public endpoint
  (timing-safe secret comparison) for non-CF platforms
- Extract ZPAN_CLOUD_URL_DEFAULT to shared/constants.ts, replacing
  four duplicated literals
- Document REFRESH_CRON_SECRET + scheduler setup in all 5 non-CF
  deploy guides (vercel, netlify, aws-lambda, azure-functions, cloud-run)

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 08:22:10 -04:00
saltboandClaude Opus 4.7 5de2100435 docs(roadmap): update v2.6 cloud dependency to v1.0
zpan-cloud renumbered from v0.x to v1.x (SemVer: first stable
= 1.0). v2.6 Pro Launch now depends on cloud v1.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:36:07 -04:00
saltboandClaude Opus 4.7 751b09366d docs(roadmap): restructure v2.6–v2.10 around Pro Launch
- v2.6 becomes Pro Launch: cloud-account binding activation,
  Ed25519 entitlement certificates, white-label as first Pro
  feature, retroactive Pro gates (open registration, team count
  ≤3, per-team storage quota)
- v2.7 becomes Pro Operator: audit log, webhooks, anti-abuse
  stack for the Pro-gated open registration mode
- v2.8 becomes Pro Analytics & Identity: analytics dashboard,
  multi-IdP SSO + SAML, LDAP / SCIM
- Original Backup (v2.6) content moved to v2.9; Sync & Desktop
  (v2.7) content moved to new v2.10
- Managed Cloud Services extracted to a separate closed-source
  zpan-cloud repo (content moderation, server-side processing,
  managed custom domains)
- Community vs Pro split rewritten around the operator rule
  ("features that help you use ZPan are free; features that
  help you operate ZPan for others are Pro")

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:29:54 -04:00
Jasper VanandBob d33800f23e feat: Azure Functions deployment target (v4, Node 22) (#330)
* feat: add Azure Functions deployment target (v4, Node 22)

- server/entry-azure.ts: Azure Functions v4 handler wrapping the Hono
  app via app.http(); uses createLibsqlPlatform for Turso and serves
  the SPA from ./dist via @hono/node-server/serve-static
- server/azure-host.json: runtime manifest (extensionBundle v4)
- deploy/azure-functions/main.bicep: idempotent Bicep template
  provisioning Storage Account, Consumption plan and Function App;
  BETTER_AUTH_SECRET handled separately by the workflow
- .github/workflows/deploy-azure.yml: 8-step workflow (secret check,
  checkout, Node setup, az login, Bicep deploy, build, db:migrate,
  func publish) with BETTER_AUTH_SECRET generate-if-missing logic
- package.json: build:azure script + @azure/functions dependency
- docs/deploy/azure-functions.md: setup guide covering SP JSON format,
  required secrets, and local emulation with func start

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

* fix: address review issues in Azure Functions deploy

- Move BETTER_AUTH_SECRET and APP_URL setup to before func publish
  (bootstrap.ts throws on missing secret; any request between publish
  and the old secret-set step would have returned 500)
- Remove placeholder appUrl Bicep param; workflow sets APP_URL and
  BETTER_AUTH_URL via appsettings after Bicep, before publish
- Fix HttpRequest→Request body handling: construct a proper Web API
  Request with body cast and duplex option instead of double-casting
  HttpRequest, ensuring POST/PUT/PATCH body-reading routes work
- Add push: branches: [master] trigger + upstream guard to match other
  deploy workflow conventions; document the auto-deploy behaviour
- Update docs/deploy/azure-functions.md to reflect the push trigger

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

* ci: re-trigger CI for review fixes

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-22 02:08:54 -04:00
8b72a7dba9 feat: v2.5.0 T6 — Google Cloud Run deployment (service.yaml + workflow + docs) (#332)
* feat: v2.5.0 T6 — Google Cloud Run deployment (service.yaml + workflow + docs)

Add Cloud Run as a first-class deploy target. Reuses the existing root
Dockerfile via gcloud run deploy --source (Cloud Build). Turso for DB,
external S3-compatible storage. Follows the standard 8-step workflow
contract: secret check, release resolution, GCloud auth, Turso migration,
Secret Manager upsert, deploy. min-instances=0 for free-tier eligibility
with cold-start callout in docs.

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

* fix: resolve first-deploy failure and drive deploy from service.yaml

BLOCKER: BETTER_AUTH_URL and TURSO_AUTH_TOKEN were passed to --set-secrets
even when the corresponding Secret Manager entries didn't exist yet.
BETTER_AUTH_URL isn't known until after the first deploy (it IS the Cloud
Run service URL). TURSO_AUTH_TOKEN is optional. Both caused 'secret not
found' aborts.

Fix: separate deploy into two phases.
Phase 1 — gcloud run services replace with service.yaml, which only
references the guaranteed secrets (turso-database-url, better-auth-secret).
Phase 2 — post-deploy step captures the service URL, upserts better-auth-url
and (if provided) turso-auth-token in Secret Manager, then wires them into
the running service via gcloud run services update --update-secrets.

MINOR: service.yaml was orphaned — the workflow used gcloud run deploy
--source . with inline flags instead. Rebuilt workflow to use gcloud builds
submit to build the image, then gcloud run services replace to drive the
deploy from the manifest. PROJECT_ID is substituted at deploy time.

Also demote BETTER_AUTH_URL from required to optional GitHub secret
(auto-derived from Cloud Run service URL on first deploy) and update docs.

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

---------

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