@nominalso/vibe-auth 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +101 -0
- package/LICENSE +10 -0
- package/README.md +103 -0
- package/dist/index.cjs +725 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +338 -0
- package/dist/index.d.ts +338 -0
- package/dist/index.js +695 -0
- package/dist/index.js.map +1 -0
- package/llms.txt +18 -0
- package/package.json +56 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# AGENTS.md — @nominalso/vibe-auth
|
|
2
|
+
|
|
3
|
+
Instructions for AI coding agents (Claude Code, Cursor, Copilot, Lovable, etc.) integrating
|
|
4
|
+
this package. This is a **consumer usage guide**, not repo build config.
|
|
5
|
+
|
|
6
|
+
## What this package is
|
|
7
|
+
|
|
8
|
+
`@nominalso/vibe-auth` is the silent-SSO login flow for a Nominal Vibe App that authenticates
|
|
9
|
+
through Supabase federated with the Nominal host's IdP (Descope). It replaces a hand-written
|
|
10
|
+
`silentAuth.ts` + `AuthGate.tsx` pair generated from the `silent-supabase-oidc-login` skill.
|
|
11
|
+
**If the app has such files, migrate them to this package — do not hand-write this flow.**
|
|
12
|
+
Follow the skill's Step 1b (delete the hand-written files; keep `provider`, callback path, and
|
|
13
|
+
the existing Supabase client).
|
|
14
|
+
|
|
15
|
+
## The one rule
|
|
16
|
+
|
|
17
|
+
Call `createVibeAuth(...)` **once**, **synchronously**, at the top of its own module
|
|
18
|
+
(conventionally `src/lib/auth.ts`), and import that module **eagerly** — never lazily, never
|
|
19
|
+
inside a component or a conditional. `AuthGate`/`SilentCallback` capture the callback URL at
|
|
20
|
+
that call, before any React effect or async work lets supabase-js's `detectSessionInUrl`
|
|
21
|
+
strip it first.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/lib/auth.ts
|
|
25
|
+
import { createVibeAuth } from '@nominalso/vibe-auth'
|
|
26
|
+
import { supabase } from './supabaseClient'
|
|
27
|
+
|
|
28
|
+
export const auth = createVibeAuth({
|
|
29
|
+
supabase,
|
|
30
|
+
provider: 'custom:supabase-fedapp', // ask the user for this — never guess it
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## The three wiring points
|
|
35
|
+
|
|
36
|
+
1. **The callback route.** Mount `auth.SilentCallback` at `auth.callbackPath` (default
|
|
37
|
+
`/silent-callback`), and ONLY that — it must render ungated, outside `AuthGate`. SSR
|
|
38
|
+
frameworks: `ssr: false` (or equivalent) on this route, and the root layout must not
|
|
39
|
+
render the app's provider tree there (see **Root wiring** below).
|
|
40
|
+
2. **The app root.** Wrap everything in `<auth.AuthGate>`. The app renders nothing of itself
|
|
41
|
+
until authenticated.
|
|
42
|
+
3. **The host bridge**, if embedded via `@nominalso/vibe-bridge`. Call
|
|
43
|
+
`auth.wireHostAuth(bridge)` at module scope BEFORE `bridge.connect()`, then
|
|
44
|
+
`auth.seedLastUserId(ctx.user.id, ctx.tenant)` once `connect()` resolves —
|
|
45
|
+
from a **parent** of `AuthGate`, not inside gated children.
|
|
46
|
+
|
|
47
|
+
## Root wiring (SSR — React #418)
|
|
48
|
+
|
|
49
|
+
`AuthGate` / `SilentCallback` defer `window` reads to an effect. Your **root layout** must
|
|
50
|
+
not decide "is this the callback?" with `typeof window !== 'undefined' && window.location…`
|
|
51
|
+
at render time: the server always sees `window` missing and renders the gated app; the
|
|
52
|
+
client's first pass on `/silent-callback` wants a bare outlet. That subtree mismatch is
|
|
53
|
+
React #418, and it can fire `bridge.connect()` inside a popup/hidden iframe.
|
|
54
|
+
|
|
55
|
+
Ask the **router** for the path (identical on server and client):
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// TanStack Start — Next.js: usePathname() from next/navigation in a Client Component
|
|
59
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
|
60
|
+
if (pathname === auth.callbackPath) return <Outlet />
|
|
61
|
+
return (
|
|
62
|
+
<auth.AuthGate>
|
|
63
|
+
<App />
|
|
64
|
+
</auth.AuthGate>
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
export const Route = createFileRoute('/silent-callback')({
|
|
70
|
+
ssr: false,
|
|
71
|
+
component: auth.SilentCallback,
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Do NOT
|
|
76
|
+
|
|
77
|
+
- Do not create your own `signInWithOAuth`/`exchangeCodeForSession` calls anywhere in the app
|
|
78
|
+
— every code path that can create a session goes through this package.
|
|
79
|
+
- Do not add a `signOut()` call on a failed `ensureSession()` — a failed silent attempt is not
|
|
80
|
+
proof the user is logged out (see README's security-boundary section).
|
|
81
|
+
- Do not read `window.location` at component render time to decide whether the current route
|
|
82
|
+
is the callback route — use **Root wiring** above.
|
|
83
|
+
- Do not branch on `typeof window !== 'undefined'` around this package's exports — they are
|
|
84
|
+
already SSR-safe.
|
|
85
|
+
|
|
86
|
+
## Host identity
|
|
87
|
+
|
|
88
|
+
The gate waits for a seeded `{userId, tenant}` before opening. A leftover Supabase session
|
|
89
|
+
is not enough. Unmigrated sibling documents may still write `{userId, at}` markers (no
|
|
90
|
+
`tenant`); this app will not treat those as a completed rebind and will rerun silent SSO
|
|
91
|
+
under the shared Web Lock — safe, just not a skip.
|
|
92
|
+
|
|
93
|
+
Same-user **tenant** switches rebind. If the callback-document race loses and the leftover
|
|
94
|
+
session is the same Supabase user, adoption cannot tell "fresh" from "old" and fail-closes
|
|
95
|
+
(`signOut` + sign-in screen). That is intentional.
|
|
96
|
+
|
|
97
|
+
## Configuration reference
|
|
98
|
+
|
|
99
|
+
See `VibeAuthConfig` in the package's `.d.ts` for every option (including timeouts). The two
|
|
100
|
+
REQUIRED fields are `supabase` (your own client, PKCE-configured) and `provider` (from the
|
|
101
|
+
app's Supabase Auth → Providers setup — ask the user, never guess).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Copyright © 2026 Nominal. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its source code are proprietary and confidential.
|
|
4
|
+
The packages published from this repository are marked "UNLICENSED": no
|
|
5
|
+
license or right to use, copy, modify, distribute, or create derivative
|
|
6
|
+
works is granted except under a separate written agreement with Nominal.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
9
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @nominalso/vibe-auth
|
|
2
|
+
|
|
3
|
+
Silent Supabase OIDC login for **Nominal Vibe Apps** — a configurable, cross-document-safe
|
|
4
|
+
auth gate that replaces a hand-copied per-app implementation of the
|
|
5
|
+
[`silent-supabase-oidc-login`](../../standalone-agents/skills/silent-supabase-oidc-login/SKILL.md)
|
|
6
|
+
skill. If your app authenticates through Supabase federated with the Nominal host's IdP
|
|
7
|
+
(Descope), this package IS that flow — written once, fixed once, and configurable per app.
|
|
8
|
+
|
|
9
|
+
> **For AI agents / Lovable:** see [`AGENTS.md`](./AGENTS.md) for the condensed integration
|
|
10
|
+
> guide (including the SSR root-wiring recipe). To replace a hand-written
|
|
11
|
+
> `silentAuth.ts`/`AuthGate.tsx` pair, follow the `silent-supabase-oidc-login` skill Step 1b.
|
|
12
|
+
|
|
13
|
+
## Why this exists
|
|
14
|
+
|
|
15
|
+
Six generated apps each carried a hand-copied version of this login flow. Five rounds of bug
|
|
16
|
+
fixes had to be re-applied per app, and the copies drifted: one app grew a React hydration
|
|
17
|
+
crash in its root route, three ended up mounting bridge providers inside the auth callback
|
|
18
|
+
document, and all of them still carried a production bug where a stale session's failed
|
|
19
|
+
boot-time refresh briefly flashed the sign-in screen. This package is that logic, fixed and
|
|
20
|
+
written once.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install @nominalso/vibe-auth
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Peer dependencies: `react >=18`, `@supabase/supabase-js ^2`.
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// src/lib/auth.ts — called synchronously, at module scope, imported eagerly.
|
|
34
|
+
import { createVibeAuth } from '@nominalso/vibe-auth'
|
|
35
|
+
import { supabase } from './supabaseClient' // YOUR existing client — this package never creates one
|
|
36
|
+
|
|
37
|
+
export const auth = createVibeAuth({
|
|
38
|
+
supabase,
|
|
39
|
+
provider: 'custom:supabase-fedapp', // from your Supabase Auth → Providers config
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// the callback route — mount ONLY this, ungated, at the callback path
|
|
45
|
+
// (default '/silent-callback'; see AGENTS.md for the SSR-safe recipe)
|
|
46
|
+
import { auth } from '@/lib/auth'
|
|
47
|
+
export default auth.SilentCallback
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// wrap the whole app — it never renders until authenticated
|
|
52
|
+
import { auth } from '@/lib/auth'
|
|
53
|
+
|
|
54
|
+
function Root() {
|
|
55
|
+
return (
|
|
56
|
+
<auth.AuthGate>
|
|
57
|
+
<App />
|
|
58
|
+
</auth.AuthGate>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// wire the Nominal host bridge, before connect()
|
|
65
|
+
import { auth } from '@/lib/auth'
|
|
66
|
+
import { bridge } from './bridge'
|
|
67
|
+
|
|
68
|
+
const unsub = auth.wireHostAuth(bridge)
|
|
69
|
+
const ctx = await bridge.connect()
|
|
70
|
+
auth.seedLastUserId(ctx.user.id, ctx.tenant)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See [`AGENTS.md`](./AGENTS.md) for SSR root wiring and host `seedLastUserId` rules.
|
|
74
|
+
Timeouts live on `VibeAuthTimeouts` in the package `.d.ts`.
|
|
75
|
+
|
|
76
|
+
## Why a client you own, not one this package creates
|
|
77
|
+
|
|
78
|
+
Your `client.ts` is often Lovable-generated (marked "do not edit"), carries your generated
|
|
79
|
+
`Database` types, and may have its own storage adapter (e.g. for Lovable preview brokering).
|
|
80
|
+
This package only requires it: at `createVibeAuth()`, it best-effort-checks
|
|
81
|
+
`auth.flowType === 'pkce'` and logs a loud `console.error` if it isn't — PKCE is a **security
|
|
82
|
+
control**, not a preference (the `.d.ts` on `VibeAuthConfig` explains why).
|
|
83
|
+
|
|
84
|
+
## What you get
|
|
85
|
+
|
|
86
|
+
- `ensureSession()` / `rebindSession({ userId, tenant })` — the cross-document-safe core (Web Lock
|
|
87
|
+
serialised, terminal-capped, sibling-adopting). Most apps never call these directly; the
|
|
88
|
+
gate and `wireHostAuth` do.
|
|
89
|
+
- `AuthGate` / `DefaultSignInScreen` / `SilentCallback` — the three React pieces. Override
|
|
90
|
+
`signInScreen`/`loader` props on `AuthGate` for branding.
|
|
91
|
+
- `wireHostAuth(bridge)` / `seedLastUserId(userId, tenant)` — the Nominal-host integration (logout,
|
|
92
|
+
identity/tenant switch). Seed from a parent of `AuthGate` after `connect()`.
|
|
93
|
+
- Fully configurable timeouts (`VibeAuthTimeouts` in the `.d.ts`) — every value defaults to
|
|
94
|
+
what shipped after this flow's production incidents.
|
|
95
|
+
|
|
96
|
+
## Security boundary
|
|
97
|
+
|
|
98
|
+
`signOut()` calls made by this package (on host logout, on a failed identity-switch rebind)
|
|
99
|
+
are **client-side, best-effort UX**: they stop rendering the app and drop the local session,
|
|
100
|
+
but a persisted Supabase access token stays technically valid until it expires, and only this
|
|
101
|
+
browser is affected. **Enforced logout must happen server-side** — OIDC back-channel logout
|
|
102
|
+
revoking the Supabase refresh tokens (Supabase Admin API, per project). That back-channel
|
|
103
|
+
receiver is infrastructure outside this package.
|