@dropthis/cli 0.9.3 → 0.10.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.
@@ -307,10 +307,49 @@ async function resolveInMemory(input, options = {}) {
307
307
  });
308
308
  return buildStagedRequest(files, options, input.entry);
309
309
  }
310
+ var UPLOAD_CONCURRENCY = 5;
310
311
  function errorResult(result) {
311
312
  if (!result.error) throw new Error("Expected an error result");
312
313
  return { data: null, error: result.error, headers: result.headers };
313
314
  }
315
+ async function uploadStagedFiles(transport, jobs) {
316
+ let nextIndex = 0;
317
+ let failed = false;
318
+ const failures = /* @__PURE__ */ new Map();
319
+ async function worker() {
320
+ while (!failed) {
321
+ const index = nextIndex;
322
+ nextIndex += 1;
323
+ if (index >= jobs.length) return;
324
+ const job = jobs[index];
325
+ if (!job) return;
326
+ const put = await transport.putSignedUrl(
327
+ job.target.upload.url,
328
+ await job.file.getBody(),
329
+ job.target.upload.headers
330
+ );
331
+ if (put.error) {
332
+ failed = true;
333
+ failures.set(index, {
334
+ data: null,
335
+ error: {
336
+ ...put.error,
337
+ message: `Upload failed for ${job.file.path}: ${put.error.message}`
338
+ },
339
+ headers: put.headers
340
+ });
341
+ }
342
+ }
343
+ }
344
+ const workers = Array.from(
345
+ { length: Math.min(UPLOAD_CONCURRENCY, jobs.length) },
346
+ () => worker()
347
+ );
348
+ await Promise.all(workers);
349
+ if (failures.size === 0) return null;
350
+ const firstIndex = Math.min(...failures.keys());
351
+ return failures.get(firstIndex) ?? null;
352
+ }
314
353
  async function publishStaged(transport, prepared, finalPath, options = {}) {
315
354
  const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
316
355
  const createResult = await transport.request(
@@ -322,6 +361,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
322
361
  }
323
362
  );
324
363
  if (createResult.error) return errorResult(createResult);
