@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
@@ -91,6 +91,36 @@ export interface SwitchableAccount {
91
91
  /** The underlying per-account user payload. */
92
92
  user: SwitchableAccountUser;
93
93
  }
94
+ /**
95
+ * Whether the caller can BECOME this account — the one question every account
96
+ * switcher asks, answered here so no surface has to re-derive it.
97
+ *
98
+ * Two independent grounds, either of which suffices:
99
+ *
100
+ * - **It is already the caller's own identity** (`relationship: 'self'`).
101
+ * `GET /accounts` resolves its caller through `resolveOperatorId`, so `self`
102
+ * is the HUMAN operator's personal account even while they are operating an
103
+ * org — never the operated account. Kind is irrelevant on this ground: the
104
+ * caller IS that account, so returning to it asks the server for nothing.
105
+ * - **The server will mint a session for it** — `isActAsEligibleKind(kind)` is
106
+ * the exact predicate `POST /accounts/:id/switch` enforces, so a row offered
107
+ * on this ground is never a dead button.
108
+ *
109
+ * `isActAsEligibleKind` ALONE is not this question, and reaching for it
110
+ * directly is the mistake this function exists to prevent: it is false for
111
+ * `personal` as well as `channel`, so a switcher gated on it alone renders an
112
+ * empty list rather than a filtered one. Equally, `kind !== 'channel'` is not
113
+ * this question either — it silently admits every kind invented after it was
114
+ * written, which is the same trap `isActAsEligibleKind` was introduced to close
115
+ * on the server.
116
+ *
117
+ * Takes a structural subset rather than a whole {@link AccountNode} so a caller
118
+ * holding a projected {@link SwitchableAccount} can ask it too.
119
+ */
120
+ export declare function isSwitchTargetAccount(node: {
121
+ kind?: AccountKind | null;
122
+ relationship?: AccountRelationship;
123
+ }): boolean;
94
124
  /** Input to {@link projectSwitchableAccounts}. */
