@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.
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  CursorPage: () => CursorPage,
34
34
  DeploymentsResource: () => DeploymentsResource,
35
+ DomainsResource: () => DomainsResource,
35
36
  Dropthis: () => Dropthis,
36
37
  PublishInputError: () => PublishInputError,
37
38
  UploadsResource: () => UploadsResource,
@@ -245,6 +246,8 @@ function optionsBody(options) {
245
246
  if (options.expiresAt !== void 0) {
246
247
  body.expiresAt = options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt;
247
248
  }
249
+ if (options.domain !== void 0) body.domain = options.domain;
250
+ if (options.slug !== void 0) body.slug = options.slug;
248
251
  return body;
249
252
  }
250
253
  function updateContentOptions(options) {
@@ -373,10 +376,49 @@ async function resolveInMemory(input, options = {}) {
373
376
  });
374
377
  return buildStagedRequest(files, options, input.entry);
375
378
  }
379
+ var UPLOAD_CONCURRENCY = 5;
376
380
  function errorResult(result) {
377
381
  if (!result.error) throw new Error("Expected an error result");
378
382
  return { data: null, error: result.error, headers: result.headers };
379
383
  }
384
+ async function uploadStagedFiles(transport, jobs) {
385
+ let nextIndex = 0;
386
+ let failed = false;
387
+ const failures = /* @__PURE__ */ new Map();
388
+ async function worker() {
389
+ while (!failed) {
390
+ const index = nextIndex;
391
+ nextIndex += 1;
392
+ if (index >= jobs.length) return;
393
+ const job = jobs[index];
394
+ if (!job) return;
395
+ const put = await transport.putSignedUrl(
396
+ job.target.upload.url,
397
+ await job.file.getBody(),
398
+ job.target.upload.headers
399
+ );
400
+ if (put.error) {
401
+ failed = true;
402
+ failures.set(index, {
403
+ data: null,
404
+ error: {
405
+ ...put.error,
406
+ message: `Upload failed for ${job.file.path}: ${put.error.message}`
407
+ },
408
+ headers: put.headers
409
+ });
410
+ }
411
+ }
412
+ }
413
+ const workers = Array.from(
414
+ { length: Math.min(UPLOAD_CONCURRENCY, jobs.length) },
415
+ () => worker()
416
+ );
417
+ await Promise.all(workers);
418
+ if (failures.size === 0) return null;
419
+ const firstIndex = Math.min(...failures.keys());
420
+ return failures.get(firstIndex) ?? null;
421
+ }
380
422
  async function publishStaged(transport, prepared, finalPath, options = {}) {
381
423
  const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
382
424
  const createResult = await transport.request(
@@ -388,6 +430,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
388
430
  }
389
431
  );
390
432
  if (createResult.error) return errorResult(createResult);
