@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.
- package/CHANGELOG.md +127 -0
- package/README.md +54 -11
- package/dist/client.cjs +420 -53
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +6 -6
- package/dist/client.d.ts +6 -6
- package/dist/client.js +420 -53
- package/dist/client.js.map +1 -1
- package/dist/{collection-client-hYvVfHIm.d.ts → collection-client-Bmr53saa.d.ts} +4 -4
- package/dist/{collection-client-CWAeuzV4.d.cts → collection-client-DxuBDso_.d.cts} +4 -4
- package/dist/{const-D8LixhAJ.d.cts → const-BKUqg96Q.d.ts} +2 -2
- package/dist/{const-JD8obaKg.d.ts → const-Cv1DCsp6.d.cts} +2 -2
- package/dist/{index-Bu0kCc6o.d.cts → index-CGsT4AQ8.d.cts} +24 -7
- package/dist/{index-B0Hjh3Ey.d.ts → index-DFHLLqYu.d.ts} +24 -7
- package/dist/index.cjs +434 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.js +434 -55
- package/dist/index.js.map +1 -1
- package/dist/{payload-types-BOsckg8B.d.cts → payload-types-Bk-duc5k.d.cts} +295 -100
- package/dist/{payload-types-BOsckg8B.d.ts → payload-types-Bk-duc5k.d.ts} +295 -100
- package/dist/query.cjs +14 -1
- package/dist/query.cjs.map +1 -1
- package/dist/query.d.cts +5 -5
- package/dist/query.d.ts +5 -5
- package/dist/query.js +14 -1
- package/dist/query.js.map +1 -1
- package/dist/realtime.d.cts +2 -2
- package/dist/realtime.d.ts +2 -2
- package/dist/server.cjs +98 -13
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +9 -8
- package/dist/server.d.ts +9 -8
- package/dist/server.js +98 -13
- package/dist/server.js.map +1 -1
- package/dist/{types-Bj8j7VHU.d.ts → types-CEK8uTvU.d.ts} +212 -11
- package/dist/{types-B14sEt1z.d.cts → types-CNdpj2fI.d.cts} +2 -2
- package/dist/{types-5ZlcAFuU.d.ts → types-D4mjGFj4.d.ts} +2 -2
- package/dist/{types-CM5qiR2k.d.cts → types-Dhk5NX7Z.d.cts} +212 -11
- package/dist/ui/form.d.cts +1 -1
- package/dist/ui/form.d.ts +1 -1
- package/dist/ui/video.d.cts +1 -1
- package/dist/ui/video.d.ts +1 -1
- package/dist/webhook.cjs +13 -2
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +5 -5
- package/dist/webhook.d.ts +5 -5
- package/dist/webhook.js +13 -2
- package/dist/webhook.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1360,6 +1360,11 @@ var COMMERCE_ENDPOINT_OPERATIONS = {
|
|
|
1360
1360
|
detail: strictCommerceOperation("checkouts", "post", "/detail"),
|
|
1361
1361
|
query: strictCommerceOperation("checkouts", "post", "/query")
|
|
1362
1362
|
},
|
|
1363
|
+
draftOrders: {
|
|
1364
|
+
calculate: operation("draft-orders", "post", "/:id/calculate"),
|
|
1365
|
+
complete: operation("draft-orders", "post", "/:id/complete"),
|
|
1366
|
+
finalize: operation("draft-orders", "post", "/:id/finalize")
|
|
1367
|
+
},
|
|
1363
1368
|
transactions: {
|
|
1364
1369
|
update: operation("transactions", "post", "/update")
|
|
1365
1370
|
},
|
|
@@ -1460,7 +1465,15 @@ var portOneStandardActionCapabilityOperations = /* @__PURE__ */ new Set([
|
|
|
1460
1465
|
var MEMBERSHIP_ENDPOINT_OPERATIONS = {
|
|
1461
1466
|
subscribe: operation(null, "post", "/memberships/subscribe"),
|
|
1462
1467
|
cancel: operation(null, "post", "/memberships/:id/cancel"),
|
|
1463
|
-
me: operation(null, "get", "/memberships/me")
|
|
1468
|
+
me: operation(null, "get", "/memberships/me"),
|
|
1469
|
+
chargeResult: operation(null, "post", "/memberships/:id/charge-result"),
|
|
1470
|
+
refundResult: operation(null, "post", "/memberships/:id/refund-result"),
|
|
1471
|
+
revoke: operation(null, "post", "/memberships/:id/revoke")
|
|
1472
|
+
};
|
|
1473
|
+
var CUSTOMER_GROUP_ENDPOINT_OPERATIONS = {
|
|
1474
|
+
members: operation("customer-groups", "get", "/:id/members"),
|
|
1475
|
+
grant: operation("customer-groups", "post", "/:id/members/grant"),
|
|
1476
|
+
revoke: operation("customer-groups", "post", "/:id/members/revoke")
|
|
1464
1477
|
};
|
|
1465
1478
|
var EMBEDDED_ADMIN_ENDPOINT_OPERATIONS = {
|
|
1466
1479
|
show: operation("tenants", "get", "/embedded-admin-app"),
|
|
@@ -2170,7 +2183,10 @@ var ORDER_PRIMARY_DISPLAY_STATUSES = [
|
|
|
2170
2183
|
];
|
|
2171
2184
|
var CHECKOUT_STATUSES = ["open", "placed", "abandoned"];
|
|
2172
2185
|
var ABANDONED_REASONS = ["expired", "customer", "operator"];
|
|
2173
|
-
|
|
2186
|
+
function managedPaymentProviders(providers) {
|
|
2187
|
+
return providers;
|
|
2188
|
+
}
|
|
2189
|
+
var PAYMENT_PROVIDERS = managedPaymentProviders(["toss", "portone"]);
|
|
2174
2190
|
var PAYMENT_PROVIDER_STATUSES = [
|
|
2175
2191
|
"requires_payment_method",
|
|
2176
2192
|
"requires_action",
|
|
@@ -2218,6 +2234,26 @@ function invalidCanonicalResponse(resource, details) {
|
|
|
2218
2234
|
function fail(resource, path, expected) {
|
|
2219
2235
|
invalidCanonicalResponse(resource, { path, expected });
|
|
2220
2236
|
}
|
|
2237
|
+
function providerMismatch(resource, expected, actual, operation2) {
|
|
2238
|
+
if (operation2 === "cancel") {
|
|
2239
|
+
const method = {
|
|
2240
|
+
toss: "cancel()",
|
|
2241
|
+
portone: "cancelPortOne()"
|
|
2242
|
+
}[actual];
|
|
2243
|
+
throw createApiError(
|
|
2244
|
+
`Canonical ${resource} response provider mismatch: expected ${expected}, received ${actual}.`,
|
|
2245
|
+
502,
|
|
2246
|
+
{ resource, path: "$.provider", expected, actual },
|
|
2247
|
+
`The server applied the cancellation to a ${actual} PaymentSession, but this SDK method expects ${expected}.`,
|
|
2248
|
+
`Re-invoke ${method} with the same idempotencyKey; do not create a new key.`
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
invalidCanonicalResponse(resource, {
|
|
2252
|
+
path: "$.provider",
|
|
2253
|
+
expected,
|
|
2254
|
+
actual
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2221
2257
|
function record(value, resource, path) {
|
|
2222
2258
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2223
2259
|
fail(resource, path, "object");
|
|
@@ -3510,7 +3546,7 @@ function validateNextAction(value, provider, resource, path) {
|
|
|
3510
3546
|
if (action.customerEmail !== void 0)
|
|
3511
3547
|
validateCustomer({ email: action.customerEmail }, resource, path);
|
|
3512
3548
|
}
|
|
3513
|
-
function validatePublicPaymentSession(value, expectedProvider) {
|
|
3549
|
+
function validatePublicPaymentSession(value, expectedProvider, operation2) {
|
|
3514
3550
|
const resource = "PaymentSession";
|
|
3515
3551
|
const session = record(value, resource, "$");
|
|
3516
3552
|
const provider = enumValue(
|
|
@@ -3519,9 +3555,6 @@ function validatePublicPaymentSession(value, expectedProvider) {
|
|
|
3519
3555
|
resource,
|
|
3520
3556
|
"$.provider"
|
|
3521
3557
|
);
|
|
3522
|
-
if (expectedProvider !== void 0 && provider !== expectedProvider) {
|
|
3523
|
-
fail(resource, "$.provider", expectedProvider);
|
|
3524
|
-
}
|
|
3525
3558
|
const status = enumValue(
|
|
3526
3559
|
session.providerStatus,
|
|
3527
3560
|
PAYMENT_PROVIDER_STATUSES,
|
|
@@ -3725,44 +3758,128 @@ function validatePublicPaymentSession(value, expectedProvider) {
|
|
|
3725
3758
|
"$.cancellationReason"
|
|
3726
3759
|
);
|
|
3727
3760
|
}
|
|
3761
|
+
if (expectedProvider !== void 0 && provider !== expectedProvider) {
|
|
3762
|
+
if (operation2 === "cancel" && status !== "canceling" && status !== "canceled") {
|
|
3763
|
+
fail(
|
|
3764
|
+
resource,
|
|
3765
|
+
"$.providerStatus",
|
|
3766
|
+
"canceling or canceled after a successful cancellation"
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3769
|
+
providerMismatch(resource, expectedProvider, provider, operation2);
|
|
3770
|
+
}
|
|
3728
3771
|
return value;
|
|
3729
3772
|
}
|
|
3730
3773
|
|
|
3731
3774
|
// src/core/customer/customer-auth.ts
|
|
3732
3775
|
var DEFAULT_TIMEOUT2 = 15e3;
|
|
3733
|
-
|
|
3776
|
+
var REFRESH_LOCK_TIMEOUT = DEFAULT_TIMEOUT2 * 3;
|
|
3777
|
+
var SESSION_RECORD_VERSION = 1;
|
|
3778
|
+
var STORAGE_UNUSABLE = /* @__PURE__ */ Symbol("storage-unusable");
|
|
3779
|
+
function canonicalApiIdentity(apiUrl) {
|
|
3734
3780
|
try {
|
|
3735
|
-
|
|
3781
|
+
const url = new URL(apiUrl);
|
|
3782
|
+
return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
|
|
3783
|
+
} catch {
|
|
3784
|
+
return apiUrl.trim().toLowerCase().replace(/\/+$/, "");
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
function buildRefreshLockName(apiUrl, publishableKey, storageKey) {
|
|
3788
|
+
return `01software:customer-auth-refresh:${canonicalApiIdentity(apiUrl)}:${publishableKey}:${storageKey}`;
|
|
3789
|
+
}
|
|
3790
|
+
function getRefreshLockRequester() {
|
|
3791
|
+
try {
|
|
3792
|
+
if (typeof navigator === "undefined") return null;
|
|
3793
|
+
const locks = navigator.locks;
|
|
3794
|
+
if (!locks || typeof locks.request !== "function") return null;
|
|
3795
|
+
return (name, options, callback) => locks.request(
|
|
3796
|
+
name,
|
|
3797
|
+
options,
|
|
3798
|
+
() => callback()
|
|
3799
|
+
);
|
|
3800
|
+
} catch {
|
|
3801
|
+
return null;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
function parseSessionRecord(raw) {
|
|
3805
|
+
try {
|
|
3806
|
+
const parsed = JSON.parse(raw);
|
|
3807
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
3808
|
+
const candidate = parsed;
|
|
3809
|
+
if (candidate.v !== SESSION_RECORD_VERSION) return null;
|
|
3810
|
+
if (typeof candidate.rev !== "number" || !Number.isFinite(candidate.rev)) {
|
|
3811
|
+
return null;
|
|
3812
|
+
}
|
|
3813
|
+
const token = candidate.token;
|
|
3814
|
+
const refreshToken = candidate.refreshToken;
|
|
3815
|
+
if (token !== null && typeof token !== "string") return null;
|
|
3816
|
+
if (refreshToken !== null && typeof refreshToken !== "string") return null;
|
|
3817
|
+
return {
|
|
3818
|
+
v: SESSION_RECORD_VERSION,
|
|
3819
|
+
rev: candidate.rev,
|
|
3820
|
+
token,
|
|
3821
|
+
refreshToken
|
|
3822
|
+
};
|
|
3736
3823
|
} catch {
|
|
3737
3824
|
return null;
|
|
3738
3825
|
}
|
|
3739
3826
|
}
|
|
3827
|
+
function readSessionRecord(recordKey, mirrorKey, mirrorRefreshKey) {
|
|
3828
|
+
let raw;
|
|
3829
|
+
try {
|
|
3830
|
+
raw = localStorage.getItem(recordKey);
|
|
3831
|
+
} catch {
|
|
3832
|
+
return STORAGE_UNUSABLE;
|
|
3833
|
+
}
|
|
3834
|
+
if (raw !== null) {
|
|
3835
|
+
const parsed = parseSessionRecord(raw);
|
|
3836
|
+
return parsed ? { record: parsed, stored: true } : STORAGE_UNUSABLE;
|
|
3837
|
+
}
|
|
3838
|
+
try {
|
|
3839
|
+
return {
|
|
3840
|
+
record: {
|
|
3841
|
+
v: SESSION_RECORD_VERSION,
|
|
3842
|
+
rev: 0,
|
|
3843
|
+
token: localStorage.getItem(mirrorKey),
|
|
3844
|
+
refreshToken: localStorage.getItem(mirrorRefreshKey)
|
|
3845
|
+
},
|
|
3846
|
+
stored: false
|
|
3847
|
+
};
|
|
3848
|
+
} catch {
|
|
3849
|
+
return STORAGE_UNUSABLE;
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3740
3852
|
var CustomerAuth = class {
|
|
3741
3853
|
constructor(publishableKey, options, apiUrl) {
|
|
3742
3854
|
this.refreshPromise = null;
|
|
3855
|
+
/** Canonical record key; null unless persistence is active in a browser. */
|
|
3856
|
+
this.persistRecordKey = null;
|
|
3857
|
+
/** Advisory single-value access mirror, plus the retired refresh mirror. */
|
|
3858
|
+
this.persistMirrorKey = null;
|
|
3859
|
+
this.persistMirrorRefreshKey = null;
|
|
3860
|
+
/** Cross-context refresh lock name; null when there is no shared storage. */
|
|
3861
|
+
this.refreshLockName = null;
|
|
3862
|
+
/** Revision of the canonical record this instance last observed. */
|
|
3863
|
+
this.sessionRev = 0;
|
|
3743
3864
|
this.publishableKey = publishableKey;
|
|
3744
3865
|
this.baseUrl = resolveApiUrl(apiUrl);
|
|
3745
3866
|
const persist = options?.persist ?? true;
|
|
3746
3867
|
if (persist) {
|
|
3747
3868
|
const key = typeof persist === "string" ? persist : "customer-token";
|
|
3748
|
-
const refreshKey = `${key}:refresh`;
|
|
3749
3869
|
const isBrowser = typeof window !== "undefined";
|
|
3750
|
-
this.token =
|
|
3751
|
-
this.refreshTokenValue =
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
} catch {
|
|
3764
|
-
}
|
|
3765
|
-
} : void 0;
|
|
3870
|
+
this.token = null;
|
|
3871
|
+
this.refreshTokenValue = null;
|
|
3872
|
+
if (isBrowser) {
|
|
3873
|
+
this.persistRecordKey = `${key}:session`;
|
|
3874
|
+
this.persistMirrorKey = key;
|
|
3875
|
+
this.persistMirrorRefreshKey = `${key}:refresh`;
|
|
3876
|
+
this.refreshLockName = buildRefreshLockName(
|
|
3877
|
+
this.baseUrl,
|
|
3878
|
+
publishableKey,
|
|
3879
|
+
key
|
|
3880
|
+
);
|
|
3881
|
+
this.syncFromPersistedStorage();
|
|
3882
|
+
}
|
|
3766
3883
|
} else {
|
|
3767
3884
|
this.token = options?.token ?? null;
|
|
3768
3885
|
this.refreshTokenValue = options?.refreshToken ?? null;
|
|
@@ -3790,66 +3907,299 @@ var CustomerAuth = class {
|
|
|
3790
3907
|
body: JSON.stringify(data)
|
|
3791
3908
|
}
|
|
3792
3909
|
);
|
|
3793
|
-
this.
|
|
3794
|
-
|
|
3910
|
+
if (result.refreshToken) this.setSession(result.token, result.refreshToken);
|
|
3911
|
+
else this.setToken(result.token);
|
|
3795
3912
|
return result;
|
|
3796
3913
|
}
|
|
3797
3914
|
/**
|
|
3798
3915
|
* Refresh the current token. Uses a refresh token when available, otherwise
|
|
3799
3916
|
* falls back to the legacy bearer-token compatibility path.
|
|
3917
|
+
*
|
|
3918
|
+
* Refresh tokens are single-use, so a persisted session shared by several
|
|
3919
|
+
* tabs or SDK instances must rotate at most once: concurrent rotations of the
|
|
3920
|
+
* same token are replay from the server's point of view and revoke the whole
|
|
3921
|
+
* family. Persisted browser sessions therefore serialize rotation across
|
|
3922
|
+
* contexts and re-read the canonical persisted credentials inside the lock.
|
|
3800
3923
|
*/
|
|
3801
3924
|
async refreshToken() {
|
|
3925
|
+
this.syncFromPersistedStorage();
|
|
3802
3926
|
if (!this.refreshTokenValue && !this.token) {
|
|
3803
3927
|
throw new ApiError("Not authenticated", 401);
|
|
3804
3928
|
}
|
|
3805
3929
|
if (this.refreshPromise) return this.refreshPromise;
|
|
3806
|
-
|
|
3930
|
+
const intended = this.currentFence();
|
|
3931
|
+
this.refreshPromise = this.rotateAcrossContexts(intended);
|
|
3807
3932
|
try {
|
|
3808
3933
|
return await this.refreshPromise;
|
|
3809
3934
|
} finally {
|
|
3810
3935
|
this.refreshPromise = null;
|
|
3811
3936
|
}
|
|
3812
3937
|
}
|
|
3813
|
-
|
|
3814
|
-
|
|
3938
|
+
/**
|
|
3939
|
+
* Re-read the canonical persisted session. In persisted mode the shared
|
|
3940
|
+
* record — not this instance's memory — is the source of truth, so another
|
|
3941
|
+
* context's rotation, login, or logout is observed before the next credential
|
|
3942
|
+
* use. Change callbacks are deliberately not invoked: this is a read.
|
|
3943
|
+
*/
|
|
3944
|
+
syncFromPersistedStorage() {
|
|
3945
|
+
const canonical = this.readCanonicalSession();
|
|
3946
|
+
if (canonical === STORAGE_UNUSABLE) {
|
|
3947
|
+
this.disablePersistence();
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
if (!canonical) return;
|
|
3951
|
+
this.sessionRev = canonical.record.rev;
|
|
3952
|
+
this.token = canonical.record.token;
|
|
3953
|
+
this.refreshTokenValue = canonical.record.refreshToken;
|
|
3954
|
+
}
|
|
3955
|
+
/** `null` when this instance does not persist. */
|
|
3956
|
+
readCanonicalSession() {
|
|
3957
|
+
if (!this.persistRecordKey || !this.persistMirrorKey || !this.persistMirrorRefreshKey) {
|
|
3958
|
+
return null;
|
|
3959
|
+
}
|
|
3960
|
+
return readSessionRecord(
|
|
3961
|
+
this.persistRecordKey,
|
|
3962
|
+
this.persistMirrorKey,
|
|
3963
|
+
this.persistMirrorRefreshKey
|
|
3964
|
+
);
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Publish a change to canonical storage.
|
|
3968
|
+
*
|
|
3969
|
+
* The canonical pair is re-read immediately before the single record write so
|
|
3970
|
+
* that a field this call does not change is taken from that read rather than
|
|
3971
|
+
* from this instance's possibly stale copy. That merge is BEST-EFFORT: it
|
|
3972
|
+
* removes the stale counterpart an instance would otherwise carry, but it is
|
|
3973
|
+
* not a compare-and-swap. Web Storage offers none, and the synchronous public
|
|
3974
|
+
* mutators cannot await the refresh lease, so a commit that lands between
|
|
3975
|
+
* this read and the store operation is lost by ordering — including the
|
|
3976
|
+
* counterpart just read, which is then republished stale. The window is one
|
|
3977
|
+
* store operation wide and the server remains authoritative; see
|
|
3978
|
+
* `CUST-AUTH-033` for the stated limit.
|
|
3979
|
+
*
|
|
3980
|
+
* Returns `false` only when `requireLiveSession` refused a write because the
|
|
3981
|
+
* canonical session had already been cleared.
|
|
3982
|
+
*/
|
|
3983
|
+
commitPersistedSession(change, options) {
|
|
3984
|
+
const recordKey = this.persistRecordKey;
|
|
3985
|
+
const mirrorKey = this.persistMirrorKey;
|
|
3986
|
+
const mirrorRefreshKey = this.persistMirrorRefreshKey;
|
|
3987
|
+
const canonical = this.readCanonicalSession();
|
|
3988
|
+
if (canonical === null || !recordKey || !mirrorKey || !mirrorRefreshKey) {
|
|
3989
|
+
return true;
|
|
3990
|
+
}
|
|
3991
|
+
if (canonical === STORAGE_UNUSABLE) {
|
|
3992
|
+
this.disablePersistence();
|
|
3993
|
+
return true;
|
|
3994
|
+
}
|
|
3995
|
+
const current = canonical.record;
|
|
3996
|
+
if (options?.requireLiveSession && !current.token && !current.refreshToken) {
|
|
3997
|
+
return false;
|
|
3998
|
+
}
|
|
3999
|
+
const next = {
|
|
4000
|
+
v: SESSION_RECORD_VERSION,
|
|
4001
|
+
rev: current.rev + 1,
|
|
4002
|
+
token: "token" in change ? change.token ?? null : current.token,
|
|
4003
|
+
refreshToken: "refreshToken" in change ? change.refreshToken ?? null : current.refreshToken
|
|
4004
|
+
};
|
|
4005
|
+
if (
|
|
4006
|
+
// Only a state that is genuinely persisted can already say what this
|
|
4007
|
+
// write would say. An absent record reads as an empty pair, and treating
|
|
4008
|
+
// that as a no-op would drop the very tombstone that stops a later
|
|
4009
|
+
// migration from resurrecting the session out of the earlier layout.
|
|
4010
|
+
canonical.stored && next.token === current.token && next.refreshToken === current.refreshToken
|
|
4011
|
+
) {
|
|
4012
|
+
this.sessionRev = current.rev;
|
|
4013
|
+
this.token = current.token;
|
|
4014
|
+
this.refreshTokenValue = current.refreshToken;
|
|
4015
|
+
return true;
|
|
4016
|
+
}
|
|
4017
|
+
try {
|
|
4018
|
+
localStorage.setItem(recordKey, JSON.stringify(next));
|
|
4019
|
+
} catch {
|
|
4020
|
+
this.disablePersistence();
|
|
4021
|
+
return true;
|
|
4022
|
+
}
|
|
4023
|
+
this.sessionRev = next.rev;
|
|
4024
|
+
this.token = next.token;
|
|
4025
|
+
this.refreshTokenValue = next.refreshToken;
|
|
4026
|
+
try {
|
|
4027
|
+
localStorage.removeItem(mirrorRefreshKey);
|
|
4028
|
+
} catch {
|
|
4029
|
+
return true;
|
|
4030
|
+
}
|
|
4031
|
+
try {
|
|
4032
|
+
if (next.token) localStorage.setItem(mirrorKey, next.token);
|
|
4033
|
+
else localStorage.removeItem(mirrorKey);
|
|
4034
|
+
} catch {
|
|
4035
|
+
}
|
|
4036
|
+
return true;
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* Storage rejected a write or a read, so it is not a canonical store for this
|
|
4040
|
+
* instance. Fall back to the memory-only session and take no lease: there is
|
|
4041
|
+
* no shared state left to coordinate. Nothing is purged — the record commit
|
|
4042
|
+
* is atomic, so no inconsistent state was published, and another context's
|
|
4043
|
+
* valid session must not be destroyed by this instance's local failure.
|
|
4044
|
+
*/
|
|
4045
|
+
disablePersistence() {
|
|
4046
|
+
this.persistRecordKey = null;
|
|
4047
|
+
this.persistMirrorKey = null;
|
|
4048
|
+
this.persistMirrorRefreshKey = null;
|
|
4049
|
+
this.refreshLockName = null;
|
|
4050
|
+
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Compare the canonical record with the fence this call captured before it
|
|
4053
|
+
* left. Every persisted decision — rotate, converge, or fail closed — goes
|
|
4054
|
+
* through this one comparison. The revision catches a logout, a fresh login,
|
|
4055
|
+
* or a manual setter even when token values coincide; the value comparison
|
|
4056
|
+
* additionally catches a mirror-only writer that cannot bump a revision.
|
|
4057
|
+
*/
|
|
4058
|
+
classifyCanonicalState(intended) {
|
|
4059
|
+
if (!this.persistRecordKey) return "current";
|
|
4060
|
+
this.syncFromPersistedStorage();
|
|
4061
|
+
if (!this.persistRecordKey) return "current";
|
|
4062
|
+
if (!this.token && !this.refreshTokenValue) return "cleared";
|
|
4063
|
+
if (intended.refreshToken !== null) {
|
|
4064
|
+
return this.refreshTokenValue === intended.refreshToken ? "current" : "superseded";
|
|
4065
|
+
}
|
|
4066
|
+
return this.token === intended.token && !this.refreshTokenValue ? "current" : "superseded";
|
|
4067
|
+
}
|
|
4068
|
+
/** The canonical persisted pair, as a refresh result. */
|
|
4069
|
+
canonicalPair(token) {
|
|
4070
|
+
return {
|
|
4071
|
+
token,
|
|
4072
|
+
...this.refreshTokenValue ? { refreshToken: this.refreshTokenValue } : {}
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
/** Current credentials plus the revision they were observed at. */
|
|
4076
|
+
currentFence() {
|
|
4077
|
+
return { token: this.token, refreshToken: this.refreshTokenValue };
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* Clear an access credential a 401 just proved unusable — but only while it
|
|
4081
|
+
* is still the canonical one. A rotation committed by another context while
|
|
4082
|
+
* the request was in flight is newer than this 401, so it must survive; the
|
|
4083
|
+
* instance adopts it instead. The refresh entry is never touched here: a
|
|
4084
|
+
* rejected access token says nothing about the refresh family.
|
|
4085
|
+
*/
|
|
4086
|
+
invalidateAccessToken(spentToken) {
|
|
4087
|
+
this.syncFromPersistedStorage();
|
|
4088
|
+
if (this.token !== spentToken) return;
|
|
4089
|
+
this.setToken(null);
|
|
4090
|
+
}
|
|
4091
|
+
/** Commit both credentials in one canonical write. */
|
|
4092
|
+
setSession(token, refreshToken, options) {
|
|
4093
|
+
this.token = token;
|
|
4094
|
+
this.refreshTokenValue = refreshToken;
|
|
4095
|
+
const committed = this.commitPersistedSession(
|
|
4096
|
+
{ token, refreshToken },
|
|
4097
|
+
options
|
|
4098
|
+
);
|
|
4099
|
+
if (!committed) return false;
|
|
4100
|
+
this.onTokenChange?.(this.token);
|
|
4101
|
+
this.onRefreshTokenChange?.(this.refreshTokenValue);
|
|
4102
|
+
return true;
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Serialize rotation against every other context sharing the same storage
|
|
4106
|
+
* key and API/tenant identity. Coordination is best-effort: when it is
|
|
4107
|
+
* unavailable or times out the rotation still happens, so server-side reuse
|
|
4108
|
+
* detection is never bypassed by a client that failed to coordinate.
|
|
4109
|
+
*/
|
|
4110
|
+
async rotateAcrossContexts(intended) {
|
|
4111
|
+
const lockName = this.refreshLockName;
|
|
4112
|
+
const requestLock = lockName ? getRefreshLockRequester() : null;
|
|
4113
|
+
if (!lockName || !requestLock) {
|
|
4114
|
+
return this.rotateWithCanonicalState(intended);
|
|
4115
|
+
}
|
|
4116
|
+
const controller = new AbortController();
|
|
4117
|
+
const timer = setTimeout(() => controller.abort(), REFRESH_LOCK_TIMEOUT);
|
|
4118
|
+
let entered = false;
|
|
4119
|
+
try {
|
|
4120
|
+
return await requestLock(
|
|
4121
|
+
lockName,
|
|
4122
|
+
{ signal: controller.signal },
|
|
4123
|
+
async () => {
|
|
4124
|
+
entered = true;
|
|
4125
|
+
return this.rotateWithCanonicalState(intended);
|
|
4126
|
+
}
|
|
4127
|
+
);
|
|
4128
|
+
} catch (error) {
|
|
4129
|
+
if (entered) throw error;
|
|
4130
|
+
return this.rotateWithCanonicalState(intended);
|
|
4131
|
+
} finally {
|
|
4132
|
+
clearTimeout(timer);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
/**
|
|
4136
|
+
* Decide, against freshly re-read canonical state, whether this context still
|
|
4137
|
+
* owns the rotation.
|
|
4138
|
+
*/
|
|
4139
|
+
async rotateWithCanonicalState(intended) {
|
|
4140
|
+
let fence = intended;
|
|
4141
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
4142
|
+
const state = this.classifyCanonicalState(fence);
|
|
4143
|
+
if (state === "cleared") throw new ApiError("Not authenticated", 401);
|
|
4144
|
+
if (state === "current") return this._doRefreshToken(fence);
|
|
4145
|
+
if (this.token) return this.canonicalPair(this.token);
|
|
4146
|
+
fence = this.currentFence();
|
|
4147
|
+
}
|
|
4148
|
+
throw new ApiError("Not authenticated", 401);
|
|
4149
|
+
}
|
|
4150
|
+
async _doRefreshToken(intended) {
|
|
4151
|
+
const init = intended.refreshToken ? {
|
|
3815
4152
|
method: "POST",
|
|
3816
|
-
body: JSON.stringify({ refreshToken:
|
|
4153
|
+
body: JSON.stringify({ refreshToken: intended.refreshToken })
|
|
3817
4154
|
} : {
|
|
3818
4155
|
method: "POST",
|
|
3819
|
-
headers: { Authorization: `Bearer ${
|
|
4156
|
+
headers: { Authorization: `Bearer ${intended.token}` }
|
|
3820
4157
|
};
|
|
3821
4158
|
const result = await this.requestJson(
|
|
3822
4159
|
"/api/customers/refresh",
|
|
3823
4160
|
init
|
|
3824
4161
|
);
|
|
3825
|
-
this.
|
|
3826
|
-
if (
|
|
3827
|
-
|
|
4162
|
+
const state = this.classifyCanonicalState(intended);
|
|
4163
|
+
if (state === "cleared") throw new ApiError("Not authenticated", 401);
|
|
4164
|
+
if (state === "superseded") {
|
|
4165
|
+
if (this.token) return this.canonicalPair(this.token);
|
|
4166
|
+
throw new ApiError("Not authenticated", 401);
|
|
4167
|
+
}
|
|
4168
|
+
const replacedAccessToken = this.token !== null && this.token !== intended.token ? this.token : null;
|
|
4169
|
+
const nextToken = replacedAccessToken ?? result.token;
|
|
4170
|
+
const nextRefresh = result.refreshToken ?? null;
|
|
4171
|
+
if (!this.setSession(nextToken, nextRefresh, { requireLiveSession: true })) {
|
|
4172
|
+
throw new ApiError("Not authenticated", 401);
|
|
4173
|
+
}
|
|
4174
|
+
return {
|
|
4175
|
+
token: nextToken,
|
|
4176
|
+
...nextRefresh ? { refreshToken: nextRefresh } : {}
|
|
4177
|
+
};
|
|
3828
4178
|
}
|
|
3829
4179
|
/**
|
|
3830
4180
|
* Clear the stored token
|
|
3831
4181
|
*/
|
|
3832
4182
|
logout() {
|
|
3833
|
-
this.
|
|
3834
|
-
this.setRefreshToken(null);
|
|
4183
|
+
this.setSession(null, null);
|
|
3835
4184
|
}
|
|
3836
4185
|
/**
|
|
3837
4186
|
* Get the current authenticated customer's profile
|
|
3838
4187
|
*/
|
|
3839
4188
|
async me() {
|
|
3840
|
-
|
|
4189
|
+
const token = this.getToken();
|
|
4190
|
+
if (!token) return null;
|
|
3841
4191
|
try {
|
|
3842
4192
|
const data = await this.requestJson(
|
|
3843
4193
|
"/api/customers/me",
|
|
3844
4194
|
{
|
|
3845
4195
|
method: "GET",
|
|
3846
|
-
headers: { Authorization: `Bearer ${
|
|
4196
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3847
4197
|
}
|
|
3848
4198
|
);
|
|
3849
4199
|
return data.customer ?? null;
|
|
3850
4200
|
} catch (error) {
|
|
3851
4201
|
if (error instanceof SDKError && error.status === 401) {
|
|
3852
|
-
this.
|
|
4202
|
+
this.invalidateAccessToken(token);
|
|
3853
4203
|
return null;
|
|
3854
4204
|
}
|
|
3855
4205
|
throw error;
|
|
@@ -3877,12 +4227,13 @@ var CustomerAuth = class {
|
|
|
3877
4227
|
* Update the authenticated customer's profile (name, phone, marketingConsent)
|
|
3878
4228
|
*/
|
|
3879
4229
|
async updateProfile(data) {
|
|
3880
|
-
|
|
4230
|
+
const token = this.getToken();
|
|
4231
|
+
if (!token) throw new ApiError("Not authenticated", 401);
|
|
3881
4232
|
const result = await this.requestJson(
|
|
3882
4233
|
"/api/customers/me",
|
|
3883
4234
|
{
|
|
3884
4235
|
method: "PATCH",
|
|
3885
|
-
headers: { Authorization: `Bearer ${
|
|
4236
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
3886
4237
|
body: JSON.stringify(data)
|
|
3887
4238
|
}
|
|
3888
4239
|
);
|
|
@@ -3892,10 +4243,11 @@ var CustomerAuth = class {
|
|
|
3892
4243
|
* Change the password of the currently authenticated customer
|
|
3893
4244
|
*/
|
|
3894
4245
|
async changePassword(currentPassword, newPassword) {
|
|
3895
|
-
|
|
4246
|
+
const token = this.getToken();
|
|
4247
|
+
if (!token) throw new ApiError("Not authenticated", 401);
|
|
3896
4248
|
await this.requestJson("/api/customers/change-password", {
|
|
3897
4249
|
method: "POST",
|
|
3898
|
-
headers: { Authorization: `Bearer ${
|
|
4250
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
3899
4251
|
body: JSON.stringify({ currentPassword, newPassword })
|
|
3900
4252
|
});
|
|
3901
4253
|
}
|
|
@@ -3903,7 +4255,8 @@ var CustomerAuth = class {
|
|
|
3903
4255
|
* Get the authenticated customer's orders with pagination and optional status filter
|
|
3904
4256
|
*/
|
|
3905
4257
|
async getMyOrders(options) {
|
|
3906
|
-
|
|
4258
|
+
const token = this.getToken();
|
|
4259
|
+
if (!token) throw new ApiError("Not authenticated", 401);
|
|
3907
4260
|
const params = new URLSearchParams();
|
|
3908
4261
|
if (options?.page) params.set("page", String(options.page));
|
|
3909
4262
|
if (options?.limit) params.set("limit", String(options.limit));
|
|
@@ -3925,21 +4278,27 @@ var CustomerAuth = class {
|
|
|
3925
4278
|
`/api/customers/me/orders${qs ? `?${qs}` : ""}`,
|
|
3926
4279
|
{
|
|
3927
4280
|
method: "GET",
|
|
3928
|
-
headers: { Authorization: `Bearer ${
|
|
4281
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3929
4282
|
}
|
|
3930
4283
|
);
|
|
3931
4284
|
return validatePublicOrderList(result);
|
|
3932
4285
|
}
|
|
3933
4286
|
/**
|
|
3934
|
-
* Get the current token (or null if not authenticated)
|
|
4287
|
+
* Get the current token (or null if not authenticated).
|
|
4288
|
+
*
|
|
4289
|
+
* In persisted mode this re-reads shared storage first, so a rotation or
|
|
4290
|
+
* logout performed by another tab is observed before the token is used.
|
|
3935
4291
|
*/
|
|
3936
4292
|
getToken() {
|
|
4293
|
+
this.syncFromPersistedStorage();
|
|
3937
4294
|
return this.token;
|
|
3938
4295
|
}
|
|
3939
4296
|
/**
|
|
3940
|
-
* Get the current refresh token (or null if unavailable)
|
|
4297
|
+
* Get the current refresh token (or null if unavailable). Persisted mode
|
|
4298
|
+
* re-reads shared storage first, like {@link CustomerAuth.getToken}.
|
|
3941
4299
|
*/
|
|
3942
4300
|
getRefreshToken() {
|
|
4301
|
+
this.syncFromPersistedStorage();
|
|
3943
4302
|
return this.refreshTokenValue;
|
|
3944
4303
|
}
|
|
3945
4304
|
/**
|
|
@@ -3947,6 +4306,7 @@ var CustomerAuth = class {
|
|
|
3947
4306
|
*/
|
|
3948
4307
|
setToken(token) {
|
|
3949
4308
|
this.token = token;
|
|
4309
|
+
this.commitPersistedSession({ token });
|
|
3950
4310
|
this.onTokenChange?.(token);
|
|
3951
4311
|
}
|
|
3952
4312
|
/**
|
|
@@ -3954,13 +4314,14 @@ var CustomerAuth = class {
|
|
|
3954
4314
|
*/
|
|
3955
4315
|
setRefreshToken(refreshToken) {
|
|
3956
4316
|
this.refreshTokenValue = refreshToken;
|
|
4317
|
+
this.commitPersistedSession({ refreshToken });
|
|
3957
4318
|
this.onRefreshTokenChange?.(refreshToken);
|
|
3958
4319
|
}
|
|
3959
4320
|
/**
|
|
3960
4321
|
* Check if the customer is currently authenticated
|
|
3961
4322
|
*/
|
|
3962
4323
|
isAuthenticated() {
|
|
3963
|
-
return this.
|
|
4324
|
+
return this.getToken() !== null;
|
|
3964
4325
|
}
|
|
3965
4326
|
/**
|
|
3966
4327
|
* Internal: make a request with timeout and error handling.
|
|
@@ -4310,8 +4671,12 @@ var PaymentSessionApi = class extends CustomerScopedApi {
|
|
|
4310
4671
|
...Object.keys(headers).length > 0 && { headers }
|
|
4311
4672
|
});
|
|
4312
4673
|
}
|
|
4313
|
-
async paymentSession(result, expectedProvider) {
|
|
4314
|
-
return validatePublicPaymentSession(
|
|
4674
|
+
async paymentSession(result, expectedProvider, operation2) {
|
|
4675
|
+
return validatePublicPaymentSession(
|
|
4676
|
+
await result,
|
|
4677
|
+
expectedProvider,
|
|
4678
|
+
operation2
|
|
4679
|
+
);
|
|
4315
4680
|
}
|
|
4316
4681
|
create(params) {
|
|
4317
4682
|
const { idempotencyKey, ...body } = params;
|
|
@@ -4358,7 +4723,8 @@ var PaymentSessionApi = class extends CustomerScopedApi {
|
|
|
4358
4723
|
const endpoint = this.serverTransport ? COMMERCE_ENDPOINTS.paymentSessions.server.cancel : COMMERCE_ENDPOINTS.paymentSessions.cancel;
|
|
4359
4724
|
return this.paymentSession(
|
|
4360
4725
|
this.post(endpoint, body, idempotencyKey),
|
|
4361
|
-
"toss"
|
|
4726
|
+
"toss",
|
|
4727
|
+
"cancel"
|
|
4362
4728
|
);
|
|
4363
4729
|
}
|
|
4364
4730
|
cancelPortOne(params) {
|
|
@@ -4367,7 +4733,8 @@ var PaymentSessionApi = class extends CustomerScopedApi {
|
|
|
4367
4733
|
const endpoint = this.serverTransport ? COMMERCE_ENDPOINTS.paymentSessions.server.cancel : COMMERCE_ENDPOINTS.paymentSessions.cancel;
|
|
4368
4734
|
return this.paymentSession(
|
|
4369
4735
|
this.post(endpoint, body, idempotencyKey),
|
|
4370
|
-
"portone"
|
|
4736
|
+
"portone",
|
|
4737
|
+
"cancel"
|
|
4371
4738
|
);
|
|
4372
4739
|
}
|
|
4373
4740
|
confirm(params) {
|
|
@@ -6808,7 +7175,11 @@ var COMMERCE_NOTIFICATION_EVENTS = [
|
|
|
6808
7175
|
"returnCanceled",
|
|
6809
7176
|
"returnCompleted",
|
|
6810
7177
|
"paymentReconciliationRequired",
|
|
6811
|
-
"paymentRefundRequiresAttention"
|
|
7178
|
+
"paymentRefundRequiresAttention",
|
|
7179
|
+
"membershipActivated",
|
|
7180
|
+
"membershipPastDue",
|
|
7181
|
+
"membershipExpired",
|
|
7182
|
+
"membershipCancelled"
|
|
6812
7183
|
];
|
|
6813
7184
|
function isStringOrNumber(value) {
|
|
6814
7185
|
return typeof value === "string" || typeof value === "number";
|
|
@@ -6838,7 +7209,7 @@ function isCommerceNotificationEventName(value) {
|
|
|
6838
7209
|
return typeof value === "string" && COMMERCE_NOTIFICATION_EVENTS.includes(value);
|
|
6839
7210
|
}
|
|
6840
7211
|
function isCommerceNotificationSourceCollection(value) {
|
|
6841
|
-
return value === "orders" || value === "fulfillments" || value === "returns" || value === "payment-sessions" || value === "payment-refund-attempts";
|
|
7212
|
+
return value === "orders" || value === "fulfillments" || value === "returns" || value === "payment-sessions" || value === "payment-refund-attempts" || value === "customer-memberships";
|
|
6842
7213
|
}
|
|
6843
7214
|
function isCommerceNotificationData(value) {
|
|
6844
7215
|
if (!isRecord(value)) return false;
|
|
@@ -6858,6 +7229,10 @@ function isPaymentIncidentNotificationData(value, event) {
|
|
|
6858
7229
|
const expectedReason = reconciliation ? "orderless_capture" : "local_commit_ambiguous";
|
|
6859
7230
|
return value.source?.collection === expectedCollection && typeof value.source.id === "string" && value.source.id.length > 0 && typeof value.paymentSessionId === "string" && value.paymentSessionId.length > 0 && isRecord(value.providerObject) && typeof value.providerObject.provider === "string" && value.providerObject.provider.length > 0 && (value.providerObject.kind === "payment" || value.providerObject.kind === "refund") && typeof value.providerObject.id === "string" && value.providerObject.id.length > 0 && isRecord(value.money) && Number.isSafeInteger(value.money.amountMinor) && Number(value.money.amountMinor) > 0 && typeof value.money.currency === "string" && value.money.currency.length > 0 && value.status === expectedStatus && value.reason === expectedReason && typeof value.observedAt === "string" && value.observedAt.length > 0 && !Number.isNaN(Date.parse(value.observedAt)) && (value.checkoutId === void 0 || typeof value.checkoutId === "string" && value.checkoutId.length > 0) && (value.orderId === void 0 || typeof value.orderId === "string" && value.orderId.length > 0) && (reconciliation ? value.source.id === value.paymentSessionId && value.providerObject.kind === "payment" && value.refundAttemptId === void 0 : typeof value.refundAttemptId === "string" && value.refundAttemptId.length > 0 && value.source.id === value.refundAttemptId);
|
|
6860
7231
|
}
|
|
7232
|
+
function isMembershipNotificationData(value, event) {
|
|
7233
|
+
const expectedStatus = event === "membershipActivated" ? "active" : event === "membershipPastDue" ? "past_due" : event === "membershipExpired" ? "expired" : "cancelled";
|
|
7234
|
+
return typeof value.membershipId === "string" && value.membershipId.length > 0 && typeof value.planId === "string" && value.planId.length > 0 && typeof value.customerId === "string" && value.customerId.length > 0 && typeof value.fromStatus === "string" && value.fromStatus.length > 0 && value.fromStatus !== value.toStatus && value.toStatus === expectedStatus && typeof value.occurredAt === "string" && !Number.isNaN(Date.parse(value.occurredAt));
|
|
7235
|
+
}
|
|
6861
7236
|
function isWebhookCommerceNotificationChange(value) {
|
|
6862
7237
|
if (!isRecord(value)) return false;
|
|
6863
7238
|
return value.type === "notification" && value.source === "commerce-notifications" && isCommerceNotificationEventName(value.event) && isCommerceNotificationSourceCollection(value.sourceCollection) && isStringOrNumber(value.sourceId);
|
|
@@ -6906,6 +7281,9 @@ function isCommerceNotificationWebhookEvent(event) {
|
|
|
6906
7281
|
const notificationSourceId = notification.event === "paymentReconciliationRequired" ? notification.paymentSessionId : notification.refundAttemptId;
|
|
6907
7282
|
return sourceCollection === expectedCollection && typeof notification.paymentSessionId === "string" && notification.paymentSessionId === incidentData.paymentSessionId && typeof notificationSourceId === "string" && notificationSourceId === incidentData.source.id && matchesOptionalId(changeSourceId, incidentData.source.id);
|
|
6908
7283
|
}
|
|
7284
|
+
if (notification.event === "membershipActivated" || notification.event === "membershipPastDue" || notification.event === "membershipExpired" || notification.event === "membershipCancelled") {
|
|
7285
|
+
return sourceCollection === "customer-memberships" && isMembershipNotificationData(data, notification.event) && typeof notification.membershipId === "string" && notification.membershipId === data.membershipId && matchesOptionalId(changeSourceId, data.membershipId);
|
|
7286
|
+
}
|
|
6909
7287
|
if (notification.event === "orderPaid" || notification.event === "orderCanceled" || notification.event === "orderDelivered") {
|
|
6910
7288
|
return sourceCollection === "orders" && typeof notification.orderId === "string" && notification.orderId.length > 0 && matchesOptionalId(data.orderId, sourceId) && matchesOptionalId(notification.orderId, sourceId) && matchesOptionalId(data.orderId, notification.orderId) && matchesOptionalId(changeSourceId, data.orderId ?? notification.orderId);
|
|
6911
7289
|
}
|
|
@@ -7023,6 +7401,7 @@ var INTERNAL_COLLECTIONS = [
|
|
|
7023
7401
|
"fulfillment-events",
|
|
7024
7402
|
"tenant-commerce-config",
|
|
7025
7403
|
"customer-oauth-clients",
|
|
7404
|
+
"customer-group-grants",
|
|
7026
7405
|
"api-keys",
|
|
7027
7406
|
"personal-access-tokens",
|
|
7028
7407
|
"tenant-entitlements",
|