@payloadcms/figma 0.0.1-alpha.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/LICENSE.md +11 -0
- package/bin/cli.js +4 -0
- package/dist/api/control-plane.d.ts +124 -0
- package/dist/api/control-plane.d.ts.map +1 -0
- package/dist/api/control-plane.js +245 -0
- package/dist/api/control-plane.js.map +1 -0
- package/dist/api/control-plane.spec.d.ts +2 -0
- package/dist/api/control-plane.spec.d.ts.map +1 -0
- package/dist/api/control-plane.spec.js +296 -0
- package/dist/api/control-plane.spec.js.map +1 -0
- package/dist/api/figma-api.d.ts +31 -0
- package/dist/api/figma-api.d.ts.map +1 -0
- package/dist/api/figma-api.js +169 -0
- package/dist/api/figma-api.js.map +1 -0
- package/dist/api/figma-api.spec.d.ts +14 -0
- package/dist/api/figma-api.spec.d.ts.map +1 -0
- package/dist/api/figma-api.spec.js +201 -0
- package/dist/api/figma-api.spec.js.map +1 -0
- package/dist/auth/__tests__/fixtures.d.ts +26 -0
- package/dist/auth/__tests__/fixtures.d.ts.map +1 -0
- package/dist/auth/__tests__/fixtures.js +67 -0
- package/dist/auth/__tests__/fixtures.js.map +1 -0
- package/dist/auth/__tests__/mocks.d.ts +33 -0
- package/dist/auth/__tests__/mocks.d.ts.map +1 -0
- package/dist/auth/__tests__/mocks.js +63 -0
- package/dist/auth/__tests__/mocks.js.map +1 -0
- package/dist/auth/browser.d.ts +31 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/browser.js +41 -0
- package/dist/auth/browser.js.map +1 -0
- package/dist/auth/callback-server.d.ts +61 -0
- package/dist/auth/callback-server.d.ts.map +1 -0
- package/dist/auth/callback-server.js +168 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/callback-server.spec.d.ts +2 -0
- package/dist/auth/callback-server.spec.d.ts.map +1 -0
- package/dist/auth/callback-server.spec.js +85 -0
- package/dist/auth/callback-server.spec.js.map +1 -0
- package/dist/auth/crypto-utils.d.ts +33 -0
- package/dist/auth/crypto-utils.d.ts.map +1 -0
- package/dist/auth/crypto-utils.js +96 -0
- package/dist/auth/crypto-utils.js.map +1 -0
- package/dist/auth/crypto-utils.spec.d.ts +2 -0
- package/dist/auth/crypto-utils.spec.d.ts.map +1 -0
- package/dist/auth/crypto-utils.spec.js +36 -0
- package/dist/auth/crypto-utils.spec.js.map +1 -0
- package/dist/auth/jwt-validator.d.ts +41 -0
- package/dist/auth/jwt-validator.d.ts.map +1 -0
- package/dist/auth/jwt-validator.js +113 -0
- package/dist/auth/jwt-validator.js.map +1 -0
- package/dist/auth/jwt-validator.spec.d.ts +10 -0
- package/dist/auth/jwt-validator.spec.d.ts.map +1 -0
- package/dist/auth/jwt-validator.spec.js +236 -0
- package/dist/auth/jwt-validator.spec.js.map +1 -0
- package/dist/auth/oauth-flow.d.ts +62 -0
- package/dist/auth/oauth-flow.d.ts.map +1 -0
- package/dist/auth/oauth-flow.js +172 -0
- package/dist/auth/oauth-flow.js.map +1 -0
- package/dist/auth/oauth-flow.spec.d.ts +2 -0
- package/dist/auth/oauth-flow.spec.d.ts.map +1 -0
- package/dist/auth/oauth-flow.spec.js +134 -0
- package/dist/auth/oauth-flow.spec.js.map +1 -0
- package/dist/auth/pages/error.html +85 -0
- package/dist/auth/pages/success.html +74 -0
- package/dist/auth/pkce.d.ts +51 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +71 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/pkce.spec.d.ts +2 -0
- package/dist/auth/pkce.spec.d.ts.map +1 -0
- package/dist/auth/pkce.spec.js +32 -0
- package/dist/auth/pkce.spec.js.map +1 -0
- package/dist/auth/project-token.d.ts +47 -0
- package/dist/auth/project-token.d.ts.map +1 -0
- package/dist/auth/project-token.js +134 -0
- package/dist/auth/project-token.js.map +1 -0
- package/dist/auth/project-token.spec.d.ts +2 -0
- package/dist/auth/project-token.spec.d.ts.map +1 -0
- package/dist/auth/project-token.spec.js +332 -0
- package/dist/auth/project-token.spec.js.map +1 -0
- package/dist/auth/refresh.d.ts +35 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/auth/refresh.js +149 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/auth/refresh.spec.d.ts +2 -0
- package/dist/auth/refresh.spec.d.ts.map +1 -0
- package/dist/auth/refresh.spec.js +105 -0
- package/dist/auth/refresh.spec.js.map +1 -0
- package/dist/auth/token-store.d.ts +110 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +188 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/token-store.spec.d.ts +2 -0
- package/dist/auth/token-store.spec.d.ts.map +1 -0
- package/dist/auth/token-store.spec.js +276 -0
- package/dist/auth/token-store.spec.js.map +1 -0
- package/dist/auth/types.d.ts +197 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +6 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +108 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/__tests__/test-utils.d.ts +28 -0
- package/dist/commands/__tests__/test-utils.d.ts.map +1 -0
- package/dist/commands/__tests__/test-utils.js +34 -0
- package/dist/commands/__tests__/test-utils.js.map +1 -0
- package/dist/commands/deploy.d.ts +16 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +297 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/deploy.spec.d.ts +2 -0
- package/dist/commands/deploy.spec.d.ts.map +1 -0
- package/dist/commands/deploy.spec.js +18 -0
- package/dist/commands/deploy.spec.js.map +1 -0
- package/dist/commands/init.d.ts +30 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +410 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.spec.d.ts +2 -0
- package/dist/commands/init.spec.d.ts.map +1 -0
- package/dist/commands/init.spec.js +283 -0
- package/dist/commands/init.spec.js.map +1 -0
- package/dist/commands/list-tokens.d.ts +16 -0
- package/dist/commands/list-tokens.d.ts.map +1 -0
- package/dist/commands/list-tokens.js +44 -0
- package/dist/commands/list-tokens.js.map +1 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +69 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +16 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +43 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/config/oauth.d.ts +19 -0
- package/dist/config/oauth.d.ts.map +1 -0
- package/dist/config/oauth.js +43 -0
- package/dist/config/oauth.js.map +1 -0
- package/dist/constants.d.ts +27 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +27 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/adapter.d.ts +5 -0
- package/dist/plugin/adapter.d.ts.map +1 -0
- package/dist/plugin/adapter.js +139 -0
- package/dist/plugin/adapter.js.map +1 -0
- package/dist/plugin/build-config.d.ts +37 -0
- package/dist/plugin/build-config.d.ts.map +1 -0
- package/dist/plugin/build-config.js +25 -0
- package/dist/plugin/build-config.js.map +1 -0
- package/dist/plugin/build-config.spec.d.ts +2 -0
- package/dist/plugin/build-config.spec.d.ts.map +1 -0
- package/dist/plugin/build-config.spec.js +72 -0
- package/dist/plugin/build-config.spec.js.map +1 -0
- package/dist/types/config.d.ts +45 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +9 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/asset-collection.d.ts +30 -0
- package/dist/utils/asset-collection.d.ts.map +1 -0
- package/dist/utils/asset-collection.js +117 -0
- package/dist/utils/asset-collection.js.map +1 -0
- package/dist/utils/asset-collection.spec.d.ts +2 -0
- package/dist/utils/asset-collection.spec.d.ts.map +1 -0
- package/dist/utils/asset-collection.spec.js +155 -0
- package/dist/utils/asset-collection.spec.js.map +1 -0
- package/dist/utils/build-detection.d.ts +32 -0
- package/dist/utils/build-detection.d.ts.map +1 -0
- package/dist/utils/build-detection.js +55 -0
- package/dist/utils/build-detection.js.map +1 -0
- package/dist/utils/build-detection.spec.d.ts +2 -0
- package/dist/utils/build-detection.spec.d.ts.map +1 -0
- package/dist/utils/build-detection.spec.js +110 -0
- package/dist/utils/build-detection.spec.js.map +1 -0
- package/dist/utils/config.d.ts +33 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +78 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/config.spec.d.ts +2 -0
- package/dist/utils/config.spec.d.ts.map +1 -0
- package/dist/utils/config.spec.js +167 -0
- package/dist/utils/config.spec.js.map +1 -0
- package/dist/utils/download-template.d.ts +15 -0
- package/dist/utils/download-template.d.ts.map +1 -0
- package/dist/utils/download-template.js +74 -0
- package/dist/utils/download-template.js.map +1 -0
- package/dist/utils/download-template.spec.d.ts +2 -0
- package/dist/utils/download-template.spec.d.ts.map +1 -0
- package/dist/utils/download-template.spec.js +76 -0
- package/dist/utils/download-template.spec.js.map +1 -0
- package/dist/utils/env-management.d.ts +16 -0
- package/dist/utils/env-management.d.ts.map +1 -0
- package/dist/utils/env-management.js +98 -0
- package/dist/utils/env-management.js.map +1 -0
- package/dist/utils/env-management.spec.d.ts +2 -0
- package/dist/utils/env-management.spec.d.ts.map +1 -0
- package/dist/utils/env-management.spec.js +123 -0
- package/dist/utils/env-management.spec.js.map +1 -0
- package/dist/utils/format.d.ts +23 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +49 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/formatter.d.ts +11 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +47 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/git.d.ts +15 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +92 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/git.spec.d.ts +2 -0
- package/dist/utils/git.spec.d.ts.map +1 -0
- package/dist/utils/git.spec.js +99 -0
- package/dist/utils/git.spec.js.map +1 -0
- package/dist/utils/is-debug.d.ts +6 -0
- package/dist/utils/is-debug.d.ts.map +1 -0
- package/dist/utils/is-debug.js +8 -0
- package/dist/utils/is-debug.js.map +1 -0
- package/dist/utils/lambda-config.d.ts +7 -0
- package/dist/utils/lambda-config.d.ts.map +1 -0
- package/dist/utils/lambda-config.js +144 -0
- package/dist/utils/lambda-config.js.map +1 -0
- package/dist/utils/lambda-config.spec.d.ts +2 -0
- package/dist/utils/lambda-config.spec.d.ts.map +1 -0
- package/dist/utils/lambda-config.spec.js +141 -0
- package/dist/utils/lambda-config.spec.js.map +1 -0
- package/dist/utils/log.d.ts +6 -0
- package/dist/utils/log.d.ts.map +1 -0
- package/dist/utils/log.js +22 -0
- package/dist/utils/log.js.map +1 -0
- package/dist/utils/messages.d.ts +10 -0
- package/dist/utils/messages.d.ts.map +1 -0
- package/dist/utils/messages.js +96 -0
- package/dist/utils/messages.js.map +1 -0
- package/dist/utils/package-manager.d.ts +12 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +37 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/dist/utils/payload-config-ast.d.ts +30 -0
- package/dist/utils/payload-config-ast.d.ts.map +1 -0
- package/dist/utils/payload-config-ast.js +259 -0
- package/dist/utils/payload-config-ast.js.map +1 -0
- package/dist/utils/payload-config-ast.spec.d.ts +2 -0
- package/dist/utils/payload-config-ast.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-ast.spec.js +303 -0
- package/dist/utils/payload-config-ast.spec.js.map +1 -0
- package/dist/utils/payload-config-finder.d.ts +9 -0
- package/dist/utils/payload-config-finder.d.ts.map +1 -0
- package/dist/utils/payload-config-finder.js +26 -0
- package/dist/utils/payload-config-finder.js.map +1 -0
- package/dist/utils/payload-config-modifier.d.ts +18 -0
- package/dist/utils/payload-config-modifier.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.int.spec.d.ts +2 -0
- package/dist/utils/payload-config-modifier.int.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.int.spec.js +116 -0
- package/dist/utils/payload-config-modifier.int.spec.js.map +1 -0
- package/dist/utils/payload-config-modifier.js +232 -0
- package/dist/utils/payload-config-modifier.js.map +1 -0
- package/dist/utils/payload-config-modifier.spec.d.ts +2 -0
- package/dist/utils/payload-config-modifier.spec.d.ts.map +1 -0
- package/dist/utils/payload-config-modifier.spec.js +162 -0
- package/dist/utils/payload-config-modifier.spec.js.map +1 -0
- package/dist/utils/payload-package-check.d.ts +15 -0
- package/dist/utils/payload-package-check.d.ts.map +1 -0
- package/dist/utils/payload-package-check.js +116 -0
- package/dist/utils/payload-package-check.js.map +1 -0
- package/dist/utils/payload-package-check.spec.d.ts +2 -0
- package/dist/utils/payload-package-check.spec.d.ts.map +1 -0
- package/dist/utils/payload-package-check.spec.js +148 -0
- package/dist/utils/payload-package-check.spec.js.map +1 -0
- package/dist/utils/project.d.ts +58 -0
- package/dist/utils/project.d.ts.map +1 -0
- package/dist/utils/project.js +252 -0
- package/dist/utils/project.js.map +1 -0
- package/dist/utils/project.spec.d.ts +2 -0
- package/dist/utils/project.spec.d.ts.map +1 -0
- package/dist/utils/project.spec.js +222 -0
- package/dist/utils/project.spec.js.map +1 -0
- package/dist/utils/s3-upload.d.ts +43 -0
- package/dist/utils/s3-upload.d.ts.map +1 -0
- package/dist/utils/s3-upload.js +128 -0
- package/dist/utils/s3-upload.js.map +1 -0
- package/dist/utils/s3-upload.spec.d.ts +2 -0
- package/dist/utils/s3-upload.spec.d.ts.map +1 -0
- package/dist/utils/s3-upload.spec.js +140 -0
- package/dist/utils/s3-upload.spec.js.map +1 -0
- package/dist/utils/token-display.d.ts +14 -0
- package/dist/utils/token-display.d.ts.map +1 -0
- package/dist/utils/token-display.js +62 -0
- package/dist/utils/token-display.js.map +1 -0
- package/dist/utils/typescript-validator.d.ts +11 -0
- package/dist/utils/typescript-validator.d.ts.map +1 -0
- package/dist/utils/typescript-validator.js +58 -0
- package/dist/utils/typescript-validator.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
+
import { JWKSMultipleMatchingKeys, JWKSNoMatchingKey, JWSInvalid, JWSSignatureVerificationFailed, JWTExpired } from 'jose/errors';
|
|
3
|
+
import { getJWKSUri } from '../constants.js';
|
|
4
|
+
/**
|
|
5
|
+
* JWT validation error with specific error codes
|
|
6
|
+
*/ export class JWTValidationError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
cause;
|
|
9
|
+
constructor(message, code, cause){
|
|
10
|
+
super(message), this.code = code, this.cause = cause;
|
|
11
|
+
this.name = 'JWTValidationError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* JWKS resolvers cached per environment
|
|
16
|
+
* Uses jose's createRemoteJWKSet which handles:
|
|
17
|
+
* - Automatic JWKS fetching from URI
|
|
18
|
+
* - Built-in caching with configurable max age
|
|
19
|
+
* - Automatic key rotation handling
|
|
20
|
+
* - Cooldown protection between refetches
|
|
21
|
+
*/ const jwksResolvers = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Get or create a JWKS resolver for a given environment
|
|
24
|
+
*
|
|
25
|
+
* The resolver is cached in module scope and reused across validations.
|
|
26
|
+
* Jose handles all the complexity of fetching, caching, and rotating keys.
|
|
27
|
+
*
|
|
28
|
+
* @param environment - Figma environment ('production' or 'staging')
|
|
29
|
+
* @returns JWKS resolver function
|
|
30
|
+
*/ function getJWKSResolver(environment) {
|
|
31
|
+
const jwksUri = getJWKSUri(environment);
|
|
32
|
+
if (!jwksResolvers.has(jwksUri)) {
|
|
33
|
+
jwksResolvers.set(jwksUri, createRemoteJWKSet(new URL(jwksUri), {
|
|
34
|
+
cacheMaxAge: 24 * 60 * 60 * 1000,
|
|
35
|
+
cooldownDuration: 30000
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
return jwksResolvers.get(jwksUri);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate a project token JWT
|
|
42
|
+
*
|
|
43
|
+
* This function:
|
|
44
|
+
* 1. Gets the JWKS resolver for the environment (cached)
|
|
45
|
+
* 2. Uses jose's jwtVerify to validate signature and claims in one call
|
|
46
|
+
* 3. Maps jose errors to our custom error types
|
|
47
|
+
*
|
|
48
|
+
* Jose automatically:
|
|
49
|
+
* - Fetches JWKS from the URI (with caching)
|
|
50
|
+
* - Matches the key by kid from the JWT header
|
|
51
|
+
* - Verifies the signature using the correct algorithm
|
|
52
|
+
* - Validates standard claims (exp, iat, nbf)
|
|
53
|
+
* - Handles clock skew tolerance
|
|
54
|
+
*
|
|
55
|
+
* @param token - JWT token string
|
|
56
|
+
* @param environment - Figma environment ('production' or 'staging')
|
|
57
|
+
* @returns Promise resolving to validated JWT payload
|
|
58
|
+
* @throws {JWTValidationError} If validation fails
|
|
59
|
+
*/ export async function validateProjectToken(token, environment) {
|
|
60
|
+
try {
|
|
61
|
+
const JWKS = getJWKSResolver(environment);
|
|
62
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
63
|
+
clockTolerance: 300
|
|
64
|
+
});
|
|
65
|
+
return payload;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Map jose errors to our custom error types using instanceof checks
|
|
68
|
+
// This is more reliable than checking error.code strings
|
|
69
|
+
// Token expired
|
|
70
|
+
if (error instanceof JWTExpired) {
|
|
71
|
+
throw new JWTValidationError('Project token has expired', 'EXPIRED', error);
|
|
72
|
+
}
|
|
73
|
+
// Key not found in JWKS (possible key rotation or invalid kid)
|
|
74
|
+
if (error instanceof JWKSNoMatchingKey) {
|
|
75
|
+
throw new JWTValidationError('Key not found in JWKS - this may indicate key rotation or an invalid token', 'SIGNATURE_INVALID', error);
|
|
76
|
+
}
|
|
77
|
+
// Multiple matching keys (JWKS configuration issue)
|
|
78
|
+
if (error instanceof JWKSMultipleMatchingKeys) {
|
|
79
|
+
throw new JWTValidationError('Multiple matching keys found in JWKS', 'SIGNATURE_INVALID', error);
|
|
80
|
+
}
|
|
81
|
+
// Invalid signature or other verification failure
|
|
82
|
+
if (error instanceof JWSSignatureVerificationFailed) {
|
|
83
|
+
throw new JWTValidationError('JWT signature verification failed - token may be tampered with', 'SIGNATURE_INVALID', error);
|
|
84
|
+
}
|
|
85
|
+
// Missing or invalid kid/alg in JWT header
|
|
86
|
+
if (error instanceof JWSInvalid) {
|
|
87
|
+
throw new JWTValidationError('Invalid JWT structure - missing required header fields (kid or alg)', 'MISSING_KID', error);
|
|
88
|
+
}
|
|
89
|
+
// Network or fetch errors (not jose errors, generic Error instances)
|
|
90
|
+
if (error instanceof Error) {
|
|
91
|
+
const message = error.message;
|
|
92
|
+
if (message.includes('fetch') || message.includes('network') || message.includes('ENOTFOUND') || message.includes('ECONNREFUSED')) {
|
|
93
|
+
throw new JWTValidationError(`Failed to fetch JWKS: ${message}`, 'JWKS_FETCH_FAILED', error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Generic error fallback
|
|
97
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
98
|
+
throw new JWTValidationError(`JWT validation failed: ${message}`, 'SIGNATURE_INVALID', error instanceof Error ? error : undefined);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Clear JWKS cache (useful for testing or forcing refresh)
|
|
103
|
+
*
|
|
104
|
+
* This clears the cached JWKS resolvers, forcing fresh fetches
|
|
105
|
+
* on the next validation. Useful for:
|
|
106
|
+
* - Testing with different JWKS
|
|
107
|
+
* - Forcing key rotation updates
|
|
108
|
+
* - Clearing state between test runs
|
|
109
|
+
*/ export function clearJWKSCache() {
|
|
110
|
+
jwksResolvers.clear();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=jwt-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/jwt-validator.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from 'jose'\nimport {\n JWKSMultipleMatchingKeys,\n JWKSNoMatchingKey,\n JWSInvalid,\n JWSSignatureVerificationFailed,\n JWTExpired,\n} from 'jose/errors'\n\nimport type { JWTPayload } from './types.js'\n\nimport { getJWKSUri } from '../constants.js'\n\n/**\n * JWT validation error with specific error codes\n */\nexport class JWTValidationError extends Error {\n constructor(\n message: string,\n public code: 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID',\n public cause?: Error,\n ) {\n super(message)\n this.name = 'JWTValidationError'\n }\n}\n\n/**\n * JWKS resolvers cached per environment\n * Uses jose's createRemoteJWKSet which handles:\n * - Automatic JWKS fetching from URI\n * - Built-in caching with configurable max age\n * - Automatic key rotation handling\n * - Cooldown protection between refetches\n */\nconst jwksResolvers = new Map<string, ReturnType<typeof createRemoteJWKSet>>()\n\n/**\n * Get or create a JWKS resolver for a given environment\n *\n * The resolver is cached in module scope and reused across validations.\n * Jose handles all the complexity of fetching, caching, and rotating keys.\n *\n * @param environment - Figma environment ('production' or 'staging')\n * @returns JWKS resolver function\n */\nfunction getJWKSResolver(environment: 'production' | 'staging') {\n const jwksUri = getJWKSUri(environment)\n\n if (!jwksResolvers.has(jwksUri)) {\n jwksResolvers.set(\n jwksUri,\n createRemoteJWKSet(new URL(jwksUri), {\n cacheMaxAge: 24 * 60 * 60 * 1000, // 24 hours\n cooldownDuration: 30000, // 30 seconds between refetches\n }),\n )\n }\n\n return jwksResolvers.get(jwksUri)!\n}\n\n/**\n * Validate a project token JWT\n *\n * This function:\n * 1. Gets the JWKS resolver for the environment (cached)\n * 2. Uses jose's jwtVerify to validate signature and claims in one call\n * 3. Maps jose errors to our custom error types\n *\n * Jose automatically:\n * - Fetches JWKS from the URI (with caching)\n * - Matches the key by kid from the JWT header\n * - Verifies the signature using the correct algorithm\n * - Validates standard claims (exp, iat, nbf)\n * - Handles clock skew tolerance\n *\n * @param token - JWT token string\n * @param environment - Figma environment ('production' or 'staging')\n * @returns Promise resolving to validated JWT payload\n * @throws {JWTValidationError} If validation fails\n */\nexport async function validateProjectToken(\n token: string,\n environment: 'production' | 'staging',\n): Promise<JWTPayload> {\n try {\n const JWKS = getJWKSResolver(environment)\n\n const { payload } = await jwtVerify(token, JWKS, {\n clockTolerance: 300, // 5 minutes clock skew tolerance\n // Optional additional validation (uncomment if Figma provides these):\n // issuer: 'https://api.figma.com',\n // audience: 'payloadcms-figma',\n })\n\n return payload as JWTPayload\n } catch (error: unknown) {\n // Map jose errors to our custom error types using instanceof checks\n // This is more reliable than checking error.code strings\n\n // Token expired\n if (error instanceof JWTExpired) {\n throw new JWTValidationError('Project token has expired', 'EXPIRED', error)\n }\n\n // Key not found in JWKS (possible key rotation or invalid kid)\n if (error instanceof JWKSNoMatchingKey) {\n throw new JWTValidationError(\n 'Key not found in JWKS - this may indicate key rotation or an invalid token',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Multiple matching keys (JWKS configuration issue)\n if (error instanceof JWKSMultipleMatchingKeys) {\n throw new JWTValidationError(\n 'Multiple matching keys found in JWKS',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Invalid signature or other verification failure\n if (error instanceof JWSSignatureVerificationFailed) {\n throw new JWTValidationError(\n 'JWT signature verification failed - token may be tampered with',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Missing or invalid kid/alg in JWT header\n if (error instanceof JWSInvalid) {\n throw new JWTValidationError(\n 'Invalid JWT structure - missing required header fields (kid or alg)',\n 'MISSING_KID',\n error,\n )\n }\n\n // Network or fetch errors (not jose errors, generic Error instances)\n if (error instanceof Error) {\n const message = error.message\n if (\n message.includes('fetch') ||\n message.includes('network') ||\n message.includes('ENOTFOUND') ||\n message.includes('ECONNREFUSED')\n ) {\n throw new JWTValidationError(`Failed to fetch JWKS: ${message}`, 'JWKS_FETCH_FAILED', error)\n }\n }\n\n // Generic error fallback\n const message = error instanceof Error ? error.message : 'Unknown error'\n throw new JWTValidationError(\n `JWT validation failed: ${message}`,\n 'SIGNATURE_INVALID',\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Clear JWKS cache (useful for testing or forcing refresh)\n *\n * This clears the cached JWKS resolvers, forcing fresh fetches\n * on the next validation. Useful for:\n * - Testing with different JWKS\n * - Forcing key rotation updates\n * - Clearing state between test runs\n */\nexport function clearJWKSCache(): void {\n jwksResolvers.clear()\n}\n"],"names":["createRemoteJWKSet","jwtVerify","JWKSMultipleMatchingKeys","JWKSNoMatchingKey","JWSInvalid","JWSSignatureVerificationFailed","JWTExpired","getJWKSUri","JWTValidationError","Error","message","code","cause","name","jwksResolvers","Map","getJWKSResolver","environment","jwksUri","has","set","URL","cacheMaxAge","cooldownDuration","get","validateProjectToken","token","JWKS","payload","clockTolerance","error","includes","undefined","clearJWKSCache","clear"],"mappings":"AAAA,SAASA,kBAAkB,EAAEC,SAAS,QAAQ,OAAM;AACpD,SACEC,wBAAwB,EACxBC,iBAAiB,EACjBC,UAAU,EACVC,8BAA8B,EAC9BC,UAAU,QACL,cAAa;AAIpB,SAASC,UAAU,QAAQ,kBAAiB;AAE5C;;CAEC,GACD,OAAO,MAAMC,2BAA2BC;;;IACtC,YACEC,OAAe,EACf,AAAOC,IAA2E,EAClF,AAAOC,KAAa,CACpB;QACA,KAAK,CAACF,eAHCC,OAAAA,WACAC,QAAAA;QAGP,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA;;;;;;;CAOC,GACD,MAAMC,gBAAgB,IAAIC;AAE1B;;;;;;;;CAQC,GACD,SAASC,gBAAgBC,WAAqC;IAC5D,MAAMC,UAAUX,WAAWU;IAE3B,IAAI,CAACH,cAAcK,GAAG,CAACD,UAAU;QAC/BJ,cAAcM,GAAG,CACfF,SACAlB,mBAAmB,IAAIqB,IAAIH,UAAU;YACnCI,aAAa,KAAK,KAAK,KAAK;YAC5BC,kBAAkB;QACpB;IAEJ;IAEA,OAAOT,cAAcU,GAAG,CAACN;AAC3B;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,eAAeO,qBACpBC,KAAa,EACbT,WAAqC;IAErC,IAAI;QACF,MAAMU,OAAOX,gBAAgBC;QAE7B,MAAM,EAAEW,OAAO,EAAE,GAAG,MAAM3B,UAAUyB,OAAOC,MAAM;YAC/CE,gBAAgB;QAIlB;QAEA,OAAOD;IACT,EAAE,OAAOE,OAAgB;QACvB,oEAAoE;QACpE,yDAAyD;QAEzD,gBAAgB;QAChB,IAAIA,iBAAiBxB,YAAY;YAC/B,MAAM,IAAIE,mBAAmB,6BAA6B,WAAWsB;QACvE;QAEA,+DAA+D;QAC/D,IAAIA,iBAAiB3B,mBAAmB;YACtC,MAAM,IAAIK,mBACR,8EACA,qBACAsB;QAEJ;QAEA,oDAAoD;QACpD,IAAIA,iBAAiB5B,0BAA0B;YAC7C,MAAM,IAAIM,mBACR,wCACA,qBACAsB;QAEJ;QAEA,kDAAkD;QAClD,IAAIA,iBAAiBzB,gCAAgC;YACnD,MAAM,IAAIG,mBACR,kEACA,qBACAsB;QAEJ;QAEA,2CAA2C;QAC3C,IAAIA,iBAAiB1B,YAAY;YAC/B,MAAM,IAAII,mBACR,uEACA,eACAsB;QAEJ;QAEA,qEAAqE;QACrE,IAAIA,iBAAiBrB,OAAO;YAC1B,MAAMC,UAAUoB,MAAMpB,OAAO;YAC7B,IACEA,QAAQqB,QAAQ,CAAC,YACjBrB,QAAQqB,QAAQ,CAAC,cACjBrB,QAAQqB,QAAQ,CAAC,gBACjBrB,QAAQqB,QAAQ,CAAC,iBACjB;gBACA,MAAM,IAAIvB,mBAAmB,CAAC,sBAAsB,EAAEE,SAAS,EAAE,qBAAqBoB;YACxF;QACF;QAEA,yBAAyB;QACzB,MAAMpB,UAAUoB,iBAAiBrB,QAAQqB,MAAMpB,OAAO,GAAG;QACzD,MAAM,IAAIF,mBACR,CAAC,uBAAuB,EAAEE,SAAS,EACnC,qBACAoB,iBAAiBrB,QAAQqB,QAAQE;IAErC;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC;IACdnB,cAAcoB,KAAK;AACrB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for JWT validator
|
|
3
|
+
*
|
|
4
|
+
* Tests our wrapper logic around jose's JWT validation:
|
|
5
|
+
* - Cache management (clearJWKSCache)
|
|
6
|
+
* - Environment-based JWKS URI routing
|
|
7
|
+
* - Error mapping from jose errors to our custom error codes
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=jwt-validator.spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt-validator.spec.d.ts","sourceRoot":"","sources":["../../src/auth/jwt-validator.spec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for JWT validator
|
|
3
|
+
*
|
|
4
|
+
* Tests our wrapper logic around jose's JWT validation:
|
|
5
|
+
* - Cache management (clearJWKSCache)
|
|
6
|
+
* - Environment-based JWKS URI routing
|
|
7
|
+
* - Error mapping from jose errors to our custom error codes
|
|
8
|
+
*/ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { clearJWKSCache, JWTValidationError, validateProjectToken } from './jwt-validator.js';
|
|
10
|
+
// Helper to create a simple test JWT (will fail validation but tests the flow)
|
|
11
|
+
function createTestJWT(payload, kid = 'test-key-id') {
|
|
12
|
+
const header = Buffer.from(JSON.stringify({
|
|
13
|
+
alg: 'RS256',
|
|
14
|
+
kid,
|
|
15
|
+
typ: 'JWT'
|
|
16
|
+
})).toString('base64url');
|
|
17
|
+
const payloadStr = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
18
|
+
const signature = 'fake_signature';
|
|
19
|
+
return `${header}.${payloadStr}.${signature}`;
|
|
20
|
+
}
|
|
21
|
+
describe('JWT Validator', ()=>{
|
|
22
|
+
// Mock JWKS with RSA key
|
|
23
|
+
const mockJWKS = {
|
|
24
|
+
keys: [
|
|
25
|
+
{
|
|
26
|
+
alg: 'RS256',
|
|
27
|
+
e: 'AQAB',
|
|
28
|
+
kid: 'test-key-id',
|
|
29
|
+
kty: 'RSA',
|
|
30
|
+
n: 'xGOr-H7A-PWO8VcD4sqDpCciAuqYDPq8zKWb4Vh6YhP8Ny3KcJaJhLMDEBJB3fKVvT-pPlkHPFBv7L7iHzRrJMXz8uDJVK-FTYmTQhcvKPyMWUDGDPNlVXEGkBLdj3DZqKjJ8iVChSh5DzKJvzr-RyGnH8Z7nLqBa91zxWfm7ztJxqP0qPRKz6vb4h8QyN-5X4Gh8gU7ZcLLnqLqJ6FhPvqKVcBhfN3KjM6h8hLXz6pPzKqJ8z6r8-9KcZXlWvR',
|
|
31
|
+
use: 'sig'
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
beforeEach(()=>{
|
|
36
|
+
clearJWKSCache();
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
afterEach(()=>{
|
|
40
|
+
clearJWKSCache();
|
|
41
|
+
});
|
|
42
|
+
describe('Cache Management', ()=>{
|
|
43
|
+
it('should clear resolver cache when clearJWKSCache is called', async ()=>{
|
|
44
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
45
|
+
json: ()=>Promise.resolve(mockJWKS),
|
|
46
|
+
ok: true,
|
|
47
|
+
status: 200
|
|
48
|
+
});
|
|
49
|
+
global.fetch = fetchMock;
|
|
50
|
+
const token = createTestJWT({
|
|
51
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
52
|
+
});
|
|
53
|
+
// First validation - creates and caches resolver
|
|
54
|
+
try {
|
|
55
|
+
await validateProjectToken(token, 'staging');
|
|
56
|
+
} catch (_error) {
|
|
57
|
+
// Expected to fail on signature
|
|
58
|
+
}
|
|
59
|
+
const firstCallCount = fetchMock.mock.calls.length;
|
|
60
|
+
expect(firstCallCount).toBeGreaterThanOrEqual(1);
|
|
61
|
+
// Clear the cache - this is OUR logic
|
|
62
|
+
clearJWKSCache();
|
|
63
|
+
// Second validation after clearing cache - should create new resolver
|
|
64
|
+
try {
|
|
65
|
+
await validateProjectToken(token, 'staging');
|
|
66
|
+
} catch (_error) {
|
|
67
|
+
// Expected to fail on signature
|
|
68
|
+
}
|
|
69
|
+
// Verify fetch was called again after cache clear
|
|
70
|
+
expect(fetchMock.mock.calls.length).toBeGreaterThan(firstCallCount);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('Error Code Mapping', ()=>{
|
|
74
|
+
it('should map network errors to JWKS_FETCH_FAILED code', async ()=>{
|
|
75
|
+
global.fetch = vi.fn().mockRejectedValue(new Error('ENOTFOUND'));
|
|
76
|
+
const token = createTestJWT({
|
|
77
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
await validateProjectToken(token, 'staging');
|
|
81
|
+
expect.fail('Should have thrown an error');
|
|
82
|
+
} catch (error) {
|
|
83
|
+
expect(error).toBeInstanceOf(JWTValidationError);
|
|
84
|
+
if (error instanceof JWTValidationError) {
|
|
85
|
+
// Verify our error mapping logic
|
|
86
|
+
expect(error.code).toBe('JWKS_FETCH_FAILED');
|
|
87
|
+
expect(error.message).toContain('Failed to fetch JWKS');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
it('should map jose signature errors to SIGNATURE_INVALID code', async ()=>{
|
|
92
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
93
|
+
json: ()=>Promise.resolve(mockJWKS),
|
|
94
|
+
ok: true,
|
|
95
|
+
status: 200
|
|
96
|
+
});
|
|
97
|
+
const badToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.e30.bad_signature';
|
|
98
|
+
try {
|
|
99
|
+
await validateProjectToken(badToken, 'staging');
|
|
100
|
+
expect.fail('Should have thrown an error');
|
|
101
|
+
} catch (error) {
|
|
102
|
+
expect(error).toBeInstanceOf(JWTValidationError);
|
|
103
|
+
if (error instanceof JWTValidationError) {
|
|
104
|
+
// Verify our error mapping logic - invalid tokens may fail at different stages
|
|
105
|
+
// The important thing is that signature-related errors map to either SIGNATURE_INVALID or MISSING_KID
|
|
106
|
+
expect([
|
|
107
|
+
'SIGNATURE_INVALID',
|
|
108
|
+
'MISSING_KID'
|
|
109
|
+
]).toContain(error.code);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
it('should map expired token errors to EXPIRED code', async ()=>{
|
|
114
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
115
|
+
json: ()=>Promise.resolve(mockJWKS),
|
|
116
|
+
ok: true,
|
|
117
|
+
status: 200
|
|
118
|
+
});
|
|
119
|
+
// Create token that expired 1 hour ago
|
|
120
|
+
const expiredToken = createTestJWT({
|
|
121
|
+
exp: Math.floor(Date.now() / 1000) - 3600,
|
|
122
|
+
iat: Math.floor(Date.now() / 1000) - 7200
|
|
123
|
+
});
|
|
124
|
+
try {
|
|
125
|
+
await validateProjectToken(expiredToken, 'staging');
|
|
126
|
+
expect.fail('Should have thrown an error');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
expect(error).toBeInstanceOf(JWTValidationError);
|
|
129
|
+
if (error instanceof JWTValidationError) {
|
|
130
|
+
// Verify our error mapping logic
|
|
131
|
+
// Note: Mock tokens with bad signatures may fail signature validation before expiry check
|
|
132
|
+
// In real scenarios with valid signatures, EXPIRED would be returned
|
|
133
|
+
expect([
|
|
134
|
+
'EXPIRED',
|
|
135
|
+
'SIGNATURE_INVALID',
|
|
136
|
+
'MISSING_KID'
|
|
137
|
+
]).toContain(error.code);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
it('should map missing kid errors to MISSING_KID code', async ()=>{
|
|
142
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
143
|
+
json: ()=>Promise.resolve(mockJWKS),
|
|
144
|
+
ok: true,
|
|
145
|
+
status: 200
|
|
146
|
+
});
|
|
147
|
+
// Create token without kid in header
|
|
148
|
+
const header = Buffer.from(JSON.stringify({
|
|
149
|
+
alg: 'RS256',
|
|
150
|
+
typ: 'JWT'
|
|
151
|
+
})).toString('base64url');
|
|
152
|
+
const payload = Buffer.from(JSON.stringify({
|
|
153
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
154
|
+
})).toString('base64url');
|
|
155
|
+
const tokenWithoutKid = `${header}.${payload}.fake_signature`;
|
|
156
|
+
try {
|
|
157
|
+
await validateProjectToken(tokenWithoutKid, 'staging');
|
|
158
|
+
expect.fail('Should have thrown an error');
|
|
159
|
+
} catch (error) {
|
|
160
|
+
expect(error).toBeInstanceOf(JWTValidationError);
|
|
161
|
+
if (error instanceof JWTValidationError) {
|
|
162
|
+
// Verify our error mapping logic - tokens without kid may fail at various stages
|
|
163
|
+
expect([
|
|
164
|
+
'MISSING_KID',
|
|
165
|
+
'SIGNATURE_INVALID'
|
|
166
|
+
]).toContain(error.code);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
it('should map no matching key errors to SIGNATURE_INVALID code', async ()=>{
|
|
171
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
172
|
+
json: ()=>Promise.resolve(mockJWKS),
|
|
173
|
+
ok: true,
|
|
174
|
+
status: 200
|
|
175
|
+
});
|
|
176
|
+
// Create token with a kid that doesn't exist in JWKS
|
|
177
|
+
const tokenWithWrongKid = createTestJWT({
|
|
178
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
179
|
+
}, 'non-existent-key-id');
|
|
180
|
+
try {
|
|
181
|
+
await validateProjectToken(tokenWithWrongKid, 'staging');
|
|
182
|
+
expect.fail('Should have thrown an error');
|
|
183
|
+
} catch (error) {
|
|
184
|
+
expect(error).toBeInstanceOf(JWTValidationError);
|
|
185
|
+
if (error instanceof JWTValidationError) {
|
|
186
|
+
// Verify our error mapping logic
|
|
187
|
+
expect(error.code).toBe('SIGNATURE_INVALID');
|
|
188
|
+
expect(error.message).toContain('Key not found in JWKS');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
it('should handle multiple network error formats', async ()=>{
|
|
193
|
+
const networkErrors = [
|
|
194
|
+
new Error('fetch failed'),
|
|
195
|
+
new Error('network error'),
|
|
196
|
+
new Error('ECONNREFUSED')
|
|
197
|
+
];
|
|
198
|
+
for (const networkError of networkErrors){
|
|
199
|
+
clearJWKSCache();
|
|
200
|
+
global.fetch = vi.fn().mockRejectedValue(networkError);
|
|
201
|
+
const token = createTestJWT({
|
|
202
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
203
|
+
});
|
|
204
|
+
try {
|
|
205
|
+
await validateProjectToken(token, 'staging');
|
|
206
|
+
expect.fail('Should have thrown an error');
|
|
207
|
+
} catch (error) {
|
|
208
|
+
expect(error).toBeInstanceOf(JWTValidationError);
|
|
209
|
+
if (error instanceof JWTValidationError) {
|
|
210
|
+
expect(error.code).toBe('JWKS_FETCH_FAILED');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
it('should handle generic errors with fallback', async ()=>{
|
|
216
|
+
// Mock an unexpected error type
|
|
217
|
+
global.fetch = vi.fn().mockRejectedValue('string error');
|
|
218
|
+
const token = createTestJWT({
|
|
219
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
await validateProjectToken(token, 'staging');
|
|
223
|
+
expect.fail('Should have thrown an error');
|
|
224
|
+
} catch (error) {
|
|
225
|
+
expect(error).toBeInstanceOf(JWTValidationError);
|
|
226
|
+
if (error instanceof JWTValidationError) {
|
|
227
|
+
// Generic errors fall back to SIGNATURE_INVALID
|
|
228
|
+
expect(error.code).toBe('SIGNATURE_INVALID');
|
|
229
|
+
expect(error.message).toContain('JWT validation failed');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
//# sourceMappingURL=jwt-validator.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/jwt-validator.spec.ts"],"sourcesContent":["/**\n * Unit tests for JWT validator\n *\n * Tests our wrapper logic around jose's JWT validation:\n * - Cache management (clearJWKSCache)\n * - Environment-based JWKS URI routing\n * - Error mapping from jose errors to our custom error codes\n */\n\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport { clearJWKSCache, JWTValidationError, validateProjectToken } from './jwt-validator.js'\n\n// Helper to create a simple test JWT (will fail validation but tests the flow)\nfunction createTestJWT(payload: Record<string, unknown>, kid = 'test-key-id'): string {\n const header = Buffer.from(JSON.stringify({ alg: 'RS256', kid, typ: 'JWT' })).toString(\n 'base64url',\n )\n const payloadStr = Buffer.from(JSON.stringify(payload)).toString('base64url')\n const signature = 'fake_signature'\n return `${header}.${payloadStr}.${signature}`\n}\n\ndescribe('JWT Validator', () => {\n // Mock JWKS with RSA key\n const mockJWKS = {\n keys: [\n {\n alg: 'RS256',\n e: 'AQAB',\n kid: 'test-key-id',\n kty: 'RSA',\n n: 'xGOr-H7A-PWO8VcD4sqDpCciAuqYDPq8zKWb4Vh6YhP8Ny3KcJaJhLMDEBJB3fKVvT-pPlkHPFBv7L7iHzRrJMXz8uDJVK-FTYmTQhcvKPyMWUDGDPNlVXEGkBLdj3DZqKjJ8iVChSh5DzKJvzr-RyGnH8Z7nLqBa91zxWfm7ztJxqP0qPRKz6vb4h8QyN-5X4Gh8gU7ZcLLnqLqJ6FhPvqKVcBhfN3KjM6h8hLXz6pPzKqJ8z6r8-9KcZXlWvR',\n use: 'sig',\n },\n ],\n }\n\n beforeEach(() => {\n clearJWKSCache()\n vi.restoreAllMocks()\n })\n\n afterEach(() => {\n clearJWKSCache()\n })\n\n describe('Cache Management', () => {\n it('should clear resolver cache when clearJWKSCache is called', async () => {\n const fetchMock = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n global.fetch = fetchMock\n\n const token = createTestJWT({ exp: Math.floor(Date.now() / 1000) + 3600 })\n\n // First validation - creates and caches resolver\n try {\n await validateProjectToken(token, 'staging')\n } catch (_error) {\n // Expected to fail on signature\n }\n\n const firstCallCount = fetchMock.mock.calls.length\n expect(firstCallCount).toBeGreaterThanOrEqual(1)\n\n // Clear the cache - this is OUR logic\n clearJWKSCache()\n\n // Second validation after clearing cache - should create new resolver\n try {\n await validateProjectToken(token, 'staging')\n } catch (_error) {\n // Expected to fail on signature\n }\n\n // Verify fetch was called again after cache clear\n expect(fetchMock.mock.calls.length).toBeGreaterThan(firstCallCount)\n })\n })\n\n describe('Error Code Mapping', () => {\n it('should map network errors to JWKS_FETCH_FAILED code', async () => {\n global.fetch = vi.fn().mockRejectedValue(new Error('ENOTFOUND'))\n\n const token = createTestJWT({ exp: Math.floor(Date.now() / 1000) + 3600 })\n\n try {\n await validateProjectToken(token, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic\n expect(error.code).toBe('JWKS_FETCH_FAILED')\n expect(error.message).toContain('Failed to fetch JWKS')\n }\n }\n })\n\n it('should map jose signature errors to SIGNATURE_INVALID code', async () => {\n global.fetch = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n\n const badToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.e30.bad_signature'\n\n try {\n await validateProjectToken(badToken, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic - invalid tokens may fail at different stages\n // The important thing is that signature-related errors map to either SIGNATURE_INVALID or MISSING_KID\n expect(['SIGNATURE_INVALID', 'MISSING_KID']).toContain(error.code)\n }\n }\n })\n\n it('should map expired token errors to EXPIRED code', async () => {\n global.fetch = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n\n // Create token that expired 1 hour ago\n const expiredToken = createTestJWT({\n exp: Math.floor(Date.now() / 1000) - 3600,\n iat: Math.floor(Date.now() / 1000) - 7200,\n })\n\n try {\n await validateProjectToken(expiredToken, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic\n // Note: Mock tokens with bad signatures may fail signature validation before expiry check\n // In real scenarios with valid signatures, EXPIRED would be returned\n expect(['EXPIRED', 'SIGNATURE_INVALID', 'MISSING_KID']).toContain(error.code)\n }\n }\n })\n\n it('should map missing kid errors to MISSING_KID code', async () => {\n global.fetch = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n\n // Create token without kid in header\n const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url')\n const payload = Buffer.from(\n JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 3600 }),\n ).toString('base64url')\n const tokenWithoutKid = `${header}.${payload}.fake_signature`\n\n try {\n await validateProjectToken(tokenWithoutKid, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic - tokens without kid may fail at various stages\n expect(['MISSING_KID', 'SIGNATURE_INVALID']).toContain(error.code)\n }\n }\n })\n\n it('should map no matching key errors to SIGNATURE_INVALID code', async () => {\n global.fetch = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n\n // Create token with a kid that doesn't exist in JWKS\n const tokenWithWrongKid = createTestJWT(\n { exp: Math.floor(Date.now() / 1000) + 3600 },\n 'non-existent-key-id',\n )\n\n try {\n await validateProjectToken(tokenWithWrongKid, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic\n expect(error.code).toBe('SIGNATURE_INVALID')\n expect(error.message).toContain('Key not found in JWKS')\n }\n }\n })\n\n it('should handle multiple network error formats', async () => {\n const networkErrors = [\n new Error('fetch failed'),\n new Error('network error'),\n new Error('ECONNREFUSED'),\n ]\n\n for (const networkError of networkErrors) {\n clearJWKSCache()\n global.fetch = vi.fn().mockRejectedValue(networkError)\n\n const token = createTestJWT({ exp: Math.floor(Date.now() / 1000) + 3600 })\n\n try {\n await validateProjectToken(token, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n expect(error.code).toBe('JWKS_FETCH_FAILED')\n }\n }\n }\n })\n\n it('should handle generic errors with fallback', async () => {\n // Mock an unexpected error type\n global.fetch = vi.fn().mockRejectedValue('string error')\n\n const token = createTestJWT({ exp: Math.floor(Date.now() / 1000) + 3600 })\n\n try {\n await validateProjectToken(token, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Generic errors fall back to SIGNATURE_INVALID\n expect(error.code).toBe('SIGNATURE_INVALID')\n expect(error.message).toContain('JWT validation failed')\n }\n }\n })\n })\n})\n"],"names":["afterEach","beforeEach","describe","expect","it","vi","clearJWKSCache","JWTValidationError","validateProjectToken","createTestJWT","payload","kid","header","Buffer","from","JSON","stringify","alg","typ","toString","payloadStr","signature","mockJWKS","keys","e","kty","n","use","restoreAllMocks","fetchMock","fn","mockResolvedValue","json","Promise","resolve","ok","status","global","fetch","token","exp","Math","floor","Date","now","_error","firstCallCount","mock","calls","length","toBeGreaterThanOrEqual","toBeGreaterThan","mockRejectedValue","Error","fail","error","toBeInstanceOf","code","toBe","message","toContain","badToken","expiredToken","iat","tokenWithoutKid","tokenWithWrongKid","networkErrors","networkError"],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { TokenStore } from './token-store.js';
|
|
2
|
+
import type { FigmaTokens } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Error during OAuth flow
|
|
5
|
+
*/
|
|
6
|
+
export declare class OAuthFlowError extends Error {
|
|
7
|
+
cause?: Error | undefined;
|
|
8
|
+
constructor(message: string, cause?: Error | undefined);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Options for OAuth flow
|
|
12
|
+
*/
|
|
13
|
+
export interface OAuthFlowOptions {
|
|
14
|
+
/** OAuth2 client ID (defaults to config) */
|
|
15
|
+
clientId?: string;
|
|
16
|
+
/** Callback server port (defaults to 3000) */
|
|
17
|
+
port?: number;
|
|
18
|
+
/** Redirect URI (defaults to config) */
|
|
19
|
+
redirectUri?: string;
|
|
20
|
+
/** OAuth scopes (defaults to config) */
|
|
21
|
+
scopes?: string[];
|
|
22
|
+
/** Callback timeout in milliseconds (defaults to 120000) */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Result of OAuth flow
|
|
27
|
+
*/
|
|
28
|
+
export interface OAuthFlowResult {
|
|
29
|
+
/** OAuth2 tokens */
|
|
30
|
+
tokens: FigmaTokens;
|
|
31
|
+
/** Figma user ID */
|
|
32
|
+
userId?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Execute complete OAuth2 authorization flow
|
|
36
|
+
*
|
|
37
|
+
* This orchestrates the full OAuth flow:
|
|
38
|
+
* 1. Generate PKCE pair for security
|
|
39
|
+
* 2. Start local callback server
|
|
40
|
+
* 3. Open browser to authorization URL
|
|
41
|
+
* 4. Wait for user to authorize
|
|
42
|
+
* 5. Exchange authorization code for tokens
|
|
43
|
+
* 6. Store tokens securely
|
|
44
|
+
*
|
|
45
|
+
* @param tokenStore - Token store for persisting tokens
|
|
46
|
+
* @param options - Flow configuration options
|
|
47
|
+
* @returns Promise resolving to OAuth flow result
|
|
48
|
+
* @throws OAuthFlowError if any step fails
|
|
49
|
+
*/
|
|
50
|
+
export declare function executeOAuthFlow(tokenStore: TokenStore, options?: OAuthFlowOptions): Promise<OAuthFlowResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Get valid access token, refreshing if necessary
|
|
53
|
+
*
|
|
54
|
+
* Checks if stored tokens exist and are valid. If expired, automatically
|
|
55
|
+
* refreshes using the refresh token. If no tokens exist, returns null.
|
|
56
|
+
*
|
|
57
|
+
* @param tokenStore - Token store to use
|
|
58
|
+
* @returns Promise resolving to access token or null if no valid tokens
|
|
59
|
+
* @throws OAuthFlowError if refresh fails
|
|
60
|
+
*/
|
|
61
|
+
export declare function getValidAccessToken(tokenStore: TokenStore): Promise<null | string>;
|
|
62
|
+
//# sourceMappingURL=oauth-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-flow.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-flow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAU7C;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAG9B,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,MAAM,EAAE,WAAW,CAAA;IACnB,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAwI1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAyBxF"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { OAUTH_CONFIG } from '../config/oauth.js';
|
|
4
|
+
import { isDebug } from '../utils/is-debug.js';
|
|
5
|
+
import * as log from '../utils/log.js';
|
|
6
|
+
import { buildAuthorizationUrl, openBrowser } from './browser.js';
|
|
7
|
+
import { CallbackServer } from './callback-server.js';
|
|
8
|
+
import { generatePKCEPair } from './pkce.js';
|
|
9
|
+
import { exchangeCodeForTokens, refreshAccessToken } from './refresh.js';
|
|
10
|
+
/**
|
|
11
|
+
* Error during OAuth flow
|
|
12
|
+
*/ export class OAuthFlowError extends Error {
|
|
13
|
+
cause;
|
|
14
|
+
constructor(message, cause){
|
|
15
|
+
super(message), this.cause = cause;
|
|
16
|
+
this.name = 'OAuthFlowError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Execute complete OAuth2 authorization flow
|
|
21
|
+
*
|
|
22
|
+
* This orchestrates the full OAuth flow:
|
|
23
|
+
* 1. Generate PKCE pair for security
|
|
24
|
+
* 2. Start local callback server
|
|
25
|
+
* 3. Open browser to authorization URL
|
|
26
|
+
* 4. Wait for user to authorize
|
|
27
|
+
* 5. Exchange authorization code for tokens
|
|
28
|
+
* 6. Store tokens securely
|
|
29
|
+
*
|
|
30
|
+
* @param tokenStore - Token store for persisting tokens
|
|
31
|
+
* @param options - Flow configuration options
|
|
32
|
+
* @returns Promise resolving to OAuth flow result
|
|
33
|
+
* @throws OAuthFlowError if any step fails
|
|
34
|
+
*/ export async function executeOAuthFlow(tokenStore, options = {}) {
|
|
35
|
+
// Check if OAuth flow should be mocked
|
|
36
|
+
// Default to true for OAuth (allow testing without real authentication by default)
|
|
37
|
+
const shouldMock = process.env.FIGMA_MOCK_OAUTH !== 'false';
|
|
38
|
+
if (shouldMock) {
|
|
39
|
+
// MOCK IMPLEMENTATION: Return mock tokens for testing
|
|
40
|
+
if (isDebug()) {
|
|
41
|
+
log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH != "false")');
|
|
42
|
+
}
|
|
43
|
+
const mockTokens = {
|
|
44
|
+
accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),
|
|
45
|
+
expiresAt: Date.now() + 2 * 60 * 60 * 1000,
|
|
46
|
+
refreshToken: 'figt_mock_refresh_token_' + crypto.randomBytes(16).toString('hex'),
|
|
47
|
+
scopes: options.scopes || OAUTH_CONFIG.scopes,
|
|
48
|
+
tokenType: 'Bearer'
|
|
49
|
+
};
|
|
50
|
+
// Store mock tokens
|
|
51
|
+
tokenStore.setTokens(mockTokens);
|
|
52
|
+
return {
|
|
53
|
+
tokens: mockTokens,
|
|
54
|
+
userId: 'mock_user_123'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// REAL OAUTH IMPLEMENTATION
|
|
58
|
+
const { clientId = OAUTH_CONFIG.clientId, port = 3000, redirectUri = OAUTH_CONFIG.redirectUri, scopes = OAUTH_CONFIG.scopes, timeoutMs = 120000 } = options;
|
|
59
|
+
// Generate PKCE pair for enhanced security
|
|
60
|
+
const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair();
|
|
61
|
+
// Generate random state for CSRF protection
|
|
62
|
+
const state = crypto.randomBytes(32).toString('base64url');
|
|
63
|
+
// Create callback server
|
|
64
|
+
const callbackServer = new CallbackServer({
|
|
65
|
+
port,
|
|
66
|
+
timeoutMs
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
// Start waiting for callback (this starts the server and captures actual port)
|
|
70
|
+
const callbackPromise = callbackServer.waitForCallback(state);
|
|
71
|
+
// Wait a bit for server to start and determine actual port
|
|
72
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
73
|
+
// Get the actual port (may differ from preferred if that was in use)
|
|
74
|
+
const actualPort = callbackServer.getActualPort();
|
|
75
|
+
// Build redirect URI with actual port
|
|
76
|
+
const actualRedirectUri = redirectUri.replace(/:\d+/, `:${actualPort}`);
|
|
77
|
+
// Build authorization URL
|
|
78
|
+
const authUrl = buildAuthorizationUrl({
|
|
79
|
+
authorizationUrl: OAUTH_CONFIG.authorizationUrl,
|
|
80
|
+
clientId,
|
|
81
|
+
codeChallenge,
|
|
82
|
+
codeChallengeMethod,
|
|
83
|
+
redirectUri: actualRedirectUri,
|
|
84
|
+
scopes,
|
|
85
|
+
state
|
|
86
|
+
});
|
|
87
|
+
// Prompt user to open browser
|
|
88
|
+
const shouldOpenBrowser = await p.confirm({
|
|
89
|
+
initialValue: true,
|
|
90
|
+
message: 'Log in or create Figma account in your browser?'
|
|
91
|
+
});
|
|
92
|
+
if (p.isCancel(shouldOpenBrowser)) {
|
|
93
|
+
throw new OAuthFlowError('Authentication cancelled by user');
|
|
94
|
+
}
|
|
95
|
+
if (shouldOpenBrowser) {
|
|
96
|
+
// Try to open browser, but don't fail if it doesn't work
|
|
97
|
+
try {
|
|
98
|
+
await openBrowser(authUrl);
|
|
99
|
+
} catch (_error) {
|
|
100
|
+
// Browser opening failed, show URL for manual opening
|
|
101
|
+
log.warning('Could not open browser automatically.');
|
|
102
|
+
log.info(`Please open this URL in your browser:`);
|
|
103
|
+
log.info(` ${authUrl}`);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// User declined automatic opening, show URL
|
|
107
|
+
log.info(`Please open this URL manually in your browser:`);
|
|
108
|
+
log.info(` ${authUrl}`);
|
|
109
|
+
}
|
|
110
|
+
// Wait for callback
|
|
111
|
+
const { code } = await callbackPromise;
|
|
112
|
+
// Exchange authorization code for tokens
|
|
113
|
+
let tokens;
|
|
114
|
+
try {
|
|
115
|
+
tokens = await exchangeCodeForTokens(code, codeVerifier, actualRedirectUri, clientId);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new OAuthFlowError('Failed to exchange authorization code for tokens', error instanceof Error ? error : undefined);
|
|
118
|
+
}
|
|
119
|
+
// Store tokens
|
|
120
|
+
try {
|
|
121
|
+
tokenStore.setTokens(tokens);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new OAuthFlowError('Failed to store tokens', error instanceof Error ? error : undefined);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
tokens,
|
|
127
|
+
userId: tokens.userId
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Clean up server on error
|
|
131
|
+
callbackServer.stop();
|
|
132
|
+
if (error instanceof OAuthFlowError) {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
throw new OAuthFlowError(`OAuth flow failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
136
|
+
} finally{
|
|
137
|
+
// Ensure server is always stopped
|
|
138
|
+
callbackServer.stop();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get valid access token, refreshing if necessary
|
|
143
|
+
*
|
|
144
|
+
* Checks if stored tokens exist and are valid. If expired, automatically
|
|
145
|
+
* refreshes using the refresh token. If no tokens exist, returns null.
|
|
146
|
+
*
|
|
147
|
+
* @param tokenStore - Token store to use
|
|
148
|
+
* @returns Promise resolving to access token or null if no valid tokens
|
|
149
|
+
* @throws OAuthFlowError if refresh fails
|
|
150
|
+
*/ export async function getValidAccessToken(tokenStore) {
|
|
151
|
+
// Check if we have valid tokens
|
|
152
|
+
if (tokenStore.hasValidTokens()) {
|
|
153
|
+
return tokenStore.getAccessToken();
|
|
154
|
+
}
|
|
155
|
+
// Check if we have a refresh token
|
|
156
|
+
const refreshToken = tokenStore.getRefreshToken();
|
|
157
|
+
if (!refreshToken) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// Try to refresh
|
|
161
|
+
try {
|
|
162
|
+
const tokens = await refreshAccessToken(refreshToken);
|
|
163
|
+
tokenStore.setTokens(tokens);
|
|
164
|
+
return tokens.accessToken;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
// Refresh failed - clear invalid tokens
|
|
167
|
+
tokenStore.clearTokens();
|
|
168
|
+
throw new OAuthFlowError('Failed to refresh access token', error instanceof Error ? error : undefined);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=oauth-flow.js.map
|