Source: database/repositories/webhookTokenRepo.js

/**
 * @module database/repositories/webhookTokenRepo
 * @description Data-access layer for the `webhook_tokens` table (ENH-011).
 *
 * Stores per-project CI/CD trigger tokens.  The plaintext token is shown
 * exactly once at creation and never stored — only the SHA-256 hash is
 * persisted.  Authentication checks hash the incoming token and compare.
 *
 * ### Schema
 * ```
 * webhook_tokens(id TEXT PK, projectId TEXT, tokenHash TEXT UNIQUE,
 *                label TEXT, createdAt TEXT, lastUsedAt TEXT)
 * ```
 *
 * ### Exports
 * - {@link create}           — insert a new hashed token row
 * - {@link getByProjectId}   — list all tokens for a project (no hash)
 * - {@link findByHash}       — look up a token by its SHA-256 hash
 * - {@link touch}            — update `lastUsedAt` after a successful trigger
 * - {@link deleteById}       — remove a single token
 * - {@link deleteByProjectId} — remove all tokens for a project (project delete)
 */

import crypto from "crypto";
import { getDatabase } from "../sqlite.js";

// ─── Hashing ──────────────────────────────────────────────────────────────────

/**
 * Hash a plaintext token with SHA-256.
 * @param {string} plaintext
 * @returns {string} 64-char hex digest
 */
export function hashToken(plaintext) {
  return crypto.createHash("sha256").update(plaintext).digest("hex");
}

/**
 * Generate a cryptographically random URL-safe token string.
 * Returns 40 bytes of randomness encoded as hex (80 chars).
 * @returns {string}
 */
export function generateToken() {
  return crypto.randomBytes(40).toString("hex");
}

// ─── @typedef ─────────────────────────────────────────────────────────────────

/**
 * @typedef {Object} WebhookTokenRow
 * @property {string}      id          - Primary key (e.g. "WH-1")
 * @property {string}      projectId
 * @property {string}      label       - Human-readable label (optional)
 * @property {string}      createdAt   - ISO 8601
 * @property {string|null} lastUsedAt  - ISO 8601, null if never used
 */

// ─── Write ────────────────────────────────────────────────────────────────────

/**
 * Insert a new webhook token row.
 * The caller is responsible for generating the ID (use idGenerator) and
 * hashing the plaintext (use {@link hashToken}).
 *
 * @param {Object} opts
 * @param {string} opts.id         - Primary key
 * @param {string} opts.projectId
 * @param {string} opts.tokenHash  - SHA-256 hex of the plaintext token
 * @param {string} [opts.label]    - Optional human label
 * @returns {void}
 */
export function create({ id, projectId, tokenHash, label }) {
  const db = getDatabase();
  db.prepare(
    `INSERT INTO webhook_tokens (id, projectId, tokenHash, label, createdAt)
     VALUES (?, ?, ?, ?, ?)`
  ).run(id, projectId, tokenHash, label || null, new Date().toISOString());
}

// ─── Read ─────────────────────────────────────────────────────────────────────

/**
 * Get all tokens for a project.
 * Returns public fields only — the `tokenHash` is intentionally omitted
 * so it cannot be accidentally logged or sent to the client.
 *
 * @param {string} projectId
 * @returns {WebhookTokenRow[]}
 */
export function getByProjectId(projectId) {
  const db = getDatabase();
  return db.prepare(
    "SELECT id, projectId, label, createdAt, lastUsedAt FROM webhook_tokens WHERE projectId = ? ORDER BY createdAt DESC"
  ).all(projectId);
}

/**
 * Look up a token row by its SHA-256 hash.
 * Used to authenticate incoming trigger requests.
 * Returns the full row including `tokenHash` (needed to verify it still matches).
 *
 * @param {string} hash - 64-char hex SHA-256 digest
 * @returns {{ id: string, projectId: string, tokenHash: string, label: string|null, createdAt: string, lastUsedAt: string|null }|undefined}
 */
export function findByHash(hash) {
  const db = getDatabase();
  return db.prepare(
    "SELECT id, projectId, tokenHash, label, createdAt, lastUsedAt FROM webhook_tokens WHERE tokenHash = ?"
  ).get(hash);
}

// ─── Update ───────────────────────────────────────────────────────────────────

/**
 * Record a successful use of the token (updates `lastUsedAt`).
 * @param {string} id - Token primary key
 * @returns {void}
 */
export function touch(id) {
  const db = getDatabase();
  db.prepare(
    "UPDATE webhook_tokens SET lastUsedAt = ? WHERE id = ?"
  ).run(new Date().toISOString(), id);
}

// ─── Delete ───────────────────────────────────────────────────────────────────

/**
 * Delete a single token by ID.
 * @param {string} id
 * @returns {boolean} `true` if a row was deleted.
 */
export function deleteById(id) {
  const db = getDatabase();
  const info = db.prepare("DELETE FROM webhook_tokens WHERE id = ?").run(id);
  return info.changes > 0;
}

/**
 * Delete all tokens for a project (cascade on project delete).
 * @param {string} projectId
 * @returns {number} Number of rows deleted.
 */
export function deleteByProjectId(projectId) {
  const db = getDatabase();
  const info = db.prepare("DELETE FROM webhook_tokens WHERE projectId = ?").run(projectId);
  return info.changes;
}