@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.
Files changed (35) hide show
  1. package/dist/auth/crypto-utils.d.ts +11 -0
  2. package/dist/auth/crypto-utils.js +51 -0
  3. package/dist/auth/oauth-flow.js +3 -3
  4. package/dist/auth/project-token.d.ts +17 -6
  5. package/dist/auth/project-token.js +37 -19
  6. package/dist/auth/token-store-migration.d.ts +58 -0
  7. package/dist/auth/token-store-migration.js +156 -0
  8. package/dist/auth/token-store.d.ts +51 -128
  9. package/dist/auth/token-store.js +326 -187
  10. package/dist/auth/types.d.ts +8 -1
  11. package/dist/cli.js +6 -0
  12. package/dist/commands/debug.js +16 -12
  13. package/dist/commands/dump-tokens.d.ts +12 -0
  14. package/dist/commands/dump-tokens.js +69 -0
  15. package/dist/commands/init.js +7 -3
  16. package/dist/commands/list-tokens.js +2 -2
  17. package/dist/commands/login.js +1 -1
  18. package/dist/commands/logout.js +23 -4
  19. package/dist/db-content-api/utilities/auth.js +4 -1
  20. package/dist/oauth/defaults.d.ts +3 -1
  21. package/dist/oauth/defaults.js +6 -5
  22. package/dist/oauth/endpoints/getLoginEndpoint.js +1 -1
  23. package/dist/oauth/index.js +1 -0
  24. package/dist/oauth/types.d.ts +10 -0
  25. package/dist/plugin/auth-preflight.d.ts +8 -0
  26. package/dist/plugin/auth-preflight.js +20 -0
  27. package/dist/plugin/bootstrap-preflight.d.ts +23 -0
  28. package/dist/plugin/bootstrap-preflight.js +45 -0
  29. package/dist/plugin/build-config.js +73 -12
  30. package/dist/plugin/dev-cookie-names.d.ts +14 -0
  31. package/dist/plugin/dev-cookie-names.js +19 -0
  32. package/dist/storage-content-api/client.js +4 -1
  33. package/dist/utils/token-display.d.ts +5 -1
  34. package/dist/utils/token-display.js +47 -26
  35. package/package.json +2 -1
@@ -36,14 +36,14 @@ function appendSystemSection(lines, env) {
36
36
  lines.push('');
37
37
  }
