## Summary
In `UserDialog`, duplicate-email errors (`UserWithEmailAlreadyExists`,
`ContactChannelAlreadyUsedForAuthBySomeoneElse`) were shown via
`window.alert()`. Replaced with a controlled `DesignDialog` using
`WarningCircleIcon`, so the error appears as a styled modal instead of a
native browser popup.
Link to Devin session:
https://app.devin.ai/sessions/99d73091c47a4a58b33d8724df5a7ce8
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Replaced `window.alert()` in the user create/edit flow with a controlled
`DesignDialog` for duplicate-email errors (`UserWithEmailAlreadyExists`,
`ContactChannelAlreadyUsedForAuthBySomeoneElse`). Uses
`WarningCircleIcon` with an OK action, preserves form data via
`prevent-close-and-prevent-reset`, clears dialog state when the form
closes, and fixes a max-statements-per-line lint warning.
<sup>Written for commit 274fcb8e9b.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1680?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. -->
---------
Co-authored-by: vedanta.gawande <vedanta.gawande@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Armaan Jain <84474476+Developing-Gamer@users.noreply.github.com>
- Changed the GitHub owner from "mantrakp04" to "hexcalve" in the seedDummyProject function to reflect the correct repository ownership.
This update ensures that the dummy data seeding process points to the appropriate GitHub account.
- Refactored the onboarding logic to differentiate between local emulator and development environments for OAuth provider configuration.
- Introduced a new mock for public environment variables in tests to enhance test coverage.
- Added a test case to ensure the onboarding process correctly waits for user actions before applying configuration updates.
These changes improve the onboarding experience and ensure proper handling of OAuth settings based on the environment.
## Summary
Two changes to the user data export dialog on the project Users page:
1. The export scope now defaults to **"Export only filtered/searched
users"** instead of "all users".
2. The all-users option label is now **"Export all users in the project
(includes Anonymous)"**.
To keep this scoped to the Users table (the shared export dialog is
reused by other tables), the dialog's default scope is made configurable
rather than changed globally:
- `DataGridExportOptions` gains `defaultScope?: DataGridExportScope`
(defaults to `"all"`).
- The dialog initializes `useState(exportOptions?.defaultScope ??
"all")`.
- `user-table.tsx` passes `defaultScope: "filtered"` and the updated
`allScopeLabel`.
Other tables (teams, transactions, emails) are unaffected — they keep
the `"all"` default.
Link to Devin session:
https://app.devin.ai/sessions/4996678b2b944090b6eef2f64f0a62a1
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Default the Users export dialog to filtered scope and clarify that the
"all users" option includes Anonymous; scope resets to the per-table
default only when the dialog reopens, and other tables keep "all".
- **New Features**
- Added `defaultScope` to export options and initialized scope from it;
Users table sets `defaultScope: "filtered"` and updates the all-users
label.
- Reset scope to `defaultScope` only on a closed→open transition to
avoid changing it while the dialog is open.
- **Bug Fixes**
- Stubbed `NODE_ENV` via `vi.stubEnv` in
`apps/backend/src/oauth/ssrf-protection.test.ts` to fix lint and prevent
env mutation.
<sup>Written for commit 3aa670b6d2.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1679?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. -->
---------
Co-authored-by: vedanta.gawande <vedanta.gawande@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Adds .uuid() to the yupString() validator for run_id in all three config
agent routes (cancel, commit, run). Without this, a non-UUID string
reaches lockConfigAgentRun's ::uuid SQL cast and causes a PostgreSQL
'invalid input syntax for type uuid' exception — surfacing as a 500
instead of a clean 400.
Co-Authored-By: mantra <mantra@stack-auth.com>
- config-eval test: assert a malformed file throws a loader error, NOT a
ConfigFileEvalError, so the documented "Failed to load config file" routing
is actually protected (cubic)
- progress-content: guard useElapsedSeconds against the startedAt=0 "not started"
sentinel so the counter shows 0 instead of ~epoch-since-1970 on first paint (vercel)
- config index: clear a cancelled run's captured diff so it can't linger in the
API shape or be replayed by the commit route (greptile observation)
- index/commit route: gate commit_hash advance on committedRef identity so a
mid-run repo re-link can't stamp a foreign commit SHA (cross-repo TOCTOU)
- github-push-dialog: cancel handler now settles the dialog itself instead of
relying on a poll loop that has already exited at awaiting_review
- progress-content: useElapsedSeconds reacts to startedAt changes (fresh anchor)
so a post-mount start time no longer freezes a stale offset
- schema-fields: configAgentRunSchema.id uses .uuid() to match the @db.Uuid column
- tests: cover the SyntaxError config-eval path and the re-link commit-hash case
- Updated comments in the `applyConfigUpdate` function to enhance clarity regarding the handling of Git diffs and token management.
- Removed redundant token redaction logic, ensuring the diff captures the authoritative commit source without alteration.
These changes aim to improve code readability and maintainability in the configuration update process.
- Added a new `ConfigAgentRun` model to track the state of configuration agent runs in the database.
- Updated the Prisma schema to include new fields for the `ConfigAgentRun` model, allowing for detailed tracking of run status, timestamps, and associated metadata.
- Introduced new API routes for starting, cancelling, and committing configuration agent runs, improving user interaction and feedback during updates.
- Updated existing routes to utilize the new `run_id` for better tracking and management of agent runs.
- Added a new dependency `diff` to facilitate change tracking in configuration files.
These changes aim to improve the overall functionality and user experience of the configuration agent integration with GitHub.
## What
Custom emails / templates / drafts sent through Hexclave's **shared
(development) email server** are no longer blocked with
`RequiresCustomEmailServer`. They are now allowed, but their **subject
and body are wrapped** at send time with a notice that this is a
development email from Hexclave, so unexpected recipients know they can
safely ignore it.
The wrapper only applies to **project-defined content addressed to the
project's own users**. Hexclave's own default-template emails
(verification, password reset, magic link, etc.) and system
notifications (credential-scanning alerts, internal feedback) are sent
**verbatim**.
## How
-
**[send-email/route.tsx](apps/backend/src/app/api/latest/emails/send-email/route.tsx)**
— removed the `RequiresCustomEmailServer` throw that blocked the shared
server.
- **[emails.tsx](apps/backend/src/lib/emails.tsx)** — added
`wrapSharedDevEmail()` (prefixes the subject with `[Hexclave dev email]`
and prepends a notice banner to HTML/text) and
`isCustomEmailForSharedServer(recipient, createdWith, templateId)`.
- **[email-queue-step.tsx](apps/backend/src/lib/email-queue-step.tsx)**
— applies the wrapper at send time, gated on `emailConfig.type ===
"shared"` **and** the email being project-defined custom content.
Applying it at send time reliably wraps both the subject (from
`overrideSubject` or the template's `<Subject>`) and the rendered HTML.
### What counts as "wrap-eligible"
`isCustomEmailForSharedServer` returns true only when **all** hold:
1. the email is addressed to one of the project's own users (recipient
type is not `custom-emails`), **and**
2. it is a draft, a custom template, or raw HTML — i.e. **not** one of
the built-in `DEFAULT_TEMPLATE_IDS`.
Condition (1) exempts Hexclave's own system senders (credential-scanning
revoke, internal feedback) which send raw HTML to bare addresses via
`custom-emails` and would otherwise be mis-classified as project
content. This was a bug caught in review — a leaked-API-key security
alert to a shared-server customer would have been prefixed `[Hexclave
dev email]` with a "you can safely ignore it" banner. The recipient type
is already persisted on the outbox row, so no schema change was needed.
## Tests
- **send-email.test.ts** — replaced the old "400 on shared config" test
with two new tests: (a) a custom email on the shared server is delivered
with the `[Hexclave dev email]` subject prefix + notice banner, and (b)
a **default template** (`sign_in_invitation`) on the shared server is
delivered **verbatim** (no prefix, no banner) — pinning the core safety
contract.
- **js/email.test.ts** — flipped the "throws RequiresCustomEmailServer"
test to assert the send now resolves.
Verified locally against a full stack:
- ✅ `send-email.test.ts` — 18/18
- ✅ `js/email.test.ts` — 12/12
- ✅ `password/send-reset-code.test.ts` — passes (default templates on
shared server stay unwrapped)
## Known limitations (intentional scope)
- **Template CRUD still blocked on the shared server.**
`internal/email-templates` routes still throw
`RequiresCustomEmailServer`, so a shared-server project can send raw
HTML / a default template via the API but cannot create or edit a
*saved* custom template. Sending arbitrary HTML is unaffected; only the
saved-template editor remains gated.
- **A project can send a (project-edited) default template unwrapped**
by calling `send-email` with a `template_id` equal to a built-in
`DEFAULT_TEMPLATE_IDS` value. Low impact (requires a server key, limited
upside), noted for awareness.
## Note: freestyle-mock fix included
[freestyle-mock/Dockerfile](docker/dependencies/freestyle-mock/Dockerfile)
now also accepts `/execute/v3/script`. The `freestyle` SDK bump in #1654
moved to `/v3`, but the mock only served `/v1`+`/v2`, so **all** local
email rendering 404'd (pre-existing `dev` breakage, not from this
feature). The v3 request/response is identical to v2. Happy to split
this into its own PR if preferred.
Out of scope: `emails/email-queue.test.ts` has 2 pre-existing snapshot
failures (`margin:0` vs recorded `margin:0rem`, a
`@react-email/components` version drift in the mock) — those tests use a
custom email server, so this PR's shared-only code path never runs for
them.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Email sending can now proceed when using a shared email server.
* Development-style wrapping is applied to eligible shared-server custom
email content, including HTML notice injection.
* **Bug Fixes**
* Removed the previous blocking “requires custom email server” behavior
for shared-server configurations.
* Default-template emails over the shared server are no longer wrapped.
* **Tests**
* Updated end-to-end and JS email tests to validate both wrapped
custom-email behavior and unwrapped default-template behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Removed outdated comments from `.env.development`, `.eslintrc.cjs`, and `schema.prisma` for clarity.
- Cleaned up import statements in `local-emulator.ts` and `repo-agent.ts` to improve code organization.
- Adjusted import order in `ssrf-protection.ts` and `cli.test.ts` for consistency.
- Updated `init.ts` to streamline imports and enhance readability.
- Minor adjustments in `admin-interface.ts` and `schema-fields.ts` to maintain code quality.
These changes aim to enhance maintainability and readability across the codebase.
- Added support for a new shared backend package in the pnpm workspace.
- Updated the Prisma schema to include a new field for tracking the latest config agent run state.
- Refactored config agent scripts for improved clarity and functionality, including renaming the build image script.
- Removed obsolete scripts related to linking projects to GitHub and seeding config tests.
- Introduced a new API route to retrieve the state of the most recent config agent run, enhancing user feedback during updates.
Co-Authored-By: mantra <mantra@stack-auth.com>
## Problem
In **preview mode**, clicking the user profile → **Account settings**
logs the user out and strands them on a sign-in page they can't get
past.
Root cause is a combination of three things:
- Preview mode uses an **in-memory** token store (`tokenStore:
"memory"`), so any full page reload wipes the session.
- The account-settings menu item calls
`app.redirectToAccountSettings()`, whose `redirectTo*` helpers fall
through to `window.location.assign` on the client — a **hard reload**.
- The `/handler/account-settings` route lives **outside** the
`(protected)` layout that performs the preview auto-login, so nothing
re-mints the preview session after the reload.
Net effect: hard nav → in-memory session wiped → `useUser({ or:
'redirect' })` finds no user → redirect to sign-in, with no way back in
(preview credentials are a throwaway UUID).
In normal (cookie) mode this is harmless because the cookie survives the
reload — the bug is preview-specific.
## Fix
Soft-navigate from the menu item using `app.useNavigate()` (a
`router.push` wrapper under the `"nextjs"` redirect method) instead of
`redirectToAccountSettings()`. A soft client-side navigation does not
re-evaluate the JS bundle, so the module-singleton app instance and its
in-memory token store stay alive.
The fix is scoped to the single dashboard call site rather than the
shared SDK `redirectToAccountSettings()` method, which ships to all
customers and intentionally hard-navigates for cross-domain / sign-out
flows.
## Note / follow-up
This covers navigation **from within the app**, which is the only in-app
entry point to account settings. It does **not** cover a hard refresh or
a direct link to `/handler/account-settings` in preview mode — those
still wipe the in-memory session because the handler route has no
auto-login. A fuller root-cause fix would bring the preview auto-login
(or the `(protected)` layout) to the handler route.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes preview-mode logout when opening Account settings by switching the
dashboard menu to a soft client-side nav using `app.useNavigate()`
instead of a hard reload. This preserves the in-memory session.
- **Bug Fixes**
- Soft-navigate to `/handler/account-settings` via `app.useNavigate()`
to avoid wiping the preview token.
- Change scoped to the dashboard menu; `redirectToAccountSettings()` in
the SDK remains unchanged.
<sup>Written for commit 0e0c3a1bab.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1670?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**
* The “Account settings” menu now opens with smooth in-app navigation
instead of a full page reload, making the experience faster and more
seamless.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Introduced a new API route for committing changes after user review, allowing the agent to keep the sandbox alive for inspection before finalizing updates.
- Enhanced the existing applyConfigUpdate function to transition to an awaiting review state, storing the diff for user visibility.
- Added progress tracking and stage reporting for the config agent run, improving user feedback during the update process.
- Updated the dashboard to reflect the new review stages and provide a more interactive experience for managing configuration changes.
Co-Authored-By: mantra <mantra@stack-auth.com>
P0: Strip OAuth token from git origin after clone so LLM agent
never sees credentials (repo-agent.tsx)
P1: Replace raw error.message with safe hardcoded text in API
response and dashboard UI (apply/route.tsx, config-update.tsx)
P1: E2E spike script now requires explicit env vars instead of
falling back to pushing to main (spike-orchestrator-e2e.mts)
P2: Use urlSchema for commit_url (schema-fields.ts)
P2: Return commitSha directly instead of parsing from URL
(repo-agent.tsx, apply/route.tsx)
P2: Support LINK_BRANCH_ID env var (link-project-to-github.ts)
P2: Widen structural fallback regex (config-updater.ts)
P2: Log warning when cancel has no sandboxId (cancel/route.tsx)
P2: Reject arbitrary string config values (config-eval.ts)
Co-Authored-By: mantra <mantra@stack-auth.com>
## 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