Skip to content

Commit 13903cd

Browse files
committed
add client-only export
1 parent 369afff commit 13903cd

File tree

4 files changed

+301
-22
lines changed

4 files changed

+301
-22
lines changed

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@
103103
"require": "./lib/cjs/index.cjs",
104104
"default": "./lib/esm/index.js"
105105
},
106+
"./client": {
107+
"types": {
108+
"require": "./lib/cjs/index.client.d.cts",
109+
"import": "./lib/esm/index.client.d.ts"
110+
},
111+
"import": "./lib/esm/index.client.js",
112+
"require": "./lib/cjs/index.client.cjs",
113+
"default": "./lib/esm/index.client.js"
114+
},
106115
"./worker": {
107116
"types": {
108117
"require": "./lib/cjs/index.worker.d.cts",

src/index.client.spec.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { WorkOS } from './index.client';
2+
3+
describe('WorkOS Client', () => {
4+
let workosClient: WorkOS;
5+
6+
beforeEach(() => {
7+
workosClient = new WorkOS({
8+
clientId: 'client_123',
9+
apiHostname: 'api.workos.dev',
10+
});
11+
});
12+
13+
describe('instantiation', () => {
14+
it('should instantiate without requiring an API key', () => {
15+
expect(() => new WorkOS()).not.toThrow();
16+
});
17+
18+
it('should accept client configuration options', () => {
19+
const client = new WorkOS({
20+
clientId: 'test_client',
21+
apiHostname: 'test.api.com',
22+
https: false,
23+
port: 3000,
24+
});
25+
26+
expect(client).toBeDefined();
27+
expect(client.version).toBeDefined();
28+
});
29+
});
30+
31+
describe('exposed services', () => {
32+
it('should expose webhooks service', () => {
33+
expect(workosClient.webhooks).toBeDefined();
34+
expect(workosClient.webhooks.verifyHeader).toBeDefined();
35+
expect(workosClient.webhooks.computeSignature).toBeDefined();
36+
expect(workosClient.webhooks.constructEvent).toBeDefined();
37+
});
38+
39+
it('should expose actions service', () => {
40+
expect(workosClient.actions).toBeDefined();
41+
expect(workosClient.actions.verifyHeader).toBeDefined();
42+
expect(workosClient.actions.signResponse).toBeDefined();
43+
expect(workosClient.actions.constructAction).toBeDefined();
44+
});
45+
46+
it('should expose client-safe user management methods', () => {
47+
expect(workosClient.userManagement).toBeDefined();
48+
expect(
49+
workosClient.userManagement.authenticateWithCodeAndVerifier,
50+
).toBeDefined();
51+
expect(workosClient.userManagement.getAuthorizationUrl).toBeDefined();
52+
expect(workosClient.userManagement.getLogoutUrl).toBeDefined();
53+
expect(workosClient.userManagement.getJwksUrl).toBeDefined();
54+
});
55+
56+
it('should expose client-safe SSO methods', () => {
57+
expect(workosClient.sso).toBeDefined();
58+
expect(workosClient.sso.getAuthorizationUrl).toBeDefined();
59+
});
60+
61+
it('should expose version', () => {
62+
expect(workosClient.version).toBeDefined();
63+
expect(typeof workosClient.version).toBe('string');
64+
});
65+
});
66+
67+
describe('method behavior', () => {
68+
it('should be able to call URL generation methods', () => {
69+
const authUrl = workosClient.userManagement.getAuthorizationUrl({
70+
clientId: 'client_123',
71+
redirectUri: 'https://example.com/callback',
72+
provider: 'GoogleOAuth',
73+
});
74+
75+
expect(authUrl).toContain('api.workos.dev');
76+
expect(authUrl).toContain('client_id=client_123');
77+
expect(authUrl).toContain('provider=GoogleOAuth');
78+
});
79+
80+
it('should be able to call JWKS URL generation', () => {
81+
const jwksUrl = workosClient.userManagement.getJwksUrl('client_123');
82+
expect(jwksUrl).toBe('https://api.workos.dev/sso/jwks/client_123');
83+
});
84+
85+
it('should be able to call logout URL generation', () => {
86+
const logoutUrl = workosClient.userManagement.getLogoutUrl({
87+
sessionId: 'session_123',
88+
returnTo: 'https://example.com',
89+
});
90+
91+
expect(logoutUrl).toContain('api.workos.dev');
92+
expect(logoutUrl).toContain('session_id=session_123');
93+
});
94+
95+
it('should be able to call SSO authorization URL generation', () => {
96+
const authUrl = workosClient.sso.getAuthorizationUrl({
97+
clientId: 'client_123',
98+
redirectUri: 'https://example.com/callback',
99+
provider: 'GoogleOAuth',
100+
});
101+
102+
expect(authUrl).toContain('api.workos.dev');
103+
expect(authUrl).toContain('client_id=client_123');
104+
});
105+
});
106+
107+
describe('server-only methods should not be exposed', () => {
108+
it('should not expose getUser on userManagement', () => {
109+
expect((workosClient.userManagement as any).getUser).toBeUndefined();
110+
});
111+
112+
it('should not expose createUser on userManagement', () => {
113+
expect((workosClient.userManagement as any).createUser).toBeUndefined();
114+
});
115+
116+
it('should not expose listUsers on userManagement', () => {
117+
expect((workosClient.userManagement as any).listUsers).toBeUndefined();
118+
});
119+
120+
it('should not expose updateUser on userManagement', () => {
121+
expect((workosClient.userManagement as any).updateUser).toBeUndefined();
122+
});
123+
124+
it('should not expose server-only authentication methods', () => {
125+
expect(
126+
(workosClient.userManagement as any).authenticateWithPassword,
127+
).toBeUndefined();
128+
expect(
129+
(workosClient.userManagement as any).authenticateWithMagicAuth,
130+
).toBeUndefined();
131+
expect(
132+
(workosClient.userManagement as any).authenticateWithRefreshToken,
133+
).toBeUndefined();
134+
});
135+
});
136+
137+
describe('type safety', () => {
138+
it('should provide proper TypeScript types for exposed methods', () => {
139+
// These should compile without errors
140+
const authUrl: string = workosClient.userManagement.getAuthorizationUrl({
141+
clientId: 'test',
142+
redirectUri: 'https://example.com',
143+
provider: 'GoogleOAuth',
144+
});
145+
146+
const jwksUrl: string =
147+
workosClient.userManagement.getJwksUrl('client_id');
148+
149+
expect(typeof authUrl).toBe('string');
150+
expect(typeof jwksUrl).toBe('string');
151+
});
152+
});
153+
});

src/index.client.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { WorkOSBase } from './workos';
2+
import { WorkOSOptions } from './common/interfaces';
3+
4+
interface WorkOSClientOptions {
5+
clientId?: string;
6+
apiHostname?: string;
7+
https?: boolean;
8+
port?: number;
9+
}
10+
11+
/**
12+
* WorkOS client-safe SDK for browser environments.
13+
* Only exposes methods that are safe to use without API keys.
14+
*/
15+
export class WorkOS {
16+
private _base: WorkOSBase;
17+
18+
// Client-safe services
19+
readonly userManagement: {
20+
authenticateWithCodeAndVerifier: typeof WorkOSBase.prototype.userManagement.authenticateWithCodeAndVerifier;
21+
getAuthorizationUrl: typeof WorkOSBase.prototype.userManagement.getAuthorizationUrl;
22+
getLogoutUrl: typeof WorkOSBase.prototype.userManagement.getLogoutUrl;
23+
getJwksUrl: typeof WorkOSBase.prototype.userManagement.getJwksUrl;
24+
};
25+
26+
readonly sso: {
27+
getAuthorizationUrl: typeof WorkOSBase.prototype.sso.getAuthorizationUrl;
28+
};
29+
30+
readonly webhooks: typeof WorkOSBase.prototype.webhooks;
31+
readonly actions: typeof WorkOSBase.prototype.actions;
32+
33+
constructor(options: WorkOSClientOptions = {}) {
34+
// Convert client options to base WorkOS options format
35+
const baseOptions: WorkOSOptions = {
36+
clientId: options.clientId,
37+
apiHostname: options.apiHostname,
38+
https: options.https,
39+
port: options.port,
40+
};
41+
42+
// Create base instance without API key
43+
this._base = new WorkOSBase(undefined, baseOptions);
44+
45+
// Initialize client-safe service methods
46+
this.userManagement = {
47+
authenticateWithCodeAndVerifier:
48+
this._base.userManagement.authenticateWithCodeAndVerifier.bind(
49+
this._base.userManagement,
50+
),
51+
getAuthorizationUrl: this._base.userManagement.getAuthorizationUrl.bind(
52+
this._base.userManagement,
53+
),
54+
getLogoutUrl: this._base.userManagement.getLogoutUrl.bind(
55+
this._base.userManagement,
56+
),
57+
getJwksUrl: this._base.userManagement.getJwksUrl.bind(
58+
this._base.userManagement,
59+
),
60+
};
61+
62+
this.sso = {
63+
getAuthorizationUrl: this._base.sso.getAuthorizationUrl.bind(
64+
this._base.sso,
65+
),
66+
};
67+
68+
// These services are fully client-safe - expose entirely
69+
this.webhooks = this._base.webhooks;
70+
this.actions = this._base.actions;
71+
}
72+
73+
get version() {
74+
return this._base.version;
75+
}
76+
}

src/workos.ts

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,38 +48,35 @@ const HEADER_AUTHORIZATION = 'Authorization';
4848
const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';
4949
const HEADER_WARRANT_TOKEN = 'Warrant-Token';
5050

51-
export class WorkOS {
51+
export class WorkOSBase {
5252
readonly baseURL: string;
5353
readonly client: HttpClient;
5454
readonly clientId?: string;
5555

5656
readonly actions: Actions;
57-
readonly auditLogs = new AuditLogs(this);
58-
readonly directorySync = new DirectorySync(this);
59-
readonly organizations = new Organizations(this);
60-
readonly organizationDomains = new OrganizationDomains(this);
61-
readonly passwordless = new Passwordless(this);
62-
readonly portal = new Portal(this);
63-
readonly sso = new SSO(this);
57+
readonly auditLogs: AuditLogs;
58+
readonly directorySync: DirectorySync;
59+
readonly organizations: Organizations;
60+
readonly organizationDomains: OrganizationDomains;
61+
readonly passwordless: Passwordless;
62+
readonly portal: Portal;
63+
readonly sso: SSO;
6464
readonly webhooks: Webhooks;
65-
readonly mfa = new Mfa(this);
66-
readonly events = new Events(this);
65+
readonly mfa: Mfa;
66+
readonly events: Events;
6767
readonly userManagement: UserManagement;
68-
readonly fga = new FGA(this);
69-
readonly widgets = new Widgets(this);
70-
readonly vault = new Vault(this);
68+
readonly fga: FGA;
69+
readonly widgets: Widgets;
70+
readonly vault: Vault;
7171

7272
constructor(
7373
readonly key?: string,
7474
readonly options: WorkOSOptions = {},
7575
) {
7676
if (!key) {
7777
this.key = getEnv('WORKOS_API_KEY');
78-
79-
if (!this.key) {
80-
throw new NoApiKeyProvidedException();
81-
}
8278
}
79+
// Note: Don't throw here - let subclasses decide validation policy
8380

8481
if (this.options.https === undefined) {
8582
this.options.https = true;
@@ -107,6 +104,20 @@ export class WorkOS {
107104
userAgent += ` ${name}: ${version}`;
108105
}
109106

107+
// Initialize all service clients
108+
this.auditLogs = new AuditLogs(this);
109+
this.directorySync = new DirectorySync(this);
110+
this.organizations = new Organizations(this);
111+
this.organizationDomains = new OrganizationDomains(this);
112+
this.passwordless = new Passwordless(this);
113+
this.portal = new Portal(this);
114+
this.sso = new SSO(this);
115+
this.mfa = new Mfa(this);
116+
this.events = new Events(this);
117+
this.fga = new FGA(this);
118+
this.widgets = new Widgets(this);
119+
this.vault = new Vault(this);
120+
110121
this.webhooks = this.createWebhookClient();
111122
this.actions = this.createActionsClient();
112123

@@ -129,13 +140,26 @@ export class WorkOS {
129140
}
130141

131142
createHttpClient(options: WorkOSOptions, userAgent: string) {
143+
const headers: Record<string, string> = {
144+
'User-Agent': userAgent,
145+
};
146+
147+
// Add existing headers if they exist
148+
if (options.config?.headers) {
149+
const existingHeaders = new Headers(options.config.headers);
150+
existingHeaders.forEach((value, key) => {
151+
headers[key] = value;
152+
});
153+
}
154+
155+
// Only add Authorization header if we have an API key
156+
if (this.key) {
157+
headers.Authorization = `Bearer ${this.key}`;
158+
}
159+
132160
return new FetchHttpClient(this.baseURL, {
133161
...options.config,
134-
headers: {
135-
...options.config?.headers,
136-
Authorization: `Bearer ${this.key}`,
137-
'User-Agent': userAgent,
138-
},
162+
headers,
139163
}) as HttpClient;
140164
}
141165

@@ -361,3 +385,20 @@ export class WorkOS {
361385
}
362386
}
363387
}
388+
389+
export class WorkOS extends WorkOSBase {
390+
constructor(
391+
key?: string,
392+
readonly options: WorkOSOptions = {},
393+
) {
394+
if (!key) {
395+
key = getEnv('WORKOS_API_KEY');
396+
397+
if (!key) {
398+
throw new NoApiKeyProvidedException();
399+
}
400+
}
401+
402+
super(key, options);
403+
}
404+
}

0 commit comments

Comments
 (0)