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.