@01.software/sdk 0.29.0 → 0.30.1

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 (78) hide show
  1. package/README.md +273 -73
  2. package/dist/analytics/react.cjs +4 -1
  3. package/dist/analytics/react.cjs.map +1 -1
  4. package/dist/analytics/react.js +4 -1
  5. package/dist/analytics/react.js.map +1 -1
  6. package/dist/analytics.cjs +4 -1
  7. package/dist/analytics.cjs.map +1 -1
  8. package/dist/analytics.js +4 -1
  9. package/dist/analytics.js.map +1 -1
  10. package/dist/client.cjs +1476 -0
  11. package/dist/client.cjs.map +1 -0
  12. package/dist/client.d.cts +28 -0
  13. package/dist/client.d.ts +28 -0
  14. package/dist/client.js +1453 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/collection-client-B9d9kr1d.d.ts +218 -0
  17. package/dist/collection-client-QPbwimkU.d.cts +218 -0
  18. package/dist/{const-DAjQYNuM.d.ts → const-B75IFDRi.d.ts} +2 -4
  19. package/dist/{const-Dsixdi6z.d.cts → const-VZuk2tWc.d.cts} +2 -4
  20. package/dist/index-B2WbhEgT.d.cts +106 -0
  21. package/dist/index-B2WbhEgT.d.ts +106 -0
  22. package/dist/index.cjs +784 -1530
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +11 -115
  25. package/dist/index.d.ts +11 -115
  26. package/dist/index.js +784 -1548
  27. package/dist/index.js.map +1 -1
  28. package/dist/metadata.cjs +91 -0
  29. package/dist/metadata.cjs.map +1 -0
  30. package/dist/metadata.d.cts +58 -0
  31. package/dist/metadata.d.ts +58 -0
  32. package/dist/metadata.js +68 -0
  33. package/dist/metadata.js.map +1 -0
  34. package/dist/{payload-types-Ci-ZA7aM.d.cts → payload-types-DPjO_IbQ.d.cts} +9 -3
  35. package/dist/{payload-types-Ci-ZA7aM.d.ts → payload-types-DPjO_IbQ.d.ts} +9 -3
  36. package/dist/query.cjs +1791 -0
  37. package/dist/query.cjs.map +1 -0
  38. package/dist/query.d.cts +244 -0
  39. package/dist/query.d.ts +244 -0
  40. package/dist/query.js +1786 -0
  41. package/dist/query.js.map +1 -0
  42. package/dist/realtime.cjs +4 -1
  43. package/dist/realtime.cjs.map +1 -1
  44. package/dist/realtime.d.cts +2 -2
  45. package/dist/realtime.d.ts +2 -2
  46. package/dist/realtime.js +4 -1
  47. package/dist/realtime.js.map +1 -1
  48. package/dist/{server-BINWywT8.d.cts → server-CrsPyqEc.d.cts} +14 -31
  49. package/dist/{server-BINWywT8.d.ts → server-CrsPyqEc.d.ts} +14 -31
  50. package/dist/server.cjs +299 -840
  51. package/dist/server.cjs.map +1 -1
  52. package/dist/server.d.cts +112 -7
  53. package/dist/server.d.ts +112 -7
  54. package/dist/server.js +299 -858
  55. package/dist/server.js.map +1 -1
  56. package/dist/{types-BWq_WlbB.d.ts → types-1fBLrYU7.d.ts} +1 -1
  57. package/dist/{types-zKjATmDK.d.cts → types-BwT0eeaz.d.cts} +1 -1
  58. package/dist/{server-Cv0Q4dPQ.d.ts → types-Dlb2mwpX.d.cts} +228 -741
  59. package/dist/{server-C0C8dtms.d.cts → types-DuSKPiY5.d.ts} +228 -741
  60. package/dist/ui/canvas/server.cjs +7 -6
  61. package/dist/ui/canvas/server.cjs.map +1 -1
  62. package/dist/ui/canvas/server.d.cts +1 -3
  63. package/dist/ui/canvas/server.d.ts +1 -3
  64. package/dist/ui/canvas/server.js +7 -6
  65. package/dist/ui/canvas/server.js.map +1 -1
  66. package/dist/ui/canvas.cjs +11 -10
  67. package/dist/ui/canvas.cjs.map +1 -1
  68. package/dist/ui/canvas.d.cts +29 -6
  69. package/dist/ui/canvas.d.ts +29 -6
  70. package/dist/ui/canvas.js +11 -10
  71. package/dist/ui/canvas.js.map +1 -1
  72. package/dist/ui/form.d.cts +1 -1
  73. package/dist/ui/form.d.ts +1 -1
  74. package/dist/ui/video.d.cts +1 -1
  75. package/dist/ui/video.d.ts +1 -1
  76. package/dist/webhook.d.cts +3 -3
  77. package/dist/webhook.d.ts +3 -3
  78. package/package.json +84 -15
