@payloadcms/figma 0.0.1-alpha.49 → 0.0.1-alpha.50

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 (267) hide show
  1. package/dist/api/control-plane.d.ts +23 -14
  2. package/dist/api/control-plane.js +23 -22
  3. package/dist/api/figma-api.js +3 -5
  4. package/dist/auth/project-token.js +2 -2
  5. package/dist/auth/token-store.d.ts +29 -2
  6. package/dist/auth/token-store.js +38 -1
  7. package/dist/auth/types.d.ts +10 -0
  8. package/dist/cli.js +18 -9
  9. package/dist/commands/debug.js +31 -26
  10. package/dist/commands/deploy.d.ts +3 -1
  11. package/dist/commands/deploy.js +27 -15
  12. package/dist/commands/env.d.ts +13 -0
  13. package/dist/commands/env.js +88 -0
  14. package/dist/commands/init.d.ts +2 -0
  15. package/dist/commands/init.js +49 -52
  16. package/dist/commands/list-tokens.js +2 -2
  17. package/dist/commands/login.js +3 -3
  18. package/dist/commands/logout.js +2 -2
  19. package/dist/config/oauth.d.ts +1 -1
  20. package/dist/config/oauth.js +3 -4
  21. package/dist/constants.d.ts +11 -7
  22. package/dist/constants.js +14 -9
  23. package/dist/db-content-api/index.js +6 -11
  24. package/dist/oauth/strategy/index.js +2 -2
  25. package/dist/oauth/utilities/refreshTokens.js +2 -2
  26. package/dist/plugin/build-config.d.ts +0 -10
  27. package/dist/plugin/build-config.js +57 -22
  28. package/dist/templates/.env.example +4 -10
  29. package/dist/types.d.ts +2 -0
  30. package/dist/utils/cache-bootstrap.d.ts +8 -0
  31. package/dist/utils/cache-bootstrap.js +18 -0
  32. package/dist/utils/messages.js +15 -2
  33. package/dist/utils/resolve-environment.d.ts +14 -0
  34. package/dist/utils/resolve-environment.js +28 -0
  35. package/package.json +3 -2
  36. package/dist/api/control-plane.d.ts.map +0 -1
  37. package/dist/api/control-plane.js.map +0 -1
  38. package/dist/api/figma-api.d.ts.map +0 -1
  39. package/dist/api/figma-api.js.map +0 -1
  40. package/dist/auth/browser.d.ts.map +0 -1
  41. package/dist/auth/browser.js.map +0 -1
  42. package/dist/auth/callback-server.d.ts.map +0 -1
  43. package/dist/auth/callback-server.js.map +0 -1
  44. package/dist/auth/crypto-utils.d.ts.map +0 -1
  45. package/dist/auth/crypto-utils.js.map +0 -1
  46. package/dist/auth/jwt-validator.d.ts.map +0 -1
  47. package/dist/auth/jwt-validator.js.map +0 -1
  48. package/dist/auth/oauth-flow.d.ts.map +0 -1
  49. package/dist/auth/oauth-flow.js.map +0 -1
  50. package/dist/auth/pkce.d.ts.map +0 -1
  51. package/dist/auth/pkce.js.map +0 -1
  52. package/dist/auth/project-token.d.ts.map +0 -1
  53. package/dist/auth/project-token.js.map +0 -1
  54. package/dist/auth/refresh.d.ts.map +0 -1
  55. package/dist/auth/refresh.js.map +0 -1
  56. package/dist/auth/token-store.d.ts.map +0 -1
  57. package/dist/auth/token-store.js.map +0 -1
  58. package/dist/auth/types.d.ts.map +0 -1
  59. package/dist/auth/types.js.map +0 -1
  60. package/dist/cli.d.ts.map +0 -1
  61. package/dist/cli.js.map +0 -1
  62. package/dist/commands/build-lambda-zip.d.ts.map +0 -1
  63. package/dist/commands/build-lambda-zip.js.map +0 -1
  64. package/dist/commands/debug.d.ts.map +0 -1
  65. package/dist/commands/debug.js.map +0 -1
  66. package/dist/commands/deploy.d.ts.map +0 -1
  67. package/dist/commands/deploy.js.map +0 -1
  68. package/dist/commands/init.d.ts.map +0 -1
  69. package/dist/commands/init.js.map +0 -1
  70. package/dist/commands/list-tokens.d.ts.map +0 -1
  71. package/dist/commands/list-tokens.js.map +0 -1
  72. package/dist/commands/login.d.ts.map +0 -1
  73. package/dist/commands/login.js.map +0 -1
  74. package/dist/commands/logout.d.ts.map +0 -1
  75. package/dist/commands/logout.js.map +0 -1
  76. package/dist/config/oauth.d.ts.map +0 -1
  77. package/dist/config/oauth.js.map +0 -1
  78. package/dist/constants.d.ts.map +0 -1
  79. package/dist/constants.js.map +0 -1
  80. package/dist/db-content-api/generated/content-api-types.d.ts.map +0 -1
  81. package/dist/db-content-api/generated/content-api-types.js.map +0 -1
  82. package/dist/db-content-api/index.d.ts.map +0 -1
  83. package/dist/db-content-api/index.js.map +0 -1
  84. package/dist/db-content-api/temp-utilities/slug.d.ts.map +0 -1
  85. package/dist/db-content-api/temp-utilities/slug.js.map +0 -1
  86. package/dist/db-content-api/temp-utilities/sorting.d.ts.map +0 -1
  87. package/dist/db-content-api/temp-utilities/sorting.js.map +0 -1
  88. package/dist/db-content-api/temp-utilities/types.d.ts.map +0 -1
  89. package/dist/db-content-api/temp-utilities/types.js.map +0 -1
  90. package/dist/db-content-api/temp-utilities/unwrapDocument.d.ts.map +0 -1
  91. package/dist/db-content-api/temp-utilities/unwrapDocument.js.map +0 -1
  92. package/dist/db-content-api/utilities/auth.d.ts.map +0 -1
  93. package/dist/db-content-api/utilities/auth.js.map +0 -1
  94. package/dist/db-content-api/utilities/data/applyDefaults.d.ts.map +0 -1
  95. package/dist/db-content-api/utilities/data/applyDefaults.js.map +0 -1
  96. package/dist/db-content-api/utilities/data/castFieldValue.d.ts.map +0 -1
  97. package/dist/db-content-api/utilities/data/castFieldValue.js.map +0 -1
  98. package/dist/db-content-api/utilities/data/index.d.ts.map +0 -1
  99. package/dist/db-content-api/utilities/data/index.js.map +0 -1
  100. package/dist/db-content-api/utilities/data/removeVirtualFields.d.ts.map +0 -1
  101. package/dist/db-content-api/utilities/data/removeVirtualFields.js.map +0 -1
  102. package/dist/db-content-api/utilities/data/stripFields.d.ts.map +0 -1
  103. package/dist/db-content-api/utilities/data/stripFields.js.map +0 -1
  104. package/dist/db-content-api/utilities/data/transformPublishedLocale.d.ts.map +0 -1
  105. package/dist/db-content-api/utilities/data/transformPublishedLocale.js.map +0 -1
  106. package/dist/db-content-api/utilities/joins.d.ts.map +0 -1
  107. package/dist/db-content-api/utilities/joins.js.map +0 -1
  108. package/dist/db-content-api/utilities/locale/index.d.ts.map +0 -1
  109. package/dist/db-content-api/utilities/locale/index.js.map +0 -1
  110. package/dist/db-content-api/utilities/meta/buildLocalizedPaths.d.ts.map +0 -1
  111. package/dist/db-content-api/utilities/meta/buildLocalizedPaths.js.map +0 -1
  112. package/dist/db-content-api/utilities/meta/buildMeta.d.ts.map +0 -1
  113. package/dist/db-content-api/utilities/meta/buildMeta.js.map +0 -1
  114. package/dist/db-content-api/utilities/meta/buildPathTypes.d.ts.map +0 -1
  115. package/dist/db-content-api/utilities/meta/buildPathTypes.js.map +0 -1
  116. package/dist/db-content-api/utilities/where.d.ts.map +0 -1
  117. package/dist/db-content-api/utilities/where.js.map +0 -1
  118. package/dist/endpoints/health.d.ts.map +0 -1
  119. package/dist/endpoints/health.js.map +0 -1
  120. package/dist/endpoints/schema.d.ts.map +0 -1
  121. package/dist/endpoints/schema.js.map +0 -1
  122. package/dist/exports/client.d.ts.map +0 -1
  123. package/dist/exports/client.js.map +0 -1
  124. package/dist/index.d.ts.map +0 -1
  125. package/dist/index.js.map +0 -1
  126. package/dist/oauth/components/FigmaAvatar/index.d.ts.map +0 -1
  127. package/dist/oauth/components/FigmaAvatar/index.js.map +0 -1
  128. package/dist/oauth/components/FigmaAvatarCell/index.d.ts.map +0 -1
  129. package/dist/oauth/components/FigmaAvatarCell/index.js.map +0 -1
  130. package/dist/oauth/components/FigmaAvatarPlaceholder/index.d.ts.map +0 -1
  131. package/dist/oauth/components/FigmaAvatarPlaceholder/index.js.map +0 -1
  132. package/dist/oauth/components/LoginButton/index.d.ts.map +0 -1
  133. package/dist/oauth/components/LoginButton/index.js.map +0 -1
  134. package/dist/oauth/components/LogoutButton/index.d.ts.map +0 -1
  135. package/dist/oauth/components/LogoutButton/index.js.map +0 -1
  136. package/dist/oauth/defaults.d.ts.map +0 -1
  137. package/dist/oauth/defaults.js.map +0 -1
  138. package/dist/oauth/endpoints/getLoginEndpoint.d.ts.map +0 -1
  139. package/dist/oauth/endpoints/getLoginEndpoint.js.map +0 -1
  140. package/dist/oauth/endpoints/getLogoutEndpoint.d.ts.map +0 -1
  141. package/dist/oauth/endpoints/getLogoutEndpoint.js.map +0 -1
  142. package/dist/oauth/endpoints/getMetaEndpoint.d.ts.map +0 -1
  143. package/dist/oauth/endpoints/getMetaEndpoint.js.map +0 -1
  144. package/dist/oauth/endpoints/getRedirectToLoginEndpoint.d.ts.map +0 -1
  145. package/dist/oauth/endpoints/getRedirectToLoginEndpoint.js.map +0 -1
  146. package/dist/oauth/exports/client.d.ts.map +0 -1
  147. package/dist/oauth/exports/client.js.map +0 -1
  148. package/dist/oauth/hooks/afterLogout.d.ts.map +0 -1
  149. package/dist/oauth/hooks/afterLogout.js.map +0 -1
  150. package/dist/oauth/hooks/me.d.ts.map +0 -1
  151. package/dist/oauth/hooks/me.js.map +0 -1
  152. package/dist/oauth/hooks/refresh.d.ts.map +0 -1
  153. package/dist/oauth/hooks/refresh.js.map +0 -1
  154. package/dist/oauth/index.d.ts.map +0 -1
  155. package/dist/oauth/index.js.map +0 -1
  156. package/dist/oauth/strategy/index.d.ts.map +0 -1
  157. package/dist/oauth/strategy/index.js.map +0 -1
  158. package/dist/oauth/types.d.ts.map +0 -1
  159. package/dist/oauth/types.js.map +0 -1
  160. package/dist/oauth/utilities/clearCookie.d.ts.map +0 -1
  161. package/dist/oauth/utilities/clearCookie.js.map +0 -1
  162. package/dist/oauth/utilities/createCookieOptions.d.ts.map +0 -1
  163. package/dist/oauth/utilities/createCookieOptions.js.map +0 -1
  164. package/dist/oauth/utilities/createDebugLogger.d.ts.map +0 -1
  165. package/dist/oauth/utilities/createDebugLogger.js.map +0 -1
  166. package/dist/oauth/utilities/extractOrigin.d.ts.map +0 -1
  167. package/dist/oauth/utilities/extractOrigin.js.map +0 -1
  168. package/dist/oauth/utilities/getAdminCollectionSlug.d.ts.map +0 -1
  169. package/dist/oauth/utilities/getAdminCollectionSlug.js.map +0 -1
  170. package/dist/oauth/utilities/getAuthorizeURL.d.ts.map +0 -1
  171. package/dist/oauth/utilities/getAuthorizeURL.js.map +0 -1
  172. package/dist/oauth/utilities/getCookieExpiration.d.ts.map +0 -1
  173. package/dist/oauth/utilities/getCookieExpiration.js.map +0 -1
  174. package/dist/oauth/utilities/getSecondsFromTokenExp.d.ts.map +0 -1
  175. package/dist/oauth/utilities/getSecondsFromTokenExp.js.map +0 -1
  176. package/dist/oauth/utilities/getTokenExp.d.ts.map +0 -1
  177. package/dist/oauth/utilities/getTokenExp.js.map +0 -1
  178. package/dist/oauth/utilities/getUsernameField.d.ts.map +0 -1
  179. package/dist/oauth/utilities/getUsernameField.js.map +0 -1
  180. package/dist/oauth/utilities/hasUserTokenPropsChanged.d.ts.map +0 -1
  181. package/dist/oauth/utilities/hasUserTokenPropsChanged.js.map +0 -1
  182. package/dist/oauth/utilities/isDuplicateKeyError.d.ts.map +0 -1
  183. package/dist/oauth/utilities/isDuplicateKeyError.js.map +0 -1
  184. package/dist/oauth/utilities/mergeHeaders.d.ts.map +0 -1
  185. package/dist/oauth/utilities/mergeHeaders.js.map +0 -1
  186. package/dist/oauth/utilities/refreshTokens.d.ts.map +0 -1
  187. package/dist/oauth/utilities/refreshTokens.js.map +0 -1
  188. package/dist/plugin/adapter.d.ts.map +0 -1
  189. package/dist/plugin/adapter.js.map +0 -1
  190. package/dist/plugin/build-config.d.ts.map +0 -1
  191. package/dist/plugin/build-config.js.map +0 -1
  192. package/dist/storage-content-api/api-types.d.ts.map +0 -1
  193. package/dist/storage-content-api/api-types.js.map +0 -1
  194. package/dist/storage-content-api/client-uploads/ClientUploadHandler.d.ts.map +0 -1
  195. package/dist/storage-content-api/client-uploads/ClientUploadHandler.js.map +0 -1
  196. package/dist/storage-content-api/client-uploads/generateSignedURL.d.ts.map +0 -1
  197. package/dist/storage-content-api/client-uploads/generateSignedURL.js.map +0 -1
  198. package/dist/storage-content-api/client.d.ts.map +0 -1
  199. package/dist/storage-content-api/client.js.map +0 -1
  200. package/dist/storage-content-api/generateURL.d.ts.map +0 -1
  201. package/dist/storage-content-api/generateURL.js.map +0 -1
  202. package/dist/storage-content-api/handleDelete.d.ts.map +0 -1
  203. package/dist/storage-content-api/handleDelete.js.map +0 -1
  204. package/dist/storage-content-api/handleUpload.d.ts.map +0 -1
  205. package/dist/storage-content-api/handleUpload.js.map +0 -1
  206. package/dist/storage-content-api/index.d.ts.map +0 -1
  207. package/dist/storage-content-api/index.js.map +0 -1
  208. package/dist/storage-content-api/staticHandler.d.ts.map +0 -1
  209. package/dist/storage-content-api/staticHandler.js.map +0 -1
  210. package/dist/storage-content-api/types.d.ts.map +0 -1
  211. package/dist/storage-content-api/types.js.map +0 -1
  212. package/dist/storage-content-api/utilities/getSafeFilename.d.ts.map +0 -1
  213. package/dist/storage-content-api/utilities/getSafeFilename.js.map +0 -1
  214. package/dist/storage-content-api/utilities/index.d.ts.map +0 -1
  215. package/dist/storage-content-api/utilities/index.js.map +0 -1
  216. package/dist/types/config.d.ts.map +0 -1
  217. package/dist/types/config.js.map +0 -1
  218. package/dist/types.d.ts.map +0 -1
  219. package/dist/types.js.map +0 -1
  220. package/dist/utils/asset-collection.d.ts.map +0 -1
  221. package/dist/utils/asset-collection.js.map +0 -1
  222. package/dist/utils/build-detection.d.ts.map +0 -1
  223. package/dist/utils/build-detection.js.map +0 -1
  224. package/dist/utils/build-lambda-zip.d.ts.map +0 -1
  225. package/dist/utils/build-lambda-zip.js.map +0 -1
  226. package/dist/utils/config.d.ts.map +0 -1
  227. package/dist/utils/config.js.map +0 -1
  228. package/dist/utils/download-template.d.ts.map +0 -1
  229. package/dist/utils/download-template.js.map +0 -1
  230. package/dist/utils/env-management.d.ts.map +0 -1
  231. package/dist/utils/env-management.js.map +0 -1
  232. package/dist/utils/format-env-suffix.d.ts.map +0 -1
  233. package/dist/utils/format-env-suffix.js.map +0 -1
  234. package/dist/utils/format.d.ts.map +0 -1
  235. package/dist/utils/format.js.map +0 -1
  236. package/dist/utils/formatter.d.ts.map +0 -1
  237. package/dist/utils/formatter.js.map +0 -1
  238. package/dist/utils/git.d.ts.map +0 -1
  239. package/dist/utils/git.js.map +0 -1
  240. package/dist/utils/is-debug.d.ts.map +0 -1
  241. package/dist/utils/is-debug.js.map +0 -1
  242. package/dist/utils/lambda-config.d.ts.map +0 -1
  243. package/dist/utils/lambda-config.js.map +0 -1
  244. package/dist/utils/log.d.ts.map +0 -1
  245. package/dist/utils/log.js.map +0 -1
  246. package/dist/utils/messages.d.ts.map +0 -1
  247. package/dist/utils/messages.js.map +0 -1
  248. package/dist/utils/package-manager.d.ts.map +0 -1
  249. package/dist/utils/package-manager.js.map +0 -1
  250. package/dist/utils/payload-config-ast.d.ts.map +0 -1
  251. package/dist/utils/payload-config-ast.js.map +0 -1
  252. package/dist/utils/payload-config-finder.d.ts.map +0 -1
  253. package/dist/utils/payload-config-finder.js.map +0 -1
  254. package/dist/utils/payload-config-modifier.d.ts.map +0 -1
  255. package/dist/utils/payload-config-modifier.js.map +0 -1
  256. package/dist/utils/payload-package-check.d.ts.map +0 -1
  257. package/dist/utils/payload-package-check.js.map +0 -1
  258. package/dist/utils/project.d.ts.map +0 -1
  259. package/dist/utils/project.js.map +0 -1
  260. package/dist/utils/s3-upload.d.ts.map +0 -1
  261. package/dist/utils/s3-upload.js.map +0 -1
  262. package/dist/utils/token-display.d.ts.map +0 -1
  263. package/dist/utils/token-display.js.map +0 -1
  264. package/dist/utils/typescript-validator.d.ts.map +0 -1
  265. package/dist/utils/typescript-validator.js.map +0 -1
  266. package/dist/utils/version-check.d.ts.map +0 -1
  267. package/dist/utils/version-check.js.map +0 -1
