@oxyhq/core 18.0.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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/i18n/locales/en-US.json +48 -5
- package/dist/cjs/i18n/locales/es-ES.json +48 -5
- package/dist/cjs/i18n/locales/locales/en-US.json +48 -5
- package/dist/cjs/i18n/locales/locales/es-ES.json +48 -5
- package/dist/cjs/index.js +10 -6
- package/dist/cjs/mixins/OxyServices.accounts.js +27 -2
- package/dist/cjs/mixins/OxyServices.user.js +14 -20
- package/dist/cjs/server/index.js +8 -2
- package/dist/cjs/server/userInvalidation.js +6 -28
- package/dist/cjs/utils/identityCacheSweep.js +97 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/i18n/locales/en-US.json +48 -5
- package/dist/esm/i18n/locales/es-ES.json +48 -5
- package/dist/esm/i18n/locales/locales/en-US.json +48 -5
- package/dist/esm/i18n/locales/locales/es-ES.json +48 -5
- package/dist/esm/index.js +1 -1
- package/dist/esm/mixins/OxyServices.accounts.js +22 -1
- package/dist/esm/mixins/OxyServices.user.js +14 -20
- package/dist/esm/server/index.js +5 -1
- package/dist/esm/server/userInvalidation.js +5 -26
- package/dist/esm/utils/identityCacheSweep.js +92 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.accounts.d.ts +39 -7
- package/dist/types/mixins/OxyServices.user.d.ts +9 -7
- package/dist/types/models/interfaces.d.ts +11 -3
- package/dist/types/server/index.d.ts +4 -2
- package/dist/types/server/userInvalidation.d.ts +5 -24
- package/dist/types/utils/identityCacheSweep.d.ts +80 -0
- package/package.json +2 -2
- package/src/i18n/locales/en-US.json +48 -5
- package/src/i18n/locales/es-ES.json +48 -5
- package/src/index.ts +8 -2
- package/src/mixins/OxyServices.accounts.ts +58 -7
- package/src/mixins/OxyServices.user.ts +14 -20
- package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +370 -0
- package/src/models/interfaces.ts +11 -3
- package/src/server/__tests__/userInvalidation.test.ts +3 -20
- package/src/server/index.ts +5 -2
- package/src/server/userInvalidation.ts +8 -36
- package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
- package/src/utils/identityCacheSweep.ts +104 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -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,
|
|
43
|
-
export {
|
|
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 {
|
|
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 {
|
|
49
|
-
export { ACCOUNT_KINDS,
|
|
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.
|
|
@@ -168,8 +168,17 @@ export interface CreateAccountInput {
|
|
|
168
168
|
};
|
|
169
169
|
bio?: string;
|
|
170
170
|
avatar?: string;
|
|
171
|
-
/**
|
|
172
|
-
|
|
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[];
|
|
173
182
|
}
|
|
174
183
|
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
175
184
|
export interface UpdateAccountInput {
|
|
@@ -187,8 +196,19 @@ export interface UpdateAccountInput {
|
|
|
187
196
|
};
|
|
188
197
|
bio?: string | null;
|
|
189
198
|
avatar?: string | null;
|
|
190
|
-
/**
|
|
191
|
-
|
|
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[];
|
|
192
212
|
}
|
|
193
213
|
/** Input accepted by `provisionChannelAccount` (service token + `accounts:provision`). */
|
|
194
214
|
export interface ProvisionChannelInput {
|
|
@@ -560,6 +580,18 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
560
580
|
/**
|
|
561
581
|
* Update an account's mutable profile fields. Tree placement changes
|
|
562
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
|
+
*
|
|
563
595
|
* @param accountId - The account's Mongo `_id`.
|
|
564
596
|
* @param data - Subset of updatable profile fields.
|
|
565
597
|
*/
|
|
@@ -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
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
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,
|
|
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
|
-
/**
|
|
155
|
-
|
|
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,
|
|
29
|
-
export type {
|
|
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 (
|
|
10
|
-
* never when somebody else does — which is the normal case, since
|
|
11
|
-
* edited in Oxy's own apps. So an avatar or display-name change is
|
|
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;
|
|
@@ -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": "
|
|
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.
|
|
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",
|
|
@@ -1748,11 +1748,53 @@
|
|
|
1748
1748
|
"description": "A programmatic account with service credentials"
|
|
1749
1749
|
}
|
|
1750
1750
|
},
|
|
1751
|
-
"
|
|
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
|
-
"
|
|
1755
|
-
"
|
|
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
|
-
"
|
|
1776
|
-
"label": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1755
|
-
"
|
|
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
|
-
"
|
|
1776
|
-
"label": "
|
|
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
|
-
|
|
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 {
|
|
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).
|
|
@@ -33,10 +33,11 @@
|
|
|
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 {
|
|
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
|
import { normalizeUserIdentity } from '../utils/userIdentity';
|
|
40
|
+
import { evictOxyIdentityCache } from '../utils/identityCacheSweep';
|
|
40
41
|
import { CACHE_TIMES } from './mixinHelpers';
|
|
41
42
|
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
@@ -52,8 +53,16 @@ import { CACHE_TIMES } from './mixinHelpers';
|
|
|
52
53
|
*
|
|
53
54
|
* Single source of truth is `@oxyhq/contracts`.
|
|
54
55
|
*/
|
|
55
|
-
export type {
|
|
56
|
-
export {
|
|
56
|
+
export type { AccountCategoryId, AccountKind } from '@oxyhq/contracts';
|
|
57
|
+
export {
|
|
58
|
+
ACCOUNT_CATEGORY_IDS,
|
|
59
|
+
ACCOUNT_KINDS,
|
|
60
|
+
MAX_ACCOUNT_CATEGORIES,
|
|
61
|
+
SELECTABLE_ACCOUNT_CATEGORY_IDS,
|
|
62
|
+
isActAsEligibleKind,
|
|
63
|
+
isSelectableAccountCategoryId,
|
|
64
|
+
kindAcceptsAccountCategories,
|
|
65
|
+
} from '@oxyhq/contracts';
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
68
|
* The calling user's relationship to an account node, as resolved by the API:
|
|
@@ -179,8 +188,17 @@ export interface CreateAccountInput {
|
|
|
179
188
|
name?: { first?: string; last?: string; displayName?: string };
|
|
180
189
|
bio?: string;
|
|
181
190
|
avatar?: string;
|
|
182
|
-
/**
|
|
183
|
-
|
|
191
|
+
/**
|
|
192
|
+
* What the account is about. ORDERED — the FIRST element is the primary
|
|
193
|
+
* category, so a picker must submit them in the order the user arranged them
|
|
194
|
+
* and must not sort. Stable ids, never labels: render each one through the
|
|
195
|
+
* `accounts.accountCategory.<id>` translation key.
|
|
196
|
+
*
|
|
197
|
+
* Offer `SELECTABLE_ACCOUNT_CATEGORY_IDS`, not `ACCOUNT_CATEGORY_IDS` — the
|
|
198
|
+
* latter still contains withdrawn ids so that accounts already carrying one
|
|
199
|
+
* keep working. At most `MAX_ACCOUNT_CATEGORIES`, no duplicates.
|
|
200
|
+
*/
|
|
201
|
+
accountCategories?: AccountCategoryId[];
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
@@ -195,8 +213,19 @@ export interface UpdateAccountInput {
|
|
|
195
213
|
name?: { first?: string; last?: string; displayName?: string };
|
|
196
214
|
bio?: string | null;
|
|
197
215
|
avatar?: string | null;
|
|
198
|
-
/**
|
|
199
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Replaces the WHOLE list, in the order given — there is no add/remove verb,
|
|
218
|
+
* because a partial edit cannot express a re-ordering and the order is what
|
|
219
|
+
* names the primary category. `[]` clears it.
|
|
220
|
+
*
|
|
221
|
+
* Not nullable, unlike `bio` and `avatar`: the empty case already has a
|
|
222
|
+
* spelling of its own, so a second one could only ever disagree with it.
|
|
223
|
+
*
|
|
224
|
+
* Rejected for a `personal` account, and rejected when it ADDS a withdrawn
|
|
225
|
+
* id the account did not already carry — keeping or re-ordering one it has is
|
|
226
|
+
* always allowed.
|
|
227
|
+
*/
|
|
228
|
+
accountCategories?: AccountCategoryId[];
|
|
200
229
|
}
|
|
201
230
|
|
|
202
231
|
/** Input accepted by `provisionChannelAccount` (service token + `accounts:provision`). */
|
|
@@ -736,6 +765,18 @@ export function OxyServicesAccountsMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
736
765
|
/**
|
|
737
766
|
* Update an account's mutable profile fields. Tree placement changes
|
|
738
767
|
* (reparenting) go through the dedicated move endpoint, not here.
|
|
768
|
+
*
|
|
769
|
+
* An account IS a user, so this write changes identity — and a profile
|
|
770
|
+
* screen never reads `/accounts/<id>`. It reads `GET /users/<id>` and
|
|
771
|
+
* `GET /profiles/username/<handle>`, both cached for 5 minutes in the
|
|
772
|
+
* CALLER'S OWN process, so busting only the account-graph keys left every
|
|
773
|
+
* profile surface serving the pre-edit avatar and name for the full TTL
|
|
774
|
+
* with a perfectly healthy server (the cross-service `oxy:user:invalidate`
|
|
775
|
+
* signal does not help: it evicts BACKEND caches, and cannot reach a cache
|
|
776
|
+
* living in a browser tab). {@link evictOxyIdentityCache} owns that key
|
|
777
|
+
* list — see its docs for why the handle-keyed entries are prefix-swept
|
|
778
|
+
* (a RENAME leaves the old handle's entry unreachable by any targeted key).
|
|
779
|
+
*
|
|
739
780
|
* @param accountId - The account's Mongo `_id`.
|
|
740
781
|
* @param data - Subset of updatable profile fields.
|
|
741
782
|
*/
|
|
@@ -754,6 +795,16 @@ export function OxyServicesAccountsMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
754
795
|
// data) so neither serves the pre-update snapshot.
|
|
755
796
|
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
756
797
|
this._invalidateAccountLists();
|
|
798
|
+
// The parent's children list embeds this account's profile and is keyed
|
|
799
|
+
// by the PARENT id, so it is reachable only from the response node.
|
|
800
|
+
const parentAccountId = res.account?.parentAccountId;
|
|
801
|
+
if (parentAccountId) {
|
|
802
|
+
this.clearCacheEntry(
|
|
803
|
+
`GET:/accounts/${encodeURIComponent(parentAccountId)}/children`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
// Every identity read of this account, whichever key it lands under.
|
|
807
|
+
evictOxyIdentityCache(this, accountId);
|
|
757
808
|
return res.account;
|
|
758
809
|
} catch (error) {
|
|
759
810
|
throw this.handleError(error);
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { KeyManager } from '../crypto/keyManager';
|
|
29
29
|
import { SignatureService } from '../crypto/signatureService';
|
|
30
30
|
import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity';
|
|
31
|
+
import { evictOxyIdentityCache } from '../utils/identityCacheSweep';
|
|
31
32
|
import { logger } from '../logger';
|
|
32
33
|
import { extractErrorStatus } from '../utils/errorUtils';
|
|
33
34
|
|
|
@@ -534,13 +535,15 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
534
535
|
/**
|
|
535
536
|
* Update user profile.
|
|
536
537
|
*
|
|
537
|
-
* Invalidates the SDK-side response cache for every endpoint that
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
538
|
+
* Invalidates the SDK-side response cache for every endpoint that can
|
|
539
|
+
* return this user — the list is owned by {@link evictOxyIdentityCache}, so
|
|
540
|
+
* a new identity read is added in one place instead of to each writer
|
|
541
|
+
* separately (this method's own hand-written copy had already drifted from
|
|
542
|
+
* the server-side one, missing `GET /auth/lookup/*` and
|
|
543
|
+
* `GET /profiles/resolve`). Without the sweep a follow-up
|
|
544
|
+
* `getUserBySession` inside the cache window returns the pre-update user —
|
|
545
|
+
* most visibly during onboarding, where the username step flickers back as
|
|
546
|
+
* if nothing was saved.
|
|
544
547
|
*
|
|
545
548
|
* TanStack Query handles offline queuing automatically.
|
|
546
549
|
*/
|
|
@@ -550,15 +553,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
550
553
|
await this.makeRequest<User>('PUT', '/users/me', updates, { cache: false }),
|
|
551
554
|
);
|
|
552
555
|
|
|
553
|
-
|
|
554
|
-
// prefix sweep rather than an enumeration because the SDK never
|
|
555
|
-
// tracks the set of active session IDs centrally.
|
|
556
|
-
this.clearCacheByPrefix('GET:/session/user/');
|
|
557
|
-
this.clearCacheByPrefix('GET:/users/me');
|
|
558
|
-
this.clearCacheByPrefix('GET:/profiles/username/');
|
|
559
|
-
if (result?.id) {
|
|
560
|
-
this.clearCacheEntry(`GET:/users/${result.id}`);
|
|
561
|
-
}
|
|
556
|
+
evictOxyIdentityCache(this, result?.id);
|
|
562
557
|
|
|
563
558
|
return result;
|
|
564
559
|
} catch (error) {
|
|
@@ -615,10 +610,9 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
615
610
|
const result = await this.makeRequest<PrivacySettings>('PATCH', `/privacy/${id}/privacy`, settings, {
|
|
616
611
|
cache: false,
|
|
617
612
|
});
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
this
|
|
621
|
-
this.clearCacheEntry(`GET:/users/${id}`);
|
|
613
|
+
// Privacy settings ride the user DTO, so every identity read goes stale
|
|
614
|
+
// too — same key list as any other profile write.
|
|
615
|
+
evictOxyIdentityCache(this, id);
|
|
622
616
|
this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
|
|
623
617
|
return result;
|
|
624
618
|
} catch (error) {
|