@payloadcms/figma 0.0.1-alpha.74 → 0.0.1-alpha.75

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.
@@ -30,7 +30,7 @@ export declare class FigmaApiError extends Error {
30
30
  * requests to the Content API. The token contains the CMS ID and
31
31
  * user identifying information in its claims.
32
32
  *
33
- * Endpoint: GET https://api.figma.com/v1/cms/:id/token/
33
+ * Endpoint: POST https://api.figma.com/v1/cms/content-systems/:id/token/
34
34
  *
35
35
  * @param accessToken - OAuth access token for authentication
36
36
  * @param tenantId - The tenant/CMS ID
@@ -38,6 +38,7 @@ export declare class FigmaApiError extends Error {
38
38
  * @throws {FigmaApiError} If the API call fails
39
39
  */
40
40
  export declare function getProjectToken(credential: AuthCredential, tenantId: string): Promise<ProjectToken>;
41
+ export declare function getPayloadAdminToken(credential: AuthCredential, environmentId: string): Promise<ProjectToken>;
41
42
  /**
42
43
  * Get user info from Figma API
43
44
  *
@@ -24,7 +24,7 @@ import { parseControlPlaneApiError } from './error-response.js';
24
24
  * requests to the Content API. The token contains the CMS ID and
25
25
  * user identifying information in its claims.
26
26
  *
27
- * Endpoint: GET https://api.figma.com/v1/cms/:id/token/
27
+ * Endpoint: POST https://api.figma.com/v1/cms/content-systems/:id/token/
28
28
  *
29
29
  * @param accessToken - OAuth access token for authentication
30
30
  * @param tenantId - The tenant/CMS ID
@@ -66,7 +66,7 @@ import { parseControlPlaneApiError } from './error-response.js';
66
66
  if (shouldMock) {
67
67
  // MOCK IMPLEMENTATION: Generate a mock JWT token for testing
68
68
  log.debug('Using mock project token API for getProjectToken (FIGMA_MOCK_PROJECT_TOKEN = "true")');
69
- const mockToken = createMockJWT(tenantId, 25) // 25 minutes expiry
69
+ const mockToken = createMockJWT(`csid_${tenantId}`, 25) // 25 minutes expiry
70
70
  ;
71
71
  // Mock response data
72
72
  const data = {
@@ -87,7 +87,7 @@ import { parseControlPlaneApiError } from './error-response.js';
87
87
  }
88
88
  // REAL API IMPLEMENTATION
89
89
  const baseUrl = getEnvConfig().apiBaseUrl;
90
- const url = `${baseUrl}/v1/cms/${tenantId}/token/`;
90
+ const url = `${baseUrl}/v1/cms/content-systems/${tenantId}/token/`;
91
91
  const response = await fetch(url, {
92
92
  headers: getAuthHeaders(credential),
93
93
  method: 'POST'
@@ -122,6 +122,36 @@ import { parseControlPlaneApiError } from './error-response.js';
122
122
  throw new FigmaApiError(`Failed to get project token: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, error instanceof Error ? error : undefined);
123
123
  }
124
124
  }
125
+ export async function getPayloadAdminToken(credential, environmentId) {
126
+ if (process.env.FIGMA_MOCK_PROJECT_TOKEN === 'true') {
127
+ const token = createMockJWT(`eid_${environmentId}`, 25);
128
+ return {
129
+ expiresAt: parseJwtExpiry(token),
130
+ tenantId: environmentId,
131
+ token
132
+ };
133
+ }
134
+ const baseUrl = getEnvConfig().apiBaseUrl;
135
+ const response = await fetch(`${baseUrl}/v1/cms/environments/${environmentId}/admin-token`, {
136
+ headers: getAuthHeaders(credential),
137
+ method: 'POST'
138
+ });
139
+ if (!response.ok) {
140
+ throw new FigmaApiError(await parseControlPlaneApiError({
141
+ context: 'get Payload Admin token',
142
+ response
143
+ }), response.status);
144
+ }
145
+ const data = await response.json();
146
+ if (!data.meta?.token) {
147
+ throw new FigmaApiError('Invalid response: missing token in meta');
148
+ }
149
+ return {
150
+ expiresAt: parseJwtExpiry(data.meta.token),
151
+ tenantId: environmentId,
152
+ token: data.meta.token
153
+ };
154
+ }
125
155
  /**
126
156
  * Create a mock JWT token for testing
127
157
  * TODO: Remove this when real API is available
@@ -4,9 +4,9 @@ import type { JWTPayload } from './types.js';
4
4
  * JWT validation error with specific error codes
5
5
  */
6
6
  export declare class JWTValidationError extends Error {
7
- code: 'AUDIENCE_INVALID' | 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID';
7
+ code: 'AUDIENCE_INVALID' | 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID' | 'TOKEN_CLAIMS_INVALID';
8
8
  cause?: Error | undefined;
9
- constructor(message: string, code: 'AUDIENCE_INVALID' | 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID', cause?: Error | undefined);
9
+ constructor(message: string, code: 'AUDIENCE_INVALID' | 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID' | 'TOKEN_CLAIMS_INVALID', cause?: Error | undefined);
10
10
  }
11
11
  /**
12
12
  * Validate a project token JWT
@@ -36,6 +36,16 @@ export declare function validateProjectToken(params: {
36
36
  environment: Environment;
37
37
  token: string;
38
38
  }): Promise<JWTPayload>;
39
+ export declare function validatePayloadAdminToken(params: {
40
+ audience: string;
41
+ environment: Environment;
42
+ token: string;
43
+ }): Promise<JWTPayload>;
44
+ export declare function validateContentApiToken(params: {
45
+ audience: string;
46
+ environment: Environment;
47
+ token: string;
48
+ }): Promise<JWTPayload>;
39
49
  /**
40
50
  * Clear JWKS cache (useful for testing or forcing refresh)
41
51
  *
@@ -63,7 +63,7 @@ import { getEnvConfig } from '../constants.js';
63
63
  try {
64
64
  const JWKS = getJWKSResolver(environment);
65
65
  const { payload } = await jwtVerify(token, JWKS, {
66
- clockTolerance: 300,
66
+ clockTolerance: 30,
67
67
  ...audience ? {
68
68
  audience
69
69
  } : {}
@@ -108,6 +108,24 @@ import { getEnvConfig } from '../constants.js';
108
108
  throw new JWTValidationError(`JWT validation failed: ${message}`, 'SIGNATURE_INVALID', error instanceof Error ? error : undefined);
109
109
  }
110
110
  }
111
+ export async function validatePayloadAdminToken(params) {
112
+ const payload = await validateProjectToken({
113
+ ...params,
114
+ audience: `eid_${params.audience}`
115
+ });
116
+ if (typeof payload.sub !== 'string' || typeof payload.email !== 'string') {
117
+ throw new JWTValidationError('Payload Admin token claims are invalid', 'TOKEN_CLAIMS_INVALID');
118
+ }
119
+ return payload;
120
+ }
121
+ export async function validateContentApiToken(params) {
122
+ // Content API tokens are issued by the content-system-scoped endpoint, which
123
+ // prefixes the `aud` with `csid_` (mirrors the `eid_` admin-token scoping).
124
+ return validateProjectToken({
125
+ ...params,
126
+ audience: `csid_${params.audience}`
127
+ });
128
+ }
111
129
  /**
112
130
  * Clear JWKS cache (useful for testing or forcing refresh)
113
131
  *
@@ -8,7 +8,7 @@
8
8
  import { PROJECT_TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
9
9
  import { getInfraEnvironment } from '../constants.js';
10
10
  import * as log from '../utils/log.js';
11
- import { JWTValidationError, validateProjectToken } from './jwt-validator.js';
11
+ import { JWTValidationError, validateContentApiToken } from './jwt-validator.js';
12
12
  import { getValidCredential } from './oauth-flow.js';
13
13
  /**
14
14
  * Error during project token operations
@@ -153,7 +153,8 @@ function isCachedTokenValid(entry) {
153
153
  validatedClaims = parseJWTClaims(fetchedToken.token);
154
154
  } else {
155
155
  try {
156
- validatedClaims = await validateProjectToken({
156
+ validatedClaims = await validateContentApiToken({
157
+ audience: resolvedTenantId,
157
158
  environment: getInfraEnvironment(),
158
159
  token: fetchedToken.token
159
160
  });
@@ -197,8 +197,12 @@ export interface JWTPayload extends JoseJWTPayload {
197
197
  [key: string]: unknown;
198
198
  /** User email address */
199
199
  email?: string;
200
+ /** Make permission tier the authed user holds on this CMS */
201
+ make_permission?: 'edit' | 'view';
200
202
  /** Tenant/CMS ID this token is scoped to */
201
203
  tenant_id?: string;
204
+ /** File user groups the authed user is a member of (scoped to the linked Make file) */
205
+ user_groups?: string[];
202
206
  }
203
207
  /**
204
208
  * Project token (JWT) for authenticating to Content API
@@ -216,7 +220,7 @@ export interface ProjectToken {
216
220
  }
217
221
  /**
218
222
  * Raw project token response from Figma API
219
- * Response from GET https://api.figma.com/v1/cms/:id/token/
223
+ * Response from POST https://api.figma.com/v1/cms/content-systems/:id/token/
220
224
  */
221
225
  export interface ProjectTokenResponse {
222
226
  /** Response metadata containing the JWT token */
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Raw project token response from Figma API
3
- * Response from GET https://api.figma.com/v1/cms/:id/token/
3
+ * Response from POST https://api.figma.com/v1/cms/content-systems/:id/token/
4
4
  */ export { };
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { listTokensCommand } from './commands/list-tokens.js';
12
12
  import { loginCommand } from './commands/login.js';
13
13
  import { logoutCommand } from './commands/logout.js';
14
14
  import { upgradeCommand } from './commands/upgrade.js';
15
+ import { uploadSchemaCommand } from './commands/upload-schema.js';
15
16
  import { setInfraEnvironment } from './constants.js';
16
17
  import { helpMessage } from './utils/messages.js';
17
18
  /**
@@ -178,6 +179,12 @@ class Main {
178
179
  isDryRun: this.args['--dry-run']
179
180
  });
180
181
  break;
182
+ case 'upload-schema':
183
+ await uploadSchemaCommand({
184
+ id: this.args['--id'],
185
+ env: this.args['--env']
186
+ });
187
+ break;
181
188
  default:
182
189
  p.log.error(pc.red(`Unknown command: ${subcommand}`));
183
190
  p.note('Use --help to see available commands', 'Tip');
@@ -0,0 +1,8 @@
1
+ export interface UploadSchemaCommandOptions {
2
+ /** Environment name (overrides FIGMA_ENVIRONMENT_NAME). */
3
+ env?: string;
4
+ /** CMS Resource ID (overrides FIGMA_PROJECT_ID). */
5
+ id?: string;
6
+ }
7
+ /** Upload the current Payload collection schema for Content API migration tooling. */
8
+ export declare function uploadSchemaCommand(options?: UploadSchemaCommandOptions): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { getBootstrapInfo } from '../api/control-plane.js';
4
+ import { tryGetCredential } from '../auth/oauth-flow.js';
5
+ import { getValidProjectToken } from '../auth/project-token.js';
6
+ import { getTokenStore } from '../auth/token-store.js';
7
+ import { getEnvConfig } from '../constants.js';
8
+ import { buildDocumentSchema } from '../db-content-api/utilities/schema/buildDocumentSchema.js';
9
+ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
10
+ import { getEnvVar } from '../utils/env-management.js';
11
+ import { loadPayloadConfig } from '../utils/load-payload-config.js';
12
+ import { resolveEnvironment } from '../utils/resolve-environment.js';
13
+ import { loginCommand } from './login.js';
14
+ /** Upload the current Payload collection schema for Content API migration tooling. */ export async function uploadSchemaCommand(options = {}) {
15
+ const projectPath = process.cwd();
16
+ const projectId = options.id ?? await getEnvVar(projectPath, 'FIGMA_PROJECT_ID') ?? undefined;
17
+ const environmentName = options.env ?? await getEnvVar(projectPath, 'FIGMA_ENVIRONMENT_NAME') ?? undefined;
18
+ if (!projectId) {
19
+ p.log.error(pc.red('Project ID not found. Run `@payloadcms/figma init` first, or use --id.'));
20
+ process.exit(1);
21
+ }
22
+ // Bootstrap still requires a CLI login when the upload uses the Content API key.
23
+ const tokenStore = getTokenStore();
24
+ let credential = await tryGetCredential(tokenStore);
25
+ if (!credential) {
26
+ p.log.message('Please log in to continue');
27
+ await loginCommand();
28
+ credential = await tryGetCredential(tokenStore);
29
+ }
30
+ if (!credential) {
31
+ p.log.error(pc.red('Authentication failed'));
32
+ process.exit(1);
33
+ }
34
+ const spinner = p.spinner();
35
+ spinner.start('Resolving schema upload target...');
36
+ try {
37
+ const bootstrapInfo = await getBootstrapInfo(credential, projectId);
38
+ const environment = resolveEnvironment({
39
+ environmentName,
40
+ environments: bootstrapInfo.environments
41
+ });
42
+ cacheAllEnvironments(tokenStore, projectId, bootstrapInfo);
43
+ spinner.message('Loading Payload config...');
44
+ // --id/--env select the upload target only. Collections conditionally configured from
45
+ // FIGMA_ENVIRONMENT_NAME still use process.env/.env and may not match that upload target.
46
+ const config = await loadPayloadConfig(projectPath);
47
+ const documentSchema = buildDocumentSchema(config);
48
+ spinner.message('Uploading collection schema...');
49
+ const apiKey = process.env.FIGMA_CONTENT_API_ACCESS_KEY;
50
+ const projectToken = apiKey ? null : await getValidProjectToken({
51
+ projectInfo: {
52
+ environmentName: environment.name,
53
+ projectId
54
+ },
55
+ tenantId: environment.contentSystemId,
56
+ tokenStore
57
+ });
58
+ if (!apiKey && !projectToken) {
59
+ throw new Error('Could not get a project token. Run `@payloadcms/figma login` and retry.');
60
+ }
61
+ // --infra-env is applied by the CLI before this fallback selects the Content API URL.
62
+ const contentApiUrl = process.env.FIGMA_CONTENT_API_URL || getEnvConfig().contentApiUrl;
63
+ if (!contentApiUrl) {
64
+ throw new Error('FIGMA_CONTENT_API_URL is required for this infrastructure environment.');
65
+ }
66
+ const response = await fetch(`${contentApiUrl}/api/v0/content_systems/${encodeURIComponent(environment.contentSystemId)}/jsonb_to_index_migration_schema`, {
67
+ body: JSON.stringify({
68
+ documentSchema
69
+ }),
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ ...apiKey ? {
73
+ 'X-Api-Key': apiKey
74
+ } : {
75
+ Authorization: `Bearer ${projectToken}`
76
+ }
77
+ },
78
+ method: 'PUT'
79
+ });
80
+ if (!response.ok) {
81
+ const responseBody = await response.text();
82
+ throw new Error(`Content API returned ${response.status}${responseBody ? `: ${responseBody}` : ''}`);
83
+ }
84
+ const collectionCount = Object.keys(documentSchema.collections).length;
85
+ spinner.stop(pc.green(`Uploaded ${collectionCount} collection schema${collectionCount === 1 ? '' : 's'} to ${environment.name}`));
86
+ } catch (error) {
87
+ spinner.stop(pc.red('Failed to upload collection schema'));
88
+ p.log.error(error instanceof Error ? error.message : 'Unknown error');
89
+ process.exit(1);
90
+ }
91
+ }