launchpd 1.0.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.
@@ -0,0 +1,181 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, relative, posix, sep } from 'node:path';
3
+ import mime from 'mime-types';
4
+ import { config } from '../config.js';
5
+ import { getApiKey, getApiSecret } from './credentials.js';
6
+ import { createHmac } from 'node:crypto';
7
+
8
+ const API_BASE_URL = config.apiUrl;
9
+
10
+ /**
11
+ * Convert Windows path to POSIX for R2 keys
12
+ * @param {string} windowsPath
13
+ * @returns {string}
14
+ */
15
+ function toPosixPath(windowsPath) {
16
+ return windowsPath.split(sep).join(posix.sep);
17
+ }
18
+
19
+ /**
20
+ * Upload a single file via API proxy
21
+ * @param {Buffer} content - File content
22
+ * @param {string} subdomain - Target subdomain
23
+ * @param {number} version - Version number
24
+ * @param {string} filePath - Relative file path
25
+ * @param {string} contentType - MIME type
26
+ */
27
+ async function uploadFile(content, subdomain, version, filePath, contentType) {
28
+ const apiKey = await getApiKey();
29
+ const apiSecret = await getApiSecret();
30
+ const headers = {
31
+ 'X-API-Key': apiKey,
32
+ 'X-Subdomain': subdomain,
33
+ 'X-Version': String(version),
34
+ 'X-File-Path': filePath,
35
+ 'X-Content-Type': contentType,
36
+ 'Content-Type': 'application/octet-stream',
37
+ };
38
+
39
+ if (apiSecret) {
40
+ const timestamp = Date.now().toString();
41
+ const endpoint = '/api/upload/file'; // Match the worker path
42
+ const hmac = createHmac('sha256', apiSecret);
43
+ hmac.update('POST');
44
+ hmac.update(endpoint);
45
+ hmac.update(timestamp);
46
+ hmac.update(content); // Buffer is fine for update()
47
+
48
+ headers['X-Timestamp'] = timestamp;
49
+ headers['X-Signature'] = hmac.digest('hex');
50
+ }
51
+
52
+ const response = await fetch(`${API_BASE_URL}/api/upload/file`, {
53
+ method: 'POST',
54
+ headers,
55
+ body: content,
56
+ });
57
+
58
+ if (!response.ok) {
59
+ const error = await response.json().catch(() => ({ error: 'Upload failed' }));
60
+ throw new Error(error.error || `Upload failed: ${response.status}`);
61
+ }
62
+
63
+ return response.json();
64
+ }
65
+
66
+ /**
67
+ * Mark upload complete and set active version
68
+ * @param {string} subdomain - Target subdomain
69
+ * @param {number} version - Version number
70
+ * @param {number} fileCount - Number of files uploaded
71
+ * @param {number} totalBytes - Total bytes uploaded
72
+ * @param {string} folderName - Original folder name
73
+ * @param {string|null} expiresAt - ISO expiration timestamp
74
+ */
75
+ async function completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt) {
76
+ const apiKey = await getApiKey();
77
+ const apiSecret = await getApiSecret();
78
+ const headers = {
79
+ 'X-API-Key': apiKey,
80
+ 'Content-Type': 'application/json',
81
+ };
82
+
83
+ const body = JSON.stringify({
84
+ subdomain,
85
+ version,
86
+ fileCount,
87
+ totalBytes,
88
+ folderName,
89
+ expiresAt,
90
+ });
91
+
92
+ if (apiSecret) {
93
+ const timestamp = Date.now().toString();
94
+ const endpoint = '/api/upload/complete';
95
+ const hmac = createHmac('sha256', apiSecret);
96
+ hmac.update('POST');
97
+ hmac.update(endpoint);
98
+ hmac.update(timestamp);
99
+ hmac.update(body);
100
+
101
+ headers['X-Timestamp'] = timestamp;
102
+ headers['X-Signature'] = hmac.digest('hex');
103
+ }
104
+
105
+ const response = await fetch(`${API_BASE_URL}/api/upload/complete`, {
106
+ method: 'POST',
107
+ headers,
108
+ body,
109
+ });
110
+
111
+ if (!response.ok) {
112
+ let errorMsg = 'Complete upload failed';
113
+ const text = await response.text();
114
+ try {
115
+ const data = JSON.parse(text);
116
+ errorMsg = data.error || `Complete upload failed: ${response.status} ${response.statusText}`;
117
+ } catch {
118
+ errorMsg = `Complete upload failed: ${response.status} ${response.statusText} - ${text.substring(0, 100)}`;
119
+ }
120
+ throw new Error(errorMsg);
121
+ }
122
+
123
+ return response.json();
124
+ }
125
+
126
+ /**
127
+ * Upload a folder to Launchpd via API proxy
128
+ * @param {string} localPath - Local folder path
129
+ * @param {string} subdomain - Subdomain to use as bucket prefix
130
+ * @param {number} version - Version number for this deployment
131
+ * @param {function} onProgress - Progress callback (uploaded, total, fileName)
132
+ */
133
+ export async function uploadFolder(localPath, subdomain, version = 1, onProgress = null) {
134
+ const files = await readdir(localPath, { recursive: true, withFileTypes: true });
135
+
136
+ let uploaded = 0;
137
+ let totalBytes = 0;
138
+ const total = files.filter(f => f.isFile()).length;
139
+
140
+ for (const file of files) {
141
+ if (!file.isFile()) continue;
142
+
143
+ // Build full local path
144
+ const fullPath = join(file.parentPath || file.path, file.name);
145
+
146
+ // Build relative path for R2 key
147
+ const relativePath = relative(localPath, fullPath);
148
+ const posixPath = toPosixPath(relativePath);
149
+
150
+ // Detect content type
151
+ const contentType = mime.lookup(file.name) || 'application/octet-stream';
152
+
153
+ // Read file and upload via API
154
+ const body = await readFile(fullPath);
155
+ totalBytes += body.length;
156
+
157
+ await uploadFile(body, subdomain, version, posixPath, contentType);
158
+
159
+ uploaded++;
160
+
161
+ // Call progress callback if provided
162
+ if (onProgress) {
163
+ onProgress(uploaded, total, posixPath);
164
+ }
165
+ }
166
+
167
+ return { uploaded, subdomain, totalBytes };
168
+ }
169
+
170
+ /**
171
+ * Complete the upload and set active version
172
+ * @param {string} subdomain - Target subdomain
173
+ * @param {number} version - Version number
174
+ * @param {number} fileCount - Number of files
175
+ * @param {number} totalBytes - Total bytes
176
+ * @param {string} folderName - Folder name
177
+ * @param {string|null} expiresAt - Expiration ISO timestamp
178
+ */
179
+ export async function finalizeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt = null) {
180
+ return completeUpload(subdomain, version, fileCount, totalBytes, folderName, expiresAt);
181
+ }