@payloadcms/figma 0.0.1-alpha.63 → 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/oauth-flow.js +1 -0
- package/dist/auth/project-token.d.ts +15 -10
- package/dist/auth/project-token.js +147 -64
- package/dist/auth/token-store-migration.js +2 -2
- package/dist/auth/token-store.js +2 -2
- package/dist/auth/types.d.ts +4 -0
- package/dist/cli.js +9 -0
- package/dist/commands/bootstrap.d.ts +18 -0
- package/dist/commands/bootstrap.js +90 -0
- package/dist/commands/init.js +32 -2
- package/dist/db-content-api/index.js +27 -73
- package/dist/plugin/build-config.js +62 -46
- 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/package.json +1 -1
package/dist/auth/oauth-flow.js
CHANGED
|
@@ -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
|
|
@@ -14,15 +14,18 @@ export declare class ProjectTokenError extends Error {
|
|
|
14
14
|
cause?: Error | undefined;
|
|
15
15
|
constructor(message: string, cause?: Error | undefined);
|
|
16
16
|
}
|
|
17
|
+
/** Test-only: clears the in-memory token cache and any in-flight refresh promises. */
|
|
18
|
+
export declare function __resetProjectTokenCacheForTests(): void;
|
|
17
19
|
/**
|
|
18
|
-
* Get a valid project token for a tenant, refreshing if necessary
|
|
20
|
+
* Get a valid project token for a tenant, refreshing if necessary.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
* 1.
|
|
22
|
-
* 2.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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.
|
|
26
29
|
*
|
|
27
30
|
* The project token is used by local Payload instances to authenticate
|
|
28
31
|
* requests to the Content API without requiring a round-trip to Sinatra.
|
|
@@ -39,10 +42,12 @@ export declare function getValidProjectToken({ projectInfo, tenantId, tokenStore
|
|
|
39
42
|
tokenStore: TokenStore;
|
|
40
43
|
}): Promise<null | string>;
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
45
|
+
* Force-refresh a project token, bypassing all cache layers.
|
|
43
46
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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.
|
|
46
51
|
*
|
|
47
52
|
* @param params.tokenStore - Token store for persisting tokens
|
|
48
53
|
* @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
|
|
@@ -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,14 +59,15 @@ 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.
|
|
@@ -71,70 +88,125 @@ import { getValidCredential } from './oauth-flow.js';
|
|
|
71
88
|
if (!resolvedTenantId) {
|
|
72
89
|
throw new ProjectTokenError('tenantId was not provided and could not be resolved from bootstrap data');
|
|
73
90
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return projectToken?.token || null;
|
|
82
|
-
}
|
|
83
|
-
// We need to refresh the project token
|
|
84
|
-
// An oauth access token is required for the call
|
|
85
|
-
let credential;
|
|
86
|
-
try {
|
|
87
|
-
credential = await getValidCredential(tokenStore);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
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;
|
|
90
98
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
99
|
+
const inFlight = inFlightRefreshes.get(cacheKey);
|
|
100
|
+
if (inFlight) {
|
|
101
|
+
return inFlight;
|
|
94
102
|
}
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
|
107
140
|
try {
|
|
108
|
-
|
|
109
|
-
} catch (
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
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;
|
|
113
162
|
}
|
|
114
|
-
throw validationError;
|
|
115
163
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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,
|
|
128
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);
|
|
129
200
|
}
|
|
130
|
-
}
|
|
131
|
-
return fetchedToken.token;
|
|
201
|
+
}
|
|
132
202
|
}
|
|
133
203
|
/**
|
|
134
|
-
*
|
|
204
|
+
* Force-refresh a project token, bypassing all cache layers.
|
|
135
205
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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.
|
|
138
210
|
*
|
|
139
211
|
* @param params.tokenStore - Token store for persisting tokens
|
|
140
212
|
* @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
|
|
@@ -142,12 +214,23 @@ import { getValidCredential } from './oauth-flow.js';
|
|
|
142
214
|
* @returns Promise resolving to JWT token string or null if failed
|
|
143
215
|
* @throws {ProjectTokenError} If token refresh fails
|
|
144
216
|
*/ export async function refreshProjectToken(params) {
|
|
145
|
-
const { projectInfo, tokenStore } = params;
|
|
146
|
-
|
|
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
|
+
}
|
|
147
231
|
tokenStore.clearProjectToken({
|
|
148
232
|
projectInfo
|
|
149
233
|
});
|
|
150
|
-
// Get a new token (which will fetch from API since we just cleared it)
|
|
151
234
|
return getValidProjectToken(params);
|
|
152
235
|
}
|
|
153
236
|
|
|
@@ -131,7 +131,7 @@ function safeGet(config, key) {
|
|
|
131
131
|
try {
|
|
132
132
|
return config.get(key);
|
|
133
133
|
} catch (error) {
|
|
134
|
-
log.
|
|
134
|
+
log.debug(`Migration read failed (key: ${String(key)}). ` + `File: ${config.path}. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
135
135
|
return undefined;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -148,7 +148,7 @@ function safeGet(config, key) {
|
|
|
148
148
|
projectName: params.projectName
|
|
149
149
|
});
|
|
150
150
|
} catch (error) {
|
|
151
|
-
log.
|
|
151
|
+
log.debug(`Legacy token store could not be opened; skipping migration and leaving file intact. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
154
|
}
|
package/dist/auth/token-store.js
CHANGED
|
@@ -51,7 +51,7 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
|
|
|
51
51
|
// Resolve the legacy root dir (matching Conf's env-paths convention)
|
|
52
52
|
// without instantiating Conf, which would read/parse the encrypted file
|
|
53
53
|
// and could wipe it under clearInvalidConfig.
|
|
54
|
-
this.legacyRootDir = envPaths(projectName).config;
|
|
54
|
+
this.legacyRootDir = options?.rootDir ?? envPaths(projectName).config;
|
|
55
55
|
const legacyFilePath = path.join(this.legacyRootDir, `${baseConfigName}.json`);
|
|
56
56
|
const newOAuthCwd = this.buildOAuthStoreCwd();
|
|
57
57
|
const newOAuthFilePath = path.join(newOAuthCwd, `${this.buildOAuthConfigName()}.json`);
|
|
@@ -84,7 +84,7 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
|
|
|
84
84
|
if (environment !== 'production') {
|
|
85
85
|
const oldEnvProjectName = `payloadcms-figma-${environment}`;
|
|
86
86
|
const oldEnvConfigName = options?.configName || oldEnvProjectName;
|
|
87
|
-
const oldEnvLegacyDir = envPaths(oldEnvProjectName).config;
|
|
87
|
+
const oldEnvLegacyDir = options?.oldEnvLegacyDir ?? envPaths(oldEnvProjectName).config;
|
|
88
88
|
const oldEnvLegacyFilePath = path.join(oldEnvLegacyDir, `${oldEnvConfigName}.json`);
|
|
89
89
|
if (needsMigration({
|
|
90
90
|
legacyFilePath: oldEnvLegacyFilePath,
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -113,6 +113,10 @@ export type TokenStoreConfig = {
|
|
|
113
113
|
encryptionKey?: string;
|
|
114
114
|
/** Environment for config file naming */
|
|
115
115
|
environment?: Environment;
|
|
116
|
+
/** Override for the old per-environment legacy directory (primarily for tests). */
|
|
117
|
+
oldEnvLegacyDir?: string;
|
|
118
|
+
/** Override for the storage root directory (primarily for tests). */
|
|
119
|
+
rootDir?: string;
|
|
116
120
|
};
|
|
117
121
|
/**
|
|
118
122
|
* PKCE (Proof Key for Code Exchange) pair for OAuth2 public clients
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import arg from 'arg';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
+
import { bootstrapCommand } from './commands/bootstrap.js';
|
|
4
5
|
import { buildLambdaZipCommand } from './commands/build-lambda-zip.js';
|
|
5
6
|
import { debugCommand } from './commands/debug.js';
|
|
6
7
|
import { deployCommand } from './commands/deploy.js';
|
|
@@ -42,6 +43,7 @@ class Main {
|
|
|
42
43
|
'--help': Boolean,
|
|
43
44
|
'--id': String,
|
|
44
45
|
'--infra-env': String,
|
|
46
|
+
'--json': Boolean,
|
|
45
47
|
'--name': String,
|
|
46
48
|
'--skip-auth': Boolean,
|
|
47
49
|
'--skip-build': Boolean,
|
|
@@ -112,6 +114,13 @@ class Main {
|
|
|
112
114
|
p.intro(pc.bgCyan(pc.black(' @payloadcms/figma ')));
|
|
113
115
|
// Route to appropriate command handler
|
|
114
116
|
switch(subcommand){
|
|
117
|
+
case 'bootstrap':
|
|
118
|
+
await bootstrapCommand({
|
|
119
|
+
id: this.args['--id'],
|
|
120
|
+
env: this.args['--env'],
|
|
121
|
+
json: this.args['--json']
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
115
124
|
case 'build-lambda-zip':
|
|
116
125
|
await buildLambdaZipCommand();
|
|
117
126
|
break;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type BootstrapCommandOptions = {
|
|
2
|
+
/** Filter to a single environment (e.g. "production", "staging"). */
|
|
3
|
+
env?: string;
|
|
4
|
+
/** CMS Resource ID (FIGMA_PROJECT_ID, e.g. cms_…). */
|
|
5
|
+
id?: string;
|
|
6
|
+
/** Emit plain JSON instead of a styled note (handy for piping into jq). */
|
|
7
|
+
json?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Fetch and display bootstrap info for a CMS resource.
|
|
11
|
+
*
|
|
12
|
+
* Calls the Control Plane bootstrap endpoint with the current OAuth credential
|
|
13
|
+
* and prints the resulting environments (`contentSystemId`, `tenantInstanceId`)
|
|
14
|
+
* plus OAuth client credentials. Useful for grabbing a tenant ID without having
|
|
15
|
+
* to `init` a project.
|
|
16
|
+
*/
|
|
17
|
+
export declare function bootstrapCommand(options?: BootstrapCommandOptions): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { ControlPlaneError, getBootstrapInfo } from '../api/control-plane.js';
|
|
4
|
+
import { getValidCredential } from '../auth/oauth-flow.js';
|
|
5
|
+
import { getTokenStore } from '../auth/token-store.js';
|
|
6
|
+
import { getInfraEnvironment } from '../constants.js';
|
|
7
|
+
import { isDebug } from '../utils/is-debug.js';
|
|
8
|
+
import { maskToken } from '../utils/token-display.js';
|
|
9
|
+
/**
|
|
10
|
+
* Fetch and display bootstrap info for a CMS resource.
|
|
11
|
+
*
|
|
12
|
+
* Calls the Control Plane bootstrap endpoint with the current OAuth credential
|
|
13
|
+
* and prints the resulting environments (`contentSystemId`, `tenantInstanceId`)
|
|
14
|
+
* plus OAuth client credentials. Useful for grabbing a tenant ID without having
|
|
15
|
+
* to `init` a project.
|
|
16
|
+
*/ export async function bootstrapCommand(options = {}) {
|
|
17
|
+
if (!options.id) {
|
|
18
|
+
p.log.error(pc.red('--id <project-id> is required'));
|
|
19
|
+
p.note('The CMS Resource ID (FIGMA_PROJECT_ID), e.g. cms_abc123', 'Tip');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const tokenStore = getTokenStore(getInfraEnvironment());
|
|
23
|
+
let credential;
|
|
24
|
+
try {
|
|
25
|
+
credential = await getValidCredential(tokenStore);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
p.log.error(pc.red(`Failed to read stored credentials: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
28
|
+
p.note(`Run ${pc.cyan(`@payloadcms/figma logout --infra-env ${getInfraEnvironment()} -y`)} ` + `then ${pc.cyan(`@payloadcms/figma login --infra-env ${getInfraEnvironment()}`)} and retry.`, 'Re-authenticate');
|
|
29
|
+
if (isDebug() && error instanceof Error) {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.error(error);
|
|
32
|
+
}
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (!credential) {
|
|
36
|
+
p.log.error(pc.red('Not authenticated.'));
|
|
37
|
+
p.note(`Run ${pc.cyan('@payloadcms/figma login')} first`, 'Tip');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
let info;
|
|
41
|
+
try {
|
|
42
|
+
info = await getBootstrapInfo(credential, options.id, options.env);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof ControlPlaneError) {
|
|
45
|
+
p.log.error(pc.red(`Bootstrap fetch failed: ${error.statusCode} ${error.message}`));
|
|
46
|
+
if (error.statusCode === 401) {
|
|
47
|
+
p.note(`Stored credentials for infra env ${pc.bold(getInfraEnvironment())} were rejected.\n` + `Run ${pc.cyan(`@payloadcms/figma logout --infra-env ${getInfraEnvironment()} -y`)} ` + `then ${pc.cyan(`@payloadcms/figma login --infra-env ${getInfraEnvironment()}`)} and retry.`, 'Re-authenticate');
|
|
48
|
+
} else if (error.statusCode === 404) {
|
|
49
|
+
p.note(`Resource ${pc.bold(options.id)} was not found in infra env ${pc.bold(getInfraEnvironment())}.\n` + `Try ${pc.cyan('--infra-env staging')} (or production) to target a different infra.`, 'Wrong infra environment?');
|
|
50
|
+
}
|
|
51
|
+
} else if (error instanceof Error) {
|
|
52
|
+
const causeMessage = error.cause instanceof Error ? `: ${error.cause.message}` : '';
|
|
53
|
+
p.log.error(pc.red(`${error.name}: ${error.message}${causeMessage}`));
|
|
54
|
+
if (isDebug()) {
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.error(error);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
p.log.error(pc.red('Unknown error'));
|
|
60
|
+
}
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (options.json) {
|
|
64
|
+
const showSecretsInJson = isDebug();
|
|
65
|
+
const payload = showSecretsInJson ? info : {
|
|
66
|
+
...info,
|
|
67
|
+
oauthCredentials: {
|
|
68
|
+
...info.oauthCredentials,
|
|
69
|
+
oauthClientSecret: maskToken(info.oauthCredentials.oauthClientSecret)
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const showSecret = isDebug();
|
|
77
|
+
const lines = [
|
|
78
|
+
`Project ID: ${options.id}`,
|
|
79
|
+
`OAuth Client ID: ${info.oauthCredentials.oauthClientId}`,
|
|
80
|
+
`OAuth Client Secret: ${showSecret ? info.oauthCredentials.oauthClientSecret : maskToken(info.oauthCredentials.oauthClientSecret)}`,
|
|
81
|
+
'',
|
|
82
|
+
'Environments:'
|
|
83
|
+
];
|
|
84
|
+
for (const env of info.environments){
|
|
85
|
+
lines.push(` ${pc.bold(env.name)}`, ` Tenant ID (contentSystemId): ${env.contentSystemId}`, ` Tenant Instance ID: ${env.tenantInstanceId}`);
|
|
86
|
+
}
|
|
87
|
+
p.note(lines.join('\n'), 'Bootstrap Info');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=bootstrap.js.map
|
package/dist/commands/init.js
CHANGED
|
@@ -225,6 +225,18 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
225
225
|
p.note('Run this command in a directory without an existing project or in an existing Payload project.', 'Action Required');
|
|
226
226
|
process.exit(1);
|
|
227
227
|
}
|
|
228
|
+
// npm leaves node_modules in a transient state after a sequence of
|
|
229
|
+
// add/remove calls; without a final `install` the subprocess that runs
|
|
230
|
+
// generate:importmap silently misses @payloadcms/figma subpath components.
|
|
231
|
+
s.start('Reconciling dependencies...');
|
|
232
|
+
try {
|
|
233
|
+
await installDependencies(process.cwd(), packageManager);
|
|
234
|
+
s.stop(pc.green('✓ Dependencies reconciled'));
|
|
235
|
+
} catch (error) {
|
|
236
|
+
s.stop(pc.red('✗ Failed to reconcile dependencies before generate:importmap'));
|
|
237
|
+
log.error(error instanceof Error ? error.message : 'Unknown error');
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
228
240
|
// Generate import map to prevent errors on first dev run
|
|
229
241
|
const importMapResult = await runScript(process.cwd(), 'generate:importmap', packageManager);
|
|
230
242
|
if (!importMapResult) {
|
|
@@ -240,7 +252,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
240
252
|
]);
|
|
241
253
|
// Generate project token (unless skipping auth)
|
|
242
254
|
if (!options.skipAuth && resolvedEnv) {
|
|
243
|
-
await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s
|
|
255
|
+
await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s, {
|
|
256
|
+
environmentName: resolvedEnv.name,
|
|
257
|
+
projectId: cmsResourceId
|
|
258
|
+
});
|
|
244
259
|
}
|
|
245
260
|
// Success message for existing project
|
|
246
261
|
p.outro(pc.green('✓ Project initialized successfully!'));
|
|
@@ -339,6 +354,18 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
339
354
|
} catch (error) {
|
|
340
355
|
p.log.warn(pc.yellow(`⚠ Failed to update .env file: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
341
356
|
}
|
|
357
|
+
// npm leaves node_modules in a transient state after a sequence of
|
|
358
|
+
// add/remove calls; without a final `install` the subprocess that runs
|
|
359
|
+
// generate:importmap silently misses @payloadcms/figma subpath components.
|
|
360
|
+
s.start('Reconciling dependencies...');
|
|
361
|
+
try {
|
|
362
|
+
await installDependencies(fullPath, packageManager);
|
|
363
|
+
s.stop(pc.green('✓ Dependencies reconciled'));
|
|
364
|
+
} catch (error) {
|
|
365
|
+
s.stop(pc.red('✗ Failed to reconcile dependencies before generate:importmap'));
|
|
366
|
+
log.error(error instanceof Error ? error.message : 'Unknown error');
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
342
369
|
// Generate import map to prevent errors on first dev run
|
|
343
370
|
const importMapResult = await runScript(fullPath, 'generate:importmap', packageManager);
|
|
344
371
|
if (!importMapResult) {
|
|
@@ -346,7 +373,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
346
373
|
}
|
|
347
374
|
// Generate project token (unless skipping auth)
|
|
348
375
|
if (!options.skipAuth && resolvedEnv) {
|
|
349
|
-
await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s
|
|
376
|
+
await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s, {
|
|
377
|
+
environmentName: resolvedEnv.name,
|
|
378
|
+
projectId: cmsResourceId
|
|
379
|
+
});
|
|
350
380
|
}
|
|
351
381
|
// Initialize git repository (after all files including lock file are ready)
|
|
352
382
|
initializeGitRepo(fullPath);
|