Commit Graph

342 Commits

Author SHA1 Message Date
Aman Ganapathy
32daff06f5
[Fix]: OOM Risk with Data Table (#1766)
### Context
We were seeing the txn table and the customers tab under product page
OOM.

It turns out this was because the team icon when loaded would fetch all
of the teams.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes OOM and renderer crashes in the transactions and product customers
tables by stopping per-row refetch storms and avoiding list‑all‑teams
calls. Infinite scroll is now robust, and shared avatar skeletons keep
table rendering smooth.

- **Bug Fixes**
- Data grid (`@hexclave/dashboard-ui-components` `use-data-source`):
queue `loadMore` while a fetch is in flight and replay only after a
successful settle; discard queued requests when pagination mode leaves
infinite; commit the cursor only after a result is delivered; skip
redundant refetches when inputs match a completed fetch; abort in‑flight
requests on unmount.
- Transactions table and product customers tab: wrap `User*`/`Team*`
avatar cells in `Suspense` with a shared `AvatarCellSkeleton` to prevent
per‑row fetch storms and suspend thrash.
- `serverApp.getTeam`/`useTeam` (in `@hexclave/shared` consumers): fetch
a single team by id via a cache; return null for invalid ids; keep
user‑scoped variants membership‑scoped; stabilize hook order. This
removes the “fetch all teams per row” behavior that triggered OOMs.
- Prefetching: cap `/projects/*/teams` to `useTeams({ limit: 1 })` and
remove the heavy owner‑team users prefetch.

- **Refactors**
- Add unit tests for infinite pagination in `use-data-source` (deferred
`loadMore`, aborted resets, error handling, and cursor continuity).

<sup>Written for commit 970abc0f03.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1766?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Added loading placeholders for customer avatar/name rendering across
payment and transaction customer tables to prevent jarring updates while
data loads.
* Improved infinite data loading to correctly queue and replay “load
more” after in-flight requests, handle abort/reset races, and avoid
stale cursor behavior; deferred requests are discarded when leaving
infinite mode.
* Improved user-scoped team lookups by validating team identifiers and
reliably returning `null` for missing/invalid teams.
* **Documentation**
* Clarified that user-level team lookups only return teams the user is a
member of.
* **Tests**
* Added/expanded coverage for infinite pagination cursor correctness,
race conditions, error handling, and mode transitions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: aman <aman@stack-auth.com>
2026-07-14 11:06:41 -07:00
Konstantin Wohlwend
770f01a057 Various improvements 2026-07-13 13:40:56 -07:00
Konstantin Wohlwend
31dc84075a Hosted component hacks 2026-07-10 10:45:35 -07:00
Konsti Wohlwend
d5ed20863a
Enable dev tool Dashboard tab via development-environment probe instead of env var (#1749) 2026-07-09 20:23:07 -07:00
Armaan Jain
d78405d7e7
Gate usage limit banners behind HEXCLAVE_DISABLE_PLAN_LIMITS (#1728)
## Summary

Threads `arePlanLimitsEnforced()` from the backend through the
plan-usage API response so the dashboard can hide all usage limit
banners when `HEXCLAVE_DISABLE_PLAN_LIMITS=true`.

Backend adds `are_plan_limits_enforced` to `planUsageResponseSchema`
(`.optional().default(true)` for backward compat with older backends) →
SDK surfaces it as `PlanUsage.arePlanLimitsEnforced` with `?? true`
runtime fallback (since `getPlanUsage()` returns raw JSON without yup
validation) → banner components early-return `null` when
`!arePlanLimitsEnforced`.

For the projects page (outside admin-app context, no project selected),
a server action reads the env var directly via
`getEnvVariable(\"STACK_DISABLE_PLAN_LIMITS\", \"false\")`.

Link to Devin session:
https://app.devin.ai/sessions/09ca53f13c294c8e98c7a7227a52217d
Requested by: @Developing-Gamer

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for a “plan limits enforced” flag to control messaging
across usage and billing-related screens.
* Projects and team invitation capacity checks now respect the same
enforcement toggle (including admin-seat invitation blocking).

* **Bug Fixes**
* Limit banners and “plan limit exceeded” alerts now only render when
enforcement is enabled (and overage/threshold conditions are met).
* Updated usage payload handling to default to enforcement enabled when
the flag is missing; added/updated tests for both enabled and disabled
scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-05 21:17:21 -07:00
BilalG1
4988312133
feat(dashboard): manual item quantity adjustments from customer payments view (#1731)
Adds a way for dashboard users to create a **manual item quantity
change** — positive or negative — for a single customer and a single
item, directly from the customer payments view.

**Base:** `dev` → **Head:** `feat/manual-item-quantity-change` · 4
files, +219/−10

## Demo

Full flow on a user detail page: open the Payments tab, hit the ± button
on an item balance row, apply **+5** with a description, then **−3** —
balance and transaction history update live.

![Manual item quantity change
flow](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/manual-item-quantity-change-flow.gif)

<sub>WebM version:
[trimmed.webm](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/trimmed.webm)</sub>

## Screenshots

| Adjust dialog (positive) | Adjust dialog (negative) |
| --- | --- |
| ![Positive change with live
preview](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/07-adjust-dialog-positive.png)
| ![Negative
change](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/09-adjust-dialog-negative.png)
|

| After applying changes | Entry point on item balance rows |
| --- | --- |
| ![Balance + transaction history
updated](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/10-after-negative-change.png)
| ![Item balances
card](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/06-user-payments-item-balances.png)
|

## What's new

- **`ItemQuantityChangeDialog`**
(`apps/dashboard/src/components/payments/item-quantity-change-dialog.tsx`)
- Signed integer quantity input (`10` adds, `-5` subtracts), validated
as a non-zero whole number, with a live `New balance: X → Y` preview.
  - Optional description, stored on the transaction.
- Submits via `adminApp.createItemQuantityChange` (which passes
`allow_negative: true`, so balances may go negative — consistent with
the existing admin API semantics).
- **Entry point** — a ± icon button on every row of the *Item balances*
card in `customer-payments-section.tsx`. Since that card is shared, the
feature works on the **user detail page**, **team detail page**, and the
payments **Customers page**, i.e. all three customer types (`user` /
`team` / `custom`).
- **SDK cache fix** (`packages/template`, SDKs regenerated) —
`createItemQuantityChange` previously left the item and transaction
caches stale, so a dashboard caller would keep seeing the old quantity.
It now refreshes the affected item cache (new protected
`_refreshItemCache` helper on the server app impl, mirroring what
`item.increaseQuantity` already did) and invalidates the transactions
cache. This is what makes the balance and transaction history update
immediately after applying a change.

## Notes for reviewers

- The dialog intentionally does not expose `expiresAt` (the API supports
it) — kept the surface minimal for the first pass.
- Transaction history already knew how to render
`manual-item-quantity-change` transactions (`api_credits (+25)` detail
rows), so no changes were needed there.
- The per-row dialog receives `currentQuantity` from the same `useItem`
hook that renders the row, so the preview can never disagree with the
displayed balance.

## Test plan

- [x] `pnpm typecheck` and `pnpm lint` green
- [x] Manual e2e against the seeded Demo Project (production builds of
dashboard + backend): created a user-scoped `api_credits` item, applied
`+25`, `−10`, `+5`, `−3` on a user — balance updated live (25 → 15 → 20
→ 17), each change produced an *Item quantity change* transaction,
success/error toasts behave, invalid input (`0`, non-integers)
disables/blocks Apply
- [x] Verified the adjust button renders for user-scoped items on the
user page (team/customers pages use the identical shared component)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an item quantity adjustment dialog for customer payments, with
live preview and “Apply change” support for manual whole-number deltas
(non-zero), plus success/error toasts.
* Payment item rows now include quick-action controls to open the
adjustment dialog directly.
* **Bug Fixes**
* Quantity updates now immediately refresh the affected item details and
keep related transaction data in sync after changes.
* Improved validation to reject invalid quantity inputs and provide
clearer inline feedback during submission.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-03 18:12:42 -07:00
aadesh18
d5d2f27a96
Move queryAnalytics to server app (#1687)
Moves `queryAnalytics` from the admin app to the server app so backend
code can call it without needing an over-privileged admin key. All
existing dashboard pages that ran queries internally are updated to use
`useServerApp()` instead of `useAdminApp()`.

## What changed

- **Backend**: New `POST /api/v1/analytics/query` and `POST
/api/v1/analytics/query/timing` endpoints on the **server** app
(previously admin-only)
- **Dashboard**: `queryAnalytics` calls in Queries, Query Analytics,
Session Replays, and Sign-up Rules pages switched from `adminApp` →
`serverApp`
- **Docs**: Added SDK + REST API usage example under the Queries section
of the Analytics guide
- **REST API reference**: New `Run analytics query` and `Get analytics
query timing` endpoints visible under Server API > Analytics

## Docs — Analytics › Queries section

New paragraph + code snippet showing
`hexclaveServerApp.queryAnalytics()` from backend code.

| | Light | Dark |
|---|---|---|
| **Before** |
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-before-light-queries.png)
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-before-dark-queries.png)
|
| **After** |
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-after-light-queries.png)
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-after-dark-queries.png)
|

<details>
<summary>Full-page overview (both themes)</summary>

| Light | Dark |
|---|---|
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-after-light.png)
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-after-dark.png)
|

</details>

## REST API reference — new Server API endpoints

`POST /analytics/query` and `POST /analytics/query/timing` now appear
under **Server API > Analytics** in the Mintlify reference.

| Light | Dark |
|---|---|
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/mintlify-run-analytics-query-light.png)
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/mintlify-run-analytics-query-dark.png)
|


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added public server-access analytics query support, including a query
timing endpoint for performance details.
  * Enriched API documentation for analytics queries and results.
* **Bug Fixes**
* Switched analytics querying across the dashboard and E2E tests to use
the correct public route and server access flow.
* Increased the long-request warning threshold for the analytics query
endpoint.
* **Documentation**
* Updated OpenAPI specs and the analytics guide with REST endpoint
details and examples.
* **Breaking Changes**
* Removed analytics query support from the admin client-side flow; use
the server-access method instead.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-02 09:42:10 -07:00
BilalG1
2f0d9bc6cb
fix(session-replay): gzip replay batches so large rrweb snapshots aren't dropped (#1675)
## Problem

Session replay batches are uploaded as **raw, uncompressed JSON**. A
single large rrweb full snapshot (>900 KB) is dropped client-side in
`SessionRecorder._flush`, and any batch near the server's 1 MB body
limit is rejected. Meanwhile the **analytics events** path right next
door already gzips on the wire (`encodeAnalyticsBody` + a server-side
gunzip with zip-bomb guards) — replays just never got the same
treatment.

Notably, `dev` was already **red** on the SDK test `sends a single
oversized event alone without dropping it` (the code dropped at 900 KB
while the test expected the event to be sent). This change makes that
test pass.

## What this does

Ports the proven analytics compression pattern to session replays:

- **`packages/shared` (client transport):** generalize
`encodeAnalyticsBody` → `encodeGzipJsonBody` and wire it into
`sendSessionReplayBatch`. Replays now gzip via native
`CompressionStream` (`application/octet-stream`), with the same
fallbacks: keepalive flushes send plain JSON (so the request survives
page teardown), and a missing/throwing `CompressionStream` (Safari <
16.4) degrades to plain JSON.
- **`packages/template` (SDK recorder):** replace the 900 KB hard-drop
with `MAX_SINGLE_EVENT_BYTES = 8 MB`, kept in sync with the server's
decompressed cap. The wire is gzipped downstream, so an oversized single
event is now sent compressed instead of discarded; only an event that
could never fit *decompressed* is dropped.
- **`apps/backend` (route):** add `maybeDecodeBinaryBody` (gunzip with a
1 MB compressed cap + 8 MB `maxOutputLength` zip-bomb guard) via a
`.transform` on the batch route body schema. Plain-JSON bodies pass
through untouched.

## Why gzip / CompressionStream

Benchmarked on a real rrweb capture (Puppeteer, rrweb 1.1.3): a **1.09
MiB full snapshot → 134 KiB gzip (~8×) in ~6.6 ms**. Native
`CompressionStream` adds **0 bundle bytes** and was faster than fflate.
This mirrors what PostHog (gzip, native `CompressionStream` + fflate
fallback) and Sentry (fflate deflate in a worker) both ship — both rely
on compression, not splitting, as the primary mechanism for large
events.

