Commit Graph
23 Commits
Author SHA1 Message Date
Jasper VanandClaude Fable 5 7bad8d2aea fix: audit must-fixes + product gaps (SSRF, move-quota, multipart, password-reset, trash retention, notifications) (#428)
* fix(downloads): block SSRF targets in remote-download source URL

The remote-download source URI was only length-validated, so an
authenticated editor could point a task at the cloud metadata endpoint,
loopback, or RFC 1918 hosts and have the response exfiltrated to their
own drive. Add a shared isSafeHttpUrl/isBlockedUrlHost guard (scheme
allowlist + private/loopback/link-local/metadata/IPv6 blocking) and
cross-check source type vs uri in createDownloadTaskSchema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(api): cover 9 untested src/lib/api.ts wrappers

Adds api.test.ts coverage (RPC path, method, payload, success + ApiError
paths) for listObjectsByPath, isNameConflictError, listAdminAuthProviders,
upsertAuthProvider, deleteAuthProvider, listInviteCodes, generateInviteCodes,
deleteInviteCode, and listTeamActivities — satisfying the CLAUDE.md coverage
gate that otherwise blocks PRs touching api.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(spaces): release source quota on cross-space move

A cross-space "move" copied bytes into the target (reserving quota there)
but only trashed the source. Trashed files still count toward usage, so the
moved bytes were billed in both spaces and the source never freed — contrary
to the design doc ("copy + delete source, quota effectively transfers").

Purge the source subtree (independent S3 copy already exists in the target)
instead of trashing it, which deletes the objects, cascades share cleanup,
and reconciles usage. Rename the response field sourceTrashed -> sourceDeleted
and update the move hint copy accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(upload): wire S3 multipart for large files

The upload UI only ever did a single presigned PUT, which caps at S3's
5 GiB limit and fails the whole transfer on any network blip — despite a
complete multipart backend (object-upload-sessions) sitting unused.

Add uploadPartToS3 (PUTs a part, returns its ETag) and a multipart-upload
orchestrator: open session -> presign parts in batches of 100 -> PUT parts
with bounded concurrency and per-part retry -> complete. Files over 100 MiB
take this path; smaller files keep the single-PUT flow. Cancellation aborts
the multipart and the draft. Also fixes the presignObjectUploadParts wrapper
type to match the server's actual `url` field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(auth): add password-reset flow

There was no self-service password recovery — a forgotten password needed
admin intervention. SMTP/email sending was already built; this wires the
last mile: better-auth sendResetPassword (reset email), a "Forgot password?"
link on sign-in, and /forgot-password + /reset-password pages. The
forgot-password page never reveals whether an account exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(trash): auto-purge trashed items past a retention window

Trashed files counted toward quota forever — trash never auto-emptied, so
storage was never reclaimed without a manual "empty trash". Add a daily cron
(CF Workers 0 4 * * * + Node setInterval) that purges trashed items older than
ZPAN_TRASH_RETENTION_DAYS (default 30, 0 disables) across all orgs, reusing the
existing purge path so S3 objects, share references, and quota are all cleaned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(notifications): typed NotificationType, i18n rendering, team-join

Notifications were a bare-string type with only 3 producers, and server copy
was stored as hardcoded English (zh users saw English).

- Add a NotificationType union in shared/ and type the notification service.
- Render notification title/body client-side from type + metadata via i18n,
  falling back to stored strings for older rows (fixes the hardcoded-English gap).
- Notify users when they join a team (team_join).

(Login auditing was intentionally dropped: reusing the activity-events feed for
sign_in events would spam every user's per-org activity timeline. Proper auth
auditing belongs in a dedicated log and can be added separately.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: cover SSRF guard and multipart upload branches

Raise patch coverage on the new code: uploadPartToS3 pre-aborted-signal and
network-error paths, the url-safety octet-overflow and public-IPv6 branches,
and the invalid-magnet rejection in the download-task schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:06:25 -04:00
saltboandClaude Fable 5 60c51cf3b1 fix(auth): eliminate get-session hangs from cross-request pending init
better-auth starts its $context init synchronously inside betterAuth(),
within whichever request constructs the instance. Init eagerly resolves
all social providers, and ours were 35 async functions doing one D1
query each. When the isolate's first request didn't touch auth (share
SSR, /r/*, public APIs) or disconnected mid-init, its I/O context died
with the queries in flight and $context never settled — on Workers a
pending promise awaited from a later request never resolves, so the
cached auth instance hung every subsequent auth call in the isolate
(the recurring "get-session pending forever / 10s timeout" reports).

- load all OAuth provider configs with one snapshot query; register
  builtin providers as static objects (init does zero per-provider I/O)
- await auth.$context before returning from createAuth so a cached
  instance can never carry a pending promise tied to its creating
  request
- only load captcha config for captcha-protected endpoints instead of
  every /api/auth/* request
- cache the resolved site public origin at module scope (the WeakMap
  was keyed by the per-request db instance and never hit on Workers);
  cache settled values only, never promises
- client: share one in-flight get-session across callers regardless of
  TTL, cache resolved values for 5s, never cache failures

Regression tests pin the invariants: createAuth performs exactly one
DB query during init and returns with $context already settled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 00:31:29 -04:00
saltbo a3866e2f67 perf: fix get-session worker slowness and implement client-side cache 2026-06-10 22:45:59 -04:00
saltbo 47b11bfcca fix: update the get-session cache logic
Signed-off-by: saltbo <saltbo@foxmail.com>
2026-06-10 21:51:37 -04:00
saltboandClaude Opus 4.8 53ca110218 feat(about): split runtime into runtime engine + deployment platform
Flatten the instance `runtime` object into two fields: `runtime` (the JS engine,
node | workerd) and `platform` (the deployment host). The About page shows each
as its own row with friendly labels (e.g. "workerd" + "Cloudflare Workers",
"Node.js" + "Docker").

- Detect the platform from the entry file (entry === target): each serverless
  entry declares it; entry-node sniffs Cloud Run (K_SERVICE) / Docker
  (ZPAN_RUNTIME, set in the Dockerfile) / bare node. Cloudflare is detected from
  the D1 binding.
- Decouple the cloud payload: zpan-cloud-sdk fixes runtime { provider, target },
  so CloudInstanceInfo keeps that shape and buildCloudInstanceInfo maps to it;
  buildInstanceInfo serves the richer flat shape to the About API.
- Migrate PostHog instance telemetry to the runtime/platform shape and merge the
  duplicate runtimeInfo in licensing-admin into the shared one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:56:29 -04:00
saltboandClaude Opus 4.8 9c16adbdc0 perf(quota): batch admin quota listing and move monthly reset to cron
The admin quota listing called getEffectiveQuota per org, firing ~8
sequential queries each (N+1). On D1 every query is a network round-trip,
so the endpoint scaled linearly with org count and risked the Workers
subrequest cap.

- Add getEffectiveQuotasByOrg: resolves every org in 2 queries (quota
  rows + active entitlements) and aggregates in memory. Route uses it.
- Remove the inline traffic-period reset writes from the read paths
  (getEffectiveQuota and the listing route). getEffectiveQuota already
  normalizes a stale period in memory, so reads stay correct.
- Add resetExpiredTrafficQuotas and run it on a new monthly cron
  (0 0 1 * *) for CF Workers and a daily idempotent interval for Node.
  The consume write path keeps its atomic reset-and-consume as a
  self-healing fallback if a scheduled run is missed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 03:08:27 -04:00
saltbo 3b5f6c7fb5 fix(telemetry): persist detected site origin 2026-06-08 14:43:34 -04:00
saltbo 48b82cc804 fix(telemetry): avoid generic disable flag 2026-06-08 14:08:32 -04:00
saltbo 1a6a77435b fix(telemetry): disable reports during e2e 2026-06-08 14:04:57 -04:00
saltbo f52213b202 fix(telemetry): standardize instance reports 2026-06-08 13:47:20 -04:00
saltbo 400940204f fix(telemetry): align instance report fields 2026-06-08 13:25:55 -04:00
Jasper Van 1d7ac9b071 fix(telemetry): use built-in product reporting endpoint (#416)
* fix(telemetry): use built-in product reporting endpoint

* fix(telemetry): send reports through posthog capture
2026-06-08 11:32:15 -04:00
Jasper Van de0213ad5d feat: add PostHog instance telemetry (#414)
* feat: add PostHog instance telemetry

Agent-Profile: https://agent-kanban.dev/agents/7bf89fb1be06098c

* fix: include node os release in telemetry

Agent-Profile: https://agent-kanban.dev/agents/7bf89fb1be06098c
2026-06-08 10:50:54 -04:00
saltbo ecb38df84d fix(downloads): make remote usage billing resilient 2026-06-07 12:47:07 -04:00
saltbo 2438275ea8 feat(archive): queue streaming archive jobs 2026-05-15 09:59:08 -04:00
saltbo 88f6e22f72 fix(traffic): sync cloud usage in background 2026-05-09 11:46:26 -04:00
saltbo d75d7e5461 feat(email): add cloudflare worker mail service toggle 2026-04-27 21:28:07 -04:00
Jasper VanandBob 29102e623d feat: v2.6 Z6 — 6h background entitlement refresh (#345)
- Add server/services/licensing-refresh-runner.ts: shared runner with
  5-min dedup guard, structured INFO logs, and no-op for unbound state
- Add workers/scheduled.ts + export scheduled() in workers/bootstrap.ts
  for CF Workers cron (every 6 hours)
- Add [triggers] crons = ["0 */6 * * *"] to wrangler.toml
- Add setInterval refresh on boot in server/entry-node.ts with
  "licensing.refresh.scheduler.started interval=6h" log
- Add POST /api/licensing/refresh-cron?secret=... public endpoint
  (timing-safe secret comparison) for non-CF platforms
- Extract ZPAN_CLOUD_URL_DEFAULT to shared/constants.ts, replacing
  four duplicated literals
- Document REFRESH_CRON_SECRET + scheduler setup in all 5 non-CF
  deploy guides (vercel, netlify, aws-lambda, azure-functions, cloud-run)

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 08:22:10 -04:00
Jasper VanandClaude Sonnet 4.6 66b3ee3435 feat: public share landing page /s/:token + Workers SSR OG meta (#312)
* 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>
2026-04-20 13:16:20 -04:00
4a5fbb7f4a feat(auth): add dynamic OAuth provider system (#282)
* 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>
2026-04-12 21:46:11 -04:00
saltboandClaude Opus 4.6 ad69c386f2 fix(auth): infer baseURL and trustedOrigins from request when not configured
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>
2026-04-12 11:57:51 -04:00
saltboandClaude Opus 4.6 575c10c77d fix(ci): provide fallback BETTER_AUTH_SECRET in bootstrap cf-test
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>
2026-04-12 11:25:09 -04:00
saltboandClaude Opus 4.6 3b80eff413 feat: migrate from CF Pages to Workers with one-click deploy button
- 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>
2026-04-12 10:40:15 -04:00