launchpd 1.0.3 → 1.0.6
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/LICENSE +21 -21
- package/README.md +61 -39
- package/bin/cli.js +149 -124
- package/bin/setup.js +42 -40
- package/package.json +4 -4
- package/src/commands/auth.js +516 -522
- package/src/commands/deploy.js +745 -386
- package/src/commands/index.js +14 -7
- package/src/commands/init.js +95 -72
- package/src/commands/list.js +120 -122
- package/src/commands/rollback.js +139 -102
- package/src/commands/status.js +75 -51
- package/src/commands/versions.js +153 -113
- package/src/config.js +32 -31
- package/src/utils/api.js +220 -195
- package/src/utils/credentials.js +88 -85
- package/src/utils/endpoint.js +58 -0
- package/src/utils/errors.js +79 -69
- package/src/utils/expiration.js +49 -47
- package/src/utils/id.js +5 -5
- package/src/utils/ignore.js +35 -36
- package/src/utils/index.js +10 -11
- package/src/utils/localConfig.js +39 -43
- package/src/utils/logger.js +113 -106
- package/src/utils/machineId.js +15 -19
- package/src/utils/metadata.js +113 -87
- package/src/utils/projectConfig.js +48 -45
- package/src/utils/prompt.js +91 -82
- package/src/utils/quota.js +261 -225
- package/src/utils/remoteSource.js +680 -0
- package/src/utils/upload.js +197 -127
- package/src/utils/validator.js +116 -68
package/src/utils/api.js
CHANGED
|
@@ -3,283 +3,308 @@
|
|
|
3
3
|
* Communicates with the Cloudflare Worker API endpoints
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { config } from '../config.js'
|
|
7
|
-
import { getApiKey, getApiSecret } from './credentials.js'
|
|
8
|
-
import { createHmac } from 'node:crypto'
|
|
9
|
-
import { getMachineId } from './machineId.js'
|
|
6
|
+
import { config } from '../config.js'
|
|
7
|
+
import { getApiKey, getApiSecret } from './credentials.js'
|
|
8
|
+
import { createHmac } from 'node:crypto'
|
|
9
|
+
import { getMachineId } from './machineId.js'
|
|
10
|
+
import { validateEndpoint } from './endpoint.js'
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from './errors.js'
|
|
12
|
+
APIError,
|
|
13
|
+
MaintenanceError,
|
|
14
|
+
AuthError,
|
|
15
|
+
NetworkError,
|
|
16
|
+
TwoFactorRequiredError
|
|
17
|
+
} from './errors.js'
|
|
17
18
|
|
|
18
|
-
const API_BASE_URL = config.apiUrl
|
|
19
|
+
const API_BASE_URL = config.apiUrl
|
|
20
|
+
|
|
21
|
+
/** Fetch timeout for LaunchPd API requests in milliseconds (30 seconds) */
|
|
22
|
+
export const API_TIMEOUT_MS = 30_000
|
|
19
23
|
|
|
20
24
|
// Re-export error classes for convenience
|
|
21
25
|
export {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from './errors.js'
|
|
26
|
+
APIError,
|
|
27
|
+
MaintenanceError,
|
|
28
|
+
AuthError,
|
|
29
|
+
NetworkError,
|
|
30
|
+
TwoFactorRequiredError
|
|
31
|
+
} from './errors.js'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create an AbortController with a timeout.
|
|
35
|
+
* @param {number} ms - Timeout in milliseconds
|
|
36
|
+
* @returns {{ signal: AbortSignal, clear: () => void }}
|
|
37
|
+
*/
|
|
38
|
+
export function createFetchTimeout (ms) {
|
|
39
|
+
const controller = new AbortController()
|
|
40
|
+
const timer = setTimeout(() => controller.abort(), ms)
|
|
41
|
+
return {
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
clear: () => clearTimeout(timer)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
28
46
|
|
|
29
47
|
/**
|
|
30
48
|
* Make an authenticated API request
|
|
31
49
|
*/
|
|
32
|
-
async function apiRequest(endpoint, options = {}) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
export async function apiRequest (endpoint, options = {}) {
|
|
51
|
+
validateEndpoint(endpoint)
|
|
52
|
+
const url = `${API_BASE_URL}${endpoint}`
|
|
53
|
+
|
|
54
|
+
const apiKey = await getApiKey()
|
|
55
|
+
const apiSecret = await getApiSecret()
|
|
56
|
+
const headers = {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'X-API-Key': apiKey,
|
|
59
|
+
'X-Device-Fingerprint': getMachineId(),
|
|
60
|
+
...options.headers
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add HMAC signature if secret is available
|
|
64
|
+
if (apiSecret) {
|
|
65
|
+
const timestamp = Date.now().toString()
|
|
66
|
+
const method = (options.method || 'GET').toUpperCase()
|
|
67
|
+
const body = options.body || ''
|
|
68
|
+
|
|
69
|
+
// HMAC-SHA256 for REQUEST SIGNING - this is NOT password hashing.
|
|
70
|
+
// The request body (which may contain passwords) is signed to authenticate
|
|
71
|
+
// the API request. Password hashing happens server-side using bcrypt/argon2.
|
|
72
|
+
// skipcq: JS-D003 - HMAC-SHA256 is appropriate for request signing
|
|
73
|
+
const hmac = createHmac('sha256', apiSecret)
|
|
74
|
+
hmac.update(method)
|
|
75
|
+
hmac.update(endpoint)
|
|
76
|
+
hmac.update(timestamp)
|
|
77
|
+
// skipcq: JS-D003 - Request body signing, not password storage
|
|
78
|
+
hmac.update(body)
|
|
79
|
+
|
|
80
|
+
const signature = hmac.digest('hex')
|
|
81
|
+
|
|
82
|
+
headers['X-Timestamp'] = timestamp
|
|
83
|
+
headers['X-Signature'] = signature
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { signal, clear } = createFetchTimeout(API_TIMEOUT_MS)
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(url, {
|
|
89
|
+
...options,
|
|
90
|
+
headers,
|
|
91
|
+
signal
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Handle maintenance mode (503 with maintenance_mode flag)
|
|
95
|
+
if (response.status === 503) {
|
|
96
|
+
const data = await response.json().catch(() => ({}))
|
|
97
|
+
if (data.maintenance_mode) {
|
|
98
|
+
throw new MaintenanceError(
|
|
99
|
+
data.message || 'LaunchPd is under maintenance'
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
throw new APIError(data.message || 'Service unavailable', 503, data)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Handle authentication errors
|
|
106
|
+
if (response.status === 401) {
|
|
107
|
+
const data = await response.json().catch(() => ({}))
|
|
108
|
+
// Check if 2FA is required (special case - not a real auth error)
|
|
109
|
+
if (data.requires_2fa) {
|
|
110
|
+
throw new TwoFactorRequiredError(data.two_factor_type, data.message)
|
|
111
|
+
}
|
|
112
|
+
throw new AuthError(data.message || 'Authentication failed', data)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle rate limiting / quota errors
|
|
116
|
+
if (response.status === 429) {
|
|
117
|
+
const data = await response.json().catch(() => ({}))
|
|
118
|
+
throw new APIError(data.message || 'Rate limit exceeded', 429, data)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const data = await response.json()
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new APIError(
|
|
125
|
+
data.error || data.message || `API error: ${response.status}`,
|
|
126
|
+
response.status,
|
|
127
|
+
data
|
|
128
|
+
)
|
|
61
129
|
}
|
|
62
130
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Handle maintenance mode (503 with maintenance_mode flag)
|
|
70
|
-
if (response.status === 503) {
|
|
71
|
-
const data = await response.json().catch(() => ({}));
|
|
72
|
-
if (data.maintenance_mode) {
|
|
73
|
-
throw new MaintenanceError(data.message || 'LaunchPd is under maintenance');
|
|
74
|
-
}
|
|
75
|
-
throw new APIError(data.message || 'Service unavailable', 503, data);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Handle authentication errors
|
|
79
|
-
if (response.status === 401) {
|
|
80
|
-
const data = await response.json().catch(() => ({}));
|
|
81
|
-
// Check if 2FA is required (special case - not a real auth error)
|
|
82
|
-
if (data.requires_2fa) {
|
|
83
|
-
throw new TwoFactorRequiredError(data.two_factor_type, data.message);
|
|
84
|
-
}
|
|
85
|
-
throw new AuthError(data.message || 'Authentication failed', data);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Handle rate limiting / quota errors
|
|
89
|
-
if (response.status === 429) {
|
|
90
|
-
const data = await response.json().catch(() => ({}));
|
|
91
|
-
throw new APIError(data.message || 'Rate limit exceeded', 429, data);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const data = await response.json();
|
|
95
|
-
|
|
96
|
-
if (!response.ok) {
|
|
97
|
-
throw new APIError(data.error || data.message || `API error: ${response.status}`, response.status, data);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return data;
|
|
101
|
-
} catch (err) {
|
|
102
|
-
// Re-throw our custom errors
|
|
103
|
-
if (err instanceof APIError || err instanceof NetworkError) {
|
|
104
|
-
throw err;
|
|
105
|
-
}
|
|
106
|
-
// If API is unavailable, throw NetworkError for consistent handling
|
|
107
|
-
if (err.message.includes('fetch failed') || err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED')) {
|
|
108
|
-
throw new NetworkError('Unable to connect to LaunchPd servers');
|
|
109
|
-
}
|
|
110
|
-
throw err;
|
|
131
|
+
return data
|
|
132
|
+
} catch (err) {
|
|
133
|
+
// Re-throw our custom errors
|
|
134
|
+
if (err instanceof APIError || err instanceof NetworkError) {
|
|
135
|
+
throw err
|
|
111
136
|
}
|
|
137
|
+
// Handle fetch timeout
|
|
138
|
+
if (err.name === 'AbortError') {
|
|
139
|
+
throw new NetworkError(
|
|
140
|
+
`Request timed out after ${API_TIMEOUT_MS / 1000}s. The server did not respond in time.`
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
// If API is unavailable, throw NetworkError for consistent handling
|
|
144
|
+
if (
|
|
145
|
+
err.message.includes('fetch failed') ||
|
|
146
|
+
err.message.includes('ENOTFOUND') ||
|
|
147
|
+
err.message.includes('ECONNREFUSED')
|
|
148
|
+
) {
|
|
149
|
+
throw new NetworkError('Unable to connect to LaunchPd servers')
|
|
150
|
+
}
|
|
151
|
+
throw err
|
|
152
|
+
} finally {
|
|
153
|
+
clear()
|
|
154
|
+
}
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
/**
|
|
115
158
|
* Get the next version number for a subdomain
|
|
116
159
|
*/
|
|
117
|
-
export async function getNextVersionFromAPI(subdomain) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const maxVersion = Math.max(...result.versions.map(v => v.version));
|
|
123
|
-
return maxVersion + 1;
|
|
160
|
+
export async function getNextVersionFromAPI (subdomain) {
|
|
161
|
+
const result = await apiRequest(`/api/versions/${subdomain}`)
|
|
162
|
+
if (!result?.versions?.length) return 1
|
|
163
|
+
const maxVersion = Math.max(...result.versions.map((v) => v.version))
|
|
164
|
+
return maxVersion + 1
|
|
124
165
|
}
|
|
125
166
|
|
|
126
167
|
/**
|
|
127
168
|
* Record a new deployment in the API
|
|
128
169
|
*/
|
|
129
|
-
export
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
export function recordDeployment (deploymentData) {
|
|
171
|
+
const {
|
|
172
|
+
subdomain,
|
|
173
|
+
folderName,
|
|
174
|
+
fileCount,
|
|
175
|
+
totalBytes,
|
|
176
|
+
version,
|
|
177
|
+
expiresAt,
|
|
178
|
+
message
|
|
179
|
+
} = deploymentData
|
|
180
|
+
|
|
181
|
+
return apiRequest('/api/deployments', {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
subdomain,
|
|
185
|
+
folderName,
|
|
186
|
+
fileCount,
|
|
187
|
+
totalBytes,
|
|
188
|
+
version,
|
|
189
|
+
cliVersion: config.version,
|
|
190
|
+
expiresAt,
|
|
191
|
+
message
|
|
192
|
+
})
|
|
193
|
+
})
|
|
145
194
|
}
|
|
146
195
|
|
|
147
196
|
/**
|
|
148
197
|
* Get list of user's deployments
|
|
149
198
|
*/
|
|
150
|
-
export
|
|
151
|
-
|
|
199
|
+
export function listDeployments (limit = 50, offset = 0) {
|
|
200
|
+
return apiRequest(`/api/deployments?limit=${limit}&offset=${offset}`)
|
|
152
201
|
}
|
|
153
202
|
|
|
154
203
|
/**
|
|
155
204
|
* Get deployment details for a subdomain
|
|
156
205
|
*/
|
|
157
|
-
export
|
|
158
|
-
|
|
206
|
+
export function getDeployment (subdomain) {
|
|
207
|
+
return apiRequest(`/api/deployments/${subdomain}`)
|
|
159
208
|
}
|
|
160
209
|
|
|
161
210
|
/**
|
|
162
211
|
* Get version history for a subdomain
|
|
163
212
|
*/
|
|
164
|
-
export
|
|
165
|
-
|
|
213
|
+
export function getVersions (subdomain) {
|
|
214
|
+
return apiRequest(`/api/versions/${subdomain}`)
|
|
166
215
|
}
|
|
167
216
|
|
|
168
217
|
/**
|
|
169
218
|
* Rollback to a specific version
|
|
170
219
|
*/
|
|
171
|
-
export
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
220
|
+
export function rollbackVersion (subdomain, version) {
|
|
221
|
+
return apiRequest(`/api/versions/${subdomain}/rollback`, {
|
|
222
|
+
method: 'PUT',
|
|
223
|
+
body: JSON.stringify({ version })
|
|
224
|
+
})
|
|
176
225
|
}
|
|
177
226
|
|
|
178
227
|
/**
|
|
179
228
|
* Check if a subdomain is available
|
|
180
229
|
*/
|
|
181
|
-
export async function checkSubdomainAvailable(subdomain) {
|
|
182
|
-
|
|
183
|
-
|
|
230
|
+
export async function checkSubdomainAvailable (subdomain) {
|
|
231
|
+
const result = await apiRequest(`/api/public/check/${subdomain}`)
|
|
232
|
+
return result?.available ?? true
|
|
184
233
|
}
|
|
185
234
|
|
|
186
235
|
/**
|
|
187
236
|
* Reserve a subdomain
|
|
188
237
|
*/
|
|
189
|
-
export
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
238
|
+
export function reserveSubdomain (subdomain) {
|
|
239
|
+
return apiRequest('/api/subdomains/reserve', {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
body: JSON.stringify({ subdomain })
|
|
242
|
+
})
|
|
194
243
|
}
|
|
195
244
|
|
|
196
245
|
/**
|
|
197
246
|
* Unreserve a subdomain
|
|
198
247
|
*/
|
|
199
|
-
export
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
248
|
+
export function unreserveSubdomain (subdomain) {
|
|
249
|
+
// Note: Admin only, but good to have in client client lib
|
|
250
|
+
return apiRequest(`/api/admin/reserve-subdomain/${subdomain}`, {
|
|
251
|
+
method: 'DELETE'
|
|
252
|
+
})
|
|
204
253
|
}
|
|
205
254
|
|
|
206
255
|
/**
|
|
207
256
|
* Get user's subdomains
|
|
208
257
|
*/
|
|
209
|
-
export
|
|
210
|
-
|
|
258
|
+
export function listSubdomains () {
|
|
259
|
+
return apiRequest('/api/subdomains')
|
|
211
260
|
}
|
|
212
261
|
|
|
213
262
|
/**
|
|
214
263
|
* Get current user info
|
|
215
264
|
*/
|
|
216
|
-
export
|
|
217
|
-
|
|
265
|
+
export function getCurrentUser () {
|
|
266
|
+
return apiRequest('/api/users/me')
|
|
218
267
|
}
|
|
219
268
|
|
|
220
269
|
/**
|
|
221
270
|
* Health check
|
|
222
271
|
*/
|
|
223
|
-
export
|
|
224
|
-
|
|
272
|
+
export function healthCheck () {
|
|
273
|
+
return apiRequest('/api/health')
|
|
225
274
|
}
|
|
226
275
|
|
|
227
276
|
/**
|
|
228
277
|
* Resend email verification
|
|
229
278
|
*/
|
|
230
|
-
export
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
279
|
+
export function resendVerification () {
|
|
280
|
+
return apiRequest('/api/auth/resend-verification', {
|
|
281
|
+
method: 'POST'
|
|
282
|
+
})
|
|
234
283
|
}
|
|
235
284
|
|
|
236
285
|
/**
|
|
237
286
|
* Regenerate API key
|
|
238
287
|
*/
|
|
239
|
-
export
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Change password
|
|
248
|
-
*/
|
|
249
|
-
export async function changePassword(currentPassword, newPassword, confirmPassword) {
|
|
250
|
-
return await apiRequest('/api/settings/change-password', {
|
|
251
|
-
method: 'POST',
|
|
252
|
-
body: JSON.stringify({
|
|
253
|
-
current_password: currentPassword,
|
|
254
|
-
new_password: newPassword,
|
|
255
|
-
confirm_password: confirmPassword,
|
|
256
|
-
}),
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Server-side logout
|
|
262
|
-
*/
|
|
263
|
-
export async function serverLogout() {
|
|
264
|
-
return await apiRequest('/api/auth/logout', {
|
|
265
|
-
method: 'POST',
|
|
266
|
-
});
|
|
288
|
+
export function regenerateApiKey () {
|
|
289
|
+
return apiRequest('/api/api-key/regenerate', {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
body: JSON.stringify({ confirm: 'yes' })
|
|
292
|
+
})
|
|
267
293
|
}
|
|
268
294
|
|
|
269
295
|
export default {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
};
|
|
296
|
+
apiRequest,
|
|
297
|
+
recordDeployment,
|
|
298
|
+
listDeployments,
|
|
299
|
+
getDeployment,
|
|
300
|
+
getVersions,
|
|
301
|
+
rollbackVersion,
|
|
302
|
+
checkSubdomainAvailable,
|
|
303
|
+
reserveSubdomain,
|
|
304
|
+
unreserveSubdomain,
|
|
305
|
+
listSubdomains,
|
|
306
|
+
getCurrentUser,
|
|
307
|
+
healthCheck,
|
|
308
|
+
resendVerification,
|
|
309
|
+
regenerateApiKey
|
|
310
|
+
}
|