react-shopwave-connect 0.1.2 → 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.
- package/README.md +281 -39
- package/dist/core/index.cjs +435 -93
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +241 -28
- package/dist/core/index.d.ts +241 -28
- package/dist/core/index.js +405 -94
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +328 -116
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +57 -12
- package/dist/hooks/index.d.ts +57 -12
- package/dist/hooks/index.js +329 -118
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +553 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +265 -29
- package/dist/index.d.ts +265 -29
- package/dist/index.js +523 -114
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +1019 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +423 -0
- package/dist/next/index.d.ts +423 -0
- package/dist/next/index.js +987 -0
- package/dist/next/index.js.map +1 -0
- package/dist/server/index.cjs +670 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +346 -0
- package/dist/server/index.d.ts +346 -0
- package/dist/server/index.js +650 -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","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"],"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"],"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"],"mappings":"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"}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
//#region src/server/token.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Token model + helpers shared by every server-side integration.
|
|
4
|
+
*
|
|
5
|
+
* The Shopwave auth server returns (snake_case):
|
|
6
|
+
* { access_token, refresh_token, token_type: "OAuth", expires_in: 43200 }
|
|
7
|
+
*
|
|
8
|
+
* We store a normalised, camelCase shape with an absolute expiry so any
|
|
9
|
+
* request can decide whether the token is still usable without extra state.
|
|
10
|
+
*/
|
|
11
|
+
interface ShopwaveToken {
|
|
12
|
+
accessToken: string;
|
|
13
|
+
/** Long-lived; the Shopwave server does not rotate it on refresh. */
|
|
14
|
+
refreshToken?: string;
|
|
15
|
+
/** Scheme used in the Authorization header. Shopwave uses "OAuth". */
|
|
16
|
+
tokenType: string;
|
|
17
|
+
/** Absolute expiry, epoch milliseconds. Undefined if the server didn't say. */
|
|
18
|
+
expiresAt?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Raw token response from `POST {authServer}/oauth/token`. */
|
|
21
|
+
interface ShopwaveTokenResponse {
|
|
22
|
+
access_token?: string;
|
|
23
|
+
refresh_token?: string;
|
|
24
|
+
token_type?: string;
|
|
25
|
+
expires_in?: number | string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
/** Shopwave API error id meaning "access token expired / invalid". */
|
|
29
|
+
declare const SHOPWAVE_TOKEN_EXPIRED_ERROR_ID = 908;
|
|
30
|
+
/**
|
|
31
|
+
* Converts a token-endpoint response into a {@link ShopwaveToken}.
|
|
32
|
+
* Returns `null` when the response has no access token.
|
|
33
|
+
*
|
|
34
|
+
* @param previous the token being refreshed; its refresh token is kept when
|
|
35
|
+
* the server doesn't send a new one.
|
|
36
|
+
*/
|
|
37
|
+
declare function tokenFromResponse(response: ShopwaveTokenResponse, previous?: ShopwaveToken, now?: number): ShopwaveToken | null;
|
|
38
|
+
/**
|
|
39
|
+
* Reads a token from session storage. Accepts both the current shape and the
|
|
40
|
+
* legacy raw response that older apps (e.g. AdminUI ≤ 0.1) stored directly in
|
|
41
|
+
* the session, so existing sessions survive an upgrade.
|
|
42
|
+
*/
|
|
43
|
+
declare function normalizeStoredToken(raw: unknown): ShopwaveToken | null;
|
|
44
|
+
/**
|
|
45
|
+
* True when the token has passed its expiry (minus `skewSeconds`).
|
|
46
|
+
* Tokens without a known expiry are treated as valid.
|
|
47
|
+
*
|
|
48
|
+
* Shopwave only issues a new access token once the old one has actually
|
|
49
|
+
* expired, so the default skew is 0 — refreshing early just returns the same
|
|
50
|
+
* token.
|
|
51
|
+
*/
|
|
52
|
+
declare function isTokenExpired(token: ShopwaveToken, {
|
|
53
|
+
skewSeconds,
|
|
54
|
+
now
|
|
55
|
+
}?: {
|
|
56
|
+
skewSeconds?: number;
|
|
57
|
+
now?: number;
|
|
58
|
+
}): boolean;
|
|
59
|
+
/** `Authorization` header value, e.g. `OAuth 111ad…`. */
|
|
60
|
+
declare function authorizationHeader(token: ShopwaveToken): string;
|
|
61
|
+
/**
|
|
62
|
+
* True when a Shopwave API response means "your access token is no longer
|
|
63
|
+
* valid": HTTP 401, or the API error 908 in the response envelope.
|
|
64
|
+
* Use it to trigger a forced refresh + a single retry.
|
|
65
|
+
*/
|
|
66
|
+
declare function isExpiredTokenResponse(status: number, body?: unknown): boolean;
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/server/oauth.d.ts
|
|
69
|
+
/**
|
|
70
|
+
* Per-app OAuth settings. Everything that differs between Shopwave apps lives
|
|
71
|
+
* here; the flow itself is identical for all of them.
|
|
72
|
+
*/
|
|
73
|
+
interface ShopwaveOAuthConfig {
|
|
74
|
+
/** e.g. `https://secure.merchantstack.com` (no trailing slash needed). */
|
|
75
|
+
authServerUrl: string;
|
|
76
|
+
clientId: string;
|
|
77
|
+
/** Server-side only. Never expose it to the browser. */
|
|
78
|
+
clientSecret: string;
|
|
79
|
+
/**
|
|
80
|
+
* The callback URL registered for this client on the auth server, e.g.
|
|
81
|
+
* `https://admin.example.com/auth`. Must match exactly.
|
|
82
|
+
*/
|
|
83
|
+
redirectUri: string;
|
|
84
|
+
/** Where the auth server sends the user after logout. Defaults to `redirectUri`. */
|
|
85
|
+
postLogoutRedirectUri?: string;
|
|
86
|
+
/** Defaults to `"application"`. */
|
|
87
|
+
scope?: string;
|
|
88
|
+
/** Defaults to `"online"`. */
|
|
89
|
+
accessType?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Body encoding for `POST /oauth/token`. Defaults to standard
|
|
92
|
+
* `application/x-www-form-urlencoded`; `"multipart"` sends `FormData`.
|
|
93
|
+
*/
|
|
94
|
+
tokenRequestFormat?: "urlencoded" | "multipart";
|
|
95
|
+
/** Auth-server paths. Defaults match Shopwave: `/login`, `/oauth/token`, `/logout`. */
|
|
96
|
+
endpoints?: {
|
|
97
|
+
login?: string;
|
|
98
|
+
token?: string;
|
|
99
|
+
logout?: string;
|
|
100
|
+
};
|
|
101
|
+
/** Custom fetch (tests, proxies). Defaults to global `fetch`. */
|
|
102
|
+
fetch?: typeof fetch;
|
|
103
|
+
}
|
|
104
|
+
interface ShopwaveOAuthClient {
|
|
105
|
+
readonly config: Readonly<ShopwaveOAuthConfig>;
|
|
106
|
+
/** URL to send the browser to so the user can sign in. */
|
|
107
|
+
buildLoginUrl(params?: {
|
|
108
|
+
state?: string;
|
|
109
|
+
}): string;
|
|
110
|
+
/** URL to send the browser to so the auth server ends its own session. */
|
|
111
|
+
buildLogoutUrl(params?: {
|
|
112
|
+
redirectUri?: string;
|
|
113
|
+
}): string;
|
|
114
|
+
/** Exchanges the `?code=` from the callback for tokens (server-to-server). */
|
|
115
|
+
exchangeCode(code: string): Promise<ShopwaveToken>;
|
|
116
|
+
/**
|
|
117
|
+
* Gets a fresh access token. Shopwave keeps the same refresh token, so the
|
|
118
|
+
* returned token carries the previous refresh token when none is sent back.
|
|
119
|
+
*/
|
|
120
|
+
refreshToken(token: ShopwaveToken | string): Promise<ShopwaveToken>;
|
|
121
|
+
}
|
|
122
|
+
/** Throws a readable error when required settings are missing. */
|
|
123
|
+
declare function assertOAuthConfig(config: Partial<ShopwaveOAuthConfig>): asserts config is ShopwaveOAuthConfig;
|
|
124
|
+
/**
|
|
125
|
+
* Creates a framework-agnostic Shopwave OAuth client (authorization-code flow
|
|
126
|
+
* with a client secret — the Shopwave auth server does not support PKCE, so
|
|
127
|
+
* this must run on a server).
|
|
128
|
+
*
|
|
129
|
+
* Config is validated lazily on first use, so creating the client at module
|
|
130
|
+
* scope doesn't break builds where env vars aren't present.
|
|
131
|
+
*/
|
|
132
|
+
declare function createShopwaveOAuth(config: ShopwaveOAuthConfig): ShopwaveOAuthClient;
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/server/returnTo.d.ts
|
|
135
|
+
/**
|
|
136
|
+
* Helpers for the round trip through the auth server.
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* Only allows same-origin relative paths ("/products?tab=1"). Anything that
|
|
140
|
+
* could send the user to another site after login — absolute URLs,
|
|
141
|
+
* protocol-relative "//evil.com", backslash tricks, control characters —
|
|
142
|
+
* falls back to `fallback`.
|
|
143
|
+
*/
|
|
144
|
+
declare function sanitizeReturnTo(value: unknown, fallback?: string): string;
|
|
145
|
+
/** Random, URL-safe value for the OAuth `state` parameter (128 bits, hex). */
|
|
146
|
+
declare function createState(): string;
|
|
147
|
+
/** Constant-time string comparison for state values. */
|
|
148
|
+
declare function safeEqual(a: string, b: string): boolean;
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/server/errors.d.ts
|
|
151
|
+
/**
|
|
152
|
+
* Error thrown by the server-side auth helpers.
|
|
153
|
+
*
|
|
154
|
+
* `code` is stable and safe to branch on; `status` is the HTTP status returned
|
|
155
|
+
* by the Shopwave auth server when there was one.
|
|
156
|
+
*/
|
|
157
|
+
type ShopwaveAuthErrorCode = "config_invalid" | "token_exchange_failed" | "token_refresh_failed" | "token_response_invalid" | "network_error";
|
|
158
|
+
declare class ShopwaveAuthError extends Error {
|
|
159
|
+
readonly code: ShopwaveAuthErrorCode;
|
|
160
|
+
readonly status?: number;
|
|
161
|
+
/** Raw response body from the auth server, if any. Never contains our secret. */
|
|
162
|
+
readonly body?: string;
|
|
163
|
+
constructor(code: ShopwaveAuthErrorCode, message: string, details?: {
|
|
164
|
+
status?: number;
|
|
165
|
+
body?: string;
|
|
166
|
+
cause?: unknown;
|
|
167
|
+
});
|
|
168
|
+
/**
|
|
169
|
+
* True when the auth server rejected the grant itself (bad/expired code or
|
|
170
|
+
* refresh token) rather than failing for a transient reason. Callers should
|
|
171
|
+
* treat the user as logged out.
|
|
172
|
+
*/
|
|
173
|
+
get isInvalidGrant(): boolean;
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/core/entities.d.ts
|
|
177
|
+
/**
|
|
178
|
+
* How each Shopwave entity is addressed, shared by the client functions below
|
|
179
|
+
* and the server route handlers in `react-shopwave-connect/next`, so the two
|
|
180
|
+
* halves of the contract can't drift apart.
|
|
181
|
+
*/
|
|
182
|
+
interface EntityDefinition {
|
|
183
|
+
/** App route segment: `/api/<route>` and `/api/<route>/<id>`. */
|
|
184
|
+
route: string;
|
|
185
|
+
/** Key of the entity map in requests and responses (`{ products: { … } }`). */
|
|
186
|
+
collection: string;
|
|
187
|
+
/** Shopwave API path (`GET/POST/DELETE {apiUrl}/<upstream>`). */
|
|
188
|
+
upstream: string;
|
|
189
|
+
/** Header that filters reads by id (`productIds: 1,2`). */
|
|
190
|
+
idsHeader: string;
|
|
191
|
+
/** Header that names the record to delete (`productId: 1`). */
|
|
192
|
+
idHeader: string;
|
|
193
|
+
/** False for read-only entities (the API has no POST for them). */
|
|
194
|
+
writable: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* How a delete is done upstream:
|
|
197
|
+
* - `"delete"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)
|
|
198
|
+
* - `"retire"`: the API has no DELETE; the record is retired by POSTing
|
|
199
|
+
* `{ id, [retireField]: <now> }` (employees: `exitDate`, promotions: `endDate`)
|
|
200
|
+
* - `"none"`: can't be deleted
|
|
201
|
+
*/
|
|
202
|
+
deleteMode: "delete" | "retire" | "none";
|
|
203
|
+
/** Date field set to "now" when `deleteMode` is `"retire"`. */
|
|
204
|
+
retireField?: string;
|
|
205
|
+
}
|
|
206
|
+
type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
|
|
207
|
+
/** Entities that can be created/updated through the API. */
|
|
208
|
+
type WritableEntityKind = Exclude<EntityKind, "consumer">;
|
|
209
|
+
/** Entities that can be deleted (or retired) through the API. */
|
|
210
|
+
type DeletableEntityKind = Exclude<EntityKind, "consumer">;
|
|
211
|
+
/**
|
|
212
|
+
* Addressing per entity, following the Shopwave API reference
|
|
213
|
+
* (https://developer.merchantstack.com/api-reference.html) and checked against
|
|
214
|
+
* the live API where noted:
|
|
215
|
+
* - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
|
|
216
|
+
* - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
|
|
217
|
+
* - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
|
|
218
|
+
* changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
|
|
219
|
+
* - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
|
|
220
|
+
*/
|
|
221
|
+
declare const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>;
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/server/api.d.ts
|
|
224
|
+
/**
|
|
225
|
+
* The OAuth token a caller sent with the request, without its scheme, or `null`.
|
|
226
|
+
*
|
|
227
|
+
* Reads, in order: `Authorization: OAuth|Bearer <token>` (what the SDK sends
|
|
228
|
+
* from 0.3), the `token` header (SDK ≤ 0.2 writes), and `extras.token`
|
|
229
|
+
* (SDK ≤ 0.2 reads).
|
|
230
|
+
*/
|
|
231
|
+
declare function readRequestToken(request: Request): string | null;
|
|
232
|
+
/**
|
|
233
|
+
* `extras` keys that are never forwarded upstream (case-insensitive).
|
|
234
|
+
* `Content-Type` is allowed (SDK clients have always sent `application/json`
|
|
235
|
+
* on reads) but is replaced by the form content type on writes.
|
|
236
|
+
*/
|
|
237
|
+
declare const BLOCKED_EXTRAS: ReadonlySet<string>;
|
|
238
|
+
/**
|
|
239
|
+
* Parses the `extras` request header into upstream headers.
|
|
240
|
+
* Returns `null` when the header isn't a JSON object.
|
|
241
|
+
*/
|
|
242
|
+
declare function parseExtras(raw: string | null, blocked?: ReadonlySet<string>): Record<string, string> | null;
|
|
243
|
+
declare class RequestBodyError extends Error {}
|
|
244
|
+
/**
|
|
245
|
+
* Normalises a write request body into the Shopwave envelope
|
|
246
|
+
* `{ <collection>: { <ref>: entity, … } }`.
|
|
247
|
+
*
|
|
248
|
+
* Accepts:
|
|
249
|
+
* - the SDK ≥ 0.3 shape `{ <collection>: { "0": {...}, "1": {...} } }` (refs kept),
|
|
250
|
+
* - the older AdminUI shapes `{ <collection>: { new: {...} } }` / `{ updated: {...} }`,
|
|
251
|
+
* - a bare entity object `{ title: … }`.
|
|
252
|
+
*
|
|
253
|
+
* With `forcedId` (item routes) exactly one entity is allowed and its `id` is
|
|
254
|
+
* set from the URL.
|
|
255
|
+
*/
|
|
256
|
+
declare function normalizeWriteBody(def: Pick<EntityDefinition, "collection">, body: unknown, forcedId?: string): Record<string, Record<string, Record<string, unknown>>>;
|
|
257
|
+
interface ShopwaveApiConfig {
|
|
258
|
+
/** Shopwave API base URL, e.g. `process.env.SHOPWAVE_API_SERVER_URL`. */
|
|
259
|
+
apiUrl: string;
|
|
260
|
+
/**
|
|
261
|
+
* `Authorization` header value for the logged-in user (`"OAuth <token>"`),
|
|
262
|
+
* or `null` when logged out. Called again with `forceRefresh: true` when the
|
|
263
|
+
* API reports the token expired.
|
|
264
|
+
*/
|
|
265
|
+
getAuthorization?: (options?: {
|
|
266
|
+
forceRefresh?: boolean;
|
|
267
|
+
}) => Promise<string | null>;
|
|
268
|
+
/**
|
|
269
|
+
* Accept a token sent by the caller (`Authorization: OAuth <token>`, or the
|
|
270
|
+
* legacy `token` header / `extras.token`) instead of the session. The token
|
|
271
|
+
* is only forwarded to the Shopwave API, which validates it. Defaults to `true`
|
|
272
|
+
* (SDK integration tests, scripts and server-to-server calls rely on it).
|
|
273
|
+
*/
|
|
274
|
+
allowRequestToken?: boolean;
|
|
275
|
+
/** `x-accept-version` sent upstream. Defaults to `"2.0"`. */
|
|
276
|
+
apiVersion?: string;
|
|
277
|
+
/** Custom fetch for the upstream call. */
|
|
278
|
+
fetch?: typeof fetch;
|
|
279
|
+
/** Override how an entity is addressed (e.g. a different id header). */
|
|
280
|
+
entities?: Partial<Record<EntityKind, Partial<EntityDefinition>>>;
|
|
281
|
+
/** Extra `extras` keys to drop, on top of {@link BLOCKED_EXTRAS}. */
|
|
282
|
+
blockedExtras?: string[];
|
|
283
|
+
/**
|
|
284
|
+
* Called when the upstream call throws (network error). Defaults to a
|
|
285
|
+
* `console.error` with the method and path only — never headers or tokens.
|
|
286
|
+
*/
|
|
287
|
+
onError?: (error: unknown, context: {
|
|
288
|
+
method: string;
|
|
289
|
+
path: string;
|
|
290
|
+
}) => void;
|
|
291
|
+
}
|
|
292
|
+
interface ForwardInit {
|
|
293
|
+
/** HTTP method sent upstream. Defaults to `GET`. */
|
|
294
|
+
method?: string;
|
|
295
|
+
/** Shopwave API path, e.g. `"product"`. */
|
|
296
|
+
path: string;
|
|
297
|
+
/** Extra upstream headers; they win over `extras`. */
|
|
298
|
+
headers?: Record<string, string>;
|
|
299
|
+
/** Sent as the form field `postBody=<JSON>`. */
|
|
300
|
+
postBody?: unknown;
|
|
301
|
+
/** Forward the caller's `extras` header as upstream headers. Defaults to `true`. */
|
|
302
|
+
forwardExtras?: boolean;
|
|
303
|
+
}
|
|
304
|
+
/** Second argument Next.js (and similar) pass to dynamic route handlers. */
|
|
305
|
+
interface RouteContext {
|
|
306
|
+
params?: Promise<Record<string, string | string[] | undefined>> | Record<string, string | string[] | undefined>;
|
|
307
|
+
}
|
|
308
|
+
type RouteHandler = (request: Request, context?: RouteContext) => Promise<Response>;
|
|
309
|
+
interface CollectionHandlers {
|
|
310
|
+
/** List/filter via `extras` (e.g. `productIds`, `deleted`). */
|
|
311
|
+
GET: RouteHandler;
|
|
312
|
+
/** Create or update (Shopwave upserts: an `id` means update). */
|
|
313
|
+
POST: RouteHandler;
|
|
314
|
+
/** Same as POST (kept for older clients). */
|
|
315
|
+
PUT: RouteHandler;
|
|
316
|
+
}
|
|
317
|
+
interface ItemHandlers {
|
|
318
|
+
/** Read one record by the `[id]` route param. */
|
|
319
|
+
GET: RouteHandler;
|
|
320
|
+
/** Update the record at `[id]` (the id in the URL wins). */
|
|
321
|
+
PUT: RouteHandler;
|
|
322
|
+
/** Delete the record at `[id]`. Shopwave answers 205 with an empty body. */
|
|
323
|
+
DELETE: RouteHandler;
|
|
324
|
+
}
|
|
325
|
+
interface ShopwaveApiHandlers {
|
|
326
|
+
/** Calls the Shopwave API for this request (token, extras, refresh-retry) and returns its answer. */
|
|
327
|
+
forward(request: Request, init: ForwardInit): Promise<Response>;
|
|
328
|
+
/** Handlers for `app/api/<route>/route.ts`. */
|
|
329
|
+
collection(kind: EntityKind): CollectionHandlers;
|
|
330
|
+
/** Handlers for `app/api/<route>/[id]/route.ts`. */
|
|
331
|
+
item(kind: EntityKind, options?: {
|
|
332
|
+
param?: string;
|
|
333
|
+
}): ItemHandlers;
|
|
334
|
+
/** GET-only proxy for a Shopwave path (e.g. `"report"`, `"user"`, `"merchant"`). */
|
|
335
|
+
passthrough(path: string): {
|
|
336
|
+
GET: RouteHandler;
|
|
337
|
+
};
|
|
338
|
+
/** The entity table in use (defaults merged with `config.entities`). */
|
|
339
|
+
entity(kind: EntityKind): EntityDefinition;
|
|
340
|
+
}
|
|
341
|
+
/** `YYYY-MM-DD HH:mm:ss` in UTC, the datetime format Shopwave writes use (e.g. promotions). */
|
|
342
|
+
declare function shopwaveDateTime(date?: Date): string;
|
|
343
|
+
declare function createShopwaveApiHandlers(config: ShopwaveApiConfig): ShopwaveApiHandlers;
|
|
344
|
+
//#endregion
|
|
345
|
+
export { BLOCKED_EXTRAS, type CollectionHandlers, type DeletableEntityKind, type EntityDefinition, type EntityKind, type ForwardInit, type ItemHandlers, RequestBodyError, type RouteContext, type RouteHandler, SHOPWAVE_ENTITIES, SHOPWAVE_TOKEN_EXPIRED_ERROR_ID, type ShopwaveApiConfig, type ShopwaveApiHandlers, ShopwaveAuthError, type ShopwaveAuthErrorCode, type ShopwaveOAuthClient, type ShopwaveOAuthConfig, type ShopwaveToken, type ShopwaveTokenResponse, type WritableEntityKind, assertOAuthConfig, authorizationHeader, createShopwaveApiHandlers, createShopwaveOAuth, createState, isExpiredTokenResponse, isTokenExpired, normalizeStoredToken, normalizeWriteBody, parseExtras, readRequestToken, safeEqual, sanitizeReturnTo, shopwaveDateTime, tokenFromResponse };
|
|
346
|
+
//# sourceMappingURL=index.d.cts.map
|