@foir/sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,9 @@ pnpm add yjs y-websocket # for @foir/sdk/realtime
24
24
  | --- | --- | --- |
25
25
  | `@foir/sdk` | Editor iframe client — `EditorProvider`, `useEditor`, `useAutoResize`, `createPlatformClient`, host↔editor message protocol | `react` |
26
26
  | `@foir/sdk/client` | Client-agnostic GraphQL transport for SSR/edge loaders — `foirRequest`, `isFoirPreview`, `absolutiseFoirMediaUrl` | `graphql` |
27
- | `@foir/sdk/auth` | Framework-agnostic customer auth — login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `authedRequest`, guards | `graphql` |
27
+ | `@foir/sdk/auth` | Framework-agnostic customer auth — login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `createFoirSessionClient` (Foir-hosted cookie sessions), `authedRequest`, guards | `graphql` |
28
+ | `@foir/sdk/react` | Drop-in React auth — `<FoirProvider>` (token or `hostedSession` mode), `useFoirSession`/`useFoirUser`/`useFoirAuth`, `<SignIn>`, `<OAuthButtons>`, `<UserButton>` | `react`, `graphql` |
29
+ | `@foir/sdk/next` | Next.js App Router BFF auth — `createFoirCookieAdapter`, `getFoirSession`, `foirSignIn*`, `completeFoirProviderLogin`, `createFoirMiddleware` | `graphql` |
28
30
  | `@foir/sdk/server` | Server-side helpers for extension/config API backends — `createServerClient`, `verifyWebhookSignature`, `verifyScopedToken`, callback client | — |
29
31
  | `@foir/sdk/media` | Progressive media — `ProgressiveMedia`, `BlurhashCanvas`, `useProgressiveLoad`, variant-tier adapters | `react` (`hls.js`, `blurhash` optional) |
30
32
  | `@foir/sdk/content` | Content-rendering primitives — `createContentRenderer` block dispatch + Lexical `RichTextRenderer` | `react` |
@@ -72,6 +74,110 @@ const customer = await loginWithPassword(adapter, env, { email, password });
72
74
  const { myProfile } = await authedRequest(adapter, env, MyProfileDocument);
