@oxyhq/core 17.1.0 → 19.0.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 (54) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/boot/sessionColdBoot.js +4 -5
  3. package/dist/cjs/i18n/locales/en-US.json +48 -5
  4. package/dist/cjs/i18n/locales/es-ES.json +48 -5
  5. package/dist/cjs/i18n/locales/locales/en-US.json +48 -5
  6. package/dist/cjs/i18n/locales/locales/es-ES.json +48 -5
  7. package/dist/cjs/index.js +10 -6
  8. package/dist/cjs/mixins/OxyServices.accounts.js +27 -2
  9. package/dist/cjs/mixins/OxyServices.deviceBoot.js +3 -2
  10. package/dist/cjs/mixins/OxyServices.user.js +14 -20
  11. package/dist/cjs/server/index.js +8 -2
  12. package/dist/cjs/server/userInvalidation.js +6 -28
  13. package/dist/cjs/session/refresh.js +9 -14
  14. package/dist/cjs/utils/identityCacheSweep.js +97 -0
  15. package/dist/esm/.tsbuildinfo +1 -1
  16. package/dist/esm/boot/sessionColdBoot.js +4 -5
  17. package/dist/esm/i18n/locales/en-US.json +48 -5
  18. package/dist/esm/i18n/locales/es-ES.json +48 -5
  19. package/dist/esm/i18n/locales/locales/en-US.json +48 -5
  20. package/dist/esm/i18n/locales/locales/es-ES.json +48 -5
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/mixins/OxyServices.accounts.js +22 -1
  23. package/dist/esm/mixins/OxyServices.deviceBoot.js +3 -2
  24. package/dist/esm/mixins/OxyServices.user.js +14 -20
  25. package/dist/esm/server/index.js +5 -1
  26. package/dist/esm/server/userInvalidation.js +5 -26
  27. package/dist/esm/session/refresh.js +9 -14
  28. package/dist/esm/utils/identityCacheSweep.js +92 -0
  29. package/dist/types/.tsbuildinfo +1 -1
  30. package/dist/types/index.d.ts +2 -2
  31. package/dist/types/mixins/OxyServices.accounts.d.ts +54 -11
  32. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +3 -2
  33. package/dist/types/mixins/OxyServices.user.d.ts +9 -7
  34. package/dist/types/models/interfaces.d.ts +11 -3
  35. package/dist/types/server/index.d.ts +4 -2
  36. package/dist/types/server/userInvalidation.d.ts +5 -24
  37. package/dist/types/session/refresh.d.ts +13 -18
  38. package/dist/types/utils/identityCacheSweep.d.ts +80 -0
  39. package/package.json +2 -2
  40. package/src/boot/sessionColdBoot.ts +4 -5
  41. package/src/i18n/locales/en-US.json +48 -5
  42. package/src/i18n/locales/es-ES.json +48 -5
  43. package/src/index.ts +8 -3
  44. package/src/mixins/OxyServices.accounts.ts +73 -12
  45. package/src/mixins/OxyServices.deviceBoot.ts +3 -2
  46. package/src/mixins/OxyServices.user.ts +14 -20
  47. package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +370 -0
  48. package/src/models/interfaces.ts +11 -3
  49. package/src/server/__tests__/userInvalidation.test.ts +3 -20
  50. package/src/server/index.ts +5 -2
  51. package/src/server/userInvalidation.ts +8 -36
  52. package/src/session/refresh.ts +15 -20
  53. package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
  54. package/src/utils/identityCacheSweep.ts +104 -0
@@ -39,8 +39,8 @@ export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHand
39
39
  export { normalizeProfileLinks } from './utils/profileLinks';
40
40
  export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
41
41
  export type { PublicApplication, ConnectedApp, } from './mixins/OxyServices.connectedApps';
