@devsym/subscription-portal-s2s-client 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +140 -0
- package/dist/index.cjs +449 -0
- package/dist/index.d.cts +255 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +419 -0
- package/package.json +52 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PortalS2sClient: () => PortalS2sClient,
|
|
24
|
+
PortalS2sError: () => PortalS2sError,
|
|
25
|
+
S2S_API_VERSION: () => S2S_API_VERSION,
|
|
26
|
+
defaultTokenProvider: () => defaultTokenProvider
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/errors.ts
|
|
31
|
+
var PortalS2sError = class extends Error {
|
|
32
|
+
status;
|
|
33
|
+
code;
|
|
34
|
+
correlationId;
|
|
35
|
+
details;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
super(options.message);
|
|
38
|
+
this.name = "PortalS2sError";
|
|
39
|
+
this.status = options.status;
|
|
40
|
+
this.code = options.code;
|
|
41
|
+
this.correlationId = options.correlationId;
|
|
42
|
+
this.details = options.details;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/retry.ts
|
|
47
|
+
var MAX_ATTEMPTS = 3;
|
|
48
|
+
var MAX_RETRY_DELAY_MS = 3e4;
|
|
49
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
50
|
+
var TransportRetryError = class extends Error {
|
|
51
|
+
cause;
|
|
52
|
+
constructor(cause) {
|
|
53
|
+
super("Portal request transport failed");
|
|
54
|
+
this.name = "TransportRetryError";
|
|
55
|
+
this.cause = cause;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function backoffDelay(attempt) {
|
|
59
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
60
|
+
return 250 * 2 ** (attempt - 1) + jitter;
|
|
61
|
+
}
|
|
62
|
+
function retryAfterDelay(value) {
|
|
63
|
+
if (value === null) {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
if (/^\d+$/.test(trimmed)) {
|
|
68
|
+
return Math.min(Number(trimmed) * 1e3, MAX_RETRY_DELAY_MS);
|
|
69
|
+
}
|
|
70
|
+
const timestamp = Date.parse(trimmed);
|
|
71
|
+
if (Number.isNaN(timestamp)) {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
return Math.min(Math.max(0, timestamp - Date.now()), MAX_RETRY_DELAY_MS);
|
|
75
|
+
}
|
|
76
|
+
async function discardResponseBody(response) {
|
|
77
|
+
if (!response.body) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await response.body.cancel();
|
|
82
|
+
} catch {
|
|
83
|
+
try {
|
|
84
|
+
await response.arrayBuffer();
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function fetchWithRetry(operation, sleep) {
|
|
90
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await operation();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (attempt === MAX_ATTEMPTS) {
|
|
96
|
+
throw new TransportRetryError(error);
|
|
97
|
+
}
|
|
98
|
+
await sleep(backoffDelay(attempt));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!RETRYABLE_STATUSES.has(response.status) || attempt === MAX_ATTEMPTS) {
|
|
102
|
+
return response;
|
|
103
|
+
}
|
|
104
|
+
await discardResponseBody(response);
|
|
105
|
+
await sleep(
|
|
106
|
+
Math.min(
|
|
107
|
+
retryAfterDelay(response.headers.get("retry-after")) ?? backoffDelay(attempt),
|
|
108
|
+
MAX_RETRY_DELAY_MS
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
throw new Error("Unreachable retry state");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/token-provider.ts
|
|
116
|
+
var import_identity = require("@azure/identity");
|
|
117
|
+
function defaultTokenProvider() {
|
|
118
|
+
return new import_identity.DefaultAzureCredential();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/client.ts
|
|
122
|
+
var S2S_API_VERSION = "v1";
|
|
123
|
+
function isRecord(value) {
|
|
124
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
125
|
+
}
|
|
126
|
+
function isString(value) {
|
|
127
|
+
return typeof value === "string";
|
|
128
|
+
}
|
|
129
|
+
function isOptionalString(value) {
|
|
130
|
+
return value === void 0 || isString(value);
|
|
131
|
+
}
|
|
132
|
+
function isNumber(value) {
|
|
133
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
134
|
+
}
|
|
135
|
+
function isOneOf(value, choices) {
|
|
136
|
+
return isString(value) && choices.includes(value);
|
|
137
|
+
}
|
|
138
|
+
var providers = ["microsoft", "paddle", "trial", "manual"];
|
|
139
|
+
var entitlementTypes = ["paid", "trial", "manual"];
|
|
140
|
+
var subscriptionStatuses = [
|
|
141
|
+
"pending",
|
|
142
|
+
"active",
|
|
143
|
+
"suspended",
|
|
144
|
+
"canceled",
|
|
145
|
+
"expired",
|
|
146
|
+
"incomplete",
|
|
147
|
+
"unassigned"
|
|
148
|
+
];
|
|
149
|
+
function isSubscriptionStatusResponse(value) {
|
|
150
|
+
if (!isRecord(value) || !Array.isArray(value.subscriptions)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return isString(value.appId) && isString(value.microsoftTenantId) && isOptionalString(value.customerOrganizationId) && typeof value.hasActiveEntitlement === "boolean" && isNumber(value.aggregateEffectiveQuantity) && value.subscriptions.every((subscription) => isRecord(subscription) && isString(subscription.subscriptionId) && isOneOf(subscription.status, subscriptionStatuses) && isOneOf(subscription.entitlementType, entitlementTypes) && isOneOf(subscription.provider, providers) && (subscription.effectiveQuantity === void 0 || isNumber(subscription.effectiveQuantity)) && isOptionalString(subscription.lastSynchronizedOn));
|
|
154
|
+
}
|
|
155
|
+
function isStringArray(value) {
|
|
156
|
+
return Array.isArray(value) && value.every(isString);
|
|
157
|
+
}
|
|
158
|
+
function isCapabilitiesResponse(value) {
|
|
159
|
+
return isRecord(value) && value.apiVersion === S2S_API_VERSION && isString(value.appId) && isOptionalString(value.portalVersion) && isStringArray(value.operations) && isStringArray(value.grantedRoles);
|
|
160
|
+
}
|
|
161
|
+
function isEntitlementsResponse(value) {
|
|
162
|
+
if (!isRecord(value) || !Array.isArray(value.items)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return isString(value.appId) && isString(value.microsoftTenantId) && isOptionalString(value.customerOrganizationId) && typeof value.hasActiveEntitlement === "boolean" && isNumber(value.aggregateEffectiveQuantity) && value.items.every((item) => isRecord(item) && isString(item.subscriptionId) && isOneOf(item.provider, providers) && isOneOf(item.entitlementType, entitlementTypes) && isString(item.status) && (item.effectiveQuantity === void 0 || isNumber(item.effectiveQuantity)) && isOptionalString(item.lastSynchronizedOn));
|
|
166
|
+
}
|
|
167
|
+
function isLinkageResponse(value) {
|
|
168
|
+
if (!isRecord(value) || !Array.isArray(value.claims)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const tenantLinkIsValid = value.tenantLink === void 0 || isRecord(value.tenantLink) && isOneOf(
|
|
172
|
+
value.tenantLink.linkSource,
|
|
173
|
+
["activation", "claim", "manualAssignment", "trial"]
|
|
174
|
+
) && isString(value.tenantLink.linkedOn) && typeof value.tenantLink.isActive === "boolean";
|
|
175
|
+
return isString(value.appId) && isString(value.microsoftTenantId) && isOptionalString(value.customerOrganizationId) && tenantLinkIsValid && value.claims.every((claim) => isRecord(claim) && isString(claim.subscriptionId) && isOneOf(claim.claimState, ["assigned", "unassigned", "released"]));
|
|
176
|
+
}
|
|
177
|
+
function isRedeemLicenseKeyResponse(value) {
|
|
178
|
+
return isRecord(value) && isOneOf(value.result, ["linked", "alreadyLinked"]) && isString(value.subscriptionId) && isString(value.customerOrganizationId) && isString(value.effectiveEntitledAppId) && isString(value.status) && isNumber(value.quantity) && isString(value.ownershipAcquisitionMode) && isString(value.correlationId);
|
|
179
|
+
}
|
|
180
|
+
function isReleaseClaimResponse(value) {
|
|
181
|
+
return isRecord(value) && value.result === "released" && isOptionalString(value.subscriptionId) && isString(value.correlationId);
|
|
182
|
+
}
|
|
183
|
+
function isStartTrialResponse(value) {
|
|
184
|
+
return isRecord(value) && isOneOf(value.result, ["started", "alreadyActive"]) && isString(value.subscriptionId) && isString(value.customerOrganizationId) && isOneOf(value.trialMode, ["timeBased", "seatLimited"]) && value.trialStatus === "active" && isString(value.trialStartOn) && isOptionalString(value.trialEndOn) && (value.trialSeatLimit === void 0 || isNumber(value.trialSeatLimit)) && (value.trialUsage === void 0 || isNumber(value.trialUsage)) && isString(value.correlationId);
|
|
185
|
+
}
|
|
186
|
+
function isSyncRequestResponse(value) {
|
|
187
|
+
return isRecord(value) && typeof value.accepted === "boolean" && isString(value.syncRequestId) && value.status === "queued" && isString(value.correlationId);
|
|
188
|
+
}
|
|
189
|
+
function isApiErrorEnvelope(value) {
|
|
190
|
+
if (!isRecord(value) || !isRecord(value.error)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const { code, message, correlationId, details } = value.error;
|
|
194
|
+
return typeof code === "string" && typeof message === "string" && (correlationId === void 0 || typeof correlationId === "string") && (details === void 0 || isRecord(details));
|
|
195
|
+
}
|
|
196
|
+
var PortalS2sClient = class {
|
|
197
|
+
baseUrl;
|
|
198
|
+
audience;
|
|
199
|
+
credential;
|
|
200
|
+
fetch;
|
|
201
|
+
randomUuid;
|
|
202
|
+
sleep;
|
|
203
|
+
constructor(options) {
|
|
204
|
+
if (typeof options.baseUrl !== "string" || !options.baseUrl.trim()) {
|
|
205
|
+
throw new TypeError("baseUrl must be a non-blank string");
|
|
206
|
+
}
|
|
207
|
+
const rawBaseUrl = options.baseUrl.trim();
|
|
208
|
+
let parsedBaseUrl;
|
|
209
|
+
try {
|
|
210
|
+
parsedBaseUrl = new URL(rawBaseUrl);
|
|
211
|
+
} catch {
|
|
212
|
+
throw new TypeError("baseUrl must be an absolute HTTPS URL");
|
|
213
|
+
}
|
|
214
|
+
const authorityStart = rawBaseUrl.indexOf("://") + 3;
|
|
215
|
+
const authoritySuffix = authorityStart >= 3 ? rawBaseUrl.slice(authorityStart) : "";
|
|
216
|
+
const authorityEndOffset = authoritySuffix.search(/[/?#]/);
|
|
217
|
+
const authority = authorityEndOffset === -1 ? authoritySuffix : authoritySuffix.slice(0, authorityEndOffset);
|
|
218
|
+
const pathAndSuffix = authorityEndOffset === -1 ? "" : authoritySuffix.slice(authorityEndOffset);
|
|
219
|
+
const hasUserInfo = authority.includes("@");
|
|
220
|
+
const hasQueryOrFragmentDelimiter = pathAndSuffix.includes("?") || pathAndSuffix.includes("#");
|
|
221
|
+
const isLoopback = parsedBaseUrl.hostname === "localhost" || parsedBaseUrl.hostname === "127.0.0.1" || parsedBaseUrl.hostname === "[::1]" || parsedBaseUrl.hostname === "::1";
|
|
222
|
+
const hasAllowedProtocol = parsedBaseUrl.protocol === "https:" || parsedBaseUrl.protocol === "http:" && isLoopback;
|
|
223
|
+
if (!hasAllowedProtocol || hasUserInfo || hasQueryOrFragmentDelimiter || parsedBaseUrl.username !== "" || parsedBaseUrl.password !== "" || parsedBaseUrl.search !== "" || parsedBaseUrl.hash !== "") {
|
|
224
|
+
throw new TypeError(
|
|
225
|
+
"baseUrl must use HTTPS (or HTTP loopback) without userinfo, query, or fragment"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const audience = typeof options.audience === "string" ? options.audience.trim() : "";
|
|
229
|
+
if (!audience) {
|
|
230
|
+
throw new TypeError("audience must be a non-blank string");
|
|
231
|
+
}
|
|
232
|
+
this.baseUrl = parsedBaseUrl.toString().replace(/\/+$/, "");
|
|
233
|
+
this.audience = audience;
|
|
234
|
+
this.credential = options.credential ?? defaultTokenProvider();
|
|
235
|
+
this.fetch = options.fetch ?? globalThis.fetch;
|
|
236
|
+
this.randomUuid = options.randomUuid ?? (() => globalThis.crypto.randomUUID());
|
|
237
|
+
this.sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => {
|
|
238
|
+
setTimeout(resolve, milliseconds);
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Reports what this portal instance serves and what this caller may do.
|
|
243
|
+
*
|
|
244
|
+
* Instances are self-hosted and may run an older build than this client, so
|
|
245
|
+
* call this at startup: check `operations` for the routes you rely on and
|
|
246
|
+
* `grantedRoles` for the roles you need, and fail configuration loudly rather
|
|
247
|
+
* than discovering the gap on a customer's first redeem or trial start.
|
|
248
|
+
* Requires no application role.
|
|
249
|
+
*/
|
|
250
|
+
getCapabilities(appId) {
|
|
251
|
+
return this.request(
|
|
252
|
+
`/api/s2s/${S2S_API_VERSION}/apps/${encodeURIComponent(appId)}/capabilities`,
|
|
253
|
+
{},
|
|
254
|
+
200,
|
|
255
|
+
isCapabilitiesResponse
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
getSubscriptionStatus(appId, microsoftTenantId) {
|
|
259
|
+
return this.request(
|
|
260
|
+
`/api/s2s/v1/apps/${encodeURIComponent(appId)}/tenants/${encodeURIComponent(microsoftTenantId)}/subscription-status`,
|
|
261
|
+
{},
|
|
262
|
+
200,
|
|
263
|
+
isSubscriptionStatusResponse
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
getEntitlements(appId, microsoftTenantId) {
|
|
267
|
+
return this.request(
|
|
268
|
+
`/api/s2s/v1/apps/${encodeURIComponent(appId)}/tenants/${encodeURIComponent(microsoftTenantId)}/entitlements`,
|
|
269
|
+
{},
|
|
270
|
+
200,
|
|
271
|
+
isEntitlementsResponse
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
getLinkage(appId, microsoftTenantId) {
|
|
275
|
+
return this.request(
|
|
276
|
+
`/api/s2s/v1/apps/${encodeURIComponent(appId)}/tenants/${encodeURIComponent(microsoftTenantId)}/linkage`,
|
|
277
|
+
{},
|
|
278
|
+
200,
|
|
279
|
+
isLinkageResponse
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
redeemLicenseKey(appId, request) {
|
|
283
|
+
return this.request(
|
|
284
|
+
`/api/s2s/v1/apps/${encodeURIComponent(appId)}/claims/redeem-license-key`,
|
|
285
|
+
{
|
|
286
|
+
method: "POST",
|
|
287
|
+
body: request,
|
|
288
|
+
idempotencyKey: this.randomUuid()
|
|
289
|
+
},
|
|
290
|
+
200,
|
|
291
|
+
isRedeemLicenseKeyResponse
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
releaseClaim(appId, request) {
|
|
295
|
+
return this.request(
|
|
296
|
+
`/api/s2s/v1/apps/${encodeURIComponent(appId)}/claims/release`,
|
|
297
|
+
{
|
|
298
|
+
method: "POST",
|
|
299
|
+
body: request,
|
|
300
|
+
idempotencyKey: this.randomUuid()
|
|
301
|
+
},
|
|
302
|
+
200,
|
|
303
|
+
isReleaseClaimResponse
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
requestSubscriptionSync(appId, subscriptionId, request) {
|
|
307
|
+
return this.request(
|
|
308
|
+
`/api/s2s/v1/apps/${encodeURIComponent(appId)}/subscriptions/${encodeURIComponent(subscriptionId)}/sync`,
|
|
309
|
+
{
|
|
310
|
+
method: "POST",
|
|
311
|
+
body: request,
|
|
312
|
+
idempotencyKey: this.randomUuid()
|
|
313
|
+
},
|
|
314
|
+
202,
|
|
315
|
+
isSyncRequestResponse
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
startTrial(appId, microsoftTenantId, request = {}) {
|
|
319
|
+
return this.request(
|
|
320
|
+
`/api/s2s/v1/apps/${encodeURIComponent(appId)}/tenants/${encodeURIComponent(microsoftTenantId)}/trial`,
|
|
321
|
+
{
|
|
322
|
+
method: "POST",
|
|
323
|
+
body: request,
|
|
324
|
+
idempotencyKey: this.randomUuid()
|
|
325
|
+
},
|
|
326
|
+
200,
|
|
327
|
+
isStartTrialResponse
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
requestTenantSync(appId, microsoftTenantId, request) {
|
|
331
|
+
return this.request(
|
|
332
|
+
`/api/s2s/v1/apps/${encodeURIComponent(appId)}/tenants/${encodeURIComponent(microsoftTenantId)}/sync`,
|
|
333
|
+
{
|
|
334
|
+
method: "POST",
|
|
335
|
+
body: request,
|
|
336
|
+
idempotencyKey: this.randomUuid()
|
|
337
|
+
},
|
|
338
|
+
202,
|
|
339
|
+
isSyncRequestResponse
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
async request(path, options, expectedSuccessStatus, isSuccessResponse) {
|
|
343
|
+
const correlationId = this.randomUuid();
|
|
344
|
+
let accessToken;
|
|
345
|
+
try {
|
|
346
|
+
accessToken = await this.credential.getToken(`${this.audience}/.default`);
|
|
347
|
+
} catch {
|
|
348
|
+
throw new PortalS2sError({
|
|
349
|
+
code: "token_unavailable",
|
|
350
|
+
correlationId,
|
|
351
|
+
message: "Portal access token is unavailable"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (!accessToken) {
|
|
355
|
+
throw new PortalS2sError({
|
|
356
|
+
code: "token_unavailable",
|
|
357
|
+
correlationId,
|
|
358
|
+
message: "Portal access token is unavailable"
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const headers = {
|
|
362
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
363
|
+
"x-correlation-id": correlationId
|
|
364
|
+
};
|
|
365
|
+
if (options.idempotencyKey) {
|
|
366
|
+
headers["idempotency-key"] = options.idempotencyKey;
|
|
367
|
+
}
|
|
368
|
+
if (options.body !== void 0) {
|
|
369
|
+
headers["content-type"] = "application/json";
|
|
370
|
+
}
|
|
371
|
+
let serializedBody;
|
|
372
|
+
try {
|
|
373
|
+
serializedBody = options.body === void 0 ? void 0 : JSON.stringify(options.body);
|
|
374
|
+
} catch {
|
|
375
|
+
throw new PortalS2sError({
|
|
376
|
+
code: "request_serialization_failed",
|
|
377
|
+
correlationId,
|
|
378
|
+
message: "The portal request body could not be serialized."
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
let response;
|
|
382
|
+
try {
|
|
383
|
+
response = await fetchWithRetry(
|
|
384
|
+
() => this.fetch(`${this.baseUrl}${path}`, {
|
|
385
|
+
method: options.method ?? "GET",
|
|
386
|
+
headers,
|
|
387
|
+
body: serializedBody
|
|
388
|
+
}),
|
|
389
|
+
this.sleep
|
|
390
|
+
);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
if (!(error instanceof TransportRetryError)) {
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
throw new PortalS2sError({
|
|
396
|
+
code: "transport_error",
|
|
397
|
+
correlationId,
|
|
398
|
+
message: "Unable to reach the subscription portal."
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
let responseBody;
|
|
402
|
+
let responseBodyParsed = true;
|
|
403
|
+
try {
|
|
404
|
+
responseBody = await response.json();
|
|
405
|
+
} catch {
|
|
406
|
+
responseBodyParsed = false;
|
|
407
|
+
}
|
|
408
|
+
if (response.ok) {
|
|
409
|
+
if (responseBodyParsed && response.status === expectedSuccessStatus && isSuccessResponse(responseBody)) {
|
|
410
|
+
return responseBody;
|
|
411
|
+
}
|
|
412
|
+
throw this.unexpectedResponse(response, correlationId);
|
|
413
|
+
}
|
|
414
|
+
if (response.status === 404 && !isApiErrorEnvelope(responseBody)) {
|
|
415
|
+
throw new PortalS2sError({
|
|
416
|
+
status: response.status,
|
|
417
|
+
code: "endpoint_unavailable",
|
|
418
|
+
correlationId: response.headers.get("x-correlation-id") ?? correlationId,
|
|
419
|
+
details: { path, apiVersion: S2S_API_VERSION },
|
|
420
|
+
message: `The portal at ${this.baseUrl} does not serve ${path}. The instance is likely older than this client; check its version against the ${S2S_API_VERSION} contract.`
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (responseBodyParsed && isApiErrorEnvelope(responseBody)) {
|
|
424
|
+
throw new PortalS2sError({
|
|
425
|
+
status: response.status,
|
|
426
|
+
code: responseBody.error.code,
|
|
427
|
+
correlationId: responseBody.error.correlationId ?? response.headers.get("x-correlation-id") ?? correlationId,
|
|
428
|
+
details: responseBody.error.details,
|
|
429
|
+
message: responseBody.error.message
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
throw this.unexpectedResponse(response, correlationId);
|
|
433
|
+
}
|
|
434
|
+
unexpectedResponse(response, requestCorrelationId) {
|
|
435
|
+
return new PortalS2sError({
|
|
436
|
+
status: response.status,
|
|
437
|
+
code: "unexpected_response",
|
|
438
|
+
correlationId: response.headers.get("x-correlation-id") ?? requestCorrelationId,
|
|
439
|
+
message: "The subscription portal returned an unexpected response."
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
444
|
+
0 && (module.exports = {
|
|
445
|
+
PortalS2sClient,
|
|
446
|
+
PortalS2sError,
|
|
447
|
+
S2S_API_VERSION,
|
|
448
|
+
defaultTokenProvider
|
|
449
|
+
});
|