@@ -2,8 +2,10 @@ import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage';
2
2
  import { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities';
3
3
  import { lexicalEditor } from '@payloadcms/richtext-lexical';
4
4
  import { buildConfig as payloadBuildConfig } from 'payload';
5
+ import { getBootstrapInfo } from '../api/control-plane.js';
6
+ import { getValidAccessToken } from '../auth/oauth-flow.js';
5
7
  import { getTokenStore } from '../auth/token-store.js';
6
- import { ENV_CONFIG, getEnvironment } from '../constants.js';
8
+ import { getEnvConfig } from '../constants.js';
7
9
  import { contentAPIAdapter } from '../db-content-api/index.js';
8
10
  import { health } from '../endpoints/health.js';
9
11
  import { schema } from '../endpoints/schema.js';
@@ -11,6 +13,8 @@ import { oAuth2Plugin } from '../oauth/index.js';
11
13
  import { createStorageClient } from '../storage-content-api/client.js';
12
14
  import { getGenerateSignedURLHandler } from '../storage-content-api/client-uploads/generateSignedURL.js';
13
15
  import { contentApiStorageAdapter } from '../storage-content-api/index.js';
16
+ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
17
+ import * as log from '../utils/log.js';
14
18
  /**
15
19
  * Fields added to users collection for Figma profile info
16
20
  */ const figmaUserFields = [
@@ -36,23 +40,51 @@ import { contentApiStorageAdapter } from '../storage-content-api/index.js';
36
40
  label: 'Profile Picture'
37
41
  }
38
42
  ];