42
- export type { AccountKind, OrganizationCategory, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, UserCreatableAccountKind, CreateAccountInput, UpdateAccountInput, ProvisionChannelInput, ProvisionChannelMemberInput, ProvisionChannelResult, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, CreateAccountCredentialInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
43
- export { ORGANIZATION_CATEGORIES } from './mixins/OxyServices.accounts';
42
+ export type { AccountKind, AccountCategoryId, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, ProvisionChannelInput, ProvisionChannelMemberInput, ProvisionChannelResult, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, CreateAccountCredentialInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
43
+ export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './mixins/OxyServices.accounts';
44
44
  export { buildUserDid } from './mixins/OxyServices.identity';
45
45
  export type { IdentityRecordType, UnlinkableAuthMethodType, LinkAuthMethodResult, PublishRecordResult, VerifyRecordResult, VerifyDomainResult, RemoveDomainResult, RotateKeyProof, RotateKeyOptions, RotateKeyResult, } from './mixins/OxyServices.identity';
46
46
  export { parseIdPayload, parseAttestPayload, verifyPublicCardAttestation, } from './mixins/OxyServices.civic';
@@ -33,7 +33,7 @@
33
33
  * registers the switched session into the operator's device-set directly).
34
34
  */
35
35
  import type { User } from '../models/interfaces';
36
- import type { AccountKind, OrganizationCategory } from '@oxyhq/contracts';
36
+ import type { AccountCategoryId, AccountKind, ChildAccountKind } from '@oxyhq/contracts';
37
37
  import type { SessionLoginResponse } from '../models/session';
38
38
  import type { OxyServicesBase } from '../OxyServices.base';
39
39
  /**
@@ -45,8 +45,8 @@ import type { OxyServicesBase } from '../OxyServices.base';
45
45
  *
46
46
  * Single source of truth is `@oxyhq/contracts`.
47
47
  */
48
- export type { AccountKind, OrganizationCategory } from '@oxyhq/contracts';
49
- export { ACCOUNT_KINDS, ORGANIZATION_CATEGORIES, isActAsEligibleKind } from '@oxyhq/contracts';
48
+ export type { AccountCategoryId, AccountKind } from '@oxyhq/contracts';
49
+ export { ACCOUNT_CATEGORY_IDS, ACCOUNT_KINDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isActAsEligibleKind, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from '@oxyhq/contracts';
50
50
  /**
51
51
  * The calling user's relationship to an account node, as resolved by the API:
52
52
  * - `self` — the caller's own personal (root) account.
@@ -131,12 +131,23 @@ export interface ListAccountsOptions {
131
131
  */
132
132
  tree?: boolean;
133
133
  }
134
- /** Kinds a signed-in user may create via `POST /accounts`. Channels are service-provisioned only. */
135
- export type UserCreatableAccountKind = 'organization' | 'project' | 'bot';
136
134
  /** Input accepted by `createAccount`. */
137
135
  export interface CreateAccountInput {
138
- /** Classification of the new account. `personal` and `channel` are not creatable here. */
139
- kind: UserCreatableAccountKind;
136
+ /**
137
+ * Classification of the new account. Every CHILD kind is creatable here with
138
+ * the caller's own bearer, `channel` included: a signed-in person has already
139
+ * proven who they are, and minting a child under their own tree is the same
140
+ * operation whichever kind it is.
141
+ *
142
+ * `channel` used to be excluded, on the reasoning that channels are
143
+ * service-provisioned only. What actually makes a channel safe does not depend
144
+ * on who creates it: `createChildAccount` writes no auth method, so it is born
145
+ * with no login, and `POST /accounts/:id/switch` refuses it via
146
+ * `isActAsEligibleKind`, so no session can ever have a channel as its subject
147
+ * and therefore no bearer exists that could add one. `personal` is excluded
148
+ * because it is a human login, minted at signup.
149
+ */
150
+ kind: ChildAccountKind;
140
151
  /**
141
152
  * Parent account `_id` to nest the new account under. Omitted → the API roots
142
153
  * it under the caller's personal account.
@@ -157,8 +168,17 @@ export interface CreateAccountInput {
157
168
  };
158
169
  bio?: string;
159
170
  avatar?: string;
160
- /** Meaningful only when `kind` is `organization`. */
161
- organizationCategory?: OrganizationCategory;
171
+ /**
172
+ * What the account is about. ORDERED — the FIRST element is the primary
173
+ * category, so a picker must submit them in the order the user arranged them
174
+ * and must not sort. Stable ids, never labels: render each one through the
175
+ * `accounts.accountCategory.<id>` translation key.
176
+ *
177
+ * Offer `SELECTABLE_ACCOUNT_CATEGORY_IDS`, not `ACCOUNT_CATEGORY_IDS` — the
178
+ * latter still contains withdrawn ids so that accounts already carrying one
179
+ * keep working. At most `MAX_ACCOUNT_CATEGORIES`, no duplicates.
180
+ */
181
+ accountCategories?: AccountCategoryId[];
162
182
  }
163
183
  /** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
164
184
  export interface UpdateAccountInput {
@@ -176,8 +196,19 @@ export interface UpdateAccountInput {
176
196
  };
177
197
  bio?: string | null;
178
198
  avatar?: string | null;
179
- /** Clears the category when `null`; only valid on `kind: 'organization'`. */
180
- organizationCategory?: OrganizationCategory | null;
199
+ /**
200
+ * Replaces the WHOLE list, in the order given — there is no add/remove verb,
201
+ * because a partial edit cannot express a re-ordering and the order is what
202
+ * names the primary category. `[]` clears it.
203
+ *
204
+ * Not nullable, unlike `bio` and `avatar`: the empty case already has a
205
+ * spelling of its own, so a second one could only ever disagree with it.
206
+ *
207
+ * Rejected for a `personal` account, and rejected when it ADDS a withdrawn
208
+ * id the account did not already carry — keeping or re-ordering one it has is
209
+ * always allowed.
210
+ */
211
+ accountCategories?: AccountCategoryId[];
181
212
  }
182
213
  /** Input accepted by `provisionChannelAccount` (service token + `accounts:provision`). */
183
214
  export interface ProvisionChannelInput {
@@ -549,6 +580,18 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
549
580
  /**
550
581
  * Update an account's mutable profile fields. Tree placement changes
551
582
  * (reparenting) go through the dedicated move endpoint, not here.
583
+ *
584
+ * An account IS a user, so this write changes identity — and a profile
585
+ * screen never reads `/accounts/<id>`. It reads `GET /users/<id>` and
586
+ * `GET /profiles/username/<handle>`, both cached for 5 minutes in the
587
+ * CALLER'S OWN process, so busting only the account-graph keys left every
588
+ * profile surface serving the pre-edit avatar and name for the full TTL
589
+ * with a perfectly healthy server (the cross-service `oxy:user:invalidate`
590
+ * signal does not help: it evicts BACKEND caches, and cannot reach a cache
591
+ * living in a browser tab). {@link evictOxyIdentityCache} owns that key
592
+ * list — see its docs for why the handle-keyed entries are prefix-swept
593
+ * (a RENAME leaves the old handle's entry unreachable by any targeted key).
594
+ *
552
595
  * @param accountId - The account's Mongo `_id`.
553
596
  * @param data - Subset of updatable profile fields.
554
597
  */
@@ -44,8 +44,9 @@ export declare function OxyServicesDeviceBootMixin<T extends typeof OxyServicesB
44
44
  * Zero-cookie mint. Present the first-party `deviceId` + `deviceSecret` to
45
45
  * `POST /session/device/token` — NO bearer, NO cookies: possession of the
46
46
  * secret IS the device-ownership proof. Returns a fresh short access token
47
- * for the device's active account plus `nextDeviceSecret` (rotation-in-use)
48
- * and the projected device-session `state`.
47
+ * for the device's active account plus `nextDeviceSecret` (on mint, the same
48
+ * proven secret echoed back — rotation happens on sign-in, not mint) and
49
+ * the projected device-session `state`.
49
50
  *
50
51
  * `skipAuth`: this call carries no bearer, so a 401 must surface DIRECTLY —
51
52
  * never trigger `HttpService`'s 401→refresh→retry dance. The cold boot / re-
@@ -254,13 +254,15 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
254
254
  /**
255
255
  * Update user profile.
256
256
  *
257
- * Invalidates the SDK-side response cache for every endpoint that
258
- * returns the current user (`GET /users/me`, `GET /session/user/*`,
259
- * `GET /users/<id>`, `GET /profiles/username/*`) so the next read
260
- * doesn't return a stale snapshot. Without this, a follow-up
261
- * `getUserBySession` call inside the 2-minute cache window can return
262
- * the pre-update user most visibly during onboarding, where it
263
- * causes the username step to flicker back as if nothing was saved.
257
+ * Invalidates the SDK-side response cache for every endpoint that can
258
+ * return this user — the list is owned by {@link evictOxyIdentityCache}, so
259
+ * a new identity read is added in one place instead of to each writer
260
+ * separately (this method's own hand-written copy had already drifted from
261
+ * the server-side one, missing `GET /auth/lookup/*` and
262
+ * `GET /profiles/resolve`). Without the sweep a follow-up
263
+ * `getUserBySession` inside the cache window returns the pre-update user
264
+ * most visibly during onboarding, where the username step flickers back as
265
+ * if nothing was saved.
264
266
  *
265
267
  * TanStack Query handles offline queuing automatically.
266
268
  */
@@ -1,4 +1,4 @@
1
- import type { AccountKind, OrganizationCategory, UserNameResponse, UserRelationship, ThemePreference } from '@oxyhq/contracts';
1
+ import type { AccountKind, AccountCategoryId, UserNameResponse, UserRelationship, ThemePreference } from '@oxyhq/contracts';
2
2
  export interface OxyConfig {
3
3
  baseURL: string;
4
4
  cloudURL?: string;
@@ -151,8 +151,16 @@ export interface User {
151
151
  };
152
152
  isManagedAccount?: boolean;
153
153
  managedBy?: string;
154
- /** Real-estate taxonomy when this user is a `kind: 'organization'` account. */
155
- organizationCategory?: OrganizationCategory;
154
+ /**
155
+ * What this account is about, for any NON-personal account. ORDERED — the
156
+ * first element is the primary category, and nothing may reorder it.
157
+ *
158
+ * Stable ids, not labels: render each through the
159
+ * `accounts.accountCategory.<id>` translation key so the reader sees their own
160
+ * language rather than the language of whoever chose it. Absent when the
161
+ * account has none.
162
+ */
163
+ accountCategories?: AccountCategoryId[];
156
164
  /**
157
165
  * The account's languages as full BCP-47 locales (`language-REGION`, e.g.
158
166
  * `en-US`, `es-MX`, `pt-BR`), ordered with the PRIMARY (UI) locale first.
@@ -25,7 +25,9 @@ export type { OxyCorsOptions } from './cors';
25
25
  export { buildOxyCspDirectives, buildOxyPagesHeaders, createOxySecurityHeaders, formatOxyCspPolicy, OXY_CSP_BASELINE, } from './securityHeaders';
26
26
  export type { OxyCspDirective, OxyCspExtensions, OxyPagesHeadersOptions, OxySecurityHeadersOptions, } from './securityHeaders';
27
27
  export { verifySecret } from './verifySecret';
28
- export { createOxyUserInvalidationHandler, evictOxyIdentityCache, publishOxyUserInvalidation, } from './userInvalidation';
29
- export type { OxyIdentityCacheEvictor, OxyInvalidationPublisher, OxyUserInvalidationHandlerOptions, } from './userInvalidation';
28
+ export { createOxyUserInvalidationHandler, publishOxyUserInvalidation, } from './userInvalidation';
29
+ export type { OxyInvalidationPublisher, OxyUserInvalidationHandlerOptions, } from './userInvalidation';
30
+ export { evictOxyIdentityCache, oxyUserByIdCacheKey, OXY_IDENTITY_CACHE_PREFIXES } from '../utils/identityCacheSweep';
31
+ export type { OxyIdentityCacheEvictor } from '../utils/identityCacheSweep';
30
32
  export { registrableApex } from '../utils/registrableApex';
31
33
  export { isOfficialWebOrigin } from '../utils/officialOrigins';
@@ -6,10 +6,10 @@
6
6
  * Every Oxy backend caches Oxy identity, and none of them find out when it
7
7
  * changes. The `OxyServices` GET response cache holds `GET /users/:id` and
8
8
  * `GET /profiles/username/:name` for five minutes; it is swept when THIS process
9
- * writes the profile (see the `clearCacheEntry` calls in the user mixin) and
10
- * never when somebody else does — which is the normal case, since profiles are
11
- * edited in Oxy's own apps. So an avatar or display-name change is invisible to
12
- * every consuming backend for up to five minutes, per process.
9
+ * writes the profile (the `evictOxyIdentityCache` calls in the user and accounts
10
+ * mixins) and never when somebody else does — which is the normal case, since
11
+ * profiles are edited in Oxy's own apps. So an avatar or display-name change is
12
+ * invisible to every consuming backend for up to five minutes, per process.
13
13
  *
14
14
  * oxy-api broadcasts {@link OXY_USER_INVALIDATION_CHANNEL} on the shared Valkey
15
15
  * when a user's identity changes. This module is the consumer half: it parses
@@ -51,6 +51,7 @@
51
51
  * Node-only; exported solely from `@oxyhq/core/server`.
52
52
  */
53
53
  import { type OxyUserChangeReason, type OxyUserInvalidationEvent } from '@oxyhq/contracts';
54
+ import { type OxyIdentityCacheEvictor } from '../utils/identityCacheSweep';
54
55
  /**
55
56
  * The publish surface of a Redis client. Both `ioredis` and `node-redis`
56
57
  * satisfy this structurally, so neither library is a dependency here.
@@ -58,14 +59,6 @@ import { type OxyUserChangeReason, type OxyUserInvalidationEvent } from '@oxyhq/
58
59
  export interface OxyInvalidationPublisher {
59
60
  publish(channel: string, message: string): unknown;
60
61
  }
61
- /**
62
- * The cache-eviction surface of an {@link OxyServices} instance. Declared
63
- * structurally so this Node-only module does not pull in the client.
64
- */
65
- export interface OxyIdentityCacheEvictor {
66
- clearCacheEntry(key: string): void;
67
- clearCacheByPrefix(prefix: string): number;
68
- }
69
62
  /**
70
63
  * Broadcast that an Oxy user's record changed.
71
64
  *
@@ -119,15 +112,3 @@ export interface OxyUserInvalidationHandlerOptions {
119
112
  * fixes.
120
113
  */
121
114
  export declare function createOxyUserInvalidationHandler(options?: OxyUserInvalidationHandlerOptions): (raw: string) => void;
122
- /**
123
- * Sweep an `OxyServices` GET response cache of everything that could carry the
124
- * given user's identity.
125
- *
126
- * The by-id entry is exact. The by-username and resolve entries are keyed by
127
- * HANDLE, which cannot be derived from an id without the very lookup we are
128
- * invalidating, so those are swept by prefix — the same imprecision the SDK
129
- * already accepts when it sweeps its own cache after a local profile write, and
130
- * bounded by the fact that over-eviction costs a refetch and can never serve
131
- * wrong data.
132
- */
133
- export declare function evictOxyIdentityCache(oxy: OxyIdentityCacheEvictor, userId: string): void;
@@ -34,10 +34,10 @@ export interface RefreshDeps {
34
34
  * The outcome of ONE device-secret mint attempt (arm 1). Discriminated so both
35
35
  * the re-mint handler and the cold boot can react per the transport contract
36
36
  * without re-classifying the raw error:
37
- * - `ok` — minted, persisted the rotated secret, planted the token.
37
+ * - `ok` — minted, persisted `nextDeviceSecret`, planted the token.
38
38
  * - `no-secret` — the store holds no `deviceId` + `deviceSecret` to mint from.
39
39
  * - `invalid-secret` — 401 `invalid_device_secret`: the presented secret
40
- * diverged (another tab/device rotated it past the grace window).
40
+ * no longer matches the server's stored hash.
41
41
  * - `no-session` — 401 `no_active_session`: the device is known but has no live
42
42
  * session (authoritative signed-out).
43
43
  * - `account-not-on-device` — 401 `account_not_on_device` for a PINNED mint: the
@@ -45,10 +45,10 @@ export interface RefreshDeps {
45
45
  * The device secret is FINE — it is the identity binding that went stale, so
46
46
  * the caller must re-establish from the local key, never drop the credential.
47
47
  * - `transient` — network / 5xx; keep the secret, a later attempt can succeed.
48
- * - `persist-failed` — the mint succeeded (the SERVER rotated the secret) but
49
- * the rotated `nextDeviceSecret` could NOT be durably persisted. The token is
50
- * deliberately NOT planted: advertising a healthy session on a secret that
51
- * will not survive a reload is exactly the divergence that logs users out.
48
+ * - `persist-failed` — the mint succeeded but `nextDeviceSecret` could NOT be
49
+ * durably persisted. The token is deliberately NOT planted: advertising a
50
+ * healthy session on a secret that will not survive a reload is exactly the
51
+ * divergence that logs users out.
52
52
  */
53
53
  export type DeviceSecretMintOutcome = {
54
54
  status: 'ok';
@@ -69,19 +69,14 @@ export type DeviceSecretMintOutcome = {
69
69
  status: 'persist-failed';
70
70
  };
71
71
  /**
72
- * Arm 1 — the rotating device-secret mint, run under the owning client's
73
- * PROCESS-WIDE single-flight (`httpService.runSingleFlightDeviceSecretMint`).
72
+ * Arm 1 — the device-secret mint, run under the owning client's PROCESS-WIDE
73
+ * single-flight (`httpService.runSingleFlightDeviceSecretMint`).
74
74
  *
75
- * The server ROTATES the presented `deviceSecret` on every successful mint and
76
- * the just-presented secret is valid only for a short grace window. If two lanes
77
- * (cold boot, the proactive scheduler, a request-time preflight, a 401 retry,
78
- * the socket token transport, or a tab-focus reconcile) minted concurrently they
79
- * would double-rotate the server and the durable store could converge on the
80
- * SUPERSEDED secret — after the grace window the next cold boot mint 401s and the
81
- * user is signed out. Routing EVERY lane through this one single-flight makes
82
- * concurrent callers await the SAME in-flight mint and all receive its result, so
83
- * there is exactly one rotation and the store always converges on the true
84
- * `current` secret.
75
+ * Concurrent lanes (cold boot, the proactive scheduler, a request-time preflight,
76
+ * a 401 retry, the socket token transport, or a tab-focus reconcile) must not
77
+ * each persist a different view of the mint response. Routing EVERY lane through
78
+ * this one single-flight makes concurrent callers await the SAME in-flight mint
79
+ * and all receive its result, so the durable store converges on one credential.
85
80
  *
86
81
  * On success it persists `nextDeviceSecret` (read-back-verified) BEFORE planting
87
82
  * the access token; a failed durable persist yields `persist-failed` WITHOUT
@@ -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": "17.1.0",
3
+ "version": "19.0.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.23.0",
119
119
  "@oxyhq/protocol": "^0.1.6",
120
120
  "@scure/bip39": "^1.6.0",
121
121
  "@types/elliptic": "^6.4.18",
@@ -227,10 +227,9 @@ export async function runSessionColdBoot(
227
227
  // origin persisted a deviceId + deviceSecret, mint a short access token with
228
228
  // a single bearer-less POST (no cookie, no navigation). The mint itself runs
229
229
  // through `refreshDeviceSecretArm`, which acquires the client's PROCESS-WIDE
230
- // single-flight, persists the rotated `nextDeviceSecret` BEFORE planting the
231
- // token, and returns a classified outcome — so this step can never
232
- // double-rotate the server against the scheduler/transport/401 lanes, and
233
- // the durable store always converges on the true `current` secret.
230
+ // single-flight, persists `nextDeviceSecret` BEFORE planting the token, and
231
+ // returns a classified outcome — so concurrent mint lanes share one in-flight
232
+ // request and the durable store always converges on the server's credential.
234
233
  steps.push({
235
234
  id: 'device-secret-mint',
236
235
  // Network step — skip entirely when the caller reports the device offline so
@@ -249,7 +248,7 @@ export async function runSessionColdBoot(
249
248
  const result = await refreshDeviceSecretArm({ oxy, store, pin });
250
249
  switch (result.status) {
251
250
  case 'ok':
252
- // The arm persisted the rotated secret and planted the token.
251
+ // The arm persisted nextDeviceSecret and planted the token.
253
252
  return {
254
253
  kind: 'session',
255
254
  session: {
@@ -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",
@@ -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",
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,
@@ -136,7 +136,6 @@ export type {
136
136
  AccountCredentialWithSecret,
137
137
  RotateAccountCredentialResult,
138
138
  ListAccountsOptions,
139
- UserCreatableAccountKind,
140
139
  CreateAccountInput,
141
140
  UpdateAccountInput,
142
141
  ProvisionChannelInput,
@@ -168,7 +167,13 @@ export type {
168
167
  ApplicationUsageStats,
169
168
  } from './mixins/OxyServices.accounts';
170
169
 
171
- 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';
172
177
 
173
178
  // ---------------------------------------------------------------------------
174
179
  // Reputation (Oxy Trust: ledger, balances, disputes, rules, influence).