@meshconnect/web-link-sdk 3.12.0 → 3.14.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/Link.js CHANGED
@@ -49,6 +49,8 @@ import { addPopup, buildIframeAllowPolicy, iframeId, removePopup } from './utils
49
49
  import { isLinkEventTypeKey } from './utils/event-types';
50
50
  import { sdkSpecs } from './utils/sdk-specs';
51
51
  import { appendQueryParam } from './utils/url';
52
+ import { JIT_REQUEST_MESSAGE_TYPE, SESSION_NONCE_PARAM } from './utils/backup';
53
+ import { createBackupFlow } from './backupFlow';
52
54
  import { BridgeParent } from '@meshconnect/uwc-bridge-parent';
53
55
  import { createPrewarmIframe, removePrewarmIframe } from './utils/prewarm';
54
56
  var currentOptions;
@@ -66,85 +68,147 @@ function sendMessageToIframe(message) {
66
68
  console.warn("Mesh SDK: Failed to deliver ".concat(message.type, " message to the iframe - no iframe element found"));
67
69
  return;
68
70
  }
69
- if (!linkTokenOrigin) {
71
+ // Tier 2 is a sandboxed, opaque-origin iframe (no pinnable origin) — post with a
72
+ // '*' target; the widget is our own bundled content and authenticates us in turn.
73
+ var target = backup.isTier2() ? '*' : linkTokenOrigin;
74
+ if (!target) {
70
75
  console.warn("Mesh SDK: Failed to deliver ".concat(message.type, " message to the iframe - no link token origin found"));
71
76
  return;
72
77
  }
73
78
  try {
74
- (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message, linkTokenOrigin);
79
+ (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message, target);
75
80
  }
76
81
  catch (e) {
77
82
  console.error("Mesh SDK: Failed to deliver ".concat(message.type, " message to the iframe"));
78
83
  console.error(e);
79
84
  }
80
85
  }
