@dropthis/cli 0.4.1 → 0.6.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.
@@ -33,28 +33,62 @@ __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
39
40
  });
40
41
  module.exports = __toCommonJS(index_exports);
41
42
 
42
- // src/publish/prepare.ts
43
- var import_promises2 = require("fs/promises");
44
- var import_node_path2 = require("path");
45
-
46
- // src/publish/checksum.ts
47
- var import_node_crypto = require("crypto");
48
- var import_node_fs = require("fs");
49
- function sha256Bytes(bytes) {
50
- return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
43
+ // src/errors.ts
44
+ var API_KEY_RE = /sk_[A-Za-z0-9_-]+/g;
45
+ function redactSecrets(message) {
46
+ return message.replace(API_KEY_RE, "sk_[redacted]");
51
47
  }
52
- async function sha256File(path) {
53
- const hash = (0, import_node_crypto.createHash)("sha256");
54
- for await (const chunk of (0, import_node_fs.createReadStream)(path)) {
55
- hash.update(chunk);
56
- }
57
- return hash.digest("hex");
48
+ function createErrorResult(code, message, statusCode, extra = {}) {
49
+ return {
50
+ data: null,
51
+ error: {
52
+ code,
53
+ message: redactSecrets(message),
54
+ statusCode,
55
+ ...extra.type ? { type: extra.type } : {},
56
+ ...extra.title !== void 0 ? { title: extra.title } : {},
57
+ ...extra.detail !== void 0 ? { detail: extra.detail } : {},
58
+ ...extra.instance !== void 0 ? { instance: extra.instance } : {},
59
+ ...extra.param !== void 0 ? { param: extra.param } : {},
60
+ ...extra.currentRevision !== void 0 ? { currentRevision: extra.currentRevision } : {},
61
+ ...extra.requestId !== void 0 ? { requestId: extra.requestId } : {},
62
+ ...extra.suggestion !== void 0 ? { suggestion: extra.suggestion } : {},
63
+ ...extra.retryable !== void 0 ? { retryable: extra.retryable } : {},
64
+ ...extra.body !== void 0 ? { body: extra.body } : {}
65
+ },
66
+ headers: extra.headers ?? {}
67
+ };
68
+ }
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
+ );
58
92
  }
59
93
 
60
94
  // src/publish/detect.ts
@@ -66,246 +100,100 @@ function isHttpUrl(value) {
66
100
  return false;
67
101
  }
68
102
  }
69
- function looksLikeHtml(value) {
70
- const trimmed = value.trimStart().toLowerCase();
71
- 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);
72
105
  }
73
- function detectStringInput(value) {
74
- if (isHttpUrl(value)) return { mode: "sourceUrl" };
75
- return {
76
- mode: "content",
77
- contentType: looksLikeHtml(value) ? "text/html" : "text/plain; charset=utf-8"
78
- };
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";
79
118
  }