73
75
  ```
74
76
 
77
+ ### React
78
+
79
+ `@foir/sdk/react` builds the reactive layer on top of `@foir/sdk/auth`. Wrap
80
+ your app once, then read the session with hooks or drop in the components. The
81
+ default session store is `localStorage`; pass your own `adapter` for cookies.
82
+
83
+ ```tsx
84
+ 'use client';
85
+ import { FoirProvider, SignIn, UserButton, useFoirSession } from '@foir/sdk/react';
86
+
87
+ const env = {
88
+ PLATFORM_API_URL: process.env.NEXT_PUBLIC_PLATFORM_API_URL,
89
+ PLATFORM_API_KEY: process.env.NEXT_PUBLIC_PLATFORM_API_KEY, // a publishable pk_ key
90
+ };
91
+
92
+ export function App() {
93
+ return (
94
+ <FoirProvider env={env}>
95
+ <Header />
96
+ <SignIn afterSignInUrl="/dashboard" />
97
+ </FoirProvider>
98
+ );
99
+ }
100
+
101
+ function Header() {
102
+ const { isAuthenticated, isLoading } = useFoirSession();
103
+ if (isLoading) return null;
104
+ return isAuthenticated ? <UserButton afterSignOutUrl="/" /> : null;
105
+ }
106
+ ```
107
+
108
+ For a custom form, use `useFoirAuth()` (`signIn`, `register`, `signInWithOtp`,
109
+ `requestOtp`, `signInWithProvider`, `signOut`, `getToken`) and `useFoirUser()`.
110
+
111
+ ### Backend-less SPA (Foir-hosted sessions)
112
+
113
+ By default `<FoirProvider>` stores tokens in `localStorage`. If your SPA has no
114
+ backend but you have a **verified custom login domain** on the same registrable
115
+ domain (`app.acme.com` + `login.acme.com`), prefer `hostedSession`: the durable
116
+ credential becomes an HttpOnly cookie Foir sets on your domain, and the 15-minute
117
+ access token lives only in memory. Nothing authentication-related is reachable
118
+ from JS, so XSS has nothing to exfiltrate — the BFF security posture without a
119
+ backend.
120
+
121
+ ```tsx
122
+ <FoirProvider
123
+ env={{ PLATFORM_API_URL: 'https://api.foir.dev/graphql', PLATFORM_API_KEY: 'pk_…' }}
124
+ hostedSession={{ loginOrigin: 'https://login.acme.com' }}
125
+ >
126
+ <App />
127
+ </FoirProvider>
128
+ ```
129
+
130
+ Sign-in is a redirect, not a form — `useFoirAuth().hostedSignInUrl()` gives you
131
+ the URL (the password/OTP actions throw in this mode). `getToken()` exchanges the
132
+ cookie and re-exchanges it silently before expiry; `signOut()` revokes the session
133
+ server-side. Everything else — `useFoirSession`, `useFoirUser`, `<UserButton>` —
134
+ behaves the same.
135
+
136
+ Framework-agnostic equivalent:
137
+
138
+ ```ts
139
+ import { createFoirSessionClient } from '@foir/sdk/auth';
140
+
141
+ const session = createFoirSessionClient({ loginOrigin: 'https://login.acme.com' });
142
+ const token = await session.getToken(); // null when signed out
143
+ ```
144
+
145
+ ### Next.js (App Router, BFF)
146
+
147
+ `@foir/sdk/next` keeps the tokens in HttpOnly cookies server-side. Protect
148
+ routes with the middleware, read the session in a server component, and sign in
149
+ from a route handler.
150
+
151
+ ```ts
152
+ // middleware.ts
153
+ import { createFoirMiddleware } from '@foir/sdk/next';
154
+ export const middleware = createFoirMiddleware({ signInUrl: '/sign-in' });
155
+ export const config = { matcher: ['/dashboard/:path*'] };
156
+ ```
157
+
158
+ ```ts
159
+ // app/dashboard/page.tsx (Server Component)
160
+ import { cookies } from 'next/headers';
161
+ import { getFoirSession } from '@foir/sdk/next';
162
+
163
+ export default async function Dashboard() {
164
+ const customer = await getFoirSession(await cookies());
165
+ return <p>Signed in as {customer?.email}</p>;
166
+ }
167
+ ```
168
+
169
+ ```ts
170
+ // app/sign-in/route.ts (Route Handler)
171
+ import { cookies } from 'next/headers';
172
+ import { foirSignInWithPassword } from '@foir/sdk/next';
173
+
174
+ export async function POST(req: Request) {
175
+ const { email, password } = await req.json();
176
+ await foirSignInWithPassword({ env, store: await cookies(), email, password });
177
+ return Response.json({ ok: true });
178
+ }
179
+ ```
180
+
75
181
  ### Codegen
76
182
 
77
183
  ```ts
