@01.software/sdk 0.51.0 → 0.65.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 (67) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +228 -102
  3. package/dist/client.cjs +556 -60
  4. package/dist/client.cjs.map +1 -1
  5. package/dist/client.d.cts +6 -6
  6. package/dist/client.d.ts +6 -6
  7. package/dist/client.js +556 -60
  8. package/dist/client.js.map +1 -1
  9. package/dist/{collection-client-CWAeuzV4.d.cts → collection-client-B7rsTtVj.d.cts} +6 -6
  10. package/dist/{collection-client-hYvVfHIm.d.ts → collection-client-C2sMvfme.d.ts} +6 -6
  11. package/dist/{const-JD8obaKg.d.ts → const-CRxPTDGt.d.ts} +2 -2
  12. package/dist/{const-D8LixhAJ.d.cts → const-D6ZRQLxI.d.cts} +2 -2
  13. package/dist/{index-B0Hjh3Ey.d.ts → index-1auUEYrl.d.ts} +24 -7
  14. package/dist/{index-Bu0kCc6o.d.cts → index-Bbpf5t-l.d.cts} +24 -7
  15. package/dist/index.cjs +573 -260
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +19 -12
  18. package/dist/index.d.ts +19 -12
  19. package/dist/index.js +573 -260
  20. package/dist/index.js.map +1 -1
  21. package/dist/{payload-types-BOsckg8B.d.cts → payload-types-BnIET0xU.d.cts} +567 -438
  22. package/dist/{payload-types-BOsckg8B.d.ts → payload-types-BnIET0xU.d.ts} +567 -438
  23. package/dist/query.cjs +62 -3
  24. package/dist/query.cjs.map +1 -1
  25. package/dist/query.d.cts +5 -5
  26. package/dist/query.d.ts +5 -5
  27. package/dist/query.js +62 -3
  28. package/dist/query.js.map +1 -1
  29. package/dist/realtime.d.cts +2 -2
  30. package/dist/realtime.d.ts +2 -2
  31. package/dist/server.cjs +557 -20
  32. package/dist/server.cjs.map +1 -1
  33. package/dist/server.d.cts +14 -8
  34. package/dist/server.d.ts +14 -8
  35. package/dist/server.js +557 -20
  36. package/dist/server.js.map +1 -1
  37. package/dist/{types-5ZlcAFuU.d.ts → types-D1y4TgQm.d.ts} +2 -2
  38. package/dist/{types-Bj8j7VHU.d.ts → types-DnRC3nLG.d.ts} +464 -16
  39. package/dist/{types-B14sEt1z.d.cts → types-Dp1oYpD3.d.cts} +2 -2
  40. package/dist/{types-CM5qiR2k.d.cts → types-Dzn0LD2N.d.cts} +464 -16
  41. package/dist/ui/form.d.cts +1 -1
  42. package/dist/ui/form.d.ts +1 -1
  43. package/dist/ui/video.cjs +4 -2
  44. package/dist/ui/video.cjs.map +1 -1
  45. package/dist/ui/video.d.cts +4 -2
  46. package/dist/ui/video.d.ts +4 -2
  47. package/dist/ui/video.js +4 -2
  48. package/dist/ui/video.js.map +1 -1
  49. package/dist/webhook.cjs +13 -2
  50. package/dist/webhook.cjs.map +1 -1
  51. package/dist/webhook.d.cts +5 -5
  52. package/dist/webhook.d.ts +5 -5
  53. package/dist/webhook.js +13 -2
  54. package/dist/webhook.js.map +1 -1
  55. package/package.json +9 -28
  56. package/dist/analytics/react.cjs +0 -344
  57. package/dist/analytics/react.cjs.map +0 -1
  58. package/dist/analytics/react.d.cts +0 -24
  59. package/dist/analytics/react.d.ts +0 -24
  60. package/dist/analytics/react.js +0 -319
  61. package/dist/analytics/react.js.map +0 -1
  62. package/dist/analytics.cjs +0 -238
  63. package/dist/analytics.cjs.map +0 -1
  64. package/dist/analytics.d.cts +0 -39
  65. package/dist/analytics.d.ts +0 -39
  66. package/dist/analytics.js +0 -215
  67. package/dist/analytics.js.map +0 -1
