@payloadcms/figma 0.0.1-alpha.29 → 0.0.1-alpha.3
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/dist/api/control-plane.d.ts +1 -22
- package/dist/api/control-plane.d.ts.map +1 -1
- package/dist/api/control-plane.js +15 -54
- package/dist/api/control-plane.js.map +1 -1
- package/dist/api/figma-api.d.ts.map +1 -1
- package/dist/api/figma-api.js +12 -38
- package/dist/api/figma-api.js.map +1 -1
- package/dist/auth/callback-server.d.ts.map +1 -1
- package/dist/auth/callback-server.js +1 -2
- package/dist/auth/callback-server.js.map +1 -1
- package/dist/auth/crypto-utils.d.ts.map +1 -1
- package/dist/auth/crypto-utils.js +5 -5
- package/dist/auth/crypto-utils.js.map +1 -1
- package/dist/auth/oauth-flow.d.ts +1 -1
- package/dist/auth/oauth-flow.d.ts.map +1 -1
- package/dist/auth/oauth-flow.js +5 -5
- package/dist/auth/oauth-flow.js.map +1 -1
- package/dist/auth/project-token.d.ts.map +1 -1
- package/dist/auth/project-token.js +4 -9
- package/dist/auth/project-token.js.map +1 -1
- package/dist/auth/token-store.d.ts +0 -10
- package/dist/auth/token-store.d.ts.map +1 -1
- package/dist/auth/token-store.js +0 -20
- package/dist/auth/token-store.js.map +1 -1
- package/dist/cli.js +0 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/deploy.d.ts +0 -4
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +81 -41
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/init.d.ts +5 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +85 -90
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list-tokens.js +2 -2
- package/dist/commands/list-tokens.js.map +1 -1
- package/dist/commands/login.d.ts +1 -8
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +6 -12
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +2 -2
- package/dist/commands/logout.js.map +1 -1
- package/dist/config/oauth.d.ts +0 -5
- package/dist/config/oauth.d.ts.map +1 -1
- package/dist/config/oauth.js +4 -9
- package/dist/config/oauth.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/oauth/components/LoginButton/index.d.ts.map +1 -1
- package/dist/oauth/components/LoginButton/index.js +6 -50
- package/dist/oauth/components/LoginButton/index.js.map +1 -1
- package/dist/oauth/components/LoginButton/index.scss +3 -50
- package/dist/oauth/components/LogoutButton/index.d.ts.map +1 -1
- package/dist/oauth/components/LogoutButton/index.js +1 -0
- package/dist/oauth/components/LogoutButton/index.js.map +1 -1
- package/dist/oauth/defaults.d.ts.map +1 -1
- package/dist/oauth/defaults.js +5 -29
- package/dist/oauth/defaults.js.map +1 -1
- package/dist/oauth/endpoints/getLoginEndpoint.d.ts.map +1 -1
- package/dist/oauth/endpoints/getLoginEndpoint.js +1 -1
- package/dist/oauth/endpoints/getLoginEndpoint.js.map +1 -1
- package/dist/oauth/index.d.ts.map +1 -1
- package/dist/oauth/index.js +6 -42
- package/dist/oauth/index.js.map +1 -1
- package/dist/oauth/strategy/index.d.ts.map +1 -1
- package/dist/oauth/strategy/index.js +2 -6
- package/dist/oauth/strategy/index.js.map +1 -1
- package/dist/oauth/types.d.ts +6 -6
- package/dist/oauth/types.d.ts.map +1 -1
- package/dist/oauth/types.js.map +1 -1
- package/dist/oauth/utilities/createCookieOptions.d.ts.map +1 -1
- package/dist/oauth/utilities/createCookieOptions.js.map +1 -1
- package/dist/oauth/utilities/refreshTokens.d.ts.map +1 -1
- package/dist/oauth/utilities/refreshTokens.js +1 -1
- package/dist/oauth/utilities/refreshTokens.js.map +1 -1
- package/dist/plugin/build-config.d.ts +4 -20
- package/dist/plugin/build-config.d.ts.map +1 -1
- package/dist/plugin/build-config.js +9 -51
- package/dist/plugin/build-config.js.map +1 -1
- package/dist/utils/env-management.d.ts +8 -9
- package/dist/utils/env-management.d.ts.map +1 -1
- package/dist/utils/env-management.js +74 -73
- package/dist/utils/env-management.js.map +1 -1
- package/dist/utils/formatter.d.ts.map +1 -1
- package/dist/utils/formatter.js +7 -1
- package/dist/utils/formatter.js.map +1 -1
- package/dist/utils/messages.d.ts.map +1 -1
- package/dist/utils/messages.js +7 -5
- package/dist/utils/messages.js.map +1 -1
- package/dist/utils/payload-config-ast.d.ts +3 -22
- package/dist/utils/payload-config-ast.d.ts.map +1 -1
- package/dist/utils/payload-config-ast.js +31 -89
- package/dist/utils/payload-config-ast.js.map +1 -1
- package/dist/utils/payload-config-finder.d.ts +2 -2
- package/dist/utils/payload-config-finder.d.ts.map +1 -1
- package/dist/utils/payload-config-finder.js +9 -48
- package/dist/utils/payload-config-finder.js.map +1 -1
- package/dist/utils/payload-config-modifier.d.ts.map +1 -1
- package/dist/utils/payload-config-modifier.js +4 -33
- package/dist/utils/payload-config-modifier.js.map +1 -1
- package/dist/utils/payload-package-check.d.ts +0 -5
- package/dist/utils/payload-package-check.d.ts.map +1 -1
- package/dist/utils/payload-package-check.js +4 -48
- package/dist/utils/payload-package-check.js.map +1 -1
- package/dist/utils/project.d.ts.map +1 -1
- package/dist/utils/project.js +6 -8
- package/dist/utils/project.js.map +1 -1
- package/dist/utils/s3-upload.d.ts +1 -0
- package/dist/utils/s3-upload.d.ts.map +1 -1
- package/dist/utils/s3-upload.js +3 -10
- package/dist/utils/s3-upload.js.map +1 -1
- package/dist/utils/token-display.d.ts.map +1 -1
- package/dist/utils/token-display.js +5 -11
- package/dist/utils/token-display.js.map +1 -1
- package/dist/utils/typescript-validator.d.ts.map +1 -1
- package/dist/utils/typescript-validator.js +6 -1
- package/dist/utils/typescript-validator.js.map +1 -1
- package/package.json +6 -20
- package/dist/db-content-api/generated/content-api-types.d.ts +0 -3129
- package/dist/db-content-api/generated/content-api-types.d.ts.map +0 -1
- package/dist/db-content-api/generated/content-api-types.js +0 -7
- package/dist/db-content-api/generated/content-api-types.js.map +0 -1
- package/dist/db-content-api/index.d.ts +0 -31
- package/dist/db-content-api/index.d.ts.map +0 -1
- package/dist/db-content-api/index.js +0 -842
- package/dist/db-content-api/index.js.map +0 -1
- package/dist/db-content-api/types.d.ts +0 -31
- package/dist/db-content-api/types.d.ts.map +0 -1
- package/dist/db-content-api/types.js +0 -7
- package/dist/db-content-api/types.js.map +0 -1
- package/dist/endpoints/health.d.ts +0 -3
- package/dist/endpoints/health.d.ts.map +0 -1
- package/dist/endpoints/health.js +0 -11
- package/dist/endpoints/health.js.map +0 -1
- package/dist/exports/client.d.ts +0 -3
- package/dist/exports/client.d.ts.map +0 -1
- package/dist/exports/client.js +0 -4
- package/dist/exports/client.js.map +0 -1
- package/dist/oauth/utilities/isDuplicateKeyError.d.ts +0 -7
- package/dist/oauth/utilities/isDuplicateKeyError.d.ts.map +0 -1
- package/dist/oauth/utilities/isDuplicateKeyError.js +0 -14
- package/dist/oauth/utilities/isDuplicateKeyError.js.map +0 -1
- package/dist/templates/README.md +0 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-flow.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-flow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAS7C;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAG9B,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB
|
|
1
|
+
{"version":3,"file":"oauth-flow.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-flow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAS7C;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAG9B,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,MAAM,EAAE,WAAW,CAAA;IACnB,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAsI1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAyBxF"}
|
package/dist/auth/oauth-flow.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { OAUTH_CONFIG } from '../config/oauth.js';
|
|
4
4
|
import * as log from '../utils/log.js';
|
|
5
5
|
import { buildAuthorizationUrl, openBrowser } from './browser.js';
|
|
6
6
|
import { CallbackServer } from './callback-server.js';
|
|
@@ -32,11 +32,11 @@ import { exchangeCodeForTokens, refreshAccessToken } from './refresh.js';
|
|
|
32
32
|
* @throws OAuthFlowError if any step fails
|
|
33
33
|
*/ export async function executeOAuthFlow(tokenStore, options = {}) {
|
|
34
34
|
// Check if OAuth flow should be mocked
|
|
35
|
-
// Default to
|
|
36
|
-
const shouldMock = process.env.FIGMA_MOCK_OAUTH
|
|
35
|
+
// Default to true for OAuth (allow testing without real authentication by default)
|
|
36
|
+
const shouldMock = process.env.FIGMA_MOCK_OAUTH !== 'false';
|
|
37
37
|
if (shouldMock) {
|
|
38
38
|
// MOCK IMPLEMENTATION: Return mock tokens for testing
|
|
39
|
-
log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH
|
|
39
|
+
log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH != "false")');
|
|
40
40
|
const mockTokens = {
|
|
41
41
|
accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),
|
|
42
42
|
expiresAt: Date.now() + 2 * 60 * 60 * 1000,
|
|
@@ -52,7 +52,7 @@ import { exchangeCodeForTokens, refreshAccessToken } from './refresh.js';
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
// REAL OAUTH IMPLEMENTATION
|
|
55
|
-
const { clientId = OAUTH_CONFIG.clientId, port =
|
|
55
|
+
const { clientId = OAUTH_CONFIG.clientId, port = 3000, redirectUri = OAUTH_CONFIG.redirectUri, scopes = OAUTH_CONFIG.scopes, timeoutMs = 120000 } = options;
|
|
56
56
|
// Generate PKCE pair for enhanced security
|
|
57
57
|
const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair();
|
|
58
58
|
// Generate random state for CSRF protection
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/oauth-flow.ts"],"sourcesContent":["import * as p from '@clack/prompts'\nimport crypto from 'crypto'\n\nimport type { TokenStore } from './token-store.js'\nimport type { FigmaTokens } from './types.js'\n\nimport { DEFAULT_CALLBACK_PORT, OAUTH_CONFIG } from '../config/oauth.js'\nimport * as log from '../utils/log.js'\nimport { buildAuthorizationUrl, openBrowser } from './browser.js'\nimport { CallbackServer } from './callback-server.js'\nimport { generatePKCEPair } from './pkce.js'\nimport { exchangeCodeForTokens, refreshAccessToken } from './refresh.js'\n\n/**\n * Error during OAuth flow\n */\nexport class OAuthFlowError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'OAuthFlowError'\n }\n}\n\n/**\n * Options for OAuth flow\n */\nexport interface OAuthFlowOptions {\n /** OAuth2 client ID (defaults to config) */\n clientId?: string\n /** Callback server port (defaults to DEFAULT_CALLBACK_PORT) */\n port?: number\n /** Redirect URI (defaults to config) */\n redirectUri?: string\n /** OAuth scopes (defaults to config) */\n scopes?: string[]\n /** Callback timeout in milliseconds (defaults to 120000) */\n timeoutMs?: number\n}\n\n/**\n * Result of OAuth flow\n */\nexport interface OAuthFlowResult {\n /** OAuth2 tokens */\n tokens: FigmaTokens\n /** Figma user ID */\n userId?: string\n}\n\n/**\n * Execute complete OAuth2 authorization flow\n *\n * This orchestrates the full OAuth flow:\n * 1. Generate PKCE pair for security\n * 2. Start local callback server\n * 3. Open browser to authorization URL\n * 4. Wait for user to authorize\n * 5. Exchange authorization code for tokens\n * 6. Store tokens securely\n *\n * @param tokenStore - Token store for persisting tokens\n * @param options - Flow configuration options\n * @returns Promise resolving to OAuth flow result\n * @throws OAuthFlowError if any step fails\n */\nexport async function executeOAuthFlow(\n tokenStore: TokenStore,\n options: OAuthFlowOptions = {},\n): Promise<OAuthFlowResult> {\n // Check if OAuth flow should be mocked\n // Default to false for OAuth\n const shouldMock = process.env.FIGMA_MOCK_OAUTH === 'true'\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Return mock tokens for testing\n log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH = true)')\n const mockTokens: FigmaTokens = {\n accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),\n expiresAt: Date.now() + 2 * 60 * 60 * 1000, // 2 hours\n refreshToken: 'figt_mock_refresh_token_' + crypto.randomBytes(16).toString('hex'),\n scopes: (options.scopes || OAUTH_CONFIG.scopes) as FigmaTokens['scopes'],\n tokenType: 'Bearer',\n }\n\n // Store mock tokens\n tokenStore.setTokens(mockTokens)\n\n return {\n tokens: mockTokens,\n userId: 'mock_user_123',\n }\n }\n\n // REAL OAUTH IMPLEMENTATION\n const {\n clientId = OAUTH_CONFIG.clientId,\n port = DEFAULT_CALLBACK_PORT,\n redirectUri = OAUTH_CONFIG.redirectUri,\n scopes = OAUTH_CONFIG.scopes,\n timeoutMs = 120000,\n } = options\n\n // Generate PKCE pair for enhanced security\n const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair()\n\n // Generate random state for CSRF protection\n const state = crypto.randomBytes(32).toString('base64url')\n\n // Create callback server\n const callbackServer = new CallbackServer({ port, timeoutMs })\n\n try {\n // Start waiting for callback (this starts the server and captures actual port)\n const callbackPromise = callbackServer.waitForCallback(state)\n\n // Wait a bit for server to start and determine actual port\n await new Promise((resolve) => setTimeout(resolve, 100))\n\n // Get the actual port (may differ from preferred if that was in use)\n const actualPort = callbackServer.getActualPort()\n\n // Build redirect URI with actual port\n const actualRedirectUri = redirectUri.replace(/:\\d+/, `:${actualPort}`)\n\n // Build authorization URL\n const authUrl = buildAuthorizationUrl({\n authorizationUrl: OAUTH_CONFIG.authorizationUrl,\n clientId,\n codeChallenge,\n codeChallengeMethod,\n redirectUri: actualRedirectUri,\n scopes,\n state,\n })\n\n // Prompt user to open browser\n const shouldOpenBrowser = await p.confirm({\n initialValue: true,\n message: 'Log in or create Figma account in your browser?',\n })\n\n if (p.isCancel(shouldOpenBrowser)) {\n throw new OAuthFlowError('Authentication cancelled by user')\n }\n\n if (shouldOpenBrowser) {\n // Try to open browser, but don't fail if it doesn't work\n try {\n await openBrowser(authUrl)\n } catch (_error) {\n // Browser opening failed, show URL for manual opening\n log.warning('Could not open browser automatically.')\n log.info(`Please open this URL in your browser:`)\n log.info(` ${authUrl}`)\n }\n } else {\n // User declined automatic opening, show URL\n log.info(`Please open this URL manually in your browser:`)\n log.info(` ${authUrl}`)\n }\n\n // Wait for callback\n const { code } = await callbackPromise\n\n // Exchange authorization code for tokens\n let tokens: FigmaTokens\n try {\n tokens = await exchangeCodeForTokens(code, codeVerifier, actualRedirectUri, clientId)\n } catch (error) {\n throw new OAuthFlowError(\n 'Failed to exchange authorization code for tokens',\n error instanceof Error ? error : undefined,\n )\n }\n\n // Store tokens\n try {\n tokenStore.setTokens(tokens)\n } catch (error) {\n throw new OAuthFlowError('Failed to store tokens', error instanceof Error ? error : undefined)\n }\n\n return {\n tokens,\n userId: tokens.userId,\n }\n } catch (error) {\n // Clean up server on error\n callbackServer.stop()\n\n if (error instanceof OAuthFlowError) {\n throw error\n }\n\n throw new OAuthFlowError(\n `OAuth flow failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n } finally {\n // Ensure server is always stopped\n callbackServer.stop()\n }\n}\n\n/**\n * Get valid access token, refreshing if necessary\n *\n * Checks if stored tokens exist and are valid. If expired, automatically\n * refreshes using the refresh token. If no tokens exist, returns null.\n *\n * @param tokenStore - Token store to use\n * @returns Promise resolving to access token or null if no valid tokens\n * @throws OAuthFlowError if refresh fails\n */\nexport async function getValidAccessToken(tokenStore: TokenStore): Promise<null | string> {\n // Check if we have valid tokens\n if (tokenStore.hasValidTokens()) {\n return tokenStore.getAccessToken()\n }\n\n // Check if we have a refresh token\n const refreshToken = tokenStore.getRefreshToken()\n if (!refreshToken) {\n return null\n }\n\n // Try to refresh\n try {\n const tokens = await refreshAccessToken(refreshToken)\n tokenStore.setTokens(tokens)\n return tokens.accessToken\n } catch (error) {\n // Refresh failed - clear invalid tokens\n tokenStore.clearTokens()\n throw new OAuthFlowError(\n 'Failed to refresh access token',\n error instanceof Error ? error : undefined,\n )\n }\n}\n"],"names":["p","crypto","DEFAULT_CALLBACK_PORT","OAUTH_CONFIG","log","buildAuthorizationUrl","openBrowser","CallbackServer","generatePKCEPair","exchangeCodeForTokens","refreshAccessToken","OAuthFlowError","Error","message","cause","name","executeOAuthFlow","tokenStore","options","shouldMock","process","env","FIGMA_MOCK_OAUTH","debug","mockTokens","accessToken","randomBytes","toString","expiresAt","Date","now","refreshToken","scopes","tokenType","setTokens","tokens","userId","clientId","port","redirectUri","timeoutMs","codeChallenge","codeChallengeMethod","codeVerifier","state","callbackServer","callbackPromise","waitForCallback","Promise","resolve","setTimeout","actualPort","getActualPort","actualRedirectUri","replace","authUrl","authorizationUrl","shouldOpenBrowser","confirm","initialValue","isCancel","_error","warning","info","code","error","undefined","stop","getValidAccessToken","hasValidTokens","getAccessToken","getRefreshToken","clearTokens"],"mappings":"AAAA,YAAYA,OAAO,iBAAgB;AACnC,OAAOC,YAAY,SAAQ;AAK3B,SAASC,qBAAqB,EAAEC,YAAY,QAAQ,qBAAoB;AACxE,YAAYC,SAAS,kBAAiB;AACtC,SAASC,qBAAqB,EAAEC,WAAW,QAAQ,eAAc;AACjE,SAASC,cAAc,QAAQ,uBAAsB;AACrD,SAASC,gBAAgB,QAAQ,YAAW;AAC5C,SAASC,qBAAqB,EAAEC,kBAAkB,QAAQ,eAAc;AAExE;;CAEC,GACD,OAAO,MAAMC,uBAAuBC;;IAClC,YACEC,OAAe,EACf,AAAOC,KAAa,CACpB;QACA,KAAK,CAACD,eAFCC,QAAAA;QAGP,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AA4BA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,eAAeC,iBACpBC,UAAsB,EACtBC,UAA4B,CAAC,CAAC;IAE9B,uCAAuC;IACvC,6BAA6B;IAC7B,MAAMC,aAAaC,QAAQC,GAAG,CAACC,gBAAgB,KAAK;IAEpD,IAAIH,YAAY;QACd,sDAAsD;QACtDf,IAAImB,KAAK,CAAC;QACV,MAAMC,aAA0B;YAC9BC,aAAa,4BAA4BxB,OAAOyB,WAAW,CAAC,IAAIC,QAAQ,CAAC;YACzEC,WAAWC,KAAKC,GAAG,KAAK,IAAI,KAAK,KAAK;YACtCC,cAAc,6BAA6B9B,OAAOyB,WAAW,CAAC,IAAIC,QAAQ,CAAC;YAC3EK,QAASd,QAAQc,MAAM,IAAI7B,aAAa6B,MAAM;YAC9CC,WAAW;QACb;QAEA,oBAAoB;QACpBhB,WAAWiB,SAAS,CAACV;QAErB,OAAO;YACLW,QAAQX;YACRY,QAAQ;QACV;IACF;IAEA,4BAA4B;IAC5B,MAAM,EACJC,WAAWlC,aAAakC,QAAQ,EAChCC,OAAOpC,qBAAqB,EAC5BqC,cAAcpC,aAAaoC,WAAW,EACtCP,SAAS7B,aAAa6B,MAAM,EAC5BQ,YAAY,MAAM,EACnB,GAAGtB;IAEJ,2CAA2C;IAC3C,MAAM,EAAEuB,aAAa,EAAEC,mBAAmB,EAAEC,YAAY,EAAE,GAAGnC;IAE7D,4CAA4C;IAC5C,MAAMoC,QAAQ3C,OAAOyB,WAAW,CAAC,IAAIC,QAAQ,CAAC;IAE9C,yBAAyB;IACzB,MAAMkB,iBAAiB,IAAItC,eAAe;QAAE+B;QAAME;IAAU;IAE5D,IAAI;QACF,+EAA+E;QAC/E,MAAMM,kBAAkBD,eAAeE,eAAe,CAACH;QAEvD,2DAA2D;QAC3D,MAAM,IAAII,QAAQ,CAACC,UAAYC,WAAWD,SAAS;QAEnD,qEAAqE;QACrE,MAAME,aAAaN,eAAeO,aAAa;QAE/C,sCAAsC;QACtC,MAAMC,oBAAoBd,YAAYe,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAEH,YAAY;QAEtE,0BAA0B;QAC1B,MAAMI,UAAUlD,sBAAsB;YACpCmD,kBAAkBrD,aAAaqD,gBAAgB;YAC/CnB;YACAI;YACAC;YACAH,aAAac;YACbrB;YACAY;QACF;QAEA,8BAA8B;QAC9B,MAAMa,oBAAoB,MAAMzD,EAAE0D,OAAO,CAAC;YACxCC,cAAc;YACd9C,SAAS;QACX;QAEA,IAAIb,EAAE4D,QAAQ,CAACH,oBAAoB;YACjC,MAAM,IAAI9C,eAAe;QAC3B;QAEA,IAAI8C,mBAAmB;YACrB,yDAAyD;YACzD,IAAI;gBACF,MAAMnD,YAAYiD;YACpB,EAAE,OAAOM,QAAQ;gBACf,sDAAsD;gBACtDzD,IAAI0D,OAAO,CAAC;gBACZ1D,IAAI2D,IAAI,CAAC,CAAC,qCAAqC,CAAC;gBAChD3D,IAAI2D,IAAI,CAAC,CAAC,EAAE,EAAER,SAAS;YACzB;QACF,OAAO;YACL,4CAA4C;YAC5CnD,IAAI2D,IAAI,CAAC,CAAC,8CAA8C,CAAC;YACzD3D,IAAI2D,IAAI,CAAC,CAAC,EAAE,EAAER,SAAS;QACzB;QAEA,oBAAoB;QACpB,MAAM,EAAES,IAAI,EAAE,GAAG,MAAMlB;QAEvB,yCAAyC;QACzC,IAAIX;QACJ,IAAI;YACFA,SAAS,MAAM1B,sBAAsBuD,MAAMrB,cAAcU,mBAAmBhB;QAC9E,EAAE,OAAO4B,OAAO;YACd,MAAM,IAAItD,eACR,oDACAsD,iBAAiBrD,QAAQqD,QAAQC;QAErC;QAEA,eAAe;QACf,IAAI;YACFjD,WAAWiB,SAAS,CAACC;QACvB,EAAE,OAAO8B,OAAO;YACd,MAAM,IAAItD,eAAe,0BAA0BsD,iBAAiBrD,QAAQqD,QAAQC;QACtF;QAEA,OAAO;YACL/B;YACAC,QAAQD,OAAOC,MAAM;QACvB;IACF,EAAE,OAAO6B,OAAO;QACd,2BAA2B;QAC3BpB,eAAesB,IAAI;QAEnB,IAAIF,iBAAiBtD,gBAAgB;YACnC,MAAMsD;QACR;QAEA,MAAM,IAAItD,eACR,CAAC,mBAAmB,EAAEsD,iBAAiBrD,QAAQqD,MAAMpD,OAAO,GAAG,iBAAiB,EAChFoD,iBAAiBrD,QAAQqD,QAAQC;IAErC,SAAU;QACR,kCAAkC;QAClCrB,eAAesB,IAAI;IACrB;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,eAAeC,oBAAoBnD,UAAsB;IAC9D,gCAAgC;IAChC,IAAIA,WAAWoD,cAAc,IAAI;QAC/B,OAAOpD,WAAWqD,cAAc;IAClC;IAEA,mCAAmC;IACnC,MAAMvC,eAAed,WAAWsD,eAAe;IAC/C,IAAI,CAACxC,cAAc;QACjB,OAAO;IACT;IAEA,iBAAiB;IACjB,IAAI;QACF,MAAMI,SAAS,MAAMzB,mBAAmBqB;QACxCd,WAAWiB,SAAS,CAACC;QACrB,OAAOA,OAAOV,WAAW;IAC3B,EAAE,OAAOwC,OAAO;QACd,wCAAwC;QACxChD,WAAWuD,WAAW;QACtB,MAAM,IAAI7D,eACR,kCACAsD,iBAAiBrD,QAAQqD,QAAQC;IAErC;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/oauth-flow.ts"],"sourcesContent":["import * as p from '@clack/prompts'\nimport crypto from 'crypto'\n\nimport type { TokenStore } from './token-store.js'\nimport type { FigmaTokens } from './types.js'\n\nimport { OAUTH_CONFIG } from '../config/oauth.js'\nimport * as log from '../utils/log.js'\nimport { buildAuthorizationUrl, openBrowser } from './browser.js'\nimport { CallbackServer } from './callback-server.js'\nimport { generatePKCEPair } from './pkce.js'\nimport { exchangeCodeForTokens, refreshAccessToken } from './refresh.js'\n\n/**\n * Error during OAuth flow\n */\nexport class OAuthFlowError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'OAuthFlowError'\n }\n}\n\n/**\n * Options for OAuth flow\n */\nexport interface OAuthFlowOptions {\n /** OAuth2 client ID (defaults to config) */\n clientId?: string\n /** Callback server port (defaults to 3000) */\n port?: number\n /** Redirect URI (defaults to config) */\n redirectUri?: string\n /** OAuth scopes (defaults to config) */\n scopes?: string[]\n /** Callback timeout in milliseconds (defaults to 120000) */\n timeoutMs?: number\n}\n\n/**\n * Result of OAuth flow\n */\nexport interface OAuthFlowResult {\n /** OAuth2 tokens */\n tokens: FigmaTokens\n /** Figma user ID */\n userId?: string\n}\n\n/**\n * Execute complete OAuth2 authorization flow\n *\n * This orchestrates the full OAuth flow:\n * 1. Generate PKCE pair for security\n * 2. Start local callback server\n * 3. Open browser to authorization URL\n * 4. Wait for user to authorize\n * 5. Exchange authorization code for tokens\n * 6. Store tokens securely\n *\n * @param tokenStore - Token store for persisting tokens\n * @param options - Flow configuration options\n * @returns Promise resolving to OAuth flow result\n * @throws OAuthFlowError if any step fails\n */\nexport async function executeOAuthFlow(\n tokenStore: TokenStore,\n options: OAuthFlowOptions = {},\n): Promise<OAuthFlowResult> {\n // Check if OAuth flow should be mocked\n // Default to true for OAuth (allow testing without real authentication by default)\n const shouldMock = process.env.FIGMA_MOCK_OAUTH !== 'false'\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Return mock tokens for testing\n log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH != \"false\")')\n const mockTokens: FigmaTokens = {\n accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),\n expiresAt: Date.now() + 2 * 60 * 60 * 1000, // 2 hours\n refreshToken: 'figt_mock_refresh_token_' + crypto.randomBytes(16).toString('hex'),\n scopes: (options.scopes || OAUTH_CONFIG.scopes) as FigmaTokens['scopes'],\n tokenType: 'Bearer',\n }\n\n // Store mock tokens\n tokenStore.setTokens(mockTokens)\n\n return {\n tokens: mockTokens,\n userId: 'mock_user_123',\n }\n }\n\n // REAL OAUTH IMPLEMENTATION\n const {\n clientId = OAUTH_CONFIG.clientId,\n port = 3000,\n redirectUri = OAUTH_CONFIG.redirectUri,\n scopes = OAUTH_CONFIG.scopes,\n timeoutMs = 120000,\n } = options\n\n // Generate PKCE pair for enhanced security\n const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair()\n\n // Generate random state for CSRF protection\n const state = crypto.randomBytes(32).toString('base64url')\n\n // Create callback server\n const callbackServer = new CallbackServer({ port, timeoutMs })\n\n try {\n // Start waiting for callback (this starts the server and captures actual port)\n const callbackPromise = callbackServer.waitForCallback(state)\n\n // Wait a bit for server to start and determine actual port\n await new Promise((resolve) => setTimeout(resolve, 100))\n\n // Get the actual port (may differ from preferred if that was in use)\n const actualPort = callbackServer.getActualPort()\n\n // Build redirect URI with actual port\n const actualRedirectUri = redirectUri.replace(/:\\d+/, `:${actualPort}`)\n\n // Build authorization URL\n const authUrl = buildAuthorizationUrl({\n authorizationUrl: OAUTH_CONFIG.authorizationUrl,\n clientId,\n codeChallenge,\n codeChallengeMethod,\n redirectUri: actualRedirectUri,\n scopes,\n state,\n })\n\n // Prompt user to open browser\n const shouldOpenBrowser = await p.confirm({\n initialValue: true,\n message: 'Log in or create Figma account in your browser?',\n })\n\n if (p.isCancel(shouldOpenBrowser)) {\n throw new OAuthFlowError('Authentication cancelled by user')\n }\n\n if (shouldOpenBrowser) {\n // Try to open browser, but don't fail if it doesn't work\n try {\n await openBrowser(authUrl)\n } catch (_error) {\n // Browser opening failed, show URL for manual opening\n log.warning('Could not open browser automatically.')\n log.info(`Please open this URL in your browser:`)\n log.info(` ${authUrl}`)\n }\n } else {\n // User declined automatic opening, show URL\n log.info(`Please open this URL manually in your browser:`)\n log.info(` ${authUrl}`)\n }\n\n // Wait for callback\n const { code } = await callbackPromise\n\n // Exchange authorization code for tokens\n let tokens: FigmaTokens\n try {\n tokens = await exchangeCodeForTokens(code, codeVerifier, actualRedirectUri, clientId)\n } catch (error) {\n throw new OAuthFlowError(\n 'Failed to exchange authorization code for tokens',\n error instanceof Error ? error : undefined,\n )\n }\n\n // Store tokens\n try {\n tokenStore.setTokens(tokens)\n } catch (error) {\n throw new OAuthFlowError('Failed to store tokens', error instanceof Error ? error : undefined)\n }\n\n return {\n tokens,\n userId: tokens.userId,\n }\n } catch (error) {\n // Clean up server on error\n callbackServer.stop()\n\n if (error instanceof OAuthFlowError) {\n throw error\n }\n\n throw new OAuthFlowError(\n `OAuth flow failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n } finally {\n // Ensure server is always stopped\n callbackServer.stop()\n }\n}\n\n/**\n * Get valid access token, refreshing if necessary\n *\n * Checks if stored tokens exist and are valid. If expired, automatically\n * refreshes using the refresh token. If no tokens exist, returns null.\n *\n * @param tokenStore - Token store to use\n * @returns Promise resolving to access token or null if no valid tokens\n * @throws OAuthFlowError if refresh fails\n */\nexport async function getValidAccessToken(tokenStore: TokenStore): Promise<null | string> {\n // Check if we have valid tokens\n if (tokenStore.hasValidTokens()) {\n return tokenStore.getAccessToken()\n }\n\n // Check if we have a refresh token\n const refreshToken = tokenStore.getRefreshToken()\n if (!refreshToken) {\n return null\n }\n\n // Try to refresh\n try {\n const tokens = await refreshAccessToken(refreshToken)\n tokenStore.setTokens(tokens)\n return tokens.accessToken\n } catch (error) {\n // Refresh failed - clear invalid tokens\n tokenStore.clearTokens()\n throw new OAuthFlowError(\n 'Failed to refresh access token',\n error instanceof Error ? error : undefined,\n )\n }\n}\n"],"names":["p","crypto","OAUTH_CONFIG","log","buildAuthorizationUrl","openBrowser","CallbackServer","generatePKCEPair","exchangeCodeForTokens","refreshAccessToken","OAuthFlowError","Error","message","cause","name","executeOAuthFlow","tokenStore","options","shouldMock","process","env","FIGMA_MOCK_OAUTH","debug","mockTokens","accessToken","randomBytes","toString","expiresAt","Date","now","refreshToken","scopes","tokenType","setTokens","tokens","userId","clientId","port","redirectUri","timeoutMs","codeChallenge","codeChallengeMethod","codeVerifier","state","callbackServer","callbackPromise","waitForCallback","Promise","resolve","setTimeout","actualPort","getActualPort","actualRedirectUri","replace","authUrl","authorizationUrl","shouldOpenBrowser","confirm","initialValue","isCancel","_error","warning","info","code","error","undefined","stop","getValidAccessToken","hasValidTokens","getAccessToken","getRefreshToken","clearTokens"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-token.d.ts","sourceRoot":"","sources":["../../src/auth/project-token.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AASlD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAkCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"project-token.d.ts","sourceRoot":"","sources":["../../src/auth/project-token.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AASlD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAkCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAiExB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAMxB"}
|
|
@@ -60,17 +60,13 @@ import { getValidAccessToken } from './oauth-flow.js';
|
|
|
60
60
|
* @returns Promise resolving to JWT token string or null if failed
|
|
61
61
|
* @throws {ProjectTokenError} If token generation fails
|
|
62
62
|
*/ export async function getValidProjectToken(tokenStore, tenantId) {
|
|
63
|
-
if
|
|
64
|
-
log.debug('FIGMA_CONTENT_API_ACCESS_KEY is set; skipping project token retrieval from API');
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
// Check for existing valid project token
|
|
63
|
+
// Check if we have a valid project token already
|
|
68
64
|
if (tokenStore.hasValidProjectToken(tenantId)) {
|
|
69
65
|
const projectToken = tokenStore.getProjectToken(tenantId);
|
|
70
66
|
return projectToken?.token || null;
|
|
71
67
|
}
|
|
72
68
|
// We need to refresh the project token
|
|
73
|
-
//
|
|
69
|
+
// First, get a valid user access token (will auto-refresh if needed)
|
|
74
70
|
let accessToken;
|
|
75
71
|
try {
|
|
76
72
|
accessToken = await getValidAccessToken(tokenStore);
|
|
@@ -84,12 +80,11 @@ import { getValidAccessToken } from './oauth-flow.js';
|
|
|
84
80
|
// Fetch a new project token from the Figma API
|
|
85
81
|
const projectToken = await fetchProjectToken(accessToken, tenantId);
|
|
86
82
|
// Validate the JWT signature and claims (skip for mocks)
|
|
87
|
-
const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE;
|
|
88
83
|
const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
|
|
89
84
|
let validatedClaims;
|
|
90
|
-
if (
|
|
85
|
+
if (shouldMock) {
|
|
91
86
|
// MOCK: Skip JWT validation and parse claims directly
|
|
92
|
-
log.debug('Skipping JWT validation for mock
|
|
87
|
+
log.debug('Skipping JWT validation for mock project token');
|
|
93
88
|
validatedClaims = parseJWTClaims(projectToken.token);
|
|
94
89
|
} else {
|
|
95
90
|
// REAL: Validate JWT signature against JWKS
|
|
@@ -1 +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 * 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
|
|
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 * 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 log.debug('Skipping JWT validation for mock project token')\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","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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"}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { FigmaTokens, JWTPayload, ProjectToken, TokenStoreConfig } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Get the singleton TokenStore instance
|
|
4
|
-
* Use this for all production code to ensure a single shared instance
|
|
5
|
-
*
|
|
6
|
-
* @returns The singleton TokenStore instance
|
|
7
|
-
*/
|
|
8
|
-
export declare function getTokenStore(): TokenStore;
|
|
9
2
|
/**
|
|
10
3
|
* Secure storage manager for Figma OAuth2 tokens
|
|
11
4
|
*
|
|
@@ -16,9 +9,6 @@ export declare function getTokenStore(): TokenStore;
|
|
|
16
9
|
*
|
|
17
10
|
* Tokens are encrypted at rest with a machine-specific key and file permissions
|
|
18
11
|
* are set to 0600 (owner read/write only)
|
|
19
|
-
*
|
|
20
|
-
* For production code, use getTokenStore() to get the singleton instance.
|
|
21
|
-
* The constructor is still exported for test isolation.
|
|
22
12
|
*/
|
|
23
13
|
export declare class TokenStore {
|
|
24
14
|
private config;
|
|
@@ -1 +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;
|
|
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"}
|
package/dist/auth/token-store.js
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
/* eslint-disable perfectionist/sort-classes */ import Conf from 'conf';
|
|
2
2
|
import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
|
|
3
3
|
import { deriveEncryptionKey } from './crypto-utils.js';
|
|
4
|
-
/**
|
|
5
|
-
* Module-level singleton instance for production use
|
|
6
|
-
*/ let defaultInstance = null;
|
|
7
|
-
/**
|
|
8
|
-
* Get the singleton TokenStore instance
|
|
9
|
-
* Use this for all production code to ensure a single shared instance
|
|
10
|
-
*
|
|
11
|
-
* @returns The singleton TokenStore instance
|
|
12
|
-
*/ export function getTokenStore() {
|
|
13
|
-
if (!defaultInstance) {
|
|
14
|
-
defaultInstance = new TokenStore();
|
|
15
|
-
}
|
|
16
|
-
return defaultInstance;
|
|
17
|
-
}
|
|
18
4
|
/**
|
|
19
5
|
* Secure storage manager for Figma OAuth2 tokens
|
|
20
6
|
*
|
|
@@ -25,15 +11,9 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
25
11
|
*
|
|
26
12
|
* Tokens are encrypted at rest with a machine-specific key and file permissions
|
|
27
13
|
* are set to 0600 (owner read/write only)
|
|
28
|
-
*
|
|
29
|
-
* For production code, use getTokenStore() to get the singleton instance.
|
|
30
|
-
* The constructor is still exported for test isolation.
|
|
31
14
|
*/ export class TokenStore {
|
|
32
15
|
config;
|
|
33
16
|
constructor(options){
|
|
34
|
-
if (process.env.AWS_EXECUTION_ENV) {
|
|
35
|
-
throw new Error('TokenStore cannot be used in AWS Lambda environments');
|
|
36
|
-
}
|
|
37
17
|
this.config = new Conf({
|
|
38
18
|
configName: options?.configName || 'payloadcms-figma',
|
|
39
19
|
projectName: 'payloadcms-figma',
|
|
@@ -1 +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 * Module-level singleton instance for production use\n */\nlet defaultInstance: null | TokenStore = null\n\n/**\n * Get the singleton TokenStore instance\n * Use this for all production code to ensure a single shared instance\n *\n * @returns The singleton TokenStore instance\n */\nexport function getTokenStore(): TokenStore {\n if (!defaultInstance) {\n defaultInstance = new TokenStore()\n }\n return defaultInstance\n}\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 *\n * For production code, use getTokenStore() to get the singleton instance.\n * The constructor is still exported for test isolation.\n */\nexport class TokenStore {\n private config: Conf<{ projectTokens: Record<string, ProjectToken>; tokens: FigmaTokens }>\n\n constructor(options?: TokenStoreConfig) {\n if (process.env.AWS_EXECUTION_ENV) {\n throw new Error('TokenStore cannot be used in AWS Lambda environments')\n }\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","defaultInstance","getTokenStore","TokenStore","config","options","process","env","AWS_EXECUTION_ENV","Error","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","getStoragePath","path","getProjectToken","tenantId","projectTokens","projectToken","isProjectTokenExpired","clearProjectToken","setProjectToken","clearAllProjectTokens","hasValidProjectToken","getProjectTokenClaims","claims","getAllProjectTokenTenantIds","Object","keys"],"mappings":"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"}
|
|
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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"}
|
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,6 @@ class Main {
|
|
|
30
30
|
'--id': String,
|
|
31
31
|
'--name': String,
|
|
32
32
|
'--skip-auth': Boolean,
|
|
33
|
-
'--skip-build': Boolean,
|
|
34
33
|
'--yes': Boolean,
|
|
35
34
|
// Aliases
|
|
36
35
|
'-f': '--force',
|
|
@@ -59,9 +58,7 @@ class Main {
|
|
|
59
58
|
switch(subcommand){
|
|
60
59
|
case 'deploy':
|
|
61
60
|
await deployCommand({
|
|
62
|
-
id: this.args['--id'],
|
|
63
61
|
debug: this.args['--debug'],
|
|
64
|
-
skipBuild: this.args['--skip-build'],
|
|
65
62
|
yes: this.args['--yes']
|
|
66
63
|
});
|
|
67
64
|
break;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import * as p from '@clack/prompts'\nimport arg from 'arg'\nimport pc from 'picocolors'\n\nimport type { CliArgs } from './types.js'\n\nimport { deployCommand } from './commands/deploy.js'\nimport { initCommand } from './commands/init.js'\nimport { listTokensCommand } from './commands/list-tokens.js'\nimport { loginCommand } from './commands/login.js'\nimport { logoutCommand } from './commands/logout.js'\nimport { feedbackOutro, helpMessage } from './utils/messages.js'\n\n/**\n * Entrypoint for bin/cli.js\n */\nexport async function main(): Promise<void> {\n try {\n await new Main().init()\n } catch (e) {\n if (e instanceof Error) {\n // eslint-disable-next-line no-console\n console.error(e.message)\n }\n }\n}\n\nclass Main {\n args: CliArgs\n\n constructor() {\n // @ts-expect-error bad typings\n this.args = arg(\n {\n '--debug': Boolean,\n '--force': Boolean,\n '--help': Boolean,\n '--id': String,\n '--name': String,\n '--skip-auth': Boolean,\n '--
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import * as p from '@clack/prompts'\nimport arg from 'arg'\nimport pc from 'picocolors'\n\nimport type { CliArgs } from './types.js'\n\nimport { deployCommand } from './commands/deploy.js'\nimport { initCommand } from './commands/init.js'\nimport { listTokensCommand } from './commands/list-tokens.js'\nimport { loginCommand } from './commands/login.js'\nimport { logoutCommand } from './commands/logout.js'\nimport { feedbackOutro, helpMessage } from './utils/messages.js'\n\n/**\n * Entrypoint for bin/cli.js\n */\nexport async function main(): Promise<void> {\n try {\n await new Main().init()\n } catch (e) {\n if (e instanceof Error) {\n // eslint-disable-next-line no-console\n console.error(e.message)\n }\n }\n}\n\nclass Main {\n args: CliArgs\n\n constructor() {\n // @ts-expect-error bad typings\n this.args = arg(\n {\n '--debug': Boolean,\n '--force': Boolean,\n '--help': Boolean,\n '--id': String,\n '--name': String,\n '--skip-auth': Boolean,\n '--yes': Boolean,\n\n // Aliases\n '-f': '--force',\n '-h': '--help',\n '-y': '--yes',\n },\n { permissive: true },\n )\n\n // Set debug flag globally\n if (this.args['--debug']) {\n process.env.DEBUG = 'true'\n }\n }\n\n async init(): Promise<void> {\n try {\n if (this.args['--help']) {\n helpMessage()\n process.exit(0)\n }\n\n // Get the subcommand (first non-flag argument)\n const subcommand = this.args._[0]\n\n // eslint-disable-next-line no-console\n console.log('\\n')\n p.intro(pc.bgCyan(pc.black(' @payloadcms/figma ')))\n\n // Route to appropriate command handler\n switch (subcommand) {\n case 'deploy':\n await deployCommand({\n debug: this.args['--debug'],\n yes: this.args['--yes'],\n })\n break\n\n case 'init':\n await initCommand({\n id: this.args['--id'],\n name: this.args['--name'],\n debug: this.args['--debug'],\n force: this.args['--force'],\n skipAuth: this.args['--skip-auth'],\n })\n break\n\n case 'list-tokens':\n listTokensCommand({\n debug: this.args['--debug'],\n })\n break\n\n case 'login':\n await loginCommand()\n break\n\n case 'logout':\n await logoutCommand({\n debug: this.args['--debug'],\n })\n break\n\n case undefined:\n // No subcommand - show default message\n p.note(\"Let's create a new CMS!\")\n p.outro(feedbackOutro())\n break\n\n default:\n p.log.error(pc.red(`Unknown command: ${subcommand}`))\n p.note('Use --help to see available commands', 'Tip')\n process.exit(1)\n }\n\n process.exit(0)\n } catch (err: unknown) {\n // eslint-disable-next-line no-console\n console.error(err instanceof Error ? err.message : 'An error occurred')\n process.exit(1)\n }\n }\n}\n"],"names":["p","arg","pc","deployCommand","initCommand","listTokensCommand","loginCommand","logoutCommand","feedbackOutro","helpMessage","main","Main","init","e","Error","console","error","message","args","Boolean","String","permissive","process","env","DEBUG","exit","subcommand","_","log","intro","bgCyan","black","debug","yes","id","name","force","skipAuth","undefined","note","outro","red","err"],"mappings":"AAAA,YAAYA,OAAO,iBAAgB;AACnC,OAAOC,SAAS,MAAK;AACrB,OAAOC,QAAQ,aAAY;AAI3B,SAASC,aAAa,QAAQ,uBAAsB;AACpD,SAASC,WAAW,QAAQ,qBAAoB;AAChD,SAASC,iBAAiB,QAAQ,4BAA2B;AAC7D,SAASC,YAAY,QAAQ,sBAAqB;AAClD,SAASC,aAAa,QAAQ,uBAAsB;AACpD,SAASC,aAAa,EAAEC,WAAW,QAAQ,sBAAqB;AAEhE;;CAEC,GACD,OAAO,eAAeC;IACpB,IAAI;QACF,MAAM,IAAIC,OAAOC,IAAI;IACvB,EAAE,OAAOC,GAAG;QACV,IAAIA,aAAaC,OAAO;YACtB,sCAAsC;YACtCC,QAAQC,KAAK,CAACH,EAAEI,OAAO;QACzB;IACF;AACF;AAEA,MAAMN;IACJO,KAAa;IAEb,aAAc;QACZ,+BAA+B;QAC/B,IAAI,CAACA,IAAI,GAAGjB,IACV;YACE,WAAWkB;YACX,WAAWA;YACX,UAAUA;YACV,QAAQC;YACR,UAAUA;YACV,eAAeD;YACf,SAASA;YAET,UAAU;YACV,MAAM;YACN,MAAM;YACN,MAAM;QACR,GACA;YAAEE,YAAY;QAAK;QAGrB,0BAA0B;QAC1B,IAAI,IAAI,CAACH,IAAI,CAAC,UAAU,EAAE;YACxBI,QAAQC,GAAG,CAACC,KAAK,GAAG;QACtB;IACF;IAEA,MAAMZ,OAAsB;QAC1B,IAAI;YACF,IAAI,IAAI,CAACM,IAAI,CAAC,SAAS,EAAE;gBACvBT;gBACAa,QAAQG,IAAI,CAAC;YACf;YAEA,+CAA+C;YAC/C,MAAMC,aAAa,IAAI,CAACR,IAAI,CAACS,CAAC,CAAC,EAAE;YAEjC,sCAAsC;YACtCZ,QAAQa,GAAG,CAAC;YACZ5B,EAAE6B,KAAK,CAAC3B,GAAG4B,MAAM,CAAC5B,GAAG6B,KAAK,CAAC;YAE3B,uCAAuC;YACvC,OAAQL;gBACN,KAAK;oBACH,MAAMvB,cAAc;wBAClB6B,OAAO,IAAI,CAACd,IAAI,CAAC,UAAU;wBAC3Be,KAAK,IAAI,CAACf,IAAI,CAAC,QAAQ;oBACzB;oBACA;gBAEF,KAAK;oBACH,MAAMd,YAAY;wBAChB8B,IAAI,IAAI,CAAChB,IAAI,CAAC,OAAO;wBACrBiB,MAAM,IAAI,CAACjB,IAAI,CAAC,SAAS;wBACzBc,OAAO,IAAI,CAACd,IAAI,CAAC,UAAU;wBAC3BkB,OAAO,IAAI,CAAClB,IAAI,CAAC,UAAU;wBAC3BmB,UAAU,IAAI,CAACnB,IAAI,CAAC,cAAc;oBACpC;oBACA;gBAEF,KAAK;oBACHb,kBAAkB;wBAChB2B,OAAO,IAAI,CAACd,IAAI,CAAC,UAAU;oBAC7B;oBACA;gBAEF,KAAK;oBACH,MAAMZ;oBACN;gBAEF,KAAK;oBACH,MAAMC,cAAc;wBAClByB,OAAO,IAAI,CAACd,IAAI,CAAC,UAAU;oBAC7B;oBACA;gBAEF,KAAKoB;oBACH,uCAAuC;oBACvCtC,EAAEuC,IAAI,CAAC;oBACPvC,EAAEwC,KAAK,CAAChC;oBACR;gBAEF;oBACER,EAAE4B,GAAG,CAACZ,KAAK,CAACd,GAAGuC,GAAG,CAAC,CAAC,iBAAiB,EAAEf,YAAY;oBACnD1B,EAAEuC,IAAI,CAAC,wCAAwC;oBAC/CjB,QAAQG,IAAI,CAAC;YACjB;YAEAH,QAAQG,IAAI,CAAC;QACf,EAAE,OAAOiB,KAAc;YACrB,sCAAsC;YACtC3B,QAAQC,KAAK,CAAC0B,eAAe5B,QAAQ4B,IAAIzB,OAAO,GAAG;YACnDK,QAAQG,IAAI,CAAC;QACf;IACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAoBA;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,wBAAwB;IACxB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,gCAAgC;IAChC,GAAG,CAAC,EAAE,OAAO,CAAA;CACd;AA6BD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyThF"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -2,14 +2,17 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import {
|
|
5
|
+
import { Project } from 'ts-morph';
|
|
6
|
+
import { createDeployment, getTenantDetails, performDeployment } from '../api/control-plane.js';
|
|
6
7
|
import { getValidAccessToken } from '../auth/oauth-flow.js';
|
|
7
|
-
import {
|
|
8
|
+
import { getValidProjectToken } from '../auth/project-token.js';
|
|
9
|
+
import { TokenStore } from '../auth/token-store.js';
|
|
8
10
|
import { collectStaticAssets, createLambdaZip, getFileSize } from '../utils/asset-collection.js';
|
|
9
11
|
import { detectBuild, getBuildCommand } from '../utils/build-detection.js';
|
|
10
|
-
import { getEnvVar } from '../utils/env-management.js';
|
|
11
12
|
import * as log from '../utils/log.js';
|
|
12
13
|
import { getPackageManager } from '../utils/package-manager.js';
|
|
14
|
+
import { readFigmaConfig } from '../utils/payload-config-ast.js';
|
|
15
|
+
import { findPayloadConfig } from '../utils/payload-config-finder.js';
|
|
13
16
|
import { isInProjectDirectory } from '../utils/project.js';
|
|
14
17
|
import { uploadLambdaZip, uploadStaticAssets } from '../utils/s3-upload.js';
|
|
15
18
|
import { loginCommand } from './login.js';
|
|
@@ -48,14 +51,6 @@ import { loginCommand } from './login.js';
|
|
|
48
51
|
*/ export async function deployCommand(options) {
|
|
49
52
|
const projectPath = process.cwd();
|
|
50
53
|
const spinner = p.spinner();
|
|
51
|
-
let tenantId = options.id;
|
|
52
|
-
if (!tenantId) {
|
|
53
|
-
tenantId = await getEnvVar(projectPath, 'FIGMA_TENANT_ID') ?? undefined;
|
|
54
|
-
}
|
|
55
|
-
if (!tenantId) {
|
|
56
|
-
p.log.error(pc.red('✗ Tenant ID not found. Run `@payloadcms/figma init` first.'));
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
54
|
try {
|
|
60
55
|
// ===== PRE-FLIGHT CHECKS =====
|
|
61
56
|
// Check if in project directory
|
|
@@ -64,8 +59,27 @@ import { loginCommand } from './login.js';
|
|
|
64
59
|
p.note('Create a project or connect an existing one to Figma: npx @payloadcms/figma init', 'Tip');
|
|
65
60
|
process.exit(1);
|
|
66
61
|
}
|
|
62
|
+
// Find and read payload config
|
|
63
|
+
const configPath = await findPayloadConfig(projectPath);
|
|
64
|
+
if (!configPath) {
|
|
65
|
+
p.log.error(pc.red('✗ payload.config.ts not found'));
|
|
66
|
+
p.note('Run npx @payloadcms/figma init to configure your project.', 'Tip');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
// Parse config with ts-morph
|
|
70
|
+
const project = new Project({
|
|
71
|
+
skipAddingFilesFromTsConfig: true
|
|
72
|
+
});
|
|
73
|
+
const sourceFile = project.addSourceFileAtPath(configPath);
|
|
74
|
+
// Read figma configuration from buildConfig
|
|
75
|
+
const config = readFigmaConfig(sourceFile);
|
|
76
|
+
if (!config || !config.tenantId) {
|
|
77
|
+
p.log.error(pc.red('✗ No figma configuration found in payload.config.ts'));
|
|
78
|
+
p.note('Run npx @payloadcms/figma init to configure your project.', 'Tip');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
67
81
|
// Check authentication
|
|
68
|
-
const tokenStore =
|
|
82
|
+
const tokenStore = new TokenStore();
|
|
69
83
|
let accessToken = await getValidAccessToken(tokenStore);
|
|
70
84
|
if (!accessToken) {
|
|
71
85
|
p.log.message('Please log in to continue');
|
|
@@ -76,33 +90,40 @@ import { loginCommand } from './login.js';
|
|
|
76
90
|
process.exit(1);
|
|
77
91
|
}
|
|
78
92
|
}
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
spinner.stop(pc.
|
|
91
|
-
|
|
92
|
-
`${pc.cyan('Build ID:')} ${buildInfo.buildId}`,
|
|
93
|
-
`${pc.cyan('Date:')} ${formatDate(buildInfo.timestamp)}`
|
|
94
|
-
].join('\n'), 'Build Details');
|
|
95
|
-
} else {
|
|
96
|
-
// Always build (no prompt)
|
|
93
|
+
// Get project token
|
|
94
|
+
try {
|
|
95
|
+
await getValidProjectToken(tokenStore, config.tenantId);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
p.log.error(pc.yellow('⚠ Project token invalid'));
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
// ===== BUILD VALIDATION =====
|
|
101
|
+
spinner.start('Checking for build...');
|
|
102
|
+
let buildInfo = await detectBuild(projectPath);
|
|
103
|
+
if (!buildInfo) {
|
|
104
|
+
spinner.stop(pc.yellow('⚠ No build found for this project.'));
|
|
105
|
+
// Try to find build command
|
|
97
106
|
const buildCmd = await getBuildCommand(projectPath);
|
|
98
107
|
if (!buildCmd) {
|
|
99
108
|
p.log.error('No build script found in package.json');
|
|
100
109
|
p.note('Add a build script to package.json: "build": "next build"', 'Tip');
|
|
101
110
|
process.exit(1);
|
|
102
111
|
}
|
|
112
|
+
// Prompt to run build
|
|
113
|
+
if (!options.yes) {
|
|
114
|
+
const shouldBuild = await p.confirm({
|
|
115
|
+
initialValue: true,
|
|
116
|
+
message: `Build command found in package.json. Run it?`
|
|
117
|
+
});
|
|
118
|
+
if (p.isCancel(shouldBuild) || !shouldBuild) {
|
|
119
|
+
p.log.message('Cancelling deployment...');
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Run build
|
|
103
124
|
const packageManager = await getPackageManager(projectPath);
|
|
104
125
|
const buildCommand = `${packageManager} run build`;
|
|
105
|
-
|
|
126
|
+
spinner.stop(`Running ${buildCommand}...\n`);
|
|
106
127
|
try {
|
|
107
128
|
// Stream build output to user with formatting
|
|
108
129
|
const buildProcess = spawn(packageManager, [
|
|
@@ -122,7 +143,7 @@ import { loginCommand } from './login.js';
|
|
|
122
143
|
lines.forEach((line)=>{
|
|
123
144
|
if (line.trim()) {
|
|
124
145
|
// eslint-disable-next-line no-console
|
|
125
|
-
console.log(pc.dim(` ${line}`));
|
|
146
|
+
console.log(pc.dim(` ${line}`)); // Standard output
|
|
126
147
|
}
|
|
127
148
|
});
|
|
128
149
|
});
|
|
@@ -131,7 +152,7 @@ import { loginCommand } from './login.js';
|
|
|
131
152
|
lines.forEach((line)=>{
|
|
132
153
|
if (line.trim()) {
|
|
133
154
|
// eslint-disable-next-line no-console
|
|
134
|
-
console.log(pc.dim(` ${line}`));
|
|
155
|
+
console.log(pc.dim(` ${line}`)); // Standard error
|
|
135
156
|
}
|
|
136
157
|
});
|
|
137
158
|
});
|
|
@@ -149,13 +170,19 @@ import { loginCommand } from './login.js';
|
|
|
149
170
|
}
|
|
150
171
|
process.exit(1);
|
|
151
172
|
}
|
|
152
|
-
//
|
|
173
|
+
// Re-check for build
|
|
153
174
|
buildInfo = await detectBuild(projectPath);
|
|
154
175
|
if (!buildInfo) {
|
|
155
176
|
p.log.error(pc.red('✗ Build completed but standalone build not found'));
|
|
156
177
|
p.note('Ensure next.config.js has output: "standalone"', 'Check Lambda Configuration');
|
|
157
178
|
process.exit(1);
|
|
158
179
|
}
|
|
180
|
+
} else {
|
|
181
|
+
spinner.stop(pc.green('✓ Build found'));
|
|
182
|
+
p.note([
|
|
183
|
+
`${pc.cyan('Build ID:')} ${buildInfo.buildId}`,
|
|
184
|
+
`${pc.cyan('Date:')} ${formatDate(buildInfo.timestamp)}`
|
|
185
|
+
].join('\n'), 'Build Details');
|
|
159
186
|
}
|
|
160
187
|
// ===== DEPLOYMENT PREPARATION =====
|
|
161
188
|
// Create Lambda zip
|
|
@@ -183,6 +210,14 @@ import { loginCommand } from './login.js';
|
|
|
183
210
|
`${pc.cyan('Code size:')} ${formatBytes(zipSize)}`,
|
|
184
211
|
`${pc.cyan('Static assets:')} ${staticAssets.length} files`
|
|
185
212
|
].join('\n'), 'Details');
|
|
213
|
+
const shouldDeploy = await p.confirm({
|
|
214
|
+
initialValue: true,
|
|
215
|
+
message: 'Deploy this build to Figma?'
|
|
216
|
+
});
|
|
217
|
+
if (p.isCancel(shouldDeploy) || !shouldDeploy) {
|
|
218
|
+
p.log.message('Cancelling deployment...');
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
186
221
|
}
|
|
187
222
|
// ===== DEPLOYMENT EXECUTION =====
|
|
188
223
|
// Step 1: Create deployment
|
|
@@ -191,7 +226,7 @@ import { loginCommand } from './login.js';
|
|
|
191
226
|
let codeUploadUrl;
|
|
192
227
|
let staticAssetUploadUrls;
|
|
193
228
|
try {
|
|
194
|
-
const createResponse = await createDeployment(accessToken, tenantId, {
|
|
229
|
+
const createResponse = await createDeployment(accessToken, config.tenantId, {
|
|
195
230
|
staticAssets
|
|
196
231
|
});
|
|
197
232
|
deploymentId = createResponse.deploymentId;
|
|
@@ -231,12 +266,10 @@ import { loginCommand } from './login.js';
|
|
|
231
266
|
}
|
|
232
267
|
// Step 4: Perform deployment
|
|
233
268
|
spinner.start('Performing deployment...');
|
|
234
|
-
let tenantDomains = [];
|
|
235
269
|
try {
|
|
236
|
-
|
|
270
|
+
await performDeployment(accessToken, config.tenantId, {
|
|
237
271
|
deploymentId
|
|
238
272
|
});
|
|
239
|
-
tenantDomains = result.tenantDomains ?? [];
|
|
240
273
|
spinner.stop(pc.green('✓ Deployment initiated'));
|
|
241
274
|
} catch (error) {
|
|
242
275
|
spinner.stop(pc.red('✗ Failed to perform deployment'));
|
|
@@ -246,10 +279,17 @@ import { loginCommand } from './login.js';
|
|
|
246
279
|
// Future: Poll deployment status and show progress
|
|
247
280
|
// API endpoint: GET /v1/tenant/{tenant_id}/deploy/{deployment_id}/status
|
|
248
281
|
// Success output
|
|
249
|
-
|
|
250
|
-
|
|
282
|
+
p.log.message('This usually takes 2-3 minutes to complete.');
|
|
283
|
+
// Get tenant details for URL
|
|
284
|
+
try {
|
|
285
|
+
const tenant = await getTenantDetails(accessToken, config.tenantId);
|
|
286
|
+
if (tenant.domain) {
|
|
287
|
+
p.log.message(`View: ${pc.cyan(`https://${tenant.domain}/`)}`);
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
// Non-critical - just skip URL display
|
|
251
291
|
}
|
|
252
|
-
p.
|
|
292
|
+
p.note(`Manage your deployment at:\n${pc.cyan(`https://cms.figma.com/manage/tenants/${config.tenantId}`)}`, 'Next Steps');
|
|
253
293
|
} catch (error) {
|
|
254
294
|
spinner.stop(pc.red('✗ Failed'));
|
|
255
295
|
const message = error instanceof Error ? error.message : 'Unknown error';
|