80
119
  function detectBytesContentType(bytes) {
81
- const text = Buffer.from(bytes).toString("utf8");
120
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
82
121
  if (text.includes("\uFFFD")) return "application/octet-stream";
83
- return looksLikeHtml(text) ? "text/html" : "text/plain; charset=utf-8";
84
- }
85
-
86
- // src/publish/files.ts
87
- var import_promises = require("fs/promises");
88
- var import_node_path = require("path");
89
- var import_fast_glob = __toESM(require("fast-glob"), 1);
90
- var import_ignore = __toESM(require("ignore"), 1);
91
- var import_mime_types = require("mime-types");
92
- var DEFAULT_IGNORES = [
93
- ".git",
94
- ".git/**",
95
- "node_modules",
96
- "node_modules/**",
97
- ".DS_Store",
98
- ".env",
99
- ".env.*",
100
- ".cache",
101
- ".cache/**",
102
- ".tmp",
103
- ".tmp/**",
104
- "dist/.cache",
105
- "dist/.cache/**"
106
- ];
107
- async function collectPublishFiles(inputPath, options = {}) {
108
- const inputStat = await (0, import_promises.stat)(inputPath);
109
- if (inputStat.isFile()) {
110
- return [
111
- {
112
- absolutePath: inputPath,
113
- path: (0, import_node_path.basename)(inputPath),
114
- contentType: await detectPathContentType(inputPath),
115
- sizeBytes: inputStat.size
116
- }
117
- ];
118
- }
119
- if (!inputStat.isDirectory()) {
120
- throw new Error(`Input is not a file or directory: ${inputPath}`);
121
- }
122
- const matcher = (0, import_ignore.default)().add([
123
- ...options.ignoreDefaults === false ? [] : DEFAULT_IGNORES,
124
- ...options.ignore ?? []
125
- ]);
126
- const entries = await (0, import_fast_glob.default)("**/*", {
127
- cwd: inputPath,
128
- onlyFiles: true,
129
- followSymbolicLinks: false,
130
- dot: true,
131
- unique: true
132
- });
133
- const paths = entries.map((entry) => entry.split(import_node_path.sep).join("/")).filter((entry) => !matcher.ignores(entry)).sort((a, b) => a.localeCompare(b));
134
- return Promise.all(
135
- paths.map(async (path) => {
136
- const absolutePath = `${inputPath}/${path}`;
137
- const fileStat = await (0, import_promises.stat)(absolutePath);
138
- return {
139
- absolutePath,
140
- path,
141
- contentType: await detectPathContentType(absolutePath),
142
- sizeBytes: fileStat.size
143
- };
144
- })
145
- );
146
- }
147
- async function detectPathContentType(path) {
148
- const detected = (0, import_mime_types.lookup)(path);
149
- if (detected) return detected;
150
- return detectBytesContentType(await (0, import_promises.readFile)(path));
151
- }
152
-
153
- // src/publish/prepare.ts
154
- var SINGLE_PUT_CHECKSUM_THRESHOLD_BYTES = 10 * 1024 * 1024;
155
- async function preparePublishRequest(input, options = {}) {
156
- if (input instanceof URL) {
157
- throw new Error(
158
- "URL inputs are not supported. Fetch the content first, then pass it as a string or file."
159
- );
160
- }
161
- if (typeof input === "string") {
162
- const local = await localKind(input);
163
- if (local === "file") {
164
- return fileStaged(input, options);
165
- }
166
- if (local === "directory") {
167
- return folderStaged(input, options);
168
- }
169
- const detection = detectStringInput(input);
170
- if (detection.mode === "sourceUrl")
171
- throw new Error(
172
- "URL inputs are not supported. Fetch the content first, then pass it as a string or file."
173
- );
174
- return stringContent(input, options, detection.contentType);
175
- }
176
- if (input instanceof Uint8Array || Buffer.isBuffer(input)) {
177
- return bytesStaged(input, options);
178
- }
179
- return explicitInput(input, options);
180
- }
181
- async function stringContent(content, options, detectedContentType) {
182
- const contentType = options.contentType ?? detectedContentType;
183
- return bytesStaged(Buffer.from(content), {
184
- ...options,
185
- path: options.path ?? entryForContentType(contentType),
186
- contentType
187
- });
122
+ return containsHtmlTag(text) ? "text/html" : "text/plain; charset=utf-8";
188
123
  }
189
124
  function entryForContentType(contentType) {
190
- const normalized = contentType.toLowerCase().split(";", 1)[0]?.trim() ?? "";
191
- if (normalized === "text/html" || normalized === "application/xhtml+xml")
192
- return "index.html";
193
- if (normalized === "application/json") return "index.json";
194
- if (normalized === "text/css") return "index.css";
195
- 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")
196
130
  return "index.js";
197
- if (normalized.startsWith("text/")) return "index.txt";
131
+ if (m.startsWith("text/")) return "index.txt";
198
132
  return "index";
199
133
  }
