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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
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>
* 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>
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.
* 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>
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>
- 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>
* 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>
* 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>