/ opencodehub / src / lib / runner-token-audit.ts
Swadhin Biswas feat(api): add runner, analytics, packages, and security services · 3/6/2026
28f7954
103 lines · 3.0 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import { desc, eq, and } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { getDatabase, schema } from "@/db";
import { generateId } from "@/lib/utils";
import { logger } from "@/lib/logger";

export type RunnerTokenAuditAction = "issued" | "revoked" | "consumed";

const TARGET_TYPE = "runner_registration_token";

function extractClientIp(request: Request): string | null {
  const forwarded = request.headers.get("x-forwarded-for");
  if (forwarded) {
    const first = forwarded.split(",")[0]?.trim();
    if (first) return first;
  }
  return request.headers.get("x-real-ip");
}

export async function logRunnerTokenAuditEvent(options: {
  request: Request;
  repositoryId: string;
  tokenId: string;
  action: RunnerTokenAuditAction;
  userId?: string | null;
  actorType?: string;
  actorId?: string | null;
  data?: Record<string, unknown>;
}): Promise<void> {
  try {
    const db = getDatabase() as NodePgDatabase<typeof schema>;
    await db.insert(schema.auditLogs).values({
      id: generateId("audit"),
      userId: options.userId || null,
      repositoryId: options.repositoryId,
      action: `runner_token_${options.action}`,
      actorType: options.actorType || (options.userId ? "user" : "system"),
      actorId: options.actorId || options.userId || null,
      actorIp: extractClientIp(options.request),
      actorUserAgent: options.request.headers.get("user-agent"),
      targetType: TARGET_TYPE,
      targetId: options.tokenId,
      targetName: options.tokenId,
      data: options.data ? JSON.stringify(options.data) : null,
      createdAt: new Date(),
    });
  } catch (error) {
    logger.warn({ error, tokenId: options.tokenId, action: options.action }, "Failed to log runner token audit event");
  }
}

export async function getRunnerTokenAuditHistory(
  repositoryId: string,
  limit = 50
): Promise<Array<{
  id: string;
  action: string;
  tokenId: string | null;
  actorType: string | null;
  actorId: string | null;
  actorIp: string | null;
  actorUserAgent: string | null;
  data: Record<string, unknown> | null;
  createdAt: Date;
}>> {
  const db = getDatabase() as NodePgDatabase<typeof schema>;
  const rows = await db.query.auditLogs.findMany({
    where: and(
      eq(schema.auditLogs.repositoryId, repositoryId),
      eq(schema.auditLogs.targetType, TARGET_TYPE)
    ),
    orderBy: [desc(schema.auditLogs.createdAt)],
    limit,
  });

  return rows.map((row) => {
    let parsedData: Record<string, unknown> | null = null;
    if (row.data) {
      try {
        const parsed = JSON.parse(row.data);
        if (typeof parsed === "object" && parsed !== null) {
          parsedData = parsed as Record<string, unknown>;
        }
      } catch {
        parsedData = null;
      }
    }

    return {
      id: row.id,
      action: row.action,
      tokenId: row.targetId,
      actorType: row.actorType,
      actorId: row.actorId,
      actorIp: row.actorIp,
      actorUserAgent: row.actorUserAgent,
      data: parsedData,
      createdAt: row.createdAt,
    };
  });
}