@payloadcms/figma 0.0.1-alpha.62 → 0.0.1-alpha.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/auth/crypto-utils.d.ts +11 -0
  2. package/dist/auth/crypto-utils.js +51 -0
  3. package/dist/auth/oauth-flow.js +4 -3
  4. package/dist/auth/project-token.d.ts +32 -16
  5. package/dist/auth/project-token.js +165 -64
  6. package/dist/auth/token-store-migration.d.ts +58 -0
  7. package/dist/auth/token-store-migration.js +156 -0
  8. package/dist/auth/token-store.d.ts +51 -128
  9. package/dist/auth/token-store.js +326 -187
  10. package/dist/auth/types.d.ts +12 -1
  11. package/dist/cli.js +15 -0
  12. package/dist/commands/bootstrap.d.ts +18 -0
  13. package/dist/commands/bootstrap.js +90 -0
  14. package/dist/commands/debug.js +16 -12
  15. package/dist/commands/dump-tokens.d.ts +12 -0
  16. package/dist/commands/dump-tokens.js +69 -0
  17. package/dist/commands/init.js +39 -5
  18. package/dist/commands/list-tokens.js +2 -2
  19. package/dist/commands/login.js +1 -1
  20. package/dist/commands/logout.js +23 -4
  21. package/dist/db-content-api/index.js +27 -73
  22. package/dist/db-content-api/utilities/auth.js +4 -1
  23. package/dist/oauth/defaults.d.ts +3 -1
  24. package/dist/oauth/defaults.js +6 -5
  25. package/dist/oauth/endpoints/getLoginEndpoint.js +1 -1
  26. package/dist/oauth/index.js +1 -0
  27. package/dist/oauth/types.d.ts +10 -0
  28. package/dist/plugin/auth-preflight.d.ts +8 -0
  29. package/dist/plugin/auth-preflight.js +20 -0
  30. package/dist/plugin/bootstrap-preflight.d.ts +23 -0
  31. package/dist/plugin/bootstrap-preflight.js +45 -0
  32. package/dist/plugin/build-config.js +135 -58
  33. package/dist/plugin/dev-cookie-names.d.ts +14 -0
  34. package/dist/plugin/dev-cookie-names.js +19 -0
  35. package/dist/storage-content-api/client.js +4 -1
  36. package/dist/utils/messages.js +7 -0
  37. package/dist/utils/payload-config-modifier.js +96 -107
  38. package/dist/utils/payload-package-check.d.ts +21 -1
  39. package/dist/utils/payload-package-check.js +66 -26
  40. package/dist/utils/token-display.d.ts +5 -1
  41. package/dist/utils/token-display.js +47 -26
  42. package/package.json +2 -1
@@ -36,4 +36,15 @@ export declare function deriveEncryptionKey(): string;
36
36
  * without exposing the actual key.
37
37
  */
38
38
  export declare function getEncryptionKeyHash(): string;
39
+ /**
40
+ * Write decrypted versions of all encrypted store files for debugging.
41
+ *
42
+ * For each .json file in the store directory, creates a neighboring .decrypted.json
43
+ * file containing the decrypted contents. Useful for debugging token storage issues.
44
+ *
45
+ * @param rootDir - Root directory of the token store (e.g., ~/Library/Preferences/payloadcms-figma-nodejs)
46
+ * @param encryptionKey - The encryption key used by Conf
47
+ * @returns Array of paths to decrypted files created
48
+ */
49
+ export declare function exportDecryptedStoreFiles(rootDir: string, encryptionKey: string): string[];
39
50
  //# sourceMappingURL=crypto-utils.d.ts.map
@@ -1,6 +1,8 @@
1
1
  import Conf from 'conf';
2
2
  import crypto from 'crypto';
3
+ import fsSync from 'node:fs';
3
4
  import os from 'os';
