launchpd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of launchpd might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/bin/cli.js +90 -0
- package/bin/setup.js +62 -0
- package/package.json +68 -0
- package/src/commands/auth.js +324 -0
- package/src/commands/deploy.js +194 -0
- package/src/commands/index.js +9 -0
- package/src/commands/list.js +111 -0
- package/src/commands/rollback.js +101 -0
- package/src/commands/versions.js +75 -0
- package/src/config.js +36 -0
- package/src/utils/api.js +158 -0
- package/src/utils/credentials.js +143 -0
- package/src/utils/expiration.js +89 -0
- package/src/utils/id.js +17 -0
- package/src/utils/index.js +13 -0
- package/src/utils/localConfig.js +85 -0
- package/src/utils/logger.js +33 -0
- package/src/utils/metadata.js +354 -0
- package/src/utils/quota.js +229 -0
- package/src/utils/upload.js +74 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { getVersionsForSubdomain, setActiveVersion, getActiveVersion } from '../utils/metadata.js';
|
|
2
|
+
import { getVersions as getVersionsFromAPI, rollbackVersion as rollbackViaAPI } from '../utils/api.js';
|
|
3
|
+
import { success, error, info, warning } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rollback a subdomain to a previous version
|
|
7
|
+
* @param {string} subdomain - Subdomain to rollback
|
|
8
|
+
* @param {object} options - Command options
|
|
9
|
+
* @param {number} options.to - Specific version to rollback to (optional)
|
|
10
|
+
*/
|
|
11
|
+
export async function rollback(subdomain, options) {
|
|
12
|
+
try {
|
|
13
|
+
info(`Checking versions for ${subdomain}...`);
|
|
14
|
+
|
|
15
|
+
// Get all versions for this subdomain (try API first)
|
|
16
|
+
let versions = [];
|
|
17
|
+
let currentActive = 1;
|
|
18
|
+
let useAPI = false;
|
|
19
|
+
|
|
20
|
+
const apiResult = await getVersionsFromAPI(subdomain);
|
|
21
|
+
if (apiResult && apiResult.versions) {
|
|
22
|
+
versions = apiResult.versions.map(v => ({
|
|
23
|
+
version: v.version,
|
|
24
|
+
timestamp: v.created_at,
|
|
25
|
+
fileCount: v.file_count,
|
|
26
|
+
}));
|
|
27
|
+
currentActive = apiResult.activeVersion || 1;
|
|
28
|
+
useAPI = true;
|
|
29
|
+
} else {
|
|
30
|
+
// Fallback to R2 metadata
|
|
31
|
+
versions = await getVersionsForSubdomain(subdomain);
|
|
32
|
+
currentActive = await getActiveVersion(subdomain);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (versions.length === 0) {
|
|
36
|
+
error(`No deployments found for subdomain: ${subdomain}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (versions.length === 1) {
|
|
41
|
+
warning('Only one version exists. Nothing to rollback to.');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
info(`Current active version: v${currentActive}`);
|
|
46
|
+
|
|
47
|
+
// Determine target version
|
|
48
|
+
let targetVersion;
|
|
49
|
+
if (options.to) {
|
|
50
|
+
targetVersion = Number.parseInt(options.to, 10);
|
|
51
|
+
const versionExists = versions.some(v => v.version === targetVersion);
|
|
52
|
+
if (!versionExists) {
|
|
53
|
+
error(`Version ${targetVersion} does not exist.`);
|
|
54
|
+
info('Available versions:');
|
|
55
|
+
versions.forEach(v => {
|
|
56
|
+
info(` v${v.version} - ${v.timestamp}`);
|
|
57
|
+
});
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Default: rollback to previous version
|
|
62
|
+
const sortedVersions = versions.map(v => v.version).sort((a, b) => b - a);
|
|
63
|
+
const currentIndex = sortedVersions.indexOf(currentActive);
|
|
64
|
+
if (currentIndex === sortedVersions.length - 1) {
|
|
65
|
+
warning('Already at the oldest version. Cannot rollback further.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
targetVersion = sortedVersions[currentIndex + 1];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (targetVersion === currentActive) {
|
|
72
|
+
warning(`Version ${targetVersion} is already active.`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
info(`Rolling back from v${currentActive} to v${targetVersion}...`);
|
|
77
|
+
|
|
78
|
+
// Set the target version as active
|
|
79
|
+
if (useAPI) {
|
|
80
|
+
// Use API for centralized rollback (updates both D1 and R2)
|
|
81
|
+
const result = await rollbackViaAPI(subdomain, targetVersion);
|
|
82
|
+
if (!result) {
|
|
83
|
+
warning('API unavailable, falling back to local rollback');
|
|
84
|
+
await setActiveVersion(subdomain, targetVersion);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
await setActiveVersion(subdomain, targetVersion);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Find the target version's deployment record for file count
|
|
91
|
+
const targetDeployment = versions.find(v => v.version === targetVersion);
|
|
92
|
+
|
|
93
|
+
success(`Rolled back to v${targetVersion} successfully!`);
|
|
94
|
+
console.log(`\n 🔄 https://${subdomain}.launchpd.cloud\n`);
|
|
95
|
+
info(`Restored deployment from: ${targetDeployment?.timestamp || 'unknown'}`);
|
|
96
|
+
|
|
97
|
+
} catch (err) {
|
|
98
|
+
error(`Rollback failed: ${err.message}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getVersionsForSubdomain, getActiveVersion } from '../utils/metadata.js';
|
|
2
|
+
import { getVersions as getVersionsFromAPI } from '../utils/api.js';
|
|
3
|
+
import { success, error, info } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List all versions for a subdomain
|
|
7
|
+
* @param {string} subdomain - Subdomain to list versions for
|
|
8
|
+
* @param {object} options - Command options
|
|
9
|
+
* @param {boolean} options.json - Output as JSON
|
|
10
|
+
*/
|
|
11
|
+
export async function versions(subdomain, options) {
|
|
12
|
+
try {
|
|
13
|
+
info(`Fetching versions for ${subdomain}...`);
|
|
14
|
+
|
|
15
|
+
let versionList = [];
|
|
16
|
+
let activeVersion = 1;
|
|
17
|
+
|
|
18
|
+
// Try API first
|
|
19
|
+
const apiResult = await getVersionsFromAPI(subdomain);
|
|
20
|
+
if (apiResult && apiResult.versions) {
|
|
21
|
+
versionList = apiResult.versions.map(v => ({
|
|
22
|
+
version: v.version,
|
|
23
|
+
timestamp: v.created_at,
|
|
24
|
+
fileCount: v.file_count,
|
|
25
|
+
totalBytes: v.total_bytes,
|
|
26
|
+
}));
|
|
27
|
+
activeVersion = apiResult.activeVersion || 1;
|
|
28
|
+
} else {
|
|
29
|
+
// Fallback to R2 metadata
|
|
30
|
+
versionList = await getVersionsForSubdomain(subdomain);
|
|
31
|
+
activeVersion = await getActiveVersion(subdomain);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (versionList.length === 0) {
|
|
35
|
+
error(`No deployments found for subdomain: ${subdomain}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.json) {
|
|
40
|
+
console.log(JSON.stringify({
|
|
41
|
+
subdomain,
|
|
42
|
+
activeVersion,
|
|
43
|
+
versions: versionList.map(v => ({
|
|
44
|
+
version: v.version,
|
|
45
|
+
timestamp: v.timestamp,
|
|
46
|
+
fileCount: v.fileCount,
|
|
47
|
+
totalBytes: v.totalBytes,
|
|
48
|
+
isActive: v.version === activeVersion,
|
|
49
|
+
})),
|
|
50
|
+
}, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('');
|
|
55
|
+
success(`Versions for ${subdomain}.launchpd.cloud:`);
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
for (const v of versionList) {
|
|
59
|
+
const isActive = v.version === activeVersion;
|
|
60
|
+
const activeMarker = isActive ? ' ← active' : '';
|
|
61
|
+
const sizeKB = v.totalBytes ? `${(v.totalBytes / 1024).toFixed(1)} KB` : 'unknown size';
|
|
62
|
+
const date = new Date(v.timestamp).toLocaleString();
|
|
63
|
+
|
|
64
|
+
console.log(` v${v.version} │ ${date} │ ${v.fileCount} files │ ${sizeKB}${activeMarker}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('');
|
|
68
|
+
info(`Use 'launchpd rollback ${subdomain} --to <n>' to restore a version.`);
|
|
69
|
+
console.log('');
|
|
70
|
+
|
|
71
|
+
} catch (err) {
|
|
72
|
+
error(`Failed to list versions: ${err.message}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application configuration loaded from environment variables
|
|
5
|
+
*/
|
|
6
|
+
export const config = {
|
|
7
|
+
r2: {
|
|
8
|
+
accountId: process.env.R2_ACCOUNT_ID || '',
|
|
9
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
|
|
10
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
|
|
11
|
+
bucketName: process.env.R2_BUCKET_NAME || 'launchpd',
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
// Base domain for deployments
|
|
15
|
+
domain: process.env.STATICLAUNCH_DOMAIN || 'launchpd.cloud',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate required configuration
|
|
20
|
+
* @returns {boolean} true if all required config is present
|
|
21
|
+
*/
|
|
22
|
+
export function validateConfig() {
|
|
23
|
+
const required = [
|
|
24
|
+
'R2_ACCOUNT_ID',
|
|
25
|
+
'R2_ACCESS_KEY_ID',
|
|
26
|
+
'R2_SECRET_ACCESS_KEY',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const missing = required.filter(key => !process.env[key]);
|
|
30
|
+
|
|
31
|
+
if (missing.length > 0) {
|
|
32
|
+
return { valid: false, missing };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { valid: true, missing: [] };
|
|
36
|
+
}
|
package/src/utils/api.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client for StaticLaunch Metadata API
|
|
3
|
+
* Communicates with the Cloudflare Worker API endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { config } from '../config.js';
|
|
7
|
+
|
|
8
|
+
const API_BASE_URL = `https://api.${config.domain}`;
|
|
9
|
+
const API_KEY = process.env.STATICLAUNCH_API_KEY || 'public-beta-key';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Make an authenticated API request
|
|
13
|
+
*/
|
|
14
|
+
async function apiRequest(endpoint, options = {}) {
|
|
15
|
+
const url = `${API_BASE_URL}${endpoint}`;
|
|
16
|
+
|
|
17
|
+
const headers = {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
'X-API-Key': API_KEY,
|
|
20
|
+
...options.headers,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
...options,
|
|
26
|
+
headers,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(data.error || `API error: ${response.status}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return data;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// If API is unavailable, return null to allow fallback to local storage
|
|
38
|
+
if (err.message.includes('fetch failed') || err.message.includes('ENOTFOUND')) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the next version number for a subdomain
|
|
47
|
+
*/
|
|
48
|
+
export async function getNextVersionFromAPI(subdomain) {
|
|
49
|
+
const result = await apiRequest(`/api/versions/${subdomain}`);
|
|
50
|
+
if (!result || !result.versions || result.versions.length === 0) {
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const maxVersion = Math.max(...result.versions.map(v => v.version));
|
|
54
|
+
return maxVersion + 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Record a new deployment in the API
|
|
59
|
+
*/
|
|
60
|
+
export async function recordDeployment(deploymentData) {
|
|
61
|
+
const { subdomain, folderName, fileCount, totalBytes, version, expiresAt } = deploymentData;
|
|
62
|
+
|
|
63
|
+
return apiRequest('/api/deployments', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
subdomain,
|
|
67
|
+
folderName,
|
|
68
|
+
fileCount,
|
|
69
|
+
totalBytes,
|
|
70
|
+
version,
|
|
71
|
+
cliVersion: '0.1.0',
|
|
72
|
+
expiresAt,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get list of user's deployments
|
|
79
|
+
*/
|
|
80
|
+
export async function listDeployments(limit = 50, offset = 0) {
|
|
81
|
+
return apiRequest(`/api/deployments?limit=${limit}&offset=${offset}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get deployment details for a subdomain
|
|
86
|
+
*/
|
|
87
|
+
export async function getDeployment(subdomain) {
|
|
88
|
+
return apiRequest(`/api/deployments/${subdomain}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get version history for a subdomain
|
|
93
|
+
*/
|
|
94
|
+
export async function getVersions(subdomain) {
|
|
95
|
+
return apiRequest(`/api/versions/${subdomain}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Rollback to a specific version
|
|
100
|
+
*/
|
|
101
|
+
export async function rollbackVersion(subdomain, version) {
|
|
102
|
+
return apiRequest(`/api/versions/${subdomain}/rollback`, {
|
|
103
|
+
method: 'PUT',
|
|
104
|
+
body: JSON.stringify({ version }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a subdomain is available
|
|
110
|
+
*/
|
|
111
|
+
export async function checkSubdomainAvailable(subdomain) {
|
|
112
|
+
const result = await apiRequest(`/api/public/check/${subdomain}`);
|
|
113
|
+
return result?.available ?? true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reserve a subdomain
|
|
118
|
+
*/
|
|
119
|
+
export async function reserveSubdomain(subdomain) {
|
|
120
|
+
return apiRequest('/api/subdomains/reserve', {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: JSON.stringify({ subdomain }),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get user's subdomains
|
|
128
|
+
*/
|
|
129
|
+
export async function listSubdomains() {
|
|
130
|
+
return apiRequest('/api/subdomains');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get current user info
|
|
135
|
+
*/
|
|
136
|
+
export async function getCurrentUser() {
|
|
137
|
+
return apiRequest('/api/users/me');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Health check
|
|
142
|
+
*/
|
|
143
|
+
export async function healthCheck() {
|
|
144
|
+
return apiRequest('/api/health');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default {
|
|
148
|
+
recordDeployment,
|
|
149
|
+
listDeployments,
|
|
150
|
+
getDeployment,
|
|
151
|
+
getVersions,
|
|
152
|
+
rollbackVersion,
|
|
153
|
+
checkSubdomainAvailable,
|
|
154
|
+
reserveSubdomain,
|
|
155
|
+
listSubdomains,
|
|
156
|
+
getCurrentUser,
|
|
157
|
+
healthCheck,
|
|
158
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials management for StaticLaunch CLI
|
|
3
|
+
* Stores API key and user info in ~/.staticlaunch/credentials.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the credentials directory path
|
|
14
|
+
*/
|
|
15
|
+
function getConfigDir() {
|
|
16
|
+
return join(homedir(), '.staticlaunch');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the credentials file path
|
|
21
|
+
*/
|
|
22
|
+
function getCredentialsPath() {
|
|
23
|
+
return join(getConfigDir(), 'credentials.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the client token path (for anonymous tracking)
|
|
28
|
+
*/
|
|
29
|
+
function getClientTokenPath() {
|
|
30
|
+
return join(getConfigDir(), 'client_token');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ensure config directory exists
|
|
35
|
+
*/
|
|
36
|
+
async function ensureConfigDir() {
|
|
37
|
+
const dir = getConfigDir();
|
|
38
|
+
if (!existsSync(dir)) {
|
|
39
|
+
await mkdir(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get or create a persistent client token for anonymous tracking
|
|
45
|
+
* This helps identify the same anonymous user across sessions
|
|
46
|
+
*/
|
|
47
|
+
export async function getClientToken() {
|
|
48
|
+
await ensureConfigDir();
|
|
49
|
+
const tokenPath = getClientTokenPath();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
if (existsSync(tokenPath)) {
|
|
53
|
+
return await readFile(tokenPath, 'utf-8');
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Token file corrupted, regenerate
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Generate new token
|
|
60
|
+
const token = `cli_${randomBytes(16).toString('hex')}`;
|
|
61
|
+
await writeFile(tokenPath, token, 'utf-8');
|
|
62
|
+
return token;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get stored credentials
|
|
67
|
+
* @returns {Promise<{apiKey: string, userId: string, email: string, tier: string} | null>}
|
|
68
|
+
*/
|
|
69
|
+
export async function getCredentials() {
|
|
70
|
+
const filePath = getCredentialsPath();
|
|
71
|
+
try {
|
|
72
|
+
if (existsSync(filePath)) {
|
|
73
|
+
const text = await readFile(filePath, 'utf-8');
|
|
74
|
+
const data = JSON.parse(text);
|
|
75
|
+
|
|
76
|
+
// Validate the structure
|
|
77
|
+
if (data.apiKey) {
|
|
78
|
+
return {
|
|
79
|
+
apiKey: data.apiKey,
|
|
80
|
+
userId: data.userId || null,
|
|
81
|
+
email: data.email || null,
|
|
82
|
+
tier: data.tier || 'free',
|
|
83
|
+
savedAt: data.savedAt || null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Corrupted or invalid JSON file
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Save credentials
|
|
95
|
+
* @param {object} credentials - Credentials to save
|
|
96
|
+
*/
|
|
97
|
+
export async function saveCredentials(credentials) {
|
|
98
|
+
await ensureConfigDir();
|
|
99
|
+
|
|
100
|
+
const data = {
|
|
101
|
+
apiKey: credentials.apiKey,
|
|
102
|
+
userId: credentials.userId || null,
|
|
103
|
+
email: credentials.email || null,
|
|
104
|
+
tier: credentials.tier || 'free',
|
|
105
|
+
savedAt: new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await writeFile(
|
|
109
|
+
getCredentialsPath(),
|
|
110
|
+
JSON.stringify(data, null, 2),
|
|
111
|
+
'utf-8'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Delete stored credentials (logout)
|
|
117
|
+
*/
|
|
118
|
+
export async function clearCredentials() {
|
|
119
|
+
const filePath = getCredentialsPath();
|
|
120
|
+
try {
|
|
121
|
+
if (existsSync(filePath)) {
|
|
122
|
+
await unlink(filePath);
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// File doesn't exist or can't be deleted
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if user is logged in
|
|
131
|
+
*/
|
|
132
|
+
export async function isLoggedIn() {
|
|
133
|
+
const creds = await getCredentials();
|
|
134
|
+
return creds !== null && creds.apiKey !== null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the API key for requests (falls back to public beta key)
|
|
139
|
+
*/
|
|
140
|
+
export async function getApiKey() {
|
|
141
|
+
const creds = await getCredentials();
|
|
142
|
+
return creds?.apiKey || process.env.STATICLAUNCH_API_KEY || 'public-beta-key';
|
|
143
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a time string into milliseconds
|
|
3
|
+
* Supports: 30m, 1h, 2h, 1d, 7d, etc.
|
|
4
|
+
* Minimum: 30 minutes
|
|
5
|
+
*
|
|
6
|
+
* @param {string} timeStr - Time string (e.g., "30m", "2h", "1d")
|
|
7
|
+
* @returns {number} Milliseconds
|
|
8
|
+
*/
|
|
9
|
+
export function parseTimeString(timeStr) {
|
|
10
|
+
const regex = /^(\d+)([mhd])$/i;
|
|
11
|
+
const match = regex.exec(timeStr);
|
|
12
|
+
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new Error(`Invalid time format: "${timeStr}". Use format like 30m, 2h, 1d`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const value = Number.parseInt(match[1], 10);
|
|
18
|
+
const unit = match[2].toLowerCase();
|
|
19
|
+
|
|
20
|
+
let ms;
|
|
21
|
+
switch (unit) {
|
|
22
|
+
case 'm':
|
|
23
|
+
ms = value * 60 * 1000;
|
|
24
|
+
break;
|
|
25
|
+
case 'h':
|
|
26
|
+
ms = value * 60 * 60 * 1000;
|
|
27
|
+
break;
|
|
28
|
+
case 'd':
|
|
29
|
+
ms = value * 24 * 60 * 60 * 1000;
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unknown time unit: ${unit}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Minimum 30 minutes
|
|
36
|
+
const minMs = 30 * 60 * 1000;
|
|
37
|
+
if (ms < minMs) {
|
|
38
|
+
throw new Error('Minimum expiration time is 30 minutes (30m)');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return ms;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calculate expiration timestamp from a time string
|
|
46
|
+
* @param {string} timeStr - Time string (e.g., "30m", "2h", "1d")
|
|
47
|
+
* @returns {string} ISO timestamp of expiration
|
|
48
|
+
*/
|
|
49
|
+
export function calculateExpiresAt(timeStr) {
|
|
50
|
+
const ms = parseTimeString(timeStr);
|
|
51
|
+
return new Date(Date.now() + ms).toISOString();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format remaining time until expiration
|
|
56
|
+
* @param {string} expiresAt - ISO timestamp
|
|
57
|
+
* @returns {string} Human-readable time remaining
|
|
58
|
+
*/
|
|
59
|
+
export function formatTimeRemaining(expiresAt) {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const expiry = new Date(expiresAt).getTime();
|
|
62
|
+
const remaining = expiry - now;
|
|
63
|
+
|
|
64
|
+
if (remaining <= 0) {
|
|
65
|
+
return 'expired';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const minutes = Math.floor(remaining / (60 * 1000));
|
|
69
|
+
const hours = Math.floor(remaining / (60 * 60 * 1000));
|
|
70
|
+
const days = Math.floor(remaining / (24 * 60 * 60 * 1000));
|
|
71
|
+
|
|
72
|
+
if (days > 0) {
|
|
73
|
+
return `${days}d ${hours % 24}h remaining`;
|
|
74
|
+
} else if (hours > 0) {
|
|
75
|
+
return `${hours}h ${minutes % 60}m remaining`;
|
|
76
|
+
} else {
|
|
77
|
+
return `${minutes}m remaining`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a deployment has expired
|
|
83
|
+
* @param {string} expiresAt - ISO timestamp or null
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function isExpired(expiresAt) {
|
|
87
|
+
if (!expiresAt) return false;
|
|
88
|
+
return new Date(expiresAt).getTime() < Date.now();
|
|
89
|
+
}
|
package/src/utils/id.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { customAlphabet } from 'nanoid';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a subdomain-safe unique ID
|
|
5
|
+
* Uses lowercase alphanumeric characters only (valid for DNS)
|
|
6
|
+
* 12 characters provides ~62 bits of entropy
|
|
7
|
+
*/
|
|
8
|
+
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
9
|
+
const nanoid = customAlphabet(alphabet, 12);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a unique subdomain ID
|
|
13
|
+
* @returns {string} A 12-character lowercase alphanumeric string
|
|
14
|
+
*/
|
|
15
|
+
export function generateSubdomain() {
|
|
16
|
+
return nanoid();
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utils index - exports all utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './api.js';
|
|
6
|
+
export * from './credentials.js';
|
|
7
|
+
export * from './expiration.js';
|
|
8
|
+
export * from './id.js';
|
|
9
|
+
export * from './localConfig.js';
|
|
10
|
+
export * from './logger.js';
|
|
11
|
+
export * from './metadata.js';
|
|
12
|
+
export * from './quota.js';
|
|
13
|
+
export * from './upload.js';
|