@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.
- package/dist/auth/callback-server.d.ts +19 -7
- package/dist/auth/callback-server.js +72 -31
- package/dist/auth/crypto-utils.d.ts +11 -0
- package/dist/auth/crypto-utils.js +22 -1
- package/dist/auth/oauth-flow.d.ts +3 -1
- package/dist/auth/oauth-flow.js +13 -6
- package/dist/auth/project-token.js +2 -2
- package/dist/auth/token-store.d.ts +2 -0
- package/dist/auth/token-store.js +43 -3
- package/dist/auth/types.d.ts +7 -0
- package/dist/cli.js +6 -1
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +44 -2
- package/dist/config/oauth.d.ts +9 -1
- package/dist/config/oauth.js +13 -1
- package/dist/db-content-api/generated/content-api-types.d.ts +6 -0
- package/dist/db-content-api/index.d.ts +2 -0
- package/dist/db-content-api/index.js +9 -1
- package/dist/lib/download-skill.d.ts +13 -0
- package/dist/lib/download-skill.js +79 -0
- package/dist/oauth/endpoints/getLoginEndpoint.js +26 -107
- package/dist/oauth/endpoints/getTokenLoginEndpoint.d.ts +17 -0
- package/dist/oauth/endpoints/getTokenLoginEndpoint.js +105 -0
- package/dist/oauth/index.js +8 -0
- package/dist/oauth/utilities/establishSession.d.ts +23 -0
- package/dist/oauth/utilities/establishSession.js +82 -0
- package/dist/oauth/utilities/exchangeCodeForAccessToken.d.ts +24 -0
- package/dist/oauth/utilities/exchangeCodeForAccessToken.js +28 -0
- package/dist/oauth/utilities/isAbsoluteURL.d.ts +2 -0
- package/dist/oauth/utilities/isAbsoluteURL.js +3 -0
- package/dist/plugin/build-config.js +3 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/download-template.d.ts +9 -1
- package/dist/utils/download-template.js +24 -19
- package/dist/utils/messages.js +2 -0
- package/dist/utils/parse-template-spec.d.ts +12 -0
- package/dist/utils/parse-template-spec.js +62 -0
- package/dist/utils/project.d.ts +2 -1
- package/dist/utils/project.js +2 -2
- package/package.json +9 -1
- 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
|
-
*
|
|
24
|
-
* OS-assigned port
|
|
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
|
|
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
|
-
*
|
|
24
|
-
* OS-assigned port
|
|
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
|
-
|
|
27
|
+
callbackPorts;
|
|
28
|
+
readyPromise = null;
|
|
29
|
+
rejectReady;
|
|
30
|
+
resolveReady;
|
|
28
31
|
server = null;
|
|
29
32
|
timeoutMs;
|
|
30
33
|
constructor(options = {}){
|
|
31
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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
|
|
139
|
-
const tryListen = (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 =
|
|
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
|
|
164
|
-
tryListen(
|
|
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
|
|
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;
|
package/dist/auth/oauth-flow.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
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.
|
|
74
|
+
const actualPort = await callbackServer.waitUntilReady();
|
|
73
75
|
// Build redirect URI with actual port
|
|
74
|
-
const actualRedirectUri = redirectUri
|
|
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 {
|
|
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 =
|
|
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
|
package/dist/auth/token-store.js
CHANGED
|
@@ -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 =
|
|
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 {
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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':
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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, {
|
package/dist/config/oauth.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OAuthConfig } from '../auth/types.js';
|
|
2
|
-
export declare const
|
|
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
|
package/dist/config/oauth.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { getEnvConfig, getInfraEnvironment } from '../constants.js';
|
|
2
|
-
export const
|
|
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
|