@oxyhq/core 3.14.0 → 3.16.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/mixins/OxyServices.applications.js +43 -0
- package/dist/cjs/mixins/OxyServices.assets.js +14 -35
- package/dist/cjs/mixins/OxyServices.auth.js +12 -1
- package/dist/cjs/mixins/OxyServices.links.js +68 -0
- package/dist/cjs/mixins/OxyServices.nodes.js +175 -0
- package/dist/cjs/mixins/index.js +8 -0
- package/dist/cjs/utils/ssoBounce.js +48 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.applications.js +43 -0
- package/dist/esm/mixins/OxyServices.assets.js +14 -35
- package/dist/esm/mixins/OxyServices.auth.js +12 -1
- package/dist/esm/mixins/OxyServices.links.js +65 -0
- package/dist/esm/mixins/OxyServices.nodes.js +172 -0
- package/dist/esm/mixins/index.js +8 -0
- package/dist/esm/utils/ssoBounce.js +46 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/mixins/OxyServices.applications.d.ts +51 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +8 -29
- package/dist/types/mixins/OxyServices.auth.d.ts +8 -0
- package/dist/types/mixins/OxyServices.links.d.ts +102 -0
- package/dist/types/mixins/OxyServices.nodes.d.ts +242 -0
- package/dist/types/mixins/index.d.ts +3 -1
- package/dist/types/utils/ssoBounce.d.ts +61 -0
- package/package.json +1 -1
- package/src/index.ts +5 -0
- package/src/mixins/OxyServices.applications.ts +79 -0
- package/src/mixins/OxyServices.assets.ts +14 -44
- package/src/mixins/OxyServices.auth.ts +35 -1
- package/src/mixins/OxyServices.links.ts +103 -0
- package/src/mixins/OxyServices.nodes.ts +348 -0
- package/src/mixins/__tests__/OxyServices.links.test.ts +154 -0
- package/src/mixins/__tests__/OxyServices.nodes.test.ts +341 -0
- package/src/mixins/__tests__/commonsSignIn.test.ts +41 -16
- package/src/mixins/__tests__/connectedApps.test.ts +123 -0
- package/src/mixins/__tests__/getFileDownloadUrl.test.ts +10 -24
- package/src/mixins/index.ts +10 -0
- package/src/utils/__tests__/ssoBounce.test.ts +28 -0
- package/src/utils/ssoBounce.ts +69 -0
package/src/index.ts
CHANGED
|
@@ -98,6 +98,7 @@ export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
|
98
98
|
export type {
|
|
99
99
|
Application,
|
|
100
100
|
PublicApplication,
|
|
101
|
+
ConnectedApp,
|
|
101
102
|
ApplicationMember,
|
|
102
103
|
ApplicationCredential,
|
|
103
104
|
ApplicationRole,
|
|
@@ -210,6 +211,7 @@ export type {
|
|
|
210
211
|
IssueCredentialInput,
|
|
211
212
|
RevokeCredentialResult,
|
|
212
213
|
} from './mixins/OxyServices.civic';
|
|
214
|
+
export type { UserNodeStatus, UserNodeMode, UserNodeController, UserNodeLivenessStatus, RegisterNodeInput, RemoveNodeResult } from './mixins/OxyServices.nodes';
|
|
213
215
|
|
|
214
216
|
// ---------------------------------------------------------------------------
|
|
215
217
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
@@ -532,13 +534,16 @@ export {
|
|
|
532
534
|
ssoDestKey,
|
|
533
535
|
ssoNoSessionKey,
|
|
534
536
|
ssoAttemptedKey,
|
|
537
|
+
ssoPriorSessionKey,
|
|
535
538
|
ssoCallbackBootstrapKey,
|
|
536
539
|
ssoNavigate,
|
|
537
540
|
getSsoCallbackBootstrapScript,
|
|
538
541
|
buildSsoBounceUrl,
|
|
539
542
|
isCentralIdPOrigin,
|
|
540
543
|
guardActive,
|
|
544
|
+
allowSsoBounce,
|
|
541
545
|
} from './utils/ssoBounce';
|
|
546
|
+
export type { SsoBounceGate } from './utils/ssoBounce';
|
|
542
547
|
|
|
543
548
|
export { runColdBoot } from './utils/coldBoot';
|
|
544
549
|
export type {
|
|
@@ -164,6 +164,32 @@ export interface PublicApplication {
|
|
|
164
164
|
developerName?: string;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* A connected (OAuth-authorized) application from the current user's point of
|
|
169
|
+
* view: an application the user has granted access to via the consent flow.
|
|
170
|
+
*
|
|
171
|
+
* Returned by `GET /auth/grants` and rendered in the user-facing "Connected
|
|
172
|
+
* apps" management surface. Keyed by `applicationId` (the application's Mongo
|
|
173
|
+
* `_id`) rather than a credential/client id, so the grant — and a subsequent
|
|
174
|
+
* {@link OxyServicesApplicationsMixin.revokeAppGrant} — survive credential
|
|
175
|
+
* rotation. This is a display shape: it carries the application's name/logo and
|
|
176
|
+
* the granted scopes, never any membership or credential material.
|
|
177
|
+
*/
|
|
178
|
+
export interface ConnectedApp {
|
|
179
|
+
/** The connected application's Mongo `_id`. Use this to revoke the grant. */
|
|
180
|
+
applicationId: string;
|
|
181
|
+
/** Human-readable application name shown to the user. */
|
|
182
|
+
name: string;
|
|
183
|
+
/** Optional logo URL for the application. */
|
|
184
|
+
logoUrl?: string;
|
|
185
|
+
/** OAuth scopes the user has granted to the application. */
|
|
186
|
+
scopes: string[];
|
|
187
|
+
/** ISO timestamp of when the user first authorized the application. */
|
|
188
|
+
firstGrantedAt: string;
|
|
189
|
+
/** ISO timestamp of when the grant was last exercised. */
|
|
190
|
+
lastUsedAt: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
167
193
|
/** Input accepted by `createApplication`. Staff-only fields are not settable here. */
|
|
168
194
|
export interface CreateApplicationInput {
|
|
169
195
|
name: string;
|
|
@@ -309,6 +335,59 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
|
|
|
309
335
|
}
|
|
310
336
|
}
|
|
311
337
|
|
|
338
|
+
/**
|
|
339
|
+
* List the OAuth-authorized applications the current user has connected —
|
|
340
|
+
* the third-party apps the user granted access to via the consent flow.
|
|
341
|
+
* Each entry is a {@link ConnectedApp} carrying the application's display
|
|
342
|
+
* identity, the granted scopes, and when the grant was first made and last
|
|
343
|
+
* exercised. Requires an authenticated session.
|
|
344
|
+
*
|
|
345
|
+
* Backed by `GET /auth/grants`. The response is briefly cached
|
|
346
|
+
* (identity-scoped); {@link revokeAppGrant} busts that cache so a revoke is
|
|
347
|
+
* reflected on the next read.
|
|
348
|
+
*/
|
|
349
|
+
async listConnectedApps(): Promise<ConnectedApp[]> {
|
|
350
|
+
try {
|
|
351
|
+
return await this.makeRequest<ConnectedApp[]>(
|
|
352
|
+
'GET',
|
|
353
|
+
'/auth/grants',
|
|
354
|
+
undefined,
|
|
355
|
+
{ cache: true, cacheTTL: CACHE_TIMES.SHORT },
|
|
356
|
+
);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw this.handleError(error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Revoke the current user's grant for a connected application, identified by
|
|
364
|
+
* its application `_id` (a {@link ConnectedApp.applicationId}, NOT a
|
|
365
|
+
* credential/client id — keyed by application so the revocation survives
|
|
366
|
+
* credential rotation). After this the application can no longer act on the
|
|
367
|
+
* user's behalf until it is re-authorized.
|
|
368
|
+
*
|
|
369
|
+
* Backed by `DELETE /auth/grants/:applicationId`. On success the cached
|
|
370
|
+
* connected-apps list (`GET:/auth/grants`) is invalidated so the next
|
|
371
|
+
* {@link listConnectedApps} read reflects the removal.
|
|
372
|
+
*
|
|
373
|
+
* @param applicationId - The connected application's Mongo `_id`.
|
|
374
|
+
*/
|
|
375
|
+
async revokeAppGrant(applicationId: string): Promise<void> {
|
|
376
|
+
try {
|
|
377
|
+
await this.makeRequest<{ revoked: boolean }>(
|
|
378
|
+
'DELETE',
|
|
379
|
+
`/auth/grants/${applicationId}`,
|
|
380
|
+
undefined,
|
|
381
|
+
{ cache: false },
|
|
382
|
+
);
|
|
383
|
+
// A revoke removes an entry from the user's connected-apps list; bust
|
|
384
|
+
// the cached `GET /auth/grants` so the next read re-fetches.
|
|
385
|
+
this.clearCacheEntry('GET:/auth/grants');
|
|
386
|
+
} catch (error) {
|
|
387
|
+
throw this.handleError(error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
312
391
|
/**
|
|
313
392
|
* List applications the current user is an active member of.
|
|
314
393
|
*
|
|
@@ -2,11 +2,6 @@ import type { AccountStorageUsageResponse, AssetUploadInput, AssetUrlResponse, A
|
|
|
2
2
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
3
3
|
import { isReactNative } from '../utils/platform';
|
|
4
4
|
|
|
5
|
-
interface FileDownloadUrlOptions {
|
|
6
|
-
/** Omit bearer access tokens from generated URLs, even when authenticated. */
|
|
7
|
-
omitToken?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
11
6
|
return class extends Base {
|
|
12
7
|
constructor(...args: any[]) {
|
|
@@ -25,55 +20,30 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
/**
|
|
28
|
-
* Build a synchronous file URL from an Oxy asset id.
|
|
29
|
-
*
|
|
30
|
-
* This is the single chokepoint every Oxy app uses to turn a stored file id
|
|
31
|
-
* (avatars, post media, etc.) into a `<img src>`-ready URL, so it resolves to
|
|
32
|
-
* one of two forms depending on whether the caller needs a signed/private URL:
|
|
33
|
-
*
|
|
34
|
-
* - **Public asset (default)** — no access token planted on the client AND no
|
|
35
|
-
* `expiresIn` requested → returns the clean CDN form
|
|
36
|
-
* `${cloudURL}/<id>[?variant=...]` (e.g. `https://cloud.oxy.so/<id>?variant=thumb`).
|
|
37
|
-
* CloudFront resolves the id against the public media origin. No token,
|
|
38
|
-
* `fallback`, or origin query params are emitted — these URLs are cacheable
|
|
39
|
-
* and shareable.
|
|
40
|
-
* - **Signed / private asset** — an access token is present on the client OR
|
|
41
|
-
* `expiresIn` was passed (the caller explicitly wants an expiring/authorized
|
|
42
|
-
* URL) → keeps the authenticated origin form
|
|
43
|
-
* `${baseURL}/assets/<id>/stream?...&token=...`. Private assets are NOT on
|
|
44
|
-
* the public CDN, so they must go through the API origin that can authorize
|
|
45
|
-
* the request.
|
|
23
|
+
* Build a synchronous, `<img src>`-ready file URL from an Oxy asset id.
|
|
46
24
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
25
|
+
* This method must never embed the caller's general access token in the
|
|
26
|
+
* returned URL. The URL is commonly rendered into DOM attributes, browser
|
|
27
|
+
* network panels, caches, and logs. Public asset URLs use the clean CDN
|
|
28
|
+
* origin; callers that need authorized/private access should use
|
|
29
|
+
* {@link getFileDownloadUrlAsync}, which asks the API for a scoped download
|
|
30
|
+
* URL instead of exposing the in-memory bearer token in a query string.
|
|
52
31
|
*/
|
|
53
|
-
getFileDownloadUrl(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const token = options.omitToken ? undefined : this.getClient().getAccessToken();
|
|
60
|
-
|
|
61
|
-
// Public case: no auth token and no expiry requested → clean CDN URL.
|
|
62
|
-
// CloudFront serves the public media origin under `${cloudURL}/<id>`.
|
|
63
|
-
if (!token && !expiresIn) {
|
|
32
|
+
getFileDownloadUrl(fileId: string, variant?: string, expiresIn?: number): string {
|
|
33
|
+
// Never embed the in-memory bearer token: this URL is rendered into DOM
|
|
34
|
+
// attributes, browser network panels, caches, and logs. Public assets get
|
|
35
|
+
// the clean CDN origin; private/authorized access goes through
|
|
36
|
+
// `getFileDownloadUrlAsync`.
|
|
37
|
+
if (!expiresIn) {
|
|
64
38
|
const variantQs = variant ? `?variant=${encodeURIComponent(variant)}` : '';
|
|
65
39
|
return `${this.getCloudURL()}/${encodeURIComponent(fileId)}${variantQs}`;
|
|
66
40
|
}
|
|
67
41
|
|
|
68
|
-
// Signed / private case: route through the authenticated API origin's
|
|
69
|
-
// stream endpoint so the request can be authorized (private assets are not
|
|
70
|
-
// exposed on the public CDN).
|
|
71
42
|
const base = this.getBaseURL();
|
|
72
43
|
const params = new URLSearchParams();
|
|
73
44
|
if (variant) params.set('variant', variant);
|
|
74
|
-
|
|
45
|
+
params.set('expiresIn', String(expiresIn));
|
|
75
46
|
params.set('fallback', 'placeholderVisible');
|
|
76
|
-
if (token) params.set('token', token);
|
|
77
47
|
|
|
78
48
|
const qs = params.toString();
|
|
79
49
|
return `${base}/assets/${encodeURIComponent(fileId)}/stream${qs ? `?${qs}` : ''}`;
|
|
@@ -125,12 +125,35 @@ export interface CommonsApprovalInfo {
|
|
|
125
125
|
scopes: string[];
|
|
126
126
|
/** The origin the session is bound to (the RP web origin), when applicable. */
|
|
127
127
|
boundOrigin?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Server-authoritative anti-phishing flag: `true` only when this device-flow
|
|
130
|
+
* sign-in was started from a verified, registered origin of a trusted app.
|
|
131
|
+
* The approver (Commons) shows a warning when this is `false`. Always present
|
|
132
|
+
* — a missing/non-boolean server value is coerced to `false` (fail-safe to
|
|
133
|
+
* "not verified") by {@link OxyServicesAuthMixin.getCommonsApprovalInfo}.
|
|
134
|
+
*/
|
|
135
|
+
originVerified: boolean;
|
|
128
136
|
/** Server-authoritative expiry (epoch milliseconds). */
|
|
129
137
|
expiresAt: number;
|
|
130
138
|
/** Session lifecycle status. */
|
|
131
139
|
status: string;
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
/**
|
|
143
|
+
* @internal Raw server response of `GET /auth/session/approve-info/:code`.
|
|
144
|
+
* `originVerified` is typed loosely here because older servers may omit it (or
|
|
145
|
+
* send a non-boolean); the SDK coerces it to a strict `boolean` when mapping
|
|
146
|
+
* into {@link CommonsApprovalInfo}.
|
|
147
|
+
*/
|
|
148
|
+
interface CommonsApprovalInfoResponse {
|
|
149
|
+
application: PublicApplication;
|
|
150
|
+
scopes: string[];
|
|
151
|
+
boundOrigin?: string;
|
|
152
|
+
originVerified?: unknown;
|
|
153
|
+
expiresAt: number;
|
|
154
|
+
status: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
/** Result of approving / denying a "Sign in with Oxy" request. */
|
|
135
158
|
export interface CommonsSignInActionResult {
|
|
136
159
|
success: boolean;
|
|
@@ -826,12 +849,23 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
826
849
|
*/
|
|
827
850
|
async getCommonsApprovalInfo(authorizeCode: string): Promise<CommonsApprovalInfo> {
|
|
828
851
|
try {
|
|
829
|
-
|
|
852
|
+
const raw = await this.makeRequest<CommonsApprovalInfoResponse>(
|
|
830
853
|
'GET',
|
|
831
854
|
`/auth/session/approve-info/${encodeURIComponent(authorizeCode)}`,
|
|
832
855
|
undefined,
|
|
833
856
|
{ cache: false }
|
|
834
857
|
);
|
|
858
|
+
return {
|
|
859
|
+
application: raw.application,
|
|
860
|
+
scopes: raw.scopes,
|
|
861
|
+
boundOrigin: raw.boundOrigin,
|
|
862
|
+
// Fail-safe: only a literal boolean `true` counts as verified. A
|
|
863
|
+
// missing or non-boolean value (older server, malformed response)
|
|
864
|
+
// coerces to `false` so a stale server can never imply trust.
|
|
865
|
+
originVerified: raw.originVerified === true,
|
|
866
|
+
expiresAt: raw.expiresAt,
|
|
867
|
+
status: raw.status,
|
|
868
|
+
};
|
|
835
869
|
} catch (error) {
|
|
836
870
|
throw this.handleError(error);
|
|
837
871
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Preview (unfurl) Mixin
|
|
3
|
+
*
|
|
4
|
+
* Resolves link previews ("unfurls") through the Oxy API so every Oxy app
|
|
5
|
+
* stops scraping link metadata locally. The API owns resolution and re-hosts
|
|
6
|
+
* the preview `image`/`favicon` on Oxy media (`cloud.oxy.so/<fileId>`), so
|
|
7
|
+
* consumers render the returned URLs directly with no per-app proxy.
|
|
8
|
+
*
|
|
9
|
+
* Wire shapes (`LinkPreview`, `LinkPreviewBatchResponse`) are the single source
|
|
10
|
+
* of truth in `@oxyhq/contracts`; this mixin imports them rather than
|
|
11
|
+
* redefining them so producer (oxy-api) and consumers cannot drift.
|
|
12
|
+
*
|
|
13
|
+
* Caching note: these GET/POST reads are NOT cached at the SDK layer (`cache:
|
|
14
|
+
* false`). A preview can be returned `'pending'` first and `'resolved'` on a
|
|
15
|
+
* later read, so an SDK GET cache would pin the stale `'pending'` snapshot.
|
|
16
|
+
* App-side caching (React Query / stores) owns this responsibility.
|
|
17
|
+
*/
|
|
18
|
+
import type { LinkPreview, LinkPreviewBatchResponse } from '@oxyhq/contracts';
|
|
19
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
20
|
+
import { buildUrl } from '../utils/apiUtils';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Maximum number of URLs sent per `POST /links/previews` request. Matches the
|
|
24
|
+
* server-side batch cap (`linkPreviewBatchRequestSchema`'s `.max(50)`); larger
|
|
25
|
+
* inputs are split into multiple chunked calls and the result maps merged,
|
|
26
|
+
* mirroring how `getUsersByIds` chunks at 100.
|
|
27
|
+
*/
|
|
28
|
+
const LINK_PREVIEWS_CHUNK_SIZE = 50;
|
|
29
|
+
|
|
30
|
+
export function OxyServicesLinksMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
31
|
+
return class extends Base {
|
|
32
|
+
constructor(...args: any[]) {
|
|
33
|
+
super(...(args as [any]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a single link preview via `GET /links/preview?url=<encoded>&wait=0|1`.
|
|
38
|
+
*
|
|
39
|
+
* @param url - The URL to unfurl. Sent percent-encoded in the query string.
|
|
40
|
+
* @param opts.wait - When `true`, asks the server to resolve synchronously
|
|
41
|
+
* (`wait=1`) instead of returning a `'pending'` placeholder for a
|
|
42
|
+
* first-seen URL. Defaults to `false` (`wait=0`).
|
|
43
|
+
*
|
|
44
|
+
* Not cached at the SDK layer: a `'pending'` result can become `'resolved'`
|
|
45
|
+
* on a later read, so caching here would serve the stale placeholder.
|
|
46
|
+
*/
|
|
47
|
+
async getLinkPreview(url: string, opts?: { wait?: boolean }): Promise<LinkPreview> {
|
|
48
|
+
try {
|
|
49
|
+
const path = buildUrl('/links/preview', { url, wait: opts?.wait ? 1 : 0 });
|
|
50
|
+
return await this.makeRequest<LinkPreview>('GET', path, undefined, { cache: false });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw this.handleError(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve multiple link previews via `POST /links/previews` (body `{ urls }`).
|
|
58
|
+
*
|
|
59
|
+
* Inputs are de-duplicated and split into chunks of {@link LINK_PREVIEWS_CHUNK_SIZE}
|
|
60
|
+
* (the server-side cap). Chunks run concurrently and their `data` maps are
|
|
61
|
+
* merged into a single result keyed by the REQUESTED url (the exact string
|
|
62
|
+
* passed in `urls`) — matching the batch contract — so a caller can always
|
|
63
|
+
* look its own input back up.
|
|
64
|
+
*
|
|
65
|
+
* An empty / whitespace-only input resolves immediately with `{}` and
|
|
66
|
+
* performs no network call. A failure in any chunk surfaces (via
|
|
67
|
+
* `handleError`) rather than being swallowed.
|
|
68
|
+
*/
|
|
69
|
+
async getLinkPreviews(urls: string[]): Promise<Record<string, LinkPreview>> {
|
|
70
|
+
const uniqueUrls = Array.from(
|
|
71
|
+
new Set(urls.filter((u): u is string => typeof u === 'string' && u.trim().length > 0)),
|
|
72
|
+
);
|
|
73
|
+
if (uniqueUrls.length === 0) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const chunks: string[][] = [];
|
|
78
|
+
for (let i = 0; i < uniqueUrls.length; i += LINK_PREVIEWS_CHUNK_SIZE) {
|
|
79
|
+
chunks.push(uniqueUrls.slice(i, i + LINK_PREVIEWS_CHUNK_SIZE));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const responses = await Promise.all(
|
|
84
|
+
chunks.map((chunk) =>
|
|
85
|
+
this.makeRequest<LinkPreviewBatchResponse>(
|
|
86
|
+
'POST',
|
|
87
|
+
'/links/previews',
|
|
88
|
+
{ urls: chunk },
|
|
89
|
+
{ cache: false },
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return responses.reduce<Record<string, LinkPreview>>(
|
|
95
|
+
(merged, response) => Object.assign(merged, response?.data ?? {}),
|
|
96
|
+
{},
|
|
97
|
+
);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw this.handleError(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|