@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.
@@ -373,10 +373,49 @@ async function resolveInMemory(input, options = {}) {
373
373
  });
374
374
  return buildStagedRequest(files, options, input.entry);
375
375
  }
376
+ var UPLOAD_CONCURRENCY = 5;
376
377
  function errorResult(result) {
377
378
  if (!result.error) throw new Error("Expected an error result");
378
379
  return { data: null, error: result.error, headers: result.headers };
379
380
  }
381
+ async function uploadStagedFiles(transport, jobs) {
382
+ let nextIndex = 0;
383
+ let failed = false;
384
+ const failures = /* @__PURE__ */ new Map();
385
+ async function worker() {
386
+ while (!failed) {
387
+ const index = nextIndex;
388
+ nextIndex += 1;
389
+ if (index >= jobs.length) return;
390
+ const job = jobs[index];
391
+ if (!job) return;
392
+ const put = await transport.putSignedUrl(
393
+ job.target.upload.url,
394
+ await job.file.getBody(),
395
+ job.target.upload.headers
396
+ );
397
+ if (put.error) {
398
+ failed = true;
399
+ failures.set(index, {
400
+ data: null,
401
+ error: {
402
+ ...put.error,
403
+ message: `Upload failed for ${job.file.path}: ${put.error.message}`
404
+ },
405
+ headers: put.headers
406
+ });
407
+ }
408
+ }
409
+ }
410
+ const workers = Array.from(
411
+ { length: Math.min(UPLOAD_CONCURRENCY, jobs.length) },
412
+ () => worker()
413
+ );
414
+ await Promise.all(workers);
415
+ if (failures.size === 0) return null;
416
+ const firstIndex = Math.min(...failures.keys());
417
+ return failures.get(firstIndex) ?? null;
418
+ }
380
419
  async function publishStaged(transport, prepared, finalPath, options = {}) {
381
420
  const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
382
421
  const createResult = await transport.request(
@@ -388,6 +427,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
388
427
  }
389
428
  );
390
429
  if (createResult.error) return errorResult(createResult);
