cligentic
← back to catalog
cligentic block

session

Auth token persistence. Load on boot, save after login, clear on logout, check expiry. Atomic writes, 0o600 permissions. The companion of every login command.

Install
session demo
NPM dependencies

zero deps — pure TS

Block dependencies
Size

66 LOC

What it does

Persists auth tokens between CLI invocations. Load on boot, save after login, clear on logout, check expiry. Uses atomic writes and 0o600 permissions so tokens aren't world-readable.

Quickstart

import { getAppPaths } from "@/cli/foundation/xdg-paths";
import { loadSession, saveSession, clearSession, isExpired } from "@/cli/foundation/session";
 
type MySession = {
  accessToken: string;
  refreshToken: string;
  createdAt: string;
  expiresAt: string;
};
 
const paths = getAppPaths("myapp");
 
// On boot
const session = loadSession<MySession>(paths.sessions);
if (!session || isExpired(session.expiresAt)) {
  // re-auth flow
}
 
// After login
saveSession(paths.sessions, {
  accessToken: "...",
  refreshToken: "...",
  createdAt: new Date().toISOString(),
  expiresAt: new Date(Date.now() + 3600_000).toISOString(),
});
 
// On logout
clearSession(paths.sessions);

File location

Sessions are stored at {sessionsDir}/current.json with mode 0o600. Uses atomic-write so a crash mid-save never corrupts the token file.

API

loadSession<T>(sessionsDir): T | null
saveSession<T>(sessionsDir, session): void
clearSession(sessionsDir): void
isExpired(expiresAt?): boolean

Pair with

Source

The full file, 66 lines.

This is the exact TypeScript that lands in your project when you run the install command. Read it, copy it, edit it, own it. cligentic never touches it again.

src/cli/foundation/session.ts
// cligentic block: session
//
// Auth token persistence. Load on boot, save after login, clear on logout,
// check expiry. Uses atomic writes and 0o600 permissions.
//
// Usage:
//   import { loadSession, saveSession, clearSession, isExpired } from "./foundation/session";
//
//   const session = loadSession<MySession>(paths.sessions);
//   if (!session || isExpired(session.expiresAt)) {
//     // re-auth
//   }

import { existsSync, readFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { atomicWriteJson } from "./atomic-write";

export type SessionBase = {
  /** ISO 8601 timestamp when the session was created. */
  createdAt: string;
  /** ISO 8601 timestamp when the session expires. Optional. */
  expiresAt?: string;
};

const SESSION_FILE = "current.json";

/**
 * Loads the current session from {sessionsDir}/current.json.
 * Returns null if no session exists or the file is corrupted.
 */
export function loadSession<T extends SessionBase>(sessionsDir: string): T | null {
  const filePath = join(sessionsDir, SESSION_FILE);
  if (!existsSync(filePath)) return null;
  try {
    return JSON.parse(readFileSync(filePath, "utf8")) as T;
  } catch {
    return null;
  }
}

/**
 * Saves a session to {sessionsDir}/current.json with 0o600 permissions.
 * Uses atomic write to prevent corruption.
 */
export function saveSession<T extends SessionBase>(sessionsDir: string, session: T): void {
  atomicWriteJson(join(sessionsDir, SESSION_FILE), session, { mode: 0o600 });
}

/**
 * Deletes the current session file.
 */
export function clearSession(sessionsDir: string): void {
  const filePath = join(sessionsDir, SESSION_FILE);
  if (existsSync(filePath)) {
    unlinkSync(filePath);
  }
}

/**
 * Checks if an ISO 8601 expiry timestamp has passed.
 */
export function isExpired(expiresAt?: string): boolean {
  if (!expiresAt) return false;
  return new Date(expiresAt).getTime() < Date.now();
}