@knowttl/artium-axi 0.0.2 → 0.0.3

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
@@ -660,11 +660,12 @@ See the [API contract](src/shared/api.ts) and [retention and export runbook](doc
660
660
 
661
661
  Use `login --password-stdin` or `passwd --password-stdin` to supply the login or new password through stdin.
662
662
  Stdin removes one final LF or CRLF and preserves all other spaces and Unicode.
663
- Password arguments are refused; tokens are never printed.
663
+ Password arguments are refused; tokens are never printed to stdout or stderr.
664
664
  Ordinary password changes also require the current password through the terminal prompt or `ARTIUM_AXI_CURRENT_PASSWORD`.
665
665
  Clear password environment values after use.
666
666
 
667
- Sessions live in `artium-axi/session.json` under the applicable base directory and are bound to the normalized server URL.
667
+ By default, sessions live in `artium-axi/session.json` under the applicable base directory and are bound to the normalized server URL.
668
+ Callers that manage their own tokens can instead use [externally managed client sessions](#externally-managed-client-sessions).
668
669
  POSIX directories use mode 0700 and files mode 0600; Windows uses a restricted ACL and refuses storage if it cannot establish it.
669
670
  Do not export or share the session file.
670
671
  CLI processes serialize refreshes through `session.lock`; after `SESSION_BUSY`, remove that lock directory only when every CLI process has exited.
@@ -675,6 +676,46 @@ Certificate verification remains enforced even with `NODE_TLS_REJECT_UNAUTHORIZE
675
676
  Only an explicit pre-execution `TOKEN_EXPIRED` response permits one serialized refresh and retry.
676
677
  A network failure or uncertain command outcome is never retried automatically.
677
678
 
679
+ ### Externally managed client sessions
680
+
681
+ Callers that keep credentials outside the CLI can opt in with one leading flag: `artium-axi --managed-session <command> <args>`.
682
+ The caller supplies inherited pipes or sockets: fd 3 for session input and fd 4 for private rotation output.
683
+ Stdin remains available for tool input; stdout and stderr retain their normal command formats.
684
+ Do not pass tokens in arguments or environment variables, connect fd 4 to a log or public output, or redirect either private channel to a disk file.
685
+ The CLI refuses regular files as private channels and never reads, creates, locks, updates or deletes `session.json` in this mode.
686
+
687
+ Send one UTF-8 JSON object of at most 64 KiB to fd 3, then close the input pipe:
688
+
689
+ ```json
690
+ {"serverUrl":"https://artium.example.test","accessToken":"CANARY-access","refreshToken":"CANARY-refresh"}
691
+ ```
692
+
693
+ `serverUrl` must match the normalized `ARTIUM_AXI_URL`, allowing a final slash.
694
+ `accessToken` is required and nonempty; `refreshToken` is optional and nonempty when supplied.
695
+ The optional fields `status: "authenticated"`, `accessExpiresAt` and `familyExpiresAt` (integer Unix milliseconds) allow a rotation record to be reused as input; other fields are refused.
696
+ The caller owns token acquisition, expiry policy, refresh serialization across processes and secure persistence.
697
+ Managed mode supports the home view, `whoami`, `tools`, `tools show`, `doctor` and tool commands.
698
+ `login`, `passwd`, `logout` and `setup` are refused before any network call; ordinary sign-in and its server-version warning remain unchanged without the flag.
699
+ Help is available without private channels through `artium-axi --help`.
700
+
701
+ Only an explicit `TOKEN_EXPIRED` response permits a single refresh and retry of that request.
702
+ Without a refresh token, or if refresh is refused, the CLI returns the authentication error for the caller to handle.
703
+ Network failures and uncertain outcomes are never automatically retried.
704
+ After a successful refresh, fd 4 receives one newline-terminated JSON object before the request is retried:
705
+
706
+ ```json
707
+ {"serverUrl":"https://artium.example.test","status":"authenticated","accessToken":"CANARY-new-access","refreshToken":"CANARY-new-refresh","accessExpiresAt":1800000000000,"familyExpiresAt":1800003600000}
708
+ ```
709
+
710
+ Every rotation is delivered even if the subsequent request fails; an invocation without rotation writes no record.
711
+ Consume fd 4 concurrently with stdout and stderr, and retain rotations regardless of the command's exit status.
712
+ If rotation delivery fails, the CLI refuses to retry the request and returns `INVALID_INPUT` (exit 2); the caller must recover the session rather than reuse the consumed refresh token.
713
+ Managed public output is buffered and scanned against supplied and rotated tokens and their canonical encodings before printing; a match withholds both streams and returns `SECRET_IN_OUTPUT`.
714
+
715
+ Exit codes are 0 for success, 2 for invalid input, client configuration or private channels and refused commands, and 1 for authentication, server-binding, transport or output-scanning failures.
716
+ Tool exit codes pass through as usual.
717
+ Missing or closed descriptors, invalid JSON and mismatched server bindings fail before network access; no failure falls back to a session file.
718
+
678
719
  ### Server settings and custody
679
720
 
680
721
  | Variable | Meaning |
@@ -4498,9 +4498,9 @@ var $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) =>
4498
4498
  }
4499
4499
  const _out = def.transform(payload.value, payload);
4500
4500
  if (ctx.async) {
4501
- const output2 = _out instanceof Promise ? _out : Promise.resolve(_out);
4502
- return output2.then((output3) => {
4503
- payload.value = output3;
4501
+ const output3 = _out instanceof Promise ? _out : Promise.resolve(_out);
4502
+ return output3.then((output4) => {
4503
+ payload.value = output4;
4504
4504
  return payload;
4505
4505
  });
4506
4506
  }
@@ -4950,12 +4950,12 @@ var $ZodFunction = /* @__PURE__ */ $constructor("$ZodFunction", (inst, def) => {
4950
4950
  output: inst._def.output
4951
4951
  });
4952
4952
  };
4953
- inst.output = (output2) => {
4953
+ inst.output = (output3) => {
4954
4954
  const F = inst.constructor;
4955
4955
  return new F({
4956
4956
  type: "function",
4957
4957
  input: inst._def.input,
4958
- output: output2
4958
+ output: output3
4959
4959
  });
4960
4960
  };
4961
4961
  return inst;
@@ -18364,14 +18364,14 @@ var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
18364
18364
  payload.issues.push(util_exports.issue(_issue));
18365
18365
  }
18366
18366
  };
18367
- const output2 = def.transform(payload.value, payload);
18368
- if (output2 instanceof Promise) {
18369
- return output2.then((output3) => {
18370
- payload.value = output3;
18367
+ const output3 = def.transform(payload.value, payload);
18368
+ if (output3 instanceof Promise) {
18369
+ return output3.then((output4) => {
18370
+ payload.value = output4;
18371
18371
  return payload;
18372
18372
  });
18373
18373
  }
18374
- payload.value = output2;
18374
+ payload.value = output3;
18375
18375
  return payload;
18376
18376
  };
18377
18377
  });
@@ -20452,7 +20452,7 @@ import { rootCertificates } from "node:tls";
20452
20452
  // package.json
20453
20453
  var package_default = {
20454
20454
  name: "artium-axi",
20455
- version: "0.0.2",
20455
+ version: "0.0.3",
20456
20456
  private: true,
20457
20457
  license: "MIT",
20458
20458
  type: "module",
@@ -20636,20 +20636,27 @@ var SessionStore = class {
20636
20636
  // src/cli/http.ts
20637
20637
  var DEFAULT_REQUEST_DEADLINE_MS = 9e4;
20638
20638
  var Client = class {
20639
- constructor(environment = process.env) {
20639
+ constructor(environment = process.env, managed2) {
20640
20640
  this.environment = environment;
20641
+ this.managed = managed2;
20641
20642
  try {
20642
20643
  const url2 = new URL(environment.ARTIUM_AXI_URL);
20643
20644
  if (url2.username || url2.password || url2.href.includes("?") || url2.href.includes("#") || url2.protocol !== "https:" && !(url2.protocol === "http:" && environment.ARTIUM_AXI_TEST_HTTP === "1" && ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname))) throw new Error();
20644
20645
  this.serverUrl = url2.href.replace(/\/$/, "");
20645
- this.sessions = new SessionStore(this.serverUrl, sessionPath(process.platform, environment));
20646
+ if (!managed2) this.storedSessions = new SessionStore(this.serverUrl, sessionPath(process.platform, environment));
20646
20647
  } catch {
20647
20648
  throw new ArtiumError("CLIENT_CONFIG_INVALID");
20648
20649
  }
20650
+ managed2?.checkServer(this.serverUrl);
20649
20651
  }
20650
20652
  environment;
20653
+ managed;
20651
20654
  serverUrl;
20652
- sessions;
20655
+ storedSessions;
20656
+ get sessions() {
20657
+ if (!this.storedSessions) throw new ArtiumError("INVALID_INPUT", "Session-file operations are unavailable in managed mode.");
20658
+ return this.storedSessions;
20659
+ }
20653
20660
  certificateVerified = false;
20654
20661
  async request(path, method, input2, token) {
20655
20662
  this.certificateVerified = false;
@@ -20725,6 +20732,10 @@ var Client = class {
20725
20732
  return result.body;
20726
20733
  }
20727
20734
  async authenticated(path, method, input2, previous) {
20735
+ if (this.managed) return this.managed.authenticated(
20736
+ (token) => this.request(path, method, input2, token),
20737
+ (refreshToken) => this.request("/v1/auth/refresh", "POST", { refreshToken })
20738
+ );
20728
20739
  const session = previous ? this.reload(previous) : this.sessions.read();
20729
20740
  if (!session) throw new ArtiumError("TOKEN_INVALID", void 0, "Run artium-axi login --username <username> --password-stdin.");
20730
20741
  if (session.status === "password-change-required") throw new ArtiumError("PASSWORD_CHANGE_REQUIRED");
@@ -20808,16 +20819,18 @@ async function readPassword(stdin, environmentVariable) {
20808
20819
  }
20809
20820
 
20810
20821
  // src/cli/doctor.ts
20811
- async function doctor() {
20822
+ async function doctor(createClient = () => new Client(), write = (text2) => {
20823
+ process.stdout.write(text2);
20824
+ }) {
20812
20825
  let failure2;
20813
20826
  const line = (name, pass, detail, error62) => {
20814
20827
  if (!pass) failure2 ??= error62 ?? new ArtiumError("INVALID_RESPONSE");
20815
- process.stdout.write(`${pass ? "pass" : "fail"}: ${name}${detail ? ` (${detail})` : ""}
20828
+ write(`${pass ? "pass" : "fail"}: ${name}${detail ? ` (${detail})` : ""}
20816
20829
  `);
20817
20830
  };
20818
20831
  let client;
20819
20832
  try {
20820
- client = new Client();
20833
+ client = createClient();
20821
20834
  } catch (error62) {
20822
20835
  for (const name of ["server address", "certificate", "token", "CLI version"]) line(name, false, "CLIENT_CONFIG_INVALID");
20823
20836
  throw error62;
@@ -22878,7 +22891,13 @@ var commands = {
22878
22891
  };
22879
22892
  function commandHelp(command) {
22880
22893
  const entry = commands[command];
22881
- const flags = [command === "falcon-axi" || command === "vectra-axi" || command === "ldap-axi" ? "--help before the tool name (default false)" : "--help (default false)", ...entry.flags];
22894
+ const flags = [
22895
+ command === "falcon-axi" || command === "vectra-axi" || command === "ldap-axi" ? "--help before the tool name (default false)" : "--help (default false)",
22896
+ ...entry.flags,
22897
+ "--managed-session before the command (default false; authenticated calls only; session JSON on inherited pipe fd 3, rotations as JSON lines on private pipe fd 4; no session file)",
22898
+ "Managed input: {serverUrl, accessToken, refreshToken?}; close fd 3 after JSON (64 KiB maximum). Output: {serverUrl, status, accessToken, refreshToken, accessExpiresAt, familyExpiresAt} per rotation, before retry, including on later failure.",
22899
+ "Managed exits: 0 success; 2 invalid JSON/descriptors or refused login/passwd/logout/setup; 1 authentication/transport failure; tool exit codes pass through. Only TOKEN_EXPIRED permits one refresh/retry; without refreshToken or on refusal, return control to the caller."
22900
+ ];
22882
22901
  return `usage: ${entry.usage}
22883
22902
  purpose: ${entry.purpose}
22884
22903
  flags[${flags.length}]:
@@ -33713,25 +33732,25 @@ function rewriteDiscoveryHelp(help, tool) {
33713
33732
  examples: help.examples.map((example) => hostedHelp(example, tool, flags, true))
33714
33733
  });
33715
33734
  }
33716
- function rewriteSuggestions(output2, tool, truncated = false) {
33717
- if (truncated) return output2;
33735
+ function rewriteSuggestions(output3, tool, truncated = false) {
33736
+ if (truncated) return output3;
33718
33737
  const flags = managedFlags[tool];
33719
- if (!flags) return output2;
33738
+ if (!flags) return output3;
33720
33739
  const executable = new RegExp(`^(?:artium-axi |npx -y @knowttl/)?${tool}(?=\\s|$)`);
33721
- if (executable.test(output2.replace(/^[ \t]*(?:(?:Command|Native operation|Examples): )?/, "")) || tool === "ldap-axi" && ldapHelp.test(output2)) return hostedHelp(output2, tool, flags, true);
33740
+ if (executable.test(output3.replace(/^[ \t]*(?:(?:Command|Native operation|Examples): )?/, "")) || tool === "ldap-axi" && ldapHelp.test(output3)) return hostedHelp(output3, tool, flags, true);
33722
33741
  let spans;
33723
33742
  let unknownOutcome = false;
33724
33743
  try {
33725
- const json2 = output2.trimStart().startsWith("{");
33726
- let parsed = json2 ? jsonHelp(output2) : toonHelp(output2);
33744
+ const json2 = output3.trimStart().startsWith("{");
33745
+ let parsed = json2 ? jsonHelp(output3) : toonHelp(output3);
33727
33746
  if (tool === "ldap-axi" && parsed.document.description === "Read Active Directory and LDAPv3 directories safely with ldap-axi (read-only)" && Array.isArray(parsed.document.commands) && parsed.spans.length) {
33728
33747
  const home = {
33729
33748
  ...parsed.document,
33730
33749
  commands: parsed.document.commands.filter((command) => ldapCommands.includes(command)),
33731
33750
  help: [...parsed.spans.map((span) => span.value).filter((value) => !/^(?:Authentication auto selects|Set LDAP_AXI_URL)/.test(value) && value !== ldapCredentialGuidance), ldapCredentialGuidance]
33732
33751
  };
33733
- output2 = (json2 ? JSON.stringify(home) : encode3(home)) + "\n";
33734
- parsed = json2 ? jsonHelp(output2) : toonHelp(output2);
33752
+ output3 = (json2 ? JSON.stringify(home) : encode3(home)) + "\n";
33753
+ parsed = json2 ? jsonHelp(output3) : toonHelp(output3);
33735
33754
  }
33736
33755
  const { spans: suggestions, document } = parsed;
33737
33756
  spans = suggestions;
@@ -33739,15 +33758,15 @@ function rewriteSuggestions(output2, tool, truncated = false) {
33739
33758
  unknownOutcome = document?.code === "UNKNOWN_OUTCOME" || document?.error?.code === "UNKNOWN_OUTCOME";
33740
33759
  }
33741
33760
  } catch {
33742
- return output2;
33761
+ return output3;
33743
33762
  }
33744
33763
  for (const span of [...spans].reverse()) {
33745
33764
  const value = hostedHelp(span.value, tool, flags, /^[ \t]*(?:Command|Native operation|Examples):/m.test(span.value));
33746
33765
  if (value === span.value) continue;
33747
- output2 = output2.slice(0, span.start) + (span.quoted || tool === "ldap-axi" && /[,\r\n]/.test(value) ? JSON.stringify(value) : value) + output2.slice(span.start + span.raw.length);
33766
+ output3 = output3.slice(0, span.start) + (span.quoted || tool === "ldap-axi" && /[,\r\n]/.test(value) ? JSON.stringify(value) : value) + output3.slice(span.start + span.raw.length);
33748
33767
  }
33749
- spans = (output2.trimStart().startsWith("{") ? jsonHelp(output2) : toonHelp(output2)).spans;
33750
- if (spans.some((span) => /\\[`'"]/.test(span.value))) return output2;
33768
+ spans = (output3.trimStart().startsWith("{") ? jsonHelp(output3) : toonHelp(output3)).spans;
33769
+ if (spans.some((span) => /\\[`'"]/.test(span.value))) return output3;
33751
33770
  const insertions = /* @__PURE__ */ new Map();
33752
33771
  const removals = /* @__PURE__ */ new Map();
33753
33772
  for (const span of spans) {
@@ -33781,28 +33800,28 @@ function rewriteSuggestions(output2, tool, truncated = false) {
33781
33800
  if (!quote2 && /^(?:artium-axi |npx -y @knowttl\/)?\S+-axi(?:\s|$)/.test(command)) commandRanges.push([match.index, match.index + match[0].length]);
33782
33801
  const wrapped = command.startsWith(`artium-axi ${tool}`);
33783
33802
  if (!executable.test(quote2 ? command.split(quote2)[0] : command)) continue;
33784
- if (quote2 && !command.includes(quote2)) return output2;
33803
+ if (quote2 && !command.includes(quote2)) return output3;
33785
33804
  const start = match.index + match[0].length - command.length;
33786
33805
  commandRanges.push([match.index, quote2 ? start + command.indexOf(quote2) + 1 : match.index + match[0].length]);
33787
33806
  commandEdit2(start, quote2 ? start + command.indexOf(quote2) : start + command.length, wrapped);
33788
33807
  }
33789
33808
  for (const match of span.value.matchAll(/(?<![^\s(])([`'"])([^\n]*?)(\1|(?=\n|$))/g)) {
33790
33809
  if (commandRanges.some(([start, end]) => match.index >= start && match.index < end)) continue;
33791
- if (!/^(?:artium-axi |npx -y @knowttl\/)?[^\s`'"]+-axi(?:\s|$)/.test(match[2])) return output2;
33810
+ if (!/^(?:artium-axi |npx -y @knowttl\/)?[^\s`'"]+-axi(?:\s|$)/.test(match[2])) return output3;
33792
33811
  const preceding = span.value.slice(0, match.index);
33793
33812
  if (/(?:^|\n)[ \t]*Valid flags for $/.test(preceding) && match[1] === "`" && match[3] === "`") continue;
33794
- if (!/(?:^|\n|\S[`'"])[ \t]*(?:(?:Run|Use|run|use|Verify with|If you're unsure, run|Check current state before trying again:|or(?:, if you're unsure, run)?)[ \t]+)?$/.test(preceding)) return output2;
33813
+ if (!/(?:^|\n|\S[`'"])[ \t]*(?:(?:Run|Use|run|use|Verify with|If you're unsure, run|Check current state before trying again:|or(?:, if you're unsure, run)?)[ \t]+)?$/.test(preceding)) return output3;
33795
33814
  const wrapped = match[2].startsWith(`artium-axi ${tool}`);
33796
33815
  if (executable.test(match[2])) {
33797
- if (!match[3]) return output2;
33816
+ if (!match[3]) return output3;
33798
33817
  commandEdit2(match.index + 1, match.index + 1 + match[2].length, wrapped);
33799
33818
  }
33800
33819
  }
33801
33820
  }
33802
33821
  for (const offset of [.../* @__PURE__ */ new Set([...insertions.keys(), ...removals.keys()])].sort((a, b) => b - a)) {
33803
- output2 = output2.slice(0, offset) + (insertions.get(offset) ?? "") + output2.slice(removals.get(offset) ?? offset);
33822
+ output3 = output3.slice(0, offset) + (insertions.get(offset) ?? "") + output3.slice(removals.get(offset) ?? offset);
33804
33823
  }
33805
- return output2;
33824
+ return output3;
33806
33825
  }
33807
33826
  function managedFlagRanges(command, tool) {
33808
33827
  const tokens = [...command.matchAll(/(?:[^\s;'"`]+|'[^']*'|"(?:\\.|[^"\\])*")+|;/g)];
@@ -33848,10 +33867,10 @@ function hostedHelp(help, tool, flags, commands2) {
33848
33867
  (match, separator) => separator ? `; artium-axi ${tool}` : match
33849
33868
  )).replace(new RegExp("`(?:npx -y @knowttl/)?" + tool + "(?=\\s|`)([^\\r\\n`]*`)", "g"), "`artium-axi " + tool + "$1").replace(new RegExp(`^([ ]*cat [^\\r\\n|]+ \\| )(?:npx -y @knowttl/)?${tool}(?=\\s|$)`, "gm"), `$1artium-axi ${tool}`).replace(new RegExp(`\\buse ${tool} entry search\\b`, "g"), `use artium-axi ${tool} entry search`);
33850
33869
  }
33851
- function jsonHelp(output2) {
33852
- const parsed = JSON.parse(output2);
33870
+ function jsonHelp(output3) {
33871
+ const parsed = JSON.parse(output3);
33853
33872
  if (typeof parsed.help !== "string" && !(Array.isArray(parsed.help) && parsed.help.every((item) => typeof item === "string"))) return { spans: [], document: parsed };
33854
- const tokens = [...output2.matchAll(/"(?:\\[\s\S]|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\]:,]/g)];
33873
+ const tokens = [...output3.matchAll(/"(?:\\[\s\S]|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\]:,]/g)];
33855
33874
  const spans = [];
33856
33875
  let index = 0;
33857
33876
  let helpFields = 0;
@@ -33879,8 +33898,8 @@ function jsonHelp(output2) {
33879
33898
  value(false, true);
33880
33899
  return { spans: helpFields === 1 ? spans : [], document: parsed };
33881
33900
  }
33882
- function toonHelp(output2) {
33883
- const lines = [...output2.matchAll(/[^\r\n]*(?:\r\n|\n|$)/g)].filter((line) => line[0]);
33901
+ function toonHelp(output3) {
33902
+ const lines = [...output3.matchAll(/[^\r\n]*(?:\r\n|\n|$)/g)].filter((line) => line[0]);
33884
33903
  const spans = [];
33885
33904
  const validation = lines.map((line) => line[0].replace(/\r?\n$/, ""));
33886
33905
  let helpFields = 0;
@@ -34059,15 +34078,15 @@ function claudeSettings(path, executable) {
34059
34078
  if (!Array.isArray(sessions) || sessions.some((entry) => !object2(entry) || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !object2(hook)))) {
34060
34079
  throw new ArtiumError("INVALID_INPUT", "Claude SessionStart hooks have an unsupported shape; no installation was written.");
34061
34080
  }
34062
- const managed = { type: "command", command: process.execPath, args: [executable, "tools"], statusMessage: hookName };
34081
+ const managed2 = { type: "command", command: process.execPath, args: [executable, "tools"], statusMessage: hookName };
34063
34082
  let found = false;
34064
34083
  const merged = sessions.map((entry) => ({ ...entry, hooks: entry.hooks.flatMap((hook) => {
34065
34084
  if (hook.statusMessage !== hookName) return [hook];
34066
34085
  if (found) return [];
34067
34086
  found = true;
34068
- return [managed];
34087
+ return [managed2];
34069
34088
  }) }));
34070
- if (!found) merged.push({ hooks: [managed] });
34089
+ if (!found) merged.push({ hooks: [managed2] });
34071
34090
  settings.hooks = { ...hooks, SessionStart: merged };
34072
34091
  return JSON.stringify(settings, null, 2) + "\n";
34073
34092
  }
@@ -34228,10 +34247,179 @@ next: artium-axi tools
34228
34247
  throw new ArtiumError("DEVICE_CODE_EXPIRED");
34229
34248
  }
34230
34249
 
34250
+ // src/cli/managed-session.ts
34251
+ import { fstatSync, writeSync } from "node:fs";
34252
+ import { Socket } from "node:net";
34253
+
34254
+ // src/server/runner/scan.ts
34255
+ function prefixLengths(bytes) {
34256
+ const lengths = new Uint32Array(bytes.length);
34257
+ let length = 0;
34258
+ for (let index = 1; index < bytes.length; index++) {
34259
+ while (length > 0 && bytes[index] !== bytes[length]) length = lengths[length - 1];
34260
+ if (bytes[index] === bytes[length]) length++;
34261
+ lengths[index] = length;
34262
+ }
34263
+ return lengths;
34264
+ }
34265
+ var SecretScanner = class {
34266
+ #patterns;
34267
+ #positions;
34268
+ #matched = false;
34269
+ constructor(secrets) {
34270
+ const forms = /* @__PURE__ */ new Set();
34271
+ for (const secret of secrets) {
34272
+ if (secret.length === 0) throw new Error("Secret values must be nonempty");
34273
+ const bytes = Buffer.from(secret, "utf8");
34274
+ forms.add(secret);
34275
+ forms.add(bytes.toString("base64"));
34276
+ forms.add(bytes.toString("base64url"));
34277
+ forms.add(encodeURIComponent(secret));
34278
+ forms.add(JSON.stringify(secret).slice(1, -1));
34279
+ }
34280
+ this.#patterns = [...forms].map((form) => {
34281
+ const bytes = Buffer.from(form, "utf8");
34282
+ return { bytes, prefixes: prefixLengths(bytes) };
34283
+ });
34284
+ this.#positions = {
34285
+ stdout: new Uint32Array(this.#patterns.length),
34286
+ stderr: new Uint32Array(this.#patterns.length)
34287
+ };
34288
+ }
34289
+ scan(stream, chunk) {
34290
+ if (this.#matched) return true;
34291
+ const positions = this.#positions[stream];
34292
+ for (let index = 0; index < this.#patterns.length; index++) {
34293
+ const { bytes, prefixes } = this.#patterns[index];
34294
+ let position = positions[index];
34295
+ for (const byte of chunk) {
34296
+ while (position > 0 && byte !== bytes[position]) position = prefixes[position - 1];
34297
+ if (byte === bytes[position]) position++;
34298
+ if (position === bytes.length) {
34299
+ this.#matched = true;
34300
+ return true;
34301
+ }
34302
+ }
34303
+ positions[index] = position;
34304
+ }
34305
+ return false;
34306
+ }
34307
+ };
34308
+
34309
+ // src/cli/managed-session.ts
34310
+ var sessionSchema = external_exports.strictObject({
34311
+ serverUrl: external_exports.string().url(),
34312
+ accessToken: external_exports.string().min(1),
34313
+ refreshToken: external_exports.string().min(1).optional(),
34314
+ status: external_exports.literal("authenticated").optional(),
34315
+ accessExpiresAt: external_exports.number().int().optional(),
34316
+ familyExpiresAt: external_exports.number().int().optional()
34317
+ });
34318
+ var ManagedSession = class _ManagedSession {
34319
+ constructor(session, channel) {
34320
+ this.session = session;
34321
+ this.channel = channel;
34322
+ this.secrets = [session.accessToken, ...session.refreshToken ? [session.refreshToken] : []];
34323
+ }
34324
+ session;
34325
+ channel;
34326
+ secrets;
34327
+ output = { stdout: "", stderr: "" };
34328
+ static async read() {
34329
+ try {
34330
+ for (const fd of [3, 4]) {
34331
+ const stat = fstatSync(fd);
34332
+ if (!stat.isFIFO() && !stat.isSocket()) throw new Error();
34333
+ }
34334
+ writeSync(4, Buffer.alloc(0));
34335
+ const input2 = new Socket({ fd: 3, readable: true, writable: false });
34336
+ const chunks = [];
34337
+ let bytes = 0;
34338
+ for await (const chunk of input2) {
34339
+ bytes += chunk.length;
34340
+ if (bytes > 64 * 1024) {
34341
+ input2.destroy();
34342
+ throw new Error();
34343
+ }
34344
+ chunks.push(chunk);
34345
+ }
34346
+ const session = sessionSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
34347
+ const channel = new Socket({ fd: 4, readable: false, writable: true });
34348
+ channel.on("error", () => {
34349
+ });
34350
+ return new _ManagedSession(session, channel);
34351
+ } catch {
34352
+ throw new ArtiumError(
34353
+ "INVALID_INPUT",
34354
+ "Managed session input or private descriptors are invalid.",
34355
+ "Supply session JSON on inherited pipe fd 3 and a writable private pipe on fd 4; close fd 3 after sending JSON."
34356
+ );
34357
+ }
34358
+ }
34359
+ checkServer(serverUrl) {
34360
+ if (this.session.serverUrl.replace(/\/$/, "") !== serverUrl) throw new ArtiumError("SESSION_SERVER_MISMATCH");
34361
+ }
34362
+ async authenticated(request, refresh) {
34363
+ try {
34364
+ return await request(this.session.accessToken);
34365
+ } catch (error62) {
34366
+ if (!(error62 instanceof ArtiumError) || error62.code !== "TOKEN_EXPIRED" || !this.session.refreshToken) throw error62;
34367
+ }
34368
+ const parsed = loginResponseSchema.safeParse(await refresh(this.session.refreshToken));
34369
+ if (!parsed.success || parsed.data.status !== "authenticated" || !parsed.data.accessToken || !parsed.data.refreshToken) {
34370
+ throw new ArtiumError("INVALID_RESPONSE");
34371
+ }
34372
+ this.session = { serverUrl: this.session.serverUrl, ...parsed.data };
34373
+ this.secrets.push(parsed.data.accessToken, parsed.data.refreshToken);
34374
+ try {
34375
+ await new Promise((resolve3, reject) => {
34376
+ this.channel.write(`${JSON.stringify(this.session)}
34377
+ `, (error62) => error62 ? reject(error62) : resolve3());
34378
+ });
34379
+ } catch {
34380
+ throw new ArtiumError(
34381
+ "INVALID_INPUT",
34382
+ "Managed session rotation could not be returned.",
34383
+ "Recover the session through the caller; do not retry using the old refresh token."
34384
+ );
34385
+ }
34386
+ return request(this.session.accessToken);
34387
+ }
34388
+ write(stream, text2) {
34389
+ this.output[stream] += text2;
34390
+ }
34391
+ flush() {
34392
+ this.channel.destroy();
34393
+ const scanner = new SecretScanner(this.secrets);
34394
+ if (scanner.scan("stdout", Buffer.from(this.output.stdout)) || scanner.scan("stderr", Buffer.from(this.output.stderr))) {
34395
+ throw new ArtiumError("SECRET_IN_OUTPUT");
34396
+ }
34397
+ process.stdout.write(this.output.stdout);
34398
+ process.stderr.write(this.output.stderr);
34399
+ }
34400
+ };
34401
+
34231
34402
  // src/cli/main.ts
34403
+ var managed;
34404
+ function output2(stream, text2) {
34405
+ if (managed) managed.write(stream, text2);
34406
+ else process[stream].write(text2);
34407
+ }
34232
34408
  async function main(args) {
34409
+ const managedMode = args[0] === "--managed-session";
34410
+ if (managedMode) {
34411
+ args = args.slice(1);
34412
+ if (["login", "passwd", "logout", "setup"].includes(args[0])) {
34413
+ throw new ArtiumError(
34414
+ "INVALID_INPUT",
34415
+ "This command is unavailable in managed session mode.",
34416
+ "The caller must acquire and manage tokens; use managed mode for authenticated calls."
34417
+ );
34418
+ }
34419
+ managed = await ManagedSession.read();
34420
+ }
34233
34421
  if (args.length === 1 && args[0] === "--version") {
34234
- process.stdout.write(`${cliVersion}
34422
+ output2("stdout", `${cliVersion}
34235
34423
  `);
34236
34424
  return;
34237
34425
  }
@@ -34241,16 +34429,17 @@ async function main(args) {
34241
34429
  const help = helpCommand(prefixHelp ? [...args, "--help"] : args, prefixHelp);
34242
34430
  if (prefixHelp && !help) throw new ArtiumError("INVALID_INPUT", void 0, "Use artium-axi --help for commands.");
34243
34431
  if (help) {
34244
- process.stdout.write(commandHelp(help));
34432
+ output2("stdout", commandHelp(help));
34245
34433
  return;
34246
34434
  }
34247
34435
  if (command === "setup") {
34248
- process.stdout.write(await setup(args.slice(1)));
34436
+ output2("stdout", await setup(args.slice(1)));
34249
34437
  return;
34250
34438
  }
34251
34439
  if (command === "doctor") {
34252
34440
  if (args.length !== 1) throw new ArtiumError("INVALID_INPUT", void 0, "Use artium-axi doctor.");
34253
- await doctor();
34441
+ if (managed) await doctor(() => new Client(process.env, managed), (text2) => output2("stdout", text2));
34442
+ else await doctor();
34254
34443
  return;
34255
34444
  }
34256
34445
  if (command === "login" || command === "passwd") {
@@ -34285,14 +34474,14 @@ async function main(args) {
34285
34474
  return parsed.data;
34286
34475
  });
34287
34476
  if (session.status === "password-change-required") throw new ArtiumError("PASSWORD_CHANGE_REQUIRED");
34288
- process.stdout.write("status: authenticated\nnext: artium-axi tools\n");
34477
+ output2("stdout", "status: authenticated\nnext: artium-axi tools\n");
34289
34478
  } else {
34290
34479
  await client2.changePassword(async (session) => {
34291
34480
  const newPassword = await readPassword(stdin);
34292
34481
  const currentPassword = session.status === "authenticated" ? await readPassword(false, "ARTIUM_AXI_CURRENT_PASSWORD") : void 0;
34293
34482
  return { newPassword, ...currentPassword === void 0 ? {} : { currentPassword } };
34294
34483
  });
34295
- process.stdout.write("status: password-changed\nnext: artium-axi login --username <username> --password-stdin\n");
34484
+ output2("stdout", "status: password-changed\nnext: artium-axi login --username <username> --password-stdin\n");
34296
34485
  }
34297
34486
  return;
34298
34487
  }
@@ -34317,10 +34506,10 @@ async function main(args) {
34317
34506
  return false;
34318
34507
  }
34319
34508
  });
34320
- process.stdout.write(`status: logged-out
34509
+ output2("stdout", `status: logged-out
34321
34510
  remoteRevocation: ${verified ? "verified" : "unverified"}
34322
34511
  `);
34323
- if (!verified) process.stdout.write("next: Remote revocation is unverified. The local tokens were removed.\n");
34512
+ if (!verified) output2("stdout", "next: Remote revocation is unverified. The local tokens were removed.\n");
34324
34513
  return;
34325
34514
  }
34326
34515
  const home = args.length === 0;
@@ -34331,25 +34520,25 @@ remoteRevocation: ${verified ? "verified" : "unverified"}
34331
34520
  throw new ArtiumError("INVALID_INPUT", "Unknown command, argument or flag.", "Use artium-axi --help for commands or artium-axi <command> --help for valid flags.");
34332
34521
  }
34333
34522
  const input2 = home || listing || whoami ? void 0 : await toolInput(command, args.slice(1));
34334
- const client = new Client();
34335
- const homeSession = home ? client.sessions.read() : void 0;
34336
- if (home && !homeSession) throw new ArtiumError("TOKEN_INVALID", void 0, "Run artium-axi login --username <username> --password-stdin.");
34523
+ const client = new Client(process.env, managed);
34524
+ const homeSession = home && !managed ? client.sessions.read() : void 0;
34525
+ if (home && !managed && !homeSession) throw new ArtiumError("TOKEN_INVALID", void 0, "Run artium-axi login --username <username> --password-stdin.");
34337
34526
  const result = await client.authenticated(home || whoami ? "/v1/me" : `/v1/tools${listing ? "" : `/${command}/run`}`, home || listing || whoami ? "GET" : "POST", input2, homeSession);
34338
34527
  if (home || whoami) {
34339
34528
  const parsed = meResponseSchema.safeParse(result);
34340
34529
  if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
34341
- let output2 = identityView(parsed.data, client.serverUrl);
34530
+ let view = identityView(parsed.data, client.serverUrl);
34342
34531
  if (home) {
34343
34532
  const tools = toolsResponseSchema.safeParse(await client.authenticated("/v1/tools", "GET", void 0, homeSession));
34344
34533
  if (!tools.success) throw new ArtiumError("INVALID_RESPONSE");
34345
34534
  const executable = resolve2(process.argv[1]);
34346
34535
  const local = relative(homedir3(), executable);
34347
34536
  const bin = local && !isAbsolute(local) && !local.startsWith(`..${sep}`) && local !== ".." ? `~${sep}${local}` : executable;
34348
- output2 = `bin: ${JSON.stringify(bin)}
34537
+ view = `bin: ${JSON.stringify(bin)}
34349
34538
  description: Run managed upstream tools through one local sign-in.
34350
- ${output2}${toolsView(tools.data.tools)}`;
34539
+ ${view}${toolsView(tools.data.tools)}`;
34351
34540
  }
34352
- process.stdout.write(output2);
34541
+ output2("stdout", view);
34353
34542
  } else if (listing) {
34354
34543
  const parsed = toolsResponseSchema.safeParse(result);
34355
34544
  if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
@@ -34357,23 +34546,30 @@ ${output2}${toolsView(tools.data.tools)}`;
34357
34546
  if (detail) {
34358
34547
  const tool = tools.find((tool2) => tool2.name === args[2]);
34359
34548
  if (!tool) throw new ArtiumError("TOOL_NOT_GRANTED", "The tool is not available to your account.", "Ask an administrator to check your role assignments and enabled tools.");
34360
- process.stdout.write(toolView(tool));
34361
- } else process.stdout.write(toolsView(tools));
34549
+ output2("stdout", toolView(tool));
34550
+ } else output2("stdout", toolsView(tools));
34362
34551
  } else {
34363
34552
  const parsed = runSuccessSchema.safeParse(result);
34364
34553
  if (!parsed.success || parsed.data.source !== command) throw new ArtiumError("INVALID_RESPONSE");
34365
34554
  const { stdout, stderr, exitCode, truncated } = parsed.data;
34366
- if (truncated.stdout || truncated.stderr) process.stderr.write(`artium-axi: output truncated (${["stdout", "stderr"].filter((stream) => truncated[stream]).join(", ")}).
34555
+ if (truncated.stdout || truncated.stderr) output2("stderr", `artium-axi: output truncated (${["stdout", "stderr"].filter((stream) => truncated[stream]).join(", ")}).
34367
34556
  `);
34368
- process.stdout.write(rewriteSuggestions(stdout, command, truncated.stdout));
34369
- process.stderr.write(rewriteSuggestions(stderr, command, truncated.stderr));
34557
+ output2("stdout", rewriteSuggestions(stdout, command, truncated.stdout));
34558
+ output2("stderr", rewriteSuggestions(stderr, command, truncated.stderr));
34370
34559
  process.exitCode = exitCode;
34371
34560
  }
34372
34561
  }
34373
34562
  function printError(response) {
34374
- process.stdout.write(`error:
34563
+ output2("stdout", `error:
34375
34564
  ${Object.entries(response.error).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`).join("\n")}
34376
34565
  `);
34377
34566
  process.exitCode = errors[response.error.code].exitCode;
34378
34567
  }
34379
- main(process.argv.slice(2)).catch((error62) => printError(errorResponse(error62 instanceof ArtiumError ? error62 : new ArtiumError("INTERNAL_ERROR"), randomUUID3())));
34568
+ main(process.argv.slice(2)).catch((error62) => printError(errorResponse(error62 instanceof ArtiumError ? error62 : new ArtiumError("INTERNAL_ERROR"), randomUUID3()))).then(() => {
34569
+ try {
34570
+ managed?.flush();
34571
+ } catch {
34572
+ managed = void 0;
34573
+ printError(errorResponse(new ArtiumError("SECRET_IN_OUTPUT"), randomUUID3()));
34574
+ }
34575
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowttl/artium-axi",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Client CLI and embedded skill for the Artium managed tools server.",
5
5
  "license": "MIT",
6
6
  "type": "module",