86
+ // The SDK-backup (deposit-only fallback) flow lives in its own module. It reads and
87
+ // writes the shared Link state through this host bridge; the message handlers below
88
+ // query it for backup state (`isTier2`/`getActiveFlow`/`isFromActiveWidget`) and
89
+ // delegate the backup branches to it (`handleJitRequest`/`completeReadyHandshake`).
90
+ var backupHost = {
91
+ getOptions: function () { return currentOptions; },
92
+ setOptions: function (options) {
93
+ currentOptions = options;
94
+ },
95
+ getLinkTokenOrigin: function () { return linkTokenOrigin; },
96
+ setLinkTokenOrigin: function (origin) {
97
+ linkTokenOrigin = origin;
98
+ },
99
+ setTargetOrigin: function (origin) {
100
+ targetOrigin = origin;
101
+ },
102
+ iframeElement: iframeElement,
103
+ setCurrentIframeId: function (id) {
104
+ currentIframeId = id;
105
+ },
106
+ getBridgeParent: function () { return bridgeParent; },
107
+ setBridgeParent: function (bridge) {
108
+ bridgeParent = bridge;
109
+ },
110
+ addMessageListener: function () { return window.addEventListener('message', eventsListener); },
111
+ removeMessageListener: function () {
112
+ return window.removeEventListener('message', eventsListener);
113
+ },
114
+ sendMessageToIframe: sendMessageToIframe
115
+ };
116
+ var backup = createBackupFlow(backupHost);
81
117
  function handleLinkEvent(event) {
82
118
  return __awaiter(this, void 0, void 0, function () {
83
- var payload, payload, payload, payload;
84
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
85
- return __generator(this, function (_l) {
86
- switch (event.data.type) {
87
- case 'brokerageAccountAccessToken': {
88
- payload = {
89
- accessToken: event.data.payload
90
- };
91
- (_a = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _a === void 0 ? void 0 : _a.call(currentOptions, {
92
- type: 'integrationConnected',
93
- payload: payload
94
- });
95
- (_b = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onIntegrationConnected) === null || _b === void 0 ? void 0 : _b.call(currentOptions, payload);
96
- break;
97
- }
98
- case 'delayedAuthentication': {
99
- payload = {
100
- delayedAuth: event.data.payload
101
- };
102
- (_c = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _c === void 0 ? void 0 : _c.call(currentOptions, {
103
- type: 'integrationConnected',
104
- payload: payload
105
- });
106
- (_d = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onIntegrationConnected) === null || _d === void 0 ? void 0 : _d.call(currentOptions, payload);
107
- break;
108
- }
109
- case 'transferFinished': {
110
- payload = event.data.payload;
111
- (_e = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _e === void 0 ? void 0 : _e.call(currentOptions, {
112
- type: 'transferCompleted',
113
- payload: payload
114
- });
115
- (_f = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onTransferFinished) === null || _f === void 0 ? void 0 : _f.call(currentOptions, payload);
116
- break;
117
- }
118
- case 'close':
119
- case 'done': {
120
- payload = (_g = event.data) === null || _g === void 0 ? void 0 : _g.payload;
121
- (_h = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onExit) === null || _h === void 0 ? void 0 : _h.call(currentOptions, payload === null || payload === void 0 ? void 0 : payload.errorMessage, payload);
122
- bridgeParent === null || bridgeParent === void 0 ? void 0 : bridgeParent.destroy();
123
- removePopup();
124
- break;
125
- }
126
- case 'loaded': {
127
- sendMessageToIframe({
128
- type: 'meshSDKSpecs',
129
- payload: __assign({}, sdkSpecs)
130
- });
131
- if (currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.accessTokens) {
132
- sendMessageToIframe({
133
- type: 'frontAccessTokens',
134
- payload: currentOptions.accessTokens
135
- });
136
- }
137
- (_j = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _j === void 0 ? void 0 : _j.call(currentOptions, { type: 'pageLoaded' });
138
- break;
139
- }
140
- default: {
141
- if (isLinkEventTypeKey(event.data.type)) {
142
- (_k = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _k === void 0 ? void 0 : _k.call(currentOptions, event.data);
119
+ var payload, payload, payload, payload, onExit, linkEvent;
120
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
121
+ return __generator(this, function (_k) {
122
+ switch (_k.label) {
123
+ case 0:
124
+ if (!(event.data.type === JIT_REQUEST_MESSAGE_TYPE)) return [3 /*break*/, 2];
125
+ return [4 /*yield*/, backup.handleJitRequest(event)];
126
+ case 1:
127
+ _k.sent();
128
+ return [2 /*return*/];
129
+ case 2:
130
+ switch (event.data.type) {
131
+ case 'brokerageAccountAccessToken': {
132
+ payload = {
133
+ accessToken: event.data.payload
134
+ };
135
+ (_a = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _a === void 0 ? void 0 : _a.call(currentOptions, {
136
+ type: 'integrationConnected',
137
+ payload: payload
138
+ });
139
+ (_b = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onIntegrationConnected) === null || _b === void 0 ? void 0 : _b.call(currentOptions, payload);
140
+ break;
141
+ }
142
+ case 'delayedAuthentication': {
143
+ payload = {
144
+ delayedAuth: event.data.payload
145
+ };
146
+ (_c = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _c === void 0 ? void 0 : _c.call(currentOptions, {
147
+ type: 'integrationConnected',
148
+ payload: payload
149
+ });
150
+ (_d = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onIntegrationConnected) === null || _d === void 0 ? void 0 : _d.call(currentOptions, payload);
151
+ break;
152
+ }
153
+ case 'transferFinished': {
154
+ payload = event.data.payload;
155
+ (_e = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _e === void 0 ? void 0 : _e.call(currentOptions, {
156
+ type: 'transferCompleted',
157
+ payload: payload
158
+ });
159
+ (_f = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onTransferFinished) === null || _f === void 0 ? void 0 : _f.call(currentOptions, payload);
160
+ break;
161
+ }
162
+ case 'close':
163
+ case 'done': {
164
+ payload = (_g = event.data) === null || _g === void 0 ? void 0 : _g.payload;
165
+ onExit = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onExit;
166
+ window.removeEventListener('message', eventsListener);
167
+ bridgeParent === null || bridgeParent === void 0 ? void 0 : bridgeParent.destroy();
168
+ removePopup();
169
+ backup.reset();
170
+ onExit === null || onExit === void 0 ? void 0 : onExit(payload === null || payload === void 0 ? void 0 : payload.errorMessage, payload);
171
+ break;
172
+ }
173
+ case 'loaded': {
174
+ sendMessageToIframe({
175
+ type: 'meshSDKSpecs',
176
+ payload: __assign({}, sdkSpecs)
177
+ });
178
+ // Never forward integration access tokens to the backup widget: it is a
179
+ // deposit-only flow served from an independent origin (no shared failure
180
+ // domain with Mesh) and has no use for them — forwarding would leak the
181
+ // user's credentials cross-origin. Gate on the POSITIVE `activeFlow ===
182
+ // 'primary'` (not `!backupSession`): after an aborted re-open the stale
183
+ // backup iframe can still fire `loaded` with the session cleared, and only
184
+ // this positive check withholds the tokens in that dead window.
185
+ if ((currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.accessTokens) &&
186
+ backup.getActiveFlow() === 'primary') {
187
+ sendMessageToIframe({
188
+ type: 'frontAccessTokens',
189
+ payload: currentOptions.accessTokens
190
+ });
191
+ }
192
+ // The backup flow completes its ready handshake + config delivery here, but
193
+ // only for a `loaded` from its own widget window (no-op otherwise).
194
+ backup.completeReadyHandshake(event);
195
+ (_h = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _h === void 0 ? void 0 : _h.call(currentOptions, { type: 'pageLoaded' });
196
+ break;
197
+ }
198
+ default: {
199
+ // `backupTierChanged` is generated by the SDK itself (backupFlow) — never
200
+ // accept it from a widget, which could otherwise forge tier transitions.
201
+ if (isLinkEventTypeKey(event.data.type) &&
202
+ event.data.type !== 'backupTierChanged') {
203
+ linkEvent = __assign({}, event.data);
204
+ delete linkEvent[SESSION_NONCE_PARAM];
205
+ (_j = currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.onEvent) === null || _j === void 0 ? void 0 : _j.call(currentOptions, linkEvent);
206
+ }
207
+ break;
208
+ }
143
209
  }
144
- break;
145
- }
210
+ return [2 /*return*/];
146
211
  }
147
- return [2 /*return*/];
148
212
  });
149
213
  });
150
214
  }
@@ -153,14 +217,48 @@ function eventsListener(event) {
153
217
  return __generator(this, function (_a) {
154
218
  switch (_a.label) {
155
219
  case 0:
156
- if (!(event.origin !== targetOrigin && event.origin !== linkTokenOrigin)) return [3 /*break*/, 1];
157
- console.warn('Received message from untrusted origin:', event.origin);
220
+ if (!backup.isTier2()) return [3 /*break*/, 4];
221
+ if (!(backup.isFromActiveWidget(event) && event.origin === 'null')) return [3 /*break*/, 2];
222
+ return [4 /*yield*/, handleLinkEvent(event)];
223
+ case 1:
224
+ _a.sent();
158
225
  return [3 /*break*/, 3];
159
- case 1: return [4 /*yield*/, handleLinkEvent(event)];
160
226
  case 2:
161
- _a.sent();
227
+ console.warn('Ignored backup Tier-2 message from an unexpected source');
162
228
  _a.label = 3;
163
229
  case 3: return [2 /*return*/];
230
+ case 4:
231
+ if (event.origin !== targetOrigin && event.origin !== linkTokenOrigin) {
232
+ console.warn('Received message from untrusted origin:', event.origin);
233
+ return [2 /*return*/];
234
+ }
235
+ // No flow is live. An aborted re-open (`openLink` with a bad token, or
236
+ // `openLinkBackup` with a bad origin) calls `backup.reset()` — clearing the
237
+ // active-flow marker — but returns early WITHOUT removing the previous iframe or
238
+ // this listener. A late `close`/`transferFinished` from that stale iframe still
239
+ // passes the retained origin check, so gate on the POSITIVE active flow: drop
240
+ // everything while no flow is active.
241
+ if (backup.getActiveFlow() === null) {
242
+ console.warn('Ignored message: no active Link flow');
243
+ return [2 /*return*/];
244
+ }
245
+ // During a BACKUP flow, additionally require the message to come from the active
246
+ // widget window — not just the backup ORIGIN. In embedded mode a reopen into a
247
+ // different iframe leaves the previous one alive on the SAME backup origin, so an
248
+ // origin-only check would let a late `close`/`done`/`transferFinished` from the
249
+ // stale iframe tear down or report a transfer for the current session. A reopen
250
+ // into the SAME embedded iframe keeps its window AND origin, so the message must
251
+ // also echo this open's session nonce (`isFromActiveWidget`). (Primary flow has
252
+ // no backup widget window, so it is gated by origin only, as before.)
253
+ if (backup.getActiveFlow() === 'backup' &&
254
+ !backup.isFromActiveWidget(event)) {
255
+ console.warn('Ignored backup message from an unexpected source');
256
+ return [2 /*return*/];
257
+ }
258
+ return [4 /*yield*/, handleLinkEvent(event)];
259
+ case 5:
260
+ _a.sent();
261
+ return [2 /*return*/];
164
262
  }
165
263
  });
166
264
  });
@@ -169,6 +267,9 @@ export var createLink = function (options) {
169
267
  var openLink = function (linkToken, customIframeId) {
170
268
  var _a, _b, _c;
171
269
  removePrewarmIframe();
270
+ // Clear any prior backup session so its config can't leak into the token
271
+ // path and its pending tier timer can't fire against this iframe.
272
+ backup.reset();
172
273
  if (!linkToken) {
173
274
  (_a = options === null || options === void 0 ? void 0 : options.onExit) === null || _a === void 0 ? void 0 : _a.call(options, 'Invalid link token!');
174
275
  return;
@@ -186,6 +287,9 @@ export var createLink = function (options) {
186
287
  (_c = options === null || options === void 0 ? void 0 : options.onExit) === null || _c === void 0 ? void 0 : _c.call(options, 'Invalid link token!');
187
288
  return;
188
289
  }
290
+ // Committed to the primary (token) flow — safe to forward access tokens on the
291
+ // `loaded` handshake. (`backup.reset()` above cleared any prior flow.)
292
+ backup.setPrimaryFlow();
189
293
  linkUrl = addLanguage(linkUrl, currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.language);
190
294
  linkUrl = addDisplayFiatCurrency(linkUrl, currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.displayFiatCurrency);
191
295
  linkUrl = addTheme(linkUrl, currentOptions === null || currentOptions === void 0 ? void 0 : currentOptions.theme);
@@ -196,6 +300,11 @@ export var createLink = function (options) {
196
300
  var iframe_1 = document.getElementById(customIframeId);
197
301
  if (iframe_1) {
198
302
  iframe_1.allow = buildIframeAllowPolicy(linkTokenOrigin);
303
+ // Clear any leftover Tier-2 state from a prior backup session on this
304
+ // reused iframe: `srcdoc` (takes precedence over `src`) and the `sandbox`
305
+ // attribute (would otherwise keep it opaque-origin).
306
+ iframe_1.removeAttribute('srcdoc');
307
+ iframe_1.removeAttribute('sandbox');
199
308
  iframe_1.src = linkUrl;
200
309
  currentIframeId = customIframeId;
201
310
  }
@@ -214,11 +323,15 @@ export var createLink = function (options) {
214
323
  bridgeParent = new BridgeParent(iframe);
215
324
  }
216
325
  };
326
+ var openLinkBackup = function (session, backupOptions) {
327
+ backup.open(options, session, backupOptions);
328
+ };
217
329
  var closeLink = function () {
218
330
  var _a;
219
331
  bridgeParent === null || bridgeParent === void 0 ? void 0 : bridgeParent.destroy();
220
332
  removePopup();
221
333
  window.removeEventListener('message', eventsListener);
334
+ backup.reset();
222
335
  (_a = options.onExit) === null || _a === void 0 ? void 0 : _a.call(options);
223
336
  };
224
337
  var closeLinkRequested = function () {
@@ -231,6 +344,7 @@ export var createLink = function (options) {
231
344
  };
232
345
  return {
233
346
  openLink: openLink,
347
+ openLinkBackup: openLinkBackup,
234
348
  closeLink: closeLink,
235
349
  closeLinkRequested: closeLinkRequested
236
350
  };
package/README.md CHANGED
@@ -94,26 +94,79 @@ After successfull authentication on the Link session, the popup will be closed a
94
94
 
95
95
  #### `createLink` arguments
96
96
 
97
- | key | type | description |
98
- | ------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------ |
99
- | `onIntegrationConnected` | `(payload: LinkPayload) => void` | Callback called when users connects their accounts |
100
- | `onExit` | `((error?: string \| undefined) => void) \| undefined` | Called if connection not happened |
101
- | `onTransferFinished` | `(payload: TransferFinishedPayload) => void` | Callback called when a crypto transfer is executed |
102
- | `onEvent` | `(payload: LinkEventType) => void` | A callback function that is called when various events occur within the Front iframe |
103
- | `accessTokens` | `IntegrationAccessToken[]` | An array of integration access tokens |
104
- | `language` | `'en' \| undefined` | Link UI language |
105
- | `displayFiatCurrency` | `'USD' \| undefined` | A fiat currency to display fiat equivalent of a crypto amount |
106
- | `theme` | `'dark' \| 'light' \| 'system' \| undefined` | Color theme of Link UI interface |
107
- | `renderType` | `'overlay' \| 'embedded' \| undefined` | `'overlay'` (default) renders a full-screen popup; `'embedded'` renders inside a client-supplied iframe (requires `customIframeId` in `openLink`) |
97
+ | key | type | description |
98
+ | ------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | `onIntegrationConnected` | `((payload: LinkPayload) => void) \| undefined` | Callback called when users connects their accounts |
100
+ | `onExit` | `((error?: string \| undefined) => void) \| undefined` | Called if connection not happened |
101
+ | `onTransferFinished` | `((payload: TransferFinishedPayload) => void) \| undefined` | Callback called when a crypto transfer is executed |
102
+ | `onEvent` | `((payload: LinkEventType) => void) \| undefined` | A callback function that is called when various events occur within the Front iframe |
103
+ | `accessTokens` | `IntegrationAccessToken[]` | An array of integration access tokens |
104
+ | `language` | `'en' \| undefined` | Link UI language |
105
+ | `displayFiatCurrency` | `'USD' \| undefined` | A fiat currency to display fiat equivalent of a crypto amount |
106
+ | `theme` | `'dark' \| 'light' \| 'system' \| undefined` | Color theme of Link UI interface |
107
+ | `renderType` | `'overlay' \| 'embedded' \| undefined` | `'overlay'` (default) renders a full-screen popup; `'embedded'` renders inside a client-supplied iframe (requires `customIframeId` in `openLink`) |
108
+
109
+ All callbacks are optional.
110
+
111
+ #### Withdrawal events
112
+
113
+ When a user confirms a withdrawal, `onEvent` receives a `withdrawalRequested` event, then Link closes and calls `onExit`.
114
+ With `renderType: 'embedded'`, the SDK does not remove your iframe: hide it in `onExit`.
115
+ Keep the `transferId` and continue the withdrawal once Link has closed, for example with your own 2FA prompt.
116
+ Treat the event, not `onExit`, as confirmation of the withdrawal.
117
+ The payload carries no address or amount: read the transfer details from the webhook or the transfer API.
118
+
119
+ ```ts
120
+ onEvent: event => {
121
+ if (event.type === 'withdrawalRequested') {
122
+ const { transferId, status } = event.payload // 'pending' or 'success'; treat any other value as pending
123
+ }
124
+ }
125
+ ```
108
126
 
109
127
  #### `createLink` return value
110
128
 
111
129
  | key | type | description |
112
130
  | -------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
113
131
  | `openLink` | `(linkToken: string, customIframeId?: string) => void` | Opens the Link UI popup. Optionally targets an existing iframe by ID instead of creating a new popup |
132
+ | `openLinkBackup` | `(backupConfig: MeshBackupConfig, options?: MeshBackupOptions) => void` | Opens the deposit-only **backup flow** when the Mesh API is unavailable (see below) |
114
133
  | `closeLink` | `() => void` | Closes the Link UI popup immediately |
115
134
  | `closeLinkRequested` | `() => void` | Requests graceful close in `embedded` mode (sends `closeRequested` to iframe); closes immediately otherwise |
116
135
 
136
+ ### Backup / outage flow
137
+
138
+ If a `linktoken` request fails in a way that indicates a Mesh outage (network/DNS error, timeout, HTTP 5xx or 429), you can open a **deposit-only backup flow** instead of the normal flow. It runs select-token → select-network → QR / copy-address from Mesh infrastructure on a separate domain, and resolves deposit addresses either from static config or through callbacks you provide.
139
+
140
+ ```ts
141
+ const link = createLink({
142
+ onIntegrationConnected, onTransferFinished, onEvent, onExit,
143
+
144
+ // JIT callbacks — required only if any destination omits `address`. They run in
145
+ // YOUR app with YOUR session; only (symbol, networkId) and the resolved address
146
+ // cross the bridge. `fetch` resolves on 4xx/5xx, so throw on a non-OK response to
147
+ // fail closed.
148
+ onAddressInit: async (symbol, networkId) => {
149
+ const res = await api.post('/wallets/addresses/assign', { currency: symbol, network_id: networkId })
150
+ if (!res.ok) throw new Error('address init failed')
151
+ },
152
+ onStatusPoll: async (symbol, networkId) => {
153
+ const res = await api.get('/wallets/deposit_addresses', { currency: symbol })
154
+ if (!res.ok) throw new Error('status poll failed')
155
+ const m = (await res.json()).deposit_addresses.find(a => a.network_id === networkId && a.address)
156
+ return m ? { status: 'ready', address: m.address, addressTag: m.address_tag } : { status: 'pending' }
157
+ }
158
+ })
159
+
160
+ // Normal path:
161
+ link.openLink(linkToken)
162
+ // Outage path — same handlers, no link token:
163
+ link.openLinkBackup(backupConfig)
164
+ ```
165
+
166
+ Callbacks (`onAddressInit`, `onStatusPoll`) are passed to `createLink`. `onAddressInit(symbol, networkId)` kicks off generation (its return is ignored; a throw/reject is a failure). `onStatusPoll(symbol, networkId)` returns `Promise<{ status: 'pending' | 'ready' | 'failed'; address?; addressTag? }>` and is polled until `ready`/`failed`; it must be idempotent per `(symbol, networkId)`. Supply both only if any `backupConfig.destinations[]` entry omits `address`.
167
+
168
+ > **⚠️ Host CSP requirements (web only).** Serve the embedding page over **https**. Your Content-Security-Policy's `frame-src`/`child-src` (or `default-src`) must allow **the backup widget origin** (`https://backup.meshconnect.com` by default) and **`blob:`** — the bundled last-resort fallback renders as a sandboxed `blob:` iframe, so a CSP that pins `frame-src` to the primary Mesh origin will block it and the fallback will fail. (React Native is not affected — it uses a native WebView, not an iframe.)
169
+
117
170
  ### Using tokens
118
171
 
119
172
  You can use broker tokens to perform requests to get current balance, assets and execute transactions. Full API reference can be found [here](https://integration-api.meshconnect.com/apireference).
@@ -134,6 +187,9 @@ TypeScript definitions for `@meshconnect/web-link-sdk` are built into the npm pa
134
187
  | `BrokerType` | Union of supported broker/integration type strings (re-exported from `@meshconnect/node-api`) |
135
188
  | `LinkOptions` | Full options object passed to `createLink` |
136
189
  | `Link` | Return type of `createLink` |
190
+ | `MeshBackupConfig` | Config passed to `openLinkBackup` (clientId, userId, destinations, preselectedSymbol) |
191
+ | `MeshBackupOptions` | Options for `openLinkBackup` (e.g. `customIframeId`, `widgetOrigin`) |
192
+ | `MeshBackupStatusResult` | Return type of the `onStatusPoll` callback |
137
193
  | `AccountToken` | Account token within `AccessTokenPayload` |
138
194
  | `Account` | Account details within `AccountToken` |
139
195
  | `BrandInfo` | Integration brand/logo info |
@@ -0,0 +1,2 @@
1
+ /** Self-contained Tier-2 offline widget, loaded into a sandboxed blob: iframe. */
2
+ export declare const OFFLINE_WIDGET_HTML: string;