Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

80 lines · 3.3 KBRaw History
1export const now = (): string => new Date().toISOString();
2
3export function newId(): string {
4 return crypto.randomUUID().replace(/-/g, "");
5}
6
7export function randomToken(bytes = 24): string {
8 const b = crypto.getRandomValues(new Uint8Array(bytes));
9 return b64url(b);
10}
11
12export function b64url(b: Uint8Array): string {
13 let s = "";
14 for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]!);
15 return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
16}
17
18export function fromB64url(s: string): Uint8Array {
19 const pad = s.length % 4 ? "=".repeat(4 - (s.length % 4)) : "";
20 const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
21 const out = new Uint8Array(bin.length);
22 for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
23 return out;
24}
25
26export function b64(b: Uint8Array): string {
27 let s = "";
28 for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]!);
29 return btoa(s);
30}
31
32export function fromB64(s: string): Uint8Array {
33 const bin = atob(s);
34 const out = new Uint8Array(bin.length);
35 for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
36 return out;
37}
38
39export async function sha256(s: string): Promise<string> {
40 const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
41 return [...new Uint8Array(d)].map((x) => x.toString(16).padStart(2, "0")).join("");
42}
43
44export async function hmacSha256Hex(secret: string, body: string | Uint8Array): Promise<string> {
45 const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
46 const data = typeof body === "string" ? new TextEncoder().encode(body) : body;
47 const sig = await crypto.subtle.sign("HMAC", key, data as BufferSource);
48 return [...new Uint8Array(sig)].map((x) => x.toString(16).padStart(2, "0")).join("");
49}
50
51/** Constant-time string comparison. */
52export function safeEqual(a: string, b: string): boolean {
53 if (a.length !== b.length) return false;
54 let r = 0;
55 for (let i = 0; i < a.length; i++) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
56 return r === 0;
57}
58
59export class HttpError extends Error {
60 /** The message without the status prefix. */
61 readonly detail: string;
62 constructor(public status: number, message: string, public extra?: Record<string, unknown>) {
63 // The status is part of the message so it survives Durable Object RPC boundaries.
64 super(`${status}: ${message}`);
65 this.detail = message;
66 }
67}
68
69/** Top-level paths the app serves; no account may use them as its login. */
70export const RESERVED_LOGINS = new Set(["api", "mcp", "runner", "settings", "new", "notifications", "invite", "request-access", "healthz", "login", "logout", "admin", "explore", "favicon.ico", "robots.txt", "about", "docs", "static", "assets", "www", "git", "diffinery", "auth", "signin", "signup", "security", "-"]);
71
72export const isValidLogin = (s: string): boolean => isValidName(s) && !s.includes(".") && !RESERVED_LOGINS.has(s.toLowerCase());
73
74export const isValidName = (s: string): boolean => /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(s) && !s.endsWith(".git") && s !== "." && s !== "..";
75
76export function isValidRefName(ref: string): boolean {
77 if (!ref.startsWith("refs/")) return false;
78 if (/(^|\/)\.|\.\.|[\x00-\x20~^:?*[\\\x7f]|@\{|\/\/|\/$|\.lock(\/|$)|\.$/.test(ref)) return false;
79 return true;
80}