@metasession.co/devaudit-cli 0.1.63 → 0.1.64

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
@@ -56,7 +56,7 @@ function emitJsonResult(payload) {
56
56
 
57
57
  // package.json
58
58
  var package_default = {
59
- version: "0.1.63"};
59
+ version: "0.1.64"};
60
60
 
61
61
  // src/lib/version.ts
62
62
  var CLI_VERSION = package_default.version;
@@ -599,6 +599,9 @@ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
599
599
  var DEFAULT_MAX_ATTEMPTS = 5;
600
600
  var DEFAULT_UPLOAD_MAX_TIME_SECONDS = 120;
601
601
  var INITIAL_BACKOFF_MS = 1e3;
602
+ var DEFAULT_PRESIGNED_THRESHOLD_BYTES = 26214400;
603
+ var DEFAULT_PRESIGNED_MAX_ATTEMPTS = 3;
604
+ var DEFAULT_PRESIGNED_UPLOAD_MAX_TIME_SECONDS = 300;
602
605
  function maxAttempts() {
603
606
  const raw = Number.parseInt(process.env["UPLOAD_MAX_ATTEMPTS"] ?? "", 10);
604
607
  return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_ATTEMPTS;
@@ -648,6 +651,7 @@ function buildUploadForm(file, buf, opts) {
648
651
  if (opts.releaseTitle) form.set("releaseTitle", opts.releaseTitle);
649
652
  if (opts.changeType) form.set("changeType", opts.changeType);
650
653
  if (opts.gateStatus) form.set("gateStatus", opts.gateStatus);
654
+ if (opts.sdlcStage) form.set("sdlcStage", opts.sdlcStage);
651
655
  return form;
652
656
  }
653
657
  function uploadFailureMessage(err, timeoutSeconds) {
@@ -724,6 +728,164 @@ async function uploadOne(file, buf, opts) {
724
728
  }
725
729
  return { file, ok: false, status: 0, error: "max retries exhausted" };
726
730
  }
731
+ var PRESIGNED_FALLBACK = /* @__PURE__ */ Symbol("presigned-fallback");
732
+ async function uploadPresigned(file, buf, opts) {
733
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
734
+ const presignUrl = `${baseUrl}/api/evidence/upload-url`;
735
+ const completeUrl = `${baseUrl}/api/evidence/upload-complete`;
736
+ const maxAttemptsPresigned = DEFAULT_PRESIGNED_MAX_ATTEMPTS;
737
+ const timeoutSeconds = uploadMaxTimeSeconds();
738
+ const presignedTimeoutSeconds = DEFAULT_PRESIGNED_UPLOAD_MAX_TIME_SECONDS;
739
+ const mimeType = deriveMimeType(file);
740
+ const metadata = opts.metadata ?? {};
741
+ let uploadUrl = "";
742
+ let evidenceId = "";
743
+ let attempt = 1;
744
+ let backoff = INITIAL_BACKOFF_MS;
745
+ while (attempt <= maxAttemptsPresigned) {
746
+ const controller = new AbortController();
747
+ const timer = setTimeout(() => controller.abort(), timeoutSeconds * 1e3);
748
+ let res;
749
+ try {
750
+ res = await fetch(presignUrl, {
751
+ method: "POST",
752
+ headers: {
753
+ authorization: `Bearer ${opts.apiKey}`,
754
+ "content-type": "application/json"
755
+ },
756
+ body: JSON.stringify({
757
+ projectSlug: opts.projectSlug,
758
+ requirementId: opts.requirementId,
759
+ evidenceType: opts.evidenceType,
760
+ fileName: basename(file),
761
+ fileSizeBytes: buf.byteLength,
762
+ mimeType,
763
+ metadata,
764
+ ...opts.releaseVersion ? { releaseVersion: opts.releaseVersion } : {},
765
+ ...opts.createReleaseIfMissing ? { createReleaseIfMissing: true } : {},
766
+ ...opts.releaseBranch ? { releaseBranch: opts.releaseBranch } : {},
767
+ ...opts.environment ? { environment: opts.environment } : {},
768
+ ...opts.evidenceCategory ? { evidenceCategory: opts.evidenceCategory } : {},
769
+ ...opts.sdlcStage ? { sdlcStage: opts.sdlcStage } : {}
770
+ }),
771
+ signal: controller.signal
772
+ });
773
+ } catch (err) {
774
+ if (attempt < maxAttemptsPresigned) {
775
+ await delay(backoff);
776
+ backoff *= 2;
777
+ attempt += 1;
778
+ continue;
779
+ }
780
+ return { file, ok: false, status: 0, error: uploadFailureMessage(err, timeoutSeconds) };
781
+ } finally {
782
+ clearTimeout(timer);
783
+ }
784
+ if (res.ok) {
785
+ const body = await res.json().catch(() => null);
786
+ if (body?.uploadUrl && body?.evidenceId) {
787
+ uploadUrl = body.uploadUrl;
788
+ evidenceId = body.evidenceId;
789
+ break;
790
+ }
791
+ return PRESIGNED_FALLBACK;
792
+ }
793
+ if (RETRYABLE_STATUSES.has(res.status) && attempt < maxAttemptsPresigned) {
794
+ await delay(backoff);
795
+ backoff *= 2;
796
+ attempt += 1;
797
+ continue;
798
+ }
799
+ const errText = await res.text().catch(() => "(no body)");
800
+ return { file, ok: false, status: res.status, error: `presigned step 1: ${errText}` };
801
+ }
802
+ if (!uploadUrl || !evidenceId) {
803
+ return { file, ok: false, status: 0, error: "presigned step 1: no URL after retries" };
804
+ }
805
+ attempt = 1;
806
+ backoff = INITIAL_BACKOFF_MS;
807
+ while (attempt <= maxAttemptsPresigned) {
808
+ const controller = new AbortController();
809
+ const timer = setTimeout(() => controller.abort(), presignedTimeoutSeconds * 1e3);
810
+ let res;
811
+ try {
812
+ res = await fetch(uploadUrl, {
813
+ method: "PUT",
814
+ headers: { "content-type": mimeType },
815
+ body: new Uint8Array(buf),
816
+ signal: controller.signal
817
+ });
818
+ } catch (err) {
819
+ if (attempt < maxAttemptsPresigned) {
820
+ await delay(backoff);
821
+ backoff *= 2;
822
+ attempt += 1;
823
+ continue;
824
+ }
825
+ return { file, ok: false, status: 0, error: `presigned step 2: ${uploadFailureMessage(err, presignedTimeoutSeconds)}` };
826
+ } finally {
827
+ clearTimeout(timer);
828
+ }
829
+ if (res.ok) break;
830
+ if (RETRYABLE_STATUSES.has(res.status) && attempt < maxAttemptsPresigned) {
831
+ await delay(backoff);
832
+ backoff *= 2;
833
+ attempt += 1;
834
+ continue;
835
+ }
836
+ return { file, ok: false, status: res.status, error: `presigned step 2: HTTP ${res.status}` };
837
+ }
838
+ attempt = 1;
839
+ backoff = INITIAL_BACKOFF_MS;
840
+ while (attempt <= maxAttemptsPresigned) {
841
+ const controller = new AbortController();
842
+ const timer = setTimeout(() => controller.abort(), timeoutSeconds * 1e3);
843
+ let res;
844
+ try {
845
+ res = await fetch(completeUrl, {
846
+ method: "POST",
847
+ headers: {
848
+ authorization: `Bearer ${opts.apiKey}`,
849
+ "content-type": "application/json"
850
+ },
851
+ body: JSON.stringify({ evidenceId }),
852
+ signal: controller.signal
853
+ });
854
+ } catch (err) {
855
+ if (attempt < maxAttemptsPresigned) {
856
+ await delay(backoff);
857
+ backoff *= 2;
858
+ attempt += 1;
859
+ continue;
860
+ }
861
+ return { file, ok: false, status: 0, error: `presigned step 3: ${uploadFailureMessage(err, timeoutSeconds)}` };
862
+ } finally {
863
+ clearTimeout(timer);
864
+ }
865
+ if (res.ok) {
866
+ const body = await res.json().catch(() => null);
867
+ return { file, ok: true, status: res.status, body };
868
+ }
869
+ if (RETRYABLE_STATUSES.has(res.status) && attempt < maxAttemptsPresigned) {
870
+ await delay(backoff);
871
+ backoff *= 2;
872
+ attempt += 1;
873
+ continue;
874
+ }
875
+ return { file, ok: false, status: res.status, error: `presigned step 3: HTTP ${res.status}` };
876
+ }
877
+ return { file, ok: false, status: 0, error: "presigned step 3: max retries exhausted" };
878
+ }
879
+ function deriveMimeType(file) {
880
+ const lower = file.toLowerCase();
881
+ if (lower.endsWith(".zip")) return "application/zip";
882
+ if (lower.endsWith(".json")) return "application/json";
883
+ if (lower.endsWith(".png")) return "image/png";
884
+ if (lower.endsWith(".pdf")) return "application/pdf";
885
+ if (lower.endsWith(".md")) return "text/markdown";
886
+ if (lower.endsWith(".html")) return "text/html";
887
+ return "application/octet-stream";
888
+ }
727
889
  async function uploadEvidence(opts) {
728
890
  const files = await collectFiles(opts.filePath);
729
891
  if (files.length === 0) {
@@ -736,6 +898,13 @@ async function uploadEvidence(opts) {
736
898
  results.push({ file, ok: true, status: 0, skipped: true });
737
899
  continue;
738
900
  }
901
+ if (buf.byteLength >= DEFAULT_PRESIGNED_THRESHOLD_BYTES) {
902
+ const presignedResult = await uploadPresigned(file, buf, opts);
903
+ if (presignedResult !== PRESIGNED_FALLBACK) {
904
+ results.push(presignedResult);
905
+ continue;
906
+ }
907
+ }
739
908
  results.push(await uploadOne(file, buf, opts));
740
909
  }
741
910
  return results;
@@ -839,6 +1008,7 @@ async function runPush(options) {
839
1008
  ...options.releaseTitle !== void 0 ? { releaseTitle: options.releaseTitle } : {},
840
1009
  ...options.changeType !== void 0 ? { changeType: options.changeType } : {},
841
1010
  ...options.gateStatus !== void 0 ? { gateStatus: options.gateStatus } : {},
1011
+ ...options.sdlcStage !== void 0 ? { sdlcStage: options.sdlcStage } : {},
842
1012
  metadata
843
1013
  });
844
1014
  let okCount = 0;
@@ -2977,7 +3147,7 @@ async function main(argv) {
2977
3147
  ...globals.dryRun !== void 0 ? { dryRun: Boolean(globals.dryRun) } : {}
2978
3148
  });
