@dropthis/cli 0.9.3 → 0.11.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.
@@ -205,6 +205,8 @@ function optionsBody(options) {
205
205
  if (options.expiresAt !== void 0) {
206
206
  body.expiresAt = options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt;
207
207
  }
208
+ if (options.domain !== void 0) body.domain = options.domain;
209
+ if (options.slug !== void 0) body.slug = options.slug;
208
210
  return body;
209
211
  }
210
212
  function updateContentOptions(options) {
@@ -333,10 +335,49 @@ async function resolveInMemory(input, options = {}) {
333
335
  });
334
336
  return buildStagedRequest(files, options, input.entry);
335
337
  }
338
+ var UPLOAD_CONCURRENCY = 5;
336
339
  function errorResult(result) {
337
340
  if (!result.error) throw new Error("Expected an error result");
338
341
  return { data: null, error: result.error, headers: result.headers };
339
342
  }
343
+ async function uploadStagedFiles(transport, jobs) {
344
+ let nextIndex = 0;
345
+ let failed = false;
346
+ const failures = /* @__PURE__ */ new Map();
347
+ async function worker() {
348
+ while (!failed) {
349
+ const index = nextIndex;
350
+ nextIndex += 1;
351
+ if (index >= jobs.length) return;
352
+ const job = jobs[index];
353
+ if (!job) return;
354
+ const put = await transport.putSignedUrl(
355
+ job.target.upload.url,
356
+ await job.file.getBody(),
357
+ job.target.upload.headers
358
+ );
359
+ if (put.error) {
360
+ failed = true;
361
+ failures.set(index, {
362
+ data: null,
363
+ error: {
364
+ ...put.error,
365
+ message: `Upload failed for ${job.file.path}: ${put.error.message}`
366
+ },
367
+ headers: put.headers
368
+ });
369
+ }
370
+ }
371
+ }
372
+ const workers = Array.from(
373
+ { length: Math.min(UPLOAD_CONCURRENCY, jobs.length) },
374
+ () => worker()
375
+ );
376
+ await Promise.all(workers);
377
+ if (failures.size === 0) return null;
378
+ const firstIndex = Math.min(...failures.keys());
379
+ return failures.get(firstIndex) ?? null;
380
+ }
340
381
  async function publishStaged(transport, prepared, finalPath, options = {}) {
341
382
  const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
342
383
  const createResult = await transport.request(
@@ -348,6 +389,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
348
389
  }
349
390
  );
350
391
  if (createResult.error) return errorResult(createResult);
