mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-19 01:51:11 +08:00
b7f7fa7ecd1283d27d5cbca9cd3b8974dc6ea127
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b7f7fa7ecd | feat(admin): implement operations dashboard stats | ||
|
|
554c231536 | feat(admin): redesign dashboard with pro analytics | ||
|
|
7ccaba2f8b |
feat: refine admin audit filtering (#495)
* feat: refine admin audit filtering Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133 * test: add audit filter spec scenarios Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133 * chore: refresh openapi client Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133 --------- Co-authored-by: Noah Reed <noah-reed@mails.agent-kanban.dev> |
||
|
|
3695c80c2e | feat(downloads): add task event timeline | ||
|
|
3fdc7b4ae0 | refactor(downloader): reorganize cmd downloader runtime | ||
|
|
cf7b1de112 | fix(downloader): consolidate polling into heartbeat | ||
|
|
c55f6f460c | chore(openapi): update generated storage client | ||
|
|
3f6751d60c | chore(openapi): update generated downloader client | ||
|
|
f41ed27bba |
[codex] separate billing configuration (#479)
* feat(admin): separate billing configuration Add dedicated storage egress and downloader credit billing contracts, usecases, RPC wrappers, drawers, generated client updates, and coverage. Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0 * fix(billing): preserve not found ordering Check storage and downloader existence before quota_store gating in dedicated billing usecases, and cover enabled missing-resource requests at usecase and route levels. Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0 --------- Co-authored-by: Jordan Park <jordan-park@mails.agent-kanban.dev> |
||
|
|
82c5452782 |
feat(auth): move OAuth provider editor to drawer (#480)
Agent-Profile: https://agent-kanban.dev/agents/57ed5bcf43079e29 Co-authored-by: Jordan Park <jordan-park@mails.agent-kanban.dev> |
||
|
|
c7f3d11793 |
[codex] Add admin storage connection testing (#475)
* feat(storage): add admin connection testing Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0 * fix: correct storage CORS guidance Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0 --------- Co-authored-by: Jordan Park <jordan-park@mails.agent-kanban.dev> |
||
|
|
f4b65e4987 |
feat: make forcePathStyle configurable per storage (#474)
* feat: make forcePathStyle configurable per storage Previously hardcoded to true, which breaks S3-compatible backends that require virtual-hosted-style addressing (e.g. Alibaba Cloud OSS). Now configurable via admin storage settings with a toggle switch, defaulting to true for backwards compatibility. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover storage force path style --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: saltbo <saltbo@foxmail.com> |
||
|
|
00f48cf355 |
feat(avatars): host avatars + team logos on Cloud via SDK 2.4.0; remove public-bucket mode (#467)
* feat(avatars): host avatars + team logos on Cloud via SDK 2.4.0; remove public-bucket mode Host user avatars and org logos on the ZPan Cloud avatar service (zpan-cloud-sdk ^2.4.0) instead of a public S3/R2 bucket, then remove the now-dead storages.mode / public-bucket concept entirely (#456 parts 2-3). - image-upload gateway: upload/delete via SDK uploadAvatar/deleteAvatar against a bound Cloud client; validate mime (AVATAR_CONTENT_TYPES) + size (MAX_AVATAR_BYTES) before the call; map cloud error codes to 400/403/413/500; unbound instance returns 503 cloud_required (delete is a best-effort no-op). - licensing-cloud: createAvatarUploadClient builds the client with a plain-object bearer header so both the image content-type and Authorization survive hono's per-request header merge (a Headers instance would be dropped). - drop storages.mode (migration via drizzle-kit), StorageRepo.select() no longer takes a mode, remove StorageMode / Storage.mode / mode schema+audit+UI+i18n and the PUBLIC_IMAGES bucket + PUBLIC_IMAGES_URL wiring. Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a * ci(deploy): drop dead PUBLIC_IMAGES R2 provisioning from CF deploy The Cloud avatar migration removed the PUBLIC_IMAGES binding from wrangler.toml, so the deploy workflow's R2 public-images steps are dead and must go too — otherwise every CF deploy keeps re-provisioning a public-read zpan-public-images bucket (the footgun #456 eliminates) and sets an unused PUBLIC_IMAGES_URL secret. Removes the bucket-create, managed-public-URL, and secret steps (steps.r2 was only consumed by the secret step). Also drops a stale storage-modes line from the v2.0 roadmap. Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a --------- Co-authored-by: Alex Chen <alex-chen@mails.agent-kanban.dev> |
||
|
|
a020cf74f0 |
refactor(branding): store logo + favicon as base64 data URIs (drop public bucket) (#466)
Branding's logo/favicon are now encoded as `data:${mime};base64,…` URIs and
stored directly in the `branding_logo_url` / `branding_favicon_url` system
options instead of being uploaded to a `mode='public'` S3 bucket. This removes
branding's dependency on public storage entirely (#456 Part 1).
- uploadBrandingImage encodes the raw file bytes and drops select('public') /
s3.putObject / s3.getPublicUrl; s3 + storages removed from BrandingDeps.
- Per-field raw-byte caps replace the single 2 MiB limit: logo ≤ 256 KB,
favicon ≤ 64 KB; over-cap returns 413 naming the field's limit.
- The 503 "no public storage" path is gone: uploads succeed with no public
storage configured, and the updateBranding route no longer advertises 503
(operationId unchanged; Go OpenAPI client regenerated).
- GET shape unchanged; legacy absolute-URL values keep rendering as-is (no
migration, no backfill, old _system/branding objects untouched).
Out of scope (#456 Parts 2-3): avatar/team-logo upload, storages.mode,
StorageRepo.select.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
63d5b45e0e |
refactor(api)!: collapse polymorphic GET responses to one monomorphic schema (#449) (#455)
Every endpoint now exposes one monomorphic schema: role/state changes field
values (mask / null / filter), never the shape.
image-hosting/config: drop the `full config | { enabled: false }` union. GET
always returns the full ImageHostingConfig shape carrying `enabled`; not-configured
→ `enabled: false` with every other field null (`createdAt` is now nullable).
`buildResponse` is made total over `row | null` so it is the single producer of
the shape, and `getIhostConfig` no longer returns `| null`.
auth-providers: collapse the admin-config vs public-display union into one
AuthProvider schema (providerId, type, enabled, name, icon, clientId, discoveryUrl,
scopes, clientSecret). Same endpoint, no path split — role changes one value:
admin gets a masked clientSecret, front-of-house gets `clientSecret: null` and the
enabled-only list. The two list usecases collapse into listAuthProviders(deps,
{ isAdmin }); the PUT response and the merged frontend wrapper adopt the same
schema, deleting AuthProviderConfig/PublicAuthProvider entirely.
Regenerated the Go client (union types removed) and updated frontend types/consumers.
Closes #449.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7b8c8c915e |
refactor(api)!: unify object upload + rework delete/trash lifecycle (#448) (#454)
Resolve #448 — one upload entry point and an AIP-164 soft delete. Upload: POST /objects now returns size-decided upload instructions { sessionId, partSize, urls }; the server picks single PutObject (<=5 GiB) vs 5 GiB-part multipart (>5 GiB) and rejects >5 TiB. The client PUTs each slice, reads its ETag, then POSTs them to POST /objects/{id}/uploads/{sid}/completions (returns the live object). DELETE /objects/{id}/uploads/{sid} aborts and discards the draft. Trash: matters.status drops 'trashed' (enum is {draft,active}); trash is tracked by the existing trashedAt timestamp. DELETE /objects/{id} now soft-deletes; the recycle bin lives under /trash/objects (list roots, get, restorations, purge). Empty-trash is a frontend loop over roots. BREAKING CHANGE: - removes PUT /objects/{id}/status and POST /objects/{id}/uploads - PUT .../uploads/{sid}/status -> POST .../uploads/{sid}/completions {parts} - DELETE /objects/{id} flips hard-purge -> soft-delete; permanent purge moves to DELETE /trash/objects/{id} - DELETE /trash removed; restore is POST /trash/objects/{id}/restorations - matters.status enum loses 'trashed' (migration backfills to trashedAt) The migration swaps the matters_active_name_uniq partial index to exclude trashed rows (WHERE status='active' AND trashed_at IS NULL). The single-PUT presign is header-free so the uniform slice uploader's raw PUT matches the S3 signature. Go downloader client + agent reworked to the unified flow. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
78e2550e67 |
refactor(api)!: unify revoke/cancel on PUT /{resource}/{id}/status (#452) (#453)
* refactor(api)!: unify revoke/cancel on PUT /{resource}/{id}/status (#452)
Retire the misleading DELETE /shares/{token} and PATCH /store/orders/{orderId}
shapes in favor of the existing status-subresource convention already used by
background-jobs and download-tasks.
- Shares: PUT /api/shares/{token}/status {status:'revoked'} -> 200 + the updated
creator ShareView. revokeShare now resolves the share before the UPDATE (the
record is unresolvable once revoked) and builds the view via a composeShareView
helper shared with viewShare; concurrently-revoked tokens now return 404.
Removed the now-dead getCreatorByToken repo port/adapter method.
- Store: PUT /api/store/orders/{orderId}/status {status:'canceled'} -> 200. Only
the local route shape changed; the upstream cloud SDK $patch call is untouched.
- Frontend: deleteShare -> revokeShare and cancelCloudOrder now use .status.$put
via the Hono RPC client; updated the shares route component.
- Regenerated the Go OpenAPI client.
Note: revoking a share whose matter is trashed-but-not-purged now returns 404
(was 204), a consequence of reusing viewShare's resolution path.
Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(spec): rename share delete scenarios to revoke status-subresource
Align spec/shares.feature scenario tags (@shares/revoke,
@shares/revoke-non-creator) with the renamed [spec:] breadcrumbs so
lint:spec traceability passes.
Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(shares): keep revoke working for a trashed-but-not-purged matter
revokeShare switched to resolveByToken, which returned matter_trashed for a
soft-deleted (not purged) matter and was short-circuited to 404. Because
trashing a matter does not cascade to its shares, the share stayed active and
still appeared in the owner's list — so the owner could no longer revoke it
(privacy footgun: restoring the file re-exposed a share they believed revoked).
ShareResolution now carries the share/matter/recipient records on the
matter_trashed variant (and splits not_found/revoked into single-literal members
so control-flow narrowing works). Viewer-facing callers still branch on status,
so trashed -> 410 for viewers is unchanged. revokeShare treats matter_trashed as
revocable (ownership check, revokeByToken, revoked creator view), while not_found
and already-revoked still map to 404.
Adds unit coverage (trashed-matter revoke succeeds; non-creator still 403) and a
backend integration test (share a landing matter, trash it, PUT status revoked ->
200 + status:'revoked', DB flips).
Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
2ae603bbab |
refactor(api)!: DELETE endpoints return 204 No Content (#443) (#447)
* refactor(api)!: DELETE endpoints return 204 No Content (#443) Resolves item #4 of #443 — DELETE return-shape inconsistency (8 different conventions). Standardize every DELETE on 204 No Content with an empty body, dropping the ack/result bodies: `{id,deleted}`, `{providerId,deleted}`, `{key,deleted}`, `{id,revoked}`, `{ok:true}`, the download-task tombstone, license `{deleted,cloud_unbind_error}`, and the entitlement-revoke / abort-upload-session objects. Kept (the issue's flagged special case): object-delete and empty-trash still carry a purge count — the only delete responses with information a caller can't otherwise derive. Object delete is trimmed from `{id,deleted,purged}` to just `{ purged: number | false }`; empty-trash keeps `{ purged: number }`. Backend: 15 DELETE routes → `204: { description }` + `c.body(null, 204)`; removed the now-dead `deleteDownloaderResponseSchema`. Frontend: added a `discard()` helper (the 204 counterpart to `unwrap()`); the unwrap-based delete wrappers now resolve `void`. cancelUpload/deleteObject now return `{ purged }`. The already-void wrappers (deleteShare, deleteAvatar, …) were untouched — they never read the body. OpenAPI document + Go client regenerated (go build clean). BREAKING CHANGE: all DELETE endpoints now respond 204 with no body. License unbind no longer returns `cloud_unbind_error`, so a partial cloud-unbind failure is no longer surfaced in the response body. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(licensing): surface cloud-unbind failure as 502, don't swallow it as 204 The DELETE→204 sweep turned license unbind into an unconditional 204, which hid a real partial failure: when the best-effort cloud unbind throws, the local binding is cleared but the cloud side is left dangling. Reporting 204 (success) in that case swallows the error. `unbindLicense` now returns a Result — `{ ok: true }` only when the cloud unbind also succeeds, and a 502 AppError (reason `CLOUD_UNBIND_FAILED`, the cloud error in `details.metadata`) when it fails. The local binding is still cleared either way; the handler returns 204 on ok and throws the error otherwise. DELETE success is still an empty 204 — this only restores fail-fast on the one endpoint whose failure was a soft body field, never a thrown error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): reconcile users.feature with #446 better-auth migration `pnpm lint:spec` (a CI gate) was red on 11 orphaned `spec/users.feature` scenarios — leftover from #446, which moved admin user management off our `/api/users/*` routes onto better-auth's admin plugin and deleted the old endpoints/tests but not their spec scenarios. Pre-existing on main; surfaced here as the only failing CI check. Reconcile the spec with reality: - Re-link the behaviors that survived (now via better-auth) to the tests that already cover them: list / admin-only(403) / disable(ban) / delete(remove) / patch-missing(act-on-missing→404) → admin-users-ba.integration.test.ts; quota-personal-org → the per-user quota test in users.integration.test.ts. - Drop scenarios for behavior that no longer exists: batch-toggle (now a client-side fan-out, no endpoint), invalid-status (ban/unban are explicit), multi-field filter (better-auth search is single-field, untested), the unauthenticated 401 guard (better-auth owns it), and the inline quota-entitlements-in-list (quota is now a per-user sub-resource). lint:spec: 413 scenarios, all covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7d819a5b9f | refactor(users)!: move admin user management to better-auth admin plugin (#446) | ||
|
|
8abca2f88c |
refactor(errors)!: unify error handling on typed AppError + single jsonError renderer (#445)
Collapse the two error conventions (string-reason `{ok:false,reason}` outcomes
and thrown domain-error classes) onto one. Usecases now produce typed `AppError`
values via factories (`notFound()`/`quotaExceeded()`/`featureBlocked()`/…);
handlers `throw result.error`; and `jsonError` (renamed from `renderError`) is the
single place that renders any error to an AIP-193 body + access-log line, in
`app.onError`/accessLog.
Why: the previous setup had a string→code mapping (`outcomeError` + the `OUTCOME`
table) living in parallel with a type→code mapping (`mapDomainError`), plus inline
`apiError(c, <status>, …)` calls that hand-wrote the status at every site — exactly
the drift that left the same `quota_exceeded` at 400 in one handler and 422 in the
rest. Now the status/reason live once, in the factory.
- Add `server/usecases/ports/app-error.ts`: `AppError` + factories. Status/reason
are baked in per factory, so no usecase or handler writes an HTTP code or a
magic-string reason. `AppError` also carries optional response headers
(`Retry-After`) via a `rateLimited()` factory.
- Delete `apiError`, `outcomeError`, the `OUTCOME` table, and the dead `ApiError`
class. The 67 inline guard/middleware `apiError` sites became `throw <factory>()`.
- Control-flow outcomes a handler branches on (not just renders) stay discriminated
reasons (e.g. `deleteObject` `not_trashed`); internal shared sub-usecases
(traffic-metering, licensing internals) keep string reasons, mapped at the boundary.
- Regenerate the Go OpenAPI client (saveShare gained a 422 response).
BREAKING CHANGE: POST /shares/{token}/objects quota rejection now returns 422
(was an inconsistent 400); every other quota path already returned 422.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
b3ba6c00ff |
refactor(api)!: unify errors to AIP-193 + Page<T> pagination, enrich access log (#443) (#444)
* refactor(api)!: unify errors to AIP-193 + Page<T> pagination, enrich access log (#443) Settle the API consistency issues from #443 before SDKs ship. Breaking changes across the error envelope, list envelopes, and the generated Go client. Errors → AIP-193 google.rpc.Status (https://google.aip.dev/193): - every error body is now { error: { code, message, status, details:[ErrorInfo] } } - machine-readable, switchable key is details[0].reason (UPPER_SNAKE); status is the canonical google.rpc.Code; dynamic context lives in metadata (string→string) - built once in server/lib/http-errors.ts (buildErrorBody/ApiError/mapDomainError); inline handlers use apiError(c,status,msg,opts?); thrown errors flow through app.onError → renderError. Resolves #8 (one casing; no-storage 503 everywhere) and #9 (resource/maxBytes/conflictingName/licensing fields folded into metadata; featureGateErrorSchema removed) Pagination → Page<T> = { items, total, page, pageSize } via pageSchema + integer pageQuerySchema, applied to every list endpoint. image-hosting/images stays cursor (the one intentional exception). unreadCount moved out of the notifications list into /notifications/stats; entitlements drop the redundant orgId; team invitations use items. Access log: every 4xx/5xx carries reason + full message (set by apiError and renderError); a thrown domain error logs its mapped status (409, not 500); unhandled 500s log the full cause chain while the client gets a generic message. Frontend ApiError exposes reason/metadata/canonicalStatus; consumers updated. Go client regenerated from the new OpenAPI document. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(api): fix e2e name-conflict assertion + cover AIP-193 error branches - e2e/name-conflict.spec.ts: assert body.error.details[0].reason (AIP-193) instead of the removed top-level body.code - unit-test buildErrorBody, ApiError, and every mapDomainError branch (server/lib/http-errors.test.ts) and renderError + isHandledError (server/middleware/error-handler.test.ts) - integration-test the apiError error-branch guards the refactor touched: shares, redirect, site/invitations, objects, store/storefront, and the requirePermission middleware (authz) — restoring patch coverage above target Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(api): drop ad-hoc [spec:] breadcrumbs from new coverage tests lint:spec governs spec↔test traceability: a [spec: id] breadcrumb must map to a documented @id scenario in spec/**/*.feature. The added error-branch coverage tests are not Gherkin scenarios, so reference no spec id — use plain titles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(objects): allow the file-manager pageSize (500) on the objects list The shared pageQuerySchema caps pageSize at 100, but the file manager loads a whole folder client-side (FILES_PAGE_SIZE=500, transfer dialog 200) — the old z.string() query param was unbounded. With the cap, GET /api/objects?pageSize=500 returned 400, the file-manager list query errored and retried, and the toolbar / table never rendered (e2e: responsive @desktop + name-conflict table state). Raise just this list's ceiling to 1000 (default stays 20); other lists keep the 100 cap. Regression-tested: GET /api/objects?pageSize=500 → 200. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e132cb9e41 |
feat(openapi): complete API coverage with truthful schemas + unified error handling (#442)
* feat(openapi): complete API coverage with truthful schemas + unified error handling
Migrate every resource router to `@hono/zod-openapi` so the global OpenAPI
document (and the SDKs generated from it) covers the whole product API, not just
~15% of it. The document now describes 25 resources with named component schemas,
operationIds, and accurate response shapes.
What changed:
- Unified error handling: a single `mapDomainError` (DownloadError, ObjectUpload-
SessionError, NameConflictError, StorageQuotaExceededError, BackgroundJobError,
WebDavPathError) wired into a global `app.onError`; handlers throw domain errors
instead of hand-rolling per-route try/catch. One shared `ErrorResponse` envelope.
- Shared http helpers (`server/http/openapi.ts`): generic `jsonContent`/`jsonBody`/
`errorResponse` so the precise schema type reaches `createRoute` — typing
`c.req.valid()` and strictly checking `c.json()` returns (no widened `z.ZodType`).
- Schemas are the truth: response schemas are named (`.openapi('X')`), wire-shaped
(ISO-string timestamps via per-resource `toXDTO` mappers where the domain type
uses `Date`), and strictly enforced against handler returns. The strict pass
surfaced and fixed several latent schema lies (e.g. transfer result shape,
download-task delete tombstone, object `purged`).
- operationId + summary on every route → clean SDK method names.
- Curated out of the public SDK (kept as plain routes): the `/r` redirect resolver,
store webhook receiver, internal telemetry endpoint, the PicGo/ShareX image
upload tool endpoint, the share download redirect, and cron-secret licensing
sync endpoints.
- Disambiguated user operationIds that collided with better-auth's admin API;
dropped `additionalProperties` schemas that oapi-codegen mis-generates.
- Regenerated the Go downloader client and realigned its hand-written wrapper to
the operationId-derived names.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(cmd): gofmt the realigned downloader client
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
f31278b434 | refactor(cli): move zpan command module to cmd |