@01.software/sdk 0.51.0 → 0.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +54 -11
  3. package/dist/client.cjs +420 -53
  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 +420 -53
  8. package/dist/client.js.map +1 -1
  9. package/dist/{collection-client-hYvVfHIm.d.ts → collection-client-Bmr53saa.d.ts} +4 -4
  10. package/dist/{collection-client-CWAeuzV4.d.cts → collection-client-DxuBDso_.d.cts} +4 -4
  11. package/dist/{const-D8LixhAJ.d.cts → const-BKUqg96Q.d.ts} +2 -2
  12. package/dist/{const-JD8obaKg.d.ts → const-Cv1DCsp6.d.cts} +2 -2
  13. package/dist/{index-Bu0kCc6o.d.cts → index-CGsT4AQ8.d.cts} +24 -7
  14. package/dist/{index-B0Hjh3Ey.d.ts → index-DFHLLqYu.d.ts} +24 -7
  15. package/dist/index.cjs +434 -55
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +11 -10
  18. package/dist/index.d.ts +11 -10
  19. package/dist/index.js +434 -55
  20. package/dist/index.js.map +1 -1
  21. package/dist/{payload-types-BOsckg8B.d.cts → payload-types-Bk-duc5k.d.cts} +295 -100
  22. package/dist/{payload-types-BOsckg8B.d.ts → payload-types-Bk-duc5k.d.ts} +295 -100
  23. package/dist/query.cjs +14 -1
  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 +14 -1
  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 +98 -13
  32. package/dist/server.cjs.map +1 -1
  33. package/dist/server.d.cts +9 -8
  34. package/dist/server.d.ts +9 -8
  35. package/dist/server.js +98 -13
  36. package/dist/server.js.map +1 -1
  37. package/dist/{types-Bj8j7VHU.d.ts → types-CEK8uTvU.d.ts} +212 -11
  38. package/dist/{types-B14sEt1z.d.cts → types-CNdpj2fI.d.cts} +2 -2
  39. package/dist/{types-5ZlcAFuU.d.ts → types-D4mjGFj4.d.ts} +2 -2
  40. package/dist/{types-CM5qiR2k.d.cts → types-Dhk5NX7Z.d.cts} +212 -11
  41. package/dist/ui/form.d.cts +1 -1
  42. package/dist/ui/form.d.ts +1 -1
  43. package/dist/ui/video.d.cts +1 -1
  44. package/dist/ui/video.d.ts +1 -1
  45. package/dist/webhook.cjs +13 -2
  46. package/dist/webhook.cjs.map +1 -1
  47. package/dist/webhook.d.cts +5 -5
  48. package/dist/webhook.d.ts +5 -5
  49. package/dist/webhook.js +13 -2
  50. package/dist/webhook.js.map +1 -1
  51. package/package.json +6 -5
package/dist/client.cjs CHANGED
@@ -1396,6 +1396,11 @@ var COMMERCE_ENDPOINT_OPERATIONS = {
1396
1396
  detail: strictCommerceOperation("checkouts", "post", "/detail"),
1397
1397
  query: strictCommerceOperation("checkouts", "post", "/query")
1398
1398
  },
1399
+ draftOrders: {
1400
+ calculate: operation("draft-orders", "post", "/:id/calculate"),
1401
+ complete: operation("draft-orders", "post", "/:id/complete"),
1402
+ finalize: operation("draft-orders", "post", "/:id/finalize")
1403
+ },
1399
1404
  transactions: {
1400
1405
  update: operation("transactions", "post", "/update")
1401
1406
  },
