Initial commit

This commit is contained in:
Zoe
2026-08-07 22:13:25 -05:00
commit 8006fd67f3
40 changed files with 11017 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
import {
CHUNK_SIZE,
EXPIRY_VALUES,
GCM_TAG_BYTES,
MAX_CHUNK_BODY,
MAX_MANIFEST_BODY,
MAX_MANIFEST_PLAINTEXT,
PROTOCOL_VERSION,
READ_VALUES,
LEASE_IDLE_MS,
UPLOAD_RESERVATION_MS,
type ExpiryChoice,
type ReadChoice,
type ReserveRequest,
} from "../types/drop";
export const ROOM_ID_PATTERN = /^[23456789ABCDEFGHJKLMNPQRSTUVWXYZ]{32}$/u;
export const B64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
const MAX_CHUNKS = 0xfffffffe;
export const EXPIRY_MS: Record<ExpiryChoice, number> = {
"1h": 3600000,
"2h": 7200000,
"6h": 21600000,
"12h": 43200000,
"24h": 86400000,
"7d": 604800000,
};
export function isExpiry(value: unknown): value is ExpiryChoice {
return (
typeof value === "string" &&
(EXPIRY_VALUES as readonly string[]).includes(value)
);
}
export function isReadLimit(value: unknown): value is ReadChoice {
return (
value === null ||
(typeof value === "number" && READ_VALUES.includes(value as never))
);
}
export function isBase64Url(value: unknown, bytes?: number): value is string {
return (
typeof value === "string" &&
value.length > 0 &&
value.length <= 512 &&
B64URL_PATTERN.test(value) &&
(!bytes ||
Math.ceil((value.length * 6) / 8) === bytes ||
value.length === Math.ceil((bytes * 8) / 6))
);
}
export function decodeBase64Url(value: string): Uint8Array {
if (!B64URL_PATTERN.test(value) || value.length % 4 === 1)
throw new Error("Invalid base64url");
const normalized = value
.replaceAll("-", "+")
.replaceAll("_", "/")
.padEnd(Math.ceil(value.length / 4) * 4, "=");
const binary = atob(normalized);
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
}
export function validateRoomId(id: unknown): asserts id is string {
if (typeof id !== "string" || !ROOM_ID_PATTERN.test(id))
throw new Error("Invalid drop id");
}
export function validateReservation(input: unknown): ReserveRequest {
if (!input || typeof input !== "object" || Array.isArray(input))
throw new Error("Invalid reservation");
const value = input as Record<string, unknown>;
const allowed = [
"version",
"chunkSize",
"chunkCount",
"noncePrefix",
"expiresIn",
"maxReads",
];
if (Object.keys(value).some((key) => !allowed.includes(key)))
throw new Error("Unknown reservation field");
const chunkCount = value.chunkCount;
if (
value.version !== PROTOCOL_VERSION ||
value.chunkSize !== CHUNK_SIZE ||
!Number.isSafeInteger(chunkCount) ||
(chunkCount as number) < 1 ||
(chunkCount as number) > MAX_CHUNKS
)
throw new Error("Invalid chunk geometry");
if (
!isExpiry(value.expiresIn) ||
!isReadLimit(value.maxReads) ||
!isBase64Url(value.noncePrefix, 8)
)
throw new Error("Invalid reservation policy");
if (decodeBase64Url(value.noncePrefix as string).byteLength !== 8)
throw new Error("Invalid nonce prefix");
return value as unknown as ReserveRequest;
}
export function validateChunkIndex(index: unknown, count: number): number {
const parsed = typeof index === "number" ? index : Number(index);
if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed >= count)
throw new Error("Invalid chunk index");
return parsed;
}
export function validateChunkBodyLength(length: number, final: boolean): void {
if (
!Number.isSafeInteger(length) ||
length < (final ? GCM_TAG_BYTES : MAX_CHUNK_BODY) ||
length > MAX_CHUNK_BODY
)
throw new Error("Invalid chunk body length");
}
export function validateManifestBodyLength(length: number): void {
if (
!Number.isSafeInteger(length) ||
length <= GCM_TAG_BYTES ||
length > MAX_MANIFEST_BODY
)
throw new Error("Invalid manifest body length");
}
export function expiresInMs(value: ExpiryChoice): number {
return EXPIRY_MS[value];
}