@oxyhq/core 3.10.0 → 3.10.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 (35) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/i18n/locales/en-US.json +9 -0
  3. package/dist/cjs/i18n/locales/es-ES.json +9 -0
  4. package/dist/cjs/i18n/locales/locales/en-US.json +9 -0
  5. package/dist/cjs/i18n/locales/locales/es-ES.json +9 -0
  6. package/dist/cjs/index.js +2 -3
  7. package/dist/cjs/mixins/OxyServices.assets.js +29 -6
  8. package/dist/cjs/mixins/OxyServices.utility.js +52 -23
  9. package/dist/cjs/utils/fapiAutoDetect.js +12 -42
  10. package/dist/esm/.tsbuildinfo +1 -1
  11. package/dist/esm/i18n/locales/en-US.json +9 -0
  12. package/dist/esm/i18n/locales/es-ES.json +9 -0
  13. package/dist/esm/i18n/locales/locales/en-US.json +9 -0
  14. package/dist/esm/i18n/locales/locales/es-ES.json +9 -0
  15. package/dist/esm/index.js +1 -1
  16. package/dist/esm/mixins/OxyServices.assets.js +29 -6
  17. package/dist/esm/mixins/OxyServices.utility.js +52 -23
  18. package/dist/esm/utils/fapiAutoDetect.js +12 -41
  19. package/dist/types/.tsbuildinfo +1 -1
  20. package/dist/types/index.d.ts +1 -1
  21. package/dist/types/mixins/OxyServices.assets.d.ts +6 -1
  22. package/dist/types/mixins/OxyServices.utility.d.ts +3 -3
  23. package/dist/types/utils/fapiAutoDetect.d.ts +6 -23
  24. package/package.json +2 -1
  25. package/src/__tests__/authSocket.test.ts +96 -0
  26. package/src/i18n/locales/en-US.json +9 -0
  27. package/src/i18n/locales/es-ES.json +9 -0
  28. package/src/index.ts +1 -1
  29. package/src/mixins/OxyServices.assets.ts +40 -6
  30. package/src/mixins/OxyServices.utility.ts +57 -23
  31. package/src/mixins/__tests__/assetUpload.test.ts +191 -0
  32. package/src/mixins/__tests__/getFileDownloadUrl.test.ts +13 -0
  33. package/src/mixins/__tests__/serviceAuth.test.ts +30 -2
  34. package/src/utils/__tests__/fapiAutoDetect.test.ts +40 -11
  35. package/src/utils/fapiAutoDetect.ts +12 -39
@@ -129,6 +129,15 @@
129
129
  "title": "Reputation = Trust & Growth",
130
130
  "body": "Oxy Trust is a reputation system that reacts to what you do. Helpful, respectful, constructive actions earn it. Harmful or low‑effort stuff chips it away. More reputation can unlock benefits; low reputation can limit features. It keeps things fair and rewards real contribution."
131
131
  },
132
+ "name": {
133
+ "title": "What's your name?",
134
+ "body": "Add your name so people know who you are.",
135
+ "firstLabel": "First name",
136
+ "firstPlaceholder": "Your first name",
137
+ "lastLabel": "Last name",
138
+ "lastPlaceholder": "Your last name",
139
+ "saveFailed": "Could not save your name"
140
+ },
132
141
  "avatar": {
133
142
  "title": "Make It Yours",
134
143
  "body": "Add an avatar so people recognize you. It will show anywhere you show up here. Skip if you want — you can add it later.",
@@ -849,6 +849,15 @@
849
849
  "title": "Reputación = Confianza y crecimiento",
850
850
  "body": "Oxy Trust es un sistema de reputación que reacciona a lo que haces. Las acciones útiles, respetuosas y constructivas la aumentan. Las acciones dañinas o de poco esfuerzo la reducen. Más reputación puede desbloquear beneficios; poca reputación puede limitar funciones. Mantiene la justicia y recompensa la contribución real."
851
851
  },
