@sdg.la/wishlist-stack-sdk 0.10.1 → 0.12.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,209 @@
1
+ # Getting a customer access token (Shopify Liquid storefront)
2
+
3
+ Authenticated SDK endpoints require a `customerAccessToken`. In a Liquid theme, you typically run client-side JavaScript (theme asset or inline script) to perform the OAuth 2.0 + PKCE flow and pass the resulting `access_token` into `createWishlistStackClient({ customerAccessToken })`.
4
+
5
+ **Official Shopify documentation:**
6
+
7
+ - [Getting started with the Customer Account API](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/getting-started) — setup, credentials, OAuth overview
8
+ - [Customer Account API reference (OAuth)](https://shopify.dev/docs/api/customer/latest) — authorize/token endpoints, PKCE, `prompt=none` silent auth
9
+
10
+ > **Example only.** This snippet is adapted from Shopify's documentation for illustration. It is not maintained or supported by this SDK. Your theme, Customer Account API settings, and Shopify session behavior may differ — mileage may vary. For Hydrogen storefronts, use [`@sdg.la/wishlist-stack-hydrogen`](../hydrogen/README.md) instead.
11
+
12
+ ## Flow
13
+
14
+ 1. Handle OAuth callback — exchange `code` for tokens
15
+ 2. Refresh existing tokens when possible
16
+ 3. Otherwise redirect with `prompt=none` for silent auth (requires an existing Shopify login session)
17
+
18
+ ## Prerequisites
19
+
20
+ - Store ID and client ID from Customer Account API settings in Shopify admin
21
+ - Redirect URI registered in admin (must match `redirectUri` in the script)
22
+ - Customer logged in via the account link for silent auth to succeed
23
+
24
+ ## Example
25
+
26
+ ```js
27
+ (async function () {
28
+ const storeId = "YOUR_STORE_ID";
29
+ const clientId = ""; // Client id from headless environment
30
+ const redirectUri = window.location.origin;
31
+
32
+ function generateRandomCode() {
33
+ const array = new Uint8Array(32);
34
+ crypto.getRandomValues(array);
35
+ return String.fromCharCode.apply(null, Array.from(array));
36
+ }
37
+
38
+ function base64UrlEncode(str) {
39
+ const base64 = btoa(str);
40
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
41
+ }
42
+
43
+ function convertBufferToString(hash) {
44
+ const uintArray = new Uint8Array(hash);
45
+ const numberArray = Array.from(uintArray);
46
+ return String.fromCharCode(...numberArray);
47
+ }
48
+
49
+ async function generateCodeVerifier() {
50
+ const rando = generateRandomCode();
51
+ return base64UrlEncode(rando);
52
+ }
53
+
54
+ async function generateCodeChallenge(codeVerifier) {
55
+ const digestOp = await crypto.subtle.digest(
56
+ { name: "SHA-256" },
57
+ new TextEncoder().encode(codeVerifier),
58
+ );
59
+ const hash = convertBufferToString(digestOp);
60
+ return base64UrlEncode(hash);
61
+ }
62
+
63
+ async function generateState() {
64
+ const timestamp = Date.now().toString();
65
+ const randomString = Math.random().toString(36).substring(2);
66
+ return timestamp + randomString;
67
+ }
68
+
69
+ async function generateNonce(length) {
70
+ const characters =
71
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
72
+ let nonce = "";
73
+ for (let i = 0; i < length; i++) {
74
+ const randomIndex = Math.floor(Math.random() * characters.length);
75
+ nonce += characters.charAt(randomIndex);
76
+ }
77
+ return nonce;
78
+ }
79
+
80
+ // ── Step 1: Check for authorization code in URL (callback handler) ──
81
+
82
+ const urlParams = new URLSearchParams(window.location.search);
83
+ const code = urlParams.get("code");
84
+
85
+ if (code) {
86
+ console.log("Authorization code found, exchanging for tokens...");
87
+
88
+ const codeVerifier = localStorage.getItem("code-verifier");
89
+ if (!codeVerifier) {
90
+ console.error("No code-verifier found in localStorage. Cannot exchange code.");
91
+ return;
92
+ }
93
+
94
+ // Clean up URL
95
+ urlParams.delete("code");
96
+ urlParams.delete("state");
97
+ const newUrl = `${window.location.pathname}${urlParams.toString() ? `?${urlParams.toString()}` : ""}${window.location.hash}`;
98
+ window.history.replaceState({}, "", newUrl);
99
+ localStorage.removeItem("code-verifier");
100
+
101
+ // Exchange code for tokens
102
+ const body = new URLSearchParams();
103
+ body.append("grant_type", "authorization_code");
104
+ body.append("client_id", clientId);
105
+ body.append("redirect_uri", redirectUri);
106
+ body.append("code", code);
107
+ body.append("code_verifier", codeVerifier);
108
+
109
+ try {
110
+ const response = await fetch(
111
+ `https://shopify.com/authentication/${storeId}/oauth/token`,
112
+ {
113
+ method: "POST",
114
+ headers: { "content-type": "application/x-www-form-urlencoded" },
115
+ body,
116
+ },
117
+ );
118
+ const data = await response.json();
119
+
120
+ if (!data.access_token) {
121
+ console.error("Token exchange failed:", data);
122
+ return;
123
+ }
124
+
125
+ const { access_token, expires_in, id_token, refresh_token } = data;
126
+ localStorage.setItem(
127
+ "customer-access-data",
128
+ JSON.stringify({ access_token, expires_in, id_token, refresh_token }),
129
+ );
130
+ console.log("Access token obtained successfully!");
131
+ console.log("Access Token:", access_token);
132
+
133
+ return;
134
+ } catch (error) {
135
+ console.error("Error exchanging code for tokens:", error);
136
+ return;
137
+ }
138
+ }
139
+
140
+ // ── Step 2: Check for existing tokens and try refresh ──
141
+
142
+ const existingData = localStorage.getItem("customer-access-data");
143
+ if (existingData) {
144
+ const parsed = JSON.parse(existingData);
145
+ if (parsed.refresh_token) {
146
+ console.log("Found existing refresh token, attempting refresh...");
147
+
148
+ const body = new URLSearchParams();
149
+ body.append("grant_type", "refresh_token");
150
+ body.append("client_id", clientId);
151
+ body.append("refresh_token", parsed.refresh_token);
152
+
153
+ try {
154
+ const response = await fetch(
155
+ `https://shopify.com/authentication/${storeId}/oauth/token`,
156
+ {
157
+ method: "POST",
158
+ headers: { "content-type": "application/x-www-form-urlencoded" },
159
+ body,
160
+ },
161
+ );
162
+ const data = await response.json();
163
+
164
+ if (data.access_token) {
165
+ localStorage.setItem(
166
+ "customer-access-data",
167
+ JSON.stringify({
168
+ access_token: data.access_token,
169
+ expires_in: data.expires_in,
170
+ id_token: data.id_token || parsed.id_token,
171
+ refresh_token: data.refresh_token,
172
+ }),
173
+ );
174
+ console.log("Token refreshed successfully!");
175
+ console.log("Access Token:", data.access_token);
176
+ return;
177
+ }
178
+ } catch (error) {
179
+ console.warn("Refresh failed, will attempt silent auth:", error);
180
+ }
181
+ }
182
+ }
183
+
184
+ const state = await generateState();
185
+ const nonce = await generateNonce(16);
186
+ const verifier = await generateCodeVerifier();
187
+ const challenge = await generateCodeChallenge(verifier);
188
+ localStorage.setItem("code-verifier", verifier);
189
+
190
+ const authUrl = new URL(
191
+ `https://shopify.com/authentication/${storeId}/oauth/authorize`,
192
+ );
193
+ authUrl.searchParams.append("scope", "openid email customer-account-api:full");
194
+ authUrl.searchParams.append("client_id", clientId);
195
+ authUrl.searchParams.append("response_type", "code");
196
+ authUrl.searchParams.append("redirect_uri", redirectUri);
197
+ authUrl.searchParams.append("state", state);
198
+ authUrl.searchParams.append("nonce", nonce);
199
+ authUrl.searchParams.append("code_challenge", challenge);
200
+ authUrl.searchParams.append("code_challenge_method", "S256");
201
+ authUrl.searchParams.append("prompt", "none"); // Silent auth - no login UI
202
+
203
+ console.log("Redirecting for silent auth...");
204
+ console.log("If you see a login_required error in the URL after redirect,");
205
+ console.log("it means no Shopify session exists - log in via the account link first.");
206
+
207
+ window.location.href = authUrl.toString();
208
+ })();
209
+ ```
package/README.md CHANGED
@@ -14,9 +14,12 @@ TypeScript/JavaScript SDK for the Wishlist Stack API.
14
14
  - [Usage](#usage)
15
15
  - [Server / Node.js / Workers](#server--nodejs--workers)
16
16
  - [Browser](#browser)
17
+ - [Raw HTML (script module)](#raw-html-script-module)
18
+ - [Shopify Liquid authentication](#shopify-liquid-authentication)
17
19
  - [Class API](#class-api)
18
20
  - [Error Handling](#error-handling)
19
21
  - [Pagination](#pagination)
22
+ - [Sorting](#sorting)
20
23
  - [API Reference](#api-reference)
21
24
  - [Groups](#groups)
22
25
  - [Lists](#lists)
@@ -33,6 +36,7 @@ TypeScript/JavaScript SDK for the Wishlist Stack API.
33
36
  - **Flexible API** — Choose between functional `createWishlistStackClient()` or class-based `new WishlistStackClient()`
34
37
  - **Built-in error handling** — Structured `WishlistStackApiError` with status codes and API error messages
35
38
  - **Pagination support** — All list endpoints support `page` and `pageSize` parameters
39
+ - **Sort support** — Collection endpoints accept `sortBy` (`position` | `createdAt` | `updatedAt`) and `sortDirection` (`asc` | `desc`)
36
40
 
37
41
  ## Requirements
38
42
 
@@ -102,6 +106,35 @@ const { lists } = await client.lists.getAll();
102
106
 
103
107
  > **Note:** If you use a strict Content Security Policy (CSP), you must allow your API domain in `connect-src`.
104
108
 
109
+ ### Raw HTML (script module)
110
+
111
+ You can load the SDK in plain HTML without a bundler by using a `<script type="module">` tag and importing from a CDN:
112
+
113
+ ```html
114
+ <script type="module">
115
+ import { createWishlistStackClient } from "https://cdn.jsdelivr.net/npm/@sdg.la/wishlist-stack-sdk@0.10.1/+esm";
116
+
117
+ window.wishlistClient = createWishlistStackClient({
118
+ apiKey: "YOUR_MERCHANT_API_KEY",
119
+ customerAccessToken: "...",
120
+ });
121
+
122
+ // Example usage:
123
+ await window.wishlistClient.groups.getAll();
124
+ await window.wishlistClient.lists.getAll();
125
+ </script>
126
+ ```
127
+
128
+ Pin the version in the CDN URL (as shown above) so updates do not break your integration unexpectedly.
129
+
130
+ > **Note:** With a strict CSP, allow `https://cdn.jsdelivr.net` in `script-src` (for the module import) and your API domain in `connect-src`.
131
+
132
+ ### Shopify Liquid authentication
133
+
134
+ Authenticated SDK calls require a customer access token. For Liquid themes, obtain one via Shopify's Customer Account API OAuth flow in the browser.
135
+
136
+ See [AUTH_EXAMPLE.md](./AUTH_EXAMPLE.md) for an example-only reference implementation adapted from Shopify's docs.
137
+
105
138
  ### Class API
