@knowttl/artium-axi 0.0.14 → 0.0.15

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/README.md CHANGED
@@ -98,7 +98,9 @@ Replace the synthetic username with your account; login uses a hidden terminal p
98
98
  For `PASSWORD_CHANGE_REQUIRED`, run `artium-axi passwd`, then log in again.
99
99
  For SSO, use `artium-axi login --sso` and approve only the browser code matching your own terminal.
100
100
  After ordinary login, later commands, installed hooks and skills use [saved connection settings](docs/client-reference.md#client-settings).
101
- After a server upgrade, run `artium-axi update`, then `artium-axi --version`.
101
+ After a server upgrade, run `artium-axi update` if your client has that command, then `artium-axi --version`.
102
+ For clients without `update`, repeat the verified download and checksum check above to install the server's current client.
103
+ For `CLI_OUTDATED`, the server uses the client version header to select the update hint or safe download guidance; the capability boundary is defined by `firstVersionWithUpdate` in [the version contract](src/shared/version.ts).
102
104
  The updater uses those connection settings, verifies the server download’s SHA-256, replaces the running client file and keeps the previous client beside it as `<client-file>.bak`.
103
105
  The client file and its directory must be writable.
104
106
  The Falcon read requires an enabled tool, a role grant and API permissions; choose the example from an available tool's detail view.
@@ -178,7 +180,7 @@ See [client input handling](docs/client-reference.md#command-surface-and-subcomm
178
180
  | Azure DevOps | Reviewed mutations with administrator `allowWrites: true`; raw `api` remains GET-only. |
179
181
  | AWX | Launch/cancel/relaunch, workflow approval/denial, project/inventory-source sync and notification tests with `allowWrites: true` and upstream `--confirm`; configuration, deletion and security writes stay blocked. |
180
182
  | Tenable One | Business writes and export-job creation with administrator `writes.allowWrites: true`, an explicit operations allow-list, native preview, `--execute`, exact `--confirm` and durable journaling; downloads and local configuration stay blocked. |
181
- | Hybrid Analysis | Sandbox submissions, YARA create/update/delete and consuming hunting matches require administrator `writes.allowWrites: true`, an explicit operations allow-list, native preview, `--execute`, exact `--confirm` and durable journaling. All downloads, including report artifacts, samples, dropped-file archives and memory dumps, are unavailable pending a separate hosted download path with its own security design; use hybrid-axi locally with its native guards and live-malware warnings. Local configuration and credential/origin overrides are blocked. |
183
+ | Hybrid Analysis | Sandbox submissions, YARA create/update/delete, consuming hunting matches and the four reviewed archive downloads require administrator `writes.allowWrites: true`, independently selected operations, native preview, `--execute`, exact `--confirm` and durable journaling. Downloads are limited to `hash-download-bin`, `report-download-bin`, `report-download-dropped-files` and `report-download-memory`, through the [authenticated CLI verified-save path](docs/client-reference.md#hybrid-analysis-archive-saving) after the [operator prerequisites](docs/runbook.md#scope-and-prerequisites) pass. All operations default off; enabling other writes never enables downloads or grants upstream entitlement. The browser provides warnings, command guidance and metadata/save status only. Ordinary tool-run downloads, local configuration and credential/origin overrides are blocked. |
182
184
 
183
185
  Hybrid Analysis quick scans transmit the supplied sample or URL as read exceptions without a write gate; sandbox submission previews disclose that samples and reports can become visible to the community.
184
186
  Consuming hunting matches marks every returned match delivered; reconcile ambiguous outcomes using the audit ID, durable native journal and returned read-back guidance before retrying.
@@ -239,6 +241,7 @@ This summary follows the [CLI help registry](src/cli/help.ts); `artium-axi --hel
239
241
  | `tools` / `tools show <tool>` | Granted enabled tools / pinned help and first call |
240
242
  | `doctor` | Server, TLS, token, version and cached health; five-second request deadlines |
241
243
  | `update` | Update the [server-downloaded client](#verified-client) |
244
+ | `download` | Preview and explicitly verify/save administrator-selected Hybrid Analysis archives through the local CLI. Run `artium-axi download --help` for guards and warnings. |
242
245
  | `setup --host <host>` | Install managed skills and optional Claude hook |
243
246
  | `<tool> <args>` | Run any of the nine managed tools above |
244
247
  | `--managed-session --help` | Private-pipe session protocol |
@@ -248,7 +251,9 @@ The [full client reference](docs/client-reference.md#command-surface-and-subcomm
248
251
 
249
252
  ### TOON output and errors
250
253
  Discovery and Artium errors use TOON on stdout; progress uses stderr.
251
- Upstream stdout, stderr and exit status pass through, with managed suggestions adapted to Artium commands.
254
+ Upstream stdout, stderr and exit status pass through, subject to managed suggestion adaptation and hosted status filtering.
255
+ Hosted home and authentication status omit server executable and configuration paths; secret references report only their source kind, `env` or `file`.
256
+ Malformed or truncated hosted status output is withheld with `INVALID_RESPONSE`.
252
257
  Artium exits 0 for success, 2 for usage/configuration errors and 1 for authentication/server/transport failures.
253
258
  See the generated [error catalogue](docs/errors.md) for actionable codes and retry guidance.
254
259
 
@@ -6,9 +6,9 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/cli/main.ts
9
- import { randomUUID as randomUUID4 } from "node:crypto";
9
+ import { randomUUID as randomUUID5 } from "node:crypto";
10
10
  import { homedir as homedir3 } from "node:os";
11
- import { resolve as resolve3, relative, sep, isAbsolute } from "node:path";
11
+ import { resolve as resolve4, relative, sep, isAbsolute as isAbsolute2 } from "node:path";
12
12
 
13
13
  // node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/classic/external.js
14
14
  var external_exports = {};
@@ -5036,7 +5036,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5036
5036
  var NONE = 0;
5037
5037
  var ASSUMED = 1;
5038
5038
  var PROVEN = 2;
5039
- function isRecursive(inst, stack, resolve4) {
5039
+ function isRecursive(inst, stack, resolve5) {
5040
5040
  const cached2 = recursive.get(inst);
5041
5041
  if (cached2 !== void 0)
5042
5042
  return cached2 ? PROVEN : NONE;
@@ -5046,7 +5046,7 @@ function isRecursive(inst, stack, resolve4) {
5046
5046
  let result = NONE;
5047
5047
  const check2 = (child) => {
5048
5048
  if (result !== PROVEN && child?._zod) {
5049
- const answer = isRecursive(child, stack, resolve4);
5049
+ const answer = isRecursive(child, stack, resolve5);
5050
5050
  if (answer > result)
5051
5051
  result = answer;
5052
5052
  }
@@ -5057,7 +5057,7 @@ function isRecursive(inst, stack, resolve4) {
5057
5057
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5058
5058
  if (spread && !desc.enumerable)
5059
5059
  continue;
5060
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : NONE;
5060
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
5061
5061
  if (child > answer)
5062
5062
  answer = child;
5063
5063
  }
@@ -5121,7 +5121,7 @@ function isRecursive(inst, stack, resolve4) {
5121
5121
  break;
5122
5122
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5123
5123
  case "lazy": {
5124
- const inner = def._cachedInner ?? (resolve4 ? inst._zod.innerType : void 0);
5124
+ const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
5125
5125
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5126
5126
  break;
5127
5127
  }
@@ -19679,11 +19679,12 @@ function date4(params) {
19679
19679
  }
19680
19680
 
19681
19681
  // src/shared/errors.ts
19682
+ var cliOutdatedDownloadNext = "Download the current CLI from /cli/artium-axi.mjs on this server, verify it with the SHA-256 checksum at /cli/artium-axi.mjs.sha256, and install it as artium-axi.";
19682
19683
  var definitions = {
19683
19684
  STORE_SCHEMA_INVALID: { status: 503, message: "The store schema is unsupported by this image.", next: "Use the matching image, or restore the pre-upgrade backup into a new volume with its matching image and key." },
19684
19685
  BACKUP_FAILED: { status: 500, message: "Database backup did not complete.", next: "Check the source store and a writable backup directory; choose a new output file." },
19685
19686
  RESTORE_FAILED: { status: 500, message: "Database restore did not complete.", next: "Stop the destination server and use an empty new data volume, an intact backup and its matching master key." },
19686
- CLI_OUTDATED: { status: 426, message: "The CLI version is too old for this server.", next: "Run artium-axi update to install the current CLI." },
19687
+ CLI_OUTDATED: { status: 426, message: "The CLI version is too old for this server.", next: cliOutdatedDownloadNext },
19687
19688
  CERTIFICATE_INVALID: { status: 503, message: "The server certificate could not be verified.", next: "Check certificate trust or set ARTIUM_AXI_CA_FILE to the trusted CA file." },
19688
19689
  ADMIN_REQUIRED: { status: 403, message: "Administrator capability is required.", next: "Ask an administrator to perform this operation." },
19689
19690
  REVISION_CONFLICT: { status: 409, message: "The record changed since it was read.", next: "Read the current record revision and retry your edit." },
@@ -19719,6 +19720,9 @@ var definitions = {
19719
19720
  PASSWORD_CHANGE_REQUIRED: { status: 409, message: "Change your temporary password before using tools.", next: "Run artium-axi passwd --password-stdin." },
19720
19721
  SERVER_UNAVAILABLE: { status: 503, message: "The CLI could not receive a server response.", next: "Check the server address and certificate trust; confirm the previous call outcome before retrying." },
19721
19722
  INVALID_RESPONSE: { status: 500, message: "The server returned an invalid response.", next: "Check the server URL and API version." },
19723
+ KEY_LEVEL_RESTRICTED: { status: 403, message: "HTTP 403: restricted key cannot download.", next: "Request vetted-account download access. Administrator write settings cannot grant upstream entitlement. No other key or route was tried." },
19724
+ PERMISSION_DENIED: { status: 403, message: "HTTP 403: download denied; key level unknown.", next: "Check upstream entitlement and request account vetting where required. Administrator write settings cannot grant upstream entitlement. No other key or route was tried." },
19725
+ ARTIFACT_SAVE_FAILED: { status: 400, message: "The archive was not verified and saved.", next: "Check the dedicated private destination directory. Start a fresh preview and confirmation; interrupted transfers cannot be retried." },
19722
19726
  DEV_AUTH_INVALID: { status: 503, message: "Development authentication settings are unsupported.", next: "Remove ARTIUM_AXI_DEV and ARTIUM_AXI_DEV_TOKEN_FILE and use local sign-in." },
19723
19727
  PROXY_HEADERS_UNTRUSTED: { status: 400, message: "Forwarding headers came from an untrusted address.", next: "Connect through the configured trusted proxy or remove forwarding headers." },
19724
19728
  NOT_FOUND: { status: 404, message: "The endpoint does not exist.", next: "Use a supported API endpoint and tool name." },
@@ -19781,6 +19785,103 @@ var inputLimits = {
19781
19785
  totalFileBytes: 4 * 1024 * 1024,
19782
19786
  files: 8
19783
19787
  };
19788
+ var transferLimits = {
19789
+ artifactBytes: 250 * 1024 * 1024,
19790
+ reservationsPerUser: 1,
19791
+ reservations: 4,
19792
+ reservedBytes: 1024 * 1024 * 1024,
19793
+ lifetimeMs: 10 * 60 * 1e3
19794
+ };
19795
+ var transferWarning = "Live malware: this archive may contain executable malware. Save only; do not extract or execute. Password protection is unverified; bytes are saved exactly as provided.";
19796
+ var sha256Schema = external_exports.string().regex(/^[a-f0-9]{64}$/);
19797
+ var sampleSha256Schema = external_exports.string().regex(/^[a-f0-9]{64}$/i);
19798
+ var reportIdSchema = external_exports.string().regex(/^(?:[A-Za-z0-9_-]{1,256}|[a-f0-9]{64}:\d+)$/i);
19799
+ var transferOwnerIdSchema = external_exports.string().regex(/^[A-Za-z0-9_-]{1,256}$/);
19800
+ var transferSelectionSchema = external_exports.discriminatedUnion("operation", [
19801
+ external_exports.strictObject({ operation: external_exports.literal("hash-download-bin"), selector: sampleSha256Schema, type: external_exports.literal("bin") }),
19802
+ external_exports.strictObject({ operation: external_exports.literal("report-download-bin"), selector: reportIdSchema, type: external_exports.literal("bin") }),
19803
+ external_exports.strictObject({ operation: external_exports.literal("report-download-dropped-files"), selector: reportIdSchema, type: external_exports.literal("dropped-files") }),
19804
+ external_exports.strictObject({ operation: external_exports.literal("report-download-memory"), selector: reportIdSchema, type: external_exports.literal("memory") })
19805
+ ]);
19806
+ var transferPrepareSchema = external_exports.strictObject({ selection: transferSelectionSchema, targetDigest: sha256Schema });
19807
+ var transferIdSchema = external_exports.uuid();
19808
+ var transferScopeSchema = external_exports.strictObject({
19809
+ transferId: transferIdSchema,
19810
+ userId: transferOwnerIdSchema,
19811
+ sessionFamilyId: transferOwnerIdSchema,
19812
+ credentialRevision: external_exports.number().int().positive(),
19813
+ selection: transferSelectionSchema,
19814
+ targetDigest: sha256Schema,
19815
+ preparedAt: external_exports.number().int().nonnegative(),
19816
+ expiresAt: external_exports.number().int().nonnegative()
19817
+ }).refine((scope) => scope.expiresAt === scope.preparedAt + transferLimits.lifetimeMs);
19818
+ var transferPreviewSchema = external_exports.strictObject({
19819
+ transferId: transferIdSchema,
19820
+ expiresAt: external_exports.number().int(),
19821
+ confirm: external_exports.string().min(1).max(64 * 1024),
19822
+ warnings: external_exports.array(external_exports.string()),
19823
+ targetDigest: sha256Schema
19824
+ });
19825
+ var transferExecuteSchema = external_exports.strictObject({
19826
+ transferId: transferIdSchema,
19827
+ selection: transferSelectionSchema,
19828
+ targetDigest: sha256Schema,
19829
+ execute: external_exports.literal(true),
19830
+ confirm: external_exports.string().min(1).max(64 * 1024)
19831
+ });
19832
+ var transferRetrieveSchema = transferPrepareSchema.extend({ transferId: transferIdSchema });
19833
+ var transferOutcomeSchema = external_exports.enum([
19834
+ "preview",
19835
+ "denied-not-sent",
19836
+ "native-outcome-unknown",
19837
+ "upstream-failure",
19838
+ "ready",
19839
+ "delivery-completed",
19840
+ "delivery-interrupted",
19841
+ "expired",
19842
+ "deleted",
19843
+ "interrupted"
19844
+ ]);
19845
+ var transferSaveOutcomeSchema = external_exports.enum(["unconfirmed", "verified-save", "save-failed"]);
19846
+ var transferArchiveSchema = external_exports.strictObject({
19847
+ archiveBytes: external_exports.number().int().nonnegative().max(transferLimits.artifactBytes),
19848
+ archiveSha256: sha256Schema
19849
+ });
19850
+ var transferReceiptSchema = transferRetrieveSchema.extend({
19851
+ archive: transferArchiveSchema,
19852
+ saveOutcome: external_exports.enum(["verified-save", "save-failed"])
19853
+ });
19854
+ var transferEvidenceSchema = external_exports.strictObject({
19855
+ outcome: transferOutcomeSchema,
19856
+ archive: transferArchiveSchema.nullable(),
19857
+ saveOutcome: transferSaveOutcomeSchema
19858
+ }).refine((evidence) => !["ready", "delivery-completed"].includes(evidence.outcome) || evidence.archive !== null).refine((evidence) => !["preview", "denied-not-sent", "native-outcome-unknown", "upstream-failure"].includes(evidence.outcome) || evidence.archive === null).refine((evidence) => evidence.saveOutcome !== "verified-save" || evidence.archive !== null);
19859
+ var transferStatusSchema = transferEvidenceSchema.safeExtend({ transferId: transferIdSchema, expiresAt: external_exports.number().int() });
19860
+ var transferRoutes = {
19861
+ prepare: { method: "POST", path: "/v1/transfers/hybrid/prepare", input: transferPrepareSchema, response: transferPreviewSchema },
19862
+ execute: { method: "POST", path: "/v1/transfers/hybrid/execute", input: transferExecuteSchema, response: transferStatusSchema },
19863
+ retrieve: { method: "POST", path: "/v1/transfers/hybrid/retrieve", input: transferRetrieveSchema },
19864
+ receipt: { method: "POST", path: "/v1/transfers/hybrid/receipt", input: transferReceiptSchema },
19865
+ cancel: { method: "POST", path: "/v1/transfers/hybrid/cancel", input: transferRetrieveSchema },
19866
+ status: { method: "POST", path: "/v1/transfers/hybrid/status", input: transferRetrieveSchema, response: transferStatusSchema }
19867
+ };
19868
+ var transferAuditSchema = external_exports.strictObject({
19869
+ transfer_id: transferIdSchema,
19870
+ session_family_id: transferOwnerIdSchema,
19871
+ transfer_operation: transferSelectionSchema.options[0].shape.operation.or(transferSelectionSchema.options[1].shape.operation).or(transferSelectionSchema.options[2].shape.operation).or(transferSelectionSchema.options[3].shape.operation),
19872
+ artifact_type: external_exports.enum(["bin", "dropped-files", "memory"]),
19873
+ artifact_selector: sha256Schema.or(reportIdSchema),
19874
+ requested_target_digest: sha256Schema,
19875
+ archive_bytes: external_exports.number().int().nonnegative().max(transferLimits.artifactBytes).nullable(),
19876
+ archive_sha256: sha256Schema.nullable(),
19877
+ native_audit_id: external_exports.uuid().nullable(),
19878
+ native_journal_result: external_exports.enum(["SUCCESS", "FAILED", "NOT_SENT", "OUTCOME_UNKNOWN"]).nullable(),
19879
+ client_save_outcome: transferSaveOutcomeSchema
19880
+ }).refine((fields) => fields.archive_bytes === null === (fields.archive_sha256 === null)).refine((fields) => transferSelectionSchema.safeParse({
19881
+ operation: fields.transfer_operation,
19882
+ type: fields.artifact_type,
19883
+ selector: fields.artifact_selector
19884
+ }).success);
19784
19885
  var byteLength = (value) => new TextEncoder().encode(value).length;
19785
19886
  var passwordSchema = external_exports.string().min(1).refine((value) => byteLength(value) <= 512, "Expected at most 512 UTF-8 bytes");
19786
19887
  var passwordChangeSchema = external_exports.strictObject({ newPassword: external_exports.string(), currentPassword: external_exports.string().optional() });
@@ -19998,7 +20099,8 @@ var auditFiltersSchema = external_exports.strictObject({
19998
20099
  event: external_exports.string().min(1).max(100).optional(),
19999
20100
  outcome: external_exports.string().min(1).max(100).optional(),
20000
20101
  from: external_exports.iso.datetime().optional(),
20001
- to: external_exports.iso.datetime().optional()
20102
+ to: external_exports.iso.datetime().optional(),
20103
+ errorCode: external_exports.string().min(1).max(100).optional()
20002
20104
  });
20003
20105
  var orderedAuditRange = (input2) => !input2.from || !input2.to || Date.parse(input2.from) <= Date.parse(input2.to);
20004
20106
  var adminAuditQuerySchema = auditFiltersSchema.extend({
@@ -20036,7 +20138,18 @@ var adminAuditRecordSchema = external_exports.strictObject({
20036
20138
  error_code: external_exports.string().nullable(),
20037
20139
  authentication_method: external_exports.string().nullable(),
20038
20140
  provider_id: external_exports.string().nullable(),
20039
- subject: external_exports.string().nullable()
20141
+ subject: external_exports.string().nullable(),
20142
+ transfer_id: transferIdSchema.nullable(),
20143
+ session_family_id: external_exports.string().nullable(),
20144
+ transfer_operation: external_exports.string().nullable(),
20145
+ artifact_type: external_exports.string().nullable(),
20146
+ artifact_selector: external_exports.string().nullable(),
20147
+ requested_target_digest: sha256Schema.nullable(),
20148
+ archive_bytes: external_exports.number().int().nullable(),
20149
+ archive_sha256: sha256Schema.nullable(),
20150
+ native_audit_id: external_exports.string().nullable(),
20151
+ native_journal_result: external_exports.string().nullable(),
20152
+ client_save_outcome: transferSaveOutcomeSchema.nullable()
20040
20153
  });
20041
20154
  var adminAuditRoute = {
20042
20155
  method: "GET",
@@ -20117,7 +20230,7 @@ var diagnosticSchema = external_exports.strictObject({
20117
20230
  credentialRevision: external_exports.number().int().nullable().optional()
20118
20231
  });
20119
20232
  var healthSchema = external_exports.strictObject({
20120
- status: external_exports.enum(["unconfigured", "unknown", "ok", "error", "disabled"]),
20233
+ status: external_exports.enum(["unconfigured", "unknown", "checking", "ok", "error", "disabled"]),
20121
20234
  diagnostic: diagnosticSchema.optional(),
20122
20235
  readOnly: external_exports.boolean().default(true),
20123
20236
  checkedAt: external_exports.number().int().nullable(),
@@ -20575,7 +20688,7 @@ var toolsResponseSchema = external_exports.strictObject({
20575
20688
  name: toolSchema,
20576
20689
  version: external_exports.string(),
20577
20690
  readOnly: external_exports.boolean().default(true),
20578
- status: external_exports.enum(["unconfigured", "unknown", "ok", "error", "disabled"])
20691
+ status: external_exports.enum(["unconfigured", "unknown", "checking", "ok", "error", "disabled"])
20579
20692
  }))
20580
20693
  });
20581
20694
  var inputFileSchema = external_exports.strictObject({
@@ -20632,7 +20745,7 @@ import { resolve } from "node:path";
20632
20745
  // package.json
20633
20746
  var package_default = {
20634
20747
  name: "artium-axi",
20635
- version: "0.0.14",
20748
+ version: "0.0.15",
20636
20749
  private: true,
20637
20750
  license: "MIT",
20638
20751
  type: "module",
@@ -20821,8 +20934,8 @@ var SessionStore = class {
20821
20934
  while (true) {
20822
20935
  const watcher = watch(this.directory);
20823
20936
  let timer;
20824
- const changed = new Promise((resolve4, reject) => {
20825
- watcher.once("change", () => resolve4());
20937
+ const changed = new Promise((resolve5, reject) => {
20938
+ watcher.once("change", () => resolve5());
20826
20939
  watcher.once("error", () => reject(new ArtiumError("SESSION_STORAGE_UNAVAILABLE")));
20827
20940
  timer = setTimeout(() => reject(new ArtiumError("SESSION_BUSY")), Math.max(0, deadline - Date.now()));
20828
20941
  });
@@ -20870,6 +20983,10 @@ var Client = class {
20870
20983
  }
20871
20984
  managed;
20872
20985
  requestDeadlineMs;
20986
+ cancellation = new AbortController();
20987
+ cancel() {
20988
+ this.cancellation.abort();
20989
+ }
20873
20990
  serverUrl;
20874
20991
  serverUrlSource;
20875
20992
  caFile;
@@ -20879,7 +20996,7 @@ var Client = class {
20879
20996
  return this.storedSessions;
20880
20997
  }
20881
20998
  certificateVerified = false;
20882
- async request(path, method, input2, token, bytes = false) {
20999
+ async request(path, method, input2, token, bytes = false, headers = {}) {
20883
21000
  this.certificateVerified = false;
20884
21001
  let ca;
20885
21002
  try {
@@ -20891,13 +21008,13 @@ var Client = class {
20891
21008
  const controller = new AbortController();
20892
21009
  const deadline = setTimeout(() => controller.abort(), this.requestDeadlineMs);
20893
21010
  const url2 = new URL(`${this.serverUrl}${path}`);
20894
- const result = await new Promise((resolve4, reject) => {
21011
+ const result = await new Promise((resolve5, reject) => {
20895
21012
  const request = (url2.protocol === "https:" ? httpsRequest : httpRequest)(url2, {
20896
21013
  method,
20897
21014
  ca,
20898
21015
  rejectUnauthorized: true,
20899
- signal: controller.signal,
20900
- headers: { "x-artium-cli-version": cliVersion, ...token ? { authorization: `Bearer ${token}` } : {}, ...body ? { "content-type": "application/json" } : {} }
21016
+ signal: AbortSignal.any([controller.signal, this.cancellation.signal]),
21017
+ headers: { ...headers, "x-artium-cli-version": cliVersion, ...token ? { authorization: `Bearer ${token}` } : {}, ...body ? { "content-type": "application/json" } : {} }
20901
21018
  }, (response) => {
20902
21019
  if (response.statusCode >= 300 && response.statusCode < 400) {
20903
21020
  response.destroy();
@@ -20909,7 +21026,7 @@ var Client = class {
20909
21026
  response.on("error", () => reject(new ArtiumError("SERVER_UNAVAILABLE")));
20910
21027
  response.on("end", () => {
20911
21028
  try {
20912
- resolve4({
21029
+ resolve5({
20913
21030
  status: response.statusCode,
20914
21031
  body: bytes ? Buffer.concat(chunks) : response.statusCode === 204 ? void 0 : JSON.parse(Buffer.concat(chunks).toString("utf8")),
20915
21032
  serverVersion: typeof response.headers["x-artium-cli-version"] === "string" ? response.headers["x-artium-cli-version"] : void 0
@@ -20973,6 +21090,70 @@ var Client = class {
20973
21090
  if (previous) this.reload(previous);
20974
21091
  return result;
20975
21092
  }
21093
+ // Transfers never refresh/replay a request, including an ambiguous execute or
21094
+ // a retrieval whose response was interrupted before headers arrived.
21095
+ async transferSession(work) {
21096
+ if (this.managed) return this.managed.once(work);
21097
+ const session = this.sessions.read();
21098
+ if (!session) throw new ArtiumError("TOKEN_INVALID");
21099
+ if (session.status !== "authenticated") throw new ArtiumError("PASSWORD_CHANGE_REQUIRED");
21100
+ return work(session.accessToken);
21101
+ }
21102
+ async artifact(path, input2, token, ticket, expiresAt, consume) {
21103
+ let ca;
21104
+ try {
21105
+ if (this.caFile) ca = [...rootCertificates, readFileSync2(this.caFile, "utf8")];
21106
+ } catch {
21107
+ throw new ArtiumError("CLIENT_CONFIG_INVALID");
21108
+ }
21109
+ const url2 = new URL(`${this.serverUrl}${path}`);
21110
+ const controller = new AbortController();
21111
+ const signal = AbortSignal.any([controller.signal, this.cancellation.signal]);
21112
+ const deadline = setTimeout(() => controller.abort(), Math.max(0, expiresAt - Date.now()));
21113
+ try {
21114
+ await new Promise((resolve5, reject) => {
21115
+ let incoming;
21116
+ const request = (url2.protocol === "https:" ? httpsRequest : httpRequest)(url2, {
21117
+ method: "POST",
21118
+ ca,
21119
+ rejectUnauthorized: true,
21120
+ signal,
21121
+ headers: {
21122
+ authorization: `Bearer ${token}`,
21123
+ "x-artium-cli-version": cliVersion,
21124
+ "x-artium-reauth-ticket": ticket,
21125
+ "accept-encoding": "identity",
21126
+ "content-type": "application/json"
21127
+ }
21128
+ }, (response) => {
21129
+ incoming = response;
21130
+ void (async () => {
21131
+ if (response.statusCode !== 200) {
21132
+ const chunks = [];
21133
+ let size = 0;
21134
+ for await (const chunk of response) {
21135
+ size += chunk.length;
21136
+ if (size > 64 * 1024) throw new ArtiumError("INVALID_RESPONSE");
21137
+ chunks.push(chunk);
21138
+ }
21139
+ const failure2 = errorResponseSchema.safeParse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
21140
+ if (failure2.success) throw new ArtiumError(failure2.data.error.code, failure2.data.error.message, failure2.data.error.next);
21141
+ throw new ArtiumError("INVALID_RESPONSE");
21142
+ }
21143
+ await consume(response, signal);
21144
+ })().then(resolve5, reject).finally(() => response.destroy());
21145
+ });
21146
+ request.on("error", () => {
21147
+ const error62 = new ArtiumError("SERVER_UNAVAILABLE");
21148
+ if (incoming) incoming.destroy(error62);
21149
+ else reject(error62);
21150
+ });
21151
+ request.end(JSON.stringify(input2));
21152
+ });
21153
+ } finally {
21154
+ clearTimeout(deadline);
21155
+ }
21156
+ }
20976
21157
  // The caller holds the session lock, including the reload after acquisition.
20977
21158
  async refresh(previous) {
20978
21159
  const current = this.reload(previous);
@@ -21021,13 +21202,13 @@ async function readPassword(stdin, environmentVariable) {
21021
21202
  process.stdin.setRawMode(true);
21022
21203
  process.stdin.resume();
21023
21204
  try {
21024
- return await new Promise((resolve4, reject) => {
21205
+ return await new Promise((resolve5, reject) => {
21025
21206
  let password = "";
21026
21207
  const keypress = (text2, key) => {
21027
21208
  if (key.name === "return" || key.ctrl && key.name === "c") {
21028
21209
  process.stdin.off("keypress", keypress);
21029
21210
  if (key.ctrl) reject(new ArtiumError("CANCELLED"));
21030
- else resolve4(password);
21211
+ else resolve5(password);
21031
21212
  } else if (key.name === "backspace") password = Array.from(password).slice(0, -1).join("");
21032
21213
  else if (text2 && !key.ctrl && !new RegExp("\\p{Cc}", "u").test(text2)) password += text2;
21033
21214
  };
@@ -41174,7 +41355,11 @@ var hybrid_axi_default = {
41174
41355
  nativeWriteSettings: {
41175
41356
  supportedOperations: [
41176
41357
  "convertQuickScan",
41358
+ "hash-download-bin",
41177
41359
  "huntingMatches",
41360
+ "report-download-bin",
41361
+ "report-download-dropped-files",
41362
+ "report-download-memory",
41178
41363
  "submitDroppedFile",
41179
41364
  "submitFile",
41180
41365
  "submitUrl",
@@ -41206,7 +41391,7 @@ var hybrid_axi_default = {
41206
41391
  inputs: {
41207
41392
  stdin: "body-file - only",
41208
41393
  files: "uploaded leaves only",
41209
- downloads: "not available through artium; use hybrid-axi locally"
41394
+ downloads: "artium-axi download only; authenticated one-shot CLI verified save"
41210
41395
  },
41211
41396
  nativeWrites: "native preview, operation allow-list, execute, exact confirm and durable journal"
41212
41397
  },
@@ -41229,7 +41414,7 @@ var hybrid_axi_default = {
41229
41414
  example: [
41230
41415
  "quota"
41231
41416
  ],
41232
- help: "hybrid-axi quota\nArtium manages the API key and fixed Hybrid Analysis origin. Submissions, YARA mutations and consuming matches are off by default; administrator-enabled operations still require native preview, --execute, exact --confirm and journal. Quick scans transmit the supplied sample or URL as read exceptions. File flags accept uploaded leaves; body-file also accepts explicit stdin (-). Downloads are not available through artium; use hybrid-axi locally.\n"
41417
+ help: "hybrid-axi quota\nArtium manages the API key and fixed Hybrid Analysis origin. Submissions, YARA mutations, consuming matches and the four reviewed archive downloads are off by default and selected independently; administrator-enabled operations still require native preview, --execute, exact --confirm and journal. Quick scans transmit the supplied sample or URL as read exceptions. File flags accept uploaded leaves; body-file also accepts explicit stdin (-). Use artium-axi download --help for authenticated one-shot CLI verified saving and live-malware warnings.\n"
41233
41418
  },
41234
41419
  suggestionFields: [
41235
41420
  "help"
@@ -43448,7 +43633,7 @@ function managedFlagRanges(command, tool) {
43448
43633
  function hostedHelp(help, tool, flags, commands2) {
43449
43634
  if (tool === "hybrid-axi") {
43450
43635
  help = help.split("\n").filter((line) => !/HYBRID_AXI_|SecretRefs|hybrid-axi (?:setup|config|profile)|--(?:config|profile|origin|output)\b/.test(line)).join("\n");
43451
- const guidance = "Artium manages the API key and fixed origin. Submissions, YARA mutations and consuming matches require administrator-enabled operations, native preview, execute, exact confirmation and journal. Quick scans transmit the supplied sample or URL. Downloads are not available through artium; use hybrid-axi locally.";
43636
+ const guidance = "Artium manages the API key and fixed origin. Submissions, YARA mutations, consuming matches and four reviewed archive downloads require independently selected administrator-enabled operations, native preview, execute, exact confirmation and journal. Quick scans transmit the supplied sample or URL. Use artium-axi download --help for authenticated one-shot CLI verified saving and live-malware warnings.";
43452
43637
  if (commands2 && !help.includes(guidance)) help = help.trimEnd() + "\n" + guidance + "\n";
43453
43638
  }
43454
43639
  if (tool === "tenable-axi") {
@@ -43700,12 +43885,19 @@ async function doctor(createClient = (deadline) => new Client(process.env, void
43700
43885
  try {
43701
43886
  const parsed = toolsResponseSchema.safeParse(await client.authenticated("/v1/tools", "GET"));
43702
43887
  if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
43703
- for (const tool of parsed.data.tools) line(
43704
- tool.name,
43705
- tool.status === "ok",
43706
- tool.status,
43707
- new ArtiumError(tool.status === "unconfigured" ? "CREDENTIAL_NOT_CONFIGURED" : tool.status === "error" ? "TOOL_FAILED" : tool.status === "disabled" ? "TOOL_DISABLED" : "SERVER_UNAVAILABLE")
43708
- );
43888
+ for (const tool of parsed.data.tools) {
43889
+ if (tool.status === "checking") {
43890
+ write(`pass: ${tool.name} (checking)
43891
+ `);
43892
+ continue;
43893
+ }
43894
+ line(
43895
+ tool.name,
43896
+ tool.status === "ok",
43897
+ tool.status,
43898
+ new ArtiumError(tool.status === "unconfigured" ? "CREDENTIAL_NOT_CONFIGURED" : tool.status === "error" ? "TOOL_FAILED" : tool.status === "disabled" ? "TOOL_DISABLED" : "SERVER_UNAVAILABLE")
43899
+ );
43900
+ }
43709
43901
  } catch (error62) {
43710
43902
  const cause = error62 instanceof ArtiumError ? error62 : new ArtiumError("INVALID_RESPONSE");
43711
43903
  line("tools", false, cause.code, cause);
@@ -43716,7 +43908,7 @@ async function doctor(createClient = (deadline) => new Client(process.env, void
43716
43908
 
43717
43909
  // src/cli/help.ts
43718
43910
  var commands = {
43719
- home: { usage: "artium-axi [tools [show <tool>]|whoami|login|passwd|logout|doctor|setup|update|<tool> <args>]", purpose: "Show your identity, roles and available tools.", flags: ["--version (default false; print the client package version and exit)"], example: "artium-axi" },
43911
+ home: { usage: "artium-axi [tools [show <tool>]|whoami|login|passwd|logout|doctor|setup|update|download|<tool> <args>]", purpose: "Show your identity, roles and available tools.", flags: ["--version (default false; print the client package version and exit)"], example: "artium-axi" },
43720
43912
  setup: {
43721
43913
  usage: "artium-axi setup --host <claude|codex|opencode|pi> [--hook] [--force]",
43722
43914
  purpose: "Install skills for your signed-in account\u2019s granted tools and an optional Claude Code session hook.",
@@ -43741,6 +43933,24 @@ var commands = {
43741
43933
  logout: { usage: "artium-axi logout", purpose: "Remove local tokens and revoke the server session.", flags: [], example: "artium-axi logout" },
43742
43934
  doctor: { usage: "artium-axi doctor", purpose: "Check the server, certificate, token, CLI version and cached tool health.", flags: [], example: "artium-axi doctor" },
43743
43935
  update: { usage: "artium-axi update", purpose: "Download and verify the server\u2019s client, replace this file and keep a .bak backup. npm installations must use npm install -g @knowttl/artium-axi.", flags: [], example: "artium-axi update" },
43936
+ download: {
43937
+ usage: "artium-axi download --operation <operation> --selector <hash|report-id> --type <bin|dropped-files|memory> --destination <absolute-new-path> [--transfer <id> --execute --confirm <exact-confirm>] [--password-stdin|--sso]",
43938
+ purpose: "Preview or explicitly save a hosted Hybrid Analysis archive. Operations remain unavailable until adapter enablement.",
43939
+ flags: [
43940
+ "--operation <hash-download-bin|report-download-bin|report-download-dropped-files|report-download-memory> (required)",
43941
+ "--selector <hash|report-id> (required)",
43942
+ "--type <bin|dropped-files|memory> (required)",
43943
+ "--destination <absolute-new-path> (required; Linux private 0700 directory, no symlink components or overwrite)",
43944
+ "--transfer <id> (required for execution; preview transfer ID)",
43945
+ "--execute (default false)",
43946
+ "--confirm <exact-confirm> (required for execution; unchanged native preview string)",
43947
+ "--password-stdin (default false; otherwise terminal password)",
43948
+ "--sso (default false; browser identity confirmation)",
43949
+ transferWarning,
43950
+ `Limit ${transferLimits.artifactBytes / 1024 ** 2} MiB; ${transferLimits.reservationsPerUser} reservation per user, ${transferLimits.reservations} globally, ${transferLimits.reservedBytes / 1024 ** 3} GiB total artifact capacity; absolute expiry ${transferLimits.lifetimeMs / 6e4} minutes after preview preparation, including generation and retrieval. No resume or automatic retry. Missing receipt is unconfirmed.`
43951
+ ],
43952
+ example: "artium-axi download --operation report-download-bin --selector synthetic-report --type bin --destination /controlled/artifacts/new-archive.bin --sso"
43953
+ },
43744
43954
  "falcon-axi": {
43745
43955
  usage: "artium-axi falcon-axi [<args>]",
43746
43956
  purpose: "Run Falcon commands with your managed credentials.",
@@ -43780,7 +43990,7 @@ var commands = {
43780
43990
  "hybrid-axi": {
43781
43991
  usage: "artium-axi hybrid-axi [<args>]",
43782
43992
  purpose: "Investigate Hybrid Analysis with a managed API key.",
43783
- flags: ["Submissions, YARA mutations and consuming matches are off by default and require administrator-enabled operations, preview, --execute and exact --confirm. Downloads are unavailable through Artium."],
43993
+ flags: ["Submissions, YARA mutations, consuming matches and four reviewed archive downloads are off by default and selected independently by an administrator. Native preview, --execute and exact --confirm remain required. Use artium-axi download --help for authenticated CLI verified saving.", transferWarning],
43784
43994
  example: "artium-axi hybrid-axi quota"
43785
43995
  },
43786
43996
  "az-axi": {
@@ -43853,7 +44063,7 @@ function validateArguments(args, prefixHelp) {
43853
44063
  start = 2;
43854
44064
  }
43855
44065
  const entry = commands[level];
43856
- const validFlags = ["--help", ...entry.flags.map((flag) => flag.split(" ")[0])];
44066
+ const validFlags = ["--help", ...entry.flags.filter((flag) => flag.startsWith("--")).map((flag) => flag.split(" ")[0])];
43857
44067
  const choices = `Valid flags for ${level}: ${validFlags.join(", ")}.` + (level === "tools" ? " Valid subcommands: show <tool>." : level === "tools show" ? ` Valid tools: ${toolSchema.options.join(", ")}.` : "");
43858
44068
  let tool = false;
43859
44069
  const seen = /* @__PURE__ */ new Set();
@@ -43865,8 +44075,9 @@ function validateArguments(args, prefixHelp) {
43865
44075
  } else {
43866
44076
  if (!validFlags.includes(token) || seen.has(token)) fail(token, level, choices);
43867
44077
  seen.add(token);
43868
- if (token === "--username" || token === "--host") {
44078
+ if (token === "--username" || token === "--host" || command === "download" && ["--operation", "--selector", "--type", "--destination", "--transfer", "--confirm"].includes(token)) {
43869
44079
  const value = args[index + 1];
44080
+ if (!value && command === "download") fail(value, `${level} ${token}`, `Supply a value for ${token}. Use ${entry.usage}. ${choices}`);
43870
44081
  if (!value) fail(value, `${level} ${token}`, `Supply ${token} ${token === "--host" ? "<claude|codex|opencode|pi>" : "<username>"}. ${choices}`);
43871
44082
  if (token === "--host" && !["claude", "codex", "opencode", "pi"].includes(value)) fail(value, `${level} --host`, `Valid hosts: claude, codex, opencode, pi. Use ${entry.usage}.`);
43872
44083
  index++;
@@ -43876,6 +44087,7 @@ function validateArguments(args, prefixHelp) {
43876
44087
  if (level === "tools show" && !tool && !seen.has("--help") && !prefixHelp) fail(void 0, level, choices);
43877
44088
  }
43878
44089
  function helpCommand(args, prefixHelp = false) {
44090
+ if (args[0] === "download" && args.includes("--help")) return "download";
43879
44091
  if (args[0] === "setup" && args.includes("--help")) {
43880
44092
  const path2 = args.filter((arg) => arg !== "--help");
43881
44093
  for (let index = 1; index < path2.length; index++) {
@@ -44279,6 +44491,10 @@ var ManagedSession = class _ManagedSession {
44279
44491
  checkServer(serverUrl) {
44280
44492
  if (this.session && this.session.serverUrl.replace(/\/$/, "") !== serverUrl) throw new ArtiumError("SESSION_SERVER_MISMATCH");
44281
44493
  }
44494
+ async once(request) {
44495
+ if (!this.session) throw new ArtiumError("TOKEN_INVALID");
44496
+ return request(this.session.accessToken);
44497
+ }
44282
44498
  async authenticated(request, refresh) {
44283
44499
  if (!this.session) throw new ArtiumError("TOKEN_INVALID");
44284
44500
  try {
@@ -44301,9 +44517,9 @@ var ManagedSession = class _ManagedSession {
44301
44517
  if (session.status === "authenticated") this.protect(session.accessToken, session.refreshToken);
44302
44518
  else this.protect(session.passwordChangeToken);
44303
44519
  try {
44304
- await new Promise((resolve4, reject) => {
44520
+ await new Promise((resolve5, reject) => {
44305
44521
  this.channel.write(`${JSON.stringify({ serverUrl, ...session })}
44306
- `, (error62) => error62 ? reject(error62) : resolve4());
44522
+ `, (error62) => error62 ? reject(error62) : resolve5());
44307
44523
  });
