File size: 3,412 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
 * Invite/redeem tokens for server connection.
 *
 * @module lib/gamification/invites
 */

import crypto from "crypto";

/**
 * Generate an invite code (8-char alphanumeric, human-readable).
 */
function generateInviteCode(): string {
  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  let code = "";
  for (let i = 0; i < 8; i++) {
    code += chars[crypto.randomInt(0, chars.length)];
  }
  return code;
}

/**
 * Hash a token for storage (SHA-256).
 */
function hashToken(token: string): string {
  return crypto.createHash("sha256").update(token).digest("hex");
}

/**
 * Create an invite token for server connection.
 * Returns the plaintext code (show to user) and token (for redemption).
 */
export async function createInvite(
  createdByApiKeyId: string,
  serverUrl?: string,
  maxUses: number = 1
): Promise<{ code: string; token: string }> {
  const code = generateInviteCode();
  const token = crypto.randomBytes(32).toString("base64url");
  const tokenHash = hashToken(token);
  const id = crypto.randomUUID();

  const { createInviteToken } = await import("../db/gamification");
  createInviteToken(id, code, tokenHash, createdByApiKeyId, serverUrl, maxUses);

  return { code, token };
}

/**
 * Redeem an invite code. Returns server info if successful.
 */
export async function redeemInvite(
  code: string,
  usedByApiKeyId: string
): Promise<{ success: boolean; serverUrl?: string; error?: string }> {
  const { getInviteByCode, redeemInvite: dbRedeem } = await import("../db/gamification");

  const invite = getInviteByCode(code);
  if (!invite) {
    return { success: false, error: "Invalid invite code" };
  }

  if (invite.revokedAt) {
    return { success: false, error: "Invite has been revoked" };
  }

  if (invite.expiresAt && new Date(invite.expiresAt) < new Date()) {
    return { success: false, error: "Invite has expired" };
  }

  if (invite.useCount >= invite.maxUses) {
    return { success: false, error: "Invite has been fully redeemed" };
  }

  if (invite.createdBy === usedByApiKeyId) {
    return { success: false, error: "Cannot redeem your own invite" };
  }

  const redeemed = dbRedeem(code, usedByApiKeyId);
  if (!redeemed) {
    return { success: false, error: "Failed to redeem invite" };
  }

  return { success: true, serverUrl: invite.serverUrl || undefined };
}

/**
 * List invites created by an API key.
 */
export async function listInvites(apiKeyId: string) {
  const db = (await import("../db/core")).getDbInstance();

  const rows = db
    .prepare(
      `SELECT id, code, server_url, max_uses, use_count, expires_at, revoked_at, created_at
       FROM invite_tokens WHERE created_by = ? ORDER BY created_at DESC`
    )
    .all(apiKeyId) as Array<{
    id: string;
    code: string;
    server_url: string | null;
    max_uses: number;
    use_count: number;
    expires_at: string | null;
    revoked_at: string | null;
    created_at: string;
  }>;

  return rows.map((r) => ({
    id: r.id,
    code: r.code,
    serverUrl: r.server_url,
    maxUses: r.max_uses,
    useCount: r.use_count,
    expiresAt: r.expires_at,
    revokedAt: r.revoked_at,
    createdAt: r.created_at,
  }));
}

/**
 * Revoke an invite token.
 */
export async function revokeInvite(inviteId: string): Promise<boolean> {
  const { revokeInvite: dbRevoke } = await import("../db/gamification");
  dbRevoke(inviteId);
  return true;
}