@payloadcms/figma 0.0.1-alpha.0

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 (305) hide show
  1. package/LICENSE.md +11 -0
  2. package/bin/cli.js +4 -0
  3. package/dist/api/control-plane.d.ts +124 -0
  4. package/dist/api/control-plane.d.ts.map +1 -0
  5. package/dist/api/control-plane.js +245 -0
  6. package/dist/api/control-plane.js.map +1 -0
  7. package/dist/api/control-plane.spec.d.ts +2 -0
  8. package/dist/api/control-plane.spec.d.ts.map +1 -0
  9. package/dist/api/control-plane.spec.js +296 -0
  10. package/dist/api/control-plane.spec.js.map +1 -0
  11. package/dist/api/figma-api.d.ts +31 -0
  12. package/dist/api/figma-api.d.ts.map +1 -0
  13. package/dist/api/figma-api.js +169 -0
  14. package/dist/api/figma-api.js.map +1 -0
  15. package/dist/api/figma-api.spec.d.ts +14 -0
  16. package/dist/api/figma-api.spec.d.ts.map +1 -0
  17. package/dist/api/figma-api.spec.js +201 -0
  18. package/dist/api/figma-api.spec.js.map +1 -0
  19. package/dist/auth/__tests__/fixtures.d.ts +26 -0
  20. package/dist/auth/__tests__/fixtures.d.ts.map +1 -0
  21. package/dist/auth/__tests__/fixtures.js +67 -0
  22. package/dist/auth/__tests__/fixtures.js.map +1 -0
  23. package/dist/auth/__tests__/mocks.d.ts +33 -0
  24. package/dist/auth/__tests__/mocks.d.ts.map +1 -0
  25. package/dist/auth/__tests__/mocks.js +63 -0
  26. package/dist/auth/__tests__/mocks.js.map +1 -0
  27. package/dist/auth/browser.d.ts +31 -0
  28. package/dist/auth/browser.d.ts.map +1 -0
  29. package/dist/auth/browser.js +41 -0
  30. package/dist/auth/browser.js.map +1 -0
  31. package/dist/auth/callback-server.d.ts +61 -0
  32. package/dist/auth/callback-server.d.ts.map +1 -0
  33. package/dist/auth/callback-server.js +168 -0
  34. package/dist/auth/callback-server.js.map +1 -0
  35. package/dist/auth/callback-server.spec.d.ts +2 -0
  36. package/dist/auth/callback-server.spec.d.ts.map +1 -0
  37. package/dist/auth/callback-server.spec.js +85 -0
  38. package/dist/auth/callback-server.spec.js.map +1 -0
  39. package/dist/auth/crypto-utils.d.ts +33 -0
  40. package/dist/auth/crypto-utils.d.ts.map +1 -0
  41. package/dist/auth/crypto-utils.js +96 -0
  42. package/dist/auth/crypto-utils.js.map +1 -0
  43. package/dist/auth/crypto-utils.spec.d.ts +2 -0
  44. package/dist/auth/crypto-utils.spec.d.ts.map +1 -0
  45. package/dist/auth/crypto-utils.spec.js +36 -0
  46. package/dist/auth/crypto-utils.spec.js.map +1 -0
  47. package/dist/auth/jwt-validator.d.ts +41 -0
  48. package/dist/auth/jwt-validator.d.ts.map +1 -0
  49. package/dist/auth/jwt-validator.js +113 -0
  50. package/dist/auth/jwt-validator.js.map +1 -0
  51. package/dist/auth/jwt-validator.spec.d.ts +10 -0
  52. package/dist/auth/jwt-validator.spec.d.ts.map +1 -0
  53. package/dist/auth/jwt-validator.spec.js +236 -0
  54. package/dist/auth/jwt-validator.spec.js.map +1 -0
  55. package/dist/auth/oauth-flow.d.ts +62 -0
  56. package/dist/auth/oauth-flow.d.ts.map +1 -0
  57. package/dist/auth/oauth-flow.js +172 -0
  58. package/dist/auth/oauth-flow.js.map +1 -0
  59. package/dist/auth/oauth-flow.spec.d.ts +2 -0
  60. package/dist/auth/oauth-flow.spec.d.ts.map +1 -0
  61. package/dist/auth/oauth-flow.spec.js +134 -0
  62. package/dist/auth/oauth-flow.spec.js.map +1 -0
  63. package/dist/auth/pages/error.html +85 -0
  64. package/dist/auth/pages/success.html +74 -0
  65. package/dist/auth/pkce.d.ts +51 -0
  66. package/dist/auth/pkce.d.ts.map +1 -0
  67. package/dist/auth/pkce.js +71 -0
  68. package/dist/auth/pkce.js.map +1 -0
  69. package/dist/auth/pkce.spec.d.ts +2 -0
  70. package/dist/auth/pkce.spec.d.ts.map +1 -0
  71. package/dist/auth/pkce.spec.js +32 -0
  72. package/dist/auth/pkce.spec.js.map +1 -0
  73. package/dist/auth/project-token.d.ts +47 -0
  74. package/dist/auth/project-token.d.ts.map +1 -0
  75. package/dist/auth/project-token.js +134 -0
  76. package/dist/auth/project-token.js.map +1 -0
  77. package/dist/auth/project-token.spec.d.ts +2 -0
  78. package/dist/auth/project-token.spec.d.ts.map +1 -0
  79. package/dist/auth/project-token.spec.js +332 -0
  80. package/dist/auth/project-token.spec.js.map +1 -0
  81. package/dist/auth/refresh.d.ts +35 -0
  82. package/dist/auth/refresh.d.ts.map +1 -0
  83. package/dist/auth/refresh.js +149 -0
  84. package/dist/auth/refresh.js.map +1 -0
  85. package/dist/auth/refresh.spec.d.ts +2 -0
  86. package/dist/auth/refresh.spec.d.ts.map +1 -0
  87. package/dist/auth/refresh.spec.js +105 -0
  88. package/dist/auth/refresh.spec.js.map +1 -0
  89. package/dist/auth/token-store.d.ts +110 -0
  90. package/dist/auth/token-store.d.ts.map +1 -0
  91. package/dist/auth/token-store.js +188 -0
  92. package/dist/auth/token-store.js.map +1 -0
  93. package/dist/auth/token-store.spec.d.ts +2 -0
  94. package/dist/auth/token-store.spec.d.ts.map +1 -0
  95. package/dist/auth/token-store.spec.js +276 -0
  96. package/dist/auth/token-store.spec.js.map +1 -0
  97. package/dist/auth/types.d.ts +197 -0
  98. package/dist/auth/types.d.ts.map +1 -0
  99. package/dist/auth/types.js +6 -0
  100. package/dist/auth/types.js.map +1 -0
  101. package/dist/cli.d.ts +5 -0
  102. package/dist/cli.d.ts.map +1 -0
  103. package/dist/cli.js +108 -0
  104. package/dist/cli.js.map +1 -0
  105. package/dist/commands/__tests__/test-utils.d.ts +28 -0
  106. package/dist/commands/__tests__/test-utils.d.ts.map +1 -0
  107. package/dist/commands/__tests__/test-utils.js +34 -0
  108. package/dist/commands/__tests__/test-utils.js.map +1 -0
  109. package/dist/commands/deploy.d.ts +16 -0
  110. package/dist/commands/deploy.d.ts.map +1 -0
  111. package/dist/commands/deploy.js +297 -0
  112. package/dist/commands/deploy.js.map +1 -0
  113. package/dist/commands/deploy.spec.d.ts +2 -0
  114. package/dist/commands/deploy.spec.d.ts.map +1 -0
  115. package/dist/commands/deploy.spec.js +18 -0
  116. package/dist/commands/deploy.spec.js.map +1 -0
  117. package/dist/commands/init.d.ts +30 -0
  118. package/dist/commands/init.d.ts.map +1 -0
  119. package/dist/commands/init.js +410 -0
  120. package/dist/commands/init.js.map +1 -0
  121. package/dist/commands/init.spec.d.ts +2 -0
  122. package/dist/commands/init.spec.d.ts.map +1 -0
  123. package/dist/commands/init.spec.js +283 -0
  124. package/dist/commands/init.spec.js.map +1 -0
  125. package/dist/commands/list-tokens.d.ts +16 -0
  126. package/dist/commands/list-tokens.d.ts.map +1 -0
  127. package/dist/commands/list-tokens.js +44 -0
  128. package/dist/commands/list-tokens.js.map +1 -0
  129. package/dist/commands/login.d.ts +17 -0
  130. package/dist/commands/login.d.ts.map +1 -0
  131. package/dist/commands/login.js +69 -0
  132. package/dist/commands/login.js.map +1 -0
  133. package/dist/commands/logout.d.ts +16 -0
  134. package/dist/commands/logout.d.ts.map +1 -0
  135. package/dist/commands/logout.js +43 -0
  136. package/dist/commands/logout.js.map +1 -0
  137. package/dist/config/oauth.d.ts +19 -0
  138. package/dist/config/oauth.d.ts.map +1 -0
  139. package/dist/config/oauth.js +43 -0
  140. package/dist/config/oauth.js.map +1 -0
  141. package/dist/constants.d.ts +27 -0
  142. package/dist/constants.d.ts.map +1 -0
  143. package/dist/constants.js +27 -0
  144. package/dist/constants.js.map +1 -0
  145. package/dist/index.d.ts +5 -0
  146. package/dist/index.d.ts.map +1 -0
  147. package/dist/index.js +6 -0
  148. package/dist/index.js.map +1 -0
  149. package/dist/plugin/adapter.d.ts +5 -0
  150. package/dist/plugin/adapter.d.ts.map +1 -0
  151. package/dist/plugin/adapter.js +139 -0
  152. package/dist/plugin/adapter.js.map +1 -0
  153. package/dist/plugin/build-config.d.ts +37 -0
  154. package/dist/plugin/build-config.d.ts.map +1 -0
  155. package/dist/plugin/build-config.js +25 -0
  156. package/dist/plugin/build-config.js.map +1 -0
  157. package/dist/plugin/build-config.spec.d.ts +2 -0
  158. package/dist/plugin/build-config.spec.d.ts.map +1 -0
  159. package/dist/plugin/build-config.spec.js +72 -0
  160. package/dist/plugin/build-config.spec.js.map +1 -0
  161. package/dist/types/config.d.ts +45 -0
  162. package/dist/types/config.d.ts.map +1 -0
  163. package/dist/types/config.js +9 -0
  164. package/dist/types/config.js.map +1 -0
  165. package/dist/types.d.ts +60 -0
  166. package/dist/types.d.ts.map +1 -0
  167. package/dist/types.js +3 -0
  168. package/dist/types.js.map +1 -0
  169. package/dist/utils/asset-collection.d.ts +30 -0
  170. package/dist/utils/asset-collection.d.ts.map +1 -0
  171. package/dist/utils/asset-collection.js +117 -0
  172. package/dist/utils/asset-collection.js.map +1 -0
  173. package/dist/utils/asset-collection.spec.d.ts +2 -0
  174. package/dist/utils/asset-collection.spec.d.ts.map +1 -0
  175. package/dist/utils/asset-collection.spec.js +155 -0
  176. package/dist/utils/asset-collection.spec.js.map +1 -0
  177. package/dist/utils/build-detection.d.ts +32 -0
  178. package/dist/utils/build-detection.d.ts.map +1 -0
  179. package/dist/utils/build-detection.js +55 -0
  180. package/dist/utils/build-detection.js.map +1 -0
  181. package/dist/utils/build-detection.spec.d.ts +2 -0
  182. package/dist/utils/build-detection.spec.d.ts.map +1 -0
  183. package/dist/utils/build-detection.spec.js +110 -0
  184. package/dist/utils/build-detection.spec.js.map +1 -0
  185. package/dist/utils/config.d.ts +33 -0
  186. package/dist/utils/config.d.ts.map +1 -0
  187. package/dist/utils/config.js +78 -0
  188. package/dist/utils/config.js.map +1 -0
  189. package/dist/utils/config.spec.d.ts +2 -0
  190. package/dist/utils/config.spec.d.ts.map +1 -0
  191. package/dist/utils/config.spec.js +167 -0
  192. package/dist/utils/config.spec.js.map +1 -0
  193. package/dist/utils/download-template.d.ts +15 -0
  194. package/dist/utils/download-template.d.ts.map +1 -0
  195. package/dist/utils/download-template.js +74 -0
  196. package/dist/utils/download-template.js.map +1 -0
  197. package/dist/utils/download-template.spec.d.ts +2 -0
  198. package/dist/utils/download-template.spec.d.ts.map +1 -0
  199. package/dist/utils/download-template.spec.js +76 -0
  200. package/dist/utils/download-template.spec.js.map +1 -0
  201. package/dist/utils/env-management.d.ts +16 -0
  202. package/dist/utils/env-management.d.ts.map +1 -0
  203. package/dist/utils/env-management.js +98 -0
  204. package/dist/utils/env-management.js.map +1 -0
  205. package/dist/utils/env-management.spec.d.ts +2 -0
  206. package/dist/utils/env-management.spec.d.ts.map +1 -0
  207. package/dist/utils/env-management.spec.js +123 -0
  208. package/dist/utils/env-management.spec.js.map +1 -0
  209. package/dist/utils/format.d.ts +23 -0
  210. package/dist/utils/format.d.ts.map +1 -0
  211. package/dist/utils/format.js +49 -0
  212. package/dist/utils/format.js.map +1 -0
  213. package/dist/utils/formatter.d.ts +11 -0
  214. package/dist/utils/formatter.d.ts.map +1 -0
  215. package/dist/utils/formatter.js +47 -0
  216. package/dist/utils/formatter.js.map +1 -0
  217. package/dist/utils/git.d.ts +15 -0
  218. package/dist/utils/git.d.ts.map +1 -0
  219. package/dist/utils/git.js +92 -0
  220. package/dist/utils/git.js.map +1 -0
  221. package/dist/utils/git.spec.d.ts +2 -0
  222. package/dist/utils/git.spec.d.ts.map +1 -0
  223. package/dist/utils/git.spec.js +99 -0
  224. package/dist/utils/git.spec.js.map +1 -0
  225. package/dist/utils/is-debug.d.ts +6 -0
  226. package/dist/utils/is-debug.d.ts.map +1 -0
  227. package/dist/utils/is-debug.js +8 -0
  228. package/dist/utils/is-debug.js.map +1 -0
  229. package/dist/utils/lambda-config.d.ts +7 -0
  230. package/dist/utils/lambda-config.d.ts.map +1 -0
  231. package/dist/utils/lambda-config.js +144 -0
  232. package/dist/utils/lambda-config.js.map +1 -0
  233. package/dist/utils/lambda-config.spec.d.ts +2 -0
  234. package/dist/utils/lambda-config.spec.d.ts.map +1 -0
  235. package/dist/utils/lambda-config.spec.js +141 -0
  236. package/dist/utils/lambda-config.spec.js.map +1 -0
  237. package/dist/utils/log.d.ts +6 -0
  238. package/dist/utils/log.d.ts.map +1 -0
  239. package/dist/utils/log.js +22 -0
  240. package/dist/utils/log.js.map +1 -0
  241. package/dist/utils/messages.d.ts +10 -0
  242. package/dist/utils/messages.d.ts.map +1 -0
  243. package/dist/utils/messages.js +96 -0
  244. package/dist/utils/messages.js.map +1 -0
  245. package/dist/utils/package-manager.d.ts +12 -0
  246. package/dist/utils/package-manager.d.ts.map +1 -0
  247. package/dist/utils/package-manager.js +37 -0
  248. package/dist/utils/package-manager.js.map +1 -0
  249. package/dist/utils/payload-config-ast.d.ts +30 -0
  250. package/dist/utils/payload-config-ast.d.ts.map +1 -0
  251. package/dist/utils/payload-config-ast.js +259 -0
  252. package/dist/utils/payload-config-ast.js.map +1 -0
  253. package/dist/utils/payload-config-ast.spec.d.ts +2 -0
  254. package/dist/utils/payload-config-ast.spec.d.ts.map +1 -0
  255. package/dist/utils/payload-config-ast.spec.js +303 -0
  256. package/dist/utils/payload-config-ast.spec.js.map +1 -0
  257. package/dist/utils/payload-config-finder.d.ts +9 -0
  258. package/dist/utils/payload-config-finder.d.ts.map +1 -0
  259. package/dist/utils/payload-config-finder.js +26 -0
  260. package/dist/utils/payload-config-finder.js.map +1 -0
  261. package/dist/utils/payload-config-modifier.d.ts +18 -0
  262. package/dist/utils/payload-config-modifier.d.ts.map +1 -0
  263. package/dist/utils/payload-config-modifier.int.spec.d.ts +2 -0
  264. package/dist/utils/payload-config-modifier.int.spec.d.ts.map +1 -0
  265. package/dist/utils/payload-config-modifier.int.spec.js +116 -0
  266. package/dist/utils/payload-config-modifier.int.spec.js.map +1 -0
  267. package/dist/utils/payload-config-modifier.js +232 -0
  268. package/dist/utils/payload-config-modifier.js.map +1 -0
  269. package/dist/utils/payload-config-modifier.spec.d.ts +2 -0
  270. package/dist/utils/payload-config-modifier.spec.d.ts.map +1 -0
  271. package/dist/utils/payload-config-modifier.spec.js +162 -0
  272. package/dist/utils/payload-config-modifier.spec.js.map +1 -0
  273. package/dist/utils/payload-package-check.d.ts +15 -0
  274. package/dist/utils/payload-package-check.d.ts.map +1 -0
  275. package/dist/utils/payload-package-check.js +116 -0
  276. package/dist/utils/payload-package-check.js.map +1 -0
  277. package/dist/utils/payload-package-check.spec.d.ts +2 -0
  278. package/dist/utils/payload-package-check.spec.d.ts.map +1 -0
  279. package/dist/utils/payload-package-check.spec.js +148 -0
  280. package/dist/utils/payload-package-check.spec.js.map +1 -0
  281. package/dist/utils/project.d.ts +58 -0
  282. package/dist/utils/project.d.ts.map +1 -0
  283. package/dist/utils/project.js +252 -0
  284. package/dist/utils/project.js.map +1 -0
  285. package/dist/utils/project.spec.d.ts +2 -0
  286. package/dist/utils/project.spec.d.ts.map +1 -0
  287. package/dist/utils/project.spec.js +222 -0
  288. package/dist/utils/project.spec.js.map +1 -0
  289. package/dist/utils/s3-upload.d.ts +43 -0
  290. package/dist/utils/s3-upload.d.ts.map +1 -0
  291. package/dist/utils/s3-upload.js +128 -0
  292. package/dist/utils/s3-upload.js.map +1 -0
  293. package/dist/utils/s3-upload.spec.d.ts +2 -0
  294. package/dist/utils/s3-upload.spec.d.ts.map +1 -0
  295. package/dist/utils/s3-upload.spec.js +140 -0
  296. package/dist/utils/s3-upload.spec.js.map +1 -0
  297. package/dist/utils/token-display.d.ts +14 -0
  298. package/dist/utils/token-display.d.ts.map +1 -0
  299. package/dist/utils/token-display.js +62 -0
  300. package/dist/utils/token-display.js.map +1 -0
  301. package/dist/utils/typescript-validator.d.ts +11 -0
  302. package/dist/utils/typescript-validator.d.ts.map +1 -0
  303. package/dist/utils/typescript-validator.js +58 -0
  304. package/dist/utils/typescript-validator.js.map +1 -0
  305. package/package.json +69 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Project Token Management