package/README.md CHANGED
@@ -50,7 +50,6 @@ import { receiveEmbeddedAdminSession } from '@01.software/sdk/embedded-admin'
50
50
  import { verifyEmbeddedAdminSession } from '@01.software/sdk/embedded-admin/server'
51
51
 
52
52
  // Components - sub-path imports per domain
53
- import { Analytics } from '@01.software/sdk/analytics/react'
54
53
  import { RichTextContent } from '@01.software/sdk/ui/rich-text'
55
54
  import { Image } from '@01.software/sdk/ui/image'
56
55
  import { FormRenderer } from '@01.software/sdk/ui/form'
@@ -70,7 +69,6 @@ entry.
70
69
  | `@01.software/sdk/client` | browser-safe `createClient` entry and SDK error guards | none |
71
70
  | `@01.software/sdk/server` | `createServerClient`, server-only APIs, and SDK error guards | none; keep `secretKey` code on the server |
72
71
  | `@01.software/sdk/errors` | SDK error classes and guards | none |
73
- | `@01.software/sdk/analytics` | browser analytics client and typed event helpers | none |
74
72
  | `@01.software/sdk/metadata` | SEO metadata extraction and generation helpers | none |
75
73
  | `@01.software/sdk/webhook` | webhook handlers, event guards, and webhook types | none |
76
74
  | `@01.software/sdk/query` | React Query hooks, cache helpers, `getQueryClient` | `@tanstack/react-query`, `react`, `react-dom` |
@@ -78,7 +76,6 @@ entry.
78
76
  | `@01.software/sdk/storefront-cache` | product storefront cache resource name helpers | none |
79
77
  | `@01.software/sdk/embedded-admin` | embedded app browser handshake and shared protocol types | none |
80
78
  | `@01.software/sdk/embedded-admin/server` | assertion verification and JWKS loading | none; keep receiver session creation on the server |
81
- | `@01.software/sdk/analytics/react` | `<Analytics />` | `react`, `react-dom` |
82
79
  | `@01.software/sdk/ui/rich-text` | `RichTextContent`, `StyledRichTextContent` | `react`, `react-dom`, `@payloadcms/richtext-lexical` |
83
80
  | `@01.software/sdk/ui/form` | `FormRenderer` | `react`, `react-dom` |
84
81
  | `@01.software/sdk/ui/code-block` | `CodeBlock`, `highlight` | `react`, `react-dom`, `shiki`, `hast-util-to-jsx-runtime` |
@@ -87,6 +84,18 @@ entry.
87
84
  | `@01.software/sdk/ui/video` | `VideoPlayer` | `react`, `react-dom`, `@mux/mux-player-react` |
88
85
  | `@01.software/sdk/ui/image` | `Image` | `react`, `react-dom` |
89
86
 
87
+ ### Video playback admission
88
+
89
+ On your server, call `createServerClient(...).video.playback(video.id)` after
90
+ applying your viewer's content-access policy. Pass its `streamUrl` to
91
+ `<VideoPlayer video={video} streamUrl={streamUrl} />`.
92
+ The bearer URL expires after two hours; refresh it before the deadline during
93
+ long playback or after a long pause. Free playback uses a quota-metered gateway and can return 403 when the
94
+ monthly allowance is exhausted or 503 when admission cannot be verified.
95
+ `muxPlaybackId`, `getVideoStreamUrl`, and raw Mux thumbnail/MP4 helpers are
96
+ legacy public-ID paths for unmigrated paid assets. A signed asset's retained
97
+ `muxPlaybackId` is historical usage evidence and is not a playable URL.
98
+
90
99
  `CanvasRenderer` renders reserved `shape` nodes (`terminator`, `process`,
91
100
  `decision`, `io`, `subprocess`) with first-party SVG geometry, without a tenant
92
101
  catalog row or QuickJS template.
@@ -162,91 +171,7 @@ Migration quick reference:
162
171
  self-contained and must not import private contracts; Console code should
163
172
  import shared helpers from contracts directly.
164
173
 
