├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js └── index.js ├── public ├── histogram.csv ├── histogram.png └── randogram.png ├── src ├── collision-test.js ├── histogram.js ├── index-test.js ├── index.js └── test-utils.js ├── styles └── globals.css ├── tea.yaml └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:prettier/recommended" 5 | ], 6 | "parserOptions": { 7 | "sourceType": "module", 8 | "ecmaVersion": 2022 9 | }, 10 | "rules": { 11 | "no-unused-vars": "error" 12 | } 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 16 4 | install: 5 | - npm install --legacy-peer-deps 6 | before_script: 7 | - export NODE_OPTIONS=–max_old_space_size=8192 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eric Elliott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cuid2 2 | 3 | Secure, collision-resistant ids optimized for horizontal scaling and performance. Next generation UUIDs. 4 | 5 | Need unique ids in your app? Forget UUIDs and GUIDs which often collide in large apps. Use Cuid2, instead. 6 | 7 | **Cuid2 is:** 8 | 9 | * **Secure:** It's not feasible to guess the next id, existing valid ids, or learn anything about the referenced data from the id. Cuid2 uses multiple, independent entropy sources and hashes them with a security-audited, NIST-standard cryptographically secure hashing algorithm (Sha3). 10 | * **Collision resistant:** It's extremely unlikely to generate the same id twice (by default, you'd need to generate roughly 4,000,000,000,000,000,000 ids ([`sqrt(36^(24-1) * 26) = 4.0268498e+18`](https://en.wikipedia.org/wiki/Birthday_problem#Square_approximation)) to reach 50% chance of collision.) 11 | * **Horizontally scalable:** Generate ids on multiple machines without coordination. 12 | * **Offline-compatible:** Generate ids without a network connection. 13 | * **URL and name-friendly:** No special characters. 14 | * **Fast and convenient:** No async operations. Won't introduce user-noticeable delays. Less than 5k, gzipped. 15 | * **But not *too fast*:** If you can hash too quickly you can launch parallel attacks to find duplicates or break entropy-hiding. For unique ids, the fastest runner loses the security race. 16 | 17 | 18 | **Cuid2 is not good for:** 19 | 20 | * Sequential ids (see the [note on K-sortable ids](https://github.com/paralleldrive/cuid2#note-on-k-sortablesequentialmonotonically-increasing-ids), below) 21 | * High performance tight loops, such as render loops (if you don't need cross-host unique ids or security, consider a simple counter for this use-case, or try [Ulid](https://github.com/ulid/javascript) or [NanoId](https://github.com/ai/nanoid)). 22 | 23 | 24 | ## Getting Started 25 | 26 | ``` 27 | npm install --save @paralleldrive/cuid2 28 | ``` 29 | 30 | Or 31 | 32 | ``` 33 | yarn add @paralleldrive/cuid2 34 | ``` 35 | 36 | ```js 37 | import { createId } from '@paralleldrive/cuid2'; 38 | 39 | const ids = [ 40 | createId(), // 'tz4a98xxat96iws9zmbrgj3a' 41 | createId(), // 'pfh0haxfpzowht3oi213cqos' 42 | createId(), // 'nc6bzmkmd014706rfda898to' 43 | ]; 44 | ``` 45 | 46 | Using Jest? Jump to [Using with Jest](#using-in-jest). 47 | 48 | ### Configuration 49 | 50 | ```js 51 | import { init } from '@paralleldrive/cuid2'; 52 | 53 | // The init function returns a custom createId function with the specified 54 | // configuration. All configuration properties are optional. 55 | const createId = init({ 56 | // A custom random function with the same API as Math.random. 57 | // You can use this to pass a cryptographically secure random function. 58 | random: Math.random, 59 | // the length of the id 60 | length: 10, 61 | // A custom fingerprint for the host environment. This is used to help 62 | // prevent collisions when generating ids in a distributed system. 63 | fingerprint: 'a-custom-host-fingerprint', 64 | }); 65 | 66 | console.log( 67 | createId(), // wjfazn7qnd 68 | createId(), // cerhuy9499 69 | createId(), // itp2u4ozr4 70 | ); 71 | ``` 72 | 73 | 74 | ### Validation 75 | 76 | ```js 77 | import { createId, isCuid } from '@paralleldrive/cuid2'; 78 | 79 | 80 | console.log( 81 | isCuid(createId()), // true 82 | isCuid('not a cuid'), // false 83 | ); 84 | ``` 85 | 86 | 87 | ## Trusted By 88 | 89 | * [Greenruhm](https://twitter.com/greenruhm) 90 | * [Typebot](https://typebot.io/) 91 | * [Submit my project](https://github.com/paralleldrive/cuid2/issues/new?title=Social+proof) 92 | 93 | ## Why? 94 | 95 | Ids should be secure by default for the same reason that browser sessions should be secure by default. There are too many things that can go wrong when they're not, and insecure ids can cause problems in unexpected ways, including [unauthorized user](https://www.intruder.io/research/in-guid-we-trust) [account access](https://infosecwriteups.com/bugbounty-how-i-was-able-to-compromise-any-user-account-via-reset-password-functionality-a11bb5f863b3), [unauthorized access to user data](https://infosecwriteups.com/how-this-easy-vulnerability-resulted-in-a-20-000-bug-bounty-from-gitlab-d9dc9312c10a), and accidental leaks of user's personal data which can lead to catastrophic effects, even in innocent-sounding applications like fitness run trackers (see the [2018 Strava Pentagon breach](https://www.engadget.com/2018-02-02-strava-s-fitness-heatmaps-are-a-potential-catastrophe.html) and [PleaseRobMe](https://pleaserobme.com/why)). 96 | 97 | Not all security measures should be considered equal. For example, it's not a good idea to trust your browser's "Cryptographically Secure" Psuedo Random Number Generator (CSPRNG) (used in tools like `uuid` and `nanoid`). For example, there may be [bugs in browser CSPRNGs](https://bugs.chromium.org/p/chromium/issues/detail?id=552749). For many years, Chromium's `Math.random()` [wasn't very random at all](https://thenextweb.com/news/google-chromes-javascript-engine-finally-returns-actual-random-numbers). Cuid was created to solve the issue of untrustworthy entropy in id generators that led to frequent id collisions and related problems in production applications. Instead of trusting a single source of entropy, Cuid2 combines several sources of entropy to provide stronger security and collision-resistance guarantees than are available in other solutions. 98 | 99 | Modern web applications have different requirements than applications written in the early days of GUID (globally unique identifiers) and UUIDs (universally unique identifiers). In particular, Cuid2 aims to provide stronger uniqueness guarantees than any existing GUID or UUID implementation and protect against leaking any information about the data being referenced, or the system that generated the id. 100 | 101 | Cuid2 is the next generation of Cuid, which has been used in thousands of applications for over a decade with no confirmed collision reports. The changes in Cuid2 are significant and could potentially disrupt the many projects that rely on Cuid, so we decided to create a replacement library and id standard, instead. Cuid is now deprecated in favor of Cuid2. 102 | 103 | Entropy is a measure of the total information in a system. In the context of unique ids, a higher entropy will lead to fewer collisions, and can also make it more difficult for an attacker to guess a valid id. 104 | 105 | Cuid2 is made up of the following entropy sources: 106 | 107 | * An initial letter to make the id a usable identifier in JavaScript and HTML/CSS 108 | * The current system time 109 | * Pseudorandom values 110 | * A session counter 111 | * A host fingerprint 112 | 113 | The string is Base36 encoded, which means it contains only lowercase letters and the numbers: 0 - 9, with no special symbols. 114 | 115 | 116 | ### Horizontal scalability 117 | 118 | Today's applications don't run on any single machine. 119 | 120 | Applications might need to support online / offline capability, which means we need a way for clients on different hosts to generate ids that won't collide with ids generated by other hosts -- even if they're not connected to the network. 121 | 122 | Most pseudo-random algorithms use time in ms as a random seed. Random IDs lack sufficient entropy when running in separate processes (such as cloned virtual machines or client browsers) to guarantee against collisions. Application developers report v4 UUID collisions causing problems in their applications when the ID generation is distributed between lots of machines such that lots of IDs are generated in the same millisecond. 123 | 124 | Each new client exponentially increases the chance of collision in the same way that each new character in a random string exponentially reduces the chance of collision. Successful apps scale at hundreds or thousands of new clients per day, so fighting the lack of entropy by adding random characters is a recipe for ridiculously long identifiers. 125 | 126 | Because of the nature of this problem, it's possible to build an app from the ground up and scale it to a million users before this problem is detected. By the time you notice the problem (when your peak hour use requires dozens of ids to be created per ms), if your db doesn't have unique constraints on the id because you thought your guids were safe, you're in a world of hurt. Your users start to see data that doesn't belong to them because the db just returns the first ID match it finds. 127 | 128 | Alternatively, you've played it safe and you only let your database create ids. Writes only happen on a master database, and load is spread out over read replicas. But with this kind of strain, you have to start scaling your database writes horizontally, too, and suddenly your application starts to crawl (if the db is smart enough to guarantee unique ids between write hosts), or you start getting id collisions between different db hosts, so your write hosts don't agree about which ids represent which data. 129 | 130 | 131 | ### Performance 132 | 133 | Id generation should be fast enough that humans won't notice a delay, but too slow to feasibly brute force (even in parallel). That means no waiting around for asynchronous entropy pool requests, or cross-process/cross-network communication. Performance slows to impracticality in the browser. All sources of entropy need to be fast enough for synchronous access. 134 | 135 | Even worse, when the database is the only guarantee that ids are unique, that means that clients are forced to send incomplete records to the database, and wait for a network round-trip before they can use the ids in any algorithm. Forget about fast client performance. It simply isn't possible. 136 | 137 | That situation has caused some clients to create ids that are only usable in a single client session (such as an in-memory counter). When the database returns the real id, the client has to do some juggling logic to swap out the id being used, adding complexity to the client implementation code. 138 | 139 | If client side ID generation were stronger, the chances of collision would be much smaller, and the client could send complete records to the db for insertion without waiting for a full round-trip request to finish before using the ID. 140 | 141 | 142 | #### Tiny 143 | 144 | Page loads need to be FAST, and that means we can't waste a lot of JavaScript on a complex algorithm. Cuid2 is tiny. This is especially important for thick-client JavaScript applications. 145 | 146 | 147 | ### Secure 148 | 149 | Client-visible ids often need to have sufficient random data and entropy to make it practically impossible to try to guess valid IDs based on an existing, known id. That makes simple sequential ids unusable in the context of client-side generated database keys. Additionally, using V4 UUIDs is also not safe, because there are known attacks on several id generating algorithms that a sophisticated attacker can use to predict next ids. Cuid2 has been audited by security experts and artificial intelligence, and is considered safe to use for use-cases like secret sharing links. 150 | 151 | 152 | ### Portable 153 | 154 | Most stronger forms of the UUID / GUID algorithms require access to OS services that are not available in browsers, meaning that they are impossible to implement as specified. Further, our id standard needs to be portable to many languages (the original cuid has 22 different language implementations). 155 | 156 | #### Ports 157 | 158 | * [Cuid2 for Clojure](https://github.com/hden/cuid2) - [Haokang Den](https://github.com/hden) 159 | * [Cuid2 for ColdFusion](https://github.com/bennadel/CUID2-For-ColdFusion) - [Ben Nadel](https://github.com/bennadel) 160 | * [Cuid2 for Dart](https://github.com/obsidiaHQ/cuid2) - [George Mamar](https://github.com/obsidiaHQ) 161 | * [Cuid2 for Java](https://github.com/thibaultmeyer/cuid-java) - [Thibault Meyer](https://github.com/thibaultmeyer) 162 | * [Cuid2 for .NET](https://github.com/visus-io/cuid.net) - [Visus](https://github.com/xaevik) 163 | * [Cuid2 for PHP](https://github.com/visus-io/php-cuid2) - [Visus](https://github.com/xaevik) 164 | * [Cuid2 for Python](https://github.com/gordon-code/cuid2) - [Gordon Code](https://github.com/gordon-code) 165 | * [Cuid2 for Ruby](https://github.com/stulzer/cuid2/blob/main/lib/cuid2.rb) - [Rubens Stulzer](https://github.com/stulzer) 166 | * [Cuid2 for Rust](https://github.com/mplanchard/cuid-rust) - [Matthew Planchard](https://github.com/mplanchard) 167 | 168 | ## Improvements Over Cuid 169 | 170 | The original Cuid served us well for more than a decade. We used it across 2 different social networks, and to generate ids for Adobe Creative Cloud. We never had a problem with collisions in production systems using it. But there was room for improvement. 171 | 172 | ### Better Collision Resistance 173 | 174 | Available entropy is the maximum number of unique ids that can be generated. Generally more entropy leads to lower probability of collision. For simplicity, we will assume a perfectly random distribution in the following discussion. 175 | 176 | The original Cuid ran for more than 10 years in across thousands of software implementations with zero confirmed collision reports, in some cases with more than 100 million users generating ids. 177 | 178 | The original Cuid had a maximum available entropy of about `3.71319E+29` (assuming 1 id per session). That's already a really big number, but the maximum recommended entropy in Cuid2 is `4.57458E+49`. For reference, that's about the same entropy difference as the size of a mosquito compared to the distance from earth to the nearest star. Cuid2 has a default entropy of `1.62155E+37`, which is a significant increase from the original Cuid and is comparable to the difference between the size of a baseball and the size of the moon. 179 | 180 | The hashing function mixes all the sources of entropy together into a single value, so it's important we use a high quality hashing algorithm. We have tested billions of ids with Cuid2 with zero collisions detected to-date. 181 | 182 | 183 | ### More Portable 184 | 185 | The original Cuid used different methods to generate fingerprints across different kinds of hosts, including browsers, Node, and React Native. Unfortunately, this caused several compatability problems across the cuid user ecosystem. 186 | 187 | In Node, each production host was slightly different, and we could reliable grab process ids and such to differentiate between hosts. Our early assumptions about different hosts generating different PIDs in Node proved to be false when we started deploying on cloud virtual hosts using identical containers and micro-container architectures. The result was that host fingerprint entropy was low in Node, limiting their ability to provide good collision resistance for horizontal server scaling in environments like cloud workers and micro-containers. 188 | 189 | It was also not possible to customize your fingerprint function if you had different fingerprinting needs using Cuid, e.g., if both `global` and `window` are `undefined`. 190 | 191 | Cuid2 uses a list of all global names in the JavaScript environment. Hashing it produces a very good host fingerprint, but we intentionally did not include a hash function in the original Cuid because all the secure ones we could find would bloat the bundle, so the original Cuid was unable to take full advantage of all of that unique host entropy. 192 | 193 | In Cuid2, we use a tiny, fast, security-audited, NIST-standardized hash function and we seed it with random entropy, so on production environments where the globals are all identical, we lose the unique fingerprint, but still get random entropy to replace it, strengthening collision resistance. 194 | 195 | 196 | ### Deterministic Length 197 | 198 | Length was non-deterministic in Cuid. This worked fine in almost all cases, but proved to be problematic for some data structure uses, forcing some users to create wrapper code to pad the output. We recommend sticking to the defaults for most cases, but if you don't need strong uniqueness guarantees, (e.g., your use-case is something like username or URL disambiguation), it can be fine to use a shorter version. 199 | 200 | 201 | ### More Efficient Session Counter Entropy 202 | 203 | The original Cuid wasted entropy on session counters that were not always used, rarely filled, and sometimes rolled over, meaning they could collide with each other if you generate enough ids in a tight loop, reducing their effectiveness. Cuid2 initializes the counter with a random number so the entropy is never wasted. It also uses the full precision of the native JS number type. If you only generate a single id, the counter just extends the random entropy, rather than wasting digits, providing even stronger anti-collision protection. 204 | 205 | 206 | ### Parameterized Length 207 | 208 | Different use-cases have different needs for entropy resistance. Sometimes, a short disambiguating series of random digits is enough: for example, it's common to use short slugs to disambiguate similar names, e.g. usernames or URL slugs. Since the original cuid did not hash its output, we had to make some seriously limiting entropy decisions to produce a short slug. In the new version, all sources of entropy are mixed with the hash function, and you can safely grab a substring of any length shorter than 32 digits. You can roughly estimate how many ids you can generate before reaching 50% chance of collision with: [`sqrt(36^(n-1)*26)`](https://en.wikipedia.org/wiki/Birthday_problem#Square_approximation), so if you use `4` digits, you'll reach 50% chance of collision after generating only `~1101` ids. That might be fine for username disambiguation. Are there more than 1k people who want to share the same username? 209 | 210 | By default, you'd need to generate `~4.0268498e+18` ids to reach a 50% chance of collision, and at maximum length, you'd need to generate `~6.7635614e+24` ids to reach 50% odds of collision. To use a custom length, import the `init` function, which takes configuration options: 211 | 212 | ```js 213 | import { init } from '@paralleldrive/cuid2'; 214 | const length = 10; // 50% odds of collision after ~51,386,368 ids 215 | const cuid = init({ length }); 216 | console.log(cuid()); // nw8zzfaa4v 217 | ``` 218 | 219 | 220 | ### Enhanced Security 221 | 222 | The original Cuid leaked details about the id, including very limited data from the host environment (via the host fingerprint), and the exact time that the id was created. The new Cuid2 hashes all sources of entropy into a random-looking string. 223 | 224 | Due to the hashing algorithm, it should not be practically possible to recover any of the entropy sources from the generated ids. Cuid used roughly monotonically increasing ids for database performance reasons. Some people abused them to select data by creation date. If you want to be able to sort items by creation date, we recommend making a separate, indexed `createdAt` field in your database instead of using monotonic ids because: 225 | 226 | * It's easy to trick a client system to generate ids in the past or future. 227 | * Order is not guaranteed across multiple hosts generating ids at nearly the same time. 228 | * Deterministically monotic resolution was never guaranteed. 229 | 230 | In Cuid2, the hashing algorithm uses a salt. The salt is a random string which is added to the input entropy sources before the hashing function is applied. This makes it much more difficult for an attacker to guess valid ids, as the salt changes with each id, meaning the attacker is unable to use any existing ids as a basis for guessing others. 231 | 232 | 233 | ## Comparisons 234 | 235 | Security was the primary motivation for creating Cuid2. Our ids should be default-secure for the same reason we use https instead of http. The problem is, all our current id specifications are based on decades-old standards that were never designed with security in mind, optimizing for database performance charactaristics which are no longer relevant in modern, distributed applications. Almost all of the popular ids today optimize for being k-sortable, which was important 10 years ago. Here's what k-sortable means, and why it's no longer as important is it was when we created the Cuid specification which [helped inspire current standards like UUID v6 - v8](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-00.html#section-11.2): 236 | 237 | 238 | ### Note on K-Sortable/Sequential/Monotonically Increasing Ids 239 | 240 | TL;DR: Stop worrying about K-Sortable ids. They're not a big deal anymore. Use `createdAt` fields instead. 241 | 242 | **The performance impact of using sequential keys in modern systems is often exaggerated.** If your database is too small to use cloud-native solutions, it's also too small to worry about the performance impact of sequential vs random ids unless you're living in the distant past (i.e. you're using hardware from 2010). If it's large enough to worry, random ids may still be faster. 243 | 244 | In the past, sequential keys could potentially have a significant impact on performance, but this is no longer the case in modern systems. 245 | 246 | One reason for using sequential keys is to avoid id fragmentation, which can require a large amount of disk space for databases with billions of records. However, at such a large scale, modern systems often use cloud-native databases that are designed to handle terabytes of data efficiently and at a low cost. Additionally, the entire database may be stored in memory, providing fast random-access lookup performance. Therefore, the impact of fragmented keys on performance is minimal. 247 | 248 | Worse, K-Sortable ids are not always a good thing for performance anyway, because they can cause hotspots in the database. If you have a system that generates a large number of ids in a short period of time, the ids will be generated in a sequential order, causing the tree to become unbalanced, which will lead to frequent rebalancing. This can cause a significant performance impact. 249 | 250 | So what kinds of operations suffer from a non-sequential id? Paged, sorted operations. Stuff like "fetch me 100000 records, sorted by id". That would be noticeably impacted, but how often do you need to sort by id if your id is opaque? I have never needed to. Modern cloud databases allow you to create indexes on `createdAt` fields which perform extremely well. 251 | 252 | The worst part of K-Sortable ids is their impact on security. K-Sortable = insecure. 253 | 254 | 255 | ### The Contenders 256 | 257 | We're unaware of any standard or library in the space that adequately meets all of our requirements. We'll use the following criteria to filter a list of commonly used alternatives. Let's start with the contenders: 258 | 259 | Database increment (Int, BigInt, AutoIncrement), [UUID v1 - v8](https://www.ietf.org/archive/id/draft-ietf-UUIDrev-rfc4122bis-00.html), [NanoId](https://github.com/ai/nanoid), [Ulid](https://github.com/ulid/javascript), [Sony Snowflake](https://github.com/sony/sonyflake) (inspired by Twitter Snowflake), [ShardingID](https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c) (Instagram), [KSUID](https://github.com/segmentio/ksuid), [PushId](https://firebase.blog/posts/2015/02/the-2120-ways-to-ensure-unique_68) (Google), [XID](https://github.com/rs/xid), [ObjectId](https://www.mongodb.com/docs/manual/reference/method/ObjectId/) (MongoDB). 260 | 261 | Here are the disqualifiers we care about: 262 | 263 | * **Leaks information:** Database auto-increment, all UUIDs (except V4 and including V6 - V8), Ulid, Snowflake, ShardingId, pushId, ObjectId, KSUID 264 | * **Collision Prone:** Database auto-increment, v4 UUID 265 | * **Not cryptographically secure random output:** Database auto-increment, UUID v1, UUID v4 266 | * **Requires distributed coordination:** Snowflake, ShardingID, database increment 267 | * **Not URL or name friendly:** UUID (too long, dashes), Ulid (too long), UUID v7 (too long) - anything else that supports special characters like dashes, spaces, underscores, #$%^&, etc. 268 | * **Too fast:** UUID v1, UUID v4, NanoId, Ulid, Xid 269 | 270 | 271 | Here are the qualifiers we care about: 272 | 273 | * **Secure - No leaked info, attack-resistant:** Cuid2, NanoId (Medium - trusts web crypto API entropy). 274 | * **Collision resistant:** Cuid2, Cuid v1, NanoId, Snowflake, KSUID, XID, Ulid, ShardingId, ObjectId, UUID v6 - v8. 275 | * **Horizontally scalable:** Cuid2, Cuid v1, NanoId, ObjectId, Ulid, KSUID, Xid, ShardingId, ObjectId, UUID v6 - v8. 276 | * **Offline-compatible:** Cuid2, Cuid v1, NanoId, Ulid, UUID v6 - v8. 277 | * **URL and name-friendly:** Cuid2, Cuid v1, NanoId (with custom alphabet). 278 | * **Fast and convenient:** Cuid2, Cuid v1, NanoId, Ulid, KSUID, Xid, UUID v4, UUID v7. 279 | * **But not *too fast*:** Cuid2, Cuid v1, UUID v7, Snowflake, ShardingId, ObjectId. 280 | 281 | Cuid2 is the only solution that passed all of our tests. 282 | 283 | 284 | ### NanoId and Ulid 285 | 286 | Overall, NanoId and Ulid seem to hit most of our requirements, Ulid leaks timestamps, and they both trust the random entropy from the Web Crypto API too much. The Web Crypto API trusts 2 untrustworthy things: The [random entropy source](https://docs.rs/bug/0.2.0/bug/rand/index.html#cryptographic-security), and the hashing algorithm used to stretch the entropy into random-looking data. Some implementations have had [serious bugs that made them vulnerable to attack](https://bugs.chromium.org/p/chromium/issues/detail?id=552749). 287 | 288 | Along with using cryptographically secure methods, Cuid2 supplies its own known entropy from a diverse pool and uses a security audited, NIST-standard cryptographically secure hashing algorithm. 289 | 290 | **Too fast:** NanoId and Ulid are also very fast. But that's not a good thing. The faster you can generate ids, the faster you can run collision attacks. Bad guys looking for statistical anomalies in the distribution of ids can use the speed of NanoId to their advantage. Cuid2 is fast enough to be convenient, but not so fast that it's a security risk. 291 | 292 | 293 | #### Entropy Security Comparison 294 | 295 | * NanoId Entropy: Web Crypto. 296 | * Ulid Entropy: Web Crypto + time stamp (leaked). 297 | * Cuid2 Entropy: Web Crypto + time stamp + counter + host fingerprint + hashing algorithm. 298 | 299 | 300 | ## Testing 301 | 302 | Before each commit, we test over 10 million ids generated in parallel across 7 different CPU cores. With each batch of tests, we run a histogram analysis to ensure an even, random distribution across the entire entropy range. Any bias would make it more likely for ids to collide, so our tests will automatically fail if it finds any. 303 | 304 | Screen Shot 2022-12-30 at 6 19 15 PM 305 | 306 | 307 | We also generate randograms and do spot visual inspections. 308 | 309 | ![randogram](public/randogram.png) 310 | 311 | ## Troubleshooting 312 | 313 | Some React Native environments may be missing TextEncoding features, which will need to be polyfilled. The following have both worked for users who have encountered this issue: 314 | 315 | ``` 316 | npm install --save fast-text-encoding 317 | ``` 318 | 319 | Then, before importing Cuid2: 320 | 321 | ```js 322 | import "fast-text-encoding"; 323 | ``` 324 | 325 | Alternatively, if that doesn't work: 326 | 327 | ``` 328 | npm install --save text-encoding-polyfill 329 | ``` 330 | 331 | Then, before importing Cuid2: 332 | 333 | ```js 334 | import "text-encoding-polyfill"; 335 | ``` 336 | 337 | ### Using in Jest 338 | 339 | Jest uses jsdom, which builds a global object which doesn't comply with current standards. There is a known issue in Jest when jsdom environment is used. The results of `new TextEncoder().encode()` and `new Uint8Array()` are different, refer to [jestjs/jest#9983](https://github.com/jestjs/jest/issues/9983). 340 | 341 | To work around this limitation on jsdom (and by extension, Jest), you'll need to use custom environment which overwrites Uint8Array provided by jsdom: 342 | 343 | Install jest-environment-jsdom. Make sure to use the same version as your jest. See [this answer on Stackoverflow for reference](https://stackoverflow.com/a/72124554). 344 | 345 | ``` 346 | ❯ npm i jest-environment-jsdom@27 347 | ``` 348 | 349 | Create `jsdom-env.js` file in the root: 350 | 351 | ```js 352 | const JSDOMEnvironmentBase = require('jest-environment-jsdom'); 353 | 354 | Object.defineProperty(exports, '__esModule', { 355 | value: true 356 | }); 357 | 358 | class JSDOMEnvironment extends JSDOMEnvironmentBase { 359 | constructor(...args) { 360 | const { global } = super(...args); 361 | 362 | global.Uint8Array = Uint8Array; 363 | } 364 | } 365 | 366 | exports.default = JSDOMEnvironment; 367 | exports.TestEnvironment = JSDOMEnvironment; 368 | ``` 369 | 370 | Update scripts to use the custom environment: 371 | 372 | ```js 373 | { 374 | // ... 375 | "scripts": { 376 | // ... 377 | "test": "react-scripts test --env=./jsdom-env.js", 378 | // ... 379 | }, 380 | } 381 | ``` 382 | 383 | #### JSDOM is Missing Features 384 | 385 | JSDOM doesn't support TextEncoder and TextDecoder, refer jsdom/jsdom#2524. 386 | 387 | In Jest, features like Uint8Array/TextEncoder/TextDecoder may be available in the jsdom environment but may produce results different from the platform standards. These are known bugs which may be resolved by jsdom at some point, but there is no clear ETA. 388 | 389 | Note that this issue may impact any package that relies on the TextEncoder or TextDecorder standards. If you would like to use a simple test runner that just works, try [Riteway](https://github.com/paralleldrive/riteway). 390 | 391 | 392 | ## Sponsors 393 | 394 | This project is made possible by: 395 | 396 | * [DevAnywhere](https://devanywhere.io) - Expert mentorship for software builders, from junior developers to software leaders like VPE, CTO, and CEO. 397 | * [EricElliottJS.com](https://ericelliottjs.com) - Learn JavaScript on demand with videos and interactive lessons. 398 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace cuid2 { 2 | export function getConstants(): { 3 | defaultLength: number 4 | bigLength: number 5 | } 6 | 7 | export function init(options?: { 8 | random?: () => number 9 | counter?: () => number 10 | length?: number 11 | fingerprint?: string 12 | }): () => string 13 | 14 | export function isCuid(id: string, options?: { minLength?: number, maxLength?: number }): boolean 15 | 16 | export function createId(): string 17 | } 18 | 19 | export = cuid2; 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { createId, init, getConstants, isCuid } = require("./src/index"); 2 | 3 | module.exports.createId = createId; 4 | module.exports.init = init; 5 | module.exports.getConstants = getConstants; 6 | module.exports.isCuid = isCuid; 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@paralleldrive/cuid2", 3 | "private": false, 4 | "types": "index.d.ts", 5 | "files": [ 6 | "src/index.js", 7 | "index.js", 8 | "index.d.ts" 9 | ], 10 | "scripts": { 11 | "dev": "next dev", 12 | "build": "next build", 13 | "start": "next start", 14 | "lint": "next lint --fix", 15 | "typescript": "npx -p typescript tsc --esModuleInterop --rootDir . src/index-test.js --noEmit --allowJs --checkJs --target es2020 --moduleResolution node && echo 'TypeScript Complete.'", 16 | "test": "NODE_ENV=test node src/index-test.js", 17 | "test-color": "NODE_ENV=test node src/index-test.js | tap-nirvana", 18 | "collision-test": "NODE_ENV=test node src/collision-test.js | tap-nirvana", 19 | "histogram": "NODE_ENV=test node src/histogram.js | tap-nirvana", 20 | "watch": "watch 'npm run -s test | tap-nirvana && npm run -s lint && npm run -s typescript' src", 21 | "update": "updtr", 22 | "release": "release-it" 23 | }, 24 | "pre-commit": [ 25 | "lint", 26 | "test-color", 27 | "collision-test" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/ericelliott/cuid2.git" 32 | }, 33 | "keywords": [ 34 | "uuid", 35 | "guid", 36 | "cuid", 37 | "unique", 38 | "id", 39 | "ids", 40 | "identifier", 41 | "identifiers" 42 | ], 43 | "author": "Eric Elliott", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/ericelliott/cuid2/issues" 47 | }, 48 | "homepage": "https://github.com/ericelliott/cuid2#readme", 49 | "devDependencies": { 50 | "eslint": "8.30.0", 51 | "eslint-config-next": "13.1.1", 52 | "eslint-config-prettier": "8.5.0", 53 | "eslint-plugin-prettier": "4.2.1", 54 | "next": "13.1.1", 55 | "pre-commit": "1.2.2", 56 | "prettier": "2.8.1", 57 | "react": "18.2.0", 58 | "react-dom": "18.2.0", 59 | "release-it": "15.5.1", 60 | "riteway": "7.0.0", 61 | "tap-nirvana": "1.1.0", 62 | "updtr": "4.0.0", 63 | "watch": "1.0.2" 64 | }, 65 | "dependencies": { 66 | "@noble/hashes": "^1.7.1" 67 | }, 68 | "version": "2.2.2" 69 | } 70 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import { createId } from "../src/index"; 3 | 4 | const width = 250; 5 | const height = width; 6 | 7 | const RandomDistribution = () => { 8 | const canvasRef = useRef(null); 9 | 10 | useEffect(() => { 11 | const canvas = canvasRef.current; 12 | const ctx = canvas.getContext("2d"); 13 | 14 | const plotPixel = () => { 15 | // Generate a unique id using the createId function 16 | const id = createId().substring(1); 17 | 18 | // Convert the id to a number between 0 and 1 19 | const xValue = Number( 20 | parseInt(id.substring(0, 10), 36) / Math.pow(36, 10) 21 | ); 22 | const yValue = Number( 23 | parseInt(id.substring(11, 21), 36) / Math.pow(36, 10) 24 | ); 25 | 26 | // Plot a pixel at a position determined by the id 27 | const x = Math.floor(xValue * width); 28 | const y = Math.floor(yValue * height); 29 | console.log({ xValue, yValue, x, y }); 30 | ctx.fillStyle = `rgb(0, 0, 0, 0.5)`; 31 | ctx.fillRect(x, y, 1, 1); 32 | 33 | // Schedule the next pixel to be plotted 34 | requestAnimationFrame(plotPixel); 35 | }; 36 | 37 | // Start plotting pixels 38 | plotPixel(); 39 | }, []); 40 | 41 | return ( 42 | <> 43 |
44 | 45 |
46 | 55 | 56 | ); 57 | }; 58 | 59 | export default RandomDistribution; 60 | -------------------------------------------------------------------------------- /public/histogram.csv: -------------------------------------------------------------------------------- 1 | 5016,4925,4987,4973,4963,4931,5066,5066,5101,5094,5012,5087,5075,5067,4893,5094,4804,5100,4912,4834 2 | -------------------------------------------------------------------------------- /public/histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paralleldrive/cuid2/942ba7c48d18de9fa8361df314b3b4859b69db67/public/histogram.png -------------------------------------------------------------------------------- /public/randogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paralleldrive/cuid2/942ba7c48d18de9fa8361df314b3b4859b69db67/public/randogram.png -------------------------------------------------------------------------------- /src/collision-test.js: -------------------------------------------------------------------------------- 1 | const { describe } = require("riteway"); 2 | const { Worker } = require("worker_threads"); 3 | 4 | const { info } = require("./test-utils.js"); 5 | 6 | // This is the code that will be run in each worker thread. 7 | // It creates an id pool and returns it. 8 | const workerCode = ` 9 | const { parentPort } = require('node:worker_threads'); 10 | const { createIdPool } = require('./src/test-utils.js'); 11 | const { max } = JSON.parse(process.argv[2]); 12 | createIdPool({ max }).then((idPool) => parentPort.postMessage(idPool)); 13 | `; 14 | 15 | // This function creates a worker thread and returns a promise that resolves 16 | // with the id pool returned by the worker. 17 | async function createIdPoolInWorker(max) { 18 | return new Promise((resolve, reject) => { 19 | const worker = new Worker(workerCode, { 20 | eval: true, 21 | argv: [JSON.stringify({ max })], 22 | }); 23 | worker.on("message", resolve); 24 | worker.on("error", reject); 25 | }); 26 | } 27 | 28 | // This function creates an array of promises, each of which creates an id pool 29 | // in a worker thread. 30 | const createIdPoolsInWorkers = (numWorkers, max) => { 31 | return Promise.all( 32 | Array.from({ length: numWorkers }, () => createIdPoolInWorker(max)) 33 | ); 34 | }; 35 | 36 | describe("Collision Test", async (assert) => { 37 | { 38 | const n = 7 ** 8 * 2; 39 | info(`Testing ${n} unique IDs...`); 40 | const numPools = 7; 41 | const pools = await createIdPoolsInWorkers(numPools, n / numPools); 42 | const ids = [].concat(...pools.map((x) => x.ids)); 43 | const sampleIds = ids.slice(0, 10); 44 | const set = new Set(ids); 45 | const histogram = pools[0].histogram; 46 | info(`sample ids: ${sampleIds}`); 47 | info(`histogram: ${histogram}`); 48 | const expectedBinSize = Math.ceil(n / numPools / histogram.length); 49 | const tolerance = 0.05; 50 | const minBinSize = Math.round(expectedBinSize * (1 - tolerance)); 51 | const maxBinSize = Math.round(expectedBinSize * (1 + tolerance)); 52 | info(`expectedBinSize: ${expectedBinSize}`); 53 | info(`minBinSize: ${minBinSize}`); 54 | info(`maxBinSize: ${maxBinSize}`); 55 | 56 | assert({ 57 | given: "lots of ids generated", 58 | should: "generate no collissions", 59 | actual: set.size, 60 | expected: n, 61 | }); 62 | 63 | assert({ 64 | given: "lots of ids generated", 65 | should: "produce a histogram within distribution tolerance", 66 | actual: histogram.every((x) => x > minBinSize && x < maxBinSize), 67 | expected: true, 68 | }); 69 | 70 | assert({ 71 | given: "lots of ids generated", 72 | should: "contain only valid characters", 73 | actual: ids.every((id) => /^[a-z0-9]+$/.test(id)), 74 | expected: true, 75 | }); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /src/histogram.js: -------------------------------------------------------------------------------- 1 | const { describe } = require("riteway"); 2 | const { createIdPool, info } = require("./test-utils.js"); 3 | 4 | describe("Histogram", async (assert) => { 5 | const n = 100000; 6 | info(`Testing ${n} unique IDs...`); 7 | const pool = await createIdPool({ max: n }); 8 | const ids = pool.ids; 9 | const sampleIds = ids.slice(0, 10); 10 | const set = new Set(ids); 11 | 12 | assert({ 13 | given: "lots of ids generated", 14 | should: "generate no collissions", 15 | actual: set.size, 16 | expected: n, 17 | }); 18 | 19 | { 20 | // Arrange 21 | const tolerance = 0.1; 22 | const idLength = 23; 23 | const totalLetters = idLength * n; 24 | const base = 36; 25 | const expectedBinSize = Math.ceil(totalLetters / base); 26 | const minBinSize = Math.round(expectedBinSize * (1 - tolerance)); 27 | const maxBinSize = Math.round(expectedBinSize * (1 + tolerance)); 28 | 29 | // Act 30 | // Drop the first character because it will always be a letter, making 31 | // the letter frequency skewed. 32 | const testIds = ids.map((id) => id.slice(2)); 33 | const charFrequencies = {}; 34 | testIds.forEach((id) => { 35 | id.split("").forEach( 36 | (char) => (charFrequencies[char] = (charFrequencies[char] || 0) + 1) 37 | ); 38 | }); 39 | 40 | info("Testing character frequency..."); 41 | info(`expectedBinSize: ${expectedBinSize}`); 42 | info(`minBinSize: ${minBinSize}`); 43 | info(`maxBinSize: ${maxBinSize}`); 44 | info(`charFrequencies: ${JSON.stringify(charFrequencies)}`); 45 | 46 | // Assert 47 | assert({ 48 | given: "lots of ids generated", 49 | should: "produce even character frequency", 50 | actual: Object.values(charFrequencies).every( 51 | (x) => x > minBinSize && x < maxBinSize 52 | ), 53 | expected: true, 54 | }); 55 | 56 | assert({ 57 | given: "lots of ids generated", 58 | should: "represent all character values", 59 | actual: Object.keys(charFrequencies).length, 60 | expected: base, 61 | }); 62 | } 63 | 64 | { 65 | const histogram = pool.histogram; 66 | info(`sample ids:`); 67 | sampleIds.forEach((id) => info(` ${id}`)); 68 | info(`histogram: ${histogram}`); 69 | const expectedBinSize = Math.ceil(n / histogram.length); 70 | const tolerance = 0.1; 71 | const minBinSize = Math.round(expectedBinSize * (1 - tolerance)); 72 | const maxBinSize = Math.round(expectedBinSize * (1 + tolerance)); 73 | info(`expectedBinSize: ${expectedBinSize}`); 74 | info(`minBinSize: ${minBinSize}`); 75 | info(`maxBinSize: ${maxBinSize}`); 76 | 77 | assert({ 78 | given: "lots of ids generated", 79 | should: "produce a histogram within distribution tolerance", 80 | actual: histogram.every((x) => x > minBinSize && x < maxBinSize), 81 | expected: true, 82 | }); 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /src/index-test.js: -------------------------------------------------------------------------------- 1 | const { describe } = require("riteway"); 2 | const { 3 | createId, 4 | init, 5 | getConstants, 6 | createCounter, 7 | bufToBigInt, 8 | createFingerprint, 9 | isCuid, 10 | } = require("./index"); 11 | 12 | const { info } = require("./test-utils.js"); 13 | 14 | describe("Cuid2", async (assert) => { 15 | { 16 | const id = createId(); 17 | info(id); 18 | 19 | assert({ 20 | given: "nothing", 21 | should: "return a cuid string", 22 | actual: typeof id, 23 | expected: "string", 24 | }); 25 | } 26 | 27 | { 28 | const id = createId(); 29 | const defaultLength = getConstants().defaultLength; 30 | info(id); 31 | 32 | assert({ 33 | given: "nothing", 34 | should: "return a cuid of the default length", 35 | actual: id.length, 36 | expected: defaultLength, 37 | }); 38 | } 39 | 40 | { 41 | const length = 10; 42 | // Test that custom cuid lengths work 43 | const cuid = init({ length }); 44 | const id = cuid(); 45 | info(id); 46 | 47 | assert({ 48 | given: "custom cuid with a smaller length", 49 | should: "return a cuid with the specified smaller length", 50 | actual: id.length, 51 | expected: length, 52 | }); 53 | } 54 | 55 | { 56 | const length = 32; 57 | // Test that large cuid lengths work 58 | const cuid = init({ length }); 59 | const id = cuid(); 60 | info(id); 61 | 62 | assert({ 63 | given: "custom cuid with a larger length", 64 | should: "return a cuid with the specified larger length", 65 | actual: id.length, 66 | expected: length, 67 | }); 68 | } 69 | }); 70 | 71 | describe("createCounter", async (assert) => { 72 | const counter = createCounter(10); 73 | const expected = [10, 11, 12, 13]; 74 | const actual = [counter(), counter(), counter(), counter()]; 75 | info(actual); 76 | 77 | assert({ 78 | given: "a starting number", 79 | should: "return a function that increments the number", 80 | actual, 81 | expected, 82 | }); 83 | }); 84 | 85 | describe("bufToBigInt", async (assert) => { 86 | { 87 | const actual = bufToBigInt(new Uint8Array(2)); 88 | const expected = BigInt(0); 89 | 90 | assert({ 91 | given: "an empty Uint8Array", 92 | should: "return 0", 93 | actual, 94 | expected, 95 | }); 96 | } 97 | 98 | { 99 | const actual = bufToBigInt(new Uint8Array([0xff, 0xff, 0xff, 0xff])); 100 | const expected = BigInt("4294967295"); 101 | 102 | assert({ 103 | given: "a maximum-value Uint32Array", 104 | should: "return 2^32 - 1", 105 | actual, 106 | expected, 107 | }); 108 | } 109 | }); 110 | 111 | describe("createFingerprint", async (assert) => { 112 | { 113 | const fingerprint = createFingerprint(); 114 | const actual = fingerprint.length >= 24; 115 | const expected = true; 116 | info(`Host fingerprint: ${fingerprint}`); 117 | 118 | assert({ 119 | given: "no arguments", 120 | should: "return a string of sufficient length", 121 | actual, 122 | expected, 123 | }); 124 | } 125 | { 126 | const fingerprint = createFingerprint({ globalObj: {} }); 127 | const actual = fingerprint.length >= 24; 128 | const expected = true; 129 | 130 | info(`Empty global fingerprint: ${fingerprint}`); 131 | 132 | assert({ 133 | given: "an empty global object", 134 | should: "fall back on random entropy", 135 | actual, 136 | expected, 137 | }); 138 | } 139 | }); 140 | 141 | describe("isCuid", async (assert) => { 142 | { 143 | const actual = isCuid(createId()); 144 | const expected = true; 145 | 146 | assert({ 147 | given: "a valid cuid", 148 | should: "return true", 149 | actual, 150 | expected, 151 | }); 152 | } 153 | 154 | { 155 | const actual = isCuid(createId() + createId() + createId()); 156 | const expected = false; 157 | 158 | assert({ 159 | given: "a cuid that is too long", 160 | should: "return false", 161 | actual, 162 | expected, 163 | }); 164 | } 165 | 166 | { 167 | const actual = isCuid(""); 168 | const expected = false; 169 | 170 | assert({ 171 | given: "an empty string", 172 | should: "return false", 173 | actual, 174 | expected, 175 | }); 176 | } 177 | 178 | { 179 | const actual = isCuid("42"); 180 | const expected = false; 181 | 182 | assert({ 183 | given: "a non-CUID string", 184 | should: "return false", 185 | actual, 186 | expected, 187 | }); 188 | } 189 | 190 | { 191 | const actual = isCuid("aaaaDLL"); 192 | const expected = false; 193 | 194 | assert({ 195 | given: "a string with capital letters", 196 | should: "return false", 197 | actual, 198 | expected, 199 | }); 200 | } 201 | 202 | { 203 | const actual = isCuid("yi7rqj1trke"); 204 | const expected = true; 205 | 206 | assert({ 207 | given: "a valid CUID2 string", 208 | should: "return true", 209 | actual, 210 | expected, 211 | }); 212 | } 213 | 214 | { 215 | const actual = isCuid("-x!ha"); 216 | const expected = false; 217 | 218 | assert({ 219 | given: "a string with invalid characters", 220 | should: "return false", 221 | actual, 222 | expected, 223 | }); 224 | } 225 | 226 | { 227 | const actual = isCuid("ab*%@#x"); 228 | const expected = false; 229 | 230 | assert({ 231 | given: "a string with invalid characters", 232 | should: "return false", 233 | actual, 234 | expected, 235 | }); 236 | } 237 | }); 238 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* global global, window, module */ 2 | const { sha3_512: sha3 } = require("@noble/hashes/sha3"); 3 | 4 | const defaultLength = 24; 5 | const bigLength = 32; 6 | 7 | const createEntropy = (length = 4, random = Math.random) => { 8 | let entropy = ""; 9 | 10 | while (entropy.length < length) { 11 | entropy = entropy + Math.floor(random() * 36).toString(36); 12 | } 13 | return entropy; 14 | }; 15 | 16 | /* 17 | * Adapted from https://github.com/juanelas/bigint-conversion 18 | * MIT License Copyright (c) 2018 Juan Hernández Serrano 19 | */ 20 | function bufToBigInt(buf) { 21 | let bits = 8n; 22 | 23 | let value = 0n; 24 | for (const i of buf.values()) { 25 | const bi = BigInt(i); 26 | value = (value << bits) + bi; 27 | } 28 | return value; 29 | } 30 | 31 | const hash = (input = "") => { 32 | // Drop the first character because it will bias the histogram 33 | // to the left. 34 | return bufToBigInt(sha3(input)).toString(36).slice(1); 35 | }; 36 | 37 | const alphabet = Array.from({ length: 26 }, (x, i) => 38 | String.fromCharCode(i + 97) 39 | ); 40 | 41 | const randomLetter = (random) => 42 | alphabet[Math.floor(random() * alphabet.length)]; 43 | 44 | /* 45 | This is a fingerprint of the host environment. It is used to help 46 | prevent collisions when generating ids in a distributed system. 47 | If no global object is available, you can pass in your own, or fall back 48 | on a random string. 49 | */ 50 | const createFingerprint = ({ 51 | globalObj = typeof global !== "undefined" 52 | ? global 53 | : typeof window !== "undefined" 54 | ? window 55 | : {}, 56 | random = Math.random, 57 | } = {}) => { 58 | const globals = Object.keys(globalObj).toString(); 59 | const sourceString = globals.length 60 | ? globals + createEntropy(bigLength, random) 61 | : createEntropy(bigLength, random); 62 | 63 | return hash(sourceString).substring(0, bigLength); 64 | }; 65 | 66 | const createCounter = (count) => () => { 67 | return count++; 68 | }; 69 | 70 | // ~22k hosts before 50% chance of initial counter collision 71 | // with a remaining counter range of 9.0e+15 in JavaScript. 72 | const initialCountMax = 476782367; 73 | 74 | const init = ({ 75 | // Fallback if the user does not pass in a CSPRNG. This should be OK 76 | // because we don't rely solely on the random number generator for entropy. 77 | // We also use the host fingerprint, current time, and a session counter. 78 | random = Math.random, 79 | counter = createCounter(Math.floor(random() * initialCountMax)), 80 | length = defaultLength, 81 | fingerprint = createFingerprint({ random }), 82 | } = {}) => { 83 | return function cuid2() { 84 | const firstLetter = randomLetter(random); 85 | 86 | // If we're lucky, the `.toString(36)` calls may reduce hashing rounds 87 | // by shortening the input to the hash function a little. 88 | const time = Date.now().toString(36); 89 | const count = counter().toString(36); 90 | 91 | // The salt should be long enough to be globally unique across the full 92 | // length of the hash. For simplicity, we use the same length as the 93 | // intended id output. 94 | const salt = createEntropy(length, random); 95 | const hashInput = `${time + salt + count + fingerprint}`; 96 | 97 | return `${firstLetter + hash(hashInput).substring(1, length)}`; 98 | }; 99 | }; 100 | 101 | const createId = init(); 102 | 103 | const isCuid = (id, { minLength = 2, maxLength = bigLength } = {}) => { 104 | const length = id.length; 105 | const regex = /^[a-z][0-9a-z]+$/; 106 | 107 | try { 108 | if ( 109 | typeof id === "string" && 110 | length >= minLength && 111 | length <= maxLength && 112 | regex.test(id) 113 | ) 114 | return true; 115 | } finally { 116 | } 117 | 118 | return false; 119 | }; 120 | 121 | module.exports.getConstants = () => ({ defaultLength, bigLength }); 122 | module.exports.init = init; 123 | module.exports.createId = createId; 124 | module.exports.bufToBigInt = bufToBigInt; 125 | module.exports.createCounter = createCounter; 126 | module.exports.createFingerprint = createFingerprint; 127 | module.exports.isCuid = isCuid; 128 | -------------------------------------------------------------------------------- /src/test-utils.js: -------------------------------------------------------------------------------- 1 | const { createId } = require("./index.js"); 2 | 3 | const info = (txt) => console.log(`# - ${txt}`); 4 | 5 | const idToBigInt = (id, _, __, radix = 36) => 6 | [...id.toString()].reduce( 7 | (r, v) => r * BigInt(radix) + BigInt(parseInt(v, radix)), 8 | 0n 9 | ); 10 | 11 | const buildHistogram = (numbers, bucketCount = 20) => { 12 | const buckets = Array(bucketCount).fill(0); 13 | let counter = 1; 14 | const bucketLength = Math.ceil( 15 | Number(BigInt(36 ** 23) / BigInt(bucketCount)) 16 | ); 17 | 18 | for (const number of numbers) { 19 | if (counter % bucketLength === 0) console.log(number); 20 | 21 | const bucket = Math.floor(Number(number / BigInt(bucketLength))); 22 | if (counter % bucketLength === 0) console.log(bucket); 23 | 24 | buckets[bucket] += 1; 25 | counter++; 26 | } 27 | return buckets; 28 | }; 29 | 30 | const createIdPool = async ({ max = 100000 } = {}) => { 31 | const set = new Set(); 32 | 33 | for (let i = 0; i < max; i++) { 34 | set.add(createId()); 35 | if (i % 10000 === 0) console.log(`${Math.floor((i / max) * 100)}%`); 36 | if (set.size < i) { 37 | info(`Collision at: ${i}`); 38 | break; 39 | } 40 | } 41 | info("No collisions detected"); 42 | 43 | const ids = [...set]; 44 | const numbers = ids.map((x) => idToBigInt(x.substring(1))); 45 | const histogram = buildHistogram(numbers); 46 | return { ids, numbers, histogram }; 47 | }; 48 | 49 | module.exports.createIdPool = createIdPool; 50 | module.exports.buildHistogram = buildHistogram; 51 | module.exports.info = info; 52 | module.exports.idToBigInt = idToBigInt; 53 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x85fb7e6Fc8FC092e0D279A48988840fa0090c3E5' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "baseUrl": ".", 7 | "lib": [ 8 | "es2020", 9 | "dom" 10 | ], 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmit": true, 16 | "incremental": true, 17 | "esModuleInterop": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------