@01.software/sdk 0.46.1 → 0.49.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 (57) hide show
  1. package/CHANGELOG.md +96 -1
  2. package/README.md +145 -14
  3. package/dist/client.cjs +485 -81
  4. package/dist/client.cjs.map +1 -1
  5. package/dist/client.d.cts +7 -6
  6. package/dist/client.d.ts +7 -6
  7. package/dist/client.js +485 -81
  8. package/dist/client.js.map +1 -1
  9. package/dist/{collection-client-BEGXnfQe.d.ts → collection-client-DnDt6EQz.d.ts} +4 -4
  10. package/dist/{collection-client-L2rEgBaR.d.cts → collection-client-XfGLPuTf.d.cts} +4 -4
  11. package/dist/{const-y0eipvnQ.d.cts → const-CBba4lDo.d.cts} +4 -4
  12. package/dist/{const-GYVwlXLr.d.ts → const-DCNV5Kq7.d.ts} +4 -4
  13. package/dist/errors.cjs +112 -16
  14. package/dist/errors.cjs.map +1 -1
  15. package/dist/errors.d.cts +13 -4
  16. package/dist/errors.d.ts +13 -4
  17. package/dist/errors.js +112 -16
  18. package/dist/errors.js.map +1 -1
  19. package/dist/{index-BfYL6Qb1.d.cts → index-CbKyoEzy.d.cts} +3 -3
  20. package/dist/{index-Bei4WlVw.d.ts → index-DUuIqUS8.d.ts} +3 -3
  21. package/dist/index.cjs +452 -104
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +8 -8
  24. package/dist/index.d.ts +8 -8
  25. package/dist/index.js +452 -104
  26. package/dist/index.js.map +1 -1
  27. package/dist/{payload-types-3WNyC0gk.d.cts → payload-types-MKbx5MuJ.d.cts} +309 -17
  28. package/dist/{payload-types-3WNyC0gk.d.ts → payload-types-MKbx5MuJ.d.ts} +309 -17
  29. package/dist/query.cjs +165 -43
  30. package/dist/query.cjs.map +1 -1
  31. package/dist/query.d.cts +5 -5
  32. package/dist/query.d.ts +5 -5
  33. package/dist/query.js +165 -43
  34. package/dist/query.js.map +1 -1
  35. package/dist/realtime.d.cts +2 -2
  36. package/dist/realtime.d.ts +2 -2
  37. package/dist/server.cjs +566 -81
  38. package/dist/server.cjs.map +1 -1
  39. package/dist/server.d.cts +8 -7
  40. package/dist/server.d.ts +8 -7
  41. package/dist/server.js +566 -81
  42. package/dist/server.js.map +1 -1
  43. package/dist/storefront-cache.cjs +70 -1
  44. package/dist/storefront-cache.cjs.map +1 -1
  45. package/dist/storefront-cache.js +70 -1
  46. package/dist/storefront-cache.js.map +1 -1
  47. package/dist/{types-mIROvhhP.d.ts → types-1UeOk1Uy.d.cts} +110 -8
  48. package/dist/{types-CAicTn-o.d.cts → types-BKr_2avj.d.cts} +2 -2
  49. package/dist/{types-CDJ2M-MJ.d.cts → types-Du4s5anw.d.ts} +110 -8
  50. package/dist/{types-DpSyNTxy.d.ts → types-L_nh-ED7.d.ts} +2 -2
  51. package/dist/ui/form.d.cts +1 -1
  52. package/dist/ui/form.d.ts +1 -1
  53. package/dist/ui/video.d.cts +1 -1
  54. package/dist/ui/video.d.ts +1 -1
  55. package/dist/webhook.d.cts +4 -4
  56. package/dist/webhook.d.ts +4 -4
  57. package/package.json +6 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,100 @@
1
1
  # @01.software/sdk
2
2
 
3
+ ## 0.49.0
4
+
5
+ ### Minor Changes
6
+
7
+ - aef44ec: Fix SDK error guards across independently bundled entrypoints and expose the guards from the client and server entries.
8
+ - 86f74d3: Expose immutable membership subscribe-admission snapshot fields in the generated SDK types.
9
+ - edc790e: BREAKING (0.x minor): Checkout `paymentState.activePaymentSession.provider` can
10
+ now be `portone`. Consumers using `@01.software/sdk` 0.45.0, 0.46.0, or 0.46.1
11
+ must upgrade to the 0.49.0 release produced by this minor changeset, or later,
12
+ before the server exposes managed PortOne sessions; otherwise
13
+ `checkouts.retrieve` and `checkouts.query` throw during strict response
14
+ validation. SDK 0.48.0's runtime decoder already accepts `portone`, but its
15
+ published TypeScript `CheckoutPaymentSessionRef` remains `provider: 'toss'`, so
16
+ 0.48.0 is not the fully compatible public-contract floor. `checkouts.create` is
17
+ not part of this break because the current producer does not return an active
18
+ PortOne session reference.
19
+
20
+ Keep custom bridge recovery on the server-only exact lookup.
21
+
22
+ - fa71a21: Add a fail-closed PortOne Standard V2 Toss Payments CARD browser action with a required redirect, PG-safe payment identity, and durable single-slot retry semantics.
23
+ - 5ea6348: Expose non-commerce API reasons through `SDKError.apiReason` and the family-aware `getSDKErrorReason` accessor while preserving strict commerce error typing.
24
+
25
+ ### Patch Changes
26
+
27
+ - cb2a742: Clarify that idempotent commerce mutation response fields do not provide exactly-once side-effect semantics.
28
+ - 15c1024: Synchronize generated Payload types for the internal payment-kernel result and owner-commit records without exposing those collections through a public SDK collection bucket.
29
+
30
+ ## 0.48.0
31
+
32
+ ### Minor Changes
33
+
34
+ - 715fe57: Expose active tenant custom-field structure in the optional version-1
35
+ collection schema response and preserve it through Agent CLI schema output.
36
+ - b23608d: Preserve canonical strict commerce errors through the mounted Console handlers and public Cart, Checkout, and PaymentSession facades, including the additive handler reason vocabulary.
37
+
38
+ ### Patch Changes
39
+
40
+ - 3096b74: Align the SDK build and test toolchain with the Payload release that removes
41
+ the vulnerable archived image parser dependency.
42
+ - d98f7b3: Synchronize generated Payload types for internal settlement receipt and reconciliation evidence. These collections remain absent from every public SDK collection bucket and add no public SDK API.
43
+
44
+ ## 0.47.0
45
+
46
+ ### Minor Changes
47
+
48
+ - 5a523e8: Add the managed PortOne Hosted v2 payment-session flow, provider-specific
49
+ checkout action parsing, CLI dispatch, and compatible event-template recovery
50
+ behavior. Expose the full current Hosted v2 payment-method vocabulary while
51
+ preserving per-connection allowlists.
52
+ - 5e68929: Add a browser-safe shaped `community.getPost({ postId })` helper while keeping
53
+ raw community posts server-only, and update the generated community template to
54
+ consume that helper without a raw collection workaround.
55
+ - 5a523e8: Add a server-only exact custom PaymentSession recovery lookup by provider
56
+ session or payment identity.
57
+ - 5a523e8: Expose the additive `tenantPurpose` and `testExpiresAt` fields on the generated
58
+ `Tenant` type for time-limited, externally fenced test tenants.
59
+ - d63c3c8: Reclassify `tenants` from the browser-public `COLLECTIONS` bucket to
60
+ `SERVER_ONLY_COLLECTIONS`. The tenant document denies reads without an
61
+ authenticated user, so a publishable key never reached it; the bucket promised a
62
+ browser surface that always failed at runtime. `tenant-metadata` remains the
63
+ browser-public tenant identity slug, and server (secret key) access to `tenants`
64
+ is unchanged because `SERVER_COLLECTIONS` still contains it.
65
+
66
+ Runtime behavior changes for one case, and the change is a narrowing.
67
+ `SERVER_ONLY_COLLECTIONS` is a runtime guard input, not only a type: the Console
68
+ REST route imports it and refuses a publishable-key raw read of any member. A
69
+ browser request to `/api/tenants` that previously reached Payload and was denied
70
+ by collection access now stops at that guard and returns **404** instead of 403.
71
+ Both were failures before and after; only the status and the point of denial
72
+ move. No caller could have succeeded on this path.
73
+
74
+ Type-level impact: `tenants` leaves the `PublicCollection` union, so a browser
75
+ client typed against it now fails to compile instead of failing at request time.
76
+
77
+ ### Patch Changes
78
+
79
+ - 05f7b8d: Send the publishable-key raw-read safety query when listing community post
80
+ categories, post tags, and reaction types.
81
+ - 2fd5f57: Synchronize generated types for the new Discount operator field descriptions (validity window, scope target requirement, usage limit minimums, unsupported each-line allocation). Comment-only; the Discount type shape and every public wire contract are unchanged.
82
+ - 5a523e8: Synchronize generated types for the write-only provider-credential inputs on the SDK-hidden `commerce-payment-accounts` collection. The public SDK surface is unchanged: the collection remains internal, and the two added fields are virtual Admin-only inputs that are never persisted or returned.
83
+ - 07db902: Send the publishable-key raw-read safety query when listing or resolving
84
+ community customer profile lists.
85
+ - 309418a: Correct three drifted README examples. The realtime sub-path import now names
86
+ `RealtimeConnection`, which the package actually exports (`createRealtimeClient`
87
+ does not exist), and publishable-key placeholders use the real `pk01_` prefix
88
+ instead of `pk_xxx` / `pk_live_...`. Documentation only; no runtime or type
89
+ change.
90
+ - c5e896c: Correct the generated `ShippingPolicy` field documentation. The
91
+ `freeShippingMinAmount` doc comment previously pointed at the shipping rates
92
+ array, which owns no free-shipping condition; it now names the two surfaces
93
+ that do — an order-amount rate row priced at 0, or an automatic
94
+ `free_shipping` discount with a minimum order amount. `isActive` and
95
+ `isDefault` gain doc comments describing their pricing side effects. Types and
96
+ runtime behavior are unchanged.
97
+
3
98
  ## 0.46.1
4
99
 
5
100
  ### Patch Changes
@@ -993,7 +1088,7 @@ displayStatus` JSDoc comment from `Order.status`. The Console removed that
993
1088
  customer auth rather than public request body input. **Breaking:** **`POST
994
1089
  /api/event-registrations/register` rejects `body.customer` with `400
995
1090
  bad_request`;** bind customers with `Authorization: Bearer` only. See
996
- `docs/events/public-api-contract.md` for migration. Guest-token lookup/cancel
1091
+ `docs/events/public-api-reference.md` for migration. Guest-token lookup/cancel
997
1092
  helpers send tokens in POST bodies. Range helpers support calendar, category,
998
1093
  and tag filters aligned with the Console public range endpoint. Exported
999
1094
  structural `RootClient` / `RootServerClient` keep `events` optional for
package/README.md CHANGED
@@ -29,7 +29,7 @@ pnpm add @01.software/sdk
29
29
  import { createClient } from '@01.software/sdk'
30
30
 
31
31
  // Server-only entry - keep Secret Key code out of browser-facing imports
32
- import { createServerClient } from '@01.software/sdk/server'
32
+ import { createServerClient, isCommerceSDKError } from '@01.software/sdk/server'
33
33
 
34
34
  // Webhook only - webhook handlers
35
35
  import {
@@ -38,7 +38,7 @@ import {
38
38
  } from '@01.software/sdk/webhook'
39
39
 
40
40
  // Realtime only
41
- import { createRealtimeClient } from '@01.software/sdk/realtime'
41
+ import { RealtimeConnection } from '@01.software/sdk/realtime'
42
42
 
43
43
  // Storefront cache resource names for SSG/ISR adapters
44
44
  import { storefrontCacheResources } from '@01.software/sdk/storefront-cache'
@@ -61,8 +61,8 @@ entry.
61
61
  | Import | Feature(s) | Install when used |
62
62
  | ----------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
63
63
  | `@01.software/sdk` | browser-safe `createClient`, commerce helpers, collection helpers, types | none |
64
- | `@01.software/sdk/client` | browser-safe `createClient` entry | none |
65
- | `@01.software/sdk/server` | `createServerClient`, server-only collection, commerce, and preview APIs | none; keep `secretKey` code on the server |
64
+ | `@01.software/sdk/client` | browser-safe `createClient` entry and SDK error guards | none |
65
+ | `@01.software/sdk/server` | `createServerClient`, server-only APIs, and SDK error guards | none; keep `secretKey` code on the server |
66
66
  | `@01.software/sdk/errors` | SDK error classes and guards | none |
67
67
  | `@01.software/sdk/analytics` | browser analytics client and typed event helpers | none |
68
68
  | `@01.software/sdk/metadata` | SEO metadata extraction and generation helpers | none |
@@ -92,6 +92,9 @@ Migration quick reference:
92
92
  - `createClient` remains available from `@01.software/sdk` and
93
93
  `@01.software/sdk/client`.
94
94
  - `createServerClient` must be imported from `@01.software/sdk/server`.
95
+ - Error guards are available from the same `@01.software/sdk/client` or
96
+ `@01.software/sdk/server` entry as the client that throws; the root and
97
+ `@01.software/sdk/errors` guards also classify errors across entries.
95
98
  - React Query hooks and cache helpers must be imported from
96
99
  `@01.software/sdk/query`.
97
100
  - Product storefront cache resource helpers must be imported from
@@ -109,7 +112,7 @@ Migration quick reference:
109
112
  // Pageviews + custom events (browser)
110
113
  import { createAnalytics } from '@01.software/sdk/analytics'
111
114
 
112
- const analytics = createAnalytics({ publishableKey: 'pk_xxx' })
115
+ const analytics = createAnalytics({ publishableKey: 'pk01_xxx' })
113
116
  // auto-tracks pageviews; call analytics.pageview('/custom-path') manually if needed
114
117
  analytics.track('signup', { plan: 'pro', trial: false })
115
118
  ```
@@ -137,7 +140,7 @@ type ShopEvents = {
137
140
  checkout_start: undefined // no props
138
141
  }
139
142
 
140
- const analytics = createAnalytics<ShopEvents>({ publishableKey: 'pk_xxx' })
143
+ const analytics = createAnalytics<ShopEvents>({ publishableKey: 'pk01_xxx' })
141
144
  analytics.track('signup', { plan: 'pro', trial: false }) // ✅
142
145
  analytics.track('checkout_start') // ✅ no props
143
146
  // analytics.track('signpu', { plan: 'pro', trial: false }) // ❌ unknown event
@@ -145,7 +148,7 @@ analytics.track('checkout_start') // ✅ no props
145
148
 
146
149
  // Bind the map once to avoid repeating the generic:
147
150
  const create = defineAnalyticsEvents<ShopEvents>()
148
- const a2 = create({ publishableKey: 'pk_xxx' })
151
+ const a2 = create({ publishableKey: 'pk01_xxx' })
149
152
  ```
150
153
 
151
154
  **React.** Use `AnalyticsProvider` + `useAnalytics()` to fire events from
@@ -256,10 +259,12 @@ without calling the Console endpoint directly:
256
259
 
257
260
  ```typescript
258
261
  const context = await server.tenant.context()
262
+ const productSchema = await server.tenant.collectionSchema('products')
259
263
 
260
264
  console.log(context.features)
261
265
  console.log(context.collections.active)
262
266
  console.log(context.fieldConfigs)
267
+ console.log(productSchema.collection.customFields)
263
268
  ```
264
269
 
265
270
  Pass `{ includeCounts: true }` only when document counts and webhook
@@ -267,6 +272,12 @@ configuration are needed. That mode bypasses the endpoint cache and performs
267
272
  additional collection reads. Tenant introspection is server-only and is not
268
273
  exported from the browser-safe root entry.
269
274
 
275
+ `collection.customFields` is optional and currently applies to `customers` and
276
+ `products`. It exposes `storagePath: 'customData'`, active definition machine
277
+ keys and types, optional `required: true`, and select option storage values.
278
+ Operator labels and stored document values are excluded; this schema metadata
279
+ does not grant collection read or write access.
280
+
270
281
  Server-rendered preview routes can use `server.preview.detail()` with the
271
282
  short-lived preview token issued by Console:
272
283
 
@@ -714,7 +725,7 @@ Membership self-service is split by credential boundary:
714
725
  import { createClient } from '@01.software/sdk'
715
726
  import { createServerClient } from '@01.software/sdk/server'
716
727
 
717
- const client = createClient({ publishableKey: 'pk_live_...' })
728
+ const client = createClient({ publishableKey: 'pk01_...' })
718
729
 
719
730
  const membership = await client.memberships.me()
720
731
  if (
@@ -725,7 +736,7 @@ if (
725
736
  }
726
737
 
727
738
  const server = createServerClient({
728
- publishableKey: 'pk_live_...',
739
+ publishableKey: 'pk01_...',
729
740
  secretKey: process.env.SOFTWARE_SECRET_KEY!,
730
741
  })
731
742
 
@@ -1317,10 +1328,8 @@ Treat a replayed Cart result as an operation receipt: never replace a newer
1317
1328
  cached Cart with a lower `revision`; retrieve when response ordering is unclear.
1318
1329
 
1319
1330
  ```typescript
1320
- import {
1321
- createCommerceIdempotencyKey,
1322
- isCommerceSDKError,
1323
- } from '@01.software/sdk'
1331
+ import { createCommerceIdempotencyKey } from '@01.software/sdk'
1332
+ import { isCommerceSDKError } from '@01.software/sdk/errors'
1324
1333
 
1325
1334
  const checkoutIdempotencyKey = createCommerceIdempotencyKey()
1326
1335
  const { checkout, checkoutToken } = await client.commerce.checkouts.create({
@@ -1345,12 +1354,57 @@ if (
1345
1354
  // Initialize the Toss widget with the returned public client/variant fields.
1346
1355
  }
1347
1356
 
1357
+ // PortOne Hosted V2 uses the same canonical lifecycle with a provider-specific
1358
+ // HTTPS action. Redirect completion is not payment finality; retrieve the
1359
+ // PaymentSession and let Console converge from PortOne's authenticated API.
1360
+ const portOneSession = await client.commerce.paymentSessions.createPortOne({
1361
+ checkoutToken,
1362
+ provider: 'portone',
1363
+ providerOptions: { profile: 'hosted_v2', paymentMethod: 'CARD_KR' },
1364
+ idempotencyKey: createCommerceIdempotencyKey(),
1365
+ })
1366
+ if (
1367
+ portOneSession.presentationStatus === 'actionable' &&
1368
+ portOneSession.nextAction.type === 'portone_checkout'
1369
+ ) {
1370
+ window.location.assign(portOneSession.nextAction.checkoutUrl)
1371
+ }
1372
+
1373
+ // Standard V2 is a distinct Browser SDK capability, not a Hosted fallback.
1374
+ // Do not treat the SDK result or mobile redirect as payment finality.
1375
+ const standardPortOneSession =
1376
+ await client.commerce.paymentSessions.createPortOne({
1377
+ checkoutToken,
1378
+ provider: 'portone',
1379
+ providerOptions: { profile: 'standard_v2', paymentMethod: 'CARD' },
1380
+ idempotencyKey: createCommerceIdempotencyKey(),
1381
+ })
1382
+ if (
1383
+ standardPortOneSession.presentationStatus === 'actionable' &&
1384
+ standardPortOneSession.nextAction.type === 'portone_sdk'
1385
+ ) {
1386
+ // PortOne V2 requires its currency constant (`CURRENCY_KRW`), not ISO `KRW`.
1387
+ // await PortOne.requestPayment(standardPortOneSession.nextAction)
1388
+ // Always retrieve the session afterwards; Console lookup/webhook is authoritative.
1389
+ }
1390
+
1348
1391
  // Browser recovery requires the parent capability.
1349
1392
  await client.commerce.paymentSessions.retrieve({
1350
1393
  paymentSessionId: paymentSession.id,
1351
1394
  checkoutToken,
1352
1395
  })
1353
1396
 
1397
+ await client.commerce.paymentSessions.retrievePortOne({
1398
+ paymentSessionId: portOneSession.id,
1399
+ checkoutToken,
1400
+ })
1401
+
1402
+ // The server equivalent requires a write-capable credential because the
1403
+ // provider-authoritative lookup can converge and finalize domain state.
1404
+ await server.commerce.paymentSessions.retrievePortOne({
1405
+ paymentSessionId: portOneSession.id,
1406
+ })
1407
+
1354
1408
  // Toss confirmation is a typed server-only adapter action. There is no generic
1355
1409
  // `confirmVerified` or provider-paid Order creation escape hatch.
1356
1410
  const confirmationIdempotencyKey = createCommerceIdempotencyKey()
@@ -1389,6 +1443,12 @@ authoritative API response or signed webhook, then report the exact captured
1389
1443
  identity and Money. The create response is intentionally limited to
1390
1444
  `{ id, provider, providerSessionId, money }`.
1391
1445
 
1446
+ Custom bridge sessions intentionally remain `null` in
1447
+ `checkout.paymentState.activePaymentSession`: Checkout exposes only managed
1448
+ adapters from its closed provider vocabulary. Persist the `providerSessionId`
1449
+ returned by `custom.create` and use `custom.retrieve` with that identity as the
1450
+ supported custom-lane settlement recovery path.
1451
+
1392
1452
  ```typescript
1393
1453
  const customSession = await server.commerce.paymentSessions.custom.create({
1394
1454
  provider: 'my-provider',
@@ -1396,6 +1456,12 @@ const customSession = await server.commerce.paymentSessions.custom.create({
1396
1456
  idempotencyKey: createCommerceIdempotencyKey(),
1397
1457
  })
1398
1458
 
1459
+ // A recovery path can resolve the custom session without a 01 session ID.
1460
+ const pending = await server.commerce.paymentSessions.custom.retrieve({
1461
+ provider: customSession.provider,
1462
+ by: { providerSessionId: customSession.providerSessionId },
1463
+ })
1464
+
1399
1465
  const verifiedCapture = await verifyMyProviderCapture({
1400
1466
  expectedAmount: customSession.money.amountMinor,
1401
1467
  expectedCurrency: customSession.money.currency,
@@ -1416,8 +1482,22 @@ const customConfirmation = await server.commerce.paymentSessions.custom.confirm(
1416
1482
  )
1417
1483
 
1418
1484
  console.log(customConfirmation.orderId)
1485
+
1486
+ const recovered = await server.commerce.paymentSessions.custom.retrieve({
1487
+ provider: customSession.provider,
1488
+ by: { providerPaymentId: verifiedCapture.paymentId },
1489
+ })
1490
+ if (recovered.finalizationStatus === 'committed') {
1491
+ console.log(recovered.orderId)
1492
+ }
1419
1493
  ```
1420
1494
 
1495
+ `custom.retrieve` is an exact lookup by either `providerSessionId` or
1496
+ `providerPaymentId` (exactly one). It returns only bounded recovery state:
1497
+ provider identity, session/provider/finalization status, Money, and the resolved
1498
+ `orderId`. It does not list sessions or expose raw PaymentSession relations,
1499
+ provider evidence, credentials, or fingerprints.
1500
+
1421
1501
  If the tenant's custom-provider policy requires HMAC, pass
1422
1502
  `{ createProof(rawBody) }` as the second argument to `custom.confirm`. The
1423
1503
  callback receives the exact JSON bytes the SDK sends and returns `{ timestamp,
@@ -1436,6 +1516,38 @@ Canonical commerce error envelopes become `CommerceSDKError`. Branch on its
1436
1516
  stable `reason`, inspect `retryable`, and log `requestId`; a newer server reason
1437
1517
  is preserved as `reason: 'unknown_commerce_error'` plus `rawReason`.
1438
1518
 
1519
+ Non-commerce endpoints preserve their open-ended machine reason separately as
1520
+ `SDKError.apiReason`. Use `getSDKErrorReason(error)` when shared error handling
1521
+ must read either family: it returns the canonical commerce reason, the verbatim
1522
+ unknown commerce `rawReason`, or the non-commerce `apiReason`. Existing
1523
+ `details` payloads remain available for compatibility, but consumers no longer
1524
+ need to normalize `details.reason` and `details.body.reason` themselves.
1525
+
1526
+ #### Idempotent mutation results are not exactly-once receipts
1527
+
1528
+ No commerce mutation response field is a safe exactly-once gate for a
1529
+ non-idempotent side effect such as an operational alert, outbound webhook, or
1530
+ ledger entry. Use a consumer-owned durable claim keyed by the side effect's
1531
+ stable logical identity. The public webhook guide's
1532
+ [`processOnce`](https://docs.01.software/developers/webhooks) pattern uses a
1533
+ database unique key, queue idempotency store, or provider-safe send ledger.
1534
+
1535
+ `custom.confirm` returns `alreadyConfirmed: true` when the session was already
1536
+ confirmed at observation time. It does not attribute the transition to the
1537
+ current call. The field is absent from the initial successful confirmation, so
1538
+ code that only interprets the response shape must test
1539
+ `'alreadyConfirmed' in result`, not `result.alreadyConfirmed === false`. That
1540
+ presence check is still not a side-effect gate: when the initial commit succeeds
1541
+ but its response is lost, a retry returns `alreadyConfirmed: true` and would
1542
+ otherwise silently miss the transition.
1543
+
1544
+ Likewise, the `orders.cancelOrder` fields `cancelCommitted` and
1545
+ `alreadyCanceled` are domain effect dispositions, not transport replay markers.
1546
+ A same-key retry replays the cached initial 2xx body byte-for-byte and can
1547
+ therefore return the original `cancelCommitted: true` again. Neither field says
1548
+ whether the response was replayed or whether an external side effect has already
1549
+ run.
1550
+
1439
1551
  ### Commerce Orders (post-placement operations)
1440
1552
 
1441
1553
  Provider-paid `orders.create`, `orders.confirmPayment`, and ordinary
@@ -1693,6 +1805,25 @@ replacement API. Pass an empty list to clear codes. Singular `admission`,
1693
1805
  Browser `mine()`/`merge({ guestCartToken })` derive customer identity from the
1694
1806
  JWT; server calls require an explicit `customerToken`.
1695
1807
 
1808
+ ### Community public reads
1809
+
1810
+ Use the shaped `community` helpers for browser/customer community screens. A
1811
+ publishable key is sufficient for list and detail reads; a customer JWT may be
1812
+ attached for the same response shape, but no secret key is needed.
1813
+
1814
+ ```typescript
1815
+ const page = await client.community.listPosts({ limit: 20 })
1816
+ const post = await client.community.getPost({ postId: page.docs[0].id })
1817
+
1818
+ if (!post) {
1819
+ // Missing, hidden, and cross-tenant posts are intentionally indistinguishable.
1820
+ }
1821
+ ```
1822
+
1823
+ `getPost()` returns the allowlisted `CommunityPost` DTO and resolves `404
1824
+ not_found` to `null`. Raw `client.collections.from('posts')` remains
1825
+ server-only; do not use it in browser or customer template code.
1826
+
1696
1827
  ### Community Moderation (ServerClient-only)
1697
1828
 
1698
1829
  Available only on ServerClient via `server.community.moderation.*`.
@@ -1802,7 +1933,7 @@ Source of truth: `packages/published/sdk/src/core/collection/const.ts` (`COLLECT
1802
1933
 
1803
1934
  | Category | Browser-public generic collections |
1804
1935
  | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1805
- | Tenant | `tenants`, `tenant-metadata` |
1936
+ | Tenant | `tenant-metadata` |
1806
1937
  | Products | `products`, `product-variants`, `product-options`, `product-option-values`, `product-categories`, `product-tags`, `product-collections`, `markets`, `brands` |
1807
1938
  | Customers | `customer-profiles` |
1808
1939
  | Commerce | `discounts`, `shipping-policies`, `shipping-zones` |