@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.
@@ -203,6 +203,8 @@ function optionsBody(options) {
203
203
  if (options.expiresAt !== void 0) {
204
204
  body.expiresAt = options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt;
205
205
  }
206
+ if (options.domain !== void 0) body.domain = options.domain;
207
+ if (options.slug !== void 0) body.slug = options.slug;
206
208
  return body;
207
209
  }
208
210
  function updateContentOptions(options) {
@@ -331,10 +333,49 @@ async function resolveInMemory(input, options = {}) {
331
333
  });
332
334
  return buildStagedRequest(files, options, input.entry);
333
335
  }
336
+ var UPLOAD_CONCURRENCY = 5;
334
337
  function errorResult(result) {
335
338
  if (!result.error) throw new Error("Expected an error result");
336
339
  return { data: null, error: result.error, headers: result.headers };
337
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
+ }
338
379
  async function publishStaged(transport, prepared, finalPath, options = {}) {
339
380
  const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
340
381
  const createResult = await transport.request(
@@ -346,6 +387,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
346
387
  }
347
388
  );
348
389
  if (createResult.error) return errorResult(createResult);
390
+ const jobs = [];
349
391
  for (const file of prepared.files) {
350
392
  const target = createResult.data.files.find((f) => f.path === file.path);
351
393
  if (!target) {
@@ -358,22 +400,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
358
400
  if (target.upload.strategy !== "single_put") {
359
401
  return createErrorResult(
360
402
  "unsupported_upload_strategy",
361
- `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.`,
362
404
  null
363
405
  );
364
406
  }
365
- const put = await transport.putSignedUrl(
366
- target.upload.url,
367
- await file.getBody(),
368
- target.upload.headers
369
- );
370
- if (put.error) return errorResult(put);
407
+ jobs.push({ file, target });
371
408
  }
409
+ const uploadFailure = await uploadStagedFiles(transport, jobs);
410
+ if (uploadFailure) return uploadFailure;
372
411
  const completeResult = await transport.request(
373
412
  "POST",
374
413
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
375
414
  {
376
- body: { files: {} },
377
415
  idempotencyKey: `${baseKey}:complete-upload`
378
416
  }
379
417
  );
@@ -584,6 +622,7 @@ var ApiKeysResource = class {
584
622
  create(input) {
585
623
  return this.transport.request("POST", "/api-keys", { body: input });
586
624
  }
625
+ /** Revoke an API key. 204 No Content — data is null on success. */
587
626
  delete(keyId) {
588
627
  return this.transport.request(
589
628
  "DELETE",
@@ -610,6 +649,7 @@ var AuthResource = class {
610
649
  authenticated: false
611
650
  });
612
651
  }
652
+ /** Revoke the current `at_` session token. 204 No Content — data is null on success. */
613
653
  logout() {
614
654
  return this.transport.request("DELETE", "/auth/session");
615
655
  }
@@ -636,6 +676,67 @@ var DeploymentsResource = class {
636
676
  }
637
677
  };
638
678
 
679
+ // src/resources/domains.ts
680
+ var DomainsResource = class {
681
+ constructor(transport) {
682
+ this.transport = transport;
683
+ }
684
+ transport;
685
+ /**
686
+ * Connect a custom domain to the account. Returns the domain in `pending_dns` status with
687
+ * DNS instructions. Idempotent on (account, hostname) — re-connecting an already-connected
688
+ * domain returns the existing row. POST /domains.
689
+ */
690
+ connect(input) {
691
+ return this.transport.request("POST", "/domains", { body: input });
692
+ }
693
+ /** List all custom domains connected to this account. GET /domains. */
694
+ list() {
695
+ return this.transport.request("GET", "/domains");
696
+ }
697
+ /** Get a domain by its stable id or hostname. GET /domains/{id_or_hostname}. */
698
+ get(idOrHostname) {
699
+ return this.transport.request(
700
+ "GET",
701
+ `/domains/${encodeURIComponent(idOrHostname)}`
702
+ );
703
+ }
704
+ /**
705
+ * Trigger a DNS + Cloudflare verification check. Returns the domain with updated status and
706
+ * per-record diagnostics. If DNS is still propagating, `dns[].retryAfter` tells you when to
707
+ * re-call. POST /domains/{id_or_hostname}/verify.
708
+ */
709
+ verify(idOrHostname) {
710
+ return this.transport.request(
711
+ "POST",
712
+ `/domains/${encodeURIComponent(idOrHostname)}/verify`
713
+ );
714
+ }
715
+ /**
716
+ * Update a domain's `dropId` (dedicated mode: repoint to a different drop) or `default`
717
+ * flag (path mode only: set/clear the account's default publish domain). Mode is immutable
718
+ * — delete and reconnect to change it. PATCH /domains/{id_or_hostname}.
719
+ */
720
+ update(idOrHostname, input) {
721
+ return this.transport.request(
722
+ "PATCH",
723
+ `/domains/${encodeURIComponent(idOrHostname)}`,
724
+ { body: input }
725
+ );
726
+ }
727
+ /**
728
+ * Delete a custom domain and remove all its routes. The response includes a dangling-CNAME
729
+ * warning — remove the DNS record after deleting so another account cannot re-claim the
730
+ * hostname. DELETE /domains/{id_or_hostname}.
731
+ */
732
+ delete(idOrHostname) {
733
+ return this.transport.request(
734
+ "DELETE",
735
+ `/domains/${encodeURIComponent(idOrHostname)}`
736
+ );
737
+ }
738
+ };
739
+
639
740
  // src/pagination.ts
640
741
  var CursorPage = class {
641
742
  object = "list";
@@ -671,12 +772,29 @@ var CursorPage = class {
671
772
 
672
773
  // src/resources/drops.ts
673
774
  var DropsResource = class {
674
- constructor(transport, resolve) {
775
+ constructor(transport, resolveInput2) {
675
776
  this.transport = transport;
676
- this.resolve = resolve;
777
+ this.resolveInput = resolveInput2;
677
778
  }
678
779
  transport;
679
- resolve;
780
+ resolveInput;
781
+ parentHosts;
782
+ /**
783
+ * Hostnames the SDK can attribute to dropthis for slug parsing: the canonical
784
+ * viewer domain plus the configured baseUrl's host (covers staging/self-hosted
785
+ * setups where drops are served under the API's own domain).
786
+ */
787
+ allowedParentHosts() {
788
+ if (!this.parentHosts) {
789
+ const hosts = /* @__PURE__ */ new Set([CANONICAL_VIEWER_HOST]);
790
+ try {
791
+ hosts.add(new URL(this.transport.baseUrl).hostname);
792
+ } catch {
793
+ }
794
+ this.parentHosts = hosts;
795
+ }
796
+ return this.parentHosts;
797
+ }
680
798
  /**
681
799
  * Publish content to a NEW permanent public URL; returns the created drop (with its `drop_…` id).
682
800
  * Use to publish / share / post / put online / make public a report, dashboard, site, or file.
@@ -687,7 +805,7 @@ var DropsResource = class {
687
805
  async publish(input, options = {}) {
688
806
  let prepared;
689
807
  try {
690
- prepared = await this.resolve(input, options);
808
+ prepared = await this.resolveInput(input, options);
691
809
  } catch (e) {
692
810
  return toErrorResult(e);
693
811
  }
@@ -704,7 +822,7 @@ var DropsResource = class {
704
822
  const opts = updateContentOptions(options);
705
823
  let prepared;
706
824
  try {
707
- prepared = await this.resolve(input, opts);
825
+ prepared = await this.resolveInput(input, opts);
708
826
  } catch (e) {
709
827
  return toErrorResult(e);
710
828
  }
@@ -732,10 +850,67 @@ var DropsResource = class {
732
850
  `/drops/${encodeURIComponent(dropId)}`
733
851
  );
734
852
  }
853
+ /**
854
+ * Resolve a drop URL (or bare slug) back to the drop — the way to recover a lost
855
+ * `drop_…` id. The slug is parsed client-side from the first hostname label of a
856
+ * dropthis-attributable hostname (`https://<slug>.dropthis.app/…`, or `<slug>.` +
857
+ * the configured baseUrl's host), then matched owner-scoped via GET /drops?slug=.
858
+ * Returns the drop, or `data: null` when no drop of yours has that slug.
859
+ * Custom-domain URLs are rejected client-side with `invalid_drop_url` — they are
860
+ * not resolvable yet.
861
+ */
862
+ async resolve(urlOrSlug) {
863
+ const slug = parseSlug(urlOrSlug, this.allowedParentHosts());
864
+ if (slug === null) {
865
+ return createErrorResult(
866
+ "invalid_drop_url",
867
+ `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.`,
868
+ null
869
+ );
870
+ }
871
+ const result = await this.transport.request("GET", "/drops", { params: { slug } });
872
+ if (result.error)
873
+ return { data: null, error: result.error, headers: result.headers };
874
+ return {
875
+ data: result.data.drops[0] ?? null,
876
+ error: null,
877
+ headers: result.headers
878
+ };
879
+ }
880
+ async getContent(dropId, options = {}) {
881
+ const base = options.deploymentId !== void 0 ? `/drops/${encodeURIComponent(dropId)}/deployments/${encodeURIComponent(options.deploymentId)}/content` : `/drops/${encodeURIComponent(dropId)}/content`;
882
+ if (options.path === void 0)
883
+ return this.transport.request("GET", base);
884
+ const path = options.path;
885
+ const result = await this.transport.requestBytes(base, {
886
+ params: { path }
887
+ });
888
+ if (result.error)
889
+ return { data: null, error: result.error, headers: result.headers };
890
+ const { bytes, contentType } = result.data;
891
+ return {
892
+ data: {
893
+ path,
894
+ contentType,
895
+ bytes,
896
+ text: () => new TextDecoder().decode(bytes)
897
+ },
898
+ error: null,
899
+ headers: result.headers
900
+ };
901
+ }
735
902
  /**
736
903
  * Change an EXISTING drop's settings — title, visibility, password, noindex, expiry,
737
- * metadata — by its `drop_…` id. Does not touch content; replace that with {@link updateContent}.
738
- * Idempotent. PATCH /drops/{id}.
904
+ * metadata, domain, or slug — by its `drop_…` id. Does not touch content; replace that
905
+ * with {@link updateContent}. Idempotent. PATCH /drops/{id}.
906
+ *
907
+ * **`domain`** — move the drop to a different custom domain (must be live). Pass `null` to
908
+ * move the drop back to the shared pool (unmount from its current domain).
909
+ *
910
+ * **`slug`** — rename the vanity slug on a path-mode custom domain. Only valid when the drop
911
+ * lives on a path-mode domain. Unlike {@link publish} (which auto-suffixes taken slugs),
912
+ * `updateSettings` returns 409 on a slug conflict and never auto-suffixes — your code must
913
+ * catch 409 and retry with a different slug. Passing `slug` on the shared pool returns 422.
739
914
  */
740
915
  updateSettings(dropId, options = {}) {
741
916
  const requestOptions = {
@@ -759,12 +934,33 @@ var DropsResource = class {
759
934
  );
760
935
  }
761
936
  };
937
+ var CANONICAL_VIEWER_HOST = "dropthis.app";
938
+ function parseSlug(input, allowedParents) {
939
+ const value = input.trim();
940
+ if (!value) return null;
941
+ if (!/[./]/.test(value)) return value;
942
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
943
+ let hostname;
944
+ try {
945
+ hostname = new URL(withScheme).hostname;
946
+ } catch {
947
+ return null;
948
+ }
949
+ hostname = hostname.replace(/\.$/, "");
950
+ const dot = hostname.indexOf(".");
951
+ if (dot <= 0) return null;
952
+ const slug = hostname.slice(0, dot);
953
+ const parent = hostname.slice(dot + 1);
954
+ return allowedParents.has(parent) ? slug : null;
955
+ }
762
956
  var SETTINGS = [
763
957
  "title",
764
958
  "visibility",
765
959
  "password",
766
960
  "noindex",
767
- "expiresAt"
961
+ "expiresAt",
962
+ "domain",
963
+ "slug"
768
964
  ];
769
965
  function updateBody(options) {
770
966
  const out = {};
@@ -794,8 +990,12 @@ var UploadsResource = class {
794
990
  `/uploads/${encodeURIComponent(uploadId)}`
795
991
  );
796
992
  }
797
- complete(uploadId, body, options = {}) {
798
- const requestOptions = { body };
993
+ /**
994
+ * Verify staged objects and mark the session complete. Takes no request body
995
+ * the server verifies the uploaded bytes against the staged manifest.
996
+ */
997
+ complete(uploadId, options = {}) {
998
+ const requestOptions = {};
799
999
  if (options.idempotencyKey)
800
1000
  requestOptions.idempotencyKey = options.idempotencyKey;
801
1001
  return this.transport.request(
@@ -847,7 +1047,7 @@ function toSnakeCase(value) {
847
1047
 
848
1048
  // src/transport.ts
849
1049
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
850
- var SDK_VERSION = "0.8.0";
1050
+ var SDK_VERSION = "0.10.0";
851
1051
  var Transport = class {
852
1052
  apiKey;
853
1053
  baseUrl;
@@ -907,6 +1107,63 @@ var Transport = class {
907
1107
  clearTimeout(timeout);
908
1108
  }
909
1109
  }
1110
+ /**
1111
+ * Authenticated GET that returns the raw response bytes untouched (no JSON parsing,
1112
+ * no case conversion). Error responses are still parsed as problem+json. Used for
1113
+ * content read-back (`drops.getContent` with a file path).
1114
+ */
1115
+ async requestBytes(path, options = {}) {
1116
+ if (!this.apiKey) {
1117
+ return createErrorResult(
1118
+ "missing_api_key",
1119
+ "No API key provided. Pass apiKey or set DROPTHIS_API_KEY.",
1120
+ null
1121
+ );
1122
+ }
1123
+ const headers = {
1124
+ "user-agent": `dropthis-node/${SDK_VERSION}`,
1125
+ authorization: `Bearer ${this.apiKey}`
1126
+ };
1127
+ const url = new URL(`${this.baseUrl}${path}`);
1128
+ for (const [key, value] of Object.entries(options.params ?? {})) {
1129
+ if (value !== void 0 && value !== null)
1130
+ url.searchParams.set(key, String(value));
1131
+ }
1132
+ const controller = new AbortController();
1133
+ const timeout = setTimeout(
1134
+ () => controller.abort(),
1135
+ options.timeoutMs ?? this.timeoutMs
1136
+ );
1137
+ try {
1138
+ const response = await this.fetchImpl(url.toString(), {
1139
+ method: "GET",
1140
+ headers,
1141
+ signal: controller.signal
1142
+ });
1143
+ const responseHeaders = headersToObject(response.headers);
1144
+ if (!response.ok) {
1145
+ const text = await response.text();
1146
+ return problemResult(response, text, responseHeaders);
1147
+ }
1148
+ return {
1149
+ data: {
1150
+ bytes: new Uint8Array(await response.arrayBuffer()),
1151
+ contentType: response.headers.get("content-type") ?? responseHeaders["content-type"] ?? null
1152
+ },
1153
+ error: null,
1154
+ headers: responseHeaders
1155
+ };
1156
+ } catch (error) {
1157
+ const message = error instanceof Error && error.name === "AbortError" ? "Request timed out" : error instanceof Error ? error.message : "Network error";
1158
+ return createErrorResult(
1159
+ error instanceof Error && error.name === "AbortError" ? "timeout" : "network_error",
1160
+ message,
1161
+ null
1162
+ );
1163
+ } finally {
1164
+ clearTimeout(timeout);
1165
+ }
1166
+ }
910
1167
  async request(method, path, options = {}) {
911
1168
  const authenticated = options.authenticated ?? true;
912
1169
  if (authenticated && !this.apiKey) {
@@ -953,31 +1210,10 @@ var Transport = class {
953
1210
  const response = await this.fetchImpl(url.toString(), request);
954
1211
  const responseHeaders = headersToObject(response.headers);
955
1212
  const text = await response.text();
956
- const parsed = parseJson(text);
957
- if (!response.ok) {
958
- const body = parsed && typeof parsed === "object" ? parsed : {};
959
- const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
960
- const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
961
- const currentRevision = numberValue(body.current_revision);
962
- const type = stringValue(body.type);
963
- const title = stringValue(body.title);
964
- const instance = stringValue(body.instance);
965
- return createErrorResult(code, message, response.status, {
966
- body,
967
- ...type !== null ? { type } : {},
968
- ...title !== null ? { title } : {},
969
- ...instance !== null ? { instance } : {},
970
- detail: stringValue(body.detail),
971
- param: stringValue(body.param),
972
- ...currentRevision !== void 0 ? { currentRevision } : {},
973
- requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
974
- suggestion: stringValue(body.suggestion),
975
- retryable: booleanValue(body.retryable),
976
- headers: responseHeaders
977
- });
978
- }
1213
+ if (!response.ok)
1214
+ return problemResult(response, text, responseHeaders);
979
1215
  return {
980
- data: toCamelCase(parsed),
1216
+ data: toCamelCase(parseJson(text)),
981
1217
  error: null,
982
1218
  headers: responseHeaders
983
1219
  };
@@ -993,6 +1229,29 @@ var Transport = class {
993
1229
  }
994
1230
  }
995
1231
  };
1232
+ function problemResult(response, text, responseHeaders) {
1233
+ const parsed = parseJson(text);
1234
+ const body = parsed && typeof parsed === "object" ? parsed : {};
1235
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
1236
+ const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
1237
+ const currentRevision = numberValue(body.current_revision);
1238
+ const type = stringValue(body.type);
1239
+ const title = stringValue(body.title);
1240
+ const instance = stringValue(body.instance);
1241
+ return createErrorResult(code, message, response.status, {
1242
+ body,
1243
+ ...type !== null ? { type } : {},
1244
+ ...title !== null ? { title } : {},
1245
+ ...instance !== null ? { instance } : {},
1246
+ detail: stringValue(body.detail),
1247
+ param: stringValue(body.param),
1248
+ ...currentRevision !== void 0 ? { currentRevision } : {},
1249
+ requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
1250
+ suggestion: stringValue(body.suggestion),
1251
+ retryable: booleanValue(body.retryable),
1252
+ headers: responseHeaders
1253
+ });
1254
+ }
996
1255
  function headersToObject(headers) {
997
1256
  const result = {};
998
1257
  headers.forEach((value, key) => {
@@ -1027,6 +1286,7 @@ var Dropthis = class {
1027
1286
  dropsResource;
1028
1287
  deploymentsResource;
1029
1288
  uploadsResource;
1289
+ domainsResource;
1030
1290
  constructor(options = {}) {
1031
1291
  this.transport = new Transport(options);
1032
1292
  }
@@ -1060,6 +1320,11 @@ var Dropthis = class {
1060
1320
  this.uploadsResource = new UploadsResource(this.transport);
1061
1321
  return this.uploadsResource;
1062
1322
  }
1323
+ get domains() {
1324
+ if (!this.domainsResource)
1325
+ this.domainsResource = new DomainsResource(this.transport);
1326
+ return this.domainsResource;
1327
+ }
1063
1328
  async prepare(input, options = {}) {
1064
1329
  return resolveInput(input, options);
1065
1330
  }
@@ -1067,6 +1332,7 @@ var Dropthis = class {
1067
1332
  export {
1068
1333
  CursorPage,
1069
1334
  DeploymentsResource,
1335
+ DomainsResource,
1070
1336
  Dropthis,
1071
1337
  PublishInputError,
1072
1338
  UploadsResource,