165
- ### Analytics
166
-
167
- ```typescript
168
- // Pageviews + custom events (browser)
169
- import { createAnalytics } from '@01.software/sdk/analytics'
170
-
171
- const analytics = createAnalytics({ publishableKey: 'pk01_xxx' })
172
- // auto-tracks pageviews; call analytics.pageview('/custom-path') manually if needed
173
- analytics.track('signup', { plan: 'pro', trial: false })
174
- ```
175
-
176
- **Custom events must be registered first.** Each custom event name, its
177
- dimensions, and any enum/boolean value sets are defined per workspace in
178
- Console → Analytics. An unregistered or mistyped event is accepted by the
179
- browser (200/204) and then **silently dropped server-side** — no client error.
180
- Register the event before firing it.
181
-
182
- **Typed events (compile-time safety).** Declare an event map as a plain `type`
183
- (do **not** `extends AnalyticsEventMap` — an index signature would defeat
184
- typo detection) and pass it as the generic. Mistyped names and out-of-enum
185
- values then fail to compile:
186
-
187
- ```typescript
188
- import {
189
- createAnalytics,
190
- defineAnalyticsEvents,
191
- } from '@01.software/sdk/analytics'
192
-
193
- type ShopEvents = {
194
- signup: { plan: 'free' | 'pro'; trial: boolean }
195
- add_to_cart: { productId: string; price: number }
196
- checkout_start: undefined // no props
197
- }
198
-
199
- const analytics = createAnalytics<ShopEvents>({ publishableKey: 'pk01_xxx' })
200
- analytics.track('signup', { plan: 'pro', trial: false }) // ✅
201
- analytics.track('checkout_start') // ✅ no props
202
- // analytics.track('signpu', { plan: 'pro', trial: false }) // ❌ unknown event
203
- // analytics.track('signup', { plan: 'team', trial: false }) // ❌ enum violation
204
-
205
- // Bind the map once to avoid repeating the generic:
206
- const create = defineAnalyticsEvents<ShopEvents>()
207
- const a2 = create({ publishableKey: 'pk01_xxx' })
208
- ```
209
-
210
- **React.** Use `AnalyticsProvider` + `useAnalytics()` to fire events from
211
- components. The provider auto-tracks pageviews like `<Analytics/>` and owns one
212
- instance for its subtree:
213
-
214
- Reusing the `ShopEvents` type from above:
215
-
216
- ```tsx
217
- import {
218
- AnalyticsProvider,
219
- useAnalytics,
220
- } from '@01.software/sdk/analytics/react'
221
-
222
- function Root() {
223
- return (
224
- <AnalyticsProvider>
225
- <App />
226
- </AnalyticsProvider>
227
- )
228
- }
229
-
230
- function SignupButton() {
231
- const { track, pageview } = useAnalytics<ShopEvents>()
232
- // pageview(path?) is available for manual SPA pageviews; the provider already auto-tracks pageviews
233
- return (
234
- <button onClick={() => track('signup', { plan: 'pro', trial: false })}>
235
- Sign up
236
- </button>
237
- )
238
- }
239
- ```
240
-
241
- `<Analytics />` remains available as a pageview-only mount helper for apps that
242
- do not fire custom events from components.
243
-
244
- **Send mode.** `mode: 'auto'` (default) suppresses sends on local hosts
245
- (`localhost` / `127.0.0.1` / `*.local`); `'production'` always sends, even
246
- locally; `'development'` never sends. The hosted `<script>` snippet uses the
247
- same vocabulary via `window.__01_analytics__.mode` or `data-mode`; its legacy
248
- `captureOnLocalhost: true` flag is equivalent to `mode: 'production'` and is
249
- honored only when `mode` is unset.
174
+ For website analytics, integrate an external tool such as GA4, Plausible, or Vercel Analytics in your own frontend. The SDK no longer provides analytics collection helpers.
250
175
 
251
176
  ## Getting Started
252
177
 
@@ -390,11 +315,26 @@ storefronts can render time-sensitive copy.
390
315
 
391
316
  `content.galleryItems.list()` calls `GET /api/gallery-items/storefront`;
392
317
  gallery item DTOs include title, description, content, gallery reference, and
393
- image media. Tenant, metadata, draft status, and storage/provider internals are
318
+ optional `mediaType` (`image` or `video`). Treat an omitted `mediaType` as `image`
319
+ for older responses. Image items retain `image` media. Video items include a
320
+ safe `video` descriptor with `availability: 'ready' | 'processing' | 'unavailable'`,
321
+ and may include a video document `id` and uploaded `thumbnail`. Disabled or
322
+ plan-blocked video features, missing/deleted targets, and failed videos return
323
+ only `{ availability: 'unavailable' }`, preserving each item's position and
324
+ pagination. The image relationship is not used as a video poster.
325
+ Tenant, metadata, draft status, and storage/provider internals are
394
326
  omitted. Gallery item reads require either `gallerySlug` or `galleryId`; prefer
395
327
  `gallerySlug` for storefront routes. The default gallery item sort is `manual`,
396
328
  matching curator order from the Console.
397
329
 
330
+ The descriptor's `ready` state is preparation metadata, not playback authority.
331
+ Your application server must verify that the requested video belongs to the
332
+ published gallery/item and apply its viewer policy before calling
333
+ `server.video.playback(video.id)`. Deliver that response's `streamUrl` to the
334
+ player; playback rechecks the current tenant, video state, and feature/plan.
335
+ Keep server credentials on the application server. Gallery responses do not
336
+ include playback URLs, provider IDs, or tokens.
337
+
398
338
  Both helpers return Payload-style pagination (`docs`, `totalDocs`, `page`,
399
339
  `limit`, etc.). `limit` is bounded server-side to `1..100`; invalid query,
400
340
  publishable-key, feature, rate-limit, and server errors surface through the
@@ -779,8 +719,8 @@ Membership self-service is split by credential boundary:
779
719
  - Browser/customer JWT clients can read the signed-in customer's current or
780
720
  latest membership and request cancellation for active, trialing, or past-due
781
721
  memberships.
782
- - Server clients with a Secret Key can subscribe a customer after the storefront
783
- has completed the delegated first charge.
722
+ - Server clients with a Secret Key prepare a paid customer subscription before
723
+ the tenant's PSP executes the first charge.
784
724
 
785
725
  ```typescript
786
726
  import { createClient } from '@01.software/sdk'
@@ -801,28 +741,115 @@ const server = createServerClient({
801
741
  secretKey: process.env.SOFTWARE_SECRET_KEY!,
802
742
  })
803
743
 
804
- await server.memberships.subscribe({
744
+ const prepared = await server.memberships.subscribe({
805
745
  planId: 'plan_123',
746
+ priceId: 'price_monthly_123',
806
747
  customerId: 'customer_123',
807
748
  customerRef: 'cus_provider_123',
808
- pgPaymentId: 'pay_123',
809
- amount: 9900,
810
- currency: 'KRW',
811
749
  subscribeIntentKey: 'checkout-session-123',
812
750
  card: { last4: '4242', brand: 'visa' },
813
751
  })
752
+
753
+ if (prepared.status === 'incomplete') {
754
+ await tenantPsp.charge({
755
+ idempotencyKey: prepared.firstCharge.idempotencyKey,
756
+ amount: prepared.firstCharge.amount,
757
+ currency: prepared.firstCharge.currency,
758
+ })
759
+ // Post the PSP outcome to Console's existing signed charge-result webhook.
760
+ }
814
761
  ```
815
762
 
816
763
  `subscribeIntentKey` is sent as `X-Idempotency-Key`; it is not part of the JSON
817
- body. Replays must use the same plan, customer reference, amount, currency, and
818
- paid-start provider payment id snapshot as the original incomplete membership.
819
- Trial subscriptions may omit `pgPaymentId` because no first-period charge has
820
- been captured yet; paid starts must include the provider payment id that funded
821
- the first period.
764
+ body. A paid response is `incomplete` and contains the immutable first-charge
765
+ instruction. The tenant charges its PSP with `firstCharge.idempotencyKey`, then
766
+ posts the signed outcome through the configured charge-result webhook. A trial
767
+ response is immediately `trialing` and has no `firstCharge` field. Before a paid
768
+ subscribe, configure an active delegated billing connection for
769
+ the tenant; otherwise Console returns `409 conflict` without lifecycle writes.
770
+ Do not start
771
+ the PSP payment at or after `expiresAt`; deduplicate PSP work with
772
+ `firstCharge.idempotencyKey`. A late paid result remains evidence but cannot
773
+ activate the Membership, so the tenant handles any refund outside this API. Replays use
774
+ the same plan, price, customer reference, and idempotency key while the first
775
+ charge is pending. A replay after settlement or `expiresAt` returns `409`
776
+ instead of a payment instruction.
822
777
  `memberships.me()` and browser `memberships.cancel()` require the customer JWT
823
778
  configured on the client. Refund webhook `refundedAmount` values are cumulative
824
779
  charge totals, not per-event deltas.
825
780
 
781
+ ### Source-aware customer groups (server only)
782
+
783
+ Manage manual Customer Group membership through the shaped server API. The
784
+ read result includes every current reason a customer qualifies, so revoking a
785
+ manual source does not hide a still-active Membership source.
786
+
787
+ ```typescript
788
+ const page = await server.customerGroups.listMembers({
789
+ groupId: '00000000-0000-4000-8000-000000000002',
790
+ limit: 100,
791
+ })
792
+
793
+ await server.customerGroups.grantMember({
794
+ groupId: '00000000-0000-4000-8000-000000000002',
795
+ customerId: '00000000-0000-4000-8000-000000000003',
796
+ reason: 'support-approved',
797
+ })
798
+
799
+ await server.customerGroups.revokeMember({
800
+ groupId: '00000000-0000-4000-8000-000000000002',
801
+ customerId: '00000000-0000-4000-8000-000000000003',
802
+ })
803
+ ```
804
+
805
+ Keep these calls in trusted server code. Do not mutate `customers.groups`
806
+ directly: after source-authority activation it is a read-compatible projection
807
+ and direct writes return `customer_groups_direct_write_retired`.
808
+
809
+ ### External Customer links (server only)
810
+
811
+ `server.externalCustomerLinks` connects a platform Customer to an external
812
+ Customer ID within the namespace and environment derived from an active
813
+ integration registration. Your app supplies a normalized stable `accountScope`
814
+ to reserve identity; namespace-bound keys authorize all accounts in that
815
+ namespace, and account scope is not an account ACL. All six methods (`connect`,
816
+ `get`, `list`,
817
+ `unlink`, `capturePreviousState`, `clearPreviousState`) require server
818
+ credentials and Customer read or write scope.
819
+
820
+ ```typescript
821
+ const link = await server.externalCustomerLinks.connect({
822
+ integrationId,
823
+ customerId,
824
+ accountScope,
825
+ externalCustomerId: 'contact-123',
826
+ requestId: crypto.randomUUID(),
827
+ })
828
+ const capture = {
829
+ integrationId,
830
+ customerId,
831
+ linkId: link.id,
832
+ requestId: crypto.randomUUID(),
833
+ expectedVersion: link.version,
834
+ previousState: { tier: 'silver' },
835
+ }
836
+ await server.externalCustomerLinks.capturePreviousState(capture)
837
+ // After an interrupted response, retry this exact request, including expectedVersion.
838
+ ```
839
+
840
+ The bounded previous state accepts up to 16 flat string/number/boolean/null
841
+ fields, 512 characters per string, and 2048 UTF-8 JSON bytes. It records only
842
+ what your server supplies; the platform performs no external provider I/O.
843
+ Before restoring provider state, check the current active owner with
844
+ `list({ integrationId, accountScope, externalCustomerId, state: 'active' })`
845
+ and evaluate whether the restoration is still appropriate. Lists accept optional Customer,
846
+ account scope, external ID, and state filters (external ID requires account
847
+ scope), `limit` (default 25, maximum 100), and `afterId`;
848
+ use `nextCursor` for the next page. Unlink and clear also require a fresh
849
+ request UUID and the current version; exact retries retain the original values.
850
+ A privacy-erased snapshot has state `erased`, null `externalCustomerId`, and
851
+ null `previousState`. Keep these calls and snapshots in trusted server code.
852
+
826
853
  ## Storefront performance defaults
827
854
 
828
855
  - **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.
@@ -1094,6 +1121,49 @@ await server.collections.from('articles').updateMany(where, data)
1094
1121
  await server.collections.from('articles').removeMany(where)
