@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
package/dist/auth/token-store.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/* eslint-disable perfectionist/sort-classes */ import Conf from 'conf';
|
|
2
|
+
import envPaths from 'env-paths';
|
|
2
3
|
import fsSync from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
|
|
4
6
|
import { getInfraEnvironment } from '../constants.js';
|
|
7
|
+
import { getEnvVarSync } from '../utils/env-management.js';
|
|
5
8
|
import * as log from '../utils/log.js';
|
|
6
9
|
import { deriveEncryptionKey } from './crypto-utils.js';
|
|
10
|
+
import { migrateOAuth, migrateProjectData, needsMigration } from './token-store-migration.js';
|
|
7
11
|
/**
|
|
8
|
-
* Environment-keyed instances for singleton pattern
|
|
12
|
+
* Environment-keyed instances for singleton pattern.
|
|
9
13
|
*/ const instances = {};
|
|
14
|
+
const OAUTH_STORE_SCHEMA_VERSION = 1;
|
|
15
|
+
const PROJECT_STORE_SCHEMA_VERSION = 1;
|
|
10
16
|
/**
|
|
11
|
-
* Get the singleton TokenStore instance for an environment
|
|
12
|
-
* Use this for all production code to ensure a single shared instance per environment
|
|
13
|
-
*
|
|
14
|
-
* @param environment - The environment to get the token store for
|
|
15
|
-
* @returns The singleton TokenStore instance for that environment
|
|
17
|
+
* Get the singleton TokenStore instance for an environment.
|
|
16
18
|
*/ export function getTokenStore(environment = getInfraEnvironment()) {
|
|
17
19
|
if (!instances[environment]) {
|
|
18
20
|
instances[environment] = new TokenStore({
|
|
@@ -22,110 +24,180 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
22
24
|
return instances[environment];
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
|
-
* Secure storage manager for
|
|
26
|
-
*
|
|
27
|
-
* Uses the `conf` library to store tokens in an OS-specific secure location:
|
|
28
|
-
* - macOS: ~/Library/Preferences/payloadcms-figma
|
|
29
|
-
* - Linux: ~/.config/payloadcms-figma or $XDG_CONFIG_HOME/payloadcms-figma
|
|
30
|
-
* - Windows: %APPDATA%/payloadcms-figma/Config
|
|
31
|
-
*
|
|
32
|
-
* Tokens are encrypted at rest with a machine-specific key and file permissions
|
|
33
|
-
* are set to 0600 (owner read/write only)
|
|
27
|
+
* Secure storage manager for OAuth tokens and project-scoped bootstrap/token data.
|
|
34
28
|
*
|
|
35
29
|
* For production code, use getTokenStore() to get the singleton instance.
|
|
36
30
|
* The constructor is still exported for test isolation.
|
|
37
31
|
*/ export class TokenStore {
|
|
38
|
-
|
|
32
|
+
baseConfigName;
|
|
33
|
+
encryptionKey;
|
|
39
34
|
environment;
|
|
35
|
+
legacyRootDir;
|
|
36
|
+
oauthConfig;
|
|
37
|
+
projectName;
|
|
38
|
+
projectStoreConfigs = new Map();
|
|
40
39
|
constructor(options){
|
|
41
40
|
if (process.env.AWS_EXECUTION_ENV) {
|
|
42
41
|
throw new Error('TokenStore cannot be used in AWS Lambda environments');
|
|
43
42
|
}
|
|
44
43
|
const environment = options?.environment ?? 'production';
|
|
45
|
-
const projectName =
|
|
44
|
+
const projectName = 'payloadcms-figma';
|
|
45
|
+
const encryptionKey = options?.encryptionKey ?? deriveEncryptionKey();
|
|
46
|
+
const baseConfigName = options?.configName || projectName;
|
|
47
|
+
this.baseConfigName = baseConfigName;
|
|
48
|
+
this.encryptionKey = encryptionKey;
|
|
46
49
|
this.environment = environment;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
this.projectName = projectName;
|
|
51
|
+
// Resolve the legacy root dir (matching Conf's env-paths convention)
|
|
52
|
+
// without instantiating Conf, which would read/parse the encrypted file
|
|
53
|
+
// and could wipe it under clearInvalidConfig.
|
|
54
|
+
this.legacyRootDir = envPaths(projectName).config;
|
|
55
|
+
const legacyFilePath = path.join(this.legacyRootDir, `${baseConfigName}.json`);
|
|
56
|
+
const newOAuthCwd = this.buildOAuthStoreCwd();
|
|
57
|
+
const newOAuthFilePath = path.join(newOAuthCwd, `${this.buildOAuthConfigName()}.json`);
|
|
58
|
+
const resolveProjectStorePath = (scope)=>({
|
|
59
|
+
configName: this.buildProjectStoreConfigName(scope),
|
|
60
|
+
cwd: this.buildProjectStoreCwd(scope)
|
|
61
|
+
});
|
|
62
|
+
if (needsMigration({
|
|
63
|
+
legacyFilePath,
|
|
64
|
+
newOAuthFilePath
|
|
65
|
+
})) {
|
|
66
|
+
migrateOAuth({
|
|
67
|
+
encryptionKey,
|
|
68
|
+
legacyConfigName: baseConfigName,
|
|
69
|
+
newOAuthConfigName: this.buildOAuthConfigName(),
|
|
70
|
+
newOAuthCwd,
|
|
71
|
+
projectName
|
|
65
72
|
});
|
|
66
|
-
// Force a write so the corrupt file is replaced with an empty store.
|
|
67
|
-
// Without this, clearInvalidConfig only ignores bad reads in memory
|
|
68
|
-
// and the corrupt file persists, triggering this fallback every time.
|
|
69
|
-
this.config.clear();
|
|
70
73
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
if (fsSync.existsSync(legacyFilePath)) {
|
|
75
|
+
migrateProjectData({
|
|
76
|
+
encryptionKey,
|
|
77
|
+
legacyConfigName: baseConfigName,
|
|
78
|
+
projectName,
|
|
79
|
+
resolveProjectStorePath
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Also check for legacy files in old environment-specific directories
|
|
83
|
+
// (e.g., payloadcms-figma-staging-nodejs/ for staging environment).
|
|
84
|
+
if (environment !== 'production') {
|
|
85
|
+
const oldEnvProjectName = `payloadcms-figma-${environment}`;
|
|
86
|
+
const oldEnvConfigName = options?.configName || oldEnvProjectName;
|
|
87
|
+
const oldEnvLegacyDir = envPaths(oldEnvProjectName).config;
|
|
88
|
+
const oldEnvLegacyFilePath = path.join(oldEnvLegacyDir, `${oldEnvConfigName}.json`);
|
|
89
|
+
if (needsMigration({
|
|
90
|
+
legacyFilePath: oldEnvLegacyFilePath,
|
|
91
|
+
newOAuthFilePath
|
|
92
|
+
})) {
|
|
93
|
+
migrateOAuth({
|
|
94
|
+
encryptionKey,
|
|
95
|
+
legacyConfigName: oldEnvConfigName,
|
|
96
|
+
legacyCwd: oldEnvLegacyDir,
|
|
97
|
+
newOAuthConfigName: this.buildOAuthConfigName(),
|
|
98
|
+
newOAuthCwd,
|
|
99
|
+
projectName: oldEnvProjectName
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (fsSync.existsSync(oldEnvLegacyFilePath)) {
|
|
103
|
+
migrateProjectData({
|
|
104
|
+
encryptionKey,
|
|
105
|
+
legacyConfigName: oldEnvConfigName,
|
|
106
|
+
legacyCwd: oldEnvLegacyDir,
|
|
107
|
+
projectName: oldEnvProjectName,
|
|
108
|
+
resolveProjectStorePath
|
|
109
|
+
});
|
|
81
110
|
}
|
|
82
|
-
return undefined;
|
|
83
111
|
}
|
|
112
|
+
this.oauthConfig = this.createConfig({
|
|
113
|
+
configName: this.buildOAuthConfigName(),
|
|
114
|
+
cwd: this.buildOAuthStoreCwd(),
|
|
115
|
+
encryptionKey: this.encryptionKey,
|
|
116
|
+
storeType: 'oauth'
|
|
117
|
+
});
|
|
84
118
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
getOauthInfo() {
|
|
120
|
+
const accessToken = this.safeGet({
|
|
121
|
+
config: this.oauthConfig,
|
|
122
|
+
key: 'accessToken'
|
|
123
|
+
});
|
|
124
|
+
const expiresAt = this.safeGet({
|
|
125
|
+
config: this.oauthConfig,
|
|
126
|
+
key: 'expiresAt'
|
|
127
|
+
});
|
|
128
|
+
const refreshToken = this.safeGet({
|
|
129
|
+
config: this.oauthConfig,
|
|
130
|
+
key: 'refreshToken'
|
|
131
|
+
});
|
|
132
|
+
if (typeof accessToken !== 'string' || typeof expiresAt !== 'number' || typeof refreshToken !== 'string') {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
accessToken,
|
|
137
|
+
expiresAt,
|
|
138
|
+
refreshToken,
|
|
139
|
+
scopes: this.safeGet({
|
|
140
|
+
config: this.oauthConfig,
|
|
141
|
+
key: 'scopes'
|
|
142
|
+
}),
|
|
143
|
+
tokenType: this.safeGet({
|
|
144
|
+
config: this.oauthConfig,
|
|
145
|
+
key: 'tokenType'
|
|
146
|
+
}),
|
|
147
|
+
userId: this.safeGet({
|
|
148
|
+
config: this.oauthConfig,
|
|
149
|
+
key: 'userId'
|
|
150
|
+
})
|
|
151
|
+
};
|
|
91
152
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
|
|
153
|
+
setTokens(tokens) {
|
|
154
|
+
this.oauthConfig.set('_schemaVersion', OAUTH_STORE_SCHEMA_VERSION);
|
|
155
|
+
this.oauthConfig.set('accessToken', tokens.accessToken);
|
|
156
|
+
this.oauthConfig.set('expiresAt', tokens.expiresAt);
|
|
157
|
+
this.oauthConfig.set('refreshToken', tokens.refreshToken);
|
|
158
|
+
if (tokens.scopes) {
|
|
159
|
+
this.oauthConfig.set('scopes', tokens.scopes);
|
|
160
|
+
} else {
|
|
161
|
+
this.oauthConfig.delete('scopes');
|
|
162
|
+
}
|
|
163
|
+
if (tokens.tokenType) {
|
|
164
|
+
this.oauthConfig.set('tokenType', tokens.tokenType);
|
|
165
|
+
} else {
|
|
166
|
+
this.oauthConfig.delete('tokenType');
|
|
167
|
+
}
|
|
168
|
+
if (tokens.userId) {
|
|
169
|
+
this.oauthConfig.set('userId', tokens.userId);
|
|
170
|
+
} else {
|
|
171
|
+
this.oauthConfig.delete('userId');
|
|
172
|
+
}
|
|
173
|
+
// Verify the round-trip: read back what we just wrote.
|
|
174
|
+
const readBack = this.getOauthInfo();
|
|
99
175
|
if (!readBack || readBack.accessToken !== tokens.accessToken) {
|
|
100
176
|
if (this.environment === 'staging') {
|
|
101
|
-
const filePath = this.
|
|
177
|
+
const filePath = this.oauthConfig.path;
|
|
102
178
|
const fileExists = fsSync.existsSync(filePath);
|
|
103
179
|
const fileSize = fileExists ? fsSync.statSync(filePath).size : 0;
|
|
104
180
|
log.warning(`Token verify-after-write FAILED. ` + `File: ${filePath}, exists: ${fileExists}, size: ${fileSize}B. ` + `Written accessToken starts: ${tokens.accessToken.substring(0, 10)}..., ` + `Read back: ${readBack ? readBack.accessToken.substring(0, 10) + '...' : 'null'}`);
|
|
105
181
|
}
|
|
106
182
|
}
|
|
107
183
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.
|
|
184
|
+
clearTokens() {
|
|
185
|
+
this.oauthConfig.delete('accessToken');
|
|
186
|
+
this.oauthConfig.delete('expiresAt');
|
|
187
|
+
this.oauthConfig.delete('refreshToken');
|
|
188
|
+
this.oauthConfig.delete('scopes');
|
|
189
|
+
this.oauthConfig.delete('tokenType');
|
|
190
|
+
this.oauthConfig.delete('userId');
|
|
112
191
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
* @returns true if tokens exist and are not expired
|
|
116
|
-
*/ hasValidTokens() {
|
|
117
|
-
const tokens = this.getTokens();
|
|
192
|
+
hasValidOauthToken() {
|
|
193
|
+
const tokens = this.getOauthInfo();
|
|
118
194
|
if (!tokens) {
|
|
119
195
|
return false;
|
|
120
196
|
}
|
|
121
|
-
return !this.
|
|
197
|
+
return !this.isOauthExpired();
|
|
122
198
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* Includes a buffer time to avoid using tokens that are about to expire
|
|
126
|
-
* @returns true if token is expired or will expire within buffer time
|
|
127
|
-
*/ isExpired() {
|
|
128
|
-
const tokens = this.safeGet('tokens');
|
|
199
|
+
isOauthExpired() {
|
|
200
|
+
const tokens = this.getOauthInfo();
|
|
129
201
|
if (!tokens) {
|
|
130
202
|
return true;
|
|
131
203
|
}
|
|
@@ -134,30 +206,19 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
134
206
|
const expiryWithBuffer = tokens.expiresAt - bufferMs;
|
|
135
207
|
return now >= expiryWithBuffer;
|
|
136
208
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
* @returns Access token string or null if expired/missing
|
|
140
|
-
*/ getAccessToken() {
|
|
141
|
-
if (!this.hasValidTokens()) {
|
|
209
|
+
getCurrentOauthToken() {
|
|
210
|
+
if (!this.hasValidOauthToken()) {
|
|
142
211
|
return null;
|
|
143
212
|
}
|
|
144
|
-
const tokens = this.
|
|
213
|
+
const tokens = this.getOauthInfo();
|
|
145
214
|
return tokens?.accessToken || null;
|
|
146
215
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
* @returns Refresh token string or null if missing
|
|
150
|
-
*/ getRefreshToken() {
|
|
151
|
-
const tokens = this.getTokens();
|
|
216
|
+
getCurrentRefreshToken() {
|
|
217
|
+
const tokens = this.getOauthInfo();
|
|
152
218
|
return tokens?.refreshToken || null;
|
|
153
219
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
* Preserves the existing refresh token and other metadata
|
|
157
|
-
* @param accessToken - New access token
|
|
158
|
-
* @param expiresIn - Expiration time in seconds
|
|
159
|
-
*/ updateAccessToken(accessToken, expiresIn) {
|
|
160
|
-
const existingTokens = this.getTokens();
|
|
220
|
+
updateAccessToken(accessToken, expiresIn) {
|
|
221
|
+
const existingTokens = this.getOauthInfo();
|
|
161
222
|
if (!existingTokens) {
|
|
162
223
|
throw new Error('Cannot update access token: no existing tokens found');
|
|
163
224
|
}
|
|
@@ -169,123 +230,201 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
169
230
|
});
|
|
170
231
|
}
|
|
171
232
|
/**
|
|
172
|
-
*
|
|
173
|
-
* @returns Absolute path to the token storage file
|
|
233
|
+
* Returns the OAuth storage file path.
|
|
174
234
|
*/ getStoragePath() {
|
|
175
|
-
return this.
|
|
235
|
+
return this.oauthConfig.path;
|
|
176
236
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
237
|
+
getProjectToken({ projectInfo } = {}) {
|
|
238
|
+
const resolvedProjectInfo = projectInfo ?? this.getProjectInfoFromEnv();
|
|
239
|
+
if (!resolvedProjectInfo) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const config = this.getProjectStore(resolvedProjectInfo);
|
|
243
|
+
const projectToken = this.safeGet({
|
|
244
|
+
config,
|
|
245
|
+
key: 'projectToken'
|
|
246
|
+
});
|
|
185
247
|
if (!projectToken) {
|
|
186
248
|
return null;
|
|
187
249
|
}
|
|
188
|
-
// Auto-cleanup expired project token
|
|
189
250
|
if (this.isProjectTokenExpired(projectToken)) {
|
|
190
|
-
this.clearProjectToken(
|
|
251
|
+
this.clearProjectToken({
|
|
252
|
+
projectInfo: resolvedProjectInfo
|
|
253
|
+
});
|
|
191
254
|
return null;
|
|
192
255
|
}
|
|
193
256
|
return projectToken;
|
|
194
257
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Clear a project token for a specific tenant
|
|
206
|
-
* @param tenantId - The tenant/CMS ID
|
|
207
|
-
*/ clearProjectToken(tenantId) {
|
|
208
|
-
const projectTokens = this.safeGet('projectTokens') || {};
|
|
209
|
-
delete projectTokens[tenantId];
|
|
210
|
-
this.config.set('projectTokens', projectTokens);
|
|
258
|
+
setProjectToken({ projectInfo, token }) {
|
|
259
|
+
const resolvedProjectInfo = projectInfo ?? this.getProjectInfoFromEnv();
|
|
260
|
+
if (!resolvedProjectInfo) {
|
|
261
|
+
throw new Error('Cannot set project token: FIGMA_PROJECT_ID and FIGMA_ENVIRONMENT_NAME must be set');
|
|
262
|
+
}
|
|
263
|
+
const config = this.getProjectStore(resolvedProjectInfo);
|
|
264
|
+
config.set('_schemaVersion', PROJECT_STORE_SCHEMA_VERSION);
|
|
265
|
+
config.set('projectToken', token);
|
|
211
266
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
267
|
+
clearProjectToken({ projectInfo } = {}) {
|
|
268
|
+
const resolvedProjectInfo = projectInfo ?? this.getProjectInfoFromEnv();
|
|
269
|
+
if (!resolvedProjectInfo) {
|
|
270
|
+
return; // Nothing to clear if we can't determine the project
|
|
271
|
+
}
|
|
272
|
+
const config = this.getProjectStore(resolvedProjectInfo);
|
|
273
|
+
config.delete('projectToken');
|
|
216
274
|
}
|
|
217
|
-
|
|
218
|
-
* Check if a project token is expired
|
|
219
|
-
* Includes a buffer time to avoid using tokens that are about to expire
|
|
220
|
-
* @param projectToken - The project token to check
|
|
221
|
-
* @returns true if token is expired or will expire within buffer time
|
|
222
|
-
*/ isProjectTokenExpired(projectToken) {
|
|
275
|
+
isProjectTokenExpired(token) {
|
|
223
276
|
const now = Date.now();
|
|
224
277
|
const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
|
|
225
|
-
const expiryWithBuffer =
|
|
278
|
+
const expiryWithBuffer = token.expiresAt - bufferMs;
|
|
226
279
|
return now >= expiryWithBuffer;
|
|
227
280
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
*/ hasValidProjectToken(tenantId) {
|
|
233
|
-
const projectToken = this.getProjectToken(tenantId);
|
|
281
|
+
hasValidProjectToken({ projectInfo } = {}) {
|
|
282
|
+
const projectToken = this.getProjectToken({
|
|
283
|
+
projectInfo
|
|
284
|
+
});
|
|
234
285
|
if (!projectToken) {
|
|
235
286
|
return false;
|
|
236
287
|
}
|
|
237
288
|
return !this.isProjectTokenExpired(projectToken);
|
|
238
289
|
}
|
|
239
290
|
/**
|
|
240
|
-
* Get
|
|
241
|
-
*
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
291
|
+
* Get the tenantId (contentSystemId) for the project scope.
|
|
292
|
+
* Returns null if bootstrap data is not available.
|
|
293
|
+
*/ getTenantId({ projectInfo } = {}) {
|
|
294
|
+
const resolvedProjectInfo = projectInfo ?? this.getProjectInfoFromEnv();
|
|
295
|
+
if (!resolvedProjectInfo) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const bootstrapData = this.getBootstrapData(resolvedProjectInfo.projectId, resolvedProjectInfo.environmentName);
|
|
299
|
+
return bootstrapData?.contentSystemId ?? null;
|
|
246
300
|
}
|
|
247
301
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*/
|
|
251
|
-
const
|
|
252
|
-
|
|
302
|
+
* Derive project info from environment variables.
|
|
303
|
+
* Returns null if either FIGMA_PROJECT_ID or FIGMA_ENVIRONMENT_NAME is not set.
|
|
304
|
+
*/ getProjectInfoFromEnv() {
|
|
305
|
+
const projectId = process.env.FIGMA_PROJECT_ID || getEnvVarSync(process.cwd(), 'FIGMA_PROJECT_ID');
|
|
306
|
+
const environmentName = process.env.FIGMA_ENVIRONMENT_NAME || getEnvVarSync(process.cwd(), 'FIGMA_ENVIRONMENT_NAME');
|
|
307
|
+
if (!projectId || !environmentName) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
environmentName,
|
|
312
|
+
projectId
|
|
313
|
+
};
|
|
253
314
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
315
|
+
getBootstrapData(projectId, environmentName) {
|
|
316
|
+
const config = this.getProjectStore({
|
|
317
|
+
environmentName,
|
|
318
|
+
projectId
|
|
319
|
+
});
|
|
320
|
+
const bootstrapData = this.safeGet({
|
|
321
|
+
config,
|
|
322
|
+
key: 'bootstrapData'
|
|
323
|
+
});
|
|
324
|
+
return bootstrapData || null;
|
|
261
325
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return allData[this.bootstrapKey(projectId, environmentName)] || null;
|
|
326
|
+
setBootstrapData(projectId, environmentName, data) {
|
|
327
|
+
const config = this.getProjectStore({
|
|
328
|
+
environmentName,
|
|
329
|
+
projectId
|
|
330
|
+
});
|
|
331
|
+
config.set('_schemaVersion', PROJECT_STORE_SCHEMA_VERSION);
|
|
332
|
+
config.set('bootstrapData', data);
|
|
270
333
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const allData = this.safeGet('bootstrapData') || {};
|
|
278
|
-
allData[this.bootstrapKey(projectId, environmentName)] = data;
|
|
279
|
-
this.config.set('bootstrapData', allData);
|
|
334
|
+
clearBootstrapData(projectId, environmentName) {
|
|
335
|
+
const config = this.getProjectStore({
|
|
336
|
+
environmentName,
|
|
337
|
+
projectId
|
|
338
|
+
});
|
|
339
|
+
config.delete('bootstrapData');
|
|
280
340
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
341
|
+
buildOAuthConfigName() {
|
|
342
|
+
// For test isolation: if using a custom config name, use it as-is with -oauth suffix
|
|
343
|
+
// Otherwise use the standard naming that includes environment
|
|
344
|
+
if (this.baseConfigName !== this.projectName) {
|
|
345
|
+
return `${this.baseConfigName}-oauth`;
|
|
346
|
+
}
|
|
347
|
+
return `payloadcms-figma-${this.environment}-oauth`;
|
|
348
|
+
}
|
|
349
|
+
buildOAuthStoreCwd() {
|
|
350
|
+
return path.join(this.getLegacyRootDir(), 'oauth');
|
|
351
|
+
}
|
|
352
|
+
buildProjectStoreConfigName(projectInfo) {
|
|
353
|
+
const env = this.sanitizeConfigSegment(projectInfo.environmentName);
|
|
354
|
+
if (this.baseConfigName !== this.projectName) {
|
|
355
|
+
return `${this.baseConfigName}-${env}-contentAPI`;
|
|
356
|
+
}
|
|
357
|
+
return `payloadcms-figma-${env}-contentAPI`;
|
|
358
|
+
}
|
|
359
|
+
buildProjectStoreCwd(projectInfo) {
|
|
360
|
+
return path.join(this.getLegacyRootDir(), 'projects', this.sanitizeConfigSegment(projectInfo.projectId));
|
|
361
|
+
}
|
|
362
|
+
sanitizeConfigSegment(value) {
|
|
363
|
+
return value.replace(/[^\w-]/g, '-').replace(/-+/g, '-');
|
|
364
|
+
}
|
|
365
|
+
projectInfoKey(projectInfo) {
|
|
366
|
+
return `${projectInfo.projectId}:${projectInfo.environmentName}`;
|
|
367
|
+
}
|
|
368
|
+
getLegacyRootDir() {
|
|
369
|
+
return this.legacyRootDir;
|
|
370
|
+
}
|
|
371
|
+
getProjectStore(projectInfo) {
|
|
372
|
+
const projectInfoKey = this.projectInfoKey(projectInfo);
|
|
373
|
+
const existing = this.projectStoreConfigs.get(projectInfoKey);
|
|
374
|
+
if (existing) {
|
|
375
|
+
return existing;
|
|
376
|
+
}
|
|
377
|
+
const config = this.createConfig({
|
|
378
|
+
configName: this.buildProjectStoreConfigName(projectInfo),
|
|
379
|
+
cwd: this.buildProjectStoreCwd(projectInfo),
|
|
380
|
+
encryptionKey: this.encryptionKey,
|
|
381
|
+
storeType: 'project'
|
|
382
|
+
});
|
|
383
|
+
this.projectStoreConfigs.set(projectInfoKey, config);
|
|
384
|
+
return config;
|
|
385
|
+
}
|
|
386
|
+
createConfig(params) {
|
|
387
|
+
const confOptions = {
|
|
388
|
+
clearInvalidConfig: false,
|
|
389
|
+
configName: params.configName,
|
|
390
|
+
cwd: params.cwd,
|
|
391
|
+
...params.encryptionKey ? {
|
|
392
|
+
encryptionKey: params.encryptionKey
|
|
393
|
+
} : {},
|
|
394
|
+
projectName: this.projectName
|
|
395
|
+
};
|
|
396
|
+
try {
|
|
397
|
+
return new Conf(confOptions);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
// Deserialization failed (e.g. encryption key changed).
|
|
400
|
+
// Log diagnostics, then clear and replace with a fresh store.
|
|
401
|
+
if (this.environment === 'staging') {
|
|
402
|
+
log.warning(`${params.storeType} token store corrupted, resetting. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
403
|
+
}
|
|
404
|
+
const config = new Conf({
|
|
405
|
+
...confOptions,
|
|
406
|
+
clearInvalidConfig: true
|
|
407
|
+
});
|
|
408
|
+
// Force a write so the corrupt file is replaced with an empty store.
|
|
409
|
+
// Without this, clearInvalidConfig only ignores bad reads in memory
|
|
410
|
+
// and the corrupt file persists, triggering this fallback every time.
|
|
411
|
+
config.clear();
|
|
412
|
+
return config;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
safeGet(params) {
|
|
416
|
+
const { config, key } = params;
|
|
417
|
+
try {
|
|
418
|
+
return config.get(key);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (this.environment === 'staging') {
|
|
421
|
+
const filePath = config.path;
|
|
422
|
+
const fileExists = fsSync.existsSync(filePath);
|
|
423
|
+
const fileSize = fileExists ? fsSync.statSync(filePath).size : 0;
|
|
424
|
+
log.warning(`Token store read failed (key: ${String(key)}). ` + `File: ${filePath}, exists: ${fileExists}, size: ${fileSize}B. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
289
428
|
}
|
|
290
429
|
}
|
|
291
430
|
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -109,7 +109,7 @@ export interface OAuthTokenRefreshParams {
|
|
|
109
109
|
export type TokenStoreConfig = {
|
|
110
110
|
/** Config file name (for testing) */
|
|
111
111
|
configName?: string;
|
|
112
|
-
/**
|
|
112
|
+
/** Optional override for storage encryption key (primarily for tests). */
|
|
113
113
|
encryptionKey?: string;
|
|
114
114
|
/** Environment for config file naming */
|
|
115
115
|
environment?: Environment;
|
|
@@ -171,6 +171,13 @@ export type BootstrapData = {
|
|
|
171
171
|
oauthClientSecret: string;
|
|
172
172
|
tenantInstanceId: string;
|
|
173
173
|
};
|
|
174
|
+
/**
|
|
175
|
+
* Scope information used for project/environment-specific token storage.
|
|
176
|
+
*/
|
|
177
|
+
export type ProjectStoreScope = {
|
|
178
|
+
environmentName: string;
|
|
179
|
+
projectId: string;
|
|
180
|
+
};
|
|
174
181
|
/**
|
|
175
182
|
* JWT payload structure from Figma project tokens
|
|
176
183
|
* Extends jose's standard JWTPayload with Figma-specific claims
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import pc from 'picocolors';
|
|
|
4
4
|
import { buildLambdaZipCommand } from './commands/build-lambda-zip.js';
|
|
5
5
|
import { debugCommand } from './commands/debug.js';
|
|
6
6
|
import { deployCommand } from './commands/deploy.js';
|
|
7
|
+
import { dumpTokensCommand } from './commands/dump-tokens.js';
|
|
7
8
|
import { envCommand } from './commands/env.js';
|
|
8
9
|
import { initCommand } from './commands/init.js';
|
|
9
10
|
import { listTokensCommand } from './commands/list-tokens.js';
|
|
@@ -101,6 +102,11 @@ class Main {
|
|
|
101
102
|
await debugCommand();
|
|
102
103
|
process.exit(0);
|
|
103
104
|
}
|
|
105
|
+
// dump-tokens outputs plain text
|
|
106
|
+
if (subcommand === 'dump-tokens') {
|
|
107
|
+
await dumpTokensCommand();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
104
110
|
// eslint-disable-next-line no-console
|
|
105
111
|
console.log('\n');
|
|
106
112
|
p.intro(pc.bgCyan(pc.black(' @payloadcms/figma ')));
|