@dropthis/cli 0.20.0 → 0.21.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.
@@ -185,11 +185,13 @@ The `drops.publish()` and `drops.updateContent()` methods accept:
185
185
  - **URL object** -- `new URL("https://example.com/page")` (source fetch)
186
186
  - **Bytes** -- `new Uint8Array(...)` (raw bytes)
187
187
  - **Explicit content** -- `{ kind: "content", content: "...", contentType?: "text/html", path?: "page.html" }`
188
- - **Source URL** -- `{ kind: "source_url", sourceUrl: "https://example.com/page" }`
189
- - **File bundle** -- `{ kind: "files", files: [{ path, content?, contentBase64?, bytes?, contentType? }], entry? }`
188
+ - **Source URL** -- `{ kind: "source_url", sourceUrl: "https://example.com/page" }` (the server fetches it; an HTML page becomes a site, any other file becomes a single-file drop with a `rawUrl`)
189
+ - **File bundle** -- `{ kind: "files", files: [{ path, content? | contentBase64? | bytes? | sourceUrl?, contentType? }], entry? }`. Give each file its bytes inline (`content`/`contentBase64`/`bytes`) **or** a `sourceUrl` for the server to fetch — never both on one file. Mix them freely in one bundle.
190
190
 
191
191
  Drop **settings** (title, visibility, password, noindex, expiresAt, metadata) go in the second `options` argument, not in the input object.
192
192
 
193
+ > **A `source_url` to a non-HTML file now becomes a single-file drop.** Point a top-level source URL (`new URL(...)` or `{ kind: "source_url" }`) at an image, PDF, or text file and you get a single-file `file_viewer` drop with `rawUrl` set — the same as publishing those bytes directly. Previously a source URL only worked for HTML pages.
194
+
193
195
  All inputs are uploaded through staged presigned URLs — one signed PUT per file, up to 5 files in parallel. The SDK handles this transparently.
194
196
 
195
197
  ### Explicit input examples
@@ -220,6 +222,22 @@ await dropthis.drops.publish(
220
222
  },
221
223
  { title: "My Site" },
222
224
  );
