@01.software/sdk 0.37.0 → 0.39.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.
Files changed (74) hide show
  1. package/README.md +189 -84
  2. package/dist/analytics/react.cjs.map +1 -1
  3. package/dist/analytics/react.js.map +1 -1
  4. package/dist/analytics.cjs.map +1 -1
  5. package/dist/analytics.js.map +1 -1
  6. package/dist/client.cjs +1286 -109
  7. package/dist/client.cjs.map +1 -1
  8. package/dist/client.d.cts +8 -7
  9. package/dist/client.d.ts +8 -7
  10. package/dist/client.js +1286 -109
  11. package/dist/client.js.map +1 -1
  12. package/dist/{collection-client-DyELGUcL.d.ts → collection-client-CaMgs5KE.d.ts} +18 -12
  13. package/dist/{collection-client-zOmnxwdA.d.cts → collection-client-DVfB0Em1.d.cts} +18 -12
  14. package/dist/const-6XHz_jej.d.ts +32 -0
  15. package/dist/const-B5KT72c7.d.cts +32 -0
  16. package/dist/errors.cjs +4 -1
  17. package/dist/errors.cjs.map +1 -1
  18. package/dist/errors.js +4 -1
  19. package/dist/errors.js.map +1 -1
  20. package/dist/{index-DRJs7QIh.d.cts → index-BOLQxveo.d.cts} +3 -3
  21. package/dist/{index-DTqoUZk_.d.ts → index-CSwR2HSg.d.ts} +3 -3
  22. package/dist/index.cjs +2861 -2714
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +9 -9
  25. package/dist/index.d.ts +9 -9
  26. package/dist/index.js +2861 -2714
  27. package/dist/index.js.map +1 -1
  28. package/dist/{payload-types-CREOjFNT.d.cts → payload-types-m3jjhxk9.d.cts} +418 -106
  29. package/dist/{payload-types-CREOjFNT.d.ts → payload-types-m3jjhxk9.d.ts} +418 -106
  30. package/dist/query.cjs +244 -1093
  31. package/dist/query.cjs.map +1 -1
  32. package/dist/query.d.cts +159 -34
  33. package/dist/query.d.ts +159 -34
  34. package/dist/query.js +244 -1093
  35. package/dist/query.js.map +1 -1
  36. package/dist/realtime.cjs +5 -1
  37. package/dist/realtime.cjs.map +1 -1
  38. package/dist/realtime.d.cts +2 -2
  39. package/dist/realtime.d.ts +2 -2
  40. package/dist/realtime.js +5 -1
  41. package/dist/realtime.js.map +1 -1
  42. package/dist/server.cjs +1191 -22
  43. package/dist/server.cjs.map +1 -1
  44. package/dist/server.d.cts +7 -7
  45. package/dist/server.d.ts +7 -7
  46. package/dist/server.js +1191 -22
  47. package/dist/server.js.map +1 -1
  48. package/dist/storefront-cache.cjs +144 -0
  49. package/dist/storefront-cache.cjs.map +1 -0
  50. package/dist/storefront-cache.d.cts +24 -0
  51. package/dist/storefront-cache.d.ts +24 -0
  52. package/dist/storefront-cache.js +121 -0
  53. package/dist/storefront-cache.js.map +1 -0
  54. package/dist/{types-DMvVHdb1.d.ts → types-BQo7UdI9.d.cts} +1608 -1215
  55. package/dist/{types-BWMUr3Zw.d.cts → types-CVf8sCZ-.d.ts} +1608 -1215
  56. package/dist/{types-CxzWHspI.d.ts → types-Cmrd1ezc.d.ts} +1 -15
  57. package/dist/{types-BkZNhuBh.d.cts → types-D0ubzQw0.d.cts} +1 -15
  58. package/dist/ui/canvas/server.cjs +5 -1
  59. package/dist/ui/canvas/server.cjs.map +1 -1
  60. package/dist/ui/canvas/server.js +5 -1
  61. package/dist/ui/canvas/server.js.map +1 -1
  62. package/dist/ui/canvas.cjs +5 -1
  63. package/dist/ui/canvas.cjs.map +1 -1
  64. package/dist/ui/canvas.js +5 -1
  65. package/dist/ui/canvas.js.map +1 -1
  66. package/dist/ui/form.d.cts +1 -1
  67. package/dist/ui/form.d.ts +1 -1
  68. package/dist/ui/video.d.cts +1 -1
  69. package/dist/ui/video.d.ts +1 -1
  70. package/dist/webhook.d.cts +4 -4
  71. package/dist/webhook.d.ts +4 -4
  72. package/package.json +11 -1
  73. package/dist/const-CK_FPaIn.d.cts +0 -32
  74. package/dist/const-Dqz05oaG.d.ts +0 -32
package/README.md CHANGED
@@ -19,7 +19,7 @@ pnpm add @01.software/sdk
19
19
  - Customer auth hooks (useCustomerMe, useCustomerLogin, etc.) with cache management
