@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
@@ -163,6 +163,9 @@ const dict = {
163
163
  requestExpired: 'This sign-in expired. Send a new email.',
164
164
  secondFactorInvalid: "That code isn't right. Try the current one.",
165
165
  originNotAllowed: "Sign-in isn't available here.",
166
+ network: "Couldn't reach Oxy. Check your connection and try again.",
167
+ invalidInput: "Something you entered isn't valid. Check it and try again.",
168
+ server: "Oxy couldn't finish this right now. Try again in a moment.",
166
169
  generic: 'Something went wrong. Please try again.',
167
170
  },
168
171
  subtitleToApp: 'to continue to {{app}}',
@@ -225,6 +228,31 @@ const dict = {
225
228
  invalidDescription: 'It expired or was already used. Ask for a new email in the app, or type the code from the latest one.',
226
229
  },
227
230
  },
231
+ actionErrors: {
232
+ updateProfile: "Couldn't update your profile. Please try again.",
233
+ uploadAvatar: "Couldn't upload the picture. Please try again.",
234
+ updateSettings: "Couldn't save your settings. Please try again.",
235
+ updatePrivacy: "Couldn't save your privacy settings. Please try again.",
236
+ updateNotifications: "Couldn't save your notification preferences. Please try again.",
237
+ updatePreferences: "Couldn't save your preferences. Please try again.",
238
+ revokeConnectedApp: "Couldn't disconnect the app. Please try again.",
239
+ switchSession: "Couldn't switch accounts. Please try again.",
240
+ logout: "Couldn't sign out. Please try again.",
241
+ logoutAll: "Couldn't sign out everywhere. Please try again.",
242
+ updateDeviceName: "Couldn't rename the device. Please try again.",
243
+ removeDevice: "Couldn't remove the device. Please try again.",
244
+ },
245
+ usernamePolicy: {
246
+ hint: 'Letters, numbers, - and _. No spaces.',
247
+ required: 'Choose a username.',
248
+ tooShort: 'Use at least {{min}} characters.',
249
+ tooLong: 'Use at most {{max}} characters.',
250
+ noSpaces: "Usernames can't contain spaces. Use - or _ instead.",
251
+ characters: 'Use only letters, numbers, - and _. Start and end with a letter or number, and never put two of - or _ together.',
252
+ onlyNumbers: "A username can't be only numbers. Add a letter.",
253
+ reserved: 'That username is reserved. Choose another one.',
254
+ mustEndInBot: 'A bot account’s username must end in “bot”.',
255
+ },
228
256
  signup: {
229
257
  title: 'Create your account',
230
258
  subtitle: 'Choose your username. Then we confirm your email.',
@@ -233,7 +261,6 @@ const dict = {
233
261
  username: {
234
262
  label: 'Username',
235
263
  placeholder: 'yourname',
236
- required: 'Choose a username.',
237
264
  taken: 'That username is taken.',
238
265
  },
239
266
  email: {
@@ -281,7 +308,6 @@ const dict = {
281
308
  firstPlaceholder: 'Your first name',
282
309
  lastLabel: 'Last name',
283
310
  lastPlaceholder: 'Your last name',
284
- saveFailed: 'Could not save your name',
285
311
  },
286
312
  avatar: {
287
313
  title: 'Make It Yours',
@@ -604,8 +630,6 @@ const dict = {
604
630
  label: 'Username',
605
631
  placeholder: 'Choose a username',
606
632
  choose: 'Choose a username',
607
- required: 'Username is required',
608
- tooShort: 'Username must be at least 3 characters',
609
633
  },
610
634
  email: {
611
635
  title: 'Email',
@@ -1260,7 +1284,7 @@ const dict = {
1260
1284
  },
1261
1285
  rules: {
1262
1286
  title: 'Username rules',
1263
- content: 'Usernames can only contain lowercase letters (a-z) and numbers (0-9). They must be at least 4 characters long. Special characters, spaces, and uppercase letters are not allowed to keep usernames simple and easy to remember.',
1287
+ content: 'Usernames are 3 to 30 characters long and use letters, numbers, hyphens (-) and underscores (_). They start and end with a letter or number, never put two of - or _ together, and are not only numbers. Spaces are not allowed. Capital letters are kept as you type them, but Alice and alice are the same username.',
1264
1288
  },
1265
1289
  unique: {
1266
1290
  title: 'Why must it be unique?',
@@ -1839,9 +1863,6 @@ const dict = {
1839
1863
  label: 'Username',
1840
1864
  available: 'Username is available',
1841
1865
  taken: 'Username is taken',
1842
- tooShort: 'Username must be at least 3 characters',
1843
- invalidChars: 'Only letters, numbers, hyphens, and underscores',
1844
- mustEndInBot: 'A bot account’s username must end in “bot”',
1845
1866
  checkFailed: 'Could not check availability',
1846
1867
  },
1847
1868
  accountCategory: {
@@ -1876,16 +1897,15 @@ const dict = {
1876
1897
  transfer: 'Transfer ownership',
1877
1898
  },
1878
1899
  errors: {
1879
- identifierRequired: 'Enter a username or email to invite',
1900
+ emailInvalid: 'Enter a valid email address, like name@example.com.',
1880
1901
  missingAccount: 'No account selected.',
1881
1902
  noPermission: 'You do not have permission to view members.',
1882
- userNotFound: 'User not found',
1883
1903
  },
1884
1904
  invite: {
1885
- title: 'Add a member',
1886
- identifierLabel: 'Username or email',
1905
+ title: 'Invite a member',
1906
+ emailLabel: 'Email',
1887
1907
  roleLabel: 'Role',
1888
- submit: 'Add member',
1908
+ submit: 'Send invitation',
1889
1909
  },
1890
1910
  removeConfirm: {
1891
1911
  title: 'Remove member',
@@ -1896,8 +1916,9 @@ const dict = {
1896
1916
  description: 'Transfer ownership of this account to this member? You will be demoted to admin. This cannot be undone.',
1897
1917
  },
1898
1918
  toasts: {
1899
- added: 'Member added',
1900
- addFailed: 'Failed to add member',
1919
+ invited: 'Invitation sent',
1920
+ inviteFailed: "Couldn't send the invitation. Please try again.",
1921
+ invitationUpdateFailed: "Couldn't update the invitation. Please try again.",
1901
1922
  removed: 'Member removed',
1902
1923
  removeFailed: 'Failed to remove member',
1903
1924
  roleUpdated: 'Role updated',
@@ -1959,6 +1980,7 @@ const dict = {
1959
1980
  title: 'Delete Account',
1960
1981
  warning: 'This action cannot be undone. Your account and all associated data will be permanently deleted.',
1961
1982
  confirmLabel: 'Type "{{username}}" to confirm',
1983
+ confirmMismatch: 'That isn\'t your username. Type "{{username}}" exactly.',
1962
1984
  confirm: 'Delete Forever',
1963
1985
  error: 'Failed to delete account',
1964
1986
  handoff: {
@@ -2373,6 +2395,7 @@ const dict = {
2373
2395
  request: "Couldn't start the verification. Check the domain and try again.",
2374
2396
  notYet: 'Not verified yet. Changes to DNS can take a while to show up.',
2375
2397
  remove: "Couldn't remove the domain",
2398
+ invalid: 'Enter just the domain, like example.com — no https://, slashes or spaces.',
2376
2399
  },
2377
2400
  remove: {
2378
2401
  title: 'Remove domain',
@@ -163,6 +163,9 @@ const dict = {
163
163
  requestExpired: 'Este inicio de sesión ha caducado. Envía un email nuevo.',
164
164
  secondFactorInvalid: 'Ese código no es correcto. Prueba con el actual.',
165
165
  originNotAllowed: 'Aquí no se puede iniciar sesión.',
166
+ network: 'No se ha podido conectar con Oxy. Comprueba tu conexión e inténtalo de nuevo.',
167
+ invalidInput: 'Algo de lo que has escrito no es válido. Revísalo e inténtalo de nuevo.',
168
+ server: 'Oxy no ha podido terminar esto ahora. Inténtalo de nuevo en un momento.',
166
169
  generic: 'Algo ha ido mal. Inténtalo de nuevo.',
167
170
  },
168
171
  subtitleToApp: 'para continuar en {{app}}',
@@ -225,6 +228,31 @@ const dict = {
225
228
  invalidDescription: 'Ha caducado o ya se ha usado. Pide un email nuevo en la app o escribe el código del último.',
226
229
  },
227
230
  },
231
+ actionErrors: {
232
+ updateProfile: 'No se ha podido actualizar tu perfil. Inténtalo de nuevo.',
233
+ uploadAvatar: 'No se ha podido subir la foto. Inténtalo de nuevo.',
234
+ updateSettings: 'No se han podido guardar tus ajustes. Inténtalo de nuevo.',
235
+ updatePrivacy: 'No se han podido guardar tus ajustes de privacidad. Inténtalo de nuevo.',
236
+ updateNotifications: 'No se han podido guardar tus preferencias de notificaciones. Inténtalo de nuevo.',
237
+ updatePreferences: 'No se han podido guardar tus preferencias. Inténtalo de nuevo.',
238
+ revokeConnectedApp: 'No se ha podido desconectar la app. Inténtalo de nuevo.',
239
+ switchSession: 'No se ha podido cambiar de cuenta. Inténtalo de nuevo.',
240
+ logout: 'No se ha podido cerrar la sesión. Inténtalo de nuevo.',
241
+ logoutAll: 'No se ha podido cerrar la sesión en todas partes. Inténtalo de nuevo.',
242
+ updateDeviceName: 'No se ha podido cambiar el nombre del dispositivo. Inténtalo de nuevo.',
243
+ removeDevice: 'No se ha podido quitar el dispositivo. Inténtalo de nuevo.',
244
+ },
245
+ usernamePolicy: {
246
+ hint: 'Letras, números, - y _. Sin espacios.',
247
+ required: 'Elige un nombre de usuario.',
248
+ tooShort: 'Usa al menos {{min}} caracteres.',
249
+ tooLong: 'Usa como máximo {{max}} caracteres.',
250
+ noSpaces: 'El nombre de usuario no puede tener espacios. Usa - o _ en su lugar.',
251
+ characters: 'Usa solo letras, números, - y _. Empieza y termina con una letra o un número, y no pongas dos - o _ seguidos.',
252
+ onlyNumbers: 'El nombre de usuario no puede ser solo números. Añade una letra.',
253
+ reserved: 'Ese nombre de usuario está reservado. Elige otro.',
254
+ mustEndInBot: 'El nombre de usuario de una cuenta bot debe terminar en «bot».',
255
+ },
228
256
  signup: {
229
257
  title: 'Crea tu cuenta',
230
258
  subtitle: 'Elige tu nombre de usuario. Después confirmamos tu email.',
@@ -233,7 +261,6 @@ const dict = {
233
261
  username: {
234
262
  label: 'Nombre de usuario',
235
263
  placeholder: 'tunombre',
236
- required: 'Elige un nombre de usuario.',
237
264
  taken: 'Ese nombre de usuario ya está en uso.',
238
265
  },
239
266
  email: {
@@ -305,8 +332,6 @@ const dict = {
305
332
  label: 'Usuario',
306
333
  placeholder: 'Elige un usuario',
307
334
  choose: 'Elige un usuario',
308
- required: 'El usuario es obligatorio',
309
- tooShort: 'El usuario debe tener al menos 3 caracteres',
310
335
  },
311
336
  email: {
312
337
  title: 'Correo electrónico',
@@ -904,7 +929,6 @@ const dict = {
904
929
  firstPlaceholder: 'Tu nombre',
905
930
  lastLabel: 'Apellidos',
906
931
  lastPlaceholder: 'Tus apellidos',
907
- saveFailed: 'No se pudo guardar tu nombre',
908
932
  },
909
933
  avatar: {
910
934
  title: 'Hazlo tuyo',
@@ -1260,7 +1284,7 @@ const dict = {
1260
1284
  },
1261
1285
  rules: {
1262
1286
  title: 'Reglas del nombre de usuario',
1263
- content: 'Los nombres de usuario solo pueden contener letras minúsculas (a-z) y números (0-9). Deben tener al menos 4 caracteres. No se permiten caracteres especiales, espacios ni mayúsculas para mantener los nombres de usuario simples y fáciles de recordar.',
1287
+ content: 'Los nombres de usuario tienen entre 3 y 30 caracteres y usan letras, números, guiones (-) y guiones bajos (_). Empiezan y terminan con una letra o un número, nunca llevan dos - o _ seguidos y no son solo números. No se permiten espacios. Las mayúsculas se guardan tal como las escribes, pero Alicia y alicia son el mismo nombre de usuario.',
1264
1288
  },
1265
1289
  unique: {
1266
1290
  title: '¿Por qué debe ser único?',
@@ -1839,9 +1863,6 @@ const dict = {
1839
1863
  label: 'Nombre de usuario',
1840
1864
  available: 'El nombre de usuario está disponible',
1841
1865
  taken: 'El nombre de usuario ya está en uso',
1842
- tooShort: 'El nombre de usuario debe tener al menos 3 caracteres',
1843
- invalidChars: 'Solo letras, números, guiones y guiones bajos',
1844
- mustEndInBot: 'El nombre de usuario de una cuenta de bot debe terminar en “bot”',
1845
1866
  checkFailed: 'No se pudo comprobar la disponibilidad',
1846
1867
  },
1847
1868
  accountCategory: {
@@ -1876,16 +1897,15 @@ const dict = {
1876
1897
  transfer: 'Transferir propiedad',
1877
1898
  },
1878
1899
  errors: {
1879
- identifierRequired: 'Introduce un nombre de usuario o correo para invitar',
1900
+ emailInvalid: 'Introduce un email válido, como nombre@ejemplo.com.',
1880
1901
  missingAccount: 'Ninguna cuenta seleccionada.',
1881
1902
  noPermission: 'No tienes permiso para ver los miembros.',
1882
- userNotFound: 'Usuario no encontrado',
1883
1903
  },
1884
1904
  invite: {
1885
- title: 'Añadir un miembro',
1886
- identifierLabel: 'Nombre de usuario o correo',
1905
+ title: 'Invitar a un miembro',
1906
+ emailLabel: 'Email',
1887
1907
  roleLabel: 'Rol',
1888
- submit: 'Añadir miembro',
1908
+ submit: 'Enviar invitación',
1889
1909
  },
1890
1910
  removeConfirm: {
1891
1911
  title: 'Eliminar miembro',
@@ -1896,8 +1916,9 @@ const dict = {
1896
1916
  description: '¿Transferir la propiedad de esta cuenta a este miembro? Pasarás a ser administrador. Esto no se puede deshacer.',
1897
1917
  },
1898
1918
  toasts: {
1899
- added: 'Miembro añadido',
1900
- addFailed: 'No se pudo añadir el miembro',
1919
+ invited: 'Invitación enviada',
1920
+ inviteFailed: 'No se ha podido enviar la invitación. Inténtalo de nuevo.',
1921
+ invitationUpdateFailed: 'No se ha podido actualizar la invitación. Inténtalo de nuevo.',
1901
1922
  removed: 'Miembro eliminado',
1902
1923
  removeFailed: 'No se pudo eliminar el miembro',
1903
1924
  roleUpdated: 'Rol actualizado',
@@ -1959,6 +1980,7 @@ const dict = {
1959
1980
  title: 'Eliminar cuenta',
1960
1981
  warning: 'Esta acción no se puede deshacer. Tu cuenta y todos los datos asociados se eliminarán de forma permanente.',
1961
1982
  confirmLabel: 'Escribe "{{username}}" para confirmar',
1983
+ confirmMismatch: 'Ese no es tu nombre de usuario. Escribe "{{username}}" exactamente.',
1962
1984
  confirm: 'Eliminar para siempre',
1963
1985
  error: 'No se pudo eliminar la cuenta',
1964
1986
  handoff: {
@@ -2373,6 +2395,7 @@ const dict = {
2373
2395
  request: 'No se pudo empezar la verificación. Revisa el dominio e inténtalo otra vez.',
2374
2396
  notYet: 'Aún no está verificado. Los cambios de DNS pueden tardar en aparecer.',
2375
2397
  remove: 'No se pudo quitar el dominio',
2398
+ invalid: 'Escribe solo el dominio, como ejemplo.com, sin https://, barras ni espacios.',
2376
2399
  },
2377
2400
  remove: {
2378
2401
  title: 'Quitar dominio',
@@ -53,6 +53,7 @@
53
53
  */
54
54
  Object.defineProperty(exports, "__esModule", { value: true });
55
55
  exports.OXY_IDENTITY_CACHE_PREFIXES = void 0;
56
+ exports.oxyUserByIdPath = oxyUserByIdPath;
56
57
  exports.oxyUserByIdCacheKey = oxyUserByIdCacheKey;
57
58
  exports.evictOxyIdentityCache = evictOxyIdentityCache;
58
59
  /**
@@ -69,12 +70,19 @@ exports.OXY_IDENTITY_CACHE_PREFIXES = [
69
70
  // `resolveProfile` — keyed by fediverse handle in the query payload.
70
71
  'GET:/profiles/resolve',
71
72
  ];
73
+ /**
74
+ * The path `getUserById` reads, its id encoded. Built in one place so the
75
+ * request and every key that evicts it cannot spell the id differently.
76
+ */
77
+ function oxyUserByIdPath(userId) {
78
+ return `/users/${encodeURIComponent(userId)}`;
79
+ }
72
80
  /**
73
81
  * Build the exact cache key `getUserById` reads under. The only identity key
74
82
  * derivable from a user id, so the only one that does not need a prefix sweep.
75
83
  */
76
84
  function oxyUserByIdCacheKey(userId) {
77
- return `GET:/users/${userId}`;
85
+ return `GET:${oxyUserByIdPath(userId)}`;
78
86
  }
79
87
  /**
80
88
  * Sweep an `OxyServices` GET response cache of everything that could carry the
@@ -81,7 +81,7 @@ export class ConnectedAppsApi {
81
81
  * `ConnectedApp.applicationId`, NOT a credential/client id).
82
82
  */
83
83
  async revoke(applicationId) {
84
- await this.ctx.request('DELETE', `/auth/grants/${applicationId}`, undefined, { cache: false });
84
+ await this.ctx.request('DELETE', `/auth/grants/${enc(applicationId)}`, undefined, { cache: false });
85
85
  this.ctx.oxy.cache.delete('GET:/auth/grants');
86
86
  }
87
87
  /** Resource-bound external MCP connections for the active Oxy account. */
@@ -932,7 +932,7 @@ export class AuthApi {
932
932
  */
933
933
  async checkUsername(username) {
934
934
  // Public availability lookup (pre-session) — skip the bearer preflight.
935
- return this.ctx.request('GET', `/auth/check-username/${username}`, undefined, {
935
+ return this.ctx.request('GET', `/auth/check-username/${encodeURIComponent(username)}`, undefined, {
936
936
  cache: false,
937
937
  skipAuth: true,
938
938
  });
@@ -10,6 +10,7 @@
10
10
  * arrive as ISO-8601 strings.
11
11
  */
12
12
  import { cancelProductSubscriptionCheck, parseContract, personalPlanCatalogueCheck, personalPlanCheckoutRequestCheck, personalPlanCheckoutResultCheck, productPlanCatalogueCheck, productPlanCheckoutRequestCheck, productSubscriptionCancellationResultCheck, productSubscriptionRenewalRequestCheck, productSubscriptionsResponseCheck, subjectProductAccessCheck, subjectProductAccessQueryCheck, subscriptionCreditGrantsResponseCheck, } from '@oxy.so/contracts/client';
13
+ const enc = encodeURIComponent;
13
14
  const SUBSCRIPTION_TTL = 2 * 60 * 1000;
14
15
  const WALLET_TTL = 60 * 1000;
15
16
  export class BillingApi {
@@ -73,7 +74,7 @@ export class BillingApi {
73
74
  /** A user's subscription (default: the signed-in user's). */
74
75
  async subscription(userId) {
75
76
  const id = this.resolveUserId(userId);
76
- return this.ctx.request('GET', `/subscription/${id}`, undefined, {
77
+ return this.ctx.request('GET', `/subscription/${enc(id)}`, undefined, {
77
78
  cache: true,
78
79
  cacheTTL: SUBSCRIPTION_TTL,
79
80
  });
@@ -81,7 +82,7 @@ export class BillingApi {
81
82
  /** A user's wallet (default: the signed-in user's). Cached briefly: the balance moves. */
82
83
  async wallet(userId) {
83
84
  const id = this.resolveUserId(userId);
84
- return this.ctx.request('GET', `/wallet/${id}`, undefined, {
85
+ return this.ctx.request('GET', `/wallet/${enc(id)}`, undefined, {
85
86
  cache: true,
86
87
  cacheTTL: WALLET_TTL,
87
88
  });
@@ -94,7 +95,7 @@ export class BillingApi {
94
95
  params.limit = options.limit;
95
96
  if (options.offset)
96
97
  params.offset = options.offset;
97
- return this.ctx.request('GET', `/wallet/transactions/${id}`, params, {
98
+ return this.ctx.request('GET', `/wallet/transactions/${enc(id)}`, params, {
98
99
  cache: false,
99
100
  });
100
101
  }
@@ -1,6 +1,7 @@
1
1
  import { checkContract, deviceBackgroundCredentialResponseCheck, deviceJoinCodeResponseCheck, deviceJoinResponseCheck, deviceRegisterResponseCheck, deviceTokenMintResponseCheck, } from '@oxy.so/contracts/client';
2
2
  import { logger } from '../logger/index.js';
3
3
  import { extractErrorStatus } from '../utils/errorUtils.js';
4
+ const enc = encodeURIComponent;
4
5
  /**
5
6
  * The server's `401 account_not_on_device` for a PINNED mint: the requested
6
7
  * `accountId` is not (or is no longer) a live account of this device session.
@@ -71,7 +72,7 @@ export class DevicesApi {
71
72
  * @param deviceId - The device ID to remove
72
73
  */
73
74
  async remove(deviceId) {
74
- await this.ctx.request('DELETE', `/devices/${deviceId}`, undefined, { cache: false });
75
+ await this.ctx.request('DELETE', `/devices/${enc(deviceId)}`, undefined, { cache: false });
75
76
  }
76
77
  /**
77
78
  * Get device sessions for a given session ID
@@ -81,7 +82,7 @@ export class DevicesApi {
81
82
  */
82
83
  async sessions(sessionId) {
83
84
  // Cache disabled by default to ensure fresh session data
84
- return this.ctx.request('GET', `/session/device/sessions/${sessionId}`, undefined, {
85
+ return this.ctx.request('GET', `/session/device/sessions/${enc(sessionId)}`, undefined, {
85
86
  cache: false, // Don't cache sessions - always get fresh data
86
87
  deduplicate: true, // Deduplicate concurrent requests for same sessionId
87
88
  });
@@ -99,7 +100,7 @@ export class DevicesApi {
99
100
  urlParams.deviceId = deviceId;
100
101
  if (excludeCurrent)
101
102
  urlParams.excludeCurrent = 'true';
102
- return this.ctx.request('POST', `/session/device/logout-all/${sessionId}`, urlParams, { cache: false });
103
+ return this.ctx.request('POST', `/session/device/logout-all/${enc(sessionId)}`, urlParams, { cache: false });
103
104
  }
104
105
  /**
105
106
  * Update device name
@@ -108,7 +109,7 @@ export class DevicesApi {
108
109
  * @returns Updated device object
109
110
  */
110
111
  async rename(sessionId, deviceName) {
111
- return this.ctx.request('PUT', `/session/device/name/${sessionId}`, { deviceName }, { cache: false });
112
+ return this.ctx.request('PUT', `/session/device/name/${enc(sessionId)}`, { deviceName }, { cache: false });
112
113
  }
113
114
  /**
114
115
  * Get security information
@@ -1,4 +1,5 @@
1
1
  import { buildPaginationParams, buildQueryParams, buildUrl, } from '../utils/apiUtils.js';
2
+ import { oxyUserByIdCacheKey, oxyUserByIdPath } from '../utils/identityCacheSweep.js';
2
3
  const GRAPH_TTL = 2 * 60 * 1000;
3
4
  const STATUS_TTL = 60 * 1000;
4
5
  /**
@@ -13,7 +14,7 @@ export class FollowsApi {
13
14
  // ── People ───────────────────────────────────────────────────────────────
14
15
  /** Follow a user. */
15
16
  async follow(userId) {
16
- const result = await this.ctx.request('POST', `/users/${userId}/follow`, undefined, { cache: false });
17
+ const result = await this.ctx.request('POST', `${oxyUserByIdPath(userId)}/follow`, undefined, { cache: false });
17
18
  this.invalidate([userId]);
18
19
  return result;
19
20
  }
@@ -27,7 +28,7 @@ export class FollowsApi {
27
28
  }
28
29
  /** Unfollow a user. */
29
30
  async unfollow(userId) {
30
- const result = await this.ctx.request('DELETE', `/users/${userId}/follow`, undefined, { cache: false });
31
+ const result = await this.ctx.request('DELETE', `${oxyUserByIdPath(userId)}/follow`, undefined, { cache: false });
31
32
  this.invalidate([userId]);
32
33
  return result;
33
34
  }
@@ -41,7 +42,7 @@ export class FollowsApi {
41
42
  }
42
43
  /** Whether the signed-in user follows `userId`. Cached 1 minute, busted by every write. */
43
44
  async status(userId) {
44
- return this.ctx.request('GET', `/users/${userId}/follow-status`, undefined, {
45
+ return this.ctx.request('GET', `${oxyUserByIdPath(userId)}/follow-status`, undefined, {
45
46
  cache: true,
46
47
  cacheTTL: STATUS_TTL,
47
48
  });
@@ -71,12 +72,12 @@ export class FollowsApi {
71
72
  * `limit`/`offset`/`sort` combination is its own cache entry (2 minutes).
72
73
  */
73
74
  async followers(userId, params) {
74
- const { data, total, hasMore } = await this.userPage(`/users/${userId}/followers`, params);
75
+ const { data, total, hasMore } = await this.userPage(`${oxyUserByIdPath(userId)}/followers`, params);
75
76
  return { followers: data, total, hasMore };
76
77
  }
77
78
  /** Who a user follows. `sort` as in `followers`. */
78
79
  async following(userId, params) {
79
- const { data, total, hasMore } = await this.userPage(`/users/${userId}/following`, params);
80
+ const { data, total, hasMore } = await this.userPage(`${oxyUserByIdPath(userId)}/following`, params);
80
81
  return { following: data, total, hasMore };
81
82
  }
82
83
  /**
@@ -84,7 +85,7 @@ export class FollowsApi {
84
85
  * `userId`.
85
86
  */
86
87
  async mutuals(userId, params) {
87
- const { data, total, hasMore } = await this.userPage(`/users/${userId}/mutuals`, params);
88
+ const { data, total, hasMore } = await this.userPage(`${oxyUserByIdPath(userId)}/mutuals`, params);
88
89
  return { mutuals: data, total, hasMore };
89
90
  }
90
91
  /**
@@ -219,16 +220,17 @@ export class FollowsApi {
219
220
  const keys = ['GET:/users/me/graph'];
220
221
  const prefixes = ['GET:/profiles/username/', 'GET:/profiles/resolve'];
221
222
  for (const id of targetUserIds) {
222
- keys.push(`GET:/users/${id}/follow-status`);
223
+ const user = oxyUserByIdPath(id);
224
+ keys.push(`GET:${user}/follow-status`);
223
225
  // Profiles embed the viewer-relative `relationship`.
224
- keys.push(`GET:/users/${id}`);
226
+ keys.push(oxyUserByIdCacheKey(id));
225
227
  // The target gained or lost a follower, and "followers you know" with it.
226
- prefixes.push(`GET:/users/${id}/followers`, `GET:/users/${id}/mutuals`);
228
+ prefixes.push(`GET:${user}/followers`, `GET:${user}/mutuals`);
227
229
  }
228
230
  // The viewer's own following list changed.
229
231
  const viewerId = this.ctx.oxy.session.userId;
230
232
  if (viewerId)
231
- prefixes.push(`GET:/users/${viewerId}/following`);
233
+ prefixes.push(`GET:${oxyUserByIdPath(viewerId)}/following`);
232
234
  this.ctx.http.invalidateCache({ keys, prefixes });
233
235
  }
234
236
  }
@@ -1,4 +1,5 @@
1
1
  import { evictOxyIdentityCache } from '../utils/identityCacheSweep.js';
2
+ const enc = encodeURIComponent;
2
3
  const LIST_TTL = 60 * 1000;
3
4
  const SETTINGS_TTL = 2 * 60 * 1000;
4
5
  const refId = (ref) => (typeof ref === 'string' ? ref : ref._id);
@@ -10,7 +11,7 @@ export class PrivacyApi {
10
11
  /** A user's privacy settings (default: the signed-in user's). */
11
12
  async settings(userId) {
12
13
  const id = await this.resolveUserId(userId);
13
- return this.ctx.request('GET', `/privacy/${id}/privacy`, undefined, {
14
+ return this.ctx.request('GET', `/privacy/${enc(id)}/privacy`, undefined, {
14
15
  cache: true,
15
16
  cacheTTL: SETTINGS_TTL,
16
17
  });
@@ -18,10 +19,10 @@ export class PrivacyApi {
18
19
  /** Update privacy settings (default: the signed-in user's). */
19
20
  async updateSettings(settings, userId) {
20
21
  const id = await this.resolveUserId(userId);
21
- const res = await this.ctx.request('PATCH', `/privacy/${id}/privacy`, settings, { cache: false });
22
+ const res = await this.ctx.request('PATCH', `/privacy/${enc(id)}/privacy`, settings, { cache: false });
22
23
  // Privacy settings ride the user DTO, so every identity read goes stale too.
23
24
  evictOxyIdentityCache(this.ctx.http, id);
24
- this.ctx.oxy.cache.delete(`GET:/privacy/${id}/privacy`);
25
+ this.ctx.oxy.cache.delete(`GET:/privacy/${enc(id)}/privacy`);
25
26
  return res;
26
27
  }
27
28
  // ── Blocked ──────────────────────────────────────────────────────────────
@@ -35,14 +36,14 @@ export class PrivacyApi {
35
36
  /** Block a user. */
36
37
  async block(userId) {
37
38
  requireId(userId);
38
- const res = await this.ctx.request('POST', `/privacy/blocked/${userId}`, undefined, { cache: false });
39
+ const res = await this.ctx.request('POST', `/privacy/blocked/${enc(userId)}`, undefined, { cache: false });
39
40
  this.invalidate('GET:/privacy/blocked');
40
41
  return res;
41
42
  }
42
43
  /** Unblock a user. */
43
44
  async unblock(userId) {
44
45
  requireId(userId);
45
- const res = await this.ctx.request('DELETE', `/privacy/blocked/${userId}`, undefined, { cache: false });
46
+ const res = await this.ctx.request('DELETE', `/privacy/blocked/${enc(userId)}`, undefined, { cache: false });
46
47
  this.invalidate('GET:/privacy/blocked');
47
48
  return res;
48
49
  }
@@ -68,14 +69,14 @@ export class PrivacyApi {
68
69
  /** Restrict a user: limit their interactions without blocking them. */
69
70
  async restrict(userId) {
70
71
  requireId(userId);
71
- const res = await this.ctx.request('POST', `/privacy/restricted/${userId}`, undefined, { cache: false });
72
+ const res = await this.ctx.request('POST', `/privacy/restricted/${enc(userId)}`, undefined, { cache: false });
72
73
  this.invalidate('GET:/privacy/restricted');
73
74
  return res;
74
75
  }
75
76
  /** Lift a restriction. */
76
77
  async unrestrict(userId) {
77
78
  requireId(userId);
78
- const res = await this.ctx.request('DELETE', `/privacy/restricted/${userId}`, undefined, { cache: false });
79
+ const res = await this.ctx.request('DELETE', `/privacy/restricted/${enc(userId)}`, undefined, { cache: false });
79
80
  this.invalidate('GET:/privacy/restricted');
80
81
  return res;
81
82
  }
@@ -9,6 +9,7 @@ import { jwtDecode } from 'jwt-decode';
9
9
  import { normalizeUserIdentity } from '../utils/userIdentity.js';
10
10
  import { OxyApiError } from '../OxyServices.errors.js';
11
11
  import { checkContract, holderContextEvidenceV1ResponseCheck } from '@oxy.so/contracts/client';
12
+ const enc = encodeURIComponent;
12
13
  /** How long a refusal (disabled, rate limited, unreachable) is remembered per installation. */
13
14
  const EVIDENCE_REFUSAL_MS = 5000;
14
15
  const unavailable = (reason) => ({
@@ -290,7 +291,7 @@ export class SessionApi {
290
291
  : {}),
291
292
  },
292
293
  })
293
- : await this.ctx.request('GET', `/session/validate/${sessionId}`, {
294
+ : await this.ctx.request('GET', `/session/validate/${enc(sessionId)}`, {
294
295
  ...(options.deviceFingerprint
295
296
  ? { deviceFingerprint: options.deviceFingerprint }
296
297
  : {}),
@@ -317,7 +318,7 @@ export class SessionApi {
317
318
  const invalid = candidate?.status === 401 && candidate.code === 'OXY_LIVE_SESSION_INVALID_V1';
318
319
  const denied = candidate?.status === 403 && candidate.code === 'OXY_LIVE_SESSION_POLICY_V1';
319
320
  if (invalid)
320
- this.ctx.oxy.cache.delete(`GET:/session/user/${sessionId}`);
321
+ this.ctx.oxy.cache.delete(`GET:/session/user/${enc(sessionId)}`);
321
322
  if (invalid || denied)
322
323
  throw error;
323
324
  throw new OxyApiError('Live session authority is unavailable', {
@@ -327,26 +328,28 @@ export class SessionApi {
327
328
  });
328
329
  }
329
330
  // The session is gone: drop any user cached for it (#196).
330
- this.ctx.oxy.cache.delete(`GET:/session/user/${sessionId}`);
331
+ this.ctx.oxy.cache.delete(`GET:/session/user/${enc(sessionId)}`);
331
332
  throw error;
332
333
  }
333
334
  }
334
335
  /** The sessions that belong with `sessionId` (same account set). */
335
336
  async list(sessionId) {
336
- return this.ctx.request('GET', `/session/sessions/${sessionId}`, undefined, {
337
+ return this.ctx.request('GET', `/session/sessions/${enc(sessionId)}`, undefined, {
337
338
  cache: false,
338
339
  });
339
340
  }
340
341
  /** Sign out `sessionId`, or — with `targetSessionId` — one of the sessions beside it. */
341
342
  async logout(sessionId, targetSessionId) {
342
343
  const url = targetSessionId
343
- ? `/session/logout/${sessionId}/${targetSessionId}`
344
- : `/session/logout/${sessionId}`;
344
+ ? `/session/logout/${enc(sessionId)}/${enc(targetSessionId)}`
345
+ : `/session/logout/${enc(sessionId)}`;
345
346
  await this.ctx.request('POST', url, undefined, { cache: false });
346
347
  }
347
348
  /** Sign out the other sessions for this account; preserve `sessionId`. */
348
349
  async logoutAll(sessionId) {
349
- await this.ctx.request('POST', `/session/logout-all/${sessionId}`, undefined, { cache: false });
350
+ await this.ctx.request('POST', `/session/logout-all/${enc(sessionId)}`, undefined, {
351
+ cache: false,
352
+ });
350
353
  }
351
354
  /** Decoded claims of the current token, memoised per token string. */
352
355
  claims() {
@@ -1,3 +1,4 @@
1
+ const enc = encodeURIComponent;
1
2
  const SHORT = 60 * 1000;
2
3
  const LONG = 5 * 60 * 1000;
3
4
  const EXTRA_LONG = 30 * 60 * 1000;
@@ -41,7 +42,7 @@ export class TopicsApi {
41
42
  }
42
43
  /** One topic by slug. */
43
44
  async get(slug) {
44
- return this.ctx.request('GET', `/topics/${slug}`, undefined, {
45
+ return this.ctx.request('GET', `/topics/${enc(slug)}`, undefined, {
45
46
  cache: true,
46
47
  cacheTTL: LONG,
47
48
  });
@@ -60,10 +61,10 @@ export class TopicsApi {
60
61
  }
61
62
  /** Update a topic's description and translations. Staff or service only. */
62
63
  async update(slug, data) {
63
- const topic = await this.ctx.request('PATCH', `/topics/${slug}`, data, {
64
+ const topic = await this.ctx.request('PATCH', `/topics/${enc(slug)}`, data, {
64
65
  cache: false,
65
66
  });
66
- this.ctx.oxy.cache.delete(`GET:/topics/${slug}`);
67
+ this.ctx.oxy.cache.delete(`GET:/topics/${enc(slug)}`);
67
68
  return topic;
68
69
  }
69
70
  }