feat: passkey (WebAuthn) auth + auth-config flags for local-mode routing#905
Conversation
…ode routing Two additions on top of the local-first refactor (#902): 1. @better-auth/passkey plugin enabled in auth/index.tsx ------------------------------------------------------- The operator signs up once with email+password, then can enroll a passkey (Touch ID / Face ID / hardware key) for biometric sign-in. The plugin's default `requireSession: true` for registration is kept — this PR adds the sign-in side; post-signup enrollment UI is a small follow-up. Codex review caught two subtle bugs: - `origin: resolveBaseUrl({ request })` would freeze the WebAuthn origin onto the first request that constructed the cached BA instance — request from localhost could poison passkey for a subsequent Tailscale request. Now `origin: null` (plugin reads the request Origin header at verification time). - `rpID: new URL(resolveBaseUrl({ request })).hostname` had the same freezing problem. Now derived from `PUBLIC_WEB_URL` env (stable per deployment), defaults to "localhost" for local-mode. Migration db/migrations/20260519000000_passkey_table.sql creates the `passkey` table with the schema @better-auth/passkey's adapter expects: publicKey, credentialID, counter, deviceType, backedUp, transports, aaguid. FK userId → user(id) ON DELETE CASCADE. 2. /api/auth-config extended with passkey + singleUserMode + hasUser -------------------------------------------------------------------- SPA currently reads auth-config to gate visible providers (magic-link, social, email-password). After #902 it also needs to know: - `passkey: true` — plugin is always wired, so the SPA can render the Sign-in-with-passkey button unconditionally. - `singleUserMode: env.LOBU_SINGLE_USER === '1'` — drives the SPA's copy + routing for local-first installs. - `hasUser`: SELECT EXISTS(SELECT 1 FROM "user" WHERE id <> 'bootstrap-user') — when singleUserMode + !hasUser, SPA forces /sign-up; when +hasUser, forces /sign-in. Filters out the legacy bootstrap-user row so an install that still has it pre-#902 isn't mis-routed. Submodule bump -------------- packages/owletto → feat/passkey-client (lobu-ai/owletto#8c6c283): - authClient wires passkeyClient(). - login.tsx renders the Sign-in-with-passkey button + checks result.error (third codex finding: BA client returns failure as result.error, not via throw). - login.tsx auto-toggles authIntent based on singleUserMode + hasUser. Verified: - `make typecheck` clean. - `bun test packages/server/src/__tests__/unit` — 201 pass, 0 fail. - Codex reviewed; 3 findings (origin freeze, rpID freeze, result.error not inspected) all fixed before push. Not in this PR (deferred): - Post-signup passkey enrollment prompt — operator needs to sign up with password first, then go to /settings to add a passkey. A one-click "enrol now?" after first sign-up is a follow-up. - Passkey-only signup (no password at all) — requires the plugin's custom `resolveUser` callback + a user-create hook. Bigger lift.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds WebAuthn/passkey support: a DB migration and schema for passkeys, extended AuthConfig (passkey, singleUserMode, hasUser) with a DB-backed hasUser check, and wiring the Better Auth passkey plugin plus dependency updates for server and CLI. ChangesPasskey WebAuthn Authentication Support
Sequence DiagramsequenceDiagram
participant getAuthConfig as getAuthConfig
participant Database as Database
participant createAuth as createAuth()
participant PasskeyPlugin as BetterAuth Passkey Plugin
getAuthConfig->>Database: query users (exclude 'bootstrap-user')
Database-->>getAuthConfig: user existence boolean
getAuthConfig->>createAuth: return AuthConfig (passkey, singleUserMode, hasUser)
createAuth->>PasskeyPlugin: configure(passkey: { rpID, rpName: "Lobu", origin: null })
PasskeyPlugin-->>createAuth: initialized
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/server/src/auth/config.ts (1)
415-433: ⚡ Quick winSkip the
hasUserDB probe when single-user mode is off.The query runs even when
singleUserModeis false, which adds unnecessary DB load on a hot config path.Refactor to gate the query
const singleUserMode = env.LOBU_SINGLE_USER === '1'; let hasUser = false; -try { - const sql = getDb(); - const rows = (await sql` - SELECT EXISTS( - SELECT 1 FROM "user" WHERE id <> 'bootstrap-user' - ) AS has_user - `) as unknown as Array<{ has_user: boolean }>; - hasUser = !!rows[0]?.has_user; -} catch { - hasUser = false; -} +if (singleUserMode) { + try { + const sql = getDb(); + const rows = (await sql` + SELECT EXISTS( + SELECT 1 FROM "user" WHERE id <> 'bootstrap-user' + ) AS has_user + `) as unknown as Array<{ has_user: boolean }>; + hasUser = !!rows[0]?.has_user; + } catch { + hasUser = false; + } +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/server/src/auth/config.ts` around lines 415 - 433, The DB probe that sets hasUser (using getDb() and the SQL EXISTS query) should be skipped when singleUserMode is false to avoid unnecessary DB load; wrap the try/catch and the SQL call that assigns hasUser in an if (singleUserMode) { ... } block (leaving hasUser default false and the catch behavior unchanged) so the query only runs when singleUserMode is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@db/migrations/20260519000000_passkey_table.sql`:
- Around line 18-29: The passkey table defines "credentialID" without a
uniqueness constraint, risking duplicate credentials; modify the migration to
enforce uniqueness by altering the table definition or adding a unique index on
"credentialID" (replace or convert the existing passkey_credential_id_idx to a
UNIQUE index) so that "credentialID" is unique across the "passkey" table and
migrations will fail if duplicates exist.
In `@packages/server/src/auth/index.tsx`:
- Around line 537-548: The rpID resolution IIFE currently falls back to
"localhost" when PUBLIC_WEB_URL is missing/invalid; change it to fail fast in
production by detecting production mode (e.g., process.env.NODE_ENV ===
"production" or an existing isProduction flag) and throw a clear error (or call
process.exit(1)) if PUBLIC_WEB_URL is unset or yields an invalid URL/hostname,
otherwise keep the existing parsing behavior for non-production; update the rpID
assignment code (the rpID: (() => { ... })() block) to perform this check and
surface a descriptive error mentioning PUBLIC_WEB_URL and rpID resolution.
---
Nitpick comments:
In `@packages/server/src/auth/config.ts`:
- Around line 415-433: The DB probe that sets hasUser (using getDb() and the SQL
EXISTS query) should be skipped when singleUserMode is false to avoid
unnecessary DB load; wrap the try/catch and the SQL call that assigns hasUser in
an if (singleUserMode) { ... } block (leaving hasUser default false and the
catch behavior unchanged) so the query only runs when singleUserMode is true.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 02ed667c-d3c7-4ee3-be02-fe14004f4b9b
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
db/migrations/20260519000000_passkey_table.sqlpackages/owlettopackages/server/package.jsonpackages/server/src/auth/config.tspackages/server/src/auth/index.tsx
| "credentialID" TEXT NOT NULL, | ||
| counter BIGINT NOT NULL DEFAULT 0, | ||
| "deviceType" TEXT NOT NULL, | ||
| "backedUp" BOOLEAN NOT NULL DEFAULT FALSE, | ||
| transports TEXT, | ||
| "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| aaguid TEXT | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS passkey_user_id_idx ON "passkey"("userId"); | ||
| CREATE INDEX IF NOT EXISTS passkey_credential_id_idx ON "passkey"("credentialID"); | ||
|
|
There was a problem hiding this comment.
Enforce uniqueness for credentialID.
credentialID is currently only indexed, not constrained unique. Duplicate credential IDs can make passkey lookup ambiguous during authentication.
Suggested migration adjustment
-CREATE INDEX IF NOT EXISTS passkey_credential_id_idx ON "passkey"("credentialID");
+CREATE UNIQUE INDEX IF NOT EXISTS passkey_credential_id_uq ON "passkey"("credentialID");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "credentialID" TEXT NOT NULL, | |
| counter BIGINT NOT NULL DEFAULT 0, | |
| "deviceType" TEXT NOT NULL, | |
| "backedUp" BOOLEAN NOT NULL DEFAULT FALSE, | |
| transports TEXT, | |
| "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), | |
| aaguid TEXT | |
| ); | |
| CREATE INDEX IF NOT EXISTS passkey_user_id_idx ON "passkey"("userId"); | |
| CREATE INDEX IF NOT EXISTS passkey_credential_id_idx ON "passkey"("credentialID"); | |
| "credentialID" TEXT NOT NULL, | |
| counter BIGINT NOT NULL DEFAULT 0, | |
| "deviceType" TEXT NOT NULL, | |
| "backedUp" BOOLEAN NOT NULL DEFAULT FALSE, | |
| transports TEXT, | |
| "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), | |
| aaguid TEXT | |
| ); | |
| CREATE INDEX IF NOT EXISTS passkey_user_id_idx ON "passkey"("userId"); | |
| CREATE UNIQUE INDEX IF NOT EXISTS passkey_credential_id_uq ON "passkey"("credentialID"); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@db/migrations/20260519000000_passkey_table.sql` around lines 18 - 29, The
passkey table defines "credentialID" without a uniqueness constraint, risking
duplicate credentials; modify the migration to enforce uniqueness by altering
the table definition or adding a unique index on "credentialID" (replace or
convert the existing passkey_credential_id_idx to a UNIQUE index) so that
"credentialID" is unique across the "passkey" table and migrations will fail if
duplicates exist.
| rpID: (() => { | ||
| const publicWebUrl = process.env.PUBLIC_WEB_URL?.trim(); | ||
| if (publicWebUrl) { | ||
| try { | ||
| const host = new URL(publicWebUrl).hostname; | ||
| if (host) return host; | ||
| } catch { | ||
| /* fallthrough to default */ | ||
| } | ||
| } | ||
| return "localhost"; | ||
| })(), |
There was a problem hiding this comment.
Fail fast in production when PUBLIC_WEB_URL is missing/invalid.
Falling back to "localhost" in production can silently misconfigure RP ID and break passkey ceremonies at runtime.
Safer rpID resolution
rpID: (() => {
const publicWebUrl = process.env.PUBLIC_WEB_URL?.trim();
- if (publicWebUrl) {
- try {
- const host = new URL(publicWebUrl).hostname;
- if (host) return host;
- } catch {
- /* fallthrough to default */
- }
- }
- return "localhost";
+ if (!publicWebUrl) {
+ if (runtimeNodeEnv === "production") {
+ throw new Error("PUBLIC_WEB_URL is required for passkey auth in production");
+ }
+ return "localhost";
+ }
+ try {
+ const host = new URL(publicWebUrl).hostname;
+ if (host) return host;
+ } catch {
+ if (runtimeNodeEnv === "production") {
+ throw new Error("PUBLIC_WEB_URL must be a valid absolute URL for passkey auth");
+ }
+ }
+ return "localhost";
})(),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/server/src/auth/index.tsx` around lines 537 - 548, The rpID
resolution IIFE currently falls back to "localhost" when PUBLIC_WEB_URL is
missing/invalid; change it to fail fast in production by detecting production
mode (e.g., process.env.NODE_ENV === "production" or an existing isProduction
flag) and throw a clear error (or call process.exit(1)) if PUBLIC_WEB_URL is
unset or yields an invalid URL/hostname, otherwise keep the existing parsing
behavior for non-production; update the rpID assignment code (the rpID: (() => {
... })() block) to perform this check and surface a descriptive error mentioning
PUBLIC_WEB_URL and rpID resolution.
CI catches: 1. db/schema.sql must match `dbmate up` output (Schema snapshot drift check). My branch added a migration that creates the `passkey` table but forgot to regenerate db/schema.sql. 2. CLI declares the runtime deps the embedded server bundle pulls at start (dev.test.ts 'CLI package declares runtime deps'). Adding @better-auth/passkey to packages/server/* needs the matching entry in packages/cli/package.json so `lobu run` can resolve it. Hand-edited db/schema.sql to insert only the four passkey blocks (CREATE TABLE, PK, two INDEXes, FK) at the alphabetically correct positions. Avoided a full `make db-schema` regen because my local Postgres has postgis enabled (not in CI's pg16 image) so a full regen would also add postgis extension + geo_lookup function rows that shouldn't be in the snapshot.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@db/schema.sql`:
- Around line 1524-1536: The schema snapshot adds the passkey table DDL but the
schema_migrations seed wasn’t updated, causing the new migration
(20260519000000_passkey_table.sql) to be considered pending; update the
schema_migrations seed to include the new migration ID so the snapshot and
migration history match. Locate the seed data insertion for schema_migrations
(which currently ends at 20260518070000) and append the 20260519000000 entry
(and similarly update the other missing spots noted around lines ~2747, ~4176,
~4974) so that the passkey table creation migration is recorded and will not be
replayed by dbmate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3199e6af-258f-4d2c-a9c9-1cbf3758fc7a
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (2)
db/schema.sqlpackages/cli/package.json
Previous patch put the passkey table after personal_access_tokens, but 'passkey' < 'pending_interactions' < 'personal_access_tokens' alphabetically — dbmate's pg_dump emits them in alpha order, so CI's schema-drift check flagged the mis-positioning. Re-anchored each block: - TABLE: before pending_interactions - PK CONSTRAINT: before pending_interactions_pkey - INDEXes: before personal_access_tokens_active_idx (after oauth_tokens_*) - FK CONSTRAINT: before pending_interactions_organization_id_fkey
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@db/schema.sql`:
- Around line 1456-1472: The schema contains duplicate DDL for the passkey
artifacts causing "already exists" errors; locate the duplicate CREATE TABLE
public.passkey block and the repeated definitions for passkey_pkey (PRIMARY
KEY), passkey_credential_id_idx, passkey_user_id_idx (INDEXes) and
passkey_userId_fkey (FK) and remove the second occurrences so each object
(CREATE TABLE, primary key, indexes, foreign key) is defined only once; keep the
first/canonical definitions and delete the later duplicated blocks, then run a
quick SQL parse to verify no remaining duplicate object names.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: eb619d51-7f62-408a-9769-3d08c96d8374
📒 Files selected for processing (2)
db/schema.sqlpackages/owletto
✅ Files skipped from review due to trivial changes (1)
- packages/owletto
Previous attempts left duplicate passkey blocks: re-patching after git checkout but before merge layered the new blocks on top of the old wrong-position ones. Reset db/schema.sql to origin/main's clean state, then spliced the 4 passkey blocks once at the correct positions (alphabetical: passkey < pending_interactions < personal_access_tokens) + inserted the 20260519000000 row in INSERT INTO schema_migrations.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@db/schema.sql`:
- Line 1561: Change the transports column from a scalar TEXT to a Postgres
text[] array to match the `@better-auth/passkey` expectation: locate the column
definition named "transports" in the schema (currently "transports text,") and
update it to use the text[] type; also update any related DEFAULTs, INSERTs, or
CHECK constraints to use array syntax and add a small migration step to convert
existing scalar values into single-element arrays (or NULL) so existing data
remains valid.
- Around line 1552-1564: The passkey table lacks a uniqueness constraint on the
credentialID column which must be globally unique; add a UNIQUE constraint/index
for "credentialID" on the passkey table (referencing the passkey table and its
"credentialID" column) — either create a unique index on passkey("credentialID")
or run an ALTER TABLE passkey ADD CONSTRAINT ... UNIQUE("credentialID") after
the primary key is defined so duplicate credential IDs cannot be inserted.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| CREATE TABLE public.passkey ( | ||
| id text NOT NULL, | ||
| name text, | ||
| "publicKey" text NOT NULL, | ||
| "userId" text NOT NULL, | ||
| "credentialID" text NOT NULL, | ||
| counter bigint DEFAULT 0 NOT NULL, | ||
| "deviceType" text NOT NULL, | ||
| "backedUp" boolean DEFAULT false NOT NULL, | ||
| transports text, | ||
| "createdAt" timestamp with time zone DEFAULT now() NOT NULL, | ||
| aaguid text | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does the @better-auth/passkey plugin require a unique constraint on the credentialID column in the passkey table schema?
💡 Result:
Yes, the @better-auth/passkey plugin requires the credentialID column to be unique [1]. In the official schema definition for the passkey table, the credentialID is identified as the unique identifier for the registered credential [2][1]. While the plugin's documentation and schema generation tools (such as the CLI migrate or generate commands) handle the creation of the necessary table structure [3][4][1], it is standard practice and a functional requirement for the credentialID to be unique to ensure that each passkey can be correctly identified and retrieved during the authentication ceremony [2][1]. If you are defining the schema manually, you should ensure that the credentialID column has a unique constraint applied to it [1].
Citations:
- 1: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/passkey.mdx
- 2: https://www.better-auth.com/docs/plugins/passkey
- 3: https://www.better-auth.com/docs/concepts/database
- 4: https://www.better-auth.com/docs/installation
Add UNIQUE constraint on credentialID column.
The credentialID column stores the WebAuthn credential identifier, which must be globally unique per the WebAuthn specification and the @better-auth/passkey plugin requirements. During authentication, the server looks up credentials by this ID; without a UNIQUE constraint, duplicate credential IDs could be stored, breaking the authentication flow.
Proposed fix
Add a unique index on credentialID. After the existing non-unique index at line 4325:
CREATE INDEX passkey_credential_id_idx ON public.passkey USING btree ("credentialID");
+CREATE UNIQUE INDEX passkey_credential_id_key ON public.passkey USING btree ("credentialID");Alternatively, add the constraint via ALTER TABLE after the primary key constraint at line 2875:
ALTER TABLE ONLY public.passkey
ADD CONSTRAINT passkey_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.passkey
+ ADD CONSTRAINT passkey_credential_id_key UNIQUE ("credentialID");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@db/schema.sql` around lines 1552 - 1564, The passkey table lacks a uniqueness
constraint on the credentialID column which must be globally unique; add a
UNIQUE constraint/index for "credentialID" on the passkey table (referencing the
passkey table and its "credentialID" column) — either create a unique index on
passkey("credentialID") or run an ALTER TABLE passkey ADD CONSTRAINT ...
UNIQUE("credentialID") after the primary key is defined so duplicate credential
IDs cannot be inserted.
| counter bigint DEFAULT 0 NOT NULL, | ||
| "deviceType" text NOT NULL, | ||
| "backedUp" boolean DEFAULT false NOT NULL, | ||
| transports text, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the expected data type for the transports column in the @better-auth/passkey schema - TEXT, text array, or jsonb?
💡 Result:
The expected data type for the transports column in the @better-auth/passkey schema is a text array (Postgres type: text[]). Evidence: Better Auth’s passkey plugin documentation describes transports as “The transports used to register the passkey” in the generated passkey table schema [1]. The underlying Better Auth schema type definition for the passkey plugin declares transports as an array of strings, which maps to a Postgres text[] column in the generated schema [2].
Citations:
- 1: https://www.better-auth.com/docs/plugins/passkey
- 2: https://github.com/better-auth/better-auth/blob/acbd6ef6/packages/passkey/src/types.ts
Change transports column type from TEXT to text[].
The @better-auth/passkey schema expects transports as a Postgres array type (text[]) to store the list of WebAuthn transports (e.g., ["usb", "nfc", "ble"]), not a scalar TEXT column. This is required for the plugin to correctly handle transport data.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@db/schema.sql` at line 1561, Change the transports column from a scalar TEXT
to a Postgres text[] array to match the `@better-auth/passkey` expectation: locate
the column definition named "transports" in the schema (currently "transports
text,") and update it to use the text[] type; also update any related DEFAULTs,
INSERTs, or CHECK constraints to use array syntax and add a small migration step
to convert existing scalar values into single-element arrays (or NULL) so
existing data remains valid.
Summary
Adds biometric sign-in (Touch ID / Face ID / security key) via
@better-auth/passkey, and the auth-config flags the SPA needs to route + render correctly in single-user local mode.Backend
auth/index.tsx.rpIDderived fromPUBLIC_WEB_URLenv (stable per deployment);origin: nullso the plugin reads the request Origin header at verification time — avoids the cache-poisoning issue where the first request that constructs the BA instance freezes the WebAuthn origin for everyone.db/migrations/20260519000000_passkey_table.sqlcreates thepasskeytable matching@better-auth/passkey's schema./api/auth-configextended withpasskey,singleUserMode,hasUserso the SPA can route to/sign-upor/sign-inbased on whether anyone has signed up yet.SPA (owletto submodule)
Companion PR: lobu-ai/owletto#TBD (
8c6c283).authClientaddspasskeyClient()./sign-inpage renders a "Sign in with passkey" button whenauthConfig.passkeyis enabled. Inspectsresult.errorsince the BA client returns failure modes as a result object rather than throwing.singleUserModeis on, forceauthIntenttosign-upif!hasUser,sign-inifhasUser. Operator only sees the relevant tab.Codex review
Caught three real bugs, all fixed before push:
originfroze for cached BA instance → would silently break for multi-host (localhost + Tailscale + prod) deployments.rpID.signIn.passkey()returned failure asresult.errornot via throw → button cleared loading state with no message on cancel / failed auth.Test plan
make typecheckclean.bun test packages/server/src/__tests__/unit— 201 pass, 0 fail.Deferred
resolveUserhook; bigger change.Summary by CodeRabbit
New Features
Chores