@oxyhq/core 5.0.0 → 5.1.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/mixins/OxyServices.accounts.js +42 -6
- package/dist/cjs/mixins/OxyServices.assets.js +66 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.accounts.js +42 -6
- package/dist/esm/mixins/OxyServices.assets.js +66 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.accounts.d.ts +13 -4
- package/dist/types/mixins/OxyServices.assets.d.ts +42 -1
- package/dist/types/models/interfaces.d.ts +18 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/mixins/OxyServices.accounts.ts +51 -6
- package/src/mixins/OxyServices.assets.ts +95 -1
- package/src/mixins/__tests__/OxyServices.serviceAssetMetadata.test.ts +116 -0
- package/src/mixins/__tests__/accounts.test.ts +66 -16
- package/src/models/interfaces.ts +19 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { normalizeUserIdentity } from '../utils/userIdentity.js';
|
|
2
|
+
import { isWeb } from '../utils/platform.js';
|
|
3
|
+
import { logger } from '../utils/loggerUtils.js';
|
|
2
4
|
import { CACHE_TIMES } from './mixinHelpers.js';
|
|
3
5
|
export function OxyServicesAccountsMixin(Base) {
|
|
4
6
|
return class extends Base {
|
|
@@ -54,10 +56,19 @@ export function OxyServicesAccountsMixin(Base) {
|
|
|
54
56
|
* Unlike the removed `X-Acting-As` delegation header, the returned session
|
|
55
57
|
* IS the new identity: this plants `accessToken` as the active token —
|
|
56
58
|
* exactly like `claimSessionByToken` / `verifyChallenge` — so every
|
|
57
|
-
* subsequent request authenticates as the target account.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* subsequent request authenticates as the target account.
|
|
60
|
+
*
|
|
61
|
+
* Joining the device multi-account set (so the switch survives a reload and
|
|
62
|
+
* propagates cross-domain via `/auth/refresh-all`) requires a SECOND call, to
|
|
63
|
+
* `POST /auth/session`, made here after the token is planted. The switch route
|
|
64
|
+
* lives at `/accounts/*`, OUTSIDE the `oxy_rt_<authuser>` cookie's `Path=/auth`
|
|
65
|
+
* scope, so the server never sees the device's existing slots from it and
|
|
66
|
+
* would clobber slot 0 (destroying the operator's own session). `/auth/session`
|
|
67
|
+
* runs where those cookies ARE visible, so the server allocates a NEW slot that
|
|
68
|
+
* coexists with the operator's and returns its `authuser`. This step is
|
|
69
|
+
* web-only (native multi-account uses stored sessions, not cookies) and
|
|
70
|
+
* best-effort — a failure leaves the in-session switch intact; the switched
|
|
71
|
+
* account simply won't survive a reload until the cookie is next established.
|
|
61
72
|
*
|
|
62
73
|
* After planting, the SDK's identity-scoped GET cache is fully cleared so
|
|
63
74
|
* every cached read re-fetches as the new account. (The consuming
|
|
@@ -74,11 +85,35 @@ export function OxyServicesAccountsMixin(Base) {
|
|
|
74
85
|
const res = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/switch`, undefined, { cache: false });
|
|
75
86
|
// Plant the freshly minted session as the ACTIVE session, mirroring
|
|
76
87
|
// `claimSessionByToken` / `verifyChallenge`: the response body carries
|
|
77
|
-
// the first access token
|
|
78
|
-
// cookie, so there is nothing else to store here.
|
|
88
|
+
// the first access token. The device refresh cookie is established below.
|
|
79
89
|
if (res?.accessToken) {
|
|
80
90
|
this.setTokens(res.accessToken);
|
|
81
91
|
}
|
|
92
|
+
// Register the switched session in the device's multi-account set by
|
|
93
|
+
// establishing its first-party refresh cookie. This MUST be a separate
|
|
94
|
+
// call to `POST /auth/session`: the switch route is at `/accounts/*`,
|
|
95
|
+
// outside the `oxy_rt_<authuser>` cookie's `Path=/auth` scope, so it can
|
|
96
|
+
// never read the device's existing slots and would overwrite slot 0
|
|
97
|
+
// (destroying the operator's own session). `/auth/session` runs where the
|
|
98
|
+
// cookies ARE visible, so the server allocates a NEW slot that coexists
|
|
99
|
+
// with the operator's and returns its `authuser`. Web-only; best-effort.
|
|
100
|
+
let authuser = res.authuser;
|
|
101
|
+
if (isWeb()) {
|
|
102
|
+
try {
|
|
103
|
+
const established = await this.makeRequest('POST', '/auth/session', undefined, { cache: false });
|
|
104
|
+
if (typeof established?.authuser === 'number') {
|
|
105
|
+
authuser = established.authuser;
|
|
106
|
+
}
|
|
107
|
+
// /auth/session mints a fresh access token off the same session;
|
|
108
|
+
// re-plant it so the active token matches the rotated cookie.
|
|
109
|
+
if (established?.accessToken) {
|
|
110
|
+
this.setTokens(established.accessToken);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
logger.warn('[OxyServices] Failed to establish device refresh cookie after account switch; the switch is active in-session but may not survive a reload', { component: 'OxyServices', method: 'switchToAccount' }, error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
82
117
|
// Identity changed → drop the entire GET response cache so no entry
|
|
83
118
|
// personalised for the previous identity is reused. Cache keys are
|
|
84
119
|
// identity-scoped, so a different identity could not READ the old
|
|
@@ -87,6 +122,7 @@ export function OxyServicesAccountsMixin(Base) {
|
|
|
87
122
|
this.clearCache();
|
|
88
123
|
return {
|
|
89
124
|
...res,
|
|
125
|
+
...(typeof authuser === 'number' ? { authuser } : {}),
|
|
90
126
|
user: normalizeUserIdentity(res.user),
|
|
91
127
|
};
|
|
92
128
|
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { isReactNative } from '@oxyhq/protocol';
|
|
2
|
+
import { logger } from '../utils/loggerUtils.js';
|
|
3
|
+
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Maximum number of ids sent per `POST /assets/service/by-ids` request. Matches
|
|
6
|
+
* the server-side batch cap (the route rejects empty or > 100 id arrays with a
|
|
7
|
+
* 400); larger inputs are split into multiple chunked calls and merged. Mirrors
|
|
8
|
+
* `getUsersByIds`'s `USERS_BY_IDS_CHUNK_SIZE`.
|
|
9
|
+
*/
|
|
10
|
+
const SERVICE_ASSET_METADATA_CHUNK_SIZE = 100;
|
|
2
11
|
export function OxyServicesAssetsMixin(Base) {
|
|
3
12
|
return class extends Base {
|
|
4
13
|
constructor(...args) {
|
|
@@ -144,6 +153,63 @@ export function OxyServicesAssetsMixin(Base) {
|
|
|
144
153
|
}
|
|
145
154
|
return urls;
|
|
146
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve many Oxy asset ids to their content-addressed metadata in one
|
|
158
|
+
* round-trip per chunk via `POST /assets/service/by-ids` (body `{ ids }`).
|
|
159
|
+
*
|
|
160
|
+
* Returns each asset's `sha256`, `mime`, byte `size`, and `status` — built
|
|
161
|
+
* for server-to-server callers (e.g. Mention's MTN Protocol blob-ref
|
|
162
|
+
* resolution) that need the content hash for an asset id. Ids are
|
|
163
|
+
* deduplicated and validated (empty/blank ids dropped) before being split
|
|
164
|
+
* into chunks of {@link SERVICE_ASSET_METADATA_CHUNK_SIZE} (the server-side
|
|
165
|
+
* cap). The server omits unknown/deleted ids from each chunk's `data`, so
|
|
166
|
+
* the merged result may be shorter than the requested id list and the caller
|
|
167
|
+
* is expected to map by `id`.
|
|
168
|
+
*
|
|
169
|
+
* **Service-token auth (required).** `/assets/service/by-ids` is guarded by
|
|
170
|
+
* `serviceAuthMiddleware` + the `files:read` scope and is called via
|
|
171
|
+
* `makeServiceRequest`, which attaches `Authorization: Bearer <serviceToken>`
|
|
172
|
+
* (the same client that calls `POST /assets/service/cache`). The calling
|
|
173
|
+
* client MUST be service-configured (`configureServiceAuth(apiKey,
|
|
174
|
+
* apiSecret)`) before invoking this method; otherwise `getServiceToken()`
|
|
175
|
+
* throws because no credentials are available. A plain user-session request
|
|
176
|
+
* is rejected by the route's service-auth guard.
|
|
177
|
+
*
|
|
178
|
+
* Resilience: chunks are independent. A failed chunk is logged and skipped —
|
|
179
|
+
* the method returns every entry that resolved successfully rather than
|
|
180
|
+
* discarding the whole call on one chunk's failure. An empty/whitespace-only
|
|
181
|
+
* input resolves immediately with `[]` and performs no network call.
|
|
182
|
+
*
|
|
183
|
+
* Not cached at the SDK layer: it's a POST keyed on a multi-id body (low hit
|
|
184
|
+
* rate), mirroring the sibling service/POST methods which never cache.
|
|
185
|
+
*/
|
|
186
|
+
async getServiceAssetMetadataByIds(ids) {
|
|
187
|
+
const uniqueIds = Array.from(new Set(ids.filter((id) => typeof id === 'string' && id.trim().length > 0)));
|
|
188
|
+
if (uniqueIds.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const chunks = [];
|
|
192
|
+
for (let i = 0; i < uniqueIds.length; i += SERVICE_ASSET_METADATA_CHUNK_SIZE) {
|
|
193
|
+
chunks.push(uniqueIds.slice(i, i + SERVICE_ASSET_METADATA_CHUNK_SIZE));
|
|
194
|
+
}
|
|
195
|
+
// Run chunks concurrently; a single chunk failure must not sink the rest.
|
|
196
|
+
const settled = await Promise.all(chunks.map(async (chunk) => {
|
|
197
|
+
try {
|
|
198
|
+
const entries = await this.makeServiceRequest('POST', '/assets/service/by-ids', { ids: chunk });
|
|
199
|
+
return Array.isArray(entries) ? entries : [];
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
logger.warn('getServiceAssetMetadataByIds: chunk failed, continuing with remaining chunks', {
|
|
203
|
+
method: 'getServiceAssetMetadataByIds',
|
|
204
|
+
chunkSize: chunk.length,
|
|
205
|
+
status: extractErrorStatus(error),
|
|
206
|
+
error: error instanceof Error ? error.message : String(error),
|
|
207
|
+
});
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}));
|
|
211
|
+
return settled.flat();
|
|
212
|
+
}
|
|
147
213
|
/**
|
|
148
214
|
* Upload raw file data
|
|
149
215
|
*/
|