852
+ "name": {
853
+ "title": "¿Cuál es tu nombre?",
854
+ "body": "Añade tu nombre para que la gente sepa quién eres.",
855
+ "firstLabel": "Nombre",
856
+ "firstPlaceholder": "Tu nombre",
857
+ "lastLabel": "Apellidos",
858
+ "lastPlaceholder": "Tus apellidos",
859
+ "saveFailed": "No se pudo guardar tu nombre"
860
+ },
852
861
  "avatar": {
853
862
  "title": "Hazlo tuyo",
854
863
  "body": "Añade un avatar para que te reconozcan. Se mostrará donde aparezcas aquí. Puedes omitirlo — puedes añadirlo más tarde.",
@@ -129,6 +129,15 @@
129
129
  "title": "Reputation = Trust & Growth",
130
130
  "body": "Oxy Trust is a reputation system that reacts to what you do. Helpful, respectful, constructive actions earn it. Harmful or low‑effort stuff chips it away. More reputation can unlock benefits; low reputation can limit features. It keeps things fair and rewards real contribution."
131
131
  },
132
+ "name": {
133
+ "title": "What's your name?",
134
+ "body": "Add your name so people know who you are.",
135
+ "firstLabel": "First name",
136
+ "firstPlaceholder": "Your first name",
137
+ "lastLabel": "Last name",
138
+ "lastPlaceholder": "Your last name",
139
+ "saveFailed": "Could not save your name"
140
+ },
132
141
  "avatar": {
133
142
  "title": "Make It Yours",
134
143
  "body": "Add an avatar so people recognize you. It will show anywhere you show up here. Skip if you want — you can add it later.",
@@ -849,6 +849,15 @@
849
849
  "title": "Reputación = Confianza y crecimiento",
850
850
  "body": "Oxy Trust es un sistema de reputación que reacciona a lo que haces. Las acciones útiles, respetuosas y constructivas la aumentan. Las acciones dañinas o de poco esfuerzo la reducen. Más reputación puede desbloquear beneficios; poca reputación puede limitar funciones. Mantiene la justicia y recompensa la contribución real."
851
851
  },