44308
44524
  } catch {
44309
44525
  throw new ArtiumError(
@@ -44316,6 +44532,12 @@ var ManagedSession = class _ManagedSession {
44316
44532
  write(stream, text2) {
44317
44533
  this.output[stream] += text2;
44318
44534
  }
44535
+ // Identity approval and pre-execution notices must reach the caller before
44536
+ // the pending operation. Tool output retains the buffered flush contract.
44537
+ notice(text2) {
44538
+ if (new SecretScanner(this.secrets).scan("stdout", Buffer.from(text2))) throw new ArtiumError("SECRET_IN_OUTPUT");
44539
+ process.stdout.write(text2);
44540
+ }
44319
44541
  flush() {
44320
44542
  this.channel.destroy();
44321
44543
  const scanner = new SecretScanner(this.secrets);
@@ -44357,6 +44579,255 @@ async function update() {
44357
44579
  return "status: updated\nnext: artium-axi --version\n";
44358
44580
  }
44359
44581
 
44582
+ // src/cli/download.ts
44583
+ import { createHash as createHash4 } from "node:crypto";
44584
+ import { setTimeout as delay } from "node:timers/promises";
44585
+
44586
+ // src/cli/artifact-save.ts
44587
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
44588
+ import { constants } from "node:fs";
44589
+ import { link, open as open2, unlink } from "node:fs/promises";
44590
+ import { basename as basename2, dirname as dirname4, isAbsolute, resolve as resolve3 } from "node:path";
44591
+ async function privateParent(destination) {
44592
+ let parent = await open2("/", constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
44593
+ try {
44594
+ for (const component of dirname4(destination).split("/").filter(Boolean)) {
44595
+ const child = await open2(`/proc/self/fd/${parent.fd}/${component}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
44596
+ await parent.close();
44597
+ parent = child;
44598
+ }
44599
+ const info = await parent.stat();
44600
+ if (info.uid !== process.getuid?.() || (info.mode & 511) !== 448) throw new ArtiumError("ARTIFACT_SAVE_FAILED");
44601
+ return parent;
44602
+ } catch (error62) {
44603
+ await parent.close();
44604
+ throw error62;
44605
+ }
44606
+ }
44607
+ async function artifactDestination(destination) {
44608
+ if (process.platform !== "linux" || !isAbsolute(destination) || resolve3(destination) !== destination || basename2(destination) === "") {
44609
+ throw new ArtiumError("ARTIFACT_SAVE_FAILED");
44610
+ }
44611
+ let parent;
44612
+ try {
44613
+ parent = await privateParent(destination);
44614
+ } catch {
44615
+ throw new ArtiumError("ARTIFACT_SAVE_FAILED");
44616
+ }
44617
+ let temporary;
44618
+ let file2;
44619
+ let closed = false;
44620
+ try {
44621
+ const info = await parent.stat();
44622
+ const target = `/proc/self/fd/${parent.fd}/${basename2(destination)}`;
44623
+ try {
44624
+ const existing = await open2(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
44625
+ await existing.close();
44626
+ throw new ArtiumError("ARTIFACT_SAVE_FAILED");
44627
+ } catch (error62) {
44628
+ if (error62.code !== "ENOENT") throw error62;
44629
+ }
44630
+ return {
44631
+ async save(response, archive, signal) {
44632
+ signal.throwIfAborted();
44633
+ if (response.headers["content-type"] !== "application/octet-stream" || response.headers["content-length"] !== String(archive.archiveBytes) || response.headers["x-artium-archive-sha256"] !== archive.archiveSha256 || response.headers["content-encoding"] || response.headers["content-range"]) throw new ArtiumError("INVALID_RESPONSE");
44634
+ temporary = `/proc/self/fd/${parent.fd}/.artium-${randomUUID4()}.tmp`;
44635
+ file2 = await open2(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
44636
+ const hash2 = createHash3("sha256");
44637
+ let size = 0;
44638
+ for await (const chunk of response) {
44639
+ signal.throwIfAborted();
44640
+ size += chunk.length;
44641
+ if (size > archive.archiveBytes) throw new ArtiumError("INVALID_RESPONSE");
44642
+ hash2.update(chunk);
44643
+ await file2.writeFile(chunk);
44644
+ }
44645
+ if (size !== archive.archiveBytes || hash2.digest("hex") !== archive.archiveSha256) throw new ArtiumError("INVALID_RESPONSE");
44646
+ await file2.sync();
44647
+ await file2.close();
44648
+ file2 = void 0;
44649
+ const current = await privateParent(destination);
44650
+ try {
44651
+ const actual = await current.stat();
44652
+ if (info.dev !== actual.dev || info.ino !== actual.ino) throw new ArtiumError("ARTIFACT_SAVE_FAILED");
44653
+ } finally {
44654
+ await current.close();
44655
+ }
44656
+ signal.throwIfAborted();
44657
+ await link(temporary, target);
44658
+ },
44659
+ async close() {
44660
+ if (closed) return;
44661
+ closed = true;
44662
+ try {
44663
+ await file2?.close();
44664
+ } finally {
44665
+ try {
44666
+ if (temporary) await unlink(temporary);
44667
+ } finally {
44668
+ await parent.close();
44669
+ }
44670
+ }
44671
+ }
44672
+ };
44673
+ } catch {
44674
+ await parent.close();
44675
+ throw new ArtiumError("ARTIFACT_SAVE_FAILED");
44676
+ }
44677
+ }
44678
+
44679
+ // src/cli/download.ts
44680
+ async function download(args, output3, managed2) {
44681
+ const announce = (value) => {
44682
+ const text2 = encode3(value) + "\n";
44683
+ if (managed2) managed2.notice(text2);
44684
+ else output3(text2);
44685
+ };
44686
+ announce({ warning: transferWarning });
44687
+ const flags = /* @__PURE__ */ new Map();
44688
+ for (let index = 0; index < args.length; index++) {
44689
+ const name = args[index];
44690
+ if (["--execute", "--password-stdin", "--sso"].includes(name)) flags.set(name, "true");
44691
+ else flags.set(name, args[++index]);
44692
+ }
44693
+ const destination = flags.get("--destination") ?? "";
44694
+ const input2 = transferPrepareSchema.safeParse({
44695
+ selection: {
44696
+ operation: flags.get("--operation"),
44697
+ selector: flags.get("--selector"),
44698
+ type: flags.get("--type")
44699
+ },
44700
+ targetDigest: createHash4("sha256").update(destination).digest("hex")
44701
+ });
44702
+ if (!input2.success || !destination || flags.has("--sso") && flags.has("--password-stdin")) throw new ArtiumError("INVALID_INPUT");
44703
+ const execute = flags.has("--execute");
44704
+ if (execute !== flags.has("--transfer") || execute !== flags.has("--confirm")) throw new ArtiumError("INVALID_INPUT");
44705
+ let execution;
44706
+ if (execute) {
44707
+ const parsed = transferExecuteSchema.safeParse({
44708
+ ...input2.data,
44709
+ transferId: flags.get("--transfer"),
44710
+ execute: true,
44711
+ confirm: flags.get("--confirm")
44712
+ });
44713
+ if (!parsed.success) throw new ArtiumError("INVALID_INPUT");
44714
+ execution = parsed.data;
44715
+ }
44716
+ const client = new Client(process.env, managed2);
44717
+ const local = await artifactDestination(destination);
44718
+ const interrupt = () => client.cancel();
44719
+ process.once("SIGINT", interrupt);
44720
+ process.once("SIGTERM", interrupt);
44721
+ try {
44722
+ await client.transferSession(async (token) => {
44723
+ let proof;
44724
+ if (flags.has("--sso")) {
44725
+ const start = deviceStartResponseSchema.parse(await client.request("/v1/auth/reauth", "POST", {}, token));
44726
+ if (start.verificationUri !== client.serverUrl + "/device") throw new ArtiumError("INVALID_RESPONSE");
44727
+ managed2?.protect(start.deviceCode);
44728
+ announce({ status: "approval-required", open: start.verificationUri, code: start.userCode, expiresAt: start.expiresAt });
44729
+ while (Date.now() < start.expiresAt) {
44730
+ try {
44731
+ proof = ssoReauthResponseSchema.parse(await client.request("/v1/auth/reauth", "POST", { deviceCode: start.deviceCode }, token));
44732
+ if (reauthResponseSchema.safeParse(proof).success) break;
44733
+ } catch (error62) {
44734
+ if (!(error62 instanceof ArtiumError) || error62.code !== "DEVICE_POLL_LIMITED") throw error62;
44735
+ }
44736
+ await delay(Math.min(start.intervalMs, Math.max(0, start.expiresAt - Date.now())));
44737
+ }
44738
+ } else {
44739
+ const password = await readPassword(flags.has("--password-stdin"));
44740
+ managed2?.protect(password);
44741
+ proof = await client.request("/v1/auth/reauth", "POST", { password }, token);
44742
+ }
44743
+ const { ticket } = reauthResponseSchema.parse(proof);
44744
+ managed2?.protect(ticket);
44745
+ const request = (path, body) => client.request(
44746
+ path,
44747
+ "POST",
44748
+ body,
44749
+ token,
44750
+ false,
44751
+ { "x-artium-reauth-ticket": ticket }
44752
+ );
44753
+ if (!execution) {
44754
+ const preview = transferPreviewSchema.parse(await request(transferRoutes.prepare.path, input2.data));
44755
+ if (preview.targetDigest !== input2.data.targetDigest) throw new ArtiumError("INVALID_RESPONSE");
44756
+ announce({
44757
+ ...input2.data.selection,
44758
+ destination,
44759
+ limits: transferLimits,
44760
+ ...preview,
44761
+ saveOutcome: "unconfirmed",
44762
+ next: "Repeat this exact selection and destination with --transfer <transferId> --execute --confirm <exact confirm> and recent identity confirmation. No automatic retries."
44763
+ });
44764
+ return;
44765
+ }
44766
+ const retrieval = { ...input2.data, transferId: execution.transferId };
44767
+ let archive;
44768
+ let saved = false;
44769
+ let receipt = "unconfirmed";
44770
+ try {
44771
+ const pending = transferStatusSchema.parse(await request(transferRoutes.status.path, retrieval));
44772
+ if (pending.transferId !== execution.transferId || pending.outcome !== "preview" || pending.archive !== null) throw new ArtiumError("INVALID_RESPONSE");
44773
+ announce({
44774
+ ...input2.data.selection,
44775
+ destination,
44776
+ limits: transferLimits,
44777
+ expiresAt: pending.expiresAt,
44778
+ saveOutcome: "unconfirmed"
44779
+ });
44780
+ const status = transferStatusSchema.parse(await request(transferRoutes.execute.path, execution));
44781
+ if (status.transferId !== execution.transferId || status.expiresAt !== pending.expiresAt || status.outcome !== "ready" || !status.archive) throw new ArtiumError("INVALID_RESPONSE");
44782
+ archive = status.archive;
44783
+ await client.artifact(
44784
+ transferRoutes.retrieve.path,
44785
+ retrieval,
44786
+ token,
44787
+ ticket,
44788
+ status.expiresAt,
44789
+ (response, signal) => local.save(response, archive, signal)
44790
+ );
44791
+ saved = true;
44792
+ await local.close();
44793
+ try {
44794
+ await request(transferRoutes.receipt.path, { ...retrieval, archive, saveOutcome: "verified-save" });
44795
+ receipt = "confirmed";
44796
+ } catch {
44797
+ }
44798
+ } catch (error62) {
44799
+ if (archive && !saved) {
44800
+ try {
44801
+ await request(transferRoutes.receipt.path, { ...retrieval, archive, saveOutcome: "save-failed" });
44802
+ } catch {
44803
+ }
44804
+ }
44805
+ try {
44806
+ await request(transferRoutes.cancel.path, retrieval);
44807
+ } catch {
44808
+ }
44809
+ throw error62;
44810
+ } finally {
44811
+ output3(encode3({
44812
+ ...input2.data.selection,
44813
+ destination,
44814
+ archiveBytes: archive?.archiveBytes ?? null,
44815
+ archiveSha256: archive?.archiveSha256 ?? null,
44816
+ saveOutcome: saved ? "verified-save" : "save-failed",
44817
+ receipt
44818
+ }) + "\n");
44819
+ }
44820
+ });
44821
+ } catch (error62) {
44822
+ if (error62 instanceof ZodError) throw new ArtiumError("INVALID_RESPONSE");
44823
+ throw error62;
44824
+ } finally {
44825
+ process.off("SIGINT", interrupt);
44826
+ process.off("SIGTERM", interrupt);
44827
+ await local.close();
44828
+ }
44829
+ }
44830
+
44360
44831
  // src/cli/main.ts
44361
44832
  var managed;
44362
44833
  for (const stream of [process.stdout, process.stderr]) {
@@ -44406,6 +44877,10 @@ async function main(args) {
44406
44877
  output2("stdout", await update());
44407
44878
  return;
44408
44879
  }
44880
+ if (command === "download") {
44881
+ await download(args.slice(1), (text2) => output2("stdout", text2), managed);
44882
+ return;
44883
+ }
44409
44884
  if (command === "setup") {
44410
44885
  output2("stdout", await setup(args.slice(1)));
44411
44886
  return;
@@ -44525,9 +45000,9 @@ remoteRevocation: ${verified ? "verified" : "unverified"}
44525
45000
  if (home) {
44526
45001
  const tools = toolsResponseSchema.safeParse(await client.authenticated("/v1/tools", "GET", void 0, homeSession));
44527
45002
  if (!tools.success) throw new ArtiumError("INVALID_RESPONSE");
44528
- const executable = resolve3(process.argv[1]);
45003
+ const executable = resolve4(process.argv[1]);
44529
45004
  const local = relative(homedir3(), executable);
44530
- const bin = local && !isAbsolute(local) && !local.startsWith(`..${sep}`) && local !== ".." ? `~${sep}${local}` : executable;
45005
+ const bin = local && !isAbsolute2(local) && !local.startsWith(`..${sep}`) && local !== ".." ? `~${sep}${local}` : executable;
44531
45006
  view = `bin: ${JSON.stringify(bin)}
44532
45007
  description: Run managed upstream tools through one local sign-in.
44533
45008
  ${view}${toolsView(tools.data.tools)}`;
@@ -44554,16 +45029,20 @@ ${view}${toolsView(tools.data.tools)}`;
44554
45029
  }
44555
45030
  }
44556
45031
  function printError(response) {
45032
+ if (process.argv[2] === "download" || process.argv[2] === "--managed-session" && process.argv[3] === "download") {
45033
+ output2("stdout", `warning: ${JSON.stringify(transferWarning)}
45034
+ `);
45035
+ }
44557
45036
  output2("stdout", `error:
44558
45037
  ${Object.entries(response.error).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`).join("\n")}
44559
45038
  `);
44560
45039
  process.exitCode = errors[response.error.code].exitCode;
44561
45040
  }
44562
- main(process.argv.slice(2)).catch((error62) => printError(errorResponse(error62 instanceof ArtiumError ? error62 : new ArtiumError("INTERNAL_ERROR"), randomUUID4()))).then(() => {
45041
+ main(process.argv.slice(2)).catch((error62) => printError(errorResponse(error62 instanceof ArtiumError ? error62 : new ArtiumError("INTERNAL_ERROR"), randomUUID5()))).then(() => {
44563
45042
  try {
44564
45043
  managed?.flush();
44565
45044
  } catch {
44566
45045
  managed = void 0;
44567
- printError(errorResponse(new ArtiumError("SECRET_IN_OUTPUT"), randomUUID4()));
45046
+ printError(errorResponse(new ArtiumError("SECRET_IN_OUTPUT"), randomUUID5()));
44568
45047
  }
44569
45048
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowttl/artium-axi",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "Client CLI and embedded skill for the Artium managed tools server.",
5
5
  "license": "MIT",
6
6
  "type": "module",