diff --git a/.changeset/client-workspace.md b/.changeset/client-workspace.md new file mode 100644 index 0000000..07b6236 --- /dev/null +++ b/.changeset/client-workspace.md @@ -0,0 +1,12 @@ +--- +'@seamless-auth/client': minor +'@seamless-auth/react': patch +--- + +Split the framework-agnostic core into `@seamless-auth/client`, and make this repository an npm workspace that publishes both packages. + +`@seamless-auth/react` was one package holding two layers: the headless client, session store, and result types that any binding needs, and the React provider, hooks, and screens on top. A React Native binding is next, and it must share the first layer rather than copy it, since the session state machine is the worst place for two implementations to drift (#64). + +`@seamless-auth/client` now carries `createSeamlessAuthClient`, `createAuthSession`, `SessionStoragePort` and its implementations, `createFetchWithAuth`, the error and result types, the PRF helpers, role matching, and the wire type aliases. `@seamless-auth/react` depends on it and re-exports the same public surface it did before, so an application installing `@seamless-auth/react` sees no change in what it imports or how it behaves. + +Packaging only: no runtime behaviour changes in either package. diff --git a/AGENTS.md b/AGENTS.md index 2290632..f863936 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,15 +101,15 @@ Important implication: ## Wire Types Request and response shapes come from `@seamless-auth/types`, which is generated -from the auth API's schemas. `src/types.ts` and the type declarations in -`src/client/createSeamlessAuthClient.ts` alias that package rather than -redeclaring shapes. +from the auth API's schemas. `packages/client/src/types.ts` and the type +declarations in `packages/client/src/client/createSeamlessAuthClient.ts` alias +that package rather than redeclaring shapes. Rules for this dependency: - types only. Import with `import type` so the package's Zod dependency never reaches the browser bundle. There is a `Record` in - `src/client/errors.ts` that exists for exactly this reason: it is a + `packages/client/src/client/errors.ts` that exists for exactly this reason: it is a compile-time membership check standing in for the upstream runtime list. - keep the SDK's own export names. Adopters import `Credential` from this package, so alias upstream shapes to local names instead of re-exporting @@ -127,8 +127,10 @@ not wire contracts. ## Current Public API -`src/index.ts` is the authoritative export list. Treat the enumeration below as a -summary and re-check `src/index.ts` before relying on it. +`packages/react/src/index.ts` is the authoritative export list for +`@seamless-auth/react`, and `packages/client/src/index.ts` for +`@seamless-auth/client`. Treat the enumeration below as a summary and re-check +those files before relying on it. Runtime exports currently include: @@ -164,13 +166,22 @@ domain models, for example: Public API changes should be treated deliberately: -- if something is not exported from `src/index.ts`, it is not public +- if something is not exported from a package's `src/index.ts`, it is not public - once something is exported, it should be supportable and documented - built-in UI should consume public primitives whenever practical instead of reaching into private helpers ## Current Architecture -The current package is organized around a shared SDK core with optional UI layered on top: +This repository is an npm workspace with two published packages: + +- `packages/client`, published as `@seamless-auth/client`: the framework-agnostic + core every binding shares. No React, no router, no DOM types beyond what + `fetch` and WebAuthn JSON need. Lint-enforced (`no-restricted-imports` in + `eslint.config.mjs`). +- `packages/react`, published as `@seamless-auth/react`: the React binding, + hooks, and the optional prebuilt screens. Depends on `@seamless-auth/client`. + +`@seamless-auth/client`: - `src/session/createAuthSession.ts` - framework-agnostic session store: `getState`, `subscribe`, `actions`, `destroy` @@ -179,12 +190,22 @@ The current package is organized around a shared SDK core with optional UI layer - `src/session/storage.ts` - `SessionStoragePort` plus browser, memory, and default implementations - the store's only browser dependency, which is what keeps it SSR safe -- `src/AuthProvider.tsx` - - React binding over the session store, via `useSyncExternalStore` - - exposes the main provider context and holds no session state of its own - `src/client/createSeamlessAuthClient.ts` - shared headless auth client - contains the backend request choreography for login, registration, OTP, magic-link, passkey flows, and credential mutations +- `src/client/errors.ts`, `src/client/result.ts`, `src/client/webauthnPrf.ts`, `src/client/webauthnSupport.ts` +- `src/fetchWithAuth.ts` + - `/auth` request construction +- `src/scopedRoles.ts` + - role matching, kept byte-for-byte with `@seamless-auth/types/role/matching` +- `src/types.ts` + - aliases of the wire contract in `@seamless-auth/types`, not hand-written shapes + +`@seamless-auth/react`: + +- `src/AuthProvider.tsx` + - React binding over the session store, via `useSyncExternalStore` + - exposes the main provider context and holds no session state of its own - `src/hooks/useAuthClient.ts` - creates a memoized client from provider configuration - `src/hooks/usePasskeySupport.ts` @@ -192,25 +213,28 @@ The current package is organized around a shared SDK core with optional UI layer - `src/AuthRoutes.tsx` - bundles the prebuilt auth route flow - `src/views/*` - - bundled route screens that now consume the public provider/client layer + - bundled route screens that consume the public provider/client layer - `src/components/*` - reusable UI pieces for those bundled screens -- `src/fetchWithAuth.ts` - - `/auth` request construction -- `src/types.ts` - - aliases of the wire contract in `@seamless-auth/types`, not hand-written shapes -- `tests/*` - - Jest + Testing Library coverage for provider, client, hooks, and views +- `src/utils.ts` + - browser-only helpers (`parseUserAgent`) and validators the screens use + +Tests live in each package's `tests/` directory and run as two Jest projects +from the root (`npm test`). The React project maps `@seamless-auth/client` to the +client package's source, so a change in the core is exercised by the React suite +without a build in between. `packages/react/tsconfig.json` carries the same path +mapping for type-checking; `tsconfig.build.json` drops it so the emitted +declarations reference the package by name. Important architectural reality: - the internal-only auth context path is gone -- built-in screens now use public primitives instead of hidden refresh helpers -- the session state machine lives in `src/session`, not in the provider. It is - lint-enforced framework agnostic, so keep React and router imports out of it. - This is phase 1 of #64: the store stays in this repo and unexported until a - second binding exists to validate its API -- remaining work is mostly docs, examples, and incremental polish rather than major extraction plumbing +- built-in screens use public primitives instead of hidden refresh helpers +- the session state machine lives in `@seamless-auth/client`, not in the + provider. Keep React, router, and React Native imports out of that package +- this is phase 3 of #64: the workspace conversion. Ports for a native binding + (transport, token storage, passkeys, OAuth redirect) and the + `@seamless-auth/react-native` package follow as separate changes ## Backend Endpoints Assumed By The SDK @@ -259,7 +283,7 @@ configured, so treat the refusal as reachable rather than exceptional. `@seamless-auth/types` 0.16.0 publishes `WebAuthnErrorCode`, which covers every machine code the API sends for WebAuthn across all of its operations, so -`PasskeyPolicyErrorCode` in `src/client/errors.ts` is derived from it rather than +`PasskeyPolicyErrorCode` in `packages/client/src/client/errors.ts` is derived from it rather than kept as a local list. It is that union minus `prf_output_not_allowed`, a `400` from login and step-up finish that reports a caller which failed to strip PRF output, not a deployment refusing an authenticator. @@ -293,9 +317,9 @@ That means future work should usually build on the current public surface rather Bias toward these patterns: - add reusable behavior to the headless client first, then expose it through React hooks or provider helpers as needed -- keep the session store in `src/session` as the source of truth for auth/session state, and keep `AuthProvider` a thin binding over it +- keep the session store in `packages/client/src/session` as the source of truth for auth/session state, and keep `AuthProvider` a thin binding over it - use `refreshSession()` when custom flows need to synchronize provider state after a successful auth step -- export types intentionally from `src/index.ts` +- export types intentionally from each package's `src/index.ts` - keep built-in views thin and aligned with public APIs - update README and adjacent docs when the supported contract changes diff --git a/README.md b/README.md index 2a06f95..f2cddb7 100644 --- a/README.md +++ b/README.md @@ -1,1125 +1,33 @@ -# @seamless-auth/react +# Seamless Auth client SDKs -[![npm version](https://img.shields.io/npm/v/@seamless-auth/react.svg?label=%40seamless-auth%2Freact)](https://www.npmjs.com/package/@seamless-auth/react) -[![CI](https://github.com/fells-code/seamless-auth-react/actions/workflows/ci.yml/badge.svg)](https://github.com/fells-code/seamless-auth-react/actions/workflows/ci.yml) -[![Release](https://github.com/fells-code/seamless-auth-react/actions/workflows/release.yml/badge.svg)](https://github.com/fells-code/seamless-auth-react/actions/workflows/release.yml) -[![coverage](https://img.shields.io/codecov/c/github/fells-code/seamless-auth-react)](https://app.codecov.io/gh/fells-code/seamless-auth-react) -[![license](https://img.shields.io/github/license/fells-code/seamless-auth-react)](./LICENSE) +This repository is an npm workspace that publishes the client-side packages for +[Seamless Auth](https://github.com/fells-code/seamless-auth-api): -`@seamless-auth/react` is a React SDK for Seamless Auth. It gives you a provider for auth state, a headless client and hooks for custom auth UIs, and optional prebuilt auth routes when you want a faster drop-in flow. +| Package | What it is | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [`@seamless-auth/client`](packages/client/README.md) | Framework-agnostic core: the headless auth client, session store, result and error types. | +| [`@seamless-auth/react`](packages/react/README.md) | React binding: `AuthProvider`, hooks, and optional prebuilt auth screens. Depends on the client package. | -## What It Exports +Most React applications only install `@seamless-auth/react`; it brings the client +core with it. The client package exists so that other bindings (React Native +next) share one implementation of the auth flows and session state instead of +re-implementing them. -- `AuthProvider` -- `AuthRoutes` -- `useAuth()` -- `createSeamlessAuthClient()` -- `useAuthClient()` -- `usePasskeySupport()` -- `hasScopedRole()` and `roleGrantsAccess()` -- `SeamlessAuthError`, the error type carried on a failed result -- `getOAuthErrorCode()`, which reads the known OAuth callback failure codes off that error -- `getWebAuthnErrorDetail()`, which reads the underlying failure of a passkey or step-up ceremony -- `getPasskeyPolicyErrorCode()`, which reads the code the API refused a passkey registration with -- types including `AuthContextType`, `Credential`, `User`, `OAuthProvider`, `StepUpStatus`, the `SeamlessAuthResult` wrapper, and the headless client input/result types - -## Installation +## Working in this repository ```bash -npm install @seamless-auth/react -``` - -## Releases - -Published versions are listed in [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases. Releases are -managed with Changesets: adopter-facing changes include a changeset, the `Release` workflow opens a -version PR for review, and merging that PR publishes the npm package with provenance from GitHub -Actions. See [RELEASES.md](./RELEASES.md) for maintainer release details. - -## Choose Your Integration Style - -You can use this package in three ways: - -1. `AuthProvider` + `useAuth()` for auth state and core auth actions -2. `createSeamlessAuthClient()` or `useAuthClient()` to build fully custom login and registration screens -3. `AuthRoutes` when you want the built-in login, OTP, magic-link, and passkey screens - -Most apps will use `AuthProvider` either way. - -## Quick Start - -### Wrap your app with `AuthProvider` - -```tsx -import { AuthProvider } from '@seamless-auth/react'; -import { BrowserRouter } from 'react-router-dom'; - - - - - -; -``` - -### Read auth state with `useAuth()` - -```tsx -import { useAuth } from '@seamless-auth/react'; - -function Dashboard() { - const { user, logout, refreshSession } = useAuth(); - - return ( -
-

Welcome, {user?.email}

- - -
- ); -} -``` - -### Use built-in auth routes with `AuthRoutes` - -```tsx -import { AuthRoutes, useAuth } from '@seamless-auth/react'; -import { Route, Routes } from 'react-router-dom'; - -function AppRoutes() { - const { isAuthenticated } = useAuth(); - - return ( - - {isAuthenticated ? ( - } /> - ) : ( - } /> - )} - - ); -} -``` - -You are still responsible for your app’s route protection and redirects. - -## `useAuth()` API - -`useAuth()` returns the current auth state plus the provider-backed helpers: - -```ts -{ - user: User | null; - credentials: Credential[]; - stepUpStatus: StepUpStatus | null; - isAuthenticated: boolean; - loading: boolean; - apiHost: string; - hasSignedInBefore: boolean; - markSignedIn(): void; - hasRole(role: string): boolean | undefined; - hasScopedRole(role: string | string[]): boolean | undefined; - listOAuthProviders(): Promise>; - startOAuthLogin(input: StartOAuthLoginInput): Promise>; - finishOAuthLogin(input: FinishOAuthLoginInput): Promise>; - refreshSession(): Promise>; - refreshStepUpStatus(): Promise>; - verifyStepUpWithPasskey(): Promise>; - verifyStepUpWithPasskeyPrf(input: PasskeyPrfInput): Promise>; - verifyStepUpWithTotp(code: string): Promise>; - logout(): Promise>; - logoutAllSessions(): Promise>; - deleteUser(): Promise>; - login(identifier: string, passkeyAvailable: boolean): Promise>; - handlePasskeyLogin(): Promise>; - updateCredential(credential: Credential): Promise>; - deleteCredential(credentialId: string): Promise>; -} -``` - -Use `refreshSession()` after completing a custom auth flow that should update provider state. - -### `hasSignedInBefore` - -`hasSignedInBefore` is a small convenience flag backed by `localStorage`. The provider reads the `seamlessauth_seen` key on load and sets the flag to `true` after `markSignedIn()` runs. - -This is mainly useful for login UIs that want to branch between first-time and returning-user behavior. For example, the built-in `Login` view uses it to default returning users to sign-in mode instead of registration. - -```tsx -import { useAuth } from '@seamless-auth/react'; - -function SignInHint() { - const { hasSignedInBefore } = useAuth(); - - return hasSignedInBefore ? ( -

Welcome back. Sign in with your email, phone, or passkey.

- ) : ( -

New here? Start by creating your account.

- ); -} -``` - -If you are building a fully custom flow, call `markSignedIn()` after a successful sign-in or registration step once you want future visits treated as returning-user sessions. - -```tsx -const { markSignedIn, refreshSession } = useAuth(); - -async function completeLogin() { - const { error } = await authClient.login({ - identifier: 'user@example.com', - passkeyAvailable: true, - }); - - if (!error) { - markSignedIn(); - await refreshSession(); - } -} -``` - -To disable this auto-detection entirely, pass `autoDetectPreviousSignin={false}` to `AuthProvider`. - -### Magic link destination - -By default a magic link lands wherever the deployment is configured to send it. A deployment serving -more than one front end can override that per application with `magicLinkRedirectUri`: - -```tsx - - - -``` - -Every magic link the bundled screens send uses it, including the resend on the "check your email" -screen, so a resent link always lands where the first one did. The deployment validates the value -against its configured origins and refuses anything else, which comes back as an ordinary error -result. - -Custom UIs get the same default through `useAuthClient()`, and can still override a single send with -`requestMagicLink(uri)`. - -### Scoped roles - -`hasRole(role)` remains an exact role check. Use `hasScopedRole(role)` for colon-separated scoped -roles such as `admin:read` and `admin:write`. - -```tsx -const { hasRole, hasScopedRole } = useAuth(); - -hasRole('admin'); // exact legacy role check -hasScopedRole('admin:read'); // true for admin, admin:read, or admin:write -hasScopedRole('admin:write'); // true for admin or admin:write -``` - -The package also exports standalone `hasScopedRole(roles, required)` and `roleGrantsAccess(...)` -helpers for code that is not inside `AuthProvider`. - -### Step-up authentication - -Use step-up authentication before sensitive actions that should require a fresh user verification, such as deleting an account, changing MFA settings, or viewing recovery material. - -```tsx -import { useAuth } from '@seamless-auth/react'; - -function DeleteAccountButton() { - const { refreshStepUpStatus, verifyStepUpWithPasskey } = useAuth(); - - async function handleDeleteAccount() { - const { data: status } = await refreshStepUpStatus(); - const fresh = status?.fresh ? true : !(await verifyStepUpWithPasskey()).error; - - if (!fresh) { - return; - } - - await deleteAccount(); - } - - return ; -} -``` - -Step-up supports WebAuthn/passkeys and TOTP (authenticator apps). `refreshStepUpStatus()` calls `/step-up/status`, `verifyStepUpWithPasskey()` performs the `/step-up/webauthn/start` and `/step-up/webauthn/finish` challenge flow, and `verifyStepUpWithTotp(code)` verifies a 6-digit authenticator code via `/totp/verify-mfa`. The verification helpers return a `SeamlessAuthResult` and refresh the provider's `stepUpStatus` when they succeed. - -```tsx -const { verifyStepUpWithTotp } = useAuth(); - -const { error } = await verifyStepUpWithTotp('123456'); // 6-digit code from the authenticator app -if (!error) { - // step-up is fresh; proceed with the sensitive action -} -``` - -### TOTP (authenticator apps) - -TOTP lets users register an authenticator app (Google Authenticator, 1Password, etc.) as a second factor for step-up verification. The SDK exposes headless client methods for enrollment and management; use them from a settings screen. All require an authenticated session. - -```ts -import { createSeamlessAuthClient } from '@seamless-auth/react'; -import type { TotpStatus, TotpEnrollmentStartResult } from '@seamless-auth/react'; - -const authClient = createSeamlessAuthClient({ apiHost: 'https://your.api' }); - -// 1. Check whether TOTP is already enabled -const { data: status } = await authClient.getTotpStatus(); - -// 2. Start enrollment: render `otpauthUrl` as a QR code (or show `secret` for manual entry) -const { data: enrollment } = await authClient.startTotpEnrollment(); - -// 3. Confirm the first code from the user's authenticator app -const { error } = await authClient.verifyTotpEnrollment('123456'); -if (!error) { - // TOTP is now enabled -} - -// Disabling requires a current code -await authClient.disableTotp('123456'); -``` - -These methods follow the standard result convention: check `error`, then read `data`. Enrolling TOTP is a sensitive change; gate it behind a fresh step-up when appropriate. - -> TOTP is not currently a login second factor. The Seamless Auth API issues a full session on the first factor and does not gate login on TOTP, so TOTP applies to step-up verification, not to the login flow. - -### WebAuthn PRF - -WebAuthn PRF lets a compatible passkey and browser derive local key material during a WebAuthn assertion. Seamless Auth verifies the passkey assertion on the server, while the React SDK returns the PRF output only to the browser caller. PRF output is stripped before `/webAuthn/login/finish` and `/step-up/webauthn/finish`, and should never be logged, stored, or sent to your API. - -Browser and authenticator support is not universal. Call `isPasskeyPrfSupported()` before offering PRF-required flows, and keep a fallback for passkeys that authenticate successfully without returning PRF output. - -Treat PRF salts as sensitive in client logs. PRF output is browser-local key material; keep it in -memory only as long as your application needs it and do not send it to Seamless Auth or your own API. - -```ts -import { createSeamlessAuthClient } from '@seamless-auth/react'; - -const authClient = createSeamlessAuthClient({ - apiHost: 'https://your.api', -}); - -const prfSupported = await authClient.isPasskeyPrfSupported(); - -if (prfSupported) { - await authClient.registerPasskey({ - metadata: { - friendlyName: 'My laptop', - platform: 'macOS', - browser: 'Chrome', - deviceInfo: navigator.userAgent, - }, - requirePrf: true, - }); -} -``` - -For local key unwrap flows such as Seamless Secrets, use PRF during step-up and consume the returned bytes in browser memory: - -```ts -const { data, error } = await authClient.verifyStepUpWithPasskeyPrf({ - salt: vaultSaltBase64url, - credentialId, -}); - -if (error) { - throw error; -} - -const vaultUnlockMaterial: { credentialId: string; output: Uint8Array } = { - credentialId: data.credentialId, - output: data.prf.output, -}; -``` - -The salt may be an `ArrayBuffer`, `ArrayBufferView`, or base64url string. Authentication proves identity and user presence; the PRF output is local key material for your application to use without sending it to Seamless Auth. - -### OAuth Login - -OAuth lets your app offer external identity providers such as Google, GitHub, Facebook, or custom -OIDC-style providers configured on the Seamless Auth API. The React SDK does not receive provider -access tokens. It only starts the provider redirect and completes the callback so Seamless Auth can -issue the normal access/refresh session. - -Use `listOAuthProviders()` when you want to render enabled providers dynamically: - -```tsx -import { useEffect, useState } from 'react'; -import { useAuth } from '@seamless-auth/react'; -import type { OAuthProvider } from '@seamless-auth/react'; - -function OAuthButtons() { - const { listOAuthProviders, startOAuthLogin } = useAuth(); - const [providers, setProviders] = useState([]); - - useEffect(() => { - void listOAuthProviders().then(result => setProviders(result.providers)); - }, [listOAuthProviders]); - - async function signIn(providerId: string) { - const result = await startOAuthLogin({ - providerId, - redirectUri: `${window.location.origin}/oauth/callback`, - returnTo: `${window.location.origin}/dashboard`, - }); - - window.location.assign(result.authorizationUrl); - } - - return ( -
- {providers.map(provider => ( - - ))} -
- ); -} -``` - -Create a callback route that reads the provider query params and asks Seamless Auth to complete the -login: - -```tsx -import { useEffect } from 'react'; -import { useAuth } from '@seamless-auth/react'; - -function OAuthCallback() { - const { finishOAuthLogin } = useAuth(); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - // Persist the provider you passed to startOAuthLogin so the callback knows - // which provider to finish. The built-in AuthRoutes flow stores this in - // sessionStorage; use whatever your custom start flow saved. - const providerId = sessionStorage.getItem('seamless:oauth:provider'); - const code = params.get('code'); - const state = params.get('state'); - - if (!providerId || !code || !state) { - return; - } - - void finishOAuthLogin({ providerId, code, state }).then(() => { - window.location.assign('/dashboard'); - }); - }, [finishOAuthLogin]); - - return

Finishing sign-in...

; -} -``` - -Some callback failures are the user's to fix, so the API returns a stable `code` alongside the error -message. `getOAuthErrorCode()` narrows it to the codes this SDK knows about and returns `undefined` -for everything else, so unexpected failures keep your generic message: - -```tsx -import { getOAuthErrorCode, useAuth } from '@seamless-auth/react'; - -const { error } = await finishOAuthLogin({ providerId, code, state }); - -switch (getOAuthErrorCode(error)) { - case 'oauth_missing_email': - // The provider account shared no email address. - break; - case 'oauth_email_not_verified': - // The provider account's email is unverified. - break; - case 'oauth_missing_subject': - // The provider returned no usable account identifier. - break; - default: - // No error, or one without a recognized code. - break; -} +npm install +npm run lint +npm test +npm run build ``` -The bundled `AuthRoutes` callback screen already maps these three codes to actionable text. - -For fully custom UI without `useAuth()`, call the headless client directly: - -```ts -const providers = await authClient.listOAuthProviders(); -const started = await authClient.startOAuthLogin({ - providerId: providers.providers[0].id, - redirectUri: `${window.location.origin}/oauth/callback`, -}); - -window.location.assign(started.authorizationUrl); -``` - -OAuth must be enabled on the Seamless Auth API with `LOGIN_METHODS` including `oauth` and at least -one configured `oauth_providers` entry. Provider client secrets live on the server and are referenced -by environment variable name; they are never passed through this SDK. - -For production providers, configure exact `redirectUris` on the Seamless Auth API. The SDK should -send the callback URL it expects to receive, but redirect allowlisting, signed state expiry, OIDC -nonce handling, email verification policy, and account-linking policy are enforced by the API. - -The built-in views avoid logging OTPs, magic-link tokens, PRF salts, or raw -exception payloads that may contain sensitive request URLs. - -## Headless Client - -For custom auth UIs, use the exported client directly: - -```ts -import { createSeamlessAuthClient } from '@seamless-auth/react'; - -const authClient = createSeamlessAuthClient({ - apiHost: 'https://your.api', -}); - -const { data, error } = await authClient.login({ - identifier: 'user@example.com', - passkeyAvailable: true, -}); - -if (error) { - // error.message, error.status, and error.body carry the server detail - return; -} - -// data is typed as LoginStartResult -console.log(data.loginMethods); -``` - -The headless client exposes helpers for: - -- current-user/session lookup -- login and passkey login -- registration -- phone OTP and email OTP -- magic-link request, verify, and polling -- OAuth provider listing, start, and callback completion -- passkey registration -- step-up status, passkey verification, and TOTP verification -- TOTP enrollment, status, and disable -- logout and delete-user -- credential update and deletion - -### Where the response types come from - -The request and response types are aliases of -[`@seamless-auth/types`](https://www.npmjs.com/package/@seamless-auth/types), which is generated from -the auth API's schemas. `User`, `Credential`, `Organization`, `StepUpStatus`, `MessageResult`, and the -other wire shapes describe what the API actually sends, rather than a second copy maintained here that -could drift from it. - -The dependency is types-only. Nothing from it is imported at runtime, so no schema validation library -reaches your bundle. Names exported from this package stay the SDK's own, so you keep importing -`Credential` from `@seamless-auth/react`. - -Two SDK concerns are deliberately not shared, because they are not wire contracts: the PRF helper -types and the `SeamlessAuthResult` wrapper. - -### Result convention - -Every request method resolves to a `SeamlessAuthResult`: - -```ts -type SeamlessAuthResult = - | { data: T; error: null } - | { data: null; error: SeamlessAuthError }; -``` - -Check `error` first, then read `data`. TypeScript enforces this: `data` is not readable until the -error has been ruled out. - -```ts -const { data, error } = await authClient.getCurrentUser(); - -if (error) { - console.log(error.message, error.status, error.body); - return; -} - -setUser(data.user); // typed as CurrentUserResult -``` - -Nothing throws for an HTTP failure, and transport failures are absorbed too, reported as an error -with `status` `0`. That means an expected auth outcome such as a wrong OTP, an expired magic link, or -a disabled provider is a value you can map straight to UI state rather than an exception to catch. - -`SeamlessAuthError` carries the server's `message`, the HTTP `status`, and the parsed response -`body`, so you can branch on a specific failure. - -### WebAuthn ceremony failures - -A passkey or step-up ceremony can fail in the browser before any request is sent, so those results -carry the thrown error as `cause` with `status` `0`. Use `getWebAuthnErrorDetail()` to read it: the -`name` is the `DOMException` name that separates the cases a user can act on, and `code` is -SimpleWebAuthn's narrower reason when it identified one. - -```ts -import { getWebAuthnErrorDetail } from '@seamless-auth/react'; - -const { error } = await authClient.verifyStepUpWithPasskey(); -const detail = getWebAuthnErrorDetail(error); - -switch (detail?.name) { - case 'NotAllowedError': - // The prompt was dismissed, or the account has no passkey to assert. - break; - case 'SecurityError': - // The origin or RP ID does not match what the API is configured for. - break; - case 'InvalidStateError': - // This authenticator already holds a passkey for the account. - break; - default: - // Not a ceremony failure. Fall back to error?.message. - break; -} -``` - -`getWebAuthnErrorDetail()` returns `undefined` for any error that did not come from a ceremony, so an -HTTP failure keeps flowing through `error.message` and `error.body` as usual. - -### Choosing the authenticator - -By default the browser offers every kind of authenticator the deployment enrols, which is what -`authenticator_policy.attachment: 'any'` means on the API. Pass `attachment` to narrow the picker to -one kind, for example to send someone straight to an issued security key rather than leaving them to -find it in a browser dialog: - -```ts -import { getPasskeyPolicyErrorCode } from '@seamless-auth/react'; - -const { error } = await authClient.registerPasskey({ - metadata, - attachment: 'cross-platform', -}); - -if (getPasskeyPolicyErrorCode(error) === 'attachment_not_allowed') { - // This deployment pins the other kind. Fall back to the default path. -} -``` - -`'cross-platform'` is a roaming authenticator such as a USB or NFC security key. `'platform'` is the -one built into the device, such as Touch ID or Windows Hello. Omit the option to leave the choice to -the deployment. - -This is a request, not an override. A deployment that has pinned -`authenticator_policy.attachment` to the other kind refuses the registration with -`attachment_not_allowed`, covered below. The bundled enrolment view offers a "Use a security key -instead" control that takes this path. - -### Passkey policy refusals - -A registration can also be refused by the policy the API is configured with. `registerPasskey()` -then fails with a body whose `error` is a stable code rather than a sentence, so rendering -`error.message` would put that code in front of a user. Use `getPasskeyPolicyErrorCode()` to branch -on it: - -```ts -import { getPasskeyPolicyErrorCode } from '@seamless-auth/react'; - -const { error } = await authClient.registerPasskey({ metadata }); - -switch (getPasskeyPolicyErrorCode(error)) { - case 'attachment_not_allowed': - // The requested `attachment` is not the kind this deployment enrols. - break; - case 'synced_passkey_not_allowed': - // This passkey syncs to iCloud Keychain or Google Password Manager, and - // this deployment requires a device-bound one such as a security key. - break; - case 'authenticator_not_allowed': - // This authenticator model is not permitted here. - break; - case 'prf_required': - // Registration asked for PRF and the authenticator does not support it. - break; - default: - // No error, or one without a recognized code. Fall back to error?.message. - break; -} -``` - -| Code | Stage | Status | When the API sends it | -| ---------------------------- | --------------- | ------ | -------------------------------------------------------------------------------------------- | -| `attachment_not_allowed` | register/start | 400 | the requested `attachment` is not the kind `authenticator_policy.attachment` pins | -| `synced_passkey_not_allowed` | register/finish | 403 | `authenticator_policy.syncedPasskeys` is `block` and the credential is backup eligible | -| `authenticator_not_allowed` | register/finish | 403 | the credential's AAGUID is on `aaguidDenyList`, or absent from a non-empty `aaguidAllowList` | -| `prf_required` | register/finish | 403 | registration required PRF and the credential did not report support for it | - -`attachment_not_allowed` is refused before any ceremony runs, so the browser never prompts. The rest -are refused after a credential exists and can be inspected. - -`syncedPasskeys` defaults to `allow` on the Seamless Auth API, so a default deployment enrols the -passkeys iCloud Keychain and Google Password Manager create. A deployment that issues its own -authenticators can set `authenticator_policy.syncedPasskeys` to `block` in the API's system config, -and every backup-eligible passkey is then refused at registration. Handle the code: the SDK cannot -tell from the client which way the API is configured. - -Like `getOAuthErrorCode()`, this returns `undefined` for anything it does not recognize, including -codes added by a newer API, so an unexpected refusal keeps your generic messaging. - -The single exception is `isPasskeySupported`-style capability checks: -`isPasskeyPrfSupported(): Promise` is a local check rather than a request, so it returns a -plain boolean. - -## React Hooks For Custom UI - -If you want custom React screens but do not want to manually recreate the client, use the exported hooks: - -```tsx -import { useAuth, useAuthClient, usePasskeySupport } from '@seamless-auth/react'; - -function CustomLogin() { - const { refreshSession } = useAuth(); - const authClient = useAuthClient(); - const { passkeySupported, loading } = usePasskeySupport(); - - async function handleEmailLogin() { - const { error } = await authClient.login({ - identifier: 'user@example.com', - passkeyAvailable: passkeySupported, - }); - - if (!error) { - await refreshSession(); - } - } - - return ( - - ); -} -``` - -### One error style everywhere - -`useAuth()` helpers and the headless client report failure the same way: both return -`{ data, error }` and neither throws. Whatever surface you reach for, the handling is identical. - -```tsx -const { error } = await updateCredential({ ...credential, friendlyName: 'Work laptop' }); - -if (error) { - setMessage(error.message); -} -``` - -Helpers that also mutate provider state, such as `switchOrganization` and `deleteCredential`, apply -that state change only when the call succeeds, then hand the result back for you to inspect. - -## Custom UI Recipes - -Worked examples for the flows the bundled screens cover, using only public primitives. - -### Custom registration - -Registration is two steps: create the account, then verify the emailed code. Call `markSignedIn()` -once the account is live so returning visits can default to sign-in. - -```tsx -import { useAuth, useAuthClient } from '@seamless-auth/react'; -import { useState } from 'react'; - -function CustomRegistration() { - const { markSignedIn, refreshSession } = useAuth(); - const authClient = useAuthClient(); - const [step, setStep] = useState<'details' | 'verify'>('details'); - const [message, setMessage] = useState(''); - - async function createAccount(email: string) { - // Registration needs only an email. A phone can be added and verified later. - const { error } = await authClient.register({ email }); - - if (error) { - setMessage(error.message); - return; - } - - // The API emails a verification code as part of registering. - setStep('verify'); - } - - async function verifyCode(code: string) { - const { error } = await authClient.verifyEmailOtp(code); - - if (error) { - setMessage(error.message); - return; - } - - markSignedIn(); - await refreshSession(); - } - - return step === 'details' ? ( - - ) : ( - authClient.requestEmailOtp()} - error={message} - /> - ); -} -``` - -Enrolment takes the signed-in session, so it comes after the step that establishes one. -Verifying the email OTP signs the user in, which is why the bundled flow offers a passkey -on the screen after it rather than before. - -To offer a passkey right after registering, call `registerPasskey()` before `refreshSession()`: - -```ts -const { data, error } = await authClient.registerPasskey({ - friendlyName: 'My laptop', - platform: 'macOS', - browser: 'Chrome', - deviceInfo: navigator.userAgent, -}); - -if (!error) { - console.log(data.credentialId, data.prfCapable); -} -``` - -### Adding a passkey from a settings screen - -The same call adds a passkey to an account that already has one, or gives one to a user -who declined at signup. Use it from `useAuth()` rather than the client directly: that -version refreshes the session afterwards, so `credentials` includes the new passkey -without a reload. - -```tsx -function AddPasskey() { - const { registerPasskey, credentials } = useAuth(); - - const add = async () => { - const { error } = await registerPasskey({ - friendlyName: 'My laptop', - platform: 'macOS', - browser: 'Chrome', - deviceInfo: navigator.userAgent, - }); - - if (error) { - // A 401 means the session expired rather than anything about the - // authenticator. `isUnauthenticated(error)` tells the two apart. - } - }; - - return ( - <> -

{credentials.length} passkeys

- - - ); -} -``` - -### OTP and magic-link continuation - -> **The request helpers take no identifier.** `requestMagicLink()`, `requestLoginEmailOtp()`, and -> `requestLoginPhoneOtp()` send nothing but the session cookie. They rely on server-side state -> established by a preceding `login()` call, so calling them without it fails or targets the wrong -> account. This is not obvious from their signatures. Always call `login()` first, and use the same -> browser session for the continuation step. - -```tsx -function CustomLoginContinuation() { - const { login, refreshSession } = useAuth(); - const authClient = useAuthClient(); - - async function start(identifier: string) { - // Required first: this is what the request helpers below depend on. - const { data, error } = await login(identifier, false); - - if (error) { - return; - } - - // Offer only what the server says this account supports. - return data.loginMethods ?? ['magic_link', 'email_otp']; - } - - async function sendEmailCode() { - const { error } = await authClient.requestLoginEmailOtp(); - if (error) { - // surface error.message - } - } - - async function submitEmailCode(code: string) { - const { error } = await authClient.verifyLoginEmailOtp(code); - - if (!error) { - await refreshSession(); - } - } -} -``` - -Magic links complete in whichever tab opens the emailed link, so a custom flow needs two pieces. - -The waiting screen polls until the link is used: - -```ts -const interval = setInterval(async () => { - const { error } = await authClient.checkMagicLink(); - - if (!error) { - clearInterval(interval); - await refreshSession(); - } -}, 5000); -``` - -The landing route verifies the token from the query string, then refreshes its own session: - -```tsx -function CustomMagicLinkLanding() { - const { refreshSession } = useAuth(); // plus: import { useEffect } from 'react' - const authClient = useAuthClient(); - - useEffect(() => { - const token = new URLSearchParams(window.location.search).get('token'); - if (!token) return; - - void authClient.verifyMagicLink(token).then(async ({ error }) => { - if (!error) { - // Refresh here too. This tab set the cookie, but its provider state - // was loaded before the cookie existed. - await refreshSession(); - } - }); - }, [authClient, refreshSession]); - - return

Finishing sign-in...

; -} -``` - -The auth API emails a link pointing at `/verify-magiclink?token=...`, so a custom app must serve that -path. - -### Credential management - -`useAuth()` exposes the signed-in user's passkeys plus helpers to rename and remove them. These -helpers update provider state on success and report failure through `error`. - -```tsx -import { useAuth } from '@seamless-auth/react'; -import type { Credential } from '@seamless-auth/react'; -import { useState } from 'react'; - -function PasskeyList() { - const { credentials, updateCredential, deleteCredential } = useAuth(); - const [message, setMessage] = useState(''); - - async function rename(credential: Credential, friendlyName: string) { - const { error } = await updateCredential({ ...credential, friendlyName }); - - if (error) { - setMessage(error.message); - } - } - - async function remove(credentialId: string) { - const { error } = await deleteCredential(credentialId); - - if (error) { - setMessage(error.message); - } - } - - return ( -
    - {credentials.map(credential => ( -
  • - {credential.friendlyName ?? credential.deviceInfo} - - -
  • - ))} -
- ); -} -``` - -`Credential.lastUsedAt` and `Credential.createdAt` are ISO 8601 strings, which is what the API sends. -Wrap them yourself to format: - -```tsx -const lastUsed = credential.lastUsedAt ? new Date(credential.lastUsedAt) : null; -``` - -Removing a passkey is a sensitive change. Gate it behind a fresh step-up when the account has other -factors, using `refreshStepUpStatus()` and `verifyStepUpWithPasskey()` from the step-up section. - -## Built-In Routes - -`AuthRoutes` serves these canonical paths: - -- `/login` -- `/passkey-login` -- `/verify-phone-otp` -- `/verify-email-otp` -- `/verify-magic-link` -- `/oauth/callback` -- `/register-passkey` -- `/magic-link-sent` - -These are optional UI wrappers over the same SDK primitives the package now exports for custom flows. - -### Renamed routes - -The earlier mixed-case paths were renamed and are no longer served. Anything linking directly to -them now falls through to `/login`, so update those links: - -| Old path | New path | -| ------------------ | ------------------- | -| `/passKeyLogin` | `/passkey-login` | -| `/verifyPhoneOTP` | `/verify-phone-otp` | -| `/verifyEmailOTP` | `/verify-email-otp` | -| `/registerPasskey` | `/register-passkey` | -| `/magiclinks-sent` | `/magic-link-sent` | - -Two paths are unchanged because they are owned by contracts outside this package: - -- `/verify-magiclink` is the URL the auth API builds when it emails a magic link, so it has to match - that value exactly. Renaming it here would send every emailed link to `/login` with the token - discarded. -- `/oauth/callback` is registered with OAuth providers as an allowed redirect URI, so renaming it - would break configured integrations. - -## Theming The Built-In UI - -Every colour in the built-in screens is a CSS custom property with a fallback, so the default look is -unchanged if you set nothing. To match your brand, set the tokens you care about on `:root`, or on any -element that wraps ``. - -```css -:root { - --seamless-accent: #1f3a34; - --seamless-accent-hover: #16302b; - --seamless-surface: #f7f5f0; - --seamless-text: #1a1a1a; -} -``` - -Scoping to a wrapper also works, which is useful when the auth screens should look different from the -rest of the app: - -```css -.auth-shell { - --seamless-accent: #1f3a34; - --seamless-accent-soft: #3c6f63; -} -``` - -There is no provider prop or JavaScript API for this. Setting the variables is the whole interface. - -### Tokens - -| Token | Used for | Default | -| ---------------------------- | ---------------------------------------------------------------------- | --------------------------------------------- | -| `--seamless-accent` | Primary buttons, focus rings, selected states, accent borders, spinner | `#2563eb` fills, `#3b82f6` rings and borders | -| `--seamless-accent-hover` | Hover state of primary buttons | `#1d4ed8` | -| `--seamless-accent-contrast` | Label text on accent-filled buttons | `white` | -| `--seamless-accent-soft` | Links, toggle buttons, accent icons | `#60a5fa`, `#a5b4fc` on the magic-link screen | -| `--seamless-surface` | Card and modal backgrounds | `#1f2937` | -| `--seamless-surface-raised` | Inputs and panels sitting on a card | `#374151`, `#4b5563` on the login form | -| `--seamless-surface-hover` | Hover state of secondary buttons | `#4b5563` | -| `--seamless-border` | Input, button, and panel borders | `#4b5563`, `#d1d5db` | -| `--seamless-text` | Headings and body text | `white` | -| `--seamless-text-muted` | Labels, helper text, secondary copy | `#9ca3af`, `#d1d5db` | -| `--seamless-danger` | Error messages | `#f87171` | -| `--seamless-success` | Success messages and the verified check icon | `#34d399` | -| `--seamless-warning` | OTP countdown and resend timers | `#facc15` | -| `--seamless-overlay` | Modal backdrop scrim | `rgba(0, 0, 0, 0.45)` | -| `--seamless-shadow` | Card and modal shadow colour | `rgba(0, 0, 0, 0.1)` to `rgba(0, 0, 0, 0.4)` | - -### Notes - -- Where the original design used near-duplicate shades for the same role, each declaration keeps its - own original value as the fallback. That is why some rows list more than one default. Nothing shifts - until you set the token, and once you do, every use of that role picks up your value. -- `--seamless-shadow` is the shadow colour only. Offsets and blur are fixed. Setting it applies one - colour to every shadow in the built-in UI, replacing the per-screen alpha values. -- If you set `--seamless-surface` to a light colour, set `--seamless-text` too. The default text - colour is white and will disappear otherwise. -- Two decorative tints are deliberately not tokenised: the pulse ring behind the magic-link mail icon - and the disc behind the success check. Both are translucent and sit directly under an icon, so an - opaque override would hide the icon it is meant to frame. -- Disabled buttons are not a separate colour. They are the enabled button at reduced opacity, so the - label and its background always come from the same accent pair you set and the contrast between - them cannot invert when the theme changes. `--seamless-disabled` used to set a standalone grey fill - and no longer does anything; remove it from your overrides. -- The package ships one palette and no `prefers-color-scheme` rules. If you want the auth UI to follow - the system theme, wrap your own overrides in a media query. - -## Backend Expectations - -This package assumes a Seamless Auth-compatible backend with the auth adapter mounted at `/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 - -The built-in flows assume compatible endpoints for: - -- `/login` -- `DELETE /logout` for the current session -- `DELETE /logout/all` for every session owned by the current user -- `/registration/register` -- `/webAuthn/login/start` -- `/webAuthn/login/finish` -- `/webAuthn/register/start` -- `/webAuthn/register/finish` -- `POST /otp/generate-phone-otp` -- `POST /otp/generate-email-otp` -- `/otp/verify-phone-otp` -- `/otp/verify-email-otp` -- `POST /otp/generate-login-phone-otp` -- `POST /otp/generate-login-email-otp` -- `/otp/verify-login-phone-otp` -- `/otp/verify-login-email-otp` -- `POST /magic-link` -- `/magic-link/check` -- `/magic-link/verify/:token` -- `/oauth/providers` -- `/oauth/:providerId/start` -- `/oauth/:providerId/callback` -- `/step-up/status` -- `/step-up/webauthn/start` -- `/step-up/webauthn/finish` -- `/totp/status` -- `/totp/enroll/start` -- `/totp/enroll/verify` -- `/totp/disable` -- `/totp/verify-mfa` -- `/users/me` -- `/users/credentials` -- `/users/delete` -- `/organizations` -- `/organizations/:organizationId` -- `/organizations/:organizationId/switch` -- `/organizations/:organizationId/members` -- `/organizations/:organizationId/members/:userId` - -The state-changing OTP and magic-link request routes are `POST` (marked above). They were previously -`GET`, which made them reachable as simple cross-site requests, so an `` tag could trigger SMS or -email sends to a signed-in user. Using `@seamless-auth/react` with an older adapter that only serves the -`GET` forms returns a 404 for those requests. See the changelog for the minimum adapter version. - -`/webAuthn/register/finish` can refuse a verified credential on policy grounds with a `403` whose -body is a stable code. `syncedPasskeys` defaults to `allow`, but a deployment that sets `block` -refuses every backup-eligible passkey, so handle the code rather than assuming the default. See -[Passkey policy refusals](#passkey-policy-refusals). - -## Notes - -- This package does not create its own ``. -- It is designed to fit into your app’s existing routing tree. -- The quickest path is `AuthProvider` + `AuthRoutes`. -- The most flexible path is `AuthProvider` + custom UI using `useAuth()`, `useAuthClient()`, and `usePasskeySupport()`. +Tests run as one Jest project per package from the root. The React project +resolves `@seamless-auth/client` to the client package's source, so a change in +the core is exercised by the React suite without a build in between. Releases +are managed with Changesets; see [RELEASES.md](RELEASES.md) and +[CONTRIBUTING.md](CONTRIBUTING.md). ## License -AGPL-3.0-only +AGPL-3.0-only. See [LICENSE](LICENSE). diff --git a/eslint.config.mjs b/eslint.config.mjs index 82571ff..371fda5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -56,15 +56,9 @@ export default [ }, }, { - // The client layer stays framework agnostic so it can be extracted into a - // shared package for non-React adapters. See #64. - files: [ - 'src/client/**/*.ts', - 'src/session/**/*.ts', - 'src/fetchWithAuth.ts', - 'src/scopedRoles.ts', - 'src/types.ts', - ], + // @seamless-auth/client is the framework-agnostic core every binding shares, + // so nothing React (or from a binding) may enter it. See #64. + files: ['packages/client/src/**/*.ts'], rules: { 'no-restricted-imports': [ 'error', @@ -78,25 +72,19 @@ export default [ 'react-dom/*', 'react-router', 'react-router-dom', + 'react-native', + 'react-native/*', ], message: - 'The client layer must stay framework agnostic. Keep React and router imports in the binding layer. See #64.', + 'The client core must stay framework agnostic. Keep React and router imports in a binding package. See #64.', }, { group: [ - '@/AuthProvider', - '@/AuthRoutes', - '@/hooks/*', - '@/views/*', - '@/components/*', - '**/AuthProvider', - '**/AuthRoutes', - '**/hooks/*', - '**/views/*', - '**/components/*', + '@seamless-auth/react', + '@seamless-auth/react-native', + '../react/*', ], - message: - 'The client layer must not import from the React binding layer. See #64.', + message: 'The client core must not import from a binding package. See #64.', }, ], }, @@ -104,6 +92,6 @@ export default [ }, }, { - ignores: ['dist/', 'coverage/', 'node_modules/'], + ignores: ['**/dist/', '**/coverage/', '**/node_modules/'], }, ]; diff --git a/jest.config.ts b/jest.config.ts index 1016064..ff7db69 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,19 +1,5 @@ export default { - preset: 'ts-jest', - testEnvironment: 'jsdom', - setupFilesAfterEnv: ['/jest.setup.ts'], - transform: { - '^.+\\.(t|j)sx?$': ['ts-jest', { useESM: true, tsconfig: './tsconfig.json' }], - }, - extensionsToTreatAsEsm: ['.ts', '.tsx'], - moduleNameMapper: { - '\\.(css|less|scss|sass)$': 'identity-obj-proxy', - '^@/(.*)$': '/src/$1', - }, - testMatch: [ - '/tests/**/*.(test|spec).[tj]s?(x)', - '/src/**/*.(test|spec).[tj]s?(x)', - ], + projects: ['/packages/client', '/packages/react'], collectCoverage: true, collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'], coverageThreshold: { diff --git a/package-lock.json b/package-lock.json index a8d9991..fdab5a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,16 @@ { - "name": "@seamless-auth/react", + "name": "seamless-auth-client-sdks", "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@seamless-auth/react", - "version": "0.10.0", + "name": "seamless-auth-client-sdks", "license": "AGPL-3.0-only", - "dependencies": { - "@seamless-auth/types": "^0.20.0", - "@simplewebauthn/browser": "^13.1.0", - "eslint-plugin-license-header": "^0.9.0", - "libphonenumber-js": "^1.12.7", - "ts-node": "^10.9.2" - }, + "workspaces": [ + "packages/client", + "packages/react" + ], "devDependencies": { "@changesets/cli": "^2.31.0", "@commitlint/cli": "^20.1.0", @@ -34,6 +30,7 @@ "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-license-header": "^0.9.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.24", @@ -44,9 +41,11 @@ "jest-environment-jsdom": "^30.2.0", "lint-staged": "^16.2.5", "prettier": "^3.6.2", + "rollup": "^4.59.0", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "ts-jest": "^29.4.5", + "ts-node": "^10.9.2", "tsc-alias": "^1.9.1", "tslib": "^2.7.0", "typescript": "^5.9.3", @@ -55,11 +54,6 @@ "engines": { "node": ">=24.0.0 <25.0.0", "npm": ">=9.0.0 <13.0.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0", - "react-router-dom": "^6.4.0 || ^7.15.1" } }, "node_modules/@adobe/css-tools": { @@ -1150,6 +1144,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -1162,6 +1157,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -1287,6 +1283,7 @@ "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -1305,6 +1302,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1317,6 +1315,7 @@ "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1326,6 +1325,7 @@ "version": "0.21.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", @@ -1340,6 +1340,7 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.17.0" @@ -1352,6 +1353,7 @@ "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -1364,6 +1366,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -1387,6 +1390,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -1403,12 +1407,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, "node_modules/@eslint/js": { "version": "9.39.2", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1421,6 +1427,7 @@ "version": "2.1.7", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1430,6 +1437,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.17.0", @@ -1443,6 +1451,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -1452,6 +1461,7 @@ "version": "0.16.7", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -1465,6 +1475,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -1478,6 +1489,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -2292,6 +2304,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2312,6 +2325,7 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -2656,8 +2670,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.59.0", @@ -2671,8 +2684,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.59.0", @@ -2686,8 +2698,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.59.0", @@ -2701,8 +2712,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.59.0", @@ -2716,8 +2726,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.59.0", @@ -2731,8 +2740,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.59.0", @@ -2749,8 +2757,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.59.0", @@ -2767,8 +2774,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.59.0", @@ -2785,8 +2791,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.59.0", @@ -2803,8 +2808,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.59.0", @@ -2821,8 +2825,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { "version": "4.59.0", @@ -2839,8 +2842,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.59.0", @@ -2857,8 +2859,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { "version": "4.59.0", @@ -2875,8 +2876,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.59.0", @@ -2893,8 +2893,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.59.0", @@ -2911,8 +2910,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.59.0", @@ -2929,8 +2927,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.59.0", @@ -2947,8 +2944,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.59.0", @@ -2965,8 +2961,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-openbsd-x64": { "version": "4.59.0", @@ -2980,8 +2975,7 @@ "optional": true, "os": [ "openbsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.59.0", @@ -2995,8 +2989,7 @@ "optional": true, "os": [ "openharmony" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.59.0", @@ -3010,8 +3003,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.59.0", @@ -3025,8 +3017,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.59.0", @@ -3040,8 +3031,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.59.0", @@ -3055,8 +3045,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -3065,6 +3054,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@seamless-auth/client": { + "resolved": "packages/client", + "link": true + }, + "node_modules/@seamless-auth/react": { + "resolved": "packages/react", + "link": true + }, "node_modules/@seamless-auth/types": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.20.0.tgz", @@ -3204,24 +3201,28 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, "license": "MIT" }, "node_modules/@types/aria-query": { @@ -3281,6 +3282,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -3382,6 +3384,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -3395,6 +3398,7 @@ "version": "25.2.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -3739,6 +3743,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3751,6 +3756,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -3760,6 +3766,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -3835,6 +3842,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3877,12 +3885,14 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -4228,6 +4238,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -4277,6 +4288,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -4414,6 +4426,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4467,6 +4480,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -4707,6 +4721,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4719,6 +4734,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/colord": { @@ -4767,6 +4783,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/concat-with-sourcemaps": { @@ -4913,12 +4930,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5214,6 +5233,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5253,6 +5273,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -5335,6 +5356,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -5779,6 +5801,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5791,6 +5814,7 @@ "version": "9.39.2", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", @@ -5970,6 +5994,7 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/eslint-plugin-license-header/-/eslint-plugin-license-header-0.9.0.tgz", "integrity": "sha512-Qd7cCljVC0h+uJjcIuYjpRFrdzwqBBDCi5U0ocr6Bt/5t3zuBkZSa1Igc4lBLEVBDoUUqIcok/UUNAAu6CtwmQ==", + "dev": true, "license": "MIT", "dependencies": { "requireindex": "^1.2.0" @@ -6076,6 +6101,7 @@ "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -6092,6 +6118,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6104,6 +6131,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -6120,12 +6148,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", @@ -6157,6 +6187,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -6169,6 +6200,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -6181,6 +6213,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -6197,6 +6230,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -6277,6 +6311,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -6313,12 +6348,14 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, "license": "MIT" }, "node_modules/fast-uri": { @@ -6380,6 +6417,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -6405,6 +6443,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -6421,6 +6460,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -6434,6 +6474,7 @@ "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, "license": "ISC" }, "node_modules/for-each": { @@ -6750,6 +6791,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -6804,6 +6846,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6916,6 +6959,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7126,6 +7170,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -7148,6 +7193,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -7164,6 +7210,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -7217,6 +7264,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -7435,6 +7483,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7506,6 +7555,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7806,6 +7856,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -9085,7 +9136,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -9102,6 +9153,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9167,6 +9219,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -9187,6 +9240,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -9232,6 +9286,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -9261,6 +9316,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -9406,6 +9462,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -9442,6 +9499,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, "license": "MIT" }, "node_modules/lodash.mergewith": { @@ -9635,6 +9693,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, "license": "ISC" }, "node_modules/makeerror": { @@ -9758,6 +9817,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9800,6 +9860,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/mylas": { @@ -9853,6 +9914,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, "license": "MIT" }, "node_modules/neo-async": { @@ -10085,6 +10147,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -10150,6 +10213,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -10165,6 +10229,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -10254,6 +10319,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -10298,6 +10364,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10317,6 +10384,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11149,6 +11217,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -11247,6 +11316,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11556,6 +11626,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.5" @@ -11682,7 +11753,6 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -11990,6 +12060,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -12002,6 +12073,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12544,6 +12616,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12580,6 +12653,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -12903,6 +12977,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -13021,6 +13096,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -13134,6 +13210,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13204,6 +13281,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -13251,6 +13329,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -13267,6 +13346,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -13359,6 +13439,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13463,6 +13544,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13748,6 +13830,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13757,6 +13840,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -13773,6 +13857,39 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "packages/client": { + "name": "@seamless-auth/client", + "version": "0.0.0", + "license": "AGPL-3.0-only", + "dependencies": { + "@seamless-auth/types": "^0.20.0", + "@simplewebauthn/browser": "^13.1.0" + }, + "engines": { + "node": ">=24.0.0 <25.0.0", + "npm": ">=9.0.0 <13.0.0" + } + }, + "packages/react": { + "name": "@seamless-auth/react", + "version": "0.12.0", + "license": "AGPL-3.0-only", + "dependencies": { + "@seamless-auth/client": "^0.0.0", + "@seamless-auth/types": "^0.20.0", + "@simplewebauthn/browser": "^13.1.0", + "libphonenumber-js": "^1.12.7" + }, + "engines": { + "node": ">=24.0.0 <25.0.0", + "npm": ">=9.0.0 <13.0.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "react-router-dom": "^6.4.0 || ^7.15.1" + } } } } diff --git a/package.json b/package.json index 90797fc..5715c11 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,29 @@ { - "name": "@seamless-auth/react", - "version": "0.12.0", - "description": "A drop-in authentication solution for modern React applications.", + "name": "seamless-auth-client-sdks", + "private": true, + "description": "Seamless Auth client SDKs: the framework-agnostic client core and its React bindings.", "type": "module", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - } - }, - "types": "./dist/index.d.ts", - "files": [ - "dist", - "README.md", - "CHANGELOG.md", - "LICENSE" + "workspaces": [ + "packages/client", + "packages/react" ], "engines": { "node": ">=24.0.0 <25.0.0", "npm": ">=9.0.0 <13.0.0" }, "scripts": { - "build": "node ./scripts/clean-dist.mjs && rollup -c && tsc-alias -p tsconfig.build.json", + "build": "npm run build -w @seamless-auth/client && npm run build -w @seamless-auth/react", "test": "jest", "coverage": "npm test -- --coverage", - "typecheck": "tsc --noEmit -p tsconfig.dev.json", - "lint": "eslint ./src ./tests", + "typecheck": "npm run typecheck -w @seamless-auth/client && npm run typecheck -w @seamless-auth/react", + "lint": "eslint ./packages", "format": "prettier --write .", "format:check": "prettier --check .", "prepare": "husky", "changeset": "changeset", "version-packages": "changeset version", "release:stable": "npm run build && changeset publish", - "check-npm-build": "npm pack --dry-run", + "check-npm-build": "npm pack --dry-run --workspaces", "lint-staged": "lint-staged" }, "lint-staged": { @@ -54,16 +45,6 @@ "url": "https://github.com/fells-code/seamless-auth-react/issues" }, "homepage": "https://github.com/fells-code/seamless-auth-react#readme", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/", - "provenance": true - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0", - "react-router-dom": "^6.4.0 || ^7.15.1" - }, "devDependencies": { "@changesets/cli": "^2.31.0", "@commitlint/cli": "^20.1.0", @@ -83,6 +64,7 @@ "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-license-header": "^0.9.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.24", @@ -93,20 +75,14 @@ "jest-environment-jsdom": "^30.2.0", "lint-staged": "^16.2.5", "prettier": "^3.6.2", + "rollup": "^4.59.0", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "ts-jest": "^29.4.5", + "ts-node": "^10.9.2", "tsc-alias": "^1.9.1", "tslib": "^2.7.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.1" - }, - "dependencies": { - "@seamless-auth/types": "^0.20.0", - "@simplewebauthn/browser": "^13.1.0", - "eslint-plugin-license-header": "^0.9.0", - "libphonenumber-js": "^1.12.7", - "ts-node": "^10.9.2" - }, - "sideEffects": false + } } diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md new file mode 100644 index 0000000..1e85d34 --- /dev/null +++ b/packages/client/CHANGELOG.md @@ -0,0 +1 @@ +# @seamless-auth/client diff --git a/packages/client/LICENSE b/packages/client/LICENSE new file mode 100644 index 0000000..162676c --- /dev/null +++ b/packages/client/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/client/README.md b/packages/client/README.md new file mode 100644 index 0000000..cc2ec42 --- /dev/null +++ b/packages/client/README.md @@ -0,0 +1,46 @@ +# @seamless-auth/client + +The framework-agnostic core of the Seamless Auth client SDKs. It holds the +headless auth client (`createSeamlessAuthClient`), the session store +(`createAuthSession`), and the result and error types that every binding shares. +`@seamless-auth/react` is a thin binding over this package, and it is the +package a React Native or other non-React binding builds on. + +If you are building a React application, install +[`@seamless-auth/react`](https://www.npmjs.com/package/@seamless-auth/react) +instead; it depends on this package and re-exports what an application needs. + +## Install + +```bash +npm install @seamless-auth/client +``` + +## What is here + +- `createSeamlessAuthClient({ apiHost })`: the request choreography for login, + registration, OTP, magic link, passkey, OAuth, step-up, TOTP, credential and + organization flows against a Seamless Auth server adapter mounted at `/auth`. + Every method returns a `SeamlessAuthResult`. +- `createAuthSession({ apiHost, storage?, detectPreviousSignIn? })`: the session + state machine (`getState`, `subscribe`, `actions`, `destroy`), designed to be + bound with `useSyncExternalStore` or an equivalent. +- `SessionStoragePort` with browser and memory implementations. +- `SeamlessAuthError`, `getWebAuthnErrorDetail`, `getOAuthErrorCode`, + `isUnauthenticated`, and the other error readers. +- PRF helpers for passkey-derived secrets. +- `hasScopedRole` / `roleGrantsAccess`, the same role matching the auth API and + server adapters apply. + +The wire types come from `@seamless-auth/types`; this package aliases them rather +than redeclaring shapes. + +## Transport + +Today the client speaks the cookie contract: requests go to `${apiHost}/auth/*` +with `credentials: 'include'` and the server adapter holds the tokens. A bearer +transport for native clients is the next change to this package. + +## License + +AGPL-3.0-only. See [LICENSE](LICENSE). diff --git a/packages/client/jest.config.ts b/packages/client/jest.config.ts new file mode 100644 index 0000000..62cbf5b --- /dev/null +++ b/packages/client/jest.config.ts @@ -0,0 +1,21 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +export default { + displayName: 'client', + preset: 'ts-jest', + testEnvironment: 'jsdom', + rootDir: '.', + setupFilesAfterEnv: ['/../../jest.setup.ts'], + transform: { + '^.+\\.(t|j)sx?$': ['ts-jest', { useESM: true, tsconfig: '/tsconfig.json' }], + }, + extensionsToTreatAsEsm: ['.ts', '.tsx'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + testMatch: ['/tests/**/*.(test|spec).[tj]s?(x)'], +}; diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 0000000..651f91a --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,49 @@ +{ + "name": "@seamless-auth/client", + "version": "0.0.0", + "description": "Framework-agnostic client core for Seamless Auth: the HTTP client, session store, and result types the framework bindings share.", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "types": "./dist/index.d.ts", + "files": [ + "dist", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "engines": { + "node": ">=24.0.0 <25.0.0", + "npm": ">=9.0.0 <13.0.0" + }, + "scripts": { + "build": "node ../../scripts/clean-dist.mjs && rollup -c && tsc-alias -p tsconfig.build.json", + "typecheck": "tsc --noEmit -p tsconfig.dev.json", + "check-npm-build": "npm pack --dry-run" + }, + "repository": { + "type": "git", + "url": "https://github.com/fells-code/seamless-auth-react.git", + "directory": "packages/client" + }, + "author": "Fells Code, LLC", + "license": "AGPL-3.0-only", + "bugs": { + "url": "https://github.com/fells-code/seamless-auth-react/issues" + }, + "homepage": "https://github.com/fells-code/seamless-auth-react/tree/main/packages/client#readme", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/", + "provenance": true + }, + "dependencies": { + "@seamless-auth/types": "^0.20.0", + "@simplewebauthn/browser": "^13.1.0" + }, + "sideEffects": false +} diff --git a/packages/client/rollup.config.js b/packages/client/rollup.config.js new file mode 100644 index 0000000..eb46e33 --- /dev/null +++ b/packages/client/rollup.config.js @@ -0,0 +1,31 @@ +import alias from '@rollup/plugin-alias'; +import commonjs from '@rollup/plugin-commonjs'; +import terser from '@rollup/plugin-terser'; +import typescript from '@rollup/plugin-typescript'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default [ + { + input: 'src/index.ts', + output: { + file: 'dist/index.js', + format: 'esm', + sourcemap: true, + }, + external: ['@simplewebauthn/browser'], + plugins: [ + alias({ + entries: [{ find: '@', replacement: path.resolve(__dirname, 'src') }], + }), + commonjs(), + typescript({ + tsconfig: './tsconfig.build.json', + }), + terser(), + ], + }, +]; diff --git a/src/client/createSeamlessAuthClient.ts b/packages/client/src/client/createSeamlessAuthClient.ts similarity index 100% rename from src/client/createSeamlessAuthClient.ts rename to packages/client/src/client/createSeamlessAuthClient.ts diff --git a/src/client/errors.ts b/packages/client/src/client/errors.ts similarity index 100% rename from src/client/errors.ts rename to packages/client/src/client/errors.ts diff --git a/src/client/result.ts b/packages/client/src/client/result.ts similarity index 100% rename from src/client/result.ts rename to packages/client/src/client/result.ts diff --git a/src/client/webauthnPrf.ts b/packages/client/src/client/webauthnPrf.ts similarity index 100% rename from src/client/webauthnPrf.ts rename to packages/client/src/client/webauthnPrf.ts diff --git a/src/client/webauthnSupport.ts b/packages/client/src/client/webauthnSupport.ts similarity index 100% rename from src/client/webauthnSupport.ts rename to packages/client/src/client/webauthnSupport.ts diff --git a/src/fetchWithAuth.ts b/packages/client/src/fetchWithAuth.ts similarity index 100% rename from src/fetchWithAuth.ts rename to packages/client/src/fetchWithAuth.ts diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts new file mode 100644 index 0000000..2811583 --- /dev/null +++ b/packages/client/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +export * from './client/createSeamlessAuthClient'; +export * from './client/errors'; +export * from './client/result'; +export * from './client/webauthnPrf'; +export * from './client/webauthnSupport'; +export * from './fetchWithAuth'; +export * from './scopedRoles'; +export * from './session/createAuthSession'; +export * from './session/storage'; +export * from './types'; diff --git a/src/scopedRoles.ts b/packages/client/src/scopedRoles.ts similarity index 100% rename from src/scopedRoles.ts rename to packages/client/src/scopedRoles.ts diff --git a/src/session/createAuthSession.ts b/packages/client/src/session/createAuthSession.ts similarity index 100% rename from src/session/createAuthSession.ts rename to packages/client/src/session/createAuthSession.ts diff --git a/src/session/storage.ts b/packages/client/src/session/storage.ts similarity index 100% rename from src/session/storage.ts rename to packages/client/src/session/storage.ts diff --git a/src/types.ts b/packages/client/src/types.ts similarity index 100% rename from src/types.ts rename to packages/client/src/types.ts diff --git a/tests/authSession.test.ts b/packages/client/tests/authSession.test.ts similarity index 100% rename from tests/authSession.test.ts rename to packages/client/tests/authSession.test.ts diff --git a/tests/createSeamlessAuthClient.test.ts b/packages/client/tests/createSeamlessAuthClient.test.ts similarity index 100% rename from tests/createSeamlessAuthClient.test.ts rename to packages/client/tests/createSeamlessAuthClient.test.ts diff --git a/tests/errors.test.ts b/packages/client/tests/errors.test.ts similarity index 100% rename from tests/errors.test.ts rename to packages/client/tests/errors.test.ts diff --git a/tests/fetchWithAuth.test.tsx b/packages/client/tests/fetchWithAuth.test.tsx similarity index 100% rename from tests/fetchWithAuth.test.tsx rename to packages/client/tests/fetchWithAuth.test.tsx diff --git a/tests/result.test.ts b/packages/client/tests/result.test.ts similarity index 100% rename from tests/result.test.ts rename to packages/client/tests/result.test.ts diff --git a/tests/scopedRoles.test.ts b/packages/client/tests/scopedRoles.test.ts similarity index 100% rename from tests/scopedRoles.test.ts rename to packages/client/tests/scopedRoles.test.ts diff --git a/tests/sessionStorage.ssr.test.ts b/packages/client/tests/sessionStorage.ssr.test.ts similarity index 100% rename from tests/sessionStorage.ssr.test.ts rename to packages/client/tests/sessionStorage.ssr.test.ts diff --git a/tests/sessionStorage.test.ts b/packages/client/tests/sessionStorage.test.ts similarity index 100% rename from tests/sessionStorage.test.ts rename to packages/client/tests/sessionStorage.test.ts diff --git a/tests/webauthnPrf.test.ts b/packages/client/tests/webauthnPrf.test.ts similarity index 100% rename from tests/webauthnPrf.test.ts rename to packages/client/tests/webauthnPrf.test.ts diff --git a/tests/webauthnSupport.ssr.test.ts b/packages/client/tests/webauthnSupport.ssr.test.ts similarity index 100% rename from tests/webauthnSupport.ssr.test.ts rename to packages/client/tests/webauthnSupport.ssr.test.ts diff --git a/tests/webauthnSupport.test.ts b/packages/client/tests/webauthnSupport.test.ts similarity index 100% rename from tests/webauthnSupport.test.ts rename to packages/client/tests/webauthnSupport.test.ts diff --git a/tests/wireTypes.test.ts b/packages/client/tests/wireTypes.test.ts similarity index 100% rename from tests/wireTypes.test.ts rename to packages/client/tests/wireTypes.test.ts diff --git a/tsconfig.build.json b/packages/client/tsconfig.build.json similarity index 100% rename from tsconfig.build.json rename to packages/client/tsconfig.build.json diff --git a/packages/client/tsconfig.dev.json b/packages/client/tsconfig.dev.json new file mode 100644 index 0000000..a6668d6 --- /dev/null +++ b/packages/client/tsconfig.dev.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "tests", "../../jest.setup.ts"] +} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json new file mode 100644 index 0000000..4bf6210 --- /dev/null +++ b/packages/client/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/CHANGELOG.md b/packages/react/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/react/CHANGELOG.md diff --git a/packages/react/LICENSE b/packages/react/LICENSE new file mode 100644 index 0000000..162676c --- /dev/null +++ b/packages/react/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/react/README.md b/packages/react/README.md new file mode 100644 index 0000000..2a06f95 --- /dev/null +++ b/packages/react/README.md @@ -0,0 +1,1125 @@ +# @seamless-auth/react + +[![npm version](https://img.shields.io/npm/v/@seamless-auth/react.svg?label=%40seamless-auth%2Freact)](https://www.npmjs.com/package/@seamless-auth/react) +[![CI](https://github.com/fells-code/seamless-auth-react/actions/workflows/ci.yml/badge.svg)](https://github.com/fells-code/seamless-auth-react/actions/workflows/ci.yml) +[![Release](https://github.com/fells-code/seamless-auth-react/actions/workflows/release.yml/badge.svg)](https://github.com/fells-code/seamless-auth-react/actions/workflows/release.yml) +[![coverage](https://img.shields.io/codecov/c/github/fells-code/seamless-auth-react)](https://app.codecov.io/gh/fells-code/seamless-auth-react) +[![license](https://img.shields.io/github/license/fells-code/seamless-auth-react)](./LICENSE) + +`@seamless-auth/react` is a React SDK for Seamless Auth. It gives you a provider for auth state, a headless client and hooks for custom auth UIs, and optional prebuilt auth routes when you want a faster drop-in flow. + +## What It Exports + +- `AuthProvider` +- `AuthRoutes` +- `useAuth()` +- `createSeamlessAuthClient()` +- `useAuthClient()` +- `usePasskeySupport()` +- `hasScopedRole()` and `roleGrantsAccess()` +- `SeamlessAuthError`, the error type carried on a failed result +- `getOAuthErrorCode()`, which reads the known OAuth callback failure codes off that error +- `getWebAuthnErrorDetail()`, which reads the underlying failure of a passkey or step-up ceremony +- `getPasskeyPolicyErrorCode()`, which reads the code the API refused a passkey registration with +- types including `AuthContextType`, `Credential`, `User`, `OAuthProvider`, `StepUpStatus`, the `SeamlessAuthResult` wrapper, and the headless client input/result types + +## Installation + +```bash +npm install @seamless-auth/react +``` + +## Releases + +Published versions are listed in [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases. Releases are +managed with Changesets: adopter-facing changes include a changeset, the `Release` workflow opens a +version PR for review, and merging that PR publishes the npm package with provenance from GitHub +Actions. See [RELEASES.md](./RELEASES.md) for maintainer release details. + +## Choose Your Integration Style + +You can use this package in three ways: + +1. `AuthProvider` + `useAuth()` for auth state and core auth actions +2. `createSeamlessAuthClient()` or `useAuthClient()` to build fully custom login and registration screens +3. `AuthRoutes` when you want the built-in login, OTP, magic-link, and passkey screens + +Most apps will use `AuthProvider` either way. + +## Quick Start + +### Wrap your app with `AuthProvider` + +```tsx +import { AuthProvider } from '@seamless-auth/react'; +import { BrowserRouter } from 'react-router-dom'; + + + + + +; +``` + +### Read auth state with `useAuth()` + +```tsx +import { useAuth } from '@seamless-auth/react'; + +function Dashboard() { + const { user, logout, refreshSession } = useAuth(); + + return ( +
+

Welcome, {user?.email}

+ + +
+ ); +} +``` + +### Use built-in auth routes with `AuthRoutes` + +```tsx +import { AuthRoutes, useAuth } from '@seamless-auth/react'; +import { Route, Routes } from 'react-router-dom'; + +function AppRoutes() { + const { isAuthenticated } = useAuth(); + + return ( + + {isAuthenticated ? ( + } /> + ) : ( + } /> + )} + + ); +} +``` + +You are still responsible for your app’s route protection and redirects. + +## `useAuth()` API + +`useAuth()` returns the current auth state plus the provider-backed helpers: + +```ts +{ + user: User | null; + credentials: Credential[]; + stepUpStatus: StepUpStatus | null; + isAuthenticated: boolean; + loading: boolean; + apiHost: string; + hasSignedInBefore: boolean; + markSignedIn(): void; + hasRole(role: string): boolean | undefined; + hasScopedRole(role: string | string[]): boolean | undefined; + listOAuthProviders(): Promise>; + startOAuthLogin(input: StartOAuthLoginInput): Promise>; + finishOAuthLogin(input: FinishOAuthLoginInput): Promise>; + refreshSession(): Promise>; + refreshStepUpStatus(): Promise>; + verifyStepUpWithPasskey(): Promise>; + verifyStepUpWithPasskeyPrf(input: PasskeyPrfInput): Promise>; + verifyStepUpWithTotp(code: string): Promise>; + logout(): Promise>; + logoutAllSessions(): Promise>; + deleteUser(): Promise>; + login(identifier: string, passkeyAvailable: boolean): Promise>; + handlePasskeyLogin(): Promise>; + updateCredential(credential: Credential): Promise>; + deleteCredential(credentialId: string): Promise>; +} +``` + +Use `refreshSession()` after completing a custom auth flow that should update provider state. + +### `hasSignedInBefore` + +`hasSignedInBefore` is a small convenience flag backed by `localStorage`. The provider reads the `seamlessauth_seen` key on load and sets the flag to `true` after `markSignedIn()` runs. + +This is mainly useful for login UIs that want to branch between first-time and returning-user behavior. For example, the built-in `Login` view uses it to default returning users to sign-in mode instead of registration. + +```tsx +import { useAuth } from '@seamless-auth/react'; + +function SignInHint() { + const { hasSignedInBefore } = useAuth(); + + return hasSignedInBefore ? ( +

Welcome back. Sign in with your email, phone, or passkey.

+ ) : ( +

New here? Start by creating your account.

+ ); +} +``` + +If you are building a fully custom flow, call `markSignedIn()` after a successful sign-in or registration step once you want future visits treated as returning-user sessions. + +```tsx +const { markSignedIn, refreshSession } = useAuth(); + +async function completeLogin() { + const { error } = await authClient.login({ + identifier: 'user@example.com', + passkeyAvailable: true, + }); + + if (!error) { + markSignedIn(); + await refreshSession(); + } +} +``` + +To disable this auto-detection entirely, pass `autoDetectPreviousSignin={false}` to `AuthProvider`. + +### Magic link destination + +By default a magic link lands wherever the deployment is configured to send it. A deployment serving +more than one front end can override that per application with `magicLinkRedirectUri`: + +```tsx + + + +``` + +Every magic link the bundled screens send uses it, including the resend on the "check your email" +screen, so a resent link always lands where the first one did. The deployment validates the value +against its configured origins and refuses anything else, which comes back as an ordinary error +result. + +Custom UIs get the same default through `useAuthClient()`, and can still override a single send with +`requestMagicLink(uri)`. + +### Scoped roles + +`hasRole(role)` remains an exact role check. Use `hasScopedRole(role)` for colon-separated scoped +roles such as `admin:read` and `admin:write`. + +```tsx +const { hasRole, hasScopedRole } = useAuth(); + +hasRole('admin'); // exact legacy role check +hasScopedRole('admin:read'); // true for admin, admin:read, or admin:write +hasScopedRole('admin:write'); // true for admin or admin:write +``` + +The package also exports standalone `hasScopedRole(roles, required)` and `roleGrantsAccess(...)` +helpers for code that is not inside `AuthProvider`. + +### Step-up authentication + +Use step-up authentication before sensitive actions that should require a fresh user verification, such as deleting an account, changing MFA settings, or viewing recovery material. + +```tsx +import { useAuth } from '@seamless-auth/react'; + +function DeleteAccountButton() { + const { refreshStepUpStatus, verifyStepUpWithPasskey } = useAuth(); + + async function handleDeleteAccount() { + const { data: status } = await refreshStepUpStatus(); + const fresh = status?.fresh ? true : !(await verifyStepUpWithPasskey()).error; + + if (!fresh) { + return; + } + + await deleteAccount(); + } + + return ; +} +``` + +Step-up supports WebAuthn/passkeys and TOTP (authenticator apps). `refreshStepUpStatus()` calls `/step-up/status`, `verifyStepUpWithPasskey()` performs the `/step-up/webauthn/start` and `/step-up/webauthn/finish` challenge flow, and `verifyStepUpWithTotp(code)` verifies a 6-digit authenticator code via `/totp/verify-mfa`. The verification helpers return a `SeamlessAuthResult` and refresh the provider's `stepUpStatus` when they succeed. + +```tsx +const { verifyStepUpWithTotp } = useAuth(); + +const { error } = await verifyStepUpWithTotp('123456'); // 6-digit code from the authenticator app +if (!error) { + // step-up is fresh; proceed with the sensitive action +} +``` + +### TOTP (authenticator apps) + +TOTP lets users register an authenticator app (Google Authenticator, 1Password, etc.) as a second factor for step-up verification. The SDK exposes headless client methods for enrollment and management; use them from a settings screen. All require an authenticated session. + +```ts +import { createSeamlessAuthClient } from '@seamless-auth/react'; +import type { TotpStatus, TotpEnrollmentStartResult } from '@seamless-auth/react'; + +const authClient = createSeamlessAuthClient({ apiHost: 'https://your.api' }); + +// 1. Check whether TOTP is already enabled +const { data: status } = await authClient.getTotpStatus(); + +// 2. Start enrollment: render `otpauthUrl` as a QR code (or show `secret` for manual entry) +const { data: enrollment } = await authClient.startTotpEnrollment(); + +// 3. Confirm the first code from the user's authenticator app +const { error } = await authClient.verifyTotpEnrollment('123456'); +if (!error) { + // TOTP is now enabled +} + +// Disabling requires a current code +await authClient.disableTotp('123456'); +``` + +These methods follow the standard result convention: check `error`, then read `data`. Enrolling TOTP is a sensitive change; gate it behind a fresh step-up when appropriate. + +> TOTP is not currently a login second factor. The Seamless Auth API issues a full session on the first factor and does not gate login on TOTP, so TOTP applies to step-up verification, not to the login flow. + +### WebAuthn PRF + +WebAuthn PRF lets a compatible passkey and browser derive local key material during a WebAuthn assertion. Seamless Auth verifies the passkey assertion on the server, while the React SDK returns the PRF output only to the browser caller. PRF output is stripped before `/webAuthn/login/finish` and `/step-up/webauthn/finish`, and should never be logged, stored, or sent to your API. + +Browser and authenticator support is not universal. Call `isPasskeyPrfSupported()` before offering PRF-required flows, and keep a fallback for passkeys that authenticate successfully without returning PRF output. + +Treat PRF salts as sensitive in client logs. PRF output is browser-local key material; keep it in +memory only as long as your application needs it and do not send it to Seamless Auth or your own API. + +```ts +import { createSeamlessAuthClient } from '@seamless-auth/react'; + +const authClient = createSeamlessAuthClient({ + apiHost: 'https://your.api', +}); + +const prfSupported = await authClient.isPasskeyPrfSupported(); + +if (prfSupported) { + await authClient.registerPasskey({ + metadata: { + friendlyName: 'My laptop', + platform: 'macOS', + browser: 'Chrome', + deviceInfo: navigator.userAgent, + }, + requirePrf: true, + }); +} +``` + +For local key unwrap flows such as Seamless Secrets, use PRF during step-up and consume the returned bytes in browser memory: + +```ts +const { data, error } = await authClient.verifyStepUpWithPasskeyPrf({ + salt: vaultSaltBase64url, + credentialId, +}); + +if (error) { + throw error; +} + +const vaultUnlockMaterial: { credentialId: string; output: Uint8Array } = { + credentialId: data.credentialId, + output: data.prf.output, +}; +``` + +The salt may be an `ArrayBuffer`, `ArrayBufferView`, or base64url string. Authentication proves identity and user presence; the PRF output is local key material for your application to use without sending it to Seamless Auth. + +### OAuth Login + +OAuth lets your app offer external identity providers such as Google, GitHub, Facebook, or custom +OIDC-style providers configured on the Seamless Auth API. The React SDK does not receive provider +access tokens. It only starts the provider redirect and completes the callback so Seamless Auth can +issue the normal access/refresh session. + +Use `listOAuthProviders()` when you want to render enabled providers dynamically: + +```tsx +import { useEffect, useState } from 'react'; +import { useAuth } from '@seamless-auth/react'; +import type { OAuthProvider } from '@seamless-auth/react'; + +function OAuthButtons() { + const { listOAuthProviders, startOAuthLogin } = useAuth(); + const [providers, setProviders] = useState([]); + + useEffect(() => { + void listOAuthProviders().then(result => setProviders(result.providers)); + }, [listOAuthProviders]); + + async function signIn(providerId: string) { + const result = await startOAuthLogin({ + providerId, + redirectUri: `${window.location.origin}/oauth/callback`, + returnTo: `${window.location.origin}/dashboard`, + }); + + window.location.assign(result.authorizationUrl); + } + + return ( +
+ {providers.map(provider => ( + + ))} +
+ ); +} +``` + +Create a callback route that reads the provider query params and asks Seamless Auth to complete the +login: + +```tsx +import { useEffect } from 'react'; +import { useAuth } from '@seamless-auth/react'; + +function OAuthCallback() { + const { finishOAuthLogin } = useAuth(); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + // Persist the provider you passed to startOAuthLogin so the callback knows + // which provider to finish. The built-in AuthRoutes flow stores this in + // sessionStorage; use whatever your custom start flow saved. + const providerId = sessionStorage.getItem('seamless:oauth:provider'); + const code = params.get('code'); + const state = params.get('state'); + + if (!providerId || !code || !state) { + return; + } + + void finishOAuthLogin({ providerId, code, state }).then(() => { + window.location.assign('/dashboard'); + }); + }, [finishOAuthLogin]); + + return

Finishing sign-in...

; +} +``` + +Some callback failures are the user's to fix, so the API returns a stable `code` alongside the error +message. `getOAuthErrorCode()` narrows it to the codes this SDK knows about and returns `undefined` +for everything else, so unexpected failures keep your generic message: + +```tsx +import { getOAuthErrorCode, useAuth } from '@seamless-auth/react'; + +const { error } = await finishOAuthLogin({ providerId, code, state }); + +switch (getOAuthErrorCode(error)) { + case 'oauth_missing_email': + // The provider account shared no email address. + break; + case 'oauth_email_not_verified': + // The provider account's email is unverified. + break; + case 'oauth_missing_subject': + // The provider returned no usable account identifier. + break; + default: + // No error, or one without a recognized code. + break; +} +``` + +The bundled `AuthRoutes` callback screen already maps these three codes to actionable text. + +For fully custom UI without `useAuth()`, call the headless client directly: + +```ts +const providers = await authClient.listOAuthProviders(); +const started = await authClient.startOAuthLogin({ + providerId: providers.providers[0].id, + redirectUri: `${window.location.origin}/oauth/callback`, +}); + +window.location.assign(started.authorizationUrl); +``` + +OAuth must be enabled on the Seamless Auth API with `LOGIN_METHODS` including `oauth` and at least +one configured `oauth_providers` entry. Provider client secrets live on the server and are referenced +by environment variable name; they are never passed through this SDK. + +For production providers, configure exact `redirectUris` on the Seamless Auth API. The SDK should +send the callback URL it expects to receive, but redirect allowlisting, signed state expiry, OIDC +nonce handling, email verification policy, and account-linking policy are enforced by the API. + +The built-in views avoid logging OTPs, magic-link tokens, PRF salts, or raw +exception payloads that may contain sensitive request URLs. + +## Headless Client + +For custom auth UIs, use the exported client directly: + +```ts +import { createSeamlessAuthClient } from '@seamless-auth/react'; + +const authClient = createSeamlessAuthClient({ + apiHost: 'https://your.api', +}); + +const { data, error } = await authClient.login({ + identifier: 'user@example.com', + passkeyAvailable: true, +}); + +if (error) { + // error.message, error.status, and error.body carry the server detail + return; +} + +// data is typed as LoginStartResult +console.log(data.loginMethods); +``` + +The headless client exposes helpers for: + +- current-user/session lookup +- login and passkey login +- registration +- phone OTP and email OTP +- magic-link request, verify, and polling +- OAuth provider listing, start, and callback completion +- passkey registration +- step-up status, passkey verification, and TOTP verification +- TOTP enrollment, status, and disable +- logout and delete-user +- credential update and deletion + +### Where the response types come from + +The request and response types are aliases of +[`@seamless-auth/types`](https://www.npmjs.com/package/@seamless-auth/types), which is generated from +the auth API's schemas. `User`, `Credential`, `Organization`, `StepUpStatus`, `MessageResult`, and the +other wire shapes describe what the API actually sends, rather than a second copy maintained here that +could drift from it. + +The dependency is types-only. Nothing from it is imported at runtime, so no schema validation library +reaches your bundle. Names exported from this package stay the SDK's own, so you keep importing +`Credential` from `@seamless-auth/react`. + +Two SDK concerns are deliberately not shared, because they are not wire contracts: the PRF helper +types and the `SeamlessAuthResult` wrapper. + +### Result convention + +Every request method resolves to a `SeamlessAuthResult`: + +```ts +type SeamlessAuthResult = + | { data: T; error: null } + | { data: null; error: SeamlessAuthError }; +``` + +Check `error` first, then read `data`. TypeScript enforces this: `data` is not readable until the +error has been ruled out. + +```ts +const { data, error } = await authClient.getCurrentUser(); + +if (error) { + console.log(error.message, error.status, error.body); + return; +} + +setUser(data.user); // typed as CurrentUserResult +``` + +Nothing throws for an HTTP failure, and transport failures are absorbed too, reported as an error +with `status` `0`. That means an expected auth outcome such as a wrong OTP, an expired magic link, or +a disabled provider is a value you can map straight to UI state rather than an exception to catch. + +`SeamlessAuthError` carries the server's `message`, the HTTP `status`, and the parsed response +`body`, so you can branch on a specific failure. + +### WebAuthn ceremony failures + +A passkey or step-up ceremony can fail in the browser before any request is sent, so those results +carry the thrown error as `cause` with `status` `0`. Use `getWebAuthnErrorDetail()` to read it: the +`name` is the `DOMException` name that separates the cases a user can act on, and `code` is +SimpleWebAuthn's narrower reason when it identified one. + +```ts +import { getWebAuthnErrorDetail } from '@seamless-auth/react'; + +const { error } = await authClient.verifyStepUpWithPasskey(); +const detail = getWebAuthnErrorDetail(error); + +switch (detail?.name) { + case 'NotAllowedError': + // The prompt was dismissed, or the account has no passkey to assert. + break; + case 'SecurityError': + // The origin or RP ID does not match what the API is configured for. + break; + case 'InvalidStateError': + // This authenticator already holds a passkey for the account. + break; + default: + // Not a ceremony failure. Fall back to error?.message. + break; +} +``` + +`getWebAuthnErrorDetail()` returns `undefined` for any error that did not come from a ceremony, so an +HTTP failure keeps flowing through `error.message` and `error.body` as usual. + +### Choosing the authenticator + +By default the browser offers every kind of authenticator the deployment enrols, which is what +`authenticator_policy.attachment: 'any'` means on the API. Pass `attachment` to narrow the picker to +one kind, for example to send someone straight to an issued security key rather than leaving them to +find it in a browser dialog: + +```ts +import { getPasskeyPolicyErrorCode } from '@seamless-auth/react'; + +const { error } = await authClient.registerPasskey({ + metadata, + attachment: 'cross-platform', +}); + +if (getPasskeyPolicyErrorCode(error) === 'attachment_not_allowed') { + // This deployment pins the other kind. Fall back to the default path. +} +``` + +`'cross-platform'` is a roaming authenticator such as a USB or NFC security key. `'platform'` is the +one built into the device, such as Touch ID or Windows Hello. Omit the option to leave the choice to +the deployment. + +This is a request, not an override. A deployment that has pinned +`authenticator_policy.attachment` to the other kind refuses the registration with +`attachment_not_allowed`, covered below. The bundled enrolment view offers a "Use a security key +instead" control that takes this path. + +### Passkey policy refusals + +A registration can also be refused by the policy the API is configured with. `registerPasskey()` +then fails with a body whose `error` is a stable code rather than a sentence, so rendering +`error.message` would put that code in front of a user. Use `getPasskeyPolicyErrorCode()` to branch +on it: + +```ts +import { getPasskeyPolicyErrorCode } from '@seamless-auth/react'; + +const { error } = await authClient.registerPasskey({ metadata }); + +switch (getPasskeyPolicyErrorCode(error)) { + case 'attachment_not_allowed': + // The requested `attachment` is not the kind this deployment enrols. + break; + case 'synced_passkey_not_allowed': + // This passkey syncs to iCloud Keychain or Google Password Manager, and + // this deployment requires a device-bound one such as a security key. + break; + case 'authenticator_not_allowed': + // This authenticator model is not permitted here. + break; + case 'prf_required': + // Registration asked for PRF and the authenticator does not support it. + break; + default: + // No error, or one without a recognized code. Fall back to error?.message. + break; +} +``` + +| Code | Stage | Status | When the API sends it | +| ---------------------------- | --------------- | ------ | -------------------------------------------------------------------------------------------- | +| `attachment_not_allowed` | register/start | 400 | the requested `attachment` is not the kind `authenticator_policy.attachment` pins | +| `synced_passkey_not_allowed` | register/finish | 403 | `authenticator_policy.syncedPasskeys` is `block` and the credential is backup eligible | +| `authenticator_not_allowed` | register/finish | 403 | the credential's AAGUID is on `aaguidDenyList`, or absent from a non-empty `aaguidAllowList` | +| `prf_required` | register/finish | 403 | registration required PRF and the credential did not report support for it | + +`attachment_not_allowed` is refused before any ceremony runs, so the browser never prompts. The rest +are refused after a credential exists and can be inspected. + +`syncedPasskeys` defaults to `allow` on the Seamless Auth API, so a default deployment enrols the +passkeys iCloud Keychain and Google Password Manager create. A deployment that issues its own +authenticators can set `authenticator_policy.syncedPasskeys` to `block` in the API's system config, +and every backup-eligible passkey is then refused at registration. Handle the code: the SDK cannot +tell from the client which way the API is configured. + +Like `getOAuthErrorCode()`, this returns `undefined` for anything it does not recognize, including +codes added by a newer API, so an unexpected refusal keeps your generic messaging. + +The single exception is `isPasskeySupported`-style capability checks: +`isPasskeyPrfSupported(): Promise` is a local check rather than a request, so it returns a +plain boolean. + +## React Hooks For Custom UI + +If you want custom React screens but do not want to manually recreate the client, use the exported hooks: + +```tsx +import { useAuth, useAuthClient, usePasskeySupport } from '@seamless-auth/react'; + +function CustomLogin() { + const { refreshSession } = useAuth(); + const authClient = useAuthClient(); + const { passkeySupported, loading } = usePasskeySupport(); + + async function handleEmailLogin() { + const { error } = await authClient.login({ + identifier: 'user@example.com', + passkeyAvailable: passkeySupported, + }); + + if (!error) { + await refreshSession(); + } + } + + return ( + + ); +} +``` + +### One error style everywhere + +`useAuth()` helpers and the headless client report failure the same way: both return +`{ data, error }` and neither throws. Whatever surface you reach for, the handling is identical. + +```tsx +const { error } = await updateCredential({ ...credential, friendlyName: 'Work laptop' }); + +if (error) { + setMessage(error.message); +} +``` + +Helpers that also mutate provider state, such as `switchOrganization` and `deleteCredential`, apply +that state change only when the call succeeds, then hand the result back for you to inspect. + +## Custom UI Recipes + +Worked examples for the flows the bundled screens cover, using only public primitives. + +### Custom registration + +Registration is two steps: create the account, then verify the emailed code. Call `markSignedIn()` +once the account is live so returning visits can default to sign-in. + +```tsx +import { useAuth, useAuthClient } from '@seamless-auth/react'; +import { useState } from 'react'; + +function CustomRegistration() { + const { markSignedIn, refreshSession } = useAuth(); + const authClient = useAuthClient(); + const [step, setStep] = useState<'details' | 'verify'>('details'); + const [message, setMessage] = useState(''); + + async function createAccount(email: string) { + // Registration needs only an email. A phone can be added and verified later. + const { error } = await authClient.register({ email }); + + if (error) { + setMessage(error.message); + return; + } + + // The API emails a verification code as part of registering. + setStep('verify'); + } + + async function verifyCode(code: string) { + const { error } = await authClient.verifyEmailOtp(code); + + if (error) { + setMessage(error.message); + return; + } + + markSignedIn(); + await refreshSession(); + } + + return step === 'details' ? ( + + ) : ( + authClient.requestEmailOtp()} + error={message} + /> + ); +} +``` + +Enrolment takes the signed-in session, so it comes after the step that establishes one. +Verifying the email OTP signs the user in, which is why the bundled flow offers a passkey +on the screen after it rather than before. + +To offer a passkey right after registering, call `registerPasskey()` before `refreshSession()`: + +```ts +const { data, error } = await authClient.registerPasskey({ + friendlyName: 'My laptop', + platform: 'macOS', + browser: 'Chrome', + deviceInfo: navigator.userAgent, +}); + +if (!error) { + console.log(data.credentialId, data.prfCapable); +} +``` + +### Adding a passkey from a settings screen + +The same call adds a passkey to an account that already has one, or gives one to a user +who declined at signup. Use it from `useAuth()` rather than the client directly: that +version refreshes the session afterwards, so `credentials` includes the new passkey +without a reload. + +```tsx +function AddPasskey() { + const { registerPasskey, credentials } = useAuth(); + + const add = async () => { + const { error } = await registerPasskey({ + friendlyName: 'My laptop', + platform: 'macOS', + browser: 'Chrome', + deviceInfo: navigator.userAgent, + }); + + if (error) { + // A 401 means the session expired rather than anything about the + // authenticator. `isUnauthenticated(error)` tells the two apart. + } + }; + + return ( + <> +

{credentials.length} passkeys

+ + + ); +} +``` + +### OTP and magic-link continuation + +> **The request helpers take no identifier.** `requestMagicLink()`, `requestLoginEmailOtp()`, and +> `requestLoginPhoneOtp()` send nothing but the session cookie. They rely on server-side state +> established by a preceding `login()` call, so calling them without it fails or targets the wrong +> account. This is not obvious from their signatures. Always call `login()` first, and use the same +> browser session for the continuation step. + +```tsx +function CustomLoginContinuation() { + const { login, refreshSession } = useAuth(); + const authClient = useAuthClient(); + + async function start(identifier: string) { + // Required first: this is what the request helpers below depend on. + const { data, error } = await login(identifier, false); + + if (error) { + return; + } + + // Offer only what the server says this account supports. + return data.loginMethods ?? ['magic_link', 'email_otp']; + } + + async function sendEmailCode() { + const { error } = await authClient.requestLoginEmailOtp(); + if (error) { + // surface error.message + } + } + + async function submitEmailCode(code: string) { + const { error } = await authClient.verifyLoginEmailOtp(code); + + if (!error) { + await refreshSession(); + } + } +} +``` + +Magic links complete in whichever tab opens the emailed link, so a custom flow needs two pieces. + +The waiting screen polls until the link is used: + +```ts +const interval = setInterval(async () => { + const { error } = await authClient.checkMagicLink(); + + if (!error) { + clearInterval(interval); + await refreshSession(); + } +}, 5000); +``` + +The landing route verifies the token from the query string, then refreshes its own session: + +```tsx +function CustomMagicLinkLanding() { + const { refreshSession } = useAuth(); // plus: import { useEffect } from 'react' + const authClient = useAuthClient(); + + useEffect(() => { + const token = new URLSearchParams(window.location.search).get('token'); + if (!token) return; + + void authClient.verifyMagicLink(token).then(async ({ error }) => { + if (!error) { + // Refresh here too. This tab set the cookie, but its provider state + // was loaded before the cookie existed. + await refreshSession(); + } + }); + }, [authClient, refreshSession]); + + return

Finishing sign-in...

; +} +``` + +The auth API emails a link pointing at `/verify-magiclink?token=...`, so a custom app must serve that +path. + +### Credential management + +`useAuth()` exposes the signed-in user's passkeys plus helpers to rename and remove them. These +helpers update provider state on success and report failure through `error`. + +```tsx +import { useAuth } from '@seamless-auth/react'; +import type { Credential } from '@seamless-auth/react'; +import { useState } from 'react'; + +function PasskeyList() { + const { credentials, updateCredential, deleteCredential } = useAuth(); + const [message, setMessage] = useState(''); + + async function rename(credential: Credential, friendlyName: string) { + const { error } = await updateCredential({ ...credential, friendlyName }); + + if (error) { + setMessage(error.message); + } + } + + async function remove(credentialId: string) { + const { error } = await deleteCredential(credentialId); + + if (error) { + setMessage(error.message); + } + } + + return ( +
    + {credentials.map(credential => ( +
  • + {credential.friendlyName ?? credential.deviceInfo} + + +
  • + ))} +
+ ); +} +``` + +`Credential.lastUsedAt` and `Credential.createdAt` are ISO 8601 strings, which is what the API sends. +Wrap them yourself to format: + +```tsx +const lastUsed = credential.lastUsedAt ? new Date(credential.lastUsedAt) : null; +``` + +Removing a passkey is a sensitive change. Gate it behind a fresh step-up when the account has other +factors, using `refreshStepUpStatus()` and `verifyStepUpWithPasskey()` from the step-up section. + +## Built-In Routes + +`AuthRoutes` serves these canonical paths: + +- `/login` +- `/passkey-login` +- `/verify-phone-otp` +- `/verify-email-otp` +- `/verify-magic-link` +- `/oauth/callback` +- `/register-passkey` +- `/magic-link-sent` + +These are optional UI wrappers over the same SDK primitives the package now exports for custom flows. + +### Renamed routes + +The earlier mixed-case paths were renamed and are no longer served. Anything linking directly to +them now falls through to `/login`, so update those links: + +| Old path | New path | +| ------------------ | ------------------- | +| `/passKeyLogin` | `/passkey-login` | +| `/verifyPhoneOTP` | `/verify-phone-otp` | +| `/verifyEmailOTP` | `/verify-email-otp` | +| `/registerPasskey` | `/register-passkey` | +| `/magiclinks-sent` | `/magic-link-sent` | + +Two paths are unchanged because they are owned by contracts outside this package: + +- `/verify-magiclink` is the URL the auth API builds when it emails a magic link, so it has to match + that value exactly. Renaming it here would send every emailed link to `/login` with the token + discarded. +- `/oauth/callback` is registered with OAuth providers as an allowed redirect URI, so renaming it + would break configured integrations. + +## Theming The Built-In UI + +Every colour in the built-in screens is a CSS custom property with a fallback, so the default look is +unchanged if you set nothing. To match your brand, set the tokens you care about on `:root`, or on any +element that wraps ``. + +```css +:root { + --seamless-accent: #1f3a34; + --seamless-accent-hover: #16302b; + --seamless-surface: #f7f5f0; + --seamless-text: #1a1a1a; +} +``` + +Scoping to a wrapper also works, which is useful when the auth screens should look different from the +rest of the app: + +```css +.auth-shell { + --seamless-accent: #1f3a34; + --seamless-accent-soft: #3c6f63; +} +``` + +There is no provider prop or JavaScript API for this. Setting the variables is the whole interface. + +### Tokens + +| Token | Used for | Default | +| ---------------------------- | ---------------------------------------------------------------------- | --------------------------------------------- | +| `--seamless-accent` | Primary buttons, focus rings, selected states, accent borders, spinner | `#2563eb` fills, `#3b82f6` rings and borders | +| `--seamless-accent-hover` | Hover state of primary buttons | `#1d4ed8` | +| `--seamless-accent-contrast` | Label text on accent-filled buttons | `white` | +| `--seamless-accent-soft` | Links, toggle buttons, accent icons | `#60a5fa`, `#a5b4fc` on the magic-link screen | +| `--seamless-surface` | Card and modal backgrounds | `#1f2937` | +| `--seamless-surface-raised` | Inputs and panels sitting on a card | `#374151`, `#4b5563` on the login form | +| `--seamless-surface-hover` | Hover state of secondary buttons | `#4b5563` | +| `--seamless-border` | Input, button, and panel borders | `#4b5563`, `#d1d5db` | +| `--seamless-text` | Headings and body text | `white` | +| `--seamless-text-muted` | Labels, helper text, secondary copy | `#9ca3af`, `#d1d5db` | +| `--seamless-danger` | Error messages | `#f87171` | +| `--seamless-success` | Success messages and the verified check icon | `#34d399` | +| `--seamless-warning` | OTP countdown and resend timers | `#facc15` | +| `--seamless-overlay` | Modal backdrop scrim | `rgba(0, 0, 0, 0.45)` | +| `--seamless-shadow` | Card and modal shadow colour | `rgba(0, 0, 0, 0.1)` to `rgba(0, 0, 0, 0.4)` | + +### Notes + +- Where the original design used near-duplicate shades for the same role, each declaration keeps its + own original value as the fallback. That is why some rows list more than one default. Nothing shifts + until you set the token, and once you do, every use of that role picks up your value. +- `--seamless-shadow` is the shadow colour only. Offsets and blur are fixed. Setting it applies one + colour to every shadow in the built-in UI, replacing the per-screen alpha values. +- If you set `--seamless-surface` to a light colour, set `--seamless-text` too. The default text + colour is white and will disappear otherwise. +- Two decorative tints are deliberately not tokenised: the pulse ring behind the magic-link mail icon + and the disc behind the success check. Both are translucent and sit directly under an icon, so an + opaque override would hide the icon it is meant to frame. +- Disabled buttons are not a separate colour. They are the enabled button at reduced opacity, so the + label and its background always come from the same accent pair you set and the contrast between + them cannot invert when the theme changes. `--seamless-disabled` used to set a standalone grey fill + and no longer does anything; remove it from your overrides. +- The package ships one palette and no `prefers-color-scheme` rules. If you want the auth UI to follow + the system theme, wrap your own overrides in a media query. + +## Backend Expectations + +This package assumes a Seamless Auth-compatible backend with the auth adapter mounted at `/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 + +The built-in flows assume compatible endpoints for: + +- `/login` +- `DELETE /logout` for the current session +- `DELETE /logout/all` for every session owned by the current user +- `/registration/register` +- `/webAuthn/login/start` +- `/webAuthn/login/finish` +- `/webAuthn/register/start` +- `/webAuthn/register/finish` +- `POST /otp/generate-phone-otp` +- `POST /otp/generate-email-otp` +- `/otp/verify-phone-otp` +- `/otp/verify-email-otp` +- `POST /otp/generate-login-phone-otp` +- `POST /otp/generate-login-email-otp` +- `/otp/verify-login-phone-otp` +- `/otp/verify-login-email-otp` +- `POST /magic-link` +- `/magic-link/check` +- `/magic-link/verify/:token` +- `/oauth/providers` +- `/oauth/:providerId/start` +- `/oauth/:providerId/callback` +- `/step-up/status` +- `/step-up/webauthn/start` +- `/step-up/webauthn/finish` +- `/totp/status` +- `/totp/enroll/start` +- `/totp/enroll/verify` +- `/totp/disable` +- `/totp/verify-mfa` +- `/users/me` +- `/users/credentials` +- `/users/delete` +- `/organizations` +- `/organizations/:organizationId` +- `/organizations/:organizationId/switch` +- `/organizations/:organizationId/members` +- `/organizations/:organizationId/members/:userId` + +The state-changing OTP and magic-link request routes are `POST` (marked above). They were previously +`GET`, which made them reachable as simple cross-site requests, so an `` tag could trigger SMS or +email sends to a signed-in user. Using `@seamless-auth/react` with an older adapter that only serves the +`GET` forms returns a 404 for those requests. See the changelog for the minimum adapter version. + +`/webAuthn/register/finish` can refuse a verified credential on policy grounds with a `403` whose +body is a stable code. `syncedPasskeys` defaults to `allow`, but a deployment that sets `block` +refuses every backup-eligible passkey, so handle the code rather than assuming the default. See +[Passkey policy refusals](#passkey-policy-refusals). + +## Notes + +- This package does not create its own ``. +- It is designed to fit into your app’s existing routing tree. +- The quickest path is `AuthProvider` + `AuthRoutes`. +- The most flexible path is `AuthProvider` + custom UI using `useAuth()`, `useAuthClient()`, and `usePasskeySupport()`. + +## License + +AGPL-3.0-only diff --git a/packages/react/jest.config.ts b/packages/react/jest.config.ts new file mode 100644 index 0000000..1c0039a --- /dev/null +++ b/packages/react/jest.config.ts @@ -0,0 +1,25 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +export default { + displayName: 'react', + preset: 'ts-jest', + testEnvironment: 'jsdom', + rootDir: '.', + setupFilesAfterEnv: ['/../../jest.setup.ts'], + transform: { + '^.+\\.(t|j)sx?$': ['ts-jest', { useESM: true, tsconfig: '/tsconfig.json' }], + }, + extensionsToTreatAsEsm: ['.ts', '.tsx'], + moduleNameMapper: { + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + '^@/(.*)$': '/src/$1', + // Tests run against the client source, so a change there is exercised here + // without a build in between. + '^@seamless-auth/client$': '/../client/src/index.ts', + }, + testMatch: ['/tests/**/*.(test|spec).[tj]s?(x)'], +}; diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 0000000..729dbbb --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,56 @@ +{ + "name": "@seamless-auth/react", + "version": "0.12.0", + "description": "A drop-in authentication solution for modern React applications.", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "types": "./dist/index.d.ts", + "files": [ + "dist", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "engines": { + "node": ">=24.0.0 <25.0.0", + "npm": ">=9.0.0 <13.0.0" + }, + "scripts": { + "build": "node ../../scripts/clean-dist.mjs && rollup -c && tsc-alias -p tsconfig.build.json", + "typecheck": "tsc --noEmit -p tsconfig.dev.json", + "check-npm-build": "npm pack --dry-run" + }, + "repository": { + "type": "git", + "url": "https://github.com/fells-code/seamless-auth-react.git", + "directory": "packages/react" + }, + "author": "Fells Code, LLC", + "license": "AGPL-3.0-only", + "bugs": { + "url": "https://github.com/fells-code/seamless-auth-react/issues" + }, + "homepage": "https://github.com/fells-code/seamless-auth-react#readme", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/", + "provenance": true + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "react-router-dom": "^6.4.0 || ^7.15.1" + }, + "dependencies": { + "@seamless-auth/client": "^0.0.0", + "@seamless-auth/types": "^0.20.0", + "@simplewebauthn/browser": "^13.1.0", + "libphonenumber-js": "^1.12.7" + }, + "sideEffects": false +} diff --git a/rollup.config.js b/packages/react/rollup.config.js similarity index 97% rename from rollup.config.js rename to packages/react/rollup.config.js index 394e805..2cc802f 100644 --- a/rollup.config.js +++ b/packages/react/rollup.config.js @@ -22,7 +22,7 @@ export default [ 'react', 'react-dom', 'react-router-dom', - + '@seamless-auth/client', '@simplewebauthn/browser', 'libphonenumber-js', ], diff --git a/src/AuthProvider.tsx b/packages/react/src/AuthProvider.tsx similarity index 94% rename from src/AuthProvider.tsx rename to packages/react/src/AuthProvider.tsx index 75357c2..4951d98 100644 --- a/src/AuthProvider.tsx +++ b/packages/react/src/AuthProvider.tsx @@ -20,11 +20,11 @@ import { StartOAuthLoginResult, StepUpPrfData, StepUpStatus, -} from '@/client/createSeamlessAuthClient'; -import type { SeamlessAuthResult } from '@/client/result'; -import { PasskeyPrfInput } from '@/client/webauthnPrf'; -import { createAuthSession } from '@/session/createAuthSession'; -import { Credential, Organization, User } from '@/types'; +} from '@seamless-auth/client'; +import type { SeamlessAuthResult } from '@seamless-auth/client'; +import { PasskeyPrfInput } from '@seamless-auth/client'; +import { createAuthSession } from '@seamless-auth/client'; +import { Credential, Organization, User } from '@seamless-auth/client'; import React, { createContext, ReactNode, diff --git a/src/AuthRoutes.tsx b/packages/react/src/AuthRoutes.tsx similarity index 100% rename from src/AuthRoutes.tsx rename to packages/react/src/AuthRoutes.tsx diff --git a/src/components/AuthFallbackOptions.tsx b/packages/react/src/components/AuthFallbackOptions.tsx similarity index 97% rename from src/components/AuthFallbackOptions.tsx rename to packages/react/src/components/AuthFallbackOptions.tsx index f103029..a1107f7 100644 --- a/src/components/AuthFallbackOptions.tsx +++ b/packages/react/src/components/AuthFallbackOptions.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { isValidEmail, isValidPhoneNumber } from '../utils'; -import type { LoginMethod } from '@/client/createSeamlessAuthClient'; +import type { LoginMethod } from '@seamless-auth/client'; import styles from '../styles/login.module.css'; diff --git a/src/components/MagicLinkSent.tsx b/packages/react/src/components/MagicLinkSent.tsx similarity index 100% rename from src/components/MagicLinkSent.tsx rename to packages/react/src/components/MagicLinkSent.tsx diff --git a/src/components/OAuthProviderButtons.tsx b/packages/react/src/components/OAuthProviderButtons.tsx similarity index 96% rename from src/components/OAuthProviderButtons.tsx rename to packages/react/src/components/OAuthProviderButtons.tsx index 8bc838d..c1b7b48 100644 --- a/src/components/OAuthProviderButtons.tsx +++ b/packages/react/src/components/OAuthProviderButtons.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react'; import { useHref } from 'react-router-dom'; import { useAuth } from '@/AuthProvider'; -import type { OAuthProvider } from '@/client/createSeamlessAuthClient'; +import type { OAuthProvider } from '@seamless-auth/client'; import styles from '../styles/login.module.css'; diff --git a/src/components/OtpInput.tsx b/packages/react/src/components/OtpInput.tsx similarity index 100% rename from src/components/OtpInput.tsx rename to packages/react/src/components/OtpInput.tsx diff --git a/src/components/TermsModal.tsx b/packages/react/src/components/TermsModal.tsx similarity index 100% rename from src/components/TermsModal.tsx rename to packages/react/src/components/TermsModal.tsx diff --git a/src/components/phoneInput.tsx b/packages/react/src/components/phoneInput.tsx similarity index 100% rename from src/components/phoneInput.tsx rename to packages/react/src/components/phoneInput.tsx diff --git a/src/hooks/useAuthClient.ts b/packages/react/src/hooks/useAuthClient.ts similarity index 86% rename from src/hooks/useAuthClient.ts rename to packages/react/src/hooks/useAuthClient.ts index 35b5001..1e9bb17 100644 --- a/src/hooks/useAuthClient.ts +++ b/packages/react/src/hooks/useAuthClient.ts @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import { useAuth } from '@/AuthProvider'; -import { createSeamlessAuthClient } from '@/client/createSeamlessAuthClient'; +import { createSeamlessAuthClient } from '@seamless-auth/client'; export const useAuthClient = () => { const { apiHost, magicLinkRedirectUri } = useAuth(); diff --git a/src/hooks/useLoginMethods.ts b/packages/react/src/hooks/useLoginMethods.ts similarity index 97% rename from src/hooks/useLoginMethods.ts rename to packages/react/src/hooks/useLoginMethods.ts index a57bfd0..d7f4c8e 100644 --- a/src/hooks/useLoginMethods.ts +++ b/packages/react/src/hooks/useLoginMethods.ts @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; -import type { LoginMethod } from '@/client/createSeamlessAuthClient'; +import type { LoginMethod } from '@seamless-auth/client'; import { useAuthClient } from '@/hooks/useAuthClient'; /** diff --git a/src/hooks/usePasskeySupport.ts b/packages/react/src/hooks/usePasskeySupport.ts similarity index 100% rename from src/hooks/usePasskeySupport.ts rename to packages/react/src/hooks/usePasskeySupport.ts diff --git a/src/index.ts b/packages/react/src/index.ts similarity index 90% rename from src/index.ts rename to packages/react/src/index.ts index 35c6bdd..3b72e12 100644 --- a/src/index.ts +++ b/packages/react/src/index.ts @@ -43,7 +43,7 @@ import { TotpEnrollmentStartResult, TotpStatus, UpdateOrganizationInput, -} from '@/client/createSeamlessAuthClient'; +} from '@seamless-auth/client'; import { getOAuthErrorCode, getPasskeyPolicyErrorCode, @@ -53,20 +53,25 @@ import { PasskeyPolicyErrorCode, SeamlessAuthError, WebAuthnErrorDetail, -} from '@/client/errors'; -import type { SeamlessAuthResult } from '@/client/result'; +} from '@seamless-auth/client'; +import type { SeamlessAuthResult } from '@seamless-auth/client'; import { encodePrfSalt, extractPasskeyPrfResult, isPasskeyPrfSupported, PasskeyPrfInput, PasskeyPrfResult, -} from '@/client/webauthnPrf'; +} from '@seamless-auth/client'; import { useAuthClient } from '@/hooks/useAuthClient'; import { hasNonPasskeyLoginMethod, useLoginMethods } from '@/hooks/useLoginMethods'; import { usePasskeySupport } from '@/hooks/usePasskeySupport'; -import { hasScopedRole, roleGrantsAccess } from '@/scopedRoles'; -import { Credential, Organization, OrganizationMembership, User } from '@/types'; +import { hasScopedRole, roleGrantsAccess } from '@seamless-auth/client'; +import { + Credential, + Organization, + OrganizationMembership, + User, +} from '@seamless-auth/client'; export { AuthProvider, diff --git a/src/routes.ts b/packages/react/src/routes.ts similarity index 100% rename from src/routes.ts rename to packages/react/src/routes.ts diff --git a/src/styles/index.css.d.ts b/packages/react/src/styles/index.css.d.ts similarity index 100% rename from src/styles/index.css.d.ts rename to packages/react/src/styles/index.css.d.ts diff --git a/src/styles/login.module.css b/packages/react/src/styles/login.module.css similarity index 100% rename from src/styles/login.module.css rename to packages/react/src/styles/login.module.css diff --git a/src/styles/magiclink.module.css b/packages/react/src/styles/magiclink.module.css similarity index 100% rename from src/styles/magiclink.module.css rename to packages/react/src/styles/magiclink.module.css diff --git a/src/styles/mfaLogin.module.css b/packages/react/src/styles/mfaLogin.module.css similarity index 100% rename from src/styles/mfaLogin.module.css rename to packages/react/src/styles/mfaLogin.module.css diff --git a/src/styles/otpInput.module.css b/packages/react/src/styles/otpInput.module.css similarity index 100% rename from src/styles/otpInput.module.css rename to packages/react/src/styles/otpInput.module.css diff --git a/src/styles/passKeyLogin.module.css b/packages/react/src/styles/passKeyLogin.module.css similarity index 100% rename from src/styles/passKeyLogin.module.css rename to packages/react/src/styles/passKeyLogin.module.css diff --git a/src/styles/registerPasskey.module.css b/packages/react/src/styles/registerPasskey.module.css similarity index 100% rename from src/styles/registerPasskey.module.css rename to packages/react/src/styles/registerPasskey.module.css diff --git a/src/styles/termsModal.module.css b/packages/react/src/styles/termsModal.module.css similarity index 100% rename from src/styles/termsModal.module.css rename to packages/react/src/styles/termsModal.module.css diff --git a/src/styles/verifyMagiclink.module.css b/packages/react/src/styles/verifyMagiclink.module.css similarity index 100% rename from src/styles/verifyMagiclink.module.css rename to packages/react/src/styles/verifyMagiclink.module.css diff --git a/src/styles/verifyOTP.module.css b/packages/react/src/styles/verifyOTP.module.css similarity index 100% rename from src/styles/verifyOTP.module.css rename to packages/react/src/styles/verifyOTP.module.css diff --git a/src/utils.ts b/packages/react/src/utils.ts similarity index 97% rename from src/utils.ts rename to packages/react/src/utils.ts index e61d026..d46477a 100644 --- a/src/utils.ts +++ b/packages/react/src/utils.ts @@ -6,7 +6,7 @@ import parsePhoneNumberFromString from 'libphonenumber-js'; -import { isPlatformAuthenticatorAvailable } from '@/client/webauthnSupport'; +import { isPlatformAuthenticatorAvailable } from '@seamless-auth/client'; /** * isValidEmail * diff --git a/src/views/EmailRegistration.tsx b/packages/react/src/views/EmailRegistration.tsx similarity index 100% rename from src/views/EmailRegistration.tsx rename to packages/react/src/views/EmailRegistration.tsx diff --git a/src/views/Login.tsx b/packages/react/src/views/Login.tsx similarity index 99% rename from src/views/Login.tsx rename to packages/react/src/views/Login.tsx index bcea136..3839fec 100644 --- a/src/views/Login.tsx +++ b/packages/react/src/views/Login.tsx @@ -15,7 +15,7 @@ import styles from '@/styles/login.module.css'; import { isValidEmail, isValidPhoneNumber } from '../utils'; import AuthFallbackOptions from '@/components/AuthFallbackOptions'; import OAuthProviderButtons from '@/components/OAuthProviderButtons'; -import type { LoginMethod } from '@/client/createSeamlessAuthClient'; +import type { LoginMethod } from '@seamless-auth/client'; const Login: React.FC = () => { const navigate = useNavigate(); diff --git a/src/views/OAuthCallback.tsx b/packages/react/src/views/OAuthCallback.tsx similarity index 97% rename from src/views/OAuthCallback.tsx rename to packages/react/src/views/OAuthCallback.tsx index 42626f0..25d81f1 100644 --- a/src/views/OAuthCallback.tsx +++ b/packages/react/src/views/OAuthCallback.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAuth } from '@/AuthProvider'; -import { getOAuthErrorCode, OAuthErrorCode } from '@/client/errors'; +import { getOAuthErrorCode, OAuthErrorCode } from '@seamless-auth/client'; import { OAUTH_PROVIDER_STORAGE_KEY } from '@/components/OAuthProviderButtons'; import styles from '@/styles/verifyMagiclink.module.css'; diff --git a/src/views/PassKeyLogin.tsx b/packages/react/src/views/PassKeyLogin.tsx similarity index 100% rename from src/views/PassKeyLogin.tsx rename to packages/react/src/views/PassKeyLogin.tsx diff --git a/src/views/PassKeyRegistration.tsx b/packages/react/src/views/PassKeyRegistration.tsx similarity index 98% rename from src/views/PassKeyRegistration.tsx rename to packages/react/src/views/PassKeyRegistration.tsx index 1541fba..66d0c0e 100644 --- a/src/views/PassKeyRegistration.tsx +++ b/packages/react/src/views/PassKeyRegistration.tsx @@ -5,12 +5,12 @@ */ import { useAuth } from '@/AuthProvider'; -import { PasskeyAttachment, PasskeyMetadata } from '@/client/createSeamlessAuthClient'; +import { PasskeyAttachment, PasskeyMetadata } from '@seamless-auth/client'; import { getPasskeyPolicyErrorCode, isUnauthenticated, type PasskeyPolicyErrorCode, -} from '@/client/errors'; +} from '@seamless-auth/client'; import React, { useState } from 'react'; import { useAuthClient } from '@/hooks/useAuthClient'; import { hasNonPasskeyLoginMethod, useLoginMethods } from '@/hooks/useLoginMethods'; diff --git a/src/views/PhoneRegistration.tsx b/packages/react/src/views/PhoneRegistration.tsx similarity index 100% rename from src/views/PhoneRegistration.tsx rename to packages/react/src/views/PhoneRegistration.tsx diff --git a/src/views/VerifyMagicLink.tsx b/packages/react/src/views/VerifyMagicLink.tsx similarity index 100% rename from src/views/VerifyMagicLink.tsx rename to packages/react/src/views/VerifyMagicLink.tsx diff --git a/tests/AuthFallbackOptions.test.tsx b/packages/react/tests/AuthFallbackOptions.test.tsx similarity index 100% rename from tests/AuthFallbackOptions.test.tsx rename to packages/react/tests/AuthFallbackOptions.test.tsx diff --git a/tests/AuthRoutes.test.tsx b/packages/react/tests/AuthRoutes.test.tsx similarity index 100% rename from tests/AuthRoutes.test.tsx rename to packages/react/tests/AuthRoutes.test.tsx diff --git a/tests/EmailRegistration.test.tsx b/packages/react/tests/EmailRegistration.test.tsx similarity index 100% rename from tests/EmailRegistration.test.tsx rename to packages/react/tests/EmailRegistration.test.tsx diff --git a/tests/MagicLinkSent.test.tsx b/packages/react/tests/MagicLinkSent.test.tsx similarity index 100% rename from tests/MagicLinkSent.test.tsx rename to packages/react/tests/MagicLinkSent.test.tsx diff --git a/tests/OAuthCallback.test.tsx b/packages/react/tests/OAuthCallback.test.tsx similarity index 98% rename from tests/OAuthCallback.test.tsx rename to packages/react/tests/OAuthCallback.test.tsx index a8eef69..5e894a2 100644 --- a/tests/OAuthCallback.test.tsx +++ b/packages/react/tests/OAuthCallback.test.tsx @@ -8,7 +8,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import OAuthCallback from '@/views/OAuthCallback'; import { useAuth } from '@/AuthProvider'; -import { SeamlessAuthError } from '@/client/errors'; +import { SeamlessAuthError } from '@seamless-auth/client'; import { useNavigate, useSearchParams } from 'react-router-dom'; jest.mock('@/AuthProvider'); diff --git a/tests/OAuthProviderButtons.test.tsx b/packages/react/tests/OAuthProviderButtons.test.tsx similarity index 100% rename from tests/OAuthProviderButtons.test.tsx rename to packages/react/tests/OAuthProviderButtons.test.tsx diff --git a/tests/OtpInput.test.tsx b/packages/react/tests/OtpInput.test.tsx similarity index 100% rename from tests/OtpInput.test.tsx rename to packages/react/tests/OtpInput.test.tsx diff --git a/tests/PassKeyLogin.test.tsx b/packages/react/tests/PassKeyLogin.test.tsx similarity index 100% rename from tests/PassKeyLogin.test.tsx rename to packages/react/tests/PassKeyLogin.test.tsx diff --git a/tests/PhoneInput.test.tsx b/packages/react/tests/PhoneInput.test.tsx similarity index 100% rename from tests/PhoneInput.test.tsx rename to packages/react/tests/PhoneInput.test.tsx diff --git a/tests/PhoneRegistration.test.tsx b/packages/react/tests/PhoneRegistration.test.tsx similarity index 100% rename from tests/PhoneRegistration.test.tsx rename to packages/react/tests/PhoneRegistration.test.tsx diff --git a/tests/RegisterPassKey.test.tsx b/packages/react/tests/RegisterPassKey.test.tsx similarity index 99% rename from tests/RegisterPassKey.test.tsx rename to packages/react/tests/RegisterPassKey.test.tsx index 4c3e970..f4f04a0 100644 --- a/tests/RegisterPassKey.test.tsx +++ b/packages/react/tests/RegisterPassKey.test.tsx @@ -6,7 +6,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import RegisterPasskey from '../src/views/PassKeyRegistration'; -import { SeamlessAuthError } from '@/client/errors'; +import { SeamlessAuthError } from '@seamless-auth/client'; import { useAuthClient } from '@/hooks/useAuthClient'; import { useLoginMethods } from '@/hooks/useLoginMethods'; import { usePasskeySupport } from '@/hooks/usePasskeySupport'; diff --git a/tests/TermsModal.test.tsx b/packages/react/tests/TermsModal.test.tsx similarity index 100% rename from tests/TermsModal.test.tsx rename to packages/react/tests/TermsModal.test.tsx diff --git a/tests/VerifyMagicLink.test.tsx b/packages/react/tests/VerifyMagicLink.test.tsx similarity index 100% rename from tests/VerifyMagicLink.test.tsx rename to packages/react/tests/VerifyMagicLink.test.tsx diff --git a/tests/authProvider.test.tsx b/packages/react/tests/authProvider.test.tsx similarity index 99% rename from tests/authProvider.test.tsx rename to packages/react/tests/authProvider.test.tsx index ecde6f9..9bb7452 100644 --- a/tests/authProvider.test.tsx +++ b/packages/react/tests/authProvider.test.tsx @@ -7,9 +7,9 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { StrictMode } from 'react'; import { AuthProvider, useAuth } from '../src/AuthProvider'; -import { createFetchWithAuth } from '../src/fetchWithAuth'; +import { createFetchWithAuth } from '../../client/src/fetchWithAuth'; -jest.mock('../src/fetchWithAuth'); +jest.mock('../../client/src/fetchWithAuth'); // the mock returned fetch function const mockFetchWithAuthImpl = jest.fn(); diff --git a/tests/login.test.tsx b/packages/react/tests/login.test.tsx similarity index 100% rename from tests/login.test.tsx rename to packages/react/tests/login.test.tsx diff --git a/tests/magicLinkDestination.test.tsx b/packages/react/tests/magicLinkDestination.test.tsx similarity index 97% rename from tests/magicLinkDestination.test.tsx rename to packages/react/tests/magicLinkDestination.test.tsx index b2eed36..7b03df0 100644 --- a/tests/magicLinkDestination.test.tsx +++ b/packages/react/tests/magicLinkDestination.test.tsx @@ -9,13 +9,13 @@ import { render, screen, fireEvent, act } from '@testing-library/react'; import Login from '@/views/Login'; import MagicLinkSent from '@/components/MagicLinkSent'; import { useAuth } from '@/AuthProvider'; -import { createFetchWithAuth } from '@/fetchWithAuth'; +import { createFetchWithAuth } from '../../client/src/fetchWithAuth'; import { useNavigate, useLocation } from 'react-router-dom'; // `useAuthClient` and the client itself stay real here: the whole point is that // the destination survives the trip from provider config into the request body. jest.mock('@/AuthProvider'); -jest.mock('@/fetchWithAuth'); +jest.mock('../../client/src/fetchWithAuth'); jest.mock('@/utils', () => ({ isValidEmail: jest.fn(() => true), isValidPhoneNumber: jest.fn(() => false), diff --git a/tests/useAuthClient.test.tsx b/packages/react/tests/useAuthClient.test.tsx similarity index 89% rename from tests/useAuthClient.test.tsx rename to packages/react/tests/useAuthClient.test.tsx index ad5a50c..22e0944 100644 --- a/tests/useAuthClient.test.tsx +++ b/packages/react/tests/useAuthClient.test.tsx @@ -7,11 +7,11 @@ import { renderHook } from '@testing-library/react'; import { useAuth } from '@/AuthProvider'; -import { createSeamlessAuthClient } from '@/client/createSeamlessAuthClient'; +import { createSeamlessAuthClient } from '../../client/src/client/createSeamlessAuthClient'; import { useAuthClient } from '@/hooks/useAuthClient'; jest.mock('@/AuthProvider'); -jest.mock('@/client/createSeamlessAuthClient'); +jest.mock('../../client/src/client/createSeamlessAuthClient'); describe('useAuthClient', () => { it('creates a client from the current auth config', () => { diff --git a/tests/useLoginMethods.test.tsx b/packages/react/tests/useLoginMethods.test.tsx similarity index 100% rename from tests/useLoginMethods.test.tsx rename to packages/react/tests/useLoginMethods.test.tsx diff --git a/tests/usePasskeySupport.test.tsx b/packages/react/tests/usePasskeySupport.test.tsx similarity index 100% rename from tests/usePasskeySupport.test.tsx rename to packages/react/tests/usePasskeySupport.test.tsx diff --git a/tests/utils.test.ts b/packages/react/tests/utils.test.ts similarity index 100% rename from tests/utils.test.ts rename to packages/react/tests/utils.test.ts diff --git a/packages/react/tsconfig.build.json b/packages/react/tsconfig.build.json new file mode 100644 index 0000000..22f66ac --- /dev/null +++ b/packages/react/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "./dist", + "outDir": "./dist", + "sourceMap": true, + "rootDir": "./src", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["tests", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/react/tsconfig.dev.json b/packages/react/tsconfig.dev.json new file mode 100644 index 0000000..a6668d6 --- /dev/null +++ b/packages/react/tsconfig.dev.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "tests", "../../jest.setup.ts"] +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 0000000..9b7c6d3 --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "paths": { + "@/*": ["./src/*"], + "@seamless-auth/client": ["../client/src/index.ts"] + } + } +} diff --git a/scripts/clean-dist.mjs b/scripts/clean-dist.mjs index a2d5d7e..09aeeec 100644 --- a/scripts/clean-dist.mjs +++ b/scripts/clean-dist.mjs @@ -1,3 +1,5 @@ import { rm } from 'node:fs/promises'; +import path from 'node:path'; -await rm(new URL('../dist', import.meta.url), { force: true, recursive: true }); +// Run from a package directory: clears that package's dist before a build. +await rm(path.join(process.cwd(), 'dist'), { force: true, recursive: true }); diff --git a/tsconfig.json b/tsconfig.base.json similarity index 85% rename from tsconfig.json rename to tsconfig.base.json index a2e990d..9398b73 100644 --- a/tsconfig.json +++ b/tsconfig.base.json @@ -7,9 +7,6 @@ "esModuleInterop": true, "skipLibCheck": true, "strict": true, - "paths": { - "@/*": ["./src/*"] - }, "types": ["jest", "node", "@testing-library/jest-dom"] } } diff --git a/tsconfig.dev.json b/tsconfig.dev.json deleted file mode 100644 index b9ec368..0000000 --- a/tsconfig.dev.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src", "tests"] -}