Commit Graph
117 Commits
Author SHA1 Message Date
Jasper Van 9908953252 fix: drop local quota store package table
Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-06 17:50:34 -04:00
Jasper VanandCopilot bf0bf99ef4 feat: separate storage code revoke and delete
* feat(quota-store): separate revoke and delete semantics for redemption codes

- PATCH /api/admin/quota-store/storage-codes/:code accepts { revokedAt } to
  revoke a code (keeps it visible, prevents future redemptions)
- DELETE /api/admin/quota-store/storage-codes/:code removes eligible codes
- Added revokeStorageCodeSchema (shared) with zValidator on PATCH route
- Fixed revokeStorageRedemptionCode API fn to use PATCH with revokedAt payload
- Added deleteStorageRedemptionCode API fn using DELETE
- UI: separate Revoke and Delete buttons per code row with distinct disabled rules
  - Revoke disabled when already revoked or fully redeemed
  - Delete disabled when any redemptions exist (usesCount > 0)
- Updated confirmation copy to describe product outcome, not technical action
- i18n: added delete keys in en.json and zh.json; updated revokeConfirm copy
- Updated integration, unit, and component tests

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

* fix: align storage code revoke contract

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 17:08:41 -04:00
Jasper Van df2e2a8cd3 feat: manage Cloud storage packages
Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-06 16:46:10 -04:00
Jasper Van a4f9e3ff6a refactor: proxy quota store through cloud
Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-06 16:16:20 -04:00
Jasper Van da62c9e6c2 fix: enforce monthly traffic on download URL issuance
* fix: enforce download traffic quota before presign

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

