Files
zpan/wrangler.toml
T
Jasper VanandClaude Opus 4.8 f24c6e2021 feat(avatar): self-host avatars on R2 on Workers + fix avatar refresh/fallback (#468)
* fix(avatar): refresh session after change and show fallback on remove

Two avatar-display bugs surfaced post-#456:
- After uploading/removing an avatar the UI showed the old image until a full
  reload. refreshSession() now calls getSession({ disableCookieCache: true }) to
  re-read user.image past the 5-min session cookie cache, then
  $store.notify('$sessionSignal') so useSession() actually refetches and
  re-renders (an external endpoint never toggles better-auth's session signal).
- Removing an avatar left a blank circle: the conditional `{user.image && <AvatarImage>}`
  unmounts the radix Image, which keeps a stale "loaded" status so the Fallback
  stays hidden. Always render <AvatarImage src={user?.image ?? undefined}> so radix
  re-runs its loading status (src -> undefined => "error") and shows the initials.

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

* feat(avatar): self-host avatars on R2 when deployed on Workers

The image-upload gateway now branches per request on the AVATARS R2 binding:
- binding present (Cloudflare) -> upload straight to R2 (key `scope/id`, content
  type in R2 metadata, a content-hash `?v=` cache-buster) and return either an
  AVATARS_PUBLIC_URL (R2 custom domain) URL or a relative /api/avatar-blobs URL.
- binding absent (Node/Docker, or a Worker without it) -> the existing ZPan Cloud
  avatar service, unchanged.

Adds a public GET /api/avatar-blobs/:scope/:id route that streams the blob from
the AVATARS binding (so local miniflare, which gives R2 no public URL, can serve
avatars too). AVATARS_BINDING / R2BucketLike live in platform/interface so both
the adapter and the http route can use them without crossing the arch boundary.
wrangler.toml declares the AVATARS bucket (prod + staging).

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

* ci(deploy): provision the zpan-avatars R2 bucket + AVATARS_PUBLIC_URL

The AVATARS R2 binding added for self-hosted avatars needs the bucket to exist on
deploy. Mirror the resource-provisioning pattern (D1/Queue): create zpan-avatars if
missing, enable its managed public URL, and upsert AVATARS_PUBLIC_URL so prod serves
avatars straight from R2 (zero Worker egress). Without the secret the app still works
via its /api/avatar-blobs route.

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

* refactor(avatar): reuse the original PUBLIC_IMAGES bucket/binding/env names

Keep the same Cloudflare resource names as before #456 removed them so the existing
`zpan-public-images` bucket is reused (not orphaned) and the API token scopes still
apply: R2 binding PUBLIC_IMAGES, bucket zpan-public-images(-staging), public-URL
secret PUBLIC_IMAGES_URL. Pure rename of the AVATARS naming I'd introduced — no
behavior change.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 02:05:09 -04:00

77 lines
2.1 KiB
TOML

name = "zpan"
main = "./workers/bootstrap.ts"
compatibility_date = "2026-04-12"
compatibility_flags = ["nodejs_compat", "global_fetch_strictly_public"]
[assets]
binding = "ASSETS"
not_found_handling = "single-page-application"
run_worker_first = ["/api/*", "/dav", "/dav/*", "/r/*", "/s/*"]
[[d1_databases]]
binding = "DB"
database_name = "zpan-db"
database_id = "5bd64957-fa49-4da0-8b7b-6e0ad84037e7"
migrations_dir = "./migrations"
[[send_email]]
name = "EMAIL"
[[queues.producers]]
binding = "ARCHIVE_QUEUE"
queue = "zpan-archive-jobs"
[[queues.consumers]]
queue = "zpan-archive-jobs"
max_batch_size = 1
max_batch_timeout = 1
max_retries = 3
# Self-hosted avatar storage. When present, avatars upload straight to R2 (served from
# /api/avatar-blobs, or from PUBLIC_IMAGES_URL if an R2 custom domain is configured)
# instead of the ZPan Cloud avatar service.
[[r2_buckets]]
binding = "PUBLIC_IMAGES"
bucket_name = "zpan-public-images"
[observability]
enabled = true
[triggers]
crons = ["*/10 * * * *", "0 */6 * * *", "0 */12 * * *", "0 0 1 * *", "0 4 * * *"]
# ----------------------------------------------------------------------------
# Staging environment — used by non-production branch builds.
# CLOUDFLARE_ENV=staging is set automatically in the build script.
# Workers Builds creates preview versions on the same zpan Worker
# but with staging D1 binding.
# ----------------------------------------------------------------------------
[env.staging]
[env.staging.vars]
BETTER_AUTH_URL = "https://zpan-staging.saltbo.workers.dev"
ZPAN_CLOUD_URL = "https://zpan-cloud-staging.saltbo.workers.dev"
[[env.staging.d1_databases]]
binding = "DB"
database_name = "zpan-db-staging"
database_id = "a9197d7a-6524-42f0-b646-e714dcb30b3b"
migrations_dir = "./migrations"
[[env.staging.r2_buckets]]
binding = "PUBLIC_IMAGES"
bucket_name = "zpan-public-images-staging"
[[env.staging.send_email]]
name = "EMAIL"
[[env.staging.queues.producers]]
binding = "ARCHIVE_QUEUE"
queue = "zpan-archive-jobs-staging"
[[env.staging.queues.consumers]]
queue = "zpan-archive-jobs-staging"
max_batch_size = 1
max_batch_timeout = 1
max_retries = 3