Merge remote-tracking branch 'origin/dev' into cl/hexclave-pr3

# Conflicts:
#	README.md
#	packages/template/src/lib/stack-app/apps/implementations/session-replay.ts
This commit is contained in:
Bilal Godil 2026-05-27 15:57:38 -07:00
commit 9bfd0af8fe
43 changed files with 262 additions and 231 deletions

BIN
.github/assets/app-shots/analytics.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
.github/assets/app-shots/api-keys.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
.github/assets/app-shots/data-vault.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
.github/assets/app-shots/emails.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
.github/assets/app-shots/payments.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
.github/assets/app-shots/rbac.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
.github/assets/app-shots/teams.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
.github/assets/app-shots/webhooks.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
.github/assets/comparison.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

1
.github/assets/hexclave-header.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

BIN
.github/assets/logos/analytics.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
.github/assets/logos/api-keys.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
.github/assets/logos/authentication.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
.github/assets/logos/data-vault.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
.github/assets/logos/emails.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
.github/assets/logos/payments.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
.github/assets/logos/rbac.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
.github/assets/logos/teams.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
.github/assets/logos/webhooks.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -22,4 +22,4 @@ jobs:
- uses: technote-space/toc-generator@9e238e7974de5fcf7b17b7acc54c896160dda0a3 # v4
with:
TOC_TITLE: ""
TARGET_PATHS: "README*.md,CONTRIBUTING.md"
TARGET_PATHS: "CONTRIBUTING.md"

View File

