@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,113 @@
1
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
2
+ import { JWKSMultipleMatchingKeys, JWKSNoMatchingKey, JWSInvalid, JWSSignatureVerificationFailed, JWTExpired } from 'jose/errors';
3
+ import { getJWKSUri } from '../constants.js';
4
+ /**
5
+ * JWT validation error with specific error codes
6
+ */ export class JWTValidationError extends Error {
7
+ code;
8
+ cause;
9
+ constructor(message, code, cause){
10
+ super(message), this.code = code, this.cause = cause;
11
+ this.name = 'JWTValidationError';
12
+ }
13
+ }
14
+ /**
15
+ * JWKS resolvers cached per environment
16
+ * Uses jose's createRemoteJWKSet which handles:
17
+ * - Automatic JWKS fetching from URI
18
+ * - Built-in caching with configurable max age
19
+ * - Automatic key rotation handling
20
+ * - Cooldown protection between refetches
21
+ */ const jwksResolvers = new Map();
22
+ /**
23
+ * Get or create a JWKS resolver for a given environment
24
+ *
25
+ * The resolver is cached in module scope and reused across validations.
26
+ * Jose handles all the complexity of fetching, caching, and rotating keys.
27
+ *
28
+ * @param environment - Figma environment ('production' or 'staging')
29
+ * @returns JWKS resolver function
30
+ */ function getJWKSResolver(environment) {
31
+ const jwksUri = getJWKSUri(environment);
32
+ if (!jwksResolvers.has(jwksUri)) {
33
+ jwksResolvers.set(jwksUri, createRemoteJWKSet(new URL(jwksUri), {
34
+ cacheMaxAge: 24 * 60 * 60 * 1000,
35
+ cooldownDuration: 30000
36
+ }));
37
+ }
38
+ return jwksResolvers.get(jwksUri);
39
+ }
40
+ /**
41
+ * Validate a project token JWT
42
+ *
43
+ * This function:
44
+ * 1. Gets the JWKS resolver for the environment (cached)
45
+ * 2. Uses jose's jwtVerify to validate signature and claims in one call
46
+ * 3. Maps jose errors to our custom error types
47
+ *
48
+ * Jose automatically:
49
+ * - Fetches JWKS from the URI (with caching)
50
+ * - Matches the key by kid from the JWT header
51
+ * - Verifies the signature using the correct algorithm
52
+ * - Validates standard claims (exp, iat, nbf)
53
+ * - Handles clock skew tolerance
54
+ *
55
+ * @param token - JWT token string
56
+ * @param environment - Figma environment ('production' or 'staging')
57
+ * @returns Promise resolving to validated JWT payload
58
+ * @throws {JWTValidationError} If validation fails
59
+ */ export async function validateProjectToken(token, environment) {
60
+ try {
61
+ const JWKS = getJWKSResolver(environment);
62
+ const { payload } = await jwtVerify(token, JWKS, {
63
+ clockTolerance: 300
64
+ });
65
+ return payload;
66
+ } catch (error) {
67
+ // Map jose errors to our custom error types using instanceof checks
68
+ // This is more reliable than checking error.code strings
69
+ // Token expired
70
+ if (error instanceof JWTExpired) {
71
+ throw new JWTValidationError('Project token has expired', 'EXPIRED', error);
72
+ }
73
+ // Key not found in JWKS (possible key rotation or invalid kid)
74
+ if (error instanceof JWKSNoMatchingKey) {
75
+ throw new JWTValidationError('Key not found in JWKS - this may indicate key rotation or an invalid token', 'SIGNATURE_INVALID', error);
76
+ }
77
+ // Multiple matching keys (JWKS configuration issue)
78
+ if (error instanceof JWKSMultipleMatchingKeys) {
79
+ throw new JWTValidationError('Multiple matching keys found in JWKS', 'SIGNATURE_INVALID', error);
80
+ }
81
+ // Invalid signature or other verification failure
82
+ if (error instanceof JWSSignatureVerificationFailed) {
83
+ throw new JWTValidationError('JWT signature verification failed - token may be tampered with', 'SIGNATURE_INVALID', error);
84
+ }
85
+ // Missing or invalid kid/alg in JWT header
86
+ if (error instanceof JWSInvalid) {
87
+ throw new JWTValidationError('Invalid JWT structure - missing required header fields (kid or alg)', 'MISSING_KID', error);
88
+ }
89
+ // Network or fetch errors (not jose errors, generic Error instances)
90
+ if (error instanceof Error) {
91
+ const message = error.message;
92
+ if (message.includes('fetch') || message.includes('network') || message.includes('ENOTFOUND') || message.includes('ECONNREFUSED')) {
93
+ throw new JWTValidationError(`Failed to fetch JWKS: ${message}`, 'JWKS_FETCH_FAILED', error);
94
+ }
95
+ }
96
+ // Generic error fallback
97
+ const message = error instanceof Error ? error.message : 'Unknown error';
98
+ throw new JWTValidationError(`JWT validation failed: ${message}`, 'SIGNATURE_INVALID', error instanceof Error ? error : undefined);
99
+ }
100
+ }
101
+ /**
102
+ * Clear JWKS cache (useful for testing or forcing refresh)
103
+ *
104
+ * This clears the cached JWKS resolvers, forcing fresh fetches
105
+ * on the next validation. Useful for:
106
+ * - Testing with different JWKS
107
+ * - Forcing key rotation updates
108
+ * - Clearing state between test runs
109
+ */ export function clearJWKSCache() {
110
+ jwksResolvers.clear();
111
+ }
112
+
113
+ //# sourceMappingURL=jwt-validator.js.map
@@ -0,0 +1 @@
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 { JWTPayload } from './types.js'\n\nimport { getJWKSUri } 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: 'production' | 'staging') {\n const jwksUri = getJWKSUri(environment)\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: 'production' | 'staging',\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 as JWTPayload\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","getJWKSUri","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;AAIpB,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,WAAqC;IAC5D,MAAMC,UAAUX,WAAWU;IAE3B,IAAI,CAACH,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,WAAqC;IAErC,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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Unit tests for JWT validator
3
+ *
4
+ * Tests our wrapper logic around jose's JWT validation:
5
+ * - Cache management (clearJWKSCache)
6
+ * - Environment-based JWKS URI routing
7
+ * - Error mapping from jose errors to our custom error codes
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=jwt-validator.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt-validator.spec.d.ts","sourceRoot":"","sources":["../../src/auth/jwt-validator.spec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Unit tests for JWT validator
3
+ *
4
+ * Tests our wrapper logic around jose's JWT validation:
5
+ * - Cache management (clearJWKSCache)
6
+ * - Environment-based JWKS URI routing
7
+ * - Error mapping from jose errors to our custom error codes
8
+ */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { clearJWKSCache, JWTValidationError, validateProjectToken } from './jwt-validator.js';
10
+ // Helper to create a simple test JWT (will fail validation but tests the flow)
11
+ function createTestJWT(payload, kid = 'test-key-id') {
12
+ const header = Buffer.from(JSON.stringify({
13
+ alg: 'RS256',
14
+ kid,
15
+ typ: 'JWT'
16
+ })).toString('base64url');
17
+ const payloadStr = Buffer.from(JSON.stringify(payload)).toString('base64url');
18
+ const signature = 'fake_signature';
19
+ return `${header}.${payloadStr}.${signature}`;
20
+ }
21
+ describe('JWT Validator', ()=>{
22
+ // Mock JWKS with RSA key
23
+ const mockJWKS = {
24
+ keys: [
25
+ {
26
+ alg: 'RS256',
27
+ e: 'AQAB',
28
+ kid: 'test-key-id',
29
+ kty: 'RSA',
30
+ n: 'xGOr-H7A-PWO8VcD4sqDpCciAuqYDPq8zKWb4Vh6YhP8Ny3KcJaJhLMDEBJB3fKVvT-pPlkHPFBv7L7iHzRrJMXz8uDJVK-FTYmTQhcvKPyMWUDGDPNlVXEGkBLdj3DZqKjJ8iVChSh5DzKJvzr-RyGnH8Z7nLqBa91zxWfm7ztJxqP0qPRKz6vb4h8QyN-5X4Gh8gU7ZcLLnqLqJ6FhPvqKVcBhfN3KjM6h8hLXz6pPzKqJ8z6r8-9KcZXlWvR',
31
+ use: 'sig'
32
+ }
33
+ ]
34
+ };
35
+ beforeEach(()=>{
36
+ clearJWKSCache();
37
+ vi.restoreAllMocks();
38
+ });
39
+ afterEach(()=>{
40
+ clearJWKSCache();
41
+ });
42
+ describe('Cache Management', ()=>{
43
+ it('should clear resolver cache when clearJWKSCache is called', async ()=>{
44
+ const fetchMock = vi.fn().mockResolvedValue({
45
+ json: ()=>Promise.resolve(mockJWKS),
46
+ ok: true,
47
+ status: 200
48
+ });
49
+ global.fetch = fetchMock;
50
+ const token = createTestJWT({
51
+ exp: Math.floor(Date.now() / 1000) + 3600
52
+ });
53
+ // First validation - creates and caches resolver
54
+ try {
55
+ await validateProjectToken(token, 'staging');
56
+ } catch (_error) {
57
+ // Expected to fail on signature
58
+ }
59
+ const firstCallCount = fetchMock.mock.calls.length;
60
+ expect(firstCallCount).toBeGreaterThanOrEqual(1);
61
+ // Clear the cache - this is OUR logic
62
+ clearJWKSCache();
63
+ // Second validation after clearing cache - should create new resolver
64
+ try {
65
+ await validateProjectToken(token, 'staging');
66
+ } catch (_error) {
67
+ // Expected to fail on signature
68
+ }
69
+ // Verify fetch was called again after cache clear
70
+ expect(fetchMock.mock.calls.length).toBeGreaterThan(firstCallCount);
71
+ });
72
+ });
73
+ describe('Error Code Mapping', ()=>{
74
+ it('should map network errors to JWKS_FETCH_FAILED code', async ()=>{
75
+ global.fetch = vi.fn().mockRejectedValue(new Error('ENOTFOUND'));
76
+ const token = createTestJWT({
77
+ exp: Math.floor(Date.now() / 1000) + 3600
78
+ });
79
+ try {
80
+ await validateProjectToken(token, 'staging');
81
+ expect.fail('Should have thrown an error');
82
+ } catch (error) {
83
+ expect(error).toBeInstanceOf(JWTValidationError);
84
+ if (error instanceof JWTValidationError) {
85
+ // Verify our error mapping logic
86
+ expect(error.code).toBe('JWKS_FETCH_FAILED');
87
+ expect(error.message).toContain('Failed to fetch JWKS');
88
+ }
89
+ }
90
+ });
91
+ it('should map jose signature errors to SIGNATURE_INVALID code', async ()=>{
92
+ global.fetch = vi.fn().mockResolvedValue({
93
+ json: ()=>Promise.resolve(mockJWKS),
94
+ ok: true,
95
+ status: 200
96
+ });
97
+ const badToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.e30.bad_signature';
98
+ try {
99
+ await validateProjectToken(badToken, 'staging');
100
+ expect.fail('Should have thrown an error');
101
+ } catch (error) {
102
+ expect(error).toBeInstanceOf(JWTValidationError);
103
+ if (error instanceof JWTValidationError) {
104
+ // Verify our error mapping logic - invalid tokens may fail at different stages
105
+ // The important thing is that signature-related errors map to either SIGNATURE_INVALID or MISSING_KID
106
+ expect([
107
+ 'SIGNATURE_INVALID',
108
+ 'MISSING_KID'
109
+ ]).toContain(error.code);
110
+ }
111
+ }
112
+ });
113
+ it('should map expired token errors to EXPIRED code', async ()=>{
114
+ global.fetch = vi.fn().mockResolvedValue({
115
+ json: ()=>Promise.resolve(mockJWKS),
116
+ ok: true,
117
+ status: 200
118
+ });
119
+ // Create token that expired 1 hour ago
120
+ const expiredToken = createTestJWT({
121
+ exp: Math.floor(Date.now() / 1000) - 3600,
122
+ iat: Math.floor(Date.now() / 1000) - 7200
123
+ });
124
+ try {
125
+ await validateProjectToken(expiredToken, 'staging');
126
+ expect.fail('Should have thrown an error');
127
+ } catch (error) {
128
+ expect(error).toBeInstanceOf(JWTValidationError);
129
+ if (error instanceof JWTValidationError) {
130
+ // Verify our error mapping logic
131
+ // Note: Mock tokens with bad signatures may fail signature validation before expiry check
132
+ // In real scenarios with valid signatures, EXPIRED would be returned
133
+ expect([
134
+ 'EXPIRED',
135
+ 'SIGNATURE_INVALID',
136
+ 'MISSING_KID'
137
+ ]).toContain(error.code);
138
+ }
139
+ }
140
+ });
141
+ it('should map missing kid errors to MISSING_KID code', async ()=>{
142
+ global.fetch = vi.fn().mockResolvedValue({
143
+ json: ()=>Promise.resolve(mockJWKS),
144
+ ok: true,
145
+ status: 200
146
+ });
147
+ // Create token without kid in header
148
+ const header = Buffer.from(JSON.stringify({
149
+ alg: 'RS256',
150
+ typ: 'JWT'
151
+ })).toString('base64url');
152
+ const payload = Buffer.from(JSON.stringify({
153
+ exp: Math.floor(Date.now() / 1000) + 3600
154
+ })).toString('base64url');
155
+ const tokenWithoutKid = `${header}.${payload}.fake_signature`;
156
+ try {
157
+ await validateProjectToken(tokenWithoutKid, 'staging');
158
+ expect.fail('Should have thrown an error');
159
+ } catch (error) {
160
+ expect(error).toBeInstanceOf(JWTValidationError);
161
+ if (error instanceof JWTValidationError) {
162
+ // Verify our error mapping logic - tokens without kid may fail at various stages
163
+ expect([
164
+ 'MISSING_KID',
165
+ 'SIGNATURE_INVALID'
166
+ ]).toContain(error.code);
167
+ }
168
+ }
169
+ });
170
+ it('should map no matching key errors to SIGNATURE_INVALID code', async ()=>{
171
+ global.fetch = vi.fn().mockResolvedValue({
172
+ json: ()=>Promise.resolve(mockJWKS),
173
+ ok: true,
174
+ status: 200
175
+ });
176
+ // Create token with a kid that doesn't exist in JWKS
177
+ const tokenWithWrongKid = createTestJWT({
178
+ exp: Math.floor(Date.now() / 1000) + 3600
179
+ }, 'non-existent-key-id');
180
+ try {
181
+ await validateProjectToken(tokenWithWrongKid, 'staging');
182
+ expect.fail('Should have thrown an error');
183
+ } catch (error) {
184
+ expect(error).toBeInstanceOf(JWTValidationError);
185
+ if (error instanceof JWTValidationError) {
186
+ // Verify our error mapping logic
187
+ expect(error.code).toBe('SIGNATURE_INVALID');
188
+ expect(error.message).toContain('Key not found in JWKS');
189
+ }
190
+ }
191
+ });
192
+ it('should handle multiple network error formats', async ()=>{
193
+ const networkErrors = [
194
+ new Error('fetch failed'),
195
+ new Error('network error'),
196
+ new Error('ECONNREFUSED')
197
+ ];
198
+ for (const networkError of networkErrors){
199
+ clearJWKSCache();
200
+ global.fetch = vi.fn().mockRejectedValue(networkError);
201
+ const token = createTestJWT({
202
+ exp: Math.floor(Date.now() / 1000) + 3600
203
+ });
204
+ try {
205
+ await validateProjectToken(token, 'staging');
206
+ expect.fail('Should have thrown an error');
207
+ } catch (error) {
208
+ expect(error).toBeInstanceOf(JWTValidationError);
209
+ if (error instanceof JWTValidationError) {
210
+ expect(error.code).toBe('JWKS_FETCH_FAILED');
211
+ }
212
+ }
213
+ }
214
+ });
215
+ it('should handle generic errors with fallback', async ()=>{
216
+ // Mock an unexpected error type
217
+ global.fetch = vi.fn().mockRejectedValue('string error');
218
+ const token = createTestJWT({
219
+ exp: Math.floor(Date.now() / 1000) + 3600
220
+ });
221
+ try {
222
+ await validateProjectToken(token, 'staging');
223
+ expect.fail('Should have thrown an error');
224
+ } catch (error) {
225
+ expect(error).toBeInstanceOf(JWTValidationError);
226
+ if (error instanceof JWTValidationError) {
227
+ // Generic errors fall back to SIGNATURE_INVALID
228
+ expect(error.code).toBe('SIGNATURE_INVALID');
229
+ expect(error.message).toContain('JWT validation failed');
230
+ }
231
+ }
232
+ });
233
+ });
234
+ });
235
+
236
+ //# sourceMappingURL=jwt-validator.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/jwt-validator.spec.ts"],"sourcesContent":["/**\n * Unit tests for JWT validator\n *\n * Tests our wrapper logic around jose's JWT validation:\n * - Cache management (clearJWKSCache)\n * - Environment-based JWKS URI routing\n * - Error mapping from jose errors to our custom error codes\n */\n\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport { clearJWKSCache, JWTValidationError, validateProjectToken } from './jwt-validator.js'\n\n// Helper to create a simple test JWT (will fail validation but tests the flow)\nfunction createTestJWT(payload: Record<string, unknown>, kid = 'test-key-id'): string {\n const header = Buffer.from(JSON.stringify({ alg: 'RS256', kid, typ: 'JWT' })).toString(\n 'base64url',\n )\n const payloadStr = Buffer.from(JSON.stringify(payload)).toString('base64url')\n const signature = 'fake_signature'\n return `${header}.${payloadStr}.${signature}`\n}\n\ndescribe('JWT Validator', () => {\n // Mock JWKS with RSA key\n const mockJWKS = {\n keys: [\n {\n alg: 'RS256',\n e: 'AQAB',\n kid: 'test-key-id',\n kty: 'RSA',\n n: 'xGOr-H7A-PWO8VcD4sqDpCciAuqYDPq8zKWb4Vh6YhP8Ny3KcJaJhLMDEBJB3fKVvT-pPlkHPFBv7L7iHzRrJMXz8uDJVK-FTYmTQhcvKPyMWUDGDPNlVXEGkBLdj3DZqKjJ8iVChSh5DzKJvzr-RyGnH8Z7nLqBa91zxWfm7ztJxqP0qPRKz6vb4h8QyN-5X4Gh8gU7ZcLLnqLqJ6FhPvqKVcBhfN3KjM6h8hLXz6pPzKqJ8z6r8-9KcZXlWvR',\n use: 'sig',\n },\n ],\n }\n\n beforeEach(() => {\n clearJWKSCache()\n vi.restoreAllMocks()\n })\n\n afterEach(() => {\n clearJWKSCache()\n })\n\n describe('Cache Management', () => {\n it('should clear resolver cache when clearJWKSCache is called', async () => {\n const fetchMock = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n global.fetch = fetchMock\n\n const token = createTestJWT({ exp: Math.floor(Date.now() / 1000) + 3600 })\n\n // First validation - creates and caches resolver\n try {\n await validateProjectToken(token, 'staging')\n } catch (_error) {\n // Expected to fail on signature\n }\n\n const firstCallCount = fetchMock.mock.calls.length\n expect(firstCallCount).toBeGreaterThanOrEqual(1)\n\n // Clear the cache - this is OUR logic\n clearJWKSCache()\n\n // Second validation after clearing cache - should create new resolver\n try {\n await validateProjectToken(token, 'staging')\n } catch (_error) {\n // Expected to fail on signature\n }\n\n // Verify fetch was called again after cache clear\n expect(fetchMock.mock.calls.length).toBeGreaterThan(firstCallCount)\n })\n })\n\n describe('Error Code Mapping', () => {\n it('should map network errors to JWKS_FETCH_FAILED code', async () => {\n global.fetch = vi.fn().mockRejectedValue(new Error('ENOTFOUND'))\n\n const token = createTestJWT({ exp: Math.floor(Date.now() / 1000) + 3600 })\n\n try {\n await validateProjectToken(token, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic\n expect(error.code).toBe('JWKS_FETCH_FAILED')\n expect(error.message).toContain('Failed to fetch JWKS')\n }\n }\n })\n\n it('should map jose signature errors to SIGNATURE_INVALID code', async () => {\n global.fetch = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n\n const badToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIn0.e30.bad_signature'\n\n try {\n await validateProjectToken(badToken, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic - invalid tokens may fail at different stages\n // The important thing is that signature-related errors map to either SIGNATURE_INVALID or MISSING_KID\n expect(['SIGNATURE_INVALID', 'MISSING_KID']).toContain(error.code)\n }\n }\n })\n\n it('should map expired token errors to EXPIRED code', async () => {\n global.fetch = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n\n // Create token that expired 1 hour ago\n const expiredToken = createTestJWT({\n exp: Math.floor(Date.now() / 1000) - 3600,\n iat: Math.floor(Date.now() / 1000) - 7200,\n })\n\n try {\n await validateProjectToken(expiredToken, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic\n // Note: Mock tokens with bad signatures may fail signature validation before expiry check\n // In real scenarios with valid signatures, EXPIRED would be returned\n expect(['EXPIRED', 'SIGNATURE_INVALID', 'MISSING_KID']).toContain(error.code)\n }\n }\n })\n\n it('should map missing kid errors to MISSING_KID code', async () => {\n global.fetch = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n\n // Create token without kid in header\n const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url')\n const payload = Buffer.from(\n JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 3600 }),\n ).toString('base64url')\n const tokenWithoutKid = `${header}.${payload}.fake_signature`\n\n try {\n await validateProjectToken(tokenWithoutKid, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic - tokens without kid may fail at various stages\n expect(['MISSING_KID', 'SIGNATURE_INVALID']).toContain(error.code)\n }\n }\n })\n\n it('should map no matching key errors to SIGNATURE_INVALID code', async () => {\n global.fetch = vi.fn().mockResolvedValue({\n json: () => Promise.resolve(mockJWKS),\n ok: true,\n status: 200,\n })\n\n // Create token with a kid that doesn't exist in JWKS\n const tokenWithWrongKid = createTestJWT(\n { exp: Math.floor(Date.now() / 1000) + 3600 },\n 'non-existent-key-id',\n )\n\n try {\n await validateProjectToken(tokenWithWrongKid, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Verify our error mapping logic\n expect(error.code).toBe('SIGNATURE_INVALID')\n expect(error.message).toContain('Key not found in JWKS')\n }\n }\n })\n\n it('should handle multiple network error formats', async () => {\n const networkErrors = [\n new Error('fetch failed'),\n new Error('network error'),\n new Error('ECONNREFUSED'),\n ]\n\n for (const networkError of networkErrors) {\n clearJWKSCache()\n global.fetch = vi.fn().mockRejectedValue(networkError)\n\n const token = createTestJWT({ exp: Math.floor(Date.now() / 1000) + 3600 })\n\n try {\n await validateProjectToken(token, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n expect(error.code).toBe('JWKS_FETCH_FAILED')\n }\n }\n }\n })\n\n it('should handle generic errors with fallback', async () => {\n // Mock an unexpected error type\n global.fetch = vi.fn().mockRejectedValue('string error')\n\n const token = createTestJWT({ exp: Math.floor(Date.now() / 1000) + 3600 })\n\n try {\n await validateProjectToken(token, 'staging')\n expect.fail('Should have thrown an error')\n } catch (error) {\n expect(error).toBeInstanceOf(JWTValidationError)\n if (error instanceof JWTValidationError) {\n // Generic errors fall back to SIGNATURE_INVALID\n expect(error.code).toBe('SIGNATURE_INVALID')\n expect(error.message).toContain('JWT validation failed')\n }\n }\n })\n })\n})\n"],"names":["afterEach","beforeEach","describe","expect","it","vi","clearJWKSCache","JWTValidationError","validateProjectToken","createTestJWT","payload","kid","header","Buffer","from","JSON","stringify","alg","typ","toString","payloadStr","signature","mockJWKS","keys","e","kty","n","use","restoreAllMocks","fetchMock","fn","mockResolvedValue","json","Promise","resolve","ok","status","global","fetch","token","exp","Math","floor","Date","now","_error","firstCallCount","mock","calls","length","toBeGreaterThanOrEqual","toBeGreaterThan","mockRejectedValue","Error","fail","error","toBeInstanceOf","code","toBe","message","toContain","badToken","expiredToken","iat","tokenWithoutKid","tokenWithWrongKid","networkErrors","networkError"],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { TokenStore } from './token-store.js';
2
+ import type { FigmaTokens } from './types.js';
3
+ /**
4
+ * Error during OAuth flow
5
+ */
6
+ export declare class OAuthFlowError extends Error {
7
+ cause?: Error | undefined;
8
+ constructor(message: string, cause?: Error | undefined);
9
+ }
10
+ /**
11
+ * Options for OAuth flow
12
+ */
13
+ export interface OAuthFlowOptions {
14
+ /** OAuth2 client ID (defaults to config) */
15
+ clientId?: string;
16
+ /** Callback server port (defaults to 3000) */
17
+ port?: number;
18
+ /** Redirect URI (defaults to config) */
19
+ redirectUri?: string;
20
+ /** OAuth scopes (defaults to config) */
21
+ scopes?: string[];
22
+ /** Callback timeout in milliseconds (defaults to 120000) */
23
+ timeoutMs?: number;
24
+ }
25
+ /**
26
+ * Result of OAuth flow
27
+ */
28
+ export interface OAuthFlowResult {
29
+ /** OAuth2 tokens */
30
+ tokens: FigmaTokens;
31
+ /** Figma user ID */
32
+ userId?: string;
33
+ }
34
+ /**
35
+ * Execute complete OAuth2 authorization flow
36
+ *
37
+ * This orchestrates the full OAuth flow:
38
+ * 1. Generate PKCE pair for security
39
+ * 2. Start local callback server
40
+ * 3. Open browser to authorization URL
41
+ * 4. Wait for user to authorize
42
+ * 5. Exchange authorization code for tokens
43
+ * 6. Store tokens securely
44
+ *
45
+ * @param tokenStore - Token store for persisting tokens
46
+ * @param options - Flow configuration options
47
+ * @returns Promise resolving to OAuth flow result
48
+ * @throws OAuthFlowError if any step fails
49
+ */
50
+ export declare function executeOAuthFlow(tokenStore: TokenStore, options?: OAuthFlowOptions): Promise<OAuthFlowResult>;
51
+ /**
52
+ * Get valid access token, refreshing if necessary
53
+ *
54
+ * Checks if stored tokens exist and are valid. If expired, automatically
55
+ * refreshes using the refresh token. If no tokens exist, returns null.
56
+ *
57
+ * @param tokenStore - Token store to use
58
+ * @returns Promise resolving to access token or null if no valid tokens
59
+ * @throws OAuthFlowError if refresh fails
60
+ */
61
+ export declare function getValidAccessToken(tokenStore: TokenStore): Promise<null | string>;
62
+ //# sourceMappingURL=oauth-flow.d.ts.map
@@ -0,0 +1 @@
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;AAU7C;;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,8CAA8C;IAC9C,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,CAyBxF"}
@@ -0,0 +1,172 @@
1
+ import * as p from '@clack/prompts';
2
+ import crypto from 'crypto';
3
+ import { OAUTH_CONFIG } from '../config/oauth.js';
4
+ import { isDebug } from '../utils/is-debug.js';
5
+ import * as log from '../utils/log.js';
6
+ import { buildAuthorizationUrl, openBrowser } from './browser.js';
7
+ import { CallbackServer } from './callback-server.js';
8
+ import { generatePKCEPair } from './pkce.js';
9
+ import { exchangeCodeForTokens, refreshAccessToken } from './refresh.js';
10
+ /**
11
+ * Error during OAuth flow
12
+ */ export class OAuthFlowError extends Error {
13
+ cause;
14
+ constructor(message, cause){
15
+ super(message), this.cause = cause;
16
+ this.name = 'OAuthFlowError';
17
+ }
18
+ }
19
+ /**
20
+ * Execute complete OAuth2 authorization flow
21
+ *
22
+ * This orchestrates the full OAuth flow:
23
+ * 1. Generate PKCE pair for security
24
+ * 2. Start local callback server
25
+ * 3. Open browser to authorization URL
26
+ * 4. Wait for user to authorize
27
+ * 5. Exchange authorization code for tokens
28
+ * 6. Store tokens securely
29
+ *
30
+ * @param tokenStore - Token store for persisting tokens
31
+ * @param options - Flow configuration options
32
+ * @returns Promise resolving to OAuth flow result
33
+ * @throws OAuthFlowError if any step fails
34
+ */ export async function executeOAuthFlow(tokenStore, options = {}) {
35
+ // Check if OAuth flow should be mocked
36
+ // Default to true for OAuth (allow testing without real authentication by default)
37
+ const shouldMock = process.env.FIGMA_MOCK_OAUTH !== 'false';
38
+ if (shouldMock) {
39
+ // MOCK IMPLEMENTATION: Return mock tokens for testing
40
+ if (isDebug()) {
41
+ log.debug('Using mock OAuth flow (FIGMA_MOCK_OAUTH != "false")');
42
+ }
43
+ const mockTokens = {
44
+ accessToken: 'figt_mock_access_token_' + crypto.randomBytes(16).toString('hex'),
45
+ expiresAt: Date.now() + 2 * 60 * 60 * 1000,
46
+ refreshToken: 'figt_mock_refresh_token_' + crypto.randomBytes(16).toString('hex'),
47
+ scopes: options.scopes || OAUTH_CONFIG.scopes,
48
+ tokenType: 'Bearer'
49
+ };
50
+ // Store mock tokens
51
+ tokenStore.setTokens(mockTokens);
52
+ return {
53
+ tokens: mockTokens,
54
+ userId: 'mock_user_123'
55
+ };
56
+ }
57
+ // REAL OAUTH IMPLEMENTATION
58
+ const { clientId = OAUTH_CONFIG.clientId, port = 3000, redirectUri = OAUTH_CONFIG.redirectUri, scopes = OAUTH_CONFIG.scopes, timeoutMs = 120000 } = options;
59
+ // Generate PKCE pair for enhanced security
60
+ const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair();
61
+ // Generate random state for CSRF protection
62
+ const state = crypto.randomBytes(32).toString('base64url');
63
+ // Create callback server
64
+ const callbackServer = new CallbackServer({
65
+ port,
66
+ timeoutMs
67
+ });
68
+ try {
69
+ // Start waiting for callback (this starts the server and captures actual port)
70
+ const callbackPromise = callbackServer.waitForCallback(state);
71
+ // Wait a bit for server to start and determine actual port
72
+ await new Promise((resolve)=>setTimeout(resolve, 100));
73
+ // Get the actual port (may differ from preferred if that was in use)
74
+ const actualPort = callbackServer.getActualPort();
75
+ // Build redirect URI with actual port
76
+ const actualRedirectUri = redirectUri.replace(/:\d+/, `:${actualPort}`);
77
+ // Build authorization URL
78
+ const authUrl = buildAuthorizationUrl({
79
+ authorizationUrl: OAUTH_CONFIG.authorizationUrl,
80
+ clientId,
81
+ codeChallenge,
82
+ codeChallengeMethod,
83
+ redirectUri: actualRedirectUri,
84
+ scopes,
85
+ state
86
+ });
87
+ // Prompt user to open browser
88
+ const shouldOpenBrowser = await p.confirm({
89
+ initialValue: true,
90
+ message: 'Log in or create Figma account in your browser?'
91
+ });
92
+ if (p.isCancel(shouldOpenBrowser)) {
93
+ throw new OAuthFlowError('Authentication cancelled by user');
94
+ }
95
+ if (shouldOpenBrowser) {
96
+ // Try to open browser, but don't fail if it doesn't work
97
+ try {
98
+ await openBrowser(authUrl);
99
+ } catch (_error) {
100
+ // Browser opening failed, show URL for manual opening
101
+ log.warning('Could not open browser automatically.');
102
+ log.info(`Please open this URL in your browser:`);
103
+ log.info(` ${authUrl}`);
104
+ }
105
+ } else {
106
+ // User declined automatic opening, show URL
107
+ log.info(`Please open this URL manually in your browser:`);
108
+ log.info(` ${authUrl}`);
109
+ }
110
+ // Wait for callback
111
+ const { code } = await callbackPromise;
112
+ // Exchange authorization code for tokens
113
+ let tokens;
114
+ try {
115
+ tokens = await exchangeCodeForTokens(code, codeVerifier, actualRedirectUri, clientId);
116
+ } catch (error) {
117
+ throw new OAuthFlowError('Failed to exchange authorization code for tokens', error instanceof Error ? error : undefined);
118
+ }
119
+ // Store tokens
120
+ try {
121
+ tokenStore.setTokens(tokens);
122
+ } catch (error) {
123
+ throw new OAuthFlowError('Failed to store tokens', error instanceof Error ? error : undefined);
124
+ }
125
+ return {
126
+ tokens,
127
+ userId: tokens.userId
128
+ };
129
+ } catch (error) {
130
+ // Clean up server on error
131
+ callbackServer.stop();
132
+ if (error instanceof OAuthFlowError) {
133
+ throw error;
134
+ }
135
+ throw new OAuthFlowError(`OAuth flow failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
136
+ } finally{
137
+ // Ensure server is always stopped
138
+ callbackServer.stop();
139
+ }
140
+ }
141
+ /**
142
+ * Get valid access token, refreshing if necessary
143
+ *
144
+ * Checks if stored tokens exist and are valid. If expired, automatically
145
+ * refreshes using the refresh token. If no tokens exist, returns null.
146
+ *
147
+ * @param tokenStore - Token store to use
148
+ * @returns Promise resolving to access token or null if no valid tokens
149
+ * @throws OAuthFlowError if refresh fails
150
+ */ export async function getValidAccessToken(tokenStore) {
151
+ // Check if we have valid tokens
152
+ if (tokenStore.hasValidTokens()) {
153
+ return tokenStore.getAccessToken();
154
+ }
155
+ // Check if we have a refresh token
156
+ const refreshToken = tokenStore.getRefreshToken();
157
+ if (!refreshToken) {
158
+ return null;
159
+ }
160
+ // Try to refresh
161
+ try {
162
+ const tokens = await refreshAccessToken(refreshToken);
163
+ tokenStore.setTokens(tokens);
164
+ return tokens.accessToken;
165
+ } catch (error) {
166
+ // Refresh failed - clear invalid tokens
167
+ tokenStore.clearTokens();
168
+ throw new OAuthFlowError('Failed to refresh access token', error instanceof Error ? error : undefined);
169
+ }
170
+ }
171
+
172
+ //# sourceMappingURL=oauth-flow.js.map