@clickraft/cli 0.8.6 → 0.8.7

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.
package/dist/index.js CHANGED
@@ -14,6 +14,8 @@ import { request as undiciRequest } from "undici";
14
14
  import { z as z6 } from "zod";
15
15
  import { createHash, randomUUID } from "crypto";
16
16
  import { z as z5 } from "zod";
17
+ import { createHash as createHash2 } from "crypto";
18
+ import { request as undiciRequest2 } from "undici";
17
19
  var TOKEN_FIELD_PATTERN = /"(access_token|refresh_token|device_code|user_code|code_verifier|client_secret)"\s*:\s*"[^"]*"/gi;
18
20
  var BARE_TOKEN_PATTERN = /\bck_(?:live|dev|test)_[A-Za-z0-9_-]+/gi;
19
21
  function redactTokenLikeFields(input) {
@@ -677,6 +679,140 @@ async function patch(params) {
677
679
  workflow: data.workflow
678
680
  };
679
681
  }
682
+ async function uploadAsset(params) {
683
+ const contentType = detectContentType(params.filename, params.content);
684
+ const sha = sha256Hex(params.content);
685
+ const contentHash = `sha256:${sha}`;
686
+ const idempotencyKey = generateUploadIdempotencyKey(params.content, params.scope);
687
+ const response = await params.client.request({
688
+ method: "POST",
689
+ path: "/assets",
690
+ body: {
691
+ filename: params.filename,
692
+ contentType,
693
+ sizeBytes: params.content.length,
694
+ contentHash
695
+ },
696
+ idempotencyKey,
697
+ responseSchema: params.schema,
698
+ signal: params.signal
699
+ });
700
+ if (!response.deduplicated) {
701
+ const first = await tryPut({
702
+ uploadUrl: response.uploadUrl,
703
+ content: params.content,
704
+ contentType,
705
+ signal: params.signal,
706
+ dispatcher: params.putDispatcher
707
+ });
708
+ if (!first.ok) {
709
+ if (first.kind === "fatal") {
710
+ throw new ApiError(
711
+ "STORAGE_UPLOAD_FAILED",
712
+ `Upload to signed URL failed for ${params.filename} (HTTP ${first.status ?? "unknown"}).`,
713
+ { retryable: false }
714
+ );
715
+ }
716
+ const retry = await tryPut({
717
+ uploadUrl: response.uploadUrl,
718
+ content: params.content,
719
+ contentType,
720
+ signal: params.signal,
721
+ dispatcher: params.putDispatcher
722
+ });
723
+ if (!retry.ok) {
724
+ throw new ApiError(
725
+ "STORAGE_UPLOAD_FAILED",
726
+ `Upload to signed URL failed for ${params.filename} after one retry.`,
727
+ { retryable: true }
728
+ );
729
+ }
730
+ }
731
+ }
732
+ return {
733
+ assetId: response.assetId,
734
+ publicUrl: response.publicUrl,
735
+ deduplicated: response.deduplicated,
736
+ bytes: params.content.length,
737
+ contentHash
738
+ };
739
+ }
740
+ async function tryPut(args) {
741
+ try {
742
+ const response = await undiciRequest2(args.uploadUrl, {
743
+ method: "PUT",
744
+ body: args.content,
745
+ headers: {
746
+ "content-type": args.contentType,
747
+ "content-length": String(args.content.length)
748
+ },
749
+ signal: args.signal,
750
+ dispatcher: args.dispatcher
751
+ });
752
+ await response.body.text();
753
+ const status = response.statusCode;
754
+ if (status >= 200 && status < 300) return { ok: true };
755
+ if (status >= 400 && status < 500) return { ok: false, kind: "fatal", status };
756
+ return { ok: false, kind: "retryable", status };
757
+ } catch {
758
+ return { ok: false, kind: "retryable" };
759
+ }
760
+ }
761
+ function sha256Hex(content) {
762
+ return createHash2("sha256").update(content).digest("hex");
763
+ }
764
+ var MAGIC_NUMBERS = [
765
+ { contentType: "image/png", prefix: [137, 80, 78, 71, 13, 10, 26, 10] },
766
+ { contentType: "image/jpeg", prefix: [255, 216, 255] },
767
+ { contentType: "image/webp", prefix: [82, 73, 70, 70] }
768
+ // also matches WAV — disambiguated below
769
+ ];
770
+ var EXTENSION_FALLBACK = {
771
+ png: "image/png",
772
+ jpg: "image/jpeg",
773
+ jpeg: "image/jpeg",
774
+ gif: "image/gif",
775
+ webp: "image/webp",
776
+ mp4: "video/mp4",
777
+ mov: "video/quicktime",
778
+ mp3: "audio/mpeg",
779
+ wav: "audio/wav",
780
+ pdf: "application/pdf",
781
+ txt: "text/plain",
782
+ json: "application/json"
783
+ };
784
+ function detectContentType(filename, content) {
785
+ if (content.length >= 6) {
786
+ const head = content.subarray(0, 6).toString("ascii");
787
+ if (head === "GIF87a" || head === "GIF89a") return "image/gif";
788
+ }
789
+ if (content.length >= 12 && content.subarray(4, 8).toString("ascii") === "ftyp") {
790
+ return "video/mp4";
791
+ }
792
+ for (const { contentType, prefix } of MAGIC_NUMBERS) {
793
+ if (matchesPrefix(content, prefix)) {
794
+ if (contentType === "image/webp") {
795
+ const formatId = content.subarray(8, 12).toString("ascii");
796
+ if (formatId === "WEBP") return "image/webp";
797
+ if (formatId === "WAVE") return "audio/wav";
798
+ break;
799
+ }
800
+ return contentType;
801
+ }
802
+ }
803
+ const ext = filename.toLowerCase().split(".").pop();
804
+ if (ext && EXTENSION_FALLBACK[ext]) {
805
+ return EXTENSION_FALLBACK[ext];
806
+ }
807
+ return "application/octet-stream";
808
+ }
809
+ function matchesPrefix(content, prefix) {
810
+ if (content.length < prefix.length) return false;
811
+ for (let i = 0; i < prefix.length; i++) {
812
+ if (content[i] !== prefix[i]) return false;
813
+ }
814
+ return true;
815
+ }
680
816
 
