@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.
@@ -333,10 +333,49 @@ async function resolveInMemory(input, options = {}) {
333
333
  });
334
334
  return buildStagedRequest(files, options, input.entry);
335
335
  }
336
+ var UPLOAD_CONCURRENCY = 5;
336
337
  function errorResult(result) {
337
338
  if (!result.error) throw new Error("Expected an error result");
338
339
  return { data: null, error: result.error, headers: result.headers };
339
340
  }
341
+ async function uploadStagedFiles(transport, jobs) {
342
+ let nextIndex = 0;
343
+ let failed = false;
344
+ const failures = /* @__PURE__ */ new Map();
345
+ async function worker() {
346
+ while (!failed) {
347
+ const index = nextIndex;
348
+ nextIndex += 1;
349
+ if (index >= jobs.length) return;
350
+ const job = jobs[index];
351
+ if (!job) return;
352
+ const put = await transport.putSignedUrl(
353
+ job.target.upload.url,
354
+ await job.file.getBody(),
355
+ job.target.upload.headers
356
+ );
357
+ if (put.error) {
358
+ failed = true;
359
+ failures.set(index, {
360
+ data: null,
361
+ error: {
362
+ ...put.error,
363
+ message: `Upload failed for ${job.file.path}: ${put.error.message}`
364
+ },
365
+ headers: put.headers
366
+ });
367
+ }
368
+ }
369
+ }
370
+ const workers = Array.from(
371
+ { length: Math.min(UPLOAD_CONCURRENCY, jobs.length) },
372
+ () => worker()
373
+ );
374
+ await Promise.all(workers);
375
+ if (failures.size === 0) return null;
376
+ const firstIndex = Math.min(...failures.keys());
377
+ return failures.get(firstIndex) ?? null;
378
+ }
340
379
  async function publishStaged(transport, prepared, finalPath, options = {}) {
341
380
  const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
342
381
  const createResult = await transport.request(
@@ -348,6 +387,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
348
387
  }
349
388
  );
350
389
  if (createResult.error) return errorResult(createResult);
