react-shopwave-connect 0.2.0 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["body: URLSearchParams | FormData","response: Response","cookieOptions: SessionOptions[\"cookieOptions\"]","options: SessionOptions","returnTo","token: ShopwaveToken"],"sources":["../../src/server/errors.ts","../../src/server/token.ts","../../src/server/oauth.ts","../../src/server/returnTo.ts","../../src/next/index.ts"],"sourcesContent":["/**\n * Error thrown by the server-side auth helpers.\n *\n * `code` is stable and safe to branch on; `status` is the HTTP status returned\n * by the Shopwave auth server when there was one.\n */\nexport type ShopwaveAuthErrorCode =\n | \"config_invalid\"\n | \"token_exchange_failed\"\n | \"token_refresh_failed\"\n | \"token_response_invalid\"\n | \"network_error\";\n\nexport class ShopwaveAuthError extends Error {\n readonly code: ShopwaveAuthErrorCode;\n readonly status?: number;\n /** Raw response body from the auth server, if any. Never contains our secret. */\n readonly body?: string;\n\n constructor(\n code: ShopwaveAuthErrorCode,\n message: string,\n details: { status?: number; body?: string; cause?: unknown } = {}\n ) {\n super(message, details.cause !== undefined ? { cause: details.cause } : undefined);\n this.name = \"ShopwaveAuthError\";\n this.code = code;\n this.status = details.status;\n this.body = details.body;\n }\n\n /**\n * True when the auth server rejected the grant itself (bad/expired code or\n * refresh token) rather than failing for a transient reason. Callers should\n * treat the user as logged out.\n */\n get isInvalidGrant(): boolean {\n return (\n (this.code === \"token_exchange_failed\" || this.code === \"token_refresh_failed\") &&\n this.status !== undefined &&\n this.status >= 400 &&\n this.status < 500\n );\n }\n}\n","/**\n * Token model + helpers shared by every server-side integration.\n *\n * The Shopwave auth server returns (snake_case):\n * { access_token, refresh_token, token_type: \"OAuth\", expires_in: 43200 }\n *\n * We store a normalised, camelCase shape with an absolute expiry so any\n * request can decide whether the token is still usable without extra state.\n */\n\nexport interface ShopwaveToken {\n accessToken: string;\n /** Long-lived; the Shopwave server does not rotate it on refresh. */\n refreshToken?: string;\n /** Scheme used in the Authorization header. Shopwave uses \"OAuth\". */\n tokenType: string;\n /** Absolute expiry, epoch milliseconds. Undefined if the server didn't say. */\n expiresAt?: number;\n}\n\n/** Raw token response from `POST {authServer}/oauth/token`. */\nexport interface ShopwaveTokenResponse {\n access_token?: string;\n refresh_token?: string;\n token_type?: string;\n expires_in?: number | string;\n [key: string]: unknown;\n}\n\n/** Shopwave API error id meaning \"access token expired / invalid\". */\nexport const SHOPWAVE_TOKEN_EXPIRED_ERROR_ID = 908;\n\n/**\n * Converts a token-endpoint response into a {@link ShopwaveToken}.\n * Returns `null` when the response has no access token.\n *\n * @param previous the token being refreshed; its refresh token is kept when\n * the server doesn't send a new one.\n */\nexport function tokenFromResponse(\n response: ShopwaveTokenResponse,\n previous?: ShopwaveToken,\n now: number = Date.now()\n): ShopwaveToken | null {\n if (!response || typeof response.access_token !== \"string\" || !response.access_token) {\n return null;\n }\n\n const expiresIn = Number(response.expires_in);\n\n return {\n accessToken: response.access_token,\n refreshToken:\n (typeof response.refresh_token === \"string\" && response.refresh_token) ||\n previous?.refreshToken,\n tokenType: (typeof response.token_type === \"string\" && response.token_type) || \"OAuth\",\n expiresAt: Number.isFinite(expiresIn) && expiresIn > 0 ? now + expiresIn * 1000 : undefined,\n };\n}\n\n/**\n * Reads a token from session storage. Accepts both the current shape and the\n * legacy raw response that older apps (e.g. AdminUI ≤ 0.1) stored directly in\n * the session, so existing sessions survive an upgrade.\n */\nexport function normalizeStoredToken(raw: unknown): ShopwaveToken | null {\n if (!raw || typeof raw !== \"object\") return null;\n const t = raw as Record<string, unknown>;\n\n if (typeof t.accessToken === \"string\" && t.accessToken) {\n return {\n accessToken: t.accessToken,\n refreshToken: typeof t.refreshToken === \"string\" ? t.refreshToken : undefined,\n tokenType: typeof t.tokenType === \"string\" && t.tokenType ? t.tokenType : \"OAuth\",\n expiresAt: typeof t.expiresAt === \"number\" ? t.expiresAt : undefined,\n };\n }\n\n if (typeof t.access_token === \"string\" && t.access_token) {\n // Legacy shape: we don't know when it was issued, so no expiresAt.\n // It will be refreshed reactively when the API reports it expired.\n return {\n accessToken: t.access_token,\n refreshToken: typeof t.refresh_token === \"string\" ? t.refresh_token : undefined,\n tokenType: typeof t.token_type === \"string\" && t.token_type ? t.token_type : \"OAuth\",\n };\n }\n\n return null;\n}\n\n/**\n * True when the token has passed its expiry (minus `skewSeconds`).\n * Tokens without a known expiry are treated as valid.\n *\n * Shopwave only issues a new access token once the old one has actually\n * expired, so the default skew is 0 — refreshing early just returns the same\n * token.\n */\nexport function isTokenExpired(\n token: ShopwaveToken,\n { skewSeconds = 0, now = Date.now() }: { skewSeconds?: number; now?: number } = {}\n): boolean {\n if (token.expiresAt === undefined) return false;\n return now >= token.expiresAt - skewSeconds * 1000;\n}\n\n/** `Authorization` header value, e.g. `OAuth 111ad…`. */\nexport function authorizationHeader(token: ShopwaveToken): string {\n return `${token.tokenType || \"OAuth\"} ${token.accessToken}`;\n}\n\n/**\n * True when a Shopwave API response means \"your access token is no longer\n * valid\": HTTP 401, or the API error 908 in the response envelope.\n * Use it to trigger a forced refresh + a single retry.\n */\nexport function isExpiredTokenResponse(status: number, body?: unknown): boolean {\n if (status === 401) return true;\n const errors = (body as { api?: { message?: { errors?: Record<string, { id?: number }> } } })\n ?.api?.message?.errors;\n if (!errors || typeof errors !== \"object\") return false;\n return Object.entries(errors).some(\n ([key, value]) =>\n key === String(SHOPWAVE_TOKEN_EXPIRED_ERROR_ID) ||\n Number(value?.id) === SHOPWAVE_TOKEN_EXPIRED_ERROR_ID\n );\n}\n","import { ShopwaveAuthError } from \"./errors\";\nimport { tokenFromResponse, type ShopwaveToken, type ShopwaveTokenResponse } from \"./token\";\n\n/**\n * Per-app OAuth settings. Everything that differs between Shopwave apps lives\n * here; the flow itself is identical for all of them.\n */\nexport interface ShopwaveOAuthConfig {\n /** e.g. `https://secure.merchantstack.com` (no trailing slash needed). */\n authServerUrl: string;\n clientId: string;\n /** Server-side only. Never expose it to the browser. */\n clientSecret: string;\n /**\n * The callback URL registered for this client on the auth server, e.g.\n * `https://admin.example.com/auth`. Must match exactly.\n */\n redirectUri: string;\n /** Where the auth server sends the user after logout. Defaults to `redirectUri`. */\n postLogoutRedirectUri?: string;\n /** Defaults to `\"application\"`. */\n scope?: string;\n /** Defaults to `\"online\"`. */\n accessType?: string;\n /**\n * Body encoding for `POST /oauth/token`. Defaults to standard\n * `application/x-www-form-urlencoded`; `\"multipart\"` sends `FormData`.\n */\n tokenRequestFormat?: \"urlencoded\" | \"multipart\";\n /** Auth-server paths. Defaults match Shopwave: `/login`, `/oauth/token`, `/logout`. */\n endpoints?: { login?: string; token?: string; logout?: string };\n /** Custom fetch (tests, proxies). Defaults to global `fetch`. */\n fetch?: typeof fetch;\n}\n\nexport interface ShopwaveOAuthClient {\n readonly config: Readonly<ShopwaveOAuthConfig>;\n /** URL to send the browser to so the user can sign in. */\n buildLoginUrl(params?: { state?: string }): string;\n /** URL to send the browser to so the auth server ends its own session. */\n buildLogoutUrl(params?: { redirectUri?: string }): string;\n /** Exchanges the `?code=` from the callback for tokens (server-to-server). */\n exchangeCode(code: string): Promise<ShopwaveToken>;\n /**\n * Gets a fresh access token. Shopwave keeps the same refresh token, so the\n * returned token carries the previous refresh token when none is sent back.\n */\n refreshToken(token: ShopwaveToken | string): Promise<ShopwaveToken>;\n}\n\nconst REQUIRED_KEYS = [\"authServerUrl\", \"clientId\", \"clientSecret\", \"redirectUri\"] as const;\n\n/** Throws a readable error when required settings are missing. */\nexport function assertOAuthConfig(config: Partial<ShopwaveOAuthConfig>): asserts config is ShopwaveOAuthConfig {\n const missing = REQUIRED_KEYS.filter((k) => !config?.[k]);\n if (missing.length > 0) {\n throw new ShopwaveAuthError(\n \"config_invalid\",\n `Shopwave auth is missing required settings: ${missing.join(\", \")}`\n );\n }\n}\n\nfunction trimSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\n/**\n * Creates a framework-agnostic Shopwave OAuth client (authorization-code flow\n * with a client secret — the Shopwave auth server does not support PKCE, so\n * this must run on a server).\n *\n * Config is validated lazily on first use, so creating the client at module\n * scope doesn't break builds where env vars aren't present.\n */\nexport function createShopwaveOAuth(config: ShopwaveOAuthConfig): ShopwaveOAuthClient {\n const endpoints = {\n login: config.endpoints?.login ?? \"/login\",\n token: config.endpoints?.token ?? \"/oauth/token\",\n logout: config.endpoints?.logout ?? \"/logout\",\n };\n\n const base = () => {\n assertOAuthConfig(config);\n return trimSlash(config.authServerUrl);\n };\n\n const commonParams = (redirectUri: string) =>\n new URLSearchParams({\n access_type: config.accessType ?? \"online\",\n redirect_uri: redirectUri,\n response_type: \"code\",\n client_id: config.clientId,\n scope: config.scope ?? \"application\",\n });\n\n async function postToken(\n fields: Record<string, string>,\n errorCode: \"token_exchange_failed\" | \"token_refresh_failed\"\n ): Promise<ShopwaveTokenResponse> {\n const url = `${base()}${endpoints.token}`;\n const doFetch = config.fetch ?? globalThis.fetch;\n\n let body: URLSearchParams | FormData;\n if (config.tokenRequestFormat === \"multipart\") {\n const fd = new FormData();\n for (const [k, v] of Object.entries(fields)) fd.append(k, v);\n body = fd;\n } else {\n body = new URLSearchParams(fields);\n }\n\n let response: Response;\n try {\n response = await doFetch(url, {\n method: \"POST\",\n body,\n headers: { Accept: \"application/json\" },\n cache: \"no-store\",\n } as RequestInit);\n } catch (cause) {\n throw new ShopwaveAuthError(\"network_error\", `Could not reach the Shopwave auth server`, { cause });\n }\n\n const text = await response.text();\n if (!response.ok) {\n throw new ShopwaveAuthError(\n errorCode,\n `Shopwave auth server returned ${response.status} for ${fields.grant_type}`,\n { status: response.status, body: text.slice(0, 2000) }\n );\n }\n\n try {\n return JSON.parse(text) as ShopwaveTokenResponse;\n } catch (cause) {\n throw new ShopwaveAuthError(\"token_response_invalid\", \"Token response was not JSON\", {\n status: response.status,\n body: text.slice(0, 2000),\n cause,\n });\n }\n }\n\n return {\n config,\n\n buildLoginUrl({ state } = {}) {\n const params = commonParams(config.redirectUri);\n if (state) params.set(\"state\", state);\n return `${base()}${endpoints.login}?${params.toString()}`;\n },\n\n buildLogoutUrl({ redirectUri } = {}) {\n const params = commonParams(redirectUri ?? config.postLogoutRedirectUri ?? config.redirectUri);\n return `${base()}${endpoints.logout}?${params.toString()}`;\n },\n\n async exchangeCode(code) {\n if (!code) {\n throw new ShopwaveAuthError(\"token_exchange_failed\", \"Missing authorization code\");\n }\n const json = await postToken(\n {\n code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n scope: config.scope ?? \"application\",\n grant_type: \"authorization_code\",\n },\n \"token_exchange_failed\"\n );\n const token = tokenFromResponse(json);\n if (!token) {\n throw new ShopwaveAuthError(\"token_response_invalid\", \"Token response had no access_token\");\n }\n return token;\n },\n\n async refreshToken(tokenOrRefreshToken) {\n const previous =\n typeof tokenOrRefreshToken === \"string\"\n ? ({ accessToken: \"\", refreshToken: tokenOrRefreshToken, tokenType: \"OAuth\" } as ShopwaveToken)\n : tokenOrRefreshToken;\n\n if (!previous.refreshToken) {\n throw new ShopwaveAuthError(\"token_refresh_failed\", \"No refresh token available\", { status: 400 });\n }\n\n const json = await postToken(\n {\n refresh_token: previous.refreshToken,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n grant_type: \"refresh_token\",\n },\n \"token_refresh_failed\"\n );\n const token = tokenFromResponse(json, previous);\n if (!token) {\n throw new ShopwaveAuthError(\"token_response_invalid\", \"Refresh response had no access_token\");\n }\n return token;\n },\n };\n}\n","/**\n * Helpers for the round trip through the auth server.\n */\n\n/**\n * Only allows same-origin relative paths (\"/products?tab=1\"). Anything that\n * could send the user to another site after login — absolute URLs,\n * protocol-relative \"//evil.com\", backslash tricks, control characters —\n * falls back to `fallback`.\n */\nexport function sanitizeReturnTo(value: unknown, fallback = \"/\"): string {\n if (typeof value !== \"string\" || value.length === 0 || value.length > 2048) {\n return fallback;\n }\n // Must start with a single \"/\" not followed by \"/\" or \"\\\".\n if (!/^\\/(?![/\\\\])/.test(value)) return fallback;\n // No control characters (CR/LF header injection, tabs, NUL, …).\n // eslint-disable-next-line no-control-regex\n if (/[\\u0000-\\u001f\\u007f]/.test(value)) return fallback;\n if (value.includes(\"\\\\\")) return fallback;\n return value;\n}\n\n/** Random, URL-safe value for the OAuth `state` parameter (128 bits, hex). */\nexport function createState(): string {\n const bytes = new Uint8Array(16);\n globalThis.crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** Constant-time string comparison for state values. */\nexport function safeEqual(a: string, b: string): boolean {\n if (typeof a !== \"string\" || typeof b !== \"string\" || a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n","/**\n * react-shopwave-connect/next\n *\n * Drop-in Shopwave OAuth for Next.js (App Router, v14+): login + callback +\n * logout route handlers, an encrypted httpOnly session cookie (iron-session),\n * automatic token refresh, a route-handler guard and a proxy/middleware guard.\n *\n * Per app you only supply config (client id/secret, redirect URI, session\n * password). Server-only: never import this from a client component.\n *\n * Peer dependencies: `next` (>=14) and `iron-session` (8 or 9).\n */\n\nimport { cookies } from \"next/headers\";\nimport { NextResponse } from \"next/server\";\nimport { getIronSession, unsealData, type IronSession, type SessionOptions } from \"iron-session\";\n\nimport { createShopwaveOAuth, type ShopwaveOAuthClient, type ShopwaveOAuthConfig } from \"../server/oauth\";\nimport {\n authorizationHeader,\n isTokenExpired,\n normalizeStoredToken,\n type ShopwaveToken,\n} from \"../server/token\";\nimport { createState, safeEqual, sanitizeReturnTo } from \"../server/returnTo\";\nimport { ShopwaveAuthError } from \"../server/errors\";\nimport type { SessionStatus } from \"../core/session\";\n\nexport type { SessionStatus } from \"../core/session\";\nexport type { ShopwaveToken } from \"../server/token\";\nexport { ShopwaveAuthError } from \"../server/errors\";\nexport { isExpiredTokenResponse, authorizationHeader } from \"../server/token\";\n\n// ---------------------------------------------------------------------------\n// Config & types\n// ---------------------------------------------------------------------------\n\nexport interface ShopwaveSessionConfig {\n /**\n * Encrypts the session cookie. At least 32 characters. Pass a map such as\n * `{ 2: newPassword, 1: oldPassword }` to rotate without logging users out.\n */\n password: string | Record<string, string>;\n /** Defaults to `\"shopwave_session\"`. Use a different name per app on a shared domain. */\n cookieName?: string;\n /** Session lifetime in seconds. Defaults to iron-session's 14 days. */\n ttl?: number;\n /** Defaults to `true` in production, `false` otherwise (so http://localhost works). */\n secure?: boolean;\n /** Defaults to `\"lax\"`, which the OAuth redirect back to your app needs. */\n sameSite?: \"lax\" | \"strict\" | \"none\";\n domain?: string;\n}\n\nexport interface ShopwaveAuthConfig extends ShopwaveOAuthConfig {\n session: ShopwaveSessionConfig;\n /**\n * Path where `handlers.auth` is mounted. It starts the login AND receives the\n * callback, so it should be the path of `redirectUri`. Defaults to `\"/auth\"`.\n */\n authPath?: string;\n /** Path where `handlers.logout` is mounted. Defaults to `${authPath}/logout`. */\n logoutPath?: string;\n /**\n * Path where `handlers.session` is mounted. Always public, because it is how\n * the browser finds out it is logged out. Defaults to `\"/api/session\"`.\n */\n sessionPath?: string;\n /** Where users land after login when no `returnTo` was given. Defaults to `\"/\"`. */\n defaultReturnTo?: string;\n /**\n * Reject callbacks whose `state` doesn't match the one we sent (login-CSRF\n * protection). Defaults to `true`. Only turn off if the auth server does not\n * echo `state` back.\n */\n requireState?: boolean;\n /**\n * Refresh this many seconds before expiry. Defaults to `0`, because the\n * Shopwave server only issues a new access token once the old one expired.\n */\n refreshSkewSeconds?: number;\n /** How long a started login stays valid, in seconds. Defaults to 600. */\n loginTimeoutSeconds?: number;\n}\n\n/** What we keep in the encrypted cookie. Tokens never leave the server. */\nexport interface ShopwaveSessionData {\n token?: ShopwaveToken | Record<string, unknown>;\n pendingLogin?: { state: string; returnTo: string; createdAt: number };\n}\n\nexport interface AuthContext {\n accessToken: string;\n /** Ready-to-use `Authorization` header value, e.g. `OAuth 111ad…`. */\n authorization: string;\n token: ShopwaveToken;\n}\n\nexport interface ProtectOptions {\n /**\n * Paths that don't need a login. A path matches itself and everything below\n * it (`\"/tools/tag-joiner\"` covers `/tools/tag-joiner/x`). `\"/\"` matches only\n * the root. The auth, logout and session paths are always public.\n */\n publicPaths?: string[];\n /** Paths that get a 401 JSON response instead of a login redirect. Defaults to `[\"/api\"]`. */\n apiPaths?: string[];\n}\n\nexport interface ShopwaveAuth {\n /** The underlying framework-agnostic OAuth client. */\n readonly oauth: ShopwaveOAuthClient;\n /** Route handlers to export from your app. */\n readonly handlers: {\n /** Mount at the redirect-URI path (e.g. `app/auth/route.ts`): `export const GET = auth.handlers.auth`. */\n auth: (request: Request) => Promise<Response>;\n /** Mount at `app/auth/logout/route.ts`: `export const GET = auth.handlers.logout`. */\n logout: (request: Request) => Promise<Response>;\n /** Mount at `app/api/session/route.ts`: `export const { GET, DELETE } = auth.handlers.session`. */\n session: {\n GET: (request?: Request) => Promise<Response>;\n DELETE: (request?: Request) => Promise<Response>;\n };\n };\n /** Login status for the current request. Never includes tokens. */\n getStatus(): Promise<SessionStatus>;\n /**\n * A valid access token for the current user, refreshing it when expired\n * (and saving the new one in the cookie when called from a route handler,\n * server action or proxy). `null` when logged out or the refresh token was\n * rejected. Pass `forceRefresh` after the API answered 401 / error 908.\n */\n getAccessToken(options?: { forceRefresh?: boolean }): Promise<string | null>;\n /** Same as `getAccessToken` but returns the whole token object. */\n getToken(options?: { forceRefresh?: boolean }): Promise<ShopwaveToken | null>;\n /** `Authorization` header value (`\"OAuth <token>\"`), or `null` when logged out. */\n getAuthorizationHeader(options?: { forceRefresh?: boolean }): Promise<string | null>;\n /** Wraps a route handler; responds 401 JSON when there's no valid session. */\n withAuth<C = unknown>(\n handler: (request: Request, context: C, auth: AuthContext) => Response | Promise<Response>\n ): (request: Request, context: C) => Promise<Response>;\n /**\n * For `proxy.ts` / `middleware.ts`. Returns a redirect (pages) or 401\n * (API paths) when the request has no session, or `undefined` to continue.\n * Only reads the cookie — it never calls the auth server.\n */\n protect(request: Request, options?: ProtectOptions): Promise<Response | undefined>;\n /** Cookie-only check usable anywhere you have the Request. */\n isAuthenticated(request: Request): Promise<boolean>;\n /** Link that starts the login and comes back to `returnTo`. */\n loginPath(returnTo?: string): string;\n /** Link that logs the user out of the app and the auth server. */\n readonly logoutPath: string;\n /** Raw iron-session for advanced use (e.g. storing app data alongside the token). */\n getSession(): Promise<IronSession<ShopwaveSessionData>>;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\nfunction normalizePath(p: string): string {\n const withSlash = p.startsWith(\"/\") ? p : `/${p}`;\n return withSlash.length > 1 ? withSlash.replace(/\\/+$/, \"\") : withSlash;\n}\n\nfunction pathMatches(pathname: string, list: string[]): boolean {\n return list.some((raw) => {\n const p = normalizePath(raw);\n if (p === \"/\") return pathname === \"/\";\n return pathname === p || pathname.startsWith(`${p}/`);\n });\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) =>\n ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c] as string\n );\n}\n\nfunction readCookie(request: Request, name: string): string | undefined {\n const fromNext = (request as { cookies?: { get?: (n: string) => { value?: string } | undefined } })\n .cookies?.get?.(name)?.value;\n if (fromNext) return fromNext;\n const header = request.headers.get(\"cookie\");\n if (!header) return undefined;\n for (const part of header.split(\";\")) {\n const idx = part.indexOf(\"=\");\n if (idx === -1) continue;\n if (part.slice(0, idx).trim() === name) {\n const value = part.slice(idx + 1).trim();\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n }\n }\n return undefined;\n}\n\nlet warnedAboutReadOnlyCookies = false;\n\n/**\n * Creates the Shopwave auth instance for one Next.js app. Call once at module\n * scope (e.g. `lib/auth.ts`) and reuse. Config is validated on first use, so\n * a missing env var doesn't fail `next build`.\n */\nexport function createShopwaveAuth(config: ShopwaveAuthConfig): ShopwaveAuth {\n if (typeof window !== \"undefined\") {\n throw new Error(\"react-shopwave-connect/next is server-only. Import it from route handlers, server code or proxy.ts.\");\n }\n\n const oauth = createShopwaveOAuth(config);\n const authPath = normalizePath(config.authPath ?? \"/auth\");\n const logoutPath = normalizePath(config.logoutPath ?? `${authPath}/logout`);\n const sessionPath = normalizePath(config.sessionPath ?? \"/api/session\");\n const defaultReturnTo = sanitizeReturnTo(config.defaultReturnTo, \"/\");\n const requireState = config.requireState ?? true;\n const skewSeconds = config.refreshSkewSeconds ?? 0;\n const loginTimeoutMs = (config.loginTimeoutSeconds ?? 600) * 1000;\n const cookieName = config.session?.cookieName ?? \"shopwave_session\";\n\n /**\n * Refresh de-duplication, per server instance. Requests that read the same\n * (old) token share one refresh while it's in flight, and for a short while\n * afterwards reuse its result instead of refreshing again.\n */\n const inflightRefresh = new Map<string, Promise<ShopwaveToken>>();\n const recentRefresh = new Map<string, { token: ShopwaveToken; at: number }>();\n const RECENT_REFRESH_MS = 30_000;\n\n function sessionOptions(): SessionOptions {\n const password = config.session?.password;\n const passwords = typeof password === \"string\" ? [password] : Object.values(password ?? {});\n if (passwords.length === 0 || passwords.some((p) => typeof p !== \"string\" || p.length < 32)) {\n throw new ShopwaveAuthError(\n \"config_invalid\",\n \"Shopwave auth: session.password must be at least 32 characters (set a long random secret in your env).\"\n );\n }\n const cookieOptions: SessionOptions[\"cookieOptions\"] = {\n httpOnly: true,\n secure: config.session.secure ?? process.env.NODE_ENV === \"production\",\n sameSite: config.session.sameSite ?? \"lax\",\n path: \"/\",\n };\n if (config.session.domain) cookieOptions.domain = config.session.domain;\n const options: SessionOptions = {\n cookieName,\n password: password as SessionOptions[\"password\"],\n cookieOptions,\n };\n // Only pass ttl when set: an explicit `undefined` overrides iron-session's\n // 14-day default and produces `Max-Age=NaN`.\n if (typeof config.session.ttl === \"number\") options.ttl = config.session.ttl;\n return options;\n }\n\n async function getSession(): Promise<IronSession<ShopwaveSessionData>> {\n return getIronSession<ShopwaveSessionData>(await cookies(), sessionOptions());\n }\n\n /**\n * Cookies can only be written from route handlers, server actions and proxy.\n * From a Server Component the token is still usable for this request; it will\n * simply be refreshed again next time.\n */\n async function trySave(session: IronSession<ShopwaveSessionData>): Promise<void> {\n try {\n await session.save();\n } catch (error) {\n if (!warnedAboutReadOnlyCookies) {\n warnedAboutReadOnlyCookies = true;\n console.warn(\n \"[react-shopwave-connect] Could not update the session cookie (called outside a route handler / server action?).\",\n (error as Error)?.message\n );\n }\n }\n }\n\n function tryDestroy(session: IronSession<ShopwaveSessionData>): void {\n try {\n session.destroy();\n } catch {\n /* read-only cookie context — nothing to clear */\n }\n }\n\n function refreshOnce(token: ShopwaveToken): Promise<ShopwaveToken> {\n const key = `${token.refreshToken}\\u0000${token.accessToken}`;\n const now = Date.now();\n\n const recent = recentRefresh.get(key);\n if (recent && now - recent.at < RECENT_REFRESH_MS) return Promise.resolve(recent.token);\n\n let pending = inflightRefresh.get(key);\n if (!pending) {\n pending = oauth\n .refreshToken(token)\n .then((fresh) => {\n for (const [k, v] of recentRefresh) if (now - v.at >= RECENT_REFRESH_MS) recentRefresh.delete(k);\n recentRefresh.set(key, { token: fresh, at: Date.now() });\n return fresh;\n })\n .finally(() => inflightRefresh.delete(key));\n inflightRefresh.set(key, pending);\n }\n return pending;\n }\n\n async function getToken({ forceRefresh = false }: { forceRefresh?: boolean } = {}): Promise<ShopwaveToken | null> {\n const session = await getSession();\n const token = normalizeStoredToken(session.token);\n if (!token) return null;\n\n const expired = isTokenExpired(token, { skewSeconds });\n if (!forceRefresh && !expired) return token;\n\n if (!token.refreshToken) {\n if (expired || forceRefresh) {\n tryDestroy(session);\n return null;\n }\n return token;\n }\n\n try {\n const refreshed = await refreshOnce(token);\n session.token = refreshed;\n await trySave(session);\n return refreshed;\n } catch (error) {\n if (error instanceof ShopwaveAuthError && error.isInvalidGrant) {\n // Refresh token rejected: the user has to sign in again.\n tryDestroy(session);\n return null;\n }\n throw error;\n }\n }\n\n function loginPath(returnTo?: string): string {\n if (!returnTo) return authPath;\n const safe = sanitizeReturnTo(returnTo, defaultReturnTo);\n return `${authPath}?returnTo=${encodeURIComponent(safe)}`;\n }\n\n function redirectTo(location: string, request: Request): Response {\n return NextResponse.redirect(new URL(location, request.url), 302);\n }\n\n function errorPage(status: number, message: string, retryHref: string): Response {\n const html = `<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Sign-in problem</title></head><body style=\"font-family:system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 1rem;line-height:1.5\"><h1 style=\"font-size:1.25rem\">Sign-in didn’t complete</h1><p>${escapeHtml(message)}</p><p><a href=\"${escapeHtml(retryHref)}\">Try again</a></p></body></html>`;\n return new NextResponse(html, {\n status,\n headers: { \"Content-Type\": \"text/html; charset=utf-8\", \"Cache-Control\": \"no-store\" },\n });\n }\n\n async function authHandler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const session = await getSession();\n const pending = session.pendingLogin;\n\n // The auth server reported a problem (e.g. user cancelled).\n const oauthError = url.searchParams.get(\"error\");\n if (oauthError) {\n delete session.pendingLogin;\n await trySave(session);\n const detail = url.searchParams.get(\"error_description\") || oauthError;\n return errorPage(400, `Shopwave sign-in reported: ${detail}.`, loginPath(pending?.returnTo));\n }\n\n const code = url.searchParams.get(\"code\");\n\n // 1) No code: start a login.\n if (!code) {\n const returnTo = sanitizeReturnTo(url.searchParams.get(\"returnTo\"), defaultReturnTo);\n const state = createState();\n session.pendingLogin = { state, returnTo, createdAt: Date.now() };\n await session.save();\n return NextResponse.redirect(oauth.buildLoginUrl({ state }), 302);\n }\n\n // 2) Callback: verify state, exchange the code, store the token.\n const returnedState = url.searchParams.get(\"state\");\n const isFresh = !!pending && Date.now() - pending.createdAt <= loginTimeoutMs;\n const stateMatches = !!pending && !!returnedState && safeEqual(returnedState, pending.state);\n const rejected = requireState\n ? !(stateMatches && isFresh)\n : !!pending && !!returnedState && !stateMatches;\n\n if (rejected) {\n delete session.pendingLogin;\n await trySave(session);\n return errorPage(\n 400,\n \"This sign-in link has expired or didn’t start from this browser. Please sign in again.\",\n loginPath(pending?.returnTo)\n );\n }\n\n let token: ShopwaveToken;\n try {\n token = await oauth.exchangeCode(code);\n } catch (error) {\n console.error(\"[react-shopwave-connect] Code exchange failed:\", (error as Error)?.message);\n delete session.pendingLogin;\n await trySave(session);\n const status = error instanceof ShopwaveAuthError && error.isInvalidGrant ? 400 : 502;\n return errorPage(status, \"We couldn’t complete sign-in with Shopwave.\", loginPath(pending?.returnTo));\n }\n\n const returnTo = pending?.returnTo ?? defaultReturnTo;\n delete session.pendingLogin;\n session.token = token;\n await session.save();\n return redirectTo(returnTo, request);\n }\n\n async function logoutHandler(_request: Request): Promise<Response> {\n const session = await getSession();\n session.destroy();\n return NextResponse.redirect(oauth.buildLogoutUrl(), 302);\n }\n\n async function getStatus(): Promise<SessionStatus> {\n const session = await getSession();\n const token = normalizeStoredToken(session.token);\n if (!token) return { loggedIn: false };\n const usable = !isTokenExpired(token) || !!token.refreshToken;\n return usable ? { loggedIn: true, expiresAt: token.expiresAt } : { loggedIn: false };\n }\n\n async function isAuthenticated(request: Request): Promise<boolean> {\n const value = readCookie(request, cookieName);\n if (!value) return false;\n try {\n const { password, ttl } = sessionOptions();\n const data = await unsealData<ShopwaveSessionData>(value, { password, ttl });\n const token = normalizeStoredToken(data?.token);\n return !!token && (!isTokenExpired(token) || !!token.refreshToken);\n } catch {\n return false;\n }\n }\n\n const noStore = { \"Cache-Control\": \"no-store\" };\n\n return {\n oauth,\n logoutPath,\n loginPath,\n getSession,\n getStatus,\n getToken,\n\n async getAccessToken(options) {\n return (await getToken(options))?.accessToken ?? null;\n },\n\n async getAuthorizationHeader(options) {\n const token = await getToken(options);\n return token ? authorizationHeader(token) : null;\n },\n\n withAuth(handler) {\n return async (request, context) => {\n const token = await getToken();\n if (!token) {\n return NextResponse.json({ error: \"unauthorized\" }, { status: 401, headers: noStore });\n }\n return handler(request, context, {\n accessToken: token.accessToken,\n authorization: authorizationHeader(token),\n token,\n });\n };\n },\n\n isAuthenticated,\n\n async protect(request, options = {}) {\n const { pathname, search } = new URL(request.url);\n if (pathMatches(pathname, [authPath, logoutPath, sessionPath, ...(options.publicPaths ?? [])])) {\n return undefined;\n }\n if (await isAuthenticated(request)) return undefined;\n if (pathMatches(pathname, options.apiPaths ?? [\"/api\"])) {\n return NextResponse.json({ error: \"unauthorized\" }, { status: 401, headers: noStore });\n }\n return redirectTo(loginPath(pathname + search), request);\n },\n\n handlers: {\n auth: authHandler,\n logout: logoutHandler,\n session: {\n async GET() {\n return NextResponse.json(await getStatus(), { headers: noStore });\n },\n async DELETE() {\n const session = await getSession();\n session.destroy();\n return new NextResponse(null, { status: 204, headers: noStore });\n },\n },\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["body: URLSearchParams | FormData","response: Response","SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>","BLOCKED_EXTRAS: ReadonlySet<string>","text: string","parsed: unknown","headers: Record<string, string>","refs: Record<string, Record<string, unknown>>","authorization: string | null","postBody: unknown","save: RouteHandler","cookieOptions: SessionOptions[\"cookieOptions\"]","options: SessionOptions","returnTo","token: ShopwaveToken"],"sources":["../../src/server/errors.ts","../../src/server/token.ts","../../src/server/oauth.ts","../../src/server/returnTo.ts","../../src/core/entities.ts","../../src/server/api.ts","../../src/next/index.ts"],"sourcesContent":["/**\n * Error thrown by the server-side auth helpers.\n *\n * `code` is stable and safe to branch on; `status` is the HTTP status returned\n * by the Shopwave auth server when there was one.\n */\nexport type ShopwaveAuthErrorCode =\n | \"config_invalid\"\n | \"token_exchange_failed\"\n | \"token_refresh_failed\"\n | \"token_response_invalid\"\n | \"network_error\";\n\nexport class ShopwaveAuthError extends Error {\n readonly code: ShopwaveAuthErrorCode;\n readonly status?: number;\n /** Raw response body from the auth server, if any. Never contains our secret. */\n readonly body?: string;\n\n constructor(\n code: ShopwaveAuthErrorCode,\n message: string,\n details: { status?: number; body?: string; cause?: unknown } = {}\n ) {\n super(message, details.cause !== undefined ? { cause: details.cause } : undefined);\n this.name = \"ShopwaveAuthError\";\n this.code = code;\n this.status = details.status;\n this.body = details.body;\n }\n\n /**\n * True when the auth server rejected the grant itself (bad/expired code or\n * refresh token) rather than failing for a transient reason. Callers should\n * treat the user as logged out.\n */\n get isInvalidGrant(): boolean {\n return (\n (this.code === \"token_exchange_failed\" || this.code === \"token_refresh_failed\") &&\n this.status !== undefined &&\n this.status >= 400 &&\n this.status < 500\n );\n }\n}\n","/**\n * Token model + helpers shared by every server-side integration.\n *\n * The Shopwave auth server returns (snake_case):\n * { access_token, refresh_token, token_type: \"OAuth\", expires_in: 43200 }\n *\n * We store a normalised, camelCase shape with an absolute expiry so any\n * request can decide whether the token is still usable without extra state.\n */\n\nexport interface ShopwaveToken {\n accessToken: string;\n /** Long-lived; the Shopwave server does not rotate it on refresh. */\n refreshToken?: string;\n /** Scheme used in the Authorization header. Shopwave uses \"OAuth\". */\n tokenType: string;\n /** Absolute expiry, epoch milliseconds. Undefined if the server didn't say. */\n expiresAt?: number;\n}\n\n/** Raw token response from `POST {authServer}/oauth/token`. */\nexport interface ShopwaveTokenResponse {\n access_token?: string;\n refresh_token?: string;\n token_type?: string;\n expires_in?: number | string;\n [key: string]: unknown;\n}\n\n/** Shopwave API error id meaning \"access token expired / invalid\". */\nexport const SHOPWAVE_TOKEN_EXPIRED_ERROR_ID = 908;\n\n/**\n * Converts a token-endpoint response into a {@link ShopwaveToken}.\n * Returns `null` when the response has no access token.\n *\n * @param previous the token being refreshed; its refresh token is kept when\n * the server doesn't send a new one.\n */\nexport function tokenFromResponse(\n response: ShopwaveTokenResponse,\n previous?: ShopwaveToken,\n now: number = Date.now()\n): ShopwaveToken | null {\n if (!response || typeof response.access_token !== \"string\" || !response.access_token) {\n return null;\n }\n\n const expiresIn = Number(response.expires_in);\n\n return {\n accessToken: response.access_token,\n refreshToken:\n (typeof response.refresh_token === \"string\" && response.refresh_token) ||\n previous?.refreshToken,\n tokenType: (typeof response.token_type === \"string\" && response.token_type) || \"OAuth\",\n expiresAt: Number.isFinite(expiresIn) && expiresIn > 0 ? now + expiresIn * 1000 : undefined,\n };\n}\n\n/**\n * Reads a token from session storage. Accepts both the current shape and the\n * legacy raw response that older apps (e.g. AdminUI ≤ 0.1) stored directly in\n * the session, so existing sessions survive an upgrade.\n */\nexport function normalizeStoredToken(raw: unknown): ShopwaveToken | null {\n if (!raw || typeof raw !== \"object\") return null;\n const t = raw as Record<string, unknown>;\n\n if (typeof t.accessToken === \"string\" && t.accessToken) {\n return {\n accessToken: t.accessToken,\n refreshToken: typeof t.refreshToken === \"string\" ? t.refreshToken : undefined,\n tokenType: typeof t.tokenType === \"string\" && t.tokenType ? t.tokenType : \"OAuth\",\n expiresAt: typeof t.expiresAt === \"number\" ? t.expiresAt : undefined,\n };\n }\n\n if (typeof t.access_token === \"string\" && t.access_token) {\n // Legacy shape: we don't know when it was issued, so no expiresAt.\n // It will be refreshed reactively when the API reports it expired.\n return {\n accessToken: t.access_token,\n refreshToken: typeof t.refresh_token === \"string\" ? t.refresh_token : undefined,\n tokenType: typeof t.token_type === \"string\" && t.token_type ? t.token_type : \"OAuth\",\n };\n }\n\n return null;\n}\n\n/**\n * True when the token has passed its expiry (minus `skewSeconds`).\n * Tokens without a known expiry are treated as valid.\n *\n * Shopwave only issues a new access token once the old one has actually\n * expired, so the default skew is 0 — refreshing early just returns the same\n * token.\n */\nexport function isTokenExpired(\n token: ShopwaveToken,\n { skewSeconds = 0, now = Date.now() }: { skewSeconds?: number; now?: number } = {}\n): boolean {\n if (token.expiresAt === undefined) return false;\n return now >= token.expiresAt - skewSeconds * 1000;\n}\n\n/** `Authorization` header value, e.g. `OAuth 111ad…`. */\nexport function authorizationHeader(token: ShopwaveToken): string {\n return `${token.tokenType || \"OAuth\"} ${token.accessToken}`;\n}\n\n/**\n * True when a Shopwave API response means \"your access token is no longer\n * valid\": HTTP 401, or the API error 908 in the response envelope.\n * Use it to trigger a forced refresh + a single retry.\n */\nexport function isExpiredTokenResponse(status: number, body?: unknown): boolean {\n if (status === 401) return true;\n // `errors` in responses seen so far; the API reference calls it `error`.\n const message = (body as { api?: { message?: { errors?: Record<string, { id?: number }>; error?: Record<string, { id?: number }> } } })\n ?.api?.message;\n const errors = message?.errors ?? message?.error;\n if (!errors || typeof errors !== \"object\") return false;\n return Object.entries(errors).some(\n ([key, value]) =>\n key === String(SHOPWAVE_TOKEN_EXPIRED_ERROR_ID) ||\n Number(value?.id) === SHOPWAVE_TOKEN_EXPIRED_ERROR_ID\n );\n}\n","import { ShopwaveAuthError } from \"./errors\";\nimport { tokenFromResponse, type ShopwaveToken, type ShopwaveTokenResponse } from \"./token\";\n\n/**\n * Per-app OAuth settings. Everything that differs between Shopwave apps lives\n * here; the flow itself is identical for all of them.\n */\nexport interface ShopwaveOAuthConfig {\n /** e.g. `https://secure.merchantstack.com` (no trailing slash needed). */\n authServerUrl: string;\n clientId: string;\n /** Server-side only. Never expose it to the browser. */\n clientSecret: string;\n /**\n * The callback URL registered for this client on the auth server, e.g.\n * `https://admin.example.com/auth`. Must match exactly.\n */\n redirectUri: string;\n /** Where the auth server sends the user after logout. Defaults to `redirectUri`. */\n postLogoutRedirectUri?: string;\n /** Defaults to `\"application\"`. */\n scope?: string;\n /** Defaults to `\"online\"`. */\n accessType?: string;\n /**\n * Body encoding for `POST /oauth/token`. Defaults to standard\n * `application/x-www-form-urlencoded`; `\"multipart\"` sends `FormData`.\n */\n tokenRequestFormat?: \"urlencoded\" | \"multipart\";\n /** Auth-server paths. Defaults match Shopwave: `/login`, `/oauth/token`, `/logout`. */\n endpoints?: { login?: string; token?: string; logout?: string };\n /** Custom fetch (tests, proxies). Defaults to global `fetch`. */\n fetch?: typeof fetch;\n}\n\nexport interface ShopwaveOAuthClient {\n readonly config: Readonly<ShopwaveOAuthConfig>;\n /** URL to send the browser to so the user can sign in. */\n buildLoginUrl(params?: { state?: string }): string;\n /** URL to send the browser to so the auth server ends its own session. */\n buildLogoutUrl(params?: { redirectUri?: string }): string;\n /** Exchanges the `?code=` from the callback for tokens (server-to-server). */\n exchangeCode(code: string): Promise<ShopwaveToken>;\n /**\n * Gets a fresh access token. Shopwave keeps the same refresh token, so the\n * returned token carries the previous refresh token when none is sent back.\n */\n refreshToken(token: ShopwaveToken | string): Promise<ShopwaveToken>;\n}\n\nconst REQUIRED_KEYS = [\"authServerUrl\", \"clientId\", \"clientSecret\", \"redirectUri\"] as const;\n\n/** Throws a readable error when required settings are missing. */\nexport function assertOAuthConfig(config: Partial<ShopwaveOAuthConfig>): asserts config is ShopwaveOAuthConfig {\n const missing = REQUIRED_KEYS.filter((k) => !config?.[k]);\n if (missing.length > 0) {\n throw new ShopwaveAuthError(\n \"config_invalid\",\n `Shopwave auth is missing required settings: ${missing.join(\", \")}`\n );\n }\n}\n\nfunction trimSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\n/**\n * Creates a framework-agnostic Shopwave OAuth client (authorization-code flow\n * with a client secret — the Shopwave auth server does not support PKCE, so\n * this must run on a server).\n *\n * Config is validated lazily on first use, so creating the client at module\n * scope doesn't break builds where env vars aren't present.\n */\nexport function createShopwaveOAuth(config: ShopwaveOAuthConfig): ShopwaveOAuthClient {\n const endpoints = {\n login: config.endpoints?.login ?? \"/login\",\n token: config.endpoints?.token ?? \"/oauth/token\",\n logout: config.endpoints?.logout ?? \"/logout\",\n };\n\n const base = () => {\n assertOAuthConfig(config);\n return trimSlash(config.authServerUrl);\n };\n\n const commonParams = (redirectUri: string) =>\n new URLSearchParams({\n access_type: config.accessType ?? \"online\",\n redirect_uri: redirectUri,\n response_type: \"code\",\n client_id: config.clientId,\n scope: config.scope ?? \"application\",\n });\n\n async function postToken(\n fields: Record<string, string>,\n errorCode: \"token_exchange_failed\" | \"token_refresh_failed\"\n ): Promise<ShopwaveTokenResponse> {\n const url = `${base()}${endpoints.token}`;\n const doFetch = config.fetch ?? globalThis.fetch;\n\n let body: URLSearchParams | FormData;\n if (config.tokenRequestFormat === \"multipart\") {\n const fd = new FormData();\n for (const [k, v] of Object.entries(fields)) fd.append(k, v);\n body = fd;\n } else {\n body = new URLSearchParams(fields);\n }\n\n let response: Response;\n try {\n response = await doFetch(url, {\n method: \"POST\",\n body,\n headers: { Accept: \"application/json\" },\n cache: \"no-store\",\n } as RequestInit);\n } catch (cause) {\n throw new ShopwaveAuthError(\"network_error\", `Could not reach the Shopwave auth server`, { cause });\n }\n\n const text = await response.text();\n if (!response.ok) {\n throw new ShopwaveAuthError(\n errorCode,\n `Shopwave auth server returned ${response.status} for ${fields.grant_type}`,\n { status: response.status, body: text.slice(0, 2000) }\n );\n }\n\n try {\n return JSON.parse(text) as ShopwaveTokenResponse;\n } catch (cause) {\n throw new ShopwaveAuthError(\"token_response_invalid\", \"Token response was not JSON\", {\n status: response.status,\n body: text.slice(0, 2000),\n cause,\n });\n }\n }\n\n return {\n config,\n\n buildLoginUrl({ state } = {}) {\n const params = commonParams(config.redirectUri);\n if (state) params.set(\"state\", state);\n return `${base()}${endpoints.login}?${params.toString()}`;\n },\n\n buildLogoutUrl({ redirectUri } = {}) {\n const params = commonParams(redirectUri ?? config.postLogoutRedirectUri ?? config.redirectUri);\n return `${base()}${endpoints.logout}?${params.toString()}`;\n },\n\n async exchangeCode(code) {\n if (!code) {\n throw new ShopwaveAuthError(\"token_exchange_failed\", \"Missing authorization code\");\n }\n const json = await postToken(\n {\n code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n scope: config.scope ?? \"application\",\n grant_type: \"authorization_code\",\n },\n \"token_exchange_failed\"\n );\n const token = tokenFromResponse(json);\n if (!token) {\n throw new ShopwaveAuthError(\"token_response_invalid\", \"Token response had no access_token\");\n }\n return token;\n },\n\n async refreshToken(tokenOrRefreshToken) {\n const previous =\n typeof tokenOrRefreshToken === \"string\"\n ? ({ accessToken: \"\", refreshToken: tokenOrRefreshToken, tokenType: \"OAuth\" } as ShopwaveToken)\n : tokenOrRefreshToken;\n\n if (!previous.refreshToken) {\n throw new ShopwaveAuthError(\"token_refresh_failed\", \"No refresh token available\", { status: 400 });\n }\n\n const json = await postToken(\n {\n refresh_token: previous.refreshToken,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n grant_type: \"refresh_token\",\n },\n \"token_refresh_failed\"\n );\n const token = tokenFromResponse(json, previous);\n if (!token) {\n throw new ShopwaveAuthError(\"token_response_invalid\", \"Refresh response had no access_token\");\n }\n return token;\n },\n };\n}\n","/**\n * Helpers for the round trip through the auth server.\n */\n\n/**\n * Only allows same-origin relative paths (\"/products?tab=1\"). Anything that\n * could send the user to another site after login — absolute URLs,\n * protocol-relative \"//evil.com\", backslash tricks, control characters —\n * falls back to `fallback`.\n */\nexport function sanitizeReturnTo(value: unknown, fallback = \"/\"): string {\n if (typeof value !== \"string\" || value.length === 0 || value.length > 2048) {\n return fallback;\n }\n // Must start with a single \"/\" not followed by \"/\" or \"\\\".\n if (!/^\\/(?![/\\\\])/.test(value)) return fallback;\n // No control characters (CR/LF header injection, tabs, NUL, …).\n // eslint-disable-next-line no-control-regex\n if (/[\\u0000-\\u001f\\u007f]/.test(value)) return fallback;\n if (value.includes(\"\\\\\")) return fallback;\n return value;\n}\n\n/** Random, URL-safe value for the OAuth `state` parameter (128 bits, hex). */\nexport function createState(): string {\n const bytes = new Uint8Array(16);\n globalThis.crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** Constant-time string comparison for state values. */\nexport function safeEqual(a: string, b: string): boolean {\n if (typeof a !== \"string\" || typeof b !== \"string\" || a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n","import { apiDelete, apiGet, apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport type { Product } from \"./product\";\nimport type { Category } from \"./category\";\nimport type { Store } from \"./store\";\nimport type { Promotion } from \"./promotion\";\nimport type { Employee } from \"./employee\";\nimport type { Consumer } from \"./consumer\";\n\n/**\n * How each Shopwave entity is addressed, shared by the client functions below\n * and the server route handlers in `react-shopwave-connect/next`, so the two\n * halves of the contract can't drift apart.\n */\nexport interface EntityDefinition {\n /** App route segment: `/api/<route>` and `/api/<route>/<id>`. */\n route: string;\n /** Key of the entity map in requests and responses (`{ products: { … } }`). */\n collection: string;\n /** Shopwave API path (`GET/POST/DELETE {apiUrl}/<upstream>`). */\n upstream: string;\n /** Header that filters reads by id (`productIds: 1,2`). */\n idsHeader: string;\n /** Header that names the record to delete (`productId: 1`). */\n idHeader: string;\n /** False for read-only entities (the API has no POST for them). */\n writable: boolean;\n /**\n * How a delete is done upstream:\n * - `\"delete\"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)\n * - `\"retire\"`: the API has no DELETE; the record is retired by POSTing\n * `{ id, [retireField]: <now> }` (employees: `exitDate`, promotions: `endDate`)\n * - `\"none\"`: can't be deleted\n */\n deleteMode: \"delete\" | \"retire\" | \"none\";\n /** Date field set to \"now\" when `deleteMode` is `\"retire\"`. */\n retireField?: string;\n}\n\nexport type EntityKind = \"product\" | \"category\" | \"store\" | \"promotion\" | \"employee\" | \"consumer\";\n\n/** Entities that can be created/updated through the API. */\nexport type WritableEntityKind = Exclude<EntityKind, \"consumer\">;\n\n/** Entities that can be deleted (or retired) through the API. */\nexport type DeletableEntityKind = Exclude<EntityKind, \"consumer\">;\n\n/**\n * Addressing per entity, following the Shopwave API reference\n * (https://developer.merchantstack.com/api-reference.html) and checked against\n * the live API where noted:\n * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.\n * - promotion: no DELETE; \"deleting\" a promotion ends it by setting `endDate` to now.\n * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be\n * changed); no DELETE (live API: \"Cannot DELETE /employee\") → retired via `exitDate`.\n * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.\n */\nexport const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>> = Object.freeze({\n product: { route: \"products\", collection: \"products\", upstream: \"product\", idsHeader: \"productIds\", idHeader: \"productId\", writable: true, deleteMode: \"delete\" },\n category: { route: \"categories\", collection: \"categories\", upstream: \"category\", idsHeader: \"categoryIds\", idHeader: \"categoryId\", writable: true, deleteMode: \"delete\" },\n store: { route: \"stores\", collection: \"stores\", upstream: \"store\", idsHeader: \"storeIds\", idHeader: \"storeId\", writable: true, deleteMode: \"delete\" },\n promotion: { route: \"promotions\", collection: \"promotions\", upstream: \"promotion\", idsHeader: \"promotionIds\", idHeader: \"promotionId\", writable: true, deleteMode: \"retire\", retireField: \"endDate\" },\n employee: { route: \"employees\", collection: \"employees\", upstream: \"employee\", idsHeader: \"employeeIds\", idHeader: \"employeeId\", writable: true, deleteMode: \"retire\", retireField: \"exitDate\" },\n consumer: { route: \"consumer\", collection: \"consumers\", upstream: \"consumer\", idsHeader: \"ids\", idHeader: \"consumerId\", writable: false, deleteMode: \"none\" },\n});\n\nexport interface EntityTypes {\n product: Product;\n category: Category;\n store: Store;\n promotion: Promotion;\n employee: Employee;\n consumer: Consumer;\n}\n\n/**\n * What a save accepts: any subset of the entity's fields (plus extra API\n * fields the type doesn't list). Include `id` to update; leave it out to create.\n */\nexport type EntityInput<T> = Partial<T> & { [field: string]: unknown };\n\n/** A saved entity: always has its `id`. */\nexport type Saved<T> = T & { id: number };\n\nexport type EntityId = number | string;\n\nfunction definition(kind: EntityKind): EntityDefinition {\n const def = SHOPWAVE_ENTITIES[kind];\n if (!def) throw new Error(`Unknown Shopwave entity \"${kind}\"`);\n return def;\n}\n\nfunction assertId(kind: EntityKind, id: EntityId): string {\n const s = id == null ? \"\" : String(id).trim();\n if (!s) throw new Error(`${kind} id is missing`);\n return encodeURIComponent(s);\n}\n\n/**\n * Creates or updates several entities of one kind in a single request and\n * returns them as saved (with their ids), in the order given.\n *\n * The request is `{ <collection>: { \"0\": item0, \"1\": item1, … } }`. Shopwave\n * answers 201 and echoes every entity under the ref it was sent with, so each\n * result is matched back by ref. Throws {@link ShopwaveApiError} when the\n * response carries `api.message.errors` or any ref is missing from it.\n *\n * The API's partial-success semantics for multi-entity saves haven't been\n * confirmed yet; single saves (`saveProduct` etc.) are the tested path.\n */\nexport async function saveEntities<K extends WritableEntityKind>(\n kind: K,\n items: ReadonlyArray<EntityInput<EntityTypes[K]>>,\n options: RequestOptions = {}\n): Promise<Array<Saved<EntityTypes[K]>>> {\n const def = definition(kind);\n if (items.length === 0) return [];\n\n const refs = items.map((_, i) => String(i));\n const payload = { [def.collection]: Object.fromEntries(refs.map((ref, i) => [ref, items[i]])) };\n\n const { status, body: raw } = await apiRequest(\n `/api/${def.route}`,\n { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(payload) },\n options\n );\n const body = raw as Record<string, unknown> | null;\n assertNoApiErrors(body, status);\n\n const echoed = (body?.[def.collection] ?? {}) as Record<string, Record<string, unknown> | undefined>;\n return refs.map((ref, i) => {\n const saved = echoed[ref];\n if (!saved || saved.id == null) {\n throw new ShopwaveApiError(status, `Shopwave API error (${status}): the saved ${kind} (ref \"${ref}\") was not returned`, {\n body,\n });\n }\n // The write response echoes what was sent plus server fields; keep any\n // field it leaves out.\n return { ...items[i], ...saved, id: Number(saved.id) } as unknown as Saved<EntityTypes[K]>;\n });\n}\n\n/** Creates (no `id`) or updates (with `id`) one entity and returns it with its id. */\nexport async function saveEntity<K extends WritableEntityKind>(\n kind: K,\n item: EntityInput<EntityTypes[K]>,\n options: RequestOptions = {}\n): Promise<Saved<EntityTypes[K]>> {\n const [saved] = await saveEntities(kind, [item], options);\n return saved;\n}\n\n/**\n * Deletes one entity (a soft delete: it stays readable with `deleted: true`).\n * Employees and promotions can't be deleted in Shopwave; for them the route\n * sets `exitDate` / `endDate` to now instead (see `SHOPWAVE_ENTITIES`).\n *\n * Shopwave answers 205 with an empty body — also for ids that don't exist — so\n * a resolved promise means \"the API accepted the request\", not \"a record was\n * deleted\".\n */\nexport async function deleteEntityById(kind: DeletableEntityKind, id: EntityId, options: RequestOptions = {}): Promise<void> {\n await apiDelete(`/api/${definition(kind).route}/${assertId(kind, id)}`, options);\n}\n\nexport interface FetchByIdParams {\n /** Also return the record when it has been deleted. Defaults to `false`. */\n deleted?: boolean;\n}\n\n/** Reads one entity by id, or `null` when there's no such (non-deleted) record. */\nexport async function fetchEntityById<K extends EntityKind>(\n kind: K,\n id: EntityId,\n params: FetchByIdParams = {},\n options: RequestOptions = {}\n): Promise<EntityTypes[K] | null> {\n const def = definition(kind);\n const body = await apiGet<Record<string, unknown> | null>(\n `/api/${def.route}/${assertId(kind, id)}`,\n { deleted: params.deleted ?? false },\n options\n );\n assertNoApiErrors(body, 200);\n const map = (body?.[def.collection] ?? {}) as Record<string, EntityTypes[K]>;\n const match = map[String(id)] ?? Object.values(map).find((e) => String((e as { id?: unknown })?.id) === String(id));\n return match ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Typed per-entity functions\n// ---------------------------------------------------------------------------\n\nexport const saveProduct = (product: EntityInput<Product>, options?: RequestOptions) =>\n saveEntity(\"product\", product, options);\nexport const deleteProduct = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"product\", id, options);\nexport const fetchProduct = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"product\", id, params, options);\n\nexport const saveCategory = (category: EntityInput<Category>, options?: RequestOptions) =>\n saveEntity(\"category\", category, options);\nexport const deleteCategory = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"category\", id, options);\nexport const fetchCategory = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"category\", id, params, options);\n\nexport const saveStore = (store: EntityInput<Store>, options?: RequestOptions) => saveEntity(\"store\", store, options);\nexport const deleteStore = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"store\", id, options);\nexport const fetchStore = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"store\", id, params, options);\n\nexport const savePromotion = (promotion: EntityInput<Promotion>, options?: RequestOptions) =>\n saveEntity(\"promotion\", promotion, options);\n/** Ends a promotion: Shopwave has no promotion DELETE, so this sets `endDate` to now. */\nexport const deletePromotion = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"promotion\", id, options);\nexport const fetchPromotion = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"promotion\", id, params, options);\n\n/**\n * Creates an employee, or updates one (with `id`). On update Shopwave only\n * applies `roleId`, `joinedDate` and `exitDate` (and store roles); names and\n * email can't be changed, and the echo only carries the updated fields — re-read\n * with `fetchEmployee` if you need the stored record.\n */\nexport const saveEmployee = (employee: EntityInput<Employee>, options?: RequestOptions) =>\n saveEntity(\"employee\", employee, options);\n/** Retires an employee: Shopwave has no employee DELETE, so this sets `exitDate` to now. */\nexport const deleteEmployee = (id: EntityId, options?: RequestOptions) => deleteEntityById(\"employee\", id, options);\nexport const fetchEmployee = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"employee\", id, params, options);\n\n// Consumers are read-only in the Shopwave API: there is no save or delete.\nexport const fetchConsumer = (id: EntityId, params?: FetchByIdParams, options?: RequestOptions) =>\n fetchEntityById(\"consumer\", id, params, options);\n","/**\n * Server half of the SDK's API contract: route handlers that sit between the\n * `core` client functions (which call `/api/<route>`) and the Shopwave REST API.\n *\n * Framework-neutral (Web `Request` → `Response`), so they work in Next.js route\n * handlers, Remix, Hono, Bun, Deno or Node 18+. `react-shopwave-connect/next`\n * wires them to the Next.js session (`createShopwaveApi`).\n *\n * What a handler does for every call:\n * 1. Picks the token: the one the caller sent (`Authorization: OAuth <token>`,\n * when `allowRequestToken` is on), otherwise the logged-in user's.\n * 2. Turns the client's `extras` header (query parameters, JSON) into upstream\n * headers, dropping anything that could override auth or transport headers.\n * 3. Sends writes as the form field `postBody=<JSON>` that Shopwave expects.\n * 4. When the API says the session token expired (HTTP 401 or error 908),\n * refreshes it once and retries.\n * 5. Answers with the upstream status and body unchanged (201 for saves,\n * 205 + empty body for deletes, Shopwave error envelopes as-is).\n */\n\nimport {\n SHOPWAVE_ENTITIES,\n type EntityDefinition,\n type EntityKind,\n} from \"../core/entities\";\nimport { isExpiredTokenResponse } from \"./token\";\n\n// ---------------------------------------------------------------------------\n// Request token\n// ---------------------------------------------------------------------------\n\n/**\n * The OAuth token a caller sent with the request, without its scheme, or `null`.\n *\n * Reads, in order: `Authorization: OAuth|Bearer <token>` (what the SDK sends\n * from 0.3), the `token` header (SDK ≤ 0.2 writes), and `extras.token`\n * (SDK ≤ 0.2 reads).\n */\nexport function readRequestToken(request: Request): string | null {\n const authz = request.headers.get(\"authorization\");\n if (authz) {\n const match = /^(?:oauth|bearer)\\s+(\\S+)\\s*$/i.exec(authz);\n if (match) return match[1];\n }\n const legacyHeader = request.headers.get(\"token\");\n if (legacyHeader && legacyHeader.trim()) return legacyHeader.trim();\n\n const extras = request.headers.get(\"extras\");\n if (extras) {\n try {\n const token = (JSON.parse(extras) as { token?: unknown } | null)?.token;\n if (typeof token === \"string\" && token.trim()) return token.trim();\n } catch {\n /* invalid extras are reported by the handler */\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// extras → upstream headers\n// ---------------------------------------------------------------------------\n\n/**\n * `extras` keys that are never forwarded upstream (case-insensitive).\n * `Content-Type` is allowed (SDK clients have always sent `application/json`\n * on reads) but is replaced by the form content type on writes.\n */\nexport const BLOCKED_EXTRAS: ReadonlySet<string> = new Set([\n \"authorization\",\n \"token\",\n \"cookie\",\n \"host\",\n \"x-accept-version\",\n \"content-length\",\n \"connection\",\n \"transfer-encoding\",\n \"te\",\n \"upgrade\",\n \"expect\",\n \"keep-alive\",\n \"proxy-authorization\",\n \"forwarded\",\n]);\n\nconst HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;\n\nfunction headerValue(value: unknown): string | null {\n if (value == null) return null;\n let text: string;\n if (Array.isArray(value)) text = value.map((v) => (v != null && typeof v === \"object\" ? JSON.stringify(v) : String(v))).join(\",\");\n else if (typeof value === \"object\") text = JSON.stringify(value);\n else text = String(value);\n // CR/LF/NUL would make the header invalid (and could split it).\n return /[\\r\\n\\0]/.test(text) ? null : text;\n}\n\n/**\n * Parses the `extras` request header into upstream headers.\n * Returns `null` when the header isn't a JSON object.\n */\nexport function parseExtras(\n raw: string | null,\n blocked: ReadonlySet<string> = BLOCKED_EXTRAS\n): Record<string, string> | null {\n if (!raw) return {};\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {\n const lower = key.toLowerCase();\n if (!HEADER_NAME.test(key) || blocked.has(lower) || lower.startsWith(\"x-forwarded-\") || lower.startsWith(\"proxy-\")) {\n continue;\n }\n const text = headerValue(value);\n if (text !== null) headers[key] = text;\n }\n return headers;\n}\n\n// ---------------------------------------------------------------------------\n// Write bodies\n// ---------------------------------------------------------------------------\n\nexport class RequestBodyError extends Error {}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Normalises a write request body into the Shopwave envelope\n * `{ <collection>: { <ref>: entity, … } }`.\n *\n * Accepts:\n * - the SDK ≥ 0.3 shape `{ <collection>: { \"0\": {...}, \"1\": {...} } }` (refs kept),\n * - the older AdminUI shapes `{ <collection>: { new: {...} } }` / `{ updated: {...} }`,\n * - a bare entity object `{ title: … }`.\n *\n * With `forcedId` (item routes) exactly one entity is allowed and its `id` is\n * set from the URL.\n */\nexport function normalizeWriteBody(\n def: Pick<EntityDefinition, \"collection\">,\n body: unknown,\n forcedId?: string\n): Record<string, Record<string, Record<string, unknown>>> {\n if (!isPlainObject(body)) throw new RequestBodyError(\"Request body must be a JSON object\");\n\n let refs: Record<string, Record<string, unknown>>;\n const wrapped = body[def.collection];\n\n if (wrapped !== undefined) {\n if (!isPlainObject(wrapped)) throw new RequestBodyError(`\"${def.collection}\" must be an object keyed by ref`);\n const keys = Object.keys(wrapped);\n const legacy = keys.length === 1 && (keys[0] === \"new\" || keys[0] === \"updated\");\n const entries = legacy ? [[\"0\", wrapped[keys[0]]] as const] : Object.entries(wrapped);\n refs = {};\n for (const [ref, entity] of entries) {\n if (!isPlainObject(entity)) throw new RequestBodyError(`\"${def.collection}.${ref}\" must be an object`);\n refs[ref] = { ...entity };\n }\n } else {\n refs = { \"0\": { ...body } };\n }\n\n const count = Object.keys(refs).length;\n if (count === 0) throw new RequestBodyError(`No ${def.collection} to save`);\n\n if (forcedId !== undefined) {\n if (count !== 1) throw new RequestBodyError(\"Only one entity can be saved at an id route\");\n const only = refs[Object.keys(refs)[0]];\n only.id = /^\\d+$/.test(forcedId) ? Number(forcedId) : forcedId;\n }\n\n return { [def.collection]: refs };\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\nexport interface ShopwaveApiConfig {\n /** Shopwave API base URL, e.g. `process.env.SHOPWAVE_API_SERVER_URL`. */\n apiUrl: string;\n /**\n * `Authorization` header value for the logged-in user (`\"OAuth <token>\"`),\n * or `null` when logged out. Called again with `forceRefresh: true` when the\n * API reports the token expired.\n */\n getAuthorization?: (options?: { forceRefresh?: boolean }) => Promise<string | null>;\n /**\n * Accept a token sent by the caller (`Authorization: OAuth <token>`, or the\n * legacy `token` header / `extras.token`) instead of the session. The token\n * is only forwarded to the Shopwave API, which validates it. Defaults to `true`\n * (SDK integration tests, scripts and server-to-server calls rely on it).\n */\n allowRequestToken?: boolean;\n /** `x-accept-version` sent upstream. Defaults to `\"2.0\"`. */\n apiVersion?: string;\n /** Custom fetch for the upstream call. */\n fetch?: typeof fetch;\n /** Override how an entity is addressed (e.g. a different id header). */\n entities?: Partial<Record<EntityKind, Partial<EntityDefinition>>>;\n /** Extra `extras` keys to drop, on top of {@link BLOCKED_EXTRAS}. */\n blockedExtras?: string[];\n /**\n * Called when the upstream call throws (network error). Defaults to a\n * `console.error` with the method and path only — never headers or tokens.\n */\n onError?: (error: unknown, context: { method: string; path: string }) => void;\n}\n\nexport interface ForwardInit {\n /** HTTP method sent upstream. Defaults to `GET`. */\n method?: string;\n /** Shopwave API path, e.g. `\"product\"`. */\n path: string;\n /** Extra upstream headers; they win over `extras`. */\n headers?: Record<string, string>;\n /** Sent as the form field `postBody=<JSON>`. */\n postBody?: unknown;\n /** Forward the caller's `extras` header as upstream headers. Defaults to `true`. */\n forwardExtras?: boolean;\n}\n\n/** Second argument Next.js (and similar) pass to dynamic route handlers. */\nexport interface RouteContext {\n params?: Promise<Record<string, string | string[] | undefined>> | Record<string, string | string[] | undefined>;\n}\n\nexport type RouteHandler = (request: Request, context?: RouteContext) => Promise<Response>;\n\nexport interface CollectionHandlers {\n /** List/filter via `extras` (e.g. `productIds`, `deleted`). */\n GET: RouteHandler;\n /** Create or update (Shopwave upserts: an `id` means update). */\n POST: RouteHandler;\n /** Same as POST (kept for older clients). */\n PUT: RouteHandler;\n}\n\nexport interface ItemHandlers {\n /** Read one record by the `[id]` route param. */\n GET: RouteHandler;\n /** Update the record at `[id]` (the id in the URL wins). */\n PUT: RouteHandler;\n /** Delete the record at `[id]`. Shopwave answers 205 with an empty body. */\n DELETE: RouteHandler;\n}\n\nexport interface ShopwaveApiHandlers {\n /** Calls the Shopwave API for this request (token, extras, refresh-retry) and returns its answer. */\n forward(request: Request, init: ForwardInit): Promise<Response>;\n /** Handlers for `app/api/<route>/route.ts`. */\n collection(kind: EntityKind): CollectionHandlers;\n /** Handlers for `app/api/<route>/[id]/route.ts`. */\n item(kind: EntityKind, options?: { param?: string }): ItemHandlers;\n /** GET-only proxy for a Shopwave path (e.g. `\"report\"`, `\"user\"`, `\"merchant\"`). */\n passthrough(path: string): { GET: RouteHandler };\n /** The entity table in use (defaults merged with `config.entities`). */\n entity(kind: EntityKind): EntityDefinition;\n}\n\nconst NO_STORE = { \"Cache-Control\": \"no-store\" } as const;\n\nfunction jsonError(status: number, error: string, message: string): Response {\n return new Response(JSON.stringify({ error, message }), {\n status,\n headers: { \"Content-Type\": \"application/json\", ...NO_STORE },\n });\n}\n\n/** `YYYY-MM-DD HH:mm:ss` in UTC, the datetime format Shopwave writes use (e.g. promotions). */\nexport function shopwaveDateTime(date: Date = new Date()): string {\n return date.toISOString().slice(0, 19).replace(\"T\", \" \");\n}\n\nconst NULL_BODY_STATUS = new Set([101, 103, 204, 205, 304]);\nconst ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\n\nasync function readUpstream(response: Response): Promise<{ text: string; json: unknown }> {\n const text = await response.text();\n if (!text) return { text, json: null };\n try {\n return { text, json: JSON.parse(text) };\n } catch {\n return { text, json: null };\n }\n}\n\nasync function routeParam(context: RouteContext | undefined, name: string): Promise<string | null> {\n const params = await context?.params;\n const value = params?.[name];\n const id = Array.isArray(value) ? value[0] : value;\n return typeof id === \"string\" && ID_PATTERN.test(id) ? id : null;\n}\n\nexport function createShopwaveApiHandlers(config: ShopwaveApiConfig): ShopwaveApiHandlers {\n const allowRequestToken = config.allowRequestToken ?? true;\n const apiVersion = config.apiVersion ?? \"2.0\";\n const blocked = new Set([...BLOCKED_EXTRAS, ...(config.blockedExtras ?? []).map((k) => k.toLowerCase())]);\n const onError =\n config.onError ??\n ((error: unknown, ctx: { method: string; path: string }) =>\n console.error(`[react-shopwave-connect] Shopwave API ${ctx.method} /${ctx.path} failed:`, (error as Error)?.message ?? error));\n\n function apiBase(): string {\n const url = config.apiUrl;\n if (!url) throw new Error(\"Shopwave API: apiUrl is not set (e.g. SHOPWAVE_API_SERVER_URL).\");\n return url.replace(/\\/+$/, \"\");\n }\n\n function entity(kind: EntityKind): EntityDefinition {\n const base = SHOPWAVE_ENTITIES[kind];\n if (!base) throw new Error(`Unknown Shopwave entity \"${kind}\"`);\n return { ...base, ...(config.entities?.[kind] ?? {}) };\n }\n\n async function forward(request: Request, init: ForwardInit): Promise<Response> {\n const method = (init.method ?? \"GET\").toUpperCase();\n const path = init.path.replace(/^\\/+/, \"\");\n\n const extras = init.forwardExtras === false ? {} : parseExtras(request.headers.get(\"extras\"), blocked);\n if (!extras) return jsonError(400, \"invalid_extras\", \"The extras header must be a JSON object\");\n\n const requestToken = allowRequestToken ? readRequestToken(request) : null;\n let authorization: string | null;\n try {\n authorization = requestToken ? `OAuth ${requestToken}` : (await config.getAuthorization?.()) ?? null;\n } catch (error) {\n onError(error, { method, path: \"oauth/token\" });\n return jsonError(502, \"auth_unavailable\", \"Could not refresh the Shopwave session\");\n }\n if (!authorization) return jsonError(401, \"unauthorized\", \"Not logged in\");\n\n const body =\n init.postBody === undefined ? undefined : new URLSearchParams({ postBody: JSON.stringify(init.postBody) });\n\n const call = (authz: string) => {\n // Headers is case-insensitive, so explicit headers replace any extras\n // key that differs only in case.\n const headers = new Headers(extras);\n for (const [k, v] of Object.entries(init.headers ?? {})) headers.set(k, v);\n headers.set(\"Authorization\", authz);\n headers.set(\"x-accept-version\", apiVersion);\n if (body) headers.set(\"Content-Type\", \"application/x-www-form-urlencoded;charset=UTF-8\");\n const doFetch = config.fetch ?? fetch;\n return doFetch(`${apiBase()}/${path}`, { method, headers, body, cache: \"no-store\" } as RequestInit);\n };\n\n try {\n let response = await call(authorization);\n let upstream = await readUpstream(response);\n\n // Session token expired: refresh once and retry. Caller-supplied tokens\n // are the caller's to refresh.\n if (!requestToken && config.getAuthorization && isExpiredTokenResponse(response.status, upstream.json)) {\n const fresh = await config.getAuthorization({ forceRefresh: true });\n if (!fresh) return jsonError(401, \"unauthorized\", \"The Shopwave session has expired\");\n response = await call(fresh);\n upstream = await readUpstream(response);\n }\n\n if (NULL_BODY_STATUS.has(response.status) || !upstream.text) {\n return new Response(null, { status: response.status, headers: NO_STORE });\n }\n return new Response(upstream.text, {\n status: response.status,\n headers: {\n \"Content-Type\": upstream.json !== null ? \"application/json\" : response.headers.get(\"content-type\") ?? \"text/plain\",\n ...NO_STORE,\n },\n });\n } catch (error) {\n onError(error, { method, path });\n return jsonError(502, \"upstream_unavailable\", \"The Shopwave API could not be reached\");\n }\n }\n\n async function readJsonBody(request: Request): Promise<unknown> {\n const text = await request.text();\n if (!text) throw new RequestBodyError(\"Request body is empty\");\n try {\n return JSON.parse(text);\n } catch {\n throw new RequestBodyError(\"Request body is not valid JSON\");\n }\n }\n\n function notSupported(def: EntityDefinition, what: string): Response {\n return jsonError(405, \"method_not_allowed\", `The Shopwave API doesn't support ${what} for ${def.collection}`);\n }\n\n async function write(request: Request, def: EntityDefinition, forcedId?: string): Promise<Response> {\n if (!def.writable) return notSupported(def, \"writes\");\n let postBody: unknown;\n try {\n postBody = normalizeWriteBody(def, await readJsonBody(request), forcedId);\n } catch (error) {\n if (error instanceof RequestBodyError) return jsonError(400, \"invalid_body\", error.message);\n throw error;\n }\n return forward(request, { method: \"POST\", path: def.upstream, postBody });\n }\n\n function badId(): Response {\n return jsonError(400, \"invalid_id\", \"Missing or invalid id\");\n }\n\n return {\n forward,\n entity,\n\n collection(kind) {\n const def = entity(kind);\n const save: RouteHandler = (request) => write(request, def);\n return {\n GET: (request) => forward(request, { path: def.upstream }),\n POST: save,\n PUT: save,\n };\n },\n\n item(kind, options = {}) {\n const def = entity(kind);\n const param = options.param ?? \"id\";\n return {\n async GET(request, context) {\n const id = await routeParam(context, param);\n if (!id) return badId();\n return forward(request, { path: def.upstream, headers: { [def.idsHeader]: id } });\n },\n async PUT(request, context) {\n const id = await routeParam(context, param);\n if (!id) return badId();\n return write(request, def, id);\n },\n async DELETE(request, context) {\n const id = await routeParam(context, param);\n if (!id) return badId();\n if (def.deleteMode === \"none\") return notSupported(def, \"deletes\");\n if (def.deleteMode === \"retire\") {\n // No DELETE upstream (employees, promotions): set the end date to now.\n const field = def.retireField ?? \"exitDate\";\n return forward(request, {\n method: \"POST\",\n path: def.upstream,\n postBody: { [def.collection]: { 0: { id: /^\\d+$/.test(id) ? Number(id) : id, [field]: shopwaveDateTime() } } },\n forwardExtras: false,\n });\n }\n return forward(request, { method: \"DELETE\", path: def.upstream, headers: { [def.idHeader]: id }, forwardExtras: false });\n },\n };\n },\n\n passthrough(path) {\n return { GET: (request) => forward(request, { path }) };\n },\n };\n}\n","/**\n * react-shopwave-connect/next\n *\n * Drop-in Shopwave OAuth for Next.js (App Router, v14+): login + callback +\n * logout route handlers, an encrypted httpOnly session cookie (iron-session),\n * automatic token refresh, a route-handler guard and a proxy/middleware guard.\n *\n * Per app you only supply config (client id/secret, redirect URI, session\n * password). Server-only: never import this from a client component.\n *\n * Peer dependencies: `next` (>=14) and `iron-session` (8 or 9).\n */\n\nimport { cookies } from \"next/headers\";\nimport { NextResponse } from \"next/server\";\nimport { getIronSession, unsealData, type IronSession, type SessionOptions } from \"iron-session\";\n\nimport { createShopwaveOAuth, type ShopwaveOAuthClient, type ShopwaveOAuthConfig } from \"../server/oauth\";\nimport {\n authorizationHeader,\n isTokenExpired,\n normalizeStoredToken,\n type ShopwaveToken,\n} from \"../server/token\";\nimport { createState, safeEqual, sanitizeReturnTo } from \"../server/returnTo\";\nimport { ShopwaveAuthError } from \"../server/errors\";\nimport {\n createShopwaveApiHandlers,\n readRequestToken,\n type ShopwaveApiConfig,\n type ShopwaveApiHandlers,\n} from \"../server/api\";\nimport type { SessionStatus } from \"../core/session\";\n\nexport type { SessionStatus } from \"../core/session\";\nexport type { ShopwaveToken } from \"../server/token\";\nexport { ShopwaveAuthError } from \"../server/errors\";\nexport { isExpiredTokenResponse, authorizationHeader } from \"../server/token\";\nexport { readRequestToken, SHOPWAVE_ENTITIES } from \"../server\";\nexport type {\n ShopwaveApiHandlers,\n CollectionHandlers,\n ItemHandlers,\n RouteContext,\n RouteHandler,\n ForwardInit,\n EntityKind,\n EntityDefinition,\n} from \"../server\";\n\n// ---------------------------------------------------------------------------\n// Config & types\n// ---------------------------------------------------------------------------\n\nexport interface ShopwaveSessionConfig {\n /**\n * Encrypts the session cookie. At least 32 characters. Pass a map such as\n * `{ 2: newPassword, 1: oldPassword }` to rotate without logging users out.\n */\n password: string | Record<string, string>;\n /** Defaults to `\"shopwave_session\"`. Use a different name per app on a shared domain. */\n cookieName?: string;\n /** Session lifetime in seconds. Defaults to iron-session's 14 days. */\n ttl?: number;\n /** Defaults to `true` in production, `false` otherwise (so http://localhost works). */\n secure?: boolean;\n /** Defaults to `\"lax\"`, which the OAuth redirect back to your app needs. */\n sameSite?: \"lax\" | \"strict\" | \"none\";\n domain?: string;\n}\n\nexport interface ShopwaveAuthConfig extends ShopwaveOAuthConfig {\n session: ShopwaveSessionConfig;\n /**\n * Path where `handlers.auth` is mounted. It starts the login AND receives the\n * callback, so it should be the path of `redirectUri`. Defaults to `\"/auth\"`.\n */\n authPath?: string;\n /** Path where `handlers.logout` is mounted. Defaults to `${authPath}/logout`. */\n logoutPath?: string;\n /**\n * Path where `handlers.session` is mounted. Always public, because it is how\n * the browser finds out it is logged out. Defaults to `\"/api/session\"`.\n */\n sessionPath?: string;\n /** Where users land after login when no `returnTo` was given. Defaults to `\"/\"`. */\n defaultReturnTo?: string;\n /**\n * Reject callbacks whose `state` doesn't match the one we sent (login-CSRF\n * protection). Defaults to `true`. Only turn off if the auth server does not\n * echo `state` back.\n */\n requireState?: boolean;\n /**\n * Refresh this many seconds before expiry. Defaults to `0`, because the\n * Shopwave server only issues a new access token once the old one expired.\n */\n refreshSkewSeconds?: number;\n /** How long a started login stays valid, in seconds. Defaults to 600. */\n loginTimeoutSeconds?: number;\n}\n\n/** What we keep in the encrypted cookie. Tokens never leave the server. */\nexport interface ShopwaveSessionData {\n token?: ShopwaveToken | Record<string, unknown>;\n pendingLogin?: { state: string; returnTo: string; createdAt: number };\n}\n\nexport interface AuthContext {\n accessToken: string;\n /** Ready-to-use `Authorization` header value, e.g. `OAuth 111ad…`. */\n authorization: string;\n token: ShopwaveToken;\n}\n\nexport interface ProtectOptions {\n /**\n * Paths that don't need a login. A path matches itself and everything below\n * it (`\"/tools/tag-joiner\"` covers `/tools/tag-joiner/x`). `\"/\"` matches only\n * the root. The auth, logout and session paths are always public.\n */\n publicPaths?: string[];\n /** Paths that get a 401 JSON response instead of a login redirect. Defaults to `[\"/api\"]`. */\n apiPaths?: string[];\n /**\n * Let API requests that carry their own token (`Authorization: OAuth <token>`,\n * or the legacy `token` header / `extras.token`) through without a session\n * cookie. The route handlers from {@link createShopwaveApi} forward that token\n * to the Shopwave API, which validates it. Defaults to `false`.\n */\n allowRequestToken?: boolean;\n}\n\nexport interface ShopwaveAuth {\n /** The underlying framework-agnostic OAuth client. */\n readonly oauth: ShopwaveOAuthClient;\n /** Route handlers to export from your app. */\n readonly handlers: {\n /** Mount at the redirect-URI path (e.g. `app/auth/route.ts`): `export const GET = auth.handlers.auth`. */\n auth: (request: Request) => Promise<Response>;\n /** Mount at `app/auth/logout/route.ts`: `export const GET = auth.handlers.logout`. */\n logout: (request: Request) => Promise<Response>;\n /** Mount at `app/api/session/route.ts`: `export const { GET, DELETE } = auth.handlers.session`. */\n session: {\n GET: (request?: Request) => Promise<Response>;\n DELETE: (request?: Request) => Promise<Response>;\n };\n };\n /** Login status for the current request. Never includes tokens. */\n getStatus(): Promise<SessionStatus>;\n /**\n * A valid access token for the current user, refreshing it when expired\n * (and saving the new one in the cookie when called from a route handler,\n * server action or proxy). `null` when logged out or the refresh token was\n * rejected. Pass `forceRefresh` after the API answered 401 / error 908.\n */\n getAccessToken(options?: { forceRefresh?: boolean }): Promise<string | null>;\n /** Same as `getAccessToken` but returns the whole token object. */\n getToken(options?: { forceRefresh?: boolean }): Promise<ShopwaveToken | null>;\n /** `Authorization` header value (`\"OAuth <token>\"`), or `null` when logged out. */\n getAuthorizationHeader(options?: { forceRefresh?: boolean }): Promise<string | null>;\n /** Wraps a route handler; responds 401 JSON when there's no valid session. */\n withAuth<C = unknown>(\n handler: (request: Request, context: C, auth: AuthContext) => Response | Promise<Response>\n ): (request: Request, context: C) => Promise<Response>;\n /**\n * For `proxy.ts` / `middleware.ts`. Returns a redirect (pages) or 401\n * (API paths) when the request has no session, or `undefined` to continue.\n * Only reads the cookie — it never calls the auth server.\n */\n protect(request: Request, options?: ProtectOptions): Promise<Response | undefined>;\n /** Cookie-only check usable anywhere you have the Request. */\n isAuthenticated(request: Request): Promise<boolean>;\n /** Link that starts the login and comes back to `returnTo`. */\n loginPath(returnTo?: string): string;\n /** Link that logs the user out of the app and the auth server. */\n readonly logoutPath: string;\n /** Raw iron-session for advanced use (e.g. storing app data alongside the token). */\n getSession(): Promise<IronSession<ShopwaveSessionData>>;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\nfunction normalizePath(p: string): string {\n const withSlash = p.startsWith(\"/\") ? p : `/${p}`;\n return withSlash.length > 1 ? withSlash.replace(/\\/+$/, \"\") : withSlash;\n}\n\nfunction pathMatches(pathname: string, list: string[]): boolean {\n return list.some((raw) => {\n const p = normalizePath(raw);\n if (p === \"/\") return pathname === \"/\";\n return pathname === p || pathname.startsWith(`${p}/`);\n });\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) =>\n ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c] as string\n );\n}\n\nfunction readCookie(request: Request, name: string): string | undefined {\n const fromNext = (request as { cookies?: { get?: (n: string) => { value?: string } | undefined } })\n .cookies?.get?.(name)?.value;\n if (fromNext) return fromNext;\n const header = request.headers.get(\"cookie\");\n if (!header) return undefined;\n for (const part of header.split(\";\")) {\n const idx = part.indexOf(\"=\");\n if (idx === -1) continue;\n if (part.slice(0, idx).trim() === name) {\n const value = part.slice(idx + 1).trim();\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n }\n }\n return undefined;\n}\n\nlet warnedAboutReadOnlyCookies = false;\n\n/**\n * Creates the Shopwave auth instance for one Next.js app. Call once at module\n * scope (e.g. `lib/auth.ts`) and reuse. Config is validated on first use, so\n * a missing env var doesn't fail `next build`.\n */\nexport function createShopwaveAuth(config: ShopwaveAuthConfig): ShopwaveAuth {\n if (typeof window !== \"undefined\") {\n throw new Error(\"react-shopwave-connect/next is server-only. Import it from route handlers, server code or proxy.ts.\");\n }\n\n const oauth = createShopwaveOAuth(config);\n const authPath = normalizePath(config.authPath ?? \"/auth\");\n const logoutPath = normalizePath(config.logoutPath ?? `${authPath}/logout`);\n const sessionPath = normalizePath(config.sessionPath ?? \"/api/session\");\n const defaultReturnTo = sanitizeReturnTo(config.defaultReturnTo, \"/\");\n const requireState = config.requireState ?? true;\n const skewSeconds = config.refreshSkewSeconds ?? 0;\n const loginTimeoutMs = (config.loginTimeoutSeconds ?? 600) * 1000;\n const cookieName = config.session?.cookieName ?? \"shopwave_session\";\n\n /**\n * Refresh de-duplication, per server instance. Requests that read the same\n * (old) token share one refresh while it's in flight, and for a short while\n * afterwards reuse its result instead of refreshing again.\n */\n const inflightRefresh = new Map<string, Promise<ShopwaveToken>>();\n const recentRefresh = new Map<string, { token: ShopwaveToken; at: number }>();\n const RECENT_REFRESH_MS = 30_000;\n\n function sessionOptions(): SessionOptions {\n const password = config.session?.password;\n const passwords = typeof password === \"string\" ? [password] : Object.values(password ?? {});\n if (passwords.length === 0 || passwords.some((p) => typeof p !== \"string\" || p.length < 32)) {\n throw new ShopwaveAuthError(\n \"config_invalid\",\n \"Shopwave auth: session.password must be at least 32 characters (set a long random secret in your env).\"\n );\n }\n const cookieOptions: SessionOptions[\"cookieOptions\"] = {\n httpOnly: true,\n secure: config.session.secure ?? process.env.NODE_ENV === \"production\",\n sameSite: config.session.sameSite ?? \"lax\",\n path: \"/\",\n };\n if (config.session.domain) cookieOptions.domain = config.session.domain;\n const options: SessionOptions = {\n cookieName,\n password: password as SessionOptions[\"password\"],\n cookieOptions,\n };\n // Only pass ttl when set: an explicit `undefined` overrides iron-session's\n // 14-day default and produces `Max-Age=NaN`.\n if (typeof config.session.ttl === \"number\") options.ttl = config.session.ttl;\n return options;\n }\n\n async function getSession(): Promise<IronSession<ShopwaveSessionData>> {\n return getIronSession<ShopwaveSessionData>(await cookies(), sessionOptions());\n }\n\n /**\n * Cookies can only be written from route handlers, server actions and proxy.\n * From a Server Component the token is still usable for this request; it will\n * simply be refreshed again next time.\n */\n async function trySave(session: IronSession<ShopwaveSessionData>): Promise<void> {\n try {\n await session.save();\n } catch (error) {\n if (!warnedAboutReadOnlyCookies) {\n warnedAboutReadOnlyCookies = true;\n console.warn(\n \"[react-shopwave-connect] Could not update the session cookie (called outside a route handler / server action?).\",\n (error as Error)?.message\n );\n }\n }\n }\n\n function tryDestroy(session: IronSession<ShopwaveSessionData>): void {\n try {\n session.destroy();\n } catch {\n /* read-only cookie context — nothing to clear */\n }\n }\n\n function refreshOnce(token: ShopwaveToken): Promise<ShopwaveToken> {\n const key = `${token.refreshToken}\\u0000${token.accessToken}`;\n const now = Date.now();\n\n const recent = recentRefresh.get(key);\n if (recent && now - recent.at < RECENT_REFRESH_MS) return Promise.resolve(recent.token);\n\n let pending = inflightRefresh.get(key);\n if (!pending) {\n pending = oauth\n .refreshToken(token)\n .then((fresh) => {\n for (const [k, v] of recentRefresh) if (now - v.at >= RECENT_REFRESH_MS) recentRefresh.delete(k);\n recentRefresh.set(key, { token: fresh, at: Date.now() });\n return fresh;\n })\n .finally(() => inflightRefresh.delete(key));\n inflightRefresh.set(key, pending);\n }\n return pending;\n }\n\n async function getToken({ forceRefresh = false }: { forceRefresh?: boolean } = {}): Promise<ShopwaveToken | null> {\n const session = await getSession();\n const token = normalizeStoredToken(session.token);\n if (!token) return null;\n\n const expired = isTokenExpired(token, { skewSeconds });\n if (!forceRefresh && !expired) return token;\n\n if (!token.refreshToken) {\n if (expired || forceRefresh) {\n tryDestroy(session);\n return null;\n }\n return token;\n }\n\n try {\n const refreshed = await refreshOnce(token);\n session.token = refreshed;\n await trySave(session);\n return refreshed;\n } catch (error) {\n if (error instanceof ShopwaveAuthError && error.isInvalidGrant) {\n // Refresh token rejected: the user has to sign in again.\n tryDestroy(session);\n return null;\n }\n throw error;\n }\n }\n\n function loginPath(returnTo?: string): string {\n if (!returnTo) return authPath;\n const safe = sanitizeReturnTo(returnTo, defaultReturnTo);\n return `${authPath}?returnTo=${encodeURIComponent(safe)}`;\n }\n\n function redirectTo(location: string, request: Request): Response {\n return NextResponse.redirect(new URL(location, request.url), 302);\n }\n\n function errorPage(status: number, message: string, retryHref: string): Response {\n const html = `<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Sign-in problem</title></head><body style=\"font-family:system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 1rem;line-height:1.5\"><h1 style=\"font-size:1.25rem\">Sign-in didn’t complete</h1><p>${escapeHtml(message)}</p><p><a href=\"${escapeHtml(retryHref)}\">Try again</a></p></body></html>`;\n return new NextResponse(html, {\n status,\n headers: { \"Content-Type\": \"text/html; charset=utf-8\", \"Cache-Control\": \"no-store\" },\n });\n }\n\n async function authHandler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const session = await getSession();\n const pending = session.pendingLogin;\n\n // The auth server reported a problem (e.g. user cancelled).\n const oauthError = url.searchParams.get(\"error\");\n if (oauthError) {\n delete session.pendingLogin;\n await trySave(session);\n const detail = url.searchParams.get(\"error_description\") || oauthError;\n return errorPage(400, `Shopwave sign-in reported: ${detail}.`, loginPath(pending?.returnTo));\n }\n\n const code = url.searchParams.get(\"code\");\n\n // 1) No code: start a login.\n if (!code) {\n const returnTo = sanitizeReturnTo(url.searchParams.get(\"returnTo\"), defaultReturnTo);\n const state = createState();\n session.pendingLogin = { state, returnTo, createdAt: Date.now() };\n await session.save();\n return NextResponse.redirect(oauth.buildLoginUrl({ state }), 302);\n }\n\n // 2) Callback: verify state, exchange the code, store the token.\n const returnedState = url.searchParams.get(\"state\");\n const isFresh = !!pending && Date.now() - pending.createdAt <= loginTimeoutMs;\n const stateMatches = !!pending && !!returnedState && safeEqual(returnedState, pending.state);\n const rejected = requireState\n ? !(stateMatches && isFresh)\n : !!pending && !!returnedState && !stateMatches;\n\n if (rejected) {\n delete session.pendingLogin;\n await trySave(session);\n return errorPage(\n 400,\n \"This sign-in link has expired or didn’t start from this browser. Please sign in again.\",\n loginPath(pending?.returnTo)\n );\n }\n\n let token: ShopwaveToken;\n try {\n token = await oauth.exchangeCode(code);\n } catch (error) {\n console.error(\"[react-shopwave-connect] Code exchange failed:\", (error as Error)?.message);\n delete session.pendingLogin;\n await trySave(session);\n const status = error instanceof ShopwaveAuthError && error.isInvalidGrant ? 400 : 502;\n return errorPage(status, \"We couldn’t complete sign-in with Shopwave.\", loginPath(pending?.returnTo));\n }\n\n const returnTo = pending?.returnTo ?? defaultReturnTo;\n delete session.pendingLogin;\n session.token = token;\n await session.save();\n return redirectTo(returnTo, request);\n }\n\n async function logoutHandler(_request: Request): Promise<Response> {\n const session = await getSession();\n session.destroy();\n return NextResponse.redirect(oauth.buildLogoutUrl(), 302);\n }\n\n async function getStatus(): Promise<SessionStatus> {\n const session = await getSession();\n const token = normalizeStoredToken(session.token);\n if (!token) return { loggedIn: false };\n const usable = !isTokenExpired(token) || !!token.refreshToken;\n return usable ? { loggedIn: true, expiresAt: token.expiresAt } : { loggedIn: false };\n }\n\n async function isAuthenticated(request: Request): Promise<boolean> {\n const value = readCookie(request, cookieName);\n if (!value) return false;\n try {\n const { password, ttl } = sessionOptions();\n const data = await unsealData<ShopwaveSessionData>(value, { password, ttl });\n const token = normalizeStoredToken(data?.token);\n return !!token && (!isTokenExpired(token) || !!token.refreshToken);\n } catch {\n return false;\n }\n }\n\n const noStore = { \"Cache-Control\": \"no-store\" };\n\n return {\n oauth,\n logoutPath,\n loginPath,\n getSession,\n getStatus,\n getToken,\n\n async getAccessToken(options) {\n return (await getToken(options))?.accessToken ?? null;\n },\n\n async getAuthorizationHeader(options) {\n const token = await getToken(options);\n return token ? authorizationHeader(token) : null;\n },\n\n withAuth(handler) {\n return async (request, context) => {\n const token = await getToken();\n if (!token) {\n return NextResponse.json({ error: \"unauthorized\" }, { status: 401, headers: noStore });\n }\n return handler(request, context, {\n accessToken: token.accessToken,\n authorization: authorizationHeader(token),\n token,\n });\n };\n },\n\n isAuthenticated,\n\n async protect(request, options = {}) {\n const { pathname, search } = new URL(request.url);\n if (pathMatches(pathname, [authPath, logoutPath, sessionPath, ...(options.publicPaths ?? [])])) {\n return undefined;\n }\n const isApi = pathMatches(pathname, options.apiPaths ?? [\"/api\"]);\n if (isApi && options.allowRequestToken && readRequestToken(request)) return undefined;\n if (await isAuthenticated(request)) return undefined;\n if (isApi) {\n return NextResponse.json({ error: \"unauthorized\" }, { status: 401, headers: noStore });\n }\n return redirectTo(loginPath(pathname + search), request);\n },\n\n handlers: {\n auth: authHandler,\n logout: logoutHandler,\n session: {\n async GET() {\n return NextResponse.json(await getStatus(), { headers: noStore });\n },\n async DELETE() {\n const session = await getSession();\n session.destroy();\n return new NextResponse(null, { status: 204, headers: noStore });\n },\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// API route handlers\n// ---------------------------------------------------------------------------\n\nexport interface ShopwaveNextApiConfig extends Omit<ShopwaveApiConfig, \"getAuthorization\"> {\n /** The app's auth instance: supplies (and refreshes) the logged-in user's token. */\n auth: Pick<ShopwaveAuth, \"getAuthorizationHeader\">;\n}\n\n/**\n * Ready-made Next.js route handlers for the SDK's `/api/*` contract, using the\n * session from `createShopwaveAuth`. Create once (e.g. `lib/shopwave.ts`):\n *\n * ```ts\n * export const shopwave = createShopwaveApi({ auth, apiUrl: process.env.SHOPWAVE_API_SERVER_URL! });\n * ```\n *\n * then each route file is one line:\n *\n * ```ts\n * // app/api/products/route.ts\n * export const { GET, POST, PUT } = shopwave.collection(\"product\");\n * // app/api/products/[id]/route.ts\n * export const { GET, PUT, DELETE } = shopwave.item(\"product\");\n * // app/api/report/route.ts\n * export const { GET } = shopwave.passthrough(\"report\");\n * ```\n */\nexport function createShopwaveApi(config: ShopwaveNextApiConfig): ShopwaveApiHandlers {\n if (typeof window !== \"undefined\") {\n throw new Error(\"react-shopwave-connect/next is server-only. Import it from route handlers or server code.\");\n }\n const { auth, ...rest } = config;\n return createShopwaveApiHandlers({\n ...rest,\n getAuthorization: (options) => auth.getAuthorizationHeader(options),\n });\n}\n"],"mappings":"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"}