@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.
- package/jest.config.js +15 -0
- package/lib/core.d.ts +1 -1
- package/lib/core.js +31 -31
- package/lib/core.js.map +1 -1
- package/lib/errors/base-exception.d.ts +32 -0
- package/lib/errors/base-exception.js +238 -0
- package/lib/errors/base-exception.js.map +1 -0
- package/lib/errors/index.d.ts +2 -0
- package/lib/errors/index.js +20 -0
- package/lib/errors/index.js.map +1 -0
- package/lib/errors/specific-exceptions.d.ts +39 -0
- package/lib/errors/specific-exceptions.js +90 -0
- package/lib/errors/specific-exceptions.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.d.ts +34 -87
- package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.js +31 -120
- package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.d.ts +19 -10
- package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.js +18 -9
- package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.d.ts +209 -6
- package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.js +272 -5
- package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.d.ts +29 -0
- package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.js +40 -1
- package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.d.ts +25 -0
- package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.js +38 -1
- package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.d.ts +21 -1
- package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.js +20 -0
- package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.d.ts +110 -5
- package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.js +164 -5
- package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/users/users_connect.d.ts +48 -1
- package/lib/pkg/grpc/scalekit/v1/users/users_connect.js +47 -0
- package/lib/pkg/grpc/scalekit/v1/users/users_connect.js.map +1 -1
- package/lib/pkg/grpc/scalekit/v1/users/users_pb.d.ts +280 -4
- package/lib/pkg/grpc/scalekit/v1/users/users_pb.js +449 -11
- package/lib/pkg/grpc/scalekit/v1/users/users_pb.js.map +1 -1
- package/lib/scalekit.d.ts +3 -3
- package/lib/scalekit.js +35 -22
- package/lib/scalekit.js.map +1 -1
- package/lib/types/user.d.ts +1 -1
- package/lib/user.d.ts +10 -3
- package/lib/user.js +26 -5
- package/lib/user.js.map +1 -1
- package/package.json +6 -2
- package/src/core.ts +31 -32
- package/src/errors/base-exception.ts +262 -0
- package/src/errors/index.ts +3 -0
- package/src/errors/specific-exceptions.ts +88 -0
- package/src/index.ts +3 -1
- package/src/pkg/grpc/scalekit/v1/commons/commons_pb.ts +49 -129
- package/src/pkg/grpc/scalekit/v1/connections/connections_connect.ts +19 -10
- package/src/pkg/grpc/scalekit/v1/connections/connections_pb.ts +377 -8
- package/src/pkg/grpc/scalekit/v1/domains/domains_pb.ts +44 -0
- package/src/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.ts +49 -0
- package/src/pkg/grpc/scalekit/v1/organizations/organizations_connect.ts +21 -1
- package/src/pkg/grpc/scalekit/v1/organizations/organizations_pb.ts +218 -5
- package/src/pkg/grpc/scalekit/v1/users/users_connect.ts +48 -1
- package/src/pkg/grpc/scalekit/v1/users/users_pb.ts +558 -6
- package/src/scalekit.ts +39 -23
- package/src/types/user.ts +1 -1
- package/src/user.ts +34 -7
- package/tests/README.md +25 -0
- package/tests/connection.test.ts +42 -0
- package/tests/directory.test.ts +46 -0
- package/tests/organization.test.ts +65 -0
- package/tests/passwordless.test.ts +108 -0
- package/tests/scalekit.test.ts +104 -0
- package/tests/setup.ts +34 -0
- package/tests/users.test.ts +168 -0
- 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
|
-
*
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
351
|
+
throw new WebhookVerificationError("Invalid Signature Headers");
|
|
336
352
|
}
|
|
337
353
|
if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
338
|
-
throw new
|
|
354
|
+
throw new WebhookVerificationError("Message timestamp too old");
|
|
339
355
|
}
|
|
340
356
|
if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
341
|
-
throw 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
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.
|
|
71
|
-
request.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
201
|
-
request.
|
|
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
|
}
|
package/tests/README.md
ADDED
|
@@ -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
|
+
});
|