@oxy.so/core 7.12.0 → 7.13.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.
Files changed (43) hide show
  1. package/dist/cjs/api/apps.js +1 -1
  2. package/dist/cjs/api/auth.js +1 -1
  3. package/dist/cjs/api/billing.js +4 -3
  4. package/dist/cjs/api/devices.js +5 -4
  5. package/dist/cjs/api/follows.js +12 -10
  6. package/dist/cjs/api/privacy.js +8 -7
  7. package/dist/cjs/api/session.js +10 -7
  8. package/dist/cjs/api/topics.js +4 -3
  9. package/dist/cjs/api/users.js +5 -4
  10. package/dist/cjs/i18n/locales/compact/es-ES.generated.js +1 -1
  11. package/dist/cjs/i18n/locales/en-US.js +38 -15
  12. package/dist/cjs/i18n/locales/es-ES.js +38 -15
  13. package/dist/cjs/utils/identityCacheSweep.js +9 -1
  14. package/dist/esm/api/apps.js +1 -1
  15. package/dist/esm/api/auth.js +1 -1
  16. package/dist/esm/api/billing.js +4 -3
  17. package/dist/esm/api/devices.js +5 -4
  18. package/dist/esm/api/follows.js +12 -10
  19. package/dist/esm/api/privacy.js +8 -7
  20. package/dist/esm/api/session.js +10 -7
  21. package/dist/esm/api/topics.js +4 -3
  22. package/dist/esm/api/users.js +6 -5
  23. package/dist/esm/i18n/locales/compact/es-ES.generated.js +1 -1
  24. package/dist/esm/i18n/locales/en-US.js +38 -15
  25. package/dist/esm/i18n/locales/es-ES.js +38 -15
  26. package/dist/esm/utils/identityCacheSweep.js +8 -1
  27. package/dist/types/i18n/locales/en-US.d.ts +35 -12
  28. package/dist/types/i18n/locales/es-ES.d.ts +35 -12
  29. package/dist/types/utils/identityCacheSweep.d.ts +5 -0
  30. package/package.json +2 -2
  31. package/src/api/apps.ts +1 -1
  32. package/src/api/auth.ts +9 -4
  33. package/src/api/billing.ts +12 -5
  34. package/src/api/devices.ts +6 -4
  35. package/src/api/follows.ts +21 -10
  36. package/src/api/privacy.ts +9 -7
  37. package/src/api/session.ts +18 -9
  38. package/src/api/topics.ts +5 -3
  39. package/src/api/users.ts +11 -5
  40. package/src/i18n/locales/compact/es-ES.generated.ts +1 -1
  41. package/src/i18n/locales/en-US.ts +39 -15
  42. package/src/i18n/locales/es-ES.ts +40 -15
  43. package/src/utils/identityCacheSweep.ts +9 -1
