🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

otterkit

Package Overview
Dependencies
Maintainers
1
Versions
41
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

otterkit - npm Package Compare versions

Comparing version
0.30.0
to
0.31.0
+15
-1
dist/daemon.d.ts

@@ -28,4 +28,15 @@ /**

profile?: string;
/**
* Session archive: set when the session ends (stopped, expired, or the
* owning process died). Ended entries and their capture logs are kept for
* inspection/replay until the retention sweep prunes them.
*/
endedAt?: string;
}
export declare function addDaemon(entry: DaemonEntry): void;
/**
* A session ended (stopped, expired, or its process died): archive the entry
* instead of deleting it, keeping the capture log for inspection/replay.
* The retention sweep in cleanupDaemons prunes old archives.
*/
export declare function removeDaemon(subdomain: string): void;

@@ -37,3 +48,6 @@ /**

/**
* Clean up dead/expired daemons from the state file
* Reconcile the registry: archive sessions that ended, prune old archives
* (and their capture logs) past retention. Returns the ACTIVE entries -
* callers that manage running sessions keep their old semantics; ended
* entries stay in the file for `status`, the Mac app, and inspection.
*/

@@ -40,0 +54,0 @@ export declare function cleanupDaemons(): DaemonEntry[];

@@ -8,3 +8,3 @@ /**

*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
import { homedir } from 'node:os';

@@ -14,2 +14,6 @@ import { join } from 'node:path';

const STATE_FILE = join(STATE_DIR, 'daemons.json');
/** Ended sessions are kept this long... */
const ENDED_RETENTION_MS = 7 * 86_400_000;
/** ...and at most this many (newest first). */
const ENDED_RETENTION_COUNT = 20;
function ensureStateDir() {

@@ -37,9 +41,20 @@ if (!existsSync(STATE_DIR)) {

export function addDaemon(entry) {
const daemons = loadDaemons();
// A reused (reserved) subdomain replaces any archived entry for it - one
// registry row per subdomain, always the current life.
const daemons = loadDaemons().filter(d => d.subdomain !== entry.subdomain);
daemons.push(entry);
saveDaemons(daemons);
}
/**
* A session ended (stopped, expired, or its process died): archive the entry
* instead of deleting it, keeping the capture log for inspection/replay.
* The retention sweep in cleanupDaemons prunes old archives.
*/
export function removeDaemon(subdomain) {
const daemons = loadDaemons().filter(d => d.subdomain !== subdomain);
saveDaemons(daemons);
const daemons = loadDaemons();
const entry = daemons.find(d => d.subdomain === subdomain);
if (entry && !entry.endedAt) {
entry.endedAt = new Date().toISOString();
saveDaemons(daemons);
}
}

@@ -59,3 +74,6 @@ /**

/**
* Clean up dead/expired daemons from the state file
* Reconcile the registry: archive sessions that ended, prune old archives
* (and their capture logs) past retention. Returns the ACTIVE entries -
* callers that manage running sessions keep their old semantics; ended
* entries stay in the file for `status`, the Mac app, and inspection.
*/

@@ -65,11 +83,43 @@ export function cleanupDaemons() {

const now = Date.now();
const alive = daemons.filter(d => {
// 'never' parses to NaN - a TTL-less daemon is never expiry-pruned.
let changed = false;
for (const d of daemons) {
if (d.endedAt)
continue;
// 'never' parses to NaN - a TTL-less session is never expiry-archived.
const exp = new Date(d.expiresAt).getTime();
const expired = Number.isFinite(exp) && exp <= now;
const running = isProcessAlive(d.pid);
return running && !expired;
});
saveDaemons(alive);
return alive;
// A dead process ends a TUNNEL (nothing serves without it). A webhook
// endpoint is server-answered and stays live without any local process -
// only expiry (or an explicit stop) ends it.
const ended = expired || (!running && d.port !== 0);
if (ended) {
d.endedAt = new Date().toISOString();
changed = true;
}
}
// Retention sweep over the archive: newest ENDED_RETENTION_COUNT within
// ENDED_RETENTION_MS survive; pruned entries take their capture log along.
const ended = daemons.filter(d => d.endedAt).sort((a, b) => (a.endedAt < b.endedAt ? 1 : -1));
const keep = new Set(ended
.filter(d => now - new Date(d.endedAt).getTime() < ENDED_RETENTION_MS)
.slice(0, ENDED_RETENTION_COUNT)
.map(d => d.subdomain));
const kept = [];
for (const d of daemons) {
if (d.endedAt && !keep.has(d.subdomain)) {
changed = true;
try {
unlinkSync(join(STATE_DIR, 'requests', `${d.subdomain}.jsonl`));
}
catch {
/* no log or already gone */
}
continue;
}
kept.push(d);
}
if (changed)
saveDaemons(kept);
return kept.filter(d => !d.endedAt);
}

@@ -76,0 +126,0 @@ /**

+8
-2

@@ -136,4 +136,10 @@ /**

: undefined;
// v6-only dev servers: when delivering to default localhost, also try
// the v6 loopback before calling it a failure.
const urls = [url, url];
if (url.startsWith('http://127.0.0.1:')) {
urls.push(url.replace('http://127.0.0.1:', 'http://[::1]:'));
}
let lastError = '';
for (let attempt = 0; attempt < 2; attempt++) {
for (let attempt = 0; attempt < urls.length; attempt++) {
if (attempt > 0)

@@ -144,3 +150,3 @@ await sleep(HTTP_RETRY_DELAY_MS);

try {
const response = await fetch(url, {
const response = await fetch(urls[attempt], {
method: entry.method,

@@ -147,0 +153,0 @@ headers,

@@ -184,3 +184,9 @@ /**

const startTime = Date.now();
const targetUrl = `http://${this.config.targetHost}:${this.config.targetPort}${req.path}`;
// Dev servers often bind IPv6 localhost only (`app.listen(port)` →
// [::1] on modern macOS): when targeting default localhost, fall back
// to the v6 loopback if the v4 dial is refused.
const hosts = this.config.targetHost === '127.0.0.1' || this.config.targetHost === 'localhost'
? ['127.0.0.1', '[::1]']
: [this.config.targetHost];
const targetUrl = `http://${hosts[0]}:${this.config.targetPort}${req.path}`;
try {

@@ -201,8 +207,20 @@ // Decode base64 body if present

}
// Forward to local server
const response = await fetch(targetUrl, {
method: req.method,
headers,
body: ['GET', 'HEAD'].includes(req.method) ? undefined : body,
});
// Forward to local server (each loopback family in turn)
let response;
let lastError;
for (const host of hosts) {
try {
response = await fetch(`http://${host}:${this.config.targetPort}${req.path}`, {
method: req.method,
headers,
body: ['GET', 'HEAD'].includes(req.method) ? undefined : body,
});
break;
}
catch (err) {
lastError = err;
}
}
if (!response)
throw lastError;
// Read response body

@@ -209,0 +227,0 @@ const responseBody = await response.arrayBuffer();

{
"name": "otterkit",
"version": "0.30.0",
"version": "0.31.0",
"description": "OtterKit CLI - provision and connect tunnels for AI agents",

@@ -5,0 +5,0 @@ "mcpName": "io.github.useotterkit/otterkit",

Sorry, the diff of this file is too big to display