@mediaviz/sdk 1.0.79 → 1.0.81
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/sdk.cjs +64 -14
- package/dist/sdk.d.cts +2 -1
- package/dist/sdk.d.ts +2 -1
- package/dist/sdk.esm.d.ts +2 -1
- package/dist/sdk.esm.js +64 -14
- package/dist/sdk.umd.js +64 -14
- package/package.json +1 -1
package/dist/sdk.cjs
CHANGED
|
@@ -91,8 +91,15 @@ function requireErrors () {
|
|
|
91
91
|
* @returns {OAuthError}
|
|
92
92
|
*/
|
|
93
93
|
static fromResponse(status, body) {
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
// FastAPI wraps HTTPException payloads in a `detail` envelope, so an OAuth
|
|
95
|
+
// error arrives as { detail: { error, error_description } }. Unwrap it
|
|
96
|
+
// before falling back, or every server-side OAuth error reads as server_error.
|
|
97
|
+
const payload =
|
|
98
|
+
body && typeof body === 'object' && typeof body.error !== 'string' && body.detail
|
|
99
|
+
? body.detail
|
|
100
|
+
: body;
|
|
101
|
+
if (payload && typeof payload === 'object' && typeof payload.error === 'string') {
|
|
102
|
+
return new OAuthError(payload.error, payload.error_description ?? '', status, body);
|
|
96
103
|
}
|
|
97
104
|
return new OAuthError('server_error', 'Unexpected server response', status, body);
|
|
98
105
|
}
|
|
@@ -173,6 +180,9 @@ function requireClient () {
|
|
|
173
180
|
const { postForm, postJson } = requireHttp();
|
|
174
181
|
const { OAuthError } = requireErrors();
|
|
175
182
|
|
|
183
|
+
// In-flight dedup key for client_credentials re-mints (no refresh_token to key on).
|
|
184
|
+
const CLIENT_CREDENTIALS_KEY = Symbol('client_credentials');
|
|
185
|
+
|
|
176
186
|
class OAuthClient {
|
|
177
187
|
/**
|
|
178
188
|
* @param {import('./types').OAuthClientConfig} config
|
|
@@ -188,12 +198,22 @@ function requireClient () {
|
|
|
188
198
|
*/
|
|
189
199
|
static async registerClient(params) {
|
|
190
200
|
const baseUrl = params.baseUrl.replace(/\/+$/, '');
|
|
191
|
-
|
|
201
|
+
const body = {
|
|
192
202
|
client_name: params.clientName,
|
|
193
203
|
client_type: params.clientType,
|
|
194
204
|
redirect_uris: params.redirectUris,
|
|
195
205
|
is_first_party: params.isFirstParty,
|
|
196
|
-
}
|
|
206
|
+
};
|
|
207
|
+
// grant_types/company_id are required to provision a confidential
|
|
208
|
+
// client_credentials (machine-to-machine) client; omitted bodies keep the
|
|
209
|
+
// legacy public/authorization_code shape. The endpoint is admin-gated, so a
|
|
210
|
+
// bearer token must be supplied for the request to authenticate.
|
|
211
|
+
if (params.grantTypes !== undefined) body.grant_types = params.grantTypes;
|
|
212
|
+
if (params.companyId !== undefined) body.company_id = params.companyId;
|
|
213
|
+
const headers = params.accessToken
|
|
214
|
+
? { Authorization: `Bearer ${params.accessToken}` }
|
|
215
|
+
: {};
|
|
216
|
+
return postJson(`${baseUrl}/oauth/clients`, body, headers);
|
|
197
217
|
}
|
|
198
218
|
|
|
199
219
|
/**
|
|
@@ -311,15 +331,27 @@ function requireClient () {
|
|
|
311
331
|
return { data: json, updatedTokens: null };
|
|
312
332
|
}
|
|
313
333
|
|
|
314
|
-
// 401:
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
334
|
+
// 401: re-authenticate, then retry once.
|
|
335
|
+
// - User sessions (refresh_token present) use the refresh_token grant. The server
|
|
336
|
+
// rotates the token one-time, so concurrent callers share one in-flight call
|
|
337
|
+
// (a second rotation would get invalid_grant).
|
|
338
|
+
// - Machine-to-machine sessions (no refresh_token, client_secret configured) re-mint
|
|
339
|
+
// via client_credentials, which issues no refresh token to rotate (RFC 6749 §4.4).
|
|
340
|
+
// - With neither a refresh_token nor a client_secret there is nothing to retry with.
|
|
341
|
+
if (!refreshToken && !this._config.clientSecret) {
|
|
342
|
+
throw OAuthError.fromResponse(401, await firstResponse.json());
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const reauthKey = refreshToken || CLIENT_CREDENTIALS_KEY;
|
|
346
|
+
let pending = this._inFlightRefreshes.get(reauthKey);
|
|
318
347
|
if (!pending) {
|
|
319
|
-
|
|
320
|
-
this.
|
|
348
|
+
const reauth = refreshToken
|
|
349
|
+
? this.refreshAccessToken(refreshToken)
|
|
350
|
+
: this.getClientCredentialsToken();
|
|
351
|
+
pending = reauth.finally(() => {
|
|
352
|
+
this._inFlightRefreshes.delete(reauthKey);
|
|
321
353
|
});
|
|
322
|
-
this._inFlightRefreshes.set(
|
|
354
|
+
this._inFlightRefreshes.set(reauthKey, pending);
|
|
323
355
|
}
|
|
324
356
|
const newTokens = await pending;
|
|
325
357
|
|
|
@@ -401,6 +433,9 @@ function requireTypes () {
|
|
|
401
433
|
* @property {string} clientType - "public" or "confidential"
|
|
402
434
|
* @property {string[]} redirectUris - HTTPS redirect URIs
|
|
403
435
|
* @property {boolean} isFirstParty - Whether this is a first-party client
|
|
436
|
+
* @property {string[]} [grantTypes] - Allowed grants, e.g. ["client_credentials"]; required for machine-to-machine clients
|
|
437
|
+
* @property {number} [companyId] - Company to bind the client to; required for the client_credentials grant
|
|
438
|
+
* @property {string} [accessToken] - Admin bearer token; the registration endpoint is authorization-gated
|
|
404
439
|
*/
|
|
405
440
|
|
|
406
441
|
/**
|
|
@@ -1498,7 +1533,7 @@ class Search {
|
|
|
1498
1533
|
return data;
|
|
1499
1534
|
}
|
|
1500
1535
|
|
|
1501
|
-
async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr,
|
|
1536
|
+
async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, ascOrDesc, bestOfSimilarSetsOnly, splitByTier, lastId, limit } = {}) {
|
|
1502
1537
|
this._ctx.requireTokens();
|
|
1503
1538
|
let path = `/api/v1/search/parametrized/${encodeURIComponent(projectTableName)}/`;
|
|
1504
1539
|
const query = new URLSearchParams();
|
|
@@ -1513,10 +1548,25 @@ class Search {
|
|
|
1513
1548
|
if (dateMax !== undefined) (Array.isArray(dateMax) ? dateMax : [dateMax]).forEach(v => query.append('date_max', v));
|
|
1514
1549
|
if (dateNullAnd !== undefined) (Array.isArray(dateNullAnd) ? dateNullAnd : [dateNullAnd]).forEach(v => query.append('date_null_and', v));
|
|
1515
1550
|
if (dateNullOr !== undefined) (Array.isArray(dateNullOr) ? dateNullOr : [dateNullOr]).forEach(v => query.append('date_null_or', v));
|
|
1516
|
-
if (
|
|
1551
|
+
if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
|
|
1517
1552
|
if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
|
|
1518
1553
|
if (splitByTier !== undefined) (Array.isArray(splitByTier) ? splitByTier : [splitByTier]).forEach(v => query.append('split_by_tier', v));
|
|
1519
|
-
if (
|
|
1554
|
+
if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
|
|
1555
|
+
if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
|
|
1556
|
+
const qs = query.toString();
|
|
1557
|
+
if (qs) path += '?' + qs;
|
|
1558
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
1559
|
+
return data;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async recallProjectPhotoIds(projectTableName, { ascOrDesc, bestOfSimilarSetsOnly, lastId, limit } = {}) {
|
|
1563
|
+
this._ctx.requireTokens();
|
|
1564
|
+
let path = `/api/v1/search/recall/${encodeURIComponent(projectTableName)}/`;
|
|
1565
|
+
const query = new URLSearchParams();
|
|
1566
|
+
if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
|
|
1567
|
+
if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
|
|
1568
|
+
if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
|
|
1569
|
+
if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
|
|
1520
1570
|
const qs = query.toString();
|
|
1521
1571
|
if (qs) path += '?' + qs;
|
|
1522
1572
|
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
package/dist/sdk.d.cts
CHANGED
|
@@ -455,7 +455,8 @@ export class Projects {
|
|
|
455
455
|
|
|
456
456
|
export class Search {
|
|
457
457
|
searchProjectPhotos(projectTableName: string, options?: { andParams?: any, andStringParams?: any, orParams?: any, orStringParams?: any, notParams?: any, notStringParams?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], customAlbumId?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], curatedAlbumId?: number | number[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
|
|
458
|
-
searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[],
|
|
458
|
+
searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<Record<string, any>>;
|
|
459
|
+
recallProjectPhotoIds(projectTableName: string, options?: { ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<any>;
|
|
459
460
|
searchProjectPhotosNaturalLanguage(projectTableName: string, searchText: string, size?: number, options?: { minCosine?: number | number[], labelMinCosine?: number | number[], labelTopK?: number | number[], labelDelta?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
|
|
460
461
|
getProjectSavedSearches(projectTableName: string): Promise<any>;
|
|
461
462
|
getSavedSearchById(searchId: number): Promise<SearchDisplay>;
|
package/dist/sdk.d.ts
CHANGED
|
@@ -455,7 +455,8 @@ export class Projects {
|
|
|
455
455
|
|
|
456
456
|
export class Search {
|
|
457
457
|
searchProjectPhotos(projectTableName: string, options?: { andParams?: any, andStringParams?: any, orParams?: any, orStringParams?: any, notParams?: any, notStringParams?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], customAlbumId?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], curatedAlbumId?: number | number[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
|
|
458
|
-
searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[],
|
|
458
|
+
searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<Record<string, any>>;
|
|
459
|
+
recallProjectPhotoIds(projectTableName: string, options?: { ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<any>;
|
|
459
460
|
searchProjectPhotosNaturalLanguage(projectTableName: string, searchText: string, size?: number, options?: { minCosine?: number | number[], labelMinCosine?: number | number[], labelTopK?: number | number[], labelDelta?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
|
|
460
461
|
getProjectSavedSearches(projectTableName: string): Promise<any>;
|
|
461
462
|
getSavedSearchById(searchId: number): Promise<SearchDisplay>;
|
package/dist/sdk.esm.d.ts
CHANGED
|
@@ -455,7 +455,8 @@ export class Projects {
|
|
|
455
455
|
|
|
456
456
|
export class Search {
|
|
457
457
|
searchProjectPhotos(projectTableName: string, options?: { andParams?: any, andStringParams?: any, orParams?: any, orStringParams?: any, notParams?: any, notStringParams?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], customAlbumId?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], curatedAlbumId?: number | number[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
|
|
458
|
-
searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[],
|
|
458
|
+
searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<Record<string, any>>;
|
|
459
|
+
recallProjectPhotoIds(projectTableName: string, options?: { ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<any>;
|
|
459
460
|
searchProjectPhotosNaturalLanguage(projectTableName: string, searchText: string, size?: number, options?: { minCosine?: number | number[], labelMinCosine?: number | number[], labelTopK?: number | number[], labelDelta?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
|
|
460
461
|
getProjectSavedSearches(projectTableName: string): Promise<any>;
|
|
461
462
|
getSavedSearchById(searchId: number): Promise<SearchDisplay>;
|
package/dist/sdk.esm.js
CHANGED
|
@@ -89,8 +89,15 @@ function requireErrors () {
|
|
|
89
89
|
* @returns {OAuthError}
|
|
90
90
|
*/
|
|
91
91
|
static fromResponse(status, body) {
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
// FastAPI wraps HTTPException payloads in a `detail` envelope, so an OAuth
|
|
93
|
+
// error arrives as { detail: { error, error_description } }. Unwrap it
|
|
94
|
+
// before falling back, or every server-side OAuth error reads as server_error.
|
|
95
|
+
const payload =
|
|
96
|
+
body && typeof body === 'object' && typeof body.error !== 'string' && body.detail
|
|
97
|
+
? body.detail
|
|
98
|
+
: body;
|
|
99
|
+
if (payload && typeof payload === 'object' && typeof payload.error === 'string') {
|
|
100
|
+
return new OAuthError(payload.error, payload.error_description ?? '', status, body);
|
|
94
101
|
}
|
|
95
102
|
return new OAuthError('server_error', 'Unexpected server response', status, body);
|
|
96
103
|
}
|
|
@@ -171,6 +178,9 @@ function requireClient () {
|
|
|
171
178
|
const { postForm, postJson } = requireHttp();
|
|
172
179
|
const { OAuthError } = requireErrors();
|
|
173
180
|
|
|
181
|
+
// In-flight dedup key for client_credentials re-mints (no refresh_token to key on).
|
|
182
|
+
const CLIENT_CREDENTIALS_KEY = Symbol('client_credentials');
|
|
183
|
+
|
|
174
184
|
class OAuthClient {
|
|
175
185
|
/**
|
|
176
186
|
* @param {import('./types').OAuthClientConfig} config
|
|
@@ -186,12 +196,22 @@ function requireClient () {
|
|
|
186
196
|
*/
|
|
187
197
|
static async registerClient(params) {
|
|
188
198
|
const baseUrl = params.baseUrl.replace(/\/+$/, '');
|
|
189
|
-
|
|
199
|
+
const body = {
|
|
190
200
|
client_name: params.clientName,
|
|
191
201
|
client_type: params.clientType,
|
|
192
202
|
redirect_uris: params.redirectUris,
|
|
193
203
|
is_first_party: params.isFirstParty,
|
|
194
|
-
}
|
|
204
|
+
};
|
|
205
|
+
// grant_types/company_id are required to provision a confidential
|
|
206
|
+
// client_credentials (machine-to-machine) client; omitted bodies keep the
|
|
207
|
+
// legacy public/authorization_code shape. The endpoint is admin-gated, so a
|
|
208
|
+
// bearer token must be supplied for the request to authenticate.
|
|
209
|
+
if (params.grantTypes !== undefined) body.grant_types = params.grantTypes;
|
|
210
|
+
if (params.companyId !== undefined) body.company_id = params.companyId;
|
|
211
|
+
const headers = params.accessToken
|
|
212
|
+
? { Authorization: `Bearer ${params.accessToken}` }
|
|
213
|
+
: {};
|
|
214
|
+
return postJson(`${baseUrl}/oauth/clients`, body, headers);
|
|
195
215
|
}
|
|
196
216
|
|
|
197
217
|
/**
|
|
@@ -309,15 +329,27 @@ function requireClient () {
|
|
|
309
329
|
return { data: json, updatedTokens: null };
|
|
310
330
|
}
|
|
311
331
|
|
|
312
|
-
// 401:
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
|
|
332
|
+
// 401: re-authenticate, then retry once.
|
|
333
|
+
// - User sessions (refresh_token present) use the refresh_token grant. The server
|
|
334
|
+
// rotates the token one-time, so concurrent callers share one in-flight call
|
|
335
|
+
// (a second rotation would get invalid_grant).
|
|
336
|
+
// - Machine-to-machine sessions (no refresh_token, client_secret configured) re-mint
|
|
337
|
+
// via client_credentials, which issues no refresh token to rotate (RFC 6749 §4.4).
|
|
338
|
+
// - With neither a refresh_token nor a client_secret there is nothing to retry with.
|
|
339
|
+
if (!refreshToken && !this._config.clientSecret) {
|
|
340
|
+
throw OAuthError.fromResponse(401, await firstResponse.json());
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const reauthKey = refreshToken || CLIENT_CREDENTIALS_KEY;
|
|
344
|
+
let pending = this._inFlightRefreshes.get(reauthKey);
|
|
316
345
|
if (!pending) {
|
|
317
|
-
|
|
318
|
-
this.
|
|
346
|
+
const reauth = refreshToken
|
|
347
|
+
? this.refreshAccessToken(refreshToken)
|
|
348
|
+
: this.getClientCredentialsToken();
|
|
349
|
+
pending = reauth.finally(() => {
|
|
350
|
+
this._inFlightRefreshes.delete(reauthKey);
|
|
319
351
|
});
|
|
320
|
-
this._inFlightRefreshes.set(
|
|
352
|
+
this._inFlightRefreshes.set(reauthKey, pending);
|
|
321
353
|
}
|
|
322
354
|
const newTokens = await pending;
|
|
323
355
|
|
|
@@ -399,6 +431,9 @@ function requireTypes () {
|
|
|
399
431
|
* @property {string} clientType - "public" or "confidential"
|
|
400
432
|
* @property {string[]} redirectUris - HTTPS redirect URIs
|
|
401
433
|
* @property {boolean} isFirstParty - Whether this is a first-party client
|
|
434
|
+
* @property {string[]} [grantTypes] - Allowed grants, e.g. ["client_credentials"]; required for machine-to-machine clients
|
|
435
|
+
* @property {number} [companyId] - Company to bind the client to; required for the client_credentials grant
|
|
436
|
+
* @property {string} [accessToken] - Admin bearer token; the registration endpoint is authorization-gated
|
|
402
437
|
*/
|
|
403
438
|
|
|
404
439
|
/**
|
|
@@ -1496,7 +1531,7 @@ class Search {
|
|
|
1496
1531
|
return data;
|
|
1497
1532
|
}
|
|
1498
1533
|
|
|
1499
|
-
async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr,
|
|
1534
|
+
async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, ascOrDesc, bestOfSimilarSetsOnly, splitByTier, lastId, limit } = {}) {
|
|
1500
1535
|
this._ctx.requireTokens();
|
|
1501
1536
|
let path = `/api/v1/search/parametrized/${encodeURIComponent(projectTableName)}/`;
|
|
1502
1537
|
const query = new URLSearchParams();
|
|
@@ -1511,10 +1546,25 @@ class Search {
|
|
|
1511
1546
|
if (dateMax !== undefined) (Array.isArray(dateMax) ? dateMax : [dateMax]).forEach(v => query.append('date_max', v));
|
|
1512
1547
|
if (dateNullAnd !== undefined) (Array.isArray(dateNullAnd) ? dateNullAnd : [dateNullAnd]).forEach(v => query.append('date_null_and', v));
|
|
1513
1548
|
if (dateNullOr !== undefined) (Array.isArray(dateNullOr) ? dateNullOr : [dateNullOr]).forEach(v => query.append('date_null_or', v));
|
|
1514
|
-
if (
|
|
1549
|
+
if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
|
|
1515
1550
|
if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
|
|
1516
1551
|
if (splitByTier !== undefined) (Array.isArray(splitByTier) ? splitByTier : [splitByTier]).forEach(v => query.append('split_by_tier', v));
|
|
1517
|
-
if (
|
|
1552
|
+
if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
|
|
1553
|
+
if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
|
|
1554
|
+
const qs = query.toString();
|
|
1555
|
+
if (qs) path += '?' + qs;
|
|
1556
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
1557
|
+
return data;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
async recallProjectPhotoIds(projectTableName, { ascOrDesc, bestOfSimilarSetsOnly, lastId, limit } = {}) {
|
|
1561
|
+
this._ctx.requireTokens();
|
|
1562
|
+
let path = `/api/v1/search/recall/${encodeURIComponent(projectTableName)}/`;
|
|
1563
|
+
const query = new URLSearchParams();
|
|
1564
|
+
if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
|
|
1565
|
+
if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
|
|
1566
|
+
if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
|
|
1567
|
+
if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
|
|
1518
1568
|
const qs = query.toString();
|
|
1519
1569
|
if (qs) path += '?' + qs;
|
|
1520
1570
|
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
package/dist/sdk.umd.js
CHANGED
|
@@ -95,8 +95,15 @@
|
|
|
95
95
|
* @returns {OAuthError}
|
|
96
96
|
*/
|
|
97
97
|
static fromResponse(status, body) {
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// FastAPI wraps HTTPException payloads in a `detail` envelope, so an OAuth
|
|
99
|
+
// error arrives as { detail: { error, error_description } }. Unwrap it
|
|
100
|
+
// before falling back, or every server-side OAuth error reads as server_error.
|
|
101
|
+
const payload =
|
|
102
|
+
body && typeof body === 'object' && typeof body.error !== 'string' && body.detail
|
|
103
|
+
? body.detail
|
|
104
|
+
: body;
|
|
105
|
+
if (payload && typeof payload === 'object' && typeof payload.error === 'string') {
|
|
106
|
+
return new OAuthError(payload.error, payload.error_description ?? '', status, body);
|
|
100
107
|
}
|
|
101
108
|
return new OAuthError('server_error', 'Unexpected server response', status, body);
|
|
102
109
|
}
|
|
@@ -177,6 +184,9 @@
|
|
|
177
184
|
const { postForm, postJson } = requireHttp();
|
|
178
185
|
const { OAuthError } = requireErrors();
|
|
179
186
|
|
|
187
|
+
// In-flight dedup key for client_credentials re-mints (no refresh_token to key on).
|
|
188
|
+
const CLIENT_CREDENTIALS_KEY = Symbol('client_credentials');
|
|
189
|
+
|
|
180
190
|
class OAuthClient {
|
|
181
191
|
/**
|
|
182
192
|
* @param {import('./types').OAuthClientConfig} config
|
|
@@ -192,12 +202,22 @@
|
|
|
192
202
|
*/
|
|
193
203
|
static async registerClient(params) {
|
|
194
204
|
const baseUrl = params.baseUrl.replace(/\/+$/, '');
|
|
195
|
-
|
|
205
|
+
const body = {
|
|
196
206
|
client_name: params.clientName,
|
|
197
207
|
client_type: params.clientType,
|
|
198
208
|
redirect_uris: params.redirectUris,
|
|
199
209
|
is_first_party: params.isFirstParty,
|
|
200
|
-
}
|
|
210
|
+
};
|
|
211
|
+
// grant_types/company_id are required to provision a confidential
|
|
212
|
+
// client_credentials (machine-to-machine) client; omitted bodies keep the
|
|
213
|
+
// legacy public/authorization_code shape. The endpoint is admin-gated, so a
|
|
214
|
+
// bearer token must be supplied for the request to authenticate.
|
|
215
|
+
if (params.grantTypes !== undefined) body.grant_types = params.grantTypes;
|
|
216
|
+
if (params.companyId !== undefined) body.company_id = params.companyId;
|
|
217
|
+
const headers = params.accessToken
|
|
218
|
+
? { Authorization: `Bearer ${params.accessToken}` }
|
|
219
|
+
: {};
|
|
220
|
+
return postJson(`${baseUrl}/oauth/clients`, body, headers);
|
|
201
221
|
}
|
|
202
222
|
|
|
203
223
|
/**
|
|
@@ -315,15 +335,27 @@
|
|
|
315
335
|
return { data: json, updatedTokens: null };
|
|
316
336
|
}
|
|
317
337
|
|
|
318
|
-
// 401:
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
338
|
+
// 401: re-authenticate, then retry once.
|
|
339
|
+
// - User sessions (refresh_token present) use the refresh_token grant. The server
|
|
340
|
+
// rotates the token one-time, so concurrent callers share one in-flight call
|
|
341
|
+
// (a second rotation would get invalid_grant).
|
|
342
|
+
// - Machine-to-machine sessions (no refresh_token, client_secret configured) re-mint
|
|
343
|
+
// via client_credentials, which issues no refresh token to rotate (RFC 6749 §4.4).
|
|
344
|
+
// - With neither a refresh_token nor a client_secret there is nothing to retry with.
|
|
345
|
+
if (!refreshToken && !this._config.clientSecret) {
|
|
346
|
+
throw OAuthError.fromResponse(401, await firstResponse.json());
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const reauthKey = refreshToken || CLIENT_CREDENTIALS_KEY;
|
|
350
|
+
let pending = this._inFlightRefreshes.get(reauthKey);
|
|
322
351
|
if (!pending) {
|
|
323
|
-
|
|
324
|
-
this.
|
|
352
|
+
const reauth = refreshToken
|
|
353
|
+
? this.refreshAccessToken(refreshToken)
|
|
354
|
+
: this.getClientCredentialsToken();
|
|
355
|
+
pending = reauth.finally(() => {
|
|
356
|
+
this._inFlightRefreshes.delete(reauthKey);
|
|
325
357
|
});
|
|
326
|
-
this._inFlightRefreshes.set(
|
|
358
|
+
this._inFlightRefreshes.set(reauthKey, pending);
|
|
327
359
|
}
|
|
328
360
|
const newTokens = await pending;
|
|
329
361
|
|
|
@@ -405,6 +437,9 @@
|
|
|
405
437
|
* @property {string} clientType - "public" or "confidential"
|
|
406
438
|
* @property {string[]} redirectUris - HTTPS redirect URIs
|
|
407
439
|
* @property {boolean} isFirstParty - Whether this is a first-party client
|
|
440
|
+
* @property {string[]} [grantTypes] - Allowed grants, e.g. ["client_credentials"]; required for machine-to-machine clients
|
|
441
|
+
* @property {number} [companyId] - Company to bind the client to; required for the client_credentials grant
|
|
442
|
+
* @property {string} [accessToken] - Admin bearer token; the registration endpoint is authorization-gated
|
|
408
443
|
*/
|
|
409
444
|
|
|
410
445
|
/**
|
|
@@ -1502,7 +1537,7 @@
|
|
|
1502
1537
|
return data;
|
|
1503
1538
|
}
|
|
1504
1539
|
|
|
1505
|
-
async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr,
|
|
1540
|
+
async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, ascOrDesc, bestOfSimilarSetsOnly, splitByTier, lastId, limit } = {}) {
|
|
1506
1541
|
this._ctx.requireTokens();
|
|
1507
1542
|
let path = `/api/v1/search/parametrized/${encodeURIComponent(projectTableName)}/`;
|
|
1508
1543
|
const query = new URLSearchParams();
|
|
@@ -1517,10 +1552,25 @@
|
|
|
1517
1552
|
if (dateMax !== undefined) (Array.isArray(dateMax) ? dateMax : [dateMax]).forEach(v => query.append('date_max', v));
|
|
1518
1553
|
if (dateNullAnd !== undefined) (Array.isArray(dateNullAnd) ? dateNullAnd : [dateNullAnd]).forEach(v => query.append('date_null_and', v));
|
|
1519
1554
|
if (dateNullOr !== undefined) (Array.isArray(dateNullOr) ? dateNullOr : [dateNullOr]).forEach(v => query.append('date_null_or', v));
|
|
1520
|
-
if (
|
|
1555
|
+
if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
|
|
1521
1556
|
if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
|
|
1522
1557
|
if (splitByTier !== undefined) (Array.isArray(splitByTier) ? splitByTier : [splitByTier]).forEach(v => query.append('split_by_tier', v));
|
|
1523
|
-
if (
|
|
1558
|
+
if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
|
|
1559
|
+
if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
|
|
1560
|
+
const qs = query.toString();
|
|
1561
|
+
if (qs) path += '?' + qs;
|
|
1562
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
1563
|
+
return data;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
async recallProjectPhotoIds(projectTableName, { ascOrDesc, bestOfSimilarSetsOnly, lastId, limit } = {}) {
|
|
1567
|
+
this._ctx.requireTokens();
|
|
1568
|
+
let path = `/api/v1/search/recall/${encodeURIComponent(projectTableName)}/`;
|
|
1569
|
+
const query = new URLSearchParams();
|
|
1570
|
+
if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
|
|
1571
|
+
if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
|
|
1572
|
+
if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
|
|
1573
|
+
if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
|
|
1524
1574
|
const qs = query.toString();
|
|
1525
1575
|
if (qs) path += '?' + qs;
|
|
1526
1576
|
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|