1import { GLOBAL_DEFAULT_TTL, parseManifest, parseTarget } from "../../../shared/mod.ts";2import type { Manifest, Ttl, TtlSource } from "../../../shared/mod.ts";3import { resolveTarget, type SiteRow } from "../db.ts";4import type { Env } from "../env.ts";5import { hashPassword } from "../password.ts";6import { ApiError, clientInput } from "./errors.ts";78export const HEX64 = /^[0-9a-f]{64}$/;9export function ttlToExpiry(10 ttl: Ttl,11 now = Math.floor(Date.now() / 1000),12): number | null {13 if (ttl === "forever") return null;14 if (!Number.isFinite(ttl) || ttl <= 0 || ttl > 3650) {15 throw new ApiError(400, 'ttl must be 1-3650 days or "forever"');16 }17 return now + Math.round(ttl * 86400);18}1920export function storedDefaultTtl(value: number | null): Ttl | null {21 return value === null ? null : value === 0 ? "forever" : value;22}2324export interface ResolvedTtl {25 expiresAt: number | null;26 ttl: Ttl;27 ttlSource: TtlSource;28}2930/** Apply explicit → existing-site → vault → global retention precedence. */31export function resolveCommitTtl(32 explicit: Ttl | undefined,33 existingExpiresAt: number | null | undefined,34 vaultDefault: number | null,35 now = Math.floor(Date.now() / 1000),36): ResolvedTtl {37 if (explicit !== undefined) {38 return {39 expiresAt: ttlToExpiry(explicit, now),40 ttl: explicit,41 ttlSource: "explicit",42 };43 }44 if (existingExpiresAt !== undefined) {45 return {46 expiresAt: existingExpiresAt,47 ttl: existingExpiresAt === null48 ? "forever"49 : Math.max(0, Math.ceil((existingExpiresAt - now) / 86400)),50 ttlSource: "existing-site",51 };52 }53 const inherited = storedDefaultTtl(vaultDefault);54 if (inherited !== null) {55 return {56 expiresAt: ttlToExpiry(inherited, now),57 ttl: inherited,58 ttlSource: "vault",59 };60 }61 return {62 expiresAt: ttlToExpiry(GLOBAL_DEFAULT_TTL, now),63 ttl: GLOBAL_DEFAULT_TTL,64 ttlSource: "global",65 };66}6768export async function passwordHashFor(69 password: unknown,70): Promise<string | null | undefined> {71 if (password === undefined || password === null) return password;72 if (73 typeof password !== "string" || password.length < 4 || password.length > 25674 ) {75 throw new ApiError(400, "password must be 4-256 characters");76 }77 return await hashPassword(password);78}7980export async function resolvePathTarget(81 env: Env,82 segment: string,83): Promise<SiteRow | null> {84 const parsed = await clientInput(() => parseTarget(decodeURIComponent(segment)));85 if (parsed.kind === "address") {86 return await resolveTarget(env, { address: parsed.address });87 }88 if (parsed.kind === "vaultAlias") {89 return await resolveTarget(env, {90 vault: parsed.vault,91 alias: parsed.alias,92 });93 }94 return null;95}9697export async function sourceManifest(98 env: Env,99 site: SiteRow,100): Promise<Manifest> {101 if (102 site.expires_at !== null && site.expires_at < Math.floor(Date.now() / 1000)103 ) {104 throw new ApiError(410, "site expired");105 }106 const object = await env.CONTENT.get(`manifest/${site.current_manifest}`);107 if (!object) throw new ApiError(404, "source manifest not found");108 return parseManifest(new Uint8Array(await object.arrayBuffer()));109}