@oxyhq/core 15.0.1 → 16.0.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/OxyServices.errors.js +33 -1
- package/dist/cjs/OxyServices.js +2 -1
- package/dist/cjs/index.js +6 -5
- package/dist/cjs/mixins/OxyServices.assets.js +31 -8
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +5 -5
- package/dist/cjs/utils/accountUtils.js +6 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/OxyServices.errors.js +31 -0
- package/dist/esm/OxyServices.js +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.assets.js +32 -9
- package/dist/esm/mixins/OxyServices.deviceBoot.js +5 -5
- package/dist/esm/utils/accountUtils.js +6 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/OxyServices.d.ts +2 -2
- package/dist/types/OxyServices.errors.d.ts +33 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.assets.d.ts +17 -5
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +5 -5
- package/package.json +2 -2
- package/src/OxyServices.errors.ts +45 -0
- package/src/OxyServices.ts +2 -2
- package/src/index.ts +1 -1
- package/src/mixins/OxyServices.assets.ts +36 -9
- package/src/mixins/OxyServices.deviceBoot.ts +5 -5
- package/src/mixins/__tests__/OxyServices.serviceAssetMetadata.test.ts +52 -3
- package/src/utils/__tests__/accountUtils.test.ts +22 -0
- package/src/utils/accountUtils.ts +9 -1
|
@@ -49,6 +49,37 @@ export class AssetUrlResolutionError extends Error {
|
|
|
49
49
|
this.cause = cause;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when one or more chunks of `getServiceAssetMetadataByIds` could not be
|
|
54
|
+
* resolved.
|
|
55
|
+
*
|
|
56
|
+
* Exists because for this endpoint a FAILED request and an ABSENT asset produce
|
|
57
|
+
* the same observable result. The server legitimately omits unknown/deleted ids,
|
|
58
|
+
* so callers are documented to map the response by `id` and treat a missing
|
|
59
|
+
* entry as "no such asset" — which means a chunk that 429s, times out or 5xxs
|
|
60
|
+
* reads as authoritative absence unless it is raised.
|
|
61
|
+
*
|
|
62
|
+
* That was not hypothetical: a metadata backfill counted every throttled asset
|
|
63
|
+
* as needing no update and exited 0, and the MTN signed-record builder embedded
|
|
64
|
+
* media with no content hash into records that are immutable once signed. Both
|
|
65
|
+
* paths reported success and wrote nothing.
|
|
66
|
+
*
|
|
67
|
+
* `unresolvedIds` carries every id in a failed chunk — not the subset the server
|
|
68
|
+
* would have omitted anyway, which is unknowable when the request never landed.
|
|
69
|
+
* A caller that wants best-effort passes `{ partial: true }` and never sees this.
|
|
70
|
+
*/
|
|
71
|
+
export class ServiceAssetMetadataError extends Error {
|
|
72
|
+
constructor(unresolvedIds, statuses, cause) {
|
|
73
|
+
const uniqueStatuses = Array.from(new Set(statuses)).sort((a, b) => a - b);
|
|
74
|
+
const statusSuffix = uniqueStatuses.length > 0 ? ` — status ${uniqueStatuses.join(', ')}` : '';
|
|
75
|
+
super(`Could not resolve asset metadata for ${unresolvedIds.length} id(s)${statusSuffix}. Treat this as unknown, not as absent; pass { partial: true } for best-effort.`);
|
|
76
|
+
this.code = 'SERVICE_ASSET_METADATA_UNRESOLVED';
|
|
77
|
+
this.name = 'ServiceAssetMetadataError';
|
|
78
|
+
this.unresolvedIds = unresolvedIds;
|
|
79
|
+
this.statuses = uniqueStatuses;
|
|
80
|
+
this.cause = cause;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
52
83
|
export class OxyAuthenticationTimeoutError extends OxyAuthenticationError {
|
|
53
84
|
constructor(operationName, timeoutMs) {
|
|
54
85
|
super(`Authentication timeout (${timeoutMs}ms): ${operationName} requires user authentication. Please ensure the user is logged in before calling this method.`, 'AUTH_TIMEOUT', 408);
|
package/dist/esm/OxyServices.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AssetUrlResolutionError, OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors.js';
|
|
1
|
+
import { AssetUrlResolutionError, OxyAuthenticationError, OxyAuthenticationTimeoutError, ServiceAssetMetadataError } from './OxyServices.errors.js';
|
|
2
2
|
// Import mixin composition helper
|
|
3
3
|
import { composeOxyServices } from './mixins/index.js';
|
|
4
4
|
/**
|
|
@@ -50,7 +50,7 @@ export class OxyServices extends OxyServicesComposed {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
// Re-export error classes for convenience
|
|
53
|
-
export { AssetUrlResolutionError, OxyAuthenticationError, OxyAuthenticationTimeoutError };
|
|
53
|
+
export { AssetUrlResolutionError, OxyAuthenticationError, OxyAuthenticationTimeoutError, ServiceAssetMetadataError };
|
|
54
54
|
/**
|
|
55
55
|
* Default Oxy Cloud URL — used when no `cloudURL` is provided to OxyServices.
|
|
56
56
|
*/
|
package/dist/esm/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import './crypto/polyfill.js';
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// API client
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
|
-
export { OxyServices, AssetUrlResolutionError, OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.js';
|
|
24
|
+
export { OxyServices, AssetUrlResolutionError, OxyAuthenticationError, OxyAuthenticationTimeoutError, ServiceAssetMetadataError } from './OxyServices.js';
|
|
25
25
|
export { OXY_CLOUD_URL, oxyClient } from './OxyServices.js';
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
// Authentication
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isReactNative } from '@oxyhq/protocol';
|
|
2
2
|
import { logger } from '../logger/index.js';
|
|
3
|
-
import { AssetUrlResolutionError } from '../OxyServices.errors.js';
|
|
3
|
+
import { AssetUrlResolutionError, ServiceAssetMetadataError } from '../OxyServices.errors.js';
|
|
4
4
|
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
5
5
|
/**
|
|
6
6
|
* Maximum number of ids sent per `POST /assets/service/by-ids` request. Matches
|
|
@@ -300,15 +300,25 @@ export function OxyServicesAssetsMixin(Base) {
|
|
|
300
300
|
* throws because no credentials are available. A plain user-session request
|
|
301
301
|
* is rejected by the route's service-auth guard.
|
|
302
302
|
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
303
|
+
* FAILURE IS NOT ABSENCE. The server legitimately omits unknown/deleted ids,
|
|
304
|
+
* so a short result is normal — which means a chunk that FAILED (a 429, a
|
|
305
|
+
* timeout, a 5xx) is indistinguishable from "those assets don't exist" if it
|
|
306
|
+
* simply contributes nothing. This method used to swallow a failed chunk and
|
|
307
|
+
* return the rest, so every caller silently read a throttled request as "no
|
|
308
|
+
* metadata": a backfill counted the asset as needing no update, and the
|
|
309
|
+
* signed-record builder embedded a media item with no hash. Both reported
|
|
310
|
+
* success while writing nothing, and the MTN chain's records are immutable.
|
|
311
|
+
*
|
|
312
|
+
* So a failed chunk THROWS {@link ServiceAssetMetadataError} by default,
|
|
313
|
+
* carrying the ids it could not resolve. A caller that genuinely wants
|
|
314
|
+
* best-effort opts in with `{ partial: true }` and gets the old behaviour
|
|
315
|
+
* explicitly. An empty/whitespace-only input resolves immediately with `[]`
|
|
316
|
+
* and performs no network call.
|
|
307
317
|
*
|
|
308
318
|
* Not cached at the SDK layer: it's a POST keyed on a multi-id body (low hit
|
|
309
319
|
* rate), mirroring the sibling service/POST methods which never cache.
|
|
310
320
|
*/
|
|
311
|
-
async getServiceAssetMetadataByIds(ids) {
|
|
321
|
+
async getServiceAssetMetadataByIds(ids, options = {}) {
|
|
312
322
|
const uniqueIds = Array.from(new Set(ids.filter((id) => typeof id === 'string' && id.trim().length > 0)));
|
|
313
323
|
if (uniqueIds.length === 0) {
|
|
314
324
|
return [];
|
|
@@ -317,22 +327,35 @@ export function OxyServicesAssetsMixin(Base) {
|
|
|
317
327
|
for (let i = 0; i < uniqueIds.length; i += SERVICE_ASSET_METADATA_CHUNK_SIZE) {
|
|
318
328
|
chunks.push(uniqueIds.slice(i, i + SERVICE_ASSET_METADATA_CHUNK_SIZE));
|
|
319
329
|
}
|
|
320
|
-
//
|
|
330
|
+
// Chunks stay independent so one failure never cancels work already in
|
|
331
|
+
// flight; the failures are collected and re-raised together below.
|
|
332
|
+
const unresolvedIds = [];
|
|
333
|
+
const statuses = [];
|
|
334
|
+
let firstError;
|
|
321
335
|
const settled = await Promise.all(chunks.map(async (chunk) => {
|
|
322
336
|
try {
|
|
323
337
|
const entries = await this.makeServiceRequest('POST', '/assets/service/by-ids', { ids: chunk });
|
|
324
338
|
return Array.isArray(entries) ? entries : [];
|
|
325
339
|
}
|
|
326
340
|
catch (error) {
|
|
327
|
-
|
|
341
|
+
const status = extractErrorStatus(error);
|
|
342
|
+
logger.warn('getServiceAssetMetadataByIds: chunk failed', {
|
|
328
343
|
method: 'getServiceAssetMetadataByIds',
|
|
329
344
|
chunkSize: chunk.length,
|
|
330
|
-
status
|
|
345
|
+
status,
|
|
346
|
+
partial: options.partial === true,
|
|
331
347
|
error: error instanceof Error ? error.message : String(error),
|
|
332
348
|
});
|
|
349
|
+
unresolvedIds.push(...chunk);
|
|
350
|
+
if (typeof status === 'number')
|
|
351
|
+
statuses.push(status);
|
|
352
|
+
firstError ?? (firstError = error);
|
|
333
353
|
return [];
|
|
334
354
|
}
|
|
335
355
|
}));
|
|
356
|
+
if (unresolvedIds.length > 0 && options.partial !== true) {
|
|
357
|
+
throw new ServiceAssetMetadataError(unresolvedIds, statuses, firstError);
|
|
358
|
+
}
|
|
336
359
|
return settled.flat();
|
|
337
360
|
}
|
|
338
361
|
/**
|
|
@@ -151,11 +151,11 @@ export function OxyServicesDeviceBootMixin(Base) {
|
|
|
151
151
|
*
|
|
152
152
|
* That paragraph is LOAD-BEARING, not belt-and-braces: the route sits above
|
|
153
153
|
* oxy-api's router-wide origin guard (deliberately, so a native client with
|
|
154
|
-
* no `Origin` is not rejected)
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
154
|
+
* no `Origin` is not rejected). oxy-api additionally refuses callers that
|
|
155
|
+
* carry browser context signals (`Origin` or `Sec-Fetch-Site`) with
|
|
156
|
+
* `403 browser_not_allowed` — native HTTP clients send neither. Gate the
|
|
157
|
+
* caller by platform on the client as well; do not widen the 404 degrade to
|
|
158
|
+
* 403, which would also swallow a genuine origin misconfiguration.
|
|
159
159
|
*
|
|
160
160
|
* @returns the provisioned credential, or `null` when the endpoint is absent
|
|
161
161
|
* (404). The API deploy leads the SDK release, so a client on a newer SDK
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Used by the @oxyhq/services account stores (Expo/RN and RN-Web).
|
|
4
4
|
*/
|
|
5
5
|
import { translate } from '../i18n/index.js';
|
|
6
|
+
import { getNormalizedUserHandle } from './userHandle.js';
|
|
6
7
|
/**
|
|
7
8
|
* Truncate a long public key for display, e.g. `0x12345678…`.
|
|
8
9
|
* Falls back to the raw key if it's too short to truncate.
|
|
@@ -96,7 +97,11 @@ export const buildAccountsArray = (accounts, order) => {
|
|
|
96
97
|
* @param getFileDownloadUrl - Function to generate avatar download URL from file ID
|
|
97
98
|
*/
|
|
98
99
|
export const createQuickAccount = (sessionId, userData, existingAccount, getFileDownloadUrl) => {
|
|
99
|
-
const
|
|
100
|
+
const nameObj = userData.name && typeof userData.name === 'object' ? userData.name : undefined;
|
|
101
|
+
const apiDisplayName = typeof nameObj?.displayName === 'string' ? nameObj.displayName.trim() : '';
|
|
102
|
+
const displayName = apiDisplayName ||
|
|
103
|
+
getNormalizedUserHandle(userData) ||
|
|
104
|
+
getAccountDisplayName(null);
|
|
100
105
|
const userId = userData.id || (typeof userData._id === 'string' ? userData._id : userData._id?.toString());
|
|
101
106
|
// Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
|
|
102
107
|
let avatarUrl;
|