20
20
  - Automatic retry with exponential backoff (non-retryable: 400, 401, 403, 404, 409, 422)
21
21
  - Webhook handling with HMAC-SHA256 signature verification
22
- - Sub-path imports (`./server`, `./webhook`, `./realtime`, `./ui/*`) for tree-shaking
22
+ - Sub-path imports (`./server`, `./webhook`, `./realtime`, `./storefront-cache`, `./ui/*`) for tree-shaking
23
23
  - Type-safe read-only `collections.from()` for Client (compile-time write prevention)
24
24
 
25
25
  ### Sub-path Imports
@@ -57,6 +57,9 @@ import {
57
57
  // Realtime only
58
58
  import { createRealtimeClient } from '@01.software/sdk/realtime'
59
59
 
60
+ // Storefront cache resource names for SSG/ISR adapters
61
+ import { storefrontCacheResources } from '@01.software/sdk/storefront-cache'
62
+
60
63
  // Components - sub-path imports per domain
61
64
  import { Analytics } from '@01.software/sdk/analytics/react'
62
65
  import { RichTextContent } from '@01.software/sdk/ui/rich-text'
@@ -79,6 +82,7 @@ entry.
79
82
  | `@01.software/sdk/server` | `createServerClient`, server-only collection, commerce, and preview APIs | none; keep `secretKey` code on the server |
80
83
  | `@01.software/sdk/query` | React Query hooks, cache helpers, `getQueryClient` | `@tanstack/react-query`, `react`, `react-dom` |
81
84
  | `@01.software/sdk/realtime` | `RealtimeConnection`, `useRealtimeQuery` | `@tanstack/react-query`, `react`, `react-dom` |
85
+ | `@01.software/sdk/storefront-cache` | product storefront cache resource name helpers | none |
82
86
  | `@01.software/sdk/analytics/react` | `<Analytics />` | `react`, `react-dom` |
83
87
  | `@01.software/sdk/ui/rich-text` | `RichTextContent`, `StyledRichTextContent` | `react`, `react-dom`, `@payloadcms/richtext-lexical` |
84
88
  | `@01.software/sdk/ui/form` | `FormRenderer` | `react`, `react-dom` |
@@ -99,6 +103,8 @@ Migration quick reference:
99
103
  - `createServerClient` must be imported from `@01.software/sdk/server`.
100
104
  - React Query hooks and cache helpers must be imported from
101
105
  `@01.software/sdk/query`.
106
+ - Product storefront cache resource helpers must be imported from
107
+ `@01.software/sdk/storefront-cache`.
102
108
  - UI components must be imported from the specific `@01.software/sdk/ui/*`
103
109
  sub-path and require only that row's peers.
104
110
  - Console-shared pure ecommerce helpers live in private
@@ -140,8 +146,16 @@ const serverQuery = createServerQueryHooks(server)
140
146
  const order = await server.commerce.orders.create({
141
147
  orderNumber: generateOrderNumber(),
142
148
  customerSnapshot: { email: 'user@example.com' },
143
- shippingAddress: { recipientName: 'John', phone: '010-1234-5678', postalCode: '12345', address: 'Seoul', detailAddress: 'Apt 101' },
144
- items: [{ product: productId, variant: variantId, option: optionId, quantity: 1 }],
149
+ shippingAddress: {
150
+ recipientName: 'John',
151
+ phone: '010-1234-5678',
152
+ postalCode: '12345',
153
+ address: 'Seoul',
154
+ detailAddress: 'Apt 101',
155
+ },
156
+ items: [
157
+ { product: productId, variant: variantId, option: optionId, quantity: 1 },
158
+ ],
145
159
  totalAmount: 10000,
146
160
  pgPaymentId: 'provider-payment-id', // optional (omit for free orders)
147
161
  discountCode: 'WELCOME10', // optional
@@ -176,6 +190,61 @@ previewToken })` returns the raw product detail payload for the saved
176
190
  draft/unpublished record addressed by the preview token. `detail()` wraps the
177
191
  published storefront payload in a `{ found, product | reason }` result.
178
192
 
193
+ ## Getting storefront content
194
+
195
+ Use shaped content helpers when a browser storefront needs relationship-backed
196
+ media for common public content. These helpers use the publishable key only;
197
+ the server resolves the tenant, enforces the owning feature, excludes drafts,
198
+ and returns allowlisted DTOs instead of raw Payload documents.
199
+
200
+ ```typescript
201
+ import { createClient } from '@01.software/sdk'
202
+
203
+ const client = createClient({ publishableKey: '<publishable-key>' })
204
+
205
+ const links = await client.content.links.list({
206
+ limit: 10,
207
+ categorySlug: 'social',
208
+ tagSlug: 'footer',
209
+ featured: true,
210
+ sort: 'title',
211
+ })
212
+
213
+ const gallery = await client.content.galleryItems.list({
214
+ gallerySlug: 'spring-lookbook',
215
+ limit: 24,
216
+ })
217
+ ```
218
+
219
+ `content.links.list()` calls `GET /api/links/storefront`; link DTOs include
220
+ display fields, categories/tags, thumbnail, and icon media. Operator-only
221
+ fields such as tenant, metadata, click counters, and private storage details
222
+ are omitted. Use `categorySlug` and `tagSlug` for stable storefront URLs when
223
+ available; `categoryId` and `tagId` are also supported. Expired links are
224
+ excluded by default, though visible link DTOs may include `expiresAt` so
225
+ storefronts can render time-sensitive copy.
226
+
227
+ `content.galleryItems.list()` calls `GET /api/gallery-items/storefront`;
228
+ gallery item DTOs include title, description, content, gallery reference, and
229
+ image media. Tenant, metadata, draft status, and storage/provider internals are
230
+ omitted. Gallery item reads require either `gallerySlug` or `galleryId`; prefer
231
+ `gallerySlug` for storefront routes. The default gallery item sort is `manual`,
232
+ matching curator order from the Admin Panel.
233
+
234
+ Both helpers return Payload-style pagination (`docs`, `totalDocs`, `page`,
235
+ `limit`, etc.). `limit` is bounded server-side to `1..100`; invalid query,
236
+ publishable-key, feature, rate-limit, and server errors surface through the
237
+ SDK's typed error classes and preserve request IDs on `client.lastRequestId`.
238
+ SDK sort inputs are typed public allowlists: links support created/updated/
239
+ published dates, title, and featured ordering; gallery items support manual
240
+ curator order, created/updated dates, and title.
241
+
242
+ Use `client.collections.from(...).find()` only as the advanced raw collection
243
+ escape hatch. Browser publishable-key raw reads stay shallow (`depth: 0`,
244
+ `joins: false`) and are not for relationship-expanded media. Use shaped helpers
245
+ for storefront content media, or `createServerClient()` when a server route
246
+ needs full raw collection access with server credentials.
247
+
179
248
  ## Getting product detail
180
249
 
181
250
  The recommended way to fetch a single product is the shaped helper:
@@ -219,14 +288,13 @@ catalog/stock split instead of reading `variant.stock` from a cached
219
288
  `detail()` response (inventory in that payload can lag for the catalog TTL).
220
289
 
221
290
  ```typescript
222
- import {
223
- createClient,
224
- mergeProductDetailWithStock,
225
- } from '@01.software/sdk'
291
+ import { createClient, mergeProductDetailWithStock } from '@01.software/sdk'
226
292
 
227
293
  const client = createClient({ publishableKey: '<publishable-key>' })
228
294
 
229
- const catalog = await client.commerce.product.detailCatalog({ slug: 'every-peach-tee' })
295
+ const catalog = await client.commerce.product.detailCatalog({
296
+ slug: 'every-peach-tee',
297
+ })
230
298
  if (!catalog.found) return notFound()
231
299
 
232
300
  const variantIds = catalog.product.variants.map((v) => v.id)
@@ -238,46 +306,63 @@ const { product, stockMergeStatus } = mergeProductDetailWithStock(
238
306
  // stockMergeStatus: 'complete' | 'partial' — partial when a variant id is missing from snapshot
239
307
  ```
240
308
 
241
- Listing groups use the same pattern: `listingGroupsCatalog()` for the cacheable
242
- shell, then `stockSnapshot()` (or `stock-check` at cart/checkout) for live
309
+ Listing UIs use the same pattern: `listingPage()` for PLP/search grids, or
310
+ `listingGroupsCatalog({ productIds })` when curated product IDs are already
311
+ known, then `stockSnapshot()` (or `stock-check` at cart/checkout) for live
243
312
  availability. `detail()` and POST detail/listing endpoints are unchanged during
244
313
  the migration window; see ADR 0012 addendum in `docs/decisions/0012-sdk-public-commerce-contract.md`.
245
314
 
246
315
  ### Product Listing Pages (PLP) — join-safe queries
247
316
 
248
- **Recommended path:** Use `commerce.product.listingGroupsCatalog()` (or the
249
- `useProductListingGroupsCatalogQuery` hook) together with
250
- `buildProductListingCard()`. The endpoint applies unlimited join fetches
251
- server-side and returns pre-grouped `ProductListingGroupsItem[]` data, so color
252
- swatches are never truncated regardless of how many option values the product
253
- has.
317
+ **Recommended path:** Use `commerce.product.listingPage()` (or
318
+ `createQueryHooks(client).useProductListingPage()`) for greenfield storefront
319
+ PLPs. It wraps the cacheable listing-groups query endpoint, keeps the raw
320
+ Payload pagination response, and adds `cards` built with
321
+ `buildProductListingCard()`. The endpoint returns pre-grouped listing data and
322
+ avoids the top-level `products.options` / `products.variants` join truncation
323
+ that raw REST product queries hit by default.
254
324
 
255
325
  ```typescript
256
- import {
257
- buildProductListingCard,
258
- type ProductListingCard,
259
- } from '@01.software/sdk'
260
-
261
- const response = await client.commerce.product.listingGroupsCatalog({
262
- where: { status: { equals: 'published' } },
326
+ const response = await client.commerce.product.listingPage({
327
+ search: 'shirt',
263
328
  limit: 24,
329
+ filters: {
330
+ categoryIds: ['category-1'],
331
+ price: { min: 10000, max: 50000 },
332
+ availableForSale: true,
333
+ },
334
+ basePath: '/shop',
264
335
  })
265
336
 
266
- const cards: ProductListingCard[] = response.docs.map((item) =>
267
- buildProductListingCard(item, { basePath: '/shop' }),
268
- )
337
+ const cards = response.cards
338
+ ```
339
+
340
+ Use `commerce.product.listingGroupsCatalog({ productIds })` when product IDs
341
+ are already known, for example curated rails, recommendations, or editorial
342
+ sections:
343
+
344
+ ```typescript
345
+ const response = await client.commerce.product.listingGroupsCatalog({
346
+ productIds: ['product-1', 'product-2'],
347
+ })
269
348
  ```
270
349
 
271
- **Escape hatch:** When you need to query the `products` collection directly
272
- (bulk operations, custom filters, fields the helper does not expose), spread
350
+ **Server-auth escape hatch:** When server code deliberately needs raw
351
+ `products` collection reads (bulk operations, custom filters, fields the helper
352
+ does not expose), use `createServerClient()` and spread
273
353
  `PRODUCT_PLP_FIND_OPTIONS` to raise the default Payload join limit of 10:
274
354
 
275
355
  ```typescript
276
- import {
277
- PRODUCT_PLP_FIND_OPTIONS,
278
- } from '@01.software/sdk'
356
+ import { PRODUCT_PLP_FIND_OPTIONS } from '@01.software/sdk'
357
+ import { createServerClient } from '@01.software/sdk/server'
279
358
 
280
- const { docs } = await client.collections.from('products').find({
359
+ const server = createServerClient({
360
+ publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
361
+ apiUrl: process.env.SOFTWARE_API_URL!,
362
+ secretKey: process.env.SOFTWARE_SECRET_KEY!,
363
+ })
364
+
365
+ const { docs } = await server.collections.from('products').find({
281
366
  ...PRODUCT_PLP_FIND_OPTIONS,
282
367
  where: { status: { equals: 'published' } },
283
368
  limit: 24,
@@ -288,7 +373,10 @@ const { docs } = await client.collections.from('products').find({
288
373
  limits with `sort: '_order'`. It cures top-level `products.options` and
289
374
  `products.variants` join truncation but **cannot** cure the nested
290
375
  `options[].values.docs` join — the Payload REST `joins` param is flat and
291
- nested join limits require the listing-groups endpoint.
376
+ nested join limits require the listing-groups endpoint. This preset is not
377
+ accepted by publishable `createClient().collections.from('products').find()`
378
+ because browser-public raw reads are constrained to `depth: 0` and
379
+ `joins: false`, and cannot use `populate`.
292
380
 
293
381
  ### Product selection helpers
294
382
 
@@ -325,9 +413,9 @@ aligned without rebuilding media priority in storefront code.
325
413
 
326
414
  For new storefront work, prefer pool-pointer and gallery-aware resolution from
327
415
  `commerce.product.detail()` + `resolveProductSelection()` (and
328
- `getProductSelectionImages()` when a list is needed). Direct legacy fields like
416
+ `getProductSelectionImages()` when a list is needed). Direct pre-ADR-0025 fields like
329
417
  `variant.thumbnail` and option-value direct `images` are still accepted as
330
- compatibility input, but are deprecated as primary storefront media sources.
418
+ transitional input, but are no longer primary storefront media sources.
331
419
 
332
420
  `availableValuesByOptionSlug` / `availableValuesByOptionId` include
333
421
  `availableStock`, `isUnlimited`, and `availableForSale` per value so option UIs
@@ -393,9 +481,13 @@ By default, partial selections (for example color only) leave
393
481
  `selectedOrFirstAvailableVariant` behavior with `fillDefaults: true`:
394
482
 
395
483
  ```typescript
396
- const resolution = resolveProductSelection(product, codec.parse('?opt.color=ivory'), {
397
- fillDefaults: true,
398
- })
484
+ const resolution = resolveProductSelection(
485
+ product,
486
+ codec.parse('?opt.color=ivory'),
487
+ {
488
+ fillDefaults: true,
489
+ },
490
+ )
399
491
  // resolution.selectedVariant is concrete; unselected options are filled
400
492
  // using the same available-by-order rules as listing selectionHintVariant.
401
493
  ```
@@ -446,8 +538,8 @@ Selection query params are share/deep-link state, not index targets.
446
538
  ### Product listing card helper
447
539
 
448
540
  `buildProductListingCard(item, options?)` turns a single
449
- `commerce.product.listingGroups()` response item into a render-ready
450
- `ProductListingCard`. Each item includes `listingGroupingState` (`grouped`,
541
+ `commerce.product.listingPage()` or `listingGroupsCatalog()` response item into
542
+ a render-ready `ProductListingCard`. Each item includes `listingGroupingState` (`grouped`,
451
543
  `no_primary_option`, or `empty`) and, when empty, `listingGroupingEmptyReason`
452
544
  (`primary_option_not_linked`, `primary_option_has_no_values`, or
453
545
  `no_variants_for_primary_option`). Each group includes public-safe
@@ -488,14 +580,17 @@ the card should deep-link a complete hint variant.
488
580
 
489
581
  ## Storefront performance defaults
490
582
 
491
- - **PLP:** prefer `useProductListingGroupsCatalogQuery()` (GET `/api/products/listing-groups/query/catalog`, CDN-cacheable) or `commerce.product.listingGroupsCatalog()` + `buildProductListingCard()`. Use `useProductListingGroupsQuery()` when you need the full POST `/query` response shape. Avoid fetching a product list and then calling `detail()` per card.
583
+ - **PLP:** prefer `commerce.product.listingPage()` or `useProductListingPage()` (GET `/api/products/listing-groups/query/catalog`, CDN-cacheable, card-ready). Use `listingGroupsCatalog({ productIds })` only when IDs are already known. Treat the full listing-groups response shape as a server-auth escape hatch because it can include operational stock fields. Avoid fetching a product list and then calling `detail()` per card.
492
584
  - **PDP:** prefer `useProductDetailBySlug()` / `commerce.product.detail()`. Override `staleTime` / `retry` on the hook when you need fresher catalog data or faster failure on errors.
493
585
  - **CDN-friendly reads:** server/edge code can use `detailCatalog()` and `listingGroupsCatalog()` (GET, cacheable) plus batched `stockSnapshot()` for live inventory.
494
586
  - **React Query in the browser:** default `getQueryClient()` keeps SSR data fresh forever (`staleTime: Infinity`). For client-only storefronts, use `getStorefrontQueryClient()` (~1 minute staleTime) when creating query hooks:
495
587
 
496
588
  ```ts
497
589
  import { createClient } from '@01.software/sdk'
498
- import { createQueryHooks, getStorefrontQueryClient } from '@01.software/sdk/query'
590
+ import {
591
+ createQueryHooks,
592
+ getStorefrontQueryClient,
593
+ } from '@01.software/sdk/query'
499
594
 
500
595
  const client = createClient({ publishableKey: '...' })
501
596
  const query = createQueryHooks(client, getStorefrontQueryClient())
@@ -507,11 +602,12 @@ Most consumers should use the helper APIs above (`commerce.product.detail`, etc.
507
602
 
508
603
  ### `depth` — how deep to populate relationship fields
509
604
 
510
- `depth` is the primary control for populating relationships like `category`, `images`, `brand`. The configured Payload default applies when unset.
605
+ `depth` is the primary control for populating relationships like `category`, `images`, `brand`. Browser publishable-key raw collection reads are constrained to `depth: 0` with `joins: false` and no `populate`; relationship-rich storefront reads should use shaped helpers such as `commerce.product.detail()` / `listingPage()`, or a `createServerClient()` raw query when server credentials are appropriate. Browser SDK raw reads add those safe defaults automatically and reject relationship-expanded raw read options before making a request.
511
606
 
512
607
  ```typescript
513
608
  const product = await client.collections.from('products').findById(id, {
514
- depth: 2, // populates product.category, product.category.parent, etc.
609
+ depth: 0,
610
+ joins: false,
515
611
  })
516
612
  ```
517
613
 
@@ -520,7 +616,7 @@ const product = await client.collections.from('products').findById(id, {
520
616
  `populate` controls which fields are returned per collection. It does NOT decide which relationships to populate — that is `depth`.
521
617
 
522
618
  ```typescript
523
- await client.collections.from('products').find({
619
+ await server.collections.from('products').find({
524
620
  depth: 2,
525
621
  populate: {
526
622
  categories: { title: true, slug: true },
@@ -531,11 +627,11 @@ await client.collections.from('products').find({
531
627
 
532
628
  ### `joins` — Payload join-field reverse-relations
533
629
 
534
- `joins` is the correct control for Payload `type: 'join'` virtual reverse-relation fields. In this platform's public SDK schema, `products.variants`, `products.options`, `customers.orders`, `customers.addresses`, `posts.comments`, `article-authors.articles`, `orders.{items,transactions,fulfillments,returns}`, and similar reverse-relations are all join fields — you must use `joins` (not `depth`/`populate`) to control their pagination, sorting, filtering, and count. Internal backing joins such as product collection memberships are intentionally omitted from public SDK collection types.
630
+ `joins` is the correct control for Payload `type: 'join'` virtual reverse-relation fields. In this platform's SDK schema, browser-public relations such as `products.variants`, `products.options`, and `article-authors.articles`, plus server-auth relations such as `customers.orders`, `customers.addresses`, `posts.comments`, and `orders.{items,transactions,fulfillments,returns}`, are all join fields — you must use `joins` (not `depth`/`populate`) to control their pagination, sorting, filtering, and count. Internal backing joins such as product collection memberships are intentionally omitted from SDK collection types.
535
631
 
536
632
  ```typescript
537
633
  // Canonical product detail query — variants/options are join fields on Products
538
- await client.collections.from('products').find({
634
+ await server.collections.from('products').find({
539
635
  where: { slug: { equals } },
540
636
  joins: {
541
637
  variants: { limit: 50, sort: '_order' },
@@ -546,11 +642,14 @@ await client.collections.from('products').find({
546
642
 
547
643
  // Disable all join-field population for a lightweight list query
548
644
  await client.collections.from('products').find({
645
+ depth: 0,
549
646
  joins: false,
550
647
  })
551
648
  ```
552
649
 
553
- Each join field defaults to **limit 10** when `joins` is omitted. `depth` does not raise that cap — storefront PLPs that call `products.find()` with only `depth` and then `buildProductListingGroupsByOption()` can silently drop color swatches. Prefer `listingGroupsCatalog()` for PLP cards, or spread `PRODUCT_PLP_FIND_OPTIONS` for raw product queries (see [PLP join-safe queries](#product-listing-pages-plp--join-safe-queries) above).
650
+ Each join field defaults to **limit 10** when `joins` is omitted. `depth` does not raise that cap — storefront PLPs that call `products.find()` with only `depth` and then `buildProductListingGroupsByOption()` can silently drop color swatches. Prefer `listingPage()` for PLP cards, or use `PRODUCT_PLP_FIND_OPTIONS` only in server-auth raw product queries (see [PLP join-safe queries](#product-listing-pages-plp--join-safe-queries) above).
651
+
652
+ Publishable-key browser raw reads must keep `depth: 0`, `joins: false`, and omit `populate`; relationship-expanded public storefront reads belong behind shaped helpers. Use `createServerClient()` for raw `joins` queries that need server credentials.
554
653
 
555
654
  `joins` does NOT populate normal relationship fields. Keys that do not match a `type: 'join'` field on the queried collection are silently ignored — e.g. `joins: { category: {} }` on Products is a no-op because `category` is not a join field there. For normal relationships use `depth` (and optionally `populate`).
556
655
 
@@ -651,7 +750,7 @@ const { docs, totalDocs, hasNextPage } = await client.collections
651
750
  page: 1,
652
751
  sort: '-createdAt',
653
752
  where: { status: { equals: 'published' } },
654
- depth: 2,
753
+ depth: 0,
655
754
  select: { title: true, slug: true },
656
755
  })
657
756
 
@@ -661,8 +760,8 @@ const { docs } = await client.collections.from('products').find({
661
760
  joins: false, // disable joins for lightweight list
662
761
  })
663
762
 
664
- // Override relationship populate
665
- const product = await client.collections.from('products').findById(id, {
763
+ // Override relationship populate and join expansion (server credentials only)
764
+ const product = await server.collections.from('products').findById(id, {
666
765
  populate: { brands: { name: true, logo: true } },
667
766
  joins: { variants: { limit: 50 } },
668
767
  })
@@ -708,7 +807,7 @@ import { extractSeo, generateMetadata } from '@01.software/sdk/metadata'
708
807
  const { docs } = await client.collections.from('products').find({
709
808
  where: { slug: { equals: 'my-product' } },
710
809
  limit: 1,
711
- depth: 1,
810
+ depth: 0,
712
811
  })
713
812
  const metadata = docs[0]
714
813
  ? generateMetadata(extractSeo(docs[0]), { siteName: 'My Store' })
@@ -893,7 +992,7 @@ await client.customer.auth.changePassword(currentPassword, newPassword)
893
992
 
894
993
  ### Commerce Orders (ServerClient-only writes)
895
994
 
896
- Available on ServerClient via `server.commerce.orders.*`. Only `checkout` and `listMine` are also on Client.
995
+ Available on ServerClient via `server.commerce.orders.*`. `checkout` and `listMine` are also on Client and return sanitized customer-facing order DTOs. Use `server.collections.from('orders')` for raw operational order documents.
897
996
 
898
997
  ```typescript
899
998
  // Orders
@@ -927,6 +1026,8 @@ await server.commerce.orders.confirmPayment({
927
1026
 
928
1027
  // Low-level transaction annotation / compatibility path. Prefer confirmPayment()
929
1028
  // for normal provider-verified paid transitions.
1029
+ // status: 'failed' records a retryable PG failure on the Transaction only; the
1030
+ // Order stays pending until verified payment succeeds or the merchant cancels it.
930
1031
  await server.commerce.orders.updateTransaction({ pgPaymentId, status, paymentMethod, receiptUrl })
931
1032
 
932
1033
  // Returns
@@ -974,15 +1075,15 @@ not provide.
974
1075
  documents, Payload saves product fields first; upsert receives `productId`,
975
1076
  `graphRevision` (required when updating an existing product via `productId` -
976
1077
  load from `GET /api/products/:id/composer-draft`), options, and variants. Do
977
- not send removed legacy media inputs (`optionValue.thumbnail`,
1078
+ not send removed pre-ADR-0025 media inputs (`optionValue.thumbnail`,
978
1079
  `optionValue.images`, `variant.thumbnail`); use `swatch.mediaItemId` and
979
1080
  variant `images[]`. Unknown keys are not part of the published upsert contract.
980
1081
 
981
- | Payload | Valid | Invalid |
982
- | ------- | ----- | ------- |
983
- | Create | `product: { title, ... }` + graph | `productId` on create; missing `product.title` |
984
- | Edit graph | `productId` + `graphRevision?` + graph | `product.title` / SEO on upsert; conflicting `productId` vs `product.id` |
985
- | Edit (legacy) | `product: { id }` + graph only | `product: { id, title }` on edit |
1082
+ | Payload | Valid | Invalid |
1083
+ | ------------- | -------------------------------------- | ------------------------------------------------------------------------ |
1084
+ | Create | `product: { title, ... }` + graph | `productId` on create; missing `product.title` |
1085
+ | Edit graph | `productId` + `graphRevision?` + graph | `product.title` / SEO on upsert; conflicting `productId` vs `product.id` |
1086
+ | Edit existing | `product: { id }` + graph only | `product: { id, title }` on edit |
986
1087
 
987
1088
  ```typescript
988
1089
  const result = await server.commerce.product.upsert({
@@ -1076,7 +1177,7 @@ for (const item of results) {
1076
1177
 
1077
1178
  ### Commerce Cart
1078
1179
 
1079
- Available on both Client and ServerClient via `commerce.cart.*`.
1180
+ Available on both Client and ServerClient via `commerce.cart.*`. These helpers return sanitized customer-facing cart DTOs; use `server.collections.from('carts' | 'cart-items')` for raw operational cart documents.
1080
1181
 
1081
1182
  ```typescript
1082
1183
  // Add item to cart
@@ -1094,7 +1195,7 @@ await client.commerce.cart.updateItem({ cartItemId, quantity })
1094
1195
  // Remove item from cart
1095
1196
  await client.commerce.cart.removeItem({ cartItemId })
1096
1197
 
1097
- // Other cart operations
1198
+ // Other cart operations return sanitized customer-facing cart DTOs.
1098
1199
  await client.commerce.cart.get(cartId)
1099
1200
  await client.commerce.cart.applyDiscount({ cartId, discountCode })
1100
1201
  await client.commerce.cart.removeDiscount({ cartId })
@@ -1170,7 +1271,10 @@ const customerAuthHandler = createCustomerAuthWebhookHandler({
1170
1271
  // Semantic order-change events keep operation as "update" for compatibility.
1171
1272
  // Use isOrderChangedWebhookEvent when you need to distinguish manual ordering
1172
1273
  // from content field edits.
1173
- import { handleWebhook, isOrderChangedWebhookEvent } from '@01.software/sdk/webhook'
1274
+ import {
1275
+ handleWebhook,
1276
+ isOrderChangedWebhookEvent,
1277
+ } from '@01.software/sdk/webhook'
1174
1278
 
1175
1279
  function getWebhookSecret(): string {
1176
1280
  const secret = process.env.WEBHOOK_SECRET
@@ -1203,31 +1307,32 @@ join-order surface and does not emit a semantic order-change webhook.
1203
1307
 
1204
1308
  ## Supported Collections
1205
1309
 
1206
- Source of truth: `packages/sdk/src/core/collection/const.ts` (`COLLECTIONS`: 73).
1310
+ Source of truth: `packages/sdk/src/core/collection/const.ts` (`COLLECTIONS`: 53).
1207
1311
 
1208
- | Category | Collections |
1312
+ | Category | Browser-public generic collections |
1209
1313
  | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1210
- | Tenant | `tenants`, `tenant-metadata`, `tenant-logos` |
1211
- | Products | `products`, `product-variants`, `product-options`, `product-option-values`, `product-categories`, `product-tags`, `product-collections`, `brands`, `brand-logos` |
1212
- | Orders | `orders`, `order-items`, `returns`, `return-items`, `fulfillments`, `fulfillment-items`, `transactions` |
1213
- | Customers | `customers`, `customer-profiles`, `customer-addresses` |
1214
- | Carts | `carts`, `cart-items` |
1314
+ | Tenant | `tenants`, `tenant-metadata` |
1315
+ | Products | `products`, `product-variants`, `product-options`, `product-option-values`, `product-categories`, `product-tags`, `product-collections`, `brands` |
1316
+ | Customers | `customer-profiles` |
1215
1317
  | Commerce | `discounts`, `shipping-policies`, `shipping-zones` |
1216
1318
  | Content | `documents`, `document-categories`, `document-types`, `articles`, `article-authors`, `article-categories`, `article-tags`, `links`, `link-categories`, `link-tags` |
1217
1319
  | Playlists / Tracks | `playlists`, `playlist-categories`, `playlist-tags`, `tracks`, `track-categories`, `track-tags` |
1218
1320
  | Galleries | `galleries`, `gallery-categories`, `gallery-tags`, `gallery-items` |
1219
1321
  | Canvas | `canvases`, `canvas-node-types`, `canvas-edge-types`, `canvas-categories`, `canvas-tags`, `canvas-nodes`, `canvas-edges` |
1220
- | Videos | `videos`, `video-categories`, `video-tags` |
1221
- | Live Streams | `live-streams` |
1222
- | Media | `images` |
1223
- | Forms | `forms`, `form-submissions` |
1224
- | Community | `posts`, `comments`, `reactions`, `reaction-types`, `bookmarks`, `post-categories`, `customer-profile-lists` |
1322
+ | Videos | `video-categories`, `video-tags` |
1323
+ | Forms | `forms` |
1324
+ | Community | `reaction-types`, `post-categories`, `post-tags`, `customer-profile-lists` |
1225
1325
  | Events | `event-calendars`, `events`, `event-categories`, `event-occurrences`, `event-tags` |
1226
1326
 
1227
- Server-only collections: `customer-groups`, `reports`, and `community-bans`
1228
- are available from `createServerClient().collections` for segmentation and
1229
- moderation workflows, but are intentionally absent from browser collection
1230
- discovery.
1327
+ Server-only collections include raw media/logo records, customer and order
1328
+ operational records, raw cart/cart-item records, form submissions, raw community documents
1329
+ (`posts`, `comments`, `reactions`, `bookmarks`), live stream provider records,
1330
+ segmentation records, and moderation records. They remain available from
1331
+ `createServerClient().collections` with secret/PAT credentials. Shaped
1332
+ browser/customer helpers such as `commerce.cart.*`,
1333
+ `commerce.orders.listMine()`, and `commerce.orders.checkout()` remain available
1334
+ where customer-facing DTOs are needed, but raw slugs are intentionally absent
1335
+ from browser collection discovery.
1231
1336
 
1232
1337
  ## Utilities
1233
1338
 
@@ -1362,18 +1467,18 @@ API keys created without explicit scopes use the default `['read', 'write']`. Co
1362
1467
  `swatch` (`type`, `color`, `mediaItemId`). Legacy public fields are removed
1363
1468
  from SDK input/output helpers and from listing/detail contracts.
1364
1469
 
1365
- | Surface | Removed | Replacement |
1366
- | ------- | ------- | ----------- |
1367
- | Upsert option value | `swatchColor`, `thumbnail`, `images` | `swatch.type`, `swatch.color`, `swatch.mediaItemId` |
1368
- | Listing groups | `optionValueSwatchColor`, `optionValueThumbnail`, `optionValueImages` | `optionValueSwatch` |
1369
- | Product detail option value | `swatchColor`, `thumbnail`, `images` | `swatch` |
1470
+ | Surface | Removed | Replacement |
1471
+ | --------------------------- | --------------------------------------------------------------------- | --------------------------------------------------- |
1472
+ | Upsert option value | `swatchColor`, `thumbnail`, `images` | `swatch.type`, `swatch.color`, `swatch.mediaItemId` |
1473
+ | Listing groups | `optionValueSwatchColor`, `optionValueThumbnail`, `optionValueImages` | `optionValueSwatch` |
1474
+ | Product detail option value | `swatchColor`, `thumbnail`, `images` | `swatch` |
1370
1475
 
1371
1476
  Migration steps:
1372
1477
 
1373
1478
  1. Replace color-only values with `swatch: { type: 'color', color: '#111111' }`.
1374
1479
  2. Replace thumbnail/gallery values with `swatch: { type: 'media', mediaItemId: '<product-pool-image-id>' }`.
1375
1480
  3. Ensure media swatches reference an image already attached to the parent product (`products.images` / `products.thumbnail`).
1376
- 4. Remove reads of legacy response fields; consume `optionValueSwatch` / `optionValue.swatch` instead.
1481
+ 4. Remove reads of pre-ADR-0025 response fields; consume `optionValueSwatch` / `optionValue.swatch` instead.
1377
1482
 
1378
1483
  ## Migration Guide
1379
1484