From 824287e26685afdbe14912e5b8ca17c0a5597a70 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Mon, 8 Jun 2026 13:54:07 +0700 Subject: [PATCH] fix(flags): skip issuer iframe for board-choice flags --- README.md | 10 ++++--- src/index.ts | 13 +++++++++ tests/challenge.test.ts | 63 ++++++++++++++++------------------------- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 2f549c7..b721834 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ Verified flag issuer challenge for Bitsocial communities and clients. -This package runs on a Bitsocial community owner node as a challenge. It is not specific to 5chan: any Bitsocial client can use the same challenge pattern with its own issuer service, namespace, and flag profile. The first bundled profile is the 5chan profile, issued by `flags.5chan.app`, for country flags, `/pol/` memeflags, and `/mlp/` pony flags. +This package runs on a Bitsocial community owner node as a challenge. It is not specific to 5chan: any Bitsocial client can use the same challenge pattern with its own issuer service, namespace, and flag profile. The first bundled profile is the 5chan profile. Country flags are issued by `flags.5chan.app`; `/pol/` memeflags and `/mlp/` pony flags are validated as board-choice flairs without an issuer iframe. -The challenge writes two pieces of data when a flag is verified: +For issuer-verified country flags, the challenge writes two pieces of data: - immutable comment data under a client namespace such as `comment["5chan"]`; - a compatibility mirror under `commentUpdate.author.community.flairs`, so clients that already render author flairs can show the same flag. +For `/pol/` memeflags and `/mlp/` pony flags, clients should publish the selected flag as normal comment flair data. The challenge validates that the requested family is allowed and that the code is known, then accepts the publication without contacting the issuer service. + The namespace is configurable. For example, a future Seedit profile could write to `comment["seedit"]` while using its own issuer and flag metadata. ## Installation @@ -83,7 +85,7 @@ Supported 5chan request strings: - `flag:pol:AC` - `flag:pony:AJ` -Country flags use `auto` because the issuer service must derive the country from the challenge iframe request IP. The challenge does not trust a client-provided country code as proof of location. +Country flags use `auto` because the issuer service must derive the country from the challenge iframe request IP. The challenge does not trust a client-provided country code as proof of location. `/pol/` memeflags and `/mlp/` pony flags are free user choices, so they do not require the iframe flow. ## Verified Comment Shape @@ -120,7 +122,7 @@ For a country flag, the challenge returns a result like: } ``` -For `/pol/` memeflags and `/mlp/` pony flags, `comment["5chan"].flag` carries the selected flag and `comment["5chan"].memeflag` or `comment["5chan"].pony` carries the short code. +For `/pol/` memeflags and `/mlp/` pony flags, clients render the selected flag from the comment flair fields they publish. The challenge does not add a signed `comment["5chan"]` assertion for those board-choice flags. ## Issuer Service Contract diff --git a/src/index.ts b/src/index.ts index 113de27..50311cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -318,6 +318,9 @@ const allow = (): ChallengeResultInput => ({ success: true }); const isAllowed = (flag: RequestedFlag, options: ParsedOptions) => options.allowedFlags.includes(flag.type); +const requiresIssuerVerification = (flag: RequestedFlag) => + flag.type === "country"; + const getVerifyResponseFlagInput = ( response: Extract, ) => { @@ -459,6 +462,16 @@ const getChallenge = async ( return reject(options, `Flag family ${requestedFlag.type} is not allowed.`); } + if (!requiresIssuerVerification(requestedFlag)) { + logInfo( + "Requested board-choice flag does not require issuer iframe; allowing. community=%s source=%s flag=%o", + communityLabel, + requestedFlagSource, + summarizeFlag(requestedFlag), + ); + return allow(); + } + const signer = runtimeCommunity?.signer; if (!signer) { logError( diff --git a/tests/challenge.test.ts b/tests/challenge.test.ts index 2f5e80a..892a613 100644 --- a/tests/challenge.test.ts +++ b/tests/challenge.test.ts @@ -344,49 +344,34 @@ describe("Bitsocial flags challenge package", () => { }); }); - it("emits signed 5chan memeflag data", async () => { + it("accepts valid memeflags without an issuer iframe", async () => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + const result = await runChallenge(createCommentRequest("flag:pol:AC")); - if (!("verify" in result)) throw new Error("Expected verify callback"); + expect(result).toEqual({ success: true }); + expect(fetchMock).not.toHaveBeenCalled(); + }); - stubFetch( - createResponse({ - success: true, - flag: { - type: "pol", - code: "AC", - issuer: "flags.5chan.app", - issuedAt: 1770000100, - signature: { signature: "signed-ac" }, - }, - }), + it("accepts valid pony flags without an issuer iframe or community signer", async () => { + const result = await runChallenge( + createReplyRequest([{ text: "flag:pony:AJ" }]), + {}, + { address: "pony-posting.bso" }, ); - await expect(result.verify("")).resolves.toMatchObject({ - success: true, - comment: { - "5chan": { - memeflag: "AC", - flag: { - type: "pol", - code: "AC", - label: "Anarcho-Capitalist", - }, - signature: { signature: "signed-ac" }, - }, - }, - commentUpdate: { - author: { - community: { - flairs: [{ text: "flag:pol:AC", type: "pol", code: "AC" }], - }, - }, - }, - }); + expect(result).toEqual({ success: true }); }); - it("rejects service responses that do not match the requested flag", async () => { - const result = await runChallenge(createCommentRequest("flag:pony:AJ")); + it("rejects service responses that do not match a requested country flag", async () => { + const result = await runChallenge( + createCommentRequest({ + type: "country", + code: "auto", + text: "flag:country:auto", + }), + ); if (!("verify" in result)) throw new Error("Expected verify callback"); @@ -394,10 +379,10 @@ describe("Bitsocial flags challenge package", () => { createResponse({ success: true, flag: { - type: "pony", - code: "RD", + type: "pol", + code: "AC", issuer: "flags.5chan.app", - signature: { signature: "signed-rd" }, + signature: { signature: "signed-ac" }, }, }), );