cybertoken 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ github: nikeee
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "npm"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "daily"
7
+
8
+ - package-ecosystem: "github-actions"
9
+ directory: "/"
10
+ schedule:
11
+ interval: "weekly"
@@ -0,0 +1,66 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ test:
10
+ name: Test
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ matrix:
15
+ node-version: [16.x, 18.x, 19.x]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+ - uses: actions/setup-node@v3
20
+ with:
21
+ node-version: ${{ matrix.node-version }}
22
+ cache: npm
23
+
24
+ - run: npm ci
25
+ - run: npm run test:coverage
26
+
27
+ docs-build:
28
+ name: Build Docs
29
+ runs-on: ubuntu-latest
30
+ needs:
31
+ - test
32
+
33
+ steps:
34
+ - uses: actions/checkout@v3
35
+ - uses: actions/setup-node@v3
36
+ with:
37
+ node-version: 19.x
38
+ cache: npm
39
+
40
+ - run: npm ci
41
+ - run: npm run docs
42
+
43
+ - uses: actions/upload-pages-artifact@v1
44
+ with:
45
+ path: ./docs
46
+
47
+ docs-deploy:
48
+ name: Deploy Docs
49
+ runs-on: ubuntu-latest
50
+ if: ${{ github.ref == 'refs/heads/master' }}
51
+
52
+ permissions:
53
+ pages: write
54
+ id-token: write
55
+
56
+ needs:
57
+ - docs-build
58
+
59
+ environment:
60
+ name: github-pages
61
+ url: ${{ steps.deployment.outputs.page_url }}
62
+
63
+ steps:
64
+ - name: Deploy to GitHub Pages
65
+ id: deployment
66
+ uses: actions/deploy-pages@v1
@@ -0,0 +1,3 @@
1
+ {
2
+ "arrowParens": "avoid"
3
+ }
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # cybertoken [![CI](https://github.com/nikeee/cybertoken/actions/workflows/CI.yaml/badge.svg)](https://github.com/nikeee/cybertoken/actions/workflows/CI.yaml) [![npm badge](https://img.shields.io/npm/v/cybertoken)](https://www.npmjs.com/package/cybertoken)
2
+
3
+ A token format for APIs inspired by the GitHub's API token format. Think of it as a standardized password format with some handy properties. Intended for API token and password generation.
4
+
5
+ ## What is this?
6
+
7
+ GitHub changed their token format a while back and explained the reasons in an insightful blog post[^1].
8
+
9
+ tl;dr:
10
+
11
+ 1. Having a defined format for secrets is good for **automated secret scanning**.
12
+ 2. Putting some prefix to a secret makes **debugging easier** when looking at some random token.
13
+ 3. Using `_` instead of `-` is better for double-click **text selection**: `abc_def` vs `abc-def`.
14
+ 4. Using Base62[^2] instead of Base64 results in **less encoding errors** when passing the token somewhere.
15
+ 5. **CRC32 checksum** at the end of the token for faster offline syntactic check of a token (used as a heuristic, not for security).
16
+
17
+ These properties make this token format suitable for things like API tokens, which is what cybertoken is.
18
+
19
+ ## Usage
20
+
21
+ Install:
22
+
23
+ ```sh
24
+ npm install cybertoken
25
+ ```
26
+
27
+ ```js
28
+ import { createTokenGenerator } from "cybertoken";
29
+
30
+ const apiTokenGenerator = createTokenGenerator({
31
+ prefixWithoutUnderscore: "contoso",
32
+ });
33
+
34
+ const token = apiTokenGenerator.generateToken();
35
+ // Securely hash `token` and save it in your database. Return `token` to the user once.
36
+
37
+ console.log(token);
38
+ // contoso_IvN2xEuHUDjQ3PNQgZkILWc7GUxpmThDD410NQxJalD5GjY
39
+ ```
40
+
41
+ ### Bonus
42
+
43
+ You can also use cybertokens as passwords. If you use GitHub, you can set up a [custom secret scanning pattern](https://docs.github.com/en/enterprise-cloud@latest/code-security/secret-scanning/defining-custom-patterns-for-secret-scanning) that will prevent anyone from pushing anything that looks like a cybertoken that your org uses.
44
+
45
+ For example, use this pattern for Cybertokens with the prefix `contosopass`:
46
+
47
+ ```regex
48
+ contosopass_[0-9A-Za-z]{10,}
49
+ ```
50
+
51
+ #### CLI Usage
52
+ The npm package also offers a command-line utility to generate tokens.
53
+ ```sh
54
+ npm install -g cybertoken
55
+ ```
56
+ Usage:
57
+ ```
58
+ cybertoken <prefix>
59
+
60
+ # example:
61
+ cybertoken test
62
+ test_KOK5QxQr4GBGk97X8Ij5ZnyZO24kMIEiW1LdUYIqEj7A5Nv
63
+ ```
64
+
65
+ You can also provide the token prefix via an env variable:
66
+ ```sh
67
+ CYBERTOKEN_PREFIX=foo cybertoken
68
+ ```
69
+
70
+ To make thing easier, you can put this in your `.bashrc`:
71
+ ```sh
72
+ echo "export CYBERTOKEN_PREFIX=foo" >> ~/.bashrc
73
+ ```
74
+ ...and now you can just use `cybertoken`!
75
+
76
+ ## Anatomy
77
+
78
+ This is what a cybertoken consists of:
79
+
80
+ ```
81
+ prefix base62(n-bytes + v + crc32(n-bytes + v))
82
+ ┌────────┐ ┌──────────────────────────────────────────────┐
83
+ myapitoken_161YNJQOaoFoWLaoXeMVHYyrSLhGPOjSBYBtxY6V3GqFFZKV
84
+ ```
85
+
86
+ Explanation:
87
+
88
+ - `n-bytes` are `n` random bytes, but at least 21 (default: 30), generated by a cryptographically secure random source
89
+ - `v` is a single byte containing the version of the cybertoken format used (currently, only `0x00`)
90
+ - The bytes are concatenated with the version and their CRC32 sum before base62 encoding
91
+ - base62 alphabet used: `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`
92
+
93
+ [^1]: GitHub's blog post covering this: https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats
94
+ [^2]: More info in Base62: https://en.wikipedia.org/wiki/Base62
package/a.mjs ADDED
@@ -0,0 +1,8 @@
1
+ import * as b from "./built/index.js";
2
+
3
+ const a = b.createTokenGenerator({ prefixWithoutUnderscore: "test" });
4
+ console.log(a.generateToken());
5
+ console.log(a.generateToken());
6
+ console.log(a.generateToken());
7
+ console.log(a.generateToken());
8
+ console.log(a.generateToken());
package/built/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/built/cli.js ADDED
@@ -0,0 +1,20 @@
1
+ var _a;
2
+ import { createTokenGenerator } from "./index.js";
3
+ const prefixWithoutUnderscore = (_a = process.argv[2]) !== null && _a !== void 0 ? _a : process.env.CYBERTOKEN_PREFIX;
4
+ if (!prefixWithoutUnderscore) {
5
+ console.error("Please specify a prefix for the token.");
6
+ console.error("A prefix must be a non-empty string.");
7
+ console.error();
8
+ console.error("Usage:");
9
+ console.error(" cybertoken <prefix>");
10
+ console.error();
11
+ console.error("You can also provide the prefix via the CYBERTOKEN_PREFIX environment variable.");
12
+ console.error("For example:");
13
+ console.error(" CYBERTOKEN_PREFIX=foo cybertoken");
14
+ process.exit(1);
15
+ }
16
+ const tokenGenerator = createTokenGenerator({
17
+ prefixWithoutUnderscore,
18
+ });
19
+ const token = tokenGenerator.generateToken();
20
+ console.log(token);
@@ -0,0 +1,4 @@
1
+ import baseX from "base-x";
2
+ export declare const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3
+ export declare const version = 0;
4
+ export declare const base62: baseX.BaseConverter;
@@ -0,0 +1,4 @@
1
+ import baseX from "base-x";
2
+ export const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3
+ export const version = 0;
4
+ export const base62 = baseX(alphabet);
@@ -0,0 +1 @@
1
+ export default function crc32(inputBuffer: Uint8Array): Uint8Array;
package/built/crc32.js ADDED
@@ -0,0 +1,56 @@
1
+ const table = new Uint32Array([
2
+ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
3
+ 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
4
+ 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
5
+ 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
6
+ 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
7
+ 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
8
+ 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
9
+ 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
10
+ 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
11
+ 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
12
+ 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
13
+ 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
14
+ 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
15
+ 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
16
+ 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
17
+ 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
18
+ 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
19
+ 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
20
+ 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
21
+ 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
22
+ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
23
+ 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
24
+ 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
25
+ 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
26
+ 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
27
+ 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
28
+ 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
29
+ 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
30
+ 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
31
+ 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
32
+ 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
33
+ 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
34
+ 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
35
+ 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
36
+ 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
37
+ 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
38
+ 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
39
+ 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
40
+ 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
41
+ 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
42
+ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
43
+ 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
44
+ 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
45
+ ]);
46
+ export default function crc32(inputBuffer) {
47
+ const ds = new Uint8Array(inputBuffer);
48
+ let crc = 0 ^ -1;
49
+ for (var i = 0; i < ds.length; i++) {
50
+ crc = (crc >>> 8) ^ table[(crc ^ ds[i]) & 0xff];
51
+ }
52
+ const result = new Uint8Array(4);
53
+ const dv = new DataView(result.buffer);
54
+ dv.setUint32(0, (crc ^ -1) >>> 0);
55
+ return result;
56
+ }
@@ -0,0 +1,8 @@
1
+ export interface TokenGeneratorOptions {
2
+ prefixWithoutUnderscore: string;
3
+ entropyBytes?: number;
4
+ }
5
+ export declare function createTokenGenerator(options: TokenGeneratorOptions): {
6
+ generateToken: () => string;
7
+ isTokenString: (value: unknown) => boolean;
8
+ };
package/built/index.js ADDED
@@ -0,0 +1,47 @@
1
+ var _a;
2
+ import crc32 from "./crc32.js";
3
+ import { base62, version } from "./constants.js";
4
+ import { getTokenPattern, parseTokenData } from "./parse.js";
5
+ const cryptoServices = (_a = globalThis.crypto) !== null && _a !== void 0 ? _a : require("node:crypto").webcrypto;
6
+ export function createTokenGenerator(options) {
7
+ var _a;
8
+ if (!options.prefixWithoutUnderscore) {
9
+ throw new Error("The `prefixWithoutUnderscore` option is required and must not be an empty string.");
10
+ }
11
+ const prefixWithUnderscore = options.prefixWithoutUnderscore + "_";
12
+ const tokenPattern = getTokenPattern(prefixWithUnderscore);
13
+ const tokenSecretByteCount = (_a = options.entropyBytes) !== null && _a !== void 0 ? _a : 30;
14
+ if (tokenSecretByteCount <= 20) {
15
+ throw new Error("The token secret byte count (`entropyBytes`) must be greater than 20.");
16
+ }
17
+ return {
18
+ generateToken,
19
+ isTokenString,
20
+ };
21
+ function generateToken() {
22
+ const tokenData = generateTokenData();
23
+ const encodedData = base62.encode(tokenData);
24
+ return prefixWithUnderscore + encodedData;
25
+ }
26
+ function generateTokenData() {
27
+ const entropyWithVersion = cryptoServices.getRandomValues(new Uint8Array(tokenSecretByteCount + 1));
28
+ entropyWithVersion[entropyWithVersion.length - 1] = version;
29
+ const checksum = crc32(entropyWithVersion);
30
+ console.assert(checksum.byteLength === 4);
31
+ const payloadWithChecksum = new Uint8Array(entropyWithVersion.byteLength + checksum.byteLength);
32
+ payloadWithChecksum.set(entropyWithVersion, 0);
33
+ payloadWithChecksum.set(checksum, entropyWithVersion.byteLength);
34
+ console.assert(payloadWithChecksum.length === tokenSecretByteCount + 4 + 1);
35
+ return payloadWithChecksum;
36
+ }
37
+ function isTokenString(value) {
38
+ if (!value ||
39
+ typeof value !== "string" ||
40
+ !value.startsWith(prefixWithUnderscore) ||
41
+ !tokenPattern.test(value)) {
42
+ return false;
43
+ }
44
+ const tokenData = parseTokenData(value);
45
+ return !!tokenData && tokenData.isSyntacticallyValid;
46
+ }
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { test, expect } from "vitest";
11
+ import { createTokenGenerator } from "./index.js";
12
+ test("Instance creation smoke test", () => {
13
+ createTokenGenerator({
14
+ prefixWithoutUnderscore: "test",
15
+ });
16
+ });
17
+ test("Instance creation expect prefix", () => {
18
+ expect(() => createTokenGenerator({})).toThrowError("The `prefixWithoutUnderscore` option is required and must not be an empty string.");
19
+ });
20
+ test("Instance creation expect proper byte count", () => {
21
+ expect(() => createTokenGenerator({
22
+ prefixWithoutUnderscore: "a",
23
+ entropyBytes: -1,
24
+ })).toThrowError("The token secret byte count (`entropyBytes`) must be greater than 20.");
25
+ expect(() => createTokenGenerator({
26
+ prefixWithoutUnderscore: "a",
27
+ entropyBytes: 0,
28
+ })).toThrowError("The token secret byte count (`entropyBytes`) must be greater than 20.");
29
+ expect(() => createTokenGenerator({
30
+ prefixWithoutUnderscore: "a",
31
+ entropyBytes: 19,
32
+ })).toThrowError("The token secret byte count (`entropyBytes`) must be greater than 20.");
33
+ expect(() => createTokenGenerator({
34
+ prefixWithoutUnderscore: "a",
35
+ entropyBytes: 20,
36
+ })).toThrowError("The token secret byte count (`entropyBytes`) must be greater than 20.");
37
+ createTokenGenerator({
38
+ prefixWithoutUnderscore: "a",
39
+ entropyBytes: 21,
40
+ });
41
+ });
42
+ test("Roundtrip syntax check", () => __awaiter(void 0, void 0, void 0, function* () {
43
+ const g = createTokenGenerator({ prefixWithoutUnderscore: "test" });
44
+ const token = yield g.generateToken();
45
+ expect(g.isTokenString(token)).toBe(true);
46
+ }));
47
+ test("Non-happy paths in isTokenString", () => __awaiter(void 0, void 0, void 0, function* () {
48
+ const g = createTokenGenerator({ prefixWithoutUnderscore: "test" });
49
+ expect(g.isTokenString()).toBe(false);
50
+ expect(g.isTokenString(null)).toBe(false);
51
+ expect(g.isTokenString(undefined)).toBe(false);
52
+ expect(g.isTokenString("")).toBe(false);
53
+ expect(g.isTokenString("a")).toBe(false);
54
+ expect(g.isTokenString("a_")).toBe(false);
55
+ expect(g.isTokenString("a_1234")).toBe(false);
56
+ expect(g.isTokenString("test_")).toBe(false);
57
+ expect(g.isTokenString("test_1234")).toBe(false);
58
+ expect(g.isTokenString("test_")).toBe(false);
59
+ expect(g.isTokenString("test_AAAABBBB")).toBe(false);
60
+ expect(g.isTokenString("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbN")).toBe(false);
61
+ expect(g.isTokenString("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHd")).toBe(false);
62
+ expect(g.isTokenString("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHdKNhiyW")).toBe(false);
63
+ expect(g.isTokenString("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHdKNhiyw")).toBe(true);
64
+ }));
@@ -0,0 +1,10 @@
1
+ export declare function getTokenPattern(prefixWithUnderscore: string): RegExp;
2
+ export interface TokenContents {
3
+ prefixWithoutUnderscore: string;
4
+ secret: Uint8Array;
5
+ version: number;
6
+ suppliedChecksum: Uint8Array;
7
+ actualChecksum: Uint8Array;
8
+ isSyntacticallyValid: boolean;
9
+ }
10
+ export declare function parseTokenData(token: string): TokenContents | undefined;
package/built/parse.js ADDED
@@ -0,0 +1,52 @@
1
+ import crc32 from "./crc32.js";
2
+ import { alphabet, base62, version } from "./constants.js";
3
+ export function getTokenPattern(prefixWithUnderscore) {
4
+ return new RegExp(`^${prefixWithUnderscore}[${alphabet}]+$`);
5
+ }
6
+ export function parseTokenData(token) {
7
+ if (!token.includes("_")) {
8
+ return undefined;
9
+ }
10
+ const splitData = token.split("_");
11
+ if (splitData.length !== 2) {
12
+ return undefined;
13
+ }
14
+ const [prefix, encodedTokenData] = splitData;
15
+ const secretWithVersionAndChecksum = base62.decode(encodedTokenData);
16
+ if (!secretWithVersionAndChecksum ||
17
+ secretWithVersionAndChecksum.length <= 4) {
18
+ return undefined;
19
+ }
20
+ const suppliedChecksum = secretWithVersionAndChecksum.slice(secretWithVersionAndChecksum.length - 4);
21
+ if (suppliedChecksum.length !== 4) {
22
+ return undefined;
23
+ }
24
+ const secretAndVersionBuffer = secretWithVersionAndChecksum.slice(0, secretWithVersionAndChecksum.length - 4);
25
+ const actualChecksum = crc32(secretAndVersionBuffer);
26
+ const isSyntacticallyValid = secretAndVersionBuffer.length > 0 &&
27
+ buffersEqual(suppliedChecksum, actualChecksum);
28
+ const suppliedVersion = secretAndVersionBuffer[secretAndVersionBuffer.length - 1];
29
+ if (suppliedVersion !== version) {
30
+ return undefined;
31
+ }
32
+ const secretPayload = secretAndVersionBuffer.slice(0, secretAndVersionBuffer.length - 1);
33
+ return {
34
+ version: suppliedVersion,
35
+ prefixWithoutUnderscore: prefix,
36
+ secret: secretPayload,
37
+ suppliedChecksum,
38
+ actualChecksum,
39
+ isSyntacticallyValid,
40
+ };
41
+ }
42
+ function buffersEqual(a, b) {
43
+ if (a.byteLength !== b.byteLength) {
44
+ return false;
45
+ }
46
+ for (let i = 0; i < a.byteLength; ++i) {
47
+ if (a[i] !== b[i]) {
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { test, expect } from "vitest";
2
+ import { parseTokenData } from "./parse.js";
3
+ test("Parse token contents", () => {
4
+ let contents;
5
+ contents = parseTokenData("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHd");
6
+ expect(contents).toBeUndefined();
7
+ contents = parseTokenData("test_");
8
+ expect(contents).toBeUndefined();
9
+ contents = parseTokenData("test_abc_def");
10
+ expect(contents).toBeUndefined();
11
+ contents = parseTokenData("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHdKNhiyw");
12
+ expect(contents).toEqual({
13
+ prefixWithoutUnderscore: "test",
14
+ actualChecksum: new Uint8Array([94, 199, 163, 74]),
15
+ isSyntacticallyValid: true,
16
+ secret: new Uint8Array([
17
+ 100, 166, 3, 29, 89, 224, 104, 216, 82, 241, 34, 139, 193, 245, 62, 254,
18
+ 71, 254, 26, 57, 131, 99, 11, 222, 170, 63, 150, 82, 53, 165,
19
+ ]),
20
+ suppliedChecksum: new Uint8Array([94, 199, 163, 74]),
21
+ version: 0,
22
+ });
23
+ contents = parseTokenData("randomToken_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHdKNhiyw");
24
+ expect(contents).toEqual({
25
+ prefixWithoutUnderscore: "randomToken",
26
+ actualChecksum: new Uint8Array([94, 199, 163, 74]),
27
+ isSyntacticallyValid: true,
28
+ secret: new Uint8Array([
29
+ 100, 166, 3, 29, 89, 224, 104, 216, 82, 241, 34, 139, 193, 245, 62, 254,
30
+ 71, 254, 26, 57, 131, 99, 11, 222, 170, 63, 150, 82, 53, 165,
31
+ ]),
32
+ suppliedChecksum: new Uint8Array([94, 199, 163, 74]),
33
+ version: 0,
34
+ });
35
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "cybertoken",
3
+ "version": "1.0.1",
4
+ "description": "A token format for APIs inspired by the GitHub's API token format.",
5
+ "author": "Niklas Mollenhauer",
6
+ "license": "ISC",
7
+ "type": "module",
8
+ "main": "built/index.js",
9
+ "bin": "built/cli.js",
10
+ "scripts": {
11
+ "compile": "tsc",
12
+ "clean": "rimraf built",
13
+ "docs": "typedoc",
14
+ "test": "tsc --noEmit && vitest run",
15
+ "test:coverage": "vitest run --coverage",
16
+ "test:watch": "vitest watch --coverage",
17
+ "prepare": "npm run clean && npm run compile"
18
+ },
19
+ "keywords": [
20
+ "token",
21
+ "api",
22
+ "format",
23
+ "secret",
24
+ "generator"
25
+ ],
26
+ "dependencies": {
27
+ "base-x": "^4.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@vitest/coverage-c8": "^0.26.3",
31
+ "rimraf": "^3.0.2",
32
+ "typedoc": "^0.23.23",
33
+ "vitest": "^0.26.3"
34
+ }
35
+ }