@oxyhq/core 3.1.0 → 3.2.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.
@@ -58,12 +58,22 @@ export interface Application {
58
58
  webhookUrl?: string;
59
59
  devWebhookUrl?: string;
60
60
  createdByUserId: string;
61
+ /**
62
+ * The workspace this application belongs to (workspace `_id`), or `null` for
63
+ * applications not owned by a workspace. Used by the console to scope apps to
64
+ * a workspace and to branch on workspace-derived access.
65
+ */
66
+ workspaceId: string | null;
61
67
  createdAt: string;
62
68
  updatedAt: string;
63
69
  /**
64
70
  * The calling user's own membership in this application, embedded by the API
65
71
  * on list (`GET /applications`) and detail (`GET /applications/:appId`)
66
72
  * responses. Use `callerMembership.permissions` to gate UI affordances.
73
+ *
74
+ * When the caller's access is derived from a workspace membership rather than
75
+ * a direct application membership, the API returns a synthetic membership
76
+ * with `source: 'workspace'` and `_id: null`.
67
77
  */
68
78
  callerMembership?: ApplicationMember;
69
79
  }
@@ -73,7 +83,11 @@ export interface Application {
73
83
  * on the server at write time.
74
84
  */
75
85
  export interface ApplicationMember {
76
- _id: string;
86
+ /**
87
+ * The membership's Mongo `_id`. `null` for a synthetic, workspace-derived
88
+ * membership (see {@link Application.callerMembership} and `source`).
89
+ */
90
+ _id: string | null;
77
91
  applicationId: string;
78
92
  userId: string;
79
93
  role: ApplicationRole;
@@ -81,6 +95,13 @@ export interface ApplicationMember {
81
95
  invitedByUserId?: string;
82
96
  joinedAt?: string;
83
97
  status: ApplicationMemberStatus;
98
+ /**
99
+ * Origin of this membership. When `'workspace'`, the membership is synthetic
100
+ * and derived from the caller's workspace membership rather than a direct
101
+ * application membership (in which case `_id` is `null`). Absent or any other
102
+ * value indicates a direct application membership.
103
+ */
104
+ source?: 'workspace';
84
105
  createdAt: string;
85
106
  updatedAt: string;
86
107
  }
@@ -110,6 +131,39 @@ export interface ApplicationCredential {
110
131
  updatedAt: string;
111
132
  }
112
133
 
134
+ /**
135
+ * Sanitized, PUBLIC application identity returned by the API when resolving a
136
+ * cross-app/OAuth client to a registered {@link Application}.
137
+ *
138
+ * Unlike {@link Application}, this shape carries NO sensitive or membership
139
+ * fields — it is safe to display unauthenticated in consent/authorize screens
140
+ * and device-flow approval UIs. The API resolves a `client_id` (OAuth
141
+ * credential public key) to the owning application and projects only the
142
+ * fields below. `id` is the application's `_id` as a string.
143
+ */
144
+ export interface PublicApplication {
145
+ /** The application's Mongo `_id` as a string. */
146
+ id: string;
147
+ /** Human-readable application name shown to the user. */
148
+ name: string;
149
+ /** Optional short description of what the application does. */
150
+ description?: string;
151
+ /** Optional icon URL for the application. */
152
+ icon?: string;
153
+ /** Optional public website/homepage URL for the application. */
154
+ websiteUrl?: string;
155
+ /** Application classification (set by Oxy platform staff). */
156
+ type: ApplicationType;
157
+ /** Whether the application is an officially endorsed Oxy application. */
158
+ isOfficial: boolean;
159
+ /** Whether the application is an internal Oxy ecosystem application. */
160
+ isInternal: boolean;
161
+ /** OAuth scopes the application is configured to request. */
162
+ scopes: string[];
163
+ /** Optional display name of the developer/owner organisation. */
164
+ developerName?: string;
165
+ }
166
+
113
167
  /** Input accepted by `createApplication`. Staff-only fields are not settable here. */
114
168
  export interface CreateApplicationInput {
115
169
  name: string;
@@ -118,6 +172,11 @@ export interface CreateApplicationInput {
118
172
  icon?: string;
119
173
  redirectUris?: string[];
120
174
  scopes?: string[];
175
+ /**
176
+ * Optional workspace `_id` to create the app in. Omitted → API defaults to
177
+ * the caller's personal workspace.
178
+ */
179
+ workspaceId?: string;
121
180
  }
122
181
 
123
182
  /** Input accepted by `updateApplication`. Staff-only fields are not settable here. */
@@ -222,14 +281,47 @@ export function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(B
222
281
  super(...(args as [any]));
223
282
  }
224
283
 
284
+ /**
285
+ * Resolve an OAuth client identifier to the owning application's PUBLIC
286
+ * identity. No authentication required — the API returns only sanitized,
287
+ * display-safe metadata ({@link PublicApplication}). Use this to render the
288
+ * requesting application's name/icon in consent, authorize, and device-flow
289
+ * approval UIs before any session exists.
290
+ *
291
+ * @param clientId - The OAuth `client_id` (an active credential's public
292
+ * key). URL-encoded before being placed in the path.
293
+ */
294
+ async getPublicApplication(clientId: string): Promise<PublicApplication> {
295
+ try {
296
+ const res = await this.makeRequest<{ application: PublicApplication }>(
297
+ 'GET',
298
+ `/auth/oauth/client/${encodeURIComponent(clientId)}`,
299
+ undefined,
300
+ { cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
301
+ );
302
+ return res.application;
303
+ } catch (error) {
304
+ throw this.handleError(error);
305
+ }
306
+ }
307
+
225
308
  /**
226
309
  * List applications the current user is an active member of.
310
+ *
311
+ * @param workspaceId - Optional workspace `_id` to scope the listing to
312
+ * applications belonging to that workspace. When provided it is appended
313
+ * as a `workspaceId` query parameter (URL-encoded). The query string is
314
+ * part of the request path, so the response cache keys on it
315
+ * automatically — scoped and unscoped lists never collide.
227
316
  */
228
- async getApplications(): Promise<Application[]> {
317
+ async getApplications(workspaceId?: string): Promise<Application[]> {
229
318
  try {
319
+ const path = workspaceId
320
+ ? `/applications?workspaceId=${encodeURIComponent(workspaceId)}`
321
+ : '/applications';
230
322
  const res = await this.makeRequest<{ applications?: Application[] }>(
231
323
  'GET',
232
- '/applications',
324
+ path,
233
325
  undefined,
234
326
  { cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
235
327
  );
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Workspaces Methods Mixin
3
+ *
4
+ * Provides methods for managing Oxy workspaces and their members via the
5
+ * `/workspaces` API. A workspace is a multi-user container that owns
6
+ * applications and other resources: membership (with a role) grants
7
+ * permissions. A `personal` workspace is created implicitly for every user;
8
+ * `team` workspaces are created explicitly and can invite additional members.
9
+ *
10
+ * Reference workspaces by their Mongo `_id` and members by their member `_id`.
11
+ * Never by name or slug.
12
+ */
13
+ import type { OxyServicesBase } from '../OxyServices.base';
14
+ import { CACHE_TIMES } from './mixinHelpers';
15
+
16
+ /** Role a member holds within a workspace. */
17
+ export type WorkspaceRole = 'owner' | 'admin' | 'member' | 'viewer';
18
+
19
+ /** Workspace classification. A `personal` workspace is implicit per user. */
20
+ export type WorkspaceType = 'personal' | 'team';
21
+
22
+ /** Lifecycle status of a workspace. */
23
+ export type WorkspaceStatus = 'active' | 'deleted';
24
+
25
+ /** Membership lifecycle status. */
26
+ export type WorkspaceMemberStatus = 'active' | 'invited' | 'removed';
27
+
28
+ /**
29
+ * Client-facing WorkspaceMember shape. `permissions` is derived from `role`
30
+ * on the server at write time.
31
+ */
32
+ export interface WorkspaceMember {
33
+ _id: string;
34
+ workspaceId: string;
35
+ userId: string;
36
+ role: WorkspaceRole;
37
+ permissions: string[];
38
+ invitedByUserId?: string | null;
39
+ joinedAt?: string | null;
40
+ status: WorkspaceMemberStatus;
41
+ createdAt: string;
42
+ updatedAt: string;
43
+ }
44
+
45
+ /**
46
+ * Client-facing Workspace shape returned by the `/workspaces` API. Mirrors the
47
+ * server `Workspace` model with `_id` as a string and dates serialized to ISO
48
+ * strings.
49
+ */
50
+ export interface Workspace {
51
+ _id: string;
52
+ name: string;
53
+ slug: string;
54
+ type: WorkspaceType;
55
+ description?: string | null;
56
+ icon?: string | null;
57
+ ownerId: string;
58
+ status: WorkspaceStatus;
59
+ createdAt: string;
60
+ updatedAt: string;
61
+ /**
62
+ * The calling user's own membership in this workspace, embedded by the API
63
+ * on list (`GET /workspaces`) and detail (`GET /workspaces/:id`) responses.
64
+ * Use `callerMembership.permissions` to gate UI affordances.
65
+ */
66
+ callerMembership?: WorkspaceMember | null;
67
+ }
68
+
69
+ /** Input accepted by `createWorkspace`. */
70
+ export interface CreateWorkspaceInput {
71
+ name: string;
72
+ description?: string;
73
+ icon?: string;
74
+ }
75
+
76
+ /** Input accepted by `updateWorkspace`. */
77
+ export interface UpdateWorkspaceInput {
78
+ name?: string;
79
+ description?: string | null;
80
+ icon?: string | null;
81
+ }
82
+
83
+ /** Input accepted by `inviteWorkspaceMember`. The owner role cannot be invited. */
84
+ export interface InviteWorkspaceMemberInput {
85
+ userId: string;
86
+ role: Exclude<WorkspaceRole, 'owner'>;
87
+ }
88
+
89
+ /** Input accepted by `updateWorkspaceMember`. The owner role cannot be assigned. */
90
+ export interface UpdateWorkspaceMemberInput {
91
+ role: Exclude<WorkspaceRole, 'owner'>;
92
+ }
93
+
94
+ /** Input accepted by `transferWorkspaceOwnership`. */
95
+ export interface TransferWorkspaceOwnershipInput {
96
+ userId: string;
97
+ }
98
+
99
+ /** Result of a delete/remove/transfer operation. */
100
+ export interface WorkspaceSuccessResult {
101
+ success: boolean;
102
+ }
103
+
104
+ export function OxyServicesWorkspacesMixin<T extends typeof OxyServicesBase>(Base: T) {
105
+ return class extends Base {
106
+ constructor(...args: any[]) {
107
+ super(...(args as [any]));
108
+ }
109
+
110
+ /**
111
+ * List workspaces the current user is an active member of.
112
+ */
113
+ async getWorkspaces(): Promise<Workspace[]> {
114
+ try {
115
+ const res = await this.makeRequest<{ workspaces?: Workspace[] }>(
116
+ 'GET',
117
+ '/workspaces',
118
+ undefined,
119
+ { cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
120
+ );
121
+ return res.workspaces ?? [];
122
+ } catch (error) {
123
+ throw this.handleError(error);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Create a new team workspace. The caller becomes its `owner`.
129
+ * @param data - Workspace configuration.
130
+ */
131
+ async createWorkspace(data: CreateWorkspaceInput): Promise<Workspace> {
132
+ try {
133
+ const res = await this.makeRequest<{ workspace: Workspace }>(
134
+ 'POST',
135
+ '/workspaces',
136
+ data,
137
+ { cache: false },
138
+ );
139
+ return res.workspace;
140
+ } catch (error) {
141
+ throw this.handleError(error);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Fetch a single workspace by id.
147
+ * @param workspaceId - The workspace's Mongo `_id`.
148
+ */
149
+ async getWorkspace(workspaceId: string): Promise<Workspace> {
150
+ try {
151
+ const res = await this.makeRequest<{ workspace: Workspace }>(
152
+ 'GET',
153
+ `/workspaces/${encodeURIComponent(workspaceId)}`,
154
+ undefined,
155
+ { cache: true, cacheTTL: CACHE_TIMES.LONG },
156
+ );
157
+ return res.workspace;
158
+ } catch (error) {
159
+ throw this.handleError(error);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Update a workspace's mutable fields.
165
+ * @param workspaceId - The workspace's Mongo `_id`.
166
+ * @param data - Subset of updatable fields.
167
+ */
168
+ async updateWorkspace(
169
+ workspaceId: string,
170
+ data: UpdateWorkspaceInput,
171
+ ): Promise<Workspace> {
172
+ try {
173
+ const res = await this.makeRequest<{ workspace: Workspace }>(
174
+ 'PATCH',
175
+ `/workspaces/${encodeURIComponent(workspaceId)}`,
176
+ data,
177
+ { cache: false },
178
+ );
179
+ return res.workspace;
180
+ } catch (error) {
181
+ throw this.handleError(error);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Soft-delete a workspace (owner only).
187
+ * @param workspaceId - The workspace's Mongo `_id`.
188
+ */
189
+ async deleteWorkspace(workspaceId: string): Promise<WorkspaceSuccessResult> {
190
+ try {
191
+ return await this.makeRequest<WorkspaceSuccessResult>(
192
+ 'DELETE',
193
+ `/workspaces/${encodeURIComponent(workspaceId)}`,
194
+ undefined,
195
+ { cache: false },
196
+ );
197
+ } catch (error) {
198
+ throw this.handleError(error);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * List members of a workspace.
204
+ * @param workspaceId - The workspace's Mongo `_id`.
205
+ */
206
+ async getWorkspaceMembers(workspaceId: string): Promise<WorkspaceMember[]> {
207
+ try {
208
+ const res = await this.makeRequest<{ members?: WorkspaceMember[] }>(
209
+ 'GET',
210
+ `/workspaces/${encodeURIComponent(workspaceId)}/members`,
211
+ undefined,
212
+ { cache: true, cacheTTL: CACHE_TIMES.MEDIUM },
213
+ );
214
+ return res.members ?? [];
215
+ } catch (error) {
216
+ throw this.handleError(error);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Add a member to a workspace.
222
+ * @param workspaceId - The workspace's Mongo `_id`.
223
+ * @param data - Target user id and role (never `owner`).
224
+ */
225
+ async inviteWorkspaceMember(
226
+ workspaceId: string,
227
+ data: InviteWorkspaceMemberInput,
228
+ ): Promise<WorkspaceMember> {
229
+ try {
230
+ const res = await this.makeRequest<{ member: WorkspaceMember }>(
231
+ 'POST',
232
+ `/workspaces/${encodeURIComponent(workspaceId)}/members`,
233
+ data,
234
+ { cache: false },
235
+ );
236
+ return res.member;
237
+ } catch (error) {
238
+ throw this.handleError(error);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Change a member's role.
244
+ * @param workspaceId - The workspace's Mongo `_id`.
245
+ * @param memberId - The member's Mongo `_id`.
246
+ * @param data - New role (never `owner`).
247
+ */
248
+ async updateWorkspaceMember(
249
+ workspaceId: string,
250
+ memberId: string,
251
+ data: UpdateWorkspaceMemberInput,
252
+ ): Promise<WorkspaceMember> {
253
+ try {
254
+ const res = await this.makeRequest<{ member: WorkspaceMember }>(
255
+ 'PATCH',
256
+ `/workspaces/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(memberId)}`,
257
+ data,
258
+ { cache: false },
259
+ );
260
+ return res.member;
261
+ } catch (error) {
262
+ throw this.handleError(error);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Remove a member from a workspace.
268
+ * @param workspaceId - The workspace's Mongo `_id`.
269
+ * @param memberId - The member's Mongo `_id`.
270
+ */
271
+ async removeWorkspaceMember(
272
+ workspaceId: string,
273
+ memberId: string,
274
+ ): Promise<WorkspaceSuccessResult> {
275
+ try {
276
+ return await this.makeRequest<WorkspaceSuccessResult>(
277
+ 'DELETE',
278
+ `/workspaces/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(memberId)}`,
279
+ undefined,
280
+ { cache: false },
281
+ );
282
+ } catch (error) {
283
+ throw this.handleError(error);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Transfer ownership of a workspace to another member (owner only).
289
+ * Demotes the current owner and promotes the target to `owner`.
290
+ * @param workspaceId - The workspace's Mongo `_id`.
291
+ * @param data - Target user id.
292
+ */
293
+ async transferWorkspaceOwnership(
294
+ workspaceId: string,
295
+ data: TransferWorkspaceOwnershipInput,
296
+ ): Promise<WorkspaceSuccessResult> {
297
+ try {
298
+ return await this.makeRequest<WorkspaceSuccessResult>(
299
+ 'POST',
300
+ `/workspaces/${encodeURIComponent(workspaceId)}/transfer-ownership`,
301
+ data,
302
+ { cache: false },
303
+ );
304
+ } catch (error) {
305
+ throw this.handleError(error);
306
+ }
307
+ }
308
+ };
309
+ }
@@ -18,6 +18,7 @@ import { OxyServicesPaymentMixin } from './OxyServices.payment';
18
18
  import { OxyServicesKarmaMixin } from './OxyServices.karma';
19
19
  import { OxyServicesAssetsMixin } from './OxyServices.assets';
20
20
  import { OxyServicesApplicationsMixin } from './OxyServices.applications';
21
+ import { OxyServicesWorkspacesMixin } from './OxyServices.workspaces';
21
22
  import { OxyServicesLocationMixin } from './OxyServices.location';
22
23
  import { OxyServicesAnalyticsMixin } from './OxyServices.analytics';
23
24
  import { OxyServicesDevicesMixin } from './OxyServices.devices';
@@ -51,6 +52,7 @@ type AllMixinInstances =
51
52
  & InstanceType<ReturnType<typeof OxyServicesKarmaMixin<typeof OxyServicesBase>>>
52
53
  & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>>
53
54
  & InstanceType<ReturnType<typeof OxyServicesApplicationsMixin<typeof OxyServicesBase>>>
55
+ & InstanceType<ReturnType<typeof OxyServicesWorkspacesMixin<typeof OxyServicesBase>>>
54
56
  & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>>
55
57
  & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>>
56
58
  & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>>
@@ -115,6 +117,7 @@ const MIXIN_PIPELINE: MixinFunction[] = [
115
117
  OxyServicesKarmaMixin,
116
118
  OxyServicesAssetsMixin,
117
119
  OxyServicesApplicationsMixin,
120
+ OxyServicesWorkspacesMixin,
118
121
  OxyServicesLocationMixin,
119
122
  OxyServicesAnalyticsMixin,
120
123
  OxyServicesDevicesMixin,
@@ -17,6 +17,15 @@ export interface OxyConfig {
17
17
  sessionBaseUrl?: string;
18
18
  authWebUrl?: string;
19
19
  authRedirectUri?: string;
20
+ /**
21
+ * The app's Oxy OAuth client id (ApplicationCredential publicKey).
22
+ *
23
+ * Identifies this app in OAuth authorize / consent flows (issue #214). Purely
24
+ * declarative: the SDK stores it on `OxyServices.config.clientId` for later
25
+ * OAuth-authorize use. It is unrelated to the cross-domain `/sso?client_id=…`
26
+ * bounce (which uses the RP origin, not this registered client id).
27
+ */
28
+ clientId?: string;
20
29
  // Performance & caching options
21
30
  enableCache?: boolean;
22
31
  cacheTTL?: number; // Cache TTL in milliseconds (default: 5 minutes)