@knowttl/artium-axi 0.0.13 → 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",
@@ -20051,13 +20164,11 @@ var falconRtrSettingsSchema = external_exports.strictObject({
20051
20164
  commands: external_exports.array(external_exports.enum([
20052
20165
  "cat",
20053
20166
  "cd",
20054
- "clear",
20055
20167
  "csrutil",
20056
20168
  "env",
20057
20169
  "eventlog",
20058
20170
  "filehash",
20059
20171
  "getsid",
20060
- "history",
20061
20172
  "ifconfig",
20062
20173
  "ipconfig",
20063
20174
  "ls",
@@ -20119,7 +20230,7 @@ var diagnosticSchema = external_exports.strictObject({
20119
20230
  credentialRevision: external_exports.number().int().nullable().optional()
20120
20231
  });
20121
20232
  var healthSchema = external_exports.strictObject({
20122
- status: external_exports.enum(["unconfigured", "unknown", "ok", "error", "disabled"]),
20233
+ status: external_exports.enum(["unconfigured", "unknown", "checking", "ok", "error", "disabled"]),
20123
20234
  diagnostic: diagnosticSchema.optional(),
20124
20235
  readOnly: external_exports.boolean().default(true),
20125
20236
  checkedAt: external_exports.number().int().nullable(),
@@ -20577,7 +20688,7 @@ var toolsResponseSchema = external_exports.strictObject({
20577
20688
  name: toolSchema,
20578
20689
  version: external_exports.string(),
20579
20690
  readOnly: external_exports.boolean().default(true),
20580
- status: external_exports.enum(["unconfigured", "unknown", "ok", "error", "disabled"])
20691
+ status: external_exports.enum(["unconfigured", "unknown", "checking", "ok", "error", "disabled"])
20581
20692
  }))
20582
20693
  });
20583
20694
  var inputFileSchema = external_exports.strictObject({
@@ -20634,7 +20745,7 @@ import { resolve } from "node:path";
20634
20745
  // package.json
20635
20746
  var package_default = {
20636
20747
  name: "artium-axi",
20637
- version: "0.0.13",
20748
+ version: "0.0.15",
20638
20749
  private: true,
20639
20750
  license: "MIT",
20640
20751
  type: "module",
@@ -20823,8 +20934,8 @@ var SessionStore = class {
20823
20934
  while (true) {
20824
20935
  const watcher = watch(this.directory);
20825
20936
  let timer;
20826
- const changed = new Promise((resolve4, reject) => {
20827
- watcher.once("change", () => resolve4());
20937
+ const changed = new Promise((resolve5, reject) => {
20938
+ watcher.once("change", () => resolve5());
20828
20939
  watcher.once("error", () => reject(new ArtiumError("SESSION_STORAGE_UNAVAILABLE")));
20829
20940
  timer = setTimeout(() => reject(new ArtiumError("SESSION_BUSY")), Math.max(0, deadline - Date.now()));
20830
20941
  });
@@ -20872,6 +20983,10 @@ var Client = class {
20872
20983
  }
20873
20984
  managed;
20874
20985
  requestDeadlineMs;
20986
+ cancellation = new AbortController();
20987
+ cancel() {
20988
+ this.cancellation.abort();
20989
+ }
20875
20990
  serverUrl;
20876
20991
  serverUrlSource;
20877
20992
  caFile;
@@ -20881,7 +20996,7 @@ var Client = class {
20881
20996
  return this.storedSessions;
20882
20997
  }
20883
20998
  certificateVerified = false;
20884
- async request(path, method, input2, token, bytes = false) {
20999
+ async request(path, method, input2, token, bytes = false, headers = {}) {
20885
21000
  this.certificateVerified = false;
20886
21001
  let ca;
20887
21002
  try {
@@ -20893,13 +21008,13 @@ var Client = class {
20893
21008
  const controller = new AbortController();
20894
21009
  const deadline = setTimeout(() => controller.abort(), this.requestDeadlineMs);
20895
21010
  const url2 = new URL(`${this.serverUrl}${path}`);
20896
- const result = await new Promise((resolve4, reject) => {
21011
+ const result = await new Promise((resolve5, reject) => {
20897
21012
  const request = (url2.protocol === "https:" ? httpsRequest : httpRequest)(url2, {
20898
21013
  method,
20899
21014
  ca,
20900
21015
  rejectUnauthorized: true,
20901
- signal: controller.signal,
20902
- 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" } : {} }
20903
21018
  }, (response) => {
20904
21019
  if (response.statusCode >= 300 && response.statusCode < 400) {
20905
21020
  response.destroy();
@@ -20911,7 +21026,7 @@ var Client = class {
20911
21026
  response.on("error", () => reject(new ArtiumError("SERVER_UNAVAILABLE")));
20912
21027
  response.on("end", () => {
20913
21028
  try {
20914
- resolve4({
21029
+ resolve5({
20915
21030
  status: response.statusCode,
20916
21031
  body: bytes ? Buffer.concat(chunks) : response.statusCode === 204 ? void 0 : JSON.parse(Buffer.concat(chunks).toString("utf8")),
20917
21032
  serverVersion: typeof response.headers["x-artium-cli-version"] === "string" ? response.headers["x-artium-cli-version"] : void 0
@@ -20975,6 +21090,70 @@ var Client = class {
20975
21090
  if (previous) this.reload(previous);
20976
21091
  return result;
20977
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
+ }
20978
21157
  // The caller holds the session lock, including the reload after acquisition.
20979
21158
  async refresh(previous) {
20980
21159
  const current = this.reload(previous);
@@ -21023,13 +21202,13 @@ async function readPassword(stdin, environmentVariable) {
21023
21202
  process.stdin.setRawMode(true);
21024
21203
  process.stdin.resume();
21025
21204
  try {
21026
- return await new Promise((resolve4, reject) => {
21205
+ return await new Promise((resolve5, reject) => {
21027
21206
  let password = "";
21028
21207
  const keypress = (text2, key) => {
21029
21208
  if (key.name === "return" || key.ctrl && key.name === "c") {
21030
21209
  process.stdin.off("keypress", keypress);
21031
21210
  if (key.ctrl) reject(new ArtiumError("CANCELLED"));
21032
- else resolve4(password);
21211
+ else resolve5(password);
21033
21212
  } else if (key.name === "backspace") password = Array.from(password).slice(0, -1).join("");
21034
21213
  else if (text2 && !key.ctrl && !new RegExp("\\p{Cc}", "u").test(text2)) password += text2;
21035
21214
  };
@@ -21048,13 +21227,13 @@ var falcon_axi_default = {
21048
21227
  generatedBy: "scripts/falcon-inventory.mjs",
21049
21228
  tool: "falcon-axi",
21050
21229
  repository: "https://github.com/knowttl/falcon-axi.git",
21051
- tag: "v0.7.1",
21052
- commit: "de453560f74398d0435fa5d2beadcd3e1dc8085c",
21053
- version: "0.7.1",
21230
+ tag: "v0.7.2",
21231
+ commit: "96392a6b2afeb86d5422e3ff55e5a3d5e8af769f",
21232
+ version: "0.7.2",
21054
21233
  distribution: {
21055
- filename: "falcon_axi-0.7.1-py3-none-any.whl",
21056
- sha256: "d5862a46a63358af379101b0bf94b734cd6cb5c2717528a6a2ebe1d9e9b88f23",
21057
- provenance: "https://pypi.org/integrity/falcon-axi/0.7.1/falcon_axi-0.7.1-py3-none-any.whl/provenance"
21234
+ filename: "falcon_axi-0.7.2-py3-none-any.whl",
21235
+ sha256: "8b87c8047f782a6d390324a26179def0f77e6c7c5bf4212012d5802a249132b6",
21236
+ provenance: "https://pypi.org/integrity/falcon-axi/0.7.2/falcon_axi-0.7.2-py3-none-any.whl/provenance"
21058
21237
  },
21059
21238
  parser: {
21060
21239
  path: "falcon_axi/cli.py",
@@ -41176,7 +41355,11 @@ var hybrid_axi_default = {
41176
41355
  nativeWriteSettings: {
41177
41356
  supportedOperations: [
41178
41357
  "convertQuickScan",
41358
+ "hash-download-bin",
41179
41359
  "huntingMatches",
41360
+ "report-download-bin",
41361
+ "report-download-dropped-files",
41362
+ "report-download-memory",
41180
41363
  "submitDroppedFile",
41181
41364
  "submitFile",
41182
41365
  "submitUrl",
@@ -41208,7 +41391,7 @@ var hybrid_axi_default = {
41208
41391
  inputs: {
41209
41392
  stdin: "body-file - only",
41210
41393
  files: "uploaded leaves only",
41211
- downloads: "not available through artium; use hybrid-axi locally"
41394
+ downloads: "artium-axi download only; authenticated one-shot CLI verified save"
41212
41395
  },
41213
41396
  nativeWrites: "native preview, operation allow-list, execute, exact confirm and durable journal"
41214
41397
  },
@@ -41231,7 +41414,7 @@ var hybrid_axi_default = {
41231
41414
  example: [
41232
41415
  "quota"
41233
41416
  ],
41234
- 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"
41235
41418
  },
41236
41419
  suggestionFields: [
41237
41420
  "help"
@@ -43450,7 +43633,7 @@ function managedFlagRanges(command, tool) {
43450
43633
  function hostedHelp(help, tool, flags, commands2) {
43451
43634
  if (tool === "hybrid-axi") {
43452
43635
  help = help.split("\n").filter((line) => !/HYBRID_AXI_|SecretRefs|hybrid-axi (?:setup|config|profile)|--(?:config|profile|origin|output)\b/.test(line)).join("\n");
43453
- 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.";
43454
43637
  if (commands2 && !help.includes(guidance)) help = help.trimEnd() + "\n" + guidance + "\n";
43455
43638
  }
43456
43639
  if (tool === "tenable-axi") {
@@ -43702,12 +43885,19 @@ async function doctor(createClient = (deadline) => new Client(process.env, void
43702
43885
  try {
43703
43886
  const parsed = toolsResponseSchema.safeParse(await client.authenticated("/v1/tools", "GET"));
43704
43887
  if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
43705
- for (const tool of parsed.data.tools) line(
43706
- tool.name,
43707
- tool.status === "ok",
43708
- tool.status,
43709
- new ArtiumError(tool.status === "unconfigured" ? "CREDENTIAL_NOT_CONFIGURED" : tool.status === "error" ? "TOOL_FAILED" : tool.status === "disabled" ? "TOOL_DISABLED" : "SERVER_UNAVAILABLE")
43710
- );
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
+ }
43711
43901
  } catch (error62) {
43712
43902
  const cause = error62 instanceof ArtiumError ? error62 : new ArtiumError("INVALID_RESPONSE");
43713
43903
  line("tools", false, cause.code, cause);
@@ -43718,7 +43908,7 @@ async function doctor(createClient = (deadline) => new Client(process.env, void
43718
43908
 
43719
43909
  // src/cli/help.ts
43720
43910
  var commands = {
43721
- 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" },
43722
43912
  setup: {
43723
43913
  usage: "artium-axi setup --host <claude|codex|opencode|pi> [--hook] [--force]",
43724
43914
  purpose: "Install skills for your signed-in account\u2019s granted tools and an optional Claude Code session hook.",
@@ -43743,6 +43933,24 @@ var commands = {
43743
43933
  logout: { usage: "artium-axi logout", purpose: "Remove local tokens and revoke the server session.", flags: [], example: "artium-axi logout" },
43744
43934
  doctor: { usage: "artium-axi doctor", purpose: "Check the server, certificate, token, CLI version and cached tool health.", flags: [], example: "artium-axi doctor" },
43745
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
+ },
43746
43954
  "falcon-axi": {
43747
43955
  usage: "artium-axi falcon-axi [<args>]",
43748
43956
  purpose: "Run Falcon commands with your managed credentials.",
@@ -43782,7 +43990,7 @@ var commands = {
43782
43990
  "hybrid-axi": {
43783
43991
  usage: "artium-axi hybrid-axi [<args>]",
43784
43992
  purpose: "Investigate Hybrid Analysis with a managed API key.",
43785
- 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],
43786
43994
  example: "artium-axi hybrid-axi quota"
43787
43995
  },
43788
43996
  "az-axi": {
@@ -43855,7 +44063,7 @@ function validateArguments(args, prefixHelp) {
43855
44063
  start = 2;
43856
44064
  }
43857
44065
  const entry = commands[level];
43858
- 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])];
43859
44067
  const choices = `Valid flags for ${level}: ${validFlags.join(", ")}.` + (level === "tools" ? " Valid subcommands: show <tool>." : level === "tools show" ? ` Valid tools: ${toolSchema.options.join(", ")}.` : "");
43860
44068
  let tool = false;
43861
44069
  const seen = /* @__PURE__ */ new Set();
@@ -43867,8 +44075,9 @@ function validateArguments(args, prefixHelp) {
43867
44075
  } else {
43868
44076
  if (!validFlags.includes(token) || seen.has(token)) fail(token, level, choices);
43869
44077
  seen.add(token);
43870
- if (token === "--username" || token === "--host") {
44078
+ if (token === "--username" || token === "--host" || command === "download" && ["--operation", "--selector", "--type", "--destination", "--transfer", "--confirm"].includes(token)) {
43871
44079
  const value = args[index + 1];
44080
+ if (!value && command === "download") fail(value, `${level} ${token}`, `Supply a value for ${token}. Use ${entry.usage}. ${choices}`);
43872
44081
  if (!value) fail(value, `${level} ${token}`, `Supply ${token} ${token === "--host" ? "<claude|codex|opencode|pi>" : "<username>"}. ${choices}`);
43873
44082
  if (token === "--host" && !["claude", "codex", "opencode", "pi"].includes(value)) fail(value, `${level} --host`, `Valid hosts: claude, codex, opencode, pi. Use ${entry.usage}.`);
43874
44083
  index++;
@@ -43878,6 +44087,7 @@ function validateArguments(args, prefixHelp) {
43878
44087
  if (level === "tools show" && !tool && !seen.has("--help") && !prefixHelp) fail(void 0, level, choices);
43879
44088
  }
43880
44089
  function helpCommand(args, prefixHelp = false) {
44090
+ if (args[0] === "download" && args.includes("--help")) return "download";
43881
44091
  if (args[0] === "setup" && args.includes("--help")) {
43882
44092
  const path2 = args.filter((arg) => arg !== "--help");
43883
44093
  for (let index = 1; index < path2.length; index++) {
@@ -44281,6 +44491,10 @@ var ManagedSession = class _ManagedSession {
44281
44491
  checkServer(serverUrl) {
44282
44492
  if (this.session && this.session.serverUrl.replace(/\/$/, "") !== serverUrl) throw new ArtiumError("SESSION_SERVER_MISMATCH");
44283
44493
  }
44494
+ async once(request) {
44495
+ if (!this.session) throw new ArtiumError("TOKEN_INVALID");
44496
+ return request(this.session.accessToken);
44497
+ }
44284
44498
  async authenticated(request, refresh) {
44285
44499
  if (!this.session) throw new ArtiumError("TOKEN_INVALID");
44286
44500
  try {
@@ -44303,9 +44517,9 @@ var ManagedSession = class _ManagedSession {
44303
44517
  if (session.status === "authenticated") this.protect(session.accessToken, session.refreshToken);
44304
44518
  else this.protect(session.passwordChangeToken);
44305
44519
  try {
44306
- await new Promise((resolve4, reject) => {
44520
+ await new Promise((resolve5, reject) => {
44307
44521
  this.channel.write(`${JSON.stringify({ serverUrl, ...session })}
44308
- `, (error62) => error62 ? reject(error62) : resolve4());
44522
+ `, (error62) => error62 ? reject(error62) : resolve5());
44309
44523
  });
44310
44524
  } catch {
44311
44525
  throw new ArtiumError(
@@ -44318,6 +44532,12 @@ var ManagedSession = class _ManagedSession {
44318
44532
  write(stream, text2) {
44319
44533
  this.output[stream] += text2;
44320
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
+ }
44321
44541
  flush() {
44322
44542
  this.channel.destroy();
44323
44543
  const scanner = new SecretScanner(this.secrets);
@@ -44359,6 +44579,255 @@ async function update() {
44359
44579
  return "status: updated\nnext: artium-axi --version\n";
44360
44580
  }
44361
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
+
44362
44831
  // src/cli/main.ts
44363
44832
  var managed;
44364
44833
  for (const stream of [process.stdout, process.stderr]) {
@@ -44408,6 +44877,10 @@ async function main(args) {
44408
44877
  output2("stdout", await update());
44409
44878
  return;
44410
44879
  }
44880
+ if (command === "download") {
44881
+ await download(args.slice(1), (text2) => output2("stdout", text2), managed);
44882
+ return;
44883
+ }
44411
44884
  if (command === "setup") {
44412
44885
  output2("stdout", await setup(args.slice(1)));
44413
44886
  return;
@@ -44527,9 +45000,9 @@ remoteRevocation: ${verified ? "verified" : "unverified"}
44527
45000
  if (home) {
44528
45001
  const tools = toolsResponseSchema.safeParse(await client.authenticated("/v1/tools", "GET", void 0, homeSession));
44529
45002
  if (!tools.success) throw new ArtiumError("INVALID_RESPONSE");
44530
- const executable = resolve3(process.argv[1]);
45003
+ const executable = resolve4(process.argv[1]);
44531
45004
  const local = relative(homedir3(), executable);
44532
- 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;
44533
45006
  view = `bin: ${JSON.stringify(bin)}
44534
45007
  description: Run managed upstream tools through one local sign-in.
44535
45008
  ${view}${toolsView(tools.data.tools)}`;
@@ -44556,16 +45029,20 @@ ${view}${toolsView(tools.data.tools)}`;
44556
45029
  }
44557
45030
  }
44558
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
+ }
44559
45036
  output2("stdout", `error:
44560
45037
  ${Object.entries(response.error).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`).join("\n")}
44561
45038
  `);
44562
45039
  process.exitCode = errors[response.error.code].exitCode;
44563
45040
  }
44564
- 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(() => {
44565
45042
  try {
44566
45043
  managed?.flush();
44567
45044
  } catch {
44568
45045
  managed = void 0;
44569
- printError(errorResponse(new ArtiumError("SECRET_IN_OUTPUT"), randomUUID4()));
45046
+ printError(errorResponse(new ArtiumError("SECRET_IN_OUTPUT"), randomUUID5()));
44570
45047
  }
44571
45048
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowttl/artium-axi",
3
- "version": "0.0.13",
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",