@payloadcms/figma 0.0.1-alpha.61 → 0.0.1-alpha.63

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 (50) hide show
  1. package/dist/api/control-plane.d.ts +4 -0
  2. package/dist/api/control-plane.js +11 -4
  3. package/dist/auth/crypto-utils.d.ts +11 -0
  4. package/dist/auth/crypto-utils.js +51 -0
  5. package/dist/auth/oauth-flow.js +3 -3
  6. package/dist/auth/project-token.d.ts +17 -6
  7. package/dist/auth/project-token.js +37 -19
  8. package/dist/auth/token-store-migration.d.ts +58 -0
  9. package/dist/auth/token-store-migration.js +156 -0
  10. package/dist/auth/token-store.d.ts +51 -128
  11. package/dist/auth/token-store.js +326 -187
  12. package/dist/auth/types.d.ts +8 -1
  13. package/dist/cli.js +9 -1
  14. package/dist/commands/debug.js +16 -12
  15. package/dist/commands/deploy.js +2 -0
  16. package/dist/commands/dump-tokens.d.ts +12 -0
  17. package/dist/commands/dump-tokens.js +69 -0
  18. package/dist/commands/init.js +8 -4
  19. package/dist/commands/list-tokens.js +2 -2
  20. package/dist/commands/login.js +1 -1
  21. package/dist/commands/logout.js +23 -4
  22. package/dist/config/oauth.d.ts +10 -2
  23. package/dist/config/oauth.js +18 -4
  24. package/dist/constants.d.ts +4 -2
  25. package/dist/constants.js +19 -1
  26. package/dist/db-content-api/index.js +21 -0
  27. package/dist/db-content-api/utilities/auth.js +4 -1
  28. package/dist/db-content-api/utilities/data/validateRelationships.d.ts +7 -0
  29. package/dist/db-content-api/utilities/data/validateRelationships.js +102 -0
  30. package/dist/oauth/defaults.d.ts +3 -1
  31. package/dist/oauth/defaults.js +6 -5
  32. package/dist/oauth/endpoints/getLoginEndpoint.js +1 -1
  33. package/dist/oauth/index.js +1 -0
  34. package/dist/oauth/types.d.ts +10 -0
  35. package/dist/plugin/auth-preflight.d.ts +8 -0
  36. package/dist/plugin/auth-preflight.js +20 -0
  37. package/dist/plugin/bootstrap-preflight.d.ts +23 -0
  38. package/dist/plugin/bootstrap-preflight.js +45 -0
  39. package/dist/plugin/build-config.js +73 -12
  40. package/dist/plugin/dev-cookie-names.d.ts +14 -0
  41. package/dist/plugin/dev-cookie-names.js +19 -0
  42. package/dist/storage-content-api/client.js +4 -1
  43. package/dist/storage-content-api/staticHandler.js +1 -18
  44. package/dist/utils/build-lambda-zip.d.ts +4 -2
  45. package/dist/utils/build-lambda-zip.js +4 -7
  46. package/dist/utils/messages.js +1 -1
  47. package/dist/utils/token-display.d.ts +5 -1
  48. package/dist/utils/token-display.js +47 -26
  49. package/dist/utils/version-check.js +1 -1
  50. package/package.json +2 -1
