@payloadcms/figma 0.0.1-alpha.61 → 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 (50) hide show
  1. package/dist/api/control-plane.d.ts +4 -0
  2. package/dist/api/control-plane.js +11 -4
  3. package/dist/auth/crypto-utils.d.ts +11 -0
  4. package/dist/auth/crypto-utils.js +51 -0
  5. package/dist/auth/oauth-flow.js +3 -3
  6. package/dist/auth/project-token.d.ts +17 -6
  7. package/dist/auth/project-token.js +37 -19
  8. package/dist/auth/token-store-migration.d.ts +58 -0
  9. package/dist/auth/token-store-migration.js +156 -0
  10. package/dist/auth/token-store.d.ts +51 -128
  11. package/dist/auth/token-store.js +326 -187
  12. package/dist/auth/types.d.ts +8 -1
  13. package/dist/cli.js +9 -1
  14. package/dist/commands/debug.js +16 -12
  15. package/dist/commands/deploy.js +2 -0
  16. package/dist/commands/dump-tokens.d.ts +12 -0
  17. package/dist/commands/dump-tokens.js +69 -0
  18. package/dist/commands/init.js +8 -4
  19. package/dist/commands/list-tokens.js +2 -2
  20. package/dist/commands/login.js +1 -1
  21. package/dist/commands/logout.js +23 -4
  22. package/dist/config/oauth.d.ts +10 -2
  23. package/dist/config/oauth.js +18 -4
  24. package/dist/constants.d.ts +4 -2
  25. package/dist/constants.js +19 -1
  26. package/dist/db-content-api/index.js +21 -0
  27. package/dist/db-content-api/utilities/auth.js +4 -1
  28. package/dist/db-content-api/utilities/data/validateRelationships.d.ts +7 -0
  29. package/dist/db-content-api/utilities/data/validateRelationships.js +102 -0
  30. package/dist/oauth/defaults.d.ts +3 -1
  31. package/dist/oauth/defaults.js +6 -5
  32. package/dist/oauth/endpoints/getLoginEndpoint.js +1 -1
  33. package/dist/oauth/index.js +1 -0
  34. package/dist/oauth/types.d.ts +10 -0
  35. package/dist/plugin/auth-preflight.d.ts +8 -0
  36. package/dist/plugin/auth-preflight.js +20 -0
  37. package/dist/plugin/bootstrap-preflight.d.ts +23 -0
  38. package/dist/plugin/bootstrap-preflight.js +45 -0
  39. package/dist/plugin/build-config.js +73 -12
  40. package/dist/plugin/dev-cookie-names.d.ts +14 -0
  41. package/dist/plugin/dev-cookie-names.js +19 -0
  42. package/dist/storage-content-api/client.js +4 -1
  43. package/dist/storage-content-api/staticHandler.js +1 -18
  44. package/dist/utils/build-lambda-zip.d.ts +4 -2
  45. package/dist/utils/build-lambda-zip.js +4 -7
  46. package/dist/utils/messages.js +1 -1
  47. package/dist/utils/token-display.d.ts +5 -1
  48. package/dist/utils/token-display.js +47 -26
  49. package/dist/utils/version-check.js +1 -1
  50. package/package.json +2 -1
@@ -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
@@ -18,7 +18,10 @@ function createStorageAuthMiddleware(config) {
18
18
  if (auth.mode === 'apiKey') {
19
19
  request.headers.set('X-Api-Key', auth.apiKey);
20
20
  } else if (auth.mode === 'tokenStore') {
21
- const token = await getValidProjectToken(auth.tokenStore, contentSystemId);
21
+ const token = await getValidProjectToken({
22
+ tenantId: contentSystemId,
23
+ tokenStore: auth.tokenStore
24
+ });
22
25
  if (!token) {
23
26
  throw new Error('Authentication required. Run `npx @payloadcms/figma login` to authenticate.');
24
27
  }
@@ -23,24 +23,7 @@ export const getHandler = ({ client, collection })=>{
23
23
  status: 500
24
24
  });
25
25
  }
