@scalekit-sdk/node 2.0.1 → 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 (77) 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 +3 -3
  45. package/lib/scalekit.js +35 -22
  46. package/lib/scalekit.js.map +1 -1
  47. package/lib/types/user.d.ts +1 -1
  48. package/lib/user.d.ts +10 -3
  49. package/lib/user.js +26 -5
  50. package/lib/user.js.map +1 -1
  51. package/package.json +6 -2
  52. package/src/core.ts +31 -32
  53. package/src/errors/base-exception.ts +262 -0
  54. package/src/errors/index.ts +3 -0
  55. package/src/errors/specific-exceptions.ts +88 -0
  56. package/src/index.ts +3 -1
  57. package/src/pkg/grpc/scalekit/v1/commons/commons_pb.ts +49 -129
  58. package/src/pkg/grpc/scalekit/v1/connections/connections_connect.ts +19 -10
  59. package/src/pkg/grpc/scalekit/v1/connections/connections_pb.ts +377 -8
  60. package/src/pkg/grpc/scalekit/v1/domains/domains_pb.ts +44 -0
  61. package/src/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.ts +49 -0
  62. package/src/pkg/grpc/scalekit/v1/organizations/organizations_connect.ts +21 -1
  63. package/src/pkg/grpc/scalekit/v1/organizations/organizations_pb.ts +218 -5
  64. package/src/pkg/grpc/scalekit/v1/users/users_connect.ts +48 -1
  65. package/src/pkg/grpc/scalekit/v1/users/users_pb.ts +558 -6
  66. package/src/scalekit.ts +39 -23
  67. package/src/types/user.ts +1 -1
  68. package/src/user.ts +34 -7
  69. package/tests/README.md +25 -0
  70. package/tests/connection.test.ts +42 -0
  71. package/tests/directory.test.ts +46 -0
  72. package/tests/organization.test.ts +65 -0
  73. package/tests/passwordless.test.ts +108 -0
  74. package/tests/scalekit.test.ts +104 -0
  75. package/tests/setup.ts +34 -0
  76. package/tests/users.test.ts +168 -0
  77. package/tests/utils/test-data.ts +248 -0
package/src/scalekit.ts CHANGED
@@ -12,6 +12,7 @@ import PasswordlessClient from './passwordless';
12
12
  import UserClient from './user';
13
13
  import { IdpInitiatedLoginClaims, IdTokenClaim, User } from './types/auth';
14
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";
@@ -226,7 +227,7 @@ export default class ScalekitClient {
226
227
  }
227
228
 
228
229
  /**
229
- * Verifies the payload of the webhook
230
+ * Verify webhook payload
230
231
  *
231
232
  * @param {string} secret The secret
232
233
  * @param {Record<string, string>} headers The headers
@@ -237,25 +238,40 @@ export default class ScalekitClient {
237
238
  const webhookId = headers['webhook-id'];
238
239
  const webhookTimestamp = headers['webhook-timestamp'];
239
240
  const webhookSignature = headers['webhook-signature'];
241
+
240
242
  if (!webhookId || !webhookTimestamp || !webhookSignature) {
241
- 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");
242
249
  }
243
- const timestamp = this.verifyTimestamp(webhookTimestamp);
244
- const data = `${webhookId}.${Math.floor(timestamp.getTime() / 1000)}.${payload}`;
245
- const secretBytes = Buffer.from(secret.split("_")[1], 'base64');
246
- const computedSignature = this.computeSignature(secretBytes, data);
247
- const receivedSignatures = webhookSignature.split(" ");
248
- for (const versionedSignature of receivedSignatures) {
249
- const [version, signature] = versionedSignature.split(",");
250
- if (version !== WEBHOOK_SIGNATURE_VERSION) {
251
- 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
+ }
252
266
  }
253
- if (crypto.timingSafeEqual(Buffer.from(signature, 'base64'), Buffer.from(computedSignature, 'base64'))) {
254
- return true;
267
+
268
+ throw new WebhookVerificationError("Invalid signature");
269
+ } catch (error) {
270
+ if (error instanceof WebhookVerificationError) {
271
+ throw error;
255
272
  }
273
+ throw new WebhookVerificationError("Invalid signature");
256
274
  }
257
-
258
- throw new Error("Invalid Signature");
259
275
  }
260
276
 
261
277
  /**
@@ -265,7 +281,7 @@ export default class ScalekitClient {
265
281
  * @param {string} token The token to be validated
266
282
  * @param {TokenValidationOptions} options Optional validation options for issuer, audience, and scopes
267
283
  * @return {Promise<T>} Returns the token payload if valid
268
- * @throws {Error} If token is invalid or missing required scopes
284
+ * @throws {ScalekitValidateTokenFailureException} If token is invalid or missing required scopes
269
285
  */
