🎩 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.29.0
to
0.30.0
+1
-1
dist/daemon-launch.d.ts

@@ -18,3 +18,3 @@ export interface DaemonLaunchOptions {

};
/** Keep captures in portal history too (webhook only; off by default). */
/** Keep captures in console history too (webhook only; off by default). */
store?: boolean;

@@ -21,0 +21,0 @@ /** Local delivery (webhook only): mirror captures to this base URL. */

@@ -22,5 +22,10 @@ /**

const expiresAt = ttlMs === null ? 'never' : new Date(Date.now() + ttlMs).toISOString();
const workerPath = join(__dirname, 'daemon-worker.js');
// In a Bun-compiled binary (Mac app sidecar) __dirname is a virtual embedded
// path and process.execPath is the compiled CLI itself - re-exec it with the
// "__daemon-worker" marker (dispatched at the top of index.ts) instead of
// spawning the worker file by path. Inert under Node.
const isCompiled = typeof globalThis.Bun !== 'undefined';
const workerEntry = isCompiled ? '__daemon-worker' : join(__dirname, 'daemon-worker.js');
// ttlMs 0 = no client-side auto-exit (the server owns lifecycle for 'never').
const args = [workerPath, connectUrl, options.host, String(port), String(ttlMs ?? 0), subdomain];
const args = [workerEntry, connectUrl, options.host, String(port), String(ttlMs ?? 0), subdomain];
if (capture)

@@ -27,0 +32,0 @@ args.push('--capture');

@@ -23,2 +23,5 @@ /**

deliver?: boolean;
/** Interactive terminal session (not a daemon) - registered so `status`
* and the Mac app see it; the entry dies with the process. */
foreground?: boolean;
/** Config profile name this daemon was started from (`otterkit up`). */

@@ -25,0 +28,0 @@ profile?: string;

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

/** Headers recomputed by fetch or meaningless outside the original hop. */
const STRIP_HEADERS = new Set(['host', 'connection', 'upgrade', 'transfer-encoding', 'content-length']);
const STRIP_HEADERS = new Set([
'host',
'connection',
'upgrade',
'transfer-encoding',
'content-length',
]);
const HTTP_TIMEOUT_MS = 10_000;

@@ -16,0 +22,0 @@ const HTTP_RETRY_DELAY_MS = 500;

@@ -5,5 +5,5 @@ /**

* Where `otterkit inspect` reads the LOCAL capture log (this machine's
* sessions), this reads the history OtterKit stored server-side: portal
* sessions), this reads the history OtterKit stored server-side: console
* endpoints (always stored) and CLI webhooks provisioned with --store.
* Talks to the portal API (OTTERKIT_API_URL) with the login token, so it
* Talks to the console API (OTTERKIT_API_URL) with the login token, so it
* works from any machine on the account - and gives agents the same

@@ -138,3 +138,3 @@ * "what arrived?" read the API exposes at /api/me/webhooks.

console.log(`\n ${dim('No stored requests for')} ${c.bold}${subdomain}${c.reset}`);
console.log(` ${dim('Portal endpoints store automatically; CLI webhooks need --store. Local captures: otterkit inspect.')}\n`);
console.log(` ${dim('Console endpoints store automatically; CLI webhooks need --store. Local captures: otterkit inspect.')}\n`);
return;

@@ -141,0 +141,0 @@ }

@@ -1,4 +0,11 @@

export declare function login(): Promise<void>;
export declare function logout(): void;
/**
* `--json` mode streams machine-readable JSONL events instead of ANSI text
* and does NOT open the browser (the caller owns that): one line
* {"event":"code",userCode,verificationUri,verificationUriComplete,expiresIn}
* then {"event":"approved",email} or {"event":"error",code}. Built for the
* Mac app driving the device flow, useful to any script.
*/
export declare function login(json?: boolean): Promise<void>;
export declare function logout(json?: boolean): void;
export declare function whoami(json?: boolean): Promise<void>;
export declare function balance(json?: boolean): Promise<void>;
/**
* `otterkit login` - browser device-authorization flow.
*
* 1. POST /api/device/start → prints a user code and opens the portal.
* 2. User signs in (Clerk) and approves the code in the portal.
* 1. POST /api/device/start → prints a user code and opens the console.
* 2. User signs in (Clerk) and approves the code in the console.
* 3. CLI polls /api/device/poll until approved, then saves the token.
*
* Login/balance talk to the portal API (OTTERKIT_API_URL), distinct from
* Login/balance talk to the console API (OTTERKIT_API_URL), distinct from
* tunnel provisioning which talks to OTTERKIT_TUNNEL_URL.

@@ -16,4 +16,13 @@ */