681
817
  // src/errors/codes.ts
682
818
  var LOGIN_HINT = "Run `clickraft login` to authenticate.";
@@ -1635,10 +1771,9 @@ var GenerationResultSchema = z9.object({
1635
1771
  }).strict();
1636
1772
 
1637
1773
  // src/commands/upload.ts
1638
- import { createHash as createHash2 } from "crypto";
1639
1774
  import { readFile as readFile2, stat as stat2 } from "fs/promises";
1640
1775
  import { basename } from "path";
1641
- import { request as undiciRequest2 } from "undici";
1776
+ import "undici";
1642
1777
 
1643
1778
  // src/schemas/assets.ts
1644
1779
  import { z as z10 } from "zod";
@@ -1663,60 +1798,16 @@ async function uploadFile(opts) {
1663
1798
  const size = await safeStatSize(opts.filePath);
1664
1799
  validateSize(size);
1665
1800
  const content = await safeReadFile(opts.filePath);
1666
- const contentType = detectContentType(opts.filePath, content);
1667
- const sha = sha256Hex(content);
1668
- const contentHash = `sha256:${sha}`;
1669
- const filename = basename(opts.filePath);
1670
- const idempotencyKey = generateUploadIdempotencyKey(content, scope);
1671
1801
  const client = opts.client ?? await buildClient(opts);
1672
- const response = await postAssetMetadata({
1802
+ return uploadAsset({
1673
1803
  client,
1674
- filename,
1675
- contentType,
1676
- sizeBytes: content.length,
1677
- contentHash,
1678
- idempotencyKey,
1679
- signal: opts.signal
1804
+ content,
1805
+ filename: basename(opts.filePath),
1806
+ scope,
1807
+ signal: opts.signal,
1808
+ putDispatcher: opts.putDispatcher,
1809
+ schema: UploadResponseSchema
1680
1810
  });
1681
- if (!response.deduplicated) {
1682
- const first = await tryPut({
1683
- uploadUrl: response.uploadUrl,
1684
- content,
1685
- contentType,
1686
- signal: opts.signal,
1687
- dispatcher: opts.putDispatcher
1688
- });
1689
- if (!first.ok) {
1690
- if (first.kind === "fatal") {
1691
- throw new ApiError(
1692
- "STORAGE_UPLOAD_FAILED",
1693
- `Upload to signed URL failed for ${filename} (HTTP ${first.status ?? "unknown"}).`,
1694
- { retryable: false }
1695
- );
1696
- }
1697
- const retry = await tryPut({
1698
- uploadUrl: response.uploadUrl,
1699
- content,
1700
- contentType,
1701
- signal: opts.signal,
1702
- dispatcher: opts.putDispatcher
1703
- });
1704
- if (!retry.ok) {
1705
- throw new ApiError(
1706
- "STORAGE_UPLOAD_FAILED",
1707
- `Upload to signed URL failed for ${filename} after one retry.`,
1708
- { retryable: true }
1709
- );
1710
- }
1711
- }
1712
- }
1713
- return {
1714
- assetId: response.assetId,
1715
- publicUrl: response.publicUrl,
1716
- deduplicated: response.deduplicated,
1717
- bytes: content.length,
1718
- contentHash
1719
- };
1720
1811
  }
1721
1812
  async function buildClient(opts) {
1722
1813
  const cfg = await loadConfig({
@@ -1731,42 +1822,6 @@ async function buildClient(opts) {
1731
1822
  cloudflareAccess: cfg.cloudflareAccess
1732
1823
  });
1733
1824
  }
1734
- async function postAssetMetadata(args) {
1735
- return args.client.request({
1736
- method: "POST",
1737
- path: "/assets",
1738
- body: {
1739
- filename: args.filename,
1740
- contentType: args.contentType,
1741
- sizeBytes: args.sizeBytes,
1742
- contentHash: args.contentHash
1743
- },
1744
- idempotencyKey: args.idempotencyKey,
1745
- responseSchema: UploadResponseSchema,
1746
- signal: args.signal
1747
- });
1748
- }
1749
- async function tryPut(args) {
1750
- try {
1751
- const response = await undiciRequest2(args.uploadUrl, {
1752
- method: "PUT",
1753
- body: args.content,
1754
- headers: {
1755
- "content-type": args.contentType,
1756
- "content-length": String(args.content.length)
1757
- },
1758
- signal: args.signal,
1759
- dispatcher: args.dispatcher
1760
- });
1761
- await response.body.text();
1762
- const status = response.statusCode;
1763
- if (status >= 200 && status < 300) return { ok: true };
1764
- if (status >= 400 && status < 500) return { ok: false, kind: "fatal", status };
1765
- return { ok: false, kind: "retryable", status };
1766
- } catch {
1767
- return { ok: false, kind: "retryable" };
1768
- }
1769
- }
1770
1825
  async function safeStatSize(filePath) {
1771
1826
  let stats;
1772
1827
  try {
@@ -1828,61 +1883,6 @@ function validateSize(bytes) {
1828
1883
  );
1829
1884
  }
1830
1885
  }
1831
- function sha256Hex(content) {
1832
- return createHash2("sha256").update(content).digest("hex");
1833
- }
1834
- var MAGIC_NUMBERS = [
1835
- { contentType: "image/png", prefix: [137, 80, 78, 71, 13, 10, 26, 10] },
1836
- { contentType: "image/jpeg", prefix: [255, 216, 255] },
1837
- { contentType: "image/webp", prefix: [82, 73, 70, 70] }
1838
- // also matches WAV — disambiguated below
1839
- ];
1840
- var EXTENSION_FALLBACK = {
1841
- png: "image/png",
1842
- jpg: "image/jpeg",
1843
- jpeg: "image/jpeg",
1844
- gif: "image/gif",
1845
- webp: "image/webp",
1846
- mp4: "video/mp4",
1847
- mov: "video/quicktime",
1848
- mp3: "audio/mpeg",
1849
- wav: "audio/wav",
1850
- pdf: "application/pdf",
1851
- txt: "text/plain",
1852
- json: "application/json"
1853
- };
1854
- function detectContentType(filePath, content) {
1855
- if (content.length >= 6) {
1856
- const head = content.subarray(0, 6).toString("ascii");
1857
- if (head === "GIF87a" || head === "GIF89a") return "image/gif";
1858
- }
1859
- if (content.length >= 12 && content.subarray(4, 8).toString("ascii") === "ftyp") {
1860
- return "video/mp4";
1861
- }
1862
- for (const { contentType, prefix } of MAGIC_NUMBERS) {
1863
- if (matchesPrefix(content, prefix)) {
1864
- if (contentType === "image/webp") {
1865
- const formatId = content.subarray(8, 12).toString("ascii");
1866
- if (formatId === "WEBP") return "image/webp";
1867
- if (formatId === "WAVE") return "audio/wav";
1868
- break;
1869
- }
1870
- return contentType;
1871
- }
1872
- }
1873
- const ext = filePath.toLowerCase().split(".").pop();
1874
- if (ext && EXTENSION_FALLBACK[ext]) {
1875
- return EXTENSION_FALLBACK[ext];
1876
- }
1877
- return "application/octet-stream";
1878
- }
1879
- function matchesPrefix(content, prefix) {
1880
- if (content.length < prefix.length) return false;
1881
- for (let i = 0; i < prefix.length; i++) {
1882
- if (content[i] !== prefix[i]) return false;
1883
- }
1884
- return true;
1885
- }
1886
1886
 
1887
1887
  // src/commands/generate/internal.ts
1888
1888
  async function buildGenerateClient(opts) {