392
+ const jobs = [];
351
393
  for (const file of prepared.files) {
352
394
  const target = createResult.data.files.find((f) => f.path === file.path);
353
395
  if (!target) {
@@ -360,22 +402,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
360
402
  if (target.upload.strategy !== "single_put") {
361
403
  return createErrorResult(
362
404
  "unsupported_upload_strategy",
363
- `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
405
+ `Unexpected upload strategy "${target.upload.strategy}" for ${file.path}. The dropthis upload contract is one signed PUT per file; update @dropthis/node.`,
364
406
  null
365
407
  );
366
408
  }
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);
409
+ jobs.push({ file, target });
373
410
  }
411
+ const uploadFailure = await uploadStagedFiles(transport, jobs);
412
+ if (uploadFailure) return uploadFailure;
374
413
  const completeResult = await transport.request(
375
414
  "POST",
376
415
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
377
416
  {
378
- body: { files: {} },
379
417
  idempotencyKey: `${baseKey}:complete-upload`
380
418
  }
381
419
  );
@@ -434,6 +472,7 @@ var ApiKeysResource = class {
434
472
  create(input) {
435
473
  return this.transport.request("POST", "/api-keys", { body: input });
436
474
  }
475
+ /** Revoke an API key. 204 No Content — data is null on success. */
437
476
  delete(keyId) {
438
477
  return this.transport.request(
439
478
  "DELETE",
@@ -463,6 +502,67 @@ var DeploymentsResource = class {
463
502
  }
464
503
  };
465
504
 
505
+ // src/resources/domains.ts
506
+ var DomainsResource = class {
507
+ constructor(transport) {
508
+ this.transport = transport;
509
+ }
510
+ transport;
511
+ /**
512
+ * Connect a custom domain to the account. Returns the domain in `pending_dns` status with
513
+ * DNS instructions. Idempotent on (account, hostname) — re-connecting an already-connected
514
+ * domain returns the existing row. POST /domains.
515
+ */
516
+ connect(input) {
517
+ return this.transport.request("POST", "/domains", { body: input });
518
+ }
519
+ /** List all custom domains connected to this account. GET /domains. */
520
+ list() {
521
+ return this.transport.request("GET", "/domains");
522
+ }
523
+ /** Get a domain by its stable id or hostname. GET /domains/{id_or_hostname}. */
524
+ get(idOrHostname) {
525
+ return this.transport.request(
526
+ "GET",
527
+ `/domains/${encodeURIComponent(idOrHostname)}`
528
+ );
529
+ }
530
+ /**
531
+ * Trigger a DNS + Cloudflare verification check. Returns the domain with updated status and
532
+ * per-record diagnostics. If DNS is still propagating, `dns[].retryAfter` tells you when to
533
+ * re-call. POST /domains/{id_or_hostname}/verify.
534
+ */
535
+ verify(idOrHostname) {
536
+ return this.transport.request(
537
+ "POST",
538
+ `/domains/${encodeURIComponent(idOrHostname)}/verify`
539
+ );
540
+ }
541
+ /**
542
+ * Update a domain's `dropId` (dedicated mode: repoint to a different drop) or `default`
543
+ * flag (path mode only: set/clear the account's default publish domain). Mode is immutable
544
+ * — delete and reconnect to change it. PATCH /domains/{id_or_hostname}.
545
+ */
546
+ update(idOrHostname, input) {
547
+ return this.transport.request(
548
+ "PATCH",
549
+ `/domains/${encodeURIComponent(idOrHostname)}`,
550
+ { body: input }
551
+ );
552
+ }
553
+ /**
554
+ * Delete a custom domain and remove all its routes. The response includes a dangling-CNAME
555
+ * warning — remove the DNS record after deleting so another account cannot re-claim the
556
+ * hostname. DELETE /domains/{id_or_hostname}.
557
+ */
558
+ delete(idOrHostname) {
559
+ return this.transport.request(
560
+ "DELETE",
561
+ `/domains/${encodeURIComponent(idOrHostname)}`
562
+ );
563
+ }
564
+ };
565
+
466
566
  // src/pagination.ts
467
567
  var CursorPage = class {
468
568
  object = "list";
@@ -498,12 +598,29 @@ var CursorPage = class {
498
598
 
499
599
  // src/resources/drops.ts
500
600
  var DropsResource = class {
501
- constructor(transport, resolve) {
601
+ constructor(transport, resolveInput) {
502
602
  this.transport = transport;
503
- this.resolve = resolve;
603
+ this.resolveInput = resolveInput;
504
604
  }
505
605
  transport;
506
- resolve;
606
+ resolveInput;
607
+ parentHosts;
608
+ /**
609
+ * Hostnames the SDK can attribute to dropthis for slug parsing: the canonical
610
+ * viewer domain plus the configured baseUrl's host (covers staging/self-hosted
611
+ * setups where drops are served under the API's own domain).
612
+ */
613
+ allowedParentHosts() {
614
+ if (!this.parentHosts) {
615
+ const hosts = /* @__PURE__ */ new Set([CANONICAL_VIEWER_HOST]);
616
+ try {
617
+ hosts.add(new URL(this.transport.baseUrl).hostname);
618
+ } catch {
619
+ }
620
+ this.parentHosts = hosts;
621
+ }
622
+ return this.parentHosts;
623
+ }
507
624
  /**
508
625
  * Publish content to a NEW permanent public URL; returns the created drop (with its `drop_…` id).
509
626
  * Use to publish / share / post / put online / make public a report, dashboard, site, or file.
@@ -514,7 +631,7 @@ var DropsResource = class {
514
631
  async publish(input, options = {}) {
515
632
  let prepared;
516
633
  try {
517
- prepared = await this.resolve(input, options);
634
+ prepared = await this.resolveInput(input, options);
518
635
  } catch (e) {
519
636
  return toErrorResult(e);
520
637
  }
@@ -531,7 +648,7 @@ var DropsResource = class {
531
648
  const opts = updateContentOptions(options);
532
649
  let prepared;
533
650
  try {
534
- prepared = await this.resolve(input, opts);
651
+ prepared = await this.resolveInput(input, opts);
535
652
  } catch (e) {
536
653
  return toErrorResult(e);
537
654
  }
@@ -559,10 +676,67 @@ var DropsResource = class {
559
676
  `/drops/${encodeURIComponent(dropId)}`
560
677
  );
561
678
  }
679
+ /**
680
+ * Resolve a drop URL (or bare slug) back to the drop — the way to recover a lost
681
+ * `drop_…` id. The slug is parsed client-side from the first hostname label of a
682
+ * dropthis-attributable hostname (`https://<slug>.dropthis.app/…`, or `<slug>.` +
683
+ * the configured baseUrl's host), then matched owner-scoped via GET /drops?slug=.
684
+ * Returns the drop, or `data: null` when no drop of yours has that slug.
685
+ * Custom-domain URLs are rejected client-side with `invalid_drop_url` — they are
686
+ * not resolvable yet.
687
+ */
688
+ async resolve(urlOrSlug) {
689
+ const slug = parseSlug(urlOrSlug, this.allowedParentHosts());
690
+ if (slug === null) {
691
+ return createErrorResult(
692
+ "invalid_drop_url",
693
+ `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.`,
694
+ null
695
+ );
696
+ }
697
+ const result = await this.transport.request("GET", "/drops", { params: { slug } });
698
+ if (result.error)
699
+ return { data: null, error: result.error, headers: result.headers };
700
+ return {
701
+ data: result.data.drops[0] ?? null,
702
+ error: null,
703
+ headers: result.headers
704
+ };
705
+ }
706
+ async getContent(dropId, options = {}) {
707
+ const base = options.deploymentId !== void 0 ? `/drops/${encodeURIComponent(dropId)}/deployments/${encodeURIComponent(options.deploymentId)}/content` : `/drops/${encodeURIComponent(dropId)}/content`;
708
+ if (options.path === void 0)
709
+ return this.transport.request("GET", base);
710
+ const path = options.path;
711
+ const result = await this.transport.requestBytes(base, {
712
+ params: { path }
713
+ });
714
+ if (result.error)
715
+ return { data: null, error: result.error, headers: result.headers };
716
+ const { bytes, contentType } = result.data;
717
+ return {
718
+ data: {
719
+ path,
720
+ contentType,
721
+ bytes,
722
+ text: () => new TextDecoder().decode(bytes)
723
+ },
724
+ error: null,
725
+ headers: result.headers
726
+ };
727
+ }
562
728
  /**
563
729
  * Change an EXISTING drop's settings — title, visibility, password, noindex, expiry,
564
- * metadata — by its `drop_…` id. Does not touch content; replace that with {@link updateContent}.
565
- * Idempotent. PATCH /drops/{id}.
730
+ * metadata, domain, or slug — by its `drop_…` id. Does not touch content; replace that
731
+ * with {@link updateContent}. Idempotent. PATCH /drops/{id}.
732
+ *
733
+ * **`domain`** — move the drop to a different custom domain (must be live). Pass `null` to
734
+ * move the drop back to the shared pool (unmount from its current domain).
735
+ *
736
+ * **`slug`** — rename the vanity slug on a path-mode custom domain. Only valid when the drop
737
+ * lives on a path-mode domain. Unlike {@link publish} (which auto-suffixes taken slugs),
738
+ * `updateSettings` returns 409 on a slug conflict and never auto-suffixes — your code must
739
+ * catch 409 and retry with a different slug. Passing `slug` on the shared pool returns 422.
566
740
  */
567
741
  updateSettings(dropId, options = {}) {
568
742
  const requestOptions = {
@@ -586,12 +760,33 @@ var DropsResource = class {
586
760
  );
587
761
  }
588
762
  };
763
+ var CANONICAL_VIEWER_HOST = "dropthis.app";
764
+ function parseSlug(input, allowedParents) {
765
+ const value = input.trim();
766
+ if (!value) return null;
767
+ if (!/[./]/.test(value)) return value;
768
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
769
+ let hostname;
770
+ try {
771
+ hostname = new URL(withScheme).hostname;
772
+ } catch {
773
+ return null;
774
+ }
775
+ hostname = hostname.replace(/\.$/, "");
776
+ const dot = hostname.indexOf(".");
777
+ if (dot <= 0) return null;
778
+ const slug = hostname.slice(0, dot);
779
+ const parent = hostname.slice(dot + 1);
780
+ return allowedParents.has(parent) ? slug : null;
781
+ }
589
782
  var SETTINGS = [
590
783
  "title",
591
784
  "visibility",
592
785
  "password",
593
786
  "noindex",
594
- "expiresAt"
787
+ "expiresAt",
788
+ "domain",
789
+ "slug"
595
790
  ];
596
791
  function updateBody(options) {
597
792
  const out = {};
@@ -638,7 +833,7 @@ function toSnakeCase(value) {
638
833
 
639
834
  // src/transport.ts
640
835
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
641
- var SDK_VERSION = "0.8.0";
836
+ var SDK_VERSION = "0.10.0";
642
837
  var Transport = class {
643
838
  apiKey;
644
839
  baseUrl;
@@ -698,6 +893,63 @@ var Transport = class {
698
893
  clearTimeout(timeout);
699
894
  }
700
895
  }
896
+ /**
897
+ * Authenticated GET that returns the raw response bytes untouched (no JSON parsing,
898
+ * no case conversion). Error responses are still parsed as problem+json. Used for
899
+ * content read-back (`drops.getContent` with a file path).
900
+ */
901
+ async requestBytes(path, options = {}) {
902
+ if (!this.apiKey) {
903
+ return createErrorResult(
904
+ "missing_api_key",
905
+ "No API key provided. Pass apiKey or set DROPTHIS_API_KEY.",
906
+ null
907
+ );
908
+ }
909
+ const headers = {
910
+ "user-agent": `dropthis-node/${SDK_VERSION}`,
911
+ authorization: `Bearer ${this.apiKey}`
912
+ };
913
+ const url = new URL(`${this.baseUrl}${path}`);
914
+ for (const [key, value] of Object.entries(options.params ?? {})) {
915
+ if (value !== void 0 && value !== null)
916
+ url.searchParams.set(key, String(value));
917
+ }
918
+ const controller = new AbortController();
919
+ const timeout = setTimeout(
920
+ () => controller.abort(),
921
+ options.timeoutMs ?? this.timeoutMs
922
+ );
923
+ try {
924
+ const response = await this.fetchImpl(url.toString(), {
925
+ method: "GET",
926
+ headers,
927
+ signal: controller.signal
928
+ });
929
+ const responseHeaders = headersToObject(response.headers);
930
+ if (!response.ok) {
931
+ const text = await response.text();
932
+ return problemResult(response, text, responseHeaders);
933
+ }
934
+ return {
935
+ data: {
936
+ bytes: new Uint8Array(await response.arrayBuffer()),
937
+ contentType: response.headers.get("content-type") ?? responseHeaders["content-type"] ?? null
938
+ },
939
+ error: null,
940
+ headers: responseHeaders
941
+ };
942
+ } catch (error) {
943
+ const message = error instanceof Error && error.name === "AbortError" ? "Request timed out" : error instanceof Error ? error.message : "Network error";
944
+ return createErrorResult(
945
+ error instanceof Error && error.name === "AbortError" ? "timeout" : "network_error",
946
+ message,
947
+ null
948
+ );
949
+ } finally {
950
+ clearTimeout(timeout);
951
+ }
952
+ }
701
953
  async request(method, path, options = {}) {
702
954
  const authenticated = options.authenticated ?? true;
703
955
  if (authenticated && !this.apiKey) {
@@ -744,31 +996,10 @@ var Transport = class {
744
996
  const response = await this.fetchImpl(url.toString(), request);
745
997
  const responseHeaders = headersToObject(response.headers);
746
998
  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
- }
999
+ if (!response.ok)
1000
+ return problemResult(response, text, responseHeaders);
770
1001
  return {
771
- data: toCamelCase(parsed),
1002
+ data: toCamelCase(parseJson(text)),
772
1003
  error: null,
773
1004
  headers: responseHeaders
774
1005
  };
@@ -784,6 +1015,29 @@ var Transport = class {
784
1015
  }
785
1016
  }
786
1017
  };
1018
+ function problemResult(response, text, responseHeaders) {
1019
+ const parsed = parseJson(text);
1020
+ const body = parsed && typeof parsed === "object" ? parsed : {};
1021
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
1022
+ const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
1023
+ const currentRevision = numberValue(body.current_revision);
1024
+ const type = stringValue(body.type);
1025
+ const title = stringValue(body.title);
1026
+ const instance = stringValue(body.instance);
1027
+ return createErrorResult(code, message, response.status, {
1028
+ body,
1029
+ ...type !== null ? { type } : {},
1030
+ ...title !== null ? { title } : {},
1031
+ ...instance !== null ? { instance } : {},
1032
+ detail: stringValue(body.detail),
1033
+ param: stringValue(body.param),
1034
+ ...currentRevision !== void 0 ? { currentRevision } : {},
1035
+ requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
1036
+ suggestion: stringValue(body.suggestion),
1037
+ retryable: booleanValue(body.retryable),
1038
+ headers: responseHeaders
1039
+ });
1040
+ }
787
1041
  function headersToObject(headers) {
788
1042
  const result = {};
789
1043
  headers.forEach((value, key) => {
@@ -816,6 +1070,7 @@ var DropthisEdge = class {
816
1070
  accountResource;
817
1071
  apiKeysResource;
818
1072
  deploymentsResource;
1073
+ domainsResource;
819
1074
  constructor(options = {}) {
820
1075
  this.transport = new Transport(options);
821
1076
  }
@@ -839,6 +1094,11 @@ var DropthisEdge = class {
839
1094
  this.deploymentsResource = new DeploymentsResource(this.transport);
840
1095
  return this.deploymentsResource;
841
1096
  }
1097
+ get domains() {
1098
+ if (!this.domainsResource)
1099
+ this.domainsResource = new DomainsResource(this.transport);
1100
+ return this.domainsResource;
1101
+ }
842
1102
  };
843
1103
  // Annotate the CommonJS export names for ESM import in node:
844
1104
  0 && (module.exports = {