@buildersgarden/siwa 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/memory.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * memory.ts
3
+ *
4
+ * Read/write helpers for the agent's MEMORY.md public identity file.
5
+ * MEMORY.md uses the pattern: - **Key**: `value`
6
+ *
7
+ * IMPORTANT: This file stores ONLY public data (address, agentId, etc.).
8
+ * Private keys are managed exclusively by keystore.ts.
9
+ *
10
+ * Dependencies: fs (Node built-in)
11
+ */
12
+ import * as fs from 'fs';
13
+ const DEFAULT_MEMORY_PATH = './MEMORY.md';
14
+ /**
15
+ * Ensure MEMORY.md exists. If not, copy from template or create minimal.
16
+ */
17
+ export function ensureMemoryExists(memoryPath = DEFAULT_MEMORY_PATH, templatePath) {
18
+ if (fs.existsSync(memoryPath))
19
+ return;
20
+ if (templatePath && fs.existsSync(templatePath)) {
21
+ fs.copyFileSync(templatePath, memoryPath);
22
+ }
23
+ else {
24
+ const minimal = `# Agent Identity Memory
25
+
26
+ > This file contains **public** agent identity data only. The private key is stored
27
+ > securely in the keystore backend (OS keychain or encrypted file) — never here.
28
+
29
+ ## Wallet
30
+
31
+ - **Address**: \`<NOT SET>\`
32
+ - **Keystore Backend**: \`<NOT SET>\`
33
+ - **Keystore Path**: \`<NOT SET>\`
34
+ - **Created At**: \`<NOT SET>\`
35
+
36
+ ## Registration
37
+
38
+ - **Status**: \`unregistered\`
39
+ - **Agent ID**: \`<NOT SET>\`
40
+ - **Agent Registry**: \`<NOT SET>\`
41
+ - **Agent URI**: \`<NOT SET>\`
42
+ - **Chain ID**: \`<NOT SET>\`
43
+ - **Registered At**: \`<NOT SET>\`
44
+
45
+ ## Agent Profile
46
+
47
+ - **Name**: \`<NOT SET>\`
48
+ - **Description**: \`<NOT SET>\`
49
+ - **Image**: \`<NOT SET>\`
50
+
51
+ ## Services
52
+
53
+ ## Sessions
54
+
55
+ ## Notes
56
+ `;
57
+ fs.writeFileSync(memoryPath, minimal);
58
+ }
59
+ }
60
+ /**
61
+ * Read all populated fields from MEMORY.md.
62
+ * Returns a map of Key → value (skips <NOT SET> fields).
63
+ */
64
+ export function readMemory(memoryPath = DEFAULT_MEMORY_PATH) {
65
+ if (!fs.existsSync(memoryPath))
66
+ return {};
67
+ const content = fs.readFileSync(memoryPath, 'utf-8');
68
+ const fields = {};
69
+ for (const line of content.split('\n')) {
70
+ const match = line.match(/^- \*\*(.+?)\*\*:\s*`(.+?)`/);
71
+ if (match && match[2] !== '<NOT SET>') {
72
+ fields[match[1]] = match[2];
73
+ }
74
+ }
75
+ return fields;
76
+ }
77
+ /**
78
+ * Write a single field value in MEMORY.md.
79
+ */
80
+ export function writeMemoryField(key, value, memoryPath = DEFAULT_MEMORY_PATH) {
81
+ let content = fs.readFileSync(memoryPath, 'utf-8');
82
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
83
+ const pattern = new RegExp(`(- \\*\\*${escaped}\\*\\*:\\s*)\`.+?\``);
84
+ if (pattern.test(content)) {
85
+ content = content.replace(pattern, `$1\`${value}\``);
86
+ }
87
+ else {
88
+ content += `\n- **${key}**: \`${value}\`\n`;
89
+ }
90
+ fs.writeFileSync(memoryPath, content);
91
+ }
92
+ /**
93
+ * Append a line under a ## Section header in MEMORY.md.
94
+ */
95
+ export function appendToMemorySection(section, line, memoryPath = DEFAULT_MEMORY_PATH) {
96
+ let content = fs.readFileSync(memoryPath, 'utf-8');
97
+ const marker = `## ${section}`;
98
+ const idx = content.indexOf(marker);
99
+ if (idx === -1) {
100
+ content += `\n## ${section}\n\n${line}\n`;
101
+ }
102
+ else {
103
+ const headerEnd = content.indexOf('\n', idx);
104
+ if (headerEnd === -1) {
105
+ content += `\n${line}\n`;
106
+ }
107
+ else {
108
+ let insertPos = headerEnd + 1;
109
+ const afterHeader = content.slice(insertPos);
110
+ const commentMatch = afterHeader.match(/^<!--.*?-->\n/);
111
+ if (commentMatch)
112
+ insertPos += commentMatch[0].length;
113
+ if (content[insertPos] === '\n')
114
+ insertPos += 1;
115
+ content = content.slice(0, insertPos) + line + '\n' + content.slice(insertPos);
116
+ }
117
+ }
118
+ fs.writeFileSync(memoryPath, content);
119
+ }
120
+ /**
121
+ * Check if the agent has a wallet address recorded.
122
+ * Note: This only checks MEMORY.md — the actual key is in the keystore.
123
+ */
124
+ export function hasWalletRecord(memoryPath = DEFAULT_MEMORY_PATH) {
125
+ const mem = readMemory(memoryPath);
126
+ return !!mem['Address'];
127
+ }
128
+ /**
129
+ * Check if the agent is registered onchain.
130
+ */
131
+ export function isRegistered(memoryPath = DEFAULT_MEMORY_PATH) {
132
+ const mem = readMemory(memoryPath);
133
+ return mem['Status'] === 'registered' && !!mem['Agent ID'];
134
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * proxy-auth.ts
3
+ *
4
+ * Shared HMAC-SHA256 authentication utility for the keyring proxy.
5
+ * Used by both the proxy client (keystore.ts) and server (test/proxy/index.ts).
6
+ *
7
+ * Security features:
8
+ * - HMAC-SHA256 over method + path + body + timestamp
9
+ * - 30-second timestamp drift limit for replay protection
10
+ * - Constant-time comparison via crypto.timingSafeEqual
11
+ */
12
+ export interface HmacHeaders {
13
+ 'X-Proxy-Timestamp': string;
14
+ 'X-Proxy-Signature': string;
15
+ }
16
+ /**
17
+ * Compute HMAC-SHA256 headers for an outgoing request.
18
+ */
19
+ export declare function computeHmac(secret: string, method: string, path: string, body: string): HmacHeaders;
20
+ /**
21
+ * Verify an incoming HMAC-SHA256 signature.
22
+ * Returns { valid: true } or { valid: false, error: string }.
23
+ */
24
+ export declare function verifyHmac(secret: string, method: string, path: string, body: string, timestamp: string, signature: string): {
25
+ valid: boolean;
26
+ error?: string;
27
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * proxy-auth.ts
3
+ *
4
+ * Shared HMAC-SHA256 authentication utility for the keyring proxy.
5
+ * Used by both the proxy client (keystore.ts) and server (test/proxy/index.ts).
6
+ *
7
+ * Security features:
8
+ * - HMAC-SHA256 over method + path + body + timestamp
9
+ * - 30-second timestamp drift limit for replay protection
10
+ * - Constant-time comparison via crypto.timingSafeEqual
11
+ */
12
+ import * as crypto from 'crypto';
13
+ const MAX_DRIFT_MS = 30_000; // 30 seconds
14
+ /**
15
+ * Compute HMAC-SHA256 headers for an outgoing request.
16
+ */
17
+ export function computeHmac(secret, method, path, body) {
18
+ const timestamp = Date.now().toString();
19
+ const payload = `${method.toUpperCase()}\n${path}\n${timestamp}\n${body}`;
20
+ const signature = crypto
21
+ .createHmac('sha256', secret)
22
+ .update(payload)
23
+ .digest('hex');
24
+ return {
25
+ 'X-Proxy-Timestamp': timestamp,
26
+ 'X-Proxy-Signature': signature,
27
+ };
28
+ }
29
+ /**
30
+ * Verify an incoming HMAC-SHA256 signature.
31
+ * Returns { valid: true } or { valid: false, error: string }.
32
+ */
33
+ export function verifyHmac(secret, method, path, body, timestamp, signature) {
34
+ // Check timestamp drift
35
+ const ts = parseInt(timestamp, 10);
36
+ if (isNaN(ts))
37
+ return { valid: false, error: 'Invalid timestamp' };
38
+ const drift = Math.abs(Date.now() - ts);
39
+ if (drift > MAX_DRIFT_MS) {
40
+ return { valid: false, error: `Timestamp drift ${drift}ms exceeds ${MAX_DRIFT_MS}ms limit` };
41
+ }
42
+ // Recompute expected signature
43
+ const payload = `${method.toUpperCase()}\n${path}\n${timestamp}\n${body}`;
44
+ const expected = crypto
45
+ .createHmac('sha256', secret)
46
+ .update(payload)
47
+ .digest('hex');
48
+ // Constant-time comparison
49
+ const sigBuf = Buffer.from(signature, 'utf-8');
50
+ const expBuf = Buffer.from(expected, 'utf-8');
51
+ if (sigBuf.length !== expBuf.length) {
52
+ return { valid: false, error: 'Signature mismatch' };
53
+ }
54
+ if (!crypto.timingSafeEqual(sigBuf, expBuf)) {
55
+ return { valid: false, error: 'Signature mismatch' };
56
+ }
57
+ return { valid: true };
58
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * registry.ts
3
+ *
4
+ * Agent Identity Registry reader.
5
+ * Provides functions to read agent profiles and reputation from on-chain registries.
6
+ *
7
+ * Dependencies:
8
+ * npm install ethers
9
+ */
10
+ import { ethers } from 'ethers';
11
+ /** Service endpoint types defined in ERC-8004 */
12
+ export type ServiceType = 'web' | 'A2A' | 'MCP' | 'OASF' | 'ENS' | 'DID' | 'email';
13
+ /** Trust models defined in ERC-8004 */
14
+ export type TrustModel = 'reputation' | 'crypto-economic' | 'tee-attestation';
15
+ /** Predefined reputation feedback tags defined in ERC-8004 */
16
+ export type ReputationTag = 'starred' | 'reachable' | 'ownerVerified' | 'uptime' | 'successRate' | 'responseTime' | 'blocktimeFreshness' | 'revenues' | 'tradingYield';
17
+ export interface AgentService {
18
+ name: ServiceType | (string & {});
19
+ endpoint: string;
20
+ version?: string;
21
+ }
22
+ export interface AgentRegistration {
23
+ agentId: number;
24
+ agentRegistry: string;
25
+ }
26
+ export interface AgentMetadata {
27
+ name: string;
28
+ description: string;
29
+ image: string;
30
+ services: AgentService[];
31
+ active: boolean;
32
+ x402Support?: boolean;
33
+ supportedTrust?: (TrustModel | (string & {}))[];
34
+ registrations?: AgentRegistration[];
35
+ }
36
+ export interface AgentProfile {
37
+ agentId: number;
38
+ owner: string;
39
+ uri: string;
40
+ agentWallet: string | null;
41
+ metadata: AgentMetadata | null;
42
+ }
43
+ export interface GetAgentOptions {
44
+ registryAddress: string;
45
+ provider: ethers.Provider;
46
+ fetchMetadata?: boolean;
47
+ }
48
+ export interface ReputationSummary {
49
+ count: number;
50
+ score: number;
51
+ rawValue: bigint;
52
+ decimals: number;
53
+ }
54
+ export interface GetReputationOptions {
55
+ reputationRegistryAddress: string;
56
+ provider: ethers.Provider;
57
+ clients?: string[];
58
+ tag1?: ReputationTag | (string & {});
59
+ tag2?: string;
60
+ }
61
+ /**
62
+ * Read an agent from the Identity Registry and parse its profile.
63
+ *
64
+ * @param agentId The on-chain agent token ID
65
+ * @param options Registry address, provider, and optional fetchMetadata flag
66
+ */
67
+ export declare function getAgent(agentId: number, options: GetAgentOptions): Promise<AgentProfile>;
68
+ /**
69
+ * Read an agent's reputation summary from the Reputation Registry.
70
+ *
71
+ * @param agentId The on-chain agent token ID
72
+ * @param options Reputation registry address, provider, and optional filters
73
+ */
74
+ export declare function getReputation(agentId: number, options: GetReputationOptions): Promise<ReputationSummary>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * registry.ts
3
+ *
4
+ * Agent Identity Registry reader.
5
+ * Provides functions to read agent profiles and reputation from on-chain registries.
6
+ *
7
+ * Dependencies:
8
+ * npm install ethers
9
+ */
10
+ import { ethers } from 'ethers';
11
+ // ─── ABI Fragments ──────────────────────────────────────────────────
12
+ const IDENTITY_REGISTRY_ABI = [
13
+ 'function ownerOf(uint256 tokenId) view returns (address)',
14
+ 'function tokenURI(uint256 tokenId) view returns (string)',
15
+ 'function getAgentWallet(uint256 agentId) view returns (address)',
16
+ ];
17
+ const REPUTATION_REGISTRY_ABI = [
18
+ 'function getSummary(uint256 agentId, address[] clients, string tag1, string tag2) view returns (uint64 count, int128 summaryValue, uint8 valueDecimals)',
19
+ ];
20
+ // ─── Internal Helpers ───────────────────────────────────────────────
21
+ const IPFS_GATEWAY = 'https://gateway.pinata.cloud/ipfs/';
22
+ /**
23
+ * Resolve a URI to its JSON content.
24
+ * Supports ipfs://, data:application/json;base64, and http(s):// schemes.
25
+ */
26
+ async function resolveURI(uri) {
27
+ if (uri.startsWith('ipfs://')) {
28
+ const cid = uri.slice('ipfs://'.length);
29
+ const response = await fetch(`${IPFS_GATEWAY}${cid}`);
30
+ if (!response.ok)
31
+ throw new Error(`IPFS fetch failed: ${response.status}`);
32
+ return response.json();
33
+ }
34
+ if (uri.startsWith('data:application/json;base64,')) {
35
+ const base64 = uri.slice('data:application/json;base64,'.length);
36
+ const json = Buffer.from(base64, 'base64').toString('utf-8');
37
+ return JSON.parse(json);
38
+ }
39
+ if (uri.startsWith('http://') || uri.startsWith('https://')) {
40
+ const response = await fetch(uri);
41
+ if (!response.ok)
42
+ throw new Error(`HTTP fetch failed: ${response.status}`);
43
+ return response.json();
44
+ }
45
+ throw new Error(`Unsupported URI scheme: ${uri}`);
46
+ }
47
+ // ─── Public API ─────────────────────────────────────────────────────
48
+ /**
49
+ * Read an agent from the Identity Registry and parse its profile.
50
+ *
51
+ * @param agentId The on-chain agent token ID
52
+ * @param options Registry address, provider, and optional fetchMetadata flag
53
+ */
54
+ export async function getAgent(agentId, options) {
55
+ const { registryAddress, provider, fetchMetadata = true } = options;
56
+ const registry = new ethers.Contract(registryAddress, IDENTITY_REGISTRY_ABI, provider);
57
+ const [owner, uri, walletAddr] = await Promise.all([
58
+ registry.ownerOf(agentId),
59
+ registry.tokenURI(agentId),
60
+ registry.getAgentWallet(agentId),
61
+ ]);
62
+ const agentWallet = walletAddr === ethers.ZeroAddress ? null : walletAddr;
63
+ let metadata = null;
64
+ if (fetchMetadata) {
65
+ try {
66
+ const raw = await resolveURI(uri);
67
+ metadata = raw;
68
+ }
69
+ catch {
70
+ metadata = null;
71
+ }
72
+ }
73
+ return { agentId, owner, uri, agentWallet, metadata };
74
+ }
75
+ /**
76
+ * Read an agent's reputation summary from the Reputation Registry.
77
+ *
78
+ * @param agentId The on-chain agent token ID
79
+ * @param options Reputation registry address, provider, and optional filters
80
+ */
81
+ export async function getReputation(agentId, options) {
82
+ const { reputationRegistryAddress, provider, clients = [], tag1 = '', tag2 = '', } = options;
83
+ const reputation = new ethers.Contract(reputationRegistryAddress, REPUTATION_REGISTRY_ABI, provider);
84
+ const [count, summaryValue, valueDecimals] = await reputation.getSummary(agentId, clients, tag1, tag2);
85
+ const decimals = Number(valueDecimals);
86
+ const rawValue = BigInt(summaryValue);
87
+ const score = Number(rawValue) / 10 ** decimals;
88
+ return { count: Number(count), score, rawValue, decimals };
89
+ }
package/dist/siwa.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * siwa.ts
3
+ *
4
+ * SIWA (Sign In With Agent) utility functions.
5
+ * Provides message building, signing (agent-side), and verification (server-side).
6
+ *
7
+ * Dependencies:
8
+ * npm install ethers
9
+ */
10
+ import { ethers } from 'ethers';
11
+ import { AgentProfile, ServiceType, TrustModel } from './registry.js';
12
+ export interface SIWAMessageFields {
13
+ domain: string;
14
+ address: string;
15
+ statement?: string;
16
+ uri: string;
17
+ version?: string;
18
+ agentId: number;
19
+ agentRegistry: string;
20
+ chainId: number;
21
+ nonce: string;
22
+ issuedAt: string;
23
+ expirationTime?: string;
24
+ notBefore?: string;
25
+ requestId?: string;
26
+ }
27
+ export interface SIWAVerificationResult {
28
+ valid: boolean;
29
+ address: string;
30
+ agentId: number;
31
+ agentRegistry: string;
32
+ chainId: number;
33
+ error?: string;
34
+ agent?: AgentProfile;
35
+ }
36
+ export interface SIWAVerifyCriteria {
37
+ minScore?: number;
38
+ minFeedbackCount?: number;
39
+ reputationRegistryAddress?: string;
40
+ requiredServices?: (ServiceType | (string & {}))[];
41
+ mustBeActive?: boolean;
42
+ requiredTrust?: (TrustModel | (string & {}))[];
43
+ custom?: (agent: AgentProfile) => boolean | Promise<boolean>;
44
+ }
45
+ /**
46
+ * Build a SIWA plaintext message string from structured fields.
47
+ */
48
+ export declare function buildSIWAMessage(fields: SIWAMessageFields): string;
49
+ /**
50
+ * Parse a SIWA message string back into structured fields.
51
+ */
52
+ export declare function parseSIWAMessage(message: string): SIWAMessageFields;
53
+ /**
54
+ * Generate a cryptographically secure nonce (≥ 8 alphanumeric characters).
55
+ */
56
+ export declare function generateNonce(length?: number): string;
57
+ /**
58
+ * Sign a SIWA message using the secure keystore.
59
+ *
60
+ * The private key is loaded from the keystore, used to sign, and discarded.
61
+ * It is NEVER returned or exposed to the caller.
62
+ *
63
+ * @param fields — SIWA message fields (domain, agentId, etc.)
64
+ * @param keystoreConfig — Optional keystore configuration override
65
+ * @returns { message, signature } — only the plaintext message and EIP-191 signature
66
+ */
67
+ export declare function signSIWAMessage(fields: SIWAMessageFields, keystoreConfig?: import('./keystore').KeystoreConfig): Promise<{
68
+ message: string;
69
+ signature: string;
70
+ }>;
71
+ /**
72
+ * Sign a SIWA message using a raw private key.
73
+ * ⚠️ DEPRECATED: Use signSIWAMessage() with keystore instead.
74
+ * Kept only for server-side testing or environments without keystore.
75
+ */
76
+ export declare function signSIWAMessageUnsafe(privateKey: string, fields: SIWAMessageFields): Promise<{
77
+ message: string;
78
+ signature: string;
79
+ }>;
80
+ /**
81
+ * Verify a SIWA message + signature.
82
+ *
83
+ * Checks:
84
+ * 1. Message format validity
85
+ * 2. Signature → address recovery
86
+ * 3. Address matches message
87
+ * 4. Domain matches expected domain
88
+ * 5. Nonce matches (caller must validate against their nonce store)
89
+ * 6. Time window (expirationTime / notBefore)
90
+ * 7. Onchain: ownerOf(agentId) === recovered address
91
+ *
92
+ * @param message Full SIWA message string
93
+ * @param signature EIP-191 signature hex string
94
+ * @param expectedDomain The server's domain (for domain binding)
95
+ * @param nonceValid Callback that returns true if the nonce is valid and unconsumed
96
+ * @param provider ethers Provider for onchain verification
97
+ * @param criteria Optional criteria to validate agent profile/reputation after ownership check
98
+ */
99
+ export declare function verifySIWA(message: string, signature: string, expectedDomain: string, nonceValid: (nonce: string) => boolean | Promise<boolean>, provider: ethers.Provider, criteria?: SIWAVerifyCriteria): Promise<SIWAVerificationResult>;