Fixes four recurring production Sentry issues (triaged from the last 24h
of `vercel-production`):
## X OAuth: 403 client-not-enrolled → 400 (STACK-BACKEND-1JE)
When a customer's X developer App isn't attached to an X API Project, X
deterministically rejects `GET /2/users/me` with 403
`client-not-enrolled` even though the token exchange succeeds. This
previously surfaced as a `HexclaveAssertionError` (opaque 500 + Sentry
alert) on every sign-in attempt for the misconfigured project. It's now
a 400 `StatusError` telling the developer to attach their App to a
Project, mirroring the existing `invalid_client` handling in
`providers/base.tsx`. Other non-OK statuses still fail loud.
## Stripe: ignore `refund.updated` (STACK-BACKEND-1D9)
Stripe emits `refund.updated` asynchronously when the card network fills
in `destination_details` on refunds we created — and already ledgered —
synchronously in the internal refund route, so it carries no actionable
information. Added it to `ignoredEvents` (same approach as #1667/#1461).
`refund.failed` deliberately stays fail-loud, since a refund failing
after it's ledgered is actionable.
## external-db-sync: fix enqueue deadlocks (STACK-BACKEND-16P)
`enqueueExternalDbSyncBatch` batch-inserts deduplication keys with `ON
CONFLICT DO NOTHING`; two concurrent batches inserting overlapping keys
in different orders could deadlock (Postgres 40P01, surfaced as an
empty-titled `PrismaClientKnownRequestError`). Worse than the noise: the
flag-clearing UPDATE and the enqueue run in separate transactions, so a
deadlocked enqueue silently lost that batch's sync trigger until the
next change for the tenancy. Fixed by deduplicating and inserting keys
in canonical (sorted) order, so the circular wait between two batches
can't form. Deliberately minimal: deadlocks against other
`OutgoingRequest` writers (e.g. `recoverStaleOutgoingRequests`) are
theoretically still possible but not evidenced in the Sentry events; if
40P01 reappears, a retry can be added then.
## Password reset: 404 instead of 500 for deleted users
(STACK-BACKEND-1JD)
Password reset codes embed the `user_id` at issuance time, so a code can
be redeemed after the user was deleted (observed in prod from a scripted
client replaying a reset code). The `usersCrudHandlers.adminUpdate` call
didn't declare `UserNotFound` via `allowedErrorTypes`, so the CRUD
handler wrapped it in `CrudHandlerInvocationError` and the endpoint
returned an internal 500. Now declares it and lets the client-safe
`USER_NOT_FOUND` KnownError propagate as a 404, matching the ~10 sibling
call sites that already do this.
## Testing
- New e2e test: resetting the password of a deleted user returns 404
`USER_NOT_FOUND` (`auth/password/reset.test.ts`) — verified passing
locally against the full stack
- Backend + e2e typecheck and lint pass
- No new tests for the other three: the ignore-list entry is covered by
`satisfies Stripe.Event.Type[]` and the existing unknown-type webhook
tests; the X fix would need the X API base URL to be injectable plus a
mock user-info endpoint (possible follow-up); the deadlock is a Postgres
timing race that can't be deterministically reproduced through the e2e
API surface
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved password reset handling when the account no longer exists.
* Added clearer guidance for X API configuration errors.
* Prevented unsupported Stripe refund update events from triggering
processing.
* Ensured external database synchronization requests are deduplicated
and consistently ordered.
* **Tests**
* Added coverage for password reset attempts involving deleted accounts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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
Added functionality to log request timings in development mode. Introduced a WeakMap to track request start times and log the elapsed time along with the request method, pathname, and response status after each request. This improves observability during development.
## What
Removes the Next.js compatibility shim layer that the ElysiaJS backend
migration (#1630) introduced, replacing it with standard Web APIs and
de-aliased local runtime helpers. Addresses N2D4's review note on #1630:
> i think we should create a followup PR which cleans these up and uses
the elysia methods directly — so we don't need a nextjs compat layer
forever
Stacked on `migrate-backend-to-elysiajs`.
## Changes
**`next/server` → standard Web APIs (eliminated)**
- `NextRequest` → `Request`, `NextResponse.json()` → `Response.json()`,
`new NextResponse(...)` → `new Response(...)`, `req.nextUrl` → `new
URL(req.url)`
- Hot path (`smart-route-handler`) computes `const requestUrl = new
URL(req.url)` once
- Deleted `lib/next-compat/server.tsx`; rewrote
`server/next-request-shim.ts` → `server/backend-request.ts`
(`createBackendRequest`, returns a plain `Request`)
**`next/headers` + `next/navigation` → `lib/runtime/` (de-aliased)**
- `git mv`'d the real runtime helpers (`headers`, `navigation`,
`request-context`) out of `lib/next-compat/` into `lib/runtime/`,
repointing all consumers to `@/lib/runtime/*`
- The cookie/header/redirect mechanism (AsyncLocalStorage + thrown
redirect errors, driven by `app.ts`) is unchanged — it was only *named*
after Next, never actually Next
**Config + cleanup**
- Dropped the `next/*` path aliases from `tsconfig.json`,
`vitest.config.ts`, `tsdown.config.ts` (removed `nextCompatPlugin`, the
alias map, and the `next` bundling special-case)
- Deleted `fetch.d.ts` and removed the no-op `next: { revalidate }`
fetch option in `changelog/route.tsx` (caching never worked outside the
Next runtime)
- Converted the unreferenced `proxy.tsx` so it still compiles (it's dead
code superseded by `server/middleware.ts` — deletion candidate, left out
of this PR)
No `next/*` imports remain in the backend.
## Verification
- `tsc --noEmit` — passes (exit 0)
- `eslint` on all changed files — clean
## Notes / not in scope
- This removes the **`next/*` façade**. It keeps the underlying
ALS-based request-context mechanism rather than threading Elysia's
native context (`set.cookie`, `set.redirect`) into every handler —
that's a much larger change touching handler signatures across the whole
API surface.
- One internal redirect digest string is still `"NEXT_REDIRECT"`
(matched in both `navigation.tsx` and `app.ts`); renamable but cosmetic.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Remove the `next/*` compatibility layer and switch the backend to
standard Web APIs. This simplifies handlers and config while keeping the
ALS request context and redirect behavior unchanged.
- **Refactors**
- Replaced `NextRequest`/`NextResponse` and `req.nextUrl` with
`Request`/`Response` and `new URL(req.url)` across handlers, proxies,
health/unsubscribe routes, and IDP endpoints.
- Moved runtime helpers to `@/lib/runtime/*` (`headers`, `navigation`,
`request-context`) and updated imports.
- Added `server/backend-request.ts` to build backend `Request`s with
merged headers; removed `server/next-request-shim.ts` and
`lib/next-compat/server.tsx`.
- Updated proxy rewrite to set `x-middleware-rewrite`; IDP routes now
return `Response` directly with 307/308 mapping.
- Removed the `next:{revalidate}` fetch option; deleted
`lib/next-compat/fetch.d.ts`.
- Dropped `next/*` aliases from `tsconfig`, `tsdown`, and `vitest`;
route registry/types now use `Request`.
- **Migration**
- Import from `@/lib/runtime/headers` and `@/lib/runtime/navigation`
instead of `next/headers` and `next/navigation`.
- Route handlers should accept `Request` and use `new URL(req.url)` for
URL parsing.
<sup>Written for commit b4a534ff36.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1652?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: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mantra <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Bilal Godil <[email protected]>
## Summary
New "CLI Auth" app registered in `apps-config.ts` (alpha, parent:
authentication) and `apps-frontend.tsx` (TerminalWindowIcon, `/cli-auth`
route).
**Backend** — `GET /internal/cli-auth`:
- Queries `CliAuthAttempt` (last 50) with computed status from
`usedAt`/`refreshToken`/`expiresAt`
- Joins claimed `refreshToken` values against `ProjectUserRefreshToken`
to find active CLI sessions + user info via `ProjectUser`
- Returns `{ summary, recent_attempts, active_cli_users }`
**Dashboard** — `/cli-auth/page-client.tsx`:
- Fetches via `hexclaveAppInternalsSymbol` →
`sendRequest("/internal/cli-auth", {}, "admin")`
- Renders KPI cards (total/completed/expired/active), active sessions
list with last-active time, and recent attempts with status badges
- Expired sessions collapsed by default under `<details>`
Link to Devin session:
https://app.devin.ai/sessions/0868d1452a024b9da36b9d6a45044ff3
Requested by: @N2D4
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds an alpha CLI Auth dashboard to track CLI login attempts and active
refresh tokens, powered by a hidden admin-only endpoint.
- **New Features**
- Registers `cli-auth` under Authentication; adds `/cli-auth` route with
TerminalWindowIcon and docs link.
- Backend `GET /internal/cli-auth`: summary stats (incl. used_attempts),
last 50 attempts with computed status, and active CLI users by joining
CLI-issued refresh tokens (bounded).
- Dashboard fetches via admin request and shows KPIs, active sessions
(last-active/expiry), and recent attempts; expired sessions are
collapsed.
- **Bug Fixes**
- Use per-project admin app (`useAdminApp`) to avoid
ADMIN_AUTHENTICATION_REQUIRED and ensure correct tenancy scoping.
- Resolve primary emails for active sessions via `ContactChannel` join
to prevent 500s.
- Fix badges by using `DesignBadge` label prop and set expired to red;
add default cases in status switches.
- Bound token lookup to last 200 CLI-issued tokens and use a separate
COUNT(*) for accurate `active_tokens`.
- Align loading skeleton grid with the KPI layout.
- Docs: add `cli-auth` icon.
<sup>Written for commit 292859e921.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1739?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**
* Added a **CLI Auth** dashboard page with metrics for active sessions,
an expandable expired session list, and recent login attempts.
* Added a hidden internal analytics endpoint powering the dashboard
(summary, recent attempts, and active users).
* Registered **CLI Auth** in the app catalog/navigation (alpha) and
added its icon to the docs UI.
* **Bug Fixes**
* Improved request/response validation, loading/error states, and
avoided state updates after unmount.
* **Documentation**
* Updated docs indexing settings and added a redirect for a related
guide.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
## 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 <[email protected]>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
## 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 -->
## What
The `/internal/analytics/query` route accepts an `include_all_branches`
flag (defaults to `false`). Previously, passing `true` threw a
`HexclaveAssertionError`. Since the flag is reachable from user input,
that assertion surfaced as a 500 / error-tracking noise rather than a
clean response.
This makes the flag a **no-op** for now and documents the intended
behavior with a TODO.
## Behavior
- Regardless of `include_all_branches`, queries remain scoped to the
current branch via the ClickHouse row policy that filters on
`SQL_branch_id` (set from `auth.tenancy.branchId`).
- Callers passing `include_all_branches=true` will still only receive
data for the **current branch** until cross-branch filtering is
implemented.
## Note / follow-up
Because the flag is silently ignored, a caller asking for "all branches"
gets single-branch results with no signal. The TODO tracks implementing
real cross-branch querying. Flagging in case we'd rather surface a
proper client-facing error in the interim instead of a silent no-op.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Made the `include_all_branches` flag on `/internal/analytics/query` a
no-op so `true` no longer throws and avoids 500s from user input.
Branching isn’t implemented yet, so queries always scope to the current
branch via the ClickHouse `SQL_branch_id` row policy; TODO added for
real cross-branch querying.
<sup>Written for commit a07e1237e9.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1678?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**
* Analytics requests now accept the “include all branches” option
without failing.
* Branch-scoped analytics queries continue to run as before, while the
option is treated as a no-op for now.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## 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 -->
## Problem
The QA reviewer (`apps/backend/src/lib/ai/qa-reviewer.ts`) extracted the
model's JSON verdict from free-form text with a greedy `/\{[\s\S]*\}/`
regex followed by `JSON.parse`. Whenever Haiku wrapped its JSON in prose
or tool-call text, the captured slice was malformed and `JSON.parse`
threw:
```
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
```
This is Sentry issue **STACK-BACKEND-18F** — **218 occurrences in the
last 30 days** (262 all-time), still ongoing. It's caught (background QA
task, 0 users impacted) but noisy, and on failure the review is silently
marked failed.
### Root cause
The brace-matching heuristic assumes the first `{` in the response
starts the JSON object and the last `}` ends it. Any stray brace in
surrounding prose (a preamble like `"Here's my review {note}: {...}"`,
unquoted/single-quoted keys, or `<function_calls>` tool text) breaks the
extraction. The reviewer output is non-deterministic, so a low steady
fraction of reviews fail.
## Fix
Switch to schema-enforced **structured output** via `generateText`'s
`output: Output.object({ schema })` (AI SDK v6), which keeps the
existing tool-calling loop while constraining the final result to a
validated object. The verdict is read directly off `result.output`,
removing the regex, `JSON.parse`, and manual `typeof` shape validation.
- Net **−12 lines**.
- Trimmed the now-redundant "respond with ONLY valid JSON … {inline
schema}" block from the system prompt (the schema enforces structure);
kept the semantic guidance (flag types, scoring, `needsHumanReview`
rule).
- Kept the existing `overallScore` clamp.
## Verification
- **Typecheck** (`tsc --noEmit`): clean.
- **Lint** (`eslint`): clean.
- **Live end-to-end**: ran the real Haiku-4.5 reviewer against the exact
prompt/input that failed **11/15** on the old path → **15/15** return a
validated object on the new path, zero parse failures.
## Follow-ups (out of scope, noted during investigation)
- These Sentry events leak a live access token + publishable key into
the request-context payload; the OpenRouter key sits in plaintext
(commented) in `apps/backend/.env.local`. Worth rotating/scrubbing.
- Consider capturing `result.text` on the failure path so any residual
structured-output failures are inspectable (currently discarded).
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Replaced regex/JSON.parse extraction in the QA reviewer with
schema-enforced structured output using `ai`’s `generateText` +
`Output.object`. Also defaulted `improvementSuggestions` to an empty
string to avoid false failures when the model omits suggestions.
- **Bug Fixes**
- Added a `zod` verdict schema and read result from `result.output`.
- Removed greedy JSON extraction and manual shape checks.
- Defaulted `improvementSuggestions` to "" to tolerate omission and
prevent false `needsHumanReview` verdicts.
- Slimmed prompt by dropping “respond with ONLY JSON” block; kept review
guidance.
- Kept `overallScore` clamp; tool-calling loop unchanged.
- Verification: typecheck/lint clean; 15/15 successful runs on a
previously failing case.
<sup>Written for commit 72ae71017f.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1688?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**
* QA reviews now use structured output, making review results more
reliable and consistently formatted.
* Review flags now include clearer details such as type, severity, and
explanation.
* **Bug Fixes**
* Improved handling of review output by removing fragile text parsing
and manual JSON extraction.
* Added safer defaults for missing review suggestions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Standardize the unauthenticated `smart` and `smartest` model tiers in
`apps/backend/src/lib/ai/models.ts` on `z-ai/glm-5.2:nitro` (previously
a mix of `z-ai/glm-5.2` and `z-ai/glm-5:nitro`).
- Document the `npx @hexclave/cli exec <javascript>` command (runs JS
with a pre-configured `hexclaveServerApp`) in the agent reminders.
- Note that the config file is automatically synced when using the local
dashboard/dev environment via `npx @hexclave/cli dev --config-file
<path>`.
These reminder updates are reflected across `docs-mintlify/llms.txt`,
`docs-mintlify/snippets/hexclave-agent-reminders.jsx`,
`docs-mintlify/snippets/home-prompt-island.jsx`, and
`docs-mintlify/guides/getting-started/setup.mdx`.
## Test plan
- [ ] Verify unauthenticated AI requests resolve to `z-ai/glm-5.2:nitro`
for smart/smartest tiers.
- [ ] Confirm docs render the new agent-reminder bullets correctly.
Made with [Cursor](https://cursor.com)
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Standardized unauthenticated `smart` and `smartest` tiers to
`z-ai/glm-5.2:nitro` for both slow and fast paths to ensure consistent
model selection. Updated docs to include `npx @hexclave/cli exec
<javascript>` (preloads `hexclaveServerApp`) and to note config
auto-sync when using `npx @hexclave/cli dev --config-file <path>`.
<sup>Written for commit 3363c8d9f3.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1693?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**
* Updated unauthenticated AI model selection to use a newer variant for
faster and more reliable responses in supported smart modes.
* **Documentation**
* Added clearer CLI guidance for running JavaScript with a
pre-configured app context.
* Expanded reminder text to better explain local config syncing during
development and dashboard runs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Cursor <[email protected]>