draftlink 0.1.0

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.
package/src/util.ts ADDED
@@ -0,0 +1,119 @@
1
+ const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
2
+
3
+ export const enc = new TextEncoder();
4
+ export const dec = new TextDecoder();
5
+
6
+ export function randomId(len = 12): string {
7
+ const bytes = crypto.getRandomValues(new Uint8Array(len));
8
+ let out = "";
9
+ for (let i = 0; i < len; i++) out += CHARS[bytes[i] % CHARS.length];
10
+ return out;
11
+ }
12
+
13
+ export function randomHex(bytes = 16): string {
14
+ return [...crypto.getRandomValues(new Uint8Array(bytes))]
15
+ .map((b) => b.toString(16).padStart(2, "0"))
16
+ .join("");
17
+ }
18
+
19
+ export function b64urlEncodeBytes(bytes: Uint8Array): string {
20
+ let s = "";
21
+ for (const b of bytes) s += String.fromCharCode(b);
22
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
23
+ }
24
+
25
+ export function b64urlEncode(s: string): string {
26
+ return b64urlEncodeBytes(enc.encode(s));
27
+ }
28
+
29
+ export function b64urlDecode(s: string): string {
30
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/");
31
+ return dec.decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)));
32
+ }
33
+
34
+ export async function hmacSign(secret: string, msg: string): Promise<string> {
35
+ const key = await crypto.subtle.importKey(
36
+ "raw",
37
+ enc.encode(secret),
38
+ { name: "HMAC", hash: "SHA-256" },
39
+ false,
40
+ ["sign"]
41
+ );
42
+ const sig = await crypto.subtle.sign("HMAC", key, enc.encode(msg));
43
+ return b64urlEncodeBytes(new Uint8Array(sig));
44
+ }
45
+
46
+ export async function sha256Hex(input: string): Promise<string> {
47
+ const digest = await crypto.subtle.digest("SHA-256", enc.encode(input));
48
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
49
+ }
50
+
51
+ export function timingSafeEqual(a: string, b: string): boolean {
52
+ if (a.length !== b.length) return false;
53
+ let diff = 0;
54
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
55
+ return diff === 0;
56
+ }
57
+
58
+ export interface SignedPayload {
59
+ [k: string]: unknown;
60
+ }
61
+
62
+ export async function signPayload(secret: string, payload: SignedPayload): Promise<string> {
63
+ const body = b64urlEncode(JSON.stringify(payload));
64
+ return `${body}.${await hmacSign(secret, body)}`;
65
+ }
66
+
67
+ export async function verifyPayload(secret: string, token: string | undefined): Promise<SignedPayload | null> {
68
+ if (!token) return null;
69
+ const dot = token.lastIndexOf(".");
70
+ if (dot < 0) return null;
71
+ const body = token.slice(0, dot);
72
+ const sig = token.slice(dot + 1);
73
+ if (!timingSafeEqual(sig, await hmacSign(secret, body))) return null;
74
+ try {
75
+ const payload = JSON.parse(b64urlDecode(body)) as SignedPayload;
76
+ if (typeof payload.exp !== "number" || payload.exp < Date.now()) return null;
77
+ return payload;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ export function esc(s: string): string {
84
+ return s
85
+ .replace(/&/g, "&amp;")
86
+ .replace(/</g, "&lt;")
87
+ .replace(/>/g, "&gt;")
88
+ .replace(/"/g, "&quot;")
89
+ .replace(/'/g, "&#39;");
90
+ }
91
+
92
+ export function getCookie(req: Request, name: string): string | undefined {
93
+ const header = req.headers.get("Cookie");
94
+ if (!header) return undefined;
95
+ for (const part of header.split(";")) {
96
+ const idx = part.indexOf("=");
97
+ if (idx < 0) continue;
98
+ if (part.slice(0, idx).trim() === name) return part.slice(idx + 1).trim();
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ export function cookieHeader(name: string, value: string, maxAge: number, secure: boolean): [string, string] {
104
+ const flags = `Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}`;
105
+ return ["Set-Cookie", `${name}=${value}; ${secure ? "Secure; " : ""}${flags}`];
106
+ }
107
+
108
+ const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
109
+
110
+ export function monthLabel(ms: number): string {
111
+ const d = new Date(ms);
112
+ return `${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
113
+ }
114
+
115
+ export function dateLabel(ms: number): string {
116
+ const d = new Date(ms);
117
+ const p = (n: number) => String(n).padStart(2, "0");
118
+ return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()].slice(0, 3)} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())} UTC`;
119
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers/types"],
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src", "tests", "vitest.config.ts"]
15
+ }
package/wrangler.jsonc ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "draftlink",
3
+ "main": "src/index.ts",
4
+ "compatibility_date": "2026-08-01",
5
+ "assets": {
6
+ "directory": "assets"
7
+ },
8
+ "d1_databases": [
9
+ {
10
+ "binding": "DB",
11
+ "database_name": "draftlink",
12
+ "migrations_dir": "migrations"
13
+ }
14
+ ]
15
+ }