Skip to content
colibri-socialPublic

About

A labeler to apply badges to users.

Resources

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

9 Commits

Folders and files

Repository files navigation

colibri-labeler

A standalone AT Protocol labeler service that hands out badges to Colibri users. Badges are ordinary com.atproto.label labels, signed with the labeler's #atproto_label key and served over the standard query and firehose endpoints.

Badges like team and play-store-tester are granted by hand with pnpm label. Supporter badges are granted automatically once a user links their Open Collective account, see Supporter badges from Open Collective.

One-time setup

  1. Create a fresh atproto account dedicated to this labeler. Do not use an existing personal or Colibri account.
  2. Add the #atproto_label verification method and the #atproto_labeler service endpoint to its DID document. This is a PLC operation and only needs doing once.
  3. Copy .env.example to .env and fill it in. LABELER_LABEL_PUBLIC_KEY is worth setting: the server then refuses to start if LABELER_SIGNING_KEY does not derive to the key in the DID document, which otherwise fails silently by writing labels nobody can verify.
  4. Publish the declaration record:
    pnpm install
    pnpm declare

Running

pnpm dev                   # tsx watch, for local development
pnpm build && pnpm start   # production
pnpm migrate               # apply pending database migrations on their own
pnpm test                  # vitest

LABELER_DB_URL accepts a remote libSQL URL (LABELER_DB_TOKEN then required) or a local file: path, which is what the tests use.

The service must be reachable at a stable public HTTPS URL matching the #atproto_labeler service endpoint, reverse-proxied with TLS.

Assigning a badge

pnpm label

The TUI resolves a handle or DID, picks a badge (or a custom value), grants or revokes it, and confirms before applying. It writes to the database directly. The running server picks those writes up by polling and forwards them to subscribeLabels subscribers, so a badge granted this way still reaches the firehose.

Revoking appends a negation row rather than deleting anything. Consumers resolve the current state by taking the newest cts per label value and dropping negated and expired ones.

Supporter badges from Open Collective

Supporter badges (sponsor-twenty-five, supporter-ten, backer-five, donator) are handed out without anyone running pnpm label, once the LABELER_OC_* block in .env is filled in. Set all of LABELER_PUBLIC_URL, LABELER_OC_CLIENT_ID, LABELER_OC_CLIENT_SECRET, LABELER_OC_STATE_SECRET, LABELER_HANDLE and LABELER_APP_PASSWORD, or none of them. A partial configuration is refused at boot rather than half-enabled.

Register the OAuth app at https://opencollective.com/<collective>/admin/for-developers with <LABELER_PUBLIC_URL>/oc/callback as the redirect.

How a link happens:

  1. The client calls social.colibri.labeler.linkExternalAccount with service auth. The JWT's issuer is the subject, so an account can only ever link itself. This is deliberately not gated on LABELER_ADMIN_DIDS.
  2. The labeler answers with an Open Collective authorization URL whose state is an HMAC-signed, ten-minute token carrying the subject DID. Nothing is stored at this point.
  3. Open Collective redirects to /oc/callback. The labeler verifies the state, exchanges the code, reads me, and throws the access token away. No token is persisted and no refresh token is kept.
  4. It publishes a social.colibri.labeler.attestation record keyed by the subject DID, then evaluates that subject's entitlements immediately so the badge appears at once.

The attestation record is what makes this work without a mapping table. It lives in the labeler's own repo, so it is public, auditable, and replicated like any other record.

Reading contributions needs no authentication at all: the collective's orders, including each contributor's account id, are public. The access token from step 3 is only ever used to prove which Open Collective account the user controls.

Entitlements are recomputed every LABELER_OC_SYNC_SECS (default 900):

Contribution Badge
active recurring, 2500 cents/month or more sponsor-twenty-five
active recurring, 1000 cents/month or more supporter-ten
active recurring, 500 cents/month or more backer-five
any paid one-time contribution donator

Amounts drive this, not Open Collective tiers. The collective has only a $5 and a $25 tier, both flexible, so a $10 contributor arrives on the $5 tier at a custom amount or with no tier at all. Matching on tier would quietly underpay them. Yearly contributions are divided by 12, several active contributions are summed, and anything not in LABELER_OC_CURRENCY is skipped because there is no conversion source.

A lapse is handled twice over. Each pass reissues the badge with exp set to the next charge date plus LABELER_OC_GRACE_DAYS, so badges expire on their own if the loop ever stops running, and a contribution that goes inactive also gets an immediate negation row. donator is permanent: it never expires and reconciliation never revokes it, though unlinking does.

Guest profiles cannot link, and need no manual auditing. A guest profile has no credentials, so it cannot complete OAuth in the first place. Claiming one means logging in with the email used for the contribution, which converts the profile in place and keeps the contribution attached, so anyone who does reach the callback is already a claimed account. The callback rejects isGuest accounts anyway, and each pass logs how many contributions came from unclaimed guest profiles. A guest who never claims theirs is the one case still needing pnpm label.

Reading badges

  • com.atproto.label.queryLabels: HTTP query. uriPatterns is a repeated parameter, so one request can ask about many subjects at once, which is how the Colibri client avoids a request per member. See colibri.social/packages/client/src/atproto/labeler-lookup.ts.
  • com.atproto.label.subscribeLabels: WebSocket firehose, with cursor backfill.
  • tools.ozone.moderation.emitEvent: authenticated write path, kept for compatibility. Requires a service JWT whose issuer is the labeler account or a DID listed in LABELER_ADMIN_DIDS.
  • social.colibri.labeler.attestation: read the labeler's repo to see which subjects have a verified external account.

About

A labeler to apply badges to users.

Resources

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages