drop2run 0.11.0 → 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.
- package/README.md +3 -3
- package/dist/index.js +1686 -1601
- package/dist/{package-B79z0E4v.js → package-DgeW4aj9.js} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { join, dirname, relative, sep, resolve } from "node:path";
|
|
2
2
|
import { readFileSync, writeFileSync, chmodSync, renameSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
|
+
import { realpath, readdir, stat, readFile } from "node:fs/promises";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { randomBytes, createHash } from "node:crypto";
|
|
6
7
|
import { createServer } from "node:http";
|
|
7
|
-
|
|
8
|
+
const DEFAULT_API_BASE_URL = "https://dropto.run/api";
|
|
9
|
+
const PRODUCTION_SITE_ORIGIN = "https://dropto.run";
|
|
10
|
+
const PRODUCTION_DASHBOARD_ORIGIN = "https://app.dropto.run";
|
|
11
|
+
function dashboardUrlFor(apiBaseUrl) {
|
|
12
|
+
const origin = apiBaseUrl.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
13
|
+
return origin === PRODUCTION_SITE_ORIGIN ? PRODUCTION_DASHBOARD_ORIGIN : origin;
|
|
14
|
+
}
|
|
8
15
|
const TOKEN_VARIABLE = "DROP2RUN_TOKEN";
|
|
9
16
|
function configPath() {
|
|
10
17
|
return join(homedir(), ".config", "drop2run", "config.json");
|
|
11
18
|
}
|
|
12
|
-
const DEFAULT_API_BASE_URL = "https://dropto.run/api";
|
|
13
|
-
const PRODUCTION_SITE_ORIGIN = "https://dropto.run";
|
|
14
|
-
const PRODUCTION_DASHBOARD_ORIGIN = "https://app.dropto.run";
|
|
15
19
|
function loadCredentials(env = process.env, read = (path) => readFileSync(path, "utf8")) {
|
|
16
20
|
let stored = {};
|
|
17
21
|
try {
|
|
@@ -34,10 +38,6 @@ function resolveApiBaseUrl(env = process.env, read = (path) => readFileSync(path
|
|
|
34
38
|
const url = env.DROP2RUN_API_URL?.trim() || stored.apiBaseUrl?.trim() || DEFAULT_API_BASE_URL;
|
|
35
39
|
return url.replace(/\/+$/, "");
|
|
36
40
|
}
|
|
37
|
-
function dashboardUrlFor(apiBaseUrl) {
|
|
38
|
-
const origin = apiBaseUrl.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
39
|
-
return origin === PRODUCTION_SITE_ORIGIN ? PRODUCTION_DASHBOARD_ORIGIN : origin;
|
|
40
|
-
}
|
|
41
41
|
function saveToken(token, path = configPath()) {
|
|
42
42
|
let existing = {};
|
|
43
43
|
try {
|
|
@@ -100,723 +100,208 @@ function missingCredentialsMessage(surface = "cli") {
|
|
|
100
100
|
"The token is shown once when it is created and cannot be recovered afterwards."
|
|
101
101
|
].join("\n");
|
|
102
102
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
function sizeOf(content) {
|
|
104
|
+
if (ArrayBuffer.isView(content)) return content.byteLength;
|
|
105
|
+
if (isArrayBuffer(content)) return content.byteLength;
|
|
106
|
+
if (typeof content.size === "number") return content.size;
|
|
107
|
+
throw new TypeError("File contents must be a Uint8Array, an ArrayBuffer or a Blob.");
|
|
108
|
+
}
|
|
109
|
+
function isArrayBuffer(value) {
|
|
110
|
+
return Object.prototype.toString.call(value) === "[object ArrayBuffer]";
|
|
111
|
+
}
|
|
112
|
+
async function bytesOf(content) {
|
|
113
|
+
if (ArrayBuffer.isView(content)) return content;
|
|
114
|
+
if (isArrayBuffer(content)) return new Uint8Array(content);
|
|
115
|
+
return new Uint8Array(await content.arrayBuffer());
|
|
116
|
+
}
|
|
117
|
+
class DeployError extends Error {
|
|
118
|
+
/**
|
|
119
|
+
* @param code Stable code, either an API error type (`quota_exceeded`, `unsafe_path`, …) or one of
|
|
120
|
+
* the client-side codes in {@link ClientErrorCode}.
|
|
121
|
+
* @param message Text safe to show the user.
|
|
122
|
+
* @param detail Anything extra worth logging, such as the RFC 9457 body from the API.
|
|
123
|
+
*/
|
|
124
|
+
constructor(code, message, detail) {
|
|
125
|
+
super(message);
|
|
126
|
+
this.code = code;
|
|
127
|
+
this.detail = detail;
|
|
128
|
+
this.name = "DeployError";
|
|
115
129
|
}
|
|
116
|
-
return await response.json();
|
|
117
130
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
const ClientErrorCode = {
|
|
132
|
+
/** The drop held no files once junk was filtered out. */
|
|
133
|
+
Empty: "empty_drop",
|
|
134
|
+
/** No `index.html` at the root of the drop. */
|
|
135
|
+
MissingIndex: "missing_index",
|
|
136
|
+
/** The drop is larger than the plan allows; the server enforces this too. */
|
|
137
|
+
TooLarge: "too_large",
|
|
138
|
+
/** A single file is larger than the plan allows. */
|
|
139
|
+
FileTooLarge: "file_too_large",
|
|
140
|
+
/** More files than the plan allows. */
|
|
141
|
+
TooManyFiles: "too_many_files",
|
|
142
|
+
/**
|
|
143
|
+
* A zip claims to expand to more than the plan allows.
|
|
144
|
+
*
|
|
145
|
+
* Distinct from {@link TooLarge}, which is about a drop whose files have already been read. This one
|
|
146
|
+
* is raised from the archive's own directory before anything is decompressed, so the numbers in it
|
|
147
|
+
* are what the zip declared rather than what was measured.
|
|
148
|
+
*/
|
|
149
|
+
ZipTooLarge: "zip_too_large",
|
|
150
|
+
/** The user cancelled. */
|
|
151
|
+
Cancelled: "cancelled",
|
|
152
|
+
/** An upload kept failing after every retry. */
|
|
153
|
+
UploadFailed: "upload_failed",
|
|
154
|
+
/**
|
|
155
|
+
* The browser could not hold what was dropped in memory. Raised when allocating the buffer for a
|
|
156
|
+
* large zip or file fails, which a browser reports as a `RangeError` rather than as anything that
|
|
157
|
+
* names memory.
|
|
158
|
+
*/
|
|
159
|
+
OutOfMemory: "out_of_memory",
|
|
160
|
+
/**
|
|
161
|
+
* A dropped file could not be read when it came to be hashed. Files from a folder stay on disk until
|
|
162
|
+
* then, so one moved, deleted or changed after the drop fails here rather than at the drop.
|
|
163
|
+
*/
|
|
164
|
+
ReadFailed: "read_failed",
|
|
165
|
+
/**
|
|
166
|
+
* A control-plane call got no answer: the connection failed before a response arrived, or the
|
|
167
|
+
* request timeout passed. Distinct from an HTTP error, which is an answer — this one says nothing
|
|
168
|
+
* about whether the server acted, which is why only an idempotent call may retry on it.
|
|
169
|
+
*/
|
|
170
|
+
NetworkFailed: "network_failed"
|
|
171
|
+
};
|
|
172
|
+
function describeError(error) {
|
|
173
|
+
const parts = [];
|
|
174
|
+
let current = error;
|
|
175
|
+
for (let depth = 0; depth < 5 && current !== void 0 && current !== null; depth++) {
|
|
176
|
+
if (current instanceof Error) {
|
|
177
|
+
const code = current.code;
|
|
178
|
+
parts.push(typeof code === "string" ? `${current.message} (${code})` : current.message);
|
|
179
|
+
current = current.cause;
|
|
180
|
+
} else {
|
|
181
|
+
parts.push(String(current));
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
124
184
|
}
|
|
125
|
-
return
|
|
185
|
+
return parts.join(" ← ");
|
|
126
186
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
const ACCOUNT_HEADER = "X-Drop2Run-Account";
|
|
188
|
+
const CLAIM_TOKEN_HEADER = "X-Claim-Token";
|
|
189
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
190
|
+
const COMPLETE_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
191
|
+
async function prepareDeploy(siteId, files, options, signal) {
|
|
192
|
+
const manifest = files.map((file) => ({
|
|
193
|
+
path: file.path,
|
|
194
|
+
sha256: file.sha256,
|
|
195
|
+
size: sizeOf(file.bytes)
|
|
196
|
+
}));
|
|
197
|
+
const prepared = await request(
|
|
198
|
+
`sites/${encodeURIComponent(siteId)}/deploys/prepare`,
|
|
199
|
+
// `batches` asks for batch permits; an API that predates them ignores the field, and one that
|
|
200
|
+
// knows them answers with single permits until they are switched on.
|
|
201
|
+
{ files: manifest, batches: true },
|
|
202
|
+
options,
|
|
203
|
+
signal
|
|
141
204
|
);
|
|
142
|
-
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
const problem = await response.json().catch(() => null);
|
|
145
|
-
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
146
|
-
}
|
|
147
|
-
const detail = await response.json();
|
|
148
|
-
return { siteId: detail.siteId, subdomain: detail.subdomain, url: detail.url, name: detail.name };
|
|
205
|
+
return { ...prepared, uploadUrl: absolute(prepared.uploadUrl, options.baseUrl) };
|
|
149
206
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
207
|
+
function absolute(uploadUrl, baseUrl) {
|
|
208
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(uploadUrl)) return uploadUrl;
|
|
209
|
+
if (baseUrl === void 0 || !/^[a-z][a-z0-9+.-]*:/i.test(baseUrl)) return uploadUrl;
|
|
210
|
+
return new URL(uploadUrl, baseUrl).toString();
|
|
211
|
+
}
|
|
212
|
+
async function completeDeploy(siteId, deployId, options, signal, name) {
|
|
213
|
+
const path = `sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/complete`;
|
|
214
|
+
const body = name === null || name === void 0 ? void 0 : { name };
|
|
215
|
+
const delay = options.delay ?? wait;
|
|
216
|
+
for (let attempt = 0; ; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
return await request(path, body, options, signal);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const unanswered = error instanceof DeployError && error.code === ClientErrorCode.NetworkFailed;
|
|
221
|
+
if (!unanswered || attempt >= COMPLETE_RETRY_DELAYS_MS.length || signal?.aborted) throw error;
|
|
222
|
+
await delay(COMPLETE_RETRY_DELAYS_MS[attempt] ?? 1e3);
|
|
223
|
+
}
|
|
158
224
|
}
|
|
159
225
|
}
|
|
160
|
-
|
|
161
|
-
return
|
|
162
|
-
credentials,
|
|
163
|
-
`sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/promote`,
|
|
164
|
-
{ method: "POST" }
|
|
165
|
-
);
|
|
226
|
+
function wait(ms) {
|
|
227
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
166
228
|
}
|
|
167
|
-
function
|
|
168
|
-
|
|
229
|
+
async function request(path, body, options, signal) {
|
|
230
|
+
const doFetch = options.fetch ?? globalFetch$1();
|
|
231
|
+
const base = options.baseUrl ?? "/api";
|
|
232
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
233
|
+
const headers = { ...options.headers };
|
|
234
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
235
|
+
if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
|
|
236
|
+
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
237
|
+
if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
let timedOut = false;
|
|
240
|
+
const timer = setTimeout(() => {
|
|
241
|
+
timedOut = true;
|
|
242
|
+
controller.abort();
|
|
243
|
+
}, timeoutMs);
|
|
244
|
+
try {
|
|
245
|
+
const response = await doFetch(`${base}/${path}`, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers,
|
|
248
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
249
|
+
signal: controller.signal
|
|
250
|
+
});
|
|
251
|
+
if (!response.ok) throw await problemToError(response);
|
|
252
|
+
return await response.json();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error instanceof DeployError || signal?.aborted) throw error;
|
|
255
|
+
if (timedOut) {
|
|
256
|
+
throw new DeployError(
|
|
257
|
+
ClientErrorCode.NetworkFailed,
|
|
258
|
+
`The server did not answer within ${Math.round(timeoutMs / 1e3)} seconds.`,
|
|
259
|
+
{ path, cause: describeError(error) }
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (error instanceof TypeError) {
|
|
263
|
+
throw new DeployError(ClientErrorCode.NetworkFailed, "Could not reach the server.", {
|
|
264
|
+
path,
|
|
265
|
+
cause: describeError(error)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
throw error;
|
|
269
|
+
} finally {
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
}
|
|
169
272
|
}
|
|
170
|
-
function
|
|
171
|
-
return
|
|
273
|
+
function globalFetch$1() {
|
|
274
|
+
return globalThis.fetch.bind(globalThis);
|
|
172
275
|
}
|
|
173
|
-
async function
|
|
276
|
+
async function problemToError(response) {
|
|
277
|
+
let problem = {};
|
|
174
278
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
279
|
+
problem = await response.json();
|
|
280
|
+
} catch {
|
|
281
|
+
return new DeployError(
|
|
282
|
+
`http_${response.status}`,
|
|
283
|
+
`The server responded with ${response.status}.`
|
|
178
284
|
);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
subdomain: site.subdomain,
|
|
200
|
-
url: site.url,
|
|
201
|
-
name: site.name,
|
|
202
|
-
status: enumName(site.status),
|
|
203
|
-
invitedOnly,
|
|
204
|
-
liveDeployId: site.liveDeployId,
|
|
205
|
-
spaMode: site.spaMode,
|
|
206
|
-
docsMode: site.docsMode,
|
|
207
|
-
modeIsManual: site.modeIsManual,
|
|
208
|
-
passwordProtected: site.passwordProtected,
|
|
209
|
-
passwordProtectionAvailable: site.passwordProtectionAvailable,
|
|
210
|
-
formsEnabled: site.formsEnabled,
|
|
211
|
-
formsAvailable: site.formsAvailable,
|
|
212
|
-
expiresAt: site.expiresAt,
|
|
213
|
-
expiryAction: actionOf(site.expiresAt, site.expiryAction ?? ""),
|
|
214
|
-
scheduledExpiryAvailable: site.scheduledExpiryAvailable,
|
|
215
|
-
comments: audienceOf(site.commentAudience),
|
|
216
|
-
commentsAvailable: site.feedbackAvailable === true,
|
|
217
|
-
folderId,
|
|
218
|
-
deploys: site.deploys.map((deploy2) => ({
|
|
219
|
-
deployId: deploy2.deployId,
|
|
220
|
-
status: deploy2.status,
|
|
221
|
-
fileCount: deploy2.fileCount,
|
|
222
|
-
totalBytes: deploy2.totalBytes,
|
|
223
|
-
createdAt: deploy2.createdAt,
|
|
224
|
-
completedAt: deploy2.completedAt,
|
|
225
|
-
filesKept: deploy2.filesKept
|
|
226
|
-
}))
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
async function updateSiteSettings(credentials, siteId, change) {
|
|
230
|
-
const body = {};
|
|
231
|
-
if (change.name !== void 0) body.name = change.name;
|
|
232
|
-
if (change.mode !== void 0) {
|
|
233
|
-
body.spaMode = change.mode === "spa";
|
|
234
|
-
body.docsMode = change.mode === "docs";
|
|
235
|
-
}
|
|
236
|
-
if (change.password !== void 0) body.password = change.password;
|
|
237
|
-
if (change.expiresAt !== void 0) body.expiresAt = change.expiresAt;
|
|
238
|
-
if (change.expiryAction !== void 0) body.expiryAction = change.expiryAction;
|
|
239
|
-
if (change.formsEnabled !== void 0) body.formsEnabled = change.formsEnabled;
|
|
240
|
-
if (change.folderId !== void 0) body.folderId = change.folderId;
|
|
241
|
-
if (change.invitedOnly !== void 0) body.invitedOnly = change.invitedOnly;
|
|
242
|
-
if (change.comments !== void 0) body.commentAudience = API_AUDIENCE[change.comments];
|
|
243
|
-
if (Object.keys(body).length === 0) throw new Error("Nothing to change — name a setting.");
|
|
244
|
-
const wasInvitedOnly = change.password !== void 0 && change.password !== "" ? await readInvitedOnly(credentials, siteId) : null;
|
|
245
|
-
const { commentAudience, ...settings } = await call(credentials, `sites/${encodeURIComponent(siteId)}`, {
|
|
246
|
-
method: "PATCH",
|
|
247
|
-
body: JSON.stringify(body)
|
|
248
|
-
});
|
|
249
|
-
const invitedOnly = typeof settings.invitedOnly === "boolean" ? settings.invitedOnly : null;
|
|
250
|
-
return {
|
|
251
|
-
...settings,
|
|
252
|
-
expiryAction: actionOf(settings.expiresAt, settings.expiryAction ?? ""),
|
|
253
|
-
invitedOnly,
|
|
254
|
-
comments: audienceOf(commentAudience),
|
|
255
|
-
replacedInviteOnly: wasInvitedOnly === true && invitedOnly === false
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
const API_AUDIENCE = {
|
|
259
|
-
off: "Off",
|
|
260
|
-
invited: "Invited",
|
|
261
|
-
anyone: "Anyone"
|
|
262
|
-
};
|
|
263
|
-
function audienceOf(value) {
|
|
264
|
-
const lower = value?.toLowerCase();
|
|
265
|
-
return lower === "off" || lower === "invited" || lower === "anyone" ? lower : null;
|
|
266
|
-
}
|
|
267
|
-
async function setSitePaused(credentials, siteId, action) {
|
|
268
|
-
const state = await call(
|
|
269
|
-
credentials,
|
|
270
|
-
`sites/${encodeURIComponent(siteId)}/${action}`,
|
|
271
|
-
{ method: "POST" }
|
|
272
|
-
);
|
|
273
|
-
return {
|
|
274
|
-
siteId: state.siteId,
|
|
275
|
-
subdomain: state.subdomain,
|
|
276
|
-
status: enumName(state.status),
|
|
277
|
-
ownerPaused: state.ownerPaused
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
async function listTokens(credentials) {
|
|
281
|
-
const body = await call(credentials, "tokens");
|
|
282
|
-
return body.tokens;
|
|
283
|
-
}
|
|
284
|
-
const SITES_PER_REQUEST = 100;
|
|
285
|
-
async function listSites(credentials) {
|
|
286
|
-
const body = await call(
|
|
287
|
-
credentials,
|
|
288
|
-
`sites?pageSize=${SITES_PER_REQUEST}`
|
|
289
|
-
);
|
|
290
|
-
return { sites: body.sites, total: body.total };
|
|
291
|
-
}
|
|
292
|
-
async function createSite(credentials, subdomain, folderId) {
|
|
293
|
-
const created = await call(
|
|
294
|
-
credentials,
|
|
295
|
-
"sites",
|
|
296
|
-
{
|
|
297
|
-
method: "POST",
|
|
298
|
-
// Absent fields rather than null ones, because the API reads null, empty and missing the same
|
|
299
|
-
// way and a body carrying only what was asked for is the one that reads correctly in a log.
|
|
300
|
-
body: JSON.stringify({
|
|
301
|
-
...subdomain === void 0 ? {} : { subdomain },
|
|
302
|
-
...folderId === void 0 ? {} : { folderId }
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
|
-
);
|
|
306
|
-
return { ...created, name: null };
|
|
307
|
-
}
|
|
308
|
-
async function listFolders(credentials) {
|
|
309
|
-
const body = await call(credentials, "site-folders");
|
|
310
|
-
const byId = new Map(body.folders.map((folder) => [folder.folderId, folder]));
|
|
311
|
-
const pathOf = (folderId) => {
|
|
312
|
-
const names = [];
|
|
313
|
-
let cursor = byId.get(folderId);
|
|
314
|
-
while (cursor !== void 0 && names.length <= byId.size) {
|
|
315
|
-
names.unshift(cursor.name);
|
|
316
|
-
cursor = cursor.parentId === null ? void 0 : byId.get(cursor.parentId);
|
|
317
|
-
}
|
|
318
|
-
return names.join("/");
|
|
319
|
-
};
|
|
320
|
-
return body.folders.map((folder) => ({ folderId: folder.folderId, path: pathOf(folder.folderId) })).sort((a, b) => a.path.localeCompare(b.path));
|
|
321
|
-
}
|
|
322
|
-
function comparablePath(path) {
|
|
323
|
-
return path.split("/").map((name) => name.trim()).filter((name) => name !== "").join("/").toLowerCase();
|
|
324
|
-
}
|
|
325
|
-
const FOLDERS_LISTED_ON_MISS = 20;
|
|
326
|
-
async function resolveFolder(credentials, folder) {
|
|
327
|
-
const wanted = comparablePath(folder);
|
|
328
|
-
if (wanted === "") {
|
|
329
|
-
throw new Error(
|
|
330
|
-
`"${folder}" does not name a folder. Leave the folder out to put the site at the top level.`
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
const folders2 = await listFolders(credentials);
|
|
334
|
-
const byId = folders2.find((candidate) => candidate.folderId.toLowerCase() === wanted);
|
|
335
|
-
if (byId !== void 0) return byId.folderId;
|
|
336
|
-
const matches = folders2.filter((candidate) => comparablePath(candidate.path) === wanted);
|
|
337
|
-
if (matches.length === 1 && matches[0] !== void 0) return matches[0].folderId;
|
|
338
|
-
if (matches.length > 1) {
|
|
339
|
-
throw new Error(
|
|
340
|
-
`More than one folder is at "${folder}", because a folder name can contain "/". Name it by id instead: ${matches.map((match) => match.folderId).join(", ")}.`
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
if (folders2.length === 0) {
|
|
344
|
-
throw new Error(
|
|
345
|
-
`No folder of yours is called "${folder}" — this account has no folders yet. Make one in the dashboard at ${dashboardUrlFor(credentials.apiBaseUrl)}, or leave the folder out.`
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
const listed = folders2.slice(0, FOLDERS_LISTED_ON_MISS).map((candidate) => candidate.path);
|
|
349
|
-
const more = folders2.length - listed.length;
|
|
350
|
-
throw new Error(
|
|
351
|
-
`No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
const ROOT_FOLDER = "root";
|
|
355
|
-
async function resolveFolderTarget(credentials, folder) {
|
|
356
|
-
return folder.trim().toLowerCase() === ROOT_FOLDER ? ROOT_FOLDER : await resolveFolder(credentials, folder);
|
|
357
|
-
}
|
|
358
|
-
async function listComments(credentials, siteId, query = {}) {
|
|
359
|
-
const search = new URLSearchParams();
|
|
360
|
-
if (query.status !== void 0) search.set("status", query.status);
|
|
361
|
-
if (query.path !== void 0) search.set("path", query.path);
|
|
362
|
-
if (query.since !== void 0) search.set("since", String(query.since));
|
|
363
|
-
if (query.page !== void 0) search.set("page", String(query.page));
|
|
364
|
-
const encoded = search.toString();
|
|
365
|
-
const suffix = encoded === "" ? "" : `?${encoded}`;
|
|
366
|
-
return await call(
|
|
367
|
-
credentials,
|
|
368
|
-
`sites/${encodeURIComponent(siteId)}/feedback/threads${suffix}`
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
async function replyToComment(credentials, siteId, threadId, body, resolve2 = false) {
|
|
372
|
-
return await call(
|
|
373
|
-
credentials,
|
|
374
|
-
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/comments`,
|
|
375
|
-
{ method: "POST", body: JSON.stringify({ body, resolve: resolve2 }) }
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
async function setCommentResolved(credentials, siteId, threadId, resolved) {
|
|
379
|
-
return await call(
|
|
380
|
-
credentials,
|
|
381
|
-
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/${resolved ? "resolve" : "reopen"}`,
|
|
382
|
-
{ method: "POST" }
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
const PEOPLE_PER_REQUEST = 50;
|
|
386
|
-
async function listPeople(credentials, siteId) {
|
|
387
|
-
const page = await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers?pageSize=${PEOPLE_PER_REQUEST}`);
|
|
388
|
-
return {
|
|
389
|
-
invitedOnly: page.invitedOnly,
|
|
390
|
-
total: page.total,
|
|
391
|
-
limit: page.limit,
|
|
392
|
-
people: page.viewers.map((row) => ({
|
|
393
|
-
email: row.email,
|
|
394
|
-
role: enumName(row.role),
|
|
395
|
-
owner: row.owner,
|
|
396
|
-
status: enumName(row.status)
|
|
397
|
-
}))
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
async function invitePeople(credentials, siteId, emails, role) {
|
|
401
|
-
return await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers`, {
|
|
402
|
-
method: "POST",
|
|
403
|
-
body: JSON.stringify({ emails, role: role === "comment" ? "Comment" : "View", notify: true })
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
async function removePerson(credentials, siteId, email) {
|
|
407
|
-
await call(
|
|
408
|
-
credentials,
|
|
409
|
-
`sites/${encodeURIComponent(siteId)}/viewers?email=${encodeURIComponent(email)}`,
|
|
410
|
-
{ method: "DELETE" }
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
const VERSIONS_LISTED = 10;
|
|
414
|
-
function servingModeOf$1(site) {
|
|
415
|
-
if (site.spaMode) return "spa";
|
|
416
|
-
if (site.docsMode) return "docs";
|
|
417
|
-
return "static";
|
|
418
|
-
}
|
|
419
|
-
function accessOf(site) {
|
|
420
|
-
if (site.passwordProtected) return "password";
|
|
421
|
-
if (site.invitedOnly === true) return "invite-only";
|
|
422
|
-
if (site.invitedOnly === false) return "public";
|
|
423
|
-
return "no password (whether it is invite-only could not be read)";
|
|
424
|
-
}
|
|
425
|
-
function describeSite(site, folderPath) {
|
|
426
|
-
const unavailable = (available) => available ? "" : " (not on this plan)";
|
|
427
|
-
const versions = site.deploys.slice(0, VERSIONS_LISTED).map((deploy2) => {
|
|
428
|
-
const live = deploy2.deployId === site.liveDeployId ? " ← served" : "";
|
|
429
|
-
const kept = deploy2.filesKept ? "" : ", files collected — cannot be rolled back to";
|
|
430
|
-
return ` ${deploy2.deployId} ${deploy2.createdAt} ${deploy2.status}, ${deploy2.fileCount} files${kept}${live}`;
|
|
431
|
-
});
|
|
432
|
-
const more = site.deploys.length - versions.length;
|
|
433
|
-
return [
|
|
434
|
-
`${site.subdomain} — ${site.url}`,
|
|
435
|
-
` name ${site.name ?? "(none)"}`,
|
|
436
|
-
` status ${site.status}`,
|
|
437
|
-
` access ${accessOf(site)}`,
|
|
438
|
-
` mode ${servingModeOf$1(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
|
|
439
|
-
` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
|
|
440
|
-
` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
|
|
441
|
-
` comments ${site.comments ?? "unknown"}${unavailable(site.commentsAvailable)}`,
|
|
442
|
-
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
443
|
-
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
444
|
-
"",
|
|
445
|
-
site.deploys.length === 0 ? "No versions yet." : "Versions, newest first:",
|
|
446
|
-
...versions,
|
|
447
|
-
...more > 0 ? [` … and ${more} older`] : []
|
|
448
|
-
].join("\n");
|
|
449
|
-
}
|
|
450
|
-
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
451
|
-
function newAttempt() {
|
|
452
|
-
const verifier = randomBytes(32).toString("base64url");
|
|
453
|
-
return {
|
|
454
|
-
verifier,
|
|
455
|
-
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
456
|
-
state: randomBytes(16).toString("base64url")
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
function clientName(surface) {
|
|
460
|
-
const clean2 = (value) => value.trim().replace(/[^A-Za-z0-9._-]/g, "-");
|
|
461
|
-
const label = "";
|
|
462
|
-
const raw = clean2(hostname()).slice(0, 64 - label.length);
|
|
463
|
-
return `${raw === "" ? "drop2run" : raw}${label}`;
|
|
464
|
-
}
|
|
465
|
-
function openBrowser(url) {
|
|
466
|
-
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
467
|
-
try {
|
|
468
|
-
const child = spawn(command, [...args], { stdio: "ignore", detached: true });
|
|
469
|
-
child.unref();
|
|
470
|
-
child.on("error", () => {
|
|
471
|
-
});
|
|
472
|
-
return true;
|
|
473
|
-
} catch {
|
|
474
|
-
return false;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function listen(state) {
|
|
478
|
-
return new Promise((resolveListener, rejectListener) => {
|
|
479
|
-
let settle = null;
|
|
480
|
-
let fail = null;
|
|
481
|
-
const received = new Promise((resolveCode, rejectCode) => {
|
|
482
|
-
settle = resolveCode;
|
|
483
|
-
fail = rejectCode;
|
|
484
|
-
});
|
|
485
|
-
const server = createServer((request2, response) => {
|
|
486
|
-
const url = new URL(request2.url ?? "/", "http://127.0.0.1");
|
|
487
|
-
if (url.pathname !== "/cb") {
|
|
488
|
-
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
489
|
-
response.end("Not found.\n");
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
const code = url.searchParams.get("code") ?? "";
|
|
493
|
-
const returned = url.searchParams.get("state") ?? "";
|
|
494
|
-
if (returned !== state || code === "") {
|
|
495
|
-
respond(
|
|
496
|
-
response,
|
|
497
|
-
400,
|
|
498
|
-
"Sign-in could not be completed",
|
|
499
|
-
"This did not come from the sign-in that was started here. Nothing has been changed."
|
|
500
|
-
);
|
|
501
|
-
fail?.(new Error("The browser returned a response that did not match this sign-in."));
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
respond(response, 200, "Signed in", "You can close this tab now.");
|
|
505
|
-
settle?.({ code });
|
|
506
|
-
});
|
|
507
|
-
server.on("error", (error) => {
|
|
508
|
-
rejectListener(error);
|
|
509
|
-
fail?.(error instanceof Error ? error : new Error(String(error)));
|
|
510
|
-
});
|
|
511
|
-
server.listen(0, "127.0.0.1", () => {
|
|
512
|
-
const address = server.address();
|
|
513
|
-
if (address === null) {
|
|
514
|
-
rejectListener(new Error("Could not open a local port to receive the sign-in."));
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
resolveListener({
|
|
518
|
-
port: address.port,
|
|
519
|
-
received,
|
|
520
|
-
close: () => server.close()
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
function respond(response, status, title, detail) {
|
|
526
|
-
response.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
527
|
-
response.end(
|
|
528
|
-
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title} — drop2run</title><style>body{font:16px/1.6 system-ui,sans-serif;margin:20vh auto;max-width:28rem;padding:0 1.5rem;color:#12211f}h1{font-size:1.4rem;margin:0 0 .5rem}p{margin:0;color:#64706e}@media(prefers-color-scheme:dark){body{background:#0d1413;color:#e8efee}p{color:#9aa8a6}}</style></head><body><h1>${title}</h1><p>${detail}</p></body></html>
|
|
529
|
-
`
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
async function waitForCallback(received, timeoutMs = TIMEOUT_MS) {
|
|
533
|
-
let timer;
|
|
534
|
-
try {
|
|
535
|
-
return await Promise.race([
|
|
536
|
-
received,
|
|
537
|
-
new Promise((_, reject) => {
|
|
538
|
-
timer = setTimeout(
|
|
539
|
-
() => reject(new Error("Timed out waiting for the browser. Nothing has been changed.")),
|
|
540
|
-
timeoutMs
|
|
541
|
-
);
|
|
542
|
-
})
|
|
543
|
-
]);
|
|
544
|
-
} finally {
|
|
545
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
function consentUrl(dashboardUrl, attempt, port, name) {
|
|
549
|
-
const url = new URL("/cli-auth", dashboardUrl);
|
|
550
|
-
url.searchParams.set("challenge", attempt.challenge);
|
|
551
|
-
url.searchParams.set("state", attempt.state);
|
|
552
|
-
url.searchParams.set("port", String(port));
|
|
553
|
-
url.searchParams.set("name", name);
|
|
554
|
-
return url.toString();
|
|
555
|
-
}
|
|
556
|
-
async function startDevice(apiBaseUrl, clientName2) {
|
|
557
|
-
const response = await fetch(`${apiBaseUrl}/auth/cli/device`, {
|
|
558
|
-
method: "POST",
|
|
559
|
-
headers: { "Content-Type": "application/json" },
|
|
560
|
-
body: JSON.stringify({ clientName: clientName2 })
|
|
561
|
-
});
|
|
562
|
-
if (!response.ok) throw new Error(await problemDetail(response));
|
|
563
|
-
return await response.json();
|
|
564
|
-
}
|
|
565
|
-
async function pollDevice(apiBaseUrl, deviceCode) {
|
|
566
|
-
let response;
|
|
567
|
-
try {
|
|
568
|
-
response = await fetch(`${apiBaseUrl}/auth/cli/device/token`, {
|
|
569
|
-
method: "POST",
|
|
570
|
-
headers: { "Content-Type": "application/json" },
|
|
571
|
-
body: JSON.stringify({ deviceCode })
|
|
572
|
-
});
|
|
573
|
-
} catch {
|
|
574
|
-
return { state: "pending" };
|
|
575
|
-
}
|
|
576
|
-
if (response.ok) {
|
|
577
|
-
const body = await response.json();
|
|
578
|
-
return { state: "granted", token: { token: body.token, name: body.name, email: body.email } };
|
|
579
|
-
}
|
|
580
|
-
const problem = await response.json().catch(() => null);
|
|
581
|
-
if (problem?.type === "authorization_pending") return { state: "pending" };
|
|
582
|
-
if (problem?.type === "slow_down") return { state: "slow_down" };
|
|
583
|
-
return {
|
|
584
|
-
state: "dead",
|
|
585
|
-
message: problem?.detail ?? `The API answered ${response.status}.`
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
async function problemDetail(response) {
|
|
589
|
-
const problem = await response.json().catch(() => null);
|
|
590
|
-
return problem?.detail ?? `The API answered ${response.status}.`;
|
|
591
|
-
}
|
|
592
|
-
async function exchange(apiBaseUrl, code, verifier) {
|
|
593
|
-
const response = await fetch(`${apiBaseUrl}/auth/cli/token`, {
|
|
594
|
-
method: "POST",
|
|
595
|
-
headers: { "Content-Type": "application/json" },
|
|
596
|
-
body: JSON.stringify({ code, verifier })
|
|
597
|
-
});
|
|
598
|
-
if (!response.ok) throw new Error(await problemDetail(response));
|
|
599
|
-
const body = await response.json();
|
|
600
|
-
return { token: body.token, name: body.name, email: body.email };
|
|
601
|
-
}
|
|
602
|
-
const PROJECT_FILE = "drop2run.json";
|
|
603
|
-
function projectPath(directory = process.cwd()) {
|
|
604
|
-
return join(directory, PROJECT_FILE);
|
|
605
|
-
}
|
|
606
|
-
function readProject(directory = process.cwd(), read = (path) => readFileSync(path, "utf8")) {
|
|
607
|
-
let parsed;
|
|
608
|
-
try {
|
|
609
|
-
parsed = JSON.parse(read(projectPath(directory)));
|
|
610
|
-
} catch {
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
if (typeof parsed.siteId !== "string" || parsed.siteId === "") return null;
|
|
614
|
-
return {
|
|
615
|
-
siteId: parsed.siteId,
|
|
616
|
-
subdomain: typeof parsed.subdomain === "string" ? parsed.subdomain : "",
|
|
617
|
-
dir: typeof parsed.dir === "string" && parsed.dir !== "" ? parsed.dir : "."
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
function writeProject(project, directory = process.cwd()) {
|
|
621
|
-
const path = projectPath(directory);
|
|
622
|
-
writeFileSync(path, `${JSON.stringify(project, null, " ")}
|
|
623
|
-
`);
|
|
624
|
-
return path;
|
|
625
|
-
}
|
|
626
|
-
function sizeOf(content) {
|
|
627
|
-
return ArrayBuffer.isView(content) ? content.byteLength : content.size;
|
|
628
|
-
}
|
|
629
|
-
async function bytesOf(content) {
|
|
630
|
-
return ArrayBuffer.isView(content) ? content : new Uint8Array(await content.arrayBuffer());
|
|
631
|
-
}
|
|
632
|
-
class DeployError extends Error {
|
|
633
|
-
/**
|
|
634
|
-
* @param code Stable code, either an API error type (`quota_exceeded`, `unsafe_path`, …) or one of
|
|
635
|
-
* the client-side codes in {@link ClientErrorCode}.
|
|
636
|
-
* @param message Text safe to show the user.
|
|
637
|
-
* @param detail Anything extra worth logging, such as the RFC 9457 body from the API.
|
|
638
|
-
*/
|
|
639
|
-
constructor(code, message, detail) {
|
|
640
|
-
super(message);
|
|
641
|
-
this.code = code;
|
|
642
|
-
this.detail = detail;
|
|
643
|
-
this.name = "DeployError";
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
const ClientErrorCode = {
|
|
647
|
-
/** The drop held no files once junk was filtered out. */
|
|
648
|
-
Empty: "empty_drop",
|
|
649
|
-
/** No `index.html` at the root of the drop. */
|
|
650
|
-
MissingIndex: "missing_index",
|
|
651
|
-
/** The drop is larger than the plan allows; the server enforces this too. */
|
|
652
|
-
TooLarge: "too_large",
|
|
653
|
-
/** A single file is larger than the plan allows. */
|
|
654
|
-
FileTooLarge: "file_too_large",
|
|
655
|
-
/** More files than the plan allows. */
|
|
656
|
-
TooManyFiles: "too_many_files",
|
|
657
|
-
/**
|
|
658
|
-
* A zip claims to expand to more than the plan allows.
|
|
659
|
-
*
|
|
660
|
-
* Distinct from {@link TooLarge}, which is about a drop whose files have already been read. This one
|
|
661
|
-
* is raised from the archive's own directory before anything is decompressed, so the numbers in it
|
|
662
|
-
* are what the zip declared rather than what was measured.
|
|
663
|
-
*/
|
|
664
|
-
ZipTooLarge: "zip_too_large",
|
|
665
|
-
/** The user cancelled. */
|
|
666
|
-
Cancelled: "cancelled",
|
|
667
|
-
/** An upload kept failing after every retry. */
|
|
668
|
-
UploadFailed: "upload_failed",
|
|
669
|
-
/**
|
|
670
|
-
* The browser could not hold what was dropped in memory. Raised when allocating the buffer for a
|
|
671
|
-
* large zip or file fails, which a browser reports as a `RangeError` rather than as anything that
|
|
672
|
-
* names memory.
|
|
673
|
-
*/
|
|
674
|
-
OutOfMemory: "out_of_memory",
|
|
675
|
-
/**
|
|
676
|
-
* A dropped file could not be read when it came to be hashed. Files from a folder stay on disk until
|
|
677
|
-
* then, so one moved, deleted or changed after the drop fails here rather than at the drop.
|
|
678
|
-
*/
|
|
679
|
-
ReadFailed: "read_failed",
|
|
680
|
-
/**
|
|
681
|
-
* A control-plane call got no answer: the connection failed before a response arrived, or the
|
|
682
|
-
* request timeout passed. Distinct from an HTTP error, which is an answer — this one says nothing
|
|
683
|
-
* about whether the server acted, which is why only an idempotent call may retry on it.
|
|
684
|
-
*/
|
|
685
|
-
NetworkFailed: "network_failed"
|
|
686
|
-
};
|
|
687
|
-
function describeError(error) {
|
|
688
|
-
const parts = [];
|
|
689
|
-
let current = error;
|
|
690
|
-
for (let depth = 0; depth < 5 && current !== void 0 && current !== null; depth++) {
|
|
691
|
-
if (current instanceof Error) {
|
|
692
|
-
const code = current.code;
|
|
693
|
-
parts.push(typeof code === "string" ? `${current.message} (${code})` : current.message);
|
|
694
|
-
current = current.cause;
|
|
695
|
-
} else {
|
|
696
|
-
parts.push(String(current));
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return parts.join(" ← ");
|
|
701
|
-
}
|
|
702
|
-
const ACCOUNT_HEADER = "X-Drop2Run-Account";
|
|
703
|
-
const CLAIM_TOKEN_HEADER = "X-Claim-Token";
|
|
704
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
705
|
-
const COMPLETE_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
706
|
-
async function prepareDeploy(siteId, files, options, signal) {
|
|
707
|
-
const manifest = files.map((file) => ({
|
|
708
|
-
path: file.path,
|
|
709
|
-
sha256: file.sha256,
|
|
710
|
-
size: sizeOf(file.bytes)
|
|
711
|
-
}));
|
|
712
|
-
const prepared = await request(
|
|
713
|
-
`sites/${encodeURIComponent(siteId)}/deploys/prepare`,
|
|
714
|
-
// `batches` asks for batch permits; an API that predates them ignores the field, and one that
|
|
715
|
-
// knows them answers with single permits until they are switched on.
|
|
716
|
-
{ files: manifest, batches: true },
|
|
717
|
-
options,
|
|
718
|
-
signal
|
|
719
|
-
);
|
|
720
|
-
return { ...prepared, uploadUrl: absolute(prepared.uploadUrl, options.baseUrl) };
|
|
721
|
-
}
|
|
722
|
-
function absolute(uploadUrl, baseUrl) {
|
|
723
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(uploadUrl)) return uploadUrl;
|
|
724
|
-
if (baseUrl === void 0 || !/^[a-z][a-z0-9+.-]*:/i.test(baseUrl)) return uploadUrl;
|
|
725
|
-
return new URL(uploadUrl, baseUrl).toString();
|
|
726
|
-
}
|
|
727
|
-
async function completeDeploy(siteId, deployId, options, signal, name) {
|
|
728
|
-
const path = `sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/complete`;
|
|
729
|
-
const body = name === null || name === void 0 ? void 0 : { name };
|
|
730
|
-
const delay = options.delay ?? wait;
|
|
731
|
-
for (let attempt = 0; ; attempt++) {
|
|
732
|
-
try {
|
|
733
|
-
return await request(path, body, options, signal);
|
|
734
|
-
} catch (error) {
|
|
735
|
-
const unanswered = error instanceof DeployError && error.code === ClientErrorCode.NetworkFailed;
|
|
736
|
-
if (!unanswered || attempt >= COMPLETE_RETRY_DELAYS_MS.length || signal?.aborted) throw error;
|
|
737
|
-
await delay(COMPLETE_RETRY_DELAYS_MS[attempt] ?? 1e3);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
function wait(ms) {
|
|
742
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
743
|
-
}
|
|
744
|
-
async function request(path, body, options, signal) {
|
|
745
|
-
const doFetch = options.fetch ?? globalFetch$1();
|
|
746
|
-
const base = options.baseUrl ?? "/api";
|
|
747
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
748
|
-
const headers = {};
|
|
749
|
-
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
750
|
-
if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
|
|
751
|
-
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
752
|
-
if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
|
|
753
|
-
const controller = new AbortController();
|
|
754
|
-
let timedOut = false;
|
|
755
|
-
const timer = setTimeout(() => {
|
|
756
|
-
timedOut = true;
|
|
757
|
-
controller.abort();
|
|
758
|
-
}, timeoutMs);
|
|
759
|
-
try {
|
|
760
|
-
const response = await doFetch(`${base}/${path}`, {
|
|
761
|
-
method: "POST",
|
|
762
|
-
headers,
|
|
763
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
764
|
-
signal: controller.signal
|
|
765
|
-
});
|
|
766
|
-
if (!response.ok) throw await problemToError(response);
|
|
767
|
-
return await response.json();
|
|
768
|
-
} catch (error) {
|
|
769
|
-
if (error instanceof DeployError || signal?.aborted) throw error;
|
|
770
|
-
if (timedOut) {
|
|
771
|
-
throw new DeployError(
|
|
772
|
-
ClientErrorCode.NetworkFailed,
|
|
773
|
-
`The server did not answer within ${Math.round(timeoutMs / 1e3)} seconds.`,
|
|
774
|
-
{ path, cause: describeError(error) }
|
|
775
|
-
);
|
|
776
|
-
}
|
|
777
|
-
if (error instanceof TypeError) {
|
|
778
|
-
throw new DeployError(ClientErrorCode.NetworkFailed, "Could not reach the server.", {
|
|
779
|
-
path,
|
|
780
|
-
cause: describeError(error)
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
throw error;
|
|
784
|
-
} finally {
|
|
785
|
-
clearTimeout(timer);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
function globalFetch$1() {
|
|
789
|
-
return globalThis.fetch.bind(globalThis);
|
|
790
|
-
}
|
|
791
|
-
async function problemToError(response) {
|
|
792
|
-
let problem = {};
|
|
793
|
-
try {
|
|
794
|
-
problem = await response.json();
|
|
795
|
-
} catch {
|
|
796
|
-
return new DeployError(
|
|
797
|
-
`http_${response.status}`,
|
|
798
|
-
`The server responded with ${response.status}.`
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
const code = typeof problem.type === "string" ? problem.type : `http_${response.status}`;
|
|
802
|
-
const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
|
|
803
|
-
return new DeployError(code, message, problem);
|
|
804
|
-
}
|
|
805
|
-
function batchLayout(members) {
|
|
806
|
-
return members.map((member) => `${member.sha256} ${member.size}
|
|
807
|
-
`).join("");
|
|
808
|
-
}
|
|
809
|
-
function isBatch(target) {
|
|
810
|
-
return "files" in target;
|
|
811
|
-
}
|
|
812
|
-
function parseBatchResult(body) {
|
|
813
|
-
try {
|
|
814
|
-
const parsed = JSON.parse(body);
|
|
815
|
-
if (parsed === null || typeof parsed !== "object") return null;
|
|
816
|
-
const { stored, present, failed } = parsed;
|
|
817
|
-
if (typeof stored !== "number" || typeof present !== "number" || !Array.isArray(failed))
|
|
818
|
-
return null;
|
|
819
|
-
return { stored, present, failed };
|
|
285
|
+
}
|
|
286
|
+
const code = typeof problem.type === "string" ? problem.type : `http_${response.status}`;
|
|
287
|
+
const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
|
|
288
|
+
return new DeployError(code, message, problem);
|
|
289
|
+
}
|
|
290
|
+
function batchLayout(members) {
|
|
291
|
+
return members.map((member) => `${member.sha256} ${member.size}
|
|
292
|
+
`).join("");
|
|
293
|
+
}
|
|
294
|
+
function isBatch(target) {
|
|
295
|
+
return "files" in target;
|
|
296
|
+
}
|
|
297
|
+
function parseBatchResult(body) {
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(body);
|
|
300
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
301
|
+
const { stored, present, failed } = parsed;
|
|
302
|
+
if (typeof stored !== "number" || typeof present !== "number" || !Array.isArray(failed))
|
|
303
|
+
return null;
|
|
304
|
+
return { stored, present, failed };
|
|
820
305
|
} catch {
|
|
821
306
|
return null;
|
|
822
307
|
}
|
|
@@ -916,7 +401,7 @@ const FILE_TYPES = [
|
|
|
916
401
|
{
|
|
917
402
|
ext: ".csv",
|
|
918
403
|
mime: "text/csv; charset=utf-8",
|
|
919
|
-
kind: "
|
|
404
|
+
kind: "csv",
|
|
920
405
|
document: false,
|
|
921
406
|
reader: true,
|
|
922
407
|
editable: true
|
|
@@ -1326,963 +811,1528 @@ async function sha256Hex(bytes) {
|
|
|
1326
811
|
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
1327
812
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1328
813
|
}
|
|
1329
|
-
const HASH_CONCURRENCY = 4;
|
|
1330
|
-
const HASH_BYTES_IN_FLIGHT = 16 * 1024 * 1024;
|
|
1331
|
-
async function hashAll(files, onProgress, signal) {
|
|
1332
|
-
const budget = byteBudget(HASH_BYTES_IN_FLIGHT);
|
|
1333
|
-
let done = 0;
|
|
1334
|
-
return mapInOrder(
|
|
1335
|
-
files,
|
|
1336
|
-
HASH_CONCURRENCY,
|
|
1337
|
-
async (file) => {
|
|
1338
|
-
const size = sizeOf(file.bytes);
|
|
1339
|
-
await budget.acquire(size);
|
|
1340
|
-
try {
|
|
1341
|
-
signal?.throwIfAborted();
|
|
1342
|
-
const hashed = { ...file, sha256: await sha256Hex(await readForHash(file)) };
|
|
1343
|
-
done += 1;
|
|
1344
|
-
onProgress?.(done, files.length);
|
|
1345
|
-
return hashed;
|
|
1346
|
-
} finally {
|
|
1347
|
-
budget.release(size);
|
|
814
|
+
const HASH_CONCURRENCY = 4;
|
|
815
|
+
const HASH_BYTES_IN_FLIGHT = 16 * 1024 * 1024;
|
|
816
|
+
async function hashAll(files, onProgress, signal) {
|
|
817
|
+
const budget = byteBudget(HASH_BYTES_IN_FLIGHT);
|
|
818
|
+
let done = 0;
|
|
819
|
+
return mapInOrder(
|
|
820
|
+
files,
|
|
821
|
+
HASH_CONCURRENCY,
|
|
822
|
+
async (file) => {
|
|
823
|
+
const size = sizeOf(file.bytes);
|
|
824
|
+
await budget.acquire(size);
|
|
825
|
+
try {
|
|
826
|
+
signal?.throwIfAborted();
|
|
827
|
+
const hashed = { ...file, sha256: await sha256Hex(await readForHash(file)) };
|
|
828
|
+
done += 1;
|
|
829
|
+
onProgress?.(done, files.length);
|
|
830
|
+
return hashed;
|
|
831
|
+
} finally {
|
|
832
|
+
budget.release(size);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
function byteBudget(limit) {
|
|
838
|
+
let held = 0;
|
|
839
|
+
let waiting = [];
|
|
840
|
+
return {
|
|
841
|
+
async acquire(size) {
|
|
842
|
+
while (held > 0 && held + size > limit) {
|
|
843
|
+
await new Promise((resolve2) => waiting.push(resolve2));
|
|
844
|
+
}
|
|
845
|
+
held += size;
|
|
846
|
+
},
|
|
847
|
+
release(size) {
|
|
848
|
+
held -= size;
|
|
849
|
+
const woken = waiting;
|
|
850
|
+
waiting = [];
|
|
851
|
+
for (const wake of woken) wake();
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
async function readForHash(file) {
|
|
856
|
+
try {
|
|
857
|
+
return await bytesOf(file.bytes);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (error instanceof RangeError) throw error;
|
|
860
|
+
throw new DeployError(
|
|
861
|
+
ClientErrorCode.ReadFailed,
|
|
862
|
+
`Could not read ${file.path}. It may have been moved, renamed or changed since you dropped it — drop the folder again.`,
|
|
863
|
+
{ path: file.path, cause: describeError(error) }
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const IGNORED_EXACT = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".gitignore", ".gitkeep"]);
|
|
868
|
+
const IGNORED_DIRECTORIES = [
|
|
869
|
+
"__MACOSX",
|
|
870
|
+
".git",
|
|
871
|
+
"node_modules",
|
|
872
|
+
".svn",
|
|
873
|
+
".hg",
|
|
874
|
+
".idea",
|
|
875
|
+
".vscode",
|
|
876
|
+
".claude",
|
|
877
|
+
".agents",
|
|
878
|
+
".cursor",
|
|
879
|
+
".codex",
|
|
880
|
+
".gemini"
|
|
881
|
+
];
|
|
882
|
+
function isEnvironmentFile(name) {
|
|
883
|
+
return name === ".env" || name === ".envrc" || name.startsWith(".env.");
|
|
884
|
+
}
|
|
885
|
+
function shouldIgnore(path) {
|
|
886
|
+
const segments = path.split("/");
|
|
887
|
+
const name = segments.at(-1) ?? "";
|
|
888
|
+
if (name === "" || IGNORED_EXACT.has(name) || isEnvironmentFile(name)) return true;
|
|
889
|
+
if (name.startsWith("._")) return true;
|
|
890
|
+
return segments.some((segment) => IGNORED_DIRECTORIES.includes(segment));
|
|
891
|
+
}
|
|
892
|
+
const REQUIRED_INDEX = "index.html";
|
|
893
|
+
const DOCUMENT_EXTENSIONS = DOCUMENT_EXTENSIONS$1;
|
|
894
|
+
function isDocumentPath(path) {
|
|
895
|
+
const lower = path.toLowerCase();
|
|
896
|
+
return DOCUMENT_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
897
|
+
}
|
|
898
|
+
const VIEWABLE_EXTENSIONS = VIEWABLE_EXTENSIONS$1;
|
|
899
|
+
function isViewablePath(path) {
|
|
900
|
+
const lower = path.toLowerCase();
|
|
901
|
+
return VIEWABLE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
902
|
+
}
|
|
903
|
+
function isImageFolder(paths) {
|
|
904
|
+
let pictures = false;
|
|
905
|
+
for (const path of paths) {
|
|
906
|
+
const ext = extensionOf(path);
|
|
907
|
+
if (WEB_ASSET_EXTENSIONS.includes(ext)) return false;
|
|
908
|
+
if (GALLERY_EXTENSIONS.includes(ext)) pictures = true;
|
|
909
|
+
}
|
|
910
|
+
return pictures;
|
|
911
|
+
}
|
|
912
|
+
function canPublish(paths) {
|
|
913
|
+
if (paths.some((path) => path === REQUIRED_INDEX)) return true;
|
|
914
|
+
if (paths.some(isDocumentPath)) return true;
|
|
915
|
+
if (isImageFolder(paths)) return true;
|
|
916
|
+
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
917
|
+
}
|
|
918
|
+
function servingModeOf$1(flags) {
|
|
919
|
+
if (flags.docsMode) return "docs";
|
|
920
|
+
if (flags.spaMode) return "spa";
|
|
921
|
+
return "static";
|
|
922
|
+
}
|
|
923
|
+
function predictedModeOf(paths) {
|
|
924
|
+
if (paths.some((path) => path === REQUIRED_INDEX)) return "static";
|
|
925
|
+
if (paths.some(isDocumentPath)) return "docs";
|
|
926
|
+
if (isImageFolder(paths)) return "docs";
|
|
927
|
+
if (paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0])) return "docs";
|
|
928
|
+
return "static";
|
|
929
|
+
}
|
|
930
|
+
function nestedIndexOf(paths) {
|
|
931
|
+
if (predictedModeOf(paths) !== "docs") return null;
|
|
932
|
+
const suffix = `/${REQUIRED_INDEX}`;
|
|
933
|
+
const folders2 = paths.filter((path) => path.endsWith(suffix)).map((path) => path.slice(0, -suffix.length)).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
|
|
934
|
+
return folders2[0] ?? null;
|
|
935
|
+
}
|
|
936
|
+
function renameOfLoneHtmlPage(paths) {
|
|
937
|
+
if (paths.length !== 1) return null;
|
|
938
|
+
const [only] = paths;
|
|
939
|
+
if (only === void 0 || only === REQUIRED_INDEX) return null;
|
|
940
|
+
const lower = only.toLowerCase();
|
|
941
|
+
return lower.endsWith(".html") || lower.endsWith(".htm") ? only : null;
|
|
942
|
+
}
|
|
943
|
+
function formatBytes$1(bytes) {
|
|
944
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
945
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
946
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
947
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
948
|
+
}
|
|
949
|
+
function checkLimits(files, limits) {
|
|
950
|
+
if (files.length === 0) {
|
|
951
|
+
throw new DeployError(
|
|
952
|
+
ClientErrorCode.Empty,
|
|
953
|
+
"That drop contained no files to deploy. Folders like .git and node_modules are skipped."
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (!canPublish(files.map((file) => file.path))) {
|
|
957
|
+
throw new DeployError(
|
|
958
|
+
ClientErrorCode.MissingIndex,
|
|
959
|
+
`A site needs an ${REQUIRED_INDEX} at its top level, or at least one .md or .pdf file to publish as a documents site — or be a single file that can be displayed on its own, such as a note, a PDF, a page or an image. Drop the folder that contains it, not the folder above.`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const refusal = sizeRefusal(
|
|
963
|
+
files.map((file) => ({ path: file.path, size: sizeOf(file.bytes) })),
|
|
964
|
+
limits
|
|
965
|
+
);
|
|
966
|
+
if (refusal) throw refusal;
|
|
967
|
+
}
|
|
968
|
+
function sizeRefusal(files, limits) {
|
|
969
|
+
if (!limits) return null;
|
|
970
|
+
if (files.length > limits.maxFiles) {
|
|
971
|
+
return new DeployError(
|
|
972
|
+
ClientErrorCode.TooManyFiles,
|
|
973
|
+
`That is ${files.length} files, and this plan allows ${limits.maxFiles}.`,
|
|
974
|
+
{ limit: limits.maxFiles, actual: files.length }
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const oversized = files.find((file) => file.size > limits.maxFileBytes);
|
|
978
|
+
if (oversized) {
|
|
979
|
+
return new DeployError(
|
|
980
|
+
ClientErrorCode.FileTooLarge,
|
|
981
|
+
`${oversized.path} is ${formatBytes$1(oversized.size)}, and this plan allows ${formatBytes$1(limits.maxFileBytes)} per file.`,
|
|
982
|
+
{
|
|
983
|
+
path: oversized.path,
|
|
984
|
+
limit: limits.maxFileBytes,
|
|
985
|
+
actual: oversized.size
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
990
|
+
if (limits.maxSiteBytes !== null && total > limits.maxSiteBytes) {
|
|
991
|
+
return new DeployError(
|
|
992
|
+
ClientErrorCode.TooLarge,
|
|
993
|
+
`That drop is ${formatBytes$1(total)}, and this plan allows ${formatBytes$1(limits.maxSiteBytes)}.`,
|
|
994
|
+
{ limit: limits.maxSiteBytes, actual: total }
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
const MAX_NAME_LENGTH = 60;
|
|
1000
|
+
const HEAD_BYTES = 64 * 1024;
|
|
1001
|
+
const TITLE_SEPARATORS = ["|", "·", "—", "–", " - "];
|
|
1002
|
+
function suggestSiteName(files) {
|
|
1003
|
+
const index = files.find((file) => file.path === "index.html");
|
|
1004
|
+
if (index === void 0 || !ArrayBuffer.isView(index.bytes)) return null;
|
|
1005
|
+
try {
|
|
1006
|
+
return nameFromHtml(decodeHead(index.bytes));
|
|
1007
|
+
} catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function readSiteName(files) {
|
|
1012
|
+
const index = files.find((file) => file.path === "index.html");
|
|
1013
|
+
if (index === void 0) return null;
|
|
1014
|
+
if (ArrayBuffer.isView(index.bytes)) return suggestSiteName(files);
|
|
1015
|
+
try {
|
|
1016
|
+
const head = new Uint8Array(await index.bytes.slice(0, HEAD_BYTES).arrayBuffer());
|
|
1017
|
+
return suggestSiteName([{ path: index.path, bytes: head }]);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function decodeHead(bytes) {
|
|
1023
|
+
return new TextDecoder("utf-8", { fatal: false, ignoreBOM: false }).decode(
|
|
1024
|
+
bytes.subarray(0, HEAD_BYTES)
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
function nameFromHtml(html) {
|
|
1028
|
+
const document = parse(html);
|
|
1029
|
+
const candidates = document ? [
|
|
1030
|
+
document.querySelector('meta[property="og:site_name"]')?.getAttribute("content"),
|
|
1031
|
+
document.querySelector("title")?.textContent,
|
|
1032
|
+
document.querySelector("h1")?.textContent
|
|
1033
|
+
] : [matchOgSiteName(html), matchTag(html, "title"), matchTag(html, "h1")];
|
|
1034
|
+
for (const candidate of candidates) {
|
|
1035
|
+
const name = clean(candidate);
|
|
1036
|
+
if (name !== null) return name;
|
|
1037
|
+
}
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
function parse(html) {
|
|
1041
|
+
const parser = globalThis.DOMParser;
|
|
1042
|
+
if (parser === void 0) return null;
|
|
1043
|
+
try {
|
|
1044
|
+
return new parser().parseFromString(html, "text/html");
|
|
1045
|
+
} catch {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function matchTag(html, tag) {
|
|
1050
|
+
const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}\\s*>`, "i").exec(html);
|
|
1051
|
+
return match?.[1] === void 0 ? null : decodeEntities(match[1]);
|
|
1052
|
+
}
|
|
1053
|
+
function matchOgSiteName(html) {
|
|
1054
|
+
const patterns = [
|
|
1055
|
+
/<meta[^>]*property\s*=\s*["']og:site_name["'][^>]*content\s*=\s*["']([^"']*)["']/i,
|
|
1056
|
+
/<meta[^>]*content\s*=\s*["']([^"']*)["'][^>]*property\s*=\s*["']og:site_name["']/i
|
|
1057
|
+
];
|
|
1058
|
+
for (const pattern of patterns) {
|
|
1059
|
+
const match = pattern.exec(html);
|
|
1060
|
+
if (match?.[1] !== void 0) return decodeEntities(match[1]);
|
|
1061
|
+
}
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
const LATIN1_ENTITIES = "nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr deg plusmn sup2 sup3 acute micro para middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest Agrave Aacute Acirc Atilde Auml Aring AElig Ccedil Egrave Eacute Ecirc Euml Igrave Iacute Icirc Iuml ETH Ntilde Ograve Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc Uuml Yacute THORN szlig agrave aacute acirc atilde auml aring aelig ccedil egrave eacute ecirc euml igrave iacute icirc iuml eth ntilde ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc uuml yacute thorn yuml".split(" ");
|
|
1065
|
+
const NAMED_ENTITIES = new Map([
|
|
1066
|
+
...LATIN1_ENTITIES.map((name, index) => [name, 160 + index]),
|
|
1067
|
+
["amp", 38],
|
|
1068
|
+
["lt", 60],
|
|
1069
|
+
["gt", 62],
|
|
1070
|
+
["quot", 34],
|
|
1071
|
+
["apos", 39],
|
|
1072
|
+
["ensp", 8194],
|
|
1073
|
+
["emsp", 8195],
|
|
1074
|
+
["thinsp", 8201],
|
|
1075
|
+
["ndash", 8211],
|
|
1076
|
+
["mdash", 8212],
|
|
1077
|
+
["lsquo", 8216],
|
|
1078
|
+
["rsquo", 8217],
|
|
1079
|
+
["sbquo", 8218],
|
|
1080
|
+
["ldquo", 8220],
|
|
1081
|
+
["rdquo", 8221],
|
|
1082
|
+
["bdquo", 8222],
|
|
1083
|
+
["dagger", 8224],
|
|
1084
|
+
["Dagger", 8225],
|
|
1085
|
+
["bull", 8226],
|
|
1086
|
+
["hellip", 8230],
|
|
1087
|
+
["prime", 8242],
|
|
1088
|
+
["lsaquo", 8249],
|
|
1089
|
+
["rsaquo", 8250],
|
|
1090
|
+
["euro", 8364],
|
|
1091
|
+
["trade", 8482],
|
|
1092
|
+
["larr", 8592],
|
|
1093
|
+
["rarr", 8594],
|
|
1094
|
+
["hearts", 9829]
|
|
1095
|
+
]);
|
|
1096
|
+
function decodeEntities(text) {
|
|
1097
|
+
return text.replace(
|
|
1098
|
+
/&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g,
|
|
1099
|
+
(whole, decimal, hex, name) => {
|
|
1100
|
+
if (name !== void 0) {
|
|
1101
|
+
const code2 = NAMED_ENTITIES.get(name);
|
|
1102
|
+
return code2 === void 0 ? whole : String.fromCodePoint(code2);
|
|
1348
1103
|
}
|
|
1104
|
+
const code = decimal !== void 0 ? Number(decimal) : Number.parseInt(hex ?? "", 16);
|
|
1105
|
+
const valid = code > 0 && code <= 1114111 && (code < 55296 || code > 57343);
|
|
1106
|
+
return String.fromCodePoint(valid ? code : 65533);
|
|
1349
1107
|
}
|
|
1350
1108
|
);
|
|
1351
1109
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1110
|
+
const INVISIBLE = new RegExp(
|
|
1111
|
+
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}${String.fromCharCode(8203)}-${String.fromCharCode(8207)}${String.fromCharCode(8232)}-${String.fromCharCode(8238)}${String.fromCharCode(8288)}-${String.fromCharCode(8292)}${String.fromCharCode(8294)}-${String.fromCharCode(8303)}${String.fromCharCode(65279)}]`,
|
|
1112
|
+
"g"
|
|
1113
|
+
);
|
|
1114
|
+
function clean(raw) {
|
|
1115
|
+
if (raw === null || raw === void 0) return null;
|
|
1116
|
+
if (raw.includes("<") || raw.includes(">")) return null;
|
|
1117
|
+
const collapsed = raw.replace(INVISIBLE, " ").replace(/\s+/g, " ").trim();
|
|
1118
|
+
const name = trimSuffix(collapsed);
|
|
1119
|
+
if (name.length === 0) return null;
|
|
1120
|
+
if (PLACEHOLDERS.has(name.toLowerCase())) return null;
|
|
1121
|
+
return name.length > MAX_NAME_LENGTH ? name.slice(0, MAX_NAME_LENGTH).trimEnd() : name;
|
|
1122
|
+
}
|
|
1123
|
+
const PLACEHOLDERS = /* @__PURE__ */ new Set([
|
|
1124
|
+
"document",
|
|
1125
|
+
"untitled",
|
|
1126
|
+
"untitled document",
|
|
1127
|
+
"index",
|
|
1128
|
+
"home",
|
|
1129
|
+
"new page",
|
|
1130
|
+
"my site",
|
|
1131
|
+
"vite app",
|
|
1132
|
+
"vite + react",
|
|
1133
|
+
"react app",
|
|
1134
|
+
"create next app",
|
|
1135
|
+
"svelte app",
|
|
1136
|
+
"webpack app",
|
|
1137
|
+
"hello world",
|
|
1138
|
+
"title"
|
|
1139
|
+
]);
|
|
1140
|
+
function trimSuffix(title) {
|
|
1141
|
+
for (const separator of TITLE_SEPARATORS) {
|
|
1142
|
+
const at = title.lastIndexOf(separator);
|
|
1143
|
+
if (at <= 0) continue;
|
|
1144
|
+
const before = title.slice(0, at).trim();
|
|
1145
|
+
const after = title.slice(at + separator.length).trim();
|
|
1146
|
+
if (before.length > 0 && after.length > 0) return after;
|
|
1147
|
+
}
|
|
1148
|
+
return title;
|
|
1149
|
+
}
|
|
1150
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
1151
|
+
const MAX_BATCH_CONCURRENCY = 64;
|
|
1152
|
+
const DEFAULT_BYTES_IN_FLIGHT = 64 * 1024 * 1024;
|
|
1153
|
+
const DEFAULT_RETRIES = 3;
|
|
1154
|
+
const BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
1155
|
+
const DEFAULT_STALL_MS = 3e4;
|
|
1156
|
+
const PROGRESS_INTERVAL_MS = 200;
|
|
1157
|
+
class StalledError extends Error {
|
|
1158
|
+
/** @param seconds How long nothing moved. */
|
|
1159
|
+
constructor(seconds) {
|
|
1160
|
+
super(`No data moved for ${seconds}s.`);
|
|
1161
|
+
this.name = "StalledError";
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {}, onRetry) {
|
|
1165
|
+
const put = deps.put ?? defaultTransport(deps.fetch);
|
|
1166
|
+
const delay = deps.delay ?? defaultDelay;
|
|
1167
|
+
const concurrency = deps.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1168
|
+
const batchConcurrency = Math.max(
|
|
1169
|
+
1,
|
|
1170
|
+
Math.min(MAX_BATCH_CONCURRENCY, Math.floor(deps.batchConcurrency ?? concurrency))
|
|
1171
|
+
);
|
|
1172
|
+
const retries = deps.retries ?? DEFAULT_RETRIES;
|
|
1173
|
+
const stallMs = deps.stallMs ?? DEFAULT_STALL_MS;
|
|
1174
|
+
const now = deps.now ?? Date.now;
|
|
1175
|
+
const maxBytesInFlight = deps.maxBytesInFlight ?? DEFAULT_BYTES_IN_FLIGHT;
|
|
1176
|
+
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
1177
|
+
const fileCount = targets.reduce(
|
|
1178
|
+
(sum, target) => sum + (isBatch(target) ? target.files.length : 1),
|
|
1179
|
+
0
|
|
1180
|
+
);
|
|
1181
|
+
let nextIndex = 0;
|
|
1182
|
+
let done = 0;
|
|
1183
|
+
let failed = false;
|
|
1184
|
+
let firstFailure;
|
|
1185
|
+
const local = new AbortController();
|
|
1186
|
+
const inner = local.signal;
|
|
1187
|
+
const fail = (error) => {
|
|
1188
|
+
if (failed) return;
|
|
1189
|
+
failed = true;
|
|
1190
|
+
firstFailure = error;
|
|
1191
|
+
local.abort();
|
|
1192
|
+
};
|
|
1193
|
+
let reserved = 0;
|
|
1354
1194
|
let waiting = [];
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1195
|
+
const acquire = async (size) => {
|
|
1196
|
+
while (reserved > 0 && reserved + size > maxBytesInFlight) {
|
|
1197
|
+
await new Promise((resolve2) => waiting.push(resolve2));
|
|
1198
|
+
}
|
|
1199
|
+
reserved += size;
|
|
1200
|
+
};
|
|
1201
|
+
const release = (size) => {
|
|
1202
|
+
reserved -= size;
|
|
1203
|
+
const woken = waiting;
|
|
1204
|
+
waiting = [];
|
|
1205
|
+
for (const wake of woken) wake();
|
|
1206
|
+
};
|
|
1207
|
+
const running = { single: 0, batch: 0 };
|
|
1208
|
+
const limit = { single: concurrency, batch: batchConcurrency };
|
|
1209
|
+
let slotWaiting = [];
|
|
1210
|
+
const takeSlot = async (kind) => {
|
|
1211
|
+
while (running[kind] >= limit[kind]) {
|
|
1212
|
+
await new Promise((resolve2) => slotWaiting.push(resolve2));
|
|
1213
|
+
}
|
|
1214
|
+
running[kind] += 1;
|
|
1215
|
+
};
|
|
1216
|
+
const freeSlot = (kind) => {
|
|
1217
|
+
running[kind] -= 1;
|
|
1218
|
+
const woken = slotWaiting;
|
|
1219
|
+
slotWaiting = [];
|
|
1220
|
+
for (const wake of woken) wake();
|
|
1221
|
+
};
|
|
1222
|
+
let landedBytes = 0;
|
|
1223
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1224
|
+
let lastReport = Number.NEGATIVE_INFINITY;
|
|
1225
|
+
const report = (path) => {
|
|
1226
|
+
if (onProgress === void 0 || failed) return;
|
|
1227
|
+
const at = now();
|
|
1228
|
+
if (path === void 0 && at - lastReport < PROGRESS_INTERVAL_MS) return;
|
|
1229
|
+
lastReport = at;
|
|
1230
|
+
let bytes = landedBytes;
|
|
1231
|
+
for (const sent of inFlight.values()) bytes += sent;
|
|
1232
|
+
onProgress(done, fileCount, bytes, path);
|
|
1233
|
+
};
|
|
1234
|
+
const worker = async () => {
|
|
1235
|
+
for (; ; ) {
|
|
1236
|
+
if (failed) return;
|
|
1237
|
+
const index = nextIndex++;
|
|
1238
|
+
const target = targets[index];
|
|
1239
|
+
if (target === void 0) return;
|
|
1240
|
+
inner.throwIfAborted();
|
|
1241
|
+
const refs = isBatch(target) ? target.files : [target];
|
|
1242
|
+
const members = [];
|
|
1243
|
+
for (const ref of refs) {
|
|
1244
|
+
const file = byPath.get(ref.path);
|
|
1245
|
+
if (file === void 0) {
|
|
1246
|
+
const missing = new DeployError(
|
|
1247
|
+
ClientErrorCode.UploadFailed,
|
|
1248
|
+
`The server asked for ${ref.path}, which is not in the manifest.`,
|
|
1249
|
+
{ path: ref.path }
|
|
1250
|
+
);
|
|
1251
|
+
fail(missing);
|
|
1252
|
+
throw missing;
|
|
1253
|
+
}
|
|
1254
|
+
members.push(file);
|
|
1255
|
+
}
|
|
1256
|
+
const layout = isBatch(target) ? new TextEncoder().encode(
|
|
1257
|
+
batchLayout(
|
|
1258
|
+
target.files.map((ref, i) => ({
|
|
1259
|
+
sha256: ref.sha256,
|
|
1260
|
+
size: sizeOf(members[i].bytes)
|
|
1261
|
+
}))
|
|
1262
|
+
)
|
|
1263
|
+
) : null;
|
|
1264
|
+
const size = members.reduce((sum, file) => sum + sizeOf(file.bytes), layout?.length ?? 0);
|
|
1265
|
+
const copied = isBatch(target) ? members.reduce(
|
|
1266
|
+
(sum, file) => sum + (file.bytes instanceof Blob ? 0 : sizeOf(file.bytes)),
|
|
1267
|
+
0
|
|
1268
|
+
) : 0;
|
|
1269
|
+
const reservation = size + copied;
|
|
1270
|
+
const first = members[0].path;
|
|
1271
|
+
const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
|
|
1272
|
+
const kind = isBatch(target) ? "batch" : "single";
|
|
1273
|
+
await takeSlot(kind);
|
|
1274
|
+
await acquire(reservation);
|
|
1275
|
+
try {
|
|
1276
|
+
if (failed) return;
|
|
1277
|
+
inner.throwIfAborted();
|
|
1278
|
+
const context = {
|
|
1279
|
+
put,
|
|
1280
|
+
delay,
|
|
1281
|
+
retries,
|
|
1282
|
+
signal: inner,
|
|
1283
|
+
uploadUrl,
|
|
1284
|
+
stallMs,
|
|
1285
|
+
onSent: (sent) => {
|
|
1286
|
+
inFlight.set(index, sent);
|
|
1287
|
+
report();
|
|
1288
|
+
},
|
|
1289
|
+
onRetry: (attempt, stalled) => {
|
|
1290
|
+
inFlight.set(index, 0);
|
|
1291
|
+
onRetry?.(label, attempt, retries + 1, stalled);
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
if (isBatch(target) && layout !== null)
|
|
1295
|
+
await uploadBatchOne(target, members, layout, context);
|
|
1296
|
+
else await uploadOne(target, members[0], context);
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
fail(error);
|
|
1299
|
+
throw error;
|
|
1300
|
+
} finally {
|
|
1301
|
+
release(reservation);
|
|
1302
|
+
freeSlot(kind);
|
|
1359
1303
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
const woken = waiting;
|
|
1365
|
-
waiting = [];
|
|
1366
|
-
for (const wake of woken) wake();
|
|
1304
|
+
inFlight.delete(index);
|
|
1305
|
+
done += members.length;
|
|
1306
|
+
landedBytes += size;
|
|
1307
|
+
report(members.at(-1).path);
|
|
1367
1308
|
}
|
|
1368
1309
|
};
|
|
1369
|
-
|
|
1370
|
-
|
|
1310
|
+
const workers = Array.from(
|
|
1311
|
+
{ length: Math.min(Math.max(concurrency, batchConcurrency), targets.length) },
|
|
1312
|
+
worker
|
|
1313
|
+
);
|
|
1371
1314
|
try {
|
|
1372
|
-
|
|
1315
|
+
await Promise.all(workers);
|
|
1373
1316
|
} catch (error) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
ClientErrorCode.ReadFailed,
|
|
1377
|
-
`Could not read ${file.path}. It may have been moved, renamed or changed since you dropped it — drop the folder again.`,
|
|
1378
|
-
{ path: file.path, cause: describeError(error) }
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
const IGNORED_EXACT = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".gitignore", ".gitkeep"]);
|
|
1383
|
-
const IGNORED_DIRECTORIES = [
|
|
1384
|
-
"__MACOSX",
|
|
1385
|
-
".git",
|
|
1386
|
-
"node_modules",
|
|
1387
|
-
".svn",
|
|
1388
|
-
".hg",
|
|
1389
|
-
".idea",
|
|
1390
|
-
".vscode",
|
|
1391
|
-
".claude",
|
|
1392
|
-
".agents",
|
|
1393
|
-
".cursor",
|
|
1394
|
-
".codex",
|
|
1395
|
-
".gemini"
|
|
1396
|
-
];
|
|
1397
|
-
function isEnvironmentFile(name) {
|
|
1398
|
-
return name === ".env" || name === ".envrc" || name.startsWith(".env.");
|
|
1399
|
-
}
|
|
1400
|
-
function shouldIgnore(path) {
|
|
1401
|
-
const segments = path.split("/");
|
|
1402
|
-
const name = segments.at(-1) ?? "";
|
|
1403
|
-
if (name === "" || IGNORED_EXACT.has(name) || isEnvironmentFile(name)) return true;
|
|
1404
|
-
if (name.startsWith("._")) return true;
|
|
1405
|
-
return segments.some((segment) => IGNORED_DIRECTORIES.includes(segment));
|
|
1406
|
-
}
|
|
1407
|
-
const REQUIRED_INDEX = "index.html";
|
|
1408
|
-
const DOCUMENT_EXTENSIONS = DOCUMENT_EXTENSIONS$1;
|
|
1409
|
-
function isDocumentPath(path) {
|
|
1410
|
-
const lower = path.toLowerCase();
|
|
1411
|
-
return DOCUMENT_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
1412
|
-
}
|
|
1413
|
-
const VIEWABLE_EXTENSIONS = VIEWABLE_EXTENSIONS$1;
|
|
1414
|
-
function isViewablePath(path) {
|
|
1415
|
-
const lower = path.toLowerCase();
|
|
1416
|
-
return VIEWABLE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
1417
|
-
}
|
|
1418
|
-
function isImageFolder(paths) {
|
|
1419
|
-
let pictures = false;
|
|
1420
|
-
for (const path of paths) {
|
|
1421
|
-
const ext = extensionOf(path);
|
|
1422
|
-
if (WEB_ASSET_EXTENSIONS.includes(ext)) return false;
|
|
1423
|
-
if (GALLERY_EXTENSIONS.includes(ext)) pictures = true;
|
|
1424
|
-
}
|
|
1425
|
-
return pictures;
|
|
1426
|
-
}
|
|
1427
|
-
function canPublish(paths) {
|
|
1428
|
-
if (paths.some((path) => path === REQUIRED_INDEX)) return true;
|
|
1429
|
-
if (paths.some(isDocumentPath)) return true;
|
|
1430
|
-
if (isImageFolder(paths)) return true;
|
|
1431
|
-
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
1432
|
-
}
|
|
1433
|
-
function servingModeOf(flags) {
|
|
1434
|
-
if (flags.docsMode) return "docs";
|
|
1435
|
-
if (flags.spaMode) return "spa";
|
|
1436
|
-
return "static";
|
|
1437
|
-
}
|
|
1438
|
-
function predictedModeOf(paths) {
|
|
1439
|
-
if (paths.some((path) => path === REQUIRED_INDEX)) return "static";
|
|
1440
|
-
if (paths.some(isDocumentPath)) return "docs";
|
|
1441
|
-
if (isImageFolder(paths)) return "docs";
|
|
1442
|
-
if (paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0])) return "docs";
|
|
1443
|
-
return "static";
|
|
1444
|
-
}
|
|
1445
|
-
function nestedIndexOf(paths) {
|
|
1446
|
-
if (predictedModeOf(paths) !== "docs") return null;
|
|
1447
|
-
const suffix = `/${REQUIRED_INDEX}`;
|
|
1448
|
-
const folders2 = paths.filter((path) => path.endsWith(suffix)).map((path) => path.slice(0, -suffix.length)).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
|
|
1449
|
-
return folders2[0] ?? null;
|
|
1450
|
-
}
|
|
1451
|
-
function renameOfLoneHtmlPage(paths) {
|
|
1452
|
-
if (paths.length !== 1) return null;
|
|
1453
|
-
const [only] = paths;
|
|
1454
|
-
if (only === void 0 || only === REQUIRED_INDEX) return null;
|
|
1455
|
-
const lower = only.toLowerCase();
|
|
1456
|
-
return lower.endsWith(".html") || lower.endsWith(".htm") ? only : null;
|
|
1457
|
-
}
|
|
1458
|
-
function formatBytes$1(bytes) {
|
|
1459
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
1460
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1461
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1462
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
1463
|
-
}
|
|
1464
|
-
function checkLimits(files, limits) {
|
|
1465
|
-
if (files.length === 0) {
|
|
1466
|
-
throw new DeployError(
|
|
1467
|
-
ClientErrorCode.Empty,
|
|
1468
|
-
"That drop contained no files to deploy. Folders like .git and node_modules are skipped."
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
if (!canPublish(files.map((file) => file.path))) {
|
|
1472
|
-
throw new DeployError(
|
|
1473
|
-
ClientErrorCode.MissingIndex,
|
|
1474
|
-
`A site needs an ${REQUIRED_INDEX} at its top level, or at least one .md or .pdf file to publish as a documents site — or be a single file that can be displayed on its own, such as a note, a PDF, a page or an image. Drop the folder that contains it, not the folder above.`
|
|
1475
|
-
);
|
|
1317
|
+
throw asDeployError(firstFailure ?? error);
|
|
1318
|
+
} finally {
|
|
1476
1319
|
}
|
|
1477
|
-
const refusal = sizeRefusal(
|
|
1478
|
-
files.map((file) => ({ path: file.path, size: sizeOf(file.bytes) })),
|
|
1479
|
-
limits
|
|
1480
|
-
);
|
|
1481
|
-
if (refusal) throw refusal;
|
|
1482
1320
|
}
|
|
1483
|
-
function
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1321
|
+
async function uploadOne(target, file, context) {
|
|
1322
|
+
let lastError;
|
|
1323
|
+
for (let attempt = 0; attempt <= context.retries; attempt++) {
|
|
1324
|
+
context.signal?.throwIfAborted();
|
|
1325
|
+
if (attempt > 0) {
|
|
1326
|
+
context.onRetry(attempt + 1, lastError instanceof StalledError);
|
|
1327
|
+
await context.delay(BACKOFF_MS[attempt - 1] ?? BACKOFF_MS.at(-1) ?? 1e3);
|
|
1328
|
+
context.signal?.throwIfAborted();
|
|
1329
|
+
}
|
|
1330
|
+
try {
|
|
1331
|
+
const answer = await context.put({
|
|
1332
|
+
url: context.uploadUrl,
|
|
1333
|
+
token: target.token,
|
|
1334
|
+
body: untyped(file.bytes),
|
|
1335
|
+
signal: context.signal,
|
|
1336
|
+
onSent: context.onSent,
|
|
1337
|
+
stallMs: context.stallMs
|
|
1338
|
+
});
|
|
1339
|
+
const status = typeof answer === "number" ? answer : answer.status;
|
|
1340
|
+
if (status >= 200 && status < 300) return;
|
|
1341
|
+
if (status === 401 || status === 403 || status === 429) {
|
|
1342
|
+
throw new DeployError(
|
|
1343
|
+
ClientErrorCode.UploadFailed,
|
|
1344
|
+
`Upload of ${target.path} was refused. The upload window may have expired — try deploying again.`,
|
|
1345
|
+
{ path: target.path, status }
|
|
1346
|
+
);
|
|
1501
1347
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
)
|
|
1348
|
+
if (status === 413) {
|
|
1349
|
+
throw new DeployError(
|
|
1350
|
+
ClientErrorCode.UploadFailed,
|
|
1351
|
+
`${target.path} is too large to upload in one request. Make it smaller, or leave it out of this publish.`,
|
|
1352
|
+
{ path: target.path, status }
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
lastError = new Error(`HTTP ${status}`);
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
if (error instanceof DeployError) throw error;
|
|
1358
|
+
if (isAbort(error)) throw cancelled();
|
|
1359
|
+
lastError = error;
|
|
1360
|
+
}
|
|
1511
1361
|
}
|
|
1512
|
-
|
|
1362
|
+
const message = lastError instanceof StalledError ? `Upload of ${target.path} stopped moving and did not recover after ${context.retries + 1} attempts. Check your connection and try again.` : `Could not upload ${target.path} after ${context.retries + 1} attempts.`;
|
|
1363
|
+
throw new DeployError(ClientErrorCode.UploadFailed, message, {
|
|
1364
|
+
path: target.path,
|
|
1365
|
+
cause: describeError(lastError)
|
|
1366
|
+
});
|
|
1513
1367
|
}
|
|
1514
|
-
|
|
1515
|
-
const
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1368
|
+
async function uploadBatchOne(batch, members, layout, context) {
|
|
1369
|
+
const first = members[0].path;
|
|
1370
|
+
const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
|
|
1371
|
+
let lastError;
|
|
1372
|
+
for (let attempt = 0; attempt <= context.retries; attempt++) {
|
|
1373
|
+
context.signal?.throwIfAborted();
|
|
1374
|
+
if (attempt > 0) {
|
|
1375
|
+
context.onRetry(attempt + 1, lastError instanceof StalledError);
|
|
1376
|
+
await context.delay(BACKOFF_MS[attempt - 1] ?? BACKOFF_MS.at(-1) ?? 1e3);
|
|
1377
|
+
context.signal?.throwIfAborted();
|
|
1378
|
+
}
|
|
1379
|
+
try {
|
|
1380
|
+
const body = new Blob([layout, ...members.map((file) => file.bytes)]);
|
|
1381
|
+
const answer = await context.put({
|
|
1382
|
+
url: context.uploadUrl,
|
|
1383
|
+
token: batch.token,
|
|
1384
|
+
body,
|
|
1385
|
+
signal: context.signal,
|
|
1386
|
+
onSent: context.onSent,
|
|
1387
|
+
stallMs: context.stallMs,
|
|
1388
|
+
wantsBody: true
|
|
1389
|
+
});
|
|
1390
|
+
const status = typeof answer === "number" ? answer : answer.status;
|
|
1391
|
+
if (status >= 200 && status < 300) {
|
|
1392
|
+
const result = typeof answer === "number" ? null : parseBatchResult(answer.body);
|
|
1393
|
+
if (result === null) {
|
|
1394
|
+
lastError = new Error(
|
|
1395
|
+
"The upload service answered with something that is not a batch result."
|
|
1396
|
+
);
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (result.failed.length === 0) return;
|
|
1400
|
+
const changed = result.failed.find((failure2) => failure2.reason === "hash_mismatch");
|
|
1401
|
+
if (changed !== void 0) {
|
|
1402
|
+
const path = members[changed.i]?.path ?? first;
|
|
1403
|
+
throw new DeployError(
|
|
1404
|
+
ClientErrorCode.ReadFailed,
|
|
1405
|
+
`${path} changed after it was read, so it no longer matches what was checked. Drop the folder again.`,
|
|
1406
|
+
{ path }
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
lastError = new Error(
|
|
1410
|
+
`${result.failed.length} of ${members.length} files were not stored.`
|
|
1411
|
+
);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if ([400, 401, 403, 411, 413, 429].includes(status)) {
|
|
1415
|
+
throw new DeployError(ClientErrorCode.UploadFailed, refusalMessage(status, label), {
|
|
1416
|
+
path: first,
|
|
1417
|
+
status
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
lastError = new Error(`HTTP ${status}`);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
if (error instanceof DeployError) throw error;
|
|
1423
|
+
if (isAbort(error)) throw cancelled();
|
|
1424
|
+
lastError = error;
|
|
1425
|
+
}
|
|
1524
1426
|
}
|
|
1427
|
+
const message = lastError instanceof StalledError ? `Upload of ${label} stopped moving and did not recover after ${context.retries + 1} attempts. Check your connection and try again.` : `Could not upload ${label} after ${context.retries + 1} attempts.`;
|
|
1428
|
+
throw new DeployError(ClientErrorCode.UploadFailed, message, {
|
|
1429
|
+
path: first,
|
|
1430
|
+
cause: describeError(lastError)
|
|
1431
|
+
});
|
|
1525
1432
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1433
|
+
function refusalMessage(status, label) {
|
|
1434
|
+
switch (status) {
|
|
1435
|
+
case 401:
|
|
1436
|
+
return `Upload of ${label} was refused because its upload window expired. Try deploying again.`;
|
|
1437
|
+
case 429:
|
|
1438
|
+
return `Upload of ${label} was refused: this deploy has used every upload it was given. Try deploying again.`;
|
|
1439
|
+
case 400:
|
|
1440
|
+
case 411:
|
|
1441
|
+
case 413:
|
|
1442
|
+
return `Upload of ${label} was refused because it no longer matches what was checked — a file may have changed since you dropped it. Drop the folder again.`;
|
|
1443
|
+
default:
|
|
1444
|
+
return `Upload of ${label} was refused by the upload service. Try deploying again.`;
|
|
1535
1445
|
}
|
|
1536
1446
|
}
|
|
1537
|
-
function
|
|
1538
|
-
|
|
1447
|
+
function untyped(content) {
|
|
1448
|
+
if (ArrayBuffer.isView(content) || content.type === "") return content;
|
|
1449
|
+
return content.slice(0, content.size);
|
|
1539
1450
|
}
|
|
1540
|
-
function
|
|
1541
|
-
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
document.querySelector("title")?.textContent,
|
|
1545
|
-
document.querySelector("h1")?.textContent
|
|
1546
|
-
] : [matchOgSiteName(html), matchTag(html, "title"), matchTag(html, "h1")];
|
|
1547
|
-
for (const candidate of candidates) {
|
|
1548
|
-
const name = clean(candidate);
|
|
1549
|
-
if (name !== null) return name;
|
|
1550
|
-
}
|
|
1551
|
-
return null;
|
|
1451
|
+
function defaultTransport(doFetch) {
|
|
1452
|
+
if (doFetch !== void 0) return fetchTransport(doFetch);
|
|
1453
|
+
const Xhr = globalThis.XMLHttpRequest;
|
|
1454
|
+
return Xhr === void 0 ? fetchTransport(globalFetch()) : xhrTransport(Xhr);
|
|
1552
1455
|
}
|
|
1553
|
-
function
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1456
|
+
function fetchTransport(doFetch) {
|
|
1457
|
+
return async (request2) => {
|
|
1458
|
+
const { url, token, body, signal } = request2;
|
|
1459
|
+
const response = await doFetch(url, {
|
|
1460
|
+
method: "PUT",
|
|
1461
|
+
// The view, not a copy. Wrapping it in `new Blob([body.slice().buffer])` made two extra
|
|
1462
|
+
// full-size copies of every file in flight — eight at once, so up to 400 MB on a drop of
|
|
1463
|
+
// 50 MB files. `fetch` sends exactly the range a view covers. The cast is TypeScript's
|
|
1464
|
+
// SharedArrayBuffer distinction again; see `sha256Hex`. A `Blob` goes as it is, and the runtime
|
|
1465
|
+
// streams it from disk.
|
|
1466
|
+
body,
|
|
1467
|
+
// The permit is the whole request. `Content-Length` is required by the Worker and cannot be
|
|
1468
|
+
// set here — it is a forbidden header name — but the runtime derives it from the body, which
|
|
1469
|
+
// is exactly the length the token names.
|
|
1470
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1471
|
+
...signal ? { signal } : {}
|
|
1472
|
+
});
|
|
1473
|
+
return request2.wantsBody === true ? { status: response.status, body: await response.text() } : response.status;
|
|
1474
|
+
};
|
|
1561
1475
|
}
|
|
1562
|
-
function
|
|
1563
|
-
|
|
1564
|
-
|
|
1476
|
+
function xhrTransport(Xhr) {
|
|
1477
|
+
return ({ url, token, body, signal, onSent, stallMs, wantsBody }) => new Promise((resolve2, reject) => {
|
|
1478
|
+
const xhr = new Xhr();
|
|
1479
|
+
let settled = false;
|
|
1480
|
+
let timer;
|
|
1481
|
+
const settle = (finish) => {
|
|
1482
|
+
if (settled) return;
|
|
1483
|
+
settled = true;
|
|
1484
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1485
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1486
|
+
finish();
|
|
1487
|
+
};
|
|
1488
|
+
const arm = () => {
|
|
1489
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1490
|
+
timer = setTimeout(() => {
|
|
1491
|
+
xhr.abort();
|
|
1492
|
+
settle(() => reject(new StalledError(Math.round(stallMs / 1e3))));
|
|
1493
|
+
}, stallMs);
|
|
1494
|
+
};
|
|
1495
|
+
function onAbort() {
|
|
1496
|
+
xhr.abort();
|
|
1497
|
+
settle(() => reject(abortReason(signal)));
|
|
1498
|
+
}
|
|
1499
|
+
if (signal?.aborted) {
|
|
1500
|
+
reject(abortReason(signal));
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1504
|
+
xhr.open("PUT", url);
|
|
1505
|
+
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
|
|
1506
|
+
xhr.upload.onprogress = (event) => {
|
|
1507
|
+
arm();
|
|
1508
|
+
onSent(event.loaded);
|
|
1509
|
+
};
|
|
1510
|
+
xhr.onprogress = arm;
|
|
1511
|
+
xhr.onload = () => settle(
|
|
1512
|
+
() => resolve2(
|
|
1513
|
+
wantsBody === true ? { status: xhr.status, body: xhr.responseText ?? "" } : xhr.status
|
|
1514
|
+
)
|
|
1515
|
+
);
|
|
1516
|
+
xhr.onerror = () => settle(() => reject(new TypeError("Network error during upload.")));
|
|
1517
|
+
arm();
|
|
1518
|
+
xhr.send(body);
|
|
1519
|
+
});
|
|
1565
1520
|
}
|
|
1566
|
-
function
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
const match = pattern.exec(html);
|
|
1573
|
-
if (match?.[1] !== void 0) return decodeEntities(match[1]);
|
|
1574
|
-
}
|
|
1575
|
-
return null;
|
|
1521
|
+
function abortReason(signal) {
|
|
1522
|
+
const reason = signal?.reason;
|
|
1523
|
+
if (reason instanceof Error) return reason;
|
|
1524
|
+
const error = new Error("Aborted.");
|
|
1525
|
+
error.name = "AbortError";
|
|
1526
|
+
return error;
|
|
1576
1527
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
...LATIN1_ENTITIES.map((name, index) => [name, 160 + index]),
|
|
1580
|
-
["amp", 38],
|
|
1581
|
-
["lt", 60],
|
|
1582
|
-
["gt", 62],
|
|
1583
|
-
["quot", 34],
|
|
1584
|
-
["apos", 39],
|
|
1585
|
-
["ensp", 8194],
|
|
1586
|
-
["emsp", 8195],
|
|
1587
|
-
["thinsp", 8201],
|
|
1588
|
-
["ndash", 8211],
|
|
1589
|
-
["mdash", 8212],
|
|
1590
|
-
["lsquo", 8216],
|
|
1591
|
-
["rsquo", 8217],
|
|
1592
|
-
["sbquo", 8218],
|
|
1593
|
-
["ldquo", 8220],
|
|
1594
|
-
["rdquo", 8221],
|
|
1595
|
-
["bdquo", 8222],
|
|
1596
|
-
["dagger", 8224],
|
|
1597
|
-
["Dagger", 8225],
|
|
1598
|
-
["bull", 8226],
|
|
1599
|
-
["hellip", 8230],
|
|
1600
|
-
["prime", 8242],
|
|
1601
|
-
["lsaquo", 8249],
|
|
1602
|
-
["rsaquo", 8250],
|
|
1603
|
-
["euro", 8364],
|
|
1604
|
-
["trade", 8482],
|
|
1605
|
-
["larr", 8592],
|
|
1606
|
-
["rarr", 8594],
|
|
1607
|
-
["hearts", 9829]
|
|
1608
|
-
]);
|
|
1609
|
-
function decodeEntities(text) {
|
|
1610
|
-
return text.replace(
|
|
1611
|
-
/&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g,
|
|
1612
|
-
(whole, decimal, hex, name) => {
|
|
1613
|
-
if (name !== void 0) {
|
|
1614
|
-
const code2 = NAMED_ENTITIES.get(name);
|
|
1615
|
-
return code2 === void 0 ? whole : String.fromCodePoint(code2);
|
|
1616
|
-
}
|
|
1617
|
-
const code = decimal !== void 0 ? Number(decimal) : Number.parseInt(hex ?? "", 16);
|
|
1618
|
-
const valid = code > 0 && code <= 1114111 && (code < 55296 || code > 57343);
|
|
1619
|
-
return String.fromCodePoint(valid ? code : 65533);
|
|
1620
|
-
}
|
|
1621
|
-
);
|
|
1528
|
+
function globalFetch() {
|
|
1529
|
+
return globalThis.fetch.bind(globalThis);
|
|
1622
1530
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
"g"
|
|
1626
|
-
);
|
|
1627
|
-
function clean(raw) {
|
|
1628
|
-
if (raw === null || raw === void 0) return null;
|
|
1629
|
-
if (raw.includes("<") || raw.includes(">")) return null;
|
|
1630
|
-
const collapsed = raw.replace(INVISIBLE, " ").replace(/\s+/g, " ").trim();
|
|
1631
|
-
const name = trimSuffix(collapsed);
|
|
1632
|
-
if (name.length === 0) return null;
|
|
1633
|
-
if (PLACEHOLDERS.has(name.toLowerCase())) return null;
|
|
1634
|
-
return name.length > MAX_NAME_LENGTH ? name.slice(0, MAX_NAME_LENGTH).trimEnd() : name;
|
|
1531
|
+
function defaultDelay(ms) {
|
|
1532
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1635
1533
|
}
|
|
1636
|
-
|
|
1637
|
-
"
|
|
1638
|
-
"untitled",
|
|
1639
|
-
"untitled document",
|
|
1640
|
-
"index",
|
|
1641
|
-
"home",
|
|
1642
|
-
"new page",
|
|
1643
|
-
"my site",
|
|
1644
|
-
"vite app",
|
|
1645
|
-
"vite + react",
|
|
1646
|
-
"react app",
|
|
1647
|
-
"create next app",
|
|
1648
|
-
"svelte app",
|
|
1649
|
-
"webpack app",
|
|
1650
|
-
"hello world",
|
|
1651
|
-
"title"
|
|
1652
|
-
]);
|
|
1653
|
-
function trimSuffix(title) {
|
|
1654
|
-
for (const separator of TITLE_SEPARATORS) {
|
|
1655
|
-
const at = title.lastIndexOf(separator);
|
|
1656
|
-
if (at <= 0) continue;
|
|
1657
|
-
const before = title.slice(0, at).trim();
|
|
1658
|
-
const after = title.slice(at + separator.length).trim();
|
|
1659
|
-
if (before.length > 0 && after.length > 0) return after;
|
|
1660
|
-
}
|
|
1661
|
-
return title;
|
|
1534
|
+
function isAbort(error) {
|
|
1535
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
1662
1536
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
const DEFAULT_BYTES_IN_FLIGHT = 64 * 1024 * 1024;
|
|
1666
|
-
const DEFAULT_RETRIES = 3;
|
|
1667
|
-
const BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
1668
|
-
const DEFAULT_STALL_MS = 3e4;
|
|
1669
|
-
const PROGRESS_INTERVAL_MS = 200;
|
|
1670
|
-
class StalledError extends Error {
|
|
1671
|
-
/** @param seconds How long nothing moved. */
|
|
1672
|
-
constructor(seconds) {
|
|
1673
|
-
super(`No data moved for ${seconds}s.`);
|
|
1674
|
-
this.name = "StalledError";
|
|
1675
|
-
}
|
|
1537
|
+
function cancelled() {
|
|
1538
|
+
return new DeployError(ClientErrorCode.Cancelled, "Deploy cancelled.");
|
|
1676
1539
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
1,
|
|
1683
|
-
Math.min(MAX_BATCH_CONCURRENCY, Math.floor(deps.batchConcurrency ?? concurrency))
|
|
1540
|
+
function outOfMemory(detail) {
|
|
1541
|
+
return new DeployError(
|
|
1542
|
+
ClientErrorCode.OutOfMemory,
|
|
1543
|
+
"This browser ran out of memory reading what you dropped. Drop the unzipped folder instead, or try again in a desktop browser with fewer tabs open.",
|
|
1544
|
+
detail
|
|
1684
1545
|
);
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1546
|
+
}
|
|
1547
|
+
function asDeployError(error) {
|
|
1548
|
+
if (error instanceof DeployError) return error;
|
|
1549
|
+
if (isAbort(error)) return cancelled();
|
|
1550
|
+
if (error instanceof RangeError) return outOfMemory(error.message);
|
|
1551
|
+
return new DeployError(
|
|
1552
|
+
ClientErrorCode.UploadFailed,
|
|
1553
|
+
error instanceof Error ? error.message : "Upload failed.",
|
|
1554
|
+
error
|
|
1693
1555
|
);
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1556
|
+
}
|
|
1557
|
+
async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
1558
|
+
try {
|
|
1559
|
+
onProgress({ type: "reading", bytes: 0, total: 0 });
|
|
1560
|
+
const collected = await source(
|
|
1561
|
+
signal,
|
|
1562
|
+
(bytes, total) => onProgress({ type: "reading", bytes, total })
|
|
1563
|
+
);
|
|
1564
|
+
onProgress({ type: "collecting", files: collected.length });
|
|
1565
|
+
const rename = renameOfLoneHtmlPage(collected.map((file) => file.path));
|
|
1566
|
+
const files = rename === null ? collected : collected.map((file) => file.path === rename ? { ...file, path: "index.html" } : file);
|
|
1567
|
+
const hashed = await hashAll(
|
|
1568
|
+
files,
|
|
1569
|
+
(done, total) => onProgress({ type: "hashing", done, total }),
|
|
1570
|
+
signal
|
|
1571
|
+
);
|
|
1572
|
+
checkLimits(hashed, options.limits ?? null);
|
|
1573
|
+
onProgress({ type: "preparing" });
|
|
1574
|
+
const prepared = await prepareDeploy(siteId, hashed, options, signal);
|
|
1575
|
+
if (prepared.unchanged) {
|
|
1576
|
+
onProgress({ type: "unchanged", url: prepared.url ?? "" });
|
|
1577
|
+
return;
|
|
1711
1578
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1579
|
+
const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
|
|
1580
|
+
const batches = prepared.batches ?? [];
|
|
1581
|
+
const targets = [...prepared.upload, ...batches];
|
|
1582
|
+
const totalBytes = prepared.upload.reduce((sum, target) => sum + (sizes.get(target.path) ?? 0), 0) + batches.reduce((sum, batch) => {
|
|
1583
|
+
const members = batch.files.map((ref) => ({
|
|
1584
|
+
sha256: ref.sha256,
|
|
1585
|
+
size: sizes.get(ref.path) ?? 0
|
|
1586
|
+
}));
|
|
1587
|
+
return sum + batchLayout(members).length + members.reduce((n, member) => n + member.size, 0);
|
|
1588
|
+
}, 0);
|
|
1589
|
+
if (targets.length > 0) {
|
|
1590
|
+
onProgress({
|
|
1591
|
+
type: "uploading",
|
|
1592
|
+
done: prepared.reused,
|
|
1593
|
+
total: prepared.total,
|
|
1594
|
+
bytes: 0,
|
|
1595
|
+
totalBytes,
|
|
1596
|
+
reused: prepared.reused
|
|
1597
|
+
});
|
|
1726
1598
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
0
|
|
1781
|
-
) : 0;
|
|
1782
|
-
const reservation = size + copied;
|
|
1783
|
-
const first = members[0].path;
|
|
1784
|
-
const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
|
|
1785
|
-
const kind = isBatch(target) ? "batch" : "single";
|
|
1786
|
-
await takeSlot(kind);
|
|
1787
|
-
await acquire(reservation);
|
|
1788
|
-
try {
|
|
1789
|
-
if (failed) return;
|
|
1790
|
-
inner.throwIfAborted();
|
|
1791
|
-
const context = {
|
|
1792
|
-
put,
|
|
1793
|
-
delay,
|
|
1794
|
-
retries,
|
|
1795
|
-
signal: inner,
|
|
1796
|
-
uploadUrl,
|
|
1797
|
-
stallMs,
|
|
1798
|
-
onSent: (sent) => {
|
|
1799
|
-
inFlight.set(index, sent);
|
|
1800
|
-
report();
|
|
1801
|
-
},
|
|
1802
|
-
onRetry: (attempt, stalled) => {
|
|
1803
|
-
inFlight.set(index, 0);
|
|
1804
|
-
onRetry?.(label, attempt, retries + 1, stalled);
|
|
1805
|
-
}
|
|
1806
|
-
};
|
|
1807
|
-
if (isBatch(target) && layout !== null)
|
|
1808
|
-
await uploadBatchOne(target, members, layout, context);
|
|
1809
|
-
else await uploadOne(target, members[0], context);
|
|
1810
|
-
} catch (error) {
|
|
1811
|
-
fail(error);
|
|
1812
|
-
throw error;
|
|
1813
|
-
} finally {
|
|
1814
|
-
release(reservation);
|
|
1815
|
-
freeSlot(kind);
|
|
1816
|
-
}
|
|
1817
|
-
inFlight.delete(index);
|
|
1818
|
-
done += members.length;
|
|
1819
|
-
landedBytes += size;
|
|
1820
|
-
report(members.at(-1).path);
|
|
1599
|
+
await uploadAll(
|
|
1600
|
+
targets,
|
|
1601
|
+
prepared.uploadUrl,
|
|
1602
|
+
hashed,
|
|
1603
|
+
(done, _total, bytes, path) => onProgress({
|
|
1604
|
+
type: "uploading",
|
|
1605
|
+
done: prepared.reused + done,
|
|
1606
|
+
total: prepared.total,
|
|
1607
|
+
bytes,
|
|
1608
|
+
totalBytes,
|
|
1609
|
+
reused: prepared.reused,
|
|
1610
|
+
...path === void 0 ? {} : { path }
|
|
1611
|
+
}),
|
|
1612
|
+
signal,
|
|
1613
|
+
// The server's number unless the caller set one: it is tuned on the server so a deploy of many
|
|
1614
|
+
// small files speeds up without a CLI or MCP release.
|
|
1615
|
+
typeof prepared.batchConcurrency === "number" && options.upload?.batchConcurrency === void 0 ? { ...options.upload, batchConcurrency: prepared.batchConcurrency } : options.upload,
|
|
1616
|
+
(path, attempt, attempts, stalled) => onProgress({ type: "retrying", path, attempt, attempts, stalled })
|
|
1617
|
+
);
|
|
1618
|
+
onProgress({ type: "completing", reused: prepared.reused });
|
|
1619
|
+
const completed = await completeDeploy(
|
|
1620
|
+
siteId,
|
|
1621
|
+
prepared.deployId,
|
|
1622
|
+
options,
|
|
1623
|
+
signal,
|
|
1624
|
+
await readSiteName(files)
|
|
1625
|
+
);
|
|
1626
|
+
onProgress({
|
|
1627
|
+
type: "done",
|
|
1628
|
+
url: completed.url,
|
|
1629
|
+
name: completed.name,
|
|
1630
|
+
mode: servingModeOf$1(completed),
|
|
1631
|
+
modeIsManual: completed.modeIsManual
|
|
1632
|
+
});
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
const failure2 = asDeployError(error);
|
|
1635
|
+
onProgress({
|
|
1636
|
+
type: "error",
|
|
1637
|
+
code: failure2.code,
|
|
1638
|
+
message: failure2.message,
|
|
1639
|
+
...failure2.detail === void 0 ? {} : { detail: failure2.detail }
|
|
1640
|
+
});
|
|
1641
|
+
throw failure2;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function call(credentials, path, init2 = {}) {
|
|
1645
|
+
const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
|
|
1646
|
+
...init2,
|
|
1647
|
+
headers: {
|
|
1648
|
+
...credentials.headers,
|
|
1649
|
+
...init2.headers,
|
|
1650
|
+
Authorization: `Bearer ${credentials.token}`,
|
|
1651
|
+
...init2.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
1821
1652
|
}
|
|
1822
|
-
};
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1653
|
+
});
|
|
1654
|
+
if (!response.ok) throw await failureOf(response);
|
|
1655
|
+
return await response.json();
|
|
1656
|
+
}
|
|
1657
|
+
async function failureOf(response) {
|
|
1658
|
+
const problem = await response.json().catch(() => null);
|
|
1659
|
+
return Object.assign(new Error(problem?.detail ?? `The API answered ${response.status}.`), {
|
|
1660
|
+
status: response.status
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
async function findSite(credentials, site) {
|
|
1664
|
+
const found = await lookUpSite(credentials, site);
|
|
1665
|
+
if (found === void 0) {
|
|
1666
|
+
throw new Error(
|
|
1667
|
+
`No site of yours is called "${site}". \`drop2run ls\` or the list_sites tool shows what exists.`
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
return found;
|
|
1671
|
+
}
|
|
1672
|
+
const SITE_ID_SHAPE = /^[0-9a-z]{26}$/i;
|
|
1673
|
+
const FORMER_NAME_CANDIDATES = 3;
|
|
1674
|
+
async function lookUpSite(credentials, site) {
|
|
1675
|
+
const wanted = site.trim().toLowerCase();
|
|
1676
|
+
if (wanted === "") return void 0;
|
|
1677
|
+
const { sites } = await call(
|
|
1678
|
+
credentials,
|
|
1679
|
+
`sites?pageSize=${SITES_PER_REQUEST}&q=${encodeURIComponent(wanted)}`
|
|
1680
|
+
);
|
|
1681
|
+
const bySubdomain = sites.find((candidate) => candidate.subdomain.toLowerCase() === wanted);
|
|
1682
|
+
if (bySubdomain !== void 0) return bySubdomain;
|
|
1683
|
+
for (const candidate of sites.slice(0, FORMER_NAME_CANDIDATES)) {
|
|
1684
|
+
const detail2 = await call(
|
|
1685
|
+
credentials,
|
|
1686
|
+
`sites/${encodeURIComponent(candidate.siteId)}`
|
|
1687
|
+
);
|
|
1688
|
+
if (detail2.formerSubdomains?.some((former) => former.subdomain === wanted)) return candidate;
|
|
1689
|
+
}
|
|
1690
|
+
if (!SITE_ID_SHAPE.test(wanted)) return void 0;
|
|
1691
|
+
const response = await fetch(
|
|
1692
|
+
`${credentials.apiBaseUrl}/sites/${encodeURIComponent(wanted.toUpperCase())}`,
|
|
1693
|
+
{ headers: { ...credentials.headers, Authorization: `Bearer ${credentials.token}` } }
|
|
1694
|
+
);
|
|
1695
|
+
if (response.status === 404) return void 0;
|
|
1696
|
+
if (!response.ok) throw await failureOf(response);
|
|
1697
|
+
const detail = await response.json();
|
|
1698
|
+
return { siteId: detail.siteId, subdomain: detail.subdomain, url: detail.url, name: detail.name };
|
|
1699
|
+
}
|
|
1700
|
+
async function deleteSite(credentials, siteId) {
|
|
1701
|
+
const response = await fetch(`${credentials.apiBaseUrl}/sites/${encodeURIComponent(siteId)}`, {
|
|
1702
|
+
method: "DELETE",
|
|
1703
|
+
headers: { ...credentials.headers, Authorization: `Bearer ${credentials.token}` }
|
|
1704
|
+
});
|
|
1705
|
+
if (!response.ok) throw await failureOf(response);
|
|
1706
|
+
}
|
|
1707
|
+
async function promoteDeploy(credentials, siteId, deployId) {
|
|
1708
|
+
return await call(
|
|
1709
|
+
credentials,
|
|
1710
|
+
`sites/${encodeURIComponent(siteId)}/deploys/${encodeURIComponent(deployId)}/promote`,
|
|
1711
|
+
{ method: "POST" }
|
|
1826
1712
|
);
|
|
1713
|
+
}
|
|
1714
|
+
function enumName(value) {
|
|
1715
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1716
|
+
}
|
|
1717
|
+
function actionOf(expiresAt, action) {
|
|
1718
|
+
return expiresAt === null ? null : enumName(action);
|
|
1719
|
+
}
|
|
1720
|
+
async function readInvitedOnly(credentials, siteId) {
|
|
1827
1721
|
try {
|
|
1828
|
-
await
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1722
|
+
const page = await call(
|
|
1723
|
+
credentials,
|
|
1724
|
+
`sites/${encodeURIComponent(siteId)}/viewers?pageSize=1`
|
|
1725
|
+
);
|
|
1726
|
+
return typeof page.invitedOnly === "boolean" ? page.invitedOnly : null;
|
|
1727
|
+
} catch {
|
|
1728
|
+
return null;
|
|
1832
1729
|
}
|
|
1833
1730
|
}
|
|
1834
|
-
async function
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1731
|
+
async function folderFromListing(credentials, siteId, subdomain) {
|
|
1732
|
+
const { sites } = await call(
|
|
1733
|
+
credentials,
|
|
1734
|
+
`sites?pageSize=${SITES_PER_REQUEST}&q=${encodeURIComponent(subdomain)}`
|
|
1735
|
+
);
|
|
1736
|
+
return sites.find((candidate) => candidate.siteId === siteId)?.folderId ?? null;
|
|
1737
|
+
}
|
|
1738
|
+
async function getSite(credentials, siteId) {
|
|
1739
|
+
const [site, invitedOnly] = await Promise.all([
|
|
1740
|
+
call(credentials, `sites/${encodeURIComponent(siteId)}`),
|
|
1741
|
+
readInvitedOnly(credentials, siteId)
|
|
1742
|
+
]);
|
|
1743
|
+
const folderId = site.folderId !== void 0 ? site.folderId : await folderFromListing(credentials, site.siteId, site.subdomain);
|
|
1744
|
+
return {
|
|
1745
|
+
siteId: site.siteId,
|
|
1746
|
+
subdomain: site.subdomain,
|
|
1747
|
+
url: site.url,
|
|
1748
|
+
name: site.name,
|
|
1749
|
+
status: enumName(site.status),
|
|
1750
|
+
invitedOnly,
|
|
1751
|
+
liveDeployId: site.liveDeployId,
|
|
1752
|
+
spaMode: site.spaMode,
|
|
1753
|
+
docsMode: site.docsMode,
|
|
1754
|
+
modeIsManual: site.modeIsManual,
|
|
1755
|
+
passwordProtected: site.passwordProtected,
|
|
1756
|
+
passwordProtectionAvailable: site.passwordProtectionAvailable,
|
|
1757
|
+
formsEnabled: site.formsEnabled,
|
|
1758
|
+
formsAvailable: site.formsAvailable,
|
|
1759
|
+
expiresAt: site.expiresAt,
|
|
1760
|
+
expiryAction: actionOf(site.expiresAt, site.expiryAction ?? ""),
|
|
1761
|
+
scheduledExpiryAvailable: site.scheduledExpiryAvailable,
|
|
1762
|
+
comments: audienceOf(site.commentAudience),
|
|
1763
|
+
commentsAvailable: site.feedbackAvailable === true,
|
|
1764
|
+
folderId,
|
|
1765
|
+
deploys: site.deploys.map((deploy2) => ({
|
|
1766
|
+
deployId: deploy2.deployId,
|
|
1767
|
+
status: deploy2.status,
|
|
1768
|
+
fileCount: deploy2.fileCount,
|
|
1769
|
+
totalBytes: deploy2.totalBytes,
|
|
1770
|
+
createdAt: deploy2.createdAt,
|
|
1771
|
+
completedAt: deploy2.completedAt,
|
|
1772
|
+
filesKept: deploy2.filesKept
|
|
1773
|
+
})),
|
|
1774
|
+
subdomainChangesLeft: site.subdomainChangesLeft ?? null,
|
|
1775
|
+
formerSubdomains: (site.formerSubdomains ?? []).map((former) => former.subdomain)
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
async function changeSubdomain(credentials, siteId, subdomain) {
|
|
1779
|
+
const changed = await call(credentials, `sites/${encodeURIComponent(siteId)}/subdomain`, {
|
|
1780
|
+
method: "POST",
|
|
1781
|
+
body: JSON.stringify({ subdomain })
|
|
1782
|
+
});
|
|
1783
|
+
return {
|
|
1784
|
+
...changed,
|
|
1785
|
+
formerSubdomains: changed.formerSubdomains.map((former) => former.subdomain)
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
async function updateSiteSettings(credentials, siteId, change) {
|
|
1789
|
+
const body = {};
|
|
1790
|
+
if (change.name !== void 0) body.name = change.name;
|
|
1791
|
+
if (change.mode !== void 0) {
|
|
1792
|
+
body.spaMode = change.mode === "spa";
|
|
1793
|
+
body.docsMode = change.mode === "docs";
|
|
1874
1794
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1795
|
+
if (change.password !== void 0) body.password = change.password;
|
|
1796
|
+
if (change.expiresAt !== void 0) body.expiresAt = change.expiresAt;
|
|
1797
|
+
if (change.expiryAction !== void 0) body.expiryAction = change.expiryAction;
|
|
1798
|
+
if (change.formsEnabled !== void 0) body.formsEnabled = change.formsEnabled;
|
|
1799
|
+
if (change.folderId !== void 0) body.folderId = change.folderId;
|
|
1800
|
+
if (change.invitedOnly !== void 0) body.invitedOnly = change.invitedOnly;
|
|
1801
|
+
if (change.comments !== void 0) body.commentAudience = API_AUDIENCE[change.comments];
|
|
1802
|
+
if (Object.keys(body).length === 0) throw new Error("Nothing to change — name a setting.");
|
|
1803
|
+
const wasInvitedOnly = change.password !== void 0 && change.password !== "" ? await readInvitedOnly(credentials, siteId) : null;
|
|
1804
|
+
const { commentAudience, ...settings } = await call(credentials, `sites/${encodeURIComponent(siteId)}`, {
|
|
1805
|
+
method: "PATCH",
|
|
1806
|
+
body: JSON.stringify(body)
|
|
1879
1807
|
});
|
|
1808
|
+
const invitedOnly = typeof settings.invitedOnly === "boolean" ? settings.invitedOnly : null;
|
|
1809
|
+
return {
|
|
1810
|
+
...settings,
|
|
1811
|
+
expiryAction: actionOf(settings.expiresAt, settings.expiryAction ?? ""),
|
|
1812
|
+
invitedOnly,
|
|
1813
|
+
comments: audienceOf(commentAudience),
|
|
1814
|
+
replacedInviteOnly: wasInvitedOnly === true && invitedOnly === false
|
|
1815
|
+
};
|
|
1880
1816
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
}
|
|
1927
|
-
if ([400, 401, 403, 411, 413, 429].includes(status)) {
|
|
1928
|
-
throw new DeployError(ClientErrorCode.UploadFailed, refusalMessage(status, label), {
|
|
1929
|
-
path: first,
|
|
1930
|
-
status
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
lastError = new Error(`HTTP ${status}`);
|
|
1934
|
-
} catch (error) {
|
|
1935
|
-
if (error instanceof DeployError) throw error;
|
|
1936
|
-
if (isAbort(error)) throw cancelled();
|
|
1937
|
-
lastError = error;
|
|
1817
|
+
const API_AUDIENCE = {
|
|
1818
|
+
off: "Off",
|
|
1819
|
+
invited: "Invited",
|
|
1820
|
+
anyone: "Anyone"
|
|
1821
|
+
};
|
|
1822
|
+
function audienceOf(value) {
|
|
1823
|
+
const lower = value?.toLowerCase();
|
|
1824
|
+
return lower === "off" || lower === "invited" || lower === "anyone" ? lower : null;
|
|
1825
|
+
}
|
|
1826
|
+
async function setSitePaused(credentials, siteId, action) {
|
|
1827
|
+
const state = await call(
|
|
1828
|
+
credentials,
|
|
1829
|
+
`sites/${encodeURIComponent(siteId)}/${action}`,
|
|
1830
|
+
{ method: "POST" }
|
|
1831
|
+
);
|
|
1832
|
+
return {
|
|
1833
|
+
siteId: state.siteId,
|
|
1834
|
+
subdomain: state.subdomain,
|
|
1835
|
+
status: enumName(state.status),
|
|
1836
|
+
ownerPaused: state.ownerPaused
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
async function listTokens(credentials) {
|
|
1840
|
+
const body = await call(credentials, "tokens");
|
|
1841
|
+
return body.tokens;
|
|
1842
|
+
}
|
|
1843
|
+
const SITES_PER_REQUEST = 100;
|
|
1844
|
+
async function listSites(credentials) {
|
|
1845
|
+
const body = await call(
|
|
1846
|
+
credentials,
|
|
1847
|
+
`sites?pageSize=${SITES_PER_REQUEST}`
|
|
1848
|
+
);
|
|
1849
|
+
return { sites: body.sites, total: body.total };
|
|
1850
|
+
}
|
|
1851
|
+
async function createSite(credentials, subdomain, folderId) {
|
|
1852
|
+
const created = await call(
|
|
1853
|
+
credentials,
|
|
1854
|
+
"sites",
|
|
1855
|
+
{
|
|
1856
|
+
method: "POST",
|
|
1857
|
+
// Absent fields rather than null ones, because the API reads null, empty and missing the same
|
|
1858
|
+
// way and a body carrying only what was asked for is the one that reads correctly in a log.
|
|
1859
|
+
body: JSON.stringify({
|
|
1860
|
+
...subdomain === void 0 ? {} : { subdomain },
|
|
1861
|
+
...folderId === void 0 ? {} : { folderId }
|
|
1862
|
+
})
|
|
1938
1863
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
throw new DeployError(ClientErrorCode.UploadFailed, message, {
|
|
1942
|
-
path: first,
|
|
1943
|
-
cause: describeError(lastError)
|
|
1944
|
-
});
|
|
1864
|
+
);
|
|
1865
|
+
return { ...created, name: null };
|
|
1945
1866
|
}
|
|
1946
|
-
function
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1867
|
+
async function listFolders(credentials) {
|
|
1868
|
+
const body = await call(credentials, "site-folders");
|
|
1869
|
+
const byId = new Map(body.folders.map((folder) => [folder.folderId, folder]));
|
|
1870
|
+
const pathOf = (folderId) => {
|
|
1871
|
+
const names = [];
|
|
1872
|
+
let cursor = byId.get(folderId);
|
|
1873
|
+
while (cursor !== void 0 && names.length <= byId.size) {
|
|
1874
|
+
names.unshift(cursor.name);
|
|
1875
|
+
cursor = cursor.parentId === null ? void 0 : byId.get(cursor.parentId);
|
|
1876
|
+
}
|
|
1877
|
+
return names.join("/");
|
|
1878
|
+
};
|
|
1879
|
+
return body.folders.map((folder) => ({ folderId: folder.folderId, path: pathOf(folder.folderId) })).sort((a, b) => a.path.localeCompare(b.path));
|
|
1880
|
+
}
|
|
1881
|
+
function comparablePath(path) {
|
|
1882
|
+
return path.split("/").map((name) => name.trim()).filter((name) => name !== "").join("/").toLowerCase();
|
|
1883
|
+
}
|
|
1884
|
+
const FOLDERS_LISTED_ON_MISS = 20;
|
|
1885
|
+
async function resolveFolder(credentials, folder) {
|
|
1886
|
+
const wanted = comparablePath(folder);
|
|
1887
|
+
if (wanted === "") {
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
`"${folder}" does not name a folder. Leave the folder out to put the site at the top level.`
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
const folders2 = await listFolders(credentials);
|
|
1893
|
+
const byId = folders2.find((candidate) => candidate.folderId.toLowerCase() === wanted);
|
|
1894
|
+
if (byId !== void 0) return byId.folderId;
|
|
1895
|
+
const matches = folders2.filter((candidate) => comparablePath(candidate.path) === wanted);
|
|
1896
|
+
if (matches.length === 1 && matches[0] !== void 0) return matches[0].folderId;
|
|
1897
|
+
if (matches.length > 1) {
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
`More than one folder is at "${folder}", because a folder name can contain "/". Name it by id instead: ${matches.map((match) => match.folderId).join(", ")}.`
|
|
1900
|
+
);
|
|
1958
1901
|
}
|
|
1902
|
+
if (folders2.length === 0) {
|
|
1903
|
+
throw new Error(
|
|
1904
|
+
`No folder of yours is called "${folder}" — this account has no folders yet. Make one in the dashboard at ${dashboardUrlFor(credentials.apiBaseUrl)}, or leave the folder out.`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const listed = folders2.slice(0, FOLDERS_LISTED_ON_MISS).map((candidate) => candidate.path);
|
|
1908
|
+
const more = folders2.length - listed.length;
|
|
1909
|
+
throw new Error(
|
|
1910
|
+
`No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
|
|
1911
|
+
);
|
|
1959
1912
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
return
|
|
1913
|
+
const ROOT_FOLDER = "root";
|
|
1914
|
+
async function resolveFolderTarget(credentials, folder) {
|
|
1915
|
+
return folder.trim().toLowerCase() === ROOT_FOLDER ? ROOT_FOLDER : await resolveFolder(credentials, folder);
|
|
1963
1916
|
}
|
|
1964
|
-
function
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1917
|
+
async function listComments(credentials, siteId, query = {}) {
|
|
1918
|
+
const search = new URLSearchParams();
|
|
1919
|
+
if (query.status !== void 0) search.set("status", query.status);
|
|
1920
|
+
if (query.path !== void 0) search.set("path", query.path);
|
|
1921
|
+
if (query.since !== void 0) search.set("since", String(query.since));
|
|
1922
|
+
if (query.page !== void 0) search.set("page", String(query.page));
|
|
1923
|
+
const encoded = search.toString();
|
|
1924
|
+
const suffix = encoded === "" ? "" : `?${encoded}`;
|
|
1925
|
+
return await call(
|
|
1926
|
+
credentials,
|
|
1927
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads${suffix}`
|
|
1928
|
+
);
|
|
1968
1929
|
}
|
|
1969
|
-
function
|
|
1970
|
-
return
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1930
|
+
async function replyToComment(credentials, siteId, threadId, body, resolve2 = false) {
|
|
1931
|
+
return await call(
|
|
1932
|
+
credentials,
|
|
1933
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/comments`,
|
|
1934
|
+
{ method: "POST", body: JSON.stringify({ body, resolve: resolve2 }) }
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
async function setCommentResolved(credentials, siteId, threadId, resolved) {
|
|
1938
|
+
return await call(
|
|
1939
|
+
credentials,
|
|
1940
|
+
`sites/${encodeURIComponent(siteId)}/feedback/threads/${encodeURIComponent(threadId)}/${resolved ? "resolve" : "reopen"}`,
|
|
1941
|
+
{ method: "POST" }
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
const PEOPLE_PER_REQUEST = 50;
|
|
1945
|
+
async function listPeople(credentials, siteId) {
|
|
1946
|
+
const page = await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers?pageSize=${PEOPLE_PER_REQUEST}`);
|
|
1947
|
+
return {
|
|
1948
|
+
invitedOnly: page.invitedOnly,
|
|
1949
|
+
total: page.total,
|
|
1950
|
+
limit: page.limit,
|
|
1951
|
+
people: page.viewers.map((row) => ({
|
|
1952
|
+
email: row.email,
|
|
1953
|
+
role: enumName(row.role),
|
|
1954
|
+
owner: row.owner,
|
|
1955
|
+
status: enumName(row.status)
|
|
1956
|
+
}))
|
|
1987
1957
|
};
|
|
1988
1958
|
}
|
|
1989
|
-
function
|
|
1990
|
-
return (
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
1959
|
+
async function invitePeople(credentials, siteId, emails, role) {
|
|
1960
|
+
return await call(credentials, `sites/${encodeURIComponent(siteId)}/viewers`, {
|
|
1961
|
+
method: "POST",
|
|
1962
|
+
body: JSON.stringify({ emails, role: role === "comment" ? "Comment" : "View", notify: true })
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
async function removePerson(credentials, siteId, email) {
|
|
1966
|
+
await call(
|
|
1967
|
+
credentials,
|
|
1968
|
+
`sites/${encodeURIComponent(siteId)}/viewers?email=${encodeURIComponent(email)}`,
|
|
1969
|
+
{ method: "DELETE" }
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
function describeMode(result) {
|
|
1973
|
+
if (result.mode === null) return null;
|
|
1974
|
+
if (result.nestedIndex !== null) {
|
|
1975
|
+
return `Served as documents: there is no index.html at the top, but ${result.nestedIndex}/index.html is there. To publish the website, publish the ${result.nestedIndex} folder instead.`;
|
|
1976
|
+
}
|
|
1977
|
+
if (result.mode === "docs") return "Served as documents, in the documents reader.";
|
|
1978
|
+
if (result.mode === "spa") return "Served as a single-page app.";
|
|
1979
|
+
return "Served as a static site.";
|
|
1980
|
+
}
|
|
1981
|
+
async function ownSiteAt(credentials, subdomain) {
|
|
1982
|
+
const found = await lookUpSite(credentials, subdomain);
|
|
1983
|
+
return found?.subdomain.toLowerCase() === subdomain.trim().toLowerCase() ? found : void 0;
|
|
1984
|
+
}
|
|
1985
|
+
async function resolveSite(credentials, site, newSite) {
|
|
1986
|
+
const { subdomain, folder } = newSite;
|
|
1987
|
+
if (site !== void 0 && subdomain !== void 0) {
|
|
1988
|
+
throw new Error(
|
|
1989
|
+
"Give either an existing site or a subdomain for a new one, not both: publishing over a site and creating one are different requests."
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
if (site !== void 0 && folder !== void 0) {
|
|
1993
|
+
throw new Error(
|
|
1994
|
+
"A folder only says where a new site goes, so it cannot be given with an existing site. Leave the site out to create one in that folder, or move the existing site with `drop2run set folder` or the `update_site` tool's `folder`."
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
if (site === void 0) {
|
|
1998
|
+
const folderId = folder === void 0 ? void 0 : await resolveFolder(credentials, folder);
|
|
1999
|
+
const held = subdomain === void 0 ? void 0 : await ownSiteAt(credentials, subdomain);
|
|
2000
|
+
if (held !== void 0) {
|
|
2001
|
+
if (held.liveDeployId === null) return held;
|
|
2002
|
+
throw new Error(
|
|
2003
|
+
`You already have a site at ${held.subdomain}, and it is live. To replace what it serves, publish to it as an existing site (the \`site\` argument, or \`--site\` on the command line); to make a new site, choose another subdomain.`
|
|
2004
|
+
);
|
|
2015
2005
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2006
|
+
return await createSite(credentials, subdomain, folderId);
|
|
2007
|
+
}
|
|
2008
|
+
const found = await lookUpSite(credentials, site);
|
|
2009
|
+
if (found === void 0) {
|
|
2010
|
+
throw new Error(
|
|
2011
|
+
`No site of yours is called "${site}". Leave the site out to publish to a new one, or list your sites to see what exists.`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
return found;
|
|
2015
|
+
}
|
|
2016
|
+
async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
2017
|
+
const target = await resolveSite(credentials, site, newSite);
|
|
2018
|
+
let files = 0;
|
|
2019
|
+
let url = target.url;
|
|
2020
|
+
let unchanged = false;
|
|
2021
|
+
let mode = null;
|
|
2022
|
+
let paths = [];
|
|
2023
|
+
let failure2 = null;
|
|
2024
|
+
let failureCode;
|
|
2025
|
+
let failureDetail;
|
|
2026
|
+
const record = (event) => {
|
|
2027
|
+
if (event.type === "collecting") files = event.files;
|
|
2028
|
+
if (event.type === "unchanged") {
|
|
2029
|
+
unchanged = true;
|
|
2030
|
+
url = event.url || target.url;
|
|
2031
|
+
}
|
|
2032
|
+
if (event.type === "done") {
|
|
2033
|
+
url = event.url;
|
|
2034
|
+
mode = event.mode;
|
|
2035
|
+
}
|
|
2036
|
+
if (event.type === "error") {
|
|
2037
|
+
failure2 = event.message;
|
|
2038
|
+
failureCode = event.code;
|
|
2039
|
+
failureDetail = event.detail;
|
|
2040
|
+
}
|
|
2041
|
+
onProgress?.(event);
|
|
2042
|
+
};
|
|
2043
|
+
const watched = async (signal, onRead) => {
|
|
2044
|
+
const collected = await source(signal, onRead);
|
|
2045
|
+
paths = collected.map((file) => file.path);
|
|
2046
|
+
return collected;
|
|
2047
|
+
};
|
|
2048
|
+
await deploy(watched, target.siteId, record, void 0, {
|
|
2049
|
+
baseUrl: credentials.apiBaseUrl,
|
|
2050
|
+
token: credentials.token,
|
|
2051
|
+
...credentials.headers === void 0 ? {} : { headers: credentials.headers }
|
|
2052
|
+
}).catch((error) => {
|
|
2053
|
+
const thrown = new Error(
|
|
2054
|
+
failure2 ?? (error instanceof Error ? error.message : String(error))
|
|
2028
2055
|
);
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2056
|
+
thrown.detail = failureDetail ?? error?.detail;
|
|
2057
|
+
thrown.code = failureCode ?? error?.code;
|
|
2058
|
+
throw thrown;
|
|
2032
2059
|
});
|
|
2060
|
+
return {
|
|
2061
|
+
url,
|
|
2062
|
+
siteId: target.siteId,
|
|
2063
|
+
subdomain: target.subdomain,
|
|
2064
|
+
files,
|
|
2065
|
+
unchanged,
|
|
2066
|
+
mode,
|
|
2067
|
+
nestedIndex: mode === "docs" ? nestedIndexOf(paths) : null
|
|
2068
|
+
};
|
|
2033
2069
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
return globalThis.fetch.bind(globalThis);
|
|
2070
|
+
const MAX_FILES = 2e4;
|
|
2071
|
+
function directorySource(root) {
|
|
2072
|
+
return async () => {
|
|
2073
|
+
const base = await realpath(root);
|
|
2074
|
+
const files = [];
|
|
2075
|
+
await walk(base, base, files);
|
|
2076
|
+
return files;
|
|
2077
|
+
};
|
|
2043
2078
|
}
|
|
2044
|
-
function
|
|
2045
|
-
|
|
2079
|
+
async function walk(root, directory, into) {
|
|
2080
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
2081
|
+
const full = join(directory, entry.name);
|
|
2082
|
+
const path = relative(root, full).split(sep).join("/");
|
|
2083
|
+
if (shouldIgnore(path)) continue;
|
|
2084
|
+
if (entry.isSymbolicLink()) {
|
|
2085
|
+
const target = await realpath(full).catch(() => null);
|
|
2086
|
+
if (target === null || escapesRoot(root, target)) continue;
|
|
2087
|
+
const targeted = await stat(target);
|
|
2088
|
+
if (targeted.isDirectory()) {
|
|
2089
|
+
await walk(root, target, into);
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
if (!targeted.isFile()) continue;
|
|
2093
|
+
} else if (entry.isDirectory()) {
|
|
2094
|
+
await walk(root, full, into);
|
|
2095
|
+
continue;
|
|
2096
|
+
} else if (!entry.isFile()) {
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
if (into.length >= MAX_FILES) {
|
|
2100
|
+
throw new Error(
|
|
2101
|
+
`This folder holds more than ${MAX_FILES.toLocaleString()} files. Publish a build output directory rather than a whole project.`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
into.push({ path, bytes: new Uint8Array(await readFile(full)) });
|
|
2105
|
+
}
|
|
2046
2106
|
}
|
|
2047
|
-
function
|
|
2048
|
-
|
|
2107
|
+
function escapesRoot(root, candidate) {
|
|
2108
|
+
const inside = relative(root, candidate);
|
|
2109
|
+
return inside === "" || inside.startsWith("..");
|
|
2049
2110
|
}
|
|
2050
|
-
function
|
|
2051
|
-
|
|
2111
|
+
async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
|
|
2112
|
+
const found = await stat(directory).catch(() => null);
|
|
2113
|
+
if (found === null) throw new Error(`There is no folder at ${directory}.`);
|
|
2114
|
+
if (!found.isDirectory()) throw new Error(`${directory} is a file, not a folder to publish.`);
|
|
2115
|
+
return await publish(credentials, directorySource(directory), site, onProgress, newSite);
|
|
2052
2116
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2117
|
+
const TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2118
|
+
function newAttempt() {
|
|
2119
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
2120
|
+
return {
|
|
2121
|
+
verifier,
|
|
2122
|
+
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
2123
|
+
state: randomBytes(16).toString("base64url")
|
|
2124
|
+
};
|
|
2059
2125
|
}
|
|
2060
|
-
function
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
return
|
|
2065
|
-
ClientErrorCode.UploadFailed,
|
|
2066
|
-
error instanceof Error ? error.message : "Upload failed.",
|
|
2067
|
-
error
|
|
2068
|
-
);
|
|
2126
|
+
function clientName(surface) {
|
|
2127
|
+
const clean2 = (value) => value.trim().replace(/[^A-Za-z0-9._-]/g, "-");
|
|
2128
|
+
const label = "";
|
|
2129
|
+
const raw = clean2(hostname()).slice(0, 64 - label.length);
|
|
2130
|
+
return `${raw === "" ? "drop2run" : raw}${label}`;
|
|
2069
2131
|
}
|
|
2070
|
-
|
|
2132
|
+
function openBrowser(url) {
|
|
2133
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
2071
2134
|
try {
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
(bytes, total) => onProgress({ type: "reading", bytes, total })
|
|
2076
|
-
);
|
|
2077
|
-
onProgress({ type: "collecting", files: collected.length });
|
|
2078
|
-
const rename = renameOfLoneHtmlPage(collected.map((file) => file.path));
|
|
2079
|
-
const files = rename === null ? collected : collected.map((file) => file.path === rename ? { ...file, path: "index.html" } : file);
|
|
2080
|
-
const hashed = await hashAll(
|
|
2081
|
-
files,
|
|
2082
|
-
(done, total) => onProgress({ type: "hashing", done, total }),
|
|
2083
|
-
signal
|
|
2084
|
-
);
|
|
2085
|
-
checkLimits(hashed, options.limits ?? null);
|
|
2086
|
-
onProgress({ type: "preparing" });
|
|
2087
|
-
const prepared = await prepareDeploy(siteId, hashed, options, signal);
|
|
2088
|
-
if (prepared.unchanged) {
|
|
2089
|
-
onProgress({ type: "unchanged", url: prepared.url ?? "" });
|
|
2090
|
-
return;
|
|
2091
|
-
}
|
|
2092
|
-
const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
|
|
2093
|
-
const batches = prepared.batches ?? [];
|
|
2094
|
-
const targets = [...prepared.upload, ...batches];
|
|
2095
|
-
const totalBytes = prepared.upload.reduce((sum, target) => sum + (sizes.get(target.path) ?? 0), 0) + batches.reduce((sum, batch) => {
|
|
2096
|
-
const members = batch.files.map((ref) => ({
|
|
2097
|
-
sha256: ref.sha256,
|
|
2098
|
-
size: sizes.get(ref.path) ?? 0
|
|
2099
|
-
}));
|
|
2100
|
-
return sum + batchLayout(members).length + members.reduce((n, member) => n + member.size, 0);
|
|
2101
|
-
}, 0);
|
|
2102
|
-
if (targets.length > 0) {
|
|
2103
|
-
onProgress({
|
|
2104
|
-
type: "uploading",
|
|
2105
|
-
done: prepared.reused,
|
|
2106
|
-
total: prepared.total,
|
|
2107
|
-
bytes: 0,
|
|
2108
|
-
totalBytes,
|
|
2109
|
-
reused: prepared.reused
|
|
2110
|
-
});
|
|
2111
|
-
}
|
|
2112
|
-
await uploadAll(
|
|
2113
|
-
targets,
|
|
2114
|
-
prepared.uploadUrl,
|
|
2115
|
-
hashed,
|
|
2116
|
-
(done, _total, bytes, path) => onProgress({
|
|
2117
|
-
type: "uploading",
|
|
2118
|
-
done: prepared.reused + done,
|
|
2119
|
-
total: prepared.total,
|
|
2120
|
-
bytes,
|
|
2121
|
-
totalBytes,
|
|
2122
|
-
reused: prepared.reused,
|
|
2123
|
-
...path === void 0 ? {} : { path }
|
|
2124
|
-
}),
|
|
2125
|
-
signal,
|
|
2126
|
-
// The server's number unless the caller set one: it is tuned on the server so a deploy of many
|
|
2127
|
-
// small files speeds up without a CLI or MCP release.
|
|
2128
|
-
typeof prepared.batchConcurrency === "number" && options.upload?.batchConcurrency === void 0 ? { ...options.upload, batchConcurrency: prepared.batchConcurrency } : options.upload,
|
|
2129
|
-
(path, attempt, attempts, stalled) => onProgress({ type: "retrying", path, attempt, attempts, stalled })
|
|
2130
|
-
);
|
|
2131
|
-
onProgress({ type: "completing", reused: prepared.reused });
|
|
2132
|
-
const completed = await completeDeploy(
|
|
2133
|
-
siteId,
|
|
2134
|
-
prepared.deployId,
|
|
2135
|
-
options,
|
|
2136
|
-
signal,
|
|
2137
|
-
await readSiteName(files)
|
|
2138
|
-
);
|
|
2139
|
-
onProgress({
|
|
2140
|
-
type: "done",
|
|
2141
|
-
url: completed.url,
|
|
2142
|
-
name: completed.name,
|
|
2143
|
-
mode: servingModeOf(completed),
|
|
2144
|
-
modeIsManual: completed.modeIsManual
|
|
2135
|
+
const child = spawn(command, [...args], { stdio: "ignore", detached: true });
|
|
2136
|
+
child.unref();
|
|
2137
|
+
child.on("error", () => {
|
|
2145
2138
|
});
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2139
|
+
return true;
|
|
2140
|
+
} catch {
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function listen(state) {
|
|
2145
|
+
return new Promise((resolveListener, rejectListener) => {
|
|
2146
|
+
let settle = null;
|
|
2147
|
+
let fail = null;
|
|
2148
|
+
const received = new Promise((resolveCode, rejectCode) => {
|
|
2149
|
+
settle = resolveCode;
|
|
2150
|
+
fail = rejectCode;
|
|
2151
|
+
});
|
|
2152
|
+
const server = createServer((request2, response) => {
|
|
2153
|
+
const url = new URL(request2.url ?? "/", "http://127.0.0.1");
|
|
2154
|
+
if (url.pathname !== "/cb") {
|
|
2155
|
+
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
2156
|
+
response.end("Not found.\n");
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const code = url.searchParams.get("code") ?? "";
|
|
2160
|
+
const returned = url.searchParams.get("state") ?? "";
|
|
2161
|
+
if (returned !== state || code === "") {
|
|
2162
|
+
respond(
|
|
2163
|
+
response,
|
|
2164
|
+
400,
|
|
2165
|
+
"Sign-in could not be completed",
|
|
2166
|
+
"This did not come from the sign-in that was started here. Nothing has been changed."
|
|
2167
|
+
);
|
|
2168
|
+
fail?.(new Error("The browser returned a response that did not match this sign-in."));
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
respond(response, 200, "Signed in", "You can close this tab now.");
|
|
2172
|
+
settle?.({ code });
|
|
2173
|
+
});
|
|
2174
|
+
server.on("error", (error) => {
|
|
2175
|
+
rejectListener(error);
|
|
2176
|
+
fail?.(error instanceof Error ? error : new Error(String(error)));
|
|
2177
|
+
});
|
|
2178
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2179
|
+
const address = server.address();
|
|
2180
|
+
if (address === null) {
|
|
2181
|
+
rejectListener(new Error("Could not open a local port to receive the sign-in."));
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
resolveListener({
|
|
2185
|
+
port: address.port,
|
|
2186
|
+
received,
|
|
2187
|
+
close: () => server.close()
|
|
2188
|
+
});
|
|
2153
2189
|
});
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2190
|
+
});
|
|
2156
2191
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
return files;
|
|
2164
|
-
};
|
|
2192
|
+
function respond(response, status, title, detail) {
|
|
2193
|
+
response.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
2194
|
+
response.end(
|
|
2195
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title} — drop2run</title><style>body{font:16px/1.6 system-ui,sans-serif;margin:20vh auto;max-width:28rem;padding:0 1.5rem;color:#12211f}h1{font-size:1.4rem;margin:0 0 .5rem}p{margin:0;color:#64706e}@media(prefers-color-scheme:dark){body{background:#0d1413;color:#e8efee}p{color:#9aa8a6}}</style></head><body><h1>${title}</h1><p>${detail}</p></body></html>
|
|
2196
|
+
`
|
|
2197
|
+
);
|
|
2165
2198
|
}
|
|
2166
|
-
async function
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
} else if (entry.isDirectory()) {
|
|
2181
|
-
await walk(root, full, into);
|
|
2182
|
-
continue;
|
|
2183
|
-
} else if (!entry.isFile()) {
|
|
2184
|
-
continue;
|
|
2185
|
-
}
|
|
2186
|
-
if (into.length >= MAX_FILES) {
|
|
2187
|
-
throw new Error(
|
|
2188
|
-
`This folder holds more than ${MAX_FILES.toLocaleString()} files. Publish a build output directory rather than a whole project.`
|
|
2189
|
-
);
|
|
2190
|
-
}
|
|
2191
|
-
into.push({ path, bytes: new Uint8Array(await readFile(full)) });
|
|
2199
|
+
async function waitForCallback(received, timeoutMs = TIMEOUT_MS) {
|
|
2200
|
+
let timer;
|
|
2201
|
+
try {
|
|
2202
|
+
return await Promise.race([
|
|
2203
|
+
received,
|
|
2204
|
+
new Promise((_, reject) => {
|
|
2205
|
+
timer = setTimeout(
|
|
2206
|
+
() => reject(new Error("Timed out waiting for the browser. Nothing has been changed.")),
|
|
2207
|
+
timeoutMs
|
|
2208
|
+
);
|
|
2209
|
+
})
|
|
2210
|
+
]);
|
|
2211
|
+
} finally {
|
|
2212
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2192
2213
|
}
|
|
2193
2214
|
}
|
|
2194
|
-
function
|
|
2195
|
-
const
|
|
2196
|
-
|
|
2215
|
+
function consentUrl(dashboardUrl, attempt, port, name) {
|
|
2216
|
+
const url = new URL("/cli-auth", dashboardUrl);
|
|
2217
|
+
url.searchParams.set("challenge", attempt.challenge);
|
|
2218
|
+
url.searchParams.set("state", attempt.state);
|
|
2219
|
+
url.searchParams.set("port", String(port));
|
|
2220
|
+
url.searchParams.set("name", name);
|
|
2221
|
+
return url.toString();
|
|
2197
2222
|
}
|
|
2198
|
-
function
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
if (
|
|
2205
|
-
return
|
|
2223
|
+
async function startDevice(apiBaseUrl, clientName2) {
|
|
2224
|
+
const response = await fetch(`${apiBaseUrl}/auth/cli/device`, {
|
|
2225
|
+
method: "POST",
|
|
2226
|
+
headers: { "Content-Type": "application/json" },
|
|
2227
|
+
body: JSON.stringify({ clientName: clientName2 })
|
|
2228
|
+
});
|
|
2229
|
+
if (!response.ok) throw new Error(await problemDetail(response));
|
|
2230
|
+
return await response.json();
|
|
2206
2231
|
}
|
|
2207
|
-
async function
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
);
|
|
2218
|
-
}
|
|
2219
|
-
if (site === void 0) {
|
|
2220
|
-
const folderId = folder === void 0 ? void 0 : await resolveFolder(credentials, folder);
|
|
2221
|
-
return await createSite(credentials, subdomain, folderId);
|
|
2232
|
+
async function pollDevice(apiBaseUrl, deviceCode) {
|
|
2233
|
+
let response;
|
|
2234
|
+
try {
|
|
2235
|
+
response = await fetch(`${apiBaseUrl}/auth/cli/device/token`, {
|
|
2236
|
+
method: "POST",
|
|
2237
|
+
headers: { "Content-Type": "application/json" },
|
|
2238
|
+
body: JSON.stringify({ deviceCode })
|
|
2239
|
+
});
|
|
2240
|
+
} catch {
|
|
2241
|
+
return { state: "pending" };
|
|
2222
2242
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
`No site of yours is called "${site}". Leave the site out to publish to a new one, or list your sites to see what exists.`
|
|
2227
|
-
);
|
|
2243
|
+
if (response.ok) {
|
|
2244
|
+
const body = await response.json();
|
|
2245
|
+
return { state: "granted", token: { token: body.token, name: body.name, email: body.email } };
|
|
2228
2246
|
}
|
|
2229
|
-
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
let unchanged = false;
|
|
2236
|
-
let mode = null;
|
|
2237
|
-
let paths = [];
|
|
2238
|
-
let failure2 = null;
|
|
2239
|
-
let failureDetail;
|
|
2240
|
-
const record = (event) => {
|
|
2241
|
-
if (event.type === "collecting") files = event.files;
|
|
2242
|
-
if (event.type === "unchanged") {
|
|
2243
|
-
unchanged = true;
|
|
2244
|
-
url = event.url || target.url;
|
|
2245
|
-
}
|
|
2246
|
-
if (event.type === "done") {
|
|
2247
|
-
url = event.url;
|
|
2248
|
-
mode = event.mode;
|
|
2249
|
-
}
|
|
2250
|
-
if (event.type === "error") {
|
|
2251
|
-
failure2 = event.message;
|
|
2252
|
-
failureDetail = event.detail;
|
|
2253
|
-
}
|
|
2254
|
-
onProgress?.(event);
|
|
2255
|
-
};
|
|
2256
|
-
const watched = async (signal, onRead) => {
|
|
2257
|
-
const collected = await source(signal, onRead);
|
|
2258
|
-
paths = collected.map((file) => file.path);
|
|
2259
|
-
return collected;
|
|
2247
|
+
const problem = await response.json().catch(() => null);
|
|
2248
|
+
if (problem?.type === "authorization_pending") return { state: "pending" };
|
|
2249
|
+
if (problem?.type === "slow_down") return { state: "slow_down" };
|
|
2250
|
+
return {
|
|
2251
|
+
state: "dead",
|
|
2252
|
+
message: problem?.detail ?? `The API answered ${response.status}.`
|
|
2260
2253
|
};
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2254
|
+
}
|
|
2255
|
+
async function problemDetail(response) {
|
|
2256
|
+
const problem = await response.json().catch(() => null);
|
|
2257
|
+
return problem?.detail ?? `The API answered ${response.status}.`;
|
|
2258
|
+
}
|
|
2259
|
+
async function exchange(apiBaseUrl, code, verifier) {
|
|
2260
|
+
const response = await fetch(`${apiBaseUrl}/auth/cli/token`, {
|
|
2261
|
+
method: "POST",
|
|
2262
|
+
headers: { "Content-Type": "application/json" },
|
|
2263
|
+
body: JSON.stringify({ code, verifier })
|
|
2264
|
+
});
|
|
2265
|
+
if (!response.ok) throw new Error(await problemDetail(response));
|
|
2266
|
+
const body = await response.json();
|
|
2267
|
+
return { token: body.token, name: body.name, email: body.email };
|
|
2268
|
+
}
|
|
2269
|
+
const VERSIONS_LISTED = 10;
|
|
2270
|
+
function servingModeOf(site) {
|
|
2271
|
+
if (site.spaMode) return "spa";
|
|
2272
|
+
if (site.docsMode) return "docs";
|
|
2273
|
+
return "static";
|
|
2274
|
+
}
|
|
2275
|
+
function accessOf(site) {
|
|
2276
|
+
if (site.passwordProtected) return "password";
|
|
2277
|
+
if (site.invitedOnly === true) return "invite-only";
|
|
2278
|
+
if (site.invitedOnly === false) return "public";
|
|
2279
|
+
return "no password (whether it is invite-only could not be read)";
|
|
2280
|
+
}
|
|
2281
|
+
function addressLines(site) {
|
|
2282
|
+
return [
|
|
2283
|
+
...site.formerSubdomains.length > 0 ? [` formerly ${site.formerSubdomains.join(", ")} (redirect here)`] : [],
|
|
2284
|
+
...site.subdomainChangesLeft === null ? [] : [` address ${site.subdomainChangesLeft} change(s) left`]
|
|
2285
|
+
];
|
|
2286
|
+
}
|
|
2287
|
+
function describeSite(site, folderPath) {
|
|
2288
|
+
const unavailable = (available) => available ? "" : " (not on this plan)";
|
|
2289
|
+
const versions = site.deploys.slice(0, VERSIONS_LISTED).map((deploy2) => {
|
|
2290
|
+
const live = deploy2.deployId === site.liveDeployId ? " ← served" : "";
|
|
2291
|
+
const kept = deploy2.filesKept ? "" : ", files collected — cannot be rolled back to";
|
|
2292
|
+
return ` ${deploy2.deployId} ${deploy2.createdAt} ${deploy2.status}, ${deploy2.fileCount} files${kept}${live}`;
|
|
2270
2293
|
});
|
|
2294
|
+
const more = site.deploys.length - versions.length;
|
|
2295
|
+
return [
|
|
2296
|
+
`${site.subdomain} — ${site.url}`,
|
|
2297
|
+
` name ${site.name ?? "(none)"}`,
|
|
2298
|
+
` status ${site.status}`,
|
|
2299
|
+
` access ${accessOf(site)}`,
|
|
2300
|
+
` mode ${servingModeOf(site)}${site.modeIsManual ? "" : " (detected on each publish)"}`,
|
|
2301
|
+
` password ${site.passwordProtected ? "on" : "off"}${unavailable(site.passwordProtectionAvailable)}`,
|
|
2302
|
+
` forms ${site.formsEnabled ? "on" : "off"}${unavailable(site.formsAvailable)}`,
|
|
2303
|
+
` comments ${site.comments ?? "unknown"}${unavailable(site.commentsAvailable)}`,
|
|
2304
|
+
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
2305
|
+
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
2306
|
+
...addressLines(site),
|
|
2307
|
+
"",
|
|
2308
|
+
site.deploys.length === 0 ? "No versions yet." : "Versions, newest first:",
|
|
2309
|
+
...versions,
|
|
2310
|
+
...more > 0 ? [` … and ${more} older`] : []
|
|
2311
|
+
].join("\n");
|
|
2312
|
+
}
|
|
2313
|
+
const PROJECT_FILE = "drop2run.json";
|
|
2314
|
+
function projectPath(directory = process.cwd()) {
|
|
2315
|
+
return join(directory, PROJECT_FILE);
|
|
2316
|
+
}
|
|
2317
|
+
function readProject(directory = process.cwd(), read = (path) => readFileSync(path, "utf8")) {
|
|
2318
|
+
let parsed;
|
|
2319
|
+
try {
|
|
2320
|
+
parsed = JSON.parse(read(projectPath(directory)));
|
|
2321
|
+
} catch {
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
if (typeof parsed.siteId !== "string" || parsed.siteId === "") return null;
|
|
2271
2325
|
return {
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
files,
|
|
2276
|
-
unchanged,
|
|
2277
|
-
mode,
|
|
2278
|
-
nestedIndex: mode === "docs" ? nestedIndexOf(paths) : null
|
|
2326
|
+
siteId: parsed.siteId,
|
|
2327
|
+
subdomain: typeof parsed.subdomain === "string" ? parsed.subdomain : "",
|
|
2328
|
+
dir: typeof parsed.dir === "string" && parsed.dir !== "" ? parsed.dir : "."
|
|
2279
2329
|
};
|
|
2280
2330
|
}
|
|
2281
|
-
|
|
2282
|
-
const
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
return
|
|
2331
|
+
function writeProject(project, directory = process.cwd()) {
|
|
2332
|
+
const path = projectPath(directory);
|
|
2333
|
+
writeFileSync(path, `${JSON.stringify(project, null, " ")}
|
|
2334
|
+
`);
|
|
2335
|
+
return path;
|
|
2286
2336
|
}
|
|
2287
2337
|
const silentProgress = Object.assign(() => {
|
|
2288
2338
|
}, { done: () => {
|
|
@@ -2762,7 +2812,7 @@ async function info(site) {
|
|
|
2762
2812
|
)?.path;
|
|
2763
2813
|
return {
|
|
2764
2814
|
text: describeSite(detail, folderPath),
|
|
2765
|
-
json: { ...detail, mode: servingModeOf
|
|
2815
|
+
json: { ...detail, mode: servingModeOf(detail), folderPath: folderPath ?? null },
|
|
2766
2816
|
code: 0
|
|
2767
2817
|
};
|
|
2768
2818
|
} catch (error) {
|
|
@@ -2799,7 +2849,8 @@ const SETTINGS_USAGE = [
|
|
|
2799
2849
|
" expires <ISO 8601 instant> When it comes down; add --then delete to delete rather than pause",
|
|
2800
2850
|
" folder <path|id|root> Which folder it is filed in",
|
|
2801
2851
|
" invite-only on|off Whether only the people on its list can open it",
|
|
2802
|
-
" comments off|invited|anyone Who may comment on its pages"
|
|
2852
|
+
" comments off|invited|anyone Who may comment on its pages",
|
|
2853
|
+
" subdomain <name> Its address; needs --yes, the old one keeps redirecting"
|
|
2803
2854
|
].join("\n");
|
|
2804
2855
|
function settingChange(setting, values, then) {
|
|
2805
2856
|
const value = setting === "name" ? values.join(" ").trim() : values[0];
|
|
@@ -2847,6 +2898,8 @@ function settingChange(setting, values, then) {
|
|
|
2847
2898
|
}
|
|
2848
2899
|
case "folder":
|
|
2849
2900
|
return value ? { folder: value } : needs("Clients/Acme");
|
|
2901
|
+
case "subdomain":
|
|
2902
|
+
return value ? { subdomain: value } : needs("tidy-otter --yes");
|
|
2850
2903
|
case "invite-only":
|
|
2851
2904
|
if (value === "on" || value === "off") return { change: { invitedOnly: value === "on" } };
|
|
2852
2905
|
return { refusal: "`set invite-only` takes `on` or `off`." };
|
|
@@ -2872,6 +2925,15 @@ async function set(site, setting, values, options, readPassword = () => readSecr
|
|
|
2872
2925
|
try {
|
|
2873
2926
|
const summary = await findSite(found.credentials, target.named);
|
|
2874
2927
|
let change;
|
|
2928
|
+
if ("subdomain" in parsed) {
|
|
2929
|
+
return await moved(
|
|
2930
|
+
found.credentials,
|
|
2931
|
+
summary.siteId,
|
|
2932
|
+
summary.subdomain,
|
|
2933
|
+
parsed.subdomain,
|
|
2934
|
+
options.confirmed
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2875
2937
|
if ("folder" in parsed) {
|
|
2876
2938
|
change = { folderId: await resolveFolderTarget(found.credentials, parsed.folder) };
|
|
2877
2939
|
} else if ("password" in parsed) {
|
|
@@ -2894,6 +2956,29 @@ Add --yes if that is what you want, or leave out --then to pause it instead.`
|
|
|
2894
2956
|
return failure(error instanceof Error ? error.message : String(error));
|
|
2895
2957
|
}
|
|
2896
2958
|
}
|
|
2959
|
+
async function moved(credentials, siteId, current, wanted, confirmed) {
|
|
2960
|
+
if (!confirmed) {
|
|
2961
|
+
const site = await getSite(credentials, siteId);
|
|
2962
|
+
const left2 = site.subdomainChangesLeft === null ? "Your plan does not limit how often it changes." : `It spends one of the ${site.subdomainChangesLeft} address change(s) this site has left, a change back to an old name included.`;
|
|
2963
|
+
return failure(
|
|
2964
|
+
`This moves ${current} to ${wanted}. ${current} keeps working and redirects there, and nobody else can take it. ${left2}
|
|
2965
|
+
Add --yes if that is what you want.`
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
const change = await changeSubdomain(credentials, siteId, wanted);
|
|
2969
|
+
const project = readProject();
|
|
2970
|
+
if (project?.siteId === siteId && project.subdomain !== change.subdomain) {
|
|
2971
|
+
writeProject({ ...project, subdomain: change.subdomain });
|
|
2972
|
+
}
|
|
2973
|
+
const left = change.subdomainChangesLeft === null ? "" : `
|
|
2974
|
+
${change.subdomainChangesLeft} address change(s) left.`;
|
|
2975
|
+
return {
|
|
2976
|
+
text: `${current} is now ${change.url}
|
|
2977
|
+
The old address redirects there.${left}`,
|
|
2978
|
+
json: change,
|
|
2979
|
+
code: 0
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2897
2982
|
async function unset(site, setting, extra = []) {
|
|
2898
2983
|
if (extra.length > 0) {
|
|
2899
2984
|
return failure(
|
|
@@ -2930,7 +3015,7 @@ async function applied(credentials, siteId, subdomain, change) {
|
|
|
2930
3015
|
`Updated ${subdomain}.`,
|
|
2931
3016
|
` name ${settings.name ?? "(none)"}`,
|
|
2932
3017
|
` access ${accessOf(settings)}`,
|
|
2933
|
-
` mode ${servingModeOf
|
|
3018
|
+
` mode ${servingModeOf(settings)}`,
|
|
2934
3019
|
` forms ${settings.formsEnabled ? "on" : "off"}`,
|
|
2935
3020
|
` comments ${settings.comments ?? "unknown"}`,
|
|
2936
3021
|
` takedown ${takedown}`,
|
|
@@ -2943,7 +3028,7 @@ async function applied(credentials, siteId, subdomain, change) {
|
|
|
2943
3028
|
] : [],
|
|
2944
3029
|
settings.live ? "Reaching every edge takes up to about a minute." : "Nothing is published yet, so this applies from the first publish."
|
|
2945
3030
|
].join("\n"),
|
|
2946
|
-
json: { ...settings, mode: servingModeOf
|
|
3031
|
+
json: { ...settings, mode: servingModeOf(settings) },
|
|
2947
3032
|
code: 0
|
|
2948
3033
|
};
|
|
2949
3034
|
}
|
|
@@ -3290,7 +3375,7 @@ async function run(commandLine) {
|
|
|
3290
3375
|
const positional = [...positionals(argv), ...literal];
|
|
3291
3376
|
const [command, ...rest] = positional;
|
|
3292
3377
|
if (argv.includes("--version")) {
|
|
3293
|
-
const { version } = await import("./package-
|
|
3378
|
+
const { version } = await import("./package-DgeW4aj9.js").then(
|
|
3294
3379
|
(module) => module.default
|
|
3295
3380
|
);
|
|
3296
3381
|
return { text: version, json: { version }, code: 0 };
|