95
125
  export interface ProjectSwitchableAccountsInput {
96
126
  /**
@@ -131,8 +161,9 @@ export interface ProjectSwitchableAccountsInput {
131
161
  * and a graph node is deduped into ONE device row enriched with the graph
132
162
  * metadata (relationship / kind / parent / membership).
133
163
  *
134
- * Graph nodes of a kind nobody may act as (`channel`) are omitted see the
135
- * filter below.
164
+ * Graph nodes that are not switch targets a `channel`, which nobody may act
165
+ * as — are omitted. {@link isSwitchTargetAccount} is the rule; see the filter
166
+ * below.
136
167
  */
137
168
  export declare function projectSwitchableAccounts(input: ProjectSwitchableAccountsInput): SwitchableAccount[];
138
169
  /**
@@ -142,7 +173,10 @@ export declare function projectSwitchableAccounts(input: ProjectSwitchableAccoun
142
173
  * document, but including their ids lets the caller pass one id set and lets the
143
174
  * projection prefer freshly-fetched profiles uniformly.
144
175
  *
145
- * Applies the SAME act-as filter as {@link projectSwitchableAccounts} to graph
146
- * nodes, so this never fetches a profile for a row the projection will drop.
176
+ * Applies the SAME {@link isSwitchTargetAccount} filter as
177
+ * {@link projectSwitchableAccounts} to graph nodes, so this never fetches a
178
+ * profile for a row the projection will drop — and, just as importantly, never
179
+ * SKIPS one the projection will keep, which would leave that row unrendered
180
+ * until some later fetch happened to resolve it.
147
181
  */
148
182
  export declare function switchableAccountIds(state: DeviceSessionState | null, graph: AccountNode[]): string[];
@@ -0,0 +1,75 @@
1
+ /**
2
+ * THE enumeration of `OxyServices` GET-cache keys that serve the ACCOUNT FOREST
3
+ * — the caller's accessible accounts, as lists and as individual detail rows —
4
+ * and the one sweep that clears them.
5
+ *
6
+ * WHY THIS IS NOT A METHOD ON THE ACCOUNTS MIXIN
7
+ * ---------------------------------------------
8
+ * `AccountNode.account` is a whole `User`, so a forest read embeds the very
9
+ * profile the identity reads serve. That makes an IDENTITY write a writer of
10
+ * these keys too: `updateProfile` edits the caller's own personal account,
11
+ * which is a row in `GET /accounts` and is its own `GET /accounts/<id>`. Leave
12
+ * those cached and the account switcher keeps drawing the pre-edit name and
13
+ * picture for the full TTL, against a perfectly healthy server.
14
+ *
15
+ * The mixins compose into one class at runtime but are typed one at a time, so
16
+ * the user mixin cannot call a method the accounts mixin owns. The key list
17
+ * therefore lives here, once, and every writer calls {@link
18
+ * evictOxyAccountForestCache} — exactly like the identity key list in
19
+ * `identityCacheSweep`, which the accounts mixin already calls for the
20
+ * mirror-image case (an account write staling the identity reads). The
21
+ * alternative — a second hand-written copy of these keys in the other mixin —
22
+ * is the drift that shipped the two stale-profile bugs `identityCacheSweep`
23
+ * documents.
24
+ *
25
+ * WHY THE LIST NEEDS A PREFIX AND THE DETAIL DOES NOT
26
+ * --------------------------------------------------
27
+ * `listAccounts({tree?})` keys the flat list as `GET:/accounts` and every
28
+ * option variant as `GET:/accounts?<query>` (the query string is part of the
29
+ * URL, hence of the key), and a writer cannot enumerate which variants a caller
30
+ * has read. The detail key, by contrast, is derivable from the account id the
31
+ * writer already holds.
32
+ *
33
+ * The `GET:/accounts?` prefix matches ONLY the query-string list variants —
34
+ * never `GET:/accounts/<id>` or its `…/members`, `…/credentials`, `…/children`
35
+ * sub-resources, which are the accounts mixin's own business and stay there.
36
+ */
37
+ import type { OxyIdentityCacheEvictor } from './identityCacheSweep';
38
+ /**
39
+ * The cache-eviction surface of an `OxyServices` instance. Reused from
40
+ * `identityCacheSweep` rather than re-declared: it is the SDK's one published
41
+ * name for these two methods, and a second identical interface would be one
42
+ * more shape to keep in step.
43
+ */
44
+ export type OxyAccountCacheEvictor = OxyIdentityCacheEvictor;
45
+ /** The cache key `listAccounts()` reads under with no options. */
46
+ export declare const OXY_ACCOUNT_LIST_CACHE_KEY = "GET:/accounts";
47
+ /**
48
+ * The prefix covering every option-carrying `listAccounts(opts)` variant
49
+ * (`?tree=true`, …), none of which a writer can enumerate.
50
+ */
51
+ export declare const OXY_ACCOUNT_LIST_CACHE_QUERY_PREFIX = "GET:/accounts?";
52
+ /**
53
+ * Prefix covering every per-account sub-resource cache key
54
+ * (`GET:/accounts/<id>`, `…/members`, `…/credentials`, `…/children`). A
55
+ * membership mutation on an ancestor must sweep ALL of these, not only the
56
+ * account named in the path: descendant member rosters embed inherited rows
57
+ * resolved from that ancestor, and the writer cannot enumerate which descendant
58
+ * ids a caller has already read. The trailing slash deliberately excludes the
59
+ * forest list keys (`GET:/accounts`, `GET:/accounts?…`) documented above.
60
+ */
61
+ export declare const OXY_ACCOUNT_PER_ACCOUNT_CACHE_PREFIX = "GET:/accounts/";
62
+ /**
63
+ * Build the exact cache key `getAccount(accountId)` reads under.
64
+ */
65
+ export declare function oxyAccountDetailCacheKey(accountId: string): string;
66
+ /**
67
+ * Sweep an `OxyServices` GET response cache of the account forest.
68
+ *
69
+ * @param oxy - Anything exposing the SDK's two eviction methods.
70
+ * @param accountId - The account whose detail row to drop as well. Optional: a
71
+ * writer that changed the SHAPE of the forest rather than one
72
+ * account in it (create, archive, ownership transfer) has no
73
+ * detail row to name, and clears only the lists.
74
+ */
75
+ export declare function evictOxyAccountForestCache(oxy: OxyAccountCacheEvictor, accountId?: string): void;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * THE enumeration of `OxyServices` GET-cache keys that can carry a single
3
+ * account's identity, and the one sweep that clears them.
4
+ *
5
+ * WHY THIS IS ONE LIST
6
+ * --------------------
7
+ * An Oxy account is readable under SEVERAL cache keys, and a write that only
8
+ * busts the key it happens to know about leaves every other one serving the
9
+ * pre-write snapshot for up to its TTL — from the caller's OWN in-memory cache,
10
+ * with a perfectly healthy server. That failure has already shipped twice with
11
+ * two different sets of keys:
12
+ *
13
+ * - `updateAccount` busted `GET:/accounts/<id>` and the account lists, but a
14
+ * profile screen reads `GET:/profiles/username/<name>` and
15
+ * `GET:/users/<id>`, so a channel's new picture stayed invisible for the
16
+ * full 5-minute profile TTL.
17
+ * - `updateProfile` busted four of the six keys below, missing
18
+ * `GET:/auth/lookup/` (the login-flow avatar/display-name lookup) and
19
+ * `GET:/profiles/resolve` (handle resolution) — two independently-drifted
20
+ * copies of a list that has to agree.
21
+ *
22
+ * So the list lives here, once, and every writer calls
23
+ * {@link evictOxyIdentityCache}. Adding a new identity read means adding its key
24
+ * HERE and every writer inherits it.
25
+ *
26
+ * WHERE THE LINE IS DRAWN
27
+ * -----------------------
28
+ * These are the SINGLE-PROFILE reads — the account is the subject of the
29
+ * response and is addressable by id, handle, or session. Reads that merely
30
+ * CONTAIN an account among many (`GET:/profiles/search`,
31
+ * `GET:/users/<other>/followers`, `GET:/profiles/<other>/similar`) are
32
+ * deliberately NOT swept: an account cannot be located in them without the very
33
+ * lookup being invalidated, so sweeping them means sweeping the whole namespace
34
+ * on every identity change — a real cost on a backend consuming the
35
+ * cross-service invalidation signal, for a surface where a stale thumbnail
36
+ * expires on its own in ~2 minutes.
37
+ *
38
+ * WHY PREFIXES RATHER THAN EXACT KEYS
39
+ * -----------------------------------
40
+ * Only the by-id key can be built from a user id. The handle-keyed and
41
+ * session-keyed entries cannot — deriving a handle from an id needs the lookup
42
+ * we are invalidating, and the SDK never tracks active session ids centrally.
43
+ * Prefix sweeping is also what makes a USERNAME CHANGE correct: the entry under
44
+ * the OLD handle is unreachable by construction (nothing in the write response
45
+ * carries it), and a sweep targeted at the new handle alone would leave the old
46
+ * one serving the pre-rename profile until its TTL. Over-eviction costs a
47
+ * refetch; under-eviction serves wrong data.
48
+ *
49
+ * Platform-neutral by construction (no imports, no `OxyServices` reference) so
50
+ * the client mixins and the Node-only `@oxyhq/core/server` invalidation
51
+ * subscriber can share it without either pulling in the other.
52
+ */
53
+ /**
54
+ * The cache-eviction surface of an `OxyServices` instance. Declared
55
+ * structurally so this module stays free of any client import.
56
+ */
57
+ export interface OxyIdentityCacheEvictor {
58
+ clearCacheEntry(key: string): void;
59
+ clearCacheByPrefix(prefix: string): number;
60
+ }
61
+ /**
62
+ * Cache-key PREFIXES under which an account's identity can be served, for the
63
+ * reads whose key cannot be derived from a user id. Swept wholesale.
64
+ */
65
+ export declare const OXY_IDENTITY_CACHE_PREFIXES: readonly string[];
66
+ /**
67
+ * Build the exact cache key `getUserById` reads under. The only identity key
68
+ * derivable from a user id, so the only one that does not need a prefix sweep.
69
+ */
70
+ export declare function oxyUserByIdCacheKey(userId: string): string;
71
+ /**
72
+ * Sweep an `OxyServices` GET response cache of everything that could carry the
73
+ * given account's identity.
74
+ *
75
+ * @param oxy - Anything exposing the SDK's two eviction methods.
76
+ * @param userId - The account whose by-id entry to drop. Optional: a caller
77
+ * that does not know the id still clears every handle-, session-
78
+ * and self-keyed entry, which is the majority of the surface.
79
+ */
80
+ export declare function evictOxyIdentityCache(oxy: OxyIdentityCacheEvictor, userId?: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "18.0.0",
3
+ "version": "19.1.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -115,7 +115,7 @@
115
115
  "dependencies": {
116
116
  "@noble/ciphers": "^1.3.0",
117
117
  "@noble/hashes": "^1.8.0",
118
- "@oxyhq/contracts": "^0.22.0",
118
+ "@oxyhq/contracts": "^0.24.0",
119
119
  "@oxyhq/protocol": "^0.1.6",
120
120
  "@scure/bip39": "^1.6.0",
121
121
  "@types/elliptic": "^6.4.18",
@@ -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/src/index.ts CHANGED
@@ -122,7 +122,7 @@ export type {
122
122
  // ---------------------------------------------------------------------------
123
123
  export type {
124
124
  AccountKind,
125
- OrganizationCategory,
125
+ AccountCategoryId,
126
126
  AccountRelationship,
127
127
  AccountRole,
128
128
  AccountMemberStatus,
@@ -167,7 +167,13 @@ export type {
167
167
  ApplicationUsageStats,
168
168
  } from './mixins/OxyServices.accounts';
169
169
 
170
- export { ORGANIZATION_CATEGORIES } from './mixins/OxyServices.accounts';
170
+ export {
171
+ ACCOUNT_CATEGORY_IDS,
172
+ MAX_ACCOUNT_CATEGORIES,
173
+ SELECTABLE_ACCOUNT_CATEGORY_IDS,
174
+ isSelectableAccountCategoryId,
175
+ kindAcceptsAccountCategories,
176
+ } from './mixins/OxyServices.accounts';
171
177
 
172
178
  // ---------------------------------------------------------------------------
173
179
  // Reputation (Oxy Trust: ledger, balances, disputes, rules, influence).
@@ -641,7 +647,14 @@ export {
641
647
  // chooser: device sign-ins ∪ account graph, deduped by accountId). Pure +
642
648
  // I/O-free — the caller hydrates profiles via `getUsersByIds`. Shared by
643
649
  // `@oxyhq/services` and auth.oxy.so so the list can't diverge.
650
+ // `isSwitchTargetAccount` is the switcher's own question ("can I become this
651
+ // account?"), exported so a surface that renders `AccountNode`s rather than the
652
+ // projection — the Console's workspace switcher, the accounts app's
653
+ // managed-accounts rows — asks the SAME question instead of testing a kind
654
+ // literal. It is NOT `isActAsEligibleKind`: that one is false for `personal`
655
+ // too, so gating a switcher on it alone empties the list.
644
656
  export {
657
+ isSwitchTargetAccount,
645
658
  projectSwitchableAccounts,
646
659
  switchableAccountIds,
647
660
  } from './session/accountProjection';