@h402/cli 0.1.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.
package/dist/ows.js ADDED
@@ -0,0 +1,61 @@
1
+ const EVM_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
2
+ const HEX_SIGNATURE_PATTERN = /^(0x)?[a-fA-F0-9]+$/;
3
+ let owsCorePromise;
4
+ async function loadOwsCore() {
5
+ try {
6
+ owsCorePromise ??= import("@open-wallet-standard/core");
7
+ return await owsCorePromise;
8
+ }
9
+ catch (error) {
10
+ owsCorePromise = undefined;
11
+ throw new Error(`OWS wallet native bindings are unavailable on this platform: ${error instanceof Error ? error.message : String(error)}. ` +
12
+ "Non-wallet commands such as --help/search/quote can still run. Wallet creation and payment signing require @open-wallet-standard/core native bindings on macOS/Linux glibc x64/arm64.");
13
+ }
14
+ }
15
+ export function getEvmAddress(wallet) {
16
+ const account = wallet.accounts.find((candidate) => candidate.chainId === "eip155:8453") ??
17
+ wallet.accounts.find((candidate) => candidate.chainId.startsWith("eip155:")) ??
18
+ wallet.accounts.find((candidate) => EVM_ADDRESS_PATTERN.test(candidate.address));
19
+ if (!account || !EVM_ADDRESS_PATTERN.test(account.address)) {
20
+ throw new Error("OWS wallet was created but no EVM address was returned");
21
+ }
22
+ return account.address.toLowerCase();
23
+ }
24
+ export async function createOwsWallet(name, passphrase) {
25
+ const { createWallet } = await loadOwsCore();
26
+ const wallet = createWallet(name, passphrase);
27
+ return { name, address: getEvmAddress(wallet), wallet };
28
+ }
29
+ export async function getOwsWallet(name) {
30
+ const { getWallet } = await loadOwsCore();
31
+ const wallet = getWallet(name);
32
+ return { name: wallet.name, address: getEvmAddress(wallet), wallet };
33
+ }
34
+ export async function listOwsWallets() {
35
+ const { listWallets } = await loadOwsCore();
36
+ return listWallets().map((wallet) => ({ name: wallet.name, address: getEvmAddress(wallet), wallet }));
37
+ }
38
+ export function normalizeOwsSignature(signature, recoveryId) {
39
+ const normalized = signature.startsWith("0x") ? signature : `0x${signature}`;
40
+ if (!HEX_SIGNATURE_PATTERN.test(normalized)) {
41
+ throw new Error("OWS signMessage returned a non-hex signature");
42
+ }
43
+ if (normalized.length === 132) {
44
+ return normalized;
45
+ }
46
+ if (normalized.length === 130 && recoveryId !== undefined) {
47
+ const v = recoveryId > 1 ? recoveryId : recoveryId + 27;
48
+ return `${normalized}${v.toString(16).padStart(2, "0")}`;
49
+ }
50
+ throw new Error("OWS signMessage returned an invalid EVM signature length");
51
+ }
52
+ export async function signOwsMessage(walletName, message, passphrase) {
53
+ const { signMessage } = await loadOwsCore();
54
+ const result = signMessage(walletName, "base", message, passphrase);
55
+ return normalizeOwsSignature(result.signature, result.recoveryId);
56
+ }
57
+ export async function signOwsTypedData(walletName, typedData, passphrase) {
58
+ const { signTypedData } = await loadOwsCore();
59
+ const result = signTypedData(walletName, "base", JSON.stringify(typedData), passphrase);
60
+ return normalizeOwsSignature(result.signature, result.recoveryId);
61
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,63 @@
1
+ async function promptHidden(question) {
2
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3
+ throw new Error("Passphrase prompt requires an interactive terminal. Pass --no-passphrase if the wallet was created without one (the default agent setup), or set H402_WALLET_PASSPHRASE.");
4
+ }
5
+ return new Promise((resolve, reject) => {
6
+ const stdin = process.stdin;
7
+ const stdout = process.stdout;
8
+ const wasRaw = stdin.isRaw;
9
+ const wasPaused = stdin.isPaused();
10
+ let value = "";
11
+ const cleanup = () => {
12
+ stdin.off("data", onData);
13
+ if (stdin.isTTY) {
14
+ stdin.setRawMode(wasRaw);
15
+ }
16
+ if (wasPaused) {
17
+ stdin.pause();
18
+ }
19
+ };
20
+ const finish = () => {
21
+ stdout.write("\n");
22
+ cleanup();
23
+ resolve(value);
24
+ };
25
+ const onData = (chunk) => {
26
+ for (const char of chunk.toString("utf8")) {
27
+ if (char === "\u0003") {
28
+ stdout.write("\n");
29
+ cleanup();
30
+ reject(new Error("Interrupted"));
31
+ return;
32
+ }
33
+ if (char === "\r" || char === "\n") {
34
+ finish();
35
+ return;
36
+ }
37
+ if (char === "\u007f" || char === "\b") {
38
+ value = value.slice(0, -1);
39
+ continue;
40
+ }
41
+ value += char;
42
+ }
43
+ };
44
+ stdout.write(question);
45
+ stdin.setRawMode(true);
46
+ stdin.resume();
47
+ stdin.on("data", onData);
48
+ });
49
+ }
50
+ export async function promptPassphrase(options) {
51
+ const passphrase = await promptHidden("Wallet passphrase: ");
52
+ if (!passphrase) {
53
+ throw new Error("Wallet passphrase cannot be empty.");
54
+ }
55
+ if (!options.confirm) {
56
+ return passphrase;
57
+ }
58
+ const confirmation = await promptHidden("Confirm wallet passphrase: ");
59
+ if (passphrase !== confirmation) {
60
+ throw new Error("Wallet passphrases do not match.");
61
+ }
62
+ return passphrase;
63
+ }
package/dist/utils.js ADDED
@@ -0,0 +1,189 @@
1
+ export function isRecord(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ export function mergeH402(body, patch) {
5
+ if (isRecord(body)) {
6
+ const h402 = isRecord(body.h402) ? body.h402 : {};
7
+ return { ...body, h402: { ...h402, ...patch } };
8
+ }
9
+ return { data: body, h402: patch };
10
+ }
11
+ export function parseArgs(argv) {
12
+ const positional = [];
13
+ const flags = {};
14
+ for (let index = 0; index < argv.length; index += 1) {
15
+ const value = argv[index];
16
+ if (!value.startsWith("--")) {
17
+ positional.push(value);
18
+ continue;
19
+ }
20
+ const rawName = value.slice(2);
21
+ const equalsIndex = rawName.indexOf("=");
22
+ if (equalsIndex >= 0) {
23
+ flags[rawName.slice(0, equalsIndex)] = rawName.slice(equalsIndex + 1);
24
+ continue;
25
+ }
26
+ const name = rawName;
27
+ const next = argv[index + 1];
28
+ if (!next || next.startsWith("--")) {
29
+ flags[name] = true;
30
+ continue;
31
+ }
32
+ flags[name] = next;
33
+ index += 1;
34
+ }
35
+ return { positional, flags };
36
+ }
37
+ export function flagString(flags, name, fallback) {
38
+ const value = flags[name];
39
+ if (typeof value === "string") {
40
+ return value;
41
+ }
42
+ return fallback;
43
+ }
44
+ export function flagBoolean(flags, name) {
45
+ return flags[name] === true || flags[name] === "true";
46
+ }
47
+ // Resolve the HTTP method for a proxy call. An explicit --method must be GET or POST
48
+ // (case-insensitive, normalized to upper); anything else is rejected here instead of
49
+ // being forwarded as an invalid method the backend answers with an opaque error.
50
+ // Without --method, default to POST when there is a request body, else GET.
51
+ export function resolveMethod(flags, hasBody) {
52
+ const raw = flagString(flags, "method");
53
+ if (raw === undefined) {
54
+ return hasBody ? "POST" : "GET";
55
+ }
56
+ const normalized = raw.toUpperCase();
57
+ if (normalized !== "GET" && normalized !== "POST") {
58
+ throw new Error(`Flag --method must be GET or POST (got "${raw}").`);
59
+ }
60
+ if (normalized === "GET" && hasBody) {
61
+ throw new Error("Flag --method GET cannot be combined with --json; GET requests must use --query for URL parameters.");
62
+ }
63
+ return normalized;
64
+ }
65
+ function flagValue(flags, name) {
66
+ const value = flags[name];
67
+ if (value === undefined) {
68
+ return undefined;
69
+ }
70
+ if (typeof value !== "string" || value === "") {
71
+ throw new Error(`Flag --${name} requires a value.`);
72
+ }
73
+ return value;
74
+ }
75
+ function jsonParseMessage(flag, value, example, error) {
76
+ const parserMessage = error instanceof Error ? error.message : String(error);
77
+ const keyValueHint = flag === "query" && /^[^=\s]+=/.test(value) ? " key=value syntax is not supported;" : "";
78
+ return `Flag --${flag} must be ${flag === "query" ? "a JSON object" : "valid JSON"}, e.g. --${flag} '${example}' (got ${JSON.stringify(value)};${keyValueHint} ${parserMessage}).`;
79
+ }
80
+ export function parseJsonFlag(flags) {
81
+ const value = flagValue(flags, "json");
82
+ if (value === undefined) {
83
+ return undefined;
84
+ }
85
+ try {
86
+ return JSON.parse(value);
87
+ }
88
+ catch (error) {
89
+ throw new Error(jsonParseMessage("json", value, '{"query":"Seoul"}', error));
90
+ }
91
+ }
92
+ export function parseQueryFlag(flags) {
93
+ const value = flagValue(flags, "query");
94
+ if (value === undefined) {
95
+ return undefined;
96
+ }
97
+ let parsed;
98
+ try {
99
+ parsed = JSON.parse(value);
100
+ }
101
+ catch (error) {
102
+ throw new Error(jsonParseMessage("query", value, '{"q":"Seoul"}', error));
103
+ }
104
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
105
+ throw new Error(`Flag --query must be a JSON object, e.g. --query '{"q":"Seoul"}' (got ${JSON.stringify(value)}).`);
106
+ }
107
+ return validateQueryParams(parsed);
108
+ }
109
+ function writeStream(stream, text) {
110
+ return new Promise((resolve, reject) => {
111
+ const onError = (error) => {
112
+ stream.off("drain", onDrain);
113
+ reject(error);
114
+ };
115
+ const onDrain = () => {
116
+ stream.off("error", onError);
117
+ resolve();
118
+ };
119
+ stream.once("error", onError);
120
+ if (stream.write(text)) {
121
+ stream.off("error", onError);
122
+ resolve();
123
+ return;
124
+ }
125
+ stream.once("drain", onDrain);
126
+ });
127
+ }
128
+ export function writeStdout(text) {
129
+ return writeStream(process.stdout, text);
130
+ }
131
+ export function writeStderr(text) {
132
+ return writeStream(process.stderr, text);
133
+ }
134
+ export function printJson(data) {
135
+ return writeStdout(`${JSON.stringify(data, null, 2)}\n`);
136
+ }
137
+ export function requireValue(value, message) {
138
+ if (value === undefined || value === null || value === "") {
139
+ throw new Error(message);
140
+ }
141
+ return value;
142
+ }
143
+ const PINNED_PATH_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
144
+ export function assertConcreteProvider(provider) {
145
+ if (typeof provider !== "string" || !provider) {
146
+ throw new Error("Provider is required for a pinned route path");
147
+ }
148
+ if (provider.toLowerCase() === "auto") {
149
+ throw new Error('Provider "auto" is reserved for the retired routing endpoint; select a concrete provider.');
150
+ }
151
+ if (!PINNED_PATH_SLUG.test(provider)) {
152
+ throw new Error(`Provider must be a lowercase slug using letters, numbers, and single hyphens (got ${JSON.stringify(provider)}).`);
153
+ }
154
+ return provider;
155
+ }
156
+ export function encodeRouteId(routeId) {
157
+ const parts = routeId.split("/");
158
+ if (parts.length !== 2 || parts.some((part) => !part)) {
159
+ throw new Error("Route id must look like category/action");
160
+ }
161
+ for (const part of parts) {
162
+ if (!PINNED_PATH_SLUG.test(part)) {
163
+ throw new Error(`Route id segment must be a lowercase slug using letters, numbers, and single hyphens (got ${JSON.stringify(part)}).`);
164
+ }
165
+ }
166
+ return parts.map(encodeURIComponent).join("/");
167
+ }
168
+ function validateQueryParams(query) {
169
+ for (const [key, value] of Object.entries(query)) {
170
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
171
+ throw new Error(`--query value for "${key}" must be a string, number, or boolean; arrays, objects, and null are not supported. Use --json for structured request bodies.`);
172
+ }
173
+ }
174
+ return query;
175
+ }
176
+ // Every execution path is provider-pinned. Callers must resolve an explicit
177
+ // provider or the catalog's current display default before building this path.
178
+ export function buildProxyPath(routeId, provider, query) {
179
+ const path = `/routes/${encodeURIComponent(assertConcreteProvider(provider))}/${encodeRouteId(routeId)}`;
180
+ if (!query) {
181
+ return path;
182
+ }
183
+ const searchParams = new URLSearchParams();
184
+ for (const [key, value] of Object.entries(validateQueryParams(query))) {
185
+ searchParams.set(key, String(value));
186
+ }
187
+ const queryString = searchParams.toString();
188
+ return queryString ? `${path}?${queryString}` : path;
189
+ }
package/dist/x402.js ADDED
@@ -0,0 +1,53 @@
1
+ import { BASE_CHAIN_ID, BASE_USDC_ADDRESS, USDC_EIP712_NAME, USDC_EIP712_VERSION, X402_VERSION, buildTransferAuthorization, encodeX402Header, selectExactRequirement, transferWithAuthorizationTypes } from "@h402/core";
2
+ import { signOwsTypedData } from "./ows.js";
3
+ export { X402_HEADERS, paymentRequiredFromResponse } from "@h402/core";
4
+ // The CLI can only sign EIP-3009 Base USDC `exact` payments, so it must refuse to
5
+ // sign anything else a backend offers — a non-USDC asset or a non-EIP-3009
6
+ // transfer method (native, permit2, ...) would move funds in a way the user never
7
+ // agreed to. The asset matcher is defensive: a malformed (non-string) asset is a
8
+ // clean non-match, not a thrown error that would abort scanning valid entries.
9
+ export const BASE_USDC_REQUIREMENT_OPTIONS = {
10
+ matchAsset: (asset) => typeof asset === "string" && asset.toLowerCase() === BASE_USDC_ADDRESS,
11
+ requireEip3009: true
12
+ };
13
+ export function selectBaseUsdcRequirement(paymentRequired) {
14
+ return selectExactRequirement(paymentRequired, BASE_USDC_REQUIREMENT_OPTIONS);
15
+ }
16
+ export async function createPaymentSignatureHeader(input) {
17
+ const accepted = selectBaseUsdcRequirement(input.paymentRequired);
18
+ const authorization = buildTransferAuthorization({
19
+ from: input.walletAddress,
20
+ to: accepted.payTo,
21
+ amount: accepted.amount,
22
+ maxTimeoutSeconds: accepted.maxTimeoutSeconds,
23
+ now: input.authorizationNow
24
+ });
25
+ const typedData = {
26
+ types: {
27
+ EIP712Domain: [
28
+ { name: "name", type: "string" },
29
+ { name: "version", type: "string" },
30
+ { name: "chainId", type: "uint256" },
31
+ { name: "verifyingContract", type: "address" }
32
+ ],
33
+ ...transferWithAuthorizationTypes
34
+ },
35
+ primaryType: "TransferWithAuthorization",
36
+ domain: {
37
+ name: USDC_EIP712_NAME,
38
+ version: USDC_EIP712_VERSION,
39
+ chainId: BASE_CHAIN_ID,
40
+ verifyingContract: accepted.asset
41
+ },
42
+ message: authorization
43
+ };
44
+ const signature = await signOwsTypedData(input.walletName, typedData, input.passphrase);
45
+ return encodeX402Header({
46
+ x402Version: X402_VERSION,
47
+ accepted,
48
+ payload: {
49
+ authorization,
50
+ signature
51
+ }
52
+ });
53
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@h402/cli",
3
+ "version": "0.1.0",
4
+ "description": "Local, non-custodial CLI for h402 — browse the catalog and pay per call in Base USDC over x402 from a local wallet.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "h402": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Steemhunt/h402-cli.git",
17
+ "directory": "packages/cli"
18
+ },
19
+ "homepage": "https://h402.hunt.town",
20
+ "bugs": {
21
+ "url": "https://github.com/Steemhunt/h402-cli/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "keywords": [
27
+ "x402",
28
+ "h402",
29
+ "ai-agents",
30
+ "agent",
31
+ "payments",
32
+ "usdc",
33
+ "base",
34
+ "cli"
35
+ ],
36
+ "engines": {
37
+ "node": ">=22"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc",
41
+ "prepack": "npm run build -w @h402/core && npm run build",
42
+ "dev": "tsx src/index.ts",
43
+ "lint": "eslint .",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run"
46
+ },
47
+ "dependencies": {
48
+ "@h402/core": "^0.1.0",
49
+ "@open-wallet-standard/core": "^1.3.2",
50
+ "fs-native-extensions": "^1.5.0",
51
+ "undici": "^7.29.0"
52
+ }
53
+ }