852
+ "name": {
853
+ "title": "¿Cuál es tu nombre?",
854
+ "body": "Añade tu nombre para que la gente sepa quién eres.",
855
+ "firstLabel": "Nombre",
856
+ "firstPlaceholder": "Tu nombre",
857
+ "lastLabel": "Apellidos",
858
+ "lastPlaceholder": "Tus apellidos",
859
+ "saveFailed": "No se pudo guardar tu nombre"
860
+ },
852
861
  "avatar": {
853
862
  "title": "Hazlo tuyo",
854
863
  "body": "Añade un avatar para que te reconozcan. Se mostrará donde aparezcas aquí. Puedes omitirlo — puedes añadirlo más tarde.",
package/dist/esm/index.js CHANGED
@@ -104,7 +104,7 @@ export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccou
104
104
  // ---------------------------------------------------------------------------
105
105
  // Cross-domain SSO infrastructure
106
106
  // ---------------------------------------------------------------------------
107
- export { autoDetectAuthWebUrl, registrableApex, MULTIPART_TLDS } from './utils/fapiAutoDetect.js';
107
+ export { autoDetectAuthWebUrl, registrableApex } from './utils/fapiAutoDetect.js';
108
108
  // Central cross-domain SSO (opaque single-use code bounce via auth.oxy.so)
109
109
  export { CENTRAL_AUTH_URL, CENTRAL_IDP_APEX, resolveCentralAuthUrl } from './utils/authWebUrl.js';
110
110
  export { parseSsoReturnFragment, consumeSsoReturn } from './utils/ssoReturn.js';
@@ -1,3 +1,4 @@
1
+ import { isReactNative } from '../utils/platform.js';
1
2
  export function OxyServicesAssetsMixin(Base) {
2
3
  return class extends Base {
3
4
  constructor(...args) {
@@ -40,8 +41,8 @@ export function OxyServicesAssetsMixin(Base) {
40
41
  *
41
42
  * For a CDN-signed URL fetched from the API, use {@link getFileDownloadUrlAsync}.
42
43
  */
43
- getFileDownloadUrl(fileId, variant, expiresIn) {
44
- const token = this.getClient().getAccessToken();
44
+ getFileDownloadUrl(fileId, variant, expiresIn, options = {}) {
45
+ const token = options.omitToken ? undefined : this.getClient().getAccessToken();
45
46
  // Public case: no auth token and no expiry requested → clean CDN URL.
46
47
  // CloudFront serves the public media origin under `${cloudURL}/<id>`.
47
48
  if (!token && !expiresIn) {
@@ -189,10 +190,32 @@ export function OxyServicesAssetsMixin(Base) {
189
190
  formData.append('file', file, fileName);
190
191
  }
191
192
  else if ('uri' in file && typeof file.uri === 'string') {
192
- // React Native file descriptor — RN's FormData handles {uri, type, name} natively.
193
- // It reads the file from disk during the multipart request — no in-JS Blob
194
- // conversion (which would fail on Hermes for ArrayBuffer-backed Blobs).
195
- formData.append('file', file, fileName);
193
+ const descriptor = file;
194
+ if (isReactNative()) {
195
+ // React Native file descriptor RN's FormData handles {uri, type, name} natively.
196
+ // It reads the file from disk during the multipart request — no in-JS Blob
197
+ // conversion (which would fail on Hermes for ArrayBuffer-backed Blobs).
198
+ formData.append('file', descriptor, fileName);
199
+ }
200
+ else {
201
+ // Web (browser/Node): the browser's FormData cannot read bytes from a plain
202
+ // { uri } object — it would serialize "[object Object]" and the server would
203
+ // store a 0-byte asset. Materialize the uri into a real Blob first. `fetch`
204
+ // resolves blob:, data:, and http(s): uris on web, so all picker outputs work.
205
+ const res = await fetch(descriptor.uri);
206
+ if (!res.ok) {
207
+ throw new Error(`Failed to read file from uri (status ${res.status})`);
208
+ }
209
+ const fetched = await res.blob();
210
+ // Preserve the descriptor's declared MIME type when the fetched blob has none.
211
+ const blob = fetched.type === '' && descriptor.type
212
+ ? new Blob([fetched], { type: descriptor.type })
213
+ : fetched;
214
+ if (blob.size === 0) {
215
+ throw new Error('Cannot upload an empty file');
216
+ }
217
+ formData.append('file', blob, fileName);
218
+ }
196
219
  }
197
220
  else {
198
221
  throw new Error('Unsupported file input: expected File, Blob, or { uri, type?, name?, size? } descriptor');
@@ -688,33 +688,45 @@ export function OxyServicesUtilityMixin(Base) {
688
688
  }
689
689
  return next(new Error('Invalid token'));
690
690
  }
691
- const userId = decoded.userId || decoded.id;
692
- if (!userId) {
691
+ const claimedUserId = decoded.userId || decoded.id;
692
+ if (!claimedUserId) {
693
693
  return next(new Error('Invalid token payload'));
694
694
  }
695
695
  // Check expiration — reject tokens at exact expiry second (use <=)
696
696
  if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
697
697
  return next(new Error('Token expired'));
698
698
  }
699
- // Validate session if available
700
- if (decoded.sessionId) {
701
- try {
702
- const result = await oxyInstance.validateSession(decoded.sessionId, {
703
- useHeaderValidation: true,
704
- });
705
- if (!result || !result.valid) {
706
- return next(new Error('Session invalid'));
707
- }
699
+ // A server-validated session is mandatory. A bare decoded JWT proves
700
+ // nothing — the signature is not verified here, so without a session
701
+ // round-trip a forged token could claim any user id.
702
+ if (!decoded.sessionId) {
703
+ return next(new Error('Session required'));
704
+ }
705
+ let userId = claimedUserId;
706
+ try {
707
+ const result = await oxyInstance.validateSession(decoded.sessionId, {
708
+ useHeaderValidation: true,
709
+ });
710
+ if (!result || !result.valid || !result.user) {
711
+ return next(new Error('Session invalid'));
708
712
  }
709
- catch (validateErr) {
710
- if (debug) {
711
- logger.debug('[oxy.authSocket] Session validation failed', {
712
- component: 'auth',
713
- method: 'authSocket',
714
- }, validateErr);
715
- }
716
- return next(new Error('Session validation failed'));
713
+ // The session is the source of truth. The client-claimed user id
714
+ // must match the server-validated identity, otherwise a valid
715
+ // session could be paired with a forged user id.
716
+ const validatedUserId = getUserIdentityId(result.user);
717
+ if (!validatedUserId || validatedUserId !== claimedUserId) {
718
+ return next(new Error('Session user mismatch'));
717
719
  }
720
+ userId = validatedUserId;
721
+ }
722
+ catch (validateErr) {
723
+ if (debug) {
724
+ logger.debug('[oxy.authSocket] Session validation failed', {
725
+ component: 'auth',
726
+ method: 'authSocket',
727
+ }, validateErr);
728
+ }
729
+ return next(new Error('Session validation failed'));
718
730
  }
719
731
  // Attach user data to socket. We expose BOTH `socket.data.userId`
720
732
  // (the official Socket.IO data slot) and `socket.user` because
@@ -779,9 +791,9 @@ export function OxyServicesUtilityMixin(Base) {
779
791
  * Express.js middleware that enforces a specific service-token scope.
780
792
  *
781
793
  * Mount AFTER `auth()` / `serviceAuth()` — relies on `req.serviceApp` and
782
- * (when delegation is in effect) `req.serviceActingAs.scopes`. The scope
783
- * is granted if EITHER list contains it, mirroring the OAuth2 model where
784
- * the app's app-level scopes and the per-user delegated scopes both count.
794
+ * (when delegation is in effect) `req.serviceActingAs.scopes`. App-only
795
+ * service requests require the app scope. Delegated user requests require
796
+ * BOTH the app scope and the per-user delegation scope.
785
797
  *
786
798
  * Requests authenticated as a regular user (no service token) are rejected
787
799
  * with 403 — scope-protected endpoints are service-to-service by design.
@@ -811,7 +823,12 @@ export function OxyServicesUtilityMixin(Base) {
811
823
  });
812
824
  return;
813
825
  }
814
- if (appScopes.includes(scope) || delegatedScopes.includes(scope)) {
826
+ const appHasScope = appScopes.includes(scope);
827
+ const delegationHasScope = delegatedScopes.includes(scope);
828
+ const hasRequiredScope = req.serviceActingAs
829
+ ? appHasScope && delegationHasScope
830
+ : appHasScope;
831
+ if (hasRequiredScope) {
815
832
  next();
816
833
  return;
817
834
  }
@@ -869,6 +886,18 @@ async function verifyServiceTokenSignature(token, secret) {
869
886
  * access token signed by the same shared secret could be replayed as a
870
887
  * service token because no claim binding existed.
871
888
  */
889
+ /**
890
+ * Resolve the canonical user id from a validated session's user object.
891
+ *
892
+ * The API serializer emits `id`, but some upstream shapes carry the raw Mongo
893
+ * `_id` instead. We accept either, but only a non-empty string — anything else
894
+ * means the validated identity is unusable and the caller must reject.
895
+ */
896
+ function getUserIdentityId(user) {
897
+ const candidate = user.id
898
+ ?? user._id;
899
+ return typeof candidate === 'string' && candidate.length > 0 ? candidate : null;
900
+ }
872
901
  function verifyServiceTokenClaims(decoded, expected) {
873
902
  if (decoded.type !== 'service') {
874
903
  throw new ServiceTokenClaimError(`Service token has unexpected type '${String(decoded.type)}'`);
@@ -20,10 +20,9 @@
20
20
  * - SSR / non-browser (no `window`).
21
21
  * - `localhost`, `127.0.0.1`, IPv4/IPv6 literals.
22
22
  * - Hostnames with fewer than two labels.
23
- * - Hostnames whose trailing two labels form a known multi-part public
24
- * suffix (e.g. `co.uk`), where the naive `labels.slice(-2)` apex would be
25
- * an attacker-registrable suffix like `auth.co.uk` rather than the real
26
- * registrable domain.
23
+ * - Hostnames where a registrable domain cannot be determined from the
24
+ * Public Suffix List, including private hosted suffixes such as
25
+ * `github.io`, `pages.dev`, and `netlify.app`.
27
26
  *
28
27
  * When the page is already loaded ON the IdP itself (`auth.<anything>`),
29
28
  * the helper returns the current origin so the SDK keeps everything
@@ -34,33 +33,10 @@
34
33
  * (`packages/auth/server/index.ts`), so an honest CNAME pair is all that
35
34
  * is required for end-to-end FedCM correctness — no per-RP config.
36
35
  */
36
+ import { getDomain } from 'tldts';
37
37
  /**
38
- * Known multi-part public suffixes where the registrable domain is the LAST
39
- * THREE labels, not two. Deriving an apex from `labels.slice(-2)` against any
40
- * of these would yield an attacker-registrable suffix (e.g. `auth.co.uk`),
41
- * so we bail out instead.
42
- *
43
- * This is intentionally a small, explicit allow-list rather than the full
44
- * Public Suffix List — it covers the suffixes the Oxy ecosystem's RPs use.
45
- * Any multi-part-TLD RP MUST extend this set (or wire in a proper PSL check)
46
- * before relying on this helper, otherwise auto-detection silently bails to
47
- * `undefined` and the consumer must pass `authWebUrl` explicitly.
48
- */
49
- export const MULTIPART_TLDS = new Set([
50
- 'co.uk',
51
- 'com.au',
52
- 'co.jp',
53
- 'co.nz',
54
- 'com.br',
55
- 'co.za',
56
- 'com.mx',
57
- 'co.in',
58
- 'co.kr',
59
- 'com.sg',
60
- ]);
61
- /**
62
- * Compute the bare registrable apex (eTLD+1) of a hostname, guarding against
63
- * multi-part public suffixes.
38
+ * Compute the bare registrable apex (eTLD+1) of a hostname using the Public
39
+ * Suffix List, including private hosted suffixes.
64
40
  *
65
41
  * This is the pure host-handling kernel shared by {@link autoDetectAuthWebUrl}
66
42
  * and the IdP worker — it performs NO protocol handling, NO `auth.` prefixing,
@@ -72,10 +48,7 @@ export const MULTIPART_TLDS = new Set([
72
48
  * - IPv4 literals (`192.168.1.10`);
73
49
  * - IPv6 literals or any host carrying a port (`[::1]`, anything with `:`);
74
50
  * - single-label hosts (`intranet`, `localhost`);
75
- * - hosts whose trailing two labels form a known multi-part public suffix
76
- * (e.g. `foo.co.uk`), where `labels.slice(-2)` would yield an
77
- * attacker-registrable suffix (`co.uk`) rather than a real registrable
78
- * domain. Such hosts MUST configure `authWebUrl` explicitly.
51
+ * - public suffixes without a registrable label (e.g. `co.uk`, `github.io`).
79
52
  *
80
53
  * @param hostname - A bare hostname (no scheme), e.g. `www.mention.earth`.
81
54
  * @returns The eTLD+1 (`mention.earth`), or `null` when undefinable.
@@ -90,13 +63,11 @@ export function registrableApex(hostname) {
90
63
  // yields a registrable apex.
91
64
  if (host.startsWith('[') || host.includes(':'))
92
65
  return null;
93
- const labels = host.split('.');
94
- if (labels.length < 2)
95
- return null;
96
- const lastTwo = labels.slice(-2).join('.');
97
- if (MULTIPART_TLDS.has(lastTwo))
98
- return null;
99
- return lastTwo;
66
+ // The Public Suffix List (private suffixes included) is the source of truth
67
+ // for what an attacker can register. A multi-part public suffix like `co.uk`
68
+ // or a hosted suffix like `github.io` returns no registrable domain, so
69
+ // deriving `auth.<apex>` against it is impossible — `getDomain` returns null.
70
+ return getDomain(host, { allowPrivateDomains: true });
100
71
  }
101
72
  export function autoDetectAuthWebUrl(location = typeof window !== 'undefined' ? window.location : undefined) {
102
73
  if (!location)