@@ -40,6 +40,8 @@ import type {
40
40
  import { logger } from '../logger';
41
41
  import { extractErrorStatus } from '../utils/errorUtils';
42
42
 
43
+ const enc = encodeURIComponent;
44
+
43
45
  /** One of the signed-in user's devices (`GET /devices`). */
44
46
  export interface UserDevice {
45
47
  id: string;
@@ -139,7 +141,7 @@ export class DevicesApi {
139
141
  * @param deviceId - The device ID to remove
140
142
  */
141
143
  async remove(deviceId: string): Promise<void> {
142
- await this.ctx.request('DELETE', `/devices/${deviceId}`, undefined, { cache: false });
144
+ await this.ctx.request('DELETE', `/devices/${enc(deviceId)}`, undefined, { cache: false });
143
145
  }
144
146
 
145
147
  /**
@@ -152,7 +154,7 @@ export class DevicesApi {
152
154
  // Cache disabled by default to ensure fresh session data
153
155
  return this.ctx.request<DeviceLinkedSession[]>(
154
156
  'GET',
155
- `/session/device/sessions/${sessionId}`,
157
+ `/session/device/sessions/${enc(sessionId)}`,
156
158
  undefined,
157
159
  {
158
160
  cache: false, // Don't cache sessions - always get fresh data
@@ -178,7 +180,7 @@ export class DevicesApi {
178
180
  if (excludeCurrent) urlParams.excludeCurrent = 'true';
179
181
  return this.ctx.request<DeviceLinkedSessionLogoutResponse>(
180
182
  'POST',
181
- `/session/device/logout-all/${sessionId}`,
183
+ `/session/device/logout-all/${enc(sessionId)}`,
182
184
  urlParams,
183
185
  { cache: false },
184
186
  );
@@ -196,7 +198,7 @@ export class DevicesApi {
196
198
  ): Promise<{ success: boolean; message: string; deviceName: string }> {
197
199
  return this.ctx.request(
198
200
  'PUT',
199
- `/session/device/name/${sessionId}`,
201
+ `/session/device/name/${enc(sessionId)}`,
200
202
  { deviceName },
201
203
  { cache: false },
202
204
  );
@@ -27,6 +27,7 @@ import {
27
27
  buildUrl,
28
28
  type FollowGraphParams,
29
29
  } from '../utils/apiUtils';
30
+ import { oxyUserByIdCacheKey, oxyUserByIdPath } from '../utils/identityCacheSweep';
30
31
 
31
32
  const GRAPH_TTL = 2 * 60 * 1000;
32
33
  const STATUS_TTL = 60 * 1000;
@@ -130,7 +131,7 @@ export class FollowsApi {
130
131
  async follow(userId: string): Promise<FollowMutationResult> {
131
132
  const result = await this.ctx.request<FollowMutationResult>(
132
133
  'POST',
133
- `/users/${userId}/follow`,
134
+ `${oxyUserByIdPath(userId)}/follow`,
134
135
  undefined,
135
136
  { cache: false },
136
137
  );
@@ -155,7 +156,7 @@ export class FollowsApi {
155
156
  async unfollow(userId: string): Promise<FollowMutationResult> {
156
157
  const result = await this.ctx.request<FollowMutationResult>(
157
158
  'DELETE',
158
- `/users/${userId}/follow`,
159
+ `${oxyUserByIdPath(userId)}/follow`,
159
160
  undefined,
160
161
  { cache: false },
161
162
  );
@@ -178,7 +179,7 @@ export class FollowsApi {
178
179
 
179
180
  /** Whether the signed-in user follows `userId`. Cached 1 minute, busted by every write. */
180
181
  async status(userId: string): Promise<{ isFollowing: boolean }> {
181
- return this.ctx.request('GET', `/users/${userId}/follow-status`, undefined, {
182
+ return this.ctx.request('GET', `${oxyUserByIdPath(userId)}/follow-status`, undefined, {
182
183
  cache: true,
183
184
  cacheTTL: STATUS_TTL,
184
185
  });
@@ -221,13 +222,19 @@ export class FollowsApi {
221
222
  * `limit`/`offset`/`sort` combination is its own cache entry (2 minutes).
222
223
  */
223
224
  async followers(userId: string, params?: FollowGraphParams): Promise<UserPage<'followers'>> {
224
- const { data, total, hasMore } = await this.userPage(`/users/${userId}/followers`, params);
225
+ const { data, total, hasMore } = await this.userPage(
226
+ `${oxyUserByIdPath(userId)}/followers`,
227
+ params,
228
+ );
225
229
  return { followers: data, total, hasMore };
226
230
  }
227
231
 
228
232
  /** Who a user follows. `sort` as in `followers`. */
229
233
  async following(userId: string, params?: FollowGraphParams): Promise<UserPage<'following'>> {
230
- const { data, total, hasMore } = await this.userPage(`/users/${userId}/following`, params);
234
+ const { data, total, hasMore } = await this.userPage(
235
+ `${oxyUserByIdPath(userId)}/following`,
236
+ params,
237
+ );
231
238
  return { following: data, total, hasMore };
232
239
  }
233
240
 
@@ -236,7 +243,10 @@ export class FollowsApi {
236
243
  * `userId`.
237
244
  */
238
245
  async mutuals(userId: string, params?: FollowGraphParams): Promise<UserPage<'mutuals'>> {
239
- const { data, total, hasMore } = await this.userPage(`/users/${userId}/mutuals`, params);
246
+ const { data, total, hasMore } = await this.userPage(
247
+ `${oxyUserByIdPath(userId)}/mutuals`,
248
+ params,
249
+ );
240
250
  return { mutuals: data, total, hasMore };
241
251
  }
242
252
 
@@ -443,15 +453,16 @@ export class FollowsApi {
443
453
  const keys: string[] = ['GET:/users/me/graph'];
444
454
  const prefixes: string[] = ['GET:/profiles/username/', 'GET:/profiles/resolve'];
445
455
  for (const id of targetUserIds) {
446
- keys.push(`GET:/users/${id}/follow-status`);
456
+ const user = oxyUserByIdPath(id);
457
+ keys.push(`GET:${user}/follow-status`);
447
458
  // Profiles embed the viewer-relative `relationship`.
448
- keys.push(`GET:/users/${id}`);
459
+ keys.push(oxyUserByIdCacheKey(id));
449
460
  // The target gained or lost a follower, and "followers you know" with it.
450
- prefixes.push(`GET:/users/${id}/followers`, `GET:/users/${id}/mutuals`);
461
+ prefixes.push(`GET:${user}/followers`, `GET:${user}/mutuals`);
451
462
  }
452
463
  // The viewer's own following list changed.
453
464
  const viewerId = this.ctx.oxy.session.userId;
454
- if (viewerId) prefixes.push(`GET:/users/${viewerId}/following`);
465
+ if (viewerId) prefixes.push(`GET:${oxyUserByIdPath(viewerId)}/following`);
455
466
  this.ctx.http.invalidateCache({ keys, prefixes });
456
467
  }
457
468
  }
@@ -5,6 +5,8 @@ import type { BlockedUser, PrivacySettings, RestrictedUser } from '../models/int
5
5
  import type { OxyContext } from '../client/context';
6
6
  import { evictOxyIdentityCache } from '../utils/identityCacheSweep';
7
7
 
8
+ const enc = encodeURIComponent;
9
+
8
10
  const LIST_TTL = 60 * 1000;
9
11
  const SETTINGS_TTL = 2 * 60 * 1000;
10
12
 
@@ -20,7 +22,7 @@ export class PrivacyApi {
20
22
  /** A user's privacy settings (default: the signed-in user's). */
21
23
  async settings(userId?: string): Promise<PrivacySettings> {
22
24
  const id = await this.resolveUserId(userId);
23
- return this.ctx.request<PrivacySettings>('GET', `/privacy/${id}/privacy`, undefined, {
25
+ return this.ctx.request<PrivacySettings>('GET', `/privacy/${enc(id)}/privacy`, undefined, {
24
26
  cache: true,
25
27
  cacheTTL: SETTINGS_TTL,
26
28
  });
@@ -34,13 +36,13 @@ export class PrivacyApi {
34
36
  const id = await this.resolveUserId(userId);
35
37
  const res = await this.ctx.request<PrivacySettings>(
36
38
  'PATCH',
37
- `/privacy/${id}/privacy`,
39
+ `/privacy/${enc(id)}/privacy`,
38
40
  settings,
39
41
  { cache: false },
40
42
  );
41
43
  // Privacy settings ride the user DTO, so every identity read goes stale too.
42
44
  evictOxyIdentityCache(this.ctx.http, id);
43
- this.ctx.oxy.cache.delete(`GET:/privacy/${id}/privacy`);
45
+ this.ctx.oxy.cache.delete(`GET:/privacy/${enc(id)}/privacy`);
44
46
  return res;
45
47
  }
46
48
 
@@ -59,7 +61,7 @@ export class PrivacyApi {
59
61
  requireId(userId);
60
62
  const res = await this.ctx.request<{ message: string }>(
61
63
  'POST',
62
- `/privacy/blocked/${userId}`,
64
+ `/privacy/blocked/${enc(userId)}`,
63
65
  undefined,
64
66
  { cache: false },
65
67
  );
@@ -72,7 +74,7 @@ export class PrivacyApi {
72
74
  requireId(userId);
73
75
  const res = await this.ctx.request<{ message: string }>(
74
76
  'DELETE',
75
- `/privacy/blocked/${userId}`,
77
+ `/privacy/blocked/${enc(userId)}`,
76
78
  undefined,
77
79
  { cache: false },
78
80
  );
@@ -105,7 +107,7 @@ export class PrivacyApi {
105
107
  requireId(userId);
106
108
  const res = await this.ctx.request<{ message: string }>(
107
109
  'POST',
108
- `/privacy/restricted/${userId}`,
110
+ `/privacy/restricted/${enc(userId)}`,
109
111
  undefined,
110
112
  { cache: false },
111
113
  );
@@ -118,7 +120,7 @@ export class PrivacyApi {
118
120
  requireId(userId);
119
121
  const res = await this.ctx.request<{ message: string }>(
120
122
  'DELETE',
121
- `/privacy/restricted/${userId}`,
123
+ `/privacy/restricted/${enc(userId)}`,
122
124
  undefined,
123
125
  { cache: false },
124
126
  );
@@ -20,6 +20,8 @@ import type {
20
20
  HolderEvidenceUnavailableReason,
21
21
  } from './holderEvidence';
22
22
 
23
+ const enc = encodeURIComponent;
24
+
23
25
  /** How long a refusal (disabled, rate limited, unreachable) is remembered per installation. */
24
26
  const EVIDENCE_REFUSAL_MS = 5_000;
25
27
  const unavailable = (reason: HolderEvidenceUnavailableReason): HolderEvidenceResult => ({
@@ -376,7 +378,7 @@ export class SessionApi {
376
378
  )
377
379
  : await this.ctx.request<SessionValidation>(
378
380
  'GET',
379
- `/session/validate/${sessionId}`,
381
+ `/session/validate/${enc(sessionId)}`,
380
382
  {
381
383
  ...(options.deviceFingerprint
382
384
  ? { deviceFingerprint: options.deviceFingerprint }
@@ -407,7 +409,7 @@ export class SessionApi {
407
409
  const invalid =
408
410
  candidate?.status === 401 && candidate.code === 'OXY_LIVE_SESSION_INVALID_V1';
409
411
  const denied = candidate?.status === 403 && candidate.code === 'OXY_LIVE_SESSION_POLICY_V1';
410
- if (invalid) this.ctx.oxy.cache.delete(`GET:/session/user/${sessionId}`);
412
+ if (invalid) this.ctx.oxy.cache.delete(`GET:/session/user/${enc(sessionId)}`);
411
413
  if (invalid || denied) throw error;
412
414
  throw new OxyApiError('Live session authority is unavailable', {
413
415
  status: 503,
@@ -416,29 +418,36 @@ export class SessionApi {
416
418
  });
417
419
  }
418
420
  // The session is gone: drop any user cached for it (#196).
419
- this.ctx.oxy.cache.delete(`GET:/session/user/${sessionId}`);
421
+ this.ctx.oxy.cache.delete(`GET:/session/user/${enc(sessionId)}`);
420
422
  throw error;
421
423
  }
422
424
  }
423
425
 
424
426
  /** The sessions that belong with `sessionId` (same account set). */
425
427
  async list(sessionId: string): Promise<ClientSession[]> {
426
- return this.ctx.request<ClientSession[]>('GET', `/session/sessions/${sessionId}`, undefined, {
427
- cache: false,
428
- });
428
+ return this.ctx.request<ClientSession[]>(
429
+ 'GET',
430
+ `/session/sessions/${enc(sessionId)}`,
431
+ undefined,
432
+ {
433
+ cache: false,
434
+ },
435
+ );
429
436
  }
430
437
 
431
438
  /** Sign out `sessionId`, or — with `targetSessionId` — one of the sessions beside it. */
432
439
  async logout(sessionId: string, targetSessionId?: string): Promise<void> {
433
440
  const url = targetSessionId
434
- ? `/session/logout/${sessionId}/${targetSessionId}`
435
- : `/session/logout/${sessionId}`;
441
+ ? `/session/logout/${enc(sessionId)}/${enc(targetSessionId)}`
442
+ : `/session/logout/${enc(sessionId)}`;
436
443
  await this.ctx.request('POST', url, undefined, { cache: false });
437
444
  }
438
445
 
439
446
  /** Sign out the other sessions for this account; preserve `sessionId`. */
440
447
  async logoutAll(sessionId: string): Promise<void> {
441
- await this.ctx.request('POST', `/session/logout-all/${sessionId}`, undefined, { cache: false });
448
+ await this.ctx.request('POST', `/session/logout-all/${enc(sessionId)}`, undefined, {
449
+ cache: false,
450
+ });
442
451
  }
443
452
 
444
453
  /** Decoded claims of the current token, memoised per token string. */
package/src/api/topics.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  import type { OxyContext } from '../client/context';
5
5
  import type { TopicData, TopicListResult, TopicTranslation } from '../models/Topic';
6
6
 
7
+ const enc = encodeURIComponent;
8
+
7
9
  const SHORT = 60 * 1000;
8
10
  const LONG = 5 * 60 * 1000;
9
11
  const EXTRA_LONG = 30 * 60 * 1000;
@@ -51,7 +53,7 @@ export class TopicsApi {
51
53
 
52
54
  /** One topic by slug. */
53
55
  async get(slug: string): Promise<TopicData> {
54
- return this.ctx.request<TopicData>('GET', `/topics/${slug}`, undefined, {
56
+ return this.ctx.request<TopicData>('GET', `/topics/${enc(slug)}`, undefined, {
55
57
  cache: true,
56
58
  cacheTTL: LONG,
57
59
  });
@@ -86,10 +88,10 @@ export class TopicsApi {
86
88
  slug: string,
87
89
  data: { description?: string; translations?: Record<string, TopicTranslation> },
88
90
  ): Promise<TopicData> {
89
- const topic = await this.ctx.request<TopicData>('PATCH', `/topics/${slug}`, data, {
91
+ const topic = await this.ctx.request<TopicData>('PATCH', `/topics/${enc(slug)}`, data, {
90
92
  cache: false,
91
93
  });
92
- this.ctx.oxy.cache.delete(`GET:/topics/${slug}`);
94
+ this.ctx.oxy.cache.delete(`GET:/topics/${enc(slug)}`);
93
95
  return topic;
94
96
  }
95
97
  }
package/src/api/users.ts CHANGED
@@ -12,7 +12,11 @@ import type { OxyContext } from '../client/context';
12
12
  import type { PaginationInfo, SearchProfilesResponse, User } from '../models/interfaces';
13
13
  import { buildQueryParams, type PaginationParams } from '../utils/apiUtils';
14
14
  import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity';
15
- import { OXY_IDENTITY_CACHE_PREFIXES, oxyUserByIdCacheKey } from '../utils/identityCacheSweep';
15
+ import {
16
+ OXY_IDENTITY_CACHE_PREFIXES,
17
+ oxyUserByIdCacheKey,
18
+ oxyUserByIdPath,
19
+ } from '../utils/identityCacheSweep';
16
20
  import {
17
21
  OXY_ACCOUNT_LIST_CACHE_KEY,
18
22
  OXY_ACCOUNT_LIST_CACHE_QUERY_PREFIX,
@@ -22,6 +26,8 @@ import { extractErrorStatus } from '../utils/errorUtils';
22
26
  import { type BatchLookup, runBatchLookup } from '../utils/batchLookup';
23
27
  import { logger } from '../logger';
24
28
 
29
+ const enc = encodeURIComponent;
30
+
25
31
  const PROFILE_TTL = 5 * 60 * 1000;
26
32
  const SESSION_USER_TTL = 2 * 60 * 1000;
27
33
  const ME_TTL = 60 * 1000;
@@ -65,7 +71,7 @@ export class UsersApi {
65
71
  * registry-fresh read that neither serves nor overwrites a cached entry.
66
72
  */
67
73
  async get(userId: string, options?: { cache?: boolean }): Promise<User> {
68
- const user = await this.ctx.request<User>('GET', `/users/${userId}`, undefined, {
74
+ const user = await this.ctx.request<User>('GET', oxyUserByIdPath(userId), undefined, {
69
75
  cache: options?.cache ?? true,
70
76
  cacheTTL: PROFILE_TTL,
71
77
  });
@@ -166,7 +172,7 @@ export class UsersApi {
166
172
 
167
173
  /** The user a session belongs to. */
168
174
  async bySession(sessionId: string): Promise<User> {
169
- const user = await this.ctx.request<User>('GET', `/session/user/${sessionId}`, undefined, {
175
+ const user = await this.ctx.request<User>('GET', `/session/user/${enc(sessionId)}`, undefined, {
170
176
  cache: true,
171
177
  cacheTTL: SESSION_USER_TTL,
172
178
  });
@@ -271,7 +277,7 @@ export class UsersApi {
271
277
  : (limitOrPagination ?? {});
272
278
  const users = await this.ctx.request<User[]>(
273
279
  'GET',
274
- `/profiles/${userId}/similar`,
280
+ `/profiles/${enc(userId)}/similar`,
275
281
  buildQueryParams(pagination),
276
282
  {
277
283
  cache: true,
@@ -289,7 +295,7 @@ export class UsersApi {
289
295
  async moderators(communityId: string): Promise<User[]> {
290
296
  const moderators = await this.ctx.request<User[]>(
291
297
  'GET',
292
- `/users/${communityId}/moderators`,
298
+ `/users/${enc(communityId)}/moderators`,
293
299
  undefined,
294
300
  { cache: true, cacheTTL: PROFILE_TTL },
295
301
  );