@salesforce/retail-react-app 10.2.0 → 10.3.0-nightly-20260814082538
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 +5 -0
- package/app/components/shopper-agent/auth-link-proxy.js +317 -0
- package/app/components/shopper-agent/auth-link-proxy.test.js +518 -0
- package/app/components/shopper-agent/index.jsx +368 -8
- package/app/components/shopper-agent/index.test.js +404 -2
- package/app/components/shopper-agent/salesforce-domain-allowlist.js +87 -0
- package/app/components/shopper-agent/salesforce-domain-allowlist.test.js +94 -0
- package/app/components/shopper-agent/token-bridge.js +33 -55
- package/app/components/shopper-agent/token-bridge.test.js +109 -42
- package/app/ssr.js +18 -1
- package/app/utils/shopper-agent-utils.js +37 -0
- package/app/utils/shopper-agent-utils.test.js +111 -0
- package/config/default.js +2 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## v10.3.0-dev (Aug 12, 2026)
|
|
2
|
+
- [Feature] Forward two backend-gating signals on the Commerce Client shopper-agent widget's `routingAttributes`: `clientVersion` (sourced from `cc_cdnVersion`, so the runtime can gate rich components on the client's capability version; omitted when unset) and `isCartMgmtSupported` (string `'true'`/`'false'`, default `'false'`, read from `cc_routingAttributes`). Brings the PWA commerce-client path to parity with the SFRA cartridge.
|
|
3
|
+
- [Feature] Enable shopper identity (customer context) for the Commerce Client (Cimulate) shopper-agent provider. Unlike MIAW, Commerce Client widgets do not expose `getAuthLinkKey`, so the browser extracts the Commerce Client JWT from the deployment-scoped `cim_af_ct_<orgId>_<embeddedServiceName>` key (`sessionStorage` is authoritative; `localStorage` is a compatibility fallback) and POSTs it to a new same-origin proxy `POST /api/agent/authlink` (`registerAuthLinkRoute`, mounted in `app/ssr.js`). The proxy calls SCRT2's `/iamessage/api/v2/authorization/authlink` with an `Authorization: Bearer <jwt>` header to obtain an `auth_link_key`; the SCRT2 origin is read server-side from `scrt2Url` in `COMMERCE_AGENT_SETTINGS` (a different host from Core's My Domain) and validated against a Salesforce domain allowlist, with an Origin/Referer CSRF check, to prevent SSRF. The `auth_link_key` is then forwarded to the existing Token Bridge to bind the conversation to the shopper's SLAS session. Linking is idempotent — deduped by `conversationId` + SLAS identity — and re-runs both on a new conversation (`onCimulateWidgetReady`) and on a SLAS identity transition (guest ↔ registered / account switch), gated on `my_domain` resolution. [#3943](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3943)
|
|
4
|
+
- [Bugfix] Fix the Shopper Agent Token Bridge proxy (`/api/agent/identity/bridge`) failing to reach Core in two cases: (1) same-origin POSTs were rejected with 403 `FORBIDDEN_ORIGIN` because the CSRF Origin check compared `URL.hostname` (no port) against `req.headers.host` (with port), so a non-standard dev port like `localhost:3000` never matched — now compares `URL.host`; (2) a scheme-less My Domain value (e.g. `orgfarm-1234.my.salesforce.com`) was rejected as 400 `UNTRUSTED_MYDOMAIN` because `new URL()` threw on it — the value is now normalized to an absolute `https://` origin before validation and fetch. The `AGENT_MYDOMAIN` environment variable configures the My Domain.
|
|
5
|
+
|
|
1
6
|
## v10.2.0 (Aug 12, 2026)
|
|
2
7
|
- [Bugfix] Fix the Data Cloud recommender catalog event field name: `personalizationContextId` → `personalizationContentId`. The official `@salesforce/cc-datacloud-typescript` SDK type and Salesforce docs both define `personalizationContentId` (Content); the `Context` spelling did not match, so the recommender UUID (`__recoUUID`) was sent under a key Data Cloud does not recognize and is expected to be dropped on ingest.
|
|
3
8
|
- [Feature] Add `cc_showFab` for the Commerce Client shopper-agent widget: when `'true'`, renders a floating action button at `cc_widgetPosition` that opens the agent panel and hides while it is open. Defaults to `'false'`.
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* -------------------------------------------------------------------------
|
|
9
|
+
* Auth Link Proxy — calls SCRT's `/iamessage/api/v2/authorization/authlink` from PWA Kit.
|
|
10
|
+
*
|
|
11
|
+
* This module provides a same-origin proxy for Commerce Client to retrieve auth link keys
|
|
12
|
+
* from SCRT, since `window.embeddedservice_bootstrap.userVerificationAPI.getAuthLinkKey`
|
|
13
|
+
* is not available for Commerce Client messaging widgets.
|
|
14
|
+
*
|
|
15
|
+
* This module is intentionally free of React (and of the
|
|
16
|
+
* `@salesforce/retail-react-app/...` self-referential imports used by the
|
|
17
|
+
* UI component) so it can be loaded by `app/ssr.js` under bare `babel-node`
|
|
18
|
+
* during local development. The React component (`./index.jsx`) re-uses
|
|
19
|
+
* the browser-side helper `callAuthLinkProxy` from here.
|
|
20
|
+
*
|
|
21
|
+
* Flow:
|
|
22
|
+
* 1. Browser: Commerce Client widget is ready (onCimulateWidgetReady event)
|
|
23
|
+
* - Extract Commerce Client JWT from the cim_af_ct_* storage key
|
|
24
|
+
* - Send commerce_client_jwt in request body to /api/agent/authlink
|
|
25
|
+
* 2. Server route (registerAuthLinkRoute, mounted in app/ssr.js):
|
|
26
|
+
* - Reads commerce_client_jwt from request body
|
|
27
|
+
* - Validates JWT is present
|
|
28
|
+
* - Reads scrt2Url from the COMMERCE_AGENT_SETTINGS environment variable
|
|
29
|
+
* - Validates scrt2Url against Salesforce domain allowlist (SSRF prevention)
|
|
30
|
+
* - Forwards to SCRT with `Authorization: Bearer <commerce_client_jwt>`
|
|
31
|
+
*
|
|
32
|
+
* NOTE: unlike the Token Bridge, this route does NOT use a siteId — the SCRT
|
|
33
|
+
* authlink endpoint authenticates with the Commerce Client JWT (Bearer) alone,
|
|
34
|
+
* so there is no x-site-id header on this request.
|
|
35
|
+
* 3. SCRT's response (auth_link_key) is forwarded to the browser
|
|
36
|
+
* 4. Browser then calls the existing Token Bridge proxy with auth_link_key
|
|
37
|
+
*
|
|
38
|
+
* IMPORTANT: This endpoint uses the Commerce Client JWT, NOT the SLAS token.
|
|
39
|
+
* The Commerce Client JWT is extracted from the cim_af_ct_* storage key
|
|
40
|
+
* which is set by the Commerce Client widget itself.
|
|
41
|
+
*
|
|
42
|
+
* SCRT2 host resolution: the auth link endpoint (/iamessage/*) is served by
|
|
43
|
+
* SCRT2 (*.salesforce-scrt.com), which is a DIFFERENT host from Core's My Domain
|
|
44
|
+
* (AGENT_MYDOMAIN, used by the Token Bridge). The SCRT2 origin is read from the
|
|
45
|
+
* scrt2Url field of the COMMERCE_AGENT_SETTINGS environment variable, then
|
|
46
|
+
* validated against a Salesforce domain allowlist (prevents SSRF).
|
|
47
|
+
* ------------------------------------------------------------------------- */
|
|
48
|
+
|
|
49
|
+
// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths
|
|
50
|
+
import {isTrustedSalesforceDomain, isTrustedSCRTDomain} from './salesforce-domain-allowlist.js'
|
|
51
|
+
|
|
52
|
+
export const AUTH_LINK_PROXY_PATH = '/api/agent/authlink'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Upstream timeout (ms) for the SCRT authlink call. Without a bound, a hung SCRT
|
|
56
|
+
* connection would tie up the request until the platform's socket timeout, so
|
|
57
|
+
* we abort well before that and return a 504 the caller can act on.
|
|
58
|
+
*/
|
|
59
|
+
const SCRT_FETCH_TIMEOUT_MS = 10000
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* SCRT auth link endpoint path — fixed at the **v2** IA-message API.
|
|
63
|
+
*
|
|
64
|
+
* The presented JWT must be minted for the same version; the Commerce Client
|
|
65
|
+
* (Cimulate) widget stores a v2 continuation token, which is what this endpoint
|
|
66
|
+
* expects. A version mismatch is rejected by SCRT with HTTP 401 error 900020
|
|
67
|
+
* (`JWT_VALID_NOT_AUTHORIZED_TO_API`) — the fix is to present a v2 token, NOT
|
|
68
|
+
* to change this path.
|
|
69
|
+
*/
|
|
70
|
+
const SCRT_AUTHLINK_PATH = '/iamessage/api/v2/authorization/authlink'
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract the SCRT2 origin from the COMMERCE_AGENT_SETTINGS environment variable.
|
|
74
|
+
*
|
|
75
|
+
* The auth link endpoint (`/iamessage/*`) is served by SCRT2, whose host
|
|
76
|
+
* (`*.salesforce-scrt.com`) is different from Core's My Domain (AGENT_MYDOMAIN).
|
|
77
|
+
* SCRT2's base URL is already provisioned to the storefront as the `scrt2Url`
|
|
78
|
+
* field of COMMERCE_AGENT_SETTINGS, so we read it from the same server-side
|
|
79
|
+
* source rather than introducing a new env var.
|
|
80
|
+
*
|
|
81
|
+
* Any trailing slash is stripped so it can be concatenated with an absolute
|
|
82
|
+
* path. Example: https://orgfarm-8fcc267362.test1.my.pc-rnd.salesforce-scrt.com
|
|
83
|
+
*
|
|
84
|
+
* @returns {string|null} - The SCRT2 origin (no trailing slash) or null if not found
|
|
85
|
+
*/
|
|
86
|
+
export function extractScrt2UrlFromEnv() {
|
|
87
|
+
const raw = process.env.COMMERCE_AGENT_SETTINGS
|
|
88
|
+
|
|
89
|
+
if (!raw) {
|
|
90
|
+
console.error('[auth-link-proxy] COMMERCE_AGENT_SETTINGS environment variable not set')
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let settings
|
|
95
|
+
try {
|
|
96
|
+
settings = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('[auth-link-proxy] COMMERCE_AGENT_SETTINGS is not valid JSON', {
|
|
99
|
+
message: err.message
|
|
100
|
+
})
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const scrt2Url = settings?.scrt2Url
|
|
105
|
+
if (!scrt2Url || typeof scrt2Url !== 'string' || !scrt2Url.trim()) {
|
|
106
|
+
console.error('[auth-link-proxy] scrt2Url not present in COMMERCE_AGENT_SETTINGS')
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Strip trailing slash(es) so `${scrt2Url}${SCRT_AUTHLINK_PATH}` is well-formed.
|
|
111
|
+
return scrt2Url.trim().replace(/\/+$/, '')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// isTrustedSalesforceDomain (Core) and isTrustedSCRTDomain (SCRT2) are shared with
|
|
115
|
+
// token-bridge.js via ./salesforce-domain-allowlist.js. This proxy uses the Core list
|
|
116
|
+
// for the CSRF Origin check (Storefront Preview iframe is served from Core) and the
|
|
117
|
+
// SCRT2 list for the upstream SSRF check on scrt2Url.
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Express handler for POST /api/agent/authlink.
|
|
121
|
+
*
|
|
122
|
+
* Retrieves auth_link_key from SCRT's /iamessage/api/v2/authorization/authlink
|
|
123
|
+
* endpoint. Uses Commerce Client JWT (from the cim_af_ct_* storage key) for
|
|
124
|
+
* authorization.
|
|
125
|
+
*
|
|
126
|
+
* Request:
|
|
127
|
+
* Body:
|
|
128
|
+
* {
|
|
129
|
+
* "commerce_client_jwt": "<jwt_from_cim_af_ct_storage>"
|
|
130
|
+
* }
|
|
131
|
+
*
|
|
132
|
+
* Response:
|
|
133
|
+
* The SCRT status code and body are forwarded verbatim.
|
|
134
|
+
* Success: { "auth_link_key": "..." }
|
|
135
|
+
* SCRT error: <scrt error body, forwarded unchanged>
|
|
136
|
+
* Pre-flight error (no SCRT call): { "error": "ERROR_CODE" }
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} req - Express request object
|
|
139
|
+
* @param {Object} res - Express response object
|
|
140
|
+
*/
|
|
141
|
+
export async function handleAuthLinkProxy(req, res) {
|
|
142
|
+
try {
|
|
143
|
+
const {commerce_client_jwt: commerceClientJWT} = req.body || {}
|
|
144
|
+
|
|
145
|
+
// Validate that Commerce Client JWT is provided
|
|
146
|
+
if (!commerceClientJWT || typeof commerceClientJWT !== 'string') {
|
|
147
|
+
console.error('[auth-link-proxy] Commerce Client JWT not provided')
|
|
148
|
+
return res.status(401).json({error: 'MISSING_COMMERCE_CLIENT_JWT'})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// CSRF protection: Validate Origin header for state-changing POST
|
|
152
|
+
const origin = req.headers.origin || req.headers.referer
|
|
153
|
+
if (origin) {
|
|
154
|
+
try {
|
|
155
|
+
const originUrl = new URL(origin)
|
|
156
|
+
// Compare host (hostname + port), NOT hostname alone: the Origin/
|
|
157
|
+
// Referer header carries the port for non-default ports (e.g. local
|
|
158
|
+
// dev at localhost:3001), and so does the HTTP Host header. Using
|
|
159
|
+
// `.hostname` would strip the port from one side only ("localhost"
|
|
160
|
+
// vs "localhost:3001") and reject a genuine same-origin request.
|
|
161
|
+
// `.host` also normalizes away default ports (:443/:80), so prod
|
|
162
|
+
// (https://…, no port in Host) still matches.
|
|
163
|
+
const originHost = originUrl.host.toLowerCase()
|
|
164
|
+
|
|
165
|
+
// Allow same-origin requests (PWA Kit storefront calling its own API)
|
|
166
|
+
const requestHost = req.headers.host?.toLowerCase()
|
|
167
|
+
const isSameOrigin = originHost === requestHost
|
|
168
|
+
|
|
169
|
+
// Allow trusted Salesforce origins (for Storefront Preview iframe)
|
|
170
|
+
const isTrustedSalesforceOrigin = isTrustedSalesforceDomain(origin)
|
|
171
|
+
|
|
172
|
+
if (!isSameOrigin && !isTrustedSalesforceOrigin) {
|
|
173
|
+
console.error('[auth-link-proxy] CSRF attempt blocked: untrusted Origin', {
|
|
174
|
+
origin,
|
|
175
|
+
requestHost
|
|
176
|
+
})
|
|
177
|
+
return res.status(403).json({error: 'FORBIDDEN_ORIGIN'})
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// Invalid Origin URL
|
|
181
|
+
console.error('[auth-link-proxy] Invalid Origin header', {origin})
|
|
182
|
+
return res.status(400).json({error: 'INVALID_ORIGIN'})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// If no Origin/Referer header, allow (same-origin POSTs from some browsers/tools)
|
|
186
|
+
|
|
187
|
+
// Resolve the SCRT2 origin from COMMERCE_AGENT_SETTINGS.scrt2Url.
|
|
188
|
+
// NOTE: this is intentionally NOT AGENT_MYDOMAIN — the auth link endpoint
|
|
189
|
+
// (/iamessage/*) lives on SCRT2 (*.salesforce-scrt.com), a different host
|
|
190
|
+
// from Core's My Domain. Pointing at AGENT_MYDOMAIN returns a 404 from Core.
|
|
191
|
+
const scrt2Url = extractScrt2UrlFromEnv()
|
|
192
|
+
|
|
193
|
+
if (!scrt2Url) {
|
|
194
|
+
console.error(
|
|
195
|
+
'[auth-link-proxy] SCRT2 URL is not configured. ' +
|
|
196
|
+
'Set scrt2Url in the COMMERCE_AGENT_SETTINGS environment variable.'
|
|
197
|
+
)
|
|
198
|
+
return res.status(500).json({error: 'SCRT2_URL_NOT_CONFIGURED'})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// SSRF prevention: validate scrt2Url against the SCRT2 allowlist
|
|
202
|
+
if (!isTrustedSCRTDomain(scrt2Url)) {
|
|
203
|
+
console.error(
|
|
204
|
+
'[auth-link-proxy] SSRF attempt blocked: scrt2Url is not a trusted Salesforce domain',
|
|
205
|
+
{scrt2Url}
|
|
206
|
+
)
|
|
207
|
+
return res.status(400).json({error: 'UNTRUSTED_SCRT2_URL'})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Call SCRT's auth link endpoint with the Commerce Client JWT.
|
|
211
|
+
// The path is fixed at v2 (see SCRT_AUTHLINK_PATH). The request is bounded
|
|
212
|
+
// by SCRT_FETCH_TIMEOUT_MS via an AbortController so a hung upstream does
|
|
213
|
+
// not tie up the connection indefinitely.
|
|
214
|
+
const scrtRequestUrl = `${scrt2Url}${SCRT_AUTHLINK_PATH}`
|
|
215
|
+
const controller = new AbortController()
|
|
216
|
+
const timeoutId = setTimeout(() => controller.abort(), SCRT_FETCH_TIMEOUT_MS)
|
|
217
|
+
let scrtResponse
|
|
218
|
+
let body = null
|
|
219
|
+
try {
|
|
220
|
+
scrtResponse = await fetch(scrtRequestUrl, {
|
|
221
|
+
method: 'GET',
|
|
222
|
+
headers: {
|
|
223
|
+
Authorization: `Bearer ${commerceClientJWT}`
|
|
224
|
+
},
|
|
225
|
+
signal: controller.signal
|
|
226
|
+
})
|
|
227
|
+
try {
|
|
228
|
+
body = await scrtResponse.json()
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (err?.name === 'AbortError') {
|
|
231
|
+
throw err
|
|
232
|
+
}
|
|
233
|
+
body = null
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
clearTimeout(timeoutId)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Forward the status and body from SCRT
|
|
240
|
+
if (!scrtResponse.ok) {
|
|
241
|
+
console.error('[auth-link-proxy] SCRT auth link request failed', {
|
|
242
|
+
status: scrtResponse.status,
|
|
243
|
+
scrtUrl: scrtRequestUrl,
|
|
244
|
+
body
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Forward SCRT's status and body to the caller unchanged. The SCRT2 URL
|
|
249
|
+
// that was called is intentionally NOT put in the response — it is only
|
|
250
|
+
// logged server-side (above) for diagnostics, never exposed to the browser.
|
|
251
|
+
return res.status(scrtResponse.status).json(body)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
// AbortError => the SCRT_FETCH_TIMEOUT_MS deadline fired. Surface it as a
|
|
254
|
+
// distinct 504 so the caller can tell a slow upstream from a real 500.
|
|
255
|
+
if (err?.name === 'AbortError') {
|
|
256
|
+
console.error('[auth-link-proxy] SCRT auth link request timed out', {
|
|
257
|
+
timeoutMs: SCRT_FETCH_TIMEOUT_MS
|
|
258
|
+
})
|
|
259
|
+
return res.status(504).json({error: 'SCRT_TIMEOUT'})
|
|
260
|
+
}
|
|
261
|
+
console.error('[auth-link-proxy] Unexpected error:', err)
|
|
262
|
+
return res.status(500).json({error: 'INTERNAL_ERROR'})
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Mount the Auth Link proxy on the given Express app (called from app/ssr.js).
|
|
268
|
+
*
|
|
269
|
+
* @param {Object} app - Express application instance
|
|
270
|
+
*/
|
|
271
|
+
export function registerAuthLinkRoute(app) {
|
|
272
|
+
app.post(AUTH_LINK_PROXY_PATH, handleAuthLinkProxy)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Browser helper: POSTs to the same-origin proxy and returns SCRT's auth_link_key.
|
|
277
|
+
*
|
|
278
|
+
* Uses the Commerce Client JWT (extracted from the cim_af_ct_* storage key)
|
|
279
|
+
* to authenticate with SCRT's authlink endpoint.
|
|
280
|
+
*
|
|
281
|
+
* The SCRT2 origin is derived server-side from scrt2Url in the
|
|
282
|
+
* COMMERCE_AGENT_SETTINGS environment variable.
|
|
283
|
+
*
|
|
284
|
+
* @param {Object} options - Request options
|
|
285
|
+
* @param {string} options.commerceClientJWT - Commerce Client JWT from the cim_af_ct_* storage key (required)
|
|
286
|
+
* @returns {Promise<Object>} Promise that resolves to { auth_link_key: "..." }
|
|
287
|
+
*/
|
|
288
|
+
export const callAuthLinkProxy = async ({commerceClientJWT}) => {
|
|
289
|
+
const requestBody = {
|
|
290
|
+
commerce_client_jwt: commerceClientJWT
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// No x-site-id header: the SCRT authlink endpoint authenticates with the
|
|
294
|
+
// Commerce Client JWT (Bearer) alone. Unlike the Token Bridge — which needs
|
|
295
|
+
// the siteId to resolve HttpOnly cookie names (cc-at_{siteId}) — authlink has
|
|
296
|
+
// nothing to key off the siteId, so sending it would be dead weight.
|
|
297
|
+
const res = await fetch(AUTH_LINK_PROXY_PATH, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: {
|
|
300
|
+
'Content-Type': 'application/json'
|
|
301
|
+
},
|
|
302
|
+
body: JSON.stringify(requestBody)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
let responseBody = null
|
|
306
|
+
try {
|
|
307
|
+
responseBody = await res.json()
|
|
308
|
+
} catch {
|
|
309
|
+
responseBody = null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!res.ok) {
|
|
313
|
+
throw new Error(`Auth link proxy failed: ${responseBody?.error || `HTTP_${res.status}`}`)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return responseBody
|
|
317
|
+
}
|