* fix: prevent cached metered image redirects

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-06 14:24:45 -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
saltbo ec832b0dfd feat(storage): refine storage plan experience 2026-05-06 10:58:06 -04:00
Jasper Van 2363ca1a6c feat: manage quota store redemption codes
Adds ZPan admin quota-store APIs and UI for Cloud-bound storage redemption code management, refactors the admin quota-store page into tabs, and polishes the user store page.
2026-05-06 03:00:57 -04:00
Jasper Van 5632cbbc83 fix: use pro binding auth for quota store (#367)
* fix: use pro binding auth for quota store

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

* fix: align quota store event token audience

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

* fix: match cloud delivery event audience contract

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-06 00:29:48 -04:00
Jasper Van 8f6e4c2ff3 Fix quota store Cloud binding contract (#366)
* fix(quota-store): use Pro binding for Cloud store auth

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

* fix(quota-store): align Cloud store binding contract

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-05 22:32:40 -04:00
Jasper Van 5eac0f4642 feat(branding): add pro theme settings (#362)
Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-05 17:01:48 -04:00
Jasper Van 8a185c4cd4 feat: add quota store UI (#363)
Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-05 16:40:54 -04:00
Jasper Van 41877d384f feat: add quota store backend (#361)
* feat: add quota store backend

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

* test: cover quota store cloud responses

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

* test: cover quota store edge cases

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

* fix: align quota store cloud contract

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-05 12:01:29 -04:00
saltbo e15334e094 fix(admin): show user quota and avatars 2026-05-04 22:02:15 -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
Jasper VanandCopilot 7d81ef2b72 feat: implement admin audit logs (#355)
- Add listAdminAuditEvents service with pagination and filters (orgId, userId, action, targetType)
- Add GET /api/admin/audit route protected by requireAdmin + requireFeature('audit_log')
- Mount admin audit route in app.ts and export AdminAuditRoute type
- Add AdminAuditEvent shared type with orgName field
- Add listAdminAuditQuerySchema to shared schemas
- Add adminAuditApi RPC client and listAdminAuditLogs API wrapper
- Replace placeholder /admin/audit page with real table/list UI using React Query
- Show Pro upgrade prompt when audit_log entitlement is unavailable
- Add empty, loading, error, and load-more states to UI
- Remove comingSoon from audit_log in feature registry
- Update en.json and zh.json with real audit log labels
- Add integration tests covering 401/403/402 guards, filters, pagination, and response shape
- Add unit tests for listAdminAuditLogs API wrapper


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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-04 07:50:02 -04:00
saltbo 65b0c508ec fix(announcements): simplify publishing controls 2026-05-02 23:13:08 -04:00
saltbo 6314717795 feat(announcements): add site announcement management 2026-05-02 22:17:46 -04:00
saltbo 8b67eec1c5 feat(upload): add multi-file upload progress queue 2026-04-30 22:42:59 -04:00
saltbo 4537bfac51 feat(preview): add microsoft office viewer 2026-04-30 09:32:49 -04:00
saltbo a9cf593e82 test(licensing): authorize localhost in Pro seed 2026-04-29 21:06:55 -04:00
saltbo f0e7af2798 feat(licensing): redesign Pro license binding 2026-04-29 20:20:18 -04:00
saltbo d28dbe764a fix(email): require explicit provider selection 2026-04-27 22:39:56 -04:00
saltbo 5164c89a6f test(email): close patch coverage gaps 2026-04-27 21:57:56 -04:00
saltbo 42de0f8315 test(email): cover cloudflare fallback branches 2026-04-27 21:49:26 -04:00
saltbo 7b9206f7e9 fix(test): stabilize cloudflare storages coverage 2026-04-27 21:41:09 -04:00
saltbo 2272a87616 test(email): enable site invitation mail fixtures 2026-04-27 21:28:07 -04:00
saltbo d75d7e5461 feat(email): add cloudflare worker mail service toggle 2026-04-27 21:28:07 -04:00
saltbo bf7c3389ea refactor(site): centralize default site metadata 2026-04-27 21:00:40 -04:00
saltbo d7f1ceb6bc feat(licensing): adjust free plan limits 2026-04-27 20:41:46 -04:00
saltbo 1a1426896a test(e2e): cover site invitation signup flow 2026-04-27 20:11:07 -04:00
saltbo c4fc8c9f84 feat(admin): add site invitation signup flow 2026-04-27 19:49:19 -04:00
saltbo 4872f97d9f fix: align licensing pairing with cloud certificates 2026-04-27 08:46:05 -04:00
saltbo 45b623e362 fix: reject invalid cloud refresh certificates 2026-04-27 08:39:30 -04:00
saltboandCopilot fa943ca8b3 feat: replace license_binding table with system_options keys
- 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>
2026-04-24 22:58:13 -04:00
saltboandCopilot 17d70d605e test: add E2E licensing integration tests for zpan ↔ zpan-cloud
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>
2026-04-24 20:37:25 -04:00
saltboandCopilot 134d325f56 fix: update cloud public key for production keypair
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 20:08:43 -04:00
saltboandCopilot 76645d47b3 fix: align licensing client with zpan-cloud API contract
- 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>
2026-04-24 20:01:08 -04:00
Jasper VanandBob 04f9d93ddf feat: v2.6 Z11 — production public key, Docker cron docs, release notes (#350)
* 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>
2026-04-24 09:20:58 -04:00
Jasper VanandBob 3f2890c6ea feat: retroactive gate — per-team storage quota Pro-only (Z10) (#349)
- 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>
2026-04-24 09:03:15 -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 c86c7731c1 feat: v2.6 Z7 white-label branding (logo, favicon, wordmark, hide footer) (#346)
* 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>
2026-04-24 08:50:53 -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
Jasper VanandBob 29102e623d feat: v2.6 Z6 — 6h background entitlement refresh (#345)
- Add server/services/licensing-refresh-runner.ts: shared runner with
  5-min dedup guard, structured INFO logs, and no-op for unbound state
- Add workers/scheduled.ts + export scheduled() in workers/bootstrap.ts
  for CF Workers cron (every 6 hours)
- Add [triggers] crons = ["0 */6 * * *"] to wrangler.toml
- Add setInterval refresh on boot in server/entry-node.ts with
  "licensing.refresh.scheduler.started interval=6h" log
- Add POST /api/licensing/refresh-cron?secret=... public endpoint
  (timing-safe secret comparison) for non-CF platforms
- Extract ZPAN_CLOUD_URL_DEFAULT to shared/constants.ts, replacing
  four duplicated literals
- Document REFRESH_CRON_SECRET + scheduler setup in all 5 non-CF
  deploy guides (vercel, netlify, aws-lambda, azure-functions, cloud-run)

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 08:22:10 -04:00
Jasper VanandBob 86fab7716b feat(licensing): cloud client + binding API (pair, poll, refresh, disconnect) (#343)
* feat(licensing): cloud client + binding API (pair, poll, refresh, disconnect)

- server/licensing/public-keys.ts — DEV PASERK placeholder (production key lands via C5 cross-repo PR)
- server/licensing/verify.ts — verifyCertificate() using paseto-ts/v4, returns LicenseEntitlement | null
- server/licensing/entitlement.ts — loadEntitlement() with 60s in-process memoization + invalidateEntitlementCache()
- server/licensing/has-feature.ts — loadBindingState() + hasFeature() pure sync check
- server/licensing/instance-id.ts — getOrCreateInstanceId() lazily persisted in systemOptions under 'instance_id'
- server/licensing/refresh.ts — performRefresh(): calls cloud, verifies cert, rotates DB row; handles CloudUnboundError (clear binding) and CloudNetworkError (update error log, keep cached cert)
- server/services/licensing-cloud.ts — createPairing(), pollPairing(), refreshEntitlement() with 10s timeout; CloudUnboundError + CloudNetworkError for typed error handling
- server/routes/licensing.ts — public GET /api/licensing/status (no auth required)
- server/routes/licensing-admin.ts — admin-only: POST /pair, GET /pair/:code/poll, POST /refresh, DELETE /binding
- server/middleware/require-feature.ts — requireFeature(name) middleware, returns 402 when feature missing
- server/app.ts — mount /api/licensing (public) + /api/licensing (admin) + export route types
- src/lib/rpc.ts — licensingApi + licensingAdminApi RPC clients
- src/lib/api.ts — getLicensingStatus(), connectCloud(), pollPairing(), refreshLicense(), disconnectCloud()
- src/lib/api.test.ts — 17 new tests covering all 5 new api.ts wrappers
- shared/types/licensing.ts — update LicenseEntitlement.issued_at/expires_at to string (ISO-8601)
- paseto-ts dependency added for PASETO v4 public verification

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

* fix(licensing): fix biome lint issues — remove unused imports, format test file

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

* fix(licensing): apply biome format fixes to refresh, require-feature, licensing-cloud

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

* test(licensing): add unit and integration tests for all new licensing modules

- server/licensing/public-keys.test.ts — PUBLIC_KEYS format validation
- server/licensing/verify.test.ts — verifyCertificate: valid cert, invalid sig, expired, instance mismatch, key rotation
- server/licensing/has-feature.test.ts — hasFeature: null/unbound/empty/expired/future states
- server/services/licensing-cloud.test.ts — createPairing, pollPairing, refreshEntitlement: success, 401 Unbound, network error
- server/routes/licensing.integration.test.ts — GET /api/licensing/status: unbound, bound+cert, bound+no-cert, public access
- server/routes/licensing-admin.integration.test.ts — auth guards (401/403) + POST /pair, GET /pair/:code/poll, POST /refresh, DELETE /binding
- server/test/setup.ts — add license_binding table to in-memory schema

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

* test(licensing): add entitlement cache and refresh orchestration unit tests

- entitlement.test.ts — loadEntitlement: no row, no cert, valid PASETO cert, expired cert; invalidateEntitlementCache: re-reads from DB after invalidation
- refresh.test.ts — performRefresh: no-op when unbound, rotates token (pre-C5 object), rotates token (PASETO string), clears binding on 401 Unbound, updates error log on network failure

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

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 07:37:30 -04:00
Jasper VanandBob b9cea3e912 feat(licensing): v2.6 Z2 — Ed25519 verify + PUBLIC_KEYS + entitlement cache (#341)
* feat(licensing): add server/licensing module with Ed25519 verify + entitlement cache

- Add paseto-ts dependency (WebCrypto Ed25519, works on all 7 deploy targets)
- server/licensing/public-keys.ts: PUBLIC_KEYS array with DEV placeholder PASERK key
- server/licensing/verify.ts: verifyCertificate() iterates PUBLIC_KEYS, validates
  signature, expiry and instance_id — returns null (never throws) on invalid certs
- server/licensing/entitlement.ts: loadEntitlement() with 60s in-process memoization;
  invalidateEntitlementCache() for post-refresh invalidation
- Update LicenseEntitlement.expires_at / issued_at to string (ISO-8601 wire format)
- Unit tests: valid cert, invalid sig, expired, wrong instance_id, key rotation

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

* fix(licensing): apply biome lint fixes (import order, template literal)

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

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-24 07:34:32 -04:00
Jasper VanandBob 85554512c4 feat: add licenseBinding table, migration 0013_licensing, and shared licensing types (#339)
- 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>
2026-04-23 22:39:55 -04:00
saltboandClaude Opus 4.7 4f47a81e5b feat(image-upload): prefer R2 binding on Cloudflare, fall back to S3
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>
2026-04-22 22:56:35 -04:00
saltboandClaude Opus 4.7 bcb0b67872 feat(platform): add getBinding() for platform-native resources
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>
2026-04-22 22:56:35 -04:00