@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.
@@ -1,19 +1,52 @@
1
- // src/publish/prepare.ts
2
- import { stat as stat2 } from "fs/promises";
3
- import { basename as basename2 } from "path";
4
-
5
- // src/publish/checksum.ts
6
- import { createHash } from "crypto";
7
- import { createReadStream } from "fs";
8
- function sha256Bytes(bytes) {
9
- return createHash("sha256").update(bytes).digest("hex");
1
+ // src/errors.ts
2
+ var API_KEY_RE = /sk_[A-Za-z0-9_-]+/g;
3
+ function redactSecrets(message) {
4
+ return message.replace(API_KEY_RE, "sk_[redacted]");
10
5
  }
11
- async function sha256File(path) {
12
- const hash = createHash("sha256");
13
- for await (const chunk of createReadStream(path)) {
14
- hash.update(chunk);
15
- }
16
- return hash.digest("hex");
6
+ function createErrorResult(code, message, statusCode, extra = {}) {
7
+ return {
8
+ data: null,
9
+ error: {
10
+ code,
11
+ message: redactSecrets(message),
12
+ statusCode,
13
+ ...extra.type ? { type: extra.type } : {},
14
+ ...extra.title !== void 0 ? { title: extra.title } : {},
15
+ ...extra.detail !== void 0 ? { detail: extra.detail } : {},
16
+ ...extra.instance !== void 0 ? { instance: extra.instance } : {},
17
+ ...extra.param !== void 0 ? { param: extra.param } : {},
18
+ ...extra.currentRevision !== void 0 ? { currentRevision: extra.currentRevision } : {},
19
+ ...extra.requestId !== void 0 ? { requestId: extra.requestId } : {},
20
+ ...extra.suggestion !== void 0 ? { suggestion: extra.suggestion } : {},
21
+ ...extra.retryable !== void 0 ? { retryable: extra.retryable } : {},
22
+ ...extra.body !== void 0 ? { body: extra.body } : {}
23
+ },
24
+ headers: extra.headers ?? {}
25
+ };
26
+ }
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
+ );
17
50
  }
18
51
 
19
52
  // src/publish/detect.ts
@@ -25,246 +58,100 @@ function isHttpUrl(value) {
25
58
  return false;
26
59
  }
27
60
  }
28
- function looksLikeHtml(value) {
29
- const trimmed = value.trimStart().toLowerCase();
30
- 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);
31
63
  }
32
- function detectStringInput(value) {
33
- if (isHttpUrl(value)) return { mode: "sourceUrl" };
34
- return {
35
- mode: "content",
36
- contentType: looksLikeHtml(value) ? "text/html" : "text/plain; charset=utf-8"
37
- };
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";
38
76
  }
