Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerifyResponse, { success: true }>,
) => {
Expand Down Expand Up @@ -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(
Expand Down
63 changes: 24 additions & 39 deletions tests/challenge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,60 +344,45 @@ 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");

stubFetch(
createResponse({
success: true,
flag: {
type: "pony",
code: "RD",
type: "pol",
code: "AC",
issuer: "flags.5chan.app",
signature: { signature: "signed-rd" },
signature: { signature: "signed-ac" },
},
}),
);
Expand Down
Loading