react-shopwave-connect 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -21
- package/dist/core/index.cjs +46 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +34 -6
- package/dist/core/index.d.ts +34 -6
- package/dist/core/index.js +44 -6
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +50 -5
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +29 -1
- package/dist/hooks/index.d.ts +29 -1
- package/dist/hooks/index.js +50 -6
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +64 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -6
- package/dist/index.d.ts +45 -6
- package/dist/index.js +61 -6
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +585 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +255 -0
- package/dist/next/index.d.ts +255 -0
- package/dist/next/index.js +556 -0
- package/dist/next/index.js.map +1 -0
- package/dist/server/index.cjs +260 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +177 -0
- package/dist/server/index.d.ts +177 -0
- package/dist/server/index.js +248 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +41 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["body: URLSearchParams | FormData","response: Response","cookieOptions: SessionOptions[\"cookieOptions\"]","options: SessionOptions","NextResponse","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 ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" })[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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"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { IronSession } from "iron-session";
|
|
2
|
+
|
|
3
|
+
//#region src/server/token.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Token model + helpers shared by every server-side integration.
|
|
7
|
+
*
|
|
8
|
+
* The Shopwave auth server returns (snake_case):
|
|
9
|
+
* { access_token, refresh_token, token_type: "OAuth", expires_in: 43200 }
|
|
10
|
+
*
|
|
11
|
+
* We store a normalised, camelCase shape with an absolute expiry so any
|
|
12
|
+
* request can decide whether the token is still usable without extra state.
|
|
13
|
+
*/
|
|
14
|
+
interface ShopwaveToken {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
/** Long-lived; the Shopwave server does not rotate it on refresh. */
|
|
17
|
+
refreshToken?: string;
|
|
18
|
+
/** Scheme used in the Authorization header. Shopwave uses "OAuth". */
|
|
19
|
+
tokenType: string;
|
|
20
|
+
/** Absolute expiry, epoch milliseconds. Undefined if the server didn't say. */
|
|
21
|
+
expiresAt?: number;
|
|
22
|
+
}
|
|
23
|
+
/** `Authorization` header value, e.g. `OAuth 111ad…`. */
|
|
24
|
+
declare function authorizationHeader(token: ShopwaveToken): string;
|
|
25
|
+
/**
|
|
26
|
+
* True when a Shopwave API response means "your access token is no longer
|
|
27
|
+
* valid": HTTP 401, or the API error 908 in the response envelope.
|
|
28
|
+
* Use it to trigger a forced refresh + a single retry.
|
|
29
|
+
*/
|
|
30
|
+
declare function isExpiredTokenResponse(status: number, body?: unknown): boolean;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/server/oauth.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Per-app OAuth settings. Everything that differs between Shopwave apps lives
|
|
35
|
+
* here; the flow itself is identical for all of them.
|
|
36
|
+
*/
|
|
37
|
+
interface ShopwaveOAuthConfig {
|
|
38
|
+
/** e.g. `https://secure.merchantstack.com` (no trailing slash needed). */
|
|
39
|
+
authServerUrl: string;
|
|
40
|
+
clientId: string;
|
|
41
|
+
/** Server-side only. Never expose it to the browser. */
|
|
42
|
+
clientSecret: string;
|
|
43
|
+
/**
|
|
44
|
+
* The callback URL registered for this client on the auth server, e.g.
|
|
45
|
+
* `https://admin.example.com/auth`. Must match exactly.
|
|
46
|
+
*/
|
|
47
|
+
redirectUri: string;
|
|
48
|
+
/** Where the auth server sends the user after logout. Defaults to `redirectUri`. */
|
|
49
|
+
postLogoutRedirectUri?: string;
|
|
50
|
+
/** Defaults to `"application"`. */
|
|
51
|
+
scope?: string;
|
|
52
|
+
/** Defaults to `"online"`. */
|
|
53
|
+
accessType?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Body encoding for `POST /oauth/token`. Defaults to standard
|
|
56
|
+
* `application/x-www-form-urlencoded`; `"multipart"` sends `FormData`.
|
|
57
|
+
*/
|
|
58
|
+
tokenRequestFormat?: "urlencoded" | "multipart";
|
|
59
|
+
/** Auth-server paths. Defaults match Shopwave: `/login`, `/oauth/token`, `/logout`. */
|
|
60
|
+
endpoints?: {
|
|
61
|
+
login?: string;
|
|
62
|
+
token?: string;
|
|
63
|
+
logout?: string;
|
|
64
|
+
};
|
|
65
|
+
/** Custom fetch (tests, proxies). Defaults to global `fetch`. */
|
|
66
|
+
fetch?: typeof fetch;
|
|
67
|
+
}
|
|
68
|
+
interface ShopwaveOAuthClient {
|
|
69
|
+
readonly config: Readonly<ShopwaveOAuthConfig>;
|
|
70
|
+
/** URL to send the browser to so the user can sign in. */
|
|
71
|
+
buildLoginUrl(params?: {
|
|
72
|
+
state?: string;
|
|
73
|
+
}): string;
|
|
74
|
+
/** URL to send the browser to so the auth server ends its own session. */
|
|
75
|
+
buildLogoutUrl(params?: {
|
|
76
|
+
redirectUri?: string;
|
|
77
|
+
}): string;
|
|
78
|
+
/** Exchanges the `?code=` from the callback for tokens (server-to-server). */
|
|
79
|
+
exchangeCode(code: string): Promise<ShopwaveToken>;
|
|
80
|
+
/**
|
|
81
|
+
* Gets a fresh access token. Shopwave keeps the same refresh token, so the
|
|
82
|
+
* returned token carries the previous refresh token when none is sent back.
|
|
83
|
+
*/
|
|
84
|
+
refreshToken(token: ShopwaveToken | string): Promise<ShopwaveToken>;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/core/session.d.ts
|
|
88
|
+
/**
|
|
89
|
+
* Login status returned by the app's `/api/session` route. It deliberately
|
|
90
|
+
* never contains tokens — those stay in the encrypted httpOnly cookie.
|
|
91
|
+
*/
|
|
92
|
+
interface SessionStatus {
|
|
93
|
+
loggedIn: boolean;
|
|
94
|
+
/** Access-token expiry (epoch ms), when known. */
|
|
95
|
+
expiresAt?: number;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/server/errors.d.ts
|
|
99
|
+
/**
|
|
100
|
+
* Error thrown by the server-side auth helpers.
|
|
101
|
+
*
|
|
102
|
+
* `code` is stable and safe to branch on; `status` is the HTTP status returned
|
|
103
|
+
* by the Shopwave auth server when there was one.
|
|
104
|
+
*/
|
|
105
|
+
type ShopwaveAuthErrorCode = "config_invalid" | "token_exchange_failed" | "token_refresh_failed" | "token_response_invalid" | "network_error";
|
|
106
|
+
declare class ShopwaveAuthError extends Error {
|
|
107
|
+
readonly code: ShopwaveAuthErrorCode;
|
|
108
|
+
readonly status?: number;
|
|
109
|
+
/** Raw response body from the auth server, if any. Never contains our secret. */
|
|
110
|
+
readonly body?: string;
|
|
111
|
+
constructor(code: ShopwaveAuthErrorCode, message: string, details?: {
|
|
112
|
+
status?: number;
|
|
113
|
+
body?: string;
|
|
114
|
+
cause?: unknown;
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* True when the auth server rejected the grant itself (bad/expired code or
|
|
118
|
+
* refresh token) rather than failing for a transient reason. Callers should
|
|
119
|
+
* treat the user as logged out.
|
|
120
|
+
*/
|
|
121
|
+
get isInvalidGrant(): boolean;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/next/index.d.ts
|
|
125
|
+
interface ShopwaveSessionConfig {
|
|
126
|
+
/**
|
|
127
|
+
* Encrypts the session cookie. At least 32 characters. Pass a map such as
|
|
128
|
+
* `{ 2: newPassword, 1: oldPassword }` to rotate without logging users out.
|
|
129
|
+
*/
|
|
130
|
+
password: string | Record<string, string>;
|
|
131
|
+
/** Defaults to `"shopwave_session"`. Use a different name per app on a shared domain. */
|
|
132
|
+
cookieName?: string;
|
|
133
|
+
/** Session lifetime in seconds. Defaults to iron-session's 14 days. */
|
|
134
|
+
ttl?: number;
|
|
135
|
+
/** Defaults to `true` in production, `false` otherwise (so http://localhost works). */
|
|
136
|
+
secure?: boolean;
|
|
137
|
+
/** Defaults to `"lax"`, which the OAuth redirect back to your app needs. */
|
|
138
|
+
sameSite?: "lax" | "strict" | "none";
|
|
139
|
+
domain?: string;
|
|
140
|
+
}
|
|
141
|
+
interface ShopwaveAuthConfig extends ShopwaveOAuthConfig {
|
|
142
|
+
session: ShopwaveSessionConfig;
|
|
143
|
+
/**
|
|
144
|
+
* Path where `handlers.auth` is mounted. It starts the login AND receives the
|
|
145
|
+
* callback, so it should be the path of `redirectUri`. Defaults to `"/auth"`.
|
|
146
|
+
*/
|
|
147
|
+
authPath?: string;
|
|
148
|
+
/** Path where `handlers.logout` is mounted. Defaults to `${authPath}/logout`. */
|
|
149
|
+
logoutPath?: string;
|
|
150
|
+
/**
|
|
151
|
+
* Path where `handlers.session` is mounted. Always public, because it is how
|
|
152
|
+
* the browser finds out it is logged out. Defaults to `"/api/session"`.
|
|
153
|
+
*/
|
|
154
|
+
sessionPath?: string;
|
|
155
|
+
/** Where users land after login when no `returnTo` was given. Defaults to `"/"`. */
|
|
156
|
+
defaultReturnTo?: string;
|
|
157
|
+
/**
|
|
158
|
+
* Reject callbacks whose `state` doesn't match the one we sent (login-CSRF
|
|
159
|
+
* protection). Defaults to `true`. Only turn off if the auth server does not
|
|
160
|
+
* echo `state` back.
|
|
161
|
+
*/
|
|
162
|
+
requireState?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Refresh this many seconds before expiry. Defaults to `0`, because the
|
|
165
|
+
* Shopwave server only issues a new access token once the old one expired.
|
|
166
|
+
*/
|
|
167
|
+
refreshSkewSeconds?: number;
|
|
168
|
+
/** How long a started login stays valid, in seconds. Defaults to 600. */
|
|
169
|
+
loginTimeoutSeconds?: number;
|
|
170
|
+
}
|
|
171
|
+
/** What we keep in the encrypted cookie. Tokens never leave the server. */
|
|
172
|
+
interface ShopwaveSessionData {
|
|
173
|
+
token?: ShopwaveToken | Record<string, unknown>;
|
|
174
|
+
pendingLogin?: {
|
|
175
|
+
state: string;
|
|
176
|
+
returnTo: string;
|
|
177
|
+
createdAt: number;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
interface AuthContext {
|
|
181
|
+
accessToken: string;
|
|
182
|
+
/** Ready-to-use `Authorization` header value, e.g. `OAuth 111ad…`. */
|
|
183
|
+
authorization: string;
|
|
184
|
+
token: ShopwaveToken;
|
|
185
|
+
}
|
|
186
|
+
interface ProtectOptions {
|
|
187
|
+
/**
|
|
188
|
+
* Paths that don't need a login. A path matches itself and everything below
|
|
189
|
+
* it (`"/tools/tag-joiner"` covers `/tools/tag-joiner/x`). `"/"` matches only
|
|
190
|
+
* the root. The auth, logout and session paths are always public.
|
|
191
|
+
*/
|
|
192
|
+
publicPaths?: string[];
|
|
193
|
+
/** Paths that get a 401 JSON response instead of a login redirect. Defaults to `["/api"]`. */
|
|
194
|
+
apiPaths?: string[];
|
|
195
|
+
}
|
|
196
|
+
interface ShopwaveAuth {
|
|
197
|
+
/** The underlying framework-agnostic OAuth client. */
|
|
198
|
+
readonly oauth: ShopwaveOAuthClient;
|
|
199
|
+
/** Route handlers to export from your app. */
|
|
200
|
+
readonly handlers: {
|
|
201
|
+
/** Mount at the redirect-URI path (e.g. `app/auth/route.ts`): `export const GET = auth.handlers.auth`. */
|
|
202
|
+
auth: (request: Request) => Promise<Response>;
|
|
203
|
+
/** Mount at `app/auth/logout/route.ts`: `export const GET = auth.handlers.logout`. */
|
|
204
|
+
logout: (request: Request) => Promise<Response>;
|
|
205
|
+
/** Mount at `app/api/session/route.ts`: `export const { GET, DELETE } = auth.handlers.session`. */
|
|
206
|
+
session: {
|
|
207
|
+
GET: (request?: Request) => Promise<Response>;
|
|
208
|
+
DELETE: (request?: Request) => Promise<Response>;
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
/** Login status for the current request. Never includes tokens. */
|
|
212
|
+
getStatus(): Promise<SessionStatus>;
|
|
213
|
+
/**
|
|
214
|
+
* A valid access token for the current user, refreshing it when expired
|
|
215
|
+
* (and saving the new one in the cookie when called from a route handler,
|
|
216
|
+
* server action or proxy). `null` when logged out or the refresh token was
|
|
217
|
+
* rejected. Pass `forceRefresh` after the API answered 401 / error 908.
|
|
218
|
+
*/
|
|
219
|
+
getAccessToken(options?: {
|
|
220
|
+
forceRefresh?: boolean;
|
|
221
|
+
}): Promise<string | null>;
|
|
222
|
+
/** Same as `getAccessToken` but returns the whole token object. */
|
|
223
|
+
getToken(options?: {
|
|
224
|
+
forceRefresh?: boolean;
|
|
225
|
+
}): Promise<ShopwaveToken | null>;
|
|
226
|
+
/** `Authorization` header value (`"OAuth <token>"`), or `null` when logged out. */
|
|
227
|
+
getAuthorizationHeader(options?: {
|
|
228
|
+
forceRefresh?: boolean;
|
|
229
|
+
}): Promise<string | null>;
|
|
230
|
+
/** Wraps a route handler; responds 401 JSON when there's no valid session. */
|
|
231
|
+
withAuth<C = unknown>(handler: (request: Request, context: C, auth: AuthContext) => Response | Promise<Response>): (request: Request, context: C) => Promise<Response>;
|
|
232
|
+
/**
|
|
233
|
+
* For `proxy.ts` / `middleware.ts`. Returns a redirect (pages) or 401
|
|
234
|
+
* (API paths) when the request has no session, or `undefined` to continue.
|
|
235
|
+
* Only reads the cookie — it never calls the auth server.
|
|
236
|
+
*/
|
|
237
|
+
protect(request: Request, options?: ProtectOptions): Promise<Response | undefined>;
|
|
238
|
+
/** Cookie-only check usable anywhere you have the Request. */
|
|
239
|
+
isAuthenticated(request: Request): Promise<boolean>;
|
|
240
|
+
/** Link that starts the login and comes back to `returnTo`. */
|
|
241
|
+
loginPath(returnTo?: string): string;
|
|
242
|
+
/** Link that logs the user out of the app and the auth server. */
|
|
243
|
+
readonly logoutPath: string;
|
|
244
|
+
/** Raw iron-session for advanced use (e.g. storing app data alongside the token). */
|
|
245
|
+
getSession(): Promise<IronSession<ShopwaveSessionData>>;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Creates the Shopwave auth instance for one Next.js app. Call once at module
|
|
249
|
+
* scope (e.g. `lib/auth.ts`) and reuse. Config is validated on first use, so
|
|
250
|
+
* a missing env var doesn't fail `next build`.
|
|
251
|
+
*/
|
|
252
|
+
declare function createShopwaveAuth(config: ShopwaveAuthConfig): ShopwaveAuth;
|
|
253
|
+
//#endregion
|
|
254
|
+
export { AuthContext, ProtectOptions, type SessionStatus, ShopwaveAuth, ShopwaveAuthConfig, ShopwaveAuthError, ShopwaveSessionConfig, ShopwaveSessionData, type ShopwaveToken, authorizationHeader, createShopwaveAuth, isExpiredTokenResponse };
|
|
255
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { IronSession } from "iron-session";
|
|
2
|
+
|
|
3
|
+
//#region src/server/token.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Token model + helpers shared by every server-side integration.
|
|
7
|
+
*
|
|
8
|
+
* The Shopwave auth server returns (snake_case):
|
|
9
|
+
* { access_token, refresh_token, token_type: "OAuth", expires_in: 43200 }
|
|
10
|
+
*
|
|
11
|
+
* We store a normalised, camelCase shape with an absolute expiry so any
|
|
12
|
+
* request can decide whether the token is still usable without extra state.
|
|
13
|
+
*/
|
|
14
|
+
interface ShopwaveToken {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
/** Long-lived; the Shopwave server does not rotate it on refresh. */
|
|
17
|
+
refreshToken?: string;
|
|
18
|
+
/** Scheme used in the Authorization header. Shopwave uses "OAuth". */
|
|
19
|
+
tokenType: string;
|
|
20
|
+
/** Absolute expiry, epoch milliseconds. Undefined if the server didn't say. */
|
|
21
|
+
expiresAt?: number;
|
|
22
|
+
}
|
|
23
|
+
/** `Authorization` header value, e.g. `OAuth 111ad…`. */
|
|
24
|
+
declare function authorizationHeader(token: ShopwaveToken): string;
|
|
25
|
+
/**
|
|
26
|
+
* True when a Shopwave API response means "your access token is no longer
|
|
27
|
+
* valid": HTTP 401, or the API error 908 in the response envelope.
|
|
28
|
+
* Use it to trigger a forced refresh + a single retry.
|
|
29
|
+
*/
|
|
30
|
+
declare function isExpiredTokenResponse(status: number, body?: unknown): boolean;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/server/oauth.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Per-app OAuth settings. Everything that differs between Shopwave apps lives
|
|
35
|
+
* here; the flow itself is identical for all of them.
|
|
36
|
+
*/
|
|
37
|
+
interface ShopwaveOAuthConfig {
|
|
38
|
+
/** e.g. `https://secure.merchantstack.com` (no trailing slash needed). */
|
|
39
|
+
authServerUrl: string;
|
|
40
|
+
clientId: string;
|
|
41
|
+
/** Server-side only. Never expose it to the browser. */
|
|
42
|
+
clientSecret: string;
|
|
43
|
+
/**
|
|
44
|
+
* The callback URL registered for this client on the auth server, e.g.
|
|
45
|
+
* `https://admin.example.com/auth`. Must match exactly.
|
|
46
|
+
*/
|
|
47
|
+
redirectUri: string;
|
|
48
|
+
/** Where the auth server sends the user after logout. Defaults to `redirectUri`. */
|
|
49
|
+
postLogoutRedirectUri?: string;
|
|
50
|
+
/** Defaults to `"application"`. */
|
|
51
|
+
scope?: string;
|
|
52
|
+
/** Defaults to `"online"`. */
|
|
53
|
+
accessType?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Body encoding for `POST /oauth/token`. Defaults to standard
|
|
56
|
+
* `application/x-www-form-urlencoded`; `"multipart"` sends `FormData`.
|
|
57
|
+
*/
|
|
58
|
+
tokenRequestFormat?: "urlencoded" | "multipart";
|
|
59
|
+
/** Auth-server paths. Defaults match Shopwave: `/login`, `/oauth/token`, `/logout`. */
|
|
60
|
+
endpoints?: {
|
|
61
|
+
login?: string;
|
|
62
|
+
token?: string;
|
|
63
|
+
logout?: string;
|
|
64
|
+
};
|
|
65
|
+
/** Custom fetch (tests, proxies). Defaults to global `fetch`. */
|
|
66
|
+
fetch?: typeof fetch;
|
|
67
|
+
}
|
|
68
|
+
interface ShopwaveOAuthClient {
|
|
69
|
+
readonly config: Readonly<ShopwaveOAuthConfig>;
|
|
70
|
+
/** URL to send the browser to so the user can sign in. */
|
|
71
|
+
buildLoginUrl(params?: {
|
|
72
|
+
state?: string;
|
|
73
|
+
}): string;
|
|
74
|
+
/** URL to send the browser to so the auth server ends its own session. */
|
|
75
|
+
buildLogoutUrl(params?: {
|
|
76
|
+
redirectUri?: string;
|
|
77
|
+
}): string;
|
|
78
|
+
/** Exchanges the `?code=` from the callback for tokens (server-to-server). */
|
|
79
|
+
exchangeCode(code: string): Promise<ShopwaveToken>;
|
|
80
|
+
/**
|
|
81
|
+
* Gets a fresh access token. Shopwave keeps the same refresh token, so the
|
|
82
|
+
* returned token carries the previous refresh token when none is sent back.
|
|
83
|
+
*/
|
|
84
|
+
refreshToken(token: ShopwaveToken | string): Promise<ShopwaveToken>;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/core/session.d.ts
|
|
88
|
+
/**
|
|
89
|
+
* Login status returned by the app's `/api/session` route. It deliberately
|
|
90
|
+
* never contains tokens — those stay in the encrypted httpOnly cookie.
|
|
91
|
+
*/
|
|
92
|
+
interface SessionStatus {
|
|
93
|
+
loggedIn: boolean;
|
|
94
|
+
/** Access-token expiry (epoch ms), when known. */
|
|
95
|
+
expiresAt?: number;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/server/errors.d.ts
|
|
99
|
+
/**
|
|
100
|
+
* Error thrown by the server-side auth helpers.
|
|
101
|
+
*
|
|
102
|
+
* `code` is stable and safe to branch on; `status` is the HTTP status returned
|
|
103
|
+
* by the Shopwave auth server when there was one.
|
|
104
|
+
*/
|
|
105
|
+
type ShopwaveAuthErrorCode = "config_invalid" | "token_exchange_failed" | "token_refresh_failed" | "token_response_invalid" | "network_error";
|
|
106
|
+
declare class ShopwaveAuthError extends Error {
|
|
107
|
+
readonly code: ShopwaveAuthErrorCode;
|
|
108
|
+
readonly status?: number;
|
|
109
|
+
/** Raw response body from the auth server, if any. Never contains our secret. */
|
|
110
|
+
readonly body?: string;
|
|
111
|
+
constructor(code: ShopwaveAuthErrorCode, message: string, details?: {
|
|
112
|
+
status?: number;
|
|
113
|
+
body?: string;
|
|
114
|
+
cause?: unknown;
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* True when the auth server rejected the grant itself (bad/expired code or
|
|
118
|
+
* refresh token) rather than failing for a transient reason. Callers should
|
|
119
|
+
* treat the user as logged out.
|
|
120
|
+
*/
|
|
121
|
+
get isInvalidGrant(): boolean;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/next/index.d.ts
|
|
125
|
+
interface ShopwaveSessionConfig {
|
|
126
|
+
/**
|
|
127
|
+
* Encrypts the session cookie. At least 32 characters. Pass a map such as
|
|
128
|
+
* `{ 2: newPassword, 1: oldPassword }` to rotate without logging users out.
|
|
129
|
+
*/
|
|
130
|
+
password: string | Record<string, string>;
|
|
131
|
+
/** Defaults to `"shopwave_session"`. Use a different name per app on a shared domain. */
|
|
132
|
+
cookieName?: string;
|
|
133
|
+
/** Session lifetime in seconds. Defaults to iron-session's 14 days. */
|
|
134
|
+
ttl?: number;
|
|
135
|
+
/** Defaults to `true` in production, `false` otherwise (so http://localhost works). */
|
|
136
|
+
secure?: boolean;
|
|
137
|
+
/** Defaults to `"lax"`, which the OAuth redirect back to your app needs. */
|
|
138
|
+
sameSite?: "lax" | "strict" | "none";
|
|
139
|
+
domain?: string;
|
|
140
|
+
}
|
|
141
|
+
interface ShopwaveAuthConfig extends ShopwaveOAuthConfig {
|
|
142
|
+
session: ShopwaveSessionConfig;
|
|
143
|
+
/**
|
|
144
|
+
* Path where `handlers.auth` is mounted. It starts the login AND receives the
|
|
145
|
+
* callback, so it should be the path of `redirectUri`. Defaults to `"/auth"`.
|
|
146
|
+
*/
|
|
147
|
+
authPath?: string;
|
|
148
|
+
/** Path where `handlers.logout` is mounted. Defaults to `${authPath}/logout`. */
|
|
149
|
+
logoutPath?: string;
|
|
150
|
+
/**
|
|
151
|
+
* Path where `handlers.session` is mounted. Always public, because it is how
|
|
152
|
+
* the browser finds out it is logged out. Defaults to `"/api/session"`.
|
|
153
|
+
*/
|
|
154
|
+
sessionPath?: string;
|
|
155
|
+
/** Where users land after login when no `returnTo` was given. Defaults to `"/"`. */
|
|
156
|
+
defaultReturnTo?: string;
|
|
157
|
+
/**
|
|
158
|
+
* Reject callbacks whose `state` doesn't match the one we sent (login-CSRF
|
|
159
|
+
* protection). Defaults to `true`. Only turn off if the auth server does not
|
|
160
|
+
* echo `state` back.
|
|
161
|
+
*/
|
|
162
|
+
requireState?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Refresh this many seconds before expiry. Defaults to `0`, because the
|
|
165
|
+
* Shopwave server only issues a new access token once the old one expired.
|
|
166
|
+
*/
|
|
167
|
+
refreshSkewSeconds?: number;
|
|
168
|
+
/** How long a started login stays valid, in seconds. Defaults to 600. */
|
|
169
|
+
loginTimeoutSeconds?: number;
|
|
170
|
+
}
|
|
171
|
+
/** What we keep in the encrypted cookie. Tokens never leave the server. */
|
|
172
|
+
interface ShopwaveSessionData {
|
|
173
|
+
token?: ShopwaveToken | Record<string, unknown>;
|
|
174
|
+
pendingLogin?: {
|
|
175
|
+
state: string;
|
|
176
|
+
returnTo: string;
|
|
177
|
+
createdAt: number;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
interface AuthContext {
|
|
181
|
+
accessToken: string;
|
|
182
|
+
/** Ready-to-use `Authorization` header value, e.g. `OAuth 111ad…`. */
|
|
183
|
+
authorization: string;
|
|
184
|
+
token: ShopwaveToken;
|
|
185
|
+
}
|
|
186
|
+
interface ProtectOptions {
|
|
187
|
+
/**
|
|
188
|
+
* Paths that don't need a login. A path matches itself and everything below
|
|
189
|
+
* it (`"/tools/tag-joiner"` covers `/tools/tag-joiner/x`). `"/"` matches only
|
|
190
|
+
* the root. The auth, logout and session paths are always public.
|
|
191
|
+
*/
|
|
192
|
+
publicPaths?: string[];
|
|
193
|
+
/** Paths that get a 401 JSON response instead of a login redirect. Defaults to `["/api"]`. */
|
|
194
|
+
apiPaths?: string[];
|
|
195
|
+
}
|
|
196
|
+
interface ShopwaveAuth {
|
|
197
|
+
/** The underlying framework-agnostic OAuth client. */
|
|
198
|
+
readonly oauth: ShopwaveOAuthClient;
|
|
199
|
+
/** Route handlers to export from your app. */
|
|
200
|
+
readonly handlers: {
|
|
201
|
+
/** Mount at the redirect-URI path (e.g. `app/auth/route.ts`): `export const GET = auth.handlers.auth`. */
|
|
202
|
+
auth: (request: Request) => Promise<Response>;
|
|
203
|
+
/** Mount at `app/auth/logout/route.ts`: `export const GET = auth.handlers.logout`. */
|
|
204
|
+
logout: (request: Request) => Promise<Response>;
|
|
205
|
+
/** Mount at `app/api/session/route.ts`: `export const { GET, DELETE } = auth.handlers.session`. */
|
|
206
|
+
session: {
|
|
207
|
+
GET: (request?: Request) => Promise<Response>;
|
|
208
|
+
DELETE: (request?: Request) => Promise<Response>;
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
/** Login status for the current request. Never includes tokens. */
|
|
212
|
+
getStatus(): Promise<SessionStatus>;
|
|
213
|
+
/**
|
|
214
|
+
* A valid access token for the current user, refreshing it when expired
|
|
215
|
+
* (and saving the new one in the cookie when called from a route handler,
|
|
216
|
+
* server action or proxy). `null` when logged out or the refresh token was
|
|
217
|
+
* rejected. Pass `forceRefresh` after the API answered 401 / error 908.
|
|
218
|
+
*/
|
|
219
|
+
getAccessToken(options?: {
|
|
220
|
+
forceRefresh?: boolean;
|
|
221
|
+
}): Promise<string | null>;
|
|
222
|
+
/** Same as `getAccessToken` but returns the whole token object. */
|
|
223
|
+
getToken(options?: {
|
|
224
|
+
forceRefresh?: boolean;
|
|
225
|
+
}): Promise<ShopwaveToken | null>;
|
|
226
|
+
/** `Authorization` header value (`"OAuth <token>"`), or `null` when logged out. */
|
|
227
|
+
getAuthorizationHeader(options?: {
|
|
228
|
+
forceRefresh?: boolean;
|
|
229
|
+
}): Promise<string | null>;
|
|
230
|
+
/** Wraps a route handler; responds 401 JSON when there's no valid session. */
|
|
231
|
+
withAuth<C = unknown>(handler: (request: Request, context: C, auth: AuthContext) => Response | Promise<Response>): (request: Request, context: C) => Promise<Response>;
|
|
232
|
+
/**
|
|
233
|
+
* For `proxy.ts` / `middleware.ts`. Returns a redirect (pages) or 401
|
|
234
|
+
* (API paths) when the request has no session, or `undefined` to continue.
|
|
235
|
+
* Only reads the cookie — it never calls the auth server.
|
|
236
|
+
*/
|
|
237
|
+
protect(request: Request, options?: ProtectOptions): Promise<Response | undefined>;
|
|
238
|
+
/** Cookie-only check usable anywhere you have the Request. */
|
|
239
|
+
isAuthenticated(request: Request): Promise<boolean>;
|
|
240
|
+
/** Link that starts the login and comes back to `returnTo`. */
|
|
241
|
+
loginPath(returnTo?: string): string;
|
|
242
|
+
/** Link that logs the user out of the app and the auth server. */
|
|
243
|
+
readonly logoutPath: string;
|
|
244
|
+
/** Raw iron-session for advanced use (e.g. storing app data alongside the token). */
|
|
245
|
+
getSession(): Promise<IronSession<ShopwaveSessionData>>;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Creates the Shopwave auth instance for one Next.js app. Call once at module
|
|
249
|
+
* scope (e.g. `lib/auth.ts`) and reuse. Config is validated on first use, so
|
|
250
|
+
* a missing env var doesn't fail `next build`.
|
|
251
|
+
*/
|
|
252
|
+
declare function createShopwaveAuth(config: ShopwaveAuthConfig): ShopwaveAuth;
|
|
253
|
+
//#endregion
|
|
254
|
+
export { AuthContext, ProtectOptions, type SessionStatus, ShopwaveAuth, ShopwaveAuthConfig, ShopwaveAuthError, ShopwaveSessionConfig, ShopwaveSessionData, type ShopwaveToken, authorizationHeader, createShopwaveAuth, isExpiredTokenResponse };
|
|
255
|
+
//# sourceMappingURL=index.d.ts.map
|