@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 +43 -2
- package/bin/artium-axi.mjs +262 -66
- package/package.json +1 -1
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
|
-
|
|
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 |
|
package/bin/artium-axi.mjs
CHANGED
|
@@ -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
|
|
4502
|
-
return
|
|
4503
|
-
payload.value =
|
|
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 = (
|
|
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:
|
|
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
|
|
18368
|
-
if (
|
|
18369
|
-
return
|
|
18370
|
-
payload.value =
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
20828
|
+
write(`${pass ? "pass" : "fail"}: ${name}${detail ? ` (${detail})` : ""}
|
|
20816
20829
|
`);
|
|
20817
20830
|
};
|
|
20818
20831
|
let client;
|
|
20819
20832
|
try {
|
|
20820
|
-
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 = [
|
|
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(
|
|
33717
|
-
if (truncated) return
|
|
33735
|
+
function rewriteSuggestions(output3, tool, truncated = false) {
|
|
33736
|
+
if (truncated) return output3;
|
|
33718
33737
|
const flags = managedFlags[tool];
|
|
33719
|
-
if (!flags) return
|
|
33738
|
+
if (!flags) return output3;
|
|
33720
33739
|
const executable = new RegExp(`^(?:artium-axi |npx -y @knowttl/)?${tool}(?=\\s|$)`);
|
|
33721
|
-
if (executable.test(
|
|
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 =
|
|
33726
|
-
let parsed = json2 ? jsonHelp(
|
|
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
|
-
|
|
33734
|
-
parsed = json2 ? jsonHelp(
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
33750
|
-
if (spans.some((span) => /\\[`'"]/.test(span.value))) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
33822
|
+
output3 = output3.slice(0, offset) + (insertions.get(offset) ?? "") + output3.slice(removals.get(offset) ?? offset);
|
|
33804
33823
|
}
|
|
33805
|
-
return
|
|
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(
|
|
33852
|
-
const parsed = JSON.parse(
|
|
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 = [...
|
|
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(
|
|
33883
|
-
const lines = [...
|
|
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
|
|
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 [
|
|
34087
|
+
return [managed2];
|
|
34069
34088
|
}) }));
|
|
34070
|
-
if (!found) merged.push({ hooks: [
|
|
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
|
-
|
|
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
|
-
|
|
34432
|
+
output2("stdout", commandHelp(help));
|
|
34245
34433
|
return;
|
|
34246
34434
|
}
|
|
34247
34435
|
if (command === "setup") {
|
|
34248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34509
|
+
output2("stdout", `status: logged-out
|
|
34321
34510
|
remoteRevocation: ${verified ? "verified" : "unverified"}
|
|
34322
34511
|
`);
|
|
34323
|
-
if (!verified)
|
|
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
|
|
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
|
-
|
|
34537
|
+
view = `bin: ${JSON.stringify(bin)}
|
|
34349
34538
|
description: Run managed upstream tools through one local sign-in.
|
|
34350
|
-
${
|
|
34539
|
+
${view}${toolsView(tools.data.tools)}`;
|
|
34351
34540
|
}
|
|
34352
|
-
|
|
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
|
-
|
|
34361
|
-
} else
|
|
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)
|
|
34555
|
+
if (truncated.stdout || truncated.stderr) output2("stderr", `artium-axi: output truncated (${["stdout", "stderr"].filter((stream) => truncated[stream]).join(", ")}).
|
|
34367
34556
|
`);
|
|
34368
|
-
|
|
34369
|
-
|
|
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
|
-
|
|
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
|
+
});
|