200
- async function localKind(input) {
201
- try {
202
- const info = await (0, import_promises2.stat)(input);
203
- if (info.isFile()) return "file";
204
- if (info.isDirectory()) return "directory";
205
- return "none";
206
- } catch {
207
- return "none";
208
- }
209
- }
210
- async function fileStaged(path, options) {
211
- const fileStat = await (0, import_promises2.stat)(path);
212
- const manifestPath = options.path ?? (0, import_node_path2.basename)(path);
213
- return stagedRequest(
214
- [
215
- {
216
- path: manifestPath,
217
- contentType: options.contentType ?? await detectPathContentType(path),
218
- sizeBytes: fileStat.size,
219
- source: { kind: "path", path }
220
- }
221
- ],
222
- { ...options, entry: options.entry ?? manifestPath }
223
- );
224
- }
225
- async function folderStaged(path, options) {
226
- const files = (await collectPublishFiles(path, options)).map((file) => ({
227
- path: file.path,
228
- contentType: file.contentType,
229
- sizeBytes: file.sizeBytes,
230
- source: { kind: "path", path: file.absolutePath }
231
- }));
232
- return stagedRequest(files, options);
233
- }
234
- async function bytesStaged(bytes, options) {
235
- return stagedRequest(
236
- [
237
- {
238
- path: options.path ?? "index",
239
- contentType: options.contentType ?? detectBytesContentType(bytes),
240
- sizeBytes: bytes.byteLength,
241
- source: { kind: "bytes", bytes }
242
- }
243
- ],
244
- options
245
- );
246
- }
247
- async function stagedRequest(files, options) {
248
- const preparedFiles = await Promise.all(
249
- files.map(async (file) => {
250
- if (file.sizeBytes <= SINGLE_PUT_CHECKSUM_THRESHOLD_BYTES) return file;
251
- const checksumSha256 = file.source.kind === "bytes" ? sha256Bytes(file.source.bytes) : await sha256File(file.source.path);
252
- return { ...file, checksumSha256 };
253
- })
254
- );
255
- const manifestFiles = preparedFiles.map((file) => ({
256
- path: file.path,
257
- contentType: file.contentType,
258
- sizeBytes: file.sizeBytes,
259
- ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
260
- }));
261
- const prepared = {
262
- kind: "staged",
263
- manifest: {
264
- schemaVersion: 1,
265
- files: manifestFiles,
266
- ...options.entry ? { entry: options.entry } : {}
267
- },
268
- files: preparedFiles,
269
- options: optionsBody(options)
270
- };
271
- if (options.metadata) prepared.metadata = options.metadata;
272
- return prepared;
134
+
135
+ // src/publish/paths.ts
136
+ function normalizeManifestPath(path) {
137
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
273
138
  }
274
- async function explicitInput(input, options) {
275
- if ("content" in input)
276
- return stringContent(
277
- input.content,
278
- { ...options, ...input },
279
- input.contentType ?? options.contentType ?? "text/html"
139
+ function assertValidManifestPath(path) {
140
+ if (path.includes("\\"))
141
+ throw new PublishInputError(
142
+ "invalid_path",
143
+ `Manifest path must use "/": "${path}".`,
144
+ path
280
145
  );
281
- if ("sourceUrl" in input)
282
- throw new Error(
283
- "sourceUrl inputs are not supported. Fetch the content first, then pass it as a string or file."
146
+ const p = normalizeManifestPath(path);
147
+ if (p === "")
148
+ throw new PublishInputError(
149
+ "invalid_path",
150
+ "Manifest path must not be empty.",
151
+ path
284
152
  );
285
- if ("sourceUrls" in input)
286
- throw new Error(
287
- "sourceUrls inputs are not supported. Fetch the content first, then pass it as a string or file."
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
288
170
  );
289
- return stagedRequest(
290
- input.files.map((file) => {
291
- const bytes = explicitFileBytes(file);
292
- return {
293
- path: file.path,
294
- contentType: file.contentType,
295
- sizeBytes: bytes.byteLength,
296
- source: { kind: "bytes", bytes }
297
- };
298
- }),
299
- { ...options, ...input }
300
- );
301
171
  }
302
- function explicitFileBytes(file) {
303
- if (file.content !== void 0) return Buffer.from(file.content);
304
- if (file.contentBase64 !== void 0)
305
- return Buffer.from(file.contentBase64, "base64");
306
- if (file.bytes !== void 0) return file.bytes;
307
- 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
+ }
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
+ );
308
194
  }
195
+
196
+ // src/publish/core.ts
309
197
  var VALID_VISIBILITIES = ["public", "unlisted"];
310
198
  var MAX_TITLE_LENGTH = 500;
311
199
  function validatePublishOptions(options) {
@@ -342,93 +230,125 @@ function optionsBody(options) {
342
230
  expiresAt: options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt
343
231
  };
344
232
  }
345
-
346
- // src/publish/upload.ts
347
- var import_node_crypto2 = require("crypto");
348
- var import_promises3 = require("fs/promises");
349
-
350
- // src/errors.ts
351
- var API_KEY_RE = /sk_[A-Za-z0-9_-]+/g;
352
- function redactSecrets(message) {
353
- return message.replace(API_KEY_RE, "sk_[redacted]");
354
- }
355
- function createErrorResult(code, message, statusCode, extra = {}) {
356
- return {
357
- data: null,
358
- error: {
359
- code,
360
- message: redactSecrets(message),
361
- statusCode,
362
- ...extra.type ? { type: extra.type } : {},
363
- ...extra.detail !== void 0 ? { detail: extra.detail } : {},
364
- ...extra.param !== void 0 ? { param: extra.param } : {},
365
- ...extra.currentRevision !== void 0 ? { currentRevision: extra.currentRevision } : {},
366
- ...extra.requestId !== void 0 ? { requestId: extra.requestId } : {},
367
- ...extra.suggestion !== void 0 ? { suggestion: extra.suggestion } : {},
368
- ...extra.retryable !== void 0 ? { retryable: extra.retryable } : {}
369
- },
370
- headers: extra.headers ?? {}
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)
371
255
  };
256
+ if (options.metadata) prepared.metadata = options.metadata;
257
+ return prepared;
372
258
  }
373
-
374
- // src/publish/multipart.ts
375
- var MAX_PART_TARGETS_PER_REQUEST = 100;
376
- async function uploadMultipartFile(transport, uploadId, fileId, bytes, partSize) {
377
- if (!partSize || partSize < 1) {
378
- return createErrorResult(
379
- "invalid_upload_target",
380
- "Multipart upload target did not include a valid part size.",
381
- null
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."
382
266
  );
383
267
  }
384
- const partNumbers = Array.from(
385
- { length: Math.ceil(bytes.byteLength / partSize) },
386
- (_, 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."
387
289
  );
388
- const parts = [];
389
- for (let startIndex = 0; startIndex < partNumbers.length; startIndex += MAX_PART_TARGETS_PER_REQUEST) {
390
- const partNumberBatch = partNumbers.slice(
391
- startIndex,
392
- startIndex + MAX_PART_TARGETS_PER_REQUEST
393
- );
394
- const targetsResult = await transport.request(
395
- "POST",
396
- `/uploads/${encodeURIComponent(uploadId)}/parts`,
397
- { body: { fileId, partNumbers: partNumberBatch } }
398
- );
399
- if (targetsResult.error) return errorResult(targetsResult);
400
- for (const target of targetsResult.data.parts) {
401
- const start = (target.partNumber - 1) * partSize;
402
- const end = Math.min(start + partSize, bytes.byteLength);
403
- const uploadResult = await transport.putSignedUrl(
404
- target.url,
405
- bytes.slice(start, end),
406
- target.headers
407
- );
408
- if (uploadResult.error) return errorResult(uploadResult);
409
- if (!uploadResult.data.etag) {
410
- return createErrorResult(
411
- "missing_upload_etag",
412
- "Signed upload response did not include an ETag.",
413
- null
414
- );
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
415
299
  }
416
- parts.push({
417
- partNumber: target.partNumber,
418
- etag: uploadResult.data.etag
419
- });
420
- }
300
+ ],
301
+ options
302
+ );
303
+ }
304
+ async function resolveInMemory(input, options = {}) {
305
+ if (input instanceof URL) {
306
+ return buildSourceRequest(input.toString(), options);
421
307
  }
422
- 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);
423
345
  }
