+1
-3
.claude/settings.local.json
+1
-3
.claude/settings.local.json
-89
src/rules/handles/constants.example.ts
-89
src/rules/handles/constants.example.ts
···
1
-
import type { Checks } from "../../types.js";
2
-
3
-
/**
4
-
* Example handle check configurations
5
-
*
6
-
* This file demonstrates how to configure handle-based moderation rules.
7
-
* Copy this file to constants.ts and customize for your labeler's needs.
8
-
*
9
-
* Each check can match against handles, display names, and/or descriptions
10
-
* based on the flags you set (description: true, displayName: true).
11
-
*/
12
-
13
-
export const HANDLE_CHECKS: Checks[] = [
14
-
// Example 1: Simple pattern matching with whitelist
15
-
{
16
-
label: "spam-indicator",
17
-
comment: "Handle matches common spam patterns",
18
-
reportAcct: false,
19
-
commentAcct: false,
20
-
toLabel: true,
21
-
check: new RegExp(
22
-
"follow.*?back|gain.*?followers|crypto.*?giveaway|free.*?money",
23
-
"i",
24
-
),
25
-
whitelist: new RegExp("legitimate.*?business", "i"),
26
-
},
27
-
28
-
// Example 2: Check specific domain patterns
29
-
{
30
-
label: "suspicious-domain",
31
-
comment: "Handle uses suspicious domain pattern",
32
-
reportAcct: false,
33
-
commentAcct: false,
34
-
toLabel: true,
35
-
check: new RegExp("(?:suspicious-site\\.example)", "i"),
36
-
},
37
-
38
-
// Example 3: Check with display name and description matching
39
-
{
40
-
label: "potential-impersonator",
41
-
comment: "Account may be impersonating verified entities",
42
-
description: true,
43
-
displayName: true,
44
-
reportAcct: false,
45
-
commentAcct: false,
46
-
toLabel: true,
47
-
check: new RegExp(
48
-
"official.*?support|customer.*?service.*?rep|verified.*?account",
49
-
"i",
50
-
),
51
-
// Exclude accounts that are actually legitimate
52
-
ignoredDIDs: [
53
-
"did:plc:example123", // Real customer support account
54
-
"did:plc:example456", // Verified business account
55
-
],
56
-
},
57
-
58
-
// Example 4: Pattern with specific character variations
59
-
{
60
-
label: "suspicious-pattern",
61
-
comment: "Handle contains suspicious character patterns",
62
-
reportAcct: false,
63
-
commentAcct: false,
64
-
toLabel: true,
65
-
check: new RegExp("[a-z]{2,}[0-9]{6,}|random.*?numbers.*?[0-9]{4,}", "i"),
66
-
whitelist: new RegExp("year[0-9]{4}", "i"),
67
-
ignoredDIDs: [
68
-
"did:plc:example789", // Legitimate account with number pattern
69
-
],
70
-
},
71
-
72
-
// Example 5: Brand protection
73
-
{
74
-
label: "brand-impersonation",
75
-
comment: "Potential brand impersonation detected",
76
-
reportAcct: false,
77
-
commentAcct: false,
78
-
toLabel: true,
79
-
check: new RegExp("example-?brand|cool-?company|awesome-?corp", "i"),
80
-
whitelist: new RegExp(
81
-
"anti-example-brand|not-cool-company|parody.*awesome-corp",
82
-
"i",
83
-
),
84
-
ignoredDIDs: [
85
-
"did:plc:exampleabc", // Official brand account
86
-
"did:plc:exampledef", // Authorized partner
87
-
],
88
-
},
89
-
];
-31
src/rules/posts/constants.example.ts
-31
src/rules/posts/constants.example.ts
···
1
-
import type { Checks } from "../../types.js";
2
-
3
-
export const LINK_SHORTENER = /bit\.ly|tinyurl\.com|ow\.ly/i;
4
-
5
-
export const POST_CHECKS: Checks[] = [
6
-
// Example 1: Spam detection
7
-
{
8
-
label: "spam",
9
-
comment: "Post contains spam indicators",
10
-
reportPost: true,
11
-
reportAcct: false,
12
-
commentAcct: false,
13
-
toLabel: true,
14
-
check: new RegExp(
15
-
"click.*?here|limited.*?time.*?offer|act.*?now|100%.*?free",
16
-
"i",
17
-
),
18
-
whitelist: new RegExp("legitimate.*?offer", "i"),
19
-
},
20
-
21
-
// Example 2: Promotional content
22
-
{
23
-
label: "promotional",
24
-
comment: "Promotional content detected",
25
-
reportPost: false,
26
-
reportAcct: false,
27
-
commentAcct: false,
28
-
toLabel: true,
29
-
check: new RegExp("buy.*?now|discount.*?code|promo.*?link", "i"),
30
-
},
31
-
];
-55
src/rules/profiles/constants.example.ts
-55
src/rules/profiles/constants.example.ts
···
1
-
import type { Checks } from "../../types.js";
2
-
3
-
/**
4
-
* Example profile check configurations
5
-
*
6
-
* This file demonstrates how to configure profile moderation rules.
7
-
* Copy this file to constants.ts and customize for your labeler's needs.
8
-
*
9
-
* Profile checks can match against display names and/or descriptions.
10
-
*/
11
-
12
-
export const PROFILE_CHECKS: Checks[] = [
13
-
// Example 1: Suspicious bio patterns
14
-
{
15
-
label: "suspicious-bio",
16
-
comment: "Profile contains suspicious patterns",
17
-
description: true,
18
-
displayName: false,
19
-
reportAcct: false,
20
-
commentAcct: false,
21
-
toLabel: true,
22
-
check: new RegExp(
23
-
"dm.*?for.*?promo|follow.*?for.*?follow|gain.*?followers",
24
-
"i",
25
-
),
26
-
},
27
-
28
-
// Example 2: Display name checks
29
-
{
30
-
label: "impersonation-risk",
31
-
comment: "Display name may indicate impersonation",
32
-
description: false,
33
-
displayName: true,
34
-
reportAcct: false,
35
-
commentAcct: false,
36
-
toLabel: true,
37
-
check: new RegExp("official|verified|admin|support", "i"),
38
-
whitelist: new RegExp("unofficial|parody|fan", "i"),
39
-
ignoredDIDs: [
40
-
"did:plc:example123", // Actual official account
41
-
],
42
-
},
43
-
44
-
// Example 3: Both display name and description
45
-
{
46
-
label: "crypto-spam",
47
-
comment: "Profile suggests crypto spam activity",
48
-
description: true,
49
-
displayName: true,
50
-
reportAcct: false,
51
-
commentAcct: false,
52
-
toLabel: true,
53
-
check: new RegExp("crypto.*?giveaway|nft.*?drop|airdrop", "i"),
54
-
},
55
-
];
+183
src/tests/session.test.ts
+183
src/tests/session.test.ts
···
1
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+
import {
3
+
existsSync,
4
+
mkdirSync,
5
+
rmSync,
6
+
writeFileSync,
7
+
readFileSync,
8
+
unlinkSync,
9
+
chmodSync,
10
+
} from "node:fs";
11
+
import { join } from "node:path";
12
+
import type { SessionData } from "../session.js";
13
+
14
+
const TEST_DIR = join(process.cwd(), ".test-session");
15
+
const TEST_SESSION_PATH = join(TEST_DIR, ".session");
16
+
17
+
// Helper functions that mimic session.ts but use TEST_SESSION_PATH
18
+
function testLoadSession(): SessionData | null {
19
+
try {
20
+
if (!existsSync(TEST_SESSION_PATH)) {
21
+
return null;
22
+
}
23
+
24
+
const data = readFileSync(TEST_SESSION_PATH, "utf-8");
25
+
const session = JSON.parse(data) as SessionData;
26
+
27
+
if (!session.accessJwt || !session.refreshJwt || !session.did) {
28
+
return null;
29
+
}
30
+
31
+
return session;
32
+
} catch (error) {
33
+
return null;
34
+
}
35
+
}
36
+
37
+
function testSaveSession(session: SessionData): void {
38
+
try {
39
+
const data = JSON.stringify(session, null, 2);
40
+
writeFileSync(TEST_SESSION_PATH, data, "utf-8");
41
+
chmodSync(TEST_SESSION_PATH, 0o600);
42
+
} catch (error) {
43
+
// Ignore errors for test
44
+
}
45
+
}
46
+
47
+
function testClearSession(): void {
48
+
try {
49
+
if (existsSync(TEST_SESSION_PATH)) {
50
+
unlinkSync(TEST_SESSION_PATH);
51
+
}
52
+
} catch (error) {
53
+
// Ignore errors for test
54
+
}
55
+
}
56
+
57
+
describe("session", () => {
58
+
beforeEach(() => {
59
+
// Create test directory
60
+
if (!existsSync(TEST_DIR)) {
61
+
mkdirSync(TEST_DIR, { recursive: true });
62
+
}
63
+
});
64
+
65
+
afterEach(() => {
66
+
// Clean up test directory
67
+
if (existsSync(TEST_DIR)) {
68
+
rmSync(TEST_DIR, { recursive: true, force: true });
69
+
}
70
+
});
71
+
72
+
describe("saveSession", () => {
73
+
it("should save session to file with proper permissions", () => {
74
+
const session: SessionData = {
75
+
accessJwt: "access-token",
76
+
refreshJwt: "refresh-token",
77
+
did: "did:plc:test123",
78
+
handle: "test.bsky.social",
79
+
active: true,
80
+
};
81
+
82
+
testSaveSession(session);
83
+
84
+
expect(existsSync(TEST_SESSION_PATH)).toBe(true);
85
+
});
86
+
87
+
it("should save all session fields correctly", () => {
88
+
const session: SessionData = {
89
+
accessJwt: "access-token",
90
+
refreshJwt: "refresh-token",
91
+
did: "did:plc:test123",
92
+
handle: "test.bsky.social",
93
+
email: "test@example.com",
94
+
emailConfirmed: true,
95
+
emailAuthFactor: false,
96
+
active: true,
97
+
status: "active",
98
+
};
99
+
100
+
testSaveSession(session);
101
+
102
+
const loaded = testLoadSession();
103
+
expect(loaded).toEqual(session);
104
+
});
105
+
});
106
+
107
+
describe("loadSession", () => {
108
+
it("should return null if session file does not exist", () => {
109
+
const session = testLoadSession();
110
+
expect(session).toBeNull();
111
+
});
112
+
113
+
it("should load valid session from file", () => {
114
+
const session: SessionData = {
115
+
accessJwt: "access-token",
116
+
refreshJwt: "refresh-token",
117
+
did: "did:plc:test123",
118
+
handle: "test.bsky.social",
119
+
active: true,
120
+
};
121
+
122
+
testSaveSession(session);
123
+
const loaded = testLoadSession();
124
+
125
+
expect(loaded).toEqual(session);
126
+
});
127
+
128
+
it("should return null for corrupted session file", () => {
129
+
writeFileSync(TEST_SESSION_PATH, "{ invalid json", "utf-8");
130
+
131
+
const session = testLoadSession();
132
+
expect(session).toBeNull();
133
+
});
134
+
135
+
it("should return null for session missing required fields", () => {
136
+
writeFileSync(
137
+
TEST_SESSION_PATH,
138
+
JSON.stringify({ accessJwt: "token" }),
139
+
"utf-8"
140
+
);
141
+
142
+
const session = testLoadSession();
143
+
expect(session).toBeNull();
144
+
});
145
+
146
+
it("should return null for session missing did", () => {
147
+
writeFileSync(
148
+
TEST_SESSION_PATH,
149
+
JSON.stringify({
150
+
accessJwt: "access",
151
+
refreshJwt: "refresh",
152
+
handle: "test.bsky.social",
153
+
}),
154
+
"utf-8"
155
+
);
156
+
157
+
const session = testLoadSession();
158
+
expect(session).toBeNull();
159
+
});
160
+
});
161
+
162
+
describe("clearSession", () => {
163
+
it("should remove session file if it exists", () => {
164
+
const session: SessionData = {
165
+
accessJwt: "access-token",
166
+
refreshJwt: "refresh-token",
167
+
did: "did:plc:test123",
168
+
handle: "test.bsky.social",
169
+
active: true,
170
+
};
171
+
172
+
testSaveSession(session);
173
+
expect(existsSync(TEST_SESSION_PATH)).toBe(true);
174
+
175
+
testClearSession();
176
+
expect(existsSync(TEST_SESSION_PATH)).toBe(false);
177
+
});
178
+
179
+
it("should not throw if session file does not exist", () => {
180
+
expect(() => testClearSession()).not.toThrow();
181
+
});
182
+
});
183
+
});