@nocoo/bat 3.1.2 → 3.2.0
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/dist/bat.js +226 -6
- package/package.json +1 -1
package/dist/bat.js
CHANGED
|
@@ -3093,7 +3093,7 @@ var init_connect = () => {};
|
|
|
3093
3093
|
var init_settings = () => {};
|
|
3094
3094
|
|
|
3095
3095
|
// ../shared/dist/version.js
|
|
3096
|
-
var BAT_VERSION = "3.
|
|
3096
|
+
var BAT_VERSION = "3.2.0";
|
|
3097
3097
|
|
|
3098
3098
|
// ../shared/dist/workstations.js
|
|
3099
3099
|
function fail(error) {
|
|
@@ -3205,6 +3205,9 @@ function ipFamily(input) {
|
|
|
3205
3205
|
}
|
|
3206
3206
|
return null;
|
|
3207
3207
|
}
|
|
3208
|
+
function isPkceS256Challenge(input) {
|
|
3209
|
+
return PKCE_CHALLENGE_PATTERN.test(input);
|
|
3210
|
+
}
|
|
3208
3211
|
function isPkceCodeVerifier(input) {
|
|
3209
3212
|
return PKCE_VERIFIER_PATTERN.test(input);
|
|
3210
3213
|
}
|
|
@@ -3665,7 +3668,65 @@ function validateWorkstationLoginExchange(input) {
|
|
|
3665
3668
|
}
|
|
3666
3669
|
};
|
|
3667
3670
|
}
|
|
3668
|
-
|
|
3671
|
+
function normalizePairingCode(input) {
|
|
3672
|
+
const compact = input.replace(/[\s-]/g, "").toUpperCase();
|
|
3673
|
+
if (compact.length !== WORKSTATION_PAIRING_LENGTH) {
|
|
3674
|
+
return null;
|
|
3675
|
+
}
|
|
3676
|
+
for (const character of compact) {
|
|
3677
|
+
if (!WORKSTATION_PAIRING_ALPHABET.includes(character)) {
|
|
3678
|
+
return null;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
return compact;
|
|
3682
|
+
}
|
|
3683
|
+
function formatPairingCode(code) {
|
|
3684
|
+
const normalized = normalizePairingCode(code);
|
|
3685
|
+
if (normalized === null) {
|
|
3686
|
+
return code;
|
|
3687
|
+
}
|
|
3688
|
+
return `${normalized.slice(0, 4)}-${normalized.slice(4)}`;
|
|
3689
|
+
}
|
|
3690
|
+
function isPairingCode(input) {
|
|
3691
|
+
return normalizePairingCode(input) !== null;
|
|
3692
|
+
}
|
|
3693
|
+
function validateWorkstationName(input) {
|
|
3694
|
+
const name = readString("name", input, WORKSTATION_NAME_MAX_LENGTH);
|
|
3695
|
+
if (!name.ok) {
|
|
3696
|
+
return name;
|
|
3697
|
+
}
|
|
3698
|
+
if (name.value.trim() === "") {
|
|
3699
|
+
return fail("name must not be blank");
|
|
3700
|
+
}
|
|
3701
|
+
return { ok: true, value: name.value.trim() };
|
|
3702
|
+
}
|
|
3703
|
+
function validateWorkstationPairStart(input) {
|
|
3704
|
+
const root = readObject("pair", input, PAIR_START_KEYS);
|
|
3705
|
+
if (!root.ok) {
|
|
3706
|
+
return root;
|
|
3707
|
+
}
|
|
3708
|
+
const installationId = readUuid("installation_id", root.value.installation_id);
|
|
3709
|
+
if (!installationId.ok) {
|
|
3710
|
+
return installationId;
|
|
3711
|
+
}
|
|
3712
|
+
const name = validateWorkstationName(root.value.name);
|
|
3713
|
+
if (!name.ok) {
|
|
3714
|
+
return name;
|
|
3715
|
+
}
|
|
3716
|
+
const challenge = root.value.code_challenge;
|
|
3717
|
+
if (typeof challenge !== "string" || !isPkceS256Challenge(challenge)) {
|
|
3718
|
+
return fail("code_challenge must be a 43 character base64url S256 digest");
|
|
3719
|
+
}
|
|
3720
|
+
return {
|
|
3721
|
+
ok: true,
|
|
3722
|
+
value: {
|
|
3723
|
+
installation_id: installationId.value,
|
|
3724
|
+
name: name.value,
|
|
3725
|
+
code_challenge: challenge
|
|
3726
|
+
}
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
var WORKSTATION_INTERVAL_SECONDS = 600, WORKSTATION_PING_COUNT = 3, WORKSTATION_MAX_TARGETS = 100, WORKSTATION_BODY_MAX_BYTES = 65536, WORKSTATION_REPORT_SKEW_SECONDS = 300, WORKSTATION_MAX_RUN_MS = 120000, WORKSTATION_STALE_AFTER_SECONDS, WORKSTATION_NAME_MAX_LENGTH = 64, WORKSTATION_PAIRING_TTL_SECONDS = 300, WORKSTATION_PAIRING_ALPHABET = "ABCDEFGHJKMNPQRSTVWXYZ23456789", WORKSTATION_PAIRING_LENGTH = 8, WORKSTATION_LIMITS, WORKSTATION_CONNECTIVITY_STATUSES, UUID_PATTERN, IPV4_PATTERN, PKCE_CHALLENGE_PATTERN, PKCE_VERIFIER_PATTERN, WORKSTATION_TOKEN_PATTERN, IPV6_CHARS, REPORT_KEYS, CPU_KEYS, MEM_KEYS, DISK_KEYS, NETWORK_KEYS, INTERFACE_KEYS, ADDRESS_KEYS, CONNECTIVITY_KEYS, TARGET_KEYS, TARGETS_RESPONSE_KEYS, LOGIN_EXCHANGE_KEYS, PAIR_START_KEYS;
|
|
3669
3730
|
var init_workstations = __esm(() => {
|
|
3670
3731
|
WORKSTATION_STALE_AFTER_SECONDS = WORKSTATION_INTERVAL_SECONDS * 3;
|
|
3671
3732
|
WORKSTATION_LIMITS = {
|
|
@@ -3701,6 +3762,7 @@ var init_workstations = __esm(() => {
|
|
|
3701
3762
|
];
|
|
3702
3763
|
UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3703
3764
|
IPV4_PATTERN = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
3765
|
+
PKCE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
3704
3766
|
PKCE_VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
3705
3767
|
WORKSTATION_TOKEN_PATTERN = /^batw_[A-Za-z0-9_-]{43}$/;
|
|
3706
3768
|
IPV6_CHARS = /^[0-9a-f:.]+$/i;
|
|
@@ -3740,6 +3802,7 @@ var init_workstations = __esm(() => {
|
|
|
3740
3802
|
TARGET_KEYS = ["server_id", "name", "address", "family"];
|
|
3741
3803
|
TARGETS_RESPONSE_KEYS = ["data", "interval_seconds", "ping_count"];
|
|
3742
3804
|
LOGIN_EXCHANGE_KEYS = ["code", "code_verifier", "installation_id"];
|
|
3805
|
+
PAIR_START_KEYS = ["installation_id", "name", "code_challenge"];
|
|
3743
3806
|
});
|
|
3744
3807
|
|
|
3745
3808
|
// ../shared/dist/index.js
|
|
@@ -4058,6 +4121,54 @@ async function revokeWorkstationKey(workerUrl, token, options = {}) {
|
|
|
4058
4121
|
throw err;
|
|
4059
4122
|
}
|
|
4060
4123
|
}
|
|
4124
|
+
async function startPairing(origins, input, options = {}) {
|
|
4125
|
+
const validated = validateWorkstationPairStart(input);
|
|
4126
|
+
if (!validated.ok) {
|
|
4127
|
+
throw new Error(`invalid pairing request: ${validated.error}`);
|
|
4128
|
+
}
|
|
4129
|
+
const client = new HttpClient(origins.machine, undefined, options.timeoutMs ?? REQUEST_TIMEOUT_MS, options.signal);
|
|
4130
|
+
const raw = await client.post("/api/workstation-auth/pair", validated.value);
|
|
4131
|
+
if (typeof raw !== "object" || raw === null) {
|
|
4132
|
+
throw new Error("pairing start returned no object");
|
|
4133
|
+
}
|
|
4134
|
+
const record = raw;
|
|
4135
|
+
if (typeof record.pairing_code !== "string" || !isPairingCode(record.pairing_code)) {
|
|
4136
|
+
throw new Error("pairing start returned no valid pairing_code");
|
|
4137
|
+
}
|
|
4138
|
+
if (typeof record.code !== "string" || record.code.length < 32) {
|
|
4139
|
+
throw new Error("pairing start returned no valid exchange code");
|
|
4140
|
+
}
|
|
4141
|
+
if (typeof record.expires_at !== "number" || !Number.isFinite(record.expires_at)) {
|
|
4142
|
+
throw new Error("pairing start returned no expiry");
|
|
4143
|
+
}
|
|
4144
|
+
return {
|
|
4145
|
+
pairing_code: record.pairing_code,
|
|
4146
|
+
code: record.code,
|
|
4147
|
+
name: typeof record.name === "string" ? record.name : "",
|
|
4148
|
+
expires_at: record.expires_at
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
async function pairingStatus(origins, pairingCode, options = {}) {
|
|
4152
|
+
const normalized = normalizePairingCode(pairingCode);
|
|
4153
|
+
if (normalized === null) {
|
|
4154
|
+
throw new Error("invalid pairing code");
|
|
4155
|
+
}
|
|
4156
|
+
const client = new HttpClient(origins.machine, undefined, options.timeoutMs ?? REQUEST_TIMEOUT_MS, options.signal);
|
|
4157
|
+
const raw = await client.get(`/api/workstation-auth/pair/${normalized}`);
|
|
4158
|
+
if (typeof raw !== "object" || raw === null) {
|
|
4159
|
+
throw new Error("pairing status returned no object");
|
|
4160
|
+
}
|
|
4161
|
+
const record = raw;
|
|
4162
|
+
const status = record.status;
|
|
4163
|
+
if (status !== "pending" && status !== "approved" && status !== "expired") {
|
|
4164
|
+
throw new Error("pairing status returned an unknown state");
|
|
4165
|
+
}
|
|
4166
|
+
return {
|
|
4167
|
+
status,
|
|
4168
|
+
name: typeof record.name === "string" ? record.name : "",
|
|
4169
|
+
expires_at: typeof record.expires_at === "number" ? record.expires_at : 0
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4061
4172
|
var REQUEST_TIMEOUT_MS = 30000, UPLOAD_ATTEMPTS = 3, RETRY_BASE_MS = 500;
|
|
4062
4173
|
var init_api2 = __esm(() => {
|
|
4063
4174
|
init_dist4();
|
|
@@ -4506,6 +4617,89 @@ var init_store = __esm(() => {
|
|
|
4506
4617
|
init_plan();
|
|
4507
4618
|
});
|
|
4508
4619
|
|
|
4620
|
+
// src/workstation/headless.ts
|
|
4621
|
+
async function performHeadlessLogin(deps) {
|
|
4622
|
+
const now = deps.now ?? (() => Math.floor(Date.now() / 1000));
|
|
4623
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4624
|
+
const installationId = await ensureInstallationId(deps.paths, deps.store);
|
|
4625
|
+
const pkce = generatePkce();
|
|
4626
|
+
let start;
|
|
4627
|
+
try {
|
|
4628
|
+
start = await (deps.start ?? startPairing)(deps.origins, {
|
|
4629
|
+
installation_id: installationId,
|
|
4630
|
+
name: deps.name,
|
|
4631
|
+
code_challenge: pkce.challenge
|
|
4632
|
+
});
|
|
4633
|
+
} catch (err) {
|
|
4634
|
+
error(`Pairing failed: ${err.message}`);
|
|
4635
|
+
return null;
|
|
4636
|
+
}
|
|
4637
|
+
(deps.announce ?? defaultAnnounce)({
|
|
4638
|
+
code: formatPairingCode(start.pairing_code),
|
|
4639
|
+
url: `${deps.origins.dashboard}/workstations`,
|
|
4640
|
+
name: start.name,
|
|
4641
|
+
expiresAt: start.expires_at
|
|
4642
|
+
});
|
|
4643
|
+
const deadline = Math.min(start.expires_at, now() + WORKSTATION_PAIRING_TTL_SECONDS);
|
|
4644
|
+
for (;; ) {
|
|
4645
|
+
if (deps.signal?.aborted) {
|
|
4646
|
+
error("Pairing cancelled");
|
|
4647
|
+
return null;
|
|
4648
|
+
}
|
|
4649
|
+
if (now() >= deadline) {
|
|
4650
|
+
error("Pairing code expired before it was approved");
|
|
4651
|
+
return null;
|
|
4652
|
+
}
|
|
4653
|
+
await sleep(PAIRING_POLL_MS);
|
|
4654
|
+
let status;
|
|
4655
|
+
try {
|
|
4656
|
+
status = await (deps.status ?? pairingStatus)(deps.origins, start.pairing_code, {
|
|
4657
|
+
...deps.signal ? { signal: deps.signal } : {}
|
|
4658
|
+
});
|
|
4659
|
+
} catch (err) {
|
|
4660
|
+
error(`Pairing status failed: ${err.message}`);
|
|
4661
|
+
return null;
|
|
4662
|
+
}
|
|
4663
|
+
if (status.status === "expired") {
|
|
4664
|
+
error("Pairing code expired before it was approved");
|
|
4665
|
+
return null;
|
|
4666
|
+
}
|
|
4667
|
+
if (status.status === "approved") {
|
|
4668
|
+
break;
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
let credential;
|
|
4672
|
+
try {
|
|
4673
|
+
credential = await (deps.exchange ?? exchangeLogin)(deps.origins, {
|
|
4674
|
+
code: start.code,
|
|
4675
|
+
code_verifier: pkce.verifier,
|
|
4676
|
+
installation_id: installationId
|
|
4677
|
+
});
|
|
4678
|
+
} catch (err) {
|
|
4679
|
+
error(`Login exchange failed: ${err.message}`);
|
|
4680
|
+
return null;
|
|
4681
|
+
}
|
|
4682
|
+
await writeCredential(deps.paths, credential, deps.store);
|
|
4683
|
+
return { installationId, credential };
|
|
4684
|
+
}
|
|
4685
|
+
function defaultAnnounce(text) {
|
|
4686
|
+
info(`Pairing code: ${text.code}`);
|
|
4687
|
+
info(` name: ${text.name}`);
|
|
4688
|
+
info(` approve at: ${text.url} → "Pair a workstation"`);
|
|
4689
|
+
info(` expires: ${new Date(text.expiresAt * 1000).toLocaleTimeString()}`);
|
|
4690
|
+
info("");
|
|
4691
|
+
info("Waiting for approval...");
|
|
4692
|
+
success("(leave this terminal open)");
|
|
4693
|
+
}
|
|
4694
|
+
var PAIRING_POLL_MS = 5000;
|
|
4695
|
+
var init_headless = __esm(() => {
|
|
4696
|
+
init_dist4();
|
|
4697
|
+
init_output();
|
|
4698
|
+
init_api2();
|
|
4699
|
+
init_login2();
|
|
4700
|
+
init_store();
|
|
4701
|
+
});
|
|
4702
|
+
|
|
4509
4703
|
// src/workstation/login.ts
|
|
4510
4704
|
import { createHash, randomBytes as randomBytes2 } from "node:crypto";
|
|
4511
4705
|
import { hostname as osHostname } from "node:os";
|
|
@@ -4565,8 +4759,18 @@ async function performWorkstationLogin(deps) {
|
|
|
4565
4759
|
async function runWorkstationLogin(deps) {
|
|
4566
4760
|
info(`Dashboard: ${deps.origins.dashboard}`);
|
|
4567
4761
|
info(`Ingest: ${deps.origins.machine}`);
|
|
4568
|
-
|
|
4569
|
-
|
|
4762
|
+
if (deps.headless === undefined) {
|
|
4763
|
+
info("Opening browser for workstation authorization...");
|
|
4764
|
+
} else {
|
|
4765
|
+
info(`Headless pairing for "${deps.headless.name}" — no browser on this machine.`);
|
|
4766
|
+
}
|
|
4767
|
+
const outcome = deps.headless === undefined ? await performWorkstationLogin(deps) : await performHeadlessLogin({
|
|
4768
|
+
paths: deps.paths,
|
|
4769
|
+
origins: deps.origins,
|
|
4770
|
+
name: deps.headless.name,
|
|
4771
|
+
...deps.store ? { store: deps.store } : {},
|
|
4772
|
+
...deps.headless.announce ? { announce: deps.headless.announce } : {}
|
|
4773
|
+
});
|
|
4570
4774
|
if (!outcome) {
|
|
4571
4775
|
return { exitCode: 1 };
|
|
4572
4776
|
}
|
|
@@ -4593,6 +4797,7 @@ var init_login2 = __esm(() => {
|
|
|
4593
4797
|
init_dist3();
|
|
4594
4798
|
init_output();
|
|
4595
4799
|
init_api2();
|
|
4800
|
+
init_headless();
|
|
4596
4801
|
init_store();
|
|
4597
4802
|
});
|
|
4598
4803
|
|
|
@@ -4703,10 +4908,15 @@ async function runLoginCommand(deps = {}) {
|
|
|
4703
4908
|
const paths = getWorkstationPaths();
|
|
4704
4909
|
const origins = resolveOrigins(deps.loopbackUrl);
|
|
4705
4910
|
const store = storeDepsFor(process.platform);
|
|
4911
|
+
if (deps.headless === true && (deps.name ?? "").trim() === "") {
|
|
4912
|
+
error("Headless login needs --name so the workstation is identifiable before approval.");
|
|
4913
|
+
return 1;
|
|
4914
|
+
}
|
|
4706
4915
|
const { exitCode, report } = await runWorkstationLogin({
|
|
4707
4916
|
paths,
|
|
4708
4917
|
origins,
|
|
4709
|
-
...store ? { store } : {}
|
|
4918
|
+
...store ? { store } : {},
|
|
4919
|
+
...deps.headless === true ? { headless: { name: (deps.name ?? "").trim() } } : {}
|
|
4710
4920
|
});
|
|
4711
4921
|
if (exitCode !== 0 || !report) {
|
|
4712
4922
|
return exitCode;
|
|
@@ -4734,11 +4944,21 @@ var init_login3 = __esm(() => {
|
|
|
4734
4944
|
"loopback-url": {
|
|
4735
4945
|
type: "string",
|
|
4736
4946
|
description: "Loopback Worker origin for local acceptance (never production)"
|
|
4947
|
+
},
|
|
4948
|
+
headless: {
|
|
4949
|
+
type: "boolean",
|
|
4950
|
+
description: "Print a five minute pairing code instead of opening a browser"
|
|
4951
|
+
},
|
|
4952
|
+
name: {
|
|
4953
|
+
type: "string",
|
|
4954
|
+
description: "Display name claimed by a headless pairing"
|
|
4737
4955
|
}
|
|
4738
4956
|
},
|
|
4739
4957
|
async run({ args }) {
|
|
4740
4958
|
process.exitCode = await runLoginCommand({
|
|
4741
|
-
...args["loopback-url"] ? { loopbackUrl: args["loopback-url"] } : {}
|
|
4959
|
+
...args["loopback-url"] ? { loopbackUrl: args["loopback-url"] } : {},
|
|
4960
|
+
...args.headless ? { headless: true } : {},
|
|
4961
|
+
...typeof args.name === "string" ? { name: args.name } : {}
|
|
4742
4962
|
});
|
|
4743
4963
|
}
|
|
4744
4964
|
});
|