@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
591 lines (590 loc) • 23.2 kB
JavaScript
/**
* Peer-sharing grant store.
*
* A grant is one lender-issued, revocable authorization for one borrower. This
* module owns the persisted grant file and the token contract; it deliberately
* knows nothing about admission policy (see `sharePolicy.ts`) or transport.
*
* **Token contract.** A share token looks like `nls_<grantId>_<secret>`. The
* grant id is carried in the token so a lookup is a map hit rather than a scan
* over every grant, and only then is the secret compared — in constant time —
* against `sha256(salt + secret)`. The raw token exists exactly once, in the
* return value of `createShareGrant`; nothing persists it.
*
* **Cross-process freshness.** The CLI mutates this file in one process while
* the proxy reads it in another, so `share pause` has to land in a running
* proxy without a restart. Reads therefore re-stat the file and reload when its
* mtime moves, bounded by a short TTL so the hot path pays one `stat` per
* second at most.
*
* @module proxy/shareGrants
*/
import { createHash, randomBytes } from "node:crypto";
import { readFile, stat } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { AsyncMutex } from "../utils/asyncMutex.js";
import { logger } from "../utils/logger.js";
import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
const GRANTS_FILE = "proxy-grants.json";
/** Token prefix. Distinguishes a share token from a client's own credential. */
export const SHARE_TOKEN_PREFIX = "nls";
/** How long a loaded snapshot is trusted before the file is re-stat'd. */
const RELOAD_TTL_MS = 1_000;
let customGrantsFilePath = null;
let cache = {};
let cacheLoadedAt = 0;
let cacheMtimeMs = -1;
let cacheValid = false;
let cachedPublicUrl;
let cachedNoteSecret;
const mutationMutex = new AsyncMutex();
/** How often in-memory `lastUsedAt` updates are flushed to disk. */
const USAGE_FLUSH_INTERVAL_MS = 60_000;
let lastUsageFlushAt = 0;
/** `lastUsedAt` stamps awaiting a flush, kept across the reload that flush does. */
let pendingUsage = {};
/** Point the store at an explicit file. Used by dev mode and by tests. */
export function initShareGrants(grantsFilePath) {
customGrantsFilePath = grantsFilePath;
cache = {};
cacheLoadedAt = 0;
cacheMtimeMs = -1;
cacheValid = false;
cachedPublicUrl = undefined;
cachedNoteSecret = undefined;
lastUsageFlushAt = 0;
pendingUsage = {};
}
/**
* This node's stable public address.
*
* Recorded once — by `share url`, or by whatever already fronts this proxy — so
* a link can be minted without retyping the domain. Nothing here assumes the
* address came from `proxy expose`; a reverse proxy, a permanent named tunnel
* or a plain DNS record are all the same thing to a borrower.
*/
export async function getNodePublicUrl() {
await ensureLoaded();
return cachedPublicUrl;
}
export async function setNodePublicUrl(url) {
await mutationMutex.runExclusive(async () => {
await ensureLoaded({ force: true });
cachedPublicUrl = url ? url.trim().replace(/\/+$/, "") : undefined;
await persist();
});
}
function getGrantsFilePath() {
return customGrantsFilePath ?? join(homedir(), ".neurolink", GRANTS_FILE);
}
function isShareGrant(value) {
if (!value || typeof value !== "object") {
return false;
}
const candidate = value;
return (typeof candidate.id === "string" &&
typeof candidate.peerLabel === "string" &&
typeof candidate.tokenHash === "string" &&
typeof candidate.tokenSalt === "string" &&
(candidate.level === "live" || candidate.level === "complete") &&
typeof candidate.state === "string" &&
typeof candidate.entitlement === "object" &&
candidate.entitlement !== null &&
typeof candidate.gates === "object" &&
candidate.gates !== null);
}
/**
* Is this error simply "the file is not there yet"?
*
* The distinction is load-bearing. An absent file genuinely is an empty map —
* nothing has been written yet. Every *other* `stat`/read failure (`EACCES`,
* `EIO`, `EMFILE`, a full descriptor table) is a failure to observe the file,
* and answering one with an empty map is how a whole store gets erased: a
* caller passing `force` is about to `persist()` the map back over the real
* contents it just failed to read.
*/
function isMissingFileError(error) {
return error?.code === "ENOENT";
}
/**
* Load the grant file when the cache is cold or the file changed underneath us.
*
* A missing or unparseable file yields an empty set rather than an error: a node
* that has never shared anything is the common case, and a corrupt file must not
* take the proxy's hot path down. Corruption is announced once per load.
*/
async function ensureLoaded(options = {}) {
const now = Date.now();
if (!options.force &&
!options.revalidate &&
cacheValid &&
now - cacheLoadedAt < RELOAD_TTL_MS) {
return;
}
const path = getGrantsFilePath();
let mtimeMs;
try {
mtimeMs = (await stat(path)).mtimeMs;
}
catch (error) {
if (!isMissingFileError(error)) {
// Not "no file" but "could not look" — see `isMissingFileError`. Let it
// out: a mutation must abort rather than persist an empty map over a
// store it never managed to read.
throw error;
}
// Missing file — an empty grant set, not an error.
cache = {};
cacheMtimeMs = -1;
cacheLoadedAt = now;
cacheValid = true;
return;
}
// This is where `revalidate` and `force` part company. `revalidate` wants a
// fresh answer cheaply and stops here when the file has not moved — that is
// the read path, and it must not pay for a parse per request. `force` goes
// on regardless, because mtime is the fast path for a read and not a
// correctness check for a write: several filesystems stamp it at one-second
// granularity, so a write landing in the same second as our last read is
// indistinguishable from no write at all — and every caller passing `force`
// is about to persist the whole map back over whatever it missed.
if (!options.force && cacheValid && mtimeMs === cacheMtimeMs) {
cacheLoadedAt = now;
return;
}
try {
const parsed = JSON.parse(await readFile(path, "utf8"));
const grants = parsed?.grants ?? {};
cachedPublicUrl = parsed?.publicUrl;
cachedNoteSecret = parsed?.noteSecret;
cache = Object.fromEntries(Object.entries(grants).filter((entry) => isShareGrant(entry[1])));
}
catch (error) {
if (options.force) {
// A mutation is about to write the whole map back. Treating a corrupt
// file as empty here would make that write the thing that finishes the
// corruption off — and it would take `publicUrl` and `noteSecret` with
// it, which no grant carries a second copy of. Abort and leave the file.
throw error;
}
logger.always(`[proxy] share grants unreadable, treating as empty: ${error instanceof Error ? error.message : String(error)}`);
cache = {};
cachedPublicUrl = undefined;
cachedNoteSecret = undefined;
}
cacheMtimeMs = mtimeMs;
cacheLoadedAt = now;
cacheValid = true;
}
async function persist() {
const file = {
schemaVersion: 1,
grants: cache,
...(cachedPublicUrl ? { publicUrl: cachedPublicUrl } : {}),
...(cachedNoteSecret ? { noteSecret: cachedNoteSecret } : {}),
};
await writeJsonSnapshotAtomically(getGrantsFilePath(), file);
try {
cacheMtimeMs = (await stat(getGrantsFilePath())).mtimeMs;
}
catch {
// A stat failure only costs one redundant reload on the next read.
cacheMtimeMs = -1;
}
cacheLoadedAt = Date.now();
cacheValid = true;
}
function hashSecret(salt, secret) {
return createHash("sha256").update(`${salt}:${secret}`).digest("hex");
}
/**
* Compare two hex digests without leaking their divergence point via timing.
*
* Hand-rolled rather than `crypto.timingSafeEqual` because the package's
* browser bundle stubs `node:crypto` down to a subset that does not include it,
* and this module is reachable from that build. Both inputs are fixed-length
* SHA-256 hex, so the length check leaks nothing about the secret.
*/
function digestsMatch(left, right) {
if (left.length !== right.length || left.length === 0) {
return false;
}
let difference = 0;
for (let index = 0; index < left.length; index += 1) {
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
}
return difference === 0;
}
/**
* Split a share token into its grant id and secret.
* Returns null for anything that is not one of our tokens — including a client's
* own Anthropic credential, which must never be mistaken for a share token.
*/
export function parseShareToken(token) {
if (!token.startsWith(`${SHARE_TOKEN_PREFIX}_`)) {
return null;
}
// Split on the first two separators only. The secret is base64url, whose
// alphabet includes "_", so a naive three-way split rejects most valid
// tokens — every one that happens to contain an underscore.
const idStart = SHARE_TOKEN_PREFIX.length + 1;
const idEnd = token.indexOf("_", idStart);
if (idEnd <= idStart) {
return null;
}
const grantId = token.slice(idStart, idEnd);
const secret = token.slice(idEnd + 1);
if (!grantId || !secret) {
return null;
}
return { grantId, secret };
}
export function looksLikeShareToken(token) {
return typeof token === "string" && parseShareToken(token) !== null;
}
/** Expire-on-read: a grant past `notAfter` is expired regardless of its state. */
function withDerivedState(grant, now) {
if (grant.state === "active" &&
grant.gates.notAfter !== undefined &&
grant.gates.notAfter <= now) {
return { ...grant, state: "expired" };
}
return grant;
}
export async function listShareGrants() {
await ensureLoaded();
const now = Date.now();
return Object.values(cache)
.map((grant) => withDerivedState(grant, now))
.sort((a, b) => a.createdAt - b.createdAt);
}
export async function getShareGrant(id) {
await ensureLoaded();
const grant = cache[id];
return grant ? withDerivedState(grant, Date.now()) : undefined;
}
/** Look a grant up by peer label (case-insensitive), the CLI's addressing form. */
export async function findShareGrantByPeer(peerLabel) {
const wanted = peerLabel.trim().toLowerCase();
const grants = await listShareGrants();
return (grants.find((grant) => grant.peerLabel.toLowerCase() === wanted && grant.state !== "revoked") ??
grants.find((grant) => grant.id === peerLabel) ??
// A revoked grant is still addressable for management. Excluding it
// outright stranded it in the file: `share delete` could not find it by
// name, and only its id — which nothing prints after issue — would do.
grants.find((grant) => grant.peerLabel.toLowerCase() === wanted));
}
export async function createShareGrant(input) {
return mutationMutex.runExclusive(async () => {
// Force: `persist()` writes the whole map back, so a mutation that ran on a
// TTL-fresh snapshot would resurrect a grant the CLI deleted, or revert a
// pause it set, in the window since this process last read the file.
await ensureLoaded({ force: true });
const now = Date.now();
const id = randomBytes(6).toString("hex");
const secret = randomBytes(32).toString("base64url");
const tokenSalt = randomBytes(16).toString("hex");
const grant = {
schemaVersion: 1,
id,
peerLabel: input.peerLabel.trim(),
tokenHash: hashSecret(tokenSalt, secret),
tokenSalt,
// Keyed separately from the token so rotating the token does not
// invalidate every receipt already issued under it.
receiptSecret: randomBytes(32).toString("base64url"),
level: input.level,
state: "active",
entitlement: input.entitlement,
gates: input.gates,
createdAt: now,
updatedAt: now,
...(input.note ? { note: input.note } : {}),
};
cache[id] = grant;
await persist();
return {
grant,
token: `${SHARE_TOKEN_PREFIX}_${id}_${secret}`,
};
});
}
/**
* Replace a grant's token, keeping every control intact.
* Used by `share revoke --rotate` and by any suspected token leak.
*/
export async function rotateShareGrantToken(id) {
return mutationMutex.runExclusive(async () => {
// Force: `persist()` writes the whole map back, so a mutation that ran on a
// TTL-fresh snapshot would resurrect a grant the CLI deleted, or revert a
// pause it set, in the window since this process last read the file.
await ensureLoaded({ force: true });
const grant = cache[id];
if (!grant) {
return undefined;
}
const secret = randomBytes(32).toString("base64url");
const tokenSalt = randomBytes(16).toString("hex");
const updated = {
...grant,
tokenSalt,
tokenHash: hashSecret(tokenSalt, secret),
updatedAt: Date.now(),
};
cache[id] = updated;
await persist();
return {
grant: updated,
token: `${SHARE_TOKEN_PREFIX}_${id}_${secret}`,
};
});
}
export async function setShareGrantState(id, state) {
return mutationMutex.runExclusive(async () => {
// Force: `persist()` writes the whole map back, so a mutation that ran on a
// TTL-fresh snapshot would resurrect a grant the CLI deleted, or revert a
// pause it set, in the window since this process last read the file.
await ensureLoaded({ force: true });
const grant = cache[id];
if (!grant) {
return undefined;
}
const updated = {
...grant,
state,
updatedAt: Date.now(),
};
cache[id] = updated;
await persist();
return updated;
});
}
export async function updateShareGrant(id, patch) {
return mutationMutex.runExclusive(async () => {
// Force: `persist()` writes the whole map back, so a mutation that ran on a
// TTL-fresh snapshot would resurrect a grant the CLI deleted, or revert a
// pause it set, in the window since this process last read the file.
await ensureLoaded({ force: true });
const grant = cache[id];
if (!grant) {
return undefined;
}
const updated = {
...grant,
...(patch.level ? { level: patch.level } : {}),
...(patch.note !== undefined ? { note: patch.note } : {}),
entitlement: patch.entitlement
? { ...grant.entitlement, ...patch.entitlement }
: grant.entitlement,
gates: patch.gates ? { ...grant.gates, ...patch.gates } : grant.gates,
updatedAt: Date.now(),
};
cache[id] = updated;
await persist();
return updated;
});
}
/**
* Attach complete-mode lease material to a grant.
*
* Separate from `updateShareGrant` because these are not policy the operator
* edits — the secret is generated once and the borrower's copy is keyed to it,
* so overwriting it silently would invalidate every lease already in the field.
*/
export async function attachLeaseMaterial(id, leaseSecret, leasePolicy, provisionedAccount) {
return mutationMutex.runExclusive(async () => {
await ensureLoaded({ force: true });
const grant = cache[id];
if (!grant) {
return undefined;
}
const updated = {
...grant,
leaseSecret: grant.leaseSecret ?? leaseSecret,
leasePolicy,
...(provisionedAccount
? { provisionedAccount }
: grant.provisionedAccount
? { provisionedAccount: grant.provisionedAccount }
: {}),
updatedAt: Date.now(),
};
cache[id] = updated;
await persist();
return updated;
});
}
/**
* Subtract settled spend from a metered grant's balance.
*
* Read-modify-write **inside** the store's mutex, because settlement is
* concurrent by nature: two streams finishing together would otherwise both read
* the same balance, and the second write would erase the first one's deduction.
* Returns the new balance, or `undefined` for a grant that is not metered.
*/
export async function debitShareGrantCoins(id, coins) {
return mutationMutex.runExclusive(async () => {
// Force: `persist()` writes the whole map back, so a mutation that ran on a
// TTL-fresh snapshot would resurrect a grant the CLI deleted, or revert a
// pause it set, in the window since this process last read the file.
await ensureLoaded({ force: true });
const grant = cache[id];
if (!grant || grant.entitlement.ledger !== "coins") {
return undefined;
}
const remaining = Math.max(0, (grant.entitlement.coins ?? 0) - coins);
cache[id] = {
...grant,
entitlement: { ...grant.entitlement, coins: remaining },
updatedAt: Date.now(),
};
await persist();
return remaining;
});
}
/**
* Add coins to a metered grant's balance.
*
* The mirror of `debitShareGrantCoins`, and under the same lock for the same
* reason. Used by reciprocal netting, by `share topup`'s successor paths, and by
* redeeming a coin note — all of which can land while requests are settling.
*/
export async function creditShareGrantCoins(id, coins) {
if (!(coins > 0)) {
return undefined;
}
return mutationMutex.runExclusive(async () => {
await ensureLoaded({ force: true });
const grant = cache[id];
if (!grant || grant.entitlement.ledger !== "coins") {
return undefined;
}
const balance = (grant.entitlement.coins ?? 0) + coins;
cache[id] = {
...grant,
entitlement: { ...grant.entitlement, coins: balance },
updatedAt: Date.now(),
};
await persist();
return balance;
});
}
/**
* This node's secret for signing coin notes.
*
* Node-level rather than per-grant: a note may be redeemed by a grant that did
* not exist when it was issued, which is the entire point of a transferable
* one. Minted on first use so a node that never issues a note never has one.
*/
export async function getOrCreateNoteSecret() {
return mutationMutex.runExclusive(async () => {
await ensureLoaded({ force: true });
if (!cachedNoteSecret) {
cachedNoteSecret = randomBytes(32).toString("base64url");
await persist();
}
return cachedNoteSecret;
});
}
/** The note secret, without minting one. */
export async function getNoteSecret() {
await ensureLoaded();
return cachedNoteSecret;
}
export async function deleteShareGrant(id) {
return mutationMutex.runExclusive(async () => {
// Force: `persist()` writes the whole map back, so a mutation that ran on a
// TTL-fresh snapshot would resurrect a grant the CLI deleted, or revert a
// pause it set, in the window since this process last read the file.
await ensureLoaded({ force: true });
if (!cache[id]) {
return false;
}
delete cache[id];
await persist();
return true;
});
}
/**
* Resolve a presented token to its grant.
*
* Returns the grant whatever its state — admission is `sharePolicy`'s decision,
* and a paused grant must still be identifiable so the refusal can say *why*
* rather than "unknown token".
*/
export async function resolveShareToken(token) {
const parsed = parseShareToken(token);
if (!parsed) {
return undefined;
}
await ensureLoaded();
let grant = cache[parsed.grantId];
if (!grant) {
// A token minted moments ago would otherwise read as unknown until the
// snapshot TTL lapsed, so a miss re-stats before concluding anything.
//
// `revalidate`, emphatically not `force`. This runs on the inbound path
// before anything has authenticated, and every unknown token reaches it —
// so a client repeating one invalid token must not be able to buy a
// `readFile` plus a `JSON.parse` per request. Costing one `stat` and
// stopping on an unchanged mtime is the whole point.
await ensureLoaded({ revalidate: true });
grant = cache[parsed.grantId];
}
if (!grant) {
return undefined;
}
if (!digestsMatch(grant.tokenHash, hashSecret(grant.tokenSalt, parsed.secret))) {
return undefined;
}
return withDerivedState(grant, Date.now());
}
/**
* Record that a grant served traffic.
*
* `lastUsedAt` is cosmetic, so it is held in memory and flushed at most once a
* minute. Persisting it per request would put a file write on the hot path and —
* worse — move the file's mtime constantly, forcing every reader to reload the
* snapshot it just loaded.
*/
export function touchShareGrantUsage(id) {
const grant = cache[id];
if (!grant) {
return;
}
const now = Date.now();
// Patch the live map so an in-process read sees it immediately, and remember
// the stamp separately: the flush below reloads the file first, which
// replaces `cache` wholesale and would otherwise drop the patch.
cache[id] = { ...grant, lastUsedAt: now };
pendingUsage[id] = now;
if (now - lastUsageFlushAt < USAGE_FLUSH_INTERVAL_MS) {
return;
}
lastUsageFlushAt = now;
void mutationMutex
.runExclusive(async () => {
// Reload before writing. `persist()` serializes the whole map, so
// flushing a cosmetic timestamp onto a stale snapshot would resurrect a
// grant the CLI deleted in the meantime — the timestamp is not worth
// that. Stamps for grants that are gone are simply dropped.
await ensureLoaded({ force: true });
const stamps = pendingUsage;
pendingUsage = {};
let touched = false;
for (const [grantId, lastUsedAt] of Object.entries(stamps)) {
const current = cache[grantId];
if (current) {
cache[grantId] = { ...current, lastUsedAt };
touched = true;
}
}
if (touched) {
await persist();
}
})
.catch(() => {
// Cosmetic only — never surfaced to the request.
});
}