430
+ const jobs = [];
391
431
  for (const file of prepared.files) {
392
432
  const target = createResult.data.files.find((f) => f.path === file.path);
393
433
  if (!target) {
@@ -400,22 +440,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
400
440
  if (target.upload.strategy !== "single_put") {
401
441
  return createErrorResult(
402
442
  "unsupported_upload_strategy",
403
- `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
443
+ `Unexpected upload strategy "${target.upload.strategy}" for ${file.path}. The dropthis upload contract is one signed PUT per file; update @dropthis/node.`,
404
444
  null
405
445
  );
406
446
  }
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);
447
+ jobs.push({ file, target });
413
448
  }
449
+ const uploadFailure = await uploadStagedFiles(transport, jobs);
450
+ if (uploadFailure) return uploadFailure;
414
451
  const completeResult = await transport.request(
415
452
  "POST",
416
453
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
417
454
  {
418
- body: { files: {} },
419
455
  idempotencyKey: `${baseKey}:complete-upload`
420
456
  }
421
457
  );
@@ -626,6 +662,7 @@ var ApiKeysResource = class {
626
662
  create(input) {
627
663
  return this.transport.request("POST", "/api-keys", { body: input });
628
664
  }
665
+ /** Revoke an API key. 204 No Content — data is null on success. */
629
666
  delete(keyId) {
630
667
  return this.transport.request(
631
668
  "DELETE",
@@ -652,6 +689,7 @@ var AuthResource = class {
652
689
  authenticated: false
653
690
  });
654
691
  }
692
+ /** Revoke the current `at_` session token. 204 No Content — data is null on success. */
655
693
  logout() {
656
694
  return this.transport.request("DELETE", "/auth/session");
657
695
  }
@@ -713,12 +751,29 @@ var CursorPage = class {
713
751
 
714
752
  // src/resources/drops.ts
715
753
  var DropsResource = class {
716
- constructor(transport, resolve) {
754
+ constructor(transport, resolveInput2) {
717
755
  this.transport = transport;
718
- this.resolve = resolve;
756
+ this.resolveInput = resolveInput2;
719
757
  }
720
758
  transport;
721
- resolve;
759
+ resolveInput;
760
+ parentHosts;
761
+ /**
762
+ * Hostnames the SDK can attribute to dropthis for slug parsing: the canonical
763
+ * viewer domain plus the configured baseUrl's host (covers staging/self-hosted
764
+ * setups where drops are served under the API's own domain).
765
+ */
766
+ allowedParentHosts() {
767
+ if (!this.parentHosts) {
768
+ const hosts = /* @__PURE__ */ new Set([CANONICAL_VIEWER_HOST]);
769
+ try {
770
+ hosts.add(new URL(this.transport.baseUrl).hostname);
771
+ } catch {
772
+ }
773
+ this.parentHosts = hosts;
774
+ }
775
+ return this.parentHosts;
776
+ }
722
777
  /**
723
778
  * Publish content to a NEW permanent public URL; returns the created drop (with its `drop_…` id).
724
779
  * Use to publish / share / post / put online / make public a report, dashboard, site, or file.
@@ -729,7 +784,7 @@ var DropsResource = class {
729
784
  async publish(input, options = {}) {
730
785
  let prepared;
731
786
  try {
732
- prepared = await this.resolve(input, options);
787
+ prepared = await this.resolveInput(input, options);
733
788
  } catch (e) {
734
789
  return toErrorResult(e);
735
790
  }
@@ -746,7 +801,7 @@ var DropsResource = class {
746
801
  const opts = updateContentOptions(options);
747
802
  let prepared;
748
803
  try {
749
- prepared = await this.resolve(input, opts);
804
+ prepared = await this.resolveInput(input, opts);
750
805
  } catch (e) {
751
806
  return toErrorResult(e);
752
807
  }
@@ -774,6 +829,55 @@ var DropsResource = class {
774
829
  `/drops/${encodeURIComponent(dropId)}`
775
830
  );
776
831
  }
832
+ /**
833
+ * Resolve a drop URL (or bare slug) back to the drop — the way to recover a lost
834
+ * `drop_…` id. The slug is parsed client-side from the first hostname label of a
835
+ * dropthis-attributable hostname (`https://<slug>.dropthis.app/…`, or `<slug>.` +
836
+ * the configured baseUrl's host), then matched owner-scoped via GET /drops?slug=.
837
+ * Returns the drop, or `data: null` when no drop of yours has that slug.
838
+ * Custom-domain URLs are rejected client-side with `invalid_drop_url` — they are
839
+ * not resolvable yet.
840
+ */
841
+ async resolve(urlOrSlug) {
842
+ const slug = parseSlug(urlOrSlug, this.allowedParentHosts());
843
+ if (slug === null) {
844
+ return createErrorResult(
845
+ "invalid_drop_url",
846
+ `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.`,
847
+ null
848
+ );
849
+ }
850
+ const result = await this.transport.request("GET", "/drops", { params: { slug } });
851
+ if (result.error)
852
+ return { data: null, error: result.error, headers: result.headers };
853
+ return {
854
+ data: result.data.drops[0] ?? null,
855
+ error: null,
856
+ headers: result.headers
857
+ };
858
+ }
859
+ async getContent(dropId, options = {}) {
860
+ const base = options.deploymentId !== void 0 ? `/drops/${encodeURIComponent(dropId)}/deployments/${encodeURIComponent(options.deploymentId)}/content` : `/drops/${encodeURIComponent(dropId)}/content`;
861
+ if (options.path === void 0)
862
+ return this.transport.request("GET", base);
863
+ const path = options.path;
864
+ const result = await this.transport.requestBytes(base, {
865
+ params: { path }
866
+ });
867
+ if (result.error)
868
+ return { data: null, error: result.error, headers: result.headers };
869
+ const { bytes, contentType } = result.data;
870
+ return {
871
+ data: {
872
+ path,
873
+ contentType,
874
+ bytes,
875
+ text: () => new TextDecoder().decode(bytes)
876
+ },
877
+ error: null,
878
+ headers: result.headers
879
+ };
880
+ }
777
881
  /**
778
882
  * Change an EXISTING drop's settings — title, visibility, password, noindex, expiry,
779
883
  * metadata — by its `drop_…` id. Does not touch content; replace that with {@link updateContent}.
@@ -801,6 +905,25 @@ var DropsResource = class {
801
905
  );
802
906
  }
803
907
  };
908
+ var CANONICAL_VIEWER_HOST = "dropthis.app";
909
+ function parseSlug(input, allowedParents) {
910
+ const value = input.trim();
911
+ if (!value) return null;
912
+ if (!/[./]/.test(value)) return value;
913
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
914
+ let hostname;
915
+ try {
916
+ hostname = new URL(withScheme).hostname;
917
+ } catch {
918
+ return null;
919
+ }
920
+ hostname = hostname.replace(/\.$/, "");
921
+ const dot = hostname.indexOf(".");
922
+ if (dot <= 0) return null;
923
+ const slug = hostname.slice(0, dot);
924
+ const parent = hostname.slice(dot + 1);
925
+ return allowedParents.has(parent) ? slug : null;
926
+ }
804
927
  var SETTINGS = [
805
928
  "title",
806
929
  "visibility",
@@ -836,8 +959,12 @@ var UploadsResource = class {
836
959
  `/uploads/${encodeURIComponent(uploadId)}`
837
960
  );
838
961
  }
839
- complete(uploadId, body, options = {}) {
840
- const requestOptions = { body };
962
+ /**
963
+ * Verify staged objects and mark the session complete. Takes no request body
964
+ * the server verifies the uploaded bytes against the staged manifest.
965
+ */
966
+ complete(uploadId, options = {}) {
967
+ const requestOptions = {};
841
968
  if (options.idempotencyKey)
842
969
  requestOptions.idempotencyKey = options.idempotencyKey;
843
970
  return this.transport.request(
@@ -889,7 +1016,7 @@ function toSnakeCase(value) {
889
1016
 
890
1017
  // src/transport.ts
891
1018
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
892
- var SDK_VERSION = "0.8.0";
1019
+ var SDK_VERSION = "0.10.0";
893
1020
  var Transport = class {
894
1021
  apiKey;
895
1022
  baseUrl;
@@ -949,6 +1076,63 @@ var Transport = class {
949
1076
  clearTimeout(timeout);
950
1077
  }
951
1078
  }
1079
+ /**
1080
+ * Authenticated GET that returns the raw response bytes untouched (no JSON parsing,
1081
+ * no case conversion). Error responses are still parsed as problem+json. Used for
1082
+ * content read-back (`drops.getContent` with a file path).
1083
+ */
1084
+ async requestBytes(path, options = {}) {
1085
+ if (!this.apiKey) {
1086
+ return createErrorResult(
1087
+ "missing_api_key",
1088
+ "No API key provided. Pass apiKey or set DROPTHIS_API_KEY.",
1089
+ null
1090
+ );
1091
+ }
1092
+ const headers = {
1093
+ "user-agent": `dropthis-node/${SDK_VERSION}`,
1094
+ authorization: `Bearer ${this.apiKey}`
1095
+ };
1096
+ const url = new URL(`${this.baseUrl}${path}`);
1097
+ for (const [key, value] of Object.entries(options.params ?? {})) {
1098
+ if (value !== void 0 && value !== null)
1099
+ url.searchParams.set(key, String(value));
1100
+ }
1101
+ const controller = new AbortController();
1102
+ const timeout = setTimeout(
1103
+ () => controller.abort(),
1104
+ options.timeoutMs ?? this.timeoutMs
1105
+ );
1106
+ try {
1107
+ const response = await this.fetchImpl(url.toString(), {
1108
+ method: "GET",
1109
+ headers,
1110
+ signal: controller.signal
1111
+ });
1112
+ const responseHeaders = headersToObject(response.headers);
1113
+ if (!response.ok) {
1114
+ const text = await response.text();
1115
+ return problemResult(response, text, responseHeaders);
1116
+ }
1117
+ return {
1118
+ data: {
1119
+ bytes: new Uint8Array(await response.arrayBuffer()),
1120
+ contentType: response.headers.get("content-type") ?? responseHeaders["content-type"] ?? null
1121
+ },
1122
+ error: null,
1123
+ headers: responseHeaders
1124
+ };
1125
+ } catch (error) {
1126
+ const message = error instanceof Error && error.name === "AbortError" ? "Request timed out" : error instanceof Error ? error.message : "Network error";
1127
+ return createErrorResult(
1128
+ error instanceof Error && error.name === "AbortError" ? "timeout" : "network_error",
1129
+ message,
1130
+ null
1131
+ );
1132
+ } finally {
1133
+ clearTimeout(timeout);
1134
+ }
1135
+ }
952
1136
  async request(method, path, options = {}) {
953
1137
  const authenticated = options.authenticated ?? true;
954
1138
  if (authenticated && !this.apiKey) {
@@ -995,31 +1179,10 @@ var Transport = class {
995
1179
  const response = await this.fetchImpl(url.toString(), request);
996
1180
  const responseHeaders = headersToObject(response.headers);
997
1181
  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
- }
1182
+ if (!response.ok)
1183
+ return problemResult(response, text, responseHeaders);
1021
1184
  return {
1022
- data: toCamelCase(parsed),
1185
+ data: toCamelCase(parseJson(text)),
1023
1186
  error: null,
1024
1187
  headers: responseHeaders
1025
1188
  };
@@ -1035,6 +1198,29 @@ var Transport = class {
1035
1198
  }
1036
1199
  }
1037
1200
  };
1201
+ function problemResult(response, text, responseHeaders) {
1202
+ const parsed = parseJson(text);
1203
+ const body = parsed && typeof parsed === "object" ? parsed : {};
1204
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
1205
+ const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
1206
+ const currentRevision = numberValue(body.current_revision);
1207
+ const type = stringValue(body.type);
1208
+ const title = stringValue(body.title);
1209
+ const instance = stringValue(body.instance);
1210
+ return createErrorResult(code, message, response.status, {
1211
+ body,
1212
+ ...type !== null ? { type } : {},
1213
+ ...title !== null ? { title } : {},
1214
+ ...instance !== null ? { instance } : {},
1215
+ detail: stringValue(body.detail),
1216
+ param: stringValue(body.param),
1217
+ ...currentRevision !== void 0 ? { currentRevision } : {},
1218
+ requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
1219
+ suggestion: stringValue(body.suggestion),
1220
+ retryable: booleanValue(body.retryable),
1221
+ headers: responseHeaders
1222
+ });
1223
+ }
1038
1224
  function headersToObject(headers) {
1039
1225
  const result = {};
1040
1226
  headers.forEach((value, key) => {