@@ -1496,7 +1501,15 @@ var portOneStandardActionCapabilityOperations = /* @__PURE__ */ new Set([
1496
1501
  var MEMBERSHIP_ENDPOINT_OPERATIONS = {
1497
1502
  subscribe: operation(null, "post", "/memberships/subscribe"),
1498
1503
  cancel: operation(null, "post", "/memberships/:id/cancel"),
1499
- me: operation(null, "get", "/memberships/me")
1504
+ me: operation(null, "get", "/memberships/me"),
1505
+ chargeResult: operation(null, "post", "/memberships/:id/charge-result"),
1506
+ refundResult: operation(null, "post", "/memberships/:id/refund-result"),
1507
+ revoke: operation(null, "post", "/memberships/:id/revoke")
1508
+ };
1509
+ var CUSTOMER_GROUP_ENDPOINT_OPERATIONS = {
1510
+ members: operation("customer-groups", "get", "/:id/members"),
1511
+ grant: operation("customer-groups", "post", "/:id/members/grant"),
1512
+ revoke: operation("customer-groups", "post", "/:id/members/revoke")
1500
1513
  };
1501
1514
  var EMBEDDED_ADMIN_ENDPOINT_OPERATIONS = {
1502
1515
  show: operation("tenants", "get", "/embedded-admin-app"),
@@ -2206,7 +2219,10 @@ var ORDER_PRIMARY_DISPLAY_STATUSES = [
2206
2219
  ];
2207
2220
  var CHECKOUT_STATUSES = ["open", "placed", "abandoned"];
2208
2221
  var ABANDONED_REASONS = ["expired", "customer", "operator"];
2209
- var PAYMENT_PROVIDERS = ["toss", "portone"];
2222
+ function managedPaymentProviders(providers) {
2223
+ return providers;
2224
+ }
2225
+ var PAYMENT_PROVIDERS = managedPaymentProviders(["toss", "portone"]);
2210
2226
  var PAYMENT_PROVIDER_STATUSES = [
2211
2227
  "requires_payment_method",
2212
2228
  "requires_action",
@@ -2254,6 +2270,26 @@ function invalidCanonicalResponse(resource, details) {
2254
2270
  function fail(resource, path, expected) {
2255
2271
  invalidCanonicalResponse(resource, { path, expected });
2256
2272
  }
2273
+ function providerMismatch(resource, expected, actual, operation2) {
2274
+ if (operation2 === "cancel") {
2275
+ const method = {
2276
+ toss: "cancel()",
2277
+ portone: "cancelPortOne()"
2278
+ }[actual];
2279
+ throw createApiError(
2280
+ `Canonical ${resource} response provider mismatch: expected ${expected}, received ${actual}.`,
2281
+ 502,
2282
+ { resource, path: "$.provider", expected, actual },
2283
+ `The server applied the cancellation to a ${actual} PaymentSession, but this SDK method expects ${expected}.`,
2284
+ `Re-invoke ${method} with the same idempotencyKey; do not create a new key.`
2285
+ );
2286
+ }
2287
+ invalidCanonicalResponse(resource, {
2288
+ path: "$.provider",
2289
+ expected,
2290
+ actual
2291
+ });
2292
+ }
2257
2293
  function record(value, resource, path) {
2258
2294
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2259
2295
  fail(resource, path, "object");
@@ -3546,7 +3582,7 @@ function validateNextAction(value, provider, resource, path) {
3546
3582
  if (action.customerEmail !== void 0)
3547
3583
  validateCustomer({ email: action.customerEmail }, resource, path);
3548
3584
  }
3549
- function validatePublicPaymentSession(value, expectedProvider) {
3585
+ function validatePublicPaymentSession(value, expectedProvider, operation2) {
3550
3586
  const resource = "PaymentSession";
3551
3587
  const session = record(value, resource, "$");
3552
3588
  const provider = enumValue(
@@ -3555,9 +3591,6 @@ function validatePublicPaymentSession(value, expectedProvider) {
3555
3591
  resource,
3556
3592
  "$.provider"
3557
3593
  );
3558
- if (expectedProvider !== void 0 && provider !== expectedProvider) {
3559
- fail(resource, "$.provider", expectedProvider);
3560
- }
3561
3594
  const status = enumValue(
3562
3595
  session.providerStatus,
3563
3596
  PAYMENT_PROVIDER_STATUSES,
@@ -3761,44 +3794,128 @@ function validatePublicPaymentSession(value, expectedProvider) {
3761
3794
  "$.cancellationReason"
3762
3795
  );
3763
3796
  }
3797
+ if (expectedProvider !== void 0 && provider !== expectedProvider) {
3798
+ if (operation2 === "cancel" && status !== "canceling" && status !== "canceled") {
3799
+ fail(
3800
+ resource,
3801
+ "$.providerStatus",
3802
+ "canceling or canceled after a successful cancellation"
3803
+ );
3804
+ }
3805
+ providerMismatch(resource, expectedProvider, provider, operation2);
3806
+ }
3764
3807
  return value;
3765
3808
  }
3766
3809
 
3767
3810
  // src/core/customer/customer-auth.ts
3768
3811
  var DEFAULT_TIMEOUT2 = 15e3;
3769
- function safeGetItem(key) {
3812
+ var REFRESH_LOCK_TIMEOUT = DEFAULT_TIMEOUT2 * 3;
3813
+ var SESSION_RECORD_VERSION = 1;
3814
+ var STORAGE_UNUSABLE = /* @__PURE__ */ Symbol("storage-unusable");
3815
+ function canonicalApiIdentity(apiUrl) {
3770
3816
  try {
3771
- return localStorage.getItem(key);
3817
+ const url = new URL(apiUrl);
3818
+ return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
3819
+ } catch {
3820
+ return apiUrl.trim().toLowerCase().replace(/\/+$/, "");
3821
+ }
3822
+ }
3823
+ function buildRefreshLockName(apiUrl, publishableKey, storageKey) {
3824
+ return `01software:customer-auth-refresh:${canonicalApiIdentity(apiUrl)}:${publishableKey}:${storageKey}`;
3825
+ }
3826
+ function getRefreshLockRequester() {
3827
+ try {
3828
+ if (typeof navigator === "undefined") return null;
3829
+ const locks = navigator.locks;
3830
+ if (!locks || typeof locks.request !== "function") return null;
3831
+ return (name, options, callback) => locks.request(
3832
+ name,
3833
+ options,
3834
+ () => callback()
3835
+ );
3836
+ } catch {
3837
+ return null;
3838
+ }
3839
+ }
3840
+ function parseSessionRecord(raw) {
3841
+ try {
3842
+ const parsed = JSON.parse(raw);
3843
+ if (!parsed || typeof parsed !== "object") return null;
3844
+ const candidate = parsed;
3845
+ if (candidate.v !== SESSION_RECORD_VERSION) return null;
3846
+ if (typeof candidate.rev !== "number" || !Number.isFinite(candidate.rev)) {
3847
+ return null;
3848
+ }
3849
+ const token = candidate.token;
3850
+ const refreshToken = candidate.refreshToken;
3851
+ if (token !== null && typeof token !== "string") return null;
3852
+ if (refreshToken !== null && typeof refreshToken !== "string") return null;
3853
+ return {
3854
+ v: SESSION_RECORD_VERSION,
3855
+ rev: candidate.rev,
3856
+ token,
3857
+ refreshToken
3858
+ };
3772
3859
  } catch {
3773
3860
  return null;
3774
3861
  }
3775
3862
  }
3863
+ function readSessionRecord(recordKey, mirrorKey, mirrorRefreshKey) {
3864
+ let raw;
3865
+ try {
3866
+ raw = localStorage.getItem(recordKey);
3867
+ } catch {
3868
+ return STORAGE_UNUSABLE;
3869
+ }
3870
+ if (raw !== null) {
3871
+ const parsed = parseSessionRecord(raw);
3872
+ return parsed ? { record: parsed, stored: true } : STORAGE_UNUSABLE;
3873
+ }
3874
+ try {
3875
+ return {
3876
+ record: {
3877
+ v: SESSION_RECORD_VERSION,
3878
+ rev: 0,
3879
+ token: localStorage.getItem(mirrorKey),
3880
+ refreshToken: localStorage.getItem(mirrorRefreshKey)
3881
+ },
3882
+ stored: false
3883
+ };
3884
+ } catch {
3885
+ return STORAGE_UNUSABLE;
3886
+ }
3887
+ }
3776
3888
  var CustomerAuth = class {
3777
3889
  constructor(publishableKey, options, apiUrl) {
3778
3890
  this.refreshPromise = null;
3891
+ /** Canonical record key; null unless persistence is active in a browser. */
3892
+ this.persistRecordKey = null;
3893
+ /** Advisory single-value access mirror, plus the retired refresh mirror. */
3894
+ this.persistMirrorKey = null;
3895
+ this.persistMirrorRefreshKey = null;
3896
+ /** Cross-context refresh lock name; null when there is no shared storage. */
3897
+ this.refreshLockName = null;
3898
+ /** Revision of the canonical record this instance last observed. */
3899
+ this.sessionRev = 0;
3779
3900
  this.publishableKey = publishableKey;
3780
3901
  this.baseUrl = resolveApiUrl(apiUrl);
3781
3902
  const persist = options?.persist ?? true;
3782
3903
  if (persist) {
3783
3904
  const key = typeof persist === "string" ? persist : "customer-token";
3784
- const refreshKey = `${key}:refresh`;
3785
3905
  const isBrowser = typeof window !== "undefined";
3786
- this.token = isBrowser ? safeGetItem(key) : null;
3787
- this.refreshTokenValue = isBrowser ? safeGetItem(refreshKey) : null;
3788
- this.onTokenChange = isBrowser ? (token) => {
3789
- try {
3790
- if (token) localStorage.setItem(key, token);
3791
- else localStorage.removeItem(key);
3792
- } catch {
3793
- }
3794
- } : void 0;
3795
- this.onRefreshTokenChange = isBrowser ? (refreshToken) => {
3796
- try {
3797
- if (refreshToken) localStorage.setItem(refreshKey, refreshToken);
3798
- else localStorage.removeItem(refreshKey);
3799
- } catch {
3800
- }
3801
- } : void 0;
3906
+ this.token = null;
3907
+ this.refreshTokenValue = null;
3908
+ if (isBrowser) {
3909
+ this.persistRecordKey = `${key}:session`;
3910
+ this.persistMirrorKey = key;
3911
+ this.persistMirrorRefreshKey = `${key}:refresh`;
3912
+ this.refreshLockName = buildRefreshLockName(
3913
+ this.baseUrl,
3914
+ publishableKey,
3915
+ key
3916
+ );
3917
+ this.syncFromPersistedStorage();
3918
+ }
3802
3919
  } else {
3803
3920
  this.token = options?.token ?? null;
3804
3921
  this.refreshTokenValue = options?.refreshToken ?? null;
@@ -3826,66 +3943,299 @@ var CustomerAuth = class {
3826
3943
  body: JSON.stringify(data)
3827
3944
  }
3828
3945
  );
3829
- this.setToken(result.token);
3830
- if (result.refreshToken) this.setRefreshToken(result.refreshToken);
3946
+ if (result.refreshToken) this.setSession(result.token, result.refreshToken);
3947
+ else this.setToken(result.token);
3831
3948
  return result;
3832
3949
  }
3833
3950
  /**
3834
3951
  * Refresh the current token. Uses a refresh token when available, otherwise
3835
3952
  * falls back to the legacy bearer-token compatibility path.
3953
+ *
3954
+ * Refresh tokens are single-use, so a persisted session shared by several
3955
+ * tabs or SDK instances must rotate at most once: concurrent rotations of the
3956
+ * same token are replay from the server's point of view and revoke the whole
3957
+ * family. Persisted browser sessions therefore serialize rotation across
3958
+ * contexts and re-read the canonical persisted credentials inside the lock.
3836
3959
  */
3837
3960
  async refreshToken() {
3961
+ this.syncFromPersistedStorage();
3838
3962
  if (!this.refreshTokenValue && !this.token) {
3839
3963
  throw new ApiError("Not authenticated", 401);
3840
3964
  }
3841
3965
  if (this.refreshPromise) return this.refreshPromise;
3842
- this.refreshPromise = this._doRefreshToken();
3966
+ const intended = this.currentFence();
3967
+ this.refreshPromise = this.rotateAcrossContexts(intended);
3843
3968
  try {
3844
3969
  return await this.refreshPromise;
3845
3970
  } finally {
3846
3971
  this.refreshPromise = null;
3847
3972
  }
3848
3973
  }
3849
- async _doRefreshToken() {
3850
- const init = this.refreshTokenValue ? {
3974
+ /**
3975
+ * Re-read the canonical persisted session. In persisted mode the shared
3976
+ * record — not this instance's memory — is the source of truth, so another
3977
+ * context's rotation, login, or logout is observed before the next credential
3978
+ * use. Change callbacks are deliberately not invoked: this is a read.
3979
+ */
3980
+ syncFromPersistedStorage() {
3981
+ const canonical = this.readCanonicalSession();
3982
+ if (canonical === STORAGE_UNUSABLE) {
3983
+ this.disablePersistence();
3984
+ return;
3985
+ }
3986
+ if (!canonical) return;
3987
+ this.sessionRev = canonical.record.rev;
3988
+ this.token = canonical.record.token;
3989
+ this.refreshTokenValue = canonical.record.refreshToken;
3990
+ }
3991
+ /** `null` when this instance does not persist. */
3992
+ readCanonicalSession() {
3993
+ if (!this.persistRecordKey || !this.persistMirrorKey || !this.persistMirrorRefreshKey) {
3994
+ return null;
3995
+ }
3996
+ return readSessionRecord(
3997
+ this.persistRecordKey,
3998
+ this.persistMirrorKey,
3999
+ this.persistMirrorRefreshKey
4000
+ );
4001
+ }
4002
+ /**
4003
+ * Publish a change to canonical storage.
4004
+ *
4005
+ * The canonical pair is re-read immediately before the single record write so
4006
+ * that a field this call does not change is taken from that read rather than
4007
+ * from this instance's possibly stale copy. That merge is BEST-EFFORT: it
4008
+ * removes the stale counterpart an instance would otherwise carry, but it is
4009
+ * not a compare-and-swap. Web Storage offers none, and the synchronous public
4010
+ * mutators cannot await the refresh lease, so a commit that lands between
4011
+ * this read and the store operation is lost by ordering — including the
4012
+ * counterpart just read, which is then republished stale. The window is one
4013
+ * store operation wide and the server remains authoritative; see
4014
+ * `CUST-AUTH-033` for the stated limit.
4015
+ *
4016
+ * Returns `false` only when `requireLiveSession` refused a write because the
4017
+ * canonical session had already been cleared.
4018
+ */
4019
+ commitPersistedSession(change, options) {
4020
+ const recordKey = this.persistRecordKey;
4021
+ const mirrorKey = this.persistMirrorKey;
4022
+ const mirrorRefreshKey = this.persistMirrorRefreshKey;
4023
+ const canonical = this.readCanonicalSession();
4024
+ if (canonical === null || !recordKey || !mirrorKey || !mirrorRefreshKey) {
4025
+ return true;
4026
+ }
4027
+ if (canonical === STORAGE_UNUSABLE) {
4028
+ this.disablePersistence();
4029
+ return true;
4030
+ }
4031
+ const current = canonical.record;
4032
+ if (options?.requireLiveSession && !current.token && !current.refreshToken) {
4033
+ return false;
4034
+ }
4035
+ const next = {
4036
+ v: SESSION_RECORD_VERSION,
4037
+ rev: current.rev + 1,
4038
+ token: "token" in change ? change.token ?? null : current.token,
4039
+ refreshToken: "refreshToken" in change ? change.refreshToken ?? null : current.refreshToken
4040
+ };
4041
+ if (
4042
+ // Only a state that is genuinely persisted can already say what this
4043
+ // write would say. An absent record reads as an empty pair, and treating
4044
+ // that as a no-op would drop the very tombstone that stops a later
4045
+ // migration from resurrecting the session out of the earlier layout.
4046
+ canonical.stored && next.token === current.token && next.refreshToken === current.refreshToken
4047
+ ) {
4048
+ this.sessionRev = current.rev;
4049
+ this.token = current.token;
4050
+ this.refreshTokenValue = current.refreshToken;
4051
+ return true;
4052
+ }
4053
+ try {
4054
+ localStorage.setItem(recordKey, JSON.stringify(next));
4055
+ } catch {
4056
+ this.disablePersistence();
4057
+ return true;
4058
+ }
4059
+ this.sessionRev = next.rev;
4060
+ this.token = next.token;
4061
+ this.refreshTokenValue = next.refreshToken;
4062
+ try {
4063
+ localStorage.removeItem(mirrorRefreshKey);
4064
+ } catch {
4065
+ return true;
4066
+ }
4067
+ try {
4068
+ if (next.token) localStorage.setItem(mirrorKey, next.token);
4069
+ else localStorage.removeItem(mirrorKey);
4070
+ } catch {
4071
+ }
4072
+ return true;
4073
+ }
4074
+ /**
4075
+ * Storage rejected a write or a read, so it is not a canonical store for this
4076
+ * instance. Fall back to the memory-only session and take no lease: there is
4077
+ * no shared state left to coordinate. Nothing is purged — the record commit
4078
+ * is atomic, so no inconsistent state was published, and another context's
4079
+ * valid session must not be destroyed by this instance's local failure.
4080
+ */
4081
+ disablePersistence() {
4082
+ this.persistRecordKey = null;
4083
+ this.persistMirrorKey = null;
4084
+ this.persistMirrorRefreshKey = null;
4085
+ this.refreshLockName = null;
4086
+ }
4087
+ /**
4088
+ * Compare the canonical record with the fence this call captured before it
4089
+ * left. Every persisted decision — rotate, converge, or fail closed — goes
4090
+ * through this one comparison. The revision catches a logout, a fresh login,
4091
+ * or a manual setter even when token values coincide; the value comparison
4092
+ * additionally catches a mirror-only writer that cannot bump a revision.
4093
+ */
4094
+ classifyCanonicalState(intended) {
4095
+ if (!this.persistRecordKey) return "current";
4096
+ this.syncFromPersistedStorage();
4097
+ if (!this.persistRecordKey) return "current";
4098
+ if (!this.token && !this.refreshTokenValue) return "cleared";
4099
+ if (intended.refreshToken !== null) {
4100
+ return this.refreshTokenValue === intended.refreshToken ? "current" : "superseded";
4101
+ }
4102
+ return this.token === intended.token && !this.refreshTokenValue ? "current" : "superseded";
4103
+ }
4104
+ /** The canonical persisted pair, as a refresh result. */
4105
+ canonicalPair(token) {
4106
+ return {
4107
+ token,
4108
+ ...this.refreshTokenValue ? { refreshToken: this.refreshTokenValue } : {}
4109
+ };
4110
+ }
4111
+ /** Current credentials plus the revision they were observed at. */
4112
+ currentFence() {
4113
+ return { token: this.token, refreshToken: this.refreshTokenValue };
4114
+ }
4115
+ /**
4116
+ * Clear an access credential a 401 just proved unusable — but only while it
4117
+ * is still the canonical one. A rotation committed by another context while
4118
+ * the request was in flight is newer than this 401, so it must survive; the
4119
+ * instance adopts it instead. The refresh entry is never touched here: a
4120
+ * rejected access token says nothing about the refresh family.
4121
+ */
4122
+ invalidateAccessToken(spentToken) {
4123
+ this.syncFromPersistedStorage();
4124
+ if (this.token !== spentToken) return;
4125
+ this.setToken(null);
4126
+ }
4127
+ /** Commit both credentials in one canonical write. */
4128
+ setSession(token, refreshToken, options) {
4129
+ this.token = token;
4130
+ this.refreshTokenValue = refreshToken;
4131
+ const committed = this.commitPersistedSession(
4132
+ { token, refreshToken },
4133
+ options
4134
+ );
4135
+ if (!committed) return false;
4136
+ this.onTokenChange?.(this.token);
4137
+ this.onRefreshTokenChange?.(this.refreshTokenValue);
4138
+ return true;
4139
+ }
4140
+ /**
4141
+ * Serialize rotation against every other context sharing the same storage
4142
+ * key and API/tenant identity. Coordination is best-effort: when it is
4143
+ * unavailable or times out the rotation still happens, so server-side reuse
4144
+ * detection is never bypassed by a client that failed to coordinate.
4145
+ */
4146
+ async rotateAcrossContexts(intended) {
4147
+ const lockName = this.refreshLockName;
4148
+ const requestLock = lockName ? getRefreshLockRequester() : null;
4149
+ if (!lockName || !requestLock) {
4150
+ return this.rotateWithCanonicalState(intended);
4151
+ }
4152
+ const controller = new AbortController();
4153
+ const timer = setTimeout(() => controller.abort(), REFRESH_LOCK_TIMEOUT);
4154
+ let entered = false;
4155
+ try {
4156
+ return await requestLock(
4157
+ lockName,
4158
+ { signal: controller.signal },
4159
+ async () => {
4160
+ entered = true;
4161
+ return this.rotateWithCanonicalState(intended);
4162
+ }
4163
+ );
4164
+ } catch (error) {
4165
+ if (entered) throw error;
4166
+ return this.rotateWithCanonicalState(intended);
4167
+ } finally {
4168
+ clearTimeout(timer);
4169
+ }
4170
+ }
4171
+ /**
4172
+ * Decide, against freshly re-read canonical state, whether this context still
4173
+ * owns the rotation.
4174
+ */
4175
+ async rotateWithCanonicalState(intended) {
4176
+ let fence = intended;
4177
+ for (let attempt = 0; attempt < 2; attempt += 1) {
4178
+ const state = this.classifyCanonicalState(fence);
4179
+ if (state === "cleared") throw new ApiError("Not authenticated", 401);
4180
+ if (state === "current") return this._doRefreshToken(fence);
4181
+ if (this.token) return this.canonicalPair(this.token);
4182
+ fence = this.currentFence();
4183
+ }
4184
+ throw new ApiError("Not authenticated", 401);
4185
+ }
4186
+ async _doRefreshToken(intended) {
4187
+ const init = intended.refreshToken ? {
3851
4188
  method: "POST",
3852
- body: JSON.stringify({ refreshToken: this.refreshTokenValue })
4189
+ body: JSON.stringify({ refreshToken: intended.refreshToken })
3853
4190
  } : {
3854
4191
  method: "POST",
3855
- headers: { Authorization: `Bearer ${this.token}` }
4192
+ headers: { Authorization: `Bearer ${intended.token}` }
3856
4193
  };
3857
4194
  const result = await this.requestJson(
3858
4195
  "/api/customers/refresh",
3859
4196
  init
3860
4197
  );
3861
- this.setToken(result.token);
3862
- if (result.refreshToken) this.setRefreshToken(result.refreshToken);
3863
- return result;
4198
+ const state = this.classifyCanonicalState(intended);
4199
+ if (state === "cleared") throw new ApiError("Not authenticated", 401);
4200
+ if (state === "superseded") {
4201
+ if (this.token) return this.canonicalPair(this.token);
4202
+ throw new ApiError("Not authenticated", 401);
4203
+ }
4204
+ const replacedAccessToken = this.token !== null && this.token !== intended.token ? this.token : null;
4205
+ const nextToken = replacedAccessToken ?? result.token;
4206
+ const nextRefresh = result.refreshToken ?? null;
4207
+ if (!this.setSession(nextToken, nextRefresh, { requireLiveSession: true })) {
4208
+ throw new ApiError("Not authenticated", 401);
4209
+ }
4210
+ return {
4211
+ token: nextToken,
4212
+ ...nextRefresh ? { refreshToken: nextRefresh } : {}
4213
+ };
3864
4214
  }
3865
4215
  /**
3866
4216
  * Clear the stored token
3867
4217
  */
3868
4218
  logout() {
3869
- this.setToken(null);
3870
- this.setRefreshToken(null);
4219
+ this.setSession(null, null);
3871
4220
  }
3872
4221
  /**
3873
4222
  * Get the current authenticated customer's profile
3874
4223
  */
3875
4224
  async me() {
3876
- if (!this.token) return null;
4225
+ const token = this.getToken();
4226
+ if (!token) return null;
3877
4227
  try {
3878
4228
  const data = await this.requestJson(
3879
4229
  "/api/customers/me",
3880
4230
  {
3881
4231
  method: "GET",
3882
- headers: { Authorization: `Bearer ${this.token}` }
4232
+ headers: { Authorization: `Bearer ${token}` }
3883
4233
  }
3884
4234
  );
3885
4235
  return data.customer ?? null;
3886
4236
  } catch (error) {
3887
4237
  if (error instanceof SDKError && error.status === 401) {
3888
- this.setToken(null);
4238
+ this.invalidateAccessToken(token);
3889
4239
  return null;
3890
4240
  }
3891
4241
  throw error;
@@ -3913,12 +4263,13 @@ var CustomerAuth = class {
3913
4263
  * Update the authenticated customer's profile (name, phone, marketingConsent)
3914
4264
  */
3915
4265
  async updateProfile(data) {
3916
- if (!this.token) throw new ApiError("Not authenticated", 401);
4266
+ const token = this.getToken();
4267
+ if (!token) throw new ApiError("Not authenticated", 401);
3917
4268
  const result = await this.requestJson(
3918
4269
  "/api/customers/me",
3919
4270
  {
3920
4271
  method: "PATCH",
3921
- headers: { Authorization: `Bearer ${this.token}` },
4272
+ headers: { Authorization: `Bearer ${token}` },
3922
4273
  body: JSON.stringify(data)
3923
4274
  }
3924
4275
  );
@@ -3928,10 +4279,11 @@ var CustomerAuth = class {
3928
4279
  * Change the password of the currently authenticated customer
3929
4280
  */
3930
4281
  async changePassword(currentPassword, newPassword) {
3931
- if (!this.token) throw new ApiError("Not authenticated", 401);
4282
+ const token = this.getToken();
4283
+ if (!token) throw new ApiError("Not authenticated", 401);
3932
4284
  await this.requestJson("/api/customers/change-password", {
3933
4285
  method: "POST",
3934
- headers: { Authorization: `Bearer ${this.token}` },
4286
+ headers: { Authorization: `Bearer ${token}` },
3935
4287
  body: JSON.stringify({ currentPassword, newPassword })
3936
4288
  });
3937
4289
  }
@@ -3939,7 +4291,8 @@ var CustomerAuth = class {
3939
4291
  * Get the authenticated customer's orders with pagination and optional status filter
3940
4292
  */
3941
4293
  async getMyOrders(options) {
3942
- if (!this.token) throw new ApiError("Not authenticated", 401);
4294
+ const token = this.getToken();
4295
+ if (!token) throw new ApiError("Not authenticated", 401);
3943
4296
  const params = new URLSearchParams();
3944
4297
  if (options?.page) params.set("page", String(options.page));
3945
4298
  if (options?.limit) params.set("limit", String(options.limit));
@@ -3961,21 +4314,27 @@ var CustomerAuth = class {
3961
4314
  `/api/customers/me/orders${qs ? `?${qs}` : ""}`,
3962
4315
  {
3963
4316
  method: "GET",
3964
- headers: { Authorization: `Bearer ${this.token}` }
4317
+ headers: { Authorization: `Bearer ${token}` }
3965
4318
  }
3966
4319
  );
3967
4320
  return validatePublicOrderList(result);
3968
4321
  }
3969
4322
  /**
3970
- * Get the current token (or null if not authenticated)
4323
+ * Get the current token (or null if not authenticated).
4324
+ *
4325
+ * In persisted mode this re-reads shared storage first, so a rotation or
4326
+ * logout performed by another tab is observed before the token is used.
3971
4327
  */
3972
4328
  getToken() {
4329
+ this.syncFromPersistedStorage();
3973
4330
  return this.token;
3974
4331
  }
3975
4332
  /**
3976
- * Get the current refresh token (or null if unavailable)
4333
+ * Get the current refresh token (or null if unavailable). Persisted mode
4334
+ * re-reads shared storage first, like {@link CustomerAuth.getToken}.
3977
4335
  */
3978
4336
  getRefreshToken() {
4337
+ this.syncFromPersistedStorage();
3979
4338
  return this.refreshTokenValue;
3980
4339
  }
3981
4340
  /**
@@ -3983,6 +4342,7 @@ var CustomerAuth = class {
3983
4342
  */
3984
4343
  setToken(token) {
3985
4344
  this.token = token;
4345
+ this.commitPersistedSession({ token });
3986
4346
  this.onTokenChange?.(token);
3987
4347
  }
3988
4348
  /**
@@ -3990,13 +4350,14 @@ var CustomerAuth = class {
3990
4350
  */
3991
4351
  setRefreshToken(refreshToken) {
3992
4352
  this.refreshTokenValue = refreshToken;
4353
+ this.commitPersistedSession({ refreshToken });
3993
4354
  this.onRefreshTokenChange?.(refreshToken);
3994
4355
  }
3995
4356
  /**
3996
4357
  * Check if the customer is currently authenticated
3997
4358
  */
3998
4359
  isAuthenticated() {
3999
- return this.token !== null;
4360
+ return this.getToken() !== null;
4000
4361
  }
4001
4362
  /**
4002
4363
  * Internal: make a request with timeout and error handling.
@@ -4342,8 +4703,12 @@ var PaymentSessionApi = class extends CustomerScopedApi {
4342
4703
  ...Object.keys(headers).length > 0 && { headers }
4343
4704
  });
4344
4705
  }
4345
- async paymentSession(result, expectedProvider) {
4346
- return validatePublicPaymentSession(await result, expectedProvider);
4706
+ async paymentSession(result, expectedProvider, operation2) {
4707
+ return validatePublicPaymentSession(
4708
+ await result,
4709
+ expectedProvider,
4710
+ operation2
4711
+ );
4347
4712
  }
4348
4713
  create(params) {
4349
4714
  const { idempotencyKey, ...body } = params;
@@ -4390,7 +4755,8 @@ var PaymentSessionApi = class extends CustomerScopedApi {
4390
4755
  const endpoint = this.serverTransport ? COMMERCE_ENDPOINTS.paymentSessions.server.cancel : COMMERCE_ENDPOINTS.paymentSessions.cancel;
4391
4756
  return this.paymentSession(
4392
4757
  this.post(endpoint, body, idempotencyKey),
4393
- "toss"
4758
+ "toss",
4759
+ "cancel"
4394
4760
  );
4395
4761
  }
4396
4762
  cancelPortOne(params) {
@@ -4399,7 +4765,8 @@ var PaymentSessionApi = class extends CustomerScopedApi {
4399
4765
  const endpoint = this.serverTransport ? COMMERCE_ENDPOINTS.paymentSessions.server.cancel : COMMERCE_ENDPOINTS.paymentSessions.cancel;
4400
4766
  return this.paymentSession(
4401
4767
  this.post(endpoint, body, idempotencyKey),
4402
- "portone"
4768
+ "portone",
4769
+ "cancel"
4403
4770
  );
4404
4771
  }
4405
4772
  confirm(params) {