@oxyhq/core 17.0.2 → 17.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 +6 -0
- package/dist/cjs/i18n/locales/es-ES.json +6 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +6 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +6 -0
- package/dist/cjs/mixins/OxyServices.accounts.js +39 -1
- package/dist/cjs/session/accountDialogController.js +3 -1
- package/dist/cjs/session/accountProjection.js +19 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/i18n/locales/en-US.json +6 -0
- package/dist/esm/i18n/locales/es-ES.json +6 -0
- package/dist/esm/i18n/locales/locales/en-US.json +6 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +6 -0
- package/dist/esm/mixins/OxyServices.accounts.js +37 -1
- package/dist/esm/session/accountDialogController.js +3 -1
- package/dist/esm/session/accountProjection.js +19 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/keyManager.d.ts +2 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.accounts.d.ts +73 -9
- package/dist/types/models/interfaces.d.ts +17 -1
- package/dist/types/session/accountDialogController.d.ts +1 -1
- package/dist/types/session/accountProjection.d.ts +6 -0
- package/package.json +3 -3
- package/src/crypto/keyManager.ts +1 -2
- package/src/i18n/locales/en-US.json +6 -0
- package/src/i18n/locales/es-ES.json +6 -0
- package/src/index.ts +4 -0
- package/src/mixins/OxyServices.accounts.ts +115 -12
- package/src/models/interfaces.ts +17 -0
- package/src/session/__tests__/accountDialogController.test.ts +13 -1
- package/src/session/__tests__/accountProjection.test.ts +31 -0
- package/src/session/accountDialogController.ts +4 -2
- package/src/session/accountProjection.ts +19 -1
- package/src/types/color.d.ts +0 -20
- package/src/types/elliptic.d.ts +0 -70
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles secure generation, storage, and retrieval of cryptographic keys.
|
|
5
5
|
* Private keys are stored securely using expo-secure-store and never leave the device.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import { ec as EC } from 'elliptic';
|
|
8
8
|
import { type IdentityMarker } from './identityMarker';
|
|
9
9
|
/**
|
|
10
10
|
* Thrown when an identity-mutating operation (createIdentity / importKeyPair)
|
|
@@ -536,7 +536,7 @@ export declare class KeyManager {
|
|
|
536
536
|
* Get the elliptic curve key object from the stored private key
|
|
537
537
|
* Used internally for signing operations
|
|
538
538
|
*/
|
|
539
|
-
static getKeyPairObject(): Promise<
|
|
539
|
+
static getKeyPairObject(): Promise<EC.KeyPair | null>;
|
|
540
540
|
/**
|
|
541
541
|
* Derive public key from a private key (without storing)
|
|
542
542
|
*/
|
package/dist/types/index.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export type { CanonicalUserHandleInput, UserHandleInput } from './utils/userHand
|
|
|
39
39
|
export { normalizeProfileLinks } from './utils/profileLinks';
|
|
40
40
|
export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
|
|
41
41
|
export type { PublicApplication, ConnectedApp, } from './mixins/OxyServices.connectedApps';
|
|
42
|
-
export type { AccountKind, OrganizationCategory, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, 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';
|
|
42
|
+
export type { AccountKind, OrganizationCategory, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, UserCreatableAccountKind, CreateAccountInput, UpdateAccountInput, ProvisionChannelInput, ProvisionChannelMemberInput, ProvisionChannelResult, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, CreateAccountCredentialInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
|
|
43
43
|
export { ORGANIZATION_CATEGORIES } 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';
|
|
@@ -33,19 +33,20 @@
|
|
|
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 { OrganizationCategory } from '@oxyhq/contracts';
|
|
36
|
+
import type { AccountKind, OrganizationCategory } from '@oxyhq/contracts';
|
|
37
37
|
import type { SessionLoginResponse } from '../models/session';
|
|
38
38
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
39
39
|
/**
|
|
40
40
|
* Account classification, orthogonal to the federation `type`
|
|
41
41
|
* (`local|federated|agent|automated`). `personal` accounts have a direct login;
|
|
42
|
-
* `organization` / `project` / `bot` accounts are operated via
|
|
43
|
-
* and have no direct login.
|
|
42
|
+
* `organization` / `project` / `bot` / `channel` accounts are operated via
|
|
43
|
+
* `AccountMember` and have no direct login. Of those, only the first three may
|
|
44
|
+
* be acted AS — see `isActAsEligibleKind`.
|
|
45
|
+
*
|
|
46
|
+
* Single source of truth is `@oxyhq/contracts`.
|
|
44
47
|
*/
|
|
45
|
-
export type AccountKind
|
|
46
|
-
|
|
47
|
-
export type { OrganizationCategory } from '@oxyhq/contracts';
|
|
48
|
-
export { ORGANIZATION_CATEGORIES } from '@oxyhq/contracts';
|
|
48
|
+
export type { AccountKind, OrganizationCategory } from '@oxyhq/contracts';
|
|
49
|
+
export { ACCOUNT_KINDS, ORGANIZATION_CATEGORIES, isActAsEligibleKind } from '@oxyhq/contracts';
|
|
49
50
|
/**
|
|
50
51
|
* The calling user's relationship to an account node, as resolved by the API:
|
|
51
52
|
* - `self` — the caller's own personal (root) account.
|
|
@@ -130,10 +131,12 @@ export interface ListAccountsOptions {
|
|
|
130
131
|
*/
|
|
131
132
|
tree?: boolean;
|
|
132
133
|
}
|
|
134
|
+
/** Kinds a signed-in user may create via `POST /accounts`. Channels are service-provisioned only. */
|
|
135
|
+
export type UserCreatableAccountKind = 'organization' | 'project' | 'bot';
|
|
133
136
|
/** Input accepted by `createAccount`. */
|
|
134
137
|
export interface CreateAccountInput {
|
|
135
|
-
/** Classification of the new account. `personal`
|
|
136
|
-
kind:
|
|
138
|
+
/** Classification of the new account. `personal` and `channel` are not creatable here. */
|
|
139
|
+
kind: UserCreatableAccountKind;
|
|
137
140
|
/**
|
|
138
141
|
* Parent account `_id` to nest the new account under. Omitted → the API roots
|
|
139
142
|
* it under the caller's personal account.
|
|
@@ -141,9 +144,16 @@ export interface CreateAccountInput {
|
|
|
141
144
|
parentAccountId?: string;
|
|
142
145
|
/** Unique handle for the account (shares the `User.username` unique index). */
|
|
143
146
|
username: string;
|
|
147
|
+
/**
|
|
148
|
+
* A managed account (organization / project / bot / channel) has a TITLE, not
|
|
149
|
+
* a given-and-family name, so it sets `displayName` — the explicit
|
|
150
|
+
* `name_display` column — and leaves `first`/`last` unset. Splitting a title
|
|
151
|
+
* on whitespace into `first`/`last` is what this replaced.
|
|
152
|
+
*/
|
|
144
153
|
name?: {
|
|
145
154
|
first?: string;
|
|
146
155
|
last?: string;
|
|
156
|
+
displayName?: string;
|
|
147
157
|
};
|
|
148
158
|
bio?: string;
|
|
149
159
|
avatar?: string;
|
|
@@ -153,15 +163,47 @@ export interface CreateAccountInput {
|
|
|
153
163
|
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
154
164
|
export interface UpdateAccountInput {
|
|
155
165
|
username?: string;
|
|
166
|
+
/**
|
|
167
|
+
* Same shape as `CreateAccountInput['name']`. On update, an EMPTY STRING in
|
|
168
|
+
* `displayName` clears the explicit name and falls back to the composed
|
|
169
|
+
* `first`/`last`; omitting the key leaves the stored value untouched. The two
|
|
170
|
+
* are not interchangeable.
|
|
171
|
+
*/
|
|
156
172
|
name?: {
|
|
157
173
|
first?: string;
|
|
158
174
|
last?: string;
|
|
175
|
+
displayName?: string;
|
|
159
176
|
};
|
|
160
177
|
bio?: string | null;
|
|
161
178
|
avatar?: string | null;
|
|
162
179
|
/** Clears the category when `null`; only valid on `kind: 'organization'`. */
|
|
163
180
|
organizationCategory?: OrganizationCategory | null;
|
|
164
181
|
}
|
|
182
|
+
/** Input accepted by `provisionChannelAccount` (service token + `accounts:provision`). */
|
|
183
|
+
export interface ProvisionChannelInput {
|
|
184
|
+
/** Personal account `_id` whose tree owns the new channel. */
|
|
185
|
+
ownerUserId: string;
|
|
186
|
+
username: string;
|
|
187
|
+
name?: {
|
|
188
|
+
first?: string;
|
|
189
|
+
last?: string;
|
|
190
|
+
displayName?: string;
|
|
191
|
+
};
|
|
192
|
+
bio?: string;
|
|
193
|
+
description?: string;
|
|
194
|
+
avatar?: string;
|
|
195
|
+
}
|
|
196
|
+
/** Input accepted by `provisionChannelMember` (service token + `accounts:provision`). */
|
|
197
|
+
export interface ProvisionChannelMemberInput {
|
|
198
|
+
memberUserId: string;
|
|
199
|
+
role: Exclude<AccountRole, 'owner'>;
|
|
200
|
+
inherit?: boolean;
|
|
201
|
+
}
|
|
202
|
+
/** Result of `provisionChannelAccount`. */
|
|
203
|
+
export interface ProvisionChannelResult {
|
|
204
|
+
account: User;
|
|
205
|
+
membership: AccountMember;
|
|
206
|
+
}
|
|
165
207
|
/** Input accepted by `inviteAccountMember`. The owner role cannot be invited. */
|
|
166
208
|
export interface InviteAccountMemberInput {
|
|
167
209
|
/**
|
|
@@ -429,6 +471,11 @@ export interface SwitchAccountResult extends SessionLoginResponse {
|
|
|
429
471
|
}
|
|
430
472
|
export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
431
473
|
new (...args: any[]): {
|
|
474
|
+
/**
|
|
475
|
+
* Inherited from the auth mixin at runtime. Declared here so service-scoped
|
|
476
|
+
* account provisioning methods can call it with correct typing.
|
|
477
|
+
*/
|
|
478
|
+
makeServiceRequest: <R = unknown>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: unknown, userId?: string) => Promise<R>;
|
|
432
479
|
/**
|
|
433
480
|
* List the accounts the caller can access: their own personal (root)
|
|
434
481
|
* account, accounts they own, and accounts shared with them (including
|
|
@@ -482,6 +529,23 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
482
529
|
* @param data - Account configuration: kind, optional parent, and profile.
|
|
483
530
|
*/
|
|
484
531
|
createAccount(data: CreateAccountInput): Promise<AccountNode>;
|
|
532
|
+
/**
|
|
533
|
+
* Mint a `channel` account under `ownerUserId` via service auth
|
|
534
|
+
* (`accounts:provision` scope). Requires `configureServiceAuth` first.
|
|
535
|
+
*/
|
|
536
|
+
provisionChannelAccount(data: ProvisionChannelInput): Promise<ProvisionChannelResult>;
|
|
537
|
+
/**
|
|
538
|
+
* Grant membership on a channel account via service auth
|
|
539
|
+
* (`accounts:provision` scope).
|
|
540
|
+
*/
|
|
541
|
+
provisionChannelMember(channelAccountId: string, data: ProvisionChannelMemberInput): Promise<{
|
|
542
|
+
member: AccountMember;
|
|
543
|
+
}>;
|
|
544
|
+
/**
|
|
545
|
+
* Revoke membership on a channel account via service auth
|
|
546
|
+
* (`accounts:provision` scope).
|
|
547
|
+
*/
|
|
548
|
+
revokeChannelMember(channelAccountId: string, memberUserId: string): Promise<AccountSuccessResult>;
|
|
485
549
|
/**
|
|
486
550
|
* Update an account's mutable profile fields. Tree placement changes
|
|
487
551
|
* (reparenting) go through the dedicated move endpoint, not here.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OrganizationCategory, UserNameResponse, UserRelationship, ThemePreference } from '@oxyhq/contracts';
|
|
1
|
+
import type { AccountKind, OrganizationCategory, UserNameResponse, UserRelationship, ThemePreference } from '@oxyhq/contracts';
|
|
2
2
|
export interface OxyConfig {
|
|
3
3
|
baseURL: string;
|
|
4
4
|
cloudURL?: string;
|
|
@@ -118,6 +118,22 @@ export interface User {
|
|
|
118
118
|
following?: number;
|
|
119
119
|
};
|
|
120
120
|
accountExpiresAfterInactivityDays?: number | null;
|
|
121
|
+
/**
|
|
122
|
+
* Account-graph classification — WHAT this account is.
|
|
123
|
+
*
|
|
124
|
+
* ORTHOGONAL to `type` below, and easy to confuse with it: `type` says where
|
|
125
|
+
* the account lives and how it is driven (`local` / `federated` / `agent` /
|
|
126
|
+
* `automated`), `kind` says what it IS (`personal` / `organization` /
|
|
127
|
+
* `project` / `bot` / `channel`). A federated channel is `type: 'federated'`
|
|
128
|
+
* AND `kind: 'channel'`; neither value substitutes for the other.
|
|
129
|
+
*
|
|
130
|
+
* This is what a consumer rendering authored content reads to tell a
|
|
131
|
+
* channel's post from a person's — a channel is the author, so there is no
|
|
132
|
+
* second identity to carry alongside the user.
|
|
133
|
+
*
|
|
134
|
+
* Absent should be read as `personal` (the column's default), not as unknown.
|
|
135
|
+
*/
|
|
136
|
+
kind?: AccountKind;
|
|
121
137
|
type?: 'local' | 'federated' | 'agent' | 'automated';
|
|
122
138
|
isFederated?: boolean;
|
|
123
139
|
/** Allow sharing this user's content on the fediverse. Defaults to true. */
|
|
@@ -434,7 +434,7 @@ export declare class AccountDialogController {
|
|
|
434
434
|
* subscription, which re-projects the active row. Concurrent switches are
|
|
435
435
|
* ignored while one is in flight.
|
|
436
436
|
*/
|
|
437
|
-
switchTo(accountId: string): Promise<
|
|
437
|
+
switchTo(accountId: string): Promise<boolean>;
|
|
438
438
|
/**
|
|
439
439
|
* Start "Sign in with Oxy". Native devices with a shared identity mint a
|
|
440
440
|
* session silently (`signInWithSharedIdentity`); everything else (web, or a
|
|
@@ -130,6 +130,9 @@ export interface ProjectSwitchableAccountsInput {
|
|
|
130
130
|
* graph-only rows (in graph order). An account present as BOTH a device session
|
|
131
131
|
* and a graph node is deduped into ONE device row enriched with the graph
|
|
132
132
|
* metadata (relationship / kind / parent / membership).
|
|
133
|
+
*
|
|
134
|
+
* Graph nodes of a kind nobody may act as (`channel`) are omitted — see the
|
|
135
|
+
* filter below.
|
|
133
136
|
*/
|
|
134
137
|
export declare function projectSwitchableAccounts(input: ProjectSwitchableAccountsInput): SwitchableAccount[];
|
|
135
138
|
/**
|
|
@@ -138,5 +141,8 @@ export declare function projectSwitchableAccounts(input: ProjectSwitchableAccoun
|
|
|
138
141
|
* `oxyServices.getUsersByIds(...)`; graph nodes already embed their `account`
|
|
139
142
|
* document, but including their ids lets the caller pass one id set and lets the
|
|
140
143
|
* projection prefer freshly-fetched profiles uniformly.
|
|
144
|
+
*
|
|
145
|
+
* Applies the SAME act-as filter as {@link projectSwitchableAccounts} to graph
|
|
146
|
+
* nodes, so this never fetches a profile for a row the projection will drop.
|
|
141
147
|
*/
|
|
142
148
|
export declare function switchableAccountIds(state: DeviceSessionState | null, graph: AccountNode[]): string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "17.0
|
|
3
|
+
"version": "17.1.0",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -115,9 +115,10 @@
|
|
|
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.22.0",
|
|
119
119
|
"@oxyhq/protocol": "^0.1.6",
|
|
120
120
|
"@scure/bip39": "^1.6.0",
|
|
121
|
+
"@types/elliptic": "^6.4.18",
|
|
121
122
|
"buffer": "^6.0.3",
|
|
122
123
|
"elliptic": "^6.6.1",
|
|
123
124
|
"invariant": "^2.2.4",
|
|
@@ -154,7 +155,6 @@
|
|
|
154
155
|
"devDependencies": {
|
|
155
156
|
"@biomejs/biome": "^1.9.4",
|
|
156
157
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
157
|
-
"@types/elliptic": "^6.4.18",
|
|
158
158
|
"@types/express": "^4.17.21",
|
|
159
159
|
"@types/invariant": "^2.2.34",
|
|
160
160
|
"@types/node": "^20.19.43",
|
package/src/crypto/keyManager.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { ec as EC } from 'elliptic';
|
|
9
|
-
import type { ECKeyPair } from 'elliptic';
|
|
10
9
|
import { isWeb, isIOS, isAndroid } from '../utils/platform';
|
|
11
10
|
import { type ExpoCryptoLike, type ExpoSecureStoreLike, isReactNative, isNodeJS, loadAsyncStorage, loadExpoCrypto, loadNodeCrypto, loadSecureStore, loadSharedIdentityBridge } from '@oxyhq/protocol';
|
|
12
11
|
import { isDev, logger } from '../logger';
|
|
@@ -2490,7 +2489,7 @@ export class KeyManager {
|
|
|
2490
2489
|
* Get the elliptic curve key object from the stored private key
|
|
2491
2490
|
* Used internally for signing operations
|
|
2492
2491
|
*/
|
|
2493
|
-
static async getKeyPairObject(): Promise<
|
|
2492
|
+
static async getKeyPairObject(): Promise<EC.KeyPair | null> {
|
|
2494
2493
|
if (isWebPlatform()) {
|
|
2495
2494
|
return null; // Identity storage is only available on native platforms
|
|
2496
2495
|
}
|
|
@@ -54,6 +54,12 @@
|
|
|
54
54
|
"status": {
|
|
55
55
|
"accountSwitched": "Now using {{name}}",
|
|
56
56
|
"signingIn": "Signing in…"
|
|
57
|
+
},
|
|
58
|
+
"errors": {
|
|
59
|
+
"notConfigured": "Sign-in isn't available",
|
|
60
|
+
"notConfiguredDescription": "{{app}} isn't set up for sign-in yet. Contact the app's developer.",
|
|
61
|
+
"failed": "Couldn't start sign-in",
|
|
62
|
+
"failedDescription": "Something went wrong. Please try again."
|
|
57
63
|
}
|
|
58
64
|
},
|
|
59
65
|
"signup": {
|
|
@@ -54,6 +54,12 @@
|
|
|
54
54
|
"status": {
|
|
55
55
|
"accountSwitched": "Ahora usando {{name}}",
|
|
56
56
|
"signingIn": "Iniciando sesión…"
|
|
57
|
+
},
|
|
58
|
+
"errors": {
|
|
59
|
+
"notConfigured": "El inicio de sesión no está disponible",
|
|
60
|
+
"notConfiguredDescription": "{{app}} todavía no está configurada para iniciar sesión. Contacta con quien desarrolla la app.",
|
|
61
|
+
"failed": "No se pudo iniciar sesión",
|
|
62
|
+
"failedDescription": "Algo ha ido mal. Inténtalo de nuevo."
|
|
57
63
|
}
|
|
58
64
|
},
|
|
59
65
|
"signup": {
|
package/src/index.ts
CHANGED
|
@@ -136,8 +136,12 @@ export type {
|
|
|
136
136
|
AccountCredentialWithSecret,
|
|
137
137
|
RotateAccountCredentialResult,
|
|
138
138
|
ListAccountsOptions,
|
|
139
|
+
UserCreatableAccountKind,
|
|
139
140
|
CreateAccountInput,
|
|
140
141
|
UpdateAccountInput,
|
|
142
|
+
ProvisionChannelInput,
|
|
143
|
+
ProvisionChannelMemberInput,
|
|
144
|
+
ProvisionChannelResult,
|
|
141
145
|
InviteAccountMemberInput,
|
|
142
146
|
UpdateAccountMemberInput,
|
|
143
147
|
TransferAccountOwnershipInput,
|
|
@@ -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 { OrganizationCategory } from '@oxyhq/contracts';
|
|
36
|
+
import type { AccountKind, OrganizationCategory } 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';
|
|
@@ -46,14 +46,14 @@ import { CACHE_TIMES } from './mixinHelpers';
|
|
|
46
46
|
/**
|
|
47
47
|
* Account classification, orthogonal to the federation `type`
|
|
48
48
|
* (`local|federated|agent|automated`). `personal` accounts have a direct login;
|
|
49
|
-
* `organization` / `project` / `bot` accounts are operated via
|
|
50
|
-
* and have no direct login.
|
|
49
|
+
* `organization` / `project` / `bot` / `channel` accounts are operated via
|
|
50
|
+
* `AccountMember` and have no direct login. Of those, only the first three may
|
|
51
|
+
* be acted AS — see `isActAsEligibleKind`.
|
|
52
|
+
*
|
|
53
|
+
* Single source of truth is `@oxyhq/contracts`.
|
|
51
54
|
*/
|
|
52
|
-
export type AccountKind
|
|
53
|
-
|
|
54
|
-
/** Real-estate / team taxonomy for `kind: 'organization'` accounts. */
|
|
55
|
-
export type { OrganizationCategory } from '@oxyhq/contracts';
|
|
56
|
-
export { ORGANIZATION_CATEGORIES } from '@oxyhq/contracts';
|
|
55
|
+
export type { AccountKind, OrganizationCategory } from '@oxyhq/contracts';
|
|
56
|
+
export { ACCOUNT_KINDS, ORGANIZATION_CATEGORIES, isActAsEligibleKind } from '@oxyhq/contracts';
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* The calling user's relationship to an account node, as resolved by the API:
|
|
@@ -146,10 +146,13 @@ export interface ListAccountsOptions {
|
|
|
146
146
|
tree?: boolean;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** Kinds a signed-in user may create via `POST /accounts`. Channels are service-provisioned only. */
|
|
150
|
+
export type UserCreatableAccountKind = 'organization' | 'project' | 'bot';
|
|
151
|
+
|
|
149
152
|
/** Input accepted by `createAccount`. */
|
|
150
153
|
export interface CreateAccountInput {
|
|
151
|
-
/** Classification of the new account. `personal`
|
|
152
|
-
kind:
|
|
154
|
+
/** Classification of the new account. `personal` and `channel` are not creatable here. */
|
|
155
|
+
kind: UserCreatableAccountKind;
|
|
153
156
|
/**
|
|
154
157
|
* Parent account `_id` to nest the new account under. Omitted → the API roots
|
|
155
158
|
* it under the caller's personal account.
|
|
@@ -157,7 +160,13 @@ export interface CreateAccountInput {
|
|
|
157
160
|
parentAccountId?: string;
|
|
158
161
|
/** Unique handle for the account (shares the `User.username` unique index). */
|
|
159
162
|
username: string;
|
|
160
|
-
|
|
163
|
+
/**
|
|
164
|
+
* A managed account (organization / project / bot / channel) has a TITLE, not
|
|
165
|
+
* a given-and-family name, so it sets `displayName` — the explicit
|
|
166
|
+
* `name_display` column — and leaves `first`/`last` unset. Splitting a title
|
|
167
|
+
* on whitespace into `first`/`last` is what this replaced.
|
|
168
|
+
*/
|
|
169
|
+
name?: { first?: string; last?: string; displayName?: string };
|
|
161
170
|
bio?: string;
|
|
162
171
|
avatar?: string;
|
|
163
172
|
/** Meaningful only when `kind` is `organization`. */
|
|
@@ -167,13 +176,43 @@ export interface CreateAccountInput {
|
|
|
167
176
|
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
168
177
|
export interface UpdateAccountInput {
|
|
169
178
|
username?: string;
|
|
170
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Same shape as `CreateAccountInput['name']`. On update, an EMPTY STRING in
|
|
181
|
+
* `displayName` clears the explicit name and falls back to the composed
|
|
182
|
+
* `first`/`last`; omitting the key leaves the stored value untouched. The two
|
|
183
|
+
* are not interchangeable.
|
|
184
|
+
*/
|
|
185
|
+
name?: { first?: string; last?: string; displayName?: string };
|
|
171
186
|
bio?: string | null;
|
|
172
187
|
avatar?: string | null;
|
|
173
188
|
/** Clears the category when `null`; only valid on `kind: 'organization'`. */
|
|
174
189
|
organizationCategory?: OrganizationCategory | null;
|
|
175
190
|
}
|
|
176
191
|
|
|
192
|
+
/** Input accepted by `provisionChannelAccount` (service token + `accounts:provision`). */
|
|
193
|
+
export interface ProvisionChannelInput {
|
|
194
|
+
/** Personal account `_id` whose tree owns the new channel. */
|
|
195
|
+
ownerUserId: string;
|
|
196
|
+
username: string;
|
|
197
|
+
name?: { first?: string; last?: string; displayName?: string };
|
|
198
|
+
bio?: string;
|
|
199
|
+
description?: string;
|
|
200
|
+
avatar?: string;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Input accepted by `provisionChannelMember` (service token + `accounts:provision`). */
|
|
204
|
+
export interface ProvisionChannelMemberInput {
|
|
205
|
+
memberUserId: string;
|
|
206
|
+
role: Exclude<AccountRole, 'owner'>;
|
|
207
|
+
inherit?: boolean;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Result of `provisionChannelAccount`. */
|
|
211
|
+
export interface ProvisionChannelResult {
|
|
212
|
+
account: User;
|
|
213
|
+
membership: AccountMember;
|
|
214
|
+
}
|
|
215
|
+
|
|
177
216
|
/** Input accepted by `inviteAccountMember`. The owner role cannot be invited. */
|
|
178
217
|
export interface InviteAccountMemberInput {
|
|
179
218
|
/**
|
|
@@ -482,6 +521,17 @@ export function OxyServicesAccountsMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
482
521
|
super(...(args as [any]));
|
|
483
522
|
}
|
|
484
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Inherited from the auth mixin at runtime. Declared here so service-scoped
|
|
526
|
+
* account provisioning methods can call it with correct typing.
|
|
527
|
+
*/
|
|
528
|
+
declare makeServiceRequest: <R = unknown>(
|
|
529
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
530
|
+
url: string,
|
|
531
|
+
data?: unknown,
|
|
532
|
+
userId?: string,
|
|
533
|
+
) => Promise<R>;
|
|
534
|
+
|
|
485
535
|
// =========================================================================
|
|
486
536
|
// Accounts
|
|
487
537
|
// =========================================================================
|
|
@@ -620,6 +670,59 @@ export function OxyServicesAccountsMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
620
670
|
}
|
|
621
671
|
}
|
|
622
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Mint a `channel` account under `ownerUserId` via service auth
|
|
675
|
+
* (`accounts:provision` scope). Requires `configureServiceAuth` first.
|
|
676
|
+
*/
|
|
677
|
+
async provisionChannelAccount(data: ProvisionChannelInput): Promise<ProvisionChannelResult> {
|
|
678
|
+
try {
|
|
679
|
+
return await this.makeServiceRequest<ProvisionChannelResult>(
|
|
680
|
+
'POST',
|
|
681
|
+
'/accounts/service/channels',
|
|
682
|
+
data,
|
|
683
|
+
);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
throw this.handleError(error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Grant membership on a channel account via service auth
|
|
691
|
+
* (`accounts:provision` scope).
|
|
692
|
+
*/
|
|
693
|
+
async provisionChannelMember(
|
|
694
|
+
channelAccountId: string,
|
|
695
|
+
data: ProvisionChannelMemberInput,
|
|
696
|
+
): Promise<{ member: AccountMember }> {
|
|
697
|
+
try {
|
|
698
|
+
return await this.makeServiceRequest<{ member: AccountMember }>(
|
|
699
|
+
'POST',
|
|
700
|
+
`/accounts/service/channels/${encodeURIComponent(channelAccountId)}/members`,
|
|
701
|
+
data,
|
|
702
|
+
);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
throw this.handleError(error);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Revoke membership on a channel account via service auth
|
|
710
|
+
* (`accounts:provision` scope).
|
|
711
|
+
*/
|
|
712
|
+
async revokeChannelMember(
|
|
713
|
+
channelAccountId: string,
|
|
714
|
+
memberUserId: string,
|
|
715
|
+
): Promise<AccountSuccessResult> {
|
|
716
|
+
try {
|
|
717
|
+
return await this.makeServiceRequest<AccountSuccessResult>(
|
|
718
|
+
'DELETE',
|
|
719
|
+
`/accounts/service/channels/${encodeURIComponent(channelAccountId)}/members/${encodeURIComponent(memberUserId)}`,
|
|
720
|
+
);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
throw this.handleError(error);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
623
726
|
/**
|
|
624
727
|
* Update an account's mutable profile fields. Tree placement changes
|
|
625
728
|
* (reparenting) go through the dedicated move endpoint, not here.
|
package/src/models/interfaces.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AccountKind,
|
|
2
3
|
OrganizationCategory,
|
|
3
4
|
UserNameResponse,
|
|
4
5
|
UserRelationship,
|
|
@@ -134,6 +135,22 @@ export interface User {
|
|
|
134
135
|
following?: number;
|
|
135
136
|
};
|
|
136
137
|
accountExpiresAfterInactivityDays?: number | null; // Days of inactivity before account expires (null = never expire)
|
|
138
|
+
/**
|
|
139
|
+
* Account-graph classification — WHAT this account is.
|
|
140
|
+
*
|
|
141
|
+
* ORTHOGONAL to `type` below, and easy to confuse with it: `type` says where
|
|
142
|
+
* the account lives and how it is driven (`local` / `federated` / `agent` /
|
|
143
|
+
* `automated`), `kind` says what it IS (`personal` / `organization` /
|
|
144
|
+
* `project` / `bot` / `channel`). A federated channel is `type: 'federated'`
|
|
145
|
+
* AND `kind: 'channel'`; neither value substitutes for the other.
|
|
146
|
+
*
|
|
147
|
+
* This is what a consumer rendering authored content reads to tell a
|
|
148
|
+
* channel's post from a person's — a channel is the author, so there is no
|
|
149
|
+
* second identity to carry alongside the user.
|
|
150
|
+
*
|
|
151
|
+
* Absent should be read as `personal` (the column's default), not as unknown.
|
|
152
|
+
*/
|
|
153
|
+
kind?: AccountKind;
|
|
137
154
|
// User type and external account support
|
|
138
155
|
type?: 'local' | 'federated' | 'agent' | 'automated';
|
|
139
156
|
isFederated?: boolean;
|
|
@@ -472,7 +472,7 @@ describe('AccountDialogController — switchTo (uniform switch)', () => {
|
|
|
472
472
|
sc.set(state([{ accountId: 'a1', sessionId: 's1' }], 'a1'));
|
|
473
473
|
oxy.switchToAccount.mockRejectedValue(new Error('Cannot switch into a personal account'));
|
|
474
474
|
|
|
475
|
-
await expect(controller.switchTo('org1')).resolves.
|
|
475
|
+
await expect(controller.switchTo('org1')).resolves.toBe(false);
|
|
476
476
|
|
|
477
477
|
const snap = controller.getSnapshot();
|
|
478
478
|
expect(snap.error).toBe('Cannot switch into a personal account');
|
|
@@ -517,6 +517,18 @@ describe('AccountDialogController — switchTo (uniform switch)', () => {
|
|
|
517
517
|
release();
|
|
518
518
|
await first;
|
|
519
519
|
});
|
|
520
|
+
|
|
521
|
+
it('returns true when the switch succeeds even if the post-switch refresh fails', async () => {
|
|
522
|
+
const { controller, oxy, sc } = makeHarness();
|
|
523
|
+
sc.set(state([{ accountId: 'a1', sessionId: 's1' }, { accountId: 'a2', sessionId: 's2' }], 'a1'));
|
|
524
|
+
jest.spyOn(sc, 'switchAccount').mockResolvedValue(undefined);
|
|
525
|
+
oxy.listAccounts.mockRejectedValue(new Error('network down'));
|
|
526
|
+
|
|
527
|
+
await expect(controller.switchTo('a2')).resolves.toBe(true);
|
|
528
|
+
|
|
529
|
+
expect(controller.getSnapshot().error).toBe('network down');
|
|
530
|
+
expect(controller.getSnapshot().switchingAccountId).toBeNull();
|
|
531
|
+
});
|
|
520
532
|
});
|
|
521
533
|
|
|
522
534
|
describe('AccountDialogController — sign in with Oxy', () => {
|
|
@@ -115,6 +115,29 @@ describe('projectSwitchableAccounts', () => {
|
|
|
115
115
|
expect(org.parentAccountId).toBe('a1');
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* A channel is a content identity nobody acts as, so it must never be offered
|
|
120
|
+
* as a switch target. It reaches the projection through the GRAPH lane, not
|
|
121
|
+
* the device lane — a channel has no credentials and so can never be a device
|
|
122
|
+
* session, which is exactly why "no-login accounts can't appear here by
|
|
123
|
+
* construction" is false: `listAccounts()` contributes credential-less
|
|
124
|
+
* accounts on purpose (that is how an org first becomes switchable).
|
|
125
|
+
*/
|
|
126
|
+
it('omits a graph-only channel account from the switcher', () => {
|
|
127
|
+
const rows = projectSwitchableAccounts({
|
|
128
|
+
state: state([{ accountId: 'a1', sessionId: 's1' }], 'a1'),
|
|
129
|
+
graph: [
|
|
130
|
+
graphNode('org1', { kind: 'organization' }),
|
|
131
|
+
graphNode('chan1', { kind: 'channel' }),
|
|
132
|
+
],
|
|
133
|
+
profilesById: mapOf(user('a1')),
|
|
134
|
+
resolveAvatarUrl: noAvatar,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(rows.map((r) => r.accountId)).toEqual(['a1', 'org1']);
|
|
138
|
+
expect(rows.some((r) => r.kind === 'channel')).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
118
141
|
it('dedups an account present as BOTH device session and graph node into ONE enriched row', () => {
|
|
119
142
|
const rows = projectSwitchableAccounts({
|
|
120
143
|
state: state([{ accountId: 'a1', sessionId: 's1', authuser: 0 }], 'a1'),
|
|
@@ -208,4 +231,12 @@ describe('switchableAccountIds', () => {
|
|
|
208
231
|
it('returns [] for null state and empty graph', () => {
|
|
209
232
|
expect(switchableAccountIds(null, [])).toEqual([]);
|
|
210
233
|
});
|
|
234
|
+
|
|
235
|
+
it('omits a graph-only channel, so no profile is fetched for a dropped row', () => {
|
|
236
|
+
const ids = switchableAccountIds(null, [
|
|
237
|
+
graphNode('org1', { kind: 'organization' }),
|
|
238
|
+
graphNode('chan1', { kind: 'channel' }),
|
|
239
|
+
]);
|
|
240
|
+
expect(ids).toEqual(['org1']);
|
|
241
|
+
});
|
|
211
242
|
});
|
|
@@ -783,8 +783,8 @@ export class AccountDialogController {
|
|
|
783
783
|
* subscription, which re-projects the active row. Concurrent switches are
|
|
784
784
|
* ignored while one is in flight.
|
|
785
785
|
*/
|
|
786
|
-
async switchTo(accountId: string): Promise<
|
|
787
|
-
if (this.switchingAccountId) return;
|
|
786
|
+
async switchTo(accountId: string): Promise<boolean> {
|
|
787
|
+
if (this.switchingAccountId) return false;
|
|
788
788
|
this.switchingAccountId = accountId;
|
|
789
789
|
this.error = null;
|
|
790
790
|
this.emit();
|
|
@@ -815,8 +815,10 @@ export class AccountDialogController {
|
|
|
815
815
|
}
|
|
816
816
|
// Re-project + refetch immediately; the subscription also fires.
|
|
817
817
|
await this.refresh();
|
|
818
|
+
return true;
|
|
818
819
|
} catch (error) {
|
|
819
820
|
this.error = errorMessage(error);
|
|
821
|
+
return false;
|
|
820
822
|
} finally {
|
|
821
823
|
this.switchingAccountId = null;
|
|
822
824
|
this.emit();
|