- 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>
* 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(shares): authenticated CRUD API + notification dispatch (T3)
- POST/GET/GET-by-id/DELETE /api/shares endpoints with requireAuth + requireTeamRole('editor') on create
- Shares list returns matter: {name, type, dirtype} and recipientCount per item
- Creator-only access on GET/:id (404 for non-creator) and DELETE (403 for non-creator)
- share-notification service: in-app notification always sent to recipientUserId; email sent conditionally if isEmailConfigured; email failures are caught and logged, never block the 201 response
- createShareRequestSchema added to shared/schemas/share.ts for HTTP boundary validation
- ShareListItem, ShareDetail, ShareMatter types added to shared/types/index.ts; timestamps use string to match JSON wire format
- sharesApi RPC client added to src/lib/rpc.ts; listShares/getShare/deleteShare helpers added to src/lib/api.ts
- Removed dead listSharesByCreator (superseded by listSharesForApi)
- 44 new integration tests; 2026 tests total pass
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* test(shares): add api.ts wrapper tests + DIRECT_NO_RECIPIENTS coverage
- listShares, getShare, deleteShare unit tests in src/lib/api.test.ts
- DIRECT_NO_RECIPIENTS test case in shares.integration.test.ts
- Closes codecov/patch gap (was 87%, target ~94%)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(shares): cover throw-err and dispatch-catch paths in shares route
- Add test for unknown createShare error (line 69: `throw err`)
- Add test for dispatchShareCreated rejection (line 79: `.catch()` console.error)
- shares.ts now at 100% line coverage in integration project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: cover src/lib/api.ts share wrappers in integration project
Add src/lib/api.integration.test.ts with 7 tests for listShares,
getShare, and deleteShare, and extend the vitest integration project
to pick up src/**/*.integration.test.ts so codecov patch coverage
for src/lib/api.ts is reported correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add branch coverage for shares save handler edge cases
Cover two previously uncovered branches in POST /:token/save:
- Line 158: non-recipient with valid sharetk cookie bypasses 401 check
- Line 166: viewer-role member of target org gets 403 (via real DB
membership insert using sign-up response user ID)
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 public share endpoints /s/:token and /dl/:token
Implements v2.3.0 T4: landing share routes, direct download routes, folder
browsing with paginated /children, and HMAC-based child file download refs.
- GET /s/:token — returns share metadata, increments views, optional session
for recipient password-bypass (免密)
- POST /s/:token/verify — verifies password, sets httpOnly cookie
- GET /s/:token/download — access-gated presigned URL redirect
- GET /s/:token/children — paginated folder browsing with breadcrumb
- GET /s/:token/download/:childRef — HMAC-signed child file download
- GET /dl/:token — direct-kind share download (no auth, no password)
- Shared utilities in share-utils.ts (HMAC, cookie, access gate)
- Adds resolveShareByToken with discriminated union for trashed/revoked
distinction; removes getShareByToken (dead production code)
- All Node integration tests and CF tests pass; 90%+ coverage on new files
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: move share public endpoints under /api/* for CF Workers routing
/s/* was not in run_worker_first, so CF Assets served the SPA instead of
reaching the Hono app. Fix by:
- Mount landing share routes at /api/shares/public (covered by /api/*
which is already in run_worker_first); /s/:token is left free for the
T8 SPA landing page
- Add /dl/* to run_worker_first so direct-download links reach the Worker
- Update all test paths accordingly
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: rename share-landing → share-api, mount under /api/share
Move landing-kind share JSON endpoints from /api/shares/public/ to
/api/share/ (matching REST convention) and rename the route file from
share-landing.ts to share-api.ts for clarity.
This eliminates the /s/* route collision with T8's SPA landing page:
wrangler.toml run_worker_first=["/api/*","/dl/*"] now covers all T4
endpoints without any /s/* entry that would block CF Assets from
serving the React SPA.
Also adds CF routing regression tests that assert /s/:token returns
404 from the Hono app (no route registered there) and /api/share/:token
returns JSON, catching this class of deployment miss in CI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: rebase on master, consolidate resolveShareByToken into share.ts
Rebases the public share API branch onto master after T5 (save-to-drive)
merged. Resolves conflicts in server/app.ts to include both T5's
/api/shares route and T4's /api/share + /dl routes.
Consolidates the duplicate resolveShareByToken implementation: T5 had
defined it in save-to-drive.ts with status-based naming; T4 had defined
it in share.ts with found/reason naming. The single authoritative version
now lives in share.ts using T5's status-based type (status: 'ok' |
'not_found' | 'revoked' | 'matter_trashed') so save-to-drive.ts and its
callers (routes/shares.ts) just import from share.ts.
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>
Duplicate folder/file names silently created duplicates under the same
parent. Add DB-level partial unique index on (org_id, parent, LOWER(name))
for active rows, plus a centralized plan/commit helper threaded through
create, rename, move, copy, upload-confirm, and restore. 409 responses
open a Keep Both / Replace / Cancel dialog with sticky "apply to all"
for batch operations; case-insensitive match matches OS conventions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users signing up via social login (OAuth/OIDC) don't provide a username.
Generate one automatically from the email prefix, with random suffix
fallback for conflicts or short prefixes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: team invitation via email and invite link
- Add team invite dialog with email invite and shareable link tabs
- Email invite uses better-auth organizationClient.inviteMember() with configured email service
- Invite link generates a time-limited token stored in new team_invite_links table
- Accept invite page at /teams/invite?token=xxx (auto-join if logged in, redirect to sign-in if not)
- Pending invitations section shows all pending email invites; owners can cancel them
- Add editor/viewer custom roles to better-auth organization plugin
- Add sendInvitationEmail hook to send HTML invite email via configured email service
- Redirect-after-login support: _authenticated layout passes current URL to sign-in
- Add migration 0007_team_invite_links for new table
- Only team owners see the Invite Member button and pending invitations
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* test: add integration tests for team invite service and routes
Cover createInviteLink, getInviteLinkInfo, acceptInviteLink, and
listPendingInvitations service functions. Add route tests for all
public and authenticated team invite endpoints (invite-info, invite-link,
invitations list, and join). Add team_invite_links table to test setup.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* style: fix biome lint in team invite test files
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: resolve CodeQL open-redirect and missing coverage issues
- Validate redirect param in sign-in.tsx is a same-origin relative path
to prevent open redirect and javascript: URI XSS (CodeQL alerts)
- Spread defaultRoles (owner/admin/member) when configuring custom roles
in organization plugin so built-in roles retain their permissions
- Add integration tests for sendInvitationEmail callback to cover
buildInvitationEmailHtml and the email dispatch path in auth.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: use URL constructor to sanitize redirect param in sign-in
Replace regex check with URL constructor origin validation so CodeQL's
dataflow analysis can confirm the value is same-origin before it reaches
window.location.href (resolves js/xss and js/client-side-unvalidated-url-redirection alerts).
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>
* refactor: remove isPublic from matters, simplify user share homepage
- Remove is_public column from matters table and migration 0006
- Remove batchVisibilitySchema and BatchVisibilityInput from shared schemas
- Remove isPublic field from StorageObject type
- Remove /api/objects/batch/visibility endpoint
- Remove batchUpdateVisibility service function
- Simplify profile service to keep only getUserByUsername, getUserOrgId, buildBreadcrumb
- Simplify profile route to return empty shares (v2.3 share system pending)
- Add try-catch error handling to profile routes (DB errors return 500)
- Replace PublicProfileSection file-checkbox UI with link + hint text
- Simplify /u/:username page to show user info and empty state
- Remove browseProfile API client function
- Delete .codecov.yml
- Update i18n: add publicProfile.hint key, remove obsolete keys
- Trim integration tests to user-lookup cases only
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: remove 0006_public_profile from migration journal
The SQL file was deleted but the journal entry remained, causing the
drizzle migrator to fail on fresh databases when starting the Node server.
Removing the entry ensures the migrator no longer tries to find the deleted file.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: restore codecov config excluding React UI files from coverage
React route and component files require a DOM environment and cannot
be unit-tested via the server test runner. The exclusion is legitimate
and not tied to the removed isPublic feature.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: remove dead code and redundant try-catch from profile routes
- Remove getUserOrgId (dead code, never called after refactor)
- Remove unused findPersonalOrg import
- Remove try-catch wrappers in profile routes (Hono handles
uncaught errors via its default error handler — centralized
error handling, no defensive noise per coding principles)
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
* feat: add user public share homepage (/u/:username)
- Add isPublic boolean field to matters table (migration 0006)
- Create public profile API (/api/profiles/:username) without auth
- Add directory browse endpoint (/api/profiles/:username/browse)
- Add batch visibility update endpoint (/api/objects/batch/visibility)
- Create public profile page at /u/$username with breadcrumb navigation
- Add Public Profile section to settings page for managing shared files
- Update shared types and schemas to include isPublic field
- Register /u/$username route in TanStack Router route tree
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* test: add integration tests for profile routes and services
- Test GET /api/profiles/:username (404 for missing user, public shares, no-auth)
- Test GET /api/profiles/:username/browse (public folder browsing, access control)
- Test buildBreadcrumb and isPublicPath unit cases
- 20 tests, 95%+ line coverage on profile.ts and profile service
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* test: add coverage for getProfile, browseProfile, batchUpdateVisibility
Cover the new public profile API functions in src/lib/api.ts to meet
codecov patch thresholds.
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* ci: trigger test suite for coverage commit
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* test: add file comment to api.test.ts
Agent-Profile: https://agent-kanban.dev/agents/b724a773425e397c
* test: add pure-logic tests for public profile page and settings
Add unit tests for extractable logic in src/routes/u/$username.tsx
(folder detection, navigation path, breadcrumb, loading/items state)
and src/routes/_authenticated/settings/index.tsx (display name
validation, password match, toggleId set logic, visibility batch
split). Extend vitest coverage include to report on these route files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: exclude React route and component files from codecov
These files cannot be unit-tested without a DOM/jsdom environment.
Pure logic from each component is tested in co-located *.test.ts
files. Excluding them prevents false coverage failures on patch and
project checks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add patch path exclusions for React files in codecov
The patch check must also exclude src/routes and src/components
since these files cannot be measured without a DOM environment.
The project check was already fixed; this fixes the patch check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add integration tests for POST /batch/visibility endpoint
Covers the happy path (set public, set private) and error cases
(invalid input, unauthenticated) for the new batch visibility route,
ensuring patch coverage passes on the new endpoint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces requireTeamRole middleware that enforces viewer/editor/owner
role hierarchy on object and trash routes. Personal orgs bypass the
check; non-members receive 403. Adds getMemberRole and isPersonalOrg
helpers to org service. Adds 'member' default-role mapping at viewer
level to prevent silent lockout of users created by better-auth.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
* feat: remove Name field from registration form
- Remove name from signUpSchema; send empty string to satisfy DB NOT NULL
- Reorder sign-up form: Email → Username → Password → Invite Code
- createPersonalOrg falls back to username when name is empty
- Add username to UserWithOrg type (server and frontend), typed as string
- Sidebar and admin users table show username when name is empty
- Update all e2e helpers and specs to remove Name field interactions
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: update signUpSchema tests to match new schema (email, username, password)
Remove name field tests; add username validation tests.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Rename 19 integration test files from *.test.ts to *.integration.test.ts.
Configure vitest projects to run them independently with separate coverage
thresholds. CI now reports unit and integration coverage as separate flags
to Codecov.
Unit tests: pure function calls, mocked dependencies, no DB
Integration tests: createTestApp() with in-memory DB + HTTP requests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cover the happy path: successful registration with valid invite code,
usedBy stores user ID, and same code cannot be reused.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
user.id is not available in the before hook (generated after). Split
the logic: validate in before (reject invalid codes early), redeem
in after (user.id is now set). Also read inviteCode from context.body
instead of request.clone().json().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
additionalFields tries to persist inviteCode to the user table, which
fails because there's no invite_code column. Instead, read it from
the request body via the databaseHooks context parameter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enforce signup gating via auth_signup_mode system option (open/invite_only/closed).
Invite codes are atomically redeemed during signup. Email verification is
conditionally enabled when an email provider is configured.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Opus 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>
Admin can generate, list, and delete invite codes. Public endpoint
validates codes before sign-up. Codes are 8-char uppercase alphanumeric
with optional expiration. Redemption uses atomic UPDATE to prevent
concurrent double-use.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Unified email service supporting two drivers configured via system_options:
- SMTP driver using nodemailer (dynamic import for CF Workers compatibility)
- HTTP API driver using fetch (Resend-compatible)
Includes admin API at /api/admin/email-config for GET/PUT/POST test
endpoints with secret masking and discriminated union validation.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
* feat(auth): add better-auth username plugin
Enable username-based registration and sign-in by adding the username
plugin to both server and client auth configurations. Adds username
and display_username columns to the user table via migration.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test(auth): add username plugin tests and fix test setup
Add schema and integration tests for the username plugin. Fix the
in-memory SQLite test setup to include username columns so existing
auth tests don't break.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: remove custom filePath, enforce tenant-isolated storage path
Replace user-customizable filePath template with a hardcoded
tenant-isolated pattern ($ORG_ID/$UID/$NOW_DATE/$RAND_16KEY$RAW_EXT).
This ensures proper tenant isolation and removes unnecessary complexity.
The DB column is preserved to avoid migration; code simply stops
reading/writing it.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* refactor: clean up dead tokens, locale keys, and simplify path template
Remove unused template tokens ($UUID, $RAW_NAME, $NOW_YEAR, $NOW_MONTH,
$NOW_DAY) and corresponding TemplateVars fields (uuid, rawName) since
the hardcoded template doesn't use them. Remove orphaned i18n keys for
fieldFilePath. Update DB schema default to empty string. Add storage
service unit tests.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
- 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>
Admin can now set the available space for each storage backend with
MB/GB/TB unit selector. 0 means unlimited. Also added capacity/used
fields to the shared Storage type.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DeleteObjectsCommand returns XML that requires DOMParser to parse, but
Cloudflare Workers doesn't have DOMParser. Use parallel deleteObject calls instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New users get a 10MB default storage quota (configurable via admin settings).
Admin can set the default in Settings with MB/GB unit selector. Added db:generate,
db:migrate, and db:reset scripts; dev server now reads .dev.vars automatically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same metadata-JSON LIKE pattern that was removed from findPersonalOrg:
matching `organization.metadata LIKE '%"type":"personal"%'` depends
implicitly on V8's JSON.stringify output (no space after the colon)
and scans the metadata column instead of using the indexed slug.
Switch the join predicate to `organization.slug = 'personal-' || user.id`.
The SQL `||` concatenation is portable across better-sqlite3 (Node) and
D1 (CF Workers), and the slug column is UNIQUE+indexed so the join
becomes an index lookup instead of a string scan.
The INNER-member + LEFT-organization structure is preserved so users
with no personal org still appear in the list with null orgId/orgName.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related changes to remove a per-request DB lookup and harden the
personal-org semantics:
1. Add `databaseHooks.session.create.before` in server/auth.ts to pin
`activeOrganizationId` to the user's personal org at session
creation. The middleware at server/middleware/auth.ts:27 already
falls back to findPersonalOrg when the session has no activeOrgId,
but that fallback fires on every request during the 5-minute
session cookie cache window. Baking it into the session row means
the cached cookie already carries the right value.
2. Refactor `findPersonalOrg` in server/services/org.ts from a
metadata-JSON LIKE match to a slug lookup plus membership JOIN:
- Slug lookup uses `personal-${userId}` — the exact string
createPersonalOrg writes at signup. UNIQUE + indexed, no
reliance on V8's JSON.stringify output format.
- INNER JOIN on member preserves the old semantic that losing
membership (admin revokes) orphans the user from the personal
org even if the org row still exists.
Tests updated to match the new contract: three vestigial tests that
exercised JSON metadata parsing (now dead code) are removed; two
fixtures updated to write the correct slug; one new test locks in
"org exists but member row deleted → null".
The quotas.test.ts:156 scenario ("returns 404 when user has no org"
by manually deleting the member row) keeps working because the JOIN
still enforces membership.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
verifyPassword previously returned false when the stored hash lacked a
salt or key segment, which silently masqueraded as "wrong password" and
hid real data integrity problems from the operator. A corrupt credential
in the user table should surface loudly so it can be investigated, not
look like a typo in the user's password.
Throw with a descriptive error instead. Per the project principle:
"Errors must be handled explicitly. Never ignore, silence, or downgrade
an error just to keep things running."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous implementation ran the first-user-to-admin promotion in
databaseHooks.user.create.after: INSERT fired first with role='user',
then the after hook ran UPDATE to set role='admin'. But better-auth
built the session cookie from the in-memory user object BEFORE the
after hook ran, so the very first sign-up always walked away with a
session claiming role='user' — the only workaround was to log out and
log back in to force a fresh DB read.
Move the logic into the before hook and return `{ data: { ...user,
role: 'admin' } }` when the user table is empty. The INSERT now
writes 'admin' directly and the session cookie is born correct.
Also extract `isFirstUser(db)` using Drizzle's `count()` query builder
instead of the previous raw SQL, and delete the now-redundant
`promoteFirstUserToAdmin` and `setupNewUser` helpers.
Also fix vitest.cloudflare.config.ts: after wrangler.toml was
restructured in commit 4d0511b to put D1 bindings under
[env.production]/[env.preview], the CF vitest pool had no environment
specified and read no bindings, breaking all 5 cf-test files with
"env.DB is not a D1Database". Pin the pool to `environment: 'production'`
so tests receive a complete binding surface (miniflare still creates
an ephemeral in-memory D1 regardless, so no data risk).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Better-auth's default password hasher is @noble/hashes scrypt (pure JS),
which consumes ~100-200ms of CPU per call. Cloudflare Workers' free tier
caps each request at 10ms of JS CPU time, so sign-up/sign-in consistently
fails with error 1102 after the initial cold-start burst budget runs out.
See better-auth/better-auth#8860 for the upstream bug.
Override emailAndPassword.password.hash/verify with node:crypto.scryptSync.
The native OpenSSL implementation runs in ~ms of wall time and is counted
as I/O rather than JS CPU time on CF Workers, so it does not touch the
CPU budget. Works identically on the Node/Docker entry because
node:crypto is native there too.
Verified end-to-end on https://af9a6fdc.zpan.pages.dev: 5 sequential
signups + 5 sequential signins all returned HTTP 200 (previously 4/4
consecutive signups hit error 1102 on the same deployment).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createNodePlatform now invokes drizzle-orm's better-sqlite3 migrator
against the configured migrations folder (defaults to ./migrations,
overridable via MIGRATIONS_DIR). Removes the operational burden of
manually running drizzle-kit against a Docker volume — the Node
runtime image no longer needs drizzle-kit (a devDependency) at all.
Tests are unaffected: server/test/setup.ts builds an in-memory SQLite
with hand-written DDL and never touches createNodePlatform.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both entries now throw when BETTER_AUTH_SECRET is absent instead of
silently using a hardcoded dev default. The CF Pages entry was missing
trustedOrigins entirely; it now reads TRUSTED_ORIGINS from env and
shares the same split/trim/filter logic as the Node entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements atomic quota check-and-increment at upload confirm time and
file copy, so users can no longer exceed their configured quota. Fixes
a storage accounting leak in batch delete where folder descendants were
never cleared from S3 or quota, and in empty-trash where nested trashed
items caused double decrement. Extracts purgeRecursively into a shared
service to dedupe object and trash routes. Adds sidebar quota usage
display for end users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend: add `search` query param to GET /api/objects, uses LIKE %keyword% across all folders
- Frontend: 300ms debounce before sending search request to backend
- Search is global — finds files in any directory, not just current folder
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace @svar-ui/react-filemanager with custom file manager built on
@tanstack/react-table, @dnd-kit, and shadcn/ui (ContextMenu, Breadcrumb,
Table, Checkbox, ToggleGroup, Collapsible)
- List view with sortable columns (folders-first), Grid view with card layout
- Right-click context menu, row dropdown actions, drag-and-drop file moving
- Dialogs: rename, new folder, delete confirm, move (with folder picker)
- URL-based path navigation (?path=folder/subfolder), breadcrumb from URL
- Backend: parent field stores materialized path instead of folder ID,
cascade rename/move updates all descendants, self-move protection
- Backend: type filter API (?type=photos|videos|music|documents) for
cross-folder file browsing by MIME type
- Sidebar: My Files with lazy-loading folder tree (auto-expands to current
path), Photos/Videos/Music/Documents categories, Trash
- Settings moved to user avatar dropdown menu
- Migrate pnpm references to npm across docs and config
- i18n: all new keys in en.json and zh.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace all db.all(sql`...`) / db.run(sql`...`) calls with Drizzle's
type-safe query builder (select/insert/update/delete). Schema changes
now trigger TypeScript errors in queries, and column name typos are
caught at compile time.
- Unify Database type to BaseSQLiteDatabase for query builder compat
- Derive Matter/Storage types from schema via $inferSelect
- Use inArray() instead of manual sql.join() for IN clauses
- Net -90 lines across 9 files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>