@knowttl/artium-axi 0.0.3 → 0.0.5

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
@@ -337,7 +337,8 @@ The native Kerberos provider and its installation script are unnecessary for thi
337
337
 
338
338
  Obtain a dedicated directory identity with read access only, its bind DN and password, and a directory URL whose certificate can be verified.
339
339
  At `/admin/tools/ldap-axi`, enter `url`, choose `auth=simple`, and enter `bindDn` and the password value in `bindPassword`.
340
- Use `ldaps://<directory-host>:636`, or `ldap://<directory-host>:389` with `startTls=true`.
340
+ Use `ldaps://<directory-host>:636` with `startTls` left at the default (unset) or explicitly `false`, or `ldap://<directory-host>:389` with `startTls=true`.
341
+ Saving an `ldaps://` URL with `startTls=true` is rejected with `INVALID_INPUT` and the message `startTls must be false for this URL.`
341
342
  Plaintext simple bind, anonymous authentication, disabled certificate verification and caller-supplied connection or credential overrides are refused.
342
343
  If a private CA is required, supply its PEM contents in `caFile`, not a server file path.
343
344
  The multi-line `caFile` editor preserves newlines through saving and private CA file materialization for each call.
@@ -652,7 +653,7 @@ See the [API contract](src/shared/api.ts) and [retention and export runbook](doc
652
653
  | --- | --- |
653
654
  | `ARTIUM_AXI_URL` | Required HTTPS base URL without credentials, query or fragment. |
654
655
  | `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. |
656
+ | `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
657
  | `ARTIUM_AXI_CURRENT_PASSWORD` | Current password for an ordinary password change, before the terminal prompt. |
657
658
  | `XDG_CONFIG_HOME` | Session-file base on Linux/macOS; otherwise `~/.config`. |
658
659
  | `APPDATA` | Session-file base on Windows, whose execution remains unverified. |
@@ -679,12 +680,12 @@ A network failure or uncertain command outcome is never retried automatically.
679
680
  ### Externally managed client sessions
680
681
 
681
682
  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
+ 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
684
  Stdin remains available for tool input; stdout and stderr retain their normal command formats.
684
685
  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
686
  The CLI refuses regular files as private channels and never reads, creates, locks, updates or deletes `session.json` in this mode.
686
687
 
687
- Send one UTF-8 JSON object of at most 64 KiB to fd 3, then close the input pipe:
688
+ For authenticated calls, send one UTF-8 JSON object of at most 64 KiB to fd 3, then close the input pipe:
688
689
 
689
690
  ```json
690
691
  {"serverUrl":"https://artium.example.test","accessToken":"CANARY-access","refreshToken":"CANARY-refresh"}
@@ -693,11 +694,21 @@ Send one UTF-8 JSON object of at most 64 KiB to fd 3, then close the input pipe:
693
694
  `serverUrl` must match the normalized `ARTIUM_AXI_URL`, allowing a final slash.
694
695
  `accessToken` is required and nonempty; `refreshToken` is optional and nonempty when supplied.
695
696
  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.
697
+ The caller owns expiry policy, refresh serialization across processes and secure persistence.
698
+ Managed mode supports `login`, the home view, `whoami`, `tools`, `tools show`, `doctor` and tool commands.
699
+ `passwd`, `logout` and `setup` are refused before any network call; ordinary sign-in and its server-version warning remain unchanged without the flag.
699
700
  Help is available without private channels through `artium-axi --help`.
700
701
 
702
+ 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.
703
+ Login does not read or require fd 3; it requires a writable private pipe or socket on fd 4 before contacting the server.
704
+ Password arguments and `ARTIUM_AXI_PASSWORD` are refused, including when `--password-stdin` is present; unset that variable before managed sign-in.
705
+ Password input follows the stdin rules in [client settings](#client-settings).
706
+ SSO displays the approval URL and user code immediately so the user can approve while the CLI waits.
707
+ 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.
708
+ The login server-version warning still appears on stderr when versions differ.
709
+ 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).
710
+ That restricted record is not valid managed-session input; the caller must complete the password change through the server API and sign in again.
711
+
701
712
  Only an explicit `TOKEN_EXPIRED` response permits a single refresh and retry of that request.
702
713
  Without a refresh token, or if refresh is refused, the CLI returns the authentication error for the caller to handle.
703
714
  Network failures and uncertain outcomes are never automatically retried.
@@ -707,10 +718,12 @@ After a successful refresh, fd 4 receives one newline-terminated JSON object bef
707
718
  {"serverUrl":"https://artium.example.test","status":"authenticated","accessToken":"CANARY-new-access","refreshToken":"CANARY-new-refresh","accessExpiresAt":1800000000000,"familyExpiresAt":1800003600000}
708
719
  ```
709
720
 
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`.
721
+ Every rotation is delivered even if the subsequent request fails; an authenticated invocation without rotation writes no record.
722
+ Consume fd 4 concurrently with stdout and stderr, and retain new sessions and rotations regardless of the command's exit status.
723
+ If new-session or rotation delivery fails, the CLI returns `INVALID_INPUT` (exit 2); the caller must recover the session.
724
+ After failed rotation delivery, the CLI refuses to retry the request; do not reuse the consumed refresh token.
725
+ 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`.
726
+ SSO approval instructions are displayed before session issuance; managed password input and the private SSO device code are also checked against buffered output.
714
727
 
715
728
  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
729
  Tool exit codes pass through as usual.
@@ -750,7 +763,8 @@ Fake-service results do not establish live platform permissions or production re
750
763
  Never put tokens or keys in arguments, source files, images or logs.
751
764
  The server refuses the removed development-authentication settings `ARTIUM_AXI_DEV` and `ARTIUM_AXI_DEV_TOKEN_FILE`.
752
765
 
753
- Local passwords use Argon2id, require 15 to 128 code points and at most 512 UTF-8 bytes, preserve spaces and Unicode, and reject exact matches in the bundled common-password list.
766
+ Local passwords use Argon2id and accept any non-empty password of at most 512 UTF-8 bytes, preserving spaces and Unicode.
767
+ The byte cap bounds input to native hashing; there are no minimum-length, character-count or common-password rules.
754
768
  Access tokens last one hour within an 8-hour SSO CLI family or a 12-hour local CLI family; refresh-token reuse revokes the family.
755
769
  Password changes revoke existing CLI tokens and browser sessions.
756
770
  See [browser administration](#browser-administration) for session management and user archival.
@@ -790,18 +804,23 @@ pnpm test:containers
790
804
  See [checklist verification](docs/verification-s46.md) for the fake-only download, authentication, setup and first-call checks.
791
805
  It refuses unverified host platforms.
792
806
 
807
+ The [Vitest configuration](vitest.config.ts) separates unit and integration projects.
808
+ Only the integration project loads the [reduced-cost password hashing setup](test/fixtures/integration-setup.ts); local authentication unit tests and the native smoke retain production hashing costs, as does the dummy hash used for missing accounts.
809
+
793
810
  | Command | Scope |
794
811
  | --- | --- |
795
812
  | `pnpm lint` | Static TypeScript checking. |
796
813
  | `pnpm test` | Inventory/schema freshness checks and the full Vitest suite. |
797
814
  | `pnpm build` | Server compilation and standalone client bundle. |
798
815
  | `pnpm test:security` | Tests whose names contain `security`. |
799
- | `pnpm test:containers` | Mandatory fake-only runner, walking-skeleton, Graph headless and fake-service container acceptance. |
816
+ | `pnpm test:containers` | Fake-only runner, walking-skeleton, Graph headless and fake-service container acceptance. |
800
817
  | `pnpm test:browser` | Real admin pages in an isolated Chrome session through `chrome-devtools-axi`, against the local fake browser harness. |
801
818
  | `pnpm test:containers --measure-cache` | The container block plus an identical-input warm runner build measurement. |
802
819
  | `pnpm errors:generate` | Regenerate the error reference after changing the catalogue. |
803
820
 
804
- The container block builds before probing, emits head/input/output evidence and timings, and removes its uniquely owned test images and private BuildKit cache.
821
+ The container block builds before running independent acceptance phases concurrently, emits head/input/output evidence and timings, and waits for every phase before removing its uniquely owned test images and private BuildKit cache.
822
+ The [gate configuration](.no-mistakes.yaml) owns the paths that require container acceptance and the CLI, documentation, workflow and skill-only exemptions.
823
+ Every release requires container acceptance under the [release prerequisites](docs/runbook.md#npm-client-releases).
805
824
  Do not touch shared Docker resources or use real service credentials.
806
825
  Changed relevant inputs invalidate earlier container evidence.
807
826
  Upstream pins and integrity records live in [tools/package.json](tools/package.json), [tools/integrity.json](tools/integrity.json) and [tools/falcon-source.json](tools/falcon-source.json).
@@ -815,7 +834,7 @@ Before publishing a candidate, commit and build it, then set `ARTIUM_AXI_HYGIENE
815
834
  pnpm hygiene:check
816
835
  ```
817
836
 
818
- The UTF-8 list contains one literal term per line, matched case-insensitively after trimming whitespace; blanks and duplicates are ignored.
837
+ The UTF-8 list contains one literal term per line, matched after trimming whitespace using context-independent Unicode simple case folding; blanks and case-equivalent duplicates are ignored.
819
838
  Unset, unreadable, empty or in-tree lists fail.
820
839
  Never copy real terms or the list into this repository.
821
840
  Record only its revision or SHA-256, candidate commit and pass/fail result.
@@ -823,7 +842,7 @@ Record only its revision or SHA-256, candidate commit and pass/fail result.
823
842
  The check scans tracked, untracked and ignored files, generated output and every commit message reachable from `HEAD`, excluding only `.git` metadata and installed `node_modules` directories.
824
843
  Supply intended release assets outside the checkout as additional `pnpm hygiene:check` arguments, keeping the list outside those directories too.
825
844
  Symlinks, unreadable files and BOM-marked UTF-16/UTF-32 assets fail rather than reducing coverage; convert unsupported encodings to UTF-8.
826
- Diagnostics report location and normalized entry number without secret excerpts, and redact labels whose paths match a term.
845
+ Diagnostics report location (with one-based columns in the original text's UTF-16 code units) and normalized entry number without secret excerpts, and redact labels whose paths match a term using the same folding rule.
827
846
  The scan cannot guarantee absence of unknown sensitive values and does not approve publication.
828
847
 
829
848
  Further operational guidance lives in the [runbook](docs/runbook.md); local implementation and deferred live acceptance are tracked in the [evidence ledger](docs/evidence-ledger.md).
@@ -19692,7 +19692,7 @@ var definitions = {
19692
19692
  PASSWORD_FILE_INVALID: { status: 400, message: "The password file is missing or unreadable.", next: "Supply a readable UTF-8 password file containing the exact password, without an added newline." },
19693
19693
  BOOTSTRAP_INCONSISTENT: { status: 503, message: "The store contains data without a bootstrap marker.", next: "Stop the server and restore a consistent bootstrapped store and matching master key." },
19694
19694
  RECOVERY_FAILED: { status: 500, message: "Administrator recovery did not complete.", next: "Use an existing local username in a bootstrapped store, check the matching master key and inspect local storage." },
19695
- PASSWORD_INVALID: { status: 400, message: "The password does not meet the local password rules.", next: "Use 15 to 128 Unicode code points, at most 512 UTF-8 bytes, and avoid common passwords." },
19695
+ PASSWORD_INVALID: { status: 400, message: "The password must be non-empty and at most 512 UTF-8 bytes.", next: "Enter a password of at most 512 UTF-8 bytes." },
19696
19696
  SIGN_IN_FAILED: { status: 401, message: "The username or password is incorrect.", next: "Check your username and password." },
19697
19697
  SSO_STATE_INVALID: { status: 400, message: "The SSO sign-in request expired or was already used.", next: "Start a new sign-in from this browser." },
19698
19698
  SSO_TOKEN_INVALID: { status: 401, message: "The identity provider response could not be verified.", next: "Start a new sign-in or ask an administrator to check provider settings." },
@@ -19776,10 +19776,7 @@ var inputLimits = {
19776
19776
  files: 8
19777
19777
  };
19778
19778
  var byteLength = (value) => new TextEncoder().encode(value).length;
19779
- var passwordSchema = external_exports.string().refine((value) => {
19780
- const length = [...value].length;
19781
- return length >= 15 && length <= 128 && byteLength(value) <= 512;
19782
- }, "Expected 15 to 128 Unicode code points and at most 512 UTF-8 bytes");
19779
+ var passwordSchema = external_exports.string().min(1).refine((value) => byteLength(value) <= 512, "Expected at most 512 UTF-8 bytes");
19783
19780
  var passwordChangeSchema = external_exports.strictObject({ newPassword: external_exports.string(), currentPassword: external_exports.string().optional() });
19784
19781
  var loginSchema = external_exports.strictObject({ username: external_exports.string(), password: external_exports.string() });
19785
19782
  var refreshSchema = external_exports.strictObject({ refreshToken: external_exports.string().min(1) });
@@ -20452,7 +20449,7 @@ import { rootCertificates } from "node:tls";
20452
20449
  // package.json
20453
20450
  var package_default = {
20454
20451
  name: "artium-axi",
20455
- version: "0.0.3",
20452
+ version: "0.0.5",
20456
20453
  private: true,
20457
20454
  license: "MIT",
20458
20455
  type: "module",
@@ -20726,8 +20723,10 @@ var Client = class {
20726
20723
  if (result.status !== 200 && result.status !== 204) throw new ArtiumError("INVALID_RESPONSE");
20727
20724
  if ((path === "/v1/auth/login" || path === "/v1/auth/device") && result.serverVersion && result.serverVersion !== cliVersion) {
20728
20725
  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
- `);
20726
+ 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}.
20727
+ `;
20728
+ if (this.managed) this.managed.write("stderr", warning);
20729
+ else process.stderr.write(warning);
20731
20730
  }
20732
20731
  return result.body;
20733
20732
  }
@@ -22894,9 +22893,10 @@ function commandHelp(command) {
22894
22893
  const flags = [
22895
22894
  command === "falcon-axi" || command === "vectra-axi" || command === "ldap-axi" ? "--help before the tool name (default false)" : "--help (default false)",
22896
22895
  ...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)",
22896
+ "--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
22897
  "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."
22898
+ "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.",
22899
+ "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
22900
  ];
22901
22901
  return `usage: ${entry.usage}
22902
22902
  purpose: ${entry.purpose}
@@ -34213,10 +34213,11 @@ async function toolInput(tool, original) {
34213
34213
 
34214
34214
  // src/cli/device.ts
34215
34215
  import { setTimeout as setTimeout2 } from "node:timers/promises";
34216
- async function ssoLogin(client) {
34216
+ async function ssoLogin(client, managed2) {
34217
34217
  const parsed = deviceStartResponseSchema.safeParse(await client.request("/v1/auth/device", "POST", {}));
34218
34218
  if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
34219
34219
  const device = parsed.data;
34220
+ managed2?.protect(device.deviceCode);
34220
34221
  if (device.verificationUri !== client.serverUrl + "/device") throw new ArtiumError("INVALID_RESPONSE");
34221
34222
  process.stdout.write(`status: approval-required
34222
34223
  open: ${JSON.stringify(device.verificationUri)}
@@ -34230,13 +34231,16 @@ expires: ${new Date(device.expiresAt).toISOString()}
34230
34231
  if (!response.success || response.data.status === "password-change-required") throw new ArtiumError("INVALID_RESPONSE");
34231
34232
  if (response.data.status === "authenticated") {
34232
34233
  const session = response.data;
34233
- await client.sessions.locked(async () => {
34234
+ if (managed2) await managed2.emit(client.serverUrl, session);
34235
+ else await client.sessions.locked(async () => {
34234
34236
  client.sessions.write(session);
34235
34237
  });
34236
- process.stdout.write(`status: authenticated
34238
+ const message = `status: authenticated
34237
34239
  session-ends: ${new Date(session.familyExpiresAt).toISOString()}
34238
34240
  next: artium-axi tools
34239
- `);
34241
+ `;
34242
+ if (managed2) managed2.write("stdout", message);
34243
+ else process.stdout.write(message);
34240
34244
  return;
34241
34245
  }
34242
34246
  } catch (error62) {
@@ -34316,50 +34320,54 @@ var sessionSchema = external_exports.strictObject({
34316
34320
  familyExpiresAt: external_exports.number().int().optional()
34317
34321
  });
34318
34322
  var ManagedSession = class _ManagedSession {
34319
- constructor(session, channel) {
34320
- this.session = session;
34323
+ constructor(channel, session) {
34321
34324
  this.channel = channel;
34322
- this.secrets = [session.accessToken, ...session.refreshToken ? [session.refreshToken] : []];
34325
+ this.session = session;
34326
+ if (session) this.protect(session.accessToken, ...session.refreshToken ? [session.refreshToken] : []);
34323
34327
  }
34324
- session;
34325
34328
  channel;
34326
- secrets;
34329
+ session;
34330
+ secrets = [];
34327
34331
  output = { stdout: "", stderr: "" };
34328
- static async read() {
34332
+ static async read(signIn = false) {
34329
34333
  try {
34330
- for (const fd of [3, 4]) {
34334
+ for (const fd of signIn ? [4] : [3, 4]) {
34331
34335
  const stat = fstatSync(fd);
34332
34336
  if (!stat.isFIFO() && !stat.isSocket()) throw new Error();
34333
34337
  }
34334
34338
  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();
34339
+ let session;
34340
+ if (!signIn) {
34341
+ const input2 = new Socket({ fd: 3, readable: true, writable: false });
34342
+ const chunks = [];
34343
+ let bytes = 0;
34344
+ for await (const chunk of input2) {
34345
+ bytes += chunk.length;
34346
+ if (bytes > 64 * 1024) {
34347
+ input2.destroy();
34348
+ throw new Error();
34349
+ }
34350
+ chunks.push(chunk);
34343
34351
  }
34344
- chunks.push(chunk);
34352
+ session = sessionSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
34345
34353
  }
34346
- const session = sessionSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
34347
34354
  const channel = new Socket({ fd: 4, readable: false, writable: true });
34348
34355
  channel.on("error", () => {
34349
34356
  });
34350
- return new _ManagedSession(session, channel);
34357
+ return new _ManagedSession(channel, session);
34351
34358
  } catch {
34352
34359
  throw new ArtiumError(
34353
34360
  "INVALID_INPUT",
34354
34361
  "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."
34362
+ 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
34363
  );
34357
34364
  }
34358
34365
  }
34359
34366
  checkServer(serverUrl) {
34360
- if (this.session.serverUrl.replace(/\/$/, "") !== serverUrl) throw new ArtiumError("SESSION_SERVER_MISMATCH");
34367
+ if (this.session && this.session.serverUrl.replace(/\/$/, "") !== serverUrl) throw new ArtiumError("SESSION_SERVER_MISMATCH");
34361
34368
  }
34362
34369
  async authenticated(request, refresh) {
34370
+ if (!this.session) throw new ArtiumError("TOKEN_INVALID");
34363
34371
  try {
34364
34372
  return await request(this.session.accessToken);
34365
34373
  } catch (error62) {
@@ -34369,21 +34377,28 @@ var ManagedSession = class _ManagedSession {
34369
34377
  if (!parsed.success || parsed.data.status !== "authenticated" || !parsed.data.accessToken || !parsed.data.refreshToken) {
34370
34378
  throw new ArtiumError("INVALID_RESPONSE");
34371
34379
  }
34380
+ await this.emit(this.session.serverUrl, parsed.data);
34372
34381
  this.session = { serverUrl: this.session.serverUrl, ...parsed.data };
34373
- this.secrets.push(parsed.data.accessToken, parsed.data.refreshToken);
34382
+ return request(this.session.accessToken);
34383
+ }
34384
+ protect(...secrets) {
34385
+ this.secrets.push(...secrets.filter((secret) => secret.length > 0));
34386
+ }
34387
+ async emit(serverUrl, session) {
34388
+ if (session.status === "authenticated") this.protect(session.accessToken, session.refreshToken);
34389
+ else this.protect(session.passwordChangeToken);
34374
34390
  try {
34375
34391
  await new Promise((resolve3, reject) => {
34376
- this.channel.write(`${JSON.stringify(this.session)}
34392
+ this.channel.write(`${JSON.stringify({ serverUrl, ...session })}
34377
34393
  `, (error62) => error62 ? reject(error62) : resolve3());
34378
34394
  });
34379
34395
  } catch {
34380
34396
  throw new ArtiumError(
34381
34397
  "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."
34398
+ "Managed session could not be returned.",
34399
+ "Recover the session through the caller; do not reuse a consumed refresh token."
34384
34400
  );
34385
34401
  }
34386
- return request(this.session.accessToken);
34387
34402
  }
34388
34403
  write(stream, text2) {
34389
34404
  this.output[stream] += text2;
@@ -34409,14 +34424,14 @@ async function main(args) {
34409
34424
  const managedMode = args[0] === "--managed-session";
34410
34425
  if (managedMode) {
34411
34426
  args = args.slice(1);
34412
- if (["login", "passwd", "logout", "setup"].includes(args[0])) {
34427
+ if (["passwd", "logout", "setup"].includes(args[0])) {
34413
34428
  throw new ArtiumError(
34414
34429
  "INVALID_INPUT",
34415
34430
  "This command is unavailable in managed session mode.",
34416
- "The caller must acquire and manage tokens; use managed mode for authenticated calls."
34431
+ "The caller manages session persistence, password changes and revocation."
34417
34432
  );
34418
34433
  }
34419
- managed = await ManagedSession.read();
34434
+ managed = await ManagedSession.read(args[0] === "login");
34420
34435
  }
34421
34436
  if (args.length === 1 && args[0] === "--version") {
34422
34437
  output2("stdout", `${cliVersion}
@@ -34444,8 +34459,15 @@ async function main(args) {
34444
34459
  }
34445
34460
  if (command === "login" || command === "passwd") {
34446
34461
  const remaining = args.slice(1);
34462
+ if (managed && process.env.ARTIUM_AXI_PASSWORD !== void 0) {
34463
+ throw new ArtiumError(
34464
+ "INVALID_INPUT",
34465
+ "Password environment values are refused in managed mode.",
34466
+ "Unset ARTIUM_AXI_PASSWORD; use --password-stdin for local sign-in or --sso."
34467
+ );
34468
+ }
34447
34469
  if (command === "login" && remaining.length === 1 && remaining[0] === "--sso") {
34448
- await ssoLogin(new Client());
34470
+ await ssoLogin(new Client(process.env, managed), managed);
34449
34471
  return;
34450
34472
  }
34451
34473
  let stdin = false;
@@ -34464,15 +34486,25 @@ async function main(args) {
34464
34486
  command === "login" ? "Use artium-axi login --sso or artium-axi login --username <username> --password-stdin." : "Use artium-axi passwd --password-stdin."
34465
34487
  );
34466
34488
  }
34467
- const client2 = new Client();
34489
+ const client2 = new Client(process.env, managed);
34468
34490
  if (command === "login") {
34491
+ if (managed && !stdin) {
34492
+ throw new ArtiumError(
34493
+ "INVALID_INPUT",
34494
+ "Managed login requires protected password input.",
34495
+ "Use --password-stdin with inherited stdin; unset ARTIUM_AXI_PASSWORD. Password arguments and environment values are refused."
34496
+ );
34497
+ }
34469
34498
  const password = await readPassword(stdin, "ARTIUM_AXI_PASSWORD");
34470
- const session = await client2.sessions.locked(async () => {
34499
+ managed?.protect(password);
34500
+ const signIn = async () => {
34471
34501
  const parsed = loginResponseSchema.safeParse(await client2.request("/v1/auth/login", "POST", { username, password }));
34472
34502
  if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
34473
- client2.sessions.write(parsed.data);
34503
+ if (managed) await managed.emit(client2.serverUrl, parsed.data);
34504
+ else client2.sessions.write(parsed.data);
34474
34505
  return parsed.data;
34475
- });
34506
+ };
34507
+ const session = await (managed ? signIn() : client2.sessions.locked(signIn));
34476
34508
  if (session.status === "password-change-required") throw new ArtiumError("PASSWORD_CHANGE_REQUIRED");
34477
34509
  output2("stdout", "status: authenticated\nnext: artium-axi tools\n");
34478
34510
  } 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.5",
4
4
  "description": "Client CLI and embedded skill for the Artium managed tools server.",
5
5
  "license": "MIT",
6
6
  "type": "module",