@sigma-auth/cli 0.0.1 → 0.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigma-auth/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Headless Sigma Auth CLI: create a BAP identity locally, sign in with Bitcoin-Auth, push encrypted backups, register OAuth clients",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.ts CHANGED
@@ -5,11 +5,12 @@ import { isLegacyBackup, isType42Backup } from "bitcoin-backup";
5
5
  import type { ParsedArgs } from "./args.ts";
6
6
  import { boolFlag, flag, flagList } from "./args.ts";
7
7
  import { backupPath, type RuntimeConfig } from "./config.ts";
8
- import { loadJar, sessionCookieNames } from "./cookies.ts";
9
- import { CliError, cryptoFail, usage } from "./error.ts";
8
+ import { deleteJar, loadJar, sessionCookieNames } from "./cookies.ts";
9
+ import { cryptoFail, usage } from "./error.ts";
10
10
  import { ensureDir, pathExists, readText, writeSecretFile } from "./fsutil.ts";
11
11
  import { createHttp, requestJson, throwHttp } from "./http.ts";
12
12
  import {
13
+ assertBitcoinBackupCiphertext,
13
14
  bapFromBackup,
14
15
  createMasterBackup,
15
16
  decryptMaster,
@@ -76,7 +77,7 @@ export async function identityCreate(
76
77
  positional: args.positional,
77
78
  flags: { ...args.flags, backup: [out] },
78
79
  };
79
- const code = await authSignIn(signinArgs, cfg, false);
80
+ const code = await authSignIn(signinArgs, cfg, false, password);
80
81
  if (code !== 0) {
81
82
  return code;
82
83
  }
@@ -157,9 +158,10 @@ export async function backupEncrypt(
157
158
  export async function authSignIn(
158
159
  args: ParsedArgs,
159
160
  cfg: RuntimeConfig,
160
- emit = true
161
+ emit = true,
162
+ resolvedPassword?: string
161
163
  ): Promise<number> {
162
- const password = await resolvePassword(args, true);
164
+ const password = resolvedPassword ?? (await resolvePassword(args, true));
163
165
  if (!password) {
164
166
  usage("password required");
165
167
  }
@@ -183,7 +185,13 @@ export async function authSignIn(
183
185
  saveCookies: true,
184
186
  });
185
187
  if (signed.status >= 400) {
186
- throwHttp("/api/auth/sign-in/sigma", signed.status, signed.json, signed.text);
188
+ throwHttp(
189
+ "/api/auth/sign-in/sigma",
190
+ signed.status,
191
+ signed.json,
192
+ signed.text,
193
+ signed.headers
194
+ );
187
195
  }
188
196
  const payload = signed.json as {
189
197
  user?: { id?: string; pubkey?: string };
@@ -204,7 +212,14 @@ export async function authSignIn(
204
212
  await requestJson(client, "POST", "/api/auth/sign-out", {
205
213
  withCookies: true,
206
214
  });
207
- throwHttp("/api/user/bap-ids", registered.status, registered.json, registered.text);
215
+ deleteJar(client.cookieJar);
216
+ throwHttp(
217
+ "/api/user/bap-ids",
218
+ registered.status,
219
+ registered.json,
220
+ registered.text,
221
+ registered.headers
222
+ );
208
223
  }
209
224
  if (!emit) {
210
225
  return 0;
@@ -228,20 +243,14 @@ export async function backupPush(
228
243
  ): Promise<number> {
229
244
  const path = backupPath(args, cfg.home);
230
245
  const ciphertext = readText(path).replace(/\n+$/, "");
231
- if (looksLikePlaintextBackup(ciphertext)) {
232
- throw new CliError(
233
- 7,
234
- "crypto",
235
- "refusing to upload plaintext backup (rootPk/xprv/wif/mnemonic present)"
236
- );
237
- }
246
+ assertBitcoinBackupCiphertext(ciphertext);
238
247
  const client = createHttp(cfg);
239
248
  const result = await requestJson(client, "POST", "/api/backup", {
240
249
  body: { encryptedBackup: ciphertext },
241
250
  withCookies: true,
242
251
  });
243
252
  if (result.status >= 400) {
244
- throwHttp("/api/backup", result.status, result.json, result.text);
253
+ throwHttp("/api/backup", result.status, result.json, result.text, result.headers);
245
254
  }
246
255
  const payload = result.json as { bapId?: string; message?: string };
247
256
  if (!emit) {
@@ -285,7 +294,13 @@ export async function oauthRegister(
285
294
  withCookies: true,
286
295
  });
287
296
  if (result.status >= 400) {
288
- throwHttp("/api/oauth-clients", result.status, result.json, result.text);
297
+ throwHttp(
298
+ "/api/oauth-clients",
299
+ result.status,
300
+ result.json,
301
+ result.text,
302
+ result.headers
303
+ );
289
304
  }
290
305
  const payload = result.json as {
291
306
  client?: { clientId?: string; accountPubkey?: string; ownerBapId?: string };
@@ -327,7 +342,8 @@ export async function oauthRegister(
327
342
  "/api/auth/oauth2/register",
328
343
  result.status,
329
344
  result.json,
330
- result.text
345
+ result.text,
346
+ result.headers
331
347
  );
332
348
  }
333
349
  const payload = result.json as {
package/src/cookies.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
1
+ import { chmodSync, existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { ensureDir } from "./fsutil.ts";
4
4
 
@@ -83,6 +83,12 @@ export function loadJar(path: string): Cookie[] {
83
83
  return cookies;
84
84
  }
85
85
 
86
+ export function deleteJar(path: string): void {
87
+ if (existsSync(path)) {
88
+ unlinkSync(path);
89
+ }
90
+ }
91
+
86
92
  export function saveJar(path: string, cookies: Cookie[]): void {
87
93
  ensureDir(dirname(path));
88
94
  const lines = [
package/src/http.ts CHANGED
@@ -78,13 +78,23 @@ export async function requestJson(
78
78
  return { status: response.status, headers: response.headers, json, text };
79
79
  }
80
80
 
81
- export function throwHttp(path: string, status: number, json: unknown, text: string): never {
81
+ export function throwHttp(
82
+ path: string,
83
+ status: number,
84
+ json: unknown,
85
+ text: string,
86
+ headers?: Headers
87
+ ): never {
82
88
  const record = json && typeof json === "object" ? (json as Record<string, unknown>) : {};
83
- const message =
89
+ let message =
84
90
  (typeof record.error_description === "string" && record.error_description) ||
85
91
  (typeof record.message === "string" && record.message) ||
86
92
  (typeof record.error === "string" && record.error) ||
87
93
  text ||
88
94
  `${path} failed with ${status}`;
95
+ const retryAfter = headers?.get("retry-after");
96
+ if (retryAfter) {
97
+ message = `${message} (Retry-After: ${retryAfter})`;
98
+ }
89
99
  throw new CliError(exitForHttp(status), codeForHttp(status), message, status);
90
100
  }
package/src/identity.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HD, Mnemonic, PrivateKey } from "@bsv/sdk";
1
+ import { HD, Mnemonic, PrivateKey, Utils } from "@bsv/sdk";
2
2
  import {
3
3
  type BapMasterBackup,
4
4
  decryptBackup,
@@ -9,6 +9,12 @@ import {
9
9
  import { BAP } from "bsv-bap";
10
10
  import { cryptoFail } from "./error.ts";
11
11
 
12
+ /** bitcoin-backup encryptData: salt(16) || iv(12) || AES-GCM ciphertext (16-byte tag). */
13
+ const SALT_LENGTH_BYTES = 16;
14
+ const IV_LENGTH_BYTES = 12;
15
+ const AES_GCM_TAG_BYTES = 16;
16
+ const MIN_ENVELOPE_BYTES = SALT_LENGTH_BYTES + IV_LENGTH_BYTES + AES_GCM_TAG_BYTES;
17
+
12
18
  export type PublicIdentity = {
13
19
  bapId: string;
14
20
  pubkey: string;
@@ -153,3 +159,89 @@ export function looksLikePlaintextBackup(text: string): boolean {
153
159
  return false;
154
160
  }
155
161
  }
162
+
163
+ function looksLikeWif(text: string): boolean {
164
+ return /^[5KL][1-9A-HJ-NP-Za-km-z]{50,51}$/.test(text);
165
+ }
166
+
167
+ function looksLikeExtendedKey(text: string): boolean {
168
+ return /^(xprv|tprv|yprv|zprv|Yprv|Zprv)/.test(text);
169
+ }
170
+
171
+ function looksLikeMnemonic(text: string): boolean {
172
+ const words: string[] = [];
173
+ let current = "";
174
+ for (const ch of text) {
175
+ if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
176
+ if (current.length > 0) {
177
+ words.push(current);
178
+ current = "";
179
+ }
180
+ } else {
181
+ current += ch;
182
+ }
183
+ }
184
+ if (current.length > 0) {
185
+ words.push(current);
186
+ }
187
+ const wordCount = words.length;
188
+ if (
189
+ wordCount !== 12 &&
190
+ wordCount !== 15 &&
191
+ wordCount !== 18 &&
192
+ wordCount !== 21 &&
193
+ wordCount !== 24
194
+ ) {
195
+ return false;
196
+ }
197
+ return words.every((word) => /^[a-z]+$/i.test(word));
198
+ }
199
+
200
+ function looksLikeJsonObject(text: string): boolean {
201
+ try {
202
+ const parsed = JSON.parse(text) as unknown;
203
+ return parsed !== null && typeof parsed === "object";
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ export function isBitcoinBackupCiphertext(text: string): boolean {
210
+ if (text.length === 0) {
211
+ return false;
212
+ }
213
+ if (
214
+ looksLikePlaintextBackup(text) ||
215
+ looksLikeJsonObject(text) ||
216
+ looksLikeWif(text) ||
217
+ looksLikeExtendedKey(text) ||
218
+ looksLikeMnemonic(text)
219
+ ) {
220
+ return false;
221
+ }
222
+ try {
223
+ const bytes = Utils.toArray(text, "base64");
224
+ if (bytes.length < MIN_ENVELOPE_BYTES) {
225
+ return false;
226
+ }
227
+ return Utils.toBase64(bytes) === text;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ export function assertBitcoinBackupCiphertext(text: string): void {
234
+ if (
235
+ looksLikePlaintextBackup(text) ||
236
+ looksLikeWif(text) ||
237
+ looksLikeExtendedKey(text) ||
238
+ looksLikeMnemonic(text)
239
+ ) {
240
+ cryptoFail(
241
+ "refusing to upload plaintext backup (rootPk/xprv/wif/mnemonic present)"
242
+ );
243
+ }
244
+ if (!isBitcoinBackupCiphertext(text)) {
245
+ cryptoFail("refusing to upload: not opaque bitcoin-backup ciphertext");
246
+ }
247
+ }
package/src/output.ts CHANGED
@@ -10,7 +10,14 @@ export function printJson(ok: boolean, data: unknown, error?: unknown): void {
10
10
  process.stdout.write(`${JSON.stringify({ ok: true, data })}\n`);
11
11
  return;
12
12
  }
13
- process.stdout.write(`${JSON.stringify({ ok: false, error })}\n`);
13
+ const body: Record<string, unknown> = { ok: false };
14
+ if (error !== undefined) {
15
+ body.error = error;
16
+ }
17
+ if (data !== undefined) {
18
+ body.data = data;
19
+ }
20
+ process.stdout.write(`${JSON.stringify(body)}\n`);
14
21
  }
15
22
 
16
23
  export function printHuman(mode: OutputMode, text: string): void {
package/src/password.ts CHANGED
@@ -4,6 +4,15 @@ import { boolFlag, flag } from "./args.ts";
4
4
  import { MIN_PASSWORD_LENGTH } from "./config.ts";
5
5
  import { cryptoFail, usage } from "./error.ts";
6
6
 
7
+ function isAllWhitespace(value: string): boolean {
8
+ for (const ch of value) {
9
+ if (!/\s/.test(ch)) {
10
+ return false;
11
+ }
12
+ }
13
+ return true;
14
+ }
15
+
7
16
  function envPassword(): string | undefined {
8
17
  if (!("SIGMA_BACKUP_PASSWORD" in process.env)) {
9
18
  return undefined;
@@ -12,6 +21,9 @@ function envPassword(): string | undefined {
12
21
  if (value === undefined || value === "") {
13
22
  usage("SIGMA_BACKUP_PASSWORD is set but empty");
14
23
  }
24
+ if (isAllWhitespace(value)) {
25
+ usage("SIGMA_BACKUP_PASSWORD is whitespace-only");
26
+ }
15
27
  return value;
16
28
  }
17
29