@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 +35 -16
- package/bin/artium-axi.mjs +79 -47
- package/package.json +1 -1
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` |
|
|
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
|
-
|
|
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
|
|
697
|
-
Managed mode supports the home view, `whoami`, `tools`, `tools show`, `doctor` and tool commands.
|
|
698
|
-
`
|
|
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
|
|
713
|
-
|
|
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
|
|
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` |
|
|
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
|
|
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
|
|
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).
|
package/bin/artium-axi.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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(
|
|
34320
|
-
this.session = session;
|
|
34323
|
+
constructor(channel, session) {
|
|
34321
34324
|
this.channel = channel;
|
|
34322
|
-
this.
|
|
34325
|
+
this.session = session;
|
|
34326
|
+
if (session) this.protect(session.accessToken, ...session.refreshToken ? [session.refreshToken] : []);
|
|
34323
34327
|
}
|
|
34324
|
-
session;
|
|
34325
34328
|
channel;
|
|
34326
|
-
|
|
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
|
-
|
|
34336
|
-
|
|
34337
|
-
|
|
34338
|
-
|
|
34339
|
-
bytes
|
|
34340
|
-
|
|
34341
|
-
|
|
34342
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
|
34383
|
-
"Recover the session through the caller; do not
|
|
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 (["
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 {
|