@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.
@@ -179,6 +179,8 @@ function optionsBody(options) {
179
179
  if (options.expiresAt !== void 0) {
180
180
  body.expiresAt = options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt;
181
181
  }
182
+ if (options.domain !== void 0) body.domain = options.domain;
183
+ if (options.slug !== void 0) body.slug = options.slug;
182
184
  return body;
183
185
  }
184
186
  function updateContentOptions(options) {
@@ -307,10 +309,49 @@ async function resolveInMemory(input, options = {}) {
307
309
  });
308
310
  return buildStagedRequest(files, options, input.entry);
309
311
  }
312
+ var UPLOAD_CONCURRENCY = 5;
310
313
  function errorResult(result) {
311
314
  if (!result.error) throw new Error("Expected an error result");
312
315
  return { data: null, error: result.error, headers: result.headers };
313
316
  }
317
+ async function uploadStagedFiles(transport, jobs) {
318
+ let nextIndex = 0;
319
+ let failed = false;
320
+ const failures = /* @__PURE__ */ new Map();
321
+ async function worker() {
322
+ while (!failed) {
323
+ const index = nextIndex;
324
+ nextIndex += 1;
325
+ if (index >= jobs.length) return;
326
+ const job = jobs[index];
327
+ if (!job) return;
328
+ const put = await transport.putSignedUrl(
329
+ job.target.upload.url,
330
+ await job.file.getBody(),
331
+ job.target.upload.headers
332
+ );
333
+ if (put.error) {
334
+ failed = true;
335
+ failures.set(index, {
336
+ data: null,
337
+ error: {
338
+ ...put.error,
339
+ message: `Upload failed for ${job.file.path}: ${put.error.message}`
340
+ },
341
+ headers: put.headers
342
+ });
343
+ }
344
+ }
345
+ }
346
+ const workers = Array.from(
347
+ { length: Math.min(UPLOAD_CONCURRENCY, jobs.length) },
348
+ () => worker()
349
+ );
350
+ await Promise.all(workers);
351
+ if (failures.size === 0) return null;
352
+ const firstIndex = Math.min(...failures.keys());
353
+ return failures.get(firstIndex) ?? null;
354
+ }
314
355
  async function publishStaged(transport, prepared, finalPath, options = {}) {
315
356
  const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
316
357
  const createResult = await transport.request(
@@ -322,6 +363,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
322
363
  }
323
364
  );
324
365
  if (createResult.error) return errorResult(createResult);