package/dist/auth.d.ts CHANGED
@@ -302,4 +302,81 @@ interface HostedSession {
302
302
  */
303
303
  declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<HostedSession | null>;
304
304
 
305
- export { type AuthProviderPublic, AuthProvidersDocument, type CustomerAuthResult, CustomerLoginDocument, CustomerLoginOTPDocument, CustomerLoginWithProviderDocument, CustomerProviderCallbackDocument, CustomerProviderVerifyOTPDocument, CustomerRefreshTokenDocument, CustomerRegisterDocument, CustomerRequestOTPDocument, type FoirAuthTokens, type FoirCustomer, type FoirUser, type HostedLoginConfig, type HostedSession, type ProviderCallbackInput, type ProviderLoginInput, type ProviderLoginResult, type ProviderOTPVerifyInput, type RequireAdminOptions, type RequireAuthOptions, type SessionAdapter, authedRequest, clearSession, customerLogin, customerLoginOTP, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, customerRefreshToken, customerRegister, customerRequestOTP, fetchHostedSession, getAuthProviders, getSessionCustomer, getSessionToken, hostedLoginUrl, loginWithOTP, loginWithPassword, refreshSession, registerWithPassword, requireAdmin, requireAuth, saveSession };
305
+ /**
306
+ * Foir-hosted first-party sessions, from the browser — the model for a
307
+ * backend-less SPA (Vite, CRA, a static site) that wants no tokens in JS.
308
+ *
309
+ * The sibling `fetchHostedSession` in ./hosted does the same exchange from a
310
+ * SERVER: it reads the cookie off an incoming request and forwards it by hand.
311
+ * A browser cannot set a `Cookie` header, so that function is unusable from an
312
+ * SPA by construction. This one lets the browser send the cookie the only way it
313
+ * can — `credentials: 'include'` — and keeps the resulting bearer in memory.
314
+ *
315
+ * How the pieces divide:
316
+ *
317
+ * cookie — long-lived (30d), HttpOnly, first-party on your registrable
318
+ * domain, set by Foir when hosted login finalizes. JS cannot read
319
+ * it, so XSS cannot exfiltrate it.
320
+ * bearer — 15 minutes, held in a closure here. Never written to
321
+ * localStorage/sessionStorage/IndexedDB, never in a cookie of ours.
322
+ *
323
+ * There is no refresh token in this flow. Re-exchanging the cookie IS the
324
+ * refresh, which is why nothing here needs rotation or reuse-detection: the
325
+ * durable credential is an opaque server-side session handle, revocable at any
326
+ * time from the admin panel or `signOut()`.
327
+ *
328
+ * REQUIRES a verified custom login domain that shares a registrable domain with
329
+ * your app (app.acme.com + login.acme.com). That is what makes the cookie
330
+ * first-party and lets `SameSite=Lax` send it. The same endpoints exist on
331
+ * api.foir.dev but a browser cannot reach them there — the cookie would be
332
+ * cross-site, and that CORS surface deliberately refuses credentials.
333
+ */
334
+ interface FoirSessionConfig {
335
+ /**
336
+ * Your verified custom login domain, e.g. `https://login.acme.com`. Must share
337
+ * a registrable domain with the app calling it.
338
+ */
339
+ loginOrigin: string;
340
+ /** Session-exchange path. Default `/session/token`. */
341
+ sessionPath?: string;
342
+ /** Logout path. Default `/session/logout`. */
343
+ logoutPath?: string;
344
+ /** Seconds before expiry to re-exchange. Default 60. */
345
+ refreshSkewSeconds?: number;
346
+ /**
347
+ * Called when the session goes away — the cookie expired, was revoked, or
348
+ * `signOut()` ran. Use it to redirect to `signInUrl()`.
349
+ */
350
+ onSignOut?: () => void;
351
+ }
352
+ /** The identity behind the current session. */
353
+ interface FoirSessionUser {
354
+ customerId: string;
355
+ email: string;
356
+ }
357
+ interface FoirSessionClient {
358
+ /**
359
+ * The current access token, exchanging or re-exchanging the cookie as needed.
360
+ * Null when there is no session. Concurrent callers share one in-flight
361
+ * exchange. Call this before each API request — do not cache the result.
362
+ */
363
+ getToken(): Promise<string | null>;
364
+ /** The signed-in customer, or null. Exchanges if the session isn't loaded yet. */
365
+ getUser(): Promise<FoirSessionUser | null>;
366
+ /** Revoke the session server-side and clear the cookie. Idempotent. */
367
+ signOut(): Promise<void>;
368
+ /** Where to send the browser to sign in. */
369
+ signInUrl(returnTo?: string): string;
370
+ /** Observe session changes. Returns an unsubscribe function. */
371
+ subscribe(listener: (user: FoirSessionUser | null) => void): () => void;
372
+ /** Cancel the refresh timer. Call on unmount. */
373
+ destroy(): void;
374
+ }
375
+ /**
376
+ * Create a session client. Safe to call during SSR — it does nothing until a
377
+ * method is invoked, and `getToken()` on the server returns null rather than
378
+ * throwing (there is no cookie jar to draw on).
379
+ */
380
+ declare function createFoirSessionClient(config: FoirSessionConfig): FoirSessionClient;
381
+
382
+ export { type AuthProviderPublic, AuthProvidersDocument, type CustomerAuthResult, CustomerLoginDocument, CustomerLoginOTPDocument, CustomerLoginWithProviderDocument, CustomerProviderCallbackDocument, CustomerProviderVerifyOTPDocument, CustomerRefreshTokenDocument, CustomerRegisterDocument, CustomerRequestOTPDocument, type FoirAuthTokens, type FoirCustomer, type FoirSessionClient, type FoirSessionConfig, type FoirSessionUser, type FoirUser, type HostedLoginConfig, type HostedSession, type ProviderCallbackInput, type ProviderLoginInput, type ProviderLoginResult, type ProviderOTPVerifyInput, type RequireAdminOptions, type RequireAuthOptions, type SessionAdapter, authedRequest, clearSession, createFoirSessionClient, customerLogin, customerLoginOTP, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, customerRefreshToken, customerRegister, customerRequestOTP, fetchHostedSession, getAuthProviders, getSessionCustomer, getSessionToken, hostedLoginUrl, loginWithOTP, loginWithPassword, refreshSession, registerWithPassword, requireAdmin, requireAuth, saveSession };
package/dist/auth.js CHANGED
@@ -274,6 +274,141 @@ async function fetchHostedSession(request, config) {
274
274
  function escapeRegExp(s) {
275
275
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
276
276
  }
277
+
278
+ // src/auth/session-client.ts
279
+ var DEFAULT_SESSION_PATH2 = "/session/token";
280
+ var DEFAULT_LOGOUT_PATH = "/session/logout";
281
+ var DEFAULT_REFRESH_SKEW_SECONDS = 60;
282
+ var MIN_REFRESH_DELAY_MS = 5e3;
283
+ var CSRF_HEADER = { "X-Foir-Session": "1" };
284
+ function createFoirSessionClient(config) {
285
+ const sessionUrl = `${trimSlash(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH2}`;
286
+ const logoutUrl = `${trimSlash(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
287
+ const skewMs = (config.refreshSkewSeconds ?? DEFAULT_REFRESH_SKEW_SECONDS) * 1e3;
288
+ let cached = null;
289
+ let inflight = null;
290
+ let timer = null;
291
+ let destroyed = false;
292
+ const listeners = /* @__PURE__ */ new Set();
293
+ function emit(user) {
294
+ for (const l of listeners) l(user);
295
+ }
296
+ function clearTimer() {
297
+ if (timer !== null) {
298
+ clearTimeout(timer);
299
+ timer = null;
300
+ }
301
+ }
302
+ function scheduleRefresh(expiresAt) {
303
+ clearTimer();
304
+ if (destroyed || typeof setTimeout === "undefined") return;
305
+ const delay = Math.max(expiresAt - Date.now() - skewMs, MIN_REFRESH_DELAY_MS);
306
+ timer = setTimeout(() => {
307
+ void exchange();
308
+ }, delay);
309
+ timer.unref?.();
310
+ }
311
+ function forget() {
312
+ const had = cached !== null;
313
+ cached = null;
314
+ clearTimer();
315
+ if (had) emit(null);
316
+ }
317
+ function exchange() {
318
+ if (inflight) return inflight;
319
+ const pending = (async () => {
320
+ let res;
321
+ try {
322
+ res = await fetch(sessionUrl, {
323
+ method: "GET",
324
+ // The whole point: send the first-party cookie cross-origin (but
325
+ // same-site) to the login domain. Without this the cookie is not sent.
326
+ credentials: "include",
327
+ headers: { ...CSRF_HEADER }
328
+ });
329
+ } catch {
330
+ return cached;
331
+ }
332
+ if (res.status === 401) {
333
+ forget();
334
+ config.onSignOut?.();
335
+ return null;
336
+ }
337
+ if (!res.ok) return cached;
338
+ let json;
339
+ try {
340
+ json = await res.json();
341
+ } catch {
342
+ return cached;
343
+ }
344
+ if (!json.access_token || !json.customer_id) return cached;
345
+ const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 0;
346
+ const next = {
347
+ token: json.access_token,
348
+ expiresAt: Date.now() + expiresIn * 1e3,
349
+ user: { customerId: json.customer_id, email: json.email ?? "" }
350
+ };
351
+ const changed = cached?.user.customerId !== next.user.customerId;
352
+ cached = next;
353
+ scheduleRefresh(next.expiresAt);
354
+ if (changed) emit(next.user);
355
+ return next;
356
+ })();
357
+ inflight = pending;
358
+ void pending.finally(() => {
359
+ if (inflight === pending) inflight = null;
360
+ });
361
+ return pending;
362
+ }
363
+ function fresh() {
364
+ if (!cached) return null;
365
+ return Date.now() < cached.expiresAt - skewMs ? cached : null;
366
+ }
367
+ return {
368
+ async getToken() {
369
+ const hit = fresh();
370
+ if (hit) return hit.token;
371
+ const got = await exchange();
372
+ return got?.token ?? null;
373
+ },
374
+ async getUser() {
375
+ const hit = fresh();
376
+ if (hit) return hit.user;
377
+ const got = await exchange();
378
+ return got?.user ?? null;
379
+ },
380
+ async signOut() {
381
+ try {
382
+ await fetch(logoutUrl, {
383
+ method: "POST",
384
+ credentials: "include",
385
+ headers: { ...CSRF_HEADER }
386
+ });
387
+ } catch {
388
+ }
389
+ forget();
390
+ config.onSignOut?.();
391
+ },
392
+ signInUrl(returnTo) {
393
+ const dest = new URL(trimSlash(config.loginOrigin));
394
+ const target = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
395
+ if (target && target !== "/") dest.searchParams.set("returnTo", target);
396
+ return dest.toString();
397
+ },
398
+ subscribe(listener) {
399
+ listeners.add(listener);
400
+ return () => listeners.delete(listener);
401
+ },
402
+ destroy() {
403
+ destroyed = true;
404
+ clearTimer();
405
+ listeners.clear();
406
+ }
407
+ };
408
+ }
409
+ function trimSlash(s) {
410
+ return s.replace(/\/+$/, "");
411
+ }
277
412
  export {
278
413
  AuthProvidersDocument,
279
414
  CustomerLoginDocument,
@@ -286,6 +421,7 @@ export {
286
421
  CustomerRequestOTPDocument,
287
422
  authedRequest,
288
423
  clearSession,
424
+ createFoirSessionClient,
289
425
  customerLogin,
290
426
  customerLoginOTP,
291
427
  customerLoginWithProvider,
package/dist/next.d.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Foir GraphQL client — a single typed fetch wrapper for SSR/edge loaders.
3
+ *
4
+ * No GraphQL client (Apollo/urql) required: the Foir public API is a plain
5
+ * `POST` with an `x-api-key` header, and the dominant consumers (Hydrogen on
6
+ * Oxygen, React Router loaders, TanStack Start on CF Workers) fetch in
7
+ * server-only loaders and render — so Apollo's `InMemoryCache`/`useQuery`
8
+ * reactivity is dead weight, and a shared cache on a stateless multi-tenant
9
+ * edge worker is a cross-tenant hazard. This helper carries only the
10
+ * Foir-specific transport concerns (auth header, preview schema channel,
11
+ * error shape). Consumers on a GraphQL client (Apollo / urql / react-query /
12
+ * graphql-request) use the generated `TypedDocumentNode`s directly with their
13
+ * own client and replicate the two Foir-specific headers below — the
14
+ * `x-api-key` and, in preview, `X-Foir-Schema-Channel: draft`.
15
+ *
16
+ * Deduplicates the hand-copied `lib/foir/client.ts` that shipped identically
17
+ * across the Foir storefronts.
18
+ */
19
+
20
+ interface FoirEnv {
21
+ PLATFORM_API_URL?: string;
22
+ PLATFORM_API_KEY?: string;
23
+ PLATFORM_PREVIEW_API_KEY?: string;
24
+ FOIR_PREVIEW?: string;
25
+ }
26
+
27
+ /**
28
+ * Foir customer auth — framework-agnostic flows, token lifecycle, and guards.
29
+ *
30
+ * Session I/O is injected via {@link SessionAdapter} (no Hydrogen/React-Router
31
+ * coupling). Pre-login flows authenticate with the project `x-api-key`;
32
+ * post-login reads add the customer bearer. The project-specific profile (e.g.
33
+ * `isAdmin`) is resolved by a consumer-supplied `resolveUser` — the SDK never
34
+ * hardcodes the per-project customer profile shape.
35
+ */
36
+
37
+ /** The customer identity carried by an auth result. */
38
+ interface FoirCustomer {
39
+ id: string;
40
+ email: string;
41
+ }
42
+ /**
43
+ * Injected session store. Methods may be sync or async so consumers can plug
44
+ * in a cookie session (Hydrogen/Remix), a KV store, etc.
45
+ */
46
+ interface SessionAdapter {
47
+ get(key: string): string | null | undefined | Promise<string | null | undefined>;
48
+ set(key: string, value: string): void | Promise<void>;
49
+ unset(key: string): void | Promise<void>;
50
+ }
51
+
52
+ /**
53
+ * The minimal cookie-store shape this adapter needs. It's satisfied by Next's
54
+ * `cookies()` (route handlers / server actions — `await cookies()` on Next 15),
55
+ * and by `NextResponse.cookies` in middleware. Kept structural on purpose so the
56
+ * SDK doesn't import (or pin a version of) `next/headers`.
57
+ */
58
+ interface FoirCookieStore {
59
+ get(name: string): {
60
+ value: string;
61
+ } | undefined;
62
+ set(name: string, value: string, options?: Record<string, unknown>): void;
63
+ delete(name: string): void;
64
+ }
65
+ interface CookieAdapterOptions {
66
+ /** Cookie attributes for writes, merged over the secure defaults. */
67
+ cookieOptions?: Record<string, unknown>;
68
+ }
69
+ declare function foirCookieName(key: string): string;
70
+ /**
71
+ * A {@link SessionAdapter} backed by an HttpOnly cookie store — the seam for
72
+ * server-side (BFF) Foir sessions in Next.js. Pass the cookie store from a route
73
+ * handler / server action (`await cookies()`) for reads and writes; a Server
74
+ * Component's `cookies()` is read-only, so use it only for reads there.
75
+ */
76
+ declare function createFoirCookieAdapter(store: FoirCookieStore, opts?: CookieAdapterOptions): SessionAdapter;
77
+
78
+ /**
79
+ * Server-side session helpers for the Next.js BFF pattern. Each takes the Next
80
+ * cookie store (`await cookies()` in a route handler / server action, or the
81
+ * read-only `cookies()` in a Server Component for reads).
82
+ *
83
+ * Note on "verification": the customer access token is opaque to your app and
84
+ * validated by the Foir API on use, so `getFoirSession` returns the identity
85
+ * stored in the cookies rather than locally verifying a JWT. A rejected/expired
86
+ * bearer surfaces when you call the API (and `@foir/sdk/auth`'s `authedRequest`
87
+ * transparently refreshes it).
88
+ */
89
+ /** The signed-in customer from the session cookies, or null. */
90
+ declare function getFoirSession(store: FoirCookieStore): Promise<FoirCustomer | null>;
91
+ /** The current access token from the session cookies, or null. */
92
+ declare function getFoirToken(store: FoirCookieStore): Promise<string | null>;
93
+ /** Clear the session cookies (sign out). */
94
+ declare function foirSignOut(store: FoirCookieStore, opts?: CookieAdapterOptions): Promise<void>;
95
+ interface PasswordArgs {
96
+ env: FoirEnv;
97
+ store: FoirCookieStore;
98
+ email: string;
99
+ password: string;
100
+ cookieOptions?: CookieAdapterOptions['cookieOptions'];
101
+ }
102
+ /** Password sign-in in a route handler; persists the session to cookies. */
103
+ declare function foirSignInWithPassword({ env, store, email, password, cookieOptions, }: PasswordArgs): Promise<FoirCustomer>;
104
+ /** Register a customer in a route handler; persists the session to cookies. */
105
+ declare function foirRegisterWithPassword({ env, store, email, password, cookieOptions, }: PasswordArgs): Promise<FoirCustomer>;
106
+ /** Complete an OTP sign-in in a route handler; persists the session to cookies. */
107
+ declare function foirSignInWithOtp({ env, store, email, otp, cookieOptions, }: {
108
+ env: FoirEnv;
109
+ store: FoirCookieStore;
110
+ email: string;
111
+ otp: string;
112
+ cookieOptions?: CookieAdapterOptions['cookieOptions'];
113
+ }): Promise<FoirCustomer>;
114
+ /**
115
+ * Complete a provider (OAuth/OIDC) login in your callback route handler:
116
+ * exchange the `code` + `state` for tokens and persist them to the session
117
+ * cookies. Returns the customer.
118
+ */
119
+ declare function completeFoirProviderLogin(opts: {
120
+ env: FoirEnv;
121
+ store: FoirCookieStore;
122
+ providerKey: string;
123
+ code?: string;
124
+ state?: string;
125
+ cookieOptions?: CookieAdapterOptions['cookieOptions'];
126
+ }): Promise<FoirCustomer>;
127
+
128
+ /**
129
+ * The slice of `NextRequest` the middleware reads. Typed structurally so the SDK
130
+ * doesn't need `next` installed to build — a real `NextRequest` satisfies it, so
131
+ * the returned function drops straight into `export const middleware`.
132
+ */
133
+ interface FoirMiddlewareRequest {
134
+ cookies: {
135
+ get(name: string): {
136
+ value: string;
137
+ } | undefined;
138
+ };
139
+ nextUrl: {
140
+ pathname: string;
141
+ search: string;
142
+ };
143
+ url: string;
144
+ }
145
+ interface FoirMiddlewareConfig {
146
+ /** Where to send unauthenticated requests (path or absolute URL). */
147
+ signInUrl: string;
148
+ /**
149
+ * The session cookie whose presence means "signed in". Defaults to the
150
+ * access-token cookie the cookie adapter writes (`foir_token`).
151
+ */
152
+ cookieName?: string;
153
+ /**
154
+ * Return true for requests that require auth. Omit to protect every request
155
+ * the middleware `matcher` passes through.
156
+ */
157
+ isProtected?(request: FoirMiddlewareRequest): boolean;
158
+ /** Query param carrying the original path on the sign-in URL. Default `returnTo`. */
159
+ returnParam?: string;
160
+ }
161
+ /**
162
+ * Build a Next.js middleware that redirects unauthenticated requests to your
163
+ * sign-in URL. Edge-safe: it checks for the presence of the session cookie only
164
+ * — the bearer itself is validated server-side when you call the Foir API — so
165
+ * pair it with a real `getFoirSession` check in protected route handlers /
166
+ * server components. Returns a standard `Response` (redirect) or `undefined`
167
+ * (continue), both of which Next.js middleware accepts.
168
+ *
169
+ * ```ts
170
+ * // middleware.ts
171
+ * import { createFoirMiddleware } from '@foir/sdk/next';
172
+ * export const middleware = createFoirMiddleware({ signInUrl: '/sign-in' });
173
+ * export const config = { matcher: ['/dashboard/:path*'] };
174
+ * ```
175
+ */
176
+ declare function createFoirMiddleware(config: FoirMiddlewareConfig): (request: FoirMiddlewareRequest) => Response | undefined;
177
+
178
+ export { type CookieAdapterOptions, type FoirCookieStore, type FoirCustomer, type FoirEnv, type FoirMiddlewareConfig, type FoirMiddlewareRequest, type SessionAdapter, completeFoirProviderLogin, createFoirCookieAdapter, createFoirMiddleware, foirCookieName, foirRegisterWithPassword, foirSignInWithOtp, foirSignInWithPassword, foirSignOut, getFoirSession, getFoirToken };