390
+ const jobs = [];
351
391
  for (const file of prepared.files) {
352
392
  const target = createResult.data.files.find((f) => f.path === file.path);
353
393
  if (!target) {
@@ -360,22 +400,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
360
400
  if (target.upload.strategy !== "single_put") {
361
401
  return createErrorResult(
362
402
  "unsupported_upload_strategy",
363
- `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
403
+ `Unexpected upload strategy "${target.upload.strategy}" for ${file.path}. The dropthis upload contract is one signed PUT per file; update @dropthis/node.`,
364
404
  null
365
405
  );
366
406
  }
367
- const put = await transport.putSignedUrl(
368
- target.upload.url,
369
- await file.getBody(),
370
- target.upload.headers
371
- );
372
- if (put.error) return errorResult(put);
407
+ jobs.push({ file, target });
373
408
  }
409
+ const uploadFailure = await uploadStagedFiles(transport, jobs);
410
+ if (uploadFailure) return uploadFailure;
374
411
  const completeResult = await transport.request(
375
412
  "POST",
376
413
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
377
414
  {
378
- body: { files: {} },
379
415
  idempotencyKey: `${baseKey}:complete-upload`
380
416
  }
381
417
  );
@@ -434,6 +470,7 @@ var ApiKeysResource = class {
434
470
  create(input) {
435
471
  return this.transport.request("POST", "/api-keys", { body: input });
436
472
  }
473
+ /** Revoke an API key. 204 No Content — data is null on success. */
437
474
  delete(keyId) {
438
475
  return this.transport.request(
439
476
  "DELETE",
@@ -498,12 +535,29 @@ var CursorPage = class {
498
535
 
499
536
  // src/resources/drops.ts
500
537
  var DropsResource = class {
501
- constructor(transport, resolve) {
538
+ constructor(transport, resolveInput) {
502
539
  this.transport = transport;
503
- this.resolve = resolve;
540
+ this.resolveInput = resolveInput;
504
541
  }
505
542
  transport;
506
- resolve;
543
+ resolveInput;
544
+ parentHosts;
545
+ /**
546
+ * Hostnames the SDK can attribute to dropthis for slug parsing: the canonical
547
+ * viewer domain plus the configured baseUrl's host (covers staging/self-hosted
548
+ * setups where drops are served under the API's own domain).
549
+ */
550
+ allowedParentHosts() {
551
+ if (!this.parentHosts) {
552
+ const hosts = /* @__PURE__ */ new Set([CANONICAL_VIEWER_HOST]);
553
+ try {
554
+ hosts.add(new URL(this.transport.baseUrl).hostname);
555
+ } catch {
556
+ }
557
+ this.parentHosts = hosts;
558
+ }
559
+ return this.parentHosts;
560
+ }
507
561
  /**
508
562
  * Publish content to a NEW permanent public URL; returns the created drop (with its `drop_…` id).
509
563
  * Use to publish / share / post / put online / make public a report, dashboard, site, or file.
@@ -514,7 +568,7 @@ var DropsResource = class {
514
568
  async publish(input, options = {}) {
515
569
  let prepared;
516
570
  try {
517
- prepared = await this.resolve(input, options);
571
+ prepared = await this.resolveInput(input, options);
518
572
  } catch (e) {
519
573
  return toErrorResult(e);
520
574
  }
@@ -531,7 +585,7 @@ var DropsResource = class {
531
585
  const opts = updateContentOptions(options);
532
586
  let prepared;
533
587
  try {
534
- prepared = await this.resolve(input, opts);
588
+ prepared = await this.resolveInput(input, opts);
535
589
  } catch (e) {
536
590
  return toErrorResult(e);
537
591
  }
@@ -559,6 +613,55 @@ var DropsResource = class {
559
613
  `/drops/${encodeURIComponent(dropId)}`
560
614
  );
561
615
  }
616
+ /**
617
+ * Resolve a drop URL (or bare slug) back to the drop — the way to recover a lost
618
+ * `drop_…` id. The slug is parsed client-side from the first hostname label of a
619
+ * dropthis-attributable hostname (`https://<slug>.dropthis.app/…`, or `<slug>.` +
620
+ * the configured baseUrl's host), then matched owner-scoped via GET /drops?slug=.
621
+ * Returns the drop, or `data: null` when no drop of yours has that slug.
622
+ * Custom-domain URLs are rejected client-side with `invalid_drop_url` — they are
623
+ * not resolvable yet.
624
+ */
625
+ async resolve(urlOrSlug) {
626
+ const slug = parseSlug(urlOrSlug, this.allowedParentHosts());
627
+ if (slug === null) {
628
+ return createErrorResult(
629
+ "invalid_drop_url",
630
+ `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.`,
631
+ null
632
+ );
633
+ }
634
+ const result = await this.transport.request("GET", "/drops", { params: { slug } });
635
+ if (result.error)
636
+ return { data: null, error: result.error, headers: result.headers };
637
+ return {
638
+ data: result.data.drops[0] ?? null,
639
+ error: null,
640
+ headers: result.headers
641
+ };
642
+ }
643
+ async getContent(dropId, options = {}) {
644
+ const base = options.deploymentId !== void 0 ? `/drops/${encodeURIComponent(dropId)}/deployments/${encodeURIComponent(options.deploymentId)}/content` : `/drops/${encodeURIComponent(dropId)}/content`;
645
+ if (options.path === void 0)
646
+ return this.transport.request("GET", base);
647
+ const path = options.path;
648
+ const result = await this.transport.requestBytes(base, {
649
+ params: { path }
650
+ });
651
+ if (result.error)
652
+ return { data: null, error: result.error, headers: result.headers };
653
+ const { bytes, contentType } = result.data;
654
+ return {
655
+ data: {
656
+ path,
657
+ contentType,
658
+ bytes,
659
+ text: () => new TextDecoder().decode(bytes)
660
+ },
661
+ error: null,
662
+ headers: result.headers
663
+ };
664
+ }
562
665
  /**
563
666
  * Change an EXISTING drop's settings — title, visibility, password, noindex, expiry,
564
667
  * metadata — by its `drop_…` id. Does not touch content; replace that with {@link updateContent}.
@@ -586,6 +689,25 @@ var DropsResource = class {
586
689
  );
587
690
  }
588
691
  };
692
+ var CANONICAL_VIEWER_HOST = "dropthis.app";
693
+ function parseSlug(input, allowedParents) {
694
+ const value = input.trim();
695
+ if (!value) return null;
696
+ if (!/[./]/.test(value)) return value;
697
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
698
+ let hostname;
699
+ try {
700
+ hostname = new URL(withScheme).hostname;
701
+ } catch {
702
+ return null;
703
+ }
704
+ hostname = hostname.replace(/\.$/, "");
705
+ const dot = hostname.indexOf(".");
706
+ if (dot <= 0) return null;
707
+ const slug = hostname.slice(0, dot);
708
+ const parent = hostname.slice(dot + 1);
709
+ return allowedParents.has(parent) ? slug : null;
710
+ }
589
711
  var SETTINGS = [
590
712
  "title",
591
713
  "visibility",
@@ -638,7 +760,7 @@ function toSnakeCase(value) {
638
760
 
639
761
  // src/transport.ts
640
762
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
641
- var SDK_VERSION = "0.8.0";
763
+ var SDK_VERSION = "0.10.0";
642
764
  var Transport = class {
643
765
  apiKey;
644
766
  baseUrl;
@@ -698,6 +820,63 @@ var Transport = class {
698
820
  clearTimeout(timeout);
699
821
  }
700
822
  }
823
+ /**
824
+ * Authenticated GET that returns the raw response bytes untouched (no JSON parsing,
825
+ * no case conversion). Error responses are still parsed as problem+json. Used for
826
+ * content read-back (`drops.getContent` with a file path).
827
+ */
828
+ async requestBytes(path, options = {}) {
829
+ if (!this.apiKey) {
830
+ return createErrorResult(
831
+ "missing_api_key",
832
+ "No API key provided. Pass apiKey or set DROPTHIS_API_KEY.",
833
+ null
834
+ );
835
+ }
836
+ const headers = {
837
+ "user-agent": `dropthis-node/${SDK_VERSION}`,
838
+ authorization: `Bearer ${this.apiKey}`
839
+ };
840
+ const url = new URL(`${this.baseUrl}${path}`);
841
+ for (const [key, value] of Object.entries(options.params ?? {})) {
842
+ if (value !== void 0 && value !== null)
843
+ url.searchParams.set(key, String(value));
844
+ }
845
+ const controller = new AbortController();
846
+ const timeout = setTimeout(
847
+ () => controller.abort(),
848
+ options.timeoutMs ?? this.timeoutMs
849
+ );
850
+ try {
851
+ const response = await this.fetchImpl(url.toString(), {
852
+ method: "GET",
853
+ headers,
854
+ signal: controller.signal
855
+ });
856
+ const responseHeaders = headersToObject(response.headers);
857
+ if (!response.ok) {
858
+ const text = await response.text();
859
+ return problemResult(response, text, responseHeaders);
860
+ }
861
+ return {
862
+ data: {
863
+ bytes: new Uint8Array(await response.arrayBuffer()),
864
+ contentType: response.headers.get("content-type") ?? responseHeaders["content-type"] ?? null
865
+ },
866
+ error: null,
867
+ headers: responseHeaders
868
+ };
869
+ } catch (error) {
870
+ const message = error instanceof Error && error.name === "AbortError" ? "Request timed out" : error instanceof Error ? error.message : "Network error";
871
+ return createErrorResult(
872
+ error instanceof Error && error.name === "AbortError" ? "timeout" : "network_error",
873
+ message,
874
+ null
875
+ );
876
+ } finally {
877
+ clearTimeout(timeout);
878
+ }
879
+ }
701
880
  async request(method, path, options = {}) {
702
881
  const authenticated = options.authenticated ?? true;
703
882
  if (authenticated && !this.apiKey) {
@@ -744,31 +923,10 @@ var Transport = class {
744
923
  const response = await this.fetchImpl(url.toString(), request);
745
924
  const responseHeaders = headersToObject(response.headers);
746
925
  const text = await response.text();
747
- const parsed = parseJson(text);
748
- if (!response.ok) {
749
- const body = parsed && typeof parsed === "object" ? parsed : {};
750
- const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
751
- const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
752
- const currentRevision = numberValue(body.current_revision);
753
- const type = stringValue(body.type);
754
- const title = stringValue(body.title);
755
- const instance = stringValue(body.instance);
756
- return createErrorResult(code, message, response.status, {
757
- body,
758
- ...type !== null ? { type } : {},
759
- ...title !== null ? { title } : {},
760
- ...instance !== null ? { instance } : {},
761
- detail: stringValue(body.detail),
762
- param: stringValue(body.param),
763
- ...currentRevision !== void 0 ? { currentRevision } : {},
764
- requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
765
- suggestion: stringValue(body.suggestion),
766
- retryable: booleanValue(body.retryable),
767
- headers: responseHeaders
768
- });
769
- }
926
+ if (!response.ok)
927
+ return problemResult(response, text, responseHeaders);
770
928
  return {
771
- data: toCamelCase(parsed),
929
+ data: toCamelCase(parseJson(text)),
772
930
  error: null,
773
931
  headers: responseHeaders
774
932
  };
@@ -784,6 +942,29 @@ var Transport = class {
784
942
  }
785
943
  }
786
944
  };
945
+ function problemResult(response, text, responseHeaders) {
946
+ const parsed = parseJson(text);
947
+ const body = parsed && typeof parsed === "object" ? parsed : {};
948
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
949
+ const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
950
+ const currentRevision = numberValue(body.current_revision);
951
+ const type = stringValue(body.type);
952
+ const title = stringValue(body.title);
953
+ const instance = stringValue(body.instance);
954
+ return createErrorResult(code, message, response.status, {
955
+ body,
956
+ ...type !== null ? { type } : {},
957
+ ...title !== null ? { title } : {},
958
+ ...instance !== null ? { instance } : {},
959
+ detail: stringValue(body.detail),
960
+ param: stringValue(body.param),
961
+ ...currentRevision !== void 0 ? { currentRevision } : {},
962
+ requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
963
+ suggestion: stringValue(body.suggestion),
964
+ retryable: booleanValue(body.retryable),
965
+ headers: responseHeaders
966
+ });
967
+ }
787
968
  function headersToObject(headers) {
788
969
  const result = {};
789
970
  headers.forEach((value, key) => {