1095
1122
  ```
1096
1123
 
1124
+ Collection `find`, `count`, `updateMany`, and `removeMany` share filter validation.
1125
+ `equals: null` and `not_equals: null` retain their null meaning through Payload
1126
+ REST, including text fields; null-containing filters use Payload's native JSON
1127
+ `where` query parameter. Other filters keep bracketed encoding. The SDK throws
1128
+ `ValidationError` before a request for undefined values, null outside those two
1129
+ operators, empty condition objects, empty arrays (including `and`/`or`), nesting
1130
+ beyond 10 levels, and arrays with more than 1000 elements. Omit `where` for an
1131
+ unfiltered read. For a dynamically empty ID list, return an empty result before
1132
+ calling the SDK rather than passing `in: []`. False, zero, empty strings, and
1133
+ Date conditions remain valid.
1134
+
1135
+ Check your bulk filters carefully: calls that previously failed with HTTP 400
1136
+ now perform updates or deletions. Large `in`/`not_in`
1137
+ lists can exceed URL length limits (around 250 UUIDs); split work into smaller
1138
+ batches. For `not_in`, keep the full exclusion condition in every batch rather
1139
+ than splitting the exclusions across separate mutations.
1140
+
1141
+ ### Orders update compatibility
1142
+
1143
+ Orders accept one-document updates only. Admin Panel bulk edits and generic
1144
+ `server.collections.from('orders').updateMany(...)` are rejected before any
1145
+ Order changes with HTTP 400 (`errors[0].data.code: order_bulk_update_unsupported`).
1146
+ Use `update(orderId, data)` for an unconditional edit. For a conditional update,
1147
+ keep the entire where predicate in the raw `PATCH /api/orders` query and set
1148
+ `limit=1`; send the field data as the body. Local API callers use numeric
1149
+ `limit: 1`, or a strict UUID `id.equals` that bounds the complete predicate.
1150
+ An ID in only one OR branch is insufficient. SDK `updateMany` has no limit
1151
+ option and is not a supported Orders CAS path.
1152
+
1153
+ A conditional miss returns `docs: [], errors: []`. A collected hook failure
1154
+ instead returns HTTP 500 (`errors[0].data.code: order_update_failed`), with a
1155
+ localized message and rollback of the supported single operation. Access and
1156
+ tenant checks remain in force. Do not replace a conditional update with an
1157
+ unconditional ID update or automatically retry payment capture. Separate
1158
+ per-order edits do not promise atomic success across the whole selection.
1159
+
1160
+ Console Local API Orders updates also require a live transaction. Setting
1161
+ `disableTransaction: true` without a live external transaction rejects both
1162
+ top-ID and where updates before writes with HTTP 400
1163
+ (`errors[0].data.code: order_transaction_required`). Remove that option to
1164
+ use Payload's normal transaction, or pass a live external transaction;
1165
+ a request transaction ID by itself is not sufficient.
1166
+
1097
1167
  ### API Response Types (Payload Native)
1098
1168
 
1099
1169
  The SDK returns Payload CMS native response types without wrapping:
@@ -1296,7 +1366,6 @@ const { token, customer } = await client.customer.auth.login({
1296
1366
  // Profile & token management
1297
1367
  const profile = await client.customer.auth.me()
1298
1368
  client.customer.auth.isAuthenticated()
1299
- client.customer.auth.logout()
1300
1369
 
1301
1370
  // Authenticated customer's own orders (Client-only)
1302
1371
  const orders = await client.commerce.orders.listMine({
@@ -1305,12 +1374,22 @@ const orders = await client.commerce.orders.listMine({
1305
1374
  financialStatus: 'paid',
1306
1375
  })
1307
1376
 
1377
+ await client.customer.auth.logoutAsync()
1378
+
1308
1379
  // Password
1309
1380
  await client.customer.auth.forgotPassword('john@example.com')
1310
1381
  await client.customer.auth.resetPassword(token, newPassword)
1311
1382
  await client.customer.auth.changePassword(currentPassword, newPassword)
1312
1383
  ```
1313
1384
 
1385
+ `logoutAsync()` revokes the direct refresh-token family on the server and clears
1386
+ local credentials even when the request fails. Handle a rejection as incomplete
1387
+ server revocation. The older synchronous `logout(): void` clears local state
1388
+ immediately but cannot revoke the server session. `useCustomerLogout()` awaits
1389
+ the server request and clears authenticated customer query data on success or
1390
+ failure. An issued access token can remain valid for up to 15 minutes; legacy
1391
+ bearer-only refresh remains a separate compatibility path.
1392
+
1314
1393
  `forgotPassword()` keeps the same SDK signature in both delivery modes. A
1315
1394
  tenant administrator chooses either backward-compatible signed webhook
1316
1395
  delivery or platform-managed email in the Console customer-authentication
@@ -1319,6 +1398,31 @@ the platform appends the `token` query parameter and sends through the verified
1319
1398
  tenant sender or the platform fallback. Do not add reset tokens or dynamic
1320
1399
  redirect URLs to the SDK call.
1321
1400
 
