@knowttl/artium-axi 0.0.3 → 0.0.4
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 +22 -10
- package/bin/artium-axi.mjs +77 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -652,7 +652,7 @@ See the [API contract](src/shared/api.ts) and [retention and export runbook](doc
|
|
|
652
652
|
| --- | --- |
|
|
653
653
|
| `ARTIUM_AXI_URL` | Required HTTPS base URL without credentials, query or fragment. |
|
|
654
654
|
| `ARTIUM_AXI_CA_FILE` | Optional readable PEM CA file added to Node's certificate trust. |
|
|
655
|
-
| `ARTIUM_AXI_PASSWORD` |
|
|
655
|
+
| `ARTIUM_AXI_PASSWORD` | Ordinary login password source when `--password-stdin` is absent, before the terminal prompt; refused for [managed sign-in](#externally-managed-client-sessions). |
|
|
656
656
|
| `ARTIUM_AXI_CURRENT_PASSWORD` | Current password for an ordinary password change, before the terminal prompt. |
|
|
657
657
|
| `XDG_CONFIG_HOME` | Session-file base on Linux/macOS; otherwise `~/.config`. |
|
|
658
658
|
| `APPDATA` | Session-file base on Windows, whose execution remains unverified. |
|
|
@@ -679,12 +679,12 @@ A network failure or uncertain command outcome is never retried automatically.
|
|
|
679
679
|
### Externally managed client sessions
|
|
680
680
|
|
|
681
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.
|
|
682
|
+
The caller supplies inherited pipes or sockets: fd 3 for session input on authenticated calls and fd 4 for private new-session and rotation output.
|
|
683
683
|
Stdin remains available for tool input; stdout and stderr retain their normal command formats.
|
|
684
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
685
|
The CLI refuses regular files as private channels and never reads, creates, locks, updates or deletes `session.json` in this mode.
|
|
686
686
|
|
|
687
|
-
|
|
687
|
+
For authenticated calls, send one UTF-8 JSON object of at most 64 KiB to fd 3, then close the input pipe:
|
|
688
688
|
|
|
689
689
|
```json
|
|
690
690
|
{"serverUrl":"https://artium.example.test","accessToken":"CANARY-access","refreshToken":"CANARY-refresh"}
|
|
@@ -693,11 +693,21 @@ Send one UTF-8 JSON object of at most 64 KiB to fd 3, then close the input pipe:
|
|
|
693
693
|
`serverUrl` must match the normalized `ARTIUM_AXI_URL`, allowing a final slash.
|
|
694
694
|
`accessToken` is required and nonempty; `refreshToken` is optional and nonempty when supplied.
|
|
695
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
|
|
697
|
-
Managed mode supports the home view, `whoami`, `tools`, `tools show`, `doctor` and tool commands.
|
|
698
|
-
`
|
|
696
|
+
The caller owns expiry policy, refresh serialization across processes and secure persistence.
|
|
697
|
+
Managed mode supports `login`, the home view, `whoami`, `tools`, `tools show`, `doctor` and tool commands.
|
|
698
|
+
`passwd`, `logout` and `setup` are refused before any network call; ordinary sign-in and its server-version warning remain unchanged without the flag.
|
|
699
699
|
Help is available without private channels through `artium-axi --help`.
|
|
700
700
|
|
|
701
|
+
To obtain the first session, run `artium-axi --managed-session login --username <username> --password-stdin` with the password on protected inherited stdin, or `artium-axi --managed-session login --sso` for browser approval.
|
|
702
|
+
Login does not read or require fd 3; it requires a writable private pipe or socket on fd 4 before contacting the server.
|
|
703
|
+
Password arguments and `ARTIUM_AXI_PASSWORD` are refused, including when `--password-stdin` is present; unset that variable before managed sign-in.
|
|
704
|
+
Password input follows the stdin rules in [client settings](#client-settings).
|
|
705
|
+
SSO displays the approval URL and user code immediately so the user can approve while the CLI waits.
|
|
706
|
+
Both sign-in methods return the new session as one newline-terminated JSON object on fd 4, using the same envelope shown below for rotations, and create nothing under the config or session paths.
|
|
707
|
+
The login server-version warning still appears on stderr when versions differ.
|
|
708
|
+
If a local account requires a password change, fd 4 instead receives `{serverUrl, status: "password-change-required", passwordChangeToken, passwordChangeExpiresAt}` and the command returns `PASSWORD_CHANGE_REQUIRED` (exit 1).
|
|
709
|
+
That restricted record is not valid managed-session input; the caller must complete the password change through the server API and sign in again.
|
|
710
|
+
|
|
701
711
|
Only an explicit `TOKEN_EXPIRED` response permits a single refresh and retry of that request.
|
|
702
712
|
Without a refresh token, or if refresh is refused, the CLI returns the authentication error for the caller to handle.
|
|
703
713
|
Network failures and uncertain outcomes are never automatically retried.
|
|
@@ -707,10 +717,12 @@ After a successful refresh, fd 4 receives one newline-terminated JSON object bef
|
|
|
707
717
|
{"serverUrl":"https://artium.example.test","status":"authenticated","accessToken":"CANARY-new-access","refreshToken":"CANARY-new-refresh","accessExpiresAt":1800000000000,"familyExpiresAt":1800003600000}
|
|
708
718
|
```
|
|
709
719
|
|
|
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
|
|
713
|
-
|
|
720
|
+
Every rotation is delivered even if the subsequent request fails; an authenticated invocation without rotation writes no record.
|
|
721
|
+
Consume fd 4 concurrently with stdout and stderr, and retain new sessions and rotations regardless of the command's exit status.
|
|
722
|
+
If new-session or rotation delivery fails, the CLI returns `INVALID_INPUT` (exit 2); the caller must recover the session.
|
|
723
|
+
After failed rotation delivery, the CLI refuses to retry the request; do not reuse the consumed refresh token.
|
|
724
|
+
Managed public output is buffered and scanned against supplied, newly issued and rotated tokens and their canonical encodings before printing; a match withholds both buffered streams and returns `SECRET_IN_OUTPUT`.
|
|
725
|
+
SSO approval instructions are displayed before session issuance; managed password input and the private SSO device code are also checked against buffered output.
|
|
714
726
|
|
|
715
727
|
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
728
|
Tool exit codes pass through as usual.
|
package/bin/artium-axi.mjs
CHANGED
|
@@ -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.4",
|
|
20456
20456
|
private: true,
|
|
20457
20457
|
license: "MIT",
|
|
20458
20458
|
type: "module",
|
|
@@ -20726,8 +20726,10 @@ var Client = class {
|
|
|
20726
20726
|
if (result.status !== 200 && result.status !== 204) throw new ArtiumError("INVALID_RESPONSE");
|
|
20727
20727
|
if ((path === "/v1/auth/login" || path === "/v1/auth/device") && result.serverVersion && result.serverVersion !== cliVersion) {
|
|
20728
20728
|
const version2 = result.serverVersion.replace(/[\x00-\x1f\x7f]/g, "");
|
|
20729
|
-
|
|
20730
|
-
|
|
20729
|
+
const warning = `warning: CLI ${cliVersion} differs from server ${version2}; download the matching client from ${this.serverUrl}/cli/artium-axi.mjs and verify its .sha256, or run npm install -g @knowttl/artium-axi@${version2}.
|
|
20730
|
+
`;
|
|
20731
|
+
if (this.managed) this.managed.write("stderr", warning);
|
|
20732
|
+
else process.stderr.write(warning);
|
|
20731
20733
|
}
|
|
20732
20734
|
return result.body;
|
|
20733
20735
|
}
|
|
@@ -22894,9 +22896,10 @@ function commandHelp(command) {
|
|
|
22894
22896
|
const flags = [
|
|
22895
22897
|
command === "falcon-axi" || command === "vectra-axi" || command === "ldap-axi" ? "--help before the tool name (default false)" : "--help (default false)",
|
|
22896
22898
|
...entry.flags,
|
|
22897
|
-
"--managed-session before the command (default false; authenticated calls
|
|
22899
|
+
"--managed-session before the command (default false; login and authenticated calls; session JSON on inherited pipe fd 3, new sessions and rotations as JSON lines on private pipe fd 4; no session file)",
|
|
22898
22900
|
"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
|
|
22901
|
+
"Managed login: fd 3 is unused; fd 4 must be a writable private pipe/socket. Local sign-in requires --username <username> --password-stdin with protected inherited stdin; password argv and ARTIUM_AXI_PASSWORD are refused. SSO uses login --sso and displays browser approval instructions immediately. New sessions use the rotation envelope; password-change-required returns {serverUrl, status, passwordChangeToken, passwordChangeExpiresAt} on fd 4 and exits 1.",
|
|
22902
|
+
"Managed exits: 0 success; 2 invalid JSON/descriptors or refused 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
22903
|
];
|
|
22901
22904
|
return `usage: ${entry.usage}
|
|
22902
22905
|
purpose: ${entry.purpose}
|
|
@@ -34213,10 +34216,11 @@ async function toolInput(tool, original) {
|
|
|
34213
34216
|
|
|
34214
34217
|
// src/cli/device.ts
|
|
34215
34218
|
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
34216
|
-
async function ssoLogin(client) {
|
|
34219
|
+
async function ssoLogin(client, managed2) {
|
|
34217
34220
|
const parsed = deviceStartResponseSchema.safeParse(await client.request("/v1/auth/device", "POST", {}));
|
|
34218
34221
|
if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
|
|
34219
34222
|
const device = parsed.data;
|
|
34223
|
+
managed2?.protect(device.deviceCode);
|
|
34220
34224
|
if (device.verificationUri !== client.serverUrl + "/device") throw new ArtiumError("INVALID_RESPONSE");
|
|
34221
34225
|
process.stdout.write(`status: approval-required
|
|
34222
34226
|
open: ${JSON.stringify(device.verificationUri)}
|
|
@@ -34230,13 +34234,16 @@ expires: ${new Date(device.expiresAt).toISOString()}
|
|
|
34230
34234
|
if (!response.success || response.data.status === "password-change-required") throw new ArtiumError("INVALID_RESPONSE");
|
|
34231
34235
|
if (response.data.status === "authenticated") {
|
|
34232
34236
|
const session = response.data;
|
|
34233
|
-
await client.
|
|
34237
|
+
if (managed2) await managed2.emit(client.serverUrl, session);
|
|
34238
|
+
else await client.sessions.locked(async () => {
|
|
34234
34239
|
client.sessions.write(session);
|
|
34235
34240
|
});
|
|
34236
|
-
|
|
34241
|
+
const message = `status: authenticated
|
|
34237
34242
|
session-ends: ${new Date(session.familyExpiresAt).toISOString()}
|
|
34238
34243
|
next: artium-axi tools
|
|
34239
|
-
|
|
34244
|
+
`;
|
|
34245
|
+
if (managed2) managed2.write("stdout", message);
|
|
34246
|
+
else process.stdout.write(message);
|
|
34240
34247
|
return;
|
|
34241
34248
|
}
|
|
34242
34249
|
} catch (error62) {
|
|
@@ -34316,50 +34323,54 @@ var sessionSchema = external_exports.strictObject({
|
|
|
34316
34323
|
familyExpiresAt: external_exports.number().int().optional()
|
|
34317
34324
|
});
|
|
34318
34325
|
var ManagedSession = class _ManagedSession {
|
|
34319
|
-
constructor(
|
|
34320
|
-
this.session = session;
|
|
34326
|
+
constructor(channel, session) {
|
|
34321
34327
|
this.channel = channel;
|
|
34322
|
-
this.
|
|
34328
|
+
this.session = session;
|
|
34329
|
+
if (session) this.protect(session.accessToken, ...session.refreshToken ? [session.refreshToken] : []);
|
|
34323
34330
|
}
|
|
34324
|
-
session;
|
|
34325
34331
|
channel;
|
|
34326
|
-
|
|
34332
|
+
session;
|
|
34333
|
+
secrets = [];
|
|
34327
34334
|
output = { stdout: "", stderr: "" };
|
|
34328
|
-
static async read() {
|
|
34335
|
+
static async read(signIn = false) {
|
|
34329
34336
|
try {
|
|
34330
|
-
for (const fd of [3, 4]) {
|
|
34337
|
+
for (const fd of signIn ? [4] : [3, 4]) {
|
|
34331
34338
|
const stat = fstatSync(fd);
|
|
34332
34339
|
if (!stat.isFIFO() && !stat.isSocket()) throw new Error();
|
|
34333
34340
|
}
|
|
34334
34341
|
writeSync(4, Buffer.alloc(0));
|
|
34335
|
-
|
|
34336
|
-
|
|
34337
|
-
|
|
34338
|
-
|
|
34339
|
-
bytes
|
|
34340
|
-
|
|
34341
|
-
|
|
34342
|
-
|
|
34342
|
+
let session;
|
|
34343
|
+
if (!signIn) {
|
|
34344
|
+
const input2 = new Socket({ fd: 3, readable: true, writable: false });
|
|
34345
|
+
const chunks = [];
|
|
34346
|
+
let bytes = 0;
|
|
34347
|
+
for await (const chunk of input2) {
|
|
34348
|
+
bytes += chunk.length;
|
|
34349
|
+
if (bytes > 64 * 1024) {
|
|
34350
|
+
input2.destroy();
|
|
34351
|
+
throw new Error();
|
|
34352
|
+
}
|
|
34353
|
+
chunks.push(chunk);
|
|
34343
34354
|
}
|
|
34344
|
-
chunks.
|
|
34355
|
+
session = sessionSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
34345
34356
|
}
|
|
34346
|
-
const session = sessionSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
34347
34357
|
const channel = new Socket({ fd: 4, readable: false, writable: true });
|
|
34348
34358
|
channel.on("error", () => {
|
|
34349
34359
|
});
|
|
34350
|
-
return new _ManagedSession(
|
|
34360
|
+
return new _ManagedSession(channel, session);
|
|
34351
34361
|
} catch {
|
|
34352
34362
|
throw new ArtiumError(
|
|
34353
34363
|
"INVALID_INPUT",
|
|
34354
34364
|
"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."
|
|
34365
|
+
signIn ? "Supply a writable private pipe on fd 4 for the new session." : "Supply session JSON on inherited pipe fd 3 and a writable private pipe on fd 4; close fd 3 after sending JSON."
|
|
34356
34366
|
);
|
|
34357
34367
|
}
|
|
34358
34368
|
}
|
|
34359
34369
|
checkServer(serverUrl) {
|
|
34360
|
-
if (this.session.serverUrl.replace(/\/$/, "") !== serverUrl) throw new ArtiumError("SESSION_SERVER_MISMATCH");
|
|
34370
|
+
if (this.session && this.session.serverUrl.replace(/\/$/, "") !== serverUrl) throw new ArtiumError("SESSION_SERVER_MISMATCH");
|
|
34361
34371
|
}
|
|
34362
34372
|
async authenticated(request, refresh) {
|
|
34373
|
+
if (!this.session) throw new ArtiumError("TOKEN_INVALID");
|
|
34363
34374
|
try {
|
|
34364
34375
|
return await request(this.session.accessToken);
|
|
34365
34376
|
} catch (error62) {
|
|
@@ -34369,21 +34380,28 @@ var ManagedSession = class _ManagedSession {
|
|
|
34369
34380
|
if (!parsed.success || parsed.data.status !== "authenticated" || !parsed.data.accessToken || !parsed.data.refreshToken) {
|
|
34370
34381
|
throw new ArtiumError("INVALID_RESPONSE");
|
|
34371
34382
|
}
|
|
34383
|
+
await this.emit(this.session.serverUrl, parsed.data);
|
|
34372
34384
|
this.session = { serverUrl: this.session.serverUrl, ...parsed.data };
|
|
34373
|
-
this.
|
|
34385
|
+
return request(this.session.accessToken);
|
|
34386
|
+
}
|
|
34387
|
+
protect(...secrets) {
|
|
34388
|
+
this.secrets.push(...secrets.filter((secret) => secret.length > 0));
|
|
34389
|
+
}
|
|
34390
|
+
async emit(serverUrl, session) {
|
|
34391
|
+
if (session.status === "authenticated") this.protect(session.accessToken, session.refreshToken);
|
|
34392
|
+
else this.protect(session.passwordChangeToken);
|
|
34374
34393
|
try {
|
|
34375
34394
|
await new Promise((resolve3, reject) => {
|
|
34376
|
-
this.channel.write(`${JSON.stringify(
|
|
34395
|
+
this.channel.write(`${JSON.stringify({ serverUrl, ...session })}
|
|
34377
34396
|
`, (error62) => error62 ? reject(error62) : resolve3());
|
|
34378
34397
|
});
|
|
34379
34398
|
} catch {
|
|
34380
34399
|
throw new ArtiumError(
|
|
34381
34400
|
"INVALID_INPUT",
|
|
34382
|
-
"Managed session
|
|
34383
|
-
"Recover the session through the caller; do not
|
|
34401
|
+
"Managed session could not be returned.",
|
|
34402
|
+
"Recover the session through the caller; do not reuse a consumed refresh token."
|
|
34384
34403
|
);
|
|
34385
34404
|
}
|
|
34386
|
-
return request(this.session.accessToken);
|
|
34387
34405
|
}
|
|
34388
34406
|
write(stream, text2) {
|
|
34389
34407
|
this.output[stream] += text2;
|
|
@@ -34409,14 +34427,14 @@ async function main(args) {
|
|
|
34409
34427
|
const managedMode = args[0] === "--managed-session";
|
|
34410
34428
|
if (managedMode) {
|
|
34411
34429
|
args = args.slice(1);
|
|
34412
|
-
if (["
|
|
34430
|
+
if (["passwd", "logout", "setup"].includes(args[0])) {
|
|
34413
34431
|
throw new ArtiumError(
|
|
34414
34432
|
"INVALID_INPUT",
|
|
34415
34433
|
"This command is unavailable in managed session mode.",
|
|
34416
|
-
"The caller
|
|
34434
|
+
"The caller manages session persistence, password changes and revocation."
|
|
34417
34435
|
);
|
|
34418
34436
|
}
|
|
34419
|
-
managed = await ManagedSession.read();
|
|
34437
|
+
managed = await ManagedSession.read(args[0] === "login");
|
|
34420
34438
|
}
|
|
34421
34439
|
if (args.length === 1 && args[0] === "--version") {
|
|
34422
34440
|
output2("stdout", `${cliVersion}
|
|
@@ -34444,8 +34462,15 @@ async function main(args) {
|
|
|
34444
34462
|
}
|
|
34445
34463
|
if (command === "login" || command === "passwd") {
|
|
34446
34464
|
const remaining = args.slice(1);
|
|
34465
|
+
if (managed && process.env.ARTIUM_AXI_PASSWORD !== void 0) {
|
|
34466
|
+
throw new ArtiumError(
|
|
34467
|
+
"INVALID_INPUT",
|
|
34468
|
+
"Password environment values are refused in managed mode.",
|
|
34469
|
+
"Unset ARTIUM_AXI_PASSWORD; use --password-stdin for local sign-in or --sso."
|
|
34470
|
+
);
|
|
34471
|
+
}
|
|
34447
34472
|
if (command === "login" && remaining.length === 1 && remaining[0] === "--sso") {
|
|
34448
|
-
await ssoLogin(new Client());
|
|
34473
|
+
await ssoLogin(new Client(process.env, managed), managed);
|
|
34449
34474
|
return;
|
|
34450
34475
|
}
|
|
34451
34476
|
let stdin = false;
|
|
@@ -34464,15 +34489,25 @@ async function main(args) {
|
|
|
34464
34489
|
command === "login" ? "Use artium-axi login --sso or artium-axi login --username <username> --password-stdin." : "Use artium-axi passwd --password-stdin."
|
|
34465
34490
|
);
|
|
34466
34491
|
}
|
|
34467
|
-
const client2 = new Client();
|
|
34492
|
+
const client2 = new Client(process.env, managed);
|
|
34468
34493
|
if (command === "login") {
|
|
34494
|
+
if (managed && !stdin) {
|
|
34495
|
+
throw new ArtiumError(
|
|
34496
|
+
"INVALID_INPUT",
|
|
34497
|
+
"Managed login requires protected password input.",
|
|
34498
|
+
"Use --password-stdin with inherited stdin; unset ARTIUM_AXI_PASSWORD. Password arguments and environment values are refused."
|
|
34499
|
+
);
|
|
34500
|
+
}
|
|
34469
34501
|
const password = await readPassword(stdin, "ARTIUM_AXI_PASSWORD");
|
|
34470
|
-
|
|
34502
|
+
managed?.protect(password);
|
|
34503
|
+
const signIn = async () => {
|
|
34471
34504
|
const parsed = loginResponseSchema.safeParse(await client2.request("/v1/auth/login", "POST", { username, password }));
|
|
34472
34505
|
if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
|
|
34473
|
-
client2.
|
|
34506
|
+
if (managed) await managed.emit(client2.serverUrl, parsed.data);
|
|
34507
|
+
else client2.sessions.write(parsed.data);
|
|
34474
34508
|
return parsed.data;
|
|
34475
|
-
}
|
|
34509
|
+
};
|
|
34510
|
+
const session = await (managed ? signIn() : client2.sessions.locked(signIn));
|
|
34476
34511
|
if (session.status === "password-change-required") throw new ArtiumError("PASSWORD_CHANGE_REQUIRED");
|
|
34477
34512
|
output2("stdout", "status: authenticated\nnext: artium-axi tools\n");
|
|
34478
34513
|
} else {
|