433
+ const jobs = [];
391
434
  for (const file of prepared.files) {
392
435
  const target = createResult.data.files.find((f) => f.path === file.path);
393
436
  if (!target) {
@@ -400,22 +443,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
400
443
  if (target.upload.strategy !== "single_put") {
401
444
  return createErrorResult(
402
445
  "unsupported_upload_strategy",
403
- `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
446
+ `Unexpected upload strategy "${target.upload.strategy}" for ${file.path}. The dropthis upload contract is one signed PUT per file; update @dropthis/node.`,
404
447
  null
405
448
  );
406
449
  }
407
- const put = await transport.putSignedUrl(
408
- target.upload.url,
409
- await file.getBody(),
410
- target.upload.headers
411
- );
412
- if (put.error) return errorResult(put);
450
+ jobs.push({ file, target });
413
451
  }
452
+ const uploadFailure = await uploadStagedFiles(transport, jobs);
453
+ if (uploadFailure) return uploadFailure;
414
454
  const completeResult = await transport.request(
415
455
  "POST",
416
456
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
417
457
  {
418
- body: { files: {} },
419
458
  idempotencyKey: `${baseKey}:complete-upload`
420
459
  }
421
460
  );
@@ -626,6 +665,7 @@ var ApiKeysResource = class {
626
665
  create(input) {
627
666
  return this.transport.request("POST", "/api-keys", { body: input });
628
667
  }
668
+ /** Revoke an API key. 204 No Content — data is null on success. */
629
669
  delete(keyId) {
630
670
  return this.transport.request(
631
671
  "DELETE",
@@ -652,6 +692,7 @@ var AuthResource = class {
652
692
  authenticated: false
653
693
  });
654
694
  }
695
+ /** Revoke the current `at_` session token. 204 No Content — data is null on success. */
655
696
  logout() {
656
697
  return this.transport.request("DELETE", "/auth/session");
657
698
  }
@@ -678,6 +719,67 @@ var DeploymentsResource = class {
678
719
  }
679
720
  };
680
721
 
722
+ // src/resources/domains.ts
723
+ var DomainsResource = class {
724
+ constructor(transport) {
725
+ this.transport = transport;
726
+ }
727
+ transport;
728
+ /**
729
+ * Connect a custom domain to the account. Returns the domain in `pending_dns` status with
730
+ * DNS instructions. Idempotent on (account, hostname) — re-connecting an already-connected
731
+ * domain returns the existing row. POST /domains.
732
+ */
733
+ connect(input) {
734
+ return this.transport.request("POST", "/domains", { body: input });
735
+ }
736
+ /** List all custom domains connected to this account. GET /domains. */
737
+ list() {
738
+ return this.transport.request("GET", "/domains");
739
+ }
740
+ /** Get a domain by its stable id or hostname. GET /domains/{id_or_hostname}. */
741
+ get(idOrHostname) {
742
+ return this.transport.request(
743
+ "GET",
744
+ `/domains/${encodeURIComponent(idOrHostname)}`
745
+ );
746
+ }
747
+ /**
748
+ * Trigger a DNS + Cloudflare verification check. Returns the domain with updated status and
749
+ * per-record diagnostics. If DNS is still propagating, `dns[].retryAfter` tells you when to
750
+ * re-call. POST /domains/{id_or_hostname}/verify.
751
+ */
752
+ verify(idOrHostname) {
753
+ return this.transport.request(
754
+ "POST",
755
+ `/domains/${encodeURIComponent(idOrHostname)}/verify`
756
+ );
757
+ }
758
+ /**
759
+ * Update a domain's `dropId` (dedicated mode: repoint to a different drop) or `default`
760
+ * flag (path mode only: set/clear the account's default publish domain). Mode is immutable
761
+ * — delete and reconnect to change it. PATCH /domains/{id_or_hostname}.
762
+ */
763
+ update(idOrHostname, input) {
764
+ return this.transport.request(
765
+ "PATCH",
766
+ `/domains/${encodeURIComponent(idOrHostname)}`,
767
+ { body: input }
768
+ );
769
+ }
770
+ /**
771
+ * Delete a custom domain and remove all its routes. The response includes a dangling-CNAME
772
+ * warning — remove the DNS record after deleting so another account cannot re-claim the
773
+ * hostname. DELETE /domains/{id_or_hostname}.
774
+ */
775
+ delete(idOrHostname) {
776
+ return this.transport.request(
777
+ "DELETE",
778
+ `/domains/${encodeURIComponent(idOrHostname)}`
779
+ );
780
+ }
781
+ };
782
+
681
783
  // src/pagination.ts
682
784
  var CursorPage = class {
683
785
  object = "list";
@@ -713,12 +815,29 @@ var CursorPage = class {
713
815
 
714
816
  // src/resources/drops.ts
715
817
  var DropsResource = class {
716
- constructor(transport, resolve) {
818
+ constructor(transport, resolveInput2) {
717
819
  this.transport = transport;
718
- this.resolve = resolve;
820
+ this.resolveInput = resolveInput2;
719
821
  }
720
822
  transport;
721
- resolve;
823
+ resolveInput;
824
+ parentHosts;
825
+ /**
826
+ * Hostnames the SDK can attribute to dropthis for slug parsing: the canonical
827
+ * viewer domain plus the configured baseUrl's host (covers staging/self-hosted
828
+ * setups where drops are served under the API's own domain).
829
+ */
830
+ allowedParentHosts() {
831
+ if (!this.parentHosts) {
832
+ const hosts = /* @__PURE__ */ new Set([CANONICAL_VIEWER_HOST]);
833
+ try {
834
+ hosts.add(new URL(this.transport.baseUrl).hostname);
835
+ } catch {
836
+ }
837
+ this.parentHosts = hosts;
838
+ }
839
+ return this.parentHosts;
840
+ }
722
841
  /**
723
842
  * Publish content to a NEW permanent public URL; returns the created drop (with its `drop_…` id).
724
843
  * Use to publish / share / post / put online / make public a report, dashboard, site, or file.
@@ -729,7 +848,7 @@ var DropsResource = class {
729
848
  async publish(input, options = {}) {
730
849
  let prepared;
731
850
  try {
732
- prepared = await this.resolve(input, options);
851
+ prepared = await this.resolveInput(input, options);
733
852
  } catch (e) {
734
853
  return toErrorResult(e);
735
854
  }
@@ -746,7 +865,7 @@ var DropsResource = class {
746
865
  const opts = updateContentOptions(options);
747
866
  let prepared;
748
867
  try {
749
- prepared = await this.resolve(input, opts);
868
+ prepared = await this.resolveInput(input, opts);
750
869
  } catch (e) {
751
870
  return toErrorResult(e);
752
871
  }
@@ -774,10 +893,67 @@ var DropsResource = class {
774
893
  `/drops/${encodeURIComponent(dropId)}`
775
894
  );
776
895
  }
896
+ /**
897
+ * Resolve a drop URL (or bare slug) back to the drop — the way to recover a lost
898
+ * `drop_…` id. The slug is parsed client-side from the first hostname label of a
899
+ * dropthis-attributable hostname (`https://<slug>.dropthis.app/…`, or `<slug>.` +
900
+ * the configured baseUrl's host), then matched owner-scoped via GET /drops?slug=.
901
+ * Returns the drop, or `data: null` when no drop of yours has that slug.
902
+ * Custom-domain URLs are rejected client-side with `invalid_drop_url` — they are
903
+ * not resolvable yet.
904
+ */
905
+ async resolve(urlOrSlug) {
906
+ const slug = parseSlug(urlOrSlug, this.allowedParentHosts());
907
+ if (slug === null) {
908
+ return createErrorResult(
909
+ "invalid_drop_url",
910
+ `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.`,
911
+ null
912
+ );
913
+ }
914
+ const result = await this.transport.request("GET", "/drops", { params: { slug } });
915
+ if (result.error)
916
+ return { data: null, error: result.error, headers: result.headers };
917
+ return {
918
+ data: result.data.drops[0] ?? null,
919
+ error: null,
920
+ headers: result.headers
921
+ };
922
+ }
923
+ async getContent(dropId, options = {}) {
924
+ const base = options.deploymentId !== void 0 ? `/drops/${encodeURIComponent(dropId)}/deployments/${encodeURIComponent(options.deploymentId)}/content` : `/drops/${encodeURIComponent(dropId)}/content`;
925
+ if (options.path === void 0)
926
+ return this.transport.request("GET", base);
927
+ const path = options.path;
928
+ const result = await this.transport.requestBytes(base, {
929
+ params: { path }
930
+ });
931
+ if (result.error)
932
+ return { data: null, error: result.error, headers: result.headers };
933
+ const { bytes, contentType } = result.data;
934
+ return {
935
+ data: {
936
+ path,
937
+ contentType,
938
+ bytes,
939
+ text: () => new TextDecoder().decode(bytes)
940
+ },
941
+ error: null,
942
+ headers: result.headers
943
+ };
944
+ }
777
945
  /**
778
946
  * Change an EXISTING drop's settings — title, visibility, password, noindex, expiry,
779
- * metadata — by its `drop_…` id. Does not touch content; replace that with {@link updateContent}.
780
- * Idempotent. PATCH /drops/{id}.
947
+ * metadata, domain, or slug — by its `drop_…` id. Does not touch content; replace that
948
+ * with {@link updateContent}. Idempotent. PATCH /drops/{id}.
949
+ *
950
+ * **`domain`** — move the drop to a different custom domain (must be live). Pass `null` to
951
+ * move the drop back to the shared pool (unmount from its current domain).
952
+ *
953
+ * **`slug`** — rename the vanity slug on a path-mode custom domain. Only valid when the drop
954
+ * lives on a path-mode domain. Unlike {@link publish} (which auto-suffixes taken slugs),
955
+ * `updateSettings` returns 409 on a slug conflict and never auto-suffixes — your code must
956
+ * catch 409 and retry with a different slug. Passing `slug` on the shared pool returns 422.
781
957
  */
782
958
  updateSettings(dropId, options = {}) {
783
959
  const requestOptions = {
@@ -801,12 +977,33 @@ var DropsResource = class {
801
977
  );
802
978
  }
803
979
  };
980
+ var CANONICAL_VIEWER_HOST = "dropthis.app";
981
+ function parseSlug(input, allowedParents) {
982
+ const value = input.trim();
983
+ if (!value) return null;
984
+ if (!/[./]/.test(value)) return value;
985
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
986
+ let hostname;
987
+ try {
988
+ hostname = new URL(withScheme).hostname;
989
+ } catch {
990
+ return null;
991
+ }
992
+ hostname = hostname.replace(/\.$/, "");
993
+ const dot = hostname.indexOf(".");
994
+ if (dot <= 0) return null;
995
+ const slug = hostname.slice(0, dot);
996
+ const parent = hostname.slice(dot + 1);
997
+ return allowedParents.has(parent) ? slug : null;
998
+ }
804
999
  var SETTINGS = [
805
1000
  "title",
806
1001
  "visibility",
807
1002
  "password",
808
1003
  "noindex",
809
- "expiresAt"
1004
+ "expiresAt",
1005
+ "domain",
1006
+ "slug"
810
1007
  ];
811
1008
  function updateBody(options) {
812
1009
  const out = {};
@@ -836,8 +1033,12 @@ var UploadsResource = class {
836
1033
  `/uploads/${encodeURIComponent(uploadId)}`
837
1034
  );
838
1035
  }
839
- complete(uploadId, body, options = {}) {
840
- const requestOptions = { body };
1036
+ /**
1037
+ * Verify staged objects and mark the session complete. Takes no request body
1038
+ * the server verifies the uploaded bytes against the staged manifest.
1039
+ */
1040
+ complete(uploadId, options = {}) {
1041
+ const requestOptions = {};
841
1042
  if (options.idempotencyKey)
842
1043
  requestOptions.idempotencyKey = options.idempotencyKey;
843
1044
  return this.transport.request(
@@ -889,7 +1090,7 @@ function toSnakeCase(value) {
889
1090
 
890
1091
  // src/transport.ts
891
1092
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
892
- var SDK_VERSION = "0.8.0";
1093
+ var SDK_VERSION = "0.10.0";
893
1094
  var Transport = class {
894
1095
  apiKey;
895
1096
  baseUrl;
@@ -949,6 +1150,63 @@ var Transport = class {
949
1150
  clearTimeout(timeout);
950
1151
  }
951
1152
  }
1153
+ /**
1154
+ * Authenticated GET that returns the raw response bytes untouched (no JSON parsing,
1155
+ * no case conversion). Error responses are still parsed as problem+json. Used for
1156
+ * content read-back (`drops.getContent` with a file path).
1157
+ */
1158
+ async requestBytes(path, options = {}) {
1159
+ if (!this.apiKey) {
1160
+ return createErrorResult(
1161
+ "missing_api_key",
1162
+ "No API key provided. Pass apiKey or set DROPTHIS_API_KEY.",
1163
+ null
1164
+ );
1165
+ }
1166
+ const headers = {
1167
+ "user-agent": `dropthis-node/${SDK_VERSION}`,
1168
+ authorization: `Bearer ${this.apiKey}`
1169
+ };
1170
+ const url = new URL(`${this.baseUrl}${path}`);
1171
+ for (const [key, value] of Object.entries(options.params ?? {})) {
1172
+ if (value !== void 0 && value !== null)
1173
+ url.searchParams.set(key, String(value));
1174
+ }
1175
+ const controller = new AbortController();
1176
+ const timeout = setTimeout(
1177
+ () => controller.abort(),
1178
+ options.timeoutMs ?? this.timeoutMs
1179
+ );
1180
+ try {
1181
+ const response = await this.fetchImpl(url.toString(), {
1182
+ method: "GET",
1183
+ headers,
1184
+ signal: controller.signal
1185
+ });
1186
+ const responseHeaders = headersToObject(response.headers);
1187
+ if (!response.ok) {
1188
+ const text = await response.text();
1189
+ return problemResult(response, text, responseHeaders);
1190
+ }
1191
+ return {
1192
+ data: {
1193
+ bytes: new Uint8Array(await response.arrayBuffer()),
1194
+ contentType: response.headers.get("content-type") ?? responseHeaders["content-type"] ?? null
1195
+ },
1196
+ error: null,
1197
+ headers: responseHeaders
1198
+ };
1199
+ } catch (error) {
1200
+ const message = error instanceof Error && error.name === "AbortError" ? "Request timed out" : error instanceof Error ? error.message : "Network error";
1201
+ return createErrorResult(
1202
+ error instanceof Error && error.name === "AbortError" ? "timeout" : "network_error",
1203
+ message,
1204
+ null
1205
+ );
1206
+ } finally {
1207
+ clearTimeout(timeout);
1208
+ }
1209
+ }
952
1210
  async request(method, path, options = {}) {
953
1211
  const authenticated = options.authenticated ?? true;
954
1212
  if (authenticated && !this.apiKey) {
@@ -995,31 +1253,10 @@ var Transport = class {
995
1253
  const response = await this.fetchImpl(url.toString(), request);
996
1254
  const responseHeaders = headersToObject(response.headers);
997
1255
  const text = await response.text();
998
- const parsed = parseJson(text);
999
- if (!response.ok) {
1000
- const body = parsed && typeof parsed === "object" ? parsed : {};
1001
- const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
1002
- const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
1003
- const currentRevision = numberValue(body.current_revision);
1004
- const type = stringValue(body.type);
1005
- const title = stringValue(body.title);
1006
- const instance = stringValue(body.instance);
1007
- return createErrorResult(code, message, response.status, {
1008
- body,
1009
- ...type !== null ? { type } : {},
1010
- ...title !== null ? { title } : {},
1011
- ...instance !== null ? { instance } : {},
1012
- detail: stringValue(body.detail),
1013
- param: stringValue(body.param),
1014
- ...currentRevision !== void 0 ? { currentRevision } : {},
1015
- requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
1016
- suggestion: stringValue(body.suggestion),
1017
- retryable: booleanValue(body.retryable),
1018
- headers: responseHeaders
1019
- });
1020
- }
1256
+ if (!response.ok)
1257
+ return problemResult(response, text, responseHeaders);
1021
1258
  return {
1022
- data: toCamelCase(parsed),
1259
+ data: toCamelCase(parseJson(text)),
1023
1260
  error: null,
1024
1261
  headers: responseHeaders
1025
1262
  };
@@ -1035,6 +1272,29 @@ var Transport = class {
1035
1272
  }
1036
1273
  }
1037
1274
  };
1275
+ function problemResult(response, text, responseHeaders) {
1276
+ const parsed = parseJson(text);
1277
+ const body = parsed && typeof parsed === "object" ? parsed : {};
1278
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
1279
+ const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
1280
+ const currentRevision = numberValue(body.current_revision);
1281
+ const type = stringValue(body.type);
1282
+ const title = stringValue(body.title);
1283
+ const instance = stringValue(body.instance);
1284
+ return createErrorResult(code, message, response.status, {
1285
+ body,
1286
+ ...type !== null ? { type } : {},
1287
+ ...title !== null ? { title } : {},
1288
+ ...instance !== null ? { instance } : {},
1289
+ detail: stringValue(body.detail),
1290
+ param: stringValue(body.param),
1291
+ ...currentRevision !== void 0 ? { currentRevision } : {},
1292
+ requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
1293
+ suggestion: stringValue(body.suggestion),
1294
+ retryable: booleanValue(body.retryable),
1295
+ headers: responseHeaders
1296
+ });
1297
+ }
1038
1298
  function headersToObject(headers) {
1039
1299
  const result = {};
1040
1300
  headers.forEach((value, key) => {
@@ -1069,6 +1329,7 @@ var Dropthis = class {
1069
1329
  dropsResource;
1070
1330
  deploymentsResource;
1071
1331
  uploadsResource;
1332
+ domainsResource;
1072
1333
  constructor(options = {}) {
1073
1334
  this.transport = new Transport(options);
1074
1335
  }
@@ -1102,6 +1363,11 @@ var Dropthis = class {
1102
1363
  this.uploadsResource = new UploadsResource(this.transport);
1103
1364
  return this.uploadsResource;
1104
1365
  }
1366
+ get domains() {
1367
+ if (!this.domainsResource)
1368
+ this.domainsResource = new DomainsResource(this.transport);
1369
+ return this.domainsResource;
1370
+ }
1105
1371
  async prepare(input, options = {}) {
1106
1372
  return resolveInput(input, options);
1107
1373
  }
@@ -1110,6 +1376,7 @@ var Dropthis = class {
1110
1376
  0 && (module.exports = {
1111
1377
  CursorPage,
1112
1378
  DeploymentsResource,
1379
+ DomainsResource,
1113
1380
  Dropthis,
1114
1381
  PublishInputError,
1115
1382
  UploadsResource,