@isaacthoman/pulpo 0.51.2 → 0.52.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 (3) hide show
  1. package/README.md +6 -0
  2. package/dist/index.js +121 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,9 @@ Operator-first command-line client for a Pulpo instance.
6
6
  npm install --global @isaacthoman/pulpo
7
7
  pulpo context add production --url https://pulpo.example.com
8
8
  pulpo auth login --email admin@example.com
9
+ pulpo auth 2fa status
10
+ pulpo auth 2fa setup
11
+ pulpo auth 2fa confirm
9
12
  pulpo settings export --output pulpo-settings.json
10
13
  ```
11
14
 
@@ -32,6 +35,9 @@ lives in the platform config directory. Session tokens use Keychain on macOS or
32
35
  Secret Service on Linux when available, with a mode-`0600` file and warning as
33
36
  the fallback. Passwords are read from a hidden prompt, stdin, or
34
37
  `PULPO_PASSWORD`; no secret-bearing password/token option is accepted.
38
+ Authenticator and recovery codes use the hidden prompt or `PULPO_2FA_CODE`.
39
+ Enrollment secrets and recovery codes are printed only when created; store them
40
+ securely before continuing.
35
41
 
36
42
  Settings exports use `apiVersion: pulpo.dev/management/v1` and carry an opaque
37
43
  revision. `settings diff` exits with status 2 when changes exist. A stale
package/dist/index.js CHANGED
@@ -16304,7 +16304,8 @@ var userSchema = external_exports.object({
16304
16304
  });
16305
16305
  var loginInputSchema = external_exports.object({
16306
16306
  email: external_exports.email(),
16307
- password: external_exports.string().min(1).max(1024)
16307
+ password: external_exports.string().min(1).max(1024),
16308
+ twoFactorCode: external_exports.string().trim().min(6).max(32).optional()
16308
16309
  });
16309
16310
  var signupInputSchema = external_exports.object({
16310
16311
  name: external_exports.string().trim().min(1).max(120),
@@ -16344,9 +16345,34 @@ var mobileConfigSchema = external_exports.object({
16344
16345
  chatDuplication: external_exports.literal(true),
16345
16346
  publicSharing: external_exports.literal(true),
16346
16347
  attachments: external_exports.literal(true),
16347
- folders: external_exports.literal(true)
16348
+ folders: external_exports.literal(true),
16349
+ twoFactorAuth: external_exports.boolean().optional().default(false)
16348
16350
  })
16349
16351
  });
16352
+ var twoFactorStatusSchema = external_exports.object({
16353
+ enabled: external_exports.boolean(),
16354
+ recoveryCodesRemaining: external_exports.number().int().nonnegative()
16355
+ });
16356
+ var beginTwoFactorEnrollmentInputSchema = external_exports.object({
16357
+ currentPassword: external_exports.string().min(1).max(1024),
16358
+ verificationCode: external_exports.string().trim().min(6).max(32).optional()
16359
+ });
16360
+ var twoFactorEnrollmentSchema = external_exports.object({
16361
+ manualKey: external_exports.string().min(16),
16362
+ otpauthUri: external_exports.string().startsWith("otpauth://totp/"),
16363
+ qrCodeDataUrl: external_exports.string().startsWith("data:image/"),
16364
+ expiresAt: isoDateSchema
16365
+ });
16366
+ var confirmTwoFactorEnrollmentInputSchema = external_exports.object({
16367
+ code: external_exports.string().trim().regex(/^\d{6}$/, "Enter a six-digit authenticator code")
16368
+ });
16369
+ var verifyTwoFactorChangeInputSchema = external_exports.object({
16370
+ currentPassword: external_exports.string().min(1).max(1024),
16371
+ verificationCode: external_exports.string().trim().min(6).max(32)
16372
+ });
16373
+ var twoFactorRecoveryCodesSchema = external_exports.object({
16374
+ recoveryCodes: external_exports.array(external_exports.string()).length(10)
16375
+ });
16350
16376
  var updateProfileInputSchema = external_exports.object({
16351
16377
  name: external_exports.string().trim().min(1).max(120)
16352
16378
  });
@@ -17033,8 +17059,8 @@ var PulpoManagementClient = class {
17033
17059
  info() {
17034
17060
  return this.request("/api/management/v1/info");
17035
17061
  }
17036
- login(email3, password, deviceLabel = "Pulpo CLI") {
17037
- return this.request("/api/management/v1/auth/login", { method: "POST", body: { email: email3, password, deviceLabel } });
17062
+ login(email3, password, deviceLabel = "Pulpo CLI", twoFactorCode) {
17063
+ return this.request("/api/management/v1/auth/login", { method: "POST", body: { email: email3, password, deviceLabel, twoFactorCode } });
17038
17064
  }
17039
17065
  logout() {
17040
17066
  return this.request("/api/management/v1/auth/logout", { method: "POST" });
@@ -17042,6 +17068,21 @@ var PulpoManagementClient = class {
17042
17068
  me() {
17043
17069
  return this.request("/api/management/v1/auth/me");
17044
17070
  }
17071
+ twoFactorStatus() {
17072
+ return this.request("/api/me/two-factor");
17073
+ }
17074
+ beginTwoFactorEnrollment(input) {
17075
+ return this.request("/api/me/two-factor/enrollment", { method: "POST", body: input });
17076
+ }
17077
+ confirmTwoFactorEnrollment(code) {
17078
+ return this.request("/api/me/two-factor/enrollment/confirm", { method: "POST", body: { code } });
17079
+ }
17080
+ regenerateTwoFactorRecoveryCodes(input) {
17081
+ return this.request("/api/me/two-factor/recovery-codes", { method: "POST", body: input });
17082
+ }
17083
+ disableTwoFactor(input) {
17084
+ return this.request("/api/me/two-factor", { method: "DELETE", body: input });
17085
+ }
17045
17086
  tokens() {
17046
17087
  return this.request("/api/management/v1/tokens");
17047
17088
  }
@@ -17344,7 +17385,7 @@ async function confirmExact(io, expected, yes, json2) {
17344
17385
  }
17345
17386
 
17346
17387
  // src/index.ts
17347
- var CLI_VERSION = true ? "0.51.2" : "0.1.0";
17388
+ var CLI_VERSION = true ? "0.52.0" : "0.1.0";
17348
17389
  var CLI_BUNDLED = true;
17349
17390
  var commandIo = /* @__PURE__ */ new WeakMap();
17350
17391
  var commandClientFactory = /* @__PURE__ */ new WeakMap();
@@ -17464,7 +17505,31 @@ async function clientFor(command, authenticated = true) {
17464
17505
  if (capability && !info.capabilities.includes(capability)) {
17465
17506
  throw new Error(`The selected Pulpo instance does not advertise the ${capability} capability`);
17466
17507
  }
17467
- return { client, contextName: connection.contextName, context: connection.context };
17508
+ return { client, contextName: connection.contextName, context: connection.context, info };
17509
+ }
17510
+ async function twoFactorSecret(io, json2, prompt = "Authenticator or recovery code: ") {
17511
+ const value = process.env.PULPO_2FA_CODE;
17512
+ if (value !== void 0) return value;
17513
+ if (!io.stdin.isTTY) throw new Error("Noninteractive two-factor verification requires PULPO_2FA_CODE");
17514
+ if (json2) throw new Error("Machine-mode two-factor verification requires PULPO_2FA_CODE");
17515
+ return readSecret(io, prompt);
17516
+ }
17517
+ async function currentPassword(io, json2) {
17518
+ const value = process.env.PULPO_PASSWORD;
17519
+ if (value !== void 0) return value;
17520
+ if (!io.stdin.isTTY) throw new Error("Noninteractive account security changes require PULPO_PASSWORD");
17521
+ if (json2) throw new Error("Machine-mode account security changes require PULPO_PASSWORD");
17522
+ return readSecret(io, "Current password: ");
17523
+ }
17524
+ function requireTwoFactorCapability(info) {
17525
+ if (!info.capabilities.includes("twoFactor")) throw new Error("The selected Pulpo instance does not support two-factor authentication");
17526
+ }
17527
+ function emitRecoveryCodes(io, command, recoveryCodes) {
17528
+ io.stderr.write("Save these recovery codes now. They will not be shown again.\n");
17529
+ if (globalOptions(command).json) emit(io, command, { recoveryCodes });
17530
+ else {
17531
+ writeOutput(io, recoveryCodes.map((recoveryCode) => ({ recoveryCode })), false);
17532
+ }
17468
17533
  }
17469
17534
  function emit(io, command, value) {
17470
17535
  writeOutput(io, value, Boolean(globalOptions(command).json));
@@ -17563,7 +17628,14 @@ function createProgram(io = processIo, dependencies = {}) {
17563
17628
  throw new Error("Machine-mode login requires the password on stdin or in PULPO_PASSWORD");
17564
17629
  }
17565
17630
  const password = process.env.PULPO_PASSWORD ?? await readSecret(io, "Password: ");
17566
- const result = await client.login(email3, password);
17631
+ let result;
17632
+ try {
17633
+ result = await client.login(email3, password);
17634
+ } catch (error51) {
17635
+ if (!(error51 instanceof ManagementApiError) || error51.code !== "two_factor_required") throw error51;
17636
+ const code = await twoFactorSecret(io, Boolean(globalOptions(command).json));
17637
+ result = await client.login(email3, password, "Pulpo CLI", code);
17638
+ }
17567
17639
  await setCredential(contextName, result.session.token, (message) => io.stderr.write(`${message}
