@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
package/dist/esm/OxyServices.js
CHANGED
|
@@ -20,8 +20,9 @@ import { composeOxyServices } from './mixins/index.js';
|
|
|
20
20
|
* - **Payment**: Payment processing
|
|
21
21
|
* - **Reputation**: Reputation system (Oxy Trust)
|
|
22
22
|
* - **Assets**: File upload and asset management
|
|
23
|
-
* - **
|
|
24
|
-
*
|
|
23
|
+
* - **Accounts**: Unified account graph (tree, members, roles, bot credentials)
|
|
24
|
+
* and the applications owned within it (Application = OAuth client)
|
|
25
|
+
* - **Connected apps**: OAuth-consent surface (public app identity, grants)
|
|
25
26
|
* - **Location**: Location-based features
|
|
26
27
|
* - **Analytics**: Analytics tracking
|
|
27
28
|
* - **Devices**: Device management
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { CACHE_TIMES } from './mixinHelpers.js';
|
|
2
|
+
export function OxyServicesAccountsMixin(Base) {
|
|
3
|
+
return class extends Base {
|
|
4
|
+
constructor(...args) {
|
|
5
|
+
super(...args);
|
|
6
|
+
}
|
|
7
|
+
// =========================================================================
|
|
8
|
+
// Accounts
|
|
9
|
+
// =========================================================================
|
|
10
|
+
/**
|
|
11
|
+
* List the accounts the caller can access: their own personal (root)
|
|
12
|
+
* account, accounts they own, and accounts shared with them (including
|
|
13
|
+
* external organisations), plus the reachable subtree of each.
|
|
14
|
+
*
|
|
15
|
+
* @param opts - `{ tree: true }` requests the nested tree representation
|
|
16
|
+
* (`children` populated) instead of a flat list. The flag is appended to
|
|
17
|
+
* the path as `?tree=true`, so the response cache keys on it automatically —
|
|
18
|
+
* the flat and tree variants never collide.
|
|
19
|
+
*/
|
|
20
|
+
async listAccounts(opts) {
|
|
21
|
+
try {
|
|
22
|
+
const path = opts?.tree ? '/accounts?tree=true' : '/accounts';
|
|
23
|
+
const res = await this.makeRequest('GET', path, undefined, { cache: true, cacheTTL: CACHE_TIMES.MEDIUM });
|
|
24
|
+
return res.accounts ?? [];
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw this.handleError(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch a single account node by id.
|
|
32
|
+
* @param accountId - The account's Mongo `_id`.
|
|
33
|
+
*/
|
|
34
|
+
async getAccount(accountId) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await this.makeRequest('GET', `/accounts/${encodeURIComponent(accountId)}`, undefined, { cache: true, cacheTTL: CACHE_TIMES.LONG });
|
|
37
|
+
return res.account;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw this.handleError(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a new (non-personal) account. The caller becomes its `owner`.
|
|
45
|
+
* @param data - Account configuration: kind, optional parent, and profile.
|
|
46
|
+
*/
|
|
47
|
+
async createAccount(data) {
|
|
48
|
+
try {
|
|
49
|
+
const res = await this.makeRequest('POST', '/accounts', data, { cache: false });
|
|
50
|
+
// A new account changes the accessible forest — bust every cached list
|
|
51
|
+
// (flat + tree) so it appears on the next `listAccounts()` read.
|
|
52
|
+
this._invalidateAccountLists();
|
|
53
|
+
return res.account;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw this.handleError(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Update an account's mutable profile fields. Tree placement changes
|
|
61
|
+
* (reparenting) go through the dedicated move endpoint, not here.
|
|
62
|
+
* @param accountId - The account's Mongo `_id`.
|
|
63
|
+
* @param data - Subset of updatable profile fields.
|
|
64
|
+
*/
|
|
65
|
+
async updateAccount(accountId, data) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await this.makeRequest('PATCH', `/accounts/${encodeURIComponent(accountId)}`, data, { cache: false });
|
|
68
|
+
// Bust the cached detail and every list (which embeds account profile
|
|
69
|
+
// data) so neither serves the pre-update snapshot.
|
|
70
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
71
|
+
this._invalidateAccountLists();
|
|
72
|
+
return res.account;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw this.handleError(error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Archive an account (soft delete). Named `archiveAccount` — NOT
|
|
80
|
+
* `deleteAccount`, which is reserved for the GDPR self-deletion flow on the
|
|
81
|
+
* user mixin (`OxyServices.user.ts`).
|
|
82
|
+
* @param accountId - The account's Mongo `_id`.
|
|
83
|
+
*/
|
|
84
|
+
async archiveAccount(accountId) {
|
|
85
|
+
try {
|
|
86
|
+
const result = await this.makeRequest('DELETE', `/accounts/${encodeURIComponent(accountId)}`, undefined, { cache: false });
|
|
87
|
+
// Bust every cached representation of the archived account.
|
|
88
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
89
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/members`);
|
|
90
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
|
|
91
|
+
this._invalidateAccountLists();
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw this.handleError(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* List the direct child accounts of an account.
|
|
100
|
+
* @param accountId - The parent account's Mongo `_id`.
|
|
101
|
+
*/
|
|
102
|
+
async listChildAccounts(accountId) {
|
|
103
|
+
try {
|
|
104
|
+
const res = await this.makeRequest('GET', `/accounts/${encodeURIComponent(accountId)}/children`, undefined, { cache: true, cacheTTL: CACHE_TIMES.MEDIUM });
|
|
105
|
+
return res.accounts ?? [];
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw this.handleError(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// =========================================================================
|
|
112
|
+
// Account members
|
|
113
|
+
// =========================================================================
|
|
114
|
+
/**
|
|
115
|
+
* List members of an account (direct membership rows on the account).
|
|
116
|
+
* @param accountId - The account's Mongo `_id`.
|
|
117
|
+
*/
|
|
118
|
+
async listAccountMembers(accountId) {
|
|
119
|
+
try {
|
|
120
|
+
const res = await this.makeRequest('GET', `/accounts/${encodeURIComponent(accountId)}/members`, undefined, { cache: true, cacheTTL: CACHE_TIMES.MEDIUM });
|
|
121
|
+
return res.members ?? [];
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw this.handleError(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Add a member to an account.
|
|
129
|
+
* @param accountId - The account's Mongo `_id`.
|
|
130
|
+
* @param data - Target user's username or email and role (never `owner`).
|
|
131
|
+
* The server resolves `usernameOrEmail` to a personal account; an unknown
|
|
132
|
+
* value yields a 404 "User not found".
|
|
133
|
+
*/
|
|
134
|
+
async inviteAccountMember(accountId, data) {
|
|
135
|
+
try {
|
|
136
|
+
const res = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/members`, data, { cache: false });
|
|
137
|
+
this._invalidateAccountMembership(accountId);
|
|
138
|
+
return res.member;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
throw this.handleError(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Change a member's role.
|
|
146
|
+
* @param accountId - The account's Mongo `_id`.
|
|
147
|
+
* @param memberId - The member's Mongo `_id`.
|
|
148
|
+
* @param data - New role (never `owner`).
|
|
149
|
+
*/
|
|
150
|
+
async updateAccountMember(accountId, memberId, data) {
|
|
151
|
+
try {
|
|
152
|
+
const res = await this.makeRequest('PATCH', `/accounts/${encodeURIComponent(accountId)}/members/${encodeURIComponent(memberId)}`, data, { cache: false });
|
|
153
|
+
this._invalidateAccountMembership(accountId);
|
|
154
|
+
return res.member;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw this.handleError(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Remove a member from an account.
|
|
162
|
+
* @param accountId - The account's Mongo `_id`.
|
|
163
|
+
* @param memberId - The member's Mongo `_id`.
|
|
164
|
+
*/
|
|
165
|
+
async removeAccountMember(accountId, memberId) {
|
|
166
|
+
try {
|
|
167
|
+
const result = await this.makeRequest('DELETE', `/accounts/${encodeURIComponent(accountId)}/members/${encodeURIComponent(memberId)}`, undefined, { cache: false });
|
|
168
|
+
this._invalidateAccountMembership(accountId);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
throw this.handleError(error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Transfer ownership of an account to another member (owner only).
|
|
177
|
+
* @param accountId - The account's Mongo `_id`.
|
|
178
|
+
* @param data - Target user id.
|
|
179
|
+
*/
|
|
180
|
+
async transferAccountOwnership(accountId, data) {
|
|
181
|
+
try {
|
|
182
|
+
const result = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/transfer-ownership`, data, { cache: false });
|
|
183
|
+
// Ownership change alters roles in the member list AND the detail, and
|
|
184
|
+
// can change which accounts the caller "owns" in the list view.
|
|
185
|
+
this._invalidateAccountMembership(accountId);
|
|
186
|
+
this._invalidateAccountLists();
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
throw this.handleError(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// =========================================================================
|
|
194
|
+
// Bot (account) service credentials — /accounts/:id/credentials
|
|
195
|
+
// =========================================================================
|
|
196
|
+
/**
|
|
197
|
+
* List a bot account's service credentials. The response NEVER includes
|
|
198
|
+
* secrets.
|
|
199
|
+
* @param accountId - The account's Mongo `_id`.
|
|
200
|
+
*/
|
|
201
|
+
async listAccountCredentials(accountId) {
|
|
202
|
+
try {
|
|
203
|
+
const res = await this.makeRequest('GET', `/accounts/${encodeURIComponent(accountId)}/credentials`, undefined, { cache: true, cacheTTL: CACHE_TIMES.MEDIUM });
|
|
204
|
+
return res.credentials ?? [];
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
throw this.handleError(error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Create a service credential for a bot account. The plaintext `secret` is
|
|
212
|
+
* returned exactly ONCE; the server stores only a hash and will never return
|
|
213
|
+
* it again.
|
|
214
|
+
* @param accountId - The account's Mongo `_id`.
|
|
215
|
+
* @param data - Credential configuration (`type` is always `service`).
|
|
216
|
+
*/
|
|
217
|
+
async createAccountCredential(accountId, data) {
|
|
218
|
+
try {
|
|
219
|
+
const result = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/credentials`, data, { cache: false });
|
|
220
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
throw this.handleError(error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Rotate a bot credential's secret. The new plaintext `secret` is returned
|
|
229
|
+
* exactly ONCE, along with audit fields: `rotatedFrom` (the previous
|
|
230
|
+
* credentialId) and `graceExpiresAt` (ISO string for the grace window during
|
|
231
|
+
* which the old credential is still honoured).
|
|
232
|
+
* @param accountId - The account's Mongo `_id`.
|
|
233
|
+
* @param credentialId - The credential's Mongo `_id`.
|
|
234
|
+
*/
|
|
235
|
+
async rotateAccountCredential(accountId, credentialId) {
|
|
236
|
+
try {
|
|
237
|
+
const result = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/credentials/${encodeURIComponent(credentialId)}/rotate`, undefined, { cache: false });
|
|
238
|
+
// Rotation changes credential status/audit fields surfaced by the list.
|
|
239
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
throw this.handleError(error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Revoke a bot credential (`status='revoked'`). Revoked credentials can no
|
|
248
|
+
* longer authenticate.
|
|
249
|
+
* @param accountId - The account's Mongo `_id`.
|
|
250
|
+
* @param credentialId - The credential's Mongo `_id`.
|
|
251
|
+
*/
|
|
252
|
+
async revokeAccountCredential(accountId, credentialId) {
|
|
253
|
+
try {
|
|
254
|
+
const result = await this.makeRequest('DELETE', `/accounts/${encodeURIComponent(accountId)}/credentials/${encodeURIComponent(credentialId)}`, undefined, { cache: false });
|
|
255
|
+
// Revocation flips the credential's status in the cached list.
|
|
256
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
throw this.handleError(error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// =========================================================================
|
|
264
|
+
// Applications owned by an account — /applications
|
|
265
|
+
// =========================================================================
|
|
266
|
+
/**
|
|
267
|
+
* List the applications owned by an account. Backed by
|
|
268
|
+
* `GET /applications?ownerAccountId=<id>`.
|
|
269
|
+
* @param accountId - The owning account's Mongo `_id`.
|
|
270
|
+
*/
|
|
271
|
+
async listAccountApps(accountId) {
|
|
272
|
+
try {
|
|
273
|
+
const res = await this.makeRequest('GET', `/applications?ownerAccountId=${encodeURIComponent(accountId)}`, undefined, { cache: true, cacheTTL: CACHE_TIMES.MEDIUM });
|
|
274
|
+
return res.applications ?? [];
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
throw this.handleError(error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Create a new application owned by an account.
|
|
282
|
+
* @param data - Application configuration. `ownerAccountId` defaults to the
|
|
283
|
+
* caller's personal account when omitted. Staff-only fields are ignored.
|
|
284
|
+
*/
|
|
285
|
+
async createApp(data) {
|
|
286
|
+
try {
|
|
287
|
+
const res = await this.makeRequest('POST', '/applications', data, { cache: false });
|
|
288
|
+
// Bust every cached application list (per owning account) so the new app
|
|
289
|
+
// appears on the next `listAccountApps()` read.
|
|
290
|
+
this._invalidateAppLists();
|
|
291
|
+
return res.application;
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
throw this.handleError(error);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Fetch a single application by id.
|
|
299
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
300
|
+
*/
|
|
301
|
+
async getApp(applicationId) {
|
|
302
|
+
try {
|
|
303
|
+
const res = await this.makeRequest('GET', `/applications/${encodeURIComponent(applicationId)}`, undefined, { cache: true, cacheTTL: CACHE_TIMES.LONG });
|
|
304
|
+
return res.application;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
throw this.handleError(error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Update an application's mutable fields.
|
|
312
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
313
|
+
* @param data - Subset of updatable fields. Staff-only fields are ignored.
|
|
314
|
+
*/
|
|
315
|
+
async updateApp(applicationId, data) {
|
|
316
|
+
try {
|
|
317
|
+
const res = await this.makeRequest('PATCH', `/applications/${encodeURIComponent(applicationId)}`, data, { cache: false });
|
|
318
|
+
// Bust the cached detail and every list (which embeds application fields).
|
|
319
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}`);
|
|
320
|
+
this._invalidateAppLists();
|
|
321
|
+
return res.application;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
throw this.handleError(error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Soft-delete an application.
|
|
329
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
330
|
+
*/
|
|
331
|
+
async deleteApp(applicationId) {
|
|
332
|
+
try {
|
|
333
|
+
const result = await this.makeRequest('DELETE', `/applications/${encodeURIComponent(applicationId)}`, undefined, { cache: false });
|
|
334
|
+
// Bust every cached representation of the deleted application.
|
|
335
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}`);
|
|
336
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
|
|
337
|
+
this._invalidateAppLists();
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
throw this.handleError(error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// =========================================================================
|
|
345
|
+
// Application OAuth credentials — /applications/:appId/credentials
|
|
346
|
+
// =========================================================================
|
|
347
|
+
/**
|
|
348
|
+
* List an application's OAuth credentials. The response NEVER includes
|
|
349
|
+
* secrets.
|
|
350
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
351
|
+
*/
|
|
352
|
+
async listAppCredentials(applicationId) {
|
|
353
|
+
try {
|
|
354
|
+
const res = await this.makeRequest('GET', `/applications/${encodeURIComponent(applicationId)}/credentials`, undefined, { cache: true, cacheTTL: CACHE_TIMES.MEDIUM });
|
|
355
|
+
return res.credentials ?? [];
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
throw this.handleError(error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Create an application credential. The plaintext `secret` is returned
|
|
363
|
+
* exactly ONCE; the server stores only a hash and will never return it again.
|
|
364
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
365
|
+
* @param data - Credential configuration.
|
|
366
|
+
*/
|
|
367
|
+
async createAppCredential(applicationId, data) {
|
|
368
|
+
try {
|
|
369
|
+
const result = await this.makeRequest('POST', `/applications/${encodeURIComponent(applicationId)}/credentials`, data, { cache: false });
|
|
370
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
throw this.handleError(error);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Rotate an application credential's secret. The new plaintext `secret` is
|
|
379
|
+
* returned exactly ONCE, along with audit fields: `rotatedFrom` (the previous
|
|
380
|
+
* credentialId) and `graceExpiresAt` (ISO string for the grace window during
|
|
381
|
+
* which the old credential is still honoured).
|
|
382
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
383
|
+
* @param credentialId - The credential's Mongo `_id`.
|
|
384
|
+
*/
|
|
385
|
+
async rotateAppCredential(applicationId, credentialId) {
|
|
386
|
+
try {
|
|
387
|
+
const result = await this.makeRequest('POST', `/applications/${encodeURIComponent(applicationId)}/credentials/${encodeURIComponent(credentialId)}/rotate`, undefined, { cache: false });
|
|
388
|
+
// Rotation changes credential status/audit fields surfaced by the list.
|
|
389
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
throw this.handleError(error);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Revoke an application credential (`status='revoked'`). Revoked credentials
|
|
398
|
+
* can no longer authenticate.
|
|
399
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
400
|
+
* @param credentialId - The credential's Mongo `_id`.
|
|
401
|
+
*/
|
|
402
|
+
async revokeAppCredential(applicationId, credentialId) {
|
|
403
|
+
try {
|
|
404
|
+
const result = await this.makeRequest('DELETE', `/applications/${encodeURIComponent(applicationId)}/credentials/${encodeURIComponent(credentialId)}`, undefined, { cache: false });
|
|
405
|
+
// Revocation flips the credential's status in the cached list.
|
|
406
|
+
this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
throw this.handleError(error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Fetch usage statistics for an application.
|
|
415
|
+
* @param applicationId - The application's Mongo `_id`.
|
|
416
|
+
* @param period - Time window (defaults to the server default).
|
|
417
|
+
*/
|
|
418
|
+
async getAppUsage(applicationId, period) {
|
|
419
|
+
try {
|
|
420
|
+
return await this.makeRequest('GET', `/applications/${encodeURIComponent(applicationId)}/usage`, period ? { period } : undefined, { cache: true, cacheTTL: CACHE_TIMES.SHORT });
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
throw this.handleError(error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// =========================================================================
|
|
427
|
+
// Cache-invalidation helpers
|
|
428
|
+
// =========================================================================
|
|
429
|
+
/**
|
|
430
|
+
* Bust every cached account list. `listAccounts({tree?})` keys the flat list
|
|
431
|
+
* as `GET:/accounts` and the tree variant as `GET:/accounts?tree=true` (the
|
|
432
|
+
* query string is part of the URL path). A change to the accessible forest
|
|
433
|
+
* (create/archive/ownership transfer) invalidates both, so we clear the
|
|
434
|
+
* unscoped entry plus every `?`-query variant via a prefix sweep. The prefix
|
|
435
|
+
* `GET:/accounts?` matches only the query-string list variants, never the
|
|
436
|
+
* `GET:/accounts/<id>…` detail/sub-resource keys.
|
|
437
|
+
*
|
|
438
|
+
* Internal helper (leading underscore); not part of the supported public
|
|
439
|
+
* surface. Public rather than `private` because mixins compose into an
|
|
440
|
+
* exported anonymous class, where TypeScript cannot represent a private
|
|
441
|
+
* member in the emitted declaration file (TS4094).
|
|
442
|
+
*/
|
|
443
|
+
_invalidateAccountLists() {
|
|
444
|
+
this.clearCacheEntry('GET:/accounts');
|
|
445
|
+
this.clearCacheByPrefix('GET:/accounts?');
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Bust the cached member list and detail for an account after a membership
|
|
449
|
+
* mutation. The member list (`listAccountMembers`) and the detail
|
|
450
|
+
* (`getAccount`, which can embed the caller's membership) both go stale when
|
|
451
|
+
* the member set or a member's role changes.
|
|
452
|
+
*
|
|
453
|
+
* Internal helper (leading underscore); see `_invalidateAccountLists` for why
|
|
454
|
+
* this is public rather than `private`.
|
|
455
|
+
*/
|
|
456
|
+
_invalidateAccountMembership(accountId) {
|
|
457
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/members`);
|
|
458
|
+
this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}`);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Bust every cached application list. `listAccountApps(accountId)` keys each
|
|
462
|
+
* owner-scoped list as `GET:/applications?ownerAccountId=<id>` (the query
|
|
463
|
+
* string is part of the URL path). A change to any list (create/delete)
|
|
464
|
+
* invalidates them all, so we clear the unscoped entry plus every `?`-query
|
|
465
|
+
* variant via a prefix sweep. The prefix `GET:/applications?` matches only the
|
|
466
|
+
* query-string list variants, never the `GET:/applications/<id>…`
|
|
467
|
+
* detail/sub-resource keys.
|
|
468
|
+
*
|
|
469
|
+
* Internal helper (leading underscore); see `_invalidateAccountLists` for why
|
|
470
|
+
* this is public rather than `private`.
|
|
471
|
+
*/
|
|
472
|
+
_invalidateAppLists() {
|
|
473
|
+
this.clearCacheEntry('GET:/applications');
|
|
474
|
+
this.clearCacheByPrefix('GET:/applications?');
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CACHE_TIMES } from './mixinHelpers.js';
|
|
2
|
+
export function OxyServicesConnectedAppsMixin(Base) {
|
|
3
|
+
return class extends Base {
|
|
4
|
+
constructor(...args) {
|
|
5
|
+
super(...args);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolve an OAuth client identifier to the owning application's PUBLIC
|
|
9
|
+
* identity. No authentication required — the API returns only sanitized,
|
|
10
|
+
* display-safe metadata ({@link PublicApplication}). Use this to render the
|
|
11
|
+
* requesting application's name/icon in consent, authorize, and device-flow
|
|
12
|
+
* approval UIs before any session exists.
|
|
13
|
+
*
|
|
14
|
+
* @param clientId - The OAuth `client_id` (an active credential's public
|
|
15
|
+
* key). URL-encoded before being placed in the path.
|
|
16
|
+
*/
|
|
17
|
+
async getPublicApplication(clientId) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await this.makeRequest('GET', `/auth/oauth/client/${encodeURIComponent(clientId)}`, undefined, { cache: true, cacheTTL: CACHE_TIMES.MEDIUM });
|
|
20
|
+
return res.application;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw this.handleError(error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* List the OAuth-authorized applications the current user has connected —
|
|
28
|
+
* the third-party apps the user granted access to via the consent flow.
|
|
29
|
+
* Each entry is a {@link ConnectedApp} carrying the application's display
|
|
30
|
+
* identity, the granted scopes, and when the grant was first made and last
|
|
31
|
+
* exercised. Requires an authenticated session.
|
|
32
|
+
*
|
|
33
|
+
* Backed by `GET /auth/grants`. The response is briefly cached
|
|
34
|
+
* (identity-scoped); {@link revokeAppGrant} busts that cache so a revoke is
|
|
35
|
+
* reflected on the next read.
|
|
36
|
+
*/
|
|
37
|
+
async listConnectedApps() {
|
|
38
|
+
try {
|
|
39
|
+
return await this.makeRequest('GET', '/auth/grants', undefined, { cache: true, cacheTTL: CACHE_TIMES.SHORT });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw this.handleError(error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Revoke the current user's grant for a connected application, identified by
|
|
47
|
+
* its application `_id` (a {@link ConnectedApp.applicationId}, NOT a
|
|
48
|
+
* credential/client id — keyed by application so the revocation survives
|
|
49
|
+
* credential rotation). After this the application can no longer act on the
|
|
50
|
+
* user's behalf until it is re-authorized.
|
|
51
|
+
*
|
|
52
|
+
* Backed by `DELETE /auth/grants/:applicationId`. On success the cached
|
|
53
|
+
* connected-apps list (`GET:/auth/grants`) is invalidated so the next
|
|
54
|
+
* {@link listConnectedApps} read reflects the removal.
|
|
55
|
+
*
|
|
56
|
+
* @param applicationId - The connected application's Mongo `_id`.
|
|
57
|
+
*/
|
|
58
|
+
async revokeAppGrant(applicationId) {
|
|
59
|
+
try {
|
|
60
|
+
await this.makeRequest('DELETE', `/auth/grants/${applicationId}`, undefined, { cache: false });
|
|
61
|
+
// A revoke removes an entry from the user's connected-apps list; bust
|
|
62
|
+
// the cached `GET /auth/grants` so the next read re-fetches.
|
|
63
|
+
this.clearCacheEntry('GET:/auth/grants');
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw this.handleError(error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -59,7 +59,8 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
59
59
|
this._serviceActingAsCache = new Map();
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
* Verify that a user is authorized to act as
|
|
62
|
+
* Verify that a user is authorized to act as an account (direct membership
|
|
63
|
+
* or inherited via an ancestor). Backed by `GET /accounts/verify-acting-as`.
|
|
63
64
|
* Results are cached in-memory for 5 minutes to avoid repeated API calls.
|
|
64
65
|
*
|
|
65
66
|
* @internal Used by the auth() middleware — not part of the public API
|
|
@@ -74,7 +75,7 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
74
75
|
}
|
|
75
76
|
// Query the API
|
|
76
77
|
try {
|
|
77
|
-
const result = await this.makeRequest('GET', '/
|
|
78
|
+
const result = await this.makeRequest('GET', '/accounts/verify-acting-as', { accountId, userId }, { cache: false, retry: false, timeout: 5000 });
|
|
78
79
|
// Cache successful result for 5 minutes
|
|
79
80
|
this._actingAsCache.set(cacheKey, {
|
|
80
81
|
result: result && result.authorized ? result : null,
|
package/dist/esm/mixins/index.js
CHANGED
|
@@ -17,8 +17,8 @@ import { OxyServicesLanguageMixin } from './OxyServices.language.js';
|
|
|
17
17
|
import { OxyServicesPaymentMixin } from './OxyServices.payment.js';
|
|
18
18
|
import { OxyServicesReputationMixin } from './OxyServices.reputation.js';
|
|
19
19
|
import { OxyServicesAssetsMixin } from './OxyServices.assets.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { OxyServicesAccountsMixin } from './OxyServices.accounts.js';
|
|
21
|
+
import { OxyServicesConnectedAppsMixin } from './OxyServices.connectedApps.js';
|
|
22
22
|
import { OxyServicesLocationMixin } from './OxyServices.location.js';
|
|
23
23
|
import { OxyServicesAnalyticsMixin } from './OxyServices.analytics.js';
|
|
24
24
|
import { OxyServicesDevicesMixin } from './OxyServices.devices.js';
|
|
@@ -26,7 +26,6 @@ import { OxyServicesSecurityMixin } from './OxyServices.security.js';
|
|
|
26
26
|
import { OxyServicesUtilityMixin } from './OxyServices.utility.js';
|
|
27
27
|
import { OxyServicesFeaturesMixin } from './OxyServices.features.js';
|
|
28
28
|
import { OxyServicesTopicsMixin } from './OxyServices.topics.js';
|
|
29
|
-
import { OxyServicesManagedAccountsMixin } from './OxyServices.managedAccounts.js';
|
|
30
29
|
import { OxyServicesContactsMixin } from './OxyServices.contacts.js';
|
|
31
30
|
import { OxyServicesAppDataMixin } from './OxyServices.appData.js';
|
|
32
31
|
import { OxyServicesCivicMixin } from './OxyServices.civic.js';
|
|
@@ -66,15 +65,19 @@ const MIXIN_PIPELINE = [
|
|
|
66
65
|
OxyServicesPaymentMixin,
|
|
67
66
|
OxyServicesReputationMixin,
|
|
68
67
|
OxyServicesAssetsMixin,
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// Unified account graph + the applications owned within it. The clean-cut
|
|
69
|
+
// replacement for the former managedAccounts + workspaces + applications
|
|
70
|
+
// (account-management) mixins.
|
|
71
|
+
OxyServicesAccountsMixin,
|
|
72
|
+
// OAuth-consent surface (public app identity + connected-app grants). Kept
|
|
73
|
+
// separate from account ownership.
|
|
74
|
+
OxyServicesConnectedAppsMixin,
|
|
71
75
|
OxyServicesLocationMixin,
|
|
72
76
|
OxyServicesAnalyticsMixin,
|
|
73
77
|
OxyServicesDevicesMixin,
|
|
74
78
|
OxyServicesSecurityMixin,
|
|
75
79
|
OxyServicesFeaturesMixin,
|
|
76
80
|
OxyServicesTopicsMixin,
|
|
77
|
-
OxyServicesManagedAccountsMixin,
|
|
78
81
|
OxyServicesContactsMixin,
|
|
79
82
|
OxyServicesAppDataMixin,
|
|
80
83
|
// Civic / Commons "Oxy ID" (public signed cards, Oxy ID QR payload)
|