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
import { createHash } from "node:crypto";
import { Context, Hono } from "npm:hono@3";
import { cors } from "npm:hono@3/cors";
const HASH_LENGTH = 32;
const HASH_MAX = 2 ** HASH_LENGTH;
class Flag {
allowlist: Set<string> = new Set();
blocklist: Set<string> = new Set();
static rollout(percent: number): Flag {
return new Flag(percent);
}
static enabled(): Flag {
return new Flag(1);
}
static disabled(): Flag {
return new Flag(0);
}
private constructor(private readonly rollout: number) {
if (rollout < 0 || rollout > 1) {
throw new Error("Rollout must be between 0 and 1");
}
}
allow(userId: string | string[]): Flag {
if (!Array.isArray(userId)) {
userId = [userId];
}
for (const id of userId) {
this.allowlist.add(id);
}
return this;
}
block(userId: string | string[]): Flag {
if (!Array.isArray(userId)) {
userId = [userId];
}
for (const id of userId) {
this.blocklist.add(id);
}
return this;
}
isEnabled(userId: string): boolean {
if (this.allowlist.has(userId)) {
return true;
}
if (this.blocklist.has(userId)) {
return false;
}
const hash = createHash("sha256").update(userId).digest("hex");
const decimalHash = parseInt(hash.substring(0, HASH_LENGTH / 4), 16);
return decimalHash / HASH_MAX < this.rollout;
}
}
const FLAGS: Record<string, Flag> = {
"flag1": Flag.rollout(0.5),
"flag2": Flag.enabled().block("user_123"),
"flag3": Flag.disabled().allow("hello@samwho.dev"),
};
function getUserId(c: Context): string {
return c.req.query("userId")
|| c.req.header("x-forwarded-for")
|| c.req.header("x-real-ip");
}
const app = new Hono();
app.use(
"/*",
cors({
origin: "*",
allowHeaders: ["Content-Type"],
allowMethods: ["GET", "OPTIONS"],
}),
);
app.get("/:id", async (c) => {
const userId = getUserId(c);
if (!userId) {
return c.json({ error: "User ID must be provided" }, 400);
}
const flagId = c.req.param("id");
const flag = FLAGS[flagId];
if (!flag) {
return c.json({ error: `flag "${flagId}" not found` }, 404);
}
return c.json({ enabled: flag.isEnabled(`${flagId}:${userId}`) });
});