2979
3149
  });
2980
- program.command("push <project-slug> <requirement-id> <evidence-type> <file>").description("Upload evidence file(s) to DevAudit (port of scripts/upload-evidence.sh)").option("--release <version>", "release version (e.g. v1.0.0)").option("--create-release-if-missing", "auto-create the release as 'draft' if absent").option("--environment <env>", "uat | production").option("--category <cat>", "ci_pipeline | local_dev | planning | test_report | security_scan | release_artifact").option("--git-sha <sha>", "attached to metadata.gitSha").option("--ci-run-id <id>", "attached to metadata.ciRunId").option("--branch <name>", "git branch \u2014 sent as releaseBranch + metadata.branch").option("--release-title <text>", "human title for the release row (releaseTitle; portal no-clobbers)").option("--change-type <type>", "conventional-commit prefix for the release row (changeType)").option("--gate-status <status>", "passed | failed | skipped (gateStatus)").option(
3150
+ program.command("push <project-slug> <requirement-id> <evidence-type> <file>").description("Upload evidence file(s) to DevAudit (port of scripts/upload-evidence.sh)").option("--release <version>", "release version (e.g. v1.0.0)").option("--create-release-if-missing", "auto-create the release as 'draft' if absent").option("--environment <env>", "uat | production").option("--category <cat>", "ci_pipeline | local_dev | planning | test_report | security_scan | release_artifact").option("--git-sha <sha>", "attached to metadata.gitSha").option("--ci-run-id <id>", "attached to metadata.ciRunId").option("--branch <name>", "git branch \u2014 sent as releaseBranch + metadata.branch").option("--release-title <text>", "human title for the release row (releaseTitle; portal no-clobbers)").option("--change-type <type>", "conventional-commit prefix for the release row (changeType)").option("--gate-status <status>", "passed | failed | skipped (gateStatus)").option("--sdlc-stage <stage>", "SDLC stage 1-5 (sdlcStage)").option(
2981
3151
  "--meta-key <pair>",
2982
3152
  "repeatable key=value merged into the metadata JSON",
2983
3153
  (val, acc) => [...acc, val],
@@ -3000,6 +3170,7 @@ async function main(argv) {
3000
3170
  ...opts.releaseTitle !== void 0 ? { releaseTitle: opts.releaseTitle } : {},
3001
3171
  ...opts.changeType !== void 0 ? { changeType: opts.changeType } : {},
3002
3172
  ...opts.gateStatus !== void 0 ? { gateStatus: opts.gateStatus } : {},
3173
+ ...opts.sdlcStage !== void 0 ? { sdlcStage: opts.sdlcStage } : {},
3003
3174
  ...opts.metaKey !== void 0 && opts.metaKey.length > 0 ? { metaKeys: opts.metaKey } : {},
3004
3175
  ...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {},
3005
3176
  ...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},