@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 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` | Login password source when `--password-stdin` is absent, before the terminal prompt. |
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
- Send one UTF-8 JSON object of at most 64 KiB to fd 3, then close the input pipe:
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 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.
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 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`.
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.
@@ -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.3",
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
- process.stderr.write(`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
- `);
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 only; session JSON on inherited pipe fd 3, rotations as JSON lines on private pipe fd 4; no session file)",
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 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."
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.sessions.locked(async () => {
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
- process.stdout.write(`status: authenticated
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(session, channel) {
34320
- this.session = session;
34326
+ constructor(channel, session) {
34321
34327
  this.channel = channel;
34322
- this.secrets = [session.accessToken, ...session.refreshToken ? [session.refreshToken] : []];
34328
+ this.session = session;
34329
+ if (session) this.protect(session.accessToken, ...session.refreshToken ? [session.refreshToken] : []);
34323
34330
  }
34324
- session;
34325
34331
  channel;
34326
- secrets;
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
- 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();
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.push(chunk);
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(session, channel);
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.secrets.push(parsed.data.accessToken, parsed.data.refreshToken);
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(this.session)}
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 rotation could not be returned.",
34383
- "Recover the session through the caller; do not retry using the old refresh token."
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 (["login", "passwd", "logout", "setup"].includes(args[0])) {
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 must acquire and manage tokens; use managed mode for authenticated calls."
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
- const session = await client2.sessions.locked(async () => {
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.sessions.write(parsed.data);
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowttl/artium-axi",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Client CLI and embedded skill for the Artium managed tools server.",
5
5
  "license": "MIT",
6
6
  "type": "module",