@payloadcms/figma 0.0.1-alpha.64 → 0.0.1-alpha.66

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 (41) hide show
  1. package/dist/auth/callback-server.d.ts +19 -7
  2. package/dist/auth/callback-server.js +72 -31
  3. package/dist/auth/crypto-utils.d.ts +11 -0
  4. package/dist/auth/crypto-utils.js +22 -1
  5. package/dist/auth/oauth-flow.d.ts +3 -1
  6. package/dist/auth/oauth-flow.js +13 -6
  7. package/dist/auth/project-token.js +2 -2
  8. package/dist/auth/token-store.d.ts +2 -0
  9. package/dist/auth/token-store.js +43 -3
  10. package/dist/auth/types.d.ts +7 -0
  11. package/dist/cli.js +6 -1
  12. package/dist/commands/init.d.ts +4 -0
  13. package/dist/commands/init.js +44 -2
  14. package/dist/config/oauth.d.ts +9 -1
  15. package/dist/config/oauth.js +13 -1
  16. package/dist/db-content-api/generated/content-api-types.d.ts +6 -0
  17. package/dist/db-content-api/index.d.ts +2 -0
  18. package/dist/db-content-api/index.js +9 -1
  19. package/dist/lib/download-skill.d.ts +13 -0
  20. package/dist/lib/download-skill.js +79 -0
  21. package/dist/oauth/endpoints/getLoginEndpoint.js +26 -107
  22. package/dist/oauth/endpoints/getTokenLoginEndpoint.d.ts +17 -0
  23. package/dist/oauth/endpoints/getTokenLoginEndpoint.js +105 -0
  24. package/dist/oauth/index.js +8 -0
  25. package/dist/oauth/utilities/establishSession.d.ts +23 -0
  26. package/dist/oauth/utilities/establishSession.js +82 -0
  27. package/dist/oauth/utilities/exchangeCodeForAccessToken.d.ts +24 -0
  28. package/dist/oauth/utilities/exchangeCodeForAccessToken.js +28 -0
  29. package/dist/oauth/utilities/isAbsoluteURL.d.ts +2 -0
  30. package/dist/oauth/utilities/isAbsoluteURL.js +3 -0
  31. package/dist/plugin/build-config.js +3 -0
  32. package/dist/types.d.ts +2 -0
  33. package/dist/utils/download-template.d.ts +9 -1
  34. package/dist/utils/download-template.js +24 -19
  35. package/dist/utils/messages.js +2 -0
  36. package/dist/utils/parse-template-spec.d.ts +12 -0
  37. package/dist/utils/parse-template-spec.js +62 -0
  38. package/dist/utils/project.d.ts +2 -1
  39. package/dist/utils/project.js +2 -2
  40. package/package.json +9 -1
  41. package/dist/db-content-api/README.md +0 -98
@@ -7,6 +7,14 @@ export interface CallbackResult {
7
7
  /** State parameter for CSRF verification */
8
8
  state: string;
9
9
  }
10
+ export type CallbackServerOptions = {
11
+ /** Additional callback ports to try after port. */
12
+ fallbackPorts?: number[];
13
+ /** First callback port to try. Defaults to the configured callback port range. */
14
+ port?: number;
15
+ /** Callback timeout in milliseconds. */
16
+ timeoutMs?: number;
17
+ };
10
18
  /**
11
19
  * Error from OAuth callback
12
20
  */