39
77
  function detectBytesContentType(bytes) {
40
- const text = Buffer.from(bytes).toString("utf8");
78
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
41
79
  if (text.includes("\uFFFD")) return "application/octet-stream";
42
- return looksLikeHtml(text) ? "text/html" : "text/plain; charset=utf-8";
43
- }
44
-
45
- // src/publish/files.ts
46
- import { readFile, stat } from "fs/promises";
47
- import { basename, sep } from "path";
48
- import fg from "fast-glob";
49
- import ignore from "ignore";
50
- import { lookup } from "mime-types";
51
- var DEFAULT_IGNORES = [
52
- ".git",
53
- ".git/**",
54
- "node_modules",
55
- "node_modules/**",
56
- ".DS_Store",
57
- ".env",
58
- ".env.*",
59
- ".cache",
60
- ".cache/**",
61
- ".tmp",
62
- ".tmp/**",
63
- "dist/.cache",
64
- "dist/.cache/**"
65
- ];
66
- async function collectPublishFiles(inputPath, options = {}) {
67
- const inputStat = await stat(inputPath);
68
- if (inputStat.isFile()) {
69
- return [
70
- {
71
- absolutePath: inputPath,
72
- path: basename(inputPath),
73
- contentType: await detectPathContentType(inputPath),
74
- sizeBytes: inputStat.size
75
- }
76
- ];
77
- }
78
- if (!inputStat.isDirectory()) {
79
- throw new Error(`Input is not a file or directory: ${inputPath}`);
80
- }
81
- const matcher = ignore().add([
82
- ...options.ignoreDefaults === false ? [] : DEFAULT_IGNORES,
83
- ...options.ignore ?? []
84
- ]);
85
- const entries = await fg("**/*", {
86
- cwd: inputPath,
87
- onlyFiles: true,
88
- followSymbolicLinks: false,
89
- dot: true,
90
- unique: true
91
- });
92
- const paths = entries.map((entry) => entry.split(sep).join("/")).filter((entry) => !matcher.ignores(entry)).sort((a, b) => a.localeCompare(b));
93
- return Promise.all(
94
- paths.map(async (path) => {
95
- const absolutePath = `${inputPath}/${path}`;
96
- const fileStat = await stat(absolutePath);
97
- return {
98
- absolutePath,
99
- path,
100
- contentType: await detectPathContentType(absolutePath),
101
- sizeBytes: fileStat.size
102
- };
103
- })
104
- );
105
- }
106
- async function detectPathContentType(path) {
107
- const detected = lookup(path);
108
- if (detected) return detected;
109
- return detectBytesContentType(await readFile(path));
110
- }
111
-
112
- // src/publish/prepare.ts
113
- var SINGLE_PUT_CHECKSUM_THRESHOLD_BYTES = 10 * 1024 * 1024;
114
- async function preparePublishRequest(input, options = {}) {
115
- if (input instanceof URL) {
116
- throw new Error(
117
- "URL inputs are not supported. Fetch the content first, then pass it as a string or file."
118
- );
119
- }
120
- if (typeof input === "string") {
121
- const local = await localKind(input);
122
- if (local === "file") {
123
- return fileStaged(input, options);
124
- }
125
- if (local === "directory") {
126
- return folderStaged(input, options);
127
- }
128
- const detection = detectStringInput(input);
129
- if (detection.mode === "sourceUrl")
130
- throw new Error(
131
- "URL inputs are not supported. Fetch the content first, then pass it as a string or file."
132
- );
133
- return stringContent(input, options, detection.contentType);
134
- }
135
- if (input instanceof Uint8Array || Buffer.isBuffer(input)) {
136
- return bytesStaged(input, options);
137
- }
138
- return explicitInput(input, options);
139
- }
140
- async function stringContent(content, options, detectedContentType) {
141
- const contentType = options.contentType ?? detectedContentType;
142
- return bytesStaged(Buffer.from(content), {
143
- ...options,
144
- path: options.path ?? entryForContentType(contentType),
145
- contentType
146
- });
80
+ return containsHtmlTag(text) ? "text/html" : "text/plain; charset=utf-8";
147
81
  }
148
82
  function entryForContentType(contentType) {
149
- const normalized = contentType.toLowerCase().split(";", 1)[0]?.trim() ?? "";
150
- if (normalized === "text/html" || normalized === "application/xhtml+xml")
151
- return "index.html";
152
- if (normalized === "application/json") return "index.json";
153
- if (normalized === "text/css") return "index.css";
154
- 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")
155
88
  return "index.js";
156
- if (normalized.startsWith("text/")) return "index.txt";
89
+ if (m.startsWith("text/")) return "index.txt";
157
90
  return "index";
158
91
  }
