@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.
@@ -83,6 +83,7 @@ import { composeOxyServices } from './mixins';
83
83
  * - **Karma**: Karma system
84
84
  * - **Assets**: File upload and asset management
85
85
  * - **Applications**: Application, membership, and credential management
86
+ * - **Workspaces**: Workspace and membership management
86
87
  * - **Location**: Location-based features
87
88
  * - **Analytics**: Analytics tracking
88
89
  * - **Devices**: Device management
@@ -33,7 +33,8 @@ export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServic
33
33
  export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount, } from './mixins/OxyServices.managedAccounts';
34
34
  export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
35
35
  export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
36
- export type { Application, ApplicationMember, ApplicationCredential, ApplicationRole, ApplicationType, ApplicationStatus, ApplicationMemberStatus, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, InviteApplicationMemberInput, UpdateApplicationMemberInput, TransferApplicationOwnershipInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, ApplicationSuccessResult, } from './mixins/OxyServices.applications';
36
+ export type { Application, PublicApplication, ApplicationMember, ApplicationCredential, ApplicationRole, ApplicationType, ApplicationStatus, ApplicationMemberStatus, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, InviteApplicationMemberInput, UpdateApplicationMemberInput, TransferApplicationOwnershipInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, ApplicationSuccessResult, } from './mixins/OxyServices.applications';
37
+ export type { Workspace, WorkspaceMember, WorkspaceRole, WorkspaceType, WorkspaceStatus, WorkspaceMemberStatus, CreateWorkspaceInput, UpdateWorkspaceInput, InviteWorkspaceMemberInput, UpdateWorkspaceMemberInput, TransferWorkspaceOwnershipInput, WorkspaceSuccessResult, } from './mixins/OxyServices.workspaces';
37
38
  export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
38
39
  export type { HandleApiErrorOptions } from './utils/authHelpers';
39
40
  export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from './utils/sessionUtils';
@@ -49,12 +49,22 @@ export interface Application {
49
49
  webhookUrl?: string;
50
50
  devWebhookUrl?: string;
51
51
  createdByUserId: string;
52
+ /**
53
+ * The workspace this application belongs to (workspace `_id`), or `null` for
54
+ * applications not owned by a workspace. Used by the console to scope apps to
55
+ * a workspace and to branch on workspace-derived access.
56
+ */
57
+ workspaceId: string | null;
52
58
  createdAt: string;
53
59
  updatedAt: string;
54
60
  /**
55
61
  * The calling user's own membership in this application, embedded by the API
56
62
  * on list (`GET /applications`) and detail (`GET /applications/:appId`)
57
63
  * responses. Use `callerMembership.permissions` to gate UI affordances.
64
+ *
65
+ * When the caller's access is derived from a workspace membership rather than
66
+ * a direct application membership, the API returns a synthetic membership
67
+ * with `source: 'workspace'` and `_id: null`.
58
68
  */
59
69
  callerMembership?: ApplicationMember;
60
70
  }
@@ -63,7 +73,11 @@ export interface Application {
63
73
  * on the server at write time.
64
74
  */
65
75
  export interface ApplicationMember {
66
- _id: string;
76
+ /**
77
+ * The membership's Mongo `_id`. `null` for a synthetic, workspace-derived
78
+ * membership (see {@link Application.callerMembership} and `source`).
79
+ */
80
+ _id: string | null;
67
81
  applicationId: string;
68
82
  userId: string;
69
83
  role: ApplicationRole;
@@ -71,6 +85,13 @@ export interface ApplicationMember {
71
85
  invitedByUserId?: string;
72
86
  joinedAt?: string;
73
87
  status: ApplicationMemberStatus;
88
+ /**
89
+ * Origin of this membership. When `'workspace'`, the membership is synthetic
90
+ * and derived from the caller's workspace membership rather than a direct
91
+ * application membership (in which case `_id` is `null`). Absent or any other
92
+ * value indicates a direct application membership.
93
+ */
94
+ source?: 'workspace';
74
95
  createdAt: string;
75
96
  updatedAt: string;
76
97
  }
@@ -98,6 +119,38 @@ export interface ApplicationCredential {
98
119
  createdAt: string;
99
120
  updatedAt: string;
100
121
  }
122
+ /**
123
+ * Sanitized, PUBLIC application identity returned by the API when resolving a
124
+ * cross-app/OAuth client to a registered {@link Application}.
125
+ *
126
+ * Unlike {@link Application}, this shape carries NO sensitive or membership
127
+ * fields — it is safe to display unauthenticated in consent/authorize screens
128
+ * and device-flow approval UIs. The API resolves a `client_id` (OAuth
129
+ * credential public key) to the owning application and projects only the
130
+ * fields below. `id` is the application's `_id` as a string.
131
+ */
132
+ export interface PublicApplication {
133
+ /** The application's Mongo `_id` as a string. */
134
+ id: string;
135
+ /** Human-readable application name shown to the user. */
136
+ name: string;
137
+ /** Optional short description of what the application does. */
138
+ description?: string;
139
+ /** Optional icon URL for the application. */
140
+ icon?: string;
141
+ /** Optional public website/homepage URL for the application. */
142
+ websiteUrl?: string;
143
+ /** Application classification (set by Oxy platform staff). */
144
+ type: ApplicationType;
145
+ /** Whether the application is an officially endorsed Oxy application. */
146
+ isOfficial: boolean;
147
+ /** Whether the application is an internal Oxy ecosystem application. */
148
+ isInternal: boolean;
149
+ /** OAuth scopes the application is configured to request. */
150
+ scopes: string[];
151
+ /** Optional display name of the developer/owner organisation. */
152
+ developerName?: string;
153
+ }
101
154
  /** Input accepted by `createApplication`. Staff-only fields are not settable here. */
102
155
  export interface CreateApplicationInput {
103
156
  name: string;
@@ -106,6 +159,11 @@ export interface CreateApplicationInput {
106
159
  icon?: string;
107
160
  redirectUris?: string[];
108
161
  scopes?: string[];
162
+ /**
163
+ * Optional workspace `_id` to create the app in. Omitted → API defaults to
164
+ * the caller's personal workspace.
165
+ */
166
+ workspaceId?: string;
109
167
  }
110
168
  /** Input accepted by `updateApplication`. Staff-only fields are not settable here. */
111
169
  export interface UpdateApplicationInput {
@@ -192,10 +250,27 @@ export interface ApplicationSuccessResult {
192
250
  }
193
251
  export declare function OxyServicesApplicationsMixin<T extends typeof OxyServicesBase>(Base: T): {
194
252
  new (...args: any[]): {
253
+ /**
254
+ * Resolve an OAuth client identifier to the owning application's PUBLIC
255
+ * identity. No authentication required — the API returns only sanitized,
256
+ * display-safe metadata ({@link PublicApplication}). Use this to render the
257
+ * requesting application's name/icon in consent, authorize, and device-flow
258
+ * approval UIs before any session exists.
259
+ *
260
+ * @param clientId - The OAuth `client_id` (an active credential's public
261
+ * key). URL-encoded before being placed in the path.
262
+ */
263
+ getPublicApplication(clientId: string): Promise<PublicApplication>;
195
264
  /**
196
265
  * List applications the current user is an active member of.
266
+ *
267
+ * @param workspaceId - Optional workspace `_id` to scope the listing to
268
+ * applications belonging to that workspace. When provided it is appended
269
+ * as a `workspaceId` query parameter (URL-encoded). The query string is
270
+ * part of the request path, so the response cache keys on it
271
+ * automatically — scoped and unscoped lists never collide.
197
272
  */
198
- getApplications(): Promise<Application[]>;
273
+ getApplications(workspaceId?: string): Promise<Application[]>;
199
274
  /**
200
275
  * Create a new application. The caller becomes its `owner`.
201
276
  * @param data - Application configuration. Staff-only fields are ignored.
@@ -329,12 +404,7 @@ export declare function OxyServicesApplicationsMixin<T extends typeof OxyService
329
404
  healthCheck(): Promise<{
330
405
  status: string;
331
406
  users?: number;
332
- timestamp? /**
333
- * Create a credential. The plaintext `secret` is returned exactly ONCE;
334
- * the server stores only a hash and will never return it again.
335
- * @param applicationId - The application's Mongo `_id`.
336
- * @param data - Credential configuration.
337
- */: string;
407
+ timestamp?: string;
338
408
  [key: string]: any;
339
409
  }>;
340
410
  };
@@ -0,0 +1,199 @@
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
+ /** Role a member holds within a workspace. */
15
+ export type WorkspaceRole = 'owner' | 'admin' | 'member' | 'viewer';
16
+ /** Workspace classification. A `personal` workspace is implicit per user. */
17
+ export type WorkspaceType = 'personal' | 'team';
18
+ /** Lifecycle status of a workspace. */
19
+ export type WorkspaceStatus = 'active' | 'deleted';
20
+ /** Membership lifecycle status. */
21
+ export type WorkspaceMemberStatus = 'active' | 'invited' | 'removed';
22
+ /**
23
+ * Client-facing WorkspaceMember shape. `permissions` is derived from `role`
24
+ * on the server at write time.
25
+ */
26
+ export interface WorkspaceMember {
27
+ _id: string;
28
+ workspaceId: string;
29
+ userId: string;
30
+ role: WorkspaceRole;
31
+ permissions: string[];
32
+ invitedByUserId?: string | null;
33
+ joinedAt?: string | null;
34
+ status: WorkspaceMemberStatus;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ }
38
+ /**
39
+ * Client-facing Workspace shape returned by the `/workspaces` API. Mirrors the
40
+ * server `Workspace` model with `_id` as a string and dates serialized to ISO
41
+ * strings.
42
+ */
43
+ export interface Workspace {
44
+ _id: string;
45
+ name: string;
46
+ slug: string;
47
+ type: WorkspaceType;
48
+ description?: string | null;
49
+ icon?: string | null;
50
+ ownerId: string;
51
+ status: WorkspaceStatus;
52
+ createdAt: string;
53
+ updatedAt: string;
54
+ /**
55
+ * The calling user's own membership in this workspace, embedded by the API
56
+ * on list (`GET /workspaces`) and detail (`GET /workspaces/:id`) responses.
57
+ * Use `callerMembership.permissions` to gate UI affordances.
58
+ */
59
+ callerMembership?: WorkspaceMember | null;
60
+ }
61
+ /** Input accepted by `createWorkspace`. */
62
+ export interface CreateWorkspaceInput {
63
+ name: string;
64
+ description?: string;
65
+ icon?: string;
66
+ }
67
+ /** Input accepted by `updateWorkspace`. */
68
+ export interface UpdateWorkspaceInput {
69
+ name?: string;
70
+ description?: string | null;
71
+ icon?: string | null;
72
+ }
73
+ /** Input accepted by `inviteWorkspaceMember`. The owner role cannot be invited. */
74
+ export interface InviteWorkspaceMemberInput {
75
+ userId: string;
76
+ role: Exclude<WorkspaceRole, 'owner'>;
77
+ }
78
+ /** Input accepted by `updateWorkspaceMember`. The owner role cannot be assigned. */
79
+ export interface UpdateWorkspaceMemberInput {
80
+ role: Exclude<WorkspaceRole, 'owner'>;
81
+ }
82
+ /** Input accepted by `transferWorkspaceOwnership`. */
83
+ export interface TransferWorkspaceOwnershipInput {
84
+ userId: string;
85
+ }
86
+ /** Result of a delete/remove/transfer operation. */
87
+ export interface WorkspaceSuccessResult {
88
+ success: boolean;
89
+ }
90
+ export declare function OxyServicesWorkspacesMixin<T extends typeof OxyServicesBase>(Base: T): {
91
+ new (...args: any[]): {
92
+ /**
93
+ * List workspaces the current user is an active member of.
94
+ */
95
+ getWorkspaces(): Promise<Workspace[]>;
96
+ /**
97
+ * Create a new team workspace. The caller becomes its `owner`.
98
+ * @param data - Workspace configuration.
99
+ */
100
+ createWorkspace(data: CreateWorkspaceInput): Promise<Workspace>;
101
+ /**
102
+ * Fetch a single workspace by id.
103
+ * @param workspaceId - The workspace's Mongo `_id`.
104
+ */
105
+ getWorkspace(workspaceId: string): Promise<Workspace>;
106
+ /**
107
+ * Update a workspace's mutable fields.
108
+ * @param workspaceId - The workspace's Mongo `_id`.
109
+ * @param data - Subset of updatable fields.
110
+ */
111
+ updateWorkspace(workspaceId: string, data: UpdateWorkspaceInput): Promise<Workspace>;
112
+ /**
113
+ * Soft-delete a workspace (owner only).
114
+ * @param workspaceId - The workspace's Mongo `_id`.
115
+ */
116
+ deleteWorkspace(workspaceId: string): Promise<WorkspaceSuccessResult>;
117
+ /**
118
+ * List members of a workspace.
119
+ * @param workspaceId - The workspace's Mongo `_id`.
120
+ */
121
+ getWorkspaceMembers(workspaceId: string): Promise<WorkspaceMember[]>;
122
+ /**
123
+ * Add a member to a workspace.
124
+ * @param workspaceId - The workspace's Mongo `_id`.
125
+ * @param data - Target user id and role (never `owner`).
126
+ */
127
+ inviteWorkspaceMember(workspaceId: string, data: InviteWorkspaceMemberInput): Promise<WorkspaceMember>;
128
+ /**
129
+ * Change a member's role.
130
+ * @param workspaceId - The workspace's Mongo `_id`.
131
+ * @param memberId - The member's Mongo `_id`.
132
+ * @param data - New role (never `owner`).
133
+ */
134
+ updateWorkspaceMember(workspaceId: string, memberId: string, data: UpdateWorkspaceMemberInput): Promise<WorkspaceMember>;
135
+ /**
136
+ * Remove a member from a workspace.
137
+ * @param workspaceId - The workspace's Mongo `_id`.
138
+ * @param memberId - The member's Mongo `_id`.
139
+ */
140
+ removeWorkspaceMember(workspaceId: string, memberId: string): Promise<WorkspaceSuccessResult>;
141
+ /**
142
+ * Transfer ownership of a workspace to another member (owner only).
143
+ * Demotes the current owner and promotes the target to `owner`.
144
+ * @param workspaceId - The workspace's Mongo `_id`.
145
+ * @param data - Target user id.
146
+ */
147
+ transferWorkspaceOwnership(workspaceId: string, data: TransferWorkspaceOwnershipInput): Promise<WorkspaceSuccessResult>;
148
+ httpService: import("../HttpService").HttpService;
149
+ cloudURL: string;
150
+ config: import("../OxyServices.base").OxyConfig;
151
+ __resetTokensForTests(): void;
152
+ makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
153
+ getBaseURL(): string;
154
+ getSessionBaseUrl(): string;
155
+ getClient(): import("../HttpService").HttpService;
156
+ getMetrics(): {
157
+ totalRequests: number;
158
+ successfulRequests: number;
159
+ failedRequests: number;
160
+ cacheHits: number;
161
+ cacheMisses: number;
162
+ averageResponseTime: number;
163
+ };
164
+ clearCache(): void;
165
+ clearCacheEntry(key: string): void;
166
+ clearCacheByPrefix(prefix: string): number;
167
+ getCacheStats(): {
168
+ size: number;
169
+ hits: number;
170
+ misses: number;
171
+ hitRate: number;
172
+ };
173
+ getCloudURL(): string;
174
+ setTokens(accessToken: string, refreshToken?: string): void;
175
+ clearTokens(): void;
176
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
177
+ _cachedUserId: string | null | undefined;
178
+ _cachedAccessToken: string | null;
179
+ getCurrentUserId(): string | null;
180
+ hasValidToken(): boolean;
181
+ getAccessToken(): string | null;
182
+ setActingAs(userId: string | null): void;
183
+ getActingAs(): string | null;
184
+ waitForAuth(timeoutMs?: number): Promise<boolean>;
185
+ withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
186
+ maxRetries?: number;
187
+ retryDelay?: number;
188
+ authTimeoutMs?: number;
189
+ }): Promise<T_1>;
190
+ validate(): Promise<boolean>;
191
+ handleError(error: unknown): Error;
192
+ healthCheck(): Promise<{
193
+ status: string;
194
+ users?: number;
195
+ timestamp?: string;
196
+ [key: string]: any;
197
+ }>;
198
+ };
199
+ } & T;
@@ -17,6 +17,7 @@ import { OxyServicesPaymentMixin } from './OxyServices.payment';
17
17
  import { OxyServicesKarmaMixin } from './OxyServices.karma';
18
18
  import { OxyServicesAssetsMixin } from './OxyServices.assets';
19
19
  import { OxyServicesApplicationsMixin } from './OxyServices.applications';
20
+ import { OxyServicesWorkspacesMixin } from './OxyServices.workspaces';
20
21
  import { OxyServicesLocationMixin } from './OxyServices.location';
21
22
  import { OxyServicesAnalyticsMixin } from './OxyServices.analytics';
22
23
  import { OxyServicesDevicesMixin } from './OxyServices.devices';
@@ -36,7 +37,7 @@ import { OxyServicesAppDataMixin } from './OxyServices.appData';
36
37
  * If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
37
38
  * are visible without a cast.
38
39
  */
39
- type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFedCMMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPopupAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesRedirectAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSsoMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesKarmaMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesApplicationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesManagedAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
40
+ type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFedCMMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPopupAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesRedirectAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSsoMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesKarmaMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesApplicationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesWorkspacesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesManagedAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
40
41
  /**
41
42
  * Constructor type for the fully composed mixin pipeline. Each mixin returns
42
43
  * a new constructor that augments its input; reducing across the pipeline
@@ -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
  enableCache?: boolean;
21
30
  cacheTTL?: number;
22
31
  enableRequestDeduplication?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -851,7 +851,14 @@ export class AuthManager {
851
851
  * Get a valid access token, refreshing automatically if expired or expiring soon.
852
852
  */
853
853
  async getAccessToken(): Promise<string | null> {
854
- const token = await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
854
+ // In cookieOnly / cookie-restore flows the active access token lives only in
855
+ // memory (`_lastKnownAccessToken` + httpService) and is intentionally never
856
+ // written to JS storage — the cookieOnly contract forbids persisting tokens
857
+ // in JS-accessible storage. Fall back to the in-memory token when storage has
858
+ // none, otherwise getAccessToken returns null after every cold-boot/reload and
859
+ // standalone API clients (e.g. the Console axios client) send no Authorization
860
+ // header → 401 on every authed endpoint while `isAuthenticated` is still true.
861
+ const token = (await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN)) ?? this._lastKnownAccessToken;
855
862
  if (!token) return null;
856
863
 
857
864
  try {
@@ -862,7 +869,9 @@ export class AuthManager {
862
869
  if (decoded.exp - now < buffer) {
863
870
  const refreshed = await this.refreshToken();
864
871
  if (refreshed) {
865
- return this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
872
+ // refreshToken() updates both storage and `_lastKnownAccessToken`;
873
+ // prefer storage but fall back to memory for the cookieOnly path.
874
+ return (await this.storage.getItem(STORAGE_KEYS.ACCESS_TOKEN)) ?? this._lastKnownAccessToken;
866
875
  }
867
876
  }
868
877
  }
@@ -86,6 +86,7 @@ import { composeOxyServices } from './mixins';
86
86
  * - **Karma**: Karma system
87
87
  * - **Assets**: File upload and asset management
88
88
  * - **Applications**: Application, membership, and credential management
89
+ * - **Workspaces**: Workspace and membership management
89
90
  * - **Location**: Location-based features
90
91
  * - **Analytics**: Analytics tracking
91
92
  * - **Devices**: Device management
@@ -337,3 +337,52 @@ describe('AuthManager.initialize (cookieOnly)', () => {
337
337
  expect(services.httpService.setTokens).not.toHaveBeenCalled();
338
338
  });
339
339
  });
340
+
341
+ describe('AuthManager.getAccessToken (cookieOnly in-memory fallback)', () => {
342
+ it('returns the in-memory token after a cold-boot cookie restore even though storage holds no token', async () => {
343
+ // Regression: the cookie restore path plants the active token ONLY in memory
344
+ // (`_lastKnownAccessToken` + httpService) and intentionally NEVER writes
345
+ // `oxy_access_token`. getAccessToken() must fall back to that in-memory token,
346
+ // otherwise standalone API clients reading `authManager.getAccessToken()` send
347
+ // no Authorization header → 401 on every authed endpoint after every reload.
348
+ const services = makeMockServices();
349
+ services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
350
+ const storage = new InMemoryStorage();
351
+ const manager = makeManager(services, storage);
352
+
353
+ await manager.restoreFromCookies();
354
+
355
+ // Storage was never touched for the access token (cookieOnly contract holds).
356
+ expect(storage.has('oxy_access_token')).toBe(false);
357
+
358
+ // getAccessToken still resolves the active slot's token from memory.
359
+ const token = await manager.getAccessToken();
360
+ expect(token).toBe(TOKEN_SLOT_0);
361
+ });
362
+
363
+ it('returns null when neither storage nor the in-memory token is present', async () => {
364
+ const services = makeMockServices();
365
+ const storage = new InMemoryStorage();
366
+ const manager = makeManager(services, storage);
367
+
368
+ const token = await manager.getAccessToken();
369
+ expect(token).toBeNull();
370
+ });
371
+
372
+ it('prefers the storage token over the in-memory token when both are present', async () => {
373
+ const services = makeMockServices();
374
+ services.refreshAllSessions.mockResolvedValueOnce(TWO_ACCOUNTS);
375
+ const storage = new InMemoryStorage();
376
+ const manager = makeManager(services, storage);
377
+
378
+ // After restore the in-memory token is TOKEN_SLOT_0.
379
+ await manager.restoreFromCookies();
380
+
381
+ // Simulate a path that DID write storage (legacy/bearer flow). Storage wins.
382
+ const STORAGE_TOKEN = buildAccessToken({ sessionId: 'sess-storage', userId: 'user-storage', exp: 9999999999 });
383
+ storage.setItem('oxy_access_token', STORAGE_TOKEN);
384
+
385
+ const token = await manager.getAccessToken();
386
+ expect(token).toBe(STORAGE_TOKEN);
387
+ });
388
+ });
package/src/index.ts CHANGED
@@ -67,6 +67,7 @@ export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
67
67
  // ---------------------------------------------------------------------------
68
68
  export type {
69
69
  Application,
70
+ PublicApplication,
70
71
  ApplicationMember,
71
72
  ApplicationCredential,
72
73
  ApplicationRole,
@@ -92,6 +93,24 @@ export type {
92
93
  ApplicationSuccessResult,
93
94
  } from './mixins/OxyServices.applications';
94
95
 
96
+ // ---------------------------------------------------------------------------
97
+ // Workspaces (multi-user containers: membership, roles)
98
+ // ---------------------------------------------------------------------------
99
+ export type {
100
+ Workspace,
101
+ WorkspaceMember,
102
+ WorkspaceRole,
103
+ WorkspaceType,
104
+ WorkspaceStatus,
105
+ WorkspaceMemberStatus,
106
+ CreateWorkspaceInput,
107
+ UpdateWorkspaceInput,
108
+ InviteWorkspaceMemberInput,
109
+ UpdateWorkspaceMemberInput,
110
+ TransferWorkspaceOwnershipInput,
111
+ WorkspaceSuccessResult,
112
+ } from './mixins/OxyServices.workspaces';
113
+
95
114
  // ---------------------------------------------------------------------------
96
115
  // Auth helpers (token refresh, error normalisation, retry policies)
97
116
  // ---------------------------------------------------------------------------