17568
17640
  `));
17569
17641
  const config2 = await loadConfig();
@@ -17581,6 +17653,48 @@ function createProgram(io = processIo, dependencies = {}) {
17581
17653
  const { client } = await clientFor(command);
17582
17654
  emit(io, command, await client.me());
17583
17655
  });
17656
+ const twoFactor = auth.command("2fa").description("Manage authenticator-app two-factor authentication");
17657
+ twoFactor.command("status").action(async (_options, command) => {
17658
+ const { client, info } = await clientFor(command);
17659
+ requireTwoFactorCapability(info);
17660
+ emit(io, command, await client.twoFactorStatus());
17661
+ });
17662
+ twoFactor.command("setup").description("Start or replace authenticator enrollment").action(async (_options, command) => {
17663
+ const { client, info } = await clientFor(command);
17664
+ requireTwoFactorCapability(info);
17665
+ const status = await client.twoFactorStatus();
17666
+ const password = await currentPassword(io, Boolean(globalOptions(command).json));
17667
+ const verificationCode = status.enabled ? await twoFactorSecret(io, Boolean(globalOptions(command).json), "Current authenticator or recovery code: ") : void 0;
17668
+ const enrollment = await client.beginTwoFactorEnrollment({ currentPassword: password, verificationCode });
17669
+ io.stderr.write("Save this enrollment secret now. It will not be shown again.\n");
17670
+ if (globalOptions(command).json) emit(io, command, enrollment);
17671
+ else {
17672
+ io.stderr.write("Add this account to your authenticator, then run `pulpo auth 2fa confirm`.\n");
17673
+ writeOutput(io, [{ manualKey: enrollment.manualKey, otpauthUri: enrollment.otpauthUri, expiresAt: enrollment.expiresAt }], false);
17674
+ }
17675
+ });
17676
+ twoFactor.command("confirm").description("Confirm a pending enrollment").action(async (_options, command) => {
17677
+ const { client, info } = await clientFor(command);
17678
+ requireTwoFactorCapability(info);
17679
+ const code = await twoFactorSecret(io, Boolean(globalOptions(command).json), "New six-digit authenticator code: ");
17680
+ emitRecoveryCodes(io, command, (await client.confirmTwoFactorEnrollment(code)).recoveryCodes);
17681
+ });
17682
+ twoFactor.command("regenerate-recovery-codes").description("Replace all recovery codes").action(async (_options, command) => {
17683
+ const { client, info } = await clientFor(command);
17684
+ requireTwoFactorCapability(info);
17685
+ const password = await currentPassword(io, Boolean(globalOptions(command).json));
17686
+ const verificationCode = await twoFactorSecret(io, Boolean(globalOptions(command).json));
17687
+ emitRecoveryCodes(io, command, (await client.regenerateTwoFactorRecoveryCodes({ currentPassword: password, verificationCode })).recoveryCodes);
17688
+ });
17689
+ twoFactor.command("disable").description("Disable two-factor authentication").action(async (_options, command) => {
17690
+ await confirmExact(io, "DISABLE", Boolean(globalOptions(command).yes), Boolean(globalOptions(command).json));
17691
+ const { client, info } = await clientFor(command);
17692
+ requireTwoFactorCapability(info);
17693
+ const password = await currentPassword(io, Boolean(globalOptions(command).json));
17694
+ const verificationCode = await twoFactorSecret(io, Boolean(globalOptions(command).json));
17695
+ await client.disableTwoFactor({ currentPassword: password, verificationCode });
17696
+ emit(io, command, { enabled: false });
17697
+ });
17584
17698
  const token = program.command("token").description("Manage scoped automation tokens");
17585
17699
  token.command("list").action(async (_options, command) => {
17586
17700
  const { client } = await clientFor(command);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isaacthoman/pulpo",
3
- "version": "0.51.2",
3
+ "version": "0.52.0",
4
4
  "description": "Operator-first command-line client for Pulpo",
5
5
  "type": "module",
6
6
  "bin": {