- Add license-state.ts helper for reading/writing license state as
system_options key-value pairs instead of a dedicated singleton table
- Rewrite refresh.ts, has-feature.ts, entitlement.ts, licensing-admin.ts,
licensing-refresh-runner.ts to use license-state helpers
- Generate migration 0014 to drop license_binding table
- Update all 10 test files to use setLicenseOptions instead of
db.insert(licenseBinding)
- All 2809 tests pass
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comprehensive end-to-end test covering:
- Live cloud API contract (pairing create/poll, entitlement 401)
- Feature gates: community (unbound) → Pro features blocked
- Feature gates: Pro binding → all 4 features enabled
- Feature gates: expired cert → features revoked
- Feature gates: partial features (subset of Pro)
- Full lifecycle: pair → approve → activate → open_registration → unbind → blocked
- PASETO verification: reject unknown keys, verify PUBLIC_KEYS configured
- System options: 402 without Pro, 201 with Pro for open_registration
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Send refresh token as Authorization Bearer header (not JSON body)
- Read 'certificate' field from refresh response (was 'entitlement')
- Handle both PASETO tokens and legacy JSON in cached certificates
- Verify PASETO tokens in getPlanFromCert() for admin display
- Extract expires_at from snake_case entitlement in pairing poll
- Update all related tests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* 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>
- server/routes/quotas.ts: gate PUT /:orgId with requireFeature('team_quotas') → 402 on Community
- server/services/matter.ts: add teamQuotaEnabled param to incrementUsageIfAllowed and confirmUpload; when false, skip per-team quota check but still track storage usage
- server/services/save-to-drive.ts: thread teamQuotaEnabled through saveShareToDrive → saveFile/saveFolderRecursive
- server/routes/objects.ts: check hasFeature('team_quotas') before confirmUpload
- server/routes/shares.ts: skip isQuotaSufficient pre-check and pass teamQuotaEnabled to saveShareToDriveService when not Pro
- src/routes/_authenticated/admin/users/index.tsx: hide quota column/button behind useEntitlement('team_quotas'); show UpgradeHint when not Pro
- server/test/setup.ts: add seedProLicense helper for integration tests
- Update affected integration tests to seed Pro license where quota enforcement is expected
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
- Add COMMUNITY_TEAM_LIMIT=3 to shared/constants so both server and frontend share one source of truth
- Add server/services/team-count-guard.ts: countUserOrgs + checkTeamLimit (consults licensing state)
- Hook into better-auth organization.beforeCreateOrganization to throw 402 when limit is reached
- Update /teams UI: isAtLimit guard (with loading protection) shows ProBadge on "New Team" button and opens UpgradeHint dialog instead of create dialog when user is at limit
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
* feat: v2.6 Z7 white-label branding — logo, favicon, wordmark, hide footer
Implements the white-label branding feature gated by `white_label` Pro entitlement:
Backend:
- GET /api/branding (public) — returns BrandingConfig from systemOptions
- PUT /api/admin/branding (admin + requireFeature) — multipart upload for
logo/favicon files + wordmark_text/hide_powered_by fields
- DELETE /api/admin/branding/:field (admin + requireFeature) — resets one field
- server/services/branding.ts — S3 upload to _system/branding/, atomic
upsertOption via onConflictDoUpdate
Frontend:
- BrandingProvider wraps the app; fetches branding on boot, applies favicon via
<link>, sets --site-wordmark CSS var
- AppSidebar uses branding context: custom logo src, wordmark text, and
"Powered by ZPan" footer (hidden when hide_powered_by is set)
- Admin /branding page: shows UpgradeHint for non-Pro, upload form with live
preview panel for Pro users
- BrandingConfig and BrandingField types moved to shared/types/ per convention
Tests: integration tests for all auth guards (401/403/402), validation (415/422),
and happy paths; frontend api.test.ts covers getBranding, saveBranding,
resetBrandingField.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* test: add S3 upload and validation coverage for branding routes
Add integration tests for logo/favicon file upload, MIME validation,
size limits, missing storage, and seeded branding values to bring
patch coverage above codecov threshold.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
- Add server/services/signup-mode-guard.ts: getEffectiveSignupMode()
applies Pro check when stored mode is 'open'; non-Pro falls back to
invite-only so downgraded instances stay secure
- Update server/auth.ts: replace internal getSignupMode() with
getEffectiveSignupMode() from new service
- Update server/routes/system.ts: PUT auth_signup_mode=open returns 402
feature_not_available when open_registration feature is absent
- Update RegistrationModeSection: 'open' radio disabled with ProBadge
for non-Pro; clicking it opens UpgradeHint dialog instead of saving
- Add integration tests: 6-combination matrix (3 modes × 2 plans) plus
admin API guard tests
- Update auth.integration.test.ts: split open-mode tests for Pro/non-Pro
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
- Add `licenseBinding` singleton table (id=1) to server/db/schema.ts
- Correct `0012_image-hosting` journal idx from 11→12 (matches its prefix;
was left wrong by previous rename-only fix in ef1fab8), then auto-generate
migration 0013_licensing.sql via drizzle-kit
- New 0013_snapshot.json created; 0012_snapshot.json unchanged
- Add ProFeatures enum to shared/constants.ts
- Add shared/types/licensing.ts with LicenseEntitlement, ProFeature, BindingState
- Export new types from shared/types/index.ts
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
On Cloudflare Workers deployments with PUBLIC_IMAGES (R2 binding) +
PUBLIC_IMAGES_URL (env) configured, writes go through the R2 binding
directly — zero auth overhead, zero egress cost, no AWS SDK in the hot
path. Reads are served straight from R2's managed public domain; no
Worker round-trip per image.
Non-CF deployments (Node/Docker/Lambda/Vercel/etc.) keep the existing
behavior: select the DB-configured mode='public' storage row and use
S3Service. The only requirement is that users still add a public
storage via Admin → Storages.
`uploadPublicImage(platform, prefix, id, file)` and
`deletePublicImageVariants(platform, prefix, id)` now take a Platform
rather than a bare Database. They internally pick the backend:
getBackend:
if getBinding('PUBLIC_IMAGES') && getEnv('PUBLIC_IMAGES_URL')
→ R2 backend
else if selectStorage(db, 'public') succeeds
→ S3 backend
else
→ none (returns 503)
The R2Bucket type is declared locally (minimal structural shape) so we
avoid pulling @cloudflare/workers-types into non-CF builds.
12 new unit tests in image-upload.test.ts cover both backends —
mime/size validation, URL construction (including trailing-slash
normalization + jpeg→jpg extension), delete-all-variants, and the
fallback precedence matrix (binding alone / URL alone / neither →
expected result).
Callers updated:
- server/routes/me.ts — PUT/DELETE /avatar
- server/routes/teams.ts — PUT/DELETE /:teamId/logo
Existing integration tests (54 cases across me + teams) continue to
pass via the S3 fallback path — they use mockPlatform without a binding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Platform-native bindings (Cloudflare R2/D1/KV, Azure Storage contexts,
etc.) are not representable as strings, so getEnv() can't carry them.
Add a typed getBinding<T>() accessor: returns the binding on platforms
that support it, undefined on others.
Callers branch on the return — e.g. \`getBinding<R2Bucket>('PUBLIC_IMAGES')\`
will be defined on CF and undefined on Node/Docker, letting the same
code pick a runtime-appropriate path without platform-specific imports.
Used in the next commit to switch the public image upload flow to R2
binding on CF (zero-auth, zero-egress) while keeping the S3 fallback
for every other platform.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avatar upload (T7 #327) was a 3-endpoint presigned-URL flow:
POST /api/profile/avatar (presign)
POST /api/profile/avatar/commit (verify + write)
DELETE /api/profile/avatar
This mixed two anti-patterns: (1) an action verb `/commit` in the URL
and (2) two-phase client orchestration per upload. Closed PR #335 was
extending the same pattern to org logo — 6 endpoints for what's
conceptually one operation ("replace this image").
Collapse to two clean REST resources:
PUT /api/me/avatar (multipart/form-data, file field)
DELETE /api/me/avatar
PUT /api/teams/:teamId/logo (multipart/form-data, file field)
DELETE /api/teams/:teamId/logo
PUT is idempotent — re-uploading produces the same resource state,
matching "set the avatar" semantics. Stream-proxy through Worker
(read bytes → putObject → headObject no longer needed since we just
wrote it → DB update → return public URL). Zero client orchestration:
one fetch per user action.
### Backend
- NEW `server/services/image-upload.ts` — shared `uploadPublicImage` +
`deletePublicImageVariants` helpers. Both routes use them, zero
duplication. Constants `PUBLIC_IMAGE_MIMES` (png/jpg/webp) and
`MAX_PUBLIC_IMAGE_SIZE` (2 MiB) live in shared/schemas for client +
server reuse.
- NEW `server/routes/me.ts` — `/api/me/*` namespace for session-scoped
resources. Separate from `/api/profiles/:username` (public read-only).
- EXTENDED `server/routes/teams.ts` with `:teamId/logo` PUT/DELETE.
Owner/admin only via `getMemberRole`.
- REMOVED avatar endpoints from `server/routes/profile.ts` and the
`profileMe` mount from `server/app.ts`.
- REMOVED `AVATAR_MIMES` / `requestAvatarUploadSchema` /
`commitAvatarSchema` from shared/schemas; superseded by the simpler
constants above.
### Frontend
- Hono RPC client: `profileMeApi` → `meApi` rename; new DELETE wrappers
go through RPC for type safety. PUT goes through raw fetch
(multipart/form-data — Hono RPC doesn't express it cleanly).
- NEW wrappers: `uploadAvatar(file)`, `deleteAvatar()`,
`uploadTeamLogo(teamId, file)`, `deleteTeamLogo(teamId)`.
- REMOVED wrappers: `requestAvatarUpload`, `commitAvatar`, the old
`deleteAvatar` (3 calls → 2).
- Settings Profile AvatarCard: one mutation (upload) instead of three
(presign → uploadToS3 → commit). Same UX, fewer round trips + less
code.
- Teams settings page: redesigned to the Vercel-style card layout that
#334 established for other settings tabs (LogoCard / TeamNameCard /
SlugCard / DangerZoneCard). Logo uses hover-to-upload (Cal.com
pattern) — click avatar → camera overlay → file picker.
### Tests
- NEW `server/routes/me.integration.test.ts` — 10 cases covering auth
(401), Content-Type validation (415), missing file (400), mime
rejection (400), size > 2 MiB (413), no public storage (503), happy
path, PUT idempotency, DELETE authoritative DB clear, graceful
fallback when no public storage.
- EXTENDED `server/routes/teams.integration.test.ts` with 11 logo cases
mirroring the above + owner-vs-admin permission matrix.
- REMOVED avatar tests from `server/routes/profile.integration.test.ts`
(those endpoints no longer exist).
- Frontend `src/lib/api.test.ts`: 4 new test blocks for the 4 new
wrappers — path/method/form-body/error assertions, plus URL-encoding
check for teamId in the team logo wrapper.
Total: 83 test files, 2587 tests all green (+~15 new cases; the rest
was replacing avatar tests 1:1 with new PUT-based equivalents).
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 T4 — Netlify deployment target
- server/entry-netlify.ts: Netlify Functions v2 (ESM) handler using hono/netlify
adapter; connects to Turso via @libsql/client; skips in-process migrations
(workflow applies them before deploy via drizzle-kit)
- deploy/netlify/netlify.toml: build command, functions directory, SPA fallback redirect
- .github/workflows/deploy-netlify.yml: 8-step workflow — secret guard, tag resolve,
Turso migrations, build, netlify deploy --prod, BETTER_AUTH_SECRET first-deploy, summary
- package.json: add build:netlify script (tsup ESM → netlify/functions)
- docs/deploy/netlify.md: 5-section setup guide covering Turso, site creation,
secrets, deploy trigger, first-boot storage setup, and cost breakdown
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: address Netlify deploy review blockers
BLOCKER 1 — move BETTER_AUTH_SECRET step before Deploy in workflow so
the function always has the secret set before its first cold start.
BLOCKER 2 — replace inline platform construction in entry-netlify.ts
with createLibsqlPlatform(); removes duplicated db/schema wiring and
re-unifies with the shared factory. migrate() runs at cold start and
is idempotent (~50–100ms) per the workflow's prior drizzle-kit migrate.
BLOCKER 3 — add --external @libsql/client to build:netlify so tsup
leaves the native-binding package for Netlify to resolve; switch
netlify.toml to node_bundler=esbuild so Netlify bundles @libsql/client
from node_modules. Add included_files=["migrations/**"] so the
migrations folder is available in the function zip for migrate().
Minor — replace 2>/dev/null with 2>&1 in deploy step so netlify-cli
errors surface in CI logs instead of being silently swallowed.
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>
* feat: add Vercel deployment target (Node runtime + Turso)
Adds first-class Vercel support: server/entry-vercel.ts using hono/vercel
handler, deploy/vercel/vercel.json with nodejs22.x function config and SPA
rewrites, build:vercel npm script producing api/entry-vercel.js + dist/,
deploy-vercel GitHub Actions workflow (8-step: secrets check, tag resolve,
checkout, install, migrate, build, link, deploy), and docs/deploy/vercel.md
documenting secrets, quick-start, local dev, and pricing notes.
Edge runtime is explicitly not used — @aws-sdk/client-s3 requires Node APIs.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: auto-generate BETTER_AUTH_SECRET on first Vercel deploy
Remove BETTER_AUTH_SECRET from the required secrets check. Add a
dedicated step that detects whether the secret already exists in the
Vercel project env via `vercel env ls production`, then either upserts
the user-supplied GitHub secret, auto-generates one with openssl on
first deploy, or skips if already present. Auto-generation case appends
a backup warning to GITHUB_STEP_SUMMARY. Docs move BETTER_AUTH_SECRET
to Optional Secrets with a note about the auto-gen behaviour.
Matches the existing CF Workers deploy.yml pattern (step 8 contract).
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
- POST /api/profile/avatar: validates mime (png/jpg/webp) and size (≤2 MiB),
returns presigned PUT URL for _system/avatars/<userId>.<ext> on public storage
- POST /api/profile/avatar/commit: verifies S3 object exists, updates user.image
with the public URL from the storage's endpoint/customHost
- DELETE /api/profile/avatar: clears user.image (authoritative), best-effort
removes all MIME-variant S3 objects via Promise.allSettled
- Shared schemas: AVATAR_MIMES, MAX_AVATAR_SIZE, requestAvatarUploadSchema
(with .max(MAX_AVATAR_SIZE) enforcement), commitAvatarSchema
- Frontend: AvatarSection in Settings -> Profile with file picker, drag-drop,
preview, and remove button; uses uploadToS3 + commitAvatar pattern
- App sidebar: renders AvatarImage when user.image is present
- Integration tests: 11 new test cases covering auth, mime/size validation,
presign generation, commit persistence, delete cleanup
- API tests: 7 new test cases for requestAvatarUpload, commitAvatar, deleteAvatar
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
* feat: add libSQL (Turso) platform adapter and Docker opt-in
- server/platform/libsql.ts: createLibsqlPlatform() using @libsql/client +
drizzle-orm/libsql; accepts plain env record; async migrate at boot;
authToken optional for file:// URLs
- server/entry-node.ts: select platform at startup — libsql when
TURSO_DATABASE_URL is set, otherwise existing SQLite via createNodePlatform()
- drizzle.config.ts: switch to turso dialect when TURSO_DATABASE_URL is set
- vitest.libsql.config.ts + server/platform/libsql.libsql-test.ts: smoke suite
covering connect, migrations, insert/select against users + storages tables
- package.json: add @libsql/client dependency; add test:libsql script;
externalize @libsql/client in build:node tsup command
- vitest.config.ts: exclude *.libsql-test.ts from coverage
- docs/deploy/docker.md: document Turso opt-in with copy-pasteable
docker-compose snippet
- CONTRIBUTING.md: add Turso migrate path paragraph under Database Migrations
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* refactor: turn bootstrap.ts into a Platform-accepting factory
- server/bootstrap.ts: replace singleton module-scope script with
exportable createBootstrap(platform) async factory; reads
BETTER_AUTH_SECRET/BETTER_AUTH_URL/TRUSTED_ORIGINS from platform.getEnv
so every future entry (Lambda, Vercel, Netlify, Azure) can reuse it
- server/entry-node.ts: slim down to platform selection + createBootstrap
call; no more duplicate auth/app wiring
- server/dev.ts: thin vite-dev-server entry that creates NodePlatform and
calls createBootstrap; replaces the former default export in bootstrap.ts
- vite.config.ts: update node dev server entry to server/dev.ts
- server/platform/libsql.ts: fix getEnv to check env record before
falling back to process.env, matching the cloudflare.ts pattern
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* style: apply biome auto-fixes for pre-existing lint issues
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
The CF test expected 415 for JSON POST, but now that JSON base64
uploads are supported it reaches the config check (403) instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Allow empty Referer in allowlist check — matches industry standard
(Cloudflare, AWS, etc.): anti-hotlinking targets other websites, not
direct access from tools/address bar
- Support explicit 'path' field in JSON base64 uploads (uPic)
- Set global API key rate limit: 60 req / 60s window
- Remove temporary debug logging
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
uPic sends images as base64-encoded JSON: {"file": "iVBORw0K..."}
with Content-Type: application/json, NOT multipart/form-data.
Refactor POST /images to accept both formats:
- multipart/form-data: PicGo, ShareX (file in form field)
- application/json: uPic (base64 string in file field)
Also add magic-byte MIME detection (PNG/JPEG/GIF/WEBP headers)
for when tools don't provide MIME type.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two fixes for external tool compatibility:
1. Non-multipart POST with auth returns 200 instead of 415. uPic and
similar tools send a JSON POST to validate the connection before
uploading — the 415 made validation fail even with correct config.
2. Infer MIME type from file extension when the client sends
application/octet-stream or empty type. Some tools (uPic, PicGo)
don't always set the correct MIME on the multipart file field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two issues:
1. listImageHostings did not filter by status, so draft images appeared
in the gallery but /r/ only serves active ones → 404 on thumbnails.
2. Referer allowlist blocked same-origin requests from the Web UI,
so users who configured a referer whitelist could not view their
own images in the dashboard → 403.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The test sent { name: 'Nope' } without the required 'action' field,
causing zod validation to reject with 400 instead of reaching the
handler's 404 path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bug 1: server/services/s3.ts — putObject passed Web ReadableStream to
AWS SDK which only accepts Node Readable or Uint8Array. Convert to
Uint8Array via Response.arrayBuffer() for cross-runtime compatibility.
Bug 2: server/routes/ihost.ts + shared/schemas — presign endpoint
returned 400 (zod validation) instead of 413 for oversized files.
Moved size check from schema .max() to handler with proper 413 status.
Bug 3: src/lib/api.ts — createIhostApiKey sent `permissions` in the
request body, but better-auth's apiKey plugin rejects client-set
permissions (SERVER_ONLY_PROPERTY). Removed it; server defaultPermissions
handles it automatically.
Also: session.create.before now checks org existence by slug before
creating, preventing UNIQUE constraint failures when membership was
revoked but org still exists.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
better-auth defers `user.create.after` hooks until after the transaction
commits, but the session cookie cache is written inside the transaction.
This caused `activeOrganizationId` to be null on first load after sign-up,
breaking features that depend on an active organization (e.g. Image Host).
Solution: create the personal org in `session.create.before` (which runs
inside the transaction, after user INSERT) and set `activeOrganizationId`
on the session before it's cached. Also keep idempotent org creation in
`user.create.after` so orgs are created even without auto sign-in (e.g.
when email verification is required).
Additionally fix an ambiguous Playwright locator in image-host.spec.ts
that matched multiple elements.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: v2.4.0 T7 — Image Host gallery page with FileManager reuse
- Add ImageHostView component that wires FileManager with image-host-specific
config: upload via /api/ihost/images presigned flow, delete with 5s undo toast,
copy URL in raw/Markdown/HTML/BBCode formats, and thumbnail rendering
- Extend FileManager with new capabilities (copyUrl, delete), getThumbnailUrl
prop, onDeleteItems/onCopyUrl callbacks, and viewModeStorageKey for isolated
view-mode persistence per page
- Extend FilesGrid with optional getThumbnailUrl prop: renders lazy-loaded image
thumbnails with FileIcon fallback on error; backward-compatible with Files page
- Extend FileRowActions with Copy URL submenu (raw/Markdown/HTML/BBCode) and a
Delete action separate from Move to Trash; fully backward-compatible
- Extend UploadDropzone with optional uploadFn prop to bypass the default
object-upload flow; Files page behavior unchanged
- Parameterize useViewMode hook with optional storageKey argument
- Add API wrappers: listIhostImages, createIhostImagePresign, confirmIhostImage,
deleteIhostImage with matching tests in api.test.ts
- Add useClipboard hook; refactor navigator.clipboard.writeText usage in
share-dialog.tsx and shares/index.tsx to use the hook
- Add IhostRoute to rpc.ts
- Add ihost.copy.*, ihost.delete.*, ihost.upload.*, ihost.table.* i18n keys
to en.json and zh.json; add common.copied key
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* fix: use Hono RPC client for ihost API, add presign endpoint, expand test coverage
- Rewrite server/routes/ihost.ts to use method chaining, fixing Hono RPC type
inference (imperative app.post() calls prevented the schema from being typed)
- Extract POST /images/presign as a dedicated typed endpoint (zValidator) for the
browser client; POST /images becomes multipart-only for API-key/PicGo compat
- Frontend: replace raw ihostFetch() with ihostApi RPC calls for all four
wrappers (listIhostImages, createIhostImagePresign, confirmIhostImage,
deleteIhostImage); mime parameter typed as AllowedImageMime
- Update integration tests to use /images/presign for JSON presign cases; adjust
status expectations to 400 (Zod) vs 413/415 (manual checks no longer needed)
- Add unit tests: use-clipboard, image-host-data-source, image-host-view,
file-row-actions, upload-dropzone, use-view-mode custom-key
- Add e2e/image-host.spec.ts: enable feature gate, upload (mocked S3 PUT),
grid→table view switching, copy Markdown URL, delete with Undo, delete permanently
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* test: improve patch coverage for ihost routes and file-row-actions
Add missing 503/401 integration tests for multipart endpoint and API key
error paths. Extract testable pure functions from file-row-actions.tsx and
image-host-view.tsx and update tests to import from source files.
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* test: fix coverage cascade, add component rendering tests and branch tests
Revert buildCopyText export which caused file-manager/files-grid/upload-dropzone
to appear in coverage at 0% via transitive imports. Restore inline switch logic
in handleCopyUrl and define buildCopyText locally in the test.
Install @testing-library/react + jsdom, add React plugin to vitest unit project,
and write FileRowActions rendering tests (file-row-actions.render.test.tsx) to
cover JSX branches including Copy URL sub-menu and delete item.
Add missing DELETE 403 (no config) and storage-null branch tests to
ihost.integration.test.ts to cover uncovered branches in ihost.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: update ihost.cf-test.ts for multipart-only POST /images endpoint
POST /api/ihost/images now returns 415 for JSON (multipart only).
Add separate test for POST /api/ihost/images/presign returning 403
when image hosting is not enabled.
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add /api/ihost/images CRUD API with two-stage and stream-proxy upload
Implements the full image-hosting CRUD at /api/ihost/images:
- POST (JSON): two-stage upload — creates draft row + returns presigned URL
- POST (multipart): stream-proxy to S3 via PicGo-compatible tool response
- GET /: cursor-based list with optional pathPrefix filter
- GET /🆔 detail with org isolation enforced
- PATCH /:id action=confirm: transitions draft → active, increments quota
- DELETE /🆔 hard-deletes S3 object + DB row, decrements quota
Auth: session (all verbs) or apiKey with image-hosting:upload (POST only).
Path validation: no .., no leading/trailing /, max depth 5, max 256 chars.
Collision: auto-appends 4-hex suffix on (orgId, path) conflict.
MIME gate: allows png/jpeg/gif/webp; rejects svg+xml with 415.
Size gate: max 20 MB enforced at both JSON and multipart paths.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix(ihost): resolve PR #317 blockers — API key auth, status codes, test coverage
- Blocker 1: replace raw SQL key lookup with auth.api.verifyApiKey() so the
SHA-256-hashed better-auth API keys are verified correctly
- Blocker 2: add explicit pre-checks in JSON branch returning 413 for size
> 20 MB, 415 for SVG/unsupported MIME before falling through to zod (which
was returning 400 for all of these); also guard non-JSON content type → 415
- Blocker 3: replace raw insertApiKey() SQL helper with createTestApiKey()
that calls auth.api.createApiKey() server-side so tests use properly hashed
keys; fix expected status codes (401 for missing permission, 415/413); add
quota-refund assertion in S3 failure test; add quota exceeded confirm test
- Additional: handle selectStorage failure → 503, use Number.isFinite guard
for Content-Length, add null guards to getOrgId/getUserId test helpers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ihost): correct rebase conflicts — merge T5 schema + remove duplicate tables
- Merge T5's image_hosting_configs/image_hostings FK constraints with CRUD
service (resolveActiveImageByToken + incrementAccessCount) and CRUD schemas
- Remove duplicate table definitions in test/setup.ts left by rebase conflict
resolution (keep T5's FK-constrained versions, add apikey table once)
- Fix org-isolation test: insert a real organization row to satisfy the
image_hostings.org_id FK constraint added by T5
Agent-Profile: https://agent-kanban.dev/agents/$AK_AGENT_ID
* chore: trigger CI on rebased PR #317
Agent-Profile: https://agent-kanban.dev/agents/$AK_AGENT_ID
* chore(ihost): add export comment to trigger CI sync event
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(ihost): add targeted tests to meet 95% patch coverage gate
Cover previously uncovered patch lines:
- s3.ts: add putObject unit test (was 0% — 3 missing lines)
- ihost.ts: add tests for 503 no-storage, 413 Content-Length header,
415 unsupported content-type, 400 zod parse failure, 400 missing
file field, 415 non-image MIME in multipart, 422 quota exceeded in
multipart, nanoid fallback after collision retries, validatePath
edge cases (starts-with-/, ends-with-/, invalid chars, path too long)
- image-hosting.ts: add direct service tests for deleteImageHosting
null guard and confirmImageHosting with size=0; add validatePath
tests via multipart path (bypasses zod max-256 guard)
- ihost.ts: remove dead code (unreachable 'Unknown action' branch —
patchIhostImageSchema discriminated union only allows 'confirm')
Agent-Profile: https://agent-kanban.dev/agents/$AK_AGENT_ID
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds imageHostingDomain middleware that inspects the Host header and,
when it matches a verified customDomain in image_hosting_configs,
resolves the virtual path to an image and 302-redirects to a presigned
inline URL. Non-matching hosts fall through to normal routing.
- server/services/image-hosting.ts: add resolveCustomDomain (host→orgId
via single indexed lookup) and getImageByOrgPath (orgId+path→image)
- server/middleware/image-hosting-domain.ts: new middleware with
lowercase+port-strip normalisation, workers.dev/PUBLIC_APP_HOST
fast-path, referer allowlist enforcement, accessCount increment,
and presigned 302 with Cache-Control: public, max-age=270
- server/app.ts: register middleware on /* after platformMiddleware
and before all route definitions
- server/test/setup.ts: accept optional envOverrides in createTestApp
- integration and CF tests covering all spec scenarios
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
- Replace verb-based URLs with proper HTTP methods and resource nouns:
- Objects: merge PATCH /:id/done, /trash, /restore into PATCH /:id
with discriminated union (action: update|confirm|trash|restore)
- Objects: POST /:id/copy → POST /copy with copyFrom in body
- Objects: POST /batch/move, /batch/trash → PATCH /batch;
POST /batch/delete → DELETE /batch
- Notifications: POST /:id/read → PATCH /:id,
POST /read-all → PATCH /, GET /unread-count → GET /stats
- Users: PUT /:id/status → PATCH /:id
- Teams: POST /join → POST /:teamId/members
- Email-config: POST /test → POST /test-messages
- Invite-codes: POST /validate → POST /validations
- Fix path hierarchy: move admin auth-providers from
/api/auth-providers/admin/* to /api/admin/auth-providers/*
- Rename recycle-bin to trash across API, frontend, and e2e tests
- Update all integration tests, CF tests, unit tests, and schemas
BREAKING CHANGE: all listed API endpoints have changed paths or methods
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Collapse the split /api/share/* (public) and /api/shares/* (authed) into a
single /api/shares resource mounted as two sub-apps (publicShares before
authMiddleware, authedShares after). All action verbs are removed from paths
and replaced with nouns:
POST /api/share/:token/verify → POST /api/shares/:token/sessions
GET /api/share/:token/children → GET /api/shares/:token/objects
GET /api/share/:token/download[/x] → GET /api/shares/:token/objects/:ref
POST /api/shares/:token/save → POST /api/shares/:token/objects
GET /api/shares/:id, DELETE /:id → GET/DELETE /api/shares/:token
Token is now the canonical external identifier; the internal matter id is
never exposed. GET /api/shares/:token handles both visitor and creator views
and returns creator-only fields (id, orgId, recipients, …) only when the
viewer is the creator. /dl/:token and /s/:token short-links are preserved
unchanged.
Drive-by hardening:
- Remove passwordHash from the shared Share wire type; list endpoint uses
explicit column projection so the hash cannot leak.
- revokeShareByToken returns boolean; DELETE handler maps a lost race to 404
instead of propagating an unhandled 500.
- Save-to-drive password gate now uses the shared checkAccessGate helper,
fixing a looseness where any non-empty sharetk cookie bypassed the check.
Frontend: rpc.ts splits into publicSharesApi / authedSharesApi; api.ts
wrappers take token (not id); new buildShareObjectUrl for download URL
construction; ShareView replaces ShareLandingResponse / ShareDetail.
2188 node tests + 43 CF tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dev (vite + miniflare + undici) returns 500 "fetch failed" when the
Worker responds 401 to a POST with a body. undici follows the Fetch
spec's HTTP-auth retry branch, which needs to re-extract the request
body from its source — but the body comes from Node's IncomingMessage
stream, so `body.source` is null and undici throws
`expected non-null body source`. Production (direct CF edge) is
unaffected, this only breaks local dev.
403 is also semantically more accurate: the client isn't performing
HTTP authentication, just supplying a shared secret.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>