## Tests

- **Client SDK** (`client-interface.test.ts`): gzip+octet-stream on
non-keepalive (round-trip + size assert), plain JSON on keepalive, and
both `CompressionStream`-unavailable fallbacks.
- **SDK recorder** (`session-replay.test.ts`): existing oversized-event
test now passes; new test that a >8 MB event is dropped while a
following normal event still sends.
- **Backend e2e** (`session-replays.test.ts`): gzipped happy path
(`compressed < 1MB < raw`), invalid gzip → 400, oversized compressed →
413, zip-bomb → 400. Verified passing against a live local backend.

## Verification notes

- `@hexclave/shared` + `@hexclave/template` typecheck pass; touched
files lint clean; SDK unit tests pass.
- Two **pre-existing, unrelated** local failures (each confirmed by
stashing this change and re-running on clean `dev`): a backend typecheck
error in `oauth/ssrf-protection.test.ts` (`NODE_ENV` read-only), and 3
session-replay **quota** e2e tests failing in the payments
`setSessionReplayItemQuantity` setup helper.

## Follow-up (out of scope)

The keepalive/unload path still sends uncompressed (64 KB browser
keepalive cap), so a very large final batch on page exit can still be
lost — not addressed here.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Gzip session replay batches so large rrweb snapshots are sent instead of
dropped. Keepalive flushes cap single-event size to avoid 413s on page
unload, and limits now include a 1 KiB envelope margin to match server
caps.

- **Bug Fixes**
- Client (`@hexclave/shared`): generalized `encodeAnalyticsBody` →
`encodeGzipJsonBody`; gzip via native `CompressionStream` for
`/session-replays/batch`; keepalive and missing/throwing
`CompressionStream` fall back to JSON.
- SDK recorder (`@hexclave/template`): normal flushes allow a single
event up to `MAX_SINGLE_EVENT_BYTES = 8 MiB - 1 KiB`; keepalive flushes
cap single-event size at `MAX_BATCH_UNCOMPRESSED_BYTES = 900 KB`; send
oversized events alone and rely on gzip; drop only above those
thresholds; log a distinct 413 warning with the count of buffered events
dropped and stop the loop.
- Backend: gunzip `application/octet-stream` bodies with caps (`1 MB`
compressed, `8 MiB` decompressed); invalid gzip → 400; oversized
compressed → 413; plain JSON passes through.
- Tests: added client encoding tests for session replays; SDK tests for
>8 MiB drop, keepalive drop, and 413 warning; backend e2e for gzip,
invalid gzip, oversized compressed, and zip-bomb. The oversized-event
SDK test now passes.

- **Refactors**
  - Hoisted shared test helpers and trimmed comments.

