@sigma-auth/cli 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -4,6 +4,8 @@ Headless CLI for [Sigma Auth](https://auth.sigmaidentity.com). Create a Bitcoin
4
4
 
5
5
  The server never sees private keys. Identity is a BAP member key, not an API key.
6
6
 
7
+ `identity create` mints a Type42 `rootPk` directly. Existing Type42 `.bep` files keep working; no re-key.
8
+
7
9
  ```bash
8
10
  bunx @sigma-auth/cli --help
9
11
  ```
@@ -26,14 +28,65 @@ bunx @sigma-auth/cli identity create \
26
28
 
27
29
  | Command | Job |
28
30
  | --- | --- |
29
- | `sigma identity create` | Create Type42 master + first BAP, encrypt `.bep` |
31
+ | `sigma identity create` | Mint a Type42 `rootPk` + first BAP, encrypt `.bep` |
30
32
  | `sigma identity info` | Public fields from a local backup |
31
33
  | `sigma backup encrypt` | JSON → `.bep` |
32
34
  | `sigma auth sign-in` | Member-key Bitcoin-Auth; cookie jar; register BAP |
33
35
  | `sigma backup push` | POST ciphertext only |
34
36
  | `sigma oauth register` | Session `/api/oauth-clients` or RFC 7591 DCR |
35
37
  | `sigma doctor` | Env, files, RFC 8414, session |
38
+ | `sigma diagnose bap` | Public BAP profile; optional `--pubkey` registered-list check |
39
+ | `sigma diagnose identities` | `GET /api/user/bap-ids` (`--pubkey` or session cookie) |
40
+ | `sigma diagnose last-oauth` | Last selected BAP for `--pubkey` `--client-id` |
41
+ | `sigma diagnose client` | Public OAuth client metadata |
36
42
 
37
43
  Password sources (exactly one): `--password-file`, `--password-stdin`, or `SIGMA_BACKUP_PASSWORD`. `--password` on argv is rejected.
38
44
 
45
+ ## Diagnose (no private keys)
46
+
47
+ Public HTTP lookups against `https://auth.sigmaidentity.com`. Use these when a Sigma login shows the wrong faucet, a profile 404s, or last-selected identity looks stuck.
48
+
49
+ ```bash
50
+ bunx @sigma-auth/cli diagnose bap --bap-id 3QpdyNb9HScYmWEyfqtRQbKzwyf --json
51
+ bunx @sigma-auth/cli diagnose bap --bap-id 33mGVYzkGE9XMbu346XkUaMHyzwV --pubkey 03a42932… --json
52
+ bunx @sigma-auth/cli diagnose identities --pubkey 03a42932… --json
53
+ bunx @sigma-auth/cli diagnose last-oauth --pubkey 03a42932… --client-id droplit --json
54
+ ```
55
+
56
+ `diagnose bap` treats a profile 404 as a successful diagnosis (`found: false`), not a command failure.
57
+
39
58
  Contract: `docs/specs/sigma-cli-v1.md` in [sigma-auth](https://github.com/b-open-io/sigma-auth).
59
+
60
+
61
+ ### Delegated agent authorization
62
+
63
+ ```sh
64
+ sigma agent capabilities --json
65
+ sigma agent connect --name "My agent" --capability profile.read --capability identity.read --json
66
+ # Complete the returned verificationUri in your browser, using userCode when provided.
67
+ # Save the returned agentId; connect does not wait for approval.
68
+ sigma agent status --agent-id AGENT_ID --json
69
+ sigma agent execute --agent-id AGENT_ID --capability profile.read --args-file arguments.json --json
70
+ sigma agent disconnect --agent-id AGENT_ID --json
71
+ ```
72
+
73
+ Use capability names from discovery and a JSON object in `arguments.json`.
74
+ Repeat `status` after its `nextPollAt` (Unix milliseconds). It makes at most one
75
+ status request, preserves server throttling across restarts, and reports terminal
76
+ approval states. Reuse the agent ID instead of repeating registration. After a
77
+ failed connect request, do not blindly reconnect: the registration may have
78
+ reached the provider. Retain `SIGMA_HOME` for diagnosis.
79
+
80
+ This uses Better Auth Agent Auth via `@auth/agent` 0.6.2, with delegated human
81
+ approval. It is not WorkOS auth.md, RFC 8628 device authorization, or a BRC100
82
+ wallet signer. The CLI never submits human approval or uses browser session
83
+ cookies. Execution requires an active capability explicitly requested at connect.
84
+ Agent and host keys live separately under `SIGMA_HOME/agent-auth`, with private
85
+ atomic files (0600) and directory (0700). Revocation failures retain credentials
86
+ for a retry. Keep this directory private and use one CLI process per SIGMA_HOME
87
+ at a time; the SDK's index updates are not a multi-process transaction.
88
+
89
+ `--base-url` or `SIGMA_AUTH_URL` selects the provider. HTTPS is required except
90
+ explicit localhost HTTP testing; discovery and execution URLs must share the
91
+ provider origin. Redirects are rejected. Approval URL query strings and complete
92
+ claim URLs are omitted from output because they may contain bearer artifacts.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sigma-auth/cli",
3
- "version": "0.0.2",
4
- "description": "Headless Sigma Auth CLI: create a BAP identity locally, sign in with Bitcoin-Auth, push encrypted backups, register OAuth clients",
3
+ "version": "0.0.4",
4
+ "description": "Headless Sigma Auth CLI: create a BAP identity locally, sign in with Bitcoin-Auth, diagnose public identities, push encrypted backups, register OAuth clients",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "sigma": "./src/index.ts"
@@ -35,6 +35,7 @@
35
35
  "bun": ">=1.1.0"
36
36
  },
37
37
  "dependencies": {
38
+ "@auth/agent": "0.6.2",
38
39
  "@bsv/sdk": "^2.1.6",
39
40
  "bitcoin-auth": "^0.0.8",
40
41
  "bitcoin-backup": "^0.0.13",
@@ -0,0 +1,57 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ fsyncSync,
6
+ openSync,
7
+ readFileSync,
8
+ renameSync,
9
+ rmSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { KVStorage } from "@auth/agent";
15
+ import { ensureDir } from "./fsutil.ts";
16
+
17
+ export function agentStore(home: string) {
18
+ const directory = join(home, "agent-auth");
19
+ ensureDir(directory);
20
+ const path = (key: string) =>
21
+ join(directory, `${createHash("sha256").update(key).digest("hex")}.json`);
22
+ const kv = {
23
+ async get(key: string): Promise<string | null> {
24
+ try {
25
+ return readFileSync(path(key), "utf8");
26
+ } catch (error) {
27
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
28
+ throw error;
29
+ }
30
+ },
31
+ async set(key: string, value: string) {
32
+ const target = path(key);
33
+ const temporary = `${target}.${randomUUID()}.tmp`;
34
+ const fd = openSync(temporary, "wx", 0o600);
35
+ try {
36
+ writeFileSync(fd, value);
37
+ fsyncSync(fd);
38
+ } finally {
39
+ closeSync(fd);
40
+ }
41
+ try {
42
+ renameSync(temporary, target);
43
+ chmodSync(target, 0o600);
44
+ } finally {
45
+ rmSync(temporary, { force: true });
46
+ }
47
+ },
48
+ async del(key: string) {
49
+ try {
50
+ unlinkSync(path(key));
51
+ } catch (error) {
52
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
53
+ }
54
+ },
55
+ };
56
+ return { kv, storage: new KVStorage(kv) };
57
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,366 @@
1
+ import {
2
+ AgentAuthClient,
3
+ type ApprovalInfo,
4
+ type ProviderConfig,
5
+ } from "@auth/agent";
6
+ import { agentStore } from "./agent-store.ts";
7
+ import { flag, flagList, type ParsedArgs } from "./args.ts";
8
+ import type { RuntimeConfig } from "./config.ts";
9
+ import { CliError, usage } from "./error.ts";
10
+ import { readText } from "./fsutil.ts";
11
+ import { printHuman, printJson } from "./output.ts";
12
+
13
+ type State = {
14
+ agentId: string;
15
+ provider: string;
16
+ requestedCapabilities: string[];
17
+ status: string;
18
+ verificationUri?: string;
19
+ userCode?: string;
20
+ expiresAt?: number;
21
+ nextPollAt: number;
22
+ intervalMs: number;
23
+ };
24
+ const pending = Symbol("approval saved");
25
+ const terminal = new Set(["denied", "rejected", "revoked", "expired"]);
26
+ function fail(message: string): never {
27
+ throw new CliError(1, "agent", message);
28
+ }
29
+
30
+ export function agentUrl(value: string, origin?: string): URL {
31
+ let url: URL;
32
+ try {
33
+ url = new URL(value);
34
+ } catch {
35
+ return fail("Invalid agent provider URL");
36
+ }
37
+ const localhost = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
38
+ if (
39
+ (url.protocol !== "https:" && !(url.protocol === "http:" && localhost)) ||
40
+ url.username ||
41
+ url.password ||
42
+ url.hash ||
43
+ (origin && url.origin !== origin)
44
+ ) {
45
+ fail(
46
+ "Agent URLs must use HTTPS (HTTP localhost is allowed), have no credentials, and share the provider origin",
47
+ );
48
+ }
49
+ return url;
50
+ }
51
+
52
+ export function validateAgentProvider(
53
+ config: ProviderConfig,
54
+ origin: string,
55
+ ): void {
56
+ agentUrl(config.issuer, origin);
57
+ if (!config.endpoints || typeof config.endpoints !== "object")
58
+ fail("Invalid agent discovery endpoints");
59
+ for (const endpoint of Object.values(config.endpoints)) {
60
+ if (typeof endpoint !== "string") fail("Invalid agent discovery endpoint");
61
+ agentUrl(
62
+ new URL(endpoint, `${config.issuer.replace(/\/+$/, "")}/`).href,
63
+ origin,
64
+ );
65
+ }
66
+ for (const location of [
67
+ config.default_location,
68
+ config.jwks_uri,
69
+ ...(config.capabilities ?? []).map((cap) => cap.location),
70
+ ]) {
71
+ if (location) agentUrl(location, origin);
72
+ }
73
+ }
74
+
75
+ function publicApproval(info: ApprovalInfo, origin: string) {
76
+ const uri = info.verification_uri
77
+ ? agentUrl(info.verification_uri, origin)
78
+ : undefined;
79
+ // Complete URIs and query strings can contain bearer claim artifacts.
80
+ if (uri) uri.search = "";
81
+ return { verificationUri: uri?.href, userCode: info.user_code };
82
+ }
83
+
84
+ function redact(value: unknown, secrets: string[]): unknown {
85
+ if (typeof value === "string") {
86
+ let text = value.replace(
87
+ /Bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/gi,
88
+ "[redacted]",
89
+ );
90
+ for (const secret of secrets)
91
+ if (secret) text = text.replaceAll(secret, "[redacted]");
92
+ return text;
93
+ }
94
+ if (Array.isArray(value)) return value.map((item) => redact(item, secrets));
95
+ if (value && typeof value === "object") {
96
+ return Object.fromEntries(
97
+ Object.entries(value)
98
+ .filter(
99
+ ([key]) =>
100
+ !/^(d|privateKey|private_key|agentKeypair|keypair|authorization|headers|cookie|cookies|.*token.*|.*secret.*|.*password.*|verification_uri_complete)$/i.test(
101
+ key,
102
+ ),
103
+ )
104
+ .map(([key, item]) => [key, redact(item, secrets)]),
105
+ );
106
+ }
107
+ return value;
108
+ }
109
+
110
+ export async function agentCommand(
111
+ args: ParsedArgs,
112
+ cfg: RuntimeConfig,
113
+ ): Promise<number> {
114
+ const command = args.positional[1];
115
+ if (
116
+ !["capabilities", "connect", "status", "execute", "disconnect"].includes(
117
+ command ?? "",
118
+ )
119
+ )
120
+ usage(
121
+ "Expected agent capabilities, connect, status, execute, or disconnect",
122
+ );
123
+ const required = (name: string) =>
124
+ flag(args, name)?.trim() || usage(`--${name} is required`);
125
+ const requested = [...new Set(flagList(args, "capability"))];
126
+ const name = command === "connect" ? required("name") : undefined;
127
+ if (
128
+ command === "connect" &&
129
+ (!requested.length || requested.some((cap) => !cap.trim()))
130
+ )
131
+ usage("At least one --capability is required");
132
+ const agentId = ["status", "execute", "disconnect"].includes(command ?? "")
133
+ ? required("agent-id")
134
+ : undefined;
135
+ const capability = command === "execute" ? required("capability") : undefined;
136
+ if (command === "execute" && requested.length !== 1)
137
+ usage("Execute accepts exactly one --capability");
138
+ let argumentsValue: Record<string, unknown> | undefined;
139
+ if (command === "execute") {
140
+ try {
141
+ argumentsValue = JSON.parse(readText(required("args-file")));
142
+ } catch {
143
+ usage("--args-file must contain a JSON object");
144
+ }
145
+ if (
146
+ !argumentsValue ||
147
+ Array.isArray(argumentsValue) ||
148
+ typeof argumentsValue !== "object"
149
+ )
150
+ usage("--args-file must contain a JSON object");
151
+ }
152
+ const origin = agentUrl(cfg.baseUrl).origin;
153
+ const { kv, storage } = agentStore(cfg.home);
154
+ let state: State | undefined;
155
+ let revokeConfirmed = false;
156
+ const stateKey = (id: string) => `cli:state:${id}`;
157
+ const save = async () => {
158
+ if (state) await kv.set(stateKey(state.agentId), JSON.stringify(state));
159
+ };
160
+ if (agentId) {
161
+ const raw = await kv.get(stateKey(agentId));
162
+ if (!raw) fail("No saved agent connection; connect first");
163
+ state = JSON.parse(raw) as State;
164
+ agentUrl(state.provider, origin);
165
+ }
166
+ const originalDelete = storage.deleteAgentConnection.bind(storage);
167
+ storage.deleteAgentConnection = async (id) => {
168
+ // SDK 0.6.2 ignores revoke HTTP/network failures. Keep credentials for retry.
169
+ if (command === "disconnect" && !revokeConfirmed)
170
+ fail(
171
+ "Revocation was not confirmed; local credentials retained. Retry disconnect",
172
+ );
173
+ await originalDelete(id);
174
+ };
175
+ const transport = (async (
176
+ input: string | URL | Request,
177
+ init?: RequestInit,
178
+ ) => {
179
+ const url = agentUrl(
180
+ typeof input === "string"
181
+ ? input
182
+ : input instanceof URL
183
+ ? input.href
184
+ : input.url,
185
+ origin,
186
+ );
187
+ const response = await fetch(input, {
188
+ ...init,
189
+ redirect: "error",
190
+ credentials: "omit",
191
+ signal: AbortSignal.timeout(cfg.timeoutMs),
192
+ });
193
+ if (response.url) agentUrl(response.url, origin);
194
+ if (command === "disconnect") revokeConfirmed = response.ok;
195
+ if (state && command === "status") {
196
+ const retry = response.headers.get("retry-after");
197
+ const seconds = retry === null ? Number.NaN : Number(retry);
198
+ const until = Number.isFinite(seconds)
199
+ ? Date.now() + Math.max(0, seconds) * 1000
200
+ : Date.parse(retry ?? "");
201
+ if (response.status === 429)
202
+ state.intervalMs = Math.max(state.intervalMs * 2, 5000);
203
+ state.nextPollAt = Math.max(
204
+ state.nextPollAt,
205
+ Date.now() + state.intervalMs,
206
+ Number.isFinite(until) ? until : 0,
207
+ );
208
+ await save();
209
+ if (response.status === 429)
210
+ throw new CliError(
211
+ 5,
212
+ "rate_limit",
213
+ "Agent status is throttled; retry after nextPollAt",
214
+ 429,
215
+ );
216
+ }
217
+ if (response.ok && url.pathname.endsWith("agent-configuration"))
218
+ validateAgentProvider(
219
+ (await response.clone().json()) as ProviderConfig,
220
+ origin,
221
+ );
222
+ return response;
223
+ }) as typeof fetch;
224
+ // URL-only mode disables the SDK default public directory. Discovery stays on this provider.
225
+ const client = new AgentAuthClient({
226
+ storage,
227
+ urls: [cfg.baseUrl],
228
+ fetch: transport,
229
+ onApprovalRequired: async (info) => {
230
+ if (!state) fail("Registration state was not saved");
231
+ Object.assign(state, publicApproval(info, origin), {
232
+ status: "pending",
233
+ expiresAt: Date.now() + Math.max(0, info.expires_in) * 1000,
234
+ intervalMs: Math.max(1, info.interval || 5) * 1000,
235
+ nextPollAt: Date.now() + Math.max(1, info.interval || 5) * 1000,
236
+ });
237
+ await save();
238
+ throw pending;
239
+ },
240
+ });
241
+ // Hook SDK persistence, which occurs before onApprovalRequired (no polling or duplicate registration).
242
+ const originalSet = storage.setAgentConnection.bind(storage);
243
+ storage.setAgentConnection = async (id, connection) => {
244
+ await originalSet(id, connection);
245
+ if (command === "connect") {
246
+ state = {
247
+ agentId: id,
248
+ provider: connection.issuer,
249
+ requestedCapabilities: requested,
250
+ status: "pending",
251
+ nextPollAt: Date.now() + 5000,
252
+ intervalMs: 5000,
253
+ };
254
+ await save();
255
+ }
256
+ };
257
+ const output = async (data: unknown) => {
258
+ const host = await storage.getHostIdentity();
259
+ const connection = state
260
+ ? await storage.getAgentConnection(state.agentId)
261
+ : null;
262
+ const secrets = [
263
+ host?.keypair.privateKey.d,
264
+ connection?.agentKeypair.privateKey.d,
265
+ ].filter((value): value is string => typeof value === "string");
266
+ const safe = redact(data, secrets);
267
+ if (cfg.json) printJson(true, safe);
268
+ else printHuman(cfg, JSON.stringify(safe, null, 2));
269
+ return 0;
270
+ };
271
+ try {
272
+ const provider = agentId
273
+ ? await storage.getProviderConfig(state!.provider)
274
+ : await client.discoverProvider(cfg.baseUrl);
275
+ if (!provider)
276
+ fail(
277
+ "Saved provider configuration is missing; connection cannot be used",
278
+ );
279
+ validateAgentProvider(provider, origin);
280
+ if (command === "capabilities")
281
+ return output(
282
+ await client.listCapabilities({ provider: provider.issuer }),
283
+ );
284
+ if (command === "connect") {
285
+ if (!provider.modes.includes("delegated"))
286
+ fail("Provider does not support delegated approval");
287
+ try {
288
+ const result = await client.connectAgent({
289
+ provider: provider.issuer,
290
+ name,
291
+ capabilities: requested,
292
+ mode: "delegated",
293
+ });
294
+ state!.status = result.status;
295
+ await save();
296
+ } catch (error) {
297
+ if (error !== pending) throw error;
298
+ }
299
+ return output(state);
300
+ }
301
+ if (command === "status") {
302
+ // Approval may have completed before expiresAt while this CLI was stopped.
303
+ // Only the provider can declare the registration expired.
304
+ if (!terminal.has(state!.status) && Date.now() >= state!.nextPollAt) {
305
+ state!.nextPollAt = Date.now() + state!.intervalMs;
306
+ await save();
307
+ try {
308
+ const result = await client.agentStatus(agentId!);
309
+ state!.status = result.status;
310
+ } catch (error) {
311
+ if (!(error instanceof CliError && error.status === 429)) throw error;
312
+ }
313
+ }
314
+ await save();
315
+ const connection = await storage.getAgentConnection(agentId!);
316
+ return output({
317
+ ...state,
318
+ grants: connection?.capabilityGrants.map(
319
+ ({ capability, status, constraints }) => ({
320
+ capability,
321
+ status,
322
+ constraints,
323
+ }),
324
+ ),
325
+ action: terminal.has(state!.status)
326
+ ? "Connect again to request new approval"
327
+ : state!.status === "pending"
328
+ ? "Complete approval in your browser, then run status after nextPollAt"
329
+ : undefined,
330
+ });
331
+ }
332
+ if (command === "disconnect") {
333
+ await client.disconnectAgent(agentId!);
334
+ state!.status = "revoked";
335
+ await save();
336
+ return output({ agentId, status: "revoked" });
337
+ }
338
+ if (state!.status !== "active")
339
+ fail("Agent is not active; run status after approval");
340
+ const connection = await storage.getAgentConnection(agentId!);
341
+ if (
342
+ !state!.requestedCapabilities.includes(capability!) ||
343
+ !connection?.capabilityGrants.some(
344
+ (grant) => grant.capability === capability && grant.status === "active",
345
+ )
346
+ )
347
+ fail("Capability is not an active requested grant");
348
+ return output(
349
+ await client.executeCapability({
350
+ agentId: agentId!,
351
+ capability: capability!,
352
+ arguments: argumentsValue,
353
+ }),
354
+ );
355
+ } catch (error) {
356
+ if (error instanceof CliError) throw error;
357
+ // SDK errors may contain untrusted server response text, URLs, and headers.
358
+ throw new CliError(
359
+ 1,
360
+ "agent",
361
+ "Agent request failed; credentials were retained. Check provider availability and run status before retrying",
362
+ );
363
+ } finally {
364
+ client.destroy();
365
+ }
366
+ }
package/src/args.ts CHANGED
@@ -7,12 +7,11 @@ const BOOL_FLAGS = new Set([
7
7
  "help",
8
8
  "signin",
9
9
  "push-backup",
10
- "show-mnemonic",
11
10
  "public",
12
11
  "password-stdin",
13
12
  ]);
14
13
 
15
- const REPEATABLE = new Set(["redirect-uri", "grant-type"]);
14
+ const REPEATABLE = new Set(["redirect-uri", "grant-type", "capability"]);
16
15
 
17
16
  export type ParsedArgs = {
18
17
  positional: string[];
@@ -42,7 +41,9 @@ export function parseArgs(argv: string[]): ParsedArgs {
42
41
  break;
43
42
  }
44
43
  if (token === "--password" || token.startsWith("--password=")) {
45
- usage("--password is not allowed; use --password-file, --password-stdin, or SIGMA_BACKUP_PASSWORD");
44
+ usage(
45
+ "--password is not allowed; use --password-file, --password-stdin, or SIGMA_BACKUP_PASSWORD",
46
+ );
46
47
  }
47
48
  if (token === "-h") {
48
49
  flags.help = ["true"];
package/src/commands.ts CHANGED
@@ -20,14 +20,18 @@ import {
20
20
  publicFields,
21
21
  rootPubkey,
22
22
  } from "./identity.ts";
23
- import { printHuman, printJson, printWarn, type OutputMode } from "./output.ts";
23
+ import { type OutputMode, printHuman, printJson, printWarn } from "./output.ts";
24
24
  import { resolvePassword } from "./password.ts";
25
25
 
26
26
  function mode(cfg: RuntimeConfig): OutputMode {
27
27
  return { json: cfg.json, quiet: cfg.quiet };
28
28
  }
29
29
 
30
- function succeed(cfg: RuntimeConfig, data: Record<string, unknown>, human: string): number {
30
+ function succeed(
31
+ cfg: RuntimeConfig,
32
+ data: Record<string, unknown>,
33
+ human: string,
34
+ ): number {
31
35
  if (cfg.json) {
32
36
  printJson(true, data);
33
37
  } else {
@@ -39,7 +43,7 @@ function succeed(cfg: RuntimeConfig, data: Record<string, unknown>, human: strin
39
43
  async function loadBackup(
40
44
  args: ParsedArgs,
41
45
  cfg: RuntimeConfig,
42
- password: string
46
+ password: string,
43
47
  ): Promise<{ path: string; backup: BapMasterBackup; ciphertext: string }> {
44
48
  const path = backupPath(args, cfg.home);
45
49
  const ciphertext = readText(path);
@@ -49,7 +53,7 @@ async function loadBackup(
49
53
 
50
54
  export async function identityCreate(
51
55
  args: ParsedArgs,
52
- cfg: RuntimeConfig
56
+ cfg: RuntimeConfig,
53
57
  ): Promise<number> {
54
58
  const label = flag(args, "label");
55
59
  if (!label) {
@@ -63,13 +67,6 @@ export async function identityCreate(
63
67
  const encrypted = await encryptMaster(created.backup, password);
64
68
  const out = flag(args, "out") ?? `${cfg.home}/identity.bep`;
65
69
  writeSecretFile(out, encrypted, cfg.force);
66
- if (boolFlag(args, "show-mnemonic")) {
67
- process.stderr.write(`${created.mnemonic}\n`);
68
- }
69
- const mnemonicFile = flag(args, "mnemonic-file");
70
- if (mnemonicFile) {
71
- writeSecretFile(mnemonicFile, `${created.mnemonic}\n`, cfg.force);
72
- }
73
70
 
74
71
  let userId: string | undefined;
75
72
  if (boolFlag(args, "signin") || boolFlag(args, "push-backup")) {
@@ -98,13 +95,13 @@ export async function identityCreate(
98
95
  backupPath: out,
99
96
  userId,
100
97
  },
101
- `created ${created.bapId}\n${out}`
98
+ `created ${created.bapId}\n${out}`,
102
99
  );
103
100
  }
104
101
 
105
102
  export async function identityInfo(
106
103
  args: ParsedArgs,
107
- cfg: RuntimeConfig
104
+ cfg: RuntimeConfig,
108
105
  ): Promise<number> {
109
106
  const password = await resolvePassword(args, true);
110
107
  if (!password) {
@@ -115,13 +112,13 @@ export async function identityInfo(
115
112
  return succeed(
116
113
  cfg,
117
114
  fields,
118
- `${fields.bapId}\n${fields.pubkey}\n${fields.address}`
115
+ `${fields.bapId}\n${fields.pubkey}\n${fields.address}`,
119
116
  );
120
117
  }
121
118
 
122
119
  export async function backupEncrypt(
123
120
  args: ParsedArgs,
124
- cfg: RuntimeConfig
121
+ cfg: RuntimeConfig,
125
122
  ): Promise<number> {
126
123
  const input = flag(args, "in");
127
124
  const out = flag(args, "out");
@@ -148,18 +145,14 @@ export async function backupEncrypt(
148
145
  }
149
146
  const encrypted = await encryptMaster(parsed, password);
150
147
  writeSecretFile(out, encrypted, cfg.force);
151
- return succeed(
152
- cfg,
153
- { backupPath: out, warning },
154
- out
155
- );
148
+ return succeed(cfg, { backupPath: out, warning }, out);
156
149
  }
157
150
 
158
151
  export async function authSignIn(
159
152
  args: ParsedArgs,
160
153
  cfg: RuntimeConfig,
161
154
  emit = true,
162
- resolvedPassword?: string
155
+ resolvedPassword?: string,
163
156
  ): Promise<number> {
164
157
  const password = resolvedPassword ?? (await resolvePassword(args, true));
165
158
  if (!password) {
@@ -190,14 +183,13 @@ export async function authSignIn(
190
183
  signed.status,
191
184
  signed.json,
192
185
  signed.text,
193
- signed.headers
186
+ signed.headers,
194
187
  );
195
188
  }
196
189
  const payload = signed.json as {
197
190
  user?: { id?: string; pubkey?: string };
198
191
  };
199
- const name =
200
- ("label" in backup && backup.label) || "Identity 1";
192
+ const name = ("label" in backup && backup.label) || "Identity 1";
201
193
  const registered = await requestJson(client, "POST", "/api/user/bap-ids", {
202
194
  body: {
203
195
  bapId: member.bapId,
@@ -218,7 +210,7 @@ export async function authSignIn(
218
210
  registered.status,
219
211
  registered.json,
220
212
  registered.text,
221
- registered.headers
213
+ registered.headers,
222
214
  );
223
215
  }
224
216
  if (!emit) {
@@ -232,14 +224,14 @@ export async function authSignIn(
232
224
  bapId: member.bapId,
233
225
  cookieJar: cfg.cookieJar,
234
226
  },
235
- `signed in as ${member.bapId}`
227
+ `signed in as ${member.bapId}`,
236
228
  );
237
229
  }
238
230
 
239
231
  export async function backupPush(
240
232
  args: ParsedArgs,
241
233
  cfg: RuntimeConfig,
242
- emit = true
234
+ emit = true,
243
235
  ): Promise<number> {
244
236
  const path = backupPath(args, cfg.home);
245
237
  const ciphertext = readText(path).replace(/\n+$/, "");
@@ -250,7 +242,13 @@ export async function backupPush(
250
242
  withCookies: true,
251
243
  });
252
244
  if (result.status >= 400) {
253
- throwHttp("/api/backup", result.status, result.json, result.text, result.headers);
245
+ throwHttp(
246
+ "/api/backup",
247
+ result.status,
248
+ result.json,
249
+ result.text,
250
+ result.headers,
251
+ );
254
252
  }
255
253
  const payload = result.json as { bapId?: string; message?: string };
256
254
  if (!emit) {
@@ -259,13 +257,13 @@ export async function backupPush(
259
257
  return succeed(
260
258
  cfg,
261
259
  { bapId: payload.bapId, message: payload.message },
262
- payload.message ?? "backup stored"
260
+ payload.message ?? "backup stored",
263
261
  );
264
262
  }
265
263
 
266
264
  export async function oauthRegister(
267
265
  args: ParsedArgs,
268
- cfg: RuntimeConfig
266
+ cfg: RuntimeConfig,
269
267
  ): Promise<number> {
270
268
  const name = flag(args, "name");
271
269
  const redirectUris = flagList(args, "redirect-uri");
@@ -281,7 +279,9 @@ export async function oauthRegister(
281
279
  const ownerBapId = flag(args, "owner-bap-id");
282
280
  const clientId = flag(args, "client-id");
283
281
  if (!ownerBapId || !clientId) {
284
- usage("--owner-bap-id and --client-id are required with --signing-pubkey");
282
+ usage(
283
+ "--owner-bap-id and --client-id are required with --signing-pubkey",
284
+ );
285
285
  }
286
286
  const result = await requestJson(client, "POST", "/api/oauth-clients", {
287
287
  body: {
@@ -299,11 +299,15 @@ export async function oauthRegister(
299
299
  result.status,
300
300
  result.json,
301
301
  result.text,
302
- result.headers
302
+ result.headers,
303
303
  );
304
304
  }
305
305
  const payload = result.json as {
306
- client?: { clientId?: string; accountPubkey?: string; ownerBapId?: string };
306
+ client?: {
307
+ clientId?: string;
308
+ accountPubkey?: string;
309
+ ownerBapId?: string;
310
+ };
307
311
  };
308
312
  return succeed(
309
313
  cfg,
@@ -315,7 +319,7 @@ export async function oauthRegister(
315
319
  public: true,
316
320
  path: "session",
317
321
  },
318
- payload.client?.clientId ?? clientId
322
+ payload.client?.clientId ?? clientId,
319
323
  );
320
324
  }
321
325
 
@@ -335,7 +339,7 @@ export async function oauthRegister(
335
339
  response_types: ["code"],
336
340
  token_endpoint_auth_method: "none",
337
341
  },
338
- }
342
+ },
339
343
  );
340
344
  if (result.status >= 400) {
341
345
  throwHttp(
@@ -343,7 +347,7 @@ export async function oauthRegister(
343
347
  result.status,
344
348
  result.json,
345
349
  result.text,
346
- result.headers
350
+ result.headers,
347
351
  );
348
352
  }
349
353
  const payload = result.json as {
@@ -352,7 +356,7 @@ export async function oauthRegister(
352
356
  };
353
357
  printWarn(
354
358
  mode(cfg),
355
- "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."
359
+ "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.",
356
360
  );
357
361
  return succeed(
358
362
  cfg,
@@ -362,7 +366,7 @@ export async function oauthRegister(
362
366
  path: "dcr",
363
367
  client_secret: payload.client_secret,
364
368
  },
365
- payload.client_id ?? "registered"
369
+ payload.client_id ?? "registered",
366
370
  );
367
371
  }
368
372
 
@@ -374,7 +378,10 @@ type Check = {
374
378
  skipped?: boolean;
375
379
  };
376
380
 
377
- export async function doctor(args: ParsedArgs, cfg: RuntimeConfig): Promise<number> {
381
+ export async function doctor(
382
+ args: ParsedArgs,
383
+ cfg: RuntimeConfig,
384
+ ): Promise<number> {
378
385
  const checks: Check[] = [];
379
386
  const add = (check: Check) => {
380
387
  checks.push(check);
@@ -461,7 +468,7 @@ export async function doctor(args: ParsedArgs, cfg: RuntimeConfig): Promise<numb
461
468
  const meta = await requestJson(
462
469
  client,
463
470
  "GET",
464
- "/.well-known/oauth-authorization-server"
471
+ "/.well-known/oauth-authorization-server",
465
472
  );
466
473
  const body = meta.json as {
467
474
  issuer?: string;
@@ -544,7 +551,7 @@ export async function doctor(args: ParsedArgs, cfg: RuntimeConfig): Promise<numb
544
551
  const sessionNameOk = names.some(
545
552
  (name) =>
546
553
  name === "better-auth.session_token" ||
547
- name === "__Secure-better-auth.session_token"
554
+ name === "__Secure-better-auth.session_token",
548
555
  );
549
556
  add({
550
557
  id: "session.cookie",
@@ -553,9 +560,14 @@ export async function doctor(args: ParsedArgs, cfg: RuntimeConfig): Promise<numb
553
560
  detail: names.join(",") || "no cookies",
554
561
  });
555
562
  try {
556
- const session = await requestJson(client, "GET", "/api/auth/get-session", {
557
- withCookies: true,
558
- });
563
+ const session = await requestJson(
564
+ client,
565
+ "GET",
566
+ "/api/auth/get-session",
567
+ {
568
+ withCookies: true,
569
+ },
570
+ );
559
571
  const body = session.json as { user?: { id?: string } } | null;
560
572
  add({
561
573
  id: "session.get",
@@ -573,7 +585,9 @@ export async function doctor(args: ParsedArgs, cfg: RuntimeConfig): Promise<numb
573
585
  }
574
586
  }
575
587
 
576
- const failed = checks.some((check) => check.required && !check.ok && !check.skipped);
588
+ const failed = checks.some(
589
+ (check) => check.required && !check.ok && !check.skipped,
590
+ );
577
591
  if (cfg.json) {
578
592
  printJson(!failed, { baseUrl: cfg.baseUrl, checks });
579
593
  }
@@ -586,6 +600,11 @@ Create a Bitcoin (BAP) identity key locally, sign in with Bitcoin-Auth, push
586
600
  encrypted bitcoin-backup ciphertext, and register OAuth clients.
587
601
 
588
602
  Commands:
603
+ agent capabilities [--json]
604
+ agent connect --name NAME --capability NAME [--capability NAME] [--json]
605
+ agent status --agent-id ID [--json]
606
+ agent execute --agent-id ID --capability NAME --args-file FILE [--json]
607
+ agent disconnect --agent-id ID [--json]
589
608
  identity create Create Type42 master + first BAP, encrypt, write .bep
590
609
  identity info Decrypt local backup; print public fields
591
610
  backup encrypt Encrypt a BapMasterBackup JSON file to .bep
@@ -593,6 +612,10 @@ Commands:
593
612
  backup push POST ciphertext with session; never decrypt
594
613
  oauth register Register an OAuth client (session path or DCR)
595
614
  doctor Non-interactive health check
615
+ diagnose bap Public BAP profile (+ --pubkey registered-list check)
616
+ diagnose identities GET /api/user/bap-ids [--pubkey]
617
+ diagnose last-oauth Last selected BAP for --pubkey --client-id
618
+ diagnose client Public OAuth client metadata --client-id
596
619
 
597
620
  Global flags:
598
621
  --base-url <url> SIGMA_AUTH_URL (default https://auth.sigmaidentity.com)
@@ -0,0 +1,191 @@
1
+ import type { ParsedArgs } from "./args.ts";
2
+ import { flag } from "./args.ts";
3
+ import type { RuntimeConfig } from "./config.ts";
4
+ import { usage } from "./error.ts";
5
+ import { createHttp, requestJson, throwHttp } from "./http.ts";
6
+ import { printHuman, printJson, type OutputMode } from "./output.ts";
7
+
8
+ function mode(cfg: RuntimeConfig): OutputMode {
9
+ return { json: cfg.json, quiet: cfg.quiet };
10
+ }
11
+
12
+ function succeed(cfg: RuntimeConfig, data: Record<string, unknown>, human: string): number {
13
+ if (cfg.json) {
14
+ printJson(true, data);
15
+ } else {
16
+ printHuman(mode(cfg), human);
17
+ }
18
+ return 0;
19
+ }
20
+
21
+ function record(json: unknown): Record<string, unknown> {
22
+ return json && typeof json === "object" ? (json as Record<string, unknown>) : {};
23
+ }
24
+
25
+ export async function diagnoseBap(args: ParsedArgs, cfg: RuntimeConfig): Promise<number> {
26
+ const bapId = flag(args, "bap-id");
27
+ if (!bapId) {
28
+ usage("--bap-id is required");
29
+ }
30
+ const pubkey = flag(args, "pubkey");
31
+ const client = createHttp(cfg);
32
+ const profileRes = await requestJson(
33
+ client,
34
+ "GET",
35
+ `/api/bap/profile?bapId=${encodeURIComponent(bapId)}`
36
+ );
37
+ if (profileRes.status >= 500) {
38
+ throwHttp(
39
+ "/api/bap/profile",
40
+ profileRes.status,
41
+ profileRes.json,
42
+ profileRes.text,
43
+ profileRes.headers
44
+ );
45
+ }
46
+ const found = profileRes.status === 200 && record(profileRes.json).status === "OK";
47
+ const profile = found ? record(profileRes.json).result ?? record(profileRes.json) : null;
48
+
49
+ let registeredToPubkey: boolean | null = null;
50
+ let identities: unknown[] | undefined;
51
+ if (pubkey) {
52
+ const listRes = await requestJson(
53
+ client,
54
+ "GET",
55
+ `/api/user/bap-ids?pubkey=${encodeURIComponent(pubkey)}`
56
+ );
57
+ if (listRes.status >= 400) {
58
+ throwHttp(
59
+ "/api/user/bap-ids",
60
+ listRes.status,
61
+ listRes.json,
62
+ listRes.text,
63
+ listRes.headers
64
+ );
65
+ }
66
+ const bapIds = record(listRes.json).bapIds;
67
+ identities = Array.isArray(bapIds) ? bapIds : [];
68
+ registeredToPubkey = identities.some((row) => {
69
+ const item = record(row);
70
+ return item.identity_key === bapId || item.id === bapId || item.bapId === bapId;
71
+ });
72
+ }
73
+
74
+ let hint: string;
75
+ if (registeredToPubkey === true && found) {
76
+ hint = "registered to this pubkey and has a published profile";
77
+ } else if (registeredToPubkey === true && !found) {
78
+ hint = "registered to this pubkey but no published BAP profile";
79
+ } else if (registeredToPubkey === false && found) {
80
+ hint = "published profile exists but this BAP is not in this pubkey's registered list";
81
+ } else if (registeredToPubkey === false && !found) {
82
+ hint = "not in this pubkey's registered list and no published BAP profile (typical leftover HD identity)";
83
+ } else if (found) {
84
+ hint = "published BAP profile found";
85
+ } else {
86
+ hint = "no published BAP profile (404). Pass --pubkey to also check GET /api/user/bap-ids";
87
+ }
88
+
89
+ const data: Record<string, unknown> = {
90
+ bapId,
91
+ found,
92
+ profile,
93
+ hint,
94
+ };
95
+ if (pubkey) {
96
+ data.pubkey = pubkey;
97
+ data.registeredToPubkey = registeredToPubkey;
98
+ data.registeredCount = identities?.length ?? 0;
99
+ }
100
+
101
+ const lines = [
102
+ `bapId ${bapId}`,
103
+ `found ${found}`,
104
+ pubkey ? `registeredToPubkey ${registeredToPubkey}` : null,
105
+ hint,
106
+ ].filter((line): line is string => line !== null);
107
+ return succeed(cfg, data, lines.join("\n"));
108
+ }
109
+
110
+ export async function diagnoseIdentities(
111
+ args: ParsedArgs,
112
+ cfg: RuntimeConfig
113
+ ): Promise<number> {
114
+ const pubkey = flag(args, "pubkey");
115
+ const client = createHttp(cfg);
116
+ const path = pubkey
117
+ ? `/api/user/bap-ids?pubkey=${encodeURIComponent(pubkey)}`
118
+ : "/api/user/bap-ids";
119
+ const result = await requestJson(client, "GET", path, {
120
+ withCookies: !pubkey,
121
+ });
122
+ if (result.status >= 400) {
123
+ throwHttp(path, result.status, result.json, result.text, result.headers);
124
+ }
125
+ const bapIds = record(result.json).bapIds;
126
+ const list = Array.isArray(bapIds) ? bapIds : [];
127
+ const names = list
128
+ .map((row) => {
129
+ const item = record(row);
130
+ const id = String(item.identity_key ?? item.id ?? "");
131
+ const name = typeof item.name === "string" ? item.name : "";
132
+ const primary = item.is_primary === true ? " (primary)" : "";
133
+ return `${id}${name ? ` ${name}` : ""}${primary}`;
134
+ })
135
+ .join("\n");
136
+ return succeed(
137
+ cfg,
138
+ { pubkey: pubkey ?? null, count: list.length, bapIds: list },
139
+ names || "(none)"
140
+ );
141
+ }
142
+
143
+ export async function diagnoseLastOauth(
144
+ args: ParsedArgs,
145
+ cfg: RuntimeConfig
146
+ ): Promise<number> {
147
+ const pubkey = flag(args, "pubkey");
148
+ const clientId = flag(args, "client-id");
149
+ if (!pubkey || !clientId) {
150
+ usage("--pubkey and --client-id are required");
151
+ }
152
+ const client = createHttp(cfg);
153
+ const path = `/api/user/last-oauth-identity?pubkey=${encodeURIComponent(pubkey)}&clientId=${encodeURIComponent(clientId)}`;
154
+ const result = await requestJson(client, "GET", path);
155
+ if (result.status >= 400) {
156
+ throwHttp(path, result.status, result.json, result.text, result.headers);
157
+ }
158
+ const lastSelectedBapId = record(result.json).lastSelectedBapId ?? null;
159
+ return succeed(
160
+ cfg,
161
+ { pubkey, clientId, lastSelectedBapId },
162
+ typeof lastSelectedBapId === "string" ? lastSelectedBapId : "(none)"
163
+ );
164
+ }
165
+
166
+ export async function diagnoseClient(
167
+ args: ParsedArgs,
168
+ cfg: RuntimeConfig
169
+ ): Promise<number> {
170
+ const clientId = flag(args, "client-id");
171
+ if (!clientId) {
172
+ usage("--client-id is required");
173
+ }
174
+ const client = createHttp(cfg);
175
+ const path = `/api/oauth-clients?clientId=${encodeURIComponent(clientId)}`;
176
+ const result = await requestJson(client, "GET", path);
177
+ if (result.status >= 400) {
178
+ throwHttp(path, result.status, result.json, result.text, result.headers);
179
+ }
180
+ const payload = record(result.json);
181
+ const clients = Array.isArray(payload.clients) ? payload.clients : [];
182
+ const first = clients[0] ? record(clients[0]) : payload;
183
+ if (!first.clientId && !first.name) {
184
+ return succeed(cfg, { clientId, client: null }, "(not found)");
185
+ }
186
+ return succeed(
187
+ cfg,
188
+ { clientId, client: first },
189
+ `${String(first.clientId ?? clientId)} ${String(first.name ?? "")}`
190
+ );
191
+ }
package/src/identity.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HD, Mnemonic, PrivateKey, Utils } from "@bsv/sdk";
1
+ import { PrivateKey, Utils } from "@bsv/sdk";
2
2
  import {
3
3
  type BapMasterBackup,
4
4
  decryptBackup,
@@ -25,19 +25,12 @@ export type PublicIdentity = {
25
25
  };
26
26
 
27
27
  export function createMasterBackup(label: string): {
28
- mnemonic: string;
29
28
  backup: BapMasterBackup;
30
29
  bapId: string;
31
30
  pubkey: string;
32
31
  address: string;
33
32
  } {
34
- const mnemonic = Mnemonic.fromRandom();
35
- const hdKey = HD.fromSeed(mnemonic.toSeed());
36
- const rootKey = hdKey.derive("m/0'/0");
37
- const rootPk = rootKey.privKey?.toWif();
38
- if (!rootPk) {
39
- cryptoFail("Failed to derive wallet root private key");
40
- }
33
+ const rootPk = PrivateKey.fromRandom().toWif();
41
34
  const bap = new BAP({ rootPk });
42
35
  const first = bap.newId();
43
36
  const backup: BapMasterBackup = {
@@ -48,7 +41,6 @@ export function createMasterBackup(label: string): {
48
41
  };
49
42
  const member = first.getAccountKey();
50
43
  return {
51
- mnemonic: mnemonic.toString(),
52
44
  backup,
53
45
  bapId: first.bapId,
54
46
  pubkey: member.toPublicKey().toString(),
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { agentCommand } from "./agent.ts";
3
4
  import { boolFlag, parseArgs } from "./args.ts";
4
5
  import {
5
6
  authSignIn,
@@ -12,6 +13,12 @@ import {
12
13
  oauthRegister,
13
14
  } from "./commands.ts";
14
15
  import { loadConfig } from "./config.ts";
16
+ import {
17
+ diagnoseBap,
18
+ diagnoseClient,
19
+ diagnoseIdentities,
20
+ diagnoseLastOauth,
21
+ } from "./diagnose.ts";
15
22
  import { usage } from "./error.ts";
16
23
  import { reportError } from "./output.ts";
17
24
 
@@ -26,6 +33,7 @@ export async function run(argv: string[]): Promise<number> {
26
33
  return 0;
27
34
  }
28
35
  const [group, command] = args.positional;
36
+ if (group === "agent") return await agentCommand(args, cfg);
29
37
  if (group === "identity" && command === "create") {
30
38
  return await identityCreate(args, cfg);
31
39
  }
@@ -47,6 +55,18 @@ export async function run(argv: string[]): Promise<number> {
47
55
  if (group === "doctor") {
48
56
  return await doctor(args, cfg);
49
57
  }
58
+ if (group === "diagnose" && command === "bap") {
59
+ return await diagnoseBap(args, cfg);
60
+ }
61
+ if (group === "diagnose" && command === "identities") {
62
+ return await diagnoseIdentities(args, cfg);
63
+ }
64
+ if (group === "diagnose" && command === "last-oauth") {
65
+ return await diagnoseLastOauth(args, cfg);
66
+ }
67
+ if (group === "diagnose" && command === "client") {
68
+ return await diagnoseClient(args, cfg);
69
+ }
50
70
  if (!group) {
51
71
  process.stdout.write(HELP);
52
72
  return 0;