PageSourceSearch

https://badmickey.netlify.app/assets/crew/api.js?v=36

js badmickey.netlify.app collected 2026-10-03 10:29:20 UTC 6,563 bytes, 135 lines download raw bytes

1/**
2 * Bad Crew API client.
3 *
4 * Every call goes to a Netlify Function. The badge shown on screen is always the
5 * one the server computed — the browser never derives a tier from local input.
6 */
7
8export class ApiError extends Error {
9  constructor(message, status, payload) {
10    super(message);
11    this.name = 'ApiError';
12    this.status = status;
13    this.payload = payload || {};
14  }
15}
16
17async function request(path, { method = 'GET', body, headers = {} } = {}) {
18  const response = await fetch(path, {
19    method,
20    credentials: 'same-origin',
21    headers: { ...(body !== undefined ? { 'content-type': 'application/json' } : {}), ...headers },
22    body: body !== undefined ? JSON.stringify(body) : undefined,
23  });
24
25  let payload = {};
26  try {
27    payload = await response.json();
28  } catch {
29    payload = {};
30  }
31
32  if (!response.ok) {
33    throw new ApiError(payload.error || `Request failed (${response.status})`, response.status, payload);
34  }
35  return payload;
36}
37
38/** Member-facing endpoints. Auth rides on the HttpOnly session cookie. */
39export const api = {
40  session: () => request('/api/auth/session'),
41  nonce: (wallet) => request('/api/auth/nonce', { method: 'POST', body: { wallet } }),
42  verify: (wallet, nonce, signature) =>
43    request('/api/auth/verify', { method: 'POST', body: { wallet, nonce, signature } }),
44  logout: () => request('/api/auth/logout', { method: 'POST' }),
45
46  me: () => request('/api/me'),
47
48  telegramStatus: () => request('/api/telegram/status'),
49  telegramChallenge: () => request('/api/telegram/challenge', { method: 'POST' }),
50  telegramCancel: () => request('/api/telegram/cancel', { method: 'POST' }),
51
52  verifyBadMickey: () => request('/api/bad-mickey/verify', { method: 'POST' }),
53  claimNft: (badgeKey) => request('/api/claims/nft', { method: 'POST', body: { badgeKey } }),
54  claimShirt: (shirtSize) => request('/api/claims/shirt', { method: 'POST', body: { shirtSize } }),
55
56  minnieHeist: () => request('/api/minnie/heist/me'),
57  enterMinnieHeist: () => request('/api/minnie/heist/enter', { method: 'POST', body: {} }),
58  submitMinnieHeistClue: (dayNumber, answer) =>
59    request('/api/minnie/heist/clue', { method: 'POST', body: { dayNumber, answer } }),
60  submitMinnieHeistPhrase: (answer) =>
61    request('/api/minnie/heist/phrase', { method: 'POST', body: { answer } }),
62  attachMinnieHeistReferral: (code) =>
63    request('/api/minnie/heist/referral', { method: 'POST', body: { code } }),
64};
65
66/**
67 * Admin endpoints. The operator's token is held in memory/sessionStorage only and
68 * sent as a bearer header; it is never written to a cookie or to localStorage.
69 */
70export function adminApi(getToken, getActor) {
71  const authed = (path, options = {}) =>
72    request(path, {
73      ...options,
74      headers: {
75        ...(options.headers || {}),
76        authorization: `Bearer ${getToken()}`,
77        ...(getActor && getActor() ? { 'x-admin-actor': getActor() } : {}),
78      },
79    });
80
81  return {
82    status: () => authed('/api/admin/status'),
83    members: (query = '') => authed(`/api/admin/members?q=${encodeURIComponent(query)}`),
84    member: (wallet) => authed(`/api/admin/member?wallet=${encodeURIComponent(wallet)}`),
85    recordHoldings: (body) => authed('/api/admin/holdings', { method: 'POST', body }),
86    recordPresale: (body) => authed('/api/admin/presale', { method: 'POST', body }),
87    removePresale: (id) => authed('/api/admin/presale/remove', { method: 'POST', body: { id } }),
88    grantReward: (body) => authed('/api/admin/reward', { method: 'POST', body }),
89    setNftStatus: (body) => authed('/api/admin/nft', { method: 'POST', body }),
90    setMerchStatus: (body) => authed('/api/admin/merch', { method: 'POST', body }),
91    setNote: (body) => authed('/api/admin/note', { method: 'POST', body }),
92
93    /**
94     * Member management. Every one of these is enforced server-side behind the
95     * same bearer token — hiding a button in this file protects nobody, and is
96     * not what protects these routes.
97     */
98    editMember: (wallet, fields) => authed('/api/admin/member/edit', { method: 'POST', body: { wallet, fields } }),
99    enrollMember: (wallet) => authed('/api/admin/member/enroll', { method: 'POST', body: { wallet } }),
100    reverifyHoldings: (wallet) => authed('/api/admin/member/reverify', { method: 'POST', body: { wallet } }),
101    revokeTelegram: (wallet, reason = {}) =>
102      authed('/api/admin/member/telegram/revoke', { method: 'POST', body: { wallet, ...reason } }),
103    unlinkTelegram: (wallet, reason = {}) =>
104      authed('/api/admin/member/telegram/unlink', { method: 'POST', body: { wallet, ...reason } }),
105    deleteMember: (wallet, { confirmation, reasonCode = null, reasonNote = null } = {}) =>
106      authed('/api/admin/member/delete', {
107        method: 'POST',
108        body: { wallet, confirmation, reasonCode, reasonNote },
109      }),
110    memberHistory: (wallet) => authed(`/api/admin/member/history?wallet=${encodeURIComponent(wallet)}`),
111
112    telegramWebhookInfo: () => authed('/api/admin/telegram/webhook'),
113    registerTelegramWebhook: (body = {}) =>
114      authed('/api/admin/telegram/webhook', { method: 'POST', body }),
115
116    raidRounds: () => authed('/api/admin/raid-rounds'),
117    addRaidEntry: (body) => authed('/api/admin/raid-entry', { method: 'POST', body }),
118    removeRaidEntry: (id) => authed('/api/admin/raid-entry/remove', { method: 'POST', body: { id } }),
119    lockRaidRound: (roundKey) => authed('/api/admin/raid-round/lock', { method: 'POST', body: { roundKey } }),
120    prepareRaidPayout: (roundKey) => authed('/api/admin/raid-payout/prepare', { method: 'POST', body: { roundKey } }),
121    confirmRaidPayout: (batchId, signature) =>
122      authed('/api/admin/raid-payout/confirm', { method: 'POST', body: { batchId, signature } }),
123
124    minnieHeist: () => authed('/api/admin/minnie-heist'),
125    configureMinnieHeist: (body) => authed('/api/admin/minnie-heist/configure', { method: 'POST', body }),
126    setMinnieHeistClue: (dayNumber, answer) =>
127      authed('/api/admin/minnie-heist/clue', { method: 'POST', body: { dayNumber, answer } }),
128    activateMinnieHeist: () => authed('/api/admin/minnie-heist/activate', { method: 'POST', body: {} }),
129    lockMinnieHeist: () => authed('/api/admin/minnie-heist/lock', { method: 'POST', body: {} }),
130    disqualifyMinnieHeistEntry: (wallet, reason) =>
131      authed('/api/admin/minnie-heist/entry/disqualify', { method: 'POST', body: { wallet, reason } }),
132    restoreMinnieHeistEntry: (wallet) =>
133      authed('/api/admin/minnie-heist/entry/restore', { method: 'POST', body: { wallet } }),
134  };
135}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.