Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,12 @@ Common usage patterns:

## Runtime Model

This package assumes cookie-based auth flows and a Seamless Auth-compatible backend.
This package assumes a Seamless Auth-compatible backend mounted under `/auth`.

`createFetchWithAuth()` is the shared request helper:

- it always sends `credentials: "include"`
- in `web` mode it targets `${authHost}/...`
- in `server` mode it targets `${authHost}/auth/...`

The common deployment shape is `server` mode against a backend that mounts the Seamless Auth routes under `/auth`.
- it targets `${authHost}/auth/...`

Important implication:

Expand All @@ -70,7 +67,6 @@ Exports from `src/index.ts` currently include:
Exported types currently include:

- `AuthContextType`
- `AuthMode`
- `Credential`
- `CurrentUserResult`
- `LoginInput`
Expand Down Expand Up @@ -111,7 +107,7 @@ The current package is organized around a shared SDK core with optional UI layer
- `src/components/*`
- reusable UI pieces for those bundled screens
- `src/fetchWithAuth.ts`
- mode-aware request construction
- `/auth` request construction
- `src/types.ts`
- shared user and credential types
- `tests/*`
Expand Down
7 changes: 3 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,8 @@ cp .env.example .env
```

> IMPORTANT
> Change the AUTH_MODE env to "web" NOT "server".
> Change the APP_ORIGIN env to `http://localhost:5173` to match vite
> This lets you authenticate between a web app and the auth server with no need for an API.
> Change the APP_ORIGIN env to `http://localhost:5173` to match vite.
> The React SDK talks to a server adapter mounted at `/auth`, not directly to API auth cookies.

### If docker and docker compose are avaliable

