@aixyz/cli 0.6.0 → 0.8.0

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.
@@ -0,0 +1,94 @@
1
+ import { createPublicClient, http, type Chain, type Log } from "viem";
2
+ import { startSpinner } from "./spinner";
3
+ import { getExplorerUrl } from "./chain";
4
+ import type { SignTransactionResult } from "../wallet/sign";
5
+ import chalk from "chalk";
6
+ import boxen from "boxen";
7
+
8
+ export function label(text: string): string {
9
+ return chalk.dim(text.padEnd(14));
10
+ }
11
+
12
+ export function truncateUri(uri: string, maxLength = 80): string {
13
+ if (uri.length <= maxLength) return uri;
14
+ return uri.slice(0, maxLength) + "...";
15
+ }
16
+
17
+ export interface BroadcastAndConfirmParams {
18
+ result: SignTransactionResult;
19
+ chain: Chain;
20
+ rpcUrl?: string;
21
+ }
22
+
23
+ export interface BroadcastAndConfirmResult {
24
+ hash: `0x${string}`;
25
+ receipt: {
26
+ blockNumber: bigint;
27
+ gasUsed: bigint;
28
+ effectiveGasPrice: bigint;
29
+ logs: Log[];
30
+ };
31
+ timestamp: bigint;
32
+ }
33
+
34
+ export async function broadcastAndConfirm({
35
+ result,
36
+ chain,
37
+ rpcUrl,
38
+ }: BroadcastAndConfirmParams): Promise<BroadcastAndConfirmResult> {
39
+ const transport = rpcUrl ? http(rpcUrl) : http();
40
+ const publicClient = createPublicClient({ chain, transport });
41
+
42
+ let hash: `0x${string}`;
43
+ if (result.kind === "sent") {
44
+ hash = result.txHash;
45
+ } else {
46
+ const broadcastSpinner = startSpinner("Broadcasting transaction...");
47
+ try {
48
+ hash = await publicClient.sendRawTransaction({ serializedTransaction: result.raw });
49
+ } catch (err) {
50
+ broadcastSpinner.stop();
51
+ throw err;
52
+ }
53
+ broadcastSpinner.stop(`${chalk.green("\u2713")} Transaction broadcast`);
54
+ }
55
+
56
+ printTxHashBox(chain, hash);
57
+
58
+ const confirmSpinner = startSpinner("Waiting for confirmation...");
59
+ let receipt;
60
+ try {
61
+ receipt = await publicClient.waitForTransactionReceipt({ hash });
62
+ } catch (err) {
63
+ confirmSpinner.stop();
64
+ throw err;
65
+ }
66
+ confirmSpinner.stop(`${chalk.green("\u2713")} Confirmed in block ${chalk.bold(receipt.blockNumber.toString())}`);
67
+
68
+ const block = await publicClient.getBlock({ blockNumber: receipt.blockNumber });
69
+
70
+ return { hash, receipt, timestamp: block.timestamp };
71
+ }
72
+
73
+ function printTxHashBox(chain: Chain, hash: `0x${string}`): void {
74
+ const lines = [`${label("Tx Hash")}${hash}`];
75
+ const explorerUrl = getExplorerUrl(chain, hash);
76
+ if (explorerUrl) {
77
+ lines.push(`${label("Explorer")}${chalk.cyan(explorerUrl)}`);
78
+ }
79
+ console.log(
80
+ boxen(lines.join("\n"), {
81
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
82
+ borderStyle: "round",
83
+ borderColor: "cyan",
84
+ }),
85
+ );
86
+ }
87
+
88
+ export function logSignResult(walletType: string, result: SignTransactionResult): void {
89
+ if (result.kind === "signed") {
90
+ console.log(`${chalk.green("\u2713")} Transaction signed ${chalk.dim(`(${walletType} \u00b7 ${result.address})`)}`);
91
+ } else {
92
+ console.log(`${chalk.green("\u2713")} Transaction signed ${chalk.dim(`(${walletType})`)}`);
93
+ }
94
+ }
@@ -0,0 +1,154 @@
1
+ import { describe, expect, test, afterAll, beforeAll } from "bun:test";
2
+ import { rmSync } from "fs";
3
+ import { mkdir } from "node:fs/promises";
4
+ import { validatePrivateKey, CliError, resolveUri } from "./utils";
5
+ import { join } from "path";
6
+
7
+ describe("validatePrivateKey", () => {
8
+ test("accepts valid 64-char hex key with 0x prefix", () => {
9
+ const key = "0x0000000000000000000000000000000000000000000000000000000000000001";
10
+ const result = validatePrivateKey(key);
11
+ expect(result).toStrictEqual(key);
12
+ });
13
+
14
+ test("accepts valid 64-char hex key without 0x prefix", () => {
15
+ const key = "0000000000000000000000000000000000000000000000000000000000000001";
16
+ const result = validatePrivateKey(key);
17
+ expect(result).toStrictEqual(`0x${key}`);
18
+ });
19
+
20
+ test("accepts mixed case hex characters", () => {
21
+ const key = "0xaAbBcCdDeEfF0000000000000000000000000000000000000000000000000001";
22
+ const result = validatePrivateKey(key);
23
+ expect(result).toStrictEqual(key);
24
+ });
25
+
26
+ test("rejects key that is too short", () => {
27
+ const key = "0x1234";
28
+ expect(() => validatePrivateKey(key)).toThrow(CliError);
29
+ expect(() => validatePrivateKey(key)).toThrow("Invalid private key format");
30
+ });
31
+
32
+ test("rejects key that is too long", () => {
33
+ const key = "0x00000000000000000000000000000000000000000000000000000000000000001";
34
+ expect(() => validatePrivateKey(key)).toThrow(CliError);
35
+ });
36
+
37
+ test("rejects key with invalid characters", () => {
38
+ const key = "0xGGGG000000000000000000000000000000000000000000000000000000000001";
39
+ expect(() => validatePrivateKey(key)).toThrow(CliError);
40
+ });
41
+
42
+ test("rejects empty string", () => {
43
+ expect(() => validatePrivateKey("")).toThrow(CliError);
44
+ });
45
+
46
+ test("rejects random string", () => {
47
+ expect(() => validatePrivateKey("not-a-key")).toThrow(CliError);
48
+ });
49
+ });
50
+
51
+ describe("CliError", () => {
52
+ test("is an instance of Error", () => {
53
+ const error = new CliError("test message");
54
+ expect(error).toBeInstanceOf(Error);
55
+ });
56
+
57
+ test("has correct name property", () => {
58
+ const error = new CliError("test message");
59
+ expect(error.name).toStrictEqual("CliError");
60
+ });
61
+
62
+ test("has correct message property", () => {
63
+ const error = new CliError("test message");
64
+ expect(error.message).toStrictEqual("test message");
65
+ });
66
+
67
+ test("can be caught as Error", () => {
68
+ let caught = false;
69
+ try {
70
+ throw new CliError("test");
71
+ } catch (e) {
72
+ if (e instanceof Error) {
73
+ caught = true;
74
+ }
75
+ }
76
+ expect(caught).toStrictEqual(true);
77
+ });
78
+ });
79
+
80
+ describe("resolveUri", () => {
81
+ const testDir = join(import.meta.dir, "__test_fixtures__");
82
+ const testJsonPath = join(testDir, "test-metadata.json");
83
+ const testMetadata = { name: "Test Agent", description: "A test agent" };
84
+
85
+ beforeAll(() => {
86
+ // ensure test directory exists
87
+ mkdir(testDir, { recursive: true });
88
+ });
89
+
90
+ afterAll(() => {
91
+ // clean up test directory
92
+ rmSync(testDir, { recursive: true, force: true });
93
+ });
94
+
95
+ test("returns ipfs:// URIs unchanged", () => {
96
+ const uri = "ipfs://QmTest123";
97
+ expect(resolveUri(uri)).toStrictEqual(uri);
98
+ });
99
+
100
+ test("returns https:// URIs unchanged", () => {
101
+ const uri = "https://example.com/metadata.json";
102
+ expect(resolveUri(uri)).toStrictEqual(uri);
103
+ });
104
+
105
+ test("returns http:// URIs unchanged", () => {
106
+ const uri = "http://example.com/metadata.json";
107
+ expect(resolveUri(uri)).toStrictEqual(uri);
108
+ });
109
+
110
+ test("converts .json file to base64 data URI", async () => {
111
+ await Bun.write(testJsonPath, JSON.stringify(testMetadata));
112
+
113
+ try {
114
+ const result = resolveUri(testJsonPath);
115
+ expect(result.startsWith("data:application/json;base64,")).toStrictEqual(true);
116
+
117
+ // Decode and verify content
118
+ const base64 = result.replace("data:application/json;base64,", "");
119
+ const decoded = JSON.parse(Buffer.from(base64, "base64").toString("utf-8"));
120
+ expect(decoded).toStrictEqual(testMetadata);
121
+ } finally {
122
+ await Bun.file(testJsonPath).unlink();
123
+ }
124
+ });
125
+
126
+ test("throws for directory path", async () => {
127
+ await mkdir(testDir, { recursive: true });
128
+ const dirWithJsonSuffix = join(testDir, "not-a-file.json");
129
+ await mkdir(dirWithJsonSuffix, { recursive: true });
130
+
131
+ try {
132
+ expect(() => resolveUri(dirWithJsonSuffix)).toThrow(CliError);
133
+ expect(() => resolveUri(dirWithJsonSuffix)).toThrow("Not a file");
134
+ } finally {
135
+ rmSync(dirWithJsonSuffix, { recursive: true, force: true });
136
+ }
137
+ });
138
+
139
+ test("throws for non-existent .json file", () => {
140
+ expect(() => resolveUri("./non-existent.json")).toThrow(CliError);
141
+ expect(() => resolveUri("./non-existent.json")).toThrow("File not found");
142
+ });
143
+
144
+ test("throws for invalid JSON content", async () => {
145
+ await Bun.write(testJsonPath, "not valid json {{{");
146
+
147
+ try {
148
+ expect(() => resolveUri(testJsonPath)).toThrow(CliError);
149
+ expect(() => resolveUri(testJsonPath)).toThrow("Invalid JSON");
150
+ } finally {
151
+ await Bun.file(testJsonPath).unlink();
152
+ }
153
+ });
154
+ });
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export function resolveUri(uri: string): string {
5
+ // Return as-is for URLs (ipfs://, https://, data:, etc.)
6
+ if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("ipfs://") || uri.startsWith("data:")) {
7
+ return uri;
8
+ }
9
+
10
+ // Check if it's a file path (ends with .json or exists as a file)
11
+ if (uri.endsWith(".json") || existsSync(uri)) {
12
+ const filePath = resolve(uri);
13
+
14
+ if (!existsSync(filePath)) {
15
+ throw new CliError(`File not found: ${filePath}`);
16
+ }
17
+
18
+ if (!statSync(filePath).isFile()) {
19
+ throw new CliError(`Not a file: ${filePath}`);
20
+ }
21
+
22
+ const content = readFileSync(filePath, "utf-8");
23
+
24
+ // Validate it's valid JSON
25
+ try {
26
+ JSON.parse(content);
27
+ } catch {
28
+ throw new CliError(`Invalid JSON in file: ${filePath}`);
29
+ }
30
+
31
+ // Convert to base64 data URI
32
+ const base64 = Buffer.from(content).toString("base64");
33
+ return `data:application/json;base64,${base64}`;
34
+ }
35
+
36
+ // Return as-is for other URIs
37
+ return uri;
38
+ }
39
+
40
+ export function validatePrivateKey(key: string): `0x${string}` {
41
+ const normalizedKey = key.startsWith("0x") ? key : `0x${key}`;
42
+
43
+ if (!/^0x[0-9a-fA-F]{64}$/.test(normalizedKey)) {
44
+ throw new CliError("Invalid private key format. Expected 64 hex characters (with or without 0x prefix).");
45
+ }
46
+
47
+ return normalizedKey as `0x${string}`;
48
+ }
49
+
50
+ export class CliError extends Error {
51
+ constructor(message: string) {
52
+ super(message);
53
+ this.name = "CliError";
54
+ }
55
+ }
@@ -0,0 +1,106 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { escapeHtml, safeJsonEmbed, buildHtml } from "./browser";
3
+
4
+ describe("escapeHtml", () => {
5
+ test("escapes ampersand", () => {
6
+ expect(escapeHtml("a&b")).toBe("a&amp;b");
7
+ });
8
+
9
+ test("escapes less-than", () => {
10
+ expect(escapeHtml("a<b")).toBe("a&lt;b");
11
+ });
12
+
13
+ test("escapes greater-than", () => {
14
+ expect(escapeHtml("a>b")).toBe("a&gt;b");
15
+ });
16
+
17
+ test("escapes double quote", () => {
18
+ expect(escapeHtml('a"b')).toBe("a&quot;b");
19
+ });
20
+
21
+ test("escapes single quote", () => {
22
+ expect(escapeHtml("a'b")).toBe("a&#039;b");
23
+ });
24
+
25
+ test("escapes combined XSS string", () => {
26
+ expect(escapeHtml('<script>alert("xss")</script>')).toBe("&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;");
27
+ });
28
+
29
+ test("returns empty string unchanged", () => {
30
+ expect(escapeHtml("")).toBe("");
31
+ });
32
+
33
+ test("returns plain string unchanged", () => {
34
+ expect(escapeHtml("hello world")).toBe("hello world");
35
+ });
36
+ });
37
+
38
+ describe("safeJsonEmbed", () => {
39
+ test("escapes </script> in strings", () => {
40
+ const result = safeJsonEmbed("</script>");
41
+ expect(result).not.toContain("<");
42
+ expect(result).toContain("\\u003c");
43
+ });
44
+
45
+ test("encodes a simple string", () => {
46
+ expect(safeJsonEmbed("hello")).toBe('"hello"');
47
+ });
48
+
49
+ test("encodes an object", () => {
50
+ const result = safeJsonEmbed({ key: "value" });
51
+ expect(JSON.parse(result)).toEqual({ key: "value" });
52
+ });
53
+
54
+ test("escapes strings containing <", () => {
55
+ const result = safeJsonEmbed("a < b");
56
+ expect(result).not.toContain("<");
57
+ expect(result).toContain("\\u003c");
58
+ });
59
+ });
60
+
61
+ describe("buildHtml", () => {
62
+ const baseParams = {
63
+ registryAddress: "0x1234567890abcdef1234567890abcdef12345678",
64
+ calldata: "0xdeadbeef",
65
+ chainId: 11155111,
66
+ chainName: "sepolia",
67
+ nonce: "test-nonce-123",
68
+ };
69
+
70
+ test("returns valid HTML document", () => {
71
+ const html = buildHtml(baseParams);
72
+ expect(html).toStartWith("<!DOCTYPE html>");
73
+ expect(html).toContain("<html");
74
+ expect(html).toContain("</html>");
75
+ });
76
+
77
+ test("embeds chain name and ID", () => {
78
+ const html = buildHtml(baseParams);
79
+ expect(html).toContain("sepolia");
80
+ expect(html).toContain("11155111");
81
+ });
82
+
83
+ test("embeds registry address", () => {
84
+ const html = buildHtml(baseParams);
85
+ expect(html).toContain(baseParams.registryAddress);
86
+ });
87
+
88
+ test("embeds nonce in result endpoint", () => {
89
+ const html = buildHtml(baseParams);
90
+ expect(html).toContain("test-nonce-123");
91
+ });
92
+
93
+ test("displays URI when provided", () => {
94
+ const html = buildHtml({ ...baseParams, uri: "https://example.com/agent" });
95
+ expect(html).toContain("https://example.com/agent");
96
+ });
97
+
98
+ test("escapes user-provided values in HTML", () => {
99
+ const html = buildHtml({
100
+ ...baseParams,
101
+ chainName: '<script>alert("xss")</script>',
102
+ });
103
+ expect(html).not.toContain('<script>alert("xss")</script>');
104
+ expect(html).toContain("&lt;script&gt;");
105
+ });
106
+ });