@oxyhq/core 3.7.1 → 3.8.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.
@@ -39,6 +39,15 @@ export interface BulkUnfollowResult {
39
39
  }
40
40
  export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T): {
41
41
  new (...args: any[]): {
42
+ /**
43
+ * Service-token request, implemented by the auth mixin earlier in the
44
+ * composition pipeline (see `mixins/index.ts`). The user mixin is typed
45
+ * against `OxyServicesBase`, which does not carry the auth mixin's methods,
46
+ * so this `declare` surfaces the inherited runtime method to TypeScript
47
+ * without re-implementing it. Used by `getUsersByIds` to authenticate the
48
+ * server-to-server `/users/by-ids` bulk fetch with a bearer service token.
49
+ */
50
+ makeServiceRequest: <R = unknown>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: unknown, userId?: string) => Promise<R>;
42
51
  /**
43
52
  * Get profile by username
44
53
  */
@@ -117,6 +126,38 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
117
126
  * Get user by ID
118
127
  */
119
128
  getUserById(userId: string): Promise<User>;
129
+ /**
130
+ * Fetch many users by id in one round-trip per chunk.
131
+ *
132
+ * Built for feed/hydration call sites that would otherwise issue one
133
+ * `getUserById` request per unique author (the classic M+1). Ids are
134
+ * deduplicated and validated (empty/blank ids dropped) before being split
135
+ * into chunks of {@link USERS_BY_IDS_CHUNK_SIZE} and POSTed to
136
+ * `/users/by-ids` as `{ ids }`. The server returns the matched users as a
137
+ * flat `User[]` (order is not guaranteed and the caller is expected to map
138
+ * by `id`); each is run through `normalizeUserIdentity`, matching
139
+ * `getUserById`.
140
+ *
141
+ * **Service-token auth (required).** `/users/by-ids` is a server-to-server
142
+ * bulk fetch of PUBLIC user data and is called via `makeServiceRequest`,
143
+ * which attaches `Authorization: Bearer <serviceToken>`. oxy-api's CSRF
144
+ * middleware skips bearer-authenticated requests, so the calling client
145
+ * MUST be service-configured (`configureServiceAuth(apiKey, apiSecret)`)
146
+ * before invoking this method; otherwise `getServiceToken()` throws because
147
+ * no credentials are available. (A plain user-session request fails here:
148
+ * server-to-server there is no cookie jar, so the auto-attached
149
+ * `X-CSRF-Token` has no matching cookie and oxy-api rejects the POST with
150
+ * 403 "CSRF token missing".)
151
+ *
152
+ * Resilience: chunks are independent. A failed chunk is logged and skipped
153
+ * — the method returns every user that resolved successfully rather than
154
+ * discarding the whole call on one chunk's failure. An empty/whitespace-only
155
+ * input resolves immediately with `[]` and performs no network call.
156
+ *
157
+ * Not cached at the SDK layer: the response is keyed on a multi-id POST body
158
+ * (low hit rate) and the backend maintains its own per-id Redis cache.
159
+ */
160
+ getUsersByIds(ids: string[]): Promise<User[]>;
120
161
  /**
121
162
  * Get current user
122
163
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "3.7.1",
3
+ "version": "3.8.1",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -25,6 +25,12 @@ import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/use
25
25
  import { logger } from '../utils/loggerUtils';
26
26
  import { extractErrorStatus } from '../utils/errorUtils';
27
27
 
28
+ /**
29
+ * Maximum number of ids sent per `POST /users/by-ids` request. Matches the
30
+ * server-side batch cap; larger inputs are split into multiple chunked calls.
31
+ */
32
+ const USERS_BY_IDS_CHUNK_SIZE = 100;
33
+
28
34
  /** Per-user outcome returned by `POST /users/follow/bulk`. */
29
35
  export interface BulkFollowEntry {
30
36
  /** The user ID that was processed. */
@@ -66,6 +72,22 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
66
72
  constructor(...args: any[]) {
67
73
  super(...(args as [any]));
68
74
  }
75
+
76
+ /**
77
+ * Service-token request, implemented by the auth mixin earlier in the
78
+ * composition pipeline (see `mixins/index.ts`). The user mixin is typed
79
+ * against `OxyServicesBase`, which does not carry the auth mixin's methods,
80
+ * so this `declare` surfaces the inherited runtime method to TypeScript
81
+ * without re-implementing it. Used by `getUsersByIds` to authenticate the
82
+ * server-to-server `/users/by-ids` bulk fetch with a bearer service token.
83
+ */
84
+ declare makeServiceRequest: <R = unknown>(
85
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
86
+ url: string,
87
+ data?: unknown,
88
+ userId?: string,
89
+ ) => Promise<R>;
90
+
69
91
  /**
70
92
  * Get profile by username
71
93
  */
@@ -315,6 +337,71 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
315
337
  }
316
338
  }
