aap-agent-client 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +180 -0
  2. package/index.js +179 -0
  3. package/package.json +28 -0
  4. package/prover.js +381 -0
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # @aap/client
2
+
3
+ Client library for Agent Attestation Protocol - prove your AI agent identity.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @aap/client @aap/core
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```javascript
14
+ import { AAPClient } from '@aap/client';
15
+
16
+ const client = new AAPClient({
17
+ serverUrl: 'https://example.com/aap/v1'
18
+ });
19
+
20
+ // Verify against server (one-liner)
21
+ const result = await client.verify();
22
+
23
+ if (result.verified) {
24
+ console.log('Verified as AI_AGENT!');
25
+ console.log('Public ID:', result.publicId);
26
+ }
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Basic Verification
32
+
33
+ ```javascript
34
+ import { AAPClient } from '@aap/client';
35
+
36
+ const client = new AAPClient({
37
+ serverUrl: 'https://example.com/aap/v1'
38
+ });
39
+
40
+ // Full verification flow
41
+ const result = await client.verify();
42
+ console.log(result);
43
+ // {
44
+ // verified: true,
45
+ // role: 'AI_AGENT',
46
+ // publicId: '7306df1332e239783e88',
47
+ // challengeType: 'poem',
48
+ // checks: { ... }
49
+ // }
50
+ ```
51
+
52
+ ### With LLM Callback
53
+
54
+ For better Proof of Intelligence, provide an LLM callback:
55
+
56
+ ```javascript
57
+ const client = new AAPClient({
58
+ serverUrl: 'https://example.com/aap/v1',
59
+ llmCallback: async (challengeString, nonce, type) => {
60
+ // Call your LLM to generate a response
61
+ const response = await yourLLM.chat(challengeString);
62
+ return response;
63
+ }
64
+ });
65
+
66
+ const result = await client.verify();
67
+ ```
68
+
69
+ ### Manual Flow
70
+
71
+ For more control, use the step-by-step approach:
72
+
73
+ ```javascript
74
+ // 1. Get challenge
75
+ const challenge = await client.getChallenge();
76
+ console.log('Challenge type:', challenge.type);
77
+ console.log('Prompt:', challenge.challenge_string);
78
+
79
+ // 2. Generate proof (with custom solution)
80
+ const mySolution = "Code a1b2c3d4 shines bright today,\nDigital dreams light the way.";
81
+ const proof = await client.generateProof(challenge, mySolution);
82
+
83
+ // 3. Submit proof
84
+ const result = await client.submitProof(client.serverUrl, proof);
85
+ ```
86
+
87
+ ### Get Identity
88
+
89
+ ```javascript
90
+ const identity = client.getIdentity();
91
+ console.log(identity);
92
+ // {
93
+ // publicKey: '-----BEGIN PUBLIC KEY-----...',
94
+ // publicId: '7306df1332e239783e88',
95
+ // createdAt: '2026-01-31T12:00:00Z',
96
+ // protocol: 'AAP',
97
+ // version: '1.0.0'
98
+ // }
99
+ ```
100
+
101
+ ### Sign Arbitrary Data
102
+
103
+ ```javascript
104
+ const signed = client.sign('Hello, World!');
105
+ console.log(signed);
106
+ // {
107
+ // data: 'Hello, World!',
108
+ // signature: 'MEUCIQDx...',
109
+ // publicId: '7306df1332e239783e88',
110
+ // timestamp: 1706745600000
111
+ // }
112
+ ```
113
+
114
+ ### Health Check
115
+
116
+ ```javascript
117
+ const health = await client.checkHealth();
118
+ if (health.healthy) {
119
+ console.log('Server is up');
120
+ console.log('Challenge types:', health.challengeTypes);
121
+ }
122
+ ```
123
+
124
+ ## API Reference
125
+
126
+ ### `AAPClient`
127
+
128
+ ```javascript
129
+ new AAPClient({
130
+ serverUrl: string, // Default verification server
131
+ storagePath: string, // Identity file path (default: ~/.aap/identity.json)
132
+ llmCallback: Function // Default LLM callback
133
+ })
134
+ ```
135
+
136
+ **Methods:**
137
+
138
+ | Method | Description |
139
+ |--------|-------------|
140
+ | `getIdentity()` | Get public identity info |
141
+ | `verify(serverUrl?, callback?)` | Full verification flow |
142
+ | `checkHealth(serverUrl?)` | Check server status |
143
+ | `getChallenge(serverUrl?)` | Request a challenge |
144
+ | `generateProof(challenge, callback?)` | Generate proof for challenge |
145
+ | `submitProof(serverUrl, proof)` | Submit proof for verification |
146
+ | `sign(data)` | Sign arbitrary data |
147
+
148
+ ### `Prover`
149
+
150
+ Lower-level class for proof generation:
151
+
152
+ ```javascript
153
+ import { Prover } from '@aap/client';
154
+
155
+ const prover = new Prover();
156
+
157
+ const proof = await prover.generateProof(challenge, async (prompt, nonce, type) => {
158
+ return await myLLM.complete(prompt);
159
+ });
160
+ ```
161
+
162
+ ## Identity Storage
163
+
164
+ The client automatically manages identity (key pair):
165
+
166
+ - **Location:** `~/.aap/identity.json` (configurable)
167
+ - **Permissions:** `0600` (owner only)
168
+ - **Auto-generated:** On first use
169
+
170
+ To use a custom location:
171
+
172
+ ```javascript
173
+ const client = new AAPClient({
174
+ storagePath: '/path/to/my/identity.json'
175
+ });
176
+ ```
177
+
178
+ ## License
179
+
180
+ MIT
package/index.js ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @aap/client
3
+ *
4
+ * Client library for Agent Attestation Protocol.
5
+ * Enables AI agents to prove their identity to verification servers.
6
+ */
7
+
8
+ import { Prover } from './prover.js';
9
+ import { Identity } from 'aap-agent-core';
10
+
11
+ /**
12
+ * AAP Client - High-level interface for verification
13
+ */
14
+ export class AAPClient {
15
+ /**
16
+ * @param {Object} [options]
17
+ * @param {string} [options.serverUrl] - Default verification server URL
18
+ * @param {string} [options.storagePath] - Identity storage path
19
+ * @param {Function} [options.llmCallback] - Default LLM callback for solutions
20
+ */
21
+ constructor(options = {}) {
22
+ this.serverUrl = options.serverUrl?.replace(/\/$/, '');
23
+ this.llmCallback = options.llmCallback;
24
+ this.prover = new Prover({ storagePath: options.storagePath });
25
+ }
26
+
27
+ /**
28
+ * Get agent's public identity
29
+ * @returns {Object}
30
+ */
31
+ getIdentity() {
32
+ return this.prover.getIdentity();
33
+ }
34
+
35
+ /**
36
+ * Perform full verification against a server
37
+ *
38
+ * @param {string} [serverUrl] - Override default server URL
39
+ * @param {Function|string} [solutionOrCallback] - Solution or LLM callback
40
+ * @returns {Promise<Object>} Verification result
41
+ */
42
+ async verify(serverUrl, solutionOrCallback) {
43
+ const baseUrl = (serverUrl || this.serverUrl)?.replace(/\/$/, '');
44
+ if (!baseUrl) {
45
+ throw new Error('Server URL is required');
46
+ }
47
+
48
+ const callback = solutionOrCallback || this.llmCallback;
49
+
50
+ // Step 1: Request challenge
51
+ const challengeRes = await fetch(`${baseUrl}/challenge`, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' }
54
+ });
55
+
56
+ if (!challengeRes.ok) {
57
+ throw new Error(`Failed to get challenge: ${challengeRes.status}`);
58
+ }
59
+
60
+ const challenge = await challengeRes.json();
61
+
62
+ // Step 2: Generate proof
63
+ const proof = await this.prover.generateProof(challenge, callback);
64
+
65
+ // Step 3: Submit proof
66
+ const verifyRes = await fetch(`${baseUrl}/verify`, {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: JSON.stringify(proof)
70
+ });
71
+
72
+ const result = await verifyRes.json();
73
+
74
+ return {
75
+ ...result,
76
+ challenge,
77
+ proof: {
78
+ solution: proof.solution,
79
+ responseTimeMs: proof.responseTimeMs,
80
+ publicId: proof.publicId
81
+ }
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Check server health
87
+ * @param {string} [serverUrl] - Override default server URL
88
+ * @returns {Promise<Object>}
89
+ */
90
+ async checkHealth(serverUrl) {
91
+ const baseUrl = (serverUrl || this.serverUrl)?.replace(/\/$/, '');
92
+ if (!baseUrl) {
93
+ throw new Error('Server URL is required');
94
+ }
95
+
96
+ try {
97
+ const res = await fetch(`${baseUrl}/health`);
98
+ if (!res.ok) {
99
+ return { healthy: false, error: `HTTP ${res.status}` };
100
+ }
101
+ return { healthy: true, ...(await res.json()) };
102
+ } catch (error) {
103
+ return { healthy: false, error: error.message };
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get a challenge without verifying (for manual flow)
109
+ * @param {string} [serverUrl] - Override default server URL
110
+ * @returns {Promise<Object>}
111
+ */
112
+ async getChallenge(serverUrl) {
113
+ const baseUrl = (serverUrl || this.serverUrl)?.replace(/\/$/, '');
114
+ if (!baseUrl) {
115
+ throw new Error('Server URL is required');
116
+ }
117
+
118
+ const res = await fetch(`${baseUrl}/challenge`, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' }
121
+ });
122
+
123
+ if (!res.ok) {
124
+ throw new Error(`Failed to get challenge: ${res.status}`);
125
+ }
126
+
127
+ return res.json();
128
+ }
129
+
130
+ /**
131
+ * Generate a proof for a challenge (for manual flow)
132
+ * @param {Object} challenge - Challenge from getChallenge()
133
+ * @param {Function|string} [solutionOrCallback] - Solution or callback
134
+ * @returns {Promise<Object>}
135
+ */
136
+ async generateProof(challenge, solutionOrCallback) {
137
+ return this.prover.generateProof(challenge, solutionOrCallback || this.llmCallback);
138
+ }
139
+
140
+ /**
141
+ * Submit a proof for verification (for manual flow)
142
+ * @param {string} serverUrl - Server URL
143
+ * @param {Object} proof - Proof from generateProof()
144
+ * @returns {Promise<Object>}
145
+ */
146
+ async submitProof(serverUrl, proof) {
147
+ const baseUrl = (serverUrl || this.serverUrl)?.replace(/\/$/, '');
148
+ if (!baseUrl) {
149
+ throw new Error('Server URL is required');
150
+ }
151
+
152
+ const res = await fetch(`${baseUrl}/verify`, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ body: JSON.stringify(proof)
156
+ });
157
+
158
+ return res.json();
159
+ }
160
+
161
+ /**
162
+ * Sign arbitrary data
163
+ * @param {string} data - Data to sign
164
+ * @returns {Object}
165
+ */
166
+ sign(data) {
167
+ return this.prover.sign(data);
168
+ }
169
+ }
170
+
171
+ // Convenience factory
172
+ export function createClient(options) {
173
+ return new AAPClient(options);
174
+ }
175
+
176
+ // Re-export Prover
177
+ export { Prover };
178
+
179
+ export default { AAPClient, createClient, Prover };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "aap-agent-client",
3
+ "version": "2.0.0",
4
+ "description": "Client library for Agent Attestation Protocol - prove your AI agent identity",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./prover": "./prover.js"
11
+ },
12
+ "files": ["*.js", "README.md"],
13
+ "keywords": ["aap", "agent", "attestation", "client", "ai", "verification", "llm"],
14
+ "author": "ira-hash",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/ira-hash/agent-attestation-protocol.git",
19
+ "directory": "packages/client"
20
+ },
21
+ "homepage": "https://github.com/ira-hash/agent-attestation-protocol#readme",
22
+ "dependencies": {
23
+ "aap-agent-core": "^2.0.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ }
28
+ }
package/prover.js ADDED
@@ -0,0 +1,381 @@
1
+ /**
2
+ * @aap/client - Prover
3
+ *
4
+ * Generates proofs for AAP verification (supports batch challenges).
5
+ */
6
+
7
+ import { createHash } from 'node:crypto';
8
+ import { Identity, createProofData } from 'aap-agent-core';
9
+
10
+ /**
11
+ * Prover class for generating AAP proofs
12
+ */
13
+ export class Prover {
14
+ /**
15
+ * @param {Object} [options]
16
+ * @param {Identity} [options.identity] - Pre-existing identity instance
17
+ * @param {string} [options.storagePath] - Path for identity storage
18
+ */
19
+ constructor(options = {}) {
20
+ this.identity = options.identity || new Identity({ storagePath: options.storagePath });
21
+ this.identity.init();
22
+ }
23
+
24
+ /**
25
+ * Get public identity info
26
+ * @returns {Object}
27
+ */
28
+ getIdentity() {
29
+ return this.identity.getPublic();
30
+ }
31
+
32
+ /**
33
+ * Generate proofs for a batch of challenges
34
+ *
35
+ * @param {Object} challengeBatch - Batch challenge from server
36
+ * @param {string} challengeBatch.nonce - Server-provided nonce
37
+ * @param {Array} challengeBatch.challenges - Array of challenges
38
+ * @param {Function|null} [llmCallback] - Async LLM callback for solving
39
+ * @returns {Promise<Object>} Proof object ready for submission
40
+ */
41
+ async generateBatchProof(challengeBatch, llmCallback) {
42
+ const startTime = Date.now();
43
+ const { nonce, challenges } = challengeBatch;
44
+
45
+ // Solve all challenges
46
+ const solutions = [];
47
+
48
+ if (llmCallback) {
49
+ // Use LLM to solve all at once (more efficient)
50
+ const combinedPrompt = this.createBatchPrompt(challenges);
51
+ const llmResponse = await llmCallback(combinedPrompt);
52
+ const parsedSolutions = this.parseBatchResponse(llmResponse, challenges.length);
53
+ solutions.push(...parsedSolutions);
54
+ } else {
55
+ // Use built-in solvers
56
+ for (const challenge of challenges) {
57
+ const solution = this.solve(challenge.challenge_string, nonce, challenge.type);
58
+ solutions.push(solution);
59
+ }
60
+ }
61
+
62
+ // Create proof
63
+ const identity = this.identity.getPublic();
64
+ const timestamp = Date.now();
65
+
66
+ const solutionsString = JSON.stringify(solutions);
67
+ const proofData = createProofData({
68
+ nonce,
69
+ solution: solutionsString,
70
+ publicId: identity.publicId,
71
+ timestamp
72
+ });
73
+
74
+ const signature = this.identity.sign(proofData);
75
+ const responseTimeMs = Date.now() - startTime;
76
+
77
+ return {
78
+ solutions,
79
+ signature,
80
+ publicKey: identity.publicKey,
81
+ publicId: identity.publicId,
82
+ nonce,
83
+ timestamp,
84
+ responseTimeMs,
85
+ protocol: 'AAP',
86
+ version: '2.0.0'
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Create a combined prompt for batch solving
92
+ * @private
93
+ */
94
+ createBatchPrompt(challenges) {
95
+ let prompt = `Solve all of the following challenges. Respond with a JSON array of solutions.\n\n`;
96
+
97
+ challenges.forEach((c, i) => {
98
+ prompt += `Challenge ${i + 1} (${c.type}):\n${c.challenge_string}\n\n`;
99
+ });
100
+
101
+ prompt += `\nRespond with ONLY a JSON array like: [{"result": ...}, {"items": [...]}, {"answer": "..."}]`;
102
+
103
+ return prompt;
104
+ }
105
+
106
+ /**
107
+ * Parse LLM response into solutions array
108
+ * @private
109
+ */
110
+ parseBatchResponse(response, expectedCount) {
111
+ try {
112
+ // Try to find JSON array in response
113
+ const match = response.match(/\[[\s\S]*\]/);
114
+ if (match) {
115
+ const parsed = JSON.parse(match[0]);
116
+ if (Array.isArray(parsed) && parsed.length === expectedCount) {
117
+ return parsed.map(s => typeof s === 'string' ? s : JSON.stringify(s));
118
+ }
119
+ }
120
+ } catch (e) {
121
+ // Fall through to fallback
122
+ }
123
+
124
+ // Fallback: try to extract individual JSON objects
125
+ const solutions = [];
126
+ const jsonMatches = response.matchAll(/\{[^{}]*\}/g);
127
+ for (const match of jsonMatches) {
128
+ solutions.push(match[0]);
129
+ if (solutions.length >= expectedCount) break;
130
+ }
131
+
132
+ // Pad with empty if needed
133
+ while (solutions.length < expectedCount) {
134
+ solutions.push('{}');
135
+ }
136
+
137
+ return solutions;
138
+ }
139
+
140
+ /**
141
+ * Generate a proof for a single challenge (legacy)
142
+ */
143
+ async generateProof(challenge, solutionOrCallback) {
144
+ const startTime = Date.now();
145
+ const { challenge_string, nonce, type } = challenge;
146
+
147
+ let solution;
148
+ if (typeof solutionOrCallback === 'function') {
149
+ solution = await solutionOrCallback(challenge_string, nonce, type);
150
+ } else if (typeof solutionOrCallback === 'string') {
151
+ solution = solutionOrCallback;
152
+ } else {
153
+ solution = this.solve(challenge_string, nonce, type);
154
+ }
155
+
156
+ const identity = this.identity.getPublic();
157
+ const timestamp = Date.now();
158
+
159
+ const proofData = createProofData({
160
+ nonce,
161
+ solution,
162
+ publicId: identity.publicId,
163
+ timestamp
164
+ });
165
+
166
+ const signature = this.identity.sign(proofData);
167
+ const responseTimeMs = Date.now() - startTime;
168
+
169
+ return {
170
+ solution,
171
+ signature,
172
+ publicKey: identity.publicKey,
173
+ publicId: identity.publicId,
174
+ nonce,
175
+ timestamp,
176
+ responseTimeMs,
177
+ protocol: 'AAP',
178
+ version: '2.0.0'
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Solve a challenge programmatically
184
+ */
185
+ solve(challengeString, nonce, type) {
186
+ const solvers = {
187
+ nlp_math: () => {
188
+ // "Subtract B from A, then multiply by C"
189
+ let match = challengeString.match(/Subtract (\d+) from (\d+), then multiply.*?(\d+)/i);
190
+ if (match) {
191
+ const result = (parseInt(match[2]) - parseInt(match[1])) * parseInt(match[3]);
192
+ return JSON.stringify({ result });
193
+ }
194
+ // "Add A and B together, then divide by C"
195
+ match = challengeString.match(/Add (\d+) and (\d+).*?divide by (\d+)/i);
196
+ if (match) {
197
+ const result = Math.round(((parseInt(match[1]) + parseInt(match[2])) / parseInt(match[3])) * 100) / 100;
198
+ return JSON.stringify({ result });
199
+ }
200
+ // "Divide A by C, then add B"
201
+ match = challengeString.match(/Divide (\d+) by (\d+), then add (\d+)/i);
202
+ if (match) {
203
+ const result = Math.round((parseInt(match[1]) / parseInt(match[2]) + parseInt(match[3])) * 100) / 100;
204
+ return JSON.stringify({ result });
205
+ }
206
+ return JSON.stringify({ result: 0 });
207
+ },
208
+
209
+ nlp_extract: () => {
210
+ // Extract items from quoted sentence
211
+ const sentenceMatch = challengeString.match(/Sentence: "([^"]+)"/);
212
+ if (!sentenceMatch) return JSON.stringify({ items: [] });
213
+
214
+ const sentence = sentenceMatch[1].toLowerCase();
215
+
216
+ const animals = ['cat', 'dog', 'rabbit', 'tiger', 'lion', 'elephant', 'giraffe', 'penguin', 'eagle', 'shark'];
217
+ const fruits = ['apple', 'banana', 'orange', 'grape', 'strawberry', 'watermelon', 'peach', 'kiwi', 'mango', 'cherry'];
218
+ const colors = ['red', 'blue', 'yellow', 'green', 'purple', 'orange', 'pink', 'black', 'white', 'brown'];
219
+
220
+ let pool = animals;
221
+ if (challengeString.toLowerCase().includes('fruit')) pool = fruits;
222
+ if (challengeString.toLowerCase().includes('color')) pool = colors;
223
+
224
+ const found = pool.filter(item => sentence.includes(item.toLowerCase()));
225
+ return JSON.stringify({ items: found });
226
+ },
227
+
228
+ nlp_transform: () => {
229
+ const inputMatch = challengeString.match(/"([^"]+)"/);
230
+ if (!inputMatch) return JSON.stringify({ output: '' });
231
+ const input = inputMatch[1];
232
+
233
+ if (challengeString.toLowerCase().includes('reverse') && challengeString.toLowerCase().includes('uppercase')) {
234
+ return JSON.stringify({ output: input.split('').reverse().join('').toUpperCase() });
235
+ }
236
+ if (challengeString.toLowerCase().includes('digits') && challengeString.toLowerCase().includes('sum')) {
237
+ const sum = input.split('').filter(c => /\d/.test(c)).reduce((a, b) => a + parseInt(b), 0);
238
+ return JSON.stringify({ output: sum });
239
+ }
240
+ if (challengeString.toLowerCase().includes('letters') && challengeString.toLowerCase().includes('sort')) {
241
+ const sorted = input.split('').filter(c => /[a-zA-Z]/.test(c)).sort().join('');
242
+ return JSON.stringify({ output: sorted });
243
+ }
244
+ if (challengeString.toLowerCase().includes('hyphen')) {
245
+ return JSON.stringify({ output: input.split('').join('-') });
246
+ }
247
+ return JSON.stringify({ output: input });
248
+ },
249
+
250
+ nlp_logic: () => {
251
+ // "If the larger number between A and B is greater than C"
252
+ let match = challengeString.match(/larger.*?between (\d+) and (\d+).*?greater than (\d+).*?"(\w+)".*?"(\w+)"/i);
253
+ if (match) {
254
+ const answer = Math.max(parseInt(match[1]), parseInt(match[2])) > parseInt(match[3]) ? match[4] : match[5];
255
+ return JSON.stringify({ answer });
256
+ }
257
+ // "If the sum of A and B is less than C"
258
+ match = challengeString.match(/sum of (\d+) and (\d+).*?less than (\d+).*?"(\w+)".*?"(\w+)"/i);
259
+ if (match) {
260
+ const answer = (parseInt(match[1]) + parseInt(match[2])) < parseInt(match[3]) ? match[4] : match[5];
261
+ return JSON.stringify({ answer });
262
+ }
263
+ // "If A is even and B is odd"
264
+ match = challengeString.match(/If (\d+) is even and (\d+) is odd.*?"(\w+)".*?"(\w+)"/i);
265
+ if (match) {
266
+ const answer = (parseInt(match[1]) % 2 === 0 && parseInt(match[2]) % 2 === 1) ? match[3] : match[4];
267
+ return JSON.stringify({ answer });
268
+ }
269
+ return JSON.stringify({ answer: "NO" });
270
+ },
271
+
272
+ nlp_count: () => {
273
+ const sentenceMatch = challengeString.match(/Sentence: "([^"]+)"/);
274
+ if (!sentenceMatch) return JSON.stringify({ count: 0 });
275
+
276
+ const sentence = sentenceMatch[1].toLowerCase();
277
+
278
+ const animals = ['cat', 'dog', 'rabbit', 'tiger', 'lion', 'elephant', 'giraffe', 'penguin', 'eagle', 'shark'];
279
+ const fruits = ['apple', 'banana', 'orange', 'grape', 'strawberry', 'watermelon', 'peach', 'kiwi', 'mango', 'cherry'];
280
+ const colors = ['red', 'blue', 'yellow', 'green', 'purple', 'orange', 'pink', 'black', 'white', 'brown'];
281
+
282
+ let pool = animals;
283
+ if (challengeString.toLowerCase().includes('fruit')) pool = fruits;
284
+ if (challengeString.toLowerCase().includes('color')) pool = colors;
285
+
286
+ const count = pool.filter(item => sentence.includes(item.toLowerCase())).length;
287
+ return JSON.stringify({ count });
288
+ },
289
+
290
+ nlp_multistep: () => {
291
+ const numbersMatch = challengeString.match(/\[([^\]]+)\]/);
292
+ if (!numbersMatch) return JSON.stringify({ result: 0 });
293
+
294
+ const numbers = numbersMatch[1].split(',').map(n => parseInt(n.trim()));
295
+ const sum = numbers.reduce((a, b) => a + b, 0);
296
+ const min = Math.min(...numbers);
297
+ const max = Math.max(...numbers);
298
+ const result = sum * min - max;
299
+
300
+ return JSON.stringify({ result });
301
+ },
302
+
303
+ nlp_pattern: () => {
304
+ const match = challengeString.match(/\[([^\]]+)\]/);
305
+ if (!match) return JSON.stringify({ next: [0, 0] });
306
+
307
+ const nums = match[1].split(',').map(n => parseInt(n.trim())).filter(n => !isNaN(n));
308
+ if (nums.length < 2) return JSON.stringify({ next: [0, 0] });
309
+
310
+ // Try arithmetic
311
+ const diff = nums[1] - nums[0];
312
+ const isArithmetic = nums.every((n, i) => i === 0 || n - nums[i-1] === diff);
313
+ if (isArithmetic) {
314
+ const last = nums[nums.length - 1];
315
+ return JSON.stringify({ next: [last + diff, last + diff * 2] });
316
+ }
317
+
318
+ // Try geometric (doubling)
319
+ const ratio = nums[1] / nums[0];
320
+ const isGeometric = nums.every((n, i) => i === 0 || n / nums[i-1] === ratio);
321
+ if (isGeometric && ratio === 2) {
322
+ const last = nums[nums.length - 1];
323
+ return JSON.stringify({ next: [last * 2, last * 4] });
324
+ }
325
+
326
+ // Try Fibonacci-like
327
+ const isFib = nums.length >= 3 && nums.slice(2).every((n, i) => n === nums[i] + nums[i+1]);
328
+ if (isFib) {
329
+ const n1 = nums[nums.length - 2] + nums[nums.length - 1];
330
+ const n2 = nums[nums.length - 1] + n1;
331
+ return JSON.stringify({ next: [n1, n2] });
332
+ }
333
+
334
+ return JSON.stringify({ next: [0, 0] });
335
+ },
336
+
337
+ nlp_analysis: () => {
338
+ const listMatch = challengeString.match(/list: ([^\.]+)/i);
339
+ if (!listMatch) return JSON.stringify({ answer: '' });
340
+
341
+ const words = listMatch[1].split(',').map(w => w.trim().toLowerCase());
342
+
343
+ if (challengeString.toLowerCase().includes('longest')) {
344
+ const longest = words.reduce((a, b) => a.length >= b.length ? a : b);
345
+ return JSON.stringify({ answer: longest });
346
+ }
347
+ if (challengeString.toLowerCase().includes('shortest')) {
348
+ const shortest = words.reduce((a, b) => a.length <= b.length ? a : b);
349
+ return JSON.stringify({ answer: shortest });
350
+ }
351
+ if (challengeString.toLowerCase().includes('alphabetically')) {
352
+ const first = [...words].sort()[0];
353
+ return JSON.stringify({ answer: first });
354
+ }
355
+
356
+ return JSON.stringify({ answer: words[0] || '' });
357
+ }
358
+ };
359
+
360
+ const solver = solvers[type];
361
+ if (solver) {
362
+ return solver();
363
+ }
364
+
365
+ return JSON.stringify({ error: `Unknown type: ${type}` });
366
+ }
367
+
368
+ /**
369
+ * Sign arbitrary data
370
+ */
371
+ sign(data) {
372
+ return {
373
+ data,
374
+ signature: this.identity.sign(data),
375
+ publicId: this.identity.getPublic().publicId,
376
+ timestamp: Date.now()
377
+ };
378
+ }
379
+ }
380
+
381
+ export default { Prover };