@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.
@@ -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
- * This function:
21
- * 1. Checks if a stored project token exists and is valid (not expired)
22
- * 2. If expired or missing, gets a valid user access token
23
- * 3. Calls the Figma API to generate a new project token
24
- * 4. Stores the new project token in encrypted storage
25
- * 5. Returns the JWT string
22
+ * Lookup order:
23
+ * 1. Module-level in-memory cache (no I/O on hits).
24
+ * 2. In-flight refresh map concurrent callers with no cached entry share a single
25
+ * refresh promise, so the project-token endpoint is only hit once per cache key.
26
+ * 3. Persisted (`conf`-backed) token store picks up tokens minted by sibling processes.
27
+ * 4. Figma API — fetch + JWT validation, then write to both the persisted store and
28
+ * the in-memory cache.
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
- * Refresh a project token for a tenant
45
+ * Force-refresh a project token, bypassing all cache layers.
43
46
  *
44
- * Forces a refresh of the project token even if the current one is still valid.
45
- * Useful for testing or when you want to ensure you have the freshest token.
47
+ * Invalidates the in-memory cache entry, the in-flight refresh promise (so a
48
+ * concurrently-running refresh that may already be returning a soon-to-be-stale
49
+ * token is dropped from the dedup map), and the persisted token store. Then
50
+ * delegates to {@link getValidProjectToken} to mint a fresh token.
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
- * This function:
49
- * 1. Checks if a stored project token exists and is valid (not expired)
50
- * 2. If expired or missing, gets a valid user access token
51
- * 3. Calls the Figma API to generate a new project token
52
- * 4. Stores the new project token in encrypted storage
53
- * 5. Returns the JWT string
64
+ * Lookup order:
65
+ * 1. Module-level in-memory cache (no I/O on hits).
66
+ * 2. In-flight refresh map concurrent callers with no cached entry share a single
67
+ * refresh promise, so the project-token endpoint is only hit once per cache key.
68
+ * 3. Persisted (`conf`-backed) token store picks up tokens minted by sibling processes.
69
+ * 4. Figma API — fetch + JWT validation, then write to both the persisted store and
70
+ * the in-memory cache.
54
71
  *
55
72
  * The project token is used by local Payload instances to authenticate
56
73
  * requests to the Content API without requiring a round-trip to Sinatra.
