@oxyhq/core 19.0.0 → 19.1.1
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 +1 -0
- package/dist/cjs/i18n/locales/es-ES.json +1 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +1 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +1 -0
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/mixins/OxyServices.accounts.js +42 -29
- package/dist/cjs/mixins/OxyServices.followGraph.js +204 -0
- package/dist/cjs/mixins/OxyServices.user.js +7 -4
- package/dist/cjs/mixins/index.js +4 -0
- package/dist/cjs/session/accountProjection.js +45 -9
- package/dist/cjs/utils/accountCacheSweep.js +80 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/i18n/locales/en-US.json +1 -0
- package/dist/esm/i18n/locales/es-ES.json +1 -0
- package/dist/esm/i18n/locales/locales/en-US.json +1 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +1 -0
- package/dist/esm/index.js +7 -1
- package/dist/esm/mixins/OxyServices.accounts.js +42 -29
- package/dist/esm/mixins/OxyServices.followGraph.js +201 -0
- package/dist/esm/mixins/OxyServices.user.js +7 -4
- package/dist/esm/mixins/index.js +4 -0
- package/dist/esm/session/accountProjection.js +44 -9
- package/dist/esm/utils/accountCacheSweep.js +75 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.accounts.d.ts +52 -27
- package/dist/types/mixins/OxyServices.followGraph.d.ts +210 -0
- package/dist/types/mixins/OxyServices.user.d.ts +5 -4
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/session/accountProjection.d.ts +38 -4
- package/dist/types/utils/accountCacheSweep.d.ts +75 -0
- package/package.json +2 -2
- package/src/i18n/locales/en-US.json +1 -0
- package/src/i18n/locales/es-ES.json +1 -0
- package/src/index.ts +8 -1
- package/src/mixins/OxyServices.accounts.ts +65 -38
- package/src/mixins/OxyServices.followGraph.ts +269 -0
- package/src/mixins/OxyServices.user.ts +7 -4
- 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 +37 -0
- package/src/mixins/index.ts +5 -0
- package/src/session/__tests__/accountProjection.test.ts +109 -2
- package/src/session/accountProjection.ts +47 -9
- package/src/utils/accountCacheSweep.ts +93 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -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';
|
|
@@ -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;
|
|
@@ -246,7 +258,10 @@ export interface InviteAccountMemberInput {
|
|
|
246
258
|
}
|
|
247
259
|
/** Input accepted by `updateAccountMember`. The owner role cannot be assigned. */
|
|
248
260
|
export interface UpdateAccountMemberInput {
|
|
249
|
-
role
|
|
261
|
+
role?: Exclude<AccountRole, 'owner'>;
|
|
262
|
+
inherit?: boolean;
|
|
263
|
+
permissionGrants?: string[];
|
|
264
|
+
permissionRevokes?: string[];
|
|
250
265
|
}
|
|
251
266
|
/** Input accepted by `transferAccountOwnership`. */
|
|
252
267
|
export interface TransferAccountOwnershipInput {
|
|
@@ -609,7 +624,27 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
609
624
|
*/
|
|
610
625
|
listChildAccounts(accountId: string): Promise<AccountNode[]>;
|
|
611
626
|
/**
|
|
612
|
-
* 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
|
+
*
|
|
613
648
|
* @param accountId - The account's Mongo `_id`.
|
|
614
649
|
*/
|
|
615
650
|
listAccountMembers(accountId: string): Promise<AccountMember[]>;
|
|
@@ -734,28 +769,18 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
734
769
|
*/
|
|
735
770
|
getAppUsage(applicationId: string, period?: ApplicationUsagePeriod): Promise<ApplicationUsageStats>;
|
|
736
771
|
/**
|
|
737
|
-
* Bust
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
* unscoped entry plus every `?`-query variant via a prefix sweep. The prefix
|
|
742
|
-
* `GET:/accounts?` matches only the query-string list variants, never the
|
|
743
|
-
* `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.
|
|
744
776
|
*
|
|
745
777
|
* Internal helper (leading underscore); not part of the supported public
|
|
746
778
|
* surface. Public rather than `private` because mixins compose into an
|
|
747
779
|
* exported anonymous class, where TypeScript cannot represent a private
|
|
748
780
|
* member in the emitted declaration file (TS4094).
|
|
749
|
-
*/
|
|
750
|
-
_invalidateAccountLists(): void;
|
|
751
|
-
/**
|
|
752
|
-
* Bust the cached member list and detail for an account after a membership
|
|
753
|
-
* mutation. The member list (`listAccountMembers`) and the detail
|
|
754
|
-
* (`getAccount`, which can embed the caller's membership) both go stale when
|
|
755
|
-
* the member set or a member's role changes.
|
|
756
781
|
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
782
|
+
* The forest keys themselves are NOT owned here — see
|
|
783
|
+
* `utils/accountCacheSweep`, which the user mixin has to reach as well.
|
|
759
784
|
*/
|
|
760
785
|
_invalidateAccountMembership(accountId: string): void;
|
|
761
786
|
/**
|
|
@@ -767,8 +792,8 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
767
792
|
* query-string list variants, never the `GET:/applications/<id>…`
|
|
768
793
|
* detail/sub-resource keys.
|
|
769
794
|
*
|
|
770
|
-
* Internal helper (leading underscore); see `
|
|
771
|
-
* this is public rather than `private`.
|
|
795
|
+
* Internal helper (leading underscore); see `_invalidateAccountMembership`
|
|
796
|
+
* for why this is public rather than `private`.
|
|
772
797
|
*/
|
|
773
798
|
_invalidateAppLists(): void;
|
|
774
799
|
httpService: import("../HttpService").HttpService;
|
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Drop the override so this application follows the global relationship
|
|
84
|
+
* again. Distinct from setting `enabled`: inheriting means a later global
|
|
85
|
+
* change takes effect here, and an explicit `enabled` means it does not.
|
|
86
|
+
*/
|
|
87
|
+
restoreFollowInheritance(relationshipId: string, applicationId?: string): Promise<{
|
|
88
|
+
ok: true;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a target by canonical URI, registering it the first time anyone
|
|
92
|
+
* asks. The call an application makes on the way into a screen, before it
|
|
93
|
+
* can render a button.
|
|
94
|
+
*
|
|
95
|
+
* Idempotent on the URI, which is what makes two applications describing
|
|
96
|
+
* the same thing — the same fediverse actor, the same topic — arrive at ONE
|
|
97
|
+
* row, and therefore at one relationship per user rather than one per app.
|
|
98
|
+
*
|
|
99
|
+
* `metadata` is a display snapshot (name, handle, icon) and is refreshed
|
|
100
|
+
* only for the application that provides the target: a second application
|
|
101
|
+
* passing its own idea of the name would make the display flip depending on
|
|
102
|
+
* which app last looked.
|
|
103
|
+
*/
|
|
104
|
+
ensureFollowTarget(input: {
|
|
105
|
+
uri: string;
|
|
106
|
+
kind: string;
|
|
107
|
+
metadata?: Record<string, unknown>;
|
|
108
|
+
providerReference?: string;
|
|
109
|
+
localUserId?: string;
|
|
110
|
+
}): Promise<{
|
|
111
|
+
id: string;
|
|
112
|
+
uri: string;
|
|
113
|
+
kind: string;
|
|
114
|
+
created: boolean;
|
|
115
|
+
}>;
|
|
116
|
+
/**
|
|
117
|
+
* Claim a namespace for the calling application. First come, and idempotent
|
|
118
|
+
* for the holder — an application that registers on every boot must not
|
|
119
|
+
* fail the second time.
|
|
120
|
+
*/
|
|
121
|
+
claimFollowNamespace(namespace: string): Promise<{
|
|
122
|
+
namespace: string;
|
|
123
|
+
created: boolean;
|
|
124
|
+
}>;
|
|
125
|
+
/**
|
|
126
|
+
* Declare what following a kind of thing MEANS: the verb clients render,
|
|
127
|
+
* whether reverse lookups are public, whether it federates.
|
|
128
|
+
*
|
|
129
|
+
* Declared once by the application that owns the concept, rather than
|
|
130
|
+
* passed per call site — otherwise two screens of one app can disagree
|
|
131
|
+
* about whether a store is followed or subscribed to.
|
|
132
|
+
*/
|
|
133
|
+
registerFollowKind(input: {
|
|
134
|
+
kind: string;
|
|
135
|
+
label?: string;
|
|
136
|
+
capabilities?: {
|
|
137
|
+
verb?: "follow" | "subscribe" | "join" | "watch";
|
|
138
|
+
reverse?: "public" | "private" | "aggregate" | "unavailable";
|
|
139
|
+
federated?: boolean;
|
|
140
|
+
};
|
|
141
|
+
}): Promise<{
|
|
142
|
+
kind: string;
|
|
143
|
+
created: boolean;
|
|
144
|
+
}>;
|
|
145
|
+
/**
|
|
146
|
+
* Everything the signed-in user follows, newest first.
|
|
147
|
+
*
|
|
148
|
+
* Owner-only by construction server-side — there is no parameter naming a
|
|
149
|
+
* user, so this cannot be pointed at somebody else's graph.
|
|
150
|
+
*
|
|
151
|
+
* Paginate by passing back `nextCursor`, never an offset: the list changes
|
|
152
|
+
* while it is being read, and an offset silently skips or repeats rows
|
|
153
|
+
* exactly when it does.
|
|
154
|
+
*/
|
|
155
|
+
listFollows(params?: {
|
|
156
|
+
kind?: string;
|
|
157
|
+
cursor?: string;
|
|
158
|
+
limit?: number;
|
|
159
|
+
}): Promise<FollowListPage>;
|
|
160
|
+
httpService: import("../HttpService").HttpService;
|
|
161
|
+
cloudURL: string;
|
|
162
|
+
config: import("../OxyServices.base").OxyConfig;
|
|
163
|
+
__resetTokensForTests(): void;
|
|
164
|
+
makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
|
|
165
|
+
getBaseURL(): string;
|
|
166
|
+
getClient(): import("../HttpService").HttpService;
|
|
167
|
+
createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
|
|
168
|
+
getMetrics(): {
|
|
169
|
+
totalRequests: number;
|
|
170
|
+
successfulRequests: number;
|
|
171
|
+
failedRequests: number;
|
|
172
|
+
cacheHits: number;
|
|
173
|
+
cacheMisses: number;
|
|
174
|
+
averageResponseTime: number;
|
|
175
|
+
};
|
|
176
|
+
clearCache(): void;
|
|
177
|
+
clearCacheEntry(key: string): void;
|
|
178
|
+
clearCacheByPrefix(prefix: string): number;
|
|
179
|
+
getCacheStats(): {
|
|
180
|
+
size: number;
|
|
181
|
+
hits: number;
|
|
182
|
+
misses: number;
|
|
183
|
+
hitRate: number;
|
|
184
|
+
};
|
|
185
|
+
getCloudURL(): string;
|
|
186
|
+
setTokens(accessToken: string): void;
|
|
187
|
+
clearTokens(): void;
|
|
188
|
+
onTokensChanged(listener: (accessToken: string | null) => void): () => void;
|
|
189
|
+
_cachedUserId: string | null | undefined;
|
|
190
|
+
_cachedAccessToken: string | null;
|
|
191
|
+
getCurrentUserId(): string | null;
|
|
192
|
+
hasValidToken(): boolean;
|
|
193
|
+
getAccessToken(): string | null;
|
|
194
|
+
getAccessTokenExpiry(): number | null;
|
|
195
|
+
waitForAuth(timeoutMs?: number): Promise<boolean>;
|
|
196
|
+
withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
|
|
197
|
+
maxRetries?: number;
|
|
198
|
+
retryDelay?: number;
|
|
199
|
+
authTimeoutMs?: number;
|
|
200
|
+
}): Promise<T_1>;
|
|
201
|
+
validate(): Promise<boolean>;
|
|
202
|
+
handleError(error: unknown): Error;
|
|
203
|
+
healthCheck(): Promise<{
|
|
204
|
+
status: string;
|
|
205
|
+
users?: number;
|
|
206
|
+
timestamp?: string;
|
|
207
|
+
[key: string]: any;
|
|
208
|
+
}>;
|
|
209
|
+
};
|
|
210
|
+
} & T;
|
|
@@ -259,10 +259,11 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
259
259
|
* a new identity read is added in one place instead of to each writer
|
|
260
260
|
* separately (this method's own hand-written copy had already drifted from
|
|
261
261
|
* the server-side one, missing `GET /auth/lookup/*` and
|
|
262
|
-
* `GET /profiles/resolve`).
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
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.
|
|
266
267
|
*
|
|
267
268
|
* TanStack Query handles offline queuing automatically.
|
|
268
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
|
|
@@ -91,6 +91,36 @@ export interface SwitchableAccount {
|
|
|
91
91
|
/** The underlying per-account user payload. */
|
|
92
92
|
user: SwitchableAccountUser;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether the caller can BECOME this account — the one question every account
|
|
96
|
+
* switcher asks, answered here so no surface has to re-derive it.
|
|
97
|
+
*
|
|
98
|
+
* Two independent grounds, either of which suffices:
|
|
99
|
+
*
|
|
100
|
+
* - **It is already the caller's own identity** (`relationship: 'self'`).
|
|
101
|
+
* `GET /accounts` resolves its caller through `resolveOperatorId`, so `self`
|
|
102
|
+
* is the HUMAN operator's personal account even while they are operating an
|
|
103
|
+
* org — never the operated account. Kind is irrelevant on this ground: the
|
|
104
|
+
* caller IS that account, so returning to it asks the server for nothing.
|
|
105
|
+
* - **The server will mint a session for it** — `isActAsEligibleKind(kind)` is
|
|
106
|
+
* the exact predicate `POST /accounts/:id/switch` enforces, so a row offered
|
|
107
|
+
* on this ground is never a dead button.
|
|
108
|
+
*
|
|
109
|
+
* `isActAsEligibleKind` ALONE is not this question, and reaching for it
|
|
110
|
+
* directly is the mistake this function exists to prevent: it is false for
|
|
111
|
+
* `personal` as well as `channel`, so a switcher gated on it alone renders an
|
|
112
|
+
* empty list rather than a filtered one. Equally, `kind !== 'channel'` is not
|
|
113
|
+
* this question either — it silently admits every kind invented after it was
|
|
114
|
+
* written, which is the same trap `isActAsEligibleKind` was introduced to close
|
|
115
|
+
* on the server.
|
|
116
|
+
*
|
|
117
|
+
* Takes a structural subset rather than a whole {@link AccountNode} so a caller
|
|
118
|
+
* holding a projected {@link SwitchableAccount} can ask it too.
|
|
119
|
+
*/
|
|
120
|
+
export declare function isSwitchTargetAccount(node: {
|
|
121
|
+
kind?: AccountKind | null;
|
|
122
|
+
relationship?: AccountRelationship;
|
|
123
|
+
}): boolean;
|
|
94
124
|
/** Input to {@link projectSwitchableAccounts}. */
|
|
95
125
|
export interface ProjectSwitchableAccountsInput {
|
|
96
126
|
/**
|
|
@@ -131,8 +161,9 @@ export interface ProjectSwitchableAccountsInput {
|
|
|
131
161
|
* and a graph node is deduped into ONE device row enriched with the graph
|
|
132
162
|
* metadata (relationship / kind / parent / membership).
|
|
133
163
|
*
|
|
134
|
-
* Graph nodes
|
|
135
|
-
*
|
|
164
|
+
* Graph nodes that are not switch targets — a `channel`, which nobody may act
|
|
165
|
+
* as — are omitted. {@link isSwitchTargetAccount} is the rule; see the filter
|
|
166
|
+
* below.
|
|
136
167
|
*/
|
|
137
168
|
export declare function projectSwitchableAccounts(input: ProjectSwitchableAccountsInput): SwitchableAccount[];
|
|
138
169
|
/**
|
|
@@ -142,7 +173,10 @@ export declare function projectSwitchableAccounts(input: ProjectSwitchableAccoun
|
|
|
142
173
|
* document, but including their ids lets the caller pass one id set and lets the
|
|
143
174
|
* projection prefer freshly-fetched profiles uniformly.
|
|
144
175
|
*
|
|
145
|
-
* Applies the SAME
|
|
146
|
-
* nodes, so this never fetches a
|
|
176
|
+
* Applies the SAME {@link isSwitchTargetAccount} filter as
|
|
177
|
+
* {@link projectSwitchableAccounts} to graph nodes, so this never fetches a
|
|
178
|
+
* profile for a row the projection will drop — and, just as importantly, never
|
|
179
|
+
* SKIPS one the projection will keep, which would leave that row unrendered
|
|
180
|
+
* until some later fetch happened to resolve it.
|
|
147
181
|
*/
|
|
148
182
|
export declare function switchableAccountIds(state: DeviceSessionState | null, graph: AccountNode[]): string[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE enumeration of `OxyServices` GET-cache keys that serve the ACCOUNT FOREST
|
|
3
|
+
* — the caller's accessible accounts, as lists and as individual detail rows —
|
|
4
|
+
* and the one sweep that clears them.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS NOT A METHOD ON THE ACCOUNTS MIXIN
|
|
7
|
+
* ---------------------------------------------
|
|
8
|
+
* `AccountNode.account` is a whole `User`, so a forest read embeds the very
|
|
9
|
+
* profile the identity reads serve. That makes an IDENTITY write a writer of
|
|
10
|
+
* these keys too: `updateProfile` edits the caller's own personal account,
|
|
11
|
+
* which is a row in `GET /accounts` and is its own `GET /accounts/<id>`. Leave
|
|
12
|
+
* those cached and the account switcher keeps drawing the pre-edit name and
|
|
13
|
+
* picture for the full TTL, against a perfectly healthy server.
|
|
14
|
+
*
|
|
15
|
+
* The mixins compose into one class at runtime but are typed one at a time, so
|
|
16
|
+
* the user mixin cannot call a method the accounts mixin owns. The key list
|
|
17
|
+
* therefore lives here, once, and every writer calls {@link
|
|
18
|
+
* evictOxyAccountForestCache} — exactly like the identity key list in
|
|
19
|
+
* `identityCacheSweep`, which the accounts mixin already calls for the
|
|
20
|
+
* mirror-image case (an account write staling the identity reads). The
|
|
21
|
+
* alternative — a second hand-written copy of these keys in the other mixin —
|
|
22
|
+
* is the drift that shipped the two stale-profile bugs `identityCacheSweep`
|
|
23
|
+
* documents.
|
|
24
|
+
*
|
|
25
|
+
* WHY THE LIST NEEDS A PREFIX AND THE DETAIL DOES NOT
|
|
26
|
+
* --------------------------------------------------
|
|
27
|
+
* `listAccounts({tree?})` keys the flat list as `GET:/accounts` and every
|
|
28
|
+
* option variant as `GET:/accounts?<query>` (the query string is part of the
|
|
29
|
+
* URL, hence of the key), and a writer cannot enumerate which variants a caller
|
|
30
|
+
* has read. The detail key, by contrast, is derivable from the account id the
|
|
31
|
+
* writer already holds.
|
|
32
|
+
*
|
|
33
|
+
* The `GET:/accounts?` prefix matches ONLY the query-string list variants —
|
|
34
|
+
* never `GET:/accounts/<id>` or its `…/members`, `…/credentials`, `…/children`
|
|
35
|
+
* sub-resources, which are the accounts mixin's own business and stay there.
|
|
36
|
+
*/
|
|
37
|
+
import type { OxyIdentityCacheEvictor } from './identityCacheSweep';
|
|
38
|
+
/**
|
|
39
|
+
* The cache-eviction surface of an `OxyServices` instance. Reused from
|
|
40
|
+
* `identityCacheSweep` rather than re-declared: it is the SDK's one published
|
|
41
|
+
* name for these two methods, and a second identical interface would be one
|
|
42
|
+
* more shape to keep in step.
|
|
43
|
+
*/
|
|
44
|
+
export type OxyAccountCacheEvictor = OxyIdentityCacheEvictor;
|
|
45
|
+
/** The cache key `listAccounts()` reads under with no options. */
|
|
46
|
+
export declare const OXY_ACCOUNT_LIST_CACHE_KEY = "GET:/accounts";
|
|
47
|
+
/**
|
|
48
|
+
* The prefix covering every option-carrying `listAccounts(opts)` variant
|
|
49
|
+
* (`?tree=true`, …), none of which a writer can enumerate.
|
|
50
|
+
*/
|
|
51
|
+
export declare const OXY_ACCOUNT_LIST_CACHE_QUERY_PREFIX = "GET:/accounts?";
|
|
52
|
+
/**
|
|
53
|
+
* Prefix covering every per-account sub-resource cache key
|
|
54
|
+
* (`GET:/accounts/<id>`, `…/members`, `…/credentials`, `…/children`). A
|
|
55
|
+
* membership mutation on an ancestor must sweep ALL of these, not only the
|
|
56
|
+
* account named in the path: descendant member rosters embed inherited rows
|
|
57
|
+
* resolved from that ancestor, and the writer cannot enumerate which descendant
|
|
58
|
+
* ids a caller has already read. The trailing slash deliberately excludes the
|
|
59
|
+
* forest list keys (`GET:/accounts`, `GET:/accounts?…`) documented above.
|
|
60
|
+
*/
|
|
61
|
+
export declare const OXY_ACCOUNT_PER_ACCOUNT_CACHE_PREFIX = "GET:/accounts/";
|
|
62
|
+
/**
|
|
63
|
+
* Build the exact cache key `getAccount(accountId)` reads under.
|
|
64
|
+
*/
|
|
65
|
+
export declare function oxyAccountDetailCacheKey(accountId: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Sweep an `OxyServices` GET response cache of the account forest.
|
|
68
|
+
*
|
|
69
|
+
* @param oxy - Anything exposing the SDK's two eviction methods.
|
|
70
|
+
* @param accountId - The account whose detail row to drop as well. Optional: a
|
|
71
|
+
* writer that changed the SHAPE of the forest rather than one
|
|
72
|
+
* account in it (create, archive, ownership transfer) has no
|
|
73
|
+
* detail row to name, and clears only the lists.
|
|
74
|
+
*/
|
|
75
|
+
export declare function evictOxyAccountForestCache(oxy: OxyAccountCacheEvictor, accountId?: string): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "19.
|
|
3
|
+
"version": "19.1.1",
|
|
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.24.0",
|
|
119
119
|
"@oxyhq/protocol": "^0.1.6",
|
|
120
120
|
"@scure/bip39": "^1.6.0",
|
|
121
121
|
"@types/elliptic": "^6.4.18",
|
package/src/index.ts
CHANGED
|
@@ -647,7 +647,14 @@ export {
|
|
|
647
647
|
// chooser: device sign-ins ∪ account graph, deduped by accountId). Pure +
|
|
648
648
|
// I/O-free — the caller hydrates profiles via `getUsersByIds`. Shared by
|
|
649
649
|
// `@oxyhq/services` and auth.oxy.so so the list can't diverge.
|
|
650
|
-
|
|
650
|
+
// `isSwitchTargetAccount` is the switcher's own question ("can I become this
|
|
651
|
+
// account?"), exported so a surface that renders `AccountNode`s rather than the
|
|
652
|
+
// projection — the Console's workspace switcher, the accounts app's
|
|
653
|
+
// managed-accounts rows — asks the SAME question instead of testing a kind
|
|
654
|
+
// literal. It is NOT `isActAsEligibleKind`: that one is false for `personal`
|
|
655
|
+
// too, so gating a switcher on it alone empties the list.
|
|
656
|
+
export {
|
|
657
|
+
isSwitchTargetAccount,
|
|
651
658
|
projectSwitchableAccounts,
|
|
652
659
|
switchableAccountIds,
|
|
653
660
|
} from './session/accountProjection';
|