@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/dist/client.js CHANGED
@@ -1005,6 +1005,49 @@ function stockSnapshotSearchParams(params) {
1005
1005
  }
1006
1006
 
1007
1007
  // src/core/collection/http-client.ts
1008
+ function validateWhere(where) {
1009
+ if (where === void 0) return false;
1010
+ let hasNull = false;
1011
+ function visit(value, depth, key) {
1012
+ if (depth > 10) {
1013
+ throw createValidationError(
1014
+ "Collection where must not exceed nesting depth 10."
1015
+ );
1016
+ }
1017
+ if (value === null && (key === "equals" || key === "not_equals")) {
1018
+ hasNull = true;
1019
+ return;
1020
+ }
1021
+ if (value == null) {
1022
+ throw createValidationError(
1023
+ "Collection where must not contain null or undefined values."
1024
+ );
1025
+ }
1026
+ if (Array.isArray(value)) {
1027
+ if (value.length === 0) {
1028
+ throw createValidationError(
1029
+ "Collection where must not contain empty arrays, including and/or."
1030
+ );
1031
+ }
1032
+ if (value.length > 1e3) {
1033
+ throw createValidationError(
1034
+ "Collection where arrays must not contain more than 1000 elements."
1035
+ );
1036
+ }
1037
+ for (const item of value) visit(item, depth + 1);
1038
+ } else if (typeof value === "object" && !(value instanceof Date)) {
1039
+ const values = Object.entries(value);
1040
+ if (values.length === 0) {
1041
+ throw createValidationError(
1042
+ "Collection where must not contain empty condition objects."
1043
+ );
1044
+ }
1045
+ for (const [key2, item] of values) visit(item, depth + 1, key2);
1046
+ }
1047
+ }
1048
+ visit(where, 0);
1049
+ return hasNull;
1050
+ }
1008
1051
  var HttpClient = class {
1009
1052
  constructor(publishableKey, secretKey, getCustomerToken, onUnauthorized, onRequestId, apiUrl) {
1010
1053
  this.publishableKey = requirePublishableKeyForSecret(
@@ -1051,7 +1094,8 @@ var HttpClient = class {
1051
1094
  ...rest,
1052
1095
  "fallback-locale": fallbackLocale === false ? "none" : fallbackLocale
1053
1096
  };
1054
- const queryString = stringify(queryOptions, { addQueryPrefix: true });
1097
+ const encodedOptions = validateWhere(queryOptions.where) ? { ...queryOptions, where: JSON.stringify(queryOptions.where) } : queryOptions;
1098
+ const queryString = stringify(encodedOptions, { addQueryPrefix: true });
1055
1099
  return queryString ? `${endpoint}${queryString}` : endpoint;
1056
1100
  }
1057
1101
  assertJsonResponse(response) {
@@ -1353,6 +1397,11 @@ var COMMERCE_ENDPOINT_OPERATIONS = {
1353
1397
  detail: strictCommerceOperation("checkouts", "post", "/detail"),
1354
1398
  query: strictCommerceOperation("checkouts", "post", "/query")
1355
1399
  },
1400
+ draftOrders: {
1401
+ calculate: operation("draft-orders", "post", "/:id/calculate"),
1402
+ complete: operation("draft-orders", "post", "/:id/complete"),
1403
+ finalize: operation("draft-orders", "post", "/:id/finalize")
1404
+ },
1356
1405
  transactions: {
1357
1406
  update: operation("transactions", "post", "/update")
1358
1407
  },
@@ -1452,8 +1501,51 @@ var portOneStandardActionCapabilityOperations = /* @__PURE__ */ new Set([
1452
1501
  ]);
1453
1502
  var MEMBERSHIP_ENDPOINT_OPERATIONS = {
1454
1503
  subscribe: operation(null, "post", "/memberships/subscribe"),
1504
+ billingCustody: operation(null, "post", "/memberships/billing-custody"),
1455
1505
  cancel: operation(null, "post", "/memberships/:id/cancel"),
1456
- me: operation(null, "get", "/memberships/me")
1506
+ me: operation(null, "get", "/memberships/me"),
1507
+ chargeResult: operation(null, "post", "/memberships/:id/charge-result"),
1508
+ refundResult: operation(null, "post", "/memberships/:id/refund-result"),
1509
+ revoke: operation(null, "post", "/memberships/:id/revoke")
1510
+ };
1511
+ var EXTERNAL_EFFECT_CLAIM_ENDPOINT_OPERATIONS = {
1512
+ claim: operation(null, "post", "/external-effect-claims/claim"),
1513
+ get: operation(null, "post", "/external-effect-claims/get"),
1514
+ arm: operation(null, "post", "/external-effect-claims/arm"),
1515
+ release: operation(null, "post", "/external-effect-claims/release"),
1516
+ report: operation(null, "post", "/external-effect-claims/report"),
1517
+ recovery: operation(null, "post", "/external-effect-claims/recovery")
1518
+ };
1519
+ var INSTALLATION_CREDENTIAL_ENDPOINT_OPERATIONS = {
1520
+ create: operation(null, "post", "/installation-credentials/create"),
1521
+ get: operation(null, "post", "/installation-credentials/get"),
1522
+ replace: operation(null, "post", "/installation-credentials/replace"),
1523
+ dispose: operation(null, "post", "/installation-credentials/dispose")
1524
+ };
1525
+ var EXTERNAL_CUSTOMER_LINK_ENDPOINT_OPERATIONS = {
1526
+ connect: operation("customers", "post", "/external-links/connect"),
1527
+ get: operation("customers", "post", "/external-links/get"),
1528
+ list: operation("customers", "post", "/external-links/list"),
1529
+ unlink: operation("customers", "post", "/external-links/unlink"),
1530
+ capturePreviousState: operation(
1531
+ "customers",
1532
+ "post",
1533
+ "/external-links/capture-previous-state"
1534
+ ),
1535
+ clearPreviousState: operation(
1536
+ "customers",
1537
+ "post",
1538
+ "/external-links/clear-previous-state"
1539
+ )
1540
+ };
1541
+ var CUSTOMER_GROUP_ENDPOINT_OPERATIONS = {
1542
+ members: operation("customer-groups", "get", "/:id/members"),
1543
+ grant: operation("customer-groups", "post", "/:id/members/grant"),
1544
+ revoke: operation("customer-groups", "post", "/:id/members/revoke")
1545
+ };
1546
+ var CUSTOMER_AUTH_ENDPOINT_OPERATIONS = {
1547
+ revokeRefresh: operation("customers", "post", "/revoke-refresh"),
1548
+ withdraw: operation("customers", "post", "/withdraw")
1457
1549
  };
1458
1550
  var EMBEDDED_ADMIN_ENDPOINT_OPERATIONS = {
1459
1551
  show: operation("tenants", "get", "/embedded-admin-app"),
@@ -1478,6 +1570,14 @@ var COMMUNITY_ENDPOINT_OPERATIONS = {
1478
1570
  detail: operation("posts", "get", "/detail")
1479
1571
  }
1480
1572
  };
1573
+ var GALLERY_ENDPOINT_OPERATIONS = {
1574
+ items: {
1575
+ storefront: operation("gallery-items", "get", "/storefront")
1576
+ }
1577
+ };
1578
+ var VIDEO_ENDPOINT_OPERATIONS = {
1579
+ playback: operation("videos", "post", "/:id/playback")
1580
+ };
1481
1581
  function resolveOperationTemplate(descriptor) {
1482
1582
  return descriptor.collection ? `/api/${descriptor.collection}${descriptor.path}` : `/api${descriptor.path}`;
1483
1583
  }
@@ -2067,6 +2167,15 @@ var PublicContentApi = class extends RawCustomerScopedApi {
2067
2167
  get(endpoint) {
2068
2168
  return this.request(endpoint, { method: "GET" });
2069
2169
  }
2170
+ galleryItems(options) {
2171
+ const operation2 = GALLERY_ENDPOINT_OPERATIONS.items.storefront;
2172
+ return this.request(
2173
+ resolveOperationPath(operation2, {
2174
+ query: galleryItemsListQuery(options)
2175
+ }),
2176
+ { method: resolveOperationMethod(operation2) }
2177
+ );
2178
+ }
2070
2179
  };
2071
2180
  function appendCommonParams(search, options, defaultSort) {
2072
2181
  search.set("sort", options?.sort ?? defaultSort);
@@ -2086,12 +2195,12 @@ function linksListPath(options) {
2086
2195
  search.set("featured", String(options.featured));
2087
2196
  return `/api/links/storefront?${search}`;
2088
2197
  }
2089
- function galleryItemsListPath(options) {
2198
+ function galleryItemsListQuery(options) {
2090
2199
  const search = new URLSearchParams();
2091
2200
  appendCommonParams(search, options, "manual");
2092
2201
  if (options?.galleryId) search.set("galleryId", options.galleryId);
2093
2202
  if (options?.gallerySlug) search.set("gallerySlug", options.gallerySlug);
2094
- return `/api/gallery-items/storefront?${search}`;
2203
+ return search;
2095
2204
  }
2096
2205
  var ContentClient = class {
2097
2206
  constructor(options) {
@@ -2105,9 +2214,7 @@ var ContentClient = class {
2105
2214
  list: (params) => api.get(linksListPath(params))
2106
2215
  };
2107
2216
  this.galleryItems = {
2108
- list: (params) => api.get(
2109
- galleryItemsListPath(params)
2110
- )
2217
+ list: (params) => api.galleryItems(params)
2111
2218
  };
2112
2219
  }
2113
2220
  };
@@ -2163,7 +2270,10 @@ var ORDER_PRIMARY_DISPLAY_STATUSES = [
2163
2270
  ];
2164
2271
  var CHECKOUT_STATUSES = ["open", "placed", "abandoned"];
2165
2272
  var ABANDONED_REASONS = ["expired", "customer", "operator"];
2166
- var PAYMENT_PROVIDERS = ["toss", "portone"];
2273
+ function managedPaymentProviders(providers) {
2274
+ return providers;
2275
+ }
2276
+ var PAYMENT_PROVIDERS = managedPaymentProviders(["toss", "portone"]);
2167
2277
  var PAYMENT_PROVIDER_STATUSES = [
2168
2278
  "requires_payment_method",
2169
2279
  "requires_action",
@@ -2211,6 +2321,26 @@ function invalidCanonicalResponse(resource, details) {
2211
2321
  function fail(resource, path, expected) {
2212
2322
  invalidCanonicalResponse(resource, { path, expected });
2213
2323
  }
2324
+ function providerMismatch(resource, expected, actual, operation2) {
2325
+ if (operation2 === "cancel") {
2326
+ const method = {
2327
+ toss: "cancel()",
2328
+ portone: "cancelPortOne()"
2329
+ }[actual];
2330
+ throw createApiError(
2331
+ `Canonical ${resource} response provider mismatch: expected ${expected}, received ${actual}.`,
2332
+ 502,
2333
+ { resource, path: "$.provider", expected, actual },
2334
+ `The server applied the cancellation to a ${actual} PaymentSession, but this SDK method expects ${expected}.`,
2335
+ `Re-invoke ${method} with the same idempotencyKey; do not create a new key.`
2336
+ );
2337
+ }
2338
+ invalidCanonicalResponse(resource, {
2339
+ path: "$.provider",
2340
+ expected,
2341
+ actual
2342
+ });
2343
+ }
2214
2344
  function record(value, resource, path) {
2215
2345
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2216
2346
  fail(resource, path, "object");
@@ -3503,7 +3633,7 @@ function validateNextAction(value, provider, resource, path) {
3503
3633
  if (action.customerEmail !== void 0)
3504
3634
  validateCustomer({ email: action.customerEmail }, resource, path);
3505
3635
  }
3506
- function validatePublicPaymentSession(value, expectedProvider) {
3636
+ function validatePublicPaymentSession(value, expectedProvider, operation2) {
3507
3637
  const resource = "PaymentSession";
3508
3638
  const session = record(value, resource, "$");
3509
3639
  const provider = enumValue(
@@ -3512,9 +3642,6 @@ function validatePublicPaymentSession(value, expectedProvider) {
3512
3642
  resource,
3513
3643
  "$.provider"
3514
3644
  );
3515
- if (expectedProvider !== void 0 && provider !== expectedProvider) {
3516
- fail(resource, "$.provider", expectedProvider);
3517
- }
3518
3645
  const status = enumValue(
3519
3646
  session.providerStatus,
3520
3647
  PAYMENT_PROVIDER_STATUSES,
@@ -3718,44 +3845,128 @@ function validatePublicPaymentSession(value, expectedProvider) {
3718
3845
  "$.cancellationReason"
3719
3846
  );
3720
3847
  }
3848
+ if (expectedProvider !== void 0 && provider !== expectedProvider) {
3849
+ if (operation2 === "cancel" && status !== "canceling" && status !== "canceled") {
3850
+ fail(
3851
+ resource,
3852
+ "$.providerStatus",
3853
+ "canceling or canceled after a successful cancellation"
3854
+ );
3855
+ }
3856
+ providerMismatch(resource, expectedProvider, provider, operation2);
3857
+ }
3721
3858
  return value;
3722
3859
  }
3723
3860
 
3724
3861
  // src/core/customer/customer-auth.ts
3725
3862
  var DEFAULT_TIMEOUT2 = 15e3;
3726
- function safeGetItem(key) {
3863
+ var REFRESH_LOCK_TIMEOUT = DEFAULT_TIMEOUT2 * 3;
3864
+ var SESSION_RECORD_VERSION = 1;
3865
+ var STORAGE_UNUSABLE = /* @__PURE__ */ Symbol("storage-unusable");
3866
+ function canonicalApiIdentity(apiUrl) {
3727
3867
  try {
3728
- return localStorage.getItem(key);
3868
+ const url = new URL(apiUrl);
3869
+ return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
3870
+ } catch {
3871
+ return apiUrl.trim().toLowerCase().replace(/\/+$/, "");
3872
+ }
3873
+ }
3874
+ function buildRefreshLockName(apiUrl, publishableKey, storageKey) {
3875
+ return `01software:customer-auth-refresh:${canonicalApiIdentity(apiUrl)}:${publishableKey}:${storageKey}`;
3876
+ }
3877
+ function getRefreshLockRequester() {
3878
+ try {
3879
+ if (typeof navigator === "undefined") return null;
3880
+ const locks = navigator.locks;
3881
+ if (!locks || typeof locks.request !== "function") return null;
3882
+ return (name, options, callback) => locks.request(
3883
+ name,
3884
+ options,
3885
+ () => callback()
3886
+ );
3729
3887
  } catch {
3730
3888
  return null;
3731
3889
  }
3732
3890
  }
3891
+ function parseSessionRecord(raw) {
3892
+ try {
3893
+ const parsed = JSON.parse(raw);
3894
+ if (!parsed || typeof parsed !== "object") return null;
3895
+ const candidate = parsed;
3896
+ if (candidate.v !== SESSION_RECORD_VERSION) return null;
3897
+ if (typeof candidate.rev !== "number" || !Number.isFinite(candidate.rev)) {
3898
+ return null;
3899
+ }
3900
+ const token = candidate.token;
3901
+ const refreshToken = candidate.refreshToken;
3902
+ if (token !== null && typeof token !== "string") return null;
3903
+ if (refreshToken !== null && typeof refreshToken !== "string") return null;
3904
+ return {
3905
+ v: SESSION_RECORD_VERSION,
3906
+ rev: candidate.rev,
3907
+ token,
3908
+ refreshToken
3909
+ };
3910
+ } catch {
3911
+ return null;
3912
+ }
3913
+ }
3914
+ function readSessionRecord(recordKey, mirrorKey, mirrorRefreshKey) {
3915
+ let raw;
3916
+ try {
3917
+ raw = localStorage.getItem(recordKey);
3918
+ } catch {
3919
+ return STORAGE_UNUSABLE;
3920
+ }
3921
+ if (raw !== null) {
3922
+ const parsed = parseSessionRecord(raw);
3923
+ return parsed ? { record: parsed, stored: true } : STORAGE_UNUSABLE;
3924
+ }
3925
+ try {
3926
+ return {
3927
+ record: {
3928
+ v: SESSION_RECORD_VERSION,
3929
+ rev: 0,
3930
+ token: localStorage.getItem(mirrorKey),
3931
+ refreshToken: localStorage.getItem(mirrorRefreshKey)
3932
+ },
3933
+ stored: false
3934
+ };
3935
+ } catch {
3936
+ return STORAGE_UNUSABLE;
3937
+ }
3938
+ }
3733
3939
  var CustomerAuth = class {
3734
3940
  constructor(publishableKey, options, apiUrl) {
3735
3941
  this.refreshPromise = null;
3942
+ /** Canonical record key; null unless persistence is active in a browser. */
3943
+ this.persistRecordKey = null;
3944
+ /** Advisory single-value access mirror, plus the retired refresh mirror. */
3945
+ this.persistMirrorKey = null;
3946
+ this.persistMirrorRefreshKey = null;
3947
+ /** Cross-context refresh lock name; null when there is no shared storage. */
3948
+ this.refreshLockName = null;
3949
+ /** Revision of the canonical record this instance last observed. */
3950
+ this.sessionRev = 0;
3736
3951
  this.publishableKey = publishableKey;
3737
3952
  this.baseUrl = resolveApiUrl(apiUrl);
3738
3953
  const persist = options?.persist ?? true;
3739
3954
  if (persist) {
3740
3955
  const key = typeof persist === "string" ? persist : "customer-token";
3741
- const refreshKey = `${key}:refresh`;
3742
3956
  const isBrowser = typeof window !== "undefined";
3743
- this.token = isBrowser ? safeGetItem(key) : null;
3744
- this.refreshTokenValue = isBrowser ? safeGetItem(refreshKey) : null;
3745
- this.onTokenChange = isBrowser ? (token) => {
3746
- try {
3747
- if (token) localStorage.setItem(key, token);
3748
- else localStorage.removeItem(key);
3749
- } catch {
3750
- }
3751
- } : void 0;
3752
- this.onRefreshTokenChange = isBrowser ? (refreshToken) => {
3753
- try {
3754
- if (refreshToken) localStorage.setItem(refreshKey, refreshToken);
3755
- else localStorage.removeItem(refreshKey);
3756
- } catch {
3757
- }
3758
- } : void 0;
3957
+ this.token = null;
3958
+ this.refreshTokenValue = null;
3959
+ if (isBrowser) {
3960
+ this.persistRecordKey = `${key}:session`;
3961
+ this.persistMirrorKey = key;
3962
+ this.persistMirrorRefreshKey = `${key}:refresh`;
3963
+ this.refreshLockName = buildRefreshLockName(
3964
+ this.baseUrl,
3965
+ publishableKey,
3966
+ key
3967
+ );
3968
+ this.syncFromPersistedStorage();
3969
+ }
3759
3970
  } else {
3760
3971
  this.token = options?.token ?? null;
3761
3972
  this.refreshTokenValue = options?.refreshToken ?? null;
@@ -3783,66 +3994,334 @@ var CustomerAuth = class {
3783
3994
  body: JSON.stringify(data)
3784
3995
  }
3785
3996
  );
3786
- this.setToken(result.token);
3787
- if (result.refreshToken) this.setRefreshToken(result.refreshToken);
3997
+ if (result.refreshToken) this.setSession(result.token, result.refreshToken);
3998
+ else this.setToken(result.token);
3788
3999
  return result;
3789
4000
  }
3790
4001
  /**
3791
4002
  * Refresh the current token. Uses a refresh token when available, otherwise
3792
4003
  * falls back to the legacy bearer-token compatibility path.
4004
+ *
4005
+ * Refresh tokens are single-use, so a persisted session shared by several
4006
+ * tabs or SDK instances must rotate at most once: concurrent rotations of the
4007
+ * same token are replay from the server's point of view and revoke the whole
4008
+ * family. Persisted browser sessions therefore serialize rotation across
4009
+ * contexts and re-read the canonical persisted credentials inside the lock.
3793
4010
  */
3794
4011
  async refreshToken() {
4012
+ this.syncFromPersistedStorage();
3795
4013
  if (!this.refreshTokenValue && !this.token) {
3796
4014
  throw new ApiError("Not authenticated", 401);
3797
4015
  }
3798
4016
  if (this.refreshPromise) return this.refreshPromise;
3799
- this.refreshPromise = this._doRefreshToken();
4017
+ const intended = this.currentFence();
4018
+ this.refreshPromise = this.rotateAcrossContexts(intended);
3800
4019
  try {
3801
4020
  return await this.refreshPromise;
3802
4021
  } finally {
3803
4022
  this.refreshPromise = null;
3804
4023
  }
3805
4024
  }
3806
- async _doRefreshToken() {
3807
- const init = this.refreshTokenValue ? {
4025
+ /**
4026
+ * Re-read the canonical persisted session. In persisted mode the shared
4027
+ * record — not this instance's memory — is the source of truth, so another
4028
+ * context's rotation, login, or logout is observed before the next credential
4029
+ * use. Change callbacks are deliberately not invoked: this is a read.
4030
+ */
4031
+ syncFromPersistedStorage() {
4032
+ const canonical = this.readCanonicalSession();
4033
+ if (canonical === STORAGE_UNUSABLE) {
4034
+ this.disablePersistence();
4035
+ return;
4036
+ }
4037
+ if (!canonical) return;
4038
+ this.sessionRev = canonical.record.rev;
4039
+ this.token = canonical.record.token;
4040
+ this.refreshTokenValue = canonical.record.refreshToken;
4041
+ }
4042
+ /** `null` when this instance does not persist. */
4043
+ readCanonicalSession() {
4044
+ if (!this.persistRecordKey || !this.persistMirrorKey || !this.persistMirrorRefreshKey) {
4045
+ return null;
4046
+ }
4047
+ return readSessionRecord(
4048
+ this.persistRecordKey,
4049
+ this.persistMirrorKey,
4050
+ this.persistMirrorRefreshKey
4051
+ );
4052
+ }
4053
+ /**
4054
+ * Publish a change to canonical storage.
4055
+ *
4056
+ * The canonical pair is re-read immediately before the single record write so
4057
+ * that a field this call does not change is taken from that read rather than
4058
+ * from this instance's possibly stale copy. That merge is BEST-EFFORT: it
4059
+ * removes the stale counterpart an instance would otherwise carry, but it is
4060
+ * not a compare-and-swap. Web Storage offers none, and the synchronous public
4061
+ * mutators cannot await the refresh lease, so a commit that lands between
4062
+ * this read and the store operation is lost by ordering — including the
4063
+ * counterpart just read, which is then republished stale. The window is one
4064
+ * store operation wide and the server remains authoritative; see
4065
+ * `CUST-AUTH-033` for the stated limit.
4066
+ *
4067
+ * Returns `false` only when `requireLiveSession` refused a write because the
4068
+ * canonical session had already been cleared.
4069
+ */
4070
+ commitPersistedSession(change, options) {
4071
+ const recordKey = this.persistRecordKey;
4072
+ const mirrorKey = this.persistMirrorKey;
4073
+ const mirrorRefreshKey = this.persistMirrorRefreshKey;
4074
+ const canonical = this.readCanonicalSession();
4075
+ if (canonical === null || !recordKey || !mirrorKey || !mirrorRefreshKey) {
4076
+ return true;
4077
+ }
4078
+ if (canonical === STORAGE_UNUSABLE) {
4079
+ this.disablePersistence();
4080
+ return true;
4081
+ }
4082
+ const current = canonical.record;
4083
+ if (options?.requireLiveSession && !current.token && !current.refreshToken) {
4084
+ return false;
4085
+ }
4086
+ const next = {
4087
+ v: SESSION_RECORD_VERSION,
4088
+ rev: current.rev + 1,
4089
+ token: "token" in change ? change.token ?? null : current.token,
4090
+ refreshToken: "refreshToken" in change ? change.refreshToken ?? null : current.refreshToken
4091
+ };
4092
+ if (
4093
+ // Only a state that is genuinely persisted can already say what this
4094
+ // write would say. An absent record reads as an empty pair, and treating
4095
+ // that as a no-op would drop the very tombstone that stops a later
4096
+ // migration from resurrecting the session out of the earlier layout.
4097
+ canonical.stored && next.token === current.token && next.refreshToken === current.refreshToken
4098
+ ) {
4099
+ this.sessionRev = current.rev;
4100
+ this.token = current.token;
4101
+ this.refreshTokenValue = current.refreshToken;
4102
+ return true;
4103
+ }
4104
+ try {
4105
+ localStorage.setItem(recordKey, JSON.stringify(next));
4106
+ } catch {
4107
+ this.disablePersistence();
4108
+ return true;
4109
+ }
4110
+ this.sessionRev = next.rev;
4111
+ this.token = next.token;
4112
+ this.refreshTokenValue = next.refreshToken;
4113
+ try {
4114
+ localStorage.removeItem(mirrorRefreshKey);
4115
+ } catch {
4116
+ return true;
4117
+ }
4118
+ try {
4119
+ if (next.token) localStorage.setItem(mirrorKey, next.token);
4120
+ else localStorage.removeItem(mirrorKey);
4121
+ } catch {
4122
+ }
4123
+ return true;
4124
+ }
4125
+ /**
4126
+ * Storage rejected a write or a read, so it is not a canonical store for this
4127
+ * instance. Fall back to the memory-only session and take no lease: there is
4128
+ * no shared state left to coordinate. Nothing is purged — the record commit
4129
+ * is atomic, so no inconsistent state was published, and another context's
4130
+ * valid session must not be destroyed by this instance's local failure.
4131
+ */
4132
+ disablePersistence() {
4133
+ this.persistRecordKey = null;
4134
+ this.persistMirrorKey = null;
4135
+ this.persistMirrorRefreshKey = null;
4136
+ this.refreshLockName = null;
4137
+ }
4138
+ /**
4139
+ * Compare the canonical record with the fence this call captured before it
4140
+ * left. Every persisted decision — rotate, converge, or fail closed — goes
4141
+ * through this one comparison. The revision catches a logout, a fresh login,
4142
+ * or a manual setter even when token values coincide; the value comparison
4143
+ * additionally catches a mirror-only writer that cannot bump a revision.
4144
+ */
4145
+ classifyCanonicalState(intended) {
4146
+ if (!this.persistRecordKey) return "current";
4147
+ this.syncFromPersistedStorage();
4148
+ if (!this.persistRecordKey) return "current";
4149
+ if (!this.token && !this.refreshTokenValue) return "cleared";
4150
+ if (intended.refreshToken !== null) {
4151
+ return this.refreshTokenValue === intended.refreshToken ? "current" : "superseded";
4152
+ }
4153
+ return this.token === intended.token && !this.refreshTokenValue ? "current" : "superseded";
4154
+ }
4155
+ /** The canonical persisted pair, as a refresh result. */
4156
+ canonicalPair(token) {
4157
+ return {
4158
+ token,
4159
+ ...this.refreshTokenValue ? { refreshToken: this.refreshTokenValue } : {}
4160
+ };
4161
+ }
4162
+ /** Current credentials plus the revision they were observed at. */
4163
+ currentFence() {
4164
+ return { token: this.token, refreshToken: this.refreshTokenValue };
4165
+ }
4166
+ /**
4167
+ * Clear an access credential a 401 just proved unusable — but only while it
4168
+ * is still the canonical one. A rotation committed by another context while
4169
+ * the request was in flight is newer than this 401, so it must survive; the
4170
+ * instance adopts it instead. The refresh entry is never touched here: a
4171
+ * rejected access token says nothing about the refresh family.
4172
+ */
4173
+ invalidateAccessToken(spentToken) {
4174
+ this.syncFromPersistedStorage();
4175
+ if (this.token !== spentToken) return;
4176
+ this.setToken(null);
4177
+ }
4178
+ /** Commit both credentials in one canonical write. */
4179
+ setSession(token, refreshToken, options) {
4180
+ this.token = token;
4181
+ this.refreshTokenValue = refreshToken;
4182
+ const committed = this.commitPersistedSession(
4183
+ { token, refreshToken },
4184
+ options
4185
+ );
4186
+ if (!committed) return false;
4187
+ this.onTokenChange?.(this.token);
4188
+ this.onRefreshTokenChange?.(this.refreshTokenValue);
4189
+ return true;
4190
+ }
4191
+ /**
4192
+ * Serialize rotation against every other context sharing the same storage
4193
+ * key and API/tenant identity. Coordination is best-effort: when it is
4194
+ * unavailable or times out the rotation still happens, so server-side reuse
4195
+ * detection is never bypassed by a client that failed to coordinate.
4196
+ */
4197
+ async rotateAcrossContexts(intended) {
4198
+ const lockName = this.refreshLockName;
4199
+ const requestLock = lockName ? getRefreshLockRequester() : null;
4200
+ if (!lockName || !requestLock) {
4201
+ return this.rotateWithCanonicalState(intended);
4202
+ }
4203
+ const controller = new AbortController();
4204
+ const timer = setTimeout(() => controller.abort(), REFRESH_LOCK_TIMEOUT);
4205
+ let entered = false;
4206
+ try {
4207
+ return await requestLock(
4208
+ lockName,
4209
+ { signal: controller.signal },
4210
+ async () => {
4211
+ entered = true;
4212
+ return this.rotateWithCanonicalState(intended);
4213
+ }
4214
+ );
4215
+ } catch (error) {
4216
+ if (entered) throw error;
4217
+ return this.rotateWithCanonicalState(intended);
4218
+ } finally {
4219
+ clearTimeout(timer);
4220
+ }
4221
+ }
4222
+ /**
4223
+ * Decide, against freshly re-read canonical state, whether this context still
4224
+ * owns the rotation.
4225
+ */
4226
+ async rotateWithCanonicalState(intended) {
4227
+ let fence = intended;
4228
+ for (let attempt = 0; attempt < 2; attempt += 1) {
4229
+ const state = this.classifyCanonicalState(fence);
4230
+ if (state === "cleared") throw new ApiError("Not authenticated", 401);
4231
+ if (state === "current") return this._doRefreshToken(fence);
4232
+ if (this.token) return this.canonicalPair(this.token);
4233
+ fence = this.currentFence();
4234
+ }
4235
+ throw new ApiError("Not authenticated", 401);
4236
+ }
4237
+ async _doRefreshToken(intended) {
4238
+ const init = intended.refreshToken ? {
3808
4239
  method: "POST",
3809
- body: JSON.stringify({ refreshToken: this.refreshTokenValue })
4240
+ body: JSON.stringify({ refreshToken: intended.refreshToken })
3810
4241
  } : {
3811
4242
  method: "POST",
3812
- headers: { Authorization: `Bearer ${this.token}` }
4243
+ headers: { Authorization: `Bearer ${intended.token}` }
3813
4244
  };
3814
4245
  const result = await this.requestJson(
3815
4246
  "/api/customers/refresh",
3816
4247
  init
3817
4248
  );
3818
- this.setToken(result.token);
3819
- if (result.refreshToken) this.setRefreshToken(result.refreshToken);
3820
- return result;
4249
+ const state = this.classifyCanonicalState(intended);
4250
+ if (state === "cleared") throw new ApiError("Not authenticated", 401);
4251
+ if (state === "superseded") {
4252
+ if (this.token) return this.canonicalPair(this.token);
4253
+ throw new ApiError("Not authenticated", 401);
4254
+ }
4255
+ const replacedAccessToken = this.token !== null && this.token !== intended.token ? this.token : null;
4256
+ const nextToken = replacedAccessToken ?? result.token;
4257
+ const nextRefresh = result.refreshToken ?? null;
4258
+ if (!this.setSession(nextToken, nextRefresh, { requireLiveSession: true })) {
4259
+ throw new ApiError("Not authenticated", 401);
4260
+ }
4261
+ return {
4262
+ token: nextToken,
4263
+ ...nextRefresh ? { refreshToken: nextRefresh } : {}
4264
+ };
3821
4265
  }
3822
4266
  /**
3823
- * Clear the stored token
4267
+ * Clear local credentials synchronously. This compatibility API cannot
4268
+ * revoke the server-side refresh family; use logoutAsync() for that.
4269
+ * @deprecated Use logoutAsync() to end the direct server session.
3824
4270
  */
3825
4271
  logout() {
3826
- this.setToken(null);
3827
- this.setRefreshToken(null);
4272
+ this.setSession(null, null);
4273
+ }
4274
+ /**
4275
+ * End the current direct refresh family and clear local credentials even if
4276
+ * revocation fails. A legacy access-only session has no refresh family here.
4277
+ */
4278
+ async logoutAsync() {
4279
+ this.syncFromPersistedStorage();
4280
+ const refreshToken = this.refreshTokenValue;
4281
+ this.logout();
4282
+ if (!refreshToken) return;
4283
+ const operation2 = CUSTOMER_AUTH_ENDPOINT_OPERATIONS.revokeRefresh;
4284
+ await this.requestJson(resolveOperationPath(operation2), {
4285
+ method: resolveOperationMethod(operation2),
4286
+ body: JSON.stringify({ refreshToken })
4287
+ });
4288
+ }
4289
+ /** Archive the current password account and clear this client's credentials on success. */
4290
+ async withdraw(currentPassword) {
4291
+ const token = this.getToken();
4292
+ if (!token) throw new ApiError("Not authenticated", 401);
4293
+ const operation2 = CUSTOMER_AUTH_ENDPOINT_OPERATIONS.withdraw;
4294
+ const result = await this.requestJson(
4295
+ resolveOperationPath(operation2),
4296
+ {
4297
+ method: resolveOperationMethod(operation2),
4298
+ headers: { Authorization: `Bearer ${token}` },
4299
+ body: JSON.stringify({ currentPassword })
4300
+ }
4301
+ );
4302
+ if (!result || typeof result.withdrawnAt !== "string" || !Number.isFinite(Date.parse(result.withdrawnAt)))
4303
+ throw new ApiError("Invalid customer withdrawal response", 502);
4304
+ this.logout();
4305
+ return { withdrawnAt: result.withdrawnAt };
3828
4306
  }
3829
4307
  /**
3830
4308
  * Get the current authenticated customer's profile
3831
4309
  */
3832
4310
  async me() {
3833
- if (!this.token) return null;
4311
+ const token = this.getToken();
4312
+ if (!token) return null;
3834
4313
  try {
3835
4314
  const data = await this.requestJson(
3836
4315
  "/api/customers/me",
3837
4316
  {
3838
4317
  method: "GET",
3839
- headers: { Authorization: `Bearer ${this.token}` }
4318
+ headers: { Authorization: `Bearer ${token}` }
3840
4319
  }
3841
4320
  );
3842
4321
  return data.customer ?? null;
3843
4322
  } catch (error) {
3844
4323
  if (error instanceof SDKError && error.status === 401) {
3845
- this.setToken(null);
4324
+ this.invalidateAccessToken(token);
3846
4325
  return null;
3847
4326
  }
3848
4327
  throw error;
@@ -3870,12 +4349,13 @@ var CustomerAuth = class {
3870
4349
  * Update the authenticated customer's profile (name, phone, marketingConsent)
3871
4350
  */
3872
4351
  async updateProfile(data) {
3873
- if (!this.token) throw new ApiError("Not authenticated", 401);
4352
+ const token = this.getToken();
4353
+ if (!token) throw new ApiError("Not authenticated", 401);
3874
4354
  const result = await this.requestJson(
3875
4355
  "/api/customers/me",
3876
4356
  {
3877
4357
  method: "PATCH",
3878
- headers: { Authorization: `Bearer ${this.token}` },
4358
+ headers: { Authorization: `Bearer ${token}` },
3879
4359
  body: JSON.stringify(data)
3880
4360
  }
3881
4361
  );
@@ -3885,10 +4365,11 @@ var CustomerAuth = class {
3885
4365
  * Change the password of the currently authenticated customer
3886
4366
  */
3887
4367
  async changePassword(currentPassword, newPassword) {
3888
- if (!this.token) throw new ApiError("Not authenticated", 401);
4368
+ const token = this.getToken();
4369
+ if (!token) throw new ApiError("Not authenticated", 401);
3889
4370
  await this.requestJson("/api/customers/change-password", {
3890
4371
  method: "POST",
3891
- headers: { Authorization: `Bearer ${this.token}` },
4372
+ headers: { Authorization: `Bearer ${token}` },
3892
4373
  body: JSON.stringify({ currentPassword, newPassword })
3893
4374
  });
3894
4375
  }
@@ -3896,7 +4377,8 @@ var CustomerAuth = class {
3896
4377
  * Get the authenticated customer's orders with pagination and optional status filter
3897
4378
  */
3898
4379
  async getMyOrders(options) {
3899
- if (!this.token) throw new ApiError("Not authenticated", 401);
4380
+ const token = this.getToken();
4381
+ if (!token) throw new ApiError("Not authenticated", 401);
3900
4382
  const params = new URLSearchParams();
3901
4383
  if (options?.page) params.set("page", String(options.page));
3902
4384
  if (options?.limit) params.set("limit", String(options.limit));
@@ -3918,21 +4400,27 @@ var CustomerAuth = class {
3918
4400
  `/api/customers/me/orders${qs ? `?${qs}` : ""}`,
3919
4401
  {
3920
4402
  method: "GET",
3921
- headers: { Authorization: `Bearer ${this.token}` }
4403
+ headers: { Authorization: `Bearer ${token}` }
3922
4404
  }
3923
4405
  );
3924
4406
  return validatePublicOrderList(result);
3925
4407
  }
3926
4408
  /**
3927
- * Get the current token (or null if not authenticated)
4409
+ * Get the current token (or null if not authenticated).
4410
+ *
4411
+ * In persisted mode this re-reads shared storage first, so a rotation or
4412
+ * logout performed by another tab is observed before the token is used.
3928
4413
  */
3929
4414
  getToken() {
4415
+ this.syncFromPersistedStorage();
3930
4416
  return this.token;
3931
4417
  }
3932
4418
  /**
3933
- * Get the current refresh token (or null if unavailable)
4419
+ * Get the current refresh token (or null if unavailable). Persisted mode
4420
+ * re-reads shared storage first, like {@link CustomerAuth.getToken}.
3934
4421
  */
3935
4422
  getRefreshToken() {
4423
+ this.syncFromPersistedStorage();
3936
4424
  return this.refreshTokenValue;
3937
4425
  }
3938
4426
  /**
@@ -3940,6 +4428,7 @@ var CustomerAuth = class {
3940
4428
  */
3941
4429
  setToken(token) {
3942
4430
  this.token = token;
4431
+ this.commitPersistedSession({ token });
3943
4432
  this.onTokenChange?.(token);
3944
4433
  }
3945
4434
  /**
@@ -3947,13 +4436,14 @@ var CustomerAuth = class {
3947
4436
  */
3948
4437
  setRefreshToken(refreshToken) {
3949
4438
  this.refreshTokenValue = refreshToken;
4439
+ this.commitPersistedSession({ refreshToken });
3950
4440
  this.onRefreshTokenChange?.(refreshToken);
3951
4441
  }
3952
4442
  /**
3953
4443
  * Check if the customer is currently authenticated
3954
4444
  */
3955
4445
  isAuthenticated() {
3956
- return this.token !== null;
4446
+ return this.getToken() !== null;
3957
4447
  }
3958
4448
  /**
3959
4449
  * Internal: make a request with timeout and error handling.
@@ -4299,8 +4789,12 @@ var PaymentSessionApi = class extends CustomerScopedApi {
4299
4789
  ...Object.keys(headers).length > 0 && { headers }
4300
4790
  });
4301
4791
  }
4302
- async paymentSession(result, expectedProvider) {
4303
- return validatePublicPaymentSession(await result, expectedProvider);
4792
+ async paymentSession(result, expectedProvider, operation2) {
4793
+ return validatePublicPaymentSession(
4794
+ await result,
4795
+ expectedProvider,
4796
+ operation2
4797
+ );
4304
4798
  }
4305
4799
  create(params) {
4306
4800
  const { idempotencyKey, ...body } = params;
@@ -4347,7 +4841,8 @@ var PaymentSessionApi = class extends CustomerScopedApi {
4347
4841
  const endpoint = this.serverTransport ? COMMERCE_ENDPOINTS.paymentSessions.server.cancel : COMMERCE_ENDPOINTS.paymentSessions.cancel;
4348
4842
  return this.paymentSession(
4349
4843
  this.post(endpoint, body, idempotencyKey),
4350
- "toss"
4844
+ "toss",
4845
+ "cancel"
4351
4846
  );
4352
4847
  }
4353
4848
  cancelPortOne(params) {
@@ -4356,7 +4851,8 @@ var PaymentSessionApi = class extends CustomerScopedApi {
4356
4851
  const endpoint = this.serverTransport ? COMMERCE_ENDPOINTS.paymentSessions.server.cancel : COMMERCE_ENDPOINTS.paymentSessions.cancel;
4357
4852
  return this.paymentSession(
4358
4853
  this.post(endpoint, body, idempotencyKey),
4359
- "portone"
4854
+ "portone",
4855
+ "cancel"
4360
4856
  );
4361
4857
  }
4362
4858
  confirm(params) {