@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/i18n/locales/en-US.json +49 -5
- package/dist/cjs/i18n/locales/es-ES.json +49 -5
- package/dist/cjs/i18n/locales/locales/en-US.json +49 -5
- package/dist/cjs/i18n/locales/locales/es-ES.json +49 -5
- package/dist/cjs/index.js +17 -6
- package/dist/cjs/mixins/OxyServices.accounts.js +69 -31
- package/dist/cjs/mixins/OxyServices.followGraph.js +204 -0
- package/dist/cjs/mixins/OxyServices.user.js +17 -20
- package/dist/cjs/mixins/index.js +4 -0
- package/dist/cjs/server/index.js +8 -2
- package/dist/cjs/server/userInvalidation.js +6 -28
- package/dist/cjs/session/accountProjection.js +45 -9
- package/dist/cjs/utils/accountCacheSweep.js +80 -0
- package/dist/cjs/utils/identityCacheSweep.js +97 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/i18n/locales/en-US.json +49 -5
- package/dist/esm/i18n/locales/es-ES.json +49 -5
- package/dist/esm/i18n/locales/locales/en-US.json +49 -5
- package/dist/esm/i18n/locales/locales/es-ES.json +49 -5
- package/dist/esm/index.js +8 -2
- package/dist/esm/mixins/OxyServices.accounts.js +64 -30
- package/dist/esm/mixins/OxyServices.followGraph.js +201 -0
- package/dist/esm/mixins/OxyServices.user.js +17 -20
- package/dist/esm/mixins/index.js +4 -0
- package/dist/esm/server/index.js +5 -1
- package/dist/esm/server/userInvalidation.js +5 -26
- package/dist/esm/session/accountProjection.js +44 -9
- package/dist/esm/utils/accountCacheSweep.js +75 -0
- package/dist/esm/utils/identityCacheSweep.js +92 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/mixins/OxyServices.accounts.d.ts +91 -34
- package/dist/types/mixins/OxyServices.followGraph.d.ts +211 -0
- package/dist/types/mixins/OxyServices.user.d.ts +10 -7
- package/dist/types/mixins/index.d.ts +2 -1
- 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/session/accountProjection.d.ts +38 -4
- package/dist/types/utils/accountCacheSweep.d.ts +75 -0
- package/dist/types/utils/identityCacheSweep.d.ts +80 -0
- package/package.json +2 -2
- package/src/i18n/locales/en-US.json +49 -5
- package/src/i18n/locales/es-ES.json +49 -5
- package/src/index.ts +15 -2
- package/src/mixins/OxyServices.accounts.ts +123 -45
- package/src/mixins/OxyServices.followGraph.ts +266 -0
- package/src/mixins/OxyServices.user.ts +17 -20
- package/src/mixins/__tests__/accounts.test.ts +5 -0
- package/src/mixins/__tests__/followGraph.test.ts +128 -0
- package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +407 -0
- package/src/mixins/index.ts +5 -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/session/__tests__/accountProjection.test.ts +109 -2
- package/src/session/accountProjection.ts +47 -9
- package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
- package/src/utils/accountCacheSweep.ts +93 -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';
|
|
@@ -104,7 +104,7 @@ export type { SocketIOFactory, MinimalSocket } from './session/socketLoader';
|
|
|
104
104
|
export { createSessionClientHost } from './session/sessionClientHost';
|
|
105
105
|
export { createSessionClient } from './session/createSessionClient';
|
|
106
106
|
export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountIdsOf, } from './session/projectSessionState';
|
|
107
|
-
export { projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection';
|
|
107
|
+
export { isSwitchTargetAccount, projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection';
|
|
108
108
|
export type { SwitchableAccount, SwitchableAccountUser, ProjectSwitchableAccountsInput, } from './session/accountProjection';
|
|
109
109
|
export { AccountDialogController, createAccountDialogController, } from './session/accountDialogController';
|
|
110
110
|
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, PopupWindowHandle, SignInFlowPhase, SignInFlowState, SignInProgress, } from './session/accountDialogController';
|
|
@@ -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.
|
|
@@ -66,8 +66,8 @@ export type AccountMemberStatus = 'active' | 'invited' | 'removed';
|
|
|
66
66
|
*/
|
|
67
67
|
export type AccountMemberSource = 'direct' | 'inherited';
|
|
68
68
|
/**
|
|
69
|
-
* Client-facing AccountMember shape. `permissions` is
|
|
70
|
-
*
|
|
69
|
+
* Client-facing AccountMember shape. `permissions` is the effective permission
|
|
70
|
+
* set (role baseline plus `permissionGrants` minus `permissionRevokes`).
|
|
71
71
|
*/
|
|
72
72
|
export interface AccountMember {
|
|
73
73
|
_id: string;
|
|
@@ -77,6 +77,10 @@ export interface AccountMember {
|
|
|
77
77
|
memberUserId: string;
|
|
78
78
|
role: AccountRole;
|
|
79
79
|
permissions: string[];
|
|
80
|
+
/** Permissions granted beyond the role baseline. */
|
|
81
|
+
permissionGrants?: string[];
|
|
82
|
+
/** Permissions revoked from the role baseline. */
|
|
83
|
+
permissionRevokes?: string[];
|
|
80
84
|
/**
|
|
81
85
|
* Whether this membership cascades to the account's subtree. `true` (default)
|
|
82
86
|
* lets descendants inherit this role unless a nearer row overrides it; `false`
|
|
@@ -85,12 +89,20 @@ export interface AccountMember {
|
|
|
85
89
|
inherit: boolean;
|
|
86
90
|
status: AccountMemberStatus;
|
|
87
91
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
+
* Where this membership COMES FROM relative to the account it is being
|
|
93
|
+
* reported for: `direct` when the row lives on that account, `inherited` when
|
|
94
|
+
* it lives on an ancestor whose `inherit` flag cascades it down.
|
|
95
|
+
*
|
|
96
|
+
* Present on every membership the API serialises — a resolved
|
|
97
|
+
* `callerMembership` and every entry of a member list alike. It is not
|
|
98
|
+
* decoration: an `inherited` entry's `accountId` is the ANCESTOR's, and the
|
|
99
|
+
* member-mutation endpoints are scoped to rows on the account named in the
|
|
100
|
+
* path, so `PATCH`/`DELETE .../members/<that row's _id>` against the
|
|
101
|
+
* descendant 404s. **Branch on `source === 'direct'` before offering to edit,
|
|
102
|
+
* remove or transfer to a member**, and count owners for a last-owner check
|
|
103
|
+
* over direct entries only.
|
|
92
104
|
*/
|
|
93
|
-
source
|
|
105
|
+
source: AccountMemberSource;
|
|
94
106
|
invitedByUserId?: string | null;
|
|
95
107
|
joinedAt?: string | null;
|
|
96
108
|
createdAt: string;
|
|
@@ -168,8 +180,17 @@ export interface CreateAccountInput {
|
|
|
168
180
|
};
|
|
169
181
|
bio?: string;
|
|
170
182
|
avatar?: string;
|
|
171
|
-
/**
|
|
172
|
-
|
|
183
|
+
/**
|
|
184
|
+
* What the account is about. ORDERED — the FIRST element is the primary
|
|
185
|
+
* category, so a picker must submit them in the order the user arranged them
|
|
186
|
+
* and must not sort. Stable ids, never labels: render each one through the
|
|
187
|
+
* `accounts.accountCategory.<id>` translation key.
|
|
188
|
+
*
|
|
189
|
+
* Offer `SELECTABLE_ACCOUNT_CATEGORY_IDS`, not `ACCOUNT_CATEGORY_IDS` — the
|
|
190
|
+
* latter still contains withdrawn ids so that accounts already carrying one
|
|
191
|
+
* keep working. At most `MAX_ACCOUNT_CATEGORIES`, no duplicates.
|
|
192
|
+
*/
|
|
193
|
+
accountCategories?: AccountCategoryId[];
|
|
173
194
|
}
|
|
174
195
|
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
175
196
|
export interface UpdateAccountInput {
|
|
@@ -187,8 +208,19 @@ export interface UpdateAccountInput {
|
|
|
187
208
|
};
|
|
188
209
|
bio?: string | null;
|
|
189
210
|
avatar?: string | null;
|
|
190
|
-
/**
|
|
191
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Replaces the WHOLE list, in the order given — there is no add/remove verb,
|
|
213
|
+
* because a partial edit cannot express a re-ordering and the order is what
|
|
214
|
+
* names the primary category. `[]` clears it.
|
|
215
|
+
*
|
|
216
|
+
* Not nullable, unlike `bio` and `avatar`: the empty case already has a
|
|
217
|
+
* spelling of its own, so a second one could only ever disagree with it.
|
|
218
|
+
*
|
|
219
|
+
* Rejected for a `personal` account, and rejected when it ADDS a withdrawn
|
|
220
|
+
* id the account did not already carry — keeping or re-ordering one it has is
|
|
221
|
+
* always allowed.
|
|
222
|
+
*/
|
|
223
|
+
accountCategories?: AccountCategoryId[];
|
|
192
224
|
}
|
|
193
225
|
/** Input accepted by `provisionChannelAccount` (service token + `accounts:provision`). */
|
|
194
226
|
export interface ProvisionChannelInput {
|
|
@@ -226,7 +258,10 @@ export interface InviteAccountMemberInput {
|
|
|
226
258
|
}
|
|
227
259
|
/** Input accepted by `updateAccountMember`. The owner role cannot be assigned. */
|
|
228
260
|
export interface UpdateAccountMemberInput {
|
|
229
|
-
role
|
|
261
|
+
role?: Exclude<AccountRole, 'owner'>;
|
|
262
|
+
inherit?: boolean;
|
|
263
|
+
permissionGrants?: string[];
|
|
264
|
+
permissionRevokes?: string[];
|
|
230
265
|
}
|
|
231
266
|
/** Input accepted by `transferAccountOwnership`. */
|
|
232
267
|
export interface TransferAccountOwnershipInput {
|
|
@@ -560,6 +595,18 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
560
595
|
/**
|
|
561
596
|
* Update an account's mutable profile fields. Tree placement changes
|
|
562
597
|
* (reparenting) go through the dedicated move endpoint, not here.
|
|
598
|
+
*
|
|
599
|
+
* An account IS a user, so this write changes identity — and a profile
|
|
600
|
+
* screen never reads `/accounts/<id>`. It reads `GET /users/<id>` and
|
|
601
|
+
* `GET /profiles/username/<handle>`, both cached for 5 minutes in the
|
|
602
|
+
* CALLER'S OWN process, so busting only the account-graph keys left every
|
|
603
|
+
* profile surface serving the pre-edit avatar and name for the full TTL
|
|
604
|
+
* with a perfectly healthy server (the cross-service `oxy:user:invalidate`
|
|
605
|
+
* signal does not help: it evicts BACKEND caches, and cannot reach a cache
|
|
606
|
+
* living in a browser tab). {@link evictOxyIdentityCache} owns that key
|
|
607
|
+
* list — see its docs for why the handle-keyed entries are prefix-swept
|
|
608
|
+
* (a RENAME leaves the old handle's entry unreachable by any targeted key).
|
|
609
|
+
*
|
|
563
610
|
* @param accountId - The account's Mongo `_id`.
|
|
564
611
|
* @param data - Subset of updatable profile fields.
|
|
565
612
|
*/
|
|
@@ -577,7 +624,27 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
577
624
|
*/
|
|
578
625
|
listChildAccounts(accountId: string): Promise<AccountNode[]>;
|
|
579
626
|
/**
|
|
580
|
-
* List members of an account
|
|
627
|
+
* List the members of an account: the membership rows ON it, plus the rows
|
|
628
|
+
* on its ancestors that cascade into it. Each entry carries `source`
|
|
629
|
+
* (`direct` | `inherited`) saying which it is.
|
|
630
|
+
*
|
|
631
|
+
* Inherited entries are members in every sense the server enforces — an
|
|
632
|
+
* ancestor row with `inherit: true` resolves through
|
|
633
|
+
* `resolveEffectiveAccess` and confers every account permission on the
|
|
634
|
+
* descendant, `account:act_as` included — so a roster that omitted them
|
|
635
|
+
* answered `[]` for accounts several people could act on.
|
|
636
|
+
*
|
|
637
|
+
* Two things follow for a caller. An entry's `accountId` is the account its
|
|
638
|
+
* ROW lives on, so an inherited entry names an ancestor rather than the
|
|
639
|
+
* account you asked about; and the member-mutation endpoints only accept
|
|
640
|
+
* rows on the account in the path, so gate any edit/remove/transfer
|
|
641
|
+
* affordance on `source === 'direct'`.
|
|
642
|
+
*
|
|
643
|
+
* Asking what the CALLER holds over an account is a different question, and
|
|
644
|
+
* scanning this list for yourself is the wrong way to answer it — use
|
|
645
|
+
* {@link OxyServicesAccountsMixin.getAccount}, whose `callerMembership` is
|
|
646
|
+
* the server's own resolution.
|
|
647
|
+
*
|
|
581
648
|
* @param accountId - The account's Mongo `_id`.
|
|
582
649
|
*/
|
|
583
650
|
listAccountMembers(accountId: string): Promise<AccountMember[]>;
|
|
@@ -702,28 +769,18 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
702
769
|
*/
|
|
703
770
|
getAppUsage(applicationId: string, period?: ApplicationUsagePeriod): Promise<ApplicationUsageStats>;
|
|
704
771
|
/**
|
|
705
|
-
* Bust
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
* unscoped entry plus every `?`-query variant via a prefix sweep. The prefix
|
|
710
|
-
* `GET:/accounts?` matches only the query-string list variants, never the
|
|
711
|
-
* `GET:/accounts/<id>…` detail/sub-resource keys.
|
|
772
|
+
* Bust the cached member list and detail for an account after a membership
|
|
773
|
+
* mutation. The member list (`listAccountMembers`) and the detail
|
|
774
|
+
* (`getAccount`, which can embed the caller's membership) both go stale when
|
|
775
|
+
* the member set or a member's role changes.
|
|
712
776
|
*
|
|
713
777
|
* Internal helper (leading underscore); not part of the supported public
|
|
714
778
|
* surface. Public rather than `private` because mixins compose into an
|
|
715
779
|
* exported anonymous class, where TypeScript cannot represent a private
|
|
716
780
|
* member in the emitted declaration file (TS4094).
|
|
717
|
-
*/
|
|
718
|
-
_invalidateAccountLists(): void;
|
|
719
|
-
/**
|
|
720
|
-
* Bust the cached member list and detail for an account after a membership
|
|
721
|
-
* mutation. The member list (`listAccountMembers`) and the detail
|
|
722
|
-
* (`getAccount`, which can embed the caller's membership) both go stale when
|
|
723
|
-
* the member set or a member's role changes.
|
|
724
781
|
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
782
|
+
* The forest keys themselves are NOT owned here — see
|
|
783
|
+
* `utils/accountCacheSweep`, which the user mixin has to reach as well.
|
|
727
784
|
*/
|
|
728
785
|
_invalidateAccountMembership(accountId: string): void;
|
|
729
786
|
/**
|
|
@@ -735,8 +792,8 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
735
792
|
* query-string list variants, never the `GET:/applications/<id>…`
|
|
736
793
|
* detail/sub-resource keys.
|
|
737
794
|
*
|
|
738
|
-
* Internal helper (leading underscore); see `
|
|
739
|
-
* this is public rather than `private`.
|
|
795
|
+
* Internal helper (leading underscore); see `_invalidateAccountMembership`
|
|
796
|
+
* for why this is public rather than `private`.
|
|
740
797
|
*/
|
|
741
798
|
_invalidateAppLists(): void;
|
|
742
799
|
httpService: import("../HttpService").HttpService;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follow Graph Mixin (`/v2/follows`)
|
|
3
|
+
*
|
|
4
|
+
* The user-owned follow graph: one relationship per user and target, shared by
|
|
5
|
+
* every application, with per-application context on top. This is the SDK half
|
|
6
|
+
* of #809 and the replacement for the per-app follow endpoints each application
|
|
7
|
+
* grew for itself.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is not `followUser` with more parameters
|
|
10
|
+
*
|
|
11
|
+
* `followUser` answers "does A follow B" and nothing else. This answers "what
|
|
12
|
+
* does this user follow, anywhere, and which applications act on it" — a
|
|
13
|
+
* different question with a different owner. The legacy methods stay for the
|
|
14
|
+
* Mongo-backed social graph they were written for; new kinds (topics, stores,
|
|
15
|
+
* artists, channels) come here, and users will migrate behind an adapter rather
|
|
16
|
+
* than through a flag day.
|
|
17
|
+
*
|
|
18
|
+
* ## Caching
|
|
19
|
+
*
|
|
20
|
+
* Every method is `cache: false`. A follow status is exactly the shape that
|
|
21
|
+
* must never be served stale: the SDK's GET cache is identity-scoped but
|
|
22
|
+
* time-based, and a status cached across a write is the "follow reverts after
|
|
23
|
+
* navigating away and back" bug — which the legacy `followUser` had to fix with
|
|
24
|
+
* explicit invalidation. Not caching at this layer means an app's own store
|
|
25
|
+
* (React Query, Zustand) is the single cache authority, which is the rule the
|
|
26
|
+
* ecosystem already follows for anything written and read in the same session.
|
|
27
|
+
*/
|
|
28
|
+
import type { FollowListPage, FollowMutation, FollowOptions, FollowStatus, UnfollowMutation } from '@oxyhq/contracts';
|
|
29
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
30
|
+
export declare function OxyServicesFollowGraphMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
31
|
+
new (...args: any[]): {
|
|
32
|
+
/**
|
|
33
|
+
* Follow a target. Idempotent — following something already followed
|
|
34
|
+
* returns the same relationship with `created: false`.
|
|
35
|
+
*
|
|
36
|
+
* The follower and the acting application are BOTH derived server-side from
|
|
37
|
+
* the session. There is deliberately no parameter for either: a client that
|
|
38
|
+
* could name them could forge a follow on another user's behalf, or record
|
|
39
|
+
* one as coming from an application it is not.
|
|
40
|
+
*
|
|
41
|
+
* @param targetId - The registered target's id, not its URI. Registration is
|
|
42
|
+
* a separate operation precisely so following cannot silently create
|
|
43
|
+
* targets — a typo would otherwise become a permanent row nobody follows.
|
|
44
|
+
* @param options.expiresIn - Seconds until the follow lapses on its own. For
|
|
45
|
+
* an event, a trial, a topic followed for a week. The server bounds it.
|
|
46
|
+
*/
|
|
47
|
+
followTarget(targetId: string, options?: FollowOptions): Promise<FollowMutation>;
|
|
48
|
+
/**
|
|
49
|
+
* Unfollow everywhere.
|
|
50
|
+
*
|
|
51
|
+
* There is no "unfollow here" — that is `setFollowApplicationMode(...,
|
|
52
|
+
* 'disabled')`, and keeping the two distinct is the point of the design. An
|
|
53
|
+
* application that quietly turned a global unfollow into a local one would
|
|
54
|
+
* leave the user believing they had stopped following something they still
|
|
55
|
+
* follow everywhere else.
|
|
56
|
+
*
|
|
57
|
+
* Idempotent: `removed: false` when it was already gone, because the state
|
|
58
|
+
* the caller asked for is the state that holds.
|
|
59
|
+
*/
|
|
60
|
+
unfollowTarget(relationshipId: string): Promise<UnfollowMutation>;
|
|
61
|
+
/**
|
|
62
|
+
* The three-part status: globally, in this application, and in effect.
|
|
63
|
+
*
|
|
64
|
+
* Render `effectiveState` on the button and keep the other two for the
|
|
65
|
+
* explanation. A UI that collapses them cannot tell the user why a follow
|
|
66
|
+
* they can see in their list is not showing up in this app's feed.
|
|
67
|
+
*/
|
|
68
|
+
getFollowTargetStatus(targetId: string): Promise<FollowStatus>;
|
|
69
|
+
/**
|
|
70
|
+
* Turn a relationship off, or back on, in ONE application.
|
|
71
|
+
*
|
|
72
|
+
* Omit `applicationId` and it applies to the calling application, which is
|
|
73
|
+
* the only form an ordinary app should ever need. Naming a DIFFERENT
|
|
74
|
+
* application requires `follows:manage` server-side — acting on another
|
|
75
|
+
* app's behalf is exactly the cross-application authority this design
|
|
76
|
+
* otherwise refuses, so it is a distinct permission and not a parameter an
|
|
77
|
+
* app happens to fill in.
|
|
78
|
+
*/
|
|
79
|
+
setFollowApplicationMode(relationshipId: string, mode: "enabled" | "disabled", applicationId?: string): Promise<{
|
|
80
|
+
ok: true;
|
|
81
|
+
mode: "enabled" | "disabled";
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* Drop the override so this application follows the global relationship
|
|
85
|
+
* again. Distinct from setting `enabled`: inheriting means a later global
|
|
86
|
+
* change takes effect here, and an explicit `enabled` means it does not.
|
|
87
|
+
*/
|
|
88
|
+
restoreFollowInheritance(relationshipId: string, applicationId?: string): Promise<{
|
|
89
|
+
ok: true;
|
|
90
|
+
}>;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a target by canonical URI, registering it the first time anyone
|
|
93
|
+
* asks. The call an application makes on the way into a screen, before it
|
|
94
|
+
* can render a button.
|
|
95
|
+
*
|
|
96
|
+
* Idempotent on the URI, which is what makes two applications describing
|
|
97
|
+
* the same thing — the same fediverse actor, the same topic — arrive at ONE
|
|
98
|
+
* row, and therefore at one relationship per user rather than one per app.
|
|
99
|
+
*
|
|
100
|
+
* `metadata` is a display snapshot (name, handle, icon) and is refreshed
|
|
101
|
+
* only for the application that provides the target: a second application
|
|
102
|
+
* passing its own idea of the name would make the display flip depending on
|
|
103
|
+
* which app last looked.
|
|
104
|
+
*/
|
|
105
|
+
ensureFollowTarget(input: {
|
|
106
|
+
uri: string;
|
|
107
|
+
kind: string;
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
providerReference?: string;
|
|
110
|
+
localUserId?: string;
|
|
111
|
+
}): Promise<{
|
|
112
|
+
id: string;
|
|
113
|
+
uri: string;
|
|
114
|
+
kind: string;
|
|
115
|
+
created: boolean;
|
|
116
|
+
}>;
|
|
117
|
+
/**
|
|
118
|
+
* Claim a namespace for the calling application. First come, and idempotent
|
|
119
|
+
* for the holder — an application that registers on every boot must not
|
|
120
|
+
* fail the second time.
|
|
121
|
+
*/
|
|
122
|
+
claimFollowNamespace(namespace: string): Promise<{
|
|
123
|
+
namespace: string;
|
|
124
|
+
created: boolean;
|
|
125
|
+
}>;
|
|
126
|
+
/**
|
|
127
|
+
* Declare what following a kind of thing MEANS: the verb clients render,
|
|
128
|
+
* whether reverse lookups are public, whether it federates.
|
|
129
|
+
*
|
|
130
|
+
* Declared once by the application that owns the concept, rather than
|
|
131
|
+
* passed per call site — otherwise two screens of one app can disagree
|
|
132
|
+
* about whether a store is followed or subscribed to.
|
|
133
|
+
*/
|
|
134
|
+
registerFollowKind(input: {
|
|
135
|
+
kind: string;
|
|
136
|
+
label?: string;
|
|
137
|
+
capabilities?: {
|
|
138
|
+
verb?: "follow" | "subscribe" | "join";
|
|
139
|
+
reverse?: "public" | "private" | "aggregate" | "unavailable";
|
|
140
|
+
federated?: boolean;
|
|
141
|
+
};
|
|
142
|
+
}): Promise<{
|
|
143
|
+
kind: string;
|
|
144
|
+
created: boolean;
|
|
145
|
+
}>;
|
|
146
|
+
/**
|
|
147
|
+
* Everything the signed-in user follows, newest first.
|
|
148
|
+
*
|
|
149
|
+
* Owner-only by construction server-side — there is no parameter naming a
|
|
150
|
+
* user, so this cannot be pointed at somebody else's graph.
|
|
151
|
+
*
|
|
152
|
+
* Paginate by passing back `nextCursor`, never an offset: the list changes
|
|
153
|
+
* while it is being read, and an offset silently skips or repeats rows
|
|
154
|
+
* exactly when it does.
|
|
155
|
+
*/
|
|
156
|
+
listFollows(params?: {
|
|
157
|
+
kind?: string;
|
|
158
|
+
cursor?: string;
|
|
159
|
+
limit?: number;
|
|
160
|
+
}): Promise<FollowListPage>;
|
|
161
|
+
httpService: import("../HttpService").HttpService;
|
|
162
|
+
cloudURL: string;
|
|
163
|
+
config: import("../OxyServices.base").OxyConfig;
|
|
164
|
+
__resetTokensForTests(): void;
|
|
165
|
+
makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
|
|
166
|
+
getBaseURL(): string;
|
|
167
|
+
getClient(): import("../HttpService").HttpService;
|
|
168
|
+
createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
|
|
169
|
+
getMetrics(): {
|
|
170
|
+
totalRequests: number;
|
|
171
|
+
successfulRequests: number;
|
|
172
|
+
failedRequests: number;
|
|
173
|
+
cacheHits: number;
|
|
174
|
+
cacheMisses: number;
|
|
175
|
+
averageResponseTime: number;
|
|
176
|
+
};
|
|
177
|
+
clearCache(): void;
|
|
178
|
+
clearCacheEntry(key: string): void;
|
|
179
|
+
clearCacheByPrefix(prefix: string): number;
|
|
180
|
+
getCacheStats(): {
|
|
181
|
+
size: number;
|
|
182
|
+
hits: number;
|
|
183
|
+
misses: number;
|
|
184
|
+
hitRate: number;
|
|
185
|
+
};
|
|
186
|
+
getCloudURL(): string;
|
|
187
|
+
setTokens(accessToken: string): void;
|
|
188
|
+
clearTokens(): void;
|
|
189
|
+
onTokensChanged(listener: (accessToken: string | null) => void): () => void;
|
|
190
|
+
_cachedUserId: string | null | undefined;
|
|
191
|
+
_cachedAccessToken: string | null;
|
|
192
|
+
getCurrentUserId(): string | null;
|
|
193
|
+
hasValidToken(): boolean;
|
|
194
|
+
getAccessToken(): string | null;
|
|
195
|
+
getAccessTokenExpiry(): number | null;
|
|
196
|
+
waitForAuth(timeoutMs?: number): Promise<boolean>;
|
|
197
|
+
withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
|
|
198
|
+
maxRetries?: number;
|
|
199
|
+
retryDelay?: number;
|
|
200
|
+
authTimeoutMs?: number;
|
|
201
|
+
}): Promise<T_1>;
|
|
202
|
+
validate(): Promise<boolean>;
|
|
203
|
+
handleError(error: unknown): Error;
|
|
204
|
+
healthCheck(): Promise<{
|
|
205
|
+
status: string;
|
|
206
|
+
users?: number;
|
|
207
|
+
timestamp?: string;
|
|
208
|
+
[key: string]: any;
|
|
209
|
+
}>;
|
|
210
|
+
};
|
|
211
|
+
} & T;
|
|
@@ -254,13 +254,16 @@ 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`). The account forest (`GET /accounts` and the
|
|
263
|
+
* caller's own detail row) is swept too — a personal account IS this user,
|
|
264
|
+
* and `AccountNode.account` embeds the whole profile — from the list that
|
|
265
|
+
* {@link evictOxyAccountForestCache} owns, for the same reason: the accounts
|
|
266
|
+
* mixin writes those keys as well, and two hand-written copies drift.
|
|
264
267
|
*
|
|
265
268
|
* TanStack Query handles offline queuing automatically.
|
|
266
269
|
*/
|
|
@@ -29,6 +29,7 @@ import { OxyServicesAppDataMixin } from './OxyServices.appData';
|
|
|
29
29
|
import { OxyServicesCivicMixin } from './OxyServices.civic';
|
|
30
30
|
import { OxyServicesNodesMixin } from './OxyServices.nodes';
|
|
31
31
|
import { OxyServicesLinksMixin } from './OxyServices.links';
|
|
32
|
+
import { OxyServicesFollowGraphMixin } from './OxyServices.followGraph';
|
|
32
33
|
import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
|
|
33
34
|
import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
|
|
34
35
|
/**
|
|
@@ -40,7 +41,7 @@ import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
|
|
|
40
41
|
* If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
|
|
41
42
|
* are visible without a cast.
|
|
42
43
|
*/
|
|
43
|
-
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
44
|
+
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
44
45
|
/**
|
|
45
46
|
* Constructor type for the fully composed mixin pipeline. Each mixin returns
|
|
46
47
|
* a new constructor that augments its input; reducing across the pipeline
|
|
@@ -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;
|