43
+ function missingOAuthCredential(name) {
44
+ log.warning(`${name} could not be resolved. OAuth login will not work until this is configured. ` + 'Run `npx @payloadcms/figma init` to set up your project.');
45
+ return '';
46
+ }
39
47
  /**
40
- * Figma platform wrapper for Payload's buildConfig.
41
- * Automatically injects:
42
- * - Figma database adapter
43
- * - Secret (platform-provided)
44
- * - Default lexicalEditor (if no custom editor specified)
45
- *
46
- * @param config - Payload configuration (db and secret omitted, editor optional)
47
- * @returns Promise<SanitizedConfig> - Built Payload configuration
48
- */ export async function buildFigmaConfig(config) {
49
- // Validate required environment variable
50
- if (!config.figma.contentSystemId) {
51
- throw new Error('FIGMA_CONTENT_API_CONTENT_SYSTEM_ID is required. ' + 'Ensure your .env file contains: FIGMA_CONTENT_API_CONTENT_SYSTEM_ID=your-content-system-id');
48
+ * Fetch bootstrap data from the API and cache all environments locally.
49
+ * Returns the requested environment's data, or null if resolution fails.
50
+ */ async function resolveAndCacheBootstrap(store, projectId, environmentName) {
51
+ try {
52
+ const accessToken = await getValidAccessToken(store);
53
+ if (!accessToken) {
54
+ return null;
55
+ }
56
+ const bootstrapInfo = await getBootstrapInfo(accessToken, projectId);
57
+ cacheAllEnvironments(store, projectId, bootstrapInfo);
58
+ return store.getBootstrapData(projectId, environmentName);
59
+ } catch (error) {
60
+ log.warning(`Failed to resolve bootstrap data: ${error instanceof Error ? error.message : 'Unknown error'}. ` + 'Try running `npx @payloadcms/figma login` to refresh authentication.');
61
+ return null;
52
62
  }
53
- const env = getEnvironment();
54
- const url = process.env.FIGMA_CONTENT_API_URL || ENV_CONFIG[env].contentApiUrl;
55
- const { contentSystemId } = config.figma;
63
+ }
64
+ export async function buildFigmaConfig(config) {
65
+ const envConfig = getEnvConfig();
66
+ // Resolve contentSystemId: config first, then local store fallback (dev)
67
+ let { contentSystemId } = config.figma;
68
+ let bootstrapData = null;
69
+ if (!contentSystemId) {
70
+ const projectId = process.env.FIGMA_PROJECT_ID;
71
+ const environmentName = process.env.FIGMA_ENVIRONMENT_NAME;
72
+ if (projectId && environmentName) {
73
+ const store = getTokenStore();
74
+ bootstrapData = store.getBootstrapData(projectId, environmentName);
75
+ // Runtime fallback: fetch from API if not cached (e.g. environment switch without re-init)
76
+ if (!bootstrapData) {
77
+ bootstrapData = await resolveAndCacheBootstrap(store, projectId, environmentName);
78
+ }
79
+ if (bootstrapData) {
80
+ contentSystemId = bootstrapData.contentSystemId;
81
+ }
82
+ }
83
+ }
84
+ if (!contentSystemId) {
85
+ throw new Error('Content System ID could not be resolved. ' + 'Run `npx @payloadcms/figma init` to set up your project.');
86
+ }
87
+ const url = process.env.FIGMA_CONTENT_API_URL || envConfig.contentApiUrl;
56
88
  const isProduction = process.env.NODE_ENV === 'production';
57
89
  // Determine database adapter based on environment
58
90
  let db;
@@ -79,7 +111,7 @@ import { contentApiStorageAdapter } from '../storage-content-api/index.js';
79
111
  db = contentAPIAdapter({
80
112
  auth: {
81
113
  mode: 'tokenStore',
82
- tokenStore: getTokenStore(getEnvironment())
114
+ tokenStore: getTokenStore()
83
115
  },
84
116
  contentSystemId,
85
117
  url
@@ -101,7 +133,7 @@ import { contentApiStorageAdapter } from '../storage-content-api/index.js';
101
133
  } : {
102
134
  auth: {
103
135
  mode: 'tokenStore',
104
- tokenStore: getTokenStore(getEnvironment())
136
+ tokenStore: getTokenStore()
105
137
  },
106
138
  baseUrl: url,
107
139
  contentSystemId
@@ -141,7 +173,10 @@ import { contentApiStorageAdapter } from '../storage-content-api/index.js';
141
173
  ...config.admin
142
174
  },
143
175
  custom: {
144
- figma: config.figma || {}
176
+ figma: {
177
+ ...config.figma,
178
+ contentSystemId
179
+ }
145
180
  },
146
181
  db,
147
182
  editor: config.editor ?? lexicalEditor(),
@@ -171,9 +206,9 @@ import { contentApiStorageAdapter } from '../storage-content-api/index.js';
171
206
  response_mode: 'query',
172
207
  response_type: 'code'
173
208
  },
174
- clientID: process.env.FIGMA_OAUTH_CLIENT_ID,
175
- clientSecret: process.env.FIGMA_OAUTH_CLIENT_SECRET,
176
- identityMetadata: ENV_CONFIG[env].identityMetadata,
209
+ clientID: process.env.FIGMA_OAUTH_CLIENT_ID ?? bootstrapData?.oauthClientId ?? missingOAuthCredential('FIGMA_OAUTH_CLIENT_ID'),
210
+ clientSecret: process.env.FIGMA_OAUTH_CLIENT_SECRET ?? bootstrapData?.oauthClientSecret,
211
+ identityMetadata: envConfig.identityMetadata,
177
212
  scope: [
178
213
  'current_user:read'
179
214
  ],
@@ -1,11 +1,5 @@
1
- # Content System ID for Content API access
2
- FIGMA_CONTENT_API_CONTENT_SYSTEM_ID=
1
+ # Figma Project ID (CMS Resource ID)
2
+ FIGMA_PROJECT_ID=
3
3
 
4
- # Tenant ID for deploy operations
5
- FIGMA_TENANT_ID=
6
-
7
- # OAuth client ID for admin panel authentication
8
- FIGMA_OAUTH_CLIENT_ID=
9
-
10
- # OAuth client secret for admin panel authentication
11
- FIGMA_OAUTH_CLIENT_SECRET=
4
+ # Environment name (e.g., production, staging)
5
+ FIGMA_ENVIRONMENT_NAME=production
package/dist/types.d.ts CHANGED
@@ -4,6 +4,8 @@ export interface Args extends arg.Spec {
4
4
  '--env': StringConstructor;
5
5
  '--force': BooleanConstructor;
6
6
  '--help': BooleanConstructor;
7
+ '--id': StringConstructor;
8
+ '--infra-env': StringConstructor;
7
9
  '--list': BooleanConstructor;
8
10
  '--logout': BooleanConstructor;
9
11
  '--yes': BooleanConstructor;
@@ -0,0 +1,8 @@
1
+ import type { BootstrapInfo } from '../api/control-plane.js';
2
+ import type { TokenStore } from '../auth/token-store.js';
3
+ /**
4
+ * Cache bootstrap data for all environments in the local encrypted store.
5
+ * Called from init, env, and build-config to keep the store populated.
6
+ */
7
+ export declare function cacheAllEnvironments(tokenStore: TokenStore, projectId: string, bootstrapInfo: BootstrapInfo): void;
8
+ //# sourceMappingURL=cache-bootstrap.d.ts.map
@@ -0,0 +1,18 @@
1
+ import * as log from './log.js';
2
+ /**
3
+ * Cache bootstrap data for all environments in the local encrypted store.
4
+ * Called from init, env, and build-config to keep the store populated.
5
+ */ export function cacheAllEnvironments(tokenStore, projectId, bootstrapInfo) {
6
+ const { oauthCredentials } = bootstrapInfo;
7
+ for (const env of bootstrapInfo.environments){
8
+ tokenStore.setBootstrapData(projectId, env.name, {
9
+ contentSystemId: env.contentSystemId,
10
+ oauthClientId: oauthCredentials.oauthClientId,
11
+ oauthClientSecret: oauthCredentials.oauthClientSecret,
12
+ tenantInstanceId: env.tenantInstanceId
13
+ });
14
+ }
15
+ log.debug(`Cached bootstrap data for environments: ${bootstrapInfo.environments.map((e)=>e.name).join(', ')}`);
16
+ }
17
+
18
+ //# sourceMappingURL=cache-bootstrap.js.map
@@ -15,6 +15,7 @@ export function helpMessage() {
15
15
  ${pc.cyan('list-tokens')} Show stored token information
16
16
  ${pc.cyan('init')} Initialize a Figma CMS project
17
17
  ${pc.cyan('debug')} Show debug info for troubleshooting
18
+ ${pc.cyan('env')} Switch active environment
18
19
  ${pc.cyan('deploy')} Deploy your project to Figma
19
20
  ${pc.cyan('build-lambda-zip')} Build Lambda deployment zip
20
21
 
@@ -32,15 +33,27 @@ export function helpMessage() {
32
33
 
33
34
  ${pc.bold('INIT COMMAND')}
34
35
 
35
- ${pc.cyan('@payloadcms/figma init --id <content-system-id>')} Initialize project with Content System ID
36
- ${pc.cyan('@payloadcms/figma init --force')} Force reconfiguration of existing project
36
+ ${pc.cyan('@payloadcms/figma init --id <cms-resource-id>')} Initialize project
37
+ ${pc.cyan('@payloadcms/figma init --id <id> --env staging')} Initialize for specific environment
38
+ ${pc.dim('--force')} Force reconfiguration of existing project
39
+
40
+ ${pc.bold('ENV COMMAND')}
41
+
42
+ ${pc.cyan('@payloadcms/figma env')} List and switch environments
43
+ ${pc.cyan('@payloadcms/figma env production')} Switch to a named environment
37
44
 
38
45
  ${pc.bold('DEPLOY COMMAND')}
39
46
 
40
47
  ${pc.cyan('@payloadcms/figma deploy')} Deploy your project to Figma
48
+ ${pc.dim('--id <project-id>')} Override FIGMA_PROJECT_ID
49
+ ${pc.dim('--env <environment>')} Override FIGMA_ENVIRONMENT_NAME
41
50
  ${pc.dim('--yes, -y')} Skip confirmation prompts
42
51
  ${pc.dim('--skip-build')} Skip building and use existing build
43
52
 
53
+ ${pc.bold('GLOBAL OPTIONS')}
54
+
55
+ ${pc.dim('--infra-env <env>')} Target infrastructure (production or staging)
56
+
44
57
  ${pc.bold('DOCUMENTATION')}
45
58
 
46
59
  ${createTerminalLink('Getting Started', 'https://payloadcms.com/docs/getting-started/what-is-payload')}
@@ -0,0 +1,14 @@
1
+ import type { BootstrapEnvironment } from '../api/control-plane.js';
2
+ /**
3
+ * Resolve which environment to use from the bootstrap response.
4
+ *
5
+ * - Single environment: auto-select (environmentName optional)
6
+ * - Multiple environments: environmentName required, must match
7
+ *
8
+ * @throws Error if resolution fails (with available environment names)
9
+ */
10
+ export declare function resolveEnvironment(params: {
11
+ environmentName?: string;
12
+ environments: BootstrapEnvironment[];
13
+ }): BootstrapEnvironment;
14
+ //# sourceMappingURL=resolve-environment.d.ts.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolve which environment to use from the bootstrap response.
3
+ *
4
+ * - Single environment: auto-select (environmentName optional)
5
+ * - Multiple environments: environmentName required, must match
6
+ *
7
+ * @throws Error if resolution fails (with available environment names)
8
+ */ export function resolveEnvironment(params) {
9
+ const { environmentName, environments } = params;
10
+ if (environments.length === 0) {
11
+ throw new Error('No environments found for this project.');
12
+ }
13
+ if (environments.length === 1) {
14
+ return environments[0];
15
+ }
16
+ // Multiple environments — name required
17
+ const availableNames = environments.map((e)=>e.name).join(', ');
18
+ if (!environmentName) {
19
+ throw new Error(`Multiple environments available. Use --env to specify one: ${availableNames}`);
20
+ }
21
+ const matched = environments.find((e)=>e.name === environmentName);
22
+ if (!matched) {
23
+ throw new Error(`Environment "${environmentName}" not found. Available: ${availableNames}`);
24
+ }
25
+ return matched;
26
+ }
27
+
28
+ //# sourceMappingURL=resolve-environment.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payloadcms/figma",
3
- "version": "0.0.1-alpha.49",
3
+ "version": "0.0.1-alpha.50",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,7 +23,8 @@
23
23
  "files": [
24
24
  "package.json",
25
25
  "dist",
26
- "bin"
26
+ "bin",
27
+ "!dist/**/*.map"
27
28
  ],
28
29
  "dependencies": {
29
30
  "@clack/prompts": "^0.11.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"control-plane.d.ts","sourceRoot":"","sources":["../../src/api/control-plane.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/api/control-plane.ts"],"sourcesContent":["/**\n * Control Plane API Client\n *\n * TODO: Replace with real API calls when Control Plane is available\n */\n\nimport type { Tenant } from '../types/config.js'\n\nimport { ENV_CONFIG, getEnvironment } from '../constants.js'\nimport * as log from '../utils/log.js'\n\n/**\n * Get the Control Plane API base URL based on environment\n */\nfunction getControlPlaneBaseUrl(): string {\n return process.env.FIGMA_API_BASE_URL || ENV_CONFIG[getEnvironment()].apiBaseUrl\n}\n\n/**\n * Error thrown when Control Plane API calls fail\n */\nexport class ControlPlaneError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'ControlPlaneError'\n }\n}\n\n/**\n * Options for creating a new tenant\n */\nexport interface CreateTenantOptions {\n /** Name for the new CMS instance */\n name: string\n /** Optional parent ID */\n parentId?: string\n /** Optional parent type */\n parentType?: 'org' | 'team' | 'workspace'\n}\n\n/**\n * Options for creating a new deployment\n */\nexport interface CreateDeploymentOptions {\n /** List of static asset paths that need upload URLs */\n staticAssets: string[]\n}\n\n/**\n * Response from creating a new deployment\n */\nexport interface CreateDeploymentResponse {\n /** Upload URL for the Lambda function code zip */\n codeUploadUrl: string\n /** Unique deployment ID */\n deploymentId: string\n /** Map of static asset paths to their signed S3 upload URLs */\n staticAssetUploadUrls: Record<string, string>\n}\n\nexport interface CreateDeploymentApiResponse {\n error: boolean\n meta: {\n deployment_id: string\n lambda_zip_upload_url: string\n static_asset_upload_urls: Record<string, string>\n }\n status: number\n}\n\n/**\n * Options for performing a deployment\n */\nexport interface PerformDeploymentOptions {\n /** Deployment ID from createDeployment */\n deploymentId: string\n}\n\n/**\n * Response from performing a deployment\n */\nexport interface PerformDeploymentResponse {\n /** Deployment ID that was deployed */\n deploymentId: string\n /** Status message */\n message: string\n\n /** List of tenant domains */\n tenantDomains: string[]\n}\n\nexport interface PerformDeploymentApiResponse {\n error: boolean\n meta: {\n deployment_id: string\n message: string\n status: string\n tenant_domains: string[]\n }\n status: number\n}\n\n/**\n * Response from getDatasetInfo endpoint\n */\nexport type DatasetInfo = {\n cmsId: string\n oauthClientId: string\n oauthClientSecret: string\n tenantId: string\n}\n\n/**\n * Raw API response from /v1/cms/dataset/:datasetId\n */\ntype DatasetInfoApiResponse = {\n error: boolean\n meta: {\n cms_resource_id: string\n oauth_client_id: string\n oauth_client_secret: string\n tenant_instance_id: string\n }\n status: number\n}\n\n/**\n * List all tenant instances for the authenticated user\n * Maps to: GET /v1/tenant\n *\n * @param accessToken - OAuth access token\n * @returns Array of tenant instances\n */\nexport async function listTenants(accessToken: string): Promise<Tenant[]> {\n const shouldMock = process.env.FIGMA_MOCK_CONTROL_PLANE === 'true'\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Return sample tenants for testing\n log.debug('Using mock Control Plane API for listTenants (FIGMA_MOCK_CONTROL_PLANE != \"false\")')\n return [\n {\n id: 'tenant_abc123',\n createdAt: '2025-01-15T10:00:00Z',\n domain: 'my-blog.figmacms.com',\n parentId: 'team_456',\n parentType: 'team',\n status: 'active',\n },\n {\n id: 'tenant_def456',\n createdAt: '2025-01-10T14:30:00Z',\n domain: 'my-store.figmacms.com',\n parentId: 'team_456',\n parentType: 'team',\n status: 'active',\n },\n ]\n }\n\n // REAL API IMPLEMENTATION\n const response = await controlPlaneFetch({\n context: 'list tenants',\n options: { headers: { Authorization: `Bearer ${accessToken}` } },\n url: `${getControlPlaneBaseUrl()}/v1/tenant`,\n })\n\n return response.json()\n}\n\n/**\n * Create a new tenant instance\n * Maps to: POST /v1/tenant\n *\n * @param accessToken - OAuth access token\n * @param options - Tenant creation options\n * @returns Newly created tenant\n */\nexport async function createTenant(\n accessToken: string,\n options: CreateTenantOptions,\n): Promise<Tenant> {\n // Check if control plane API should be mocked\n const shouldMock = process.env.FIGMA_MOCK_CONTROL_PLANE === 'true'\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Generate mock tenant for testing\n log.debug('Using mock Control Plane API for createTenant (FIGMA_MOCK_CONTROL_PLANE != \"false\")')\n const tenantId = `tenant_${Math.random().toString(36).substring(2, 11)}`\n const slug = options.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')\n const domain = `${slug}.figmacms.com`\n\n return {\n id: tenantId,\n createdAt: new Date().toISOString(),\n domain,\n parentId: options.parentId || 'team_456',\n parentType: options.parentType || 'team',\n status: 'provisioning',\n }\n }\n\n // REAL API IMPLEMENTATION\n const response = await controlPlaneFetch({\n context: 'create tenant',\n options: {\n body: JSON.stringify({ name: options.name }),\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n },\n url: `${getControlPlaneBaseUrl()}/v1/tenant`,\n })\n\n return response.json()\n}\n\n/**\n * Get details for a specific tenant\n * Maps to: GET /v1/tenant/{tenant_id}\n *\n * @param accessToken - OAuth access token\n * @param tenantId - Tenant ID\n * @returns Tenant details\n */\nexport async function getTenantDetails(accessToken: string, tenantId: string): Promise<Tenant> {\n // Check if control plane API should be mocked\n const shouldMock = process.env.FIGMA_MOCK_CONTROL_PLANE === 'true'\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Return mock tenant details for testing\n log.debug(\n 'Using mock Control Plane API for getTenantDetails (FIGMA_MOCK_CONTROL_PLANE != \"false\")',\n )\n return {\n id: tenantId,\n createdAt: '2025-01-15T10:00:00Z',\n domain: 'example.figmacms.com',\n parentId: 'team_456',\n parentType: 'team',\n status: 'active',\n }\n }\n\n // REAL API IMPLEMENTATION\n const response = await controlPlaneFetch({\n context: 'get tenant details',\n options: { headers: { Authorization: `Bearer ${accessToken}` } },\n url: `${getControlPlaneBaseUrl()}/v1/tenant/${tenantId}`,\n })\n\n return response.json()\n}\n\n/**\n * Create a new deployment and get signed upload URLs\n * Maps to: POST /v1/tenant/{tenant_id}/deploy/create\n *\n * @param accessToken - OAuth access token\n * @param tenantId - Tenant ID\n * @param options - Deployment creation options\n * @returns Deployment ID and upload URLs\n */\nexport async function createDeployment(\n accessToken: string,\n tenantId: string,\n options: CreateDeploymentOptions,\n): Promise<CreateDeploymentResponse> {\n // Check if control plane API should be mocked\n const shouldMock = process.env.FIGMA_MOCK_CONTROL_PLANE === 'true'\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Generate mock deployment and signed URLs\n log.debug(\n 'Using mock Control Plane API for createDeployment (FIGMA_MOCK_CONTROL_PLANE === \"true\")',\n )\n\n const deploymentId = `deploy_${Math.random().toString(36).substring(2, 11)}`\n const mockBucket = 'figma-cms-deployments-mock'\n const mockRegion = 'us-west-2'\n const mockBaseUrl = `https://${mockBucket}.s3.${mockRegion}.amazonaws.com`\n\n // Generate mock signed URLs for static assets\n const staticAssetUploadUrls: Record<string, string> = {}\n for (const assetPath of options.staticAssets) {\n const encodedPath = encodeURIComponent(assetPath)\n staticAssetUploadUrls[assetPath] =\n `${mockBaseUrl}/${tenantId}/${deploymentId}/static/${encodedPath}?X-Amz-Signature=mock`\n }\n\n // Generate mock signed URL for code zip\n const codeUploadUrl = `${mockBaseUrl}/${tenantId}/${deploymentId}/lambda.zip?X-Amz-Signature=mock`\n\n return {\n codeUploadUrl,\n deploymentId,\n staticAssetUploadUrls,\n }\n }\n\n const url = `${getControlPlaneBaseUrl()}/v1/cms/tenant/${tenantId}/deploy/create`\n\n log.debug(`Calling createDeployment API at ${url}`)\n\n // REAL API IMPLEMENTATION\n const response = await controlPlaneFetch({\n context: 'create deployment',\n options: {\n body: JSON.stringify({ static_assets: options.staticAssets }),\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n },\n url,\n })\n\n const data = (await response.json()) as CreateDeploymentApiResponse\n\n // Map API response format to our interface\n return {\n codeUploadUrl: data.meta.lambda_zip_upload_url,\n deploymentId: data.meta.deployment_id,\n staticAssetUploadUrls: data.meta.static_asset_upload_urls,\n }\n}\n\n/**\n * Perform deployment for a created deployment ID\n * Maps to: POST /v1/tenant/{tenant_id}/deploy/perform\n *\n * @param accessToken - OAuth access token\n * @param tenantId - Tenant ID\n * @param options - Deployment perform options\n * @returns Deployment status and message\n */\nexport async function performDeployment(\n accessToken: string,\n tenantId: string,\n options: PerformDeploymentOptions,\n): Promise<PerformDeploymentResponse> {\n const shouldMock = process.env.FIGMA_MOCK_CONTROL_PLANE === 'true'\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Return mock deployment status\n log.debug(\n 'Using mock Control Plane API for performDeployment (FIGMA_MOCK_CONTROL_PLANE != \"false\")',\n )\n\n return {\n deploymentId: options.deploymentId,\n message: 'Deployment initiated successfully',\n tenantDomains: ['example.figmasource.site'],\n }\n }\n\n const url = `${getControlPlaneBaseUrl()}/v1/cms/tenant/${tenantId}/deploy/perform`\n log.debug(`Calling performDeployment API at ${url}`)\n\n // REAL API IMPLEMENTATION\n const response = await controlPlaneFetch({\n context: 'perform deployment',\n options: {\n body: JSON.stringify({ deployment_id: options.deploymentId }),\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n },\n url,\n })\n\n const data = (await response.json()) as PerformDeploymentApiResponse\n\n // Map API response to our interface\n return {\n deploymentId: data.meta.deployment_id,\n message: data.meta.message || 'Deployment initiated',\n tenantDomains: data.meta.tenant_domains,\n }\n}\n\n/**\n * Get dataset info including tenant ID and OAuth credentials\n * Maps to: GET /v1/cms/dataset/:datasetId\n *\n * @param accessToken - OAuth access token\n * @param datasetId - Content System ID (dataset ID)\n * @returns Dataset info with tenant ID and OAuth credentials\n */\nexport async function getDatasetInfo(accessToken: string, datasetId: string): Promise<DatasetInfo> {\n const url = `${getControlPlaneBaseUrl()}/v1/cms/dataset/${datasetId}`\n log.debug(`Calling getDatasetInfo API at ${url}`)\n\n const response = await controlPlaneFetch({\n context: 'get dataset info',\n options: { headers: { Authorization: `Bearer ${accessToken}` } },\n url,\n })\n\n const data = (await response.json()) as DatasetInfoApiResponse\n const sanitizedData = {\n ...data,\n meta: { ...data.meta, oauth_client_secret: '[REDACTED]' },\n }\n log.debug(`Dataset API response: ${JSON.stringify(sanitizedData, null, 2)}`)\n\n return {\n cmsId: data.meta.cms_resource_id,\n oauthClientId: data.meta.oauth_client_id,\n oauthClientSecret: data.meta.oauth_client_secret,\n tenantId: data.meta.tenant_instance_id,\n }\n}\n\nconst MAX_RETRIES = 3\nconst INITIAL_RETRY_DELAY = 1000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nasync function controlPlaneFetch(params: {\n context: string\n options: RequestInit\n url: string\n}): Promise<Response> {\n const { context, options, url } = params\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, options)\n\n if (response.ok) {\n return response\n }\n\n if (response.status < 500) {\n throw new ControlPlaneError(\n `Failed to ${context}: ${response.status} ${response.statusText}`,\n response.status,\n )\n }\n\n lastError = new ControlPlaneError(\n `Failed to ${context}: ${response.status} ${response.statusText}`,\n response.status,\n )\n } catch (error) {\n if (error instanceof ControlPlaneError) {\n throw error\n }\n lastError = error instanceof Error ? error : new Error('Unknown error')\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await sleep(INITIAL_RETRY_DELAY * Math.pow(2, attempt))\n }\n }\n\n throw new ControlPlaneError(\n `Network error while ${context}: ${lastError?.message || 'Unknown error'}`,\n undefined,\n lastError,\n )\n}\n"],"names":["ENV_CONFIG","getEnvironment","log","getControlPlaneBaseUrl","process","env","FIGMA_API_BASE_URL","apiBaseUrl","ControlPlaneError","Error","message","statusCode","cause","name","listTenants","accessToken","shouldMock","FIGMA_MOCK_CONTROL_PLANE","debug","id","createdAt","domain","parentId","parentType","status","response","controlPlaneFetch","context","options","headers","Authorization","url","json","createTenant","tenantId","Math","random","toString","substring","slug","toLowerCase","replace","Date","toISOString","body","JSON","stringify","method","getTenantDetails","createDeployment","deploymentId","mockBucket","mockRegion","mockBaseUrl","staticAssetUploadUrls","assetPath","staticAssets","encodedPath","encodeURIComponent","codeUploadUrl","static_assets","data","meta","lambda_zip_upload_url","deployment_id","static_asset_upload_urls","performDeployment","tenantDomains","tenant_domains","getDatasetInfo","datasetId","sanitizedData","oauth_client_secret","cmsId","cms_resource_id","oauthClientId","oauth_client_id","oauthClientSecret","tenant_instance_id","MAX_RETRIES","INITIAL_RETRY_DELAY","sleep","ms","Promise","resolve","setTimeout","params","lastError","attempt","fetch","ok","statusText","error","pow","undefined"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"figma-api.d.ts","sourceRoot":"","sources":["../../src/api/figma-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAwB,MAAM,kBAAkB,CAAA;AAK1E;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAG7B,UAAU,CAAC,EAAE,MAAM;IACnB,KAAK,CAAC,EAAE,KAAK;gBAFpB,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,CAwIvB;AAgFD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqC7E"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/api/figma-api.ts"],"sourcesContent":["/**\n * Figma API Client\n *\n * Client for Figma REST API (Pixie) endpoints\n * Handles project token generation and other CMS-related API calls\n */\n\nimport type { ProjectToken, ProjectTokenResponse } from '../auth/types.js'\n\nimport { ENV_CONFIG, getEnvironment } from '../constants.js'\nimport * as log from '../utils/log.js'\n\n/**\n * User info from Figma API /v1/me endpoint\n */\nexport type FigmaUserInfo = {\n email: string\n handle: string\n id: string\n img_url: string\n}\n\n/**\n * Error thrown when Figma API calls fail\n */\nexport class FigmaApiError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'FigmaApiError'\n }\n}\n\n/**\n * Get a project token (JWT) for a specific tenant/CMS\n *\n * This token is used by local Payload instances to authenticate\n * requests to the Content API. The token contains the CMS ID and\n * user identifying information in its claims.\n *\n * Endpoint: GET https://api.figma.com/v1/cms/:id/token/\n *\n * @param accessToken - OAuth access token for authentication\n * @param tenantId - The tenant/CMS ID\n * @returns ProjectToken with JWT and expiry information\n * @throws {FigmaApiError} If the API call fails\n */\nexport async function getProjectToken(\n accessToken: string,\n tenantId: string,\n): Promise<ProjectToken> {\n // Priority 1: Check for direct token value from environment\n const envTokenValue = process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE\n if (envTokenValue) {\n log.debug('Using direct project token from FIGMA_MOCK_PROJECT_TOKEN_VALUE')\n try {\n // Parse JWT payload to extract claims\n const parts = envTokenValue.split('.')\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format: must have 3 parts (header.payload.signature)')\n }\n\n const payload = parts[1]\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=')\n const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8')) as {\n aud?: string\n exp?: number\n }\n\n if (!decodedPayload.exp || typeof decodedPayload.exp !== 'number') {\n throw new Error('JWT payload missing exp claim')\n }\n\n // Extract tenant ID from aud claim (use parameter as fallback)\n const extractedTenantId = decodedPayload.aud || tenantId\n\n return {\n expiresAt: decodedPayload.exp * 1000, // Convert from seconds to milliseconds\n tenantId: extractedTenantId,\n token: envTokenValue,\n }\n } catch (error) {\n throw new FigmaApiError(\n `Invalid JWT token in FIGMA_MOCK_PROJECT_TOKEN_VALUE: ${error instanceof Error ? error.message : 'Unknown error'}`,\n undefined,\n error instanceof Error ? error : undefined,\n )\n }\n }\n\n // Priority 2: Check if project token API should be mocked\n // Default to false (use real API)\n const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN === 'true'\n\n try {\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Generate a mock JWT token for testing\n log.debug(\n 'Using mock project token API for getProjectToken (FIGMA_MOCK_PROJECT_TOKEN = \"true\")',\n )\n const mockToken = createMockJWT(tenantId, 25) // 25 minutes expiry\n\n // Mock response data\n const data: ProjectTokenResponse = {\n meta: {\n token: mockToken,\n },\n }\n\n if (!data.meta?.token) {\n throw new FigmaApiError('Invalid response: missing token in meta')\n }\n\n // Parse the JWT to extract expiry (it's in the payload)\n const expiresAt = parseJwtExpiry(data.meta.token)\n\n return {\n expiresAt,\n tenantId,\n token: data.meta.token,\n }\n }\n\n // REAL API IMPLEMENTATION\n const env = getEnvironment()\n const baseUrl = process.env.FIGMA_API_BASE_URL || ENV_CONFIG[env].apiBaseUrl\n const url = `${baseUrl}/v1/cms/${tenantId}/token/`\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n // Handle error responses\n let errorMessage = `Failed to get project token: ${response.status} ${response.statusText}`\n\n try {\n const errorBody = await response.json()\n // Only use string error messages (API may return { error: true })\n if (typeof errorBody.message === 'string') {\n errorMessage = errorBody.message\n } else if (typeof errorBody.error === 'string') {\n errorMessage = errorBody.error\n }\n } catch {\n // Ignore JSON parse errors for error body\n }\n\n // Provide actionable error for common cases\n if (response.status === 404) {\n errorMessage = `Content System ID \"${tenantId}\" not found. Ensure FIGMA_CONTENT_API_CONTENT_SYSTEM_ID is set to a valid content system ID in your .env file.`\n }\n\n throw new FigmaApiError(errorMessage, response.status)\n }\n\n const data: ProjectTokenResponse = await response.json()\n\n if (!data.meta?.token) {\n throw new FigmaApiError('Invalid response: missing token in meta')\n }\n\n // Parse the JWT to extract expiry (it's in the payload)\n const expiresAt = parseJwtExpiry(data.meta.token)\n\n return {\n expiresAt,\n tenantId,\n token: data.meta.token,\n }\n } catch (error) {\n if (error instanceof FigmaApiError) {\n throw error\n }\n\n // Handle network errors and other unexpected errors\n throw new FigmaApiError(\n `Failed to get project token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n undefined,\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Create a mock JWT token for testing\n * TODO: Remove this when real API is available\n */\nfunction createMockJWT(tenantId: string, expiresInMinutes: number): string {\n const header = {\n alg: 'RS256',\n typ: 'JWT',\n }\n\n const now = Math.floor(Date.now() / 1000) // Current time in seconds\n const exp = now + expiresInMinutes * 60 // Expiry in seconds\n\n const payload = {\n aud: tenantId, // Audience claim contains the CMS ID\n email: 'developer@example.com',\n exp,\n iat: now,\n sub: 'user123',\n }\n\n // Base64url encode\n const encodeBase64Url = (obj: object): string => {\n return Buffer.from(JSON.stringify(obj))\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n }\n\n const headerEncoded = encodeBase64Url(header)\n const payloadEncoded = encodeBase64Url(payload)\n const signature = 'mock_signature_' + Math.random().toString(36).substring(2, 15)\n\n return `${headerEncoded}.${payloadEncoded}.${signature}`\n}\n\n/**\n * Parse JWT expiry from token\n * JWTs contain an 'exp' claim in the payload (second part)\n *\n * @param token - JWT token string\n * @returns Unix timestamp in milliseconds when token expires\n * @throws {FigmaApiError} If JWT cannot be parsed\n */\nfunction parseJwtExpiry(token: string): number {\n try {\n // JWT format: header.payload.signature\n const parts = token.split('.')\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format')\n }\n\n // Decode the payload (second part)\n const payload = parts[1]\n // Use atob for base64url decoding (handle padding and URL-safe characters)\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=')\n\n const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8')) as {\n exp?: number\n }\n\n if (!decodedPayload.exp || typeof decodedPayload.exp !== 'number') {\n throw new Error('JWT payload missing exp claim')\n }\n\n // JWT exp claim is in seconds, convert to milliseconds\n return decodedPayload.exp * 1000\n } catch (error) {\n throw new FigmaApiError(\n `Failed to parse JWT expiry: ${error instanceof Error ? error.message : 'Unknown error'}`,\n undefined,\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Get user info from Figma API\n *\n * Endpoint: GET https://api.figma.com/v1/me\n * Requires OAuth access_token (not project token)\n *\n * @param accessToken - OAuth access token for authentication\n * @returns FigmaUserInfo with user profile data\n * @throws {FigmaApiError} If the API call fails\n */\nexport async function getUserInfo(accessToken: string): Promise<FigmaUserInfo> {\n const env = getEnvironment()\n const baseUrl = process.env.FIGMA_API_BASE_URL || ENV_CONFIG[env].apiBaseUrl\n const url = `${baseUrl}/v1/me`\n\n try {\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new FigmaApiError(\n `Failed to get user info: ${response.status} ${response.statusText}`,\n response.status,\n )\n }\n\n const data = await response.json()\n\n if (!data.id || !data.email || typeof data.handle !== 'string') {\n throw new FigmaApiError('Invalid user info response: missing required fields')\n }\n\n return data as FigmaUserInfo\n } catch (error) {\n if (error instanceof FigmaApiError) {\n throw error\n }\n\n throw new FigmaApiError(\n `Failed to get user info: ${error instanceof Error ? error.message : 'Unknown error'}`,\n undefined,\n error instanceof Error ? error : undefined,\n )\n }\n}\n"],"names":["ENV_CONFIG","getEnvironment","log","FigmaApiError","Error","message","statusCode","cause","name","getProjectToken","accessToken","tenantId","envTokenValue","process","env","FIGMA_MOCK_PROJECT_TOKEN_VALUE","debug","parts","split","length","payload","base64","replace","paddedBase64","padEnd","decodedPayload","JSON","parse","Buffer","from","toString","exp","extractedTenantId","aud","expiresAt","token","error","undefined","shouldMock","FIGMA_MOCK_PROJECT_TOKEN","mockToken","createMockJWT","data","meta","parseJwtExpiry","baseUrl","FIGMA_API_BASE_URL","apiBaseUrl","url","response","fetch","headers","Authorization","method","ok","errorMessage","status","statusText","errorBody","json","expiresInMinutes","header","alg","typ","now","Math","floor","Date","email","iat","sub","encodeBase64Url","obj","stringify","headerEncoded","payloadEncoded","signature","random","substring","getUserInfo","id","handle"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/auth/browser.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ5D;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd,GAAG,MAAM,CAYT"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/auth/browser.ts"],"sourcesContent":["import open from 'open'\n\n/**\n * Error thrown when browser launch fails\n */\nexport class BrowserError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'BrowserError'\n }\n}\n\n/**\n * Open a URL in the user's default browser\n *\n * Uses the `open` package for cross-platform browser launching\n *\n * @param url - URL to open in browser\n * @throws BrowserError if browser launch fails\n */\nexport async function openBrowser(url: string): Promise<void> {\n try {\n await open(url)\n } catch (error) {\n throw new BrowserError(\n `Failed to open browser: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n\n/**\n * Build OAuth authorization URL with all required parameters\n *\n * @param params - Authorization URL parameters\n * @returns Complete authorization URL\n */\nexport function buildAuthorizationUrl(params: {\n authorizationUrl: string\n clientId: string\n codeChallenge: string\n codeChallengeMethod: 'S256'\n redirectUri: string\n scopes: string[]\n state: string\n}): string {\n const url = new URL(params.authorizationUrl)\n\n url.searchParams.set('client_id', params.clientId)\n url.searchParams.set('redirect_uri', params.redirectUri)\n url.searchParams.set('response_type', 'code')\n url.searchParams.set('scope', params.scopes.join(' '))\n url.searchParams.set('state', params.state)\n url.searchParams.set('code_challenge', params.codeChallenge)\n url.searchParams.set('code_challenge_method', params.codeChallengeMethod)\n\n return url.toString()\n}\n"],"names":["open","BrowserError","Error","message","name","openBrowser","url","error","buildAuthorizationUrl","params","URL","authorizationUrl","searchParams","set","clientId","redirectUri","scopes","join","state","codeChallenge","codeChallengeMethod","toString"],"mappings":"AAAA,OAAOA,UAAU,OAAM;AAEvB;;CAEC,GACD,OAAO,MAAMC,qBAAqBC;IAChC,YAAYC,OAAe,CAAE;QAC3B,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,eAAeC,YAAYC,GAAW;IAC3C,IAAI;QACF,MAAMN,KAAKM;IACb,EAAE,OAAOC,OAAO;QACd,MAAM,IAAIN,aACR,CAAC,wBAAwB,EAAEM,iBAAiBL,QAAQK,MAAMJ,OAAO,GAAG,iBAAiB;IAEzF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASK,sBAAsBC,MAQrC;IACC,MAAMH,MAAM,IAAII,IAAID,OAAOE,gBAAgB;IAE3CL,IAAIM,YAAY,CAACC,GAAG,CAAC,aAAaJ,OAAOK,QAAQ;IACjDR,IAAIM,YAAY,CAACC,GAAG,CAAC,gBAAgBJ,OAAOM,WAAW;IACvDT,IAAIM,YAAY,CAACC,GAAG,CAAC,iBAAiB;IACtCP,IAAIM,YAAY,CAACC,GAAG,CAAC,SAASJ,OAAOO,MAAM,CAACC,IAAI,CAAC;IACjDX,IAAIM,YAAY,CAACC,GAAG,CAAC,SAASJ,OAAOS,KAAK;IAC1CZ,IAAIM,YAAY,CAACC,GAAG,CAAC,kBAAkBJ,OAAOU,aAAa;IAC3Db,IAAIM,YAAY,CAACC,GAAG,CAAC,yBAAyBJ,OAAOW,mBAAmB;IAExE,OAAOd,IAAIe,QAAQ;AACrB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"callback-server.d.ts","sourceRoot":"","sources":["../../src/auth/callback-server.ts"],"names":[],"mappings":"AAUA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAA;IACZ,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAG7B,IAAI,CAAC,EAAE,MAAM;gBADpB,OAAO,EAAE,MAAM,EACR,IAAI,CAAC,EAAE,MAAM,YAAA;CAKvB;AAED;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,UAAU,CAAC,CAAQ;IAC3B,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,SAAS,CAAQ;gBAEb,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO;IAM/D;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;;OAGG;IACH,aAAa,IAAI,MAAM;IAIvB;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;;;;;OAMG;IACG,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;CA8GtE"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/auth/callback-server.ts"],"sourcesContent":["import { readFileSync } from 'fs'\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'http'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\n\nimport { DEFAULT_CALLBACK_PORT } from '../config/oauth.js'\n\nconst filename = fileURLToPath(import.meta.url)\nconst dirname = path.dirname(filename)\n\n/**\n * Result from OAuth callback server\n */\nexport interface CallbackResult {\n /** Authorization code from OAuth provider */\n code: string\n /** State parameter for CSRF verification */\n state: string\n}\n\n/**\n * Error from OAuth callback\n */\nexport class CallbackError extends Error {\n constructor(\n message: string,\n public code?: string,\n ) {\n super(message)\n this.name = 'CallbackError'\n }\n}\n\n/**\n * Local HTTP server for handling OAuth callbacks\n *\n * Starts a temporary server on localhost to receive the OAuth authorization code.\n * The server automatically shuts down after receiving the callback or on timeout.\n *\n * Implements dynamic port allocation: tries preferred port first, falls back to\n * OS-assigned port if unavailable.\n */\nexport class CallbackServer {\n private actualPort?: number\n private preferredPort: number\n private server: null | Server = null\n private timeoutMs: number\n\n constructor(options: { port?: number; timeoutMs?: number } = {}) {\n this.preferredPort = options.port || DEFAULT_CALLBACK_PORT\n this.timeoutMs = options.timeoutMs || 120000 // 2 minutes default\n this.server = createServer()\n }\n\n /**\n * Render error page shown in browser when auth fails\n */\n private renderErrorPage(error: string, description?: string): string {\n const template = readFileSync(path.join(dirname, 'pages', 'error.html'), 'utf-8')\n return template\n .replace('{{description}}', description || 'An error occurred during authentication.')\n .replace('{{error}}', error ? `<p>Error: <span class=\"error-code\">${error}</span></p>` : '')\n }\n\n /**\n * Render success page shown in browser after successful auth\n */\n private renderSuccessPage(): string {\n return readFileSync(path.join(dirname, 'pages', 'success.html'), 'utf-8')\n }\n\n /**\n * Get the actual port the server is listening on\n * @returns Port number (may differ from preferred if that was in use)\n */\n getActualPort(): number {\n return this.actualPort || this.preferredPort\n }\n\n /**\n * Stop the server if running\n */\n stop(): void {\n if (this.server) {\n this.server.close()\n this.server = null\n }\n }\n\n /**\n * Start server and wait for OAuth callback\n *\n * @param expectedState - State parameter to verify CSRF protection\n * @returns Promise resolving to callback result with code and state\n * @throws CallbackError if callback fails or times out\n */\n async waitForCallback(expectedState: string): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n const cleanup = (handle: NodeJS.Timeout) => {\n clearTimeout(handle)\n if (this.server) {\n this.server.close()\n this.server = null\n }\n }\n\n // Set timeout for waiting\n const timeoutHandle = setTimeout(() => {\n cleanup(timeoutHandle)\n reject(new CallbackError('OAuth authorization timed out. Please try again.', 'timeout'))\n }, this.timeoutMs)\n\n // Create HTTP server\n this.server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '', `http://localhost:${this.getActualPort()}`)\n\n // Only handle /callback path\n if (url.pathname !== '/callback') {\n res.writeHead(404, { 'Content-Type': 'text/html' })\n res.end('<html><body><h1>404 Not Found</h1></body></html>')\n return\n }\n\n // Extract code and state from query params\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n const error = url.searchParams.get('error')\n const errorDescription = url.searchParams.get('error_description')\n\n // Handle OAuth errors\n if (error) {\n cleanup(timeoutHandle)\n res.writeHead(400, { 'Content-Type': 'text/html' })\n res.end(this.renderErrorPage(error, errorDescription || undefined))\n reject(\n new CallbackError(\n `OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ''}`,\n error,\n ),\n )\n return\n }\n\n // Validate required parameters\n if (!code || !state) {\n cleanup(timeoutHandle)\n res.writeHead(400, { 'Content-Type': 'text/html' })\n res.end(this.renderErrorPage('invalid_request', 'Missing code or state parameter'))\n reject(new CallbackError('Invalid callback: missing code or state', 'invalid_request'))\n return\n }\n\n // Verify state matches (CSRF protection)\n if (state !== expectedState) {\n cleanup(timeoutHandle)\n res.writeHead(400, { 'Content-Type': 'text/html' })\n res.end(this.renderErrorPage('invalid_state', 'State parameter mismatch'))\n reject(new CallbackError('State mismatch: possible CSRF attack', 'invalid_state'))\n return\n }\n\n // Success! Send response to browser\n res.writeHead(200, { 'Content-Type': 'text/html' })\n res.end(this.renderSuccessPage())\n\n // Resolve with the callback result\n cleanup(timeoutHandle)\n resolve({ code, state })\n })\n\n // Try to start server with dynamic port allocation\n const tryListen = (port: number, isRetry: boolean = false) => {\n if (!this.server) {\n reject(new CallbackError('Server instance is not available', 'server_error'))\n return\n }\n\n this.server.listen(port, '127.0.0.1', () => {\n // Capture the actual port that was assigned\n const addr = this.server!.address()\n if (addr && typeof addr === 'object') {\n this.actualPort = addr.port\n }\n })\n\n // Handle server errors (port in use, etc)\n this.server.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE' && !isRetry) {\n // Port in use, try with OS-assigned port (0 = dynamic)\n this.server!.removeAllListeners()\n tryListen(0, true)\n } else {\n cleanup(timeoutHandle)\n reject(\n new CallbackError(\n `Failed to start callback server: ${err.message}${!isRetry ? ' (tried fallback port)' : ''}`,\n ),\n )\n }\n })\n }\n\n // Start with preferred port\n tryListen(this.preferredPort)\n })\n }\n}\n"],"names":["readFileSync","createServer","path","fileURLToPath","DEFAULT_CALLBACK_PORT","filename","url","dirname","CallbackError","Error","message","code","name","CallbackServer","actualPort","preferredPort","server","timeoutMs","options","port","renderErrorPage","error","description","template","join","replace","renderSuccessPage","getActualPort","stop","close","waitForCallback","expectedState","Promise","resolve","reject","cleanup","handle","clearTimeout","timeoutHandle","setTimeout","req","res","URL","pathname","writeHead","end","searchParams","get","state","errorDescription","undefined","tryListen","isRetry","listen","addr","address","once","err","removeAllListeners"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"crypto-utils.d.ts","sourceRoot":"","sources":["../../src/auth/crypto-utils.ts"],"names":[],"mappings":"AAoEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAe5C"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/auth/crypto-utils.ts"],"sourcesContent":["import Conf from 'conf'\nimport crypto from 'crypto'\nimport os from 'os'\n\n/**\n * Get or generate a random salt for key derivation\n *\n * Generates a cryptographically secure random salt on first run and persists it\n * for future use. This ensures the encryption key is unique per machine and not\n * reproducible even if machine identifiers are known.\n *\n * @returns 32-byte hex string (64 characters)\n */\nfunction getOrCreateSalt(): string {\n // Configuration store for cryptographic salt\n const cryptoConf = new Conf<{ salt: string }>({\n configName: 'payloadcms-figma-crypto',\n projectName: 'payloadcms-figma',\n })\n let salt = cryptoConf.get('salt')\n if (!salt) {\n // Generate cryptographically secure random salt (32 bytes = 256 bits)\n salt = crypto.randomBytes(32).toString('hex')\n cryptoConf.set('salt', salt)\n }\n return salt\n}\n\n/**\n * Gather machine-specific entropy sources\n *\n * Collects multiple machine and user identifiers to create a unique input\n * for key derivation. More entropy sources make the key harder to guess.\n *\n * @returns Combined machine identifier string\n * @throws Error if unable to gather sufficient entropy\n */\nfunction gatherMachineEntropy(): string {\n try {\n const userInfo = os.userInfo()\n const networkInterfaces = os.networkInterfaces()\n\n // Get MAC address from first available network interface (if available)\n const macAddress = Object.values(networkInterfaces)\n .flat()\n .find((iface) => iface && !iface.internal && iface.mac !== '00:00:00:00:00:00')?.mac\n\n const entropy = [\n os.hostname(), // Machine hostname\n os.homedir(), // User home directory path\n userInfo.username, // OS username\n os.platform(), // Operating system platform\n os.arch(), // CPU architecture\n macAddress, // MAC address (if available)\n ].filter(Boolean) // Remove any undefined values\n\n if (entropy.length < 4) {\n throw new Error('Insufficient machine entropy available')\n }\n\n return entropy.join('::')\n } catch (error) {\n throw new Error(\n `Failed to gather machine entropy: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n\n/**\n * Derive a machine-specific encryption key for token storage\n *\n * Creates a deterministic encryption key using PBKDF2 with machine-specific\n * identifiers and a random salt. This provides stronger security than simple\n * hashing by making brute force attacks computationally expensive.\n *\n * Security model - What this protects against:\n * ✅ Accidental file exposure (cloud backups, shared drives)\n * ✅ Non-technical users reading token files\n * ✅ Brute force attacks (100k PBKDF2 iterations)\n * ✅ Key guessing even with known machine identifiers (random salt)\n *\n * Security model - What this DOES NOT protect against:\n * ❌ Local user compromise (same user can run this function)\n * ❌ Root/admin access attacks\n * ❌ Sophisticated attackers with system access\n *\n * For stronger security, use OS keychain (@napi-rs/keyring) instead.\n * This is a fallback for environments without keychain access.\n *\n * Implementation details:\n * - Uses PBKDF2 with SHA-256 and 100,000 iterations (~500ms-1s)\n * - Combines multiple machine identifiers for entropy\n * - Uses persistent random salt unique to each machine\n * - Produces consistent key for same machine (deterministic)\n * - Tokens are not portable between machines (by design)\n *\n * @returns 64-character hex string (32-byte key) suitable for AES-256 encryption\n * @throws Error if unable to derive key due to missing machine information\n */\nexport function deriveEncryptionKey(): string {\n const machineId = gatherMachineEntropy()\n const salt = getOrCreateSalt()\n\n // PBKDF2 with 100k iterations provides strong security against brute force\n // while remaining performant for CLI use (~500ms-1s key derivation time)\n const key = crypto.pbkdf2Sync(\n machineId, // Password (machine identifiers)\n salt, // Salt (random, persisted)\n 100000, // Iterations (100k for strong security)\n 32, // Key length (32 bytes = 256 bits for AES-256)\n 'sha256', // Hash algorithm\n )\n\n return key.toString('hex')\n}\n"],"names":["Conf","crypto","os","getOrCreateSalt","cryptoConf","configName","projectName","salt","get","randomBytes","toString","set","gatherMachineEntropy","userInfo","networkInterfaces","macAddress","Object","values","flat","find","iface","internal","mac","entropy","hostname","homedir","username","platform","arch","filter","Boolean","length","Error","join","error","message","deriveEncryptionKey","machineId","key","pbkdf2Sync"],"mappings":"AAAA,OAAOA,UAAU,OAAM;AACvB,OAAOC,YAAY,SAAQ;AAC3B,OAAOC,QAAQ,KAAI;AAEnB;;;;;;;;CAQC,GACD,SAASC;IACP,6CAA6C;IAC7C,MAAMC,aAAa,IAAIJ,KAAuB;QAC5CK,YAAY;QACZC,aAAa;IACf;IACA,IAAIC,OAAOH,WAAWI,GAAG,CAAC;IAC1B,IAAI,CAACD,MAAM;QACT,sEAAsE;QACtEA,OAAON,OAAOQ,WAAW,CAAC,IAAIC,QAAQ,CAAC;QACvCN,WAAWO,GAAG,CAAC,QAAQJ;IACzB;IACA,OAAOA;AACT;AAEA;;;;;;;;CAQC,GACD,SAASK;IACP,IAAI;QACF,MAAMC,WAAWX,GAAGW,QAAQ;QAC5B,MAAMC,oBAAoBZ,GAAGY,iBAAiB;QAE9C,wEAAwE;QACxE,MAAMC,aAAaC,OAAOC,MAAM,CAACH,mBAC9BI,IAAI,GACJC,IAAI,CAAC,CAACC,QAAUA,SAAS,CAACA,MAAMC,QAAQ,IAAID,MAAME,GAAG,KAAK,sBAAsBA;QAEnF,MAAMC,UAAU;YACdrB,GAAGsB,QAAQ;YACXtB,GAAGuB,OAAO;YACVZ,SAASa,QAAQ;YACjBxB,GAAGyB,QAAQ;YACXzB,GAAG0B,IAAI;YACPb;SACD,CAACc,MAAM,CAACC,SAAS,8BAA8B;;QAEhD,IAAIP,QAAQQ,MAAM,GAAG,GAAG;YACtB,MAAM,IAAIC,MAAM;QAClB;QAEA,OAAOT,QAAQU,IAAI,CAAC;IACtB,EAAE,OAAOC,OAAO;QACd,MAAM,IAAIF,MACR,CAAC,kCAAkC,EAAEE,iBAAiBF,QAAQE,MAAMC,OAAO,GAAG,iBAAiB;IAEnG;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASC;IACd,MAAMC,YAAYzB;IAClB,MAAML,OAAOJ;IAEb,2EAA2E;IAC3E,yEAAyE;IACzE,MAAMmC,MAAMrC,OAAOsC,UAAU,CAC3BF,WACA9B,MACA,QACA,IACA;IAGF,OAAO+B,IAAI5B,QAAQ,CAAC;AACtB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"jwt-validator.d.ts","sourceRoot":"","sources":["../../src/auth/jwt-validator.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAI5C;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAGlC,IAAI,EAAE,SAAS,GAAG,mBAAmB,GAAG,aAAa,GAAG,mBAAmB;IAC3E,KAAK,CAAC,EAAE,KAAK;gBAFpB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,SAAS,GAAG,mBAAmB,GAAG,aAAa,GAAG,mBAAmB,EAC3E,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAqCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,UAAU,CAAC,CA8ErB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAErC"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/auth/jwt-validator.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from 'jose'\nimport {\n JWKSMultipleMatchingKeys,\n JWKSNoMatchingKey,\n JWSInvalid,\n JWSSignatureVerificationFailed,\n JWTExpired,\n} from 'jose/errors'\n\nimport type { Environment } from '../constants.js'\nimport type { JWTPayload } from './types.js'\n\nimport { ENV_CONFIG } from '../constants.js'\n\n/**\n * JWT validation error with specific error codes\n */\nexport class JWTValidationError extends Error {\n constructor(\n message: string,\n public code: 'EXPIRED' | 'JWKS_FETCH_FAILED' | 'MISSING_KID' | 'SIGNATURE_INVALID',\n public cause?: Error,\n ) {\n super(message)\n this.name = 'JWTValidationError'\n }\n}\n\n/**\n * JWKS resolvers cached per environment\n * Uses jose's createRemoteJWKSet which handles:\n * - Automatic JWKS fetching from URI\n * - Built-in caching with configurable max age\n * - Automatic key rotation handling\n * - Cooldown protection between refetches\n */\nconst jwksResolvers = new Map<string, ReturnType<typeof createRemoteJWKSet>>()\n\n/**\n * Get or create a JWKS resolver for a given environment\n *\n * The resolver is cached in module scope and reused across validations.\n * Jose handles all the complexity of fetching, caching, and rotating keys.\n *\n * @param environment - Figma environment ('production' or 'staging')\n * @returns JWKS resolver function\n */\nfunction getJWKSResolver(environment: Environment) {\n const jwksUri = ENV_CONFIG[environment].jwksUri\n\n if (!jwksResolvers.has(jwksUri)) {\n jwksResolvers.set(\n jwksUri,\n createRemoteJWKSet(new URL(jwksUri), {\n cacheMaxAge: 24 * 60 * 60 * 1000, // 24 hours\n cooldownDuration: 30000, // 30 seconds between refetches\n }),\n )\n }\n\n return jwksResolvers.get(jwksUri)!\n}\n\n/**\n * Validate a project token JWT\n *\n * This function:\n * 1. Gets the JWKS resolver for the environment (cached)\n * 2. Uses jose's jwtVerify to validate signature and claims in one call\n * 3. Maps jose errors to our custom error types\n *\n * Jose automatically:\n * - Fetches JWKS from the URI (with caching)\n * - Matches the key by kid from the JWT header\n * - Verifies the signature using the correct algorithm\n * - Validates standard claims (exp, iat, nbf)\n * - Handles clock skew tolerance\n *\n * @param token - JWT token string\n * @param environment - Figma environment ('production' or 'staging')\n * @returns Promise resolving to validated JWT payload\n * @throws {JWTValidationError} If validation fails\n */\nexport async function validateProjectToken(\n token: string,\n environment: Environment,\n): Promise<JWTPayload> {\n try {\n const JWKS = getJWKSResolver(environment)\n\n const { payload } = await jwtVerify(token, JWKS, {\n clockTolerance: 300, // 5 minutes clock skew tolerance\n // Optional additional validation (uncomment if Figma provides these):\n // issuer: 'https://api.figma.com',\n // audience: 'payloadcms-figma',\n })\n\n return payload\n } catch (error: unknown) {\n // Map jose errors to our custom error types using instanceof checks\n // This is more reliable than checking error.code strings\n\n // Token expired\n if (error instanceof JWTExpired) {\n throw new JWTValidationError('Project token has expired', 'EXPIRED', error)\n }\n\n // Key not found in JWKS (possible key rotation or invalid kid)\n if (error instanceof JWKSNoMatchingKey) {\n throw new JWTValidationError(\n 'Key not found in JWKS - this may indicate key rotation or an invalid token',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Multiple matching keys (JWKS configuration issue)\n if (error instanceof JWKSMultipleMatchingKeys) {\n throw new JWTValidationError(\n 'Multiple matching keys found in JWKS',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Invalid signature or other verification failure\n if (error instanceof JWSSignatureVerificationFailed) {\n throw new JWTValidationError(\n 'JWT signature verification failed - token may be tampered with',\n 'SIGNATURE_INVALID',\n error,\n )\n }\n\n // Missing or invalid kid/alg in JWT header\n if (error instanceof JWSInvalid) {\n throw new JWTValidationError(\n 'Invalid JWT structure - missing required header fields (kid or alg)',\n 'MISSING_KID',\n error,\n )\n }\n\n // Network or fetch errors (not jose errors, generic Error instances)\n if (error instanceof Error) {\n const message = error.message\n if (\n message.includes('fetch') ||\n message.includes('network') ||\n message.includes('ENOTFOUND') ||\n message.includes('ECONNREFUSED')\n ) {\n throw new JWTValidationError(`Failed to fetch JWKS: ${message}`, 'JWKS_FETCH_FAILED', error)\n }\n }\n\n // Generic error fallback\n const message = error instanceof Error ? error.message : 'Unknown error'\n throw new JWTValidationError(\n `JWT validation failed: ${message}`,\n 'SIGNATURE_INVALID',\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Clear JWKS cache (useful for testing or forcing refresh)\n *\n * This clears the cached JWKS resolvers, forcing fresh fetches\n * on the next validation. Useful for:\n * - Testing with different JWKS\n * - Forcing key rotation updates\n * - Clearing state between test runs\n */\nexport function clearJWKSCache(): void {\n jwksResolvers.clear()\n}\n"],"names":["createRemoteJWKSet","jwtVerify","JWKSMultipleMatchingKeys","JWKSNoMatchingKey","JWSInvalid","JWSSignatureVerificationFailed","JWTExpired","ENV_CONFIG","JWTValidationError","Error","message","code","cause","name","jwksResolvers","Map","getJWKSResolver","environment","jwksUri","has","set","URL","cacheMaxAge","cooldownDuration","get","validateProjectToken","token","JWKS","payload","clockTolerance","error","includes","undefined","clearJWKSCache","clear"],"mappings":"AAAA,SAASA,kBAAkB,EAAEC,SAAS,QAAQ,OAAM;AACpD,SACEC,wBAAwB,EACxBC,iBAAiB,EACjBC,UAAU,EACVC,8BAA8B,EAC9BC,UAAU,QACL,cAAa;AAKpB,SAASC,UAAU,QAAQ,kBAAiB;AAE5C;;CAEC,GACD,OAAO,MAAMC,2BAA2BC;;;IACtC,YACEC,OAAe,EACf,AAAOC,IAA2E,EAClF,AAAOC,KAAa,CACpB;QACA,KAAK,CAACF,eAHCC,OAAAA,WACAC,QAAAA;QAGP,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA;;;;;;;CAOC,GACD,MAAMC,gBAAgB,IAAIC;AAE1B;;;;;;;;CAQC,GACD,SAASC,gBAAgBC,WAAwB;IAC/C,MAAMC,UAAUX,UAAU,CAACU,YAAY,CAACC,OAAO;IAE/C,IAAI,CAACJ,cAAcK,GAAG,CAACD,UAAU;QAC/BJ,cAAcM,GAAG,CACfF,SACAlB,mBAAmB,IAAIqB,IAAIH,UAAU;YACnCI,aAAa,KAAK,KAAK,KAAK;YAC5BC,kBAAkB;QACpB;IAEJ;IAEA,OAAOT,cAAcU,GAAG,CAACN;AAC3B;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,eAAeO,qBACpBC,KAAa,EACbT,WAAwB;IAExB,IAAI;QACF,MAAMU,OAAOX,gBAAgBC;QAE7B,MAAM,EAAEW,OAAO,EAAE,GAAG,MAAM3B,UAAUyB,OAAOC,MAAM;YAC/CE,gBAAgB;QAIlB;QAEA,OAAOD;IACT,EAAE,OAAOE,OAAgB;QACvB,oEAAoE;QACpE,yDAAyD;QAEzD,gBAAgB;QAChB,IAAIA,iBAAiBxB,YAAY;YAC/B,MAAM,IAAIE,mBAAmB,6BAA6B,WAAWsB;QACvE;QAEA,+DAA+D;QAC/D,IAAIA,iBAAiB3B,mBAAmB;YACtC,MAAM,IAAIK,mBACR,8EACA,qBACAsB;QAEJ;QAEA,oDAAoD;QACpD,IAAIA,iBAAiB5B,0BAA0B;YAC7C,MAAM,IAAIM,mBACR,wCACA,qBACAsB;QAEJ;QAEA,kDAAkD;QAClD,IAAIA,iBAAiBzB,gCAAgC;YACnD,MAAM,IAAIG,mBACR,kEACA,qBACAsB;QAEJ;QAEA,2CAA2C;QAC3C,IAAIA,iBAAiB1B,YAAY;YAC/B,MAAM,IAAII,mBACR,uEACA,eACAsB;QAEJ;QAEA,qEAAqE;QACrE,IAAIA,iBAAiBrB,OAAO;YAC1B,MAAMC,UAAUoB,MAAMpB,OAAO;YAC7B,IACEA,QAAQqB,QAAQ,CAAC,YACjBrB,QAAQqB,QAAQ,CAAC,cACjBrB,QAAQqB,QAAQ,CAAC,gBACjBrB,QAAQqB,QAAQ,CAAC,iBACjB;gBACA,MAAM,IAAIvB,mBAAmB,CAAC,sBAAsB,EAAEE,SAAS,EAAE,qBAAqBoB;YACxF;QACF;QAEA,yBAAyB;QACzB,MAAMpB,UAAUoB,iBAAiBrB,QAAQqB,MAAMpB,OAAO,GAAG;QACzD,MAAM,IAAIF,mBACR,CAAC,uBAAuB,EAAEE,SAAS,EACnC,qBACAoB,iBAAiBrB,QAAQqB,QAAQE;IAErC;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC;IACdnB,cAAcoB,KAAK;AACrB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"oauth-flow.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-flow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAS7C;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAG9B,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,MAAM,EAAE,WAAW,CAAA;IACnB,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAwI1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAkCxF"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/auth/oauth-flow.ts"],"sourcesContent":["import * as p from '@clack/prompts'\nimport crypto from 'crypto'\n\nimport type { TokenStore } from './token-store.js'\nimport type { FigmaTokens } from './types.js'\n\nimport { DEFAULT_CALLBACK_PORT, getOAuthConfig } from '../config/oauth.js'\nimport * as log from '../utils/log.js'\nimport { buildAuthorizationUrl, openBrowser } from './browser.js'\nimport { CallbackServer } from './callback-server.js'\nimport { generatePKCEPair } from './pkce.js'\nimport { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './refresh.js'\n\n/**\n * Error during OAuth flow\n */\nexport class OAuthFlowError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'OAuthFlowError'\n }\n}\n\n/**\n * Options for OAuth flow\n */\nexport interface OAuthFlowOptions {\n /** OAuth2 client ID (defaults to config) */\n clientId?: string\n /** Callback server port (defaults to DEFAULT_CALLBACK_PORT) */\n port?: number\n /** Redirect URI (defaults to config) */\n redirectUri?: string\n /** OAuth scopes (defaults to config) */\n scopes?: string[]\n /** Callback timeout in milliseconds (defaults to 120000) */\n timeoutMs?: number\n}\n\n/**\n * Result of OAuth flow\n */\nexport interface OAuthFlowResult {\n /** OAuth2 tokens */\n tokens: FigmaTokens\n /** Figma user ID */\n userId?: string\n}\n\n/**\n * Execute complete OAuth2 authorization flow\n *\n * This orchestrates the full OAuth flow:\n * 1. Generate PKCE pair for security\n * 2. Start local callback server\n * 3. Open browser to authorization URL\n * 4. Wait for user to authorize\n * 5. Exchange authorization code for tokens\n * 6. Store tokens securely\n *\n * @param tokenStore - Token store for persisting tokens\n * @param options - Flow configuration options\n * @returns Promise resolving to OAuth flow result\n * @throws OAuthFlowError if any step fails\n */\nexport async function executeOAuthFlow(\n tokenStore: TokenStore,\n options: OAuthFlowOptions = {},\n): Promise<OAuthFlowResult> {\n // Check if OAuth flow should be mocked\n // Default to false for OAuth\n const shouldMock = process.env.FIGMA_MOCK_OAUTH === 'true'\n\n const oauthConfig = getOAuthConfig()\n\n if (shouldMock) {\n // MOCK IMPLEMENTATION: Return mock tokens for testing\n log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH = true)')\n const mockTokens: FigmaTokens = {\n accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),\n expiresAt: Date.now() + 2 * 60 * 60 * 1000, // 2 hours\n refreshToken: 'figt_mock_refresh_token_' + crypto.randomBytes(16).toString('hex'),\n scopes: (options.scopes || oauthConfig.scopes) as FigmaTokens['scopes'],\n tokenType: 'Bearer',\n }\n\n // Store mock tokens\n tokenStore.setTokens(mockTokens)\n\n return {\n tokens: mockTokens,\n userId: 'mock_user_123',\n }\n }\n\n // REAL OAUTH IMPLEMENTATION\n const {\n clientId = oauthConfig.clientId,\n port = DEFAULT_CALLBACK_PORT,\n redirectUri = oauthConfig.redirectUri,\n scopes = oauthConfig.scopes,\n timeoutMs = 120000,\n } = options\n\n // Generate PKCE pair for enhanced security\n const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair()\n\n // Generate random state for CSRF protection\n const state = crypto.randomBytes(32).toString('base64url')\n\n // Create callback server\n const callbackServer = new CallbackServer({ port, timeoutMs })\n\n try {\n // Start waiting for callback (this starts the server and captures actual port)\n const callbackPromise = callbackServer.waitForCallback(state)\n\n // Wait a bit for server to start and determine actual port\n await new Promise((resolve) => setTimeout(resolve, 100))\n\n // Get the actual port (may differ from preferred if that was in use)\n const actualPort = callbackServer.getActualPort()\n\n // Build redirect URI with actual port\n const actualRedirectUri = redirectUri.replace(/:\\d+/, `:${actualPort}`)\n\n // Build authorization URL\n const authUrl = buildAuthorizationUrl({\n authorizationUrl: oauthConfig.authorizationUrl,\n clientId,\n codeChallenge,\n codeChallengeMethod,\n redirectUri: actualRedirectUri,\n scopes,\n state,\n })\n\n // Prompt user to open browser\n const shouldOpenBrowser = await p.confirm({\n initialValue: true,\n message: 'Authenticate with Figma in your browser?',\n })\n\n if (p.isCancel(shouldOpenBrowser)) {\n throw new OAuthFlowError('Authentication cancelled by user')\n }\n\n if (shouldOpenBrowser) {\n // Try to open browser, but don't fail if it doesn't work\n try {\n await openBrowser(authUrl)\n } catch (_error) {\n // Browser opening failed, show URL for manual opening\n log.warning('Could not open browser automatically.')\n log.info(`Please open this URL in your browser:`)\n log.info(` ${authUrl}`)\n }\n } else {\n // User declined automatic opening, show URL\n log.info(`Please open this URL manually in your browser:`)\n log.info(` ${authUrl}`)\n }\n\n // Wait for callback\n const { code } = await callbackPromise\n\n // Exchange authorization code for tokens\n let tokens: FigmaTokens\n try {\n tokens = await exchangeCodeForTokens(code, codeVerifier, actualRedirectUri, clientId)\n } catch (error) {\n throw new OAuthFlowError(\n 'Failed to exchange authorization code for tokens',\n error instanceof Error ? error : undefined,\n )\n }\n\n // Store tokens\n try {\n tokenStore.setTokens(tokens)\n } catch (error) {\n throw new OAuthFlowError('Failed to store tokens', error instanceof Error ? error : undefined)\n }\n\n return {\n tokens,\n userId: tokens.userId,\n }\n } catch (error) {\n // Clean up server on error\n callbackServer.stop()\n\n if (error instanceof OAuthFlowError) {\n throw error\n }\n\n throw new OAuthFlowError(\n `OAuth flow failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n } finally {\n // Ensure server is always stopped\n callbackServer.stop()\n }\n}\n\n/**\n * Get valid access token, refreshing if necessary\n *\n * Checks if stored tokens exist and are valid. If expired, automatically\n * refreshes using the refresh token. If no tokens exist, returns null.\n *\n * @param tokenStore - Token store to use\n * @returns Promise resolving to access token or null if no valid tokens\n * @throws OAuthFlowError if refresh fails\n */\nexport async function getValidAccessToken(tokenStore: TokenStore): Promise<null | string> {\n // Check if we have valid tokens\n if (tokenStore.hasValidTokens()) {\n return tokenStore.getAccessToken()\n }\n\n // Check if we have a refresh token\n const refreshToken = tokenStore.getRefreshToken()\n if (!refreshToken) {\n return null\n }\n\n // Try to refresh\n try {\n const tokens = await refreshAccessToken(refreshToken)\n tokenStore.setTokens(tokens)\n return tokens.accessToken\n } catch (error) {\n // Only clear tokens for actual auth failures, not transient errors\n const isAuthFailure =\n error instanceof TokenRefreshError &&\n (error.statusCode === 401 ||\n error.statusCode === 403 ||\n (error.statusCode === 400 && error.errorCode === 'invalid_grant'))\n\n if (isAuthFailure) {\n tokenStore.clearTokens()\n }\n\n throw new OAuthFlowError(\n 'Failed to refresh access token',\n error instanceof Error ? error : undefined,\n )\n }\n}\n"],"names":["p","crypto","DEFAULT_CALLBACK_PORT","getOAuthConfig","log","buildAuthorizationUrl","openBrowser","CallbackServer","generatePKCEPair","exchangeCodeForTokens","refreshAccessToken","TokenRefreshError","OAuthFlowError","Error","message","cause","name","executeOAuthFlow","tokenStore","options","shouldMock","process","env","FIGMA_MOCK_OAUTH","oauthConfig","debug","mockTokens","accessToken","randomBytes","toString","expiresAt","Date","now","refreshToken","scopes","tokenType","setTokens","tokens","userId","clientId","port","redirectUri","timeoutMs","codeChallenge","codeChallengeMethod","codeVerifier","state","callbackServer","callbackPromise","waitForCallback","Promise","resolve","setTimeout","actualPort","getActualPort","actualRedirectUri","replace","authUrl","authorizationUrl","shouldOpenBrowser","confirm","initialValue","isCancel","_error","warning","info","code","error","undefined","stop","getValidAccessToken","hasValidTokens","getAccessToken","getRefreshToken","isAuthFailure","statusCode","errorCode","clearTokens"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAA;IACrB,gDAAgD;IAChD,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAK7C;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,CAS3C"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/auth/pkce.ts"],"sourcesContent":["import crypto from 'crypto'\n\n/**\n * PKCE code verifier and challenge pair\n * @see https://datatracker.ietf.org/doc/html/rfc7636\n */\nexport interface PKCEPair {\n /** SHA256 hash of verifier, base64url encoded (without padding) */\n codeChallenge: string\n /** Challenge method (always S256 for SHA256) */\n codeChallengeMethod: 'S256'\n /** Random code verifier (43-128 characters from [A-Z, a-z, 0-9, -, ., _, ~]) */\n codeVerifier: string\n}\n\n/**\n * Generate a cryptographically random PKCE code verifier\n *\n * Creates a high-entropy random string suitable for use as an OAuth 2.0 PKCE code verifier.\n * The verifier must be 43-128 characters and use only [A-Z, a-z, 0-9, -, ., _, ~].\n *\n * @returns Random code verifier string (43 characters from 32 random bytes)\n * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.1\n */\nexport function generateCodeVerifier(): string {\n // Generate 32 random bytes (256 bits of entropy)\n // This will produce a 43-character base64url string\n const buffer = crypto.randomBytes(32)\n return base64UrlEncode(buffer)\n}\n\n/**\n * Generate code challenge from verifier using S256 method\n *\n * Creates a SHA256 hash of the code verifier and encodes it in base64url format.\n * Formula: challenge = BASE64URL(SHA256(ASCII(code_verifier)))\n *\n * @param verifier - The code verifier to hash\n * @returns Code challenge string (43 characters)\n * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.2\n */\nexport function generateCodeChallenge(verifier: string): string {\n const hash = crypto.createHash('sha256').update(verifier).digest()\n return base64UrlEncode(hash)\n}\n\n/**\n * Generate complete PKCE pair for OAuth flow\n *\n * Creates both a code verifier and its corresponding code challenge.\n * Use this at the start of an OAuth flow:\n * 1. Generate pair\n * 2. Store verifier securely (needed for token exchange)\n * 3. Send challenge in authorization request\n * 4. Send verifier in token exchange request\n *\n * @returns Complete PKCE pair with verifier, challenge, and method\n * @example\n * const { codeVerifier, codeChallenge, codeChallengeMethod } = generatePKCEPair()\n * // Store codeVerifier for later\n * // Include codeChallenge and codeChallengeMethod in authorization URL\n */\nexport function generatePKCEPair(): PKCEPair {\n const codeVerifier = generateCodeVerifier()\n const codeChallenge = generateCodeChallenge(codeVerifier)\n\n return {\n codeChallenge,\n codeChallengeMethod: 'S256',\n codeVerifier,\n }\n}\n\n/**\n * Base64URL encode without padding (RFC 7636 requirement)\n *\n * Converts a buffer to base64url format by:\n * 1. Base64 encoding\n * 2. Replacing + with -\n * 3. Replacing / with _\n * 4. Removing = padding\n *\n * This format is URL-safe and required by OAuth 2.0 PKCE spec.\n *\n * @param buffer - Buffer to encode\n * @returns Base64url encoded string (no padding)\n * @see https://datatracker.ietf.org/doc/html/rfc7636#appendix-A\n */\nfunction base64UrlEncode(buffer: Buffer): string {\n return buffer.toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '')\n}\n"],"names":["crypto","generateCodeVerifier","buffer","randomBytes","base64UrlEncode","generateCodeChallenge","verifier","hash","createHash","update","digest","generatePKCEPair","codeVerifier","codeChallenge","codeChallengeMethod","toString","replace"],"mappings":"AAAA,OAAOA,YAAY,SAAQ;AAe3B;;;;;;;;CAQC,GACD,OAAO,SAASC;IACd,iDAAiD;IACjD,oDAAoD;IACpD,MAAMC,SAASF,OAAOG,WAAW,CAAC;IAClC,OAAOC,gBAAgBF;AACzB;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASG,sBAAsBC,QAAgB;IACpD,MAAMC,OAAOP,OAAOQ,UAAU,CAAC,UAAUC,MAAM,CAACH,UAAUI,MAAM;IAChE,OAAON,gBAAgBG;AACzB;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASI;IACd,MAAMC,eAAeX;IACrB,MAAMY,gBAAgBR,sBAAsBO;IAE5C,OAAO;QACLC;QACAC,qBAAqB;QACrBF;IACF;AACF;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAASR,gBAAgBF,MAAc;IACrC,OAAOA,OAAOa,QAAQ,CAAC,UAAUC,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,MAAM;AACzF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"project-token.d.ts","sourceRoot":"","sources":["../../src/auth/project-token.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AASlD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,KAAK,CAAC,EAAE,KAAK;gBADpB,OAAO,EAAE,MAAM,EACR,KAAK,CAAC,EAAE,KAAK,YAAA;CAKvB;AAkCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAuExB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAMxB"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/auth/project-token.ts"],"sourcesContent":["/**\n * Project Token Management\n *\n * Handles project token (JWT) generation and refresh for authenticating\n * to the Content API. Project tokens are scoped to a specific tenant/CMS\n * and have a shorter lifespan (15-30 minutes) than OAuth tokens.\n */\n\nimport type { TokenStore } from './token-store.js'\nimport type { JWTPayload } from './types.js'\n\nimport { getProjectToken as fetchProjectToken } from '../api/figma-api.js'\nimport { getEnvironment } from '../constants.js'\nimport * as log from '../utils/log.js'\nimport { JWTValidationError, validateProjectToken } from './jwt-validator.js'\nimport { getValidAccessToken } from './oauth-flow.js'\n\n/**\n * Error during project token operations\n */\nexport class ProjectTokenError extends Error {\n constructor(\n message: string,\n public cause?: Error,\n ) {\n super(message)\n this.name = 'ProjectTokenError'\n }\n}\n\n/**\n * Parse JWT claims from token without validation\n * Used for mock tokens where signature validation is not needed\n *\n * @param token - JWT token string\n * @returns Parsed JWT payload\n * @throws {ProjectTokenError} If JWT cannot be parsed\n */\nfunction parseJWTClaims(token: string): JWTPayload {\n try {\n // JWT format: header.payload.signature\n const parts = token.split('.')\n if (parts.length !== 3) {\n throw new Error('Invalid JWT format')\n }\n\n // Decode the payload (second part)\n const payload = parts[1]\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=')\n\n const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8'))\n\n return decodedPayload as JWTPayload\n } catch (error) {\n throw new ProjectTokenError(\n `Failed to parse JWT claims: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined,\n )\n }\n}\n\n/**\n * Get a valid project token for a tenant, refreshing if necessary\n *\n * This function:\n * 1. Checks if a stored project token exists and is valid (not expired)\n * 2. If expired or missing, gets a valid user access token\n * 3. Calls the Figma API to generate a new project token\n * 4. Stores the new project token in encrypted storage\n * 5. Returns the JWT string\n *\n * The project token is used by local Payload instances to authenticate\n * requests to the Content API without requiring a round-trip to Sinatra.\n *\n * @param tokenStore - Token store for persisting tokens\n * @param tenantId - The tenant/CMS ID to get a token for\n * @returns Promise resolving to JWT token string or null if failed\n * @throws {ProjectTokenError} If token generation fails\n */\nexport async function getValidProjectToken(\n tokenStore: TokenStore,\n tenantId: string,\n): Promise<null | string> {\n if (process.env.FIGMA_CONTENT_API_ACCESS_KEY) {\n log.debug('FIGMA_CONTENT_API_ACCESS_KEY is set; skipping project token retrieval from API')\n return null\n }\n\n // Check for existing valid project token\n if (tokenStore.hasValidProjectToken(tenantId)) {\n const projectToken = tokenStore.getProjectToken(tenantId)\n return projectToken?.token || null\n }\n\n // We need to refresh the project token\n // An oauth access token is required for the call\n let accessToken: null | string\n try {\n accessToken = await getValidAccessToken(tokenStore)\n } catch (error) {\n throw new ProjectTokenError(\n 'Failed to get valid access token for project token refresh',\n error instanceof Error ? error : undefined,\n )\n }\n\n if (!accessToken) {\n // No valid user tokens available - user needs to authenticate\n return null\n }\n\n // Fetch a new project token from the Figma API\n const projectToken = await fetchProjectToken(accessToken, tenantId)\n\n // Validate the JWT signature and claims (skip for mocks)\n const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE\n const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false'\n let validatedClaims: JWTPayload\n\n if (hasEnvToken || shouldMock) {\n // MOCK: Skip JWT validation and parse claims directly\n log.debug('Skipping JWT validation for mock/env project token')\n validatedClaims = parseJWTClaims(projectToken.token)\n } else {\n // REAL: Validate JWT signature against JWKS\n try {\n validatedClaims = await validateProjectToken(projectToken.token, getEnvironment())\n } catch (validationError) {\n // Handle validation errors\n if (validationError instanceof JWTValidationError) {\n throw new ProjectTokenError(\n `Project token validation failed: ${validationError.message}`,\n validationError,\n )\n }\n throw validationError\n }\n }\n\n // Ensure exp claim is present\n if (!validatedClaims.exp) {\n throw new ProjectTokenError('Project token missing expiration (exp) claim')\n }\n\n // Store the project token with validated expiration and claims\n tokenStore.setProjectToken(tenantId, {\n claims: validatedClaims,\n expiresAt: validatedClaims.exp * 1000, // Convert from seconds to milliseconds\n tenantId,\n token: projectToken.token,\n })\n\n return projectToken.token\n}\n\n/**\n * Refresh a project token for a tenant\n *\n * Forces a refresh of the project token even if the current one is still valid.\n * Useful for testing or when you want to ensure you have the freshest token.\n *\n * @param tokenStore - Token store for persisting tokens\n * @param tenantId - The tenant/CMS ID to refresh token for\n * @returns Promise resolving to JWT token string or null if failed\n * @throws {ProjectTokenError} If token refresh fails\n */\nexport async function refreshProjectToken(\n tokenStore: TokenStore,\n tenantId: string,\n): Promise<null | string> {\n // Clear the existing project token to force a refresh\n tokenStore.clearProjectToken(tenantId)\n\n // Get a new token (which will fetch from API since we just cleared it)\n return getValidProjectToken(tokenStore, tenantId)\n}\n"],"names":["getProjectToken","fetchProjectToken","getEnvironment","log","JWTValidationError","validateProjectToken","getValidAccessToken","ProjectTokenError","Error","message","cause","name","parseJWTClaims","token","parts","split","length","payload","base64","replace","paddedBase64","padEnd","decodedPayload","JSON","parse","Buffer","from","toString","error","undefined","getValidProjectToken","tokenStore","tenantId","process","env","FIGMA_CONTENT_API_ACCESS_KEY","debug","hasValidProjectToken","projectToken","accessToken","hasEnvToken","FIGMA_MOCK_PROJECT_TOKEN_VALUE","shouldMock","FIGMA_MOCK_PROJECT_TOKEN","validatedClaims","validationError","exp","setProjectToken","claims","expiresAt","refreshProjectToken","clearProjectToken"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/auth/refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,YAAY,CAAA;AAKjE;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,UAAU,CAAC,EAAE,MAAM;IACnB,SAAS,CAAC,EAAE,MAAM;gBAFzB,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,SAAS,CAAC,EAAE,MAAM,YAAA;CAK5B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CA8DtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CA2FtB"}