@scalekit-sdk/node 2.2.0 → 2.2.2

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 (62) hide show
  1. package/{reference.md → REFERENCE.md} +530 -77
  2. package/lib/core.js +1 -1
  3. package/package.json +9 -3
  4. package/.github/dependabot.yml +0 -10
  5. package/.nvmrc +0 -1
  6. package/buf.gen.yaml +0 -20
  7. package/jest.config.js +0 -15
  8. package/src/auth.ts +0 -99
  9. package/src/connect.ts +0 -32
  10. package/src/connection.ts +0 -267
  11. package/src/constants/user.ts +0 -22
  12. package/src/core.ts +0 -139
  13. package/src/directory.ts +0 -431
  14. package/src/domain.ts +0 -273
  15. package/src/errors/base-exception.ts +0 -263
  16. package/src/errors/index.ts +0 -3
  17. package/src/errors/specific-exceptions.ts +0 -88
  18. package/src/index.ts +0 -10
  19. package/src/organization.ts +0 -571
  20. package/src/passwordless.ts +0 -139
  21. package/src/permission.ts +0 -310
  22. package/src/pkg/grpc/buf/validate/validate_pb.ts +0 -28
  23. package/src/pkg/grpc/google/api/annotations_pb.ts +0 -28
  24. package/src/pkg/grpc/google/api/field_behavior_pb.ts +0 -28
  25. package/src/pkg/grpc/google/api/visibility_pb.ts +0 -28
  26. package/src/pkg/grpc/protoc-gen-openapiv2/options/annotations_pb.ts +0 -28
  27. package/src/pkg/grpc/scalekit/v1/auditlogs/auditlogs_pb.ts +0 -257
  28. package/src/pkg/grpc/scalekit/v1/auth/auth_pb.ts +0 -836
  29. package/src/pkg/grpc/scalekit/v1/auth/passwordless_pb.ts +0 -264
  30. package/src/pkg/grpc/scalekit/v1/auth/webauthn_pb.ts +0 -794
  31. package/src/pkg/grpc/scalekit/v1/commons/commons_pb.ts +0 -452
  32. package/src/pkg/grpc/scalekit/v1/connections/connections_pb.ts +0 -2645
  33. package/src/pkg/grpc/scalekit/v1/directories/directories_pb.ts +0 -1393
  34. package/src/pkg/grpc/scalekit/v1/domains/domains_pb.ts +0 -599
  35. package/src/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.ts +0 -311
  36. package/src/pkg/grpc/scalekit/v1/options/options_pb.ts +0 -200
  37. package/src/pkg/grpc/scalekit/v1/organizations/organizations_pb.ts +0 -1141
  38. package/src/pkg/grpc/scalekit/v1/roles/roles_pb.ts +0 -1491
  39. package/src/pkg/grpc/scalekit/v1/sessions/sessions_pb.ts +0 -497
  40. package/src/pkg/grpc/scalekit/v1/users/users_pb.ts +0 -1404
  41. package/src/role.ts +0 -463
  42. package/src/scalekit.ts +0 -800
  43. package/src/session.ts +0 -323
  44. package/src/types/auth.ts +0 -73
  45. package/src/types/organization.ts +0 -12
  46. package/src/types/scalekit.ts +0 -50
  47. package/src/types/user.ts +0 -21
  48. package/src/user.ts +0 -829
  49. package/src/webauthn.ts +0 -99
  50. package/tests/README.md +0 -25
  51. package/tests/connection.test.ts +0 -42
  52. package/tests/directory.test.ts +0 -46
  53. package/tests/domain.test.ts +0 -293
  54. package/tests/organization.test.ts +0 -81
  55. package/tests/passwordless.test.ts +0 -108
  56. package/tests/permission.test.ts +0 -399
  57. package/tests/role.test.ts +0 -323
  58. package/tests/scalekit.test.ts +0 -104
  59. package/tests/setup.ts +0 -34
  60. package/tests/users.test.ts +0 -168
  61. package/tests/utils/test-data.ts +0 -490
  62. package/tsconfig.json +0 -19