@ -2,7 +2,7 @@
Welcome to Hexclave!
Due to the nature of authentication, this may not be the easiest project to contribute to, so if you are looking for projects to help gain programming experience, we may not be a great match. If you're looking for projects for beginners, check out [Awesome First PR Opportunities](https://github.com/MunGell/awesome-for-beginners).
Due to the nature of user infrastructure, this may not be the easiest project to contribute to, so if you are looking for projects to help gain programming experience, we may not be a great match. If you're looking for projects for beginners, check out [Awesome First PR Opportunities](https://github.com/MunGell/awesome-for-beginners).
## Table of contents
@ -25,7 +25,7 @@ If you think Hexclave is a good fit for you, follow these steps:
2. [Use Hexclave](https://docs.hexclave.com/). The best way to understand the project is to use it. Build an application on top of Hexclave, and post it on GitHub or write a blog post about how you built it. This also lets us assess your skills and understand where you could best help the project.
3. Give us feedback on Discord or GitHub; let us know where you got stuck, and which things you wish were easier. (We appreciate contributions most when they solve problems the authors encountered themselves in real usage.)
4. Contribute to the [documentation](https://docs.hexclave.com) and create examples & guides. This way, you can share your knowledge and expertise with everyone else who's just getting started.
5. Only then, start [contributing to the codebase](README.md#-development--contribution). Coordinate with us on Discord beforehand to ensure we are not working on the same thing already, and to make sure a task is not more difficult than it seems.
5. Only then, start [contributing to the codebase](README.md#contributing). Coordinate with us on Discord beforehand to ensure we are not working on the same thing already, and to make sure a task is not more difficult than it seems.
## Security & bug bounties

325
README.md
View File

@ -1,196 +1,181 @@
[![Hexclave Logo](/.github/assets/logo.png)](https://hexclave.com)
<h3 align="center">
<a href="https://docs.hexclave.com">📘 Docs</a>
| <a href="https://hexclave.com/">☁️ Hosted Version</a>
| <a href="https://demo.hexclave.com/">✨ Demo</a>
| <a href="https://discord.hexclave.com">🎮 Discord</a>
</h4>
# Hexclave: The open-source auth platform
Hexclave is a managed user authentication solution. It is developer-friendly and fully open-source (licensed under MIT and AGPL).
Hexclave gets you started in just five minutes, after which you'll be ready to use all of its features as you grow your project. Our managed service is completely optional and you can export your user data and self-host, for free, at any time.
We support Next.js, React, and JavaScript frontends, along with any backend that can use our [REST API](https://docs.hexclave.com/api/overview). Check out our [setup guide](https://docs.hexclave.com/getting-started/setup) to get started.
<div align="center">
<img alt="Hexclave Setup" src=".github/assets/create-project.gif" width="400" />
<img src=".github/assets/hexclave-header.svg" alt="Hexclave" width="320"/>
<br/>
**The user infrastructure platform.**
Hexclave handles everything around your users: authentication, teams,
payments, emails, analytics, and much more. Start in minutes on the hosted
cloud. Your data is always yours to export and self-host.
[Website](https://hexclave.com) · [Docs](https://docs.hexclave.com) · [Dashboard](https://app.hexclave.com) · [Discord](https://discord.hexclave.com)
![License](https://img.shields.io/badge/license-MIT%20%2F%20AGPLv3-blue)
![SDKs](https://img.shields.io/badge/SDKs-Next.js%20%C2%B7%20React%20%C2%B7%20JS-black)
![Deploy](https://img.shields.io/badge/deploy-Cloud%20or%20self--hosted-success)
</div>
## Table of contents
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<div align="center">
<img src=".github/assets/comparison.png" alt="Where Hexclave fits in the infrastructure stack" width="900"/>
</div>
- [How is this different from X?](#how-is-this-different-from-x)
- [✨ Features](#-features)
- [📦 Installation & Setup](#-installation--setup)
- [🌱 Some community projects built with Hexclave](#-some-community-projects-built-with-hexclave)
- [Templates](#templates)
- [Examples](#examples)
- [🏗 Development & Contribution](#-development--contribution)
- [Requirements](#requirements)
- [Setup](#setup)
- [Useful commands](#useful-commands)
- [❤ Contributors](#-contributors)
## Get started
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Setting up Hexclave is one prompt. Paste this into your coding agent of choice:
## How is this different from X?
Ask yourself about `X`:
- Is `X` open-source?
- Is `X` developer-friendly, well-documented, and lets you get started in minutes?
- Besides authentication, does `X` also do authorization and user management (see feature list below)?
If you answered "no" to any of these questions, then that's how Hexclave is different from `X`.
## ✨ Features
To get notified first when we add new features, please subscribe to [our newsletter](https://stack-auth.beehiiv.com/subscribe).
| | |
|-|:-:|
| <h3>`<SignIn/>` and `<SignUp/>`</h3> Authentication components that support OAuth, password credentials, and magic links, with shared development keys to make setup faster. All components support dark/light modes. | <img alt="Sign-in component" src=".github/assets/dark-light-mode.png" width="250px"> |
| <h3>Idiomatic Next.js APIs</h3> We build on server components, React hooks, and route handlers. | ![Dark/light mode](.github/assets/components.png) |
| <h3>User dashboard</h3> Dashboard to filter, analyze, and edit users. Replaces the first internal tool you would have to build. | ![User dashboard](.github/assets/dashboard.png) |
| <h3>Account settings</h3> Lets users update their profile, verify their e-mail, or change their password. No setup required. | <img alt="Account settings component" src=".github/assets/account-settings.png" width="300px"> |
| <h3>Multi-tenancy & teams</h3> Manage B2B customers with an organization structure that makes sense and scales to millions. | <img alt="Selected team switcher component" src=".github/assets/team-switcher.png" width="400px"> |
| <h3>Role-based access control</h3> Define an arbitrary permission graph and assign it to users. Organizations can create org-specific roles. | <img alt="RBAC" src=".github/assets/permissions.png" width="400px"> |
| <h3>OAuth Connections</h3>Beyond login, Hexclave can also manage access tokens for third-party APIs, such as Outlook and Google Calendar. It handles refreshing tokens and controlling scope, making access tokens accessible via a single function call. | <img alt="OAuth tokens" src=".github/assets/connected-accounts.png" width="250px"> |
| <h3>Passkeys</h3> Support for passwordless authentication using passkeys, allowing users to sign in securely with biometrics or security keys across all their devices. | <img alt="OAuth tokens" src=".github/assets/passkeys.png" width="400px"> |
| <h3>Impersonation</h3> Impersonate users for debugging and support, logging into their account as if you were them. | <img alt="Webhooks" src=".github/assets/impersonate.png" width="350px"> |
| <h3>Webhooks</h3> Get notified when users use your product, built on Svix. | <img alt="Webhooks" src=".github/assets/stack-webhooks.png" width="300px"> |
| <h3>Automatic emails</h3> Send customizable emails on triggers such as sign-up, password reset, and email verification, editable with a WYSIWYG editor. | <img alt="Email templates" src=".github/assets/email-editor.png" width="400px"> |
| <h3>User session & JWT handling</h3> Hexclave manages refresh and access tokens, JWTs, and cookies, resulting in the best performance at no implementation cost. | <img alt="User button" src=".github/assets/user-button.png" width="400px"> |
| <h3>M2M authentication</h3> Use short-lived access tokens to authenticate your machines to other machines. | <img src=".github/assets/m2m-auth.png" alt="M2M authentication" width="400px"> |
## 📦 Installation & Setup
To install Hexclave in your Next.js project (for React, JavaScript, or other frameworks, see our [complete documentation](https://docs.hexclave.com)):
1. Run Hexclave's installation wizard with the following command:
```bash
npx @hexclave/cli@latest init
```
2. Then, create an account on the [Hexclave dashboard](https://app.hexclave.com/projects), create a new project with an API key, and copy its environment variables into the .env.local file of your Next.js project:
```
NEXT_PUBLIC_STACK_PROJECT_ID=<your-project-id>
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=<your-publishable-client-key>
STACK_SECRET_SERVER_KEY=<your-secret-server-key>
```
3. That's it! You can run your app with `npm run dev` and go to [http://localhost:3000/handler/signup](http://localhost:3000/handler/signup) to see the sign-up page. You can also check out the account settings page at [http://localhost:3000/handler/account-settings](http://localhost:3000/handler/account-settings).
Check out the [documentation](https://docs.hexclave.com/getting-started/setup) for a more detailed guide.
## 🌱 Some community projects built with Hexclave
Have your own? Happy to feature it if you create a PR or message us on [Discord](https://discord.hexclave.com).
### Templates
- [Hexclave Template by Hexclave Team](https://github.com/hexclave/hexclave-template)
- [Next SaaSkit by wolfgunblood](https://github.com/wolfgunblood/nextjs-saaskit)
- [SaaS Boilerplate by Robin Faraj](https://github.com/robinfaraj/saas-boilerplate)
### Examples
- [Hexclave Example by career-tokens](https://github.com/career-tokens/StackYCAuth)
- [Hexclave Demo by the Hexclave team](https://github.com/hexclave/hexclave/tree/dev/examples/demo)
- [Hexclave E-Commerce Example by the Hexclave team](https://github.com/hexclave/hexclave/tree/dev/examples/e-commerce)
## 🏗 Development & Contribution
This is for you if you want to contribute to the Hexclave project or run the Hexclave dashboard locally.
**Important**: Please read the [contribution guidelines](CONTRIBUTING.md) carefully and join [our Discord](https://discord.hexclave.com) if you'd like to help.
### Requirements
- Node v20
- pnpm v9
- Docker
### Setup
Note: 24GB+ of RAM is recommended for a smooth development experience.
In a new terminal:
```sh
pnpm install
# Build the packages and generate code. We only need to do this once, as `pnpm dev` will do this from now on
pnpm build:packages
pnpm codegen
# Start the dependencies (DB, Inbucket, etc.) as Docker containers, seeding the DB with the Prisma schema
# Make sure you have Docker (or OrbStack) installed and running
pnpm restart-deps
# Start the dev server
pnpm dev
# In a different terminal, run tests in watch mode
pnpm test # useful: --no-watch (disables watch mode) and --bail 1 (stops after the first failure)
```text
Read skill.hexclave.com and help me setup hexclave in this project
```
You can now open the dev launchpad at [http://localhost:8100](http://localhost:8100). From there, you can navigate to the dashboard at [http://localhost:8101](http://localhost:8101), API on port 8102, demo on port 8103, docs on port 8104, Inbucket (e-mails) on port 8105, and Prisma Studio on port 8106. See the dev launchpad for a list of all running services.
## What's included
Your IDE may show an error on all `@hexclave/XYZ` imports. To fix this, simply restart the TypeScript language server; for example, in VSCode you can open the command palette (Ctrl+Shift+P) and run `Developer: Reload Window` or `TypeScript: Restart TS server`.
Hexclave ships as a catalog of apps you switch on as your product needs them.
Each one is built on the same user model, and new apps land regularly.
Pre-populated .env files for the setup below are available and used by default in `.env.development` in each of the packages. However, if you're creating a production build (eg. with `pnpm run build`), you must supply the environment variables manually (see below).
<table><tr>
<td width="50%" valign="middle">
### Useful commands
### <img src=".github/assets/logos/authentication.png" alt="" width="40" align="top"/> &nbsp; Authentication
```sh
# NOTE:
# Please see the dev launchpad (default: http://localhost:8100) for a list of all running services.
Authentication that just works with passkeys, OAuth, and CLI auth. Drop in one component and ship the whole flow; auth methods toggle from the dashboard with no code changes needed.
# Installation commands
pnpm install: Installs dependencies
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/authentication.png" width="520" alt="Authentication"/>
</td>
</tr></table>
# Types & linting commands
pnpm typecheck: Runs the TypeScript type checker. May require a build or dev server to run first.
pnpm lint: Runs the ESLint linter. Optionally, pass `--fix` to fix some of the linting errors. May require a build or dev server to run first.
<table><tr>
<td width="50%" valign="middle">
# Build commands
pnpm build: Builds all projects, including apps, packages, examples, and docs. Also runs code-generation tasks. Before you can run this, you will have to copy all `.env.development` files in the folders to `.env.production.local` or set the environment variables manually.
pnpm build:packages: Builds all the npm packages.
pnpm codegen: Runs all the code-generation tasks, eg. Prisma client and OpenAPI docs generation.
### <img src=".github/assets/logos/teams.png" alt="" width="40" align="top"/> &nbsp; Teams
# Development commands
pnpm dev: Runs the development servers of the main projects, excluding most examples. On the first run, requires the packages to be built and codegen to be run. After that, it will watch for file changes (including those in code-generation files). If you have to restart the development server for anything, that is a bug that you can report.
pnpm dev:full: Runs the development servers for all projects, including examples.
pnpm dev:basic: Runs the development servers only for the necessary services (backend and dashboard). Not recommended for most users, upgrade your machine instead.
Build for teams, not just users, with workspaces, email invites, and roles that actually gate the work. The workspace switcher remembers selection, invites auto sign up new users, and permissions hold up under audit.
# Environment commands
pnpm start-deps: Starts the Docker dependencies (DB, Inbucket, etc.) as Docker containers, and initializes them with the seed script & migrations. Note: The started dependencies will be visible on the dev launchpad (port 8100 by default).
pnpm stop-deps: Stops the Docker dependencies (DB, Inbucket, etc.) and deletes the data on them.
pnpm restart-deps: Stops and starts the dependencies.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/teams.png" width="520" alt="Teams"/>
</td>
</tr></table>
# Database commands
pnpm db:migration-gen: Currently not used. Please generate Prisma migrations manually (or with AI).
pnpm db:reset: Resets the database to the initial state. Run automatically by `pnpm start-deps`.
pnpm db:init: Initializes the database with the seed script & migrations. Run automatically by `pnpm db:reset`.
pnpm db:seed: Re-seeds the database with the seed script. Run automatically by `pnpm db:init`.
pnpm db:migrate: Runs the migrations. Run automatically by `pnpm db:init`.
<table><tr>
<td width="50%" valign="middle">
# Testing commands
pnpm test <file-filters>: Runs the tests. Pass `--bail 1` to make the test only run until the first failure. Pass `--no-watch` to run the tests once instead of in watch mode.
### <img src=".github/assets/logos/rbac.png" alt="" width="40" align="top"/> &nbsp; RBAC
# Various commands
pnpm explain-query: Paste a SQL query to get an explanation of the query plan, helping you debug performance issues.
pnpm verify-data-integrity: Verify the integrity of the data in the database by running a bunch of integrity checks. This should never fail at any point in time (unless you messed with the DB manually).
```
Permissions, sorted: roles that nest and one permission check that works the same on server or client. Define them in the dashboard, check them anywhere in your code.
Note: When working with AI, you should keep a terminal tab with the dev server open so the AI can run queries against it.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/rbac.png" width="520" alt="RBAC"/>
</td>
</tr></table>
<table><tr>
<td width="50%" valign="middle">
### <img src=".github/assets/logos/api-keys.png" alt="" width="40" align="top"/> &nbsp; API Keys
API keys without the footguns: leaked keys get auto-revoked, work for users and teams, and show the full secret only once. We never keep the plaintext after creation.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/api-keys.png" width="520" alt="API Keys"/>
</td>
</tr></table>
<table><tr>
<td width="50%" valign="middle">
### <img src=".github/assets/logos/payments.png" alt="" width="40" align="top"/> &nbsp; Payments
Payments without the plumbing for subscriptions, one-time charges, and usage metering with credits. Bill a person or a whole team with one model, no separate codepath.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/payments.png" width="520" alt="Payments"/>
</td>
</tr></table>
<table><tr>
<td width="50%" valign="middle">
### <img src=".github/assets/logos/emails.png" alt="" width="40" align="top"/> &nbsp; Emails
Email that delivers and tells you so, handling transactional and marketing sends from one API. Edit templates with an AI editor, theme once, and track every open and click.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/emails.png" width="520" alt="Emails"/>
</td>
</tr></table>
<table><tr>
<td width="50%" valign="middle">
### <img src=".github/assets/logos/analytics.png" alt="" width="40" align="top"/> &nbsp; Analytics
Know your users with no data stack required, with live active user counts and session replays out of the box. Ask in plain English to build dashboards or write SQL to save queries, all with one flag enabled.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/analytics.png" width="520" alt="Analytics"/>
</td>
</tr></table>
<table><tr>
<td width="50%" valign="middle">
### <img src=".github/assets/logos/webhooks.png" alt="" width="40" align="top"/> &nbsp; Webhooks
React to every user event in real time with signed, tamper-proof webhooks. Retries and backoff are handled for you; verify in five lines and manage endpoints from the dashboard.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/webhooks.png" width="520" alt="Webhooks"/>
</td>
</tr></table>
<table><tr>
<td width="50%" valign="middle">
### <img src=".github/assets/logos/data-vault.png" alt="" width="40" align="top"/> &nbsp; Data Vault
A safe for the secrets your users hand you, locked with your secret so we never see the plaintext. Store and retrieve tokens in two lines each, server-only by design.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/data-vault.png" width="520" alt="Data Vault"/>
</td>
</tr></table>
<table><tr>
<td width="50%" valign="middle">
### <img src=".github/assets/logos/launch-checklist.png" alt="" width="40" align="top"/> &nbsp; Launch Checklist
Run through the must-do checks before flipping to production: domain setup, callbacks locked, secrets rotated. The progress tracker keeps your team aligned so nothing critical slips through on launch day.
</td>
<td width="50%" valign="middle" align="center">
<img src=".github/assets/app-shots/launch-checklist.png" width="520" alt="Launch Checklist"/>
</td>
</tr></table>
## Contributing
Hexclave is open source, and contributions are welcome. Read
[`CONTRIBUTING.md`](./CONTRIBUTING.md) to get started, and say hello in
[Discord](https://discord.hexclave.com) before picking up anything large.
Found a security issue? Email security@hexclave.com.
## ❤ Contributors
<a href="https://github.com/hexclave/hexclave/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hexclave/hexclave&columns=9" width="100%" />
<a href="https://github.com/hexclave/stack-auth/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hexclave/stack-auth&columns=9" alt="Contributors" width="100%" />
</a>

View File

@ -612,6 +612,9 @@ describe('validateRedirectUrl', () => {
expect(validateRedirectUrl('stack-auth-mobile-oauth-url://success', tenancy)).toBe(false);
expect(validateRedirectUrl('stack-auth-mobile-oauth-url://error', tenancy)).toBe(false);
expect(validateRedirectUrl('stack-auth-mobile-oauth-url://oauth-callback', tenancy)).toBe(false);
expect(validateRedirectUrl('hexclave-mobile-oauth-url://success', tenancy)).toBe(false);
expect(validateRedirectUrl('hexclave-mobile-oauth-url://error', tenancy)).toBe(false);
expect(validateRedirectUrl('hexclave-mobile-oauth-url://oauth-callback', tenancy)).toBe(false);
});
it('should not accept other custom schemes without trusted domain config', () => {
@ -631,15 +634,23 @@ describe('validateRedirectUrl', () => {
});
describe('isAcceptedNativeAppUrl', () => {
it('should accept the native app OAuth URL scheme', () => {
it('should accept the legacy native app OAuth URL scheme', () => {
expect(isAcceptedNativeAppUrl('stack-auth-mobile-oauth-url://success')).toBe(true);
expect(isAcceptedNativeAppUrl('stack-auth-mobile-oauth-url://error')).toBe(true);
});
it('should accept the canonical Hexclave native app OAuth URL scheme', () => {
expect(isAcceptedNativeAppUrl('hexclave-mobile-oauth-url://success')).toBe(true);
expect(isAcceptedNativeAppUrl('hexclave-mobile-oauth-url://error')).toBe(true);
expect(isAcceptedNativeAppUrl('hexclave-mobile-oauth-url://oauth-callback')).toBe(true);
});
it('should reject other custom schemes', () => {
expect(isAcceptedNativeAppUrl('myapp://callback')).toBe(false);
expect(isAcceptedNativeAppUrl('stackauth-myapp://callback')).toBe(false);
expect(isAcceptedNativeAppUrl('stack-auth://callback')).toBe(false);
expect(isAcceptedNativeAppUrl('hexclave://callback')).toBe(false);
expect(isAcceptedNativeAppUrl('hexclave-mobile-oauth-url-extra://callback')).toBe(false);
expect(isAcceptedNativeAppUrl('https://example.com/callback')).toBe(false);
expect(isAcceptedNativeAppUrl('http://localhost:3000/callback')).toBe(false);
});

View File

@ -10,13 +10,13 @@ function expandStackPortPrefix(value?: string | null) {
return prefix ? value.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, prefix) : value;
}
const sentryErrorSink = (location: string, error: unknown) => {
const sentryErrorSink = (location: string, error: unknown, level: "error" | "warning") => {
if (!("captureException" in Sentry)) {
// this happens if somehow this is called outside of a Next.js script (eg. in the Prisma seed.ts), just log and ignore
console.log("Attempted to capture Sentry error outside of Next.js script, ignoring");
return;
}
Sentry.captureException(error, { extra: { location } });
Sentry.captureException(error, { extra: { location }, level });
runAsynchronouslyAndWaitUntil(Sentry.flush());
};

View File

@ -0,0 +1,3 @@
<svg width="131" height="156" viewBox="0 0 131 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M124.042 28.6459L69.7327 1.75616C66.9416 0.374284 63.666 0.372197 60.8735 1.75051L0.335449 31.6281V87.6369L65.3045 119.91L117.154 93.675V112.414L65.3045 138.44L0.335449 106.584V119.655C0.335449 122.359 1.87599 124.827 4.30545 126.015L61.8765 154.161C64.6911 155.538 67.9883 155.515 70.7832 154.099L130.065 124.074V79.7105C130.065 74.8003 124.934 71.5769 120.51 73.7077L79.0476 93.675V75.9771L130.065 50.1589V38.3485C130.065 34.2325 127.731 30.4724 124.042 28.6459Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 597 B

View File

@ -0,0 +1,3 @@
<svg width="131" height="156" viewBox="0 0 131 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M124.447 28.6459L70.1382 1.75616C67.3472 0.374284 64.0715 0.372197 61.279 1.75051L0.740967 31.6281V87.6369L65.7101 119.91L117.56 93.675V112.414L65.7101 138.44L0.740967 106.584V119.655C0.740967 122.359 2.28151 124.827 4.71097 126.015L62.282 154.161C65.0966 155.538 68.3938 155.515 71.1888 154.099L130.47 124.074V79.7105C130.47 74.8003 125.34 71.5769 120.915 73.7077L79.4531 93.675V75.9771L130.47 50.1589V38.3485C130.47 34.2325 128.137 30.4724 124.447 28.6459Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@ -150,9 +150,11 @@ function RebrandIllustration() {
className="flex justify-center items-center gap-4 pb-2"
aria-hidden="true"
>
{/* Stack Auth: served light & dark variants depending on theme */}
{/* Stack Auth: pre-rebrand mark, preserved under a dedicated filename
because /logo.svg now resolves to the Hexclave benzene mark post-
rebrand (PR #1481). Light & dark variants served per theme. */}
<Image
src="/logo.svg"
src="/stack-auth-logo.svg"
alt=""
width={48}
height={48}
@ -160,7 +162,7 @@ function RebrandIllustration() {
className="h-12 w-auto opacity-50 block dark:hidden"
/>
<Image
src="/logo-bright.svg"
src="/stack-auth-logo-bright.svg"
alt=""
width={48}
height={48}

View File

@ -10,8 +10,8 @@ function expandStackPortPrefix(value?: string | null) {
return prefix ? value.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, prefix as string) : value;
}
const sentryErrorSink = (location: string, error: unknown) => {
Sentry.captureException(error, { extra: { location } });
const sentryErrorSink = (location: string, error: unknown, level: "error" | "warning") => {
Sentry.captureException(error, { extra: { location }, level });
};
export function ensurePolyfilled() {

View File

@ -20,7 +20,7 @@ function escapeHtml(s: string): string {
.replace(/'/g, "&#39;");
}
const INSTALL_CMD = "npx @stackframe/stack-cli@latest init";
const INSTALL_CMD = "npx @hexclave/cli@latest init";
function renderHtml(): string {
const skillEscaped = escapeHtml(SKILL_MD);
@ -32,8 +32,8 @@ function renderHtml(): string {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
<title>Stack Auth Skill</title>
<meta name="description" content="The Stack Auth agent skill — user management, auth, payments, emails, analytics, and the Stack Auth CLI." />
<title>Hexclave Skill</title>
<meta name="description" content="The Hexclave agent skill — user management, auth, payments, emails, analytics, and the Hexclave CLI." />
<style>
:root {
color-scheme: light dark;
@ -159,12 +159,12 @@ function renderHtml(): string {
<a class="skip" href="#main">Skip to Content</a>
<main id="main">
<header>
<div class="brand"><span class="brand-dot" aria-hidden="true"></span><span translate="no">Stack&nbsp;Auth</span></div>
<a class="ghost" href="https://docs.stack-auth.com" rel="noreferrer">Docs&nbsp;</a>
<div class="brand"><span class="brand-dot" aria-hidden="true"></span><span translate="no">Hexclave</span></div>
<a class="ghost" href="https://docs.hexclave.com" rel="noreferrer">Docs&nbsp;</a>
</header>
<h1>The Stack Auth Agent Skill</h1>
<p class="lede">This endpoint serves the canonical <span translate="no">SKILL.md</span> that teaches coding agents how to wire Stack Auth into a project auth, orgs, payments, emails, analytics, and the <span translate="no">stack-cli</span>.</p>
<h1>The Hexclave Agent Skill</h1>
<p class="lede">This endpoint serves the canonical <span translate="no">SKILL.md</span> that teaches coding agents how to wire Hexclave into a project auth, orgs, payments, emails, analytics, and the <span translate="no">hexclave-cli</span>.</p>
<h2>Install in One Command</h2>
<p>Run this in any project root. It detects your agent, installs the skill, registers the MCP server, and writes credentials.</p>
@ -176,17 +176,17 @@ function renderHtml(): string {
<h2>Fetch the Skill Directly</h2>
<p>Agents and tools fetch the markdown from this same URL content negotiation serves <span translate="no">text/markdown</span> to non-browser clients.</p>
<div class="cards">
<a class="card" href="https://docs.stack-auth.com/guides/getting-started/ai-integration" rel="noreferrer">
<a class="card" href="https://docs.hexclave.com/guides/getting-started/ai-integration" rel="noreferrer">
<div class="card-title">AI Integration Guide</div>
<div class="card-desc">How to point an agent at this skill.</div>
</a>
<a class="card" href="https://mcp.stack-auth.com" rel="noreferrer">
<a class="card" href="https://mcp.hexclave.com" rel="noreferrer">
<div class="card-title">MCP Server</div>
<div class="card-desc">Ask questions over the docs with citations.</div>
</a>
<a class="card" href="https://docs.stack-auth.com/guides/going-further/cli" rel="noreferrer">
<a class="card" href="https://docs.hexclave.com/guides/going-further/cli" rel="noreferrer">
<div class="card-title">CLI Reference</div>
<div class="card-desc">Every <span translate="no">stack-cli</span> command and flag.</div>
<div class="card-desc">Every <span translate="no">hexclave-cli</span> command and flag.</div>
</a>
</div>
@ -197,8 +197,8 @@ function renderHtml(): string {
</details>
<footer>
<span>© Stack Auth</span>
<a href="https://github.com/hexclave/stack-auth" rel="noreferrer">GitHub&nbsp;</a>
<span>© Hexclave</span>
<a href="https://github.com/hexclave/hexclave" rel="noreferrer">GitHub&nbsp;</a>
</footer>
</main>
<script>

View File

@ -89,8 +89,8 @@ export function initSentry() {
},
});
registerErrorSink((location, error) => {
Sentry.captureException(error, { extra: { location } });
registerErrorSink((location, error, level) => {
Sentry.captureException(error, { extra: { location }, level });
ignoreUnhandledRejection(Sentry.flush(2000));
});
}

View File

@ -94,16 +94,23 @@ export function errorToNiceString(error: unknown): string {
}
const errorSinks = new Set<(location: string, error: unknown, ...extraArgs: unknown[]) => void>();
export function registerErrorSink(sink: (location: string, error: unknown) => void): void {
export type CaptureLevel = "error" | "warning";
type ErrorSink = (location: string, error: unknown, level: CaptureLevel, ...extraArgs: unknown[]) => void;
const errorSinks = new Set<ErrorSink>();
export function registerErrorSink(sink: ErrorSink): void {
if (errorSinks.has(sink)) {
return;
}
errorSinks.add(sink);
}
registerErrorSink((location, error, ...extraArgs) => {
console.error(
`\x1b[41mCaptured error in ${location}:`,
registerErrorSink((location, error, level, ...extraArgs) => {
const logger = level === "warning" ? console.warn : console.error;
const colorCode = level === "warning" ? "\x1b[43m" : "\x1b[41m";
const label = level === "warning" ? "warning" : "error";
logger(
`${colorCode}Captured ${label} in ${location}:`,
// HACK: Log a nicified version of the error to get around buggy Next.js pretty-printing
// https://www.reddit.com/r/nextjs/comments/1gkxdqe/comment/m19kxgn/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
errorToNiceString(error),
@ -111,11 +118,22 @@ registerErrorSink((location, error, ...extraArgs) => {
"\x1b[0m",
);
});
registerErrorSink((location, error, ...extraArgs) => {
registerErrorSink((location, error, level, ...extraArgs) => {
globalVar.stackCapturedErrors = globalVar.stackCapturedErrors ?? [];
globalVar.stackCapturedErrors.push({ location, error, extraArgs });
globalVar.stackCapturedErrors.push({ location, error, level, extraArgs });
});
function dispatchToSinks(location: string, error: unknown, level: CaptureLevel): void {
for (const sink of errorSinks) {
sink(
location,
error,
level,
...error && (typeof error === 'object' || typeof error === 'function') && "customCaptureExtraArgs" in error && Array.isArray(error.customCaptureExtraArgs) ? (error.customCaptureExtraArgs as any[]) : [],
);
}
}
/**
* Captures an error and sends it to the error sinks (most notably, Sentry). Errors caught with captureError are
* supposed to be seen by an engineer, so they should be actionable and important.
@ -126,13 +144,15 @@ registerErrorSink((location, error, ...extraArgs) => {
* Errors that bubble up to the top of runAsynchronously or a route handler are already captured with captureError.
*/
export function captureError(location: string, error: unknown): void {
for (const sink of errorSinks) {
sink(
location,
error,
...error && (typeof error === 'object' || typeof error === 'function') && "customCaptureExtraArgs" in error && Array.isArray(error.customCaptureExtraArgs) ? (error.customCaptureExtraArgs as any[]) : [],
);
}
dispatchToSinks(location, error, "error");
}
/**
* Like captureError, but logs at warning level. Use for issues that an engineer should know about but that aren't
* severe enough to be treated as an error (e.g. recoverable failures in background tasks).
*/
export function captureWarning(location: string, error: unknown): void {
dispatchToSinks(location, error, "warning");
}

View File

@ -160,7 +160,9 @@ export function isAcceptedNativeAppUrl(urlOrString: string): boolean {
const url = createUrlIfValid(urlOrString);
if (!url) return false;
return url.protocol === 'stack-auth-mobile-oauth-url:';
// Legacy scheme accepted indefinitely; baked into already-shipped Swift SDK binaries.
return url.protocol === 'stack-auth-mobile-oauth-url:'
|| url.protocol === 'hexclave-mobile-oauth-url:';
}
export function validateRedirectUrl(

View File

@ -75,7 +75,10 @@ const DEFAULT_STATE: DevToolState = {
panelHeight: 520,
};
const STACK_LOGO_SVG = '<svg width="14" height="17" viewBox="0 0 131 156" fill="currentColor"><path d="M124.447 28.6459L70.1382 1.75616C67.3472 0.374284 64.0715 0.372197 61.279 1.75051L0.740967 31.6281V87.6369L65.7101 119.91L117.56 93.675V112.414L65.7101 138.44L0.740967 106.584V119.655C0.740967 122.359 2.28151 124.827 4.71097 126.015L62.282 154.161C65.0966 155.538 68.3938 155.515 71.1888 154.099L130.47 124.074V79.7105C130.47 74.8003 125.34 71.5769 120.915 73.7077L79.4531 93.675V75.9771L130.47 50.1589V38.3485C130.47 34.2325 128.137 30.4724 124.447 28.6459Z"/></svg>';
// Hexclave mark — hexagon outline with three radial bars, monochrome via currentColor
// so it inherits the trigger logo's color. Sourced from apps/dashboard/public/hexclave-icon.svg
// (gradient + glow stripped; this is a tiny trigger glyph, not the full brand mark).
const HEXCLAVE_LOGO_SVG = '<svg width="16" height="16" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="miter"><path d="M 24 4 L 41.32 14 L 41.32 34 L 24 44 L 6.68 34 L 6.68 14 Z"/><path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="currentColor" stroke="none"/><path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="currentColor" stroke="none" transform="rotate(120 24 24)"/><path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="currentColor" stroke="none" transform="rotate(240 24 24)"/></svg>';
// ---------------------------------------------------------------------------
// State management
@ -454,7 +457,7 @@ function createTrigger(onClick: () => void): { element: HTMLElement; cleanup: ()
title: 'Hexclave Dev Tools',
});
const logoSpan = h('span', { className: 'sdt-trigger-logo' });
setHtml(logoSpan, STACK_LOGO_SVG);
setHtml(logoSpan, HEXCLAVE_LOGO_SVG);
btn.appendChild(logoSpan);
let placement = loadPlacement() ?? { corner: 'bottom-right' as TriggerCorner };

View File

@ -1,4 +1,5 @@
import { isBrowserLike } from "@hexclave/shared/dist/utils/env";
import { captureWarning } from "@hexclave/shared/dist/utils/errors";
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
import { Result } from "@hexclave/shared/dist/utils/results";
@ -255,12 +256,12 @@ export class SessionRecorder {
);
if (res.status === "error") {
console.warn("SessionRecorder flush failed:", res.error);
captureWarning("SessionRecorder.flush", res.error);
return;
}
if (!res.data.ok) {
console.warn("SessionRecorder flush failed:", res.data.status, await res.data.text());
captureWarning("SessionRecorder.flush", new Error(`SessionRecorder flush failed: ${res.data.status} ${await res.data.text()}`));
}
} finally {
this._flushInProgress = false;

View File

@ -1238,8 +1238,8 @@ class MacOSPresentationContextProvider: NSObject, ASWebAuthenticationPresentatio
struct OAuthView: View {
@Bindable var viewModel: SDKTestViewModel
@State private var provider = "google"
@State private var redirectUrl = "stack-auth-mobile-oauth-url://success"
@State private var errorRedirectUrl = "stack-auth-mobile-oauth-url://error"
@State private var redirectUrl = "hexclave-mobile-oauth-url://success"
@State private var errorRedirectUrl = "hexclave-mobile-oauth-url://error"
@State private var isSigningIn = false
private let presentationProvider = MacOSPresentationContextProvider()

View File

@ -1252,8 +1252,8 @@ struct ContactChannelsView: View {
struct OAuthView: View {
@Bindable var viewModel: SDKTestViewModel
@State private var provider = "google"
@State private var redirectUrl = "stack-auth-mobile-oauth-url://success"
@State private var errorRedirectUrl = "stack-auth-mobile-oauth-url://error"
@State private var redirectUrl = "hexclave-mobile-oauth-url://success"
@State private var errorRedirectUrl = "hexclave-mobile-oauth-url://error"
@State private var isSigningIn = false
private let presentationProvider = iOSPresentationContextProvider()

View File

@ -88,20 +88,20 @@ Two approaches for OAuth authentication:
```swift
// Opens auth session, handles callback automatically
// Uses fixed callback scheme: stack-auth-mobile-oauth-url://
// Uses fixed callback scheme: hexclave-mobile-oauth-url://
try await stack.signInWithOAuth(provider: "google")
```
**2. Manual URL handling** - For custom implementations:
> **Note:** The `stack-auth-mobile-oauth-url://` scheme is automatically accepted.
> **Note:** The `hexclave-mobile-oauth-url://` scheme is automatically accepted (the legacy `stack-auth-mobile-oauth-url://` scheme also remains accepted for backwards compatibility).
```swift
// Get the OAuth URL (must provide absolute URLs)
let oauth = try await stack.getOAuthUrl(
provider: "google",
redirectUrl: "stack-auth-mobile-oauth-url://success",
errorRedirectUrl: "stack-auth-mobile-oauth-url://error"
redirectUrl: "hexclave-mobile-oauth-url://success",
errorRedirectUrl: "hexclave-mobile-oauth-url://error"
)
// Open oauth.url in your own browser/webview

View File

@ -131,10 +131,10 @@ public actor StackClientApp {
) async throws -> OAuthUrlResult {
// Validate that URLs are absolute URLs (panic if not - these are programmer errors)
guard redirectUrl.contains("://") else {
fatalError("redirectUrl must be an absolute URL (e.g., 'stack-auth-mobile-oauth-url://success')")
fatalError("redirectUrl must be an absolute URL (e.g., 'hexclave-mobile-oauth-url://success')")
}
guard errorRedirectUrl.contains("://") else {
fatalError("errorRedirectUrl must be an absolute URL (e.g., 'stack-auth-mobile-oauth-url://error')")
fatalError("errorRedirectUrl must be an absolute URL (e.g., 'hexclave-mobile-oauth-url://error')")
}
let actualState = state ?? generateRandomString(length: 32)
@ -186,7 +186,7 @@ public actor StackClientApp {
return
}
let callbackScheme = "stack-auth-mobile-oauth-url"
let callbackScheme = "hexclave-mobile-oauth-url"
let oauth = try await getOAuthUrl(
provider: provider,
redirectUrl: callbackScheme + "://success",

View File

@ -6,8 +6,8 @@ import Foundation
struct OAuthTests {
// Default test URLs (must be absolute URLs)
let testRedirectUrl = "stack-auth-mobile-oauth-url://success"
let testErrorRedirectUrl = "stack-auth-mobile-oauth-url://error"
let testRedirectUrl = "hexclave-mobile-oauth-url://success"
let testErrorRedirectUrl = "hexclave-mobile-oauth-url://error"
// MARK: - OAuth URL Generation Tests

View File

@ -66,7 +66,7 @@ Note: Additional provider scopes are configured via oauthScopesOnSignIn construc
Implementation:
1. Construct full redirect URLs using a fixed callback scheme:
- Native apps: "stack-auth-mobile-oauth-url://success" and "stack-auth-mobile-oauth-url://error"
- Native apps: "hexclave-mobile-oauth-url://success" and "hexclave-mobile-oauth-url://error"
- Browser: Use the configured OAuth callback handler URL as redirect_uri and window.location to construct absolute URLs
- Browser: If options.returnTo is provided, pass it as afterCallbackRedirectUrl, not as redirect_uri
@ -82,7 +82,7 @@ Implementation:
4. Open the authorization URL:
- Browser: perform redirect according to redirectMethod
- iOS/macOS: ASWebAuthenticationSession with callbackURLScheme: "stack-auth-mobile-oauth-url"
- iOS/macOS: ASWebAuthenticationSession with callbackURLScheme: "hexclave-mobile-oauth-url"
- Android: Custom Tabs with callback URL registered as deep link
- Desktop: Open system browser with registered URL scheme for callback
@ -166,7 +166,7 @@ Returns: { url: string, state: string, codeVerifier: string, redirectUrl: string
redirectUrl: The redirect URL (same as input, needed for token exchange - must match exactly)
Note on URL schemes:
- The "stack-auth-mobile-oauth-url://" scheme is automatically accepted by the backend without any configuration.
- The "hexclave-mobile-oauth-url://" scheme is automatically accepted by the backend without any configuration.
Implementation:
1. Generate or use provided state and codeVerifier