Commit Graph
34 Commits
Author SHA1 Message Date
saltbo 53076d7873 feat(downloads): add remote download workers 2026-06-03 02:21:50 -04:00
saltbo 7f85da222f feat(quota): consolidate storage entitlements 2026-06-02 20:12:59 -04:00
Jasper Van 8b45c6218a fix: use better auth captcha providers (#401)
Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-13 16:39:09 -04:00
Jasper Van 85cd3994b3 feat: add user-bound WebDAV app passwords 2026-05-11 22:28:46 -04:00
Jasper Van efb92397bf feat: add WebDAV API-key protocol routes (#390)
* feat: add WebDAV API key routes

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix: route WebDAV preview requests to worker

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-11 19:32:29 -04:00
saltbo b6c20d7650 fix(db): wrap multi-write flows in transactions 2026-05-09 20:03:54 -04:00
Jasper Van 3a88c17d6e feat: add monthly traffic quotas
Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-06 13:41:20 -04:00
Jasper Van ccc4266b5f Fix admin settings persistence and batch user operations (#358)
* fix: persist admin settings and batch user operations

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

* test: cover admin quota edge cases

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

* fix: avoid duplicate batch quota toast

Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-04 17:20:26 -04:00
Jasper VanandCopilot 1b734c0ff9 feat: expand audit event coverage across all state-changing operations (#357)
* feat: expand audit event coverage across all state-changing operations

Add audit events for share lifecycle, object operations, team management,
admin/system mutations, storage, quotas, invite codes, site invitations,
users, licensing, and branding.

New events recorded:
- Share: share_create, share_revoke
- Object: upload_confirm, upload_cancel, object_copy, object_purge,
  batch_trash, batch_purge, trash_empty
- Team/org: team_invite_link_create, team_member_join, team_logo_update,
  team_logo_delete
- System: system_option_set, system_option_delete
- Storage: storage_create, storage_update, storage_delete
- Quota: quota_update
- Invite codes: invite_code_generate, invite_code_delete
- Site invitations: site_invitation_create, site_invitation_revoke
- Users: user_disable, user_enable, user_delete
- Licensing: license_pair, license_disconnect
- Branding: branding_update, branding_reset

Auth sign-up/sign-in events: not logged. The Better Auth hook lifecycle
does not provide reliable org context at sign-up (personal org is created
after the user.create.after hook), and sign-in cannot be distinguished
from normal session refresh without brittle internal request parsing.

No schema migration required; existing activityEvents table fields
support all new event types.

Add i18n translations (en + zh) for all new action names and target types.
Add 26 integration tests verifying every new event category and asserting
no secrets are stored in metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agent-Profile: https://agent-kanban.dev/agents/f68cfbce6456edb5

* feat: add missing audit event coverage for share_download, org lifecycle, license_refresh

Fill gaps identified in code review:

1. share_download: record after successful file download via public share
   endpoint (publicShares GET /:token/objects/:ref). Authenticated viewer
   is used as actor; share creator is proxy for anonymous downloads with
   anonymous:true in metadata. Presigned URL never stored.

2. team lifecycle via Better Auth organizationHooks (server/auth.ts):
   - team_member_remove (afterRemoveMember): member.userId used as actor
     since BA hook does not expose the initiator
   - team_member_role_update (afterUpdateMemberRole): same limitation,
     member.userId used; previousRole/newRole recorded in metadata
   - team_settings_update (afterUpdateOrganization): actor user available
   - team_delete (afterDeleteOrganization): actor user available
   - team_member_join via email invite (afterAcceptInvitation): joins
     existing link-based join coverage in teams.ts

3. license_refresh: record after POST /api/licensing/refresh succeeds
   (licensing-admin.ts)

Unhookable events (documented with code references):
- invite_code_redeem: called at server/auth.ts:269 in user.create.after;
  personal org (required for activityEvents.orgId NOT NULL) is created in
  session.create.before which runs after this hook. Better Auth provides
  no shared state between these hooks.
- site_invitation_accept: called at server/auth.ts:275 in user.create.after;
  same orgId unavailability issue.

Add 7 new integration tests for: share_download (×2, authed + anonymous),
team_settings_update, team_member_remove, team_member_role_update,
team_delete, license_refresh.

Add i18n translations (en + zh) for new actions:
share_download, team_member_remove, team_member_role_update,
team_settings_update, team_delete, license_refresh.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agent-Profile: https://agent-kanban.dev/agents/f68cfbce6456edb5

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-04 15:25:47 -04:00
saltbo d75d7e5461 feat(email): add cloudflare worker mail service toggle 2026-04-27 21:28:07 -04:00
saltbo d7f1ceb6bc feat(licensing): adjust free plan limits 2026-04-27 20:41:46 -04:00
saltbo c4fc8c9f84 feat(admin): add site invitation signup flow 2026-04-27 19:49:19 -04:00
Jasper VanandBob de5ff92f03 feat: gate team creation at 3 orgs for community plan (teams_unlimited required for 4th) (#348)
- 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>
2026-04-24 08:59:01 -04:00
Jasper VanandBob 1cbe92640c feat: gate open registration behind Pro feature (Z8) (#347)
- 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>
2026-04-24 08:40:28 -04:00
saltboandCopilot d4be0d020f fix(ihost): allow empty referer, add JSON path field, set rate limits
- 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>
2026-04-21 15:22:02 -04:00
saltboandCopilot 6e14fd6033 fix: multipart upload 500, presign 413, API key permissions, and org slug check
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>
2026-04-21 12:37:05 -04:00
saltboandCopilot 8e079abaa6 fix(auth): create personal org during sign-up and set activeOrganizationId
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>
2026-04-21 12:20:59 -04:00
2dca9d9ffb feat(db): image hosting schema, apiKey plugin, migration 0011 (v2.4.0 T1) (#315)
* feat(db): image hosting schema, apiKey plugin, migration 0011

Adds the DB infrastructure for the v2.4 image hosting feature:

- server/db/schema.ts: new `image_hosting_configs` (per-org singleton,
  custom domain + referer allowlist) and `image_hostings` tables with
  all required indexes and cascade-delete FKs
- server/db/auth-schema.ts: `apikey` table for @better-auth/api-key
  plugin (referenceId-indexed, rate-limiting fields, permissions column)
- server/auth.ts: enable apiKey plugin (references='organization',
  image-hosting:upload permission declared)
- shared/types/index.ts: ImageHostingConfig, ImageHosting, ImageHostingStatus
- migrations/0011_image-hosting.sql: generated by drizzle-kit (NOT
  hand-authored); creates all three new tables + indexes
- migrations/meta/0011_snapshot.json: baseline snapshot for future
  migration generation (repo was missing snapshots 0002–0010)
- package.json: add @better-auth/api-key ^1.6.2 dep, bump
  better-auth to ^1.6.2

Note: migration is tagged 0011_image-hosting (idx=11 in journal) rather
than 0012 because the existing 0011_notifications was registered as idx=10
(pre-existing numbering offset from a skipped 0006_ slot).

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

* fix: resolve coverage gap and migration filename collision

- Add getTableConfig FK reference tests to cover deferred .references()
  callbacks in schema.ts (fixes Codecov patch coverage < 94.98%)
- Rename 0011_image-hosting → 0012_image-hosting to avoid filename
  collision with pre-existing 0011_notifications migration

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>
2026-04-21 03:18:52 -04:00
Jasper VanandBob de4938e75c feat: add shares schema, service layer, and lifecycle integration (#308)
- Add `shares` and `share_recipients` tables to Drizzle schema with indices
- Add migration 0010_shares.sql for shares/share_recipients tables
- Add `server/lib/password.ts` extracting scrypt hash/verify from auth.ts
  to eliminate duplicate crypto params across services
- Add `server/services/share.ts` implementing full CRUD + atomic counters:
  createShare, getShareByToken, incrementViews, incrementDownloadsAtomic
  (atomic SQL UPDATE), listSharesByCreator, revokeShare, cascadeDeleteByMatter
- Add `shared/schemas/share.ts` Zod validation schemas
- Export Share, ShareKind, ShareRecipient from shared/types
- Extend `purge.ts` to cascade-delete shares on matter hard-delete
- Add 38 integration tests and CF Workers atomic counter race tests

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-19 23:23:48 -04:00
saltboandClaude Opus 4.6 cb7df2f2da feat: auto-generate username for OAuth sign-up users
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>
2026-04-14 14:51:20 -04:00
878cdeb117 feat: team invitation via email and invite link (#302)
* 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>
2026-04-14 13:55:26 -04:00
Jasper VanandBob e40ba0785e feat: remove Name field from registration form (#291)
* 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>
2026-04-14 01:19:16 -04:00
saltboandClaude Opus 4.6 c324abdf5d fix(auth): validate invite code in before hook, redeem in after hook
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>
2026-04-13 20:46:35 -04:00
saltboandClaude Opus 4.6 92306ca862 fix(auth): store user ID instead of email in invite code usedBy
User ID is immutable; email can change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:41:50 -04:00
saltboandClaude Opus 4.6 783eba8a83 fix(auth): read inviteCode from request body instead of additionalFields
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>
2026-04-13 20:38:57 -04:00
364cbc2aee feat(auth): add registration mode control and email verification (#283)
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>
2026-04-13 11:19:54 -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
da08b014ae feat(auth): add better-auth username plugin (#279)
* 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>
2026-04-12 21:20:13 -04:00
saltboandClaude Opus 4.6 bce4da3ebf feat(quota): add default org quota with admin settings UI and db:reset script
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>
2026-04-11 23:37:54 -04:00
saltboandClaude Opus 4.6 b059af19b4 feat(auth): pin activeOrganizationId on new sessions, tighten personal-org lookup
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>
2026-04-11 12:08:17 -04:00
saltboandClaude Opus 4.6 1462a76aaa fix(auth): throw on malformed stored password hash instead of returning false
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>
2026-04-11 11:52:30 -04:00
saltboandClaude Opus 4.6 7124289793 fix(auth): promote first user before INSERT so session reflects admin role
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>
2026-04-11 11:49:29 -04:00
saltboandClaude Opus 4.6 1071f8a343 fix(auth): use native scrypt to bypass Cloudflare Workers CPU limit
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>
2026-04-11 11:19:49 -04:00
saltboandClaude Opus 4.6 310e4950b3 refactor: flatten monorepo to single-package CF Pages Functions structure
Replace pnpm workspace monorepo (packages/server, packages/web, packages/shared)
with a flat single-package layout following Hono's pages-stack pattern. Switch from
pnpm to npm and from Workers+Assets to CF Pages Functions deployment model.

- Move source: packages/server/src/ → server/, packages/web/src/ → src/, packages/shared/src/ → shared/
- Add functions/api/[[route]].ts as CF Pages Functions entry (replaces entry-cloudflare.ts)
- Update 22 import paths: server uses relative, web uses @shared/@server aliases
- Merge three package.json into one, switch to npm
- Update wrangler.toml: remove main/assets (Pages auto-detects functions/ dir)
- Add per-directory tsconfig.json for VS Code type resolution
- Simplify Dockerfile for flat layout
- Fix react-pdf CSS import path (dist/esm/ → dist/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:27:38 -04:00