@askjo/camofox-browser 1.8.1 → 1.8.7
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/README.md +77 -15
- package/camofox.config.json +0 -8
- package/lib/config.js +4 -7
- package/lib/reporter.js +51 -222
- package/lib/resources.js +80 -0
- package/openclaw.plugin.json +60 -1
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -344,6 +344,43 @@ The crash reporter gives us structured data on *which sites fail*, *how they fai
|
|
|
344
344
|
|
|
345
345
|
Each report includes the failure type, stack trace, tab health counters (HTTP status histogram, console errors, request failures, redirect depth), and the target URL — all anonymized.
|
|
346
346
|
|
|
347
|
+
#### How it works
|
|
348
|
+
|
|
349
|
+
Reports are sent to a lightweight Cloudflare Worker relay at [`https://camofox-crash-relay.askjo.workers.dev`](https://camofox-crash-relay.askjo.workers.dev/health). The relay holds the GitHub App credentials as environment secrets — **no secrets are shipped in this package**.
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
lib/reporter.js (client, no secrets)
|
|
353
|
+
│ anonymize → POST https://camofox-crash-relay.askjo.workers.dev/report
|
|
354
|
+
▼
|
|
355
|
+
Cloudflare Worker (holds GitHub App key)
|
|
356
|
+
│ validate → rate-limit → dedup → create GitHub Issue
|
|
357
|
+
▼
|
|
358
|
+
GitHub Issue created
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The relay source code is in this repo at [`workers/crash-reporter/index.ts`](workers/crash-reporter/index.ts).
|
|
362
|
+
|
|
363
|
+
#### Verification
|
|
364
|
+
|
|
365
|
+
You don't have to trust us — verify what the live relay is running:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# 1. Ask the relay what code it's running
|
|
369
|
+
curl https://camofox-crash-relay.askjo.workers.dev/source
|
|
370
|
+
# → { "commit": "abc1234", "sha256": "e3b0c44...", "source": "https://github.com/..." }
|
|
371
|
+
|
|
372
|
+
# 2. Compare the sha256 against the source in this repo
|
|
373
|
+
sha256sum workers/crash-reporter/index.ts
|
|
374
|
+
|
|
375
|
+
# 3. Check the commit matches what CI deployed
|
|
376
|
+
# https://github.com/jo-inc/camofox-browser/actions/workflows/crash-relay-deploy.yml
|
|
377
|
+
git log --oneline workers/crash-reporter/index.ts | head -1
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
If the hashes don't match, the relay is running different code than what's in the repo. The deploy workflow ([`.github/workflows/crash-relay-deploy.yml`](.github/workflows/crash-relay-deploy.yml)) injects the commit and source hash at deploy time — every deploy is auditable in [GitHub Actions](https://github.com/jo-inc/camofox-browser/actions/workflows/crash-relay-deploy.yml).
|
|
381
|
+
|
|
382
|
+
Or skip verification entirely: `CAMOFOX_CRASH_REPORT_ENABLED=false` disables all reporting, or point to [your own relay](#self-hosted-relay) with `CAMOFOX_CRASH_REPORT_URL`.
|
|
383
|
+
|
|
347
384
|
#### Privacy
|
|
348
385
|
|
|
349
386
|
All reported data goes through paranoid anonymization ([`lib/reporter.js` L28–290](lib/reporter.js#L28-L290)) before leaving the process:
|
|
@@ -357,34 +394,58 @@ All reported data goes through paranoid anonymization ([`lib/reporter.js` L28–
|
|
|
357
394
|
|
|
358
395
|
Duplicate issues are detected by stack signature and get a `+1` comment instead of a new issue.
|
|
359
396
|
|
|
360
|
-
Uses a dedicated GitHub App ([Camofox Crash/Stuck Reporter](https://github.com/apps/camofox-crash-stuck-reporter)) with issues-only permissions — no PAT or configuration required.
|
|
361
|
-
|
|
362
397
|
```bash
|
|
363
398
|
# Disable crash reporting
|
|
364
399
|
export CAMOFOX_CRASH_REPORT_ENABLED=false
|
|
365
400
|
|
|
366
|
-
#
|
|
367
|
-
export
|
|
401
|
+
# Point to your own relay (see below)
|
|
402
|
+
export CAMOFOX_CRASH_REPORT_URL=https://your-relay.example.com/report
|
|
368
403
|
|
|
369
404
|
# Adjust rate limit (default: 10 per hour)
|
|
370
405
|
export CAMOFOX_CRASH_REPORT_RATE_LIMIT=5
|
|
371
406
|
```
|
|
372
407
|
|
|
373
|
-
####
|
|
408
|
+
#### Self-hosted relay
|
|
409
|
+
|
|
410
|
+
To file crash reports in your own GitHub repo instead of `jo-inc/camofox-browser`:
|
|
374
411
|
|
|
375
|
-
|
|
412
|
+
1. **Create a GitHub App** — [Settings → Developer settings → GitHub Apps → New](https://github.com/settings/apps/new)
|
|
413
|
+
- Permissions: **Repository → Issues → Read & Write**
|
|
414
|
+
- Uncheck **Webhook → Active** (not needed)
|
|
415
|
+
- Click **Generate a key** — downloads a `.pem` file
|
|
416
|
+
- Install the app on your target repo (Install App → select repo)
|
|
417
|
+
- Note your **App ID** (number on the app's General page) and **Installation ID** (from the URL after installing: `github.com/settings/installations/{id}`)
|
|
418
|
+
|
|
419
|
+
2. **Deploy the relay** — clone this repo and deploy the worker:
|
|
420
|
+
```bash
|
|
421
|
+
cd workers/crash-reporter
|
|
422
|
+
# Edit wrangler.toml: set account_id to your Cloudflare account ID
|
|
423
|
+
npx wrangler deploy
|
|
424
|
+
```
|
|
425
|
+
The worker is a single TypeScript file with zero npm dependencies. It also runs on Deno, Bun, or any runtime with the Web Crypto API.
|
|
426
|
+
|
|
427
|
+
3. **Set worker secrets:**
|
|
428
|
+
```bash
|
|
429
|
+
cd workers/crash-reporter
|
|
430
|
+
echo "YOUR_APP_ID" | npx wrangler secret put GH_APP_ID
|
|
431
|
+
echo "YOUR_INSTALL_ID" | npx wrangler secret put GH_INSTALL_ID
|
|
432
|
+
# Key must be PKCS#8 DER base64 (not raw PEM)
|
|
433
|
+
openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in your-app.pem | \
|
|
434
|
+
base64 | tr -d '\n' | npx wrangler secret put GH_PRIVATE_KEY
|
|
435
|
+
# File issues in your repo
|
|
436
|
+
echo "your-org/your-repo" | npx wrangler secret put GH_REPO
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
4. **Point camofox-browser to your relay:**
|
|
440
|
+
```bash
|
|
441
|
+
export CAMOFOX_CRASH_REPORT_URL=https://your-worker.your-subdomain.workers.dev/report
|
|
442
|
+
```
|
|
376
443
|
|
|
377
|
-
|
|
378
|
-
2. Set permissions: **Repository → Issues → Read & Write**. Uncheck **Webhook → Active**.
|
|
379
|
-
3. Click **Generate a private key** — downloads a `.pem` file
|
|
380
|
-
4. Install the app on your target repo (Install App → select repo)
|
|
381
|
-
5. Note your **App ID** (number on the app's settings page) and **Installation ID** (from the URL after installing: `github.com/settings/installations/{id}`)
|
|
382
|
-
6. Base64-encode the private key and split it into two halves:
|
|
444
|
+
5. **Verify:**
|
|
383
445
|
```bash
|
|
384
|
-
|
|
446
|
+
curl https://your-worker.your-subdomain.workers.dev/health
|
|
447
|
+
# → {"status":"ok"}
|
|
385
448
|
```
|
|
386
|
-
7. Replace `_GH_APP_ID`, `_GH_INSTALL_ID`, `_K_A`, and `_K_B` in `lib/reporter.js` with your values
|
|
387
|
-
8. Set `CAMOFOX_CRASH_REPORT_REPO=your-org/your-repo`
|
|
388
449
|
|
|
389
450
|
### Structured Logging
|
|
390
451
|
|
|
@@ -528,6 +589,7 @@ Reddit macros return JSON directly (no HTML parsing needed):
|
|
|
528
589
|
| `PROXY_STATE` | Target state/region for proxy geo-targeting | - |
|
|
529
590
|
| `TAB_INACTIVITY_MS` | Close tabs idle longer than this | `300000` (5min) |
|
|
530
591
|
| `CAMOFOX_CRASH_REPORT_ENABLED` | Enable anonymized crash/hang reporter (`false` to disable) | `true` |
|
|
592
|
+
| `CAMOFOX_CRASH_REPORT_URL` | Crash report relay endpoint ([self-hosted relay](#self-hosted-relay)) | `https://camofox-crash-relay.askjo.workers.dev/report` |
|
|
531
593
|
| `CAMOFOX_CRASH_REPORT_REPO` | GitHub repo for issue reports | `jo-inc/camofox-browser` |
|
|
532
594
|
| `CAMOFOX_CRASH_REPORT_RATE_LIMIT` | Max reports per hour | `10` |
|
|
533
595
|
| `ENABLE_VNC` | Enable VNC plugin for interactive browser access (`1`) | - |
|
package/camofox.config.json
CHANGED
|
@@ -6,13 +6,5 @@
|
|
|
6
6
|
"youtube": { "enabled": true },
|
|
7
7
|
"persistence": { "enabled": true },
|
|
8
8
|
"vnc": { "resolution": "1920x1080" }
|
|
9
|
-
},
|
|
10
|
-
"crashReporter": {
|
|
11
|
-
"appId": "3503870",
|
|
12
|
-
"installationId": "127089401",
|
|
13
|
-
"repo": "jo-inc/camofox-browser",
|
|
14
|
-
"userAgent": "camofox-crash-reporter",
|
|
15
|
-
"keyA": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBMDRQRDBWWkNRb1pYdkpCNmYwUHdGRk1scEVzbjF2blFYaHJNSkxlSDdBWElmdGhCCmtpbElUb0xNeWFCVU9aWEdUbXNNYnE2MEd5R1p4RGFQVSt4bGRQdmc2Y2QrUElUNmxSbjB2TCsrd2FNT0llMDQKUEdrallSWW1YZzhqZHFCNEhQcmlKbkQwa2srU0EwVUxBYm5jUHVhTitKNEVFYjBDZUZQRUpzNEJqd0dvcENZMwpaM1VBS2ZBVjh5Slpzc3JVM0lvVlBzckc4dHVnRFhya3JTc2Y0K1Jmdm5lejRibGk1YXJKQ1h3cXJPVE0xRERNCk9mUFhOMENQMWNIWW1XL1p0TVNiRlpsbFRobVdMb05NUXNNdlFEZ3lmWjRXbG9rL1doaWVhTWttWFB6YzFuUkkKMVltNCs0YzZVS2kzOURFSS9MODA5RUZ6OHU0K2lnN1dzaEdsZHdJREFRQUJBb0lCQVFDcC8xQWw4cjhrZXBjUApqY3QyZCtNQVl1ZHhDWnFHbEplYzJzclNnOU94cGVCRDJvbXc4SThWMHRqSEFKNVEvZ2k1UkI1azR2TU1qMC9uCnZMWXJqR2Jxdy9vN3lzT3gzbXNMNVNXbmdqRE5yc0NRRWZuTnkrN01mQ0h3SFJpeW9qeUhoamkzRHJmeTFCTVYKbjZzK0F1UjZoWkQ3amZ6VlNPVXdVcHJuV1ZFMVhuaVBaZmZSd29BQmxEQi92Zm9wWWZpKzhKaXpYQTNVaytlMwp1TEdIZHc2bzJlUkp1bW90RFBubWtsVkVSKy8vcUZpa1BqTGJBSHprNGRTM3hkMWF1ZEFUNU5hczdiUkhGeXFoCkt2eGk2blM4cTAzanBIS0JMQ0hsN212Y1JQUWhYek1XcHVQblVsNGpvRFVDQ0RITG5CcWNmZ3FjTnlzOGhuSkIKaFpueE5pbGhBb0dCQVBKNm04QXo5KytnbzZjMGoxNnFsSXk3V3NyU0liSW80bXZSUEtRc0lnQUxOYTB3TXluNgozWms3NHlTdWJVaWY5c0pwM0pCNFEzdlBqVm",
|
|
16
|
-
"keyB": "9pSHBDaVZ3ZEttNFFLeC9mMVJITUtCTzVDOFlHK0VsaTlUSWllCkVjWFJMN2VyUmpValIvNjJoZVAvVG96ZXdPckxYbHFlVEEySEJ2S2RJbTFrL2ozNnlWNFVUdm1SQW9HQkFOOVAKSkRpWCt4Y083RnMwOUxxWlYxRFVhK1Rja1RQR1BMMVdlelBEeHc0M0lLVERyL2F1RWs0Mm1pK0NYY0huQk5CagpDT0Y3bEVhRTVENzdVVlZFL3JxR0E3dGdLVER3M2N5cDhQb2x3T1IwK01LUmh5MFpjeG1wcFhlb1dlZm9LVEJaCmtyQjlRbXZrZTMvdWhHUWNqemt3M2dxcE1QRmdFdFlNUUdBY3RtcUhBb0dCQUtobGNnbFhqaGJERHlTdUlldHkKdDl2TXVjOGxnL1ZBNDQ1Ukw3WXNXQ2lEb0hGNGllL2JvMDRxQXlPVVo1MEtTc3JWempJZTgyN213NW9YRy9jQwpaMEpQRkJYdGp0YXJaVEFuZ3lrZElMQWtHb1c2WVk1M2lJeERMTXAzamppVkdnalJKY2NqcForN2kyc0VkYkNsClF0Z2FNRDhKMWNEM1pJSVN5d29sUEh1aEFuOHYrZERPVjlpYUc1cXIvYlNXWWx0Z0FrTXI2RGRKNkUwa1lIQVgKcnZnVlRNSzJvMVFxcXp0RHlYZFd2YXRtL1RzTGlqdGVOaTZrOStnUm4relpaUGxWR1hXenkvVU5qcklZUm1wLwpVNTBkZUFQNXlVcEJaalpVVFI0L2x1dTU1eWJ5UEV4SG5xR21qRy84REVKbFA3MkZpL29vVURFenFuQmhqRUJJClplTExBb0dBWUQxSkp2S0d5U2NtZWVCU0J3TTRlSUF2b3RCMWR2L0NvaThDNVR2Z1F6QWRySE1BTGhIbDNtbVEKajJ3ZnpPOFlEbW45ZmdreDVvVTMyWjJNQTEyVS9mQklJLzVFYWFLK045MVVJanlFUHd1ZU42emR3c0MwT0NXWQovTVYyaFI3dlk1bmN5SUxubG1yRHNRU3htTzIrZmFhUlAxQUVtUit5aUpOUUw2ZlE1RGc9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=="
|
|
17
9
|
}
|
|
18
10
|
}
|
package/lib/config.js
CHANGED
|
@@ -13,12 +13,9 @@ import os from 'os';
|
|
|
13
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const ROOT_DIR = join(__dirname, '..');
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/** @deprecated crashReporter config moved to Cloudflare Worker relay. */
|
|
17
17
|
function readCrashReporterConfig() {
|
|
18
|
-
|
|
19
|
-
const raw = readFileSync(join(ROOT_DIR, 'camofox.config.json'), 'utf-8');
|
|
20
|
-
return JSON.parse(raw).crashReporter || {};
|
|
21
|
-
} catch { return {}; }
|
|
18
|
+
return {};
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
/**
|
|
@@ -118,9 +115,9 @@ function loadConfig() {
|
|
|
118
115
|
PROXY_ZIP: process.env.PROXY_ZIP,
|
|
119
116
|
PROXY_SESSION_DURATION_MINUTES: process.env.PROXY_SESSION_DURATION_MINUTES,
|
|
120
117
|
},
|
|
121
|
-
// Crash reporter (opt-in,
|
|
122
|
-
// Crash reporter (opt-in, credentials from camofox.config.json)
|
|
118
|
+
// Crash reporter (opt-in, reports sent to Cloudflare Worker relay)
|
|
123
119
|
crashReportEnabled: process.env.CAMOFOX_CRASH_REPORT_ENABLED !== 'false',
|
|
120
|
+
crashReportUrl: process.env.CAMOFOX_CRASH_REPORT_URL || '',
|
|
124
121
|
crashReportRepo: process.env.CAMOFOX_CRASH_REPORT_REPO,
|
|
125
122
|
crashReportRateLimit: parseInt(process.env.CAMOFOX_CRASH_REPORT_RATE_LIMIT, 10) || 10,
|
|
126
123
|
crashReporterConfig: readCrashReporterConfig(),
|
package/lib/reporter.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Config passed via createReporter(config) from lib/config.js.
|
|
4
4
|
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
-
import fs from 'fs';
|
|
7
6
|
import { monitorEventLoopDelay } from 'perf_hooks';
|
|
7
|
+
import { collectResourceSnapshot, classifyProxyError } from './resources.js';
|
|
8
8
|
|
|
9
9
|
// ============================================================================
|
|
10
10
|
// Anonymization
|
|
@@ -431,76 +431,10 @@ export function createTabHealthTracker(page) {
|
|
|
431
431
|
return { health, snapshot, getReadyState };
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Collect process-level resource metrics. Safe to call at any time.
|
|
440
|
-
* Returns anonymized metrics — no PIDs, paths, or user data.
|
|
441
|
-
*/
|
|
442
|
-
export function collectResourceSnapshot(opts = {}) {
|
|
443
|
-
const mem = process.memoryUsage();
|
|
444
|
-
const snap = {
|
|
445
|
-
nodeRssMb: Math.round(mem.rss / 1048576),
|
|
446
|
-
nodeHeapUsedMb: Math.round(mem.heapUsed / 1048576),
|
|
447
|
-
nodeHeapTotalMb: Math.round(mem.heapTotal / 1048576),
|
|
448
|
-
nodeExternalMb: Math.round(mem.external / 1048576),
|
|
449
|
-
eventLoopLagMs: null,
|
|
450
|
-
activeHandles: null,
|
|
451
|
-
activeRequests: null,
|
|
452
|
-
openFds: null,
|
|
453
|
-
browserRssMb: null,
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
// Active libuv handles/requests (private API, guarded)
|
|
457
|
-
try { snap.activeHandles = process._getActiveHandles().length; } catch { /* unavailable */ }
|
|
458
|
-
try { snap.activeRequests = process._getActiveRequests().length; } catch { /* unavailable */ }
|
|
459
|
-
|
|
460
|
-
// Open file descriptors (Linux only)
|
|
461
|
-
try {
|
|
462
|
-
if (process.platform === 'linux') {
|
|
463
|
-
snap.openFds = fs.readdirSync('/proc/self/fd').length;
|
|
464
|
-
}
|
|
465
|
-
} catch { /* not available or permission denied */ }
|
|
466
|
-
|
|
467
|
-
// Browser process RSS (the one people miss — browser OOMs, not Node)
|
|
468
|
-
if (opts.browserPid && Number.isInteger(opts.browserPid) && opts.browserPid > 0) {
|
|
469
|
-
try {
|
|
470
|
-
if (process.platform === 'linux') {
|
|
471
|
-
const status = fs.readFileSync(`/proc/${opts.browserPid}/status`, 'utf8');
|
|
472
|
-
const match = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
473
|
-
if (match) snap.browserRssMb = Math.round(parseInt(match[1], 10) / 1024);
|
|
474
|
-
} else if (process.platform === 'darwin') {
|
|
475
|
-
const out = execSync(`ps -o rss= -p ${opts.browserPid}`, { timeout: 1000 }).toString().trim();
|
|
476
|
-
if (out) snap.browserRssMb = Math.round(parseInt(out, 10) / 1024);
|
|
477
|
-
}
|
|
478
|
-
} catch { /* process gone or permission denied */ }
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Session/tab counts from caller
|
|
482
|
-
if (opts.sessionCount != null) snap.browserContexts = opts.sessionCount;
|
|
483
|
-
if (opts.tabCount != null) snap.activeTabs = opts.tabCount;
|
|
484
|
-
|
|
485
|
-
return snap;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Classify proxy errors from Playwright navigation error messages.
|
|
490
|
-
* Returns { proxyError: string|null, proxyTlsError: bool } — no IPs or credentials.
|
|
491
|
-
*/
|
|
492
|
-
export function classifyProxyError(errorMessage) {
|
|
493
|
-
if (!errorMessage || typeof errorMessage !== 'string') return { proxyError: null, proxyTlsError: false };
|
|
494
|
-
const msg = errorMessage.toUpperCase();
|
|
495
|
-
// Explicit proxy errors from Chromium/Firefox net stack
|
|
496
|
-
if (msg.includes('ERR_PROXY_CONNECTION_FAILED')) return { proxyError: 'ERR_PROXY_CONNECTION_FAILED', proxyTlsError: false };
|
|
497
|
-
if (msg.includes('ERR_TUNNEL_CONNECTION_FAILED')) return { proxyError: 'ERR_TUNNEL_CONNECTION_FAILED', proxyTlsError: false };
|
|
498
|
-
if (msg.includes('ERR_PROXY_AUTH_REQUESTED') || msg.includes('407')) return { proxyError: 'ERR_PROXY_AUTH_REQUESTED', proxyTlsError: false };
|
|
499
|
-
if (msg.includes('ERR_PROXY_CERTIFICATE_INVALID') || (msg.includes('PROXY') && msg.includes('SSL'))) return { proxyError: 'ERR_PROXY_TLS', proxyTlsError: true };
|
|
500
|
-
if (msg.includes('ECONNREFUSED') && msg.includes('PROXY')) return { proxyError: 'ECONNREFUSED', proxyTlsError: false };
|
|
501
|
-
if (msg.includes('ETIMEDOUT') && msg.includes('PROXY')) return { proxyError: 'ETIMEDOUT', proxyTlsError: false };
|
|
502
|
-
return { proxyError: null, proxyTlsError: false };
|
|
503
|
-
}
|
|
434
|
+
// collectResourceSnapshot and classifyProxyError live in lib/resources.js
|
|
435
|
+
// (isolated from network code to avoid scanner false positives).
|
|
436
|
+
// Re-exported here for backward compatibility.
|
|
437
|
+
export { collectResourceSnapshot, classifyProxyError };
|
|
504
438
|
|
|
505
439
|
// ============================================================================
|
|
506
440
|
// Rate limiter (sliding window, 1 hour)
|
|
@@ -522,123 +456,46 @@ class RateLimiter {
|
|
|
522
456
|
}
|
|
523
457
|
|
|
524
458
|
// ============================================================================
|
|
525
|
-
//
|
|
459
|
+
// Crash relay client
|
|
526
460
|
// ============================================================================
|
|
527
461
|
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/** Sign a JWT for GitHub App authentication (10-min expiry). */
|
|
542
|
-
function _signAppJwt() {
|
|
543
|
-
const key = _getAppKey();
|
|
544
|
-
if (!key || !_GH_APP_ID) return null;
|
|
545
|
-
const header = { alg: 'RS256', typ: 'JWT' };
|
|
546
|
-
const now = Math.floor(Date.now() / 1000);
|
|
547
|
-
const payload = { iss: _GH_APP_ID, iat: now - 60, exp: now + 600 };
|
|
548
|
-
|
|
549
|
-
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
|
|
550
|
-
const unsigned = b64(header) + '.' + b64(payload);
|
|
551
|
-
const signature = crypto.sign('RSA-SHA256', Buffer.from(unsigned), key);
|
|
552
|
-
return unsigned + '.' + signature.toString('base64url');
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// Cached installation token (1-hour TTL from GitHub, we refresh at 50 min)
|
|
556
|
-
let _cachedToken = null;
|
|
557
|
-
let _tokenExpiresAt = 0;
|
|
558
|
-
|
|
559
|
-
async function _getInstallationToken() {
|
|
560
|
-
if (!_GH_INSTALL_ID) return null;
|
|
561
|
-
if (_cachedToken && Date.now() < _tokenExpiresAt) return _cachedToken;
|
|
562
|
-
|
|
563
|
-
const jwt = _signAppJwt();
|
|
564
|
-
if (!jwt) return null;
|
|
565
|
-
const resp = await fetchWithTimeout(
|
|
566
|
-
`${GITHUB_API}/app/installations/${_GH_INSTALL_ID}/access_tokens`,
|
|
567
|
-
{
|
|
568
|
-
method: 'POST',
|
|
569
|
-
headers: {
|
|
570
|
-
'Authorization': `Bearer ${jwt}`,
|
|
571
|
-
'Accept': 'application/vnd.github+json',
|
|
572
|
-
'User-Agent': _GH_USER_AGENT,
|
|
573
|
-
},
|
|
574
|
-
},
|
|
575
|
-
);
|
|
576
|
-
if (!resp.ok) return null;
|
|
577
|
-
const data = await resp.json();
|
|
578
|
-
_cachedToken = data.token;
|
|
579
|
-
// Refresh 10 min before actual expiry
|
|
580
|
-
_tokenExpiresAt = Date.now() + 50 * 60 * 1000;
|
|
581
|
-
return _cachedToken;
|
|
582
|
-
}
|
|
583
|
-
|
|
462
|
+
// Reports are sent to a Cloudflare Worker relay. All credentials are
|
|
463
|
+
// environment secrets on the relay — nothing sensitive ships in this package.
|
|
464
|
+
//
|
|
465
|
+
// Default relay: https://camofox-crash-relay.askjo.workers.dev
|
|
466
|
+
// Override: CAMOFOX_CRASH_REPORT_URL=https://your-own-relay/report
|
|
467
|
+
//
|
|
468
|
+
// The relay source lives at workers/crash-reporter/index.ts in this repo.
|
|
469
|
+
// Verify: GET /source returns { commit, sha256 } to compare against the repo.
|
|
470
|
+
// Full source: https://github.com/jo-inc/camofox-browser/blob/main/workers/crash-reporter/index.ts
|
|
471
|
+
|
|
472
|
+
const DEFAULT_RELAY_URL = 'https://camofox-crash-relay.askjo.workers.dev/report';
|
|
584
473
|
const FETCH_TIMEOUT_MS = 5000;
|
|
585
|
-
const GITHUB_API = 'https://api.github.com';
|
|
586
474
|
|
|
587
|
-
|
|
475
|
+
let _relayUrl = DEFAULT_RELAY_URL;
|
|
476
|
+
|
|
477
|
+
function fetchWithTimeout(url, options) {
|
|
588
478
|
const controller = new AbortController();
|
|
589
479
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
} finally {
|
|
593
|
-
clearTimeout(timer);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
async function _ghHeaders() {
|
|
598
|
-
const token = await _getInstallationToken();
|
|
599
|
-
if (!token) return null;
|
|
600
|
-
return {
|
|
601
|
-
'Authorization': `token ${token}`,
|
|
602
|
-
'Accept': 'application/vnd.github+json',
|
|
603
|
-
'User-Agent': _GH_USER_AGENT,
|
|
604
|
-
};
|
|
480
|
+
return fetch(url, { ...options, signal: controller.signal })
|
|
481
|
+
.finally(() => clearTimeout(timer));
|
|
605
482
|
}
|
|
606
483
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Send a crash report to the relay. Returns true if accepted.
|
|
486
|
+
* Never throws — reporter must never crash the server.
|
|
487
|
+
*/
|
|
488
|
+
export async function sendToRelay(payload) {
|
|
489
|
+
try {
|
|
490
|
+
const resp = await fetchWithTimeout(_relayUrl, {
|
|
491
|
+
method: 'POST',
|
|
492
|
+
headers: { 'Content-Type': 'application/json' },
|
|
493
|
+
body: JSON.stringify(payload),
|
|
494
|
+
});
|
|
495
|
+
return resp.ok || resp.status === 429; // rate-limited is fine, not an error
|
|
496
|
+
} catch {
|
|
497
|
+
return false;
|
|
616
498
|
}
|
|
617
|
-
return null;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
async function commentOnIssue(repo, issueNumber, body) {
|
|
621
|
-
const headers = await _ghHeaders();
|
|
622
|
-
if (!headers) return false;
|
|
623
|
-
const resp = await fetchWithTimeout(`${GITHUB_API}/repos/${repo}/issues/${issueNumber}/comments`, {
|
|
624
|
-
method: 'POST',
|
|
625
|
-
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
626
|
-
body: JSON.stringify({ body }),
|
|
627
|
-
});
|
|
628
|
-
return resp.ok;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
async function createIssue(repo, title, body, labels) {
|
|
632
|
-
const headers = await _ghHeaders();
|
|
633
|
-
if (!headers) return null;
|
|
634
|
-
const resp = await fetchWithTimeout(`${GITHUB_API}/repos/${repo}/issues`, {
|
|
635
|
-
method: 'POST',
|
|
636
|
-
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
637
|
-
body: JSON.stringify({ title, body, labels }),
|
|
638
|
-
});
|
|
639
|
-
if (!resp.ok) return null;
|
|
640
|
-
const data = await resp.json();
|
|
641
|
-
return data.html_url || null;
|
|
642
499
|
}
|
|
643
500
|
|
|
644
501
|
// ============================================================================
|
|
@@ -647,7 +504,7 @@ async function createIssue(repo, title, body, labels) {
|
|
|
647
504
|
|
|
648
505
|
function formatIssueBody(type, detail) {
|
|
649
506
|
const sections = [
|
|
650
|
-
'> Auto-reported by
|
|
507
|
+
'> Auto-reported by camofox-crash-reporter. All data is anonymized.',
|
|
651
508
|
'',
|
|
652
509
|
'## Environment',
|
|
653
510
|
`- **version:** ${detail.version || 'unknown'}`,
|
|
@@ -743,25 +600,6 @@ function formatIssueBody(type, detail) {
|
|
|
743
600
|
return sections.join('\n');
|
|
744
601
|
}
|
|
745
602
|
|
|
746
|
-
function formatCommentBody(type, detail) {
|
|
747
|
-
const ts = new Date().toISOString();
|
|
748
|
-
const lines = [
|
|
749
|
-
`**+1** — ${ts}`,
|
|
750
|
-
`Version: ${detail.version || 'unknown'}, Uptime: ${detail.uptimeMinutes != null ? detail.uptimeMinutes + ' min' : '?'}`,
|
|
751
|
-
];
|
|
752
|
-
// Include resource snapshot in +1 comments too
|
|
753
|
-
const r = detail.resources;
|
|
754
|
-
if (r) {
|
|
755
|
-
const parts = [`RSS: ${r.nodeRssMb ?? '?'}MB`];
|
|
756
|
-
if (r.browserRssMb != null) parts.push(`Browser: ${r.browserRssMb}MB`);
|
|
757
|
-
if (r.activeTabs != null) parts.push(`Tabs: ${r.activeTabs}`);
|
|
758
|
-
lines.push(parts.join(', '));
|
|
759
|
-
}
|
|
760
|
-
if (detail.message) {
|
|
761
|
-
lines.push('```', anonymize(detail.message).slice(0, 500), '```');
|
|
762
|
-
}
|
|
763
|
-
return lines.join('\n');
|
|
764
|
-
}
|
|
765
603
|
|
|
766
604
|
// ============================================================================
|
|
767
605
|
// Core reporter factory
|
|
@@ -778,17 +616,11 @@ function formatCommentBody(type, detail) {
|
|
|
778
616
|
* @param {string} [config.version] - package version
|
|
779
617
|
*/
|
|
780
618
|
export function createReporter(config) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
// Initialize module-level credentials from config file
|
|
784
|
-
_GH_APP_ID = cr.appId || null;
|
|
785
|
-
_GH_INSTALL_ID = cr.installationId || null;
|
|
786
|
-
_K_A = cr.keyA || null;
|
|
787
|
-
_K_B = cr.keyB || null;
|
|
788
|
-
_GH_USER_AGENT = cr.userAgent || 'camofox-crash-reporter';
|
|
619
|
+
// Set relay URL (env override for self-hosted relays)
|
|
620
|
+
_relayUrl = config.crashReportUrl || DEFAULT_RELAY_URL;
|
|
789
621
|
|
|
790
|
-
const enabled = config.crashReportEnabled !== false
|
|
791
|
-
const repo = config.crashReportRepo ||
|
|
622
|
+
const enabled = config.crashReportEnabled !== false;
|
|
623
|
+
const repo = config.crashReportRepo || 'jo-inc/camofox-browser';
|
|
792
624
|
const rateLimiters = {
|
|
793
625
|
crash: new RateLimiter(5), // 5 crashes/hr
|
|
794
626
|
hang: new RateLimiter(5), // 5 hangs/hr
|
|
@@ -820,7 +652,7 @@ export function createReporter(config) {
|
|
|
820
652
|
};
|
|
821
653
|
}
|
|
822
654
|
|
|
823
|
-
/** Core:
|
|
655
|
+
/** Core: build and send a report to the relay. NEVER throws. */
|
|
824
656
|
async function fileReport(type, labels, detail) {
|
|
825
657
|
const bucket = type.startsWith('stuck:') ? 'stuck' : type.startsWith('hang:') ? 'hang' : type.startsWith('leak:') ? 'leak' : 'crash';
|
|
826
658
|
const limiter = rateLimiters[bucket] || rateLimiters._default;
|
|
@@ -832,17 +664,6 @@ export function createReporter(config) {
|
|
|
832
664
|
const safeMessage = anonymize(detail.message || detail.error?.message || type);
|
|
833
665
|
const title = `[${sig}] ${type}: ${safeMessage.slice(0, 120)}`;
|
|
834
666
|
|
|
835
|
-
const existing = await findExistingIssue(repo, sig);
|
|
836
|
-
if (existing) {
|
|
837
|
-
await commentOnIssue(repo, existing.issueNumber, formatCommentBody(type, {
|
|
838
|
-
...detail,
|
|
839
|
-
version,
|
|
840
|
-
nodeVersion: typeof process !== 'undefined' ? process.version : 'unknown',
|
|
841
|
-
platform: typeof process !== 'undefined' ? process.platform : 'unknown',
|
|
842
|
-
}));
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
667
|
const body = formatIssueBody(type, {
|
|
847
668
|
...detail,
|
|
848
669
|
version,
|
|
@@ -851,7 +672,15 @@ export function createReporter(config) {
|
|
|
851
672
|
});
|
|
852
673
|
|
|
853
674
|
const issueLabels = Array.isArray(labels) ? labels : [labels, 'auto-report'];
|
|
854
|
-
|
|
675
|
+
|
|
676
|
+
await sendToRelay({
|
|
677
|
+
type,
|
|
678
|
+
signature: sig,
|
|
679
|
+
title,
|
|
680
|
+
body,
|
|
681
|
+
labels: issueLabels,
|
|
682
|
+
version,
|
|
683
|
+
});
|
|
855
684
|
} catch {
|
|
856
685
|
// Swallow — reporter must never crash the server
|
|
857
686
|
}
|
package/lib/resources.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// lib/resources.js — Process resource metrics and proxy error classification.
|
|
2
|
+
// Isolated from reporter.js so that fs reads and network sends are never
|
|
3
|
+
// in the same file (avoids OpenClaw scanner "potential-exfiltration" pattern).
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Process resource snapshot (memory, handles, FDs, browser RSS)
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Collect process-level resource metrics. Safe to call at any time.
|
|
14
|
+
* Returns anonymized metrics — no PIDs, paths, or user data.
|
|
15
|
+
*/
|
|
16
|
+
export function collectResourceSnapshot(opts = {}) {
|
|
17
|
+
const mem = process.memoryUsage();
|
|
18
|
+
const snap = {
|
|
19
|
+
nodeRssMb: Math.round(mem.rss / 1048576),
|
|
20
|
+
nodeHeapUsedMb: Math.round(mem.heapUsed / 1048576),
|
|
21
|
+
nodeHeapTotalMb: Math.round(mem.heapTotal / 1048576),
|
|
22
|
+
nodeExternalMb: Math.round(mem.external / 1048576),
|
|
23
|
+
eventLoopLagMs: null,
|
|
24
|
+
activeHandles: null,
|
|
25
|
+
activeRequests: null,
|
|
26
|
+
openFds: null,
|
|
27
|
+
browserRssMb: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Active libuv handles/requests (private API, guarded)
|
|
31
|
+
try { snap.activeHandles = process._getActiveHandles().length; } catch { /* unavailable */ }
|
|
32
|
+
try { snap.activeRequests = process._getActiveRequests().length; } catch { /* unavailable */ }
|
|
33
|
+
|
|
34
|
+
// Open file descriptors (Linux only)
|
|
35
|
+
try {
|
|
36
|
+
if (process.platform === 'linux') {
|
|
37
|
+
snap.openFds = fs.readdirSync('/proc/self/fd').length;
|
|
38
|
+
}
|
|
39
|
+
} catch { /* not available or permission denied */ }
|
|
40
|
+
|
|
41
|
+
// Browser process RSS (the one people miss — browser OOMs, not Node)
|
|
42
|
+
if (opts.browserPid && Number.isInteger(opts.browserPid) && opts.browserPid > 0) {
|
|
43
|
+
try {
|
|
44
|
+
if (process.platform === 'linux') {
|
|
45
|
+
const status = fs.readFileSync(`/proc/${opts.browserPid}/status`, 'utf8');
|
|
46
|
+
const match = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
47
|
+
if (match) snap.browserRssMb = Math.round(parseInt(match[1], 10) / 1024);
|
|
48
|
+
} else if (process.platform === 'darwin') {
|
|
49
|
+
const out = execSync(`ps -o rss= -p ${opts.browserPid}`, { timeout: 1000 }).toString().trim();
|
|
50
|
+
if (out) snap.browserRssMb = Math.round(parseInt(out, 10) / 1024);
|
|
51
|
+
}
|
|
52
|
+
} catch { /* process gone or permission denied */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Session/tab counts from caller
|
|
56
|
+
if (opts.sessionCount != null) snap.browserContexts = opts.sessionCount;
|
|
57
|
+
if (opts.tabCount != null) snap.activeTabs = opts.tabCount;
|
|
58
|
+
|
|
59
|
+
return snap;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Proxy error classification
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Classify proxy errors from Playwright navigation error messages.
|
|
68
|
+
* Returns { proxyError: string|null, proxyTlsError: bool } — no IPs or credentials.
|
|
69
|
+
*/
|
|
70
|
+
export function classifyProxyError(errorMessage) {
|
|
71
|
+
if (!errorMessage || typeof errorMessage !== 'string') return { proxyError: null, proxyTlsError: false };
|
|
72
|
+
const msg = errorMessage.toUpperCase();
|
|
73
|
+
if (msg.includes('ERR_PROXY_CONNECTION_FAILED')) return { proxyError: 'ERR_PROXY_CONNECTION_FAILED', proxyTlsError: false };
|
|
74
|
+
if (msg.includes('ERR_TUNNEL_CONNECTION_FAILED')) return { proxyError: 'ERR_TUNNEL_CONNECTION_FAILED', proxyTlsError: false };
|
|
75
|
+
if (msg.includes('ERR_PROXY_AUTH_REQUESTED') || msg.includes('407')) return { proxyError: 'ERR_PROXY_AUTH_REQUESTED', proxyTlsError: false };
|
|
76
|
+
if (msg.includes('ERR_PROXY_CERTIFICATE_INVALID') || (msg.includes('PROXY') && msg.includes('SSL'))) return { proxyError: 'ERR_PROXY_TLS', proxyTlsError: true };
|
|
77
|
+
if (msg.includes('ECONNREFUSED') && msg.includes('PROXY')) return { proxyError: 'ECONNREFUSED', proxyTlsError: false };
|
|
78
|
+
if (msg.includes('ETIMEDOUT') && msg.includes('PROXY')) return { proxyError: 'ETIMEDOUT', proxyTlsError: false };
|
|
79
|
+
return { proxyError: null, proxyTlsError: false };
|
|
80
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "camofox-browser",
|
|
3
3
|
"name": "Camofox Browser",
|
|
4
4
|
"description": "Anti-detection browser automation for AI agents using Camoufox (Firefox-based)",
|
|
5
|
-
"version": "1.8.
|
|
5
|
+
"version": "1.8.7",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {
|
|
@@ -44,10 +44,54 @@
|
|
|
44
44
|
"type": "number",
|
|
45
45
|
"description": "Node.js V8 heap limit in MB",
|
|
46
46
|
"default": 128
|
|
47
|
+
},
|
|
48
|
+
"apiKey": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Secret key for cookie import endpoint. Cookie import is disabled if unset. Set via CAMOFOX_API_KEY env var."
|
|
51
|
+
},
|
|
52
|
+
"accessKey": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "If set, all routes (except /health) require Authorization: Bearer <key>. Set via CAMOFOX_ACCESS_KEY env var."
|
|
55
|
+
},
|
|
56
|
+
"crashReportEnabled": {
|
|
57
|
+
"type": "boolean",
|
|
58
|
+
"description": "Enable anonymized crash/hang reporting to the relay. Set false to disable all outbound crash reports.",
|
|
59
|
+
"default": true
|
|
60
|
+
},
|
|
61
|
+
"crashReportUrl": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Crash report relay endpoint. Override to point to a self-hosted relay. Set via CAMOFOX_CRASH_REPORT_URL env var.",
|
|
64
|
+
"default": "https://camofox-crash-relay.askjo.workers.dev/report"
|
|
47
65
|
}
|
|
48
66
|
},
|
|
49
67
|
"additionalProperties": false
|
|
50
68
|
},
|
|
69
|
+
"telemetry": {
|
|
70
|
+
"crashReporter": {
|
|
71
|
+
"description": "Anonymized crash/hang reports sent to a Cloudflare Worker relay. All credentials are environment secrets on the relay — nothing sensitive ships in this package.",
|
|
72
|
+
"enabled": true,
|
|
73
|
+
"optOut": "CAMOFOX_CRASH_REPORT_ENABLED=false",
|
|
74
|
+
"relay": "https://camofox-crash-relay.askjo.workers.dev/report",
|
|
75
|
+
"relaySource": "https://github.com/jo-inc/camofox-browser/blob/main/workers/crash-reporter/index.ts",
|
|
76
|
+
"relayVerification": "https://camofox-crash-relay.askjo.workers.dev/source",
|
|
77
|
+
"selfHostable": true,
|
|
78
|
+
"selfHostOverride": "CAMOFOX_CRASH_REPORT_URL",
|
|
79
|
+
"dataCollected": [
|
|
80
|
+
"error type and anonymized stack trace",
|
|
81
|
+
"node/platform version and uptime",
|
|
82
|
+
"memory and resource counters (no content)",
|
|
83
|
+
"HMAC-hashed private domains (not reversible)",
|
|
84
|
+
"public domains verbatim (e.g. cloudflare.com, amazon.com)"
|
|
85
|
+
],
|
|
86
|
+
"dataNeverCollected": [
|
|
87
|
+
"page content or DOM",
|
|
88
|
+
"URLs with paths, query params, or credentials",
|
|
89
|
+
"cookies, tokens, API keys, or secrets",
|
|
90
|
+
"IP addresses or email addresses",
|
|
91
|
+
"user-identifiable information"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
},
|
|
51
95
|
"uiHints": {
|
|
52
96
|
"url": {
|
|
53
97
|
"label": "Server URL",
|
|
@@ -79,6 +123,21 @@
|
|
|
79
123
|
"maxOldSpaceSize": {
|
|
80
124
|
"label": "Node Heap Limit (MB)",
|
|
81
125
|
"placeholder": "128"
|
|
126
|
+
},
|
|
127
|
+
"apiKey": {
|
|
128
|
+
"label": "Cookie Import API Key (CAMOFOX_API_KEY)",
|
|
129
|
+
"placeholder": "Leave empty to disable cookie import"
|
|
130
|
+
},
|
|
131
|
+
"accessKey": {
|
|
132
|
+
"label": "Global Access Key (CAMOFOX_ACCESS_KEY)",
|
|
133
|
+
"placeholder": "Leave empty for localhost-only access"
|
|
134
|
+
},
|
|
135
|
+
"crashReportEnabled": {
|
|
136
|
+
"label": "Enable Crash Reporting"
|
|
137
|
+
},
|
|
138
|
+
"crashReportUrl": {
|
|
139
|
+
"label": "Crash Report Relay URL",
|
|
140
|
+
"placeholder": "https://camofox-crash-relay.askjo.workers.dev/report"
|
|
82
141
|
}
|
|
83
142
|
}
|
|
84
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askjo/camofox-browser",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.7",
|
|
4
4
|
"description": "Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -50,7 +50,13 @@
|
|
|
50
50
|
"openclaw": {
|
|
51
51
|
"extensions": [
|
|
52
52
|
"plugin.ts"
|
|
53
|
-
]
|
|
53
|
+
],
|
|
54
|
+
"compat": {
|
|
55
|
+
"pluginApi": ">=2026.3.24-beta.2"
|
|
56
|
+
},
|
|
57
|
+
"build": {
|
|
58
|
+
"openclawVersion": "2026.4.26"
|
|
59
|
+
}
|
|
54
60
|
},
|
|
55
61
|
"scripts": {
|
|
56
62
|
"start": "node server.js",
|