@oxyhq/core 18.0.0 → 19.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.
Files changed (62) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/i18n/locales/en-US.json +49 -5
  3. package/dist/cjs/i18n/locales/es-ES.json +49 -5
  4. package/dist/cjs/i18n/locales/locales/en-US.json +49 -5
  5. package/dist/cjs/i18n/locales/locales/es-ES.json +49 -5
  6. package/dist/cjs/index.js +17 -6
  7. package/dist/cjs/mixins/OxyServices.accounts.js +69 -31
  8. package/dist/cjs/mixins/OxyServices.followGraph.js +204 -0
  9. package/dist/cjs/mixins/OxyServices.user.js +17 -20
  10. package/dist/cjs/mixins/index.js +4 -0
  11. package/dist/cjs/server/index.js +8 -2
  12. package/dist/cjs/server/userInvalidation.js +6 -28
  13. package/dist/cjs/session/accountProjection.js +45 -9
  14. package/dist/cjs/utils/accountCacheSweep.js +80 -0
  15. package/dist/cjs/utils/identityCacheSweep.js +97 -0
  16. package/dist/esm/.tsbuildinfo +1 -1
  17. package/dist/esm/i18n/locales/en-US.json +49 -5
  18. package/dist/esm/i18n/locales/es-ES.json +49 -5
  19. package/dist/esm/i18n/locales/locales/en-US.json +49 -5
  20. package/dist/esm/i18n/locales/locales/es-ES.json +49 -5
  21. package/dist/esm/index.js +8 -2
  22. package/dist/esm/mixins/OxyServices.accounts.js +64 -30
  23. package/dist/esm/mixins/OxyServices.followGraph.js +201 -0
  24. package/dist/esm/mixins/OxyServices.user.js +17 -20
  25. package/dist/esm/mixins/index.js +4 -0
  26. package/dist/esm/server/index.js +5 -1
  27. package/dist/esm/server/userInvalidation.js +5 -26
  28. package/dist/esm/session/accountProjection.js +44 -9
  29. package/dist/esm/utils/accountCacheSweep.js +75 -0
  30. package/dist/esm/utils/identityCacheSweep.js +92 -0
  31. package/dist/types/.tsbuildinfo +1 -1
  32. package/dist/types/index.d.ts +3 -3
  33. package/dist/types/mixins/OxyServices.accounts.d.ts +91 -34
  34. package/dist/types/mixins/OxyServices.followGraph.d.ts +211 -0
  35. package/dist/types/mixins/OxyServices.user.d.ts +10 -7
  36. package/dist/types/mixins/index.d.ts +2 -1
  37. package/dist/types/models/interfaces.d.ts +11 -3
  38. package/dist/types/server/index.d.ts +4 -2
  39. package/dist/types/server/userInvalidation.d.ts +5 -24
  40. package/dist/types/session/accountProjection.d.ts +38 -4
  41. package/dist/types/utils/accountCacheSweep.d.ts +75 -0
  42. package/dist/types/utils/identityCacheSweep.d.ts +80 -0
  43. package/package.json +2 -2
  44. package/src/i18n/locales/en-US.json +49 -5
  45. package/src/i18n/locales/es-ES.json +49 -5
  46. package/src/index.ts +15 -2
  47. package/src/mixins/OxyServices.accounts.ts +123 -45
  48. package/src/mixins/OxyServices.followGraph.ts +266 -0
  49. package/src/mixins/OxyServices.user.ts +17 -20
  50. package/src/mixins/__tests__/accounts.test.ts +5 -0
  51. package/src/mixins/__tests__/followGraph.test.ts +128 -0
  52. package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +407 -0
  53. package/src/mixins/index.ts +5 -0
  54. package/src/models/interfaces.ts +11 -3
  55. package/src/server/__tests__/userInvalidation.test.ts +3 -20
  56. package/src/server/index.ts +5 -2
  57. package/src/server/userInvalidation.ts +8 -36
  58. package/src/session/__tests__/accountProjection.test.ts +109 -2
  59. package/src/session/accountProjection.ts +47 -9
  60. package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
  61. package/src/utils/accountCacheSweep.ts +93 -0
  62. package/src/utils/identityCacheSweep.ts +104 -0
@@ -1748,11 +1748,53 @@
1748
1748
  "description": "A programmatic account with service credentials"