424
346
  function errorResult(result) {
425
347
  if (!result.error) throw new Error("Expected an error result");
426
348
  return { data: null, error: result.error, headers: result.headers };
427
349
  }
428
-
429
- // src/publish/upload.ts
430
350
  async function publishStaged(transport, prepared, finalPath, options = {}) {
431
- const baseKey = options.idempotencyKey ?? `sdk-${(0, import_node_crypto2.randomUUID)()}`;
351
+ const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
432
352
  const createResult = await transport.request(
433
353
  "POST",
434
354
  "/uploads",
@@ -437,12 +357,9 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
437
357
  idempotencyKey: `${baseKey}:create-upload`
438
358
  }
439
359
  );
440
- if (createResult.error) return errorResult2(createResult);
441
- const completedFiles = {};
360
+ if (createResult.error) return errorResult(createResult);
442
361
  for (const file of prepared.files) {
443
- const target = createResult.data.files.find(
444
- (candidate) => candidate.path === file.path
445
- );
362
+ const target = createResult.data.files.find((f) => f.path === file.path);
446
363
  if (!target) {
447
364
  return createErrorResult(
448
365
  "upload_target_missing",
@@ -450,36 +367,29 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
450
367
  null
451
368
  );
452
369
  }
453
- if (target.upload.strategy === "multipart") {
454
- const bytes2 = await fileBytes(file);
455
- const partsResult = await uploadMultipartFile(
456
- transport,
457
- createResult.data.uploadId,
458
- target.fileId,
459
- bytes2,
460
- 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
461
375
  );
462
- if (partsResult.error) return errorResult2(partsResult);
463
- completedFiles[target.fileId] = { parts: partsResult.data };
464
- continue;
465
376
  }
466
- const bytes = await fileBytes(file);
467
- const uploadResult = await transport.putSignedUrl(
377
+ const put = await transport.putSignedUrl(
468
378
  target.upload.url,
469
- bytes,
379
+ await file.getBody(),
470
380
  target.upload.headers
471
381
  );
472
- if (uploadResult.error) return errorResult2(uploadResult);
382
+ if (put.error) return errorResult(put);
473
383
  }
474
384
  const completeResult = await transport.request(
475
385
  "POST",
476
386
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
477
387
  {
478
- body: { files: completedFiles },
388
+ body: { files: {} },
479
389
  idempotencyKey: `${baseKey}:complete-upload`
480
390
  }
481
391
  );
482
- if (completeResult.error) return errorResult2(completeResult);
392
+ if (completeResult.error) return errorResult(completeResult);
483
393
  const finalOptions = {
484
394
  body: {
485
395
  uploadId: createResult.data.uploadId,
@@ -492,13 +402,184 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
492
402
  finalOptions.ifRevision = options.ifRevision;
493
403
  return transport.request("POST", finalPath, finalOptions);
494
404
  }
495
- async function fileBytes(file) {
496
- if (file.source.kind === "bytes") return file.source.bytes;
497
- 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
+ });
498
416
  }
