react-shopwave-connect 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,423 @@
1
+ import { IronSession } from "iron-session";
2
+
3
+ //#region src/server/token.d.ts
4
+
5
+ /**
6
+ * Token model + helpers shared by every server-side integration.
7
+ *
8
+ * The Shopwave auth server returns (snake_case):
9
+ * { access_token, refresh_token, token_type: "OAuth", expires_in: 43200 }
10
+ *
11
+ * We store a normalised, camelCase shape with an absolute expiry so any
12
+ * request can decide whether the token is still usable without extra state.
13
+ */
14
+ interface ShopwaveToken {
15
+ accessToken: string;
16
+ /** Long-lived; the Shopwave server does not rotate it on refresh. */
17
+ refreshToken?: string;
18
+ /** Scheme used in the Authorization header. Shopwave uses "OAuth". */
19
+ tokenType: string;
20
+ /** Absolute expiry, epoch milliseconds. Undefined if the server didn't say. */
21
+ expiresAt?: number;
22
+ }
23
+ /** `Authorization` header value, e.g. `OAuth 111ad…`. */
24
+ declare function authorizationHeader(token: ShopwaveToken): string;
25
+ /**
26
+ * True when a Shopwave API response means "your access token is no longer
27
+ * valid": HTTP 401, or the API error 908 in the response envelope.
28
+ * Use it to trigger a forced refresh + a single retry.
29
+ */
30
+ declare function isExpiredTokenResponse(status: number, body?: unknown): boolean;
31
+ //#endregion
32
+ //#region src/server/oauth.d.ts
33
+ /**
34
+ * Per-app OAuth settings. Everything that differs between Shopwave apps lives
35
+ * here; the flow itself is identical for all of them.
36
+ */
37
+ interface ShopwaveOAuthConfig {
38
+ /** e.g. `https://secure.merchantstack.com` (no trailing slash needed). */
39
+ authServerUrl: string;
40
+ clientId: string;
41
+ /** Server-side only. Never expose it to the browser. */
42
+ clientSecret: string;
43
+ /**
44
+ * The callback URL registered for this client on the auth server, e.g.
45
+ * `https://admin.example.com/auth`. Must match exactly.
46
+ */
47
+ redirectUri: string;
48
+ /** Where the auth server sends the user after logout. Defaults to `redirectUri`. */
49
+ postLogoutRedirectUri?: string;
50
+ /** Defaults to `"application"`. */
51
+ scope?: string;
52
+ /** Defaults to `"online"`. */
53
+ accessType?: string;
54
+ /**
55
+ * Body encoding for `POST /oauth/token`. Defaults to standard
56
+ * `application/x-www-form-urlencoded`; `"multipart"` sends `FormData`.
57
+ */
58
+ tokenRequestFormat?: "urlencoded" | "multipart";
59
+ /** Auth-server paths. Defaults match Shopwave: `/login`, `/oauth/token`, `/logout`. */
60
+ endpoints?: {
61
+ login?: string;
62
+ token?: string;
63
+ logout?: string;
64
+ };
65
+ /** Custom fetch (tests, proxies). Defaults to global `fetch`. */
66
+ fetch?: typeof fetch;
67
+ }
68
+ interface ShopwaveOAuthClient {
69
+ readonly config: Readonly<ShopwaveOAuthConfig>;
70
+ /** URL to send the browser to so the user can sign in. */
71
+ buildLoginUrl(params?: {
72
+ state?: string;
73
+ }): string;
74
+ /** URL to send the browser to so the auth server ends its own session. */
75
+ buildLogoutUrl(params?: {
76
+ redirectUri?: string;
77
+ }): string;
78
+ /** Exchanges the `?code=` from the callback for tokens (server-to-server). */
79
+ exchangeCode(code: string): Promise<ShopwaveToken>;
80
+ /**
81
+ * Gets a fresh access token. Shopwave keeps the same refresh token, so the
82
+ * returned token carries the previous refresh token when none is sent back.
83
+ */
84
+ refreshToken(token: ShopwaveToken | string): Promise<ShopwaveToken>;
85
+ }
86
+ //#endregion
87
+ //#region src/core/entities.d.ts
88
+ /**
89
+ * How each Shopwave entity is addressed, shared by the client functions below
90
+ * and the server route handlers in `react-shopwave-connect/next`, so the two
91
+ * halves of the contract can't drift apart.
92
+ */
93
+ interface EntityDefinition {
94
+ /** App route segment: `/api/<route>` and `/api/<route>/<id>`. */
95
+ route: string;
96
+ /** Key of the entity map in requests and responses (`{ products: { … } }`). */
97
+ collection: string;
98
+ /** Shopwave API path (`GET/POST/DELETE {apiUrl}/<upstream>`). */
99
+ upstream: string;
100
+ /** Header that filters reads by id (`productIds: 1,2`). */
101
+ idsHeader: string;
102
+ /** Header that names the record to delete (`productId: 1`). */
103
+ idHeader: string;
104
+ /** False for read-only entities (the API has no POST for them). */
105
+ writable: boolean;
106
+ /**
107
+ * How a delete is done upstream:
108
+ * - `"delete"`: `DELETE {apiUrl}/<upstream>` with the `idHeader` (205, empty body)
109
+ * - `"retire"`: the API has no DELETE; the record is retired by POSTing
110
+ * `{ id, [retireField]: <now> }` (employees: `exitDate`, promotions: `endDate`)
111
+ * - `"none"`: can't be deleted
112
+ */
113
+ deleteMode: "delete" | "retire" | "none";
114
+ /** Date field set to "now" when `deleteMode` is `"retire"`. */
115
+ retireField?: string;
116
+ }
117
+ type EntityKind = "product" | "category" | "store" | "promotion" | "employee" | "consumer";
118
+ /**
119
+ * Addressing per entity, following the Shopwave API reference
120
+ * (https://developer.merchantstack.com/api-reference.html) and checked against
121
+ * the live API where noted:
122
+ * - category/product/store: GET filtered by `<x>Ids`, POST upsert, DELETE with `<x>Id` → 205.
123
+ * - promotion: no DELETE; "deleting" a promotion ends it by setting `endDate` to now.
124
+ * - employee: POST updates `roleId`, `joinedDate`, `exitDate` (names can't be
125
+ * changed); no DELETE (live API: "Cannot DELETE /employee") → retired via `exitDate`.
126
+ * - consumer: read-only (`GET` with `ids`, comma-separated); live POST → 404.
127
+ */
128
+ declare const SHOPWAVE_ENTITIES: Readonly<Record<EntityKind, EntityDefinition>>;
129
+ //#endregion
130
+ //#region src/server/api.d.ts
131
+ /**
132
+ * The OAuth token a caller sent with the request, without its scheme, or `null`.
133
+ *
134
+ * Reads, in order: `Authorization: OAuth|Bearer <token>` (what the SDK sends
135
+ * from 0.3), the `token` header (SDK ≤ 0.2 writes), and `extras.token`
136
+ * (SDK ≤ 0.2 reads).
137
+ */
138
+ declare function readRequestToken(request: Request): string | null;
139
+ interface ShopwaveApiConfig {
140
+ /** Shopwave API base URL, e.g. `process.env.SHOPWAVE_API_SERVER_URL`. */
141
+ apiUrl: string;
142
+ /**
143
+ * `Authorization` header value for the logged-in user (`"OAuth <token>"`),
144
+ * or `null` when logged out. Called again with `forceRefresh: true` when the
145
+ * API reports the token expired.
146
+ */
147
+ getAuthorization?: (options?: {
148
+ forceRefresh?: boolean;
149
+ }) => Promise<string | null>;
150
+ /**
151
+ * Accept a token sent by the caller (`Authorization: OAuth <token>`, or the
152
+ * legacy `token` header / `extras.token`) instead of the session. The token
153
+ * is only forwarded to the Shopwave API, which validates it. Defaults to `true`
154
+ * (SDK integration tests, scripts and server-to-server calls rely on it).
155
+ */
156
+ allowRequestToken?: boolean;
157
+ /** `x-accept-version` sent upstream. Defaults to `"2.0"`. */
158
+ apiVersion?: string;
159
+ /** Custom fetch for the upstream call. */
160
+ fetch?: typeof fetch;
161
+ /** Override how an entity is addressed (e.g. a different id header). */
162
+ entities?: Partial<Record<EntityKind, Partial<EntityDefinition>>>;
163
+ /** Extra `extras` keys to drop, on top of {@link BLOCKED_EXTRAS}. */
164
+ blockedExtras?: string[];
165
+ /**
166
+ * Called when the upstream call throws (network error). Defaults to a
167
+ * `console.error` with the method and path only — never headers or tokens.
168
+ */
169
+ onError?: (error: unknown, context: {
170
+ method: string;
171
+ path: string;
172
+ }) => void;
173
+ }
174
+ interface ForwardInit {
175
+ /** HTTP method sent upstream. Defaults to `GET`. */
176
+ method?: string;
177
+ /** Shopwave API path, e.g. `"product"`. */
178
+ path: string;
179
+ /** Extra upstream headers; they win over `extras`. */
180
+ headers?: Record<string, string>;
181
+ /** Sent as the form field `postBody=<JSON>`. */
182
+ postBody?: unknown;
183
+ /** Forward the caller's `extras` header as upstream headers. Defaults to `true`. */
184
+ forwardExtras?: boolean;
185
+ }
186
+ /** Second argument Next.js (and similar) pass to dynamic route handlers. */
187
+ interface RouteContext {
188
+ params?: Promise<Record<string, string | string[] | undefined>> | Record<string, string | string[] | undefined>;
189
+ }
190
+ type RouteHandler = (request: Request, context?: RouteContext) => Promise<Response>;
191
+ interface CollectionHandlers {
192
+ /** List/filter via `extras` (e.g. `productIds`, `deleted`). */
193
+ GET: RouteHandler;
194
+ /** Create or update (Shopwave upserts: an `id` means update). */
195
+ POST: RouteHandler;
196
+ /** Same as POST (kept for older clients). */
197
+ PUT: RouteHandler;
198
+ }
199
+ interface ItemHandlers {
200
+ /** Read one record by the `[id]` route param. */
201
+ GET: RouteHandler;
202
+ /** Update the record at `[id]` (the id in the URL wins). */
203
+ PUT: RouteHandler;
204
+ /** Delete the record at `[id]`. Shopwave answers 205 with an empty body. */
205
+ DELETE: RouteHandler;
206
+ }
207
+ interface ShopwaveApiHandlers {
208
+ /** Calls the Shopwave API for this request (token, extras, refresh-retry) and returns its answer. */
209
+ forward(request: Request, init: ForwardInit): Promise<Response>;
210
+ /** Handlers for `app/api/<route>/route.ts`. */
211
+ collection(kind: EntityKind): CollectionHandlers;
212
+ /** Handlers for `app/api/<route>/[id]/route.ts`. */
213
+ item(kind: EntityKind, options?: {
214
+ param?: string;
215
+ }): ItemHandlers;
216
+ /** GET-only proxy for a Shopwave path (e.g. `"report"`, `"user"`, `"merchant"`). */
217
+ passthrough(path: string): {
218
+ GET: RouteHandler;
219
+ };
220
+ /** The entity table in use (defaults merged with `config.entities`). */
221
+ entity(kind: EntityKind): EntityDefinition;
222
+ }
223
+ //#endregion
224
+ //#region src/core/session.d.ts
225
+ /**
226
+ * Login status returned by the app's `/api/session` route. It deliberately
227
+ * never contains tokens — those stay in the encrypted httpOnly cookie.
228
+ */
229
+ interface SessionStatus {
230
+ loggedIn: boolean;
231
+ /** Access-token expiry (epoch ms), when known. */
232
+ expiresAt?: number;
233
+ }
234
+ //#endregion
235
+ //#region src/server/errors.d.ts
236
+ /**
237
+ * Error thrown by the server-side auth helpers.
238
+ *
239
+ * `code` is stable and safe to branch on; `status` is the HTTP status returned
240
+ * by the Shopwave auth server when there was one.
241
+ */
242
+ type ShopwaveAuthErrorCode = "config_invalid" | "token_exchange_failed" | "token_refresh_failed" | "token_response_invalid" | "network_error";
243
+ declare class ShopwaveAuthError extends Error {
244
+ readonly code: ShopwaveAuthErrorCode;
245
+ readonly status?: number;
246
+ /** Raw response body from the auth server, if any. Never contains our secret. */
247
+ readonly body?: string;
248
+ constructor(code: ShopwaveAuthErrorCode, message: string, details?: {
249
+ status?: number;
250
+ body?: string;
251
+ cause?: unknown;
252
+ });
253
+ /**
254
+ * True when the auth server rejected the grant itself (bad/expired code or
255
+ * refresh token) rather than failing for a transient reason. Callers should
256
+ * treat the user as logged out.
257
+ */
258
+ get isInvalidGrant(): boolean;
259
+ }
260
+ //#endregion
261
+ //#region src/next/index.d.ts
262
+ interface ShopwaveSessionConfig {
263
+ /**
264
+ * Encrypts the session cookie. At least 32 characters. Pass a map such as
265
+ * `{ 2: newPassword, 1: oldPassword }` to rotate without logging users out.
266
+ */
267
+ password: string | Record<string, string>;
268
+ /** Defaults to `"shopwave_session"`. Use a different name per app on a shared domain. */
269
+ cookieName?: string;
270
+ /** Session lifetime in seconds. Defaults to iron-session's 14 days. */
271
+ ttl?: number;
272
+ /** Defaults to `true` in production, `false` otherwise (so http://localhost works). */
273
+ secure?: boolean;
274
+ /** Defaults to `"lax"`, which the OAuth redirect back to your app needs. */
275
+ sameSite?: "lax" | "strict" | "none";
276
+ domain?: string;
277
+ }
278
+ interface ShopwaveAuthConfig extends ShopwaveOAuthConfig {
279
+ session: ShopwaveSessionConfig;
280
+ /**
281
+ * Path where `handlers.auth` is mounted. It starts the login AND receives the
282
+ * callback, so it should be the path of `redirectUri`. Defaults to `"/auth"`.
283
+ */
284
+ authPath?: string;
285
+ /** Path where `handlers.logout` is mounted. Defaults to `${authPath}/logout`. */
286
+ logoutPath?: string;
287
+ /**
288
+ * Path where `handlers.session` is mounted. Always public, because it is how
289
+ * the browser finds out it is logged out. Defaults to `"/api/session"`.
290
+ */
291
+ sessionPath?: string;
292
+ /** Where users land after login when no `returnTo` was given. Defaults to `"/"`. */
293
+ defaultReturnTo?: string;
294
+ /**
295
+ * Reject callbacks whose `state` doesn't match the one we sent (login-CSRF
296
+ * protection). Defaults to `true`. Only turn off if the auth server does not
297
+ * echo `state` back.
298
+ */
299
+ requireState?: boolean;
300
+ /**
301
+ * Refresh this many seconds before expiry. Defaults to `0`, because the
302
+ * Shopwave server only issues a new access token once the old one expired.
303
+ */
304
+ refreshSkewSeconds?: number;
305
+ /** How long a started login stays valid, in seconds. Defaults to 600. */
306
+ loginTimeoutSeconds?: number;
307
+ }
308
+ /** What we keep in the encrypted cookie. Tokens never leave the server. */
309
+ interface ShopwaveSessionData {
310
+ token?: ShopwaveToken | Record<string, unknown>;
311
+ pendingLogin?: {
312
+ state: string;
313
+ returnTo: string;
314
+ createdAt: number;
315
+ };
316
+ }
317
+ interface AuthContext {
318
+ accessToken: string;
319
+ /** Ready-to-use `Authorization` header value, e.g. `OAuth 111ad…`. */
320
+ authorization: string;
321
+ token: ShopwaveToken;
322
+ }
323
+ interface ProtectOptions {
324
+ /**
325
+ * Paths that don't need a login. A path matches itself and everything below
326
+ * it (`"/tools/tag-joiner"` covers `/tools/tag-joiner/x`). `"/"` matches only
327
+ * the root. The auth, logout and session paths are always public.
328
+ */
329
+ publicPaths?: string[];
330
+ /** Paths that get a 401 JSON response instead of a login redirect. Defaults to `["/api"]`. */
331
+ apiPaths?: string[];
332
+ /**
333
+ * Let API requests that carry their own token (`Authorization: OAuth <token>`,
334
+ * or the legacy `token` header / `extras.token`) through without a session
335
+ * cookie. The route handlers from {@link createShopwaveApi} forward that token
336
+ * to the Shopwave API, which validates it. Defaults to `false`.
337
+ */
338
+ allowRequestToken?: boolean;
339
+ }
340
+ interface ShopwaveAuth {
341
+ /** The underlying framework-agnostic OAuth client. */
342
+ readonly oauth: ShopwaveOAuthClient;
343
+ /** Route handlers to export from your app. */
344
+ readonly handlers: {
345
+ /** Mount at the redirect-URI path (e.g. `app/auth/route.ts`): `export const GET = auth.handlers.auth`. */
346
+ auth: (request: Request) => Promise<Response>;
347
+ /** Mount at `app/auth/logout/route.ts`: `export const GET = auth.handlers.logout`. */
348
+ logout: (request: Request) => Promise<Response>;
349
+ /** Mount at `app/api/session/route.ts`: `export const { GET, DELETE } = auth.handlers.session`. */
350
+ session: {
351
+ GET: (request?: Request) => Promise<Response>;
352
+ DELETE: (request?: Request) => Promise<Response>;
353
+ };
354
+ };
355
+ /** Login status for the current request. Never includes tokens. */
356
+ getStatus(): Promise<SessionStatus>;
357
+ /**
358
+ * A valid access token for the current user, refreshing it when expired
359
+ * (and saving the new one in the cookie when called from a route handler,
360
+ * server action or proxy). `null` when logged out or the refresh token was
361
+ * rejected. Pass `forceRefresh` after the API answered 401 / error 908.
362
+ */
363
+ getAccessToken(options?: {
364
+ forceRefresh?: boolean;
365
+ }): Promise<string | null>;
366
+ /** Same as `getAccessToken` but returns the whole token object. */
367
+ getToken(options?: {
368
+ forceRefresh?: boolean;
369
+ }): Promise<ShopwaveToken | null>;
370
+ /** `Authorization` header value (`"OAuth <token>"`), or `null` when logged out. */
371
+ getAuthorizationHeader(options?: {
372
+ forceRefresh?: boolean;
373
+ }): Promise<string | null>;
374
+ /** Wraps a route handler; responds 401 JSON when there's no valid session. */
375
+ withAuth<C = unknown>(handler: (request: Request, context: C, auth: AuthContext) => Response | Promise<Response>): (request: Request, context: C) => Promise<Response>;
376
+ /**
377
+ * For `proxy.ts` / `middleware.ts`. Returns a redirect (pages) or 401
378
+ * (API paths) when the request has no session, or `undefined` to continue.
379
+ * Only reads the cookie — it never calls the auth server.
380
+ */
381
+ protect(request: Request, options?: ProtectOptions): Promise<Response | undefined>;
382
+ /** Cookie-only check usable anywhere you have the Request. */
383
+ isAuthenticated(request: Request): Promise<boolean>;
384
+ /** Link that starts the login and comes back to `returnTo`. */
385
+ loginPath(returnTo?: string): string;
386
+ /** Link that logs the user out of the app and the auth server. */
387
+ readonly logoutPath: string;
388
+ /** Raw iron-session for advanced use (e.g. storing app data alongside the token). */
389
+ getSession(): Promise<IronSession<ShopwaveSessionData>>;
390
+ }
391
+ /**
392
+ * Creates the Shopwave auth instance for one Next.js app. Call once at module
393
+ * scope (e.g. `lib/auth.ts`) and reuse. Config is validated on first use, so
394
+ * a missing env var doesn't fail `next build`.
395
+ */
396
+ declare function createShopwaveAuth(config: ShopwaveAuthConfig): ShopwaveAuth;
397
+ interface ShopwaveNextApiConfig extends Omit<ShopwaveApiConfig, "getAuthorization"> {
398
+ /** The app's auth instance: supplies (and refreshes) the logged-in user's token. */
399
+ auth: Pick<ShopwaveAuth, "getAuthorizationHeader">;
400
+ }
401
+ /**
402
+ * Ready-made Next.js route handlers for the SDK's `/api/*` contract, using the
403
+ * session from `createShopwaveAuth`. Create once (e.g. `lib/shopwave.ts`):
404
+ *
405
+ * ```ts
406
+ * export const shopwave = createShopwaveApi({ auth, apiUrl: process.env.SHOPWAVE_API_SERVER_URL! });
407
+ * ```
408
+ *
409
+ * then each route file is one line:
410
+ *
411
+ * ```ts
412
+ * // app/api/products/route.ts
413
+ * export const { GET, POST, PUT } = shopwave.collection("product");
414
+ * // app/api/products/[id]/route.ts
415
+ * export const { GET, PUT, DELETE } = shopwave.item("product");
416
+ * // app/api/report/route.ts
417
+ * export const { GET } = shopwave.passthrough("report");
418
+ * ```
419
+ */
420
+ declare function createShopwaveApi(config: ShopwaveNextApiConfig): ShopwaveApiHandlers;
421
+ //#endregion
422
+ export { AuthContext, type CollectionHandlers, type EntityDefinition, type EntityKind, type ForwardInit, type ItemHandlers, ProtectOptions, type RouteContext, type RouteHandler, SHOPWAVE_ENTITIES, type SessionStatus, type ShopwaveApiHandlers, ShopwaveAuth, ShopwaveAuthConfig, ShopwaveAuthError, ShopwaveNextApiConfig, ShopwaveSessionConfig, ShopwaveSessionData, type ShopwaveToken, authorizationHeader, createShopwaveApi, createShopwaveAuth, isExpiredTokenResponse, readRequestToken };
423
+ //# sourceMappingURL=index.d.ts.map