@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,149 @@
1
+ /* eslint-disable perfectionist/sort-objects */ import { OAUTH_CONFIG } from '../config/oauth.js';
2
+ import { isDebug } from '../utils/is-debug.js';
3
+ import * as log from '../utils/log.js';
4
+ /**
5
+ * Error thrown when token refresh fails
6
+ */ export class TokenRefreshError extends Error {
7
+ statusCode;
8
+ constructor(message, statusCode){
9
+ super(message), this.statusCode = statusCode;
10
+ this.name = 'TokenRefreshError';
11
+ }
12
+ }
13
+ /**
14
+ * Refresh an expired OAuth2 access token using a refresh token
15
+ *
16
+ * Makes a POST request to Figma's token endpoint with the refresh token
17
+ * to obtain a new access token. Uses public client pattern (no client secret).
18
+ *
19
+ * @param refreshToken - The refresh token to use
20
+ * @param clientId - OAuth2 client ID (defaults to config)
21
+ * @returns Promise resolving to new tokens
22
+ * @throws TokenRefreshError if refresh fails
23
+ */ export async function refreshAccessToken(refreshToken, clientId = OAUTH_CONFIG.clientId) {
24
+ // Prepare form data for token refresh request (public client)
25
+ const params = new URLSearchParams({
26
+ client_id: clientId,
27
+ grant_type: 'refresh_token',
28
+ refresh_token: refreshToken
29
+ });
30
+ try {
31
+ const response = await fetch(OAUTH_CONFIG.refreshUrl, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/x-www-form-urlencoded'
35
+ },
36
+ body: params.toString()
37
+ });
38
+ if (!response.ok) {
39
+ const errorText = await response.text();
40
+ throw new TokenRefreshError(`Token refresh failed: ${response.statusText}. ${errorText}`, response.status);
41
+ }
42
+ const data = await response.json();
43
+ // Validate response contains required fields
44
+ if (!data.access_token || !data.refresh_token || !data.expires_in) {
45
+ throw new TokenRefreshError('Invalid token response: missing required fields');
46
+ }
47
+ // Convert response to our internal token format
48
+ const expiresAt = Date.now() + data.expires_in * 1000;
49
+ return {
50
+ accessToken: data.access_token,
51
+ refreshToken: data.refresh_token,
52
+ expiresAt,
53
+ userId: data.user_id_string
54
+ };
55
+ } catch (error) {
56
+ if (error instanceof TokenRefreshError) {
57
+ throw error;
58
+ }
59
+ // Handle network errors and other exceptions
60
+ throw new TokenRefreshError(`Failed to refresh token: ${error instanceof Error ? error.message : 'Unknown error'}`);
61
+ }
62
+ }
63
+ /**
64
+ * Exchange an authorization code for OAuth2 tokens
65
+ *
66
+ * This is used during the initial OAuth flow after the user authorizes the app.
67
+ * Uses public client pattern with PKCE (code verifier is required).
68
+ *
69
+ * @param code - Authorization code from OAuth callback
70
+ * @param codeVerifier - PKCE code verifier (required for public clients)
71
+ * @param redirectUri - Redirect URI used in the authorization request
72
+ * @param clientId - OAuth2 client ID (defaults to config)
73
+ * @returns Promise resolving to tokens
74
+ * @throws TokenRefreshError if exchange fails
75
+ */ export async function exchangeCodeForTokens(code, codeVerifier, redirectUri = OAUTH_CONFIG.redirectUri, clientId = OAUTH_CONFIG.clientId) {
76
+ // Prepare form data for token exchange (public client with PKCE)
77
+ const params = new URLSearchParams({
78
+ client_id: clientId,
79
+ code,
80
+ code_verifier: codeVerifier,
81
+ grant_type: 'authorization_code',
82
+ redirect_uri: redirectUri
83
+ });
84
+ try {
85
+ // Public client - no client secret or Basic Auth header needed
86
+ // Security is provided by PKCE (code_verifier in the request body)
87
+ const headers = {
88
+ 'Content-Type': 'application/x-www-form-urlencoded'
89
+ };
90
+ // Debug logging
91
+ if (isDebug()) {
92
+ log.debug(`Token exchange request: ${JSON.stringify({
93
+ url: OAUTH_CONFIG.tokenUrl,
94
+ headers,
95
+ body: params.toString()
96
+ })}`);
97
+ }
98
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
99
+ method: 'POST',
100
+ headers,
101
+ body: params.toString()
102
+ });
103
+ if (isDebug()) {
104
+ log.debug(`Token exchange response: ${JSON.stringify({
105
+ status: response.status,
106
+ statusText: response.statusText,
107
+ headers: Object.fromEntries(response.headers.entries())
108
+ })}`);
109
+ }
110
+ if (!response.ok) {
111
+ const errorText = await response.text();
112
+ if (isDebug()) {
113
+ log.debug(`Error response body: ${errorText}`);
114
+ }
115
+ throw new TokenRefreshError(`Token exchange failed: ${response.statusText}. ${errorText}`, response.status);
116
+ }
117
+ const data = await response.json();
118
+ if (isDebug()) {
119
+ log.debug(`Token response: ${JSON.stringify({
120
+ access_token: data.access_token,
121
+ token_type: data.token_type,
122
+ expires_in: data.expires_in,
123
+ scope: data.scope,
124
+ user_id_string: data.user_id_string,
125
+ refresh_token: data.refresh_token?.substring(0, 10) + '...'
126
+ })}`);
127
+ }
128
+ // Validate response contains required fields
129
+ if (!data.access_token || !data.refresh_token || !data.expires_in) {
130
+ throw new TokenRefreshError('Invalid token response: missing required fields');
131
+ }
132
+ // Convert response to our internal token format
133
+ const expiresAt = Date.now() + data.expires_in * 1000;
134
+ return {
135
+ accessToken: data.access_token,
136
+ refreshToken: data.refresh_token,
137
+ expiresAt,
138
+ userId: data.user_id_string
139
+ };
140
+ } catch (error) {
141
+ if (error instanceof TokenRefreshError) {
142
+ throw error;
143
+ }
144
+ // Handle network errors and other exceptions
145
+ throw new TokenRefreshError(`Failed to exchange code for tokens: ${error instanceof Error ? error.message : 'Unknown error'}`);
146
+ }
147
+ }
148
+
149
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/refresh.ts"],"sourcesContent":["/* eslint-disable perfectionist/sort-objects */\nimport type { FigmaTokenResponse, FigmaTokens } from './types.js'\n\nimport { OAUTH_CONFIG } from '../config/oauth.js'\nimport { isDebug } from '../utils/is-debug.js'\nimport * as log from '../utils/log.js'\n\n/**\n * Error thrown when token refresh fails\n */\nexport class TokenRefreshError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n ) {\n super(message)\n this.name = 'TokenRefreshError'\n }\n}\n\n/**\n * Refresh an expired OAuth2 access token using a refresh token\n *\n * Makes a POST request to Figma's token endpoint with the refresh token\n * to obtain a new access token. Uses public client pattern (no client secret).\n *\n * @param refreshToken - The refresh token to use\n * @param clientId - OAuth2 client ID (defaults to config)\n * @returns Promise resolving to new tokens\n * @throws TokenRefreshError if refresh fails\n */\nexport async function refreshAccessToken(\n refreshToken: string,\n clientId: string = OAUTH_CONFIG.clientId,\n): Promise<FigmaTokens> {\n // Prepare form data for token refresh request (public client)\n const params = new URLSearchParams({\n client_id: clientId,\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n })\n\n try {\n const response = await fetch(OAUTH_CONFIG.refreshUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: params.toString(),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new TokenRefreshError(\n `Token refresh failed: ${response.statusText}. ${errorText}`,\n response.status,\n )\n }\n\n const data = (await response.json()) as FigmaTokenResponse\n\n // Validate response contains required fields\n if (!data.access_token || !data.refresh_token || !data.expires_in) {\n throw new TokenRefreshError('Invalid token response: missing required fields')\n }\n\n // Convert response to our internal token format\n const expiresAt = Date.now() + data.expires_in * 1000\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt,\n userId: data.user_id_string,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n\n // Handle network errors and other exceptions\n throw new TokenRefreshError(\n `Failed to refresh token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n\n/**\n * Exchange an authorization code for OAuth2 tokens\n *\n * This is used during the initial OAuth flow after the user authorizes the app.\n * Uses public client pattern with PKCE (code verifier is required).\n *\n * @param code - Authorization code from OAuth callback\n * @param codeVerifier - PKCE code verifier (required for public clients)\n * @param redirectUri - Redirect URI used in the authorization request\n * @param clientId - OAuth2 client ID (defaults to config)\n * @returns Promise resolving to tokens\n * @throws TokenRefreshError if exchange fails\n */\nexport async function exchangeCodeForTokens(\n code: string,\n codeVerifier: string,\n redirectUri: string = OAUTH_CONFIG.redirectUri,\n clientId: string = OAUTH_CONFIG.clientId,\n): Promise<FigmaTokens> {\n // Prepare form data for token exchange (public client with PKCE)\n const params = new URLSearchParams({\n client_id: clientId,\n code,\n code_verifier: codeVerifier,\n grant_type: 'authorization_code',\n redirect_uri: redirectUri,\n })\n\n try {\n // Public client - no client secret or Basic Auth header needed\n // Security is provided by PKCE (code_verifier in the request body)\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n }\n\n // Debug logging\n if (isDebug()) {\n log.debug(\n `Token exchange request: ${JSON.stringify({\n url: OAUTH_CONFIG.tokenUrl,\n headers,\n body: params.toString(),\n })}`,\n )\n }\n\n const response = await fetch(OAUTH_CONFIG.tokenUrl, {\n method: 'POST',\n headers,\n body: params.toString(),\n })\n\n if (isDebug()) {\n log.debug(\n `Token exchange response: ${JSON.stringify({\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n })}`,\n )\n }\n\n if (!response.ok) {\n const errorText = await response.text()\n if (isDebug()) {\n log.debug(`Error response body: ${errorText}`)\n }\n throw new TokenRefreshError(\n `Token exchange failed: ${response.statusText}. ${errorText}`,\n response.status,\n )\n }\n\n const data = (await response.json()) as FigmaTokenResponse\n\n if (isDebug()) {\n log.debug(\n `Token response: ${JSON.stringify({\n access_token: data.access_token,\n token_type: data.token_type,\n expires_in: data.expires_in,\n scope: data.scope,\n user_id_string: data.user_id_string,\n refresh_token: data.refresh_token?.substring(0, 10) + '...',\n })}`,\n )\n }\n\n // Validate response contains required fields\n if (!data.access_token || !data.refresh_token || !data.expires_in) {\n throw new TokenRefreshError('Invalid token response: missing required fields')\n }\n\n // Convert response to our internal token format\n const expiresAt = Date.now() + data.expires_in * 1000\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt,\n userId: data.user_id_string,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n\n // Handle network errors and other exceptions\n throw new TokenRefreshError(\n `Failed to exchange code for tokens: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n }\n}\n"],"names":["OAUTH_CONFIG","isDebug","log","TokenRefreshError","Error","message","statusCode","name","refreshAccessToken","refreshToken","clientId","params","URLSearchParams","client_id","grant_type","refresh_token","response","fetch","refreshUrl","method","headers","body","toString","ok","errorText","text","statusText","status","data","json","access_token","expires_in","expiresAt","Date","now","accessToken","userId","user_id_string","error","exchangeCodeForTokens","code","codeVerifier","redirectUri","code_verifier","redirect_uri","debug","JSON","stringify","url","tokenUrl","Object","fromEntries","entries","token_type","scope","substring"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=refresh.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.spec.d.ts","sourceRoot":"","sources":["../../src/auth/refresh.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,105 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { mockFigmaTokenResponse } from './__tests__/fixtures.js';
3
+ import { mockFetchFailure, mockFetchSuccess, restoreFetch } from './__tests__/mocks.js';
4
+ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './refresh.js';
5
+ describe('refreshAccessToken', ()=>{
6
+ const testRefreshToken = 'figt_test_refresh_token_987654321';
7
+ const testClientId = 'test-client-id';
8
+ const originalDebug = process.env.DEBUG;
9
+ beforeEach(()=>{
10
+ vi.clearAllMocks();
11
+ delete process.env.DEBUG;
12
+ });
13
+ afterEach(()=>{
14
+ restoreFetch();
15
+ if (originalDebug !== undefined) {
16
+ process.env.DEBUG = originalDebug;
17
+ }
18
+ });
19
+ it('should successfully refresh access token with correct HTTP request', async ()=>{
20
+ const mockResponse = mockFigmaTokenResponse();
21
+ const mockFetch = mockFetchSuccess(mockResponse);
22
+ const result = await refreshAccessToken(testRefreshToken, testClientId);
23
+ // Verify response transformation
24
+ expect(result).toMatchObject({
25
+ accessToken: mockResponse.access_token,
26
+ refreshToken: mockResponse.refresh_token,
27
+ userId: mockResponse.user_id_string
28
+ });
29
+ expect(result.expiresAt).toBeGreaterThan(Date.now());
30
+ // Verify HTTP request details
31
+ expect(mockFetch).toHaveBeenCalledWith('https://api.staging.figma.com/v1/oauth/refresh', expect.objectContaining({
32
+ method: 'POST'
33
+ }));
34
+ // Verify headers (public client - no Authorization header)
35
+ const callArgs = mockFetch.mock.calls[0];
36
+ const headers = callArgs[1].headers;
37
+ expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded');
38
+ expect(headers['Authorization']).toBeUndefined(); // Public client - no Basic Auth
39
+ // Verify request body includes client_id
40
+ const body = callArgs[1].body;
41
+ expect(body).toContain('grant_type=refresh_token');
42
+ expect(body).toContain(`refresh_token=${testRefreshToken}`);
43
+ expect(body).toContain(`client_id=${testClientId}`);
44
+ });
45
+ it('should throw TokenRefreshError on HTTP error', async ()=>{
46
+ mockFetchFailure(401, 'Invalid refresh token');
47
+ await expect(refreshAccessToken(testRefreshToken, testClientId)).rejects.toThrow(TokenRefreshError);
48
+ });
49
+ });
50
+ describe('exchangeCodeForTokens', ()=>{
51
+ const testAuthCode = 'auth_code_123';
52
+ const testCodeVerifier = 'code_verifier_123456789012345678901234567890';
53
+ const testRedirectUri = 'http://localhost:3000/callback';
54
+ const testClientId = 'test-client-id';
55
+ const originalDebug = process.env.DEBUG;
56
+ beforeEach(()=>{
57
+ vi.clearAllMocks();
58
+ delete process.env.DEBUG;
59
+ });
60
+ afterEach(()=>{
61
+ restoreFetch();
62
+ if (originalDebug !== undefined) {
63
+ process.env.DEBUG = originalDebug;
64
+ }
65
+ });
66
+ it('should successfully exchange authorization code for tokens with PKCE', async ()=>{
67
+ const mockResponse = mockFigmaTokenResponse();
68
+ const mockFetch = mockFetchSuccess(mockResponse);
69
+ const result = await exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId);
70
+ // Verify response transformation
71
+ expect(result).toMatchObject({
72
+ accessToken: mockResponse.access_token,
73
+ refreshToken: mockResponse.refresh_token,
74
+ userId: mockResponse.user_id_string
75
+ });
76
+ expect(result.expiresAt).toBeGreaterThan(Date.now());
77
+ // Verify HTTP request
78
+ expect(mockFetch).toHaveBeenCalledWith('https://api.staging.figma.com/v1/oauth/token', expect.objectContaining({
79
+ method: 'POST'
80
+ }));
81
+ // Verify headers (public client - no Authorization header)
82
+ const callArgs = mockFetch.mock.calls[0];
83
+ const headers = callArgs[1].headers;
84
+ expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded');
85
+ expect(headers['Authorization']).toBeUndefined(); // Public client - no Basic Auth
86
+ });
87
+ it('should send correct request body with PKCE', async ()=>{
88
+ const mockResponse = mockFigmaTokenResponse();
89
+ const mockFetch = mockFetchSuccess(mockResponse);
90
+ await exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId);
91
+ const callArgs = mockFetch.mock.calls[0];
92
+ const body = callArgs[1].body;
93
+ expect(body).toContain('grant_type=authorization_code');
94
+ expect(body).toContain(`code=${testAuthCode}`);
95
+ expect(body).toContain(`redirect_uri=${encodeURIComponent(testRedirectUri)}`);
96
+ expect(body).toContain(`client_id=${testClientId}`);
97
+ expect(body).toContain(`code_verifier=${testCodeVerifier}`);
98
+ });
99
+ it('should throw TokenRefreshError on invalid authorization code', async ()=>{
100
+ mockFetchFailure(400, 'Invalid authorization code');
101
+ await expect(exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId)).rejects.toThrow(TokenRefreshError);
102
+ });
103
+ });
104
+
105
+ //# sourceMappingURL=refresh.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/refresh.spec.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n\nimport { mockFigmaTokenResponse } from './__tests__/fixtures.js'\nimport { mockFetchFailure, mockFetchSuccess, restoreFetch } from './__tests__/mocks.js'\nimport { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './refresh.js'\n\ndescribe('refreshAccessToken', () => {\n const testRefreshToken = 'figt_test_refresh_token_987654321'\n const testClientId = 'test-client-id'\n const originalDebug = process.env.DEBUG\n\n beforeEach(() => {\n vi.clearAllMocks()\n delete process.env.DEBUG\n })\n\n afterEach(() => {\n restoreFetch()\n if (originalDebug !== undefined) {\n process.env.DEBUG = originalDebug\n }\n })\n\n it('should successfully refresh access token with correct HTTP request', async () => {\n const mockResponse = mockFigmaTokenResponse()\n const mockFetch = mockFetchSuccess(mockResponse)\n\n const result = await refreshAccessToken(testRefreshToken, testClientId)\n\n // Verify response transformation\n expect(result).toMatchObject({\n accessToken: mockResponse.access_token,\n refreshToken: mockResponse.refresh_token,\n userId: mockResponse.user_id_string,\n })\n expect(result.expiresAt).toBeGreaterThan(Date.now())\n\n // Verify HTTP request details\n expect(mockFetch).toHaveBeenCalledWith(\n 'https://api.staging.figma.com/v1/oauth/refresh',\n expect.objectContaining({\n method: 'POST',\n }),\n )\n\n // Verify headers (public client - no Authorization header)\n const callArgs = mockFetch.mock.calls[0]\n const headers = callArgs[1].headers\n expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded')\n expect(headers['Authorization']).toBeUndefined() // Public client - no Basic Auth\n\n // Verify request body includes client_id\n const body = callArgs[1].body\n expect(body).toContain('grant_type=refresh_token')\n expect(body).toContain(`refresh_token=${testRefreshToken}`)\n expect(body).toContain(`client_id=${testClientId}`)\n })\n\n it('should throw TokenRefreshError on HTTP error', async () => {\n mockFetchFailure(401, 'Invalid refresh token')\n\n await expect(refreshAccessToken(testRefreshToken, testClientId)).rejects.toThrow(\n TokenRefreshError,\n )\n })\n})\n\ndescribe('exchangeCodeForTokens', () => {\n const testAuthCode = 'auth_code_123'\n const testCodeVerifier = 'code_verifier_123456789012345678901234567890'\n const testRedirectUri = 'http://localhost:3000/callback'\n const testClientId = 'test-client-id'\n const originalDebug = process.env.DEBUG\n\n beforeEach(() => {\n vi.clearAllMocks()\n delete process.env.DEBUG\n })\n\n afterEach(() => {\n restoreFetch()\n if (originalDebug !== undefined) {\n process.env.DEBUG = originalDebug\n }\n })\n\n it('should successfully exchange authorization code for tokens with PKCE', async () => {\n const mockResponse = mockFigmaTokenResponse()\n const mockFetch = mockFetchSuccess(mockResponse)\n\n const result = await exchangeCodeForTokens(\n testAuthCode,\n testCodeVerifier,\n testRedirectUri,\n testClientId,\n )\n\n // Verify response transformation\n expect(result).toMatchObject({\n accessToken: mockResponse.access_token,\n refreshToken: mockResponse.refresh_token,\n userId: mockResponse.user_id_string,\n })\n expect(result.expiresAt).toBeGreaterThan(Date.now())\n\n // Verify HTTP request\n expect(mockFetch).toHaveBeenCalledWith(\n 'https://api.staging.figma.com/v1/oauth/token',\n expect.objectContaining({\n method: 'POST',\n }),\n )\n\n // Verify headers (public client - no Authorization header)\n const callArgs = mockFetch.mock.calls[0]\n const headers = callArgs[1].headers\n expect(headers['Content-Type']).toBe('application/x-www-form-urlencoded')\n expect(headers['Authorization']).toBeUndefined() // Public client - no Basic Auth\n })\n\n it('should send correct request body with PKCE', async () => {\n const mockResponse = mockFigmaTokenResponse()\n const mockFetch = mockFetchSuccess(mockResponse)\n\n await exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId)\n\n const callArgs = mockFetch.mock.calls[0]\n const body = callArgs[1].body\n\n expect(body).toContain('grant_type=authorization_code')\n expect(body).toContain(`code=${testAuthCode}`)\n expect(body).toContain(`redirect_uri=${encodeURIComponent(testRedirectUri)}`)\n expect(body).toContain(`client_id=${testClientId}`)\n expect(body).toContain(`code_verifier=${testCodeVerifier}`)\n })\n\n it('should throw TokenRefreshError on invalid authorization code', async () => {\n mockFetchFailure(400, 'Invalid authorization code')\n\n await expect(\n exchangeCodeForTokens(testAuthCode, testCodeVerifier, testRedirectUri, testClientId),\n ).rejects.toThrow(TokenRefreshError)\n })\n})\n"],"names":["afterEach","beforeEach","describe","expect","it","vi","mockFigmaTokenResponse","mockFetchFailure","mockFetchSuccess","restoreFetch","exchangeCodeForTokens","refreshAccessToken","TokenRefreshError","testRefreshToken","testClientId","originalDebug","process","env","DEBUG","clearAllMocks","undefined","mockResponse","mockFetch","result","toMatchObject","accessToken","access_token","refreshToken","refresh_token","userId","user_id_string","expiresAt","toBeGreaterThan","Date","now","toHaveBeenCalledWith","objectContaining","method","callArgs","mock","calls","headers","toBe","toBeUndefined","body","toContain","rejects","toThrow","testAuthCode","testCodeVerifier","testRedirectUri","encodeURIComponent"],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ import type { FigmaTokens, JWTPayload, ProjectToken, TokenStoreConfig } from './types.js';
2
+ /**
3
+ * Secure storage manager for Figma OAuth2 tokens
4
+ *
5
+ * Uses the `conf` library to store tokens in an OS-specific secure location:
6
+ * - macOS: ~/Library/Preferences/payloadcms-figma
7
+ * - Linux: ~/.config/payloadcms-figma or $XDG_CONFIG_HOME/payloadcms-figma
8
+ * - Windows: %APPDATA%/payloadcms-figma/Config
9
+ *
10
+ * Tokens are encrypted at rest with a machine-specific key and file permissions
11
+ * are set to 0600 (owner read/write only)
12
+ */
13
+ export declare class TokenStore {
14
+ private config;
15
+ constructor(options?: TokenStoreConfig);
16
+ /**
17
+ * Retrieve stored tokens
18
+ * @returns FigmaTokens if stored, null otherwise
19
+ */
20
+ getTokens(): FigmaTokens | null;
21
+ /**
22
+ * Store OAuth2 tokens securely
23
+ * @param tokens - Figma OAuth2 tokens to store
24
+ */
25
+ setTokens(tokens: FigmaTokens): void;
26
+ /**
27
+ * Clear all stored tokens (used for logout)
28
+ */
29
+ clearTokens(): void;
30
+ /**
31
+ * Check if valid (non-expired) tokens exist
32
+ * @returns true if tokens exist and are not expired
33
+ */
34
+ hasValidTokens(): boolean;
35
+ /**
36
+ * Check if the current access token is expired
37
+ * Includes a buffer time to avoid using tokens that are about to expire
38
+ * @returns true if token is expired or will expire within buffer time
39
+ */
40
+ isExpired(): boolean;
41
+ /**
42
+ * Get the access token if it exists and is valid
43
+ * @returns Access token string or null if expired/missing
44
+ */
45
+ getAccessToken(): null | string;
46
+ /**
47
+ * Get the refresh token if it exists
48
+ * @returns Refresh token string or null if missing
49
+ */
50
+ getRefreshToken(): null | string;
51
+ /**
52
+ * Update just the access token after a refresh
53
+ * Preserves the existing refresh token and other metadata
54
+ * @param accessToken - New access token
55
+ * @param expiresIn - Expiration time in seconds
56
+ */
57
+ updateAccessToken(accessToken: string, expiresIn: number): void;
58
+ /**
59
+ * Get the path to the config file (useful for debugging)
60
+ * @returns Absolute path to the token storage file
61
+ */
62
+ getStoragePath(): string;
63
+ /**
64
+ * Retrieve a project token for a specific tenant
65
+ * Automatically removes expired tokens from storage
66
+ * @param tenantId - The tenant/CMS ID
67
+ * @returns ProjectToken if stored and valid, null otherwise
68
+ */
69
+ getProjectToken(tenantId: string): null | ProjectToken;
70
+ /**
71
+ * Store a project token for a specific tenant
72
+ * @param tenantId - The tenant/CMS ID
73
+ * @param projectToken - The project token to store
74
+ */
75
+ setProjectToken(tenantId: string, projectToken: ProjectToken): void;
76
+ /**
77
+ * Clear a project token for a specific tenant
78
+ * @param tenantId - The tenant/CMS ID
79
+ */
80
+ clearProjectToken(tenantId: string): void;
81
+ /**
82
+ * Clear all project tokens
83
+ */
84
+ clearAllProjectTokens(): void;
85
+ /**
86
+ * Check if a project token is expired
87
+ * Includes a buffer time to avoid using tokens that are about to expire
88
+ * @param projectToken - The project token to check
89
+ * @returns true if token is expired or will expire within buffer time
90
+ */
91
+ isProjectTokenExpired(projectToken: ProjectToken): boolean;
92
+ /**
93
+ * Check if a valid (non-expired) project token exists for a tenant
94
+ * @param tenantId - The tenant/CMS ID
95
+ * @returns true if token exists and is not expired
96
+ */
97
+ hasValidProjectToken(tenantId: string): boolean;
98
+ /**
99
+ * Get validated JWT claims from a project token
100
+ * @param tenantId - The tenant/CMS ID
101
+ * @returns JWT payload claims or null if token not found or has no claims
102
+ */
103
+ getProjectTokenClaims(tenantId: string): JWTPayload | null;
104
+ /**
105
+ * Get all tenant IDs that have stored project tokens
106
+ * @returns Array of tenant IDs
107
+ */
108
+ getAllProjectTokenTenantIds(): string[];
109
+ }
110
+ //# sourceMappingURL=token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAKzF;;;;;;;;;;GAUG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAA4E;gBAE9E,OAAO,CAAC,EAAE,gBAAgB;IAWtC;;;OAGG;IACH,SAAS,IAAI,WAAW,GAAG,IAAI;IAK/B;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB;;;OAGG;IACH,cAAc,IAAI,OAAO;IAQzB;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAcpB;;;OAGG;IACH,cAAc,IAAI,IAAI,GAAG,MAAM;IAQ/B;;;OAGG;IACH,eAAe,IAAI,IAAI,GAAG,MAAM;IAKhC;;;;;OAKG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAe/D;;;OAGG;IACH,cAAc,IAAI,MAAM;IAIxB;;;;;OAKG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,YAAY;IAiBtD;;;;OAIG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI;IAMnE;;;OAGG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAMzC;;OAEG;IACH,qBAAqB,IAAI,IAAI;IAI7B;;;;;OAKG;IACH,qBAAqB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO;IAQ1D;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAQ/C;;;;OAIG;IACH,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAK1D;;;OAGG;IACH,2BAA2B,IAAI,MAAM,EAAE;CAIxC"}
@@ -0,0 +1,188 @@
1
+ /* eslint-disable perfectionist/sort-classes */ import Conf from 'conf';
2
+ import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
3
+ import { deriveEncryptionKey } from './crypto-utils.js';
4
+ /**
5
+ * Secure storage manager for Figma OAuth2 tokens
6
+ *
7
+ * Uses the `conf` library to store tokens in an OS-specific secure location:
8
+ * - macOS: ~/Library/Preferences/payloadcms-figma
9
+ * - Linux: ~/.config/payloadcms-figma or $XDG_CONFIG_HOME/payloadcms-figma
10
+ * - Windows: %APPDATA%/payloadcms-figma/Config
11
+ *
12
+ * Tokens are encrypted at rest with a machine-specific key and file permissions
13
+ * are set to 0600 (owner read/write only)
14
+ */ export class TokenStore {
15
+ config;
16
+ constructor(options){
17
+ this.config = new Conf({
18
+ configName: options?.configName || 'payloadcms-figma',
19
+ projectName: 'payloadcms-figma',
20
+ // Enable encryption with machine-specific key for better security
21
+ encryptionKey: options?.encryptionKey || deriveEncryptionKey(),
22
+ // Clear schema to avoid validation issues
23
+ clearInvalidConfig: true
24
+ });
25
+ }
26
+ /**
27
+ * Retrieve stored tokens
28
+ * @returns FigmaTokens if stored, null otherwise
29
+ */ getTokens() {
30
+ const tokens = this.config.get('tokens');
31
+ return tokens || null;
32
+ }
33
+ /**
34
+ * Store OAuth2 tokens securely
35
+ * @param tokens - Figma OAuth2 tokens to store
36
+ */ setTokens(tokens) {
37
+ this.config.set('tokens', tokens);
38
+ }
39
+ /**
40
+ * Clear all stored tokens (used for logout)
41
+ */ clearTokens() {
42
+ this.config.delete('tokens');
43
+ }
44
+ /**
45
+ * Check if valid (non-expired) tokens exist
46
+ * @returns true if tokens exist and are not expired
47
+ */ hasValidTokens() {
48
+ const tokens = this.getTokens();
49
+ if (!tokens) {
50
+ return false;
51
+ }
52
+ return !this.isExpired();
53
+ }
54
+ /**
55
+ * Check if the current access token is expired
56
+ * Includes a buffer time to avoid using tokens that are about to expire
57
+ * @returns true if token is expired or will expire within buffer time
58
+ */ isExpired() {
59
+ // Access config directly to avoid circular dependency with getTokens()
60
+ const tokens = this.config.get('tokens');
61
+ if (!tokens) {
62
+ return true;
63
+ }
64
+ const now = Date.now();
65
+ const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
66
+ const expiryWithBuffer = tokens.expiresAt - bufferMs;
67
+ return now >= expiryWithBuffer;
68
+ }
69
+ /**
70
+ * Get the access token if it exists and is valid
71
+ * @returns Access token string or null if expired/missing
72
+ */ getAccessToken() {
73
+ if (!this.hasValidTokens()) {
74
+ return null;
75
+ }
76
+ const tokens = this.getTokens();
77
+ return tokens?.accessToken || null;
78
+ }
79
+ /**
80
+ * Get the refresh token if it exists
81
+ * @returns Refresh token string or null if missing
82
+ */ getRefreshToken() {
83
+ const tokens = this.getTokens();
84
+ return tokens?.refreshToken || null;
85
+ }
86
+ /**
87
+ * Update just the access token after a refresh
88
+ * Preserves the existing refresh token and other metadata
89
+ * @param accessToken - New access token
90
+ * @param expiresIn - Expiration time in seconds
91
+ */ updateAccessToken(accessToken, expiresIn) {
92
+ const existingTokens = this.getTokens();
93
+ if (!existingTokens) {
94
+ throw new Error('Cannot update access token: no existing tokens found');
95
+ }
96
+ const expiresAt = Date.now() + expiresIn * 1000;
97
+ this.setTokens({
98
+ ...existingTokens,
99
+ accessToken,
100
+ expiresAt
101
+ });
102
+ }
103
+ /**
104
+ * Get the path to the config file (useful for debugging)
105
+ * @returns Absolute path to the token storage file
106
+ */ getStoragePath() {
107
+ return this.config.path;
108
+ }
109
+ /**
110
+ * Retrieve a project token for a specific tenant
111
+ * Automatically removes expired tokens from storage
112
+ * @param tenantId - The tenant/CMS ID
113
+ * @returns ProjectToken if stored and valid, null otherwise
114
+ */ getProjectToken(tenantId) {
115
+ const projectTokens = this.config.get('projectTokens') || {};
116
+ const projectToken = projectTokens[tenantId];
117
+ if (!projectToken) {
118
+ return null;
119
+ }
120
+ // Auto-cleanup expired project token
121
+ if (this.isProjectTokenExpired(projectToken)) {
122
+ this.clearProjectToken(tenantId);
123
+ return null;
124
+ }
125
+ return projectToken;
126
+ }
127
+ /**
128
+ * Store a project token for a specific tenant
129
+ * @param tenantId - The tenant/CMS ID
130
+ * @param projectToken - The project token to store
131
+ */ setProjectToken(tenantId, projectToken) {
132
+ const projectTokens = this.config.get('projectTokens') || {};
133
+ projectTokens[tenantId] = projectToken;
134
+ this.config.set('projectTokens', projectTokens);
135
+ }
136
+ /**
137
+ * Clear a project token for a specific tenant
138
+ * @param tenantId - The tenant/CMS ID
139
+ */ clearProjectToken(tenantId) {
140
+ const projectTokens = this.config.get('projectTokens') || {};
141
+ delete projectTokens[tenantId];
142
+ this.config.set('projectTokens', projectTokens);
143
+ }
144
+ /**
145
+ * Clear all project tokens
146
+ */ clearAllProjectTokens() {
147
+ this.config.delete('projectTokens');
148
+ }
149
+ /**
150
+ * Check if a project token is expired
151
+ * Includes a buffer time to avoid using tokens that are about to expire
152
+ * @param projectToken - The project token to check
153
+ * @returns true if token is expired or will expire within buffer time
154
+ */ isProjectTokenExpired(projectToken) {
155
+ const now = Date.now();
156
+ const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
157
+ const expiryWithBuffer = projectToken.expiresAt - bufferMs;
158
+ return now >= expiryWithBuffer;
159
+ }
160
+ /**
161
+ * Check if a valid (non-expired) project token exists for a tenant
162
+ * @param tenantId - The tenant/CMS ID
163
+ * @returns true if token exists and is not expired
164
+ */ hasValidProjectToken(tenantId) {
165
+ const projectToken = this.getProjectToken(tenantId);
166
+ if (!projectToken) {
167
+ return false;
168
+ }
169
+ return !this.isProjectTokenExpired(projectToken);
170
+ }
171
+ /**
172
+ * Get validated JWT claims from a project token
173
+ * @param tenantId - The tenant/CMS ID
174
+ * @returns JWT payload claims or null if token not found or has no claims
175
+ */ getProjectTokenClaims(tenantId) {
176
+ const projectToken = this.getProjectToken(tenantId);
177
+ return projectToken?.claims || null;
178
+ }
179
+ /**
180
+ * Get all tenant IDs that have stored project tokens
181
+ * @returns Array of tenant IDs
182
+ */ getAllProjectTokenTenantIds() {
183
+ const projectTokens = this.config.get('projectTokens') || {};
184
+ return Object.keys(projectTokens);
185
+ }
186
+ }
187
+
188
+ //# sourceMappingURL=token-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/token-store.ts"],"sourcesContent":["/* eslint-disable perfectionist/sort-classes */\nimport Conf from 'conf'\n\nimport type { FigmaTokens, JWTPayload, ProjectToken, TokenStoreConfig } from './types.js'\n\nimport { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js'\nimport { deriveEncryptionKey } from './crypto-utils.js'\n\n/**\n * Secure storage manager for Figma OAuth2 tokens\n *\n * Uses the `conf` library to store tokens in an OS-specific secure location:\n * - macOS: ~/Library/Preferences/payloadcms-figma\n * - Linux: ~/.config/payloadcms-figma or $XDG_CONFIG_HOME/payloadcms-figma\n * - Windows: %APPDATA%/payloadcms-figma/Config\n *\n * Tokens are encrypted at rest with a machine-specific key and file permissions\n * are set to 0600 (owner read/write only)\n */\nexport class TokenStore {\n private config: Conf<{ projectTokens: Record<string, ProjectToken>; tokens: FigmaTokens }>\n\n constructor(options?: TokenStoreConfig) {\n this.config = new Conf<{ projectTokens: Record<string, ProjectToken>; tokens: FigmaTokens }>({\n configName: options?.configName || 'payloadcms-figma',\n projectName: 'payloadcms-figma',\n // Enable encryption with machine-specific key for better security\n encryptionKey: options?.encryptionKey || deriveEncryptionKey(),\n // Clear schema to avoid validation issues\n clearInvalidConfig: true,\n })\n }\n\n /**\n * Retrieve stored tokens\n * @returns FigmaTokens if stored, null otherwise\n */\n getTokens(): FigmaTokens | null {\n const tokens = this.config.get('tokens')\n return tokens || null\n }\n\n /**\n * Store OAuth2 tokens securely\n * @param tokens - Figma OAuth2 tokens to store\n */\n setTokens(tokens: FigmaTokens): void {\n this.config.set('tokens', tokens)\n }\n\n /**\n * Clear all stored tokens (used for logout)\n */\n clearTokens(): void {\n this.config.delete('tokens')\n }\n\n /**\n * Check if valid (non-expired) tokens exist\n * @returns true if tokens exist and are not expired\n */\n hasValidTokens(): boolean {\n const tokens = this.getTokens()\n if (!tokens) {\n return false\n }\n return !this.isExpired()\n }\n\n /**\n * Check if the current access token is expired\n * Includes a buffer time to avoid using tokens that are about to expire\n * @returns true if token is expired or will expire within buffer time\n */\n isExpired(): boolean {\n // Access config directly to avoid circular dependency with getTokens()\n const tokens = this.config.get('tokens')\n if (!tokens) {\n return true\n }\n\n const now = Date.now()\n const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000\n const expiryWithBuffer = tokens.expiresAt - bufferMs\n\n return now >= expiryWithBuffer\n }\n\n /**\n * Get the access token if it exists and is valid\n * @returns Access token string or null if expired/missing\n */\n getAccessToken(): null | string {\n if (!this.hasValidTokens()) {\n return null\n }\n const tokens = this.getTokens()\n return tokens?.accessToken || null\n }\n\n /**\n * Get the refresh token if it exists\n * @returns Refresh token string or null if missing\n */\n getRefreshToken(): null | string {\n const tokens = this.getTokens()\n return tokens?.refreshToken || null\n }\n\n /**\n * Update just the access token after a refresh\n * Preserves the existing refresh token and other metadata\n * @param accessToken - New access token\n * @param expiresIn - Expiration time in seconds\n */\n updateAccessToken(accessToken: string, expiresIn: number): void {\n const existingTokens = this.getTokens()\n if (!existingTokens) {\n throw new Error('Cannot update access token: no existing tokens found')\n }\n\n const expiresAt = Date.now() + expiresIn * 1000\n\n this.setTokens({\n ...existingTokens,\n accessToken,\n expiresAt,\n })\n }\n\n /**\n * Get the path to the config file (useful for debugging)\n * @returns Absolute path to the token storage file\n */\n getStoragePath(): string {\n return this.config.path\n }\n\n /**\n * Retrieve a project token for a specific tenant\n * Automatically removes expired tokens from storage\n * @param tenantId - The tenant/CMS ID\n * @returns ProjectToken if stored and valid, null otherwise\n */\n getProjectToken(tenantId: string): null | ProjectToken {\n const projectTokens = this.config.get('projectTokens') || {}\n const projectToken = projectTokens[tenantId]\n\n if (!projectToken) {\n return null\n }\n\n // Auto-cleanup expired project token\n if (this.isProjectTokenExpired(projectToken)) {\n this.clearProjectToken(tenantId)\n return null\n }\n\n return projectToken\n }\n\n /**\n * Store a project token for a specific tenant\n * @param tenantId - The tenant/CMS ID\n * @param projectToken - The project token to store\n */\n setProjectToken(tenantId: string, projectToken: ProjectToken): void {\n const projectTokens = this.config.get('projectTokens') || {}\n projectTokens[tenantId] = projectToken\n this.config.set('projectTokens', projectTokens)\n }\n\n /**\n * Clear a project token for a specific tenant\n * @param tenantId - The tenant/CMS ID\n */\n clearProjectToken(tenantId: string): void {\n const projectTokens = this.config.get('projectTokens') || {}\n delete projectTokens[tenantId]\n this.config.set('projectTokens', projectTokens)\n }\n\n /**\n * Clear all project tokens\n */\n clearAllProjectTokens(): void {\n this.config.delete('projectTokens')\n }\n\n /**\n * Check if a project token is expired\n * Includes a buffer time to avoid using tokens that are about to expire\n * @param projectToken - The project token to check\n * @returns true if token is expired or will expire within buffer time\n */\n isProjectTokenExpired(projectToken: ProjectToken): boolean {\n const now = Date.now()\n const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000\n const expiryWithBuffer = projectToken.expiresAt - bufferMs\n\n return now >= expiryWithBuffer\n }\n\n /**\n * Check if a valid (non-expired) project token exists for a tenant\n * @param tenantId - The tenant/CMS ID\n * @returns true if token exists and is not expired\n */\n hasValidProjectToken(tenantId: string): boolean {\n const projectToken = this.getProjectToken(tenantId)\n if (!projectToken) {\n return false\n }\n return !this.isProjectTokenExpired(projectToken)\n }\n\n /**\n * Get validated JWT claims from a project token\n * @param tenantId - The tenant/CMS ID\n * @returns JWT payload claims or null if token not found or has no claims\n */\n getProjectTokenClaims(tenantId: string): JWTPayload | null {\n const projectToken = this.getProjectToken(tenantId)\n return projectToken?.claims || null\n }\n\n /**\n * Get all tenant IDs that have stored project tokens\n * @returns Array of tenant IDs\n */\n getAllProjectTokenTenantIds(): string[] {\n const projectTokens = this.config.get('projectTokens') || {}\n return Object.keys(projectTokens)\n }\n}\n"],"names":["Conf","TOKEN_EXPIRY_BUFFER_SECONDS","deriveEncryptionKey","TokenStore","config","options","configName","projectName","encryptionKey","clearInvalidConfig","getTokens","tokens","get","setTokens","set","clearTokens","delete","hasValidTokens","isExpired","now","Date","bufferMs","expiryWithBuffer","expiresAt","getAccessToken","accessToken","getRefreshToken","refreshToken","updateAccessToken","expiresIn","existingTokens","Error","getStoragePath","path","getProjectToken","tenantId","projectTokens","projectToken","isProjectTokenExpired","clearProjectToken","setProjectToken","clearAllProjectTokens","hasValidProjectToken","getProjectTokenClaims","claims","getAllProjectTokenTenantIds","Object","keys"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=token-store.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.spec.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.spec.ts"],"names":[],"mappings":""}