@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.
@@ -11,33 +11,42 @@ function createErrorResult(code, message, statusCode, extra = {}) {
11
11
  message: redactSecrets(message),
12
12
  statusCode,
13
13
  ...extra.type ? { type: extra.type } : {},
14
+ ...extra.title !== void 0 ? { title: extra.title } : {},
14
15
  ...extra.detail !== void 0 ? { detail: extra.detail } : {},
16
+ ...extra.instance !== void 0 ? { instance: extra.instance } : {},
15
17
  ...extra.param !== void 0 ? { param: extra.param } : {},
16
18
  ...extra.currentRevision !== void 0 ? { currentRevision: extra.currentRevision } : {},
17
19
  ...extra.requestId !== void 0 ? { requestId: extra.requestId } : {},
18
20
  ...extra.suggestion !== void 0 ? { suggestion: extra.suggestion } : {},
19
- ...extra.retryable !== void 0 ? { retryable: extra.retryable } : {}
21
+ ...extra.retryable !== void 0 ? { retryable: extra.retryable } : {},
22
+ ...extra.body !== void 0 ? { body: extra.body } : {}
20
23
  },
21
24
  headers: extra.headers ?? {}
22
25
  };
23
26
  }
24
-
25
- // src/publish/prepare.ts
26
- import { stat as stat2 } from "fs/promises";
27
- import { basename as basename2 } from "path";
28
-
29
- // src/publish/checksum.ts
30
- import { createHash } from "crypto";
31
- import { createReadStream } from "fs";
32
- function sha256Bytes(bytes) {
33
- return createHash("sha256").update(bytes).digest("hex");
34
- }
35
- async function sha256File(path) {
36
- const hash = createHash("sha256");
37
- for await (const chunk of createReadStream(path)) {
38
- hash.update(chunk);
39
- }
40
- return hash.digest("hex");
27
+ var PublishInputError = class extends Error {
28
+ constructor(code, message, param, suggestion) {
29
+ super(message);
30
+ this.code = code;
31
+ this.param = param;
32
+ this.suggestion = suggestion;
33
+ this.name = "PublishInputError";
34
+ }
35
+ code;
36
+ param;
37
+ suggestion;
38
+ };
39
+ function toErrorResult(err) {
40
+ if (err instanceof PublishInputError)
41
+ return createErrorResult(err.code, err.message, null, {
42
+ param: err.param ?? null,
43
+ suggestion: err.suggestion ?? null
44
+ });
45
+ return createErrorResult(
46
+ "sdk_error",
47
+ err instanceof Error ? err.message : "Unknown SDK error",
48
+ null
49
+ );
41
50
  }
42
51
 
43
52
  // src/publish/detect.ts
@@ -49,253 +58,100 @@ function isHttpUrl(value) {
49
58
  return false;
50
59
  }
51
60
  }