Expand Down Expand Up @@ -160,7 +159,7 @@ function ApplicationRoutes() {
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<AuthProvider apiHost="http://localhost:5312" mode="web">
<AuthProvider apiHost="http://localhost:5312">
<ApplicationRoutes />
</AuthProvider>
</BrowserRouter>
Expand Down
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- `useAuthClient()`
- `usePasskeySupport()`
- `hasScopedRole()` and `roleGrantsAccess()`
- types including `AuthMode`, `AuthContextType`, `Credential`, `User`, `OAuthProvider`, `StepUpStatus`, and the headless client input/result types
- types including `AuthContextType`, `Credential`, `User`, `OAuthProvider`, `StepUpStatus`, and the headless client input/result types

## Installation

Expand All @@ -42,7 +42,7 @@ import { AuthProvider } from '@seamless-auth/react';
import { BrowserRouter } from 'react-router-dom';

<BrowserRouter>
<AuthProvider apiHost="https://your.api" mode="server">
<AuthProvider apiHost="https://your.api">
<AppRoutes />
</AuthProvider>
</BrowserRouter>;
Expand Down Expand Up @@ -101,7 +101,6 @@ You are still responsible for your app’s route protection and redirects.
isAuthenticated: boolean;
loading: boolean;
apiHost: string;
mode: AuthMode;
hasSignedInBefore: boolean;
markSignedIn(): void;
hasRole(role: string): boolean | undefined;
Expand Down Expand Up @@ -221,7 +220,6 @@ import { createSeamlessAuthClient } from '@seamless-auth/react';

const authClient = createSeamlessAuthClient({
apiHost: 'https://your.api',
mode: 'server',
});

const prfSupported = await authClient.isPasskeyPrfSupported();
Expand Down Expand Up @@ -360,7 +358,6 @@ import { createSeamlessAuthClient } from '@seamless-auth/react';

const authClient = createSeamlessAuthClient({
apiHost: 'https://your.api',
mode: 'server',
});

const response = await authClient.login({
Expand Down Expand Up @@ -444,10 +441,9 @@ These are optional UI wrappers over the same SDK primitives the package now expo

## Backend Expectations

This package assumes a Seamless Auth-compatible backend with cookie-based auth flows.
This package assumes a Seamless Auth-compatible backend with the auth adapter mounted at `/auth`.

- In `web` mode, requests target `${apiHost}/...`
- In `server` mode, requests target `${apiHost}/auth/...`
- Requests target `${apiHost}/auth/...`
- `apiHost` may be provided with or without a trailing slash
- Requests are sent with `credentials: 'include'`
- `AuthProvider` validates the current session by calling `/users/me` on load
Expand Down
9 changes: 1 addition & 8 deletions src/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import React, {
useState,
} from 'react';

import { AuthMode } from './fetchWithAuth';
import { usePreviousSignIn } from './hooks/usePreviousSignIn';
import { hasScopedRole as rolesGrantScopedAccess } from './scopedRoles';

Expand All @@ -41,7 +40,6 @@ export interface AuthContextType {
apiHost: string;
markSignedIn: () => void;
hasSignedInBefore: boolean;
mode: AuthMode;
credentials: Credential[];
organizations: Organization[];
activeOrganization: Organization | null;
Expand Down Expand Up @@ -80,14 +78,12 @@ interface AuthProviderProps {
children: ReactNode;
apiHost: string;
autoDetectPreviousSignin?: boolean;
mode?: AuthMode;
}

export const AuthProvider: React.FC<AuthProviderProps> = ({
children,
apiHost,
autoDetectPreviousSignin = true,
mode = 'web',
}) => {
const [user, setUser] = useState<User | null>(null);
const [credentials, setCredentials] = useState<Credential[]>([]);
Expand All @@ -97,15 +93,13 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const { hasSignedInBefore, markSignedIn } = usePreviousSignIn();
const authMode = mode;

const authClient = useMemo(
() =>
createSeamlessAuthClient({
mode: authMode,
apiHost,
}),
[authMode, apiHost]
[apiHost]
);

const login = async (
Expand Down Expand Up @@ -333,7 +327,6 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
apiHost,
markSignedIn,
hasSignedInBefore: autoDetectPreviousSignin ? hasSignedInBefore : false,
mode: authMode,
credentials,
organizations,
activeOrganization,
Expand Down
4 changes: 1 addition & 3 deletions src/client/createSeamlessAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
WebAuthnError,
} from '@simplewebauthn/browser';

import { AuthMode, createFetchWithAuth } from '../fetchWithAuth';
import { createFetchWithAuth } from '../fetchWithAuth';
import { Credential, Organization, OrganizationMembership, User } from '../types';
import {
createPrfRequestBody,
Expand All @@ -27,7 +27,6 @@ import {

export interface SeamlessAuthClientOptions {
apiHost: string;
mode: AuthMode;
}

export interface LoginInput {
Expand Down Expand Up @@ -287,7 +286,6 @@ export const createSeamlessAuthClient = (
opts: SeamlessAuthClientOptions
): SeamlessAuthClient => {
const fetchWithAuth = createFetchWithAuth({
authMode: opts.mode,
authHost: opts.apiHost,
});

Expand Down
8 changes: 2 additions & 6 deletions src/fetchWithAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,21 @@
* See LICENSE file in the project root for full license information
*/

export type AuthMode = 'web' | 'server';

interface FetchWithAuthOptions {
authMode: AuthMode;
authHost?: string;
}

export const createFetchWithAuth = (opts: FetchWithAuthOptions) => {
const { authMode, authHost } = opts;
const { authHost } = opts;

return async function fetchWithAuth(
input: string,
init?: RequestInit
): Promise<Response> {
const host = authHost?.replace(/\/+$/, '') ?? '';
const base = authMode === 'server' ? '/auth' : '';
const path = input.startsWith('/') ? input : `/${input}`;

const url = `${host}${base}${path}`;
const url = `${host}/auth${path}`;

const requestInit: RequestInit = {
...init,
Expand Down
5 changes: 2 additions & 3 deletions src/hooks/useAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import { useAuth } from '@/AuthProvider';
import { createSeamlessAuthClient } from '@/client/createSeamlessAuthClient';

export const useAuthClient = () => {
const { apiHost, mode } = useAuth();
const { apiHost } = useAuth();

return useMemo(
() =>
createSeamlessAuthClient({
apiHost,
mode,
}),
[apiHost, mode]
[apiHost]
);
};
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import {
PasskeyPrfInput,
PasskeyPrfResult,
} from '@/client/webauthnPrf';
import { AuthMode } from '@/fetchWithAuth';
import { useAuthClient } from '@/hooks/useAuthClient';
import { usePasskeySupport } from '@/hooks/usePasskeySupport';
import { hasScopedRole, roleGrantsAccess } from '@/scopedRoles';
Expand All @@ -65,7 +64,6 @@ export {
};
export type {
AuthContextType,
AuthMode,
Credential,
CreateOrganizationInput,
CurrentUserResult,
Expand Down
13 changes: 0 additions & 13 deletions tests/createSeamlessAuthClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'server',
});

await expect(
Expand All @@ -72,7 +71,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'server',
});

await expect(client.requestLoginPhoneOtp()).resolves.toBe(response);
Expand Down Expand Up @@ -102,7 +100,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'server',
});

await expect(client.requestLoginEmailOtp()).resolves.toBe(response);
Expand Down Expand Up @@ -130,7 +127,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'server',
});

await expect(client.listOrganizations()).resolves.toBe(response);
Expand Down Expand Up @@ -180,7 +176,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'server',
});

await expect(client.listOAuthProviders()).resolves.toEqual(providersResult);
Expand Down Expand Up @@ -229,7 +224,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'web',
});

await expect(client.loginWithPasskey()).resolves.toEqual({
Expand Down Expand Up @@ -278,7 +272,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'web',
});

const result = await client.loginWithPasskey({
Expand Down Expand Up @@ -309,7 +302,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'web',
});

await expect(
Expand Down Expand Up @@ -343,7 +335,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'web',
});

await expect(
Expand Down Expand Up @@ -395,7 +386,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'server',
});

await expect(client.getStepUpStatus()).resolves.toBe(response);
Expand Down Expand Up @@ -426,7 +416,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'web',
});

await expect(client.verifyStepUpWithPasskey()).resolves.toEqual({
Expand All @@ -453,7 +442,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'web',
});

await expect(client.verifyStepUpWithPasskey()).resolves.toEqual({
Expand Down Expand Up @@ -515,7 +503,6 @@ describe('createSeamlessAuthClient', () => {

const client = createSeamlessAuthClient({
apiHost: 'https://api.example.com',
mode: 'web',
});

const result = await client.verifyStepUpWithPasskeyPrf({ salt });
Expand Down
Loading
Loading