366
+ const jobs = [];
325
367
  for (const file of prepared.files) {
326
368
  const target = createResult.data.files.find((f) => f.path === file.path);
327
369
  if (!target) {
@@ -334,22 +376,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
334
376
  if (target.upload.strategy !== "single_put") {
335
377
  return createErrorResult(
336
378
  "unsupported_upload_strategy",
337
- `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
379
+ `Unexpected upload strategy "${target.upload.strategy}" for ${file.path}. The dropthis upload contract is one signed PUT per file; update @dropthis/node.`,
338
380
  null
339
381
  );
340
382
  }
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);
383
+ jobs.push({ file, target });
347
384
  }
385
+ const uploadFailure = await uploadStagedFiles(transport, jobs);
386
+ if (uploadFailure) return uploadFailure;
348
387
  const completeResult = await transport.request(
349
388
  "POST",
350
389
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
351
390
  {
352
- body: { files: {} },
353
391
  idempotencyKey: `${baseKey}:complete-upload`
354
392
  }
355
393
  );
@@ -408,6 +446,7 @@ var ApiKeysResource = class {
408
446
  create(input) {
409
447
  return this.transport.request("POST", "/api-keys", { body: input });
410
448
  }
449
+ /** Revoke an API key. 204 No Content — data is null on success. */
411
450
  delete(keyId) {
412
451
  return this.transport.request(
413
452
  "DELETE",
@@ -437,6 +476,67 @@ var DeploymentsResource = class {
437
476
  }
438
477
  };
439
478
 
479
+ // src/resources/domains.ts
480
+ var DomainsResource = class {
481
+ constructor(transport) {
482
+ this.transport = transport;
483
+ }
484
+ transport;
485
+ /**
486
+ * Connect a custom domain to the account. Returns the domain in `pending_dns` status with
487
+ * DNS instructions. Idempotent on (account, hostname) — re-connecting an already-connected
488
+ * domain returns the existing row. POST /domains.
489
+ */
490
+ connect(input) {
491
+ return this.transport.request("POST", "/domains", { body: input });
492
+ }
493
+ /** List all custom domains connected to this account. GET /domains. */
494
+ list() {
495
+ return this.transport.request("GET", "/domains");
496
+ }
497
+ /** Get a domain by its stable id or hostname. GET /domains/{id_or_hostname}. */
498
+ get(idOrHostname) {
499
+ return this.transport.request(
500
+ "GET",
501
+ `/domains/${encodeURIComponent(idOrHostname)}`
502
+ );
503
+ }
504
+ /**
505
+ * Trigger a DNS + Cloudflare verification check. Returns the domain with updated status and
506
+ * per-record diagnostics. If DNS is still propagating, `dns[].retryAfter` tells you when to
507
+ * re-call. POST /domains/{id_or_hostname}/verify.
508
+ */
509
+ verify(idOrHostname) {
510
+ return this.transport.request(
511
+ "POST",
512
+ `/domains/${encodeURIComponent(idOrHostname)}/verify`
513
+ );
514
+ }
515
+ /**
516
+ * Update a domain's `dropId` (dedicated mode: repoint to a different drop) or `default`
517
+ * flag (path mode only: set/clear the account's default publish domain). Mode is immutable
518
+ * — delete and reconnect to change it. PATCH /domains/{id_or_hostname}.
519
+ */
520
+ update(idOrHostname, input) {
521
+ return this.transport.request(
522
+ "PATCH",
523
+ `/domains/${encodeURIComponent(idOrHostname)}`,
524
+ { body: input }
525
+ );
526
+ }
527
+ /**
528
+ * Delete a custom domain and remove all its routes. The response includes a dangling-CNAME
529
+ * warning — remove the DNS record after deleting so another account cannot re-claim the
530
+ * hostname. DELETE /domains/{id_or_hostname}.
531
+ */
532
+ delete(idOrHostname) {
533
+ return this.transport.request(
534
+ "DELETE",
535
+ `/domains/${encodeURIComponent(idOrHostname)}`
536
+ );
537
+ }
538
+ };
539
+
440
540
  // src/pagination.ts
441
541
  var CursorPage = class {
442
542
  object = "list";
@@ -472,12 +572,29 @@ var CursorPage = class {
472
572
 
473
573
  // src/resources/drops.ts
474
574
  var DropsResource = class {
475
- constructor(transport, resolve) {
575
+ constructor(transport, resolveInput) {
476
576
  this.transport = transport;
477
- this.resolve = resolve;
577
+ this.resolveInput = resolveInput;
478
578
  }
479
579
  transport;
480
- resolve;
580
+ resolveInput;
581
+ parentHosts;
582
+ /**
583
+ * Hostnames the SDK can attribute to dropthis for slug parsing: the canonical
584
+ * viewer domain plus the configured baseUrl's host (covers staging/self-hosted
585
+ * setups where drops are served under the API's own domain).
586
+ */
587
+ allowedParentHosts() {
588
+ if (!this.parentHosts) {
589
+ const hosts = /* @__PURE__ */ new Set([CANONICAL_VIEWER_HOST]);
590
+ try {
591
+ hosts.add(new URL(this.transport.baseUrl).hostname);
592
+ } catch {
593
+ }
594
+ this.parentHosts = hosts;
595
+ }
596
+ return this.parentHosts;
597
+ }
481
598
  /**
482
599
  * Publish content to a NEW permanent public URL; returns the created drop (with its `drop_…` id).
483
600
  * Use to publish / share / post / put online / make public a report, dashboard, site, or file.
@@ -488,7 +605,7 @@ var DropsResource = class {
488
605
  async publish(input, options = {}) {
489
606
  let prepared;
490
607
  try {
491
- prepared = await this.resolve(input, options);
608
+ prepared = await this.resolveInput(input, options);
492
609
  } catch (e) {
493
610
  return toErrorResult(e);
494
611
  }
@@ -505,7 +622,7 @@ var DropsResource = class {
505
622
  const opts = updateContentOptions(options);
506
623
  let prepared;
507
624
  try {
508
- prepared = await this.resolve(input, opts);
625
+ prepared = await this.resolveInput(input, opts);
509
626
  } catch (e) {
510
627
  return toErrorResult(e);
511
628
  }
@@ -533,10 +650,67 @@ var DropsResource = class {
533
650
  `/drops/${encodeURIComponent(dropId)}`
534
651
  );
535
652
  }
653
+ /**
654
+ * Resolve a drop URL (or bare slug) back to the drop — the way to recover a lost
655
+ * `drop_…` id. The slug is parsed client-side from the first hostname label of a
656
+ * dropthis-attributable hostname (`https://<slug>.dropthis.app/…`, or `<slug>.` +
657
+ * the configured baseUrl's host), then matched owner-scoped via GET /drops?slug=.
658
+ * Returns the drop, or `data: null` when no drop of yours has that slug.
659
+ * Custom-domain URLs are rejected client-side with `invalid_drop_url` — they are
660
+ * not resolvable yet.
661
+ */
662
+ async resolve(urlOrSlug) {
663
+ const slug = parseSlug(urlOrSlug, this.allowedParentHosts());
664
+ if (slug === null) {
665
+ return createErrorResult(
666
+ "invalid_drop_url",
667
+ `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.`,
668
+ null
669
+ );
670
+ }
671
+ const result = await this.transport.request("GET", "/drops", { params: { slug } });
672
+ if (result.error)
673
+ return { data: null, error: result.error, headers: result.headers };
674
+ return {
675
+ data: result.data.drops[0] ?? null,
676
+ error: null,
677
+ headers: result.headers
678
+ };
679
+ }
680
+ async getContent(dropId, options = {}) {
681
+ const base = options.deploymentId !== void 0 ? `/drops/${encodeURIComponent(dropId)}/deployments/${encodeURIComponent(options.deploymentId)}/content` : `/drops/${encodeURIComponent(dropId)}/content`;
682
+ if (options.path === void 0)
683
+ return this.transport.request("GET", base);
684
+ const path = options.path;
685
+ const result = await this.transport.requestBytes(base, {
686
+ params: { path }
687
+ });
688
+ if (result.error)
689
+ return { data: null, error: result.error, headers: result.headers };
690
+ const { bytes, contentType } = result.data;
691
+ return {
692
+ data: {
693
+ path,
694
+ contentType,
695
+ bytes,
696
+ text: () => new TextDecoder().decode(bytes)
697
+ },
698
+ error: null,
699
+ headers: result.headers
700
+ };
701
+ }
536
702
  /**
537
703
  * Change an EXISTING drop's settings — title, visibility, password, noindex, expiry,
538
- * metadata — by its `drop_…` id. Does not touch content; replace that with {@link updateContent}.
539
- * Idempotent. PATCH /drops/{id}.
704
+ * metadata, domain, or slug — by its `drop_…` id. Does not touch content; replace that
705
+ * with {@link updateContent}. Idempotent. PATCH /drops/{id}.
706
+ *
707
+ * **`domain`** — move the drop to a different custom domain (must be live). Pass `null` to
708
+ * move the drop back to the shared pool (unmount from its current domain).
709
+ *
710
+ * **`slug`** — rename the vanity slug on a path-mode custom domain. Only valid when the drop
711
+ * lives on a path-mode domain. Unlike {@link publish} (which auto-suffixes taken slugs),
712
+ * `updateSettings` returns 409 on a slug conflict and never auto-suffixes — your code must
713
+ * catch 409 and retry with a different slug. Passing `slug` on the shared pool returns 422.
540
714
  */
541
715
  updateSettings(dropId, options = {}) {
542
716
  const requestOptions = {
@@ -560,12 +734,33 @@ var DropsResource = class {
560
734
  );
561
735
  }
562
736
  };
737
+ var CANONICAL_VIEWER_HOST = "dropthis.app";
738
+ function parseSlug(input, allowedParents) {
739
+ const value = input.trim();
740
+ if (!value) return null;
741
+ if (!/[./]/.test(value)) return value;
742
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
743
+ let hostname;
744
+ try {
745
+ hostname = new URL(withScheme).hostname;
746
+ } catch {
747
+ return null;
748
+ }
749
+ hostname = hostname.replace(/\.$/, "");
750
+ const dot = hostname.indexOf(".");
751
+ if (dot <= 0) return null;
752
+ const slug = hostname.slice(0, dot);
753
+ const parent = hostname.slice(dot + 1);
754
+ return allowedParents.has(parent) ? slug : null;
755
+ }
563
756
  var SETTINGS = [
564
757
  "title",
565
758
  "visibility",
566
759
  "password",
567
760
  "noindex",
568
- "expiresAt"
761
+ "expiresAt",
762
+ "domain",
763
+ "slug"
569
764
  ];
570
765
  function updateBody(options) {
571
766
  const out = {};
@@ -612,7 +807,7 @@ function toSnakeCase(value) {
612
807
 
613
808
  // src/transport.ts
614
809
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
615
- var SDK_VERSION = "0.8.0";
810
+ var SDK_VERSION = "0.10.0";
616
811
  var Transport = class {
617
812
  apiKey;
618
813
  baseUrl;
@@ -672,6 +867,63 @@ var Transport = class {
672
867
  clearTimeout(timeout);
673
868
  }
674
869
  }
870
+ /**
871
+ * Authenticated GET that returns the raw response bytes untouched (no JSON parsing,
872
+ * no case conversion). Error responses are still parsed as problem+json. Used for
873
+ * content read-back (`drops.getContent` with a file path).
874
+ */
875
+ async requestBytes(path, options = {}) {
876
+ if (!this.apiKey) {
877
+ return createErrorResult(
878
+ "missing_api_key",
879
+ "No API key provided. Pass apiKey or set DROPTHIS_API_KEY.",
880
+ null
881
+ );
882
+ }
883
+ const headers = {
884
+ "user-agent": `dropthis-node/${SDK_VERSION}`,
885
+ authorization: `Bearer ${this.apiKey}`
886
+ };
887
+ const url = new URL(`${this.baseUrl}${path}`);
888
+ for (const [key, value] of Object.entries(options.params ?? {})) {
889
+ if (value !== void 0 && value !== null)
890
+ url.searchParams.set(key, String(value));
891
+ }
892
+ const controller = new AbortController();
893
+ const timeout = setTimeout(
894
+ () => controller.abort(),
895
+ options.timeoutMs ?? this.timeoutMs
896
+ );
897
+ try {
898
+ const response = await this.fetchImpl(url.toString(), {
899
+ method: "GET",
900
+ headers,
901
+ signal: controller.signal
902
+ });
903
+ const responseHeaders = headersToObject(response.headers);
904
+ if (!response.ok) {
905
+ const text = await response.text();
906
+ return problemResult(response, text, responseHeaders);
907
+ }
908
+ return {
909
+ data: {
910
+ bytes: new Uint8Array(await response.arrayBuffer()),
911
+ contentType: response.headers.get("content-type") ?? responseHeaders["content-type"] ?? null
912
+ },
913
+ error: null,
914
+ headers: responseHeaders
915
+ };
916
+ } catch (error) {
917
+ const message = error instanceof Error && error.name === "AbortError" ? "Request timed out" : error instanceof Error ? error.message : "Network error";
918
+ return createErrorResult(
919
+ error instanceof Error && error.name === "AbortError" ? "timeout" : "network_error",
920
+ message,
921
+ null
922
+ );
923
+ } finally {
924
+ clearTimeout(timeout);
925
+ }
926
+ }
675
927
  async request(method, path, options = {}) {
676
928
  const authenticated = options.authenticated ?? true;
677
929
  if (authenticated && !this.apiKey) {
@@ -718,31 +970,10 @@ var Transport = class {
718
970
  const response = await this.fetchImpl(url.toString(), request);
719
971
  const responseHeaders = headersToObject(response.headers);
720
972
  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
- }
973
+ if (!response.ok)
974
+ return problemResult(response, text, responseHeaders);
744
975
  return {
745
- data: toCamelCase(parsed),
976
+ data: toCamelCase(parseJson(text)),
746
977
  error: null,
747
978
  headers: responseHeaders
748
979
  };
@@ -758,6 +989,29 @@ var Transport = class {
758
989
  }
759
990
  }
760
991
  };
992
+ function problemResult(response, text, responseHeaders) {
993
+ const parsed = parseJson(text);
994
+ const body = parsed && typeof parsed === "object" ? parsed : {};
995
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
996
+ const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
997
+ const currentRevision = numberValue(body.current_revision);
998
+ const type = stringValue(body.type);
999
+ const title = stringValue(body.title);
1000
+ const instance = stringValue(body.instance);
1001
+ return createErrorResult(code, message, response.status, {
1002
+ body,
1003
+ ...type !== null ? { type } : {},
1004
+ ...title !== null ? { title } : {},
1005
+ ...instance !== null ? { instance } : {},
1006
+ detail: stringValue(body.detail),
1007
+ param: stringValue(body.param),
1008
+ ...currentRevision !== void 0 ? { currentRevision } : {},
1009
+ requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
1010
+ suggestion: stringValue(body.suggestion),
1011
+ retryable: booleanValue(body.retryable),
1012
+ headers: responseHeaders
1013
+ });
1014
+ }
761
1015
  function headersToObject(headers) {
762
1016
  const result = {};
763
1017
  headers.forEach((value, key) => {
@@ -790,6 +1044,7 @@ var DropthisEdge = class {
790
1044
  accountResource;
791
1045
  apiKeysResource;
792
1046
  deploymentsResource;
1047
+ domainsResource;
793
1048
  constructor(options = {}) {
794
1049
  this.transport = new Transport(options);
795
1050
  }
@@ -813,6 +1068,11 @@ var DropthisEdge = class {
813
1068
  this.deploymentsResource = new DeploymentsResource(this.transport);
814
1069
  return this.deploymentsResource;
815
1070
  }
1071
+ get domains() {
1072
+ if (!this.domainsResource)
1073
+ this.domainsResource = new DomainsResource(this.transport);
1074
+ return this.domainsResource;
1075
+ }
816
1076
  };
817
1077
  export {
818
1078
  DropthisEdge