@sigma-auth/cli 0.0.3 → 0.0.5

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,7 +28,7 @@ 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 |
@@ -54,3 +56,38 @@ bunx @sigma-auth/cli diagnose last-oauth --pubkey 03a42932… --client-id dropli
54
56
  `diagnose bap` treats a profile 404 as a successful diagnosis (`found: false`), not a command failure.
55
57
 
56
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 list_my_identities --capability list_authorized_apps --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
+ printf '{}\n' > arguments.json
70
+ sigma agent execute --agent-id AGENT_ID --capability list_my_identities --args-file arguments.json --json
71
+ sigma agent disconnect --agent-id AGENT_ID --json
72
+ ```
73
+
74
+ Use capability names from discovery and a JSON object in `arguments.json`.
75
+ Repeat `status` after its `nextPollAt` (Unix milliseconds). It makes at most one
76
+ status request, preserves server throttling across restarts, and reports terminal
77
+ approval states. Reuse the agent ID instead of repeating registration. After a
78
+ failed connect request, do not blindly reconnect: the registration may have
79
+ reached the provider. Retain `SIGMA_HOME` for diagnosis.
80
+
81
+ This uses Better Auth Agent Auth via `@auth/agent` 0.6.2, with delegated human
82
+ approval. It is not WorkOS auth.md, RFC 8628 device authorization, or a BRC100
83
+ wallet signer. The CLI never submits human approval or uses browser session
84
+ cookies. Execution requires an active capability explicitly requested at connect.
85
+ Agent and host keys live separately under `SIGMA_HOME/agent-auth`, with private
86
+ atomic files (0600) and directory (0700). Revocation failures retain credentials
87
+ for a retry. Keep this directory private and use one CLI process per SIGMA_HOME
88
+ at a time; the SDK's index updates are not a multi-process transaction.
89
+
90
+ `--base-url` or `SIGMA_AUTH_URL` selects the provider. HTTPS is required except
91
+ explicit localhost HTTP testing; discovery and execution URLs must share the
92
+ provider origin. Redirects are rejected. Approval URL query strings and complete
93
+ claim URLs are omitted from output because they may contain bearer artifacts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigma-auth/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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": {
@@ -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
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,
@@ -11,13 +12,13 @@ import {
11
12
  identityInfo,
12
13
  oauthRegister,
13
14
  } from "./commands.ts";
15
+ import { loadConfig } from "./config.ts";
14
16
  import {
15
17
  diagnoseBap,
16
18
  diagnoseClient,
17
19
  diagnoseIdentities,
18
20
  diagnoseLastOauth,
19
21
  } from "./diagnose.ts";
20
- import { loadConfig } from "./config.ts";
21
22
  import { usage } from "./error.ts";
22
23
  import { reportError } from "./output.ts";
23
24
 
@@ -32,6 +33,7 @@ export async function run(argv: string[]): Promise<number> {
32
33
  return 0;
33
34
  }
34
35
  const [group, command] = args.positional;
36
+ if (group === "agent") return await agentCommand(args, cfg);
35
37
  if (group === "identity" && command === "create") {
36
38
  return await identityCreate(args, cfg);
37
39
  }