@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.
@@ -331,10 +331,49 @@ async function resolveInMemory(input, options = {}) {
331
331
  });
332
332
  return buildStagedRequest(files, options, input.entry);
333
333
  }
334
+ var UPLOAD_CONCURRENCY = 5;
334
335
  function errorResult(result) {
335
336
  if (!result.error) throw new Error("Expected an error result");
336
337
  return { data: null, error: result.error, headers: result.headers };
337
338
  }
339
+ async function uploadStagedFiles(transport, jobs) {
340
+ let nextIndex = 0;
341
+ let failed = false;
342
+ const failures = /* @__PURE__ */ new Map();
343
+ async function worker() {
344
+ while (!failed) {
345
+ const index = nextIndex;
346
+ nextIndex += 1;
347
+ if (index >= jobs.length) return;
348
+ const job = jobs[index];
349
+ if (!job) return;
350
+ const put = await transport.putSignedUrl(
351
+ job.target.upload.url,
352
+ await job.file.getBody(),
353
+ job.target.upload.headers
354
+ );
355
+ if (put.error) {
356
+ failed = true;
357
+ failures.set(index, {
358
+ data: null,
359
+ error: {
360
+ ...put.error,
361
+ message: `Upload failed for ${job.file.path}: ${put.error.message}`
362
+ },
363
+ headers: put.headers
364
+ });
365
+ }
366
+ }
367
+ }
368
+ const workers = Array.from(
369
+ { length: Math.min(UPLOAD_CONCURRENCY, jobs.length) },
370
+ () => worker()
371
+ );
372
+ await Promise.all(workers);
373
+ if (failures.size === 0) return null;
374
+ const firstIndex = Math.min(...failures.keys());
375
+ return failures.get(firstIndex) ?? null;
376
+ }
338
377
  async function publishStaged(transport, prepared, finalPath, options = {}) {
339
378
  const baseKey = options.idempotencyKey ?? `sdk-${crypto.randomUUID()}`;
340
379
  const createResult = await transport.request(
@@ -346,6 +385,7 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
346
385
  }
347
386
  );
348
387
  if (createResult.error) return errorResult(createResult);