225
+
226
+ // Publish by reference: inline your HTML, let the server fetch the images.
227
+ // Each `sourceUrl` file is fetched server-side (no bytes pass through your
228
+ // process) and lands at its path in the bundle — one self-contained drop.
229
+ await dropthis.drops.publish({
230
+ kind: "files",
231
+ files: [
232
+ {
233
+ path: "index.html",
234
+ content: '<h1>Gallery</h1><img src="hero.png"><img src="logo.svg">',
235
+ },
236
+ { path: "hero.png", sourceUrl: "https://cdn.example.com/hero.png" },
237
+ { path: "logo.svg", sourceUrl: "https://cdn.example.com/logo.svg" },
238
+ ],
239
+ entry: "index.html",
240
+ });
223
241
  ```
224
242
 
225
243
  ## Prepare (validate without sending)
@@ -204,11 +204,33 @@ type ListDropsParams = {
204
204
  /** Only drops mounted on this custom domain hostname (e.g. "reports.example.com"). */
205
205
  domain?: string;
206
206
  };
207
+ /**
208
+ * One file in an upload manifest. A discriminated union:
209
+ *
210
+ * - **client-put** — the SDK pushes the bytes through a signed PUT. Requires
211
+ * `contentType` and `sizeBytes`; an optional `checksumSha256` locks the
212
+ * transfer integrity.
213
+ * - **remote** — the server fetches the bytes itself from `sourceUrl` during
214
+ * `POST /uploads/{id}/ingest`. No bytes leave this process, so `sizeBytes`,
215
+ * `contentType`, and `checksumSha256` are all optional (the server infers
216
+ * them on fetch). Carries NO signed PUT target.
217
+ *
218
+ * `sourceUrl` is the discriminant: present ⇒ remote, absent ⇒ client-put. The
219
+ * camelCase `sourceUrl` becomes the wire field `source_url` via the transport's
220
+ * snake_case conversion (exactly like `contentType` → `content_type`).
221
+ */
207
222
  type UploadManifestFile = {
208
223
  path: string;
209
224
  contentType: string;
210
225
  sizeBytes: number;
211
226
  checksumSha256?: string | null;
227
+ sourceUrl?: never;
228
+ } | {
229
+ path: string;
230
+ sourceUrl: string;
231
+ contentType?: string;
232
+ sizeBytes?: number;
233
+ checksumSha256?: string | null;
212
234
  };
213
235
  type CreateUploadSessionRequest = {
214
236
  schemaVersion?: 1;
@@ -316,12 +338,41 @@ type UpdateContentOptions = PrepareOptions & RequestControls & {
316
338
  /** Entry file for multi-file bundles (default: index.html). */
317
339
  entry?: string;
318
340
  };
341
+ /**
342
+ * One file in a multi-file `{ kind: "files" }` bundle. Supply the bytes inline
343
+ * exactly one way — `content` (UTF-8 text), `contentBase64`, or `bytes` — OR set
344
+ * `sourceUrl` to a public http(s) URL and let the server fetch that file for you
345
+ * server-side (no bytes pass through your process). A single entry may NOT carry
346
+ * both inline bytes and `sourceUrl`. Mix freely within one bundle: e.g. inline
347
+ * `content` for `index.html` plus a `sourceUrl` for each image referenced by it,
348
+ * yielding one self-contained drop.
349
+ */
319
350
  type PublishFileInput = {
320
351
  path: string;
321
352
  contentType?: string;
322
353
  content?: string;
323
354
  contentBase64?: string;
324
355
  bytes?: Uint8Array;
356
+ /**
357
+ * Public http(s) URL the server fetches this file's bytes from during publish
358
+ * (publish by reference). Mutually exclusive with `content`/`contentBase64`/`bytes`.
359
+ */
360
+ sourceUrl?: string;
361
+ /**
362
+ * Declared byte size of the remote file (optional hint for `sourceUrl` files).
363
+ * When provided, the server uses this for upfront quota admission before fetching,
364
+ * so a large file is rejected immediately rather than after the server downloads it.
365
+ * Ignored for inline files (size is computed from the bytes).
366
+ */
367
+ sizeBytes?: number;
368
+ /**
369
+ * Expected SHA-256 hex digest of the remote file (optional hint for `sourceUrl` files).
370
+ * When provided, the server verifies the fetched content matches this checksum and
371
+ * rejects the publish if it does not, giving you integrity verification without
372
+ * downloading the bytes yourself.
373
+ * Ignored for inline files (checksum is computed by the SDK/server from actual bytes).
374
+ */
375
+ checksumSha256?: string;
325
376
  };
326
377
  type PublishInput = string | string[] | URL | Uint8Array | {
327
378
  kind: "content";
@@ -485,17 +536,32 @@ declare class Transport {
485
536
  }
486
537
 
487
538
  /**
488
- * A file ready to be staged for upload. The body is lazy: the orchestrator
489
- * calls `getBody()` per file when it is ready to push bytes to the signed URL.
490
- * This keeps the resolution layer pure and lets the filesystem layer (node.ts)
491
- * defer reads/streams until upload time.
539
+ * A file ready to be staged for upload. Two shapes:
540
+ *
541
+ * - **client-put** the SDK pushes the bytes. The body is lazy: the orchestrator
542
+ * calls `getBody()` per file when it is ready to push bytes to the signed URL.
543
+ * This keeps the resolution layer pure and lets the filesystem layer (node.ts)
544
+ * defer reads/streams until upload time.
545
+ * - **remote** (`sourceUrl` set, no `getBody`) — the server fetches the bytes from
546
+ * `sourceUrl` during `POST /uploads/{id}/ingest`. No bytes leave this process, so
547
+ * it has no `getBody`, no `sizeBytes`, and consumes no signed PUT target.
492
548
  */
493
549
  type PreparedUploadFile = {
494
550
  path: string;
495
551
  contentType: string;
496
552
  sizeBytes: number;
497
553
  checksumSha256?: string;
554
+ sourceUrl?: never;
498
555
  getBody(): Promise<Uint8Array | Blob | ReadableStream>;
556
+ } | {
557
+ path: string;
558
+ sourceUrl: string;
559
+ contentType?: string;
560
+ /** Optional hint: declared byte size for upfront quota admission (server-side). */
561
+ sizeBytes?: number;
562
+ /** Optional hint: expected SHA-256 hex digest for server-side integrity check. */
563
+ checksumSha256?: string;
564
+ getBody?: never;
499
565
  };
500
566
  /**
501
567
  * Resolves a publish input into a {@link PreparedPublishRequest}. Two implementations exist:
@@ -204,11 +204,33 @@ type ListDropsParams = {
204
204
  /** Only drops mounted on this custom domain hostname (e.g. "reports.example.com"). */
205
205
  domain?: string;
206
206
  };
207
+ /**
208
+ * One file in an upload manifest. A discriminated union:
209
+ *
210
+ * - **client-put** — the SDK pushes the bytes through a signed PUT. Requires
211
+ * `contentType` and `sizeBytes`; an optional `checksumSha256` locks the
212
+ * transfer integrity.
213
+ * - **remote** — the server fetches the bytes itself from `sourceUrl` during
214
+ * `POST /uploads/{id}/ingest`. No bytes leave this process, so `sizeBytes`,
215
+ * `contentType`, and `checksumSha256` are all optional (the server infers
216
+ * them on fetch). Carries NO signed PUT target.
217
+ *
218
+ * `sourceUrl` is the discriminant: present ⇒ remote, absent ⇒ client-put. The
219
+ * camelCase `sourceUrl` becomes the wire field `source_url` via the transport's
220
+ * snake_case conversion (exactly like `contentType` → `content_type`).
221
+ */
207
222
  type UploadManifestFile = {
208
223
  path: string;
209
224
  contentType: string;
210
225
  sizeBytes: number;
211
226
  checksumSha256?: string | null;
227
+ sourceUrl?: never;
228
+ } | {
229
+ path: string;
230
+ sourceUrl: string;
231
+ contentType?: string;
232
+ sizeBytes?: number;
233
+ checksumSha256?: string | null;
212
234
  };
213
235
  type CreateUploadSessionRequest = {
214
236
  schemaVersion?: 1;
@@ -316,12 +338,41 @@ type UpdateContentOptions = PrepareOptions & RequestControls & {
316
338
  /** Entry file for multi-file bundles (default: index.html). */
317
339
  entry?: string;
318
340
  };
341
+ /**
342
+ * One file in a multi-file `{ kind: "files" }` bundle. Supply the bytes inline
343
+ * exactly one way — `content` (UTF-8 text), `contentBase64`, or `bytes` — OR set
344
+ * `sourceUrl` to a public http(s) URL and let the server fetch that file for you
345
+ * server-side (no bytes pass through your process). A single entry may NOT carry
346
+ * both inline bytes and `sourceUrl`. Mix freely within one bundle: e.g. inline
347
+ * `content` for `index.html` plus a `sourceUrl` for each image referenced by it,
348
+ * yielding one self-contained drop.
349
+ */
319
350
  type PublishFileInput = {
320
351
  path: string;
321
352
  contentType?: string;
322
353
  content?: string;
323
354
  contentBase64?: string;
324
355
  bytes?: Uint8Array;
356
+ /**
357
+ * Public http(s) URL the server fetches this file's bytes from during publish
358
+ * (publish by reference). Mutually exclusive with `content`/`contentBase64`/`bytes`.
359
+ */
360
+ sourceUrl?: string;
361
+ /**
362
+ * Declared byte size of the remote file (optional hint for `sourceUrl` files).
363
+ * When provided, the server uses this for upfront quota admission before fetching,
364
+ * so a large file is rejected immediately rather than after the server downloads it.
365
+ * Ignored for inline files (size is computed from the bytes).
366
+ */
367
+ sizeBytes?: number;
368
+ /**
369
+ * Expected SHA-256 hex digest of the remote file (optional hint for `sourceUrl` files).
370
+ * When provided, the server verifies the fetched content matches this checksum and
371
+ * rejects the publish if it does not, giving you integrity verification without
372
+ * downloading the bytes yourself.
373
+ * Ignored for inline files (checksum is computed by the SDK/server from actual bytes).
374
+ */
375
+ checksumSha256?: string;
325
376
  };
326
377
  type PublishInput = string | string[] | URL | Uint8Array | {
327
378
  kind: "content";
@@ -485,17 +536,32 @@ declare class Transport {
485
536
  }
486
537
 
487
538
  /**
488
- * A file ready to be staged for upload. The body is lazy: the orchestrator
489
- * calls `getBody()` per file when it is ready to push bytes to the signed URL.
490
- * This keeps the resolution layer pure and lets the filesystem layer (node.ts)
491
- * defer reads/streams until upload time.
539
+ * A file ready to be staged for upload. Two shapes:
540
+ *
541
+ * - **client-put** the SDK pushes the bytes. The body is lazy: the orchestrator
542
+ * calls `getBody()` per file when it is ready to push bytes to the signed URL.
543
+ * This keeps the resolution layer pure and lets the filesystem layer (node.ts)
544
+ * defer reads/streams until upload time.
545
+ * - **remote** (`sourceUrl` set, no `getBody`) — the server fetches the bytes from
546
+ * `sourceUrl` during `POST /uploads/{id}/ingest`. No bytes leave this process, so
547
+ * it has no `getBody`, no `sizeBytes`, and consumes no signed PUT target.
492
548
  */
493
549
  type PreparedUploadFile = {
494
550
  path: string;
495
551
  contentType: string;
496
552
  sizeBytes: number;
497
553
  checksumSha256?: string;
554
+ sourceUrl?: never;
498
555
  getBody(): Promise<Uint8Array | Blob | ReadableStream>;
556
+ } | {
557
+ path: string;
558
+ sourceUrl: string;
559
+ contentType?: string;
560
+ /** Optional hint: declared byte size for upfront quota admission (server-side). */
561
+ sizeBytes?: number;
562
+ /** Optional hint: expected SHA-256 hex digest for server-side integrity check. */
563
+ checksumSha256?: string;
564
+ getBody?: never;
499
565
  };
500
566
  /**
501
567
  * Resolves a publish input into a {@link PreparedPublishRequest}. Two implementations exist:
@@ -226,12 +226,26 @@ function buildStagedRequest(files, options, entry) {
226
226
  assertNonEmptyBundle(files.map((f) => f.path));
227
227
  const normalizedPaths = files.map((f) => normalizeManifestPath(f.path));
228
228
  assertNoDuplicatePaths(normalizedPaths);
229
- const manifestFiles = files.map((file) => ({
230
- path: normalizeManifestPath(file.path),
231
- contentType: file.contentType,
232
- sizeBytes: file.sizeBytes,
233
- ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
234
- }));
229
+ const manifestFiles = files.map(
230
+ (file) => file.sourceUrl !== void 0 ? (
231
+ // Remote entry: the server fetches the bytes on /ingest. Emit sourceUrl
232
+ // plus optional metadata hints (contentType, sizeBytes, checksumSha256)
233
+ // when the caller declared them. The transport snake_cases all camelCase
234
+ // keys to source_url / content_type / size_bytes / checksum_sha256.
235
+ {
236
+ path: normalizeManifestPath(file.path),
237
+ sourceUrl: file.sourceUrl,
238
+ ...file.contentType ? { contentType: file.contentType } : {},
239
+ ...file.sizeBytes !== void 0 ? { sizeBytes: file.sizeBytes } : {},
240
+ ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
241
+ }
242
+ ) : {
243
+ path: normalizeManifestPath(file.path),
244
+ contentType: file.contentType,
245
+ sizeBytes: file.sizeBytes,
246
+ ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
247
+ }
248
+ );
235
249
  const resolvedEntry = entry ?? options.entry;
236
250
  if (resolvedEntry !== void 0) assertValidManifestPath(resolvedEntry);
237
251
  const manifest = {
@@ -277,9 +291,48 @@ function fileBytes(file) {
277
291
  "missing_file_bytes",
278
292
  `File "${file.path}" is missing content bytes.`,
279
293
  file.path,
280
- "Provide one of content, contentBase64, or bytes."
294
+ "Provide one of content, contentBase64, or bytes, or set sourceUrl."
281
295
  );
282
296
  }
297
+ function hasInlineBytes(file) {
298
+ return file.content !== void 0 || file.contentBase64 !== void 0 || file.bytes !== void 0;
299
+ }
300
+ function prepareBundleFile(file) {
301
+ assertValidManifestPath(file.path);
302
+ if (file.sourceUrl !== void 0) {
303
+ if (hasInlineBytes(file)) {
304
+ throw new PublishInputError(
305
+ "conflicting_file_source",
306
+ `File "${file.path}" sets both inline content and sourceUrl; pick one.`,
307
+ file.path,
308
+ "Drop sourceUrl to upload bytes, or drop content/contentBase64/bytes to fetch by reference."
309
+ );
310
+ }
311
+ if (!isHttpUrl(file.sourceUrl)) {
312
+ throw new PublishInputError(
313
+ "invalid_source_url",
314
+ `File "${file.path}" sourceUrl must be an http(s) URL.`,
315
+ file.path,
316
+ "Fetch the bytes first and pass them inline, or use an http(s) URL."
317
+ );
318
+ }
319
+ return {
320
+ path: file.path,
321
+ sourceUrl: file.sourceUrl,
322
+ ...file.contentType ? { contentType: file.contentType } : {},
323
+ ...file.sizeBytes !== void 0 ? { sizeBytes: file.sizeBytes } : {},
324
+ ...file.checksumSha256 ? { checksumSha256: file.checksumSha256 } : {}
325
+ };
326
+ }
327
+ const bytes = fileBytes(file);
328
+ const contentType = file.contentType ?? detectBytesContentType(bytes);
329
+ return {
330
+ path: file.path,
331
+ contentType,
332
+ sizeBytes: bytes.byteLength,
333
+ getBody: async () => bytes
334
+ };
335
+ }
283
336
  function singleStagedFile(path, contentType, bytes, options) {
284
337
  return buildStagedRequest(
285
338
  [
@@ -322,20 +375,11 @@ async function resolveInMemory(input, options = {}) {
322
375
  if (input.kind === "source_url") {
323
376
  return buildSourceRequest(input.sourceUrl, options);
324
377
  }
325
- const files = input.files.map((file) => {
326
- assertValidManifestPath(file.path);
327
- const bytes = fileBytes(file);
328
- const contentType = file.contentType ?? detectBytesContentType(bytes);
329
- return {
330
- path: file.path,
331
- contentType,
332
- sizeBytes: bytes.byteLength,
333
- getBody: async () => bytes
334
- };
335
- });
378
+ const files = input.files.map(prepareBundleFile);
336
379
  return buildStagedRequest(files, options, input.entry);
337
380
  }
338
381
  var UPLOAD_CONCURRENCY = 5;
382
+ var INGEST_TIMEOUT_MS = 12e4;
339
383
  function errorResult(result) {
340
384
  if (!result.error) throw new Error("Expected an error result");
341
385
  return { data: null, error: result.error, headers: result.headers };
@@ -391,6 +435,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
391
435
  if (createResult.error) return errorResult(createResult);
392
436
  const jobs = [];
393
437
  for (const file of prepared.files) {
438
+ if (file.sourceUrl !== void 0) continue;
394
439
  const target = createResult.data.files.find((f) => f.path === file.path);
395
440
  if (!target) {
396
441
  return createErrorResult(
@@ -410,6 +455,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
410
455
  }
411
456
  const uploadFailure = await uploadStagedFiles(transport, jobs);
412
457
  if (uploadFailure) return uploadFailure;
458
+ const hasRemote = prepared.files.some((f) => f.sourceUrl !== void 0);
459
+ if (hasRemote) {
460
+ const ingestResult = await transport.request(
461
+ "POST",
462
+ `/uploads/${encodeURIComponent(createResult.data.uploadId)}/ingest`,
463
+ {
464
+ idempotencyKey: `${baseKey}:ingest-upload`,
465
+ timeoutMs: INGEST_TIMEOUT_MS
466
+ }
467
+ );
468
+ if (ingestResult.error) return errorResult(ingestResult);
469
+ }
413
470
  const completeResult = await transport.request(
414
471
  "POST",
415
472
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
@@ -804,7 +861,7 @@ function toSnakeCase(value) {
804
861
 
805
862
  // src/transport.ts
806
863
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
807
- var SDK_VERSION = "0.18.0";
864
+ var SDK_VERSION = "0.19.0";
808
865
  var Transport = class {
809
866
  apiKey;
810
867
  baseUrl;