@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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Token Management
|
|
3
|
+
*
|
|
4
|
+
* Handles project token (JWT) generation and refresh for authenticating
|
|
5
|
+
* to the Content API. Project tokens are scoped to a specific tenant/CMS
|
|
6
|
+
* and have a shorter lifespan (15-30 minutes) than OAuth tokens.
|
|
7
|
+
*/ import { getProjectToken as fetchProjectToken } from '../api/figma-api.js';
|
|
8
|
+
import { getEnvironment } from '../constants.js';
|
|
9
|
+
import { isDebug } from '../utils/is-debug.js';
|
|
10
|
+
import * as log from '../utils/log.js';
|
|
11
|
+
import { JWTValidationError, validateProjectToken } from './jwt-validator.js';
|
|
12
|
+
import { getValidAccessToken } from './oauth-flow.js';
|
|
13
|
+
/**
|
|
14
|
+
* Error during project token operations
|
|
15
|
+
*/ export class ProjectTokenError extends Error {
|
|
16
|
+
cause;
|
|
17
|
+
constructor(message, cause){
|
|
18
|
+
super(message), this.cause = cause;
|
|
19
|
+
this.name = 'ProjectTokenError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse JWT claims from token without validation
|
|
24
|
+
* Used for mock tokens where signature validation is not needed
|
|
25
|
+
*
|
|
26
|
+
* @param token - JWT token string
|
|
27
|
+
* @returns Parsed JWT payload
|
|
28
|
+
* @throws {ProjectTokenError} If JWT cannot be parsed
|
|
29
|
+
*/ function parseJWTClaims(token) {
|
|
30
|
+
try {
|
|
31
|
+
// JWT format: header.payload.signature
|
|
32
|
+
const parts = token.split('.');
|
|
33
|
+
if (parts.length !== 3) {
|
|
34
|
+
throw new Error('Invalid JWT format');
|
|
35
|
+
}
|
|
36
|
+
// Decode the payload (second part)
|
|
37
|
+
const payload = parts[1];
|
|
38
|
+
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
39
|
+
const paddedBase64 = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=');
|
|
40
|
+
const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8'));
|
|
41
|
+
return decodedPayload;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new ProjectTokenError(`Failed to parse JWT claims: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get a valid project token for a tenant, refreshing if necessary
|
|
48
|
+
*
|
|
49
|
+
* This function:
|
|
50
|
+
* 1. Checks if a stored project token exists and is valid (not expired)
|
|
51
|
+
* 2. If expired or missing, gets a valid user access token
|
|
52
|
+
* 3. Calls the Figma API to generate a new project token
|
|
53
|
+
* 4. Stores the new project token in encrypted storage
|
|
54
|
+
* 5. Returns the JWT string
|
|
55
|
+
*
|
|
56
|
+
* The project token is used by local Payload instances to authenticate
|
|
57
|
+
* requests to the Content API without requiring a round-trip to Sinatra.
|
|
58
|
+
*
|
|
59
|
+
* @param tokenStore - Token store for persisting tokens
|
|
60
|
+
* @param tenantId - The tenant/CMS ID to get a token for
|
|
61
|
+
* @returns Promise resolving to JWT token string or null if failed
|
|
62
|
+
* @throws {ProjectTokenError} If token generation fails
|
|
63
|
+
*/ export async function getValidProjectToken(tokenStore, tenantId) {
|
|
64
|
+
// Check if we have a valid project token already
|
|
65
|
+
if (tokenStore.hasValidProjectToken(tenantId)) {
|
|
66
|
+
const projectToken = tokenStore.getProjectToken(tenantId);
|
|
67
|
+
return projectToken?.token || null;
|
|
68
|
+
}
|
|
69
|
+
// We need to refresh the project token
|
|
70
|
+
// First, get a valid user access token (will auto-refresh if needed)
|
|
71
|
+
let accessToken;
|
|
72
|
+
try {
|
|
73
|
+
accessToken = await getValidAccessToken(tokenStore);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new ProjectTokenError('Failed to get valid access token for project token refresh', error instanceof Error ? error : undefined);
|
|
76
|
+
}
|
|
77
|
+
if (!accessToken) {
|
|
78
|
+
// No valid user tokens available - user needs to authenticate
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// Fetch a new project token from the Figma API
|
|
82
|
+
const projectToken = await fetchProjectToken(accessToken, tenantId);
|
|
83
|
+
// Validate the JWT signature and claims (skip for mocks)
|
|
84
|
+
const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
|
|
85
|
+
let validatedClaims;
|
|
86
|
+
if (shouldMock) {
|
|
87
|
+
// MOCK: Skip JWT validation and parse claims directly
|
|
88
|
+
if (isDebug()) {
|
|
89
|
+
log.debug('Skipping JWT validation for mock project token');
|
|
90
|
+
}
|
|
91
|
+
validatedClaims = parseJWTClaims(projectToken.token);
|
|
92
|
+
} else {
|
|
93
|
+
// REAL: Validate JWT signature against JWKS
|
|
94
|
+
try {
|
|
95
|
+
validatedClaims = await validateProjectToken(projectToken.token, getEnvironment());
|
|
96
|
+
} catch (validationError) {
|
|
97
|
+
// Handle validation errors
|
|
98
|
+
if (validationError instanceof JWTValidationError) {
|
|
99
|
+
throw new ProjectTokenError(`Project token validation failed: ${validationError.message}`, validationError);
|
|
100
|
+
}
|
|
101
|
+
throw validationError;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Ensure exp claim is present
|
|
105
|
+
if (!validatedClaims.exp) {
|
|
106
|
+
throw new ProjectTokenError('Project token missing expiration (exp) claim');
|
|
107
|
+
}
|
|
108
|
+
// Store the project token with validated expiration and claims
|
|
109
|
+
tokenStore.setProjectToken(tenantId, {
|
|
110
|
+
claims: validatedClaims,
|
|
111
|
+
expiresAt: validatedClaims.exp * 1000,
|
|
112
|
+
tenantId,
|
|
113
|
+
token: projectToken.token
|
|
114
|
+
});
|
|
115
|
+
return projectToken.token;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Refresh a project token for a tenant
|
|
119
|
+
*
|
|
120
|
+
* Forces a refresh of the project token even if the current one is still valid.
|
|
121
|
+
* Useful for testing or when you want to ensure you have the freshest token.
|
|
122
|
+
*
|
|
123
|
+
* @param tokenStore - Token store for persisting tokens
|
|
124
|
+
* @param tenantId - The tenant/CMS ID to refresh token for
|
|
125
|
+
* @returns Promise resolving to JWT token string or null if failed
|
|
126
|
+
* @throws {ProjectTokenError} If token refresh fails
|
|
127
|
+
*/ export async function refreshProjectToken(tokenStore, tenantId) {
|
|
128
|
+
// Clear the existing project token to force a refresh
|
|
129
|
+
tokenStore.clearProjectToken(tenantId);
|
|
130
|
+
// Get a new token (which will fetch from API since we just cleared it)
|
|
131
|
+
return getValidProjectToken(tokenStore, tenantId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=project-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/project-token.ts"],"sourcesContent":["/**\n * Project Token Management\n *\n * Handles project token (JWT) generation and refresh for authenticating\n * to the Content API. Project tokens are scoped to a specific tenant/CMS\n * and have a shorter lifespan (15-30 minutes) than OAuth tokens.\n */\n\nimport type { TokenStore } from './token-store.js'\nimport type { JWTPayload } from './types.js'\n\nimport { getProjectToken as fetchProjectToken } from '../api/figma-api.js'\nimport { getEnvironment } from '../constants.js'\nimport { isDebug } from '../utils/is-debug.js'\nimport * as log from '../utils/log.js'\nimport { JWTValidationError, validateProjectToken } from './jwt-validator.js'\nimport { getValidAccessToken } from './oauth-flow.js'\n\n/**\n * Error during project token operations\n */\nexport class ProjectTokenError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'ProjectTokenError'\n }\n}\n\n/**\n * Parse JWT claims from token without validation\n * Used for mock tokens where signature validation is not needed\n *\n * @param token - JWT token string\n * @returns Parsed JWT payload\n * @throws {ProjectTokenError} If JWT cannot be parsed\n */\nfunction parseJWTClaims(token: string): JWTPayload {\n try {\n // JWT format: header.payload.signature\n const parts = token.split('.')\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format')\n }\n\n // Decode the payload (second part)\n const payload = parts[1]\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=')\n\n const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8'))\n\n return decodedPayload as JWTPayload\n } catch (error) {\n throw new ProjectTokenError(\n `Failed to parse JWT claims: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Get a valid project token for a tenant, refreshing if necessary\n *\n * This function:\n * 1. Checks if a stored project token exists and is valid (not expired)\n * 2. If expired or missing, gets a valid user access token\n * 3. Calls the Figma API to generate a new project token\n * 4. Stores the new project token in encrypted storage\n * 5. Returns the JWT string\n *\n * The project token is used by local Payload instances to authenticate\n * requests to the Content API without requiring a round-trip to Sinatra.\n *\n * @param tokenStore - Token store for persisting tokens\n * @param tenantId - The tenant/CMS ID to get a token for\n * @returns Promise resolving to JWT token string or null if failed\n * @throws {ProjectTokenError} If token generation fails\n */\nexport async function getValidProjectToken(\n tokenStore: TokenStore,\n tenantId: string,\n): Promise<null | string> {\n // Check if we have a valid project token already\n if (tokenStore.hasValidProjectToken(tenantId)) {\n const projectToken = tokenStore.getProjectToken(tenantId)\n return projectToken?.token || null\n }\n\n // We need to refresh the project token\n // First, get a valid user access token (will auto-refresh if needed)\n let accessToken: null | string\n try {\n accessToken = await getValidAccessToken(tokenStore)\n } catch (error) {\n throw new ProjectTokenError(\n 'Failed to get valid access token for project token refresh',\n error instanceof Error ? error : undefined,\n )\n }\n\n if (!accessToken) {\n // No valid user tokens available - user needs to authenticate\n return null\n }\n\n // Fetch a new project token from the Figma API\n const projectToken = await fetchProjectToken(accessToken, tenantId)\n\n // Validate the JWT signature and claims (skip for mocks)\n const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false'\n let validatedClaims: JWTPayload\n\n if (shouldMock) {\n // MOCK: Skip JWT validation and parse claims directly\n if (isDebug()) {\n log.debug('Skipping JWT validation for mock project token')\n }\n validatedClaims = parseJWTClaims(projectToken.token)\n } else {\n // REAL: Validate JWT signature against JWKS\n try {\n validatedClaims = await validateProjectToken(projectToken.token, getEnvironment())\n } catch (validationError) {\n // Handle validation errors\n if (validationError instanceof JWTValidationError) {\n throw new ProjectTokenError(\n `Project token validation failed: ${validationError.message}`,\n validationError,\n )\n }\n throw validationError\n }\n }\n\n // Ensure exp claim is present\n if (!validatedClaims.exp) {\n throw new ProjectTokenError('Project token missing expiration (exp) claim')\n }\n\n // Store the project token with validated expiration and claims\n tokenStore.setProjectToken(tenantId, {\n claims: validatedClaims,\n expiresAt: validatedClaims.exp * 1000, // Convert from seconds to milliseconds\n tenantId,\n token: projectToken.token,\n })\n\n return projectToken.token\n}\n\n/**\n * Refresh a project token for a tenant\n *\n * Forces a refresh of the project token even if the current one is still valid.\n * Useful for testing or when you want to ensure you have the freshest token.\n *\n * @param tokenStore - Token store for persisting tokens\n * @param tenantId - The tenant/CMS ID to refresh token for\n * @returns Promise resolving to JWT token string or null if failed\n * @throws {ProjectTokenError} If token refresh fails\n */\nexport async function refreshProjectToken(\n tokenStore: TokenStore,\n tenantId: string,\n): Promise<null | string> {\n // Clear the existing project token to force a refresh\n tokenStore.clearProjectToken(tenantId)\n\n // Get a new token (which will fetch from API since we just cleared it)\n return getValidProjectToken(tokenStore, tenantId)\n}\n"],"names":["getProjectToken","fetchProjectToken","getEnvironment","isDebug","log","JWTValidationError","validateProjectToken","getValidAccessToken","ProjectTokenError","Error","message","cause","name","parseJWTClaims","token","parts","split","length","payload","base64","replace","paddedBase64","padEnd","decodedPayload","JSON","parse","Buffer","from","toString","error","undefined","getValidProjectToken","tokenStore","tenantId","hasValidProjectToken","projectToken","accessToken","shouldMock","process","env","FIGMA_MOCK_PROJECT_TOKEN","validatedClaims","debug","validationError","exp","setProjectToken","claims","expiresAt","refreshProjectToken","clearProjectToken"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-token.spec.d.ts","sourceRoot":"","sources":["../../src/auth/project-token.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as figmaApi from '../api/figma-api.js';
|
|
3
|
+
import * as constants from '../constants.js';
|
|
4
|
+
import { JWTValidationError, validateProjectToken } from './jwt-validator.js';
|
|
5
|
+
import * as oauthFlow from './oauth-flow.js';
|
|
6
|
+
import { getValidProjectToken, ProjectTokenError, refreshProjectToken } from './project-token.js';
|
|
7
|
+
import { TokenStore } from './token-store.js';
|
|
8
|
+
// Mock all external dependencies
|
|
9
|
+
vi.mock('../api/figma-api.js');
|
|
10
|
+
vi.mock('./oauth-flow.js');
|
|
11
|
+
vi.mock('../constants.js');
|
|
12
|
+
// Mock only the validateProjectToken function from jwt-validator
|
|
13
|
+
// Keep JWTValidationError class unmocked so it works properly in tests
|
|
14
|
+
vi.mock('./jwt-validator.js', async (importOriginal)=>{
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
16
|
+
const actual = await importOriginal();
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
validateProjectToken: vi.fn()
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
describe('Project Token Management', ()=>{
|
|
23
|
+
let tokenStore;
|
|
24
|
+
const mockTenantId = 'test-tenant-123';
|
|
25
|
+
const mockAccessToken = 'figt_mock_access_token';
|
|
26
|
+
const mockProjectTokenJWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.mock.signature';
|
|
27
|
+
let originalEnv;
|
|
28
|
+
beforeEach(()=>{
|
|
29
|
+
// Create a unique test store for each test
|
|
30
|
+
tokenStore = new TokenStore({
|
|
31
|
+
configName: `payloadcms-figma-test-${Date.now()}`
|
|
32
|
+
});
|
|
33
|
+
// Disable mocking to test real validation flow
|
|
34
|
+
originalEnv = process.env.FIGMA_MOCK_PROJECT_TOKEN;
|
|
35
|
+
process.env.FIGMA_MOCK_PROJECT_TOKEN = 'false';
|
|
36
|
+
// Default mock implementations
|
|
37
|
+
vi.mocked(constants.getEnvironment).mockReturnValue('staging');
|
|
38
|
+
vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(mockAccessToken);
|
|
39
|
+
vi.mocked(figmaApi.getProjectToken).mockResolvedValue({
|
|
40
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
41
|
+
tenantId: mockTenantId,
|
|
42
|
+
token: mockProjectTokenJWT
|
|
43
|
+
});
|
|
44
|
+
vi.mocked(validateProjectToken).mockResolvedValue({
|
|
45
|
+
aud: mockTenantId,
|
|
46
|
+
email: 'test@example.com',
|
|
47
|
+
exp: Math.floor(Date.now() / 1000) + 1800,
|
|
48
|
+
iat: Math.floor(Date.now() / 1000),
|
|
49
|
+
seat_type: 'full'
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
afterEach(()=>{
|
|
53
|
+
tokenStore.clearTokens();
|
|
54
|
+
tokenStore.clearAllProjectTokens();
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
// Restore original environment
|
|
57
|
+
if (originalEnv === undefined) {
|
|
58
|
+
delete process.env.FIGMA_MOCK_PROJECT_TOKEN;
|
|
59
|
+
} else {
|
|
60
|
+
process.env.FIGMA_MOCK_PROJECT_TOKEN = originalEnv;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
describe('getValidProjectToken', ()=>{
|
|
64
|
+
describe('with valid cached token', ()=>{
|
|
65
|
+
it('should return cached token immediately without API calls', async ()=>{
|
|
66
|
+
// Store a valid project token
|
|
67
|
+
const validToken = {
|
|
68
|
+
claims: {
|
|
69
|
+
aud: mockTenantId,
|
|
70
|
+
email: 'test@example.com',
|
|
71
|
+
exp: Math.floor(Date.now() / 1000) + 1800,
|
|
72
|
+
iat: Math.floor(Date.now() / 1000),
|
|
73
|
+
seat_type: 'full'
|
|
74
|
+
},
|
|
75
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
76
|
+
tenantId: mockTenantId,
|
|
77
|
+
token: mockProjectTokenJWT
|
|
78
|
+
};
|
|
79
|
+
tokenStore.setProjectToken(mockTenantId, validToken);
|
|
80
|
+
const result = await getValidProjectToken(tokenStore, mockTenantId);
|
|
81
|
+
expect(result).toBe(mockProjectTokenJWT);
|
|
82
|
+
// Should not call any external APIs
|
|
83
|
+
expect(oauthFlow.getValidAccessToken).not.toHaveBeenCalled();
|
|
84
|
+
expect(figmaApi.getProjectToken).not.toHaveBeenCalled();
|
|
85
|
+
expect(validateProjectToken).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
it('should use hasValidProjectToken for expiry check', async ()=>{
|
|
88
|
+
// Mock hasValidProjectToken to return true
|
|
89
|
+
const hasValidSpy = vi.spyOn(tokenStore, 'hasValidProjectToken').mockReturnValue(true);
|
|
90
|
+
const getTokenSpy = vi.spyOn(tokenStore, 'getProjectToken').mockReturnValue({
|
|
91
|
+
claims: {
|
|
92
|
+
exp: Math.floor(Date.now() / 1000) + 1800
|
|
93
|
+
},
|
|
94
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
95
|
+
tenantId: mockTenantId,
|
|
96
|
+
token: mockProjectTokenJWT
|
|
97
|
+
});
|
|
98
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
99
|
+
expect(hasValidSpy).toHaveBeenCalledWith(mockTenantId);
|
|
100
|
+
expect(getTokenSpy).toHaveBeenCalledWith(mockTenantId);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('with expired or missing token', ()=>{
|
|
104
|
+
it('should fetch new token when no cached token exists', async ()=>{
|
|
105
|
+
const result = await getValidProjectToken(tokenStore, mockTenantId);
|
|
106
|
+
expect(result).toBe(mockProjectTokenJWT);
|
|
107
|
+
expect(oauthFlow.getValidAccessToken).toHaveBeenCalledWith(tokenStore);
|
|
108
|
+
expect(figmaApi.getProjectToken).toHaveBeenCalledWith(mockAccessToken, mockTenantId);
|
|
109
|
+
expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'staging');
|
|
110
|
+
});
|
|
111
|
+
it('should fetch new token when cached token is expired', async ()=>{
|
|
112
|
+
// Mock hasValidProjectToken to return false (expired)
|
|
113
|
+
vi.spyOn(tokenStore, 'hasValidProjectToken').mockReturnValue(false);
|
|
114
|
+
const result = await getValidProjectToken(tokenStore, mockTenantId);
|
|
115
|
+
expect(result).toBe(mockProjectTokenJWT);
|
|
116
|
+
expect(figmaApi.getProjectToken).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
it('should apply 5-minute expiry buffer through hasValidProjectToken', async ()=>{
|
|
119
|
+
// Store a token expiring in 4 minutes (within 5-minute buffer)
|
|
120
|
+
const expiringToken = {
|
|
121
|
+
claims: {
|
|
122
|
+
exp: Math.floor(Date.now() / 1000) + 240
|
|
123
|
+
},
|
|
124
|
+
expiresAt: Date.now() + 4 * 60 * 1000,
|
|
125
|
+
tenantId: mockTenantId,
|
|
126
|
+
token: 'expiring-token'
|
|
127
|
+
};
|
|
128
|
+
tokenStore.setProjectToken(mockTenantId, expiringToken);
|
|
129
|
+
// hasValidProjectToken should return false due to buffer
|
|
130
|
+
expect(tokenStore.hasValidProjectToken(mockTenantId)).toBe(false);
|
|
131
|
+
// Should refresh the token
|
|
132
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
133
|
+
expect(figmaApi.getProjectToken).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('JWT validation integration', ()=>{
|
|
137
|
+
it('should validate JWT signature after fetching', async ()=>{
|
|
138
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
139
|
+
expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'staging');
|
|
140
|
+
});
|
|
141
|
+
it('should use correct environment for validation', async ()=>{
|
|
142
|
+
vi.mocked(constants.getEnvironment).mockReturnValue('production');
|
|
143
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
144
|
+
expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'production');
|
|
145
|
+
});
|
|
146
|
+
it('should throw ProjectTokenError on validation failure', async ()=>{
|
|
147
|
+
const validationError = new JWTValidationError('Invalid signature', 'SIGNATURE_INVALID');
|
|
148
|
+
vi.mocked(validateProjectToken).mockRejectedValue(validationError);
|
|
149
|
+
try {
|
|
150
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
151
|
+
expect.fail('Should have thrown ProjectTokenError');
|
|
152
|
+
} catch (error) {
|
|
153
|
+
expect(error).toBeInstanceOf(ProjectTokenError);
|
|
154
|
+
if (error instanceof ProjectTokenError) {
|
|
155
|
+
expect(error.message).toContain('Project token validation failed');
|
|
156
|
+
expect(error.message).toContain('Invalid signature');
|
|
157
|
+
expect(error.cause).toBe(validationError);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
it('should rethrow non-JWTValidationError errors', async ()=>{
|
|
162
|
+
const genericError = new Error('Network error');
|
|
163
|
+
vi.mocked(validateProjectToken).mockRejectedValue(genericError);
|
|
164
|
+
await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(genericError);
|
|
165
|
+
});
|
|
166
|
+
it('should throw error if JWT missing exp claim', async ()=>{
|
|
167
|
+
// Mock validation returning claims without exp
|
|
168
|
+
vi.mocked(validateProjectToken).mockResolvedValue({
|
|
169
|
+
aud: mockTenantId
|
|
170
|
+
});
|
|
171
|
+
await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(ProjectTokenError);
|
|
172
|
+
await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow('missing expiration (exp) claim');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('claims storage', ()=>{
|
|
176
|
+
it('should store validated claims with token', async ()=>{
|
|
177
|
+
const mockClaims = {
|
|
178
|
+
aud: mockTenantId,
|
|
179
|
+
email: 'user@example.com',
|
|
180
|
+
exp: Math.floor(Date.now() / 1000) + 1800,
|
|
181
|
+
iat: Math.floor(Date.now() / 1000),
|
|
182
|
+
seat_type: 'full'
|
|
183
|
+
};
|
|
184
|
+
vi.mocked(validateProjectToken).mockResolvedValue(mockClaims);
|
|
185
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
186
|
+
const storedToken = tokenStore.getProjectToken(mockTenantId);
|
|
187
|
+
expect(storedToken?.claims).toEqual(mockClaims);
|
|
188
|
+
});
|
|
189
|
+
it('should convert exp from seconds to milliseconds for expiresAt', async ()=>{
|
|
190
|
+
const expInSeconds = Math.floor(Date.now() / 1000) + 1800 // 30 min from now
|
|
191
|
+
;
|
|
192
|
+
vi.mocked(validateProjectToken).mockResolvedValue({
|
|
193
|
+
aud: mockTenantId,
|
|
194
|
+
exp: expInSeconds
|
|
195
|
+
});
|
|
196
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
197
|
+
const storedToken = tokenStore.getProjectToken(mockTenantId);
|
|
198
|
+
expect(storedToken?.expiresAt).toBe(expInSeconds * 1000);
|
|
199
|
+
});
|
|
200
|
+
it('should store token with correct tenantId', async ()=>{
|
|
201
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
202
|
+
const storedToken = tokenStore.getProjectToken(mockTenantId);
|
|
203
|
+
expect(storedToken?.tenantId).toBe(mockTenantId);
|
|
204
|
+
expect(storedToken?.token).toBe(mockProjectTokenJWT);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('error handling', ()=>{
|
|
208
|
+
it('should throw ProjectTokenError when no user access token available', async ()=>{
|
|
209
|
+
vi.mocked(oauthFlow.getValidAccessToken).mockRejectedValue(new Error('User not authenticated'));
|
|
210
|
+
await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(ProjectTokenError);
|
|
211
|
+
await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow('Failed to get valid access token');
|
|
212
|
+
});
|
|
213
|
+
it('should return null when getValidAccessToken returns null', async ()=>{
|
|
214
|
+
vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(null);
|
|
215
|
+
const result = await getValidProjectToken(tokenStore, mockTenantId);
|
|
216
|
+
expect(result).toBeNull();
|
|
217
|
+
// Should not call API if no access token
|
|
218
|
+
expect(figmaApi.getProjectToken).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
it('should handle API 401 errors', async ()=>{
|
|
221
|
+
const apiError = new figmaApi.FigmaApiError('Unauthorized', 401);
|
|
222
|
+
vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError);
|
|
223
|
+
try {
|
|
224
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
225
|
+
expect.fail('Should have thrown error');
|
|
226
|
+
} catch (error) {
|
|
227
|
+
expect(error).toBe(apiError);
|
|
228
|
+
expect(error).toBeInstanceOf(figmaApi.FigmaApiError);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
it('should handle API 404 errors (tenant not found)', async ()=>{
|
|
232
|
+
const apiError = new figmaApi.FigmaApiError('Tenant not found', 404);
|
|
233
|
+
vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError);
|
|
234
|
+
try {
|
|
235
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
236
|
+
expect.fail('Should have thrown error');
|
|
237
|
+
} catch (error) {
|
|
238
|
+
expect(error).toBe(apiError);
|
|
239
|
+
expect(error).toBeInstanceOf(figmaApi.FigmaApiError);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
it('should handle network errors', async ()=>{
|
|
243
|
+
const networkError = new Error('Network timeout');
|
|
244
|
+
vi.mocked(figmaApi.getProjectToken).mockRejectedValue(networkError);
|
|
245
|
+
await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(networkError);
|
|
246
|
+
});
|
|
247
|
+
it('should wrap getValidAccessToken errors in ProjectTokenError', async ()=>{
|
|
248
|
+
const authError = new Error('OAuth refresh failed');
|
|
249
|
+
vi.mocked(oauthFlow.getValidAccessToken).mockRejectedValue(authError);
|
|
250
|
+
try {
|
|
251
|
+
await getValidProjectToken(tokenStore, mockTenantId);
|
|
252
|
+
expect.fail('Should have thrown ProjectTokenError');
|
|
253
|
+
} catch (error) {
|
|
254
|
+
expect(error).toBeInstanceOf(ProjectTokenError);
|
|
255
|
+
if (error instanceof ProjectTokenError) {
|
|
256
|
+
expect(error.cause).toBe(authError);
|
|
257
|
+
expect(error.message).toContain('Failed to get valid access token');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
describe('refreshProjectToken', ()=>{
|
|
264
|
+
it('should clear existing token before fetching new one', async ()=>{
|
|
265
|
+
// Store an existing token
|
|
266
|
+
const existingToken = {
|
|
267
|
+
claims: {
|
|
268
|
+
exp: Math.floor(Date.now() / 1000) + 1800
|
|
269
|
+
},
|
|
270
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
271
|
+
tenantId: mockTenantId,
|
|
272
|
+
token: 'old-token'
|
|
273
|
+
};
|
|
274
|
+
tokenStore.setProjectToken(mockTenantId, existingToken);
|
|
275
|
+
const clearSpy = vi.spyOn(tokenStore, 'clearProjectToken');
|
|
276
|
+
await refreshProjectToken(tokenStore, mockTenantId);
|
|
277
|
+
expect(clearSpy).toHaveBeenCalledWith(mockTenantId);
|
|
278
|
+
});
|
|
279
|
+
it('should call getValidProjectToken after clearing', async ()=>{
|
|
280
|
+
const result = await refreshProjectToken(tokenStore, mockTenantId);
|
|
281
|
+
expect(result).toBe(mockProjectTokenJWT);
|
|
282
|
+
// Verify API was called (meaning cache was bypassed)
|
|
283
|
+
expect(figmaApi.getProjectToken).toHaveBeenCalled();
|
|
284
|
+
});
|
|
285
|
+
it('should force refresh even if token is still valid', async ()=>{
|
|
286
|
+
// Store a valid token
|
|
287
|
+
const validToken = {
|
|
288
|
+
claims: {
|
|
289
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
290
|
+
},
|
|
291
|
+
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
292
|
+
tenantId: mockTenantId,
|
|
293
|
+
token: 'valid-token'
|
|
294
|
+
};
|
|
295
|
+
tokenStore.setProjectToken(mockTenantId, validToken);
|
|
296
|
+
// Verify it's considered valid
|
|
297
|
+
expect(tokenStore.hasValidProjectToken(mockTenantId)).toBe(true);
|
|
298
|
+
await refreshProjectToken(tokenStore, mockTenantId);
|
|
299
|
+
// Should have fetched new token despite valid cache
|
|
300
|
+
expect(figmaApi.getProjectToken).toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
it('should return new token after refresh', async ()=>{
|
|
303
|
+
const newTokenJWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.new.signature';
|
|
304
|
+
vi.mocked(figmaApi.getProjectToken).mockResolvedValue({
|
|
305
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
306
|
+
tenantId: mockTenantId,
|
|
307
|
+
token: newTokenJWT
|
|
308
|
+
});
|
|
309
|
+
const result = await refreshProjectToken(tokenStore, mockTenantId);
|
|
310
|
+
expect(result).toBe(newTokenJWT);
|
|
311
|
+
expect(tokenStore.getProjectToken(mockTenantId)?.token).toBe(newTokenJWT);
|
|
312
|
+
});
|
|
313
|
+
it('should propagate errors from getValidProjectToken', async ()=>{
|
|
314
|
+
const apiError = new figmaApi.FigmaApiError('API Error', 500);
|
|
315
|
+
vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError);
|
|
316
|
+
try {
|
|
317
|
+
await refreshProjectToken(tokenStore, mockTenantId);
|
|
318
|
+
expect.fail('Should have thrown error');
|
|
319
|
+
} catch (error) {
|
|
320
|
+
expect(error).toBe(apiError);
|
|
321
|
+
expect(error).toBeInstanceOf(figmaApi.FigmaApiError);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
it('should return null when refresh fails due to no access token', async ()=>{
|
|
325
|
+
vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(null);
|
|
326
|
+
const result = await refreshProjectToken(tokenStore, mockTenantId);
|
|
327
|
+
expect(result).toBeNull();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
//# sourceMappingURL=project-token.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/project-token.spec.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport type { ProjectToken } from './types.js'\n\nimport * as figmaApi from '../api/figma-api.js'\nimport * as constants from '../constants.js'\nimport { JWTValidationError, validateProjectToken } from './jwt-validator.js'\nimport * as oauthFlow from './oauth-flow.js'\nimport { getValidProjectToken, ProjectTokenError, refreshProjectToken } from './project-token.js'\nimport { TokenStore } from './token-store.js'\n\n// Mock all external dependencies\nvi.mock('../api/figma-api.js')\nvi.mock('./oauth-flow.js')\nvi.mock('../constants.js')\n\n// Mock only the validateProjectToken function from jwt-validator\n// Keep JWTValidationError class unmocked so it works properly in tests\nvi.mock('./jwt-validator.js', async (importOriginal) => {\n // eslint-disable-next-line @typescript-eslint/consistent-type-imports\n const actual = await importOriginal<typeof import('./jwt-validator.js')>()\n return {\n ...actual,\n validateProjectToken: vi.fn(),\n }\n})\n\ndescribe('Project Token Management', () => {\n let tokenStore: TokenStore\n const mockTenantId = 'test-tenant-123'\n const mockAccessToken = 'figt_mock_access_token'\n const mockProjectTokenJWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.mock.signature'\n let originalEnv: string | undefined\n\n beforeEach(() => {\n // Create a unique test store for each test\n tokenStore = new TokenStore({ configName: `payloadcms-figma-test-${Date.now()}` })\n\n // Disable mocking to test real validation flow\n originalEnv = process.env.FIGMA_MOCK_PROJECT_TOKEN\n process.env.FIGMA_MOCK_PROJECT_TOKEN = 'false'\n\n // Default mock implementations\n vi.mocked(constants.getEnvironment).mockReturnValue('staging')\n vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(mockAccessToken)\n vi.mocked(figmaApi.getProjectToken).mockResolvedValue({\n expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutes (milliseconds)\n tenantId: mockTenantId,\n token: mockProjectTokenJWT,\n })\n vi.mocked(validateProjectToken).mockResolvedValue({\n aud: mockTenantId, // Audience claim contains the CMS ID\n email: 'test@example.com',\n exp: Math.floor(Date.now() / 1000) + 1800, // 30 minutes in seconds\n iat: Math.floor(Date.now() / 1000),\n seat_type: 'full',\n })\n })\n\n afterEach(() => {\n tokenStore.clearTokens()\n tokenStore.clearAllProjectTokens()\n vi.clearAllMocks()\n\n // Restore original environment\n if (originalEnv === undefined) {\n delete process.env.FIGMA_MOCK_PROJECT_TOKEN\n } else {\n process.env.FIGMA_MOCK_PROJECT_TOKEN = originalEnv\n }\n })\n\n describe('getValidProjectToken', () => {\n describe('with valid cached token', () => {\n it('should return cached token immediately without API calls', async () => {\n // Store a valid project token\n const validToken: ProjectToken = {\n claims: {\n aud: mockTenantId, // Audience claim contains the CMS ID\n email: 'test@example.com',\n exp: Math.floor(Date.now() / 1000) + 1800, // 30 minutes from now\n iat: Math.floor(Date.now() / 1000),\n seat_type: 'full',\n },\n expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutes\n tenantId: mockTenantId,\n token: mockProjectTokenJWT,\n }\n tokenStore.setProjectToken(mockTenantId, validToken)\n\n const result = await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(mockProjectTokenJWT)\n // Should not call any external APIs\n expect(oauthFlow.getValidAccessToken).not.toHaveBeenCalled()\n expect(figmaApi.getProjectToken).not.toHaveBeenCalled()\n expect(validateProjectToken).not.toHaveBeenCalled()\n })\n\n it('should use hasValidProjectToken for expiry check', async () => {\n // Mock hasValidProjectToken to return true\n const hasValidSpy = vi.spyOn(tokenStore, 'hasValidProjectToken').mockReturnValue(true)\n const getTokenSpy = vi.spyOn(tokenStore, 'getProjectToken').mockReturnValue({\n claims: { exp: Math.floor(Date.now() / 1000) + 1800 },\n expiresAt: Date.now() + 30 * 60 * 1000,\n tenantId: mockTenantId,\n token: mockProjectTokenJWT,\n })\n\n await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(hasValidSpy).toHaveBeenCalledWith(mockTenantId)\n expect(getTokenSpy).toHaveBeenCalledWith(mockTenantId)\n })\n })\n\n describe('with expired or missing token', () => {\n it('should fetch new token when no cached token exists', async () => {\n const result = await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(mockProjectTokenJWT)\n expect(oauthFlow.getValidAccessToken).toHaveBeenCalledWith(tokenStore)\n expect(figmaApi.getProjectToken).toHaveBeenCalledWith(mockAccessToken, mockTenantId)\n expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'staging')\n })\n\n it('should fetch new token when cached token is expired', async () => {\n // Mock hasValidProjectToken to return false (expired)\n vi.spyOn(tokenStore, 'hasValidProjectToken').mockReturnValue(false)\n\n const result = await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(mockProjectTokenJWT)\n expect(figmaApi.getProjectToken).toHaveBeenCalled()\n })\n\n it('should apply 5-minute expiry buffer through hasValidProjectToken', async () => {\n // Store a token expiring in 4 minutes (within 5-minute buffer)\n const expiringToken: ProjectToken = {\n claims: { exp: Math.floor(Date.now() / 1000) + 240 }, // 4 minutes\n expiresAt: Date.now() + 4 * 60 * 1000,\n tenantId: mockTenantId,\n token: 'expiring-token',\n }\n tokenStore.setProjectToken(mockTenantId, expiringToken)\n\n // hasValidProjectToken should return false due to buffer\n expect(tokenStore.hasValidProjectToken(mockTenantId)).toBe(false)\n\n // Should refresh the token\n await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(figmaApi.getProjectToken).toHaveBeenCalled()\n })\n })\n\n describe('JWT validation integration', () => {\n it('should validate JWT signature after fetching', async () => {\n await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'staging')\n })\n\n it('should use correct environment for validation', async () => {\n vi.mocked(constants.getEnvironment).mockReturnValue('production')\n\n await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'production')\n })\n\n it('should throw ProjectTokenError on validation failure', async () => {\n const validationError = new JWTValidationError('Invalid signature', 'SIGNATURE_INVALID')\n vi.mocked(validateProjectToken).mockRejectedValue(validationError)\n\n try {\n await getValidProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown ProjectTokenError')\n } catch (error) {\n expect(error).toBeInstanceOf(ProjectTokenError)\n if (error instanceof ProjectTokenError) {\n expect(error.message).toContain('Project token validation failed')\n expect(error.message).toContain('Invalid signature')\n expect(error.cause).toBe(validationError)\n }\n }\n })\n\n it('should rethrow non-JWTValidationError errors', async () => {\n const genericError = new Error('Network error')\n vi.mocked(validateProjectToken).mockRejectedValue(genericError)\n\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(genericError)\n })\n\n it('should throw error if JWT missing exp claim', async () => {\n // Mock validation returning claims without exp\n vi.mocked(validateProjectToken).mockResolvedValue({\n aud: mockTenantId,\n // No exp claim - intentionally omitted to test validation\n })\n\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(\n ProjectTokenError,\n )\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(\n 'missing expiration (exp) claim',\n )\n })\n })\n\n describe('claims storage', () => {\n it('should store validated claims with token', async () => {\n const mockClaims = {\n aud: mockTenantId,\n email: 'user@example.com',\n exp: Math.floor(Date.now() / 1000) + 1800,\n iat: Math.floor(Date.now() / 1000),\n seat_type: 'full',\n }\n vi.mocked(validateProjectToken).mockResolvedValue(mockClaims)\n\n await getValidProjectToken(tokenStore, mockTenantId)\n\n const storedToken = tokenStore.getProjectToken(mockTenantId)\n expect(storedToken?.claims).toEqual(mockClaims)\n })\n\n it('should convert exp from seconds to milliseconds for expiresAt', async () => {\n const expInSeconds = Math.floor(Date.now() / 1000) + 1800 // 30 min from now\n vi.mocked(validateProjectToken).mockResolvedValue({\n aud: mockTenantId,\n exp: expInSeconds,\n })\n\n await getValidProjectToken(tokenStore, mockTenantId)\n\n const storedToken = tokenStore.getProjectToken(mockTenantId)\n expect(storedToken?.expiresAt).toBe(expInSeconds * 1000)\n })\n\n it('should store token with correct tenantId', async () => {\n await getValidProjectToken(tokenStore, mockTenantId)\n\n const storedToken = tokenStore.getProjectToken(mockTenantId)\n expect(storedToken?.tenantId).toBe(mockTenantId)\n expect(storedToken?.token).toBe(mockProjectTokenJWT)\n })\n })\n\n describe('error handling', () => {\n it('should throw ProjectTokenError when no user access token available', async () => {\n vi.mocked(oauthFlow.getValidAccessToken).mockRejectedValue(\n new Error('User not authenticated'),\n )\n\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(\n ProjectTokenError,\n )\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(\n 'Failed to get valid access token',\n )\n })\n\n it('should return null when getValidAccessToken returns null', async () => {\n vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(null)\n\n const result = await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBeNull()\n // Should not call API if no access token\n expect(figmaApi.getProjectToken).not.toHaveBeenCalled()\n })\n\n it('should handle API 401 errors', async () => {\n const apiError = new figmaApi.FigmaApiError('Unauthorized', 401)\n vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError)\n\n try {\n await getValidProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown error')\n } catch (error) {\n expect(error).toBe(apiError)\n expect(error).toBeInstanceOf(figmaApi.FigmaApiError)\n }\n })\n\n it('should handle API 404 errors (tenant not found)', async () => {\n const apiError = new figmaApi.FigmaApiError('Tenant not found', 404)\n vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError)\n\n try {\n await getValidProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown error')\n } catch (error) {\n expect(error).toBe(apiError)\n expect(error).toBeInstanceOf(figmaApi.FigmaApiError)\n }\n })\n\n it('should handle network errors', async () => {\n const networkError = new Error('Network timeout')\n vi.mocked(figmaApi.getProjectToken).mockRejectedValue(networkError)\n\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(networkError)\n })\n\n it('should wrap getValidAccessToken errors in ProjectTokenError', async () => {\n const authError = new Error('OAuth refresh failed')\n vi.mocked(oauthFlow.getValidAccessToken).mockRejectedValue(authError)\n\n try {\n await getValidProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown ProjectTokenError')\n } catch (error) {\n expect(error).toBeInstanceOf(ProjectTokenError)\n if (error instanceof ProjectTokenError) {\n expect(error.cause).toBe(authError)\n expect(error.message).toContain('Failed to get valid access token')\n }\n }\n })\n })\n })\n\n describe('refreshProjectToken', () => {\n it('should clear existing token before fetching new one', async () => {\n // Store an existing token\n const existingToken: ProjectToken = {\n claims: { exp: Math.floor(Date.now() / 1000) + 1800 },\n expiresAt: Date.now() + 30 * 60 * 1000,\n tenantId: mockTenantId,\n token: 'old-token',\n }\n tokenStore.setProjectToken(mockTenantId, existingToken)\n\n const clearSpy = vi.spyOn(tokenStore, 'clearProjectToken')\n\n await refreshProjectToken(tokenStore, mockTenantId)\n\n expect(clearSpy).toHaveBeenCalledWith(mockTenantId)\n })\n\n it('should call getValidProjectToken after clearing', async () => {\n const result = await refreshProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(mockProjectTokenJWT)\n // Verify API was called (meaning cache was bypassed)\n expect(figmaApi.getProjectToken).toHaveBeenCalled()\n })\n\n it('should force refresh even if token is still valid', async () => {\n // Store a valid token\n const validToken: ProjectToken = {\n claims: { exp: Math.floor(Date.now() / 1000) + 3600 }, // 1 hour\n expiresAt: Date.now() + 60 * 60 * 1000,\n tenantId: mockTenantId,\n token: 'valid-token',\n }\n tokenStore.setProjectToken(mockTenantId, validToken)\n\n // Verify it's considered valid\n expect(tokenStore.hasValidProjectToken(mockTenantId)).toBe(true)\n\n await refreshProjectToken(tokenStore, mockTenantId)\n\n // Should have fetched new token despite valid cache\n expect(figmaApi.getProjectToken).toHaveBeenCalled()\n })\n\n it('should return new token after refresh', async () => {\n const newTokenJWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.new.signature'\n vi.mocked(figmaApi.getProjectToken).mockResolvedValue({\n expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutes (milliseconds)\n tenantId: mockTenantId,\n token: newTokenJWT,\n })\n\n const result = await refreshProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(newTokenJWT)\n expect(tokenStore.getProjectToken(mockTenantId)?.token).toBe(newTokenJWT)\n })\n\n it('should propagate errors from getValidProjectToken', async () => {\n const apiError = new figmaApi.FigmaApiError('API Error', 500)\n vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError)\n\n try {\n await refreshProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown error')\n } catch (error) {\n expect(error).toBe(apiError)\n expect(error).toBeInstanceOf(figmaApi.FigmaApiError)\n }\n })\n\n it('should return null when refresh fails due to no access token', async () => {\n vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(null)\n\n const result = await refreshProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBeNull()\n })\n })\n})\n"],"names":["afterEach","beforeEach","describe","expect","it","vi","figmaApi","constants","JWTValidationError","validateProjectToken","oauthFlow","getValidProjectToken","ProjectTokenError","refreshProjectToken","TokenStore","mock","importOriginal","actual","fn","tokenStore","mockTenantId","mockAccessToken","mockProjectTokenJWT","originalEnv","configName","Date","now","process","env","FIGMA_MOCK_PROJECT_TOKEN","mocked","getEnvironment","mockReturnValue","getValidAccessToken","mockResolvedValue","getProjectToken","expiresAt","tenantId","token","aud","email","exp","Math","floor","iat","seat_type","clearTokens","clearAllProjectTokens","clearAllMocks","undefined","validToken","claims","setProjectToken","result","toBe","not","toHaveBeenCalled","hasValidSpy","spyOn","getTokenSpy","toHaveBeenCalledWith","expiringToken","hasValidProjectToken","validationError","mockRejectedValue","fail","error","toBeInstanceOf","message","toContain","cause","genericError","Error","rejects","toThrow","mockClaims","storedToken","toEqual","expInSeconds","toBeNull","apiError","FigmaApiError","networkError","authError","existingToken","clearSpy","newTokenJWT"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FigmaTokens } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when token refresh fails
|
|
4
|
+
*/
|
|
5
|
+
export declare class TokenRefreshError extends Error {
|
|
6
|
+
statusCode?: number | undefined;
|
|
7
|
+
constructor(message: string, statusCode?: number | undefined);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Refresh an expired OAuth2 access token using a refresh token
|
|
11
|
+
*
|
|
12
|
+
* Makes a POST request to Figma's token endpoint with the refresh token
|
|
13
|
+
* to obtain a new access token. Uses public client pattern (no client secret).
|
|
14
|
+
*
|
|
15
|
+
* @param refreshToken - The refresh token to use
|
|
16
|
+
* @param clientId - OAuth2 client ID (defaults to config)
|
|
17
|
+
* @returns Promise resolving to new tokens
|
|
18
|
+
* @throws TokenRefreshError if refresh fails
|
|
19
|
+
*/
|
|
20
|
+
export declare function refreshAccessToken(refreshToken: string, clientId?: string): Promise<FigmaTokens>;
|
|
21
|
+
/**
|
|
22
|
+
* Exchange an authorization code for OAuth2 tokens
|
|
23
|
+
*
|
|
24
|
+
* This is used during the initial OAuth flow after the user authorizes the app.
|
|
25
|
+
* Uses public client pattern with PKCE (code verifier is required).
|
|
26
|
+
*
|
|
27
|
+
* @param code - Authorization code from OAuth callback
|
|
28
|
+
* @param codeVerifier - PKCE code verifier (required for public clients)
|
|
29
|
+
* @param redirectUri - Redirect URI used in the authorization request
|
|
30
|
+
* @param clientId - OAuth2 client ID (defaults to config)
|
|
31
|
+
* @returns Promise resolving to tokens
|
|
32
|
+
* @throws TokenRefreshError if exchange fails
|
|
33
|
+
*/
|
|
34
|
+
export declare function exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri?: string, clientId?: string): Promise<FigmaTokens>;
|
|
35
|
+
//# sourceMappingURL=refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/auth/refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,YAAY,CAAA;AAMjE;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,UAAU,CAAC,EAAE,MAAM;gBAD1B,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA;CAK7B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,QAAQ,GAAE,MAA8B,GACvC,OAAO,CAAC,WAAW,CAAC,CAmDtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,WAAW,GAAE,MAAiC,EAC9C,QAAQ,GAAE,MAA8B,GACvC,OAAO,CAAC,WAAW,CAAC,CA8FtB"}
|