38
38
  function appendAuthSection(lines, tokenStore, cwd) {
39
- const tokens = tokenStore.getTokens();
39
+ const tokens = tokenStore.getOauthInfo();
40
40
  lines.push('Auth:');
41
41
  if (!tokens) {
42
42
  lines.push(' Status: Not authenticated');
43
43
  lines.push('');
44
44
  return;
45
45
  }
46
- const isValid = tokenStore.hasValidTokens();
46
+ const isValid = tokenStore.hasValidOauthToken();
47
47
  lines.push(` Status: ${isValid ? 'Authenticated' : 'Token expired'}`);
48
48
  const email = getEmailFromClaims(tokenStore, cwd);
49
49
  if (email) {
@@ -62,7 +62,7 @@ function appendTokenStoreDiagnostics(lines, tokenStore) {
62
62
  lines.push(` File: ${filePath}`);
63
63
  lines.push(` File Exists: ${fileExists}`);
64
64
  lines.push(` File Size: ${fileSize}B`);
65
- const canRead = tokenStore.getTokens() !== null || tokenStore.getRefreshToken() !== null;
65
+ const canRead = tokenStore.getOauthInfo() !== null || tokenStore.getCurrentRefreshToken() !== null;
66
66
  lines.push(` Decryption: ${fileExists ? canRead || fileSize === 0 ? 'OK' : 'FAILED' : 'N/A'}`);
67
67
  lines.push(` Key Hash: ${getEncryptionKeyHash()}`);
68
68
  lines.push('');
@@ -85,7 +85,12 @@ function appendProjectSection(lines, tokenStore, cwd) {
85
85
  lines.push(` Content System ID: ${bootstrapData.contentSystemId}`);
86
86
  lines.push(` Tenant Instance ID: ${bootstrapData.tenantInstanceId}`);
87
87
  lines.push(` OAuth Client ID: ${maskToken(bootstrapData.oauthClientId)}`);
88
- const projectToken = tokenStore.getProjectToken(bootstrapData.contentSystemId);
88
+ const projectToken = tokenStore.getProjectToken({
89
+ projectInfo: {
90
+ environmentName,
91
+ projectId
92
+ }
93
+ });
89
94
  if (projectToken) {
90
95
  lines.push(` Project Token: ${formatExpiryTime(projectToken.expiresAt)}`);
91
96
  }
@@ -103,25 +108,24 @@ async function appendPackagesSection(lines) {
103
108
  lines.push(` payload: ${payloadVersion}`);
104
109
  }
105
110
  /**
106
- * Try resolving email from bootstrap data, then iterate all stored project tokens.
111
+ * Resolve email from the current project + environment scoped token when available.
107
112
  */ function getEmailFromClaims(tokenStore, cwd) {
108
113
  const projectId = getEnvVarSync(cwd, 'FIGMA_PROJECT_ID');
109
114
  const environmentName = getEnvVarSync(cwd, 'FIGMA_ENVIRONMENT_NAME');
110
115
  if (projectId && environmentName) {
111
116
  const bootstrapData = tokenStore.getBootstrapData(projectId, environmentName);
112
117
  if (bootstrapData) {
113
- const email = tokenStore.getProjectTokenClaims(bootstrapData.contentSystemId)?.email;
118
+ const email = tokenStore.getProjectToken({
119
+ projectInfo: {
120
+ environmentName,
121
+ projectId
122
+ }
123
+ })?.claims?.email;
114
124
  if (email) {
115
125
  return email;
116
126
  }
117
127
  }
118
128
  }
119
- for (const id of tokenStore.getAllProjectTokenTenantIds()){
120
- const email = tokenStore.getProjectTokenClaims(id)?.email;
121
- if (email) {
122
- return email;
123
- }
124
- }
125
129
  return null;
126
130
  }
127
131
  function getPayloadVersion() {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Handle the `@payloadcms/figma dump-tokens` command
3
+ *
4
+ * Exports decrypted versions of encrypted token store files in
5
+ * `oauth/` and `projects/` subdirectories (new structure) plus any legacy
6
+ * per-env directories from older package versions. Creates
7
+ * `.decrypted.json` files next to each encrypted `.json` file.
8
+ *
9
+ * Skips the `payloadcms-figma-crypto.json` salt file at the root.
10
+ */
11
+ export declare function dumpTokensCommand(): Promise<void>;
12
+ //# sourceMappingURL=dump-tokens.d.ts.map
@@ -0,0 +1,69 @@
1
+ import * as p from '@clack/prompts';
2
+ import envPaths from 'env-paths';
3
+ import fsSync from 'node:fs';
4
+ import path from 'node:path';
5
+ import pc from 'picocolors';
6
+ import { deriveEncryptionKey, exportDecryptedStoreFiles } from '../auth/crypto-utils.js';
7
+ /**
8
+ * Handle the `@payloadcms/figma dump-tokens` command
9
+ *
10
+ * Exports decrypted versions of encrypted token store files in
11
+ * `oauth/` and `projects/` subdirectories (new structure) plus any legacy
12
+ * per-env directories from older package versions. Creates
13
+ * `.decrypted.json` files next to each encrypted `.json` file.
14
+ *
15
+ * Skips the `payloadcms-figma-crypto.json` salt file at the root.
16
+ */ export async function dumpTokensCommand() {
17
+ const encryptionKey = deriveEncryptionKey();
18
+ const newRoot = envPaths('payloadcms-figma').config;
19
+ const stagingLegacyRoot = envPaths('payloadcms-figma-staging').config;
20
+ const candidateDirs = [
21
+ path.join(newRoot, 'oauth'),
22
+ path.join(newRoot, 'projects'),
23
+ stagingLegacyRoot
24
+ ];
25
+ const targetDirs = Array.from(new Set(candidateDirs)).filter((dir)=>fsSync.existsSync(dir));
26
+ if (targetDirs.length === 0) {
27
+ // eslint-disable-next-line no-console
28
+ console.log('No token store directories found.');
29
+ return;
30
+ }
31
+ // eslint-disable-next-line no-console
32
+ console.log(pc.yellow('⚠ dump-tokens writes DECRYPTED token files to disk.'));
33
+ // eslint-disable-next-line no-console
34
+ console.log('Target directories:');
35
+ for (const dir of targetDirs){
36
+ // eslint-disable-next-line no-console
37
+ console.log(` ${dir}`);
38
+ }
39
+ // eslint-disable-next-line no-console
40
+ console.log('');
41
+ const confirmed = await p.confirm({
42
+ initialValue: false,
43
+ message: 'Continue? Remember to delete the decrypted files after use.'
44
+ });
45
+ if (p.isCancel(confirmed) || !confirmed) {
46
+ // eslint-disable-next-line no-console
47
+ console.log('Cancelled.');
48
+ return;
49
+ }
50
+ const decryptedFiles = [];
51
+ for (const dir of targetDirs){
52
+ decryptedFiles.push(...exportDecryptedStoreFiles(dir, encryptionKey));
53
+ }
54
+ if (decryptedFiles.length === 0) {
55
+ // eslint-disable-next-line no-console
56
+ console.log('No encrypted files could be decrypted with the current machine key.');
57
+ return;
58
+ }
59
+ // eslint-disable-next-line no-console
60
+ console.log(`\nCreated ${decryptedFiles.length} decrypted file(s):`);
61
+ for (const file of decryptedFiles){
62
+ // eslint-disable-next-line no-console
63
+ console.log(` ${file}`);
64
+ }
65
+ // eslint-disable-next-line no-console
66
+ console.log('\nNote: These files contain sensitive tokens. Delete after use.');
67
+ }
68
+
69
+ //# sourceMappingURL=dump-tokens.js.map
@@ -29,12 +29,16 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
29
29
  * @param tokenStore - Token store instance
30
30
  * @param contentSystemId - Content System ID to generate token for
31
31
  * @param spinner - Clack spinner instance for status updates (only used in debug mode)
32
- */ async function generateProjectTokenWithFeedback(tokenStore, contentSystemId, spinner) {
32
+ */ async function generateProjectTokenWithFeedback(tokenStore, contentSystemId, spinner, projectInfo) {
33
33
  if (isDebug()) {
34
34
  spinner.start('Generating project token...');
35
35
  }
36
36
  try {
37
- const projectToken = await getValidProjectToken(tokenStore, contentSystemId);
37
+ const projectToken = await getValidProjectToken({
38
+ projectInfo,
39
+ tenantId: contentSystemId,
40
+ tokenStore
41
+ });
38
42
  if (isDebug()) {
39
43
  if (projectToken) {
40
44
  spinner.stop(pc.green('✓ Project token generated'));
@@ -78,7 +82,7 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
78
82
  p.log.warn(pc.yellow('Skipping authentication (--skip-auth mode)'));
79
83
  } else {
80
84
  // Check for valid cached tokens first (no API call needed)
81
- if (!tokenStore.hasValidTokens()) {
85
+ if (!tokenStore.hasValidOauthToken()) {
82
86
  // Need to refresh or authenticate - show spinner for API call
83
87
  s.start('Checking authentication...');
84
88
  const credential = await tryGetCredential(tokenStore);
@@ -26,13 +26,13 @@ const ALL_ENVIRONMENTS = [
26
26
  }
27
27
  function showTokenDetails(env) {
28
28
  const tokenStore = getTokenStore(env);
29
- const tokens = tokenStore.getTokens();
29
+ const tokens = tokenStore.getOauthInfo();
30
30
  if (!tokens) {
31
31
  p.log.warn(pc.yellow(`No tokens found${formatEnvSuffix(env, ' for ')}. Not authenticated.`));
32
32
  p.note(`Run ${pc.cyan('@payloadcms/figma login')} to authenticate`, 'Tip');
33
33
  return;
34
34
  }
35
- const isValid = tokenStore.hasValidTokens();
35
+ const isValid = tokenStore.hasValidOauthToken();
36
36
  const status = isValid ? pc.green('✓ Valid') : pc.red('✗ Expired');
37
37
  p.log.step(pc.bold(`Token Status${formatEnvSuffix(env, ' (', ')')}`));
38
38
  // Build OAuth token info display
@@ -22,7 +22,7 @@ import * as log from '../utils/log.js';
22
22
  try {
23
23
  const existingCredential = await getValidCredential(tokenStore);
24
24
  if (existingCredential) {
25
- const tokens = tokenStore.getTokens();
25
+ const tokens = tokenStore.getOauthInfo();
26
26
  p.log.warn(pc.yellow(`Already logged in${formatEnvSuffix(env)}`));
27
27
  if (tokens?.userId) {
28
28
  log.info(`User ID: ${tokens.userId}`);
@@ -2,6 +2,7 @@ import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
3
  import { getTokenStore } from '../auth/token-store.js';
4
4
  import { getInfraEnvironment } from '../constants.js';
5
+ import { getEnvVarSync } from '../utils/env-management.js';
5
6
  import { formatEnvSuffix } from '../utils/format-env-suffix.js';
6
7
  import * as log from '../utils/log.js';
7
8
  const ALL_ENVIRONMENTS = [
@@ -23,7 +24,7 @@ const ALL_ENVIRONMENTS = [
23
24
  }
24
25
  async function logoutSingleEnvironment(env) {
25
26
  const tokenStore = getTokenStore(env);
26
- const hasTokens = tokenStore.getTokens() !== null;
27
+ const hasTokens = tokenStore.getOauthInfo() !== null;
27
28
  if (!hasTokens) {
28
29
  p.log.warn(pc.yellow(`No tokens found${formatEnvSuffix(env, ' for ')}. Already logged out.`));
29
30
  return;
@@ -40,7 +41,7 @@ async function logoutSingleEnvironment(env) {
40
41
  s.start('Clearing tokens...');
41
42
  try {
42
43
  tokenStore.clearTokens();
43
- tokenStore.clearAllProjectTokens();
44
+ clearCurrentProjectToken(tokenStore, env);
44
45
  s.stop(pc.green(`✓ Logged out${formatEnvSuffix(env, ' from ')}`));
45
46
  } catch (error) {
46
47
  s.stop(pc.red('✗ Failed to clear tokens'));
@@ -52,7 +53,7 @@ async function logoutAllEnvironments() {
52
53
  // Check which environments have tokens
53
54
  const envsWithTokens = ALL_ENVIRONMENTS.filter((env)=>{
54
55
  const store = getTokenStore(env);
55
- return store.getTokens() !== null;
56
+ return store.getOauthInfo() !== null;
56
57
  });
57
58
  if (envsWithTokens.length === 0) {
58
59
  p.log.warn(pc.yellow('No tokens found in any environment. Already logged out.'));
@@ -73,7 +74,7 @@ async function logoutAllEnvironments() {
73
74
  for (const env of envsWithTokens){
74
75
  const store = getTokenStore(env);
75
76
  store.clearTokens();
76
- store.clearAllProjectTokens();
77
+ clearCurrentProjectToken(store, env);
77
78
  }
78
79
  s.stop(pc.green('✓ Logged out from all environments'));
79
80
  } catch (error) {
@@ -82,5 +83,23 @@ async function logoutAllEnvironments() {
82
83
  process.exit(1);
83
84
  }
84
85
  }
86
+ function clearCurrentProjectToken(store, env) {
87
+ const projectId = getEnvVarSync(process.cwd(), 'FIGMA_PROJECT_ID');
88
+ if (!projectId) {
89
+ return;
90
+ }
91
+ const bootstrapData = store.getBootstrapData(projectId, env);
92
+ if (!bootstrapData?.contentSystemId) {
93
+ return;
94
+ }
95
+ const projectInfo = {
96
+ environmentName: env,
97
+ projectId
98
+ };
99
+ // Clear scoped token for this tenant.
100
+ store.clearProjectToken({
101
+ projectInfo
102
+ });
103
+ }
85
104
 
86
105
  //# sourceMappingURL=logout.js.map
@@ -25,7 +25,10 @@ export function createAuthMiddleware(opts) {
25
25
  if (opts.auth.mode === 'apiKey') {
26
26
  request.headers.set('X-Api-Key', opts.auth.apiKey);
27
27
  } else if (opts.auth.mode === 'tokenStore') {
28
- const token = await getValidProjectToken(opts.auth.tokenStore, opts.contentSystemId);
28
+ const token = await getValidProjectToken({
29
+ tenantId: opts.contentSystemId,
30
+ tokenStore: opts.auth.tokenStore
31
+ });
29
32
  if (!token) {
30
33
  throw new Error('Authentication required. Run `npx @payloadcms/figma login` to authenticate.');
31
34
  }
@@ -1,10 +1,12 @@
1
1
  import type { CollectionConfig, EmailField, TextField } from 'payload';
2
2
  import type { VerifyFunction } from './types.js';
3
+ export declare const DEFAULT_USER_INFO_COOKIE_NAME = "figma-user-info";
3
4
  export declare const defaultScope: string[];
4
5
  export declare const defaultUsernameField: TextField;
5
- export declare const defaultVerify: ({ collection, strategyName, usernameField, }: {
6
+ export declare const defaultVerify: ({ collection, strategyName, userInfoCookieName, usernameField, }: {
6
7
  collection: CollectionConfig;
7
8
  strategyName: string;
9
+ userInfoCookieName?: string;
8
10
  usernameField: EmailField | TextField;
9
11
  }) => VerifyFunction;
10
12
  //# sourceMappingURL=defaults.d.ts.map
@@ -2,10 +2,11 @@ import { parseCookies } from 'payload';
2
2
  import { v4 as uuid } from 'uuid';
3
3
  import { hasUserTokenPropsChanged } from './utilities/hasUserTokenPropsChanged.js';
4
4
  import { isDuplicateKeyError } from './utilities/isDuplicateKeyError.js';
5
- function getFigmaUserInfo(headers) {
5
+ export const DEFAULT_USER_INFO_COOKIE_NAME = 'figma-user-info';
6
+ function getFigmaUserInfo(headers, cookieName) {
6
7
  try {
7
8
  const cookies = parseCookies(headers);
8
- const encoded = cookies.get('figma-user-info');
9
+ const encoded = cookies.get(cookieName);
9
10
  if (!encoded) {
10
11
  return null;
11
12
  }
@@ -28,7 +29,7 @@ export const defaultUsernameField = {
28
29
  },
29
30
  unique: true
30
31
  };
31
- export const defaultVerify = ({ collection, strategyName, usernameField })=>async ({ headers, payload, token })=>{
32
+ export const defaultVerify = ({ collection, strategyName, userInfoCookieName = DEFAULT_USER_INFO_COOKIE_NAME, usernameField })=>async ({ headers, payload, token })=>{
32
33
  let tokenUsername = 'preferred_username';
33
34
  if (usernameField.name !== 'preferredUsername') {
34
35
  tokenUsername = usernameField.name;
@@ -38,12 +39,12 @@ export const defaultVerify = ({ collection, strategyName, usernameField })=>asyn
38
39
  user: null
39
40
  };
40
41
  }
41
- const figmaUserInfo = getFigmaUserInfo(headers);
42
+ const figmaUserInfo = getFigmaUserInfo(headers, userInfoCookieName);
42
43
  let responseHeaders;
43
44
  // Clear the cookie after reading
44
45
  if (figmaUserInfo) {
45
46
  responseHeaders = new Headers();
46
- responseHeaders.append('Set-Cookie', 'figma-user-info=; Max-Age=0; Path=/;');
47
+ responseHeaders.append('Set-Cookie', `${userInfoCookieName}=; Max-Age=0; Path=/;`);
47
48
  }
48
49
  let user = null;
49
50
  const depth = typeof collection.auth === 'object' ? collection.auth.depth : undefined;
@@ -262,7 +262,7 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
262
262
  })).toString('base64');
263
263
  const userInfoCookie = generateCookie({
264
264
  ...cookieOptions,
265
- name: 'figma-user-info',
265
+ name: pluginOptions?.userInfoCookieName ?? 'figma-user-info',
266
266
  expires: new Date(Date.now() + 60_000),
267
267
  returnCookieAsObject: false,
268
268
  value: userInfoValue
@@ -144,6 +144,7 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
144
144
  verify: collectionOptions.verify ?? defaultVerify({
145
145
  collection: existingCollection,
146
146
  strategyName,
147
+ userInfoCookieName: pluginOptions?.userInfoCookieName,
147
148
  usernameField
148
149
  })
149
150
  });
@@ -148,6 +148,16 @@ export interface PluginOptions {
148
148
  * @default 'oauth'
149
149
  */
150
150
  strategyName?: string;
151
+ /**
152
+ * The name of the short-lived cookie used to carry Figma user profile
153
+ * info from the login endpoint to `defaultVerify`.
154
+ *
155
+ * Override in local dev when running multiple Payload apps on localhost
156
+ * to avoid cookie collisions (all localhost apps share a cookie jar).
157
+ *
158
+ * @default 'figma-user-info'
159
+ */
160
+ userInfoCookieName?: string;
151
161
  }
152
162
  export type CookieOptions = {
153
163
  domain?: string;
@@ -0,0 +1,8 @@
1
+ import type { TokenStore } from '../auth/token-store.js';
2
+ /**
3
+ * Print a one-shot banner when the plugin is about to use `tokenStore`
4
+ * auth but no valid OAuth credential is present. Keeps the dev loop
5
+ * actionable instead of relying on per-request middleware errors.
6
+ */
7
+ export declare function logMissingCliAuth(tokenStore: TokenStore): void;
8
+ //# sourceMappingURL=auth-preflight.d.ts.map
@@ -0,0 +1,20 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ /**
4
+ * Print a one-shot banner when the plugin is about to use `tokenStore`
5
+ * auth but no valid OAuth credential is present. Keeps the dev loop
6
+ * actionable instead of relying on per-request middleware errors.
7
+ */ export function logMissingCliAuth(tokenStore) {
8
+ if (tokenStore.hasValidOauthToken()) {
9
+ return;
10
+ }
11
+ const lines = [
12
+ 'Not logged in to the Figma CLI.',
13
+ 'Content API requests will fail until you authenticate.',
14
+ '',
15
+ `Run ${pc.cyan('npx @payloadcms/figma login')} to log in, then restart the dev server.`
16
+ ];
17
+ p.note(lines.join('\n'), pc.yellow('Figma authentication required'));
18
+ }
19
+
20
+ //# sourceMappingURL=auth-preflight.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reason the Content System ID could not be resolved. Drives the
3
+ * banner's copy so the dev sees the specific remediation step.
4
+ */
5
+ export type MissingContentSystemIdReason = {
6
+ environmentName: string;
7
+ kind: 'environment-not-found';
8
+ projectId: string;
9
+ } | {
10
+ kind: 'network';
11
+ message: string;
12
+ } | {
13
+ kind: 'no-config';
14
+ } | {
15
+ kind: 'unauthenticated';
16
+ };
17
+ /**
18
+ * Print a banner explaining why Content System ID resolution failed and
19
+ * what to run next. Called just before `buildFigmaConfig` throws, so the
20
+ * actionable step is visible above the stack trace.
21
+ */
22
+ export declare function logMissingContentSystemId(reason: MissingContentSystemIdReason): void;
23
+ //# sourceMappingURL=bootstrap-preflight.d.ts.map
@@ -0,0 +1,45 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ /**
4
+ * Print a banner explaining why Content System ID resolution failed and
5
+ * what to run next. Called just before `buildFigmaConfig` throws, so the
6
+ * actionable step is visible above the stack trace.
7
+ */ export function logMissingContentSystemId(reason) {
8
+ const lines = buildBody(reason);
9
+ p.note(lines.join('\n'), pc.yellow('Figma project not configured'));
10
+ }
11
+ function buildBody(reason) {
12
+ const loginCmd = pc.cyan('npx @payloadcms/figma login');
13
+ const initCmd = pc.cyan('npx @payloadcms/figma init');
14
+ const initForceCmd = pc.cyan('npx @payloadcms/figma init --force');
15
+ if (reason.kind === 'no-config') {
16
+ return [
17
+ 'No Figma project configuration found.',
18
+ '',
19
+ `Run ${initCmd} to set up this project.`
20
+ ];
21
+ }
22
+ if (reason.kind === 'unauthenticated') {
23
+ return [
24
+ 'Project config found, but you are not logged in.',
25
+ 'Cannot fetch bootstrap data without a valid session.',
26
+ '',
27
+ `Run ${loginCmd}, then restart the dev server.`
28
+ ];
29
+ }
30
+ if (reason.kind === 'network') {
31
+ return [
32
+ 'Could not reach the Figma API to fetch bootstrap data.',
33
+ `Error: ${reason.message}`,
34
+ '',
35
+ 'Check your network, then restart the dev server.'
36
+ ];
37
+ }
38
+ return [
39
+ `Environment ${pc.bold(reason.environmentName)} not found for project ${pc.bold(reason.projectId)}.`,
40
+ '',
41
+ `Run ${initForceCmd} to reconfigure, or set FIGMA_ENVIRONMENT_NAME to a valid environment.`
42
+ ];
43
+ }
44
+
45
+ //# sourceMappingURL=bootstrap-preflight.js.map
@@ -15,7 +15,11 @@ import { createStorageClient } from '../storage-content-api/client.js';
15
15
  import { getGenerateSignedURLHandler } from '../storage-content-api/client-uploads/generateSignedURL.js';
16
16
  import { contentApiStorageAdapter } from '../storage-content-api/index.js';
17
17
  import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
18
+ import { getEnvVarSync } from '../utils/env-management.js';
18
19
  import * as log from '../utils/log.js';
20
+ import { logMissingCliAuth } from './auth-preflight.js';
21
+ import { logMissingContentSystemId } from './bootstrap-preflight.js';
22
+ import { getDevCookieNames } from './dev-cookie-names.js';
19
23
  /**
20
24
  * Fields added to users collection for Figma profile info
21
25
  */ const figmaUserFields = [
@@ -47,20 +51,58 @@ function missingOAuthCredential(name) {
47
51
  }
48
52
  /**
49
53
  * Fetch bootstrap data from the API and cache all environments locally.
50
- * Returns the requested environment's data, or null if resolution fails.
54
+ * Returns the requested environment's data, or a typed failure reason.
51
55
  */ async function resolveAndCacheBootstrap(store, projectId, environmentName) {
56
+ let credential;
57
+ try {
58
+ credential = await getValidCredential(store);
59
+ } catch (error) {
60
+ return {
61
+ ok: false,
62
+ reason: {
63
+ kind: 'network',
64
+ message: messageOf(error)
65
+ }
66
+ };
67
+ }
68
+ if (!credential) {
69
+ return {
70
+ ok: false,
71
+ reason: {
72
+ kind: 'unauthenticated'
73
+ }
74
+ };
75
+ }
52
76
  try {
53
- const credential = await getValidCredential(store);
54
- if (!credential) {
55
- return null;
56
- }
57
77
  const bootstrapInfo = await getBootstrapInfo(credential, projectId);
58
78
  cacheAllEnvironments(store, projectId, bootstrapInfo);
59
- return store.getBootstrapData(projectId, environmentName);
60
79
  } catch (error) {
61
- log.warning(`Failed to resolve bootstrap data: ${error instanceof Error ? error.message : 'Unknown error'}. ` + 'Try running `npx @payloadcms/figma login` to refresh authentication.');
62
- return null;
80
+ return {
81
+ ok: false,
82
+ reason: {
83
+ kind: 'network',
84
+ message: messageOf(error)
85
+ }
86
+ };
63
87
  }
88
+ const data = store.getBootstrapData(projectId, environmentName);
89
+ if (!data) {
90
+ return {
91
+ ok: false,
92
+ reason: {
93
+ environmentName,
94
+ kind: 'environment-not-found',
95
+ projectId
96
+ }
97
+ };
98
+ }
99
+ return {
100
+ data,
101
+ ok: true
102
+ };
103
+ }
104
+ function messageOf(error) {
105
+ return error instanceof Error ? error.message : 'Unknown error';
64
106
  }
65
107
  export async function buildFigmaConfig(config) {
66
108
  const envConfig = getEnvConfig();
@@ -68,16 +110,27 @@ export async function buildFigmaConfig(config) {
68
110
  let { contentSystemId } = config.figma;
69
111
  contentSystemId ??= process.env.FIGMA_CONTENT_API_CONTENT_SYSTEM_ID;
70
112
  let bootstrapData = null;
113
+ let bootstrapReason = null;
71
114
  // Local dev: resolve from project ID + bootstrap cache
115
+ // Use getEnvVarSync fallback because Next.js evaluates config before dotenv populates process.env
72
116
  if (!contentSystemId) {
73
- const projectId = process.env.FIGMA_PROJECT_ID;
74
- const environmentName = process.env.FIGMA_ENVIRONMENT_NAME;
75
- if (projectId && environmentName) {
117
+ const projectId = process.env.FIGMA_PROJECT_ID ?? getEnvVarSync(process.cwd(), 'FIGMA_PROJECT_ID');
118
+ const environmentName = process.env.FIGMA_ENVIRONMENT_NAME ?? getEnvVarSync(process.cwd(), 'FIGMA_ENVIRONMENT_NAME');
119
+ if (!projectId || !environmentName) {
120
+ bootstrapReason = {
121
+ kind: 'no-config'
122
+ };
123
+ } else {
76
124
  const store = getTokenStore();
77
125
  bootstrapData = store.getBootstrapData(projectId, environmentName);
78
126
  // Runtime fallback: fetch from API if not cached (e.g. environment switch without re-init)
79
127
  if (!bootstrapData) {
80
- bootstrapData = await resolveAndCacheBootstrap(store, projectId, environmentName);
128
+ const resolution = await resolveAndCacheBootstrap(store, projectId, environmentName);
129
+ if (resolution.ok) {
130
+ bootstrapData = resolution.data;
131
+ } else {
132
+ bootstrapReason = resolution.reason;
133
+ }
81
134
  }
82
135
  if (bootstrapData) {
83
136
  contentSystemId = bootstrapData.contentSystemId;
@@ -85,10 +138,17 @@ export async function buildFigmaConfig(config) {
85
138
  }
86
139
  }
87
140
  if (!contentSystemId) {
141
+ logMissingContentSystemId(bootstrapReason ?? {
142
+ kind: 'no-config'
143
+ });
88
144
  throw new Error('Content System ID could not be resolved. ' + 'Run `npx @payloadcms/figma init` to set up your project.');
89
145
  }
90
146
  const url = process.env.FIGMA_CONTENT_API_URL || envConfig.contentApiUrl;
91
147
  const isProduction = process.env.NODE_ENV === 'production';
148
+ const usesTokenStoreAuth = !process.env.FIGMA_CONTENT_API_ACCESS_KEY && process.env.FIGMA_DEV_JWT !== 'true';
149
+ if (usesTokenStoreAuth) {
150
+ logMissingCliAuth(getTokenStore());
151
+ }
92
152
  // Determine database adapter based on environment
93
153
  let db;
94
154
  if (config.figma.useContentSystem === false) {
@@ -219,6 +279,7 @@ export async function buildFigmaConfig(config) {
219
279
  usernameField: 'email'
220
280
  }
221
281
  ],
282
+ ...isProduction ? {} : getDevCookieNames(contentSystemId),
222
283
  debug: !!process.env.DEBUG,
223
284
  disabled: false
224
285
  }),
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Namespace OAuth cookies per-app in local dev so multiple Payload apps
3
+ * running on localhost (shared cookie jar across ports) don't clobber each
4
+ * other's sessions on logout. Uses projectId so the cookie stays stable when
5
+ * switching environments in the same checkout; falls back to contentSystemId
6
+ * when projectId isn't in `.env`.
7
+ *
8
+ * In production, cookies are isolated by unique hostnames and this is unused.
9
+ */
10
+ export declare function getDevCookieNames(contentSystemId: string): {
11
+ cookieName: string;
12
+ userInfoCookieName: string;
13
+ };
14
+ //# sourceMappingURL=dev-cookie-names.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { getEnvVarSync } from '../utils/env-management.js';
2
+ /**
3
+ * Namespace OAuth cookies per-app in local dev so multiple Payload apps
4
+ * running on localhost (shared cookie jar across ports) don't clobber each
5
+ * other's sessions on logout. Uses projectId so the cookie stays stable when
6
+ * switching environments in the same checkout; falls back to contentSystemId
7
+ * when projectId isn't in `.env`.
8
+ *
9
+ * In production, cookies are isolated by unique hostnames and this is unused.
10
+ */ export function getDevCookieNames(contentSystemId) {
11
+ const projectId = process.env.FIGMA_PROJECT_ID ?? getEnvVarSync(process.cwd(), 'FIGMA_PROJECT_ID') ?? contentSystemId;
12
+ const suffix = projectId.slice(0, 8);
13
+ return {
14
+ cookieName: `figma-${suffix}-oauth-token`,
15
+ userInfoCookieName: `figma-${suffix}-user-info`
16
+ };
17
+ }
18
+
19
+ //# sourceMappingURL=dev-cookie-names.js.map