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

@torknetwork/cli

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@torknetwork/cli - npm Package Compare versions

Comparing version
0.1.1
to
0.2.0
+15
dist/commands/logs.d.ts
import { Command } from '@oclif/core';
import { type ReceiptSummary } from '../lib/api.js';
export default class Logs extends Command {
static description: string;
static enableJsonFlag: boolean;
static examples: string[];
static flags: {
follow: import("@oclif/core/interfaces").BooleanFlag<boolean>;
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
};
run(): Promise<{
entries: ReceiptSummary[];
total?: number;
}>;
}
import { Command, Flags } from '@oclif/core';
import chalk from 'chalk';
import { setTimeout as sleep } from 'node:timers/promises';
import { ApiError, AuthError, NetworkError, TorkApi } from '../lib/api.js';
import { getApiKey, getBaseUrl } from '../lib/config.js';
import { ARROW } from '../lib/output.js';
import { formatLogRow, logHeader, selectNew } from '../lib/receipts.js';
const POLL_INTERVAL_MS = 5000;
export default class Logs extends Command {
static description = 'Show recent governance decisions for your org (backed by the receipts feed)';
static enableJsonFlag = true;
static examples = ['<%= config.bin %> logs', '<%= config.bin %> logs --limit 50', '<%= config.bin %> logs --follow'];
static flags = {
follow: Flags.boolean({ description: 'poll for new decisions every 5s (Ctrl+C to stop)', exclusive: ['json'] }),
limit: Flags.integer({ default: 20, description: 'number of decisions to show', min: 1 }),
};
async run() {
const { flags } = await this.parse(Logs);
const key = getApiKey();
if (key === undefined) {
this.error(`Not authenticated. Run ${chalk.bold('tork login')} first (or set TORK_API_KEY).`, { exit: 1 });
}
const api = new TorkApi(getBaseUrl(), key);
let page;
try {
page = await api.receipts(flags.limit);
}
catch (error) {
if (error instanceof AuthError) {
this.error(`Authentication failed (HTTP ${error.status}). Your key may be revoked — run ${chalk.bold('tork login')} again.`, { exit: 1 });
}
if (error instanceof ApiError && error.status === 404) {
this.error('The decision log requires server support — the receipts feed was not found on this server. Coming soon.', { exit: 1 });
}
if (error instanceof NetworkError) {
this.error(`Network error: ${error.message}`, { exit: 1 });
}
if (error instanceof ApiError) {
this.error(`Could not fetch the decision log (HTTP ${error.status}): ${error.bodyExcerpt || 'no response body'}`, { exit: 1 });
}
throw error;
}
const seen = new Set();
const entries = selectNew(page.receipts, seen);
if (!this.jsonEnabled()) {
if (entries.length === 0) {
this.log(chalk.dim('No governance decisions recorded yet — run `tork test` to send one.'));
}
else {
this.log(logHeader());
for (const entry of entries)
this.log(formatLogRow(entry));
}
}
if (flags.follow) {
this.log(chalk.dim(`… following (polling every ${POLL_INTERVAL_MS / 1000}s) — Ctrl+C to stop`));
while (true) {
await sleep(POLL_INTERVAL_MS);
try {
const next = await api.receipts(Math.max(flags.limit, 20));
for (const entry of selectNew(next.receipts, seen))
this.log(formatLogRow(entry));
}
catch (error) {
if (error instanceof AuthError) {
this.error(`Authentication failed (HTTP ${error.status}) while following — stopping.`, { exit: 1 });
}
// transient network/server hiccups should not kill a follow session
this.log(`${ARROW} ${chalk.dim(`poll failed (${error instanceof Error ? error.message : String(error)}) — retrying`)}`);
}
}
}
return { entries, ...(page.total === undefined ? {} : { total: page.total }) };
}
}
import { Command } from '@oclif/core';
import { type ReceiptSummary } from '../lib/api.js';
interface Verification {
method: 'server' | 'structure';
verified: boolean;
detail: string;
}
interface ReceiptsResult {
receipts?: ReceiptSummary[];
total?: number;
receipt?: Record<string, unknown>;
verification?: Verification;
}
export default class Receipts extends Command {
static args: {
receipt_id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
};
static description: string;
static enableJsonFlag: boolean;
static examples: string[];
static flags: {
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
};
run(): Promise<ReceiptsResult>;
private list;
private show;
/**
* Server-side merkle verification needs a fingerprint, which the receipts
* read API does not currently return — in that case fall back to a local
* structural check and say so. Never claim cryptographic verification here.
*/
private verify;
private rethrow;
}
export {};
import { Args, Command, Flags } from '@oclif/core';
import chalk from 'chalk';
import { ApiError, AuthError, NetworkError, TorkApi } from '../lib/api.js';
import { getApiKey, getBaseUrl } from '../lib/config.js';
import { ARROW, CHECK, CROSS } from '../lib/output.js';
import { checkReceiptStructure, colorAction, formatLogRow, formatTimestamp, logHeader, selectNew } from '../lib/receipts.js';
export default class Receipts extends Command {
static args = {
receipt_id: Args.string({ description: 'fetch and verify a single receipt by id', required: false }),
};
static description = 'List governance receipts, or fetch and verify one by id';
static enableJsonFlag = true;
static examples = [
'<%= config.bin %> receipts',
'<%= config.bin %> receipts --limit 50',
'<%= config.bin %> receipts tork_rcpt_1784117133408_6ce3853120157b4b',
];
static flags = {
limit: Flags.integer({ default: 20, description: 'number of receipts to list', min: 1 }),
};
async run() {
const { args, flags } = await this.parse(Receipts);
const key = getApiKey();
if (key === undefined) {
this.error(`Not authenticated. Run ${chalk.bold('tork login')} first (or set TORK_API_KEY).`, { exit: 1 });
}
const api = new TorkApi(getBaseUrl(), key);
return args.receipt_id === undefined ? this.list(api, flags.limit) : this.show(api, args.receipt_id);
}
async list(api, limit) {
let page;
try {
page = await api.receipts(limit);
}
catch (error) {
this.rethrow(error, 'fetch receipts');
}
const receipts = selectNew(page.receipts, new Set()).reverse(); // newest first, deduped
if (!this.jsonEnabled()) {
if (receipts.length === 0) {
this.log(chalk.dim('No receipts yet — run `tork test` to generate one.'));
}
else {
this.log(logHeader());
for (const receipt of receipts)
this.log(formatLogRow(receipt));
if (page.total !== undefined)
this.log(chalk.dim(`${receipts.length} of ${page.total} receipts`));
}
}
return { receipts, ...(page.total === undefined ? {} : { total: page.total }) };
}
async show(api, id) {
let receipt;
try {
receipt = await api.receipt(id);
}
catch (error) {
if (error instanceof ApiError && !(error instanceof AuthError) && error.status === 404) {
this.error(`Receipt ${chalk.bold(id)} was not found.`, { exit: 1 });
}
this.rethrow(error, 'fetch the receipt');
}
const verification = await this.verify(api, receipt);
if (!this.jsonEnabled()) {
const field = (value) => typeof value === 'string' && value !== '' ? value : chalk.dim('— not exposed by the receipts API');
const piiTypes = Array.isArray(receipt.pii_types) ? ` (${receipt.pii_types.join(', ')})` : '';
this.log(`receipt: ${chalk.bold(String(receipt.receipt_id ?? id))}`);
this.log(`timestamp: ${formatTimestamp(typeof receipt.timestamp === 'string' ? receipt.timestamp : undefined)}`);
this.log(`action: ${colorAction(typeof receipt.action === 'string' ? receipt.action : undefined)}`);
this.log(`pii: ${String(receipt.pii_count ?? 0)}${piiTypes}`);
this.log(`policy: ${field(receipt.policy_version)}`);
this.log(`content hash: ${field(receipt.content_hash)}`);
this.log(`fingerprint: ${field(receipt.fingerprint)}`);
this.log(`governance DNA: ${field(receipt.governance_dna === undefined ? undefined : JSON.stringify(receipt.governance_dna))}`);
this.log(`signature: ${typeof receipt.hmac_signature === 'string' && receipt.hmac_signature !== '' ? 'yes' : `no ${chalk.dim('(hmac_signature not exposed by the receipts API)')}`}`);
this.log(`verification: ${verification.verified ? CHECK : verification.method === 'structure' ? ARROW : CROSS} ${verification.detail}`);
}
if (verification.method === 'server' && !verification.verified)
process.exitCode = 1;
return { receipt, verification };
}
/**
* Server-side merkle verification needs a fingerprint, which the receipts
* read API does not currently return — in that case fall back to a local
* structural check and say so. Never claim cryptographic verification here.
*/
async verify(api, receipt) {
const fingerprint = receipt.fingerprint;
const orgId = receipt.organization_id ?? receipt.org_id;
const timestamp = receipt.timestamp;
const date = typeof timestamp === 'string' ? timestamp.slice(0, 10) : undefined;
if (typeof fingerprint === 'string' && fingerprint !== '' && typeof orgId === 'string' && date !== undefined) {
let result;
try {
result = await api.verify({ date, fingerprint, org_id: orgId });
}
catch (error) {
return {
detail: `server verification unavailable (${error instanceof Error ? error.message : String(error)}) — showing receipt unverified`,
method: 'server',
verified: false,
};
}
if (result.verified === true) {
return { detail: 'verified against the server merkle anchor', method: 'server', verified: true };
}
return { detail: `failed — ${typeof result.message === 'string' ? result.message : 'server did not verify this fingerprint'}`, method: 'server', verified: false };
}
const check = checkReceiptStructure(receipt);
const label = 'structure check only — server-side signature verification not exposed for this receipt';
if (check.ok) {
return { detail: `${label} (structural fields OK; not returned: ${check.notExposed.join(', ') || 'none'})`, method: 'structure', verified: false };
}
return { detail: `${label} — structural problems: ${check.problems.join('; ')}`, method: 'structure', verified: false };
}
rethrow(error, doing) {
if (error instanceof AuthError) {
this.error(`Authentication failed (HTTP ${error.status}). Your key may be revoked — run ${chalk.bold('tork login')} again.`, { exit: 1 });
}
if (error instanceof NetworkError) {
this.error(`Network error: ${error.message}`, { exit: 1 });
}
if (error instanceof ApiError) {
this.error(`Could not ${doing} (HTTP ${error.status}): ${error.bodyExcerpt || 'no response body'}`, { exit: 1 });
}
throw error;
}
}
import { Command } from '@oclif/core';
import { type UsageSummary } from '../lib/usage.js';
export default class Usage extends Command {
static description: string;
static enableJsonFlag: boolean;
static examples: string[];
run(): Promise<{
summary: UsageSummary;
raw: Record<string, unknown>;
}>;
}
import { Command } from '@oclif/core';
import chalk from 'chalk';
import { ApiError, AuthError, NetworkError, TorkApi } from '../lib/api.js';
import { getApiKey, getBaseUrl } from '../lib/config.js';
import { ARROW, CROSS } from '../lib/output.js';
import { CRITICAL_THRESHOLD, formatBar, normalizeUsage, WARN_THRESHOLD } from '../lib/usage.js';
export default class Usage extends Command {
static description = 'Show plan usage for the authenticated org (calls used / limit / remaining)';
static enableJsonFlag = true;
static examples = ['<%= config.bin %> usage', '<%= config.bin %> usage --json'];
async run() {
const key = getApiKey();
if (key === undefined) {
this.error(`Not authenticated. Run ${chalk.bold('tork login')} first (or set TORK_API_KEY).`, { exit: 1 });
}
const api = new TorkApi(getBaseUrl(), key);
let raw;
try {
raw = await api.usage();
}
catch (error) {
if (error instanceof AuthError) {
this.error(`Authentication failed (HTTP ${error.status}). Your key may be revoked — run ${chalk.bold('tork login')} again.`, { exit: 1 });
}
if (error instanceof ApiError && error.status === 404) {
this.error('Plan usage requires server support — /api/v1/usage was not found on this server. Coming soon.', { exit: 1 });
}
if (error instanceof NetworkError) {
this.error(`Network error: ${error.message}`, { exit: 1 });
}
if (error instanceof ApiError) {
this.error(`Could not fetch usage (HTTP ${error.status}): ${error.bodyExcerpt || 'no response body'}`, { exit: 1 });
}
throw error;
}
const summary = normalizeUsage(raw);
if (!this.jsonEnabled()) {
if (summary.planName !== undefined)
this.log(`plan: ${chalk.bold(summary.planName)}`);
if (summary.callsUsed !== undefined && summary.callsLimit !== undefined) {
const percent = summary.percentUsed ?? 0;
this.log(`usage: ${formatBar(percent)} ${summary.callsUsed} / ${summary.callsLimit} (${percent.toFixed(1)}%)`);
}
else {
this.log(`usage: ${chalk.dim('not reported — see --json for the raw response')}`);
}
if (summary.callsRemaining !== undefined)
this.log(`remaining: ${summary.callsRemaining}`);
if (summary.periodStart !== undefined || summary.periodEnd !== undefined) {
const start = summary.periodStart ?? '—';
this.log(`period: ${summary.periodEnd === undefined ? `started ${start}` : `${start} → ${summary.periodEnd}`}`);
}
const percent = summary.percentUsed;
if (percent !== undefined && percent >= CRITICAL_THRESHOLD) {
this.log(`${CROSS} ${chalk.red(`Plan limit reached (${percent.toFixed(1)}%) — governance calls may be rejected. Upgrade at https://tork.network.`)}`);
}
else if (percent !== undefined && percent >= WARN_THRESHOLD) {
this.log(`${ARROW} ${chalk.yellow(`Usage at ${percent.toFixed(1)}% of the plan limit.`)}`);
}
}
return { raw, summary };
}
}
export declare class OpError extends Error {
name: string;
}
/**
* Resolves a 1Password secret reference (op://Vault/Item/field) via `op read`.
* The reference is passed as a single argv element — never through a shell —
* and the resolved value is returned to the caller without being logged.
*/
export declare function resolveOpReference(ref: string): string;
import { spawnSync } from 'node:child_process';
export class OpError extends Error {
name = 'OpError';
}
/**
* Resolves a 1Password secret reference (op://Vault/Item/field) via `op read`.
* The reference is passed as a single argv element — never through a shell —
* and the resolved value is returned to the caller without being logged.
*/
export function resolveOpReference(ref) {
if (!ref.startsWith('op://')) {
throw new OpError(`Invalid 1Password reference "${ref}" — it must start with "op://" (e.g. op://Vault/Item/field).`);
}
const result = spawnSync('op', ['read', ref], { encoding: 'utf8' });
if (result.error !== undefined) {
if (result.error.code === 'ENOENT') {
throw new OpError('1Password CLI (`op`) is not installed. Install it with `brew install 1password-cli` and sign in with `op signin`.');
}
throw new OpError(`Could not run \`op read\`: ${result.error.message}`);
}
if (result.status !== 0) {
const stderr = (result.stderr ?? '').trim();
throw new OpError(`\`op read ${ref}\` failed (exit ${result.status})${stderr === '' ? '' : `: ${stderr}`}`);
}
const value = (result.stdout ?? '').trim();
if (value === '') {
throw new OpError(`\`op read ${ref}\` returned an empty value.`);
}
return value;
}
import type { ReceiptSummary } from './api.js';
/** Last underscore-separated segment of a receipt id (the unique hash), prefixed with "…". */
export declare function shortReceiptId(id: string): string;
export declare function colorAction(action: string | undefined): string;
/** Uniform UTC "YYYY-MM-DD HH:MM:SS" regardless of the offset the API sends. */
export declare function formatTimestamp(iso: string | undefined): string;
export declare function formatLogRow(receipt: ReceiptSummary): string;
export declare function logHeader(): string;
/**
* Returns receipts not yet in `seen` in chronological order (the API lists
* newest first) and records them as seen. Used by `tork logs --follow`.
*/
export declare function selectNew(receipts: ReceiptSummary[], seen: Set<string>): ReceiptSummary[];
export interface StructureCheck {
ok: boolean;
problems: string[];
/** Integrity fields the receipts API did not return (absence ≠ failure). */
notExposed: string[];
}
/**
* Local structural sanity check for a fetched receipt. This is NOT
* cryptographic verification — callers must label it accordingly.
*/
export declare function checkReceiptStructure(receipt: Record<string, unknown>): StructureCheck;
import chalk from 'chalk';
const HEX_64 = /^[\da-f]{64}$/i;
const RECEIPT_ID_PATTERN = /^tork_rcpt_/;
const SHORT_ID_TAIL = 16;
/** Last underscore-separated segment of a receipt id (the unique hash), prefixed with "…". */
export function shortReceiptId(id) {
if (id.length <= SHORT_ID_TAIL + 2)
return id;
const tail = id.includes('_') ? id.slice(id.lastIndexOf('_') + 1) : id.slice(-SHORT_ID_TAIL);
return `…${tail}`;
}
export function colorAction(action) {
if (action === undefined || action === '')
return chalk.dim('—');
const lower = action.toLowerCase();
if (lower === 'allow')
return chalk.green(action);
if (lower === 'redact')
return chalk.yellow(action);
if (lower === 'deny' || lower === 'block')
return chalk.red(action);
if (lower === 'escalate')
return chalk.magenta(action);
return action;
}
/** Uniform UTC "YYYY-MM-DD HH:MM:SS" regardless of the offset the API sends. */
export function formatTimestamp(iso) {
if (iso === undefined || iso === '')
return '—';
const date = new Date(iso);
if (Number.isNaN(date.getTime()))
return iso;
return date.toISOString().replace('T', ' ').slice(0, 19);
}
export function formatLogRow(receipt) {
const time = formatTimestamp(receipt.timestamp).padEnd(19);
const action = receipt.action ?? '';
const actionCell = colorAction(receipt.action) + ' '.repeat(Math.max(0, 9 - (action === '' ? 1 : action.length)));
const pii = String(receipt.pii_count ?? 0).padStart(3);
const latency = `${String(receipt.latency_ms ?? '—').padStart(6)} ms`;
const id = receipt.receipt_id === undefined ? chalk.dim('—') : chalk.dim(shortReceiptId(receipt.receipt_id));
return `${time} ${actionCell} ${pii} ${latency} ${id}`;
}
export function logHeader() {
return chalk.dim(`${'TIME'.padEnd(19)} ${'ACTION'.padEnd(9)} PII ${'LATENCY'.padStart(9)} RECEIPT`);
}
/**
* Returns receipts not yet in `seen` in chronological order (the API lists
* newest first) and records them as seen. Used by `tork logs --follow`.
*/
export function selectNew(receipts, seen) {
const fresh = [];
for (const receipt of receipts) {
const id = receipt.receipt_id;
if (id === undefined || seen.has(id))
continue;
seen.add(id);
fresh.push(receipt);
}
return fresh.reverse();
}
/**
* Local structural sanity check for a fetched receipt. This is NOT
* cryptographic verification — callers must label it accordingly.
*/
export function checkReceiptStructure(receipt) {
const problems = [];
const notExposed = [];
const id = receipt.receipt_id;
if (typeof id !== 'string' || id === '')
problems.push('receipt_id missing');
else if (!RECEIPT_ID_PATTERN.test(id))
problems.push(`receipt_id "${id}" does not match the tork_rcpt_ format`);
const timestamp = receipt.timestamp;
if (typeof timestamp !== 'string' || Number.isNaN(new Date(timestamp).getTime())) {
problems.push('timestamp missing or unparseable');
}
for (const field of ['content_hash', 'hmac_signature']) {
const value = receipt[field];
if (value === undefined)
notExposed.push(field);
else if (typeof value !== 'string' || !HEX_64.test(value))
problems.push(`${field} is not a 64-char hex digest`);
}
const fingerprint = receipt.fingerprint;
if (fingerprint === undefined)
notExposed.push('fingerprint');
else if (typeof fingerprint !== 'string' || fingerprint === '')
problems.push('fingerprint is not a string');
return { notExposed, ok: problems.length === 0, problems };
}
export declare const WARN_THRESHOLD = 80;
export declare const CRITICAL_THRESHOLD = 100;
export interface UsageSummary {
planName?: string;
callsUsed?: number;
callsLimit?: number;
callsRemaining?: number;
percentUsed?: number;
periodStart?: string;
periodEnd?: string;
}
/** Normalizes the /api/v1/usage response (top-level counters + plan + current_period). */
export declare function normalizeUsage(raw: Record<string, unknown>): UsageSummary;
/** Proportional bar, colored by how close usage is to the limit. */
export declare function formatBar(percentUsed: number, width?: number): string;
import chalk from 'chalk';
const BAR_WIDTH = 24;
export const WARN_THRESHOLD = 80;
export const CRITICAL_THRESHOLD = 100;
function asNumber(value) {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function asString(value) {
return typeof value === 'string' && value !== '' ? value : undefined;
}
/** Normalizes the /api/v1/usage response (top-level counters + plan + current_period). */
export function normalizeUsage(raw) {
const plan = typeof raw.plan === 'object' && raw.plan !== null ? raw.plan : {};
const period = typeof raw.current_period === 'object' && raw.current_period !== null ? raw.current_period : {};
const callsUsed = asNumber(raw.calls_used) ?? asNumber(period.calls_used);
const callsLimit = asNumber(raw.calls_limit) ?? asNumber(period.calls_limit);
const callsRemaining = asNumber(raw.calls_remaining) ?? asNumber(period.calls_remaining) ?? (callsUsed !== undefined && callsLimit !== undefined ? Math.max(0, callsLimit - callsUsed) : undefined);
let percentUsed = asNumber(period.percentage_used);
if (percentUsed === undefined && callsUsed !== undefined && callsLimit !== undefined && callsLimit > 0) {
percentUsed = (callsUsed / callsLimit) * 100;
}
return {
callsLimit,
callsRemaining,
callsUsed,
percentUsed,
periodEnd: asString(period.end) ?? asString(period.period_end) ?? asString(period.ends_at),
periodStart: asString(period.start),
planName: asString(plan.name),
};
}
/** Proportional bar, colored by how close usage is to the limit. */
export function formatBar(percentUsed, width = BAR_WIDTH) {
const clamped = Math.max(0, Math.min(100, percentUsed));
const filled = Math.round((clamped / 100) * width);
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
if (percentUsed >= CRITICAL_THRESHOLD)
return chalk.red(bar);
if (percentUsed >= WARN_THRESHOLD)
return chalk.yellow(bar);
return chalk.green(bar);
}
+1
-0

@@ -8,2 +8,3 @@ import { Command } from '@oclif/core';

key: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
op: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
};

@@ -10,0 +11,0 @@ run(): Promise<{

@@ -6,2 +6,3 @@ import { password } from '@inquirer/prompts';

import { getBaseUrl, isValidKeyFormat, maskKey, readConfig, writeConfig } from '../lib/config.js';
import { OpError, resolveOpReference } from '../lib/op.js';
import { CHECK } from '../lib/output.js';

@@ -11,9 +12,31 @@ export default class Login extends Command {

static enableJsonFlag = true;
static examples = ['<%= config.bin %> login', '<%= config.bin %> login --key tork_live_xxx'];
static examples = [
'<%= config.bin %> login',
'<%= config.bin %> login --key tork_live_xxx',
"<%= config.bin %> login --op 'op://Tork/API key/credential'",
];
static flags = {
key: Flags.string({ description: 'API key (skips the interactive prompt, for non-interactive use)' }),
op: Flags.string({ description: 'read the key from 1Password via `op read` (e.g. op://Vault/Item/field)', exclusive: ['key'] }),
};
async run() {
const { flags } = await this.parse(Login);
const key = flags.key ?? (await password({ mask: '*', message: 'Paste your Tork API key' }));
let key;
if (flags.op !== undefined) {
try {
key = resolveOpReference(flags.op);
}
catch (error) {
if (error instanceof OpError)
this.error(error.message, { exit: 1 });
throw error;
}
if (!isValidKeyFormat(key)) {
// the resolved secret must never be echoed — name only the reference
this.error(`The value at ${flags.op} does not look like a Tork API key (expected it to start with "tork_"). Check the field the reference points to.`, { exit: 1 });
}
}
else {
key = flags.key ?? (await password({ mask: '*', message: 'Paste your Tork API key' }));
}
if (!isValidKeyFormat(key)) {

@@ -20,0 +43,0 @@ this.error('Invalid key format: Tork API keys start with "tork_".', { exit: 1 });

@@ -8,2 +8,4 @@ import { Command } from '@oclif/core';

configPath: string;
verified: boolean;
verifyError: string | null;
}

@@ -10,0 +12,0 @@ export default class Whoami extends Command {

import { Command } from '@oclif/core';
import chalk from 'chalk';
import { TorkApi } from '../lib/api.js';
import { ApiError, AuthError, NetworkError, TorkApi } from '../lib/api.js';
import { configPath, getApiKey, getBaseUrl, maskKey, readConfig } from '../lib/config.js';
export default class Whoami extends Command {
static description = 'Show the active key (masked), org, base URL and config path';
static description = 'Show the active key (masked), org (live-verified), base URL and config path';
static enableJsonFlag = true;

@@ -13,15 +13,37 @@ static examples = ['<%= config.bin %> whoami', '<%= config.bin %> whoami --json'];

const source = process.env.TORK_API_KEY ? 'env TORK_API_KEY' : 'config file';
// Verify against the live API — cached org names misled a real incident
// triage, so a value we could not verify is always labeled as cached.
let orgName = readConfig().orgName;
let verified = false;
let verifyError = null;
if (key !== undefined) {
try {
const { orgName: liveOrg } = await new TorkApi(baseUrl, key).validateKey();
verified = true;
if (liveOrg !== undefined)
orgName = liveOrg;
}
catch {
// offline or invalid key — fall back to the stored org name
catch (error) {
if (error instanceof AuthError) {
verifyError = `server rejected the key (HTTP ${error.status})`;
process.exitCode = 1;
}
else if (error instanceof NetworkError) {
verifyError = 'network unreachable';
}
else if (error instanceof ApiError) {
verifyError = `server error (HTTP ${error.status})`;
}
else {
verifyError = error instanceof Error ? error.message : String(error);
}
}
}
let orgSuffix = '';
if (verified)
orgSuffix = ` ${chalk.dim('(verified just now)')}`;
else if (verifyError !== null)
orgSuffix = ` ${chalk.yellow(`(cached — could not verify: ${verifyError})`)}`;
this.log(`key: ${key === undefined ? chalk.dim('not set — run tork login') : `${chalk.bold(maskKey(key))} (${source})`}`);
this.log(`org: ${orgName ?? chalk.dim('—')}`);
this.log(`org: ${orgName ?? chalk.dim('—')}${orgSuffix}`);
this.log(`base URL: ${baseUrl}`);

@@ -35,4 +57,6 @@ this.log(`config: ${configPath()}`);

source: key === undefined ? null : source,
verified,
verifyError,
};
}
}

@@ -16,2 +16,31 @@ export interface GovernPayload {

export type GovernResponse = Record<string, unknown>;
export interface ReceiptSummary {
receipt_id?: string;
timestamp?: string;
action?: string;
pii_types?: string[];
pii_count?: number;
policy_version?: string;
latency_ms?: number;
[key: string]: unknown;
}
export interface ReceiptsPage {
receipts: ReceiptSummary[];
total?: number;
limit?: number;
offset?: number;
[key: string]: unknown;
}
export interface VerifyRequest {
fingerprint: string;
date: string;
org_id: string;
}
export interface VerifyResponse {
verified?: boolean;
message?: string;
merkle_root?: string | null;
anchor?: unknown;
[key: string]: unknown;
}
export declare class NetworkError extends Error {

@@ -48,3 +77,7 @@ name: string;

usage(): Promise<Record<string, unknown>>;
/** The receipts feed doubles as the decision log: /api/v1/logs does not exist on the server. */
receipts(limit?: number, offset?: number): Promise<ReceiptsPage>;
receipt(id: string): Promise<Record<string, unknown>>;
verify(payload: VerifyRequest): Promise<VerifyResponse>;
private request;
}

@@ -116,2 +116,20 @@ const TIMEOUT_MS = 10_000;

}
/** The receipts feed doubles as the decision log: /api/v1/logs does not exist on the server. */
async receipts(limit, offset) {
const params = new URLSearchParams();
if (limit !== undefined)
params.set('limit', String(limit));
if (offset !== undefined)
params.set('offset', String(offset));
const query = params.size > 0 ? `?${params.toString()}` : '';
const raw = await this.request({ path: `/api/v1/receipts${query}` });
const receipts = Array.isArray(raw.receipts) ? raw.receipts : [];
return { ...raw, receipts };
}
async receipt(id) {
return this.request({ path: `/api/v1/receipts/${encodeURIComponent(id)}` });
}
async verify(payload) {
return this.request({ method: 'POST', path: '/api/v1/verify', body: payload });
}
async request(options) {

@@ -118,0 +136,0 @@ const url = new URL(options.path, this.baseUrl).toString();

+1
-1
{
"name": "@torknetwork/cli",
"version": "0.1.1",
"version": "0.2.0",
"description": "Tork Governance CLI — govern your AI agents from the terminal",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -44,2 +44,3 @@ # Tork CLI

| `--key <key>` | Provide the key non-interactively (CI, scripts) |
| `--op <ref>` | Read the key from 1Password via `op read` (e.g. `'op://Vault/Item/field'`) — requires the [1Password CLI](https://developer.1password.com/docs/cli/); the resolved key is validated like `--key` and never printed |
| `--json` | Machine-readable result |

@@ -78,2 +79,36 @@

### `tork logs`
Show recent governance decisions for your org — time, action (`allow` green, `redact` yellow, `deny` red, `escalate` magenta), PII count, latency and receipt id. Backed by the receipts feed (`GET /api/v1/receipts`); entries print oldest-first so the latest decision is at the bottom.
| Flag | Description |
| --- | --- |
| `--limit <n>` | Number of decisions to show (default 20) |
| `--follow` | Poll every 5s and print new decisions as they arrive (Ctrl+C to stop; not combinable with `--json`) |
| `--json` | Machine-readable entries |
### `tork receipts`
List governance receipts, or fetch one by id and verify it:
```sh
tork receipts # list recent receipts
tork receipts tork_rcpt_xxx # fetch one receipt and verify it
```
Single-receipt view shows the receipt id, timestamp, action, PII counts, policy version, content hash, fingerprint, governance DNA and whether a signature is present. Verification is honest about what it can do: when the receipt exposes a fingerprint it is checked against the server's merkle anchor via `POST /api/v1/verify` (`✔ verified` / `✖ failed`, exit 1 on failure); when it doesn't, you get a local **structure check only** — field presence and hash-format sanity, never claimed as cryptographic verification.
| Flag | Description |
| --- | --- |
| `--limit <n>` | Number of receipts to list (default 20) |
| `--json` | Full receipt / verification result as JSON |
### `tork usage`
Show plan usage for the authenticated org from `GET /api/v1/usage` — plan name, a proportional usage bar, calls used / limit with percentage, calls remaining and the billing period. Warns in yellow at ≥80% of the limit and in red at ≥100%.
| Flag | Description |
| --- | --- |
| `--json` | Raw API response plus the normalized summary |
### `tork doctor`

@@ -100,3 +135,3 @@

Print the active key (masked to `tork_****last4`), organisation (fetched live when possible), base URL, and config path.
Print the active key (masked to `tork_****last4`), organisation, base URL, and config path. The org is always checked live against the API: on success it shows `(verified just now)`; when the server can't be reached the stored value is printed suffixed `(cached — could not verify: <reason>)`; when the server rejects the key the command additionally exits 1, so scripts never mistake a cached identity for a live one.

@@ -141,2 +176,5 @@ ## Environment variables

| Colored output breaks log parsing | ANSI codes in CI logs | Set `NO_COLOR=1` or use `--json` |
| `1Password CLI (op) is not installed` | `--op` used without the op CLI | `brew install 1password-cli`, then `op signin` |
| `op read … failed (exit 1)` | Bad reference or locked vault | Check the `op://Vault/Item/field` path; run `op signin` |
| `org: … (cached — could not verify)` | `tork whoami` couldn't reach the API or the key was rejected | Check connectivity; if HTTP 401, run `tork login` again |

@@ -143,0 +181,0 @@ ## Development