@payloadcms/figma 0.0.1-alpha.62 → 0.0.1-alpha.63
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 +3 -3
- package/dist/auth/project-token.d.ts +17 -6
- package/dist/auth/project-token.js +37 -19
- 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 +8 -1
- package/dist/cli.js +6 -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 +7 -3
- 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/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 +73 -12
- 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/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
|
}
|
|
@@ -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
|
*/
|
|
@@ -26,22 +27,32 @@ export declare class ProjectTokenError extends Error {
|
|
|
26
27
|
* The project token is used by local Payload instances to authenticate
|
|
27
28
|
* requests to the Content API without requiring a round-trip to Sinatra.
|
|
28
29
|
*
|
|
29
|
-
* @param tokenStore - Token store for persisting tokens
|
|
30
|
-
* @param tenantId -
|
|
30
|
+
* @param params.tokenStore - Token store for persisting tokens
|
|
31
|
+
* @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
|
|
32
|
+
* @param params.projectInfo - Optional project scope (defaults to env vars)
|
|
31
33
|
* @returns Promise resolving to JWT token string or null if failed
|
|
32
34
|
* @throws {ProjectTokenError} If token generation fails
|
|
33
35
|
*/
|
|
34
|
-
export declare function getValidProjectToken(
|
|
36
|
+
export declare function getValidProjectToken({ projectInfo, tenantId, tokenStore, }: {
|
|
37
|
+
projectInfo?: ProjectStoreScope;
|
|
38
|
+
tenantId?: string;
|
|
39
|
+
tokenStore: TokenStore;
|
|
40
|
+
}): Promise<null | string>;
|
|
35
41
|
/**
|
|
36
42
|
* Refresh a project token for a tenant
|
|
37
43
|
*
|
|
38
44
|
* Forces a refresh of the project token even if the current one is still valid.
|
|
39
45
|
* Useful for testing or when you want to ensure you have the freshest token.
|
|
40
46
|
*
|
|
41
|
-
* @param tokenStore - Token store for persisting tokens
|
|
42
|
-
* @param tenantId -
|
|
47
|
+
* @param params.tokenStore - Token store for persisting tokens
|
|
48
|
+
* @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
|
|
49
|
+
* @param params.projectInfo - Optional project scope (defaults to env vars)
|
|
43
50
|
* @returns Promise resolving to JWT token string or null if failed
|
|
44
51
|
* @throws {ProjectTokenError} If token refresh fails
|
|
45
52
|
*/
|
|
46
|
-
export declare function refreshProjectToken(
|
|
53
|
+
export declare function refreshProjectToken(params: {
|
|
54
|
+
projectInfo?: ProjectStoreScope;
|
|
55
|
+
tenantId?: string;
|
|
56
|
+
tokenStore: TokenStore;
|
|
57
|
+
}): Promise<null | string>;
|
|
47
58
|
//# sourceMappingURL=project-token.d.ts.map
|
|
@@ -55,18 +55,29 @@ import { getValidCredential } from './oauth-flow.js';
|
|
|
55
55
|
* The project token is used by local Payload instances to authenticate
|
|
56
56
|
* requests to the Content API without requiring a round-trip to Sinatra.
|
|
57
57
|
*
|
|
58
|
-
* @param tokenStore - Token store for persisting tokens
|
|
59
|
-
* @param tenantId -
|
|
58
|
+
* @param params.tokenStore - Token store for persisting tokens
|
|
59
|
+
* @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
|
|
60
|
+
* @param params.projectInfo - Optional project scope (defaults to env vars)
|
|
60
61
|
* @returns Promise resolving to JWT token string or null if failed
|
|
61
62
|
* @throws {ProjectTokenError} If token generation fails
|
|
62
|
-
*/ export async function getValidProjectToken(
|
|
63
|
+
*/ export async function getValidProjectToken({ projectInfo, tenantId, tokenStore }) {
|
|
63
64
|
if (process.env.FIGMA_CONTENT_API_ACCESS_KEY) {
|
|
64
65
|
log.debug('FIGMA_CONTENT_API_ACCESS_KEY is set; skipping project token retrieval from API');
|
|
65
66
|
return null;
|
|
66
67
|
}
|
|
68
|
+
const resolvedTenantId = tenantId ?? tokenStore.getTenantId({
|
|
69
|
+
projectInfo
|
|
70
|
+
});
|
|
71
|
+
if (!resolvedTenantId) {
|
|
72
|
+
throw new ProjectTokenError('tenantId was not provided and could not be resolved from bootstrap data');
|
|
73
|
+
}
|
|
67
74
|
// Check for existing valid project token
|
|
68
|
-
if (tokenStore.hasValidProjectToken(
|
|
69
|
-
|
|
75
|
+
if (tokenStore.hasValidProjectToken({
|
|
76
|
+
projectInfo
|
|
77
|
+
})) {
|
|
78
|
+
const projectToken = tokenStore.getProjectToken({
|
|
79
|
+
projectInfo
|
|
80
|
+
});
|
|
70
81
|
return projectToken?.token || null;
|
|
71
82
|
}
|
|
72
83
|
// We need to refresh the project token
|
|
@@ -82,7 +93,7 @@ import { getValidCredential } from './oauth-flow.js';
|
|
|
82
93
|
return null;
|
|
83
94
|
}
|
|
84
95
|
// Fetch a new project token from the Figma API
|
|
85
|
-
const
|
|
96
|
+
const fetchedToken = await fetchProjectToken(credential, resolvedTenantId);
|
|
86
97
|
// Validate the JWT signature and claims (skip for mocks)
|
|
87
98
|
const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE;
|
|
88
99
|
const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
|
|
@@ -90,11 +101,11 @@ import { getValidCredential } from './oauth-flow.js';
|
|
|
90
101
|
if (hasEnvToken || shouldMock) {
|
|
91
102
|
// MOCK: Skip JWT validation and parse claims directly
|
|
92
103
|
log.debug('Skipping JWT validation for mock/env project token');
|
|
93
|
-
validatedClaims = parseJWTClaims(
|
|
104
|
+
validatedClaims = parseJWTClaims(fetchedToken.token);
|
|
94
105
|
} else {
|
|
95
106
|
// REAL: Validate JWT signature against JWKS
|
|
96
107
|
try {
|
|
97
|
-
validatedClaims = await validateProjectToken(
|
|
108
|
+
validatedClaims = await validateProjectToken(fetchedToken.token, getInfraEnvironment());
|
|
98
109
|
} catch (validationError) {
|
|
99
110
|
// Handle validation errors
|
|
100
111
|
if (validationError instanceof JWTValidationError) {
|
|
@@ -108,13 +119,16 @@ import { getValidCredential } from './oauth-flow.js';
|
|
|
108
119
|
throw new ProjectTokenError('Project token missing expiration (exp) claim');
|
|
109
120
|
}
|
|
110
121
|
// Store the project token with validated expiration and claims
|
|
111
|
-
tokenStore.setProjectToken(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
tokenStore.setProjectToken({
|
|
123
|
+
projectInfo,
|
|
124
|
+
token: {
|
|
125
|
+
claims: validatedClaims,
|
|
126
|
+
expiresAt: validatedClaims.exp * 1000,
|
|
127
|
+
tenantId: resolvedTenantId,
|
|
128
|
+
token: fetchedToken.token
|
|
129
|
+
}
|
|
116
130
|
});
|
|
117
|
-
return
|
|
131
|
+
return fetchedToken.token;
|
|
118
132
|
}
|
|
119
133
|
/**
|
|
120
134
|
* Refresh a project token for a tenant
|
|
@@ -122,15 +136,19 @@ import { getValidCredential } from './oauth-flow.js';
|
|
|
122
136
|
* Forces a refresh of the project token even if the current one is still valid.
|
|
123
137
|
* Useful for testing or when you want to ensure you have the freshest token.
|
|
124
138
|
*
|
|
125
|
-
* @param tokenStore - Token store for persisting tokens
|
|
126
|
-
* @param tenantId -
|
|
139
|
+
* @param params.tokenStore - Token store for persisting tokens
|
|
140
|
+
* @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
|
|
141
|
+
* @param params.projectInfo - Optional project scope (defaults to env vars)
|
|
127
142
|
* @returns Promise resolving to JWT token string or null if failed
|
|
128
143
|
* @throws {ProjectTokenError} If token refresh fails
|
|
129
|
-
*/ export async function refreshProjectToken(
|
|
144
|
+
*/ export async function refreshProjectToken(params) {
|
|
145
|
+
const { projectInfo, tokenStore } = params;
|
|
130
146
|
// Clear the existing project token to force a refresh
|
|
131
|
-
tokenStore.clearProjectToken(
|
|
147
|
+
tokenStore.clearProjectToken({
|
|
148
|
+
projectInfo
|
|
149
|
+
});
|
|
132
150
|
// Get a new token (which will fetch from API since we just cleared it)
|
|
133
|
-
return getValidProjectToken(
|
|
151
|
+
return getValidProjectToken(params);
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
//# 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
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import * as log from '../utils/log.js';
|
|
5
|
+
const OAUTH_STORE_SCHEMA_VERSION = 1;
|
|
6
|
+
const PROJECT_STORE_SCHEMA_VERSION = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Check if migration is needed.
|
|
9
|
+
* Returns true only when legacy file exists AND new oauth file does not.
|
|
10
|
+
*/ export function needsMigration(params) {
|
|
11
|
+
const checkExists = params.checkFileExists ?? fsSync.existsSync;
|
|
12
|
+
const newExists = checkExists(params.newOAuthFilePath);
|
|
13
|
+
if (newExists) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return checkExists(params.legacyFilePath);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Migrate OAuth tokens from legacy store to new oauth store.
|
|
20
|
+
* Reads from legacy location, writes to new location with root-level keys.
|
|
21
|
+
*
|
|
22
|
+
* `clearInvalidConfig` is deliberately OFF so a transient decrypt error does
|
|
23
|
+
* not wipe the legacy file — older CLI versions elsewhere on disk still read
|
|
24
|
+
* it. If we cannot open the file, we log and skip migration.
|
|
25
|
+
*/ export function migrateOAuth(params) {
|
|
26
|
+
const legacyConfig = openLegacyConfig({
|
|
27
|
+
encryptionKey: params.encryptionKey,
|
|
28
|
+
legacyConfigName: params.legacyConfigName,
|
|
29
|
+
legacyCwd: params.legacyCwd,
|
|
30
|
+
projectName: params.projectName
|
|
31
|
+
});
|
|
32
|
+
if (!legacyConfig) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const legacyTokens = safeGet(legacyConfig, 'tokens');
|
|
36
|
+
if (!legacyTokens) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Ensure target directory exists
|
|
40
|
+
if (!fsSync.existsSync(params.newOAuthCwd)) {
|
|
41
|
+
fsSync.mkdirSync(params.newOAuthCwd, {
|
|
42
|
+
recursive: true
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const newOAuthConfig = new Conf({
|
|
46
|
+
configName: params.newOAuthConfigName,
|
|
47
|
+
cwd: params.newOAuthCwd,
|
|
48
|
+
encryptionKey: params.encryptionKey,
|
|
49
|
+
projectName: params.projectName
|
|
50
|
+
});
|
|
51
|
+
// Write root-level keys
|
|
52
|
+
newOAuthConfig.set('_schemaVersion', OAUTH_STORE_SCHEMA_VERSION);
|
|
53
|
+
newOAuthConfig.set('accessToken', legacyTokens.accessToken);
|
|
54
|
+
newOAuthConfig.set('expiresAt', legacyTokens.expiresAt);
|
|
55
|
+
newOAuthConfig.set('refreshToken', legacyTokens.refreshToken);
|
|
56
|
+
if (legacyTokens.scopes) {
|
|
57
|
+
newOAuthConfig.set('scopes', legacyTokens.scopes);
|
|
58
|
+
}
|
|
59
|
+
if (legacyTokens.tokenType) {
|
|
60
|
+
newOAuthConfig.set('tokenType', legacyTokens.tokenType);
|
|
61
|
+
}
|
|
62
|
+
if (legacyTokens.userId) {
|
|
63
|
+
newOAuthConfig.set('userId', legacyTokens.userId);
|
|
64
|
+
}
|
|
65
|
+
// Leave the legacy file untouched so older CLI versions installed in other
|
|
66
|
+
// projects on the same machine can keep reading their last known state.
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Migrate per-project bootstrap data and project tokens from the legacy
|
|
70
|
+
* combined store into individual per-project stores.
|
|
71
|
+
*
|
|
72
|
+
* Walks legacy `bootstrapData` (keyed by `projectId:environmentName`), writes
|
|
73
|
+
* each entry into its own project store file, and stashes the matching
|
|
74
|
+
* `projectToken` (looked up by `bootstrapData.contentSystemId`) alongside it.
|
|
75
|
+
* Target files that already exist are left alone.
|
|
76
|
+
*/ export function migrateProjectData(params) {
|
|
77
|
+
const legacyConfig = openLegacyConfig({
|
|
78
|
+
encryptionKey: params.encryptionKey,
|
|
79
|
+
legacyConfigName: params.legacyConfigName,
|
|
80
|
+
legacyCwd: params.legacyCwd,
|
|
81
|
+
projectName: params.projectName
|
|
82
|
+
});
|
|
83
|
+
if (!legacyConfig) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const bootstrapData = safeGet(legacyConfig, 'bootstrapData') || {};
|
|
87
|
+
const projectTokens = safeGet(legacyConfig, 'projectTokens') || {};
|
|
88
|
+
if (Object.keys(bootstrapData).length === 0 && Object.keys(projectTokens).length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const [key, bootstrap] of Object.entries(bootstrapData)){
|
|
92
|
+
const separatorIdx = key.indexOf(':');
|
|
93
|
+
if (separatorIdx === -1) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const projectId = key.slice(0, separatorIdx);
|
|
97
|
+
const environmentName = key.slice(separatorIdx + 1);
|
|
98
|
+
if (!projectId || !environmentName) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const { configName, cwd } = params.resolveProjectStorePath({
|
|
102
|
+
environmentName,
|
|
103
|
+
projectId
|
|
104
|
+
});
|
|
105
|
+
const targetPath = path.join(cwd, `${configName}.json`);
|
|
106
|
+
if (fsSync.existsSync(targetPath)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!fsSync.existsSync(cwd)) {
|
|
110
|
+
fsSync.mkdirSync(cwd, {
|
|
111
|
+
recursive: true
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const projectConfig = new Conf({
|
|
115
|
+
configName,
|
|
116
|
+
cwd,
|
|
117
|
+
encryptionKey: params.encryptionKey,
|
|
118
|
+
projectName: params.projectName
|
|
119
|
+
});
|
|
120
|
+
projectConfig.set('_schemaVersion', PROJECT_STORE_SCHEMA_VERSION);
|
|
121
|
+
projectConfig.set('bootstrapData', bootstrap);
|
|
122
|
+
const legacyProjectToken = bootstrap.contentSystemId ? projectTokens[bootstrap.contentSystemId] : undefined;
|
|
123
|
+
if (legacyProjectToken) {
|
|
124
|
+
projectConfig.set('projectToken', legacyProjectToken);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Leave the legacy file untouched so older CLI versions installed in other
|
|
128
|
+
// projects on the same machine can keep reading their last known state.
|
|
129
|
+
}
|
|
130
|
+
function safeGet(config, key) {
|
|
131
|
+
try {
|
|
132
|
+
return config.get(key);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
log.warning(`Migration read failed (key: ${String(key)}). ` + `File: ${config.path}. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Open the legacy Conf store without `clearInvalidConfig`, so a decrypt
|
|
140
|
+
* error does not destroy the file. Returns null when the file cannot be
|
|
141
|
+
* opened — callers treat that as "nothing to migrate, leave file alone".
|
|
142
|
+
*/ function openLegacyConfig(params) {
|
|
143
|
+
try {
|
|
144
|
+
return new Conf({
|
|
145
|
+
configName: params.legacyConfigName,
|
|
146
|
+
cwd: params.legacyCwd,
|
|
147
|
+
encryptionKey: params.encryptionKey,
|
|
148
|
+
projectName: params.projectName
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log.warning(`Legacy token store could not be opened; skipping migration and leaving file intact. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=token-store-migration.js.map
|
|
@@ -1,151 +1,74 @@
|
|
|
1
1
|
import type { Environment } from '../constants.js';
|
|
2
|
-
import type { BootstrapData, FigmaTokens,
|
|
2
|
+
import type { BootstrapData, FigmaTokens, ProjectStoreScope, ProjectToken, TokenStoreConfig } from './types.js';
|
|
3
3
|
/**
|
|
4
|
-
* Get the singleton TokenStore instance for an environment
|
|
5
|
-
* Use this for all production code to ensure a single shared instance per environment
|
|
6
|
-
*
|
|
7
|
-
* @param environment - The environment to get the token store for
|
|
8
|
-
* @returns The singleton TokenStore instance for that environment
|
|
4
|
+
* Get the singleton TokenStore instance for an environment.
|
|
9
5
|
*/
|
|
10
6
|
export declare function getTokenStore(environment?: Environment): TokenStore;
|
|
11
7
|
/**
|
|
12
|
-
* Secure storage manager for
|
|
13
|
-
*
|
|
14
|
-
* Uses the `conf` library to store tokens in an OS-specific secure location:
|
|
15
|
-
* - macOS: ~/Library/Preferences/payloadcms-figma
|
|
16
|
-
* - Linux: ~/.config/payloadcms-figma or $XDG_CONFIG_HOME/payloadcms-figma
|
|
17
|
-
* - Windows: %APPDATA%/payloadcms-figma/Config
|
|
18
|
-
*
|
|
19
|
-
* Tokens are encrypted at rest with a machine-specific key and file permissions
|
|
20
|
-
* are set to 0600 (owner read/write only)
|
|
8
|
+
* Secure storage manager for OAuth tokens and project-scoped bootstrap/token data.
|
|
21
9
|
*
|
|
22
10
|
* For production code, use getTokenStore() to get the singleton instance.
|
|
23
11
|
* The constructor is still exported for test isolation.
|
|
24
12
|
*/
|
|
25
13
|
export declare class TokenStore {
|
|
26
|
-
private
|
|
14
|
+
private baseConfigName;
|
|
15
|
+
private encryptionKey;
|
|
27
16
|
private environment;
|
|
17
|
+
private legacyRootDir;
|
|
18
|
+
private oauthConfig;
|
|
19
|
+
private projectName;
|
|
20
|
+
private projectStoreConfigs;
|
|
28
21
|
constructor(options?: TokenStoreConfig);
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Retrieve stored tokens
|
|
32
|
-
* @returns FigmaTokens if stored, null otherwise
|
|
33
|
-
*/
|
|
34
|
-
getTokens(): FigmaTokens | null;
|
|
35
|
-
/**
|
|
36
|
-
* Store OAuth2 tokens securely
|
|
37
|
-
* @param tokens - Figma OAuth2 tokens to store
|
|
38
|
-
*/
|
|
22
|
+
getOauthInfo(): FigmaTokens | null;
|
|
39
23
|
setTokens(tokens: FigmaTokens): void;
|
|
40
|
-
/**
|
|
41
|
-
* Clear all stored tokens (used for logout)
|
|
42
|
-
*/
|
|
43
24
|
clearTokens(): void;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
hasValidTokens(): boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Check if the current access token is expired
|
|
51
|
-
* Includes a buffer time to avoid using tokens that are about to expire
|
|
52
|
-
* @returns true if token is expired or will expire within buffer time
|
|
53
|
-
*/
|
|
54
|
-
isExpired(): boolean;
|
|
55
|
-
/**
|
|
56
|
-
* Get the access token if it exists and is valid
|
|
57
|
-
* @returns Access token string or null if expired/missing
|
|
58
|
-
*/
|
|
59
|
-
getAccessToken(): null | string;
|
|
60
|
-
/**
|
|
61
|
-
* Get the refresh token if it exists
|
|
62
|
-
* @returns Refresh token string or null if missing
|
|
63
|
-
*/
|
|
64
|
-
getRefreshToken(): null | string;
|
|
65
|
-
/**
|
|
66
|
-
* Update just the access token after a refresh
|
|
67
|
-
* Preserves the existing refresh token and other metadata
|
|
68
|
-
* @param accessToken - New access token
|
|
69
|
-
* @param expiresIn - Expiration time in seconds
|
|
70
|
-
*/
|
|
25
|
+
hasValidOauthToken(): boolean;
|
|
26
|
+
isOauthExpired(): boolean;
|
|
27
|
+
getCurrentOauthToken(): null | string;
|
|
28
|
+
getCurrentRefreshToken(): null | string;
|
|
71
29
|
updateAccessToken(accessToken: string, expiresIn: number): void;
|
|
72
30
|
/**
|
|
73
|
-
*
|
|
74
|
-
* @returns Absolute path to the token storage file
|
|
31
|
+
* Returns the OAuth storage file path.
|
|
75
32
|
*/
|
|
76
33
|
getStoragePath(): string;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* @returns true if token is expired or will expire within buffer time
|
|
104
|
-
*/
|
|
105
|
-
isProjectTokenExpired(projectToken: ProjectToken): boolean;
|
|
106
|
-
/**
|
|
107
|
-
* Check if a valid (non-expired) project token exists for a tenant
|
|
108
|
-
* @param tenantId - The tenant/CMS ID
|
|
109
|
-
* @returns true if token exists and is not expired
|
|
110
|
-
*/
|
|
111
|
-
hasValidProjectToken(tenantId: string): boolean;
|
|
112
|
-
/**
|
|
113
|
-
* Get validated JWT claims from a project token
|
|
114
|
-
* @param tenantId - The tenant/CMS ID
|
|
115
|
-
* @returns JWT payload claims or null if token not found or has no claims
|
|
116
|
-
*/
|
|
117
|
-
getProjectTokenClaims(tenantId: string): JWTPayload | null;
|
|
118
|
-
/**
|
|
119
|
-
* Get all tenant IDs that have stored project tokens
|
|
120
|
-
* @returns Array of tenant IDs
|
|
121
|
-
*/
|
|
122
|
-
getAllProjectTokenTenantIds(): string[];
|
|
123
|
-
/**
|
|
124
|
-
* Build the composite key for bootstrap data storage
|
|
125
|
-
* @param projectId - The CMS resource/project ID
|
|
126
|
-
* @param environmentName - The environment name (e.g. 'production', 'staging')
|
|
127
|
-
* @returns Composite key string
|
|
128
|
-
*/
|
|
129
|
-
private bootstrapKey;
|
|
130
|
-
/**
|
|
131
|
-
* Retrieve bootstrap data for a project + environment
|
|
132
|
-
* @param projectId - The CMS resource/project ID
|
|
133
|
-
* @param environmentName - The environment name
|
|
134
|
-
* @returns BootstrapData if stored, null otherwise
|
|
135
|
-
*/
|
|
34
|
+
getProjectToken({ projectInfo, }?: {
|
|
35
|
+
projectInfo?: ProjectStoreScope;
|
|
36
|
+
}): null | ProjectToken;
|
|
37
|
+
setProjectToken({ projectInfo, token, }: {
|
|
38
|
+
projectInfo?: ProjectStoreScope;
|
|
39
|
+
token: ProjectToken;
|
|
40
|
+
}): void;
|
|
41
|
+
clearProjectToken({ projectInfo }?: {
|
|
42
|
+
projectInfo?: ProjectStoreScope;
|
|
43
|
+
}): void;
|
|
44
|
+
isProjectTokenExpired(token: ProjectToken): boolean;
|
|
45
|
+
hasValidProjectToken({ projectInfo }?: {
|
|
46
|
+
projectInfo?: ProjectStoreScope;
|
|
47
|
+
}): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Get the tenantId (contentSystemId) for the project scope.
|
|
50
|
+
* Returns null if bootstrap data is not available.
|
|
51
|
+
*/
|
|
52
|
+
getTenantId({ projectInfo }?: {
|
|
53
|
+
projectInfo?: ProjectStoreScope;
|
|
54
|
+
}): null | string;
|
|
55
|
+
/**
|
|
56
|
+
* Derive project info from environment variables.
|
|
57
|
+
* Returns null if either FIGMA_PROJECT_ID or FIGMA_ENVIRONMENT_NAME is not set.
|
|
58
|
+
*/
|
|
59
|
+
private getProjectInfoFromEnv;
|
|
136
60
|
getBootstrapData(projectId: string, environmentName: string): BootstrapData | null;
|
|
137
|
-
/**
|
|
138
|
-
* Store bootstrap data for a project + environment
|
|
139
|
-
* @param projectId - The CMS resource/project ID
|
|
140
|
-
* @param environmentName - The environment name
|
|
141
|
-
* @param data - The bootstrap data to store
|
|
142
|
-
*/
|
|
143
61
|
setBootstrapData(projectId: string, environmentName: string, data: BootstrapData): void;
|
|
144
|
-
/**
|
|
145
|
-
* Clear bootstrap data for a project + environment
|
|
146
|
-
* @param projectId - The CMS resource/project ID
|
|
147
|
-
* @param environmentName - The environment name
|
|
148
|
-
*/
|
|
149
62
|
clearBootstrapData(projectId: string, environmentName: string): void;
|
|
63
|
+
private buildOAuthConfigName;
|
|
64
|
+
private buildOAuthStoreCwd;
|
|
65
|
+
private buildProjectStoreConfigName;
|
|
66
|
+
private buildProjectStoreCwd;
|
|
67
|
+
private sanitizeConfigSegment;
|
|
68
|
+
private projectInfoKey;
|
|
69
|
+
private getLegacyRootDir;
|
|
70
|
+
private getProjectStore;
|
|
71
|
+
private createConfig;
|
|
72
|
+
private safeGet;
|
|
150
73
|
}
|
|
151
74
|
//# sourceMappingURL=token-store.d.ts.map
|