mirror of
https://github.com/hanzoai/agency.git
synced 2026-11-02 19:13:28 +00:00
1 line
12 KiB
Plaintext
1 line
12 KiB
Plaintext
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Browser-safe REST client for a Hanzo Base waitlist plugin.\n//\n// The client speaks the /v1/waitlist/{join,status,export} surface. It has\n// no React or DOM dependency — safe to use from server code, Node tools,\n// or other frontends.\n\nexport interface JoinInput {\n /** Waitlist slug or id, as configured on the Base server. */\n waitlist: string\n email: string\n /** Optional referral code from the URL `?ref=<code>` parameter. */\n referrerCode?: string\n /** Cloudflare Turnstile token, if Turnstile is enabled server-side. */\n turnstileToken?: string\n}\n\nexport interface StatusInput {\n waitlist: string\n email: string\n}\n\nexport interface PointBreakdown {\n referrals: number\n shares: number\n invitesSent: number\n invitesConverted: number\n /** Points from verified social follows/joins (X, Discord, Telegram, ...). */\n social?: number\n /** Points from running a hanzod node. */\n hanzod?: number\n}\n\nexport interface PointValues {\n REFERRAL: number\n SHARE: number\n INVITE_SENT: number\n INVITE_CONVERTED: number\n}\n\nexport interface WaitlistEntry {\n ok: true\n waitlist: string\n email: string\n refCode: string\n rank: number\n total: number\n referralCount: number\n shareUrl: string\n /** Total points (referrals*REFERRAL + shares*SHARE + invitesSent + invitesConverted*INVITE_CONVERTED). */\n points: number\n pointBreakdown: PointBreakdown\n pointValues: PointValues\n alreadyJoined?: boolean\n aheadOf?: number\n}\n\nexport interface TrackShareInput {\n waitlist: string\n refCode: string\n platform: string\n}\n\nexport interface TrackShareResponse {\n ok: true\n awarded: number\n alreadyClaimed: boolean\n points: number\n pointBreakdown: PointBreakdown\n}\n\nexport interface InviteInput {\n waitlist: string\n refCode: string\n emails: string[]\n message?: string\n}\n\nexport interface InviteResponse {\n ok: true\n sent: number\n skipped: number\n invalid: number\n duplicates: number\n pointsAwarded: number\n points: number\n pointBreakdown: PointBreakdown\n}\n\nexport type ActivityType = 'join' | 'share' | 'invite' | 'referral' | 'social' | 'hanzod'\n\nexport interface ActivityEvent {\n ts: number\n type: ActivityType\n who: string\n source?: 'direct' | 'referral'\n platform?: string\n invited?: string\n}\n\nexport interface ActivityInput {\n waitlist: string\n limit?: number\n types?: ActivityType[]\n}\n\nexport interface ActivityResponse {\n ok: true\n waitlist: string\n /** Server wall-clock at the time of response — use this to compute\n * relative times consistently even with client clock skew. */\n now: number\n entries: ActivityEvent[]\n}\n\nexport interface ListInput {\n waitlist: string\n page?: number\n pageSize?: number\n}\n\nexport interface LeaderboardEntry {\n rank: number\n email: string // masked unless admin auth attached\n refCode: string | null\n points: number\n referralCount: number\n createdAt: string\n}\n\nexport interface LeaderboardPage {\n ok: true\n waitlist: string\n page: number\n pageSize: number\n total: number\n totalPages: number\n entries: LeaderboardEntry[]\n}\n\nexport interface NeighborhoodInput {\n waitlist: string\n email: string\n /** How many entries to show above and below the caller (1..100, default 25). */\n window?: number\n}\n\nexport interface NeighborRow {\n rank: number\n email: string // masked\n points: number\n referralCount: number\n isMe?: boolean\n}\n\nexport interface NeighborhoodResponse {\n ok: true\n waitlist: string\n email: string\n rank: number\n total: number\n points: number\n window: number\n /** The caller and its nearest neighbors, ordered by rank (best first). */\n entries: NeighborRow[]\n}\n\nexport interface ApiError {\n ok: false\n status: number\n message: string\n data?: unknown\n}\n\nexport type Result<T> = T | ApiError\n\nexport interface ClientOptions {\n /**\n * Base URL of the Hanzo Base server, e.g. `https://api.example.com`.\n * Defaults to the current page origin.\n */\n baseUrl?: string\n /**\n * Optional path prefix to prepend before `/v1/waitlist`. Useful when\n * Base is mounted under a sub-path (`BASE_API_PREFIX=/v1/foo`).\n * Should NOT include `/v1` — defaults to empty.\n */\n pathPrefix?: string\n /** Optional fetch override, for testing or custom transports. */\n fetch?: typeof fetch\n}\n\nexport class WaitlistClient {\n private readonly baseUrl: string\n private readonly prefix: string\n private readonly fetchFn: typeof fetch\n\n constructor(opts: ClientOptions = {}) {\n this.baseUrl = (opts.baseUrl ?? (typeof window !== 'undefined' ? window.location.origin : '')).replace(/\\/$/, '')\n this.prefix = (opts.pathPrefix ?? '').replace(/\\/$/, '')\n this.fetchFn = opts.fetch ?? (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : (() => { throw new Error('fetch is not available') })()) as typeof fetch\n }\n\n private url(path: string): string {\n return `${this.baseUrl}${this.prefix}/v1/waitlist${path}`\n }\n\n async join(input: JoinInput): Promise<Result<WaitlistEntry>> {\n const res = await this.fetchFn(this.url('/join'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(input),\n })\n return this.parse(res)\n }\n\n async status(input: StatusInput): Promise<Result<WaitlistEntry>> {\n const q = new URLSearchParams({ waitlist: input.waitlist, email: input.email })\n const res = await this.fetchFn(this.url(`/status?${q}`), { method: 'GET' })\n return this.parse(res)\n }\n\n async list(input: ListInput): Promise<Result<LeaderboardPage>> {\n const q = new URLSearchParams({ waitlist: input.waitlist })\n if (input.page) q.set('page', String(input.page))\n if (input.pageSize) q.set('pageSize', String(input.pageSize))\n const res = await this.fetchFn(this.url(`/list?${q}`), { method: 'GET' })\n return this.parseGeneric<LeaderboardPage>(res)\n }\n\n /**\n * The scalable \"around me\" leaderboard view: the caller's absolute rank in a\n * list of any size, plus the `window` entries just above and below. Backed by\n * keyset index seeks server-side, so it stays cheap (~0.1ms) even on a\n * multi-million-row list — never fetch the whole leaderboard to show a slice.\n */\n async neighborhood(input: NeighborhoodInput): Promise<Result<NeighborhoodResponse>> {\n const q = new URLSearchParams({ waitlist: input.waitlist, email: input.email })\n if (input.window) q.set('window', String(input.window))\n const res = await this.fetchFn(this.url(`/neighborhood?${q}`), { method: 'GET' })\n return this.parseGeneric<NeighborhoodResponse>(res)\n }\n\n async trackShare(input: TrackShareInput): Promise<Result<TrackShareResponse>> {\n const res = await this.fetchFn(this.url('/track-share'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(input),\n })\n return this.parseGeneric<TrackShareResponse>(res)\n }\n\n async invite(input: InviteInput): Promise<Result<InviteResponse>> {\n const res = await this.fetchFn(this.url('/invite'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(input),\n })\n return this.parseGeneric<InviteResponse>(res)\n }\n\n async activity(input: ActivityInput): Promise<Result<ActivityResponse>> {\n const q = new URLSearchParams({ waitlist: input.waitlist })\n if (input.limit) q.set('limit', String(input.limit))\n if (input.types?.length) q.set('types', input.types.join(','))\n const res = await this.fetchFn(this.url(`/activity?${q}`), { method: 'GET' })\n return this.parseGeneric<ActivityResponse>(res)\n }\n\n /**\n * Returns a CSV string. Requires either a superuser auth cookie/token\n * already attached to the request, OR an admin secret passed via\n * `Authorization: Bearer <secret>`.\n */\n async exportCsv(waitlist: string, init: { authorization?: string } = {}): Promise<string> {\n const q = new URLSearchParams({ waitlist })\n const headers: Record<string, string> = {}\n if (init.authorization) headers.Authorization = init.authorization\n const res = await this.fetchFn(this.url(`/export?${q}`), { method: 'GET', headers })\n if (!res.ok) throw new Error(`export failed: ${res.status} ${await res.text()}`)\n return res.text()\n }\n\n private async parse(res: Response): Promise<Result<WaitlistEntry>> {\n return this.parseGeneric<WaitlistEntry>(res)\n }\n\n private async parseGeneric<T>(res: Response): Promise<Result<T>> {\n let body: unknown = null\n try { body = await res.json() } catch { /* fall through */ }\n if (!res.ok) {\n const msg = (body && typeof body === 'object' && 'message' in body && typeof (body as { message: unknown }).message === 'string')\n ? (body as { message: string }).message\n : `request failed with ${res.status}`\n return { ok: false, status: res.status, message: msg, data: body }\n }\n return body as T\n }\n}\n\n/** Convenience one-shot client. */\nexport async function join(input: JoinInput, opts?: ClientOptions): Promise<Result<WaitlistEntry>> {\n return new WaitlistClient(opts).join(input)\n}\n\nexport async function status(input: StatusInput, opts?: ClientOptions): Promise<Result<WaitlistEntry>> {\n return new WaitlistClient(opts).status(input)\n}\n"],"mappings":";AA+LA,IAAa,IAAb,MAA4B;CAC1B;CACA;CACA;CAEA,YAAY,IAAsB,CAAC,GAAG;EAGpC,AAFA,KAAK,WAAW,EAAK,YAAY,OAAO,SAAW,MAAc,OAAO,SAAS,SAAS,IAAA,CAAK,QAAQ,OAAO,EAAE,GAChH,KAAK,UAAU,EAAK,cAAc,GAAA,CAAI,QAAQ,OAAO,EAAE,GACvD,KAAK,UAAU,EAAK,UAAU,OAAO,QAAU,MAAc,MAAM,KAAK,UAAU,WAAW;GAAE,MAAU,MAAM,wBAAwB;EAAE,EAAA,CAAG;CAC9I;CAEA,IAAY,GAAsB;EAChC,OAAO,GAAG,KAAK,UAAU,KAAK,OAAO,cAAc;CACrD;CAEA,MAAM,KAAK,GAAkD;EAC3D,IAAM,IAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,OAAO,GAAG;GAChD,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,CAAK;EAC5B,CAAC;EACD,OAAO,KAAK,MAAM,CAAG;CACvB;CAEA,MAAM,OAAO,GAAoD;EAC/D,IAAM,IAAI,IAAI,gBAAgB;GAAE,UAAU,EAAM;GAAU,OAAO,EAAM;EAAM,CAAC,GACxE,IAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,WAAW,GAAG,GAAG,EAAE,QAAQ,MAAM,CAAC;EAC1E,OAAO,KAAK,MAAM,CAAG;CACvB;CAEA,MAAM,KAAK,GAAoD;EAC7D,IAAM,IAAI,IAAI,gBAAgB,EAAE,UAAU,EAAM,SAAS,CAAC;EAE1D,AADI,EAAM,QAAM,EAAE,IAAI,QAAQ,OAAO,EAAM,IAAI,CAAC,GAC5C,EAAM,YAAU,EAAE,IAAI,YAAY,OAAO,EAAM,QAAQ,CAAC;EAC5D,IAAM,IAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,SAAS,GAAG,GAAG,EAAE,QAAQ,MAAM,CAAC;EACxE,OAAO,KAAK,aAA8B,CAAG;CAC/C;CAQA,MAAM,aAAa,GAAiE;EAClF,IAAM,IAAI,IAAI,gBAAgB;GAAE,UAAU,EAAM;GAAU,OAAO,EAAM;EAAM,CAAC;EAC9E,AAAI,EAAM,UAAQ,EAAE,IAAI,UAAU,OAAO,EAAM,MAAM,CAAC;EACtD,IAAM,IAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,iBAAiB,GAAG,GAAG,EAAE,QAAQ,MAAM,CAAC;EAChF,OAAO,KAAK,aAAmC,CAAG;CACpD;CAEA,MAAM,WAAW,GAA6D;EAC5E,IAAM,IAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,cAAc,GAAG;GACvD,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,CAAK;EAC5B,CAAC;EACD,OAAO,KAAK,aAAiC,CAAG;CAClD;CAEA,MAAM,OAAO,GAAqD;EAChE,IAAM,IAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,SAAS,GAAG;GAClD,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,CAAK;EAC5B,CAAC;EACD,OAAO,KAAK,aAA6B,CAAG;CAC9C;CAEA,MAAM,SAAS,GAAyD;EACtE,IAAM,IAAI,IAAI,gBAAgB,EAAE,UAAU,EAAM,SAAS,CAAC;EAE1D,AADI,EAAM,SAAO,EAAE,IAAI,SAAS,OAAO,EAAM,KAAK,CAAC,GAC/C,EAAM,OAAO,UAAQ,EAAE,IAAI,SAAS,EAAM,MAAM,KAAK,GAAG,CAAC;EAC7D,IAAM,IAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,aAAa,GAAG,GAAG,EAAE,QAAQ,MAAM,CAAC;EAC5E,OAAO,KAAK,aAA+B,CAAG;CAChD;CAOA,MAAM,UAAU,GAAkB,IAAmC,CAAC,GAAoB;EACxF,IAAM,IAAI,IAAI,gBAAgB,EAAE,YAAS,CAAC,GACpC,IAAkC,CAAC;EACzC,AAAI,EAAK,kBAAe,EAAQ,gBAAgB,EAAK;EACrD,IAAM,IAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,WAAW,GAAG,GAAG;GAAE,QAAQ;GAAO;EAAQ,CAAC;EACnF,IAAI,CAAC,EAAI,IAAI,MAAU,MAAM,kBAAkB,EAAI,OAAO,GAAG,MAAM,EAAI,KAAK,GAAG;EAC/E,OAAO,EAAI,KAAK;CAClB;CAEA,MAAc,MAAM,GAA+C;EACjE,OAAO,KAAK,aAA4B,CAAG;CAC7C;CAEA,MAAc,aAAgB,GAAmC;EAC/D,IAAI,IAAgB;EACpB,IAAI;GAAE,IAAO,MAAM,EAAI,KAAK;EAAE,QAAQ,CAAqB;EAC3D,IAAI,CAAC,EAAI,IAAI;GACX,IAAM,IAAO,KAAQ,OAAO,KAAS,YAAY,aAAa,KAAQ,OAAQ,EAA8B,WAAY,WACnH,EAA6B,UAC9B,uBAAuB,EAAI;GAC/B,OAAO;IAAE,IAAI;IAAO,QAAQ,EAAI;IAAQ,SAAS;IAAK,MAAM;GAAK;EACnE;EACA,OAAO;CACT;AACF;AAGA,eAAsB,EAAK,GAAkB,GAAsD;CACjG,OAAO,IAAI,EAAe,CAAI,CAAC,CAAC,KAAK,CAAK;AAC5C;AAEA,eAAsB,EAAO,GAAoB,GAAsD;CACrG,OAAO,IAAI,EAAe,CAAI,CAAC,CAAC,OAAO,CAAK;AAC9C"} |