1749
1749
  }
1750
1750
  },
1751
- "organizationCategory": {
1751
+ "accountCategory": {
1752
+ "news": "News",
1753
+ "politics": "Politics & policy",
1754
+ "business": "Business",
1755
+ "startup": "Startups & entrepreneurship",
1756
+ "finance": "Finance & investing",
1757
+ "crypto": "Crypto & web3",
1758
+ "marketplace": "Marketplace & classifieds",
1759
+ "retail": "Retail & ecommerce",
1760
+ "real_estate": "Real estate",
1752
1761
  "agency": "Real estate agency",
1762
+ "landlord": "Landlord & property management",
1753
1763
  "cooperative": "Housing cooperative",
1754
- "landlord": "Landlord / property manager",
1755
- "other": "Other organization"
1764
+ "architecture": "Architecture & construction",
1765
+ "technology": "Technology",
1766
+ "software": "Software & development",
1767
+ "ai": "AI & machine learning",
1768
+ "security": "Security & privacy",
1769
+ "automation": "Automation & tools",
1770
+ "science": "Science & research",
1771
+ "education": "Education & training",
1772
+ "books": "Books & writing",
1773
+ "health": "Health & medicine",
1774
+ "fitness": "Fitness & wellbeing",
1775
+ "sports": "Sports",
1776
+ "gaming": "Gaming & esports",
1777
+ "music": "Music",
1778
+ "film": "Film & TV",
1779
+ "podcast": "Podcasts & audio",
1780
+ "art": "Art & design",
1781
+ "photography": "Photography",
1782
+ "comedy": "Comedy",
1783
+ "food": "Food & drink",
1784
+ "travel": "Travel & places",
1785
+ "fashion": "Fashion & beauty",
1786
+ "home_garden": "Home & garden",
1787
+ "diy": "DIY & making",
1788
+ "automotive": "Cars & motoring",
1789
+ "animals": "Animals & pets",
1790
+ "family": "Family & parenting",
1791
+ "nonprofit": "Non-profit & charity",
1792
+ "government": "Government & public sector",
1793
+ "community": "Local community",
1794
+ "activism": "Activism & advocacy",
1795
+ "environment": "Environment & sustainability",
1796
+ "religion": "Religion & spirituality",
1797
+ "other": "Other"
1756
1798
  },
1757
1799
  "create": {
1758
1800
  "title": "Create account",
@@ -1772,8 +1814,9 @@
1772
1814
  "invalidChars": "Only letters, numbers, hyphens, and underscores",
1773
1815
  "checkFailed": "Could not check availability"
1774
1816
  },
1775
- "organizationCategory": {
1776
- "label": "Organization type"
1817
+ "accountCategory": {
1818
+ "label": "Categories",
1819
+ "hint": "Pick up to {{max}}. The first one is your main category."
1777
1820
  },
1778
1821
  "toasts": {
1779
1822
  "success": "Account created",
@@ -1796,6 +1839,7 @@
1796
1839
  "title": "Members",
1797
1840
  "subtitle": "People with access to this account.",
1798
1841
  "empty": "No members yet.",
1842
+ "inherited": "Inherited",
1799
1843
  "actions": {
1800
1844
  "remove": "Remove member",
1801
1845
  "transfer": "Transfer ownership"
@@ -1748,11 +1748,53 @@
1748
1748
  "description": "Una cuenta programática con credenciales de servicio"
1749
1749
  }
1750
1750
  },
1751
- "organizationCategory": {
1751
+ "accountCategory": {
1752
+ "news": "Noticias",
1753
+ "politics": "Política",
1754
+ "business": "Negocios",
1755
+ "startup": "Startups y emprendimiento",
1756
+ "finance": "Finanzas e inversión",
1757
+ "crypto": "Cripto y web3",
1758
+ "marketplace": "Marketplace y anuncios",
1759
+ "retail": "Comercio y ecommerce",
1760
+ "real_estate": "Inmobiliaria",
1752
1761
  "agency": "Agencia inmobiliaria",
1762
+ "landlord": "Casero y gestión de fincas",
1753
1763
  "cooperative": "Cooperativa de vivienda",
1754
- "landlord": "Propietario / administrador de fincas",
1755
- "other": "Otra organización"
1764
+ "architecture": "Arquitectura y construcción",
1765
+ "technology": "Tecnología",
1766
+ "software": "Software y desarrollo",
1767
+ "ai": "IA y aprendizaje automático",
1768
+ "security": "Seguridad y privacidad",
1769
+ "automation": "Automatización y herramientas",
1770
+ "science": "Ciencia e investigación",
1771
+ "education": "Educación y formación",
1772
+ "books": "Libros y escritura",
1773
+ "health": "Salud y medicina",
1774
+ "fitness": "Deporte y bienestar",
1775
+ "sports": "Deportes",
1776
+ "gaming": "Videojuegos y esports",
1777
+ "music": "Música",
1778
+ "film": "Cine y TV",
1779
+ "podcast": "Pódcast y audio",
1780
+ "art": "Arte y diseño",
1781
+ "photography": "Fotografía",
1782
+ "comedy": "Comedia",
1783
+ "food": "Comida y bebida",
1784
+ "travel": "Viajes y lugares",
1785
+ "fashion": "Moda y belleza",
1786
+ "home_garden": "Hogar y jardín",
1787
+ "diy": "Bricolaje y manualidades",
1788
+ "automotive": "Coches y motor",
1789
+ "animals": "Animales y mascotas",
1790
+ "family": "Familia y crianza",
1791
+ "nonprofit": "ONG y benéficas",
1792
+ "government": "Gobierno y sector público",
1793
+ "community": "Comunidad local",
1794
+ "activism": "Activismo y defensa",
1795
+ "environment": "Medio ambiente y sostenibilidad",
1796
+ "religion": "Religión y espiritualidad",
1797
+ "other": "Otra"
1756
1798
  },
1757
1799
  "create": {
1758
1800
  "title": "Crear cuenta",
@@ -1772,8 +1814,9 @@
1772
1814
  "invalidChars": "Solo letras, números, guiones y guiones bajos",
1773
1815
  "checkFailed": "No se pudo comprobar la disponibilidad"
1774
1816
  },
1775
- "organizationCategory": {
1776
- "label": "Tipo de organización"
1817
+ "accountCategory": {
1818
+ "label": "Categorías",
1819
+ "hint": "Elige hasta {{max}}. La primera es tu categoría principal."
1777
1820
  },
1778
1821
  "toasts": {
1779
1822
  "success": "Cuenta creada",
@@ -1796,6 +1839,7 @@
1796
1839
  "title": "Miembros",
1797
1840
  "subtitle": "Personas con acceso a esta cuenta.",
1798
1841
  "empty": "Aún no hay miembros.",
1842
+ "inherited": "Heredado",
1799
1843
  "actions": {
1800
1844
  "remove": "Eliminar miembro",
1801
1845
  "transfer": "Transferir propiedad"
@@ -1748,11 +1748,53 @@
1748
1748
  "description": "A programmatic account with service credentials"
1749
1749
  }
1750
1750
  },
1751
- "organizationCategory": {
1751
+ "accountCategory": {
1752
+ "news": "News",
1753
+ "politics": "Politics & policy",
1754
+ "business": "Business",
1755
+ "startup": "Startups & entrepreneurship",
1756
+ "finance": "Finance & investing",
1757
+ "crypto": "Crypto & web3",
1758
+ "marketplace": "Marketplace & classifieds",
1759
+ "retail": "Retail & ecommerce",
1760
+ "real_estate": "Real estate",
1752
1761
  "agency": "Real estate agency",
1762
+ "landlord": "Landlord & property management",
1753
1763
  "cooperative": "Housing cooperative",
1754
- "landlord": "Landlord / property manager",
1755
- "other": "Other organization"
1764
+ "architecture": "Architecture & construction",
1765
+ "technology": "Technology",
1766
+ "software": "Software & development",
1767
+ "ai": "AI & machine learning",
1768
+ "security": "Security & privacy",
1769
+ "automation": "Automation & tools",
1770
+ "science": "Science & research",
1771
+ "education": "Education & training",
1772
+ "books": "Books & writing",
1773
+ "health": "Health & medicine",
1774
+ "fitness": "Fitness & wellbeing",
1775
+ "sports": "Sports",
1776
+ "gaming": "Gaming & esports",
1777
+ "music": "Music",
1778
+ "film": "Film & TV",
1779
+ "podcast": "Podcasts & audio",
1780
+ "art": "Art & design",
1781
+ "photography": "Photography",
1782
+ "comedy": "Comedy",
1783
+ "food": "Food & drink",
1784
+ "travel": "Travel & places",
1785
+ "fashion": "Fashion & beauty",
1786
+ "home_garden": "Home & garden",
1787
+ "diy": "DIY & making",
1788
+ "automotive": "Cars & motoring",
1789
+ "animals": "Animals & pets",
1790
+ "family": "Family & parenting",
1791
+ "nonprofit": "Non-profit & charity",
1792
+ "government": "Government & public sector",
1793
+ "community": "Local community",
1794
+ "activism": "Activism & advocacy",
1795
+ "environment": "Environment & sustainability",
1796
+ "religion": "Religion & spirituality",
1797
+ "other": "Other"
1756
1798
  },
1757
1799
  "create": {
1758
1800
  "title": "Create account",
@@ -1772,8 +1814,9 @@
1772
1814
  "invalidChars": "Only letters, numbers, hyphens, and underscores",
1773
1815
  "checkFailed": "Could not check availability"
1774
1816
  },
1775
- "organizationCategory": {
1776
- "label": "Organization type"
1817
+ "accountCategory": {
1818
+ "label": "Categories",
1819
+ "hint": "Pick up to {{max}}. The first one is your main category."
1777
1820
  },
1778
1821
  "toasts": {
1779
1822
  "success": "Account created",
@@ -1796,6 +1839,7 @@
1796
1839
  "title": "Members",
1797
1840
  "subtitle": "People with access to this account.",
1798
1841
  "empty": "No members yet.",
1842
+ "inherited": "Inherited",
1799
1843
  "actions": {
1800
1844
  "remove": "Remove member",
1801
1845
  "transfer": "Transfer ownership"
@@ -1748,11 +1748,53 @@
1748
1748
  "description": "Una cuenta programática con credenciales de servicio"
1749
1749
  }
1750
1750
  },
1751
- "organizationCategory": {
1751
+ "accountCategory": {
1752
+ "news": "Noticias",
1753
+ "politics": "Política",
1754
+ "business": "Negocios",
1755
+ "startup": "Startups y emprendimiento",
1756
+ "finance": "Finanzas e inversión",
1757
+ "crypto": "Cripto y web3",
1758
+ "marketplace": "Marketplace y anuncios",
1759
+ "retail": "Comercio y ecommerce",
1760
+ "real_estate": "Inmobiliaria",
1752
1761
  "agency": "Agencia inmobiliaria",
1762
+ "landlord": "Casero y gestión de fincas",
1753
1763
  "cooperative": "Cooperativa de vivienda",
1754
- "landlord": "Propietario / administrador de fincas",
1755
- "other": "Otra organización"
1764
+ "architecture": "Arquitectura y construcción",
1765
+ "technology": "Tecnología",
1766
+ "software": "Software y desarrollo",
1767
+ "ai": "IA y aprendizaje automático",
1768
+ "security": "Seguridad y privacidad",
1769
+ "automation": "Automatización y herramientas",
1770
+ "science": "Ciencia e investigación",
1771
+ "education": "Educación y formación",
1772
+ "books": "Libros y escritura",
1773
+ "health": "Salud y medicina",
1774
+ "fitness": "Deporte y bienestar",
1775
+ "sports": "Deportes",
1776
+ "gaming": "Videojuegos y esports",
1777
+ "music": "Música",
1778
+ "film": "Cine y TV",
1779
+ "podcast": "Pódcast y audio",
1780
+ "art": "Arte y diseño",
1781
+ "photography": "Fotografía",
1782
+ "comedy": "Comedia",
1783
+ "food": "Comida y bebida",
1784
+ "travel": "Viajes y lugares",
1785
+ "fashion": "Moda y belleza",
1786
+ "home_garden": "Hogar y jardín",
1787
+ "diy": "Bricolaje y manualidades",
1788
+ "automotive": "Coches y motor",
1789
+ "animals": "Animales y mascotas",
1790
+ "family": "Familia y crianza",
1791
+ "nonprofit": "ONG y benéficas",
1792
+ "government": "Gobierno y sector público",
1793
+ "community": "Comunidad local",
1794
+ "activism": "Activismo y defensa",
1795
+ "environment": "Medio ambiente y sostenibilidad",
1796
+ "religion": "Religión y espiritualidad",
1797
+ "other": "Otra"
1756
1798
  },
1757
1799
  "create": {
1758
1800
  "title": "Crear cuenta",
@@ -1772,8 +1814,9 @@
1772
1814
  "invalidChars": "Solo letras, números, guiones y guiones bajos",
1773
1815
  "checkFailed": "No se pudo comprobar la disponibilidad"
1774
1816
  },
1775
- "organizationCategory": {
1776
- "label": "Tipo de organización"
1817
+ "accountCategory": {
1818
+ "label": "Categorías",
1819
+ "hint": "Elige hasta {{max}}. La primera es tu categoría principal."
1777
1820
  },
1778
1821
  "toasts": {
1779
1822
  "success": "Cuenta creada",
@@ -1796,6 +1839,7 @@
1796
1839
  "title": "Miembros",
1797
1840
  "subtitle": "Personas con acceso a esta cuenta.",
1798
1841
  "empty": "Aún no hay miembros.",
1842
+ "inherited": "Heredado",
1799
1843
  "actions": {
1800
1844
  "remove": "Eliminar miembro",
1801
1845
  "transfer": "Transferir propiedad"
package/dist/esm/index.js CHANGED
@@ -38,7 +38,7 @@ export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData.js';
38
38
  export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull, } from './utils/userIdentity.js';
39
39
  export { getCanonicalUserHandle, getNormalizedUserHandle, } from './utils/userHandle.js';
40
40
  export { normalizeProfileLinks } from './utils/profileLinks.js';
41
- export { ORGANIZATION_CATEGORIES } from './mixins/OxyServices.accounts.js';
41
+ export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './mixins/OxyServices.accounts.js';
42
42
  // ---------------------------------------------------------------------------
43
43
  // Reputation (Oxy Trust: ledger, balances, disputes, rules, influence).
44
44
  // The whole type family — the closed value sets, the two balance views and the
@@ -173,7 +173,13 @@ export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountId
173
173
  // chooser: device sign-ins ∪ account graph, deduped by accountId). Pure +
174
174
  // I/O-free — the caller hydrates profiles via `getUsersByIds`. Shared by
175
175
  // `@oxyhq/services` and auth.oxy.so so the list can't diverge.
176
- export { projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection.js';
176
+ // `isSwitchTargetAccount` is the switcher's own question ("can I become this
177
+ // account?"), exported so a surface that renders `AccountNode`s rather than the
178
+ // projection — the Console's workspace switcher, the accounts app's
179
+ // managed-accounts rows — asks the SAME question instead of testing a kind
180
+ // literal. It is NOT `isActAsEligibleKind`: that one is false for `personal`
181
+ // too, so gating a switcher on it alone empties the list.
182
+ export { isSwitchTargetAccount, projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection.js';
177
183
  // Headless controller for the unified account dialog. Framework-agnostic
178
184
  // state machine + subscribe/getSnapshot store (bind via `useSyncExternalStore`)
179
185
  // — sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff;
@@ -1,6 +1,8 @@
1
1
  import { normalizeUserIdentity } from '../utils/userIdentity.js';
2
+ import { evictOxyIdentityCache } from '../utils/identityCacheSweep.js';
3
+ import { evictOxyAccountForestCache, oxyAccountDetailCacheKey, OXY_ACCOUNT_PER_ACCOUNT_CACHE_PREFIX } from '../utils/accountCacheSweep.js';
2
4
  import { CACHE_TIMES } from './mixinHelpers.js';
3
- export { ACCOUNT_KINDS, ORGANIZATION_CATEGORIES, isActAsEligibleKind } from '@oxyhq/contracts';
5
+ export { ACCOUNT_CATEGORY_IDS, ACCOUNT_KINDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isActAsEligibleKind, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from '@oxyhq/contracts';
4
6
  export function OxyServicesAccountsMixin(Base) {
5
7
  return class extends Base {
6
8
  constructor(...args) {
@@ -112,7 +114,7 @@ export function OxyServicesAccountsMixin(Base) {
112
114
  const res = await this.makeRequest('POST', '/accounts', data, { cache: false });
113
115
  // A new account changes the accessible forest — bust every cached list
114
116
  // (flat + tree) so it appears on the next `listAccounts()` read.
115
- this._invalidateAccountLists();
117
+ evictOxyAccountForestCache(this);
116
118
  return res.account;
117
119
  }
118
120
  catch (error) {
@@ -158,6 +160,18 @@ export function OxyServicesAccountsMixin(Base) {
158
160
  /**
159
161
  * Update an account's mutable profile fields. Tree placement changes
160
162
  * (reparenting) go through the dedicated move endpoint, not here.
163
+ *
164
+ * An account IS a user, so this write changes identity — and a profile
165
+ * screen never reads `/accounts/<id>`. It reads `GET /users/<id>` and
166
+ * `GET /profiles/username/<handle>`, both cached for 5 minutes in the
167
+ * CALLER'S OWN process, so busting only the account-graph keys left every
168
+ * profile surface serving the pre-edit avatar and name for the full TTL
169
+ * with a perfectly healthy server (the cross-service `oxy:user:invalidate`
170
+ * signal does not help: it evicts BACKEND caches, and cannot reach a cache
171
+ * living in a browser tab). {@link evictOxyIdentityCache} owns that key
172
+ * list — see its docs for why the handle-keyed entries are prefix-swept
173
+ * (a RENAME leaves the old handle's entry unreachable by any targeted key).
174
+ *
161
175
  * @param accountId - The account's Mongo `_id`.
162
176
  * @param data - Subset of updatable profile fields.
163
177
  */
@@ -166,8 +180,15 @@ export function OxyServicesAccountsMixin(Base) {
166
180
  const res = await this.makeRequest('PATCH', `/accounts/${encodeURIComponent(accountId)}`, data, { cache: false });
167
181
  // Bust the cached detail and every list (which embeds account profile
168
182
  // data) so neither serves the pre-update snapshot.
169
- this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
170
- this._invalidateAccountLists();
183
+ evictOxyAccountForestCache(this, accountId);
184
+ // The parent's children list embeds this account's profile and is keyed
185
+ // by the PARENT id, so it is reachable only from the response node.
186
+ const parentAccountId = res.account?.parentAccountId;
187
+ if (parentAccountId) {
188
+ this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(parentAccountId)}/children`);
189
+ }
190
+ // Every identity read of this account, whichever key it lands under.
191
+ evictOxyIdentityCache(this, accountId);
171
192
  return res.account;
172
193
  }
173
194
  catch (error) {
@@ -184,10 +205,9 @@ export function OxyServicesAccountsMixin(Base) {
184
205
  try {
185
206
  const result = await this.makeRequest('DELETE', `/accounts/${encodeURIComponent(accountId)}`, undefined, { cache: false });
186
207
  // Bust every cached representation of the archived account.
187
- this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
188
208
  this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/members`);
189
209
  this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
190
- this._invalidateAccountLists();
210
+ evictOxyAccountForestCache(this, accountId);
191
211
  return result;
192
212
  }
193
213
  catch (error) {
@@ -211,7 +231,27 @@ export function OxyServicesAccountsMixin(Base) {
211
231
  // Account members
212
232
  // =========================================================================
213
233
  /**
214
- * List members of an account (direct membership rows on the account).
234
+ * List the members of an account: the membership rows ON it, plus the rows
235
+ * on its ancestors that cascade into it. Each entry carries `source`
236
+ * (`direct` | `inherited`) saying which it is.
237
+ *
238
+ * Inherited entries are members in every sense the server enforces — an
239
+ * ancestor row with `inherit: true` resolves through
240
+ * `resolveEffectiveAccess` and confers every account permission on the
241
+ * descendant, `account:act_as` included — so a roster that omitted them
242
+ * answered `[]` for accounts several people could act on.
243
+ *
244
+ * Two things follow for a caller. An entry's `accountId` is the account its
245
+ * ROW lives on, so an inherited entry names an ancestor rather than the
246
+ * account you asked about; and the member-mutation endpoints only accept
247
+ * rows on the account in the path, so gate any edit/remove/transfer
248
+ * affordance on `source === 'direct'`.
249
+ *
250
+ * Asking what the CALLER holds over an account is a different question, and
251
+ * scanning this list for yourself is the wrong way to answer it — use
252
+ * {@link OxyServicesAccountsMixin.getAccount}, whose `callerMembership` is
253
+ * the server's own resolution.
254
+ *
215
255
  * @param accountId - The account's Mongo `_id`.
216
256
  */
217
257
  async listAccountMembers(accountId) {
@@ -282,7 +322,7 @@ export function OxyServicesAccountsMixin(Base) {
282
322
  // Ownership change alters roles in the member list AND the detail, and
283
323
  // can change which accounts the caller "owns" in the list view.
284
324
  this._invalidateAccountMembership(accountId);
285
- this._invalidateAccountLists();
325
+ evictOxyAccountForestCache(this);
286
326
  return result;
287
327
  }
288
328
  catch (error) {
@@ -526,35 +566,29 @@ export function OxyServicesAccountsMixin(Base) {
526
566
  // Cache-invalidation helpers
527
567
  // =========================================================================
528
568
  /**
529
- * Bust every cached account list. `listAccounts({tree?})` keys the flat list
530
- * as `GET:/accounts` and the tree variant as `GET:/accounts?tree=true` (the
531
- * query string is part of the URL path). A change to the accessible forest
532
- * (create/archive/ownership transfer) invalidates both, so we clear the
533
- * unscoped entry plus every `?`-query variant via a prefix sweep. The prefix
534
- * `GET:/accounts?` matches only the query-string list variants, never the
535
- * `GET:/accounts/<id>…` detail/sub-resource keys.
569
+ * Bust the cached member list and detail for an account after a membership
570
+ * mutation. The member list (`listAccountMembers`) and the detail
571
+ * (`getAccount`, which can embed the caller's membership) both go stale when
572
+ * the member set or a member's role changes.
536
573
  *
537
574
  * Internal helper (leading underscore); not part of the supported public
538
575
  * surface. Public rather than `private` because mixins compose into an
539
576
  * exported anonymous class, where TypeScript cannot represent a private
540
577
  * member in the emitted declaration file (TS4094).
541
- */
542
- _invalidateAccountLists() {
543
- this.clearCacheEntry('GET:/accounts');
544
- this.clearCacheByPrefix('GET:/accounts?');
545
- }
546
- /**
547
- * Bust the cached member list and detail for an account after a membership
548
- * mutation. The member list (`listAccountMembers`) and the detail
549
- * (`getAccount`, which can embed the caller's membership) both go stale when
550
- * the member set or a member's role changes.
551
578
  *
552
- * Internal helper (leading underscore); see `_invalidateAccountLists` for why
553
- * this is public rather than `private`.
579
+ * The forest keys themselves are NOT owned here — see
580
+ * `utils/accountCacheSweep`, which the user mixin has to reach as well.
554
581
  */
555
582
  _invalidateAccountMembership(accountId) {
556
583
  this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/members`);
557
- this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
584
+ this.clearCacheEntry(oxyAccountDetailCacheKey(accountId));
585
+ // Inherited rows on descendant rosters are derived from this account's
586
+ // membership table — a targeted clear of only the mutated account's keys
587
+ // leaves every other cached `…/members` list serving stale inherited
588
+ // roles until MEDIUM TTL. Sweep all per-account sub-resource keys instead;
589
+ // the forest list keys (`GET:/accounts`, `GET:/accounts?…`) are excluded
590
+ // by the trailing slash on the prefix (see accountCacheSweep).
591
+ this.clearCacheByPrefix(OXY_ACCOUNT_PER_ACCOUNT_CACHE_PREFIX);
558
592
  }
559
593
  /**
560
594
  * Bust every cached application list. `listAccountApps(accountId)` keys each
@@ -565,8 +599,8 @@ export function OxyServicesAccountsMixin(Base) {
565
599
  * query-string list variants, never the `GET:/applications/<id>…`
566
600
  * detail/sub-resource keys.
567
601
  *
568
- * Internal helper (leading underscore); see `_invalidateAccountLists` for why
569
- * this is public rather than `private`.
602
+ * Internal helper (leading underscore); see `_invalidateAccountMembership`
603
+ * for why this is public rather than `private`.
570
604
  */
571
605
  _invalidateAppLists() {
572
606
  this.clearCacheEntry('GET:/applications');