388
+ const jobs = [];
349
389
  for (const file of prepared.files) {
350
390
  const target = createResult.data.files.find((f) => f.path === file.path);
351
391
  if (!target) {
@@ -358,22 +398,18 @@ async function publishStaged(transport, prepared, finalPath, options = {}) {
358
398
  if (target.upload.strategy !== "single_put") {
359
399
  return createErrorResult(
360
400
  "unsupported_upload_strategy",
361
- `Unsupported upload strategy "${target.upload.strategy}" for ${file.path}. This SDK uploads via single PUT only.`,
401
+ `Unexpected upload strategy "${target.upload.strategy}" for ${file.path}. The dropthis upload contract is one signed PUT per file; update @dropthis/node.`,
362
402
  null
363
403
  );
364
404
  }
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);
405
+ jobs.push({ file, target });
371
406
  }
407
+ const uploadFailure = await uploadStagedFiles(transport, jobs);
408
+ if (uploadFailure) return uploadFailure;
372
409
  const completeResult = await transport.request(
373
410
  "POST",
374
411
  `/uploads/${encodeURIComponent(createResult.data.uploadId)}/complete`,
375
412
  {
376
- body: { files: {} },
377
413
  idempotencyKey: `${baseKey}:complete-upload`
378
414
  }
379
415
  );
@@ -584,6 +620,7 @@ var ApiKeysResource = class {
584
620
  create(input) {
585
621
  return this.transport.request("POST", "/api-keys", { body: input });
586
622
  }
623
+ /** Revoke an API key. 204 No Content — data is null on success. */
587
624
  delete(keyId) {
588
625
  return this.transport.request(
589
626
  "DELETE",
@@ -610,6 +647,7 @@ var AuthResource = class {
610
647
  authenticated: false
611
648
  });
612
649
  }
650
+ /** Revoke the current `at_` session token. 204 No Content — data is null on success. */
613
651
  logout() {
614
652
  return this.transport.request("DELETE", "/auth/session");
615
653
  }
@@ -671,12 +709,29 @@ var CursorPage = class {
671
709
 
672
710
  // src/resources/drops.ts
673
711
  var DropsResource = class {
674
- constructor(transport, resolve) {
712
+ constructor(transport, resolveInput2) {
675
713
  this.transport = transport;
676
- this.resolve = resolve;
714
+ this.resolveInput = resolveInput2;
677
715
  }
678
716
  transport;
679
- resolve;
717
+ resolveInput;
718
+ parentHosts;
719
+ /**
720
+ * Hostnames the SDK can attribute to dropthis for slug parsing: the canonical
721
+ * viewer domain plus the configured baseUrl's host (covers staging/self-hosted
722
+ * setups where drops are served under the API's own domain).
723
+ */
724
+ allowedParentHosts() {
725
+ if (!this.parentHosts) {
726
+ const hosts = /* @__PURE__ */ new Set([CANONICAL_VIEWER_HOST]);
727
+ try {
728
+ hosts.add(new URL(this.transport.baseUrl).hostname);
729
+ } catch {
730
+ }
731
+ this.parentHosts = hosts;
732
+ }
733
+ return this.parentHosts;
734
+ }
680
735
  /**
681
736
  * Publish content to a NEW permanent public URL; returns the created drop (with its `drop_…` id).
682
737
  * Use to publish / share / post / put online / make public a report, dashboard, site, or file.
@@ -687,7 +742,7 @@ var DropsResource = class {
687
742
  async publish(input, options = {}) {
688
743
  let prepared;
689
744
  try {
690
- prepared = await this.resolve(input, options);
745
+ prepared = await this.resolveInput(input, options);
691
746
  } catch (e) {
692
747
  return toErrorResult(e);
693
748
  }
@@ -704,7 +759,7 @@ var DropsResource = class {
704
759
  const opts = updateContentOptions(options);
705
760
  let prepared;
706
761
  try {
707
- prepared = await this.resolve(input, opts);
762
+ prepared = await this.resolveInput(input, opts);
708
763
  } catch (e) {
709
764
  return toErrorResult(e);
710
765
  }
@@ -732,6 +787,55 @@ var DropsResource = class {
732
787
  `/drops/${encodeURIComponent(dropId)}`
733
788
  );
734
789
  }
790
+ /**
791
+ * Resolve a drop URL (or bare slug) back to the drop — the way to recover a lost
792
+ * `drop_…` id. The slug is parsed client-side from the first hostname label of a
793
+ * dropthis-attributable hostname (`https://<slug>.dropthis.app/…`, or `<slug>.` +
794
+ * the configured baseUrl's host), then matched owner-scoped via GET /drops?slug=.
795
+ * Returns the drop, or `data: null` when no drop of yours has that slug.
796
+ * Custom-domain URLs are rejected client-side with `invalid_drop_url` — they are
797
+ * not resolvable yet.
798
+ */
799
+ async resolve(urlOrSlug) {
800
+ const slug = parseSlug(urlOrSlug, this.allowedParentHosts());
801
+ if (slug === null) {
802
+ return createErrorResult(
803
+ "invalid_drop_url",
804
+ `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.`,
805
+ null
806
+ );
807
+ }
808
+ const result = await this.transport.request("GET", "/drops", { params: { slug } });
809
+ if (result.error)
810
+ return { data: null, error: result.error, headers: result.headers };
811
+ return {
812
+ data: result.data.drops[0] ?? null,
813
+ error: null,
814
+ headers: result.headers
815
+ };
816
+ }
817
+ async getContent(dropId, options = {}) {
818
+ const base = options.deploymentId !== void 0 ? `/drops/${encodeURIComponent(dropId)}/deployments/${encodeURIComponent(options.deploymentId)}/content` : `/drops/${encodeURIComponent(dropId)}/content`;
819
+ if (options.path === void 0)
820
+ return this.transport.request("GET", base);
821
+ const path = options.path;
822
+ const result = await this.transport.requestBytes(base, {
823
+ params: { path }
824
+ });
825
+ if (result.error)
826
+ return { data: null, error: result.error, headers: result.headers };
827
+ const { bytes, contentType } = result.data;
828
+ return {
829
+ data: {
830
+ path,
831
+ contentType,
832
+ bytes,
833
+ text: () => new TextDecoder().decode(bytes)
834
+ },
835
+ error: null,
836
+ headers: result.headers
837
+ };
838
+ }
735
839
  /**
736
840
  * Change an EXISTING drop's settings — title, visibility, password, noindex, expiry,
737
841
  * metadata — by its `drop_…` id. Does not touch content; replace that with {@link updateContent}.
@@ -759,6 +863,25 @@ var DropsResource = class {
759
863
  );
760
864
  }
761
865
  };
866
+ var CANONICAL_VIEWER_HOST = "dropthis.app";
867
+ function parseSlug(input, allowedParents) {
868
+ const value = input.trim();
869
+ if (!value) return null;
870
+ if (!/[./]/.test(value)) return value;
871
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
872
+ let hostname;
873
+ try {
874
+ hostname = new URL(withScheme).hostname;
875
+ } catch {
876
+ return null;
877
+ }
878
+ hostname = hostname.replace(/\.$/, "");
879
+ const dot = hostname.indexOf(".");
880
+ if (dot <= 0) return null;
881
+ const slug = hostname.slice(0, dot);
882
+ const parent = hostname.slice(dot + 1);
883
+ return allowedParents.has(parent) ? slug : null;
884
+ }
762
885
  var SETTINGS = [
763
886
  "title",
764
887
  "visibility",
@@ -794,8 +917,12 @@ var UploadsResource = class {
794
917
  `/uploads/${encodeURIComponent(uploadId)}`
795
918
  );
796
919
  }
797
- complete(uploadId, body, options = {}) {
798
- const requestOptions = { body };
920
+ /**
921
+ * Verify staged objects and mark the session complete. Takes no request body
922
+ * the server verifies the uploaded bytes against the staged manifest.
923
+ */
924
+ complete(uploadId, options = {}) {
925
+ const requestOptions = {};
799
926
  if (options.idempotencyKey)
800
927
  requestOptions.idempotencyKey = options.idempotencyKey;
801
928
  return this.transport.request(
@@ -847,7 +974,7 @@ function toSnakeCase(value) {
847
974
 
848
975
  // src/transport.ts
849
976
  var DEFAULT_BASE_URL = "https://api.dropthis.app";
850
- var SDK_VERSION = "0.8.0";
977
+ var SDK_VERSION = "0.10.0";
851
978
  var Transport = class {
852
979
  apiKey;
853
980
  baseUrl;
@@ -907,6 +1034,63 @@ var Transport = class {
907
1034
  clearTimeout(timeout);
908
1035
  }
909
1036
  }
1037
+ /**
1038
+ * Authenticated GET that returns the raw response bytes untouched (no JSON parsing,
1039
+ * no case conversion). Error responses are still parsed as problem+json. Used for
1040
+ * content read-back (`drops.getContent` with a file path).
1041
+ */
1042
+ async requestBytes(path, options = {}) {
1043
+ if (!this.apiKey) {
1044
+ return createErrorResult(
1045
+ "missing_api_key",
1046
+ "No API key provided. Pass apiKey or set DROPTHIS_API_KEY.",
1047
+ null
1048
+ );
1049
+ }
1050
+ const headers = {
1051
+ "user-agent": `dropthis-node/${SDK_VERSION}`,
1052
+ authorization: `Bearer ${this.apiKey}`
1053
+ };
1054
+ const url = new URL(`${this.baseUrl}${path}`);
1055
+ for (const [key, value] of Object.entries(options.params ?? {})) {
1056
+ if (value !== void 0 && value !== null)
1057
+ url.searchParams.set(key, String(value));
1058
+ }
1059
+ const controller = new AbortController();
1060
+ const timeout = setTimeout(
1061
+ () => controller.abort(),
1062
+ options.timeoutMs ?? this.timeoutMs
1063
+ );
1064
+ try {
1065
+ const response = await this.fetchImpl(url.toString(), {
1066
+ method: "GET",
1067
+ headers,
1068
+ signal: controller.signal
1069
+ });
1070
+ const responseHeaders = headersToObject(response.headers);
1071
+ if (!response.ok) {
1072
+ const text = await response.text();
1073
+ return problemResult(response, text, responseHeaders);
1074
+ }
1075
+ return {
1076
+ data: {
1077
+ bytes: new Uint8Array(await response.arrayBuffer()),
1078
+ contentType: response.headers.get("content-type") ?? responseHeaders["content-type"] ?? null
1079
+ },
1080
+ error: null,
1081
+ headers: responseHeaders
1082
+ };
1083
+ } catch (error) {
1084
+ const message = error instanceof Error && error.name === "AbortError" ? "Request timed out" : error instanceof Error ? error.message : "Network error";
1085
+ return createErrorResult(
1086
+ error instanceof Error && error.name === "AbortError" ? "timeout" : "network_error",
1087
+ message,
1088
+ null
1089
+ );
1090
+ } finally {
1091
+ clearTimeout(timeout);
1092
+ }
1093
+ }
910
1094
  async request(method, path, options = {}) {
911
1095
  const authenticated = options.authenticated ?? true;
912
1096
  if (authenticated && !this.apiKey) {
@@ -953,31 +1137,10 @@ var Transport = class {
953
1137
  const response = await this.fetchImpl(url.toString(), request);
954
1138
  const responseHeaders = headersToObject(response.headers);
955
1139
  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
- }
1140
+ if (!response.ok)
1141
+ return problemResult(response, text, responseHeaders);
979
1142
  return {
980
- data: toCamelCase(parsed),
1143
+ data: toCamelCase(parseJson(text)),
981
1144
  error: null,
982
1145
  headers: responseHeaders
983
1146
  };
@@ -993,6 +1156,29 @@ var Transport = class {
993
1156
  }
994
1157
  }
995
1158
  };
1159
+ function problemResult(response, text, responseHeaders) {
1160
+ const parsed = parseJson(text);
1161
+ const body = parsed && typeof parsed === "object" ? parsed : {};
1162
+ const message = stringValue(body.detail) ?? stringValue(body.title) ?? response.statusText;
1163
+ const code = stringValue(body.code) ?? stringValue(body.error_code) ?? `http_${response.status}`;
1164
+ const currentRevision = numberValue(body.current_revision);
1165
+ const type = stringValue(body.type);
1166
+ const title = stringValue(body.title);
1167
+ const instance = stringValue(body.instance);
1168
+ return createErrorResult(code, message, response.status, {
1169
+ body,
1170
+ ...type !== null ? { type } : {},
1171
+ ...title !== null ? { title } : {},
1172
+ ...instance !== null ? { instance } : {},
1173
+ detail: stringValue(body.detail),
1174
+ param: stringValue(body.param),
1175
+ ...currentRevision !== void 0 ? { currentRevision } : {},
1176
+ requestId: stringValue(body.request_id) ?? responseHeaders["x-request-id"] ?? null,
1177
+ suggestion: stringValue(body.suggestion),
1178
+ retryable: booleanValue(body.retryable),
1179
+ headers: responseHeaders
1180
+ });
1181
+ }
996
1182
  function headersToObject(headers) {
997
1183
  const result = {};
998
1184
  headers.forEach((value, key) => {