364
+ const jobs = [];
325
365
  for (const file of prepared.files) {
326
366
  const target = createResult.data.files.find((f) => f.path === file.path);
327
367
  if (!target) {
@@ -334,22 +374,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
334
374
  if (target.upload.strategy !== "single_put") {
335
375
  return createErrorResult(
336
376
  "unsupported_upload_strategy",
337
- `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
377
+ `Unexpected upload strategy "${target.upload.strategy}" for ${file.path}. The dropthis upload contract is one signed PUT per file; update @dropthis/node.`,
338
378
  null
339
379
  );
340
380
  }
341
- const put = await transport.putSignedUrl(
342
- target.upload.url,
343
- await file.getBody(),
344
- target.upload.headers
345
- );
346
- if (put.error) return errorResult(put);
381
+ jobs.push({ file, target });
347
382
  }
383
+ const uploadFailure = await uploadStagedFiles(transport, jobs);
384
+ if (uploadFailure) return uploadFailure;
348
385
  const completeResult = await transport.request(
349
386
  "POST",
350
387
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
351
388
  {
352
- body: { files: {} },
353
389
  idempotencyKey: `${baseKey}:complete-upload`
354
390
  }
355
391
  );
@@ -408,6 +444,7 @@ var ApiKeysResource = class {
408
444
  create(input) {
409
445
  return this.transport.request("POST", "/api-keys", { body: input });
410
446
  }
447
+ /** Revoke an API key. 204 No Content — data is null on success. */
411
448
  delete(keyId) {
412
449
  return this.transport.request(
413
450
  "DELETE",
@@ -472,12 +509,29 @@ var CursorPage = class {
472
509
 
473
510
  // src/resources/drops.ts
474
511
  var DropsResource = class {
475
- constructor(transport, resolve) {
512
+ constructor(transport, resolveInput) {
476
513
  this.transport = transport;
477
- this.resolve = resolve;
514
+ this.resolveInput = resolveInput;
478
515
  }
479
516
  transport;
480
- resolve;
517
+ resolveInput;
518
+ parentHosts;
519
+ /**
520
+ * Hostnames the SDK can attribute to dropthis for slug parsing: the canonical
521
+ * viewer domain plus the configured baseUrl's host (covers staging/self-hosted
522
+ * setups where drops are served under the API's own domain).
523
+ */
524
+ allowedParentHosts() {
525
+ if (!this.parentHosts) {
526
+ const hosts = /* @__PURE__ */ new Set([CANONICAL_VIEWER_HOST]);
527
+ try {
528
+ hosts.add(new URL(this.transport.baseUrl).hostname);
529
+ } catch {
530
+ }
531
+ this.parentHosts = hosts;
532
+ }
533
+ return this.parentHosts;
534
+ }
481
535
  /**
482
536
  * Publish content to a NEW permanent public URL; returns the created drop (with its `drop_…` id).
483
537
  * Use to publish / share / post / put online / make public a report, dashboard, site, or file.
@@ -488,7 +542,7 @@ var DropsResource = class {
488
542
  async publish(input, options = {}) {
489
543
  let prepared;
490
544
  try {
491
- prepared = await this.resolve(input, options);
545
+ prepared = await this.resolveInput(input, options);
492
546
  } catch (e) {
493
547
  return toErrorResult(e);
494
548
  }
@@ -505,7 +559,7 @@ var DropsResource = class {
505
559
  const opts = updateContentOptions(options);
506
560
  let prepared;
507
561
  try {
508
- prepared = await this.resolve(input, opts);
562
+ prepared = await this.resolveInput(input, opts);
509
563
  } catch (e) {
510
564
  return toErrorResult(e);
511
565
  }
@@ -533,6 +587,55 @@ var DropsResource = class {
533
587
  `/drops/${encodeURIComponent(dropId)}`
534
588
  );
535
589
  }
590
+ /**
591
+ * Resolve a drop URL (or bare slug) back to the drop — the way to recover a lost
592
+ * `drop_…` id. The slug is parsed client-side from the first hostname label of a
593
+ * dropthis-attributable hostname (`https://<slug>.dropthis.app/…`, or `<slug>.` +
594
+ * the configured baseUrl's host), then matched owner-scoped via GET /drops?slug=.
595
+ * Returns the drop, or `data: null` when no drop of yours has that slug.
596
+ * Custom-domain URLs are rejected client-side with `invalid_drop_url` — they are
597
+ * not resolvable yet.
598
+ */
599
+ async resolve(urlOrSlug) {
600
+ const slug = parseSlug(urlOrSlug, this.allowedParentHosts());
601
+ if (slug === null) {
602
+ return createErrorResult(
603
+ "invalid_drop_url",
604
+ `Could not resolve a drop slug from "${urlOrSlug}". Pass a canonical drop URL like https://<slug>.dropthis.app/ or the bare slug \u2014 custom-domain URLs cannot be resolved yet.`,
605
+ null
606
+ );
607
+ }
608
+ const result = await this.transport.request("GET", "/drops", { params: { slug } });
609
+ if (result.error)
610
+ return { data: null, error: result.error, headers: result.headers };
611
+ return {
612
+ data: result.data.drops[0] ?? null,
613
+ error: null,
614
+ headers: result.headers
615
+ };
616
+ }
617
+ async getContent(dropId, options = {}) {
618
+ const base = options.deploymentId !== void 0 ? `/drops/${encodeURIComponent(dropId)}/deployments/${encodeURIComponent(options.deploymentId)}/content` : `/drops/${encodeURIComponent(dropId)}/content`;
619
+ if (options.path === void 0)
620
+ return this.transport.request("GET", base);
621
+ const path = options.path;
622
+ const result = await this.transport.requestBytes(base, {
623
+ params: { path }
624
+ });
625
+ if (result.error)
626
+ return { data: null, error: result.error, headers: result.headers };
627
+ const { bytes, contentType } = result.data;
628
+ return {
629
+ data: {
630
+ path,
631
+ contentType,
632
+ bytes,
633
+ text: () => new TextDecoder().decode(bytes)
634
+ },
635
+ error: null,
636
+ headers: result.headers
637
+ };
638
+ }
536
639
  /**
537
640
  * Change an EXISTING drop's settings — title, visibility, password, noindex, expiry,
538
641
  * metadata — by its `drop_…` id. Does not touch content; replace that with {@link updateContent}.
@@ -560,6 +663,25 @@ var DropsResource = class {
560
663
  );
561
664
  }
562
665
  };
666
+ var CANONICAL_VIEWER_HOST = "dropthis.app";
667
+ function parseSlug(input, allowedParents) {
668
+ const value = input.trim();
669
+ if (!value) return null;
670
+ if (!/[./]/.test(value)) return value;
671
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
672
+ let hostname;
673
+ try {
674
+ hostname = new URL(withScheme).hostname;
675
+ } catch {
676
+ return null;
677
+ }
678
+ hostname = hostname.replace(/\.$/, "");
679
+ const dot = hostname.indexOf(".");
680
+ if (dot <= 0) return null;
681
+ const slug = hostname.slice(0, dot);
682
+ const parent = hostname.slice(dot + 1);
683
+ return allowedParents.has(parent) ? slug : null;
684
+ }
563
685
  var SETTINGS = [
564
686
  "title",
565
687
  "visibility",
@@ -612,7 +734,7 @@ function toSnakeCase(value) {
612
734
 
613
735
  // src/transport.ts
614
736
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
615
- var SDK_VERSION = "0.8.0";
737
+ var SDK_VERSION = "0.10.0";
616
738
  var Transport = class {
617
739
  apiKey;
618
740
  baseUrl;
@@ -672,6 +794,63 @@ var Transport = class {
672
794
  clearTimeout(timeout);
673
795
  }
674
796
  }
797
+ /**
798
+ * Authenticated GET that returns the raw response bytes untouched (no JSON parsing,
799
+ * no case conversion). Error responses are still parsed as problem+json. Used for
800
+ * content read-back (`drops.getContent` with a file path).
801
+ */
802
+ async requestBytes(path, options = {}) {
803
+ if (!this.apiKey) {
804
+ return createErrorResult(
805
+ "missing_api_key",
806
+ "No API key provided. Pass apiKey or set DROPTHIS_API_KEY.",
807
+ null
808
+ );
809
+ }
810
+ const headers = {
811
+ "user-agent": `dropthis-node/${SDK_VERSION}`,
812
+ authorization: `Bearer ${this.apiKey}`
813
+ };
814
+ const url = new URL(`${this.baseUrl}${path}`);
815
+ for (const [key, value] of Object.entries(options.params ?? {})) {
816
+ if (value !== void 0 && value !== null)
817
+ url.searchParams.set(key, String(value));
818
+ }
819
+ const controller = new AbortController();
820
+ const timeout = setTimeout(
821
+ () => controller.abort(),
822
+ options.timeoutMs ?? this.timeoutMs
823
+ );
824
+ try {
825
+ const response = await this.fetchImpl(url.toString(), {
826
+ method: "GET",
827
+ headers,
828
+ signal: controller.signal
829
+ });
830
+ const responseHeaders = headersToObject(response.headers);
831
+ if (!response.ok) {
832
+ const text = await response.text();
833
+ return problemResult(response, text, responseHeaders);
834
+ }
835
+ return {
836
+ data: {
837
+ bytes: new Uint8Array(await response.arrayBuffer()),
838
+ contentType: response.headers.get("content-type") ?? responseHeaders["content-type"] ?? null
839
+ },
840
+ error: null,
841
+ headers: responseHeaders
842
+ };
843
+ } catch (error) {
844
+ const message = error instanceof Error && error.name === "AbortError" ? "Request timed out" : error instanceof Error ? error.message : "Network error";
845
+ return createErrorResult(
846
+ error instanceof Error && error.name === "AbortError" ? "timeout" : "network_error",
847
+ message,
848
+ null
849
+ );
850
+ } finally {
851
+ clearTimeout(timeout);
852
+ }
853
+ }
675
854
  async request(method, path, options = {}) {
676
855
  const authenticated = options.authenticated ?? true;
677
856
  if (authenticated && !this.apiKey) {
@@ -718,31 +897,10 @@ var Transport = class {
718
897
  const response = await this.fetchImpl(url.toString(), request);
719
898
  const responseHeaders = headersToObject(response.headers);
720
899
  const text = await response.text();
721
- const parsed = parseJson(text);
722
- if (!response.ok) {
723
- const body = parsed && typeof parsed === "object" ? parsed : {};
724
- const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
725
- const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
726
- const currentRevision = numberValue(body.current_revision);
727
- const type = stringValue(body.type);
728
- const title = stringValue(body.title);
729
- const instance = stringValue(body.instance);
730
- return createErrorResult(code, message, response.status, {
731
- body,
732
- ...type !== null ? { type } : {},
733
- ...title !== null ? { title } : {},
734
- ...instance !== null ? { instance } : {},
735
- detail: stringValue(body.detail),
736
- param: stringValue(body.param),
737
- ...currentRevision !== void 0 ? { currentRevision } : {},
738
- requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
739
- suggestion: stringValue(body.suggestion),
740
- retryable: booleanValue(body.retryable),
741
- headers: responseHeaders
742
- });
743
- }
900
+ if (!response.ok)
901
+ return problemResult(response, text, responseHeaders);
744
902
  return {
745
- data: toCamelCase(parsed),
903
+ data: toCamelCase(parseJson(text)),
746
904
  error: null,
747
905
  headers: responseHeaders
748
906
  };
@@ -758,6 +916,29 @@ var Transport = class {
758
916
  }
759
917
  }
760
918
  };
919
+ function problemResult(response, text, responseHeaders) {
920
+ const parsed = parseJson(text);
921
+ const body = parsed && typeof parsed === "object" ? parsed : {};
922
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
923
+ const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
924
+ const currentRevision = numberValue(body.current_revision);
925
+ const type = stringValue(body.type);
926
+ const title = stringValue(body.title);
927
+ const instance = stringValue(body.instance);
928
+ return createErrorResult(code, message, response.status, {
929
+ body,
930
+ ...type !== null ? { type } : {},
931
+ ...title !== null ? { title } : {},
932
+ ...instance !== null ? { instance } : {},
933
+ detail: stringValue(body.detail),
934
+ param: stringValue(body.param),
935
+ ...currentRevision !== void 0 ? { currentRevision } : {},
936
+ requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
937
+ suggestion: stringValue(body.suggestion),
938
+ retryable: booleanValue(body.retryable),
939
+ headers: responseHeaders
940
+ });
941
+ }
761
942
  function headersToObject(headers) {
762
943
  const result = {};
763
944
  headers.forEach((value, key) => {