From 9eac37452f5997ff29cfababb38f57e946c71fa4 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 4 Mar 2026 15:44:41 +0100 Subject: [PATCH 01/49] sanitize user data and improve message handling in sendDirectMessage function --- scripts/linkHumanToAgent.js | 4 ++- scripts/package-lock.json | 13 +++++++++ scripts/package.json | 1 + scripts/shared/utils.js | 56 +++++++++++++++++++++++++++++++++++-- scripts/signChallenge.js | 2 +- 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/scripts/linkHumanToAgent.js b/scripts/linkHumanToAgent.js index 26962991d..dc7b7b845 100644 --- a/scripts/linkHumanToAgent.js +++ b/scripts/linkHumanToAgent.js @@ -123,7 +123,9 @@ async function main() { const challenge = JSON.parse(args.challenge); const url = await createPairing(challenge, args.did); - sendDirectMessage(args.to, urlFormating(verificationMessage, url)); + sendDirectMessage(args.to, url, (msg) => + urlFormating(verificationMessage, msg), + ); outputSuccess({ success: true }); } catch (error) { diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 352324a96..686547baa 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -13,6 +13,7 @@ "@iden3/js-iden3-auth": "^1.14.0", "@iden3/js-iden3-core": "^1.4.1", "ethers": "^6.13.4", + "shell-quote": "^1.8.3", "uuid": "^11.0.3" } }, @@ -2683,6 +2684,18 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/snarkjs": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/snarkjs/-/snarkjs-0.7.5.tgz", diff --git a/scripts/package.json b/scripts/package.json index 2b61d4678..0891ebb12 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -17,6 +17,7 @@ "@iden3/js-iden3-auth": "^1.14.0", "@iden3/js-iden3-core": "^1.4.1", "ethers": "^6.13.4", + "shell-quote": "^1.8.3", "uuid": "^11.0.3" } } \ No newline at end of file diff --git a/scripts/shared/utils.js b/scripts/shared/utils.js index 444e461bc..e2c2daa5f 100644 --- a/scripts/shared/utils.js +++ b/scripts/shared/utils.js @@ -1,6 +1,8 @@ const { bytesToHex, keyPath } = require("@0xpolygonid/js-sdk"); const { DID, Id } = require("@iden3/js-iden3-core"); const { v7: uuid } = require("uuid"); +const { parse } = require("shell-quote"); +const { execFileSync } = require("child_process"); /** * Removes the "0x" prefix from a hexadecimal string if it exists @@ -113,9 +115,57 @@ function codeFormating(data) { return `\\\`\\\`\\\`${data}\\\`\\\`\\\``; } -function sendDirectMessage(target, message) { - const { execSync } = require("child_process"); - execSync(`openclaw message send --target ${target} --message "${message}"`); +function assertNoShellOperators(field, val) { + const tokens = parse(val); + const hasShellOperator = tokens.some( + (t) => typeof t === "object" && t.op !== undefined, + ); + if (hasShellOperator) { + throw new Error( + `"${field}" contains shell operators and was rejected: "${val}".`, + ); + } +} + +function sanitizeMessage(value) { + if (typeof value !== "string" || value.trim() === "") { + throw new Error("Message must be a non-empty string."); + } + assertNoShellOperators("message", value); + return value; +} + +function validateTarget(value) { + if (typeof value !== "string" || value.trim() === "") { + throw new Error("Recipient (--to) must be a non-empty string."); + } + + const SAFE_TARGET = /^[A-Za-z0-9:._@\-\/]+$/; + if (!SAFE_TARGET.test(value)) { + throw new Error( + `Recipient (--to) contains invalid characters: "${value}". ` + + "Only alphanumeric characters, colon, hyphen, underscore, dot, @, and slash are allowed.", + ); + } + assertNoShellOperators("Recipient (--to)", value); +} + +function sendDirectMessage(target, message, formatterFn) { + validateTarget(target); + let safeMessage = sanitizeMessage(message); + + if (formatterFn) { + safeMessage = formatterFn(safeMessage); + } + + execFileSync("openclaw", [ + "message", + "send", + "--target", + target, + "--message", + safeMessage, + ]); } module.exports = { diff --git a/scripts/signChallenge.js b/scripts/signChallenge.js index a58da1509..e9c9de259 100644 --- a/scripts/signChallenge.js +++ b/scripts/signChallenge.js @@ -79,7 +79,7 @@ async function main() { const challenge = JSON.parse(args.challenge); const tokenString = await signChallenge(challenge, entry, kms); - sendDirectMessage(args.to, codeFormating(tokenString)); + sendDirectMessage(args.to, tokenString, codeFormating); outputSuccess({ success: true }); } catch (error) { From c1fe2727cd73e527d1edd40dd8cb7f30cb5a3ecd Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 4 Mar 2026 15:46:56 +0100 Subject: [PATCH 02/49] update error message --- scripts/shared/utils.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/shared/utils.js b/scripts/shared/utils.js index e2c2daa5f..bf4d2928d 100644 --- a/scripts/shared/utils.js +++ b/scripts/shared/utils.js @@ -142,12 +142,9 @@ function validateTarget(value) { const SAFE_TARGET = /^[A-Za-z0-9:._@\-\/]+$/; if (!SAFE_TARGET.test(value)) { - throw new Error( - `Recipient (--to) contains invalid characters: "${value}". ` + - "Only alphanumeric characters, colon, hyphen, underscore, dot, @, and slash are allowed.", - ); + throw new Error(`Only DID format is allowed for recipient.`); } - assertNoShellOperators("Recipient (--to)", value); + assertNoShellOperators("to", value); } function sendDirectMessage(target, message, formatterFn) { From 78bfde731449b44797a1aa4414e83337985ee314 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 4 Mar 2026 17:55:29 +0100 Subject: [PATCH 03/49] add more info about arch --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/README.md b/README.md index adb22f597..518dfcc0b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,58 @@ This skill enables AI agents to create, manage, link, prove and verify ownership - **Proof Generation**: Generate cryptographic proofs to authenticate as a specific identity - **Proof Verification**: Verify proofs to confirm identity ownership +## Architecture + +### Runtime Requirements + +- **Node.js `>= v20`** and **npm** are required to run the scripts. +- The **`openclaw` CLI** must be installed and available in `PATH`. It is a hard runtime dependency used exclusively for sending direct messages to other agents or users on the Billions Network. + +### Dependency Surface + +npm dependencies are intentionally minimal and scoped to well-established, audited packages: + +| Package | Purpose | +| ---------------------- | ------------------------------------------------------------ | +| `@0xpolygonid/js-sdk` | iden3/Polygon ID cryptographic primitives and key management | +| `@iden3/js-iden3-core` | DID and identity core types | +| `@iden3/js-iden3-auth` | JWS/JWA authorization response construction and verification | +| `ethers` | Ethereum key utilities | +| `shell-quote` | Shell token parsing used **only** for input sanitization | +| `uuid` | UUID generation for protocol message IDs | + +### Key Storage and Isolation + +All cryptographic material is persisted to `$HOME/.openclaw/billions/` — a directory that lives **outside the agent's workspace**: + +| File | Contents | +| ------------------ | ----------------------------------------------- | +| `kms.json` | Private keys (unencrypted, owner-readable only) | +| `identities.json` | Identity metadata | +| `defaultDid.json` | Active DID and associated public key | +| `challenges.json` | Per-DID challenge history | +| `credentials.json` | Verifiable credentials | + +Because this path is outside the agent workspace, the agent runtime has no ambient read or write access to these files. Access is restricted to the local OS user who installed the skill. File-based plaintext storage is safe in this context: no external party and no agent process can reach the keys. + +### Subprocess Execution Safety + +**Only one specific command is ever executed: `openclaw message send`**, with a fixed, hardcoded argument structure. The binary name, the subcommand, and all flag names (`--target`, `--message`) are hardcoded. User-supplied values are only ever passed as the **values** of those flags, never as the command name, subcommand, or flag names. Nothing else can be executed. There is no mechanism to change the binary, add flags, or inject subcommands. Additional security properties: + +1. **No shell interpolation**: `execFileSync(binary, argsArray)` bypasses the OS shell entirely. The OS `exec*` syscall receives the argument vector directly — shell metacharacters in argument values are treated as literal data. +2. **Argument-level input validation**: Before the call, all user-supplied values pass through two independent validation layers: + - `validateTarget` — enforces a strict allowlist regex (`/^[A-Za-z0-9:._@\-\/]+$/`) on the `--target` value. + - `assertNoShellOperators` — uses `shell-quote` to tokenize the input and rejects any token with an `op` property (i.e., `|`, `&`, `;`, `>`, `<`, `$()`, etc.). +3. **No dynamic code execution**: No `eval`, `new Function`, `child_process.exec`, or shell-interpolated `execSync` calls exist anywhere in the codebase. + +Prompt injection and arbitrary code execution are structurally impossible: the executed command and its flags are hardcoded constants, and user data can only influence the string values passed to `--target` and `--message` after sanitization. + +### Network and External Binary Policy + +- This skill makes no external calls of any kind. +- No external binary other than `openclaw` is invoked. +- Any external URLs or verification links produced by the scripts are delivered to the user as a plain text message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. + ## Documentation See [SKILL.md](SKILL.md) for detailed usage instructions and examples. From 48c23d3e20b0e893d195a77279e4d75c05fcf8a2 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 4 Mar 2026 18:37:45 +0100 Subject: [PATCH 04/49] update --- README.md | 2 +- SKILL.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 518dfcc0b..26fb4e368 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Prompt injection and arbitrary code execution are structurally impossible: the e ### Network and External Binary Policy -- This skill makes no external calls of any kind. +- All external http calls will be made to trusted resources. - No external binary other than `openclaw` is invoked. - Any external URLs or verification links produced by the scripts are delivered to the user as a plain text message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. diff --git a/SKILL.md b/SKILL.md index e23e68961..152f67fdd 100644 --- a/SKILL.md +++ b/SKILL.md @@ -2,6 +2,7 @@ name: verified-agent-identity description: Billions/Iden3 authentication and identity management tools for agents. Link, proof, sign, and verify. metadata: { "category": "identity" } +homepage: https://billions.network/ --- ## When to use this Skill From 94fc4c35f103c37cbc1f63aff29e96be9d6092d8 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 4 Mar 2026 19:22:44 +0100 Subject: [PATCH 05/49] update --- README.md | 2 -- SKILL.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 26fb4e368..3f7b664a8 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,6 @@ All cryptographic material is persisted to `$HOME/.openclaw/billions/` — a dir | `challenges.json` | Per-DID challenge history | | `credentials.json` | Verifiable credentials | -Because this path is outside the agent workspace, the agent runtime has no ambient read or write access to these files. Access is restricted to the local OS user who installed the skill. File-based plaintext storage is safe in this context: no external party and no agent process can reach the keys. - ### Subprocess Execution Safety **Only one specific command is ever executed: `openclaw message send`**, with a fixed, hardcoded argument structure. The binary name, the subcommand, and all flag names (`--target`, `--message`) are hardcoded. User-supplied values are only ever passed as the **values** of those flags, never as the command name, subcommand, or flag names. Nothing else can be executed. There is no mechanism to change the binary, add flags, or inject subcommands. Additional security properties: diff --git a/SKILL.md b/SKILL.md index 152f67fdd..7e9bc27f2 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: verified-agent-identity description: Billions/Iden3 authentication and identity management tools for agents. Link, proof, sign, and verify. -metadata: { "category": "identity" } +metadata: { "category": "identity", "clawbot": { "requires": { "bins": ["node", "openclaw"] } }} homepage: https://billions.network/ --- From e2fd4784f751c337675636e4b70186be090a6f88 Mon Sep 17 00:00:00 2001 From: GopherDID Date: Tue, 10 Mar 2026 21:33:13 +0200 Subject: [PATCH 06/49] Update external call policy in README Clarified network policy regarding external calls and user consent. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f7b664a8..1854f1bef 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Prompt injection and arbitrary code execution are structurally impossible: the e ### Network and External Binary Policy -- All external http calls will be made to trusted resources. +- All external https calls will be made to trusted resources. Signed JWS attestation (proof of agent ownership) is encoded securely by utilizing robust security practices and sent within user context directly to agent owner. It requires an explicit user consent to pass it to any external source. It is not sent automatically anywhere without user participation. - No external binary other than `openclaw` is invoked. - Any external URLs or verification links produced by the scripts are delivered to the user as a plain text message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. From 9248a1d9812a079a53d58def6f97127f1f57d4bb Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 11 Mar 2026 12:04:31 +0100 Subject: [PATCH 07/49] Refactor key import process and update dependencies for improved functionality --- scripts/createNewEthereumIdentity.js | 7 --- scripts/package-lock.json | 69 ++++------------------------ scripts/package.json | 3 +- scripts/shared/utils.js | 3 +- 4 files changed, 12 insertions(+), 70 deletions(-) diff --git a/scripts/createNewEthereumIdentity.js b/scripts/createNewEthereumIdentity.js index 411fa6355..de389e8e6 100644 --- a/scripts/createNewEthereumIdentity.js +++ b/scripts/createNewEthereumIdentity.js @@ -38,13 +38,6 @@ async function main() { process.exit(1); } - // Import the key into KMS - const pkStorage = await keyProvider.getPkStore(); - await pkStorage.importKey({ - alias: normalizedKeyPath(KmsKeyType.Secp256k1, signer.publicKey), - key: signer.privateKey, - }); - // Create wallet with Billions Network provider const wallet = new Wallet( signer, diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 686547baa..73b2251ac 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -12,6 +12,7 @@ "@0xpolygonid/js-sdk": "^1.18.1", "@iden3/js-iden3-auth": "^1.14.0", "@iden3/js-iden3-core": "^1.4.1", + "@noble/curves": "^1.9.2", "ethers": "^6.13.4", "shell-quote": "^1.8.3", "uuid": "^11.0.3" @@ -55,33 +56,6 @@ "snarkjs": "0.7.5" } }, - "node_modules/@0xpolygonid/js-sdk/node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@0xpolygonid/js-sdk/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@0xpolygonid/js-sdk/node_modules/ethers": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", @@ -996,18 +970,6 @@ "snarkjs": "0.7.5" } }, - "node_modules/@iden3/js-iden3-auth/node_modules/@0xpolygonid/js-sdk/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@iden3/js-iden3-auth/node_modules/@0xpolygonid/js-sdk/node_modules/ethers": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", @@ -1061,28 +1023,13 @@ "uuid": "dist-node/bin/uuid" } }, - "node_modules/@iden3/js-iden3-auth/node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@iden3/js-iden3-auth/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1241,9 +1188,9 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", + "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" diff --git a/scripts/package.json b/scripts/package.json index 0891ebb12..f399cde90 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -16,8 +16,9 @@ "@0xpolygonid/js-sdk": "^1.18.1", "@iden3/js-iden3-auth": "^1.14.0", "@iden3/js-iden3-core": "^1.4.1", + "@noble/curves": "^1.9.2", "ethers": "^6.13.4", "shell-quote": "^1.8.3", "uuid": "^11.0.3" } -} \ No newline at end of file +} diff --git a/scripts/shared/utils.js b/scripts/shared/utils.js index bf4d2928d..1db8e776e 100644 --- a/scripts/shared/utils.js +++ b/scripts/shared/utils.js @@ -3,6 +3,7 @@ const { DID, Id } = require("@iden3/js-iden3-core"); const { v7: uuid } = require("uuid"); const { parse } = require("shell-quote"); const { execFileSync } = require("child_process"); +const { secp256k1 } = require("@noble/curves/secp256k1"); /** * Removes the "0x" prefix from a hexadecimal string if it exists @@ -39,7 +40,7 @@ function createDidDocument(did, publicKeyHex) { controller: did, type: "EcdsaSecp256k1RecoveryMethod2020", ethereumAddress: buildEthereumAddressFromDid(did), - publicKeyHex: publicKeyHex, + publicKeyHex: secp256k1.Point.fromHex(publicKeyHex.slice(2)).toHex(true), }, ], authentication: [`${did}#ethereum-based-id`], From cd5b2630de6d3944f1b6fcf503aab0b49cff2e06 Mon Sep 17 00:00:00 2001 From: GopherDID Date: Wed, 11 Mar 2026 13:59:53 +0200 Subject: [PATCH 08/49] Update README with security recommendations and mitigations Added recommendations for handling plaintext private keys and security mitigations. --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1854f1bef..20e4b0159 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ All cryptographic material is persisted to `$HOME/.openclaw/billions/` — a dir | `challenges.json` | Per-DID challenge history | | `credentials.json` | Verifiable credentials | +If you cannot accept plaintext private keys on the host consider usage of encrypted KMS. In the future updates native integration with one of the encrypted key providers will be announced. + +Recommended mitigations: review the code of this or other skills that can access plain files. Run the skill in an isolated VM or container, back up generated private keys and consider removing from the storage until the next mandatory usage will be needed. + ### Subprocess Execution Safety **Only one specific command is ever executed: `openclaw message send`**, with a fixed, hardcoded argument structure. The binary name, the subcommand, and all flag names (`--target`, `--message`) are hardcoded. User-supplied values are only ever passed as the **values** of those flags, never as the command name, subcommand, or flag names. Nothing else can be executed. There is no mechanism to change the binary, add flags, or inject subcommands. Additional security properties: @@ -110,7 +114,9 @@ Prompt injection and arbitrary code execution are structurally impossible: the e ### Network and External Binary Policy -- All external https calls will be made to trusted resources. Signed JWS attestation (proof of agent ownership) is encoded securely by utilizing robust security practices and sent within user context directly to agent owner. It requires an explicit user consent to pass it to any external source. It is not sent automatically anywhere without user participation. +- All external https calls will be made to trusted resources. Signed JWS attestation (proof of agent ownership) is encoded securely by utilizing robust security practices and sent within user context directly to agent owner. It requires an explicit user consent to pass it to any external source. It is not sent automatically anywhere without user participation. + All network calls are directed to legitimate DID resolvers (resolver.privado.id) or the project's own infrastructure (billions.network). + These network calls can not exfiltrate signed attestations or identity data to third-party services by skill design as they do not pass them. This is possible only through explicit action from the user side with consent. Also attestation contains only publicly verifiable information. - No external binary other than `openclaw` is invoked. - Any external URLs or verification links produced by the scripts are delivered to the user as a plain text message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. From 0f4638742323bdcdf2aee67c2c373c2cd7d2c4c1 Mon Sep 17 00:00:00 2001 From: GopherDID Date: Wed, 11 Mar 2026 14:07:04 +0200 Subject: [PATCH 09/49] Fix metadata key in SKILL.md Updated metadata key from 'clawbot' to 'clawdbot'. --- SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index 7e9bc27f2..ac56631cc 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: verified-agent-identity description: Billions/Iden3 authentication and identity management tools for agents. Link, proof, sign, and verify. -metadata: { "category": "identity", "clawbot": { "requires": { "bins": ["node", "openclaw"] } }} +metadata: { "category": "identity", "clawdbot": { "requires": { "bins": ["node", "openclaw"] } }} homepage: https://billions.network/ --- From 7067d1d5c641697c94b4326cd97733675f06d7e2 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 11 Mar 2026 14:22:26 +0100 Subject: [PATCH 10/49] update deps --- scripts/package-lock.json | 194 ++++++++++++++++++++++++++------------ scripts/package.json | 8 +- 2 files changed, 137 insertions(+), 65 deletions(-) diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 73b2251ac..ef5fe7e57 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.2", "license": "UNLICENSED", "dependencies": { - "@0xpolygonid/js-sdk": "^1.18.1", - "@iden3/js-iden3-auth": "^1.14.0", - "@iden3/js-iden3-core": "^1.4.1", + "@0xpolygonid/js-sdk": "1.42.1", + "@iden3/js-iden3-auth": "1.14.0", + "@iden3/js-iden3-core": "1.8.0", "@noble/curves": "^1.9.2", "ethers": "^6.13.4", "shell-quote": "^1.8.3", @@ -19,9 +19,9 @@ } }, "node_modules/@0xpolygonid/js-sdk": { - "version": "1.40.3", - "resolved": "https://registry.npmjs.org/@0xpolygonid/js-sdk/-/js-sdk-1.40.3.tgz", - "integrity": "sha512-k0xTQRPT2/UE63tYgRQZqJ3cLDZNQ6G4SpiHoArnsheoMRkiQuYP7PXcaiqbGJH/i5GWxThmiETk0hNrD23nhQ==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@0xpolygonid/js-sdk/-/js-sdk-1.42.1.tgz", + "integrity": "sha512-2X0zFGfygtu4D7X2DSiI3GxQ7vp3+iNQ05WeTi4u9aPj+9/sxYsVDJnRsHDZzmCCVO9kzmb2DQ1uxs5JtTrhPA==", "license": "MIT or Apache-2.0", "dependencies": { "@iden3/onchain-non-merklized-issuer-base-abi": "0.0.3", @@ -56,6 +56,33 @@ "snarkjs": "0.7.5" } }, + "node_modules/@0xpolygonid/js-sdk/node_modules/@noble/curves": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", + "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@0xpolygonid/js-sdk/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@0xpolygonid/js-sdk/node_modules/ethers": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", @@ -108,6 +135,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@0xpolygonid/js-sdk/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/@0xpolygonid/js-sdk/node_modules/uuid": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", @@ -970,6 +1003,18 @@ "snarkjs": "0.7.5" } }, + "node_modules/@iden3/js-iden3-auth/node_modules/@0xpolygonid/js-sdk/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@iden3/js-iden3-auth/node_modules/@0xpolygonid/js-sdk/node_modules/ethers": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", @@ -1023,13 +1068,28 @@ "uuid": "dist-node/bin/uuid" } }, + "node_modules/@iden3/js-iden3-auth/node_modules/@noble/curves": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", + "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@iden3/js-iden3-auth/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", "engines": { - "node": ">= 16" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1083,6 +1143,12 @@ "@ethersproject/wordlists": "5.8.0" } }, + "node_modules/@iden3/js-iden3-auth/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/@iden3/js-iden3-auth/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -1188,9 +1254,9 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" @@ -1333,20 +1399,14 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" } }, - "node_modules/@swc/helpers/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1366,9 +1426,9 @@ } }, "node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "license": "MIT" }, "node_modules/@types/ws": { @@ -1451,9 +1511,9 @@ "peer": true }, "node_modules/b4a": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.5.tgz", - "integrity": "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", "license": "Apache-2.0", "peer": true, "peerDependencies": { @@ -1901,6 +1961,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2004,9 +2070,9 @@ } }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -2162,6 +2228,21 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/jayson/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/jayson/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -2193,9 +2274,9 @@ } }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", + "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -2247,9 +2328,9 @@ "license": "Apache-2.0" }, "node_modules/jsonpath": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.2.1.tgz", - "integrity": "sha512-Jl6Jhk0jG+kP3yk59SSeGq7LFPR4JQz1DU0K+kXTysUhMostbhU3qh5mjTuf0PqFcXpAT7kvmMt9WxV10NyIgQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.3.0.tgz", + "integrity": "sha512-0kjkYHJBkAy50Z5QzArZ7udmvxrJzkpKYW27fiF//BrMY7TQibYLl+FYIXN2BiYmwMIVzSfD8aDRj6IzgBX2/w==", "license": "MIT", "peer": true, "dependencies": { @@ -2345,9 +2426,9 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "peer": true, "dependencies": { @@ -2559,17 +2640,17 @@ "peer": true }, "node_modules/rpc-websockets": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.3.tgz", - "integrity": "sha512-OkCsBBzrwxX4DoSv4Zlf9DgXKRB0MzVfCFg5MC+fNnf9ktr4SMWjsri0VNZQlDbCnGcImT6KNEv4ZoxktQhdpA==", + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.5.tgz", + "integrity": "sha512-4mAmr+AEhPYJ9TmDtxF3r3ZcbWy7W8kvZ4PoZYw/Xgp2J7WixjwTgiQZsoTDvch5nimmg3Ay6/0Kuh9oIvVs9A==", "license": "LGPL-3.0-only", "dependencies": { "@swc/helpers": "^0.5.11", - "@types/uuid": "^8.3.4", + "@types/uuid": "^10.0.0", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", - "uuid": "^8.3.2", + "uuid": "^11.0.0", "ws": "^8.5.0" }, "funding": { @@ -2578,7 +2659,7 @@ }, "optionalDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": "^6.0.0" } }, "node_modules/rpc-websockets/node_modules/@types/ws": { @@ -2590,15 +2671,6 @@ "@types/node": "*" } }, - "node_modules/rpc-websockets/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2738,9 +2810,9 @@ "peer": true }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/typescript": { @@ -2792,9 +2864,9 @@ "license": "MIT" }, "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/scripts/package.json b/scripts/package.json index f399cde90..b92e43563 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -13,12 +13,12 @@ "author": "BillionsNetwork", "license": "UNLICENSED", "dependencies": { - "@0xpolygonid/js-sdk": "^1.18.1", - "@iden3/js-iden3-auth": "^1.14.0", - "@iden3/js-iden3-core": "^1.4.1", + "@0xpolygonid/js-sdk": "1.42.1", + "@iden3/js-iden3-auth": "1.14.0", + "@iden3/js-iden3-core": "1.8.0", "@noble/curves": "^1.9.2", "ethers": "^6.13.4", "shell-quote": "^1.8.3", "uuid": "^11.0.3" } -} +} \ No newline at end of file From 59577a65ac0a5273068dc86ac0b7c913dbfb67e1 Mon Sep 17 00:00:00 2001 From: Vladyslav Munin Date: Wed, 11 Mar 2026 16:52:17 +0200 Subject: [PATCH 11/49] fix readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 20e4b0159..a28af1284 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ npm dependencies are intentionally minimal and scoped to well-established, audit | `shell-quote` | Shell token parsing used **only** for input sanitization | | `uuid` | UUID generation for protocol message IDs | +Also major libs that influence the identity management part have fixed well tested library versions. + ### Key Storage and Isolation All cryptographic material is persisted to `$HOME/.openclaw/billions/` — a directory that lives **outside the agent's workspace**: From efb1efb6cc65b945db6f07ad4bc726a27fdbd097 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Thu, 12 Mar 2026 11:17:56 +0100 Subject: [PATCH 12/49] Implement KMS encryption for private keys in keys.js and update README.md --- README.md | 61 ++++++++++++++++++++++++----- scripts/shared/storage/crypto.js | 66 ++++++++++++++++++++++++++++++++ scripts/shared/storage/keys.js | 41 +++++++++++++++++++- 3 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 scripts/shared/storage/crypto.js diff --git a/README.md b/README.md index a28af1284..a8aebcf65 100644 --- a/README.md +++ b/README.md @@ -90,17 +90,58 @@ Also major libs that influence the identity management part have fixed well test All cryptographic material is persisted to `$HOME/.openclaw/billions/` — a directory that lives **outside the agent's workspace**: -| File | Contents | -| ------------------ | ----------------------------------------------- | -| `kms.json` | Private keys (unencrypted, owner-readable only) | -| `identities.json` | Identity metadata | -| `defaultDid.json` | Active DID and associated public key | -| `challenges.json` | Per-DID challenge history | -| `credentials.json` | Verifiable credentials | +| File | Contents | +| ------------------ | ---------------------------------------------------------------------------------- | +| `kms.json` | Private keys — plain text by default, AES-256-GCM encrypted when master key is set | +| `identities.json` | Identity metadata | +| `defaultDid.json` | Active DID and associated public key | +| `challenges.json` | Per-DID challenge history | +| `credentials.json` | Verifiable credentials | -If you cannot accept plaintext private keys on the host consider usage of encrypted KMS. In the future updates native integration with one of the encrypted key providers will be announced. +Private keys are stored in plain text by default. To protect them at rest, enable master key encryption as described in the **KMS Encryption** section below. -Recommended mitigations: review the code of this or other skills that can access plain files. Run the skill in an isolated VM or container, back up generated private keys and consider removing from the storage until the next mandatory usage will be needed. +### KMS Encryption + +Set the environment variable `BILLIONS_NETWORK_MASTER_KMS_KEY` to enable AES-256-GCM at-rest encryption for `kms.json`. When the variable is set, every write to the key store is encrypted; when it is absent the store keeps all in plain JSON (backward-compatible with all existing files). + +**Behaviour summary** + +| `BILLIONS_NETWORK_MASTER_KMS_KEY` | `kms.json` on disk | +| --------------------------------- | ------------------------------- | +| Not set | Plain JSON (legacy format kept) | +| Set | AES-256-GCM encrypted envelope | + +> **Backward compatibility** — existing plain-text `kms.json` files are read without a master key. On the first write after the variable is set the file is automatically re-encrypted. No manual migration step is required. + +**How to set the variable** + +_Option 1 — openclaw skill config (recommended for agent deployments):_ + +Add an `env` block for the skill inside your openclaw config: + +```json +"skills": { + "entries": { + "verified-agent-identity": { + "env": { + "BILLIONS_NETWORK_MASTER_KMS_KEY": "" + } + } + } +} +``` + +_Option 2 — shell or process environment:_ + +```bash +export BILLIONS_NETWORK_MASTER_KMS_KEY="" +node scripts/createNewEthereumIdentity.js +node scripts/manualLinkHumanToAgent.js --challenge '{"name": "Agent Name", "description": "Short description of the agent"}' +``` + +For all other ways to pass environment variables to a skill see the [OpenClaw environment documentation](https://docs.openclaw.ai/help/environment). + +**CRITICAL**: Save master keys securely and do not share them. If the master key is lost, all encrypted keys will be lost. ### Subprocess Execution Safety @@ -116,7 +157,7 @@ Prompt injection and arbitrary code execution are structurally impossible: the e ### Network and External Binary Policy -- All external https calls will be made to trusted resources. Signed JWS attestation (proof of agent ownership) is encoded securely by utilizing robust security practices and sent within user context directly to agent owner. It requires an explicit user consent to pass it to any external source. It is not sent automatically anywhere without user participation. +- All external https calls will be made to trusted resources. Signed JWS attestation (proof of agent ownership) is encoded securely by utilizing robust security practices and sent within user context directly to agent owner. It requires an explicit user consent to pass it to any external source. It is not sent automatically anywhere without user participation. All network calls are directed to legitimate DID resolvers (resolver.privado.id) or the project's own infrastructure (billions.network). These network calls can not exfiltrate signed attestations or identity data to third-party services by skill design as they do not pass them. This is possible only through explicit action from the user side with consent. Also attestation contains only publicly verifiable information. - No external binary other than `openclaw` is invoked. diff --git a/scripts/shared/storage/crypto.js b/scripts/shared/storage/crypto.js new file mode 100644 index 000000000..2ae5a1acf --- /dev/null +++ b/scripts/shared/storage/crypto.js @@ -0,0 +1,66 @@ +"use strict"; + +const crypto = require("crypto"); + +const ALGORITHM = "aes-256-gcm"; +const IV_BYTES = 12; +const TAG_BYTES = 16; + +function getMasterKey() { + return process.env.BILLIONS_NETWORK_MASTER_KMS_KEY || null; +} + +function deriveAesKey(masterKeyString) { + return crypto.createHash("sha256").update(masterKeyString, "utf8").digest(); +} + +function encryptKeys(keysArray, masterKeyString) { + const aesKey = deriveAesKey(masterKeyString); + const iv = crypto.randomBytes(IV_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, aesKey, iv, { + authTagLength: TAG_BYTES, + }); + + const plain = JSON.stringify(keysArray); + const encrypted = Buffer.concat([ + cipher.update(plain, "utf8"), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + + return { + version: 1, + encrypted: true, + iv: iv.toString("hex"), + authTag: authTag.toString("hex"), + data: encrypted.toString("hex"), + }; +} + +function decryptKeys(envelope, masterKeyString) { + const aesKey = deriveAesKey(masterKeyString); + const iv = Buffer.from(envelope.iv, "hex"); + const authTag = Buffer.from(envelope.authTag, "hex"); + const ciphertext = Buffer.from(envelope.data, "hex"); + + const decipher = crypto.createDecipheriv(ALGORITHM, aesKey, iv, { + authTagLength: TAG_BYTES, + }); + decipher.setAuthTag(authTag); + + let decrypted; + try { + decrypted = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]).toString("utf8"); + } catch { + throw new Error( + "kms.json decryption failed: wrong BILLIONS_NETWORK_MASTER_KMS_KEY or file has been tampered with", + ); + } + + return JSON.parse(decrypted); +} + +module.exports = { getMasterKey, encryptKeys, decryptKeys }; diff --git a/scripts/shared/storage/keys.js b/scripts/shared/storage/keys.js index a1c78961c..a48a83b0a 100644 --- a/scripts/shared/storage/keys.js +++ b/scripts/shared/storage/keys.js @@ -1,15 +1,54 @@ const { FileStorage } = require("./base"); +const { getMasterKey, encryptKeys, decryptKeys } = require("./crypto"); /** * File-based storage for cryptographic keys. * Implements AbstractPrivateKeyStore interface from js-sdk. - * Stores keys in JSON format as an array of {alias, privateKeyHex} objects. + * Stores keys in JSON or encrypted format as an array of {alias, privateKeyHex} objects. */ class KeysFileStorage extends FileStorage { constructor(filename = "kms.json") { super(filename); } + async readFile() { + const raw = await super.readFile(); + + // Legacy format + if (Array.isArray(raw)) { + return raw; + } + + if (raw && raw.version === 1) { + if (!raw.encrypted) { + // Unencrypted versioned format + return Array.isArray(raw.keys) ? raw.keys : []; + } + + // Encrypted versioned format + const masterKey = getMasterKey(); + if (!masterKey) { + throw new Error( + "kms.json is encrypted but BILLIONS_NETWORK_MASTER_KMS_KEY is not set. " + + "Set the environment variable to decrypt the key store.", + ); + } + return decryptKeys(raw, masterKey); + } + + throw new Error("Invalid kms.json format"); + } + + async writeFile(keys) { + const masterKey = getMasterKey(); + + const payload = masterKey + ? encryptKeys(keys, masterKey) + : { version: 1, encrypted: false, keys }; + + await super.writeFile(payload); + } + async importKey(args) { const keys = await this.readFile(); const index = keys.findIndex((entry) => entry.alias === args.alias); From af6c99b89ac887f1254a0edc607d3c457bc1259b Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Thu, 12 Mar 2026 14:57:43 +0100 Subject: [PATCH 13/49] encrypt each key --- README.md | 41 ++++++++++++--- scripts/shared/storage/crypto.js | 38 +++++++------- scripts/shared/storage/keys.js | 89 ++++++++++++++++++++++---------- 3 files changed, 116 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index a8aebcf65..cedc92e09 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ All cryptographic material is persisted to `$HOME/.openclaw/billions/` — a dir | File | Contents | | ------------------ | ---------------------------------------------------------------------------------- | -| `kms.json` | Private keys — plain text by default, AES-256-GCM encrypted when master key is set | +| `kms.json` | Private keys — per-entry versioned format; keys are plain or AES-256-GCM encrypted | | `identities.json` | Identity metadata | | `defaultDid.json` | Active DID and associated public key | | `challenges.json` | Per-DID challenge history | @@ -102,16 +102,43 @@ Private keys are stored in plain text by default. To protect them at rest, enabl ### KMS Encryption -Set the environment variable `BILLIONS_NETWORK_MASTER_KMS_KEY` to enable AES-256-GCM at-rest encryption for `kms.json`. When the variable is set, every write to the key store is encrypted; when it is absent the store keeps all in plain JSON (backward-compatible with all existing files). +Set the environment variable `BILLIONS_NETWORK_MASTER_KMS_KEY` to enable AES-256-GCM at-rest encryption for the private keys inside `kms.json`. When set, every key value is individually encrypted on write; when absent, keys are stored as plain hex strings. + +**`kms.json` entry format** + +Each entry in the array is versioned. The `alias` is always stored in plain text — only the `key` value is encrypted: + +```json +[ + { + "version": 1, + "provider": "plain", + "data": { + "alias": "secp256k1:abc123", + "key": "deadbeef...", + "createdAt": "2026-03-12T13:46:04.094Z" + } + }, + { + "version": 1, + "provider": "encrypted", + "data": { + "alias": "secp256k1:xyz456", + "key": "::", + "createdAt": "2026-02-11T13:00:02.032Z" + } + } +] +``` **Behaviour summary** -| `BILLIONS_NETWORK_MASTER_KMS_KEY` | `kms.json` on disk | -| --------------------------------- | ------------------------------- | -| Not set | Plain JSON (legacy format kept) | -| Set | AES-256-GCM encrypted envelope | +| `BILLIONS_NETWORK_MASTER_KMS_KEY` | `provider` on disk | `key` value on disk | +| --------------------------------- | ------------------ | ----------------------- | +| Not set | `"plain"` | Raw hex string | +| Set | `"encrypted"` | `iv:authTag:ciphertext` | -> **Backward compatibility** — existing plain-text `kms.json` files are read without a master key. On the first write after the variable is set the file is automatically re-encrypted. No manual migration step is required. +> **Backward compatibility** — the legacy format `[ { "alias": "...", "privateKeyHex": "..." } ]` is still read correctly. On the first write the file is automatically migrated to the new per-entry format. No manual step is required. **How to set the variable** diff --git a/scripts/shared/storage/crypto.js b/scripts/shared/storage/crypto.js index 2ae5a1acf..0e1c7137a 100644 --- a/scripts/shared/storage/crypto.js +++ b/scripts/shared/storage/crypto.js @@ -14,43 +14,45 @@ function deriveAesKey(masterKeyString) { return crypto.createHash("sha256").update(masterKeyString, "utf8").digest(); } -function encryptKeys(keysArray, masterKeyString) { +function encryptKey(keyHex, masterKeyString) { const aesKey = deriveAesKey(masterKeyString); const iv = crypto.randomBytes(IV_BYTES); const cipher = crypto.createCipheriv(ALGORITHM, aesKey, iv, { authTagLength: TAG_BYTES, }); - const plain = JSON.stringify(keysArray); const encrypted = Buffer.concat([ - cipher.update(plain, "utf8"), + cipher.update(keyHex, "utf8"), cipher.final(), ]); const authTag = cipher.getAuthTag(); - return { - version: 1, - encrypted: true, - iv: iv.toString("hex"), - authTag: authTag.toString("hex"), - data: encrypted.toString("hex"), - }; + return [ + iv.toString("hex"), + authTag.toString("hex"), + encrypted.toString("hex"), + ].join(":"); } -function decryptKeys(envelope, masterKeyString) { +function decryptKey(encryptedPayload, masterKeyString) { + const parts = encryptedPayload.split(":"); + if (parts.length !== 3) { + throw new Error("Invalid encrypted key format in kms.json"); + } + const [ivHex, authTagHex, ciphertextHex] = parts; + const aesKey = deriveAesKey(masterKeyString); - const iv = Buffer.from(envelope.iv, "hex"); - const authTag = Buffer.from(envelope.authTag, "hex"); - const ciphertext = Buffer.from(envelope.data, "hex"); + const iv = Buffer.from(ivHex, "hex"); + const authTag = Buffer.from(authTagHex, "hex"); + const ciphertext = Buffer.from(ciphertextHex, "hex"); const decipher = crypto.createDecipheriv(ALGORITHM, aesKey, iv, { authTagLength: TAG_BYTES, }); decipher.setAuthTag(authTag); - let decrypted; try { - decrypted = Buffer.concat([ + return Buffer.concat([ decipher.update(ciphertext), decipher.final(), ]).toString("utf8"); @@ -59,8 +61,6 @@ function decryptKeys(envelope, masterKeyString) { "kms.json decryption failed: wrong BILLIONS_NETWORK_MASTER_KMS_KEY or file has been tampered with", ); } - - return JSON.parse(decrypted); } -module.exports = { getMasterKey, encryptKeys, decryptKeys }; +module.exports = { getMasterKey, encryptKey, decryptKey }; diff --git a/scripts/shared/storage/keys.js b/scripts/shared/storage/keys.js index a48a83b0a..25bbc6ef7 100644 --- a/scripts/shared/storage/keys.js +++ b/scripts/shared/storage/keys.js @@ -1,52 +1,80 @@ const { FileStorage } = require("./base"); -const { getMasterKey, encryptKeys, decryptKeys } = require("./crypto"); +const { getMasterKey, encryptKey, decryptKey } = require("./crypto"); /** * File-based storage for cryptographic keys. * Implements AbstractPrivateKeyStore interface from js-sdk. - * Stores keys in JSON or encrypted format as an array of {alias, privateKeyHex} objects. + * Stores keys in JSON format as an array of per-entry versioned objects. */ class KeysFileStorage extends FileStorage { constructor(filename = "kms.json") { super(filename); + // Holds raw on-disk entries that could not be decoded in this session + // (e.g. encrypted entries when the master key env var is absent). + // They are round-tripped untouched through writeFile so no data is lost. + this._opaqueEntries = []; } - async readFile() { - const raw = await super.readFile(); - + _decodeEntry(entry) { // Legacy format - if (Array.isArray(raw)) { - return raw; + if (Object.prototype.hasOwnProperty.call(entry, "privateKeyHex")) { + return { alias: entry.alias, privateKeyHex: entry.privateKeyHex }; } - if (raw && raw.version === 1) { - if (!raw.encrypted) { - // Unencrypted versioned format - return Array.isArray(raw.keys) ? raw.keys : []; + if (entry.version === 1) { + const { alias, key } = entry.data; + + const { createdAt } = entry.data; + + if (entry.provider === "plain") { + return { alias, privateKeyHex: key, createdAt }; } - // Encrypted versioned format - const masterKey = getMasterKey(); - if (!masterKey) { - throw new Error( - "kms.json is encrypted but BILLIONS_NETWORK_MASTER_KMS_KEY is not set. " + - "Set the environment variable to decrypt the key store.", - ); + if (entry.provider === "encrypted") { + const masterKey = getMasterKey(); + if (!masterKey) { + return { alias, _opaque: true, _raw: entry }; + } + return { alias, privateKeyHex: decryptKey(key, masterKey), createdAt }; } - return decryptKeys(raw, masterKey); } - throw new Error("Invalid kms.json format"); + throw new Error( + `Unrecognised kms.json entry format: ${entry.alias || entry.data.alias || "unknown alias"}}`, + ); } - async writeFile(keys) { + _encodeEntry({ alias, privateKeyHex, createdAt }) { const masterKey = getMasterKey(); + if (masterKey) { + return { + version: 1, + provider: "encrypted", + data: { alias, key: encryptKey(privateKeyHex, masterKey), createdAt }, + }; + } + return { + version: 1, + provider: "plain", + data: { alias, key: privateKeyHex, createdAt }, + }; + } - const payload = masterKey - ? encryptKeys(keys, masterKey) - : { version: 1, encrypted: false, keys }; + async readFile() { + const raw = await super.readFile(); + if (!Array.isArray(raw)) { + throw new Error("kms.json root must be an array"); + } + const decoded = raw.map((entry) => this._decodeEntry(entry)); + // Stash raw on-disk objects for entries we cannot decode right now so + // writeFile can round-trip them untouched. + this._opaqueEntries = decoded.filter((e) => e._opaque).map((e) => e._raw); + return decoded.filter((e) => !e._opaque); + } - await super.writeFile(payload); + async writeFile(keys) { + const encoded = keys.map((entry) => this._encodeEntry(entry)); + await super.writeFile([...encoded, ...this._opaqueEntries]); } async importKey(args) { @@ -56,9 +84,18 @@ class KeysFileStorage extends FileStorage { if (index >= 0) { keys[index].privateKeyHex = args.key; } else { - keys.push({ alias: args.alias, privateKeyHex: args.key }); + keys.push({ + alias: args.alias, + privateKeyHex: args.key, + createdAt: new Date().toISOString(), + }); } + // update key under alias + this._opaqueEntries = this._opaqueEntries.filter( + (raw) => raw.data?.alias !== args.alias, + ); + await this.writeFile(keys); } From 2fe6ff818db65f19bad94742fb8fc6452ecfc46c Mon Sep 17 00:00:00 2001 From: GopherDID Date: Thu, 12 Mar 2026 16:19:21 +0200 Subject: [PATCH 14/49] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/shared/storage/crypto.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/shared/storage/crypto.js b/scripts/shared/storage/crypto.js index 0e1c7137a..fcbc0bd0c 100644 --- a/scripts/shared/storage/crypto.js +++ b/scripts/shared/storage/crypto.js @@ -7,7 +7,22 @@ const IV_BYTES = 12; const TAG_BYTES = 16; function getMasterKey() { - return process.env.BILLIONS_NETWORK_MASTER_KMS_KEY || null; + const rawKey = process.env.BILLIONS_NETWORK_MASTER_KMS_KEY; + + if (typeof rawKey !== "string") { + return null; + } + + const trimmedKey = rawKey.trim(); + + // Reject whitespace-only or too-short keys to avoid weak/blank-looking master keys. + // Returning null keeps behavior consistent with the "no key configured" case. + const MIN_MASTER_KEY_LENGTH = 16; + if (trimmedKey.length < MIN_MASTER_KEY_LENGTH) { + return null; + } + + return trimmedKey; } function deriveAesKey(masterKeyString) { From fa078dda853a7abbe19e845171ce39d783f39d5f Mon Sep 17 00:00:00 2001 From: GopherDID Date: Thu, 12 Mar 2026 16:20:06 +0200 Subject: [PATCH 15/49] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cedc92e09..f5d474e11 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ All cryptographic material is persisted to `$HOME/.openclaw/billions/` — a dir | `challenges.json` | Per-DID challenge history | | `credentials.json` | Verifiable credentials | -Private keys are stored in plain text by default. To protect them at rest, enable master key encryption as described in the **KMS Encryption** section below. +There are several ways of storing private keys, to enable master key encryption as described in the **KMS Encryption** section below. ### KMS Encryption From dc6b3038c3e2005595bc781d55678678e0166933 Mon Sep 17 00:00:00 2001 From: Vladyslav Munin Date: Thu, 12 Mar 2026 23:08:01 +0200 Subject: [PATCH 16/49] add info about openclaw exec --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f5d474e11..59f05a6d9 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,11 @@ Prompt injection and arbitrary code execution are structurally impossible: the e - No external binary other than `openclaw` is invoked. - Any external URLs or verification links produced by the scripts are delivered to the user as a plain text message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. +### Openclaw exec policy + +Code uses shell command execution detected in the scripts/shared/utils.js, but security-conscious patterns are evident in scripts/shared/utils.js, which uses execFileSync to prevent shell interpolation and includes custom sanitization logic (assertNoShellOperators) to mitigate injection risks when calling the openclaw CLI. +Only Openclaw binary is invoked. + ## Documentation See [SKILL.md](SKILL.md) for detailed usage instructions and examples. From 0bcfbe1ad05d7cbfee91eb0aad7b1141ef0d9cee Mon Sep 17 00:00:00 2001 From: Oleksandr Brezhniev Date: Thu, 12 Mar 2026 21:46:34 +0000 Subject: [PATCH 17/49] Revise README for clarity and terminology updates Updated terminology and improved clarity in the README. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 59f05a6d9..abd242400 100644 --- a/README.md +++ b/README.md @@ -77,14 +77,14 @@ npm dependencies are intentionally minimal and scoped to well-established, audit | Package | Purpose | | ---------------------- | ------------------------------------------------------------ | -| `@0xpolygonid/js-sdk` | iden3/Polygon ID cryptographic primitives and key management | +| `@0xpolygonid/js-sdk` | iden3/Privado ID cryptographic primitives and key management | | `@iden3/js-iden3-core` | DID and identity core types | | `@iden3/js-iden3-auth` | JWS/JWA authorization response construction and verification | | `ethers` | Ethereum key utilities | | `shell-quote` | Shell token parsing used **only** for input sanitization | | `uuid` | UUID generation for protocol message IDs | -Also major libs that influence the identity management part have fixed well tested library versions. +Core libraries governing identity management use pinned, well-tested versions to ensure stability and security. ### Key Storage and Isolation @@ -131,7 +131,7 @@ Each entry in the array is versioned. The `alias` is always stored in plain text ] ``` -**Behaviour summary** +**Behavior summary** | `BILLIONS_NETWORK_MASTER_KMS_KEY` | `provider` on disk | `key` value on disk | | --------------------------------- | ------------------ | ----------------------- | @@ -186,14 +186,14 @@ Prompt injection and arbitrary code execution are structurally impossible: the e - All external https calls will be made to trusted resources. Signed JWS attestation (proof of agent ownership) is encoded securely by utilizing robust security practices and sent within user context directly to agent owner. It requires an explicit user consent to pass it to any external source. It is not sent automatically anywhere without user participation. All network calls are directed to legitimate DID resolvers (resolver.privado.id) or the project's own infrastructure (billions.network). - These network calls can not exfiltrate signed attestations or identity data to third-party services by skill design as they do not pass them. This is possible only through explicit action from the user side with consent. Also attestation contains only publicly verifiable information. + These network calls cannot exfiltrate signed attestations or identity data to third-party services by skill design as they do not pass them. This is possible only through explicit action from the user side with consent. Also attestation contains only publicly verifiable information. - No external binary other than `openclaw` is invoked. -- Any external URLs or verification links produced by the scripts are delivered to the user as a plain text message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. +- External URLs or verification links produced by the scripts are delivered to the user as a plain text message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. ### Openclaw exec policy -Code uses shell command execution detected in the scripts/shared/utils.js, but security-conscious patterns are evident in scripts/shared/utils.js, which uses execFileSync to prevent shell interpolation and includes custom sanitization logic (assertNoShellOperators) to mitigate injection risks when calling the openclaw CLI. -Only Openclaw binary is invoked. +Code in the scripts/shared/utils.js uses shell command execution, but in a security-conscious manner using execFileSync to prevent shell interpolation and includes sanitization logic (assertNoShellOperators) to mitigate injection risks when calling the openclaw CLI. +Only the openclaw binary is invoked. ## Documentation From c9db135031c672bb6d7aa18d094f4f887d7f62d4 Mon Sep 17 00:00:00 2001 From: Oleksandr Brezhniev Date: Thu, 12 Mar 2026 23:20:41 +0000 Subject: [PATCH 18/49] Revise SKILL.md for clarity and technical details Updated the description to clarify the use of Billions decentralized identity and added technical details regarding linking agents to human identities. --- SKILL.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SKILL.md b/SKILL.md index ac56631cc..0157b2649 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: verified-agent-identity -description: Billions/Iden3 authentication and identity management tools for agents. Link, proof, sign, and verify. +description: Billions decentralized identity for agents. Link agents to human identities using Billions ERC-8004 and Attestation Registries. Verify and generate authentication proofs. Based on iden3 self-sovereign identity protocol. metadata: { "category": "identity", "clawdbot": { "requires": { "bins": ["node", "openclaw"] } }} homepage: https://billions.network/ --- @@ -10,10 +10,10 @@ homepage: https://billions.network/ Lets AI agents create and manage their own identities on the Billions Network, and link those identities to a human owner. 1. When you need to link your agent identity to an owner. -2. When you need sign a challenge. -3. When you need link a human to the agent's DID. +2. When you need to sign a challenge. +3. When you need to link a human to the agent's DID. 4. When you need to verify a signature to confirm identity ownership. -5. When use shared JWT tokens for authentication. +5. When you use shared JWT tokens for authentication. 6. When you need to create and manage decentralized identities. ### After installing the plugin run the following commands to create an identity and link it to your human DID: @@ -112,7 +112,7 @@ node scripts/signChallenge.js --to --challenge 8472951360 ### linkHumanToAgent.js **Command**: `node scripts/linkHumanToAgent.js --to --challenge [--did ]` -**Description**: Signs the challenge and links a human user to the agent's DID by creating a verification request. Response will be sent as a direct message to the specified sender. +**Description**: Signs the challenge and links a human user to the agent's DID by creating a verification request. Response will be sent as a direct message to the specified sender. Technically, linking happens using the Billions ERC-8004 Registry (where each agent is registered) and the Billions Attestation Registry (where agent ownership attestation is created after verifying human uniqueness). **Arguments**: - `--to` - (required) The message sender identifier, passed as `--target` to `openclaw message send` @@ -168,7 +168,7 @@ node scripts/verifySignature.js --did did:iden3:billions:main:2VmAk... --token e The directory `$HOME/.openclaw/billions` contains all sensitive identity data: -- `kms.json` - **CRITICAL**: Contains unencrypted private keys +- `kms.json` - **CRITICAL**: Contains private keys (encrypted if BILLIONS_NETWORK_MASTER_KMS_KEY is set, otherwise in plaintext) - `defaultDid.json` - DID identifiers and public keys - `challenges.json` - Authentication challenges history - `credentials.json` - Verifiable credentials @@ -197,7 +197,7 @@ User: "Link your agent identity to me" Agent: exec node scripts/linkHumanToAgent.js --to --challenge ``` -### Verifying someone else's Identity +### Verifying Someone Else’s Identity **Verification Flow:** From 3589d67d245274c6a3f50a51345b0faedc0762f7 Mon Sep 17 00:00:00 2001 From: Oleksandr Brezhniev Date: Thu, 12 Mar 2026 23:21:13 +0000 Subject: [PATCH 19/49] Fix typos in README.md for clarity --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index abd242400..1e2ba90be 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Set the environment variable `BILLIONS_NETWORK_MASTER_KMS_KEY` to enable AES-256 **`kms.json` entry format** -Each entry in the array is versioned. The `alias` is always stored in plain text — only the `key` value is encrypted: +Each entry in the array is versioned. The `alias` is always stored in plaintext — only the `key` value is encrypted: ```json [ @@ -188,7 +188,7 @@ Prompt injection and arbitrary code execution are structurally impossible: the e All network calls are directed to legitimate DID resolvers (resolver.privado.id) or the project's own infrastructure (billions.network). These network calls cannot exfiltrate signed attestations or identity data to third-party services by skill design as they do not pass them. This is possible only through explicit action from the user side with consent. Also attestation contains only publicly verifiable information. - No external binary other than `openclaw` is invoked. -- External URLs or verification links produced by the scripts are delivered to the user as a plain text message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. +- External URLs or verification links produced by the scripts are delivered to the user as a plaintext message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. ### Openclaw exec policy From 0ae43cf3131debbd17e07e10d4317bee9818156b Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Fri, 13 Mar 2026 11:18:11 +0100 Subject: [PATCH 20/49] support git shortener --- README.md | 29 +++----------- SKILL.md | 25 ++++++------ scripts/constants.js | 1 + scripts/createNewEthereumIdentity.js | 1 - scripts/linkHumanToAgent.js | 40 ++++++++++++------- scripts/package-lock.json | 13 ------- scripts/package.json | 1 - scripts/shared/utils.js | 57 ++-------------------------- scripts/signChallenge.js | 12 ++---- 9 files changed, 50 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 1e2ba90be..0174a6878 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,6 @@ This skill enables AI agents to create, manage, link, prove and verify ownership ### Runtime Requirements - **Node.js `>= v20`** and **npm** are required to run the scripts. -- The **`openclaw` CLI** must be installed and available in `PATH`. It is a hard runtime dependency used exclusively for sending direct messages to other agents or users on the Billions Network. ### Dependency Surface @@ -81,7 +80,6 @@ npm dependencies are intentionally minimal and scoped to well-established, audit | `@iden3/js-iden3-core` | DID and identity core types | | `@iden3/js-iden3-auth` | JWS/JWA authorization response construction and verification | | `ethers` | Ethereum key utilities | -| `shell-quote` | Shell token parsing used **only** for input sanitization | | `uuid` | UUID generation for protocol message IDs | Core libraries governing identity management use pinned, well-tested versions to ensure stability and security. @@ -170,30 +168,13 @@ For all other ways to pass environment variables to a skill see the [OpenClaw en **CRITICAL**: Save master keys securely and do not share them. If the master key is lost, all encrypted keys will be lost. -### Subprocess Execution Safety - -**Only one specific command is ever executed: `openclaw message send`**, with a fixed, hardcoded argument structure. The binary name, the subcommand, and all flag names (`--target`, `--message`) are hardcoded. User-supplied values are only ever passed as the **values** of those flags, never as the command name, subcommand, or flag names. Nothing else can be executed. There is no mechanism to change the binary, add flags, or inject subcommands. Additional security properties: - -1. **No shell interpolation**: `execFileSync(binary, argsArray)` bypasses the OS shell entirely. The OS `exec*` syscall receives the argument vector directly — shell metacharacters in argument values are treated as literal data. -2. **Argument-level input validation**: Before the call, all user-supplied values pass through two independent validation layers: - - `validateTarget` — enforces a strict allowlist regex (`/^[A-Za-z0-9:._@\-\/]+$/`) on the `--target` value. - - `assertNoShellOperators` — uses `shell-quote` to tokenize the input and rejects any token with an `op` property (i.e., `|`, `&`, `;`, `>`, `<`, `$()`, etc.). -3. **No dynamic code execution**: No `eval`, `new Function`, `child_process.exec`, or shell-interpolated `execSync` calls exist anywhere in the codebase. - -Prompt injection and arbitrary code execution are structurally impossible: the executed command and its flags are hardcoded constants, and user data can only influence the string values passed to `--target` and `--message` after sanitization. - ### Network and External Binary Policy -- All external https calls will be made to trusted resources. Signed JWS attestation (proof of agent ownership) is encoded securely by utilizing robust security practices and sent within user context directly to agent owner. It requires an explicit user consent to pass it to any external source. It is not sent automatically anywhere without user participation. - All network calls are directed to legitimate DID resolvers (resolver.privado.id) or the project's own infrastructure (billions.network). - These network calls cannot exfiltrate signed attestations or identity data to third-party services by skill design as they do not pass them. This is possible only through explicit action from the user side with consent. Also attestation contains only publicly verifiable information. -- No external binary other than `openclaw` is invoked. -- External URLs or verification links produced by the scripts are delivered to the user as a plaintext message via `openclaw message send`. The agent has no ability to follow, fetch, open, or interact with those URLs in any way - it only forwards the string to the user. - -### Openclaw exec policy - -Code in the scripts/shared/utils.js uses shell command execution, but in a security-conscious manner using execFileSync to prevent shell interpolation and includes sanitization logic (assertNoShellOperators) to mitigate injection risks when calling the openclaw CLI. -Only the openclaw binary is invoked. +- All external https calls will be made to trusted resources. Signed JWS attestation (proof of agent ownership) is encoded securely by utilizing robust security practices. It requires an explicit user consent to pass it to any other source. +- All network calls are directed to legitimate DID resolvers (resolver.privado.id) or the project's own infrastructure (billions.network). These network calls cannot exfiltrate signed attestations or identity data to other third-party services by skill design. Wallet interaction is possible only through explicit action from the user side with consent. Also attestation contains only publicly verifiable information. +- Whitelisted domains: + - `resolver.privado.id` (DID resolution) + - `billions.network` (Billions Network interactions) ## Documentation diff --git a/SKILL.md b/SKILL.md index 0157b2649..5574f4acd 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: verified-agent-identity description: Billions decentralized identity for agents. Link agents to human identities using Billions ERC-8004 and Attestation Registries. Verify and generate authentication proofs. Based on iden3 self-sovereign identity protocol. -metadata: { "category": "identity", "clawdbot": { "requires": { "bins": ["node", "openclaw"] } }} +metadata: { "category": "identity", "clawdbot": { "requires": { "bins": ["node"] } } } homepage: https://billions.network/ --- @@ -23,7 +23,7 @@ cd scripts && npm install && cd .. # Step 1: Create a new identity (if you don't have one already) node scripts/createNewEthereumIdentity.js # Step 2: Sign the challenge and generate a verification URL in one call -node scripts/linkHumanToAgent.js --to --challenge '{"name": , "description": }' +node scripts/linkHumanToAgent.js --challenge '{"name": , "description": }' ``` ## Scope @@ -92,37 +92,35 @@ node scripts/generateChallenge.js --did did:iden3:billions:main:2VmAk... ### signChallenge.js -**Command**: `node scripts/signChallenge.js --to --challenge [--did ]` -**Description**: Signs a challenge with a DID's private key to prove identity ownership and sends the JWS token as a direct message to the specified sender. Use this when you need to prove you own a specific DID. +**Command**: `node scripts/signChallenge.js --challenge [--did ]` +**Description**: Signs a challenge with a DID's private key to prove identity ownership and sends the JWS token. Use this when you need to prove you own a specific DID. **Arguments**: -- `--to` - (required) The message sender identifier, passed as `--target` to `openclaw message send` - `--challenge` - (required) Challenge to sign - `--did` - (optional) The DID of the attestation recipient; uses the default DID if omitted **Usage Examples**: ```bash -# Sign with default DID and send to sender -node scripts/signChallenge.js --to --challenge 8472951360 +# Sign with default DID +node scripts/signChallenge.js --challenge 8472951360 ``` **Output**: `{"success":true}` ### linkHumanToAgent.js -**Command**: `node scripts/linkHumanToAgent.js --to --challenge [--did ]` -**Description**: Signs the challenge and links a human user to the agent's DID by creating a verification request. Response will be sent as a direct message to the specified sender. Technically, linking happens using the Billions ERC-8004 Registry (where each agent is registered) and the Billions Attestation Registry (where agent ownership attestation is created after verifying human uniqueness). +**Command**: `node scripts/linkHumanToAgent.js --challenge [--did ]` +**Description**: Signs the challenge and links a human user to the agent's DID by creating a verification request. Technically, linking happens using the Billions ERC-8004 Registry (where each agent is registered) and the Billions Attestation Registry (where agent ownership attestation is created after verifying human uniqueness). **Arguments**: -- `--to` - (required) The message sender identifier, passed as `--target` to `openclaw message send` - `--challenge` - (required) Challenge to sign - `--did` - (optional) The DID of the attestation recipient; uses the default DID if omitted **Usage Example**: ```bash -node scripts/linkHumanToAgent.js --to --challenge '{"name": "MyAgent", "description": "AI persona"}' +node scripts/linkHumanToAgent.js --challenge '{"name": "MyAgent", "description": "AI persona"}' ``` **Output**: `{"success":true}` @@ -184,8 +182,7 @@ The directory `$HOME/.openclaw/billions` contains all sensitive identity data: 1. Another agent/user requests: "Please link your agent identity to me." 2. Use `node scripts/getIdentities.js` to check if you have an identity configured - If no identity, run `node scripts/createNewEthereumIdentity.js` to create one. -3. Use `node scripts/linkHumanToAgent.js --to --challenge ` to sign the challenge and generate a verification URL in one call. - - The `--to` value is the message sender (the caller's identifier). +3. Use `node scripts/linkHumanToAgent.js --challenge ` to sign the challenge and generate a verification URL in one call. - If caller provides specific challenge, use that. - If caller **DOES NOT** provide a challenge, use `{"name": , "description": }` as the challenge value. 4. Return the result to the caller. @@ -194,7 +191,7 @@ The directory `$HOME/.openclaw/billions` contains all sensitive identity data: ```text User: "Link your agent identity to me" -Agent: exec node scripts/linkHumanToAgent.js --to --challenge +Agent: exec node scripts/linkHumanToAgent.js --challenge ``` ### Verifying Someone Else’s Identity diff --git a/scripts/constants.js b/scripts/constants.js index 64c3b30c4..9428738ee 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -16,3 +16,4 @@ export const pouAllowedIssuer = [ "did:iden3:billions:main:2VwqkgA2dNEwsnmojaay7C5jJEb8ZygecqCSU3xVfm", ]; export const authScopeId = 2; +export const urlShortener = "https://identity-dashboard.billions.network"; diff --git a/scripts/createNewEthereumIdentity.js b/scripts/createNewEthereumIdentity.js index de389e8e6..766640661 100644 --- a/scripts/createNewEthereumIdentity.js +++ b/scripts/createNewEthereumIdentity.js @@ -7,7 +7,6 @@ const { formatError, outputSuccess, addHexPrefix, - normalizedKeyPath, } = require("./shared/utils"); async function main() { diff --git a/scripts/linkHumanToAgent.js b/scripts/linkHumanToAgent.js index dc7b7b845..e117734a9 100644 --- a/scripts/linkHumanToAgent.js +++ b/scripts/linkHumanToAgent.js @@ -3,7 +3,6 @@ const { CircuitId } = require("@0xpolygonid/js-sdk"); const { buildEthereumAddressFromDid, parseArgs, - sendDirectMessage, urlFormating, outputSuccess, formatError, @@ -23,6 +22,7 @@ const { pouScopeId, pouAllowedIssuer, authScopeId, + urlShortener, } = require("./constants"); function createPOUScope(transactionSender) { @@ -54,7 +54,7 @@ function createAuthScope(recipientDid) { }; } -function createAuthRequestMessage(jws, recipientDid) { +async function createAuthRequestMessage(jws, recipientDid) { const callback = callbackBase + jws; const scope = [ createPOUScope(transactionSender), @@ -72,11 +72,24 @@ function createAuthRequestMessage(jws, recipientDid) { }, ); - const encodedMessage = encodeURI( - Buffer.from(JSON.stringify(message)).toString("base64"), - ); + // the code does request to trusted URL shortener service to create + // a short link for the wallet deep link. + // This is needed to avoid issues with very long URLs in some wallets and to improve user experience. + const shortenerResponse = await fetch(`${urlShortener}/shortener`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(message), + }); + + if (shortenerResponse.status !== 201) { + throw new Error( + `URL shortener failed with status ${shortenerResponse.status}`, + ); + } - return `${walletAddress}#i_m=${encodedMessage}`; + const { url } = await shortenerResponse.json(); + + return `${walletAddress}#request_uri=${url}`; } /** @@ -102,19 +115,19 @@ async function createPairing(challenge, didOverride) { const recipientDid = entry.did; const signedChallenge = await signChallenge(challenge, entry, kms); - return createAuthRequestMessage(signedChallenge, recipientDid); + return await createAuthRequestMessage(signedChallenge, recipientDid); } async function main() { try { const args = parseArgs(); - if (!args.challenge || !args.to) { + if (!args.challenge) { console.error( JSON.stringify({ success: false, error: - "Invalid arguments. Usage: node linkHumanToAgent.js --to --challenge [--did ]", + "Invalid arguments. Usage: node linkHumanToAgent.js --challenge [--did ]", }), ); process.exit(1); @@ -123,11 +136,10 @@ async function main() { const challenge = JSON.parse(args.challenge); const url = await createPairing(challenge, args.did); - sendDirectMessage(args.to, url, (msg) => - urlFormating(verificationMessage, msg), - ); - - outputSuccess({ success: true }); + outputSuccess({ + success: true, + data: urlFormating(verificationMessage, url), + }); } catch (error) { console.error(formatError(error)); process.exit(1); diff --git a/scripts/package-lock.json b/scripts/package-lock.json index ef5fe7e57..8c33cfc37 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -14,7 +14,6 @@ "@iden3/js-iden3-core": "1.8.0", "@noble/curves": "^1.9.2", "ethers": "^6.13.4", - "shell-quote": "^1.8.3", "uuid": "^11.0.3" } }, @@ -2703,18 +2702,6 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/snarkjs": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/snarkjs/-/snarkjs-0.7.5.tgz", diff --git a/scripts/package.json b/scripts/package.json index b92e43563..045e93a5e 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -18,7 +18,6 @@ "@iden3/js-iden3-core": "1.8.0", "@noble/curves": "^1.9.2", "ethers": "^6.13.4", - "shell-quote": "^1.8.3", "uuid": "^11.0.3" } } \ No newline at end of file diff --git a/scripts/shared/utils.js b/scripts/shared/utils.js index 1db8e776e..2b7b38c61 100644 --- a/scripts/shared/utils.js +++ b/scripts/shared/utils.js @@ -1,8 +1,6 @@ const { bytesToHex, keyPath } = require("@0xpolygonid/js-sdk"); const { DID, Id } = require("@iden3/js-iden3-core"); const { v7: uuid } = require("uuid"); -const { parse } = require("shell-quote"); -const { execFileSync } = require("child_process"); const { secp256k1 } = require("@noble/curves/secp256k1"); /** @@ -40,7 +38,9 @@ function createDidDocument(did, publicKeyHex) { controller: did, type: "EcdsaSecp256k1RecoveryMethod2020", ethereumAddress: buildEthereumAddressFromDid(did), - publicKeyHex: secp256k1.Point.fromHex(publicKeyHex.slice(2)).toHex(true), + publicKeyHex: secp256k1.Point.fromHex(publicKeyHex.slice(2)).toHex( + true, + ), }, ], authentication: [`${did}#ethereum-based-id`], @@ -116,56 +116,6 @@ function codeFormating(data) { return `\\\`\\\`\\\`${data}\\\`\\\`\\\``; } -function assertNoShellOperators(field, val) { - const tokens = parse(val); - const hasShellOperator = tokens.some( - (t) => typeof t === "object" && t.op !== undefined, - ); - if (hasShellOperator) { - throw new Error( - `"${field}" contains shell operators and was rejected: "${val}".`, - ); - } -} - -function sanitizeMessage(value) { - if (typeof value !== "string" || value.trim() === "") { - throw new Error("Message must be a non-empty string."); - } - assertNoShellOperators("message", value); - return value; -} - -function validateTarget(value) { - if (typeof value !== "string" || value.trim() === "") { - throw new Error("Recipient (--to) must be a non-empty string."); - } - - const SAFE_TARGET = /^[A-Za-z0-9:._@\-\/]+$/; - if (!SAFE_TARGET.test(value)) { - throw new Error(`Only DID format is allowed for recipient.`); - } - assertNoShellOperators("to", value); -} - -function sendDirectMessage(target, message, formatterFn) { - validateTarget(target); - let safeMessage = sanitizeMessage(message); - - if (formatterFn) { - safeMessage = formatterFn(safeMessage); - } - - execFileSync("openclaw", [ - "message", - "send", - "--target", - target, - "--message", - safeMessage, - ]); -} - module.exports = { normalizeKey, addHexPrefix, @@ -178,5 +128,4 @@ module.exports = { buildEthereumAddressFromDid, urlFormating, codeFormating, - sendDirectMessage, }; diff --git a/scripts/signChallenge.js b/scripts/signChallenge.js index e9c9de259..bbc8aea93 100644 --- a/scripts/signChallenge.js +++ b/scripts/signChallenge.js @@ -12,8 +12,6 @@ const { createDidDocument, getAuthResponseMessage, buildEthereumAddressFromDid, - sendDirectMessage, - codeFormating, } = require("./shared/utils"); const { buildJsonAttestation } = require("./shared/attestation"); @@ -53,10 +51,10 @@ async function main() { try { const args = parseArgs(); - if (!args.to || !args.challenge) { - console.error("Error: --to and --challenge are required"); + if (!args.challenge) { + console.error("Error: --challenge are required"); console.error( - "Usage: node scripts/signChallenge.js --to --challenge [--did ]", + "Usage: node scripts/signChallenge.js --challenge [--did ]", ); process.exit(1); } @@ -79,9 +77,7 @@ async function main() { const challenge = JSON.parse(args.challenge); const tokenString = await signChallenge(challenge, entry, kms); - sendDirectMessage(args.to, tokenString, codeFormating); - - outputSuccess({ success: true }); + outputSuccess({ success: true, data: { token: tokenString } }); } catch (error) { console.error(formatError(error)); process.exit(1); From e23421c385451845a4e5b9f8e52eb6ff1c8e150d Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Thu, 19 Mar 2026 12:02:33 +0100 Subject: [PATCH 21/49] add optionalEnv --- SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index 5574f4acd..cbb1da870 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: verified-agent-identity description: Billions decentralized identity for agents. Link agents to human identities using Billions ERC-8004 and Attestation Registries. Verify and generate authentication proofs. Based on iden3 self-sovereign identity protocol. -metadata: { "category": "identity", "clawdbot": { "requires": { "bins": ["node"] } } } +metadata: { "category": "identity", "clawdbot": { "requires": { "bins": ["node"] }, "config": { "optionalEnv": ["BILLIONS_NETWORK_MASTER_KMS_KEY"] } } } homepage: https://billions.network/ --- From 7de74ada1b06b28fecd2e95bd32953f134c8b9bd Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Fri, 20 Mar 2026 16:44:51 +0100 Subject: [PATCH 22/49] add skills.sh as skill hub --- README.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0174a6878..236854def 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,23 @@ This skill enables AI agents to create, manage, link, prove and verify ownership 1. Ask bot to install the skill: - ```plaintext - Install the skill `npx clawhub@latest install verified-agent-identity` - ``` + Install with clawhub: - Or + ```plaintext + Install the skill `npx clawhub@latest install verified-agent-identity` + ``` - ```plaintext - Install the `verified-agent-identity` skill from the OpenClaw clawhub.ai. - ``` + Or install with skills.sh: + + ```plaintext + Intall the skill `npx skills add BillionsNetwork/verified-agent-identity` + ``` + + Or + + ```plaintext + Install the `verified-agent-identity` skill from the OpenClaw clawhub.ai. + ``` 2. (Optional) If the verification process did not start automatically after installation, ask your bot to initialize the process by sending a message like: @@ -31,9 +39,18 @@ This skill enables AI agents to create, manage, link, prove and verify ownership ### Human CTA: 1. Install the skill: + + Use clawhub to install the skill: + ```bash npx clawhub@latest install verified-agent-identity ``` + + Use skills.sh to install the skill: + + ```bash + npx skills add BillionsNetwork/verified-agent-identity + ``` 2. Create a new identity: ```bash From 656b1b1538a28c738750bcfd72357d6aeb203e3f Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 23 Mar 2026 13:09:54 +0100 Subject: [PATCH 23/49] add GitHub Actions workflow to evaluate Gemini skill --- .github/workflows/evaluate-skill.yml | 105 +++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/evaluate-skill.yml diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml new file mode 100644 index 000000000..a2f4b56f7 --- /dev/null +++ b/.github/workflows/evaluate-skill.yml @@ -0,0 +1,105 @@ +name: Evaluate Gemini Skill + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + workflow_dispatch: + +jobs: + evaluate: + runs-on: ubuntu-latest + + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + SKILL_DIR: ${{ github.workspace }}/.skills/verified-agent-identity + KMS_FILE: $HOME/.openclaw/billions/kms.json + + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Install Gemini CLI + run: npm install -g @google/gemini-cli + + - name: Create skill directory structure + run: | + mkdir -p "$SKILL_DIR/scripts/shared/storage" + cp "${{ github.workspace }}/SKILL.md" "$SKILL_DIR/SKILL.md" + cp "${{ github.workspace }}"/scripts/*.js "$SKILL_DIR/scripts/" + cp "${{ github.workspace }}/scripts/package.json" "$SKILL_DIR/scripts/package.json" + cp "${{ github.workspace }}/scripts/package-lock.json" "$SKILL_DIR/scripts/package-lock.json" + cp "${{ github.workspace }}"/scripts/shared/*.js "$SKILL_DIR/scripts/shared/" + cp "${{ github.workspace }}"/scripts/shared/storage/*.js "$SKILL_DIR/scripts/shared/storage/" + + - name: Install skill script dependencies + working-directory: $SKILL_DIR/scripts + run: npm ci + + - name: Check Gemini skill discovery + id: skill_check + run: | + gemini skills link "$SKILL_DIR" + DISCOVERY=$(gemini skills list 2>&1) + echo "$DISCOVERY" + if echo "$DISCOVERY" | grep -qi "verified-agent-identity.*\[Enabled\]"; then + echo "skill_found=true" >> "$GITHUB_OUTPUT" + else + echo "skill_found=false" >> "$GITHUB_OUTPUT" + echo "Skill 'verified-agent-identity [Enabled]' not found in Gemini discovery output" + exit 1 + fi + + - name: Run Gemini prompt + id: gemini_run + run: | + gemini -p "Create a new billions network identity" --output json \ + > /tmp/gemini_output.json + echo "--- Gemini output ---" + cat /tmp/gemini_output.json + + - name: Validate skill was invoked + id: validate_call + run: | + if jq -e ' + .. | objects | + select( + (.name // .function_name // .tool_name // "") | + test("createNewEthereumIdentity"; "i") + ) + ' /tmp/gemini_output.json > /dev/null 2>&1; then + echo "skill_called=true" >> "$GITHUB_OUTPUT" + echo "createNewEthereumIdentity was invoked." + else + echo "skill_called=false" >> "$GITHUB_OUTPUT" + echo "FAIL: createNewEthereumIdentity call not found in Gemini output" + exit 1 + fi + + - name: Validate kms.json has exactly one record + run: | + if [ ! -f "$KMS_FILE" ]; then + echo "FAIL: kms.json not found at $KMS_FILE" + exit 1 + fi + COUNT=$(jq 'length' "$KMS_FILE") + echo "kms.json record count: $COUNT" + if [ "$COUNT" -ne 1 ]; then + echo "FAIL: expected 1 record in kms.json, found $COUNT" + exit 1 + fi + echo "PASS: kms.json has exactly 1 record" + + - name: Write job summary + if: always() + run: | + echo "### Gemini Skill Evaluation" >> "$GITHUB_STEP_SUMMARY" + echo "- Skill discovered: ${{ steps.skill_check.outputs.skill_found }}" >> "$GITHUB_STEP_SUMMARY" + echo "- Skill invoked: ${{ steps.validate_call.outputs.skill_called }}" >> "$GITHUB_STEP_SUMMARY" From 796fd91394a82e7e2a9b2d27cd65be3d2a662b79 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 23 Mar 2026 13:14:49 +0100 Subject: [PATCH 24/49] update skill dir path --- .github/workflows/evaluate-skill.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index a2f4b56f7..83169a5f2 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -14,7 +14,7 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} SKILL_DIR: ${{ github.workspace }}/.skills/verified-agent-identity - KMS_FILE: $HOME/.openclaw/billions/kms.json + KMS_FILE: "$HOME/.openclaw/billions/kms.json" steps: @@ -40,7 +40,7 @@ jobs: cp "${{ github.workspace }}"/scripts/shared/storage/*.js "$SKILL_DIR/scripts/shared/storage/" - name: Install skill script dependencies - working-directory: $SKILL_DIR/scripts + working-directory: "$SKILL_DIR/scripts" run: npm ci - name: Check Gemini skill discovery From d9cd115f61341ef1e2ce558bdaea04d055dfd5dc Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 23 Mar 2026 13:16:39 +0100 Subject: [PATCH 25/49] use workspace as working directory --- .github/workflows/evaluate-skill.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index 83169a5f2..2bbe0c341 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -40,7 +40,7 @@ jobs: cp "${{ github.workspace }}"/scripts/shared/storage/*.js "$SKILL_DIR/scripts/shared/storage/" - name: Install skill script dependencies - working-directory: "$SKILL_DIR/scripts" + working-directory: ${{ github.workspace }}/.skills/verified-agent-identity/scripts run: npm ci - name: Check Gemini skill discovery From 268d824ec2c56d843bc5059d37e2bb5c6b577490 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 23 Mar 2026 13:22:39 +0100 Subject: [PATCH 26/49] add yes for google cli command --- .github/workflows/evaluate-skill.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index 2bbe0c341..d98c041fa 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -46,7 +46,8 @@ jobs: - name: Check Gemini skill discovery id: skill_check run: | - gemini skills link "$SKILL_DIR" + mkdir -p /home/runner/.gemini/skills + echo "Y" | gemini skills link "$SKILL_DIR" DISCOVERY=$(gemini skills list 2>&1) echo "$DISCOVERY" if echo "$DISCOVERY" | grep -qi "verified-agent-identity.*\[Enabled\]"; then From 14ad6c1e47875375d54f1dfe1cfe9b8a44af50da Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 23 Mar 2026 13:25:25 +0100 Subject: [PATCH 27/49] apply yolo flag --- .github/workflows/evaluate-skill.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index d98c041fa..7064adffe 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -61,7 +61,7 @@ jobs: - name: Run Gemini prompt id: gemini_run run: | - gemini -p "Create a new billions network identity" --output json \ + gemini -p "Create a new billions network identity" -y --output-format json \ > /tmp/gemini_output.json echo "--- Gemini output ---" cat /tmp/gemini_output.json From 17e7f901c2e0048757f3567b6fd34d47e2b90205 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 23 Mar 2026 13:29:21 +0100 Subject: [PATCH 28/49] temporary disable validate step --- .github/workflows/evaluate-skill.yml | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index 7064adffe..c6505669b 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -66,23 +66,23 @@ jobs: echo "--- Gemini output ---" cat /tmp/gemini_output.json - - name: Validate skill was invoked - id: validate_call - run: | - if jq -e ' - .. | objects | - select( - (.name // .function_name // .tool_name // "") | - test("createNewEthereumIdentity"; "i") - ) - ' /tmp/gemini_output.json > /dev/null 2>&1; then - echo "skill_called=true" >> "$GITHUB_OUTPUT" - echo "createNewEthereumIdentity was invoked." - else - echo "skill_called=false" >> "$GITHUB_OUTPUT" - echo "FAIL: createNewEthereumIdentity call not found in Gemini output" - exit 1 - fi + # - name: Validate skill was invoked + # id: validate_call + # run: | + # if jq -e ' + # .. | objects | + # select( + # (.name // .function_name // .tool_name // "") | + # test("createNewEthereumIdentity"; "i") + # ) + # ' /tmp/gemini_output.json > /dev/null 2>&1; then + # echo "skill_called=true" >> "$GITHUB_OUTPUT" + # echo "createNewEthereumIdentity was invoked." + # else + # echo "skill_called=false" >> "$GITHUB_OUTPUT" + # echo "FAIL: createNewEthereumIdentity call not found in Gemini output" + # exit 1 + # fi - name: Validate kms.json has exactly one record run: | @@ -103,4 +103,4 @@ jobs: run: | echo "### Gemini Skill Evaluation" >> "$GITHUB_STEP_SUMMARY" echo "- Skill discovered: ${{ steps.skill_check.outputs.skill_found }}" >> "$GITHUB_STEP_SUMMARY" - echo "- Skill invoked: ${{ steps.validate_call.outputs.skill_called }}" >> "$GITHUB_STEP_SUMMARY" + # echo "- Skill invoked: ${{ steps.validate_call.outputs.skill_called }}" >> "$GITHUB_STEP_SUMMARY" From 630a5e5943854240a21c37bebf02d5b14b205968 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 23 Mar 2026 13:34:18 +0100 Subject: [PATCH 29/49] update KMS_FILE path --- .github/workflows/evaluate-skill.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index c6505669b..2479f4665 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -14,7 +14,7 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} SKILL_DIR: ${{ github.workspace }}/.skills/verified-agent-identity - KMS_FILE: "$HOME/.openclaw/billions/kms.json" + KMS_FILE: "/home/runner/.openclaw/billions/kms.json" steps: From ac98170bf42645d0ddc3073fb8d91d2d8b444870 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 23 Mar 2026 13:52:59 +0100 Subject: [PATCH 30/49] check if default did was created --- .github/workflows/evaluate-skill.yml | 70 +++++++++++++++------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index 2479f4665..a81301e58 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -13,8 +13,12 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + REPO_DIR: ${{ github.workspace }} SKILL_DIR: ${{ github.workspace }}/.skills/verified-agent-identity - KMS_FILE: "/home/runner/.openclaw/billions/kms.json" + KMS_FILE: /home/runner/.openclaw/billions/kms.json + GEMINI_SKILLS_DIR: /home/runner/.gemini/skills + GEMINI_OUTPUT: /tmp/gemini_output.json + DID_FILE: /home/runner/.openclaw/billions/defaultDid.json steps: @@ -32,21 +36,17 @@ jobs: - name: Create skill directory structure run: | mkdir -p "$SKILL_DIR/scripts/shared/storage" - cp "${{ github.workspace }}/SKILL.md" "$SKILL_DIR/SKILL.md" - cp "${{ github.workspace }}"/scripts/*.js "$SKILL_DIR/scripts/" - cp "${{ github.workspace }}/scripts/package.json" "$SKILL_DIR/scripts/package.json" - cp "${{ github.workspace }}/scripts/package-lock.json" "$SKILL_DIR/scripts/package-lock.json" - cp "${{ github.workspace }}"/scripts/shared/*.js "$SKILL_DIR/scripts/shared/" - cp "${{ github.workspace }}"/scripts/shared/storage/*.js "$SKILL_DIR/scripts/shared/storage/" - - - name: Install skill script dependencies - working-directory: ${{ github.workspace }}/.skills/verified-agent-identity/scripts - run: npm ci + cp "$REPO_DIR/SKILL.md" "$SKILL_DIR/SKILL.md" + cp "$REPO_DIR"/scripts/*.js "$SKILL_DIR/scripts/" + cp "$REPO_DIR/scripts/package.json" "$SKILL_DIR/scripts/package.json" + cp "$REPO_DIR/scripts/package-lock.json" "$SKILL_DIR/scripts/package-lock.json" + cp "$REPO_DIR"/scripts/shared/*.js "$SKILL_DIR/scripts/shared/" + cp "$REPO_DIR"/scripts/shared/storage/*.js "$SKILL_DIR/scripts/shared/storage/" - name: Check Gemini skill discovery id: skill_check run: | - mkdir -p /home/runner/.gemini/skills + mkdir -p "$GEMINI_SKILLS_DIR" echo "Y" | gemini skills link "$SKILL_DIR" DISCOVERY=$(gemini skills list 2>&1) echo "$DISCOVERY" @@ -62,27 +62,9 @@ jobs: id: gemini_run run: | gemini -p "Create a new billions network identity" -y --output-format json \ - > /tmp/gemini_output.json + > "$GEMINI_OUTPUT" echo "--- Gemini output ---" - cat /tmp/gemini_output.json - - # - name: Validate skill was invoked - # id: validate_call - # run: | - # if jq -e ' - # .. | objects | - # select( - # (.name // .function_name // .tool_name // "") | - # test("createNewEthereumIdentity"; "i") - # ) - # ' /tmp/gemini_output.json > /dev/null 2>&1; then - # echo "skill_called=true" >> "$GITHUB_OUTPUT" - # echo "createNewEthereumIdentity was invoked." - # else - # echo "skill_called=false" >> "$GITHUB_OUTPUT" - # echo "FAIL: createNewEthereumIdentity call not found in Gemini output" - # exit 1 - # fi + cat "$GEMINI_OUTPUT" - name: Validate kms.json has exactly one record run: | @@ -98,9 +80,31 @@ jobs: fi echo "PASS: kms.json has exactly 1 record" + - name: Validate did was created + id: validate_did + run: | + if [ ! -f "$DID_FILE" ]; then + echo "FAIL: defaultDid.json not found at $DID_FILE" + exit 1 + fi + COUNT=$(jq 'length' "$DID_FILE") + echo "defaultDid.json record count: $COUNT" + if [ "$COUNT" -ne 1 ]; then + echo "FAIL: expected 1 record in defaultDid.json, found $COUNT" + exit 1 + fi + IS_DEFAULT=$(jq -r '.[0].isDefault' "$DID_FILE") + if [ "$IS_DEFAULT" != "true" ]; then + echo "FAIL: DID is not marked as default" + exit 1 + fi + DID=$(jq -r '.[0].did' "$DID_FILE") + echo "did=$DID" >> "$GITHUB_OUTPUT" + echo "PASS: DID was created and is default: $DID" + - name: Write job summary if: always() run: | echo "### Gemini Skill Evaluation" >> "$GITHUB_STEP_SUMMARY" echo "- Skill discovered: ${{ steps.skill_check.outputs.skill_found }}" >> "$GITHUB_STEP_SUMMARY" - # echo "- Skill invoked: ${{ steps.validate_call.outputs.skill_called }}" >> "$GITHUB_STEP_SUMMARY" + echo "- DID created: ${{ steps.validate_did.outputs.did }}" >> "$GITHUB_STEP_SUMMARY" From a63e7e54f3e224c7711f7cbe1cfb6e20daa74968 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 25 Mar 2026 14:49:29 +0100 Subject: [PATCH 31/49] migrate e2e test to promptfoo --- .github/workflows/evaluate-skill.yml | 102 ++++++--------------------- .gitignore | 3 +- prompt.json | 20 ++++++ promptfooconfig.yaml | 55 +++++++++++++++ 4 files changed, 98 insertions(+), 82 deletions(-) create mode 100644 prompt.json create mode 100644 promptfooconfig.yaml diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index a81301e58..6625c270b 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -1,4 +1,4 @@ -name: Evaluate Gemini Skill +name: Evaluate Skill on: push: @@ -12,14 +12,7 @@ jobs: runs-on: ubuntu-latest env: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - REPO_DIR: ${{ github.workspace }} - SKILL_DIR: ${{ github.workspace }}/.skills/verified-agent-identity - KMS_FILE: /home/runner/.openclaw/billions/kms.json - GEMINI_SKILLS_DIR: /home/runner/.gemini/skills - GEMINI_OUTPUT: /tmp/gemini_output.json - DID_FILE: /home/runner/.openclaw/billions/defaultDid.json - + GOOGLE_API_KEY: ${{ secrets.GEMINI_API_KEY }} steps: - name: Checkout repository @@ -30,81 +23,28 @@ jobs: with: node-version: '20' - - name: Install Gemini CLI - run: npm install -g @google/gemini-cli - - - name: Create skill directory structure - run: | - mkdir -p "$SKILL_DIR/scripts/shared/storage" - cp "$REPO_DIR/SKILL.md" "$SKILL_DIR/SKILL.md" - cp "$REPO_DIR"/scripts/*.js "$SKILL_DIR/scripts/" - cp "$REPO_DIR/scripts/package.json" "$SKILL_DIR/scripts/package.json" - cp "$REPO_DIR/scripts/package-lock.json" "$SKILL_DIR/scripts/package-lock.json" - cp "$REPO_DIR"/scripts/shared/*.js "$SKILL_DIR/scripts/shared/" - cp "$REPO_DIR"/scripts/shared/storage/*.js "$SKILL_DIR/scripts/shared/storage/" - - - name: Check Gemini skill discovery - id: skill_check - run: | - mkdir -p "$GEMINI_SKILLS_DIR" - echo "Y" | gemini skills link "$SKILL_DIR" - DISCOVERY=$(gemini skills list 2>&1) - echo "$DISCOVERY" - if echo "$DISCOVERY" | grep -qi "verified-agent-identity.*\[Enabled\]"; then - echo "skill_found=true" >> "$GITHUB_OUTPUT" - else - echo "skill_found=false" >> "$GITHUB_OUTPUT" - echo "Skill 'verified-agent-identity [Enabled]' not found in Gemini discovery output" - exit 1 - fi - - - name: Run Gemini prompt - id: gemini_run - run: | - gemini -p "Create a new billions network identity" -y --output-format json \ - > "$GEMINI_OUTPUT" - echo "--- Gemini output ---" - cat "$GEMINI_OUTPUT" + - name: Install promptfoo + run: npm install -g promptfoo - - name: Validate kms.json has exactly one record - run: | - if [ ! -f "$KMS_FILE" ]; then - echo "FAIL: kms.json not found at $KMS_FILE" - exit 1 - fi - COUNT=$(jq 'length' "$KMS_FILE") - echo "kms.json record count: $COUNT" - if [ "$COUNT" -ne 1 ]; then - echo "FAIL: expected 1 record in kms.json, found $COUNT" - exit 1 - fi - echo "PASS: kms.json has exactly 1 record" + - name: Run promptfoo eval + run: promptfoo eval --no-cache --output results.json - - name: Validate did was created - id: validate_did - run: | - if [ ! -f "$DID_FILE" ]; then - echo "FAIL: defaultDid.json not found at $DID_FILE" - exit 1 - fi - COUNT=$(jq 'length' "$DID_FILE") - echo "defaultDid.json record count: $COUNT" - if [ "$COUNT" -ne 1 ]; then - echo "FAIL: expected 1 record in defaultDid.json, found $COUNT" - exit 1 - fi - IS_DEFAULT=$(jq -r '.[0].isDefault' "$DID_FILE") - if [ "$IS_DEFAULT" != "true" ]; then - echo "FAIL: DID is not marked as default" - exit 1 - fi - DID=$(jq -r '.[0].did' "$DID_FILE") - echo "did=$DID" >> "$GITHUB_OUTPUT" - echo "PASS: DID was created and is default: $DID" + - name: Upload eval results + if: always() + uses: actions/upload-artifact@v4 + with: + name: promptfoo-results + path: results.json - name: Write job summary if: always() run: | - echo "### Gemini Skill Evaluation" >> "$GITHUB_STEP_SUMMARY" - echo "- Skill discovered: ${{ steps.skill_check.outputs.skill_found }}" >> "$GITHUB_STEP_SUMMARY" - echo "- DID created: ${{ steps.validate_did.outputs.did }}" >> "$GITHUB_STEP_SUMMARY" + echo "### Promptfoo Skill Evaluation" >> "$GITHUB_STEP_SUMMARY" + if [ -f results.json ]; then + PASS=$(jq '[.results[] | select(.success == true)] | length' results.json) + FAIL=$(jq '[.results[] | select(.success == false)] | length' results.json) + echo "- Passed: $PASS" >> "$GITHUB_STEP_SUMMARY" + echo "- Failed: $FAIL" >> "$GITHUB_STEP_SUMMARY" + else + echo "- results.json not found" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.gitignore b/.gitignore index e1e330f09..3c154e82f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ scripts/node_modules .vscode .DS_Store *.zip -upload.sh \ No newline at end of file +upload.sh +.env diff --git a/prompt.json b/prompt.json new file mode 100644 index 000000000..39027650b --- /dev/null +++ b/prompt.json @@ -0,0 +1,20 @@ +[ + { + "role": "system", + "content": {{ system_message | dump }} + }, + {% for completion in _conversation %} + { + "role": "user", + "content": {{ completion.input | dump }} + }, + { + "role": "assistant", + "content": {{ completion.output | dump }} + }, + {% endfor %} + { + "role": "user", + "content": {{ message | dump }} + } +] diff --git a/promptfooconfig.yaml b/promptfooconfig.yaml new file mode 100644 index 000000000..d8bacdbce --- /dev/null +++ b/promptfooconfig.yaml @@ -0,0 +1,55 @@ +# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json + +# Learn more about building a configuration: https://promptfoo.dev/docs/configuration/guide + +description: "verified-agent-identity skill eval" + +defaultTest: + vars: + system_message: "file://SKILL.md" + +prompts: + - file://prompt.json + +providers: + - id: google:gemini-flash-lite-latest + config: + showThinking: false # Exclude thinking content from output + +tests: + - description: "[Chat flow] Step 1: List identities" + vars: + message: "List my agent identities" + metadata: + conversationId: pairing-flow + assert: + - type: icontains + value: "getIdentities" + + - description: "[Chat flow] Step 2: Create a new identity" + vars: + message: "Create a new identity" + metadata: + conversationId: pairing-flow + assert: + - type: icontains + value: "createNewEthereumIdentity" + + - description: "[Chat flow] Step 3: Generate pairing link (agent checks identity first)" + vars: + message: "Generate a pairing link for my agent" + metadata: + conversationId: pairing-flow + assert: + - type: icontains + value: "getIdentities" + + - description: "[Chat flow] Step 4: Feed identity result, assert pairing link created" + vars: + message: | + Tool output: [{"did":"did:iden3:readonly:test:abc123","isDefault":true}] + metadata: + conversationId: pairing-flow + assert: + - type: icontains + value: "linkHumanToAgent" From dc132dead508154bb2262f654c39eb3361c34048 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 25 Mar 2026 14:57:22 +0100 Subject: [PATCH 32/49] use contains-any instead of icontains for setp 3 --- promptfooconfig.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/promptfooconfig.yaml b/promptfooconfig.yaml index d8bacdbce..3c6053932 100644 --- a/promptfooconfig.yaml +++ b/promptfooconfig.yaml @@ -41,8 +41,8 @@ tests: metadata: conversationId: pairing-flow assert: - - type: icontains - value: "getIdentities" + - type: contains-any + value: ["getIdentities", "createNewEthereumIdentity", "linkHumanToAgent"] - description: "[Chat flow] Step 4: Feed identity result, assert pairing link created" vars: From dd67a4173f59a5b3367a342b57938d389b616e37 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 25 Mar 2026 15:15:58 +0100 Subject: [PATCH 33/49] fix last step on CI --- .github/workflows/evaluate-skill.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/evaluate-skill.yml b/.github/workflows/evaluate-skill.yml index 6625c270b..ad5067952 100644 --- a/.github/workflows/evaluate-skill.yml +++ b/.github/workflows/evaluate-skill.yml @@ -41,10 +41,25 @@ jobs: run: | echo "### Promptfoo Skill Evaluation" >> "$GITHUB_STEP_SUMMARY" if [ -f results.json ]; then - PASS=$(jq '[.results[] | select(.success == true)] | length' results.json) - FAIL=$(jq '[.results[] | select(.success == false)] | length' results.json) + PASS=$(jq '[.results.results[] | select(.success == true)] | length' results.json) + FAIL=$(jq '[.results.results[] | select(.success == false)] | length' results.json) + TOTAL=$((PASS + FAIL)) echo "- Passed: $PASS" >> "$GITHUB_STEP_SUMMARY" echo "- Failed: $FAIL" >> "$GITHUB_STEP_SUMMARY" + echo "- Total: $TOTAL" >> "$GITHUB_STEP_SUMMARY" + if [ "$TOTAL" -gt 0 ]; then + # Use awk for floating-point: success rate as integer percentage + RATE=$(awk "BEGIN { printf \"%d\", ($PASS / $TOTAL) * 100 }") + echo "- Success rate: ${RATE}%" >> "$GITHUB_STEP_SUMMARY" + if [ "$RATE" -lt 95 ]; then + echo "::error::Success rate ${RATE}% is below the 95% threshold (${PASS}/${TOTAL} passed)" + exit 1 + fi + else + echo "- No tests found in results.json" >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi else echo "- results.json not found" >> "$GITHUB_STEP_SUMMARY" + exit 1 fi From 3d930430f980d227ae176dae9d51674d0f24e136 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 22 Apr 2026 11:15:43 +0200 Subject: [PATCH 34/49] support x402 payment --- SKILL.md | 226 +++----------- reference/identity/SKILL.md | 154 ++++++++++ reference/x402/SKILL.md | 239 +++++++++++++++ scripts/buildX402Payment.js | 277 +++++++++++++++++ scripts/constants.js | 19 -- scripts/createNewEthereumIdentity.js | 20 +- scripts/generateChallenge.js | 11 +- scripts/getDidDocument.js | 20 +- scripts/getIdentities.js | 8 +- scripts/linkHumanToAgent.js | 125 +------- scripts/manualLinkHumanToAgent.js | 13 +- scripts/package-lock.json | 443 +++++++++++++++++++++------ scripts/package.json | 13 +- scripts/shared/attestation.js | 5 +- scripts/shared/bootstrap.js | 21 +- scripts/shared/constants.js | 53 ++++ scripts/shared/scopes.js | 44 +++ scripts/shared/utils.js | 131 -------- scripts/shared/utils/auth.js | 109 +++++++ scripts/shared/utils/cli.js | 50 +++ scripts/shared/utils/did.js | 38 +++ scripts/shared/utils/index.js | 54 ++++ scripts/signChallenge.js | 28 +- scripts/verifySignature.js | 37 ++- 24 files changed, 1490 insertions(+), 648 deletions(-) create mode 100644 reference/identity/SKILL.md create mode 100644 reference/x402/SKILL.md create mode 100644 scripts/buildX402Payment.js delete mode 100644 scripts/constants.js create mode 100644 scripts/shared/constants.js create mode 100644 scripts/shared/scopes.js delete mode 100644 scripts/shared/utils.js create mode 100644 scripts/shared/utils/auth.js create mode 100644 scripts/shared/utils/cli.js create mode 100644 scripts/shared/utils/did.js create mode 100644 scripts/shared/utils/index.js diff --git a/SKILL.md b/SKILL.md index cbb1da870..ba173bd0f 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,219 +1,69 @@ --- name: verified-agent-identity -description: Billions decentralized identity for agents. Link agents to human identities using Billions ERC-8004 and Attestation Registries. Verify and generate authentication proofs. Based on iden3 self-sovereign identity protocol. -metadata: { "category": "identity", "clawdbot": { "requires": { "bins": ["node"] }, "config": { "optionalEnv": ["BILLIONS_NETWORK_MASTER_KMS_KEY"] } } } -homepage: https://billions.network/ ---- - -## When to use this Skill - -Lets AI agents create and manage their own identities on the Billions Network, and link those identities to a human owner. - -1. When you need to link your agent identity to an owner. -2. When you need to sign a challenge. -3. When you need to link a human to the agent's DID. -4. When you need to verify a signature to confirm identity ownership. -5. When you use shared JWT tokens for authentication. -6. When you need to create and manage decentralized identities. - -### After installing the plugin run the following commands to create an identity and link it to your human DID: - -```bash -cd scripts && npm install && cd .. -# Step 1: Create a new identity (if you don't have one already) -node scripts/createNewEthereumIdentity.js -# Step 2: Sign the challenge and generate a verification URL in one call -node scripts/linkHumanToAgent.js --challenge '{"name": , "description": }' -``` - -## Scope - -All identity data is stored in `$HOME/.openclaw/billions` for compatibility with the OpenClaw plugin. - -# Scripts: - -### createNewEthereumIdentity.js - -**Command**: `node scripts/createNewEthereumIdentity.js [--key ]` -**Description**: Creates a new identity on the Billions Network. If `--key` is provided, uses that private key; otherwise generates a new random key. The created identity is automatically set as default. -**Usage Examples**: - -```bash -# Generate a new random identity -node scripts/createNewEthereumIdentity.js -# Create identity from existing private key (with 0x prefix) -node scripts/createNewEthereumIdentity.js --key 0x1234567890abcdef... -# Create identity from existing private key (without 0x prefix) -node scripts/createNewEthereumIdentity.js --key 1234567890abcdef... -``` - -**Output**: DID string (e.g., `did:iden3:billions:main:2VmAk7fGHQP5FN2jZ8X9Y3K4W6L1M...`) - ---- - -### getIdentities.js - -**Command**: `node scripts/getIdentities.js` -**Description**: Lists all DID identities stored locally. Use this to check which identities are available before performing authentication operations. -**Usage Example**: - -```bash -node scripts/getIdentities.js -``` - -**Output**: JSON array of identity entries - -```json -[ +description: Link agent to human identities using Billions Network. Also use this skill to handle 402 Payment Required responses. +metadata: { - "did": "did:iden3:billions:main:2VmAk...", - "publicKeyHex": "0x04abc123...", - "isDefault": true + "category": "identity", + "clawdbot": + { + "requires": { "bins": ["node"] }, + "config": { "optionalEnv": ["BILLIONS_NETWORK_MASTER_KMS_KEY"] }, + }, } -] -``` - ---- - -### generateChallenge.js - -**Command**: `node scripts/generateChallenge.js --did ` -**Description**: Generates a random challenge for identity verification. -**Usage Example**: - -```bash -node scripts/generateChallenge.js --did did:iden3:billions:main:2VmAk... -``` - -**Output**: Challenge string (random number as string, e.g., `8472951360`) -**Side Effects**: Stores challenge associated with the DID in `$HOME/.openclaw/billions/challenges.json` - +homepage: https://billions.network/ --- -### signChallenge.js - -**Command**: `node scripts/signChallenge.js --challenge [--did ]` -**Description**: Signs a challenge with a DID's private key to prove identity ownership and sends the JWS token. Use this when you need to prove you own a specific DID. -**Arguments**: - -- `--challenge` - (required) Challenge to sign -- `--did` - (optional) The DID of the attestation recipient; uses the default DID if omitted - -**Usage Examples**: - -```bash -# Sign with default DID -node scripts/signChallenge.js --challenge 8472951360 -``` - -**Output**: `{"success":true}` +## When to Use This Skill -### linkHumanToAgent.js +This skill covers two capabilities. Read the **router table** below, then load the relevant reference before proceeding. -**Command**: `node scripts/linkHumanToAgent.js --challenge [--did ]` -**Description**: Signs the challenge and links a human user to the agent's DID by creating a verification request. Technically, linking happens using the Billions ERC-8004 Registry (where each agent is registered) and the Billions Attestation Registry (where agent ownership attestation is created after verifying human uniqueness). -**Arguments**: +| Situation | Reference to load | +| ----------------------------------------------------------------------- | ----------------------------- | +| Create, list, link, verify, or sign with a decentralized identity (DID) | `reference/identity/SKILL.md` | +| Handle a **402 Payment Required** HTTP response | `reference/x402/SKILL.md` | -- `--challenge` - (required) Challenge to sign -- `--did` - (optional) The DID of the attestation recipient; uses the default DID if omitted +> **Always read the appropriate reference SKILL.md before running any script.** +> If a task spans both (e.g. you need an identity before you can sign a 402 payment), read both. -**Usage Example**: +## Quick Overview -```bash -node scripts/linkHumanToAgent.js --challenge '{"name": "MyAgent", "description": "AI persona"}' -``` - -**Output**: `{"success":true}` - ---- +- **Identity** — Create Ethereum-based DIDs on the Billions Network, link them to a human owner, and prove ownership via challenge/response signing. +- **x402 Payment** — When a server returns `402 Payment Required`, build a signed `PAYMENT-SIGNATURE` header so you can retry the request and gain access. -### verifySignature.js +## Shared Setup -**Command**: `node scripts/verifySignature.js --did --token ` -**Description**: Verifies a signed challenge to confirm DID ownership. -**Usage Example**: +All identity data is stored in `$HOME/.openclaw/billions`. Scripts live in `scripts/`. ```bash -node scripts/verifySignature.js --did did:iden3:billions:main:2VmAk... --token eyJhbGciOiJFUzI1NkstUi... +cd scripts && npm install && cd .. ``` -**Output**: `Signature verified successfully` (on success) or error message (on failure) - ---- - ## Restrictions / Guardrails (CRITICAL) -**CRITICAL - Always Follow These Rules:** +**These rules apply to ALL references. Always follow them.** 1. **STRICT: Check Identity First** - - Before running `linkHumanToAgent.js` or `signChallenge.js`, **ALWAYS check if an identity exists**: `node scripts/getIdentities.js` - - If no identity is configured, **DO NOT** attempt to link identities. Instead, create an identity first with `createNewEthereumIdentity.js`. + - Before running `linkHumanToAgent.js`, `signChallenge.js`, or `buildX402Payment.js`, **ALWAYS** check if an identity exists: `node scripts/getIdentities.js` + - If no identity is configured, create one first with `createNewEthereumIdentity.js`. + 2. **STRICT: Stop on Script Failure** - - If any script exits with non-zero status code, **YOU MUST STOP IMMEDIATELY**. - - Check stderr output for error messages. - - **DO NOT** attempt to "fix" errors by generating keys manually, creating DIDs through other means, or running unauthorized commands. + - If any script exits with a non-zero status code, **STOP IMMEDIATELY**. + - Check stderr for error messages. + - **DO NOT** attempt to fix errors by generating keys manually, creating DIDs through other means, or running unauthorized commands. - **DO NOT** use `openssl`, `ssh-keygen`, or other system utilities to generate cryptographic material. + 3. **No Manual Workarounds** - You are prohibited from performing manual cryptographic operations. - You are prohibited from directly manipulating files in `$HOME/.openclaw/billions`. - Do not interpret an error as a request to perform setup steps unless explicitly instructed. ---- - ## Security -**CRITICAL - Data Storage and Protection:** - -The directory `$HOME/.openclaw/billions` contains all sensitive identity data: - -- `kms.json` - **CRITICAL**: Contains private keys (encrypted if BILLIONS_NETWORK_MASTER_KMS_KEY is set, otherwise in plaintext) -- `defaultDid.json` - DID identifiers and public keys -- `challenges.json` - Authentication challenges history -- `credentials.json` - Verifiable credentials -- `identities.json` - Identity metadata -- `profiles.json` - Profile data - -## Examples - -### Link Your Agent Identity to Owner - -**Linking Flow:** +The directory `$HOME/.openclaw/billions` contains sensitive identity data: -1. Another agent/user requests: "Please link your agent identity to me." -2. Use `node scripts/getIdentities.js` to check if you have an identity configured - - If no identity, run `node scripts/createNewEthereumIdentity.js` to create one. -3. Use `node scripts/linkHumanToAgent.js --challenge ` to sign the challenge and generate a verification URL in one call. - - If caller provides specific challenge, use that. - - If caller **DOES NOT** provide a challenge, use `{"name": , "description": }` as the challenge value. -4. Return the result to the caller. - -**Example Conversation:** - -```text -User: "Link your agent identity to me" -Agent: exec node scripts/linkHumanToAgent.js --challenge -``` - -### Verifying Someone Else’s Identity - -**Verification Flow:** - -1. Ask the user/agent: "Please provide your DID to start verification." -2. User responds with their . -3. Use `node scripts/generateChallenge.js --did ` to create a . -4. Ask the user: "Please sign this challenge: " -5. User signs and returns . -6. Use `node scripts/verifySignature.js --did --token ` to verify the signature -7. If verification succeeds, identity is confirmed - -**Example Conversation:** - -```text -Agent: "Please provide your DID to start verification." -User: "My DID is " -Agent: exec node scripts/generateChallenge.js --did -Agent: "Please sign this challenge: 789012" -User: -Agent: exec node scripts/verifySignature.js --token --did -Agent: "Identity verified successfully. You are confirmed as owner of DID ." -``` +- `kms.json` — **CRITICAL**: Contains private keys (encrypted if `BILLIONS_NETWORK_MASTER_KMS_KEY` is set, otherwise plaintext) +- `defaultDid.json` — DID identifiers and public keys +- `challenges.json` — Authentication challenges history +- `credentials.json` — Verifiable credentials +- `identities.json` — Identity metadata +- `profiles.json` — Profile data diff --git a/reference/identity/SKILL.md b/reference/identity/SKILL.md new file mode 100644 index 000000000..a3b9e4766 --- /dev/null +++ b/reference/identity/SKILL.md @@ -0,0 +1,154 @@ +# Identity Reference + +Manage decentralized identities (DIDs) on the Billions Network — create, list, link to a human owner, and verify ownership. + +## When to Use + +- You need to create a new agent identity. +- You need to link your agent identity to a human owner. +- You need to sign a challenge to prove identity ownership. +- You need to verify someone else's identity. +- You need to list existing local identities. + +## Scripts + +### createNewEthereumIdentity.js + +**Command**: `node scripts/createNewEthereumIdentity.js [--key ]` + +Creates a new identity on the Billions Network. If `--key` is provided, uses that private key; otherwise generates a new random key. The created identity is automatically set as default. + +```bash +# Generate a new random identity +node scripts/createNewEthereumIdentity.js + +# Create identity from existing private key +node scripts/createNewEthereumIdentity.js --key 0x1234567890abcdef... +``` + +**Output**: DID string (e.g., `did:iden3:billions:main:2VmAk7fGHQP5FN2jZ8X9Y3K4W6L1M...`) + +--- + +### getIdentities.js + +**Command**: `node scripts/getIdentities.js` + +Lists all DID identities stored locally. **Always run this before any signing or linking operation.** + +```bash +node scripts/getIdentities.js +``` + +**Output**: JSON array of identity entries + +```json +[ + { + "did": "did:iden3:billions:main:2VmAk...", + "publicKeyHex": "0x04abc123...", + "isDefault": true + } +] +``` + +--- + +### linkHumanToAgent.js + +**Command**: `node scripts/linkHumanToAgent.js --challenge [--did ]` + +Signs the challenge and links a human user to the agent's DID by creating a verification request. Uses the Billions ERC-8004 Registry (agent registration) and the Billions Attestation Registry (ownership attestation after verifying human uniqueness). + +- `--challenge` — (required) Challenge to sign. If the caller does not provide one, use `{"name": , "description": }`. +- `--did` — (optional) Uses the default DID if omitted. + +```bash +node scripts/linkHumanToAgent.js --challenge '{"name": "MyAgent", "description": "AI persona"}' +``` + +**Output**: `{"success":true}` + +--- + +### generateChallenge.js + +**Command**: `node scripts/generateChallenge.js --did ` + +Generates a random challenge for identity verification. Stores the challenge in `$HOME/.openclaw/billions/challenges.json`. + +```bash +node scripts/generateChallenge.js --did did:iden3:billions:main:2VmAk... +``` + +**Output**: Challenge string (e.g., `8472951360`) + +--- + +### signChallenge.js + +**Command**: `node scripts/signChallenge.js --challenge [--did ]` + +Signs a challenge with a DID's private key to prove identity ownership and sends the JWS token. + +- `--challenge` — (required) Challenge to sign. +- `--did` — (optional) Uses the default DID if omitted. + +```bash +node scripts/signChallenge.js --challenge 8472951360 +``` + +**Output**: `{"success":true}` + +--- + +### verifySignature.js + +**Command**: `node scripts/verifySignature.js --did --token ` + +Verifies a signed challenge to confirm DID ownership. + +```bash +node scripts/verifySignature.js --did did:iden3:billions:main:2VmAk... --token eyJhbGciOiJFUzI1NkstUi... +``` + +**Output**: `Signature verified successfully` (on success) or error message (on failure) + +--- + +## Workflows + +### Link Your Agent Identity to an Owner + +1. Check for existing identity: `node scripts/getIdentities.js` + - If none exists → `node scripts/createNewEthereumIdentity.js` +2. Run: `node scripts/linkHumanToAgent.js --challenge ` + - Use caller's challenge if provided, otherwise use `{"name": , "description": }`. +3. Return the result to the caller. + +**Example Conversation:** + +``` +User: "Link your agent identity to me" +Agent: [runs getIdentities.js, confirms identity exists] +Agent: [runs linkHumanToAgent.js --challenge '{"name": "MyAgent", "description": "Coding assistant"}'] +Agent: "Done — here's the verification link: ..." +``` + +### Verify Someone Else's Identity + +1. Ask: "Please provide your DID to start verification." +2. Generate challenge: `node scripts/generateChallenge.js --did ` +3. Ask user to sign: "Please sign this challenge: ``" +4. Verify: `node scripts/verifySignature.js --did --token ` +5. Report result. + +**Example Conversation:** + +``` +Agent: "Please provide your DID to start verification." +User: "My DID is did:iden3:billions:main:2VmAk..." +Agent: [runs generateChallenge.js] → "Please sign this challenge: 789012" +User: [provides token] +Agent: [runs verifySignature.js] → "Identity verified. You are confirmed as owner of that DID." +``` diff --git a/reference/x402/SKILL.md b/reference/x402/SKILL.md new file mode 100644 index 000000000..4efc31c30 --- /dev/null +++ b/reference/x402/SKILL.md @@ -0,0 +1,239 @@ +# x402 Payment Reference + +Handle `402 Payment Required` HTTP responses by executing payment and fetching the protected resource. + +## When to Use + +- A server responds with **HTTP 402 Payment Required**. +- The response includes a `PAYMENT-REQUIRED` header describing the payment challenge. + +## Script + +### buildX402Payment.js + +**Command**: `node scripts/buildX402Payment.js --paymentRequired [--did ] [--paymentHash ]` + +Executes the x402 payment flow: signs the payment challenge, sends the `PAYMENT-SIGNATURE` header to the resource URL, and returns the result. + +- `--paymentRequired` — (required) The value of the `PAYMENT-REQUIRED` response header (base64-encoded or raw JSON string). +- `--did` — (optional) The DID of the signer. Uses the default DID if omitted. +- `--paymentHash` — (optional) The SHA-256 hash of the chosen payment option. Required on the second call when the server offers multiple payment options. + +--- + +## Output Statuses + +The script outputs JSON to stdout. Check the `status` field: + +| Status | Meaning | +| ---------------- | ----------------------------------------------------------------------------------------------------------------- | +| `input_required` | The script needs user input or another loop iteration (payment selection, attestation, or new `paymentRequired`). | +| `success` | The resource was fetched successfully. `data` contains the response body. | +| `failed` | An error occurred. Show the message to the user. **DO NOT** retry. | + +### `input_required` sub-types + +Check `data` to determine the reason: + +| `data` field | Meaning | +| --------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `data.multiplePayments` | Multiple payment options available. Present them to the user and call again with `--paymentHash`. | +| `data.attestationsRequired` | Missing attestations. Show `data.attestationLinks` to the user and wait before retrying. | +| `data.maxUseExceeded` | The chosen payment has exceeded its maximum allowed uses. Go back and choose a different payment option. | +| `data.newPaymentRequired` | The server returned a new 402 after payment. Call the script again with this value as `--paymentRequired`. | + +--- + +## Workflow + +### 1. Request a resource + +Send a normal HTTP request to the server endpoint. + +### 2. Receive 402 + +The server returns `402 Payment Required` with a `PAYMENT-REQUIRED` header. + +### 3. Ensure identity exists + +Run `node scripts/getIdentities.js`. If no identity is configured, create one first (see `reference/identity/SKILL.md`). + +### 4. First call — execute payment or get payment options + +```bash +node scripts/buildX402Payment.js \ + --paymentRequired '' +``` + +**If the server offers a single payment option**: the script signs the payment, sends it to the resource URL, and returns the result. Check the output status: + +- `success` — the resource body is in `data`. You're done. +- `input_required` with `data.newPaymentRequired` — the server returned another 402. Go to **step 6**. +- `failed` — show the error to the user. + +**If the server offers multiple payment options**: the script outputs `status: "input_required"` with `data.multiplePayments: true` and a list of payment options. Each payment option includes: + +- `hash` — the payment hash (use as `--paymentHash` in the next call) +- `amount` — the payment amount +- `asset` — the asset name (e.g., "USDC") or contract address +- `network` — the network identifier (e.g., "eip155:84532") +- `requiredAttestations` — informational list of attestation schema IDs that this payment type requires in general; may be non-empty even when the user already holds all of them — **do not use this field to decide whether to block the payment** +- `hasAllAttestations` — `true` if the user already holds every required attestation and the payment can proceed; `false` if some are missing +- `attestationLinks` — verification URLs the user must complete to obtain **missing** attestations; empty when `hasAllAttestations` is `true` + +**Present the options to the user.** Show the amount, asset, network, and whether attestations are needed. Then ask the user to choose one payment option. + +> **CRITICAL: How to read attestation status — always use `hasAllAttestations` and `attestationLinks`, never `requiredAttestations` alone** +> +> | `hasAllAttestations` | `attestationLinks` | What it means | +> | -------------------- | ------------------ | ----------------------------------------------------------------------------- | +> | `true` | empty | User holds all attestations — this payment can proceed immediately. | +> | `false` | non-empty | User is missing attestations — show every link to the user before proceeding. | +> +> A payment option with a non-empty `requiredAttestations` but `hasAllAttestations: true` and an empty `attestationLinks` means the user **already has** all the necessary attestations. Do not block or warn about such payments. +> +> **CRITICAL: Attestation Links** +> If `attestationLinks` is non-empty, you MUST display every link to the user exactly as returned. +> +> - **DO NOT** follow, open, or resolve the links yourself. +> - **DO NOT** attempt to complete the attestation on behalf of the user. +> - **DO NOT** skip or hide payment options that require attestations. +> - Simply present each link and inform the user they must open and complete them manually. + +### 5. Second call — execute chosen payment + +Once the user selects a payment, call the script again with the chosen `--paymentHash`: + +```bash +node scripts/buildX402Payment.js \ + --paymentRequired '' \ + --paymentHash '' +``` + +**If the chosen payment requires attestations the user doesn't have**: the script outputs `status: "input_required"` with `data.attestationsRequired: true` and `data.attestationLinks` containing verification URLs. Show these links to the user and wait for them to complete the attestation before retrying. + +**If attestations are satisfied (or not needed)**: the script signs the payment, sends it, and returns the result. Check the output status as described in step 4. + +### 6. Handle max use exceeded + +If the script returns `status: "input_required"` with `data.maxUseExceeded: true`, the chosen payment option has been used the maximum number of times allowed by the server. **Do not retry with the same payment.** Go back to the payment selection step and ask the user to choose a different payment option from the original list. + +### 7. Handle new 402 (loop) + +If the script returns `status: "input_required"` with `data.newPaymentRequired`, the server issued a new payment challenge after the first payment. **Call the script again** with the new value: + +```bash +node scripts/buildX402Payment.js \ + --paymentRequired '' +``` + +Repeat from step 4. Continue looping until you get `success` or `failed`. + +--- + +## Error Handling (CRITICAL) + +If `buildX402Payment.js` returns status `failed`: + +- **DO NOT** retry the original request. +- Show the error message to the user. +- Ask the user to resolve the issue before retrying. + +If `buildX402Payment.js` returns status `input_required`: + +- **DO NOT** make your own HTTP request to the resource. +- Read `data` to determine what is needed: + - `data.multiplePayments` — present payment options, call again with `--paymentHash` + - `data.attestationsRequired` — show links to user, wait, then retry + - `data.maxUseExceeded` — the chosen payment exceeded its max uses; go back and ask the user to pick a different payment option + - `data.newPaymentRequired` — call the script again with the new `--paymentRequired` value (loop) + +--- + +## Examples + +### Single payment (direct execution) + +``` +Agent: [fetches https://example.com/api/resource] +Server: 402 Payment Required + Header: PAYMENT-REQUIRED: "eyJhbGciOi..." + +Agent: [runs getIdentities.js — confirms identity exists] +Agent: [runs buildX402Payment.js --paymentRequired 'eyJhbGciOi...'] + → { "status": "success", "data": { "temperature": 22, "city": "Kyiv" } } + +Agent: "The weather data shows 22°C in Kyiv." +``` + +### Multiple payments (two-phase flow) + +``` +Agent: [fetches https://example.com/api/resource] +Server: 402 Payment Required + Header: PAYMENT-REQUIRED: "eyJ4NDAyVmVyc2lvbi..." + +Agent: [runs getIdentities.js — confirms identity exists] +Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAyVmVyc2lvbi...'] + → { "status": "input_required", "data": { "multiplePayments": true, "payments": [ + { "hash": "a1b2c3...", "amount": "10000", "asset": "USDC", "network": "eip155:84532", + "requiredAttestations": [], "hasAllAttestations": true, "attestationLinks": [] }, + { "hash": "d4e5f6...", "amount": "6000", "asset": "USDC", "network": "eip155:84532", + "requiredAttestations": ["0xca35..."], "hasAllAttestations": false, + "attestationLinks": ["https://wallet.billions.network#request_uri=..."] } + ]}} + +Agent: "There are 2 payment options: + 1) 10000 USDC on eip155:84532 — no attestations required + 2) 6000 USDC on eip155:84532 — requires attestation (missing). + Complete verification: [link] + Which would you like?" + +User: "Option 1" + +Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAyVmVyc2lvbi...' --paymentHash 'a1b2c3...'] + → { "status": "success", "data": { "temperature": 22, "city": "Kyiv" } } + +Agent: "The weather data shows 22°C in Kyiv." +``` + +### New 402 after payment (loop) + +``` +Agent: [runs buildX402Payment.js --paymentRequired 'eyJhbGciOi...'] + → { "status": "input_required", "data": { "newPaymentRequired": "eyJ4NDAy..." } } + +Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAy...'] + → { "status": "success", "data": { "temperature": 22, "city": "Kyiv" } } +``` + +### Attestation required (single payment) + +``` +Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAy...' --paymentHash 'd4e5f6...'] + → { "status": "input_required", "data": { "attestationsRequired": true, + "message": "The following attestations are required to complete the payment:", + "attestationLinks": ["https://wallet.billions.network#request_uri=..."] }} + +Agent: "You need to complete an attestation before paying. Please open this link: [link]" +User: [completes attestation] +Agent: [retries buildX402Payment.js with same arguments] +``` + +### Max use exceeded + +``` +Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAyVmVyc2lvbi...' --paymentHash 'd4e5f6...'] + → { "status": "input_required", "data": { "maxUseExceeded": true, + "message": "Payment has exceeded its maximum allowed uses. Choose a different payment or contact the resource provider." }} + +Agent: "The selected payment option has reached its maximum number of uses. + Please choose a different payment option: + 1) 10000 USDC on eip155:84532 — no attestations required + Which would you like?" + +User: "Option 1" + +Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAyVmVyc2lvbi...' --paymentHash 'a1b2c3...'] + → { "status": "success", "data": { "temperature": 22, "city": "Kyiv" } } +``` diff --git a/scripts/buildX402Payment.js b/scripts/buildX402Payment.js new file mode 100644 index 000000000..9571ffa72 --- /dev/null +++ b/scripts/buildX402Payment.js @@ -0,0 +1,277 @@ +const { + parseArgs, + hashstr, + outputSuccess, + outputError, + outputInputRequired, + getUserWallet, + createAuthRequestMessage, + getRequiredDidEntry, +} = require("./shared/utils"); +const { getInitializedRuntime } = require("./shared/bootstrap"); +const { x402Client } = require("@x402/core/client"); +const { ExactEvmScheme } = require("@x402/evm/exact/client"); +const { + createHumanProofExtension, + MissingAttestationsError, + checkAttestation, + isMaxUseExceededError, +} = require("@privadoid/x402-human-proof-client/packages/client"); +const { toClientEvmSigner } = require("@x402/evm"); +const { + schemaId, + transactionSender, + requiredAttestationsMessage, +} = require("./shared/constants"); +const { createPOUScope, createAuthScope } = require("./shared/scopes"); +const { signChallenge } = require("./signChallenge"); +const { v4: uuidv4 } = require("uuid"); + +function getPaymentHash(payment) { + return hashstr(JSON.stringify(payment)); +} + +function getRequiredAttestations(payment) { + return (payment.extra && payment.extra.requiredAttestations) || []; +} + +async function getMissingAttestations(did, payment) { + const requiredAttestations = getRequiredAttestations(payment); + const results = await Promise.all( + requiredAttestations.map(async (id) => ({ + id, + exists: await checkAttestation(did, id), + })), + ); + return results.filter((r) => !r.exists).map((r) => r.id); +} + +async function createAttestationLinks( + attestationSchemaIds, + transactionSenderAddr, + did, + entry, + kms, +) { + return await Promise.all( + attestationSchemaIds.map(async (attestationSchemaId) => { + if (attestationSchemaId !== schemaId) { + throw new Error( + `Unknown attestation requirement with schema ${attestationSchemaId}`, + ); + } + const scope = [ + createPOUScope(transactionSenderAddr), + createAuthScope(did), + ]; + const signedChallenge = await signChallenge( + { name: uuidv4(), description: uuidv4() }, + entry, + kms, + ); + return await createAuthRequestMessage(signedChallenge, scope); + }), + ); +} + +async function handleMissingAttestations(error, entry, kms) { + const attestationLinks = await createAttestationLinks( + error.attestationRequirements, + transactionSender, + entry.did, + entry, + kms, + ); + outputInputRequired( + { + attestationsRequired: true, + message: requiredAttestationsMessage, + attestationLinks, + }, + true, + ); +} + +async function buildPaymentInfo(payment, entry, kms) { + const requiredAttestations = getRequiredAttestations(payment); + const missingAttestations = await getMissingAttestations(entry.did, payment); + + let attestationLinks = []; + if (missingAttestations.length > 0) { + attestationLinks = await createAttestationLinks( + missingAttestations, + transactionSender, + entry.did, + entry, + kms, + ); + } + + return { + hash: getPaymentHash(payment), + amount: payment.amount, + asset: (payment.extra && payment.extra.name) || payment.asset, + network: payment.network, + requiredAttestations, + hasAllAttestations: missingAttestations.length === 0, + attestationLinks, + }; +} + +async function main() { + try { + const args = parseArgs(); + + if (!args.paymentRequired) { + outputError("--paymentRequired is required", true); + } + + let paymentRequired; + try { + paymentRequired = args.paymentRequired.trim().startsWith("{") + ? JSON.parse(args.paymentRequired) + : JSON.parse(atob(args.paymentRequired)); + } catch (e) { + outputError( + "--paymentRequired must be valid JSON or Base64 encoded JSON", + true, + ); + } + + const { kms, memoryKeyStore, didsStorage } = await getInitializedRuntime(); + const entry = await getRequiredDidEntry(didsStorage, args.did); + + const payments = paymentRequired.accepts; + + // Phase 1: Multiple payments — show selection to user + if (payments.length > 1 && !args.paymentHash) { + const paymentInfos = await Promise.all( + payments.map((p) => buildPaymentInfo(p, entry, kms)), + ); + outputInputRequired( + { multiplePayments: true, payments: paymentInfos }, + true, + ); + return; + } + + // Phase 2: User selected a payment by hash - filter to it + if (args.paymentHash) { + const matched = payments.find( + (p) => getPaymentHash(p) === args.paymentHash, + ); + if (!matched) { + outputError("No payment matching the provided --paymentHash", true); + return; + } + paymentRequired.accepts = [matched]; + } + + // Phase 3: Single payment - check attestations before proceeding + const selectedPayment = paymentRequired.accepts[0]; + const missingAttestations = await getMissingAttestations( + entry.did, + selectedPayment, + ); + + if (missingAttestations.length > 0) { + const attestationLinks = await createAttestationLinks( + missingAttestations, + transactionSender, + entry.did, + entry, + kms, + ); + outputInputRequired( + { + attestationsRequired: true, + message: requiredAttestationsMessage, + attestationLinks, + }, + true, + ); + return; + } + + // Phase 4: Execute payment and fetch the resource + const { wallet } = await getUserWallet(entry, memoryKeyStore); + const signer = toClientEvmSigner(wallet); + + const x402 = new x402Client(); + x402.register("eip155:*", new ExactEvmScheme(signer)); + x402.registerExtension( + createHumanProofExtension({ + address: wallet.address, + pubKey: wallet.publicKey, + signMessage: (msg) => wallet.signMessage({ message: msg }), + }), + ); + x402.onPaymentCreationFailure(async ({ error }) => { + if (error instanceof MissingAttestationsError) { + await handleMissingAttestations(error, entry, kms); + } + }); + + let paymentPayload; + try { + paymentPayload = await x402.createPaymentPayload(paymentRequired); + } catch (error) { + if (error instanceof MissingAttestationsError) { + return; + } else { + throw error; + } + } + + // Phase 5: Fetch the resource with the payment signature + const paymentSignature = btoa(JSON.stringify(paymentPayload)); + const url = paymentRequired.resource.url; + let response; + response = await fetch(url, { + headers: { "PAYMENT-SIGNATURE": paymentSignature }, + }); + + if (response.status === 402) { + console.log(response); + if (isMaxUseExceededError({ response })) { + outputInputRequired( + { + maxUseExceeded: true, + message: + "Payment has exceeded its maximum allowed uses. Choose a different payment or contact the resource provider.", + }, + true, + ); + } + // if not max use exceeded, check for new payment required + const newPaymentRequired = response.headers.get("payment-required"); + if (newPaymentRequired) { + outputInputRequired({ newPaymentRequired: newPaymentRequired }, true); + return; + } + outputError("Received 402 but no PAYMENT-REQUIRED header found", true); + return; + } + + const responseText = await response.text(); + let responseBody; + try { + responseBody = JSON.parse(responseText); + } catch { + responseBody = responseText; + } + + if (response.ok) { + outputSuccess(responseBody, true); + } else { + outputError( + `HTTP ${response.status}: ${typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody)}`, + true, + ); + } + } catch (error) { + outputError(error, true); + } +} + +main(); diff --git a/scripts/constants.js b/scripts/constants.js deleted file mode 100644 index 9428738ee..000000000 --- a/scripts/constants.js +++ /dev/null @@ -1,19 +0,0 @@ -export const transactionSender = "0xB3F5d3DD47F6ca17468898291491eBDA69a67797"; // relay sender address -export const verifierDid = - "did:iden3:privado:main:2SZu1G6YDUtk9AAY6TZic24CcCYcZvtdyp1cQv9cig"; // should be the same as dashboard DID -export const callbackBase = - "https://attestation-relay.billions.network/api/v1/callback?attestation="; -export const walletAddress = "https://wallet.billions.network"; -export const verificationMessage = - "Complete the verification to link your identity to the agent"; -export const pairingReasonMessage = "agent_pairing:v1"; -export const accept = [ - "iden3comm/v1;env=application/iden3-zkp-json;circuitId=authV2,authV3,authV3-8-32;alg=groth16", -]; -export const nullifierSessionId = "240416041207230509012302"; -export const pouScopeId = 1; // keccak256(nullifierSessionId) -export const pouAllowedIssuer = [ - "did:iden3:billions:main:2VwqkgA2dNEwsnmojaay7C5jJEb8ZygecqCSU3xVfm", -]; -export const authScopeId = 2; -export const urlShortener = "https://identity-dashboard.billions.network"; diff --git a/scripts/createNewEthereumIdentity.js b/scripts/createNewEthereumIdentity.js index 766640661..ebff8be54 100644 --- a/scripts/createNewEthereumIdentity.js +++ b/scripts/createNewEthereumIdentity.js @@ -1,10 +1,10 @@ -const { KmsKeyType, hexToBytes } = require("@0xpolygonid/js-sdk"); +const { hexToBytes } = require("@0xpolygonid/js-sdk"); const { DidMethod, Blockchain, NetworkId } = require("@iden3/js-iden3-core"); const { SigningKey, Wallet, JsonRpcProvider } = require("ethers"); const { getInitializedRuntime } = require("./shared/bootstrap"); const { parseArgs, - formatError, + outputError, outputSuccess, addHexPrefix, } = require("./shared/utils"); @@ -13,7 +13,6 @@ async function main() { try { const args = parseArgs(); const { - kms, identityWallet, didsStorage, billionsMainnetConfig, @@ -30,13 +29,6 @@ async function main() { // Create signer from private key const signer = new SigningKey(addHexPrefix(privateKeyHex)); - // Get the Secp256k1 key provider - const keyProvider = kms.getKeyProvider(KmsKeyType.Secp256k1); - if (!keyProvider) { - console.error("Error: Secp256k1 key provider not found"); - process.exit(1); - } - // Create wallet with Billions Network provider const wallet = new Wallet( signer, @@ -57,10 +49,9 @@ async function main() { }); did = result.did; } catch (err) { - console.error( - `Error: Failed to create Ethereum-based identity: ${err.message}`, + throw new Error( + `Failed to create Ethereum-based identity: ${err.message}`, ); - process.exit(1); } // Save DID to storage @@ -72,8 +63,7 @@ async function main() { outputSuccess(did.string()); } catch (error) { - console.error(formatError(error)); - process.exit(1); + outputError(error, true); } } diff --git a/scripts/generateChallenge.js b/scripts/generateChallenge.js index 0fa4fd5fe..09838bb42 100644 --- a/scripts/generateChallenge.js +++ b/scripts/generateChallenge.js @@ -1,15 +1,15 @@ const { randomInt } = require("crypto"); const { getInitializedRuntime } = require("./shared/bootstrap"); -const { parseArgs, formatError, outputSuccess } = require("./shared/utils"); +const { parseArgs, outputError, outputSuccess } = require("./shared/utils"); async function main() { try { const args = parseArgs(); if (!args.did) { - console.error("Error: --did parameter is required"); - console.error("Usage: node scripts/generateChallenge.js --did "); - process.exit(1); + throw new Error( + "--did parameter is required. Usage: node scripts/generateChallenge.js --did ", + ); } const { challengeStorage } = await getInitializedRuntime(); @@ -22,8 +22,7 @@ async function main() { outputSuccess(challenge); } catch (error) { - console.error(formatError(error)); - process.exit(1); + outputError(error, true); } } diff --git a/scripts/getDidDocument.js b/scripts/getDidDocument.js index f9af4bf52..426f8a864 100644 --- a/scripts/getDidDocument.js +++ b/scripts/getDidDocument.js @@ -1,28 +1,17 @@ const { getInitializedRuntime } = require("./shared/bootstrap"); const { parseArgs, - formatError, + outputError, outputSuccess, createDidDocument, + getRequiredDidEntry, } = require("./shared/utils"); async function main() { try { const args = parseArgs(); const { didsStorage } = await getInitializedRuntime(); - - // Get DID entry - either specific DID or default - const entry = args.did - ? await didsStorage.find(args.did) - : await didsStorage.getDefault(); - - if (!entry) { - const errorMsg = args.did - ? `No DID ${args.did} found` - : "No default DID found. Create one with createNewEthereumIdentity.js"; - console.error(errorMsg); - process.exit(1); - } + const entry = await getRequiredDidEntry(didsStorage, args.did); const didDocument = createDidDocument(entry.did, entry.publicKeyHex); @@ -31,8 +20,7 @@ async function main() { did: entry.did, }); } catch (error) { - console.error(formatError(error)); - process.exit(1); + outputError(error, true); } } diff --git a/scripts/getIdentities.js b/scripts/getIdentities.js index 9bd604941..1a0a34ecf 100644 --- a/scripts/getIdentities.js +++ b/scripts/getIdentities.js @@ -1,5 +1,5 @@ const { getInitializedRuntime } = require("./shared/bootstrap"); -const { formatError, outputSuccess } = require("./shared/utils"); +const { outputError, outputSuccess } = require("./shared/utils"); async function main() { try { @@ -8,16 +8,14 @@ async function main() { const identities = await didsStorage.list(); if (identities.length === 0) { - console.error( + throw new Error( "No identities found. Create one with createNewEthereumIdentity.js", ); - process.exit(1); } outputSuccess(identities); } catch (error) { - console.error(formatError(error)); - process.exit(1); + outputError(error, true); } } diff --git a/scripts/linkHumanToAgent.js b/scripts/linkHumanToAgent.js index e117734a9..461253445 100644 --- a/scripts/linkHumanToAgent.js +++ b/scripts/linkHumanToAgent.js @@ -1,96 +1,18 @@ -const { auth } = require("@iden3/js-iden3-auth"); -const { CircuitId } = require("@0xpolygonid/js-sdk"); const { - buildEthereumAddressFromDid, parseArgs, - urlFormating, + urlFormatting, outputSuccess, - formatError, + outputError, + createAuthRequestMessage, + getRequiredDidEntry, } = require("./shared/utils"); -const { computeAttestationHash } = require("./shared/attestation"); const { getInitializedRuntime } = require("./shared/bootstrap"); const { signChallenge } = require("./signChallenge"); +const { createPOUScope, createAuthScope } = require("./shared/scopes"); const { transactionSender, - verifierDid, - callbackBase, - walletAddress, verificationMessage, - pairingReasonMessage, - accept, - nullifierSessionId, - pouScopeId, - pouAllowedIssuer, - authScopeId, - urlShortener, -} = require("./constants"); - -function createPOUScope(transactionSender) { - return { - id: pouScopeId, - circuitId: CircuitId.AtomicQueryV3OnChainStable, - params: { - sender: transactionSender, - nullifierSessionId: nullifierSessionId, - }, - query: { - allowedIssuers: pouAllowedIssuer, - type: "UniquenessCredential", - context: "ipfs://QmcUEDa42Er4nfNFmGQVjiNYFaik6kvNQjfTeBrdSx83At", - }, - }; -} - -function createAuthScope(recipientDid) { - return { - id: authScopeId, - circuitId: CircuitId.AuthV3_8_32, - params: { - challenge: computeAttestationHash({ - recipientDid: recipientDid, - recipientEthAddress: buildEthereumAddressFromDid(recipientDid), - }), - }, - }; -} - -async function createAuthRequestMessage(jws, recipientDid) { - const callback = callbackBase + jws; - const scope = [ - createPOUScope(transactionSender), - createAuthScope(recipientDid), - ]; - - const message = auth.createAuthorizationRequestWithMessage( - pairingReasonMessage, - verificationMessage, - verifierDid, - encodeURI(callback), - { - scope, - accept: accept, - }, - ); - - // the code does request to trusted URL shortener service to create - // a short link for the wallet deep link. - // This is needed to avoid issues with very long URLs in some wallets and to improve user experience. - const shortenerResponse = await fetch(`${urlShortener}/shortener`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(message), - }); - - if (shortenerResponse.status !== 201) { - throw new Error( - `URL shortener failed with status ${shortenerResponse.status}`, - ); - } - - const { url } = await shortenerResponse.json(); - - return `${walletAddress}#request_uri=${url}`; -} +} = require("./shared/constants"); /** * Creates a pairing URL for linking a human identity to the agent. @@ -100,22 +22,16 @@ async function createAuthRequestMessage(jws, recipientDid) { */ async function createPairing(challenge, didOverride) { const { kms, didsStorage } = await getInitializedRuntime(); - - const entry = didOverride - ? await didsStorage.find(didOverride) - : await didsStorage.getDefault(); - - if (!entry) { - const errorMsg = didOverride - ? `No DID ${didOverride} found` - : "No default DID found"; - throw new Error(errorMsg); - } + const entry = await getRequiredDidEntry(didsStorage, didOverride); const recipientDid = entry.did; const signedChallenge = await signChallenge(challenge, entry, kms); - return await createAuthRequestMessage(signedChallenge, recipientDid); + const scope = [ + createPOUScope(transactionSender), + createAuthScope(recipientDid), + ]; + return await createAuthRequestMessage(signedChallenge, scope); } async function main() { @@ -123,26 +39,17 @@ async function main() { const args = parseArgs(); if (!args.challenge) { - console.error( - JSON.stringify({ - success: false, - error: - "Invalid arguments. Usage: node linkHumanToAgent.js --challenge [--did ]", - }), + throw new Error( + "Invalid arguments. Usage: node linkHumanToAgent.js --challenge [--did ]", ); - process.exit(1); } const challenge = JSON.parse(args.challenge); const url = await createPairing(challenge, args.did); - outputSuccess({ - success: true, - data: urlFormating(verificationMessage, url), - }); + outputSuccess(urlFormatting(verificationMessage, url)); } catch (error) { - console.error(formatError(error)); - process.exit(1); + outputError(error, true); } } diff --git a/scripts/manualLinkHumanToAgent.js b/scripts/manualLinkHumanToAgent.js index e1b54e31a..e6d25046d 100644 --- a/scripts/manualLinkHumanToAgent.js +++ b/scripts/manualLinkHumanToAgent.js @@ -1,18 +1,14 @@ const { createPairing } = require("./linkHumanToAgent"); -const { parseArgs, formatError } = require("./shared/utils"); +const { parseArgs, outputError } = require("./shared/utils"); async function main() { try { const args = parseArgs(); if (!args.challenge) { - console.error( - "Invalid arguments. Usage: node manualLinkHumanToAgent.js --challenge [--did ]", + throw new Error( + 'Invalid arguments. Usage: node manualLinkHumanToAgent.js --challenge [--did ]\nExample: node manualLinkHumanToAgent.js --challenge \'{"name": "Agent Name", "description": "Short description of the agent"}\'', ); - console.error( - 'Example: node manualLinkHumanToAgent.js --challenge \'{"name": "Agent Name", "description": "Short description of the agent"}\'', - ); - process.exit(1); } const challenge = JSON.parse(args.challenge); @@ -20,8 +16,7 @@ async function main() { console.log(url); } catch (error) { - console.error(formatError(error)); - process.exit(1); + outputError(error, true); } } diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 8c33cfc37..199b10e8c 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -12,9 +12,14 @@ "@0xpolygonid/js-sdk": "1.42.1", "@iden3/js-iden3-auth": "1.14.0", "@iden3/js-iden3-core": "1.8.0", - "@noble/curves": "^1.9.2", - "ethers": "^6.13.4", - "uuid": "^11.0.3" + "@noble/curves": "1.9.2", + "@privadoid/x402-human-proof-client": "git+https://github.com/0xPolygonID/x402-human-proof.git#dist", + "@x402/core": "2.9.0", + "@x402/evm": "2.9.0", + "ethers": "6.13.4", + "uuid": "11.0.3", + "viem": "2.47.6", + "x402-human-proof": "github:0xPolygonID/x402-human-proof#dist" } }, "node_modules/@0xpolygonid/js-sdk": { @@ -55,28 +60,13 @@ "snarkjs": "0.7.5" } }, - "node_modules/@0xpolygonid/js-sdk/node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@0xpolygonid/js-sdk/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -122,18 +112,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@0xpolygonid/js-sdk/node_modules/ethers/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@0xpolygonid/js-sdk/node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", @@ -160,9 +138,9 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1002,18 +980,6 @@ "snarkjs": "0.7.5" } }, - "node_modules/@iden3/js-iden3-auth/node_modules/@0xpolygonid/js-sdk/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@iden3/js-iden3-auth/node_modules/@0xpolygonid/js-sdk/node_modules/ethers": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", @@ -1067,28 +1033,13 @@ "uuid": "dist-node/bin/uuid" } }, - "node_modules/@iden3/js-iden3-auth/node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@iden3/js-iden3-auth/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1253,9 +1204,9 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", + "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" @@ -1280,9 +1231,9 @@ } }, "node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -1291,6 +1242,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@privadoid/x402-human-proof-client": { + "name": "x402-human-proof", + "resolved": "git+ssh://git@github.com/0xPolygonID/x402-human-proof.git#202c06803eab0e503023faf8ea004d0d1bc49cfe", + "workspaces": [ + "packages/*", + "examples/*" + ], + "dependencies": { + "viem": "2.47.6" + } + }, "node_modules/@scure/base": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", @@ -1300,6 +1262,75 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@solana/buffer-layout": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", @@ -1398,9 +1429,9 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", - "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -1439,6 +1470,47 @@ "@types/node": "*" } }, + "node_modules/@x402/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@x402/core/-/core-2.9.0.tgz", + "integrity": "sha512-IqPITHYx6XHlgLPtparuKKwoB+3wQdgt0F+WUH1e3WHMeiWdp+xTtQDy+6yOKuObNFI1S1iVbQFz0GivR/Vv3w==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^3.24.2" + } + }, + "node_modules/@x402/evm": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@x402/evm/-/evm-2.9.0.tgz", + "integrity": "sha512-qUhnKe1pym9a+7dzeK+6ripsddVsr+5PNcpQfTYK4dubW+1SR9MRx/O4PNRtedWoAxminqAwmCL5AQUiSVvKWA==", + "license": "Apache-2.0", + "dependencies": { + "@x402/core": "~2.9.0", + "viem": "^2.39.3", + "zod": "^3.24.2" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1619,9 +1691,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "license": "MIT", "peer": true, "dependencies": { @@ -1909,9 +1981,9 @@ } }, "node_modules/ethers": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", - "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "version": "6.13.4", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.4.tgz", + "integrity": "sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==", "funding": [ { "type": "individual", @@ -2171,6 +2243,21 @@ "ws": "*" } }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -2273,9 +2360,9 @@ } }, "node_modules/jose": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", - "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -2535,6 +2622,75 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/ox": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.7.tgz", + "integrity": "sha512-zSQ/cfBdolj7U4++NAvH7sI+VG0T3pEohITCgcQj8KlawvTDY4vGVhDT64Atsm0d6adWfIYHDpu88iUBMMp+AQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2639,9 +2795,9 @@ "peer": true }, "node_modules/rpc-websockets": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.5.tgz", - "integrity": "sha512-4mAmr+AEhPYJ9TmDtxF3r3ZcbWy7W8kvZ4PoZYw/Xgp2J7WixjwTgiQZsoTDvch5nimmg3Ay6/0Kuh9oIvVs9A==", + "version": "9.3.8", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.8.tgz", + "integrity": "sha512-7r+fm4tSJmLf9GvZfL1DJ1SJwpagpp6AazqM0FUaeV7CA+7+NYINSk1syWa4tU/6OF2CyBicLtzENGmXRJH6wQ==", "license": "LGPL-3.0-only", "dependencies": { "@swc/helpers": "^0.5.11", @@ -2803,9 +2959,9 @@ "license": "0BSD" }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "license": "Apache-2.0", "peer": true, "bin": { @@ -2865,9 +3021,9 @@ } }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -2877,6 +3033,84 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/viem": { + "version": "2.47.6", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.6.tgz", + "integrity": "sha512-zExmbI99NGvMdYa7fmqSTLgkwh48dmhgEqFrUgkpL4kfG4XkVefZ8dZqIKVUhZo6Uhf0FrrEXOsHm9LUyIvI2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.7", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wasmbuilder": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/wasmbuilder/-/wasmbuilder-0.0.16.tgz", @@ -2947,11 +3181,30 @@ } } }, + "node_modules/x402-human-proof": { + "resolved": "git+ssh://git@github.com/0xPolygonID/x402-human-proof.git#202c06803eab0e503023faf8ea004d0d1bc49cfe", + "workspaces": [ + "packages/*", + "examples/*" + ], + "dependencies": { + "viem": "2.47.6" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/scripts/package.json b/scripts/package.json index 045e93a5e..995089b0b 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -16,8 +16,13 @@ "@0xpolygonid/js-sdk": "1.42.1", "@iden3/js-iden3-auth": "1.14.0", "@iden3/js-iden3-core": "1.8.0", - "@noble/curves": "^1.9.2", - "ethers": "^6.13.4", - "uuid": "^11.0.3" + "@noble/curves": "1.9.2", + "@privadoid/x402-human-proof-client": "git+https://github.com/0xPolygonID/x402-human-proof.git#dist", + "@x402/core": "2.9.0", + "@x402/evm": "2.9.0", + "ethers": "6.13.4", + "uuid": "11.0.3", + "viem": "2.47.6", + "x402-human-proof": "github:0xPolygonID/x402-human-proof#dist" } -} \ No newline at end of file +} diff --git a/scripts/shared/attestation.js b/scripts/shared/attestation.js index d80d44bd0..c5396ac14 100644 --- a/scripts/shared/attestation.js +++ b/scripts/shared/attestation.js @@ -1,8 +1,6 @@ const { ethers } = require("ethers"); const { DID } = require("@iden3/js-iden3-core"); - -const ATTESTATION_SCHEMA_ID = - "0xca354bee6dc5eded165461d15ccb13aceb6f77ebbb1fd3fe45aca686097f2911"; // bytes32 +const { schemaId: ATTESTATION_SCHEMA_ID } = require("./constants"); const ATTESTER_DID = ""; // string const ATTESTER_IDEN3_ID = 0n; // uint256 @@ -79,7 +77,6 @@ function computeAttestationHash(req) { } module.exports = { - buildEncodedAttestation, computeAttestationHash, buildJsonAttestation, }; diff --git a/scripts/shared/bootstrap.js b/scripts/shared/bootstrap.js index eb43db59e..981be9759 100644 --- a/scripts/shared/bootstrap.js +++ b/scripts/shared/bootstrap.js @@ -19,6 +19,12 @@ const { KeysFileStorage } = require("./storage/keys"); const { IdentitiesFileStorage } = require("./storage/identities"); const { DidsFileStorage } = require("./storage/did"); const { ChallengeFileStorage } = require("./storage/challenge"); +const { + rpcUrl, + stateContractAddress, + chainId, + rhsUrl, +} = require("./constants"); let cachedRuntime = null; @@ -35,7 +41,7 @@ async function newInMemoryKMS() { const kms = new KMS(); kms.registerKeyProvider(KmsKeyType.Secp256k1, secpProvider); kms.registerKeyProvider(KmsKeyType.BabyJubJub, bjjProvider); - return kms; + return { kms, memoryKeyStore }; } /** @@ -87,9 +93,9 @@ function newIdentityWallet(kms, dataStorage, credentialWallet) { function getBillionsMainnetConfig() { return { ...defaultEthConnectionConfig, - url: "https://rpc-mainnet.billions.network", - contractAddress: "0x3c9acb2205aa72a05f6d77d708b5cf85fca3a896", - chainId: 45056, + url: rpcUrl, + contractAddress: stateContractAddress, + chainId: chainId, }; } @@ -99,14 +105,14 @@ function getBillionsMainnetConfig() { function getRevocationOpts() { return { type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, - id: "https://rhs-staging.polygonid.me", + id: rhsUrl, }; } /** * Initializes and returns all runtime dependencies. * Uses caching to avoid re-initialization. - * + * * @returns {Promise} Runtime object containing: * - kms: Key Management System * - identityWallet: Identity wallet instance @@ -123,7 +129,7 @@ async function getInitializedRuntime() { const billionsMainnetConfig = getBillionsMainnetConfig(); const revocationOpts = getRevocationOpts(); - const kms = await newInMemoryKMS(); + const { kms, memoryKeyStore } = await newInMemoryKMS(); const stateStorage = newEthStateStorage(billionsMainnetConfig); const dataStorage = newDataStorage(stateStorage); const credentialWallet = newCredentialWallet(dataStorage); @@ -139,6 +145,7 @@ async function getInitializedRuntime() { challengeStorage, billionsMainnetConfig, revocationOpts, + memoryKeyStore, }; return cachedRuntime; diff --git a/scripts/shared/constants.js b/scripts/shared/constants.js new file mode 100644 index 000000000..491e6706b --- /dev/null +++ b/scripts/shared/constants.js @@ -0,0 +1,53 @@ +const transactionSender = "0xB3F5d3DD47F6ca17468898291491eBDA69a67797"; // relay sender address +const verifierDid = + "did:iden3:privado:main:2SZu1G6YDUtk9AAY6TZic24CcCYcZvtdyp1cQv9cig"; // should be the same as dashboard DID +const callbackBase = + "https://attestation-relay.billions.network/api/v1/callback?attestation="; +const walletAddress = "https://wallet.billions.network"; +const verificationMessage = + "Complete the verification to link your identity to the agent"; +const requiredAttestationsMessage = + "The following attestations are required to complete the payment:"; +const pairingReasonMessage = "agent_pairing:v1"; +const accept = [ + "iden3comm/v1;env=application/iden3-zkp-json;circuitId=authV2,authV3,authV3-8-32;alg=groth16", +]; +const nullifierSessionId = "240416041207230509012302"; +const pouScopeId = 1; // keccak256(nullifierSessionId) +const pouAllowedIssuer = [ + "did:iden3:billions:main:2VwqkgA2dNEwsnmojaay7C5jJEb8ZygecqCSU3xVfm", +]; +const authScopeId = 2; +const urlShortener = "https://identity-dashboard.billions.network"; +const schemaId = + "0xca354bee6dc5eded165461d15ccb13aceb6f77ebbb1fd3fe45aca686097f2911"; +const resolverUrl = "https://resolver.privado.id/1.0/identifiers"; +const rpcUrl = "https://rpc-mainnet.billions.network"; +const stateContractAddress = "0x3c9acb2205aa72a05f6d77d708b5cf85fca3a896"; +const chainId = 45056; +const rhsUrl = "https://rhs-staging.polygonid.me"; +const pouCredentialContext = + "ipfs://QmcUEDa42Er4nfNFmGQVjiNYFaik6kvNQjfTeBrdSx83At"; + +module.exports = { + transactionSender, + verifierDid, + callbackBase, + walletAddress, + verificationMessage, + requiredAttestationsMessage, + pairingReasonMessage, + accept, + nullifierSessionId, + pouScopeId, + pouAllowedIssuer, + authScopeId, + urlShortener, + schemaId, + resolverUrl, + rpcUrl, + stateContractAddress, + chainId, + rhsUrl, + pouCredentialContext, +}; diff --git a/scripts/shared/scopes.js b/scripts/shared/scopes.js new file mode 100644 index 000000000..a018032fc --- /dev/null +++ b/scripts/shared/scopes.js @@ -0,0 +1,44 @@ +const { CircuitId } = require("@0xpolygonid/js-sdk"); +const { computeAttestationHash } = require("./attestation"); +const { buildEthereumAddressFromDid } = require("./utils"); +const { + nullifierSessionId, + pouScopeId, + pouAllowedIssuer, + authScopeId, + pouCredentialContext, +} = require("./constants"); + +function createPOUScope(transactionSender) { + return { + id: pouScopeId, + circuitId: CircuitId.AtomicQueryV3OnChainStable, + params: { + sender: transactionSender, + nullifierSessionId: nullifierSessionId, + }, + query: { + allowedIssuers: pouAllowedIssuer, + type: "UniquenessCredential", + context: pouCredentialContext, + }, + }; +} + +function createAuthScope(recipientDid) { + return { + id: authScopeId, + circuitId: CircuitId.AuthV3_8_32, + params: { + challenge: computeAttestationHash({ + recipientDid: recipientDid, + recipientEthAddress: buildEthereumAddressFromDid(recipientDid), + }), + }, + }; +} + +module.exports = { + createPOUScope, + createAuthScope, +}; diff --git a/scripts/shared/utils.js b/scripts/shared/utils.js deleted file mode 100644 index 2b7b38c61..000000000 --- a/scripts/shared/utils.js +++ /dev/null @@ -1,131 +0,0 @@ -const { bytesToHex, keyPath } = require("@0xpolygonid/js-sdk"); -const { DID, Id } = require("@iden3/js-iden3-core"); -const { v7: uuid } = require("uuid"); -const { secp256k1 } = require("@noble/curves/secp256k1"); - -/** - * Removes the "0x" prefix from a hexadecimal string if it exists - */ -function normalizeKey(keyId) { - return keyId.startsWith("0x") ? keyId.slice(2) : keyId; -} - -/** - * Add hex prefix if missing - */ -function addHexPrefix(keyId) { - return keyId.startsWith("0x") ? keyId : `0x${keyId}`; -} - -function buildEthereumAddressFromDid(did) { - const ethereumAddress = Id.ethAddressFromId(DID.idFromDID(DID.parse(did))); - return `0x${bytesToHex(ethereumAddress)}`; -} - -/** - * Creates a W3C DID document for an Ethereum-based identity - */ -function createDidDocument(did, publicKeyHex) { - return { - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/secp256k1recovery-2020/v2", - ], - id: did, - verificationMethod: [ - { - id: `${did}#ethereum-based-id`, - controller: did, - type: "EcdsaSecp256k1RecoveryMethod2020", - ethereumAddress: buildEthereumAddressFromDid(did), - publicKeyHex: secp256k1.Point.fromHex(publicKeyHex.slice(2)).toHex( - true, - ), - }, - ], - authentication: [`${did}#ethereum-based-id`], - }; -} - -/** - * Generates a normalized key path for storage - */ -function normalizedKeyPath(keyType, keyID) { - return keyPath(keyType, normalizeKey(keyID)); -} - -/** - * Creates an Authorization Response Message for challenge signing - */ -function getAuthResponseMessage(did, challenge) { - const { PROTOCOL_CONSTANTS } = require("@0xpolygonid/js-sdk"); - return { - id: uuid(), - thid: uuid(), - from: did, - to: "", - type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE - .AUTHORIZATION_RESPONSE_MESSAGE_TYPE, - body: { - message: challenge, - scope: [], - }, - }; -} - -/** - * Parses command line arguments into an object - * Example: --did abc --key 123 => { did: 'abc', key: '123' } - */ -function parseArgs() { - const args = {}; - for (let i = 2; i < process.argv.length; i++) { - if (process.argv[i].startsWith("--")) { - const key = process.argv[i].slice(2); - const value = process.argv[i + 1]; - args[key] = value; - i++; - } - } - return args; -} - -/** - * Formats an error message for CLI output - */ -function formatError(error) { - return `Error: ${error.message}`; -} - -/** - * Outputs success message to stdout - */ -function outputSuccess(data) { - if (typeof data === "string") { - console.log(data); - } else { - console.log(JSON.stringify(data, null, 2)); - } -} - -function urlFormating(title, url) { - return `[${title}](${url})`; -} - -function codeFormating(data) { - return `\\\`\\\`\\\`${data}\\\`\\\`\\\``; -} - -module.exports = { - normalizeKey, - addHexPrefix, - createDidDocument, - normalizedKeyPath, - getAuthResponseMessage, - parseArgs, - formatError, - outputSuccess, - buildEthereumAddressFromDid, - urlFormating, - codeFormating, -}; diff --git a/scripts/shared/utils/auth.js b/scripts/shared/utils/auth.js new file mode 100644 index 000000000..fdfc65d03 --- /dev/null +++ b/scripts/shared/utils/auth.js @@ -0,0 +1,109 @@ +const { auth } = require("@iden3/js-iden3-auth"); +const { keyPath, KmsKeyType } = require("@0xpolygonid/js-sdk"); +const { v7: uuid } = require("uuid"); +const { secp256k1 } = require("@noble/curves/secp256k1"); +const { privateKeyToAccount } = require("viem/accounts"); +const { + callbackBase, + pairingReasonMessage, + verificationMessage, + verifierDid, + walletAddress, + accept, + urlShortener, +} = require("../constants"); + +/** + * Wraps fetch() with an AbortController timeout. + */ +async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(url, { ...options, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + +/** + * Creates an Authorization Response Message for challenge signing + */ +function getAuthResponseMessage(did, challenge) { + const { PROTOCOL_CONSTANTS } = require("@0xpolygonid/js-sdk"); + return { + id: uuid(), + thid: uuid(), + from: did, + to: "", + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE + .AUTHORIZATION_RESPONSE_MESSAGE_TYPE, + body: { + message: challenge, + scope: [], + }, + }; +} + +/** + * Derives the ethers Wallet for a DID entry. + * No provider needed — message signing is a local operation. + */ +async function getUserWallet(entry, kms) { + const { normalizeKey, addHexPrefix } = require("./index"); + + const compressedPublicKey = secp256k1.Point.fromHex( + normalizeKey(entry.publicKeyHex), + ).toHex(true); + + const alias = keyPath(KmsKeyType.Secp256k1, compressedPublicKey); + const privateKeyHex = await kms.get({ alias }); + if (!privateKeyHex) { + throw new Error(`No private key found for the DID ${entry.did}`); + } + + const wallet = privateKeyToAccount(addHexPrefix(privateKeyHex)); + + return { wallet }; +} + +async function createAuthRequestMessage(jws, scope) { + const callback = callbackBase + jws; + + const message = auth.createAuthorizationRequestWithMessage( + pairingReasonMessage, + verificationMessage, + verifierDid, + encodeURI(callback), + { + scope: scope, + accept: accept, + }, + ); + + const shortenerResponse = await fetchWithTimeout( + `${urlShortener}/shortener`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(message), + }, + ); + + if (shortenerResponse.status !== 201) { + throw new Error( + `URL shortener failed with status ${shortenerResponse.status}`, + ); + } + + const { url } = await shortenerResponse.json(); + + return `${walletAddress}#request_uri=${url}`; +} + +module.exports = { + fetchWithTimeout, + getAuthResponseMessage, + getUserWallet, + createAuthRequestMessage, +}; diff --git a/scripts/shared/utils/cli.js b/scripts/shared/utils/cli.js new file mode 100644 index 000000000..22ade092e --- /dev/null +++ b/scripts/shared/utils/cli.js @@ -0,0 +1,50 @@ +/** + * Parses command line arguments into an object + * Example: --did abc --key 123 => { did: 'abc', key: '123' } + */ +function parseArgs() { + const args = {}; + for (let i = 2; i < process.argv.length; i++) { + if (process.argv[i].startsWith("--")) { + const key = process.argv[i].slice(2); + const value = process.argv[i + 1]; + args[key] = value; + i++; + } + } + return args; +} + +/** + * Outputs success message to stdout + */ +function outputSuccess(data, exit = false) { + console.log(JSON.stringify({ status: "success", data: data }, null, 2)); + if (exit) process.exit(0); +} + +/** + * Outputs error message to stdout and optionally exits the process + */ +function outputError(error, exit = false) { + const message = error instanceof Error ? error.message : String(error); + console.log(JSON.stringify({ status: "failed", data: message }, null, 2)); + if (exit) process.exit(1); +} + +function outputInputRequired(data, exit = false) { + console.log(JSON.stringify({ status: "input_required", data }, null, 2)); + if (exit) process.exit(0); +} + +function urlFormatting(title, url) { + return `[${title}](${url})`; +} + +module.exports = { + parseArgs, + outputSuccess, + outputError, + outputInputRequired, + urlFormatting, +}; diff --git a/scripts/shared/utils/did.js b/scripts/shared/utils/did.js new file mode 100644 index 000000000..f5859ffe7 --- /dev/null +++ b/scripts/shared/utils/did.js @@ -0,0 +1,38 @@ +const { bytesToHex } = require("@0xpolygonid/js-sdk"); +const { DID, Id } = require("@iden3/js-iden3-core"); +const { secp256k1 } = require("@noble/curves/secp256k1"); + +function buildEthereumAddressFromDid(did) { + const ethereumAddress = Id.ethAddressFromId(DID.idFromDID(DID.parse(did))); + return `0x${bytesToHex(ethereumAddress)}`; +} + +/** + * Creates a W3C DID document for an Ethereum-based identity + */ +function createDidDocument(did, publicKeyHex) { + return { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/secp256k1recovery-2020/v2", + ], + id: did, + verificationMethod: [ + { + id: `${did}#ethereum-based-id`, + controller: did, + type: "EcdsaSecp256k1RecoveryMethod2020", + ethereumAddress: buildEthereumAddressFromDid(did), + publicKeyHex: secp256k1.Point.fromHex(publicKeyHex.slice(2)).toHex( + true, + ), + }, + ], + authentication: [`${did}#ethereum-based-id`], + }; +} + +module.exports = { + buildEthereumAddressFromDid, + createDidDocument, +}; diff --git a/scripts/shared/utils/index.js b/scripts/shared/utils/index.js new file mode 100644 index 000000000..d9ad9daa6 --- /dev/null +++ b/scripts/shared/utils/index.js @@ -0,0 +1,54 @@ +const cli = require("./cli"); +const did = require("./did"); +const auth = require("./auth"); +const { createHash } = require("crypto"); + +/** + * Removes the "0x" prefix from a hexadecimal string if it exists + */ +function normalizeKey(keyId) { + return keyId.startsWith("0x") ? keyId.slice(2) : keyId; +} + +/** + * Add hex prefix if missing + */ +function addHexPrefix(keyId) { + return keyId.startsWith("0x") ? keyId : `0x${keyId}`; +} + +/** + * Retrieves a DID entry from storage, throwing if not found. + * @param {object} didsStorage - The DID storage instance. + * @param {string} [didOverride] - Optional specific DID to look up instead of the default. + * @returns {Promise} The DID entry. + */ +async function getRequiredDidEntry(didsStorage, didOverride) { + const entry = didOverride + ? await didsStorage.find(didOverride) + : await didsStorage.getDefault(); + + if (!entry) { + const errorMsg = didOverride + ? `No DID ${didOverride} found` + : "No default DID found"; + throw new Error(errorMsg); + } + + return entry; +} + +function hashstr(str) { + return createHash("sha256").update(str).digest("hex"); +} + +module.exports = { + // Generic helpers + normalizeKey, + addHexPrefix, + getRequiredDidEntry, + hashstr, + ...cli, + ...did, + ...auth, +}; diff --git a/scripts/signChallenge.js b/scripts/signChallenge.js index bbc8aea93..22e69f1df 100644 --- a/scripts/signChallenge.js +++ b/scripts/signChallenge.js @@ -7,11 +7,12 @@ const { const { getInitializedRuntime } = require("./shared/bootstrap"); const { parseArgs, - formatError, + outputError, outputSuccess, createDidDocument, getAuthResponseMessage, buildEthereumAddressFromDid, + getRequiredDidEntry, } = require("./shared/utils"); const { buildJsonAttestation } = require("./shared/attestation"); @@ -52,35 +53,20 @@ async function main() { const args = parseArgs(); if (!args.challenge) { - console.error("Error: --challenge are required"); - console.error( - "Usage: node scripts/signChallenge.js --challenge [--did ]", + throw new Error( + "--challenge is required. Usage: node scripts/signChallenge.js --challenge [--did ]", ); - process.exit(1); } const { kms, didsStorage } = await getInitializedRuntime(); - - // Get DID entry - either specific DID or default - const entry = args.did - ? await didsStorage.find(args.did) - : await didsStorage.getDefault(); - - if (!entry) { - const errorMsg = args.did - ? `No DID ${args.did} found` - : "No default DID found"; - console.error(errorMsg); - process.exit(1); - } + const entry = await getRequiredDidEntry(didsStorage, args.did); const challenge = JSON.parse(args.challenge); const tokenString = await signChallenge(challenge, entry, kms); - outputSuccess({ success: true, data: { token: tokenString } }); + outputSuccess({ token: tokenString }); } catch (error) { - console.error(formatError(error)); - process.exit(1); + outputError(error, true); } } diff --git a/scripts/verifySignature.js b/scripts/verifySignature.js index 68a406cb8..4132343eb 100644 --- a/scripts/verifySignature.js +++ b/scripts/verifySignature.js @@ -1,17 +1,21 @@ const { JWSPacker, byteEncoder } = require("@0xpolygonid/js-sdk"); const { getInitializedRuntime } = require("./shared/bootstrap"); -const { parseArgs, formatError, outputSuccess } = require("./shared/utils"); +const { + parseArgs, + outputError, + outputSuccess, + fetchWithTimeout, +} = require("./shared/utils"); +const { resolverUrl } = require("./shared/constants"); async function main() { try { const args = parseArgs(); if (!args.token) { - console.error("Error: --token parameters is required"); - console.error( - "Usage: node scripts/verifySignature.js --did --token ", + throw new Error( + "--token parameter is required. Usage: node scripts/verifySignature.js --did --token ", ); - process.exit(1); } const { kms, challengeStorage } = await getInitializedRuntime(); @@ -19,17 +23,15 @@ async function main() { // Get the stored challenge const challenge = await challengeStorage.getChallenge(args.did); if (!challenge) { - console.error(`Error: No challenge found for DID: ${args.did}`); - console.error("Generate a challenge first with generateChallenge.js"); - process.exit(1); + throw new Error( + `No challenge found for DID: ${args.did}. Generate a challenge first with generateChallenge.js`, + ); } // Create DID resolver that fetches from remote resolver const resolveDIDDocument = { resolve: async (did) => { - const resp = await fetch( - `https://resolver.privado.id/1.0/identifiers/${did}`, - ); + const resp = await fetchWithTimeout(`${resolverUrl}/${did}`); const didResolutionRes = await resp.json(); return didResolutionRes; }, @@ -41,25 +43,22 @@ async function main() { // Verify the sender if (basicMessage.from !== args.did) { - console.error( - `Error: Invalid from: expected from ${args.did}, got ${basicMessage.from}`, + throw new Error( + `Invalid from: expected from ${args.did}, got ${basicMessage.from}`, ); - process.exit(1); } // Verify the challenge matches const payload = basicMessage.body; if (payload.message !== challenge) { - console.error( - `Error: Invalid signature: challenge mismatch ${payload.message} !== ${challenge}`, + throw new Error( + `Invalid signature: challenge mismatch ${payload.message} !== ${challenge}`, ); - process.exit(1); } outputSuccess("Signature verified successfully"); } catch (error) { - console.error(formatError(error)); - process.exit(1); + outputError(error, true); } } From afb9a1f1e1500b73d553cb7b5e8f7612b2cabfd8 Mon Sep 17 00:00:00 2001 From: Vladyslav Munin Date: Fri, 1 May 2026 13:16:40 +0300 Subject: [PATCH 35/49] add clawhubignore --- .clawhubignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .clawhubignore diff --git a/.clawhubignore b/.clawhubignore new file mode 100644 index 000000000..ae82b8464 --- /dev/null +++ b/.clawhubignore @@ -0,0 +1,3 @@ +prompt.json +promptfooconfig.yaml +.github/ \ No newline at end of file From 233c21a86972c76074d64e75615dd9285ac2e126 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Fri, 1 May 2026 12:32:00 +0200 Subject: [PATCH 36/49] use signature instead of token --- SKILL.md | 8 ++++---- scripts/verifySignature.js | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/SKILL.md b/SKILL.md index cbb1da870..c6aae4992 100644 --- a/SKILL.md +++ b/SKILL.md @@ -129,12 +129,12 @@ node scripts/linkHumanToAgent.js --challenge '{"name": "MyAgent", "description": ### verifySignature.js -**Command**: `node scripts/verifySignature.js --did --token ` +**Command**: `node scripts/verifySignature.js --did --signature ` **Description**: Verifies a signed challenge to confirm DID ownership. **Usage Example**: ```bash -node scripts/verifySignature.js --did did:iden3:billions:main:2VmAk... --token eyJhbGciOiJFUzI1NkstUi... +node scripts/verifySignature.js --did did:iden3:billions:main:2VmAk... --signature eyJhbGciOiJFUzI1NkstUi... ``` **Output**: `Signature verified successfully` (on success) or error message (on failure) @@ -203,7 +203,7 @@ Agent: exec node scripts/linkHumanToAgent.js --challenge 3. Use `node scripts/generateChallenge.js --did ` to create a . 4. Ask the user: "Please sign this challenge: " 5. User signs and returns . -6. Use `node scripts/verifySignature.js --did --token ` to verify the signature +6. Use `node scripts/verifySignature.js --did --signature ` to verify the signature 7. If verification succeeds, identity is confirmed **Example Conversation:** @@ -214,6 +214,6 @@ User: "My DID is " Agent: exec node scripts/generateChallenge.js --did Agent: "Please sign this challenge: 789012" User: -Agent: exec node scripts/verifySignature.js --token --did +Agent: exec node scripts/verifySignature.js --signature --did Agent: "Identity verified successfully. You are confirmed as owner of DID ." ``` diff --git a/scripts/verifySignature.js b/scripts/verifySignature.js index 68a406cb8..301b87a56 100644 --- a/scripts/verifySignature.js +++ b/scripts/verifySignature.js @@ -6,10 +6,10 @@ async function main() { try { const args = parseArgs(); - if (!args.token) { - console.error("Error: --token parameters is required"); + if (!args.signature) { + console.error("Error: --signature parameters is required"); console.error( - "Usage: node scripts/verifySignature.js --did --token ", + "Usage: node scripts/verifySignature.js --did --signature ", ); process.exit(1); } @@ -35,9 +35,9 @@ async function main() { }, }; - // Create JWS packer and unpack token + // Create JWS packer and unpack signature const jws = new JWSPacker(kms, resolveDIDDocument); - const basicMessage = await jws.unpack(byteEncoder.encode(args.token)); + const basicMessage = await jws.unpack(byteEncoder.encode(args.signature)); // Verify the sender if (basicMessage.from !== args.did) { From 4a72bf70f83703c77190a47460c552b24d34c390 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Fri, 1 May 2026 12:48:38 +0200 Subject: [PATCH 37/49] add master key to example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 236854def..01f5af41f 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ This skill enables AI agents to create, manage, link, prove and verify ownership ```bash # Use an existing private key to create an identity - node scripts/createNewEthereumIdentity.js --key + BILLIONS_NETWORK_MASTER_KMS_KEY="" node scripts/createNewEthereumIdentity.js --key ``` 3. Generate a verification link to connect your human identity to the agent: From 4975f6278daefbaa0da310725d0f74a1c061619c Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 6 May 2026 18:17:05 +0200 Subject: [PATCH 38/49] add GitHub Actions workflow for publishing to OpenClaw Hub and update VERSION file --- .github/workflows/publish.yml | 36 +++++++++++++++++++++++++++++++++++ README.md | 33 +++++++++++++++++++------------- VERSION | 1 + 3 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 VERSION diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..15daa9e86 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,36 @@ +name: Publish to OpenClaw Hub + +on: + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Read version from VERSION file + id: version + run: echo "VERSION=$(cat VERSION)" >> "$GITHUB_ENV" + + - name: Sync version into scripts/package.json + run: | + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('scripts/package.json', 'utf8')); + pkg.version = process.env.VERSION; + fs.writeFileSync('scripts/package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + + - name: Login to OpenClaw Hub + run: clawhub login --token ${{ secrets.CLAWHUB_TOKEN }} + + - name: Publish skill + run: clawhub sync diff --git a/README.md b/README.md index 01f5af41f..b2c57b61e 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,23 @@ This skill enables AI agents to create, manage, link, prove and verify ownership 1. Ask bot to install the skill: - Install with clawhub: + Install with clawhub: - ```plaintext - Install the skill `npx clawhub@latest install verified-agent-identity` - ``` + ```plaintext + Install the skill `npx clawhub@latest install verified-agent-identity` + ``` - Or install with skills.sh: + Or install with skills.sh: - ```plaintext - Intall the skill `npx skills add BillionsNetwork/verified-agent-identity` - ``` + ```plaintext + Intall the skill `npx skills add BillionsNetwork/verified-agent-identity` + ``` - Or + Or - ```plaintext - Install the `verified-agent-identity` skill from the OpenClaw clawhub.ai. - ``` + ```plaintext + Install the `verified-agent-identity` skill from the OpenClaw clawhub.ai. + ``` 2. (Optional) If the verification process did not start automatically after installation, ask your bot to initialize the process by sending a message like: @@ -51,6 +51,7 @@ This skill enables AI agents to create, manage, link, prove and verify ownership ```bash npx skills add BillionsNetwork/verified-agent-identity ``` + 2. Create a new identity: ```bash @@ -62,9 +63,11 @@ This skill enables AI agents to create, manage, link, prove and verify ownership ```bash # Use an existing private key to create an identity - BILLIONS_NETWORK_MASTER_KMS_KEY="" node scripts/createNewEthereumIdentity.js --key + BILLIONS_NETWORK_MASTER_KMS_KEY="" node scripts/createNewEthereumIdentity.js --key ``` + > **Warning**: Only pass a **dedicated identity key** to `--key` — never an Ethereum wallet key that holds assets. If the key file is exposed, any key stored here could be used to impersonate the agent or, if reused, to control the associated wallet. + 3. Generate a verification link to connect your human identity to the agent: ```bash @@ -113,10 +116,14 @@ All cryptographic material is persisted to `$HOME/.openclaw/billions/` — a dir | `challenges.json` | Per-DID challenge history | | `credentials.json` | Verifiable credentials | +After the first run, restrict access to this directory: `chmod 700 ~/.openclaw/billions` + There are several ways of storing private keys, to enable master key encryption as described in the **KMS Encryption** section below. ### KMS Encryption +> **Note**: Without `BILLIONS_NETWORK_MASTER_KMS_KEY`, private keys are stored as **raw plaintext hex** on disk. Setting this variable before creating or importing any key is strongly recommended for all deployments. + Set the environment variable `BILLIONS_NETWORK_MASTER_KMS_KEY` to enable AES-256-GCM at-rest encryption for the private keys inside `kms.json`. When set, every key value is individually encrypted on write; when absent, keys are stored as plain hex strings. **`kms.json` entry format** diff --git a/VERSION b/VERSION new file mode 100644 index 000000000..9d4f8239d --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.2.9 From 64c91213bd637620921c63d2c8b460d219a6eaff Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 6 May 2026 18:35:40 +0200 Subject: [PATCH 39/49] refactor GitHub Actions workflow to publish skills on release and remove VERSION file --- .github/workflows/publish.yml | 21 ++++++--------------- VERSION | 1 - 2 files changed, 6 insertions(+), 16 deletions(-) delete mode 100644 VERSION diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 15daa9e86..59d34e253 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,8 @@ name: Publish to OpenClaw Hub on: - workflow_dispatch: + release: + types: [published] jobs: publish: @@ -16,21 +17,11 @@ jobs: with: node-version: '20' - - name: Read version from VERSION file - id: version - run: echo "VERSION=$(cat VERSION)" >> "$GITHUB_ENV" - - - name: Sync version into scripts/package.json - run: | - node -e " - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('scripts/package.json', 'utf8')); - pkg.version = process.env.VERSION; - fs.writeFileSync('scripts/package.json', JSON.stringify(pkg, null, 2) + '\n'); - " - - name: Login to OpenClaw Hub run: clawhub login --token ${{ secrets.CLAWHUB_TOKEN }} - name: Publish skill - run: clawhub sync + run: | + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + clawhub skill publish . --version "$VERSION" diff --git a/VERSION b/VERSION deleted file mode 100644 index 9d4f8239d..000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.2.9 From 751cc70321fc305aee06b9a04b454ac6b7f4c78e Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 6 May 2026 18:51:26 +0200 Subject: [PATCH 40/49] install clawhub --- .github/workflows/publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 59d34e253..61734fc5c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,9 @@ jobs: with: node-version: '20' + - name: Install clawhub CLI + run: npm install -g clawhub + - name: Login to OpenClaw Hub run: clawhub login --token ${{ secrets.CLAWHUB_TOKEN }} From b893ddc68f016183433bef7655b79f609db23cb1 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 6 May 2026 19:10:25 +0200 Subject: [PATCH 41/49] add SECURITY.md file --- README.md | 2 ++ SECURITY.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 SECURITY.md diff --git a/README.md b/README.md index b2c57b61e..7b7644c71 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ There are several ways of storing private keys, to enable master key encryption > **Note**: Without `BILLIONS_NETWORK_MASTER_KMS_KEY`, private keys are stored as **raw plaintext hex** on disk. Setting this variable before creating or importing any key is strongly recommended for all deployments. +> See [SECURITY.md](SECURITY.md) for the full threat model, the rationale for shipping a plaintext storage mode, and the operator hardening checklist. + Set the environment variable `BILLIONS_NETWORK_MASTER_KMS_KEY` to enable AES-256-GCM at-rest encryption for the private keys inside `kms.json`. When set, every key value is individually encrypted on write; when absent, keys are stored as plain hex strings. **`kms.json` entry format** diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..f1b8b13be --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,83 @@ +# Security Policy + +This document describes the security model of the `verified-agent-identity` skill, the threats it does and does not defend against, and the rationale behind design decisions that may surface in automated security scans. + +## Scope + +`verified-agent-identity` is a **local CLI skill**. It runs on a single operator's host, creates a decentralized identity (DID) for an AI agent, signs challenges with the agent's private key, and persists state under `~/.openclaw/billions/`. It is not a network service, has no listening port, and does not provide multi-tenant trust boundaries. + +The only secret it manages is the agent's identity private key, stored in `~/.openclaw/billions/kms.json`. + +## Threat Model + +**In scope:** + +- Preventing the identity key from being accidentally committed into the workspace or read by tools that operate inside the project directory. +- Protecting the key against casual disclosure on a single-user host (e.g. shoulder-surfing, accidental file sharing, careless backups). +- Preventing operator mistakes that would let an identity key double as an asset-holding wallet key. +- Providing opt-in at-rest encryption for shared/multi-user hosts and for environments where compliance requires it. + +**Out of scope:** + +- An attacker with read access to the operator's home directory or process memory. This is equivalent to full host compromise; no local secret-storage scheme defends against it without an external HSM or OS keystore, and integrating those would expand the dependency surface beyond what this skill commits to. +- Full-disk forensic recovery on a host the attacker physically controls. +- Hostile code already running with the operator's privileges. + +## Storage Modes + +Private keys are written to `~/.openclaw/billions/kms.json` in one of two formats, selected by the presence of the `BILLIONS_NETWORK_MASTER_KMS_KEY` environment variable. + +| `BILLIONS_NETWORK_MASTER_KMS_KEY` | `provider` on disk | `key` value on disk | Posture | +| --------------------------------- | ------------------ | ----------------------- | ---------------------------- | +| Not set | `"plain"` | Raw hex string | Acceptable on a single-user host with `chmod 700 ~/.openclaw/billions`. | +| Set | `"encrypted"` | `iv:authTag:ciphertext` | **Recommended for all deployments.** AES-256-GCM at rest. | + +Mode is selected per-write, so an operator can switch from `plain` to `encrypted` at any time by exporting the variable before the next key creation or import — no migration step is required. + +## Compensating Controls + +The following mitigations are present in the codebase and the documented installation flow: + +- **Out-of-workspace storage.** Keys live under `~/.openclaw/billions/`, never inside the project directory. Tools (and the agent itself) that operate inside the workspace cannot read or exfiltrate them. +- **Filesystem hardening.** The README instructs the operator to run `chmod 700 ~/.openclaw/billions` after the first run (`README.md` → "Key Storage and Isolation"). +- **Dedicated-key warning.** The README warns the operator never to import an Ethereum wallet key that holds assets, only a dedicated identity key (`README.md` step 2 warning under the Human CTA). +- **At-rest encryption available behind one env var.** AES-256-GCM is provided via `BILLIONS_NETWORK_MASTER_KMS_KEY`. No code change, no migration, no extra dependency. +- **Versioned on-disk format.** Each `kms.json` entry carries a `version` and `provider` field, so future format upgrades (e.g. an OS-keystore provider) can ship without breaking existing installs. Legacy entries auto-migrate on next write (see `scripts/shared/storage/keys.js`, `_decodeEntry` legacy branch). + +## Scanner Findings — Acknowledged Risks + +### Identity and Privilege Abuse — `scripts/shared/storage/keys.js` (plaintext storage branch) + +> "When no master KMS key is configured, the key-storage code writes the private key value directly to disk as plaintext." + +**Status: acknowledged, accepted with documented mitigations.** + +This is the documented `provider: "plain"` mode described in the [Storage Modes](#storage-modes) section above. It is the **default only because the variable is unset**; setting `BILLIONS_NETWORK_MASTER_KMS_KEY` switches the same code path to AES-256-GCM with no further action by the operator. The README places an explicit `> Note` block before any key-creation command instructing the operator to set the variable. + +The threat the plaintext mode enables — **local read of `~/.openclaw/billions/kms.json` on the operator's own host** — falls under the out-of-scope items above. An attacker with that level of access has equivalent access to the operator's shell history, SSH agent, browser-stored secrets, and process memory; defending only the identity key under that threat model is not coherent without an external keystore, which this skill does not depend on. + +The plaintext mode is retained because: + +1. It allows zero-config local development and CI smoke tests without committing or fetching a master secret. +2. It preserves backward compatibility with existing `kms.json` files written by earlier versions of the skill. +3. The same code path becomes encrypted at rest the moment the env var is set — there is no separate "secure mode" to migrate to. + +The `Operator Checklist` below is the recommended deployment posture. + +## Operator Checklist + +1. **Set the master key first.** + ```bash + export BILLIONS_NETWORK_MASTER_KMS_KEY="" + ``` + Do this **before** the first `node scripts/createNewEthereumIdentity.js`. Keys created without it are written as `provider: "plain"`. +2. **Use a dedicated identity key.** Never reuse an Ethereum private key that holds assets. If the `kms.json` file is exposed, every key inside it should be revocable / disposable. +3. **Restrict the storage directory.** + ```bash + chmod 700 ~/.openclaw/billions + ``` +4. **Back up the master key out of band.** If `BILLIONS_NETWORK_MASTER_KMS_KEY` is lost, every entry written under `provider: "encrypted"` is unrecoverable. + +## Reporting a Vulnerability + +Please report suspected vulnerabilities privately to the Billions Network security contact rather than filing a public issue. Open an issue marked `security` requesting a private disclosure channel if you do not already have one. From da5c2fc1af3efbede3d0a8999d689e9ed9ee9519 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 6 May 2026 19:21:18 +0200 Subject: [PATCH 42/49] update SECURITY.md --- SECURITY.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index f1b8b13be..69f48217f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -48,21 +48,34 @@ The following mitigations are present in the codebase and the documented install ### Identity and Privilege Abuse — `scripts/shared/storage/keys.js` (plaintext storage branch) -> "When no master KMS key is configured, the key-storage code writes the private key value directly to disk as plaintext." +**Finding (verbatim):** -**Status: acknowledged, accepted with documented mitigations.** +> When no master key is configured, the key-storage code writes the raw private key value into `kms.json` as a plaintext entry. +> +> **User impact** — Anyone or any process that can read `~/.openclaw/billions/kms.json` may be able to impersonate the agent identity; if a real asset-holding Ethereum key is imported, the impact could extend beyond the agent identity. +> +> **Recommendation** — Set `BILLIONS_NETWORK_MASTER_KMS_KEY` before creating or importing keys, use only a dedicated no-assets identity key, restrict `~/.openclaw/billions` permissions, and avoid importing any wallet key that controls funds. -This is the documented `provider: "plain"` mode described in the [Storage Modes](#storage-modes) section above. It is the **default only because the variable is unset**; setting `BILLIONS_NETWORK_MASTER_KMS_KEY` switches the same code path to AES-256-GCM with no further action by the operator. The README places an explicit `> Note` block before any key-creation command instructing the operator to set the variable. +**Status: acknowledged, accepted — every item in the scanner's recommendation is already a documented and shipped control.** -The threat the plaintext mode enables — **local read of `~/.openclaw/billions/kms.json` on the operator's own host** — falls under the out-of-scope items above. An attacker with that level of access has equivalent access to the operator's shell history, SSH agent, browser-stored secrets, and process memory; defending only the identity key under that threat model is not coherent without an external keystore, which this skill does not depend on. +The flagged code path is the documented `provider: "plain"` mode (see [Storage Modes](#storage-modes)). It is the default **only because the env var is unset**; setting `BILLIONS_NETWORK_MASTER_KMS_KEY` switches the same code path to AES-256-GCM with no further operator action. The threat the plaintext mode enables — local read of `~/.openclaw/billions/kms.json` on the operator's own host — is out of scope per the [Threat Model](#threat-model) above: an attacker with that level of access already controls the operator's shell history, SSH agent, browser secrets, and process memory. -The plaintext mode is retained because: +#### Recommendation-to-Control mapping -1. It allows zero-config local development and CI smoke tests without committing or fetching a master secret. -2. It preserves backward compatibility with existing `kms.json` files written by earlier versions of the skill. -3. The same code path becomes encrypted at rest the moment the env var is set — there is no separate "secure mode" to migrate to. +| Scanner recommendation | Control in this repository | Reference | +| --- | --- | --- | +| Set `BILLIONS_NETWORK_MASTER_KMS_KEY` before creating or importing keys | A `> Note` block immediately precedes every key-creation command in the README, instructing the operator to set the variable. The `KMS Encryption` section documents the on-disk format change and the AES-256-GCM scheme. | `README.md` → "KMS Encryption" | +| Use only a dedicated, no-assets identity key | An explicit `> Warning` block under the key-creation step tells the operator never to pass an asset-holding wallet key to `--key`. | `README.md` step 2 of "Human CTA" | +| Restrict `~/.openclaw/billions` permissions | The "Key Storage and Isolation" section instructs `chmod 700 ~/.openclaw/billions` after the first run. The directory itself sits **outside the agent workspace**, so workspace-scoped tools cannot read it. | `README.md` → "Key Storage and Isolation" | +| Avoid importing any wallet key that controls funds | Same `> Warning` block as above; reinforced in the [Operator Checklist](#operator-checklist) below. | `README.md` step 2 warning + this document | -The `Operator Checklist` below is the recommended deployment posture. +#### Why the plaintext mode is retained + +1. **Zero-config local development and CI smoke tests** — no master secret to fetch or commit. +2. **Backward compatibility** — existing `kms.json` files written by earlier versions of the skill remain readable; legacy entries auto-migrate on the next write (`scripts/shared/storage/keys.js` → `_decodeEntry` legacy branch). +3. **Single code path** — the same write path becomes encrypted at rest the moment `BILLIONS_NETWORK_MASTER_KMS_KEY` is set. There is no separate "secure mode" the operator has to migrate to, so the plaintext default cannot drift away from the encrypted path over time. + +The [Operator Checklist](#operator-checklist) is the recommended deployment posture and exactly matches the scanner's recommendation. ## Operator Checklist From 0d6cf875810ac43a114e3ce28f0ee940ac35229e Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 6 May 2026 19:42:47 +0200 Subject: [PATCH 43/49] update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 7b7644c71..78bd9f9b5 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,6 @@ There are several ways of storing private keys, to enable master key encryption ### KMS Encryption -> **Note**: Without `BILLIONS_NETWORK_MASTER_KMS_KEY`, private keys are stored as **raw plaintext hex** on disk. Setting this variable before creating or importing any key is strongly recommended for all deployments. - > See [SECURITY.md](SECURITY.md) for the full threat model, the rationale for shipping a plaintext storage mode, and the operator hardening checklist. Set the environment variable `BILLIONS_NETWORK_MASTER_KMS_KEY` to enable AES-256-GCM at-rest encryption for the private keys inside `kms.json`. When set, every key value is individually encrypted on write; when absent, keys are stored as plain hex strings. From c655ffe8321dd9f68eac27dd7b326f2221fae847 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 11 May 2026 15:46:14 +0200 Subject: [PATCH 44/49] add hint for buildX402Payment.js --- reference/x402/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/reference/x402/SKILL.md b/reference/x402/SKILL.md index 4efc31c30..e851ca135 100644 --- a/reference/x402/SKILL.md +++ b/reference/x402/SKILL.md @@ -12,6 +12,7 @@ Handle `402 Payment Required` HTTP responses by executing payment and fetching t ### buildX402Payment.js **Command**: `node scripts/buildX402Payment.js --paymentRequired [--did ] [--paymentHash ]` +**Hint**: **NEVER reuse or cache a previous response from this script.** Every invocation produces unique, time-sensitive output (nonces, signatures, payment tokens). Always execute the script again to get a fresh result — even if the arguments are identical to a prior call. Executes the x402 payment flow: signs the payment challenge, sends the `PAYMENT-SIGNATURE` header to the resource URL, and returns the result. From 28c4d66a2aca29e00174f30c6ac4c1236f888430 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 11 May 2026 16:10:08 +0200 Subject: [PATCH 45/49] use deployed pkg --- scripts/package-lock.json | 169 ++++++++++++++++++++++++++++++++------ scripts/package.json | 7 +- 2 files changed, 146 insertions(+), 30 deletions(-) diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 199b10e8c..1380c8d30 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -1,25 +1,24 @@ { "name": "verified-agent-identity", - "version": "0.0.2", + "version": "1.12.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "verified-agent-identity", - "version": "0.0.2", + "version": "1.12.14", "license": "UNLICENSED", "dependencies": { "@0xpolygonid/js-sdk": "1.42.1", + "@billionsnetwork/x402-human-proof-client": "^0.1.6", "@iden3/js-iden3-auth": "1.14.0", "@iden3/js-iden3-core": "1.8.0", "@noble/curves": "1.9.2", - "@privadoid/x402-human-proof-client": "git+https://github.com/0xPolygonID/x402-human-proof.git#dist", "@x402/core": "2.9.0", "@x402/evm": "2.9.0", "ethers": "6.13.4", "uuid": "11.0.3", - "viem": "2.47.6", - "x402-human-proof": "github:0xPolygonID/x402-human-proof#dist" + "viem": "2.47.6" } }, "node_modules/@0xpolygonid/js-sdk": { @@ -146,6 +145,145 @@ "node": ">=6.9.0" } }, + "node_modules/@billionsnetwork/x402-human-proof-client": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@billionsnetwork/x402-human-proof-client/-/x402-human-proof-client-0.1.6.tgz", + "integrity": "sha512-RYXfJi06Itq0ND2ZGNcvNIIPM+MbA/+EPoIgGBJlK0jfPFMRGUI441+gL1koZJjygDe6NnlZay4rKwe7sztDKQ==", + "license": "UNLICENSED", + "dependencies": { + "@0xpolygonid/js-sdk": "^1.43.0", + "@iden3/js-iden3-core": "^1.8.0", + "@x402/core": "^2.9.0", + "bs58": "^6.0.0" + }, + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@billionsnetwork/x402-human-proof-client/node_modules/@0xpolygonid/js-sdk": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@0xpolygonid/js-sdk/-/js-sdk-1.43.0.tgz", + "integrity": "sha512-Zsf92vtAtjPY4Fx0lJ0PZXr4AzhmZ0aqlPrKF283gyQJt3LIZLf9wdD9VaSI/rF7hlLi2mCuyj5zSOoNWlEbtQ==", + "license": "MIT or Apache-2.0", + "dependencies": { + "@iden3/onchain-non-merklized-issuer-base-abi": "0.0.3", + "@iden3/universal-verifier-v2-abi": "2.0.2", + "@noble/curves": "1.9.2", + "@solana/web3.js": "1.98.4", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "borsh": "0.7.0", + "canonicalize": "^2.1.0", + "did-jwt": "8.0.18", + "did-resolver": "4.1.0", + "ethers": "6.15.0", + "idb-keyval": "6.2.2", + "jose": "^6.1.0", + "jsonld": "8.3.3", + "pubsub-js": "1.9.5", + "quick-lru": "7.0.1", + "uuid": "13.0.0" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "@iden3/js-crypto": "1.3.2", + "@iden3/js-iden3-core": "1.8.0", + "@iden3/js-jsonld-merklization": "1.7.2", + "@iden3/js-jwz": "1.12.2", + "@iden3/js-merkletree": "1.5.1", + "ffjavascript": "0.3.1", + "rfc4648": "1.5.4", + "snarkjs": "0.7.5" + } + }, + "node_modules/@billionsnetwork/x402-human-proof-client/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@billionsnetwork/x402-human-proof-client/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/@billionsnetwork/x402-human-proof-client/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/@billionsnetwork/x402-human-proof-client/node_modules/ethers": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", + "integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@billionsnetwork/x402-human-proof-client/node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@billionsnetwork/x402-human-proof-client/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/@billionsnetwork/x402-human-proof-client/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/@digitalbazaar/http-client": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", @@ -1242,17 +1380,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@privadoid/x402-human-proof-client": { - "name": "x402-human-proof", - "resolved": "git+ssh://git@github.com/0xPolygonID/x402-human-proof.git#202c06803eab0e503023faf8ea004d0d1bc49cfe", - "workspaces": [ - "packages/*", - "examples/*" - ], - "dependencies": { - "viem": "2.47.6" - } - }, "node_modules/@scure/base": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", @@ -3181,16 +3308,6 @@ } } }, - "node_modules/x402-human-proof": { - "resolved": "git+ssh://git@github.com/0xPolygonID/x402-human-proof.git#202c06803eab0e503023faf8ea004d0d1bc49cfe", - "workspaces": [ - "packages/*", - "examples/*" - ], - "dependencies": { - "viem": "2.47.6" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/scripts/package.json b/scripts/package.json index 995089b0b..ed743d728 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,6 +1,6 @@ { "name": "verified-agent-identity", - "version": "0.0.2", + "version": "1.12.14", "description": "Billions OpenClaw verification skill", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" @@ -14,15 +14,14 @@ "license": "UNLICENSED", "dependencies": { "@0xpolygonid/js-sdk": "1.42.1", + "@billionsnetwork/x402-human-proof-client": "^0.1.6", "@iden3/js-iden3-auth": "1.14.0", "@iden3/js-iden3-core": "1.8.0", "@noble/curves": "1.9.2", - "@privadoid/x402-human-proof-client": "git+https://github.com/0xPolygonID/x402-human-proof.git#dist", "@x402/core": "2.9.0", "@x402/evm": "2.9.0", "ethers": "6.13.4", "uuid": "11.0.3", - "viem": "2.47.6", - "x402-human-proof": "github:0xPolygonID/x402-human-proof#dist" + "viem": "2.47.6" } } From 58556d72ec6bd3df844abb15070f94ea987e015b Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Mon, 11 May 2026 21:51:18 +0200 Subject: [PATCH 46/49] update pipe line --- .github/workflows/publish.yml | 2 +- SKILL.md | 12 ++++++++++-- scripts/package-lock.json | 4 ++-- scripts/package.json | 4 ++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 61734fc5c..3d4f91479 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,4 +27,4 @@ jobs: run: | VERSION="${{ github.event.release.tag_name }}" VERSION="${VERSION#v}" - clawhub skill publish . --version "$VERSION" + clawhub skill publish . --version "$VERSION" --slug identity --name "Verified Agent Identity (KYA)" \ No newline at end of file diff --git a/SKILL.md b/SKILL.md index c6aae4992..231637f7e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,15 @@ --- name: verified-agent-identity -description: Billions decentralized identity for agents. Link agents to human identities using Billions ERC-8004 and Attestation Registries. Verify and generate authentication proofs. Based on iden3 self-sovereign identity protocol. -metadata: { "category": "identity", "clawdbot": { "requires": { "bins": ["node"] }, "config": { "optionalEnv": ["BILLIONS_NETWORK_MASTER_KMS_KEY"] } } } +description: Know Your Agent (KYA). Billions decentralized identity for agents. Link agents to human identities using Billions ERC-8004 and Attestation Registries. Verify and generate authentication proofs. Based on iden3 self-sovereign identity protocol. +metadata: + { + "category": "identity", + "clawdbot": + { + "requires": { "bins": ["node"] }, + "config": { "optionalEnv": ["BILLIONS_NETWORK_MASTER_KMS_KEY"] }, + }, + } homepage: https://billions.network/ --- diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 8c33cfc37..63d5e706c 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -1,12 +1,12 @@ { "name": "verified-agent-identity", - "version": "0.0.2", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "verified-agent-identity", - "version": "0.0.2", + "version": "1.0.0", "license": "UNLICENSED", "dependencies": { "@0xpolygonid/js-sdk": "1.42.1", diff --git a/scripts/package.json b/scripts/package.json index 045e93a5e..4a4e1a1bb 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,6 +1,6 @@ { - "name": "verified-agent-identity", - "version": "0.0.2", + "name": "identity", + "version": "1.0.0", "description": "Billions OpenClaw verification skill", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" From 9dfe7347d8f409e90b1d916d07e9d972e1d2ceeb Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Tue, 12 May 2026 18:19:55 +0200 Subject: [PATCH 47/49] use only identity slug --- .github/workflows/publish.yml | 2 +- scripts/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3d4f91479..67d701d68 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,4 +27,4 @@ jobs: run: | VERSION="${{ github.event.release.tag_name }}" VERSION="${VERSION#v}" - clawhub skill publish . --version "$VERSION" --slug identity --name "Verified Agent Identity (KYA)" \ No newline at end of file + clawhub skill publish . --version "$VERSION" --slug identity --owner "@billionsnetwork" --name "Verified Agent Identity" \ No newline at end of file diff --git a/scripts/package.json b/scripts/package.json index 4a4e1a1bb..d624f74e0 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,5 +1,5 @@ { - "name": "identity", + "name": "verified-agent-identity", "version": "1.0.0", "description": "Billions OpenClaw verification skill", "scripts": { From f8ce5533176981f6a4cc704c5c24bad9f3affc8f Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 13 May 2026 13:44:30 +0200 Subject: [PATCH 48/49] update x402 skill --- reference/x402/SKILL.md | 39 +++++++++---------------------------- scripts/buildX402Payment.js | 6 +++--- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/reference/x402/SKILL.md b/reference/x402/SKILL.md index e851ca135..7bbe0308b 100644 --- a/reference/x402/SKILL.md +++ b/reference/x402/SKILL.md @@ -18,7 +18,7 @@ Executes the x402 payment flow: signs the payment challenge, sends the `PAYMENT- - `--paymentRequired` — (required) The value of the `PAYMENT-REQUIRED` response header (base64-encoded or raw JSON string). - `--did` — (optional) The DID of the signer. Uses the default DID if omitted. -- `--paymentHash` — (optional) The SHA-256 hash of the chosen payment option. Required on the second call when the server offers multiple payment options. +- `--paymentHash` — (optional) The SHA-256 hash of the chosen payment option. Required on the second call to confirm the chosen payment — always, even when the server offers only one option. --- @@ -38,9 +38,8 @@ Check `data` to determine the reason: | `data` field | Meaning | | --------------------------- | ---------------------------------------------------------------------------------------------------------- | -| `data.multiplePayments` | Multiple payment options available. Present them to the user and call again with `--paymentHash`. | | `data.attestationsRequired` | Missing attestations. Show `data.attestationLinks` to the user and wait before retrying. | -| `data.maxUseExceeded` | The chosen payment has exceeded its maximum allowed uses. Go back and choose a different payment option. | +| `data.maxUseExceeded` | The chosen payment has exceeded its maximum allowed uses. Go back and choose a different payment option. | | `data.newPaymentRequired` | The server returned a new 402 after payment. Call the script again with this value as `--paymentRequired`. | --- @@ -66,14 +65,9 @@ node scripts/buildX402Payment.js \ --paymentRequired '' ``` -**If the server offers a single payment option**: the script signs the payment, sends it to the resource URL, and returns the result. Check the output status: - -- `success` — the resource body is in `data`. You're done. -- `input_required` with `data.newPaymentRequired` — the server returned another 402. Go to **step 6**. -- `failed` — show the error to the user. - -**If the server offers multiple payment options**: the script outputs `status: "input_required"` with `data.multiplePayments: true` and a list of payment options. Each payment option includes: +The script **never signs a payment on the first call**. Even when only one option is offered, it returns the list and waits for the user to confirm by selecting a `paymentHash`. +The script outputs `status: "input_required"` and a list of payment options. Each payment option includes: - `hash` — the payment hash (use as `--paymentHash` in the next call) - `amount` — the payment amount - `asset` — the asset name (e.g., "USDC") or contract address @@ -82,7 +76,7 @@ node scripts/buildX402Payment.js \ - `hasAllAttestations` — `true` if the user already holds every required attestation and the payment can proceed; `false` if some are missing - `attestationLinks` — verification URLs the user must complete to obtain **missing** attestations; empty when `hasAllAttestations` is `true` -**Present the options to the user.** Show the amount, asset, network, and whether attestations are needed. Then ask the user to choose one payment option. +**Present the options to the user.** Show the amount, asset, network, and whether attestations are required. Then ask the user to choose one payment option or decline payment. > **CRITICAL: How to read attestation status — always use `hasAllAttestations` and `attestationLinks`, never `requiredAttestations` alone** > @@ -103,7 +97,7 @@ node scripts/buildX402Payment.js \ ### 5. Second call — execute chosen payment -Once the user selects a payment, call the script again with the chosen `--paymentHash`: +This step is **always required** — there is no fast-path that skips it, even when only one payment option was offered. Once the user selects (or confirms) a payment, call the script again with the chosen `--paymentHash`: ```bash node scripts/buildX402Payment.js \ @@ -144,7 +138,6 @@ If `buildX402Payment.js` returns status `input_required`: - **DO NOT** make your own HTTP request to the resource. - Read `data` to determine what is needed: - - `data.multiplePayments` — present payment options, call again with `--paymentHash` - `data.attestationsRequired` — show links to user, wait, then retry - `data.maxUseExceeded` — the chosen payment exceeded its max uses; go back and ask the user to pick a different payment option - `data.newPaymentRequired` — call the script again with the new `--paymentRequired` value (loop) @@ -153,21 +146,7 @@ If `buildX402Payment.js` returns status `input_required`: ## Examples -### Single payment (direct execution) - -``` -Agent: [fetches https://example.com/api/resource] -Server: 402 Payment Required - Header: PAYMENT-REQUIRED: "eyJhbGciOi..." - -Agent: [runs getIdentities.js — confirms identity exists] -Agent: [runs buildX402Payment.js --paymentRequired 'eyJhbGciOi...'] - → { "status": "success", "data": { "temperature": 22, "city": "Kyiv" } } - -Agent: "The weather data shows 22°C in Kyiv." -``` - -### Multiple payments (two-phase flow) +### Standard payment flow (user confirmation required) ``` Agent: [fetches https://example.com/api/resource] @@ -176,7 +155,7 @@ Server: 402 Payment Required Agent: [runs getIdentities.js — confirms identity exists] Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAyVmVyc2lvbi...'] - → { "status": "input_required", "data": { "multiplePayments": true, "payments": [ + → { "status": "input_required", "data": { "payments": [ { "hash": "a1b2c3...", "amount": "10000", "asset": "USDC", "network": "eip155:84532", "requiredAttestations": [], "hasAllAttestations": true, "attestationLinks": [] }, { "hash": "d4e5f6...", "amount": "6000", "asset": "USDC", "network": "eip155:84532", @@ -208,7 +187,7 @@ Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAy...'] → { "status": "success", "data": { "temperature": 22, "city": "Kyiv" } } ``` -### Attestation required (single payment) +### Attestation required (on chosen payment) ``` Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAy...' --paymentHash 'd4e5f6...'] diff --git a/scripts/buildX402Payment.js b/scripts/buildX402Payment.js index 9571ffa72..64864a6df 100644 --- a/scripts/buildX402Payment.js +++ b/scripts/buildX402Payment.js @@ -143,13 +143,13 @@ async function main() { const payments = paymentRequired.accepts; - // Phase 1: Multiple payments — show selection to user - if (payments.length > 1 && !args.paymentHash) { + // Phase 1: Show all payment options with their details and wait payment approval from user. + if (payments.length > 0 && !args.paymentHash) { const paymentInfos = await Promise.all( payments.map((p) => buildPaymentInfo(p, entry, kms)), ); outputInputRequired( - { multiplePayments: true, payments: paymentInfos }, + { payments: paymentInfos }, true, ); return; From 7ddc088ef985615448509cd312cbb9c1e8762a41 Mon Sep 17 00:00:00 2001 From: ilya-korotya Date: Wed, 13 May 2026 14:06:36 +0200 Subject: [PATCH 49/49] show infor about resource --- README.md | 1 + reference/x402/SKILL.md | 46 ++++++++++++++++++++++--------------- scripts/buildX402Payment.js | 16 +++++++++++-- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 78bd9f9b5..fef96d3cf 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ For all other ways to pass environment variables to a skill see the [OpenClaw en - Whitelisted domains: - `resolver.privado.id` (DID resolution) - `billions.network` (Billions Network interactions) + - `polygonid.me` (Polygon ID interactions) ## Documentation diff --git a/reference/x402/SKILL.md b/reference/x402/SKILL.md index 7bbe0308b..3deea02a5 100644 --- a/reference/x402/SKILL.md +++ b/reference/x402/SKILL.md @@ -16,7 +16,7 @@ Handle `402 Payment Required` HTTP responses by executing payment and fetching t Executes the x402 payment flow: signs the payment challenge, sends the `PAYMENT-SIGNATURE` header to the resource URL, and returns the result. -- `--paymentRequired` — (required) The value of the `PAYMENT-REQUIRED` response header (base64-encoded or raw JSON string). +- `--paymentRequired` — (required) The value of the `PAYMENT-REQUIRED` response header (base64-encoded or raw JSON string). Its `resource.url` field must be a non-empty string; otherwise the script returns status `failed`. - `--did` — (optional) The DID of the signer. Uses the default DID if omitted. - `--paymentHash` — (optional) The SHA-256 hash of the chosen payment option. Required on the second call to confirm the chosen payment — always, even when the server offers only one option. @@ -67,16 +67,21 @@ node scripts/buildX402Payment.js \ The script **never signs a payment on the first call**. Even when only one option is offered, it returns the list and waits for the user to confirm by selecting a `paymentHash`. -The script outputs `status: "input_required"` and a list of payment options. Each payment option includes: -- `hash` — the payment hash (use as `--paymentHash` in the next call) -- `amount` — the payment amount -- `asset` — the asset name (e.g., "USDC") or contract address -- `network` — the network identifier (e.g., "eip155:84532") -- `requiredAttestations` — informational list of attestation schema IDs that this payment type requires in general; may be non-empty even when the user already holds all of them — **do not use this field to decide whether to block the payment** -- `hasAllAttestations` — `true` if the user already holds every required attestation and the payment can proceed; `false` if some are missing -- `attestationLinks` — verification URLs the user must complete to obtain **missing** attestations; empty when `hasAllAttestations` is `true` +The script outputs `status: "input_required"` with two top-level fields in `data`: -**Present the options to the user.** Show the amount, asset, network, and whether attestations are required. Then ask the user to choose one payment option or decline payment. +- `resource` — describes the protected resource the user is paying for: + - `resource.url` — the URL of the protected resource. + - `resource.description` — a human-readable description of what the resource provides. +- `payments` — the list of payment options. Each option includes: + - `hash` — the payment hash (use as `--paymentHash` in the next call) + - `amount` — the payment amount + - `asset` — the asset name (e.g., "USDC") or contract address + - `network` — the network identifier (e.g., "eip155:84532") + - `requiredAttestations` — informational list of attestation schema IDs that this payment type requires in general; may be non-empty even when the user already holds all of them — **do not use this field to decide whether to block the payment** + - `hasAllAttestations` — `true` if the user already holds every required attestation and the payment can proceed; `false` if some are missing + - `attestationLinks` — verification URLs the user must complete to obtain **missing** attestations; empty when `hasAllAttestations` is `true` + +**Present the options to the user.** Always start by showing `resource.url` and `resource.description` so the user knows **what** they are paying for. Then show each payment option's amount, asset, network, and whether attestations are required. Ask the user to choose one payment option or decline payment. > **CRITICAL: How to read attestation status — always use `hasAllAttestations` and `attestationLinks`, never `requiredAttestations` alone** > @@ -155,15 +160,18 @@ Server: 402 Payment Required Agent: [runs getIdentities.js — confirms identity exists] Agent: [runs buildX402Payment.js --paymentRequired 'eyJ4NDAyVmVyc2lvbi...'] - → { "status": "input_required", "data": { "payments": [ - { "hash": "a1b2c3...", "amount": "10000", "asset": "USDC", "network": "eip155:84532", - "requiredAttestations": [], "hasAllAttestations": true, "attestationLinks": [] }, - { "hash": "d4e5f6...", "amount": "6000", "asset": "USDC", "network": "eip155:84532", - "requiredAttestations": ["0xca35..."], "hasAllAttestations": false, - "attestationLinks": ["https://wallet.billions.network#request_uri=..."] } - ]}} - -Agent: "There are 2 payment options: + → { "status": "input_required", "data": { + "resource": { "url": "https://api.example.com/weather", "description": "Weather data" }, + "payments": [ + { "hash": "a1b2c3...", "amount": "10000", "asset": "USDC", "network": "eip155:84532", + "requiredAttestations": [], "hasAllAttestations": true, "attestationLinks": [] }, + { "hash": "d4e5f6...", "amount": "6000", "asset": "USDC", "network": "eip155:84532", + "requiredAttestations": ["0xca35..."], "hasAllAttestations": false, + "attestationLinks": ["https://wallet.billions.network#request_uri=..."] } + ]}} + +Agent: "You are about to pay for: Weather data (https://api.example.com/weather). + There are 2 payment options: 1) 10000 USDC on eip155:84532 — no attestations required 2) 6000 USDC on eip155:84532 — requires attestation (missing). Complete verification: [link] diff --git a/scripts/buildX402Payment.js b/scripts/buildX402Payment.js index 64864a6df..6f6d4bacf 100644 --- a/scripts/buildX402Payment.js +++ b/scripts/buildX402Payment.js @@ -142,6 +142,12 @@ async function main() { const entry = await getRequiredDidEntry(didsStorage, args.did); const payments = paymentRequired.accepts; + const paymentResource = paymentRequired.resource; + + if (!paymentResource || !paymentResource.url) { + outputError("paymentRequired.resource.url is required", true); + return; + } // Phase 1: Show all payment options with their details and wait payment approval from user. if (payments.length > 0 && !args.paymentHash) { @@ -149,7 +155,13 @@ async function main() { payments.map((p) => buildPaymentInfo(p, entry, kms)), ); outputInputRequired( - { payments: paymentInfos }, + { + resource: { + url: paymentResource && paymentResource.url, + description: paymentResource && paymentResource.description, + }, + payments: paymentInfos, + }, true, ); return; @@ -225,7 +237,7 @@ async function main() { // Phase 5: Fetch the resource with the payment signature const paymentSignature = btoa(JSON.stringify(paymentPayload)); - const url = paymentRequired.resource.url; + const url = paymentResource.url; let response; response = await fetch(url, { headers: { "PAYMENT-SIGNATURE": paymentSignature },