5
+ import path from 'path';
4
6
  /**
5
7
  * Get or generate a random salt for key derivation
6
8
  *
@@ -94,5 +96,54 @@ import os from 'os';
94
96
  const key = deriveEncryptionKey();
95
97
  return crypto.createHash('sha256').update(key).digest('hex').substring(0, 8);
96
98
  }
99
+ /**
100
+ * Write decrypted versions of all encrypted store files for debugging.
101
+ *
102
+ * For each .json file in the store directory, creates a neighboring .decrypted.json
103
+ * file containing the decrypted contents. Useful for debugging token storage issues.
104
+ *
105
+ * @param rootDir - Root directory of the token store (e.g., ~/Library/Preferences/payloadcms-figma-nodejs)
106
+ * @param encryptionKey - The encryption key used by Conf
107
+ * @returns Array of paths to decrypted files created
108
+ */ export function exportDecryptedStoreFiles(rootDir, encryptionKey) {
109
+ const decryptedFiles = [];
110
+ function processDirectory(dir) {
111
+ if (!fsSync.existsSync(dir)) {
112
+ return;
113
+ }
114
+ const entries = fsSync.readdirSync(dir, {
115
+ withFileTypes: true
116
+ });
117
+ for (const entry of entries){
118
+ const fullPath = path.join(dir, entry.name);
119
+ if (entry.isDirectory()) {
120
+ processDirectory(fullPath);
121
+ } else if (entry.name.endsWith('.json') && !entry.name.endsWith('.decrypted.json') && !entry.name.includes('.corrupt-')) {
122
+ try {
123
+ // Extract configName from filename (remove .json extension)
124
+ const configName = entry.name.replace(/\.json$/, '');
125
+ // Create a Conf instance to read/decrypt the file
126
+ const config = new Conf({
127
+ clearInvalidConfig: false,
128
+ configName,
129
+ cwd: dir,
130
+ encryptionKey,
131
+ projectName: 'payloadcms-figma'
132
+ });
133
+ // Read all data from the store (Conf decrypts it)
134
+ const data = config.store;
135
+ // Write decrypted content to neighboring file
136
+ const decryptedPath = fullPath.replace(/\.json$/, '.decrypted.json');
137
+ fsSync.writeFileSync(decryptedPath, JSON.stringify(data, null, 2));
138
+ decryptedFiles.push(decryptedPath);
139
+ } catch {
140
+ // Skip files that can't be decrypted (e.g., crypto config, already unencrypted)
141
+ }
142
+ }
143
+ }
144
+ }
145
+ processDirectory(rootDir);
146
+ return decryptedFiles;
147
+ }
97
148
 
98
149
  //# sourceMappingURL=crypto-utils.js.map
@@ -147,15 +147,15 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
147
147
  * @throws OAuthFlowError if refresh fails