<sup>Written for commit 855cdb052e.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1675?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Session replay batch uploads can now be automatically gzipped (when
supported) to reduce request size; analytics batch uploads use the same
behavior.
* **Bug Fixes**
* Improved handling of gzipped upload bodies, with stricter validation
and safer size/decompression limits to reject invalid payloads and “zip
bomb” scenarios.
* Updated session replay buffering so oversized events are dropped only
when they exceed decompression constraints, while allowing later events
to upload.
* **Tests**
* Added client, server, and end-to-end coverage for gzip/keepalive
behavior and limit enforcement.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 13:13:11 -07:00
BilalG1
81433e5181
feat(emails): arbitrary-recipient send + Emailable on managed domains (#1681)
## Summary

Two related email changes:

### 1. Send to arbitrary email addresses
The `sendEmail` SDK function and `POST /api/v1/emails/send-email` now
accept a third recipient selector, **`emails: string[]`**, mutually
exclusive (XOR) with `user_ids` and `all_users`. These recipients don't
have to belong to a user — they're mapped to the existing internal
`custom-emails` recipient type (no associated user object, can't
unsubscribe, so intended for transactional mail).

- Response `results` items are now `{ user_id?, email? }`; the `emails`
path returns `{ email }`.
- Validation message updated to `Exactly one of user_ids, all_users, or
emails must be provided`.
- Recipient addresses are validated with the shared `emailSchema`.

### 2. Run Emailable on managed custom domains
`LowLevelEmailConfig.type` is widened `'shared' | 'standard'` →
`'shared' | 'managed' | 'standard'`, and `getEmailConfig` returns
`'managed'` for the managed-Resend branch. The Emailable deliverability
check now runs for **shared** and **managed**, and is skipped for
**`standard`** (a customer's own SMTP server or Resend API key).

**Why:** managed domains send through our single Resend *account*
(per-domain scoped keys, but shared account → account-level bounce
penalties are collateral across managed customers), so we own that
reputation. With a custom SMTP server or the customer's own Resend key,
the customer owns their deliverability, so we don't second-guess their
recipients or spend Emailable checks on their volume. The shared
dev-email wrapper remains `shared`-only.

## Tests
- `send-email.test.ts`: added arbitrary-recipient send
(single/multiple), empty-array, and validation cases (both-selectors,
no-selector, invalid email); updated the exactly-one-of snapshot.
**24/24 pass.**
- New `emails/deliverability-gating.test.ts`: shared & managed skip an
undeliverable address (`LIKELY_NOT_DELIVERABLE`), custom SMTP still
sends. **3/3 pass.**

> Note: `email-queue.test.ts` has 2 pre-existing snapshot failures (a
`margin:0rem`→`margin:0` CSS normalization in rendered email HTML) that
are unrelated to this change — confirmed by reproducing them on `dev`
with these changes stashed.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds support for sending emails to arbitrary addresses and enables
Emailable checks for managed domains to protect shared sending
reputation. The API/SDK now accept `emails: string[]` as a recipient
selector, and deliverability checks run on shared and managed sending.

- New Features
- API/SDK: `emails: string[]` recipient selector (XOR with
`user_ids`/`all_users`) in `sendEmail` and POST
/api/v1/emails/send-email. Results return `{ email }` for this path;
addresses validated via `emailSchema`. Recipients map to
transactional-only “custom-emails” (no user, no unsubscribe).
- Deliverability: email config adds `'managed'`; `getEmailConfig`
returns it for managed Resend domains. Emailable check runs for `shared`
and `managed`; skipped for `standard` (custom SMTP or customer Resend
key).

<sup>Written for commit e48cf2ddda.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1681?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Emails can now be sent to arbitrary email addresses, not just existing
users.
* Email sending APIs and client options now accept an email list as a
recipient choice.

* **Bug Fixes**
* Improved recipient validation so exactly one target type must be
chosen.
* Fixed response results to consistently show either a user ID or an
email address.
* Corrected deliverability checks for managed email setups and updated
skip behavior for undeliverable addresses.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 11:16:25 -07:00
Konstantin Wohlwend
73b8a0c27f Fix redirect URL on devtool indicator 2026-06-26 14:12:47 -07:00
Konstantin Wohlwend
dff454f47f Document init script less 2026-06-26 13:50:48 -07:00
Konstantin Wohlwend
ee93732e8e Silence HEXCLAVE_BOT_CHALLENGE_SITE_KEY warning 2026-06-26 13:30:54 -07:00
Vedanta-Gawande
164374f6c8
User page email filtering (#1668) 2026-06-26 12:27:24 -07:00
Konsti Wohlwend
325fb791f6
fix: prevent OAuth callback race condition between startup handler and OAuthCallback component (#1671) 2026-06-26 11:44:28 -07:00
BilalG1
d2a84f5a28
fix: reduce recurring production Sentry errors (Stripe webhooks, email, session replay) (#1667)
## Summary

A cleanup pass over recurring production errors triaged from Sentry
(`stackframe-pw` org). The common thread: expected/edge-case conditions
thrown as `HexclaveAssertionError` / `captureError`, so Sentry filed
them as errors (and, for the Stripe ones, Stripe redelivered
indefinitely). Each is handled at the source or logged at the correct
severity.

| Sentry issue | Fix | Risk |
|---|---|---|
|
[STACK-BACKEND-1F5](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-1F5)
— `Unknown stripe webhook type` (`invoice_payment.paid`, `payout.paid`)
| Add both to `ignoredEvents`. They fell through to the throwing `else`
and Stripe redelivered them. (`payout.failed`/`canceled`/`updated`
intentionally left unhandled for now.) | Trivial |
|
[STACK-SERVER-1ZV](https://stackframe-pw.sentry.io/issues/STACK-SERVER-1ZV)
— session-replay `413 Request body too large` | Measure event size in
UTF-8 bytes (was UTF-16 `.length`, which undercounts multibyte content);
drop a single oversized event with a warning instead of shipping a
doomed request | Low |
|
[STACK-BACKEND-140](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-140)
+
[STACK-BACKEND-1F1](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-1F1)
— `Unknown error while sending (test) email` | Classify refused SMTP
connections (`ECONNREFUSED`, surfaced by nodemailer as `code:
'ESOCKET'`) as a typed `CONNECTION_REFUSED` error with a real
user-facing message, instead of falling through to the `UNKNOWN`
catch-all in both the low-level sender and the send-test-email route.
Marked `canRetry` so the queued-email path reschedules with backoff. |
Low |

## Notes

- **Session replay (1ZV):** edited the `packages/template`
source-of-truth; the generated SDK copies are gitignored and regenerated
by CI (`pnpm -w run generate-sdks`). The `TextEncoder` is hoisted out of
the rrweb emit hot path to avoid per-event allocation.
- **Email classification (140/1F1):** the new `CONNECTION_REFUSED`
errorType is additive — other consumers only read `errorType` for
logging, and the send-test-email route only special-cases `UNKNOWN`, so
the new type cleanly bypasses both assertion captures. `canRetry: true`
is safe because the connection is refused before any SMTP exchange (no
message handed off → no duplicate-delivery risk); transient refusals
recover, and a persistent misconfig still fails after
`MAX_SEND_ATTEMPTS`. The one-shot send-test-email path ignores
`canRetry`, so its immediate feedback is unchanged.

## Investigated but intentionally NOT changed here

These were initially included, then reverted so we keep getting Sentry
signal while the root causes are still under investigation:

-
**[STACK-BACKEND-1GM](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-1GM)**
— `Stripe webhook bad customer id`. A subscription-changed event with no
customer (the observed case was a Stripe-CLI test
`payment_intent.succeeded` against a dev-connected account). Skipping is
likely the right long-term fix, but kept the throw for now to keep
observing. Note: in live mode the same path could fire on legitimate
customerless one-time payments / guest checkouts.
-
**[STACK-BACKEND-1CN](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-1CN)**
— `Recovered N stale outgoing request(s)`. This is a self-healing
recovery notice (0 user impact); the underlying cause is the poller
process dying between the claim `UPDATE` and the delete. Kept at
`captureError` to keep collecting data on how often / why it happens.

## Verification
- `typecheck` clean: `@hexclave/backend`, `@hexclave/template`,
`@hexclave/js`, `@hexclave/react`, `@hexclave/next`,
`@hexclave/tanstack-start`
- `eslint` clean on all touched files
2026-06-25 14:48:49 -07:00
BilalG1
b8e384493d
feat(payments): unified customers table + Create checkout everywhere (#1666)
## Summary

Two related payments/dashboard changes:

1. **Customer page rework** (`/projects/<id>/payments/customers`) —
replaces the old single-customer selector page with **one unified
table** of users, teams, and custom customers, with filtering and
search. Clicking a row opens a customer view that renders the **same
data as the payments tab** on the user/team detail pages, plus a button
to return to the list.
2. **"Create checkout" everywhere** — the existing checkout dialog is
now reachable from every relevant surface (user table, team table,
user/team detail payments tabs, product detail page, product-lines
cards, and the customer detail view), all driven by **one shared
dialog**.

> ℹ️ Screenshots are from a local dev environment (test-mode banner +
dev-tools rail are dev-only chrome).

---

## 1. Customers page rework

A single `DataGrid` lists users + teams + custom customers (custom ones
are derived from transactions, since they aren't otherwise enumerable).
It uses the existing table/filter components: a **Type** filter (All /
Users / Teams / Custom) and quick-search.

![Customers
table](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/01-customers-table.png)

Filtering by type — e.g. **Custom** customers surfaced from transaction
history:

![Customers filtered to
Custom](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/02-customers-filter-custom.png)

Clicking a row opens a **read-only customer view** that is identical to
the payments tab on the user/team detail pages (metrics, products &
subscriptions, transaction history, item balances), with a **"Back to
customers"** button and a **Create checkout** button:

![Customer detail
view](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/03-customer-detail.png)

To guarantee the view is identical, the user/team payments tabs and this
page now share one generic `CustomerPaymentsSection` component keyed by
`(customerType, customerId)`.

---

## 2. Create checkout everywhere (one shared dialog)

The single `CreateCheckoutDialog` now supports:

- **Customer pre-selected** → just choose a product (tables, detail
tabs, customer detail view):

![Dialog with pre-selected
customer](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/04-dialog-preselected.png)

- **Locked product + customer selector** → launched from a product, the
product is fixed and you pick the customer:

![Dialog launched from a
product](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/05-dialog-from-product.png)

The customer selector reuses the existing searchable user/team tables
(nested dialog):

![Customer
picker](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/06-customer-picker.png)

Resulting checkout URL:

![Checkout URL
result](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/07-checkout-url-result.png)

### Entry points

**Product detail page** — in the ellipsis menu:

![Product detail ellipsis
menu](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/08-product-ellipsis-menu.png)

**Product-lines** product-card menu:

![Product-lines card
menu](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/09-productlines-card-menu.png)

**User table** row action (team table is analogous):

![User table action
menu](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/10-user-table-action.png)

**User detail payments tab** (and **team detail payments tab**) get a
Create-checkout button in the header:

![User payments
tab](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/11-user-payments-tab.png)

![Team payments
tab](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/12-team-payments-tab.png)

---

## Implementation notes

- **New SDK method**: `adminApp.createCheckoutUrl({ userId | teamId |
customCustomerId, productId, returnUrl? })` added to the
`@hexclave/template` **server** app (interface + impl). This is what
enables checkout for **custom** customers, which previously had no
checkout path (the old dialog only called
`customer.createCheckoutUrl(...)` on a `ServerUser`/`Team` object).
Regenerate SDKs after pulling (`pnpm -w run generate-sdks`).
- It lives on the server app (not client) deliberately: it targets an
*arbitrary* customer, which is a server/admin capability. The backend
route enforces this — for `client` auth it calls
`ensureClientCanAccessCustomer(...)` ("clients can only create purchase
URLs for their own user or teams they admin"). The client's safe, scoped
path is the existing customer-object method `user.createCheckoutUrl()` /
`team.createCheckoutUrl()`. The new method sits alongside
`grantProduct`/`createItemQuantityChange`/`getItem`, which take the same
`{ userId | teamId | customCustomerId }` shape.
- **New shared components** under
`apps/dashboard/src/components/payments/`:
- `customer-selector.tsx` — `CustomerSelector`, `CustomerTypeSelect`,
`SelectedCustomer`, `customerToMutationOptions` (extracted from the old
customers page).
- `customer-payments-section.tsx` — generic payments view;
`user-payments.tsx` / `team-payments.tsx` are now thin wrappers over it.
- `CreateCheckoutDialog` reworked to take a unified `customer`
descriptor and the new selector / locked-product props.

## Testing

- `pnpm typecheck` and `pnpm lint` pass.
- Manually verified end-to-end against a seeded local project (test
mode): generated real checkout URLs for a **custom** customer and a
**team**, exercised every entry point above, the type filter
(All/Users/Teams/Custom), search, row → detail → back, and the error
states (e.g. "product already granted", "no products for this customer
type").


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a unified Customers browser for payments with filter, quick
search, infinite scrolling, and a customer detail view.
* Enabled “Create checkout” directly from product and customer-related
screens (including list/dropdown actions).
* Added streamlined “customer payments” views (metrics, transactions,
product/subscription info, and item balances where available).
* Introduced a flexible customer picker to support user, team, and
custom customers in checkout flows.
* **Bug Fixes**
* Improved checkout validation, dialog open/close behavior, and
empty-state handling when no customer or products are available.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 14:48:27 -07:00
Konstantin Wohlwend
6883d83ad1 Fix overlay warning 2026-06-24 11:25:58 -07:00
Konsti Wohlwend
a6c46db72c
perf: prefetch email theme previews during earlier onboarding steps (#1655) 2026-06-23 15:46:03 -07:00
Konstantin Wohlwend
012098e1a9 Change Quick Sign Up email address 2026-06-23 13:23:17 -07:00
Konsti Wohlwend
7b0f430975
fix: suppress React 19 script warning in SsrScript (#1648) 2026-06-23 12:16:14 -07:00
Konsti Wohlwend
a2889fc044
fix: chunk SessionRecorder flush to stay under server 1MB body limit (#1647) 2026-06-23 12:04:46 -07:00
Konstantin Wohlwend
8820b39b37 Stop using app.urls everywhere 2026-06-23 11:22:44 -07:00
Armaan Jain
0132ec151a
Project onboarding speedup (#1596)
Speeds up project onboarding by consolidating API calls and adding
prefetching:

- Single PATCH endpoint for saving onboarding status + state together
- Background config save on welcome step (with proper retry/error
handling)
- Prefetch email themes and Stripe info for upcoming steps
- Suspense-based skeleton loaders instead of full-page spinners
- Extracted shared types and lightweight step components

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Unified onboarding progress saving for the new project flow,
persisting status plus optional onboarding state in a single update
path.
* **Improvements**
* Onboarding wizard now derives status/state from owned project data and
removes extra internal fetching/loading gates.
* Added skeleton/Suspense loading for email theme and payments steps,
with more reliable “final config”/completion sequencing.
* Admin project data now includes onboarding state; Stripe account info
uses cached retrieval.
* Backend avoids source-of-truth override changes during metadata-only
updates.
* **Tests**
* Updated onboarding wizard tests and added end-to-end coverage for
updating onboarding status/state together.
* **Documentation**
  * Added the “clickmaps” app icon to docs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-23 10:59:38 -07:00
Armaan Jain
80ba110e78
Usage page in settings (#1595)
## Summary

Adds a Usage page under Project Settings showing the owner team's plan,
billing period, and resource consumption (dashboard admins, auth users,
emails, analytics events, session replays) with progress bars and an
upgrade CTA.

Backend aggregates usage across the team via `sumTenancyUsage`
(parallelized with `Promise.all`) and serves it through `GET
/internal/plan-usage`. Shared types in `@hexclave/shared` define the
contract consumed by the SDK and dashboard.

## Screenshots

![Usage page — light
mode](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzE2ZTAyM2NkLTQzZjgtNDkyZS1hNDFkLTVmZjc1ZDg5NTQ3MSIsImlhdCI6MTc4MTU1MzY4OSwiZXhwIjoxNzgyMTU4NDg5LCJmaWxlbmFtZSI6InNjcmVlbnNob3RfZWE1YWU3YTJkNWQwNGM2NmFmYmM4NTY0YjQ2OTMxMDMucG5nIn0.O5H-gvyZ5an3wM7-CRcuyb6uFgg86cSftnAKnWh57VA)

Link to Devin session:
https://app.devin.ai/sessions/1bc3344126a442adb4f29ae373d346be
Requested by: @Developing-Gamer

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added Usage page in project settings displaying comprehensive plan
usage metrics including dashboard seats, authentication users, emails
sent, analytics events, and session replays
* Shows current usage against plan limits with visual progress
indicators
* Displays upgrade recommendations when plan limits are exceeded with
one-click upgrade functionality
  * Added Usage menu item to project settings navigation

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-23 10:32:07 -07:00
Armaan Jain
bd511a6255
fix(sdk): bypass urls proxy in internal SDK methods for hosted components (#1641) 2026-06-23 10:30:54 -07:00
Konstantin Wohlwend
0908170e52 Config errors are now more obvious 2026-06-22 23:42:17 -07:00
Konsti Wohlwend
591554c3b1
Silently ignore network errors in EventTracker and SessionRecorder flush (#1638) 2026-06-22 16:12:54 -07:00
Konsti Wohlwend
a66d94bb43
feat: devtool indicator - rename Quick Sign In → Quick Sign Up, remove email input (#1643) 2026-06-22 16:10:57 -07:00
BilalG1
38ae913fc9
Rename STACK_* env vars to HEXCLAVE_* in env templates, with legacy dual-read (#1588)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
## Summary

Completes the env-var side of the Hexclave rebrand: every
`STACK_*`-prefixed variable (including `NEXT_PUBLIC_STACK_*` and
`VITE_STACK_*`) is renamed to `HEXCLAVE_*` across all checked-in `.env`,
`.env.development`, and `.env.example` files (30 files, ~135 keys).
Legacy `STACK_*` names keep working everywhere via dual-read, so
**existing deployments, `.env.local` files, and self-hosted setups need
no immediate migration**.

## How legacy names keep working

- **Server code** already resolves `HEXCLAVE_*` first with `STACK_*`
fallback via `getEnvVariable`. Direct `process.env.STACK_X` readers fed
by the renamed files (prisma seed, e2e tests/helpers, internal-tool
scripts, examples, `prisma.config.ts`) now read `HEXCLAVE_X || STACK_X`.
- **Client code** (Next.js build-time inlining) uses literal dual-read
expressions; the dashboard's `_inlineEnvVars` already had them.
- **Docker/self-hosting**: `docker/server/entrypoint.sh` (shared by the
server and local-emulator images) gets a generic two-way
`HEXCLAVE_`↔`STACK_` env mirror — runs at startup and again before
sentinel replacement — replacing the previous URL-trio-only mirror.
Operators can use either prefix.

## The empty-placeholder trap (`||` vs `??`)

The checked-in templates define empty placeholders (`HEXCLAVE_X=#
comment` parses to `""` via dotenv). With `?? `-based fallbacks, that
empty string would silently shadow a real value under the legacy name —
including legacy vars set in Vercel/CI env at build time, since the
tracked `.env` is present during builds. All fallback chains therefore
treat empty-as-unset (`||`):

- `getEnvVariable` and `getProcessEnv` in `packages/shared`
- the dashboard/docs/example literal dual-reads
- the generated SDK env getters (via
`packages/template/scripts/generate-env.ts`; the generated
`src/generated/env.ts` files are gitignored and regenerate at build)

## Other notable changes

- Tests that override env now set the canonical `HEXCLAVE_*` name (it
wins over `STACK_*`): e2e `cross-domain-auth`, backend
`internal-feedback-emails` in-source test.
- e2e `helpers.ts` port-prefix expansion loop also matches the
`HEXCLAVE_` prefixes.
- `docker/local-emulator/generate-env-development.mjs` reads source keys
canonically (legacy fallback) and emits canonical keys; regenerated
output matches.
- `rotate-secrets.sh` falls back to
`HEXCLAVE_DATABASE_CONNECTION_STRING`.
- Docs code snippets (`docs/code-examples`) renamed outright to
canonical names, consistent with #1571.
- OAuth callback `console.warn` in `packages/template/src/lib/auth.ts`
now says Hexclave.

## Migration note for the team

Local `.env.local` files with legacy `STACK_*` overrides keep working
**unless** the override targets a var that `.env.development` now sets
to a real (non-empty) `HEXCLAVE_*` value — the canonical name wins over
file precedence. Rename those keys in your `.env.local` once.

## Verification

- `typecheck` + `lint` pass on every touched package (shared, backend,
dashboard, e2e, internal-tool, cli, docs, template). Pre-existing
failures on dev (`admin-app-impl.ts` typecheck, dashboard metrics-page
errors) are unchanged (identical error counts with/without this change).
- `getEnvVariable`/`getProcessEnv` fallback semantics smoke-tested
directly (empty-HEXCLAVE → legacy fallback, HEXCLAVE wins when set,
defaults intact).
- `internal-feedback-emails` in-source vitest passes; emulator env
generator `--check` passes; `bash -n` on touched shell scripts.
- Two independent review agents audited the diff for correctness bugs
and coverage gaps; all confirmed findings are fixed in the third commit.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Renamed all `STACK_*` env vars (including
`NEXT_PUBLIC_STACK_*`/`VITE_STACK_*`) to `HEXCLAVE_*` across env
templates and code, with dual‑read that treats empty as unset, detects
conflicts, ignores post‑build sentinels, and falls back to legacy names.
All GitHub Actions now use `HEXCLAVE_*`; local‑emulator e2e is fixed by
setting `NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR` in CI.

- **Refactors**
- Added conflict‑aware dual‑read helpers (prefer `HEXCLAVE_*`,
empty‑as‑unset, ignore post‑build sentinels, preserve empty passthrough)
and used them across `packages/shared` (resolver + tests),
`apps/dashboard` inline/public envs (with tests), `apps/backend` Prisma
config/seed and vitest (accept both prefixes), `packages/cli`
(API/Dashboard URLs, project ID, `HEXCLAVE_EMULATOR_HOME`; tests),
Docker (`entrypoint.sh` mirroring + `rotate-secrets.sh` DB URL),
docs/components (`docs/src/lib/env.ts`), and examples; hosted/Vite apps
now error if both spellings differ.
- Port‑prefix expansion includes `HEXCLAVE_*`; backend tests use a new
helper to resolve DB connection strings; Prisma prefers
`HEXCLAVE_DATABASE_CONNECTION_STRING` with legacy fallback.
- Generated SDK env getters use plain `HEXCLAVE_*` || `STACK_*` (no
conflict throw); dashboard inline resolver preserves empty/sentinel
passthrough to avoid build failures; docs/examples include dual‑read
utilities.
- Tests now stub canonical `HEXCLAVE_*` flags (e.g., plan limits, bot
challenge, OAuth tokens, hosted handler) to avoid shadowing/conflict
with committed defaults.

- **Migration**
  - No immediate action; legacy `STACK_*` names still work.
- If both names are set with different values, builds/scripts error. Set
only `HEXCLAVE_*` or make both equal.
- SDK consumers won’t see conflict throws; update env names to
`HEXCLAVE_*` over time.

<sup>Written for commit 7539fb9fbf.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1588?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Migrated environment variable names from the legacy `STACK_*` prefix
to the new `HEXCLAVE_*` prefix across backend, dashboard, tooling,
Docker, and examples.
* Updated environment/config resolution to prefer `HEXCLAVE_*`, treat
empty strings as unset, and detect conflicts when both `STACK_*` and
`HEXCLAVE_*` are set to different values.
* Updated local emulator, server startup, and env-generation workflows
to use the new names (with legacy fallback where applicable).
* **Documentation**
  * Updated docs and code examples to reference `HEXCLAVE_*` variables.
* **Tests**
* Refreshed unit and e2e coverage to validate dual-read behavior,
conflict detection, and empty-value handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 18:58:53 -07:00
Konsti Wohlwend
e3202a331c
feat: devtool indicator auto-visibility based on NODE_ENV (#1624)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
Publish npm packages / publish (push) Has been cancelled
Publish Swift SDK to prerelease repo / publish (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
2026-06-19 16:47:02 -07:00
Mantra
3493df464b
Fix typecheck in template cross-domain test (#1628)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
## What

Fixes a TypeScript error in `@hexclave/template` that was breaking `pnpm
typecheck` repo-wide:

```
client-app-impl.cross-domain.test.ts(450,101): error TS2345:
Argument of type '(options: { url: string | URL; }) => Promise<void>'
is not assignable to parameter of type '(...args: unknown[]) => unknown'.
```

## Why

The `vi.spyOn(...).mockImplementation(...)` callback typed its parameter
directly as `{ url: string | URL }`, but `mockImplementation` expects
`(...args: unknown[]) => unknown`. `unknown` isn't assignable to `{ url
}`, so the build failed.

## How

Accept variadic `unknown[]` args and cast `args[0]` to the expected
shape — matching the spy's actual signature while preserving the test's
behavior.

`pnpm --filter=@hexclave/template typecheck` now passes.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fix TypeScript typecheck failure in `@hexclave/template` cross-domain
auth test by updating the `vi.spyOn(...)._redirectTo.mockImplementation`
to accept `(...args: unknown[])` and casting `args[0]` to `{ url: string
| URL }`. This aligns the mock with its expected signature and restores
`pnpm typecheck`.

<sup>Written for commit 4a1787ee66.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1628?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
2026-06-18 17:55:17 -07:00
Konstantin Wohlwend
9388b99c2f Better OAuth sign-up errors 2026-06-18 11:31:25 -07:00
Konstantin Wohlwend
d88e77c67b User ID filter for email outbox 2026-06-17 13:39:26 -07:00
Konstantin Wohlwend
07449da7e9 Mute ANALYTICS_NOT_ENABLED warning 2026-06-17 12:17:17 -07:00
Konstantin Wohlwend
ec0008d515 Aggressively deprecate app.urls.xyz & update docs to avoid it
Closes #1587
2026-06-17 09:56:41 -07:00
Konsti Wohlwend
4546615713
feat: add custom OIDC provider support (team plan+ only) (#1594) 2026-06-16 16:35:11 -07:00
Konsti Wohlwend
7b824526fd
fix: silently disable EventTracker and SessionRecorder when analytics app is not enabled (#1599) 2026-06-16 10:42:57 -07:00
Armaan Jain
f97d3f5af9
Devtool tab transition fix (#1600)
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/hexclave/hexclave/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes devtool tab switching so hidden panes don’t intercept clicks and
active panes render without flicker.

- **Bug Fixes**
- Replace visibility/animation with display: none/block, plus opacity
and z-index, so only the active pane is interactive.
- Add pane background and remove fade-in animation to prevent overlap
and iframe flicker.

<sup>Written for commit ec80835045.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1600?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Refined dev tool tab pane display behavior for improved visual
rendering and interaction handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-15 18:35:56 -07:00
Konstantin Wohlwend
2eabf33612 Don't disable analytics in the setup step 2026-06-15 18:16:47 -07:00
Mantra
e93b7520c4
feat(analytics): add route analytics heatmaps (#1520)
## Summary

Adds route analytics heatmaps, stacked on top of
`codex/analytics-overview-filters` (#1496).

- Heatmap API routes (`/analytics/heatmap`, internal heatmap +
heatmap-token endpoints)
- Signed heatmap token signing/verification lib + tests
- Dashboard heatmaps page (client + route)
- Dev-tool + event-tracker support for heatmap capture
- ClickHouse migration support

## Demo


https://app.devin.ai/attachments/49cd6a96-8962-46d9-b8fb-145746cc6dee/rec-c80ec66f-21a3-49fb-bfae-19195ce7b930-edited.mp4

## Notes
Base branch is `codex/analytics-overview-filters` so the diff shows only
the heatmap changes. Will retarget to `dev` once the base PR lands.

Link to Devin session:
https://app.devin.ai/sessions/16f8adac29b948b38280c85418617fea
Requested by: @mantrakp04

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added clickmap overlay to analytics dashboard, enabling visual click
heatmap analysis on live websites.
* Enhanced analytics metrics with hourly breakdowns, bounce rates, and
top regions/browsers/devices filtering.

* **Bug Fixes**
  * Improved click event tracking accuracy and dead-click detection.
  * Fixed overlay z-index stacking for better visibility.

* **Style**
* Updated dashboard card padding and navigation button styling for
consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mantra <mantra@stack-auth.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 12:06:16 -07:00
Armaan Jain
3c89bb8c19
Hosted components redesigned (#1573)
## Summary

Redesigns the hosted components app (`apps/hosted-components`) with a
new Tailwind-based UI: rebuilt auth pages (sign-in, sign-up, magic link,
forgot/reset password, MFA, email verification, team invitation, CLI
auth confirm) and a full hosted Account Settings suite (profile, emails
&amp; auth, notifications, active sessions, API keys, payments, teams),
with dark mode support.

Also fixes found along the way: form error clearing in
forgot-password/password-reset, `runAsynchronouslyWithAlert` for the
notifications switch, a `CopyButton` DOM prop leak, a bogus
mobile-session icon check, and imports the app stylesheet in the root
route so the app's Tailwind styles actually apply.

## Before / after screenshots

Captured on the local dev setup (`internal` project). Onboarding is not
shown since it redirects without standalone UI in this setup.

<details>
<summary><b>Sign in</b></summary>

| | Before | After |
|---|---|---|
| Light | ![sign-in before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2VmMDljMzE3LTIzN2QtNGYzOC05ZDczLWI5ZmRiMGVmMWZjOSIsImlhdCI6MTc4MTEzNzkxMCwiZXhwIjoxNzgxNzQyNzEwLCJmaWxlbmFtZSI6InNpZ24taW4tYmVmb3JlLWxpZ2h0LnBuZyJ9.IMnk1DN0rLl_iW2Fgy99rVMeazQeii-DUTM0bXQsNfo)
| ![sign-in after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2FjZDQ4OTJlLTFkM2YtNGM1Ni1iMmYxLTZjNTAwMDBkNzAwYSIsImlhdCI6MTc4MTEzNzkxMCwiZXhwIjoxNzgxNzQyNzEwLCJmaWxlbmFtZSI6InNpZ24taW4tYWZ0ZXItbGlnaHQucG5nIn0.KdtHe6KxQQ3aY-J5Q2tTCDaUQIsJg1WI0J-Knp1zKMg)
|
| Dark | ![sign-in before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzZkNTFkY2E5LTUzNTAtNDExMC04ZjYzLTNlZDA3MjUyMTJiMCIsImlhdCI6MTc4MTEzNzkxMCwiZXhwIjoxNzgxNzQyNzEwLCJmaWxlbmFtZSI6InNpZ24taW4tYmVmb3JlLWRhcmsucG5nIn0.SLd1PJv0lj__533lN6YUElnAO5vtKeyokwv4EbngQHw)
| ![sign-in after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2E2ODkyZTUzLTU1YmEtNGRkNi1iYzQ4LTgzMmI0YzVmZDk2MyIsImlhdCI6MTc4MTEzNzkxMCwiZXhwIjoxNzgxNzQyNzEwLCJmaWxlbmFtZSI6InNpZ24taW4tYWZ0ZXItZGFyay5wbmcifQ.7MA8WAeZI6pgErTZ52wvkNvRBMhJUKf0SFoIenWLSLk)
|

</details>

<details>
<summary><b>Sign up</b></summary>

| | Before | After |
|---|---|---|
| Light | ![sign-up before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzlhMGU4YTYwLWNkNDAtNDAwZi1hNjAxLTZlOTBhMzc4ODAzNyIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6InNpZ24tdXAtYmVmb3JlLWxpZ2h0LnBuZyJ9._LcowDPO6d2FrWOBIAtSMha2QnoyV1pAp6O58ufrJ_M)
| ![sign-up after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzgzNjMyZmZkLTg5ZWUtNDU2Yy05YjMzLWZjYTc3NThhZDM3MSIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6InNpZ24tdXAtYWZ0ZXItbGlnaHQucG5nIn0.Tpa1eJ_I09lRhEfG1-kbZIGHenGKTo6vomTKsbjSRwg)
|
| Dark | ![sign-up before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzkyYzc3ODYzLWNmOTItNDNiZC1hZjNkLWE5ZDYyYzMyYTAxYiIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6InNpZ24tdXAtYmVmb3JlLWRhcmsucG5nIn0.QYYKXyNHQr83EFIHcttgweFWrPe_9BeEuNtlnJLlgYU)
| ![sign-up after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2UyY2U3ZTI5LTRlOGQtNGI0Mi1iYjI2LTllNDU2MjA3NGU2ZCIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6InNpZ24tdXAtYWZ0ZXItZGFyay5wbmcifQ.rO5HQURSI1MWI5gDYqQj40gNR4UXCajbzw3K8ns6S6o)
|

</details>

<details>
<summary><b>Forgot password</b></summary>

| | Before | After |
|---|---|---|
| Light | ![forgot-password before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzdkZmM5MjkwLWU1MDgtNGMyOS04MWRlLTRiY2RiMTY2M2EyYiIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6ImZvcmdvdC1wYXNzd29yZC1iZWZvcmUtbGlnaHQucG5nIn0.DYyid0bzNTRW6RTdXJlP9Wfxuc_cR9BTlC0iFK5aitA)
| ![forgot-password after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzQyYTQzZWMxLTVhYWMtNDQ5ZS04YzgyLWE2M2UwYjk4MjVhYSIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6ImZvcmdvdC1wYXNzd29yZC1hZnRlci1saWdodC5wbmcifQ.ZwFKelHXn-8wq_uY5nrpIg811b_plgXgA0BdBJifUSs)
|
| Dark | ![forgot-password before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzU5MDY3ZDkzLWJjMjEtNDZhYy1hZDkzLTNjYzQyNGFhMDI2MiIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6ImZvcmdvdC1wYXNzd29yZC1iZWZvcmUtZGFyay5wbmcifQ.lOHX4XqjhCExM7dL86O4BngKiSXYNaAvC0LH-AXHhlg)
| ![forgot-password after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzkxYTBkODk5LTk2OWUtNGU2Ni04ODE1LWU4NDA1ODhlYmQzNyIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6ImZvcmdvdC1wYXNzd29yZC1hZnRlci1kYXJrLnBuZyJ9.hvhoTYUq_cx3y9BrI03kyqA28MkXE46mv-hxSpeDuOg)
|

</details>

<details>
<summary><b>Password reset</b></summary>

| | Before | After |
|---|---|---|
| Light | ![password-reset before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2Q1MDk3ZGUzLTM5MGItNDMwNS1hM2YzLTgzMDU2Yzg4NDNlZiIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6InBhc3N3b3JkLXJlc2V0LWJlZm9yZS1saWdodC5wbmcifQ.PX_M155ZtvVqq7lVyL7LidY1GAoZkOd-6u508R0xvqU)
| ![password-reset after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzZhOWU5YTE2LTFlODEtNDRjZC1hNDY5LTc3NTg4ZGM0MDlmNCIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6InBhc3N3b3JkLXJlc2V0LWFmdGVyLWxpZ2h0LnBuZyJ9.Pg7t4tmTGIs5sZLnR69460qpaAajpfUnp7IyrxYS7TI)
|
| Dark | ![password-reset before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzFiODMwYjQwLTZkMjEtNDdkYy1iNmIzLWE4YmU5MmE5NmYxMyIsImlhdCI6MTc4MTEzNzkxMSwiZXhwIjoxNzgxNzQyNzExLCJmaWxlbmFtZSI6InBhc3N3b3JkLXJlc2V0LWJlZm9yZS1kYXJrLnBuZyJ9.DHpL8T-8OCF3UY9GObvqXPIqtShwSftgtjopKGzulww)
| ![password-reset after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzU4MTI3Y2ZkLTc3MGYtNGQzNS1hZTQ0LTEwMWVkYWVjNjM2NyIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6InBhc3N3b3JkLXJlc2V0LWFmdGVyLWRhcmsucG5nIn0.USTT-ErYoVs_ua7y6i6YyvMZD3r-WI3Ag1OPxXfijHM)
|

</details>

<details>
<summary><b>Email verification</b></summary>

| | Before | After |
|---|---|---|
| Light | ![email-verification before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzczMDlhNDM0LTZjZDItNGI0OS1hYTQ5LTM4NmY1YWZjZDMxOCIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6ImVtYWlsLXZlcmlmaWNhdGlvbi1iZWZvcmUtbGlnaHQucG5nIn0.kcRSuETM4LLSH0RWJWbXmA8_5tFstGTcIZqwudyWvQs)
| ![email-verification after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzhkMTdlMjNjLTkzNTItNGJiOS1iODMxLWM2ZDhmMzUxNDMwMiIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6ImVtYWlsLXZlcmlmaWNhdGlvbi1hZnRlci1saWdodC5wbmcifQ.WeUG_VTG4P2fsMNwXPrXn1VdNCAJfA-kM92mix2F8M0)
|
| Dark | ![email-verification before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2M2YWQzODAzLTgzMTUtNGE0NS05MzAxLWI2N2JkYzAwMmQ1NSIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6ImVtYWlsLXZlcmlmaWNhdGlvbi1iZWZvcmUtZGFyay5wbmcifQ.Xouj4wlz7IvNPPSb-c6owaC0orbtDG3N4lOe0899qZI)
| ![email-verification after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzI3OTIzYWM0LTMxMDAtNDEzYi05OTNkLWZmN2IwMGI5ZDliYyIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6ImVtYWlsLXZlcmlmaWNhdGlvbi1hZnRlci1kYXJrLnBuZyJ9.c9B6VWiHpZ2odb13_td4jc6lnXXvsjx9J3e4dqIwtkI)
|

</details>

<details>
<summary><b>MFA</b></summary>

| | Before | After |
|---|---|---|
| Light | ![mfa before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzIxY2Q2NjUzLTgwM2EtNDJmMi1hN2M3LWIyOGEwYWU4NTY2YyIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6Im1mYS1iZWZvcmUtbGlnaHQucG5nIn0.5IumrPBua_uTQq5gtUozGDkxuEM1dHjPV9mBUoWYKaM)
| ![mfa after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2Y4NWJkMzc1LWQ5NjEtNDBjOS04NzZmLTNlZmUxMWQ5YjdiOSIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6Im1mYS1hZnRlci1saWdodC5wbmcifQ.2yPq0cX2FkFgwAWm7iJ7PragpDSCEIITLcHceTKCriY)
|
| Dark | ![mfa before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2M4NmJmMTI1LWNhNzMtNDEwNi1hOGY2LTVjOTY3OTViZDUwNyIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6Im1mYS1iZWZvcmUtZGFyay5wbmcifQ.nmbD5lOLRjf-G4cnxByrwpymVNVGEIlVpYkbvSz9nB0)
| ![mfa after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzA3MzMwNzIwLTI3YmUtNGRlNy1hMDA1LWJiOGY1NzI3NmY2NSIsImlhdCI6MTc4MTEzNzkxMiwiZXhwIjoxNzgxNzQyNzEyLCJmaWxlbmFtZSI6Im1mYS1hZnRlci1kYXJrLnBuZyJ9.zcxkCgWPnOqqv0v17JqF0eFlkpBMu_skiAq9cQAJNXY)
|

</details>

<details>
<summary><b>Error</b></summary>

| | Before | After |
|---|---|---|
| Light | ![error before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzAyY2Q5MWUzLWI0YzktNDRmNi1iOWE0LTY4ZmIyMjZhOGYyYSIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6ImVycm9yLWJlZm9yZS1saWdodC5wbmcifQ.qVgMnd8c_VI82JIH9jVednhDKiVNlxv_K60eCb9UbTA)
| ![error after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzk0MzBlN2U0LWY1YjUtNDA5Ni1iZDRlLWM1YzQ3YTgwZDRjZiIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6ImVycm9yLWFmdGVyLWxpZ2h0LnBuZyJ9.612VxQuYtoittB4t-GJ8SkNaNDu5CqKierhWoJ5uA-o)
|
| Dark | ![error before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzBkZDBhYzdkLTc3NzItNDkwNS04MmEwLWUwMmZkYzMzOTg1MyIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6ImVycm9yLWJlZm9yZS1kYXJrLnBuZyJ9.UaM45CkfbiEpCZWHCt3IbgAoN_QcK6tBgPnAH85vb4k)
| ![error after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2U5NWMxYzZlLWRkNTUtNDBjMS1hYWMyLTE5MTRmYzdhZDRmZSIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6ImVycm9yLWFmdGVyLWRhcmsucG5nIn0.r3qiOPZKyNgpnDQJMPc6exaxzQnKm8wH3Jg5WcQTRCQ)
|

</details>

<details>
<summary><b>Team invitation</b></summary>

| | Before | After |
|---|---|---|
| Light | ![team-invitation before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2RkYmQ0YzMzLWFkYTctNGVlOS1iZWI5LTYyYWUxMWNlZDA5MSIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6InRlYW0taW52aXRhdGlvbi1iZWZvcmUtbGlnaHQucG5nIn0.KdNBx7LdUFquws4L8YE5bVsrtNh06hDLPcFqOVz7CD4)
| ![team-invitation after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzg2N2EwZmE2LWM4YzEtNDhkZi1iMTEwLTBmOGZiZDhiMjdiNyIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6InRlYW0taW52aXRhdGlvbi1hZnRlci1saWdodC5wbmcifQ.HLbHcxwjxObirriFXwTtY6jTiMNhKC6s-IafsG2irHA)
|
| Dark | ![team-invitation before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzNiZTAwMTZiLWIwZmQtNGRhMS05NmY1LTliMzIzY2IyNzdkNiIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6InRlYW0taW52aXRhdGlvbi1iZWZvcmUtZGFyay5wbmcifQ.W3Bout2l1fTdbBRKp_ztfUindfR2cZEJ0UTP5kmFPBg)
| ![team-invitation after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzhiZTJjODc4LTg0YzMtNDNjYy05OWRhLTI3ZmVkYmQwNWIzOCIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6InRlYW0taW52aXRhdGlvbi1hZnRlci1kYXJrLnBuZyJ9.m6G5vmWLFAh_yClcbg768rSiMZGkIyov8gHnNVLU95A)
|

</details>

<details>
<summary><b>CLI auth confirm</b></summary>

| | Before | After |
|---|---|---|
| Light | ![cli-auth-confirm before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzY4ZDA2YzdjLWFkN2ItNDE2ZS05N2E1LTZiZDI2ZDFmYmY1ZSIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6ImNsaS1hdXRoLWNvbmZpcm0tYmVmb3JlLWxpZ2h0LnBuZyJ9.0kGQpHc8RZc53_wKC8jJIhuXztp_4keASpMR24HtcK0)
| ![cli-auth-confirm after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzY4ZGU1M2VjLWU3YzItNDY5My1iZjgzLTM0OWVlYjZjY2M0ZiIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6ImNsaS1hdXRoLWNvbmZpcm0tYWZ0ZXItbGlnaHQucG5nIn0.sHm69r1KOP72z8wdKVzo38QTPp1GUIAUXVtac8IL-zM)
|
| Dark | ![cli-auth-confirm before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzVlMzVhNTQ0LTUxYjItNDFiYy1iYWNhLTA4YjcyMWQ2NzhkYSIsImlhdCI6MTc4MTEzNzkxMywiZXhwIjoxNzgxNzQyNzEzLCJmaWxlbmFtZSI6ImNsaS1hdXRoLWNvbmZpcm0tYmVmb3JlLWRhcmsucG5nIn0.MxJtvrhplMERLWTmm0KklvROBLjH90CB6FsVyQsB8ak)
| ![cli-auth-confirm after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2IxZDQxZjI3LWM0YmQtNGM5OC05OTU1LTkxOGMzNTJiN2NjMiIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImNsaS1hdXRoLWNvbmZpcm0tYWZ0ZXItZGFyay5wbmcifQ.cyjPDer_-G8i_uQWSD4ESYEZmk32VfAF73zuUf-KHPw)
|

</details>

<details>
<summary><b>Account settings — Profile</b></summary>

| | Before | After |
|---|---|---|
| Light | ![account-settings-profile before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2Q5ZTQzZjZlLWIwZGMtNDE3Yi04Njc3LWFiMzVhYjE5Mjg4MiIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtcHJvZmlsZS1iZWZvcmUtbGlnaHQucG5nIn0.lmgOIaj7IllT7txfIpzMKEo_iz1DLp00a_odsn9IW1s)
| ![account-settings-profile after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzM0YWEzNzI4LWU4MDMtNDA4Ny04NDliLTg5NWQxZjdlZWQzMiIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtcHJvZmlsZS1hZnRlci1saWdodC5wbmcifQ.512uIE8QjTWDlyrbvOqOw2QPV01_VyHUXxcdbdmr5CI)
|
| Dark | ![account-settings-profile before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzQyMzhjYmZmLWZlZTMtNDkwYS05NjE3LTdiYzU2MDY3ZDliYiIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtcHJvZmlsZS1iZWZvcmUtZGFyay5wbmcifQ.NWNH23YBwn67auhDQJNbR1jvP5BiW_QKWH0X-9ZB1wQ)
| ![account-settings-profile after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzhlODgxNDYzLWNmMDgtNDA1Ny04ZThkLTdhMjQ0NTkxYzQwZSIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtcHJvZmlsZS1hZnRlci1kYXJrLnBuZyJ9.2Nfs2UXmq4pgFhULizgIHxYuISFtkb24KKGr2hveTkM)
|

</details>

<details>
<summary><b>Account settings — Notifications</b></summary>

| | Before | After |
|---|---|---|
| Light | ![account-settings-notifications before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzIzZDk2NDQ4LTcwZWMtNDM5Ni1iNmNiLWEyZjI4MzU5Mjc2ZCIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3Mtbm90aWZpY2F0aW9ucy1iZWZvcmUtbGlnaHQucG5nIn0.2um6D6uKDsgOp8javP5PcyRdbLewxyb2vGk_qc2zeho)
| ![account-settings-notifications after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzBlYjI0YTE4LTMzZGUtNDcxNC04NGQ5LTI2ODYwZjE0YWQ3NiIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3Mtbm90aWZpY2F0aW9ucy1hZnRlci1saWdodC5wbmcifQ.iIZM0PdMO_gavcUrYORGhnZ_oqjvoE1KHCnJwBGMAJQ)
|
| Dark | ![account-settings-notifications before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2Q2OGFjZmE3LTU5MTAtNDViNi1hOWQ1LTQ2MzU2YjUyNmFkMiIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3Mtbm90aWZpY2F0aW9ucy1iZWZvcmUtZGFyay5wbmcifQ.L1VCD8J7ySFj6n_OOAZFh8gCXBWuF5j7OLWvyfGqvOc)
| ![account-settings-notifications after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2VkYmQ1NDg0LTQzMGUtNDU2Ni1hNzJhLWE0YzlhNWIyOTM2YyIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3Mtbm90aWZpY2F0aW9ucy1hZnRlci1kYXJrLnBuZyJ9.JvPKISE9uWzn18CEEzOatR2Xp8CMOD4x978wFfT4ixA)
|

</details>

<details>
<summary><b>Account settings — Active sessions</b></summary>

| | Before | After |
|---|---|---|
| Light | ![account-settings-sessions before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2NhNDQ5NjU1LWYyZDQtNGQ3Ny05YzBjLTI0MmM5ZTQ5MzdiOCIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3Mtc2Vzc2lvbnMtYmVmb3JlLWxpZ2h0LnBuZyJ9.A62xUZfvNQ-yCVUBvITnbPB19cUl9FsVHngITE2XfTI)
| ![account-settings-sessions after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzI5YzlkY2JjLWQzM2EtNGViMC04NGUzLTY1ODIyMjQ2OGM1OCIsImlhdCI6MTc4MTEzNzkxNCwiZXhwIjoxNzgxNzQyNzE0LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3Mtc2Vzc2lvbnMtYWZ0ZXItbGlnaHQucG5nIn0.JzIuAIuXxAOy4UWoaELGSkkwKx1MeUxeEBYWS1hLzeo)
|
| Dark | ![account-settings-sessions before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2IwNzkyNzcyLThmOGQtNDVhNi1iNTU3LTc0NjZlN2RhMDliMiIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3Mtc2Vzc2lvbnMtYmVmb3JlLWRhcmsucG5nIn0.TAFHH_d2vZzZouOAn8HQY_UpEHnaCNaCtDWWNGGzBZk)
| ![account-settings-sessions after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2I5MDNkZjU2LWE5NzYtNGRmZS1hMDZhLWM0ODJlNTBhODMyMyIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3Mtc2Vzc2lvbnMtYWZ0ZXItZGFyay5wbmcifQ.BS4vn6FHt0ILUGzzHkoenSg-ITTOdlCbiPI-k7wCpW0)
|

</details>

<details>
<summary><b>Account settings — API keys</b></summary>

| | Before | After |
|---|---|---|
| Light | ![account-settings-api-keys before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzY0MDQ2YjExLWEwNzYtNDFjYi1hYzE3LTU2NjI0MDZhYzFjNyIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtYXBpLWtleXMtYmVmb3JlLWxpZ2h0LnBuZyJ9.gIiXz5bL0rVTFi8m4xjBwqDrXGdep6eTAtrBl8-Vk3s)
| ![account-settings-api-keys after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzFmODUyMTEwLWM2YWQtNGVkOS04YWQ0LTAxOTVjNWMzOTkzYSIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtYXBpLWtleXMtYWZ0ZXItbGlnaHQucG5nIn0.9MQw8j0RH0nEvjWrCV478fIMeNnqhCKQr7yuUEx9HUM)
|
| Dark | ![account-settings-api-keys before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzIwNTA5ZWUzLTBjOTktNGUwNi1iODI0LTU4MjYwODA1NmJlOCIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtYXBpLWtleXMtYmVmb3JlLWRhcmsucG5nIn0.vQDdStoNd5qcCyW4jrqN5DrWjDmakmy1AG_7yjEhTVs)
| ![account-settings-api-keys after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzAxNDZiNWM2LWMwMDEtNDFiMy1hNDRiLTAzNmQ4OWJkMGE3MSIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtYXBpLWtleXMtYWZ0ZXItZGFyay5wbmcifQ.fET1gYmJX8TE_LAgEW_3mPgFMPQEf0gd3lm_SbN3CIQ)
|

</details>

<details>
<summary><b>Account settings — Payments</b></summary>

| | Before | After |
|---|---|---|
| Light | ![account-settings-payments before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzg5MDc3NWI3LTVjMzItNDBjNS04NDIyLTExOTVmMTdhNTVmMCIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtcGF5bWVudHMtYmVmb3JlLWxpZ2h0LnBuZyJ9.1EHLx9Y5eE30pX5s95qrHI63eBEqwAM_imJIvVuSkJs)
| ![account-settings-payments after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzc5YjEyYjkyLWEyZWUtNGIzMC1hYjRmLTEzZjk1N2FlZWI3ZSIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtcGF5bWVudHMtYWZ0ZXItbGlnaHQucG5nIn0.TRjKFdwfArHf5NSYH8OkKydxKWQKPcy-DbLmx641IxM)
|
| Dark | ![account-settings-payments before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzU0MjY1ZGI3LTJjOWYtNDZiNi05MzY2LTE2OWRjZTk5YjM2OSIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtcGF5bWVudHMtYmVmb3JlLWRhcmsucG5nIn0.E4qAaoYQ1_VM18WdRR0SndS1rNAy8Y_iiwZpG6XFmEA)
| ![account-settings-payments after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2E3NDUxYmQ4LTFhM2QtNDhjZC04MTdkLTEwYTc0ODNlYjdiZSIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtcGF5bWVudHMtYWZ0ZXItZGFyay5wbmcifQ.dSBbdK2uNNdQ3ZWm6wmF8B6vIldQ7C-BwuzyVkPbiTo)
|

</details>

<details>
<summary><b>Account settings — Emails &amp; auth</b></summary>

| | Before | After |
|---|---|---|
| Light | ![account-settings-email-auth before
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2M1MTNkZTFkLTY1Y2ItNGJlNi04MWEyLWZiZTJhNDk4MTllNCIsImlhdCI6MTc4MTEzNzkxNSwiZXhwIjoxNzgxNzQyNzE1LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtZW1haWwtYXV0aC1iZWZvcmUtbGlnaHQucG5nIn0.lWxhJCuN0cwTBMnbM1by6-iZoBVHlOi4Pnv4NTCJ67c)
| ![account-settings-email-auth after
light](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQL2I0NzBlOTY3LWY5NTUtNDRkMy1iZDY3LWU1ZDJiZWNkYmQ2MCIsImlhdCI6MTc4MTEzNzkxNiwiZXhwIjoxNzgxNzQyNzE2LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtZW1haWwtYXV0aC1hZnRlci1saWdodC5wbmcifQ.AH2FqUJJRBCFx5a08IkvNzScleqRd_ftlFFjbp_F3no)
|
| Dark | ![account-settings-email-auth before
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzYwYjQ2MmFlLTkzMDItNGQ2MC1iZmNkLWVhNThlN2I2OWI5ZiIsImlhdCI6MTc4MTEzNzkxNiwiZXhwIjoxNzgxNzQyNzE2LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtZW1haWwtYXV0aC1iZWZvcmUtZGFyay5wbmcifQ.PC9ll_6mOHvmd8aAlfL2IfgNBRGOL5K8QuyJoo7NpTs)
| ![account-settings-email-auth after
dark](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzQxMWUzY2M1LWViYWYtNGMzZi1hMDhkLTJlOTk0ZDdjMGU0ZCIsImlhdCI6MTc4MTEzNzkxNiwiZXhwIjoxNzgxNzQyNzE2LCJmaWxlbmFtZSI6ImFjY291bnQtc2V0dGluZ3MtZW1haWwtYXV0aC1hZnRlci1kYXJrLnBuZyJ9.OlTYac0GJEdImBtOMqFXHBG6_JQacZ9F5h3-GB7IKms)
|

</details>

Link to Devin session:
https://app.devin.ai/sessions/1d2380aa55694f2fb12ed96e200a32ad
Requested by: @Developing-Gamer

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-15 04:25:33 +05:30
BilalG1
64eeedce9f
Fix dev CI: docker prune missing template + cross-domain test failures (#1582)
Unbreaks the test workflows that have been red on every dev push since
June 4. All root causes trace to direct pushes whose own CI runs already
failed (`8b78587da`, `c60016226`, `59daf1321`).

> Note: this PR originally also fixed the "Docker Server Build and Push"
workflow (missing `@hexclave/template` in the Dockerfiles' `turbo prune`
scope), but dev picked up the identical fix via 59daf1321 while this was
open, so the Dockerfile changes dropped out after merging dev back in.

## 1. E2E cross-domain spies — broken since `c60016226` (June 4)

`c60016226` renamed `_getCurrentRefreshTokenIdIfSignedIn` →
`_fetchCurrentRefreshTokenIdIfSignedIn` in the SDK and template unit
tests, but missed the eight `vi.spyOn` calls in
`apps/e2e/tests/js/cross-domain-auth.test.ts`. `vi.spyOn` throws on
missing properties → all 8 tests failed with
`_getCurrentRefreshTokenIdIfSignedIn does not exist`.

**Fix:** rename the spies.

## 2. signOut test timeout in all 5 SDK packages — broken since
`c60016226` (June 4)

The refresh-cookie test added in the same commit writes to a cookie
token store, which queues a background trusted-parent-domain lookup.
That lookup fetches the unreachable test `baseUrl` with retries while
holding `storeLock`'s read lock (via `AsyncStore.setAsync`), so the
later signOut test deadlocks on `storeLock.withWriteLock` inside
`_signOut` and hits the 5s vitest timeout (passes in isolation, fails
when the file runs in order).

**Fix:** stub `_getTrustedParentDomain` in the cookie test so the queued
task settles immediately.

## 3. "does not await pending auth resolutions" — premise broken by
`8b78587da` (June 4), masked by the spy rename

`8b78587da` added `nonHostedHandlerNames`, making `afterSignIn` resolve
to a local URL instead of the hosted domain. The test redirected to
`afterSignIn` from a callback page expecting the nested cross-domain
auth params path to run — but the redirect became same-origin, so
`_fetchCurrentRefreshTokenIdIfSignedIn` is (correctly) never called.
This was invisible until fix 1 above unmasked it.

**Fix:** redirect to `accountSettings` (still hosted, so still
cross-origin), preserving the test's intent: the session lookup during
nested-param construction must not await pending auth resolutions.

## 4. internal-metrics e2e snapshots — broken on dev by `59daf1321`

The analytics overview filters PR reshaped the metrics endpoint response
(added `bounce_rate`, daily/hourly breakdown arrays,
`top_browsers`/`devices`/`operating_systems`/`regions`; slimmed the
zero-filled daily fallback arrays) and updated the backend unit-test
snapshots, but left the e2e snapshots stale — its own dev run fails
these two tests identically.

**Fix:** update `__snapshots__/internal-metrics.test.ts.snap`,
reconstructed from the CI diff with every context line verified against
the old snapshot, and the new fields cross-checked against the route
change in 59daf1321.

## Verification

- `client-app-impl.cross-domain.test.ts`: 7/7 in `packages/template` and
the regenerated `packages/js` copy (signOut: 5s timeout → 10ms).
- `tests/js/cross-domain-auth.test.ts`: 18/18 locally (fully mocked, no
backend needed).
- Lint + typecheck pass for the touched packages.
- The metrics snapshot can only be fully confirmed by CI (needs the live
backend).

## Out of scope

"Run setup tests with custom base port" also intermittently fails
unrelated test files at exactly 60s under runner load (last green May
5), and the local-emulator run had one `payments/switch-plans` flake —
pre-existing flakiness not addressed here.
2026-06-11 17:37:11 -07:00
BilalG1
f4c13db079
fix(sdk): stop nested cross-domain auth from restarting the redirect chain on the hosted domain (#1581)
## What users see

Setting up a new project with hosted components, clicking **Sign in**
sometimes throws the browser into a redirect ping-pong between the app
and the hosted components site — anywhere from 5 to 9+ cross-domain
redirects — before the sign-in page finally renders. Reproduced live
against production:

![redirect loop
demo](https://gist.githubusercontent.com/BilalG1/888feed849ef0bc1f73c4609bfd71662/raw/redirect-loop.gif)

Captured redirect chain from that recording (one line per navigation, ~1
per second):

```
localhost:3000/                                      ← click "Sign in"
HOSTED  /handler/sign-in?...nested_refresh_token_id  ← start session handoff
localhost:3000/?redirect_uri=...&state=S1            ← bounce: "prove the session"
HOSTED  /handler/sign-in?...&code=...                ← code delivered... then RESTART ↩
localhost:3000/?redirect_uri=...&state=S2            ← bounce again (fresh state!)
HOSTED  /handler/sign-in?...&code=...                ← code delivered... RESTART ↩
localhost:3000/?redirect_uri=...&state=S3            ← again
HOSTED  /handler/sign-in?...&code=...                ← again
localhost:3000/?redirect_uri=...&state=S4            ← again
HOSTED  /handler/sign-in?...&code=...                ← exchange finally wins the race
HOSTED  /handler/sign-in (clean URL)                 ← sign-in form renders
```

The designed handshake is only 3 cross-domain redirects. Everything past
that is one bug restarting the chain over and over.

## The bug

When a page on the hosted domain loads with a one-time `code`, the
`StackClientApp` constructor schedules **two** async startup flows
back-to-back:

1. `callOAuthCallback` — which **synchronously strips `code` + `state`
from the URL** (`history.replaceState`) before starting its network
token exchange, and
2. `_maybeHandleNestedCrossDomainAuth` — which has a guard for exactly
this situation ("a real OAuth callback wins"), implemented as *"is
`code`+`state` in the URL?"*

Flow 1 runs first. By the time flow 2 reads `window.location`, the
params it's guarding on are already gone — so it concludes no OAuth
callback is happening, sees the (un-stripped) nested handoff marker, and
bounces back to the app domain to request a *new* code, cancelling the
in-flight exchange:

```mermaid
sequenceDiagram
    participant A as Your app (a.com)
    participant B as Hosted sign-in (b.com)
    A->>B: 1. go to sign-in ("I have session X")
    B->>A: 2. "prove it" (state, code_challenge)
    A->>B: 3. one-time code for session X
    Note over B: callOAuthCallback strips code+state from URL,<br/>starts token exchange (network)
    Note over B: nested handler runs next, checks URL for code+state…<br/>already gone → guard defeated 
    B->>A: 2'. "prove it" AGAIN (fresh state) — exchange cancelled
    A->>B: 3'. another one-time code
    Note over B: …loop repeats until the exchange happens to<br/>finish before the re-bounce navigation commits
```

Whether each cycle escapes is a coin flip between two competing
navigations (the exchange's success redirect vs. the handler's
re-bounce), which is why the loop count varies run to run and the issue
reproduces so inconsistently.

## The fix

Capture the URL once, at construction time — before anything can mutate
it — and let the nested handler consult that snapshot in addition to the
live URL:

- The constructor now captures `new URL(window.location.href)` when
scheduling the nested-auth resolution and passes it in.
- `_maybeHandleNestedCrossDomainAuth(urlAtConstructionTime?)` stands
down if **either** the live URL **or** the construction-time URL carries
`code` + `state`.

A stripped callback still counts as a callback, so the handler no longer
re-bounces while the exchange is in flight. Every other path is
unchanged: the handler still reads all of its working params from the
live URL (the strip never touches the nested params), hop-1/hop-2 pages
have no `code` in either snapshot, and ordinary social-login callbacks
never had this race (the component-driven flow strips long after the
handler has run).

Note this fix removes the *restarts*. The remaining 3-redirect baseline
for signed-out users is a separate design issue (the analytics-created
anonymous session triggering the handoff at all) and is intentionally
out of scope here.

## Tests

- New: `does not re-bounce nested cross-domain auth after the OAuth
callback consumed code+state from the URL` — pins both guards
(mutation-tested: reverting either fix line fails it).
- New: `passes the construction-time URL to the nested cross-domain auth
handler` — pins the eager capture; fails if the URL is read lazily at
handler run time.
- Full cross-domain suite passes (the `signOut` timeout in that file is
a pre-existing flake on `dev`, reproducible without this change).


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes a race in nested cross-domain auth that caused repeated redirects
between the app and the hosted sign-in. We now snapshot the URL at
construction so OAuth callbacks are respected even after `code` and
`state` are stripped.

- **Bug Fixes**
- Capture `window.location` at construction and pass it to
`_maybeHandleNestedCrossDomainAuth`.
- Handler stands down if `code` and `state` exist in the live or
captured URL.
- Stops the redirect ping‑pong; the 3‑redirect baseline remains
unchanged.
- Keeps reading nested params from the live URL; no other paths changed.
  - Adds tests to pin the race and the construction‑time URL behavior.

<sup>Written for commit f312baa54c.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1581?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved cross-domain OAuth authentication handling to prevent
unnecessary redirects after OAuth callback processing.

* **Tests**
* Added test coverage for nested cross-domain OAuth authentication
scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-11 17:37:05 -07:00
Mantra
74c888fed7
chore(mcp/docs): canonicalize HEXCLAVE_ env vars in docs + raise ask_hexclave step limit & timeout (#1571)
## Summary

Follow-up from analyzing the dogfooding report on the `ask_hexclave` MCP
tool. Two root causes were confirmed against source:

1. **The "`STACK_` vs `HEXCLAVE_` env var hallucination" wasn't a
hallucination** — it's an incomplete Stack Auth → Hexclave rebrand. The
SDK resolves both prefixes (`packages/js/src/generated/env.ts`), with
`HEXCLAVE_*` canonical and `STACK_*` a legacy fallback, but several
docs/examples still showed the old `STACK_*` names. That inconsistency
is what misled agents into thinking `HEXCLAVE_*` was made up.
2. **`ask_hexclave` timeouts** — the tool proxies to a `quality:
"smart"` agentic docs-search loop. The agent step budget (50) and the
120s timeouts were too tight; broad/multi-part questions blew the budget
(reproduced 3× while investigating).

## Changes

### Docs: canonicalize client SDK auth env vars to `HEXCLAVE_*`
Converted `PROJECT_ID`, `PUBLISHABLE_CLIENT_KEY`, `SECRET_SERVER_KEY`,
`API_URL` (+ `NEXT_PUBLIC_` / `VITE_` forms) from `STACK_*` →
`HEXCLAVE_*` in app-setup docs + the package template:

-
`docs-mintlify/guides/integrations/{convex,tanstack-start,vercel}/overview.mdx`
- `docs-mintlify/guides/going-further/local-vs-cloud-dashboard.mdx`
- `docs-mintlify/guides/apps/analytics/overview.mdx`
- `docs-mintlify/guides/other/tutorials/ship-production-ready-auth.mdx`
- `docs-mintlify/sdk/objects/hexclave-app.mdx`
- `packages/template/src/integrations/convex/component/README.md` (the
tracked source of the generated `@hexclave/js` + `@hexclave/next` copies
— the generated copies are git-ignored)

**Deliberately left untouched** — read literally by the backend/CLI (no
`HEXCLAVE_` alias) or user-defined: `STACK_CLICKHOUSE_*`,
`STACK_DATABASE_*`, `STACK_OPENROUTER_*`, `STACK_CLI_*`, `STACK_SEED_*`,
`STACK_WEBHOOK_SECRET`, `STACK_DATA_VAULT_SECRET`, and the `x-stack-*`
HTTP headers. So `self-host.mdx`, `cli.mdx`, `jwts.mdx`, `webhooks`, and
`data-vault` docs are intentionally unchanged.

### Reliability: raise `ask_hexclave` step limit + timeout
- `apps/backend/src/app/api/latest/ai/query/[mode]/route.ts`:
docs/search agent step limit **50 → 75** (+50%); AI generation abort
**120s → 180s**
- `apps/mcp/src/mcp-handler.ts`: MCP function `maxDuration` **120 →
180** (kept ≥ backend timeout so the proxy doesn't die before the
backend finishes)

## Notes
- Also includes a small pre-existing `run pnpm fml` commit (regenerated
docs snippets / `llms-full.txt`).
- The step/timeout bumps address the *symptom*. The durable reliability
fix is streaming/keepalive on the MCP proxy so the client never idles
out mid-query — proposed as a follow-up.
- **Not** included: the separate `sendEmail` doc-vs-SDK drift (docs
declare `Promise<Result<void, KnownErrors>>` in
`sdk/objects/hexclave-app.mdx`, but the SDK returns `Promise<void>` and
throws). That's a docs *correctness* bug deserving its own PR.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Canonicalized auth env vars in docs/templates to `HEXCLAVE_*`, raised
docs/search step limits and timeouts, and clarified `HexclaveApp`
defaults. MCP tool and server instructions now require loading the
`skill` resource before queries.

- **Bug Fixes**
- Docs: Use `HEXCLAVE_PROJECT_ID`, `HEXCLAVE_PUBLISHABLE_CLIENT_KEY`,
`HEXCLAVE_SECRET_SERVER_KEY`, and optional `HEXCLAVE_API_URL` across
guides/templates (Vercel, Convex, TanStack Start, analytics). In SDK
docs, `secretServerKey` defaults to `HEXCLAVE_SECRET_SERVER_KEY`, and
client defaults use `NEXT_PUBLIC_HEXCLAVE_*`. Backend-only `STACK_*`
vars (`STACK_CLICKHOUSE_*`, `STACK_DATABASE_*`, `STACK_OPENROUTER_*`,
CLI/data-vault/webhook headers) unchanged.
- Reliability: Increase docs/search step limit 50→75 and timeouts
120s→180s; set MCP `maxDuration` to 180s; use `performance.now()` for
duration logging. MCP instructions updated to require loading the
`skill` resource before using tools.

<sup>Written for commit f6be2c3162.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1571?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Performance & Reliability**
* Increased AI operation timeouts and step limits for certain prompts;
improved generate-mode duration measurement for more accurate logging.
* **Documentation**
* Replaced Stack-branded environment variable names with Hexclave
equivalents across guides and examples.
* Clarified that hexclave dev injects required environment variables
automatically.
  * Added guidance on configuring custom authentication redirect URLs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-11 10:53:53 -07:00
Mantra
59daf1321c
[codex] Add analytics overview filters (#1496)
## Summary

Adds richer analytics overview metrics and filterable dashboard
breakdowns.

- adds hourly overview series for the 1-day range
- adds country, referrer, browser, OS, and device filters to internal
metrics
- adds bounce rate, session duration, top countries, top browsers, top
operating systems, and device breakdowns
- updates the overview dashboard with filter chips, top-list cards,
animated metric states, and 1-day hourly chart support
- captures user agent on page-view analytics events, with a server-side
fallback for older clients

## Validation

Attempted targeted tests:

`pnpm test run
apps/backend/src/app/api/latest/internal/metrics/route.test.ts
'apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.test.ts'`

This did not reach Vitest in the temporary split worktree because
`node_modules` is not installed there and the repo pre-step failed at
`pnpm exec tsx ./scripts/generate-sdks.ts`.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds analytics overview filters with optional date‑range bounds and
1‑day hourly charts, plus smoother, accessible animations across charts
and top lists. Improves correctness and stability with deterministic
caching, normalized inputs, client‑only user‑agent capture, and
globe/layout fixes.

- **New Features**
- Filterable analytics overview (country, referrer, browser, OS, device)
with normalized inputs and optional `since`/`until`; API/admin/dashboard
accept `AnalyticsOverviewFilters` with deterministic cache keys.
- 1‑day hourly charts (page views, visitors) and a metric mode toggle
(DAU, Visitors, Revenue); animated top‑lists and sparklines powered by
`motion` with reduced‑motion support.
- UI: filter chips/menu, clearer tooltips (incl. user metric cards),
optional interactive globe with dynamic camera distance; exported
`TooltipPortal` from `@hexclave/ui`.

- **Refactors & Bug Fixes**
- Event ingest: client sends `user_agent`; removed server‑side fallback;
added user‑agent filter‑fragment builder and tests.
- Metrics correctness: aligned hourly bounds to start of UTC hour;
derived 1‑day revenue total from daily series; resilient chart x‑axis
formatting; country filter options use analytics `top_regions`;
fixed‑'en' locale for top‑lists; added date‑range parsing/validation for
filters.
- UI/runtime: smoother pill/tab slider animations with guards for
missing Web APIs; added `containedHeight` to `PageLayout` and wired into
sidebar/session replays; globe disables zoom when non‑interactive.
- Misc: instrumentation runs only in Node (`process.env.NEXT_RUNTIME ===
"nodejs"`); analytics/overview page redirects with URL‑encoded
`projectId`; Docker: include `@hexclave/template` in `turbo prune` to
fix CI builds.

<sup>Written for commit 7fcd3558a5.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1496?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Analytics filters (country, referrer, browser, OS, device); hourly
signup and active-user series; expanded hourly/daily analytics payloads
and top-lists UI.
* Chart metric modes (DAU, Visitors, Revenue), optional page-views
series, interactive globe support, animated Top Lists, and sparkline
animations.

* **Improvements**
* Better user-agent capture/normalization for batched events and
page-view tracking; reduced-motion aware animations; enhanced tooltips
and UI slider/tab indicators.
  * Added motion library dependency.

* **Tests**
  * New unit tests for analytics filters and chart metric mode behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mantra <mantra@stack-auth.com>
2026-06-10 17:50:35 -07:00
BilalG1
7f99f15b42
fix(rde): graceful config load errors + lightweight /config import path (#1557)
## Problem

A user hit `Failed to register development environment session (500)`
when running the RDE (`hexclave dev` / `stack dev`). Removing
`defineStackConfig` from their `stack.config.ts` made it go away.

**Root cause:** the local dashboard evaluates the project's config file
in a plain Node context via `jiti`
([config-file.ts](apps/dashboard/src/lib/remote-development-environment/config-file.ts)).
When the config imports a *value* (e.g. `defineStackConfig`) from a
framework package like `@stackframe/stack` / `@hexclave/next`, jiti
executes the entire SDK — React, `server-only`, Next internals — which
throws in that context. The exception propagated as a bare 500. Dropping
`defineStackConfig` removed the value import, so jiti no longer loaded
the framework.

## Changes

**1. Graceful error (Fix 3)**
`readConfigFile` now wraps the `jiti.import` in try/catch and rethrows a
message pointing at the lightweight import path, instead of a raw 500.

**2. Lightweight `/config` subpath (Fix 1)**
Added a side-effect-free `./config` entrypoint to the framework packages
— `@hexclave/{js,next,react,tanstack-start}/config` — that re-exports
`defineHexclaveConfig` / `defineStackConfig` + the `HexclaveConfig` type
from `@hexclave/shared/config`, with **no framework runtime**. Source of
truth:
[`packages/template/src/config.ts`](packages/template/src/config.ts) +
the export in
[`package-template.json`](packages/template/package-template.json),
propagated to the generated packages via `generate-sdks`.

> Why per-package and not `@hexclave/shared/config`: `@hexclave/shared`
is only a *transitive* dependency from a user's perspective, so
importing from it fails under pnpm strict mode. Users depend on the
framework package directly, so `@hexclave/next/config` always resolves.
This was confirmed empirically — the previous tests that imported
`@hexclave/shared/config` were red.

**3. Docs / prompts / renderer aligned to the new path**
-
[`ai-setup-prompt.ts`](packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts)
+ regenerated `docs-mintlify` (setup.mdx, llms-full.txt, snippets).
- Hand-written
[`hexclave-config.mdx`](docs-mintlify/guides/going-further/hexclave-config.mdx)
and
[`local-vs-cloud-dashboard.mdx`](docs-mintlify/guides/going-further/local-vs-cloud-dashboard.mdx).
(`docs/**` left untouched — legacy.)
- `renderConfigFileContent` (the config file the dashboard/CLI
auto-writes) now emits `import type { HexclaveConfig } from
"<pkg>/config"`. Legacy `@stackframe/*` packages predate the subpath, so
they keep their root import (guarded).

## Behavioral note

Existing config files that import from a package root get their import
line upgraded to `/config` on their next dashboard/CLI sync — a
one-time, harmless rewrite that migrates them onto the safe path. The
github-config-push idempotence test was updated to use the current
`/config` format so it still genuinely verifies "no spurious commit."

## Testing

- 43 unit tests pass across `config-file`, `github-config-push`,
`config-rendering`, `config-authoring`, `local-emulator`. The two
previously-red RDE `define*` tests now pass through jiti via
`@hexclave/next/config` (the real code path), and were made
resolution-stable by rooting their temp dir at the test file instead of
`process.cwd()`.
- Typecheck green on all source-changed packages (shared, cli, js, next,
react, tanstack-start). Lint clean.
- ⚠️ The two e2e suites (`cli.test.ts`, `config-local-emulator.test.ts`)
need backend+DB infra; their snapshot updates are mechanical and
**confirmable only in CI**.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Prevents 500s when loading `hexclave.config.ts` by adding a lightweight
`<pkg>/config` entrypoint and showing a clear, actionable error without
leaking framework stacks. Import detection, rendering, CLI, tests, and
docs now default to `/config` (including `@hexclave/tanstack-start`) so
configs load in plain Node contexts.

- **New Features**
- Added `/config` subpaths in `@hexclave/js`, `@hexclave/next`,
`@hexclave/react`, `@hexclave/tanstack-start` (and template)
re-exporting `defineHexclaveConfig`, `defineStackConfig`, and
`HexclaveConfig` with no framework runtime.
- Renderer, CLI, and docs import `HexclaveConfig` from `<pkg>/config`;
legacy `@stackframe/*` keep root imports. Existing config files
auto-upgrade on next dashboard/CLI sync.

- **Bug Fixes**
- Wrapped `jiti` config load with try/catch; capture raw error for
diagnostics and show a concise message pointing to `<pkg>/config` (no
nested framework stack traces).
- Import detection accepts optional `/config` suffix; renderer always
appends `/config` for Hexclave packages and recognizes
`@hexclave/tanstack-start`.
- Tests stabilized by scoping temp dirs to the test file; CLI error
example now references `HexclaveConfig` from `<pkg>/config` for Hexclave
packages.

<sup>Written for commit dfe7d5fee4.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1557?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added lightweight "/config" subpath exports across SDK packages to
enable side-effect-free config authoring in plain Node contexts.

* **Documentation**
* Updated guides and snippets to recommend importing config types and
helpers from the "/config" entrypoint and added example usage.

* **Bug Fixes**
* Improved error messaging when dynamic config imports fail, with
guidance to use the "/config" entrypoint.

* **Tests**
* Adjusted tests and snapshots to expect normalized "/config" import
paths.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-10 11:40:19 -07:00
BilalG1
76fc62e98b
fix(rde): stop the RDE dashboard blanking on every access-token refresh (#1566)
## Problem

In the Remote Development Environment (`hexclave dev`), the dashboard
suspends and goes **blank for a moment every ~30–60 seconds**, then
repopulates. It never happens on a plain `pnpm dev` dashboard.

## Root cause

The RDE auth gate
([`remote-development-environment-auth-gate.tsx`](../blob/dev/apps/dashboard/src/app/remote-development-environment-auth-gate.tsx))
keeps the browser signed in by re-installing a freshly minted
**access-token-only** session (`signInWithTokens({ accessToken,
refreshToken: "" })`) on a timer (capped by
`RDE_ACCESS_TOKEN_MAX_AGE_MS`).

`InternalSession.calculateSessionKey` keyed access-only sessions by the
**access-token string**:

```ts
} else if (ofTokens.accessToken) {
  return `access-${ofTokens.accessToken}`;   // 👈 changes on every refresh
}
```

So each refresh = a new key = a brand-new `InternalSession` object.
Every session-scoped cache (`useUser` / `useConfig` / `useTeams` /
`useOwnedProjects`, via `createCacheBySession`) is keyed by the
**session object**, so a new object means a cold cache → pending promise
→ `React.use()` suspends → the whole tree falls back to its (empty)
Suspense boundary.

It only *shows* in RDE because the backend is remote: the post-swap
refetch has real network latency, so the blank is visible for hundreds
of ms. On localhost the same swap is a sub-frame flicker. (A background
`refresh()`/write-only is stale-preserving and does **not** suspend —
only a new session dependency does.)

## Fix

Two changes in the SDK source (`packages/shared` + `packages/template`;
the `js`/`next`/`react`/`tanstack-start` copies are generated):

1. **Stable key for access-only sessions.** Key by the token's
`refresh_token_id` (decoded from the JWT) instead of the raw token
string. Every access token minted for the same session shares that id,
so the session identity — and therefore every cache — stays stable
across refreshes. Falls back to the raw token if the JWT can't be
decoded. Refresh-keyed and not-logged-in paths are untouched.

2. **In-place token update.** New
`InternalSession.updateAccessToken(token)`, called from
`_signInToAccountWithTokens`, pushes the fresh token into the reused
session object instead of constructing a new one (no-op when the session
is invalid / the token is unchanged / null).

Net effect: re-minting the RDE access token reuses the same
`InternalSession`, caches stay warm, nothing suspends, no blank.

## Why this is safe

- Session reuse is scoped **per token store** —
`_sessionsByTokenStoreAndSessionKey` is a `WeakMap` keyed by the
token-store object first, then the session key — so server-side
per-request sessions remain isolated regardless of how coarse the key
is. No cross-user/cross-session mixup.
- `refresh_token_id` is a per-session UUID; the coarser key only merges
*the same session's* successive access tokens, which is the intent.
- This keying convention matches the existing `refresh-${refreshToken}`
path (key by the unique token value).

## Testing

- **Reproduced live** via a temporary harness driven through a real
browser: forcing the session-identity swap triggered the exact
cold-cache refetch storm (`useUser`/`useTeams`/`useOwnedProjects`
refetching) that produces the blank.
- **Unit-verified** the new behavior against the built `shared` dist:
two access tokens sharing a `refresh_token_id` → same session key;
different ids → different keys; opaque token → fallback; refresh-keyed
unchanged; `updateAccessToken` swaps the token in place and is a correct
no-op when invalid/unchanged/null.
- **Typecheck + lint clean** across `shared`, `template`, `next`,
`react`, `tanstack-start`, and the dashboard.
- Reviewed by independent passes for correctness, security/blast-radius,
and simplification — no actionable findings.

## How to verify in the real RDE path

Set `RDE_ACCESS_TOKEN_MAX_AGE_MS` to e.g. `5000` in the auth gate and
run `hexclave dev`: before this change the dashboard blanks every few
seconds; after, it stays populated.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Stops the RDE dashboard from going blank every 30–60s by keeping session
identity stable during access‑token refreshes. Session-scoped caches
stay warm, so the UI no longer suspends.

- **Bug Fixes**
- Key access-only sessions by the JWT `refresh_token_id` (stable across
re‑mints); fall back to the raw token if undecodable. Implemented in
`packages/shared`; tests in `packages/shared/src/sessions.test.ts`.
- Add `InternalSession.updateAccessToken()` and use it in
`_signInToAccountWithTokens` to update the token in place only when the
incoming pair resolves to the same `sessionKey` (rejects
foreign/null/unchanged/undecodable; covers access‑only and
refresh‑backed sessions). Prefetch the current user via
`runAsynchronously` in write‑only mode. Implemented in `packages/shared`
and `packages/template`.

<sup>Written for commit fdaf2f28be.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1566?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Improved session stability and token management to enhance
authentication reliability.
* Strengthened session validation to prevent stale token-related issues.

* **Tests**
* Added comprehensive test coverage for session and token handling
mechanisms.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-10 11:13:33 -07:00
Konstantin Wohlwend
dbb397dcbc Home URL should be non-hosted 2026-06-08 18:23:57 -07:00
Konstantin Wohlwend
10f8348b51 Fix Vite environment variables 2026-06-05 16:42:11 -07:00
Konsti Wohlwend
0c07b4b44e Show browser alert for missing Hexclave project ID 2026-06-05 12:29:12 -07:00