diff --git a/packages/ids/README.md b/packages/ids/README.md index ff49ea5..dde1d04 100644 --- a/packages/ids/README.md +++ b/packages/ids/README.md @@ -2,29 +2,41 @@ [![NPM version](https://img.shields.io/npm/v/@prsm/ids?color=a1b858&label=)](https://www.npmjs.com/package/@prsm/ids) -Short, obfuscated, collision-proof, and reversible identifiers. +Short, obfuscated, collision-proof, reversible identifiers. -Because sometimes internal identifiers are sensitive, or you just don't want to let a user know that their ID is 1. +## Usage ```typescript -import ID from "@prsm/ids"; +import id from "@prsm/ids"; -ID.encode(12389125); // phsV8T -ID.decode("phsV8T"); // 12389125 +id.encode(12389125); // "7rYTs_" +id.decode("7rYTs_"); // 12389125 ``` -You can (and should) set your own alphabet string: +## Configuration +Set custom alphabet: ```typescript -ID.alphabet = "GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"; -ID.alphabet = "TgzMhJXtRSVBnHFksZQc5j-yGx84W3rNDfK6p_Cbqd29YLm7Pwv"; -ID.alphabet = "kbHn53dZphT2FvGMBxYJKqS7-cPV_Ct6LwjWRDfXmygzrQ48N9s"; +id.setAlphabet("GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"); ``` -If your use case makes sense, you can also generate a random alphabet string with `randomizeAlphabet`. - -When the alphabet changes, though, the encoded IDs will change as well. Decoding will still work, but the decoded value will be different. - +Randomize alphabet: ```typescript -ID.randomizeAlphabet(); +id.randomizeAlphabet(); ``` + +## API + +| Function | Description | +|-----------------------|-------------------------------------------| +| `encode(num)` | Converts number to obfuscated string | +| `decode(str)` | Converts obfuscated string back to number | +| `setAlphabet(str)` | Sets custom alphabet for encoding | +| `getAlphabet()` | Returns current alphabet | +| `randomizeAlphabet()` | Shuffles alphabet characters randomly | + +## Notes + +- Maximum encodable value: 2,147,483,647 (MAX_INT32) +- Changing alphabet changes encoded values +- Encoded values must be decoded with same alphabet diff --git a/packages/ids/bun.lockb b/packages/ids/bun.lockb index 342b14c..0709c3f 100755 Binary files a/packages/ids/bun.lockb and b/packages/ids/bun.lockb differ diff --git a/packages/ids/package.json b/packages/ids/package.json index bede136..1518712 100644 --- a/packages/ids/package.json +++ b/packages/ids/package.json @@ -13,7 +13,8 @@ }, "scripts": { "build": "tsup", - "test": "bun tests/index.ts", + "test": "vitest run", + "test:watch": "vitest", "release": "bumpp package.json && npm publish --access public" }, "author": "nvms", @@ -23,8 +24,8 @@ }, "devDependencies": { "bumpp": "^9.5.1", - "manten": "^0.6.0", "tsup": "^8.2.4", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "vitest": "^3.0.9" } } diff --git a/packages/ids/src/index.ts b/packages/ids/src/index.ts index 9dbe743..22579ec 100644 --- a/packages/ids/src/index.ts +++ b/packages/ids/src/index.ts @@ -1,74 +1,76 @@ import long from "long"; -export default class ID { - private static MAX_INT32 = 2_147_483_647; - private static MULTIPLIER = 4_294_967_296; +const MAX_INT32 = 2_147_483_647; +const PRIME = 1_125_812_041; +const INVERSE = 348_986_105; +const RANDOM = 998_048_641; +const DEFAULT_ALPHABET = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ-_"; - static alphabet: string = - "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ-_"; - static prime: number = 1_125_812_041; - static inverse: number = 348_986_105; - static random: number = 998_048_641; +let alphabet = DEFAULT_ALPHABET; - static get base(): number { - return ID.alphabet.length; +const getBase = () => alphabet.length; + +const shorten = (id: number): string => { + let result = ""; + const base = getBase(); + + while (id > 0) { + result = alphabet[id % base] + result; + id = Math.floor(id / base); } - private static shorten(id: number): string { - let result = ""; + return result; +}; - while (id > 0) { - result = ID.alphabet[id % ID.base] + result; - id = Math.floor(id / ID.base); - } +const unshorten = (str: string): number => { + let result = 0; + const base = getBase(); - return result; + for (let i = 0; i < str.length; i++) { + result = result * base + alphabet.indexOf(str[i]); } - private static unshorten(str: string): number { - let result = 0; + return result; +}; - for (let i = 0; i < str.length; i++) { - result = result * ID.base + ID.alphabet.indexOf(str[i]); - } +const id = { + MAX_INT32, + DEFAULT_ALPHABET, - return result; - } - - static encode = (num: number): string => { - if (num > ID.MAX_INT32) { + encode: (num: number): string => { + if (num > MAX_INT32) { throw new Error( - `Number (${num}) is too large to encode. MAX_INT32 is ${ID.MAX_INT32}`, + `Number (${num}) is too large to encode. MAX_INT32 is ${MAX_INT32}`, ); } - const n: long = long.fromInt(num); + const n = long.fromInt(num); - return ID.shorten( - n - .multiply(ID.prime) - .and(long.fromInt(ID.MAX_INT32)) - .xor(ID.random) - .toInt(), + return shorten( + n.multiply(PRIME).and(long.fromInt(MAX_INT32)).xor(RANDOM).toInt(), ); - }; + }, - static decode = (str: string): number => { - const n: long = long.fromInt(ID.unshorten(str)); + decode: (str: string): number => { + const n = long.fromInt(unshorten(str)); - return n - .xor(ID.random) - .multiply(ID.inverse) - .and(long.fromInt(ID.MAX_INT32)) - .toInt(); - }; + return n.xor(RANDOM).multiply(INVERSE).and(long.fromInt(MAX_INT32)).toInt(); + }, - static randomizeAlphabet(): void { - const array = ID.alphabet.split(''); + getAlphabet: (): string => alphabet, + + setAlphabet: (newAlphabet: string): void => { + alphabet = newAlphabet; + }, + + randomizeAlphabet: (): void => { + const array = alphabet.split(""); for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } - ID.alphabet = array.join(''); - } -} + alphabet = array.join(""); + }, +}; + +export default id; diff --git a/packages/ids/tests/index.test.ts b/packages/ids/tests/index.test.ts new file mode 100644 index 0000000..e9e102f --- /dev/null +++ b/packages/ids/tests/index.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import id from "../src"; + +describe("ids", () => { + // Reset alphabet before each test + beforeEach(() => { + id.setAlphabet(id.DEFAULT_ALPHABET); + }); + + it("encodes as expected", () => { + const encoded = id.encode(12389125); + expect(encoded).toBe("7rYTs_"); + }); + + it("decodes as expected", () => { + const decoded = id.decode("7rYTs_"); + expect(decoded).toBe(12389125); + }); + + it("changing the alphabet is effective", () => { + id.setAlphabet("GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"); + expect(id.encode(12389125)).toBe("phsV8T"); + expect(id.decode("phsV8T")).toBe(12389125); + }); + + it("shuffling the alphabet changes encoding but preserves round-trip integrity", () => { + // First randomization + id.randomizeAlphabet(); + const encoded1 = id.encode(12389125); + const decoded1 = id.decode(encoded1); + expect(decoded1).toBe(12389125); + + // Store the current alphabet + const alphabet1 = id.getAlphabet(); + + // Second randomization + id.randomizeAlphabet(); + const alphabet2 = id.getAlphabet(); + + // Encode with the new alphabet + const encoded2 = id.encode(12389125); + const decoded2 = id.decode(encoded2); + + // Each alphabet should produce different encodings for the same number + expect(alphabet1).not.toBe(alphabet2); + expect(encoded1).not.toBe(encoded2); + + // But round-trip encoding/decoding should work with each alphabet + expect(decoded2).toBe(12389125); + }); +}); diff --git a/packages/ids/tests/index.ts b/packages/ids/tests/index.ts deleted file mode 100644 index 85e1fda..0000000 --- a/packages/ids/tests/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect } from "manten"; -import ID from "../src"; - -describe("ids", async ({ test }) => { - test("encodes as expected", () => { - const encoded = ID.encode(12389125); - expect(encoded).toBe("7rYTs_"); - }); - - test("decodes as expected", () => { - const decoded = ID.decode("7rYTs_"); - expect(decoded).toBe(12389125); - }); - - test("changing the alphabet is effective", () => { - ID.alphabet = "GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"; - expect(ID.encode(12389125)).toBe("phsV8T"); - expect(ID.decode("phsV8T")).toBe(12389125); - }); - - test("shuffling the alphabet still allows you to decode things", () => { - ID.randomizeAlphabet(); - const encoded = ID.encode(12389125); - const decoded = ID.decode(encoded); - expect(decoded).toBe(12389125); - - console.log(ID.alphabet); - - ID.randomizeAlphabet(); - // const encoded2 = ID.encode(12389125); - const decoded2 = ID.decode(encoded); - expect(decoded2).toBe(12389125); - - // expect(encoded).not.toBe(encoded2); - }) -}); diff --git a/packages/ids/vitest.config.ts b/packages/ids/vitest.config.ts new file mode 100644 index 0000000..4ac6027 --- /dev/null +++ b/packages/ids/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + }, +});