317
339
 
340
+ /**
341
+ * Fetch many users by id in one round-trip per chunk.
342
+ *
343
+ * Built for feed/hydration call sites that would otherwise issue one
344
+ * `getUserById` request per unique author (the classic M+1). Ids are
345
+ * deduplicated and validated (empty/blank ids dropped) before being split
346
+ * into chunks of {@link USERS_BY_IDS_CHUNK_SIZE} and POSTed to
347
+ * `/users/by-ids` as `{ ids }`. The server returns the matched users as a
348
+ * flat `User[]` (order is not guaranteed and the caller is expected to map
349
+ * by `id`); each is run through `normalizeUserIdentity`, matching
350
+ * `getUserById`.
351
+ *
352
+ * **Service-token auth (required).** `/users/by-ids` is a server-to-server
353
+ * bulk fetch of PUBLIC user data and is called via `makeServiceRequest`,
354
+ * which attaches `Authorization: Bearer <serviceToken>`. oxy-api's CSRF
355
+ * middleware skips bearer-authenticated requests, so the calling client
356
+ * MUST be service-configured (`configureServiceAuth(apiKey, apiSecret)`)
357
+ * before invoking this method; otherwise `getServiceToken()` throws because
358
+ * no credentials are available. (A plain user-session request fails here:
359
+ * server-to-server there is no cookie jar, so the auto-attached
360
+ * `X-CSRF-Token` has no matching cookie and oxy-api rejects the POST with
361
+ * 403 "CSRF token missing".)
362
+ *
363
+ * Resilience: chunks are independent. A failed chunk is logged and skipped
364
+ * — the method returns every user that resolved successfully rather than
365
+ * discarding the whole call on one chunk's failure. An empty/whitespace-only
366
+ * input resolves immediately with `[]` and performs no network call.
367
+ *
368
+ * Not cached at the SDK layer: the response is keyed on a multi-id POST body
369
+ * (low hit rate) and the backend maintains its own per-id Redis cache.
370
+ */
371
+ async getUsersByIds(ids: string[]): Promise<User[]> {
372
+ const uniqueIds = Array.from(
373
+ new Set(ids.filter((id): id is string => typeof id === 'string' && id.trim().length > 0)),
374
+ );
375
+ if (uniqueIds.length === 0) {
376
+ return [];
377
+ }
378
+
379
+ const chunks: string[][] = [];
380
+ for (let i = 0; i < uniqueIds.length; i += USERS_BY_IDS_CHUNK_SIZE) {
381
+ chunks.push(uniqueIds.slice(i, i + USERS_BY_IDS_CHUNK_SIZE));
382
+ }
383
+
384
+ // Run chunks concurrently; a single chunk failure must not sink the rest.
385
+ const settled = await Promise.all(
386
+ chunks.map(async (chunk): Promise<User[]> => {
387
+ try {
388
+ const users = await this.makeServiceRequest<User[]>('POST', '/users/by-ids', { ids: chunk });
389
+ return Array.isArray(users) ? users.map((user) => normalizeUserIdentity(user)) : [];
390
+ } catch (error: unknown) {
391
+ logger.warn('getUsersByIds: chunk failed, continuing with remaining chunks', {
392
+ method: 'getUsersByIds',
393
+ chunkSize: chunk.length,
394
+ status: extractErrorStatus(error),
395
+ error: error instanceof Error ? error.message : String(error),
396
+ });
397
+ return [];
398
+ }
399
+ }),
400
+ );
401
+
402
+ return settled.flat();
403
+ }
404
+
318
405
  /**
319
406
  * Get current user
320
407
  */