@oxyhq/core 3.18.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/OxyServices.js +3 -2
- package/dist/cjs/mixins/OxyServices.accounts.js +480 -0
- package/dist/cjs/mixins/OxyServices.connectedApps.js +73 -0
- package/dist/cjs/mixins/OxyServices.utility.js +3 -2
- package/dist/cjs/mixins/index.js +9 -6
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/OxyServices.js +3 -2
- package/dist/esm/mixins/OxyServices.accounts.js +477 -0
- package/dist/esm/mixins/OxyServices.connectedApps.js +70 -0
- package/dist/esm/mixins/OxyServices.utility.js +3 -2
- package/dist/esm/mixins/index.js +9 -6
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/OxyServices.d.ts +3 -2
- package/dist/types/index.d.ts +2 -3
- package/dist/types/mixins/OxyServices.accounts.d.ts +642 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -1
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +168 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +6 -3
- package/dist/types/mixins/index.d.ts +3 -4
- package/package.json +1 -1
- package/src/OxyServices.ts +3 -2
- package/src/index.ts +33 -34
- package/src/mixins/OxyServices.accounts.ts +1079 -0
- package/src/mixins/OxyServices.auth.ts +1 -1
- package/src/mixins/OxyServices.connectedApps.ts +165 -0
- package/src/mixins/OxyServices.utility.ts +7 -4
- package/src/mixins/__tests__/accounts.test.ts +667 -0
- package/src/mixins/__tests__/connectedApps.test.ts +1 -1
- package/src/mixins/index.ts +11 -9
- package/dist/cjs/mixins/OxyServices.applications.js +0 -350
- package/dist/cjs/mixins/OxyServices.managedAccounts.js +0 -143
- package/dist/cjs/mixins/OxyServices.workspaces.js +0 -181
- package/dist/esm/mixins/OxyServices.applications.js +0 -347
- package/dist/esm/mixins/OxyServices.managedAccounts.js +0 -140
- package/dist/esm/mixins/OxyServices.workspaces.js +0 -178
- package/dist/types/mixins/OxyServices.applications.d.ts +0 -496
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +0 -145
- package/dist/types/mixins/OxyServices.workspaces.d.ts +0 -219
- package/src/mixins/OxyServices.applications.ts +0 -773
- package/src/mixins/OxyServices.managedAccounts.ts +0 -173
- package/src/mixins/OxyServices.workspaces.ts +0 -351
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accounts Methods Mixin
|
|
3
|
+
*
|
|
4
|
+
* The single client surface for the unified Oxy **account graph** (`/accounts`)
|
|
5
|
+
* and the **applications** owned within it (`/applications`).
|
|
6
|
+
*
|
|
7
|
+
* An account is a relational, tree-structured principal (the `User` document
|
|
8
|
+
* generalised): a `personal` account is a human login at the root of its tree;
|
|
9
|
+
* `organization` / `project` / `bot` accounts are non-login principals operated
|
|
10
|
+
* through membership. Accounts form a tree (`parentAccountId`), own
|
|
11
|
+
* applications/bots, and expose a single membership model (`AccountMember`) with
|
|
12
|
+
* a unified role set and an explicit-but-inheritable cascade down the subtree.
|
|
13
|
+
*
|
|
14
|
+
* This mixin is the clean-cut replacement for the former `managedAccounts`,
|
|
15
|
+
* `workspaces`, and `applications` (account-management) mixins. Applications are
|
|
16
|
+
* now owned by an account (`Application.ownerAccountId`) and their access derives
|
|
17
|
+
* from the caller's `AccountMember` on that owning account — there is no separate
|
|
18
|
+
* application-membership surface. The OAuth-consent surface a user sees for
|
|
19
|
+
* THIRD-PARTY apps they authorized (`getPublicApplication`, `listConnectedApps`,
|
|
20
|
+
* `revokeAppGrant`) is unrelated to account ownership and lives in
|
|
21
|
+
* `OxyServices.connectedApps.ts`.
|
|
22
|
+
*
|
|
23
|
+
* Reference accounts by their Mongo `_id` (`accountId`, the underlying
|
|
24
|
+
* `User._id`), applications by their `_id` (`applicationId`), members by their
|
|
25
|
+
* member `_id`, and credentials by their `credentialId`. Never by name, slug, or
|
|
26
|
+
* handle.
|
|
27
|
+
*
|
|
28
|
+
* NOTE: acting-as (delegated identity) is NOT part of this mixin — `setActingAs`
|
|
29
|
+
* / `getActingAs` live on `OxyServices.base` and `verifyActingAs` on the utility
|
|
30
|
+
* mixin (it verifies against `GET /accounts/verify-acting-as`).
|
|
31
|
+
*/
|
|
32
|
+
import type { User } from '../models/interfaces';
|
|
33
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
34
|
+
import { CACHE_TIMES } from './mixinHelpers';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Account graph types
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Account classification, orthogonal to the federation `type`
|
|
42
|
+
* (`local|federated|agent|automated`). `personal` accounts have a direct login;
|
|
43
|
+
* `organization` / `project` / `bot` accounts are operated via `AccountMember`
|
|
44
|
+
* and have no direct login.
|
|
45
|
+
*/
|
|
46
|
+
export type AccountKind = 'personal' | 'organization' | 'project' | 'bot';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The calling user's relationship to an account node, as resolved by the API:
|
|
50
|
+
* - `self` — the caller's own personal (root) account.
|
|
51
|
+
* - `owner` — an account the caller owns (e.g. an org/project/bot they created).
|
|
52
|
+
* - `member` — an account shared with the caller via membership (including
|
|
53
|
+
* external organisations).
|
|
54
|
+
*/
|
|
55
|
+
export type AccountRelationship = 'self' | 'owner' | 'member';
|
|
56
|
+
|
|
57
|
+
/** Role a member holds within an account. The unified account role set. */
|
|
58
|
+
export type AccountRole = 'owner' | 'admin' | 'editor' | 'developer' | 'billing' | 'viewer';
|
|
59
|
+
|
|
60
|
+
/** Membership lifecycle status. */
|
|
61
|
+
export type AccountMemberStatus = 'active' | 'invited' | 'removed';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Origin of a resolved membership. `direct` is a membership row on the account
|
|
65
|
+
* itself; `inherited` is resolved from the nearest ancestor account whose
|
|
66
|
+
* membership row has `inherit: true` (role inheritance cascades down the tree).
|
|
67
|
+
*/
|
|
68
|
+
export type AccountMemberSource = 'direct' | 'inherited';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Client-facing AccountMember shape. `permissions` is derived from `role` on the
|
|
72
|
+
* server at write time.
|
|
73
|
+
*/
|
|
74
|
+
export interface AccountMember {
|
|
75
|
+
_id: string;
|
|
76
|
+
/** The account this membership grants access to (account `_id`). */
|
|
77
|
+
accountId: string;
|
|
78
|
+
/** The member's personal-account `User._id`. */
|
|
79
|
+
memberUserId: string;
|
|
80
|
+
role: AccountRole;
|
|
81
|
+
permissions: string[];
|
|
82
|
+
/**
|
|
83
|
+
* Whether this membership cascades to the account's subtree. `true` (default)
|
|
84
|
+
* lets descendants inherit this role unless a nearer row overrides it; `false`
|
|
85
|
+
* opts this row out of inheritance (it applies to this account only).
|
|
86
|
+
*/
|
|
87
|
+
inherit: boolean;
|
|
88
|
+
status: AccountMemberStatus;
|
|
89
|
+
/**
|
|
90
|
+
* Origin of the membership when the API resolves an effective role. Present on
|
|
91
|
+
* a resolved `callerMembership` to indicate whether the caller's access is
|
|
92
|
+
* `direct` on the account or `inherited` from an ancestor. Absent on plain
|
|
93
|
+
* member-list rows (which are always direct rows on the account).
|
|
94
|
+
*/
|
|
95
|
+
source?: AccountMemberSource;
|
|
96
|
+
invitedByUserId?: string | null;
|
|
97
|
+
joinedAt?: string | null;
|
|
98
|
+
createdAt: string;
|
|
99
|
+
updatedAt: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A node in the account graph as returned by the `/accounts` API. `account` is
|
|
104
|
+
* the underlying generalised `User` document; `relationship` and
|
|
105
|
+
* `callerMembership` describe the caller's access. On a flat list every node
|
|
106
|
+
* carries `parentAccountId`; with `tree:true`, `children` is populated and
|
|
107
|
+
* `childCount` reflects the number of direct children.
|
|
108
|
+
*/
|
|
109
|
+
export interface AccountNode {
|
|
110
|
+
/** The account's Mongo `_id` (the underlying `User._id`). */
|
|
111
|
+
accountId: string;
|
|
112
|
+
kind: AccountKind;
|
|
113
|
+
/** Parent account `_id`, or `null` for a root (personal) account. */
|
|
114
|
+
parentAccountId: string | null;
|
|
115
|
+
/** The generalised `User` document backing this account. */
|
|
116
|
+
account: User;
|
|
117
|
+
relationship: AccountRelationship;
|
|
118
|
+
/**
|
|
119
|
+
* The caller's effective membership in this account (direct or inherited), or
|
|
120
|
+
* `null` when the caller has no membership (e.g. their own `self` root, where
|
|
121
|
+
* ownership is implicit). Use `callerMembership.permissions` to gate UI.
|
|
122
|
+
*/
|
|
123
|
+
callerMembership: AccountMember | null;
|
|
124
|
+
/** Number of direct child accounts (present when the API computes it). */
|
|
125
|
+
childCount?: number;
|
|
126
|
+
/** Direct children, populated when the list is requested with `tree:true`. */
|
|
127
|
+
children?: AccountNode[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Options accepted by `listAccounts`. */
|
|
131
|
+
export interface ListAccountsOptions {
|
|
132
|
+
/**
|
|
133
|
+
* When `true`, request the nested tree representation: each returned node has
|
|
134
|
+
* its `children` populated instead of a flat list keyed by `parentAccountId`.
|
|
135
|
+
*/
|
|
136
|
+
tree?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Input accepted by `createAccount`. */
|
|
140
|
+
export interface CreateAccountInput {
|
|
141
|
+
/** Classification of the new account. `personal` accounts are not created here. */
|
|
142
|
+
kind: AccountKind;
|
|
143
|
+
/**
|
|
144
|
+
* Parent account `_id` to nest the new account under. Omitted → the API roots
|
|
145
|
+
* it under the caller's personal account.
|
|
146
|
+
*/
|
|
147
|
+
parentAccountId?: string;
|
|
148
|
+
/** Unique handle for the account (shares the `User.username` unique index). */
|
|
149
|
+
username: string;
|
|
150
|
+
name?: { first?: string; last?: string };
|
|
151
|
+
bio?: string;
|
|
152
|
+
avatar?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Input accepted by `updateAccount`. Tree placement changes go through `/move`. */
|
|
156
|
+
export interface UpdateAccountInput {
|
|
157
|
+
username?: string;
|
|
158
|
+
name?: { first?: string; last?: string };
|
|
159
|
+
bio?: string | null;
|
|
160
|
+
avatar?: string | null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Input accepted by `inviteAccountMember`. The owner role cannot be invited. */
|
|
164
|
+
export interface InviteAccountMemberInput {
|
|
165
|
+
/**
|
|
166
|
+
* The username or email of the user to invite. Resolved to a personal account
|
|
167
|
+
* server-side; an unknown value yields a 404 "User not found".
|
|
168
|
+
*/
|
|
169
|
+
usernameOrEmail: string;
|
|
170
|
+
role: Exclude<AccountRole, 'owner'>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Input accepted by `updateAccountMember`. The owner role cannot be assigned. */
|
|
174
|
+
export interface UpdateAccountMemberInput {
|
|
175
|
+
role: Exclude<AccountRole, 'owner'>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Input accepted by `transferAccountOwnership`. */
|
|
179
|
+
export interface TransferAccountOwnershipInput {
|
|
180
|
+
userId: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Bot (account) service-credential types
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/** Credential kind. Account (bot) credentials are always `service` tokens. */
|
|
188
|
+
export type AccountCredentialType = 'service';
|
|
189
|
+
|
|
190
|
+
/** Deployment environment a bot credential is scoped to. */
|
|
191
|
+
export type AccountCredentialEnvironment = 'development' | 'staging' | 'production';
|
|
192
|
+
|
|
193
|
+
/** Bot credential lifecycle status. */
|
|
194
|
+
export type AccountCredentialStatus = 'active' | 'deprecated' | 'revoked';
|
|
195
|
+
|
|
196
|
+
/** Input accepted by `createAccountCredential`. Credential `type` is always `service`. */
|
|
197
|
+
export interface CreateAccountCredentialInput {
|
|
198
|
+
name: string;
|
|
199
|
+
environment: AccountCredentialEnvironment;
|
|
200
|
+
scopes?: string[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Client-facing AccountCredential shape (a bot account's service token). The raw
|
|
205
|
+
* secret is NEVER part of this shape — it is returned exactly once, separately,
|
|
206
|
+
* at creation/rotation.
|
|
207
|
+
*/
|
|
208
|
+
export interface AccountCredential {
|
|
209
|
+
_id: string;
|
|
210
|
+
/** The bot account this credential authenticates as (account `_id`). */
|
|
211
|
+
accountId: string;
|
|
212
|
+
name: string;
|
|
213
|
+
publicKey: string;
|
|
214
|
+
type: AccountCredentialType;
|
|
215
|
+
environment: AccountCredentialEnvironment;
|
|
216
|
+
scopes: string[];
|
|
217
|
+
status: AccountCredentialStatus;
|
|
218
|
+
lastUsedAt?: string;
|
|
219
|
+
expiresAt?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Audit link to the credential this one was rotated FROM. Populated on
|
|
222
|
+
* credentials created via rotation; absent on original credentials.
|
|
223
|
+
*/
|
|
224
|
+
rotatedFromCredentialId?: string;
|
|
225
|
+
createdByUserId: string;
|
|
226
|
+
createdAt: string;
|
|
227
|
+
updatedAt: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Result of creating a bot credential — `secret` is returned ONCE. */
|
|
231
|
+
export interface AccountCredentialWithSecret {
|
|
232
|
+
credential: AccountCredential;
|
|
233
|
+
secret: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Result of rotating a bot credential. Extends the create result with audit
|
|
238
|
+
* fields: the new plaintext `secret` is returned ONCE, plus `rotatedFrom` (the
|
|
239
|
+
* previous credential's `credentialId`) and `graceExpiresAt` (ISO string marking
|
|
240
|
+
* when the old credential stops being honoured during the rotation grace window).
|
|
241
|
+
*/
|
|
242
|
+
export interface RotateAccountCredentialResult extends AccountCredentialWithSecret {
|
|
243
|
+
/** The previous credential's `credentialId` that this rotation supersedes. */
|
|
244
|
+
rotatedFrom: string;
|
|
245
|
+
/** ISO timestamp at which the rotated-from credential's grace window ends. */
|
|
246
|
+
graceExpiresAt: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Application (owned by an account) types
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Application classification. Set only by Oxy platform staff — never editable
|
|
255
|
+
* through the normal member-facing update path.
|
|
256
|
+
*/
|
|
257
|
+
export type ApplicationType = 'first_party' | 'third_party' | 'internal' | 'system';
|
|
258
|
+
|
|
259
|
+
/** Lifecycle status of an application. */
|
|
260
|
+
export type ApplicationStatus = 'active' | 'suspended' | 'deleted' | 'pending_review';
|
|
261
|
+
|
|
262
|
+
/** OAuth credential kind. `service` credentials mint service tokens. */
|
|
263
|
+
export type ApplicationCredentialType = 'public' | 'confidential' | 'service';
|
|
264
|
+
|
|
265
|
+
/** Deployment environment an application credential is scoped to. */
|
|
266
|
+
export type ApplicationEnvironment = 'development' | 'staging' | 'production';
|
|
267
|
+
|
|
268
|
+
/** Application credential lifecycle status. */
|
|
269
|
+
export type ApplicationCredentialStatus = 'active' | 'deprecated' | 'revoked';
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Client-facing Application shape returned by the `/applications` API. An
|
|
273
|
+
* application is the OAuth client; it is OWNED by an account
|
|
274
|
+
* (`ownerAccountId`), and the caller's access derives from their `AccountMember`
|
|
275
|
+
* on that owning account (with inheritance).
|
|
276
|
+
*/
|
|
277
|
+
export interface Application {
|
|
278
|
+
_id: string;
|
|
279
|
+
name: string;
|
|
280
|
+
description?: string;
|
|
281
|
+
websiteUrl?: string;
|
|
282
|
+
icon?: string;
|
|
283
|
+
type: ApplicationType;
|
|
284
|
+
status: ApplicationStatus;
|
|
285
|
+
isOfficial: boolean;
|
|
286
|
+
isInternal: boolean;
|
|
287
|
+
capabilities: string[];
|
|
288
|
+
redirectUris: string[];
|
|
289
|
+
scopes: string[];
|
|
290
|
+
webhookUrl?: string;
|
|
291
|
+
devWebhookUrl?: string;
|
|
292
|
+
createdByUserId: string;
|
|
293
|
+
/**
|
|
294
|
+
* The account that owns this application (account `_id`). Access to the
|
|
295
|
+
* application derives from the caller's `AccountMember` on this account, with
|
|
296
|
+
* inheritance up the account tree.
|
|
297
|
+
*/
|
|
298
|
+
ownerAccountId: string;
|
|
299
|
+
createdAt: string;
|
|
300
|
+
updatedAt: string;
|
|
301
|
+
/**
|
|
302
|
+
* The caller's effective membership in the OWNING account (direct or
|
|
303
|
+
* inherited), embedded by the API on list/detail responses, or `null` when the
|
|
304
|
+
* caller has no membership. Use `callerMembership.permissions` to gate UI.
|
|
305
|
+
*/
|
|
306
|
+
callerMembership?: AccountMember | null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Client-facing ApplicationCredential shape (an application's OAuth client
|
|
311
|
+
* credentials). The raw secret is NEVER part of this shape — it is returned
|
|
312
|
+
* exactly once, separately, at creation/rotation.
|
|
313
|
+
*/
|
|
314
|
+
export interface ApplicationCredential {
|
|
315
|
+
_id: string;
|
|
316
|
+
applicationId: string;
|
|
317
|
+
name: string;
|
|
318
|
+
publicKey: string;
|
|
319
|
+
type: ApplicationCredentialType;
|
|
320
|
+
environment: ApplicationEnvironment;
|
|
321
|
+
scopes: string[];
|
|
322
|
+
status: ApplicationCredentialStatus;
|
|
323
|
+
lastUsedAt?: string;
|
|
324
|
+
expiresAt?: string;
|
|
325
|
+
/**
|
|
326
|
+
* Audit link to the credential this one was rotated FROM. Populated on
|
|
327
|
+
* credentials created via rotation; absent on original credentials.
|
|
328
|
+
*/
|
|
329
|
+
rotatedFromCredentialId?: string;
|
|
330
|
+
createdByUserId: string;
|
|
331
|
+
createdAt: string;
|
|
332
|
+
updatedAt: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Input accepted by `createApp`. Staff-only fields are not settable here. */
|
|
336
|
+
export interface CreateApplicationInput {
|
|
337
|
+
name: string;
|
|
338
|
+
description?: string;
|
|
339
|
+
websiteUrl?: string;
|
|
340
|
+
icon?: string;
|
|
341
|
+
redirectUris?: string[];
|
|
342
|
+
scopes?: string[];
|
|
343
|
+
/**
|
|
344
|
+
* Owning account `_id`. Omitted → the API defaults to the caller's personal
|
|
345
|
+
* account.
|
|
346
|
+
*/
|
|
347
|
+
ownerAccountId?: string;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Input accepted by `updateApp`. Staff-only fields are not settable here. */
|
|
351
|
+
export interface UpdateApplicationInput {
|
|
352
|
+
name?: string;
|
|
353
|
+
description?: string;
|
|
354
|
+
websiteUrl?: string;
|
|
355
|
+
icon?: string;
|
|
356
|
+
redirectUris?: string[];
|
|
357
|
+
scopes?: string[];
|
|
358
|
+
webhookUrl?: string;
|
|
359
|
+
devWebhookUrl?: string;
|
|
360
|
+
status?: ApplicationStatus;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Input accepted by `createAppCredential`. */
|
|
364
|
+
export interface CreateApplicationCredentialInput {
|
|
365
|
+
name: string;
|
|
366
|
+
type: ApplicationCredentialType;
|
|
367
|
+
environment: ApplicationEnvironment;
|
|
368
|
+
scopes?: string[];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Result of creating an application credential — `secret` is returned ONCE. */
|
|
372
|
+
export interface ApplicationCredentialWithSecret {
|
|
373
|
+
credential: ApplicationCredential;
|
|
374
|
+
secret: string;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Result of rotating an application credential. Extends the create result with
|
|
379
|
+
* audit fields: the new plaintext `secret` is returned ONCE, plus `rotatedFrom`
|
|
380
|
+
* (the previous credential's `credentialId`) and `graceExpiresAt` (ISO string
|
|
381
|
+
* marking when the old credential stops being honoured during the grace window).
|
|
382
|
+
*/
|
|
383
|
+
export interface RotateApplicationCredentialResult extends ApplicationCredentialWithSecret {
|
|
384
|
+
/** The previous credential's `credentialId` that this rotation supersedes. */
|
|
385
|
+
rotatedFrom: string;
|
|
386
|
+
/** ISO timestamp at which the rotated-from credential's grace window ends. */
|
|
387
|
+
graceExpiresAt: string;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Time window for application usage statistics. */
|
|
391
|
+
export type ApplicationUsagePeriod = '24h' | '7d' | '30d' | '90d';
|
|
392
|
+
|
|
393
|
+
/** Aggregate totals for an application over the requested period. */
|
|
394
|
+
export interface ApplicationUsageSummary {
|
|
395
|
+
totalRequests: number;
|
|
396
|
+
totalTokens: number;
|
|
397
|
+
totalCredits: number;
|
|
398
|
+
avgResponseTime: number;
|
|
399
|
+
successfulRequests: number;
|
|
400
|
+
errorRequests: number;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Per-day usage bucket. `_id` is the day key (e.g. `YYYY-MM-DD`). */
|
|
404
|
+
export interface ApplicationUsageByDay {
|
|
405
|
+
_id: string;
|
|
406
|
+
requests: number;
|
|
407
|
+
tokens: number;
|
|
408
|
+
credits: number;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Per-endpoint usage bucket. `_id` is the endpoint identifier. */
|
|
412
|
+
export interface ApplicationUsageByEndpoint {
|
|
413
|
+
_id: string;
|
|
414
|
+
requests: number;
|
|
415
|
+
tokens: number;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Usage statistics for an application over a period. */
|
|
419
|
+
export interface ApplicationUsageStats {
|
|
420
|
+
summary: ApplicationUsageSummary;
|
|
421
|
+
byDay: ApplicationUsageByDay[];
|
|
422
|
+
byEndpoint: ApplicationUsageByEndpoint[];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Result of an archive/remove/revoke/transfer/delete operation. */
|
|
426
|
+
export interface AccountSuccessResult {
|
|
427
|
+
success: boolean;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function OxyServicesAccountsMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
431
|
+
return class extends Base {
|
|
432
|
+
constructor(...args: any[]) {
|
|
433
|
+
super(...(args as [any]));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// =========================================================================
|
|
437
|
+
// Accounts
|
|
438
|
+
// =========================================================================
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* List the accounts the caller can access: their own personal (root)
|
|
442
|
+
* account, accounts they own, and accounts shared with them (including
|
|
443
|
+
* external organisations), plus the reachable subtree of each.
|
|
444
|
+
*
|
|
445
|
+
* @param opts - `{ tree: true }` requests the nested tree representation
|
|
446
|
+
* (`children` populated) instead of a flat list. The flag is appended to
|
|
447
|
+
* the path as `?tree=true`, so the response cache keys on it automatically —
|
|
448
|
+
* the flat and tree variants never collide.
|
|
449
|
+
*/
|
|
450
|
+
async listAccounts(opts?: ListAccountsOptions): Promise<AccountNode[]> {
|
|
451
|
+
try {
|
|
452
|
+
const path = opts?.tree ? '/accounts?tree=true' : '/accounts';
|
|
453
|
+
const res = await this.makeRequest<{ accounts?: AccountNode[] }>(
|
|
454
|
+
'GET',
|
|
455
|
+
path,
|
|
456
|
+
undefined,
|
|
457
|
+
{ cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
|
|
458
|
+
);
|
|
459
|
+
return res.accounts ?? [];
|
|
460
|
+
} catch (error) {
|
|
461
|
+
throw this.handleError(error);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Fetch a single account node by id.
|
|
467
|
+
* @param accountId - The account's Mongo `_id`.
|
|
468
|
+
*/
|
|
469
|
+
async getAccount(accountId: string): Promise<AccountNode> {
|
|
470
|
+
try {
|
|
471
|
+
const res = await this.makeRequest<{ account: AccountNode }>(
|
|
472
|
+
'GET',
|
|
473
|
+
`/accounts/${encodeURIComponent(accountId)}`,
|
|
474
|
+
undefined,
|
|
475
|
+
{ cache: true, cacheTTL: CACHE_TIMES.LONG },
|
|
476
|
+
);
|
|
477
|
+
return res.account;
|
|
478
|
+
} catch (error) {
|
|
479
|
+
throw this.handleError(error);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Create a new (non-personal) account. The caller becomes its `owner`.
|
|
485
|
+
* @param data - Account configuration: kind, optional parent, and profile.
|
|
486
|
+
*/
|
|
487
|
+
async createAccount(data: CreateAccountInput): Promise<AccountNode> {
|
|
488
|
+
try {
|
|
489
|
+
const res = await this.makeRequest<{ account: AccountNode }>(
|
|
490
|
+
'POST',
|
|
491
|
+
'/accounts',
|
|
492
|
+
data,
|
|
493
|
+
{ cache: false },
|
|
494
|
+
);
|
|
495
|
+
// A new account changes the accessible forest — bust every cached list
|
|
496
|
+
// (flat + tree) so it appears on the next `listAccounts()` read.
|
|
497
|
+
this._invalidateAccountLists();
|
|
498
|
+
return res.account;
|
|
499
|
+
} catch (error) {
|
|
500
|
+
throw this.handleError(error);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Update an account's mutable profile fields. Tree placement changes
|
|
506
|
+
* (reparenting) go through the dedicated move endpoint, not here.
|
|
507
|
+
* @param accountId - The account's Mongo `_id`.
|
|
508
|
+
* @param data - Subset of updatable profile fields.
|
|
509
|
+
*/
|
|
510
|
+
async updateAccount(
|
|
511
|
+
accountId: string,
|
|
512
|
+
data: UpdateAccountInput,
|
|
513
|
+
): Promise<AccountNode> {
|
|
514
|
+
try {
|
|
515
|
+
const res = await this.makeRequest<{ account: AccountNode }>(
|
|
516
|
+
'PATCH',
|
|
517
|
+
`/accounts/${encodeURIComponent(accountId)}`,
|
|
518
|
+
data,
|
|
519
|
+
{ cache: false },
|
|
520
|
+
);
|
|
521
|
+
// Bust the cached detail and every list (which embeds account profile
|
|
522
|
+
// data) so neither serves the pre-update snapshot.
|
|
523
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
524
|
+
this._invalidateAccountLists();
|
|
525
|
+
return res.account;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
throw this.handleError(error);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Archive an account (soft delete). Named `archiveAccount` — NOT
|
|
533
|
+
* `deleteAccount`, which is reserved for the GDPR self-deletion flow on the
|
|
534
|
+
* user mixin (`OxyServices.user.ts`).
|
|
535
|
+
* @param accountId - The account's Mongo `_id`.
|
|
536
|
+
*/
|
|
537
|
+
async archiveAccount(accountId: string): Promise<AccountSuccessResult> {
|
|
538
|
+
try {
|
|
539
|
+
const result = await this.makeRequest<AccountSuccessResult>(
|
|
540
|
+
'DELETE',
|
|
541
|
+
`/accounts/${encodeURIComponent(accountId)}`,
|
|
542
|
+
undefined,
|
|
543
|
+
{ cache: false },
|
|
544
|
+
);
|
|
545
|
+
// Bust every cached representation of the archived account.
|
|
546
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
547
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/members`);
|
|
548
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
|
|
549
|
+
this._invalidateAccountLists();
|
|
550
|
+
return result;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
throw this.handleError(error);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* List the direct child accounts of an account.
|
|
558
|
+
* @param accountId - The parent account's Mongo `_id`.
|
|
559
|
+
*/
|
|
560
|
+
async listChildAccounts(accountId: string): Promise<AccountNode[]> {
|
|
561
|
+
try {
|
|
562
|
+
const res = await this.makeRequest<{ accounts?: AccountNode[] }>(
|
|
563
|
+
'GET',
|
|
564
|
+
`/accounts/${encodeURIComponent(accountId)}/children`,
|
|
565
|
+
undefined,
|
|
566
|
+
{ cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
|
|
567
|
+
);
|
|
568
|
+
return res.accounts ?? [];
|
|
569
|
+
} catch (error) {
|
|
570
|
+
throw this.handleError(error);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// =========================================================================
|
|
575
|
+
// Account members
|
|
576
|
+
// =========================================================================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* List members of an account (direct membership rows on the account).
|
|
580
|
+
* @param accountId - The account's Mongo `_id`.
|
|
581
|
+
*/
|
|
582
|
+
async listAccountMembers(accountId: string): Promise<AccountMember[]> {
|
|
583
|
+
try {
|
|
584
|
+
const res = await this.makeRequest<{ members?: AccountMember[] }>(
|
|
585
|
+
'GET',
|
|
586
|
+
`/accounts/${encodeURIComponent(accountId)}/members`,
|
|
587
|
+
undefined,
|
|
588
|
+
{ cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
|
|
589
|
+
);
|
|
590
|
+
return res.members ?? [];
|
|
591
|
+
} catch (error) {
|
|
592
|
+
throw this.handleError(error);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Add a member to an account.
|
|
598
|
+
* @param accountId - The account's Mongo `_id`.
|
|
599
|
+
* @param data - Target user's username or email and role (never `owner`).
|
|
600
|
+
* The server resolves `usernameOrEmail` to a personal account; an unknown
|
|
601
|
+
* value yields a 404 "User not found".
|
|
602
|
+
*/
|
|
603
|
+
async inviteAccountMember(
|
|
604
|
+
accountId: string,
|
|
605
|
+
data: InviteAccountMemberInput,
|
|
606
|
+
): Promise<AccountMember> {
|
|
607
|
+
try {
|
|
608
|
+
const res = await this.makeRequest<{ member: AccountMember }>(
|
|
609
|
+
'POST',
|
|
610
|
+
`/accounts/${encodeURIComponent(accountId)}/members`,
|
|
611
|
+
data,
|
|
612
|
+
{ cache: false },
|
|
613
|
+
);
|
|
614
|
+
this._invalidateAccountMembership(accountId);
|
|
615
|
+
return res.member;
|
|
616
|
+
} catch (error) {
|
|
617
|
+
throw this.handleError(error);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Change a member's role.
|
|
623
|
+
* @param accountId - The account's Mongo `_id`.
|
|
624
|
+
* @param memberId - The member's Mongo `_id`.
|
|
625
|
+
* @param data - New role (never `owner`).
|
|
626
|
+
*/
|
|
627
|
+
async updateAccountMember(
|
|
628
|
+
accountId: string,
|
|
629
|
+
memberId: string,
|
|
630
|
+
data: UpdateAccountMemberInput,
|
|
631
|
+
): Promise<AccountMember> {
|
|
632
|
+
try {
|
|
633
|
+
const res = await this.makeRequest<{ member: AccountMember }>(
|
|
634
|
+
'PATCH',
|
|
635
|
+
`/accounts/${encodeURIComponent(accountId)}/members/${encodeURIComponent(memberId)}`,
|
|
636
|
+
data,
|
|
637
|
+
{ cache: false },
|
|
638
|
+
);
|
|
639
|
+
this._invalidateAccountMembership(accountId);
|
|
640
|
+
return res.member;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
throw this.handleError(error);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Remove a member from an account.
|
|
648
|
+
* @param accountId - The account's Mongo `_id`.
|
|
649
|
+
* @param memberId - The member's Mongo `_id`.
|
|
650
|
+
*/
|
|
651
|
+
async removeAccountMember(
|
|
652
|
+
accountId: string,
|
|
653
|
+
memberId: string,
|
|
654
|
+
): Promise<AccountSuccessResult> {
|
|
655
|
+
try {
|
|
656
|
+
const result = await this.makeRequest<AccountSuccessResult>(
|
|
657
|
+
'DELETE',
|
|
658
|
+
`/accounts/${encodeURIComponent(accountId)}/members/${encodeURIComponent(memberId)}`,
|
|
659
|
+
undefined,
|
|
660
|
+
{ cache: false },
|
|
661
|
+
);
|
|
662
|
+
this._invalidateAccountMembership(accountId);
|
|
663
|
+
return result;
|
|
664
|
+
} catch (error) {
|
|
665
|
+
throw this.handleError(error);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Transfer ownership of an account to another member (owner only).
|
|
671
|
+
* @param accountId - The account's Mongo `_id`.
|
|
672
|
+
* @param data - Target user id.
|
|
673
|
+
*/
|
|
674
|
+
async transferAccountOwnership(
|
|
675
|
+
accountId: string,
|
|
676
|
+
data: TransferAccountOwnershipInput,
|
|
677
|
+
): Promise<AccountSuccessResult> {
|
|
678
|
+
try {
|
|
679
|
+
const result = await this.makeRequest<AccountSuccessResult>(
|
|
680
|
+
'POST',
|
|
681
|
+
`/accounts/${encodeURIComponent(accountId)}/transfer-ownership`,
|
|
682
|
+
data,
|
|
683
|
+
{ cache: false },
|
|
684
|
+
);
|
|
685
|
+
// Ownership change alters roles in the member list AND the detail, and
|
|
686
|
+
// can change which accounts the caller "owns" in the list view.
|
|
687
|
+
this._invalidateAccountMembership(accountId);
|
|
688
|
+
this._invalidateAccountLists();
|
|
689
|
+
return result;
|
|
690
|
+
} catch (error) {
|
|
691
|
+
throw this.handleError(error);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// =========================================================================
|
|
696
|
+
// Bot (account) service credentials — /accounts/:id/credentials
|
|
697
|
+
// =========================================================================
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* List a bot account's service credentials. The response NEVER includes
|
|
701
|
+
* secrets.
|
|
702
|
+
* @param accountId - The account's Mongo `_id`.
|
|
703
|
+
*/
|
|
704
|
+
async listAccountCredentials(accountId: string): Promise<AccountCredential[]> {
|
|
705
|
+
try {
|
|
706
|
+
const res = await this.makeRequest<{ credentials?: AccountCredential[] }>(
|
|
707
|
+
'GET',
|
|
708
|
+
`/accounts/${encodeURIComponent(accountId)}/credentials`,
|
|
709
|
+
undefined,
|
|
710
|
+
{ cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
|
|
711
|
+
);
|
|
712
|
+
return res.credentials ?? [];
|
|
713
|
+
} catch (error) {
|
|
714
|
+
throw this.handleError(error);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Create a service credential for a bot account. The plaintext `secret` is
|
|
720
|
+
* returned exactly ONCE; the server stores only a hash and will never return
|
|
721
|
+
* it again.
|
|
722
|
+
* @param accountId - The account's Mongo `_id`.
|
|
723
|
+
* @param data - Credential configuration (`type` is always `service`).
|
|
724
|
+
*/
|
|
725
|
+
async createAccountCredential(
|
|
726
|
+
accountId: string,
|
|
727
|
+
data: CreateAccountCredentialInput,
|
|
728
|
+
): Promise<AccountCredentialWithSecret> {
|
|
729
|
+
try {
|
|
730
|
+
const result = await this.makeRequest<AccountCredentialWithSecret>(
|
|
731
|
+
'POST',
|
|
732
|
+
`/accounts/${encodeURIComponent(accountId)}/credentials`,
|
|
733
|
+
data,
|
|
734
|
+
{ cache: false },
|
|
735
|
+
);
|
|
736
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
|
|
737
|
+
return result;
|
|
738
|
+
} catch (error) {
|
|
739
|
+
throw this.handleError(error);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Rotate a bot credential's secret. The new plaintext `secret` is returned
|
|
745
|
+
* exactly ONCE, along with audit fields: `rotatedFrom` (the previous
|
|
746
|
+
* credentialId) and `graceExpiresAt` (ISO string for the grace window during
|
|
747
|
+
* which the old credential is still honoured).
|
|
748
|
+
* @param accountId - The account's Mongo `_id`.
|
|
749
|
+
* @param credentialId - The credential's Mongo `_id`.
|
|
750
|
+
*/
|
|
751
|
+
async rotateAccountCredential(
|
|
752
|
+
accountId: string,
|
|
753
|
+
credentialId: string,
|
|
754
|
+
): Promise<RotateAccountCredentialResult> {
|
|
755
|
+
try {
|
|
756
|
+
const result = await this.makeRequest<RotateAccountCredentialResult>(
|
|
757
|
+
'POST',
|
|
758
|
+
`/accounts/${encodeURIComponent(accountId)}/credentials/${encodeURIComponent(credentialId)}/rotate`,
|
|
759
|
+
undefined,
|
|
760
|
+
{ cache: false },
|
|
761
|
+
);
|
|
762
|
+
// Rotation changes credential status/audit fields surfaced by the list.
|
|
763
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
|
|
764
|
+
return result;
|
|
765
|
+
} catch (error) {
|
|
766
|
+
throw this.handleError(error);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Revoke a bot credential (`status='revoked'`). Revoked credentials can no
|
|
772
|
+
* longer authenticate.
|
|
773
|
+
* @param accountId - The account's Mongo `_id`.
|
|
774
|
+
* @param credentialId - The credential's Mongo `_id`.
|
|
775
|
+
*/
|
|
776
|
+
async revokeAccountCredential(
|
|
777
|
+
accountId: string,
|
|
778
|
+
credentialId: string,
|
|
779
|
+
): Promise<AccountSuccessResult> {
|
|
780
|
+
try {
|
|
781
|
+
const result = await this.makeRequest<AccountSuccessResult>(
|
|
782
|
+
'DELETE',
|
|
783
|
+
`/accounts/${encodeURIComponent(accountId)}/credentials/${encodeURIComponent(credentialId)}`,
|
|
784
|
+
undefined,
|
|
785
|
+
{ cache: false },
|
|
786
|
+
);
|
|
787
|
+
// Revocation flips the credential's status in the cached list.
|
|
788
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
|
|
789
|
+
return result;
|
|
790
|
+
} catch (error) {
|
|
791
|
+
throw this.handleError(error);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// =========================================================================
|
|
796
|
+
// Applications owned by an account — /applications
|
|
797
|
+
// =========================================================================
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* List the applications owned by an account. Backed by
|
|
801
|
+
* `GET /applications?ownerAccountId=<id>`.
|
|
802
|
+
* @param accountId - The owning account's Mongo `_id`.
|
|
803
|
+
*/
|
|
804
|
+
async listAccountApps(accountId: string): Promise<Application[]> {
|
|
805
|
+
try {
|
|
806
|
+
const res = await this.makeRequest<{ applications?: Application[] }>(
|
|
807
|
+
'GET',
|
|
808
|
+
`/applications?ownerAccountId=${encodeURIComponent(accountId)}`,
|
|
809
|
+
undefined,
|
|
810
|
+
{ cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
|
|
811
|
+
);
|
|
812
|
+
return res.applications ?? [];
|
|
813
|
+
} catch (error) {
|
|
814
|
+
throw this.handleError(error);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Create a new application owned by an account.
|
|
820
|
+
* @param data - Application configuration. `ownerAccountId` defaults to the
|
|
821
|
+
* caller's personal account when omitted. Staff-only fields are ignored.
|
|
822
|
+
*/
|
|
823
|
+
async createApp(data: CreateApplicationInput): Promise<Application> {
|
|
824
|
+
try {
|
|
825
|
+
const res = await this.makeRequest<{ application: Application }>(
|
|
826
|
+
'POST',
|
|
827
|
+
'/applications',
|
|
828
|
+
data,
|
|
829
|
+
{ cache: false },
|
|
830
|
+
);
|
|
831
|
+
// Bust every cached application list (per owning account) so the new app
|
|
832
|
+
// appears on the next `listAccountApps()` read.
|
|
833
|
+
this._invalidateAppLists();
|
|
834
|
+
return res.application;
|
|
835
|
+
} catch (error) {
|
|
836
|
+
throw this.handleError(error);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Fetch a single application by id.
|
|
842
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
843
|
+
*/
|
|
844
|
+
async getApp(applicationId: string): Promise<Application> {
|
|
845
|
+
try {
|
|
846
|
+
const res = await this.makeRequest<{ application: Application }>(
|
|
847
|
+
'GET',
|
|
848
|
+
`/applications/${encodeURIComponent(applicationId)}`,
|
|
849
|
+
undefined,
|
|
850
|
+
{ cache: true, cacheTTL: CACHE_TIMES.LONG },
|
|
851
|
+
);
|
|
852
|
+
return res.application;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
throw this.handleError(error);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Update an application's mutable fields.
|
|
860
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
861
|
+
* @param data - Subset of updatable fields. Staff-only fields are ignored.
|
|
862
|
+
*/
|
|
863
|
+
async updateApp(
|
|
864
|
+
applicationId: string,
|
|
865
|
+
data: UpdateApplicationInput,
|
|
866
|
+
): Promise<Application> {
|
|
867
|
+
try {
|
|
868
|
+
const res = await this.makeRequest<{ application: Application }>(
|
|
869
|
+
'PATCH',
|
|
870
|
+
`/applications/${encodeURIComponent(applicationId)}`,
|
|
871
|
+
data,
|
|
872
|
+
{ cache: false },
|
|
873
|
+
);
|
|
874
|
+
// Bust the cached detail and every list (which embeds application fields).
|
|
875
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}`);
|
|
876
|
+
this._invalidateAppLists();
|
|
877
|
+
return res.application;
|
|
878
|
+
} catch (error) {
|
|
879
|
+
throw this.handleError(error);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Soft-delete an application.
|
|
885
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
886
|
+
*/
|
|
887
|
+
async deleteApp(applicationId: string): Promise<AccountSuccessResult> {
|
|
888
|
+
try {
|
|
889
|
+
const result = await this.makeRequest<AccountSuccessResult>(
|
|
890
|
+
'DELETE',
|
|
891
|
+
`/applications/${encodeURIComponent(applicationId)}`,
|
|
892
|
+
undefined,
|
|
893
|
+
{ cache: false },
|
|
894
|
+
);
|
|
895
|
+
// Bust every cached representation of the deleted application.
|
|
896
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}`);
|
|
897
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
|
|
898
|
+
this._invalidateAppLists();
|
|
899
|
+
return result;
|
|
900
|
+
} catch (error) {
|
|
901
|
+
throw this.handleError(error);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// =========================================================================
|
|
906
|
+
// Application OAuth credentials — /applications/:appId/credentials
|
|
907
|
+
// =========================================================================
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* List an application's OAuth credentials. The response NEVER includes
|
|
911
|
+
* secrets.
|
|
912
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
913
|
+
*/
|
|
914
|
+
async listAppCredentials(applicationId: string): Promise<ApplicationCredential[]> {
|
|
915
|
+
try {
|
|
916
|
+
const res = await this.makeRequest<{ credentials?: ApplicationCredential[] }>(
|
|
917
|
+
'GET',
|
|
918
|
+
`/applications/${encodeURIComponent(applicationId)}/credentials`,
|
|
919
|
+
undefined,
|
|
920
|
+
{ cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
|
|
921
|
+
);
|
|
922
|
+
return res.credentials ?? [];
|
|
923
|
+
} catch (error) {
|
|
924
|
+
throw this.handleError(error);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Create an application credential. The plaintext `secret` is returned
|
|
930
|
+
* exactly ONCE; the server stores only a hash and will never return it again.
|
|
931
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
932
|
+
* @param data - Credential configuration.
|
|
933
|
+
*/
|
|
934
|
+
async createAppCredential(
|
|
935
|
+
applicationId: string,
|
|
936
|
+
data: CreateApplicationCredentialInput,
|
|
937
|
+
): Promise<ApplicationCredentialWithSecret> {
|
|
938
|
+
try {
|
|
939
|
+
const result = await this.makeRequest<ApplicationCredentialWithSecret>(
|
|
940
|
+
'POST',
|
|
941
|
+
`/applications/${encodeURIComponent(applicationId)}/credentials`,
|
|
942
|
+
data,
|
|
943
|
+
{ cache: false },
|
|
944
|
+
);
|
|
945
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
|
|
946
|
+
return result;
|
|
947
|
+
} catch (error) {
|
|
948
|
+
throw this.handleError(error);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Rotate an application credential's secret. The new plaintext `secret` is
|
|
954
|
+
* returned exactly ONCE, along with audit fields: `rotatedFrom` (the previous
|
|
955
|
+
* credentialId) and `graceExpiresAt` (ISO string for the grace window during
|
|
956
|
+
* which the old credential is still honoured).
|
|
957
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
958
|
+
* @param credentialId - The credential's Mongo `_id`.
|
|
959
|
+
*/
|
|
960
|
+
async rotateAppCredential(
|
|
961
|
+
applicationId: string,
|
|
962
|
+
credentialId: string,
|
|
963
|
+
): Promise<RotateApplicationCredentialResult> {
|
|
964
|
+
try {
|
|
965
|
+
const result = await this.makeRequest<RotateApplicationCredentialResult>(
|
|
966
|
+
'POST',
|
|
967
|
+
`/applications/${encodeURIComponent(applicationId)}/credentials/${encodeURIComponent(credentialId)}/rotate`,
|
|
968
|
+
undefined,
|
|
969
|
+
{ cache: false },
|
|
970
|
+
);
|
|
971
|
+
// Rotation changes credential status/audit fields surfaced by the list.
|
|
972
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
|
|
973
|
+
return result;
|
|
974
|
+
} catch (error) {
|
|
975
|
+
throw this.handleError(error);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Revoke an application credential (`status='revoked'`). Revoked credentials
|
|
981
|
+
* can no longer authenticate.
|
|
982
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
983
|
+
* @param credentialId - The credential's Mongo `_id`.
|
|
984
|
+
*/
|
|
985
|
+
async revokeAppCredential(
|
|
986
|
+
applicationId: string,
|
|
987
|
+
credentialId: string,
|
|
988
|
+
): Promise<AccountSuccessResult> {
|
|
989
|
+
try {
|
|
990
|
+
const result = await this.makeRequest<AccountSuccessResult>(
|
|
991
|
+
'DELETE',
|
|
992
|
+
`/applications/${encodeURIComponent(applicationId)}/credentials/${encodeURIComponent(credentialId)}`,
|
|
993
|
+
undefined,
|
|
994
|
+
{ cache: false },
|
|
995
|
+
);
|
|
996
|
+
// Revocation flips the credential's status in the cached list.
|
|
997
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
|
|
998
|
+
return result;
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
throw this.handleError(error);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Fetch usage statistics for an application.
|
|
1006
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
1007
|
+
* @param period - Time window (defaults to the server default).
|
|
1008
|
+
*/
|
|
1009
|
+
async getAppUsage(
|
|
1010
|
+
applicationId: string,
|
|
1011
|
+
period?: ApplicationUsagePeriod,
|
|
1012
|
+
): Promise<ApplicationUsageStats> {
|
|
1013
|
+
try {
|
|
1014
|
+
return await this.makeRequest<ApplicationUsageStats>(
|
|
1015
|
+
'GET',
|
|
1016
|
+
`/applications/${encodeURIComponent(applicationId)}/usage`,
|
|
1017
|
+
period ? { period } : undefined,
|
|
1018
|
+
{ cache: true, cacheTTL: CACHE_TIMES.SHORT },
|
|
1019
|
+
);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
throw this.handleError(error);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// =========================================================================
|
|
1026
|
+
// Cache-invalidation helpers
|
|
1027
|
+
// =========================================================================
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Bust every cached account list. `listAccounts({tree?})` keys the flat list
|
|
1031
|
+
* as `GET:/accounts` and the tree variant as `GET:/accounts?tree=true` (the
|
|
1032
|
+
* query string is part of the URL path). A change to the accessible forest
|
|
1033
|
+
* (create/archive/ownership transfer) invalidates both, so we clear the
|
|
1034
|
+
* unscoped entry plus every `?`-query variant via a prefix sweep. The prefix
|
|
1035
|
+
* `GET:/accounts?` matches only the query-string list variants, never the
|
|
1036
|
+
* `GET:/accounts/<id>…` detail/sub-resource keys.
|
|
1037
|
+
*
|
|
1038
|
+
* Internal helper (leading underscore); not part of the supported public
|
|
1039
|
+
* surface. Public rather than `private` because mixins compose into an
|
|
1040
|
+
* exported anonymous class, where TypeScript cannot represent a private
|
|
1041
|
+
* member in the emitted declaration file (TS4094).
|
|
1042
|
+
*/
|
|
1043
|
+
_invalidateAccountLists(): void {
|
|
1044
|
+
this.clearCacheEntry('GET:/accounts');
|
|
1045
|
+
this.clearCacheByPrefix('GET:/accounts?');
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Bust the cached member list and detail for an account after a membership
|
|
1050
|
+
* mutation. The member list (`listAccountMembers`) and the detail
|
|
1051
|
+
* (`getAccount`, which can embed the caller's membership) both go stale when
|
|
1052
|
+
* the member set or a member's role changes.
|
|
1053
|
+
*
|
|
1054
|
+
* Internal helper (leading underscore); see `_invalidateAccountLists` for why
|
|
1055
|
+
* this is public rather than `private`.
|
|
1056
|
+
*/
|
|
1057
|
+
_invalidateAccountMembership(accountId: string): void {
|
|
1058
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/members`);
|
|
1059
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Bust every cached application list. `listAccountApps(accountId)` keys each
|
|
1064
|
+
* owner-scoped list as `GET:/applications?ownerAccountId=<id>` (the query
|
|
1065
|
+
* string is part of the URL path). A change to any list (create/delete)
|
|
1066
|
+
* invalidates them all, so we clear the unscoped entry plus every `?`-query
|
|
1067
|
+
* variant via a prefix sweep. The prefix `GET:/applications?` matches only the
|
|
1068
|
+
* query-string list variants, never the `GET:/applications/<id>…`
|
|
1069
|
+
* detail/sub-resource keys.
|
|
1070
|
+
*
|
|
1071
|
+
* Internal helper (leading underscore); see `_invalidateAccountLists` for why
|
|
1072
|
+
* this is public rather than `private`.
|
|
1073
|
+
*/
|
|
1074
|
+
_invalidateAppLists(): void {
|
|
1075
|
+
this.clearCacheEntry('GET:/applications');
|
|
1076
|
+
this.clearCacheByPrefix('GET:/applications?');
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
}
|