499
- function errorResult2(result) {
500
- if (!result.error) throw new Error("Expected an error result");
501
- return { data: null, error: result.error, headers: result.headers };
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");
431
+ }
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
+ };
502
583
  }
503
584
 
504
585
  // src/resources/account.ts
@@ -567,33 +648,6 @@ var DeploymentsResource = class {
567
648
  this.transport = transport;
568
649
  }
569
650
  transport;
570
- create(dropId, body, options = {}) {
571
- const requestOptions = { body };
572
- if (options.idempotencyKey)
573
- requestOptions.idempotencyKey = options.idempotencyKey;
574
- if (options.ifRevision !== void 0)
575
- requestOptions.ifRevision = options.ifRevision;
576
- return this.transport.request(
577
- "POST",
578
- `/drops/${encodeURIComponent(dropId)}/deployments`,
579
- requestOptions
580
- );
581
- }
582
- createRaw(dropId, body, options = {}) {
583
- const requestOptions = {
584
- body,
585
- bodyCase: "raw"
586
- };
587
- if (options.idempotencyKey)
588
- requestOptions.idempotencyKey = options.idempotencyKey;
589
- if (options.ifRevision !== void 0)
590
- requestOptions.ifRevision = options.ifRevision;
591
- return this.transport.request(
592
- "POST",
593
- `/drops/${encodeURIComponent(dropId)}/deployments`,
594
- requestOptions
595
- );
596
- }
597
651
  list(dropId, params = {}) {
598
652
  return this.transport.request(
599
653
  "GET",
@@ -648,21 +702,6 @@ var DropsResource = class {
648
702
  this.transport = transport;
649
703
  }
650
704
  transport;
651
- create(body, options = {}) {
652
- const requestOptions = { body };
653
- if (options.idempotencyKey)
654
- requestOptions.idempotencyKey = options.idempotencyKey;
655
- return this.transport.request("POST", "/drops", requestOptions);
656
- }
657
- createRaw(body, options = {}) {
658
- const requestOptions = {
659
- body,
660
- bodyCase: "raw"
661
- };
662
- if (options.idempotencyKey)
663
- requestOptions.idempotencyKey = options.idempotencyKey;
664
- return this.transport.request("POST", "/drops", requestOptions);
665
- }
666
705
  async list(params = {}) {
667
706
  const result = await this.transport.request("GET", "/drops", {
668
707
  params
@@ -703,15 +742,22 @@ var DropsResource = class {
703
742
  );
704
743
  }
705
744
  };
745
+ var SETTINGS = [
746
+ "title",
747
+ "visibility",
748
+ "password",
749
+ "noindex",
750
+ "expiresAt",
751
+ "slug"
752
+ ];
706
753
  function updateBody(options) {
707
- const {
708
- metadata,
709
- idempotencyKey: _idempotencyKey,
710
- ifRevision: _ifRevision,
711
- entry: _entry,
712
- ...dropOptions
713
- } = options;
714
- 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
+ };
715
761
  }
716
762
 
717
763
  // src/resources/uploads.ts
@@ -732,13 +778,6 @@ var UploadsResource = class {
732
778
  `/uploads/${encodeURIComponent(uploadId)}`
733
779
  );
734
780
  }
735
- createPartTargets(uploadId, body) {
736
- return this.transport.request(
737
- "POST",
738
- `/uploads/${encodeURIComponent(uploadId)}/parts`,
739
- { body }
740
- );
741
- }
742
781
  complete(uploadId, body, options = {}) {
743
782
  const requestOptions = { body };
744
783
  if (options.idempotencyKey)
@@ -792,25 +831,24 @@ function toSnakeCase(value) {
792
831
 
793
832
  // src/transport.ts
794
833
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
795
- var SDK_VERSION = "0.3.0";
834
+ var SDK_VERSION = "0.6.0";
796
835
  var Transport = class {
797
836
  apiKey;
798
837
  baseUrl;
799
838
  timeoutMs;
839
+ uploadTimeoutMs;
800
840
  fetchImpl;
801
841
  constructor(options = {}) {
802
842
  const resolved = typeof options === "string" ? { apiKey: options } : options;
803
- this.apiKey = resolved.apiKey ?? process.env.DROPTHIS_API_KEY;
843
+ this.apiKey = resolved.apiKey ?? (typeof process !== "undefined" ? process.env?.DROPTHIS_API_KEY : void 0);
804
844
  this.baseUrl = (resolved.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
805
845
  this.timeoutMs = resolved.timeoutMs ?? 3e4;
846
+ this.uploadTimeoutMs = resolved.uploadTimeoutMs ?? 12e4;
806
847
  this.fetchImpl = resolved.fetch ?? globalThis.fetch;
807
848
  }
808
- multipart(method, path, form, options = {}) {
809
- return this.request(method, path, { ...options, body: form });
810
- }
811
849
  async putSignedUrl(url, body, headers) {
812
850
  const controller = new AbortController();
813
- const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
851
+ const timeout = setTimeout(() => controller.abort(), this.uploadTimeoutMs);
814
852
  try {
815
853
  const request = {
816
854
  method: "PUT",
@@ -865,7 +903,7 @@ var Transport = class {
865
903
  if (options.idempotencyKey)
866
904
  headers["idempotency-key"] = options.idempotencyKey;
867
905
  if (options.ifRevision !== void 0)
868
- headers["If-Revision"] = String(options.ifRevision);
906
+ headers["if-revision"] = String(options.ifRevision);
869
907
  if (options.body !== void 0 && !(options.body instanceof FormData))
870
908
  headers["content-type"] = "application/json";
871
909
  const url = new URL(`${this.baseUrl}${path}`);
@@ -897,11 +935,18 @@ var Transport = class {
897
935
  const parsed = parseJson(text);
898
936
  if (!response.ok) {
899
937
  const body = parsed && typeof parsed === "object" ? parsed : {};
900
- const message = stringValue(body.detail) ?? stringValue(body.error) ?? stringValue(body.message) ?? response.statusText;
938
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
901
939
  const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
902
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);
903
944
  return createErrorResult(code, message, response.status, {
904
- detail: body,
945
+ body,
946
+ ...type !== null ? { type } : {},
947
+ ...title !== null ? { title } : {},
948
+ ...instance !== null ? { instance } : {},
949
+ detail: stringValue(body.detail),
905
950
  param: stringValue(body.param),
906
951
  ...currentRevision !== void 0 ? { currentRevision } : {},
907
952
  requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
@@ -995,35 +1040,37 @@ var Dropthis = class {
995
1040
  return this.uploadsResource;
996
1041
  }
997
1042
  async prepare(input, options = {}) {
998
- return preparePublishRequest(input, options);
1043
+ return resolveInput(input, options);
999
1044
  }
1000
1045
  async publish(input, options = {}) {
1001
- const prepared = await preparePublishRequest(input, options);
1002
- 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);
1003
1053
  }
1004
1054
  async deploy(dropId, input, options = {}) {
1005
- 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
+ }
1006
1061
  const path = `/drops/${encodeURIComponent(dropId)}/deployments`;
1007
- return publishStaged(this.transport, prepared, path, options);
1062
+ return prepared.kind === "source" ? publishSource(this.transport, prepared, path, options) : publishStaged(this.transport, prepared, path, options);
1008
1063
  }
1009
1064
  async update(dropId, options = {}) {
1010
1065
  return this.drops.update(dropId, options);
1011
1066
  }
1012
- request(method, path, options = {}) {
1013
- return this.transport.request(method, path, {
1014
- ...options,
1015
- bodyCase: "raw"
1016
- });
1017
- }
1018
- requestSignedUpload(url, bytes, headers) {
1019
- return this.transport.putSignedUrl(url, bytes, headers);
1020
- }
1021
1067
  };
1022
1068
  // Annotate the CommonJS export names for ESM import in node:
1023
1069
  0 && (module.exports = {
1024
1070
  CursorPage,
1025
1071
  DeploymentsResource,
1026
1072
  Dropthis,
1073
+ PublishInputError,
1027
1074
  UploadsResource,
1028
1075
  createErrorResult,
1029
1076
  redactSecrets