@@ -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
- // Check for existing valid project token
75
- if (tokenStore.hasValidProjectToken({
76
- projectInfo
77
- })) {
78
- const projectToken = tokenStore.getProjectToken({
79
- projectInfo
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
- if (!credential) {
92
- // No valid user tokens available - user needs to authenticate
93
- return null;
99
+ const inFlight = inFlightRefreshes.get(cacheKey);
100
+ if (inFlight) {
101
+ return inFlight;
94
102
  }
95
- // Fetch a new project token from the Figma API
96
- const fetchedToken = await fetchProjectToken(credential, resolvedTenantId);
97
- // Validate the JWT signature and claims (skip for mocks)
98
- const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE;
99
- const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
100
- let validatedClaims;
101
- if (hasEnvToken || shouldMock) {
102
- // MOCK: Skip JWT validation and parse claims directly
103
- log.debug('Skipping JWT validation for mock/env project token');
104
- validatedClaims = parseJWTClaims(fetchedToken.token);
105
- } else {
106
- // REAL: Validate JWT signature against JWKS
103
+ // The IIFE references this holder so it can check whether it is still the
104
+ // active refresh before writing back to the caches. Without this guard a
105
+ // stale in-flight refresh could clobber a fresh token written by a later
106
+ // refresh (e.g. one triggered by `refreshProjectToken`).
107
+ const handle = {};
108
+ handle.promise = (async ()=>{
109
+ if (tokenStore.hasValidProjectToken({
110
+ projectInfo
111
+ })) {
112
+ const stored = tokenStore.getProjectToken({
113
+ projectInfo
114
+ });
115
+ if (stored?.token && inFlightRefreshes.get(cacheKey) === handle.promise) {
116
+ projectTokenCache.set(cacheKey, {
117
+ expiresAt: stored.expiresAt,
118
+ token: stored.token
119
+ });
120
+ return stored.token;
121
+ }
122
+ if (stored?.token) {
123
+ return stored.token;
124
+ }
125
+ // hasValidProjectToken said yes but getProjectToken returned no token —
126
+ // store was likely cleared by another process between calls. Fall through
127
+ // to fetch so the caller still gets a token.
128
+ log.warning(`Stored project token vanished between hasValidProjectToken and getProjectToken (cacheKey=${cacheKey}); re-fetching`);
129
+ }
130
+ let credential;
131
+ try {
132
+ credential = await getValidCredential(tokenStore);
133
+ } catch (error) {
134
+ throw new ProjectTokenError('Failed to get valid access token for project token refresh', error instanceof Error ? error : undefined);
135
+ }
136
+ if (!credential) {
137
+ return null;
138
+ }
139
+ let fetchedToken;
107
140
  try {
108
- validatedClaims = await validateProjectToken(fetchedToken.token, getInfraEnvironment());
109
- } catch (validationError) {
110
- // Handle validation errors
111
- if (validationError instanceof JWTValidationError) {
112
- throw new ProjectTokenError(`Project token validation failed: ${validationError.message}`, validationError);
141
+ fetchedToken = await fetchProjectToken(credential, resolvedTenantId);
142
+ } catch (error) {
143
+ // Let API errors (e.g. FigmaApiError) propagate raw so callers can
144
+ // branch on status codes — but log context for debugging.
145
+ log.debug(`fetchProjectToken failed (cacheKey=${cacheKey}, tenantId=${resolvedTenantId}): ${error instanceof Error ? error.message : 'Unknown error'}`);
146
+ throw error;
147
+ }
148
+ const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE;
149
+ const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
150
+ let validatedClaims;
151
+ if (hasEnvToken || shouldMock) {
152
+ log.debug('Skipping JWT validation for mock/env project token');
153
+ validatedClaims = parseJWTClaims(fetchedToken.token);
154
+ } else {
155
+ try {
156
+ validatedClaims = await validateProjectToken(fetchedToken.token, getInfraEnvironment());
157
+ } catch (validationError) {
158
+ if (validationError instanceof JWTValidationError) {
159
+ throw new ProjectTokenError(`Project token validation failed: ${validationError.message}`, validationError);
160
+ }
161
+ throw validationError;
113
162
  }
114
- throw validationError;
115
163
  }
116
- }
117
- // Ensure exp claim is present
118
- if (!validatedClaims.exp) {
119
- throw new ProjectTokenError('Project token missing expiration (exp) claim');
120
- }
121
- // Store the project token with validated expiration and claims
122
- tokenStore.setProjectToken({
123
- projectInfo,
124
- token: {
125
- claims: validatedClaims,
126
- expiresAt: validatedClaims.exp * 1000,
127
- tenantId: resolvedTenantId,
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
- * Refresh a project token for a tenant
204
+ * Force-refresh a project token, bypassing all cache layers.
135
205
  *
136
- * Forces a refresh of the project token even if the current one is still valid.
137
- * Useful for testing or when you want to ensure you have the freshest token.
206
+ * Invalidates the in-memory cache entry, the in-flight refresh promise (so a
207
+ * concurrently-running refresh that may already be returning a soon-to-be-stale
208
+ * token is dropped from the dedup map), and the persisted token store. Then
209
+ * delegates to {@link getValidProjectToken} to mint a fresh token.
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
- // Clear the existing project token to force a refresh
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.warning(`Migration read failed (key: ${String(key)}). ` + `File: ${config.path}. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
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.warning(`Legacy token store could not be opened; skipping migration and leaving file intact. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
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
  }
@@ -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,
@@ -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
@@ -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);