@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,149 @@
|
|
|
1
|
+
/* eslint-disable perfectionist/sort-objects */ import { OAUTH_CONFIG } from '../config/oauth.js';
|
|
2
|
+
import { isDebug } from '../utils/is-debug.js';
|
|
3
|
+
import * as log from '../utils/log.js';
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when token refresh fails
|
|
6
|
+
*/ export class TokenRefreshError extends Error {
|
|
7
|
+
statusCode;
|
|
8
|
+
constructor(message, statusCode){
|
|
9
|
+
super(message), this.statusCode = statusCode;
|
|
10
|
+
this.name = 'TokenRefreshError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Refresh an expired OAuth2 access token using a refresh token
|
|
15
|
+
*
|
|
16
|
+
* Makes a POST request to Figma's token endpoint with the refresh token
|
|
17
|
+
* to obtain a new access token. Uses public client pattern (no client secret).
|
|
18
|
+
*
|
|
19
|
+
* @param refreshToken - The refresh token to use
|
|
20
|
+
* @param clientId - OAuth2 client ID (defaults to config)
|
|
21
|
+
* @returns Promise resolving to new tokens
|
|
22
|
+
* @throws TokenRefreshError if refresh fails
|
|
23
|
+
*/ export async function refreshAccessToken(refreshToken, clientId = OAUTH_CONFIG.clientId) {
|
|
24
|
+
// Prepare form data for token refresh request (public client)
|
|
25
|
+
const params = new URLSearchParams({
|
|
26
|
+
client_id: clientId,
|
|
27
|
+
grant_type: 'refresh_token',
|
|
28
|
+
refresh_token: refreshToken
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(OAUTH_CONFIG.refreshUrl, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
35
|
+
},
|
|
36
|
+
body: params.toString()
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const errorText = await response.text();
|
|
40
|
+
throw new TokenRefreshError(`Token refresh failed: ${response.statusText}. ${errorText}`, response.status);
|
|
41
|
+
}
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
// Validate response contains required fields
|
|
44
|
+
if (!data.access_token || !data.refresh_token || !data.expires_in) {
|
|
45
|
+
throw new TokenRefreshError('Invalid token response: missing required fields');
|
|
46
|
+
}
|
|
47
|
+
// Convert response to our internal token format
|
|
48
|
+
const expiresAt = Date.now() + data.expires_in * 1000;
|
|
49
|
+
return {
|
|
50
|
+
accessToken: data.access_token,
|
|
51
|
+
refreshToken: data.refresh_token,
|
|
52
|
+
expiresAt,
|
|
53
|
+
userId: data.user_id_string
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof TokenRefreshError) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
// Handle network errors and other exceptions
|
|
60
|
+
throw new TokenRefreshError(`Failed to refresh token: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Exchange an authorization code for OAuth2 tokens
|
|
65
|
+
*
|
|
66
|
+
* This is used during the initial OAuth flow after the user authorizes the app.
|
|
67
|
+
* Uses public client pattern with PKCE (code verifier is required).
|
|
68
|
+
*
|
|
69
|
+
* @param code - Authorization code from OAuth callback
|
|
70
|
+
* @param codeVerifier - PKCE code verifier (required for public clients)
|
|
71
|
+
* @param redirectUri - Redirect URI used in the authorization request
|
|
72
|
+
* @param clientId - OAuth2 client ID (defaults to config)
|
|
73
|
+
* @returns Promise resolving to tokens
|
|
74
|
+
* @throws TokenRefreshError if exchange fails
|
|
75
|
+
*/ export async function exchangeCodeForTokens(code, codeVerifier, redirectUri = OAUTH_CONFIG.redirectUri, clientId = OAUTH_CONFIG.clientId) {
|
|
76
|
+
// Prepare form data for token exchange (public client with PKCE)
|
|
77
|
+
const params = new URLSearchParams({
|
|
78
|
+
client_id: clientId,
|
|
79
|
+
code,
|
|
80
|
+
code_verifier: codeVerifier,
|
|
81
|
+
grant_type: 'authorization_code',
|
|
82
|
+
redirect_uri: redirectUri
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
// Public client - no client secret or Basic Auth header needed
|
|
86
|
+
// Security is provided by PKCE (code_verifier in the request body)
|
|
87
|
+
const headers = {
|
|
88
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
89
|
+
};
|
|
90
|
+
// Debug logging
|
|
91
|
+
if (isDebug()) {
|
|
92
|
+
log.debug(`Token exchange request: ${JSON.stringify({
|
|
93
|
+
url: OAUTH_CONFIG.tokenUrl,
|
|
94
|
+
headers,
|
|
95
|
+
body: params.toString()
|
|
96
|
+
})}`);
|
|
97
|
+
}
|
|
98
|
+
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers,
|
|
101
|
+
body: params.toString()
|
|
102
|
+
});
|
|
103
|
+
if (isDebug()) {
|
|
104
|
+
log.debug(`Token exchange response: ${JSON.stringify({
|
|
105
|
+
status: response.status,
|
|
106
|
+
statusText: response.statusText,
|
|
107
|
+
headers: Object.fromEntries(response.headers.entries())
|
|
108
|
+
})}`);
|
|
109
|
+
}
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const errorText = await response.text();
|
|
112
|
+
if (isDebug()) {
|
|
113
|
+
log.debug(`Error response body: ${errorText}`);
|
|
114
|
+
}
|
|
115
|
+
throw new TokenRefreshError(`Token exchange failed: ${response.statusText}. ${errorText}`, response.status);
|
|
116
|
+
}
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
if (isDebug()) {
|
|
119
|
+
log.debug(`Token response: ${JSON.stringify({
|
|
120
|
+
access_token: data.access_token,
|
|
121
|
+
token_type: data.token_type,
|
|
122
|
+
expires_in: data.expires_in,
|
|
123
|
+
scope: data.scope,
|
|
124
|
+
user_id_string: data.user_id_string,
|
|
125
|
+
refresh_token: data.refresh_token?.substring(0, 10) + '...'
|
|
126
|
+
})}`);
|
|
127
|
+
}
|
|
128
|
+
// Validate response contains required fields
|
|
129
|
+
if (!data.access_token || !data.refresh_token || !data.expires_in) {
|
|
130
|
+
throw new TokenRefreshError('Invalid token response: missing required fields');
|
|
131
|
+
}
|
|
132
|
+
// Convert response to our internal token format
|
|
133
|
+
const expiresAt = Date.now() + data.expires_in * 1000;
|
|
134
|
+
return {
|
|
135
|
+
accessToken: data.access_token,
|
|
136
|
+
refreshToken: data.refresh_token,
|
|
137
|
+
expiresAt,
|
|
138
|
+
userId: data.user_id_string
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof TokenRefreshError) {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
// Handle network errors and other exceptions
|
|
145
|
+
throw new TokenRefreshError(`Failed to exchange code for tokens: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/refresh.ts"],"sourcesContent":["/* eslint-disable perfectionist/sort-objects */\nimport type { FigmaTokenResponse, FigmaTokens } from './types.js'\n\nimport { OAUTH_CONFIG } from '../config/oauth.js'\nimport { isDebug } from '../utils/is-debug.js'\nimport * as log from '../utils/log.js'\n\n/**\n * Error thrown when token refresh fails\n */\nexport class TokenRefreshError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n ) {\n super(message)\n this.name = 'TokenRefreshError'\n }\n}\n\n/**\n * Refresh an expired OAuth2 access token using a refresh token\n *\n * Makes a POST request to Figma's token endpoint with the refresh token\n * to obtain a new access token. Uses public client pattern (no client secret).\n *\n * @param refreshToken - The refresh token to use\n * @param clientId - OAuth2 client ID (defaults to config)\n * @returns Promise resolving to new tokens\n * @throws TokenRefreshError if refresh fails\n */\nexport async function refreshAccessToken(\n refreshToken: string,\n clientId: string = OAUTH_CONFIG.clientId,\n): Promise<FigmaTokens> {\n // Prepare form data for token refresh request (public client)\n const params = new URLSearchParams({\n client_id: clientId,\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n })\n\n try {\n const response = await fetch(OAUTH_CONFIG.refreshUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: params.toString(),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new TokenRefreshError(\n `Token refresh failed: ${response.statusText}. ${errorText}`,\n response.status,\n )\n }\n\n const data = (await response.json()) as FigmaTokenResponse\n\n // Validate response contains required fields\n if (!data.access_token || !data.refresh_token || !data.expires_in) {\n throw new TokenRefreshError('Invalid token response: missing required fields')\n }\n\n // Convert response to our internal token format\n const expiresAt = Date.now() + data.expires_in * 1000\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt,\n userId: data.user_id_string,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n\n // Handle network errors and other exceptions\n throw new TokenRefreshError(\n `Failed to refresh token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n\n/**\n * Exchange an authorization code for OAuth2 tokens\n *\n * This is used during the initial OAuth flow after the user authorizes the app.\n * Uses public client pattern with PKCE (code verifier is required).\n *\n * @param code - Authorization code from OAuth callback\n * @param codeVerifier - PKCE code verifier (required for public clients)\n * @param redirectUri - Redirect URI used in the authorization request\n * @param clientId - OAuth2 client ID (defaults to config)\n * @returns Promise resolving to tokens\n * @throws TokenRefreshError if exchange fails\n */\nexport async function exchangeCodeForTokens(\n code: string,\n codeVerifier: string,\n redirectUri: string = OAUTH_CONFIG.redirectUri,\n clientId: string = OAUTH_CONFIG.clientId,\n): Promise<FigmaTokens> {\n // Prepare form data for token exchange (public client with PKCE)\n const params = new URLSearchParams({\n client_id: clientId,\n code,\n code_verifier: codeVerifier,\n grant_type: 'authorization_code',\n redirect_uri: redirectUri,\n })\n\n try {\n // Public client - no client secret or Basic Auth header needed\n // Security is provided by PKCE (code_verifier in the request body)\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n }\n\n // Debug logging\n if (isDebug()) {\n log.debug(\n `Token exchange request: ${JSON.stringify({\n url: OAUTH_CONFIG.tokenUrl,\n headers,\n body: params.toString(),\n })}`,\n )\n }\n\n const response = await fetch(OAUTH_CONFIG.tokenUrl, {\n method: 'POST',\n headers,\n body: params.toString(),\n })\n\n if (isDebug()) {\n log.debug(\n `Token exchange response: ${JSON.stringify({\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n })}`,\n )\n }\n\n if (!response.ok) {\n const errorText = await response.text()\n if (isDebug()) {\n log.debug(`Error response body: ${errorText}`)\n }\n throw new TokenRefreshError(\n `Token exchange failed: ${response.statusText}. ${errorText}`,\n response.status,\n )\n }\n\n const data = (await response.json()) as FigmaTokenResponse\n\n if (isDebug()) {\n log.debug(\n `Token response: ${JSON.stringify({\n access_token: data.access_token,\n token_type: data.token_type,\n expires_in: data.expires_in,\n scope: data.scope,\n user_id_string: data.user_id_string,\n refresh_token: data.refresh_token?.substring(0, 10) + '...',\n })}`,\n )\n }\n\n // Validate response contains required fields\n if (!data.access_token || !data.refresh_token || !data.expires_in) {\n throw new TokenRefreshError('Invalid token response: missing required fields')\n }\n\n // Convert response to our internal token format\n const expiresAt = Date.now() + data.expires_in * 1000\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt,\n userId: data.user_id_string,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n\n // Handle network errors and other exceptions\n throw new TokenRefreshError(\n `Failed to exchange code for tokens: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n"],"names":["OAUTH_CONFIG","isDebug","log","TokenRefreshError","Error","message","statusCode","name","refreshAccessToken","refreshToken","clientId","params","URLSearchParams","client_id","grant_type","refresh_token","response","fetch","refreshUrl","method","headers","body","toString","ok","errorText","text","statusText","status","data","json","access_token","expires_in","expiresAt","Date","now","accessToken","userId","user_id_string","error","exchangeCodeForTokens","code","codeVerifier","redirectUri","code_verifier","redirect_uri","debug","JSON","stringify","url","tokenUrl","Object","fromEntries","entries","token_type","scope","substring"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.spec.d.ts","sourceRoot":"","sources":["../../src/auth/refresh.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mockFigmaTokenResponse } from './__tests__/fixtures.js';
|
|
3
|
+
import { mockFetchFailure, mockFetchSuccess, restoreFetch } from './__tests__/mocks.js';
|
|
4
|
+
import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './refresh.js';
|
|
5
|
+
describe('refreshAccessToken', ()=>{
|
|
6
|
+
const testRefreshToken = 'figt_test_refresh_token_987654321';
|
|
7
|
+
const testClientId = 'test-client-id';
|
|
8
|
+
const originalDebug = process.env.DEBUG;
|
|
9
|
+
beforeEach(()=>{
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
delete process.env.DEBUG;
|
|
12
|
+
});
|
|
13
|
+
afterEach(()=>{
|
|
14
|
+
restoreFetch();
|
|
15
|
+
if (originalDebug !== undefined) {
|
|
16
|
+
process.env.DEBUG = originalDebug;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
it('should successfully refresh access token with correct HTTP request', async ()=>{
|
|
20
|
+
const mockResponse = mockFigmaTokenResponse();
|
|
21
|
+
const mockFetch = mockFetchSuccess(mockResponse);
|
|
22
|
+
const result = await refreshAccessToken(testRefreshToken, testClientId);
|
|
23
|
+
// Verify response transformation
|
|
24
|
+
expect(result).toMatchObject({
|
|
25
|
+
accessToken: mockResponse.access_token,
|
|
26
|
+
refreshToken: mockResponse.refresh_token,
|
|
27
|
+
userId: mockResponse.user_id_string
|
|
28
|
+
});
|
|
29
|
+
expect(result.expiresAt).toBeGreaterThan(Date.now());
|
|
30
|
+
// Verify HTTP request details
|
|
31
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.staging.figma.com/v1/oauth/refresh', expect.objectContaining({
|
|
32
|
+
method: 'POST'
|
|
33
|
+
}));
|
|
34
|
+
// Verify headers (public client - no Authorization header)
|
|
35
|
+
const callArgs = mockFetch.mock.calls[0];
|
|
36
|
+
const headers = callArgs[1].headers;
|
|
37
|
+
expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded');
|
|
38
|
+
expect(headers['Authorization']).toBeUndefined(); // Public client - no Basic Auth
|
|
39
|
+
// Verify request body includes client_id
|
|
40
|
+
const body = callArgs[1].body;
|
|
41
|
+
expect(body).toContain('grant_type=refresh_token');
|
|
42
|
+
expect(body).toContain(`refresh_token=${testRefreshToken}`);
|
|
43
|
+
expect(body).toContain(`client_id=${testClientId}`);
|
|
44
|
+
});
|
|
45
|
+
it('should throw TokenRefreshError on HTTP error', async ()=>{
|
|
46
|
+
mockFetchFailure(401, 'Invalid refresh token');
|
|
47
|
+
await expect(refreshAccessToken(testRefreshToken, testClientId)).rejects.toThrow(TokenRefreshError);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('exchangeCodeForTokens', ()=>{
|
|
51
|
+
const testAuthCode = 'auth_code_123';
|
|
52
|
+
const testCodeVerifier = 'code_verifier_123456789012345678901234567890';
|
|
53
|
+
const testRedirectUri = 'http://localhost:3000/callback';
|
|
54
|
+
const testClientId = 'test-client-id';
|
|
55
|
+
const originalDebug = process.env.DEBUG;
|
|
56
|
+
beforeEach(()=>{
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
delete process.env.DEBUG;
|
|
59
|
+
});
|
|
60
|
+
afterEach(()=>{
|
|
61
|
+
restoreFetch();
|
|
62
|
+
if (originalDebug !== undefined) {
|
|
63
|
+
process.env.DEBUG = originalDebug;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
it('should successfully exchange authorization code for tokens with PKCE', async ()=>{
|
|
67
|
+
const mockResponse = mockFigmaTokenResponse();
|
|
68
|
+
const mockFetch = mockFetchSuccess(mockResponse);
|
|
69
|
+
const result = await exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId);
|
|
70
|
+
// Verify response transformation
|
|
71
|
+
expect(result).toMatchObject({
|
|
72
|
+
accessToken: mockResponse.access_token,
|
|
73
|
+
refreshToken: mockResponse.refresh_token,
|
|
74
|
+
userId: mockResponse.user_id_string
|
|
75
|
+
});
|
|
76
|
+
expect(result.expiresAt).toBeGreaterThan(Date.now());
|
|
77
|
+
// Verify HTTP request
|
|
78
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.staging.figma.com/v1/oauth/token', expect.objectContaining({
|
|
79
|
+
method: 'POST'
|
|
80
|
+
}));
|
|
81
|
+
// Verify headers (public client - no Authorization header)
|
|
82
|
+
const callArgs = mockFetch.mock.calls[0];
|
|
83
|
+
const headers = callArgs[1].headers;
|
|
84
|
+
expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded');
|
|
85
|
+
expect(headers['Authorization']).toBeUndefined(); // Public client - no Basic Auth
|
|
86
|
+
});
|
|
87
|
+
it('should send correct request body with PKCE', async ()=>{
|
|
88
|
+
const mockResponse = mockFigmaTokenResponse();
|
|
89
|
+
const mockFetch = mockFetchSuccess(mockResponse);
|
|
90
|
+
await exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId);
|
|
91
|
+
const callArgs = mockFetch.mock.calls[0];
|
|
92
|
+
const body = callArgs[1].body;
|
|
93
|
+
expect(body).toContain('grant_type=authorization_code');
|
|
94
|
+
expect(body).toContain(`code=${testAuthCode}`);
|
|
95
|
+
expect(body).toContain(`redirect_uri=${encodeURIComponent(testRedirectUri)}`);
|
|
96
|
+
expect(body).toContain(`client_id=${testClientId}`);
|
|
97
|
+
expect(body).toContain(`code_verifier=${testCodeVerifier}`);
|
|
98
|
+
});
|
|
99
|
+
it('should throw TokenRefreshError on invalid authorization code', async ()=>{
|
|
100
|
+
mockFetchFailure(400, 'Invalid authorization code');
|
|
101
|
+
await expect(exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId)).rejects.toThrow(TokenRefreshError);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=refresh.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/refresh.spec.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport { mockFigmaTokenResponse } from './__tests__/fixtures.js'\nimport { mockFetchFailure, mockFetchSuccess, restoreFetch } from './__tests__/mocks.js'\nimport { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './refresh.js'\n\ndescribe('refreshAccessToken', () => {\n const testRefreshToken = 'figt_test_refresh_token_987654321'\n const testClientId = 'test-client-id'\n const originalDebug = process.env.DEBUG\n\n beforeEach(() => {\n vi.clearAllMocks()\n delete process.env.DEBUG\n })\n\n afterEach(() => {\n restoreFetch()\n if (originalDebug !== undefined) {\n process.env.DEBUG = originalDebug\n }\n })\n\n it('should successfully refresh access token with correct HTTP request', async () => {\n const mockResponse = mockFigmaTokenResponse()\n const mockFetch = mockFetchSuccess(mockResponse)\n\n const result = await refreshAccessToken(testRefreshToken, testClientId)\n\n // Verify response transformation\n expect(result).toMatchObject({\n accessToken: mockResponse.access_token,\n refreshToken: mockResponse.refresh_token,\n userId: mockResponse.user_id_string,\n })\n expect(result.expiresAt).toBeGreaterThan(Date.now())\n\n // Verify HTTP request details\n expect(mockFetch).toHaveBeenCalledWith(\n 'https://api.staging.figma.com/v1/oauth/refresh',\n expect.objectContaining({\n method: 'POST',\n }),\n )\n\n // Verify headers (public client - no Authorization header)\n const callArgs = mockFetch.mock.calls[0]\n const headers = callArgs[1].headers\n expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded')\n expect(headers['Authorization']).toBeUndefined() // Public client - no Basic Auth\n\n // Verify request body includes client_id\n const body = callArgs[1].body\n expect(body).toContain('grant_type=refresh_token')\n expect(body).toContain(`refresh_token=${testRefreshToken}`)\n expect(body).toContain(`client_id=${testClientId}`)\n })\n\n it('should throw TokenRefreshError on HTTP error', async () => {\n mockFetchFailure(401, 'Invalid refresh token')\n\n await expect(refreshAccessToken(testRefreshToken, testClientId)).rejects.toThrow(\n TokenRefreshError,\n )\n })\n})\n\ndescribe('exchangeCodeForTokens', () => {\n const testAuthCode = 'auth_code_123'\n const testCodeVerifier = 'code_verifier_123456789012345678901234567890'\n const testRedirectUri = 'http://localhost:3000/callback'\n const testClientId = 'test-client-id'\n const originalDebug = process.env.DEBUG\n\n beforeEach(() => {\n vi.clearAllMocks()\n delete process.env.DEBUG\n })\n\n afterEach(() => {\n restoreFetch()\n if (originalDebug !== undefined) {\n process.env.DEBUG = originalDebug\n }\n })\n\n it('should successfully exchange authorization code for tokens with PKCE', async () => {\n const mockResponse = mockFigmaTokenResponse()\n const mockFetch = mockFetchSuccess(mockResponse)\n\n const result = await exchangeCodeForTokens(\n testAuthCode,\n testCodeVerifier,\n testRedirectUri,\n testClientId,\n )\n\n // Verify response transformation\n expect(result).toMatchObject({\n accessToken: mockResponse.access_token,\n refreshToken: mockResponse.refresh_token,\n userId: mockResponse.user_id_string,\n })\n expect(result.expiresAt).toBeGreaterThan(Date.now())\n\n // Verify HTTP request\n expect(mockFetch).toHaveBeenCalledWith(\n 'https://api.staging.figma.com/v1/oauth/token',\n expect.objectContaining({\n method: 'POST',\n }),\n )\n\n // Verify headers (public client - no Authorization header)\n const callArgs = mockFetch.mock.calls[0]\n const headers = callArgs[1].headers\n expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded')\n expect(headers['Authorization']).toBeUndefined() // Public client - no Basic Auth\n })\n\n it('should send correct request body with PKCE', async () => {\n const mockResponse = mockFigmaTokenResponse()\n const mockFetch = mockFetchSuccess(mockResponse)\n\n await exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId)\n\n const callArgs = mockFetch.mock.calls[0]\n const body = callArgs[1].body\n\n expect(body).toContain('grant_type=authorization_code')\n expect(body).toContain(`code=${testAuthCode}`)\n expect(body).toContain(`redirect_uri=${encodeURIComponent(testRedirectUri)}`)\n expect(body).toContain(`client_id=${testClientId}`)\n expect(body).toContain(`code_verifier=${testCodeVerifier}`)\n })\n\n it('should throw TokenRefreshError on invalid authorization code', async () => {\n mockFetchFailure(400, 'Invalid authorization code')\n\n await expect(\n exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId),\n ).rejects.toThrow(TokenRefreshError)\n })\n})\n"],"names":["afterEach","beforeEach","describe","expect","it","vi","mockFigmaTokenResponse","mockFetchFailure","mockFetchSuccess","restoreFetch","exchangeCodeForTokens","refreshAccessToken","TokenRefreshError","testRefreshToken","testClientId","originalDebug","process","env","DEBUG","clearAllMocks","undefined","mockResponse","mockFetch","result","toMatchObject","accessToken","access_token","refreshToken","refresh_token","userId","user_id_string","expiresAt","toBeGreaterThan","Date","now","toHaveBeenCalledWith","objectContaining","method","callArgs","mock","calls","headers","toBe","toBeUndefined","body","toContain","rejects","toThrow","testAuthCode","testCodeVerifier","testRedirectUri","encodeURIComponent"],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { FigmaTokens, JWTPayload, ProjectToken, TokenStoreConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Secure storage manager for Figma OAuth2 tokens
|
|
4
|
+
*
|
|
5
|
+
* Uses the `conf` library to store tokens in an OS-specific secure location:
|
|
6
|
+
* - macOS: ~/Library/Preferences/payloadcms-figma
|
|
7
|
+
* - Linux: ~/.config/payloadcms-figma or $XDG_CONFIG_HOME/payloadcms-figma
|
|
8
|
+
* - Windows: %APPDATA%/payloadcms-figma/Config
|
|
9
|
+
*
|
|
10
|
+
* Tokens are encrypted at rest with a machine-specific key and file permissions
|
|
11
|
+
* are set to 0600 (owner read/write only)
|
|
12
|
+
*/
|
|
13
|
+
export declare class TokenStore {
|
|
14
|
+
private config;
|
|
15
|
+
constructor(options?: TokenStoreConfig);
|
|
16
|
+
/**
|
|
17
|
+
* Retrieve stored tokens
|
|
18
|
+
* @returns FigmaTokens if stored, null otherwise
|
|
19
|
+
*/
|
|
20
|
+
getTokens(): FigmaTokens | null;
|
|
21
|
+
/**
|
|
22
|
+
* Store OAuth2 tokens securely
|
|
23
|
+
* @param tokens - Figma OAuth2 tokens to store
|
|
24
|
+
*/
|
|
25
|
+
setTokens(tokens: FigmaTokens): void;
|
|
26
|
+
/**
|
|
27
|
+
* Clear all stored tokens (used for logout)
|
|
28
|
+
*/
|
|
29
|
+
clearTokens(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Check if valid (non-expired) tokens exist
|
|
32
|
+
* @returns true if tokens exist and are not expired
|
|
33
|
+
*/
|
|
34
|
+
hasValidTokens(): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Check if the current access token is expired
|
|
37
|
+
* Includes a buffer time to avoid using tokens that are about to expire
|
|
38
|
+
* @returns true if token is expired or will expire within buffer time
|
|
39
|
+
*/
|
|
40
|
+
isExpired(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Get the access token if it exists and is valid
|
|
43
|
+
* @returns Access token string or null if expired/missing
|
|
44
|
+
*/
|
|
45
|
+
getAccessToken(): null | string;
|
|
46
|
+
/**
|
|
47
|
+
* Get the refresh token if it exists
|
|
48
|
+
* @returns Refresh token string or null if missing
|
|
49
|
+
*/
|
|
50
|
+
getRefreshToken(): null | string;
|
|
51
|
+
/**
|
|
52
|
+
* Update just the access token after a refresh
|
|
53
|
+
* Preserves the existing refresh token and other metadata
|
|
54
|
+
* @param accessToken - New access token
|
|
55
|
+
* @param expiresIn - Expiration time in seconds
|
|
56
|
+
*/
|
|
57
|
+
updateAccessToken(accessToken: string, expiresIn: number): void;
|
|
58
|
+
/**
|
|
59
|
+
* Get the path to the config file (useful for debugging)
|
|
60
|
+
* @returns Absolute path to the token storage file
|
|
61
|
+
*/
|
|
62
|
+
getStoragePath(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Retrieve a project token for a specific tenant
|
|
65
|
+
* Automatically removes expired tokens from storage
|
|
66
|
+
* @param tenantId - The tenant/CMS ID
|
|
67
|
+
* @returns ProjectToken if stored and valid, null otherwise
|
|
68
|
+
*/
|
|
69
|
+
getProjectToken(tenantId: string): null | ProjectToken;
|
|
70
|
+
/**
|
|
71
|
+
* Store a project token for a specific tenant
|
|
72
|
+
* @param tenantId - The tenant/CMS ID
|
|
73
|
+
* @param projectToken - The project token to store
|
|
74
|
+
*/
|
|
75
|
+
setProjectToken(tenantId: string, projectToken: ProjectToken): void;
|
|
76
|
+
/**
|
|
77
|
+
* Clear a project token for a specific tenant
|
|
78
|
+
* @param tenantId - The tenant/CMS ID
|
|
79
|
+
*/
|
|
80
|
+
clearProjectToken(tenantId: string): void;
|
|
81
|
+
/**
|
|
82
|
+
* Clear all project tokens
|
|
83
|
+
*/
|
|
84
|
+
clearAllProjectTokens(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Check if a project token is expired
|
|
87
|
+
* Includes a buffer time to avoid using tokens that are about to expire
|
|
88
|
+
* @param projectToken - The project token to check
|
|
89
|
+
* @returns true if token is expired or will expire within buffer time
|
|
90
|
+
*/
|
|
91
|
+
isProjectTokenExpired(projectToken: ProjectToken): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Check if a valid (non-expired) project token exists for a tenant
|
|
94
|
+
* @param tenantId - The tenant/CMS ID
|
|
95
|
+
* @returns true if token exists and is not expired
|
|
96
|
+
*/
|
|
97
|
+
hasValidProjectToken(tenantId: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Get validated JWT claims from a project token
|
|
100
|
+
* @param tenantId - The tenant/CMS ID
|
|
101
|
+
* @returns JWT payload claims or null if token not found or has no claims
|
|
102
|
+
*/
|
|
103
|
+
getProjectTokenClaims(tenantId: string): JWTPayload | null;
|
|
104
|
+
/**
|
|
105
|
+
* Get all tenant IDs that have stored project tokens
|
|
106
|
+
* @returns Array of tenant IDs
|
|
107
|
+
*/
|
|
108
|
+
getAllProjectTokenTenantIds(): string[];
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAKzF;;;;;;;;;;GAUG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAA4E;gBAE9E,OAAO,CAAC,EAAE,gBAAgB;IAWtC;;;OAGG;IACH,SAAS,IAAI,WAAW,GAAG,IAAI;IAK/B;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB;;;OAGG;IACH,cAAc,IAAI,OAAO;IAQzB;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAcpB;;;OAGG;IACH,cAAc,IAAI,IAAI,GAAG,MAAM;IAQ/B;;;OAGG;IACH,eAAe,IAAI,IAAI,GAAG,MAAM;IAKhC;;;;;OAKG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAe/D;;;OAGG;IACH,cAAc,IAAI,MAAM;IAIxB;;;;;OAKG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,YAAY;IAiBtD;;;;OAIG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI;IAMnE;;;OAGG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAMzC;;OAEG;IACH,qBAAqB,IAAI,IAAI;IAI7B;;;;;OAKG;IACH,qBAAqB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO;IAQ1D;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAQ/C;;;;OAIG;IACH,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAK1D;;;OAGG;IACH,2BAA2B,IAAI,MAAM,EAAE;CAIxC"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* eslint-disable perfectionist/sort-classes */ import Conf from 'conf';
|
|
2
|
+
import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
|
|
3
|
+
import { deriveEncryptionKey } from './crypto-utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Secure storage manager for Figma OAuth2 tokens
|
|
6
|
+
*
|
|
7
|
+
* Uses the `conf` library to store tokens in an OS-specific secure location:
|
|
8
|
+
* - macOS: ~/Library/Preferences/payloadcms-figma
|
|
9
|
+
* - Linux: ~/.config/payloadcms-figma or $XDG_CONFIG_HOME/payloadcms-figma
|
|
10
|
+
* - Windows: %APPDATA%/payloadcms-figma/Config
|
|
11
|
+
*
|
|
12
|
+
* Tokens are encrypted at rest with a machine-specific key and file permissions
|
|
13
|
+
* are set to 0600 (owner read/write only)
|
|
14
|
+
*/ export class TokenStore {
|
|
15
|
+
config;
|
|
16
|
+
constructor(options){
|
|
17
|
+
this.config = new Conf({
|
|
18
|
+
configName: options?.configName || 'payloadcms-figma',
|
|
19
|
+
projectName: 'payloadcms-figma',
|
|
20
|
+
// Enable encryption with machine-specific key for better security
|
|
21
|
+
encryptionKey: options?.encryptionKey || deriveEncryptionKey(),
|
|
22
|
+
// Clear schema to avoid validation issues
|
|
23
|
+
clearInvalidConfig: true
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Retrieve stored tokens
|
|
28
|
+
* @returns FigmaTokens if stored, null otherwise
|
|
29
|
+
*/ getTokens() {
|
|
30
|
+
const tokens = this.config.get('tokens');
|
|
31
|
+
return tokens || null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Store OAuth2 tokens securely
|
|
35
|
+
* @param tokens - Figma OAuth2 tokens to store
|
|
36
|
+
*/ setTokens(tokens) {
|
|
37
|
+
this.config.set('tokens', tokens);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clear all stored tokens (used for logout)
|
|
41
|
+
*/ clearTokens() {
|
|
42
|
+
this.config.delete('tokens');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if valid (non-expired) tokens exist
|
|
46
|
+
* @returns true if tokens exist and are not expired
|
|
47
|
+
*/ hasValidTokens() {
|
|
48
|
+
const tokens = this.getTokens();
|
|
49
|
+
if (!tokens) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return !this.isExpired();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if the current access token is expired
|
|
56
|
+
* Includes a buffer time to avoid using tokens that are about to expire
|
|
57
|
+
* @returns true if token is expired or will expire within buffer time
|
|
58
|
+
*/ isExpired() {
|
|
59
|
+
// Access config directly to avoid circular dependency with getTokens()
|
|
60
|
+
const tokens = this.config.get('tokens');
|
|
61
|
+
if (!tokens) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
|
|
66
|
+
const expiryWithBuffer = tokens.expiresAt - bufferMs;
|
|
67
|
+
return now >= expiryWithBuffer;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the access token if it exists and is valid
|
|
71
|
+
* @returns Access token string or null if expired/missing
|
|
72
|
+
*/ getAccessToken() {
|
|
73
|
+
if (!this.hasValidTokens()) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const tokens = this.getTokens();
|
|
77
|
+
return tokens?.accessToken || null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get the refresh token if it exists
|
|
81
|
+
* @returns Refresh token string or null if missing
|
|
82
|
+
*/ getRefreshToken() {
|
|
83
|
+
const tokens = this.getTokens();
|
|
84
|
+
return tokens?.refreshToken || null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Update just the access token after a refresh
|
|
88
|
+
* Preserves the existing refresh token and other metadata
|
|
89
|
+
* @param accessToken - New access token
|
|
90
|
+
* @param expiresIn - Expiration time in seconds
|
|
91
|
+
*/ updateAccessToken(accessToken, expiresIn) {
|
|
92
|
+
const existingTokens = this.getTokens();
|
|
93
|
+
if (!existingTokens) {
|
|
94
|
+
throw new Error('Cannot update access token: no existing tokens found');
|
|
95
|
+
}
|
|
96
|
+
const expiresAt = Date.now() + expiresIn * 1000;
|
|
97
|
+
this.setTokens({
|
|
98
|
+
...existingTokens,
|
|
99
|
+
accessToken,
|
|
100
|
+
expiresAt
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get the path to the config file (useful for debugging)
|
|
105
|
+
* @returns Absolute path to the token storage file
|
|
106
|
+
*/ getStoragePath() {
|
|
107
|
+
return this.config.path;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Retrieve a project token for a specific tenant
|
|
111
|
+
* Automatically removes expired tokens from storage
|
|
112
|
+
* @param tenantId - The tenant/CMS ID
|
|
113
|
+
* @returns ProjectToken if stored and valid, null otherwise
|
|
114
|
+
*/ getProjectToken(tenantId) {
|
|
115
|
+
const projectTokens = this.config.get('projectTokens') || {};
|
|
116
|
+
const projectToken = projectTokens[tenantId];
|
|
117
|
+
if (!projectToken) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
// Auto-cleanup expired project token
|
|
121
|
+
if (this.isProjectTokenExpired(projectToken)) {
|
|
122
|
+
this.clearProjectToken(tenantId);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return projectToken;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Store a project token for a specific tenant
|
|
129
|
+
* @param tenantId - The tenant/CMS ID
|
|
130
|
+
* @param projectToken - The project token to store
|
|
131
|
+
*/ setProjectToken(tenantId, projectToken) {
|
|
132
|
+
const projectTokens = this.config.get('projectTokens') || {};
|
|
133
|
+
projectTokens[tenantId] = projectToken;
|
|
134
|
+
this.config.set('projectTokens', projectTokens);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Clear a project token for a specific tenant
|
|
138
|
+
* @param tenantId - The tenant/CMS ID
|
|
139
|
+
*/ clearProjectToken(tenantId) {
|
|
140
|
+
const projectTokens = this.config.get('projectTokens') || {};
|
|
141
|
+
delete projectTokens[tenantId];
|
|
142
|
+
this.config.set('projectTokens', projectTokens);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Clear all project tokens
|
|
146
|
+
*/ clearAllProjectTokens() {
|
|
147
|
+
this.config.delete('projectTokens');
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check if a project token is expired
|
|
151
|
+
* Includes a buffer time to avoid using tokens that are about to expire
|
|
152
|
+
* @param projectToken - The project token to check
|
|
153
|
+
* @returns true if token is expired or will expire within buffer time
|
|
154
|
+
*/ isProjectTokenExpired(projectToken) {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
|
|
157
|
+
const expiryWithBuffer = projectToken.expiresAt - bufferMs;
|
|
158
|
+
return now >= expiryWithBuffer;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Check if a valid (non-expired) project token exists for a tenant
|
|
162
|
+
* @param tenantId - The tenant/CMS ID
|
|
163
|
+
* @returns true if token exists and is not expired
|
|
164
|
+
*/ hasValidProjectToken(tenantId) {
|
|
165
|
+
const projectToken = this.getProjectToken(tenantId);
|
|
166
|
+
if (!projectToken) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return !this.isProjectTokenExpired(projectToken);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get validated JWT claims from a project token
|
|
173
|
+
* @param tenantId - The tenant/CMS ID
|
|
174
|
+
* @returns JWT payload claims or null if token not found or has no claims
|
|
175
|
+
*/ getProjectTokenClaims(tenantId) {
|
|
176
|
+
const projectToken = this.getProjectToken(tenantId);
|
|
177
|
+
return projectToken?.claims || null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get all tenant IDs that have stored project tokens
|
|
181
|
+
* @returns Array of tenant IDs
|
|
182
|
+
*/ getAllProjectTokenTenantIds() {
|
|
183
|
+
const projectTokens = this.config.get('projectTokens') || {};
|
|
184
|
+
return Object.keys(projectTokens);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/token-store.ts"],"sourcesContent":["/* eslint-disable perfectionist/sort-classes */\nimport Conf from 'conf'\n\nimport type { FigmaTokens, JWTPayload, ProjectToken, TokenStoreConfig } from './types.js'\n\nimport { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js'\nimport { deriveEncryptionKey } from './crypto-utils.js'\n\n/**\n * Secure storage manager for Figma OAuth2 tokens\n *\n * Uses the `conf` library to store tokens in an OS-specific secure location:\n * - macOS: ~/Library/Preferences/payloadcms-figma\n * - Linux: ~/.config/payloadcms-figma or $XDG_CONFIG_HOME/payloadcms-figma\n * - Windows: %APPDATA%/payloadcms-figma/Config\n *\n * Tokens are encrypted at rest with a machine-specific key and file permissions\n * are set to 0600 (owner read/write only)\n */\nexport class TokenStore {\n private config: Conf<{ projectTokens: Record<string, ProjectToken>; tokens: FigmaTokens }>\n\n constructor(options?: TokenStoreConfig) {\n this.config = new Conf<{ projectTokens: Record<string, ProjectToken>; tokens: FigmaTokens }>({\n configName: options?.configName || 'payloadcms-figma',\n projectName: 'payloadcms-figma',\n // Enable encryption with machine-specific key for better security\n encryptionKey: options?.encryptionKey || deriveEncryptionKey(),\n // Clear schema to avoid validation issues\n clearInvalidConfig: true,\n })\n }\n\n /**\n * Retrieve stored tokens\n * @returns FigmaTokens if stored, null otherwise\n */\n getTokens(): FigmaTokens | null {\n const tokens = this.config.get('tokens')\n return tokens || null\n }\n\n /**\n * Store OAuth2 tokens securely\n * @param tokens - Figma OAuth2 tokens to store\n */\n setTokens(tokens: FigmaTokens): void {\n this.config.set('tokens', tokens)\n }\n\n /**\n * Clear all stored tokens (used for logout)\n */\n clearTokens(): void {\n this.config.delete('tokens')\n }\n\n /**\n * Check if valid (non-expired) tokens exist\n * @returns true if tokens exist and are not expired\n */\n hasValidTokens(): boolean {\n const tokens = this.getTokens()\n if (!tokens) {\n return false\n }\n return !this.isExpired()\n }\n\n /**\n * Check if the current access token is expired\n * Includes a buffer time to avoid using tokens that are about to expire\n * @returns true if token is expired or will expire within buffer time\n */\n isExpired(): boolean {\n // Access config directly to avoid circular dependency with getTokens()\n const tokens = this.config.get('tokens')\n if (!tokens) {\n return true\n }\n\n const now = Date.now()\n const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000\n const expiryWithBuffer = tokens.expiresAt - bufferMs\n\n return now >= expiryWithBuffer\n }\n\n /**\n * Get the access token if it exists and is valid\n * @returns Access token string or null if expired/missing\n */\n getAccessToken(): null | string {\n if (!this.hasValidTokens()) {\n return null\n }\n const tokens = this.getTokens()\n return tokens?.accessToken || null\n }\n\n /**\n * Get the refresh token if it exists\n * @returns Refresh token string or null if missing\n */\n getRefreshToken(): null | string {\n const tokens = this.getTokens()\n return tokens?.refreshToken || null\n }\n\n /**\n * Update just the access token after a refresh\n * Preserves the existing refresh token and other metadata\n * @param accessToken - New access token\n * @param expiresIn - Expiration time in seconds\n */\n updateAccessToken(accessToken: string, expiresIn: number): void {\n const existingTokens = this.getTokens()\n if (!existingTokens) {\n throw new Error('Cannot update access token: no existing tokens found')\n }\n\n const expiresAt = Date.now() + expiresIn * 1000\n\n this.setTokens({\n ...existingTokens,\n accessToken,\n expiresAt,\n })\n }\n\n /**\n * Get the path to the config file (useful for debugging)\n * @returns Absolute path to the token storage file\n */\n getStoragePath(): string {\n return this.config.path\n }\n\n /**\n * Retrieve a project token for a specific tenant\n * Automatically removes expired tokens from storage\n * @param tenantId - The tenant/CMS ID\n * @returns ProjectToken if stored and valid, null otherwise\n */\n getProjectToken(tenantId: string): null | ProjectToken {\n const projectTokens = this.config.get('projectTokens') || {}\n const projectToken = projectTokens[tenantId]\n\n if (!projectToken) {\n return null\n }\n\n // Auto-cleanup expired project token\n if (this.isProjectTokenExpired(projectToken)) {\n this.clearProjectToken(tenantId)\n return null\n }\n\n return projectToken\n }\n\n /**\n * Store a project token for a specific tenant\n * @param tenantId - The tenant/CMS ID\n * @param projectToken - The project token to store\n */\n setProjectToken(tenantId: string, projectToken: ProjectToken): void {\n const projectTokens = this.config.get('projectTokens') || {}\n projectTokens[tenantId] = projectToken\n this.config.set('projectTokens', projectTokens)\n }\n\n /**\n * Clear a project token for a specific tenant\n * @param tenantId - The tenant/CMS ID\n */\n clearProjectToken(tenantId: string): void {\n const projectTokens = this.config.get('projectTokens') || {}\n delete projectTokens[tenantId]\n this.config.set('projectTokens', projectTokens)\n }\n\n /**\n * Clear all project tokens\n */\n clearAllProjectTokens(): void {\n this.config.delete('projectTokens')\n }\n\n /**\n * Check if a project token is expired\n * Includes a buffer time to avoid using tokens that are about to expire\n * @param projectToken - The project token to check\n * @returns true if token is expired or will expire within buffer time\n */\n isProjectTokenExpired(projectToken: ProjectToken): boolean {\n const now = Date.now()\n const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000\n const expiryWithBuffer = projectToken.expiresAt - bufferMs\n\n return now >= expiryWithBuffer\n }\n\n /**\n * Check if a valid (non-expired) project token exists for a tenant\n * @param tenantId - The tenant/CMS ID\n * @returns true if token exists and is not expired\n */\n hasValidProjectToken(tenantId: string): boolean {\n const projectToken = this.getProjectToken(tenantId)\n if (!projectToken) {\n return false\n }\n return !this.isProjectTokenExpired(projectToken)\n }\n\n /**\n * Get validated JWT claims from a project token\n * @param tenantId - The tenant/CMS ID\n * @returns JWT payload claims or null if token not found or has no claims\n */\n getProjectTokenClaims(tenantId: string): JWTPayload | null {\n const projectToken = this.getProjectToken(tenantId)\n return projectToken?.claims || null\n }\n\n /**\n * Get all tenant IDs that have stored project tokens\n * @returns Array of tenant IDs\n */\n getAllProjectTokenTenantIds(): string[] {\n const projectTokens = this.config.get('projectTokens') || {}\n return Object.keys(projectTokens)\n }\n}\n"],"names":["Conf","TOKEN_EXPIRY_BUFFER_SECONDS","deriveEncryptionKey","TokenStore","config","options","configName","projectName","encryptionKey","clearInvalidConfig","getTokens","tokens","get","setTokens","set","clearTokens","delete","hasValidTokens","isExpired","now","Date","bufferMs","expiryWithBuffer","expiresAt","getAccessToken","accessToken","getRefreshToken","refreshToken","updateAccessToken","expiresIn","existingTokens","Error","getStoragePath","path","getProjectToken","tenantId","projectTokens","projectToken","isProjectTokenExpired","clearProjectToken","setProjectToken","clearAllProjectTokens","hasValidProjectToken","getProjectTokenClaims","claims","getAllProjectTokenTenantIds","Object","keys"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.spec.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.spec.ts"],"names":[],"mappings":""}
|