package/src/scalekit.ts DELETED
@@ -1,800 +0,0 @@
1
- import crypto from 'crypto';
2
- import * as jose from 'jose';
3
- import QueryString from 'qs';
4
- import GrpcConnect from './connect';
5
- import ConnectionClient from './connection';
6
- import { IdTokenClaimToUserMap } from './constants/user';
7
- import CoreClient from './core';
8
- import DirectoryClient from './directory';
9
- import DomainClient from './domain';
10
- import AuthClient from './auth';
11
- import OrganizationClient from './organization';
12
- import PasswordlessClient from './passwordless';
13
- import UserClient from './user';
14
- import SessionClient from './session';
15
- import RoleClient from './role';
16
- import PermissionClient from './permission';
17
- import WebAuthnClient from './webauthn';
18
- import { IdpInitiatedLoginClaims, IdTokenClaim, User } from './types/auth';
19
- import { AuthenticationOptions, AuthenticationResponse, AuthorizationUrlOptions, GrantType, LogoutUrlOptions, RefreshTokenResponse ,TokenValidationOptions } from './types/scalekit';
20
- import { WebhookVerificationError, ScalekitValidateTokenFailureException } from './errors/base-exception';
21
-
22
- const authorizeEndpoint = "oauth/authorize";
23
- const logoutEndpoint = "oidc/logout";
24
- const WEBHOOK_TOLERANCE_IN_SECONDS = 5 * 60; // 5 minutes
25
- const WEBHOOK_SIGNATURE_VERSION = "v1";
26
-
27
- /**
28
- * Main Scalekit SDK client for interacting with all Scalekit API endpoints.
29
- *
30
- * TIP: You can use it as a singleton object - that is you can initialize it just once and use the same client variable wherever required.
31
- *
32
- * This is the primary entry point for interacting with Scalekit's authentication services,
33
- * including SSO, SCIM, user management, roles, permissions, and passwordless authentication.
34
- *
35
- * You can find the Environment URL, Client ID and Client Secret in Scalekit Dashboard -> Developers (Settings) -> API Credentials
36
- *
37
- * @param {string} envUrl - The Scalekit environment URL (e.g., "https://yourorg.scalekit.com" or your configured custom domain like "https://auth.yourapp.ai")
38
- * @param {string} clientId - Your Scalekit client ID from the Scalekit Dashboard
39
- * @param {string} clientSecret - Your Scalekit client secret from the Scalekit Dashboard
40
- *
41
- * @example
42
- * // Initialize the Scalekit client
43
- * import { ScalekitClient } from '@scalekit-sdk/node';
44
- *
45
- * const scalekitClient = new ScalekitClient(
46
- * process.env.SCALEKIT_ENV_URL,
47
- * process.env.SCALEKIT_CLIENT_ID,
48
- * process.env.SCALEKIT_CLIENT_SECRET
49
- * );
50
- *
51
- * // Access various client modules
52
- * const organizations = await scalekitClient.organization.listOrganization();
53
- * const users = await scalekitClient.user.listUsers();
54
- *
55
- * @see {@link https://docs.scalekit.com/apis/ | Scalekit API Documentation}
56
- */
57
- export default class ScalekitClient {
58
- private readonly coreClient: CoreClient;
59
- private readonly grpcConnect: GrpcConnect;
60
- readonly organization: OrganizationClient;
61
- readonly connection: ConnectionClient;
62
- readonly domain: DomainClient;
63
- readonly directory: DirectoryClient;
64
- readonly passwordless: PasswordlessClient;
65
- readonly user: UserClient;
66
- readonly session: SessionClient;
67
- readonly role: RoleClient;
68
- readonly permission: PermissionClient;
69
- readonly auth: AuthClient;
70
- readonly webauthn: WebAuthnClient;
71
- constructor(
72
- envUrl: string,
73
- clientId: string,
74
- clientSecret: string
75
- ) {
76
- this.coreClient = new CoreClient(
77
- envUrl,
78
- clientId,
79
- clientSecret
80
- );
81
- this.grpcConnect = new GrpcConnect(
82
- this.coreClient
83
- );
84
-
85
- this.organization = new OrganizationClient(
86
- this.grpcConnect,
87
- this.coreClient
88
- );
89
- this.connection = new ConnectionClient(this.grpcConnect, this.coreClient);
90
- this.domain = new DomainClient(this.grpcConnect, this.coreClient);
91
- this.directory = new DirectoryClient(this.grpcConnect, this.coreClient);
92
- this.passwordless = new PasswordlessClient(
93
- this.grpcConnect,
94
- this.coreClient
95
- );
96
- this.user = new UserClient(
97
- this.grpcConnect,
98
- this.coreClient
99
- );
100
- this.session = new SessionClient(
101
- this.grpcConnect,
102
- this.coreClient
103
- );
104
- this.role = new RoleClient(
105
- this.grpcConnect,
106
- this.coreClient
107
- );
108
- this.permission = new PermissionClient(
109
- this.grpcConnect,
110
- this.coreClient
111
- );
112
- this.auth = new AuthClient(
113
- this.grpcConnect,
114
- this.coreClient
115
- );
116
- this.webauthn = new WebAuthnClient(
117
- this.grpcConnect,
118
- this.coreClient
119
- );
120
- }
121
-
122
- /**
123
- * Utility method to generate the OAuth 2.0 authorization URL to initiate the SSO authentication flow.
124
- *
125
- * This method doesn't make any network calls but instead generates a fully formed Authorization URL
126
- * as a string that you can redirect your users to initiate authentication.
127
- *
128
- * @param {string} redirectUri - The URL where users will be redirected after authentication.
129
- * Must match one of the redirect URIs configured in your Scalekit dashboard.
130
- * @param {AuthorizationUrlOptions} [options] - Optional configuration for the authorization request
131
- * @param {string[]} [options.scopes=['openid', 'profile', 'email']] - OAuth scopes to request. Default includes openid, profile, and email.
132
- * @param {string} [options.state] - Opaque value to maintain state between request and callback. Used to prevent CSRF attacks.
133
- * @param {string} [options.nonce] - String value used to associate a client session with an ID Token.
134
- * @param {string} [options.loginHint] - Hint to the authorization server about the login identifier the user might use (e.g., email address).
135
- * @param {string} [options.domainHint] - Domain hint to identify which organization's IdP to use for authentication.
136
- * @param {string} [options.connectionId] - Specific SSO connection ID to use for authentication.
137
- * @param {string} [options.organizationId] - Organization ID to authenticate against.
138
- * @param {string} [options.provider] - Social login provider (e.g., 'google', 'github', 'microsoft').
139
- * @param {string} [options.codeChallenge] - PKCE code challenge for enhanced security in public clients.
140
- * @param {string} [options.codeChallengeMethod] - Method used to generate the code challenge (we support only 'S256').
141
- * @param {string} [options.prompt] - Controls the authorization server's authentication behavior (e.g., 'login', 'consent', 'create').
142
- *
143
- * @returns {string} The complete authorization URL to redirect the user to
144
- *
145
- * @example
146
- * // Initiate Enterprise SSO authentication for a given org_id
147
- * const authUrl = scalekitClient.getAuthorizationUrl(
148
- * 'https://yourapp.com/auth/callback',
149
- * {
150
- * state: 'random-state-value',
151
- * organizationId: 'org_123456'
152
- * }
153
- * );
154
- * // Redirect user to authUrl
155
- *
156
- * @example
157
- * // Initiate Enterprise SSO authentication for a specific connection id
158
- * // optionally, pass the loginhint to the 3rd party identity provider.
159
- * const authUrl = scalekitClient.getAuthorizationUrl(
160
- * 'https://yourapp.com/auth/callback',
161
- * {
162
- * connectionId: 'conn_abc123',
163
- * loginHint: 'user@company.com'
164
- * }
165
- * );
166
- *
167
- * @example
168
- * // Social login
169
- * const authUrl = scalekitClient.getAuthorizationUrl(
170
- * 'https://yourapp.com/auth/callback',
171
- * {
172
- * provider: 'google',
173
- * state: 'random-state'
174
- * }
175
- * );
176
- *
177
- * @example
178
- * // PKCE flow for public clients
179
- * const authUrl = scalekitClient.getAuthorizationUrl(
180
- * 'https://yourapp.com/auth/callback',
181
- * {
182
- * codeChallenge: 'your-code-challenge',
183
- * codeChallengeMethod: 'S256',
184
- * organizationId: 'org_123456'
185
- * }
186
- * );
187
- *
188
- * @see {@link https://docs.scalekit.com/apis/#tag/api%20auth | Authentication API Documentation}
189
- * @see {@link authenticateWithCode} - Use this method to exchange the authorization code for tokens
190
- */
191
- getAuthorizationUrl(
192
- redirectUri: string,
193
- options?: AuthorizationUrlOptions
194
- ): string {
195
- const defaultOptions: AuthorizationUrlOptions = {
196
- scopes: ["openid", "profile", "email"],
197
- };
198
- options = {
199
- ...defaultOptions,
200
- ...options,
201
- };
202
- const qs = QueryString.stringify({
203
- response_type: "code",
204
- client_id: this.coreClient.clientId,
205
- redirect_uri: redirectUri,
206
- scope: options.scopes?.join(" "),
207
- ...(options.state && { state: options.state }),
208
- ...(options.nonce && { nonce: options.nonce }),
209
- ...(options.loginHint && { login_hint: options.loginHint }),
210
- ...(options.domainHint && { domain_hint: options.domainHint }),
211
- ...(options.domainHint && { domain: options.domainHint }),
212
- ...(options.connectionId && { connection_id: options.connectionId }),
213
- ...(options.organizationId && {
214
- organization_id: options.organizationId,
215
- }),
216
- ...(options.codeChallenge && { code_challenge: options.codeChallenge }),
217
- ...(options.codeChallengeMethod && {
218
- code_challenge_method: options.codeChallengeMethod,
219
- }),
220
- ...(options.provider && { provider: options.provider }),
221
- ...(options.prompt && { prompt: options.prompt }),
222
- });
223
-
224
- return `${this.coreClient.envUrl}/${authorizeEndpoint}?${qs}`;
225
- }
226
-
227
- /**
228
- * Exchanges an authorization code for access tokens and user information.
229
- *
230
- * This method completes the OAuth 2.0 authorization code flow by exchanging the code
231
- * received in the callback for access tokens, ID tokens, and user profile information.
232
- * Call this method in your redirect URI handler after receiving the authorization code.
233
- *
234
- * @param {string} code - The authorization code received in the callback URL after user authentication
235
- * @param {string} redirectUri - The same redirect URI used in getAuthorizationUrl(). Must match exactly.
236
- * @param {AuthenticationOptions} [options] - Optional authentication configuration
237
- * @param {string} [options.codeVerifier] - PKCE code verifier to validate the code challenge (required if PKCE was used)
238
- *
239
- * @returns {Promise<AuthenticationResponse>} Authentication response containing:
240
- * - user: User profile information (email, name, organization, etc.)
241
- * - idToken: JWT ID token containing user claims
242
- * - accessToken: Access token for API authorization
243
- * - expiresIn: Token expiration time in seconds
244
- * - refreshToken: Refresh token for obtaining new access tokens
245
- *
246
- * @throws {Error} When the authorization code is invalid, expired, or already used
247
- * @throws {Error} When the redirect URI doesn't match the one used in authorization
248
- * @throws {Error} When PKCE code verifier is invalid or missing
249
- *
250
- * @example
251
- * // Basic code exchange (server-side flow)
252
- * app.get('/auth/callback', async (req, res) => {
253
- * const { code } = req.query;
254
- *
255
- * try {
256
- * const result = await scalekitClient.authenticateWithCode(
257
- * code,
258
- * 'https://yourapp.com/auth/callback'
259
- * );
260
- *
261
- * // Store tokens securely
262
- * req.session.accessToken = result.accessToken;
263
- * req.session.user = result.user;
264
- *
265
- * res.redirect('/dashboard');
266
- * } catch (error) {
267
- * console.error('Authentication failed:', error);
268
- * res.redirect('/login?error=auth_failed');
269
- * }
270
- * });
271
- *
272
- * @example
273
- * // PKCE flow (for public clients)
274
- * app.get('/auth/callback', async (req, res) => {
275
- * const { code } = req.query;
276
- * const codeVerifier = req.session.codeVerifier; // Stored during authorization
277
- *
278
- * const result = await scalekitClient.authenticateWithCode(
279
- * code,
280
- * 'https://yourapp.com/auth/callback',
281
- * { codeVerifier }
282
- * );
283
- *
284
- * // Use result.user, result.accessToken, etc.
285
- * });
286
- *
287
- * @see {@link getAuthorizationUrl} - Generate the authorization URL first
288
- * @see {@link validateAccessToken} - Validate tokens in subsequent requests
289
- */
290
- async authenticateWithCode(
291
- code: string,
292
- redirectUri: string,
293
- options?: AuthenticationOptions
294
- ): Promise<AuthenticationResponse> {
295
- const res = await this.coreClient.authenticate(
296
- QueryString.stringify({
297
- code: code,
298
- redirect_uri: redirectUri,
299
- grant_type: GrantType.AuthorizationCode,
300
- client_id: this.coreClient.clientId,
301
- client_secret: this.coreClient.clientSecret,
302
- ...(options?.codeVerifier && { code_verifier: options.codeVerifier }),
303
- })
304
- );
305
- const { id_token, access_token, expires_in, refresh_token } = res.data;
306
- const claims = jose.decodeJwt<IdTokenClaim>(id_token);
307
- const user = <User>{};
308
- for (const [k, v] of Object.entries(claims)) {
309
- if (IdTokenClaimToUserMap[k]) {
310
- user[IdTokenClaimToUserMap[k]] = v;
311
- }
312
- }
313
-
314
- return {
315
- user,
316
- idToken: id_token,
317
- accessToken: access_token,
318
- expiresIn: expires_in,
319
- refreshToken: refresh_token,
320
- };
321
- }
322
-
323
- /**
324
- * Extracts and validates claims from an IdP-initiated login token.
325
- *
326
- * Use this method when handling IdP-initiated SSO flows, where the authentication is
327
- * initiated from the identity provider's portal rather than your application. This validates
328
- * the token and returns the necessary information to initiate a new SP Initiated SSO workflow.
329
- *
330
- * @param {string} idpInitiatedLoginToken - The token received in the 'idp_initiated_login' query parameter
331
- * @param {TokenValidationOptions} [options] - Optional token validation configuration
332
- * @param {string} [options.issuer] - Expected token issuer for validation
333
- * @param {string} [options.audience] - Expected token audience for validation
334
- *
335
- * @returns {Promise<IdpInitiatedLoginClaims>} Claims containing:
336
- * - connection_id: The SSO connection identifier
337
- * - organization_id: The organization identifier
338
- * - login_hint: User's email or login identifier
339
- * - relay_state: Optional state parameter from the IdP
340
- *
341
- * @throws {ScalekitValidateTokenFailureException} When token validation fails
342
- *
343
- * @example
344
- * // Handle IdP-initiated login
345
- * app.get('/auth/callback', async (req, res) => {
346
- * const { idp_initiated_login } = req.query;
347
- *
348
- * if (idp_initiated_login) {
349
- * try {
350
- * const claims = await scalekitClient.getIdpInitiatedLoginClaims(idp_initiated_login);
351
- *
352
- * // Redirect to authorization URL with the claims
353
- * const authUrl = scalekitClient.getAuthorizationUrl(
354
- * 'https://yourapp.com/auth/callback',
355
- * {
356
- * connectionId: claims.connection_id,
357
- * organizationId: claims.organization_id,
358
- * loginHint: claims.login_hint,
359
- * ...(claims.relay_state && { state: claims.relay_state })
360
- * }
361
- * );
362
- *
363
- * return res.redirect(authUrl);
364
- * } catch (error) {
365
- * console.error('IdP-initiated login failed:', error);
366
- * return res.redirect('/login?error=idp_login_failed');
367
- * }
368
- * }
369
- * // Handle normal callback flow...
370
- * });
371
- *
372
- * @see {@link https://docs.scalekit.com/sso/guides/idp-init-sso/ | IdP-Initiated SSO Documentation}
373
- * @see {@link getAuthorizationUrl} - Use the claims to construct the authorization URL
374
- */
375
- async getIdpInitiatedLoginClaims(
376
- idpInitiatedLoginToken: string,
377
- options?: TokenValidationOptions
378
- ): Promise<IdpInitiatedLoginClaims> {
379
- return this.validateToken<IdpInitiatedLoginClaims>(
380
- idpInitiatedLoginToken,
381
- options
382
- );
383
- }
384
-
385
- /**
386
- * Validates the access token and returns a boolean result.
387
- *
388
- * @param {string} token The token to be validated.
389
- * @param {TokenValidationOptions} options Optional validation options for issuer, audience, and scopes
390
- * @return {Promise<boolean>} Returns true if the token is valid, false otherwise.
391
- */
392
- async validateAccessToken(
393
- token: string,
394
- options?: TokenValidationOptions
395
- ): Promise<boolean> {
396
- try {
397
- await this.validateToken(token, options);
398
- return true;
399
- } catch (_) {
400
- return false;
401
- }
402
- }
403
-
404
- /**
405
- * Returns the logout URL that can be used to log out the user.
406
- * @param {LogoutUrlOptions} options Logout URL options
407
- * @param {string} options.idTokenHint The ID Token previously issued to the client
408
- * @param {string} options.postLogoutRedirectUri URL to redirect after logout
409
- * @param {string} options.state Opaque value to maintain state between request and callback
410
- * @returns {string} The logout URL
411
- *
412
- * @example
413
- * const scalekit = new Scalekit(envUrl, clientId, clientSecret);
414
- * const logoutUrl = scalekit.getLogoutUrl({
415
- * postLogoutRedirectUri: 'https://example.com',
416
- * state: 'some-state'
417
- * });
418
- */
419
- getLogoutUrl(options?: LogoutUrlOptions): string {
420
- const qs = QueryString.stringify({
421
- ...(options?.idTokenHint && { id_token_hint: options.idTokenHint }),
422
- ...(options?.postLogoutRedirectUri && {
423
- post_logout_redirect_uri: options.postLogoutRedirectUri,
424
- }),
425
- ...(options?.state && { state: options.state }),
426
- });
427
-
428
- return `${this.coreClient.envUrl}/${logoutEndpoint}${qs ? `?${qs}` : ""}`;
429
- }
430
-
431
- /**
432
- * Verifies the authenticity and integrity of webhook payloads from Scalekit.
433
- *
434
- * Use this method to validate webhook requests from Scalekit by verifying the HMAC signature.
435
- * This ensures the webhook was sent by Scalekit and hasn't been tampered with. The method
436
- * checks the signature and timestamp to prevent replay attacks (5-minute tolerance window).
437
- *
438
- * @param {string} secret - Your webhook signing secret from the Scalekit dashboard (format: 'whsec_...')
439
- * @param {Record<string, string>} headers - The HTTP headers from the webhook request
440
- * @param {string} payload - The raw webhook request body as a string
441
- *
442
- * @returns {boolean} Returns true if the webhook signature is valid
443
- *
444
- * @throws {WebhookVerificationError} When required headers are missing
445
- * @throws {WebhookVerificationError} When the secret format is invalid
446
- * @throws {WebhookVerificationError} When the signature doesn't match
447
- * @throws {WebhookVerificationError} When the timestamp is too old (>5 minutes) or in the future
448
- *
449
- * @example
450
- * // Express.js webhook handler
451
- * app.post('/webhooks/scalekit', express.raw({ type: 'application/json' }), (req, res) => {
452
- * const secret = process.env.SCALEKIT_WEBHOOK_SECRET;
453
- * const headers = req.headers;
454
- * const payload = req.body.toString();
455
- *
456
- * try {
457
- * const isValid = scalekitClient.verifyWebhookPayload(secret, headers, payload);
458
- *
459
- * if (isValid) {
460
- * const event = JSON.parse(payload);
461
- *
462
- * // Process the webhook event
463
- * switch (event.type) {
464
- * case 'user.created':
465
- * console.log('New user created:', event.data);
466
- * break;
467
- * case 'connection.enabled':
468
- * console.log('Connection enabled:', event.data);
469
- * break;
470
- * }
471
- *
472
- * res.status(200).send('Webhook received');
473
- * }
474
- * } catch (error) {
475
- * console.error('Webhook verification failed:', error);
476
- * res.status(400).send('Invalid webhook signature');
477
- * }
478
- * });
479
- *
480
- * @see {@link https://docs.scalekit.com/reference/webhooks/overview/ | Webhook Documentation}
481
- * @see {@link verifyInterceptorPayload} - Similar method for interceptor payloads
482
- */
483
- verifyWebhookPayload(
484
- secret: string,
485
- headers: Record<string, string>,
486
- payload: string
487
- ): boolean {
488
- const webhookId = headers["webhook-id"];
489
- const webhookTimestamp = headers["webhook-timestamp"];
490
- const webhookSignature = headers["webhook-signature"];
491
-
492
- return this.verifyPayloadSignature(
493
- secret,
494
- webhookId,
495
- webhookTimestamp,
496
- webhookSignature,
497
- payload
498
- );
499
- }
500
-
501
- /**
502
- * Verifies the authenticity and integrity of interceptor payloads from Scalekit.
503
- *
504
- * Use this method to validate HTTP interceptor requests from Scalekit by verifying the HMAC signature.
505
- * This ensures the interceptor payload was sent by Scalekit and hasn't been tampered with. The method
506
- * checks the signature and timestamp to prevent replay attacks (5-minute tolerance window)
507
- *
508
- * @param {string} secret Your interceptor signing secret that you can copy from Scalekit Dashboard
509
- * @param {Record<string, string>} headers The HTTP headers from the interceptor request
510
- * @param {string} payload The raw interceptor request body as a string
511
- * @return {boolean} Returns true if the interceptor payload is valid.
512
- */
513
- verifyInterceptorPayload(
514
- secret: string,
515
- headers: Record<string, string>,
516
- payload: string
517
- ): boolean {
518
- const interceptorId = headers["interceptor-id"];
519
- const interceptorTimestamp = headers["interceptor-timestamp"];
520
- const interceptorSignature = headers["interceptor-signature"];
521
-
522
- return this.verifyPayloadSignature(
523
- secret,
524
- interceptorId,
525
- interceptorTimestamp,
526
- interceptorSignature,
527
- payload
528
- );
529
- }
530
-
531
- /**
532
- * Common payload signature verification logic
533
- *
534
- * @param {string} secret The secret
535
- * @param {string} id The webhook/interceptor id
536
- * @param {string} timestamp The timestamp
537
- * @param {string} signature The signature
538
- * @param {string} payload The payload
539
- * @return {boolean} Returns true if the payload signature is valid.
540
- */
541
- private verifyPayloadSignature(
542
- secret: string,
543
- id: string,
544
- timestamp: string,
545
- signature: string,
546
- payload: string
547
- ): boolean {
548
- if (!id || !timestamp || !signature) {
549
- throw new WebhookVerificationError("Missing required headers");
550
- }
551
-
552
- const secretParts = secret.split("_");
553
- if (secretParts.length < 2) {
554
- throw new WebhookVerificationError("Invalid secret");
555
- }
556
-
557
- try {
558
- const timestampDate = this.verifyTimestamp(timestamp);
559
- const data = `${id}.${Math.floor(
560
- timestampDate.getTime() / 1000
561
- )}.${payload}`;
562
- const secretBytes = Buffer.from(secretParts[1], "base64");
563
- const computedSignature = this.computeSignature(secretBytes, data);
564
- const receivedSignatures = signature.split(" ");
565
-
566
- for (const versionedSignature of receivedSignatures) {
567
- const [version, receivedSignature] = versionedSignature.split(",");
568
- if (version !== WEBHOOK_SIGNATURE_VERSION) {
569
- continue;
570
- }
571
- if (
572
- crypto.timingSafeEqual(
573
- Buffer.from(receivedSignature, "base64"),
574
- Buffer.from(computedSignature, "base64")
575
- )
576
- ) {
577
- return true;
578
- }
579
- }
580
-
581
- throw new WebhookVerificationError("Invalid Signature");
582
- } catch (error) {
583
- if (error instanceof WebhookVerificationError) {
584
- throw error;
585
- }
586
- throw new WebhookVerificationError("Invalid Signature");
587
- }
588
- }
589
-
590
- /**
591
- * Validates a token and returns the claims as json payload if valid.
592
- * Supports issuer, audience, and scope validation.
593
- *
594
- * @param {string} token The token to be validated
595
- * @param {TokenValidationOptions} options Optional validation options for issuer, audience, and scopes
596
- * @return {Promise<T>} Returns the token payload if valid
597
- * @throws {ScalekitValidateTokenFailureException} If token is invalid or missing required scopes
598
- */
599
- async validateToken<T>(
600
- token: string,
601
- options?: TokenValidationOptions
602
- ): Promise<T> {
603
- await this.coreClient.getJwks();
604
- const jwks = jose.createLocalJWKSet({
605
- keys: this.coreClient.keys,
606
- });
607
- try {
608
- const { payload } = await jose.jwtVerify<T>(token, jwks, {
609
- ...(options?.issuer && { issuer: options.issuer }),
610
- ...(options?.audience && { audience: options.audience }),
611
- });
612
-
613
- if (options?.requiredScopes && options.requiredScopes.length > 0) {
614
- this.verifyScopes(token, options.requiredScopes);
615
- }
616
-
617
- return payload;
618
- } catch (error) {
619
- throw new ScalekitValidateTokenFailureException(error);
620
- }
621
- }
622
-
623
- /**
624
- * Verify that the token contains the required scopes
625
- *
626
- * @param {string} token The token to verify
627
- * @param {string[]} requiredScopes The scopes that must be present in the token
628
- * @return {boolean} Returns true if all required scopes are present
629
- * @throws {ScalekitValidateTokenFailureException} If required scopes are missing, with details about which scopes are missing
630
- */
631
- verifyScopes(token: string, requiredScopes: string[]): boolean {
632
- const payload = jose.decodeJwt(token);
633
- const scopes = this.extractScopesFromPayload(payload);
634
-
635
- const missingScopes = requiredScopes.filter(
636
- (scope) => !scopes.includes(scope)
637
- );
638
-
639
- if (missingScopes.length > 0) {
640
- throw new ScalekitValidateTokenFailureException(
641
- `Token missing required scopes: ${missingScopes.join(", ")}`
642
- );
643
- }
644
-
645
- return true;
646
- }
647
-
648
- /**
649
- * Extract scopes from token payload
650
- *
651
- * @param {any} payload The token payload
652
- * @return {string[]} Array of scopes found in the token
653
- */
654
- private extractScopesFromPayload(payload: Record<string, any>): string[] {
655
- const scopes = payload.scopes;
656
- return Array.isArray(scopes)
657
- ? scopes.filter((scope) => !!scope.trim?.())
658
- : [];
659
- }
660
-
661
- /**
662
- * Verify the timestamp
663
- *
664
- * @param {string} timestampStr The timestamp string
665
- * @return {Date} Returns the timestamp
666
- */
667
- private verifyTimestamp(timestampStr: string): Date {
668
- const now = Math.floor(Date.now() / 1000);
669
- const timestamp = parseInt(timestampStr, 10);
670
- if (isNaN(timestamp)) {
671
- throw new WebhookVerificationError("Invalid Signature Headers");
672
- }
673
- if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
674
- throw new WebhookVerificationError("Message timestamp too old");
675
- }
676
- if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
677
- throw new WebhookVerificationError("Message timestamp too new");
678
- }
679
-
680
- return new Date(timestamp * 1000);
681
- }
682
-
683
- /**
684
- * Compute the signature
685
- *
686
- * @param {Buffer} secretBytes The secret bytes
687
- * @param {string} data The data to be signed
688
- * @return {string} Returns the signature
689
- */
690
- private computeSignature(secretBytes: Buffer, data: string): string {
691
- return crypto
692
- .createHmac("sha256", secretBytes)
693
- .update(data)
694
- .digest("base64");
695
- }
696
-
697
- /**
698
- * Obtains a new access token using a refresh token.
699
- *
700
- * Use this method to get a new access token when the current one expires, without requiring
701
- * the user to re-authenticate. This implements the OAuth 2.0 refresh token grant type.
702
- * The method returns both a new access token and a new refresh token (token rotation).
703
- *
704
- * @param {string} refreshToken - The refresh token obtained from a previous authentication
705
- *
706
- * @returns {Promise<RefreshTokenResponse>} Response containing:
707
- * - accessToken: New access token for API authorization
708
- * - refreshToken: New refresh token (the old one is invalidated)
709
- *
710
- * @throws {Error} When the refresh token is missing
711
- * @throws {Error} When the refresh token is invalid, expired, or revoked
712
- * @throws {Error} When the authentication server response is invalid
713
- *
714
- * @example
715
- * // Refresh tokens before they expire
716
- * async function refreshUserToken(userId) {
717
- * try {
718
- * const oldRefreshToken = await getStoredRefreshToken(userId);
719
- *
720
- * const result = await scalekitClient.refreshAccessToken(oldRefreshToken);
721
- *
722
- * // Store the new tokens (old refresh token is now invalid)
723
- * await storeTokens(userId, {
724
- * accessToken: result.accessToken,
725
- * refreshToken: result.refreshToken
726
- * });
727
- *
728
- * return result.accessToken;
729
- * } catch (error) {
730
- * console.error('Token refresh failed:', error);
731
- * // Redirect user to login
732
- * throw new Error('Please log in again');
733
- * }
734
- * }
735
- *
736
- * @example
737
- * // Automatic token refresh middleware
738
- * app.use(async (req, res, next) => {
739
- * const accessToken = req.session.accessToken;
740
- * const refreshToken = req.session.refreshToken;
741
- *
742
- * // Check if access token is expired (decode JWT and check exp claim)
743
- * if (isTokenExpired(accessToken) && refreshToken) {
744
- * try {
745
- * const result = await scalekitClient.refreshAccessToken(refreshToken);
746
- * req.session.accessToken = result.accessToken;
747
- * req.session.refreshToken = result.refreshToken;
748
- * } catch (error) {
749
- * return res.redirect('/login');
750
- * }
751
- * }
752
- * next();
753
- * });
754
- *
755
- */
756
- async refreshAccessToken(
757
- refreshToken: string
758
- ): Promise<RefreshTokenResponse> {
759
- if (!refreshToken) {
760
- throw new Error("Refresh token is required");
761
- }
762
-
763
- let res;
764
- try {
765
- res = await this.coreClient.authenticate(
766
- QueryString.stringify({
767
- grant_type: GrantType.RefreshToken,
768
- client_id: this.coreClient.clientId,
769
- client_secret: this.coreClient.clientSecret,
770
- refresh_token: refreshToken,
771
- })
772
- );
773
- } catch (error) {
774
- throw new Error(
775
- `Failed to refresh token: ${
776
- error instanceof Error ? error.message : "Unknown error"
777
- }`
778
- );
779
- }
780
-
781
- if (!res || !res.data) {
782
- throw new Error("Invalid response from authentication server");
783
- }
784
-
785
- const { access_token, refresh_token } = res.data;
786
-
787
- // Validate that all required properties exist
788
- if (!access_token) {
789
- throw new Error("Missing access_token in authentication response");
790
- }
791
- if (!refresh_token) {
792
- throw new Error("Missing refresh_token in authentication response");
793
- }
794
-
795
- return {
796
- accessToken: access_token,
797
- refreshToken: refresh_token,
798
- };
799
- }
800
- }