@oxyhq/core 3.18.0 → 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.
Files changed (42) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/OxyServices.js +3 -2
  3. package/dist/cjs/mixins/OxyServices.accounts.js +480 -0
  4. package/dist/cjs/mixins/OxyServices.connectedApps.js +73 -0
  5. package/dist/cjs/mixins/OxyServices.utility.js +3 -2
  6. package/dist/cjs/mixins/index.js +9 -6
  7. package/dist/esm/.tsbuildinfo +1 -1
  8. package/dist/esm/OxyServices.js +3 -2
  9. package/dist/esm/mixins/OxyServices.accounts.js +477 -0
  10. package/dist/esm/mixins/OxyServices.connectedApps.js +70 -0
  11. package/dist/esm/mixins/OxyServices.utility.js +3 -2
  12. package/dist/esm/mixins/index.js +9 -6
  13. package/dist/types/.tsbuildinfo +1 -1
  14. package/dist/types/OxyServices.d.ts +3 -2
  15. package/dist/types/index.d.ts +2 -3
  16. package/dist/types/mixins/OxyServices.accounts.d.ts +642 -0
  17. package/dist/types/mixins/OxyServices.auth.d.ts +1 -1
  18. package/dist/types/mixins/OxyServices.connectedApps.d.ts +168 -0
  19. package/dist/types/mixins/OxyServices.utility.d.ts +6 -3
  20. package/dist/types/mixins/index.d.ts +3 -4
  21. package/package.json +2 -2
  22. package/src/OxyServices.ts +3 -2
  23. package/src/index.ts +33 -34
  24. package/src/mixins/OxyServices.accounts.ts +1079 -0
  25. package/src/mixins/OxyServices.auth.ts +1 -1
  26. package/src/mixins/OxyServices.connectedApps.ts +165 -0
  27. package/src/mixins/OxyServices.utility.ts +7 -4
  28. package/src/mixins/__tests__/accounts.test.ts +667 -0
  29. package/src/mixins/__tests__/connectedApps.test.ts +1 -1
  30. package/src/mixins/index.ts +11 -9
  31. package/dist/cjs/mixins/OxyServices.applications.js +0 -350
  32. package/dist/cjs/mixins/OxyServices.managedAccounts.js +0 -143
  33. package/dist/cjs/mixins/OxyServices.workspaces.js +0 -181
  34. package/dist/esm/mixins/OxyServices.applications.js +0 -347
  35. package/dist/esm/mixins/OxyServices.managedAccounts.js +0 -140
  36. package/dist/esm/mixins/OxyServices.workspaces.js +0 -178
  37. package/dist/types/mixins/OxyServices.applications.d.ts +0 -496
  38. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +0 -145
  39. package/dist/types/mixins/OxyServices.workspaces.d.ts +0 -219
  40. package/src/mixins/OxyServices.applications.ts +0 -773
  41. package/src/mixins/OxyServices.managedAccounts.ts +0 -173
  42. package/src/mixins/OxyServices.workspaces.ts +0 -351
@@ -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
- * - **Applications**: Application, membership, and credential management
24
- * - **Workspaces**: Workspace and membership management
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 a managed account.
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', '/managed-accounts/verify', { accountId, userId }, { cache: false, retry: false, timeout: 5000 });
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,
@@ -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 { OxyServicesApplicationsMixin } from './OxyServices.applications.js';
21
- import { OxyServicesWorkspacesMixin } from './OxyServices.workspaces.js';
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
- OxyServicesApplicationsMixin,
70
- OxyServicesWorkspacesMixin,
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)