const API_SERVER = process.env.OTTERKIT_API_URL || 'https://api.otterkit.com';
const { version: CLI_VERSION } = createRequire(import.meta.url)('../package.json');
/** Best-effort machine identity so the portal can label the minted token. */
// Falls back to the compile-time --define value inside a compiled binary,
// where the dynamic package.json require can't resolve.
const CLI_VERSION = (() => {
try {
return createRequire(import.meta.url)('../package.json').version;
}
catch {
return process.env.OTTERKIT_CLI_VERSION || '0.0.0';
}
})();
/** Best-effort machine identity so the console can label the minted token. */
function deviceInfo() {

@@ -70,5 +79,17 @@ let username;

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
export async function login() {
/**
* `--json` mode streams machine-readable JSONL events instead of ANSI text
* and does NOT open the browser (the caller owns that): one line
* {"event":"code",userCode,verificationUri,verificationUriComplete,expiresIn}
* then {"event":"approved",email} or {"event":"error",code}. Built for the
* Mac app driving the device flow, useful to any script.
*/
export async function login(json) {
const emit = (obj) => console.log(JSON.stringify(obj));
const start = await api('/api/device/start', { device: deviceInfo() });
if (!start.ok || !start.data) {
if (json) {
emit({ event: 'error', code: 'start_failed', message: start.error ?? null });
process.exit(1);
}
console.log(`\n ${CROSS} Could not start login (${start.error || 'unknown error'})\n`);

@@ -78,10 +99,21 @@ process.exit(1);

const { device_code, user_code, verification_uri, verification_uri_complete, interval, expires_in, } = start.data;
console.log('');
console.log(` ${c.bold}Sign in to OtterKit${c.reset}`);
console.log('');
console.log(` Your code: ${c.bold}${c.cyan}${user_code}${c.reset}`);
console.log(` Opening: ${dim(verification_uri)}`);
console.log('');
console.log(` ${dim('Waiting for approval in your browser…')}`);
openBrowser(verification_uri_complete);
if (json) {
emit({
event: 'code',
userCode: user_code,
verificationUri: verification_uri,
verificationUriComplete: verification_uri_complete,
expiresIn: expires_in,
});
}
else {
console.log('');
console.log(` ${c.bold}Sign in to OtterKit${c.reset}`);
console.log('');
console.log(` Your code: ${c.bold}${c.cyan}${user_code}${c.reset}`);
console.log(` Opening: ${dim(verification_uri)}`);
console.log('');
console.log(` ${dim('Waiting for approval in your browser…')}`);
openBrowser(verification_uri_complete);
}
const deadline = Date.now() + expires_in * 1000;

@@ -94,7 +126,14 @@ const intervalMs = Math.max(1, interval) * 1000;

if (status === 'approved' && poll.data?.token) {
const token = poll.data.token;
saveCredentials({
token: poll.data.token,
token,
server: API_SERVER,
savedAt: new Date().toISOString(),
});
if (json) {
// Best-effort email for the caller's UI; the login itself is done.
const me = await api('/api/me', undefined, token).catch(() => null);
emit({ event: 'approved', email: me?.data?.email ?? null });
return;
}
console.log('');

@@ -107,2 +146,6 @@ console.log(` ${CHECK} ${c.bold}Logged in.${c.reset} Agents on this machine can now provision tunnels.`);

if (status === 'denied') {
if (json) {
emit({ event: 'error', code: 'denied' });
process.exit(1);
}
console.log(`\n ${CROSS} Login was denied.\n`);

@@ -112,2 +155,6 @@ process.exit(1);

if (status === 'expired') {
if (json) {
emit({ event: 'error', code: 'expired' });
process.exit(1);
}
console.log(`\n ${CROSS} Login code expired. Run ${c.bold}otterkit login${c.reset} again.\n`);

@@ -117,7 +164,15 @@ process.exit(1);

}
if (json) {
console.log(JSON.stringify({ event: 'error', code: 'timeout' }));
process.exit(1);
}
console.log(`\n ${CROSS} Login timed out. Run ${c.bold}otterkit login${c.reset} again.\n`);
process.exit(1);
}
export function logout() {
export function logout(json) {
const cleared = clearCredentials();
if (json) {
console.log(JSON.stringify({ loggedOut: cleared }));
return;
}
console.log('');

@@ -124,0 +179,0 @@ console.log(cleared ? ` ${CHECK} Logged out.` : ` ${dim('Not logged in.')}`);

@@ -46,3 +46,3 @@ /**

balance: e.balance,
topUpUrl: e.topUpUrl || 'https://app.otterkit.com/billing',
topUpUrl: e.topUpUrl || 'https://console.otterkit.com/billing',
});

@@ -132,3 +132,3 @@ }

.optional()
.describe('Also persist captures to the OtterKit portal history (server-side). Off by default: captures stay in the local log only - the private default for sensitive payloads.'),
.describe('Also persist captures to the OtterKit console history (server-side). Off by default: captures stay in the local log only - the private default for sensitive payloads.'),
verify: z

@@ -467,3 +467,3 @@ .object({

description: 'Read the requests OtterKit stored server-side for a webhook endpoint - works from any ' +
'machine on the account, no local log needed. Portal endpoints always store; CLI ' +
'machine on the account, no local log needed. Console endpoints always store; CLI ' +
'webhooks only with --store (otherwise use the local capture log tools). Returns ' +

@@ -505,3 +505,3 @@ 'headers, base64 body, source IP, and served status, newest first.',

'Groups by path (and by a detected event field like type/event), reports field ' +
'optionality as present-in-N/M counts. Needs stored history: portal endpoints always ' +
'optionality as present-in-N/M counts. Needs stored history: console endpoints always ' +
'store; CLI webhooks only with --store.',

@@ -551,3 +551,3 @@ inputSchema: {

workspaceId: data.data?.workspace_id,
topUpUrl: 'https://app.otterkit.com/billing',
topUpUrl: 'https://console.otterkit.com/billing',
});

@@ -554,0 +554,0 @@ }

@@ -54,3 +54,3 @@ /**

// CLI webhooks default to no-store (captures stay in the local log);
// --store opts into server-side portal history.
// --store opts into server-side console history.
if (store)

@@ -57,0 +57,0 @@ target.searchParams.set('store', 'true');

@@ -28,2 +28,6 @@ /**

replayed?: boolean;
/** Body exceeded the capture cap - body is null, bodySize is the true size. */
bodyTruncated?: boolean;
/** Response body exceeded the cap or was an asset content-type. */
responseBodyTruncated?: boolean;
}

@@ -30,0 +34,0 @@ export declare function ensureRequestsDir(): Promise<void>;

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

*/
import { appendFile, mkdir, unlink, readFile } from 'node:fs/promises';
import { appendFile, mkdir, unlink, readFile, rename, writeFile } from 'node:fs/promises';
import { existsSync, statSync, createReadStream } from 'node:fs';

@@ -19,2 +19,9 @@ import { join } from 'node:path';

}
/** Rotate past this size... */
const LOG_MAX_BYTES = 50 * 1024 * 1024;
/** ...keeping the newest entries up to this many bytes. */
const LOG_KEEP_BYTES = 25 * 1024 * 1024;
/** Stat the file only every N writes - rotation is a rare event. */
const ROTATE_CHECK_EVERY = 50;
const writesSinceCheck = new Map();
export async function logRequest(subdomain, entry) {

@@ -24,3 +31,37 @@ await ensureRequestsDir();

await appendFile(file, JSON.stringify(entry) + '\n');
const n = (writesSinceCheck.get(subdomain) ?? 0) + 1;
if (n < ROTATE_CHECK_EVERY) {
writesSinceCheck.set(subdomain, n);
return;
}
writesSinceCheck.set(subdomain, 0);
await rotateIfNeeded(file).catch(() => {
/* rotation is best-effort; capture keeps appending regardless */
});
}
/**
* Auto-rotation: when the log outgrows LOG_MAX_BYTES, rewrite it keeping the
* newest entries (~LOG_KEEP_BYTES). Written to a temp file and renamed so
* readers never see a torn file; tailers observe the size drop and re-read
* from the top.
*/
async function rotateIfNeeded(file) {
if (!existsSync(file) || statSync(file).size <= LOG_MAX_BYTES)
return;
const raw = await readFile(file, 'utf-8');
const lines = raw.split('\n');
let bytes = 0;
let start = lines.length;
for (let i = lines.length - 1; i >= 0; i--) {
const lineBytes = Buffer.byteLength(lines[i]) + 1;
if (bytes + lineBytes > LOG_KEEP_BYTES)
break;
bytes += lineBytes;
start = i;
}
const kept = lines.slice(start).join('\n');
const tmp = `${file}.rotate`;
await writeFile(tmp, kept.endsWith('\n') || kept === '' ? kept : `${kept}\n`);
await rename(tmp, file);
}
export function getLogPath(subdomain) {

@@ -27,0 +68,0 @@ return join(REQUESTS_DIR, `${subdomain}.jsonl`);

/**
* `otterkit subdomains` - manage stable, reserved tunnel subdomains.
*
* Reservations live in the OtterKit portal (workspace-scoped), so these
* commands talk to the portal API (OTTERKIT_API_URL), distinct from tunnel
* Reservations live in the OtterKit console (workspace-scoped), so these
* commands talk to the console API (OTTERKIT_API_URL), distinct from tunnel
* provisioning which talks to OTTERKIT_TUNNEL_URL. Holding a name is free;

@@ -7,0 +7,0 @@ * you still pay the normal per-hour rate only while a tunnel is connected.

@@ -35,2 +35,4 @@ /**

const MAX_RECONNECT_ATTEMPTS = 10;
/** Tunnel captures store bodies up to this size; larger = metadata only. */
const CAPTURE_MAX_BODY_BYTES = 64 * 1024;
export class TunnelClient {

@@ -280,11 +282,25 @@ config;

headers: req.headers,
body: req.body,
body: bodySize > CAPTURE_MAX_BODY_BYTES ? null : req.body,
bodySize,
};
if (bodySize > CAPTURE_MAX_BODY_BYTES)
entry.bodyTruncated = true;
if (this.config.targetPort > 0) {
// Forward to local server and capture the full response
// Forward to local server and capture the full response. Bodies over
// the cap - and pure asset responses - are logged as metadata only,
// so a dev site's bundles/images can't balloon the capture log.
const result = await this.forwardToLocal(req);
entry.status = result.status;
entry.durationMs = result.durationMs;
entry.response = { headers: result.responseHeaders, body: result.responseBody };
const responseSize = result.responseBody
? Buffer.from(result.responseBody, 'base64').length
: 0;
const contentType = result.responseHeaders['content-type'] ?? '';
const skipBody = responseSize > CAPTURE_MAX_BODY_BYTES || /^(image|font|video|audio)\//.test(contentType);
entry.response = {
headers: result.responseHeaders,
body: skipBody ? null : result.responseBody,
};
if (skipBody && responseSize > 0)
entry.responseBodyTruncated = true;
}

@@ -291,0 +307,0 @@ else {

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

* Auth is the session's connect token. For sessions this CLI started it is
* recovered from the local daemon record's connectUrl; for portal-created
* recovered from the local daemon record's connectUrl; for console-created
* endpoints it comes from the endpoint's connectUrl.

@@ -11,0 +11,0 @@ */

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

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

"clean": "rimraf node_modules .turbo dist",
"release:patch": "npm run build && npm version patch && npm publish",
"release:minor": "npm run build && npm version minor && npm publish",
"release:major": "npm run build && npm version major && npm publish"
"release:patch": "npm run build && npm version patch --no-git-tag-version --workspaces=false && npm publish --workspaces=false",
"release:minor": "npm run build && npm version minor --no-git-tag-version --workspaces=false && npm publish --workspaces=false",
"release:major": "npm run build && npm version major --no-git-tag-version --workspaces=false && npm publish --workspaces=false"
},

@@ -40,0 +40,0 @@ "dependencies": {

@@ -15,3 +15,3 @@ # otterkit

Buy credits at [app.otterkit.com](https://app.otterkit.com). Once you've logged in,
Buy credits at [console.otterkit.com](https://console.otterkit.com). Once you've logged in,
agents on the same machine provision automatically using your credits.

@@ -278,3 +278,3 @@

Metered by connected time. 1 credit = $0.01. Buy prepaid credits in the portal.
Metered by connected time. 1 credit = $0.01. Buy prepaid credits in the console.

@@ -298,7 +298,7 @@ | What | Cost |

For headless/CI, set `OTTERKIT_TOKEN` (create a token at app.otterkit.com) instead of `otterkit login`.
For headless/CI, set `OTTERKIT_TOKEN` (create a token at console.otterkit.com) instead of `otterkit login`.
## Links
- [Console](https://app.otterkit.com)
- [Console](https://console.otterkit.com)
- [Docs](https://www.otterkit.com/docs)

@@ -305,0 +305,0 @@ - [Pricing API](https://otterkit.app/api/agent/pricing)

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