@scalekit-sdk/node 2.0.0 → 2.1.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 (79) hide show
  1. package/jest.config.js +15 -0
  2. package/lib/core.d.ts +1 -1
  3. package/lib/core.js +31 -31
  4. package/lib/core.js.map +1 -1
  5. package/lib/errors/base-exception.d.ts +32 -0
  6. package/lib/errors/base-exception.js +238 -0
  7. package/lib/errors/base-exception.js.map +1 -0
  8. package/lib/errors/index.d.ts +2 -0
  9. package/lib/errors/index.js +20 -0
  10. package/lib/errors/index.js.map +1 -0
  11. package/lib/errors/specific-exceptions.d.ts +39 -0
  12. package/lib/errors/specific-exceptions.js +90 -0
  13. package/lib/errors/specific-exceptions.js.map +1 -0
  14. package/lib/index.d.ts +1 -0
  15. package/lib/index.js +1 -0
  16. package/lib/index.js.map +1 -1
  17. package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.d.ts +34 -87
  18. package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.js +31 -120
  19. package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.js.map +1 -1
  20. package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.d.ts +19 -10
  21. package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.js +18 -9
  22. package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.js.map +1 -1
  23. package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.d.ts +209 -6
  24. package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.js +272 -5
  25. package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.js.map +1 -1
  26. package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.d.ts +29 -0
  27. package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.js +40 -1
  28. package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.js.map +1 -1
  29. package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.d.ts +25 -0
  30. package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.js +38 -1
  31. package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.js.map +1 -1
  32. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.d.ts +21 -1
  33. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.js +20 -0
  34. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.js.map +1 -1
  35. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.d.ts +110 -5
  36. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.js +164 -5
  37. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.js.map +1 -1
  38. package/lib/pkg/grpc/scalekit/v1/users/users_connect.d.ts +48 -1
  39. package/lib/pkg/grpc/scalekit/v1/users/users_connect.js +47 -0
  40. package/lib/pkg/grpc/scalekit/v1/users/users_connect.js.map +1 -1
  41. package/lib/pkg/grpc/scalekit/v1/users/users_pb.d.ts +280 -4
  42. package/lib/pkg/grpc/scalekit/v1/users/users_pb.js +449 -11
  43. package/lib/pkg/grpc/scalekit/v1/users/users_pb.js.map +1 -1
  44. package/lib/scalekit.d.ts +29 -8
  45. package/lib/scalekit.js +78 -28
  46. package/lib/scalekit.js.map +1 -1
  47. package/lib/types/scalekit.d.ts +5 -0
  48. package/lib/types/user.d.ts +1 -1
  49. package/lib/user.d.ts +10 -3
  50. package/lib/user.js +26 -5
  51. package/lib/user.js.map +1 -1
  52. package/package.json +6 -2
  53. package/src/core.ts +31 -32
  54. package/src/errors/base-exception.ts +262 -0
  55. package/src/errors/index.ts +3 -0
  56. package/src/errors/specific-exceptions.ts +88 -0
  57. package/src/index.ts +3 -1
  58. package/src/pkg/grpc/scalekit/v1/commons/commons_pb.ts +49 -129
  59. package/src/pkg/grpc/scalekit/v1/connections/connections_connect.ts +19 -10
  60. package/src/pkg/grpc/scalekit/v1/connections/connections_pb.ts +377 -8
  61. package/src/pkg/grpc/scalekit/v1/domains/domains_pb.ts +44 -0
  62. package/src/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.ts +49 -0
  63. package/src/pkg/grpc/scalekit/v1/organizations/organizations_connect.ts +21 -1
  64. package/src/pkg/grpc/scalekit/v1/organizations/organizations_pb.ts +218 -5
  65. package/src/pkg/grpc/scalekit/v1/users/users_connect.ts +48 -1
  66. package/src/pkg/grpc/scalekit/v1/users/users_pb.ts +558 -6
  67. package/src/scalekit.ts +95 -30
  68. package/src/types/scalekit.ts +6 -0
  69. package/src/types/user.ts +1 -1
  70. package/src/user.ts +34 -7
  71. package/tests/README.md +25 -0
  72. package/tests/connection.test.ts +42 -0
  73. package/tests/directory.test.ts +46 -0
  74. package/tests/organization.test.ts +65 -0
  75. package/tests/passwordless.test.ts +108 -0
  76. package/tests/scalekit.test.ts +104 -0
  77. package/tests/setup.ts +34 -0
  78. package/tests/users.test.ts +168 -0
  79. package/tests/utils/test-data.ts +248 -0
