File size: 3,926 Bytes
97ee7cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Tests for convex/lib/identitySigning.ts business-invite token helpers.
 *
 * These are pure crypto tests; they do not need the Convex runtime because
 * the helpers have no Convex imports.
 */
import { beforeEach, describe, expect, test } from "vitest";
import {
  signBusinessInviteToken,
  verifyBusinessInviteToken,
} from "../lib/identitySigning";

const TEST_SECRET = "test-business-invite-secret-minimum-32-bytes-long";

describe("business invite token signing/verification", () => {
  beforeEach(() => {
    process.env.DODO_IDENTITY_SIGNING_SECRET = TEST_SECRET;
  });

  test("round-trips a valid token", async () => {
    const grantId = "k57c5e0m1234567890abcdef12345678";
    const token = await signBusinessInviteToken(grantId);
    const ok = await verifyBusinessInviteToken(grantId, token);
    expect(ok).toBe(true);
  });

  test("rejects an expired token", async () => {
    const grantId = "k57c5e0m1234567890abcdef12345679";
    const token = await signBusinessInviteToken(grantId);
    // The token has a fixed 14-day TTL, so tamper the expiry segment to a
    // timestamp in the past to simulate expiration without waiting.
    const parts = token.split(".");
    expect(parts.length).toBe(3);
    parts[1] = String(Date.now() - 1000);
    const expiredToken = parts.join(".");
    const ok = await verifyBusinessInviteToken(grantId, expiredToken);
    expect(ok).toBe(false);
  });

  test("rejects a token verified against a different grantId", async () => {
    const grantId = "k57c5e0m1234567890abcdef12345680";
    const token = await signBusinessInviteToken(grantId);
    const ok = await verifyBusinessInviteToken(
      "k57c5e0m000000000000000000000000",
      token,
    );
    expect(ok).toBe(false);
  });

  test("rejects a tampered expiry timestamp", async () => {
    const grantId = "k57c5e0m1234567890abcdef12345681";
    const token = await signBusinessInviteToken(grantId);
    const parts = token.split(".");
    expect(parts.length).toBe(3);
    parts[1] = String(Date.now() + 86_400_000);
    const tampered = parts.join(".");
    const ok = await verifyBusinessInviteToken(grantId, tampered);
    expect(ok).toBe(false);
  });

  test("rejects a token with a wrong/legacy version", async () => {
    const grantId = "k57c5e0m1234567890abcdef12345682";
    const token = await signBusinessInviteToken(grantId);
    const parts = token.split(".");
    parts[0] = "v0";
    const legacy = parts.join(".");
    const ok = await verifyBusinessInviteToken(grantId, legacy);
    expect(ok).toBe(false);
  });

  test("rejects a malformed token", async () => {
    const grantId = "k57c5e0m1234567890abcdef12345683";
    const ok = await verifyBusinessInviteToken(grantId, "not-a-token");
    expect(ok).toBe(false);
  });

  test("rejects a token signed for a different purpose (domain separation)", async () => {
    const grantId = "k57c5e0m1234567890abcdef12345684";
    const token = await signBusinessInviteToken(grantId);
    // A foreign version label should break verification even if the signature
    // format happens to be parseable.
    const parts = token.split(".");
    parts[0] = "v1-claim";
    const foreign = parts.join(".");
    const ok = await verifyBusinessInviteToken(grantId, foreign);
    expect(ok).toBe(false);
  });

  test("rejects an undefined token", async () => {
    const grantId = "k57c5e0m1234567890abcdef12345685";
    const ok = await verifyBusinessInviteToken(grantId, undefined);
    expect(ok).toBe(false);
  });

  test("throws when signing an empty grantId", async () => {
    await expect(signBusinessInviteToken("")).rejects.toThrow(
      /business invite token requires a non-empty grantId/,
    );
  });

  test("throws when signing a grantId containing the delimiter", async () => {
    await expect(signBusinessInviteToken("grant.with.dots")).rejects.toThrow(
      /business invite grantId must not contain/,
    );
  });
});