@sigma-auth/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +39 -0
- package/package.json +47 -0
- package/src/args.ts +81 -0
- package/src/commands.ts +593 -0
- package/src/config.ts +78 -0
- package/src/cookies.ts +170 -0
- package/src/error.ts +72 -0
- package/src/fsutil.ts +48 -0
- package/src/http.ts +90 -0
- package/src/identity.ts +155 -0
- package/src/index.ts +62 -0
- package/src/output.ts +50 -0
- package/src/password.ts +54 -0
package/src/commands.ts
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { getAuthToken } from "bitcoin-auth";
|
|
3
|
+
import type { BapMasterBackup } from "bitcoin-backup";
|
|
4
|
+
import { isLegacyBackup, isType42Backup } from "bitcoin-backup";
|
|
5
|
+
import type { ParsedArgs } from "./args.ts";
|
|
6
|
+
import { boolFlag, flag, flagList } from "./args.ts";
|
|
7
|
+
import { backupPath, type RuntimeConfig } from "./config.ts";
|
|
8
|
+
import { loadJar, sessionCookieNames } from "./cookies.ts";
|
|
9
|
+
import { CliError, cryptoFail, usage } from "./error.ts";
|
|
10
|
+
import { ensureDir, pathExists, readText, writeSecretFile } from "./fsutil.ts";
|
|
11
|
+
import { createHttp, requestJson, throwHttp } from "./http.ts";
|
|
12
|
+
import {
|
|
13
|
+
bapFromBackup,
|
|
14
|
+
createMasterBackup,
|
|
15
|
+
decryptMaster,
|
|
16
|
+
encryptMaster,
|
|
17
|
+
looksLikePlaintextBackup,
|
|
18
|
+
memberWif,
|
|
19
|
+
publicFields,
|
|
20
|
+
rootPubkey,
|
|
21
|
+
} from "./identity.ts";
|
|
22
|
+
import { printHuman, printJson, printWarn, type OutputMode } from "./output.ts";
|
|
23
|
+
import { resolvePassword } from "./password.ts";
|
|
24
|
+
|
|
25
|
+
function mode(cfg: RuntimeConfig): OutputMode {
|
|
26
|
+
return { json: cfg.json, quiet: cfg.quiet };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function succeed(cfg: RuntimeConfig, data: Record<string, unknown>, human: string): number {
|
|
30
|
+
if (cfg.json) {
|
|
31
|
+
printJson(true, data);
|
|
32
|
+
} else {
|
|
33
|
+
printHuman(mode(cfg), human);
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function loadBackup(
|
|
39
|
+
args: ParsedArgs,
|
|
40
|
+
cfg: RuntimeConfig,
|
|
41
|
+
password: string
|
|
42
|
+
): Promise<{ path: string; backup: BapMasterBackup; ciphertext: string }> {
|
|
43
|
+
const path = backupPath(args, cfg.home);
|
|
44
|
+
const ciphertext = readText(path);
|
|
45
|
+
const backup = await decryptMaster(ciphertext, password);
|
|
46
|
+
return { path, backup, ciphertext };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function identityCreate(
|
|
50
|
+
args: ParsedArgs,
|
|
51
|
+
cfg: RuntimeConfig
|
|
52
|
+
): Promise<number> {
|
|
53
|
+
const label = flag(args, "label");
|
|
54
|
+
if (!label) {
|
|
55
|
+
usage("--label is required");
|
|
56
|
+
}
|
|
57
|
+
const password = await resolvePassword(args, true);
|
|
58
|
+
if (!password) {
|
|
59
|
+
usage("password required");
|
|
60
|
+
}
|
|
61
|
+
const created = createMasterBackup(label);
|
|
62
|
+
const encrypted = await encryptMaster(created.backup, password);
|
|
63
|
+
const out = flag(args, "out") ?? `${cfg.home}/identity.bep`;
|
|
64
|
+
writeSecretFile(out, encrypted, cfg.force);
|
|
65
|
+
if (boolFlag(args, "show-mnemonic")) {
|
|
66
|
+
process.stderr.write(`${created.mnemonic}\n`);
|
|
67
|
+
}
|
|
68
|
+
const mnemonicFile = flag(args, "mnemonic-file");
|
|
69
|
+
if (mnemonicFile) {
|
|
70
|
+
writeSecretFile(mnemonicFile, `${created.mnemonic}\n`, cfg.force);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let userId: string | undefined;
|
|
74
|
+
if (boolFlag(args, "signin") || boolFlag(args, "push-backup")) {
|
|
75
|
+
const signinArgs: ParsedArgs = {
|
|
76
|
+
positional: args.positional,
|
|
77
|
+
flags: { ...args.flags, backup: [out] },
|
|
78
|
+
};
|
|
79
|
+
const code = await authSignIn(signinArgs, cfg, false);
|
|
80
|
+
if (code !== 0) {
|
|
81
|
+
return code;
|
|
82
|
+
}
|
|
83
|
+
if (boolFlag(args, "push-backup")) {
|
|
84
|
+
const pushCode = await backupPush(signinArgs, cfg, false);
|
|
85
|
+
if (pushCode !== 0) {
|
|
86
|
+
return pushCode;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return succeed(
|
|
92
|
+
cfg,
|
|
93
|
+
{
|
|
94
|
+
bapId: created.bapId,
|
|
95
|
+
pubkey: created.pubkey,
|
|
96
|
+
address: created.address,
|
|
97
|
+
backupPath: out,
|
|
98
|
+
userId,
|
|
99
|
+
},
|
|
100
|
+
`created ${created.bapId}\n${out}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function identityInfo(
|
|
105
|
+
args: ParsedArgs,
|
|
106
|
+
cfg: RuntimeConfig
|
|
107
|
+
): Promise<number> {
|
|
108
|
+
const password = await resolvePassword(args, true);
|
|
109
|
+
if (!password) {
|
|
110
|
+
usage("password required");
|
|
111
|
+
}
|
|
112
|
+
const { backup } = await loadBackup(args, cfg, password);
|
|
113
|
+
const fields = publicFields(backup, flag(args, "bap-id"));
|
|
114
|
+
return succeed(
|
|
115
|
+
cfg,
|
|
116
|
+
fields,
|
|
117
|
+
`${fields.bapId}\n${fields.pubkey}\n${fields.address}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function backupEncrypt(
|
|
122
|
+
args: ParsedArgs,
|
|
123
|
+
cfg: RuntimeConfig
|
|
124
|
+
): Promise<number> {
|
|
125
|
+
const input = flag(args, "in");
|
|
126
|
+
const out = flag(args, "out");
|
|
127
|
+
if (!input || !out) {
|
|
128
|
+
usage("--in and --out are required");
|
|
129
|
+
}
|
|
130
|
+
const password = await resolvePassword(args, true);
|
|
131
|
+
if (!password) {
|
|
132
|
+
usage("password required");
|
|
133
|
+
}
|
|
134
|
+
const raw = readText(input);
|
|
135
|
+
if (!looksLikePlaintextBackup(raw)) {
|
|
136
|
+
cryptoFail("--in does not look like a decrypted master backup JSON");
|
|
137
|
+
}
|
|
138
|
+
const parsed = JSON.parse(raw) as BapMasterBackup;
|
|
139
|
+
if (!(isType42Backup(parsed) || isLegacyBackup(parsed))) {
|
|
140
|
+
cryptoFail("--in is not a Type42 or legacy master backup");
|
|
141
|
+
}
|
|
142
|
+
let warning: string | undefined;
|
|
143
|
+
const bap = bapFromBackup(parsed);
|
|
144
|
+
if (bap.listIds().length === 0) {
|
|
145
|
+
warning = "ids is empty; auth sign-in will reject this backup";
|
|
146
|
+
printWarn(mode(cfg), warning);
|
|
147
|
+
}
|
|
148
|
+
const encrypted = await encryptMaster(parsed, password);
|
|
149
|
+
writeSecretFile(out, encrypted, cfg.force);
|
|
150
|
+
return succeed(
|
|
151
|
+
cfg,
|
|
152
|
+
{ backupPath: out, warning },
|
|
153
|
+
out
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function authSignIn(
|
|
158
|
+
args: ParsedArgs,
|
|
159
|
+
cfg: RuntimeConfig,
|
|
160
|
+
emit = true
|
|
161
|
+
): Promise<number> {
|
|
162
|
+
const password = await resolvePassword(args, true);
|
|
163
|
+
if (!password) {
|
|
164
|
+
usage("password required");
|
|
165
|
+
}
|
|
166
|
+
const { backup } = await loadBackup(args, cfg, password);
|
|
167
|
+
const ids = bapFromBackup(backup).listIds();
|
|
168
|
+
if (ids.length === 0) {
|
|
169
|
+
cryptoFail("backup has no identities; run identity create");
|
|
170
|
+
}
|
|
171
|
+
const member = memberWif(backup, flag(args, "bap-id"));
|
|
172
|
+
const body = { bapId: member.bapId };
|
|
173
|
+
const token = getAuthToken({
|
|
174
|
+
privateKeyWif: member.wif,
|
|
175
|
+
requestPath: "/api/auth/sign-in/sigma",
|
|
176
|
+
scheme: "brc77",
|
|
177
|
+
});
|
|
178
|
+
const client = createHttp(cfg);
|
|
179
|
+
ensureDir(cfg.home);
|
|
180
|
+
const signed = await requestJson(client, "POST", "/api/auth/sign-in/sigma", {
|
|
181
|
+
body,
|
|
182
|
+
headers: { "x-auth-token": token },
|
|
183
|
+
saveCookies: true,
|
|
184
|
+
});
|
|
185
|
+
if (signed.status >= 400) {
|
|
186
|
+
throwHttp("/api/auth/sign-in/sigma", signed.status, signed.json, signed.text);
|
|
187
|
+
}
|
|
188
|
+
const payload = signed.json as {
|
|
189
|
+
user?: { id?: string; pubkey?: string };
|
|
190
|
+
};
|
|
191
|
+
const name =
|
|
192
|
+
("label" in backup && backup.label) || "Identity 1";
|
|
193
|
+
const registered = await requestJson(client, "POST", "/api/user/bap-ids", {
|
|
194
|
+
body: {
|
|
195
|
+
bapId: member.bapId,
|
|
196
|
+
name,
|
|
197
|
+
isPrimary: true,
|
|
198
|
+
accountPubkey: member.pubkey,
|
|
199
|
+
counter: 0,
|
|
200
|
+
},
|
|
201
|
+
withCookies: true,
|
|
202
|
+
});
|
|
203
|
+
if (registered.status >= 400) {
|
|
204
|
+
await requestJson(client, "POST", "/api/auth/sign-out", {
|
|
205
|
+
withCookies: true,
|
|
206
|
+
});
|
|
207
|
+
throwHttp("/api/user/bap-ids", registered.status, registered.json, registered.text);
|
|
208
|
+
}
|
|
209
|
+
if (!emit) {
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
return succeed(
|
|
213
|
+
cfg,
|
|
214
|
+
{
|
|
215
|
+
userId: payload.user?.id,
|
|
216
|
+
pubkey: member.pubkey,
|
|
217
|
+
bapId: member.bapId,
|
|
218
|
+
cookieJar: cfg.cookieJar,
|
|
219
|
+
},
|
|
220
|
+
`signed in as ${member.bapId}`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function backupPush(
|
|
225
|
+
args: ParsedArgs,
|
|
226
|
+
cfg: RuntimeConfig,
|
|
227
|
+
emit = true
|
|
228
|
+
): Promise<number> {
|
|
229
|
+
const path = backupPath(args, cfg.home);
|
|
230
|
+
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
|
+
}
|
|
238
|
+
const client = createHttp(cfg);
|
|
239
|
+
const result = await requestJson(client, "POST", "/api/backup", {
|
|
240
|
+
body: { encryptedBackup: ciphertext },
|
|
241
|
+
withCookies: true,
|
|
242
|
+
});
|
|
243
|
+
if (result.status >= 400) {
|
|
244
|
+
throwHttp("/api/backup", result.status, result.json, result.text);
|
|
245
|
+
}
|
|
246
|
+
const payload = result.json as { bapId?: string; message?: string };
|
|
247
|
+
if (!emit) {
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
return succeed(
|
|
251
|
+
cfg,
|
|
252
|
+
{ bapId: payload.bapId, message: payload.message },
|
|
253
|
+
payload.message ?? "backup stored"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function oauthRegister(
|
|
258
|
+
args: ParsedArgs,
|
|
259
|
+
cfg: RuntimeConfig
|
|
260
|
+
): Promise<number> {
|
|
261
|
+
const name = flag(args, "name");
|
|
262
|
+
const redirectUris = flagList(args, "redirect-uri");
|
|
263
|
+
if (!name) {
|
|
264
|
+
usage("--name is required");
|
|
265
|
+
}
|
|
266
|
+
if (redirectUris.length === 0) {
|
|
267
|
+
usage("at least one --redirect-uri is required");
|
|
268
|
+
}
|
|
269
|
+
const signingPubkey = flag(args, "signing-pubkey");
|
|
270
|
+
const client = createHttp(cfg);
|
|
271
|
+
if (signingPubkey) {
|
|
272
|
+
const ownerBapId = flag(args, "owner-bap-id");
|
|
273
|
+
const clientId = flag(args, "client-id");
|
|
274
|
+
if (!ownerBapId || !clientId) {
|
|
275
|
+
usage("--owner-bap-id and --client-id are required with --signing-pubkey");
|
|
276
|
+
}
|
|
277
|
+
const result = await requestJson(client, "POST", "/api/oauth-clients", {
|
|
278
|
+
body: {
|
|
279
|
+
clientId,
|
|
280
|
+
ownerBapId,
|
|
281
|
+
name,
|
|
282
|
+
redirectUris,
|
|
283
|
+
accountPubkey: signingPubkey,
|
|
284
|
+
},
|
|
285
|
+
withCookies: true,
|
|
286
|
+
});
|
|
287
|
+
if (result.status >= 400) {
|
|
288
|
+
throwHttp("/api/oauth-clients", result.status, result.json, result.text);
|
|
289
|
+
}
|
|
290
|
+
const payload = result.json as {
|
|
291
|
+
client?: { clientId?: string; accountPubkey?: string; ownerBapId?: string };
|
|
292
|
+
};
|
|
293
|
+
return succeed(
|
|
294
|
+
cfg,
|
|
295
|
+
{
|
|
296
|
+
clientId: payload.client?.clientId ?? clientId,
|
|
297
|
+
accountPubkey: signingPubkey,
|
|
298
|
+
ownerBapId,
|
|
299
|
+
redirectUris,
|
|
300
|
+
public: true,
|
|
301
|
+
path: "session",
|
|
302
|
+
},
|
|
303
|
+
payload.client?.clientId ?? clientId
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const grantTypes = flagList(args, "grant-type");
|
|
308
|
+
const result = await requestJson(
|
|
309
|
+
client,
|
|
310
|
+
"POST",
|
|
311
|
+
"/api/auth/oauth2/register",
|
|
312
|
+
{
|
|
313
|
+
body: {
|
|
314
|
+
client_name: name,
|
|
315
|
+
redirect_uris: redirectUris,
|
|
316
|
+
grant_types:
|
|
317
|
+
grantTypes.length > 0
|
|
318
|
+
? grantTypes
|
|
319
|
+
: ["authorization_code", "refresh_token"],
|
|
320
|
+
response_types: ["code"],
|
|
321
|
+
token_endpoint_auth_method: "none",
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
if (result.status >= 400) {
|
|
326
|
+
throwHttp(
|
|
327
|
+
"/api/auth/oauth2/register",
|
|
328
|
+
result.status,
|
|
329
|
+
result.json,
|
|
330
|
+
result.text
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const payload = result.json as {
|
|
334
|
+
client_id?: string;
|
|
335
|
+
client_secret?: string;
|
|
336
|
+
};
|
|
337
|
+
printWarn(
|
|
338
|
+
mode(cfg),
|
|
339
|
+
"DCR client has no memberPubkey; POST /api/auth/oauth2/token will reject this client. Register with --signing-pubkey after auth sign-in to store the member key."
|
|
340
|
+
);
|
|
341
|
+
return succeed(
|
|
342
|
+
cfg,
|
|
343
|
+
{
|
|
344
|
+
clientId: payload.client_id,
|
|
345
|
+
public: true,
|
|
346
|
+
path: "dcr",
|
|
347
|
+
client_secret: payload.client_secret,
|
|
348
|
+
},
|
|
349
|
+
payload.client_id ?? "registered"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
type Check = {
|
|
354
|
+
id: string;
|
|
355
|
+
ok: boolean;
|
|
356
|
+
required: boolean;
|
|
357
|
+
detail: string;
|
|
358
|
+
skipped?: boolean;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export async function doctor(args: ParsedArgs, cfg: RuntimeConfig): Promise<number> {
|
|
362
|
+
const checks: Check[] = [];
|
|
363
|
+
const add = (check: Check) => {
|
|
364
|
+
checks.push(check);
|
|
365
|
+
if (cfg.json) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const mark = check.skipped ? "skip" : check.ok ? "ok" : "FAIL";
|
|
369
|
+
printHuman(mode(cfg), `${mark} ${check.id} ${check.detail}`);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
add({
|
|
373
|
+
id: "env.base_url",
|
|
374
|
+
ok: Boolean(cfg.baseUrl),
|
|
375
|
+
required: true,
|
|
376
|
+
detail: cfg.baseUrl,
|
|
377
|
+
});
|
|
378
|
+
try {
|
|
379
|
+
ensureDir(cfg.home);
|
|
380
|
+
add({ id: "env.home", ok: true, required: true, detail: cfg.home });
|
|
381
|
+
} catch (error) {
|
|
382
|
+
add({
|
|
383
|
+
id: "env.home",
|
|
384
|
+
ok: false,
|
|
385
|
+
required: true,
|
|
386
|
+
detail: error instanceof Error ? error.message : "home failed",
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let password: string | undefined;
|
|
391
|
+
try {
|
|
392
|
+
password = await resolvePassword(args, false);
|
|
393
|
+
add({
|
|
394
|
+
id: "env.password",
|
|
395
|
+
ok: true,
|
|
396
|
+
required: false,
|
|
397
|
+
detail: password ? "present" : "unset",
|
|
398
|
+
skipped: !password,
|
|
399
|
+
});
|
|
400
|
+
} catch (error) {
|
|
401
|
+
add({
|
|
402
|
+
id: "env.password",
|
|
403
|
+
ok: false,
|
|
404
|
+
required: false,
|
|
405
|
+
detail: error instanceof Error ? error.message : "password failed",
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const backup = backupPath(args, cfg.home);
|
|
410
|
+
if (pathExists(backup)) {
|
|
411
|
+
add({
|
|
412
|
+
id: "fs.backup",
|
|
413
|
+
ok: true,
|
|
414
|
+
required: false,
|
|
415
|
+
detail: backup,
|
|
416
|
+
});
|
|
417
|
+
} else {
|
|
418
|
+
add({
|
|
419
|
+
id: "fs.backup",
|
|
420
|
+
ok: true,
|
|
421
|
+
required: false,
|
|
422
|
+
detail: "missing",
|
|
423
|
+
skipped: true,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (pathExists(cfg.cookieJar)) {
|
|
427
|
+
add({
|
|
428
|
+
id: "fs.cookie_jar",
|
|
429
|
+
ok: true,
|
|
430
|
+
required: false,
|
|
431
|
+
detail: cfg.cookieJar,
|
|
432
|
+
});
|
|
433
|
+
} else {
|
|
434
|
+
add({
|
|
435
|
+
id: "fs.cookie_jar",
|
|
436
|
+
ok: true,
|
|
437
|
+
required: false,
|
|
438
|
+
detail: "missing",
|
|
439
|
+
skipped: true,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const client = createHttp(cfg);
|
|
444
|
+
try {
|
|
445
|
+
const meta = await requestJson(
|
|
446
|
+
client,
|
|
447
|
+
"GET",
|
|
448
|
+
"/.well-known/oauth-authorization-server"
|
|
449
|
+
);
|
|
450
|
+
const body = meta.json as {
|
|
451
|
+
issuer?: string;
|
|
452
|
+
token_endpoint?: string;
|
|
453
|
+
authorization_endpoint?: string;
|
|
454
|
+
registration_endpoint?: string;
|
|
455
|
+
code_challenge_methods_supported?: string[];
|
|
456
|
+
};
|
|
457
|
+
const ok =
|
|
458
|
+
meta.status === 200 &&
|
|
459
|
+
Boolean(body.issuer) &&
|
|
460
|
+
Boolean(body.token_endpoint) &&
|
|
461
|
+
Boolean(body.authorization_endpoint) &&
|
|
462
|
+
Boolean(body.registration_endpoint);
|
|
463
|
+
add({
|
|
464
|
+
id: "http.rfc8414",
|
|
465
|
+
ok,
|
|
466
|
+
required: true,
|
|
467
|
+
detail: ok
|
|
468
|
+
? `token_endpoint=${body.token_endpoint}`
|
|
469
|
+
: `status ${meta.status}`,
|
|
470
|
+
});
|
|
471
|
+
add({
|
|
472
|
+
id: "http.pkce",
|
|
473
|
+
ok: Boolean(body.code_challenge_methods_supported?.includes("S256")),
|
|
474
|
+
required: true,
|
|
475
|
+
detail: (body.code_challenge_methods_supported ?? []).join(","),
|
|
476
|
+
});
|
|
477
|
+
} catch (error) {
|
|
478
|
+
add({
|
|
479
|
+
id: "http.rfc8414",
|
|
480
|
+
ok: false,
|
|
481
|
+
required: true,
|
|
482
|
+
detail: error instanceof Error ? error.message : "rfc8414 failed",
|
|
483
|
+
});
|
|
484
|
+
add({
|
|
485
|
+
id: "http.pkce",
|
|
486
|
+
ok: false,
|
|
487
|
+
required: true,
|
|
488
|
+
detail: "skipped; rfc8414 failed",
|
|
489
|
+
skipped: true,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (password && pathExists(backup)) {
|
|
494
|
+
try {
|
|
495
|
+
const ciphertext = readFileSync(backup, "utf8");
|
|
496
|
+
const decrypted = await decryptMaster(ciphertext, password);
|
|
497
|
+
const bap = bapFromBackup(decrypted);
|
|
498
|
+
const ids = bap.listIds();
|
|
499
|
+
add({
|
|
500
|
+
id: "crypto.backup",
|
|
501
|
+
ok: ids.length >= 1,
|
|
502
|
+
required: true,
|
|
503
|
+
detail: `${ids.length} identities`,
|
|
504
|
+
});
|
|
505
|
+
if (ids.length >= 1) {
|
|
506
|
+
const member = memberWif(decrypted);
|
|
507
|
+
const root = rootPubkey(decrypted);
|
|
508
|
+
add({
|
|
509
|
+
id: "crypto.member_key",
|
|
510
|
+
ok: !root || root !== member.pubkey,
|
|
511
|
+
required: true,
|
|
512
|
+
detail: member.pubkey,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
} catch (error) {
|
|
516
|
+
add({
|
|
517
|
+
id: "crypto.backup",
|
|
518
|
+
ok: false,
|
|
519
|
+
required: true,
|
|
520
|
+
detail: error instanceof Error ? error.message : "decrypt failed",
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (pathExists(cfg.cookieJar)) {
|
|
526
|
+
const cookies = loadJar(cfg.cookieJar);
|
|
527
|
+
const names = sessionCookieNames(cookies);
|
|
528
|
+
const sessionNameOk = names.some(
|
|
529
|
+
(name) =>
|
|
530
|
+
name === "better-auth.session_token" ||
|
|
531
|
+
name === "__Secure-better-auth.session_token"
|
|
532
|
+
);
|
|
533
|
+
add({
|
|
534
|
+
id: "session.cookie",
|
|
535
|
+
ok: sessionNameOk,
|
|
536
|
+
required: false,
|
|
537
|
+
detail: names.join(",") || "no cookies",
|
|
538
|
+
});
|
|
539
|
+
try {
|
|
540
|
+
const session = await requestJson(client, "GET", "/api/auth/get-session", {
|
|
541
|
+
withCookies: true,
|
|
542
|
+
});
|
|
543
|
+
const body = session.json as { user?: { id?: string } } | null;
|
|
544
|
+
add({
|
|
545
|
+
id: "session.get",
|
|
546
|
+
ok: session.status === 200 && Boolean(body?.user?.id),
|
|
547
|
+
required: false,
|
|
548
|
+
detail: body?.user?.id ?? `status ${session.status}`,
|
|
549
|
+
});
|
|
550
|
+
} catch (error) {
|
|
551
|
+
add({
|
|
552
|
+
id: "session.get",
|
|
553
|
+
ok: false,
|
|
554
|
+
required: false,
|
|
555
|
+
detail: error instanceof Error ? error.message : "session failed",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const failed = checks.some((check) => check.required && !check.ok && !check.skipped);
|
|
561
|
+
if (cfg.json) {
|
|
562
|
+
printJson(!failed, { baseUrl: cfg.baseUrl, checks });
|
|
563
|
+
}
|
|
564
|
+
return failed ? 1 : 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export const HELP = `sigma — Sigma Auth CLI
|
|
568
|
+
|
|
569
|
+
Create a Bitcoin (BAP) identity key locally, sign in with Bitcoin-Auth, push
|
|
570
|
+
encrypted bitcoin-backup ciphertext, and register OAuth clients.
|
|
571
|
+
|
|
572
|
+
Commands:
|
|
573
|
+
identity create Create Type42 master + first BAP, encrypt, write .bep
|
|
574
|
+
identity info Decrypt local backup; print public fields
|
|
575
|
+
backup encrypt Encrypt a BapMasterBackup JSON file to .bep
|
|
576
|
+
auth sign-in Member-key Bitcoin-Auth sign-in; save cookies; register BAP
|
|
577
|
+
backup push POST ciphertext with session; never decrypt
|
|
578
|
+
oauth register Register an OAuth client (session path or DCR)
|
|
579
|
+
doctor Non-interactive health check
|
|
580
|
+
|
|
581
|
+
Global flags:
|
|
582
|
+
--base-url <url> SIGMA_AUTH_URL (default https://auth.sigmaidentity.com)
|
|
583
|
+
--home <dir> SIGMA_HOME (default ~/.sigma)
|
|
584
|
+
--cookie-jar <path>
|
|
585
|
+
--json Machine output
|
|
586
|
+
--quiet
|
|
587
|
+
-h, --help
|
|
588
|
+
|
|
589
|
+
Password: --password-file, --password-stdin, or SIGMA_BACKUP_PASSWORD.
|
|
590
|
+
--password on argv is rejected.
|
|
591
|
+
|
|
592
|
+
This CLI never sends private keys to the server. Identity is a BAP key, not an API key.
|
|
593
|
+
`;
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ParsedArgs } from "./args.ts";
|
|
4
|
+
import { boolFlag, flag } from "./args.ts";
|
|
5
|
+
import { envFail } from "./error.ts";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_BASE_URL = "https://auth.sigmaidentity.com";
|
|
8
|
+
export const MIN_PASSWORD_LENGTH = 8;
|
|
9
|
+
|
|
10
|
+
function envRaw(name: string): string | undefined {
|
|
11
|
+
if (!(name in process.env)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return process.env[name];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function envMustBeNonEmpty(name: string, value: string | undefined): string | undefined {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (value === "") {
|
|
22
|
+
envFail(`${name} is set but empty`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type RuntimeConfig = {
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
home: string;
|
|
30
|
+
cookieJar: string;
|
|
31
|
+
json: boolean;
|
|
32
|
+
quiet: boolean;
|
|
33
|
+
force: boolean;
|
|
34
|
+
timeoutMs: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function loadConfig(args: ParsedArgs): RuntimeConfig {
|
|
38
|
+
const urlFlag = flag(args, "base-url");
|
|
39
|
+
const urlEnv = envMustBeNonEmpty("SIGMA_AUTH_URL", envRaw("SIGMA_AUTH_URL"));
|
|
40
|
+
const baseUrl = urlFlag ?? urlEnv ?? DEFAULT_BASE_URL;
|
|
41
|
+
if (baseUrl === "") {
|
|
42
|
+
envFail("SIGMA_AUTH_URL is set but empty");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const homeFlag = flag(args, "home");
|
|
46
|
+
const homeEnv = envMustBeNonEmpty("SIGMA_HOME", envRaw("SIGMA_HOME"));
|
|
47
|
+
const home = homeFlag ?? homeEnv ?? join(homedir(), ".sigma");
|
|
48
|
+
if (home === "") {
|
|
49
|
+
envFail("SIGMA_HOME is set but empty");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const jarFlag = flag(args, "cookie-jar");
|
|
53
|
+
const jarEnv = envMustBeNonEmpty("SIGMA_COOKIE_JAR", envRaw("SIGMA_COOKIE_JAR"));
|
|
54
|
+
const cookieJar = jarFlag ?? jarEnv ?? join(home, "cookies.txt");
|
|
55
|
+
if (cookieJar === "") {
|
|
56
|
+
envFail("SIGMA_COOKIE_JAR is set but empty");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const timeoutRaw = flag(args, "timeout");
|
|
60
|
+
const timeoutMs = timeoutRaw ? Number.parseInt(timeoutRaw, 10) : 10_000;
|
|
61
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
62
|
+
envFail("--timeout must be a positive number of milliseconds");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
baseUrl: baseUrl.replace(/\/$/, ""),
|
|
67
|
+
home,
|
|
68
|
+
cookieJar,
|
|
69
|
+
json: boolFlag(args, "json"),
|
|
70
|
+
quiet: boolFlag(args, "quiet"),
|
|
71
|
+
force: boolFlag(args, "force"),
|
|
72
|
+
timeoutMs,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function backupPath(args: ParsedArgs, home: string): string {
|
|
77
|
+
return flag(args, "backup") ?? join(home, "identity.bep");
|
|
78
|
+
}
|