@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.
- package/dist/auth/crypto-utils.d.ts +11 -0
- package/dist/auth/crypto-utils.js +51 -0
- package/dist/auth/oauth-flow.js +4 -3
- package/dist/auth/project-token.d.ts +32 -16
- package/dist/auth/project-token.js +165 -64
- package/dist/auth/token-store-migration.d.ts +58 -0
- package/dist/auth/token-store-migration.js +156 -0
- package/dist/auth/token-store.d.ts +51 -128
- package/dist/auth/token-store.js +326 -187
- package/dist/auth/types.d.ts +12 -1
- package/dist/cli.js +15 -0
- package/dist/commands/bootstrap.d.ts +18 -0
- package/dist/commands/bootstrap.js +90 -0
- package/dist/commands/debug.js +16 -12
- package/dist/commands/dump-tokens.d.ts +12 -0
- package/dist/commands/dump-tokens.js +69 -0
- package/dist/commands/init.js +39 -5
- package/dist/commands/list-tokens.js +2 -2
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +23 -4
- package/dist/db-content-api/index.js +27 -73
- package/dist/db-content-api/utilities/auth.js +4 -1
- package/dist/oauth/defaults.d.ts +3 -1
- package/dist/oauth/defaults.js +6 -5
- package/dist/oauth/endpoints/getLoginEndpoint.js +1 -1
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/types.d.ts +10 -0
- package/dist/plugin/auth-preflight.d.ts +8 -0
- package/dist/plugin/auth-preflight.js +20 -0
- package/dist/plugin/bootstrap-preflight.d.ts +23 -0
- package/dist/plugin/bootstrap-preflight.js +45 -0
- package/dist/plugin/build-config.js +135 -58
- package/dist/plugin/dev-cookie-names.d.ts +14 -0
- package/dist/plugin/dev-cookie-names.js +19 -0
- package/dist/storage-content-api/client.js +4 -1
- package/dist/utils/messages.js +7 -0
- package/dist/utils/payload-config-modifier.js +96 -107
- package/dist/utils/payload-package-check.d.ts +21 -1
- package/dist/utils/payload-package-check.js +66 -26
- package/dist/utils/token-display.d.ts +5 -1
- package/dist/utils/token-display.js +47 -26
- 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
|
package/dist/auth/oauth-flow.js
CHANGED
|
@@ -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.
|
|
151
|
-
const token = tokenStore.
|
|
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.
|
|
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
|
-
*
|
|
20
|
-
* 1.
|
|
21
|
-
* 2.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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 -
|
|
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(
|
|
39
|
+
export declare function getValidProjectToken({ projectInfo, tenantId, tokenStore, }: {
|
|
40
|
+
projectInfo?: ProjectStoreScope;
|
|
41
|
+
tenantId?: string;
|
|
42
|
+
tokenStore: TokenStore;
|
|
43
|
+
}): Promise<null | string>;
|
|
35
44
|
/**
|
|
36
|
-
*
|
|
45
|
+
* Force-refresh a project token, bypassing all cache layers.
|
|
37
46
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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 -
|
|
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(
|
|
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
|
-
*
|
|
49
|
-
* 1.
|
|
50
|
-
* 2.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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 -
|
|
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(
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
return
|
|
99
|
+
const inFlight = inFlightRefreshes.get(cacheKey);
|
|
100
|
+
if (inFlight) {
|
|
101
|
+
return inFlight;
|
|
83
102
|
}
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
} catch (
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
204
|
+
* Force-refresh a project token, bypassing all cache layers.
|
|
121
205
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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 -
|
|
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(
|
|
130
|
-
|
|
131
|
-
tokenStore.
|
|
132
|
-
|
|
133
|
-
|
|
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
|