@@ -20,18 +28,18 @@ export declare class CallbackError extends Error {
20
28
  * Starts a temporary server on localhost to receive the OAuth authorization code.
21
29
  * The server automatically shuts down after receiving the callback or on timeout.
22
30
  *
23
- * Implements dynamic port allocation: tries preferred port first, falls back to
24
- * OS-assigned port if unavailable.
31
+ * Tries the registered localhost callback ports in order. It does not use an
32
+ * OS-assigned random port because OAuth redirect URIs must be registered.
25
33
  */
26
34
  export declare class CallbackServer {
27
35
  private actualPort?;
28
- private preferredPort;
36
+ private callbackPorts;
37
+ private readyPromise;
38
+ private rejectReady?;
39
+ private resolveReady?;
29
40
  private server;
30
41
  private timeoutMs;
31
- constructor(options?: {
32
- port?: number;
33
- timeoutMs?: number;
34
- });
42
+ constructor(options?: CallbackServerOptions);
35
43
  /**
36
44
  * Render error page shown in browser when auth fails
37
45
  */
@@ -57,5 +65,9 @@ export declare class CallbackServer {
57
65
  * @throws CallbackError if callback fails or times out
58
66
  */
59
67
  waitForCallback(expectedState: string): Promise<CallbackResult>;
68
+ /**
69
+ * Wait until the callback server is listening.
70
+ */
71
+ waitUntilReady(): Promise<number>;
60
72
  }
61
73
  //# sourceMappingURL=callback-server.d.ts.map
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs';
2
2
  import { createServer } from 'http';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
- import { DEFAULT_CALLBACK_PORT } from '../config/oauth.js';
5
+ import { DEFAULT_CALLBACK_PORT, DEFAULT_CALLBACK_PORTS } from '../config/oauth.js';
6
6
  const filename = fileURLToPath(import.meta.url);
7
7
  const dirname = path.dirname(filename);
8
8
  /**
@@ -20,17 +20,19 @@ const dirname = path.dirname(filename);
20
20
  * Starts a temporary server on localhost to receive the OAuth authorization code.
21
21
  * The server automatically shuts down after receiving the callback or on timeout.
22
22
  *
23
- * Implements dynamic port allocation: tries preferred port first, falls back to
24
- * OS-assigned port if unavailable.
23
+ * Tries the registered localhost callback ports in order. It does not use an
24
+ * OS-assigned random port because OAuth redirect URIs must be registered.
25
25
  */ export class CallbackServer {
26
26
  actualPort;
27
- preferredPort;
27
+ callbackPorts;
28
+ readyPromise = null;
29
+ rejectReady;
30
+ resolveReady;
28
31
  server = null;
29
32
  timeoutMs;
30
33
  constructor(options = {}){
31
- this.preferredPort = options.port || DEFAULT_CALLBACK_PORT;
34
+ this.callbackPorts = resolveCallbackPorts(options);
32
35
  this.timeoutMs = options.timeoutMs || 120000; // 2 minutes default
33
- this.server = createServer();
34
36
  }
35
37
  /**
36
38
  * Render error page shown in browser when auth fails
@@ -47,7 +49,7 @@ const dirname = path.dirname(filename);
47
49
  * Get the actual port the server is listening on
48
50
  * @returns Port number (may differ from preferred if that was in use)
49
51
  */ getActualPort() {
50
- return this.actualPort || this.preferredPort;
52
+ return this.actualPort || this.callbackPorts[0];
51
53
  }
52
54
  /**
53
55
  * Stop the server if running
@@ -65,6 +67,17 @@ const dirname = path.dirname(filename);
65
67
  * @throws CallbackError if callback fails or times out
66
68
  */ async waitForCallback(expectedState) {
67
69
  return new Promise((resolve, reject)=>{
70
+ if (this.server) {
71
+ reject(new CallbackError('Callback server is already running', 'server_error'));
72
+ return;
73
+ }
74
+ this.readyPromise = new Promise((resolveReady, rejectReady)=>{
75
+ this.resolveReady = resolveReady;
76
+ this.rejectReady = rejectReady;
77
+ });
78
+ this.readyPromise.catch(()=>{
79
+ // The caller may only await waitForCallback(); avoid an unhandled rejection.
80
+ });
68
81
  const cleanup = (handle)=>{
69
82
  clearTimeout(handle);
70
83
  if (this.server) {
@@ -77,8 +90,7 @@ const dirname = path.dirname(filename);
77
90
  cleanup(timeoutHandle);
78
91
  reject(new CallbackError('OAuth authorization timed out. Please try again.', 'timeout'));
79
92
  }, this.timeoutMs);
80
- // Create HTTP server
81
- this.server = createServer((req, res)=>{
93
+ const handleRequest = (req, res)=>{
82
94
  const url = new URL(req.url || '', `http://localhost:${this.getActualPort()}`);
83
95
  // Only handle /callback path
84
96
  if (url.pathname !== '/callback') {
@@ -134,36 +146,65 @@ const dirname = path.dirname(filename);
134
146
  code,
135
147
  state
136
148
  });
137
- });
138
- // Try to start server with dynamic port allocation
139
- const tryListen = (port, isRetry = false)=>{
140
- if (!this.server) {
141
- reject(new CallbackError('Server instance is not available', 'server_error'));
142
- return;
143
- }
144
- this.server.listen(port, '127.0.0.1', ()=>{
149
+ };
150
+ // Try registered callback ports in order.
151
+ const tryListen = (portIndex)=>{
152
+ const port = this.callbackPorts[portIndex];
153
+ const server = createServer(handleRequest);
154
+ this.server = server;
155
+ server.once('error', (err)=>{
156
+ if (this.server === server) {
157
+ this.server = null;
158
+ }
159
+ if (err.code === 'EADDRINUSE' && portIndex < this.callbackPorts.length - 1) {
160
+ tryListen(portIndex + 1);
161
+ return;
162
+ }
163
+ cleanup(timeoutHandle);
164
+ const callbackError = err.code === 'EADDRINUSE' ? new CallbackError(`OAuth callback ports are all in use: ${this.callbackPorts.join(', ')}. Close the process using one of these ports and run login again.`, 'ports_in_use') : new CallbackError(`Failed to start callback server: ${err.message}`, 'server_error');
165
+ this.rejectReady?.(callbackError);
166
+ reject(callbackError);
167
+ });
168
+ server.listen(port, '127.0.0.1', ()=>{
169
+ if (this.server !== server) {
170
+ return;
171
+ }
145
172
  // Capture the actual port that was assigned
146
- const addr = this.server.address();
173
+ const addr = server.address();
147
174
  if (addr && typeof addr === 'object') {
148
175
  this.actualPort = addr.port;
149
- }
150
- });
151
- // Handle server errors (port in use, etc)
152
- this.server.once('error', (err)=>{
153
- if (err.code === 'EADDRINUSE' && !isRetry) {
154
- // Port in use, try with OS-assigned port (0 = dynamic)
155
- this.server.removeAllListeners();
156
- tryListen(0, true);
157
- } else {
158
- cleanup(timeoutHandle);
159
- reject(new CallbackError(`Failed to start callback server: ${err.message}${!isRetry ? ' (tried fallback port)' : ''}`));
176
+ this.resolveReady?.(addr.port);
160
177
  }
161
178
  });
162
179
  };
163
- // Start with preferred port
164
- tryListen(this.preferredPort);
180
+ // Start with first configured port
181
+ tryListen(0);
165
182
  });
166
183
  }
184
+ /**
185
+ * Wait until the callback server is listening.
186
+ */ waitUntilReady() {
187
+ if (!this.readyPromise) {
188
+ return Promise.reject(new CallbackError('Callback server has not started', 'server_error'));
189
+ }
190
+ return this.readyPromise;
191
+ }
192
+ }
193
+ function resolveCallbackPorts(options) {
194
+ if (!options.port && !options.fallbackPorts) {
195
+ return DEFAULT_CALLBACK_PORTS;
196
+ }
197
+ const primaryPort = options.port || DEFAULT_CALLBACK_PORT;
198
+ const ports = uniquePorts([
199
+ primaryPort,
200
+ ...options.fallbackPorts || []
201
+ ]).slice(0, DEFAULT_CALLBACK_PORTS.length);
202
+ return ports.length > 0 ? ports : DEFAULT_CALLBACK_PORTS;
203
+ }
204
+ function uniquePorts(ports) {
205
+ return [
206
+ ...new Set(ports)
207
+ ];
167
208
  }
168
209
 
169
210
  //# sourceMappingURL=callback-server.js.map
@@ -30,6 +30,17 @@
30
30
  * @throws Error if unable to derive key due to missing machine information
31
31
  */
32
32
  export declare function deriveEncryptionKey(): string;
33
+ /**
34
+ * @deprecated Exists solely to read token stores written by older versions of
35
+ * this CLI that mixed `os.hostname()` into the encryption-key derivation. macOS
36
+ * rewrites the kernel hostname across DHCP/VPN/sleep events, which made those
37
+ * stores periodically un-decryptable. Used only by the transparent-migration
38
+ * path in `token-store.ts` and should be removed in a future release once
39
+ * upgraded clients are sufficiently common.
40
+ *
41
+ * @returns 64-character hex string (32-byte key) suitable for AES-256
42
+ */
43
+ export declare function deriveLegacyEncryptionKey(): string;
33
44
  /**
34
45
  * Get a short hash of the encryption key for diagnostics.
35
46
  * Returns first 8 chars of SHA-256 hash — enough to compare across invocations
@@ -36,7 +36,6 @@ import path from 'path';
36
36
  */ function gatherMachineEntropy() {
37
37
  try {
38
38
  const entropy = [
39
- os.hostname(),
40
39
  os.homedir(),
41
40
  os.userInfo().username,
42
41
  os.platform(),
@@ -88,6 +87,28 @@ import path from 'path';
88
87
  const key = crypto.pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
89
88
  return key.toString('hex');
90
89
  }
90
+ /**
91
+ * @deprecated Exists solely to read token stores written by older versions of
92
+ * this CLI that mixed `os.hostname()` into the encryption-key derivation. macOS
93
+ * rewrites the kernel hostname across DHCP/VPN/sleep events, which made those
94
+ * stores periodically un-decryptable. Used only by the transparent-migration
95
+ * path in `token-store.ts` and should be removed in a future release once
96
+ * upgraded clients are sufficiently common.
97
+ *
98
+ * @returns 64-character hex string (32-byte key) suitable for AES-256
99
+ */ export function deriveLegacyEncryptionKey() {
100
+ const entropy = [
101
+ os.hostname(),
102
+ os.homedir(),
103
+ os.userInfo().username,
104
+ os.platform(),
105
+ os.arch()
106
+ ];
107
+ const machineId = entropy.join('::');
108
+ const salt = getOrCreateSalt();
109
+ const key = crypto.pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
110
+ return key.toString('hex');
111
+ }
91
112
  /**
92
113
  * Get a short hash of the encryption key for diagnostics.
93
114
  * Returns first 8 chars of SHA-256 hash — enough to compare across invocations
@@ -14,7 +14,9 @@ export declare class OAuthFlowError extends Error {
14
14
  export interface OAuthFlowOptions {
15
15
  /** OAuth2 client ID (defaults to config) */
16
16
  clientId?: string;
17
- /** Callback server port (defaults to DEFAULT_CALLBACK_PORT) */
17
+ /** Callback server fallback ports to try after port */
18
+ fallbackPorts?: number[];
19
+ /** First callback server port to try */
18
20
  port?: number;
19
21
  /** Redirect URI (defaults to config) */
20
22
  redirectUri?: string;
@@ -1,6 +1,6 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import crypto from 'crypto';
3
- import { DEFAULT_CALLBACK_PORT, getOAuthConfig } from '../config/oauth.js';
3
+ import { getOAuthConfig } from '../config/oauth.js';
4
4
  import * as log from '../utils/log.js';
5
5
  import { buildAuthorizationUrl, openBrowser } from './browser.js';
6
6
  import { CallbackServer } from './callback-server.js';
@@ -53,25 +53,27 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
53
53
  };
54
54
  }
55
55
  // REAL OAUTH IMPLEMENTATION
56
- const { clientId = oauthConfig.clientId, port = DEFAULT_CALLBACK_PORT, redirectUri = oauthConfig.redirectUri, scopes = oauthConfig.scopes, timeoutMs = 120000 } = options;
56
+ const { clientId = oauthConfig.clientId, fallbackPorts, port, redirectUri = oauthConfig.redirectUri, scopes = oauthConfig.scopes, timeoutMs = 120000 } = options;
57
57
  // Generate PKCE pair for enhanced security
58
58
  const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair();
59
59
  // Generate random state for CSRF protection
60
60
  const state = crypto.randomBytes(32).toString('base64url');
61
61
  // Create callback server
62
62
  const callbackServer = new CallbackServer({
63
+ fallbackPorts,
63
64
  port,
64
65
  timeoutMs
65
66
  });
66
67
  try {
67
68
  // Start waiting for callback (this starts the server and captures actual port)
68
69
  const callbackPromise = callbackServer.waitForCallback(state);
69
- // Wait a bit for server to start and determine actual port
70
- await new Promise((resolve)=>setTimeout(resolve, 100));
70
+ callbackPromise.catch(()=>{
71
+ // The callback promise is awaited below; avoid an unhandled rejection if startup fails early.
72
+ });
71
73
  // Get the actual port (may differ from preferred if that was in use)
72
- const actualPort = callbackServer.getActualPort();
74
+ const actualPort = await callbackServer.waitUntilReady();
73
75
  // Build redirect URI with actual port
74
- const actualRedirectUri = redirectUri.replace(/:\d+/, `:${actualPort}`);
76
+ const actualRedirectUri = replaceRedirectUriPort(redirectUri, actualPort);
75
77
  // Build authorization URL
76
78
  const authUrl = buildAuthorizationUrl({
77
79
  authorizationUrl: oauthConfig.authorizationUrl,
@@ -213,5 +215,10 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
213
215
  return null;
214
216
  }
215
217
  }
218
+ function replaceRedirectUriPort(redirectUri, port) {
219
+ const url = new URL(redirectUri);
220
+ url.port = String(port);
221
+ return url.toString();
222
+ }
216
223
 
217
224
  //# sourceMappingURL=oauth-flow.js.map
@@ -5,7 +5,7 @@
5
5
  * to the Content API. Project tokens are scoped to a specific tenant/CMS
6
6
  * and have a shorter lifespan (15-30 minutes) than OAuth tokens.
7
7
  */ import { getProjectToken as fetchProjectToken } from '../api/figma-api.js';
8
- import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
8
+ import { PROJECT_TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
9
9
  import { getInfraEnvironment } from '../constants.js';
10
10
  import * as log from '../utils/log.js';
11
11
  import { JWTValidationError, validateProjectToken } from './jwt-validator.js';
@@ -27,7 +27,7 @@ function buildCacheKey(params) {
27
27
  return `${params.tenantId}::${projectId}::${environmentName}`;
28
28
  }
29
29
  function isCachedTokenValid(entry) {
30
- const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
30
+ const bufferMs = PROJECT_TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
31
31
  return Date.now() < entry.expiresAt - bufferMs;
32
32
  }
33
33
  /** Test-only: clears the in-memory token cache and any in-flight refresh promises. */ export function __resetProjectTokenCacheForTests() {
@@ -12,6 +12,7 @@ export declare function getTokenStore(environment?: Environment): TokenStore;
12
12
  */
13
13
  export declare class TokenStore {
14
14
  private baseConfigName;
15
+ private deriveLegacyKey;
15
16
  private encryptionKey;
16
17
  private environment;
17
18
  private legacyRootDir;
@@ -69,6 +70,7 @@ export declare class TokenStore {
69
70
  private getLegacyRootDir;
70
71
  private getProjectStore;
71
72
  private createConfig;
73
+ private tryMigrateLegacyKey;
72
74
  private safeGet;
73
75
  }
74
76
  //# sourceMappingURL=token-store.d.ts.map
@@ -2,11 +2,11 @@
2
2
  import envPaths from 'env-paths';
3
3
  import fsSync from 'node:fs';
4
4
  import path from 'node:path';
5
- import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
5
+ import { PROJECT_TOKEN_EXPIRY_BUFFER_SECONDS, TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
6
6
  import { getInfraEnvironment } from '../constants.js';
7
7
  import { getEnvVarSync } from '../utils/env-management.js';
8
8
  import * as log from '../utils/log.js';
9
- import { deriveEncryptionKey } from './crypto-utils.js';
9
+ import { deriveEncryptionKey, deriveLegacyEncryptionKey } from './crypto-utils.js';
10
10
  import { migrateOAuth, migrateProjectData, needsMigration } from './token-store-migration.js';
11
11
  /**
12
12
  * Environment-keyed instances for singleton pattern.
@@ -30,6 +30,7 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
30
30
  * The constructor is still exported for test isolation.
31
31
  */ export class TokenStore {
32
32
  baseConfigName;
33
+ deriveLegacyKey;
33
34
  encryptionKey;
34
35
  environment;
35
36
  legacyRootDir;
@@ -45,6 +46,7 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
45
46
  const encryptionKey = options?.encryptionKey ?? deriveEncryptionKey();
46
47
  const baseConfigName = options?.configName || projectName;
47
48
  this.baseConfigName = baseConfigName;
49
+ this.deriveLegacyKey = options?.deriveLegacyKey ?? deriveLegacyEncryptionKey;
48
50
  this.encryptionKey = encryptionKey;
49
51
  this.environment = environment;
50
52
  this.projectName = projectName;
@@ -274,7 +276,7 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
274
276
  }
275
277
  isProjectTokenExpired(token) {
276
278
  const now = Date.now();
277
- const bufferMs = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
279
+ const bufferMs = PROJECT_TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
278
280
  const expiryWithBuffer = token.expiresAt - bufferMs;
279
281
  return now >= expiryWithBuffer;
280
282
  }
@@ -396,6 +398,13 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
396
398
  try {
397
399
  return new Conf(confOptions);
398
400
  } catch (error) {
401
+ const migrated = this.tryMigrateLegacyKey(confOptions);
402
+ if (migrated) {
403
+ if (this.environment === 'staging') {
404
+ log.info('Successfully migrated local token cache.');
405
+ }
406
+ return migrated;
407
+ }
399
408
  // Deserialization failed (e.g. encryption key changed).
400
409
  // Log diagnostics, then clear and replace with a fresh store.
401
410
  if (this.environment === 'staging') {
@@ -412,6 +421,37 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
412
421
  return config;
413
422
  }
414
423
  }
424
+ tryMigrateLegacyKey(confOptions) {
425
+ if (!confOptions.encryptionKey) {
426
+ return null;
427
+ }
428
+ let snapshot;
429
+ try {
430
+ const legacyConf = new Conf({
431
+ ...confOptions,
432
+ clearInvalidConfig: false,
433
+ encryptionKey: this.deriveLegacyKey()
434
+ });
435
+ snapshot = {
436
+ ...legacyConf.store
437
+ };
438
+ } catch {
439
+ return null;
440
+ }
441
+ try {
442
+ const rewritten = new Conf({
443
+ ...confOptions,
444
+ clearInvalidConfig: true
445
+ });
446
+ rewritten.clear();
447
+ for (const [key, value] of Object.entries(snapshot)){
448
+ rewritten.set(key, value);
449
+ }
450
+ return rewritten;
451
+ } catch {
452
+ return null;
453
+ }
454
+ }
415
455
  safeGet(params) {
416
456
  const { config, key } = params;
417
457
  try {
@@ -109,6 +109,13 @@ export interface OAuthTokenRefreshParams {
109
109
  export type TokenStoreConfig = {
110
110
  /** Config file name (for testing) */
111
111
  configName?: string;
112
+ /**
113
+ * Override for the legacy key derivation used by transparent migration.
114
+ * Tests pass an explicit function so they don't depend on the host machine's
115
+ * actual entropy. Production code leaves this undefined and the constructor
116
+ * falls back to `deriveLegacyEncryptionKey`.
117
+ */
118
+ deriveLegacyKey?: () => string;
112
119
  /** Optional override for storage encryption key (primarily for tests). */
113
120
  encryptionKey?: string;
114
121
  /** Environment for config file naming */
package/dist/cli.js CHANGED
@@ -45,8 +45,10 @@ class Main {
45
45
  '--infra-env': String,
46
46
  '--json': Boolean,
47
47
  '--name': String,
48
+ '--no-skill': Boolean,
48
49
  '--skip-auth': Boolean,
49
50
  '--skip-build': Boolean,
51
+ '--template': String,
50
52
  '--version': Boolean,
51
53
  '--yes': Boolean,
52
54
  // Aliases
@@ -55,6 +57,7 @@ class Main {
55
57
  '-f': '--force',
56
58
  '-h': '--help',
57
59
  '-n': '--name',
60
+ '-t': '--template',
58
61
  '-v': '--version',
59
62
  '-y': '--yes'
60
63
  }, {
@@ -146,7 +149,9 @@ class Main {
146
149
  debug: this.args['--debug'],
147
150
  env: this.args['--env'],
148
151
  force: this.args['--force'],
149
- skipAuth: this.args['--skip-auth']
152
+ noSkill: this.args['--no-skill'],
153
+ skipAuth: this.args['--skip-auth'],
154
+ template: this.args['--template']
150
155
  });
151
156
  break;
152
157
  case 'list-tokens':
@@ -12,8 +12,12 @@ export interface InitCommandOptions {
12
12
  id?: string;
13
13
  /** Project name (for scaffolding) */
14
14
  name?: string;
15
+ /** Skip Payload skill installation */
16
+ noSkill?: boolean;
15
17
  /** Skip authentication check (for testing/development) */
16
18
  skipAuth?: boolean;
19
+ /** GitHub template override, e.g. "v3.80.0:website" or "owner/repo#ref:template-path" */
20
+ template?: string;
17
21
  /** Skip prompts and use defaults where possible */
18
22
  yes?: boolean;
19
23
  }
@@ -8,6 +8,7 @@ import { tryGetCredential } from '../auth/oauth-flow.js';
8
8
  import { getValidProjectToken, ProjectTokenError } from '../auth/project-token.js';
9
9
  import { getTokenStore } from '../auth/token-store.js';
10
10
  import { getInfraEnvironment, getProjectNotFoundMessage } from '../constants.js';
11
+ import { downloadSkill } from '../lib/download-skill.js';
11
12
  import { ensureGitignore } from '../utils/config.js';
12
13
  import { addOrUpdateEnvVar } from '../utils/env-management.js';
13
14
  import { isDebug } from '../utils/is-debug.js';
@@ -21,6 +22,23 @@ import { resolveEnvironment } from '../utils/resolve-environment.js';
21
22
  import { checkForUpdates, getOwnVersion } from '../utils/version-check.js';
22
23
  import { loginCommand } from './login.js';
23
24
  import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
25
+ import { DEFAULT_TEMPLATE_SOURCE } from '../utils/download-template.js';
26
+ import { parseTemplateSpec, TemplateSpecParseError } from '../utils/parse-template-spec.js';
27
+ async function maybeInstallPayloadSkill(args) {
28
+ if (args.noSkill) {
29
+ return;
30
+ }
31
+ const result = await downloadSkill({
32
+ projectDir: args.projectDir
33
+ });
34
+ if (result.ok) {
35
+ return;
36
+ }
37
+ if (result.reason === 'already-installed') {
38
+ return;
39
+ }
40
+ p.log.warn(pc.yellow(`⚠ Could not install Payload skill (${result.reason}${result.detail ? `: ${result.detail}` : ''}). Continuing.`));
41
+ }
24
42
  /**
25
43
  * Generate and store project token for a content system
26
44
  * Non-blocking - will show warning but not exit on failure
@@ -111,6 +129,18 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
111
129
  process.exit(1);
112
130
  }
113
131
  const cmsResourceId = options.id;
132
+ // Parse --template spec early so malformed values abort before any I/O
133
+ let templateSource;
134
+ if (options.template) {
135
+ try {
136
+ templateSource = parseTemplateSpec(options.template);
137
+ } catch (error) {
138
+ const message = error instanceof TemplateSpecParseError ? error.message : 'Invalid --template value';
139
+ p.log.error(pc.red(`✗ ${message}`));
140
+ p.note('Format: [<owner>/<repo>#]<ref>:<template-path>\n' + 'Examples:\n' + ' blank\n' + ' v3.80.0:website\n' + ' myorg/payload-fork#feat-x:templates/custom', 'Template spec');
141
+ process.exit(1);
142
+ }
143
+ }
114
144
  // Fetch bootstrap info (oauth creds + environments)
115
145
  let bootstrapInfo = null;
116
146
  let resolvedEnv = null;
@@ -250,6 +280,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
250
280
  '.env.local',
251
281
  'lambda.zip'
252
282
  ]);
283
+ await maybeInstallPayloadSkill({
284
+ noSkill: options.noSkill,
285
+ projectDir: process.cwd()
286
+ });
253
287
  // Generate project token (unless skipping auth)
254
288
  if (!options.skipAuth && resolvedEnv) {
255
289
  await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s, {
@@ -301,9 +335,13 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
301
335
  process.exit(1);
302
336
  }
303
337
  // Scaffold project
304
- s.start('Downloading template from GitHub...');
338
+ const resolvedSource = {
339
+ ...DEFAULT_TEMPLATE_SOURCE,
340
+ ...templateSource
341
+ };
342
+ s.start(`Downloading template "${resolvedSource.templatePath}" from ` + `${resolvedSource.owner}/${resolvedSource.repo}@${resolvedSource.ref}...`);
305
343
  try {
306
- await scaffoldProject(fullPath, projectName, packageManager);
344
+ await scaffoldProject(fullPath, projectName, packageManager, templateSource);
307
345
  s.stop(pc.green('✓ Template downloaded'));
308
346
  s.start('Installing dependencies...');
309
347
  await installDependencies(fullPath, packageManager);
@@ -371,6 +409,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
371
409
  if (!importMapResult) {
372
410
  p.log.warn(pc.yellow('⚠ Import map generation failed — it will be generated on first dev run'));
373
411
  }
412
+ await maybeInstallPayloadSkill({
413
+ noSkill: options.noSkill,
414
+ projectDir: fullPath
415
+ });
374
416
  // Generate project token (unless skipping auth)
375
417
  if (!options.skipAuth && resolvedEnv) {
376
418
  await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s, {
@@ -1,5 +1,6 @@
1
1
  import type { OAuthConfig } from '../auth/types.js';
2
- export declare const DEFAULT_CALLBACK_PORT = 34462;
2
+ export declare const DEFAULT_CALLBACK_PORTS: number[];
3
+ export declare const DEFAULT_CALLBACK_PORT: number;
3
4
  /**
4
5
  * Get OAuth configuration for current environment.
5
6
  *
@@ -14,4 +15,11 @@ export declare const DEFAULT_CALLBACK_PORT = 34462;
14
15
  */
15
16
  export declare function getOAuthConfig(): OAuthConfig;
16
17
  export declare const TOKEN_EXPIRY_BUFFER_SECONDS = 300;
18
+ /**
19
+ * Freshness buffer for project tokens. Figma's `pixie` issues 300-second project
20
+ * tokens, so reusing the 300-second OAuth buffer would treat every fresh token as
21
+ * already-expired and bypass the cache entirely. 30 seconds is well above the
22
+ * round-trip time for the calls these tokens authorize.
23
+ */
24
+ export declare const PROJECT_TOKEN_EXPIRY_BUFFER_SECONDS = 30;
17
25
  //# sourceMappingURL=oauth.d.ts.map
@@ -1,5 +1,11 @@
1
1
  import { getEnvConfig, getInfraEnvironment } from '../constants.js';
2
- export const DEFAULT_CALLBACK_PORT = 34462;
2
+ export const DEFAULT_CALLBACK_PORTS = [
3
+ 34462,
4
+ 34463,
5
+ 34464,
6
+ 34465
7
+ ];
8
+ export const DEFAULT_CALLBACK_PORT = DEFAULT_CALLBACK_PORTS[0];
3
9
  /**
4
10
  * Get OAuth configuration for current environment.
5
11
  *
@@ -31,5 +37,11 @@ export const DEFAULT_CALLBACK_PORT = 34462;
31
37
  };
32
38
  }
33
39
  export const TOKEN_EXPIRY_BUFFER_SECONDS = 300;
40
+ /**
41
+ * Freshness buffer for project tokens. Figma's `pixie` issues 300-second project
42
+ * tokens, so reusing the 300-second OAuth buffer would treat every fresh token as
43
+ * already-expired and bypass the cache entirely. 30 seconds is well above the
44
+ * round-trip time for the calls these tokens authorize.
45
+ */ export const PROJECT_TOKEN_EXPIRY_BUFFER_SECONDS = 30;
34
46
 
35
47
  //# sourceMappingURL=oauth.js.map