@@ -28,6 +28,10 @@ export interface CreateTenantOptions {
28
28
  * Options for creating a new deployment
29
29
  */
30
30
  export interface CreateDeploymentOptions {
31
+ /** Framework adapter name (e.g., "vite", "nextjs"). Forwarded to Gatekeeper. */
32
+ adapter?: string;
33
+ /** SPA fallback path (e.g., "/index.html"). Vite-only. */
34
+ fallback?: string;
31
35
  /** Pages keyed by route, each with its associated asset keys */
32
36
  pages: Record<string, {
33
37
  assets: string[];
@@ -184,14 +184,21 @@ import * as log from '../utils/log.js';
184
184
  assets: pageData.assets
185
185
  };
186
186
  }
187
+ const body = {
188
+ pages,
189
+ static_assets: options.staticAssets
190
+ };
191
+ if (options.adapter) {
192
+ body.adapter = options.adapter;
193
+ }
194
+ if (options.fallback) {
195
+ body.fallback = options.fallback;
196
+ }
187
197
  // REAL API IMPLEMENTATION
188
198
  const response = await controlPlaneFetch({
189
199
  context: 'create deployment',
190
200
  options: {
191
- body: JSON.stringify({
192
- pages,
193
- static_assets: options.staticAssets
194
- }),
201
+ body: JSON.stringify(body),
195
202
  headers: {
196
203
  ...getAuthHeaders(credential),
197
204
  'Content-Type': 'application/json'
@@ -36,4 +36,15 @@ export declare function deriveEncryptionKey(): string;
36
36
  * without exposing the actual key.
37
37
  */
38
38
  export declare function getEncryptionKeyHash(): string;
39
+ /**
40
+ * Write decrypted versions of all encrypted store files for debugging.
41
+ *
42
+ * For each .json file in the store directory, creates a neighboring .decrypted.json
43
+ * file containing the decrypted contents. Useful for debugging token storage issues.
44
+ *
45
+ * @param rootDir - Root directory of the token store (e.g., ~/Library/Preferences/payloadcms-figma-nodejs)
46
+ * @param encryptionKey - The encryption key used by Conf
47
+ * @returns Array of paths to decrypted files created
48
+ */
49
+ export declare function exportDecryptedStoreFiles(rootDir: string, encryptionKey: string): string[];
39
50
  //# sourceMappingURL=crypto-utils.d.ts.map
@@ -1,6 +1,8 @@
1
1
  import Conf from 'conf';
2
2
  import crypto from 'crypto';
3
+ import fsSync from 'node:fs';
3
4
  import os from 'os';
5
+ import path from 'path';
4
6
  /**
5
7
  * Get or generate a random salt for key derivation
6
8
  *
@@ -94,5 +96,54 @@ import os from 'os';
94
96
  const key = deriveEncryptionKey();
95
97
  return crypto.createHash('sha256').update(key).digest('hex').substring(0, 8);
96
98
  }
99
+ /**
100
+ * Write decrypted versions of all encrypted store files for debugging.
101
+ *
102
+ * For each .json file in the store directory, creates a neighboring .decrypted.json
103
+ * file containing the decrypted contents. Useful for debugging token storage issues.
104
+ *
105
+ * @param rootDir - Root directory of the token store (e.g., ~/Library/Preferences/payloadcms-figma-nodejs)
106
+ * @param encryptionKey - The encryption key used by Conf
107
+ * @returns Array of paths to decrypted files created
108
+ */ export function exportDecryptedStoreFiles(rootDir, encryptionKey) {
109
+ const decryptedFiles = [];
110
+ function processDirectory(dir) {
111
+ if (!fsSync.existsSync(dir)) {
112
+ return;
113
+ }
114
+ const entries = fsSync.readdirSync(dir, {
115
+ withFileTypes: true
116
+ });
117
+ for (const entry of entries){
118
+ const fullPath = path.join(dir, entry.name);
119
+ if (entry.isDirectory()) {
120
+ processDirectory(fullPath);
121
+ } else if (entry.name.endsWith('.json') && !entry.name.endsWith('.decrypted.json') && !entry.name.includes('.corrupt-')) {
122
+ try {
123
+ // Extract configName from filename (remove .json extension)
124
+ const configName = entry.name.replace(/\.json$/, '');
125
+ // Create a Conf instance to read/decrypt the file
126
+ const config = new Conf({
127
+ clearInvalidConfig: false,
128
+ configName,
129
+ cwd: dir,
130
+ encryptionKey,
131
+ projectName: 'payloadcms-figma'
132
+ });
133
+ // Read all data from the store (Conf decrypts it)
134
+ const data = config.store;
135
+ // Write decrypted content to neighboring file
136
+ const decryptedPath = fullPath.replace(/\.json$/, '.decrypted.json');
137
+ fsSync.writeFileSync(decryptedPath, JSON.stringify(data, null, 2));
138
+ decryptedFiles.push(decryptedPath);
139
+ } catch {
140
+ // Skip files that can't be decrypted (e.g., crypto config, already unencrypted)
141
+ }
142
+ }
143
+ }
144
+ }
145
+ processDirectory(rootDir);
146
+ return decryptedFiles;
147
+ }
97
148
 
98
149
  //# sourceMappingURL=crypto-utils.js.map
@@ -147,15 +147,15 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
147
147
  * @throws OAuthFlowError if refresh fails
148
148
  */ export async function getValidOAuthAccessToken(tokenStore) {
149
149
  // Check if we have valid tokens
150
- if (tokenStore.hasValidTokens()) {
151
- const token = tokenStore.getAccessToken();
150
+ if (tokenStore.hasValidOauthToken()) {
151
+ const token = tokenStore.getCurrentOauthToken();
152
152
  return token ? {
153
153
  type: 'oauth',
154
154
  token
155
155
  } : null;
156
156
  }
157
157
  // Check if we have a refresh token
158
- const refreshToken = tokenStore.getRefreshToken();
158
+ const refreshToken = tokenStore.getCurrentRefreshToken();
159
159
  if (!refreshToken) {
160
160
  return null;
161
161
  }
@@ -6,6 +6,7 @@
6
6
  * and have a shorter lifespan (15-30 minutes) than OAuth tokens.
7
7
  */
8
8
  import type { TokenStore } from './token-store.js';
9
+ import type { ProjectStoreScope } from './types.js';
9
10
  /**
10
11
  * Error during project token operations
11
12
  */
@@ -26,22 +27,32 @@ export declare class ProjectTokenError extends Error {
26
27
  * The project token is used by local Payload instances to authenticate
27
28
  * requests to the Content API without requiring a round-trip to Sinatra.
28
29
  *
29
- * @param tokenStore - Token store for persisting tokens
30
- * @param tenantId - The tenant/CMS ID to get a token for
30
+ * @param params.tokenStore - Token store for persisting tokens
31
+ * @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
32
+ * @param params.projectInfo - Optional project scope (defaults to env vars)
31
33
  * @returns Promise resolving to JWT token string or null if failed
32
34
  * @throws {ProjectTokenError} If token generation fails
33
35
  */
34
- export declare function getValidProjectToken(tokenStore: TokenStore, tenantId: string): Promise<null | string>;
36
+ export declare function getValidProjectToken({ projectInfo, tenantId, tokenStore, }: {
37
+ projectInfo?: ProjectStoreScope;
38
+ tenantId?: string;
39
+ tokenStore: TokenStore;
40
+ }): Promise<null | string>;
35
41
  /**
36
42
  * Refresh a project token for a tenant
37
43
  *
38
44
  * Forces a refresh of the project token even if the current one is still valid.
39
45
  * Useful for testing or when you want to ensure you have the freshest token.
40
46
  *
41
- * @param tokenStore - Token store for persisting tokens
42
- * @param tenantId - The tenant/CMS ID to refresh token for
47
+ * @param params.tokenStore - Token store for persisting tokens
48
+ * @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
49
+ * @param params.projectInfo - Optional project scope (defaults to env vars)
43
50
  * @returns Promise resolving to JWT token string or null if failed
44
51
  * @throws {ProjectTokenError} If token refresh fails
45
52
  */
46
- export declare function refreshProjectToken(tokenStore: TokenStore, tenantId: string): Promise<null | string>;
53
+ export declare function refreshProjectToken(params: {
54
+ projectInfo?: ProjectStoreScope;
55
+ tenantId?: string;
56
+ tokenStore: TokenStore;
57
+ }): Promise<null | string>;
47
58
  //# sourceMappingURL=project-token.d.ts.map
@@ -55,18 +55,29 @@ import { getValidCredential } from './oauth-flow.js';
55
55
  * The project token is used by local Payload instances to authenticate
56
56
  * requests to the Content API without requiring a round-trip to Sinatra.
57
57
  *
58
- * @param tokenStore - Token store for persisting tokens
59
- * @param tenantId - The tenant/CMS ID to get a token for
58
+ * @param params.tokenStore - Token store for persisting tokens
59
+ * @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
60
+ * @param params.projectInfo - Optional project scope (defaults to env vars)
60
61
  * @returns Promise resolving to JWT token string or null if failed
61
62
  * @throws {ProjectTokenError} If token generation fails
62
- */ export async function getValidProjectToken(tokenStore, tenantId) {
63
+ */ export async function getValidProjectToken({ projectInfo, tenantId, tokenStore }) {
63
64
  if (process.env.FIGMA_CONTENT_API_ACCESS_KEY) {
64
65
  log.debug('FIGMA_CONTENT_API_ACCESS_KEY is set; skipping project token retrieval from API');
65
66
  return null;
66
67
  }
68
+ const resolvedTenantId = tenantId ?? tokenStore.getTenantId({
69
+ projectInfo
70
+ });
71
+ if (!resolvedTenantId) {
72
+ throw new ProjectTokenError('tenantId was not provided and could not be resolved from bootstrap data');
73
+ }
67
74
  // Check for existing valid project token
68
- if (tokenStore.hasValidProjectToken(tenantId)) {
69
- const projectToken = tokenStore.getProjectToken(tenantId);
75
+ if (tokenStore.hasValidProjectToken({
76
+ projectInfo
77
+ })) {
78
+ const projectToken = tokenStore.getProjectToken({
79
+ projectInfo
80
+ });
70
81
  return projectToken?.token || null;
71
82
  }
72
83
  // We need to refresh the project token
@@ -82,7 +93,7 @@ import { getValidCredential } from './oauth-flow.js';
82
93
  return null;
83
94
  }
84
95
  // Fetch a new project token from the Figma API
85
- const projectToken = await fetchProjectToken(credential, tenantId);
96
+ const fetchedToken = await fetchProjectToken(credential, resolvedTenantId);
86
97
  // Validate the JWT signature and claims (skip for mocks)
87
98
  const hasEnvToken = !!process.env.FIGMA_MOCK_PROJECT_TOKEN_VALUE;
88
99
  const shouldMock = process.env.FIGMA_MOCK_PROJECT_TOKEN !== 'false';
@@ -90,11 +101,11 @@ import { getValidCredential } from './oauth-flow.js';
90
101
  if (hasEnvToken || shouldMock) {
91
102
  // MOCK: Skip JWT validation and parse claims directly
92
103
  log.debug('Skipping JWT validation for mock/env project token');
93
- validatedClaims = parseJWTClaims(projectToken.token);
104
+ validatedClaims = parseJWTClaims(fetchedToken.token);
94
105
  } else {
95
106
  // REAL: Validate JWT signature against JWKS
96
107
  try {
97
- validatedClaims = await validateProjectToken(projectToken.token, getInfraEnvironment());
108
+ validatedClaims = await validateProjectToken(fetchedToken.token, getInfraEnvironment());
98
109
  } catch (validationError) {
99
110
  // Handle validation errors
100
111
  if (validationError instanceof JWTValidationError) {
@@ -108,13 +119,16 @@ import { getValidCredential } from './oauth-flow.js';
108
119
  throw new ProjectTokenError('Project token missing expiration (exp) claim');
109
120
  }
110
121
  // Store the project token with validated expiration and claims
111
- tokenStore.setProjectToken(tenantId, {
112
- claims: validatedClaims,
113
- expiresAt: validatedClaims.exp * 1000,
114
- tenantId,
115
- token: projectToken.token
122
+ tokenStore.setProjectToken({
123
+ projectInfo,
124
+ token: {
125
+ claims: validatedClaims,
126
+ expiresAt: validatedClaims.exp * 1000,
127
+ tenantId: resolvedTenantId,
128
+ token: fetchedToken.token
129
+ }
116
130
  });
117
- return projectToken.token;
131
+ return fetchedToken.token;
118
132
  }
119
133
  /**
120
134
  * Refresh a project token for a tenant
@@ -122,15 +136,19 @@ import { getValidCredential } from './oauth-flow.js';
122
136
  * Forces a refresh of the project token even if the current one is still valid.
123
137
  * Useful for testing or when you want to ensure you have the freshest token.
124
138
  *
125
- * @param tokenStore - Token store for persisting tokens
126
- * @param tenantId - The tenant/CMS ID to refresh token for
139
+ * @param params.tokenStore - Token store for persisting tokens
140
+ * @param params.tenantId - Optional tenant/CMS ID; resolved from stored bootstrap data when omitted
141
+ * @param params.projectInfo - Optional project scope (defaults to env vars)
127
142
  * @returns Promise resolving to JWT token string or null if failed
128
143
  * @throws {ProjectTokenError} If token refresh fails
129
- */ export async function refreshProjectToken(tokenStore, tenantId) {
144
+ */ export async function refreshProjectToken(params) {
145
+ const { projectInfo, tokenStore } = params;
130
146
  // Clear the existing project token to force a refresh
131
- tokenStore.clearProjectToken(tenantId);
147
+ tokenStore.clearProjectToken({
148
+ projectInfo
149
+ });
132
150
  // Get a new token (which will fetch from API since we just cleared it)
133
- return getValidProjectToken(tokenStore, tenantId);
151
+ return getValidProjectToken(params);
134
152
  }
135
153
 
136
154
  //# sourceMappingURL=project-token.js.map
@@ -0,0 +1,58 @@
1
+ import type { BootstrapData, FigmaTokens, ProjectToken } from './types.js';
2
+ export type LegacyStoreSchema = {
3
+ bootstrapData?: Record<string, BootstrapData>;
4
+ projectTokens?: Record<string, ProjectToken>;
5
+ tokens?: FigmaTokens;
6
+ };
7
+ export type MigrationCheckParams = {
8
+ checkFileExists?: (filePath: string) => boolean;
9
+ legacyFilePath: string;
10
+ newOAuthFilePath: string;
11
+ };
12
+ export type MigrateOAuthParams = {
13
+ encryptionKey: string;
14
+ legacyConfigName: string;
15
+ legacyCwd?: string;
16
+ newOAuthConfigName: string;
17
+ newOAuthCwd: string;
18
+ projectName: string;
19
+ };
20
+ /**
21
+ * Check if migration is needed.
22
+ * Returns true only when legacy file exists AND new oauth file does not.
23
+ */
24
+ export declare function needsMigration(params: MigrationCheckParams): boolean;
25
+ /**
26
+ * Migrate OAuth tokens from legacy store to new oauth store.
27
+ * Reads from legacy location, writes to new location with root-level keys.
28
+ *
29
+ * `clearInvalidConfig` is deliberately OFF so a transient decrypt error does
30
+ * not wipe the legacy file — older CLI versions elsewhere on disk still read
31
+ * it. If we cannot open the file, we log and skip migration.
32
+ */
33
+ export declare function migrateOAuth(params: MigrateOAuthParams): void;
34
+ export type ResolveProjectStorePath = (scope: {
35
+ environmentName: string;
36
+ projectId: string;
37
+ }) => {
38
+ configName: string;
39
+ cwd: string;
40
+ };
41
+ export type MigrateProjectDataParams = {
42
+ encryptionKey: string;
43
+ legacyConfigName: string;
44
+ legacyCwd?: string;
45
+ projectName: string;
46
+ resolveProjectStorePath: ResolveProjectStorePath;
47
+ };
48
+ /**
49
+ * Migrate per-project bootstrap data and project tokens from the legacy
50
+ * combined store into individual per-project stores.
51
+ *
52
+ * Walks legacy `bootstrapData` (keyed by `projectId:environmentName`), writes
53
+ * each entry into its own project store file, and stashes the matching
54
+ * `projectToken` (looked up by `bootstrapData.contentSystemId`) alongside it.
55
+ * Target files that already exist are left alone.
56
+ */
57
+ export declare function migrateProjectData(params: MigrateProjectDataParams): void;
58
+ //# sourceMappingURL=token-store-migration.d.ts.map
@@ -0,0 +1,156 @@
1
+ import Conf from 'conf';
2
+ import fsSync from 'node:fs';
3
+ import path from 'node:path';
4
+ import * as log from '../utils/log.js';
5
+ const OAUTH_STORE_SCHEMA_VERSION = 1;
6
+ const PROJECT_STORE_SCHEMA_VERSION = 1;
7
+ /**
8
+ * Check if migration is needed.
9
+ * Returns true only when legacy file exists AND new oauth file does not.
10
+ */ export function needsMigration(params) {
11
+ const checkExists = params.checkFileExists ?? fsSync.existsSync;
12
+ const newExists = checkExists(params.newOAuthFilePath);
13
+ if (newExists) {
14
+ return false;
15
+ }
16
+ return checkExists(params.legacyFilePath);
17
+ }
18
+ /**
19
+ * Migrate OAuth tokens from legacy store to new oauth store.
20
+ * Reads from legacy location, writes to new location with root-level keys.
21
+ *
22
+ * `clearInvalidConfig` is deliberately OFF so a transient decrypt error does
23
+ * not wipe the legacy file — older CLI versions elsewhere on disk still read
24
+ * it. If we cannot open the file, we log and skip migration.
25
+ */ export function migrateOAuth(params) {
26
+ const legacyConfig = openLegacyConfig({
27
+ encryptionKey: params.encryptionKey,
28
+ legacyConfigName: params.legacyConfigName,
29
+ legacyCwd: params.legacyCwd,
30
+ projectName: params.projectName
31
+ });
32
+ if (!legacyConfig) {
33
+ return;
34
+ }
35
+ const legacyTokens = safeGet(legacyConfig, 'tokens');
36
+ if (!legacyTokens) {
37
+ return;
38
+ }
39
+ // Ensure target directory exists
40
+ if (!fsSync.existsSync(params.newOAuthCwd)) {
41
+ fsSync.mkdirSync(params.newOAuthCwd, {
42
+ recursive: true
43
+ });
44
+ }
45
+ const newOAuthConfig = new Conf({
46
+ configName: params.newOAuthConfigName,
47
+ cwd: params.newOAuthCwd,
48
+ encryptionKey: params.encryptionKey,
49
+ projectName: params.projectName
50
+ });
51
+ // Write root-level keys
52
+ newOAuthConfig.set('_schemaVersion', OAUTH_STORE_SCHEMA_VERSION);
53
+ newOAuthConfig.set('accessToken', legacyTokens.accessToken);
54
+ newOAuthConfig.set('expiresAt', legacyTokens.expiresAt);
55
+ newOAuthConfig.set('refreshToken', legacyTokens.refreshToken);
56
+ if (legacyTokens.scopes) {
57
+ newOAuthConfig.set('scopes', legacyTokens.scopes);
58
+ }
59
+ if (legacyTokens.tokenType) {
60
+ newOAuthConfig.set('tokenType', legacyTokens.tokenType);
61
+ }
62
+ if (legacyTokens.userId) {
63
+ newOAuthConfig.set('userId', legacyTokens.userId);
64
+ }
65
+ // Leave the legacy file untouched so older CLI versions installed in other
66
+ // projects on the same machine can keep reading their last known state.
67
+ }
68
+ /**
69
+ * Migrate per-project bootstrap data and project tokens from the legacy
70
+ * combined store into individual per-project stores.
71
+ *
72
+ * Walks legacy `bootstrapData` (keyed by `projectId:environmentName`), writes
73
+ * each entry into its own project store file, and stashes the matching
74
+ * `projectToken` (looked up by `bootstrapData.contentSystemId`) alongside it.
75
+ * Target files that already exist are left alone.
76
+ */ export function migrateProjectData(params) {
77
+ const legacyConfig = openLegacyConfig({
78
+ encryptionKey: params.encryptionKey,
79
+ legacyConfigName: params.legacyConfigName,
80
+ legacyCwd: params.legacyCwd,
81
+ projectName: params.projectName
82
+ });
83
+ if (!legacyConfig) {
84
+ return;
85
+ }
86
+ const bootstrapData = safeGet(legacyConfig, 'bootstrapData') || {};
87
+ const projectTokens = safeGet(legacyConfig, 'projectTokens') || {};
88
+ if (Object.keys(bootstrapData).length === 0 && Object.keys(projectTokens).length === 0) {
89
+ return;
90
+ }
91
+ for (const [key, bootstrap] of Object.entries(bootstrapData)){
92
+ const separatorIdx = key.indexOf(':');
93
+ if (separatorIdx === -1) {
94
+ continue;
95
+ }
96
+ const projectId = key.slice(0, separatorIdx);
97
+ const environmentName = key.slice(separatorIdx + 1);
98
+ if (!projectId || !environmentName) {
99
+ continue;
100
+ }
101
+ const { configName, cwd } = params.resolveProjectStorePath({
102
+ environmentName,
103
+ projectId
104
+ });
105
+ const targetPath = path.join(cwd, `${configName}.json`);
106
+ if (fsSync.existsSync(targetPath)) {
107
+ continue;
108
+ }
109
+ if (!fsSync.existsSync(cwd)) {
110
+ fsSync.mkdirSync(cwd, {
111
+ recursive: true
112
+ });
113
+ }
114
+ const projectConfig = new Conf({
115
+ configName,
116
+ cwd,
117
+ encryptionKey: params.encryptionKey,
118
+ projectName: params.projectName
119
+ });
120
+ projectConfig.set('_schemaVersion', PROJECT_STORE_SCHEMA_VERSION);
121
+ projectConfig.set('bootstrapData', bootstrap);
122
+ const legacyProjectToken = bootstrap.contentSystemId ? projectTokens[bootstrap.contentSystemId] : undefined;
123
+ if (legacyProjectToken) {
124
+ projectConfig.set('projectToken', legacyProjectToken);
125
+ }
126
+ }
127
+ // Leave the legacy file untouched so older CLI versions installed in other
128
+ // projects on the same machine can keep reading their last known state.
129
+ }
130
+ function safeGet(config, key) {
131
+ try {
132
+ return config.get(key);
133
+ } catch (error) {
134
+ log.warning(`Migration read failed (key: ${String(key)}). ` + `File: ${config.path}. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
135
+ return undefined;
136
+ }
137
+ }
138
+ /**
139
+ * Open the legacy Conf store without `clearInvalidConfig`, so a decrypt
140
+ * error does not destroy the file. Returns null when the file cannot be
141
+ * opened — callers treat that as "nothing to migrate, leave file alone".
142
+ */ function openLegacyConfig(params) {
143
+ try {
144
+ return new Conf({
145
+ configName: params.legacyConfigName,
146
+ cwd: params.legacyCwd,
147
+ encryptionKey: params.encryptionKey,
148
+ projectName: params.projectName
149
+ });
150
+ } catch (error) {
151
+ log.warning(`Legacy token store could not be opened; skipping migration and leaving file intact. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
152
+ return null;
153
+ }
154
+ }
155
+
156
+ //# sourceMappingURL=token-store-migration.js.map