Bluesky invite codes

Generate BSKY invite codes right from the command line

If you haven't heard of it while the birds on the social network with the well known bird logo are tweeting it 24/7: there's a new social network out there called "bluesky". Currently it's invite-only and provides an app for iOS, but the developers are working on a web client and native support for other platforms as well.

So what's the thing wih bluesky? For me personally, it is "just" another network that shows up. But from the technical point of view it's pretty interesting to be part of this, as nearly all the development is open sourced at bluesky's GitHub profile and as you can follow the changes on the code you see the impact on the look and feel of the platform and its behaviour. That's pretty impressive.

BSKY invite codes

As said beforehand, the bluesky platform is invite-only. So if you don't have an invite code, you are not able to join. Out there on the internet, the codes are treated like little gold nuggets: either you can win one in a lottery or you sell one of your kidneys in a dark corner of the internet to get one (there are some codes up on eBay for a good amount of dollars). But since nearly everything is open-sourced, you can look up the code how invite codes are generated. The code is written in TypeScript.

The anatomy of an invite code

Invite codes on bluesky looks like this: bsky.app-abc12. While this is just a demo code, we can clearly see the format. It can be splitted in two parts: the domain part (bsky.app in this case) and the random part (abc12). For the domain part, all . (dots) can be replaced with - (dash), so the codes bsky-app-abc12 and bsky.app-abc12 are the same. Since the main bluesky instance, and currently the only one, is bluesky itself, all invite codes out there start with their hostname bsky.social, followed by a -.

The random part are generated out of five (5) random bytes that are hashed with a base32 algorithm. The resulting string is longer than the five-digits that are represented in the code, so only the first five characters are taken for an invite code. Using base32 here instead of base64 is quite smart, as base32 isn't a mixture of uppercase and lowercase letters, like base64, so the resulting codes are case-insensitive and less error-prone.

Generating an invite code

As mentioned, the first part is bsky.social-. So only the last part needs to be generated. The original code is written in TypeScript with the use of some libraries, but since I don't want to download a kitchen sink full of npm packages, I was looking for a short and clean solution to generate an invite code without any dependencies. Lucky me, modern browsers and JavaScript runtimes have the tools at hand, so I picked a nodeJS 18 for playing around.

First of all, five random bytes are needed. To generate a Buffer of random bytes, the crypto API provides the function randomBytes:

let someRandomBytes = crypto.randomBytes(5);

The second step is more complicated, as the browsers out there and nodeJS doesn't have a native function to encode and decode base32. For base64 encoding and decoding one can use atob and btoa. On a side note when you're confused what's for decoding and for encoding: btoa is read as "binary-to-ascii" and is for encoding of binary data to base64-encoded ascii data. atob is read "ascii-to-binary" and is for decoding the given base64 string to binary data.

For base32 there's no such built-in functionality, so we have to dig a little bit deeper and check what base32 is about. The article on base32 in the Wikipedia is a good start. There are multiple flavors of base32 implementations, we stick to RFC4648. There are some libraries/packages out there that are able to do the job, as mentioned, but why have multiple dependencies and code downloaded from the outposts of the internet just to generate a short code that one can do in some simple steps:

  1. generate some random bytes (we already have done that)
  2. transform them one-by-one to their binary represention
  3. take the 8-digit-binary represenatons and concat them to a long string
  4. slice the string in parts of 5-digits
  5. transform the resulting 5-digit-parts back to a number to base 10
  6. pick the base32 alphabet character that belongs to this number
  7. concat the result characters
  8. pick the first five characters

Written in source code, these steps transforms to:

// step 1) generate some random bytes
let someRandomBytes = crypto.randomBytes(5);

// -- glue code: create an array out of the Buffer
let someRandomByteArray = Array.from(someRandomBytes);

// step 2) transform them one-by-one to binary
let binaryArray = someRandomByteArray.map((aNumber) => aNumber.toString(2));

// -- glue code: normalize the length of each binary item to 8
let binariesNormalized = binaryArray.map((aBinary) =>
  `${Array(8).join("0")}${aBinary}`.slice(-8)
);

// step 3) concatenate the binary chunks to a long string
let concatenatedBinaries = binariesNormalized.join("");

// step 4) slice the string in 5 digit parts
let fiveDigitParts = Array.from(concatenatedBinaries).reduce(
  (accumulator, _, index, source) => {
    if (index % 5 === 0) {
      accumulator.push(concatenatedBinaries(index, 5));
    }
    return accumulator;
  },
  []
);

// step 5) transform the binary chunks back to bytes
let byteArray = fiveDigitParts.map((binary) => parseInt(binary, 2));

// step 6) pick the corresponding character from the base32 alphabet
let characters = byteArray.map(
  (byte) => "abcdefghijklmnopqrstuvwxyz234567"[byte]
);

// step 7) concat the resulting characters
let codeTooLong = characters.join("");

// step 8) pick the first five characters
let code = codeTooLong.slice(0, 5);

// congratulations
console.log(`bsky.social-${code}`);

Congratulatons, you just created your bluesky invite code. You can make the source code even shorter, because arrays are chainable. Be aware that I'm not a big fan of writing such code in production, but as an exercise it's pretty cool to create a one-liner out of it:

let bsky = `bsky.social-${Array.from(
  Array.from(crypto.randomBytes(5))
    .map((x) =>
      (Array(8).join("0") + parseInt(String(x), 10).toString(2)).slice(-8)
    )
    .join("")
)
  .reduce((acc, _, i, arr) => {
    if (i % 5 === 0) {
      acc.push(arr.join("").substr(i, 5));
    }
    return acc;
  }, [])
  .map((x) => "abcdefghijklmnopqrstuvwxyz234567"[parseInt(x, 2)])
  .join("")
  .slice(0, 5)}`;

Note to self

While I've read a good bunch of the code I noticed that invite codes have some more information attached than just the code and meta data like the creation date. The bluesky invite system connects the following information to an invite code and their usage:

  • the code itself
  • who ceated the code
  • when the code was created
  • how many successful invitations are left for one invite code (can be more than one)
  • status of the code (disabled/enabled)
  • who used the code
  • when the code was used

Caution: for the last two bullet points you should decide on your own if you want to generate a million invite codes and try them on the platform to join or when you are already a user and post invite codes on the internet. Bluesky might be able to track down the social graph of invite codes to detect misusage or fraud, but as of now there's no evidence of this.

Just in case you want to give it a try, a good starting point for creating an account could be a cURL request.

curl 'https://bsky.social/xrpc/com.atproto.server.createAccount' \
  -H 'authority: bsky.social' \
  -H 'accept: */*' \
  -H 'accept-language: en-US,en;q=0.8' \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -H 'origin: https://staging.bsky.app' \
  -H 'pragma: no-cache' \
  -H 'referer: https://staging.bsky.app/' \
  -H 'sec-fetch-dest: empty' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-site: cross-site' \
  -H 'sec-gpc: 1' \
  -H 'user-agent: Terminal' \
  --data-raw '{"handle":"<your-handle-here>.bsky.social","password":"<your-password-here>","email":"<your-mail-address-here>","inviteCode":"<your-invite-code-here>"}' \
  --compressed

Updates

20230316: the format of the invite codes have changed and the length of the code part was increased from 5 (five) to 7 (seven)

20230427: the format of the invite codes changed again, the format of the code part is now 5 + 5 (five plus five) characters, based on 8 (eight) random bytes (RegEx: bsky-app-[a-z2-7]{5}-[a-z2-7]{5}).

Read my next post: Localize web extensions (and apps)