159
- async function localKind(input) {
160
- try {
161
- const info = await stat2(input);
162
- if (info.isFile()) return "file";
163
- if (info.isDirectory()) return "directory";
164
- return "none";
165
- } catch {
166
- return "none";
167
- }
168
- }
169
- async function fileStaged(path, options) {
170
- const fileStat = await stat2(path);
171
- const manifestPath = options.path ?? basename2(path);
172
- return stagedRequest(
173
- [
174
- {
175
- path: manifestPath,
176
- contentType: options.contentType ?? await detectPathContentType(path),
177
- sizeBytes: fileStat.size,
178
- source: { kind: "path", path }
179
- }
180
- ],
181
- { ...options, entry: options.entry ?? manifestPath }
182
- );
183
- }
184
- async function folderStaged(path, options) {
185
- const files = (await collectPublishFiles(path, options)).map((file) => ({
186
- path: file.path,
187
- contentType: file.contentType,
188
- sizeBytes: file.sizeBytes,
189
- source: { kind: "path", path: file.absolutePath }
190
- }));
191
- return stagedRequest(files, options);
192
- }
193
- async function bytesStaged(bytes, options) {
194
- return stagedRequest(
195
- [
196
- {
197
- path: options.path ?? "index",
198
- contentType: options.contentType ?? detectBytesContentType(bytes),
199
- sizeBytes: bytes.byteLength,
200
- source: { kind: "bytes", bytes }
201
- }
202
- ],
203
- options
204
- );
205
- }
206
- async function stagedRequest(files, options) {
207
- const preparedFiles = await Promise.all(
208
- files.map(async (file) => {
209
- if (file.sizeBytes <= SINGLE_PUT_CHECKSUM_THRESHOLD_BYTES) return file;
210
- const checksumSha256 = file.source.kind === "bytes" ? sha256Bytes(file.source.bytes) : await sha256File(file.source.path);
211
- return { ...file, checksumSha256 };
212
- })
213
- );
214
- const manifestFiles = preparedFiles.map((file) => ({
215
- path: file.path,
216
- contentType: file.contentType,
217
- sizeBytes: file.sizeBytes,
218
- ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
219
- }));
220
- const prepared = {
221
- kind: "staged",
222
- manifest: {
223
- schemaVersion: 1,
224
- files: manifestFiles,
225
- ...options.entry ? { entry: options.entry } : {}
226
- },
227
- files: preparedFiles,
228
- options: optionsBody(options)
229
- };
230
- if (options.metadata) prepared.metadata = options.metadata;
231
- return prepared;
92
+
93
+ // src/publish/paths.ts
94
+ function normalizeManifestPath(path) {
95
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
232
96
  }
233
- async function explicitInput(input, options) {
234
- if ("content" in input)
235
- return stringContent(
236
- input.content,
237
- { ...options, ...input },
238
- input.contentType ?? options.contentType ?? "text/html"
97
+ function assertValidManifestPath(path) {
98
+ if (path.includes("\\"))
99
+ throw new PublishInputError(
100
+ "invalid_path",
101
+ `Manifest path must use "/": "${path}".`,
102
+ path
239
103
  );
240
- if ("sourceUrl" in input)
241
- throw new Error(
242
- "sourceUrl inputs are not supported. Fetch the content first, then pass it as a string or file."
104
+ const p = normalizeManifestPath(path);
105
+ if (p === "")
106
+ throw new PublishInputError(
107
+ "invalid_path",
108
+ "Manifest path must not be empty.",
109
+ path
243
110
  );
244
- if ("sourceUrls" in input)
245
- throw new Error(
246
- "sourceUrls inputs are not supported. Fetch the content first, then pass it as a string or file."
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
247
128
  );
248
- return stagedRequest(
249
- input.files.map((file) => {
250
- const bytes = explicitFileBytes(file);
251
- return {
252
- path: file.path,
253
- contentType: file.contentType,
254
- sizeBytes: bytes.byteLength,
255
- source: { kind: "bytes", bytes }
256
- };
257
- }),
258
- { ...options, ...input }
259
- );
260
129
  }
261
- function explicitFileBytes(file) {
262
- if (file.content !== void 0) return Buffer.from(file.content);
263
- if (file.contentBase64 !== void 0)
264
- return Buffer.from(file.contentBase64, "base64");
265
- if (file.bytes !== void 0) return file.bytes;
266
- 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
+ }
267
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
268
155
  var VALID_VISIBILITIES = ["public", "unlisted"];
269
156
  var MAX_TITLE_LENGTH = 500;
270
157
  function validatePublishOptions(options) {
@@ -301,93 +188,125 @@ function optionsBody(options) {
301
188
  expiresAt: options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt
302
189
  };
303
190
  }
304
-
305
- // src/publish/upload.ts
306
- import { randomUUID } from "crypto";
307
- import { readFile as readFile2 } from "fs/promises";
308
-
309
- // src/errors.ts
310
- var API_KEY_RE = /sk_[A-Za-z0-9_-]+/g;
311
- function redactSecrets(message) {
312
- return message.replace(API_KEY_RE, "sk_[redacted]");
313
- }
314
- function createErrorResult(code, message, statusCode, extra = {}) {
315
- return {
316
- data: null,
317
- error: {
318
- code,
319
- message: redactSecrets(message),
320
- statusCode,
321
- ...extra.type ? { type: extra.type } : {},
322
- ...extra.detail !== void 0 ? { detail: extra.detail } : {},
323
- ...extra.param !== void 0 ? { param: extra.param } : {},
324
- ...extra.currentRevision !== void 0 ? { currentRevision: extra.currentRevision } : {},
325
- ...extra.requestId !== void 0 ? { requestId: extra.requestId } : {},
326
- ...extra.suggestion !== void 0 ? { suggestion: extra.suggestion } : {},
327
- ...extra.retryable !== void 0 ? { retryable: extra.retryable } : {}
328
- },
329
- headers: extra.headers ?? {}
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)
330
213
  };
214
+ if (options.metadata) prepared.metadata = options.metadata;
215
+ return prepared;
331
216
  }
332
-
333
- // src/publish/multipart.ts
334
- var MAX_PART_TARGETS_PER_REQUEST = 100;
335
- async function uploadMultipartFile(transport, uploadId, fileId, bytes, partSize) {
336
- if (!partSize || partSize < 1) {
337
- return createErrorResult(
338
- "invalid_upload_target",
339
- "Multipart upload target did not include a valid part size.",
340
- null
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."
341
224
  );
342
225
  }
343
- const partNumbers = Array.from(
344
- { length: Math.ceil(bytes.byteLength / partSize) },
345
- (_, 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."
346
247
  );
347
- const parts = [];
348
- for (let startIndex = 0; startIndex < partNumbers.length; startIndex += MAX_PART_TARGETS_PER_REQUEST) {
349
- const partNumberBatch = partNumbers.slice(
350
- startIndex,
351
- startIndex + MAX_PART_TARGETS_PER_REQUEST
352
- );
353
- const targetsResult = await transport.request(
354
- "POST",
355
- `/uploads/${encodeURIComponent(uploadId)}/parts`,
356
- { body: { fileId, partNumbers: partNumberBatch } }
357
- );
358
- if (targetsResult.error) return errorResult(targetsResult);
359
- for (const target of targetsResult.data.parts) {
360
- const start = (target.partNumber - 1) * partSize;
361
- const end = Math.min(start + partSize, bytes.byteLength);
362
- const uploadResult = await transport.putSignedUrl(
363
- target.url,
364
- bytes.slice(start, end),
365
- target.headers
366
- );
367
- if (uploadResult.error) return errorResult(uploadResult);
368
- if (!uploadResult.data.etag) {
369
- return createErrorResult(
370
- "missing_upload_etag",
371
- "Signed upload response did not include an ETag.",
372
- null
373
- );
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
374
257
  }
375
- parts.push({
376
- partNumber: target.partNumber,
377
- etag: uploadResult.data.etag
378
- });
379
- }
258
+ ],
259
+ options
260
+ );
261
+ }
262
+ async function resolveInMemory(input, options = {}) {
263
+ if (input instanceof URL) {
264
+ return buildSourceRequest(input.toString(), options);
380
265
  }
381
- 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);
382
303
  }
383
304
  function errorResult(result) {
384
305
  if (!result.error) throw new Error("Expected an error result");
385
306
  return { data: null, error: result.error, headers: result.headers };
386
307
  }
387
-
388
- // src/publish/upload.ts
389
308
  async function publishStaged(transport, prepared, finalPath, options = {}) {
390
- const baseKey = options.idempotencyKey ?? `sdk-${randomUUID()}`;
309
+ const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
391
310
  const createResult = await transport.request(
392
311
  "POST",
393
312
  "/uploads",
@@ -396,12 +315,9 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
396
315
  idempotencyKey: `${baseKey}:create-upload`
397
316
  }
398
317
  );
399
- if (createResult.error) return errorResult2(createResult);
400
- const completedFiles = {};
318
+ if (createResult.error) return errorResult(createResult);
401
319
  for (const file of prepared.files) {
402
- const target = createResult.data.files.find(
403
- (candidate) => candidate.path === file.path
404
- );
320
+ const target = createResult.data.files.find((f) => f.path === file.path);
405
321
  if (!target) {
406
322
  return createErrorResult(
407
323
  "upload_target_missing",
@@ -409,36 +325,29 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
409
325
  null
410
326
  );
411
327
  }
412
- if (target.upload.strategy === "multipart") {
413
- const bytes2 = await fileBytes(file);
414
- const partsResult = await uploadMultipartFile(
415
- transport,
416
- createResult.data.uploadId,
417
- target.fileId,
418
- bytes2,
419
- 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
420
333
  );
421
- if (partsResult.error) return errorResult2(partsResult);
422
- completedFiles[target.fileId] = { parts: partsResult.data };
423
- continue;
424
334
  }
425
- const bytes = await fileBytes(file);
426
- const uploadResult = await transport.putSignedUrl(
335
+ const put = await transport.putSignedUrl(
427
336
  target.upload.url,
428
- bytes,
337
+ await file.getBody(),
429
338
  target.upload.headers
430
339
  );
431
- if (uploadResult.error) return errorResult2(uploadResult);
340
+ if (put.error) return errorResult(put);
432
341
  }
433
342
  const completeResult = await transport.request(
434
343
  "POST",
435
344
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
436
345
  {
437
- body: { files: completedFiles },
346
+ body: { files: {} },
438
347
  idempotencyKey: `${baseKey}:complete-upload`
439
348
  }
440
349
  );
441
- if (completeResult.error) return errorResult2(completeResult);
350
+ if (completeResult.error) return errorResult(completeResult);
442
351
  const finalOptions = {
443
352
  body: {
444
353
  uploadId: createResult.data.uploadId,
@@ -451,13 +360,184 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
451
360
  finalOptions.ifRevision = options.ifRevision;
452
361
  return transport.request("POST", finalPath, finalOptions);
453
362
  }
454
- async function fileBytes(file) {
455
- if (file.source.kind === "bytes") return file.source.bytes;
456
- 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
+ });
457
374
  }
458
- function errorResult2(result) {
459
- if (!result.error) throw new Error("Expected an error result");
460
- 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
+ };
461
541
  }
462
542
 
463
543
  // src/resources/account.ts
@@ -526,33 +606,6 @@ var DeploymentsResource = class {
526
606
  this.transport = transport;
527
607
  }
528
608
  transport;
529
- create(dropId, body, options = {}) {
530
- const requestOptions = { body };
531
- if (options.idempotencyKey)
532
- requestOptions.idempotencyKey = options.idempotencyKey;
533
- if (options.ifRevision !== void 0)
534
- requestOptions.ifRevision = options.ifRevision;
535
- return this.transport.request(
536
- "POST",
537
- `/drops/${encodeURIComponent(dropId)}/deployments`,
538
- requestOptions
539
- );
540
- }
541
- createRaw(dropId, body, options = {}) {
542
- const requestOptions = {
543
- body,
544
- bodyCase: "raw"
545
- };
546
- if (options.idempotencyKey)
547
- requestOptions.idempotencyKey = options.idempotencyKey;
548
- if (options.ifRevision !== void 0)
549
- requestOptions.ifRevision = options.ifRevision;
550
- return this.transport.request(
551
- "POST",
552
- `/drops/${encodeURIComponent(dropId)}/deployments`,
553
- requestOptions
554
- );
555
- }
556
609
  list(dropId, params = {}) {
557
610
  return this.transport.request(
558
611
  "GET",
@@ -607,21 +660,6 @@ var DropsResource = class {
607
660
  this.transport = transport;
608
661
  }
609
662
  transport;
610
- create(body, options = {}) {
611
- const requestOptions = { body };
612
- if (options.idempotencyKey)
613
- requestOptions.idempotencyKey = options.idempotencyKey;
614
- return this.transport.request("POST", "/drops", requestOptions);
615
- }
616
- createRaw(body, options = {}) {
617
- const requestOptions = {
618
- body,
619
- bodyCase: "raw"
620
- };
621
- if (options.idempotencyKey)
622
- requestOptions.idempotencyKey = options.idempotencyKey;
623
- return this.transport.request("POST", "/drops", requestOptions);
624
- }
625
663
  async list(params = {}) {
626
664
  const result = await this.transport.request("GET", "/drops", {
627
665
  params
@@ -662,15 +700,22 @@ var DropsResource = class {
662
700
  );
663
701
  }
664
702
  };
703
+ var SETTINGS = [
704
+ "title",
705
+ "visibility",
706
+ "password",
707
+ "noindex",
708
+ "expiresAt",
709
+ "slug"
710
+ ];
665
711
  function updateBody(options) {
666
- const {
667
- metadata,
668
- idempotencyKey: _idempotencyKey,
669
- ifRevision: _ifRevision,
670
- entry: _entry,
671
- ...dropOptions
672
- } = options;
673
- 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
+ };
674
719
  }
675
720
 
676
721
  // src/resources/uploads.ts
@@ -691,13 +736,6 @@ var UploadsResource = class {
691
736
  `/uploads/${encodeURIComponent(uploadId)}`
692
737
  );
693
738
  }
694
- createPartTargets(uploadId, body) {
695
- return this.transport.request(
696
- "POST",
697
- `/uploads/${encodeURIComponent(uploadId)}/parts`,
698
- { body }
699
- );
700
- }
701
739
  complete(uploadId, body, options = {}) {
702
740
  const requestOptions = { body };
703
741
  if (options.idempotencyKey)
@@ -751,25 +789,24 @@ function toSnakeCase(value) {
751
789
 
752
790
  // src/transport.ts
753
791
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
754
- var SDK_VERSION = "0.3.0";
792
+ var SDK_VERSION = "0.6.0";
755
793
  var Transport = class {
756
794
  apiKey;
757
795
  baseUrl;
758
796
  timeoutMs;
797
+ uploadTimeoutMs;
759
798
  fetchImpl;
760
799
  constructor(options = {}) {
761
800
  const resolved = typeof options === "string" ? { apiKey: options } : options;
762
- this.apiKey = resolved.apiKey ?? process.env.DROPTHIS_API_KEY;
801
+ this.apiKey = resolved.apiKey ?? (typeof process !== "undefined" ? process.env?.DROPTHIS_API_KEY : void 0);
763
802
  this.baseUrl = (resolved.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
764
803
  this.timeoutMs = resolved.timeoutMs ?? 3e4;
804
+ this.uploadTimeoutMs = resolved.uploadTimeoutMs ?? 12e4;
765
805
  this.fetchImpl = resolved.fetch ?? globalThis.fetch;
766
806
  }
767
- multipart(method, path, form, options = {}) {
768
- return this.request(method, path, { ...options, body: form });
769
- }
770
807
  async putSignedUrl(url, body, headers) {
771
808
  const controller = new AbortController();
772
- const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
809
+ const timeout = setTimeout(() => controller.abort(), this.uploadTimeoutMs);
773
810
  try {
774
811
  const request = {
775
812
  method: "PUT",
@@ -824,7 +861,7 @@ var Transport = class {
824
861
  if (options.idempotencyKey)
825
862
  headers["idempotency-key"] = options.idempotencyKey;
826
863
  if (options.ifRevision !== void 0)
827
- headers["If-Revision"] = String(options.ifRevision);
864
+ headers["if-revision"] = String(options.ifRevision);
828
865
  if (options.body !== void 0 && !(options.body instanceof FormData))
829
866
  headers["content-type"] = "application/json";
830
867
  const url = new URL(`${this.baseUrl}${path}`);
@@ -856,11 +893,18 @@ var Transport = class {
856
893
  const parsed = parseJson(text);
857
894
  if (!response.ok) {
858
895
  const body = parsed && typeof parsed === "object" ? parsed : {};
859
- const message = stringValue(body.detail) ?? stringValue(body.error) ?? stringValue(body.message) ?? response.statusText;
896
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
860
897
  const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
861
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);
862
902
  return createErrorResult(code, message, response.status, {
863
- detail: body,
903
+ body,
904
+ ...type !== null ? { type } : {},
905
+ ...title !== null ? { title } : {},
906
+ ...instance !== null ? { instance } : {},
907
+ detail: stringValue(body.detail),
864
908
  param: stringValue(body.param),
865
909
  ...currentRevision !== void 0 ? { currentRevision } : {},
866
910
  requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
@@ -954,34 +998,36 @@ var Dropthis = class {
954
998
  return this.uploadsResource;
955
999
  }
956
1000
  async prepare(input, options = {}) {
957
- return preparePublishRequest(input, options);
1001
+ return resolveInput(input, options);
958
1002
  }
959
1003
  async publish(input, options = {}) {
960
- const prepared = await preparePublishRequest(input, options);
961
- 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);
962
1011
  }
963
1012
  async deploy(dropId, input, options = {}) {
964
- 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
+ }
965
1019
  const path = `/drops/${encodeURIComponent(dropId)}/deployments`;
966
- return publishStaged(this.transport, prepared, path, options);
1020
+ return prepared.kind === "source" ? publishSource(this.transport, prepared, path, options) : publishStaged(this.transport, prepared, path, options);
967
1021
  }
968
1022
  async update(dropId, options = {}) {
969
1023
  return this.drops.update(dropId, options);
970
1024
  }
971
- request(method, path, options = {}) {
972
- return this.transport.request(method, path, {
973
- ...options,
974
- bodyCase: "raw"
975
- });
976
- }
977
- requestSignedUpload(url, bytes, headers) {
978
- return this.transport.putSignedUrl(url, bytes, headers);
979
- }
980
1025
  };
981
1026
  export {
982
1027
  CursorPage,
983
1028
  DeploymentsResource,
984
1029
  Dropthis,
1030
+ PublishInputError,
985
1031
  UploadsResource,
986
1032
  createErrorResult,
987
1033
  redactSecrets