@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.cjs
CHANGED
|
@@ -1504,6 +1504,11 @@ var COMMERCE_ENDPOINT_OPERATIONS = {
|
|
|
1504
1504
|
detail: strictCommerceOperation("checkouts", "post", "/detail"),
|
|
1505
1505
|
query: strictCommerceOperation("checkouts", "post", "/query")
|
|
1506
1506
|
},
|
|
1507
|
+
draftOrders: {
|
|
1508
|
+
calculate: operation("draft-orders", "post", "/:id/calculate"),
|
|
1509
|
+
complete: operation("draft-orders", "post", "/:id/complete"),
|
|
1510
|
+
finalize: operation("draft-orders", "post", "/:id/finalize")
|
|
1511
|
+
},
|
|
1507
1512
|
transactions: {
|
|
1508
1513
|
update: operation("transactions", "post", "/update")
|
|
1509
1514
|
},
|
|
@@ -1604,7 +1609,15 @@ var portOneStandardActionCapabilityOperations = /* @__PURE__ */ new Set([
|
|
|
1604
1609
|
var MEMBERSHIP_ENDPOINT_OPERATIONS = {
|
|
1605
1610
|
subscribe: operation(null, "post", "/memberships/subscribe"),
|
|
1606
1611
|
cancel: operation(null, "post", "/memberships/:id/cancel"),
|
|
1607
|
-
me: operation(null, "get", "/memberships/me")
|
|
1612
|
+
me: operation(null, "get", "/memberships/me"),
|
|
1613
|
+
chargeResult: operation(null, "post", "/memberships/:id/charge-result"),
|
|
1614
|
+
refundResult: operation(null, "post", "/memberships/:id/refund-result"),
|
|
1615
|
+
revoke: operation(null, "post", "/memberships/:id/revoke")
|
|
1616
|
+
};
|
|
1617
|
+
var CUSTOMER_GROUP_ENDPOINT_OPERATIONS = {
|
|
1618
|
+
members: operation("customer-groups", "get", "/:id/members"),
|
|
1619
|
+
grant: operation("customer-groups", "post", "/:id/members/grant"),
|
|
1620
|
+
revoke: operation("customer-groups", "post", "/:id/members/revoke")
|
|
1608
1621
|
};
|
|
1609
1622
|
var EMBEDDED_ADMIN_ENDPOINT_OPERATIONS = {
|
|
1610
1623
|
show: operation("tenants", "get", "/embedded-admin-app"),
|
|
@@ -2314,7 +2327,10 @@ var ORDER_PRIMARY_DISPLAY_STATUSES = [
|
|
|
2314
2327
|
];
|
|
2315
2328
|
var CHECKOUT_STATUSES = ["open", "placed", "abandoned"];
|
|
2316
2329
|
var ABANDONED_REASONS = ["expired", "customer", "operator"];
|
|
2317
|
-
|
|
2330
|
+
function managedPaymentProviders(providers) {
|
|
2331
|
+
return providers;
|
|
2332
|
+
}
|
|
2333
|
+
var PAYMENT_PROVIDERS = managedPaymentProviders(["toss", "portone"]);
|
|
2318
2334
|
var PAYMENT_PROVIDER_STATUSES = [
|
|
2319
2335
|
"requires_payment_method",
|
|
2320
2336
|
"requires_action",
|
|
@@ -2362,6 +2378,26 @@ function invalidCanonicalResponse(resource, details) {
|
|
|
2362
2378
|
function fail(resource, path, expected) {
|
|
2363
2379
|
invalidCanonicalResponse(resource, { path, expected });
|
|
2364
2380
|
}
|
|
2381
|
+
function providerMismatch(resource, expected, actual, operation2) {
|
|
2382
|
+
if (operation2 === "cancel") {
|
|
2383
|
+
const method = {
|
|
2384
|
+
toss: "cancel()",
|
|
2385
|
+
portone: "cancelPortOne()"
|
|
2386
|
+
}[actual];
|
|
2387
|
+
throw createApiError(
|
|
2388
|
+
`Canonical ${resource} response provider mismatch: expected ${expected}, received ${actual}.`,
|
|
2389
|
+
502,
|
|
2390
|
+
{ resource, path: "$.provider", expected, actual },
|
|
2391
|
+
`The server applied the cancellation to a ${actual} PaymentSession, but this SDK method expects ${expected}.`,
|
|
2392
|
+
`Re-invoke ${method} with the same idempotencyKey; do not create a new key.`
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
invalidCanonicalResponse(resource, {
|
|
2396
|
+
path: "$.provider",
|
|
2397
|
+
expected,
|
|
2398
|
+
actual
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2365
2401
|
function record(value, resource, path) {
|
|
2366
2402
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2367
2403
|
fail(resource, path, "object");
|
|
@@ -3654,7 +3690,7 @@ function validateNextAction(value, provider, resource, path) {
|
|
|
3654
3690
|
if (action.customerEmail !== void 0)
|
|
3655
3691
|
validateCustomer({ email: action.customerEmail }, resource, path);
|
|
3656
3692
|
}
|
|
3657
|
-
function validatePublicPaymentSession(value, expectedProvider) {
|
|
3693
|
+
function validatePublicPaymentSession(value, expectedProvider, operation2) {
|
|
3658
3694
|
const resource = "PaymentSession";
|
|
3659
3695
|
const session = record(value, resource, "$");
|
|
3660
3696
|
const provider = enumValue(
|
|
@@ -3663,9 +3699,6 @@ function validatePublicPaymentSession(value, expectedProvider) {
|
|
|
3663
3699
|
resource,
|
|
3664
3700
|
"$.provider"
|
|
3665
3701
|
);
|
|
3666
|
-
if (expectedProvider !== void 0 && provider !== expectedProvider) {
|
|
3667
|
-
fail(resource, "$.provider", expectedProvider);
|
|
3668
|
-
}
|
|
3669
3702
|
const status = enumValue(
|
|
3670
3703
|
session.providerStatus,
|
|
3671
3704
|
PAYMENT_PROVIDER_STATUSES,
|
|
@@ -3869,44 +3902,128 @@ function validatePublicPaymentSession(value, expectedProvider) {
|
|
|
3869
3902
|
"$.cancellationReason"
|
|
3870
3903
|
);
|
|
3871
3904
|
}
|
|
3905
|
+
if (expectedProvider !== void 0 && provider !== expectedProvider) {
|
|
3906
|
+
if (operation2 === "cancel" && status !== "canceling" && status !== "canceled") {
|
|
3907
|
+
fail(
|
|
3908
|
+
resource,
|
|
3909
|
+
"$.providerStatus",
|
|
3910
|
+
"canceling or canceled after a successful cancellation"
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
providerMismatch(resource, expectedProvider, provider, operation2);
|
|
3914
|
+
}
|
|
3872
3915
|
return value;
|
|
3873
3916
|
}
|
|
3874
3917
|
|
|
3875
3918
|
// src/core/customer/customer-auth.ts
|
|
3876
3919
|
var DEFAULT_TIMEOUT2 = 15e3;
|
|
3877
|
-
|
|
3920
|
+
var REFRESH_LOCK_TIMEOUT = DEFAULT_TIMEOUT2 * 3;
|
|
3921
|
+
var SESSION_RECORD_VERSION = 1;
|
|
3922
|
+
var STORAGE_UNUSABLE = /* @__PURE__ */ Symbol("storage-unusable");
|
|
3923
|
+
function canonicalApiIdentity(apiUrl) {
|
|
3878
3924
|
try {
|
|
3879
|
-
|
|
3925
|
+
const url = new URL(apiUrl);
|
|
3926
|
+
return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
|
|
3927
|
+
} catch {
|
|
3928
|
+
return apiUrl.trim().toLowerCase().replace(/\/+$/, "");
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
function buildRefreshLockName(apiUrl, publishableKey, storageKey) {
|
|
3932
|
+
return `01software:customer-auth-refresh:${canonicalApiIdentity(apiUrl)}:${publishableKey}:${storageKey}`;
|
|
3933
|
+
}
|
|
3934
|
+
function getRefreshLockRequester() {
|
|
3935
|
+
try {
|
|
3936
|
+
if (typeof navigator === "undefined") return null;
|
|
3937
|
+
const locks = navigator.locks;
|
|
3938
|
+
if (!locks || typeof locks.request !== "function") return null;
|
|
3939
|
+
return (name, options, callback) => locks.request(
|
|
3940
|
+
name,
|
|
3941
|
+
options,
|
|
3942
|
+
() => callback()
|
|
3943
|
+
);
|
|
3944
|
+
} catch {
|
|
3945
|
+
return null;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
function parseSessionRecord(raw) {
|
|
3949
|
+
try {
|
|
3950
|
+
const parsed = JSON.parse(raw);
|
|
3951
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
3952
|
+
const candidate = parsed;
|
|
3953
|
+
if (candidate.v !== SESSION_RECORD_VERSION) return null;
|
|
3954
|
+
if (typeof candidate.rev !== "number" || !Number.isFinite(candidate.rev)) {
|
|
3955
|
+
return null;
|
|
3956
|
+
}
|
|
3957
|
+
const token = candidate.token;
|
|
3958
|
+
const refreshToken = candidate.refreshToken;
|
|
3959
|
+
if (token !== null && typeof token !== "string") return null;
|
|
3960
|
+
if (refreshToken !== null && typeof refreshToken !== "string") return null;
|
|
3961
|
+
return {
|
|
3962
|
+
v: SESSION_RECORD_VERSION,
|
|
3963
|
+
rev: candidate.rev,
|
|
3964
|
+
token,
|
|
3965
|
+
refreshToken
|
|
3966
|
+
};
|
|
3880
3967
|
} catch {
|
|
3881
3968
|
return null;
|
|
3882
3969
|
}
|
|
3883
3970
|
}
|
|
3971
|
+
function readSessionRecord(recordKey, mirrorKey, mirrorRefreshKey) {
|
|
3972
|
+
let raw;
|
|
3973
|
+
try {
|
|
3974
|
+
raw = localStorage.getItem(recordKey);
|
|
3975
|
+
} catch {
|
|
3976
|
+
return STORAGE_UNUSABLE;
|
|
3977
|
+
}
|
|
3978
|
+
if (raw !== null) {
|
|
3979
|
+
const parsed = parseSessionRecord(raw);
|
|
3980
|
+
return parsed ? { record: parsed, stored: true } : STORAGE_UNUSABLE;
|
|
3981
|
+
}
|
|
3982
|
+
try {
|
|
3983
|
+
return {
|
|
3984
|
+
record: {
|
|
3985
|
+
v: SESSION_RECORD_VERSION,
|
|
3986
|
+
rev: 0,
|
|
3987
|
+
token: localStorage.getItem(mirrorKey),
|
|
3988
|
+
refreshToken: localStorage.getItem(mirrorRefreshKey)
|
|
3989
|
+
},
|
|
3990
|
+
stored: false
|
|
3991
|
+
};
|
|
3992
|
+
} catch {
|
|
3993
|
+
return STORAGE_UNUSABLE;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3884
3996
|
var CustomerAuth = class {
|
|
3885
3997
|
constructor(publishableKey, options, apiUrl) {
|
|
3886
3998
|
this.refreshPromise = null;
|
|
3999
|
+
/** Canonical record key; null unless persistence is active in a browser. */
|
|
4000
|
+
this.persistRecordKey = null;
|
|
4001
|
+
/** Advisory single-value access mirror, plus the retired refresh mirror. */
|
|
4002
|
+
this.persistMirrorKey = null;
|
|
4003
|
+
this.persistMirrorRefreshKey = null;
|
|
4004
|
+
/** Cross-context refresh lock name; null when there is no shared storage. */
|
|
4005
|
+
this.refreshLockName = null;
|
|
4006
|
+
/** Revision of the canonical record this instance last observed. */
|
|
4007
|
+
this.sessionRev = 0;
|
|
3887
4008
|
this.publishableKey = publishableKey;
|
|
3888
4009
|
this.baseUrl = resolveApiUrl(apiUrl);
|
|
3889
4010
|
const persist = options?.persist ?? true;
|
|
3890
4011
|
if (persist) {
|
|
3891
4012
|
const key = typeof persist === "string" ? persist : "customer-token";
|
|
3892
|
-
const refreshKey = `${key}:refresh`;
|
|
3893
4013
|
const isBrowser = typeof window !== "undefined";
|
|
3894
|
-
this.token =
|
|
3895
|
-
this.refreshTokenValue =
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
} catch {
|
|
3908
|
-
}
|
|
3909
|
-
} : void 0;
|
|
4014
|
+
this.token = null;
|
|
4015
|
+
this.refreshTokenValue = null;
|
|
4016
|
+
if (isBrowser) {
|
|
4017
|
+
this.persistRecordKey = `${key}:session`;
|
|
4018
|
+
this.persistMirrorKey = key;
|
|
4019
|
+
this.persistMirrorRefreshKey = `${key}:refresh`;
|
|
4020
|
+
this.refreshLockName = buildRefreshLockName(
|
|
4021
|
+
this.baseUrl,
|
|
4022
|
+
publishableKey,
|
|
4023
|
+
key
|
|
4024
|
+
);
|
|
4025
|
+
this.syncFromPersistedStorage();
|
|
4026
|
+
}
|
|
3910
4027
|
} else {
|
|
3911
4028
|
this.token = options?.token ?? null;
|
|
3912
4029
|
this.refreshTokenValue = options?.refreshToken ?? null;
|
|
@@ -3934,66 +4051,299 @@ var CustomerAuth = class {
|
|
|
3934
4051
|
body: JSON.stringify(data)
|
|
3935
4052
|
}
|
|
3936
4053
|
);
|
|
3937
|
-
this.
|
|
3938
|
-
|
|
4054
|
+
if (result.refreshToken) this.setSession(result.token, result.refreshToken);
|
|
4055
|
+
else this.setToken(result.token);
|
|
3939
4056
|
return result;
|
|
3940
4057
|
}
|
|
3941
4058
|
/**
|
|
3942
4059
|
* Refresh the current token. Uses a refresh token when available, otherwise
|
|
3943
4060
|
* falls back to the legacy bearer-token compatibility path.
|
|
4061
|
+
*
|
|
4062
|
+
* Refresh tokens are single-use, so a persisted session shared by several
|
|
4063
|
+
* tabs or SDK instances must rotate at most once: concurrent rotations of the
|
|
4064
|
+
* same token are replay from the server's point of view and revoke the whole
|
|
4065
|
+
* family. Persisted browser sessions therefore serialize rotation across
|
|
4066
|
+
* contexts and re-read the canonical persisted credentials inside the lock.
|
|
3944
4067
|
*/
|
|
3945
4068
|
async refreshToken() {
|
|
4069
|
+
this.syncFromPersistedStorage();
|
|
3946
4070
|
if (!this.refreshTokenValue && !this.token) {
|
|
3947
4071
|
throw new ApiError("Not authenticated", 401);
|
|
3948
4072
|
}
|
|
3949
4073
|
if (this.refreshPromise) return this.refreshPromise;
|
|
3950
|
-
|
|
4074
|
+
const intended = this.currentFence();
|
|
4075
|
+
this.refreshPromise = this.rotateAcrossContexts(intended);
|
|
3951
4076
|
try {
|
|
3952
4077
|
return await this.refreshPromise;
|
|
3953
4078
|
} finally {
|
|
3954
4079
|
this.refreshPromise = null;
|
|
3955
4080
|
}
|
|
3956
4081
|
}
|
|
3957
|
-
|
|
3958
|
-
|
|
4082
|
+
/**
|
|
4083
|
+
* Re-read the canonical persisted session. In persisted mode the shared
|
|
4084
|
+
* record — not this instance's memory — is the source of truth, so another
|
|
4085
|
+
* context's rotation, login, or logout is observed before the next credential
|
|
4086
|
+
* use. Change callbacks are deliberately not invoked: this is a read.
|
|
4087
|
+
*/
|
|
4088
|
+
syncFromPersistedStorage() {
|
|
4089
|
+
const canonical = this.readCanonicalSession();
|
|
4090
|
+
if (canonical === STORAGE_UNUSABLE) {
|
|
4091
|
+
this.disablePersistence();
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
if (!canonical) return;
|
|
4095
|
+
this.sessionRev = canonical.record.rev;
|
|
4096
|
+
this.token = canonical.record.token;
|
|
4097
|
+
this.refreshTokenValue = canonical.record.refreshToken;
|
|
4098
|
+
}
|
|
4099
|
+
/** `null` when this instance does not persist. */
|
|
4100
|
+
readCanonicalSession() {
|
|
4101
|
+
if (!this.persistRecordKey || !this.persistMirrorKey || !this.persistMirrorRefreshKey) {
|
|
4102
|
+
return null;
|
|
4103
|
+
}
|
|
4104
|
+
return readSessionRecord(
|
|
4105
|
+
this.persistRecordKey,
|
|
4106
|
+
this.persistMirrorKey,
|
|
4107
|
+
this.persistMirrorRefreshKey
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
4110
|
+
/**
|
|
4111
|
+
* Publish a change to canonical storage.
|
|
4112
|
+
*
|
|
4113
|
+
* The canonical pair is re-read immediately before the single record write so
|
|
4114
|
+
* that a field this call does not change is taken from that read rather than
|
|
4115
|
+
* from this instance's possibly stale copy. That merge is BEST-EFFORT: it
|
|
4116
|
+
* removes the stale counterpart an instance would otherwise carry, but it is
|
|
4117
|
+
* not a compare-and-swap. Web Storage offers none, and the synchronous public
|
|
4118
|
+
* mutators cannot await the refresh lease, so a commit that lands between
|
|
4119
|
+
* this read and the store operation is lost by ordering — including the
|
|
4120
|
+
* counterpart just read, which is then republished stale. The window is one
|
|
4121
|
+
* store operation wide and the server remains authoritative; see
|
|
4122
|
+
* `CUST-AUTH-033` for the stated limit.
|
|
4123
|
+
*
|
|
4124
|
+
* Returns `false` only when `requireLiveSession` refused a write because the
|
|
4125
|
+
* canonical session had already been cleared.
|
|
4126
|
+
*/
|
|
4127
|
+
commitPersistedSession(change, options) {
|
|
4128
|
+
const recordKey = this.persistRecordKey;
|
|
4129
|
+
const mirrorKey = this.persistMirrorKey;
|
|
4130
|
+
const mirrorRefreshKey = this.persistMirrorRefreshKey;
|
|
4131
|
+
const canonical = this.readCanonicalSession();
|
|
4132
|
+
if (canonical === null || !recordKey || !mirrorKey || !mirrorRefreshKey) {
|
|
4133
|
+
return true;
|
|
4134
|
+
}
|
|
4135
|
+
if (canonical === STORAGE_UNUSABLE) {
|
|
4136
|
+
this.disablePersistence();
|
|
4137
|
+
return true;
|
|
4138
|
+
}
|
|
4139
|
+
const current = canonical.record;
|
|
4140
|
+
if (options?.requireLiveSession && !current.token && !current.refreshToken) {
|
|
4141
|
+
return false;
|
|
4142
|
+
}
|
|
4143
|
+
const next = {
|
|
4144
|
+
v: SESSION_RECORD_VERSION,
|
|
4145
|
+
rev: current.rev + 1,
|
|
4146
|
+
token: "token" in change ? change.token ?? null : current.token,
|
|
4147
|
+
refreshToken: "refreshToken" in change ? change.refreshToken ?? null : current.refreshToken
|
|
4148
|
+
};
|
|
4149
|
+
if (
|
|
4150
|
+
// Only a state that is genuinely persisted can already say what this
|
|
4151
|
+
// write would say. An absent record reads as an empty pair, and treating
|
|
4152
|
+
// that as a no-op would drop the very tombstone that stops a later
|
|
4153
|
+
// migration from resurrecting the session out of the earlier layout.
|
|
4154
|
+
canonical.stored && next.token === current.token && next.refreshToken === current.refreshToken
|
|
4155
|
+
) {
|
|
4156
|
+
this.sessionRev = current.rev;
|
|
4157
|
+
this.token = current.token;
|
|
4158
|
+
this.refreshTokenValue = current.refreshToken;
|
|
4159
|
+
return true;
|
|
4160
|
+
}
|
|
4161
|
+
try {
|
|
4162
|
+
localStorage.setItem(recordKey, JSON.stringify(next));
|
|
4163
|
+
} catch {
|
|
4164
|
+
this.disablePersistence();
|
|
4165
|
+
return true;
|
|
4166
|
+
}
|
|
4167
|
+
this.sessionRev = next.rev;
|
|
4168
|
+
this.token = next.token;
|
|
4169
|
+
this.refreshTokenValue = next.refreshToken;
|
|
4170
|
+
try {
|
|
4171
|
+
localStorage.removeItem(mirrorRefreshKey);
|
|
4172
|
+
} catch {
|
|
4173
|
+
return true;
|
|
4174
|
+
}
|
|
4175
|
+
try {
|
|
4176
|
+
if (next.token) localStorage.setItem(mirrorKey, next.token);
|
|
4177
|
+
else localStorage.removeItem(mirrorKey);
|
|
4178
|
+
} catch {
|
|
4179
|
+
}
|
|
4180
|
+
return true;
|
|
4181
|
+
}
|
|
4182
|
+
/**
|
|
4183
|
+
* Storage rejected a write or a read, so it is not a canonical store for this
|
|
4184
|
+
* instance. Fall back to the memory-only session and take no lease: there is
|
|
4185
|
+
* no shared state left to coordinate. Nothing is purged — the record commit
|
|
4186
|
+
* is atomic, so no inconsistent state was published, and another context's
|
|
4187
|
+
* valid session must not be destroyed by this instance's local failure.
|
|
4188
|
+
*/
|
|
4189
|
+
disablePersistence() {
|
|
4190
|
+
this.persistRecordKey = null;
|
|
4191
|
+
this.persistMirrorKey = null;
|
|
4192
|
+
this.persistMirrorRefreshKey = null;
|
|
4193
|
+
this.refreshLockName = null;
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Compare the canonical record with the fence this call captured before it
|
|
4197
|
+
* left. Every persisted decision — rotate, converge, or fail closed — goes
|
|
4198
|
+
* through this one comparison. The revision catches a logout, a fresh login,
|
|
4199
|
+
* or a manual setter even when token values coincide; the value comparison
|
|
4200
|
+
* additionally catches a mirror-only writer that cannot bump a revision.
|
|
4201
|
+
*/
|
|
4202
|
+
classifyCanonicalState(intended) {
|
|
4203
|
+
if (!this.persistRecordKey) return "current";
|
|
4204
|
+
this.syncFromPersistedStorage();
|
|
4205
|
+
if (!this.persistRecordKey) return "current";
|
|
4206
|
+
if (!this.token && !this.refreshTokenValue) return "cleared";
|
|
4207
|
+
if (intended.refreshToken !== null) {
|
|
4208
|
+
return this.refreshTokenValue === intended.refreshToken ? "current" : "superseded";
|
|
4209
|
+
}
|
|
4210
|
+
return this.token === intended.token && !this.refreshTokenValue ? "current" : "superseded";
|
|
4211
|
+
}
|
|
4212
|
+
/** The canonical persisted pair, as a refresh result. */
|
|
4213
|
+
canonicalPair(token) {
|
|
4214
|
+
return {
|
|
4215
|
+
token,
|
|
4216
|
+
...this.refreshTokenValue ? { refreshToken: this.refreshTokenValue } : {}
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
/** Current credentials plus the revision they were observed at. */
|
|
4220
|
+
currentFence() {
|
|
4221
|
+
return { token: this.token, refreshToken: this.refreshTokenValue };
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Clear an access credential a 401 just proved unusable — but only while it
|
|
4225
|
+
* is still the canonical one. A rotation committed by another context while
|
|
4226
|
+
* the request was in flight is newer than this 401, so it must survive; the
|
|
4227
|
+
* instance adopts it instead. The refresh entry is never touched here: a
|
|
4228
|
+
* rejected access token says nothing about the refresh family.
|
|
4229
|
+
*/
|
|
4230
|
+
invalidateAccessToken(spentToken) {
|
|
4231
|
+
this.syncFromPersistedStorage();
|
|
4232
|
+
if (this.token !== spentToken) return;
|
|
4233
|
+
this.setToken(null);
|
|
4234
|
+
}
|
|
4235
|
+
/** Commit both credentials in one canonical write. */
|
|
4236
|
+
setSession(token, refreshToken, options) {
|
|
4237
|
+
this.token = token;
|
|
4238
|
+
this.refreshTokenValue = refreshToken;
|
|
4239
|
+
const committed = this.commitPersistedSession(
|
|
4240
|
+
{ token, refreshToken },
|
|
4241
|
+
options
|
|
4242
|
+
);
|
|
4243
|
+
if (!committed) return false;
|
|
4244
|
+
this.onTokenChange?.(this.token);
|
|
4245
|
+
this.onRefreshTokenChange?.(this.refreshTokenValue);
|
|
4246
|
+
return true;
|
|
4247
|
+
}
|
|
4248
|
+
/**
|
|
4249
|
+
* Serialize rotation against every other context sharing the same storage
|
|
4250
|
+
* key and API/tenant identity. Coordination is best-effort: when it is
|
|
4251
|
+
* unavailable or times out the rotation still happens, so server-side reuse
|
|
4252
|
+
* detection is never bypassed by a client that failed to coordinate.
|
|
4253
|
+
*/
|
|
4254
|
+
async rotateAcrossContexts(intended) {
|
|
4255
|
+
const lockName = this.refreshLockName;
|
|
4256
|
+
const requestLock = lockName ? getRefreshLockRequester() : null;
|
|
4257
|
+
if (!lockName || !requestLock) {
|
|
4258
|
+
return this.rotateWithCanonicalState(intended);
|
|
4259
|
+
}
|
|
4260
|
+
const controller = new AbortController();
|
|
4261
|
+
const timer = setTimeout(() => controller.abort(), REFRESH_LOCK_TIMEOUT);
|
|
4262
|
+
let entered = false;
|
|
4263
|
+
try {
|
|
4264
|
+
return await requestLock(
|
|
4265
|
+
lockName,
|
|
4266
|
+
{ signal: controller.signal },
|
|
4267
|
+
async () => {
|
|
4268
|
+
entered = true;
|
|
4269
|
+
return this.rotateWithCanonicalState(intended);
|
|
4270
|
+
}
|
|
4271
|
+
);
|
|
4272
|
+
} catch (error) {
|
|
4273
|
+
if (entered) throw error;
|
|
4274
|
+
return this.rotateWithCanonicalState(intended);
|
|
4275
|
+
} finally {
|
|
4276
|
+
clearTimeout(timer);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
/**
|
|
4280
|
+
* Decide, against freshly re-read canonical state, whether this context still
|
|
4281
|
+
* owns the rotation.
|
|
4282
|
+
*/
|
|
4283
|
+
async rotateWithCanonicalState(intended) {
|
|
4284
|
+
let fence = intended;
|
|
4285
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
4286
|
+
const state = this.classifyCanonicalState(fence);
|
|
4287
|
+
if (state === "cleared") throw new ApiError("Not authenticated", 401);
|
|
4288
|
+
if (state === "current") return this._doRefreshToken(fence);
|
|
4289
|
+
if (this.token) return this.canonicalPair(this.token);
|
|
4290
|
+
fence = this.currentFence();
|
|
4291
|
+
}
|
|
4292
|
+
throw new ApiError("Not authenticated", 401);
|
|
4293
|
+
}
|
|
4294
|
+
async _doRefreshToken(intended) {
|
|
4295
|
+
const init = intended.refreshToken ? {
|
|
3959
4296
|
method: "POST",
|
|
3960
|
-
body: JSON.stringify({ refreshToken:
|
|
4297
|
+
body: JSON.stringify({ refreshToken: intended.refreshToken })
|
|
3961
4298
|
} : {
|
|
3962
4299
|
method: "POST",
|
|
3963
|
-
headers: { Authorization: `Bearer ${
|
|
4300
|
+
headers: { Authorization: `Bearer ${intended.token}` }
|
|
3964
4301
|
};
|
|
3965
4302
|
const result = await this.requestJson(
|
|
3966
4303
|
"/api/customers/refresh",
|
|
3967
4304
|
init
|
|
3968
4305
|
);
|
|
3969
|
-
this.
|
|
3970
|
-
if (
|
|
3971
|
-
|
|
4306
|
+
const state = this.classifyCanonicalState(intended);
|
|
4307
|
+
if (state === "cleared") throw new ApiError("Not authenticated", 401);
|
|
4308
|
+
if (state === "superseded") {
|
|
4309
|
+
if (this.token) return this.canonicalPair(this.token);
|
|
4310
|
+
throw new ApiError("Not authenticated", 401);
|
|
4311
|
+
}
|
|
4312
|
+
const replacedAccessToken = this.token !== null && this.token !== intended.token ? this.token : null;
|
|
4313
|
+
const nextToken = replacedAccessToken ?? result.token;
|
|
4314
|
+
const nextRefresh = result.refreshToken ?? null;
|
|
4315
|
+
if (!this.setSession(nextToken, nextRefresh, { requireLiveSession: true })) {
|
|
4316
|
+
throw new ApiError("Not authenticated", 401);
|
|
4317
|
+
}
|
|
4318
|
+
return {
|
|
4319
|
+
token: nextToken,
|
|
4320
|
+
...nextRefresh ? { refreshToken: nextRefresh } : {}
|
|
4321
|
+
};
|
|
3972
4322
|
}
|
|
3973
4323
|
/**
|
|
3974
4324
|
* Clear the stored token
|
|
3975
4325
|
*/
|
|
3976
4326
|
logout() {
|
|
3977
|
-
this.
|
|
3978
|
-
this.setRefreshToken(null);
|
|
4327
|
+
this.setSession(null, null);
|
|
3979
4328
|
}
|
|
3980
4329
|
/**
|
|
3981
4330
|
* Get the current authenticated customer's profile
|
|
3982
4331
|
*/
|
|
3983
4332
|
async me() {
|
|
3984
|
-
|
|
4333
|
+
const token = this.getToken();
|
|
4334
|
+
if (!token) return null;
|
|
3985
4335
|
try {
|
|
3986
4336
|
const data = await this.requestJson(
|
|
3987
4337
|
"/api/customers/me",
|
|
3988
4338
|
{
|
|
3989
4339
|
method: "GET",
|
|
3990
|
-
headers: { Authorization: `Bearer ${
|
|
4340
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3991
4341
|
}
|
|
3992
4342
|
);
|
|
3993
4343
|
return data.customer ?? null;
|
|
3994
4344
|
} catch (error) {
|
|
3995
4345
|
if (error instanceof SDKError && error.status === 401) {
|
|
3996
|
-
this.
|
|
4346
|
+
this.invalidateAccessToken(token);
|
|
3997
4347
|
return null;
|
|
3998
4348
|
}
|
|
3999
4349
|
throw error;
|
|
@@ -4021,12 +4371,13 @@ var CustomerAuth = class {
|
|
|
4021
4371
|
* Update the authenticated customer's profile (name, phone, marketingConsent)
|
|
4022
4372
|
*/
|
|
4023
4373
|
async updateProfile(data) {
|
|
4024
|
-
|
|
4374
|
+
const token = this.getToken();
|
|
4375
|
+
if (!token) throw new ApiError("Not authenticated", 401);
|
|
4025
4376
|
const result = await this.requestJson(
|
|
4026
4377
|
"/api/customers/me",
|
|
4027
4378
|
{
|
|
4028
4379
|
method: "PATCH",
|
|
4029
|
-
headers: { Authorization: `Bearer ${
|
|
4380
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
4030
4381
|
body: JSON.stringify(data)
|
|
4031
4382
|
}
|
|
4032
4383
|
);
|
|
@@ -4036,10 +4387,11 @@ var CustomerAuth = class {
|
|
|
4036
4387
|
* Change the password of the currently authenticated customer
|
|
4037
4388
|
*/
|
|
4038
4389
|
async changePassword(currentPassword, newPassword) {
|
|
4039
|
-
|
|
4390
|
+
const token = this.getToken();
|
|
4391
|
+
if (!token) throw new ApiError("Not authenticated", 401);
|
|
4040
4392
|
await this.requestJson("/api/customers/change-password", {
|
|
4041
4393
|
method: "POST",
|
|
4042
|
-
headers: { Authorization: `Bearer ${
|
|
4394
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
4043
4395
|
body: JSON.stringify({ currentPassword, newPassword })
|
|
4044
4396
|
});
|
|
4045
4397
|
}
|
|
@@ -4047,7 +4399,8 @@ var CustomerAuth = class {
|
|
|
4047
4399
|
* Get the authenticated customer's orders with pagination and optional status filter
|
|
4048
4400
|
*/
|
|
4049
4401
|
async getMyOrders(options) {
|
|
4050
|
-
|
|
4402
|
+
const token = this.getToken();
|
|
4403
|
+
if (!token) throw new ApiError("Not authenticated", 401);
|
|
4051
4404
|
const params = new URLSearchParams();
|
|
4052
4405
|
if (options?.page) params.set("page", String(options.page));
|
|
4053
4406
|
if (options?.limit) params.set("limit", String(options.limit));
|
|
@@ -4069,21 +4422,27 @@ var CustomerAuth = class {
|
|
|
4069
4422
|
`/api/customers/me/orders${qs ? `?${qs}` : ""}`,
|
|
4070
4423
|
{
|
|
4071
4424
|
method: "GET",
|
|
4072
|
-
headers: { Authorization: `Bearer ${
|
|
4425
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
4073
4426
|
}
|
|
4074
4427
|
);
|
|
4075
4428
|
return validatePublicOrderList(result);
|
|
4076
4429
|
}
|
|
4077
4430
|
/**
|
|
4078
|
-
* Get the current token (or null if not authenticated)
|
|
4431
|
+
* Get the current token (or null if not authenticated).
|
|
4432
|
+
*
|
|
4433
|
+
* In persisted mode this re-reads shared storage first, so a rotation or
|
|
4434
|
+
* logout performed by another tab is observed before the token is used.
|
|
4079
4435
|
*/
|
|
4080
4436
|
getToken() {
|
|
4437
|
+
this.syncFromPersistedStorage();
|
|
4081
4438
|
return this.token;
|
|
4082
4439
|
}
|
|
4083
4440
|
/**
|
|
4084
|
-
* Get the current refresh token (or null if unavailable)
|
|
4441
|
+
* Get the current refresh token (or null if unavailable). Persisted mode
|
|
4442
|
+
* re-reads shared storage first, like {@link CustomerAuth.getToken}.
|
|
4085
4443
|
*/
|
|
4086
4444
|
getRefreshToken() {
|
|
4445
|
+
this.syncFromPersistedStorage();
|
|
4087
4446
|
return this.refreshTokenValue;
|
|
4088
4447
|
}
|
|
4089
4448
|
/**
|
|
@@ -4091,6 +4450,7 @@ var CustomerAuth = class {
|
|
|
4091
4450
|
*/
|
|
4092
4451
|
setToken(token) {
|
|
4093
4452
|
this.token = token;
|
|
4453
|
+
this.commitPersistedSession({ token });
|
|
4094
4454
|
this.onTokenChange?.(token);
|
|
4095
4455
|
}
|
|
4096
4456
|
/**
|
|
@@ -4098,13 +4458,14 @@ var CustomerAuth = class {
|
|
|
4098
4458
|
*/
|
|
4099
4459
|
setRefreshToken(refreshToken) {
|
|
4100
4460
|
this.refreshTokenValue = refreshToken;
|
|
4461
|
+
this.commitPersistedSession({ refreshToken });
|
|
4101
4462
|
this.onRefreshTokenChange?.(refreshToken);
|
|
4102
4463
|
}
|
|
4103
4464
|
/**
|
|
4104
4465
|
* Check if the customer is currently authenticated
|
|
4105
4466
|
*/
|
|
4106
4467
|
isAuthenticated() {
|
|
4107
|
-
return this.
|
|
4468
|
+
return this.getToken() !== null;
|
|
4108
4469
|
}
|
|
4109
4470
|
/**
|
|
4110
4471
|
* Internal: make a request with timeout and error handling.
|
|
@@ -4454,8 +4815,12 @@ var PaymentSessionApi = class extends CustomerScopedApi {
|
|
|
4454
4815
|
...Object.keys(headers).length > 0 && { headers }
|
|
4455
4816
|
});
|
|
4456
4817
|
}
|
|
4457
|
-
async paymentSession(result, expectedProvider) {
|
|
4458
|
-
return validatePublicPaymentSession(
|
|
4818
|
+
async paymentSession(result, expectedProvider, operation2) {
|
|
4819
|
+
return validatePublicPaymentSession(
|
|
4820
|
+
await result,
|
|
4821
|
+
expectedProvider,
|
|
4822
|
+
operation2
|
|
4823
|
+
);
|
|
4459
4824
|
}
|
|
4460
4825
|
create(params) {
|
|
4461
4826
|
const { idempotencyKey, ...body } = params;
|
|
@@ -4502,7 +4867,8 @@ var PaymentSessionApi = class extends CustomerScopedApi {
|
|
|
4502
4867
|
const endpoint = this.serverTransport ? COMMERCE_ENDPOINTS.paymentSessions.server.cancel : COMMERCE_ENDPOINTS.paymentSessions.cancel;
|
|
4503
4868
|
return this.paymentSession(
|
|
4504
4869
|
this.post(endpoint, body, idempotencyKey),
|
|
4505
|
-
"toss"
|
|
4870
|
+
"toss",
|
|
4871
|
+
"cancel"
|
|
4506
4872
|
);
|
|
4507
4873
|
}
|
|
4508
4874
|
cancelPortOne(params) {
|
|
@@ -4511,7 +4877,8 @@ var PaymentSessionApi = class extends CustomerScopedApi {
|
|
|
4511
4877
|
const endpoint = this.serverTransport ? COMMERCE_ENDPOINTS.paymentSessions.server.cancel : COMMERCE_ENDPOINTS.paymentSessions.cancel;
|
|
4512
4878
|
return this.paymentSession(
|
|
4513
4879
|
this.post(endpoint, body, idempotencyKey),
|
|
4514
|
-
"portone"
|
|
4880
|
+
"portone",
|
|
4881
|
+
"cancel"
|
|
4515
4882
|
);
|
|
4516
4883
|
}
|
|
4517
4884
|
confirm(params) {
|
|
@@ -6952,7 +7319,11 @@ var COMMERCE_NOTIFICATION_EVENTS = [
|
|
|
6952
7319
|
"returnCanceled",
|
|
6953
7320
|
"returnCompleted",
|
|
6954
7321
|
"paymentReconciliationRequired",
|
|
6955
|
-
"paymentRefundRequiresAttention"
|
|
7322
|
+
"paymentRefundRequiresAttention",
|
|
7323
|
+
"membershipActivated",
|
|
7324
|
+
"membershipPastDue",
|
|
7325
|
+
"membershipExpired",
|
|
7326
|
+
"membershipCancelled"
|
|
6956
7327
|
];
|
|
6957
7328
|
function isStringOrNumber(value) {
|
|
6958
7329
|
return typeof value === "string" || typeof value === "number";
|
|
@@ -6982,7 +7353,7 @@ function isCommerceNotificationEventName(value) {
|
|
|
6982
7353
|
return typeof value === "string" && COMMERCE_NOTIFICATION_EVENTS.includes(value);
|
|
6983
7354
|
}
|
|
6984
7355
|
function isCommerceNotificationSourceCollection(value) {
|
|
6985
|
-
return value === "orders" || value === "fulfillments" || value === "returns" || value === "payment-sessions" || value === "payment-refund-attempts";
|
|
7356
|
+
return value === "orders" || value === "fulfillments" || value === "returns" || value === "payment-sessions" || value === "payment-refund-attempts" || value === "customer-memberships";
|
|
6986
7357
|
}
|
|
6987
7358
|
function isCommerceNotificationData(value) {
|
|
6988
7359
|
if (!isRecord(value)) return false;
|
|
@@ -7002,6 +7373,10 @@ function isPaymentIncidentNotificationData(value, event) {
|
|
|
7002
7373
|
const expectedReason = reconciliation ? "orderless_capture" : "local_commit_ambiguous";
|
|
7003
7374
|
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);
|
|
7004
7375
|
}
|
|
7376
|
+
function isMembershipNotificationData(value, event) {
|
|
7377
|
+
const expectedStatus = event === "membershipActivated" ? "active" : event === "membershipPastDue" ? "past_due" : event === "membershipExpired" ? "expired" : "cancelled";
|
|
7378
|
+
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));
|
|
7379
|
+
}
|
|
7005
7380
|
function isWebhookCommerceNotificationChange(value) {
|
|
7006
7381
|
if (!isRecord(value)) return false;
|
|
7007
7382
|
return value.type === "notification" && value.source === "commerce-notifications" && isCommerceNotificationEventName(value.event) && isCommerceNotificationSourceCollection(value.sourceCollection) && isStringOrNumber(value.sourceId);
|
|
@@ -7050,6 +7425,9 @@ function isCommerceNotificationWebhookEvent(event) {
|
|
|
7050
7425
|
const notificationSourceId = notification.event === "paymentReconciliationRequired" ? notification.paymentSessionId : notification.refundAttemptId;
|
|
7051
7426
|
return sourceCollection === expectedCollection && typeof notification.paymentSessionId === "string" && notification.paymentSessionId === incidentData.paymentSessionId && typeof notificationSourceId === "string" && notificationSourceId === incidentData.source.id && matchesOptionalId(changeSourceId, incidentData.source.id);
|
|
7052
7427
|
}
|
|
7428
|
+
if (notification.event === "membershipActivated" || notification.event === "membershipPastDue" || notification.event === "membershipExpired" || notification.event === "membershipCancelled") {
|
|
7429
|
+
return sourceCollection === "customer-memberships" && isMembershipNotificationData(data, notification.event) && typeof notification.membershipId === "string" && notification.membershipId === data.membershipId && matchesOptionalId(changeSourceId, data.membershipId);
|
|
7430
|
+
}
|
|
7053
7431
|
if (notification.event === "orderPaid" || notification.event === "orderCanceled" || notification.event === "orderDelivered") {
|
|
7054
7432
|
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);
|
|
7055
7433
|
}
|
|
@@ -7167,6 +7545,7 @@ var INTERNAL_COLLECTIONS = [
|
|
|
7167
7545
|
"fulfillment-events",
|
|
7168
7546
|
"tenant-commerce-config",
|
|
7169
7547
|
"customer-oauth-clients",
|
|
7548
|
+
"customer-group-grants",
|
|
7170
7549
|
"api-keys",
|
|
7171
7550
|
"personal-access-tokens",
|
|
7172
7551
|
"tenant-entitlements",
|