package/src/scalekit.ts CHANGED
@@ -11,7 +11,8 @@ import OrganizationClient from './organization';
11
11
  import PasswordlessClient from './passwordless';
12
12
  import UserClient from './user';
13
13
  import { IdpInitiatedLoginClaims, IdTokenClaim, User } from './types/auth';
14
- import { AuthenticationOptions, AuthenticationResponse, AuthorizationUrlOptions, GrantType, LogoutUrlOptions, RefreshTokenResponse } from './types/scalekit';
14
+ import { AuthenticationOptions, AuthenticationResponse, AuthorizationUrlOptions, GrantType, LogoutUrlOptions, RefreshTokenResponse ,TokenValidationOptions } from './types/scalekit';
15
+ import { WebhookVerificationError, ScalekitValidateTokenFailureException } from './errors/base-exception';
15
16
 
16
17
  const authorizeEndpoint = "oauth/authorize";
17
18
  const logoutEndpoint = "oidc/logout";
@@ -175,27 +176,31 @@ export default class ScalekitClient {
175
176
  * Get the idp initiated login claims
176
177
  *
177
178
  * @param {string} idpInitiatedLoginToken The idp_initiated_login query param from the URL
179
+ * @param {TokenValidationOptions} options Optional validation options for issuer and audience
178
180
  * @returns {object} Returns the idp initiated login claims
179
181
  */
180
- async getIdpInitiatedLoginClaims(idpInitiatedLoginToken: string): Promise<IdpInitiatedLoginClaims> {
181
- return this.validateToken<IdpInitiatedLoginClaims>(idpInitiatedLoginToken);
182
+ async getIdpInitiatedLoginClaims(idpInitiatedLoginToken: string, options?: TokenValidationOptions): Promise<IdpInitiatedLoginClaims> {
183
+ return this.validateToken<IdpInitiatedLoginClaims>(idpInitiatedLoginToken, options);
182
184
  }
183
185
 
184
186
  /**
185
- * Validates the access token.
187
+ * Validates the access token and returns a boolean result.
186
188
  *
187
189
  * @param {string} token The token to be validated.
190
+ * @param {TokenValidationOptions} options Optional validation options for issuer, audience, and scopes
188
191
  * @return {Promise<boolean>} Returns true if the token is valid, false otherwise.
189
192
  */
190
- async validateAccessToken(token: string): Promise<boolean> {
193
+ async validateAccessToken(token: string, options?: TokenValidationOptions): Promise<boolean> {
191
194
  try {
192
- await this.validateToken(token);
195
+ await this.validateToken(token, options);
193
196
  return true;
194
197
  } catch (_) {
195
198
  return false;
196
199
  }
197
200
  }
198
201
 
202
+
203
+
199
204
  /**
200
205
  * Returns the logout URL that can be used to log out the user.
201
206
  * @param {LogoutUrlOptions} options Logout URL options
@@ -222,7 +227,7 @@ export default class ScalekitClient {
222
227
  }
223
228
 
224
229
  /**
225
- * Verifies the payload of the webhook
230
+ * Verify webhook payload
226
231
  *
227
232
  * @param {string} secret The secret
228
233
  * @param {Record<string, string>} headers The headers
@@ -233,46 +238,106 @@ export default class ScalekitClient {
233
238
  const webhookId = headers['webhook-id'];
234
239
  const webhookTimestamp = headers['webhook-timestamp'];
235
240
  const webhookSignature = headers['webhook-signature'];
241
+
236
242
  if (!webhookId || !webhookTimestamp || !webhookSignature) {
237
- throw new Error("Missing required headers");
243
+ throw new WebhookVerificationError("Missing required headers");
244
+ }
245
+
246
+ const secretParts = secret.split("_");
247
+ if (secretParts.length < 2) {
248
+ throw new WebhookVerificationError("Invalid secret");
238
249
  }
239
- const timestamp = this.verifyTimestamp(webhookTimestamp);
240
- const data = `${webhookId}.${Math.floor(timestamp.getTime() / 1000)}.${payload}`;
241
- const secretBytes = Buffer.from(secret.split("_")[1], 'base64');
242
- const computedSignature = this.computeSignature(secretBytes, data);
243
- const receivedSignatures = webhookSignature.split(" ");
244
- for (const versionedSignature of receivedSignatures) {
245
- const [version, signature] = versionedSignature.split(",");
246
- if (version !== WEBHOOK_SIGNATURE_VERSION) {
247
- continue;
250
+
251
+ try {
252
+ const timestamp = this.verifyTimestamp(webhookTimestamp);
253
+ const data = `${webhookId}.${Math.floor(timestamp.getTime() / 1000)}.${payload}`;
254
+ const secretBytes = Buffer.from(secretParts[1], 'base64');
255
+ const computedSignature = this.computeSignature(secretBytes, data);
256
+ const receivedSignatures = webhookSignature.split(" ");
257
+
258
+ for (const versionedSignature of receivedSignatures) {
259
+ const [version, signature] = versionedSignature.split(",");
260
+ if (version !== WEBHOOK_SIGNATURE_VERSION) {
261
+ continue;
262
+ }
263
+ if (crypto.timingSafeEqual(Buffer.from(signature, 'base64'), Buffer.from(computedSignature, 'base64'))) {
264
+ return true;
265
+ }
248
266
  }
249
- if (crypto.timingSafeEqual(Buffer.from(signature, 'base64'), Buffer.from(computedSignature, 'base64'))) {
250
- return true;
267
+
268
+ throw new WebhookVerificationError("Invalid signature");
269
+ } catch (error) {
270
+ if (error instanceof WebhookVerificationError) {
271
+ throw error;
251
272
  }
273
+ throw new WebhookVerificationError("Invalid signature");
252
274
  }
253
-
254
- throw new Error("Invalid Signature");
255
275
  }
256
276
 
257
277
  /**
258
- * Validate token
278
+ * Validates a token and returns its payload if valid.
279
+ * Supports issuer, audience, and scope validation.
259
280
  *
260
281
  * @param {string} token The token to be validated
261
- * @return {Promise<T>} Returns the payload of the token
282
+ * @param {TokenValidationOptions} options Optional validation options for issuer, audience, and scopes
283
+ * @return {Promise<T>} Returns the token payload if valid
284
+ * @throws {ScalekitValidateTokenFailureException} If token is invalid or missing required scopes
262
285
  */
263
- private async validateToken<T>(token: string): Promise<T> {
286
+ async validateToken<T>(token: string, options?: TokenValidationOptions): Promise<T> {
264
287
  await this.coreClient.getJwks();
265
288
  const jwks = jose.createLocalJWKSet({
266
289
  keys: this.coreClient.keys
267
290
  })
268
291
  try {
269
- const { payload } = await jose.jwtVerify<T>(token, jwks);
292
+ const { payload } = await jose.jwtVerify<T>(token, jwks, {
293
+ ...(options?.issuer && { issuer: options.issuer }),
294
+ ...(options?.audience && { audience: options.audience })
295
+ });
296
+
297
+ if (options?.requiredScopes && options.requiredScopes.length > 0) {
298
+ this.verifyScopes(token, options.requiredScopes);
299
+ }
300
+
270
301
  return payload;
271
- } catch (_) {
272
- throw new Error("Invalid token");
302
+ } catch (error) {
303
+ throw new ScalekitValidateTokenFailureException(error);
273
304
  }
274
305
  }
275
306
 
307
+ /**
308
+ * Verify that the token contains the required scopes
309
+ *
310
+ * @param {string} token The token to verify
311
+ * @param {string[]} requiredScopes The scopes that must be present in the token
312
+ * @return {boolean} Returns true if all required scopes are present
313
+ * @throws {ScalekitValidateTokenFailureException} If required scopes are missing, with details about which scopes are missing
314
+ */
315
+ verifyScopes(token: string, requiredScopes: string[]): boolean {
316
+ const payload = jose.decodeJwt(token);
317
+ const scopes = this.extractScopesFromPayload(payload);
318
+
319
+ const missingScopes = requiredScopes.filter(scope => !scopes.includes(scope));
320
+
321
+ if (missingScopes.length > 0) {
322
+ throw new ScalekitValidateTokenFailureException(`Token missing required scopes: ${missingScopes.join(', ')}`);
323
+ }
324
+
325
+ return true;
326
+ }
327
+
328
+ /**
329
+ * Extract scopes from token payload
330
+ *
331
+ * @param {any} payload The token payload
332
+ * @return {string[]} Array of scopes found in the token
333
+ */
334
+ private extractScopesFromPayload(payload: Record<string, any>): string[] {
335
+ const scopes = payload.scopes;
336
+ return Array.isArray(scopes)
337
+ ? scopes.filter((scope) => !!scope.trim?.())
338
+ : [];
339
+ }
340
+
276
341
  /**
277
342
  * Verify the timestamp
278
343
  *
@@ -283,13 +348,13 @@ export default class ScalekitClient {
283
348
  const now = Math.floor(Date.now() / 1000);
284
349
  const timestamp = parseInt(timestampStr, 10);
285
350
  if (isNaN(timestamp)) {
286
- throw new Error("Invalid timestamp");
351
+ throw new WebhookVerificationError("Invalid Signature Headers");
287
352
  }
288
353
  if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
289
- throw new Error("Message timestamp too old");
354
+ throw new WebhookVerificationError("Message timestamp too old");
290
355
  }
291
356
  if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
292
- throw new Error("Message timestamp too new");
357
+ throw new WebhookVerificationError("Message timestamp too new");
293
358
  }
294
359
 
295
360
  return new Date(timestamp * 1000);
@@ -24,6 +24,12 @@ export type AuthenticationOptions = {
24
24
  codeVerifier?: string;
25
25
  }
26
26
 
27
+ export type TokenValidationOptions = {
28
+ issuer?: string;
29
+ audience?: string[];
30
+ requiredScopes?: string[];
31
+ }
32
+
27
33
  export type AuthenticationResponse = {
28
34
  user: User;
29
35
  idToken: string;
package/src/types/user.ts CHANGED
@@ -7,7 +7,7 @@ export interface CreateUserRequest {
7
7
  lastName?: string;
8
8
  };
9
9
  metadata?: Record<string, string>;
10
- sendActivationEmail?: boolean;
10
+ sendInvitationEmail?: boolean;
11
11
  }
12
12
 
13
13
  export interface UpdateUserRequest {
package/src/user.ts CHANGED
@@ -25,7 +25,9 @@ import {
25
25
  ListOrganizationUsersRequest,
26
26
  ListOrganizationUsersResponse,
27
27
  CreateMembership,
28
- UpdateMembership
28
+ UpdateMembership,
29
+ ResendInviteRequest,
30
+ ResendInviteResponse
29
31
  } from './pkg/grpc/scalekit/v1/users/users_pb';
30
32
  import { CreateUserRequest, UpdateUserRequest as UpdateUserRequestType } from './types/user';
31
33
 
@@ -67,8 +69,8 @@ export default class UserClient {
67
69
  user
68
70
  };
69
71
 
70
- if (options.sendActivationEmail !== undefined) {
71
- request.sendActivationEmail = options.sendActivationEmail;
72
+ if (options.sendInvitationEmail !== undefined) {
73
+ request.sendInvitationEmail = options.sendInvitationEmail;
72
74
  }
73
75
 
74
76
  const response = await this.coreClient.connectExec(
@@ -171,7 +173,7 @@ export default class UserClient {
171
173
  * @param {object} options The membership options
172
174
  * @param {string[]} options.roles The roles to assign
173
175
  * @param {Record<string, string>} options.metadata Optional metadata
174
- * @param {boolean} options.sendActivationEmail Whether to send activation email
176
+ * @param {boolean} options.sendInvitationEmail Whether to send invitation email
175
177
  * @returns {Promise<CreateMembershipResponse>} The response with updated user
176
178
  */
177
179
  async createMembership(
@@ -180,7 +182,7 @@ export default class UserClient {
180
182
  options: {
181
183
  roles?: string[],
182
184
  metadata?: Record<string, string>,
183
- sendActivationEmail?: boolean
185
+ sendInvitationEmail?: boolean
184
186
  } = {}
185
187
  ): Promise<CreateMembershipResponse> {
186
188
  const membership = new CreateMembership({
@@ -197,8 +199,8 @@ export default class UserClient {
197
199
  membership
198
200
  };
199
201
 
200
- if (options.sendActivationEmail !== undefined) {
201
- request.sendActivationEmail = options.sendActivationEmail;
202
+ if (options.sendInvitationEmail !== undefined) {
203
+ request.sendInvitationEmail = options.sendInvitationEmail;
202
204
  }
203
205
 
204
206
  return this.coreClient.connectExec(
@@ -288,4 +290,29 @@ export default class UserClient {
288
290
  }
289
291
  );
290
292
  }
293
+
294
+ /**
295
+ * Resend an invitation to a user
296
+ * @param {string} organizationId The organization id
297
+ * @param {string} userId The user id
298
+ * @returns {Promise<ResendInviteResponse>} The response with the invite
299
+ */
300
+ async resendInvite(organizationId: string, userId: string): Promise<ResendInviteResponse> {
301
+ if (!organizationId) {
302
+ throw new Error('organizationId is required');
303
+ }
304
+ if (!userId) {
305
+ throw new Error('userId is required');
306
+ }
307
+
308
+ const request = new ResendInviteRequest({
309
+ organizationId,
310
+ id: userId
311
+ });
312
+
313
+ return this.coreClient.connectExec(
314
+ this.client.resendInvite,
315
+ request
316
+ );
317
+ }
291
318
  }
@@ -0,0 +1,25 @@
1
+ # Scalekit Node SDK Tests
2
+
3
+ This directory contains the test suite for the Scalekit Node SDK.
4
+
5
+ ## Setup
6
+
7
+ 1. Create a `.env` file in the root directory with the following environment variables:
8
+
9
+ ```env
10
+ # Required for client initialization
11
+ SCALEKIT_ENVIRONMENT_URL= "Your Scalekit environment URL."
12
+ SCALEKIT_CLIENT_ID= " Your Scalekit environment client id "
13
+ SCALEKIT_CLIENT_SECRET= " Your Scalekit environment client secret "
14
+ ```
15
+
16
+ 2. Install dependencies:
17
+ ```bash
18
+ npm install
19
+ ```
20
+
21
+ ## Running Tests
22
+
23
+ - Run all tests: `npm test`
24
+ - Run tests in watch mode: `npm run test:watch`
25
+
@@ -0,0 +1,42 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
3
+ import { TestOrganizationManager } from './utils/test-data';
4
+
5
+ describe('Connections', () => {
6
+ let client: ScalekitClient;
7
+ let testOrg: string;
8
+
9
+ beforeEach(async () => {
10
+ // Use global client
11
+ client = global.client;
12
+
13
+ // Create test organization for each test
14
+ testOrg = await TestOrganizationManager.createTestOrganization(client);
15
+ });
16
+
17
+ afterEach(async () => {
18
+ // Clean up test organization
19
+ await TestOrganizationManager.cleanupTestOrganization(client, testOrg);
20
+ });
21
+
22
+ describe('listConnections', () => {
23
+ it('should list connections by organization', async () => {
24
+ const connections = await client.connection.listConnections(testOrg);
25
+
26
+ expect(connections).toBeDefined();
27
+ expect(connections.connections).toBeDefined();
28
+ expect(Array.isArray(connections.connections)).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe('listConnectionsByDomain', () => {
33
+ it('should list connections by domain', async () => {
34
+ const domain = 'example.com';
35
+ const connections = await client.connection.listConnectionsByDomain(domain);
36
+
37
+ expect(connections).toBeDefined();
38
+ expect(connections.connections).toBeDefined();
39
+ expect(Array.isArray(connections.connections)).toBe(true);
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,46 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
3
+ import { TestOrganizationManager } from './utils/test-data';
4
+
5
+ describe('Directories', () => {
6
+ let client: ScalekitClient;
7
+ let testOrg: string;
8
+
9
+ beforeEach(async () => {
10
+ // Use global client
11
+ client = global.client;
12
+
13
+ // Create test organization for each test
14
+ testOrg = await TestOrganizationManager.createTestOrganization(client);
15
+ });
16
+
17
+ afterEach(async () => {
18
+ // Clean up test organization
19
+ await TestOrganizationManager.cleanupTestOrganization(client, testOrg);
20
+ });
21
+
22
+ describe('listDirectories', () => {
23
+ it('should list directories', async () => {
24
+ const directories = await client.directory.listDirectories(testOrg);
25
+
26
+ expect(directories).toBeDefined();
27
+ expect(directories.directories).toBeDefined();
28
+ expect(Array.isArray(directories.directories)).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe('getPrimaryDirectoryByOrganizationId', () => {
33
+ it('should get primary directory by organization id', async () => {
34
+ try {
35
+ const primaryDirectory = await client.directory.getPrimaryDirectoryByOrganizationId(testOrg);
36
+
37
+ expect(primaryDirectory).toBeDefined();
38
+ expect(primaryDirectory.id).toBeDefined();
39
+ expect(primaryDirectory.organizationId).toBe(testOrg);
40
+ } catch (error) {
41
+ // Expected when no directories exist
42
+ expect(error).toBeDefined();
43
+ }
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,65 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
3
+ import { TestDataGenerator, TestOrganizationManager } from './utils/test-data';
4
+
5
+ describe('Organizations', () => {
6
+ let client: ScalekitClient;
7
+ let testOrg: string;
8
+
9
+ beforeEach(async () => {
10
+ // Use global client
11
+ client = global.client;
12
+
13
+ // Create test organization for each test
14
+ testOrg = await TestOrganizationManager.createTestOrganization(client);
15
+ });
16
+
17
+ afterEach(async () => {
18
+ // Clean up test organization
19
+ await TestOrganizationManager.cleanupTestOrganization(client, testOrg);
20
+ });
21
+
22
+ describe('listOrganization', () => {
23
+ it('should list organizations', async () => {
24
+ const organizations = await client.organization.listOrganization(TestDataGenerator.generatePaginationParams());
25
+
26
+ expect(organizations).toBeDefined();
27
+ expect(organizations.organizations).toBeDefined();
28
+ expect(Array.isArray(organizations.organizations)).toBe(true);
29
+ expect(organizations.organizations.length).toBeGreaterThan(0);
30
+
31
+ // Verify basic organization attributes
32
+ const firstOrg = organizations.organizations[0];
33
+ expect(firstOrg.id).toBeDefined();
34
+ expect(firstOrg.displayName).toBeDefined();
35
+ });
36
+
37
+ it('should handle pagination', async () => {
38
+ const firstPage = await client.organization.listOrganization(TestDataGenerator.generatePaginationParams(5));
39
+
40
+ expect(firstPage).toBeDefined();
41
+ expect(firstPage.organizations.length).toBeLessThanOrEqual(5);
42
+
43
+ if (firstPage.nextPageToken) {
44
+ const secondPage = await client.organization.listOrganization({
45
+ pageSize: 5,
46
+ pageToken: firstPage.nextPageToken
47
+ });
48
+
49
+ expect(secondPage).toBeDefined();
50
+ expect(secondPage.organizations).toBeDefined();
51
+ }
52
+ });
53
+ });
54
+
55
+ describe('getOrganization', () => {
56
+ it('should get organization by ID', async () => {
57
+ const organization = await client.organization.getOrganization(testOrg);
58
+
59
+ expect(organization).toBeDefined();
60
+ expect(organization.organization).toBeDefined();
61
+ expect(organization.organization?.id).toBe(testOrg);
62
+ expect(organization.organization?.displayName).toBeDefined();
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,108 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import { TemplateType } from '../src/pkg/grpc/scalekit/v1/auth/passwordless_pb';
3
+ import { describe, it, expect, beforeEach } from '@jest/globals';
4
+ import { TestDataGenerator } from './utils/test-data';
5
+
6
+ describe('Passwordless', () => {
7
+ let client: ScalekitClient;
8
+
9
+ beforeEach(() => {
10
+ // Use global client
11
+ client = global.client;
12
+ });
13
+
14
+ describe('sendPasswordlessEmail', () => {
15
+ it('should send passwordless email with basic parameters', async () => {
16
+ const email = TestDataGenerator.generateUniqueEmail();
17
+
18
+ const response = await client.passwordless.sendPasswordlessEmail(email, TestDataGenerator.generatePasswordlessEmailData());
19
+
20
+ expect(response).toBeDefined();
21
+ expect(response.authRequestId).toBeDefined();
22
+ expect(response.expiresAt).toBeDefined();
23
+ expect(response.expiresIn).toBe(3600);
24
+ expect(response.passwordlessType).toBeDefined();
25
+ });
26
+
27
+ it('should send passwordless email with template variables', async () => {
28
+ const email = TestDataGenerator.generateUniqueEmail();
29
+
30
+ const response = await client.passwordless.sendPasswordlessEmail(email, TestDataGenerator.generatePasswordlessEmailWithTemplateData());
31
+
32
+ expect(response).toBeDefined();
33
+ expect(response.authRequestId).toBeDefined();
34
+ });
35
+
36
+ it('should throw error for invalid email', async () => {
37
+ await expect(
38
+ client.passwordless.sendPasswordlessEmail('', {
39
+ template: TemplateType.SIGNIN
40
+ })
41
+ ).rejects.toThrow('Email must be a valid string');
42
+ });
43
+
44
+ it('should throw error for invalid template type', async () => {
45
+ const email = TestDataGenerator.generateUniqueEmail();
46
+
47
+ await expect(
48
+ client.passwordless.sendPasswordlessEmail(email, {
49
+ template: 'INVALID_TEMPLATE' as any
50
+ })
51
+ ).rejects.toThrow('Invalid template type');
52
+ });
53
+ });
54
+
55
+ describe('verifyPasswordlessEmail', () => {
56
+ it('should throw error when neither code nor linkToken is provided', async () => {
57
+ await expect(
58
+ client.passwordless.verifyPasswordlessEmail({})
59
+ ).rejects.toThrow('Either code or linkToken must be provided');
60
+ });
61
+
62
+ it('should verify with code', async () => {
63
+ // Mock code for testing - expected to fail
64
+ const credential = TestDataGenerator.generateCredentialData('code');
65
+
66
+ try {
67
+ const response = await client.passwordless.verifyPasswordlessEmail(credential);
68
+ expect(response).toBeDefined();
69
+ } catch (error) {
70
+ // Expected failure with mock code
71
+ expect(error).toBeDefined();
72
+ }
73
+ });
74
+
75
+ it('should verify with linkToken', async () => {
76
+ // Mock linkToken for testing - expected to fail
77
+ const credential = TestDataGenerator.generateCredentialData('linkToken');
78
+
79
+ try {
80
+ const response = await client.passwordless.verifyPasswordlessEmail(credential);
81
+ expect(response).toBeDefined();
82
+ } catch (error) {
83
+ // Expected failure with mock linkToken
84
+ expect(error).toBeDefined();
85
+ }
86
+ });
87
+ });
88
+
89
+ describe('resendPasswordlessEmail', () => {
90
+ it('should resend passwordless email', async () => {
91
+ // Send initial passwordless email
92
+ const email = TestDataGenerator.generateUniqueEmail();
93
+
94
+ const sendResponse = await client.passwordless.sendPasswordlessEmail(email, TestDataGenerator.generatePasswordlessEmailData());
95
+
96
+ // Resend the email
97
+ const resendResponse = await client.passwordless.resendPasswordlessEmail(
98
+ sendResponse.authRequestId
99
+ );
100
+
101
+ expect(resendResponse).toBeDefined();
102
+ expect(resendResponse.authRequestId).toBeDefined();
103
+ expect(resendResponse.expiresAt).toBeDefined();
104
+ expect(resendResponse.expiresIn).toBeDefined();
105
+ expect(resendResponse.passwordlessType).toBeDefined();
106
+ });
107
+ });
108
+ });