@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 +38 -1
- package/package.json +2 -1
- package/src/agent-store.ts +57 -0
- package/src/agent.ts +366 -0
- package/src/args.ts +4 -3
- package/src/commands.ts +64 -45
- package/src/identity.ts +2 -10
- package/src/index.ts +3 -1
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` |
|
|
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
|
+
"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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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?: {
|
|
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(
|
|
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(
|
|
557
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
}
|