@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.
Files changed (2) hide show
  1. package/dist/bat.js +226 -6
  2. 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.1.2";
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
- 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_LIMITS, WORKSTATION_CONNECTIVITY_STATUSES, UUID_PATTERN, IPV4_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;
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
- info("Opening browser for workstation authorization...");
4569
- const outcome = await performWorkstationLogin(deps);
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocoo/bat",
3
- "version": "3.1.2",
3
+ "version": "3.2.0",
4
4
  "description": "bat CLI — manage agents, assets and bindings for bat.hexly.ai",
5
5
  "type": "module",
6
6
  "bin": {