react-shopwave-connect 0.4.6 → 0.4.7
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 +11 -3
- package/dist/core/index.cjs +274 -130
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +292 -145
- package/dist/core/index.d.ts +292 -145
- package/dist/core/index.js +269 -131
- package/dist/core/index.js.map +1 -1
- package/dist/hooks/index.cjs +265 -112
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +90 -23
- package/dist/hooks/index.d.ts +90 -23
- package/dist/hooks/index.js +265 -113
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +294 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +301 -145
- package/dist/index.d.ts +301 -145
- package/dist/index.js +288 -131
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +67 -0
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +33 -2
- package/dist/next/index.d.ts +33 -2
- package/dist/next/index.js +67 -0
- package/dist/next/index.js.map +1 -1
- package/dist/server/index.cjs +68 -0
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +40 -3
- package/dist/server/index.d.ts +40 -3
- package/dist/server/index.js +68 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.d.ts
CHANGED
|
@@ -215,8 +215,15 @@ interface EntityDefinition {
|
|
|
215
215
|
* start = end and the record is expired without ever running.
|
|
216
216
|
*/
|
|
217
217
|
retireStartField?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Header naming child records to delete instead of the whole record
|
|
220
|
+
* (`productComponentInstanceIds: 1,2`). Only `item()` DELETE uses it, and only when
|
|
221
|
+
* the caller's `extras` carry this one key (a non-empty list of ids); every other
|
|
222
|
+
* `extras` key is still dropped. Only for `deleteMode: "delete"`.
|
|
223
|
+
*/
|
|
224
|
+
childIdsHeader?: string;
|
|
218
225
|
}
|
|
219
|
-
type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
|
|
226
|
+
type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer" | "productComponent";
|
|
220
227
|
/** Entities that can be created/updated through the API. */
|
|
221
228
|
type WritableEntityKind = Exclude<EntityKind, "consumer">;
|
|
222
229
|
/** Entities that can be deleted (or retired) through the API. */
|
|
@@ -233,6 +240,27 @@ type DeletableEntityKind = Exclude<EntityKind, "consumer">;
|
|
|
233
240
|
* - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
|
|
234
241
|
* changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
|
|
235
242
|
* - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
|
|
243
|
+
* - productComponent: modifier groups, `/product/component` (live, staging, 11 Nov 2026). The
|
|
244
|
+
* API reference lists no read filters; these are what the live API does:
|
|
245
|
+
* - Reads filter by `componentIds` (group ids) or `productIds` (the parent product; option
|
|
246
|
+
* products don't match). `productComponentIds` is ignored on reads: every group comes back.
|
|
247
|
+
* `deleted: false` gives live groups only, `true` deleted groups only, no header both.
|
|
248
|
+
* No match → 204 with an empty body. No order, sort, priority or position field anywhere.
|
|
249
|
+
* - Groups are keyed by id; `products` (option products) by product id; `productInstances`
|
|
250
|
+
* (option rows) by row id. Reads reformat values: price `"100"` → `"100.0"`,
|
|
251
|
+
* vatPercentage `"0.2000"` → `"0.200"`. `tags` and `name` are stored as sent (no trimming;
|
|
252
|
+
* a `tags` array is joined with commas; `tags` null or left out → null).
|
|
253
|
+
* - POST upserts (201, echoed under the ref). For each option product in `products`, its rows
|
|
254
|
+
* are replaced: every row sent is created afresh with a new id (a row `id` is ignored, also
|
|
255
|
+
* on a price change) and rows left out are removed. Option products left out of the save
|
|
256
|
+
* are untouched, and an empty `productInstances` removes nothing. A save without `products`
|
|
257
|
+
* updates only the group's own fields. The echo isn't proof of what was stored: renames of
|
|
258
|
+
* groups created moments earlier by a test run echoed the new name but never stuck (re-read
|
|
259
|
+
* for 90 s), while older groups, and groups created in Shopwave's own admin, renamed at
|
|
260
|
+
* once. Cause not yet known.
|
|
261
|
+
* - DELETE with `productComponentIds` (plural, comma list) → 205, soft delete. Adding
|
|
262
|
+
* `productComponentInstanceIds` deletes just those rows (205); they stay in live reads with
|
|
263
|
+
* `deleteDate` set. The singular `productComponentId` hangs (504 after 60 s).
|
|
236
264
|
*/
|
|
237
265
|
declare const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>;
|
|
238
266
|
//#endregion
|
|
@@ -438,7 +466,10 @@ interface ItemHandlers {
|
|
|
438
466
|
GET: RouteHandler;
|
|
439
467
|
/** Update the record at `[id]` (the id in the URL wins). */
|
|
440
468
|
PUT: RouteHandler;
|
|
441
|
-
/**
|
|
469
|
+
/**
|
|
470
|
+
* Delete the record at `[id]`. Shopwave answers 205 with an empty body. For an entity with
|
|
471
|
+
* `childIdsHeader`, `extras` carrying that key deletes just those child rows instead.
|
|
472
|
+
*/
|
|
442
473
|
DELETE: RouteHandler;
|
|
443
474
|
}
|
|
444
475
|
interface ResourceHandlers {
|
|
@@ -502,7 +533,13 @@ interface ShopwaveApiHandlers {
|
|
|
502
533
|
}
|
|
503
534
|
/** `YYYY-MM-DD HH:mm:ss` in UTC, the datetime format Shopwave writes use (e.g. promotions). */
|
|
504
535
|
declare function shopwaveDateTime(date?: Date): string;
|
|
536
|
+
/**
|
|
537
|
+
* The child ids a DELETE names in `extras` under `header` (case-insensitive), as one comma
|
|
538
|
+
* list: `undefined` when the key is absent, `null` when `extras` or the value is invalid (not a
|
|
539
|
+
* JSON object, or not a non-empty list of ids). The key may hold an array or a comma string.
|
|
540
|
+
*/
|
|
541
|
+
declare function readChildIds(raw: string | null, header: string): string | undefined | null;
|
|
505
542
|
declare function createShopwaveApiHandlers(config: ShopwaveApiConfig): ShopwaveApiHandlers;
|
|
506
543
|
//#endregion
|
|
507
|
-
export { BLOCKED_EXTRAS, type CollectionHandlers, type DeletableEntityKind, type EntityDefinition, type EntityKind, type ForwardInit, type ItemHandlers, type LookupDefinition, type LookupKind, RequestBodyError, type ResourceDefinition, type ResourceHandlers, type ResourceKind, type RouteContext, type RouteHandler, SHOPWAVE_ENTITIES, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_TOKEN_EXPIRED_ERROR_ID, SHOPWAVE_UPLOAD, type ShopwaveApiConfig, type ShopwaveApiHandlers, ShopwaveAuthError, type ShopwaveAuthErrorCode, type ShopwaveOAuthClient, type ShopwaveOAuthConfig, type ShopwaveToken, type ShopwaveTokenResponse, UPLOAD_KINDS, UPLOAD_PREFIXES, type UploadHandlers, type UploadKind, type UploadOptions, type UploadPrefix, type WritableEntityKind, assertOAuthConfig, authorizationHeader, createShopwaveApiHandlers, createShopwaveOAuth, createState, isExpiredTokenResponse, isTokenExpired, normalizeStoredToken, normalizeWriteBody, parseExtras, readRequestToken, safeEqual, sanitizeReturnTo, shopwaveDateTime, tokenFromResponse };
|
|
544
|
+
export { BLOCKED_EXTRAS, type CollectionHandlers, type DeletableEntityKind, type EntityDefinition, type EntityKind, type ForwardInit, type ItemHandlers, type LookupDefinition, type LookupKind, RequestBodyError, type ResourceDefinition, type ResourceHandlers, type ResourceKind, type RouteContext, type RouteHandler, SHOPWAVE_ENTITIES, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_TOKEN_EXPIRED_ERROR_ID, SHOPWAVE_UPLOAD, type ShopwaveApiConfig, type ShopwaveApiHandlers, ShopwaveAuthError, type ShopwaveAuthErrorCode, type ShopwaveOAuthClient, type ShopwaveOAuthConfig, type ShopwaveToken, type ShopwaveTokenResponse, UPLOAD_KINDS, UPLOAD_PREFIXES, type UploadHandlers, type UploadKind, type UploadOptions, type UploadPrefix, type WritableEntityKind, assertOAuthConfig, authorizationHeader, createShopwaveApiHandlers, createShopwaveOAuth, createState, isExpiredTokenResponse, isTokenExpired, normalizeStoredToken, normalizeWriteBody, parseExtras, readChildIds, readRequestToken, safeEqual, sanitizeReturnTo, shopwaveDateTime, tokenFromResponse };
|
|
508
545
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/server/index.js
CHANGED
|
@@ -326,6 +326,27 @@ const SHOPWAVE_LOOKUPS = Object.freeze({
|
|
|
326
326
|
* - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
|
|
327
327
|
* changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
|
|
328
328
|
* - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
|
|
329
|
+
* - productComponent: modifier groups, `/product/component` (live, staging, 11 Nov 2026). The
|
|
330
|
+
* API reference lists no read filters; these are what the live API does:
|
|
331
|
+
* - Reads filter by `componentIds` (group ids) or `productIds` (the parent product; option
|
|
332
|
+
* products don't match). `productComponentIds` is ignored on reads: every group comes back.
|
|
333
|
+
* `deleted: false` gives live groups only, `true` deleted groups only, no header both.
|
|
334
|
+
* No match → 204 with an empty body. No order, sort, priority or position field anywhere.
|
|
335
|
+
* - Groups are keyed by id; `products` (option products) by product id; `productInstances`
|
|
336
|
+
* (option rows) by row id. Reads reformat values: price `"100"` → `"100.0"`,
|
|
337
|
+
* vatPercentage `"0.2000"` → `"0.200"`. `tags` and `name` are stored as sent (no trimming;
|
|
338
|
+
* a `tags` array is joined with commas; `tags` null or left out → null).
|
|
339
|
+
* - POST upserts (201, echoed under the ref). For each option product in `products`, its rows
|
|
340
|
+
* are replaced: every row sent is created afresh with a new id (a row `id` is ignored, also
|
|
341
|
+
* on a price change) and rows left out are removed. Option products left out of the save
|
|
342
|
+
* are untouched, and an empty `productInstances` removes nothing. A save without `products`
|
|
343
|
+
* updates only the group's own fields. The echo isn't proof of what was stored: renames of
|
|
344
|
+
* groups created moments earlier by a test run echoed the new name but never stuck (re-read
|
|
345
|
+
* for 90 s), while older groups, and groups created in Shopwave's own admin, renamed at
|
|
346
|
+
* once. Cause not yet known.
|
|
347
|
+
* - DELETE with `productComponentIds` (plural, comma list) → 205, soft delete. Adding
|
|
348
|
+
* `productComponentInstanceIds` deletes just those rows (205); they stay in live reads with
|
|
349
|
+
* `deleteDate` set. The singular `productComponentId` hangs (504 after 60 s).
|
|
329
350
|
*/
|
|
330
351
|
const SHOPWAVE_ENTITIES = Object.freeze({
|
|
331
352
|
product: {
|
|
@@ -385,6 +406,16 @@ const SHOPWAVE_ENTITIES = Object.freeze({
|
|
|
385
406
|
idHeader: "consumerId",
|
|
386
407
|
writable: false,
|
|
387
408
|
deleteMode: "none"
|
|
409
|
+
},
|
|
410
|
+
productComponent: {
|
|
411
|
+
route: "product-components",
|
|
412
|
+
collection: "productComponents",
|
|
413
|
+
upstream: "product/component",
|
|
414
|
+
idsHeader: "componentIds",
|
|
415
|
+
idHeader: "productComponentIds",
|
|
416
|
+
writable: true,
|
|
417
|
+
deleteMode: "delete",
|
|
418
|
+
childIdsHeader: "productComponentInstanceIds"
|
|
388
419
|
}
|
|
389
420
|
});
|
|
390
421
|
|
|
@@ -558,6 +589,29 @@ async function readUpstream(response) {
|
|
|
558
589
|
};
|
|
559
590
|
}
|
|
560
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* The child ids a DELETE names in `extras` under `header` (case-insensitive), as one comma
|
|
594
|
+
* list: `undefined` when the key is absent, `null` when `extras` or the value is invalid (not a
|
|
595
|
+
* JSON object, or not a non-empty list of ids). The key may hold an array or a comma string.
|
|
596
|
+
*/
|
|
597
|
+
function readChildIds(raw, header) {
|
|
598
|
+
if (!raw) return void 0;
|
|
599
|
+
let parsed;
|
|
600
|
+
try {
|
|
601
|
+
parsed = JSON.parse(raw);
|
|
602
|
+
} catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
if (!isPlainObject(parsed)) return null;
|
|
606
|
+
const key = Object.keys(parsed).find((k) => k.toLowerCase() === header.toLowerCase());
|
|
607
|
+
if (key === void 0) return void 0;
|
|
608
|
+
const value = parsed[key];
|
|
609
|
+
const list = Array.isArray(value) ? value : typeof value === "string" || typeof value === "number" ? String(value).split(",") : null;
|
|
610
|
+
if (!list) return null;
|
|
611
|
+
const ids = list.map((v) => typeof v === "string" || typeof v === "number" ? String(v).trim() : "");
|
|
612
|
+
if (ids.length === 0 || ids.some((id) => !ID_PATTERN.test(id))) return null;
|
|
613
|
+
return ids.join(",");
|
|
614
|
+
}
|
|
561
615
|
async function routeParam(context, name) {
|
|
562
616
|
const value = (await context?.params)?.[name];
|
|
563
617
|
const id = Array.isArray(value) ? value[0] : value;
|
|
@@ -808,6 +862,19 @@ function createShopwaveApiHandlers(config) {
|
|
|
808
862
|
const id = await routeParam(context, param);
|
|
809
863
|
if (!id) return badId();
|
|
810
864
|
if (def.deleteMode === "none") return notSupported(def, "deletes");
|
|
865
|
+
if (def.deleteMode === "delete" && def.childIdsHeader) {
|
|
866
|
+
const childIds = readChildIds(request.headers.get("extras"), def.childIdsHeader);
|
|
867
|
+
if (childIds === null) return jsonError(400, "invalid_extras", `"${def.childIdsHeader}" must be a non-empty list of ids`);
|
|
868
|
+
if (childIds !== void 0) return forward(request, {
|
|
869
|
+
method: "DELETE",
|
|
870
|
+
path: def.upstream,
|
|
871
|
+
headers: {
|
|
872
|
+
[def.idHeader]: id,
|
|
873
|
+
[def.childIdsHeader]: childIds
|
|
874
|
+
},
|
|
875
|
+
forwardExtras: false
|
|
876
|
+
});
|
|
877
|
+
}
|
|
811
878
|
if (def.deleteMode === "retire") {
|
|
812
879
|
const field = def.retireField ?? "exitDate";
|
|
813
880
|
if (def.retireWithRecord) return retireWithRecord(request, def, id, field);
|
|
@@ -857,5 +924,5 @@ function createShopwaveApiHandlers(config) {
|
|
|
857
924
|
}
|
|
858
925
|
|
|
859
926
|
//#endregion
|
|
860
|
-
export { BLOCKED_EXTRAS, RequestBodyError, SHOPWAVE_ENTITIES, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_TOKEN_EXPIRED_ERROR_ID, SHOPWAVE_UPLOAD, ShopwaveAuthError, UPLOAD_KINDS, UPLOAD_PREFIXES, assertOAuthConfig, authorizationHeader, createShopwaveApiHandlers, createShopwaveOAuth, createState, isExpiredTokenResponse, isTokenExpired, normalizeStoredToken, normalizeWriteBody, parseExtras, readRequestToken, safeEqual, sanitizeReturnTo, shopwaveDateTime, tokenFromResponse };
|
|
927
|
+
export { BLOCKED_EXTRAS, RequestBodyError, SHOPWAVE_ENTITIES, SHOPWAVE_LOOKUPS, SHOPWAVE_RESOURCES, SHOPWAVE_TOKEN_EXPIRED_ERROR_ID, SHOPWAVE_UPLOAD, ShopwaveAuthError, UPLOAD_KINDS, UPLOAD_PREFIXES, assertOAuthConfig, authorizationHeader, createShopwaveApiHandlers, createShopwaveOAuth, createState, isExpiredTokenResponse, isTokenExpired, normalizeStoredToken, normalizeWriteBody, parseExtras, readChildIds, readRequestToken, safeEqual, sanitizeReturnTo, shopwaveDateTime, tokenFromResponse };
|
|
861
928
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["body: URLSearchParams | FormData","response: Response","SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>>","UPLOAD_KINDS: Readonly<Record<UploadKind, string>>","UPLOAD_PREFIXES: ReadonlyArray<UploadPrefix>","SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>>","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","body: BodyInit | undefined","postBody: unknown","record: Record<string, unknown>","form: FormData","body: unknown","retired: Record<string, unknown>","save: RouteHandler"],"sources":["../../src/server/errors.ts","../../src/server/token.ts","../../src/server/oauth.ts","../../src/server/returnTo.ts","../../src/core/resources.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 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","/**\n * Singleton resources (one per logged-in user) and the uploader.\n *\n * Like `SHOPWAVE_ENTITIES`, this table is shared by the client functions\n * (`fetchMerchant`, `fetchUser`, `uploadImage`, …) and the server route\n * handlers in `react-shopwave-connect/server`, so the two halves of the contract\n * can't drift apart. These aren't in `SHOPWAVE_ENTITIES` because they aren't\n * keyed collections: `GET /merchant` answers `{ merchant: {…} }`, not a map by id.\n */\n\nexport interface ResourceDefinition {\n /** App route: `/api/<route>`. */\n route: string;\n /** Key of the object in requests and responses (`{ merchant: {…} }`). */\n key: string;\n /** Shopwave API path. */\n upstream: string;\n /** False for read-only resources. */\n writable: boolean;\n}\n\nexport type ResourceKind = \"merchant\" | \"user\";\n\n/**\n * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`\n * updates it (without an `id` the API would create a second merchant, so the\n * route requires one).\n * - user: `GET /user` (read-only here; the API's `POST /user` invites users and\n * needs the admin scope).\n */\nexport const SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>> = Object.freeze({\n merchant: { route: \"merchant\", key: \"merchant\", upstream: \"merchant\", writable: true },\n user: { route: \"user\", key: \"user\", upstream: \"user\", writable: false },\n});\n\n/**\n * What an upload is for. Sent to Shopwave as the `contentType` header of\n * `PUT /uploader`. `merchant` is for the receipt and square logos (stored\n * under `…/merchant/<id>/logo/`), `merchantFeature` for the featured image.\n * The API spells two of its values `applicaionLogo` / `applicaionImages`; use\n * the correctly spelt names here and the server maps them.\n */\nexport type UploadKind = \"merchant\" | \"merchantFeature\" | \"product\" | \"user\" | \"applicationLogo\" | \"applicationImages\";\n\n/** `UploadKind` → the API's `contentType` value. */\nexport const UPLOAD_KINDS: Readonly<Record<UploadKind, string>> = Object.freeze({\n merchant: \"merchant\",\n /** The merchant's featured image (saved by Shopwave's existing admin; not in the API reference). */\n merchantFeature: \"merchantFeature\",\n product: \"product\",\n user: \"user\",\n applicationLogo: \"applicaionLogo\",\n applicationImages: \"applicaionImages\",\n});\n\n/**\n * The role an image plays, sent to Shopwave as the uploader's `prefix` header.\n * The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with\n * the file wherever a record stores it (a product's `images`). Live, test\n * merchant, 4 Nov 2026: all four values answered 201.\n * - `product`: a product gallery image;\n * - `product_thumbnail`: the product's lead image, shown in lists;\n * - `variant` / `variant_thumbnail`: a variant's own image and its lead image\n * (used by the Connect integrations).\n *\n * Values are the ones Connect's `uploadFileToShopwave` sends.\n */\nexport type UploadPrefix = \"product\" | \"product_thumbnail\" | \"variant\" | \"variant_thumbnail\";\n\n/** Every `UploadPrefix`, longest first so a name check never takes `product` for `product_thumbnail`. */\nexport const UPLOAD_PREFIXES: ReadonlyArray<UploadPrefix> = Object.freeze([\n \"product_thumbnail\",\n \"variant_thumbnail\",\n \"product\",\n \"variant\",\n] as const);\n\n/** How uploads are addressed (app route and Shopwave uploader). */\nexport const SHOPWAVE_UPLOAD = Object.freeze({\n /** App route: `POST /api/<route>` (multipart `file` + `kind`, optional `prefix`). */\n route: \"upload\",\n /** Shopwave path: `PUT {apiUrl}/uploader` (multipart `file`). */\n upstream: \"uploader\",\n /** Header that carries the upload kind upstream. */\n kindHeader: \"contentType\",\n /** Header that carries the image role upstream (see `UploadPrefix`). */\n prefixHeader: \"prefix\",\n /** Multipart field name for the file, on both sides. */\n fileField: \"file\",\n /** Multipart field name for the kind in the app request. */\n kindField: \"kind\",\n /** Multipart field name for the image role in the app request. */\n prefixField: \"prefix\",\n});\n\nexport function isUploadKind(value: unknown): value is UploadKind {\n return typeof value === \"string\" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);\n}\n\nexport function isUploadPrefix(value: unknown): value is UploadPrefix {\n return typeof value === \"string\" && (UPLOAD_PREFIXES as ReadonlyArray<string>).includes(value);\n}\n\n/**\n * Read-only lists that aren't records: the merchant's distinct tags.\n *\n * Like `SHOPWAVE_RESOURCES`, shared by the client functions\n * (`fetchProductTags`, `fetchProductInstanceTags`) and the server route\n * handlers (`shopwave.lookup(kind)`), so the two halves can't drift apart.\n *\n * - productTags: `GET /product/tags` → `{ tags: string[] }`, every distinct tag\n * on the merchant's products. No parameters. The API reference says\n * \"internal SKU tags are excluded\", but the live API includes system tags\n * written by imports and syncs (`PID-133`, `VID-137`, `#Hash:…`, `upc-…`;\n * a test merchant, 29 Oct 2026). Apps that offer the list as suggestions\n * filter those themselves. Tags of deleted products drop out of the list.\n * - productInstanceTags: `GET /product/instance/tags` → `{ tags: string[] }`,\n * the same for price instances (channels such as \"Eat In\", \"Takeout\").\n *\n * The app route for instance tags is `products/instance-tags`, one segment\n * under `products`, so it sits beside `products/[id]` without nesting deeper.\n */\nexport interface LookupDefinition {\n /** App route: `/api/<route>`. */\n route: string;\n /** Key of the list in the response (`{ tags: [...] }`). */\n key: string;\n /** Shopwave API path. */\n upstream: string;\n}\n\nexport type LookupKind = \"productTags\" | \"productInstanceTags\";\n\nexport const SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>> = Object.freeze({\n productTags: { route: \"products/tags\", key: \"tags\", upstream: \"product/tags\" },\n productInstanceTags: { route: \"products/instance-tags\", key: \"tags\", upstream: \"product/instance/tags\" },\n});\n","import { apiDelete, apiGet, apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport { toProductInstancesForSave, type Product, type ProductSaveInput } from \"./product\";\nimport { toStoredProductImages } from \"./productImages\";\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/server` (wrapped with\n * the session by `react-shopwave-connect/next`), so the two halves of the\n * 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 setting\n * `retireField` to 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 * Retire by reading the record and POSTing all of it back with the date(s)\n * changed, instead of a partial `{ id, [retireField]: <now> }`. Needed where\n * the API rejects a partial save (promotions: error 899, live 14 Oct 2026).\n */\n retireWithRecord?: boolean;\n /**\n * Start-date field of a record retired with `retireWithRecord`. If the record\n * hasn't started yet (start after now), the start moves to now as well, so\n * start = end and the record is expired without ever running.\n */\n retireStartField?: 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; promotions are never deleted, only expired. Expiring\n * reads the promotion and saves the whole record back with `endDate` = now (and\n * `startDate` = now too if it hadn't started); the live API rejects a partial\n * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.\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\", retireWithRecord: true, retireStartField: \"startDate\" },\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`), and a\n * promotion is saved back whole (see {@link deletePromotion}).\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\n/**\n * Creates (no `id`) or updates (with `id`) a product.\n *\n * Shopwave instances (prices) can't be updated, only replaced. Build\n * `instances` with {@link toProductInstancesForSave} (passing the product as last\n * read) so an edited price is sent as a new instance. As a safeguard, a new\n * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from\n * another product would otherwise point the save at that product's instances.\n *\n * `images` is sent as file names ({@link toStoredProductImages}), so the URLs a\n * read returns can be passed straight back; leaving `images` out keeps them.\n */\nexport const saveProduct = (product: ProductSaveInput, options?: RequestOptions) => {\n const isNew = product.id == null || String(product.id).trim() === \"\";\n let toSave: ProductSaveInput = isNew && product.instances != null ? { ...product, instances: toProductInstancesForSave(product.instances) } : product;\n // Reads return image URLs; writes take the uploader's file names.\n if (Array.isArray(toSave.images)) toSave = { ...toSave, images: toStoredProductImages(toSave.images) };\n return saveEntity(\"product\", toSave as EntityInput<Product>, options);\n};\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/**\n * Expires a promotion (Shopwave never deletes promotions). The route reads the\n * promotion and saves it back with `endDate` set to now; a promotion that hasn't\n * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't\n * exist or the save fails.\n */\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 {\n SHOPWAVE_LOOKUPS,\n SHOPWAVE_RESOURCES,\n SHOPWAVE_UPLOAD,\n UPLOAD_KINDS,\n UPLOAD_PREFIXES,\n isUploadKind,\n isUploadPrefix,\n type ResourceDefinition,\n type LookupKind,\n type ResourceKind,\n} from \"../core/resources\";\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 app 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 /** Sent as a multipart body (e.g. uploads). Ignored when `postBody` is set. */\n formData?: FormData;\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 ResourceHandlers {\n /** Read the resource (`GET {apiUrl}/<upstream>`). */\n GET: RouteHandler;\n /**\n * Update it: body `{ <key>: { id, … } }` or the bare object. The `id` is\n * required (without one Shopwave would create a new record). 405 for\n * read-only resources.\n */\n PUT: RouteHandler;\n /** Same as PUT. */\n POST: RouteHandler;\n}\n\nexport interface UploadOptions {\n /** Largest accepted file, in bytes. Defaults to 10 MB. */\n maxBytes?: number;\n /**\n * Accepted MIME types. An entry ending in `/` is a prefix (`\"image/\"`).\n * Defaults to `[\"image/\"]`.\n */\n accept?: string[];\n}\n\nexport interface UploadHandlers {\n /**\n * Multipart `file` + `kind` (see `UploadKind`), and optionally `prefix`\n * (see `UploadPrefix`) → `PUT {apiUrl}/uploader` with the `contentType` and\n * `prefix` headers. Answers with Shopwave's reply (201 `{ fileName, path, api }`);\n * 400 for a missing file or an unknown kind or prefix, 413 when too large,\n * 415 for other types.\n */\n POST: 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\"`). */\n passthrough(path: string): { GET: RouteHandler };\n /**\n * Handler for a read-only list (see `SHOPWAVE_LOOKUPS`), e.g.\n * `app/api/products/tags/route.ts`: `export const { GET } = shopwave.lookup(\"productTags\")`.\n * Takes no parameters, so the caller's `extras` aren't forwarded.\n */\n lookup(kind: LookupKind): { GET: RouteHandler };\n /** Handlers for a singleton resource: `app/api/merchant/route.ts`, `app/api/user/route.ts`. */\n resource(kind: ResourceKind): ResourceHandlers;\n /** Handler for `app/api/upload/route.ts`. */\n upload(options?: UploadOptions): UploadHandlers;\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\n/**\n * Reads a Shopwave datetime: ISO with a zone (what reads return, e.g.\n * `2026-10-20T16:00:00.000Z`) or a zoneless `YYYY-MM-DD HH:mm:ss`, which is UTC.\n * `null` when the value isn't a date.\n */\nfunction parseShopwaveDateTime(value: unknown): Date | null {\n if (typeof value !== \"string\" || !value.trim()) return null;\n const text = value.trim();\n const zoneless = /^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?$/.test(text);\n const date = new Date(zoneless ? `${text.replace(\" \", \"T\")}Z` : text);\n return Number.isNaN(date.getTime()) ? null : date;\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: BodyInit | undefined =\n init.postBody !== undefined\n ? new URLSearchParams({ postBody: JSON.stringify(init.postBody) })\n : init.formData;\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 instanceof URLSearchParams) headers.set(\"Content-Type\", \"application/x-www-form-urlencoded;charset=UTF-8\");\n // Multipart: fetch sets Content-Type with the boundary.\n else if (body) headers.delete(\"Content-Type\");\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 resourceDef(kind: ResourceKind): ResourceDefinition {\n const def = SHOPWAVE_RESOURCES[kind];\n if (!def) throw new Error(`Unknown Shopwave resource \"${kind}\"`);\n return def;\n }\n\n async function writeResource(request: Request, def: ResourceDefinition): Promise<Response> {\n if (!def.writable) return jsonError(405, \"method_not_allowed\", `The Shopwave API doesn't support writes for ${def.key}`);\n let record: Record<string, unknown>;\n try {\n const body = await readJsonBody(request);\n if (!isPlainObject(body)) throw new RequestBodyError(\"Request body must be a JSON object\");\n const inner = body[def.key];\n if (inner !== undefined && !isPlainObject(inner)) throw new RequestBodyError(`\"${def.key}\" must be an object`);\n record = { ...((inner as Record<string, unknown> | undefined) ?? body) };\n const id = record.id;\n if (id == null || id === \"\" || !ID_PATTERN.test(String(id))) {\n throw new RequestBodyError(`Missing or invalid ${def.key} id`);\n }\n record.id = /^\\d+$/.test(String(id)) ? Number(id) : id;\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: { [def.key]: record } });\n }\n\n function acceptsType(type: string, accept: string[]): boolean {\n const t = type.toLowerCase().split(\";\")[0].trim();\n return accept.some((a) => {\n const rule = a.toLowerCase();\n return rule.endsWith(\"/\") ? t.startsWith(rule) : t === rule;\n });\n }\n\n async function uploadFile(request: Request, options: UploadOptions): Promise<Response> {\n const maxBytes = options.maxBytes ?? 10 * 1024 * 1024;\n const accept = options.accept ?? [\"image/\"];\n\n // Reject obviously oversized bodies before reading them.\n const declared = Number(request.headers.get(\"content-length\") ?? 0);\n if (declared > maxBytes + 64 * 1024) return jsonError(413, \"too_large\", `The file is larger than ${maxBytes} bytes`);\n\n let form: FormData;\n try {\n form = await request.formData();\n } catch {\n return jsonError(400, \"invalid_body\", \"Expected a multipart/form-data body\");\n }\n const file = form.get(SHOPWAVE_UPLOAD.fileField);\n if (!file || typeof file === \"string\" || file.size === 0) {\n return jsonError(400, \"invalid_body\", `Missing \"${SHOPWAVE_UPLOAD.fileField}\"`);\n }\n const kind = form.get(SHOPWAVE_UPLOAD.kindField);\n if (!isUploadKind(kind)) {\n return jsonError(400, \"invalid_body\", `\"${SHOPWAVE_UPLOAD.kindField}\" must be one of ${Object.keys(UPLOAD_KINDS).join(\", \")}`);\n }\n const prefix = form.get(SHOPWAVE_UPLOAD.prefixField);\n if (prefix !== null && !isUploadPrefix(prefix)) {\n return jsonError(400, \"invalid_body\", `\"${SHOPWAVE_UPLOAD.prefixField}\" must be one of ${UPLOAD_PREFIXES.join(\", \")}`);\n }\n if (file.size > maxBytes) return jsonError(413, \"too_large\", `The file is larger than ${maxBytes} bytes`);\n if (!acceptsType(file.type || \"\", accept)) return jsonError(415, \"unsupported_type\", `Files of type \"${file.type || \"unknown\"}\" aren't accepted`);\n\n const upstreamForm = new FormData();\n upstreamForm.append(SHOPWAVE_UPLOAD.fileField, file, (file as File).name || \"upload\");\n return forward(request, {\n method: \"PUT\",\n path: SHOPWAVE_UPLOAD.upstream,\n headers: {\n [SHOPWAVE_UPLOAD.kindHeader]: UPLOAD_KINDS[kind],\n ...(prefix !== null ? { [SHOPWAVE_UPLOAD.prefixHeader]: prefix } : {}),\n },\n formData: upstreamForm,\n forwardExtras: false,\n });\n }\n\n function badId(): Response {\n return jsonError(400, \"invalid_id\", \"Missing or invalid id\");\n }\n\n /**\n * Retires a record the API won't take a partial save for (promotions): reads it,\n * sets `field` to now (and `def.retireStartField` to now too when the record\n * hasn't started yet), and POSTs the whole record back. Both dates are sent as\n * UTC `YYYY-MM-DD HH:mm:ss`; reads return them as ISO with `Z`.\n */\n async function retireWithRecord(request: Request, def: EntityDefinition, id: string, field: string): Promise<Response> {\n const read = await forward(request, { path: def.upstream, headers: { [def.idsHeader]: id }, forwardExtras: false });\n if (!read.ok) return read;\n let body: unknown;\n try {\n body = await read.json();\n } catch {\n return jsonError(502, \"invalid_upstream\", `The Shopwave API's ${def.upstream} read wasn't JSON`);\n }\n const map = isPlainObject(body) && isPlainObject(body[def.collection]) ? body[def.collection] : {};\n const record = Object.values(map as Record<string, unknown>).find(\n (entry): entry is Record<string, unknown> => isPlainObject(entry) && String(entry.id) === id\n );\n if (!record) return jsonError(404, \"not_found\", `No ${def.upstream} with id ${id}`);\n\n const now = new Date();\n const retired: Record<string, unknown> = { ...record, [field]: shopwaveDateTime(now) };\n const startField = def.retireStartField;\n if (startField) {\n const start = parseShopwaveDateTime(record[startField]);\n if (start) retired[startField] = shopwaveDateTime(start > now ? now : start);\n }\n return forward(request, {\n method: \"POST\",\n path: def.upstream,\n postBody: { [def.collection]: { 0: retired } },\n forwardExtras: false,\n });\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 if (def.retireWithRecord) return retireWithRecord(request, def, id, field);\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 lookup(kind) {\n const def = SHOPWAVE_LOOKUPS[kind];\n if (!def) throw new Error(`Unknown Shopwave lookup \"${kind}\"`);\n return { GET: (request) => forward(request, { path: def.upstream, forwardExtras: false }) };\n },\n\n resource(kind) {\n const def = resourceDef(kind);\n const save: RouteHandler = (request) => writeResource(request, def);\n return {\n GET: (request) => forward(request, { path: def.upstream }),\n PUT: save,\n POST: save,\n };\n },\n\n upload(options = {}) {\n return { POST: (request) => uploadFile(request, options) };\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["body: URLSearchParams | FormData","response: Response","SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>>","UPLOAD_KINDS: Readonly<Record<UploadKind, string>>","UPLOAD_PREFIXES: ReadonlyArray<UploadPrefix>","SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>>","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","body: BodyInit | undefined","postBody: unknown","record: Record<string, unknown>","form: FormData","body: unknown","retired: Record<string, unknown>","save: RouteHandler"],"sources":["../../src/server/errors.ts","../../src/server/token.ts","../../src/server/oauth.ts","../../src/server/returnTo.ts","../../src/core/resources.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 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","/**\n * Singleton resources (one per logged-in user) and the uploader.\n *\n * Like `SHOPWAVE_ENTITIES`, this table is shared by the client functions\n * (`fetchMerchant`, `fetchUser`, `uploadImage`, …) and the server route\n * handlers in `react-shopwave-connect/server`, so the two halves of the contract\n * can't drift apart. These aren't in `SHOPWAVE_ENTITIES` because they aren't\n * keyed collections: `GET /merchant` answers `{ merchant: {…} }`, not a map by id.\n */\n\nexport interface ResourceDefinition {\n /** App route: `/api/<route>`. */\n route: string;\n /** Key of the object in requests and responses (`{ merchant: {…} }`). */\n key: string;\n /** Shopwave API path. */\n upstream: string;\n /** False for read-only resources. */\n writable: boolean;\n}\n\nexport type ResourceKind = \"merchant\" | \"user\";\n\n/**\n * - merchant: `GET /merchant`; `POST /merchant` with `{ merchant: { id, … } }`\n * updates it (without an `id` the API would create a second merchant, so the\n * route requires one).\n * - user: `GET /user` (read-only here; the API's `POST /user` invites users and\n * needs the admin scope).\n */\nexport const SHOPWAVE_RESOURCES: Readonly<Record<ResourceKind, ResourceDefinition>> = Object.freeze({\n merchant: { route: \"merchant\", key: \"merchant\", upstream: \"merchant\", writable: true },\n user: { route: \"user\", key: \"user\", upstream: \"user\", writable: false },\n});\n\n/**\n * What an upload is for. Sent to Shopwave as the `contentType` header of\n * `PUT /uploader`. `merchant` is for the receipt and square logos (stored\n * under `…/merchant/<id>/logo/`), `merchantFeature` for the featured image.\n * The API spells two of its values `applicaionLogo` / `applicaionImages`; use\n * the correctly spelt names here and the server maps them.\n */\nexport type UploadKind = \"merchant\" | \"merchantFeature\" | \"product\" | \"user\" | \"applicationLogo\" | \"applicationImages\";\n\n/** `UploadKind` → the API's `contentType` value. */\nexport const UPLOAD_KINDS: Readonly<Record<UploadKind, string>> = Object.freeze({\n merchant: \"merchant\",\n /** The merchant's featured image (saved by Shopwave's existing admin; not in the API reference). */\n merchantFeature: \"merchantFeature\",\n product: \"product\",\n user: \"user\",\n applicationLogo: \"applicaionLogo\",\n applicationImages: \"applicaionImages\",\n});\n\n/**\n * The role an image plays, sent to Shopwave as the uploader's `prefix` header.\n * The uploader names the file `<prefix>_<hash>.<ext>`, so the role travels with\n * the file wherever a record stores it (a product's `images`). Live, test\n * merchant, 4 Nov 2026: all four values answered 201.\n * - `product`: a product gallery image;\n * - `product_thumbnail`: the product's lead image, shown in lists;\n * - `variant` / `variant_thumbnail`: a variant's own image and its lead image\n * (used by the Connect integrations).\n *\n * Values are the ones Connect's `uploadFileToShopwave` sends.\n */\nexport type UploadPrefix = \"product\" | \"product_thumbnail\" | \"variant\" | \"variant_thumbnail\";\n\n/** Every `UploadPrefix`, longest first so a name check never takes `product` for `product_thumbnail`. */\nexport const UPLOAD_PREFIXES: ReadonlyArray<UploadPrefix> = Object.freeze([\n \"product_thumbnail\",\n \"variant_thumbnail\",\n \"product\",\n \"variant\",\n] as const);\n\n/** How uploads are addressed (app route and Shopwave uploader). */\nexport const SHOPWAVE_UPLOAD = Object.freeze({\n /** App route: `POST /api/<route>` (multipart `file` + `kind`, optional `prefix`). */\n route: \"upload\",\n /** Shopwave path: `PUT {apiUrl}/uploader` (multipart `file`). */\n upstream: \"uploader\",\n /** Header that carries the upload kind upstream. */\n kindHeader: \"contentType\",\n /** Header that carries the image role upstream (see `UploadPrefix`). */\n prefixHeader: \"prefix\",\n /** Multipart field name for the file, on both sides. */\n fileField: \"file\",\n /** Multipart field name for the kind in the app request. */\n kindField: \"kind\",\n /** Multipart field name for the image role in the app request. */\n prefixField: \"prefix\",\n});\n\nexport function isUploadKind(value: unknown): value is UploadKind {\n return typeof value === \"string\" && Object.prototype.hasOwnProperty.call(UPLOAD_KINDS, value);\n}\n\nexport function isUploadPrefix(value: unknown): value is UploadPrefix {\n return typeof value === \"string\" && (UPLOAD_PREFIXES as ReadonlyArray<string>).includes(value);\n}\n\n/**\n * Read-only lists that aren't records: the merchant's distinct tags.\n *\n * Like `SHOPWAVE_RESOURCES`, shared by the client functions\n * (`fetchProductTags`, `fetchProductInstanceTags`) and the server route\n * handlers (`shopwave.lookup(kind)`), so the two halves can't drift apart.\n *\n * - productTags: `GET /product/tags` → `{ tags: string[] }`, every distinct tag\n * on the merchant's products. No parameters. The API reference says\n * \"internal SKU tags are excluded\", but the live API includes system tags\n * written by imports and syncs (`PID-133`, `VID-137`, `#Hash:…`, `upc-…`;\n * a test merchant, 29 Oct 2026). Apps that offer the list as suggestions\n * filter those themselves. Tags of deleted products drop out of the list.\n * - productInstanceTags: `GET /product/instance/tags` → `{ tags: string[] }`,\n * the same for price instances (channels such as \"Eat In\", \"Takeout\").\n *\n * The app route for instance tags is `products/instance-tags`, one segment\n * under `products`, so it sits beside `products/[id]` without nesting deeper.\n */\nexport interface LookupDefinition {\n /** App route: `/api/<route>`. */\n route: string;\n /** Key of the list in the response (`{ tags: [...] }`). */\n key: string;\n /** Shopwave API path. */\n upstream: string;\n}\n\nexport type LookupKind = \"productTags\" | \"productInstanceTags\";\n\nexport const SHOPWAVE_LOOKUPS: Readonly<Record<LookupKind, LookupDefinition>> = Object.freeze({\n productTags: { route: \"products/tags\", key: \"tags\", upstream: \"product/tags\" },\n productInstanceTags: { route: \"products/instance-tags\", key: \"tags\", upstream: \"product/instance/tags\" },\n});\n","import { apiDelete, apiGet, apiRequest, type RequestOptions } from \"./request\";\nimport { ShopwaveApiError, assertNoApiErrors } from \"./errors\";\nimport { toProductInstancesForSave, type Product, type ProductSaveInput } from \"./product\";\nimport { toStoredProductImages } from \"./productImages\";\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\";\nimport type { ProductComponent } from \"./productComponent\";\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/server` (wrapped with\n * the session by `react-shopwave-connect/next`), so the two halves of the\n * 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 setting\n * `retireField` to 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 * Retire by reading the record and POSTing all of it back with the date(s)\n * changed, instead of a partial `{ id, [retireField]: <now> }`. Needed where\n * the API rejects a partial save (promotions: error 899, live 14 Oct 2026).\n */\n retireWithRecord?: boolean;\n /**\n * Start-date field of a record retired with `retireWithRecord`. If the record\n * hasn't started yet (start after now), the start moves to now as well, so\n * start = end and the record is expired without ever running.\n */\n retireStartField?: string;\n /**\n * Header naming child records to delete instead of the whole record\n * (`productComponentInstanceIds: 1,2`). Only `item()` DELETE uses it, and only when\n * the caller's `extras` carry this one key (a non-empty list of ids); every other\n * `extras` key is still dropped. Only for `deleteMode: \"delete\"`.\n */\n childIdsHeader?: string;\n}\n\nexport type EntityKind = \"product\" | \"category\" | \"store\" | \"promotion\" | \"employee\" | \"consumer\" | \"productComponent\";\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; promotions are never deleted, only expired. Expiring\n * reads the promotion and saves the whole record back with `endDate` = now (and\n * `startDate` = now too if it hadn't started); the live API rejects a partial\n * `{ id, endDate }` save with error 899. Dates are UTC `YYYY-MM-DD HH:mm:ss`.\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 * - productComponent: modifier groups, `/product/component` (live, staging, 11 Nov 2026). The\n * API reference lists no read filters; these are what the live API does:\n * - Reads filter by `componentIds` (group ids) or `productIds` (the parent product; option\n * products don't match). `productComponentIds` is ignored on reads: every group comes back.\n * `deleted: false` gives live groups only, `true` deleted groups only, no header both.\n * No match → 204 with an empty body. No order, sort, priority or position field anywhere.\n * - Groups are keyed by id; `products` (option products) by product id; `productInstances`\n * (option rows) by row id. Reads reformat values: price `\"100\"` → `\"100.0\"`,\n * vatPercentage `\"0.2000\"` → `\"0.200\"`. `tags` and `name` are stored as sent (no trimming;\n * a `tags` array is joined with commas; `tags` null or left out → null).\n * - POST upserts (201, echoed under the ref). For each option product in `products`, its rows\n * are replaced: every row sent is created afresh with a new id (a row `id` is ignored, also\n * on a price change) and rows left out are removed. Option products left out of the save\n * are untouched, and an empty `productInstances` removes nothing. A save without `products`\n * updates only the group's own fields. The echo isn't proof of what was stored: renames of\n * groups created moments earlier by a test run echoed the new name but never stuck (re-read\n * for 90 s), while older groups, and groups created in Shopwave's own admin, renamed at\n * once. Cause not yet known.\n * - DELETE with `productComponentIds` (plural, comma list) → 205, soft delete. Adding\n * `productComponentInstanceIds` deletes just those rows (205); they stay in live reads with\n * `deleteDate` set. The singular `productComponentId` hangs (504 after 60 s).\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\", retireWithRecord: true, retireStartField: \"startDate\" },\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 productComponent: {\n route: \"product-components\",\n collection: \"productComponents\",\n upstream: \"product/component\",\n idsHeader: \"componentIds\",\n idHeader: \"productComponentIds\",\n writable: true,\n deleteMode: \"delete\",\n childIdsHeader: \"productComponentInstanceIds\",\n },\n});\n\nexport interface EntityTypes {\n product: Product;\n category: Category;\n store: Store;\n promotion: Promotion;\n employee: Employee;\n consumer: Consumer;\n productComponent: ProductComponent;\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`), and a\n * promotion is saved back whole (see {@link deletePromotion}).\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\n/**\n * Creates (no `id`) or updates (with `id`) a product.\n *\n * Shopwave instances (prices) can't be updated, only replaced. Build\n * `instances` with {@link toProductInstancesForSave} (passing the product as last\n * read) so an edited price is sent as a new instance. As a safeguard, a new\n * product (no `id`, e.g. a duplicate) never sends instance ids: an id copied from\n * another product would otherwise point the save at that product's instances.\n *\n * `images` is sent as file names ({@link toStoredProductImages}), so the URLs a\n * read returns can be passed straight back; leaving `images` out keeps them.\n */\nexport const saveProduct = (product: ProductSaveInput, options?: RequestOptions) => {\n const isNew = product.id == null || String(product.id).trim() === \"\";\n let toSave: ProductSaveInput = isNew && product.instances != null ? { ...product, instances: toProductInstancesForSave(product.instances) } : product;\n // Reads return image URLs; writes take the uploader's file names.\n if (Array.isArray(toSave.images)) toSave = { ...toSave, images: toStoredProductImages(toSave.images) };\n return saveEntity(\"product\", toSave as EntityInput<Product>, options);\n};\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/**\n * Expires a promotion (Shopwave never deletes promotions). The route reads the\n * promotion and saves it back with `endDate` set to now; a promotion that hasn't\n * started yet gets `startDate` = `endDate` = now. Rejects if the promotion doesn't\n * exist or the save fails.\n */\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 {\n SHOPWAVE_LOOKUPS,\n SHOPWAVE_RESOURCES,\n SHOPWAVE_UPLOAD,\n UPLOAD_KINDS,\n UPLOAD_PREFIXES,\n isUploadKind,\n isUploadPrefix,\n type ResourceDefinition,\n type LookupKind,\n type ResourceKind,\n} from \"../core/resources\";\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 app 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 /** Sent as a multipart body (e.g. uploads). Ignored when `postBody` is set. */\n formData?: FormData;\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 /**\n * Delete the record at `[id]`. Shopwave answers 205 with an empty body. For an entity with\n * `childIdsHeader`, `extras` carrying that key deletes just those child rows instead.\n */\n DELETE: RouteHandler;\n}\n\nexport interface ResourceHandlers {\n /** Read the resource (`GET {apiUrl}/<upstream>`). */\n GET: RouteHandler;\n /**\n * Update it: body `{ <key>: { id, … } }` or the bare object. The `id` is\n * required (without one Shopwave would create a new record). 405 for\n * read-only resources.\n */\n PUT: RouteHandler;\n /** Same as PUT. */\n POST: RouteHandler;\n}\n\nexport interface UploadOptions {\n /** Largest accepted file, in bytes. Defaults to 10 MB. */\n maxBytes?: number;\n /**\n * Accepted MIME types. An entry ending in `/` is a prefix (`\"image/\"`).\n * Defaults to `[\"image/\"]`.\n */\n accept?: string[];\n}\n\nexport interface UploadHandlers {\n /**\n * Multipart `file` + `kind` (see `UploadKind`), and optionally `prefix`\n * (see `UploadPrefix`) → `PUT {apiUrl}/uploader` with the `contentType` and\n * `prefix` headers. Answers with Shopwave's reply (201 `{ fileName, path, api }`);\n * 400 for a missing file or an unknown kind or prefix, 413 when too large,\n * 415 for other types.\n */\n POST: 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\"`). */\n passthrough(path: string): { GET: RouteHandler };\n /**\n * Handler for a read-only list (see `SHOPWAVE_LOOKUPS`), e.g.\n * `app/api/products/tags/route.ts`: `export const { GET } = shopwave.lookup(\"productTags\")`.\n * Takes no parameters, so the caller's `extras` aren't forwarded.\n */\n lookup(kind: LookupKind): { GET: RouteHandler };\n /** Handlers for a singleton resource: `app/api/merchant/route.ts`, `app/api/user/route.ts`. */\n resource(kind: ResourceKind): ResourceHandlers;\n /** Handler for `app/api/upload/route.ts`. */\n upload(options?: UploadOptions): UploadHandlers;\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\n/**\n * Reads a Shopwave datetime: ISO with a zone (what reads return, e.g.\n * `2026-10-20T16:00:00.000Z`) or a zoneless `YYYY-MM-DD HH:mm:ss`, which is UTC.\n * `null` when the value isn't a date.\n */\nfunction parseShopwaveDateTime(value: unknown): Date | null {\n if (typeof value !== \"string\" || !value.trim()) return null;\n const text = value.trim();\n const zoneless = /^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?$/.test(text);\n const date = new Date(zoneless ? `${text.replace(\" \", \"T\")}Z` : text);\n return Number.isNaN(date.getTime()) ? null : date;\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\n/**\n * The child ids a DELETE names in `extras` under `header` (case-insensitive), as one comma\n * list: `undefined` when the key is absent, `null` when `extras` or the value is invalid (not a\n * JSON object, or not a non-empty list of ids). The key may hold an array or a comma string.\n */\nexport function readChildIds(raw: string | null, header: string): string | undefined | null {\n if (!raw) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isPlainObject(parsed)) return null;\n const key = Object.keys(parsed).find((k) => k.toLowerCase() === header.toLowerCase());\n if (key === undefined) return undefined;\n const value = parsed[key];\n const list = Array.isArray(value) ? value : typeof value === \"string\" || typeof value === \"number\" ? String(value).split(\",\") : null;\n if (!list) return null;\n const ids = list.map((v) => (typeof v === \"string\" || typeof v === \"number\" ? String(v).trim() : \"\"));\n if (ids.length === 0 || ids.some((id) => !ID_PATTERN.test(id))) return null;\n return ids.join(\",\");\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: BodyInit | undefined =\n init.postBody !== undefined\n ? new URLSearchParams({ postBody: JSON.stringify(init.postBody) })\n : init.formData;\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 instanceof URLSearchParams) headers.set(\"Content-Type\", \"application/x-www-form-urlencoded;charset=UTF-8\");\n // Multipart: fetch sets Content-Type with the boundary.\n else if (body) headers.delete(\"Content-Type\");\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 resourceDef(kind: ResourceKind): ResourceDefinition {\n const def = SHOPWAVE_RESOURCES[kind];\n if (!def) throw new Error(`Unknown Shopwave resource \"${kind}\"`);\n return def;\n }\n\n async function writeResource(request: Request, def: ResourceDefinition): Promise<Response> {\n if (!def.writable) return jsonError(405, \"method_not_allowed\", `The Shopwave API doesn't support writes for ${def.key}`);\n let record: Record<string, unknown>;\n try {\n const body = await readJsonBody(request);\n if (!isPlainObject(body)) throw new RequestBodyError(\"Request body must be a JSON object\");\n const inner = body[def.key];\n if (inner !== undefined && !isPlainObject(inner)) throw new RequestBodyError(`\"${def.key}\" must be an object`);\n record = { ...((inner as Record<string, unknown> | undefined) ?? body) };\n const id = record.id;\n if (id == null || id === \"\" || !ID_PATTERN.test(String(id))) {\n throw new RequestBodyError(`Missing or invalid ${def.key} id`);\n }\n record.id = /^\\d+$/.test(String(id)) ? Number(id) : id;\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: { [def.key]: record } });\n }\n\n function acceptsType(type: string, accept: string[]): boolean {\n const t = type.toLowerCase().split(\";\")[0].trim();\n return accept.some((a) => {\n const rule = a.toLowerCase();\n return rule.endsWith(\"/\") ? t.startsWith(rule) : t === rule;\n });\n }\n\n async function uploadFile(request: Request, options: UploadOptions): Promise<Response> {\n const maxBytes = options.maxBytes ?? 10 * 1024 * 1024;\n const accept = options.accept ?? [\"image/\"];\n\n // Reject obviously oversized bodies before reading them.\n const declared = Number(request.headers.get(\"content-length\") ?? 0);\n if (declared > maxBytes + 64 * 1024) return jsonError(413, \"too_large\", `The file is larger than ${maxBytes} bytes`);\n\n let form: FormData;\n try {\n form = await request.formData();\n } catch {\n return jsonError(400, \"invalid_body\", \"Expected a multipart/form-data body\");\n }\n const file = form.get(SHOPWAVE_UPLOAD.fileField);\n if (!file || typeof file === \"string\" || file.size === 0) {\n return jsonError(400, \"invalid_body\", `Missing \"${SHOPWAVE_UPLOAD.fileField}\"`);\n }\n const kind = form.get(SHOPWAVE_UPLOAD.kindField);\n if (!isUploadKind(kind)) {\n return jsonError(400, \"invalid_body\", `\"${SHOPWAVE_UPLOAD.kindField}\" must be one of ${Object.keys(UPLOAD_KINDS).join(\", \")}`);\n }\n const prefix = form.get(SHOPWAVE_UPLOAD.prefixField);\n if (prefix !== null && !isUploadPrefix(prefix)) {\n return jsonError(400, \"invalid_body\", `\"${SHOPWAVE_UPLOAD.prefixField}\" must be one of ${UPLOAD_PREFIXES.join(\", \")}`);\n }\n if (file.size > maxBytes) return jsonError(413, \"too_large\", `The file is larger than ${maxBytes} bytes`);\n if (!acceptsType(file.type || \"\", accept)) return jsonError(415, \"unsupported_type\", `Files of type \"${file.type || \"unknown\"}\" aren't accepted`);\n\n const upstreamForm = new FormData();\n upstreamForm.append(SHOPWAVE_UPLOAD.fileField, file, (file as File).name || \"upload\");\n return forward(request, {\n method: \"PUT\",\n path: SHOPWAVE_UPLOAD.upstream,\n headers: {\n [SHOPWAVE_UPLOAD.kindHeader]: UPLOAD_KINDS[kind],\n ...(prefix !== null ? { [SHOPWAVE_UPLOAD.prefixHeader]: prefix } : {}),\n },\n formData: upstreamForm,\n forwardExtras: false,\n });\n }\n\n function badId(): Response {\n return jsonError(400, \"invalid_id\", \"Missing or invalid id\");\n }\n\n /**\n * Retires a record the API won't take a partial save for (promotions): reads it,\n * sets `field` to now (and `def.retireStartField` to now too when the record\n * hasn't started yet), and POSTs the whole record back. Both dates are sent as\n * UTC `YYYY-MM-DD HH:mm:ss`; reads return them as ISO with `Z`.\n */\n async function retireWithRecord(request: Request, def: EntityDefinition, id: string, field: string): Promise<Response> {\n const read = await forward(request, { path: def.upstream, headers: { [def.idsHeader]: id }, forwardExtras: false });\n if (!read.ok) return read;\n let body: unknown;\n try {\n body = await read.json();\n } catch {\n return jsonError(502, \"invalid_upstream\", `The Shopwave API's ${def.upstream} read wasn't JSON`);\n }\n const map = isPlainObject(body) && isPlainObject(body[def.collection]) ? body[def.collection] : {};\n const record = Object.values(map as Record<string, unknown>).find(\n (entry): entry is Record<string, unknown> => isPlainObject(entry) && String(entry.id) === id\n );\n if (!record) return jsonError(404, \"not_found\", `No ${def.upstream} with id ${id}`);\n\n const now = new Date();\n const retired: Record<string, unknown> = { ...record, [field]: shopwaveDateTime(now) };\n const startField = def.retireStartField;\n if (startField) {\n const start = parseShopwaveDateTime(record[startField]);\n if (start) retired[startField] = shopwaveDateTime(start > now ? now : start);\n }\n return forward(request, {\n method: \"POST\",\n path: def.upstream,\n postBody: { [def.collection]: { 0: retired } },\n forwardExtras: false,\n });\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 === \"delete\" && def.childIdsHeader) {\n // Child rows only (product component option rows). Never fall back to deleting the\n // whole record when the list is there but unusable.\n const childIds = readChildIds(request.headers.get(\"extras\"), def.childIdsHeader);\n if (childIds === null) {\n return jsonError(400, \"invalid_extras\", `\"${def.childIdsHeader}\" must be a non-empty list of ids`);\n }\n if (childIds !== undefined) {\n return forward(request, {\n method: \"DELETE\",\n path: def.upstream,\n headers: { [def.idHeader]: id, [def.childIdsHeader]: childIds },\n forwardExtras: false,\n });\n }\n }\n if (def.deleteMode === \"retire\") {\n // No DELETE upstream (employees, promotions): set the end date to now.\n const field = def.retireField ?? \"exitDate\";\n if (def.retireWithRecord) return retireWithRecord(request, def, id, field);\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 lookup(kind) {\n const def = SHOPWAVE_LOOKUPS[kind];\n if (!def) throw new Error(`Unknown Shopwave lookup \"${kind}\"`);\n return { GET: (request) => forward(request, { path: def.upstream, forwardExtras: false }) };\n },\n\n resource(kind) {\n const def = resourceDef(kind);\n const save: RouteHandler = (request) => writeResource(request, def);\n return {\n GET: (request) => forward(request, { path: def.upstream }),\n PUT: save,\n POST: save,\n };\n },\n\n upload(options = {}) {\n return { POST: (request) => uploadFile(request, options) };\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED