@scalekit-sdk/node 1.0.13 → 2.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/src/scalekit.ts CHANGED
@@ -9,10 +9,12 @@ import DirectoryClient from './directory';
9
9
  import DomainClient from './domain';
10
10
  import OrganizationClient from './organization';
11
11
  import PasswordlessClient from './passwordless';
12
+ import UserClient from './user';
12
13
  import { IdpInitiatedLoginClaims, IdTokenClaim, User } from './types/auth';
13
- import { AuthenticationOptions, AuthenticationResponse, AuthorizationUrlOptions, GrantType } from './types/scalekit';
14
+ import { AuthenticationOptions, AuthenticationResponse, AuthorizationUrlOptions, GrantType, LogoutUrlOptions, RefreshTokenResponse } from './types/scalekit';
14
15
 
15
16
  const authorizeEndpoint = "oauth/authorize";
17
+ const logoutEndpoint = "oidc/logout";
16
18
  const WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60; // 5 minutes
17
19
  const WEBHOOK_SIGNATURE_VERSION = "v1";
18
20
 
@@ -33,6 +35,7 @@ export default class ScalekitClient {
33
35
  readonly domain: DomainClient;
34
36
  readonly directory: DirectoryClient;
35
37
  readonly passwordless: PasswordlessClient;
38
+ readonly user: UserClient;
36
39
  constructor(
37
40
  envUrl: string,
38
41
  clientId: string,
@@ -67,6 +70,10 @@ export default class ScalekitClient {
67
70
  this.grpcConnect,
68
71
  this.coreClient
69
72
  );
73
+ this.user = new UserClient(
74
+ this.grpcConnect,
75
+ this.coreClient
76
+ );
70
77
  }
71
78
 
72
79
  /**
@@ -83,10 +90,14 @@ export default class ScalekitClient {
83
90
  * @param {string} options.provider Provider i.e. google, github, etc.
84
91
  * @param {string} options.codeChallenge Code challenge parameter in case of PKCE
85
92
  * @param {string} options.codeChallengeMethod Code challenge method parameter in case of PKCE
93
+ * @param {string} options.prompt Prompt parameter to control the authorization server's authentication behavior
86
94
  *
87
95
  * @example
88
96
  * const scalekit = new Scalekit(envUrl, clientId, clientSecret);
89
- * const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, { scopes: ['openid', 'profile'] });
97
+ * const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, {
98
+ * scopes: ['openid', 'profile'],
99
+ * prompt: 'create'
100
+ * });
90
101
  * @returns {string} authorization url
91
102
  */
92
103
  getAuthorizationUrl(
@@ -114,7 +125,8 @@ export default class ScalekitClient {
114
125
  ...(options.organizationId && { organization_id: options.organizationId }),
115
126
  ...(options.codeChallenge && { code_challenge: options.codeChallenge }),
116
127
  ...(options.codeChallengeMethod && { code_challenge_method: options.codeChallengeMethod }),
117
- ...(options.provider && { provider: options.provider })
128
+ ...(options.provider && { provider: options.provider }),
129
+ ...(options.prompt && { prompt: options.prompt })
118
130
  })
119
131
 
120
132
  return `${this.coreClient.envUrl}/${authorizeEndpoint}?${qs}`
@@ -141,7 +153,7 @@ export default class ScalekitClient {
141
153
  client_secret: this.coreClient.clientSecret,
142
154
  ...(options?.codeVerifier && { code_verifier: options.codeVerifier })
143
155
  }))
144
- const { id_token, access_token, expires_in } = res.data;
156
+ const { id_token, access_token, expires_in , refresh_token } = res.data;
145
157
  const claims = jose.decodeJwt<IdTokenClaim>(id_token);
146
158
  const user = <User>{};
147
159
  for (const [k, v] of Object.entries(claims)) {
@@ -154,7 +166,8 @@ export default class ScalekitClient {
154
166
  user,
155
167
  idToken: id_token,
156
168
  accessToken: access_token,
157
- expiresIn: expires_in
169
+ expiresIn: expires_in,
170
+ refreshToken: refresh_token
158
171
  }
159
172
  }
160
173
 
@@ -169,7 +182,7 @@ export default class ScalekitClient {
169
182
  }
170
183
 
171
184
  /**
172
- * Validates the access token.
185
+ * Validates the access token.
173
186
  *
174
187
  * @param {string} token The token to be validated.
175
188
  * @return {Promise<boolean>} Returns true if the token is valid, false otherwise.
@@ -183,6 +196,31 @@ export default class ScalekitClient {
183
196
  }
184
197
  }
185
198
 
199
+ /**
200
+ * Returns the logout URL that can be used to log out the user.
201
+ * @param {LogoutUrlOptions} options Logout URL options
202
+ * @param {string} options.idTokenHint The ID Token previously issued to the client
203
+ * @param {string} options.postLogoutRedirectUri URL to redirect after logout
204
+ * @param {string} options.state Opaque value to maintain state between request and callback
205
+ * @returns {string} The logout URL
206
+ *
207
+ * @example
208
+ * const scalekit = new Scalekit(envUrl, clientId, clientSecret);
209
+ * const logoutUrl = scalekit.getLogoutUrl({
210
+ * postLogoutRedirectUri: 'https://example.com',
211
+ * state: 'some-state'
212
+ * });
213
+ */
214
+ getLogoutUrl(options?: LogoutUrlOptions): string {
215
+ const qs = QueryString.stringify({
216
+ ...(options?.idTokenHint && { id_token_hint: options.idTokenHint }),
217
+ ...(options?.postLogoutRedirectUri && { post_logout_redirect_uri: options.postLogoutRedirectUri }),
218
+ ...(options?.state && { state: options.state })
219
+ });
220
+
221
+ return `${this.coreClient.envUrl}/${logoutEndpoint}${qs ? `?${qs}` : ''}`;
222
+ }
223
+
186
224
  /**
187
225
  * Verifies the payload of the webhook
188
226
  *
@@ -267,6 +305,48 @@ export default class ScalekitClient {
267
305
  private computeSignature(secretBytes: Buffer, data: string): string {
268
306
  return crypto.createHmac('sha256', secretBytes).update(data).digest('base64');
269
307
  }
270
- }
271
308
 
309
+ /**
310
+ * Refresh access token using a refresh token
311
+ * @param {string} refreshToken The refresh token to use
312
+ * @returns {Promise<RefreshTokenResponse>} Returns new access token, refresh token and other details
313
+ * @throws {Error} When authentication fails or response data is invalid
314
+ */
315
+ async refreshAccessToken(refreshToken: string): Promise<RefreshTokenResponse> {
316
+ if (!refreshToken) {
317
+ throw new Error("Refresh token is required");
318
+ }
319
+
320
+ let res;
321
+ try {
322
+ res = await this.coreClient.authenticate(QueryString.stringify({
323
+ grant_type: GrantType.RefreshToken,
324
+ client_id: this.coreClient.clientId,
325
+ client_secret: this.coreClient.clientSecret,
326
+ refresh_token: refreshToken
327
+ }));
328
+ } catch (error) {
329
+ throw new Error(`Failed to refresh token: ${error instanceof Error ? error.message : 'Unknown error'}`);
330
+ }
331
+
332
+ if (!res || !res.data) {
333
+ throw new Error("Invalid response from authentication server");
334
+ }
335
+
336
+ const { access_token, refresh_token } = res.data;
337
+
338
+ // Validate that all required properties exist
339
+ if (!access_token) {
340
+ throw new Error("Missing access_token in authentication response");
341
+ }
342
+ if (!refresh_token) {
343
+ throw new Error("Missing refresh_token in authentication response");
344
+ }
345
+
346
+ return {
347
+ accessToken: access_token,
348
+ refreshToken: refresh_token
349
+ };
350
+ }
351
+ }
272
352
 
package/src/types/auth.ts CHANGED
@@ -62,6 +62,7 @@ export type TokenResponse = {
62
62
  access_token: string;
63
63
  id_token: string;
64
64
  expires_in: number;
65
+ refresh_token: string;
65
66
  }
66
67
 
67
68
  export type IdpInitiatedLoginClaims ={
@@ -17,6 +17,7 @@ export type AuthorizationUrlOptions = {
17
17
  codeChallenge?: string;
18
18
  codeChallengeMethod?: string;
19
19
  provider?: string;
20
+ prompt?: string;
20
21
  }
21
22
 
22
23
  export type AuthenticationOptions = {
@@ -28,4 +29,16 @@ export type AuthenticationResponse = {
28
29
  idToken: string;
29
30
  accessToken: string;
30
31
  expiresIn: number;
32
+ refreshToken: string;
33
+ }
34
+
35
+ export type RefreshTokenResponse = {
36
+ accessToken: string;
37
+ refreshToken: string;
38
+ }
39
+
40
+ export interface LogoutUrlOptions {
41
+ idTokenHint?: string;
42
+ postLogoutRedirectUri?: string;
43
+ state?: string;
31
44
  }
@@ -0,0 +1,21 @@
1
+ export interface CreateUserRequest {
2
+ email: string;
3
+ externalId?: string;
4
+ phoneNumber?: string;
5
+ userProfile?: {
6
+ firstName?: string;
7
+ lastName?: string;
8
+ };
9
+ metadata?: Record<string, string>;
10
+ sendActivationEmail?: boolean;
11
+ }
12
+
13
+ export interface UpdateUserRequest {
14
+ email?: string;
15
+ externalId?: string;
16
+ userProfile?: {
17
+ firstName?: string;
18
+ lastName?: string;
19
+ };
20
+ metadata?: Record<string, string>;
21
+ }
package/src/user.ts ADDED
@@ -0,0 +1,291 @@
1
+ import { Empty, PartialMessage } from '@bufbuild/protobuf';
2
+ import { PromiseClient } from '@connectrpc/connect';
3
+ import GrpcConnect from './connect';
4
+ import CoreClient from './core';
5
+ import { UserService } from './pkg/grpc/scalekit/v1/users/users_connect';
6
+ import {
7
+ CreateUserAndMembershipRequest,
8
+ CreateUserAndMembershipResponse,
9
+ DeleteUserRequest,
10
+ GetUserRequest,
11
+ GetUserResponse,
12
+ ListUsersRequest,
13
+ ListUsersResponse,
14
+ UpdateUserRequest,
15
+ UpdateUserResponse,
16
+ User,
17
+ UpdateUser,
18
+ CreateUser,
19
+ CreateUserProfile,
20
+ CreateMembershipRequest,
21
+ CreateMembershipResponse,
22
+ DeleteMembershipRequest,
23
+ UpdateMembershipRequest,
24
+ UpdateMembershipResponse,
25
+ ListOrganizationUsersRequest,
26
+ ListOrganizationUsersResponse,
27
+ CreateMembership,
28
+ UpdateMembership
29
+ } from './pkg/grpc/scalekit/v1/users/users_pb';
30
+ import { CreateUserRequest, UpdateUserRequest as UpdateUserRequestType } from './types/user';
31
+
32
+ export default class UserClient {
33
+ private client: PromiseClient<typeof UserService>;
34
+
35
+ constructor(
36
+ private readonly grpcConnect: GrpcConnect,
37
+ private readonly coreClient: CoreClient
38
+ ) {
39
+ this.client = this.grpcConnect.createClient(UserService);
40
+ }
41
+
42
+ /**
43
+ * Create a new user and add them to an organization
44
+ * @param {string} organizationId The organization id
45
+ * @param {CreateUserRequest} options The user creation options
46
+ * @returns {Promise<CreateUserAndMembershipResponse>} The created user
47
+ */
48
+ async createUserAndMembership(organizationId: string, options: CreateUserRequest): Promise<CreateUserAndMembershipResponse> {
49
+ if (!organizationId) {
50
+ throw new Error('organizationId is required');
51
+ }
52
+ if (!options.email) {
53
+ throw new Error('email is required');
54
+ }
55
+
56
+ const user = new CreateUser({
57
+ email: options.email,
58
+ userProfile: options.userProfile ? new CreateUserProfile({
59
+ firstName: options.userProfile.firstName,
60
+ lastName: options.userProfile.lastName
61
+ }) : undefined,
62
+ metadata: options.metadata
63
+ });
64
+
65
+ const request: PartialMessage<CreateUserAndMembershipRequest> = {
66
+ organizationId,
67
+ user
68
+ };
69
+
70
+ if (options.sendActivationEmail !== undefined) {
71
+ request.sendActivationEmail = options.sendActivationEmail;
72
+ }
73
+
74
+ const response = await this.coreClient.connectExec(
75
+ this.client.createUserAndMembership,
76
+ request
77
+ );
78
+
79
+ if (!response.user) {
80
+ throw new Error('Failed to create user');
81
+ }
82
+
83
+ return response;
84
+ }
85
+
86
+ /**
87
+ * Get a user by id
88
+ * @param {string} userId The user id
89
+ * @returns {Promise<GetUserResponse>} The user
90
+ */
91
+ async getUser(userId: string): Promise<GetUserResponse> {
92
+ return this.coreClient.connectExec(
93
+ this.client.getUser,
94
+ {
95
+ identities: {
96
+ case: 'id',
97
+ value: userId
98
+ }
99
+ }
100
+ );
101
+ }
102
+
103
+ /**
104
+ * List users with pagination
105
+ * @param {object} options The pagination options
106
+ * @param {number} options.pageSize The page size
107
+ * @param {string} options.pageToken The page token
108
+ * @returns {Promise<ListUsersResponse>} The list of users
109
+ */
110
+ async listUsers(options?: {
111
+ pageSize?: number,
112
+ pageToken?: string
113
+ }): Promise<ListUsersResponse> {
114
+ return this.coreClient.connectExec(
115
+ this.client.listUsers,
116
+ {
117
+ pageSize: options?.pageSize,
118
+ pageToken: options?.pageToken
119
+ }
120
+ );
121
+ }
122
+
123
+ /**
124
+ * Update a user
125
+ * @param {string} userId The user id
126
+ * @param {UpdateUserRequestType} options The update options
127
+ * @returns {Promise<UpdateUserResponse>} The updated user
128
+ */
129
+ async updateUser(userId: string, options: UpdateUserRequestType): Promise<UpdateUserResponse> {
130
+ const updateUser = new UpdateUser({
131
+ userProfile: options.userProfile ? {
132
+ firstName: options.userProfile.firstName,
133
+ lastName: options.userProfile.lastName
134
+ } : undefined,
135
+ metadata: options.metadata
136
+ });
137
+
138
+ return this.coreClient.connectExec(
139
+ this.client.updateUser,
140
+ {
141
+ identities: {
142
+ case: 'id',
143
+ value: userId
144
+ },
145
+ user: updateUser
146
+ }
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Delete a user
152
+ * @param {string} userId The user id
153
+ * @returns {Promise<Empty>} Empty response
154
+ */
155
+ async deleteUser(userId: string): Promise<Empty> {
156
+ return this.coreClient.connectExec(
157
+ this.client.deleteUser,
158
+ {
159
+ identities: {
160
+ case: 'id',
161
+ value: userId
162
+ }
163
+ }
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Create a membership for a user in an organization
169
+ * @param {string} organizationId The organization id
170
+ * @param {string} userId The user id
171
+ * @param {object} options The membership options
172
+ * @param {string[]} options.roles The roles to assign
173
+ * @param {Record<string, string>} options.metadata Optional metadata
174
+ * @param {boolean} options.sendActivationEmail Whether to send activation email
175
+ * @returns {Promise<CreateMembershipResponse>} The response with updated user
176
+ */
177
+ async createMembership(
178
+ organizationId: string,
179
+ userId: string,
180
+ options: {
181
+ roles?: string[],
182
+ metadata?: Record<string, string>,
183
+ sendActivationEmail?: boolean
184
+ } = {}
185
+ ): Promise<CreateMembershipResponse> {
186
+ const membership = new CreateMembership({
187
+ roles: options.roles?.map(role => ({ name: role })) || [],
188
+ metadata: options.metadata || {}
189
+ });
190
+
191
+ const request: PartialMessage<CreateMembershipRequest> = {
192
+ organizationId,
193
+ identities: {
194
+ case: 'id',
195
+ value: userId
196
+ },
197
+ membership
198
+ };
199
+
200
+ if (options.sendActivationEmail !== undefined) {
201
+ request.sendActivationEmail = options.sendActivationEmail;
202
+ }
203
+
204
+ return this.coreClient.connectExec(
205
+ this.client.createMembership,
206
+ request
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Delete a user's membership from an organization
212
+ * @param {string} organizationId The organization id
213
+ * @param {string} userId The user id
214
+ * @returns {Promise<Empty>} Empty response
215
+ */
216
+ async deleteMembership(
217
+ organizationId: string,
218
+ userId: string
219
+ ): Promise<Empty> {
220
+ return this.coreClient.connectExec(
221
+ this.client.deleteMembership,
222
+ {
223
+ organizationId,
224
+ identities: {
225
+ case: 'id',
226
+ value: userId
227
+ }
228
+ }
229
+ );
230
+ }
231
+
232
+ /**
233
+ * Update a user's membership in an organization
234
+ * @param {string} organizationId The organization id
235
+ * @param {string} userId The user id
236
+ * @param {object} options The update options
237
+ * @param {string[]} options.roles The roles to assign
238
+ * @param {Record<string, string>} options.metadata Optional metadata
239
+ * @returns {Promise<UpdateMembershipResponse>} The response with updated user
240
+ */
241
+ async updateMembership(
242
+ organizationId: string,
243
+ userId: string,
244
+ options: {
245
+ roles?: string[],
246
+ metadata?: Record<string, string>
247
+ } = {}
248
+ ): Promise<UpdateMembershipResponse> {
249
+ const membership = new UpdateMembership({
250
+ roles: options.roles?.map(role => ({ name: role })) || [],
251
+ metadata: options.metadata || {}
252
+ });
253
+
254
+ return this.coreClient.connectExec(
255
+ this.client.updateMembership,
256
+ {
257
+ organizationId,
258
+ identities: {
259
+ case: 'id',
260
+ value: userId
261
+ },
262
+ membership
263
+ }
264
+ );
265
+ }
266
+
267
+ /**
268
+ * List users in an organization with pagination
269
+ * @param {string} organizationId The organization id
270
+ * @param {object} options The pagination options
271
+ * @param {number} options.pageSize The page size
272
+ * @param {string} options.pageToken The page token
273
+ * @returns {Promise<ListOrganizationUsersResponse>} The list of users in the organization
274
+ */
275
+ async listOrganizationUsers(
276
+ organizationId: string,
277
+ options?: {
278
+ pageSize?: number,
279
+ pageToken?: string
280
+ }
281
+ ): Promise<ListOrganizationUsersResponse> {
282
+ return this.coreClient.connectExec(
283
+ this.client.listOrganizationUsers,
284
+ {
285
+ organizationId,
286
+ pageSize: options?.pageSize,
287
+ pageToken: options?.pageToken
288
+ }
289
+ );
290
+ }
291
+ }