Skip to content
File

Blob: src/worker/services/id-service.ts

typescript113 lines
1import { encodeBase64Url } from "@/worker/services/crypto";
2 
3const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
4const BASE62_WIDTH = 22;
5const BASE62_LOOKUP = new Map(BASE62_ALPHABET.split("").map((character, index) => [character, BigInt(index)]));
6 
7export type DurableEntityPrefix = "usr" | "prj" | "run" | "whk";
8 
9const bytesToBigInt = (bytes: Uint8Array): bigint => {
10 let value = 0n;
11 for (const byte of bytes) {
12 value = (value << 8n) | BigInt(byte);
13 }
14 return value;
15};
16 
17const toBase62 = (value: bigint): string => {
18 if (value === 0n) {
19 return BASE62_ALPHABET[0].repeat(BASE62_WIDTH);
20 }
21 
22 let remainderValue = value;
23 let output = "";
24 
25 while (remainderValue > 0n) {
26 const remainder = Number(remainderValue % 62n);
27 output = `${BASE62_ALPHABET[remainder]}${output}`;
28 remainderValue /= 62n;
29 }
30 
31 return output.padStart(BASE62_WIDTH, BASE62_ALPHABET[0]);
32};
33 
34const fromBase62 = (value: string): bigint => {
35 let output = 0n;
36 
37 for (const character of value) {
38 const digit = BASE62_LOOKUP.get(character);
39 if (digit === undefined) {
40 throw new Error("Invalid base62 value.");
41 }
42 
43 output = output * 62n + digit;
44 }
45 
46 return output;
47};
48 
49const createUuidV7Bytes = (timestampMs: number): Uint8Array => {
50 const bytes = new Uint8Array(16);
51 const randomness = crypto.getRandomValues(new Uint8Array(10));
52 let remainingTimestamp = BigInt(timestampMs);
53 
54 for (let index = 5; index >= 0; index -= 1) {
55 bytes[index] = Number(remainingTimestamp & 0xffn);
56 remainingTimestamp >>= 8n;
57 }
58 
59 bytes[6] = 0x70 | (randomness[0] & 0x0f);
60 bytes[7] = randomness[1];
61 bytes[8] = 0x80 | (randomness[2] & 0x3f);
62 bytes.set(randomness.slice(3), 9);
63 
64 return bytes;
65};
66 
67export const generateDurableEntityId = (prefix: DurableEntityPrefix, timestampMs = Date.now()): string => {
68 const suffix = toBase62(bytesToBigInt(createUuidV7Bytes(timestampMs)));
69 return `${prefix}_${suffix}`;
70};
71 
72export const generateOpaqueToken = (byteLength: number): string => {
73 const bytes = crypto.getRandomValues(new Uint8Array(byteLength));
74 return encodeBase64Url(bytes);
75};
76 
77export const hashSha256 = async (value: string): Promise<Uint8Array> => {
78 const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
79 return new Uint8Array(digest);
80};
81 
82export const extractTimestampFromDurableEntityId = (id: string): number | null => {
83 const separatorIndex = id.indexOf("_");
84 if (separatorIndex === -1) {
85 return null;
86 }
87 
88 const suffix = id.slice(separatorIndex + 1);
89 if (suffix.length !== BASE62_WIDTH) {
90 return null;
91 }
92 
93 try {
94 const value = fromBase62(suffix);
95 const bytes = new Uint8Array(16);
96 let remainder = value;
97 
98 for (let index = 15; index >= 0; index -= 1) {
99 bytes[index] = Number(remainder & 0xffn);
100 remainder >>= 8n;
101 }
102 
103 let timestamp = 0n;
104 for (let index = 0; index < 6; index += 1) {
105 timestamp = (timestamp << 8n) | BigInt(bytes[index]);
106 }
107 
108 return Number(timestamp);
109 } catch {
110 return null;
111 }
112};