106
139
 
107
140
  If you prefer a class-based API, use `WishlistStackSDK`:
@@ -154,6 +187,9 @@ Endpoints that return lists support pagination via query parameters:
154
187
  // Paginate groups
155
188
  await client.groups.getAll({ page: 2, pageSize: 10 });
156
189
 
190
+ // Include lists + hydrated items on groups.getAll
191
+ await client.groups.getAll({ includeLists: true });
192
+
157
193
  // Paginate lists within a group
158
194
  await client.groups.getById('group-id', { page: 1, pageSize: 25 });
159
195
 
@@ -172,6 +208,24 @@ await client.lists.getById('list-id', { page: 1, pageSize: 25 });
172
208
  }
173
209
  ```
174
210
 
211
+ ## Sorting
212
+
213
+ Collection endpoints accept optional sort parameters. Defaults match historical behavior (`sortBy=position`, `sortDirection=asc`). Sorting applies only to the primary collection returned by the endpoint.
214
+
215
+ ```ts
216
+ // Recently updated groups first
217
+ await client.groups.getAll({ sortBy: 'updatedAt', sortDirection: 'desc' });
218
+
219
+ // Newest lists first
220
+ await client.lists.getAll({ sortBy: 'createdAt', sortDirection: 'desc' });
221
+
222
+ // Sort items in a list by updatedAt
223
+ await client.lists.getById('list-id', { sortBy: 'updatedAt', sortDirection: 'desc' });
224
+
225
+ // Shared endpoints support the same params
226
+ await client.shared.getSharedList('list-id', { sortBy: 'createdAt', sortDirection: 'asc' });
227
+ ```
228
+
175
229
  ## API Reference
176
230
 
177
231
  ### Groups
@@ -185,11 +239,16 @@ await client.lists.getById('list-id', { page: 1, pageSize: 25 });
185
239
  Fetch all groups for the authenticated customer.
186
240
 
187
241
  - **Endpoint:** `GET /api/groups`
188
- - **Parameters:** `query?` — `{ page?: number; pageSize?: number, query?: string }`
242
+ - **Parameters:** `query?` — `{ page?: number; pageSize?: number; query?: string; includeLists?: boolean | 1 | "1"; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
189
243
  - **Returns:** `Promise<GetGroupsResponse>`
190
244
 
245
+ Pass `includeLists: true` (sent as `includeLists=1`) to embed every list under each group with fully hydrated items — useful for loading projects/boards in one call instead of per-group `getById` fan-out. Without it, `lists` is an empty array and only `featuredItems` are populated.
246
+
191
247
  ```ts
192
- const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 10, query: 'holiday' });
248
+ const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 10, query: 'holiday', sortBy: 'updatedAt', sortDirection: 'desc' });
249
+
250
+ // Include lists + hydrated items
251
+ const { groups: withLists } = await client.groups.getAll({ includeLists: true });
193
252
  ```
194
253
 
195
254
  <details>
@@ -205,6 +264,7 @@ const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 1
205
264
  "position": 1,
206
265
  "shared": false,
207
266
  "listCount": 2,
267
+ "lists": [],
208
268
  "createdAt": "2025-01-10T08:00:00.000Z",
209
269
  "updatedAt": "2025-01-18T12:00:00.000Z",
210
270
  "featuredItems": [
@@ -231,6 +291,75 @@ const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 1
231
291
 
232
292
  </details>
233
293
 
294
+ <details>
295
+ <summary>Example response with <code>includeLists: true</code></summary>
296
+
297
+ ```json
298
+ {
299
+ "groups": [
300
+ {
301
+ "id": "cml8drad90001js09weonjojg",
302
+ "name": "Holiday Lists",
303
+ "description": "All my holiday wishlists",
304
+ "position": 1,
305
+ "shared": false,
306
+ "listCount": 1,
307
+ "lists": [
308
+ {
309
+ "id": "cml8drg8x0003js093j4ua6p8",
310
+ "name": "Gift Ideas",
311
+ "description": "Birthday gift ideas",
312
+ "position": 1,
313
+ "shared": false,
314
+ "itemCount": 1,
315
+ "items": [
316
+ {
317
+ "id": "item_1",
318
+ "quantity": 1,
319
+ "position": 1,
320
+ "userNote": null,
321
+ "product": {
322
+ "id": "gid://shopify/Product/1",
323
+ "title": "Example Product",
324
+ "description": "",
325
+ "availableForSale": true,
326
+ "handle": "example-product",
327
+ "productType": null,
328
+ "category": null,
329
+ "tags": [],
330
+ "onlineStoreUrl": null,
331
+ "metafields": [],
332
+ "vendor": null,
333
+ "variant": {
334
+ "id": "46932429275374",
335
+ "title": "Default",
336
+ "price": { "amount": "10.00", "currencyCode": "USD" },
337
+ "availableForSale": true,
338
+ "currentlyNotInStock": false,
339
+ "selectedOptions": [],
340
+ "metafields": []
341
+ }
342
+ }
343
+ }
344
+ ]
345
+ }
346
+ ],
347
+ "featuredItems": [],
348
+ "createdAt": "2025-01-10T08:00:00.000Z",
349
+ "updatedAt": "2025-01-18T12:00:00.000Z"
350
+ }
351
+ ],
352
+ "pagination": {
353
+ "page": 1,
354
+ "pageSize": 10,
355
+ "totalCount": 1,
356
+ "totalPages": 1
357
+ }
358
+ }
359
+ ```
360
+
361
+ </details>
362
+
234
363
  ---
235
364
 
236
365
  #### `groups.getById(groupId, query?)`
@@ -240,11 +369,11 @@ Fetch a single group by ID. Lists within the group are paginated.
240
369
  - **Endpoint:** `GET /api/groups/{groupId}`
241
370
  - **Parameters:**
242
371
  - `groupId` — `string`
243
- - `query?` — `{ page?: number; pageSize?: number, query?: string }`
372
+ - `query?` — `{ page?: number; pageSize?: number; query?: string; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
244
373
  - **Returns:** `Promise<GetGroupResponse>`
245
374
 
246
375
  ```ts
247
- const group = await client.groups.getById('group-id', { page: 1, pageSize: 25, query: 'gift' });
376
+ const group = await client.groups.getById('group-id', { page: 1, pageSize: 25, query: 'gift', sortBy: 'updatedAt', sortDirection: 'desc' });
248
377
  ```
249
378
 
250
379
  <details>
@@ -383,6 +512,38 @@ await client.groups.remove('group-id');
383
512
 
384
513
  ---
385
514
 
515
+ #### `groups.duplicate(groupId)`
516
+
517
+ Duplicate a group in a single call, including all of its lists and items. The copy is named `Copy of {original name}` and is not shared.
518
+
519
+ - **Endpoint:** `POST /api/groups/{groupId}/duplicate`
520
+ - **Parameters:** `groupId` — `string`
521
+ - **Returns:** `Promise<DuplicateGroupResponse>`
522
+
523
+ ```ts
524
+ const copy = await client.groups.duplicate('group-id');
525
+ ```
526
+
527
+ <details>
528
+ <summary>Example response</summary>
529
+
530
+ ```json
531
+ {
532
+ "id": "cmlbe49qk0001ju094x1ya9co",
533
+ "name": "Copy of Holiday Lists",
534
+ "description": "All my holiday wishlists",
535
+ "position": 2,
536
+ "shared": false,
537
+ "listCount": 2,
538
+ "createdAt": "2025-01-22T09:15:00.000Z",
539
+ "updatedAt": "2025-01-22T09:15:00.000Z"
540
+ }
541
+ ```
542
+
543
+ </details>
544
+
545
+ ---
546
+
386
547
  #### `groups.reorder(groupId, body)`
387
548
 
388
549
  Reorder lists within a group.
@@ -472,11 +633,11 @@ await client.groups.unshare('group-id');
472
633
  Fetch all lists for the authenticated customer.
473
634
 
474
635
  - **Endpoint:** `GET /api/lists`
475
- - **Parameters:** `query?` — `{ page?: number; pageSize?: number, query?: string }`
636
+ - **Parameters:** `query?` — `{ page?: number; pageSize?: number; query?: string; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
476
637
  - **Returns:** `Promise<GetListsResponse>`
477
638
 
478
639
  ```ts
479
- const { lists, pagination } = await client.lists.getAll({ page: 1, pageSize: 10 });
640
+ const { lists, pagination } = await client.lists.getAll({ page: 1, pageSize: 10, sortBy: 'updatedAt', sortDirection: 'desc' });
480
641
  ```
481
642
 
482
643
  <details>
@@ -527,11 +688,11 @@ Fetch a single list by ID. Items are hydrated with Shopify product data and pagi
527
688
  - **Endpoint:** `GET /api/lists/{listId}`
528
689
  - **Parameters:**
529
690
  - `listId` — `string`
530
- - `query?` — `{ page?: number; pageSize?: number }`
691
+ - `query?` — `{ page?: number; pageSize?: number; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
531
692
  - **Returns:** `Promise<GetListResponse>`
532
693
 
533
694
  ```ts
534
- const list = await client.lists.getById('list-id', { page: 1, pageSize: 25 });
695
+ const list = await client.lists.getById('list-id', { page: 1, pageSize: 25, sortBy: 'updatedAt', sortDirection: 'desc' });
535
696
  ```
536
697
 
537
698
  <details>
@@ -685,6 +846,38 @@ await client.lists.remove('list-id');
685
846
 
686
847
  ---
687
848
 
849
+ #### `lists.duplicate(listId)`
850
+
851
+ Duplicate a list in a single call, including all of its items. The copy stays in the same group (if any), is named `Copy of {original name}`, and is not shared.
852
+
853
+ - **Endpoint:** `POST /api/lists/{listId}/duplicate`
854
+ - **Parameters:** `listId` — `string`
855
+ - **Returns:** `Promise<DuplicateListResponse>`
856
+
857
+ ```ts
858
+ const copy = await client.lists.duplicate('list-id');
859
+ ```
860
+
861
+ <details>
862
+ <summary>Example response</summary>
863
+
864
+ ```json
865
+ {
866
+ "id": "cml8drg8x0003js093j4ua6p8",
867
+ "name": "Copy of Gift Ideas",
868
+ "description": "Birthday gift ideas",
869
+ "position": 2,
870
+ "shared": false,
871
+ "itemCount": 3,
872
+ "createdAt": "2025-01-22T09:15:00.000Z",
873
+ "updatedAt": "2025-01-22T09:15:00.000Z"
874
+ }
875
+ ```
876
+
877
+ </details>
878
+
879
+ ---
880
+
688
881
  #### `lists.addItems(listId, body)`
689
882
 
690
883
  Add one or more items to a list.
@@ -918,30 +1111,34 @@ Public read-only endpoints for accessing shared lists and groups. These require
918
1111
 
919
1112
  ---
920
1113
 
921
- #### `shared.getSharedList(listId)`
1114
+ #### `shared.getSharedList(listId, query?)`
922
1115
 
923
1116
  Fetch a public shared list with hydrated product details.
924
1117
 
925
1118
  - **Endpoint:** `GET /api/shared/list/{listId}`
926
- - **Parameters:** `listId` — `string`
1119
+ - **Parameters:**
1120
+ - `listId` — `string`
1121
+ - `query?` — `{ page?: number; pageSize?: number; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
927
1122
  - **Returns:** `Promise<GetSharedListResponse>` — same response shape as [`lists.getById`](#listsgetbyidlistid-query)
928
1123
 
929
1124
  ```ts
930
- const list = await client.shared.getSharedList('list-id');
1125
+ const list = await client.shared.getSharedList('list-id', { sortBy: 'updatedAt', sortDirection: 'desc' });
931
1126
  ```
932
1127
 
933
1128
  ---
934
1129
 
935
- #### `shared.getSharedGroup(groupId)`
1130
+ #### `shared.getSharedGroup(groupId, query?)`
936
1131
 
937
1132
  Fetch a public shared group and its lists.
938
1133
 
939
1134
  - **Endpoint:** `GET /api/shared/group/{groupId}`
940
- - **Parameters:** `groupId` — `string`
1135
+ - **Parameters:**
1136
+ - `groupId` — `string`
1137
+ - `query?` — `{ page?: number; pageSize?: number; query?: string; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
941
1138
  - **Returns:** `Promise<GetSharedGroupResponse>` — same response shape as [`groups.getById`](#groupsgetbyidgroupid-query)
942
1139
 
943
1140
  ```ts
944
- const group = await client.shared.getSharedGroup('group-id');
1141
+ const group = await client.shared.getSharedGroup('group-id', { sortBy: 'createdAt', sortDirection: 'asc' });
945
1142
  ```
946
1143
 
947
1144
  ## TypeScript Types
@@ -962,23 +1159,28 @@ import type {
962
1159
  GetGroupsResponse,
963
1160
  GetGroupResponse,
964
1161
  GroupSummary,
1162
+ GroupSummaryList,
965
1163
  GroupDetail,
966
1164
  GroupDetailList,
1165
+ GroupMutationResponse,
967
1166
  CreateGroupBody,
968
1167
  UpdateGroupBody,
969
1168
  ReorderGroupBody,
1169
+ DuplicateGroupResponse,
970
1170
 
971
1171
  // Lists
972
1172
  GetListsResponse,
973
1173
  GetListResponse,
974
1174
  ListSummary,
975
1175
  ListDetail,
1176
+ ListMutationResponse,
976
1177
  CreateListBody,
977
1178
  UpdateListBody,
978
1179
  AddItemsToListBody,
979
1180
  UpdateListItemBody,
980
1181
  ReorderListItemsBody,
981
1182
  ReorderListItemsResponse,
1183
+ DuplicateListResponse,
982
1184
 
983
1185
  // Shared
984
1186
  GetSharedListResponse,
@@ -1030,10 +1232,22 @@ type GroupSummary = {
1030
1232
  position: number;
1031
1233
  shared: boolean;
1032
1234
  listCount: number;
1235
+ /** Empty unless `includeLists` was requested on `groups.getAll()`. */
1236
+ lists: GroupSummaryList[];
1033
1237
  featuredItems: FeaturedItem[];
1034
1238
  createdAt: string;
1035
1239
  updatedAt: string;
1036
1240
  };
1241
+
1242
+ type GroupSummaryList = {
1243
+ id: string;
1244
+ name: string;
1245
+ description: string | null;
1246
+ position: number;
1247
+ shared: boolean;
1248
+ itemCount: number;
1249
+ items: HydratedWishlistItem[];
1250
+ };
1037
1251
  ```
1038
1252
 
1039
1253
  </details>