270
286
  async validateToken<T>(token: string, options?: TokenValidationOptions): Promise<T> {
271
287
  await this.coreClient.getJwks();
@@ -283,8 +299,8 @@ export default class ScalekitClient {
283
299
  }
284
300
 
285
301
  return payload;
286
- } catch (_) {
287
- throw new Error("Invalid token");
302
+ } catch (error) {
303
+ throw new ScalekitValidateTokenFailureException(error);
288
304
  }
289
305
  }
290
306
 
@@ -294,7 +310,7 @@ export default class ScalekitClient {
294
310
  * @param {string} token The token to verify
295
311
  * @param {string[]} requiredScopes The scopes that must be present in the token
296
312
  * @return {boolean} Returns true if all required scopes are present
297
- * @throws {Error} If required scopes are missing, with details about which scopes are missing
313
+ * @throws {ScalekitValidateTokenFailureException} If required scopes are missing, with details about which scopes are missing
298
314
  */
299
315
  verifyScopes(token: string, requiredScopes: string[]): boolean {
300
316
  const payload = jose.decodeJwt(token);
@@ -303,7 +319,7 @@ export default class ScalekitClient {
303
319
  const missingScopes = requiredScopes.filter(scope => !scopes.includes(scope));
304
320
 
305
321
  if (missingScopes.length > 0) {
306
- throw new Error(`Token missing required scopes: ${missingScopes.join(', ')}`);
322
+ throw new ScalekitValidateTokenFailureException(`Token missing required scopes: ${missingScopes.join(', ')}`);
307
323
  }
308
324
 
309
325
  return true;
@@ -332,13 +348,13 @@ export default class ScalekitClient {
332
348
  const now = Math.floor(Date.now() / 1000);
333
349
  const timestamp = parseInt(timestampStr, 10);
334
350
  if (isNaN(timestamp)) {
335
- throw new Error("Invalid timestamp");
351
+ throw new WebhookVerificationError("Invalid Signature Headers");
336
352
  }
337
353
  if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
338
- throw new Error("Message timestamp too old");
354
+ throw new WebhookVerificationError("Message timestamp too old");
339
355
  }
340
356
  if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
341
- throw new Error("Message timestamp too new");
357
+ throw new WebhookVerificationError("Message timestamp too new");
342
358
  }
343
359
 
344
360
  return new Date(timestamp * 1000);
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
+ });
@@ -0,0 +1,104 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import { AuthenticationOptions } from '../src/types/scalekit';
3
+ import { describe, it, expect, beforeEach } from '@jest/globals';
4
+ import { TestDataGenerator } from './utils/test-data';
5
+
6
+ describe('ScalekitClient', () => {
7
+ let client: ScalekitClient;
8
+
9
+ beforeEach(() => {
10
+ // Use global client
11
+ client = global.client;
12
+ });
13
+
14
+ describe('constructor', () => {
15
+ it('should initialize with correct parameters', () => {
16
+ expect(client).toBeInstanceOf(ScalekitClient);
17
+ expect(client.organization).toBeDefined();
18
+ expect(client.user).toBeDefined();
19
+ expect(client.connection).toBeDefined();
20
+ expect(client.directory).toBeDefined();
21
+ expect(client.passwordless).toBeDefined();
22
+ expect(client.domain).toBeDefined();
23
+ });
24
+ });
25
+
26
+ describe('getAuthorizationUrl', () => {
27
+ it('should generate authorization URL with basic parameters', () => {
28
+ const redirectUri = 'https://example.com/callback';
29
+ const url = client.getAuthorizationUrl(redirectUri);
30
+
31
+ expect(url).toContain('oauth/authorize');
32
+ expect(url).toContain(`redirect_uri=${encodeURIComponent(redirectUri)}`);
33
+ expect(url).toContain('response_type=code');
34
+ });
35
+
36
+ it('should include optional parameters when provided', () => {
37
+ const redirectUri = 'https://example.com/callback';
38
+ const options = TestDataGenerator.generateAuthorizationUrlOptions();
39
+
40
+ const url = client.getAuthorizationUrl(redirectUri, options);
41
+
42
+ expect(url).toContain('scope=openid%20profile');
43
+ expect(url).toContain('state=test-state');
44
+ expect(url).toContain('nonce=test-nonce');
45
+ expect(url).toContain('prompt=login');
46
+ });
47
+
48
+ it('should handle PKCE parameters', () => {
49
+ const redirectUri = 'https://example.com/callback';
50
+ const options = TestDataGenerator.generatePKCEParams();
51
+
52
+ const url = client.getAuthorizationUrl(redirectUri, options);
53
+
54
+ expect(url).toContain('code_challenge=test-challenge');
55
+ expect(url).toContain('code_challenge_method=S256');
56
+ });
57
+ });
58
+
59
+ describe('verifyWebhookPayload', () => {
60
+ it('should verify valid webhook payload', () => {
61
+ const webhookData = TestDataGenerator.generateWebhookData();
62
+
63
+ const result = client.verifyWebhookPayload(webhookData.secret, webhookData.headers, webhookData.payload);
64
+ expect(result).toBe(true);
65
+ });
66
+
67
+ it('should throw error for invalid signature', () => {
68
+ const webhookData = TestDataGenerator.generateWebhookData();
69
+
70
+ // Generate invalid signature using wrong payload data
71
+ const crypto = require('crypto');
72
+ const wrongData = `${webhookData.webhookId}.${webhookData.timestamp}.wrong-payload`;
73
+ const hmac = crypto.createHmac('sha256', Buffer.from('test-secret', 'base64'));
74
+ hmac.update(wrongData);
75
+ const wrongSignature = hmac.digest('base64');
76
+ const signature = `v1,${wrongSignature}`;
77
+
78
+ const headers = {
79
+ 'webhook-id': webhookData.webhookId,
80
+ 'webhook-timestamp': webhookData.timestamp,
81
+ 'webhook-signature': signature
82
+ };
83
+
84
+ expect(() => {
85
+ client.verifyWebhookPayload(webhookData.secret, headers, webhookData.payload);
86
+ }).toThrow('Invalid Signature');
87
+ });
88
+ });
89
+
90
+ describe('validateAccessToken', () => {
91
+ it('should validate access token', async () => {
92
+ // Mock token for testing - expected to fail
93
+ const token = 'mock-token';
94
+
95
+ try {
96
+ const result = await client.validateAccessToken(token);
97
+ expect(typeof result).toBe('boolean');
98
+ } catch (error) {
99
+ // Expected failure with mock token
100
+ expect(error).toBeDefined();
101
+ }
102
+ });
103
+ });
104
+ });
package/tests/setup.ts ADDED
@@ -0,0 +1,34 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import dotenv from 'dotenv';
3
+
4
+ // Import Jest globals
5
+ import { beforeAll } from '@jest/globals';
6
+
7
+ // Load environment variables
8
+ dotenv.config();
9
+
10
+ // Global test configuration
11
+ declare global {
12
+ var client: ScalekitClient;
13
+ }
14
+
15
+ beforeAll(() => {
16
+ // Validate required environment variables
17
+ const environmentUrl = process.env.SCALEKIT_ENVIRONMENT_URL;
18
+ const clientId = process.env.SCALEKIT_CLIENT_ID;
19
+ const clientSecret = process.env.SCALEKIT_CLIENT_SECRET;
20
+
21
+ // Check for required environment variables
22
+ if (!environmentUrl) {
23
+ throw new Error('SCALEKIT_ENVIRONMENT_URL environment variable is required');
24
+ }
25
+ if (!clientId) {
26
+ throw new Error('SCALEKIT_CLIENT_ID environment variable is required');
27
+ }
28
+ if (!clientSecret) {
29
+ throw new Error('SCALEKIT_CLIENT_SECRET environment variable is required');
30
+ }
31
+
32
+ // Initialize test client
33
+ global.client = new ScalekitClient(environmentUrl, clientId, clientSecret);
34
+ });