@dropthis/cli 0.5.0 → 0.6.1

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.
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  CursorPage: () => CursorPage,
34
34
  DeploymentsResource: () => DeploymentsResource,
35
35
  Dropthis: () => Dropthis,
36
+ PublishInputError: () => PublishInputError,
36
37
  UploadsResource: () => UploadsResource,
37
38
  createErrorResult: () => createErrorResult,
38
39
  redactSecrets: () => redactSecrets
@@ -52,33 +53,42 @@ function createErrorResult(code, message, statusCode, extra = {}) {
52
53
  message: redactSecrets(message),
53
54
  statusCode,
54
55
  ...extra.type ? { type: extra.type } : {},
56
+ ...extra.title !== void 0 ? { title: extra.title } : {},
55
57
  ...extra.detail !== void 0 ? { detail: extra.detail } : {},
58
+ ...extra.instance !== void 0 ? { instance: extra.instance } : {},
56
59
  ...extra.param !== void 0 ? { param: extra.param } : {},
57
60
  ...extra.currentRevision !== void 0 ? { currentRevision: extra.currentRevision } : {},
58
61
  ...extra.requestId !== void 0 ? { requestId: extra.requestId } : {},
59
62
  ...extra.suggestion !== void 0 ? { suggestion: extra.suggestion } : {},
60
- ...extra.retryable !== void 0 ? { retryable: extra.retryable } : {}
63
+ ...extra.retryable !== void 0 ? { retryable: extra.retryable } : {},
64
+ ...extra.body !== void 0 ? { body: extra.body } : {}
61
65
  },
62
66
  headers: extra.headers ?? {}
63
67
  };
64
68
  }
65
-
66
- // src/publish/prepare.ts
67
- var import_promises2 = require("fs/promises");
68
- var import_node_path2 = require("path");
69
-
70
- // src/publish/checksum.ts
71
- var import_node_crypto = require("crypto");
72
- var import_node_fs = require("fs");
73
- function sha256Bytes(bytes) {
74
- return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
75
- }
76
- async function sha256File(path) {
77
- const hash = (0, import_node_crypto.createHash)("sha256");
78
- for await (const chunk of (0, import_node_fs.createReadStream)(path)) {
79
- hash.update(chunk);
80
- }
81
- return hash.digest("hex");
69
+ var PublishInputError = class extends Error {
70
+ constructor(code, message, param, suggestion) {
71
+ super(message);
72
+ this.code = code;
73
+ this.param = param;
74
+ this.suggestion = suggestion;
75
+ this.name = "PublishInputError";
76
+ }
77
+ code;
78
+ param;
79
+ suggestion;
80
+ };
81
+ function toErrorResult(err) {
82
+ if (err instanceof PublishInputError)
83
+ return createErrorResult(err.code, err.message, null, {
84
+ param: err.param ?? null,
85
+ suggestion: err.suggestion ?? null
86
+ });
87
+ return createErrorResult(
88
+ "sdk_error",
89
+ err instanceof Error ? err.message : "Unknown SDK error",
90
+ null
91
+ );
82
92
  }
83
93
 
84
94
  // src/publish/detect.ts
@@ -90,253 +100,100 @@ function isHttpUrl(value) {
90
100
  return false;
91
101
  }
92
102
  }