148
148
  */ export async function getValidOAuthAccessToken(tokenStore) {
149
149
  // Check if we have valid tokens
150
- if (tokenStore.hasValidTokens()) {
151
- const token = tokenStore.getAccessToken();
150
+ if (tokenStore.hasValidOauthToken()) {
151
+ const token = tokenStore.getCurrentOauthToken();
152
152
  return token ? {
153
153
  type: 'oauth',
154
154
  token
155
155
  } : null;
156
156
  }
157
157
  // Check if we have a refresh token
158
- const refreshToken = tokenStore.getRefreshToken();
158
+ const refreshToken = tokenStore.getCurrentRefreshToken();
159
159
  if (!refreshToken) {
160
160
  return null;
161
161
  }
@@ -195,6 +195,7 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
195
195
  */ export async function getValidCredential(tokenStore) {
196
196
  const envFigmaAccessToken = process.env.FIGMA_ACCESS_TOKEN;
197
197
  if (envFigmaAccessToken) {
198
+ log.debug('Using FIGMA_ACCESS_TOKEN env var override; skipping stored OAuth tokens');
198
199
  return {
199
200
  type: 'plan_access_token',
200
201
  token: envFigmaAccessToken
@@ -6,6 +6,7 @@
6
6
  * and have a shorter lifespan (15-30 minutes) than OAuth tokens.
7
7
  */
8
8
  import type { TokenStore } from './token-store.js';
9
+ import type { ProjectStoreScope } from './types.js';
9
10
  /**
10
11
  * Error during project token operations
11
12
  */
@@ -13,35 +14,50 @@ export declare class ProjectTokenError extends Error {
13
14
  cause?: Error | undefined;
14
15
  constructor(message: string, cause?: Error | undefined);
15
16
  }
17
+ /** Test-only: clears the in-memory token cache and any in-flight refresh promises. */
18
+ export declare function __resetProjectTokenCacheForTests(): void;
16
19
  /**
17
- * Get a valid project token for a tenant, refreshing if necessary
20
+ * Get a valid project token for a tenant, refreshing if necessary.
18
21
  *
19
- * This function:
20
- * 1. Checks if a stored project token exists and is valid (not expired)
21
- * 2. If expired or missing, gets a valid user access token
22
- * 3. Calls the Figma API to generate a new project token
23
- * 4. Stores the new project token in encrypted storage
24
- * 5. Returns the JWT string
22
+ * Lookup order:
23
+ * 1. Module-level in-memory cache (no I/O on hits).
24
+ * 2. In-flight refresh map concurrent callers with no cached entry share a single
25
+ * refresh promise, so the project-token endpoint is only hit once per cache key.
26
+ * 3. Persisted (`conf`-backed) token store picks up tokens minted by sibling processes.
27
+ * 4. Figma API — fetch + JWT validation, then write to both the persisted store and
28
+ * the in-memory cache.
25
29
  *
26
30
  * The project token is used by local Payload instances to authenticate
27
31
  * requests to the Content API without requiring a round-trip to Sinatra.
28
32
  *
29
- * @param tokenStore - Token store for persisting tokens
30
- * @param tenantId - The tenant/CMS ID to get a token for
33
+ * @param params.tokenStore - Token store for persisting tokens
34
+ * @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
35
+ * @param params.projectInfo - Optional project scope (defaults to env vars)
31
36
  * @returns Promise resolving to JWT token string or null if failed
32
37
  * @throws {ProjectTokenError} If token generation fails
33
38
  */
34
- export declare function getValidProjectToken(tokenStore: TokenStore, tenantId: string): Promise<null | string>;
39
+ export declare function getValidProjectToken({ projectInfo, tenantId, tokenStore, }: {
40
+ projectInfo?: ProjectStoreScope;
41
+ tenantId?: string;
42
+ tokenStore: TokenStore;
43
+ }): Promise<null | string>;
35
44
  /**
36
- * Refresh a project token for a tenant
45
+ * Force-refresh a project token, bypassing all cache layers.
37
46
  *
38
- * Forces a refresh of the project token even if the current one is still valid.
39
- * Useful for testing or when you want to ensure you have the freshest token.
47
+ * Invalidates the in-memory cache entry, the in-flight refresh promise (so a
48
+ * concurrently-running refresh that may already be returning a soon-to-be-stale
49
+ * token is dropped from the dedup map), and the persisted token store. Then
50
+ * delegates to {@link getValidProjectToken} to mint a fresh token.
40
51
  *
41
- * @param tokenStore - Token store for persisting tokens
42
- * @param tenantId - The tenant/CMS ID to refresh token for
52
+ * @param params.tokenStore - Token store for persisting tokens
53
+ * @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
54
+ * @param params.projectInfo - Optional project scope (defaults to env vars)
43
55
  * @returns Promise resolving to JWT token string or null if failed
44
56
  * @throws {ProjectTokenError} If token refresh fails
45
57
  */
46
- export declare function refreshProjectToken(tokenStore: TokenStore, tenantId: string): Promise<null | string>;
58
+ export declare function refreshProjectToken(params: {
59
+ projectInfo?: ProjectStoreScope;
60
+ tenantId?: string;
61
+ tokenStore: TokenStore;
62
+ }): Promise<null | string>;
47
63
  //# sourceMappingURL=project-token.d.ts.map
@@ -5,6 +5,7 @@
5
5
  * to the Content API. Project tokens are scoped to a specific tenant/CMS
6
6
  * and have a shorter lifespan (15-30 minutes) than OAuth tokens.
7
7
  */ import { getProjectToken as fetchProjectToken } from '../api/figma-api.js';
8
+ import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
8
9
  import { getInfraEnvironment } from '../constants.js';
9
10
  import * as log from '../utils/log.js';
10
11
  import { JWTValidationError, validateProjectToken } from './jwt-validator.js';
@@ -18,6 +19,21 @@ import { getValidCredential } from './oauth-flow.js';
18
19
  this.name = 'ProjectTokenError';
19
20
  }
20
21
  }
22
+ const projectTokenCache = new Map();
23
+ const inFlightRefreshes = new Map();
24
+ function buildCacheKey(params) {
25
+ const projectId = params.projectInfo?.projectId ?? '';
26
+ const environmentName = params.projectInfo?.environmentName ?? '';
27
+ return `${params.tenantId}::${projectId}::${environmentName}`;
28
+ }
29
+ function isCachedTokenValid(entry) {
30
+ const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
31
+ return Date.now() < entry.expiresAt - bufferMs;
32
+ }
33
+ /** Test-only: clears the in-memory token cache and any in-flight refresh promises. */ export function __resetProjectTokenCacheForTests() {
34
+ projectTokenCache.clear();
35
+ inFlightRefreshes.clear();
36
+ }
21
37
  /**
22
38
  * Parse JWT claims from token without validation
23
39
  * Used for mock tokens where signature validation is not needed
@@ -43,94 +59,179 @@ import { getValidCredential } from './oauth-flow.js';
43
59
  }
44
60
  }
45
61
  /**
46
- * Get a valid project token for a tenant, refreshing if necessary
62
+ * Get a valid project token for a tenant, refreshing if necessary.
47
63
  *
48
- * This function:
49
- * 1. Checks if a stored project token exists and is valid (not expired)
50
- * 2. If expired or missing, gets a valid user access token
51
- * 3. Calls the Figma API to generate a new project token
52
- * 4. Stores the new project token in encrypted storage
53
- * 5. Returns the JWT string
64
+ * Lookup order:
65
+ * 1. Module-level in-memory cache (no I/O on hits).
66
+ * 2. In-flight refresh map concurrent callers with no cached entry share a single
67
+ * refresh promise, so the project-token endpoint is only hit once per cache key.
68
+ * 3. Persisted (`conf`-backed) token store picks up tokens minted by sibling processes.
69
+ * 4. Figma API — fetch + JWT validation, then write to both the persisted store and
70
+ * the in-memory cache.
54
71
  *
55
72
  * The project token is used by local Payload instances to authenticate
56
73
  * requests to the Content API without requiring a round-trip to Sinatra.
57
74
  *
58
- * @param tokenStore - Token store for persisting tokens
59
- * @param tenantId - The tenant/CMS ID to get a token for
75
+ * @param params.tokenStore - Token store for persisting tokens
76
+ * @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
77
+ * @param params.projectInfo - Optional project scope (defaults to env vars)
60
78
  * @returns Promise resolving to JWT token string or null if failed
61
79
  * @throws {ProjectTokenError} If token generation fails
62
- */ export async function getValidProjectToken(tokenStore, tenantId) {
80
+ */ export async function getValidProjectToken({ projectInfo, tenantId, tokenStore }) {
63
81
  if (process.env.FIGMA_CONTENT_API_ACCESS_KEY) {
64
82
  log.debug('FIGMA_CONTENT_API_ACCESS_KEY is set; skipping project token retrieval from API');
65
83
  return null;
66
84
  }
67
- // Check for existing valid project token
68
- if (tokenStore.hasValidProjectToken(tenantId)) {
69
- const projectToken = tokenStore.getProjectToken(tenantId);
70
- return projectToken?.token || null;
85
+ const resolvedTenantId = tenantId ?? tokenStore.getTenantId({
86
+ projectInfo
87
+ });
88
+ if (!resolvedTenantId) {
89
+ throw new ProjectTokenError('tenantId was not provided and could not be resolved from bootstrap data');
71
90
  }
72
- // We need to refresh the project token
73
- // An oauth access token is required for the call
74
- let credential;
75
- try {
76
- credential = await getValidCredential(tokenStore);
77
- } catch (error) {
78
- throw new ProjectTokenError('Failed to get valid access token for project token refresh', error instanceof Error ? error : undefined);
91
+ const cacheKey = buildCacheKey({
92
+ projectInfo,
93
+ tenantId: resolvedTenantId
94
+ });
95
+ const cached = projectTokenCache.get(cacheKey);
96
+ if (cached && isCachedTokenValid(cached)) {
97
+ return cached.token;
79
98
  }
80
- if (!credential) {
81
- // No valid user tokens available - user needs to authenticate
82
- return null;
99
+ const inFlight = inFlightRefreshes.get(cacheKey);
100
+ if (inFlight) {
101
+ return inFlight;
83
102
  }
84
- // Fetch a new project token from the Figma API
85
- const projectToken = await fetchProjectToken(credential, tenantId);
86
- // Validate the JWT signature and claims (skip for mocks)
87
- const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE;
88
- const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
89
- let validatedClaims;
90
- if (hasEnvToken || shouldMock) {
91
- // MOCK: Skip JWT validation and parse claims directly
92
- log.debug('Skipping JWT validation for mock/env project token');
93
- validatedClaims = parseJWTClaims(projectToken.token);
94
- } else {
95
- // REAL: Validate JWT signature against JWKS
103
+ // The IIFE references this holder so it can check whether it is still the
104
+ // active refresh before writing back to the caches. Without this guard a
105
+ // stale in-flight refresh could clobber a fresh token written by a later
106
+ // refresh (e.g. one triggered by `refreshProjectToken`).
107
+ const handle = {};
108
+ handle.promise = (async ()=>{
109
+ if (tokenStore.hasValidProjectToken({
110
+ projectInfo
111
+ })) {
112
+ const stored = tokenStore.getProjectToken({
113
+ projectInfo
114
+ });
115
+ if (stored?.token && inFlightRefreshes.get(cacheKey) === handle.promise) {
116
+ projectTokenCache.set(cacheKey, {
117
+ expiresAt: stored.expiresAt,
118
+ token: stored.token
119
+ });
120
+ return stored.token;
121
+ }
122
+ if (stored?.token) {
123
+ return stored.token;
124
+ }
125
+ // hasValidProjectToken said yes but getProjectToken returned no token —
126
+ // store was likely cleared by another process between calls. Fall through
127
+ // to fetch so the caller still gets a token.
128
+ log.warning(`Stored project token vanished between hasValidProjectToken and getProjectToken (cacheKey=${cacheKey}); re-fetching`);
129
+ }
130
+ let credential;
131
+ try {
132
+ credential = await getValidCredential(tokenStore);
133
+ } catch (error) {
134
+ throw new ProjectTokenError('Failed to get valid access token for project token refresh', error instanceof Error ? error : undefined);
135
+ }
136
+ if (!credential) {
137
+ return null;
138
+ }
139
+ let fetchedToken;
96
140
  try {
97
- validatedClaims = await validateProjectToken(projectToken.token, getInfraEnvironment());
98
- } catch (validationError) {
99
- // Handle validation errors
100
- if (validationError instanceof JWTValidationError) {
101
- throw new ProjectTokenError(`Project token validation failed: ${validationError.message}`, validationError);
141
+ fetchedToken = await fetchProjectToken(credential, resolvedTenantId);
142
+ } catch (error) {
143
+ // Let API errors (e.g. FigmaApiError) propagate raw so callers can
144
+ // branch on status codes — but log context for debugging.
145
+ log.debug(`fetchProjectToken failed (cacheKey=${cacheKey}, tenantId=${resolvedTenantId}): ${error instanceof Error ? error.message : 'Unknown error'}`);
146
+ throw error;
147
+ }
148
+ const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE;
149
+ const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
150
+ let validatedClaims;
151
+ if (hasEnvToken || shouldMock) {
152
+ log.debug('Skipping JWT validation for mock/env project token');
153
+ validatedClaims = parseJWTClaims(fetchedToken.token);
154
+ } else {
155
+ try {
156
+ validatedClaims = await validateProjectToken(fetchedToken.token, getInfraEnvironment());
157
+ } catch (validationError) {
158
+ if (validationError instanceof JWTValidationError) {
159
+ throw new ProjectTokenError(`Project token validation failed: ${validationError.message}`, validationError);
160
+ }
161
+ throw validationError;
102
162
  }
103
- throw validationError;
163
+ }
164
+ if (typeof validatedClaims.exp !== 'number' || !Number.isFinite(validatedClaims.exp)) {
165
+ throw new ProjectTokenError('Project token missing or invalid expiration (exp) claim');
166
+ }
167
+ const expiresAt = validatedClaims.exp * 1000;
168
+ if (inFlightRefreshes.get(cacheKey) !== handle.promise) {
169
+ log.debug(`Project token refresh superseded; not persisting (cacheKey=${cacheKey})`);
170
+ return fetchedToken.token;
171
+ }
172
+ try {
173
+ tokenStore.setProjectToken({
174
+ projectInfo,
175
+ token: {
176
+ claims: validatedClaims,
177
+ expiresAt,
178
+ tenantId: resolvedTenantId,
179
+ token: fetchedToken.token
180
+ }
181
+ });
182
+ } catch (error) {
183
+ // Persistence is best-effort: a failure here (disk full, permissions,
184
+ // corrupt conf entry) shouldn't lose the freshly-minted token. Log and
185
+ // populate the in-memory cache so this process can still use it.
186
+ log.error(`Failed to persist project token to store (cacheKey=${cacheKey}): ${error instanceof Error ? error.message : 'Unknown error'}`);
187
+ }
188
+ projectTokenCache.set(cacheKey, {
189
+ expiresAt,
190
+ token: fetchedToken.token
191
+ });
192
+ return fetchedToken.token;
193
+ })();
194
+ inFlightRefreshes.set(cacheKey, handle.promise);
195
+ try {
196
+ return await handle.promise;
197
+ } finally{
198
+ if (inFlightRefreshes.get(cacheKey) === handle.promise) {
199
+ inFlightRefreshes.delete(cacheKey);
104
200
  }
105
201
  }
106
- // Ensure exp claim is present
107
- if (!validatedClaims.exp) {
108
- throw new ProjectTokenError('Project token missing expiration (exp) claim');
109
- }
110
- // Store the project token with validated expiration and claims
111
- tokenStore.setProjectToken(tenantId, {
112
- claims: validatedClaims,
113
- expiresAt: validatedClaims.exp * 1000,
114
- tenantId,
115
- token: projectToken.token
116
- });
117
- return projectToken.token;
118
202
  }
119
203
  /**
120
- * Refresh a project token for a tenant
204
+ * Force-refresh a project token, bypassing all cache layers.
121
205
  *
122
- * Forces a refresh of the project token even if the current one is still valid.
123
- * Useful for testing or when you want to ensure you have the freshest token.
206
+ * Invalidates the in-memory cache entry, the in-flight refresh promise (so a
207
+ * concurrently-running refresh that may already be returning a soon-to-be-stale
208
+ * token is dropped from the dedup map), and the persisted token store. Then
209
+ * delegates to {@link getValidProjectToken} to mint a fresh token.
124
210
  *
125
- * @param tokenStore - Token store for persisting tokens
126
- * @param tenantId - The tenant/CMS ID to refresh token for
211
+ * @param params.tokenStore - Token store for persisting tokens
212
+ * @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
213
+ * @param params.projectInfo - Optional project scope (defaults to env vars)
127
214
  * @returns Promise resolving to JWT token string or null if failed
128
215
  * @throws {ProjectTokenError} If token refresh fails
129
- */ export async function refreshProjectToken(tokenStore, tenantId) {
130
- // Clear the existing project token to force a refresh
131
- tokenStore.clearProjectToken(tenantId);
132
- // Get a new token (which will fetch from API since we just cleared it)
133
- return getValidProjectToken(tokenStore, tenantId);
216
+ */ export async function refreshProjectToken(params) {
217
+ const { projectInfo, tenantId, tokenStore } = params;
218
+ const resolvedTenantId = tenantId ?? tokenStore.getTenantId({
219
+ projectInfo
220
+ });
221
+ if (resolvedTenantId) {
222
+ const cacheKey = buildCacheKey({
223
+ projectInfo,
224
+ tenantId: resolvedTenantId
225
+ });
226
+ projectTokenCache.delete(cacheKey);
227
+ // Drop any in-flight refresh so getValidProjectToken below cannot return
228
+ // a soon-to-be-stale token via the dedup map.
229
+ inFlightRefreshes.delete(cacheKey);
230
+ }
231
+ tokenStore.clearProjectToken({
232
+ projectInfo
233
+ });
234
+ return getValidProjectToken(params);
134
235
  }
135
236
 
136
237
  //# sourceMappingURL=project-token.js.map
@@ -0,0 +1,58 @@
1
+ import type { BootstrapData, FigmaTokens, ProjectToken } from './types.js';
2
+ export type LegacyStoreSchema = {
3
+ bootstrapData?: Record<string, BootstrapData>;
4
+ projectTokens?: Record<string, ProjectToken>;
5
+ tokens?: FigmaTokens;
6
+ };
7
+ export type MigrationCheckParams = {
8
+ checkFileExists?: (filePath: string) => boolean;
9
+ legacyFilePath: string;
10
+ newOAuthFilePath: string;
11
+ };
12
+ export type MigrateOAuthParams = {
13
+ encryptionKey: string;
14
+ legacyConfigName: string;
15
+ legacyCwd?: string;
16
+ newOAuthConfigName: string;
17
+ newOAuthCwd: string;
18
+ projectName: string;
19
+ };
20
+ /**
21
+ * Check if migration is needed.
22
+ * Returns true only when legacy file exists AND new oauth file does not.
23
+ */
24
+ export declare function needsMigration(params: MigrationCheckParams): boolean;
25
+ /**
26
+ * Migrate OAuth tokens from legacy store to new oauth store.
27
+ * Reads from legacy location, writes to new location with root-level keys.
28
+ *
29
+ * `clearInvalidConfig` is deliberately OFF so a transient decrypt error does
30
+ * not wipe the legacy file — older CLI versions elsewhere on disk still read
31
+ * it. If we cannot open the file, we log and skip migration.
32
+ */
33
+ export declare function migrateOAuth(params: MigrateOAuthParams): void;
34
+ export type ResolveProjectStorePath = (scope: {
35
+ environmentName: string;
36
+ projectId: string;
37
+ }) => {
38
+ configName: string;
39
+ cwd: string;
40
+ };
41
+ export type MigrateProjectDataParams = {
42
+ encryptionKey: string;
43
+ legacyConfigName: string;
44
+ legacyCwd?: string;
45
+ projectName: string;
46
+ resolveProjectStorePath: ResolveProjectStorePath;
47
+ };
48
+ /**
49
+ * Migrate per-project bootstrap data and project tokens from the legacy
50
+ * combined store into individual per-project stores.
51
+ *
52
+ * Walks legacy `bootstrapData` (keyed by `projectId:environmentName`), writes
53
+ * each entry into its own project store file, and stashes the matching
54
+ * `projectToken` (looked up by `bootstrapData.contentSystemId`) alongside it.
55
+ * Target files that already exist are left alone.
56
+ */
57
+ export declare function migrateProjectData(params: MigrateProjectDataParams): void;
58
+ //# sourceMappingURL=token-store-migration.d.ts.map