package/README.md CHANGED
@@ -45,7 +45,7 @@ export function App() {
45
45
  // Main entry - browser client, query builder, hooks, utilities
46
46
  import { createClient } from '@01.software/sdk'
47
47
 
48
- // Server-only entry - avoids importing browser Client APIs
48
+ // Server-only entry - keep Secret Key code out of browser-facing imports
49
49
  import { createServerClient } from '@01.software/sdk/server'
50
50
 
51
51
  // Webhook only - webhook handlers
@@ -67,6 +67,45 @@ import { CanvasRenderer } from '@01.software/sdk/ui/canvas'
67
67
  import { VideoPlayer } from '@01.software/sdk/ui/video'
68
68
  ```
69
69
 
70
+ The root entry keeps `createClient`, commerce helpers, collection helpers, and
71
+ types lightweight. Server, React Query, and UI features live behind explicit
72
+ sub-paths so consumers install feature peers only when they import the matching
73
+ entry.
74
+
75
+ | Import | Feature(s) | Install when used |
76
+ | --- | --- | --- |
77
+ | `@01.software/sdk` | browser-safe `createClient`, commerce helpers, collection helpers, types | none |
78
+ | `@01.software/sdk/client` | browser-safe `createClient` entry | none |
79
+ | `@01.software/sdk/server` | `createServerClient`, server-only collection and commerce APIs | none; keep `secretKey` code on the server |
80
+ | `@01.software/sdk/query` | React Query hooks, cache helpers, `getQueryClient` | `@tanstack/react-query`, `react`, `react-dom` |
81
+ | `@01.software/sdk/realtime` | `RealtimeConnection`, `useRealtimeQuery` | `@tanstack/react-query`, `react`, `react-dom` |
82
+ | `@01.software/sdk/analytics/react` | `<Analytics />` | `react`, `react-dom` |
83
+ | `@01.software/sdk/ui/rich-text` | `RichTextContent`, `StyledRichTextContent` | `react`, `react-dom`, `@payloadcms/richtext-lexical` |
84
+ | `@01.software/sdk/ui/form` | `FormRenderer` | `react`, `react-dom` |
85
+ | `@01.software/sdk/ui/code-block` | `CodeBlock`, `highlight` | `react`, `react-dom`, `shiki`, `hast-util-to-jsx-runtime` |
86
+ | `@01.software/sdk/ui/canvas` | `CanvasRenderer`, `CanvasFrame`, `useCanvas`, `prefetchCanvas` | `react`, `react-dom`, `@tanstack/react-query`, `@xyflow/react`, `quickjs-emscripten`, `postcss`, `sucrase` |
87
+ | `@01.software/sdk/ui/canvas/server` | canvas server helpers | none |
88
+ | `@01.software/sdk/ui/video` | `VideoPlayer` | `react`, `react-dom`, `@mux/mux-player-react` |
89
+ | `@01.software/sdk/ui/image` | `Image` | `react`, `react-dom` |
90
+
91
+ If a feature is not listed here, it does not need a separate peer install.
92
+ For the full component-to-peer mapping, see
93
+ `packages/sdk/.claude/rules/components-reference.md`.
94
+
95
+ Migration quick reference:
96
+
97
+ - `createClient` remains available from `@01.software/sdk` and
98
+ `@01.software/sdk/client`.
99
+ - `createServerClient` must be imported from `@01.software/sdk/server`.
100
+ - React Query hooks and cache helpers must be imported from
101
+ `@01.software/sdk/query`.
102
+ - UI components must be imported from the specific `@01.software/sdk/ui/*`
103
+ sub-path and require only that row's peers.
104
+ - Console-shared pure ecommerce helpers live in private
105
+ `@01.software/contracts`. The public SDK keeps customer-facing helpers
106
+ self-contained and must not import private contracts; Console code should
107
+ import shared helpers from contracts directly.
108
+
70
109
  ## Getting Started
71
110
 
72
111
  ### Client
@@ -89,11 +128,13 @@ const { docs } = await client.collections.from('products').find({
89
128
 
90
129
  ```typescript
91
130
  import { createServerClient } from '@01.software/sdk/server'
131
+ import { createServerQueryHooks } from '@01.software/sdk/query'
92
132
 
93
133
  const server = createServerClient({
94
134
  publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY,
95
135
  secretKey: process.env.SOFTWARE_SECRET_KEY, // sk01_... opaque API key from Console
96
136
  })
137
+ const serverQuery = createServerQueryHooks(server)
97
138
 
98
139
  // Create order (server only)
99
140
  const order = await server.commerce.orders.create({
@@ -107,12 +148,15 @@ const order = await server.commerce.orders.create({
107
148
  })
108
149
 
109
150
  // SSR prefetch (server)
110
- await server.query.prefetchQuery({
151
+ await serverQuery.prefetchQuery({
111
152
  collection: 'products',
112
153
  options: { limit: 10 },
113
154
  })
114
155
  ```
115
156
 
157
+ Always import `createServerClient` from `@01.software/sdk/server` so generated
158
+ code and bundlers do not blur the Secret Key boundary.
159
+
116
160
  ## Getting product detail
117
161
 
118
162
  The recommended way to fetch a single product is the shaped helper:
@@ -124,7 +168,9 @@ const client = createClient({
124
168
  publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
125
169
  })
126
170
 
127
- const product = await client.commerce.product.detail({ slug: 'every-peach-tee' })
171
+ const product = await client.commerce.product.detail({
172
+ slug: 'every-peach-tee',
173
+ })
128
174
  if (!product) {
129
175
  return notFound() // returned null — product missing, unpublished, or not in this tenant
130
176
  }
@@ -133,14 +179,80 @@ if (!product) {
133
179
 
134
180
  `detail()` returns `ProductDetail | null`. A `null` result covers every "no result" reason: `not_found`, `not_published`, `tenant_mismatch`, `feature_disabled`. Render the same "not available" UI for all four. To recover the exact reason for triage, `404` maps to `null` rather than a thrown error — inspect `client.lastRequestId` and match against backend logs.
135
181
 
182
+ ### Product selection helpers
183
+
184
+ ```typescript
185
+ import {
186
+ buildProductHref,
187
+ buildProductOptionMatrixFromDetail,
188
+ getProductSelectionImages,
189
+ resolveProductSelectionFromMatrix,
190
+ } from '@01.software/sdk'
191
+
192
+ const matrix = buildProductOptionMatrixFromDetail(product)
193
+ const selection = resolveProductSelectionFromMatrix(
194
+ matrix,
195
+ { search: '?opt.color=black&opt.size=s' },
196
+ undefined,
197
+ { detail: product },
198
+ )
199
+
200
+ const images = getProductSelectionImages(selection) // object media only, deduped
201
+ const href = buildProductHref(product, {
202
+ optionSlug: 'color',
203
+ optionValueSlug: 'black',
204
+ })
205
+ ```
206
+
207
+ `availableValuesByOptionSlug` / `availableValuesByOptionId` include
208
+ `availableStock`, `isUnlimited`, and `availableForSale` per value so option UIs
209
+ can render stock state without recalculating from variants.
210
+
136
211
  ### With React Query
137
212
 
138
213
  ```typescript
139
- const { data: product, isLoading } = client.query.useProductDetailBySlug(slug)
214
+ import { createQueryHooks } from '@01.software/sdk/query'
215
+
216
+ const query = createQueryHooks(client)
217
+ const { data: product, isLoading } = query.useProductDetailBySlug(slug)
140
218
  ```
141
219
 
142
220
  Cache key is `['products', 'detail', { slug }]`. Mutations on products, product-variants, product-options, product-option-values, brands, brand-logos, images, and related collections automatically invalidate this cache.
143
221
 
222
+ ### Selection URL contract
223
+
224
+ Use `createProductSelectionCodec(detail)` when product pages need to keep option
225
+ selection in the URL. Complete selections use `variant=<variantId>`; partial
226
+ selections use `opt.<optionId>=<valueId>`. Older
227
+ `opt.<optionSlug>=<valueSlug>` URLs still parse during Stage 1, but slugs are
228
+ compatibility metadata rather than canonical identity.
229
+
230
+ ```typescript
231
+ import {
232
+ createProductSelectionCodec,
233
+ resolveProductSelection,
234
+ } from '@01.software/sdk'
235
+
236
+ const codec = createProductSelectionCodec(product)
237
+ const normalizedSelection = codec.parse('?opt.option-color=color-black')
238
+ const selection = resolveProductSelection(product, normalizedSelection)
239
+ const selectionQuery = codec.stringify(normalizedSelection)
240
+ // selectionQuery === 'opt.option-color=color-black' for partial selections
241
+ // selectionQuery === 'variant=variant-black-large' once a complete variant is selected
242
+ // selection.selectedVariant, selection.price, selection.stock, selection.media
243
+ ```
244
+
245
+ Use IDs from `detail.options[].id` and `detail.options[].values[].id` when
246
+ building selection state. Slugs remain useful for display and old inbound URLs,
247
+ but new outbound URLs should use the codec output.
248
+
249
+ Do not use bare option query keys such as `?size=large`. The SDK rejects them
250
+ as ambiguous because product pages commonly share URLs with unrelated search,
251
+ filter, analytics, or framework parameters. Namespacing selection keys under
252
+ `opt.` lets the codec distinguish product-option state from ordinary query
253
+ parameters while still allowing unrelated parameters such as `utm_campaign` to
254
+ coexist without being interpreted as selection state.
255
+
144
256
  ## Advanced: direct Payload queries (escape hatch)
145
257
 
146
258
  Most consumers should use the helper APIs above (`commerce.product.detail`, etc.). The query builder below is the escape hatch for advanced cases the helpers do not cover: bulk operations, custom filter combinations, or fields the helper response does not expose.
@@ -244,29 +356,40 @@ export default async function ProductPage({ params }) {
244
356
  ```typescript
245
357
  const client = createClient({
246
358
  publishableKey: string, // Required
359
+ apiUrl?: string, // Optional API origin override
247
360
  })
248
361
 
249
362
  const server = createServerClient({
250
363
  publishableKey: string,
251
364
  secretKey: string, // sk01_... or pat01_...
365
+ apiUrl?: string, // Optional API origin override
252
366
  })
253
367
  ```
254
368
 
255
- | Option | Type | Description |
256
- | ---------------- | -------- | ---------------------------- |
257
- | `publishableKey` | `string` | API publishable key |
258
- | `secretKey` | `string` | API secret key (server only) |
369
+ | Option | Type | Description |
370
+ | ---------------- | -------- | ------------------------------------------------------------- |
371
+ | `publishableKey` | `string` | API publishable key |
372
+ | `secretKey` | `string` | API secret key or PAT (server only) |
373
+ | `apiUrl` | `string` | Optional API origin override for staging, preview, or proxies |
374
+
375
+ Use `apiUrl: string` when an SDK instance should target a non-default API
376
+ origin.
259
377
 
260
- API URL 환경변수로 오버라이드 가능합니다:
378
+ API URL resolution order:
261
379
 
262
- - `SOFTWARE_API_URL` (서버용) 또는 `NEXT_PUBLIC_SOFTWARE_API_URL` (브라우저용)
263
- - 미설정 시: dev 빌드(`@dev` 태그) `api-dev.01.software`, 정식 릴리즈는 `api.01.software`
380
+ 1. Explicit `apiUrl` passed to `createClient()` or `createServerClient()`
381
+ 2. `SOFTWARE_API_URL` (server) or `NEXT_PUBLIC_SOFTWARE_API_URL` (browser)
382
+ 3. Build-time default: `DEFAULT_API_URL` when injected at build time; otherwise dev-tagged SDK builds (`-dev.` versions) use `https://api.stg.01.software`, and regular releases use `https://api.01.software`
264
383
 
265
384
  ### Query Builder
266
385
 
267
386
  Access collections via `client.collections.from(slug)`.
268
387
 
269
- > **Note:** `client.collections.from()` returns a `ReadOnlyQueryBuilder` (only `find`, `findById`, `count`, `findMetadata`, `findMetadataById`). Write operations (`create`, `update`, `remove`, `updateMany`, `removeMany`) are only available on `server.collections.from()`.
388
+ > **Note:** the root `client.collections.from()` type exposes the lightweight
389
+ > read surface (`find`, `findById`, `count`). Metadata helpers live behind the
390
+ > optional `@01.software/sdk/metadata` entry, and write operations (`create`,
391
+ > `update`, `remove`, `updateMany`, `removeMany`) are only available on
392
+ > `server.collections.from()`.
270
393
 
271
394
  ```typescript
272
395
  // List query - returns PayloadFindResponse
@@ -295,11 +418,17 @@ const product = await client.collections.from('products').findById(id, {
295
418
 
296
419
  // Single item query - returns document directly
297
420
  const product = await client.collections.from('products').findById('id')
421
+ ```
422
+
423
+ Raw collection mutations are an escape hatch. For ecommerce product catalog
424
+ writes, prefer `server.commerce.product.upsert()` so options, option-values,
425
+ and variants are written through the domain transaction.
298
426
 
427
+ ```typescript
299
428
  // Create (server only) - returns PayloadMutationResponse
300
429
  const { doc, message } = await server.collections
301
- .from('products')
302
- .create({ name: 'Product' })
430
+ .from('articles')
431
+ .create({ title: 'Article' })
303
432
 
304
433
  // Create with file upload (server only) - uses multipart/form-data
305
434
  const { doc } = await server.collections
@@ -308,8 +437,8 @@ const { doc } = await server.collections
308
437
 
309
438
  // Update (server only) - returns PayloadMutationResponse
310
439
  const { doc } = await server.collections
311
- .from('products')
312
- .update('id', { name: 'Updated' })
440
+ .from('articles')
441
+ .update('id', { title: 'Updated article' })
313
442
 
314
443
  // Update with file replacement (server only)
315
444
  await server.collections
@@ -317,28 +446,26 @@ await server.collections
317
446
  .update('id', { alt: 'New alt' }, { file: newFile })
318
447
 
319
448
  // Delete (server only) - returns document directly
320
- const deletedDoc = await server.collections.from('products').remove('id')
449
+ const deletedDoc = await server.collections.from('articles').remove('id')
321
450
 
322
451
  // Count
323
452
  const { totalDocs } = await client.collections.from('products').count()
324
453
 
325
- // SEO Metadata (fetch + generate in one call, depth: 1 auto-applied)
326
- const metadata = await client.collections
327
- .from('products')
328
- .findMetadata(
329
- { where: { slug: { equals: 'my-product' } } },
330
- { siteName: 'My Store' },
331
- )
454
+ // SEO Metadata (generate from a fetched document)
455
+ import { extractSeo, generateMetadata } from '@01.software/sdk/metadata'
332
456
 
333
- const metadataById = await client.collections
334
- .from('products')
335
- .findMetadataById('id', {
336
- siteName: 'My Store',
337
- })
457
+ const { docs } = await client.collections.from('products').find({
458
+ where: { slug: { equals: 'my-product' } },
459
+ limit: 1,
460
+ depth: 1,
461
+ })
462
+ const metadata = docs[0]
463
+ ? generateMetadata(extractSeo(docs[0]), { siteName: 'My Store' })
464
+ : null
338
465
 
339
466
  // Bulk operations (server only)
340
- await server.collections.from('products').updateMany(where, data)
341
- await server.collections.from('products').removeMany(where)
467
+ await server.collections.from('articles').updateMany(where, data)
468
+ await server.collections.from('articles').removeMany(where)
342
469
  ```
343
470
 
344
471
  ### API Response Types (Payload Native)
@@ -370,95 +497,112 @@ interface PayloadMutationResponse<T> {
370
497
  // findById() / remove() returns T (document directly)
371
498
  ```
372
499
 
373
- | Operation | Response Type |
374
- | -------------------- | ------------------------------------------------------------------ |
375
- | `find()` | `PayloadFindResponse<T>` - `{ docs, totalDocs, hasNextPage, ... }` |
376
- | `findById()` | `T` - document object directly |
377
- | `create()` | `PayloadMutationResponse<T>` - `{ doc, message }` |
378
- | `update()` | `PayloadMutationResponse<T>` - `{ doc, message }` |
379
- | `remove()` | `T` - deleted document object directly |
380
- | `count()` | `{ totalDocs: number }` |
381
- | `findMetadata()` | `Metadata \| null` - Next.js Metadata (null if no match) |
382
- | `findMetadataById()` | `Metadata` - Next.js Metadata (throws on 404) |
500
+ | Operation | Response Type |
501
+ | ------------ | ------------------------------------------------------------------ |
502
+ | `find()` | `PayloadFindResponse<T>` - `{ docs, totalDocs, hasNextPage, ... }` |
503
+ | `findById()` | `T` - document object directly |
504
+ | `create()` | `PayloadMutationResponse<T>` - `{ doc, message }` |
505
+ | `update()` | `PayloadMutationResponse<T>` - `{ doc, message }` |
506
+ | `remove()` | `T` - deleted document object directly |
507
+ | `count()` | `{ totalDocs: number }` |
383
508
 
384
509
  ### React Query Hooks
385
510
 
386
- Read hooks are available on both `Client` and `ServerClient` via `client.query.*`. Mutation hooks (`useCreate`, `useUpdate`, `useRemove`) are only available on `ServerClient`.
511
+ React Query helpers are opt-in through `@01.software/sdk/query`. Install
512
+ `@tanstack/react-query` (and React peers) only when your app imports this
513
+ sub-path. Browser components should use `createQueryHooks(client)` for
514
+ browser-safe reads and customer auth hooks. Collection writes belong in trusted
515
+ server code via `createServerClient`.
387
516
 
388
517
  ```typescript
518
+ import { createQueryHooks } from '@01.software/sdk/query'
519
+
520
+ const query = createQueryHooks(client)
521
+
389
522
  // List query
390
- const { data, isLoading } = client.query.useQuery({
523
+ const { data, isLoading } = query.useQuery({
391
524
  collection: 'products',
392
525
  options: { limit: 10 },
393
526
  })
394
527
 
395
528
  // Suspense mode
396
- const { data } = client.query.useSuspenseQuery({
529
+ const { data } = query.useSuspenseQuery({
397
530
  collection: 'products',
398
531
  options: { limit: 10 },
399
532
  })
400
533
 
401
534
  // Query by ID
402
- const { data } = client.query.useQueryById({
535
+ const { data } = query.useQueryById({
403
536
  collection: 'products',
404
537
  id: 'product_id',
405
538
  })
406
539
 
407
540
  // Infinite scroll
408
- const { data, fetchNextPage, hasNextPage } = client.query.useInfiniteQuery({
541
+ const { data, fetchNextPage, hasNextPage } = query.useInfiniteQuery({
409
542
  collection: 'products',
410
543
  options: { limit: 20 },
411
544
  })
412
545
 
413
- // Mutation hooks — ServerClient only (auto-invalidate cache on success)
414
- const { mutate: create } = client.query.useCreate({ collection: 'images' })
415
- const { mutate: update } = client.query.useUpdate({ collection: 'products' })
416
- const { mutate: remove } = client.query.useRemove({ collection: 'products' })
417
-
418
- create({ data: { alt: 'Hero' }, file: imageFile, filename: 'hero.jpg' })
419
- update({ id: 'product_id', data: { title: 'Updated' } })
420
- remove('product_id')
421
-
422
546
  // SSR Prefetch
423
- await client.query.prefetchQuery({
547
+ await query.prefetchQuery({
424
548
  collection: 'products',
425
549
  options: { limit: 10 },
426
550
  })
427
- await client.query.prefetchQueryById({
551
+ await query.prefetchQueryById({
428
552
  collection: 'products',
429
553
  id: 'product_id',
430
554
  })
431
- await client.query.prefetchInfiniteQuery({
555
+ await query.prefetchInfiniteQuery({
432
556
  collection: 'products',
433
557
  pageSize: 20,
434
558
  })
435
559
 
436
560
  // Cache utilities
437
- client.query.invalidateQueries('products')
438
- client.query.getQueryData('products', 'list', options)
439
- client.query.setQueryData('products', 'detail', id, data)
561
+ query.invalidateQueries('products')
562
+ query.getQueryData('products', 'list', options)
563
+ query.setQueryData('products', 'detail', id, data)
440
564
 
441
565
  // Customer auth hooks (Client only)
442
- const { data: profile } = client.query.useCustomerMe()
443
- const { mutate: login } = client.query.useCustomerLogin()
444
- const { mutate: register } = client.query.useCustomerRegister()
445
- const { mutate: logout } = client.query.useCustomerLogout()
566
+ const { data: profile } = query.useCustomerMe()
567
+ const { mutate: login } = query.useCustomerLogin()
568
+ const { mutate: register } = query.useCustomerRegister()
569
+ const { mutate: logout } = query.useCustomerLogout()
446
570
 
447
571
  login({ email: 'user@example.com', password: 'password' })
448
572
 
449
573
  // Other customer mutations
450
- client.query.useCustomerForgotPassword()
451
- client.query.useCustomerResetPassword()
452
- client.query.useCustomerChangePassword()
574
+ query.useCustomerForgotPassword()
575
+ query.useCustomerResetPassword()
576
+ query.useCustomerChangePassword()
453
577
 
454
578
  // Customer cache utilities
455
- client.query.invalidateCustomerQueries()
456
- client.query.getCustomerData()
457
- client.query.setCustomerData(profile)
579
+ query.invalidateCustomerQueries()
580
+ query.getCustomerData()
581
+ query.setCustomerData(profile)
582
+ ```
583
+
584
+ ```typescript
585
+ // Server action / API route for collection writes
586
+ import { createServerClient } from '@01.software/sdk/server'
587
+
588
+ const server = createServerClient({
589
+ publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
590
+ secretKey: process.env.SOFTWARE_SECRET_KEY!,
591
+ })
592
+
593
+ await server.collections.from('articles').update('article_id', {
594
+ title: 'Updated article',
595
+ })
458
596
  ```
459
597
 
460
598
  ### Customer Auth
461
599
 
600
+ Customer auth methods currently cover local email/password flows: register,
601
+ login, refresh, password reset, profile read/update, and password change.
602
+ `CustomerProfile.authProvider` may contain `google`, `apple`, `kakao`, or
603
+ `naver` for accounts created through platform/provider integrations, but the
604
+ SDK does not expose social-login initiation or callback helpers yet.
605
+
462
606
  Available on Client via `client.customer.auth.*`.
463
607
 
464
608
  ```typescript
@@ -548,7 +692,63 @@ await client.commerce.shipping.calculate({ shippingPolicyId?, orderAmount, posta
548
692
 
549
693
  ### Commerce Product
550
694
 
551
- Available on both Client and ServerClient via `commerce.product.*`.
695
+ Product reads are available on both Client and ServerClient via `commerce.product.*`.
696
+ Product catalog writes are ServerClient-only.
697
+ Use `server.commerce.product.upsert()` for product catalog writes that include
698
+ options, option values, and variants. It is the tenant-admin safe path because
699
+ it applies the product/option/variant transaction that raw collection writes do
700
+ not provide.
701
+
702
+ ```typescript
703
+ const result = await server.commerce.product.upsert({
704
+ product: {
705
+ title: 'Every Peach Tee',
706
+ slug: 'every-peach-tee',
707
+ status: 'published',
708
+ },
709
+ options: [
710
+ {
711
+ title: 'Color',
712
+ slug: 'color',
713
+ values: [
714
+ { value: 'Black', slug: 'black', swatchColor: '#111111' },
715
+ { value: 'White', slug: 'white', swatchColor: '#ffffff' },
716
+ ],
717
+ },
718
+ {
719
+ title: 'Size',
720
+ slug: 'size',
721
+ values: [
722
+ { value: 'Small', slug: 's' },
723
+ { value: 'Medium', slug: 'm' },
724
+ ],
725
+ },
726
+ ],
727
+ variants: [
728
+ {
729
+ optionValues: { color: { valueSlug: 'black' }, size: { valueSlug: 's' } },
730
+ sku: 'TEE-BLK-S',
731
+ price: 29000,
732
+ stock: 10,
733
+ isActive: true,
734
+ },
735
+ {
736
+ optionValues: { color: { valueSlug: 'white' }, size: { valueSlug: 'm' } },
737
+ sku: 'TEE-WHT-M',
738
+ price: 29000,
739
+ stock: 8,
740
+ isActive: true,
741
+ },
742
+ ],
743
+ })
744
+
745
+ if (!result.ok) {
746
+ throw new Error(result.message)
747
+ }
748
+ ```
749
+
750
+ For updates to existing options or option-values, prefer `id` / `valueId` when
751
+ available so rename-safe updates do not depend on slugs.
552
752
 
553
753
  ```typescript
554
754
  // Batch stock check (point-in-time read, NOT a reservation)
@@ -654,7 +854,7 @@ Source of truth: `packages/sdk/src/core/collection/const.ts` (`COLLECTIONS`: 73)
654
854
  | Live Streams | `live-streams` |
655
855
  | Media | `images` |
656
856
  | Forms | `forms`, `form-submissions` |
657
- | Community | `posts`, `comments`, `reactions`, `reaction-types`, `bookmarks`, `post-categories` |
857
+ | Community | `posts`, `comments`, `reactions`, `reaction-types`, `bookmarks`, `post-categories` |
658
858
  | Events | `event-calendars`, `events`, `event-categories`, `event-occurrences`, `event-tags` |
659
859
 
660
860
  Server-only collections: `customer-groups`, `reports`, and `community-bans`
@@ -28,7 +28,10 @@ module.exports = __toCommonJS(react_exports);
28
28
  var import_react = require("react");
29
29
 
30
30
  // src/core/client/types.ts
31
- function resolveApiUrl() {
31
+ function resolveApiUrl(apiUrl) {
32
+ if (apiUrl) {
33
+ return apiUrl.replace(/\/$/, "");
34
+ }
32
35
  if (typeof process !== "undefined" && process.env) {
33
36
  const envUrl = process.env.SOFTWARE_API_URL || process.env.NEXT_PUBLIC_SOFTWARE_API_URL;
34
37
  if (envUrl) {