1401
+ #### Self-withdrawal
1402
+
1403
+ A signed-in customer with a local password can withdraw using their current
1404
+ password. With an authenticated client:
1405
+
1406
+ ```ts
1407
+ const { withdrawnAt } = await client.customer.auth.withdraw(currentPassword)
1408
+ ```
1409
+
1410
+ This calls `POST /api/customers/withdraw` with the publishable key and Customer
1411
+ JWT. Success archives the account immediately and clears the SDK's local
1412
+ credentials. Clear any application-owned session cookies and customer cache as
1413
+ well. Archived accounts cannot use existing sessions or authenticate again.
1414
+ Withdrawal preserves order, payment and membership-charge history; it does not
1415
+ erase personal data or free the email for a new registration.
1416
+
1417
+ Branch on `SDKError.apiReason`: `active_membership_exists` (409) requires the
1418
+ membership to end first, including scheduled cancellations still in an active
1419
+ status; `reauthentication_unavailable` (409) means no local password is set and
1420
+ the customer should contact the tenant administrator. Incorrect passwords use
1421
+ the shared login attempt counter and temporary lock (`account_locked`, 423).
1422
+ Refusal leaves the SDK credentials intact. Server keys cannot withdraw on a
1423
+ customer's behalf. Operator restore continues to follow the existing archive
1424
+ rules; the withdrawal timestamp is retained.
1425
+
1322
1426
  ### Market discovery
1323
1427
 
1324
1428
  Discover a tenant's active buyer markets and resolve the default market. These
@@ -1970,6 +2074,28 @@ await server.community.moderation.banCustomer({ customerId, isPermanent?, banned
1970
2074
  await server.community.moderation.unbanCustomer({ customerId })
1971
2075
  ```
1972
2076
 
2077
+ ### Installation credentials (server only)
2078
+
2079
+ `createServerClient().installationCredentials` stores one bounded ciphertext
2080
+ for a registered non-payment OAuth installation. A tenant administrator must
2081
+ register the installation and explicitly bind the server key; an ordinary
2082
+ `sk01_` key or external-effect claim grant is insufficient. Encrypt before
2083
+ `create`, decrypt only in your server, and keep OAuth and provider calls there.
2084
+
2085
+ ```typescript
2086
+ const current = await server.installationCredentials.get({ installationId })
2087
+ const next = await server.installationCredentials.replace({
2088
+ installationId,
2089
+ expectedVersion: current.version,
2090
+ ciphertext: newlyEncryptedCredential,
2091
+ })
2092
+ ```
2093
+
2094
+ Replacement uses a version condition; after an uncertain result, read back
2095
+ before retrying. `dispose` clears the platform copy but does not revoke a
2096
+ credential already read by your server. A platform database restore requires
2097
+ re-authorization before older installations become readable again.
2098
+
1973
2099
  ### Webhook
1974
2100
 
1975
2101
  Webhooks created in Console start inactive. Use this setup order so no event is
@@ -2243,7 +2369,7 @@ SOFTWARE_SECRET_KEY=sk01_... # Server only — opaque API key from Console
2243
2369
 
2244
2370
  > `secretKey` may be an `sk01_...` API key or a `pat01_...` personal access token. Server SDK calls must also send the matching `publishableKey`; PAT tenant selection is pinned server-side, and callers must not send `X-Tenant-Id`.
2245
2371
 
2246
- API keys created without explicit scopes use the default `['read', 'write']`. Console API-key creation can request narrower `scopes[]`; in the first explicit-scope slice tenant admins may grant only `read` and `write`. Higher-authority scopes such as `webhook`, `analytics`, and `super-admin` require platform authority and otherwise return `scope_grant_forbidden`.
2372
+ API keys created without explicit scopes use the default `['read', 'write']`. Console API-key creation can request narrower `scopes[]`; tenant admins may grant only `read` and `write`. `webhook` and `super-admin` require a super-admin session. Requests containing the retired `analytics` scope, alone or mixed, are rejected for every role. Previously issued keys keep their identity; an analytics-only key has no effective access, while a mixed key keeps its other scopes.
2247
2373
 
2248
2374
  > **SDK 0.9.0**: Server auth now uses opaque bearer tokens (`sk01_...`). Generate API keys from the Console. `createServerToken`, `createApiKey`, and `parseApiKey` are no longer part of the SDK surface.
2249
2375