clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

invites: the claim door joins the invite code it reads, in invites.ts

Dami committed 5031958
commit
50319588895932a761dc05d0f18bcc9ee065a81c
parent
c93450af5b2911282696d5589edafd055b8ffb41
author
Dami <dami.etoile@gmail.com>
2 files changed, +28 -25
Msrc/invites.ts26++++++++++++++++++++++++++
Msrc/relay.ts27++-------------------------
M src/invites.ts +26 -0
@@ -4,6 +4,9 @@
44 import { bytesToHex } from "./negentropy.ts";55 import { page, escapeHTML } from "./ui.ts";66 import type { Settings } from "./settings.ts";7+import type { Relay } from "./relay.ts";8+import { verifyNIP98 } from "./auth.ts";9+import { now } from "./event.ts";710 811 export type Invite = {912 code: string;
@@ -134,3 +137,26 @@ p.lead { margin-top: 1.2rem; }
134137 </main>`;135138 return page("join " + relayName, body, css);136139 }140+141+// claimInviteRequest joins the NIP-98 signer through an invite code. It is142+// open to non-members by design; that is what an invite is for.143+export async function claimInviteRequest(relay: Relay, req: Request): Promise<Response> {144+ const json = (b: unknown, status = 200) => new Response(JSON.stringify(b), { status, headers: { "content-type": "application/json", "access-control-allow-origin": "*" } });145+ const body = await req.text();146+ const auth = verifyNIP98(req.headers.get("authorization") ?? "", req.url, req.method, body);147+ if (typeof auth === "string") return json({ error: auth }, 401);148+ let code = "";149+ try {150+ code = String((JSON.parse(body) as { code?: unknown }).code ?? "");151+ } catch {152+ return json({ error: "invalid: body is not JSON" }, 400);153+ }154+ if (relay.settings.policy.owner === "") return json({ error: "invite_invalid" }, 403);155+ if (relay.settings.isBanned(auth.pubkey)) return json({ error: "blocked: this pubkey is banned from this relay" }, 403);156+ if (relay.settings.isAllowed(auth.pubkey)) return json({ status: "already_member", role: relay.settings.isOwner(auth.pubkey) ? "owner" : "member" });157+ const r = claimInvite(relay.sql, code, now());158+ if (r !== "ok") return json({ error: r }, 403);159+ relay.settings.upsertMember(auth.pubkey, { via: "invite " + code.slice(0, 8), invitedBy: inviteCreator(relay.sql, code) }, now());160+ await relay.publishMembership({ pubkey: auth.pubkey, added: true });161+ return json({ status: "joined", role: "member" });162+}
M src/relay.ts +2 -25
@@ -25,7 +25,7 @@ import { dumpBytes, dumpDue, dumpDownload, writeDump } from "./dumps.ts";
2525 import { importBytes, importUpload } from "./imports.ts";2626 import { DIGEST_DAYS, digestText } from "./notify.ts";2727 import { claimFromProfile, nip05Document } from "./nip05.ts";28-import { checkInvite, claimInvite, inviteCreator, invitePage, termsPage } from "./invites.ts";28+import { checkInvite, claimInviteRequest, invitePage, termsPage } from "./invites.ts";2929 import { verifyNIP98, whoAsks } from "./auth.ts";3030 import { FAVICON_SVG } from "./ui.ts";3131 import type { PullFilter, PullJob, PullResult } from "./pull.ts";
@@ -824,29 +824,6 @@ export class Relay extends DurableObject<Env> {
824824 if (cur === null || cur > latest * 1000) await this.ctx.storage.setAlarm(latest * 1000);825825 }826826 827- // claimInviteRequest joins the NIP-98 signer through an invite code. It is828- // open to non-members by design; that is what an invite is for.829- private async claimInviteRequest(req: Request): Promise<Response> {830- const json = (b: unknown, status = 200) => new Response(JSON.stringify(b), { status, headers: { "content-type": "application/json", "access-control-allow-origin": "*" } });831- const body = await req.text();832- const auth = verifyNIP98(req.headers.get("authorization") ?? "", req.url, req.method, body);833- if (typeof auth === "string") return json({ error: auth }, 401);834- let code = "";835- try {836- code = String((JSON.parse(body) as { code?: unknown }).code ?? "");837- } catch {838- return json({ error: "invalid: body is not JSON" }, 400);839- }840- if (this.settings.policy.owner === "") return json({ error: "invite_invalid" }, 403);841- if (this.settings.isBanned(auth.pubkey)) return json({ error: "blocked: this pubkey is banned from this relay" }, 403);842- if (this.settings.isAllowed(auth.pubkey)) return json({ status: "already_member", role: this.settings.isOwner(auth.pubkey) ? "owner" : "member" });843- const r = claimInvite(this.sql, code, now());844- if (r !== "ok") return json({ error: r }, 403);845- this.settings.upsertMember(auth.pubkey, { via: "invite " + code.slice(0, 8), invitedBy: inviteCreator(this.sql, code) }, now());846- await this.publishMembership({ pubkey: auth.pubkey, added: true });847- return json({ status: "joined", role: "member" });848- }849-850827 async fetch(req: Request): Promise<Response> {851828 this.touch();852829 const name = req.headers.get("x-relay-name");
@@ -900,7 +877,7 @@ export class Relay extends DurableObject<Env> {
900877 if (url.pathname === "/api/join-policy" && req.method === "GET") {901878 return Response.json({ terms: this.settings.policy.joinTerms }, { headers: { "access-control-allow-origin": "*" } });902879 }903- if (url.pathname === "/api/invites/claim" && req.method === "POST") return this.claimInviteRequest(req);880+ if (url.pathname === "/api/invites/claim" && req.method === "POST") return claimInviteRequest(this, req);904881 if (url.pathname.startsWith("/dumps/") && req.method === "GET") return dumpDownload(this, req);905882 if (url.pathname.startsWith("/view/") && req.method === "GET") return serveView(this, req, verifyNIP98);906883 if (url.pathname === "/import" && req.method === "PUT") return importUpload(this, req);