93
- function looksLikeHtml(value) {
94
- const trimmed = value.trimStart().toLowerCase();
95
- return trimmed.startsWith("<!doctype html") || trimmed.startsWith("<html") || /^<[a-z][a-z0-9:-]*(\s|>)/.test(trimmed);
103
+ function containsHtmlTag(value) {
104
+ return /<[a-z!/][^>]*>/i.test(value);
96
105
  }
97
- function detectStringInput(value) {
98
- if (isHttpUrl(value)) return { mode: "sourceUrl" };
99
- return {
100
- mode: "content",
101
- contentType: looksLikeHtml(value) ? "text/html" : "text/plain; charset=utf-8"
102
- };
106
+ var FILE_EXT = /\.[A-Za-z0-9]{1,8}([?#].*)?$/;
107
+ function isPathShaped(value) {
108
+ if (value.length === 0 || value.length > 4096 || value.includes("\n"))
109
+ return false;
110
+ if (containsHtmlTag(value)) return false;
111
+ if (value.includes("/") || value.includes("\\")) return true;
112
+ if (/^[A-Za-z]:[\\/]/.test(value)) return true;
113
+ return FILE_EXT.test(value);
114
+ }
115
+ function contentTypeForString(value, override) {
116
+ if (override) return override;
117
+ return containsHtmlTag(value) ? "text/html" : "text/plain; charset=utf-8";
103
118
  }
104
119
  function detectBytesContentType(bytes) {
105
- const text = Buffer.from(bytes).toString("utf8");
120
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
106
121
  if (text.includes("\uFFFD")) return "application/octet-stream";
107
- return looksLikeHtml(text) ? "text/html" : "text/plain; charset=utf-8";
108
- }
109
-
110
- // src/publish/files.ts
111
- var import_promises = require("fs/promises");
112
- var import_node_path = require("path");
113
- var import_fast_glob = __toESM(require("fast-glob"), 1);
114
- var import_ignore = __toESM(require("ignore"), 1);
115
- var import_mime_types = require("mime-types");
116
- var DEFAULT_IGNORES = [
117
- ".git",
118
- ".git/**",
119
- "node_modules",
120
- "node_modules/**",
121
- ".DS_Store",
122
- ".env",
123
- ".env.*",
124
- ".cache",
125
- ".cache/**",
126
- ".tmp",
127
- ".tmp/**",
128
- "dist/.cache",
129
- "dist/.cache/**"
130
- ];
131
- async function collectPublishFiles(inputPath, options = {}) {
132
- const inputStat = await (0, import_promises.stat)(inputPath);
133
- if (inputStat.isFile()) {
134
- return [
135
- {
136
- absolutePath: inputPath,
137
- path: (0, import_node_path.basename)(inputPath),
138
- contentType: await detectPathContentType(inputPath),
139
- sizeBytes: inputStat.size
140
- }
141
- ];
142
- }
143
- if (!inputStat.isDirectory()) {
144
- throw new Error(`Input is not a file or directory: ${inputPath}`);
145
- }
146
- const matcher = (0, import_ignore.default)().add([
147
- ...options.ignoreDefaults === false ? [] : DEFAULT_IGNORES,
148
- ...options.ignore ?? []
149
- ]);
150
- const entries = await (0, import_fast_glob.default)("**/*", {
151
- cwd: inputPath,
152
- onlyFiles: true,
153
- followSymbolicLinks: false,
154
- dot: true,
155
- unique: true
156
- });
157
- const paths = entries.map((entry) => entry.split(import_node_path.sep).join("/")).filter((entry) => !matcher.ignores(entry)).sort((a, b) => a.localeCompare(b));
158
- return Promise.all(
159
- paths.map(async (path) => {
160
- const absolutePath = `${inputPath}/${path}`;
161
- const fileStat = await (0, import_promises.stat)(absolutePath);
162
- return {
163
- absolutePath,
164
- path,
165
- contentType: await detectPathContentType(absolutePath),
166
- sizeBytes: fileStat.size
167
- };
168
- })
169
- );
170
- }
171
- async function detectPathContentType(path) {
172
- const detected = (0, import_mime_types.lookup)(path);
173
- if (detected) return detected;
174
- return detectBytesContentType(await (0, import_promises.readFile)(path));
175
- }
176
-
177
- // src/publish/prepare.ts
178
- var SINGLE_PUT_CHECKSUM_THRESHOLD_BYTES = 10 * 1024 * 1024;
179
- async function preparePublishRequest(input, options = {}) {
180
- if (input instanceof URL) {
181
- return sourceRequest(input.toString(), options);
182
- }
183
- if (typeof input === "string") {
184
- const local = await localKind(input);
185
- if (local === "file") {
186
- return fileStaged(input, options);
187
- }
188
- if (local === "directory") {
189
- return folderStaged(input, options);
190
- }
191
- const detection = detectStringInput(input);
192
- if (detection.mode === "sourceUrl") return sourceRequest(input, options);
193
- return stringContent(input, options, detection.contentType);
194
- }
195
- if (input instanceof Uint8Array || Buffer.isBuffer(input)) {
196
- return bytesStaged(input, options);
197
- }
198
- return explicitInput(input, options);
199
- }
200
- async function stringContent(content, options, detectedContentType) {
201
- const contentType = options.contentType ?? detectedContentType;
202
- return bytesStaged(Buffer.from(content), {
203
- ...options,
204
- path: options.path ?? entryForContentType(contentType),
205
- contentType
206
- });
122
+ return containsHtmlTag(text) ? "text/html" : "text/plain; charset=utf-8";
207
123
  }
208
124
  function entryForContentType(contentType) {
209
- const normalized = contentType.toLowerCase().split(";", 1)[0]?.trim() ?? "";
210
- if (normalized === "text/html" || normalized === "application/xhtml+xml")
211
- return "index.html";
212
- if (normalized === "application/json") return "index.json";
213
- if (normalized === "text/css") return "index.css";
214
- if (normalized === "text/javascript" || normalized === "application/javascript")
125
+ const m = contentType.toLowerCase().split(";", 1)[0]?.trim() ?? "";
126
+ if (m === "text/html" || m === "application/xhtml+xml") return "index.html";
127
+ if (m === "application/json") return "index.json";
128
+ if (m === "text/css") return "index.css";
129
+ if (m === "text/javascript" || m === "application/javascript")
215
130
  return "index.js";
216
- if (normalized.startsWith("text/")) return "index.txt";
131
+ if (m.startsWith("text/")) return "index.txt";
217
132
  return "index";
218
133
  }
219
- async function localKind(input) {
220
- try {
221
- const info = await (0, import_promises2.stat)(input);
222
- if (info.isFile()) return "file";
223
- if (info.isDirectory()) return "directory";
224
- return "none";
225
- } catch {
226
- return "none";
227
- }
228
- }
229
- async function fileStaged(path, options) {
230
- const fileStat = await (0, import_promises2.stat)(path);
231
- const manifestPath = options.path ?? (0, import_node_path2.basename)(path);
232
- return stagedRequest(
233
- [
234
- {
235
- path: manifestPath,
236
- contentType: options.contentType ?? await detectPathContentType(path),
237
- sizeBytes: fileStat.size,
238
- source: { kind: "path", path }
239
- }
240
- ],
241
- { ...options, entry: options.entry ?? manifestPath }
242
- );
243
- }
244
- async function folderStaged(path, options) {
245
- const files = (await collectPublishFiles(path, options)).map((file) => ({
246
- path: file.path,
247
- contentType: file.contentType,
248
- sizeBytes: file.sizeBytes,
249
- source: { kind: "path", path: file.absolutePath }
250
- }));
251
- return stagedRequest(files, options);
252
- }
253
- async function bytesStaged(bytes, options) {
254
- return stagedRequest(
255
- [
256
- {
257
- path: options.path ?? "index",
258
- contentType: options.contentType ?? detectBytesContentType(bytes),
259
- sizeBytes: bytes.byteLength,
260
- source: { kind: "bytes", bytes }
261
- }
262
- ],
263
- options
264
- );
265
- }
266
- async function stagedRequest(files, options) {
267
- const preparedFiles = await Promise.all(
268
- files.map(async (file) => {
269
- if (file.sizeBytes <= SINGLE_PUT_CHECKSUM_THRESHOLD_BYTES) return file;
270
- const checksumSha256 = file.source.kind === "bytes" ? sha256Bytes(file.source.bytes) : await sha256File(file.source.path);
271
- return { ...file, checksumSha256 };
272
- })
273
- );
274
- const manifestFiles = preparedFiles.map((file) => ({
275
- path: file.path,
276
- contentType: file.contentType,
277
- sizeBytes: file.sizeBytes,
278
- ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
279
- }));
280
- const prepared = {
281
- kind: "staged",
282
- manifest: {
283
- schemaVersion: 1,
284
- files: manifestFiles,
285
- ...options.entry ? { entry: options.entry } : {}
286
- },
287
- files: preparedFiles,
288
- options: optionsBody(options)
289
- };
290
- if (options.metadata) prepared.metadata = options.metadata;
291
- return prepared;
134
+
135
+ // src/publish/paths.ts
136
+ function normalizeManifestPath(path) {
137
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
292
138
  }
293
- function sourceRequest(sourceUrl, options) {
294
- if (!isHttpUrl(sourceUrl)) {
295
- throw new Error(
296
- "source_url must be an http(s) URL. Fetch the content first, then pass it as a string or file."
139
+ function assertValidManifestPath(path) {
140
+ if (path.includes("\\"))
141
+ throw new PublishInputError(
142
+ "invalid_path",
143
+ `Manifest path must use "/": "${path}".`,
144
+ path
297
145
  );
298
- }
299
- const prepared = {
300
- kind: "source",
301
- sourceUrl,
302
- options: optionsBody(options)
303
- };
304
- if (options.metadata) prepared.metadata = options.metadata;
305
- return prepared;
306
- }
307
- async function explicitInput(input, options) {
308
- if ("content" in input)
309
- return stringContent(
310
- input.content,
311
- { ...options, ...input },
312
- input.contentType ?? options.contentType ?? "text/html"
146
+ const p = normalizeManifestPath(path);
147
+ if (p === "")
148
+ throw new PublishInputError(
149
+ "invalid_path",
150
+ "Manifest path must not be empty.",
151
+ path
313
152
  );
314
- if ("sourceUrl" in input)
315
- return sourceRequest(input.sourceUrl, { ...options, ...input });
316
- if ("sourceUrls" in input)
317
- throw new Error(
318
- "sourceUrls inputs are not supported in v1. Publish a single source_url, or fetch the content first."
153
+ if (p.startsWith("/"))
154
+ throw new PublishInputError(
155
+ "invalid_path",
156
+ `Manifest path must be relative: "${path}".`,
157
+ path
158
+ );
159
+ if (p.startsWith("~"))
160
+ throw new PublishInputError(
161
+ "invalid_path",
162
+ `Manifest path must not start with "~": "${path}".`,
163
+ path
164
+ );
165
+ if (p.split("/").includes(".."))
166
+ throw new PublishInputError(
167
+ "invalid_path",
168
+ `Manifest path must not contain "..": "${path}".`,
169
+ path
319
170
  );
320
- return stagedRequest(
321
- input.files.map((file) => {
322
- const bytes = explicitFileBytes(file);
323
- return {
324
- path: file.path,
325
- contentType: file.contentType,
326
- sizeBytes: bytes.byteLength,
327
- source: { kind: "bytes", bytes }
328
- };
329
- }),
330
- { ...options, ...input }
331
- );
332
171
  }
333
- function explicitFileBytes(file) {
334
- if (file.content !== void 0) return Buffer.from(file.content);
335
- if (file.contentBase64 !== void 0)
336
- return Buffer.from(file.contentBase64, "base64");
337
- if (file.bytes !== void 0) return file.bytes;
338
- throw new TypeError(`Explicit file ${file.path} is missing content bytes`);
172
+ function assertNoDuplicatePaths(paths) {
173
+ const seen = /* @__PURE__ */ new Set();
174
+ for (const raw of paths) {
175
+ const p = normalizeManifestPath(raw);
176
+ if (seen.has(p))
177
+ throw new PublishInputError(
178
+ "duplicate_path",
179
+ `Duplicate file path after normalization: "${p}".`,
180
+ p,
181
+ "Give each file a unique path; basename mapping for string[] can collide."
182
+ );
183
+ seen.add(p);
184
+ }
339
185
  }
186
+ function assertNonEmptyBundle(paths) {
187
+ if (paths.length === 0)
188
+ throw new PublishInputError(
189
+ "empty_bundle",
190
+ "Nothing to publish: the bundle has no files.",
191
+ void 0,
192
+ "Pass at least one file, or a directory that contains files."
193
+ );
194
+ }
195
+
196
+ // src/publish/core.ts
340
197
  var VALID_VISIBILITIES = ["public", "unlisted"];
341
198
  var MAX_TITLE_LENGTH = 500;
342
199
  function validatePublishOptions(options) {
@@ -373,69 +230,125 @@ function optionsBody(options) {
373
230
  expiresAt: options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt
374
231
  };
375
232
  }
376
-
377
- // src/publish/upload.ts
378
- var import_node_crypto2 = require("crypto");
379
- var import_promises3 = require("fs/promises");
380
-
381
- // src/publish/multipart.ts
382
- var MAX_PART_TARGETS_PER_REQUEST = 100;
383
- async function uploadMultipartFile(transport, uploadId, fileId, bytes, partSize) {
384
- if (!partSize || partSize < 1) {
385
- return createErrorResult(
386
- "invalid_upload_target",
387
- "Multipart upload target did not include a valid part size.",
388
- null
233
+ function buildStagedRequest(files, options, entry) {
234
+ assertNonEmptyBundle(files.map((f) => f.path));
235
+ const normalizedPaths = files.map((f) => normalizeManifestPath(f.path));
236
+ assertNoDuplicatePaths(normalizedPaths);
237
+ const manifestFiles = files.map((file) => ({
238
+ path: normalizeManifestPath(file.path),
239
+ contentType: file.contentType,
240
+ sizeBytes: file.sizeBytes,
241
+ ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
242
+ }));
243
+ const resolvedEntry = entry ?? options.entry;
244
+ if (resolvedEntry !== void 0) assertValidManifestPath(resolvedEntry);
245
+ const manifest = {
246
+ schemaVersion: 1,
247
+ files: manifestFiles,
248
+ ...resolvedEntry ? { entry: resolvedEntry } : {}
249
+ };
250
+ const prepared = {
251
+ kind: "staged",
252
+ manifest,
253
+ files,
254
+ options: optionsBody(options)
255
+ };
256
+ if (options.metadata) prepared.metadata = options.metadata;
257
+ return prepared;
258
+ }
259
+ function buildSourceRequest(sourceUrl, options) {
260
+ if (!isHttpUrl(sourceUrl)) {
261
+ throw new PublishInputError(
262
+ "invalid_source_url",
263
+ "source_url must be an http(s) URL.",
264
+ sourceUrl,
265
+ "Fetch the content first, then pass it as content/files."
389
266
  );
390
267
  }
391
- const partNumbers = Array.from(
392
- { length: Math.ceil(bytes.byteLength / partSize) },
393
- (_, index) => index + 1
268
+ const prepared = {
269
+ kind: "source",
270
+ sourceUrl,
271
+ options: optionsBody(options)
272
+ };
273
+ if (options.metadata) prepared.metadata = options.metadata;
274
+ return prepared;
275
+ }
276
+ function decodeBase64(value) {
277
+ const binary = atob(value);
278
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
279
+ }
280
+ function fileBytes(file) {
281
+ if (file.content !== void 0) return new TextEncoder().encode(file.content);
282
+ if (file.contentBase64 !== void 0) return decodeBase64(file.contentBase64);
283
+ if (file.bytes !== void 0) return file.bytes;
284
+ throw new PublishInputError(
285
+ "missing_file_bytes",
286
+ `File "${file.path}" is missing content bytes.`,
287
+ file.path,
288
+ "Provide one of content, contentBase64, or bytes."
394
289
  );
395
- const parts = [];
396
- for (let startIndex = 0; startIndex < partNumbers.length; startIndex += MAX_PART_TARGETS_PER_REQUEST) {
397
- const partNumberBatch = partNumbers.slice(
398
- startIndex,
399
- startIndex + MAX_PART_TARGETS_PER_REQUEST
400
- );
401
- const targetsResult = await transport.request(
402
- "POST",
403
- `/uploads/${encodeURIComponent(uploadId)}/parts`,
404
- { body: { fileId, partNumbers: partNumberBatch } }
405
- );
406
- if (targetsResult.error) return errorResult(targetsResult);
407
- for (const target of targetsResult.data.parts) {
408
- const start = (target.partNumber - 1) * partSize;
409
- const end = Math.min(start + partSize, bytes.byteLength);
410
- const uploadResult = await transport.putSignedUrl(
411
- target.url,
412
- bytes.slice(start, end),
413
- target.headers
414
- );
415
- if (uploadResult.error) return errorResult(uploadResult);
416
- if (!uploadResult.data.etag) {
417
- return createErrorResult(
418
- "missing_upload_etag",
419
- "Signed upload response did not include an ETag.",
420
- null
421
- );
290
+ }
291
+ function singleStagedFile(path, contentType, bytes, options) {
292
+ return buildStagedRequest(
293
+ [
294
+ {
295
+ path,
296
+ contentType,
297
+ sizeBytes: bytes.byteLength,
298
+ getBody: async () => bytes
422
299
  }
423
- parts.push({
424
- partNumber: target.partNumber,
425
- etag: uploadResult.data.etag
426
- });
427
- }
300
+ ],
301
+ options
302
+ );
303
+ }
304
+ async function resolveInMemory(input, options = {}) {
305
+ if (input instanceof URL) {
306
+ return buildSourceRequest(input.toString(), options);
428
307
  }
429
- return { data: parts, error: null, headers: {} };
308
+ if (typeof input === "string") {
309
+ if (isHttpUrl(input)) return buildSourceRequest(input, options);
310
+ const contentType = contentTypeForString(input, options.contentType);
311
+ if (options.path !== void 0) assertValidManifestPath(options.path);
312
+ const path = options.path ?? entryForContentType(contentType);
313
+ const bytes = new TextEncoder().encode(input);
314
+ return singleStagedFile(path, contentType, bytes, options);
315
+ }
316
+ if (input instanceof Uint8Array) {
317
+ const contentType = options.contentType ?? detectBytesContentType(input);
318
+ if (options.path !== void 0) assertValidManifestPath(options.path);
319
+ const path = options.path ?? entryForContentType(contentType);
320
+ return singleStagedFile(path, contentType, input, options);
321
+ }
322
+ if (input.kind === "content") {
323
+ const contentType = input.contentType ?? options.contentType ?? "text/html";
324
+ if (input.path !== void 0) assertValidManifestPath(input.path);
325
+ if (options.path !== void 0) assertValidManifestPath(options.path);
326
+ const path = input.path ?? options.path ?? entryForContentType(contentType);
327
+ const bytes = new TextEncoder().encode(input.content);
328
+ return singleStagedFile(path, contentType, bytes, options);
329
+ }
330
+ if (input.kind === "source_url") {
331
+ return buildSourceRequest(input.sourceUrl, options);
332
+ }
333
+ const files = input.files.map((file) => {
334
+ assertValidManifestPath(file.path);
335
+ const bytes = fileBytes(file);
336
+ const contentType = file.contentType ?? detectBytesContentType(bytes);
337
+ return {
338
+ path: file.path,
339
+ contentType,
340
+ sizeBytes: bytes.byteLength,
341
+ getBody: async () => bytes
342
+ };
343
+ });
344
+ return buildStagedRequest(files, options, input.entry);
430
345
  }
431
346
  function errorResult(result) {
432
347
  if (!result.error) throw new Error("Expected an error result");
433
348
  return { data: null, error: result.error, headers: result.headers };
434
349
  }
435
-
436
- // src/publish/upload.ts
437
350
  async function publishStaged(transport, prepared, finalPath, options = {}) {
438
- const baseKey = options.idempotencyKey ?? `sdk-${(0, import_node_crypto2.randomUUID)()}`;
351
+ const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
439
352
  const createResult = await transport.request(
440
353
  "POST",
441
354
  "/uploads",
@@ -444,12 +357,9 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
444
357
  idempotencyKey: `${baseKey}:create-upload`
445
358
  }
446
359
  );
447
- if (createResult.error) return errorResult2(createResult);
448
- const completedFiles = {};
360
+ if (createResult.error) return errorResult(createResult);
449
361
  for (const file of prepared.files) {
450
- const target = createResult.data.files.find(
451
- (candidate) => candidate.path === file.path
452
- );
362
+ const target = createResult.data.files.find((f) => f.path === file.path);
453
363
  if (!target) {
454
364
  return createErrorResult(
455
365
  "upload_target_missing",
@@ -457,36 +367,29 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
457
367
  null
458
368
  );
459
369
  }
460
- if (target.upload.strategy === "multipart") {
461
- const bytes2 = await fileBytes(file);
462
- const partsResult = await uploadMultipartFile(
463
- transport,
464
- createResult.data.uploadId,
465
- target.fileId,
466
- bytes2,
467
- target.upload.partSize
370
+ if (target.upload.strategy !== "single_put") {
371
+ return createErrorResult(
372
+ "unsupported_upload_strategy",
373
+ `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
374
+ null
468
375
  );
469
- if (partsResult.error) return errorResult2(partsResult);
470
- completedFiles[target.fileId] = { parts: partsResult.data };
471
- continue;
472
376
  }
473
- const bytes = await fileBytes(file);
474
- const uploadResult = await transport.putSignedUrl(
377
+ const put = await transport.putSignedUrl(
475
378
  target.upload.url,
476
- bytes,
379
+ await file.getBody(),
477
380
  target.upload.headers
478
381
  );
479
- if (uploadResult.error) return errorResult2(uploadResult);
382
+ if (put.error) return errorResult(put);
480
383
  }
481
384
  const completeResult = await transport.request(
482
385
  "POST",
483
386
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
484
387
  {
485
- body: { files: completedFiles },
388
+ body: { files: {} },
486
389
  idempotencyKey: `${baseKey}:complete-upload`
487
390
  }
488
391
  );
489
- if (completeResult.error) return errorResult2(completeResult);
392
+ if (completeResult.error) return errorResult(completeResult);
490
393
  const finalOptions = {
491
394
  body: {
492
395
  uploadId: createResult.data.uploadId,
@@ -499,13 +402,184 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
499
402
  finalOptions.ifRevision = options.ifRevision;
500
403
  return transport.request("POST", finalPath, finalOptions);
501
404
  }
502
- async function fileBytes(file) {
503
- if (file.source.kind === "bytes") return file.source.bytes;
504
- return (0, import_promises3.readFile)(file.source.path);
405
+ async function publishSource(transport, prepared, finalPath, options = {}) {
406
+ const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
407
+ return transport.request("POST", finalPath, {
408
+ body: {
409
+ sourceUrl: prepared.sourceUrl,
410
+ options: prepared.options,
411
+ ...prepared.metadata ? { metadata: prepared.metadata } : {}
412
+ },
413
+ idempotencyKey: `${baseKey}:publish`,
414
+ ...options.ifRevision !== void 0 ? { ifRevision: options.ifRevision } : {}
415
+ });
416
+ }
417
+
418
+ // src/publish/node.ts
419
+ var import_promises2 = require("fs/promises");
420
+ var import_node_path2 = require("path");
421
+
422
+ // src/publish/checksum.ts
423
+ var import_node_crypto = require("crypto");
424
+ var import_node_fs = require("fs");
425
+ async function sha256File(path) {
426
+ const hash = (0, import_node_crypto.createHash)("sha256");
427
+ for await (const chunk of (0, import_node_fs.createReadStream)(path)) {
428
+ hash.update(chunk);
429
+ }
430
+ return hash.digest("hex");
505
431
  }
506
- function errorResult2(result) {
507
- if (!result.error) throw new Error("Expected an error result");
508
- return { data: null, error: result.error, headers: result.headers };
432
+
433
+ // src/publish/files.ts
434
+ var import_promises = require("fs/promises");
435
+ var import_node_path = require("path");
436
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
437
+ var import_ignore = __toESM(require("ignore"), 1);
438
+ var import_mime_types = require("mime-types");
439
+ var DEFAULT_IGNORES = [
440
+ ".git",
441
+ ".git/**",
442
+ "node_modules",
443
+ "node_modules/**",
444
+ ".DS_Store",
445
+ ".env",
446
+ ".env.*",
447
+ ".cache",
448
+ ".cache/**",
449
+ ".tmp",
450
+ ".tmp/**",
451
+ "dist/.cache",
452
+ "dist/.cache/**"
453
+ ];
454
+ async function collectPublishFiles(inputPath, options = {}) {
455
+ const inputStat = await (0, import_promises.stat)(inputPath);
456
+ if (inputStat.isFile()) {
457
+ return [
458
+ {
459
+ absolutePath: inputPath,
460
+ path: (0, import_node_path.basename)(inputPath),
461
+ contentType: await detectPathContentType(inputPath),
462
+ sizeBytes: inputStat.size
463
+ }
464
+ ];
465
+ }
466
+ if (!inputStat.isDirectory()) {
467
+ throw new Error(`Input is not a file or directory: ${inputPath}`);
468
+ }
469
+ const matcher = (0, import_ignore.default)().add([
470
+ ...options.ignoreDefaults === false ? [] : DEFAULT_IGNORES,
471
+ ...options.ignore ?? []
472
+ ]);
473
+ const entries = await (0, import_fast_glob.default)("**/*", {
474
+ cwd: inputPath,
475
+ onlyFiles: true,
476
+ followSymbolicLinks: false,
477
+ dot: true,
478
+ unique: true
479
+ });
480
+ const paths = entries.map((entry) => entry.split(import_node_path.sep).join("/")).filter((entry) => !matcher.ignores(entry)).sort((a, b) => a.localeCompare(b));
481
+ return Promise.all(
482
+ paths.map(async (path) => {
483
+ const absolutePath = `${inputPath}/${path}`;
484
+ const fileStat = await (0, import_promises.stat)(absolutePath);
485
+ return {
486
+ absolutePath,
487
+ path,
488
+ contentType: await detectPathContentType(absolutePath),
489
+ sizeBytes: fileStat.size
490
+ };
491
+ })
492
+ );
493
+ }
494
+ async function detectPathContentType(path) {
495
+ const detected = (0, import_mime_types.lookup)(path);
496
+ if (detected) return detected;
497
+ return detectBytesContentType(await (0, import_promises.readFile)(path));
498
+ }
499
+
500
+ // src/publish/node.ts
501
+ var SINGLE_PUT_CHECKSUM_THRESHOLD = 10 * 1024 * 1024;
502
+ async function resolveInput(input, options = {}) {
503
+ if (input instanceof URL || input instanceof Uint8Array || typeof input === "object" && input !== null && "kind" in input) {
504
+ return resolveInMemory(input, options);
505
+ }
506
+ if (Array.isArray(input)) {
507
+ return resolveBundle(input, options);
508
+ }
509
+ if (typeof input === "string") {
510
+ return resolveString(input, options);
511
+ }
512
+ return resolveInMemory(input, options);
513
+ }
514
+ async function resolveString(value, options) {
515
+ if (isHttpUrl(value)) return resolveInMemory(value, options);
516
+ if (value.includes("\n") || value.length > 4096) {
517
+ return resolveInMemory(value, options);
518
+ }
519
+ let info;
520
+ try {
521
+ info = await (0, import_promises2.stat)(value);
522
+ } catch {
523
+ info = void 0;
524
+ }
525
+ if (info?.isFile()) return fileStaged(value, options);
526
+ if (info?.isDirectory()) return folderStaged(value, options);
527
+ if (isPathShaped(value)) {
528
+ throw new PublishInputError(
529
+ "file_not_found",
530
+ `No file or directory at "${value}".`,
531
+ value,
532
+ 'To publish this as inline text, pass { kind: "content", content }.'
533
+ );
534
+ }
535
+ return resolveInMemory(value, options);
536
+ }
537
+ async function fileStaged(path, options) {
538
+ const fileStat = await (0, import_promises2.stat)(path);
539
+ if (options.path !== void 0) assertValidManifestPath(options.path);
540
+ const manifestPath = options.path ?? (0, import_node_path2.basename)(path);
541
+ const contentType = options.contentType ?? await detectPathContentType(path);
542
+ const checksumSha256 = fileStat.size > SINGLE_PUT_CHECKSUM_THRESHOLD ? await sha256File(path) : void 0;
543
+ const file = {
544
+ path: manifestPath,
545
+ contentType,
546
+ sizeBytes: fileStat.size,
547
+ ...checksumSha256 ? { checksumSha256 } : {},
548
+ getBody: () => (0, import_promises2.readFile)(path)
549
+ };
550
+ return buildStagedRequest([file], options);
551
+ }
552
+ async function folderStaged(path, options) {
553
+ const files = await collectPublishFiles(path, options);
554
+ const preparedFiles = await Promise.all(files.map(toPreparedFile));
555
+ return buildStagedRequest(preparedFiles, options);
556
+ }
557
+ async function resolveBundle(paths, options) {
558
+ const collected = [];
559
+ for (const element of paths) {
560
+ try {
561
+ await (0, import_promises2.stat)(element);
562
+ } catch {
563
+ throw new PublishInputError(
564
+ "file_not_found",
565
+ `No file or directory at "${element}".`,
566
+ element
567
+ );
568
+ }
569
+ collected.push(...await collectPublishFiles(element, options));
570
+ }
571
+ const preparedFiles = await Promise.all(collected.map(toPreparedFile));
572
+ return buildStagedRequest(preparedFiles, options);
573
+ }
574
+ async function toPreparedFile(pf) {
575
+ const checksumSha256 = pf.sizeBytes > SINGLE_PUT_CHECKSUM_THRESHOLD ? await sha256File(pf.absolutePath) : void 0;
576
+ return {
577
+ path: pf.path,
578
+ contentType: pf.contentType,
579
+ sizeBytes: pf.sizeBytes,
580
+ ...checksumSha256 ? { checksumSha256 } : {},
581
+ getBody: () => (0, import_promises2.readFile)(pf.absolutePath)
582
+ };
509
583
  }
510
584
 
511
585
  // src/resources/account.ts
@@ -574,33 +648,6 @@ var DeploymentsResource = class {
574
648
  this.transport = transport;
575
649
  }
576
650
  transport;
577
- create(dropId, body, options = {}) {
578
- const requestOptions = { body };
579
- if (options.idempotencyKey)
580
- requestOptions.idempotencyKey = options.idempotencyKey;
581
- if (options.ifRevision !== void 0)
582
- requestOptions.ifRevision = options.ifRevision;
583
- return this.transport.request(
584
- "POST",
585
- `/drops/${encodeURIComponent(dropId)}/deployments`,
586
- requestOptions
587
- );
588
- }
589
- createRaw(dropId, body, options = {}) {
590
- const requestOptions = {
591
- body,
592
- bodyCase: "raw"
593
- };
594
- if (options.idempotencyKey)
595
- requestOptions.idempotencyKey = options.idempotencyKey;
596
- if (options.ifRevision !== void 0)
597
- requestOptions.ifRevision = options.ifRevision;
598
- return this.transport.request(
599
- "POST",
600
- `/drops/${encodeURIComponent(dropId)}/deployments`,
601
- requestOptions
602
- );
603
- }
604
651
  list(dropId, params = {}) {
605
652
  return this.transport.request(
606
653
  "GET",
@@ -655,21 +702,6 @@ var DropsResource = class {
655
702
  this.transport = transport;
656
703
  }
657
704
  transport;
658
- create(body, options = {}) {
659
- const requestOptions = { body };
660
- if (options.idempotencyKey)
661
- requestOptions.idempotencyKey = options.idempotencyKey;
662
- return this.transport.request("POST", "/drops", requestOptions);
663
- }
664
- createRaw(body, options = {}) {
665
- const requestOptions = {
666
- body,
667
- bodyCase: "raw"
668
- };
669
- if (options.idempotencyKey)
670
- requestOptions.idempotencyKey = options.idempotencyKey;
671
- return this.transport.request("POST", "/drops", requestOptions);
672
- }
673
705
  async list(params = {}) {
674
706
  const result = await this.transport.request("GET", "/drops", {
675
707
  params
@@ -710,15 +742,22 @@ var DropsResource = class {
710
742
  );
711
743
  }
712
744
  };
745
+ var SETTINGS = [
746
+ "title",
747
+ "visibility",
748
+ "password",
749
+ "noindex",
750
+ "expiresAt",
751
+ "slug"
752
+ ];
713
753
  function updateBody(options) {
714
- const {
715
- metadata,
716
- idempotencyKey: _idempotencyKey,
717
- ifRevision: _ifRevision,
718
- entry: _entry,
719
- ...dropOptions
720
- } = options;
721
- return { options: dropOptions, metadata };
754
+ const out = {};
755
+ for (const key of SETTINGS)
756
+ if (options[key] !== void 0) out[key] = options[key];
757
+ return {
758
+ options: out,
759
+ ...options.metadata !== void 0 ? { metadata: options.metadata } : {}
760
+ };
722
761
  }
723
762
 
724
763
  // src/resources/uploads.ts
@@ -739,13 +778,6 @@ var UploadsResource = class {
739
778
  `/uploads/${encodeURIComponent(uploadId)}`
740
779
  );
741
780
  }
742
- createPartTargets(uploadId, body) {
743
- return this.transport.request(
744
- "POST",
745
- `/uploads/${encodeURIComponent(uploadId)}/parts`,
746
- { body }
747
- );
748
- }
749
781
  complete(uploadId, body, options = {}) {
750
782
  const requestOptions = { body };
751
783
  if (options.idempotencyKey)
@@ -799,25 +831,24 @@ function toSnakeCase(value) {
799
831
 
800
832
  // src/transport.ts
801
833
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
802
- var SDK_VERSION = "0.5.0";
834
+ var SDK_VERSION = "0.6.0";
803
835
  var Transport = class {
804
836
  apiKey;
805
837
  baseUrl;
806
838
  timeoutMs;
839
+ uploadTimeoutMs;
807
840
  fetchImpl;
808
841
  constructor(options = {}) {
809
842
  const resolved = typeof options === "string" ? { apiKey: options } : options;
810
843
  this.apiKey = resolved.apiKey ?? (typeof process !== "undefined" ? process.env?.DROPTHIS_API_KEY : void 0);
811
844
  this.baseUrl = (resolved.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
812
845
  this.timeoutMs = resolved.timeoutMs ?? 3e4;
846
+ this.uploadTimeoutMs = resolved.uploadTimeoutMs ?? 12e4;
813
847
  this.fetchImpl = resolved.fetch ?? globalThis.fetch;
814
848
  }
815
- multipart(method, path, form, options = {}) {
816
- return this.request(method, path, { ...options, body: form });
817
- }
818
849
  async putSignedUrl(url, body, headers) {
819
850
  const controller = new AbortController();
820
- const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
851
+ const timeout = setTimeout(() => controller.abort(), this.uploadTimeoutMs);
821
852
  try {
822
853
  const request = {
823
854
  method: "PUT",
@@ -872,7 +903,7 @@ var Transport = class {
872
903
  if (options.idempotencyKey)
873
904
  headers["idempotency-key"] = options.idempotencyKey;
874
905
  if (options.ifRevision !== void 0)
875
- headers["If-Revision"] = String(options.ifRevision);
906
+ headers["if-revision"] = String(options.ifRevision);
876
907
  if (options.body !== void 0 && !(options.body instanceof FormData))
877
908
  headers["content-type"] = "application/json";
878
909
  const url = new URL(`${this.baseUrl}${path}`);
@@ -904,11 +935,18 @@ var Transport = class {
904
935
  const parsed = parseJson(text);
905
936
  if (!response.ok) {
906
937
  const body = parsed && typeof parsed === "object" ? parsed : {};
907
- const message = stringValue(body.detail) ?? stringValue(body.error) ?? stringValue(body.message) ?? response.statusText;
938
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
908
939
  const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
909
940
  const currentRevision = numberValue(body.current_revision);
941
+ const type = stringValue(body.type);
942
+ const title = stringValue(body.title);
943
+ const instance = stringValue(body.instance);
910
944
  return createErrorResult(code, message, response.status, {
911
- detail: body,
945
+ body,
946
+ ...type !== null ? { type } : {},
947
+ ...title !== null ? { title } : {},
948
+ ...instance !== null ? { instance } : {},
949
+ detail: stringValue(body.detail),
912
950
  param: stringValue(body.param),
913
951
  ...currentRevision !== void 0 ? { currentRevision } : {},
914
952
  requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
@@ -1002,55 +1040,37 @@ var Dropthis = class {
1002
1040
  return this.uploadsResource;
1003
1041
  }
1004
1042
  async prepare(input, options = {}) {
1005
- return preparePublishRequest(input, options);
1043
+ return resolveInput(input, options);
1006
1044
  }
1007
1045
  async publish(input, options = {}) {
1008
- const prepared = await preparePublishRequest(input, options);
1009
- if (prepared.kind === "source")
1010
- return this.publishSource(prepared, "/drops", options);
1011
- return publishStaged(this.transport, prepared, "/drops", options);
1046
+ let prepared;
1047
+ try {
1048
+ prepared = await resolveInput(input, options);
1049
+ } catch (e) {
1050
+ return toErrorResult(e);
1051
+ }
1052
+ return prepared.kind === "source" ? publishSource(this.transport, prepared, "/drops", options) : publishStaged(this.transport, prepared, "/drops", options);
1012
1053
  }
1013
1054
  async deploy(dropId, input, options = {}) {
1014
- const prepared = await preparePublishRequest(input, options);
1055
+ let prepared;
1056
+ try {
1057
+ prepared = await resolveInput(input, options);
1058
+ } catch (e) {
1059
+ return toErrorResult(e);
1060
+ }
1015
1061
  const path = `/drops/${encodeURIComponent(dropId)}/deployments`;
1016
- if (prepared.kind === "source")
1017
- return createErrorResult(
1018
- "source_url_unsupported_for_deployment",
1019
- "Publishing a deployment from a source_url is not supported yet. Fetch the content first, then pass it as a string or file.",
1020
- null
1021
- );
1022
- return publishStaged(this.transport, prepared, path, options);
1023
- }
1024
- publishSource(prepared, path, options) {
1025
- const body = {
1026
- sourceUrl: prepared.sourceUrl,
1027
- options: prepared.options
1028
- };
1029
- if (prepared.metadata) body.metadata = prepared.metadata;
1030
- const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
1031
- return this.transport.request("POST", path, {
1032
- body,
1033
- idempotencyKey: `${baseKey}:publish`
1034
- });
1062
+ return prepared.kind === "source" ? publishSource(this.transport, prepared, path, options) : publishStaged(this.transport, prepared, path, options);
1035
1063
  }
1036
1064
  async update(dropId, options = {}) {
1037
1065
  return this.drops.update(dropId, options);
1038
1066
  }
1039
- request(method, path, options = {}) {
1040
- return this.transport.request(method, path, {
1041
- ...options,
1042
- bodyCase: "raw"
1043
- });
1044
- }
1045
- requestSignedUpload(url, bytes, headers) {
1046
- return this.transport.putSignedUrl(url, bytes, headers);
1047
- }
1048
1067
  };
1049
1068
  // Annotate the CommonJS export names for ESM import in node:
1050
1069
  0 && (module.exports = {
1051
1070
  CursorPage,
1052
1071
  DeploymentsResource,
1053
1072
  Dropthis,
1073
+ PublishInputError,
1054
1074
  UploadsResource,
1055
1075
  createErrorResult,
1056
1076
  redactSecrets