26
- // Fetch file bytes from S3 (required for Payload's image processing)
27
- const fileResponse = await fetch(data.url);
28
- if (!fileResponse.ok) {
29
- req.payload.logger.error(`S3 fetch failed: ${fileResponse.status} ${fileResponse.statusText}`);
30
- return new Response('Failed to fetch file from storage', {
31
- status: 502
32
- });
33
- }
34
- const contentType = fileResponse.headers.get('content-type') || 'application/octet-stream';
35
- const contentLength = fileResponse.headers.get('content-length');
36
- return new Response(fileResponse.body, {
37
- headers: {
38
- 'Content-Type': contentType,
39
- ...contentLength && {
40
- 'Content-Length': contentLength
41
- }
42
- }
43
- });
26
+ return Response.redirect(data.url, 302);
44
27
  } catch (err) {
45
28
  req.payload.logger.error({
46
29
  err,
@@ -4,8 +4,10 @@
4
4
  * Cross-platform replacement for build_for_lambda.sh:
5
5
  * 1. Copies .next/static to .next/standalone/.next/static
6
6
  * 2. Copies run.sh to .next/standalone/run.sh
7
- * 3. Copies public/ to .next/standalone/public/ (if exists)
8
- * 4. Creates lambda.zip from .next/standalone/
7
+ * 3. Creates lambda.zip from .next/standalone/
8
+ *
9
+ * Note: public/ is NOT included in the zip — those files are uploaded
10
+ * as static assets and served via CDN, not from the Lambda function.
9
11
  *
10
12
  * @param projectPath - Path to project root
11
13
  * @throws Error if standalone directory missing or zip creation fails
@@ -8,15 +8,16 @@ import path from 'path';
8
8
  * Cross-platform replacement for build_for_lambda.sh:
9
9
  * 1. Copies .next/static to .next/standalone/.next/static
10
10
  * 2. Copies run.sh to .next/standalone/run.sh
11
- * 3. Copies public/ to .next/standalone/public/ (if exists)
12
- * 4. Creates lambda.zip from .next/standalone/
11
+ * 3. Creates lambda.zip from .next/standalone/
12
+ *
13
+ * Note: public/ is NOT included in the zip — those files are uploaded
14
+ * as static assets and served via CDN, not from the Lambda function.
13
15
  *
14
16
  * @param projectPath - Path to project root
15
17
  * @throws Error if standalone directory missing or zip creation fails
16
18
  */ export async function buildLambdaZip(projectPath) {
17
19
  const standalonePath = path.join(projectPath, '.next', 'standalone');
18
20
  const staticPath = path.join(projectPath, '.next', 'static');
19
- const publicPath = path.join(projectPath, 'public');
20
21
  const runShPath = path.join(projectPath, 'run.sh');
21
22
  const zipPath = path.join(projectPath, 'lambda.zip');
22
23
  if (!await isDirectory(standalonePath)) {
@@ -31,10 +32,6 @@ import path from 'path';
31
32
  } catch {
32
33
  // run.sh may not exist in all setups
33
34
  }
34
- if (await isDirectory(publicPath)) {
35
- const destPublic = path.join(standalonePath, 'public');
36
- await copyDirectory(publicPath, destPublic);
37
- }
38
35
  await createZip(standalonePath, zipPath);
39
36
  }
40
37
  async function isDirectory(dirPath) {
@@ -59,7 +59,7 @@ export function helpMessage() {
59
59
 
60
60
  ${pc.bold('GLOBAL OPTIONS')}
61
61
 
62
- ${pc.dim('--infra-env <env>')} Target infrastructure (production or staging)
62
+ ${pc.dim('--infra-env <env>')} Target infrastructure (production, staging, or devbox)
63
63
 
64
64
  ${pc.bold('DOCUMENTATION')}
65
65
 
@@ -8,7 +8,11 @@ export declare function maskToken(token: string): string;
8
8
  */
9
9
  export declare function formatTimestamp(timestamp: number): string;
10
10
  /**
11
- * Show all stored project tokens
11
+ * Show the project token for the current project + environment scope.
12
+ *
13
+ * Resolves `projectId` and `environmentName` from env vars
14
+ * (`FIGMA_PROJECT_ID`, `FIGMA_ENVIRONMENT_NAME`) — the *user's* project
15
+ * environment, which is independent of the infra env (production/staging).
12
16
  */
13
17
  export declare function showProjectTokens(tokenStore: TokenStore): void;
14
18
  //# sourceMappingURL=token-display.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
+ import { getEnvVarSync } from './env-management.js';
3
4
  import { isDebug } from './is-debug.js';
4
5
  /**
5
6
  * Mask token for display (show first 4 and last 4 characters)
@@ -32,38 +33,58 @@ import { isDebug } from './is-debug.js';
32
33
  return pc.red(`${minutes}m`);
33
34
  }
34
35
  /**
35
- * Show all stored project tokens
36
+ * Show the project token for the current project + environment scope.
37
+ *
38
+ * Resolves `projectId` and `environmentName` from env vars
39
+ * (`FIGMA_PROJECT_ID`, `FIGMA_ENVIRONMENT_NAME`) — the *user's* project
40
+ * environment, which is independent of the infra env (production/staging).
36
41
  */ export function showProjectTokens(tokenStore) {
37
- // Get all tenant IDs that have project tokens
38
- const tenantIds = tokenStore.getAllProjectTokenTenantIds();
39
- if (tenantIds.length === 0) {
40
- p.note(pc.dim('No project tokens stored.'), `Project Tokens`);
42
+ const cwd = process.cwd();
43
+ const projectId = getEnvVarSync(cwd, 'FIGMA_PROJECT_ID');
44
+ const environmentName = getEnvVarSync(cwd, 'FIGMA_ENVIRONMENT_NAME');
45
+ if (!projectId || !environmentName) {
46
+ p.note(pc.dim('Set FIGMA_PROJECT_ID and FIGMA_ENVIRONMENT_NAME in your .env to view project tokens.'), 'Project Tokens');
41
47
  return;
42
48
  }
43
- const projectTokenLines = [];
44
- let displayedCount = 0;
45
- tenantIds.forEach((tenantId)=>{
46
- const projectToken = tokenStore.getProjectToken(tenantId);
47
- if (!projectToken) {
48
- return;
49
- }
50
- const isValid = tokenStore.hasValidProjectToken(tenantId);
51
- const status = isValid ? pc.green('✓ Valid') : pc.red('✗ Expired');
52
- if (displayedCount > 0) {
53
- projectTokenLines.push(''); // Blank line between tokens
54
- }
55
- projectTokenLines.push(`Tenant ID: ${tenantId}`);
56
- projectTokenLines.push(`Status: ${status}`);
57
- projectTokenLines.push(`Token: ${isDebug() ? projectToken.token : maskToken(projectToken.token)}`);
58
- projectTokenLines.push(`Expires: ${formatTimestamp(projectToken.expiresAt)}`);
59
- displayedCount++;
49
+ const header = [
50
+ `Project ID: ${projectId}`,
51
+ `Environment: ${environmentName}`
52
+ ];
53
+ const bootstrapData = tokenStore.getBootstrapData(projectId, environmentName);
54
+ if (!bootstrapData) {
55
+ p.note([
56
+ ...header,
57
+ pc.dim('No bootstrap data. Run `@payloadcms/figma init` to set up.')
58
+ ].join('\n'), 'Project Token');
59
+ return;
60
+ }
61
+ const projectInfo = {
62
+ environmentName,
63
+ projectId
64
+ };
65
+ const projectToken = tokenStore.getProjectToken({
66
+ projectInfo
60
67
  });
61
- // Handle case where all tokens were expired and auto-cleaned
62
- if (displayedCount === 0) {
63
- p.note(pc.dim('No project tokens stored.'), `Project Tokens`);
68
+ if (!projectToken) {
69
+ p.note([
70
+ ...header,
71
+ `Tenant ID: ${bootstrapData.contentSystemId}`,
72
+ pc.dim('No stored token.')
73
+ ].join('\n'), 'Project Token');
64
74
  return;
65
75
  }
66
- p.note(projectTokenLines.join('\n'), `Project Tokens (${displayedCount})`);
76
+ const isValid = tokenStore.hasValidProjectToken({
77
+ projectInfo
78
+ });
79
+ const status = isValid ? pc.green('✓ Valid') : pc.red('✗ Expired');
80
+ const projectTokenLines = [
81
+ ...header,
82
+ `Tenant ID: ${bootstrapData.contentSystemId}`,
83
+ `Status: ${status}`,
84
+ `Token: ${isDebug() ? projectToken.token : maskToken(projectToken.token)}`,
85
+ `Expires: ${formatTimestamp(projectToken.expiresAt)}`
86
+ ];
87
+ p.note(projectTokenLines.join('\n'), 'Project Token');
67
88
  }
68
89
 
69
90
  //# sourceMappingURL=token-display.js.map
@@ -21,7 +21,7 @@ export async function checkForUpdates(currentVersion) {
21
21
  if (!latestVersion || latestVersion === currentVersion) {
22
22
  return;
23
23
  }
24
- p.log.warn(pc.yellow(`@payloadcms/figma ${currentVersion} is outdated. Latest: ${latestVersion}`));
24
+ p.log.warn(pc.yellow(`@payloadcms/figma ${currentVersion} differs from latest. Latest: ${latestVersion}`));
25
25
  p.log.message(pc.dim(` Run: npx @payloadcms/figma@${latestVersion} init --id <your-id>`));
26
26
  } catch {
27
27
  // Silent catch - network errors, timeouts, parse errors should not interrupt CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payloadcms/figma",
3
- "version": "0.0.1-alpha.61",
3
+ "version": "0.0.1-alpha.63",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,6 +34,7 @@
34
34
  "arg": "^5.0.2",
35
35
  "conf": "^13.1.0",
36
36
  "cross-spawn": "7.0.6",
37
+ "env-paths": "^3.0.0",
37
38
  "figures": "^6.1.0",
38
39
  "jose": "6.0.12",
39
40
  "jsonwebtoken": "9.0.3",