@peac/adapter-openclaw 0.10.9 → 0.10.11

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/tools.js DELETED
@@ -1,732 +0,0 @@
1
- "use strict";
2
- /**
3
- * @peac/adapter-openclaw - Plugin Tools
4
- *
5
- * Tools exposed by the PEAC receipts plugin for OpenClaw.
6
- *
7
- * @experimental This module is experimental and may change.
8
- */
9
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- var desc = Object.getOwnPropertyDescriptor(m, k);
12
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
- desc = { enumerable: true, get: function() { return m[k]; } };
14
- }
15
- Object.defineProperty(o, k2, desc);
16
- }) : (function(o, m, k, k2) {
17
- if (k2 === undefined) k2 = k;
18
- o[k2] = m[k];
19
- }));
20
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
- Object.defineProperty(o, "default", { enumerable: true, value: v });
22
- }) : function(o, v) {
23
- o["default"] = v;
24
- });
25
- var __importStar = (this && this.__importStar) || (function () {
26
- var ownKeys = function(o) {
27
- ownKeys = Object.getOwnPropertyNames || function (o) {
28
- var ar = [];
29
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
- return ar;
31
- };
32
- return ownKeys(o);
33
- };
34
- return function (mod) {
35
- if (mod && mod.__esModule) return mod;
36
- var result = {};
37
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
- __setModuleDefault(result, mod);
39
- return result;
40
- };
41
- })();
42
- Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.createStatusTool = createStatusTool;
44
- exports.createExportBundleTool = createExportBundleTool;
45
- exports.createVerifyTool = createVerifyTool;
46
- exports.createQueryTool = createQueryTool;
47
- // =============================================================================
48
- // Constants
49
- // =============================================================================
50
- /** Maximum files to scan for status (DoS protection) */
51
- const MAX_STATUS_FILE_SCAN = 1000;
52
- /** Maximum files to parse for query (DoS protection) */
53
- const MAX_QUERY_FILE_PARSE = 10000;
54
- /** Maximum file size in bytes to read (DoS protection) - 10MB */
55
- const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
56
- /** Allowed JWS algorithms (security) */
57
- const ALLOWED_ALGORITHMS = ['EdDSA', 'ES256', 'ES384', 'ES512', 'RS256', 'RS384', 'RS512'];
58
- /** Algorithm to key type compatibility map */
59
- const ALG_KEY_TYPE_MAP = {
60
- EdDSA: { kty: 'OKP', crv: ['Ed25519', 'Ed448'] },
61
- ES256: { kty: 'EC', crv: ['P-256'] },
62
- ES384: { kty: 'EC', crv: ['P-384'] },
63
- ES512: { kty: 'EC', crv: ['P-521'] },
64
- RS256: { kty: 'RSA' },
65
- RS384: { kty: 'RSA' },
66
- RS512: { kty: 'RSA' },
67
- };
68
- // =============================================================================
69
- // Status Tool
70
- // =============================================================================
71
- /**
72
- * Create the peac_receipts.status tool.
73
- * Shows spool size, last receipt time, and config summary.
74
- */
75
- function createStatusTool(stats, outputDir) {
76
- return {
77
- name: 'peac_receipts.status',
78
- description: 'Show PEAC receipts status: spool size, last receipt time, config summary',
79
- parameters: {},
80
- async execute() {
81
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
82
- const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
83
- // Use path.resolve for portability
84
- const resolvedOutputDir = pathModule.resolve(outputDir);
85
- // Count receipts in output directory (capped for performance)
86
- let receiptCount = 0;
87
- let lastReceiptTime = null;
88
- let oldestReceiptTime = null;
89
- let countApproximate = false;
90
- try {
91
- const files = await fs.promises.readdir(resolvedOutputDir);
92
- const receiptFiles = files.filter((f) => f.endsWith('.peac.json'));
93
- receiptCount = receiptFiles.length;
94
- // Cap the scan for performance
95
- const filesToScan = receiptFiles.slice(0, MAX_STATUS_FILE_SCAN);
96
- countApproximate = receiptFiles.length > MAX_STATUS_FILE_SCAN;
97
- if (filesToScan.length > 0) {
98
- // Get timestamps from file stats (capped)
99
- const fileStats = await Promise.all(filesToScan.map(async (f) => {
100
- const stat = await fs.promises.stat(pathModule.join(resolvedOutputDir, f));
101
- return { file: f, mtime: stat.mtime };
102
- }));
103
- fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
104
- lastReceiptTime = fileStats[0].mtime.toISOString();
105
- oldestReceiptTime = fileStats[fileStats.length - 1].mtime.toISOString();
106
- }
107
- }
108
- catch {
109
- // Directory might not exist yet
110
- }
111
- return {
112
- status: 'ok',
113
- spool: {
114
- pending_entries: stats.pendingCount,
115
- total_captured: stats.totalCaptured,
116
- duplicates_skipped: stats.duplicatesSkipped,
117
- },
118
- receipts: {
119
- count: receiptCount,
120
- count_approximate: countApproximate,
121
- output_dir: resolvedOutputDir,
122
- last_receipt_time: lastReceiptTime,
123
- oldest_receipt_time: oldestReceiptTime,
124
- },
125
- emitter: {
126
- total_emitted: stats.totalEmitted,
127
- total_errors: stats.totalErrors,
128
- is_running: stats.isRunning,
129
- key_id: stats.keyId,
130
- },
131
- };
132
- },
133
- };
134
- }
135
- // =============================================================================
136
- // Export Bundle Tool
137
- // =============================================================================
138
- /**
139
- * Create the peac_receipts.export_bundle tool.
140
- * Exports receipts as a bundle directory for audit.
141
- *
142
- * Note: Creates a directory structure, not a ZIP archive.
143
- * Use @peac/audit createDisputeBundle for production ZIP bundles.
144
- */
145
- function createExportBundleTool(outputDir, logger) {
146
- return {
147
- name: 'peac_receipts.export_bundle',
148
- description: 'Export receipts as a bundle directory for audit (manifest.json + receipts/)',
149
- parameters: {
150
- type: 'object',
151
- properties: {
152
- workflow_id: {
153
- type: 'string',
154
- description: 'Filter by workflow ID',
155
- },
156
- since: {
157
- type: 'string',
158
- description: 'Include receipts since this RFC 3339 timestamp',
159
- },
160
- until: {
161
- type: 'string',
162
- description: 'Include receipts until this RFC 3339 timestamp',
163
- },
164
- output_path: {
165
- type: 'string',
166
- description: 'Output path for bundle directory (default: peac-bundle-{timestamp})',
167
- },
168
- },
169
- },
170
- async execute(params) {
171
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
172
- const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
173
- // Use path.resolve for portability
174
- const resolvedOutputDir = pathModule.resolve(outputDir);
175
- try {
176
- // List receipts
177
- const files = await fs.promises.readdir(resolvedOutputDir);
178
- const receiptFiles = files.filter((f) => f.endsWith('.peac.json'));
179
- if (receiptFiles.length === 0) {
180
- return {
181
- status: 'ok',
182
- message: 'No receipts to export',
183
- receipt_count: 0,
184
- };
185
- }
186
- // Filter receipts
187
- const receipts = [];
188
- for (const file of receiptFiles) {
189
- const filePath = pathModule.join(resolvedOutputDir, file);
190
- let stat;
191
- try {
192
- stat = await fs.promises.stat(filePath);
193
- }
194
- catch {
195
- // Skip files that can't be stat'd
196
- continue;
197
- }
198
- // Apply time filters
199
- if (params.since && stat.mtime < new Date(params.since)) {
200
- continue;
201
- }
202
- if (params.until && stat.mtime > new Date(params.until)) {
203
- continue;
204
- }
205
- // Read and parse receipt (tolerate invalid JSON)
206
- let content;
207
- try {
208
- content = JSON.parse(await fs.promises.readFile(filePath, 'utf-8'));
209
- }
210
- catch {
211
- logger.warn(`Skipping invalid JSON in ${file}`);
212
- continue;
213
- }
214
- // Apply workflow filter
215
- if (params.workflow_id) {
216
- const workflowExt = content?.auth?.extensions?.['org.peacprotocol/workflow'];
217
- if (workflowExt?.workflow_id !== params.workflow_id) {
218
- continue;
219
- }
220
- }
221
- receipts.push({ file, content, mtime: stat.mtime });
222
- }
223
- if (receipts.length === 0) {
224
- return {
225
- status: 'ok',
226
- message: 'No receipts match the filter criteria',
227
- receipt_count: 0,
228
- };
229
- }
230
- // Create bundle manifest
231
- const manifest = {
232
- version: '1.0',
233
- format: 'peac-bundle-directory',
234
- created_at: new Date().toISOString(),
235
- receipt_count: receipts.length,
236
- filters: {
237
- workflow_id: params.workflow_id || null,
238
- since: params.since || null,
239
- until: params.until || null,
240
- },
241
- receipts: receipts.map((r) => ({
242
- file: r.file,
243
- mtime: r.mtime.toISOString(),
244
- })),
245
- };
246
- // Determine output path (directory, not ZIP)
247
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
248
- const bundleDir = params.output_path
249
- ? pathModule.resolve(params.output_path)
250
- : pathModule.join(resolvedOutputDir, `peac-bundle-${timestamp}`);
251
- await fs.promises.mkdir(bundleDir, { recursive: true });
252
- // Write manifest
253
- await fs.promises.writeFile(pathModule.join(bundleDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
254
- // Copy receipts
255
- const receiptsDir = pathModule.join(bundleDir, 'receipts');
256
- await fs.promises.mkdir(receiptsDir, { recursive: true });
257
- for (const receipt of receipts) {
258
- await fs.promises.writeFile(pathModule.join(receiptsDir, receipt.file), JSON.stringify(receipt.content, null, 2));
259
- }
260
- logger.info(`Exported ${receipts.length} receipts to ${bundleDir}`);
261
- return {
262
- status: 'ok',
263
- message: `Exported ${receipts.length} receipts to directory`,
264
- receipt_count: receipts.length,
265
- bundle_path: bundleDir,
266
- };
267
- }
268
- catch (error) {
269
- logger.error('Export bundle failed:', error);
270
- return {
271
- status: 'error',
272
- message: error instanceof Error ? error.message : 'Unknown error',
273
- receipt_count: 0,
274
- };
275
- }
276
- },
277
- };
278
- }
279
- // =============================================================================
280
- // Verify Tool
281
- // =============================================================================
282
- /**
283
- * Create the peac_receipts.verify tool.
284
- * Verifies a receipt or bundle offline.
285
- */
286
- function createVerifyTool(logger) {
287
- return {
288
- name: 'peac_receipts.verify',
289
- description: 'Verify a receipt or bundle offline',
290
- parameters: {
291
- type: 'object',
292
- properties: {
293
- path: {
294
- type: 'string',
295
- description: 'Path to receipt file or bundle directory',
296
- },
297
- jwks_path: {
298
- type: 'string',
299
- description: 'Path to JWKS file for key verification (optional)',
300
- },
301
- },
302
- required: ['path'],
303
- },
304
- async execute(params) {
305
- const verifyParams = params;
306
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
307
- const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
308
- try {
309
- const stat = await fs.promises.stat(verifyParams.path);
310
- if (stat.isDirectory()) {
311
- // Verify bundle
312
- return await verifyBundle(verifyParams.path, verifyParams.jwks_path, fs, pathModule, logger);
313
- }
314
- else {
315
- // Verify single receipt
316
- return await verifySingleReceipt(verifyParams.path, verifyParams.jwks_path, fs, logger);
317
- }
318
- }
319
- catch (error) {
320
- logger.error('Verification failed:', error);
321
- return {
322
- status: 'error',
323
- valid: false,
324
- message: error instanceof Error ? error.message : 'Unknown error',
325
- errors: [error instanceof Error ? error.message : 'Unknown error'],
326
- };
327
- }
328
- },
329
- };
330
- }
331
- async function verifySingleReceipt(receiptPath, jwksPath, fs, logger) {
332
- // Check file size before reading (DoS protection)
333
- const stat = await fs.promises.stat(receiptPath);
334
- if (stat.size > MAX_FILE_SIZE_BYTES) {
335
- return {
336
- status: 'error',
337
- valid: false,
338
- message: `File too large: ${stat.size} bytes exceeds limit of ${MAX_FILE_SIZE_BYTES} bytes`,
339
- errors: [`File size ${stat.size} bytes exceeds ${MAX_FILE_SIZE_BYTES} byte limit`],
340
- };
341
- }
342
- const content = await fs.promises.readFile(receiptPath, 'utf-8');
343
- const receipt = JSON.parse(content);
344
- const errors = [];
345
- const warnings = [];
346
- // Basic structure validation
347
- if (!receipt.auth) {
348
- errors.push('Missing auth block');
349
- }
350
- if (!receipt.evidence) {
351
- errors.push('Missing evidence block');
352
- }
353
- // Check for interaction evidence
354
- const interaction = receipt.evidence?.extensions?.['org.peacprotocol/interaction@0.1'];
355
- if (interaction) {
356
- // Validate interaction fields
357
- if (!interaction.interaction_id) {
358
- errors.push('Missing interaction_id');
359
- }
360
- if (!interaction.kind) {
361
- errors.push('Missing kind');
362
- }
363
- if (!interaction.executor?.platform) {
364
- errors.push('Missing executor.platform');
365
- }
366
- if (!interaction.started_at) {
367
- errors.push('Missing started_at');
368
- }
369
- // Check timing invariant
370
- if (interaction.completed_at && interaction.started_at) {
371
- if (new Date(interaction.completed_at) < new Date(interaction.started_at)) {
372
- errors.push('completed_at is before started_at');
373
- }
374
- }
375
- // Check output requires result
376
- if (interaction.output && !interaction.result?.status) {
377
- errors.push('output present but result.status missing');
378
- }
379
- }
380
- else {
381
- warnings.push('No interaction evidence found');
382
- }
383
- // Signature verification (if JWKS provided)
384
- if (jwksPath && receipt._jws) {
385
- try {
386
- const { verify, base64urlDecode, base64urlDecodeString } = await Promise.resolve().then(() => __importStar(require('@peac/crypto')));
387
- const jwks = JSON.parse(await fs.promises.readFile(jwksPath, 'utf-8'));
388
- // Parse JWS header to get kid and alg
389
- const jwsParts = receipt._jws.split('.');
390
- if (jwsParts.length !== 3) {
391
- errors.push('Invalid JWS format (expected 3 parts)');
392
- }
393
- else {
394
- const headerJson = base64urlDecodeString(jwsParts[0]);
395
- const header = JSON.parse(headerJson);
396
- // Reject 'none' algorithm (security)
397
- if (header.alg === 'none') {
398
- errors.push('Algorithm "none" is not allowed');
399
- }
400
- // Reject algorithms not in allowlist
401
- if (header.alg &&
402
- !ALLOWED_ALGORITHMS.includes(header.alg)) {
403
- errors.push(`Algorithm "${header.alg}" is not in allowed list: ${ALLOWED_ALGORITHMS.join(', ')}`);
404
- }
405
- // Reject unknown critical headers (JOSE compliance)
406
- // Per RFC 7515: crit lists headers that MUST be understood, we don't support any
407
- if (header.crit && header.crit.length > 0) {
408
- // Check if crit references headers that are missing (per JOSE spec)
409
- const headerObj = header;
410
- const missingCrit = header.crit.filter((h) => !(h in headerObj));
411
- if (missingCrit.length > 0) {
412
- errors.push(`Critical headers declared but missing: ${missingCrit.join(', ')}`);
413
- }
414
- // Reject all crit headers since we don't understand any extensions
415
- errors.push(`Unsupported critical headers: ${header.crit.join(', ')}`);
416
- }
417
- const targetKid = header.kid;
418
- // Strict key selection by kid
419
- let keyJwk;
420
- if (!Array.isArray(jwks.keys) || jwks.keys.length === 0) {
421
- errors.push('JWKS has no keys');
422
- }
423
- else if (targetKid) {
424
- // JWS has kid: require exact match
425
- keyJwk = jwks.keys.find((k) => k.kid === targetKid);
426
- if (!keyJwk) {
427
- errors.push(`Key with kid "${targetKid}" not found in JWKS`);
428
- }
429
- }
430
- else if (jwks.keys.length === 1) {
431
- // JWS has no kid but JWKS has exactly 1 key: accept it
432
- keyJwk = jwks.keys[0];
433
- warnings.push('JWS has no kid - using only key from JWKS');
434
- }
435
- else {
436
- // JWS has no kid and JWKS has multiple keys: fail
437
- errors.push(`JWS missing kid but JWKS has ${jwks.keys.length} keys - cannot select`);
438
- }
439
- // Verify algorithm matches key type
440
- if (keyJwk && header.alg && errors.length === 0) {
441
- const expectedKeyType = ALG_KEY_TYPE_MAP[header.alg];
442
- if (expectedKeyType) {
443
- if (keyJwk.kty !== expectedKeyType.kty) {
444
- errors.push(`Algorithm "${header.alg}" requires key type "${expectedKeyType.kty}" but key has "${keyJwk.kty}"`);
445
- }
446
- else if (expectedKeyType.crv &&
447
- keyJwk.crv &&
448
- !expectedKeyType.crv.includes(keyJwk.crv)) {
449
- errors.push(`Algorithm "${header.alg}" requires curve ${expectedKeyType.crv.join(' or ')} but key has "${keyJwk.crv}"`);
450
- }
451
- }
452
- // Validate key use if present (must be "sig" for signature verification)
453
- if (keyJwk.use && keyJwk.use !== 'sig') {
454
- errors.push(`Key use "${keyJwk.use}" is not valid for signature verification (expected "sig")`);
455
- }
456
- // Validate key_ops if present (must include "verify")
457
- if (keyJwk.key_ops && !keyJwk.key_ops.includes('verify')) {
458
- errors.push(`Key operations ${JSON.stringify(keyJwk.key_ops)} do not include "verify"`);
459
- }
460
- }
461
- if (keyJwk && errors.length === 0) {
462
- // Get the public key bytes (x for OKP/EC, n for RSA)
463
- const publicKeyParam = keyJwk.x || keyJwk.n;
464
- if (!publicKeyParam) {
465
- errors.push('Selected key missing public key (x or n parameter)');
466
- }
467
- else {
468
- // Decode base64url public key bytes
469
- const publicKeyBytes = base64urlDecode(publicKeyParam);
470
- await verify(receipt._jws, publicKeyBytes);
471
- logger.info(`Signature verified with key ${keyJwk.kid || 'anonymous'}`);
472
- }
473
- }
474
- }
475
- }
476
- catch (error) {
477
- errors.push(`Signature verification failed: ${error instanceof Error ? error.message : 'Unknown'}`);
478
- }
479
- }
480
- else if (!jwksPath) {
481
- warnings.push('No JWKS provided - signature not verified');
482
- }
483
- return {
484
- status: errors.length === 0 ? 'ok' : 'error',
485
- valid: errors.length === 0,
486
- message: errors.length === 0 ? 'Receipt is valid' : 'Receipt has validation errors',
487
- errors: errors.length > 0 ? errors : undefined,
488
- warnings: warnings.length > 0 ? warnings : undefined,
489
- receipt_id: receipt.auth?.rid,
490
- interaction_id: interaction?.interaction_id,
491
- };
492
- }
493
- async function verifyBundle(bundlePath, jwksPath, fs, pathModule, logger) {
494
- // Read manifest
495
- const manifestPath = pathModule.join(bundlePath, 'manifest.json');
496
- const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf-8'));
497
- const receiptsDir = pathModule.join(bundlePath, 'receipts');
498
- const files = await fs.promises.readdir(receiptsDir);
499
- let validCount = 0;
500
- let invalidCount = 0;
501
- const allErrors = [];
502
- const allWarnings = [];
503
- for (const file of files) {
504
- if (!file.endsWith('.peac.json'))
505
- continue;
506
- const result = await verifySingleReceipt(pathModule.join(receiptsDir, file), jwksPath, fs, logger);
507
- if (result.valid) {
508
- validCount++;
509
- }
510
- else {
511
- invalidCount++;
512
- if (result.errors) {
513
- allErrors.push(...result.errors.map((e) => `${file}: ${e}`));
514
- }
515
- }
516
- if (result.warnings) {
517
- allWarnings.push(...result.warnings.map((w) => `${file}: ${w}`));
518
- }
519
- }
520
- return {
521
- status: invalidCount === 0 ? 'ok' : 'error',
522
- valid: invalidCount === 0,
523
- message: `Bundle verification: ${validCount} valid, ${invalidCount} invalid`,
524
- errors: allErrors.length > 0 ? allErrors : undefined,
525
- warnings: allWarnings.length > 0 ? allWarnings : undefined,
526
- bundle_stats: {
527
- total: validCount + invalidCount,
528
- valid: validCount,
529
- invalid: invalidCount,
530
- manifest_receipt_count: manifest.receipt_count,
531
- },
532
- };
533
- }
534
- // =============================================================================
535
- // Query Tool
536
- // =============================================================================
537
- /**
538
- * Create the peac_receipts.query tool.
539
- * Query receipts by workflow_id, tool name, or time range.
540
- */
541
- function createQueryTool(outputDir, logger) {
542
- return {
543
- name: 'peac_receipts.query',
544
- description: 'Query receipts by workflow_id, tool name, or time range',
545
- parameters: {
546
- type: 'object',
547
- properties: {
548
- workflow_id: {
549
- type: 'string',
550
- description: 'Filter by workflow ID',
551
- },
552
- tool_name: {
553
- type: 'string',
554
- description: 'Filter by tool name',
555
- },
556
- since: {
557
- type: 'string',
558
- description: 'Include receipts since this RFC 3339 timestamp',
559
- },
560
- until: {
561
- type: 'string',
562
- description: 'Include receipts until this RFC 3339 timestamp',
563
- },
564
- status: {
565
- type: 'string',
566
- enum: ['ok', 'error', 'timeout', 'canceled'],
567
- description: 'Filter by result status',
568
- },
569
- limit: {
570
- type: 'integer',
571
- description: 'Maximum number of results (default: 100)',
572
- },
573
- offset: {
574
- type: 'integer',
575
- description: 'Skip this many results (for pagination)',
576
- },
577
- },
578
- },
579
- async execute(params) {
580
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
581
- const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
582
- // Use path.resolve for portability
583
- const resolvedOutputDir = pathModule.resolve(outputDir);
584
- try {
585
- const files = await fs.promises.readdir(resolvedOutputDir);
586
- const receiptFiles = files.filter((f) => f.endsWith('.peac.json'));
587
- const limit = Math.min(params.limit || 100, MAX_QUERY_FILE_PARSE);
588
- const offset = params.offset || 0;
589
- // If we have time filters, we can sort by mtime first (cheap) to reduce parsing
590
- // Get file stats first for time-based pre-filtering
591
- const fileInfos = [];
592
- for (const file of receiptFiles.slice(0, MAX_QUERY_FILE_PARSE)) {
593
- try {
594
- const filePath = pathModule.join(resolvedOutputDir, file);
595
- const stat = await fs.promises.stat(filePath);
596
- // Apply time filters early (before parsing JSON)
597
- if (params.since && stat.mtime < new Date(params.since)) {
598
- continue;
599
- }
600
- if (params.until && stat.mtime > new Date(params.until)) {
601
- continue;
602
- }
603
- fileInfos.push({ file, mtime: stat.mtime });
604
- }
605
- catch {
606
- // Skip files that can't be stat'd
607
- continue;
608
- }
609
- }
610
- // Sort by mtime descending (newest first) before parsing
611
- fileInfos.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
612
- // Now parse files until we have enough matches
613
- const matches = [];
614
- let skippedForFilters = 0;
615
- let filesSkippedForSize = 0;
616
- let filesSkippedForInvalidJson = 0;
617
- for (const { file } of fileInfos) {
618
- const filePath = pathModule.join(resolvedOutputDir, file);
619
- // Check file size before reading (DoS protection)
620
- try {
621
- const fileStat = await fs.promises.stat(filePath);
622
- if (fileStat.size > MAX_FILE_SIZE_BYTES) {
623
- filesSkippedForSize++;
624
- continue;
625
- }
626
- }
627
- catch {
628
- continue;
629
- }
630
- // Read and parse receipt (tolerate invalid JSON)
631
- let content;
632
- try {
633
- content = JSON.parse(await fs.promises.readFile(filePath, 'utf-8'));
634
- }
635
- catch {
636
- // Skip invalid JSON files
637
- filesSkippedForInvalidJson++;
638
- continue;
639
- }
640
- const interaction = content?.evidence?.extensions?.['org.peacprotocol/interaction@0.1'];
641
- const workflow = content?.auth?.extensions?.['org.peacprotocol/workflow'];
642
- // Apply workflow filter
643
- if (params.workflow_id && workflow?.workflow_id !== params.workflow_id) {
644
- skippedForFilters++;
645
- continue;
646
- }
647
- // Apply tool filter
648
- if (params.tool_name && interaction?.tool?.name !== params.tool_name) {
649
- skippedForFilters++;
650
- continue;
651
- }
652
- // Apply status filter
653
- if (params.status && interaction?.result?.status !== params.status) {
654
- skippedForFilters++;
655
- continue;
656
- }
657
- matches.push({
658
- file,
659
- receipt_id: content.auth?.rid,
660
- interaction_id: interaction?.interaction_id,
661
- workflow_id: workflow?.workflow_id,
662
- tool_name: interaction?.tool?.name,
663
- status: interaction?.result?.status,
664
- started_at: interaction?.started_at,
665
- completed_at: interaction?.completed_at,
666
- });
667
- // Early exit if we have enough matches (limit + offset)
668
- // This avoids parsing all files when we only need a few
669
- if (matches.length >= offset + limit + 100) {
670
- // Buffer of 100 for sorting stability
671
- break;
672
- }
673
- }
674
- // Final sort by started_at (more accurate than mtime), then by filename for determinism
675
- matches.sort((a, b) => {
676
- const aTime = a.started_at ? new Date(a.started_at).getTime() : 0;
677
- const bTime = b.started_at ? new Date(b.started_at).getTime() : 0;
678
- if (bTime !== aTime)
679
- return bTime - aTime;
680
- // Secondary sort by filename for deterministic ordering
681
- return a.file.localeCompare(b.file);
682
- });
683
- // Apply pagination
684
- const paginated = matches.slice(offset, offset + limit);
685
- // Build warnings for truncation
686
- const warnings = [];
687
- if (receiptFiles.length > MAX_QUERY_FILE_PARSE) {
688
- warnings.push(`Results capped at ${MAX_QUERY_FILE_PARSE} files`);
689
- }
690
- if (filesSkippedForSize > 0) {
691
- warnings.push(`${filesSkippedForSize} files skipped (exceeded ${MAX_FILE_SIZE_BYTES} byte limit)`);
692
- }
693
- if (filesSkippedForInvalidJson > 0) {
694
- warnings.push(`${filesSkippedForInvalidJson} files skipped (invalid JSON)`);
695
- }
696
- const hasTruncation = receiptFiles.length > MAX_QUERY_FILE_PARSE ||
697
- filesSkippedForSize > 0 ||
698
- filesSkippedForInvalidJson > 0;
699
- return {
700
- status: 'ok',
701
- total: matches.length,
702
- offset,
703
- limit,
704
- results: paginated,
705
- truncated: hasTruncation,
706
- skipped: hasTruncation
707
- ? {
708
- too_large: filesSkippedForSize,
709
- invalid_json: filesSkippedForInvalidJson,
710
- capped: receiptFiles.length > MAX_QUERY_FILE_PARSE
711
- ? receiptFiles.length - MAX_QUERY_FILE_PARSE
712
- : 0,
713
- }
714
- : undefined,
715
- ...(warnings.length > 0 && { warning: warnings.join('; ') }),
716
- };
717
- }
718
- catch (error) {
719
- logger.error('Query failed:', error);
720
- return {
721
- status: 'error',
722
- total: 0,
723
- offset: 0,
724
- limit: params.limit || 100,
725
- results: [],
726
- error: error instanceof Error ? error.message : 'Unknown error',
727
- };
728
- }
729
- },
730
- };
731
- }
732
- //# sourceMappingURL=tools.js.map