52
- function looksLikeHtml(value) {
53
- const trimmed = value.trimStart().toLowerCase();
54
- return trimmed.startsWith("<!doctype html") || trimmed.startsWith("<html") || /^<[a-z][a-z0-9:-]*(\s|>)/.test(trimmed);
61
+ function containsHtmlTag(value) {
62
+ return /<[a-z!/][^>]*>/i.test(value);
55
63
  }
56
- function detectStringInput(value) {
57
- if (isHttpUrl(value)) return { mode: "sourceUrl" };
58
- return {
59
- mode: "content",
60
- contentType: looksLikeHtml(value) ? "text/html" : "text/plain; charset=utf-8"
61
- };
64
+ var FILE_EXT = /\.[A-Za-z0-9]{1,8}([?#].*)?$/;
65
+ function isPathShaped(value) {
66
+ if (value.length === 0 || value.length > 4096 || value.includes("\n"))
67
+ return false;
68
+ if (containsHtmlTag(value)) return false;
69
+ if (value.includes("/") || value.includes("\\")) return true;
70
+ if (/^[A-Za-z]:[\\/]/.test(value)) return true;
71
+ return FILE_EXT.test(value);
72
+ }
73
+ function contentTypeForString(value, override) {
74
+ if (override) return override;
75
+ return containsHtmlTag(value) ? "text/html" : "text/plain; charset=utf-8";
62
76
  }
63
77
  function detectBytesContentType(bytes) {
64
- const text = Buffer.from(bytes).toString("utf8");
78
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
65
79
  if (text.includes("\uFFFD")) return "application/octet-stream";
66
- return looksLikeHtml(text) ? "text/html" : "text/plain; charset=utf-8";
67
- }
68
-
69
- // src/publish/files.ts
70
- import { readFile, stat } from "fs/promises";
71
- import { basename, sep } from "path";
72
- import fg from "fast-glob";
73
- import ignore from "ignore";
74
- import { lookup } from "mime-types";
75
- var DEFAULT_IGNORES = [
76
- ".git",
77
- ".git/**",
78
- "node_modules",
79
- "node_modules/**",
80
- ".DS_Store",
81
- ".env",
82
- ".env.*",
83
- ".cache",
84
- ".cache/**",
85
- ".tmp",
86
- ".tmp/**",
87
- "dist/.cache",
88
- "dist/.cache/**"
89
- ];
90
- async function collectPublishFiles(inputPath, options = {}) {
91
- const inputStat = await stat(inputPath);
92
- if (inputStat.isFile()) {
93
- return [
94
- {
95
- absolutePath: inputPath,
96
- path: basename(inputPath),
97
- contentType: await detectPathContentType(inputPath),
98
- sizeBytes: inputStat.size
99
- }
100
- ];
101
- }
102
- if (!inputStat.isDirectory()) {
103
- throw new Error(`Input is not a file or directory: ${inputPath}`);
104
- }
105
- const matcher = ignore().add([
106
- ...options.ignoreDefaults === false ? [] : DEFAULT_IGNORES,
107
- ...options.ignore ?? []
108
- ]);
109
- const entries = await fg("**/*", {
110
- cwd: inputPath,
111
- onlyFiles: true,
112
- followSymbolicLinks: false,
113
- dot: true,
114
- unique: true
115
- });
116
- const paths = entries.map((entry) => entry.split(sep).join("/")).filter((entry) => !matcher.ignores(entry)).sort((a, b) => a.localeCompare(b));
117
- return Promise.all(
118
- paths.map(async (path) => {
119
- const absolutePath = `${inputPath}/${path}`;
120
- const fileStat = await stat(absolutePath);
121
- return {
122
- absolutePath,
123
- path,
124
- contentType: await detectPathContentType(absolutePath),
125
- sizeBytes: fileStat.size
126
- };
127
- })
128
- );
129
- }
130
- async function detectPathContentType(path) {
131
- const detected = lookup(path);
132
- if (detected) return detected;
133
- return detectBytesContentType(await readFile(path));
134
- }
135
-
136
- // src/publish/prepare.ts
137
- var SINGLE_PUT_CHECKSUM_THRESHOLD_BYTES = 10 * 1024 * 1024;
138
- async function preparePublishRequest(input, options = {}) {
139
- if (input instanceof URL) {
140
- return sourceRequest(input.toString(), options);
141
- }
142
- if (typeof input === "string") {
143
- const local = await localKind(input);
144
- if (local === "file") {
145
- return fileStaged(input, options);
146
- }
147
- if (local === "directory") {
148
- return folderStaged(input, options);
149
- }
150
- const detection = detectStringInput(input);
151
- if (detection.mode === "sourceUrl") return sourceRequest(input, options);
152
- return stringContent(input, options, detection.contentType);
153
- }
154
- if (input instanceof Uint8Array || Buffer.isBuffer(input)) {
155
- return bytesStaged(input, options);
156
- }
157
- return explicitInput(input, options);
158
- }
159
- async function stringContent(content, options, detectedContentType) {
160
- const contentType = options.contentType ?? detectedContentType;
161
- return bytesStaged(Buffer.from(content), {
162
- ...options,
163
- path: options.path ?? entryForContentType(contentType),
164
- contentType
165
- });
80
+ return containsHtmlTag(text) ? "text/html" : "text/plain; charset=utf-8";
166
81
  }
167
82
  function entryForContentType(contentType) {
168
- const normalized = contentType.toLowerCase().split(";", 1)[0]?.trim() ?? "";
169
- if (normalized === "text/html" || normalized === "application/xhtml+xml")
170
- return "index.html";
171
- if (normalized === "application/json") return "index.json";
172
- if (normalized === "text/css") return "index.css";
173
- if (normalized === "text/javascript" || normalized === "application/javascript")
83
+ const m = contentType.toLowerCase().split(";", 1)[0]?.trim() ?? "";
84
+ if (m === "text/html" || m === "application/xhtml+xml") return "index.html";
85
+ if (m === "application/json") return "index.json";
86
+ if (m === "text/css") return "index.css";
87
+ if (m === "text/javascript" || m === "application/javascript")
174
88
  return "index.js";
175
- if (normalized.startsWith("text/")) return "index.txt";
89
+ if (m.startsWith("text/")) return "index.txt";
176
90
  return "index";
177
91
  }
178
- async function localKind(input) {
179
- try {
180
- const info = await stat2(input);
181
- if (info.isFile()) return "file";
182
- if (info.isDirectory()) return "directory";
183
- return "none";
184
- } catch {
185
- return "none";
186
- }
187
- }
188
- async function fileStaged(path, options) {
189
- const fileStat = await stat2(path);
190
- const manifestPath = options.path ?? basename2(path);
191
- return stagedRequest(
192
- [
193
- {
194
- path: manifestPath,
195
- contentType: options.contentType ?? await detectPathContentType(path),
196
- sizeBytes: fileStat.size,
197
- source: { kind: "path", path }
198
- }
199
- ],
200
- { ...options, entry: options.entry ?? manifestPath }
201
- );
202
- }
203
- async function folderStaged(path, options) {
204
- const files = (await collectPublishFiles(path, options)).map((file) => ({
205
- path: file.path,
206
- contentType: file.contentType,
207
- sizeBytes: file.sizeBytes,
208
- source: { kind: "path", path: file.absolutePath }
209
- }));
210
- return stagedRequest(files, options);
211
- }
212
- async function bytesStaged(bytes, options) {
213
- return stagedRequest(
214
- [
215
- {
216
- path: options.path ?? "index",
217
- contentType: options.contentType ?? detectBytesContentType(bytes),
218
- sizeBytes: bytes.byteLength,
219
- source: { kind: "bytes", bytes }
220
- }
221
- ],
222
- options
223
- );
224
- }
225
- async function stagedRequest(files, options) {
226
- const preparedFiles = await Promise.all(
227
- files.map(async (file) => {
228
- if (file.sizeBytes <= SINGLE_PUT_CHECKSUM_THRESHOLD_BYTES) return file;
229
- const checksumSha256 = file.source.kind === "bytes" ? sha256Bytes(file.source.bytes) : await sha256File(file.source.path);
230
- return { ...file, checksumSha256 };
231
- })
232
- );
233
- const manifestFiles = preparedFiles.map((file) => ({
234
- path: file.path,
235
- contentType: file.contentType,
236
- sizeBytes: file.sizeBytes,
237
- ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
238
- }));
239
- const prepared = {
240
- kind: "staged",
241
- manifest: {
242
- schemaVersion: 1,
243
- files: manifestFiles,
244
- ...options.entry ? { entry: options.entry } : {}
245
- },
246
- files: preparedFiles,
247
- options: optionsBody(options)
248
- };
249
- if (options.metadata) prepared.metadata = options.metadata;
250
- return prepared;
92
+
93
+ // src/publish/paths.ts
94
+ function normalizeManifestPath(path) {
95
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
251
96
  }
252
- function sourceRequest(sourceUrl, options) {
253
- if (!isHttpUrl(sourceUrl)) {
254
- throw new Error(
255
- "source_url must be an http(s) URL. Fetch the content first, then pass it as a string or file."
97
+ function assertValidManifestPath(path) {
98
+ if (path.includes("\\"))
99
+ throw new PublishInputError(
100
+ "invalid_path",
101
+ `Manifest path must use "/": "${path}".`,
102
+ path
256
103
  );
257
- }
258
- const prepared = {
259
- kind: "source",
260
- sourceUrl,
261
- options: optionsBody(options)
262
- };
263
- if (options.metadata) prepared.metadata = options.metadata;
264
- return prepared;
265
- }
266
- async function explicitInput(input, options) {
267
- if ("content" in input)
268
- return stringContent(
269
- input.content,
270
- { ...options, ...input },
271
- input.contentType ?? options.contentType ?? "text/html"
104
+ const p = normalizeManifestPath(path);
105
+ if (p === "")
106
+ throw new PublishInputError(
107
+ "invalid_path",
108
+ "Manifest path must not be empty.",
109
+ path
272
110
  );
273
- if ("sourceUrl" in input)
274
- return sourceRequest(input.sourceUrl, { ...options, ...input });
275
- if ("sourceUrls" in input)
276
- throw new Error(
277
- "sourceUrls inputs are not supported in v1. Publish a single source_url, or fetch the content first."
111
+ if (p.startsWith("/"))
112
+ throw new PublishInputError(
113
+ "invalid_path",
114
+ `Manifest path must be relative: "${path}".`,
115
+ path
116
+ );
117
+ if (p.startsWith("~"))
118
+ throw new PublishInputError(
119
+ "invalid_path",
120
+ `Manifest path must not start with "~": "${path}".`,
121
+ path
122
+ );
123
+ if (p.split("/").includes(".."))
124
+ throw new PublishInputError(
125
+ "invalid_path",
126
+ `Manifest path must not contain "..": "${path}".`,
127
+ path
278
128
  );
279
- return stagedRequest(
280
- input.files.map((file) => {
281
- const bytes = explicitFileBytes(file);
282
- return {
283
- path: file.path,
284
- contentType: file.contentType,
285
- sizeBytes: bytes.byteLength,
286
- source: { kind: "bytes", bytes }
287
- };
288
- }),
289
- { ...options, ...input }
290
- );
291
129
  }
292
- function explicitFileBytes(file) {
293
- if (file.content !== void 0) return Buffer.from(file.content);
294
- if (file.contentBase64 !== void 0)
295
- return Buffer.from(file.contentBase64, "base64");
296
- if (file.bytes !== void 0) return file.bytes;
297
- throw new TypeError(`Explicit file ${file.path} is missing content bytes`);
130
+ function assertNoDuplicatePaths(paths) {
131
+ const seen = /* @__PURE__ */ new Set();
132
+ for (const raw of paths) {
133
+ const p = normalizeManifestPath(raw);
134
+ if (seen.has(p))
135
+ throw new PublishInputError(
136
+ "duplicate_path",
137
+ `Duplicate file path after normalization: "${p}".`,
138
+ p,
139
+ "Give each file a unique path; basename mapping for string[] can collide."
140
+ );
141
+ seen.add(p);
142
+ }
298
143
  }
144
+ function assertNonEmptyBundle(paths) {
145
+ if (paths.length === 0)
146
+ throw new PublishInputError(
147
+ "empty_bundle",
148
+ "Nothing to publish: the bundle has no files.",
149
+ void 0,
150
+ "Pass at least one file, or a directory that contains files."
151
+ );
152
+ }
153
+
154
+ // src/publish/core.ts
299
155
  var VALID_VISIBILITIES = ["public", "unlisted"];
300
156
  var MAX_TITLE_LENGTH = 500;
301
157
  function validatePublishOptions(options) {
@@ -332,69 +188,125 @@ function optionsBody(options) {
332
188
  expiresAt: options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt
333
189
  };
334
190
  }
335
-
336
- // src/publish/upload.ts
337
- import { randomUUID } from "crypto";
338
- import { readFile as readFile2 } from "fs/promises";
339
-
340
- // src/publish/multipart.ts
341
- var MAX_PART_TARGETS_PER_REQUEST = 100;
342
- async function uploadMultipartFile(transport, uploadId, fileId, bytes, partSize) {
343
- if (!partSize || partSize < 1) {
344
- return createErrorResult(
345
- "invalid_upload_target",
346
- "Multipart upload target did not include a valid part size.",
347
- null
191
+ function buildStagedRequest(files, options, entry) {
192
+ assertNonEmptyBundle(files.map((f) => f.path));
193
+ const normalizedPaths = files.map((f) => normalizeManifestPath(f.path));
194
+ assertNoDuplicatePaths(normalizedPaths);
195
+ const manifestFiles = files.map((file) => ({
196
+ path: normalizeManifestPath(file.path),
197
+ contentType: file.contentType,
198
+ sizeBytes: file.sizeBytes,
199
+ ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
200
+ }));
201
+ const resolvedEntry = entry ?? options.entry;
202
+ if (resolvedEntry !== void 0) assertValidManifestPath(resolvedEntry);
203
+ const manifest = {
204
+ schemaVersion: 1,
205
+ files: manifestFiles,
206
+ ...resolvedEntry ? { entry: resolvedEntry } : {}
207
+ };
208
+ const prepared = {
209
+ kind: "staged",
210
+ manifest,
211
+ files,
212
+ options: optionsBody(options)
213
+ };
214
+ if (options.metadata) prepared.metadata = options.metadata;
215
+ return prepared;
216
+ }
217
+ function buildSourceRequest(sourceUrl, options) {
218
+ if (!isHttpUrl(sourceUrl)) {
219
+ throw new PublishInputError(
220
+ "invalid_source_url",
221
+ "source_url must be an http(s) URL.",
222
+ sourceUrl,
223
+ "Fetch the content first, then pass it as content/files."
348
224
  );
349
225
  }
350
- const partNumbers = Array.from(
351
- { length: Math.ceil(bytes.byteLength / partSize) },
352
- (_, index) => index + 1
226
+ const prepared = {
227
+ kind: "source",
228
+ sourceUrl,
229
+ options: optionsBody(options)
230
+ };
231
+ if (options.metadata) prepared.metadata = options.metadata;
232
+ return prepared;
233
+ }
234
+ function decodeBase64(value) {
235
+ const binary = atob(value);
236
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
237
+ }
238
+ function fileBytes(file) {
239
+ if (file.content !== void 0) return new TextEncoder().encode(file.content);
240
+ if (file.contentBase64 !== void 0) return decodeBase64(file.contentBase64);
241
+ if (file.bytes !== void 0) return file.bytes;
242
+ throw new PublishInputError(
243
+ "missing_file_bytes",
244
+ `File "${file.path}" is missing content bytes.`,
245
+ file.path,
246
+ "Provide one of content, contentBase64, or bytes."
353
247
  );
354
- const parts = [];
355
- for (let startIndex = 0; startIndex < partNumbers.length; startIndex += MAX_PART_TARGETS_PER_REQUEST) {
356
- const partNumberBatch = partNumbers.slice(
357
- startIndex,
358
- startIndex + MAX_PART_TARGETS_PER_REQUEST
359
- );
360
- const targetsResult = await transport.request(
361
- "POST",
362
- `/uploads/${encodeURIComponent(uploadId)}/parts`,
363
- { body: { fileId, partNumbers: partNumberBatch } }
364
- );
365
- if (targetsResult.error) return errorResult(targetsResult);
366
- for (const target of targetsResult.data.parts) {
367
- const start = (target.partNumber - 1) * partSize;
368
- const end = Math.min(start + partSize, bytes.byteLength);
369
- const uploadResult = await transport.putSignedUrl(
370
- target.url,
371
- bytes.slice(start, end),
372
- target.headers
373
- );
374
- if (uploadResult.error) return errorResult(uploadResult);
375
- if (!uploadResult.data.etag) {
376
- return createErrorResult(
377
- "missing_upload_etag",
378
- "Signed upload response did not include an ETag.",
379
- null
380
- );
248
+ }
249
+ function singleStagedFile(path, contentType, bytes, options) {
250
+ return buildStagedRequest(
251
+ [
252
+ {
253
+ path,
254
+ contentType,
255
+ sizeBytes: bytes.byteLength,
256
+ getBody: async () => bytes
381
257
  }
382
- parts.push({
383
- partNumber: target.partNumber,
384
- etag: uploadResult.data.etag
385
- });
386
- }
258
+ ],
259
+ options
260
+ );
261
+ }
262
+ async function resolveInMemory(input, options = {}) {
263
+ if (input instanceof URL) {
264
+ return buildSourceRequest(input.toString(), options);
387
265
  }
388
- return { data: parts, error: null, headers: {} };
266
+ if (typeof input === "string") {
267
+ if (isHttpUrl(input)) return buildSourceRequest(input, options);
268
+ const contentType = contentTypeForString(input, options.contentType);
269
+ if (options.path !== void 0) assertValidManifestPath(options.path);
270
+ const path = options.path ?? entryForContentType(contentType);
271
+ const bytes = new TextEncoder().encode(input);
272
+ return singleStagedFile(path, contentType, bytes, options);
273
+ }
274
+ if (input instanceof Uint8Array) {
275
+ const contentType = options.contentType ?? detectBytesContentType(input);
276
+ if (options.path !== void 0) assertValidManifestPath(options.path);
277
+ const path = options.path ?? entryForContentType(contentType);
278
+ return singleStagedFile(path, contentType, input, options);
279
+ }
280
+ if (input.kind === "content") {
281
+ const contentType = input.contentType ?? options.contentType ?? "text/html";
282
+ if (input.path !== void 0) assertValidManifestPath(input.path);
283
+ if (options.path !== void 0) assertValidManifestPath(options.path);
284
+ const path = input.path ?? options.path ?? entryForContentType(contentType);
285
+ const bytes = new TextEncoder().encode(input.content);
286
+ return singleStagedFile(path, contentType, bytes, options);
287
+ }
288
+ if (input.kind === "source_url") {
289
+ return buildSourceRequest(input.sourceUrl, options);
290
+ }
291
+ const files = input.files.map((file) => {
292
+ assertValidManifestPath(file.path);
293
+ const bytes = fileBytes(file);
294
+ const contentType = file.contentType ?? detectBytesContentType(bytes);
295
+ return {
296
+ path: file.path,
297
+ contentType,
298
+ sizeBytes: bytes.byteLength,
299
+ getBody: async () => bytes
300
+ };
301
+ });
302
+ return buildStagedRequest(files, options, input.entry);
389
303
  }
390
304
  function errorResult(result) {
391
305
  if (!result.error) throw new Error("Expected an error result");
392
306
  return { data: null, error: result.error, headers: result.headers };
393
307
  }
394
-
395
- // src/publish/upload.ts
396
308
  async function publishStaged(transport, prepared, finalPath, options = {}) {
397
- const baseKey = options.idempotencyKey ?? `sdk-${randomUUID()}`;
309
+ const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
398
310
  const createResult = await transport.request(
399
311
  "POST",
400
312
  "/uploads",
@@ -403,12 +315,9 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
403
315
  idempotencyKey: `${baseKey}:create-upload`
404
316
  }
405
317
  );
406
- if (createResult.error) return errorResult2(createResult);
407
- const completedFiles = {};
318
+ if (createResult.error) return errorResult(createResult);
408
319
  for (const file of prepared.files) {
409
- const target = createResult.data.files.find(
410
- (candidate) => candidate.path === file.path
411
- );
320
+ const target = createResult.data.files.find((f) => f.path === file.path);
412
321
  if (!target) {
413
322
  return createErrorResult(
414
323
  "upload_target_missing",
@@ -416,36 +325,29 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
416
325
  null
417
326
  );
418
327
  }
419
- if (target.upload.strategy === "multipart") {
420
- const bytes2 = await fileBytes(file);
421
- const partsResult = await uploadMultipartFile(
422
- transport,
423
- createResult.data.uploadId,
424
- target.fileId,
425
- bytes2,
426
- target.upload.partSize
328
+ if (target.upload.strategy !== "single_put") {
329
+ return createErrorResult(
330
+ "unsupported_upload_strategy",
331
+ `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
332
+ null
427
333
  );
428
- if (partsResult.error) return errorResult2(partsResult);
429
- completedFiles[target.fileId] = { parts: partsResult.data };
430
- continue;
431
334
  }
432
- const bytes = await fileBytes(file);
433
- const uploadResult = await transport.putSignedUrl(
335
+ const put = await transport.putSignedUrl(
434
336
  target.upload.url,
435
- bytes,
337
+ await file.getBody(),
436
338
  target.upload.headers
437
339
  );
438
- if (uploadResult.error) return errorResult2(uploadResult);
340
+ if (put.error) return errorResult(put);
439
341
  }
440
342
  const completeResult = await transport.request(
441
343
  "POST",
442
344
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
443
345
  {
444
- body: { files: completedFiles },
346
+ body: { files: {} },
445
347
  idempotencyKey: `${baseKey}:complete-upload`
446
348
  }
447
349
  );
448
- if (completeResult.error) return errorResult2(completeResult);
350
+ if (completeResult.error) return errorResult(completeResult);
449
351
  const finalOptions = {
450
352
  body: {
451
353
  uploadId: createResult.data.uploadId,
@@ -458,13 +360,184 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
458
360
  finalOptions.ifRevision = options.ifRevision;
459
361
  return transport.request("POST", finalPath, finalOptions);
460
362
  }
461
- async function fileBytes(file) {
462
- if (file.source.kind === "bytes") return file.source.bytes;
463
- return readFile2(file.source.path);
363
+ async function publishSource(transport, prepared, finalPath, options = {}) {
364
+ const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
365
+ return transport.request("POST", finalPath, {
366
+ body: {
367
+ sourceUrl: prepared.sourceUrl,
368
+ options: prepared.options,
369
+ ...prepared.metadata ? { metadata: prepared.metadata } : {}
370
+ },
371
+ idempotencyKey: `${baseKey}:publish`,
372
+ ...options.ifRevision !== void 0 ? { ifRevision: options.ifRevision } : {}
373
+ });
464
374
  }
465
- function errorResult2(result) {
466
- if (!result.error) throw new Error("Expected an error result");
467
- return { data: null, error: result.error, headers: result.headers };
375
+
376
+ // src/publish/node.ts
377
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
378
+ import { basename as basename2 } from "path";
379
+
380
+ // src/publish/checksum.ts
381
+ import { createHash } from "crypto";
382
+ import { createReadStream } from "fs";
383
+ async function sha256File(path) {
384
+ const hash = createHash("sha256");
385
+ for await (const chunk of createReadStream(path)) {
386
+ hash.update(chunk);
387
+ }
388
+ return hash.digest("hex");
389
+ }
390
+
391
+ // src/publish/files.ts
392
+ import { readFile, stat } from "fs/promises";
393
+ import { basename, sep } from "path";
394
+ import fg from "fast-glob";
395
+ import ignore from "ignore";
396
+ import { lookup } from "mime-types";
397
+ var DEFAULT_IGNORES = [
398
+ ".git",
399
+ ".git/**",
400
+ "node_modules",
401
+ "node_modules/**",
402
+ ".DS_Store",
403
+ ".env",
404
+ ".env.*",
405
+ ".cache",
406
+ ".cache/**",
407
+ ".tmp",
408
+ ".tmp/**",
409
+ "dist/.cache",
410
+ "dist/.cache/**"
411
+ ];
412
+ async function collectPublishFiles(inputPath, options = {}) {
413
+ const inputStat = await stat(inputPath);
414
+ if (inputStat.isFile()) {
415
+ return [
416
+ {
417
+ absolutePath: inputPath,
418
+ path: basename(inputPath),
419
+ contentType: await detectPathContentType(inputPath),
420
+ sizeBytes: inputStat.size
421
+ }
422
+ ];
423
+ }
424
+ if (!inputStat.isDirectory()) {
425
+ throw new Error(`Input is not a file or directory: ${inputPath}`);
426
+ }
427
+ const matcher = ignore().add([
428
+ ...options.ignoreDefaults === false ? [] : DEFAULT_IGNORES,
429
+ ...options.ignore ?? []
430
+ ]);
431
+ const entries = await fg("**/*", {
432
+ cwd: inputPath,
433
+ onlyFiles: true,
434
+ followSymbolicLinks: false,
435
+ dot: true,
436
+ unique: true
437
+ });
438
+ const paths = entries.map((entry) => entry.split(sep).join("/")).filter((entry) => !matcher.ignores(entry)).sort((a, b) => a.localeCompare(b));
439
+ return Promise.all(
440
+ paths.map(async (path) => {
441
+ const absolutePath = `${inputPath}/${path}`;
442
+ const fileStat = await stat(absolutePath);
443
+ return {
444
+ absolutePath,
445
+ path,
446
+ contentType: await detectPathContentType(absolutePath),
447
+ sizeBytes: fileStat.size
448
+ };
449
+ })
450
+ );
451
+ }
452
+ async function detectPathContentType(path) {
453
+ const detected = lookup(path);
454
+ if (detected) return detected;
455
+ return detectBytesContentType(await readFile(path));
456
+ }
457
+
458
+ // src/publish/node.ts
459
+ var SINGLE_PUT_CHECKSUM_THRESHOLD = 10 * 1024 * 1024;
460
+ async function resolveInput(input, options = {}) {
461
+ if (input instanceof URL || input instanceof Uint8Array || typeof input === "object" && input !== null && "kind" in input) {
462
+ return resolveInMemory(input, options);
463
+ }
464
+ if (Array.isArray(input)) {
465
+ return resolveBundle(input, options);
466
+ }
467
+ if (typeof input === "string") {
468
+ return resolveString(input, options);
469
+ }
470
+ return resolveInMemory(input, options);
471
+ }
472
+ async function resolveString(value, options) {
473
+ if (isHttpUrl(value)) return resolveInMemory(value, options);
474
+ if (value.includes("\n") || value.length > 4096) {
475
+ return resolveInMemory(value, options);
476
+ }
477
+ let info;
478
+ try {
479
+ info = await stat2(value);
480
+ } catch {
481
+ info = void 0;
482
+ }
483
+ if (info?.isFile()) return fileStaged(value, options);
484
+ if (info?.isDirectory()) return folderStaged(value, options);
485
+ if (isPathShaped(value)) {
486
+ throw new PublishInputError(
487
+ "file_not_found",
488
+ `No file or directory at "${value}".`,
489
+ value,
490
+ 'To publish this as inline text, pass { kind: "content", content }.'
491
+ );
492
+ }
493
+ return resolveInMemory(value, options);
494
+ }
495
+ async function fileStaged(path, options) {
496
+ const fileStat = await stat2(path);
497
+ if (options.path !== void 0) assertValidManifestPath(options.path);
498
+ const manifestPath = options.path ?? basename2(path);
499
+ const contentType = options.contentType ?? await detectPathContentType(path);
500
+ const checksumSha256 = fileStat.size > SINGLE_PUT_CHECKSUM_THRESHOLD ? await sha256File(path) : void 0;
501
+ const file = {
502
+ path: manifestPath,
503
+ contentType,
504
+ sizeBytes: fileStat.size,
505
+ ...checksumSha256 ? { checksumSha256 } : {},
506
+ getBody: () => readFile2(path)
507
+ };
508
+ return buildStagedRequest([file], options);
509
+ }
510
+ async function folderStaged(path, options) {
511
+ const files = await collectPublishFiles(path, options);
512
+ const preparedFiles = await Promise.all(files.map(toPreparedFile));
513
+ return buildStagedRequest(preparedFiles, options);
514
+ }
515
+ async function resolveBundle(paths, options) {
516
+ const collected = [];
517
+ for (const element of paths) {
518
+ try {
519
+ await stat2(element);
520
+ } catch {
521
+ throw new PublishInputError(
522
+ "file_not_found",
523
+ `No file or directory at "${element}".`,
524
+ element
525
+ );
526
+ }
527
+ collected.push(...await collectPublishFiles(element, options));
528
+ }
529
+ const preparedFiles = await Promise.all(collected.map(toPreparedFile));
530
+ return buildStagedRequest(preparedFiles, options);
531
+ }
532
+ async function toPreparedFile(pf) {
533
+ const checksumSha256 = pf.sizeBytes > SINGLE_PUT_CHECKSUM_THRESHOLD ? await sha256File(pf.absolutePath) : void 0;
534
+ return {
535
+ path: pf.path,
536
+ contentType: pf.contentType,
537
+ sizeBytes: pf.sizeBytes,
538
+ ...checksumSha256 ? { checksumSha256 } : {},
539
+ getBody: () => readFile2(pf.absolutePath)
540
+ };
468
541
  }
469
542
 
470
543
  // src/resources/account.ts
@@ -533,33 +606,6 @@ var DeploymentsResource = class {
533
606
  this.transport = transport;
534
607
  }
535
608
  transport;
536
- create(dropId, body, options = {}) {
537
- const requestOptions = { body };
538
- if (options.idempotencyKey)
539
- requestOptions.idempotencyKey = options.idempotencyKey;
540
- if (options.ifRevision !== void 0)
541
- requestOptions.ifRevision = options.ifRevision;
542
- return this.transport.request(
543
- "POST",
544
- `/drops/${encodeURIComponent(dropId)}/deployments`,
545
- requestOptions
546
- );
547
- }
548
- createRaw(dropId, body, options = {}) {
549
- const requestOptions = {
550
- body,
551
- bodyCase: "raw"
552
- };
553
- if (options.idempotencyKey)
554
- requestOptions.idempotencyKey = options.idempotencyKey;
555
- if (options.ifRevision !== void 0)
556
- requestOptions.ifRevision = options.ifRevision;
557
- return this.transport.request(
558
- "POST",
559
- `/drops/${encodeURIComponent(dropId)}/deployments`,
560
- requestOptions
561
- );
562
- }
563
609
  list(dropId, params = {}) {
564
610
  return this.transport.request(
565
611
  "GET",
@@ -614,21 +660,6 @@ var DropsResource = class {
614
660
  this.transport = transport;
615
661
  }
616
662
  transport;
617
- create(body, options = {}) {
618
- const requestOptions = { body };
619
- if (options.idempotencyKey)
620
- requestOptions.idempotencyKey = options.idempotencyKey;
621
- return this.transport.request("POST", "/drops", requestOptions);
622
- }
623
- createRaw(body, options = {}) {
624
- const requestOptions = {
625
- body,
626
- bodyCase: "raw"
627
- };
628
- if (options.idempotencyKey)
629
- requestOptions.idempotencyKey = options.idempotencyKey;
630
- return this.transport.request("POST", "/drops", requestOptions);
631
- }
632
663
  async list(params = {}) {
633
664
  const result = await this.transport.request("GET", "/drops", {
634
665
  params
@@ -669,15 +700,22 @@ var DropsResource = class {
669
700
  );
670
701
  }
671
702
  };
703
+ var SETTINGS = [
704
+ "title",
705
+ "visibility",
706
+ "password",
707
+ "noindex",
708
+ "expiresAt",
709
+ "slug"
710
+ ];
672
711
  function updateBody(options) {
673
- const {
674
- metadata,
675
- idempotencyKey: _idempotencyKey,
676
- ifRevision: _ifRevision,
677
- entry: _entry,
678
- ...dropOptions
679
- } = options;
680
- return { options: dropOptions, metadata };
712
+ const out = {};
713
+ for (const key of SETTINGS)
714
+ if (options[key] !== void 0) out[key] = options[key];
715
+ return {
716
+ options: out,
717
+ ...options.metadata !== void 0 ? { metadata: options.metadata } : {}
718
+ };
681
719
  }
682
720
 
683
721
  // src/resources/uploads.ts
@@ -698,13 +736,6 @@ var UploadsResource = class {
698
736
  `/uploads/${encodeURIComponent(uploadId)}`
699
737
  );
700
738
  }
701
- createPartTargets(uploadId, body) {
702
- return this.transport.request(
703
- "POST",
704
- `/uploads/${encodeURIComponent(uploadId)}/parts`,
705
- { body }
706
- );
707
- }
708
739
  complete(uploadId, body, options = {}) {
709
740
  const requestOptions = { body };
710
741
  if (options.idempotencyKey)
@@ -758,25 +789,24 @@ function toSnakeCase(value) {
758
789
 
759
790
  // src/transport.ts
760
791
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
761
- var SDK_VERSION = "0.5.0";
792
+ var SDK_VERSION = "0.6.0";
762
793
  var Transport = class {
763
794
  apiKey;
764
795
  baseUrl;
765
796
  timeoutMs;
797
+ uploadTimeoutMs;
766
798
  fetchImpl;
767
799
  constructor(options = {}) {
768
800
  const resolved = typeof options === "string" ? { apiKey: options } : options;
769
801
  this.apiKey = resolved.apiKey ?? (typeof process !== "undefined" ? process.env?.DROPTHIS_API_KEY : void 0);
770
802
  this.baseUrl = (resolved.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
771
803
  this.timeoutMs = resolved.timeoutMs ?? 3e4;
804
+ this.uploadTimeoutMs = resolved.uploadTimeoutMs ?? 12e4;
772
805
  this.fetchImpl = resolved.fetch ?? globalThis.fetch;
773
806
  }
774
- multipart(method, path, form, options = {}) {
775
- return this.request(method, path, { ...options, body: form });
776
- }
777
807
  async putSignedUrl(url, body, headers) {
778
808
  const controller = new AbortController();
779
- const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
809
+ const timeout = setTimeout(() => controller.abort(), this.uploadTimeoutMs);
780
810
  try {
781
811
  const request = {
782
812
  method: "PUT",
@@ -831,7 +861,7 @@ var Transport = class {
831
861
  if (options.idempotencyKey)
832
862
  headers["idempotency-key"] = options.idempotencyKey;
833
863
  if (options.ifRevision !== void 0)
834
- headers["If-Revision"] = String(options.ifRevision);
864
+ headers["if-revision"] = String(options.ifRevision);
835
865
  if (options.body !== void 0 && !(options.body instanceof FormData))
836
866
  headers["content-type"] = "application/json";
837
867
  const url = new URL(`${this.baseUrl}${path}`);
@@ -863,11 +893,18 @@ var Transport = class {
863
893
  const parsed = parseJson(text);
864
894
  if (!response.ok) {
865
895
  const body = parsed && typeof parsed === "object" ? parsed : {};
866
- const message = stringValue(body.detail) ?? stringValue(body.error) ?? stringValue(body.message) ?? response.statusText;
896
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
867
897
  const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
868
898
  const currentRevision = numberValue(body.current_revision);
899
+ const type = stringValue(body.type);
900
+ const title = stringValue(body.title);
901
+ const instance = stringValue(body.instance);
869
902
  return createErrorResult(code, message, response.status, {
870
- detail: body,
903
+ body,
904
+ ...type !== null ? { type } : {},
905
+ ...title !== null ? { title } : {},
906
+ ...instance !== null ? { instance } : {},
907
+ detail: stringValue(body.detail),
871
908
  param: stringValue(body.param),
872
909
  ...currentRevision !== void 0 ? { currentRevision } : {},
873
910
  requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
@@ -961,54 +998,36 @@ var Dropthis = class {
961
998
  return this.uploadsResource;
962
999
  }
963
1000
  async prepare(input, options = {}) {
964
- return preparePublishRequest(input, options);
1001
+ return resolveInput(input, options);
965
1002
  }
966
1003
  async publish(input, options = {}) {
967
- const prepared = await preparePublishRequest(input, options);
968
- if (prepared.kind === "source")
969
- return this.publishSource(prepared, "/drops", options);
970
- return publishStaged(this.transport, prepared, "/drops", options);
1004
+ let prepared;
1005
+ try {
1006
+ prepared = await resolveInput(input, options);
1007
+ } catch (e) {
1008
+ return toErrorResult(e);
1009
+ }
1010
+ return prepared.kind === "source" ? publishSource(this.transport, prepared, "/drops", options) : publishStaged(this.transport, prepared, "/drops", options);
971
1011
  }
972
1012
  async deploy(dropId, input, options = {}) {
973
- const prepared = await preparePublishRequest(input, options);
1013
+ let prepared;
1014
+ try {
1015
+ prepared = await resolveInput(input, options);
1016
+ } catch (e) {
1017
+ return toErrorResult(e);
1018
+ }
974
1019
  const path = `/drops/${encodeURIComponent(dropId)}/deployments`;
975
- if (prepared.kind === "source")
976
- return createErrorResult(
977
- "source_url_unsupported_for_deployment",
978
- "Publishing a deployment from a source_url is not supported yet. Fetch the content first, then pass it as a string or file.",
979
- null
980
- );
981
- return publishStaged(this.transport, prepared, path, options);
982
- }
983
- publishSource(prepared, path, options) {
984
- const body = {
985
- sourceUrl: prepared.sourceUrl,
986
- options: prepared.options
987
- };
988
- if (prepared.metadata) body.metadata = prepared.metadata;
989
- const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
990
- return this.transport.request("POST", path, {
991
- body,
992
- idempotencyKey: `${baseKey}:publish`
993
- });
1020
+ return prepared.kind === "source" ? publishSource(this.transport, prepared, path, options) : publishStaged(this.transport, prepared, path, options);
994
1021
  }
995
1022
  async update(dropId, options = {}) {
996
1023
  return this.drops.update(dropId, options);
997
1024
  }
998
- request(method, path, options = {}) {
999
- return this.transport.request(method, path, {
1000
- ...options,
1001
- bodyCase: "raw"
1002
- });
1003
- }
1004
- requestSignedUpload(url, bytes, headers) {
1005
- return this.transport.putSignedUrl(url, bytes, headers);
1006
- }
1007
1025
  };
1008
1026
  export {
1009
1027
  CursorPage,
1010
1028
  DeploymentsResource,
1011
1029
  Dropthis,
1030
+ PublishInputError,
1012
1031
  UploadsResource,
1013
1032
  createErrorResult,
1014
1033
  redactSecrets