* fix(downloads): block SSRF targets in remote-download source URL
The remote-download source URI was only length-validated, so an
authenticated editor could point a task at the cloud metadata endpoint,
loopback, or RFC 1918 hosts and have the response exfiltrated to their
own drive. Add a shared isSafeHttpUrl/isBlockedUrlHost guard (scheme
allowlist + private/loopback/link-local/metadata/IPv6 blocking) and
cross-check source type vs uri in createDownloadTaskSchema.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(api): cover 9 untested src/lib/api.ts wrappers
Adds api.test.ts coverage (RPC path, method, payload, success + ApiError
paths) for listObjectsByPath, isNameConflictError, listAdminAuthProviders,
upsertAuthProvider, deleteAuthProvider, listInviteCodes, generateInviteCodes,
deleteInviteCode, and listTeamActivities — satisfying the CLAUDE.md coverage
gate that otherwise blocks PRs touching api.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(spaces): release source quota on cross-space move
A cross-space "move" copied bytes into the target (reserving quota there)
but only trashed the source. Trashed files still count toward usage, so the
moved bytes were billed in both spaces and the source never freed — contrary
to the design doc ("copy + delete source, quota effectively transfers").
Purge the source subtree (independent S3 copy already exists in the target)
instead of trashing it, which deletes the objects, cascades share cleanup,
and reconciles usage. Rename the response field sourceTrashed -> sourceDeleted
and update the move hint copy accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(upload): wire S3 multipart for large files
The upload UI only ever did a single presigned PUT, which caps at S3's
5 GiB limit and fails the whole transfer on any network blip — despite a
complete multipart backend (object-upload-sessions) sitting unused.
Add uploadPartToS3 (PUTs a part, returns its ETag) and a multipart-upload
orchestrator: open session -> presign parts in batches of 100 -> PUT parts
with bounded concurrency and per-part retry -> complete. Files over 100 MiB
take this path; smaller files keep the single-PUT flow. Cancellation aborts
the multipart and the draft. Also fixes the presignObjectUploadParts wrapper
type to match the server's actual `url` field.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(auth): add password-reset flow
There was no self-service password recovery — a forgotten password needed
admin intervention. SMTP/email sending was already built; this wires the
last mile: better-auth sendResetPassword (reset email), a "Forgot password?"
link on sign-in, and /forgot-password + /reset-password pages. The
forgot-password page never reveals whether an account exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(trash): auto-purge trashed items past a retention window
Trashed files counted toward quota forever — trash never auto-emptied, so
storage was never reclaimed without a manual "empty trash". Add a daily cron
(CF Workers 0 4 * * * + Node setInterval) that purges trashed items older than
ZPAN_TRASH_RETENTION_DAYS (default 30, 0 disables) across all orgs, reusing the
existing purge path so S3 objects, share references, and quota are all cleaned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(notifications): typed NotificationType, i18n rendering, team-join
Notifications were a bare-string type with only 3 producers, and server copy
was stored as hardcoded English (zh users saw English).
- Add a NotificationType union in shared/ and type the notification service.
- Render notification title/body client-side from type + metadata via i18n,
falling back to stored strings for older rows (fixes the hardcoded-English gap).
- Notify users when they join a team (team_join).
(Login auditing was intentionally dropped: reusing the activity-events feed for
sign_in events would spam every user's per-org activity timeline. Proper auth
auditing belongs in a dedicated log and can be added separately.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover SSRF guard and multipart upload branches
Raise patch coverage on the new code: uploadPartToS3 pre-aborted-signal and
network-error paths, the url-safety octet-overflow and public-IPv6 branches,
and the invalid-magnet rejection in the download-task schema.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
better-auth starts its $context init synchronously inside betterAuth(),
within whichever request constructs the instance. Init eagerly resolves
all social providers, and ours were 35 async functions doing one D1
query each. When the isolate's first request didn't touch auth (share
SSR, /r/*, public APIs) or disconnected mid-init, its I/O context died
with the queries in flight and $context never settled — on Workers a
pending promise awaited from a later request never resolves, so the
cached auth instance hung every subsequent auth call in the isolate
(the recurring "get-session pending forever / 10s timeout" reports).
- load all OAuth provider configs with one snapshot query; register
builtin providers as static objects (init does zero per-provider I/O)
- await auth.$context before returning from createAuth so a cached
instance can never carry a pending promise tied to its creating
request
- only load captcha config for captcha-protected endpoints instead of
every /api/auth/* request
- cache the resolved site public origin at module scope (the WeakMap
was keyed by the per-request db instance and never hit on Workers);
cache settled values only, never promises
- client: share one in-flight get-session across callers regardless of
TTL, cache resolved values for 5s, never cache failures
Regression tests pin the invariants: createAuth performs exactly one
DB query during init and returns with $context already settled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flatten the instance `runtime` object into two fields: `runtime` (the JS engine,
node | workerd) and `platform` (the deployment host). The About page shows each
as its own row with friendly labels (e.g. "workerd" + "Cloudflare Workers",
"Node.js" + "Docker").
- Detect the platform from the entry file (entry === target): each serverless
entry declares it; entry-node sniffs Cloud Run (K_SERVICE) / Docker
(ZPAN_RUNTIME, set in the Dockerfile) / bare node. Cloudflare is detected from
the D1 binding.
- Decouple the cloud payload: zpan-cloud-sdk fixes runtime { provider, target },
so CloudInstanceInfo keeps that shape and buildCloudInstanceInfo maps to it;
buildInstanceInfo serves the richer flat shape to the About API.
- Migrate PostHog instance telemetry to the runtime/platform shape and merge the
duplicate runtimeInfo in licensing-admin into the shared one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The admin quota listing called getEffectiveQuota per org, firing ~8
sequential queries each (N+1). On D1 every query is a network round-trip,
so the endpoint scaled linearly with org count and risked the Workers
subrequest cap.
- Add getEffectiveQuotasByOrg: resolves every org in 2 queries (quota
rows + active entitlements) and aggregates in memory. Route uses it.
- Remove the inline traffic-period reset writes from the read paths
(getEffectiveQuota and the listing route). getEffectiveQuota already
normalizes a stale period in memory, so reads stay correct.
- Add resetExpiredTrafficQuotas and run it on a new monthly cron
(0 0 1 * *) for CF Workers and a daily idempotent interval for Node.
The consume write path keeps its atomic reset-and-consume as a
self-healing fallback if a scheduled run is missed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: public share landing page /s/:token + Workers SSR OG meta
- Add SPA route `/s/:token` (TanStack Router, outside _authenticated)
- Implement share components: ShareLanding, FilePreview, FolderBrowser,
PasswordPrompt, SaveToDriveDialog, ShareError
- File preview: image/video/audio/PDF via object URL fetch; fallback for
other types with download CTA
- Folder browser: breadcrumb navigation + children table with download
- Password gate: POST /api/share/:token/verify with error feedback
- Save to drive: workspace + folder picker, quota/password/gone error handling
- Workers SSR: inject OG meta tags for /s/:token requests (title, description,
image, twitter:card); fetch share metadata via /api/share/:token
- Add /s/* to wrangler.toml run_worker_first for SSR routing
- Add zValidator to /:token/children endpoint for typed RPC query params
- Export ShareApiRoute type from server/app.ts; add RPC clients in rpc.ts
- Add share.* i18n keys (en + zh)
- 9 new unit tests covering error code derivation, escaping, i18n coverage
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* test: add coverage for share public API wrappers and path traversal guard
- api.test.ts: add unit tests for getShareLanding, verifySharePassword,
getShareChildren, saveShareToDrive (success + all error paths)
- share-public.integration.test.ts: add path traversal guard test
(.. in path param returns 400 Invalid path)
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* test: cover explicit page/pageSize params in children endpoint
Add integration test for GET /api/share/:token/children with explicit
page and pageSize query params to satisfy codecov/patch branch coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add error path coverage for children endpoint
Cover invalid token (404), trashed matter (410), and non-numeric
page/pageSize (NaN fallback) in GET /:token/children to satisfy
codecov/patch threshold requirements.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add ASSETS binding to wrangler.toml for Workers SSR
Without binding = "ASSETS", env.ASSETS is undefined at runtime
and the /s/:token SSR handler throws error code 1101.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve CF SSR OG meta by calling service layer directly instead of self-subrequest
Cloudflare Workers cannot fetch() their own origin when the path is listed
in run_worker_first — the request loops back and returns a 500 error code 1101.
Replace the HTTP subrequest in fetchShareMeta with a direct call to
resolveShareByToken(platform.db, token) from the service layer.
Add CF integration tests asserting that a valid landing share produces real
og:title metadata and an unknown token falls back gracefully.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(auth): add dynamic OAuth provider system
Admin can configure OAuth/OIDC providers in the database via API.
All 35 built-in better-auth providers are registered as async functions
that read config from system_options at runtime. Custom OIDC providers
use the genericOAuth plugin with configs loaded at auth init time.
New endpoints:
- GET /api/auth-providers (public, enabled only, no secrets)
- GET /api/auth-providers/admin (admin, all configs, masked secrets)
- PUT /api/auth-providers/admin/:providerId (admin, upsert)
- DELETE /api/auth-providers/admin/:providerId (admin, remove)
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: async createTestApp compat in email and invite test files
createAuth became async in the OAuth PR, which made createTestApp async.
Email and invite code test files need await + Awaited<> type wrappers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When BETTER_AUTH_URL is not set, derive it from the incoming request
origin. Same for TRUSTED_ORIGINS. This eliminates the better-auth
warning about missing base URL and makes deploy-button deployments
work without any additional configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI has no .dev.vars file, so env.BETTER_AUTH_SECRET is empty.
Tests that call worker.fetch now use a fallback test secret when
the env binding is not set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace Pages Functions with Workers entry (`workers/bootstrap.ts`)
- Add Deploy to Cloudflare button in README
- Integrate `@cloudflare/vite-plugin` for CF dev with HMR
- Integrate `@hono/vite-dev-server` for Node dev with HMR
- `npm run dev` now defaults to CF Workers mode
- Add `run_worker_first = ["/api/*"]` so static assets stay free
- Extract shared Node bootstrap (`server/bootstrap.ts`) for reuse
- Update all docs from Pages to Workers references
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>