@solgate/server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/app-CB9x11xM.d.ts +415 -0
- package/dist/chunk-MCKGQKYU.js +15 -0
- package/dist/chunk-MSHXG5G3.js +1358 -0
- package/dist/chunk-MYF3SW4P.js +43 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +12 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +56 -0
- package/dist/lib-HMVXUU3V.js +694 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.js +10 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1358 @@
|
|
|
1
|
+
// src/verifiers/types.ts
|
|
2
|
+
var pass = (key, module, evidence) => ({
|
|
3
|
+
key,
|
|
4
|
+
module,
|
|
5
|
+
passed: true,
|
|
6
|
+
evidence,
|
|
7
|
+
checkedAt: Date.now()
|
|
8
|
+
});
|
|
9
|
+
var fail = (key, module, reason, evidence) => ({
|
|
10
|
+
key,
|
|
11
|
+
module,
|
|
12
|
+
passed: false,
|
|
13
|
+
reason,
|
|
14
|
+
evidence,
|
|
15
|
+
checkedAt: Date.now()
|
|
16
|
+
});
|
|
17
|
+
var VerificationUnavailable = class extends Error {
|
|
18
|
+
code = "verification_unavailable";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/verifiers/onchain.ts
|
|
22
|
+
import { NftOwnershipModule, TokenBalanceModule, WalletSignatureModule } from "@solgate/core";
|
|
23
|
+
async function rpc(url, method, params) {
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "content-type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) throw new Error(`RPC ${method} failed: ${res.status}`);
|
|
30
|
+
const j = await res.json();
|
|
31
|
+
if (j.error) throw new Error(`RPC ${method}: ${j.error.message}`);
|
|
32
|
+
return j.result;
|
|
33
|
+
}
|
|
34
|
+
function toRawAmount(ui, decimals) {
|
|
35
|
+
const [int, frac = ""] = String(ui).split(".");
|
|
36
|
+
const fracPadded = (frac + "0".repeat(decimals)).slice(0, decimals);
|
|
37
|
+
return BigInt(int || "0") * 10n ** BigInt(decimals) + BigInt(fracPadded || "0");
|
|
38
|
+
}
|
|
39
|
+
var walletSignatureVerifier = {
|
|
40
|
+
moduleId: WalletSignatureModule.id,
|
|
41
|
+
async verify({ requirement, entry }) {
|
|
42
|
+
return pass(requirement.key, requirement.module, { wallet: entry.wallet });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var tokenBalanceVerifier = {
|
|
46
|
+
moduleId: TokenBalanceModule.id,
|
|
47
|
+
async verify({ requirement, config, wallet, cfg }) {
|
|
48
|
+
let r;
|
|
49
|
+
try {
|
|
50
|
+
r = await rpc(cfg.solana.rpcUrl, "getTokenAccountsByOwner", [wallet, { mint: config.mint }, { encoding: "jsonParsed" }]);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
throw new VerificationUnavailable(`Could not read token balance: ${e.message}`);
|
|
53
|
+
}
|
|
54
|
+
let total = 0n;
|
|
55
|
+
let decimals = 0;
|
|
56
|
+
for (const acc of r.value) {
|
|
57
|
+
const t = acc.account.data.parsed.info.tokenAmount;
|
|
58
|
+
decimals = t.decimals;
|
|
59
|
+
total += BigInt(t.amount);
|
|
60
|
+
if (!config.includeAllAccounts) break;
|
|
61
|
+
}
|
|
62
|
+
const required = toRawAmount(config.min, decimals);
|
|
63
|
+
const evidence = { mint: config.mint, balanceRaw: total.toString(), decimals, required: config.min, accounts: r.value.length };
|
|
64
|
+
return total >= required ? pass(requirement.key, requirement.module, evidence) : fail(requirement.key, requirement.module, `Need at least ${config.min}, found ${Number(total) / 10 ** decimals}`, evidence);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var nftOwnershipVerifier = {
|
|
68
|
+
moduleId: NftOwnershipModule.id,
|
|
69
|
+
async verify({ requirement, config, wallet, cfg }) {
|
|
70
|
+
const matches = [];
|
|
71
|
+
let page = 1;
|
|
72
|
+
while (page <= 10) {
|
|
73
|
+
let r;
|
|
74
|
+
try {
|
|
75
|
+
r = await rpc(cfg.solana.dasUrl, "getAssetsByOwner", [{ ownerAddress: wallet, page, limit: 1e3, displayOptions: { showCollectionMetadata: false } }]);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
throw new VerificationUnavailable(`Could not read NFT holdings: ${e.message}`);
|
|
78
|
+
}
|
|
79
|
+
for (const a of r.items) {
|
|
80
|
+
if (a.burnt) continue;
|
|
81
|
+
const inCollection = config.collection && a.grouping?.some((g) => g.group_key === "collection" && g.group_value === config.collection && g.verified !== false);
|
|
82
|
+
const inMints = config.mints?.includes(a.id);
|
|
83
|
+
const byCreator = config.creator && a.creators?.some((c) => c.address === config.creator && c.verified);
|
|
84
|
+
if (inCollection || inMints || byCreator) matches.push(a.id);
|
|
85
|
+
}
|
|
86
|
+
if (r.items.length < 1e3) break;
|
|
87
|
+
page++;
|
|
88
|
+
}
|
|
89
|
+
const evidence = { matched: matches.slice(0, 50), count: matches.length, required: config.min };
|
|
90
|
+
return matches.length >= config.min ? pass(requirement.key, requirement.module, evidence) : fail(requirement.key, requirement.module, `Need ${config.min} qualifying NFT(s), found ${matches.length}`, evidence);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/crypto.ts
|
|
95
|
+
import * as ed from "@noble/ed25519";
|
|
96
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
97
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
98
|
+
import { hmac } from "@noble/hashes/hmac";
|
|
99
|
+
import bs58 from "bs58";
|
|
100
|
+
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
|
101
|
+
var enc = new TextEncoder();
|
|
102
|
+
var toHex = (u) => Array.from(u, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
103
|
+
var sha256Hex = (s) => toHex(sha256(enc.encode(s)));
|
|
104
|
+
var hmacHex = (secret, data) => toHex(hmac(sha256, enc.encode(secret), enc.encode(data)));
|
|
105
|
+
function randomId(bytes = 16) {
|
|
106
|
+
const b = new Uint8Array(bytes);
|
|
107
|
+
crypto.getRandomValues(b);
|
|
108
|
+
return toHex(b);
|
|
109
|
+
}
|
|
110
|
+
function base64url(u) {
|
|
111
|
+
const bytes = typeof u === "string" ? enc.encode(u) : u;
|
|
112
|
+
let s = "";
|
|
113
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
114
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
115
|
+
}
|
|
116
|
+
function fromBase64url(s) {
|
|
117
|
+
const b = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
|
|
118
|
+
return Uint8Array.from(b, (c) => c.charCodeAt(0));
|
|
119
|
+
}
|
|
120
|
+
function verifyWalletSignature(wallet, message, signature) {
|
|
121
|
+
try {
|
|
122
|
+
const pub = bs58.decode(wallet);
|
|
123
|
+
let sig;
|
|
124
|
+
try {
|
|
125
|
+
sig = bs58.decode(signature);
|
|
126
|
+
if (sig.length !== 64) throw new Error();
|
|
127
|
+
} catch {
|
|
128
|
+
sig = fromBase64url(signature);
|
|
129
|
+
}
|
|
130
|
+
return ed.verify(sig, enc.encode(message), pub);
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function signSession(secret, p) {
|
|
136
|
+
const body = base64url(JSON.stringify(p));
|
|
137
|
+
return `${body}.${hmacHex(secret, body)}`;
|
|
138
|
+
}
|
|
139
|
+
function verifySession(secret, token) {
|
|
140
|
+
const [body, mac] = token.split(".");
|
|
141
|
+
if (!body || !mac) return null;
|
|
142
|
+
if (!safeEqual(hmacHex(secret, body), mac)) return null;
|
|
143
|
+
try {
|
|
144
|
+
const p = JSON.parse(new TextDecoder().decode(fromBase64url(body)));
|
|
145
|
+
if (p.exp < Date.now()) return null;
|
|
146
|
+
return p;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function pkceVerifier() {
|
|
152
|
+
return base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
153
|
+
}
|
|
154
|
+
function pkceChallenge(verifier) {
|
|
155
|
+
return base64url(sha256(enc.encode(verifier)));
|
|
156
|
+
}
|
|
157
|
+
function safeEqual(a, b) {
|
|
158
|
+
if (a.length !== b.length) return false;
|
|
159
|
+
let r = 0;
|
|
160
|
+
for (let i = 0; i < a.length; i++) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
161
|
+
return r === 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/verifiers/social.ts
|
|
165
|
+
import {
|
|
166
|
+
DiscordVerificationModule,
|
|
167
|
+
SocialTaskModule,
|
|
168
|
+
TelegramVerificationModule,
|
|
169
|
+
XVerificationModule,
|
|
170
|
+
YouTubeVerificationModule
|
|
171
|
+
} from "@solgate/core";
|
|
172
|
+
import { hmac as hmac2 } from "@noble/hashes/hmac";
|
|
173
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
174
|
+
async function claimIdentity(ctx) {
|
|
175
|
+
const { storage, campaign, wallet, input } = ctx;
|
|
176
|
+
const res = await storage.claimSocialLink({
|
|
177
|
+
campaignId: campaign.id,
|
|
178
|
+
provider: input.provider,
|
|
179
|
+
providerUserId: input.id,
|
|
180
|
+
wallet,
|
|
181
|
+
handle: input.handle,
|
|
182
|
+
meta: input.meta,
|
|
183
|
+
createdAt: Date.now()
|
|
184
|
+
});
|
|
185
|
+
return res.ok ? null : `This ${input.provider} account is already linked to another wallet.`;
|
|
186
|
+
}
|
|
187
|
+
var xVerifier = {
|
|
188
|
+
moduleId: XVerificationModule.id,
|
|
189
|
+
async verify(ctx) {
|
|
190
|
+
const { requirement, config, input } = ctx;
|
|
191
|
+
const dup = await claimIdentity(ctx);
|
|
192
|
+
if (dup) return fail(requirement.key, requirement.module, dup);
|
|
193
|
+
const meta = input.meta ?? {};
|
|
194
|
+
if (config.minAccountAgeDays > 0 && meta.created_at) {
|
|
195
|
+
const ageDays = (Date.now() - Date.parse(meta.created_at)) / 864e5;
|
|
196
|
+
if (ageDays < config.minAccountAgeDays)
|
|
197
|
+
return fail(requirement.key, requirement.module, `X account must be at least ${config.minAccountAgeDays} days old`);
|
|
198
|
+
}
|
|
199
|
+
if (config.minFollowers > 0 && (meta.followers ?? 0) < config.minFollowers)
|
|
200
|
+
return fail(requirement.key, requirement.module, `Need at least ${config.minFollowers} followers`);
|
|
201
|
+
if (config.follow && input.accessToken) {
|
|
202
|
+
const ok = await checkXFollow(input.accessToken, input.id, config.follow).catch(() => null);
|
|
203
|
+
if (ok === false) return fail(requirement.key, requirement.module, `Follow @${config.follow} on X, then retry`);
|
|
204
|
+
if (ok === null) (input.meta ??= {}).followCheck = "unavailable";
|
|
205
|
+
}
|
|
206
|
+
return pass(requirement.key, requirement.module, { id: input.id, handle: input.handle, followCheck: input.meta?.followCheck ?? "ok" });
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
async function checkXFollow(token, userId, target) {
|
|
210
|
+
const t = await fetch(`https://api.x.com/2/users/by/username/${encodeURIComponent(target)}`, {
|
|
211
|
+
headers: { authorization: `Bearer ${token}` }
|
|
212
|
+
});
|
|
213
|
+
if (!t.ok) return null;
|
|
214
|
+
const targetId = (await t.json()).data?.id;
|
|
215
|
+
if (!targetId) return null;
|
|
216
|
+
let next;
|
|
217
|
+
for (let i = 0; i < 5; i++) {
|
|
218
|
+
const u = new URL(`https://api.x.com/2/users/${userId}/following`);
|
|
219
|
+
u.searchParams.set("max_results", "1000");
|
|
220
|
+
if (next) u.searchParams.set("pagination_token", next);
|
|
221
|
+
const r = await fetch(u, { headers: { authorization: `Bearer ${token}` } });
|
|
222
|
+
if (r.status === 403 || r.status === 402) return null;
|
|
223
|
+
if (!r.ok) return null;
|
|
224
|
+
const j = await r.json();
|
|
225
|
+
if (j.data?.some((x) => x.id === targetId)) return true;
|
|
226
|
+
next = j.meta?.next_token;
|
|
227
|
+
if (!next) break;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
var discordVerifier = {
|
|
232
|
+
moduleId: DiscordVerificationModule.id,
|
|
233
|
+
async verify(ctx) {
|
|
234
|
+
const { requirement, config, input, cfg } = ctx;
|
|
235
|
+
const dup = await claimIdentity(ctx);
|
|
236
|
+
if (dup) return fail(requirement.key, requirement.module, dup);
|
|
237
|
+
if (config.guildId) {
|
|
238
|
+
const guilds = await fetch("https://discord.com/api/v10/users/@me/guilds", {
|
|
239
|
+
headers: { authorization: `Bearer ${input.accessToken}` }
|
|
240
|
+
}).then((r) => r.ok ? r.json() : []);
|
|
241
|
+
if (!guilds.some((g) => g.id === config.guildId))
|
|
242
|
+
return fail(requirement.key, requirement.module, "Join the Discord server first, then retry");
|
|
243
|
+
if (config.roleIds.length > 0) {
|
|
244
|
+
const bot = cfg.oauth?.discord?.botToken;
|
|
245
|
+
if (!bot) return fail(requirement.key, requirement.module, "Role check not configured (missing bot token)");
|
|
246
|
+
const m = await fetch(`https://discord.com/api/v10/guilds/${config.guildId}/members/${input.id}`, {
|
|
247
|
+
headers: { authorization: `Bot ${bot}` }
|
|
248
|
+
}).then((r) => r.ok ? r.json() : { roles: [] });
|
|
249
|
+
if (!m.roles?.some((r) => config.roleIds.includes(r)))
|
|
250
|
+
return fail(requirement.key, requirement.module, "You don't have the required Discord role");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return pass(requirement.key, requirement.module, { id: input.id, handle: input.handle });
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function verifyTelegramLogin(botToken, data, maxAgeSeconds = 600, clockSkewSeconds = 60) {
|
|
257
|
+
const { hash, ...rest } = data;
|
|
258
|
+
const dataCheck = Object.keys(rest).sort().map((k) => `${k}=${rest[k]}`).join("\n");
|
|
259
|
+
const secretHex = sha256Hex(botToken);
|
|
260
|
+
const secretBytes = Uint8Array.from(secretHex.match(/.{2}/g).map((h) => parseInt(h, 16)));
|
|
261
|
+
const computed = hmacHexBytes(secretBytes, dataCheck);
|
|
262
|
+
if (typeof hash !== "string" || !safeEqual(computed, hash)) return false;
|
|
263
|
+
const now = Date.now() / 1e3;
|
|
264
|
+
const age = now - Number(data.auth_date);
|
|
265
|
+
return Number.isFinite(age) && age >= -clockSkewSeconds && age <= maxAgeSeconds;
|
|
266
|
+
}
|
|
267
|
+
function hmacHexBytes(key, data) {
|
|
268
|
+
return Array.from(hmac2(sha2562, key, new TextEncoder().encode(data)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
269
|
+
}
|
|
270
|
+
var telegramVerifier = {
|
|
271
|
+
moduleId: TelegramVerificationModule.id,
|
|
272
|
+
async verify(ctx) {
|
|
273
|
+
const { requirement, config, input, cfg } = ctx;
|
|
274
|
+
if (!cfg.telegram) return fail(requirement.key, requirement.module, "Telegram not configured on server");
|
|
275
|
+
if (!verifyTelegramLogin(cfg.telegram.botToken, input))
|
|
276
|
+
return fail(requirement.key, requirement.module, "Invalid or expired Telegram login");
|
|
277
|
+
const profile = { provider: "telegram", id: String(input.id), handle: input.username };
|
|
278
|
+
const dup = await claimIdentity({ ...ctx, input: profile });
|
|
279
|
+
if (dup) return fail(requirement.key, requirement.module, dup);
|
|
280
|
+
if (config.chatId) {
|
|
281
|
+
const r = await fetch(
|
|
282
|
+
`https://api.telegram.org/bot${cfg.telegram.botToken}/getChatMember?chat_id=${encodeURIComponent(config.chatId)}&user_id=${input.id}`
|
|
283
|
+
).then((r2) => r2.json());
|
|
284
|
+
const status = r.result?.status;
|
|
285
|
+
if (!r.ok || !status || ["left", "kicked"].includes(status))
|
|
286
|
+
return fail(requirement.key, requirement.module, "Join the Telegram channel/group first, then retry");
|
|
287
|
+
}
|
|
288
|
+
return pass(requirement.key, requirement.module, { id: input.id, handle: input.username });
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
var youtubeVerifier = {
|
|
292
|
+
moduleId: YouTubeVerificationModule.id,
|
|
293
|
+
async verify(ctx) {
|
|
294
|
+
const { requirement, config, input } = ctx;
|
|
295
|
+
const dup = await claimIdentity(ctx);
|
|
296
|
+
if (dup) return fail(requirement.key, requirement.module, dup);
|
|
297
|
+
const u = new URL("https://www.googleapis.com/youtube/v3/subscriptions");
|
|
298
|
+
u.searchParams.set("part", "snippet");
|
|
299
|
+
u.searchParams.set("mine", "true");
|
|
300
|
+
u.searchParams.set("forChannelId", config.channelId);
|
|
301
|
+
const r = await fetch(u, { headers: { authorization: `Bearer ${input.accessToken}` } });
|
|
302
|
+
if (!r.ok) return fail(requirement.key, requirement.module, "Could not read YouTube subscriptions (grant the youtube.readonly scope)");
|
|
303
|
+
const j = await r.json();
|
|
304
|
+
return (j.items?.length ?? 0) > 0 ? pass(requirement.key, requirement.module, { id: input.id, channelId: config.channelId }) : fail(requirement.key, requirement.module, "Subscribe to the channel, then retry");
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var socialTaskVerifier = {
|
|
308
|
+
moduleId: SocialTaskModule.id,
|
|
309
|
+
async verify(ctx) {
|
|
310
|
+
const { requirement, config, input, storage, campaign, wallet } = ctx;
|
|
311
|
+
const key = `task:${campaign.id}:${wallet}:${requirement.key}`;
|
|
312
|
+
if (input.action === "open") {
|
|
313
|
+
await storage.setTemp(key, String(Date.now()), 3600);
|
|
314
|
+
return fail(requirement.key, requirement.module, "opened", { openedAt: Date.now() });
|
|
315
|
+
}
|
|
316
|
+
const opened = Number(await storage.getTemp(key) ?? 0);
|
|
317
|
+
if (!opened) return fail(requirement.key, requirement.module, "Open the link first");
|
|
318
|
+
if ((Date.now() - opened) / 1e3 < config.minDwellSeconds)
|
|
319
|
+
return fail(requirement.key, requirement.module, "That was quick \u2014 give it a moment and try again");
|
|
320
|
+
if (config.requireProofUrl && !/^https?:\/\//.test(input.proofUrl ?? ""))
|
|
321
|
+
return fail(requirement.key, requirement.module, "Paste the link to your post as proof");
|
|
322
|
+
const evidence = { proofUrl: input.proofUrl, pendingApproval: config.requireApproval };
|
|
323
|
+
if (config.requireApproval)
|
|
324
|
+
return { ...fail(requirement.key, requirement.module, "Submitted \u2014 waiting for review", evidence), evidence };
|
|
325
|
+
return pass(requirement.key, requirement.module, evidence);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/verifiers/input.ts
|
|
330
|
+
import { CaptchaModule, QuizModule, ReferralModule, evaluateEntry } from "@solgate/core";
|
|
331
|
+
var quizVerifier = {
|
|
332
|
+
moduleId: QuizModule.id,
|
|
333
|
+
async verify({ requirement, config, input, storage, campaign, wallet }) {
|
|
334
|
+
const attemptsKey = `quiz:${campaign.id}:${wallet}:${requirement.key}`;
|
|
335
|
+
const attempts = await storage.incrTemp(attemptsKey, 86400);
|
|
336
|
+
if (attempts > config.maxAttempts) return fail(requirement.key, requirement.module, "No attempts left today");
|
|
337
|
+
let scored = 0;
|
|
338
|
+
let correct = 0;
|
|
339
|
+
for (const q of config.questions) {
|
|
340
|
+
const a = input.answers?.[q.id];
|
|
341
|
+
if (q.required && (a === void 0 || a === "" || Array.isArray(a) && a.length === 0))
|
|
342
|
+
return fail(requirement.key, requirement.module, `Answer "${q.prompt}"`);
|
|
343
|
+
if (q.answer === void 0) continue;
|
|
344
|
+
scored++;
|
|
345
|
+
if (q.type === "single" && a === q.answer) correct++;
|
|
346
|
+
else if (q.type === "multi" && Array.isArray(a) && Array.isArray(q.answer)) {
|
|
347
|
+
const s = new Set(a);
|
|
348
|
+
if (s.size === q.answer.length && q.answer.every((x) => s.has(x))) correct++;
|
|
349
|
+
} else if (q.type === "text" && typeof a === "string" && typeof q.answer === "string") {
|
|
350
|
+
const re = q.answer.startsWith("/") && q.answer.endsWith("/") ? new RegExp(q.answer.slice(1, -1), "i") : null;
|
|
351
|
+
if (re ? re.test(a.trim()) : a.trim().toLowerCase() === q.answer.trim().toLowerCase()) correct++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const score = scored === 0 ? 1 : correct / scored;
|
|
355
|
+
const evidence = { score, correct, scored, attempt: attempts, answers: input.answers };
|
|
356
|
+
return score >= config.passScore ? pass(requirement.key, requirement.module, evidence) : fail(requirement.key, requirement.module, `Scored ${correct}/${scored}; need ${Math.ceil(config.passScore * scored)}`, evidence);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var referralVerifier = {
|
|
360
|
+
moduleId: ReferralModule.id,
|
|
361
|
+
async verify({ requirement, config, input, storage, campaign, entry }) {
|
|
362
|
+
const code = input.code?.trim().toUpperCase();
|
|
363
|
+
if (!code) {
|
|
364
|
+
return config.requireCode ? fail(requirement.key, requirement.module, "Enter a referral code") : pass(requirement.key, requirement.module, { code: null });
|
|
365
|
+
}
|
|
366
|
+
if (entry.referredBy) return pass(requirement.key, requirement.module, { code, referredBy: entry.referredBy });
|
|
367
|
+
if (code === entry.referralCode) return fail(requirement.key, requirement.module, "You can't refer yourself");
|
|
368
|
+
const referrer = await storage.findEntryByReferralCode(campaign.id, code);
|
|
369
|
+
if (!referrer) return fail(requirement.key, requirement.module, "Unknown referral code");
|
|
370
|
+
const n = await storage.incrTemp(`refcount:${campaign.id}:${referrer.wallet}`, 60 * 60 * 24 * 365);
|
|
371
|
+
if (n <= config.maxReferrals && config.referrerPoints > 0) {
|
|
372
|
+
referrer.bonusPoints = (referrer.bonusPoints ?? 0) + config.referrerPoints;
|
|
373
|
+
referrer.results[`${requirement.key}:credits`] = {
|
|
374
|
+
key: `${requirement.key}:credits`,
|
|
375
|
+
module: "referral-credit",
|
|
376
|
+
passed: true,
|
|
377
|
+
evidence: { referrals: n, bonusPoints: referrer.bonusPoints },
|
|
378
|
+
checkedAt: Date.now()
|
|
379
|
+
};
|
|
380
|
+
await storage.putEntry(evaluateEntry(campaign, referrer));
|
|
381
|
+
}
|
|
382
|
+
entry.referredBy = referrer.wallet;
|
|
383
|
+
return pass(requirement.key, requirement.module, { code, referredBy: referrer.wallet });
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
var endpoints = {
|
|
387
|
+
turnstile: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
|
388
|
+
hcaptcha: "https://api.hcaptcha.com/siteverify",
|
|
389
|
+
recaptcha: "https://www.google.com/recaptcha/api/siteverify"
|
|
390
|
+
};
|
|
391
|
+
var captchaVerifier = {
|
|
392
|
+
moduleId: CaptchaModule.id,
|
|
393
|
+
async verify({ requirement, config, input, cfg, ip }) {
|
|
394
|
+
const secret = config.provider === "turnstile" ? cfg.captcha?.turnstileSecret : config.provider === "hcaptcha" ? cfg.captcha?.hcaptchaSecret : cfg.captcha?.recaptchaSecret;
|
|
395
|
+
if (!secret) return fail(requirement.key, requirement.module, `${config.provider} secret not configured on server`);
|
|
396
|
+
if (!input?.token) return fail(requirement.key, requirement.module, "Complete the CAPTCHA");
|
|
397
|
+
const body = new URLSearchParams({ secret, response: input.token });
|
|
398
|
+
if (ip) body.set("remoteip", ip);
|
|
399
|
+
const r = await fetch(endpoints[config.provider], { method: "POST", body }).then((r2) => r2.json());
|
|
400
|
+
if (!r.success) return fail(requirement.key, requirement.module, "CAPTCHA failed", { errors: r["error-codes"] });
|
|
401
|
+
if (config.provider === "recaptcha" && (r.score ?? 0) < config.minScore)
|
|
402
|
+
return fail(requirement.key, requirement.module, "CAPTCHA score too low", { score: r.score });
|
|
403
|
+
return pass(requirement.key, requirement.module, { provider: config.provider, score: r.score });
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// src/verifiers/index.ts
|
|
408
|
+
var builtinVerifiers = [
|
|
409
|
+
walletSignatureVerifier,
|
|
410
|
+
tokenBalanceVerifier,
|
|
411
|
+
nftOwnershipVerifier,
|
|
412
|
+
xVerifier,
|
|
413
|
+
discordVerifier,
|
|
414
|
+
telegramVerifier,
|
|
415
|
+
youtubeVerifier,
|
|
416
|
+
socialTaskVerifier,
|
|
417
|
+
quizVerifier,
|
|
418
|
+
referralVerifier,
|
|
419
|
+
captchaVerifier
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
// src/webhooks.ts
|
|
423
|
+
var EventBus = class {
|
|
424
|
+
constructor(storage, waitUntil) {
|
|
425
|
+
this.storage = storage;
|
|
426
|
+
this.waitUntil = waitUntil;
|
|
427
|
+
}
|
|
428
|
+
storage;
|
|
429
|
+
waitUntil;
|
|
430
|
+
listeners = [];
|
|
431
|
+
on(fn) {
|
|
432
|
+
this.listeners.push(fn);
|
|
433
|
+
return () => this.listeners = this.listeners.filter((l) => l !== fn);
|
|
434
|
+
}
|
|
435
|
+
async emit(type, campaignId, data, wallet) {
|
|
436
|
+
const event = { id: randomId(12), type, campaignId, wallet, data, createdAt: Date.now() };
|
|
437
|
+
for (const l of this.listeners) await l(event);
|
|
438
|
+
const p = this.dispatch(event);
|
|
439
|
+
this.waitUntil ? this.waitUntil(p) : p.catch(() => {
|
|
440
|
+
});
|
|
441
|
+
return event;
|
|
442
|
+
}
|
|
443
|
+
async dispatch(event) {
|
|
444
|
+
const hooks = (await this.storage.listWebhooks()).filter(
|
|
445
|
+
(h) => h.active && (!h.campaignId || h.campaignId === event.campaignId) && (h.events.includes("*") || h.events.includes(event.type))
|
|
446
|
+
);
|
|
447
|
+
await Promise.all(hooks.map((h) => this.deliver(h.id, h.url, h.secret, event)));
|
|
448
|
+
}
|
|
449
|
+
async deliver(webhookId, url, secret, event, maxAttempts = 3) {
|
|
450
|
+
const body = JSON.stringify(event);
|
|
451
|
+
let status = 0;
|
|
452
|
+
let lastError;
|
|
453
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
454
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
455
|
+
try {
|
|
456
|
+
const r = await fetch(url, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: {
|
|
459
|
+
"content-type": "application/json",
|
|
460
|
+
"x-allowlist-event": event.type,
|
|
461
|
+
"x-allowlist-delivery": event.id,
|
|
462
|
+
"x-allowlist-signature": `t=${ts},v1=${hmacHex(secret, `${ts}.${body}`)}`
|
|
463
|
+
},
|
|
464
|
+
body,
|
|
465
|
+
signal: AbortSignal.timeout(1e4)
|
|
466
|
+
});
|
|
467
|
+
status = r.status;
|
|
468
|
+
if (r.ok) break;
|
|
469
|
+
lastError = `HTTP ${r.status}`;
|
|
470
|
+
} catch (e) {
|
|
471
|
+
lastError = e.message;
|
|
472
|
+
}
|
|
473
|
+
await new Promise((res) => setTimeout(res, 500 * 2 ** attempt));
|
|
474
|
+
if (attempt === maxAttempts) {
|
|
475
|
+
await this.storage.putDelivery({ id: randomId(8), webhookId, eventId: event.id, status, attempts: attempt, lastError, createdAt: Date.now() });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
await this.storage.putDelivery({ id: randomId(8), webhookId, eventId: event.id, status, attempts: 1, lastError, createdAt: Date.now() });
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
function verifyWebhookSignature(secret, header, body, toleranceSeconds = 300) {
|
|
483
|
+
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
|
|
484
|
+
const ts = Number(parts.t);
|
|
485
|
+
if (!ts || Math.abs(Date.now() / 1e3 - ts) > toleranceSeconds) return false;
|
|
486
|
+
return safeEqual(hmacHex(secret, `${ts}.${body}`), parts.v1 ?? "");
|
|
487
|
+
}
|
|
488
|
+
function assertSafeWebhookUrl(raw, opts = {}) {
|
|
489
|
+
let u;
|
|
490
|
+
try {
|
|
491
|
+
u = new URL(raw);
|
|
492
|
+
} catch {
|
|
493
|
+
throw new Error("Invalid URL");
|
|
494
|
+
}
|
|
495
|
+
if (opts.allowInsecure && (u.protocol === "http:" || u.protocol === "https:")) return u;
|
|
496
|
+
if (u.protocol !== "https:") throw new Error("Webhook URL must use https");
|
|
497
|
+
const h = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
498
|
+
if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".internal") || h === "0.0.0.0" || h === "::1" || h === "::") throw new Error("Webhook URL may not target localhost");
|
|
499
|
+
const m = h.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
500
|
+
if (m) {
|
|
501
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
502
|
+
if (a === 10 || a === 127 || a === 0 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127)
|
|
503
|
+
throw new Error("Webhook URL may not target a private network");
|
|
504
|
+
}
|
|
505
|
+
if (/^(fc|fd|fe80)/i.test(h)) throw new Error("Webhook URL may not target a private network");
|
|
506
|
+
return u;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/service.ts
|
|
510
|
+
import {
|
|
511
|
+
ModuleRegistry,
|
|
512
|
+
applyCaps,
|
|
513
|
+
evaluateEntry as evaluateEntry2,
|
|
514
|
+
isCampaignOpen,
|
|
515
|
+
parseCampaign
|
|
516
|
+
} from "@solgate/core";
|
|
517
|
+
import { buildMerkleTree, getMerkleProof } from "@solgate/core";
|
|
518
|
+
var AllowlistError = class extends Error {
|
|
519
|
+
constructor(status, message, code = "error") {
|
|
520
|
+
super(message);
|
|
521
|
+
this.status = status;
|
|
522
|
+
this.code = code;
|
|
523
|
+
}
|
|
524
|
+
status;
|
|
525
|
+
code;
|
|
526
|
+
};
|
|
527
|
+
var AllowlistService = class {
|
|
528
|
+
constructor(cfg, waitUntil) {
|
|
529
|
+
this.cfg = cfg;
|
|
530
|
+
this.registry = cfg.registry ?? new ModuleRegistry();
|
|
531
|
+
for (const v of [...builtinVerifiers, ...cfg.verifiers ?? []]) this.verifiers.set(v.moduleId, v);
|
|
532
|
+
this.events = new EventBus(cfg.storage, waitUntil);
|
|
533
|
+
}
|
|
534
|
+
cfg;
|
|
535
|
+
registry;
|
|
536
|
+
verifiers = /* @__PURE__ */ new Map();
|
|
537
|
+
events;
|
|
538
|
+
/* ---------- campaigns ---------- */
|
|
539
|
+
async getCampaign(id) {
|
|
540
|
+
const c = await this.cfg.storage.getCampaign(id);
|
|
541
|
+
if (!c) throw new AllowlistError(404, "Campaign not found", "campaign_not_found");
|
|
542
|
+
return c;
|
|
543
|
+
}
|
|
544
|
+
async saveCampaign(input) {
|
|
545
|
+
const c = parseCampaign(input, this.registry);
|
|
546
|
+
const prev = await this.cfg.storage.getCampaign(c.id);
|
|
547
|
+
c.createdAt = prev?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
548
|
+
c.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
549
|
+
await this.cfg.storage.putCampaign(c);
|
|
550
|
+
await this.events.emit("campaign.updated", c.id, { name: c.name });
|
|
551
|
+
return c;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Campaign as seen by the browser. Requirement config is PRIVATE BY DEFAULT:
|
|
555
|
+
* a module must opt in via `publicConfig` (on its definition, or on the
|
|
556
|
+
* verifier) to expose anything. Custom modules that forget get `{}`, not
|
|
557
|
+
* their secrets.
|
|
558
|
+
*/
|
|
559
|
+
publicCampaign(c) {
|
|
560
|
+
return {
|
|
561
|
+
...c,
|
|
562
|
+
requirements: c.requirements.map((r) => {
|
|
563
|
+
const v = this.verifiers.get(r.module);
|
|
564
|
+
const def = this.registry.get(r.module);
|
|
565
|
+
const project = def?.publicConfig ?? v?.publicConfig;
|
|
566
|
+
return {
|
|
567
|
+
...r,
|
|
568
|
+
config: project ? project(r.config) ?? {} : {},
|
|
569
|
+
category: def?.category ?? "custom",
|
|
570
|
+
label: r.title ?? def?.label ?? r.module
|
|
571
|
+
};
|
|
572
|
+
})
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/* ---------- entries ---------- */
|
|
576
|
+
async getOrCreateEntry(c, wallet) {
|
|
577
|
+
const s = this.cfg.storage;
|
|
578
|
+
let e = await s.getEntry(c.id, wallet);
|
|
579
|
+
if (e) return e;
|
|
580
|
+
if (!isCampaignOpen(c)) throw new AllowlistError(403, "Registration is closed", "campaign_closed");
|
|
581
|
+
const now = Date.now();
|
|
582
|
+
e = {
|
|
583
|
+
campaignId: c.id,
|
|
584
|
+
wallet,
|
|
585
|
+
results: {},
|
|
586
|
+
eligible: false,
|
|
587
|
+
points: 0,
|
|
588
|
+
allocation: 0,
|
|
589
|
+
referralCode: await this.uniqueReferralCode(c.id),
|
|
590
|
+
createdAt: now,
|
|
591
|
+
updatedAt: now
|
|
592
|
+
};
|
|
593
|
+
for (const r of c.requirements.filter((r2) => r2.module === "wallet-signature")) {
|
|
594
|
+
e.results[r.key] = { key: r.key, module: r.module, passed: true, checkedAt: now, evidence: { wallet } };
|
|
595
|
+
}
|
|
596
|
+
e = evaluateEntry2(c, e, now);
|
|
597
|
+
await s.putEntry(e);
|
|
598
|
+
await this.events.emit("entry.created", c.id, { eligible: e.eligible }, wallet);
|
|
599
|
+
return e;
|
|
600
|
+
}
|
|
601
|
+
async uniqueReferralCode(campaignId) {
|
|
602
|
+
for (let i = 0; i < 5; i++) {
|
|
603
|
+
const code = randomId(4).toUpperCase().slice(0, 8);
|
|
604
|
+
if (!await this.cfg.storage.findEntryByReferralCode(campaignId, code)) return code;
|
|
605
|
+
}
|
|
606
|
+
return randomId(6).toUpperCase();
|
|
607
|
+
}
|
|
608
|
+
/** Run one requirement's verifier and persist the result. */
|
|
609
|
+
async verifyRequirement(c, wallet, key, input, ip) {
|
|
610
|
+
const requirement = c.requirements.find((r) => r.key === key);
|
|
611
|
+
if (!requirement) throw new AllowlistError(404, "Unknown requirement", "requirement_not_found");
|
|
612
|
+
const verifier = this.verifiers.get(requirement.module);
|
|
613
|
+
if (!verifier) throw new AllowlistError(501, `No verifier for module '${requirement.module}'`, "no_verifier");
|
|
614
|
+
if (!isCampaignOpen(c)) throw new AllowlistError(403, "Registration is closed", "campaign_closed");
|
|
615
|
+
const entry = await this.getOrCreateEntry(c, wallet);
|
|
616
|
+
const wasEligible = entry.eligible;
|
|
617
|
+
let result;
|
|
618
|
+
try {
|
|
619
|
+
result = await verifier.verify({ campaign: c, requirement, config: requirement.config, wallet, entry, input, storage: this.cfg.storage, cfg: this.cfg, ip });
|
|
620
|
+
} catch (e) {
|
|
621
|
+
if (e instanceof VerificationUnavailable) throw new AllowlistError(503, e.message, e.code);
|
|
622
|
+
throw e;
|
|
623
|
+
}
|
|
624
|
+
entry.results[key] = result;
|
|
625
|
+
const updated = evaluateEntry2(c, entry);
|
|
626
|
+
await this.cfg.storage.putEntry(updated);
|
|
627
|
+
await this.events.emit(result.passed ? "requirement.passed" : "requirement.failed", c.id, { key, result }, wallet);
|
|
628
|
+
if (updated.eligible !== wasEligible) {
|
|
629
|
+
await this.events.emit(updated.eligible ? "entry.eligible" : "entry.ineligible", c.id, { points: updated.points, allocation: updated.allocation }, wallet);
|
|
630
|
+
} else {
|
|
631
|
+
await this.events.emit("entry.updated", c.id, { key, passed: result.passed }, wallet);
|
|
632
|
+
}
|
|
633
|
+
return { entry: updated, result };
|
|
634
|
+
}
|
|
635
|
+
/** Re-run all recheckable (on-chain) requirements for every entry — e.g. snapshot before mint. */
|
|
636
|
+
async recheckCampaign(c, onlyWallets) {
|
|
637
|
+
const recheckable = c.requirements.filter((r) => this.registry.get(r.module)?.recheckable);
|
|
638
|
+
const entries = onlyWallets ? (await Promise.all(onlyWallets.map((w) => this.cfg.storage.getEntry(c.id, w)))).filter(Boolean) : await this.cfg.storage.listEntries(c.id);
|
|
639
|
+
let changed = 0;
|
|
640
|
+
let unavailable = 0;
|
|
641
|
+
for (const entry of entries) {
|
|
642
|
+
const before = entry.eligible;
|
|
643
|
+
for (const r of recheckable) {
|
|
644
|
+
const v = this.verifiers.get(r.module);
|
|
645
|
+
try {
|
|
646
|
+
entry.results[r.key] = await v.verify({ campaign: c, requirement: r, config: r.config, wallet: entry.wallet, entry, input: {}, storage: this.cfg.storage, cfg: this.cfg });
|
|
647
|
+
} catch (e) {
|
|
648
|
+
if (!(e instanceof VerificationUnavailable)) throw e;
|
|
649
|
+
unavailable++;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const updated = evaluateEntry2(c, entry);
|
|
653
|
+
if (updated.eligible !== before) changed++;
|
|
654
|
+
await this.cfg.storage.putEntry(updated);
|
|
655
|
+
}
|
|
656
|
+
return { checked: entries.length, changed, unavailable };
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Freeze the current eligible set (with caps applied) into an immutable
|
|
660
|
+
* snapshot: root, per-wallet allocation/rank/proof. Serve mint-time lookups
|
|
661
|
+
* from this so the root can't drift while people are minting.
|
|
662
|
+
*/
|
|
663
|
+
async createSnapshot(c) {
|
|
664
|
+
const eligible = (await this.finalEntries(c)).filter((e) => e.eligible);
|
|
665
|
+
const snap = {
|
|
666
|
+
id: randomId(6),
|
|
667
|
+
campaignId: c.id,
|
|
668
|
+
createdAt: Date.now(),
|
|
669
|
+
count: eligible.length,
|
|
670
|
+
totalAllocation: eligible.reduce((s, e) => s + e.allocation, 0),
|
|
671
|
+
entries: Object.fromEntries(eligible.map((e) => [e.wallet, { allocation: e.allocation, rank: e.rank }]))
|
|
672
|
+
};
|
|
673
|
+
if (c.merkle.enabled && eligible.length > 0) {
|
|
674
|
+
const tree = buildMerkleTree(eligible.map((e) => e.wallet), c.merkle.scheme);
|
|
675
|
+
snap.merkle = { scheme: tree.scheme, root: tree.root };
|
|
676
|
+
for (const e of eligible) snap.entries[e.wallet].proof = getMerkleProof(tree, e.wallet);
|
|
677
|
+
}
|
|
678
|
+
await this.cfg.storage.putSnapshot(snap);
|
|
679
|
+
await this.events.emit("campaign.snapshot", c.id, { snapshotId: snap.id, count: snap.count, root: snap.merkle?.root });
|
|
680
|
+
return snap;
|
|
681
|
+
}
|
|
682
|
+
/** Entries with campaign-wide caps applied (ranked FCFS). */
|
|
683
|
+
async finalEntries(c) {
|
|
684
|
+
const entries = await this.cfg.storage.listEntries(c.id);
|
|
685
|
+
return applyCaps(c, entries);
|
|
686
|
+
}
|
|
687
|
+
/** Admin override for a single requirement (approve social task, manual pass/fail). */
|
|
688
|
+
async overrideRequirement(c, wallet, key, passed, note) {
|
|
689
|
+
const entry = await this.cfg.storage.getEntry(c.id, wallet);
|
|
690
|
+
if (!entry) throw new AllowlistError(404, "Entry not found", "entry_not_found");
|
|
691
|
+
const r = c.requirements.find((r2) => r2.key === key);
|
|
692
|
+
if (!r) throw new AllowlistError(404, "Unknown requirement", "requirement_not_found");
|
|
693
|
+
entry.results[key] = { key, module: r.module, passed, checkedAt: Date.now(), evidence: { ...entry.results[key]?.evidence, override: true, note } };
|
|
694
|
+
const updated = evaluateEntry2(c, entry);
|
|
695
|
+
await this.cfg.storage.putEntry(updated);
|
|
696
|
+
await this.events.emit(updated.eligible ? "entry.eligible" : "entry.updated", c.id, { key, passed, override: true }, wallet);
|
|
697
|
+
return updated;
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/app.ts
|
|
702
|
+
import { Hono } from "hono";
|
|
703
|
+
import { cors } from "hono/cors";
|
|
704
|
+
import { z } from "zod";
|
|
705
|
+
import { buildSignInMessage, buildMerkleTree as buildMerkleTree2, getMerkleProof as getMerkleProof2, toCSV, toJSON, toRows, toWalletList } from "@solgate/core";
|
|
706
|
+
|
|
707
|
+
// src/config.ts
|
|
708
|
+
function normalizeConfig(cfg) {
|
|
709
|
+
const url = new URL(cfg.baseUrl);
|
|
710
|
+
return {
|
|
711
|
+
...cfg,
|
|
712
|
+
domain: cfg.domain ?? url.host,
|
|
713
|
+
sessionTtlSeconds: cfg.sessionTtlSeconds ?? 60 * 60 * 24,
|
|
714
|
+
corsOrigins: cfg.corsOrigins ?? "*",
|
|
715
|
+
eligibilityLookup: cfg.eligibilityLookup ?? "minimal",
|
|
716
|
+
trustProxy: cfg.trustProxy ?? "none",
|
|
717
|
+
rateLimit: cfg.rateLimit ?? { windowSeconds: 60, max: 60 },
|
|
718
|
+
solana: { ...cfg.solana, dasUrl: cfg.solana.dasUrl ?? cfg.solana.rpcUrl }
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/routes/oauth.ts
|
|
723
|
+
var providers = {
|
|
724
|
+
x: {
|
|
725
|
+
authUrl: "https://x.com/i/oauth2/authorize",
|
|
726
|
+
tokenUrl: "https://api.x.com/2/oauth2/token",
|
|
727
|
+
scope: "tweet.read users.read follows.read",
|
|
728
|
+
pkce: true,
|
|
729
|
+
basicAuth: true,
|
|
730
|
+
async profile(token) {
|
|
731
|
+
const r = await fetch("https://api.x.com/2/users/me?user.fields=created_at,public_metrics", { headers: { authorization: `Bearer ${token}` } });
|
|
732
|
+
const j = await r.json();
|
|
733
|
+
return { provider: "x", id: j.data.id, handle: j.data.username, accessToken: token, meta: { created_at: j.data.created_at, followers: j.data.public_metrics?.followers_count } };
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
discord: {
|
|
737
|
+
authUrl: "https://discord.com/oauth2/authorize",
|
|
738
|
+
tokenUrl: "https://discord.com/api/v10/oauth2/token",
|
|
739
|
+
scope: "identify guilds",
|
|
740
|
+
pkce: false,
|
|
741
|
+
basicAuth: false,
|
|
742
|
+
async profile(token) {
|
|
743
|
+
const r = await fetch("https://discord.com/api/v10/users/@me", { headers: { authorization: `Bearer ${token}` } });
|
|
744
|
+
const j = await r.json();
|
|
745
|
+
return { provider: "discord", id: j.id, handle: j.username, accessToken: token };
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
google: {
|
|
749
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
750
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
751
|
+
scope: "openid https://www.googleapis.com/auth/youtube.readonly",
|
|
752
|
+
pkce: true,
|
|
753
|
+
basicAuth: false,
|
|
754
|
+
async profile(token) {
|
|
755
|
+
const r = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { authorization: `Bearer ${token}` } });
|
|
756
|
+
const j = await r.json();
|
|
757
|
+
return { provider: "google", id: j.sub, handle: j.email, accessToken: token };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
var moduleForProvider = { x: "x-verify", discord: "discord-verify", google: "youtube-verify" };
|
|
762
|
+
function resolveReturnUrl(raw, cfg) {
|
|
763
|
+
const base = new URL(cfg.baseUrl);
|
|
764
|
+
let u;
|
|
765
|
+
try {
|
|
766
|
+
u = new URL(raw ?? "/", base);
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return null;
|
|
771
|
+
const corsOrigins = Array.isArray(cfg.corsOrigins) ? cfg.corsOrigins : [];
|
|
772
|
+
const allowed = /* @__PURE__ */ new Set([base.origin, ...cfg.allowedReturnOrigins ?? [], ...corsOrigins]);
|
|
773
|
+
return allowed.has(u.origin) ? u : null;
|
|
774
|
+
}
|
|
775
|
+
function registerOAuthRoutes(app, svc, cfg, requireSession) {
|
|
776
|
+
app.get("/campaigns/:id/oauth/:provider/start", requireSession, async (c) => {
|
|
777
|
+
const provider = c.req.param("provider");
|
|
778
|
+
const def = providers[provider];
|
|
779
|
+
const client = cfg.oauth?.[provider];
|
|
780
|
+
if (!def || !client) return c.json({ error: "not_configured", message: `${provider} OAuth is not configured` }, 501);
|
|
781
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
782
|
+
const key = c.req.query("key");
|
|
783
|
+
const req = campaign.requirements.find((r) => r.key === key && r.module === moduleForProvider[provider]);
|
|
784
|
+
if (!req) return c.json({ error: "bad_request", message: "Requirement key does not match provider" }, 400);
|
|
785
|
+
const returnUrl = resolveReturnUrl(c.req.query("return"), cfg);
|
|
786
|
+
if (!returnUrl) return c.json({ error: "bad_request", message: "return URL is not an allowed origin" }, 400);
|
|
787
|
+
const returnTo = returnUrl.toString();
|
|
788
|
+
const state = randomId(16);
|
|
789
|
+
const verifier = def.pkce ? pkceVerifier() : "";
|
|
790
|
+
await cfg.storage.setTemp(`oauth:${state}`, JSON.stringify({ campaignId: campaign.id, wallet: c.get("session").wallet, key, returnTo, verifier }), 600);
|
|
791
|
+
const u = new URL(def.authUrl);
|
|
792
|
+
u.searchParams.set("response_type", "code");
|
|
793
|
+
u.searchParams.set("client_id", client.clientId);
|
|
794
|
+
u.searchParams.set("redirect_uri", `${cfg.baseUrl}/oauth/${provider}/callback`);
|
|
795
|
+
u.searchParams.set("scope", def.scope);
|
|
796
|
+
u.searchParams.set("state", state);
|
|
797
|
+
if (def.pkce) {
|
|
798
|
+
u.searchParams.set("code_challenge", pkceChallenge(verifier));
|
|
799
|
+
u.searchParams.set("code_challenge_method", "S256");
|
|
800
|
+
}
|
|
801
|
+
if (provider === "google") u.searchParams.set("access_type", "online");
|
|
802
|
+
return c.req.query("redirect") === "1" ? c.redirect(u.toString()) : c.json({ url: u.toString() });
|
|
803
|
+
});
|
|
804
|
+
app.get("/oauth/:provider/callback", async (c) => {
|
|
805
|
+
const provider = c.req.param("provider");
|
|
806
|
+
const def = providers[provider];
|
|
807
|
+
const client = cfg.oauth?.[provider];
|
|
808
|
+
const state = c.req.query("state") ?? "";
|
|
809
|
+
const raw = await cfg.storage.getTemp(`oauth:${state}`);
|
|
810
|
+
if (!def || !client || !raw) return c.text("Invalid or expired OAuth state", 400);
|
|
811
|
+
await cfg.storage.deleteTemp(`oauth:${state}`);
|
|
812
|
+
const { campaignId, wallet, key, returnTo, verifier } = JSON.parse(raw);
|
|
813
|
+
const back = (status, msg) => {
|
|
814
|
+
const u = resolveReturnUrl(returnTo, cfg) ?? new URL("/", cfg.baseUrl);
|
|
815
|
+
u.searchParams.set("allowlist", `${key}:${status}`);
|
|
816
|
+
if (msg) u.searchParams.set("allowlist_msg", msg);
|
|
817
|
+
return c.redirect(u.toString());
|
|
818
|
+
};
|
|
819
|
+
if (c.req.query("error")) return back("error", c.req.query("error_description") ?? "Authorization denied");
|
|
820
|
+
const code = c.req.query("code");
|
|
821
|
+
if (!code) return back("error", "Missing code");
|
|
822
|
+
const body = new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: `${cfg.baseUrl}/oauth/${provider}/callback`, client_id: client.clientId });
|
|
823
|
+
if (def.pkce) body.set("code_verifier", verifier);
|
|
824
|
+
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
825
|
+
if (def.basicAuth) headers.authorization = `Basic ${btoa(`${client.clientId}:${client.clientSecret}`)}`;
|
|
826
|
+
else body.set("client_secret", client.clientSecret);
|
|
827
|
+
const tokenRes = await fetch(def.tokenUrl, { method: "POST", headers, body });
|
|
828
|
+
if (!tokenRes.ok) return back("error", `Token exchange failed (${tokenRes.status})`);
|
|
829
|
+
const { access_token } = await tokenRes.json();
|
|
830
|
+
try {
|
|
831
|
+
const profile = await def.profile(access_token);
|
|
832
|
+
const campaign = await svc.getCampaign(campaignId);
|
|
833
|
+
const { result } = await svc.verifyRequirement(campaign, wallet, key, profile);
|
|
834
|
+
return back(result.passed ? "ok" : "error", result.passed ? void 0 : result.reason);
|
|
835
|
+
} catch (e) {
|
|
836
|
+
return back("error", e.message);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/app.ts
|
|
842
|
+
var pubkey = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/);
|
|
843
|
+
function createAllowlistApp(config, opts = {}) {
|
|
844
|
+
const cfg = normalizeConfig(config);
|
|
845
|
+
const svc = new AllowlistService(cfg, opts.waitUntil);
|
|
846
|
+
const app = new Hono();
|
|
847
|
+
const socketIp = (c) => {
|
|
848
|
+
try {
|
|
849
|
+
return opts.getClientIp?.(c) ?? cfg.getClientIp?.(c.req.raw);
|
|
850
|
+
} catch {
|
|
851
|
+
return void 0;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
const ipOf = (c) => {
|
|
855
|
+
switch (cfg.trustProxy) {
|
|
856
|
+
case "cloudflare":
|
|
857
|
+
return c.req.header("cf-connecting-ip") ?? socketIp(c);
|
|
858
|
+
case "x-forwarded-for":
|
|
859
|
+
return c.req.header("x-forwarded-for")?.split(",")[0].trim() ?? socketIp(c);
|
|
860
|
+
default:
|
|
861
|
+
return socketIp(c);
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
app.use("*", cors({ origin: cfg.corsOrigins === "*" ? "*" : cfg.corsOrigins, allowHeaders: ["content-type", "authorization", "x-api-key"] }));
|
|
865
|
+
app.onError((err, c) => {
|
|
866
|
+
if (err instanceof AllowlistError) return c.json({ error: err.code, message: err.message }, err.status);
|
|
867
|
+
if (err instanceof z.ZodError) return c.json({ error: "validation", message: "Invalid input", issues: err.issues }, 400);
|
|
868
|
+
console.error(err);
|
|
869
|
+
return c.json({ error: "internal", message: "Something went wrong" }, 500);
|
|
870
|
+
});
|
|
871
|
+
app.use("*", async (c, next) => {
|
|
872
|
+
const ip = ipOf(c);
|
|
873
|
+
if (ip && cfg.rateLimit.max > 0) {
|
|
874
|
+
const n = await cfg.storage.incrTemp(`rl:${ip}:${Math.floor(Date.now() / 1e3 / cfg.rateLimit.windowSeconds)}`, cfg.rateLimit.windowSeconds);
|
|
875
|
+
if (n > cfg.rateLimit.max) return c.json({ error: "rate_limited", message: "Too many requests" }, 429);
|
|
876
|
+
}
|
|
877
|
+
await next();
|
|
878
|
+
});
|
|
879
|
+
const requireSession = async (c, next) => {
|
|
880
|
+
const token = c.req.header("authorization")?.replace(/^Bearer\s+/i, "");
|
|
881
|
+
const s = token && verifySession(cfg.sessionSecret, token);
|
|
882
|
+
if (!s) return c.json({ error: "unauthorized", message: "Sign in with your wallet first" }, 401);
|
|
883
|
+
if (s.campaignId !== c.req.param("id")) return c.json({ error: "unauthorized", message: "Session is for a different campaign" }, 401);
|
|
884
|
+
c.set("session", s);
|
|
885
|
+
await next();
|
|
886
|
+
};
|
|
887
|
+
const requireApiKey = (scope) => async (c, next) => {
|
|
888
|
+
const key = c.req.header("x-api-key") ?? c.req.header("authorization")?.replace(/^Bearer\s+/i, "");
|
|
889
|
+
if (!key) return c.json({ error: "unauthorized", message: "API key required" }, 401);
|
|
890
|
+
if (cfg.adminApiKey && safeEqual(key, cfg.adminApiKey)) {
|
|
891
|
+
c.set("apiKeyScopes", ["admin", "read"]);
|
|
892
|
+
return next();
|
|
893
|
+
}
|
|
894
|
+
const h = sha256Hex(key);
|
|
895
|
+
const k = (await cfg.storage.listApiKeys()).find((k2) => k2.hash === h);
|
|
896
|
+
if (!k || scope === "admin" && !k.scopes.includes("admin")) return c.json({ error: "forbidden", message: "Invalid API key" }, 403);
|
|
897
|
+
c.set("apiKeyScopes", k.scopes);
|
|
898
|
+
await next();
|
|
899
|
+
};
|
|
900
|
+
app.get("/health", (c) => c.json({ ok: true, modules: svc.registry.list().map((m) => m.id) }));
|
|
901
|
+
app.get("/modules", (c) => c.json(svc.registry.list().map(({ configSchema: _s, ...m }) => m)));
|
|
902
|
+
app.get("/campaigns/:id", async (c) => {
|
|
903
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
904
|
+
const [total, eligible] = await Promise.all([cfg.storage.countEntries(campaign.id), cfg.storage.countEntries(campaign.id, { eligibleOnly: true })]);
|
|
905
|
+
return c.json({ campaign: svc.publicCampaign(campaign), stats: { total, eligible } });
|
|
906
|
+
});
|
|
907
|
+
app.post("/campaigns/:id/auth/nonce", async (c) => {
|
|
908
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
909
|
+
const { wallet } = z.object({ wallet: pubkey }).parse(await c.req.json());
|
|
910
|
+
const nonce = randomId(16);
|
|
911
|
+
const issuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
912
|
+
const statement = campaign.requirements.find((r) => r.module === "wallet-signature")?.config?.statement;
|
|
913
|
+
const message = buildSignInMessage({ domain: cfg.domain, wallet, campaignId: campaign.id, nonce, issuedAt, statement });
|
|
914
|
+
await cfg.storage.setTemp(`nonce:${campaign.id}:${wallet}:${nonce}`, message, 300);
|
|
915
|
+
return c.json({ nonce, message });
|
|
916
|
+
});
|
|
917
|
+
app.post("/campaigns/:id/auth/verify", async (c) => {
|
|
918
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
919
|
+
const { wallet, nonce, signature } = z.object({ wallet: pubkey, nonce: z.string().min(8), signature: z.string().min(40) }).parse(await c.req.json());
|
|
920
|
+
const key = `nonce:${campaign.id}:${wallet}:${nonce}`;
|
|
921
|
+
const message = await cfg.storage.getTemp(key);
|
|
922
|
+
if (!message) throw new AllowlistError(400, "Nonce expired \u2014 request a new one", "nonce_expired");
|
|
923
|
+
if (!verifyWalletSignature(wallet, message, signature)) throw new AllowlistError(401, "Signature does not match wallet", "bad_signature");
|
|
924
|
+
await cfg.storage.deleteTemp(key);
|
|
925
|
+
const entry = await svc.getOrCreateEntry(campaign, wallet);
|
|
926
|
+
const token = signSession(cfg.sessionSecret, { wallet, campaignId: campaign.id, exp: Date.now() + cfg.sessionTtlSeconds * 1e3 });
|
|
927
|
+
return c.json({ token, entry });
|
|
928
|
+
});
|
|
929
|
+
app.get("/campaigns/:id/me", requireSession, async (c) => {
|
|
930
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
931
|
+
const entry = await svc.getOrCreateEntry(campaign, c.get("session").wallet);
|
|
932
|
+
const links = await cfg.storage.listSocialLinksForWallet(campaign.id, entry.wallet);
|
|
933
|
+
return c.json({ entry, social: links.map(({ provider, handle }) => ({ provider, handle })) });
|
|
934
|
+
});
|
|
935
|
+
app.post("/campaigns/:id/requirements/:key/verify", requireSession, async (c) => {
|
|
936
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
937
|
+
const input = await c.req.json().catch(() => ({}));
|
|
938
|
+
const { entry, result } = await svc.verifyRequirement(campaign, c.get("session").wallet, c.req.param("key"), input, ipOf(c));
|
|
939
|
+
return c.json({ entry, result });
|
|
940
|
+
});
|
|
941
|
+
registerOAuthRoutes(app, svc, cfg, requireSession);
|
|
942
|
+
const passthrough = async (_c, n) => n();
|
|
943
|
+
const lookupGuard = cfg.eligibilityLookup === "protected" ? requireApiKey("read") : passthrough;
|
|
944
|
+
app.get("/campaigns/:id/eligibility/:wallet", lookupGuard, async (c) => {
|
|
945
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
946
|
+
const wallet = pubkey.parse(c.req.param("wallet"));
|
|
947
|
+
const hasKey = !!c.req.header("x-api-key") || !!c.req.header("authorization");
|
|
948
|
+
const detailed = cfg.eligibilityLookup === "full" || cfg.eligibilityLookup === "protected" || hasKey;
|
|
949
|
+
const snap = await cfg.storage.getSnapshot(campaign.id, c.req.query("snapshot") || void 0);
|
|
950
|
+
if (snap) {
|
|
951
|
+
const e = snap.entries[wallet];
|
|
952
|
+
const out2 = { wallet, eligible: !!e, allocation: e?.allocation ?? 0, snapshot: snap.id };
|
|
953
|
+
if (detailed) out2.rank = e?.rank;
|
|
954
|
+
if (e?.proof && snap.merkle) out2.merkle = { root: snap.merkle.root, proof: e.proof };
|
|
955
|
+
return c.json(out2);
|
|
956
|
+
}
|
|
957
|
+
const entries = await svc.finalEntries(campaign);
|
|
958
|
+
const entry = entries.find((e) => e.wallet === wallet);
|
|
959
|
+
const out = { wallet, eligible: entry?.eligible ?? false, allocation: entry?.allocation ?? 0, snapshot: null };
|
|
960
|
+
if (detailed) Object.assign(out, { points: entry?.points ?? 0, rank: entry?.rank });
|
|
961
|
+
if (campaign.merkle.enabled && entry?.eligible) {
|
|
962
|
+
const tree = buildMerkleTree2(entries.filter((e) => e.eligible).map((e) => e.wallet), campaign.merkle.scheme);
|
|
963
|
+
out.merkle = { root: tree.root, proof: getMerkleProof2(tree, wallet) };
|
|
964
|
+
}
|
|
965
|
+
return c.json(out);
|
|
966
|
+
});
|
|
967
|
+
const admin = new Hono();
|
|
968
|
+
admin.use("*", requireApiKey("admin"));
|
|
969
|
+
admin.get("/campaigns", async (c) => c.json(await cfg.storage.listCampaigns()));
|
|
970
|
+
admin.post("/campaigns", async (c) => c.json(await svc.saveCampaign(await c.req.json()), 201));
|
|
971
|
+
admin.put("/campaigns/:id", async (c) => c.json(await svc.saveCampaign({ ...await c.req.json(), id: c.req.param("id") })));
|
|
972
|
+
admin.get("/campaigns/:id", async (c) => c.json(await svc.getCampaign(c.req.param("id"))));
|
|
973
|
+
admin.delete("/campaigns/:id", async (c) => {
|
|
974
|
+
await cfg.storage.deleteCampaign(c.req.param("id"));
|
|
975
|
+
return c.body(null, 204);
|
|
976
|
+
});
|
|
977
|
+
admin.get("/campaigns/:id/stats", async (c) => {
|
|
978
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
979
|
+
const entries = await svc.finalEntries(campaign);
|
|
980
|
+
const byRequirement = Object.fromEntries(campaign.requirements.map((r) => [r.key, entries.filter((e) => e.results[r.key]?.passed).length]));
|
|
981
|
+
const pendingReview = entries.filter((e) => Object.values(e.results).some((r) => !r.passed && r.evidence?.pendingApproval)).length;
|
|
982
|
+
return c.json({
|
|
983
|
+
total: entries.length,
|
|
984
|
+
eligible: entries.filter((e) => e.eligible).length,
|
|
985
|
+
totalAllocation: entries.reduce((s, e) => s + e.allocation, 0),
|
|
986
|
+
byRequirement,
|
|
987
|
+
pendingReview,
|
|
988
|
+
last24h: entries.filter((e) => Date.now() - e.createdAt < 864e5).length
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
admin.get("/campaigns/:id/entries", async (c) => {
|
|
992
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
993
|
+
const q = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(500).default(50), eligible: z.enum(["true", "false"]).optional(), search: z.string().optional() }).parse(c.req.query());
|
|
994
|
+
let entries = await svc.finalEntries(campaign);
|
|
995
|
+
if (q.eligible) entries = entries.filter((e) => e.eligible === (q.eligible === "true"));
|
|
996
|
+
if (q.search) entries = entries.filter((e) => e.wallet.includes(q.search) || e.referralCode === q.search.toUpperCase());
|
|
997
|
+
const total = entries.length;
|
|
998
|
+
return c.json({ total, page: q.page, limit: q.limit, entries: entries.slice((q.page - 1) * q.limit, q.page * q.limit) });
|
|
999
|
+
});
|
|
1000
|
+
admin.patch("/campaigns/:id/entries/:wallet/requirements/:key", async (c) => {
|
|
1001
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
1002
|
+
const { passed, note } = z.object({ passed: z.boolean(), note: z.string().optional() }).parse(await c.req.json());
|
|
1003
|
+
return c.json(await svc.overrideRequirement(campaign, c.req.param("wallet"), c.req.param("key"), passed, note));
|
|
1004
|
+
});
|
|
1005
|
+
admin.post("/campaigns/:id/snapshot", async (c) => {
|
|
1006
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
1007
|
+
const { entries: _e, ...summary } = await svc.createSnapshot(campaign);
|
|
1008
|
+
return c.json(summary, 201);
|
|
1009
|
+
});
|
|
1010
|
+
admin.get("/campaigns/:id/snapshots", async (c) => c.json(await cfg.storage.listSnapshots(c.req.param("id"))));
|
|
1011
|
+
admin.get("/campaigns/:id/snapshots/:sid", async (c) => {
|
|
1012
|
+
const snap = await cfg.storage.getSnapshot(c.req.param("id"), c.req.param("sid"));
|
|
1013
|
+
if (!snap) throw new AllowlistError(404, "Snapshot not found");
|
|
1014
|
+
return c.json(snap);
|
|
1015
|
+
});
|
|
1016
|
+
admin.post("/campaigns/:id/recheck", async (c) => {
|
|
1017
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
1018
|
+
const { wallets } = z.object({ wallets: z.array(pubkey).optional() }).parse(await c.req.json().catch(() => ({})));
|
|
1019
|
+
return c.json(await svc.recheckCampaign(campaign, wallets));
|
|
1020
|
+
});
|
|
1021
|
+
admin.get("/campaigns/:id/export", async (c) => {
|
|
1022
|
+
const campaign = await svc.getCampaign(c.req.param("id"));
|
|
1023
|
+
const q = z.object({ format: z.enum(["csv", "json", "txt", "merkle"]).default("json"), all: z.enum(["true", "false"]).default("false"), evidence: z.enum(["true", "false"]).default("false") }).parse(c.req.query());
|
|
1024
|
+
const entries = await svc.finalEntries(campaign);
|
|
1025
|
+
const opts2 = { eligibleOnly: q.all !== "true", includeEvidence: q.evidence === "true" };
|
|
1026
|
+
await svc.events.emit("campaign.exported", campaign.id, { format: q.format, count: entries.filter((e) => e.eligible).length });
|
|
1027
|
+
const fname = `${campaign.id}-allowlist`;
|
|
1028
|
+
if (q.format === "csv") return c.body(toCSV(toRows(campaign, entries, opts2)), 200, { "content-type": "text/csv", "content-disposition": `attachment; filename="${fname}.csv"` });
|
|
1029
|
+
if (q.format === "txt") return c.body(toWalletList(entries), 200, { "content-type": "text/plain", "content-disposition": `attachment; filename="${fname}.txt"` });
|
|
1030
|
+
if (q.format === "merkle") {
|
|
1031
|
+
const eligible = entries.filter((e) => e.eligible).map((e) => e.wallet);
|
|
1032
|
+
if (eligible.length === 0) throw new AllowlistError(400, "No eligible wallets yet", "empty");
|
|
1033
|
+
const tree = buildMerkleTree2(eligible, campaign.merkle.scheme);
|
|
1034
|
+
return c.json({ scheme: tree.scheme, root: tree.root, count: eligible.length, proofs: Object.fromEntries(eligible.map((w) => [w, getMerkleProof2(tree, w)])) });
|
|
1035
|
+
}
|
|
1036
|
+
return c.json(toJSON(campaign, entries, { ...opts2, includeMerkle: campaign.merkle.enabled }));
|
|
1037
|
+
});
|
|
1038
|
+
admin.get("/webhooks", async (c) => c.json((await cfg.storage.listWebhooks()).map(({ secret: _s, ...w }) => w)));
|
|
1039
|
+
admin.post("/webhooks", async (c) => {
|
|
1040
|
+
const body = z.object({ url: z.string().url(), events: z.array(z.string()).default(["*"]), campaignId: z.string().optional() }).parse(await c.req.json());
|
|
1041
|
+
try {
|
|
1042
|
+
assertSafeWebhookUrl(body.url, { allowInsecure: cfg.allowInsecureWebhooks });
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
throw new AllowlistError(400, e.message, "unsafe_webhook_url");
|
|
1045
|
+
}
|
|
1046
|
+
const w = { id: randomId(8), secret: randomId(24), active: true, createdAt: Date.now(), ...body };
|
|
1047
|
+
await cfg.storage.putWebhook(w);
|
|
1048
|
+
return c.json(w, 201);
|
|
1049
|
+
});
|
|
1050
|
+
admin.delete("/webhooks/:id", async (c) => {
|
|
1051
|
+
await cfg.storage.deleteWebhook(c.req.param("id"));
|
|
1052
|
+
return c.body(null, 204);
|
|
1053
|
+
});
|
|
1054
|
+
admin.post("/webhooks/:id/test", async (c) => {
|
|
1055
|
+
const w = (await cfg.storage.listWebhooks()).find((w2) => w2.id === c.req.param("id"));
|
|
1056
|
+
if (!w) throw new AllowlistError(404, "Webhook not found");
|
|
1057
|
+
await svc.events.deliver(w.id, w.url, w.secret, { id: randomId(8), type: "entry.updated", campaignId: w.campaignId ?? "test", data: { test: true }, createdAt: Date.now() }, 1);
|
|
1058
|
+
return c.json({ ok: true });
|
|
1059
|
+
});
|
|
1060
|
+
admin.get("/api-keys", async (c) => c.json((await cfg.storage.listApiKeys()).map(({ hash: _h, ...k }) => k)));
|
|
1061
|
+
admin.post("/api-keys", async (c) => {
|
|
1062
|
+
const { label, scopes } = z.object({ label: z.string().min(1), scopes: z.array(z.enum(["admin", "read"])).default(["read"]) }).parse(await c.req.json());
|
|
1063
|
+
const key = `al_${randomId(24)}`;
|
|
1064
|
+
const k = { id: randomId(6), hash: sha256Hex(key), label, scopes, createdAt: Date.now() };
|
|
1065
|
+
await cfg.storage.putApiKey(k);
|
|
1066
|
+
return c.json({ id: k.id, label, scopes, key }, 201);
|
|
1067
|
+
});
|
|
1068
|
+
admin.delete("/api-keys/:id", async (c) => {
|
|
1069
|
+
await cfg.storage.deleteApiKey(c.req.param("id"));
|
|
1070
|
+
return c.body(null, 204);
|
|
1071
|
+
});
|
|
1072
|
+
app.route("/admin", admin);
|
|
1073
|
+
return { app, service: svc, config: cfg };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/storage/sqlite.ts
|
|
1077
|
+
async function createSqliteStorage(file = "allowlist.db") {
|
|
1078
|
+
const mod = await import("./lib-HMVXUU3V.js");
|
|
1079
|
+
const Database = mod.default ?? mod;
|
|
1080
|
+
const db = new Database(file);
|
|
1081
|
+
db.pragma("journal_mode = WAL");
|
|
1082
|
+
db.exec(`
|
|
1083
|
+
CREATE TABLE IF NOT EXISTS campaigns (id TEXT PRIMARY KEY, json TEXT NOT NULL, updated_at INTEGER NOT NULL);
|
|
1084
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
1085
|
+
campaign_id TEXT NOT NULL, wallet TEXT NOT NULL, json TEXT NOT NULL,
|
|
1086
|
+
eligible INTEGER NOT NULL, referral_code TEXT, created_at INTEGER NOT NULL,
|
|
1087
|
+
PRIMARY KEY (campaign_id, wallet)
|
|
1088
|
+
);
|
|
1089
|
+
CREATE INDEX IF NOT EXISTS entries_ref ON entries(campaign_id, referral_code);
|
|
1090
|
+
CREATE INDEX IF NOT EXISTS entries_created ON entries(campaign_id, created_at);
|
|
1091
|
+
CREATE TABLE IF NOT EXISTS temp (k TEXT PRIMARY KEY, v TEXT NOT NULL, exp INTEGER NOT NULL);
|
|
1092
|
+
CREATE TABLE IF NOT EXISTS social_links (
|
|
1093
|
+
campaign_id TEXT NOT NULL, provider TEXT NOT NULL, provider_user_id TEXT NOT NULL,
|
|
1094
|
+
wallet TEXT NOT NULL, json TEXT NOT NULL,
|
|
1095
|
+
PRIMARY KEY (campaign_id, provider, provider_user_id)
|
|
1096
|
+
);
|
|
1097
|
+
CREATE INDEX IF NOT EXISTS social_wallet ON social_links(campaign_id, wallet);
|
|
1098
|
+
CREATE TABLE IF NOT EXISTS webhooks (id TEXT PRIMARY KEY, json TEXT NOT NULL);
|
|
1099
|
+
CREATE TABLE IF NOT EXISTS deliveries (id TEXT PRIMARY KEY, webhook_id TEXT, json TEXT NOT NULL);
|
|
1100
|
+
CREATE TABLE IF NOT EXISTS api_keys (id TEXT PRIMARY KEY, json TEXT NOT NULL);
|
|
1101
|
+
CREATE TABLE IF NOT EXISTS snapshots (id TEXT PRIMARY KEY, campaign_id TEXT NOT NULL, created_at INTEGER NOT NULL, json TEXT NOT NULL);
|
|
1102
|
+
CREATE INDEX IF NOT EXISTS snapshots_campaign ON snapshots(campaign_id, created_at);
|
|
1103
|
+
`);
|
|
1104
|
+
const j = (row) => row ? JSON.parse(row.json) : null;
|
|
1105
|
+
const s = {
|
|
1106
|
+
async getCampaign(id) {
|
|
1107
|
+
return j(db.prepare("SELECT json FROM campaigns WHERE id=?").get(id));
|
|
1108
|
+
},
|
|
1109
|
+
async listCampaigns() {
|
|
1110
|
+
return db.prepare("SELECT json FROM campaigns ORDER BY updated_at DESC").all().map((r) => JSON.parse(r.json));
|
|
1111
|
+
},
|
|
1112
|
+
async putCampaign(c) {
|
|
1113
|
+
db.prepare("INSERT OR REPLACE INTO campaigns (id,json,updated_at) VALUES (?,?,?)").run(c.id, JSON.stringify(c), Date.now());
|
|
1114
|
+
},
|
|
1115
|
+
async deleteCampaign(id) {
|
|
1116
|
+
db.prepare("DELETE FROM campaigns WHERE id=?").run(id);
|
|
1117
|
+
db.prepare("DELETE FROM entries WHERE campaign_id=?").run(id);
|
|
1118
|
+
db.prepare("DELETE FROM social_links WHERE campaign_id=?").run(id);
|
|
1119
|
+
},
|
|
1120
|
+
async getEntry(campaignId, wallet) {
|
|
1121
|
+
return j(db.prepare("SELECT json FROM entries WHERE campaign_id=? AND wallet=?").get(campaignId, wallet));
|
|
1122
|
+
},
|
|
1123
|
+
async putEntry(e) {
|
|
1124
|
+
db.prepare(
|
|
1125
|
+
"INSERT OR REPLACE INTO entries (campaign_id,wallet,json,eligible,referral_code,created_at) VALUES (?,?,?,?,?,?)"
|
|
1126
|
+
).run(e.campaignId, e.wallet, JSON.stringify(e), e.eligible ? 1 : 0, e.referralCode ?? null, e.createdAt);
|
|
1127
|
+
},
|
|
1128
|
+
async listEntries(campaignId, opts = {}) {
|
|
1129
|
+
const rows = db.prepare(
|
|
1130
|
+
`SELECT json FROM entries WHERE campaign_id=? ${opts.eligibleOnly ? "AND eligible=1" : ""} ORDER BY created_at ASC LIMIT ? OFFSET ?`
|
|
1131
|
+
).all(campaignId, opts.limit ?? 1e6, opts.offset ?? 0);
|
|
1132
|
+
return rows.map((r) => JSON.parse(r.json));
|
|
1133
|
+
},
|
|
1134
|
+
async countEntries(campaignId, opts = {}) {
|
|
1135
|
+
const r = db.prepare(`SELECT COUNT(*) as n FROM entries WHERE campaign_id=? ${opts.eligibleOnly ? "AND eligible=1" : ""}`).get(campaignId);
|
|
1136
|
+
return r.n;
|
|
1137
|
+
},
|
|
1138
|
+
async findEntryByReferralCode(campaignId, code) {
|
|
1139
|
+
return j(db.prepare("SELECT json FROM entries WHERE campaign_id=? AND referral_code=?").get(campaignId, code));
|
|
1140
|
+
},
|
|
1141
|
+
async setTemp(k, v, ttl) {
|
|
1142
|
+
db.prepare("INSERT OR REPLACE INTO temp (k,v,exp) VALUES (?,?,?)").run(k, v, Date.now() + ttl * 1e3);
|
|
1143
|
+
},
|
|
1144
|
+
async getTemp(k) {
|
|
1145
|
+
const r = db.prepare("SELECT v,exp FROM temp WHERE k=?").get(k);
|
|
1146
|
+
if (!r) return null;
|
|
1147
|
+
if (r.exp < Date.now()) {
|
|
1148
|
+
db.prepare("DELETE FROM temp WHERE k=?").run(k);
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
return r.v;
|
|
1152
|
+
},
|
|
1153
|
+
async deleteTemp(k) {
|
|
1154
|
+
db.prepare("DELETE FROM temp WHERE k=?").run(k);
|
|
1155
|
+
},
|
|
1156
|
+
async incrTemp(k, ttl) {
|
|
1157
|
+
const now = Date.now();
|
|
1158
|
+
const r = db.prepare(
|
|
1159
|
+
`INSERT INTO temp (k,v,exp) VALUES (?, '1', ?)
|
|
1160
|
+
ON CONFLICT(k) DO UPDATE SET
|
|
1161
|
+
v = CASE WHEN temp.exp < ? THEN '1' ELSE CAST(CAST(temp.v AS INTEGER) + 1 AS TEXT) END,
|
|
1162
|
+
exp = CASE WHEN temp.exp < ? THEN excluded.exp ELSE temp.exp END
|
|
1163
|
+
RETURNING v`
|
|
1164
|
+
).get(k, now + ttl * 1e3, now, now);
|
|
1165
|
+
return Number(r.v);
|
|
1166
|
+
},
|
|
1167
|
+
async getSocialLink(campaignId, provider, providerUserId) {
|
|
1168
|
+
return j(
|
|
1169
|
+
db.prepare("SELECT json FROM social_links WHERE campaign_id=? AND provider=? AND provider_user_id=?").get(campaignId, provider, providerUserId)
|
|
1170
|
+
);
|
|
1171
|
+
},
|
|
1172
|
+
async claimSocialLink(l) {
|
|
1173
|
+
db.prepare(
|
|
1174
|
+
"INSERT INTO social_links (campaign_id,provider,provider_user_id,wallet,json) VALUES (?,?,?,?,?) ON CONFLICT(campaign_id,provider,provider_user_id) DO NOTHING"
|
|
1175
|
+
).run(l.campaignId, l.provider, l.providerUserId, l.wallet, JSON.stringify(l));
|
|
1176
|
+
const owner = db.prepare("SELECT wallet FROM social_links WHERE campaign_id=? AND provider=? AND provider_user_id=?").get(l.campaignId, l.provider, l.providerUserId).wallet;
|
|
1177
|
+
return owner === l.wallet ? { ok: true } : { ok: false, owner };
|
|
1178
|
+
},
|
|
1179
|
+
async putSnapshot(snap) {
|
|
1180
|
+
db.prepare("INSERT OR REPLACE INTO snapshots (id,campaign_id,created_at,json) VALUES (?,?,?,?)").run(snap.id, snap.campaignId, snap.createdAt, JSON.stringify(snap));
|
|
1181
|
+
},
|
|
1182
|
+
async getSnapshot(campaignId, id) {
|
|
1183
|
+
const row = id ? db.prepare("SELECT json FROM snapshots WHERE campaign_id=? AND id=?").get(campaignId, id) : db.prepare("SELECT json FROM snapshots WHERE campaign_id=? ORDER BY created_at DESC LIMIT 1").get(campaignId);
|
|
1184
|
+
return j(row);
|
|
1185
|
+
},
|
|
1186
|
+
async listSnapshots(campaignId) {
|
|
1187
|
+
return db.prepare("SELECT json FROM snapshots WHERE campaign_id=? ORDER BY created_at DESC").all(campaignId).map((r) => {
|
|
1188
|
+
const { entries: _e, ...rest } = JSON.parse(r.json);
|
|
1189
|
+
return rest;
|
|
1190
|
+
});
|
|
1191
|
+
},
|
|
1192
|
+
async listSocialLinksForWallet(campaignId, wallet) {
|
|
1193
|
+
return db.prepare("SELECT json FROM social_links WHERE campaign_id=? AND wallet=?").all(campaignId, wallet).map((r) => JSON.parse(r.json));
|
|
1194
|
+
},
|
|
1195
|
+
async listWebhooks() {
|
|
1196
|
+
return db.prepare("SELECT json FROM webhooks").all().map((r) => JSON.parse(r.json));
|
|
1197
|
+
},
|
|
1198
|
+
async putWebhook(w) {
|
|
1199
|
+
db.prepare("INSERT OR REPLACE INTO webhooks (id,json) VALUES (?,?)").run(w.id, JSON.stringify(w));
|
|
1200
|
+
},
|
|
1201
|
+
async deleteWebhook(id) {
|
|
1202
|
+
db.prepare("DELETE FROM webhooks WHERE id=?").run(id);
|
|
1203
|
+
},
|
|
1204
|
+
async putDelivery(d) {
|
|
1205
|
+
db.prepare("INSERT OR REPLACE INTO deliveries (id,webhook_id,json) VALUES (?,?,?)").run(d.id, d.webhookId, JSON.stringify(d));
|
|
1206
|
+
},
|
|
1207
|
+
async listApiKeys() {
|
|
1208
|
+
return db.prepare("SELECT json FROM api_keys").all().map((r) => JSON.parse(r.json));
|
|
1209
|
+
},
|
|
1210
|
+
async putApiKey(k) {
|
|
1211
|
+
db.prepare("INSERT OR REPLACE INTO api_keys (id,json) VALUES (?,?)").run(k.id, JSON.stringify(k));
|
|
1212
|
+
},
|
|
1213
|
+
async deleteApiKey(id) {
|
|
1214
|
+
db.prepare("DELETE FROM api_keys WHERE id=?").run(id);
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
return s;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// src/storage/memory.ts
|
|
1221
|
+
var MemoryStorage = class {
|
|
1222
|
+
campaigns = /* @__PURE__ */ new Map();
|
|
1223
|
+
entries = /* @__PURE__ */ new Map();
|
|
1224
|
+
// `${campaignId}:${wallet}`
|
|
1225
|
+
temp = /* @__PURE__ */ new Map();
|
|
1226
|
+
social = /* @__PURE__ */ new Map();
|
|
1227
|
+
webhooks = /* @__PURE__ */ new Map();
|
|
1228
|
+
deliveries = [];
|
|
1229
|
+
apiKeys = /* @__PURE__ */ new Map();
|
|
1230
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1231
|
+
async getCampaign(id) {
|
|
1232
|
+
return this.campaigns.get(id) ?? null;
|
|
1233
|
+
}
|
|
1234
|
+
async listCampaigns() {
|
|
1235
|
+
return [...this.campaigns.values()];
|
|
1236
|
+
}
|
|
1237
|
+
async putCampaign(c) {
|
|
1238
|
+
this.campaigns.set(c.id, c);
|
|
1239
|
+
}
|
|
1240
|
+
async deleteCampaign(id) {
|
|
1241
|
+
this.campaigns.delete(id);
|
|
1242
|
+
for (const k of [...this.entries.keys()]) if (k.startsWith(id + ":")) this.entries.delete(k);
|
|
1243
|
+
}
|
|
1244
|
+
async getEntry(campaignId, wallet) {
|
|
1245
|
+
return this.entries.get(`${campaignId}:${wallet}`) ?? null;
|
|
1246
|
+
}
|
|
1247
|
+
async putEntry(e) {
|
|
1248
|
+
this.entries.set(`${e.campaignId}:${e.wallet}`, e);
|
|
1249
|
+
}
|
|
1250
|
+
async listEntries(campaignId, opts = {}) {
|
|
1251
|
+
let list = [...this.entries.values()].filter((e) => e.campaignId === campaignId);
|
|
1252
|
+
if (opts.eligibleOnly) list = list.filter((e) => e.eligible);
|
|
1253
|
+
list.sort((a, b) => a.createdAt - b.createdAt);
|
|
1254
|
+
const off = opts.offset ?? 0;
|
|
1255
|
+
return list.slice(off, opts.limit ? off + opts.limit : void 0);
|
|
1256
|
+
}
|
|
1257
|
+
async countEntries(campaignId, opts = {}) {
|
|
1258
|
+
return (await this.listEntries(campaignId, opts)).length;
|
|
1259
|
+
}
|
|
1260
|
+
async findEntryByReferralCode(campaignId, code) {
|
|
1261
|
+
return [...this.entries.values()].find((e) => e.campaignId === campaignId && e.referralCode === code) ?? null;
|
|
1262
|
+
}
|
|
1263
|
+
sweep() {
|
|
1264
|
+
const now = Date.now();
|
|
1265
|
+
for (const [k, v] of this.temp) if (v.exp < now) this.temp.delete(k);
|
|
1266
|
+
}
|
|
1267
|
+
async setTemp(key, value, ttlSeconds) {
|
|
1268
|
+
this.sweep();
|
|
1269
|
+
this.temp.set(key, { v: value, exp: Date.now() + ttlSeconds * 1e3 });
|
|
1270
|
+
}
|
|
1271
|
+
async getTemp(key) {
|
|
1272
|
+
const v = this.temp.get(key);
|
|
1273
|
+
if (!v || v.exp < Date.now()) return null;
|
|
1274
|
+
return v.v;
|
|
1275
|
+
}
|
|
1276
|
+
async deleteTemp(key) {
|
|
1277
|
+
this.temp.delete(key);
|
|
1278
|
+
}
|
|
1279
|
+
async incrTemp(key, ttlSeconds) {
|
|
1280
|
+
const cur = Number(await this.getTemp(key) ?? 0) + 1;
|
|
1281
|
+
const existing = this.temp.get(key);
|
|
1282
|
+
this.temp.set(key, { v: String(cur), exp: existing?.exp ?? Date.now() + ttlSeconds * 1e3 });
|
|
1283
|
+
return cur;
|
|
1284
|
+
}
|
|
1285
|
+
async getSocialLink(campaignId, provider, providerUserId) {
|
|
1286
|
+
return this.social.get(`${campaignId}:${provider}:${providerUserId}`) ?? null;
|
|
1287
|
+
}
|
|
1288
|
+
// JS is single-threaded, so check-then-set within one synchronous block is atomic here.
|
|
1289
|
+
async claimSocialLink(link) {
|
|
1290
|
+
const k = `${link.campaignId}:${link.provider}:${link.providerUserId}`;
|
|
1291
|
+
const existing = this.social.get(k);
|
|
1292
|
+
if (existing && existing.wallet !== link.wallet) return { ok: false, owner: existing.wallet };
|
|
1293
|
+
if (!existing) this.social.set(k, link);
|
|
1294
|
+
return { ok: true };
|
|
1295
|
+
}
|
|
1296
|
+
async putSnapshot(s) {
|
|
1297
|
+
this.snapshots.set(s.id, s);
|
|
1298
|
+
}
|
|
1299
|
+
async getSnapshot(campaignId, id) {
|
|
1300
|
+
const all = [...this.snapshots.values()].filter((s) => s.campaignId === campaignId).sort((a, b) => b.createdAt - a.createdAt);
|
|
1301
|
+
return (id ? all.find((s) => s.id === id) : all[0]) ?? null;
|
|
1302
|
+
}
|
|
1303
|
+
async listSnapshots(campaignId) {
|
|
1304
|
+
return [...this.snapshots.values()].filter((s) => s.campaignId === campaignId).sort((a, b) => b.createdAt - a.createdAt).map(({ entries: _e, ...s }) => s);
|
|
1305
|
+
}
|
|
1306
|
+
async listSocialLinksForWallet(campaignId, wallet) {
|
|
1307
|
+
return [...this.social.values()].filter((l) => l.campaignId === campaignId && l.wallet === wallet);
|
|
1308
|
+
}
|
|
1309
|
+
async listWebhooks() {
|
|
1310
|
+
return [...this.webhooks.values()];
|
|
1311
|
+
}
|
|
1312
|
+
async putWebhook(w) {
|
|
1313
|
+
this.webhooks.set(w.id, w);
|
|
1314
|
+
}
|
|
1315
|
+
async deleteWebhook(id) {
|
|
1316
|
+
this.webhooks.delete(id);
|
|
1317
|
+
}
|
|
1318
|
+
async putDelivery(d) {
|
|
1319
|
+
this.deliveries.push(d);
|
|
1320
|
+
}
|
|
1321
|
+
async listApiKeys() {
|
|
1322
|
+
return [...this.apiKeys.values()];
|
|
1323
|
+
}
|
|
1324
|
+
async putApiKey(k) {
|
|
1325
|
+
this.apiKeys.set(k.id, k);
|
|
1326
|
+
}
|
|
1327
|
+
async deleteApiKey(id) {
|
|
1328
|
+
this.apiKeys.delete(id);
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
export {
|
|
1333
|
+
pass,
|
|
1334
|
+
fail,
|
|
1335
|
+
VerificationUnavailable,
|
|
1336
|
+
toRawAmount,
|
|
1337
|
+
walletSignatureVerifier,
|
|
1338
|
+
tokenBalanceVerifier,
|
|
1339
|
+
nftOwnershipVerifier,
|
|
1340
|
+
verifyWalletSignature,
|
|
1341
|
+
xVerifier,
|
|
1342
|
+
discordVerifier,
|
|
1343
|
+
verifyTelegramLogin,
|
|
1344
|
+
telegramVerifier,
|
|
1345
|
+
youtubeVerifier,
|
|
1346
|
+
socialTaskVerifier,
|
|
1347
|
+
quizVerifier,
|
|
1348
|
+
referralVerifier,
|
|
1349
|
+
captchaVerifier,
|
|
1350
|
+
builtinVerifiers,
|
|
1351
|
+
EventBus,
|
|
1352
|
+
verifyWebhookSignature,
|
|
1353
|
+
AllowlistError,
|
|
1354
|
+
AllowlistService,
|
|
1355
|
+
createAllowlistApp,
|
|
1356
|
+
createSqliteStorage,
|
|
1357
|
+
MemoryStorage
|
|
1358
|
+
};
|