3
+ *
4
+ * Handles project token (JWT) generation and refresh for authenticating
5
+ * to the Content API. Project tokens are scoped to a specific tenant/CMS
6
+ * and have a shorter lifespan (15-30 minutes) than OAuth tokens.
7
+ */ import { getProjectToken as fetchProjectToken } from '../api/figma-api.js';
8
+ import { getEnvironment } from '../constants.js';
9
+ import { isDebug } from '../utils/is-debug.js';
10
+ import * as log from '../utils/log.js';
11
+ import { JWTValidationError, validateProjectToken } from './jwt-validator.js';
12
+ import { getValidAccessToken } from './oauth-flow.js';
13
+ /**
14
+ * Error during project token operations
15
+ */ export class ProjectTokenError extends Error {
16
+ cause;
17
+ constructor(message, cause){
18
+ super(message), this.cause = cause;
19
+ this.name = 'ProjectTokenError';
20
+ }
21
+ }
22
+ /**
23
+ * Parse JWT claims from token without validation
24
+ * Used for mock tokens where signature validation is not needed
25
+ *
26
+ * @param token - JWT token string
27
+ * @returns Parsed JWT payload
28
+ * @throws {ProjectTokenError} If JWT cannot be parsed
29
+ */ function parseJWTClaims(token) {
30
+ try {
31
+ // JWT format: header.payload.signature
32
+ const parts = token.split('.');
33
+ if (parts.length !== 3) {
34
+ throw new Error('Invalid JWT format');
35
+ }
36
+ // Decode the payload (second part)
37
+ const payload = parts[1];
38
+ const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
39
+ const paddedBase64 = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=');
40
+ const decodedPayload = JSON.parse(Buffer.from(paddedBase64, 'base64').toString('utf-8'));
41
+ return decodedPayload;
42
+ } catch (error) {
43
+ throw new ProjectTokenError(`Failed to parse JWT claims: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
44
+ }
45
+ }
46
+ /**
47
+ * Get a valid project token for a tenant, refreshing if necessary
48
+ *
49
+ * This function:
50
+ * 1. Checks if a stored project token exists and is valid (not expired)
51
+ * 2. If expired or missing, gets a valid user access token
52
+ * 3. Calls the Figma API to generate a new project token
53
+ * 4. Stores the new project token in encrypted storage
54
+ * 5. Returns the JWT string
55
+ *
56
+ * The project token is used by local Payload instances to authenticate
57
+ * requests to the Content API without requiring a round-trip to Sinatra.
58
+ *
59
+ * @param tokenStore - Token store for persisting tokens
60
+ * @param tenantId - The tenant/CMS ID to get a token for
61
+ * @returns Promise resolving to JWT token string or null if failed
62
+ * @throws {ProjectTokenError} If token generation fails
63
+ */ export async function getValidProjectToken(tokenStore, tenantId) {
64
+ // Check if we have a valid project token already
65
+ if (tokenStore.hasValidProjectToken(tenantId)) {
66
+ const projectToken = tokenStore.getProjectToken(tenantId);
67
+ return projectToken?.token || null;
68
+ }
69
+ // We need to refresh the project token
70
+ // First, get a valid user access token (will auto-refresh if needed)
71
+ let accessToken;
72
+ try {
73
+ accessToken = await getValidAccessToken(tokenStore);
74
+ } catch (error) {
75
+ throw new ProjectTokenError('Failed to get valid access token for project token refresh', error instanceof Error ? error : undefined);
76
+ }
77
+ if (!accessToken) {
78
+ // No valid user tokens available - user needs to authenticate
79
+ return null;
80
+ }
81
+ // Fetch a new project token from the Figma API
82
+ const projectToken = await fetchProjectToken(accessToken, tenantId);
83
+ // Validate the JWT signature and claims (skip for mocks)
84
+ const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
85
+ let validatedClaims;
86
+ if (shouldMock) {
87
+ // MOCK: Skip JWT validation and parse claims directly
88
+ if (isDebug()) {
89
+ log.debug('Skipping JWT validation for mock project token');
90
+ }
91
+ validatedClaims = parseJWTClaims(projectToken.token);
92
+ } else {
93
+ // REAL: Validate JWT signature against JWKS
94
+ try {
95
+ validatedClaims = await validateProjectToken(projectToken.token, getEnvironment());
96
+ } catch (validationError) {
97
+ // Handle validation errors
98
+ if (validationError instanceof JWTValidationError) {
99
+ throw new ProjectTokenError(`Project token validation failed: ${validationError.message}`, validationError);
100
+ }
101
+ throw validationError;
102
+ }
103
+ }
104
+ // Ensure exp claim is present
105
+ if (!validatedClaims.exp) {
106
+ throw new ProjectTokenError('Project token missing expiration (exp) claim');
107
+ }
108
+ // Store the project token with validated expiration and claims
109
+ tokenStore.setProjectToken(tenantId, {
110
+ claims: validatedClaims,
111
+ expiresAt: validatedClaims.exp * 1000,
112
+ tenantId,
113
+ token: projectToken.token
114
+ });
115
+ return projectToken.token;
116
+ }
117
+ /**
118
+ * Refresh a project token for a tenant
119
+ *
120
+ * Forces a refresh of the project token even if the current one is still valid.
121
+ * Useful for testing or when you want to ensure you have the freshest token.
122
+ *
123
+ * @param tokenStore - Token store for persisting tokens
124
+ * @param tenantId - The tenant/CMS ID to refresh token for
125
+ * @returns Promise resolving to JWT token string or null if failed
126
+ * @throws {ProjectTokenError} If token refresh fails
127
+ */ export async function refreshProjectToken(tokenStore, tenantId) {
128
+ // Clear the existing project token to force a refresh
129
+ tokenStore.clearProjectToken(tenantId);
130
+ // Get a new token (which will fetch from API since we just cleared it)
131
+ return getValidProjectToken(tokenStore, tenantId);
132
+ }
133
+
134
+ //# sourceMappingURL=project-token.js.map
@@ -0,0 +1 @@
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 { isDebug } from '../utils/is-debug.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 // Check if we have a valid project token already\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 // First, get a valid user access token (will auto-refresh if needed)\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 shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false'\n let validatedClaims: JWTPayload\n\n if (shouldMock) {\n // MOCK: Skip JWT validation and parse claims directly\n if (isDebug()) {\n log.debug('Skipping JWT validation for mock project token')\n }\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","isDebug","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","hasValidProjectToken","projectToken","accessToken","shouldMock","process","env","FIGMA_MOCK_PROJECT_TOKEN","validatedClaims","debug","validationError","exp","setProjectToken","claims","expiresAt","refreshProjectToken","clearProjectToken"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=project-token.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-token.spec.d.ts","sourceRoot":"","sources":["../../src/auth/project-token.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,332 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as figmaApi from '../api/figma-api.js';
3
+ import * as constants from '../constants.js';
4
+ import { JWTValidationError, validateProjectToken } from './jwt-validator.js';
5
+ import * as oauthFlow from './oauth-flow.js';
6
+ import { getValidProjectToken, ProjectTokenError, refreshProjectToken } from './project-token.js';
7
+ import { TokenStore } from './token-store.js';
8
+ // Mock all external dependencies
9
+ vi.mock('../api/figma-api.js');
10
+ vi.mock('./oauth-flow.js');
11
+ vi.mock('../constants.js');
12
+ // Mock only the validateProjectToken function from jwt-validator
13
+ // Keep JWTValidationError class unmocked so it works properly in tests
14
+ vi.mock('./jwt-validator.js', async (importOriginal)=>{
15
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
16
+ const actual = await importOriginal();
17
+ return {
18
+ ...actual,
19
+ validateProjectToken: vi.fn()
20
+ };
21
+ });
22
+ describe('Project Token Management', ()=>{
23
+ let tokenStore;
24
+ const mockTenantId = 'test-tenant-123';
25
+ const mockAccessToken = 'figt_mock_access_token';
26
+ const mockProjectTokenJWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.mock.signature';
27
+ let originalEnv;
28
+ beforeEach(()=>{
29
+ // Create a unique test store for each test
30
+ tokenStore = new TokenStore({
31
+ configName: `payloadcms-figma-test-${Date.now()}`
32
+ });
33
+ // Disable mocking to test real validation flow
34
+ originalEnv = process.env.FIGMA_MOCK_PROJECT_TOKEN;
35
+ process.env.FIGMA_MOCK_PROJECT_TOKEN = 'false';
36
+ // Default mock implementations
37
+ vi.mocked(constants.getEnvironment).mockReturnValue('staging');
38
+ vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(mockAccessToken);
39
+ vi.mocked(figmaApi.getProjectToken).mockResolvedValue({
40
+ expiresAt: Date.now() + 30 * 60 * 1000,
41
+ tenantId: mockTenantId,
42
+ token: mockProjectTokenJWT
43
+ });
44
+ vi.mocked(validateProjectToken).mockResolvedValue({
45
+ aud: mockTenantId,
46
+ email: 'test@example.com',
47
+ exp: Math.floor(Date.now() / 1000) + 1800,
48
+ iat: Math.floor(Date.now() / 1000),
49
+ seat_type: 'full'
50
+ });
51
+ });
52
+ afterEach(()=>{
53
+ tokenStore.clearTokens();
54
+ tokenStore.clearAllProjectTokens();
55
+ vi.clearAllMocks();
56
+ // Restore original environment
57
+ if (originalEnv === undefined) {
58
+ delete process.env.FIGMA_MOCK_PROJECT_TOKEN;
59
+ } else {
60
+ process.env.FIGMA_MOCK_PROJECT_TOKEN = originalEnv;
61
+ }
62
+ });
63
+ describe('getValidProjectToken', ()=>{
64
+ describe('with valid cached token', ()=>{
65
+ it('should return cached token immediately without API calls', async ()=>{
66
+ // Store a valid project token
67
+ const validToken = {
68
+ claims: {
69
+ aud: mockTenantId,
70
+ email: 'test@example.com',
71
+ exp: Math.floor(Date.now() / 1000) + 1800,
72
+ iat: Math.floor(Date.now() / 1000),
73
+ seat_type: 'full'
74
+ },
75
+ expiresAt: Date.now() + 30 * 60 * 1000,
76
+ tenantId: mockTenantId,
77
+ token: mockProjectTokenJWT
78
+ };
79
+ tokenStore.setProjectToken(mockTenantId, validToken);
80
+ const result = await getValidProjectToken(tokenStore, mockTenantId);
81
+ expect(result).toBe(mockProjectTokenJWT);
82
+ // Should not call any external APIs
83
+ expect(oauthFlow.getValidAccessToken).not.toHaveBeenCalled();
84
+ expect(figmaApi.getProjectToken).not.toHaveBeenCalled();
85
+ expect(validateProjectToken).not.toHaveBeenCalled();
86
+ });
87
+ it('should use hasValidProjectToken for expiry check', async ()=>{
88
+ // Mock hasValidProjectToken to return true
89
+ const hasValidSpy = vi.spyOn(tokenStore, 'hasValidProjectToken').mockReturnValue(true);
90
+ const getTokenSpy = vi.spyOn(tokenStore, 'getProjectToken').mockReturnValue({
91
+ claims: {
92
+ exp: Math.floor(Date.now() / 1000) + 1800
93
+ },
94
+ expiresAt: Date.now() + 30 * 60 * 1000,
95
+ tenantId: mockTenantId,
96
+ token: mockProjectTokenJWT
97
+ });
98
+ await getValidProjectToken(tokenStore, mockTenantId);
99
+ expect(hasValidSpy).toHaveBeenCalledWith(mockTenantId);
100
+ expect(getTokenSpy).toHaveBeenCalledWith(mockTenantId);
101
+ });
102
+ });
103
+ describe('with expired or missing token', ()=>{
104
+ it('should fetch new token when no cached token exists', async ()=>{
105
+ const result = await getValidProjectToken(tokenStore, mockTenantId);
106
+ expect(result).toBe(mockProjectTokenJWT);
107
+ expect(oauthFlow.getValidAccessToken).toHaveBeenCalledWith(tokenStore);
108
+ expect(figmaApi.getProjectToken).toHaveBeenCalledWith(mockAccessToken, mockTenantId);
109
+ expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'staging');
110
+ });
111
+ it('should fetch new token when cached token is expired', async ()=>{
112
+ // Mock hasValidProjectToken to return false (expired)
113
+ vi.spyOn(tokenStore, 'hasValidProjectToken').mockReturnValue(false);
114
+ const result = await getValidProjectToken(tokenStore, mockTenantId);
115
+ expect(result).toBe(mockProjectTokenJWT);
116
+ expect(figmaApi.getProjectToken).toHaveBeenCalled();
117
+ });
118
+ it('should apply 5-minute expiry buffer through hasValidProjectToken', async ()=>{
119
+ // Store a token expiring in 4 minutes (within 5-minute buffer)
120
+ const expiringToken = {
121
+ claims: {
122
+ exp: Math.floor(Date.now() / 1000) + 240
123
+ },
124
+ expiresAt: Date.now() + 4 * 60 * 1000,
125
+ tenantId: mockTenantId,
126
+ token: 'expiring-token'
127
+ };
128
+ tokenStore.setProjectToken(mockTenantId, expiringToken);
129
+ // hasValidProjectToken should return false due to buffer
130
+ expect(tokenStore.hasValidProjectToken(mockTenantId)).toBe(false);
131
+ // Should refresh the token
132
+ await getValidProjectToken(tokenStore, mockTenantId);
133
+ expect(figmaApi.getProjectToken).toHaveBeenCalled();
134
+ });
135
+ });
136
+ describe('JWT validation integration', ()=>{
137
+ it('should validate JWT signature after fetching', async ()=>{
138
+ await getValidProjectToken(tokenStore, mockTenantId);
139
+ expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'staging');
140
+ });
141
+ it('should use correct environment for validation', async ()=>{
142
+ vi.mocked(constants.getEnvironment).mockReturnValue('production');
143
+ await getValidProjectToken(tokenStore, mockTenantId);
144
+ expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'production');
145
+ });
146
+ it('should throw ProjectTokenError on validation failure', async ()=>{
147
+ const validationError = new JWTValidationError('Invalid signature', 'SIGNATURE_INVALID');
148
+ vi.mocked(validateProjectToken).mockRejectedValue(validationError);
149
+ try {
150
+ await getValidProjectToken(tokenStore, mockTenantId);
151
+ expect.fail('Should have thrown ProjectTokenError');
152
+ } catch (error) {
153
+ expect(error).toBeInstanceOf(ProjectTokenError);
154
+ if (error instanceof ProjectTokenError) {
155
+ expect(error.message).toContain('Project token validation failed');
156
+ expect(error.message).toContain('Invalid signature');
157
+ expect(error.cause).toBe(validationError);
158
+ }
159
+ }
160
+ });
161
+ it('should rethrow non-JWTValidationError errors', async ()=>{
162
+ const genericError = new Error('Network error');
163
+ vi.mocked(validateProjectToken).mockRejectedValue(genericError);
164
+ await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(genericError);
165
+ });
166
+ it('should throw error if JWT missing exp claim', async ()=>{
167
+ // Mock validation returning claims without exp
168
+ vi.mocked(validateProjectToken).mockResolvedValue({
169
+ aud: mockTenantId
170
+ });
171
+ await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(ProjectTokenError);
172
+ await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow('missing expiration (exp) claim');
173
+ });
174
+ });
175
+ describe('claims storage', ()=>{
176
+ it('should store validated claims with token', async ()=>{
177
+ const mockClaims = {
178
+ aud: mockTenantId,
179
+ email: 'user@example.com',
180
+ exp: Math.floor(Date.now() / 1000) + 1800,
181
+ iat: Math.floor(Date.now() / 1000),
182
+ seat_type: 'full'
183
+ };
184
+ vi.mocked(validateProjectToken).mockResolvedValue(mockClaims);
185
+ await getValidProjectToken(tokenStore, mockTenantId);
186
+ const storedToken = tokenStore.getProjectToken(mockTenantId);
187
+ expect(storedToken?.claims).toEqual(mockClaims);
188
+ });
189
+ it('should convert exp from seconds to milliseconds for expiresAt', async ()=>{
190
+ const expInSeconds = Math.floor(Date.now() / 1000) + 1800 // 30 min from now
191
+ ;
192
+ vi.mocked(validateProjectToken).mockResolvedValue({
193
+ aud: mockTenantId,
194
+ exp: expInSeconds
195
+ });
196
+ await getValidProjectToken(tokenStore, mockTenantId);
197
+ const storedToken = tokenStore.getProjectToken(mockTenantId);
198
+ expect(storedToken?.expiresAt).toBe(expInSeconds * 1000);
199
+ });
200
+ it('should store token with correct tenantId', async ()=>{
201
+ await getValidProjectToken(tokenStore, mockTenantId);
202
+ const storedToken = tokenStore.getProjectToken(mockTenantId);
203
+ expect(storedToken?.tenantId).toBe(mockTenantId);
204
+ expect(storedToken?.token).toBe(mockProjectTokenJWT);
205
+ });
206
+ });
207
+ describe('error handling', ()=>{
208
+ it('should throw ProjectTokenError when no user access token available', async ()=>{
209
+ vi.mocked(oauthFlow.getValidAccessToken).mockRejectedValue(new Error('User not authenticated'));
210
+ await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(ProjectTokenError);
211
+ await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow('Failed to get valid access token');
212
+ });
213
+ it('should return null when getValidAccessToken returns null', async ()=>{
214
+ vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(null);
215
+ const result = await getValidProjectToken(tokenStore, mockTenantId);
216
+ expect(result).toBeNull();
217
+ // Should not call API if no access token
218
+ expect(figmaApi.getProjectToken).not.toHaveBeenCalled();
219
+ });
220
+ it('should handle API 401 errors', async ()=>{
221
+ const apiError = new figmaApi.FigmaApiError('Unauthorized', 401);
222
+ vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError);
223
+ try {
224
+ await getValidProjectToken(tokenStore, mockTenantId);
225
+ expect.fail('Should have thrown error');
226
+ } catch (error) {
227
+ expect(error).toBe(apiError);
228
+ expect(error).toBeInstanceOf(figmaApi.FigmaApiError);
229
+ }
230
+ });
231
+ it('should handle API 404 errors (tenant not found)', async ()=>{
232
+ const apiError = new figmaApi.FigmaApiError('Tenant not found', 404);
233
+ vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError);
234
+ try {
235
+ await getValidProjectToken(tokenStore, mockTenantId);
236
+ expect.fail('Should have thrown error');
237
+ } catch (error) {
238
+ expect(error).toBe(apiError);
239
+ expect(error).toBeInstanceOf(figmaApi.FigmaApiError);
240
+ }
241
+ });
242
+ it('should handle network errors', async ()=>{
243
+ const networkError = new Error('Network timeout');
244
+ vi.mocked(figmaApi.getProjectToken).mockRejectedValue(networkError);
245
+ await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(networkError);
246
+ });
247
+ it('should wrap getValidAccessToken errors in ProjectTokenError', async ()=>{
248
+ const authError = new Error('OAuth refresh failed');
249
+ vi.mocked(oauthFlow.getValidAccessToken).mockRejectedValue(authError);
250
+ try {
251
+ await getValidProjectToken(tokenStore, mockTenantId);
252
+ expect.fail('Should have thrown ProjectTokenError');
253
+ } catch (error) {
254
+ expect(error).toBeInstanceOf(ProjectTokenError);
255
+ if (error instanceof ProjectTokenError) {
256
+ expect(error.cause).toBe(authError);
257
+ expect(error.message).toContain('Failed to get valid access token');
258
+ }
259
+ }
260
+ });
261
+ });
262
+ });
263
+ describe('refreshProjectToken', ()=>{
264
+ it('should clear existing token before fetching new one', async ()=>{
265
+ // Store an existing token
266
+ const existingToken = {
267
+ claims: {
268
+ exp: Math.floor(Date.now() / 1000) + 1800
269
+ },
270
+ expiresAt: Date.now() + 30 * 60 * 1000,
271
+ tenantId: mockTenantId,
272
+ token: 'old-token'
273
+ };
274
+ tokenStore.setProjectToken(mockTenantId, existingToken);
275
+ const clearSpy = vi.spyOn(tokenStore, 'clearProjectToken');
276
+ await refreshProjectToken(tokenStore, mockTenantId);
277
+ expect(clearSpy).toHaveBeenCalledWith(mockTenantId);
278
+ });
279
+ it('should call getValidProjectToken after clearing', async ()=>{
280
+ const result = await refreshProjectToken(tokenStore, mockTenantId);
281
+ expect(result).toBe(mockProjectTokenJWT);
282
+ // Verify API was called (meaning cache was bypassed)
283
+ expect(figmaApi.getProjectToken).toHaveBeenCalled();
284
+ });
285
+ it('should force refresh even if token is still valid', async ()=>{
286
+ // Store a valid token
287
+ const validToken = {
288
+ claims: {
289
+ exp: Math.floor(Date.now() / 1000) + 3600
290
+ },
291
+ expiresAt: Date.now() + 60 * 60 * 1000,
292
+ tenantId: mockTenantId,
293
+ token: 'valid-token'
294
+ };
295
+ tokenStore.setProjectToken(mockTenantId, validToken);
296
+ // Verify it's considered valid
297
+ expect(tokenStore.hasValidProjectToken(mockTenantId)).toBe(true);
298
+ await refreshProjectToken(tokenStore, mockTenantId);
299
+ // Should have fetched new token despite valid cache
300
+ expect(figmaApi.getProjectToken).toHaveBeenCalled();
301
+ });
302
+ it('should return new token after refresh', async ()=>{
303
+ const newTokenJWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.new.signature';
304
+ vi.mocked(figmaApi.getProjectToken).mockResolvedValue({
305
+ expiresAt: Date.now() + 30 * 60 * 1000,
306
+ tenantId: mockTenantId,
307
+ token: newTokenJWT
308
+ });
309
+ const result = await refreshProjectToken(tokenStore, mockTenantId);
310
+ expect(result).toBe(newTokenJWT);
311
+ expect(tokenStore.getProjectToken(mockTenantId)?.token).toBe(newTokenJWT);
312
+ });
313
+ it('should propagate errors from getValidProjectToken', async ()=>{
314
+ const apiError = new figmaApi.FigmaApiError('API Error', 500);
315
+ vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError);
316
+ try {
317
+ await refreshProjectToken(tokenStore, mockTenantId);
318
+ expect.fail('Should have thrown error');
319
+ } catch (error) {
320
+ expect(error).toBe(apiError);
321
+ expect(error).toBeInstanceOf(figmaApi.FigmaApiError);
322
+ }
323
+ });
324
+ it('should return null when refresh fails due to no access token', async ()=>{
325
+ vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(null);
326
+ const result = await refreshProjectToken(tokenStore, mockTenantId);
327
+ expect(result).toBeNull();
328
+ });
329
+ });
330
+ });
331
+
332
+ //# sourceMappingURL=project-token.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/project-token.spec.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport type { ProjectToken } from './types.js'\n\nimport * as figmaApi from '../api/figma-api.js'\nimport * as constants from '../constants.js'\nimport { JWTValidationError, validateProjectToken } from './jwt-validator.js'\nimport * as oauthFlow from './oauth-flow.js'\nimport { getValidProjectToken, ProjectTokenError, refreshProjectToken } from './project-token.js'\nimport { TokenStore } from './token-store.js'\n\n// Mock all external dependencies\nvi.mock('../api/figma-api.js')\nvi.mock('./oauth-flow.js')\nvi.mock('../constants.js')\n\n// Mock only the validateProjectToken function from jwt-validator\n// Keep JWTValidationError class unmocked so it works properly in tests\nvi.mock('./jwt-validator.js', async (importOriginal) => {\n // eslint-disable-next-line @typescript-eslint/consistent-type-imports\n const actual = await importOriginal<typeof import('./jwt-validator.js')>()\n return {\n ...actual,\n validateProjectToken: vi.fn(),\n }\n})\n\ndescribe('Project Token Management', () => {\n let tokenStore: TokenStore\n const mockTenantId = 'test-tenant-123'\n const mockAccessToken = 'figt_mock_access_token'\n const mockProjectTokenJWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.mock.signature'\n let originalEnv: string | undefined\n\n beforeEach(() => {\n // Create a unique test store for each test\n tokenStore = new TokenStore({ configName: `payloadcms-figma-test-${Date.now()}` })\n\n // Disable mocking to test real validation flow\n originalEnv = process.env.FIGMA_MOCK_PROJECT_TOKEN\n process.env.FIGMA_MOCK_PROJECT_TOKEN = 'false'\n\n // Default mock implementations\n vi.mocked(constants.getEnvironment).mockReturnValue('staging')\n vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(mockAccessToken)\n vi.mocked(figmaApi.getProjectToken).mockResolvedValue({\n expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutes (milliseconds)\n tenantId: mockTenantId,\n token: mockProjectTokenJWT,\n })\n vi.mocked(validateProjectToken).mockResolvedValue({\n aud: mockTenantId, // Audience claim contains the CMS ID\n email: 'test@example.com',\n exp: Math.floor(Date.now() / 1000) + 1800, // 30 minutes in seconds\n iat: Math.floor(Date.now() / 1000),\n seat_type: 'full',\n })\n })\n\n afterEach(() => {\n tokenStore.clearTokens()\n tokenStore.clearAllProjectTokens()\n vi.clearAllMocks()\n\n // Restore original environment\n if (originalEnv === undefined) {\n delete process.env.FIGMA_MOCK_PROJECT_TOKEN\n } else {\n process.env.FIGMA_MOCK_PROJECT_TOKEN = originalEnv\n }\n })\n\n describe('getValidProjectToken', () => {\n describe('with valid cached token', () => {\n it('should return cached token immediately without API calls', async () => {\n // Store a valid project token\n const validToken: ProjectToken = {\n claims: {\n aud: mockTenantId, // Audience claim contains the CMS ID\n email: 'test@example.com',\n exp: Math.floor(Date.now() / 1000) + 1800, // 30 minutes from now\n iat: Math.floor(Date.now() / 1000),\n seat_type: 'full',\n },\n expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutes\n tenantId: mockTenantId,\n token: mockProjectTokenJWT,\n }\n tokenStore.setProjectToken(mockTenantId, validToken)\n\n const result = await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(mockProjectTokenJWT)\n // Should not call any external APIs\n expect(oauthFlow.getValidAccessToken).not.toHaveBeenCalled()\n expect(figmaApi.getProjectToken).not.toHaveBeenCalled()\n expect(validateProjectToken).not.toHaveBeenCalled()\n })\n\n it('should use hasValidProjectToken for expiry check', async () => {\n // Mock hasValidProjectToken to return true\n const hasValidSpy = vi.spyOn(tokenStore, 'hasValidProjectToken').mockReturnValue(true)\n const getTokenSpy = vi.spyOn(tokenStore, 'getProjectToken').mockReturnValue({\n claims: { exp: Math.floor(Date.now() / 1000) + 1800 },\n expiresAt: Date.now() + 30 * 60 * 1000,\n tenantId: mockTenantId,\n token: mockProjectTokenJWT,\n })\n\n await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(hasValidSpy).toHaveBeenCalledWith(mockTenantId)\n expect(getTokenSpy).toHaveBeenCalledWith(mockTenantId)\n })\n })\n\n describe('with expired or missing token', () => {\n it('should fetch new token when no cached token exists', async () => {\n const result = await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(mockProjectTokenJWT)\n expect(oauthFlow.getValidAccessToken).toHaveBeenCalledWith(tokenStore)\n expect(figmaApi.getProjectToken).toHaveBeenCalledWith(mockAccessToken, mockTenantId)\n expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'staging')\n })\n\n it('should fetch new token when cached token is expired', async () => {\n // Mock hasValidProjectToken to return false (expired)\n vi.spyOn(tokenStore, 'hasValidProjectToken').mockReturnValue(false)\n\n const result = await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(mockProjectTokenJWT)\n expect(figmaApi.getProjectToken).toHaveBeenCalled()\n })\n\n it('should apply 5-minute expiry buffer through hasValidProjectToken', async () => {\n // Store a token expiring in 4 minutes (within 5-minute buffer)\n const expiringToken: ProjectToken = {\n claims: { exp: Math.floor(Date.now() / 1000) + 240 }, // 4 minutes\n expiresAt: Date.now() + 4 * 60 * 1000,\n tenantId: mockTenantId,\n token: 'expiring-token',\n }\n tokenStore.setProjectToken(mockTenantId, expiringToken)\n\n // hasValidProjectToken should return false due to buffer\n expect(tokenStore.hasValidProjectToken(mockTenantId)).toBe(false)\n\n // Should refresh the token\n await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(figmaApi.getProjectToken).toHaveBeenCalled()\n })\n })\n\n describe('JWT validation integration', () => {\n it('should validate JWT signature after fetching', async () => {\n await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'staging')\n })\n\n it('should use correct environment for validation', async () => {\n vi.mocked(constants.getEnvironment).mockReturnValue('production')\n\n await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(validateProjectToken).toHaveBeenCalledWith(mockProjectTokenJWT, 'production')\n })\n\n it('should throw ProjectTokenError on validation failure', async () => {\n const validationError = new JWTValidationError('Invalid signature', 'SIGNATURE_INVALID')\n vi.mocked(validateProjectToken).mockRejectedValue(validationError)\n\n try {\n await getValidProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown ProjectTokenError')\n } catch (error) {\n expect(error).toBeInstanceOf(ProjectTokenError)\n if (error instanceof ProjectTokenError) {\n expect(error.message).toContain('Project token validation failed')\n expect(error.message).toContain('Invalid signature')\n expect(error.cause).toBe(validationError)\n }\n }\n })\n\n it('should rethrow non-JWTValidationError errors', async () => {\n const genericError = new Error('Network error')\n vi.mocked(validateProjectToken).mockRejectedValue(genericError)\n\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(genericError)\n })\n\n it('should throw error if JWT missing exp claim', async () => {\n // Mock validation returning claims without exp\n vi.mocked(validateProjectToken).mockResolvedValue({\n aud: mockTenantId,\n // No exp claim - intentionally omitted to test validation\n })\n\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(\n ProjectTokenError,\n )\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(\n 'missing expiration (exp) claim',\n )\n })\n })\n\n describe('claims storage', () => {\n it('should store validated claims with token', async () => {\n const mockClaims = {\n aud: mockTenantId,\n email: 'user@example.com',\n exp: Math.floor(Date.now() / 1000) + 1800,\n iat: Math.floor(Date.now() / 1000),\n seat_type: 'full',\n }\n vi.mocked(validateProjectToken).mockResolvedValue(mockClaims)\n\n await getValidProjectToken(tokenStore, mockTenantId)\n\n const storedToken = tokenStore.getProjectToken(mockTenantId)\n expect(storedToken?.claims).toEqual(mockClaims)\n })\n\n it('should convert exp from seconds to milliseconds for expiresAt', async () => {\n const expInSeconds = Math.floor(Date.now() / 1000) + 1800 // 30 min from now\n vi.mocked(validateProjectToken).mockResolvedValue({\n aud: mockTenantId,\n exp: expInSeconds,\n })\n\n await getValidProjectToken(tokenStore, mockTenantId)\n\n const storedToken = tokenStore.getProjectToken(mockTenantId)\n expect(storedToken?.expiresAt).toBe(expInSeconds * 1000)\n })\n\n it('should store token with correct tenantId', async () => {\n await getValidProjectToken(tokenStore, mockTenantId)\n\n const storedToken = tokenStore.getProjectToken(mockTenantId)\n expect(storedToken?.tenantId).toBe(mockTenantId)\n expect(storedToken?.token).toBe(mockProjectTokenJWT)\n })\n })\n\n describe('error handling', () => {\n it('should throw ProjectTokenError when no user access token available', async () => {\n vi.mocked(oauthFlow.getValidAccessToken).mockRejectedValue(\n new Error('User not authenticated'),\n )\n\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(\n ProjectTokenError,\n )\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(\n 'Failed to get valid access token',\n )\n })\n\n it('should return null when getValidAccessToken returns null', async () => {\n vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(null)\n\n const result = await getValidProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBeNull()\n // Should not call API if no access token\n expect(figmaApi.getProjectToken).not.toHaveBeenCalled()\n })\n\n it('should handle API 401 errors', async () => {\n const apiError = new figmaApi.FigmaApiError('Unauthorized', 401)\n vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError)\n\n try {\n await getValidProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown error')\n } catch (error) {\n expect(error).toBe(apiError)\n expect(error).toBeInstanceOf(figmaApi.FigmaApiError)\n }\n })\n\n it('should handle API 404 errors (tenant not found)', async () => {\n const apiError = new figmaApi.FigmaApiError('Tenant not found', 404)\n vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError)\n\n try {\n await getValidProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown error')\n } catch (error) {\n expect(error).toBe(apiError)\n expect(error).toBeInstanceOf(figmaApi.FigmaApiError)\n }\n })\n\n it('should handle network errors', async () => {\n const networkError = new Error('Network timeout')\n vi.mocked(figmaApi.getProjectToken).mockRejectedValue(networkError)\n\n await expect(getValidProjectToken(tokenStore, mockTenantId)).rejects.toThrow(networkError)\n })\n\n it('should wrap getValidAccessToken errors in ProjectTokenError', async () => {\n const authError = new Error('OAuth refresh failed')\n vi.mocked(oauthFlow.getValidAccessToken).mockRejectedValue(authError)\n\n try {\n await getValidProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown ProjectTokenError')\n } catch (error) {\n expect(error).toBeInstanceOf(ProjectTokenError)\n if (error instanceof ProjectTokenError) {\n expect(error.cause).toBe(authError)\n expect(error.message).toContain('Failed to get valid access token')\n }\n }\n })\n })\n })\n\n describe('refreshProjectToken', () => {\n it('should clear existing token before fetching new one', async () => {\n // Store an existing token\n const existingToken: ProjectToken = {\n claims: { exp: Math.floor(Date.now() / 1000) + 1800 },\n expiresAt: Date.now() + 30 * 60 * 1000,\n tenantId: mockTenantId,\n token: 'old-token',\n }\n tokenStore.setProjectToken(mockTenantId, existingToken)\n\n const clearSpy = vi.spyOn(tokenStore, 'clearProjectToken')\n\n await refreshProjectToken(tokenStore, mockTenantId)\n\n expect(clearSpy).toHaveBeenCalledWith(mockTenantId)\n })\n\n it('should call getValidProjectToken after clearing', async () => {\n const result = await refreshProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(mockProjectTokenJWT)\n // Verify API was called (meaning cache was bypassed)\n expect(figmaApi.getProjectToken).toHaveBeenCalled()\n })\n\n it('should force refresh even if token is still valid', async () => {\n // Store a valid token\n const validToken: ProjectToken = {\n claims: { exp: Math.floor(Date.now() / 1000) + 3600 }, // 1 hour\n expiresAt: Date.now() + 60 * 60 * 1000,\n tenantId: mockTenantId,\n token: 'valid-token',\n }\n tokenStore.setProjectToken(mockTenantId, validToken)\n\n // Verify it's considered valid\n expect(tokenStore.hasValidProjectToken(mockTenantId)).toBe(true)\n\n await refreshProjectToken(tokenStore, mockTenantId)\n\n // Should have fetched new token despite valid cache\n expect(figmaApi.getProjectToken).toHaveBeenCalled()\n })\n\n it('should return new token after refresh', async () => {\n const newTokenJWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.new.signature'\n vi.mocked(figmaApi.getProjectToken).mockResolvedValue({\n expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutes (milliseconds)\n tenantId: mockTenantId,\n token: newTokenJWT,\n })\n\n const result = await refreshProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBe(newTokenJWT)\n expect(tokenStore.getProjectToken(mockTenantId)?.token).toBe(newTokenJWT)\n })\n\n it('should propagate errors from getValidProjectToken', async () => {\n const apiError = new figmaApi.FigmaApiError('API Error', 500)\n vi.mocked(figmaApi.getProjectToken).mockRejectedValue(apiError)\n\n try {\n await refreshProjectToken(tokenStore, mockTenantId)\n expect.fail('Should have thrown error')\n } catch (error) {\n expect(error).toBe(apiError)\n expect(error).toBeInstanceOf(figmaApi.FigmaApiError)\n }\n })\n\n it('should return null when refresh fails due to no access token', async () => {\n vi.mocked(oauthFlow.getValidAccessToken).mockResolvedValue(null)\n\n const result = await refreshProjectToken(tokenStore, mockTenantId)\n\n expect(result).toBeNull()\n })\n })\n})\n"],"names":["afterEach","beforeEach","describe","expect","it","vi","figmaApi","constants","JWTValidationError","validateProjectToken","oauthFlow","getValidProjectToken","ProjectTokenError","refreshProjectToken","TokenStore","mock","importOriginal","actual","fn","tokenStore","mockTenantId","mockAccessToken","mockProjectTokenJWT","originalEnv","configName","Date","now","process","env","FIGMA_MOCK_PROJECT_TOKEN","mocked","getEnvironment","mockReturnValue","getValidAccessToken","mockResolvedValue","getProjectToken","expiresAt","tenantId","token","aud","email","exp","Math","floor","iat","seat_type","clearTokens","clearAllProjectTokens","clearAllMocks","undefined","validToken","claims","setProjectToken","result","toBe","not","toHaveBeenCalled","hasValidSpy","spyOn","getTokenSpy","toHaveBeenCalledWith","expiringToken","hasValidProjectToken","validationError","mockRejectedValue","fail","error","toBeInstanceOf","message","toContain","cause","genericError","Error","rejects","toThrow","mockClaims","storedToken","toEqual","expInSeconds","toBeNull","apiError","FigmaApiError","networkError","authError","existingToken","clearSpy","newTokenJWT"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { FigmaTokens } from './types.js';
2
+ /**
3
+ * Error thrown when token refresh fails
4
+ */
5
+ export declare class TokenRefreshError extends Error {
6
+ statusCode?: number | undefined;
7
+ constructor(message: string, statusCode?: number | undefined);
8
+ }
9
+ /**
10
+ * Refresh an expired OAuth2 access token using a refresh token
11
+ *
12
+ * Makes a POST request to Figma's token endpoint with the refresh token
13
+ * to obtain a new access token. Uses public client pattern (no client secret).
14
+ *
15
+ * @param refreshToken - The refresh token to use
16
+ * @param clientId - OAuth2 client ID (defaults to config)
17
+ * @returns Promise resolving to new tokens
18
+ * @throws TokenRefreshError if refresh fails
19
+ */
20
+ export declare function refreshAccessToken(refreshToken: string, clientId?: string): Promise<FigmaTokens>;
21
+ /**
22
+ * Exchange an authorization code for OAuth2 tokens
23
+ *
24
+ * This is used during the initial OAuth flow after the user authorizes the app.
25
+ * Uses public client pattern with PKCE (code verifier is required).
26
+ *
27
+ * @param code - Authorization code from OAuth callback
28
+ * @param codeVerifier - PKCE code verifier (required for public clients)
29
+ * @param redirectUri - Redirect URI used in the authorization request
30
+ * @param clientId - OAuth2 client ID (defaults to config)
31
+ * @returns Promise resolving to tokens
32
+ * @throws TokenRefreshError if exchange fails
33
+ */
34
+ export declare function exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri?: string, clientId?: string): Promise<FigmaTokens>;
35
+ //# sourceMappingURL=refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/auth/refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,YAAY,CAAA;AAMjE;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGjC,UAAU,CAAC,EAAE,MAAM;gBAD1B,OAAO,EAAE,MAAM,EACR,UAAU,CAAC,EAAE,MAAM,YAAA;CAK7B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,QAAQ,GAAE,MAA8B,GACvC,OAAO,CAAC,WAAW,CAAC,CAmDtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,WAAW,GAAE,MAAiC,EAC9C,QAAQ,GAAE,MAA8B,GACvC,OAAO,CAAC,WAAW,CAAC,CA8FtB"}